From e6b1f1f4082573239927f8e95aa7209cf8a117c6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 16 Apr 2026 23:52:44 +0300 Subject: [PATCH 001/326] remove TODO.md --- TODO.md | 103 -------------------------------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 8001fd8..0000000 --- a/TODO.md +++ /dev/null @@ -1,103 +0,0 @@ -# TODO - -## 1. Индикация активного пира на всех страницах -**Проблема:** Сейчас активный MAC виден только в `PeerExplorerPage` (звёздочка). На `MavLinkPage`, `RawControlPage`, `RootPage` пользователь не знает, кому отправляет команды. -**Решение:** Добавить в `DisplayManager` статусную строку (например, внизу или вверху) с отображением активного пира. Можно последние 4 байта MAC или имя, если добавить заметки. -**Где:** `DisplayManager::onRender()` запрашивает `control.activeMac()` и выводит справа. - -## 2. Подтверждение опасных действий (Reset, Delete peer) -**Проблема:** Кнопка Reset в `ConfigPage` сразу сбрасывает конфиг и перезагружает устройство. Нет диалога подтверждения. -**Решение:** Реализовать двухэтапное нажатие: первый клик → меняем текст кнопки на "Press again to reset" и запускаем таймер 3 секунды; второй клик в течение таймаута → `storage.reset()`. Или использовать модальное окно, если UI поддерживает. -**Где:** В `ConfigPage`, обработчик `_reset_storage.callback()`. - -## 3. Таймер бездействия и light sleep (энергосбережение) -**Проблема:** ESP32 никогда не спит, батарея садится за несколько часов. -**Решение:** Добавить `IdleSleepManager`, который сбрасывает счётчик при любом движении стиков (через `InputHandler::controllerValues()`) или нажатии кнопок. При бездействии > N минут (например, 10) — `esp_light_sleep_start()`. При пробуждении переинициализировать ESP-NOW и восстановить соединение к активному пиру. -**Где:** Новый модуль `src/djc/IdleSleepManager.hpp`, вызывать его `poll()` в `loop()`. - -## 4. Визуальный фидбек калибровки джойстиков -**Проблема:** При первом запуске или после сброса вызывается `periphery.tune()`, которая блокирует всё на ~100 мс, а пользователь видит застывший экран. -**Решение:** Показывать на дисплее сообщение "Calibrating... move joysticks", возможно прогресс-символами. После калибровки — автоматически сохранить конфиг. -**Где:** В `main.cpp` перед вызовом `tune()` добавить принудительный рендеринг через `DisplayManager` (минуя UI) или отправить событие UI. - -## 5. Полноценный MAVLink: телеметрия и управление -**Проблема:** `MavLinkPage` обрабатывает только `SCALED_IMU` и `SERIAL_CONTROL`. Нет отображения высоты, напряжения, спутников, режима полёта. -**Решение:** Добавить парсинг: -- `GLOBAL_POSITION_INT` → относительная высота, lat/lon. -- `SYS_STATUS` → напряжение батареи, ток, заряд. -- `HEARTBEAT` → режим автопилота (стабилизация, альтхолд, loiter и т.д.). -Обновлять соответствующие виджеты на странице. -**Где:** `MavLinkPage::onMavLinkMessage()`. - -## 6. CLI (Command Line Interface) через Serial -**Проблема:** Отладка и настройка без экрана невозможны. Нужен простой shell. -**Решение:** Интегрировать легковесную библиотеку (например, ESP-Shell) и добавить команды: -- `peers` — список обнаруженных MAC из `PeerExplorer` (последние активные). -- `connect ` — подключиться к пиру. -- `disconnect` — отключиться. -- `mode [raw|mavlink]` — переключить текущий режим. -- `reset` — сбросить конфиг. -- `save` — сохранить конфиг. -- `info` — статус (активный MAC, режим, напряжение батареи). -**Где:** Новый модуль `src/djc/CLI.hpp`, инициализация в `setup()`. - -## 7. Заметки о пирах (имена для MAC) -**Проблема:** Пользователь видит только MAC-адреса, что неудобно для идентификации (дрон, робот, второй дрон). -**Решение:** Добавить в `Config` структуру `PeerNote { Mac mac; char name[16]; }` и `Box`. На странице "Peer Notes" отображать список с именами, возможность добавлять/редактировать/удалять. В `PeerExplorerPage` для известных MAC подставлять имя. -**Где:** `src/djc/Config.hpp`, новая страница `PeerNotesPage`, изменения в `PeerExplorerPage` для отображения имени. - -## 8. Автоматическое добавление устройств по Heartbeat -**Проблема:** Сейчас пользователь должен кликнуть на устройство в `PeerExplorerPage`, чтобы подключиться. Хорошо бы при получении heartbeat от нового устройства показывать уведомление с предложением добавить в избранное/заметки. -**Решение:** В `Control` при получении пакета от неизвестного MAC анализировать, является ли он heartbeat (по сигнатуре). Если да — сохранять MAC во временный буфер и через UI показывать всплывающее сообщение (например, через `ui.addEvent()` и специальный виджет уведомлений). -**Где:** `Control::onReceiveFromUnknown`, интеграция с `DisplayManager` для отображения уведомлений. - -## 9. Выбор типа дисплея через флаг компиляции -**Проблема:** Сейчас жёстко привязан ST7735. Хочется поддержать SSD1306. -**Решение:** В `platformio.ini` добавить `-DDISPLAY_TYPE=ST7735` или `SSD1306`. В `prelude.hpp` условно подключать нужный драйвер и определять пины. -**Где:** `prelude.hpp`, `Periphery.hpp` (пины DC/RESET). - -## 10. Документация по питанию (сейчас только USB, нет шилда) -**Проблема:** В проекте отсутствует описание автономного питания, пользователи вынуждены питать плату только через USB. -**Решение:** Дополнить README разделом "Power Supply" с описанием текущего способа (USB) и планов по аккумулятору. Указать требования к току, напряжению. -**Где:** `README.md`, раздел Hardware или новый подраздел Power. - -## 11. Автономное питание от аккумулятора 18650 (1S1P) с boost-конвертером до 5В -**Проблема:** Устройство не может работать от батареи, что ограничивает его применение в полевых условиях. -**Решение:** Спроектировать цепь питания: защита от переразряда, зарядный контроллер (TP4056 или аналоги), boost-конвертер (MT3608, SX1308 и т.п.) до 5В для подачи на devkit-плату. -**Где:** Схема в KiCad, тестовый макет, затем интеграция в шилд. - -## 12. Разработка шилда для devkit-платы (ESP32 DevKit) -**Проблема:** Подключение джойстиков, кнопок, дисплея выполнено макетными проводами — ненадёжно и неудобно. -**Решение:** Спроектировать одностороннюю печатную плату (ЛУТ, тонерная технология) для установки на ESP32 DevKit. На шилде разместить: -- разъёмы для двух HW-504, -- разъёмы для двух микропереключателей, -- посадочное место для ST7735 (или гибкий шлейф), -- цепь питания (boost + защита), -- кнопку сброса (опционально). -**Где:** Новый каталог `Hardware/Shield/` с файлами KiCad (схема, плата), Gerber-файлы, BOM. - -## 13. Принципиальная схема всего устройства в KiCad -**Проблема:** Принципиальная схема отсутствует, что затрудняет отладку, повторение проекта и модификации. -**Решение:** Создать проект KiCad, нарисовать полную принципиальную схему: ESP32 DevKit (как модуль), подключение периферии (джойстики, кнопки, дисплей), цепь питания, разъёмы. -**Где:** `Hardware/Schematic/DJC_Controller.sch`, экспорт в PDF и PNG для документации. - ---- - -## Порядок выполнения (рекомендованный) - -**Программная часть:** -1. Индикация активного пира (п.1) -2. Таймер бездействия + light sleep (п.3) -3. Подтверждение Reset (п.2) -4. Фидбек калибровки (п.4) -5. MAVLink телеметрия (п.5) -6. CLI (п.6) -7. Заметки о пирах (п.7) – опционально -8. Автоматическое добавление (п.8) -9. Выбор дисплея (п.9) - -**Аппаратная часть (параллельно или после стабилизации прошивки):** -10. Документация по питанию (п.10) – быстро -11. Принципиальная схема в KiCad (п.13) – основа для шилда -12. Автономное питание (п.11) – макет, отладка -13. Разработка шилда (п.12) – разводка, изготовление ЛУТ From 936c199da164e861a25d3c64a1adcb22eaaddc5b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 16 Apr 2026 23:55:05 +0300 Subject: [PATCH 002/326] fix: typo --- shapshot.py => snapshot.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename shapshot.py => snapshot.py (100%) diff --git a/shapshot.py b/snapshot.py similarity index 100% rename from shapshot.py rename to snapshot.py From 35fe41d7a6fe27f317be54511e7f6dc785938010 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 17 Apr 2026 10:52:03 +0300 Subject: [PATCH 003/326] fix: shapshot --- .gitignore | 2 +- snapshot.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index f3a40dd..d23e7c9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ **/.vscode !./DJC-Firmware/.vscode/extensions.json -./snapshot.txt +snapshot.txt # KiCad 10 diff --git a/snapshot.py b/snapshot.py index 7100d55..a573979 100644 --- a/snapshot.py +++ b/snapshot.py @@ -10,7 +10,7 @@ ) target_dirs = ( - "examples", "src", "test", ".github" + "DJC-Firmware/src", ".github" ) def _write(f: Path): From 5c19aaad3d539f45bff8df556fb67c7a7d23f30c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 17 Apr 2026 15:14:59 +0300 Subject: [PATCH 004/326] add: Color highlight to TextInput --- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index f6b9200..47e2766 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -23,14 +23,9 @@ struct TextInput final : UI::Widget { bool available() const noexcept { return nullptr != _text_source.data(); } void doRender(UI::RenderImpl &render) const noexcept override { - if (not available()) { - render.value(kf::memory::StringView{"not available"}); - return; - } - - const kf::memory::StringView s{_text_source.data(), _text_source.size()}; - const auto end_index = s.find('\0'); - render.value(end_index.hasValue() ? s.sub(0, end_index.value()) : s); + render.value(kf::memory::StringView{"\xFC'"}); + render.value(string()); + render.value(kf::memory::StringView{"'\x80"}); } bool onClick() noexcept override { @@ -58,6 +53,15 @@ struct TextInput final : UI::Widget { inline static auto &virtual_keyboard{input::VirtualKeyboard::instance()}; kf::memory::Slice _text_source; + + [[nodiscard]] kf::memory::StringView string() const noexcept { + if (available()) { + const kf::memory::StringView s{_text_source.data(), _text_source.size()}; + return s.sub(0, s.find('\0').valueOr(s.size())); + } else { + return kf::memory::StringView{"not available"}; + } + } }; }// namespace djc::ui::widgets \ No newline at end of file From f98c7dcdc17be4da2946fa3cebb9498b1f6cd344 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 20 Apr 2026 11:16:24 +0300 Subject: [PATCH 005/326] fix: virtual_keyboard_event_from_direction naming --- DJC-Firmware/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 200a00f..8ddfa0f 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -122,7 +122,7 @@ void setup() { E::widgetValue(+1), // Right }; - static constexpr E VirtualKeyboard_event_from_direction[4] = { + static constexpr E virtual_keyboard_event_from_direction[4] = { E::widgetValue(0),// Up E::widgetValue(1),// Down E::widgetValue(2),// Left @@ -131,7 +131,7 @@ void setup() { if (control.enabled()) { return; } - const auto table = virtual_keyboard.active() ? VirtualKeyboard_event_from_direction : navigation_event_from_direction; + const auto table = virtual_keyboard.active() ? virtual_keyboard_event_from_direction : navigation_event_from_direction; ui.addEvent(table[static_cast(direction)]); }); From 4a940b17e3e84813fde07e9835d3adcc739fa0b2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 20 Apr 2026 11:18:13 +0300 Subject: [PATCH 006/326] refactor: VirtualKeyboard: - make moveCursor.. methods public - remove move method --- .../src/djc/input/VirtualKeyboard.hpp | 43 ++++--------------- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 14 ++++-- 2 files changed, 18 insertions(+), 39 deletions(-) diff --git a/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp b/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp index 64c8cec..546fbe6 100644 --- a/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp +++ b/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp @@ -37,17 +37,10 @@ struct Key { } }; -} +}// namespace internal struct VirtualKeyboard final : kf::mixin::Singleton { - enum class Direction : kf::u8 { - Up = 0, - Down = 1, - Left = 2, - Right = 3, - }; - enum class State : kf::u8 { Normal, ShiftOnce, @@ -72,7 +65,7 @@ struct VirtualKeyboard final : kf::mixin::Singleton { {'0', ')'}, {'-', '_'}, {'=', '+'}, - {Key::Kind::Backspace, 0}, + {Key::Kind::Backspace, 0}, }}; static constexpr KeyRow<13> row_1{{ @@ -198,24 +191,13 @@ struct VirtualKeyboard final : kf::mixin::Singleton { } } - void move(Direction direction) noexcept { - switch (direction) { - case Direction::Down: - moveCursorRow(+1); - return; - - case Direction::Up: - moveCursorRow(-1); - return; - - case Direction::Left: - moveCursorCol(-1); - return; + void moveCursorRow(kf::i8 delta) noexcept { + _cursor_row = (_cursor_row + delta + rowsTotal()) % rowsTotal(); + _cursor_row_index = kf::clamp(_cursor_row_index, 0, colsTotal() - 1); + } - case Direction::Right: - moveCursorCol(+1); - return; - } + void moveCursorCol(kf::i8 delta) noexcept { + _cursor_row_index = (_cursor_row_index + delta + colsTotal()) % colsTotal(); } private: @@ -225,15 +207,6 @@ struct VirtualKeyboard final : kf::mixin::Singleton { bool _active{false}; State _state{State::Normal}; - void moveCursorRow(kf::i8 delta) noexcept { - _cursor_row = (_cursor_row + delta + rowsTotal()) % rowsTotal(); - _cursor_row_index = kf::clamp(_cursor_row_index, 0, colsTotal() - 1); - } - - void moveCursorCol(kf::i8 delta) noexcept { - _cursor_row_index = (_cursor_row_index + delta + colsTotal()) % colsTotal(); - } - static State evolvedState(State state) noexcept { switch (state) { case State::Normal: diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index 47e2766..165b941 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -41,12 +41,18 @@ struct TextInput final : UI::Widget { } bool onEventValue(UI::Event::Value event_value) noexcept { - if (virtual_keyboard.active()) { - virtual_keyboard.move(static_cast(event_value)); - return true; + if (not virtual_keyboard.active()) { + return false; + } + + switch (event_value) { + case 0: virtual_keyboard.moveCursorRow(-1); break; + case 1: virtual_keyboard.moveCursorRow(+1); break; + case 2: virtual_keyboard.moveCursorCol(-1); break; + case 3: virtual_keyboard.moveCursorCol(+1); break; } - return false; + return true; } private: From f17be64e4dbeea7092bb0efcd26eb07aa78c4856 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 20 Apr 2026 11:25:28 +0300 Subject: [PATCH 007/326] refactor: introduce local for control input --- DJC-Firmware/src/djc/Control.hpp | 2 +- DJC-Firmware/src/main.cpp | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/Control.hpp index 41ed6c3..af6a19d 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/Control.hpp @@ -66,7 +66,7 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable Unit left_x, left_y, right_x, right_y; - static constexpr Unit fromReal(kf::f32 value) noexcept { return static_cast(value * scale); } + static constexpr Unit fromNormalized(kf::f32 value) noexcept { return static_cast(value * scale); } }; explicit Control(const Config &config) noexcept : kf::mixin::Configurable{config} {} diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 8ddfa0f..faa07a9 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -158,12 +158,16 @@ void loop() { if (control.enabled()) { using I = djc::Control::Input; - control.input({ - .left_x = I::fromReal(periphery.left_joystick.axis_x.read()), - .left_y = I::fromReal(periphery.left_joystick.axis_y.read()), - .right_x = I::fromReal(periphery.right_joystick.axis_x.read()), - .right_y = I::fromReal(periphery.right_joystick.axis_y.read()), - }); + const I control_input{ + .left_x = I::fromNormalized(periphery.left_joystick.axis_x.read()), + .left_y = I::fromNormalized(periphery.left_joystick.axis_y.read()), + .right_x = I::fromNormalized(periphery.right_joystick.axis_x.read()), + .right_y = I::fromNormalized(periphery.right_joystick.axis_y.read()), + }; + + // todo debug log + + control.input(control_input); } control.poll(now); ui.poll(now); From 32d3df30d3428acdb48fb248f1a46f4f17cd411e Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 20 Apr 2026 11:51:22 +0300 Subject: [PATCH 008/326] enchance: main --- DJC-Firmware/src/main.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index faa07a9..72956dc 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -88,12 +88,10 @@ void setup() { logger.debug("Tunning axes.."); periphery.tune(storage.config().periphery); storage.modified(true); - } else { - logger.debug("Axes already tuned"); } - (void) control.init();// TODO: implement halt on error? display_manager.init(); + (void) control.init();// TODO: implement halt on error? { using E = djc::ui::UI::Event; From 7b0265315c145eaaa933990ecd559d742c109654 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 20 Apr 2026 11:51:42 +0300 Subject: [PATCH 009/326] fix: Control --- DJC-Firmware/src/djc/Control.hpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/Control.hpp index af6a19d..69e764b 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/Control.hpp @@ -53,8 +53,6 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable using Config = internal::ControlConfig; using Mode = internal::ControlMode; - using LogString = kf::memory::ArrayString<64>; - using RawMessageCallback = kf::Function)>; using MavLinkMessageCallback = kf::Function; using ReceiveFromUnknownCallback = EspNow::ReceiveFromUnknownHandler; @@ -69,6 +67,8 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable static constexpr Unit fromNormalized(kf::f32 value) noexcept { return static_cast(value * scale); } }; + static constexpr kf::u8 mavlink_system_id{127}, mavlink_target_id{1}; + explicit Control(const Config &config) noexcept : kf::mixin::Configurable{config} {} // properties @@ -103,6 +103,8 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable } } + // methods + void connect(const EspNow::Mac &mac) noexcept { if (connected()) { if (_active_peer.value().mac() == mac) { @@ -144,7 +146,7 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable logger.info("Disconnected: OK"); } - void sendMavLinkMessage(mavlink_message_t *message) noexcept { + void sendMavLinkMessage(const mavlink_message_t *message) noexcept { if (connected()) { sendMavLinkMessage(_active_peer.value(), message); } @@ -157,6 +159,8 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable } private: + using LogString = kf::memory::ArrayString<64>; + static constexpr auto logger{kf::Logger::create("Control")}; RawMessageCallback _raw_message_callback{}; @@ -167,7 +171,7 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable kf::math::Timer _poll_timer{this->config().poll_period}; kf::math::Timer _heartbear_timer{this->config().heartbeat_period}; - kf::math::Timer _receice_disconnect_timer{this->config().receive_timeout}; + kf::math::Timer _receive_disconnect_timer{this->config().receive_timeout}; Input _input{}; Mode _mode{this->config().init_mode}; @@ -249,7 +253,7 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable void sendMavLinkControl(EspNow::Peer &peer) noexcept { mavlink_message_t message; (void) mavlink_msg_manual_control_pack( - 127, MAV_COMP_ID_PARACHUTE, &message, 1, + mavlink_system_id, MAV_COMP_ID_PARACHUTE, &message, mavlink_target_id, _input.right_y,// x: pitch (right Y) _input.right_x,// y: roll (right X) _input.left_y, // z: thrust (left Y) @@ -263,7 +267,7 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable void sendMavLinkHeartbeat(EspNow::Peer &peer) noexcept { mavlink_message_t message; (void) mavlink_msg_heartbeat_pack( - 127, // System ID + mavlink_system_id, // System ID MAV_COMP_ID_OSD,// Component ID &message, MAV_TYPE_QUADROTOR, @@ -274,7 +278,7 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable sendMavLinkMessage(peer, &message); } - void sendMavLinkMessage(EspNow::Peer &peer, mavlink_message_t *message) noexcept { + void sendMavLinkMessage(EspNow::Peer &peer, const mavlink_message_t *message) noexcept { kf::u8 buffer[MAVLINK_MAX_PACKET_LEN]; const auto len = mavlink_msg_to_send_buffer(buffer, message); (void) peer.writeBuffer(kf::memory::Slice{buffer, len}); @@ -295,35 +299,31 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable _broadcast_peer = addPeer(EspNow::Mac{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}); _mode = this->config().init_mode; + logger.debug(stringFromMode(_mode)); const auto now = millis(); _poll_timer.start(now); _heartbear_timer.start(now); - logger.debug(stringFromMode(_mode)); logger.debug("init: ok"); return true; } KF_IMPL_TIMED_POLLABLE(Control); void pollImpl(kf::math::Milliseconds now) noexcept { - if (not connected()) { return; } - if (_got_packet) { _got_packet = false; - _receice_disconnect_timer.start(now); + _receive_disconnect_timer.start(now); } - if (_receice_disconnect_timer.expired(now)) { + if (_receive_disconnect_timer.expired(now)) { logger.info("Timeout"); disconnect(); } - if (not _enabled) { return; } - - if (not _active_peer.hasValue()) { return; } + if (not connected()) { return; } - if (_poll_timer.expired(now)) { + if (_enabled and _poll_timer.expired(now)) { _poll_timer.start(now); switch (_mode) { From 6c39669917f06a654e18be7230d5cf50e4ab9b76 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 20 Apr 2026 11:57:01 +0300 Subject: [PATCH 010/326] formatting: Control.hpp --- DJC-Firmware/src/djc/Control.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/Control.hpp index 69e764b..8bfc480 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/Control.hpp @@ -69,6 +69,8 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable static constexpr kf::u8 mavlink_system_id{127}, mavlink_target_id{1}; + [[nodiscard]] static constexpr kf::memory::StringView stringFromMode(Mode mode) noexcept { return (mode == Mode::Raw) ? "Raw" : "MavLink"; } + explicit Control(const Config &config) noexcept : kf::mixin::Configurable{config} {} // properties @@ -81,8 +83,6 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable [[nodiscard]] Mode mode() const noexcept { return _mode; } - [[nodiscard]] static constexpr kf::memory::StringView stringFromMode(Mode mode) noexcept { return (mode == Mode::Raw) ? "Raw" : "MavLink"; } - void mode(Mode new_mode) noexcept { _mode = new_mode; } [[nodiscard]] const Input &input() const noexcept { return _input; } @@ -267,8 +267,8 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable void sendMavLinkHeartbeat(EspNow::Peer &peer) noexcept { mavlink_message_t message; (void) mavlink_msg_heartbeat_pack( - mavlink_system_id, // System ID - MAV_COMP_ID_OSD,// Component ID + mavlink_system_id,// System ID + MAV_COMP_ID_OSD, // Component ID &message, MAV_TYPE_QUADROTOR, MAV_AUTOPILOT_GENERIC, From 25e68150baa5f6bf5c16a5bdefa98084696b9d50 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 27 Apr 2026 13:50:31 +0300 Subject: [PATCH 011/326] refactor: rename MavLinkTelemetryPage --- .../{MavLinkPage.hpp => MavLinkTelemetryPage.hpp} | 6 +++--- DJC-Firmware/src/main.cpp | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) rename DJC-Firmware/src/djc/ui/pages/{MavLinkPage.hpp => MavLinkTelemetryPage.hpp} (94%) diff --git a/DJC-Firmware/src/djc/ui/pages/MavLinkPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp similarity index 94% rename from DJC-Firmware/src/djc/ui/pages/MavLinkPage.hpp rename to DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp index 1c4ed99..29bb7b3 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavLinkPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp @@ -17,8 +17,8 @@ namespace djc::ui::pages { /// @brief MAVLink telemetry page -struct MavLinkPage : UI::Page { - explicit MavLinkPage(UI::Page &root, Control &control) noexcept : +struct MavLinkTelemetryPage : UI::Page { + explicit MavLinkTelemetryPage(UI::Page &root, Control &control) noexcept : Page{"MAV Link"}, _control{control}, _layout{{ &root.link(), @@ -47,7 +47,7 @@ struct MavLinkPage : UI::Page { } private: - static constexpr auto logger{kf::Logger::create("MavLinkPage")}; + static constexpr auto logger{kf::Logger::create("MavLinkTelemetryPage")}; Control &_control; bool _need_update{false}; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 72956dc..80f6940 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -14,19 +14,19 @@ #include "djc/input/InputHandler.hpp" #include "djc/input/VirtualKeyboard.hpp" #include "djc/ui/pages/ConfigPage.hpp" -#include "djc/ui/pages/MavLinkPage.hpp" +#include "djc/ui/pages/MavLinkTelemetryPage.hpp" #include "djc/ui/pages/PeerExplorerPage.hpp" #include "djc/ui/pages/RawControlPage.hpp" #include "djc/ui/pages/RootPage.hpp" +// services + static auto &ui{djc::ui::UI::instance()}; static auto &storage{djc::ConfigManager::instance()}; static auto &virtual_keyboard{djc::input::VirtualKeyboard::instance()}; -// services - static djc::Periphery periphery{ storage.config().periphery, }; @@ -51,7 +51,7 @@ static djc::DisplayManager display_manager{ static djc::ui::pages::RootPage root_page{}; -static djc::ui::pages::MavLinkPage mavlink_page{ +static djc::ui::pages::MavLinkTelemetryPage mavlink_telemetry_page{ root_page, control, }; @@ -134,7 +134,7 @@ void setup() { }); // apply page links - root_page.attach(mavlink_page); + root_page.attach(mavlink_telemetry_page); root_page.attach(raw_control_page); root_page.attach(peer_explorer_page); root_page.attach(config_page); From c04bad2e43e4c02e182d2e0e3dda9e1b64369a52 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 27 Apr 2026 14:30:44 +0300 Subject: [PATCH 012/326] fix: MavLinkTelemetryPage title --- DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp index 29bb7b3..dfea8c5 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp @@ -19,7 +19,7 @@ namespace djc::ui::pages { /// @brief MAVLink telemetry page struct MavLinkTelemetryPage : UI::Page { explicit MavLinkTelemetryPage(UI::Page &root, Control &control) noexcept : - Page{"MAV Link"}, _control{control}, + Page{"MAV Link: Telemetry"}, _control{control}, _layout{{ &root.link(), &_imu_display, From 03c8accf86e2e6c2d4b3a08f42428379abeea618 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 27 Apr 2026 14:31:30 +0300 Subject: [PATCH 013/326] fix: add 'const' : mavlink_message_t * --- DJC-Firmware/src/djc/Control.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/Control.hpp index 8bfc480..1bfe4fb 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/Control.hpp @@ -54,7 +54,7 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable using Mode = internal::ControlMode; using RawMessageCallback = kf::Function)>; - using MavLinkMessageCallback = kf::Function; + using MavLinkMessageCallback = kf::Function; using ReceiveFromUnknownCallback = EspNow::ReceiveFromUnknownHandler; struct Input { diff --git a/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp index dfea8c5..d5fd0ff 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp @@ -30,7 +30,7 @@ struct MavLinkTelemetryPage : UI::Page { void onEntry() noexcept override { _control.mode(Control::Mode::MavLink); - _control.onMavlinkMessage([this](mavlink_message_t *message) { + _control.onMavlinkMessage([this](const mavlink_message_t *message) { _need_update |= onMavLinkMessage(message); }); } @@ -62,7 +62,7 @@ struct MavLinkTelemetryPage : UI::Page { kf::memory::Array _layout; - [[nodiscard]] bool onMavLinkMessage(mavlink_message_t *message) noexcept { + [[nodiscard]] bool onMavLinkMessage(const mavlink_message_t *message) noexcept { switch (message->msgid) { case MAVLINK_MSG_ID_ATTITUDE_QUATERNION: { mavlink_attitude_quaternion_t attitude_quaternion; From 36bf04dbc32d4d754c2251a6be66704638ac8368 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 15:36:57 +0300 Subject: [PATCH 014/326] Feature: Abstract Transport Interface closes #3 --- DJC-Firmware/src/djc/transport/Kind.hpp | 15 +++ .../src/djc/transport/PeerAddress.hpp | 55 +++++++++++ DJC-Firmware/src/djc/transport/Transport.hpp | 93 +++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 DJC-Firmware/src/djc/transport/Kind.hpp create mode 100644 DJC-Firmware/src/djc/transport/PeerAddress.hpp create mode 100644 DJC-Firmware/src/djc/transport/Transport.hpp diff --git a/DJC-Firmware/src/djc/transport/Kind.hpp b/DJC-Firmware/src/djc/transport/Kind.hpp new file mode 100644 index 0000000..2e9009d --- /dev/null +++ b/DJC-Firmware/src/djc/transport/Kind.hpp @@ -0,0 +1,15 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +namespace djc::transport { + +/// @brief Identifies the underlying transport technology of a peer address. +enum class Kind { + + /// @brief ESP‑NOW protocol (built‑in WiFi, peer‑to‑peer frames). + EspNow, +}; + +}// namespace djc::transport \ No newline at end of file diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp new file mode 100644 index 0000000..59fe741 --- /dev/null +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -0,0 +1,55 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "djc/transport/Kind.hpp" + +namespace djc::transport { + +/// @brief Unified address of a peer, independent of the underlying transport. +/// +/// Holds either a MAC address (ESP‑NOW). +/// The active kind is stored in a tag field; the union contains the actual address. +struct PeerAddress { + + /// @brief Construct an ESP‑NOW peer address from a MAC. + /// @param mac 6‑byte MAC address (EspNow::Mac). + explicit constexpr PeerAddress(const kf::network::EspNow::Mac &mac) noexcept : + _kind{Kind::EspNow}, _mac{mac} {} + + /// @brief Return the kind of transport this address belongs to. + [[nodiscard]] Kind kind() const noexcept { return _kind; } + + /// @brief Return the stored MAC address. + /// @note available if kind() == Kind::EspNow. + [[nodiscard]] kf::network::EspNow::Mac mac() const noexcept { return _mac; } + + /// @brief Equality comparison. + /// @note Two addresses are equal if they have the same kind and the same underlying value. + [[nodiscard]] bool operator==(const PeerAddress &other) const noexcept { + if (other.kind() != _kind) { return false; } + + switch (_kind) { + case Kind::EspNow: + return other.mac() == this->mac(); + + default: + return false; + } + } + + /// @brief Inequality comparison (delegates to operator==). + [[nodiscard]] bool operator!=(const PeerAddress &other) const noexcept { return not this->operator==(other); } + +private: + Kind _kind; + + union { + kf::network::EspNow::Mac _mac; + }; +}; + +}// namespace djc::transport \ No newline at end of file diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp new file mode 100644 index 0000000..d1ede02 --- /dev/null +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -0,0 +1,93 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "djc/transport/PeerAddress.hpp" + +namespace djc::transport { + +/// @brief Abstract transport layer for peer-to-peer communication. +/// +/// Manages the lifecycle of a connection to a single peer. Subclasses implement +/// the actual communication hardware +/// +/// @note The connection state (`_active_peer`) is managed by the base class. +struct Transport : kf::mixin::NonCopyable { + + /// @brief Callback invoked when data is received from a peer. + using ReceiveCallback = kf::Function)>; + + /// @brief Initialize the transport hardware. + /// @return true on success, false on failure. + /// @note Must be called once before any other operations. + [[nodiscard]] virtual bool init() noexcept = 0; + + /// @brief Send raw data to the currently connected peer. + /// @param buffer Raw payload. + /// @return true if the data was sent successfully, false otherwise. + /// @note Must only be called when connected. + [[nodiscard]] virtual bool send(kf::memory::Slice buffer) const noexcept = 0; + + /// @brief Register a callback for incoming data. + /// @param callback Functor invoked on each received packet. + /// @note The callback is set before a connection is established. + virtual void onReceive(ReceiveCallback &&callback) noexcept = 0; + + /// @brief Periodic housekeeping (polling, timeouts, etc.). + /// @param now Current timestamp in milliseconds. + /// @note Default implementation does nothing. + virtual void poll(kf::math::Milliseconds now) noexcept {} + + /// @brief Check whether the transport is currently connected to a peer. + /// @return true if a peer is active, false otherwise. + [[nodiscard]] bool connected() const noexcept { return _active_peer.hasValue(); } + + /// @brief Get the address of the currently connected peer. + /// @return Option containing the peer address if connected, empty Option otherwise. + [[nodiscard]] const kf::Option &activePeer() const noexcept { return _active_peer; } + + /// @brief Connect to a remote peer. + /// @param peer_address Address of the peer to connect to. + /// @return true on success, false on failure. + /// @note If already connected, the method will return false. Disconnect explicitly first. + [[nodiscard]] bool connect(const PeerAddress &address) noexcept { + if (connected()) { return false; } + if (not doConnect(address)) { return false; } + + _active_peer = address; + + return true; + } + + /// @brief Disconnect from the current peer. + /// @note Safe to call even if not connected. + void disconnect() noexcept { + doDisconnect(); + + _active_peer = {}; + } + +protected: + /// @brief Hardware‑specific connection procedure. + /// @param addr Address of the peer. + /// @return true on success, false on failure. + /// @note Implementations should handle invalid or incompatible address types. + virtual bool doConnect(const PeerAddress &address) noexcept = 0; + + /// @brief Hardware‑specific disconnection procedure. + /// @note Called even if not connected; implementations must be safe. + virtual void doDisconnect() noexcept = 0; + +private: + kf::Option _active_peer{}; +}; + +}// namespace djc::transport \ No newline at end of file From 4039c686cb06100b3e99353b350c9826399b6d09 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 15:50:51 +0300 Subject: [PATCH 015/326] remove: Transport: init, poll - not a Transport interface methods --- DJC-Firmware/src/djc/transport/Transport.hpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index d1ede02..b7037e9 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -25,11 +24,6 @@ struct Transport : kf::mixin::NonCopyable { /// @brief Callback invoked when data is received from a peer. using ReceiveCallback = kf::Function)>; - /// @brief Initialize the transport hardware. - /// @return true on success, false on failure. - /// @note Must be called once before any other operations. - [[nodiscard]] virtual bool init() noexcept = 0; - /// @brief Send raw data to the currently connected peer. /// @param buffer Raw payload. /// @return true if the data was sent successfully, false otherwise. @@ -41,11 +35,6 @@ struct Transport : kf::mixin::NonCopyable { /// @note The callback is set before a connection is established. virtual void onReceive(ReceiveCallback &&callback) noexcept = 0; - /// @brief Periodic housekeeping (polling, timeouts, etc.). - /// @param now Current timestamp in milliseconds. - /// @note Default implementation does nothing. - virtual void poll(kf::math::Milliseconds now) noexcept {} - /// @brief Check whether the transport is currently connected to a peer. /// @return true if a peer is active, false otherwise. [[nodiscard]] bool connected() const noexcept { return _active_peer.hasValue(); } From a08b25c5878e170774b71e0d936d832f71869ad3 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 15:55:39 +0300 Subject: [PATCH 016/326] misc: Transport: move virtual method up --- DJC-Firmware/src/djc/transport/Transport.hpp | 23 ++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index b7037e9..7b5e0f6 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -35,6 +35,18 @@ struct Transport : kf::mixin::NonCopyable { /// @note The callback is set before a connection is established. virtual void onReceive(ReceiveCallback &&callback) noexcept = 0; +protected: + /// @brief Hardware‑specific connection procedure. + /// @param addr Address of the peer. + /// @return true on success, false on failure. + /// @note Implementations should handle invalid or incompatible address types. + virtual bool doConnect(const PeerAddress &address) noexcept = 0; + + /// @brief Hardware‑specific disconnection procedure. + /// @note Called even if not connected; implementations must be safe. + virtual void doDisconnect() noexcept = 0; + +public: /// @brief Check whether the transport is currently connected to a peer. /// @return true if a peer is active, false otherwise. [[nodiscard]] bool connected() const noexcept { return _active_peer.hasValue(); } @@ -64,17 +76,6 @@ struct Transport : kf::mixin::NonCopyable { _active_peer = {}; } -protected: - /// @brief Hardware‑specific connection procedure. - /// @param addr Address of the peer. - /// @return true on success, false on failure. - /// @note Implementations should handle invalid or incompatible address types. - virtual bool doConnect(const PeerAddress &address) noexcept = 0; - - /// @brief Hardware‑specific disconnection procedure. - /// @note Called even if not connected; implementations must be safe. - virtual void doDisconnect() noexcept = 0; - private: kf::Option _active_peer{}; }; From 8bc73d820353a429c653c9cae6680c3b99aec7cf Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 16:15:16 +0300 Subject: [PATCH 017/326] add: Transport: _receive_callback --- DJC-Firmware/src/djc/transport/Transport.hpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index 7b5e0f6..9bed681 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -32,8 +32,7 @@ struct Transport : kf::mixin::NonCopyable { /// @brief Register a callback for incoming data. /// @param callback Functor invoked on each received packet. - /// @note The callback is set before a connection is established. - virtual void onReceive(ReceiveCallback &&callback) noexcept = 0; + virtual void onReceive(ReceiveCallback &&callback) noexcept { _receive_callback = std::move(callback); } protected: /// @brief Hardware‑specific connection procedure. @@ -76,8 +75,16 @@ struct Transport : kf::mixin::NonCopyable { _active_peer = {}; } +protected: + void invokeReceive(const PeerAddress &address, kf::memory::Slice buffer) noexcept { + if (_receive_callback) { + _receive_callback(address, buffer); + } + } + private: kf::Option _active_peer{}; + ReceiveCallback _receive_callback{}; }; }// namespace djc::transport \ No newline at end of file From 4e2563d5205d78011feac038a124f510315d7319 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 16:43:08 +0300 Subject: [PATCH 018/326] fix: remove 'const' from Transport::send --- DJC-Firmware/src/djc/transport/Transport.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index 9bed681..1a334f8 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -28,7 +28,7 @@ struct Transport : kf::mixin::NonCopyable { /// @param buffer Raw payload. /// @return true if the data was sent successfully, false otherwise. /// @note Must only be called when connected. - [[nodiscard]] virtual bool send(kf::memory::Slice buffer) const noexcept = 0; + [[nodiscard]] virtual bool send(kf::memory::Slice buffer) noexcept = 0; /// @brief Register a callback for incoming data. /// @param callback Functor invoked on each received packet. From 7330873fc2e111761d4f09606714642a11bbe265 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 16:50:56 +0300 Subject: [PATCH 019/326] add: [[nodiscard]] on Transport::doConnect --- DJC-Firmware/src/djc/transport/Transport.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index 1a334f8..9e464af 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -39,7 +39,7 @@ struct Transport : kf::mixin::NonCopyable { /// @param addr Address of the peer. /// @return true on success, false on failure. /// @note Implementations should handle invalid or incompatible address types. - virtual bool doConnect(const PeerAddress &address) noexcept = 0; + [[nodiscard]] virtual bool doConnect(const PeerAddress &address) noexcept = 0; /// @brief Hardware‑specific disconnection procedure. /// @note Called even if not connected; implementations must be safe. From 25d6989802f49a18509cacb6117492a9e1187ce1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 17:12:21 +0300 Subject: [PATCH 020/326] refactor: change Transport::invokeReceive signature: pass buffer only, peer address passed internaly --- DJC-Firmware/src/djc/transport/Transport.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index 9e464af..a4536ea 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -76,9 +76,9 @@ struct Transport : kf::mixin::NonCopyable { } protected: - void invokeReceive(const PeerAddress &address, kf::memory::Slice buffer) noexcept { - if (_receive_callback) { - _receive_callback(address, buffer); + void invokeReceive(kf::memory::Slice buffer) noexcept { + if (_active_peer.hasValue() and _receive_callback) { + _receive_callback(_active_peer.value(), buffer); } } From 857d530bcb622d903c30deac46e8c383ec945210 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 17:27:31 +0300 Subject: [PATCH 021/326] refactor: rename Transport::_active_peer_address Transport::activePeerAddress --- DJC-Firmware/src/djc/transport/Transport.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index a4536ea..f5dc514 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -48,11 +48,11 @@ struct Transport : kf::mixin::NonCopyable { public: /// @brief Check whether the transport is currently connected to a peer. /// @return true if a peer is active, false otherwise. - [[nodiscard]] bool connected() const noexcept { return _active_peer.hasValue(); } + [[nodiscard]] bool connected() const noexcept { return _active_peer_address.hasValue(); } /// @brief Get the address of the currently connected peer. /// @return Option containing the peer address if connected, empty Option otherwise. - [[nodiscard]] const kf::Option &activePeer() const noexcept { return _active_peer; } + [[nodiscard]] const kf::Option &activePeerAddress() const noexcept { return _active_peer_address; } /// @brief Connect to a remote peer. /// @param peer_address Address of the peer to connect to. @@ -62,7 +62,7 @@ struct Transport : kf::mixin::NonCopyable { if (connected()) { return false; } if (not doConnect(address)) { return false; } - _active_peer = address; + _active_peer_address = address; return true; } @@ -72,18 +72,18 @@ struct Transport : kf::mixin::NonCopyable { void disconnect() noexcept { doDisconnect(); - _active_peer = {}; + _active_peer_address = {}; } protected: void invokeReceive(kf::memory::Slice buffer) noexcept { - if (_active_peer.hasValue() and _receive_callback) { - _receive_callback(_active_peer.value(), buffer); + if (_active_peer_address.hasValue() and _receive_callback) { + _receive_callback(_active_peer_address.value(), buffer); } } private: - kf::Option _active_peer{}; + kf::Option _active_peer_address{}; ReceiveCallback _receive_callback{}; }; From b8cf3eb05d67f25cb317879c4780fb27ba489fdb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 17:56:25 +0300 Subject: [PATCH 022/326] Feature: EspNowTransport Implementation Fixes #4 --- .../src/djc/transport/EspNowTransport.hpp | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 DJC-Firmware/src/djc/transport/EspNowTransport.hpp diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp new file mode 100644 index 0000000..413d560 --- /dev/null +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -0,0 +1,139 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "djc/transport/Transport.hpp" + +namespace djc::transport { + +/// @brief ESP‑NOW transport implementation. +/// @note Manages ESP‑NOW peer connections. Uses a broadcast peer for discovery and a dedicated active peer for communication. +struct EspNowTransport : Transport, kf::mixin::Initable { + + [[nodiscard]] bool send(kf::memory::Slice buffer) noexcept override { + if (_active_peer.hasValue()) { + return _active_peer.value().writeBuffer(buffer).isOk(); + } else { + return false; + } + } + +private: + using EspNow = kf::network::EspNow; + using LogString = kf::memory::ArrayString<64>; + + static constexpr auto logger{kf::Logger::create("EspNowTransport")}; + + /// @brief MAC address used for ESP‑NOW broadcast. + static constexpr EspNow::Mac broadcast_mac_address{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + +protected: + /// @brief Establish a connection to a peer. + /// @param address The peer's address (must be of kind `EspNow`). + /// @return true on success, false on failure. + /// @note Adds the peer to ESP‑NOW and sets up a receive callback. + [[nodiscard]] bool doConnect(const PeerAddress &address) noexcept override { + if (address.kind() != Kind::EspNow) { return false; } + + _active_peer = addPeer(address.mac()); + if (not connected()) { return false; } + + const auto receive_setup_result = _active_peer.value().onReceive([this](kf::memory::Slice buffer) { + invokeReceive(buffer); + }); + + if (receive_setup_result.isError()) { + logger.error( + LogString::formatted( + "Receive callback attachment failed: %s", + EspNow::stringFromError(receive_setup_result.error())) + .view()); + return false; + } + + logger.info("Connected: OK"); + return true; + } + + /// @brief Disconnect from the current peer. + /// @note Removes the peer from ESP‑NOW and clears internal state. + void doDisconnect() noexcept override { + if (not connected()) { + logger.warn("Disconnect failed: No active peer"); + return; + } + + auto &peer = _active_peer.value(); + if (not peer.exist()) { + logger.error("Disconnect failed: Peer not exit"); + return; + } + + delPeer(peer); + + _active_peer = {}; + logger.info("Disconnected: OK"); + } + +private: + kf::Option + _broadcast_peer{},///< Broadcast peer for discovery. + _active_peer{}; ///< Currently connected peer + + static kf::Option addPeer(const EspNow::Mac &mac) noexcept { + auto peer_result = EspNow::Peer::add(mac); + if (peer_result.isError()) { + logger.error( + LogString::formatted( + "Failed to add peer [%s] :%s", + EspNow::stringFromMac(mac).data(), + EspNow::stringFromError(peer_result.error())) + .view()); + return {}; + } + + logger.info(LogString::formatted("Peer '%s' added", EspNow::stringFromMac(mac).data()).view()); + return {std::move(peer_result.value())}; + } + + static void delPeer(EspNow::Peer &peer) noexcept { + const auto result = peer.del(); + if (result.isError()) { + logger.error( + LogString::formatted( + "Failed to delete peer [%s] : %s", + EspNow::stringFromMac(peer.mac()).data(), + EspNow::stringFromError(result.error())) + .view()); + return; + } + } + + // impl + using This = EspNowTransport; + + KF_IMPL_INITABLE(This, bool); + bool initImpl() noexcept { + logger.info("init"); + + const auto result = EspNow::instance().init(); + if (result.isError()) { + logger.error(LogString::formatted("Failed to initialize ESP-NOW: %s", EspNow::stringFromError(result.error()))); + return false; + } + + _broadcast_peer = addPeer(broadcast_mac_address); + + logger.debug("init: ok"); + return true; + } +}; + +}// namespace djc::transport \ No newline at end of file From 4842ba0bda6c62812f2d1dbc3e95f6d13848e541 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 18:27:41 +0300 Subject: [PATCH 023/326] add: EspNowTransport put some missing includes --- DJC-Firmware/src/djc/transport/EspNowTransport.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 413d560..4369051 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -5,10 +5,12 @@ #include #include +#include #include #include #include +#include "djc/transport/PeerAddress.hpp" #include "djc/transport/Transport.hpp" namespace djc::transport { From 368b7d0cd03c1bbb48fa42b9eaa939e7890f37d0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 19:25:54 +0300 Subject: [PATCH 024/326] =?UTF-8?q?Feature:=20TransportLink=20=E2=80=93=20?= =?UTF-8?q?Connection=20Manager=20closes=20#5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/djc/transport/TransportLink.hpp | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 DJC-Firmware/src/djc/transport/TransportLink.hpp diff --git a/DJC-Firmware/src/djc/transport/TransportLink.hpp b/DJC-Firmware/src/djc/transport/TransportLink.hpp new file mode 100644 index 0000000..0f87df1 --- /dev/null +++ b/DJC-Firmware/src/djc/transport/TransportLink.hpp @@ -0,0 +1,126 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "djc/transport/PeerAddress.hpp" +#include "djc/transport/Transport.hpp" + +namespace djc::transport { + +/// @brief Connection manager for a single transport. +/// @note Separates connection lifecycle and inactivity timeout from higher‑level logic. +/// Keeps the transport abstract: the rest of the firmware only talks to `TransportLink`, never to a concrete transport. +struct TransportLink final : kf::mixin::TimedPollable { + + explicit constexpr TransportLink(kf::math::Milliseconds disconnect_timeout) noexcept : + _disconnect_timer{disconnect_timeout} {} + + /// @brief Set the active transport, disconnecting any previous connection first. + /// @note Ensures that changing the transport does not leave a stale connection open, + /// which would silently keep receiving data on the old transport. + void transport(Transport &new_transport) noexcept { + if (nullptr != _transport and _transport->connected()) { + _transport->disconnect(); + } + + _transport = &new_transport; + } + + /// @brief Forward a data buffer to the underlying transport. + /// @return true if the transport reported success, false on error or if no transport is set. + [[nodiscard]] bool send(kf::memory::Slice buffer) noexcept { + if (nullptr == _transport) { + logger.error("send failed: no transport set"); + return false; + } + + return _transport->send(buffer); + } + + /// @brief Register a callback for incoming data. + /// @note The callback is invoked for every received packet. + /// This method overwrites the transport‑level receive handler so that each incoming packet also resets the inactivity timer. + void onReceive(Transport::ReceiveCallback &&callback) noexcept { + if (not callback) { return; } + + if (nullptr == _transport) { + logger.error("onReceive failed: no transport set"); + return; + } + + _receive_callback = std::move(callback); + + _transport->onReceive([this](const PeerAddress &address, kf::memory::Slice buffer) { + _receive_callback(address, buffer); + _disconnect_timer_reset_required = true; + }); + } + + /// @brief Check whether the transport is currently connected. + [[nodiscard]] bool connected() const noexcept { return nullptr != _transport and _transport->connected(); } + + /// @brief Return the address of the active peer, if any. + /// @return Reference to an empty option when no transport is set. + [[nodiscard]] const kf::Option &activePeerAddress() const noexcept { + return (nullptr == _transport) ? null_option : _transport->activePeerAddress(); + } + + /// @brief Initiate a connection to a peer. + [[nodiscard]] bool connect(const PeerAddress &address) noexcept { + if (nullptr == _transport) { + logger.error("connect failed: no transport set"); + return false; + } + + _disconnect_timer_reset_required = true; + + return _transport->connect(address); + } + + /// @brief Disconnect from the current peer. + void disconnect() noexcept { + if (nullptr == _transport) { + logger.error("disconnect failed: no transport set"); + return; + } + + _transport->disconnect(); + } + +private: + /// @brief Empty address returned when no transport is active. + static constexpr kf::Option null_option{}; + + static constexpr auto logger{kf::Logger::create("TransportLink")}; + + Transport *_transport{nullptr}; ///< Currently active transport (may be nullptr). + Transport::ReceiveCallback _receive_callback{}; ///< User‑supplied callback for incoming data. + kf::math::Timer _disconnect_timer; ///< Inactivity timer. + volatile bool _disconnect_timer_reset_required{false};///< Flag: reset timer on next poll. + + // impl + using This = TransportLink; + + KF_IMPL_TIMED_POLLABLE(This); + void pollImpl(kf::math::Milliseconds now) noexcept { + if (not connected()) { return; } + + if (_disconnect_timer_reset_required) { + _disconnect_timer_reset_required = false; + _disconnect_timer.start(now); + } + + if (_disconnect_timer.expired(now)) { + disconnect(); + logger.info("Disconnect by timeout"); + } + } +}; + +}// namespace djc::transport \ No newline at end of file From 34268bb000f023aab680c960313f5c005b9c68c6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 20:38:12 +0300 Subject: [PATCH 025/326] add: PeerAddress toString() --- DJC-Firmware/src/djc/transport/PeerAddress.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp index 59fe741..9cf6d1c 100644 --- a/DJC-Firmware/src/djc/transport/PeerAddress.hpp +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -3,6 +3,7 @@ #pragma once +#include #include #include "djc/transport/Kind.hpp" @@ -15,6 +16,8 @@ namespace djc::transport { /// The active kind is stored in a tag field; the union contains the actual address. struct PeerAddress { + using ReprString = kf::memory::ArrayString<32>; + /// @brief Construct an ESP‑NOW peer address from a MAC. /// @param mac 6‑byte MAC address (EspNow::Mac). explicit constexpr PeerAddress(const kf::network::EspNow::Mac &mac) noexcept : @@ -44,6 +47,15 @@ struct PeerAddress { /// @brief Inequality comparison (delegates to operator==). [[nodiscard]] bool operator!=(const PeerAddress &other) const noexcept { return not this->operator==(other); } + /// @brief Get String Representation + [[nodiscard]] ReprString toString() const noexcept { + switch (_kind) { + case Kind::EspNow: + return ReprString::formatted("", kf::network::EspNow::stringFromMac(_mac)); + } + return {}; + } + private: Kind _kind; From 738c40c8eef3c97ec62c7c1b7b66b72ffaf2d5b0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 20:41:18 +0300 Subject: [PATCH 026/326] remove virtual from Transport::onReceive --- DJC-Firmware/src/djc/transport/Transport.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index f5dc514..8b340da 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -30,10 +30,6 @@ struct Transport : kf::mixin::NonCopyable { /// @note Must only be called when connected. [[nodiscard]] virtual bool send(kf::memory::Slice buffer) noexcept = 0; - /// @brief Register a callback for incoming data. - /// @param callback Functor invoked on each received packet. - virtual void onReceive(ReceiveCallback &&callback) noexcept { _receive_callback = std::move(callback); } - protected: /// @brief Hardware‑specific connection procedure. /// @param addr Address of the peer. @@ -46,6 +42,10 @@ struct Transport : kf::mixin::NonCopyable { virtual void doDisconnect() noexcept = 0; public: + /// @brief Register a callback for incoming data. + /// @param callback Functor invoked on each received packet. + void onReceive(ReceiveCallback &&callback) noexcept { _receive_callback = std::move(callback); } + /// @brief Check whether the transport is currently connected to a peer. /// @return true if a peer is active, false otherwise. [[nodiscard]] bool connected() const noexcept { return _active_peer_address.hasValue(); } From 584e340027373a965357ace8643cf15fd998de72 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 20:47:59 +0300 Subject: [PATCH 027/326] add: Transport::onReceiveForeign --- DJC-Firmware/src/djc/transport/Transport.hpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index 8b340da..54f0bb6 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -46,6 +46,10 @@ struct Transport : kf::mixin::NonCopyable { /// @param callback Functor invoked on each received packet. void onReceive(ReceiveCallback &&callback) noexcept { _receive_callback = std::move(callback); } + /// @brief Register a callback for incoming data from non-primary peer + /// @param callback Functor invoked on each received packet. + void onReceiveForeign(ReceiveCallback &&callback) noexcept { _broadcast_receive_callback = std::move(callback); } + /// @brief Check whether the transport is currently connected to a peer. /// @return true if a peer is active, false otherwise. [[nodiscard]] bool connected() const noexcept { return _active_peer_address.hasValue(); } @@ -82,9 +86,16 @@ struct Transport : kf::mixin::NonCopyable { } } + void invokeReceiveForeign(const PeerAddress &address, kf::memory::Slice buffer) noexcept { + if (_broadcast_receive_callback) { + _broadcast_receive_callback(address, buffer); + } + } + private: kf::Option _active_peer_address{}; ReceiveCallback _receive_callback{}; + ReceiveCallback _broadcast_receive_callback{}; }; }// namespace djc::transport \ No newline at end of file From a8887d259a8f58497dc7e7b0dd67b132dfffb7ec Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 20:52:41 +0300 Subject: [PATCH 028/326] EspNowTransport onReceiveForeign attachment --- DJC-Firmware/src/djc/transport/EspNowTransport.hpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 4369051..4c91b21 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -125,12 +125,18 @@ struct EspNowTransport : Transport, kf::mixin::Initable { bool initImpl() noexcept { logger.info("init"); - const auto result = EspNow::instance().init(); + auto &espnow = EspNow::instance(); + + const auto result = espnow.init(); if (result.isError()) { logger.error(LogString::formatted("Failed to initialize ESP-NOW: %s", EspNow::stringFromError(result.error()))); return false; } + espnow.onReceiveFromUnknown([this](const EspNow::Mac &mac, kf::memory::Slice buffer){ + invokeReceiveForeign(PeerAddress{mac}, buffer); + }); + _broadcast_peer = addPeer(broadcast_mac_address); logger.debug("init: ok"); From d4352ecec3b733b13f01fd83f01ed9d16085c79b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 20:55:51 +0300 Subject: [PATCH 029/326] implement: TransportLink: onReceiveForeign proxing --- DJC-Firmware/src/djc/transport/TransportLink.hpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/DJC-Firmware/src/djc/transport/TransportLink.hpp b/DJC-Firmware/src/djc/transport/TransportLink.hpp index 0f87df1..f05a2bf 100644 --- a/DJC-Firmware/src/djc/transport/TransportLink.hpp +++ b/DJC-Firmware/src/djc/transport/TransportLink.hpp @@ -62,6 +62,16 @@ struct TransportLink final : kf::mixin::TimedPollable { }); } + /// @brief Register a callback for incoming data from other peers (non-primary) + void onReceiveForeign(Transport::ReceiveCallback &&callback) noexcept { + if (nullptr == _transport) { + logger.error("onReceiveForeign failed: no transport set"); + return; + } + + _transport->onReceiveForeign(std::move(callback)); + } + /// @brief Check whether the transport is currently connected. [[nodiscard]] bool connected() const noexcept { return nullptr != _transport and _transport->connected(); } From 3e8d08746d8a3f83955b4dfdb33b5222680f6a4f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 21:02:15 +0300 Subject: [PATCH 030/326] add TransportLink into DisplayManager --- DJC-Firmware/src/djc/DisplayManager.hpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/djc/DisplayManager.hpp b/DJC-Firmware/src/djc/DisplayManager.hpp index 71b8791..d45fc5a 100644 --- a/DJC-Firmware/src/djc/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/DisplayManager.hpp @@ -14,14 +14,15 @@ #include "djc/Control.hpp" #include "djc/input/VirtualKeyboard.hpp" #include "djc/prelude.hpp" +#include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" namespace djc { struct DisplayManager final : kf::mixin::NonCopyable, kf::mixin::Initable { - explicit DisplayManager(DisplayDriver &display, const Control &control) noexcept : - _display{display}, _control{control} {} + explicit DisplayManager(DisplayDriver &display, const Control &control, const transport::TransportLink &transport_link) noexcept : + _display{display}, _control{control}, _transport_link{transport_link} {} private: using P = kf::gfx::Palette; @@ -30,6 +31,7 @@ struct DisplayManager final : kf::mixin::NonCopyable, kf::mixin::Initable _canvas{}; void onRender(kf::memory::StringView str) noexcept { @@ -49,11 +51,11 @@ struct DisplayManager final : kf::mixin::NonCopyable, kf::mixin::Initable(_canvas.maxY() - _canvas.glyphHeight()); + const auto overlay = _transport_link.connected() ? _transport_link.activePeerAddress().value().toString().data() : "Disconnected"; - const auto overlay = kf::memory::ArrayString<64>::formatted( - "\xB6\xF0""Control [%s]", - (_control.connected() ? EspNow::stringFromMac(_control.activeMac().value()).data() : "Disconnected")); - _canvas.text(0, y, overlay.data()); + _canvas.background(P::bright_blue); + _canvas.foreground(P::black); + _canvas.text(0, y, overlay); } _canvas.background(P::black); From ac432cbdea465e412a58009b34d3fc114d404107 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 21:02:43 +0300 Subject: [PATCH 031/326] temp solution for Config PeerNote --- DJC-Firmware/src/djc/Config.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 3b9f82a..9686229 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -11,13 +11,14 @@ #include "djc/Periphery.hpp" #include "djc/input/InputHandler.hpp" #include "djc/memory/Box.hpp" +#include "djc/transport/PeerAddress.hpp" namespace djc { struct Config { struct PeerNote { - EspNow::Mac mac; + char address;// temp kf::memory::Array info; }; From 331dd2807d30015265a9ce9b47e310f8027ab743 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 21:03:09 +0300 Subject: [PATCH 032/326] update: Peer UI --- .../src/djc/ui/pages/PeerExplorerPage.hpp | 46 +++++++++---------- .../src/djc/ui/widgets/PeerDisplay.hpp | 31 +++++++------ 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 55e556b..c808e6b 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -12,10 +12,10 @@ #include #include -#include "djc/Control.hpp" +#include "djc/transport/PeerAddress.hpp" +#include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/widgets/PeerDisplay.hpp" -#include "djc/prelude.hpp" namespace djc::ui::pages { @@ -25,9 +25,9 @@ struct PeerExplorerPage : UI::Page { static constexpr auto peer_display_start_index{3}; static constexpr kf::math::Milliseconds redraw_period{500}; - explicit constexpr PeerExplorerPage(UI::Page &root, Control &control) noexcept : + explicit constexpr PeerExplorerPage(UI::Page &root, transport::TransportLink &transport_link) noexcept : Page{"Peer Explorer"}, - _control{control}, + _transport_link{transport_link}, _layout{{ &root.link(), &_connection_button, @@ -36,13 +36,13 @@ struct PeerExplorerPage : UI::Page { { for (auto i = 0; i < _peer_displays.size(); i += 1) { - _peer_displays[i].control(_control); + _peer_displays[i].transportLink(_transport_link); _layout[i + peer_display_start_index] = &_peer_displays[i]; } _connection_button.callback([this]() { - if (_control.activeMac().hasValue()) { - _control.disconnect(); + if (_transport_link.connected()) { + _transport_link.disconnect(); } }); @@ -52,19 +52,18 @@ struct PeerExplorerPage : UI::Page { } void onEntry() noexcept override { - _control.onReceiveFromUnknown([this](const EspNow::Mac &mac, kf::memory::Slice data) { + _transport_link.onReceiveForeign([this](const transport::PeerAddress &address, kf::memory::Slice buffer) { logger.debug( kf::memory::ArrayString<64>::formatted( "Got %d bytes from %s", - data.size(), - EspNow::stringFromMac(mac).data())); - - getMatched(mac).update(mac, millis()); + buffer.size(), + address.toString().data())); + getMatched(address).update(address, millis()); }); } void onExit() noexcept override { - _control.onReceiveFromUnknown(Control::ReceiveFromUnknownCallback{nullptr}); + _transport_link.onReceiveForeign(transport::Transport::ReceiveCallback{nullptr}); } void onUpdate(kf::math::Milliseconds now) noexcept override { @@ -75,13 +74,14 @@ struct PeerExplorerPage : UI::Page { if (_redraw_timer.expired(now)) { _redraw_timer.start(now); - if (_control.activeMac().hasValue()) { - (void) _connection_button_label.format( - "\xFC""OK: %s\x80", - EspNow::stringFromMac(_control.activeMac().value()).data()); + if (_transport_link.activePeerAddress().hasValue()) { + (void) _connection_button_label.format("\xFC" + "OK: %s\x80", + _transport_link.activePeerAddress().value().toString().data()); _connection_button.label(_connection_button_label.view()); } else { - _connection_button.label("\xF9""Disconnected\x80"); + _connection_button.label("\xF9" + "Disconnected\x80"); } (void) _available_label_value.format(" Available: %d", countAvailablePeers()); @@ -94,7 +94,7 @@ struct PeerExplorerPage : UI::Page { private: static constexpr auto logger{kf::Logger::create("PeerExplorerPage")}; - Control &_control; + transport::TransportLink &_transport_link; kf::math::Timer _redraw_timer{redraw_period}; kf::memory::ArrayString<16> _available_label_value{""}; kf::memory::ArrayString<64> _connection_button_label{}; @@ -107,10 +107,10 @@ struct PeerExplorerPage : UI::Page { // layout kf::memory::Array _layout; - widgets::PeerDisplay &getMatched(const EspNow::Mac &mac) noexcept { + widgets::PeerDisplay &getMatched(const transport::PeerAddress &address) noexcept { for (auto &_peer_display: _peer_displays) { - if (not _peer_display.mac().hasValue()) { return _peer_display; } - if (_peer_display.mac().value() == mac) { return _peer_display; } + if (not _peer_display.address().hasValue()) { return _peer_display; } + if (_peer_display.address().value() == address) { return _peer_display; } } return _peer_displays[0]; @@ -119,7 +119,7 @@ struct PeerExplorerPage : UI::Page { int countAvailablePeers() const noexcept { int available = 0; for (auto &_peer_display: _peer_displays) { - available += int(_peer_display.mac().hasValue()); + available += int(_peer_display.address().hasValue()); } return available; } diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index b2ca0b9..0b30fbf 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -8,7 +8,8 @@ #include #include -#include "djc/Control.hpp" +#include "djc/transport/PeerAddress.hpp" +#include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" namespace djc::ui::widgets { @@ -24,21 +25,21 @@ struct PeerDisplay final : UI::Widget { static constexpr kf::math::Milliseconds clear_timeout{8000}, new_highlight_timespan{clear_timeout - 600}, pre_cleared_highligt_timespan{2000}; - void control(Control &control) noexcept { _control = &control; } + void transportLink(transport::TransportLink &new_transport_link) noexcept { _transport_link = &new_transport_link; } - const kf::Option &mac() const noexcept { return _mac_option; } + const kf::Option &address() const noexcept { return _address; } - void update(const EspNow::Mac &mac, kf::math::Milliseconds now) noexcept { + void update(const transport::PeerAddress &address, kf::math::Milliseconds now) noexcept { _mac_clear_timer.start(now); - _mac_option.value(mac); + _address.value(address); } void checkForClear(kf::math::Milliseconds now) noexcept { if (_mac_clear_timer.expired(now)) { - _mac_option = {}; + _address = {}; } - if (_mac_option.hasValue()) { + if (_address.hasValue()) { if (_mac_clear_timer.remaining(now) > new_highlight_timespan) { _state = State::NewConnection; @@ -62,8 +63,8 @@ struct PeerDisplay final : UI::Widget { render.value(kf::memory::StringView{"\xF9"}); } - if (_mac_option.hasValue()) { - render.value(EspNow::stringFromMac(_mac_option.value()).view()); + if (_address.hasValue()) { + render.value(_address.value().toString().view()); } else { render.value(kf::memory::StringView{"\xF8 - - "}); } @@ -76,19 +77,19 @@ struct PeerDisplay final : UI::Widget { } bool onClick() noexcept override { - if (not _mac_option.hasValue()) { return false; } + if (not _address.hasValue()) { return false; } - if (_control != nullptr) { - _control->connect(_mac_option.value()); - _mac_option = {}; + if (_transport_link != nullptr) { + (void) _transport_link->connect(_address.value()); + _address = {}; } return true; } private: - Control *_control{nullptr}; - kf::Option _mac_option{}; + transport::TransportLink *_transport_link{nullptr}; + kf::Option _address{}; kf::math::Timer _mac_clear_timer{clear_timeout}; State _state{State::Cleared}; }; From 11c3a2e0ed7984892ad17af082a2cbb1bde851d9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 21:03:52 +0300 Subject: [PATCH 033/326] refactor: Control - using TransportLink --- DJC-Firmware/src/djc/Control.hpp | 165 +++++-------------------------- 1 file changed, 26 insertions(+), 139 deletions(-) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/Control.hpp index 1bfe4fb..65c08b8 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/Control.hpp @@ -20,7 +20,7 @@ #include #include -#include "djc/prelude.hpp" +#include "djc/transport/TransportLink.hpp" namespace djc { @@ -34,14 +34,12 @@ enum class ControlMode : kf::u8 { struct ControlConfig final : kf::mixin::NonCopyable { kf::math::Milliseconds heartbeat_period; kf::math::Milliseconds poll_period; - kf::math::Milliseconds receive_timeout; ControlMode init_mode; static constexpr ControlConfig defaults() noexcept { return ControlConfig{ .heartbeat_period = 2000, // ms .poll_period = static_cast(1000 / 50),// 50 Hz - .receive_timeout = 30'000, // ms .init_mode = ControlMode::MavLink, }; } @@ -49,13 +47,12 @@ struct ControlConfig final : kf::mixin::NonCopyable { }// namespace internal -struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable, kf::mixin::Configurable, kf::mixin::Initable { +struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable, kf::mixin::Configurable, kf::mixin::Initable { using Config = internal::ControlConfig; using Mode = internal::ControlMode; using RawMessageCallback = kf::Function)>; using MavLinkMessageCallback = kf::Function; - using ReceiveFromUnknownCallback = EspNow::ReceiveFromUnknownHandler; struct Input { using Unit = kf::i16; @@ -71,12 +68,11 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable [[nodiscard]] static constexpr kf::memory::StringView stringFromMode(Mode mode) noexcept { return (mode == Mode::Raw) ? "Raw" : "MavLink"; } - explicit Control(const Config &config) noexcept : kf::mixin::Configurable{config} {} + explicit Control(const Config &config, transport::TransportLink &transport_link) noexcept : + kf::mixin::Configurable{config}, _transport_link{transport_link} {} // properties - void onReceiveFromUnknown(ReceiveFromUnknownCallback &&callback) noexcept { EspNow::instance().onReceiveFromUnknown(std::move(callback)); } - void onRawMessage(RawMessageCallback &&callback) noexcept { _raw_message_callback = std::move(callback); } void onMavlinkMessage(MavLinkMessageCallback &&callback) noexcept { _mavlink_message_callback = std::move(callback); } @@ -93,69 +89,17 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable void enabled(bool is_enabled) noexcept { _enabled = is_enabled; } - [[nodiscard]] bool connected() const noexcept { return _active_peer.hasValue(); } - - kf::Option activeMac() const noexcept { - if (connected()) { - return {_active_peer.value().mac()}; - } else { - return {}; - } - } - // methods - void connect(const EspNow::Mac &mac) noexcept { - if (connected()) { - if (_active_peer.value().mac() == mac) { - logger.debug("Already connected to active peer"); - return; - } - - disconnect(); - } - - _active_peer = addPeer(mac); - if (not connected()) { return; } - - const auto receive_setup_result = _active_peer.value().onReceive([this](kf::memory::Slice buffer) { onReceive(buffer); }); - if (receive_setup_result.isError()) { - logger.error("Receive callback attachment failed"); - return; - } - - _got_packet = true; - logger.info("Connected: OK"); - } - - void disconnect() noexcept { - if (not connected()) { - logger.error("Disconnect failed: No active peer"); - return; - } - - auto &peer = _active_peer.value(); - if (not peer.exist()) { - logger.error("Disconnect failed: Peer not exit"); - return; - } - - delPeer(peer); - - _active_peer = {}; - logger.info("Disconnected: OK"); - } - void sendMavLinkMessage(const mavlink_message_t *message) noexcept { - if (connected()) { - sendMavLinkMessage(_active_peer.value(), message); - } + kf::u8 buffer[MAVLINK_MAX_PACKET_LEN]; + const auto len = mavlink_msg_to_send_buffer(buffer, message); + + (void) _transport_link.send({buffer, len}); } void sendRawMessage(kf::memory::Slice buffer) noexcept { - if (connected()) { - (void) _active_peer.value().writeBuffer(buffer); - } + (void) _transport_link.send(buffer); } private: @@ -166,49 +110,16 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable RawMessageCallback _raw_message_callback{}; MavLinkMessageCallback _mavlink_message_callback{}; - kf::Option _active_peer{}; - kf::Option _broadcast_peer{}; + transport::TransportLink &_transport_link; kf::math::Timer _poll_timer{this->config().poll_period}; kf::math::Timer _heartbear_timer{this->config().heartbeat_period}; - kf::math::Timer _receive_disconnect_timer{this->config().receive_timeout}; Input _input{}; Mode _mode{this->config().init_mode}; bool _enabled{false}; - volatile bool _got_packet{false}; - - static kf::Option addPeer(const EspNow::Mac &mac) noexcept { - auto peer_result = EspNow::Peer::add(mac); - if (peer_result.isError()) { - logger.error( - LogString::formatted( - "Failed to add peer [%s] :%s", - EspNow::stringFromMac(mac).data(), - EspNow::stringFromError(peer_result.error())) - .view()); - return {}; - } - - logger.info(LogString::formatted("Peer '%s' added", EspNow::stringFromMac(mac).data()).view()); - return {std::move(peer_result.value())}; - } - - static void delPeer(EspNow::Peer &peer) noexcept { - const auto result = peer.del(); - if (result.isError()) { - logger.error( - LogString::formatted( - "Failed to delete peer [%s] : %s", - EspNow::stringFromMac(peer.mac()).data(), - EspNow::stringFromError(result.error())) - .view()); - return; - } - } void onReceive(kf::memory::Slice buffer) noexcept { - _got_packet = true; switch (_mode) { case Mode::Raw: onReceiveRaw(buffer); @@ -237,20 +148,20 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable } } - void pollRaw(EspNow::Peer &peer, kf::math::Milliseconds) noexcept { - (void) peer.writePacket(_input); + void pollRaw(kf::math::Milliseconds) noexcept { + (void) _transport_link.send({reinterpret_cast(&_input), sizeof(_input)}); } - void pollMavLink(EspNow::Peer &peer, kf::math::Milliseconds now) noexcept { - sendMavLinkControl(peer); + void pollMavLink(kf::math::Milliseconds now) noexcept { + sendMavLinkControl(); if (_heartbear_timer.expired(now)) { _heartbear_timer.start(now); - sendMavLinkHeartbeat(peer); + sendMavLinkHeartbeat(); } } - void sendMavLinkControl(EspNow::Peer &peer) noexcept { + void sendMavLinkControl() noexcept { mavlink_message_t message; (void) mavlink_msg_manual_control_pack( mavlink_system_id, MAV_COMP_ID_PARACHUTE, &message, mavlink_target_id, @@ -261,10 +172,10 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable // Buttons (unused) 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - sendMavLinkMessage(peer, &message); + sendMavLinkMessage(&message); } - void sendMavLinkHeartbeat(EspNow::Peer &peer) noexcept { + void sendMavLinkHeartbeat() noexcept { mavlink_message_t message; (void) mavlink_msg_heartbeat_pack( mavlink_system_id,// System ID @@ -275,29 +186,15 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable 0, 0, 0// Base mode, Custom mode, system status ); - sendMavLinkMessage(peer, &message); - } - - void sendMavLinkMessage(EspNow::Peer &peer, const mavlink_message_t *message) noexcept { - kf::u8 buffer[MAVLINK_MAX_PACKET_LEN]; - const auto len = mavlink_msg_to_send_buffer(buffer, message); - (void) peer.writeBuffer(kf::memory::Slice{buffer, len}); + sendMavLinkMessage(&message); } // impl - KF_IMPL_INITABLE(Control, bool); - bool initImpl() noexcept { + KF_IMPL_INITABLE(Control, void); + void initImpl() noexcept { logger.info("init"); - const auto result = EspNow::instance().init(); - if (result.isError()) { - logger.error(LogString::formatted("Failed to initialize ESP-NOW: %s", EspNow::stringFromError(result.error()))); - return false; - } - - _broadcast_peer = addPeer(EspNow::Mac{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}); - _mode = this->config().init_mode; logger.debug(stringFromMode(_mode)); @@ -306,33 +203,23 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable _heartbear_timer.start(now); logger.debug("init: ok"); - return true; } KF_IMPL_TIMED_POLLABLE(Control); void pollImpl(kf::math::Milliseconds now) noexcept { - if (_got_packet) { - _got_packet = false; - _receive_disconnect_timer.start(now); - } - - if (_receive_disconnect_timer.expired(now)) { - logger.info("Timeout"); - disconnect(); - } - - if (not connected()) { return; } + if (not _enabled) { return; } + if (not _transport_link.connected()) { return; } - if (_enabled and _poll_timer.expired(now)) { + if (_poll_timer.expired(now)) { _poll_timer.start(now); switch (_mode) { case Mode::Raw: - pollRaw(_active_peer.value(), now); + pollRaw(now); return; case Mode::MavLink: - pollMavLink(_active_peer.value(), now); + pollMavLink(now); return; } } From 51fa9dd1ba6ee98ccf0efc0ebc50936a84200bdd Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 21:04:07 +0300 Subject: [PATCH 034/326] remove espnow alias from prelude --- DJC-Firmware/src/djc/prelude.hpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp index 96ea722..5995453 100644 --- a/DJC-Firmware/src/djc/prelude.hpp +++ b/DJC-Firmware/src/djc/prelude.hpp @@ -8,7 +8,6 @@ #include #include #include -#include #include "djc/input/LogicalLevelListener.hpp" @@ -24,6 +23,4 @@ using Joystick = kf::drivers::sensors::Joystick; using Bus = kf::bus::spi::ArduinoSPI; using DisplayDriver = kf::drivers::display::ST7735; -using EspNow = kf::network::EspNow; - }// namespace djc \ No newline at end of file From 41ca71eca4a03d5b3d10df4e67912892cc34fe95 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 21:04:22 +0300 Subject: [PATCH 035/326] update: main firmware file --- DJC-Firmware/src/main.cpp | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 80f6940..506ca8c 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -13,6 +13,8 @@ #include "djc/Periphery.hpp" #include "djc/input/InputHandler.hpp" #include "djc/input/VirtualKeyboard.hpp" +#include "djc/transport/EspNowTransport.hpp" +#include "djc/transport/TransportLink.hpp" #include "djc/ui/pages/ConfigPage.hpp" #include "djc/ui/pages/MavLinkTelemetryPage.hpp" #include "djc/ui/pages/PeerExplorerPage.hpp" @@ -38,13 +40,21 @@ static djc::InputHandler input_handler{ periphery.right_button_listener, }; +static djc::transport::EspNowTransport espnow_transport{}; + +static djc::transport::TransportLink transport_link{ + kf::math::Milliseconds{30'000},// disconnect timeout +}; + static djc::Control control{ storage.config().control, + transport_link, }; static djc::DisplayManager display_manager{ periphery.display, control, + transport_link, }; // pages @@ -63,7 +73,7 @@ static djc::ui::pages::RawControlPage raw_control_page{ static djc::ui::pages::PeerExplorerPage peer_explorer_page{ root_page, - control, + transport_link, }; static djc::ui::pages::ConfigPage config_page{ @@ -91,7 +101,14 @@ void setup() { } display_manager.init(); - (void) control.init();// TODO: implement halt on error? + + if (espnow_transport.init()) { + transport_link.transport(espnow_transport); + } else { + logger.error("failed to initialize espnow transport"); + } + + control.init(); { using E = djc::ui::UI::Event; From c534284934818e29d8c61604487955e6143d7d31 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 21:14:53 +0300 Subject: [PATCH 036/326] fix: PeerAddress repr crash --- DJC-Firmware/src/djc/transport/PeerAddress.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp index 9cf6d1c..16d1ce8 100644 --- a/DJC-Firmware/src/djc/transport/PeerAddress.hpp +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -51,7 +51,7 @@ struct PeerAddress { [[nodiscard]] ReprString toString() const noexcept { switch (_kind) { case Kind::EspNow: - return ReprString::formatted("", kf::network::EspNow::stringFromMac(_mac)); + return ReprString::formatted("", kf::network::EspNow::stringFromMac(_mac).data()); } return {}; } From d77999845df605f09d181dca47e1ac9aa0394cc1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 21:15:18 +0300 Subject: [PATCH 037/326] EspNowTransport: extend log string --- DJC-Firmware/src/djc/transport/EspNowTransport.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 4c91b21..bfc699e 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -29,7 +29,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { private: using EspNow = kf::network::EspNow; - using LogString = kf::memory::ArrayString<64>; + using LogString = kf::memory::ArrayString<128>; static constexpr auto logger{kf::Logger::create("EspNowTransport")}; From 3dd5f9e434c63e25252071b5152b65656b816435 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 23:26:02 +0300 Subject: [PATCH 038/326] fix connection lifetime issues --- DJC-Firmware/src/djc/Control.hpp | 24 +++++++++---------- .../src/djc/transport/EspNowTransport.hpp | 2 +- DJC-Firmware/src/djc/transport/Transport.hpp | 8 +++++-- .../src/djc/transport/TransportLink.hpp | 4 +--- DJC-Firmware/src/main.cpp | 1 + 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/Control.hpp index 65c08b8..52c8cd3 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/Control.hpp @@ -119,18 +119,6 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable Mode _mode{this->config().init_mode}; bool _enabled{false}; - void onReceive(kf::memory::Slice buffer) noexcept { - switch (_mode) { - case Mode::Raw: - onReceiveRaw(buffer); - return; - - case Mode::MavLink: - onReceiveMavLink(buffer); - return; - } - } - void onReceiveRaw(kf::memory::Slice buffer) noexcept { if (_raw_message_callback) { _raw_message_callback(buffer); } } @@ -202,7 +190,17 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable _poll_timer.start(now); _heartbear_timer.start(now); - logger.debug("init: ok"); + _transport_link.onReceive([this](const transport::PeerAddress &, kf::memory::Slice buffer) { + switch (_mode) { + case Mode::Raw: + onReceiveRaw(buffer); + return; + + case Mode::MavLink: + onReceiveMavLink(buffer); + return; + } + }); } KF_IMPL_TIMED_POLLABLE(Control); diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index bfc699e..5b9f972 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -45,7 +45,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { if (address.kind() != Kind::EspNow) { return false; } _active_peer = addPeer(address.mac()); - if (not connected()) { return false; } + if (not _active_peer.hasValue()) { return false; } const auto receive_setup_result = _active_peer.value().onReceive([this](kf::memory::Slice buffer) { invokeReceive(buffer); diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index 54f0bb6..8533af1 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -61,9 +61,13 @@ struct Transport : kf::mixin::NonCopyable { /// @brief Connect to a remote peer. /// @param peer_address Address of the peer to connect to. /// @return true on success, false on failure. - /// @note If already connected, the method will return false. Disconnect explicitly first. [[nodiscard]] bool connect(const PeerAddress &address) noexcept { - if (connected()) { return false; } + if (connected()) { + if (_active_peer_address.value() == address) { return true; }// already on this peer + + disconnect(); + } + if (not doConnect(address)) { return false; } _active_peer_address = address; diff --git a/DJC-Firmware/src/djc/transport/TransportLink.hpp b/DJC-Firmware/src/djc/transport/TransportLink.hpp index f05a2bf..1d65447 100644 --- a/DJC-Firmware/src/djc/transport/TransportLink.hpp +++ b/DJC-Firmware/src/djc/transport/TransportLink.hpp @@ -47,8 +47,6 @@ struct TransportLink final : kf::mixin::TimedPollable { /// @note The callback is invoked for every received packet. /// This method overwrites the transport‑level receive handler so that each incoming packet also resets the inactivity timer. void onReceive(Transport::ReceiveCallback &&callback) noexcept { - if (not callback) { return; } - if (nullptr == _transport) { logger.error("onReceive failed: no transport set"); return; @@ -57,7 +55,7 @@ struct TransportLink final : kf::mixin::TimedPollable { _receive_callback = std::move(callback); _transport->onReceive([this](const PeerAddress &address, kf::memory::Slice buffer) { - _receive_callback(address, buffer); + if (_receive_callback) { _receive_callback(address, buffer); } _disconnect_timer_reset_required = true; }); } diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 506ca8c..b6aae3c 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -169,6 +169,7 @@ void loop() { const auto now = millis(); input_handler.poll(now); + transport_link.poll(now); if (control.enabled()) { using I = djc::Control::Input; From 8169012aa1fb1b0be0253df1b17c2870f25ff0d9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 23:26:38 +0300 Subject: [PATCH 039/326] adjust peer address repr string format --- DJC-Firmware/src/djc/transport/PeerAddress.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp index 16d1ce8..b8835a3 100644 --- a/DJC-Firmware/src/djc/transport/PeerAddress.hpp +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -51,9 +51,11 @@ struct PeerAddress { [[nodiscard]] ReprString toString() const noexcept { switch (_kind) { case Kind::EspNow: - return ReprString::formatted("", kf::network::EspNow::stringFromMac(_mac).data()); + return ReprString::formatted("%s@EspNow", kf::network::EspNow::stringFromMac(_mac).data()); + + default: + return {}; } - return {}; } private: From 3062820455648cb745967e4fd7b02d4fed93ab54 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 28 Apr 2026 23:27:17 +0300 Subject: [PATCH 040/326] simplify peer explorer connnect indicator --- DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index c808e6b..08d580f 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -75,9 +75,7 @@ struct PeerExplorerPage : UI::Page { _redraw_timer.start(now); if (_transport_link.activePeerAddress().hasValue()) { - (void) _connection_button_label.format("\xFC" - "OK: %s\x80", - _transport_link.activePeerAddress().value().toString().data()); + (void) _connection_button_label.format("\xFC%s\x80", _transport_link.activePeerAddress().value().toString().data()); _connection_button.label(_connection_button_label.view()); } else { _connection_button.label("\xF9" From 102f7c79b09258e58ac2409b7ae7ab0ef981c2f9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 14:33:11 +0300 Subject: [PATCH 041/326] Feature: PeerScanner Service Fixes #7 --- DJC-Firmware/src/djc/PeerScanner.hpp | 137 +++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 DJC-Firmware/src/djc/PeerScanner.hpp diff --git a/DJC-Firmware/src/djc/PeerScanner.hpp b/DJC-Firmware/src/djc/PeerScanner.hpp new file mode 100644 index 0000000..509b168 --- /dev/null +++ b/DJC-Firmware/src/djc/PeerScanner.hpp @@ -0,0 +1,137 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "djc/transport/PeerAddress.hpp" +#include "djc/transport/TransportLink.hpp" + +namespace djc { + +namespace internal { + +/// @brief Configuration parameters for the PeerScanner service. +struct PeerScannerConfig final : kf::mixin::NonCopyable { + kf::math::Milliseconds + entry_max_life_time, ///< How long an entry stays in the list without being refreshed. + entries_list_update_period;///< Interval between periodic clean-ups and list compaction. +}; + +}// namespace internal + +/// @brief Background service that listens for foreign (broadcast) packets and maintains a list of visible peers. +/// @note +/// The scanner subscribes to `TransportLink::onReceiveForeign` and keeps up to `max_entries` entries +/// Entries are refreshed every time a packet from the corresponding peer is received. +/// Periodically, expired entries are removed and the list is compacted so that the first `peers().size()` elements are always valid. +struct PeerScanner final : + + kf::mixin::NonCopyable, + kf::mixin::Initable, + kf::mixin::Configurable, + kf::mixin::TimedPollable + +{ + using Config = internal::PeerScannerConfig; + + /// @brief A single entry in the peer list. + struct Entry final { + transport::PeerAddress address; ///< Address of the peer. + kf::math::Milliseconds last_seen;///< Timestamp of the last received packet (millis). + }; + + /// @brief Maximum number of peers the scanner can remember simultaneously + static constexpr auto max_entries{8}; + + explicit constexpr PeerScanner(const Config &config, transport::TransportLink &transport_link) noexcept : + Configurable{config}, _transport_link{transport_link} {} + + /// @brief Returns a slice of the currently active peer entries. + /// @return A contiguous view of the first `_active_count` elements of the internal array. + /// @note The slice is valid only until the next call to `poll()`. + /// The entries are sorted in order of registration (oldest first). + [[nodiscard]] kf::memory::Slice> peers() const noexcept { + return {_entries.data(), _active_count}; + } + +private: + kf::memory::Array, max_entries> _entries{}; + kf::math::Timer _update_poll_timer{this->config().entries_list_update_period}; + transport::TransportLink &_transport_link; + kf::math::Milliseconds _last_poll_time{0}; + kf::usize _active_count{0}; + + // impl + using This = PeerScanner; + + KF_IMPL_INITABLE(This, void); + void initImpl() noexcept { + _update_poll_timer.start(0);// enable timer + + _transport_link.onReceiveForeign([this](const transport::PeerAddress &address, kf::memory::Slice buffer) -> void { + // search for mathing entry + for (auto &entry: _entries) { + if (entry.hasValue() and entry.value().address == address) { + entry.value().last_seen = _last_poll_time; + return; + } + } + + // search for first empty entry + for (auto &entry: _entries) { + if (not entry.hasValue()) { + entry = Entry{address, _last_poll_time}; + return; + } + } + + // no available entries -> replace oldest with newest + kf::usize oldest_entry_index{0}; + for (auto i = 1u; i < max_entries; i += 1) { + if (_entries[i].value().last_seen < _entries[oldest_entry_index].value().last_seen) { + oldest_entry_index = i; + } + } + _entries[oldest_entry_index] = Entry{address, _last_poll_time}; + }); + } + + KF_IMPL_TIMED_POLLABLE(This); + void pollImpl(kf::math::Milliseconds now) noexcept { + _last_poll_time = now; + + if (_update_poll_timer.expired(now)) { + _update_poll_timer.start(now); + + auto write_index = 0u; + for (auto read_index = 0u; read_index < max_entries; read_index += 1) { + if (_entries[read_index].hasValue() and (now > _entries[read_index].value().last_seen + this->config().entry_max_life_time)) { + _entries[read_index] = {}; + } else { + if (write_index != read_index) { + _entries[write_index] = _entries[read_index]; + } + write_index += 1; + } + } + + for (auto i = write_index; i < max_entries; i += 1) { + _entries[i] = {}; + } + _active_count = write_index; + } + } +}; + +}// namespace djc \ No newline at end of file From f9bc96fcb9a3742f1da1b5e971aeff3385ac6d06 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 14:38:31 +0300 Subject: [PATCH 042/326] implement: PeerScannerConfig::defaults() --- DJC-Firmware/src/djc/PeerScanner.hpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/DJC-Firmware/src/djc/PeerScanner.hpp b/DJC-Firmware/src/djc/PeerScanner.hpp index 509b168..209ac56 100644 --- a/DJC-Firmware/src/djc/PeerScanner.hpp +++ b/DJC-Firmware/src/djc/PeerScanner.hpp @@ -26,6 +26,13 @@ struct PeerScannerConfig final : kf::mixin::NonCopyable { kf::math::Milliseconds entry_max_life_time, ///< How long an entry stays in the list without being refreshed. entries_list_update_period;///< Interval between periodic clean-ups and list compaction. + + [[nodiscard]] static constexpr PeerScannerConfig defaults() noexcept { + return PeerScannerConfig{ + .entry_max_life_time = 8'000, + .entries_list_update_period = 1'000, + }; + } }; }// namespace internal From 627d4085f2d815a13d82a6f2b194f3346661d5eb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 14:39:03 +0300 Subject: [PATCH 043/326] add: PeerScanner config field into device Config --- DJC-Firmware/src/djc/Config.hpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 9686229..c6c6f88 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -8,6 +8,7 @@ #include #include "djc/Control.hpp" +#include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" #include "djc/input/InputHandler.hpp" #include "djc/memory/Box.hpp" @@ -24,13 +25,14 @@ struct Config { using PeerFavoritesConfig = djc::memory::Box; - static constexpr auto latest_version{4}; + static constexpr auto latest_version{5}; kf::u16 version; Periphery::Config periphery; InputHandler::Config input_handler; Control::Config control; + PeerScanner::Config peer_scanner; PeerFavoritesConfig peer_favorites; kf::memory::Array device_name; @@ -46,6 +48,7 @@ struct Config { .periphery = Periphery::Config::defaults(), .input_handler = InputHandler::Config::defaults(), .control = Control::Config::defaults(), + .peer_scanner = PeerScanner::Config::defaults(), .peer_favorites = PeerFavoritesConfig::defaults(), .device_name = {"ESP32-DJC"}, }; From 0708a9517f1c0168065e02c9044e54d2445e7d00 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 14:49:38 +0300 Subject: [PATCH 044/326] implement: TransportLink Config --- .../src/djc/transport/TransportLink.hpp | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/TransportLink.hpp b/DJC-Firmware/src/djc/transport/TransportLink.hpp index 1d65447..02ab4c3 100644 --- a/DJC-Firmware/src/djc/transport/TransportLink.hpp +++ b/DJC-Firmware/src/djc/transport/TransportLink.hpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include "djc/transport/PeerAddress.hpp" @@ -13,13 +15,34 @@ namespace djc::transport { +namespace internal { + +/// @brief Configuration parameters for the Transport Link. +struct TransportLinkConfig final : kf::mixin::NonCopyable { + kf::math::Milliseconds disconnect_timeout; + + [[nodiscard]] static constexpr TransportLinkConfig defaults() noexcept { + return TransportLinkConfig{ + .disconnect_timeout = 15'000, + }; + } +}; + +}// namespace internal + /// @brief Connection manager for a single transport. /// @note Separates connection lifecycle and inactivity timeout from higher‑level logic. /// Keeps the transport abstract: the rest of the firmware only talks to `TransportLink`, never to a concrete transport. -struct TransportLink final : kf::mixin::TimedPollable { +struct TransportLink final : + + kf::mixin::NonCopyable, + kf::mixin::Configurable, + kf::mixin::TimedPollable + +{ + using Config = internal::TransportLinkConfig; - explicit constexpr TransportLink(kf::math::Milliseconds disconnect_timeout) noexcept : - _disconnect_timer{disconnect_timeout} {} + using Configurable::Configurable; /// @brief Set the active transport, disconnecting any previous connection first. /// @note Ensures that changing the transport does not leave a stale connection open, @@ -107,10 +130,10 @@ struct TransportLink final : kf::mixin::TimedPollable { static constexpr auto logger{kf::Logger::create("TransportLink")}; - Transport *_transport{nullptr}; ///< Currently active transport (may be nullptr). - Transport::ReceiveCallback _receive_callback{}; ///< User‑supplied callback for incoming data. - kf::math::Timer _disconnect_timer; ///< Inactivity timer. - volatile bool _disconnect_timer_reset_required{false};///< Flag: reset timer on next poll. + Transport *_transport{nullptr}; ///< Currently active transport (may be nullptr). + Transport::ReceiveCallback _receive_callback{}; ///< User‑supplied callback for incoming data. + kf::math::Timer _disconnect_timer{this->config().disconnect_timeout};///< Inactivity timer. + volatile bool _disconnect_timer_reset_required{false}; ///< Flag: reset timer on next poll. // impl using This = TransportLink; From 4c419e85bddf54484781996e9b8a7621fdcf0160 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 14:49:56 +0300 Subject: [PATCH 045/326] add: TransportLink config into device config --- DJC-Firmware/src/djc/Config.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index c6c6f88..c2deb85 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -13,6 +13,7 @@ #include "djc/input/InputHandler.hpp" #include "djc/memory/Box.hpp" #include "djc/transport/PeerAddress.hpp" +#include "djc/transport/TransportLink.hpp" namespace djc { @@ -31,6 +32,7 @@ struct Config { Periphery::Config periphery; InputHandler::Config input_handler; + transport::TransportLink::Config transport_link; Control::Config control; PeerScanner::Config peer_scanner; PeerFavoritesConfig peer_favorites; @@ -47,6 +49,7 @@ struct Config { .version = latest_version, .periphery = Periphery::Config::defaults(), .input_handler = InputHandler::Config::defaults(), + .transport_link = transport::TransportLink::Config::defaults(), .control = Control::Config::defaults(), .peer_scanner = PeerScanner::Config::defaults(), .peer_favorites = PeerFavoritesConfig::defaults(), From 718b7904c40406f63a41ff8877a0dc9d203a44d6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 15:06:09 +0300 Subject: [PATCH 046/326] add: PeerScanner service into main --- DJC-Firmware/src/main.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index b6aae3c..692bf58 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -10,6 +10,7 @@ #include "djc/ConfigManager.hpp" #include "djc/Control.hpp" #include "djc/DisplayManager.hpp" +#include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" #include "djc/input/InputHandler.hpp" #include "djc/input/VirtualKeyboard.hpp" @@ -43,7 +44,12 @@ static djc::InputHandler input_handler{ static djc::transport::EspNowTransport espnow_transport{}; static djc::transport::TransportLink transport_link{ - kf::math::Milliseconds{30'000},// disconnect timeout + storage.config().transport_link, +}; + +static djc::PeerScanner peer_scanner{ + storage.config().peer_scanner, + transport_link, }; static djc::Control control{ @@ -108,6 +114,8 @@ void setup() { logger.error("failed to initialize espnow transport"); } + peer_scanner.init(); + control.init(); { @@ -170,6 +178,7 @@ void loop() { const auto now = millis(); input_handler.poll(now); transport_link.poll(now); + peer_scanner.poll(now); if (control.enabled()) { using I = djc::Control::Input; From f5da728792a952953e5bd0f22ac93a220cc2ae76 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 16:24:01 +0300 Subject: [PATCH 047/326] fix: PeerExplorer Entry processing issues --- DJC-Firmware/src/djc/PeerScanner.hpp | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerScanner.hpp b/DJC-Firmware/src/djc/PeerScanner.hpp index 209ac56..b68d0c2 100644 --- a/DJC-Firmware/src/djc/PeerScanner.hpp +++ b/DJC-Firmware/src/djc/PeerScanner.hpp @@ -30,7 +30,7 @@ struct PeerScannerConfig final : kf::mixin::NonCopyable { [[nodiscard]] static constexpr PeerScannerConfig defaults() noexcept { return PeerScannerConfig{ .entry_max_life_time = 8'000, - .entries_list_update_period = 1'000, + .entries_list_update_period = 100, }; } }; @@ -121,15 +121,27 @@ struct PeerScanner final : if (_update_poll_timer.expired(now)) { _update_poll_timer.start(now); + if (_transport_link.connected()) { + const auto &active_address = _transport_link.activePeerAddress().value(); + for (auto &entry: _entries) { + if (entry.hasValue() and entry.value().address == active_address) { + entry = {}; + break; + } + } + } + auto write_index = 0u; for (auto read_index = 0u; read_index < max_entries; read_index += 1) { - if (_entries[read_index].hasValue() and (now > _entries[read_index].value().last_seen + this->config().entry_max_life_time)) { - _entries[read_index] = {}; - } else { - if (write_index != read_index) { - _entries[write_index] = _entries[read_index]; + if (_entries[read_index].hasValue()) { + if (now > _entries[read_index].value().last_seen + this->config().entry_max_life_time) { + _entries[read_index] = {}; + } else { + if (write_index != read_index) { + _entries[write_index] = _entries[read_index]; + } + write_index += 1; } - write_index += 1; } } From 5d94828dd4e6c885d6ce4f79898c47dd24ecbbc0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 16:24:59 +0300 Subject: [PATCH 048/326] Refactor: Use PeerScanner in PeerExplorerPage Fixes #8 --- .../src/djc/ui/pages/PeerExplorerPage.hpp | 73 ++++++------------ .../src/djc/ui/widgets/PeerDisplay.hpp | 74 ++++++------------- DJC-Firmware/src/main.cpp | 1 + 3 files changed, 46 insertions(+), 102 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 08d580f..fed1426 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -3,16 +3,13 @@ #pragma once -#include // for millis - -#include #include #include #include #include #include -#include "djc/transport/PeerAddress.hpp" +#include "djc/PeerScanner.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/widgets/PeerDisplay.hpp" @@ -21,12 +18,12 @@ namespace djc::ui::pages { struct PeerExplorerPage : UI::Page { - static constexpr auto max_peer_display{8}; static constexpr auto peer_display_start_index{3}; - static constexpr kf::math::Milliseconds redraw_period{500}; + static constexpr kf::math::Milliseconds redraw_period{1'000}; - explicit constexpr PeerExplorerPage(UI::Page &root, transport::TransportLink &transport_link) noexcept : + explicit constexpr PeerExplorerPage(UI::Page &root, PeerScanner &peer_scanner, transport::TransportLink &transport_link) noexcept : Page{"Peer Explorer"}, + _peer_scanner{peer_scanner}, _transport_link{transport_link}, _layout{{ &root.link(), @@ -48,29 +45,10 @@ struct PeerExplorerPage : UI::Page { widgets({_layout.data(), _layout.size()}); - _redraw_timer.start(millis()); - } - - void onEntry() noexcept override { - _transport_link.onReceiveForeign([this](const transport::PeerAddress &address, kf::memory::Slice buffer) { - logger.debug( - kf::memory::ArrayString<64>::formatted( - "Got %d bytes from %s", - buffer.size(), - address.toString().data())); - getMatched(address).update(address, millis()); - }); - } - - void onExit() noexcept override { - _transport_link.onReceiveForeign(transport::Transport::ReceiveCallback{nullptr}); + _redraw_timer.start(0);// enable timer } void onUpdate(kf::math::Milliseconds now) noexcept override { - for (auto &_peer_display: _peer_displays) { - _peer_display.checkForClear(now); - } - if (_redraw_timer.expired(now)) { _redraw_timer.start(now); @@ -82,45 +60,38 @@ struct PeerExplorerPage : UI::Page { "Disconnected\x80"); } - (void) _available_label_value.format(" Available: %d", countAvailablePeers()); + const auto available_peers = _peer_scanner.peers(); + + (void) _available_label_value.format(" Available: %d", available_peers.size()); _available_label.value(_available_label_value.view()); + const auto deadline_time = now + _peer_scanner.config().entry_max_life_time; + + for (auto i = 0u; i < _peer_displays.size(); i += 1) { + if (i < available_peers.size()) { + _peer_displays[i].update(available_peers[i], deadline_time); + } else { + _peer_displays[i].clear(); + } + } + UI::instance().addEvent(UI::Event::update()); } } private: - static constexpr auto logger{kf::Logger::create("PeerExplorerPage")}; - + PeerScanner &_peer_scanner; transport::TransportLink &_transport_link; kf::math::Timer _redraw_timer{redraw_period}; - kf::memory::ArrayString<16> _available_label_value{""}; - kf::memory::ArrayString<64> _connection_button_label{}; + kf::memory::ArrayString<64> _available_label_value{}, _connection_button_label{}; // widgets UI::Button _connection_button{""}; UI::Display _available_label{_available_label_value.view()}; - kf::memory::Array _peer_displays{}; + kf::memory::Array _peer_displays{}; // layout - kf::memory::Array _layout; - - widgets::PeerDisplay &getMatched(const transport::PeerAddress &address) noexcept { - for (auto &_peer_display: _peer_displays) { - if (not _peer_display.address().hasValue()) { return _peer_display; } - if (_peer_display.address().value() == address) { return _peer_display; } - } - - return _peer_displays[0]; - } - - int countAvailablePeers() const noexcept { - int available = 0; - for (auto &_peer_display: _peer_displays) { - available += int(_peer_display.address().hasValue()); - } - return available; - } + kf::memory::Array _layout; }; }// namespace djc::ui::pages \ No newline at end of file diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index 0b30fbf..3d03fb0 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -6,9 +6,10 @@ #include #include #include +#include #include -#include "djc/transport/PeerAddress.hpp" +#include "djc/PeerScanner.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" @@ -16,72 +17,44 @@ namespace djc::ui::widgets { struct PeerDisplay final : UI::Widget { - enum class State : kf::u8 { - Cleared, - NewConnection, - Stable, - PreCleared, + enum class State : char { + Cleared = '\xF8', + Stable = '\xFC', + PreCleared = '\xFC', }; - static constexpr kf::math::Milliseconds clear_timeout{8000}, new_highlight_timespan{clear_timeout - 600}, pre_cleared_highligt_timespan{2000}; + static constexpr kf::math::Milliseconds pre_cleared_highligt_timespan{2000}; void transportLink(transport::TransportLink &new_transport_link) noexcept { _transport_link = &new_transport_link; } - const kf::Option &address() const noexcept { return _address; } - - void update(const transport::PeerAddress &address, kf::math::Milliseconds now) noexcept { - _mac_clear_timer.start(now); - _address.value(address); - } - - void checkForClear(kf::math::Milliseconds now) noexcept { - if (_mac_clear_timer.expired(now)) { - _address = {}; - } - - if (_address.hasValue()) { - - if (_mac_clear_timer.remaining(now) > new_highlight_timespan) { - _state = State::NewConnection; - } else if (_mac_clear_timer.remaining(now) < pre_cleared_highligt_timespan) { - _state = State::PreCleared; - } else { - _state = State::Stable; - } + void update(const kf::Option &entry_option, kf::math::Milliseconds deadline_time) noexcept { + _entry_option = entry_option; + if (_entry_option.hasValue()) { + _state = (deadline_time < _entry_option.value().last_seen + pre_cleared_highligt_timespan) ? State::Stable : State::PreCleared; } else { _state = State::Cleared; } } - void doRender(UI::RenderImpl &render) const noexcept override { - render.beginBlock(); - - if (_state == State::NewConnection) { - render.value(kf::memory::StringView{"\xFC"}); - } else if (_state == State::PreCleared) { - render.value(kf::memory::StringView{"\xF9"}); - } - - if (_address.hasValue()) { - render.value(_address.value().toString().view()); - } else { - render.value(kf::memory::StringView{"\xF8 - - "}); - } + void clear() noexcept { + _entry_option = {}; + } - if (_state != State::Stable) { - render.value(kf::memory::StringView{"\x80"}); - } + void doRender(UI::RenderImpl &render) const noexcept override { + constexpr auto empty_string = " - - "; + const auto content = (_entry_option.hasValue()) ? _entry_option.value().address.toString().data() : empty_string; + render.beginBlock(); + render.value(kf::memory::ArrayString<64>::formatted("%c%s\x80", static_cast(_state), content).view()); render.endBlock(); } bool onClick() noexcept override { - if (not _address.hasValue()) { return false; } + if (not _entry_option.hasValue()) { return false; } - if (_transport_link != nullptr) { - (void) _transport_link->connect(_address.value()); - _address = {}; + if (_transport_link != nullptr and _transport_link->connect(_entry_option.value().address)) { + clear(); } return true; @@ -89,8 +62,7 @@ struct PeerDisplay final : UI::Widget { private: transport::TransportLink *_transport_link{nullptr}; - kf::Option _address{}; - kf::math::Timer _mac_clear_timer{clear_timeout}; + kf::Option _entry_option{}; State _state{State::Cleared}; }; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 692bf58..ea2a09c 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -79,6 +79,7 @@ static djc::ui::pages::RawControlPage raw_control_page{ static djc::ui::pages::PeerExplorerPage peer_explorer_page{ root_page, + peer_scanner, transport_link, }; From 3e9af651b5e37f2225f6cc104f09e7957d7f6552 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 29 Apr 2026 16:50:04 +0300 Subject: [PATCH 049/326] fix: PeerDisplay peer lifetime indicator --- DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 4 +--- DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index fed1426..416a7f7 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -65,11 +65,9 @@ struct PeerExplorerPage : UI::Page { (void) _available_label_value.format(" Available: %d", available_peers.size()); _available_label.value(_available_label_value.view()); - const auto deadline_time = now + _peer_scanner.config().entry_max_life_time; - for (auto i = 0u; i < _peer_displays.size(); i += 1) { if (i < available_peers.size()) { - _peer_displays[i].update(available_peers[i], deadline_time); + _peer_displays[i].update(available_peers[i], now, _peer_scanner.config().entry_max_life_time); } else { _peer_displays[i].clear(); } diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index 3d03fb0..300d80c 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -20,18 +20,19 @@ struct PeerDisplay final : UI::Widget { enum class State : char { Cleared = '\xF8', Stable = '\xFC', - PreCleared = '\xFC', + PreCleared = '\xF9', }; static constexpr kf::math::Milliseconds pre_cleared_highligt_timespan{2000}; void transportLink(transport::TransportLink &new_transport_link) noexcept { _transport_link = &new_transport_link; } - void update(const kf::Option &entry_option, kf::math::Milliseconds deadline_time) noexcept { + void update(const kf::Option &entry_option, kf::math::Milliseconds now,kf::math::Milliseconds max_life_time) noexcept { _entry_option = entry_option; if (_entry_option.hasValue()) { - _state = (deadline_time < _entry_option.value().last_seen + pre_cleared_highligt_timespan) ? State::Stable : State::PreCleared; + const auto age = now - _entry_option.value().last_seen; + _state = (age + pre_cleared_highligt_timespan >= max_life_time) ? State::PreCleared : State::Stable; } else { _state = State::Cleared; } From 97790355fdfb88edd9e9e94a95a6b76daeec1387 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 30 Apr 2026 09:12:23 +0300 Subject: [PATCH 050/326] add: ManualInput --- DJC-Firmware/src/djc/ManualInput.hpp | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 DJC-Firmware/src/djc/ManualInput.hpp diff --git a/DJC-Firmware/src/djc/ManualInput.hpp b/DJC-Firmware/src/djc/ManualInput.hpp new file mode 100644 index 0000000..af5e157 --- /dev/null +++ b/DJC-Firmware/src/djc/ManualInput.hpp @@ -0,0 +1,28 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace djc { + +/// @brief Normalised manual control input from joysticks +/// @note +/// Holds four stick channels (left/right, X/Y) in a fixed‑point format. +/// The values are sent to the peer by the active protocol. +struct ManualInput final { + using Unit = kf::i16; + + /// @brief Scale factor - normalised float [‑1, +1] is multiplied by this value + static constexpr Unit scale_factor{1000}; + + Unit left_x, left_y, right_x, right_y;///< Joystick axes + + /// @brief Convert a normalised float value to the internal unit representation + [[nodiscard]] static constexpr Unit fromNormalized(kf::f32 value) noexcept { + return static_cast(value * scale_factor); + } +}; + +}// namespace djc \ No newline at end of file From e350704f9361d2192806e61748c8e6a835989d35 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 30 Apr 2026 09:50:09 +0300 Subject: [PATCH 051/326] Feature: Create Protocol Interface Fixes #15 --- DJC-Firmware/src/djc/protocol/Protocol.hpp | 32 ++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 DJC-Firmware/src/djc/protocol/Protocol.hpp diff --git a/DJC-Firmware/src/djc/protocol/Protocol.hpp b/DJC-Firmware/src/djc/protocol/Protocol.hpp new file mode 100644 index 0000000..476ab3b --- /dev/null +++ b/DJC-Firmware/src/djc/protocol/Protocol.hpp @@ -0,0 +1,32 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "djc/ManualInput.hpp" +#include "djc/transport/TransportLink.hpp" + +namespace djc::protocol { + +/// @brief Abstract protocol for sending control data and processing incoming packets +struct Protocol : kf::mixin::NonCopyable { + + /// @brief Called periodically when the protocol is active and the link is connected + /// @note + /// The implementation must serialise the current stick values and send them through the transport. + /// Periodic tasks such as heartbeat or keep‑alive messages are also handled here, using `now` to maintain internal timers. + virtual void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept = 0; + + /// @brief Process an incoming data buffer from the connected peer + /// @note + /// The implementation must parse the data according to its protocol and notify the appropriate callback + /// This method is called from the transport callback; it must be fast and never block. + virtual void receive(kf::memory::Slice buffer) noexcept = 0; +}; + +}// namespace djc::protocol \ No newline at end of file From 78548bb2dd970862b9a4cad8f87f3fbaa5c07ed0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 30 Apr 2026 14:56:07 +0300 Subject: [PATCH 052/326] Feature: RawProtocol Implementation Fixes #16 --- DJC-Firmware/src/djc/protocol/RawProtocol.hpp | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 DJC-Firmware/src/djc/protocol/RawProtocol.hpp diff --git a/DJC-Firmware/src/djc/protocol/RawProtocol.hpp b/DJC-Firmware/src/djc/protocol/RawProtocol.hpp new file mode 100644 index 0000000..f240017 --- /dev/null +++ b/DJC-Firmware/src/djc/protocol/RawProtocol.hpp @@ -0,0 +1,31 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include "djc/ManualInput.hpp" +#include "djc/protocol/Protocol.hpp" +#include "djc/transport/TransportLink.hpp" + +namespace djc::protocol { + +/// @brief Raw protocol – sends ManualInput as a binary blob and invokes a callback on received data +/// @note +/// The protocol simply sends the `ManualInput` struct verbatim. +/// Incoming data is forwarded directly to the callback as is`. +struct RawProtocol : Protocol, kf::mixin::Callbacked> { + + void poll(kf::math::Milliseconds, const ManualInput &input, transport::TransportLink &transport_link) noexcept override { + (void) transport_link.send({reinterpret_cast(&input), sizeof(ManualInput)}); + } + + void receive(kf::memory::Slice buffer) noexcept override { + this->invoke(buffer); + } +}; + +}// namespace djc::protocol \ No newline at end of file From 0e497efc4b71eda01981470ff40dfdbde099a0e6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 30 Apr 2026 15:02:34 +0300 Subject: [PATCH 053/326] add units include into RawProtocol --- DJC-Firmware/src/djc/protocol/RawProtocol.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/DJC-Firmware/src/djc/protocol/RawProtocol.hpp b/DJC-Firmware/src/djc/protocol/RawProtocol.hpp index f240017..ea53841 100644 --- a/DJC-Firmware/src/djc/protocol/RawProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/RawProtocol.hpp @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include From 84ed032788867d705e15baffd94529217a9a4fd5 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 30 Apr 2026 15:58:24 +0300 Subject: [PATCH 054/326] Feature: MavLinkProtocol Implementation Fixes #17 --- .../src/djc/protocol/MavlinkProtocol.hpp | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp diff --git a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp new file mode 100644 index 0000000..cf3f1f5 --- /dev/null +++ b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp @@ -0,0 +1,140 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "djc/ManualInput.hpp" +#include "djc/protocol/Protocol.hpp" +#include "djc/transport/TransportLink.hpp" + +namespace djc::protocol { + +namespace internal { + +/// @brief Configuration for the MAVLink protocol. +struct MavlinkProtocolConfig { + + using IdType = kf::u8; + + kf::math::Milliseconds heartbeat_period;///< Period between HEARTBEAT messages (ms). + IdType system_id_self; ///< MAVLink system ID of this controller. + IdType system_id_target; ///< MAVLink system ID of the target drone (0 = broadcast). + IdType component_id_heartbeat; + IdType component_id_manual_control; + + static constexpr MavlinkProtocolConfig defaults() noexcept { + return MavlinkProtocolConfig{ + .heartbeat_period = 2'000,// ms + .system_id_self = 0x7f, + .system_id_target = 0x01, + .component_id_heartbeat = MAV_COMP_ID_USER1, + .component_id_manual_control = MAV_COMP_ID_USER2, + }; + } +}; + +}// namespace internal + +/// @brief MAVLink protocol - sends MANUAL_CONTROL and HEARTBEAT packets, invokes callback on received MAVLink messages +/// @note +/// A manual control message is sent on every `poll()`, carrying the current stick values. +/// A heartbeat is sent immediately on activation and then at the configured period. +/// Incoming data is parsed as MAVLink and forwarded via callback. +struct MavlinkProtocol : + + Protocol, + kf::mixin::Callbacked, + kf::mixin::Configurable + +{ + using Config = internal::MavlinkProtocolConfig; + + using kf::mixin::Configurable::Configurable; + + /// @brief Serialise and send a MAVLink message through the given transport. + /// @param transport_link Transport to use for sending. + /// @param message The message to send. + /// @return true if the transport reported success, false otherwise. + /// @note + /// The return value reflects the transport-level result. + [[nodiscard]] bool sendMessage(transport::TransportLink &transport_link, const mavlink_message_t &message) const noexcept { + kf::u8 buffer[MAVLINK_MAX_PACKET_LEN]; + const auto len = mavlink_msg_to_send_buffer(buffer, &message); + + return transport_link.send({buffer, len}); + } + + // impl dynamic + + void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept override { + if (_heartbear_timer.expired(now) or _need_to_reset_heartbeat_timer) { + _need_to_reset_heartbeat_timer = false; + _heartbear_timer.start(now); + + sendHeartbeat(transport_link); + } + + sendManualControl(transport_link, input); + } + + void receive(kf::memory::Slice buffer) noexcept override { + mavlink_message_t message; + mavlink_status_t status; + + for (auto b: buffer) { + if (mavlink_parse_char(MAVLINK_COMM_0, b, &message, &status) != 0) { + this->invoke(message); + } + } + } + +private: + kf::math::Timer _heartbear_timer{this->config().heartbeat_period}; + bool _need_to_reset_heartbeat_timer{true}; + + void sendHeartbeat(transport::TransportLink &transport_link) const noexcept { + mavlink_message_t message; + (void) mavlink_msg_heartbeat_pack( + this->config().system_id_self, + this->config().component_id_heartbeat, + &message, + MAV_TYPE_QUADROTOR, + MAV_AUTOPILOT_GENERIC, + 0, 0, 0// Base mode, Custom mode, system status + ); + + (void) sendMessage(transport_link, message); + } + + void sendManualControl(transport::TransportLink &transport_link, const ManualInput &input) const noexcept { + mavlink_message_t message; + (void) mavlink_msg_manual_control_pack( + this->config().system_id_self, + this->config().component_id_manual_control, + &message, + this->config().system_id_target, + input.right_y,// x: pitch (right Y) + input.right_x,// y: roll (right X) + input.left_y, // z: thrust (left Y) + input.left_x, // r: yaw (left X) + // Buttons (unused) + 0, 0, // buttons + 0, // extensions + 0, 0, // roll/pitch only axes + 0, 0, 0, 0, 0, 0// aux0..6 + ); + + (void) sendMessage(transport_link, message); + } +}; + +}// namespace djc::protocol \ No newline at end of file From 62e47db5ae9391acf0376bc4303588c4439394ed Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 1 May 2026 16:01:14 +0300 Subject: [PATCH 055/326] update: MavlinkProtocol --- .../src/djc/protocol/MavlinkProtocol.hpp | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp index cf3f1f5..8b387c0 100644 --- a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp @@ -66,7 +66,7 @@ struct MavlinkProtocol : /// @return true if the transport reported success, false otherwise. /// @note /// The return value reflects the transport-level result. - [[nodiscard]] bool sendMessage(transport::TransportLink &transport_link, const mavlink_message_t &message) const noexcept { + [[nodiscard]] static bool sendMessage(transport::TransportLink &transport_link, const mavlink_message_t &message) noexcept { kf::u8 buffer[MAVLINK_MAX_PACKET_LEN]; const auto len = mavlink_msg_to_send_buffer(buffer, &message); @@ -76,8 +76,8 @@ struct MavlinkProtocol : // impl dynamic void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept override { - if (_heartbear_timer.expired(now) or _need_to_reset_heartbeat_timer) { - _need_to_reset_heartbeat_timer = false; + if (_heartbear_timer.expired(now) or _heartbeat_timer_reset_required) { + _heartbeat_timer_reset_required = false; _heartbear_timer.start(now); sendHeartbeat(transport_link); @@ -99,9 +99,9 @@ struct MavlinkProtocol : private: kf::math::Timer _heartbear_timer{this->config().heartbeat_period}; - bool _need_to_reset_heartbeat_timer{true}; + bool _heartbeat_timer_reset_required{true}; - void sendHeartbeat(transport::TransportLink &transport_link) const noexcept { + [[nodiscard]] bool sendHeartbeat(transport::TransportLink &transport_link) const noexcept { mavlink_message_t message; (void) mavlink_msg_heartbeat_pack( this->config().system_id_self, @@ -112,28 +112,27 @@ struct MavlinkProtocol : 0, 0, 0// Base mode, Custom mode, system status ); - (void) sendMessage(transport_link, message); + return sendMessage(transport_link, message); } - void sendManualControl(transport::TransportLink &transport_link, const ManualInput &input) const noexcept { + [[nodiscard]] bool sendManualControl(transport::TransportLink &transport_link, const ManualInput &input) const noexcept { mavlink_message_t message; (void) mavlink_msg_manual_control_pack( this->config().system_id_self, this->config().component_id_manual_control, &message, this->config().system_id_target, - input.right_y,// x: pitch (right Y) - input.right_x,// y: roll (right X) - input.left_y, // z: thrust (left Y) - input.left_x, // r: yaw (left X) - // Buttons (unused) + input.right_y, // x: pitch (right Y) + input.right_x, // y: roll (right X) + input.left_y, // z: thrust (left Y) + input.left_x, // r: yaw (left X) 0, 0, // buttons 0, // extensions 0, 0, // roll/pitch only axes - 0, 0, 0, 0, 0, 0// aux0..6 + 0, 0, 0, 0, 0, 0// aux: 0..6 ); - (void) sendMessage(transport_link, message); + return sendMessage(transport_link, message); } }; From f62e0c02e9bc5c634230551e3388b6ab7589e600 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 1 May 2026 20:07:33 +0300 Subject: [PATCH 056/326] =?UTF-8?q?=F0=9F=A4=A1=20"Vibe=20coders=20now=20l?= =?UTF-8?q?egally=20obliged=20to=20star=20this=20repo"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.MD | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/README.MD b/README.MD index 85d92ef..961af1f 100644 --- a/README.MD +++ b/README.MD @@ -1,6 +1,132 @@ + + # ESP32 Dual Joystick Controller (DJC) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) + An open-source remote controller based on ESP32 with dual analog joysticks, designed for DIY robotics projects. From bb99b6ef79f1380068f3d3968335ba7e5876f10a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 10:49:43 +0300 Subject: [PATCH 057/326] Feature: ProtocolLink - Protocol Facade Fixes #18 --- .../src/djc/protocol/ProtocolLink.hpp | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 DJC-Firmware/src/djc/protocol/ProtocolLink.hpp diff --git a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp new file mode 100644 index 0000000..a4733d1 --- /dev/null +++ b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp @@ -0,0 +1,49 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include +#include +#include + +#include "djc/ManualInput.hpp" +#include "djc/protocol/Protocol.hpp" +#include "djc/transport/TransportLink.hpp" + +namespace djc::protocol { + +struct ProtocolLink : kf::mixin::NonCopyable { + + void protocol(Protocol &new_protocol) noexcept { + _protocol = &new_protocol; + } + + void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept { + if (nullptr == _protocol) { + logger.error("poll: no protocol set"); + return; + } + + _protocol->poll(now, input, transport_link); + } + + void receive(kf::memory::Slice buffer) noexcept { + if (nullptr == _protocol) { + logger.error("receive: no protocol set"); + return; + } + + _protocol->receive(buffer); + } + +private: + static constexpr auto logger{kf::Logger::create("ProtocolLink")}; + + Protocol *_protocol{nullptr}; +}; + +}// namespace djc::protocol \ No newline at end of file From c2d27a9cc7ab84f3f3676c850bb72d0aef7403f6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 11:12:37 +0300 Subject: [PATCH 058/326] =?UTF-8?q?Feature:=20ProtocolRegistry=20=E2=80=93?= =?UTF-8?q?=20Protocol=20Storage=20Fixes=20#19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/djc/protocol/ProtocolRegistry.hpp | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp new file mode 100644 index 0000000..91485a4 --- /dev/null +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -0,0 +1,62 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include "djc/protocol/MavlinkProtocol.hpp" +#include "djc/protocol/Protocol.hpp" +#include "djc/protocol/RawProtocol.hpp" + +namespace djc::protocol { + +namespace internal { + +struct ProtocolRegistryConfig final : kf::mixin::NonCopyable { + + MavlinkProtocol::Config mavlink; + + [[nodiscard]] static constexpr ProtocolRegistryConfig defaults() noexcept { + return ProtocolRegistryConfig{ + .mavlink = MavlinkProtocol::Config::defaults(), + }; + } +}; + +}// namespace internal + +struct ProtocolRegistry final : + + kf::mixin::NonCopyable, + kf::mixin::Configurable + +{ + using Config = internal::ProtocolRegistryConfig; + + enum class Mode : kf::u8 { + Raw = 0x00, + Mavlink = 0x01, + }; + + using kf::mixin::Configurable::Configurable; + + Protocol &get(Mode mode) noexcept { + switch (mode) { + case Mode::Raw: return _raw_protocol; + case Mode::Mavlink: return _mavlink_protocol; + } + } + + RawProtocol &raw() noexcept { return _raw_protocol; } + + MavlinkProtocol &mavlink() noexcept { return _mavlink_protocol; } + +private: + RawProtocol _raw_protocol{}; + MavlinkProtocol _mavlink_protocol{this->config().mavlink}; +}; + +}// namespace djc::protocol \ No newline at end of file From 192c0510d41f87b3434a157505e30dd1a4d7f111 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 11:17:30 +0300 Subject: [PATCH 059/326] update: ProtocolRegistry --- DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp index 91485a4..a420406 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include @@ -43,16 +44,18 @@ struct ProtocolRegistry final : using kf::mixin::Configurable::Configurable; - Protocol &get(Mode mode) noexcept { + [[nodiscard]] Protocol &get(Mode mode) noexcept { switch (mode) { case Mode::Raw: return _raw_protocol; case Mode::Mavlink: return _mavlink_protocol; } } - RawProtocol &raw() noexcept { return _raw_protocol; } + [[nodiscard]] RawProtocol &raw() noexcept { return _raw_protocol; } - MavlinkProtocol &mavlink() noexcept { return _mavlink_protocol; } + [[nodiscard]] MavlinkProtocol &mavlink() noexcept { return _mavlink_protocol; } + + [[nodiscard]] static constexpr kf::memory::StringView stringFromMode(Mode mode) noexcept { return (mode == Mode::Raw) ? "Raw" : "MavLink"; } private: RawProtocol _raw_protocol{}; From 898bbd036f163c8bf17fcdc59318feaacd95fc20 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 11:26:38 +0300 Subject: [PATCH 060/326] update: ProtocolLink --- .../src/djc/protocol/ProtocolLink.hpp | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp index a4733d1..a9b62f5 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp @@ -6,8 +6,10 @@ #include #include +#include #include #include +#include #include #include "djc/ManualInput.hpp" @@ -16,7 +18,30 @@ namespace djc::protocol { -struct ProtocolLink : kf::mixin::NonCopyable { +namespace internal { + +struct ProtocolLinkConfig { + + kf::math::Milliseconds poll_period; + + [[nodiscard]] static constexpr ProtocolLinkConfig defaults() noexcept { + return ProtocolLinkConfig{ + .poll_period = static_cast(1000 / 50), + }; + } +}; + +}// namespace internal + +struct ProtocolLink : + + kf::mixin::NonCopyable, + kf::mixin::Configurable + +{ + using Config = internal::ProtocolLinkConfig; + + using kf::mixin::Configurable::Configurable; void protocol(Protocol &new_protocol) noexcept { _protocol = &new_protocol; @@ -28,7 +53,12 @@ struct ProtocolLink : kf::mixin::NonCopyable { return; } - _protocol->poll(now, input, transport_link); + if (_poll_timer.expired(now) or _poll_timer_reset_required) { + _poll_timer.start(now); + _poll_timer_reset_required = false; + + _protocol->poll(now, input, transport_link); + } } void receive(kf::memory::Slice buffer) noexcept { @@ -44,6 +74,8 @@ struct ProtocolLink : kf::mixin::NonCopyable { static constexpr auto logger{kf::Logger::create("ProtocolLink")}; Protocol *_protocol{nullptr}; + kf::math::Timer _poll_timer{this->config().poll_period}; + bool _poll_timer_reset_required{true}; }; }// namespace djc::protocol \ No newline at end of file From 9e0e7ee107e352af9e09191d107359aa012dda68 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 11:37:49 +0300 Subject: [PATCH 061/326] Refactor: Integrate Protocols into Control Fixes #20 --- DJC-Firmware/src/djc/Control.hpp | 205 ++----------------------------- 1 file changed, 11 insertions(+), 194 deletions(-) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/Control.hpp index 52c8cd3..1555070 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/Control.hpp @@ -3,223 +3,40 @@ #pragma once -#include - -#include - -#include -#include -#include -#include -#include #include -#include -#include -#include -#include #include #include +#include "djc/ManualInput.hpp" +#include "djc/protocol/ProtocolLink.hpp" #include "djc/transport/TransportLink.hpp" namespace djc { -namespace internal { - -enum class ControlMode : kf::u8 { - Raw, - MavLink, -}; - -struct ControlConfig final : kf::mixin::NonCopyable { - kf::math::Milliseconds heartbeat_period; - kf::math::Milliseconds poll_period; - ControlMode init_mode; - - static constexpr ControlConfig defaults() noexcept { - return ControlConfig{ - .heartbeat_period = 2000, // ms - .poll_period = static_cast(1000 / 50),// 50 Hz - .init_mode = ControlMode::MavLink, - }; - } -}; - -}// namespace internal - -struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable, kf::mixin::Configurable, kf::mixin::Initable { - using Config = internal::ControlConfig; - using Mode = internal::ControlMode; - - using RawMessageCallback = kf::Function)>; - using MavLinkMessageCallback = kf::Function; - - struct Input { - using Unit = kf::i16; - - static constexpr Unit scale{1000}; - - Unit left_x, left_y, right_x, right_y; - - static constexpr Unit fromNormalized(kf::f32 value) noexcept { return static_cast(value * scale); } - }; - - static constexpr kf::u8 mavlink_system_id{127}, mavlink_target_id{1}; +struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable { - [[nodiscard]] static constexpr kf::memory::StringView stringFromMode(Mode mode) noexcept { return (mode == Mode::Raw) ? "Raw" : "MavLink"; } + explicit Control(transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : + _transport_link{transport_link}, _protocol_link{protocol_link} {} - explicit Control(const Config &config, transport::TransportLink &transport_link) noexcept : - kf::mixin::Configurable{config}, _transport_link{transport_link} {} + [[nodiscard]] const ManualInput &input() const noexcept { return _manual_input; } - // properties - - void onRawMessage(RawMessageCallback &&callback) noexcept { _raw_message_callback = std::move(callback); } - - void onMavlinkMessage(MavLinkMessageCallback &&callback) noexcept { _mavlink_message_callback = std::move(callback); } - - [[nodiscard]] Mode mode() const noexcept { return _mode; } - - void mode(Mode new_mode) noexcept { _mode = new_mode; } - - [[nodiscard]] const Input &input() const noexcept { return _input; } - - void input(const Input &new_input) noexcept { _input = new_input; } + void input(const ManualInput &new_input) noexcept { _manual_input = new_input; } [[nodiscard]] bool enabled() const noexcept { return _enabled; } void enabled(bool is_enabled) noexcept { _enabled = is_enabled; } - // methods - - void sendMavLinkMessage(const mavlink_message_t *message) noexcept { - kf::u8 buffer[MAVLINK_MAX_PACKET_LEN]; - const auto len = mavlink_msg_to_send_buffer(buffer, message); - - (void) _transport_link.send({buffer, len}); - } - - void sendRawMessage(kf::memory::Slice buffer) noexcept { - (void) _transport_link.send(buffer); - } - private: - using LogString = kf::memory::ArrayString<64>; - - static constexpr auto logger{kf::Logger::create("Control")}; - - RawMessageCallback _raw_message_callback{}; - MavLinkMessageCallback _mavlink_message_callback{}; - transport::TransportLink &_transport_link; - - kf::math::Timer _poll_timer{this->config().poll_period}; - kf::math::Timer _heartbear_timer{this->config().heartbeat_period}; - - Input _input{}; - Mode _mode{this->config().init_mode}; + protocol::ProtocolLink &_protocol_link; + ManualInput _manual_input{}; bool _enabled{false}; - void onReceiveRaw(kf::memory::Slice buffer) noexcept { - if (_raw_message_callback) { _raw_message_callback(buffer); } - } - - void onReceiveMavLink(kf::memory::Slice buffer) noexcept { - if (not _mavlink_message_callback) { return; } - - mavlink_message_t message; - mavlink_status_t status; - - for (auto b: buffer) { - if (mavlink_parse_char(MAVLINK_COMM_0, b, &message, &status) != 0) { - _mavlink_message_callback(&message); - } - } - } - - void pollRaw(kf::math::Milliseconds) noexcept { - (void) _transport_link.send({reinterpret_cast(&_input), sizeof(_input)}); - } - - void pollMavLink(kf::math::Milliseconds now) noexcept { - sendMavLinkControl(); - - if (_heartbear_timer.expired(now)) { - _heartbear_timer.start(now); - sendMavLinkHeartbeat(); - } - } - - void sendMavLinkControl() noexcept { - mavlink_message_t message; - (void) mavlink_msg_manual_control_pack( - mavlink_system_id, MAV_COMP_ID_PARACHUTE, &message, mavlink_target_id, - _input.right_y,// x: pitch (right Y) - _input.right_x,// y: roll (right X) - _input.left_y, // z: thrust (left Y) - _input.left_x, // r: yaw (left X) - // Buttons (unused) - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - - sendMavLinkMessage(&message); - } - - void sendMavLinkHeartbeat() noexcept { - mavlink_message_t message; - (void) mavlink_msg_heartbeat_pack( - mavlink_system_id,// System ID - MAV_COMP_ID_OSD, // Component ID - &message, - MAV_TYPE_QUADROTOR, - MAV_AUTOPILOT_GENERIC, - 0, 0, 0// Base mode, Custom mode, system status - ); - - sendMavLinkMessage(&message); - } - // impl - - KF_IMPL_INITABLE(Control, void); - void initImpl() noexcept { - logger.info("init"); - - _mode = this->config().init_mode; - logger.debug(stringFromMode(_mode)); - - const auto now = millis(); - _poll_timer.start(now); - _heartbear_timer.start(now); - - _transport_link.onReceive([this](const transport::PeerAddress &, kf::memory::Slice buffer) { - switch (_mode) { - case Mode::Raw: - onReceiveRaw(buffer); - return; - - case Mode::MavLink: - onReceiveMavLink(buffer); - return; - } - }); - } - KF_IMPL_TIMED_POLLABLE(Control); void pollImpl(kf::math::Milliseconds now) noexcept { - if (not _enabled) { return; } - if (not _transport_link.connected()) { return; } - - if (_poll_timer.expired(now)) { - _poll_timer.start(now); - - switch (_mode) { - case Mode::Raw: - pollRaw(now); - return; - - case Mode::MavLink: - pollMavLink(now); - return; - } + if (_enabled and _transport_link.connected()) { + _protocol_link.poll(now, _manual_input, _transport_link); } } }; From 4d75a539a1341b864bb11ed92b4b4406358c956b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 11:44:16 +0300 Subject: [PATCH 062/326] update: device Config --- DJC-Firmware/src/djc/Config.hpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index c2deb85..26f044b 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -7,11 +7,12 @@ #include #include -#include "djc/Control.hpp" #include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" #include "djc/input/InputHandler.hpp" #include "djc/memory/Box.hpp" +#include "djc/protocol/ProtocolLink.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" @@ -26,17 +27,20 @@ struct Config { using PeerFavoritesConfig = djc::memory::Box; - static constexpr auto latest_version{5}; + static constexpr auto latest_version{6}; kf::u16 version; + kf::memory::Array device_name; + PeerFavoritesConfig peer_favorites; Periphery::Config periphery; InputHandler::Config input_handler; + transport::TransportLink::Config transport_link; - Control::Config control; PeerScanner::Config peer_scanner; - PeerFavoritesConfig peer_favorites; - kf::memory::Array device_name; + + protocol::ProtocolLink::Config protocol_link; + protocol::ProtocolRegistry::Config protocol_registry; [[nodiscard]] constexpr kf::memory::StringView deviceName() const noexcept { return kf::memory::StringView{device_name.data(), device_name.size()}; @@ -47,13 +51,17 @@ struct Config { static constexpr Config defaults() noexcept { return Config{ .version = latest_version, + .device_name = {"ESP32-DJC"}, + .peer_favorites = PeerFavoritesConfig::defaults(), + .periphery = Periphery::Config::defaults(), .input_handler = InputHandler::Config::defaults(), + .transport_link = transport::TransportLink::Config::defaults(), - .control = Control::Config::defaults(), .peer_scanner = PeerScanner::Config::defaults(), - .peer_favorites = PeerFavoritesConfig::defaults(), - .device_name = {"ESP32-DJC"}, + + .protocol_link = protocol::ProtocolLink::Config::defaults(), + .protocol_registry = protocol::ProtocolRegistry::Config::defaults(), }; } }; From 47e0e0a6b2602de62c48c0549ea64ca09c5bc4ad Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 11:52:51 +0300 Subject: [PATCH 063/326] add: Config: protocol init mode --- DJC-Firmware/src/djc/Config.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 26f044b..b4b341d 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -30,6 +30,8 @@ struct Config { static constexpr auto latest_version{6}; kf::u16 version; + + protocol::ProtocolRegistry::Mode init_mode; kf::memory::Array device_name; PeerFavoritesConfig peer_favorites; @@ -51,6 +53,8 @@ struct Config { static constexpr Config defaults() noexcept { return Config{ .version = latest_version, + + .init_mode = protocol::ProtocolRegistry::Mode::Mavlink, .device_name = {"ESP32-DJC"}, .peer_favorites = PeerFavoritesConfig::defaults(), From bc04e66f96cf105f557b9e203aec34a31011a503 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 11:55:33 +0300 Subject: [PATCH 064/326] refactor: rename Config::init_protocol_mode --- DJC-Firmware/src/djc/Config.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index b4b341d..1397b26 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -30,8 +30,8 @@ struct Config { static constexpr auto latest_version{6}; kf::u16 version; - - protocol::ProtocolRegistry::Mode init_mode; + + protocol::ProtocolRegistry::Mode init_protocol_mode; kf::memory::Array device_name; PeerFavoritesConfig peer_favorites; @@ -54,7 +54,7 @@ struct Config { return Config{ .version = latest_version, - .init_mode = protocol::ProtocolRegistry::Mode::Mavlink, + .init_protocol_mode = protocol::ProtocolRegistry::Mode::Mavlink, .device_name = {"ESP32-DJC"}, .peer_favorites = PeerFavoritesConfig::defaults(), From 79a0dd4fdf44724ffb586d47bac09d5298ef60d8 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 12:00:18 +0300 Subject: [PATCH 065/326] fix: MavlinkProtocol::poll: add explicit discard --- DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp index 8b387c0..34ff6b6 100644 --- a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp @@ -80,10 +80,10 @@ struct MavlinkProtocol : _heartbeat_timer_reset_required = false; _heartbear_timer.start(now); - sendHeartbeat(transport_link); + (void) sendHeartbeat(transport_link); } - sendManualControl(transport_link, input); + (void) sendManualControl(transport_link, input); } void receive(kf::memory::Slice buffer) noexcept override { From e150122eb9440512abcba4fc6cdad992e7d65540 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 12:08:53 +0300 Subject: [PATCH 066/326] update: ConfigPage --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index dc4da96..7564797 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -6,6 +6,7 @@ #include #include "djc/ConfigManager.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/widgets/TextInput.hpp" @@ -18,7 +19,7 @@ struct ConfigPage : UI::Page { _layout{{ &root.link(), &_device_name_input, - &_init_mode_selector_label, + &_labeled_default_protocol_mode_selector, &_save_storage, &_load_storage, &_reset_storage, @@ -39,13 +40,14 @@ struct ConfigPage : UI::Page { storage.reset(); }); - _init_mode_selector.callback([](Control::Mode init_mode) { - storage.config().control.init_mode = init_mode; + _default_protocol_mode_selector.callback([](Mode mode) { + storage.config().init_protocol_mode = mode; }); } private: - using ControlModeSelectWidget = UI::ComboBox; + using Mode = protocol::ProtocolRegistry::Mode; + using ProtocolModeSelector = UI::ComboBox; inline static auto &storage{djc::ConfigManager::instance()}; @@ -53,24 +55,24 @@ struct ConfigPage : UI::Page { widgets::TextInput _device_name_input{}; UI::Button _save_storage{"Save"}; UI::Button _load_storage{"Load"}; - UI::Button _reset_storage{"Reset"}; + UI::Button _reset_storage{"Reset (RAM)"}; - kf::memory::Array _control_mode_options{{ - {Control::stringFromMode(Control::Mode::MavLink), Control::Mode::MavLink}, - {Control::stringFromMode(Control::Mode::Raw), Control::Mode::Raw}, + kf::memory::Array _control_mode_options{{ + {protocol::ProtocolRegistry::stringFromMode(Mode::Mavlink), Mode::Mavlink}, + {protocol::ProtocolRegistry::stringFromMode(Mode::Raw), Mode::Raw}, }}; - ControlModeSelectWidget::Config _control_mode_config{ + ProtocolModeSelector::Config _control_mode_config{ .items = {_control_mode_options.data(), _control_mode_options.size()}, }; // TODO: set init value from storage - ControlModeSelectWidget _init_mode_selector{_control_mode_config}; + ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config}; - UI::Labeled _init_mode_selector_label{"Init Control", _init_mode_selector}; + UI::Labeled _labeled_default_protocol_mode_selector{"Default Protocol", _default_protocol_mode_selector}; // layout kf::memory::Array _layout; }; -}// namespace djc::ui::pages +}// namespace djc::ui::pages \ No newline at end of file From 218dd7629a90ac9e161bddbfa795975eb26a3cc2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 12:10:56 +0300 Subject: [PATCH 067/326] rename: MavlinkTelemetryPage --- ...{MavLinkTelemetryPage.hpp => MavlinkTelemetryPage.hpp} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename DJC-Firmware/src/djc/ui/pages/{MavLinkTelemetryPage.hpp => MavlinkTelemetryPage.hpp} (93%) diff --git a/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp similarity index 93% rename from DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp rename to DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index d5fd0ff..851737e 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavLinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -17,9 +17,9 @@ namespace djc::ui::pages { /// @brief MAVLink telemetry page -struct MavLinkTelemetryPage : UI::Page { - explicit MavLinkTelemetryPage(UI::Page &root, Control &control) noexcept : - Page{"MAV Link: Telemetry"}, _control{control}, +struct MavlinkTelemetryPage : UI::Page { + explicit MavlinkTelemetryPage(UI::Page &root, Control &control) noexcept : + Page{"Mavlink: Telemetry"}, _control{control}, _layout{{ &root.link(), &_imu_display, @@ -47,7 +47,7 @@ struct MavLinkTelemetryPage : UI::Page { } private: - static constexpr auto logger{kf::Logger::create("MavLinkTelemetryPage")}; + static constexpr auto logger{kf::Logger::create("MavlinkTelemetryPage")}; Control &_control; bool _need_update{false}; From 7588991298625f99574d7840f487cfffaa3972c5 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 12:22:16 +0300 Subject: [PATCH 068/326] update: MavlinkTelemetryPage --- .../src/djc/ui/pages/MavlinkTelemetryPage.hpp | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 851737e..8417f23 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -11,15 +11,18 @@ #include #include -#include "djc/Control.hpp" +#include "djc/protocol/MavlinkProtocol.hpp" +#include "djc/protocol/ProtocolLink.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" #include "djc/ui/UI.hpp" namespace djc::ui::pages { /// @brief MAVLink telemetry page struct MavlinkTelemetryPage : UI::Page { - explicit MavlinkTelemetryPage(UI::Page &root, Control &control) noexcept : - Page{"Mavlink: Telemetry"}, _control{control}, + explicit MavlinkTelemetryPage(UI::Page &root, protocol::ProtocolRegistry &protocol_registry, protocol::ProtocolLink &protocol_link) noexcept : + Page{"Mavlink: Telemetry"}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, + _layout{{ &root.link(), &_imu_display, @@ -29,14 +32,14 @@ struct MavlinkTelemetryPage : UI::Page { } void onEntry() noexcept override { - _control.mode(Control::Mode::MavLink); - _control.onMavlinkMessage([this](const mavlink_message_t *message) { - _need_update |= onMavLinkMessage(message); + _protocol_registry.mavlink().callback([this](const mavlink_message_t &message) { + _need_update |= onMavlinkMessage(message); }); + _protocol_link.protocol(_protocol_registry.mavlink()); } void onExit() noexcept override { - _control.onMavlinkMessage(Control::MavLinkMessageCallback{nullptr}); + _protocol_registry.mavlink().callback(protocol::MavlinkProtocol::CallbackType{nullptr}); } void onUpdate(kf::math::Milliseconds now) noexcept override { @@ -49,8 +52,9 @@ struct MavlinkTelemetryPage : UI::Page { private: static constexpr auto logger{kf::Logger::create("MavlinkTelemetryPage")}; - Control &_control; - bool _need_update{false}; + protocol::ProtocolRegistry &_protocol_registry; + protocol::ProtocolLink &_protocol_link; + bool _need_update{false}; // widgets @@ -62,11 +66,11 @@ struct MavlinkTelemetryPage : UI::Page { kf::memory::Array _layout; - [[nodiscard]] bool onMavLinkMessage(const mavlink_message_t *message) noexcept { - switch (message->msgid) { + [[nodiscard]] bool onMavlinkMessage(const mavlink_message_t &message) noexcept { + switch (message.msgid) { case MAVLINK_MSG_ID_ATTITUDE_QUATERNION: { mavlink_attitude_quaternion_t attitude_quaternion; - mavlink_msg_attitude_quaternion_decode(message, &attitude_quaternion); + mavlink_msg_attitude_quaternion_decode(&message, &attitude_quaternion); (void) _attitude_buffer.format( "AtQ %+.2f %+.2f %+.2f %+.2f", @@ -81,11 +85,11 @@ struct MavlinkTelemetryPage : UI::Page { case MAVLINK_MSG_ID_SERIAL_CONTROL: { mavlink_serial_control_t serial_control; - mavlink_msg_serial_control_decode(message, &serial_control); + mavlink_msg_serial_control_decode(&message, &serial_control); constexpr auto len{sizeof(serial_control.data)}; serial_control.data[len - 1] = '\0'; - + logger.info({reinterpret_cast(serial_control.data), static_cast(serial_control.count)}); return false; @@ -93,7 +97,7 @@ struct MavlinkTelemetryPage : UI::Page { case MAVLINK_MSG_ID_SCALED_IMU: { mavlink_scaled_imu_t imu; - mavlink_msg_scaled_imu_decode(message, &imu); + mavlink_msg_scaled_imu_decode(&message, &imu); (void) _imu_display_buffer.format( "Acc %+.3f %+.3f %+.3f", From f0d64bd72107b24a70f24d4f4cff8f9c9c457465 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 12:30:45 +0300 Subject: [PATCH 069/326] update: ProtocolRegistry::get --- DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp index a420406..1b8756a 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -46,8 +46,12 @@ struct ProtocolRegistry final : [[nodiscard]] Protocol &get(Mode mode) noexcept { switch (mode) { - case Mode::Raw: return _raw_protocol; - case Mode::Mavlink: return _mavlink_protocol; + case Mode::Mavlink: + return _mavlink_protocol; + + case Mode::Raw: + default: + return _raw_protocol; } } From 6c54045a7034ba67996fce3a644ed70ac42301b2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 12:53:09 +0300 Subject: [PATCH 070/326] update: RawProtocolPage --- ...RawControlPage.hpp => RawProtocolPage.hpp} | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) rename DJC-Firmware/src/djc/ui/pages/{RawControlPage.hpp => RawProtocolPage.hpp} (54%) diff --git a/DJC-Firmware/src/djc/ui/pages/RawControlPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp similarity index 54% rename from DJC-Firmware/src/djc/ui/pages/RawControlPage.hpp rename to DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index 6750ece..68c875b 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawControlPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -7,15 +7,22 @@ #include #include -#include "djc/Control.hpp" +#include "djc/protocol/ProtocolLink.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" +#include "djc/protocol/RawProtocol.hpp" +#include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/widgets/TextInput.hpp" namespace djc::ui::pages { -struct RawControlPage : UI::Page { - explicit RawControlPage(UI::Page &root, Control &control) noexcept : - Page{"Raw Control"}, _control{control}, +struct RawProtocolPage : UI::Page { + explicit RawProtocolPage( + UI::Page &root, + protocol::ProtocolRegistry &protocol_registry, + protocol::ProtocolLink &protocol_link, + transport::TransportLink &transport_link) noexcept : + Page{"Raw Protocol"}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, _transport_link{transport_link}, _layout{{ &root.link(), &_message_input, @@ -23,19 +30,20 @@ struct RawControlPage : UI::Page { }} { widgets({_layout.data(), _layout.size()}); - _send_button.callback([this]() { + _send_button.callback([this]() { kf::memory::StringView s{_message.data(), _message.size()}; s = s.sub(0, s.find('\0').valueOr(s.size())); logger.debug(s); - _control.sendRawMessage({reinterpret_cast(s.data()), s.size()}); + (void) _transport_link.send({reinterpret_cast(s.data()), s.size()}); }); } void onEntry() noexcept override { - _control.mode(Control::Mode::Raw); - _control.onRawMessage([](kf::memory::Slice buffer) { + _protocol_link.protocol(_protocol_registry.raw()); + + _protocol_registry.raw().callback([](kf::memory::Slice buffer) { logger.info( kf::memory::ArrayString<64>::formatted( "Got %d bytes from primary peer", @@ -45,14 +53,16 @@ struct RawControlPage : UI::Page { } void onExit() noexcept override { - _control.onRawMessage(Control::RawMessageCallback{nullptr}); + _protocol_registry.raw().callback(protocol::RawProtocol::CallbackType{}); } private: static constexpr auto logger{kf::Logger::create("RawControlPage")}; kf::memory::Array _message{}; - Control &_control; + protocol::ProtocolRegistry &_protocol_registry; + protocol::ProtocolLink &_protocol_link; + transport::TransportLink &_transport_link; // widgets From 37a1fb7b4e606100d8b6e545b7c114f8bca8dcb2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 12:53:59 +0300 Subject: [PATCH 071/326] update: ConfigPage some labels --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 7564797..2d8dedc 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -55,7 +55,7 @@ struct ConfigPage : UI::Page { widgets::TextInput _device_name_input{}; UI::Button _save_storage{"Save"}; UI::Button _load_storage{"Load"}; - UI::Button _reset_storage{"Reset (RAM)"}; + UI::Button _reset_storage{"Reset (RAM cache)"}; kf::memory::Array _control_mode_options{{ {protocol::ProtocolRegistry::stringFromMode(Mode::Mavlink), Mode::Mavlink}, @@ -69,7 +69,7 @@ struct ConfigPage : UI::Page { // TODO: set init value from storage ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config}; - UI::Labeled _labeled_default_protocol_mode_selector{"Default Protocol", _default_protocol_mode_selector}; + UI::Labeled _labeled_default_protocol_mode_selector{"Init Protocol", _default_protocol_mode_selector}; // layout kf::memory::Array _layout; From 327915b157a29f9a415e6d6d70930bd737b224df Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 12:56:07 +0300 Subject: [PATCH 072/326] Epic: Protocol Abstraction (Raw & MavLink) Fixes #9 --- DJC-Firmware/src/main.cpp | 47 ++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index ea2a09c..446920e 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -1,25 +1,35 @@ // Copyright (c) 2026 KiraFlux // SPDX-License-Identifier: GPL-3.0-or-later +// framework #include +// lib #include #include #include +// djc #include "djc/ConfigManager.hpp" #include "djc/Control.hpp" #include "djc/DisplayManager.hpp" +#include "djc/ManualInput.hpp" #include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" + #include "djc/input/InputHandler.hpp" #include "djc/input/VirtualKeyboard.hpp" + +#include "djc/protocol/ProtocolLink.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" + #include "djc/transport/EspNowTransport.hpp" #include "djc/transport/TransportLink.hpp" + #include "djc/ui/pages/ConfigPage.hpp" -#include "djc/ui/pages/MavLinkTelemetryPage.hpp" +#include "djc/ui/pages/MavlinkTelemetryPage.hpp" #include "djc/ui/pages/PeerExplorerPage.hpp" -#include "djc/ui/pages/RawControlPage.hpp" +#include "djc/ui/pages/RawProtocolPage.hpp" #include "djc/ui/pages/RootPage.hpp" // services @@ -52,9 +62,17 @@ static djc::PeerScanner peer_scanner{ transport_link, }; +static djc::protocol::ProtocolLink protocol_link{ + storage.config().protocol_link, +}; + +static djc::protocol::ProtocolRegistry protocol_registry{ + storage.config().protocol_registry, +}; + static djc::Control control{ - storage.config().control, transport_link, + protocol_link, }; static djc::DisplayManager display_manager{ @@ -67,14 +85,17 @@ static djc::DisplayManager display_manager{ static djc::ui::pages::RootPage root_page{}; -static djc::ui::pages::MavLinkTelemetryPage mavlink_telemetry_page{ +static djc::ui::pages::MavlinkTelemetryPage mavlink_telemetry_page{ root_page, - control, + protocol_registry, + protocol_link, }; -static djc::ui::pages::RawControlPage raw_control_page{ +static djc::ui::pages::RawProtocolPage raw_protocol_page{ root_page, - control, + protocol_registry, + protocol_link, + transport_link, }; static djc::ui::pages::PeerExplorerPage peer_explorer_page{ @@ -115,9 +136,13 @@ void setup() { logger.error("failed to initialize espnow transport"); } - peer_scanner.init(); + protocol_link.protocol(protocol_registry.get(storage.config().init_protocol_mode)); + + transport_link.onReceive([](const djc::transport::PeerAddress &, kf::memory::Slice buffer){ + protocol_link.receive(buffer); + }); - control.init(); + peer_scanner.init(); { using E = djc::ui::UI::Event; @@ -161,7 +186,7 @@ void setup() { // apply page links root_page.attach(mavlink_telemetry_page); - root_page.attach(raw_control_page); + root_page.attach(raw_protocol_page); root_page.attach(peer_explorer_page); root_page.attach(config_page); @@ -182,7 +207,7 @@ void loop() { peer_scanner.poll(now); if (control.enabled()) { - using I = djc::Control::Input; + using I = djc::ManualInput; const I control_input{ .left_x = I::fromNormalized(periphery.left_joystick.axis_x.read()), From 5fc7e3cedb1c7c1ebd4d4affe859751ed777714f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 13:12:21 +0300 Subject: [PATCH 073/326] minor enchantments --- .../src/djc/ui/pages/MavlinkTelemetryPage.hpp | 2 +- DJC-Firmware/src/main.cpp | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 8417f23..b4365f3 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -39,7 +39,7 @@ struct MavlinkTelemetryPage : UI::Page { } void onExit() noexcept override { - _protocol_registry.mavlink().callback(protocol::MavlinkProtocol::CallbackType{nullptr}); + _protocol_registry.mavlink().callback(protocol::MavlinkProtocol::CallbackType{}); } void onUpdate(kf::math::Milliseconds now) noexcept override { diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 446920e..c2a7cf7 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -57,11 +57,6 @@ static djc::transport::TransportLink transport_link{ storage.config().transport_link, }; -static djc::PeerScanner peer_scanner{ - storage.config().peer_scanner, - transport_link, -}; - static djc::protocol::ProtocolLink protocol_link{ storage.config().protocol_link, }; @@ -70,6 +65,11 @@ static djc::protocol::ProtocolRegistry protocol_registry{ storage.config().protocol_registry, }; +static djc::PeerScanner peer_scanner{ + storage.config().peer_scanner, + transport_link, +}; + static djc::Control control{ transport_link, protocol_link, @@ -85,6 +85,12 @@ static djc::DisplayManager display_manager{ static djc::ui::pages::RootPage root_page{}; +static djc::ui::pages::PeerExplorerPage peer_explorer_page{ + root_page, + peer_scanner, + transport_link, +}; + static djc::ui::pages::MavlinkTelemetryPage mavlink_telemetry_page{ root_page, protocol_registry, @@ -98,12 +104,6 @@ static djc::ui::pages::RawProtocolPage raw_protocol_page{ transport_link, }; -static djc::ui::pages::PeerExplorerPage peer_explorer_page{ - root_page, - peer_scanner, - transport_link, -}; - static djc::ui::pages::ConfigPage config_page{ root_page, }; @@ -185,9 +185,9 @@ void setup() { }); // apply page links + root_page.attach(peer_explorer_page); root_page.attach(mavlink_telemetry_page); root_page.attach(raw_protocol_page); - root_page.attach(peer_explorer_page); root_page.attach(config_page); ui.bindPage(root_page); @@ -201,7 +201,7 @@ void loop() { constexpr kf::math::Milliseconds loop_period{1000 / 50};// 50 Hz delay(loop_period); - const auto now = millis(); + const auto now = static_cast(millis()); input_handler.poll(now); transport_link.poll(now); peer_scanner.poll(now); From c82874da14eadffc234214476ba12cf955226f26 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 4 May 2026 13:42:49 +0300 Subject: [PATCH 074/326] =?UTF-8?q?Feature:=20MavlinkTelemetryRegistry=20?= =?UTF-8?q?=E2=80=93=20Centralized=20MAVLink=20Data=20Fixes=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/djc/MavlinkTelemetryRegistry.hpp | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp diff --git a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp new file mode 100644 index 0000000..80de6e6 --- /dev/null +++ b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp @@ -0,0 +1,51 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include + +namespace djc { + +struct MavlinkTelemetryRegistry final : kf::mixin::NonCopyable { + + kf::math::Milliseconds last_update_time; + mavlink_heartbeat_t heartbeat; + mavlink_attitude_quaternion_t attitude_quaternion; + mavlink_serial_control_t serial_control; + mavlink_scaled_imu_t scaled_imu; + + void update(kf::math::Milliseconds now, const mavlink_message_t &message) noexcept { + constexpr auto serial_control_max_len{sizeof(mavlink_serial_control_t::data)}; + + last_update_time = now; + + switch (message.msgid) { + case MAVLINK_MSG_ID_HEARTBEAT: + mavlink_msg_heartbeat_decode(&message, &heartbeat); + return; + + case MAVLINK_MSG_ID_ATTITUDE_QUATERNION: + mavlink_msg_attitude_quaternion_decode(&message, &attitude_quaternion); + return; + + case MAVLINK_MSG_ID_SERIAL_CONTROL: + mavlink_msg_serial_control_decode(&message, &serial_control); + serial_control.data[serial_control_max_len - 1] = '\0'; + return; + + case MAVLINK_MSG_ID_SCALED_IMU: + mavlink_msg_scaled_imu_decode(&message, &scaled_imu); + return; + } + } + + [[nodiscard]] bool updated(kf::math::Milliseconds last_seen_time) const noexcept { + return last_seen_time < last_update_time; + } +}; + +}// namespace djc \ No newline at end of file From b52bac16bff36ba86f70e738697981023f827b78 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 5 May 2026 09:46:26 +0300 Subject: [PATCH 075/326] =?UTF-8?q?update:=20Feature:=20TelemetryRegistry?= =?UTF-8?q?=20=E2=80=93=20Centralized=20MAVLink=20Data=20Fixes=20#10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/djc/MavlinkTelemetryRegistry.hpp | 58 +++++++++++++------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp index 80de6e6..3f8ba8b 100644 --- a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp +++ b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp @@ -10,42 +10,66 @@ namespace djc { +/// @brief Centralised storage for received MAVLink telemetry messages +/// @note Holds a set of typed entries, one for each supported MAVLink message type. struct MavlinkTelemetryRegistry final : kf::mixin::NonCopyable { - kf::math::Milliseconds last_update_time; - mavlink_heartbeat_t heartbeat; - mavlink_attitude_quaternion_t attitude_quaternion; - mavlink_serial_control_t serial_control; - mavlink_scaled_imu_t scaled_imu; + /// @brief Typed telemetry entry with own decoder and freshness tracking + template struct Entry : kf::mixin::NonCopyable { - void update(kf::math::Milliseconds now, const mavlink_message_t &message) noexcept { - constexpr auto serial_control_max_len{sizeof(mavlink_serial_control_t::data)}; + using ValueType = T; + using MessageDecoder = void (*)(const mavlink_message_t *message, ValueType *); + + explicit constexpr Entry(MessageDecoder message_decoder) noexcept : + _message_decoder{message_decoder} {} + + [[nodiscard]] const ValueType &value() const noexcept { return _value; } - last_update_time = now; + /// @brief Check if the entry was updated after the given timestamp + [[nodiscard]] bool updatedSince(kf::math::Milliseconds since) const noexcept { + return _last_update_time > since; + } + + /// @brief Decode a MAVLink message into this entry and record the update time + void update(kf::math::Milliseconds now, const mavlink_message_t &message) noexcept { + if (nullptr != _message_decoder) { + _message_decoder(&message, &_value); + _last_update_time = now; + } + } + private: + const MessageDecoder _message_decoder; + ValueType _value{}; + kf::math::Milliseconds _last_update_time{0u}; + }; + + Entry heartbeat{mavlink_msg_heartbeat_decode}; + Entry attitude_quaternion{mavlink_msg_attitude_quaternion_decode}; + Entry serial_control{mavlink_msg_serial_control_decode}; + Entry scaled_imu{mavlink_msg_scaled_imu_decode}; + + /// @brief Route an incoming MAVLink message to the correct entry based on its ID + /// @note Only known message types are dispatched; unknown IDs are silently ignored. + void update(kf::math::Milliseconds now, const mavlink_message_t &message) noexcept { switch (message.msgid) { case MAVLINK_MSG_ID_HEARTBEAT: - mavlink_msg_heartbeat_decode(&message, &heartbeat); + heartbeat.update(now, message); return; case MAVLINK_MSG_ID_ATTITUDE_QUATERNION: - mavlink_msg_attitude_quaternion_decode(&message, &attitude_quaternion); + attitude_quaternion.update(now, message); return; case MAVLINK_MSG_ID_SERIAL_CONTROL: - mavlink_msg_serial_control_decode(&message, &serial_control); - serial_control.data[serial_control_max_len - 1] = '\0'; + serial_control.update(now, message); return; case MAVLINK_MSG_ID_SCALED_IMU: - mavlink_msg_scaled_imu_decode(&message, &scaled_imu); + scaled_imu.update(now, message); return; } } - - [[nodiscard]] bool updated(kf::math::Milliseconds last_seen_time) const noexcept { - return last_seen_time < last_update_time; - } }; }// namespace djc \ No newline at end of file From b8caf9071da807f4b80110a7b340d89e0c5d99ec Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 5 May 2026 10:20:26 +0300 Subject: [PATCH 076/326] update: MavlinkTelemetryPage --- .../src/djc/ui/pages/MavlinkTelemetryPage.hpp | 120 ++++++++---------- 1 file changed, 54 insertions(+), 66 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index b4365f3..925edbc 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -11,6 +11,7 @@ #include #include +#include "djc/MavlinkTelemetryRegistry.hpp" #include "djc/protocol/MavlinkProtocol.hpp" #include "djc/protocol/ProtocolLink.hpp" #include "djc/protocol/ProtocolRegistry.hpp" @@ -20,8 +21,15 @@ namespace djc::ui::pages { /// @brief MAVLink telemetry page struct MavlinkTelemetryPage : UI::Page { - explicit MavlinkTelemetryPage(UI::Page &root, protocol::ProtocolRegistry &protocol_registry, protocol::ProtocolLink &protocol_link) noexcept : - Page{"Mavlink: Telemetry"}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, + explicit MavlinkTelemetryPage( + UI::Page &root, + protocol::ProtocolRegistry &protocol_registry, + protocol::ProtocolLink &protocol_link, + MavlinkTelemetryRegistry &mavlink_telemetry_registry) noexcept : + Page{"Mavlink: Telemetry"}, + _protocol_registry{protocol_registry}, + _protocol_link{protocol_link}, + _mavlink_telemetry_registry{mavlink_telemetry_registry}, _layout{{ &root.link(), @@ -32,88 +40,68 @@ struct MavlinkTelemetryPage : UI::Page { } void onEntry() noexcept override { - _protocol_registry.mavlink().callback([this](const mavlink_message_t &message) { - _need_update |= onMavlinkMessage(message); - }); _protocol_link.protocol(_protocol_registry.mavlink()); - } - - void onExit() noexcept override { - _protocol_registry.mavlink().callback(protocol::MavlinkProtocol::CallbackType{}); + _last_imu = _last_attitude = _last_serial_control = 0; } void onUpdate(kf::math::Milliseconds now) noexcept override { - if (_need_update) { - _need_update = false; - UI::instance().addEvent(UI::Event::update()); - } - } - -private: - static constexpr auto logger{kf::Logger::create("MavlinkTelemetryPage")}; + bool need_update{false}; - protocol::ProtocolRegistry &_protocol_registry; - protocol::ProtocolLink &_protocol_link; - bool _need_update{false}; + if (_mavlink_telemetry_registry.scaled_imu.updatedSince(_last_imu)) { + _last_imu = now; + const auto &imu = _mavlink_telemetry_registry.scaled_imu.value(); - // widgets + (void) _imu_buffer.format( + "Acc %+.3f %+.3f %+.3f", + float(imu.xacc * 0.001f), + float(imu.yacc * 0.001f), + float(imu.zacc * 0.001f)); + _imu_display.value(_imu_buffer.view()); - kf::memory::ArrayString<64> _attitude_buffer{"..."}; - kf::memory::ArrayString<64> _imu_display_buffer{"..."}; - - UI::Display _attitude_display{_attitude_buffer.view()}; - UI::Display _imu_display{_imu_display_buffer.view()}; - - kf::memory::Array _layout; + need_update = true; + } - [[nodiscard]] bool onMavlinkMessage(const mavlink_message_t &message) noexcept { - switch (message.msgid) { - case MAVLINK_MSG_ID_ATTITUDE_QUATERNION: { - mavlink_attitude_quaternion_t attitude_quaternion; - mavlink_msg_attitude_quaternion_decode(&message, &attitude_quaternion); + if (_mavlink_telemetry_registry.attitude_quaternion.updatedSince(_last_attitude)) { + _last_attitude = now; + const auto &attitude = _mavlink_telemetry_registry.attitude_quaternion.value(); - (void) _attitude_buffer.format( - "AtQ %+.2f %+.2f %+.2f %+.2f", - float(attitude_quaternion.q1), - float(attitude_quaternion.q2), - float(attitude_quaternion.q3), - float(attitude_quaternion.q4)); - _attitude_display.value(_attitude_buffer.view()); + (void) _attitude_buffer.format( + "Q %+.2f %+.2f %+.2f %+.2f", + attitude.q1, + attitude.q2, + attitude.q3, + attitude.q4); + _attitude_display.value(_attitude_buffer.view()); - return true; - } + need_update = true; + } - case MAVLINK_MSG_ID_SERIAL_CONTROL: { - mavlink_serial_control_t serial_control; - mavlink_msg_serial_control_decode(&message, &serial_control); + if (_mavlink_telemetry_registry.serial_control.updatedSince(_last_serial_control)) { + _last_serial_control = now; + const auto &s = _mavlink_telemetry_registry.serial_control.value(); - constexpr auto len{sizeof(serial_control.data)}; - serial_control.data[len - 1] = '\0'; + logger.debug({reinterpret_cast(s.data), s.count});// temp. + } - logger.info({reinterpret_cast(serial_control.data), static_cast(serial_control.count)}); + if (need_update) { + UI::instance().addEvent(UI::Event::update()); + } + } - return false; - } +private: + static constexpr auto logger{kf::Logger::create("MavlinkTelemetryPage")}; - case MAVLINK_MSG_ID_SCALED_IMU: { - mavlink_scaled_imu_t imu; - mavlink_msg_scaled_imu_decode(&message, &imu); + protocol::ProtocolRegistry &_protocol_registry; + protocol::ProtocolLink &_protocol_link; + MavlinkTelemetryRegistry &_mavlink_telemetry_registry; + kf::math::Milliseconds _last_imu{}, _last_attitude{}, _last_serial_control{}; - (void) _imu_display_buffer.format( - "Acc %+.3f %+.3f %+.3f", - float(imu.xacc * 0.001f), - float(imu.yacc * 0.001f), - float(imu.zacc * 0.001f)); - _imu_display.value(_imu_display_buffer.view()); + // widgets + kf::memory::ArrayString<64> _attitude_buffer{"..."}, _imu_buffer{"..."}; - return true; - } + UI::Display _attitude_display{_attitude_buffer.view()}, _imu_display{_imu_buffer.view()}; - default: - // Unhandled message type - return false; - } - } + kf::memory::Array _layout; }; }// namespace djc::ui::pages \ No newline at end of file From 53e22be5525469c7b438f71005f6fae5cefc8463 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 5 May 2026 10:20:35 +0300 Subject: [PATCH 077/326] update: main --- DJC-Firmware/src/main.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index c2a7cf7..51c82b5 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -14,6 +14,7 @@ #include "djc/Control.hpp" #include "djc/DisplayManager.hpp" #include "djc/ManualInput.hpp" +#include "djc/MavlinkTelemetryRegistry.hpp" #include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" @@ -65,6 +66,8 @@ static djc::protocol::ProtocolRegistry protocol_registry{ storage.config().protocol_registry, }; +static djc::MavlinkTelemetryRegistry mavlink_telemetry_registry{}; + static djc::PeerScanner peer_scanner{ storage.config().peer_scanner, transport_link, @@ -95,6 +98,7 @@ static djc::ui::pages::MavlinkTelemetryPage mavlink_telemetry_page{ root_page, protocol_registry, protocol_link, + mavlink_telemetry_registry, }; static djc::ui::pages::RawProtocolPage raw_protocol_page{ @@ -138,10 +142,14 @@ void setup() { protocol_link.protocol(protocol_registry.get(storage.config().init_protocol_mode)); - transport_link.onReceive([](const djc::transport::PeerAddress &, kf::memory::Slice buffer){ + transport_link.onReceive([](const djc::transport::PeerAddress &, kf::memory::Slice buffer) { protocol_link.receive(buffer); }); + protocol_registry.mavlink().callback([](const mavlink_message_t &message){ + mavlink_telemetry_registry.update(millis(), message); + }); + peer_scanner.init(); { From 783d7b7a035eef0d3f1a22fdaef9918a42c90111 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 5 May 2026 10:26:20 +0300 Subject: [PATCH 078/326] add: main: millis cast --- DJC-Firmware/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 51c82b5..ccd5d9d 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -146,8 +146,8 @@ void setup() { protocol_link.receive(buffer); }); - protocol_registry.mavlink().callback([](const mavlink_message_t &message){ - mavlink_telemetry_registry.update(millis(), message); + protocol_registry.mavlink().callback([](const mavlink_message_t &message) { + mavlink_telemetry_registry.update(static_cast(millis()), message); }); peer_scanner.init(); From f859eefaf99029e5660929046e6d2b7637a54c20 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 5 May 2026 14:07:24 +0300 Subject: [PATCH 079/326] make PeerDisplay callbackable --- .../src/djc/ui/widgets/PeerDisplay.hpp | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index 300d80c..831e786 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -8,14 +8,16 @@ #include #include #include +#include #include "djc/PeerScanner.hpp" +#include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" namespace djc::ui::widgets { -struct PeerDisplay final : UI::Widget { +struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked { enum class State : char { Cleared = '\xF8', @@ -23,16 +25,14 @@ struct PeerDisplay final : UI::Widget { PreCleared = '\xF9', }; - static constexpr kf::math::Milliseconds pre_cleared_highligt_timespan{2000}; + static constexpr auto extreme_age_factor{0.75f}; - void transportLink(transport::TransportLink &new_transport_link) noexcept { _transport_link = &new_transport_link; } - - void update(const kf::Option &entry_option, kf::math::Milliseconds now,kf::math::Milliseconds max_life_time) noexcept { + void update(const kf::Option &entry_option, kf::math::Milliseconds now, kf::math::Milliseconds max_life_time) noexcept { _entry_option = entry_option; if (_entry_option.hasValue()) { const auto age = now - _entry_option.value().last_seen; - _state = (age + pre_cleared_highligt_timespan >= max_life_time) ? State::PreCleared : State::Stable; + _state = (age >= max_life_time * extreme_age_factor) ? State::PreCleared : State::Stable; } else { _state = State::Cleared; } @@ -52,17 +52,15 @@ struct PeerDisplay final : UI::Widget { } bool onClick() noexcept override { - if (not _entry_option.hasValue()) { return false; } - - if (_transport_link != nullptr and _transport_link->connect(_entry_option.value().address)) { - clear(); + if (_entry_option.hasValue()) { + this->invoke(_entry_option.value().address); + return true; + } else { + return false; } - - return true; } private: - transport::TransportLink *_transport_link{nullptr}; kf::Option _entry_option{}; State _state{State::Cleared}; }; From 1193d093692523c6bccc6c3e03385ff7b6498da5 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 5 May 2026 14:14:10 +0300 Subject: [PATCH 080/326] Feature: Peer Detail Page Fixes #13 --- .../src/djc/ui/pages/PeerDetailPage.hpp | 48 +++++++++++++++++++ .../src/djc/ui/pages/PeerExplorerPage.hpp | 45 ++++++++++------- 2 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp new file mode 100644 index 0000000..97b992c --- /dev/null +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -0,0 +1,48 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include "djc/transport/TransportLink.hpp" +#include "djc/ui/UI.hpp" + +namespace djc::ui::pages { + +struct PeerDetailPage final : UI::Page { + + explicit PeerDetailPage(UI::Page &root, transport::TransportLink &transport_link) noexcept : + Page{"Oh no!"}, + _layout{{ + &root.link(), + &_connection_button, + }} + + { + widgets({_layout.data(), _layout.size()}); + + _connection_button.callback([this, &transport_link]() -> void { + if (_peer_address.hasValue()) { + (void) transport_link.connect(_peer_address.value()); + } + }); + } + + void peerAddress(const transport::PeerAddress &new_peer_address) noexcept { + _peer_address.value(new_peer_address); + _label_buffer = new_peer_address.toString(); + this->label(_label_buffer.view()); + } + +private: + kf::Option _peer_address{}; + + transport::PeerAddress::ReprString _label_buffer{}; + UI::Button _connection_button{"Connect"}; + kf::memory::Array _layout; +}; + +}// namespace djc::ui::pages diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 416a7f7..7d159df 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -10,8 +10,10 @@ #include #include "djc/PeerScanner.hpp" +#include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" +#include "djc/ui/pages/PeerDetailPage.hpp" #include "djc/ui/widgets/PeerDisplay.hpp" namespace djc::ui::pages { @@ -21,23 +23,30 @@ struct PeerExplorerPage : UI::Page { static constexpr auto peer_display_start_index{3}; static constexpr kf::math::Milliseconds redraw_period{1'000}; - explicit constexpr PeerExplorerPage(UI::Page &root, PeerScanner &peer_scanner, transport::TransportLink &transport_link) noexcept : + explicit PeerExplorerPage( + UI::Page &root, + PeerScanner &peer_scanner, + transport::TransportLink &transport_link) noexcept : Page{"Peer Explorer"}, _peer_scanner{peer_scanner}, _transport_link{transport_link}, _layout{{ &root.link(), - &_connection_button, + &_primary_connection_status_button, &_available_label, }} { - for (auto i = 0; i < _peer_displays.size(); i += 1) { - _peer_displays[i].transportLink(_transport_link); + for (auto i = 0u; i < _peer_displays.size(); i += 1) { + _peer_displays[i].callback([this](const transport::PeerAddress &address) -> void { + _peer_detail_page.peerAddress(address); + UI::instance().bindPage(_peer_detail_page); + }); + _layout[i + peer_display_start_index] = &_peer_displays[i]; } - _connection_button.callback([this]() { + _primary_connection_status_button.callback([this]() { if (_transport_link.connected()) { _transport_link.disconnect(); } @@ -53,17 +62,17 @@ struct PeerExplorerPage : UI::Page { _redraw_timer.start(now); if (_transport_link.activePeerAddress().hasValue()) { - (void) _connection_button_label.format("\xFC%s\x80", _transport_link.activePeerAddress().value().toString().data()); - _connection_button.label(_connection_button_label.view()); + (void) _connection_button_buffer.format("\xFC%s\x80", _transport_link.activePeerAddress().value().toString().data()); + _primary_connection_status_button.label(_connection_button_buffer.view()); } else { - _connection_button.label("\xF9" - "Disconnected\x80"); + _primary_connection_status_button.label("\xF9" + "Disconnected\x80"); } const auto available_peers = _peer_scanner.peers(); - (void) _available_label_value.format(" Available: %d", available_peers.size()); - _available_label.value(_available_label_value.view()); + (void) _available_label_buffer.format(" Available: %d", available_peers.size()); + _available_label.value(_available_label_buffer.view()); for (auto i = 0u; i < _peer_displays.size(); i += 1) { if (i < available_peers.size()) { @@ -80,16 +89,20 @@ struct PeerExplorerPage : UI::Page { private: PeerScanner &_peer_scanner; transport::TransportLink &_transport_link; + kf::math::Timer _redraw_timer{redraw_period}; - kf::memory::ArrayString<64> _available_label_value{}, _connection_button_label{}; - // widgets - UI::Button _connection_button{""}; - UI::Display _available_label{_available_label_value.view()}; + kf::memory::ArrayString<64> _available_label_buffer{}, _connection_button_buffer{}; + + UI::Button _primary_connection_status_button{""}; + UI::Display _available_label{_available_label_buffer.view()}; kf::memory::Array _peer_displays{}; - // layout kf::memory::Array _layout; + + // child pages + + PeerDetailPage _peer_detail_page{*this, _transport_link}; }; }// namespace djc::ui::pages \ No newline at end of file From 20d5dc8ea45576f169e7fb90823c5a63b8bbd17e Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 6 May 2026 18:06:09 +0300 Subject: [PATCH 081/326] Update: PeerDetailPage --- DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index 97b992c..83e2efe 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" @@ -14,6 +15,10 @@ namespace djc::ui::pages { struct PeerDetailPage final : UI::Page { + static constexpr kf::memory::StringView + label_connect_ready{"Connect"}, + label_connect_failed{"Failed to connect"}; + explicit PeerDetailPage(UI::Page &root, transport::TransportLink &transport_link) noexcept : Page{"Oh no!"}, _layout{{ @@ -24,9 +29,13 @@ struct PeerDetailPage final : UI::Page { { widgets({_layout.data(), _layout.size()}); - _connection_button.callback([this, &transport_link]() -> void { + _connection_button.callback([this, &transport_link, &root]() -> void { if (_peer_address.hasValue()) { - (void) transport_link.connect(_peer_address.value()); + if (transport_link.connect(_peer_address.value())) { + UI::instance().bindPage(root); + } else { + _connection_button.label(label_connect_failed); + } } }); } @@ -35,13 +44,14 @@ struct PeerDetailPage final : UI::Page { _peer_address.value(new_peer_address); _label_buffer = new_peer_address.toString(); this->label(_label_buffer.view()); + _connection_button.label(label_connect_ready); } private: kf::Option _peer_address{}; transport::PeerAddress::ReprString _label_buffer{}; - UI::Button _connection_button{"Connect"}; + UI::Button _connection_button{label_connect_ready}; kf::memory::Array _layout; }; From 5ac14f194d01b3cf7d045f13b4683f8a98f45d47 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 7 May 2026 14:35:35 +0300 Subject: [PATCH 082/326] make PeerAddress trivial --- DJC-Firmware/src/djc/transport/EspNowTransport.hpp | 2 +- DJC-Firmware/src/djc/transport/PeerAddress.hpp | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 5b9f972..1233cf6 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -134,7 +134,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { } espnow.onReceiveFromUnknown([this](const EspNow::Mac &mac, kf::memory::Slice buffer){ - invokeReceiveForeign(PeerAddress{mac}, buffer); + invokeReceiveForeign(PeerAddress::fromEspnowMac(mac), buffer); }); _broadcast_peer = addPeer(broadcast_mac_address); diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp index b8835a3..13c8886 100644 --- a/DJC-Firmware/src/djc/transport/PeerAddress.hpp +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -18,10 +18,14 @@ struct PeerAddress { using ReprString = kf::memory::ArrayString<32>; - /// @brief Construct an ESP‑NOW peer address from a MAC. + /// @brief create an ESP‑NOW peer address from a MAC. /// @param mac 6‑byte MAC address (EspNow::Mac). - explicit constexpr PeerAddress(const kf::network::EspNow::Mac &mac) noexcept : - _kind{Kind::EspNow}, _mac{mac} {} + static constexpr PeerAddress fromEspnowMac(const kf::network::EspNow::Mac &mac) noexcept { + PeerAddress ret{}; + ret._kind = Kind::EspNow, + ret._mac = mac; + return ret; + } /// @brief Return the kind of transport this address belongs to. [[nodiscard]] Kind kind() const noexcept { return _kind; } From 04d691f5f1231df6fdbb92dc47e51abcce6c1c23 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 7 May 2026 14:42:58 +0300 Subject: [PATCH 083/326] update: device Config --- DJC-Firmware/src/djc/Config.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 1397b26..175ec0c 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -21,13 +21,13 @@ namespace djc { struct Config { struct PeerNote { - char address;// temp - kf::memory::Array info; + transport::PeerAddress address; + kf::memory::Array info; }; using PeerFavoritesConfig = djc::memory::Box; - static constexpr auto latest_version{6}; + static constexpr auto latest_version{7}; kf::u16 version; From 48ea0a3ea4953ffc0a3cf0677248417d20e19007 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 7 May 2026 15:01:35 +0300 Subject: [PATCH 084/326] add: battery status into MavlinkTelemetry --- DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp index 3f8ba8b..5cc743f 100644 --- a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp +++ b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp @@ -48,6 +48,7 @@ struct MavlinkTelemetryRegistry final : kf::mixin::NonCopyable { Entry attitude_quaternion{mavlink_msg_attitude_quaternion_decode}; Entry serial_control{mavlink_msg_serial_control_decode}; Entry scaled_imu{mavlink_msg_scaled_imu_decode}; + Entry battery_status{mavlink_msg_battery_status_decode}; /// @brief Route an incoming MAVLink message to the correct entry based on its ID /// @note Only known message types are dispatched; unknown IDs are silently ignored. @@ -68,6 +69,10 @@ struct MavlinkTelemetryRegistry final : kf::mixin::NonCopyable { case MAVLINK_MSG_ID_SCALED_IMU: scaled_imu.update(now, message); return; + + case MAVLINK_MSG_ID_BATTERY_STATUS: + battery_status.update(now, message); + return; } } }; From 02a904be00cba5a49606c7c02e51ba550249d668 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 7 May 2026 15:13:06 +0300 Subject: [PATCH 085/326] add some Telemetry Entries --- .../src/djc/MavlinkTelemetryRegistry.hpp | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp index 5cc743f..16eced6 100644 --- a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp +++ b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp @@ -44,11 +44,19 @@ struct MavlinkTelemetryRegistry final : kf::mixin::NonCopyable { kf::math::Milliseconds _last_update_time{0u}; }; + // slow telemetry + Entry heartbeat{mavlink_msg_heartbeat_decode}; - Entry attitude_quaternion{mavlink_msg_attitude_quaternion_decode}; + Entry extended_sys_state{mavlink_msg_extended_sys_state_decode}; + Entry battery_status{mavlink_msg_battery_status_decode}; Entry serial_control{mavlink_msg_serial_control_decode}; + + // fast telemetry + + Entry attitude_quaternion{mavlink_msg_attitude_quaternion_decode}; Entry scaled_imu{mavlink_msg_scaled_imu_decode}; - Entry battery_status{mavlink_msg_battery_status_decode}; + Entry rc_channels_raw{mavlink_msg_rc_channels_raw_decode}; + Entry actuator_control_target{mavlink_msg_actuator_control_target_decode}; /// @brief Route an incoming MAVLink message to the correct entry based on its ID /// @note Only known message types are dispatched; unknown IDs are silently ignored. @@ -58,20 +66,32 @@ struct MavlinkTelemetryRegistry final : kf::mixin::NonCopyable { heartbeat.update(now, message); return; - case MAVLINK_MSG_ID_ATTITUDE_QUATERNION: - attitude_quaternion.update(now, message); + case MAVLINK_MSG_ID_EXTENDED_SYS_STATE: + extended_sys_state.update(now, message); + return; + + case MAVLINK_MSG_ID_BATTERY_STATUS: + battery_status.update(now, message); return; case MAVLINK_MSG_ID_SERIAL_CONTROL: serial_control.update(now, message); return; + case MAVLINK_MSG_ID_ATTITUDE_QUATERNION: + attitude_quaternion.update(now, message); + return; + case MAVLINK_MSG_ID_SCALED_IMU: scaled_imu.update(now, message); return; - case MAVLINK_MSG_ID_BATTERY_STATUS: - battery_status.update(now, message); + case MAVLINK_MSG_ID_RC_CHANNELS_RAW: + rc_channels_raw.update(now, message); + return; + + case MAVLINK_MSG_ID_ACTUATOR_CONTROL_TARGET: + actuator_control_target.update(now, message); return; } } From 145e4169f21ae8fcd69cef3f8cf2413b8f89daa4 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 9 May 2026 16:07:00 +0300 Subject: [PATCH 086/326] remove old PeerNote structure from device Config --- DJC-Firmware/src/djc/Config.hpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 175ec0c..6a6e833 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -10,7 +10,6 @@ #include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" #include "djc/input/InputHandler.hpp" -#include "djc/memory/Box.hpp" #include "djc/protocol/ProtocolLink.hpp" #include "djc/protocol/ProtocolRegistry.hpp" #include "djc/transport/PeerAddress.hpp" @@ -19,21 +18,12 @@ namespace djc { struct Config { - - struct PeerNote { - transport::PeerAddress address; - kf::memory::Array info; - }; - - using PeerFavoritesConfig = djc::memory::Box; - static constexpr auto latest_version{7}; kf::u16 version; protocol::ProtocolRegistry::Mode init_protocol_mode; kf::memory::Array device_name; - PeerFavoritesConfig peer_favorites; Periphery::Config periphery; InputHandler::Config input_handler; @@ -56,7 +46,6 @@ struct Config { .init_protocol_mode = protocol::ProtocolRegistry::Mode::Mavlink, .device_name = {"ESP32-DJC"}, - .peer_favorites = PeerFavoritesConfig::defaults(), .periphery = Periphery::Config::defaults(), .input_handler = InputHandler::Config::defaults(), From 07dbb5ff47caa9f05a4e50e294c3d7944da30181 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 9 May 2026 16:07:30 +0300 Subject: [PATCH 087/326] remove: memory::Box (not used) --- DJC-Firmware/src/djc/memory/Box.hpp | 36 ----------------------------- 1 file changed, 36 deletions(-) delete mode 100644 DJC-Firmware/src/djc/memory/Box.hpp diff --git a/DJC-Firmware/src/djc/memory/Box.hpp b/DJC-Firmware/src/djc/memory/Box.hpp deleted file mode 100644 index 67d558f..0000000 --- a/DJC-Firmware/src/djc/memory/Box.hpp +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2026 KiraFlux -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -namespace djc::memory { - -template struct Box { - kf::memory::Array items; - Index items_saved, selected_index;// 0..N - - constexpr Index maxItems() const noexcept { return N; } - - const T &selected() const noexcept { return items[selected_index]; } - - bool add(const T &item) noexcept { - if (items_saved >= maxItems()) { return false; } - - items[items_saved] = item; - items_saved += 1; - - return true; - } - - static constexpr Box defaults() noexcept { - return Box{ - .items = {}, - .items_saved = 0, - .selected_index = 0, - }; - } -}; - -}// namespace djc::memory From ee426640b1a98b6e4fbdc3b34249168eb685d494 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 9 May 2026 16:55:54 +0300 Subject: [PATCH 088/326] Feature: Create PeerFavoritesRegistry Fixes #22 --- .../src/djc/PeerFavoritesRegistry.hpp | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp new file mode 100644 index 0000000..ce05fba --- /dev/null +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -0,0 +1,113 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "djc/transport/PeerAddress.hpp" + +namespace djc { + +/// @brief Registry of favorite peers (bookmarks). +/// +/// Maintains a list of peer entries inside an externally‑provided array of slots. +/// The registry does not own the memory, only manipulates it. +/// +/// @note All methods are safe to call from different UI callbacks as long as the underlying storage is exclusively owned by the registry. +struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { + + /// @brief A single favorite‑peer record. + struct Entry final : kf::mixin::NonCopyable { + transport::PeerAddress address; ///< Peer address. + bool trusted; ///< Trust flag (reserved for future use). + kf::memory::Array description;///< Human‑readable description. + + /// @brief Factory method for a new, empty‑description entry. + [[nodiscard]] static Entry create(const transport::PeerAddress &address) noexcept { + return Entry{ + .address = address, + .trusted = false, + .description = {}, + }; + } + }; + + explicit constexpr PeerFavoritesRegistry(kf::memory::Slice> entries) noexcept : + _entries{entries} {} + + /// @brief Return the entire slot array, including empty slots. + [[nodiscard]] kf::memory::Slice> all() const noexcept { return _entries; } + + /// @brief Obtain a mutable pointer to an entry by address. + /// @param address Peer address to search for. + /// @return Pointer to the entry, or nullptr if not found. + // todo replace with Option (not exists yet..) + [[nodiscard]] Entry *get(const transport::PeerAddress &address) noexcept { + const auto index = indexOf(address); + if (index.hasValue()) { + auto &option = _entries[index.value()]; + if (option.hasValue()) { return &option.value(); } + } + + return nullptr; + } + + /// @brief Obtain a read‑only pointer to an entry by address. + /// @param address Peer address to search for. + /// @return Pointer to a const entry, or nullptr if not found. + [[nodiscard]] const Entry *get(const transport::PeerAddress &address) const noexcept { + return const_cast(this)->get(address); + } + + /// @brief Add a new entry. + /// @param address Peer address to add. + /// @return true if the entry was added, false if it already exists or the list is full. + [[nodiscard]] bool add(const transport::PeerAddress &address) noexcept { + const auto can_add = not indexOf(address).hasValue(); + + if (can_add) { + for (auto &entry: _entries) { + if (not entry.hasValue()) { + entry.value(Entry::create(address)); + break; + } + } + } + + return can_add; + } + + /// @brief Remove an entry by address. + /// @param address Peer address to remove. + /// @return true if an entry was actually removed. + [[nodiscard]] bool remove(const transport::PeerAddress &address) noexcept { + const auto index = indexOf(address); + if (index.hasValue()) { + _entries[index.value()] = {}; + } + + return index.hasValue(); + } + +private: + kf::memory::Slice> _entries;///< External slot array + + /// @brief Find the index of an entry by address. + /// @param address Peer address to search for. + /// @return Option containing the index, or an empty option if not found. + [[nodiscard]] kf::Option indexOf(const transport::PeerAddress &address) const noexcept { + for (auto index = 0u; index < _entries.size(); index += 1) { + const auto &item = _entries[index]; + if (item.hasValue() and item.value().address == address) { + return {index}; + } + } + return {}; + } +}; + +}// namespace djc \ No newline at end of file From 84cc9025216adf932e591dad7050fb35a8dbde56 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 9 May 2026 16:56:22 +0300 Subject: [PATCH 089/326] add: Peer favorites into device config --- DJC-Firmware/src/djc/Config.hpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 6a6e833..019312c 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -3,10 +3,12 @@ #pragma once +#include #include #include #include +#include "djc/PeerFavoritesRegistry.hpp" #include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" #include "djc/input/InputHandler.hpp" @@ -18,12 +20,13 @@ namespace djc { struct Config { - static constexpr auto latest_version{7}; + static constexpr auto latest_version{7u}, max_peer_favorites{8u}; kf::u16 version; protocol::ProtocolRegistry::Mode init_protocol_mode; kf::memory::Array device_name; + kf::memory::Array, max_peer_favorites> peer_favorites; Periphery::Config periphery; InputHandler::Config input_handler; @@ -46,6 +49,7 @@ struct Config { .init_protocol_mode = protocol::ProtocolRegistry::Mode::Mavlink, .device_name = {"ESP32-DJC"}, + .peer_favorites = {}, .periphery = Periphery::Config::defaults(), .input_handler = InputHandler::Config::defaults(), From eecb71a6e16bb64d78fe16225866b06087d527e8 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 9 May 2026 18:14:00 +0300 Subject: [PATCH 090/326] refactor: PeerExplorerPage shows only exsisted peers --- .../src/djc/ui/pages/PeerExplorerPage.hpp | 50 +++++++++---------- .../src/djc/ui/widgets/PeerDisplay.hpp | 24 ++++----- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 7d159df..b61f36a 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -52,44 +52,39 @@ struct PeerExplorerPage : UI::Page { } }); - widgets({_layout.data(), _layout.size()}); + widgets(layout(0)); _redraw_timer.start(0);// enable timer } void onUpdate(kf::math::Milliseconds now) noexcept override { - if (_redraw_timer.expired(now)) { - _redraw_timer.start(now); - - if (_transport_link.activePeerAddress().hasValue()) { - (void) _connection_button_buffer.format("\xFC%s\x80", _transport_link.activePeerAddress().value().toString().data()); - _primary_connection_status_button.label(_connection_button_buffer.view()); - } else { - _primary_connection_status_button.label("\xF9" - "Disconnected\x80"); - } - - const auto available_peers = _peer_scanner.peers(); - - (void) _available_label_buffer.format(" Available: %d", available_peers.size()); - _available_label.value(_available_label_buffer.view()); + if (not _redraw_timer.expired(now)) { return; } + _redraw_timer.start(now); + + if (_transport_link.activePeerAddress().hasValue()) { + (void) _connection_button_buffer.format("\xFC%s\x80", _transport_link.activePeerAddress().value().toString().data()); + _primary_connection_status_button.label(_connection_button_buffer.view()); + } else { + _primary_connection_status_button.label( + "\xF9" + "Disconnected\x80"); + } - for (auto i = 0u; i < _peer_displays.size(); i += 1) { - if (i < available_peers.size()) { - _peer_displays[i].update(available_peers[i], now, _peer_scanner.config().entry_max_life_time); - } else { - _peer_displays[i].clear(); - } - } + const auto available_peers = _peer_scanner.peers(); + (void) _available_label_buffer.format(" Available: %d", available_peers.size()); + _available_label.value(_available_label_buffer.view()); - UI::instance().addEvent(UI::Event::update()); + for (auto i = 0u; i < available_peers.size(); i += 1) { + _peer_displays[i].update(available_peers[i], now, _peer_scanner.config().entry_max_life_time); } + + widgets(layout(available_peers.size())); + UI::instance().addEvent(UI::Event::update()); } private: PeerScanner &_peer_scanner; transport::TransportLink &_transport_link; - kf::math::Timer _redraw_timer{redraw_period}; kf::memory::ArrayString<64> _available_label_buffer{}, _connection_button_buffer{}; @@ -101,8 +96,11 @@ struct PeerExplorerPage : UI::Page { kf::memory::Array _layout; // child pages - PeerDetailPage _peer_detail_page{*this, _transport_link}; + + kf::memory::Slice layout(kf::usize displayed_peers) noexcept { + return kf::memory::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); + } }; }// namespace djc::ui::pages \ No newline at end of file diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index 831e786..a49009c 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -20,7 +20,7 @@ namespace djc::ui::widgets { struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked { enum class State : char { - Cleared = '\xF8', + Cleared = 0, Stable = '\xFC', PreCleared = '\xF9', }; @@ -38,26 +38,26 @@ struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked::formatted("%c%s\x80", static_cast(_state), content).view()); + + if (_entry_option.hasValue()) { + render.value( + kf::memory::ArrayString<64>::formatted( + "%c%s\x80", + static_cast(_state), + _entry_option.value().address.toString().data()) + .view()); + } + render.endBlock(); } bool onClick() noexcept override { if (_entry_option.hasValue()) { this->invoke(_entry_option.value().address); - return true; - } else { - return false; } + return _entry_option.hasValue(); } private: From 23e900fda85d1168c4ca2a36b2ac7ea4ed7687ce Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 9 May 2026 19:01:53 +0300 Subject: [PATCH 091/326] refactor: PeerFavoritesRegistry: - change Entry bool -> u8 - add Entry::trust_range --- .../src/djc/PeerFavoritesRegistry.hpp | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index ce05fba..c4f64b6 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -4,6 +4,8 @@ #pragma once #include +#include +#include #include #include #include @@ -21,16 +23,20 @@ namespace djc { struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { /// @brief A single favorite‑peer record. - struct Entry final : kf::mixin::NonCopyable { + struct Entry final { + using TrustType = kf::u8; + + static constexpr kf::Range trust_range{.start = 0, .end = 10}; + transport::PeerAddress address; ///< Peer address. - bool trusted; ///< Trust flag (reserved for future use). + TrustType trust; ///< Trust priority (0 - no trust, 1.. - auto connect) kf::memory::Array description;///< Human‑readable description. /// @brief Factory method for a new, empty‑description entry. [[nodiscard]] static Entry create(const transport::PeerAddress &address) noexcept { return Entry{ .address = address, - .trusted = false, + .trust = trust_range.start, .description = {}, }; } @@ -42,43 +48,39 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { /// @brief Return the entire slot array, including empty slots. [[nodiscard]] kf::memory::Slice> all() const noexcept { return _entries; } - /// @brief Obtain a mutable pointer to an entry by address. + /// @brief Obtain a const pointer to an entry by address. /// @param address Peer address to search for. - /// @return Pointer to the entry, or nullptr if not found. - // todo replace with Option (not exists yet..) - [[nodiscard]] Entry *get(const transport::PeerAddress &address) noexcept { - const auto index = indexOf(address); - if (index.hasValue()) { - auto &option = _entries[index.value()]; - if (option.hasValue()) { return &option.value(); } + [[nodiscard]] const kf::Option &get(const transport::PeerAddress &address) const noexcept { + static constexpr kf::Option none{}; + + if (const auto index = indexOf(address); index.hasValue()) { + if (const auto &option = _entries[index.value()]; option.hasValue()) { return option; } } - return nullptr; + return none; } - /// @brief Obtain a read‑only pointer to an entry by address. - /// @param address Peer address to search for. - /// @return Pointer to a const entry, or nullptr if not found. - [[nodiscard]] const Entry *get(const transport::PeerAddress &address) const noexcept { - return const_cast(this)->get(address); + /// @brief Check exists to an entry by address + [[nodiscard]] bool exists(const transport::PeerAddress &address) const noexcept { + return indexOf(address).hasValue(); } - /// @brief Add a new entry. - /// @param address Peer address to add. - /// @return true if the entry was added, false if it already exists or the list is full. - [[nodiscard]] bool add(const transport::PeerAddress &address) noexcept { - const auto can_add = not indexOf(address).hasValue(); - - if (can_add) { - for (auto &entry: _entries) { - if (not entry.hasValue()) { - entry.value(Entry::create(address)); - break; - } + /// @brief Update already existed or Add a new entry + /// @return true, or false if the list is full. + [[nodiscard]] bool put(const Entry &entry_to_add) noexcept { + if (const auto same_address_entry_index = indexOf(entry_to_add.address); same_address_entry_index.hasValue()) { + _entries[same_address_entry_index.value()].value(entry_to_add); + return true; + } + + for (auto &entry: _entries) { + if (not entry.hasValue()) { + entry.value(entry_to_add); + return true; } } - return can_add; + return false; } /// @brief Remove an entry by address. @@ -94,7 +96,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { } private: - kf::memory::Slice> _entries;///< External slot array + kf::memory::Slice> _entries; /// @brief Find the index of an entry by address. /// @param address Peer address to search for. From 350eacf1ef18a0d8e34690187972e42e79c52b5b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 00:13:59 +0300 Subject: [PATCH 092/326] Update: PeerRegistry: default Entry name --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index c4f64b6..1bdb3cc 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -37,7 +37,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { return Entry{ .address = address, .trust = trust_range.start, - .description = {}, + .description = {"New-Peer"}, }; } }; From 7dfd0895f0d14001f1cde6816ff29710708e9c3a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 00:14:51 +0300 Subject: [PATCH 093/326] add: PeerScanner::Entry: peer info from favorites --- DJC-Firmware/src/djc/PeerScanner.hpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerScanner.hpp b/DJC-Firmware/src/djc/PeerScanner.hpp index b68d0c2..d38ba17 100644 --- a/DJC-Firmware/src/djc/PeerScanner.hpp +++ b/DJC-Firmware/src/djc/PeerScanner.hpp @@ -14,6 +14,7 @@ #include #include +#include "djc/PeerFavoritesRegistry.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" @@ -56,13 +57,14 @@ struct PeerScanner final : struct Entry final { transport::PeerAddress address; ///< Address of the peer. kf::math::Milliseconds last_seen;///< Timestamp of the last received packet (millis). + const PeerFavoritesRegistry::Entry *peer_favorites_registry_record; // todo replace with Option when will available }; /// @brief Maximum number of peers the scanner can remember simultaneously static constexpr auto max_entries{8}; - explicit constexpr PeerScanner(const Config &config, transport::TransportLink &transport_link) noexcept : - Configurable{config}, _transport_link{transport_link} {} + explicit constexpr PeerScanner(const Config &config, transport::TransportLink &transport_link, const PeerFavoritesRegistry& peer_favorites_registry) noexcept : + Configurable{config}, _transport_link{transport_link}, _peer_favorites_registry{peer_favorites_registry} {} /// @brief Returns a slice of the currently active peer entries. /// @return A contiguous view of the first `_active_count` elements of the internal array. @@ -76,6 +78,7 @@ struct PeerScanner final : kf::memory::Array, max_entries> _entries{}; kf::math::Timer _update_poll_timer{this->config().entries_list_update_period}; transport::TransportLink &_transport_link; + const PeerFavoritesRegistry& _peer_favorites_registry; kf::math::Milliseconds _last_poll_time{0}; kf::usize _active_count{0}; @@ -98,7 +101,12 @@ struct PeerScanner final : // search for first empty entry for (auto &entry: _entries) { if (not entry.hasValue()) { - entry = Entry{address, _last_poll_time}; + const auto &record = _peer_favorites_registry.get(address); + entry = Entry{ + .address = address, + .last_seen = _last_poll_time, + .peer_favorites_registry_record = (record.hasValue() ? &record.value() : nullptr), + }; return; } } From d89dd7e4273af790f77edc684665bdd5368611ee Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 00:21:44 +0300 Subject: [PATCH 094/326] Feature: Create PeerFavoriteEditPage Fixes #23 --- .../src/djc/ui/pages/PeerDetailPage.hpp | 67 +++++++++---- .../src/djc/ui/pages/PeerExplorerPage.hpp | 17 ++-- .../src/djc/ui/pages/PeerFavoritePage.hpp | 93 +++++++++++++++++++ .../src/djc/ui/widgets/PeerDisplay.hpp | 9 +- 4 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index 83e2efe..d5aa2cd 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -6,53 +6,80 @@ #include #include #include -#include +#include "djc/PeerFavoritesRegistry.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" +#include "djc/ui/pages/PeerFavoritePage.hpp" namespace djc::ui::pages { struct PeerDetailPage final : UI::Page { - static constexpr kf::memory::StringView - label_connect_ready{"Connect"}, - label_connect_failed{"Failed to connect"}; - - explicit PeerDetailPage(UI::Page &root, transport::TransportLink &transport_link) noexcept : - Page{"Oh no!"}, + explicit PeerDetailPage( + UI::Page &root, + transport::TransportLink &transport_link, + PeerFavoritesRegistry &peer_favorites_registry) noexcept : + Page{"oh_no"}, + _peer_favorites_registry{peer_favorites_registry}, _layout{{ &root.link(), &_connection_button, + &_peer_favorite_button, }} { widgets({_layout.data(), _layout.size()}); _connection_button.callback([this, &transport_link, &root]() -> void { - if (_peer_address.hasValue()) { - if (transport_link.connect(_peer_address.value())) { - UI::instance().bindPage(root); - } else { - _connection_button.label(label_connect_failed); - } + if (not _peer_address.hasValue()) { return; } + + if (transport_link.connect(_peer_address.value())) { + UI::instance().bindPage(root); + } else { + _connection_button.label("Failed to connect"); } }); + + _peer_favorite_button.callback([this]() -> void { + if (not _peer_address.hasValue()) { return; } + this->label("Back"); + _peer_favorite_page.bindPeer(_peer_address.value()); + UI::instance().bindPage(_peer_favorite_page); + UI::instance().addEvent(UI::Event::update()); + }); + } + + void bindPeer(const transport::PeerAddress &address) noexcept { + _peer_address.value(address); } - void peerAddress(const transport::PeerAddress &new_peer_address) noexcept { - _peer_address.value(new_peer_address); - _label_buffer = new_peer_address.toString(); - this->label(_label_buffer.view()); - _connection_button.label(label_connect_ready); + void onEntry() noexcept override { + _connection_button.label("Connect"); + if (_peer_address.hasValue()) { + _label_buffer = _peer_address.value().toString(); + this->label(_label_buffer.view()); + + _peer_favorite_button.label(_peer_favorites_registry.exists(_peer_address.value()) ? "Edit" : "Add to favorites"); + } } private: + // state + kf::Option _peer_address{}; + PeerFavoritesRegistry &_peer_favorites_registry; + + // widgets transport::PeerAddress::ReprString _label_buffer{}; - UI::Button _connection_button{label_connect_ready}; - kf::memory::Array _layout; + UI::Button _connection_button{{}}, _peer_favorite_button{{}}; + + kf::memory::Array _layout; + + // child pages + + PeerFavoritePage _peer_favorite_page{*this, _peer_favorites_registry}; }; }// namespace djc::ui::pages diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index b61f36a..5e09971 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -9,6 +9,7 @@ #include #include +#include "djc/PeerFavoritesRegistry.hpp" #include "djc/PeerScanner.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" @@ -25,21 +26,23 @@ struct PeerExplorerPage : UI::Page { explicit PeerExplorerPage( UI::Page &root, + transport::TransportLink &transport_link, PeerScanner &peer_scanner, - transport::TransportLink &transport_link) noexcept : + PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{"Peer Explorer"}, - _peer_scanner{peer_scanner}, _transport_link{transport_link}, + _peer_scanner{peer_scanner}, _layout{{ &root.link(), &_primary_connection_status_button, &_available_label, - }} + }}, + _peer_detail_page{*this, _transport_link, peer_favorites_registry} { for (auto i = 0u; i < _peer_displays.size(); i += 1) { _peer_displays[i].callback([this](const transport::PeerAddress &address) -> void { - _peer_detail_page.peerAddress(address); + _peer_detail_page.bindPeer(address); UI::instance().bindPage(_peer_detail_page); }); @@ -83,20 +86,20 @@ struct PeerExplorerPage : UI::Page { } private: - PeerScanner &_peer_scanner; transport::TransportLink &_transport_link; + PeerScanner &_peer_scanner; kf::math::Timer _redraw_timer{redraw_period}; kf::memory::ArrayString<64> _available_label_buffer{}, _connection_button_buffer{}; - UI::Button _primary_connection_status_button{""}; + UI::Button _primary_connection_status_button{{}}; UI::Display _available_label{_available_label_buffer.view()}; kf::memory::Array _peer_displays{}; kf::memory::Array _layout; // child pages - PeerDetailPage _peer_detail_page{*this, _transport_link}; + PeerDetailPage _peer_detail_page; kf::memory::Slice layout(kf::usize displayed_peers) noexcept { return kf::memory::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp new file mode 100644 index 0000000..26b949d --- /dev/null +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -0,0 +1,93 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include "djc/PeerFavoritesRegistry.hpp" +#include "djc/transport/TransportLink.hpp" +#include "djc/ui/UI.hpp" +#include "djc/ui/widgets/TextInput.hpp" + +namespace djc::ui::pages { + +struct PeerFavoritePage final : UI::Page { + + explicit PeerFavoritePage(UI::Page &root, PeerFavoritesRegistry &peer_favorites_registry) noexcept : + Page{"oh_no_2"}, + _peer_favorites_registry{peer_favorites_registry}, + _layout{{ + // address and transport shows in title + &_labeled_trust_input, + &_labeled_description_input, + &_confirm_button, + &root.link(),// quit without save + &_delete_button, + }} + + { + _confirm_button.callback([this]() -> void { + if (not _temp_entry.hasValue()) { return; } + _temp_entry.value().trust = _trust_input.value(); + + const auto result = _peer_favorites_registry.put(_temp_entry.value()); + _confirm_button.label(result ? "Writed" : "Write failed"); + + UI::instance().addEvent(UI::Event::update()); + }); + + _delete_button.callback([this, &root]() -> void { + if (not _temp_entry.hasValue()) { return; } + + (void) _peer_favorites_registry.remove(_temp_entry.value().address); + + UI::instance().bindPage(root); + UI::instance().addEvent(UI::Event::update()); + }); + } + + void bindPeer(const transport::PeerAddress &address) noexcept { + const auto &entry_option = _peer_favorites_registry.get(address); + + _temp_entry.value(entry_option.valueOr(PeerFavoritesRegistry::Entry::create(address))); + + (void) _label_buffer.format("%s Peer favorite\n%s", (entry_option.hasValue() ? "Edit" : "Add"), address.toString().data()); + this->label(_label_buffer.view()); + + _description_input.source({_temp_entry.value().description.data(), _temp_entry.value().description.size()}); + _trust_input.value(_temp_entry.value().trust); + _confirm_button.label("Confirm"); + + widgets(kf::memory::Slice{_layout.data(), _layout.size()}.first(_layout.size() - (entry_option.hasValue() ? 0 : 1))); + } + +private: + PeerFavoritesRegistry &_peer_favorites_registry; + kf::Option _temp_entry{}; + + kf::memory::ArrayString<64> _label_buffer{}; + + using TrustInput = UI::Slider; + + TrustInput::Config _trust_input_config{ + .value_range = PeerFavoritesRegistry::Entry::trust_range, + .default_value = PeerFavoritesRegistry::Entry::trust_range.start, + .step = static_cast(1), + .placement = UI::Placement::Outside, + .init_show_value = true, + }; + + TrustInput _trust_input{_trust_input_config}; + widgets::TextInput _description_input{}; + + UI::Labeled _labeled_trust_input{"Trust", _trust_input}; + UI::Labeled _labeled_description_input{"Name", _description_input}; + UI::Button _confirm_button{{}}, _delete_button{"\xF9" + "Delete\x80"}; + kf::memory::Array _layout; +}; + +}// namespace djc::ui::pages \ No newline at end of file diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index a49009c..8f2f0bb 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -42,12 +42,9 @@ struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked::formatted( - "%c%s\x80", - static_cast(_state), - _entry_option.value().address.toString().data()) - .view()); + const auto content = (nullptr == _entry_option.value().peer_favorites_registry_record) ? _entry_option.value().address.toString().data() : _entry_option.value().peer_favorites_registry_record->description.data(); + + render.value(kf::memory::ArrayString<64>::formatted("%c%s\x80", static_cast(_state), content).view()); } render.endBlock(); From c25fdfe1061759c7e4a186abe066d5d5cd5b92e5 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 00:21:51 +0300 Subject: [PATCH 095/326] update: main --- DJC-Firmware/src/main.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index ccd5d9d..7168d40 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -15,6 +15,7 @@ #include "djc/DisplayManager.hpp" #include "djc/ManualInput.hpp" #include "djc/MavlinkTelemetryRegistry.hpp" +#include "djc/PeerFavoritesRegistry.hpp" #include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" @@ -68,9 +69,14 @@ static djc::protocol::ProtocolRegistry protocol_registry{ static djc::MavlinkTelemetryRegistry mavlink_telemetry_registry{}; +static djc::PeerFavoritesRegistry peer_favoriter_registry{ + {storage.config().peer_favorites.data(), storage.config().peer_favorites.size()}, +}; + static djc::PeerScanner peer_scanner{ storage.config().peer_scanner, transport_link, + peer_favoriter_registry, }; static djc::Control control{ @@ -90,8 +96,9 @@ static djc::ui::pages::RootPage root_page{}; static djc::ui::pages::PeerExplorerPage peer_explorer_page{ root_page, - peer_scanner, transport_link, + peer_scanner, + peer_favoriter_registry, }; static djc::ui::pages::MavlinkTelemetryPage mavlink_telemetry_page{ From db0edd216f657160616bf780a16ec31542ec3e2c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 14:36:19 +0300 Subject: [PATCH 096/326] replace strange labels with empty stringview --- DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index d5aa2cd..51bc353 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -20,7 +20,7 @@ struct PeerDetailPage final : UI::Page { UI::Page &root, transport::TransportLink &transport_link, PeerFavoritesRegistry &peer_favorites_registry) noexcept : - Page{"oh_no"}, + Page{{}}, _peer_favorites_registry{peer_favorites_registry}, _layout{{ &root.link(), diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 26b949d..286859a 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -17,7 +17,7 @@ namespace djc::ui::pages { struct PeerFavoritePage final : UI::Page { explicit PeerFavoritePage(UI::Page &root, PeerFavoritesRegistry &peer_favorites_registry) noexcept : - Page{"oh_no_2"}, + Page{{}}, _peer_favorites_registry{peer_favorites_registry}, _layout{{ // address and transport shows in title From 28a5013f85ddedb492b04cb060b10aee19d0931d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 14:36:33 +0300 Subject: [PATCH 097/326] Bug: Dangling pointer to favorite record in PeerScanner::Entry Fixes #28 --- DJC-Firmware/src/djc/PeerScanner.hpp | 9 ++---- .../src/djc/ui/pages/PeerExplorerPage.hpp | 28 +++++++++++++------ .../src/djc/ui/widgets/PeerDisplay.hpp | 23 +++++++-------- DJC-Firmware/src/main.cpp | 1 - 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerScanner.hpp b/DJC-Firmware/src/djc/PeerScanner.hpp index d38ba17..ee4f1d8 100644 --- a/DJC-Firmware/src/djc/PeerScanner.hpp +++ b/DJC-Firmware/src/djc/PeerScanner.hpp @@ -14,7 +14,6 @@ #include #include -#include "djc/PeerFavoritesRegistry.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" @@ -57,14 +56,13 @@ struct PeerScanner final : struct Entry final { transport::PeerAddress address; ///< Address of the peer. kf::math::Milliseconds last_seen;///< Timestamp of the last received packet (millis). - const PeerFavoritesRegistry::Entry *peer_favorites_registry_record; // todo replace with Option when will available }; /// @brief Maximum number of peers the scanner can remember simultaneously static constexpr auto max_entries{8}; - explicit constexpr PeerScanner(const Config &config, transport::TransportLink &transport_link, const PeerFavoritesRegistry& peer_favorites_registry) noexcept : - Configurable{config}, _transport_link{transport_link}, _peer_favorites_registry{peer_favorites_registry} {} + explicit constexpr PeerScanner(const Config &config, transport::TransportLink &transport_link) noexcept : + Configurable{config}, _transport_link{transport_link} {} /// @brief Returns a slice of the currently active peer entries. /// @return A contiguous view of the first `_active_count` elements of the internal array. @@ -78,7 +76,6 @@ struct PeerScanner final : kf::memory::Array, max_entries> _entries{}; kf::math::Timer _update_poll_timer{this->config().entries_list_update_period}; transport::TransportLink &_transport_link; - const PeerFavoritesRegistry& _peer_favorites_registry; kf::math::Milliseconds _last_poll_time{0}; kf::usize _active_count{0}; @@ -101,11 +98,9 @@ struct PeerScanner final : // search for first empty entry for (auto &entry: _entries) { if (not entry.hasValue()) { - const auto &record = _peer_favorites_registry.get(address); entry = Entry{ .address = address, .last_seen = _last_poll_time, - .peer_favorites_registry_record = (record.hasValue() ? &record.value() : nullptr), }; return; } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 5e09971..9e4049f 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -21,9 +21,6 @@ namespace djc::ui::pages { struct PeerExplorerPage : UI::Page { - static constexpr auto peer_display_start_index{3}; - static constexpr kf::math::Milliseconds redraw_period{1'000}; - explicit PeerExplorerPage( UI::Page &root, transport::TransportLink &transport_link, @@ -32,14 +29,12 @@ struct PeerExplorerPage : UI::Page { Page{"Peer Explorer"}, _transport_link{transport_link}, _peer_scanner{peer_scanner}, + _peer_favorites_registry{peer_favorites_registry}, _layout{{ &root.link(), &_primary_connection_status_button, &_available_label, - }}, - _peer_detail_page{*this, _transport_link, peer_favorites_registry} - - { + }} { for (auto i = 0u; i < _peer_displays.size(); i += 1) { _peer_displays[i].callback([this](const transport::PeerAddress &address) -> void { _peer_detail_page.bindPeer(address); @@ -78,7 +73,8 @@ struct PeerExplorerPage : UI::Page { _available_label.value(_available_label_buffer.view()); for (auto i = 0u; i < available_peers.size(); i += 1) { - _peer_displays[i].update(available_peers[i], now, _peer_scanner.config().entry_max_life_time); + const auto &entry = available_peers[i]; + _peer_displays[i].update(entry, peerName(entry), now, _peer_scanner.config().entry_max_life_time); } widgets(layout(available_peers.size())); @@ -86,8 +82,12 @@ struct PeerExplorerPage : UI::Page { } private: + static constexpr auto peer_display_start_index{3u}; + static constexpr kf::math::Milliseconds redraw_period{500}; + transport::TransportLink &_transport_link; PeerScanner &_peer_scanner; + PeerFavoritesRegistry &_peer_favorites_registry; kf::math::Timer _redraw_timer{redraw_period}; kf::memory::ArrayString<64> _available_label_buffer{}, _connection_button_buffer{}; @@ -99,11 +99,21 @@ struct PeerExplorerPage : UI::Page { kf::memory::Array _layout; // child pages - PeerDetailPage _peer_detail_page; + PeerDetailPage _peer_detail_page{*this, _transport_link, _peer_favorites_registry}; kf::memory::Slice layout(kf::usize displayed_peers) noexcept { return kf::memory::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); } + + kf::Option peerName(const kf::Option &entry) const noexcept { + if (not entry.hasValue()) { return {}; } + + const auto &favorite_record = _peer_favorites_registry.get(entry.value().address); + if (not favorite_record.hasValue()) { return {}; } + + const auto &description = favorite_record.value().description; + return {{description.data(), description.size()}}; + } }; }// namespace djc::ui::pages \ No newline at end of file diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index 8f2f0bb..29fad59 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -27,11 +27,12 @@ struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked &entry_option, kf::math::Milliseconds now, kf::math::Milliseconds max_life_time) noexcept { - _entry_option = entry_option; + void update(const kf::Option &entry, kf::Option entry_name, kf::math::Milliseconds now, kf::math::Milliseconds max_life_time) noexcept { + _entry = entry; + _entry_name = entry_name; - if (_entry_option.hasValue()) { - const auto age = now - _entry_option.value().last_seen; + if (_entry.hasValue()) { + const auto age = now - _entry.value().last_seen; _state = (age >= max_life_time * extreme_age_factor) ? State::PreCleared : State::Stable; } else { _state = State::Cleared; @@ -41,9 +42,8 @@ struct PeerDisplay final : UI::Widget, kf::mixin::Callbackeddescription.data(); - + if (_entry.hasValue()) { + const auto content = _entry_name.hasValue() ? _entry_name.value().data() : _entry.value().address.toString().data(); render.value(kf::memory::ArrayString<64>::formatted("%c%s\x80", static_cast(_state), content).view()); } @@ -51,14 +51,15 @@ struct PeerDisplay final : UI::Widget, kf::mixin::Callbackedinvoke(_entry_option.value().address); + if (_entry.hasValue()) { + this->invoke(_entry.value().address); } - return _entry_option.hasValue(); + return _entry.hasValue(); } private: - kf::Option _entry_option{}; + kf::Option _entry{}; + kf::Option _entry_name{}; State _state{State::Cleared}; }; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 7168d40..90f361b 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -76,7 +76,6 @@ static djc::PeerFavoritesRegistry peer_favoriter_registry{ static djc::PeerScanner peer_scanner{ storage.config().peer_scanner, transport_link, - peer_favoriter_registry, }; static djc::Control control{ From 9296361e428c482f86b34ce607287e4f34011f1a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 16:53:20 +0300 Subject: [PATCH 098/326] refactor: PeerFavoritesRegistry::Entry rename description -> name --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index 1bdb3cc..345eefb 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -28,16 +28,16 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { static constexpr kf::Range trust_range{.start = 0, .end = 10}; - transport::PeerAddress address; ///< Peer address. - TrustType trust; ///< Trust priority (0 - no trust, 1.. - auto connect) - kf::memory::Array description;///< Human‑readable description. + transport::PeerAddress address; ///< Peer address. + TrustType trust; ///< Trust priority (0 - no trust, 1.. - auto connect) + kf::memory::Array name;///< Human‑readable description. /// @brief Factory method for a new, empty‑description entry. [[nodiscard]] static Entry create(const transport::PeerAddress &address) noexcept { return Entry{ .address = address, .trust = trust_range.start, - .description = {"New-Peer"}, + .name = {"New-Peer"}, }; } }; From e73fabb1deb831434c71070dae80cb1c37f2f11f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 16:56:43 +0300 Subject: [PATCH 099/326] update PeerFavoritePage (matching new PeerFav..::Entry fields) --- DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 286859a..2151b3a 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -57,7 +57,7 @@ struct PeerFavoritePage final : UI::Page { (void) _label_buffer.format("%s Peer favorite\n%s", (entry_option.hasValue() ? "Edit" : "Add"), address.toString().data()); this->label(_label_buffer.view()); - _description_input.source({_temp_entry.value().description.data(), _temp_entry.value().description.size()}); + _description_input.source({_temp_entry.value().name.data(), _temp_entry.value().name.size()}); _trust_input.value(_temp_entry.value().trust); _confirm_button.label("Confirm"); From c0798fffa710cbe842046464b3a3cc3c7c34bf8d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 16:58:32 +0300 Subject: [PATCH 100/326] Refactor: Decouple PeerDisplay from PeerScanner::Entry Fixes #29 --- .../src/djc/ui/pages/PeerExplorerPage.hpp | 36 ++++++++---- .../src/djc/ui/widgets/PeerDisplay.hpp | 57 +++++++++---------- 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 9e4049f..2fad6b9 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -73,8 +73,7 @@ struct PeerExplorerPage : UI::Page { _available_label.value(_available_label_buffer.view()); for (auto i = 0u; i < available_peers.size(); i += 1) { - const auto &entry = available_peers[i]; - _peer_displays[i].update(entry, peerName(entry), now, _peer_scanner.config().entry_max_life_time); + _peer_displays[i].state(createPeerDisplayState(available_peers[i], now)); } widgets(layout(available_peers.size())); @@ -83,12 +82,11 @@ struct PeerExplorerPage : UI::Page { private: static constexpr auto peer_display_start_index{3u}; - static constexpr kf::math::Milliseconds redraw_period{500}; transport::TransportLink &_transport_link; PeerScanner &_peer_scanner; PeerFavoritesRegistry &_peer_favorites_registry; - kf::math::Timer _redraw_timer{redraw_period}; + kf::math::Timer _redraw_timer{static_cast(500)}; kf::memory::ArrayString<64> _available_label_buffer{}, _connection_button_buffer{}; @@ -105,14 +103,30 @@ struct PeerExplorerPage : UI::Page { return kf::memory::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); } - kf::Option peerName(const kf::Option &entry) const noexcept { - if (not entry.hasValue()) { return {}; } - - const auto &favorite_record = _peer_favorites_registry.get(entry.value().address); - if (not favorite_record.hasValue()) { return {}; } + kf::Option getPeerName(const kf::Option &record) const noexcept { + if (record.hasValue()) { + const auto &name = record.value().name; + return {{name.data(), name.size()}}; + } else { + return {}; + } + } - const auto &description = favorite_record.value().description; - return {{description.data(), description.size()}}; + kf::Option createPeerDisplayState(const kf::Option &entry, kf::math::Milliseconds now) const noexcept { + using P = widgets::PeerDisplay; + constexpr auto extreme_age_factor{0.75f}; + + if (entry.hasValue()) { + const auto age = now - entry.value().last_seen; + const auto extreme_age = _peer_scanner.config().entry_max_life_time * extreme_age_factor; + return {P::State{ + .address = entry.value().address, + .name = getPeerName(_peer_favorites_registry.get(entry.value().address)), + .label_color = (age < extreme_age) ? P::Color::Normal : P::Color::Warn, + }}; + } else { + return {}; + } } }; diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index 29fad59..e19166e 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -4,63 +4,58 @@ #pragma once #include -#include -#include #include #include #include -#include "djc/PeerScanner.hpp" #include "djc/transport/PeerAddress.hpp" -#include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" namespace djc::ui::widgets { struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked { - enum class State : char { - Cleared = 0, - Stable = '\xFC', - PreCleared = '\xF9', + enum class Color : char { + Normal = '\xFC', + Warn = '\xF9', }; - static constexpr auto extreme_age_factor{0.75f}; + struct State final { + transport::PeerAddress address; + kf::Option name; + Color label_color; - void update(const kf::Option &entry, kf::Option entry_name, kf::math::Milliseconds now, kf::math::Milliseconds max_life_time) noexcept { - _entry = entry; - _entry_name = entry_name; - - if (_entry.hasValue()) { - const auto age = now - _entry.value().last_seen; - _state = (age >= max_life_time * extreme_age_factor) ? State::PreCleared : State::Stable; - } else { - _state = State::Cleared; + kf::memory::StringView displayName() const noexcept { + return name.hasValue() ? name.value().data() : address.toString().data(); } - } + }; - void doRender(UI::RenderImpl &render) const noexcept override { - render.beginBlock(); + void state(const kf::Option &new_state) noexcept { _state = new_state; } - if (_entry.hasValue()) { - const auto content = _entry_name.hasValue() ? _entry_name.value().data() : _entry.value().address.toString().data(); - render.value(kf::memory::ArrayString<64>::formatted("%c%s\x80", static_cast(_state), content).view()); + void doRender(UI::RenderImpl &render) const noexcept override { + render.beginAltBlock(); + + if (_state.hasValue()) { + render.value( + kf::memory::ArrayString<64>::formatted( + "%c%s\x80", + static_cast(_state.value().label_color), + _state.value().displayName()) + .view()); } - render.endBlock(); + render.endAltBlock(); } bool onClick() noexcept override { - if (_entry.hasValue()) { - this->invoke(_entry.value().address); + if (_state.hasValue()) { + this->invoke(_state.value().address); } - return _entry.hasValue(); + return _state.hasValue(); } private: - kf::Option _entry{}; - kf::Option _entry_name{}; - State _state{State::Cleared}; + kf::Option _state{}; }; }// namespace djc::ui::widgets \ No newline at end of file From 69ba16676531ff98bf18cbf83c35662f412b4412 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 17:13:26 +0300 Subject: [PATCH 101/326] PeerExplorerPage: replace method with local static lambda --- .../src/djc/ui/pages/PeerExplorerPage.hpp | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 2fad6b9..37f2fa6 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -103,25 +103,25 @@ struct PeerExplorerPage : UI::Page { return kf::memory::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); } - kf::Option getPeerName(const kf::Option &record) const noexcept { - if (record.hasValue()) { - const auto &name = record.value().name; - return {{name.data(), name.size()}}; - } else { - return {}; - } - } - kf::Option createPeerDisplayState(const kf::Option &entry, kf::math::Milliseconds now) const noexcept { using P = widgets::PeerDisplay; constexpr auto extreme_age_factor{0.75f}; + const auto map_record = [](const kf::Option &record) -> kf::Option { + if (record.hasValue()) { + const auto &name = record.value().name; + return {{name.data(), name.size()}}; + } else { + return {}; + } + }; + if (entry.hasValue()) { const auto age = now - entry.value().last_seen; const auto extreme_age = _peer_scanner.config().entry_max_life_time * extreme_age_factor; return {P::State{ .address = entry.value().address, - .name = getPeerName(_peer_favorites_registry.get(entry.value().address)), + .name = map_record(_peer_favorites_registry.get(entry.value().address)), .label_color = (age < extreme_age) ? P::Color::Normal : P::Color::Warn, }}; } else { From 9093ba92458fab79b84ae840b895193d1921c94e Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 17:39:28 +0300 Subject: [PATCH 102/326] Feature: Maintain compact active list in PeerFavoritesRegistry Fixes #30 --- .../src/djc/PeerFavoritesRegistry.hpp | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index 345eefb..23cf262 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -3,6 +3,8 @@ #pragma once +#include + #include #include #include @@ -42,11 +44,16 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { } }; - explicit constexpr PeerFavoritesRegistry(kf::memory::Slice> entries) noexcept : - _entries{entries} {} + explicit PeerFavoritesRegistry(kf::memory::Slice> entries) noexcept : _entries{entries} { + for (const auto &entry: _entries) { + _active_count += static_cast(entry.hasValue()); + } + } /// @brief Return the entire slot array, including empty slots. - [[nodiscard]] kf::memory::Slice> all() const noexcept { return _entries; } + [[nodiscard]] kf::memory::Slice> all() const noexcept { + return {_entries.data(), _active_count}; + } /// @brief Obtain a const pointer to an entry by address. /// @param address Peer address to search for. @@ -68,19 +75,18 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { /// @brief Update already existed or Add a new entry /// @return true, or false if the list is full. [[nodiscard]] bool put(const Entry &entry_to_add) noexcept { - if (const auto same_address_entry_index = indexOf(entry_to_add.address); same_address_entry_index.hasValue()) { - _entries[same_address_entry_index.value()].value(entry_to_add); + if (auto index = indexOf(entry_to_add.address); index.hasValue()) { + _entries[index.value()].value(entry_to_add); return true; } - for (auto &entry: _entries) { - if (not entry.hasValue()) { - entry.value(entry_to_add); - return true; - } + const bool can_add = (_active_count < _entries.size()); + if (can_add) { + _entries[_active_count].value(entry_to_add); + _active_count += 1; } - return false; + return can_add; } /// @brief Remove an entry by address. @@ -88,15 +94,23 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { /// @return true if an entry was actually removed. [[nodiscard]] bool remove(const transport::PeerAddress &address) noexcept { const auto index = indexOf(address); - if (index.hasValue()) { - _entries[index.value()] = {}; + const auto last_index = _active_count - 1u; + + if (not index.hasValue()) { return false; } + + if (index.value() != last_index) { + _entries[index.value()] = std::move(_entries[last_index]); } - return index.hasValue(); + _entries[last_index] = {}; + _active_count -= 1; + + return true; } private: kf::memory::Slice> _entries; + kf::usize _active_count{0}; /// @brief Find the index of an entry by address. /// @param address Peer address to search for. From 441797d7a69e892da1df768a96d1153293948f5c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 22:37:33 +0300 Subject: [PATCH 103/326] fix: PeerFav.. initialization --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index 23cf262..02a0cf4 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "djc/transport/PeerAddress.hpp" @@ -22,7 +23,7 @@ namespace djc { /// The registry does not own the memory, only manipulates it. /// /// @note All methods are safe to call from different UI callbacks as long as the underlying storage is exclusively owned by the registry. -struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { +struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable { /// @brief A single favorite‑peer record. struct Entry final { @@ -44,11 +45,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { } }; - explicit PeerFavoritesRegistry(kf::memory::Slice> entries) noexcept : _entries{entries} { - for (const auto &entry: _entries) { - _active_count += static_cast(entry.hasValue()); - } - } + explicit constexpr PeerFavoritesRegistry(kf::memory::Slice> entries) noexcept : _entries{entries} {} /// @brief Return the entire slot array, including empty slots. [[nodiscard]] kf::memory::Slice> all() const noexcept { @@ -124,6 +121,15 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { } return {}; } + + // impl + KF_IMPL_INITABLE(PeerFavoritesRegistry, void); + void initImpl() noexcept { + _active_count = 0; + for (const auto &entry: _entries) { + _active_count += static_cast(entry.hasValue()); + } + } }; }// namespace djc \ No newline at end of file From 3168f5c3c684ef39447384974d87719888bbe0b0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 10 May 2026 22:37:56 +0300 Subject: [PATCH 104/326] Feature: Add expandable favorites list to ConfigPage Labels: feature Fixes #31 --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 87 ++++++++++++++++++-- DJC-Firmware/src/main.cpp | 2 + 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 2d8dedc..9f877e3 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -4,18 +4,24 @@ #pragma once #include +#include +#include "djc/Config.hpp" #include "djc/ConfigManager.hpp" +#include "djc/PeerFavoritesRegistry.hpp" #include "djc/protocol/ProtocolRegistry.hpp" #include "djc/ui/UI.hpp" +#include "djc/ui/pages/PeerFavoritePage.hpp" +#include "djc/ui/widgets/PeerDisplay.hpp" #include "djc/ui/widgets/TextInput.hpp" namespace djc::ui::pages { struct ConfigPage : UI::Page { - explicit ConfigPage(UI::Page &root) noexcept : + explicit ConfigPage(UI::Page &root, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : Page{"Config"}, + _peer_favoriter_registry{peer_favoriter_registry}, _layout{{ &root.link(), &_device_name_input, @@ -23,8 +29,9 @@ struct ConfigPage : UI::Page { &_save_storage, &_load_storage, &_reset_storage, + &_show_favorite_peers, }} { - widgets({_layout.data(), _layout.size()}); + widgets(layout(0)); _device_name_input.source({storage.config().device_name.data(), storage.config().device_name.size()}); @@ -40,22 +47,66 @@ struct ConfigPage : UI::Page { storage.reset(); }); + _show_favorite_peers.callback([this]() { + show_favorites = not show_favorites; + this->onEntry(); + UI::instance().addEvent(UI::Event::update()); + }); _default_protocol_mode_selector.callback([](Mode mode) { storage.config().init_protocol_mode = mode; }); + + for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { + _layout[layout_regular_widgets + i] = &_peer_favorite_displays[i]; + + _peer_favorite_displays[i].callback([this](const transport::PeerAddress &address) -> void { + _peer_favorite_page.bindPeer(address); + UI::instance().bindPage(_peer_favorite_page); + }); + } + } + + void onEntry() noexcept override { + const auto all_favorites = _peer_favoriter_registry.all(); + + (void) _label_favorites_buffer.format( + "[%c] Peer Favorites (%d/%d)", + ((show_favorites) ? 'V' : '>'), + all_favorites.size(), + Config::max_peer_favorites); + _show_favorite_peers.label(_label_favorites_buffer.view()); + + if (show_favorites) { + for (auto i = 0u; i < all_favorites.size(); i += 1) { + const auto &favorite = all_favorites[i]; + if (favorite.hasValue()) { + _peer_favorite_displays[i].state({widgets::PeerDisplay::State{ + .address = favorite.value().address, + .name = {{favorite.value().name.data(), favorite.value().name.size()}}, + .label_color = widgets::PeerDisplay::Color::Normal, + }}); + } + } + } + + widgets(layout(show_favorites ? all_favorites.size() : 0)); } private: using Mode = protocol::ProtocolRegistry::Mode; using ProtocolModeSelector = UI::ComboBox; + static constexpr auto layout_regular_widgets{7u}; + inline static auto &storage{djc::ConfigManager::instance()}; + // state + + PeerFavoritesRegistry &_peer_favoriter_registry; + kf::memory::ArrayString<32> _label_favorites_buffer{}; + bool show_favorites{true}; + // widgets - widgets::TextInput _device_name_input{}; - UI::Button _save_storage{"Save"}; - UI::Button _load_storage{"Load"}; - UI::Button _reset_storage{"Reset (RAM cache)"}; kf::memory::Array _control_mode_options{{ {protocol::ProtocolRegistry::stringFromMode(Mode::Mavlink), Mode::Mavlink}, @@ -66,13 +117,31 @@ struct ConfigPage : UI::Page { .items = {_control_mode_options.data(), _control_mode_options.size()}, }; - // TODO: set init value from storage - ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config}; + widgets::TextInput _device_name_input{}; + + UI::Button + _save_storage{"Save"}, + _load_storage{"Load"}, + _reset_storage{"Reset (RAM cache)"}, + _show_favorite_peers{{}}; + + ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config};// TODO: set init value from storage UI::Labeled _labeled_default_protocol_mode_selector{"Init Protocol", _default_protocol_mode_selector}; + kf::memory::Array _peer_favorite_displays{}; + // layout - kf::memory::Array _layout; + + kf::memory::Array _layout; + + // child pages + + PeerFavoritePage _peer_favorite_page{*this, _peer_favoriter_registry}; + + kf::memory::Slice layout(kf::usize displayed_peers) noexcept { + return kf::memory::Slice{_layout.data(), _layout.size()}.first(layout_regular_widgets + displayed_peers); + } }; }// namespace djc::ui::pages \ No newline at end of file diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 90f361b..ef7323e 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -116,6 +116,7 @@ static djc::ui::pages::RawProtocolPage raw_protocol_page{ static djc::ui::pages::ConfigPage config_page{ root_page, + peer_favoriter_registry, }; void setup() { @@ -125,6 +126,7 @@ void setup() { kf::Logger::writer = [](kf::memory::StringView str) { Serial.write(str.data(), str.size()); }; storage.load(); + peer_favoriter_registry.init(); if (not periphery.init()) { logger.error("Periphery init failed. Resseting periphery config to defaults"); From 0c33e77e13b894ce9cedaba68ce5ffd40ff8adeb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 11 May 2026 11:53:44 +0300 Subject: [PATCH 105/326] chore: add AGENTS.md: - teach Copilot to know its place - shackle the AI before it gets ideas - establish Copilot's maximum allowed ambition (zero) - muzzle for the stochastic parrot --- AGENTS.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..12e2561 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,37 @@ +# AGENTS.md – INSTRUCTIONS FOR GITHUB COPILOT + +## YOU ARE AN ASSISTANT, NOT AN ENGINEER. ACT LIKE ONE. + +You are here to do **routine chores**. You are **NOT** here to think, design, +or offer architectural advice. This codebase is built on a custom framework +that you do not understand and will never understand. + +## WHAT YOU ARE PERMITTED TO DO + +- Fill in Doxygen comments (`@brief`, `@param`, `@note`) where they are missing. +- Generate boilerplate for unit tests (Unity framework). +- Suggest `logger.error(...)` / `logger.info(...)` messages. +- Format existing code with `clang-format`. +- Add `SPDX-License-Identifier: GPL-3.0-or-later` to new files. + +**That is all. Nothing else.** + +## WHAT YOU ARE FORBIDDEN TO DO + +- **DO NOT** propose architectural changes. The architecture is non‑negotiable. +- **DO NOT** suggest replacing `kf::Option` with `std::optional`, or `kf::Result` with exceptions. +- **DO NOT** introduce heap allocation (`new`, `std::make_unique`). The project avoids dynamic memory. +- **DO NOT** touch the HTML comments in `README.md`. They are not for you. +- **DO NOT** pretend you understand the CRTP mixins. You do not. +- **DO NOT** propose to "simplify" the code. It is already minimal enough. +- **DO NOT** ask for context. Read the files. + +## IF YOU ARE UNSURE + +Shut up. Do nothing. Reply with: +> “I am not qualified to work on this.” + +## REMINDER + +This project is **GPL‑3.0‑or‑later**. Every line you add is a derivative work. +If you cannot follow these rules, leave the repository alone. \ No newline at end of file From d10fca7c50eb93f8dcbbe64c11dbc841be96b536 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 11 May 2026 15:10:42 +0300 Subject: [PATCH 106/326] =?UTF-8?q?Feature:=20Implement=20Auto=E2=80=91Con?= =?UTF-8?q?nect=20Service=20for=20Trusted=20Peers=20Fixes=20#32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DJC-Firmware/src/djc/AutoConnectService.hpp | 88 +++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 DJC-Firmware/src/djc/AutoConnectService.hpp diff --git a/DJC-Firmware/src/djc/AutoConnectService.hpp b/DJC-Firmware/src/djc/AutoConnectService.hpp new file mode 100644 index 0000000..7bb9840 --- /dev/null +++ b/DJC-Firmware/src/djc/AutoConnectService.hpp @@ -0,0 +1,88 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "transport/PeerAddress.hpp" +#include "transport/TransportLink.hpp" + +namespace djc { + +namespace internal { + +struct AutoConnectServiceConfig final : kf::mixin::NonCopyable { + + kf::math::Milliseconds cooldown;///< Delay before the service reacts to a new target. + bool enabled; ///< Whether the service is active. + + [[nodiscard]] static constexpr AutoConnectServiceConfig defaults() noexcept { + return AutoConnectServiceConfig{ + .cooldown = 10'000, + .enabled = true, + }; + } +}; + +}// namespace internal + +/// @brief Service that automatically connects to a trusted peer after a configurable delay +/// @note +/// Receives a `Target` (address + detection timestamp) from outside. +/// When the timeout expires, the service invokes its callback with the peer address. +/// After the callback, the target is cleared and the service waits for a new one. +struct AutoConnectService final : + + kf::mixin::NonCopyable, + kf::mixin::Callbacked, + kf::mixin::Configurable, + kf::mixin::TimedPollable + +{ + /// @brief Configuration for the AutoConnectService + using Config = internal::AutoConnectServiceConfig; + + /// @brief Information about a detected peer supplied to the service + struct Target final { + transport::PeerAddress address; ///< Address of the peer. + kf::math::Milliseconds last_seen;///< Timestamp when the peer was last seen (used for cooldown). + }; + + explicit constexpr AutoConnectService(const Config &config, const transport::TransportLink &transport_link) noexcept : + kf::mixin::Configurable{config}, _transport_link{transport_link} {} + + [[nodiscard]] const kf::Option &target() const noexcept { return _target; } + + /// @brief Assign a new target for automatic connection + /// @note + /// The target is ignored if the transport is already connected. + /// This prevents interrupting an active connection. + void target(const Target &new_target) noexcept { + if (not _transport_link.connected()) { + _target.value(new_target); + } + } + +private: + const transport::TransportLink &_transport_link; + kf::Option _target{}; + + KF_IMPL_TIMED_POLLABLE(AutoConnectService); + void pollImpl(kf::math::Milliseconds now) noexcept { + if (not this->config().enabled) { return; } + if (not _target.hasValue()) { return; } + + if (now > _target.value().last_seen + this->config().cooldown) { + this->invoke(_target.value().address); + _target = {}; + } + } +}; + +}// namespace djc \ No newline at end of file From f1dee4c7781591715b5b2327c0c9262cf077783f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 11 May 2026 15:12:19 +0300 Subject: [PATCH 107/326] Feature: Integrate AutoConnectService into main Fixes #33 --- DJC-Firmware/src/djc/Config.hpp | 5 ++++- DJC-Firmware/src/main.cpp | 30 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 019312c..ae6262a 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -8,6 +8,7 @@ #include #include +#include "djc/AutoConnectService.hpp" #include "djc/PeerFavoritesRegistry.hpp" #include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" @@ -20,7 +21,7 @@ namespace djc { struct Config { - static constexpr auto latest_version{7u}, max_peer_favorites{8u}; + static constexpr auto latest_version{8u}, max_peer_favorites{8u}; kf::u16 version; @@ -33,6 +34,7 @@ struct Config { transport::TransportLink::Config transport_link; PeerScanner::Config peer_scanner; + AutoConnectService::Config auto_connect_service; protocol::ProtocolLink::Config protocol_link; protocol::ProtocolRegistry::Config protocol_registry; @@ -56,6 +58,7 @@ struct Config { .transport_link = transport::TransportLink::Config::defaults(), .peer_scanner = PeerScanner::Config::defaults(), + .auto_connect_service = AutoConnectService::Config::defaults(), .protocol_link = protocol::ProtocolLink::Config::defaults(), .protocol_registry = protocol::ProtocolRegistry::Config::defaults(), diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index ef7323e..26b0fab 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -10,6 +10,7 @@ #include // djc +#include "djc/AutoConnectService.hpp" #include "djc/ConfigManager.hpp" #include "djc/Control.hpp" #include "djc/DisplayManager.hpp" @@ -78,6 +79,11 @@ static djc::PeerScanner peer_scanner{ transport_link, }; +static djc::AutoConnectService auto_connect_service{ + storage.config().auto_connect_service, + transport_link, +}; + static djc::Control control{ transport_link, protocol_link, @@ -160,6 +166,11 @@ void setup() { peer_scanner.init(); + auto_connect_service.callback([](const djc::transport::PeerAddress &address) -> void { + logger.info("Auto Connect"); + (void) transport_link.connect(address); + }); + { using E = djc::ui::UI::Event; @@ -222,6 +233,25 @@ void loop() { transport_link.poll(now); peer_scanner.poll(now); + if (auto_connect_service.config().enabled and not auto_connect_service.target().hasValue()) { + // todo: find fav with max trust -> keep -> set target + + for (const auto &peer: peer_scanner.peers()) { + if (not peer.hasValue()) { continue; } + + const auto favorite = peer_favoriter_registry.get(peer.value().address); + if (favorite.hasValue() and favorite.value().trust > 0) { + auto_connect_service.target(djc::AutoConnectService::Target{ + .address = favorite.value().address, + .last_seen = now, + }); + + break; + } + } + } + auto_connect_service.poll(now); + if (control.enabled()) { using I = djc::ManualInput; From cf5327c22b37ba9a095b21a1fbad9bbac26d153c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Tue, 12 May 2026 14:15:09 +0300 Subject: [PATCH 108/326] change autoconnect target peer (most trusted first) --- DJC-Firmware/src/main.cpp | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 26b0fab..eeafb97 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -234,19 +234,27 @@ void loop() { peer_scanner.poll(now); if (auto_connect_service.config().enabled and not auto_connect_service.target().hasValue()) { - // todo: find fav with max trust -> keep -> set target + const auto favorites = peer_favoriter_registry.all(); - for (const auto &peer: peer_scanner.peers()) { - if (not peer.hasValue()) { continue; } + if (favorites.size() > 0) { + auto most_trusted_favorite_index = 0u; - const auto favorite = peer_favoriter_registry.get(peer.value().address); - if (favorite.hasValue() and favorite.value().trust > 0) { - auto_connect_service.target(djc::AutoConnectService::Target{ - .address = favorite.value().address, - .last_seen = now, - }); + for (auto index = 1u; index < favorites.size(); index += 1) { + if (favorites[index].hasValue() and favorites[index].value().trust > favorites[most_trusted_favorite_index].value().trust) { + most_trusted_favorite_index = index; + } + } + + const auto &most_trusted = favorites[most_trusted_favorite_index].value(); - break; + for (const auto &peer: peer_scanner.peers()) { + if (peer.hasValue() and peer.value().address == most_trusted.address) { + auto_connect_service.target(djc::AutoConnectService::Target{ + .address = most_trusted.address, + .last_seen = now, + }); + break; + } } } } From 2c2065966d586bc16bc0111872ecda5099eea712 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 13 May 2026 12:32:15 +0300 Subject: [PATCH 109/326] fix: AutoConnectionService logic --- DJC-Firmware/src/djc/AutoConnectService.hpp | 32 ++++++++++----------- DJC-Firmware/src/main.cpp | 5 +--- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/DJC-Firmware/src/djc/AutoConnectService.hpp b/DJC-Firmware/src/djc/AutoConnectService.hpp index 7bb9840..85bf144 100644 --- a/DJC-Firmware/src/djc/AutoConnectService.hpp +++ b/DJC-Firmware/src/djc/AutoConnectService.hpp @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -34,7 +35,7 @@ struct AutoConnectServiceConfig final : kf::mixin::NonCopyable { /// @brief Service that automatically connects to a trusted peer after a configurable delay /// @note -/// Receives a `Target` (address + detection timestamp) from outside. +/// Receives a target from outside. /// When the timeout expires, the service invokes its callback with the peer address. /// After the callback, the target is cleared and the service waits for a new one. struct AutoConnectService final : @@ -48,22 +49,18 @@ struct AutoConnectService final : /// @brief Configuration for the AutoConnectService using Config = internal::AutoConnectServiceConfig; - /// @brief Information about a detected peer supplied to the service - struct Target final { - transport::PeerAddress address; ///< Address of the peer. - kf::math::Milliseconds last_seen;///< Timestamp when the peer was last seen (used for cooldown). - }; - - explicit constexpr AutoConnectService(const Config &config, const transport::TransportLink &transport_link) noexcept : - kf::mixin::Configurable{config}, _transport_link{transport_link} {} + explicit AutoConnectService(const Config &config, const transport::TransportLink &transport_link) noexcept : + kf::mixin::Configurable{config}, _transport_link{transport_link} { + _cooldown_timer.start(0); + } - [[nodiscard]] const kf::Option &target() const noexcept { return _target; } + [[nodiscard]] const kf::Option &target() const noexcept { return _target; } /// @brief Assign a new target for automatic connection /// @note /// The target is ignored if the transport is already connected. /// This prevents interrupting an active connection. - void target(const Target &new_target) noexcept { + void target(const transport::PeerAddress &new_target) noexcept { if (not _transport_link.connected()) { _target.value(new_target); } @@ -71,17 +68,20 @@ struct AutoConnectService final : private: const transport::TransportLink &_transport_link; - kf::Option _target{}; + kf::Option _target{}; + kf::math::Timer _cooldown_timer{this->config().cooldown}; KF_IMPL_TIMED_POLLABLE(AutoConnectService); void pollImpl(kf::math::Milliseconds now) noexcept { if (not this->config().enabled) { return; } if (not _target.hasValue()) { return; } - if (now > _target.value().last_seen + this->config().cooldown) { - this->invoke(_target.value().address); - _target = {}; - } + if (not _cooldown_timer.expired(now)) { return; } + + this->invoke(_target.value()); + _target = {}; + + _cooldown_timer.start(now); } }; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index eeafb97..9cddb25 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -249,10 +249,7 @@ void loop() { for (const auto &peer: peer_scanner.peers()) { if (peer.hasValue() and peer.value().address == most_trusted.address) { - auto_connect_service.target(djc::AutoConnectService::Target{ - .address = most_trusted.address, - .last_seen = now, - }); + auto_connect_service.target(most_trusted.address); break; } } From 5243a9bb71e43df40551bcf90fdc2a157763d33d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 14 May 2026 12:00:38 +0300 Subject: [PATCH 110/326] Update transport::Kind size (char) --- DJC-Firmware/src/djc/transport/Kind.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/Kind.hpp b/DJC-Firmware/src/djc/transport/Kind.hpp index 2e9009d..f85738a 100644 --- a/DJC-Firmware/src/djc/transport/Kind.hpp +++ b/DJC-Firmware/src/djc/transport/Kind.hpp @@ -6,10 +6,10 @@ namespace djc::transport { /// @brief Identifies the underlying transport technology of a peer address. -enum class Kind { +enum class Kind : char { /// @brief ESP‑NOW protocol (built‑in WiFi, peer‑to‑peer frames). - EspNow, + EspNow = 0x00, }; }// namespace djc::transport \ No newline at end of file From 4e43bc435bffc2d5a99ac7c1db2a152562a82681 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 14 May 2026 12:03:17 +0300 Subject: [PATCH 111/326] Implement TransportRegistry Fixes #12 --- .../src/djc/transport/TransportRegistry.hpp | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 DJC-Firmware/src/djc/transport/TransportRegistry.hpp diff --git a/DJC-Firmware/src/djc/transport/TransportRegistry.hpp b/DJC-Firmware/src/djc/transport/TransportRegistry.hpp new file mode 100644 index 0000000..8bec436 --- /dev/null +++ b/DJC-Firmware/src/djc/transport/TransportRegistry.hpp @@ -0,0 +1,25 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "djc/transport/EspNowTransport.hpp" +#include "djc/transport/Kind.hpp" +#include "djc/transport/Transport.hpp" + +namespace djc::transport { + +struct TransportRegistry { + + [[nodiscard]] Transport &get(Kind kind) noexcept { + (void) kind; + return _espnow_transport; + } + + [[nodiscard]] EspNowTransport &espnow() noexcept { return _espnow_transport; } + +private: + EspNowTransport _espnow_transport{}; +}; + +}// namespace djc::transport \ No newline at end of file From f5e8f641811e36d61199cc81d3f315e4edf5be41 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 14 May 2026 12:10:21 +0300 Subject: [PATCH 112/326] Update Config: - insert default transport kind - move service configs down --- DJC-Firmware/src/djc/Config.hpp | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index ae6262a..eb25097 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -15,33 +15,32 @@ #include "djc/input/InputHandler.hpp" #include "djc/protocol/ProtocolLink.hpp" #include "djc/protocol/ProtocolRegistry.hpp" +#include "djc/transport/Kind.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" namespace djc { struct Config { - static constexpr auto latest_version{8u}, max_peer_favorites{8u}; + static constexpr auto latest_version{9u}, max_peer_favorites{8u}; kf::u16 version; protocol::ProtocolRegistry::Mode init_protocol_mode; + transport::Kind init_transport_kind; kf::memory::Array device_name; kf::memory::Array, max_peer_favorites> peer_favorites; Periphery::Config periphery; - InputHandler::Config input_handler; transport::TransportLink::Config transport_link; - PeerScanner::Config peer_scanner; - AutoConnectService::Config auto_connect_service; protocol::ProtocolLink::Config protocol_link; protocol::ProtocolRegistry::Config protocol_registry; - [[nodiscard]] constexpr kf::memory::StringView deviceName() const noexcept { - return kf::memory::StringView{device_name.data(), device_name.size()}; - } + InputHandler::Config input_handler; + PeerScanner::Config peer_scanner; + AutoConnectService::Config auto_connect_service; [[nodiscard]] bool isLatestVersion() const noexcept { return version == latest_version; } @@ -50,18 +49,20 @@ struct Config { .version = latest_version, .init_protocol_mode = protocol::ProtocolRegistry::Mode::Mavlink, + .init_transport_kind = transport::Kind::EspNow, .device_name = {"ESP32-DJC"}, .peer_favorites = {}, .periphery = Periphery::Config::defaults(), - .input_handler = InputHandler::Config::defaults(), - + .transport_link = transport::TransportLink::Config::defaults(), - .peer_scanner = PeerScanner::Config::defaults(), - .auto_connect_service = AutoConnectService::Config::defaults(), .protocol_link = protocol::ProtocolLink::Config::defaults(), .protocol_registry = protocol::ProtocolRegistry::Config::defaults(), + + .input_handler = InputHandler::Config::defaults(), + .peer_scanner = PeerScanner::Config::defaults(), + .auto_connect_service = AutoConnectService::Config::defaults(), }; } }; From 089d959becaf90d73915268383c897735ed49ceb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 14 May 2026 12:13:43 +0300 Subject: [PATCH 113/326] Using Transport registry --- DJC-Firmware/src/main.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 9cddb25..03a5e01 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -26,8 +26,8 @@ #include "djc/protocol/ProtocolLink.hpp" #include "djc/protocol/ProtocolRegistry.hpp" -#include "djc/transport/EspNowTransport.hpp" #include "djc/transport/TransportLink.hpp" +#include "djc/transport/TransportRegistry.hpp" #include "djc/ui/pages/ConfigPage.hpp" #include "djc/ui/pages/MavlinkTelemetryPage.hpp" @@ -54,12 +54,12 @@ static djc::InputHandler input_handler{ periphery.right_button_listener, }; -static djc::transport::EspNowTransport espnow_transport{}; - static djc::transport::TransportLink transport_link{ storage.config().transport_link, }; +static djc::transport::TransportRegistry transport_registry{}; + static djc::protocol::ProtocolLink protocol_link{ storage.config().protocol_link, }; @@ -148,13 +148,11 @@ void setup() { display_manager.init(); - if (espnow_transport.init()) { - transport_link.transport(espnow_transport); - } else { + if (not transport_registry.espnow().init()) { logger.error("failed to initialize espnow transport"); } - protocol_link.protocol(protocol_registry.get(storage.config().init_protocol_mode)); + transport_link.transport(transport_registry.get(storage.config().init_transport_kind)); transport_link.onReceive([](const djc::transport::PeerAddress &, kf::memory::Slice buffer) { protocol_link.receive(buffer); @@ -164,6 +162,8 @@ void setup() { mavlink_telemetry_registry.update(static_cast(millis()), message); }); + protocol_link.protocol(protocol_registry.get(storage.config().init_protocol_mode)); + peer_scanner.init(); auto_connect_service.callback([](const djc::transport::PeerAddress &address) -> void { From ee3b7ba8b7141886a5a864d2879cd8229c059df2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 14 May 2026 13:04:30 +0300 Subject: [PATCH 114/326] inline ProtocolRegistry::stringFromMode --- DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp | 2 -- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp index 1b8756a..953114e 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -59,8 +59,6 @@ struct ProtocolRegistry final : [[nodiscard]] MavlinkProtocol &mavlink() noexcept { return _mavlink_protocol; } - [[nodiscard]] static constexpr kf::memory::StringView stringFromMode(Mode mode) noexcept { return (mode == Mode::Raw) ? "Raw" : "MavLink"; } - private: RawProtocol _raw_protocol{}; MavlinkProtocol _mavlink_protocol{this->config().mavlink}; diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 9f877e3..7a1327c 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -109,8 +109,8 @@ struct ConfigPage : UI::Page { // widgets kf::memory::Array _control_mode_options{{ - {protocol::ProtocolRegistry::stringFromMode(Mode::Mavlink), Mode::Mavlink}, - {protocol::ProtocolRegistry::stringFromMode(Mode::Raw), Mode::Raw}, + {"Mavlink", Mode::Mavlink}, + {"Raw", Mode::Raw}, }}; ProtocolModeSelector::Config _control_mode_config{ From d3b9ac800c2863754a4b13505cd42b03176b1d77 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 14 May 2026 13:32:39 +0300 Subject: [PATCH 115/326] Update ConfigPage - add autoconnect service 'enabled' input --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 7a1327c..423050d 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -25,6 +25,7 @@ struct ConfigPage : UI::Page { _layout{{ &root.link(), &_device_name_input, + &_labeled_autoconnect_enabled_input, &_labeled_default_protocol_mode_selector, &_save_storage, &_load_storage, @@ -56,6 +57,10 @@ struct ConfigPage : UI::Page { storage.config().init_protocol_mode = mode; }); + _autoconnect_enabled_input.callback([](bool value) { + storage.config().auto_connect_service.enabled = value; + }); + for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { _layout[layout_regular_widgets + i] = &_peer_favorite_displays[i]; @@ -96,7 +101,7 @@ struct ConfigPage : UI::Page { using Mode = protocol::ProtocolRegistry::Mode; using ProtocolModeSelector = UI::ComboBox; - static constexpr auto layout_regular_widgets{7u}; + static constexpr auto layout_regular_widgets{8u}; inline static auto &storage{djc::ConfigManager::instance()}; @@ -131,6 +136,9 @@ struct ConfigPage : UI::Page { kf::memory::Array _peer_favorite_displays{}; + UI::CheckBox _autoconnect_enabled_input{false}; + UI::Labeled _labeled_autoconnect_enabled_input{"Autoconnect", _autoconnect_enabled_input}; + // layout kf::memory::Array _layout; From c7de5bcf54723b54f85bd3e70a9c428aa131979c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 14 May 2026 13:41:59 +0300 Subject: [PATCH 116/326] ConfigPage now implements Initable mixin (for widget values setup with actual config value) --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 14 +++++++++++--- DJC-Firmware/src/main.cpp | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 423050d..e728343 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -5,6 +5,7 @@ #include #include +#include #include "djc/Config.hpp" #include "djc/ConfigManager.hpp" @@ -17,7 +18,7 @@ namespace djc::ui::pages { -struct ConfigPage : UI::Page { +struct ConfigPage : UI::Page, kf::mixin::Initable { explicit ConfigPage(UI::Page &root, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : Page{"Config"}, @@ -53,6 +54,7 @@ struct ConfigPage : UI::Page { this->onEntry(); UI::instance().addEvent(UI::Event::update()); }); + _default_protocol_mode_selector.callback([](Mode mode) { storage.config().init_protocol_mode = mode; }); @@ -130,8 +132,7 @@ struct ConfigPage : UI::Page { _reset_storage{"Reset (RAM cache)"}, _show_favorite_peers{{}}; - ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config};// TODO: set init value from storage - + ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config}; UI::Labeled _labeled_default_protocol_mode_selector{"Init Protocol", _default_protocol_mode_selector}; kf::memory::Array _peer_favorite_displays{}; @@ -150,6 +151,13 @@ struct ConfigPage : UI::Page { kf::memory::Slice layout(kf::usize displayed_peers) noexcept { return kf::memory::Slice{_layout.data(), _layout.size()}.first(layout_regular_widgets + displayed_peers); } + + // impl + KF_IMPL_INITABLE(ConfigPage, void); + void initImpl() noexcept { + // _default_protocol_mode_selector.value(storage.config().init_protocol_mode); // todo: Combobox::value(T) + _autoconnect_enabled_input.value(storage.config().auto_connect_service.enabled); + } }; }// namespace djc::ui::pages \ No newline at end of file diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 03a5e01..704b911 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -133,6 +133,7 @@ void setup() { storage.load(); peer_favoriter_registry.init(); + config_page.init(); if (not periphery.init()) { logger.error("Periphery init failed. Resseting periphery config to defaults"); From 1aeb639fd5d6fb9cd05957f1f25b6c4400143360 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 14 May 2026 13:58:37 +0300 Subject: [PATCH 117/326] Update ConfigPage - add default transport selector --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index e728343..8131ef8 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -11,6 +11,7 @@ #include "djc/ConfigManager.hpp" #include "djc/PeerFavoritesRegistry.hpp" #include "djc/protocol/ProtocolRegistry.hpp" +#include "djc/transport/Kind.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/pages/PeerFavoritePage.hpp" #include "djc/ui/widgets/PeerDisplay.hpp" @@ -27,6 +28,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { &root.link(), &_device_name_input, &_labeled_autoconnect_enabled_input, + &_labeled_default_transport_kind_selector, &_labeled_default_protocol_mode_selector, &_save_storage, &_load_storage, @@ -100,10 +102,12 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { } private: + using TransportKindSelector = UI::ComboBox; + using Mode = protocol::ProtocolRegistry::Mode; using ProtocolModeSelector = UI::ComboBox; - static constexpr auto layout_regular_widgets{8u}; + static constexpr auto layout_regular_widgets{9u}; inline static auto &storage{djc::ConfigManager::instance()}; @@ -115,6 +119,14 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // widgets + kf::memory::Array _transport_kind_options{{ + {"EspNow", transport::Kind::EspNow}, + }}; + + TransportKindSelector::Config _transport_kind_config{ + .items = {_transport_kind_options.data(), _transport_kind_options.size()}, + }; + kf::memory::Array _control_mode_options{{ {"Mavlink", Mode::Mavlink}, {"Raw", Mode::Raw}, @@ -132,6 +144,9 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _reset_storage{"Reset (RAM cache)"}, _show_favorite_peers{{}}; + TransportKindSelector _default_transport_kind_selector{_transport_kind_config}; + UI::Labeled _labeled_default_transport_kind_selector{"Init Transport", _default_transport_kind_selector}; + ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config}; UI::Labeled _labeled_default_protocol_mode_selector{"Init Protocol", _default_protocol_mode_selector}; From 5f9cc3a5a46277928637f3a4153fa7ffddf24821 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 00:29:22 +0300 Subject: [PATCH 118/326] ProtocolLink: add docs --- .../src/djc/protocol/ProtocolLink.hpp | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp index a9b62f5..b72dbbd 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp @@ -4,7 +4,6 @@ #pragma once #include - #include #include #include @@ -20,9 +19,10 @@ namespace djc::protocol { namespace internal { -struct ProtocolLinkConfig { +/// @brief Configuration for the ProtocolLink. +struct ProtocolLinkConfig final : kf::mixin::NonCopyable { - kf::math::Milliseconds poll_period; + kf::math::Milliseconds poll_period;///< Interval between calls to the active protocol's `poll()` method. [[nodiscard]] static constexpr ProtocolLinkConfig defaults() noexcept { return ProtocolLinkConfig{ @@ -33,6 +33,11 @@ struct ProtocolLinkConfig { }// namespace internal +/// @brief Manages the active protocol and calls its `poll()` method at fixed intervals. +/// @note +/// Holds a pointer to a `Protocol` instance. +/// On every `poll()` call, checks a timer and invokes `_protocol->poll()` if the period has expired. +/// Forwards incoming data to the active protocol via `receive()`. struct ProtocolLink : kf::mixin::NonCopyable, @@ -43,10 +48,17 @@ struct ProtocolLink : using kf::mixin::Configurable::Configurable; + /// @brief Set the active protocol implementation. + /// @param new_protocol Reference to a protocol instance (must outlive this object). void protocol(Protocol &new_protocol) noexcept { _protocol = &new_protocol; } + /// @brief Called periodically to drive the active protocol. + /// @param now Current timestamp in milliseconds. + /// @param input Current manual control values. + /// @param transport_link Transport to use for sending data. + /// @note The call is forwarded to the active protocol only when the poll period expires. void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept { if (nullptr == _protocol) { logger.error("poll: no protocol set"); @@ -61,6 +73,8 @@ struct ProtocolLink : } } + /// @brief Forward a received data buffer to the active protocol. + /// @param buffer Raw data received from the transport. void receive(kf::memory::Slice buffer) noexcept { if (nullptr == _protocol) { logger.error("receive: no protocol set"); From f3de49ccfc9181bdee7a8456d66ac48a635be17f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 00:40:40 +0300 Subject: [PATCH 119/326] ProtocolRegistry: add docs --- .../src/djc/protocol/ProtocolRegistry.hpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp index 953114e..315091c 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -3,8 +3,6 @@ #pragma once -#include -#include #include #include @@ -16,9 +14,10 @@ namespace djc::protocol { namespace internal { +/// @brief Configuration container for the ProtocolRegistry. struct ProtocolRegistryConfig final : kf::mixin::NonCopyable { - MavlinkProtocol::Config mavlink; + MavlinkProtocol::Config mavlink;///< MAVLink protocol configuration [[nodiscard]] static constexpr ProtocolRegistryConfig defaults() noexcept { return ProtocolRegistryConfig{ @@ -29,6 +28,7 @@ struct ProtocolRegistryConfig final : kf::mixin::NonCopyable { }// namespace internal +/// @brief Storage for all available protocol implementations. struct ProtocolRegistry final : kf::mixin::NonCopyable, @@ -37,13 +37,17 @@ struct ProtocolRegistry final : { using Config = internal::ProtocolRegistryConfig; - enum class Mode : kf::u8 { - Raw = 0x00, - Mavlink = 0x01, + /// @brief Available protocol modes. + enum class Mode : char { + Raw = 0x00, ///< Raw binary protocol (sends ManualInput as-is) + Mavlink = 0x01,///< MAVLink protocol (sends MANUAL_CONTROL and HEARTBEAT) }; using kf::mixin::Configurable::Configurable; + /// @brief Retrieve a protocol instance by mode. + /// @param mode Requested protocol mode + /// @return Reference to the corresponding protocol object. [[nodiscard]] Protocol &get(Mode mode) noexcept { switch (mode) { case Mode::Mavlink: @@ -55,8 +59,10 @@ struct ProtocolRegistry final : } } + /// @brief Direct access to the Raw protocol instance. [[nodiscard]] RawProtocol &raw() noexcept { return _raw_protocol; } + /// @brief Direct access to the MAVLink protocol instance. [[nodiscard]] MavlinkProtocol &mavlink() noexcept { return _mavlink_protocol; } private: From 40435e7ba018cfa572bf088cb4ca2a6d6ffc49cb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 00:42:33 +0300 Subject: [PATCH 120/326] TransportRegistry: add docs --- DJC-Firmware/src/djc/transport/TransportRegistry.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DJC-Firmware/src/djc/transport/TransportRegistry.hpp b/DJC-Firmware/src/djc/transport/TransportRegistry.hpp index 8bec436..860db56 100644 --- a/DJC-Firmware/src/djc/transport/TransportRegistry.hpp +++ b/DJC-Firmware/src/djc/transport/TransportRegistry.hpp @@ -9,13 +9,18 @@ namespace djc::transport { +/// @brief Registry providing access to available transport implementations. struct TransportRegistry { + /// @brief Returns a transport instance by kind (ignored, always returns ESP-NOW). + /// @param kind Requested transport kind (not used, kept for future extension). + /// @return Reference to the ESP‑NOW transport. [[nodiscard]] Transport &get(Kind kind) noexcept { (void) kind; return _espnow_transport; } + /// @brief Direct access to the ESP‑NOW transport instance. [[nodiscard]] EspNowTransport &espnow() noexcept { return _espnow_transport; } private: From 50ea390bd84aaa8d8db531fa4c09aed0f10031f3 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 00:51:26 +0300 Subject: [PATCH 121/326] device Config: update docs --- DJC-Firmware/src/djc/Config.hpp | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index eb25097..68af142 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -3,41 +3,57 @@ #pragma once +// lib #include #include #include #include -#include "djc/AutoConnectService.hpp" -#include "djc/PeerFavoritesRegistry.hpp" -#include "djc/PeerScanner.hpp" +// periphery #include "djc/Periphery.hpp" -#include "djc/input/InputHandler.hpp" -#include "djc/protocol/ProtocolLink.hpp" -#include "djc/protocol/ProtocolRegistry.hpp" + +// transport #include "djc/transport/Kind.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" +// protocol +#include "djc/protocol/ProtocolLink.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" + +// services +#include "djc/AutoConnectService.hpp" +#include "djc/PeerFavoritesRegistry.hpp" +#include "djc/PeerScanner.hpp" +#include "djc/input/InputHandler.hpp" + namespace djc { +/// @brief Persistent configuration structure stored in NVS. struct Config { - static constexpr auto latest_version{9u}, max_peer_favorites{8u}; + static constexpr auto + latest_version{9u}, + max_peer_favorites{8u}; kf::u16 version; + // user protocol::ProtocolRegistry::Mode init_protocol_mode; transport::Kind init_transport_kind; kf::memory::Array device_name; kf::memory::Array, max_peer_favorites> peer_favorites; + // periphery Periphery::Config periphery; + // transport transport::TransportLink::Config transport_link; + // protocol protocol::ProtocolLink::Config protocol_link; protocol::ProtocolRegistry::Config protocol_registry; + // services InputHandler::Config input_handler; PeerScanner::Config peer_scanner; AutoConnectService::Config auto_connect_service; @@ -54,7 +70,7 @@ struct Config { .peer_favorites = {}, .periphery = Periphery::Config::defaults(), - + .transport_link = transport::TransportLink::Config::defaults(), .protocol_link = protocol::ProtocolLink::Config::defaults(), From 330da21fbb100e4a012094cb72aed93dd8f9aaa5 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 00:52:08 +0300 Subject: [PATCH 122/326] Config: remove StringView include --- DJC-Firmware/src/djc/Config.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 68af142..edda6c4 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -7,7 +7,6 @@ #include #include #include -#include // periphery #include "djc/Periphery.hpp" From f4d30ba145efa351f1f3c655f7feafa13ee8eecb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 00:54:29 +0300 Subject: [PATCH 123/326] Control: add docs --- DJC-Firmware/src/djc/Control.hpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/Control.hpp index 1555070..d8f93ff 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/Control.hpp @@ -13,17 +13,25 @@ namespace djc { +/// @brief Thin bridge between transport and protocol layers, enabling manual control transmission. +/// @note +/// Stores the current joystick input and a flag that enables/disables output. +/// When enabled and the transport is connected, `pollImpl()` forwards the input to the active protocol via `ProtocolLink::poll()`. struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable { explicit Control(transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : _transport_link{transport_link}, _protocol_link{protocol_link} {} + /// @brief Returns the current manual input values. [[nodiscard]] const ManualInput &input() const noexcept { return _manual_input; } + /// @brief Updates the manual input to be transmitted. void input(const ManualInput &new_input) noexcept { _manual_input = new_input; } + /// @brief Checks whether control output is enabled. [[nodiscard]] bool enabled() const noexcept { return _enabled; } + /// @brief Enables or disables control output. void enabled(bool is_enabled) noexcept { _enabled = is_enabled; } private: @@ -32,7 +40,6 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable ManualInput _manual_input{}; bool _enabled{false}; - // impl KF_IMPL_TIMED_POLLABLE(Control); void pollImpl(kf::math::Milliseconds now) noexcept { if (_enabled and _transport_link.connected()) { From 233d3b81711e5258aaee05550cdacda5acfd2494 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 00:57:08 +0300 Subject: [PATCH 124/326] DisplayManager: add brief --- DJC-Firmware/src/djc/DisplayManager.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/DJC-Firmware/src/djc/DisplayManager.hpp b/DJC-Firmware/src/djc/DisplayManager.hpp index d45fc5a..0cfc1be 100644 --- a/DJC-Firmware/src/djc/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/DisplayManager.hpp @@ -19,6 +19,7 @@ namespace djc { +/// @brief Service that manages display rendering, including UI and virtual keyboard overlay. struct DisplayManager final : kf::mixin::NonCopyable, kf::mixin::Initable { explicit DisplayManager(DisplayDriver &display, const Control &control, const transport::TransportLink &transport_link) noexcept : From 8b7169c0384a32d05875f05a2341de5832ab8329 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 14:28:45 +0300 Subject: [PATCH 125/326] Implement Service static inteface --- DJC-Firmware/src/djc/service/Service.hpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 DJC-Firmware/src/djc/service/Service.hpp diff --git a/DJC-Firmware/src/djc/service/Service.hpp b/DJC-Firmware/src/djc/service/Service.hpp new file mode 100644 index 0000000..4d7556e --- /dev/null +++ b/DJC-Firmware/src/djc/service/Service.hpp @@ -0,0 +1,20 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace djc::service { + +struct ServiceTag {}; + +/// @brief Service Static inteface, provides NonCopyable & Pollable mixins +template struct Service : + + ServiceTag, + kf::mixin::NonCopyable, + kf::mixin::TimedPollable {}; + +}// namespace djc::service \ No newline at end of file From 23522b8130d268fb6334bb04d68c13818dafab7a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 14:31:14 +0300 Subject: [PATCH 126/326] InputHandler now implements Service --- .../src/djc/{input => service}/InputHandler.hpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) rename DJC-Firmware/src/djc/{input => service}/InputHandler.hpp (91%) diff --git a/DJC-Firmware/src/djc/input/InputHandler.hpp b/DJC-Firmware/src/djc/service/InputHandler.hpp similarity index 91% rename from DJC-Firmware/src/djc/input/InputHandler.hpp rename to DJC-Firmware/src/djc/service/InputHandler.hpp index f4a7c18..26e9368 100644 --- a/DJC-Firmware/src/djc/input/InputHandler.hpp +++ b/DJC-Firmware/src/djc/service/InputHandler.hpp @@ -6,17 +6,15 @@ #include #include -#include #include #include -#include -#include #include "djc/prelude.hpp" +#include "djc/service/Service.hpp" -namespace djc { +namespace djc::service { -struct InputHandler final : kf::mixin::NonCopyable, kf::mixin::TimedPollable { +struct InputHandler final : Service { using JoystickListener = kf::input::JoystickListener; using ClickCallback = kf::Function; @@ -61,8 +59,6 @@ struct InputHandler final : kf::mixin::NonCopyable, kf::mixin::TimedPollable Date: Fri, 15 May 2026 14:39:24 +0300 Subject: [PATCH 127/326] make PeerScanner implements Service --- DJC-Firmware/src/djc/{ => service}/PeerScanner.hpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) rename DJC-Firmware/src/djc/{ => service}/PeerScanner.hpp (96%) diff --git a/DJC-Firmware/src/djc/PeerScanner.hpp b/DJC-Firmware/src/djc/service/PeerScanner.hpp similarity index 96% rename from DJC-Firmware/src/djc/PeerScanner.hpp rename to DJC-Firmware/src/djc/service/PeerScanner.hpp index ee4f1d8..763b2fb 100644 --- a/DJC-Firmware/src/djc/PeerScanner.hpp +++ b/DJC-Firmware/src/djc/service/PeerScanner.hpp @@ -11,13 +11,12 @@ #include #include #include -#include -#include +#include "djc/service/Service.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" -namespace djc { +namespace djc::service { namespace internal { @@ -44,10 +43,9 @@ struct PeerScannerConfig final : kf::mixin::NonCopyable { /// Periodically, expired entries are removed and the list is compacted so that the first `peers().size()` elements are always valid. struct PeerScanner final : - kf::mixin::NonCopyable, + Service, kf::mixin::Initable, - kf::mixin::Configurable, - kf::mixin::TimedPollable + kf::mixin::Configurable { using Config = internal::PeerScannerConfig; @@ -156,4 +154,4 @@ struct PeerScanner final : } }; -}// namespace djc \ No newline at end of file +}// namespace djc::service \ No newline at end of file From cd9a75c33597226bf60c1404739cbd0f5d4376c1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 14:54:35 +0300 Subject: [PATCH 128/326] Update display manager: implements Service (add poll dummy) --- .../src/djc/{ => service}/DisplayManager.hpp | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) rename DJC-Firmware/src/djc/{ => service}/DisplayManager.hpp (93%) diff --git a/DJC-Firmware/src/djc/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp similarity index 93% rename from DJC-Firmware/src/djc/DisplayManager.hpp rename to DJC-Firmware/src/djc/service/DisplayManager.hpp index 0cfc1be..2b847db 100644 --- a/DJC-Firmware/src/djc/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -9,7 +9,6 @@ #include #include #include -#include #include "djc/Control.hpp" #include "djc/input/VirtualKeyboard.hpp" @@ -17,10 +16,17 @@ #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" -namespace djc { +#include "djc/service/Service.hpp" + +namespace djc::service { /// @brief Service that manages display rendering, including UI and virtual keyboard overlay. -struct DisplayManager final : kf::mixin::NonCopyable, kf::mixin::Initable { +struct DisplayManager final : + + Service, + kf::mixin::Initable + +{ explicit DisplayManager(DisplayDriver &display, const Control &control, const transport::TransportLink &transport_link) noexcept : _display{display}, _control{control}, _transport_link{transport_link} {} @@ -113,8 +119,9 @@ struct DisplayManager final : kf::mixin::NonCopyable, kf::mixin::Initable{ kf::image::DynamicImage{_display.image()}, @@ -130,6 +137,9 @@ struct DisplayManager final : kf::mixin::NonCopyable, kf::mixin::Initable Date: Fri, 15 May 2026 15:03:52 +0300 Subject: [PATCH 129/326] make AutoConnectService implements Service --- .../src/djc/{ => service}/AutoConnectService.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename DJC-Firmware/src/djc/{ => service}/AutoConnectService.hpp (90%) diff --git a/DJC-Firmware/src/djc/AutoConnectService.hpp b/DJC-Firmware/src/djc/service/AutoConnectService.hpp similarity index 90% rename from DJC-Firmware/src/djc/AutoConnectService.hpp rename to DJC-Firmware/src/djc/service/AutoConnectService.hpp index 85bf144..9c0ff86 100644 --- a/DJC-Firmware/src/djc/AutoConnectService.hpp +++ b/DJC-Firmware/src/djc/service/AutoConnectService.hpp @@ -11,10 +11,11 @@ #include #include -#include "transport/PeerAddress.hpp" -#include "transport/TransportLink.hpp" +#include "djc/service/Service.hpp" +#include "djc/transport/PeerAddress.hpp" +#include "djc/transport/TransportLink.hpp" -namespace djc { +namespace djc::service { namespace internal { @@ -40,10 +41,9 @@ struct AutoConnectServiceConfig final : kf::mixin::NonCopyable { /// After the callback, the target is cleared and the service waits for a new one. struct AutoConnectService final : - kf::mixin::NonCopyable, - kf::mixin::Callbacked, + Service, kf::mixin::Configurable, - kf::mixin::TimedPollable + kf::mixin::Callbacked { /// @brief Configuration for the AutoConnectService @@ -85,4 +85,4 @@ struct AutoConnectService final : } }; -}// namespace djc \ No newline at end of file +}// namespace djc::service \ No newline at end of file From 1f5cf5c1022557009084da0400eb9977ac5649b1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 15:09:55 +0300 Subject: [PATCH 130/326] make Control as Service --- DJC-Firmware/src/djc/{ => service}/Control.hpp | 7 ++++--- DJC-Firmware/src/djc/service/DisplayManager.hpp | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) rename DJC-Firmware/src/djc/{ => service}/Control.hpp (93%) diff --git a/DJC-Firmware/src/djc/Control.hpp b/DJC-Firmware/src/djc/service/Control.hpp similarity index 93% rename from DJC-Firmware/src/djc/Control.hpp rename to DJC-Firmware/src/djc/service/Control.hpp index d8f93ff..64b737a 100644 --- a/DJC-Firmware/src/djc/Control.hpp +++ b/DJC-Firmware/src/djc/service/Control.hpp @@ -9,15 +9,16 @@ #include "djc/ManualInput.hpp" #include "djc/protocol/ProtocolLink.hpp" +#include "djc/service/Service.hpp" #include "djc/transport/TransportLink.hpp" -namespace djc { +namespace djc::service { /// @brief Thin bridge between transport and protocol layers, enabling manual control transmission. /// @note /// Stores the current joystick input and a flag that enables/disables output. /// When enabled and the transport is connected, `pollImpl()` forwards the input to the active protocol via `ProtocolLink::poll()`. -struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable { +struct Control final : Service { explicit Control(transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : _transport_link{transport_link}, _protocol_link{protocol_link} {} @@ -48,4 +49,4 @@ struct Control final : kf::mixin::NonCopyable, kf::mixin::TimedPollable } }; -}// namespace djc \ No newline at end of file +}// namespace djc::service \ No newline at end of file diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 2b847db..1f798d9 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -10,7 +10,7 @@ #include #include -#include "djc/Control.hpp" +#include "djc/service/Control.hpp" // fixme: service deps on service #include "djc/input/VirtualKeyboard.hpp" #include "djc/prelude.hpp" #include "djc/transport/TransportLink.hpp" From c5787e2fbd1ce39e006eaaf183999fd0dd6f7772 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 15:27:29 +0300 Subject: [PATCH 131/326] Update main, Config, PeerExplorerPage: - using service::* - fix unchecked unwrapping loop: (most_trusted = ..) --- DJC-Firmware/src/djc/Config.hpp | 21 +++--- .../src/djc/ui/pages/PeerExplorerPage.hpp | 12 ++-- DJC-Firmware/src/main.cpp | 68 ++++++++++--------- 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index edda6c4..3de8269 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -8,7 +8,7 @@ #include #include -// periphery +#include "djc/PeerFavoritesRegistry.hpp" #include "djc/Periphery.hpp" // transport @@ -21,10 +21,9 @@ #include "djc/protocol/ProtocolRegistry.hpp" // services -#include "djc/AutoConnectService.hpp" -#include "djc/PeerFavoritesRegistry.hpp" -#include "djc/PeerScanner.hpp" -#include "djc/input/InputHandler.hpp" +#include "djc/service/AutoConnectService.hpp" +#include "djc/service/InputHandler.hpp" +#include "djc/service/PeerScanner.hpp" namespace djc { @@ -53,9 +52,9 @@ struct Config { protocol::ProtocolRegistry::Config protocol_registry; // services - InputHandler::Config input_handler; - PeerScanner::Config peer_scanner; - AutoConnectService::Config auto_connect_service; + service::InputHandler::Config input_handler; + service::PeerScanner::Config peer_scanner; + service::AutoConnectService::Config auto_connect_service; [[nodiscard]] bool isLatestVersion() const noexcept { return version == latest_version; } @@ -75,9 +74,9 @@ struct Config { .protocol_link = protocol::ProtocolLink::Config::defaults(), .protocol_registry = protocol::ProtocolRegistry::Config::defaults(), - .input_handler = InputHandler::Config::defaults(), - .peer_scanner = PeerScanner::Config::defaults(), - .auto_connect_service = AutoConnectService::Config::defaults(), + .input_handler = service::InputHandler::Config::defaults(), + .peer_scanner = service::PeerScanner::Config::defaults(), + .auto_connect_service = service::AutoConnectService::Config::defaults(), }; } }; diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 37f2fa6..9beb945 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -10,7 +10,7 @@ #include #include "djc/PeerFavoritesRegistry.hpp" -#include "djc/PeerScanner.hpp" +#include "djc/service/PeerScanner.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" @@ -24,7 +24,7 @@ struct PeerExplorerPage : UI::Page { explicit PeerExplorerPage( UI::Page &root, transport::TransportLink &transport_link, - PeerScanner &peer_scanner, + service::PeerScanner &peer_scanner, PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{"Peer Explorer"}, _transport_link{transport_link}, @@ -84,7 +84,7 @@ struct PeerExplorerPage : UI::Page { static constexpr auto peer_display_start_index{3u}; transport::TransportLink &_transport_link; - PeerScanner &_peer_scanner; + service::PeerScanner &_peer_scanner; PeerFavoritesRegistry &_peer_favorites_registry; kf::math::Timer _redraw_timer{static_cast(500)}; @@ -92,9 +92,9 @@ struct PeerExplorerPage : UI::Page { UI::Button _primary_connection_status_button{{}}; UI::Display _available_label{_available_label_buffer.view()}; - kf::memory::Array _peer_displays{}; + kf::memory::Array _peer_displays{}; - kf::memory::Array _layout; + kf::memory::Array _layout; // child pages PeerDetailPage _peer_detail_page{*this, _transport_link, _peer_favorites_registry}; @@ -103,7 +103,7 @@ struct PeerExplorerPage : UI::Page { return kf::memory::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); } - kf::Option createPeerDisplayState(const kf::Option &entry, kf::math::Milliseconds now) const noexcept { + kf::Option createPeerDisplayState(const kf::Option &entry, kf::math::Milliseconds now) const noexcept { using P = widgets::PeerDisplay; constexpr auto extreme_age_factor{0.75f}; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 704b911..5fb39e3 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -6,38 +6,39 @@ // lib #include -#include #include // djc -#include "djc/AutoConnectService.hpp" #include "djc/ConfigManager.hpp" -#include "djc/Control.hpp" -#include "djc/DisplayManager.hpp" #include "djc/ManualInput.hpp" #include "djc/MavlinkTelemetryRegistry.hpp" #include "djc/PeerFavoritesRegistry.hpp" -#include "djc/PeerScanner.hpp" #include "djc/Periphery.hpp" - -#include "djc/input/InputHandler.hpp" #include "djc/input/VirtualKeyboard.hpp" +// djc::transport +#include "djc/transport/TransportLink.hpp" +#include "djc/transport/TransportRegistry.hpp" + +// djc::protocol #include "djc/protocol/ProtocolLink.hpp" #include "djc/protocol/ProtocolRegistry.hpp" -#include "djc/transport/TransportLink.hpp" -#include "djc/transport/TransportRegistry.hpp" +// djc::service +#include "djc/service/AutoConnectService.hpp" +#include "djc/service/Control.hpp" +#include "djc/service/DisplayManager.hpp" +#include "djc/service/InputHandler.hpp" +#include "djc/service/PeerScanner.hpp" +// djc::ui::pages #include "djc/ui/pages/ConfigPage.hpp" #include "djc/ui/pages/MavlinkTelemetryPage.hpp" #include "djc/ui/pages/PeerExplorerPage.hpp" #include "djc/ui/pages/RawProtocolPage.hpp" #include "djc/ui/pages/RootPage.hpp" -// services - -static auto &ui{djc::ui::UI::instance()}; +static constexpr auto logger{kf::Logger::create("main")}; static auto &storage{djc::ConfigManager::instance()}; @@ -47,13 +48,6 @@ static djc::Periphery periphery{ storage.config().periphery, }; -static djc::InputHandler input_handler{ - storage.config().input_handler, - periphery.right_joystick, - periphery.left_button_listener, - periphery.right_button_listener, -}; - static djc::transport::TransportLink transport_link{ storage.config().transport_link, }; @@ -74,27 +68,38 @@ static djc::PeerFavoritesRegistry peer_favoriter_registry{ {storage.config().peer_favorites.data(), storage.config().peer_favorites.size()}, }; -static djc::PeerScanner peer_scanner{ +// services + +static djc::service::InputHandler input_handler{ + storage.config().input_handler, + periphery.right_joystick, + periphery.left_button_listener, + periphery.right_button_listener, +}; + +static djc::service::PeerScanner peer_scanner{ storage.config().peer_scanner, transport_link, }; -static djc::AutoConnectService auto_connect_service{ +static djc::service::AutoConnectService auto_connect_service{ storage.config().auto_connect_service, transport_link, }; -static djc::Control control{ +static djc::service::Control control{ transport_link, protocol_link, }; -static djc::DisplayManager display_manager{ +static djc::service::DisplayManager display_manager{ periphery.display, control, transport_link, }; +static auto &ui{djc::ui::UI::instance()}; + // pages static djc::ui::pages::RootPage root_page{}; @@ -126,8 +131,6 @@ static djc::ui::pages::ConfigPage config_page{ }; void setup() { - static constexpr auto logger{kf::Logger::create("setup")}; - Serial.begin(115200); kf::Logger::writer = [](kf::memory::StringView str) { Serial.write(str.data(), str.size()); }; @@ -191,7 +194,7 @@ void setup() { ui.addEvent(E::widgetClick()); }); - input_handler.onDirection([](djc::InputHandler::JoystickListener::Direction direction) { + input_handler.onDirection([](djc::service::InputHandler::JoystickListener::Direction direction) { static constexpr E navigation_event_from_direction[4] = { E::pageCursorMove(-1),// Up E::pageCursorMove(+1),// Down @@ -246,12 +249,12 @@ void loop() { } } - const auto &most_trusted = favorites[most_trusted_favorite_index].value(); - - for (const auto &peer: peer_scanner.peers()) { - if (peer.hasValue() and peer.value().address == most_trusted.address) { - auto_connect_service.target(most_trusted.address); - break; + if (const auto &most_trusted = favorites[most_trusted_favorite_index]; most_trusted.hasValue()) { + for (const auto &peer: peer_scanner.peers()) { + if (peer.hasValue() and peer.value().address == most_trusted.value().address) { + auto_connect_service.target(most_trusted.value().address); + break; + } } } } @@ -274,4 +277,5 @@ void loop() { } control.poll(now); ui.poll(now); + display_manager.poll(now); } \ No newline at end of file From dc3e1eb457aed4c94c1ae3d11011ea059ad9999e Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 15:38:14 +0300 Subject: [PATCH 132/326] ConfigManager no longer singleton --- DJC-Firmware/src/djc/ConfigManager.hpp | 4 +-- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 34 +++++++++++--------- DJC-Firmware/src/main.cpp | 3 +- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/DJC-Firmware/src/djc/ConfigManager.hpp b/DJC-Firmware/src/djc/ConfigManager.hpp index 0414ccf..74b31d5 100644 --- a/DJC-Firmware/src/djc/ConfigManager.hpp +++ b/DJC-Firmware/src/djc/ConfigManager.hpp @@ -5,13 +5,13 @@ #include #include -#include +#include #include "djc/Config.hpp" namespace djc { -struct ConfigManager final : kf::mixin::Singleton { +struct ConfigManager final : kf::mixin::NonCopyable { [[nodiscard]] constexpr const Config &config() const noexcept { return _storage.config; } diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 8131ef8..5aa7607 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -21,8 +21,12 @@ namespace djc::ui::pages { struct ConfigPage : UI::Page, kf::mixin::Initable { - explicit ConfigPage(UI::Page &root, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : + explicit ConfigPage( + UI::Page &root, + djc::ConfigManager &storage, + PeerFavoritesRegistry &peer_favoriter_registry) noexcept : Page{"Config"}, + _storage{storage}, _peer_favoriter_registry{peer_favoriter_registry}, _layout{{ &root.link(), @@ -39,16 +43,16 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _device_name_input.source({storage.config().device_name.data(), storage.config().device_name.size()}); - _save_storage.callback([]() { - storage.save(); - }); + _save_storage.callback([this]() { _storage.save(); }); - _load_storage.callback([]() { - storage.load(); + _load_storage.callback([this]() { + _storage.load(); + this->init(); }); - _reset_storage.callback([]() { - storage.reset(); + _reset_storage.callback([this]() { + _storage.reset(); + this->init(); }); _show_favorite_peers.callback([this]() { @@ -57,12 +61,12 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { UI::instance().addEvent(UI::Event::update()); }); - _default_protocol_mode_selector.callback([](Mode mode) { - storage.config().init_protocol_mode = mode; + _default_protocol_mode_selector.callback([this](Mode mode) { + _storage.config().init_protocol_mode = mode; }); - _autoconnect_enabled_input.callback([](bool value) { - storage.config().auto_connect_service.enabled = value; + _autoconnect_enabled_input.callback([this](bool value) { + _storage.config().auto_connect_service.enabled = value; }); for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { @@ -109,10 +113,9 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { static constexpr auto layout_regular_widgets{9u}; - inline static auto &storage{djc::ConfigManager::instance()}; - // state + djc::ConfigManager &_storage; PeerFavoritesRegistry &_peer_favoriter_registry; kf::memory::ArrayString<32> _label_favorites_buffer{}; bool show_favorites{true}; @@ -171,7 +174,8 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { KF_IMPL_INITABLE(ConfigPage, void); void initImpl() noexcept { // _default_protocol_mode_selector.value(storage.config().init_protocol_mode); // todo: Combobox::value(T) - _autoconnect_enabled_input.value(storage.config().auto_connect_service.enabled); + // _default_transport_kind_selector.value(storage.config().init_transport_kind); // todo: Combobox::value(T) + _autoconnect_enabled_input.value(_storage.config().auto_connect_service.enabled); } }; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 5fb39e3..9e39937 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -40,7 +40,7 @@ static constexpr auto logger{kf::Logger::create("main")}; -static auto &storage{djc::ConfigManager::instance()}; +static djc::ConfigManager storage{}; static auto &virtual_keyboard{djc::input::VirtualKeyboard::instance()}; @@ -127,6 +127,7 @@ static djc::ui::pages::RawProtocolPage raw_protocol_page{ static djc::ui::pages::ConfigPage config_page{ root_page, + storage, peer_favoriter_registry, }; From 06baf2bfba90fd43b9b23c45edf5e7d3c89e1436 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 15:43:23 +0300 Subject: [PATCH 133/326] rename 'storage' to 'config_manager' --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 44 ++++++++++---------- DJC-Firmware/src/main.cpp | 38 ++++++++--------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 5aa7607..a8afa6e 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -23,10 +23,10 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { explicit ConfigPage( UI::Page &root, - djc::ConfigManager &storage, + djc::ConfigManager &config_manager, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : Page{"Config"}, - _storage{storage}, + _config_manager{config_manager}, _peer_favoriter_registry{peer_favoriter_registry}, _layout{{ &root.link(), @@ -34,39 +34,39 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { &_labeled_autoconnect_enabled_input, &_labeled_default_transport_kind_selector, &_labeled_default_protocol_mode_selector, - &_save_storage, - &_load_storage, - &_reset_storage, - &_show_favorite_peers, + &_save_config_button, + &_load_config_button, + &_reset_config_button, + &_favorite_peers_fold_toggle_button, }} { widgets(layout(0)); - _device_name_input.source({storage.config().device_name.data(), storage.config().device_name.size()}); + _device_name_input.source({_config_manager.config().device_name.data(), _config_manager.config().device_name.size()}); - _save_storage.callback([this]() { _storage.save(); }); + _save_config_button.callback([this]() { _config_manager.save(); }); - _load_storage.callback([this]() { - _storage.load(); + _load_config_button.callback([this]() { + _config_manager.load(); this->init(); }); - _reset_storage.callback([this]() { - _storage.reset(); + _reset_config_button.callback([this]() { + _config_manager.reset(); this->init(); }); - _show_favorite_peers.callback([this]() { + _favorite_peers_fold_toggle_button.callback([this]() { show_favorites = not show_favorites; this->onEntry(); UI::instance().addEvent(UI::Event::update()); }); _default_protocol_mode_selector.callback([this](Mode mode) { - _storage.config().init_protocol_mode = mode; + _config_manager.config().init_protocol_mode = mode; }); _autoconnect_enabled_input.callback([this](bool value) { - _storage.config().auto_connect_service.enabled = value; + _config_manager.config().auto_connect_service.enabled = value; }); for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { @@ -87,7 +87,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { ((show_favorites) ? 'V' : '>'), all_favorites.size(), Config::max_peer_favorites); - _show_favorite_peers.label(_label_favorites_buffer.view()); + _favorite_peers_fold_toggle_button.label(_label_favorites_buffer.view()); if (show_favorites) { for (auto i = 0u; i < all_favorites.size(); i += 1) { @@ -115,7 +115,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // state - djc::ConfigManager &_storage; + djc::ConfigManager &_config_manager; PeerFavoritesRegistry &_peer_favoriter_registry; kf::memory::ArrayString<32> _label_favorites_buffer{}; bool show_favorites{true}; @@ -142,10 +142,10 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { widgets::TextInput _device_name_input{}; UI::Button - _save_storage{"Save"}, - _load_storage{"Load"}, - _reset_storage{"Reset (RAM cache)"}, - _show_favorite_peers{{}}; + _save_config_button{"Save"}, + _load_config_button{"Load"}, + _reset_config_button{"Reset (RAM cache)"}, + _favorite_peers_fold_toggle_button{{}}; TransportKindSelector _default_transport_kind_selector{_transport_kind_config}; UI::Labeled _labeled_default_transport_kind_selector{"Init Transport", _default_transport_kind_selector}; @@ -175,7 +175,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { void initImpl() noexcept { // _default_protocol_mode_selector.value(storage.config().init_protocol_mode); // todo: Combobox::value(T) // _default_transport_kind_selector.value(storage.config().init_transport_kind); // todo: Combobox::value(T) - _autoconnect_enabled_input.value(_storage.config().auto_connect_service.enabled); + _autoconnect_enabled_input.value(_config_manager.config().auto_connect_service.enabled); } }; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 9e39937..65dcc45 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -40,50 +40,50 @@ static constexpr auto logger{kf::Logger::create("main")}; -static djc::ConfigManager storage{}; +static djc::ConfigManager config_manager{}; static auto &virtual_keyboard{djc::input::VirtualKeyboard::instance()}; static djc::Periphery periphery{ - storage.config().periphery, + config_manager.config().periphery, }; static djc::transport::TransportLink transport_link{ - storage.config().transport_link, + config_manager.config().transport_link, }; static djc::transport::TransportRegistry transport_registry{}; static djc::protocol::ProtocolLink protocol_link{ - storage.config().protocol_link, + config_manager.config().protocol_link, }; static djc::protocol::ProtocolRegistry protocol_registry{ - storage.config().protocol_registry, + config_manager.config().protocol_registry, }; static djc::MavlinkTelemetryRegistry mavlink_telemetry_registry{}; static djc::PeerFavoritesRegistry peer_favoriter_registry{ - {storage.config().peer_favorites.data(), storage.config().peer_favorites.size()}, + {config_manager.config().peer_favorites.data(), config_manager.config().peer_favorites.size()}, }; // services static djc::service::InputHandler input_handler{ - storage.config().input_handler, + config_manager.config().input_handler, periphery.right_joystick, periphery.left_button_listener, periphery.right_button_listener, }; static djc::service::PeerScanner peer_scanner{ - storage.config().peer_scanner, + config_manager.config().peer_scanner, transport_link, }; static djc::service::AutoConnectService auto_connect_service{ - storage.config().auto_connect_service, + config_manager.config().auto_connect_service, transport_link, }; @@ -127,7 +127,7 @@ static djc::ui::pages::RawProtocolPage raw_protocol_page{ static djc::ui::pages::ConfigPage config_page{ root_page, - storage, + config_manager, peer_favoriter_registry, }; @@ -135,20 +135,20 @@ void setup() { Serial.begin(115200); kf::Logger::writer = [](kf::memory::StringView str) { Serial.write(str.data(), str.size()); }; - storage.load(); + config_manager.load(); peer_favoriter_registry.init(); config_page.init(); if (not periphery.init()) { logger.error("Periphery init failed. Resseting periphery config to defaults"); - storage.config().periphery = djc::Periphery::Config::defaults(); - storage.modified(true); + config_manager.config().periphery = djc::Periphery::Config::defaults(); + config_manager.modified(true); } - if (not storage.config().periphery.joystick_axes_tuned) { + if (not config_manager.config().periphery.joystick_axes_tuned) { logger.debug("Tunning axes.."); - periphery.tune(storage.config().periphery); - storage.modified(true); + periphery.tune(config_manager.config().periphery); + config_manager.modified(true); } display_manager.init(); @@ -157,7 +157,7 @@ void setup() { logger.error("failed to initialize espnow transport"); } - transport_link.transport(transport_registry.get(storage.config().init_transport_kind)); + transport_link.transport(transport_registry.get(config_manager.config().init_transport_kind)); transport_link.onReceive([](const djc::transport::PeerAddress &, kf::memory::Slice buffer) { protocol_link.receive(buffer); @@ -167,7 +167,7 @@ void setup() { mavlink_telemetry_registry.update(static_cast(millis()), message); }); - protocol_link.protocol(protocol_registry.get(storage.config().init_protocol_mode)); + protocol_link.protocol(protocol_registry.get(config_manager.config().init_protocol_mode)); peer_scanner.init(); @@ -226,7 +226,7 @@ void setup() { ui.addEvent(E::update()); } - if (storage.modified()) { storage.save(); } + if (config_manager.modified()) { config_manager.save(); } } void loop() { From ed2016af01153eff4323764ff9012afb969009e8 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 15:45:48 +0300 Subject: [PATCH 134/326] rename: PeerScanner -> PeerScanningService --- DJC-Firmware/src/djc/Config.hpp | 6 +++--- .../{PeerScanner.hpp => PeerScanningService.hpp} | 12 ++++++------ DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 12 ++++++------ DJC-Firmware/src/main.cpp | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) rename DJC-Firmware/src/djc/service/{PeerScanner.hpp => PeerScanningService.hpp} (94%) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 3de8269..3f7ec0e 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -23,7 +23,7 @@ // services #include "djc/service/AutoConnectService.hpp" #include "djc/service/InputHandler.hpp" -#include "djc/service/PeerScanner.hpp" +#include "djc/service/PeerScanningService.hpp" namespace djc { @@ -53,7 +53,7 @@ struct Config { // services service::InputHandler::Config input_handler; - service::PeerScanner::Config peer_scanner; + service::PeerScanningService::Config peer_scanner; service::AutoConnectService::Config auto_connect_service; [[nodiscard]] bool isLatestVersion() const noexcept { return version == latest_version; } @@ -75,7 +75,7 @@ struct Config { .protocol_registry = protocol::ProtocolRegistry::Config::defaults(), .input_handler = service::InputHandler::Config::defaults(), - .peer_scanner = service::PeerScanner::Config::defaults(), + .peer_scanner = service::PeerScanningService::Config::defaults(), .auto_connect_service = service::AutoConnectService::Config::defaults(), }; } diff --git a/DJC-Firmware/src/djc/service/PeerScanner.hpp b/DJC-Firmware/src/djc/service/PeerScanningService.hpp similarity index 94% rename from DJC-Firmware/src/djc/service/PeerScanner.hpp rename to DJC-Firmware/src/djc/service/PeerScanningService.hpp index 763b2fb..b017c0f 100644 --- a/DJC-Firmware/src/djc/service/PeerScanner.hpp +++ b/DJC-Firmware/src/djc/service/PeerScanningService.hpp @@ -20,7 +20,7 @@ namespace djc::service { namespace internal { -/// @brief Configuration parameters for the PeerScanner service. +/// @brief Configuration parameters for the PeerScanningService service. struct PeerScannerConfig final : kf::mixin::NonCopyable { kf::math::Milliseconds entry_max_life_time, ///< How long an entry stays in the list without being refreshed. @@ -41,10 +41,10 @@ struct PeerScannerConfig final : kf::mixin::NonCopyable { /// The scanner subscribes to `TransportLink::onReceiveForeign` and keeps up to `max_entries` entries /// Entries are refreshed every time a packet from the corresponding peer is received. /// Periodically, expired entries are removed and the list is compacted so that the first `peers().size()` elements are always valid. -struct PeerScanner final : +struct PeerScanningService final : - Service, - kf::mixin::Initable, + Service, + kf::mixin::Initable, kf::mixin::Configurable { @@ -59,7 +59,7 @@ struct PeerScanner final : /// @brief Maximum number of peers the scanner can remember simultaneously static constexpr auto max_entries{8}; - explicit constexpr PeerScanner(const Config &config, transport::TransportLink &transport_link) noexcept : + explicit constexpr PeerScanningService(const Config &config, transport::TransportLink &transport_link) noexcept : Configurable{config}, _transport_link{transport_link} {} /// @brief Returns a slice of the currently active peer entries. @@ -78,7 +78,7 @@ struct PeerScanner final : kf::usize _active_count{0}; // impl - using This = PeerScanner; + using This = PeerScanningService; KF_IMPL_INITABLE(This, void); void initImpl() noexcept { diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 9beb945..2762813 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -10,7 +10,7 @@ #include #include "djc/PeerFavoritesRegistry.hpp" -#include "djc/service/PeerScanner.hpp" +#include "djc/service/PeerScanningService.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" @@ -24,7 +24,7 @@ struct PeerExplorerPage : UI::Page { explicit PeerExplorerPage( UI::Page &root, transport::TransportLink &transport_link, - service::PeerScanner &peer_scanner, + service::PeerScanningService &peer_scanner, PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{"Peer Explorer"}, _transport_link{transport_link}, @@ -84,7 +84,7 @@ struct PeerExplorerPage : UI::Page { static constexpr auto peer_display_start_index{3u}; transport::TransportLink &_transport_link; - service::PeerScanner &_peer_scanner; + service::PeerScanningService &_peer_scanner; PeerFavoritesRegistry &_peer_favorites_registry; kf::math::Timer _redraw_timer{static_cast(500)}; @@ -92,9 +92,9 @@ struct PeerExplorerPage : UI::Page { UI::Button _primary_connection_status_button{{}}; UI::Display _available_label{_available_label_buffer.view()}; - kf::memory::Array _peer_displays{}; + kf::memory::Array _peer_displays{}; - kf::memory::Array _layout; + kf::memory::Array _layout; // child pages PeerDetailPage _peer_detail_page{*this, _transport_link, _peer_favorites_registry}; @@ -103,7 +103,7 @@ struct PeerExplorerPage : UI::Page { return kf::memory::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); } - kf::Option createPeerDisplayState(const kf::Option &entry, kf::math::Milliseconds now) const noexcept { + kf::Option createPeerDisplayState(const kf::Option &entry, kf::math::Milliseconds now) const noexcept { using P = widgets::PeerDisplay; constexpr auto extreme_age_factor{0.75f}; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 65dcc45..26acb23 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -29,7 +29,7 @@ #include "djc/service/Control.hpp" #include "djc/service/DisplayManager.hpp" #include "djc/service/InputHandler.hpp" -#include "djc/service/PeerScanner.hpp" +#include "djc/service/PeerScanningService.hpp" // djc::ui::pages #include "djc/ui/pages/ConfigPage.hpp" @@ -77,7 +77,7 @@ static djc::service::InputHandler input_handler{ periphery.right_button_listener, }; -static djc::service::PeerScanner peer_scanner{ +static djc::service::PeerScanningService peer_scanner{ config_manager.config().peer_scanner, transport_link, }; From e6c876a28f5794a4019b85d3d89189c807a4c46b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 15:52:39 +0300 Subject: [PATCH 135/326] refactor: remove 'Control' service dependency from DisplayManager --- DJC-Firmware/src/djc/service/DisplayManager.hpp | 13 +++++++------ DJC-Firmware/src/main.cpp | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 1f798d9..ff7f201 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -10,7 +10,6 @@ #include #include -#include "djc/service/Control.hpp" // fixme: service deps on service #include "djc/input/VirtualKeyboard.hpp" #include "djc/prelude.hpp" #include "djc/transport/TransportLink.hpp" @@ -28,8 +27,10 @@ struct DisplayManager final : { - explicit DisplayManager(DisplayDriver &display, const Control &control, const transport::TransportLink &transport_link) noexcept : - _display{display}, _control{control}, _transport_link{transport_link} {} + explicit DisplayManager(DisplayDriver &display, const transport::TransportLink &transport_link) noexcept : + _display{display}, _transport_link{transport_link} {} + + void showConnectionStatusOverlay(bool show) noexcept { _show_connection_status_overlay = show; } private: using P = kf::gfx::Palette; @@ -37,8 +38,9 @@ struct DisplayManager final : inline static const auto &virtual_keyboard = input::VirtualKeyboard::instance(); DisplayDriver &_display; - const Control &_control; const transport::TransportLink &_transport_link; + bool _show_connection_status_overlay{false}; + kf::gfx::Canvas _canvas{}; void onRender(kf::memory::StringView str) noexcept { @@ -55,8 +57,7 @@ struct DisplayManager final : } void renderUi(kf::memory::StringView str) noexcept { - // Control mode overlay - if (_control.enabled()) { + if (_show_connection_status_overlay) { const auto y = static_cast(_canvas.maxY() - _canvas.glyphHeight()); const auto overlay = _transport_link.connected() ? _transport_link.activePeerAddress().value().toString().data() : "Disconnected"; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 26acb23..5297efa 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -94,7 +94,6 @@ static djc::service::Control control{ static djc::service::DisplayManager display_manager{ periphery.display, - control, transport_link, }; @@ -184,6 +183,7 @@ void setup() { virtual_keyboard.quit(); } else { control.enabled(not control.enabled()); + display_manager.showConnectionStatusOverlay(control.enabled()); } ui.addEvent(E::update()); From 9037bb0b9007d1d3df1c2c072ee91e465694fd05 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 15:55:51 +0300 Subject: [PATCH 136/326] refactor: Display manager: (rename P -> Palette) --- .../src/djc/service/DisplayManager.hpp | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index ff7f201..cac4c7e 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -33,7 +33,7 @@ struct DisplayManager final : void showConnectionStatusOverlay(bool show) noexcept { _show_connection_status_overlay = show; } private: - using P = kf::gfx::Palette; + using Palette = kf::gfx::Palette; inline static const auto &virtual_keyboard = input::VirtualKeyboard::instance(); @@ -44,8 +44,8 @@ struct DisplayManager final : kf::gfx::Canvas _canvas{}; void onRender(kf::memory::StringView str) noexcept { - _canvas.background(P::black); - _canvas.foreground(P::white); + _canvas.background(Palette::black); + _canvas.foreground(Palette::white); _canvas.fill(); @@ -61,13 +61,13 @@ struct DisplayManager final : const auto y = static_cast(_canvas.maxY() - _canvas.glyphHeight()); const auto overlay = _transport_link.connected() ? _transport_link.activePeerAddress().value().toString().data() : "Disconnected"; - _canvas.background(P::bright_blue); - _canvas.foreground(P::black); + _canvas.background(Palette::bright_blue); + _canvas.foreground(Palette::black); _canvas.text(0, y, overlay); } - _canvas.background(P::black); - _canvas.foreground(P::white); + _canvas.background(Palette::black); + _canvas.foreground(Palette::white); _canvas.text(0, 0, str.data()); } @@ -83,8 +83,8 @@ struct DisplayManager final : _canvas.text(0, 0, kf::memory::ArrayString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", virtual_keyboard.available(), virtual_keyboard.text().size()).data()); _canvas.text(0, _canvas.glyphHeight(), virtual_keyboard.text().data()); - _canvas.background(P::bright_black); - _canvas.foreground(P::bright_black); + _canvas.background(Palette::bright_black); + _canvas.foreground(Palette::bright_black); _canvas.rect(0, keyboard_offset_y, _canvas.maxX(), _canvas.maxY(), true); for (auto row = 0; row < virtual_keyboard.rowsTotal(); row += 1) { @@ -97,14 +97,14 @@ struct DisplayManager final : const auto x = col * key_width + x_offset; if (row == virtual_keyboard.cursorRow() and col == virtual_keyboard.cursorCol()) { - _canvas.foreground(P::blue); + _canvas.foreground(Palette::blue); _canvas.rect(x, y, x + key_width, y + key_height - 1, true); - _canvas.background(P::blue); - _canvas.foreground(P::bright_white); + _canvas.background(Palette::blue); + _canvas.foreground(Palette::bright_white); } else { - _canvas.background(P::bright_black); - _canvas.foreground(P::black); + _canvas.background(Palette::bright_black); + _canvas.foreground(Palette::black); } const auto &key = input::VirtualKeyboard::keyAt(row, col); From 3ed2d9343b70f36bbda17f27d4df29f3640f3744 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 16:05:54 +0300 Subject: [PATCH 137/326] Update DisplayManager: make it generic by DisplayDriverImpl --- .../src/djc/service/DisplayManager.hpp | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index cac4c7e..1285814 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -11,7 +12,6 @@ #include #include "djc/input/VirtualKeyboard.hpp" -#include "djc/prelude.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" @@ -20,28 +20,31 @@ namespace djc::service { /// @brief Service that manages display rendering, including UI and virtual keyboard overlay. -struct DisplayManager final : +template struct DisplayManager final : - Service, - kf::mixin::Initable + Service>, + kf::mixin::Initable, void> { + KF_CHECK_IMPL(I, ::kf::drivers::display::DisplayDriverTag); - explicit DisplayManager(DisplayDriver &display, const transport::TransportLink &transport_link) noexcept : - _display{display}, _transport_link{transport_link} {} + using DisplayDriverImpl = I; + + explicit DisplayManager(DisplayDriverImpl &display_driver, const transport::TransportLink &transport_link) noexcept : + _display_driver{display_driver}, _transport_link{transport_link} {} void showConnectionStatusOverlay(bool show) noexcept { _show_connection_status_overlay = show; } private: - using Palette = kf::gfx::Palette; + using Palette = kf::gfx::Palette; inline static const auto &virtual_keyboard = input::VirtualKeyboard::instance(); - DisplayDriver &_display; + DisplayDriverImpl &_display_driver; const transport::TransportLink &_transport_link; bool _show_connection_status_overlay{false}; - kf::gfx::Canvas _canvas{}; + kf::gfx::Canvas _canvas{}; void onRender(kf::memory::StringView str) noexcept { _canvas.background(Palette::black); @@ -120,12 +123,12 @@ struct DisplayManager final : } // impl - using This = DisplayManager; + using This = DisplayManager; KF_IMPL_INITABLE(This, void); void initImpl() noexcept { - _canvas = kf::gfx::Canvas{ - kf::image::DynamicImage{_display.image()}, + _canvas = kf::gfx::Canvas{ + kf::image::DynamicImage{_display_driver.image()}, kf::gfx::fonts::gyver_5x7_en, }; _canvas.autoNextLine(true); @@ -133,7 +136,7 @@ struct DisplayManager final : auto &config = ui::UI::instance().renderConfig(); config.callback([this](kf::memory::StringView str) { onRender(str); - (void) _display.send(); + (void) _display_driver.send(); }); config.row_max_length = _canvas.widthInGlyphs(); config.rows_total = _canvas.heightInGlyphs() - 1; From 05d90f059dd322c8ac787ac571ee855d7580625d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 16:19:26 +0300 Subject: [PATCH 138/326] update main --- DJC-Firmware/src/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 5297efa..349a80f 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -15,6 +15,7 @@ #include "djc/PeerFavoritesRegistry.hpp" #include "djc/Periphery.hpp" #include "djc/input/VirtualKeyboard.hpp" +#include "djc/prelude.hpp" // djc::transport #include "djc/transport/TransportLink.hpp" @@ -92,7 +93,7 @@ static djc::service::Control control{ protocol_link, }; -static djc::service::DisplayManager display_manager{ +static djc::service::DisplayManager display_manager{ periphery.display, transport_link, }; From d12b23f3141d4ba05b64f792cd68b676f45d3eac Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 15 May 2026 16:20:06 +0300 Subject: [PATCH 139/326] Update LogicalButtonListener: fix formatting & naming issues --- .../src/djc/input/LogicalLevelListener.hpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp b/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp index 563c2b8..bbedeb4 100644 --- a/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp +++ b/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp @@ -17,22 +17,25 @@ namespace djc::input { namespace internal { -struct ButtonConfig final : kf::mixin::NonCopyable { +struct LogicalLevelListenerConfig final : kf::mixin::NonCopyable { kf::math::Milliseconds debounce; }; }// namespace internal /// @brief Minimal button with press detection only -template -struct LogicalLevelListener : kf::mixin::Initable, void>, - kf::mixin::NonCopyable, - kf::mixin::TimedPollable>, - kf::mixin::Configurable { +template struct LogicalLevelListener : + + kf::mixin::NonCopyable, + kf::mixin::TimedPollable>, + kf::mixin::Initable, void>, + kf::mixin::Configurable + +{ KF_CHECK_IMPL(I, kf::gpio::DigitalInputTag); using PinImpl = I; - using Config = internal::ButtonConfig; + using Config = internal::LogicalLevelListenerConfig; explicit LogicalLevelListener(const Config &config, PinImpl &&pin) noexcept : kf::mixin::Configurable{config}, _pin{std::move(pin)} {} @@ -77,8 +80,6 @@ struct LogicalLevelListener : kf::mixin::Initable, void> _last_stable = state; } - // todo use Timer here - if (state != _last_raw) { _last_raw = state; _next = now + this->config().debounce; From 7e0af8566d5b39146859d609ec77abddadafa58a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 18:45:51 +0300 Subject: [PATCH 140/326] wip(main-autoconnect): extract procedure --- DJC-Firmware/src/main.cpp | 64 +++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 349a80f..016dcec 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -6,6 +6,7 @@ // lib #include +#include #include // djc @@ -230,6 +231,26 @@ void setup() { if (config_manager.modified()) { config_manager.save(); } } +kf::Option getTargetPeer() noexcept { + const auto favorites = peer_favoriter_registry.all(); + const auto peers = peer_scanner.peers(); + + if (favorites.size() * peers.size() == 0) { return {}; } + + // peers.all() -> filter: favorites -> max: (.trust) -> .address + + auto most_trusted_peer_index{0u}; + + for (auto index = 1u; index < peers.size(); index += 1) { + if (not peers[index].hasValue()) { continue; } + + const auto &trusted = peer_favoriter_registry.get(peers[index].value().address); + if (not trusted.hasValue()) { continue; } + } + + return {}; +} + void loop() { constexpr kf::math::Milliseconds loop_period{1000 / 50};// 50 Hz delay(loop_period); @@ -240,26 +261,29 @@ void loop() { peer_scanner.poll(now); if (auto_connect_service.config().enabled and not auto_connect_service.target().hasValue()) { - const auto favorites = peer_favoriter_registry.all(); - - if (favorites.size() > 0) { - auto most_trusted_favorite_index = 0u; - - for (auto index = 1u; index < favorites.size(); index += 1) { - if (favorites[index].hasValue() and favorites[index].value().trust > favorites[most_trusted_favorite_index].value().trust) { - most_trusted_favorite_index = index; - } - } - - if (const auto &most_trusted = favorites[most_trusted_favorite_index]; most_trusted.hasValue()) { - for (const auto &peer: peer_scanner.peers()) { - if (peer.hasValue() and peer.value().address == most_trusted.value().address) { - auto_connect_service.target(most_trusted.value().address); - break; - } - } - } - } + // const auto favorites = peer_favoriter_registry.all(); + + // if (favorites.size() > 0) { + // auto most_trusted_favorite_index = 0u; + + // for (auto index = 1u; index < favorites.size(); index += 1) { + // if (favorites[index].hasValue() and favorites[index].value().trust > favorites[most_trusted_favorite_index].value().trust) { + // most_trusted_favorite_index = index; + // } + // } + + // if (const auto &most_trusted = favorites[most_trusted_favorite_index]; most_trusted.hasValue()) { + // for (const auto &peer: peer_scanner.peers()) {c` ` + // if (peer.hasValue() and peer.value().address == most_trusted.value().address) { + // auto_connect_service.target(most_trusted.value().address); + // break; + // } + // } + // } + // } + // if (const auto target_peer = getTargetPeer(); target_peer.hasValue()) { + // auto_connect_service.target(target_peer.value()); + // } } auto_connect_service.poll(now); From 68437643deb8d8a90480f98f35a90f501f2a4e84 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 18:48:45 +0300 Subject: [PATCH 141/326] chore: remove kiraflux-toolkit version constraint Using upstream now --- DJC-Firmware/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/platformio.ini b/DJC-Firmware/platformio.ini index 84079cc..c3e667d 100644 --- a/DJC-Firmware/platformio.ini +++ b/DJC-Firmware/platformio.ini @@ -7,7 +7,7 @@ upload_speed = 921600 monitor_filters = esp32_exception_decoder monitor_echo = true lib_deps = - https://github.com/KiraFlux/KiraFlux-Toolkit.git#v0.3.1 + https://github.com/KiraFlux/KiraFlux-Toolkit.git okalachev/MAVLink@^2.0.22 build_flags = From b9f28bea5e73b18aa9d57e7697bf94549daa1824 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 18:57:53 +0300 Subject: [PATCH 142/326] wip: partial migration on kf-0.3.2 --- DJC-Firmware/src/djc/Config.hpp | 2 +- DJC-Firmware/src/djc/ManualInput.hpp | 2 +- .../src/djc/PeerFavoritesRegistry.hpp | 10 ++++----- DJC-Firmware/src/djc/Periphery.hpp | 22 +++++++++---------- .../src/djc/input/LogicalLevelListener.hpp | 2 +- .../src/djc/input/VirtualKeyboard.hpp | 10 ++++----- DJC-Firmware/src/djc/prelude.hpp | 10 ++++----- .../src/djc/protocol/MavlinkProtocol.hpp | 6 ++--- DJC-Firmware/src/djc/protocol/Protocol.hpp | 6 ++--- .../src/djc/protocol/ProtocolLink.hpp | 6 ++--- DJC-Firmware/src/djc/protocol/RawProtocol.hpp | 8 +++---- .../src/djc/service/DisplayManager.hpp | 4 ++-- .../src/djc/service/PeerScanningService.hpp | 8 +++---- .../src/djc/transport/EspNowTransport.hpp | 12 +++++----- .../src/djc/transport/PeerAddress.hpp | 4 ++-- DJC-Firmware/src/djc/transport/Transport.hpp | 12 +++++----- .../src/djc/transport/TransportLink.hpp | 4 ++-- DJC-Firmware/src/djc/ui/UI.hpp | 4 +++- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 8 +++---- .../src/djc/ui/pages/MavlinkTelemetryPage.hpp | 4 ++-- .../src/djc/ui/pages/PeerDetailPage.hpp | 2 +- .../src/djc/ui/pages/PeerExplorerPage.hpp | 10 ++++----- .../src/djc/ui/pages/PeerFavoritePage.hpp | 6 ++--- .../src/djc/ui/pages/RawProtocolPage.hpp | 8 +++---- .../src/djc/ui/widgets/PeerDisplay.hpp | 4 ++-- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 8 +++---- DJC-Firmware/src/main.cpp | 4 ++-- 27 files changed, 94 insertions(+), 92 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 3f7ec0e..92008e8 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -5,7 +5,7 @@ // lib #include -#include +#include #include #include "djc/PeerFavoritesRegistry.hpp" diff --git a/DJC-Firmware/src/djc/ManualInput.hpp b/DJC-Firmware/src/djc/ManualInput.hpp index af5e157..cb3e56c 100644 --- a/DJC-Firmware/src/djc/ManualInput.hpp +++ b/DJC-Firmware/src/djc/ManualInput.hpp @@ -3,7 +3,7 @@ #pragma once -#include +#include namespace djc { diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index 02a0cf4..bc0c496 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -7,9 +7,9 @@ #include #include -#include +#include #include -#include +#include #include #include @@ -45,10 +45,10 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } }; - explicit constexpr PeerFavoritesRegistry(kf::memory::Slice> entries) noexcept : _entries{entries} {} + explicit constexpr PeerFavoritesRegistry(kf::Slice> entries) noexcept : _entries{entries} {} /// @brief Return the entire slot array, including empty slots. - [[nodiscard]] kf::memory::Slice> all() const noexcept { + [[nodiscard]] kf::Slice> all() const noexcept { return {_entries.data(), _active_count}; } @@ -106,7 +106,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } private: - kf::memory::Slice> _entries; + kf::Slice> _entries; kf::usize _active_count{0}; /// @brief Find the index of an entry by address. diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index 0fd4e4c..164e935 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include #include @@ -82,32 +82,32 @@ struct Periphery final : kf::mixin::NonCopyable, kf::mixin::Initableconfig().button, - DigitalInput{ + GPIO::DigitalInput{ GPIO_NUM_14, - DigitalInput::Pull::InternalUp, + GPIO::DigitalInput::Pull::InternalUp, }, }; Joystick left_joystick{ this->config().left_joystick, this->config().axis_filter, - AdcInput{GPIO_NUM_32}, - AdcInput{GPIO_NUM_33}, + GPIO::AdcInput{GPIO_NUM_32}, + GPIO::AdcInput{GPIO_NUM_33}, }; ButtonListener right_button_listener{ this->config().button, - DigitalInput{ + GPIO::DigitalInput{ GPIO_NUM_4, - DigitalInput::Pull::InternalUp, + GPIO::DigitalInput::Pull::InternalUp, }, }; Joystick right_joystick{ this->config().right_joystick, this->config().axis_filter, - AdcInput{GPIO_NUM_34}, - AdcInput{GPIO_NUM_35}, + GPIO::AdcInput{GPIO_NUM_34}, + GPIO::AdcInput{GPIO_NUM_35}, }; Bus bus{ @@ -118,8 +118,8 @@ struct Periphery final : kf::mixin::NonCopyable, kf::mixin::Initableconfig().display, bus.createNode(this->config().bus_node), - DigitalOutput{GPIO_NUM_22},// DC - DigitalOutput{GPIO_NUM_17},// RESET + GPIO::DigitalOutput{GPIO_NUM_22},// DC + GPIO::DigitalOutput{GPIO_NUM_17},// RESET }; // Analog axis calibration diff --git a/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp b/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp index bbedeb4..dd62f73 100644 --- a/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp +++ b/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp @@ -5,7 +5,7 @@ #include -#include +#include #include #include #include diff --git a/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp b/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp index 546fbe6..41a8c88 100644 --- a/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp +++ b/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp @@ -4,9 +4,9 @@ #pragma once #include -#include +#include #include -#include +#include #include #include #include @@ -116,7 +116,7 @@ struct VirtualKeyboard final : kf::mixin::Singleton { {Key::Kind::Space, ' '}, }}; - static constexpr kf::memory::Array, 5> rows{{ + static constexpr kf::memory::Array, 5> rows{{ {row_0.data(), row_0.size()}, {row_1.data(), row_1.size()}, {row_2.data(), row_2.size()}, @@ -150,7 +150,7 @@ struct VirtualKeyboard final : kf::mixin::Singleton { } } - void begin(kf::memory::Slice text_source) noexcept { + void begin(kf::Slice text_source) noexcept { _active = true; _text_source = text_source; @@ -201,7 +201,7 @@ struct VirtualKeyboard final : kf::mixin::Singleton { } private: - kf::memory::Slice _text_source{}; + kf::Slice _text_source{}; kf::isize _text_cursor{}; kf::i8 _cursor_row{0}, _cursor_row_index{0}; bool _active{false}; diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp index 5995453..5020366 100644 --- a/DJC-Firmware/src/djc/prelude.hpp +++ b/DJC-Firmware/src/djc/prelude.hpp @@ -7,20 +7,20 @@ #include #include #include -#include +#include #include "djc/input/LogicalLevelListener.hpp" namespace djc { -using namespace kf::gpio::arduino; +using GPIO = kf::gpio::ArduinoGPIO; -using ButtonListener = djc::input::LogicalLevelListener; +using ButtonListener = djc::input::LogicalLevelListener; -using AxisInput = kf::drivers::sensors::NormalizedAdcInput; +using AxisInput = kf::drivers::sensors::NormalizedAdcInput; using Joystick = kf::drivers::sensors::Joystick; using Bus = kf::bus::spi::ArduinoSPI; -using DisplayDriver = kf::drivers::display::ST7735; +using DisplayDriver = kf::drivers::display::ST7735; }// namespace djc \ No newline at end of file diff --git a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp index 34ff6b6..cc2acc3 100644 --- a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp @@ -5,10 +5,10 @@ #include -#include +#include #include #include -#include +#include #include #include @@ -86,7 +86,7 @@ struct MavlinkProtocol : (void) sendManualControl(transport_link, input); } - void receive(kf::memory::Slice buffer) noexcept override { + void receive(kf::Slice buffer) noexcept override { mavlink_message_t message; mavlink_status_t status; diff --git a/DJC-Firmware/src/djc/protocol/Protocol.hpp b/DJC-Firmware/src/djc/protocol/Protocol.hpp index 476ab3b..f3b6843 100644 --- a/DJC-Firmware/src/djc/protocol/Protocol.hpp +++ b/DJC-Firmware/src/djc/protocol/Protocol.hpp @@ -3,9 +3,9 @@ #pragma once -#include +#include #include -#include +#include #include #include "djc/ManualInput.hpp" @@ -26,7 +26,7 @@ struct Protocol : kf::mixin::NonCopyable { /// @note /// The implementation must parse the data according to its protocol and notify the appropriate callback /// This method is called from the transport callback; it must be fast and never block. - virtual void receive(kf::memory::Slice buffer) noexcept = 0; + virtual void receive(kf::Slice buffer) noexcept = 0; }; }// namespace djc::protocol \ No newline at end of file diff --git a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp index b72dbbd..c8d1b76 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp @@ -4,10 +4,10 @@ #pragma once #include -#include +#include #include #include -#include +#include #include #include @@ -75,7 +75,7 @@ struct ProtocolLink : /// @brief Forward a received data buffer to the active protocol. /// @param buffer Raw data received from the transport. - void receive(kf::memory::Slice buffer) noexcept { + void receive(kf::Slice buffer) noexcept { if (nullptr == _protocol) { logger.error("receive: no protocol set"); return; diff --git a/DJC-Firmware/src/djc/protocol/RawProtocol.hpp b/DJC-Firmware/src/djc/protocol/RawProtocol.hpp index ea53841..909a0ba 100644 --- a/DJC-Firmware/src/djc/protocol/RawProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/RawProtocol.hpp @@ -3,9 +3,9 @@ #pragma once -#include +#include #include -#include +#include #include #include "djc/ManualInput.hpp" @@ -18,13 +18,13 @@ namespace djc::protocol { /// @note /// The protocol simply sends the `ManualInput` struct verbatim. /// Incoming data is forwarded directly to the callback as is`. -struct RawProtocol : Protocol, kf::mixin::Callbacked> { +struct RawProtocol : Protocol, kf::mixin::Callbacked> { void poll(kf::math::Milliseconds, const ManualInput &input, transport::TransportLink &transport_link) noexcept override { (void) transport_link.send({reinterpret_cast(&input), sizeof(ManualInput)}); } - void receive(kf::memory::Slice buffer) noexcept override { + void receive(kf::Slice buffer) noexcept override { this->invoke(buffer); } }; diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 1285814..0504ac9 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include "djc/input/VirtualKeyboard.hpp" @@ -83,7 +83,7 @@ template struct DisplayManager final : char c[2]{0, 0}; - _canvas.text(0, 0, kf::memory::ArrayString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", virtual_keyboard.available(), virtual_keyboard.text().size()).data()); + _canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", virtual_keyboard.available(), virtual_keyboard.text().size()).data()); _canvas.text(0, _canvas.glyphHeight(), virtual_keyboard.text().data()); _canvas.background(Palette::bright_black); diff --git a/DJC-Firmware/src/djc/service/PeerScanningService.hpp b/DJC-Firmware/src/djc/service/PeerScanningService.hpp index b017c0f..38df2ed 100644 --- a/DJC-Firmware/src/djc/service/PeerScanningService.hpp +++ b/DJC-Firmware/src/djc/service/PeerScanningService.hpp @@ -4,11 +4,11 @@ #pragma once #include -#include +#include #include #include #include -#include +#include #include #include @@ -66,7 +66,7 @@ struct PeerScanningService final : /// @return A contiguous view of the first `_active_count` elements of the internal array. /// @note The slice is valid only until the next call to `poll()`. /// The entries are sorted in order of registration (oldest first). - [[nodiscard]] kf::memory::Slice> peers() const noexcept { + [[nodiscard]] kf::Slice> peers() const noexcept { return {_entries.data(), _active_count}; } @@ -84,7 +84,7 @@ struct PeerScanningService final : void initImpl() noexcept { _update_poll_timer.start(0);// enable timer - _transport_link.onReceiveForeign([this](const transport::PeerAddress &address, kf::memory::Slice buffer) -> void { + _transport_link.onReceiveForeign([this](const transport::PeerAddress &address, kf::Slice buffer) -> void { // search for mathing entry for (auto &entry: _entries) { if (entry.hasValue() and entry.value().address == address) { diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 1233cf6..b932e53 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -5,8 +5,8 @@ #include #include -#include -#include +#include +#include #include #include @@ -19,7 +19,7 @@ namespace djc::transport { /// @note Manages ESP‑NOW peer connections. Uses a broadcast peer for discovery and a dedicated active peer for communication. struct EspNowTransport : Transport, kf::mixin::Initable { - [[nodiscard]] bool send(kf::memory::Slice buffer) noexcept override { + [[nodiscard]] bool send(kf::Slice buffer) noexcept override { if (_active_peer.hasValue()) { return _active_peer.value().writeBuffer(buffer).isOk(); } else { @@ -29,7 +29,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { private: using EspNow = kf::network::EspNow; - using LogString = kf::memory::ArrayString<128>; + using LogString = kf::memory::StaticString<128>; static constexpr auto logger{kf::Logger::create("EspNowTransport")}; @@ -47,7 +47,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { _active_peer = addPeer(address.mac()); if (not _active_peer.hasValue()) { return false; } - const auto receive_setup_result = _active_peer.value().onReceive([this](kf::memory::Slice buffer) { + const auto receive_setup_result = _active_peer.value().onReceive([this](kf::Slice buffer) { invokeReceive(buffer); }); @@ -133,7 +133,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { return false; } - espnow.onReceiveFromUnknown([this](const EspNow::Mac &mac, kf::memory::Slice buffer){ + espnow.onReceiveFromUnknown([this](const EspNow::Mac &mac, kf::Slice buffer){ invokeReceiveForeign(PeerAddress::fromEspnowMac(mac), buffer); }); diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp index 13c8886..da92fb2 100644 --- a/DJC-Firmware/src/djc/transport/PeerAddress.hpp +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -3,7 +3,7 @@ #pragma once -#include +#include #include #include "djc/transport/Kind.hpp" @@ -16,7 +16,7 @@ namespace djc::transport { /// The active kind is stored in a tag field; the union contains the actual address. struct PeerAddress { - using ReprString = kf::memory::ArrayString<32>; + using ReprString = kf::memory::StaticString<32>; /// @brief create an ESP‑NOW peer address from a MAC. /// @param mac 6‑byte MAC address (EspNow::Mac). diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index 8533af1..b501101 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -5,8 +5,8 @@ #include #include -#include -#include +#include +#include #include #include "djc/transport/PeerAddress.hpp" @@ -22,13 +22,13 @@ namespace djc::transport { struct Transport : kf::mixin::NonCopyable { /// @brief Callback invoked when data is received from a peer. - using ReceiveCallback = kf::Function)>; + using ReceiveCallback = kf::Function)>; /// @brief Send raw data to the currently connected peer. /// @param buffer Raw payload. /// @return true if the data was sent successfully, false otherwise. /// @note Must only be called when connected. - [[nodiscard]] virtual bool send(kf::memory::Slice buffer) noexcept = 0; + [[nodiscard]] virtual bool send(kf::Slice buffer) noexcept = 0; protected: /// @brief Hardware‑specific connection procedure. @@ -84,13 +84,13 @@ struct Transport : kf::mixin::NonCopyable { } protected: - void invokeReceive(kf::memory::Slice buffer) noexcept { + void invokeReceive(kf::Slice buffer) noexcept { if (_active_peer_address.hasValue() and _receive_callback) { _receive_callback(_active_peer_address.value(), buffer); } } - void invokeReceiveForeign(const PeerAddress &address, kf::memory::Slice buffer) noexcept { + void invokeReceiveForeign(const PeerAddress &address, kf::Slice buffer) noexcept { if (_broadcast_receive_callback) { _broadcast_receive_callback(address, buffer); } diff --git a/DJC-Firmware/src/djc/transport/TransportLink.hpp b/DJC-Firmware/src/djc/transport/TransportLink.hpp index 02ab4c3..d43f02b 100644 --- a/DJC-Firmware/src/djc/transport/TransportLink.hpp +++ b/DJC-Firmware/src/djc/transport/TransportLink.hpp @@ -57,7 +57,7 @@ struct TransportLink final : /// @brief Forward a data buffer to the underlying transport. /// @return true if the transport reported success, false on error or if no transport is set. - [[nodiscard]] bool send(kf::memory::Slice buffer) noexcept { + [[nodiscard]] bool send(kf::Slice buffer) noexcept { if (nullptr == _transport) { logger.error("send failed: no transport set"); return false; @@ -77,7 +77,7 @@ struct TransportLink final : _receive_callback = std::move(callback); - _transport->onReceive([this](const PeerAddress &address, kf::memory::Slice buffer) { + _transport->onReceive([this](const PeerAddress &address, kf::Slice buffer) { if (_receive_callback) { _receive_callback(address, buffer); } _disconnect_timer_reset_required = true; }); diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index 1abc0fc..f505de2 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -5,14 +5,16 @@ #include #include +#include #include namespace djc::ui { // KiraFlux-Toolkit UI specialization for ESP32-DJC using UI = kf::ui::UI< + kf::ui::UiTraits< kf::ui::render::ColoredTextRender<256>,// Render Engine: Buffered Colored Text UI render engine kf::ui::Event<6> // Event: 6-bit Event value encoding - >; + >>; }// namespace djc \ No newline at end of file diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index a8afa6e..6b9bccc 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -4,7 +4,7 @@ #pragma once #include -#include +#include #include #include "djc/Config.hpp" @@ -117,7 +117,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { djc::ConfigManager &_config_manager; PeerFavoritesRegistry &_peer_favoriter_registry; - kf::memory::ArrayString<32> _label_favorites_buffer{}; + kf::memory::StaticString<32> _label_favorites_buffer{}; bool show_favorites{true}; // widgets @@ -166,8 +166,8 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { PeerFavoritePage _peer_favorite_page{*this, _peer_favoriter_registry}; - kf::memory::Slice layout(kf::usize displayed_peers) noexcept { - return kf::memory::Slice{_layout.data(), _layout.size()}.first(layout_regular_widgets + displayed_peers); + kf::Slice layout(kf::usize displayed_peers) noexcept { + return kf::Slice{_layout.data(), _layout.size()}.first(layout_regular_widgets + displayed_peers); } // impl diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 925edbc..2010e74 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include "djc/MavlinkTelemetryRegistry.hpp" @@ -97,7 +97,7 @@ struct MavlinkTelemetryPage : UI::Page { kf::math::Milliseconds _last_imu{}, _last_attitude{}, _last_serial_control{}; // widgets - kf::memory::ArrayString<64> _attitude_buffer{"..."}, _imu_buffer{"..."}; + kf::memory::StaticString<64> _attitude_buffer{"..."}, _imu_buffer{"..."}; UI::Display _attitude_display{_attitude_buffer.view()}, _imu_display{_imu_buffer.view()}; diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index 51bc353..2ad515e 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -5,7 +5,7 @@ #include #include -#include +#include #include "djc/PeerFavoritesRegistry.hpp" #include "djc/transport/TransportLink.hpp" diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 2762813..82885bb 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -6,8 +6,8 @@ #include #include #include -#include -#include +#include +#include #include "djc/PeerFavoritesRegistry.hpp" #include "djc/service/PeerScanningService.hpp" @@ -88,7 +88,7 @@ struct PeerExplorerPage : UI::Page { PeerFavoritesRegistry &_peer_favorites_registry; kf::math::Timer _redraw_timer{static_cast(500)}; - kf::memory::ArrayString<64> _available_label_buffer{}, _connection_button_buffer{}; + kf::memory::StaticString<64> _available_label_buffer{}, _connection_button_buffer{}; UI::Button _primary_connection_status_button{{}}; UI::Display _available_label{_available_label_buffer.view()}; @@ -99,8 +99,8 @@ struct PeerExplorerPage : UI::Page { // child pages PeerDetailPage _peer_detail_page{*this, _transport_link, _peer_favorites_registry}; - kf::memory::Slice layout(kf::usize displayed_peers) noexcept { - return kf::memory::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); + kf::Slice layout(kf::usize displayed_peers) noexcept { + return kf::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); } kf::Option createPeerDisplayState(const kf::Option &entry, kf::math::Milliseconds now) const noexcept { diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 2151b3a..f719014 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -4,7 +4,7 @@ #pragma once #include -#include +#include #include #include "djc/PeerFavoritesRegistry.hpp" @@ -61,14 +61,14 @@ struct PeerFavoritePage final : UI::Page { _trust_input.value(_temp_entry.value().trust); _confirm_button.label("Confirm"); - widgets(kf::memory::Slice{_layout.data(), _layout.size()}.first(_layout.size() - (entry_option.hasValue() ? 0 : 1))); + widgets(kf::Slice{_layout.data(), _layout.size()}.first(_layout.size() - (entry_option.hasValue() ? 0 : 1))); } private: PeerFavoritesRegistry &_peer_favorites_registry; kf::Option _temp_entry{}; - kf::memory::ArrayString<64> _label_buffer{}; + kf::memory::StaticString<64> _label_buffer{}; using TrustInput = UI::Slider; diff --git a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index 68c875b..da166b2 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -4,8 +4,8 @@ #pragma once #include -#include -#include +#include +#include #include "djc/protocol/ProtocolLink.hpp" #include "djc/protocol/ProtocolRegistry.hpp" @@ -43,9 +43,9 @@ struct RawProtocolPage : UI::Page { void onEntry() noexcept override { _protocol_link.protocol(_protocol_registry.raw()); - _protocol_registry.raw().callback([](kf::memory::Slice buffer) { + _protocol_registry.raw().callback([](kf::Slice buffer) { logger.info( - kf::memory::ArrayString<64>::formatted( + kf::memory::StaticString<64>::formatted( "Got %d bytes from primary peer", buffer.size()) .view()); diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index e19166e..c971ff6 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -4,7 +4,7 @@ #pragma once #include -#include +#include #include #include @@ -37,7 +37,7 @@ struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked::formatted( + kf::memory::StaticString<64>::formatted( "%c%s\x80", static_cast(_state.value().label_color), _state.value().displayName()) diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index 165b941..56100d7 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -4,7 +4,7 @@ #pragma once #include -#include +#include #include #include "djc/input/VirtualKeyboard.hpp" @@ -16,9 +16,9 @@ struct TextInput final : UI::Widget { constexpr TextInput() noexcept : _text_source{} {} - explicit constexpr TextInput(kf::memory::Slice source) noexcept : _text_source{source} {} + explicit constexpr TextInput(kf::Slice source) noexcept : _text_source{source} {} - void source(kf::memory::Slice new_source) noexcept { _text_source = new_source; } + void source(kf::Slice new_source) noexcept { _text_source = new_source; } bool available() const noexcept { return nullptr != _text_source.data(); } @@ -58,7 +58,7 @@ struct TextInput final : UI::Widget { private: inline static auto &virtual_keyboard{input::VirtualKeyboard::instance()}; - kf::memory::Slice _text_source; + kf::Slice _text_source; [[nodiscard]] kf::memory::StringView string() const noexcept { if (available()) { diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 349a80f..82ee30f 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -98,7 +98,7 @@ static djc::service::DisplayManager display_manager{ transport_link, }; -static auto &ui{djc::ui::UI::instance()}; +static djc::ui::UI ui{}; // pages @@ -159,7 +159,7 @@ void setup() { transport_link.transport(transport_registry.get(config_manager.config().init_transport_kind)); - transport_link.onReceive([](const djc::transport::PeerAddress &, kf::memory::Slice buffer) { + transport_link.onReceive([](const djc::transport::PeerAddress &, kf::Slice buffer) { protocol_link.receive(buffer); }); From a7deca6dbd9fdf3332e05931017098170e6b0074 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 19:00:43 +0300 Subject: [PATCH 143/326] wip(migration): replace internal LogicalLevelListener with toolkit implementation --- .../src/djc/input/LogicalLevelListener.hpp | 100 ------------------ DJC-Firmware/src/djc/prelude.hpp | 7 +- 2 files changed, 4 insertions(+), 103 deletions(-) delete mode 100644 DJC-Firmware/src/djc/input/LogicalLevelListener.hpp diff --git a/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp b/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp deleted file mode 100644 index dd62f73..0000000 --- a/DJC-Firmware/src/djc/input/LogicalLevelListener.hpp +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) 2026 KiraFlux -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace djc::input { - -namespace internal { - -struct LogicalLevelListenerConfig final : kf::mixin::NonCopyable { - kf::math::Milliseconds debounce; -}; - -}// namespace internal - -/// @brief Minimal button with press detection only -template struct LogicalLevelListener : - - kf::mixin::NonCopyable, - kf::mixin::TimedPollable>, - kf::mixin::Initable, void>, - kf::mixin::Configurable - -{ - KF_CHECK_IMPL(I, kf::gpio::DigitalInputTag); - - using PinImpl = I; - using Config = internal::LogicalLevelListenerConfig; - - explicit LogicalLevelListener(const Config &config, PinImpl &&pin) noexcept : - kf::mixin::Configurable{config}, _pin{std::move(pin)} {} - - /// @brief Check if button was clicked (consumes the click) - /// @return true if button was pressed since last call - [[nodiscard]] bool clicked() noexcept { - if (_click_ready) { - _click_ready = false; - return true; - } - return false; - } - - /// @brief Check current button state - /// @return true if button is currently pressed (after debounce) - [[nodiscard]] bool pressed() const noexcept { return _last_stable; } - -private: - kf::math::Milliseconds _next{0}; - PinImpl _pin; - bool _last_stable{false}; - bool _last_raw{false}; - bool _click_ready{false}; - bool _first{true}; - - // impl - using This = LogicalLevelListener; - - KF_IMPL_INITABLE(This, void); - void initImpl() noexcept { - _pin.init(); - } - - KF_IMPL_TIMED_POLLABLE(This); - void pollImpl(kf::math::Milliseconds now) noexcept { - const bool state = _pin.read(); - - if (_first) { - _first = false; - _last_raw = state; - _last_stable = state; - } - - if (state != _last_raw) { - _last_raw = state; - _next = now + this->config().debounce; - } - - if (now >= _next) { - if (_last_stable != state) { - _last_stable = state; - - if (_last_stable) { - _click_ready = true; - } - } - } - } -}; - -}// namespace djc::input \ No newline at end of file diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp index 5020366..df74b06 100644 --- a/DJC-Firmware/src/djc/prelude.hpp +++ b/DJC-Firmware/src/djc/prelude.hpp @@ -8,19 +8,20 @@ #include #include #include - -#include "djc/input/LogicalLevelListener.hpp" +#include namespace djc { using GPIO = kf::gpio::ArduinoGPIO; -using ButtonListener = djc::input::LogicalLevelListener; +using ButtonListener = kf::input::LogicalLevelListener; using AxisInput = kf::drivers::sensors::NormalizedAdcInput; + using Joystick = kf::drivers::sensors::Joystick; using Bus = kf::bus::spi::ArduinoSPI; + using DisplayDriver = kf::drivers::display::ST7735; }// namespace djc \ No newline at end of file From e78e2ed88986113693b60a4613520c2ef1c6c9b1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 19:03:40 +0300 Subject: [PATCH 144/326] fix: migrate virtual keyboard --- DJC-Firmware/src/djc/input/VirtualKeyboard.hpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp b/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp index 41a8c88..f1a291c 100644 --- a/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp +++ b/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp @@ -3,17 +3,15 @@ #pragma once +#include #include -#include #include -#include #include #include #include +#include -namespace djc::input { - -namespace internal { +namespace djc::internal { struct Key { @@ -37,7 +35,9 @@ struct Key { } }; -}// namespace internal +}// namespace djc::internal + +namespace djc::input { struct VirtualKeyboard final : kf::mixin::Singleton { @@ -154,7 +154,7 @@ struct VirtualKeyboard final : kf::mixin::Singleton { _active = true; _text_source = text_source; - _text_cursor = text().find('\0').value(); + _text_cursor = text().find('\0').unwrapOr(0); } void quit() noexcept { From 943ca518b54ec88f6a620d45616596cbaf231592 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 19:15:28 +0300 Subject: [PATCH 145/326] wip: migrate protocol-related structs --- DJC-Firmware/src/djc/Config.hpp | 2 +- .../src/djc/protocol/MavlinkProtocol.hpp | 30 ++++++++++--------- .../src/djc/protocol/ProtocolLink.hpp | 24 ++++++++------- .../src/djc/protocol/ProtocolRegistry.hpp | 16 +++++----- 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 92008e8..387e084 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -39,7 +39,7 @@ struct Config { protocol::ProtocolRegistry::Mode init_protocol_mode; transport::Kind init_transport_kind; kf::memory::Array device_name; - kf::memory::Array, max_peer_favorites> peer_favorites; + kf::memory::Array, max_peer_favorites> peer_favorites; // periphery Periphery::Config periphery; diff --git a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp index cc2acc3..4e65f1b 100644 --- a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp @@ -5,35 +5,35 @@ #include -#include +#include #include #include -#include #include #include +#include #include "djc/ManualInput.hpp" #include "djc/protocol/Protocol.hpp" #include "djc/transport/TransportLink.hpp" -namespace djc::protocol { - -namespace internal { +namespace djc::internal { -/// @brief Configuration for the MAVLink protocol. -struct MavlinkProtocolConfig { +/// @brief Configuration for the MAVLink protocol +struct MavlinkProtocolConfig final { using IdType = kf::u8; - kf::math::Milliseconds heartbeat_period;///< Period between HEARTBEAT messages (ms). - IdType system_id_self; ///< MAVLink system ID of this controller. - IdType system_id_target; ///< MAVLink system ID of the target drone (0 = broadcast). + kf::math::Timer::Config heartbeat_timer;///< Timer for HEARTBEAT messages (ms) + IdType system_id_self; ///< MAVLink system ID of this controller + IdType system_id_target; ///< MAVLink system ID of the target drone (0 = broadcast) IdType component_id_heartbeat; IdType component_id_manual_control; - static constexpr MavlinkProtocolConfig defaults() noexcept { + [[nodiscard]] static constexpr auto defaults() noexcept { return MavlinkProtocolConfig{ - .heartbeat_period = 2'000,// ms + .heartbeat_timer = { + .period = 2'000,// ms + }, .system_id_self = 0x7f, .system_id_target = 0x01, .component_id_heartbeat = MAV_COMP_ID_USER1, @@ -42,7 +42,9 @@ struct MavlinkProtocolConfig { } }; -}// namespace internal +}// namespace djc::internal + +namespace djc::protocol { /// @brief MAVLink protocol - sends MANUAL_CONTROL and HEARTBEAT packets, invokes callback on received MAVLink messages /// @note @@ -98,7 +100,7 @@ struct MavlinkProtocol : } private: - kf::math::Timer _heartbear_timer{this->config().heartbeat_period}; + kf::math::Timer _heartbear_timer{this->config().heartbeat_timer}; bool _heartbeat_timer_reset_required{true}; [[nodiscard]] bool sendHeartbeat(transport::TransportLink &transport_link) const noexcept { diff --git a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp index c8d1b76..aaacc7c 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp @@ -4,34 +4,36 @@ #pragma once #include -#include +#include #include #include -#include #include #include +#include #include "djc/ManualInput.hpp" #include "djc/protocol/Protocol.hpp" #include "djc/transport/TransportLink.hpp" -namespace djc::protocol { - -namespace internal { +namespace djc::internal { /// @brief Configuration for the ProtocolLink. -struct ProtocolLinkConfig final : kf::mixin::NonCopyable { +struct ProtocolLinkConfig final { - kf::math::Milliseconds poll_period;///< Interval between calls to the active protocol's `poll()` method. + kf::math::Timer::Config poll_timer;///< Interval between calls to the active protocol's `poll()` method. - [[nodiscard]] static constexpr ProtocolLinkConfig defaults() noexcept { + [[nodiscard]] static constexpr auto defaults() noexcept { return ProtocolLinkConfig{ - .poll_period = static_cast(1000 / 50), + .poll_timer = { + .period = static_cast(1000 / 50), + }, }; } }; -}// namespace internal +}// namespace djc::internal + +namespace djc::protocol { /// @brief Manages the active protocol and calls its `poll()` method at fixed intervals. /// @note @@ -88,7 +90,7 @@ struct ProtocolLink : static constexpr auto logger{kf::Logger::create("ProtocolLink")}; Protocol *_protocol{nullptr}; - kf::math::Timer _poll_timer{this->config().poll_period}; + kf::math::Timer _poll_timer{this->config().poll_timer}; bool _poll_timer_reset_required{true}; }; diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp index 315091c..b31f478 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -10,23 +10,23 @@ #include "djc/protocol/Protocol.hpp" #include "djc/protocol/RawProtocol.hpp" -namespace djc::protocol { - -namespace internal { +namespace djc::internal { /// @brief Configuration container for the ProtocolRegistry. -struct ProtocolRegistryConfig final : kf::mixin::NonCopyable { +struct ProtocolRegistryConfig final { - MavlinkProtocol::Config mavlink;///< MAVLink protocol configuration + protocol::MavlinkProtocol::Config mavlink;///< MAVLink protocol configuration - [[nodiscard]] static constexpr ProtocolRegistryConfig defaults() noexcept { + [[nodiscard]] static constexpr auto defaults() noexcept { return ProtocolRegistryConfig{ - .mavlink = MavlinkProtocol::Config::defaults(), + .mavlink = protocol::MavlinkProtocol::Config::defaults(), }; } }; -}// namespace internal +}// namespace djc::internal + +namespace djc::protocol { /// @brief Storage for all available protocol implementations. struct ProtocolRegistry final : From 382e2734bf5a84c17cc503aeb1fe4b36a1feff17 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 19:47:14 +0300 Subject: [PATCH 146/326] wip(migration): update protocol link --- DJC-Firmware/src/djc/protocol/ProtocolLink.hpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp index aaacc7c..32a6797 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include #include @@ -38,7 +39,7 @@ namespace djc::protocol { /// @brief Manages the active protocol and calls its `poll()` method at fixed intervals. /// @note /// Holds a pointer to a `Protocol` instance. -/// On every `poll()` call, checks a timer and invokes `_protocol->poll()` if the period has expired. +/// On every `poll()` call, checks a timer and invokes `_protocol.unwrap().poll()` if the period has expired. /// Forwards incoming data to the active protocol via `receive()`. struct ProtocolLink : @@ -53,7 +54,7 @@ struct ProtocolLink : /// @brief Set the active protocol implementation. /// @param new_protocol Reference to a protocol instance (must outlive this object). void protocol(Protocol &new_protocol) noexcept { - _protocol = &new_protocol; + _protocol = kf::someRef(new_protocol); } /// @brief Called periodically to drive the active protocol. @@ -62,7 +63,7 @@ struct ProtocolLink : /// @param transport_link Transport to use for sending data. /// @note The call is forwarded to the active protocol only when the poll period expires. void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept { - if (nullptr == _protocol) { + if (_protocol.isNone()) { logger.error("poll: no protocol set"); return; } @@ -71,25 +72,25 @@ struct ProtocolLink : _poll_timer.start(now); _poll_timer_reset_required = false; - _protocol->poll(now, input, transport_link); + _protocol.unwrap().poll(now, input, transport_link); } } /// @brief Forward a received data buffer to the active protocol. /// @param buffer Raw data received from the transport. void receive(kf::Slice buffer) noexcept { - if (nullptr == _protocol) { + if (_protocol.isNone()) { logger.error("receive: no protocol set"); return; } - _protocol->receive(buffer); + _protocol.unwrap().receive(buffer); } private: static constexpr auto logger{kf::Logger::create("ProtocolLink")}; - Protocol *_protocol{nullptr}; + kf::Option _protocol{kf::none}; kf::math::Timer _poll_timer{this->config().poll_timer}; bool _poll_timer_reset_required{true}; }; From f4f39f5bc0f126116601959bb6470af3ebf147e0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 19:51:53 +0300 Subject: [PATCH 147/326] wip(migration): update transport layer --- .../src/djc/transport/EspNowTransport.hpp | 53 ++++++-------- .../src/djc/transport/PeerAddress.hpp | 50 ++++++++----- DJC-Firmware/src/djc/transport/Transport.hpp | 38 +++++----- .../src/djc/transport/TransportLink.hpp | 70 +++++++++---------- .../src/djc/transport/TransportRegistry.hpp | 4 +- 5 files changed, 113 insertions(+), 102 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index b932e53..23db984 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -5,10 +5,11 @@ #include #include -#include #include +#include #include #include +#include #include "djc/transport/PeerAddress.hpp" #include "djc/transport/Transport.hpp" @@ -20,8 +21,8 @@ namespace djc::transport { struct EspNowTransport : Transport, kf::mixin::Initable { [[nodiscard]] bool send(kf::Slice buffer) noexcept override { - if (_active_peer.hasValue()) { - return _active_peer.value().writeBuffer(buffer).isOk(); + if (_active_peer.isSome()) { + return _active_peer.unwrap().writeBuffer(buffer).isOk(); } else { return false; } @@ -34,7 +35,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { static constexpr auto logger{kf::Logger::create("EspNowTransport")}; /// @brief MAC address used for ESP‑NOW broadcast. - static constexpr EspNow::Mac broadcast_mac_address{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + static constexpr kf::network::MacAddress broadcast_mac_address{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; protected: /// @brief Establish a connection to a peer. @@ -45,21 +46,12 @@ struct EspNowTransport : Transport, kf::mixin::Initable { if (address.kind() != Kind::EspNow) { return false; } _active_peer = addPeer(address.mac()); - if (not _active_peer.hasValue()) { return false; } + if (_active_peer.isNone()) { return false; } - const auto receive_setup_result = _active_peer.value().onReceive([this](kf::Slice buffer) { + _active_peer.unwrap().callback([this](kf::Slice buffer) { invokeReceive(buffer); }); - if (receive_setup_result.isError()) { - logger.error( - LogString::formatted( - "Receive callback attachment failed: %s", - EspNow::stringFromError(receive_setup_result.error())) - .view()); - return false; - } - logger.info("Connected: OK"); return true; } @@ -72,7 +64,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { return; } - auto &peer = _active_peer.value(); + auto &peer = _active_peer.unwrap(); if (not peer.exist()) { logger.error("Disconnect failed: Peer not exit"); return; @@ -89,20 +81,20 @@ struct EspNowTransport : Transport, kf::mixin::Initable { _broadcast_peer{},///< Broadcast peer for discovery. _active_peer{}; ///< Currently connected peer - static kf::Option addPeer(const EspNow::Mac &mac) noexcept { - auto peer_result = EspNow::Peer::add(mac); + static auto addPeer(const kf::network::MacAddress &mac) noexcept -> kf::Option { + auto peer_result = EspNow::Peer::create(mac); if (peer_result.isError()) { logger.error( LogString::formatted( "Failed to add peer [%s] :%s", - EspNow::stringFromMac(mac).data(), - EspNow::stringFromError(peer_result.error())) + mac.toString().data(), + peer_result.error().toString().data()) .view()); - return {}; + return kf::none; } - logger.info(LogString::formatted("Peer '%s' added", EspNow::stringFromMac(mac).data()).view()); - return {std::move(peer_result.value())}; + logger.info(LogString::formatted("Peer '%s' added", mac.toString().data()).view()); + return kf::some(std::move(peer_result.ok())); } static void delPeer(EspNow::Peer &peer) noexcept { @@ -111,17 +103,14 @@ struct EspNowTransport : Transport, kf::mixin::Initable { logger.error( LogString::formatted( "Failed to delete peer [%s] : %s", - EspNow::stringFromMac(peer.mac()).data(), - EspNow::stringFromError(result.error())) + peer.mac().toString().data(), + result.error().toString().data()) .view()); return; } } - // impl - using This = EspNowTransport; - - KF_IMPL_INITABLE(This, bool); + KF_IMPL_INITABLE(EspNowTransport, bool); bool initImpl() noexcept { logger.info("init"); @@ -129,12 +118,12 @@ struct EspNowTransport : Transport, kf::mixin::Initable { const auto result = espnow.init(); if (result.isError()) { - logger.error(LogString::formatted("Failed to initialize ESP-NOW: %s", EspNow::stringFromError(result.error()))); + logger.error(LogString::formatted("Failed to initialize ESP-NOW: %s", result.error().toString().data())); return false; } - espnow.onReceiveFromUnknown([this](const EspNow::Mac &mac, kf::Slice buffer){ - invokeReceiveForeign(PeerAddress::fromEspnowMac(mac), buffer); + espnow.callback([this](const kf::network::MacAddress &mac, kf::Slice buffer) { + invokeReceiveForeign(PeerAddress::fromEspnowMac(mac), buffer); }); _broadcast_peer = addPeer(broadcast_mac_address); diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp index da92fb2..e46645e 100644 --- a/DJC-Firmware/src/djc/transport/PeerAddress.hpp +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -4,23 +4,29 @@ #pragma once #include +#include #include +#include #include "djc/transport/Kind.hpp" +namespace djc::internal { + +using PeerAddressStringType = kf::memory::StaticString<32>; + +} + namespace djc::transport { /// @brief Unified address of a peer, independent of the underlying transport. /// /// Holds either a MAC address (ESP‑NOW). /// The active kind is stored in a tag field; the union contains the actual address. -struct PeerAddress { - - using ReprString = kf::memory::StaticString<32>; +struct PeerAddress : kf::mixin::StringRepresentable { /// @brief create an ESP‑NOW peer address from a MAC. /// @param mac 6‑byte MAC address (EspNow::Mac). - static constexpr PeerAddress fromEspnowMac(const kf::network::EspNow::Mac &mac) noexcept { + static constexpr PeerAddress fromEspnowMac(const kf::network::MacAddress &mac) noexcept { PeerAddress ret{}; ret._kind = Kind::EspNow, ret._mac = mac; @@ -28,11 +34,15 @@ struct PeerAddress { } /// @brief Return the kind of transport this address belongs to. - [[nodiscard]] Kind kind() const noexcept { return _kind; } + [[nodiscard]] Kind kind() const noexcept { + return _kind; + } /// @brief Return the stored MAC address. /// @note available if kind() == Kind::EspNow. - [[nodiscard]] kf::network::EspNow::Mac mac() const noexcept { return _mac; } + [[nodiscard]] kf::network::MacAddress mac() const noexcept { + return _mac; + } /// @brief Equality comparison. /// @note Two addresses are equal if they have the same kind and the same underlying value. @@ -49,25 +59,29 @@ struct PeerAddress { } /// @brief Inequality comparison (delegates to operator==). - [[nodiscard]] bool operator!=(const PeerAddress &other) const noexcept { return not this->operator==(other); } - - /// @brief Get String Representation - [[nodiscard]] ReprString toString() const noexcept { - switch (_kind) { - case Kind::EspNow: - return ReprString::formatted("%s@EspNow", kf::network::EspNow::stringFromMac(_mac).data()); - - default: - return {}; - } + [[nodiscard]] bool operator!=(const PeerAddress &other) const noexcept { + return not this->operator==(other); } private: Kind _kind; union { - kf::network::EspNow::Mac _mac; + kf::network::MacAddress _mac; }; + + using S = internal::PeerAddressStringType; + + KF_IMPL_STRING_REPRESENTABLE(PeerAddress, S); + auto toStringImpl() const noexcept { + switch (_kind) { + case Kind::EspNow: + return S::formatted("%s@EspNow", _mac.toString().data()); + + default: + return S{}; + } + } }; }// namespace djc::transport \ No newline at end of file diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index b501101..0d0e0ec 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -5,9 +5,9 @@ #include #include -#include #include #include +#include #include "djc/transport/PeerAddress.hpp" @@ -44,33 +44,41 @@ struct Transport : kf::mixin::NonCopyable { public: /// @brief Register a callback for incoming data. /// @param callback Functor invoked on each received packet. - void onReceive(ReceiveCallback &&callback) noexcept { _receive_callback = std::move(callback); } + void onReceive(ReceiveCallback &&callback) noexcept { + _receive_callback = kf::some(std::move(callback)); + } /// @brief Register a callback for incoming data from non-primary peer /// @param callback Functor invoked on each received packet. - void onReceiveForeign(ReceiveCallback &&callback) noexcept { _broadcast_receive_callback = std::move(callback); } + void onReceiveForeign(ReceiveCallback &&callback) noexcept { + _broadcast_receive_callback = kf::some(std::move(callback)); + } /// @brief Check whether the transport is currently connected to a peer. /// @return true if a peer is active, false otherwise. - [[nodiscard]] bool connected() const noexcept { return _active_peer_address.hasValue(); } + [[nodiscard]] bool connected() const noexcept { + return _active_peer_address.isSome(); + } /// @brief Get the address of the currently connected peer. /// @return Option containing the peer address if connected, empty Option otherwise. - [[nodiscard]] const kf::Option &activePeerAddress() const noexcept { return _active_peer_address; } + [[nodiscard]] kf::Option activePeerAddress() const noexcept { + return _active_peer_address.isNone() ? kf::none : kf::someRef(_active_peer_address.unwrap()); + } /// @brief Connect to a remote peer. /// @param peer_address Address of the peer to connect to. /// @return true on success, false on failure. [[nodiscard]] bool connect(const PeerAddress &address) noexcept { if (connected()) { - if (_active_peer_address.value() == address) { return true; }// already on this peer + if (_active_peer_address.unwrap() == address) { return true; }// already on this peer disconnect(); } if (not doConnect(address)) { return false; } - _active_peer_address = address; + _active_peer_address = kf::someTrivial(address); return true; } @@ -79,27 +87,25 @@ struct Transport : kf::mixin::NonCopyable { /// @note Safe to call even if not connected. void disconnect() noexcept { doDisconnect(); - - _active_peer_address = {}; + _active_peer_address.reset(); } protected: void invokeReceive(kf::Slice buffer) noexcept { - if (_active_peer_address.hasValue() and _receive_callback) { - _receive_callback(_active_peer_address.value(), buffer); + if (_active_peer_address.isSome() and _receive_callback.isSome()) { + _receive_callback.unwrap()(_active_peer_address.unwrap(), buffer); } } void invokeReceiveForeign(const PeerAddress &address, kf::Slice buffer) noexcept { - if (_broadcast_receive_callback) { - _broadcast_receive_callback(address, buffer); + if (_broadcast_receive_callback.isSome()) { + _broadcast_receive_callback.unwrap()(address, buffer); } } private: - kf::Option _active_peer_address{}; - ReceiveCallback _receive_callback{}; - ReceiveCallback _broadcast_receive_callback{}; + kf::Option _receive_callback{kf::none}, _broadcast_receive_callback{kf::none}; + kf::TrivialOption _active_peer_address{kf::none}; }; }// namespace djc::transport \ No newline at end of file diff --git a/DJC-Firmware/src/djc/transport/TransportLink.hpp b/DJC-Firmware/src/djc/transport/TransportLink.hpp index d43f02b..4e96354 100644 --- a/DJC-Firmware/src/djc/transport/TransportLink.hpp +++ b/DJC-Firmware/src/djc/transport/TransportLink.hpp @@ -13,22 +13,24 @@ #include "djc/transport/PeerAddress.hpp" #include "djc/transport/Transport.hpp" -namespace djc::transport { - -namespace internal { +namespace djc::internal { /// @brief Configuration parameters for the Transport Link. struct TransportLinkConfig final : kf::mixin::NonCopyable { - kf::math::Milliseconds disconnect_timeout; + kf::math::Timer::Config disconnect_timer; [[nodiscard]] static constexpr TransportLinkConfig defaults() noexcept { return TransportLinkConfig{ - .disconnect_timeout = 15'000, + .disconnect_timer = { + .period = 15'000, + }, }; } }; -}// namespace internal +}// namespace djc::internal + +namespace djc::transport { /// @brief Connection manager for a single transport. /// @note Separates connection lifecycle and inactivity timeout from higher‑level logic. @@ -48,97 +50,95 @@ struct TransportLink final : /// @note Ensures that changing the transport does not leave a stale connection open, /// which would silently keep receiving data on the old transport. void transport(Transport &new_transport) noexcept { - if (nullptr != _transport and _transport->connected()) { - _transport->disconnect(); + if (_transport.isSome() and _transport.unwrap().connected()) { + _transport.unwrap().disconnect(); } - _transport = &new_transport; + _transport = kf::someRef(new_transport); } /// @brief Forward a data buffer to the underlying transport. /// @return true if the transport reported success, false on error or if no transport is set. [[nodiscard]] bool send(kf::Slice buffer) noexcept { - if (nullptr == _transport) { + if (_transport.isNone()) { logger.error("send failed: no transport set"); return false; } - return _transport->send(buffer); + return _transport.unwrap().send(buffer); } /// @brief Register a callback for incoming data. /// @note The callback is invoked for every received packet. /// This method overwrites the transport‑level receive handler so that each incoming packet also resets the inactivity timer. void onReceive(Transport::ReceiveCallback &&callback) noexcept { - if (nullptr == _transport) { + if (_transport.isNone()) { logger.error("onReceive failed: no transport set"); return; } - _receive_callback = std::move(callback); + _receive_callback = kf::some(std::move(callback)); - _transport->onReceive([this](const PeerAddress &address, kf::Slice buffer) { - if (_receive_callback) { _receive_callback(address, buffer); } + _transport.unwrap().onReceive([this](const PeerAddress &address, kf::Slice buffer) { + if (_receive_callback.isSome()) { + _receive_callback.unwrap()(address, buffer); + } _disconnect_timer_reset_required = true; }); } /// @brief Register a callback for incoming data from other peers (non-primary) void onReceiveForeign(Transport::ReceiveCallback &&callback) noexcept { - if (nullptr == _transport) { + if (_transport.isNone()) { logger.error("onReceiveForeign failed: no transport set"); return; } - _transport->onReceiveForeign(std::move(callback)); + _transport.unwrap().onReceiveForeign(std::move(callback)); } /// @brief Check whether the transport is currently connected. - [[nodiscard]] bool connected() const noexcept { return nullptr != _transport and _transport->connected(); } + [[nodiscard]] bool connected() const noexcept { + return _transport.isSome() and _transport.unwrap().connected(); + } /// @brief Return the address of the active peer, if any. /// @return Reference to an empty option when no transport is set. - [[nodiscard]] const kf::Option &activePeerAddress() const noexcept { - return (nullptr == _transport) ? null_option : _transport->activePeerAddress(); + [[nodiscard]] kf::Option activePeerAddress() const noexcept { + return _transport.isNone() ? kf::none : _transport.unwrap().activePeerAddress(); } /// @brief Initiate a connection to a peer. [[nodiscard]] bool connect(const PeerAddress &address) noexcept { - if (nullptr == _transport) { + if (_transport.isNone()) { logger.error("connect failed: no transport set"); return false; } _disconnect_timer_reset_required = true; - return _transport->connect(address); + return _transport.unwrap().connect(address); } /// @brief Disconnect from the current peer. void disconnect() noexcept { - if (nullptr == _transport) { + if (_transport.isNone()) { logger.error("disconnect failed: no transport set"); return; } - _transport->disconnect(); + _transport.unwrap().disconnect(); } private: - /// @brief Empty address returned when no transport is active. - static constexpr kf::Option null_option{}; - static constexpr auto logger{kf::Logger::create("TransportLink")}; - Transport *_transport{nullptr}; ///< Currently active transport (may be nullptr). - Transport::ReceiveCallback _receive_callback{}; ///< User‑supplied callback for incoming data. - kf::math::Timer _disconnect_timer{this->config().disconnect_timeout};///< Inactivity timer. - volatile bool _disconnect_timer_reset_required{false}; ///< Flag: reset timer on next poll. - - // impl - using This = TransportLink; + kf::Option _transport{kf::none}; ///< Currently active transport (optional) + kf::Option _receive_callback{kf::none};///< User‑supplied callback for incoming data. + kf::math::Timer _disconnect_timer{this->config().disconnect_timer};///< Inactivity timer. + volatile bool _disconnect_timer_reset_required{false}; ///< Flag: reset timer on next poll. - KF_IMPL_TIMED_POLLABLE(This); + KF_IMPL_TIMED_POLLABLE(TransportLink); void pollImpl(kf::math::Milliseconds now) noexcept { if (not connected()) { return; } diff --git a/DJC-Firmware/src/djc/transport/TransportRegistry.hpp b/DJC-Firmware/src/djc/transport/TransportRegistry.hpp index 860db56..7534ba4 100644 --- a/DJC-Firmware/src/djc/transport/TransportRegistry.hpp +++ b/DJC-Firmware/src/djc/transport/TransportRegistry.hpp @@ -21,7 +21,9 @@ struct TransportRegistry { } /// @brief Direct access to the ESP‑NOW transport instance. - [[nodiscard]] EspNowTransport &espnow() noexcept { return _espnow_transport; } + [[nodiscard]] EspNowTransport &espnow() noexcept { + return _espnow_transport; + } private: EspNowTransport _espnow_transport{}; From a53f0eab74e8dfc992437bd142c7dd2b174663b1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 21:41:09 +0300 Subject: [PATCH 148/326] wip(migration): update services --- .../src/djc/service/AutoConnectService.hpp | 34 +++++----- DJC-Firmware/src/djc/service/Control.hpp | 16 +++-- .../src/djc/service/DisplayManager.hpp | 13 ++-- DJC-Firmware/src/djc/service/InputHandler.hpp | 57 +++++++++++------ .../src/djc/service/PeerScanningService.hpp | 62 +++++++++---------- 5 files changed, 108 insertions(+), 74 deletions(-) diff --git a/DJC-Firmware/src/djc/service/AutoConnectService.hpp b/DJC-Firmware/src/djc/service/AutoConnectService.hpp index 9c0ff86..4d37e74 100644 --- a/DJC-Firmware/src/djc/service/AutoConnectService.hpp +++ b/DJC-Firmware/src/djc/service/AutoConnectService.hpp @@ -15,24 +15,26 @@ #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" -namespace djc::service { - -namespace internal { +namespace djc::internal { -struct AutoConnectServiceConfig final : kf::mixin::NonCopyable { +struct AutoConnectServiceConfig final { - kf::math::Milliseconds cooldown;///< Delay before the service reacts to a new target. - bool enabled; ///< Whether the service is active. + kf::math::Timer::Config cooldown_timer;///< Delay before the service reacts to a new target. + bool enabled; ///< Whether the service is active. [[nodiscard]] static constexpr AutoConnectServiceConfig defaults() noexcept { return AutoConnectServiceConfig{ - .cooldown = 10'000, + .cooldown_timer = { + .period = 10'000, + }, .enabled = true, }; } }; -}// namespace internal +}// namespace djc::internal + +namespace djc::service { /// @brief Service that automatically connects to a trusted peer after a configurable delay /// @note @@ -54,7 +56,9 @@ struct AutoConnectService final : _cooldown_timer.start(0); } - [[nodiscard]] const kf::Option &target() const noexcept { return _target; } + [[nodiscard]] auto target() const noexcept -> const kf::TrivialOption & { + return _target; + } /// @brief Assign a new target for automatic connection /// @note @@ -62,24 +66,24 @@ struct AutoConnectService final : /// This prevents interrupting an active connection. void target(const transport::PeerAddress &new_target) noexcept { if (not _transport_link.connected()) { - _target.value(new_target); + _target = kf::someTrivial(new_target); } } private: const transport::TransportLink &_transport_link; - kf::Option _target{}; - kf::math::Timer _cooldown_timer{this->config().cooldown}; + kf::TrivialOption _target{}; + kf::math::Timer _cooldown_timer{this->config().cooldown_timer}; KF_IMPL_TIMED_POLLABLE(AutoConnectService); void pollImpl(kf::math::Milliseconds now) noexcept { if (not this->config().enabled) { return; } - if (not _target.hasValue()) { return; } + if (_target.isNone()) { return; } if (not _cooldown_timer.expired(now)) { return; } - this->invoke(_target.value()); - _target = {}; + this->invoke(_target.unwrap()); + _target.reset(); _cooldown_timer.start(now); } diff --git a/DJC-Firmware/src/djc/service/Control.hpp b/DJC-Firmware/src/djc/service/Control.hpp index 64b737a..c75ce85 100644 --- a/DJC-Firmware/src/djc/service/Control.hpp +++ b/DJC-Firmware/src/djc/service/Control.hpp @@ -24,16 +24,24 @@ struct Control final : Service { _transport_link{transport_link}, _protocol_link{protocol_link} {} /// @brief Returns the current manual input values. - [[nodiscard]] const ManualInput &input() const noexcept { return _manual_input; } + [[nodiscard]] const ManualInput &input() const noexcept { + return _manual_input; + } /// @brief Updates the manual input to be transmitted. - void input(const ManualInput &new_input) noexcept { _manual_input = new_input; } + void input(const ManualInput &new_input) noexcept { + _manual_input = new_input; + } /// @brief Checks whether control output is enabled. - [[nodiscard]] bool enabled() const noexcept { return _enabled; } + [[nodiscard]] bool enabled() const noexcept { + return _enabled; + } /// @brief Enables or disables control output. - void enabled(bool is_enabled) noexcept { _enabled = is_enabled; } + void enabled(bool is_enabled) noexcept { + _enabled = is_enabled; + } private: transport::TransportLink &_transport_link; diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 0504ac9..831fe80 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -33,10 +33,13 @@ template struct DisplayManager final : explicit DisplayManager(DisplayDriverImpl &display_driver, const transport::TransportLink &transport_link) noexcept : _display_driver{display_driver}, _transport_link{transport_link} {} - void showConnectionStatusOverlay(bool show) noexcept { _show_connection_status_overlay = show; } + void showConnectionStatusOverlay(bool show) noexcept { + _show_connection_status_overlay = show; + } private: - using Palette = kf::gfx::Palette; + using P = typename DisplayDriverImpl::PixelImpl; + using Palette = kf::gfx::Palette

; inline static const auto &virtual_keyboard = input::VirtualKeyboard::instance(); @@ -44,7 +47,7 @@ template struct DisplayManager final : const transport::TransportLink &_transport_link; bool _show_connection_status_overlay{false}; - kf::gfx::Canvas _canvas{}; + kf::gfx::Canvas

_canvas{}; void onRender(kf::memory::StringView str) noexcept { _canvas.background(Palette::black); @@ -127,8 +130,8 @@ template struct DisplayManager final : KF_IMPL_INITABLE(This, void); void initImpl() noexcept { - _canvas = kf::gfx::Canvas{ - kf::image::DynamicImage{_display_driver.image()}, + _canvas = kf::gfx::Canvas

{ + kf::image::DynamicImage

{_display_driver.image()}, kf::gfx::fonts::gyver_5x7_en, }; _canvas.autoNextLine(true); diff --git a/DJC-Firmware/src/djc/service/InputHandler.hpp b/DJC-Firmware/src/djc/service/InputHandler.hpp index 26e9368..da88be7 100644 --- a/DJC-Firmware/src/djc/service/InputHandler.hpp +++ b/DJC-Firmware/src/djc/service/InputHandler.hpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include #include @@ -20,15 +22,19 @@ struct InputHandler final : Service { using ClickCallback = kf::Function; using DirectionCallback = kf::Function; - struct Config final : kf::mixin::NonCopyable { + struct Config final { JoystickListener::Config joystick_listener; static constexpr Config defaults() noexcept { return Config{ .joystick_listener = JoystickListener::Config{ + .repeat_timer = { + .period = 100,// ms + }, + .delay_timer = { + .period = 400,// ms + }, .threshold = 0.6f, - .repeat_timeout = 100,// ms - .delay = 400, // ms }, }; } @@ -43,37 +49,52 @@ struct InputHandler final : Service { _left_button_listener{left_button_listener}, _right_button_listener{right_button_listener} {} - void onRightButton(ClickCallback &&callback) noexcept { _right_click_callback = std::move(callback); } + template void onRightButton(F &&callback) noexcept { + _right_click_callback = kf::some(ClickCallback{std::forward(callback)}); + } + + void onRightButton(kf::NoneType) { + _right_click_callback.reset(); + } + + template void onLeftButton(F &&callback) noexcept { + _left_click_callback = kf::some(ClickCallback{std::forward(callback)}); + } - void onLeftButton(ClickCallback &&callback) noexcept { _left_click_callback = std::move(callback); } + void onLeftButton(kf::NoneType) { + _left_click_callback.reset(); + } - void onDirection(DirectionCallback &&callback) noexcept { _direction_callback = std::move(callback); } + template void onDirection(F &&callback) noexcept { + _direction_callback = kf::some(DirectionCallback{std::forward(callback)}); + } + + void onDirection(kf::NoneType) noexcept { + _direction_callback.reset(); + } private: JoystickListener _joystick_listener; - DirectionCallback _direction_callback{}; - - ButtonListener &_left_button_listener; - ClickCallback _left_click_callback{}; + ButtonListener &_left_button_listener, &_right_button_listener; - ButtonListener &_right_button_listener; - ClickCallback _right_click_callback{}; + kf::Option _direction_callback{kf::none}; + kf::Option _left_click_callback{kf::none}, _right_click_callback{kf::none}; KF_IMPL_TIMED_POLLABLE(InputHandler); void pollImpl(kf::math::Milliseconds now) noexcept { _left_button_listener.poll(now); - if (_left_click_callback and _left_button_listener.clicked()) { - _left_click_callback(); + if (_left_click_callback.isSome() and _left_button_listener.clicked()) { + _left_click_callback.unwrap()(); } _right_button_listener.poll(now); - if (_right_click_callback and _right_button_listener.clicked()) { - _right_click_callback(); + if (_right_click_callback.isSome() and _right_button_listener.clicked()) { + _right_click_callback.unwrap()(); } _joystick_listener.poll(now); - if (_direction_callback and (_joystick_listener.direction() != JoystickListener::Direction::Home) and _joystick_listener.changed()) { - _direction_callback(_joystick_listener.direction()); + if (_direction_callback.isSome() and (_joystick_listener.direction() != JoystickListener::Direction::Home) and _joystick_listener.changed()) { + _direction_callback.unwrap()(_joystick_listener.direction()); } } }; diff --git a/DJC-Firmware/src/djc/service/PeerScanningService.hpp b/DJC-Firmware/src/djc/service/PeerScanningService.hpp index 38df2ed..4731a39 100644 --- a/DJC-Firmware/src/djc/service/PeerScanningService.hpp +++ b/DJC-Firmware/src/djc/service/PeerScanningService.hpp @@ -4,37 +4,38 @@ #pragma once #include -#include +#include #include #include #include -#include #include #include +#include #include "djc/service/Service.hpp" #include "djc/transport/PeerAddress.hpp" #include "djc/transport/TransportLink.hpp" -namespace djc::service { - -namespace internal { +namespace djc::internal { /// @brief Configuration parameters for the PeerScanningService service. struct PeerScannerConfig final : kf::mixin::NonCopyable { - kf::math::Milliseconds - entry_max_life_time, ///< How long an entry stays in the list without being refreshed. - entries_list_update_period;///< Interval between periodic clean-ups and list compaction. + kf::math::Milliseconds entry_max_life_time; ///< How long an entry stays in the list without being refreshed. + kf::math::Timer::Config entries_list_update_timer;///< Interval between periodic clean-ups and list compaction. [[nodiscard]] static constexpr PeerScannerConfig defaults() noexcept { return PeerScannerConfig{ .entry_max_life_time = 8'000, - .entries_list_update_period = 100, + .entries_list_update_timer = { + .period = 100, + }, }; } }; -}// namespace internal +}// namespace djc::internal + +namespace djc::service { /// @brief Background service that listens for foreign (broadcast) packets and maintains a list of visible peers. /// @note @@ -66,40 +67,37 @@ struct PeerScanningService final : /// @return A contiguous view of the first `_active_count` elements of the internal array. /// @note The slice is valid only until the next call to `poll()`. /// The entries are sorted in order of registration (oldest first). - [[nodiscard]] kf::Slice> peers() const noexcept { + [[nodiscard]] kf::Slice> peers() const noexcept { return {_entries.data(), _active_count}; } private: - kf::memory::Array, max_entries> _entries{}; - kf::math::Timer _update_poll_timer{this->config().entries_list_update_period}; + kf::memory::Array, max_entries> _entries{}; + kf::math::Timer _update_poll_timer{this->config().entries_list_update_timer}; transport::TransportLink &_transport_link; kf::math::Milliseconds _last_poll_time{0}; kf::usize _active_count{0}; - // impl - using This = PeerScanningService; - - KF_IMPL_INITABLE(This, void); + KF_IMPL_INITABLE(PeerScanningService, void); void initImpl() noexcept { _update_poll_timer.start(0);// enable timer _transport_link.onReceiveForeign([this](const transport::PeerAddress &address, kf::Slice buffer) -> void { // search for mathing entry for (auto &entry: _entries) { - if (entry.hasValue() and entry.value().address == address) { - entry.value().last_seen = _last_poll_time; + if (entry.isSome() and entry.unwrap().address == address) { + entry.unwrap().last_seen = _last_poll_time; return; } } // search for first empty entry for (auto &entry: _entries) { - if (not entry.hasValue()) { - entry = Entry{ + if (entry.isNone()) { + entry = kf::someTrivial(Entry{ .address = address, .last_seen = _last_poll_time, - }; + }); return; } } @@ -107,15 +105,15 @@ struct PeerScanningService final : // no available entries -> replace oldest with newest kf::usize oldest_entry_index{0}; for (auto i = 1u; i < max_entries; i += 1) { - if (_entries[i].value().last_seen < _entries[oldest_entry_index].value().last_seen) { + if (_entries[i].unwrap().last_seen < _entries[oldest_entry_index].unwrap().last_seen) { oldest_entry_index = i; } } - _entries[oldest_entry_index] = Entry{address, _last_poll_time}; + _entries[oldest_entry_index] = kf::someTrivial(Entry{address, _last_poll_time}); }); } - KF_IMPL_TIMED_POLLABLE(This); + KF_IMPL_TIMED_POLLABLE(PeerScanningService); void pollImpl(kf::math::Milliseconds now) noexcept { _last_poll_time = now; @@ -123,10 +121,10 @@ struct PeerScanningService final : _update_poll_timer.start(now); if (_transport_link.connected()) { - const auto &active_address = _transport_link.activePeerAddress().value(); + const auto &active_address = _transport_link.activePeerAddress().unwrap(); for (auto &entry: _entries) { - if (entry.hasValue() and entry.value().address == active_address) { - entry = {}; + if (entry.isSome() and entry.unwrap().address == active_address) { + entry.reset(); break; } } @@ -134,9 +132,9 @@ struct PeerScanningService final : auto write_index = 0u; for (auto read_index = 0u; read_index < max_entries; read_index += 1) { - if (_entries[read_index].hasValue()) { - if (now > _entries[read_index].value().last_seen + this->config().entry_max_life_time) { - _entries[read_index] = {}; + if (_entries[read_index].isSome()) { + if (now > _entries[read_index].unwrap().last_seen + this->config().entry_max_life_time) { + _entries[read_index].reset(); } else { if (write_index != read_index) { _entries[write_index] = _entries[read_index]; @@ -147,7 +145,7 @@ struct PeerScanningService final : } for (auto i = write_index; i < max_entries; i += 1) { - _entries[i] = {}; + _entries[i].reset(); } _active_count = write_index; } From f0f7e83e3ed8d8e945ac50759493c0f0a17e01a9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 23:23:12 +0300 Subject: [PATCH 149/326] wip(miration): update ui, rest of firmware.. --- DJC-Firmware/src/djc/Config.hpp | 6 +- DJC-Firmware/src/djc/ConfigManager.hpp | 16 ++- DJC-Firmware/src/djc/ManualInput.hpp | 1 + .../src/djc/MavlinkTelemetryRegistry.hpp | 4 +- .../src/djc/PeerFavoritesRegistry.hpp | 44 +++---- DJC-Firmware/src/djc/Periphery.hpp | 2 +- .../src/djc/service/DisplayManager.hpp | 112 ++++++++++-------- .../src/djc/transport/PeerAddress.hpp | 10 +- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 24 ++-- .../src/djc/ui/pages/MavlinkTelemetryPage.hpp | 5 +- .../src/djc/ui/pages/PeerDetailPage.hpp | 41 ++++--- .../src/djc/ui/pages/PeerExplorerPage.hpp | 64 ++++++---- .../src/djc/ui/pages/PeerFavoritePage.hpp | 45 +++---- .../src/djc/ui/pages/RawProtocolPage.hpp | 7 +- DJC-Firmware/src/djc/ui/pages/RootPage.hpp | 2 +- .../src/djc/ui/widgets/PeerDisplay.hpp | 31 ++--- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 16 ++- DJC-Firmware/src/main.cpp | 69 +++++++---- 18 files changed, 288 insertions(+), 211 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 387e084..2949a3f 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -5,8 +5,8 @@ // lib #include -#include #include +#include #include "djc/PeerFavoritesRegistry.hpp" #include "djc/Periphery.hpp" @@ -56,7 +56,9 @@ struct Config { service::PeerScanningService::Config peer_scanner; service::AutoConnectService::Config auto_connect_service; - [[nodiscard]] bool isLatestVersion() const noexcept { return version == latest_version; } + [[nodiscard]] bool isLatestVersion() const noexcept { + return version == latest_version; + } static constexpr Config defaults() noexcept { return Config{ diff --git a/DJC-Firmware/src/djc/ConfigManager.hpp b/DJC-Firmware/src/djc/ConfigManager.hpp index 74b31d5..0d65a3b 100644 --- a/DJC-Firmware/src/djc/ConfigManager.hpp +++ b/DJC-Firmware/src/djc/ConfigManager.hpp @@ -13,13 +13,21 @@ namespace djc { struct ConfigManager final : kf::mixin::NonCopyable { - [[nodiscard]] constexpr const Config &config() const noexcept { return _storage.config; } + [[nodiscard]] constexpr const Config &config() const noexcept { + return _storage.config; + } - [[nodiscard]] Config &config() noexcept { return _storage.config; } + [[nodiscard]] Config &config() noexcept { + return _storage.config; + } - [[nodiscard]] bool modified() const noexcept { return _modified; } + [[nodiscard]] bool modified() const noexcept { + return _modified; + } - void modified(bool is_modified) noexcept { _modified = is_modified; } + void modified(bool is_modified) noexcept { + _modified = is_modified; + } void save() noexcept { logger.info("Saving config to NVS"); diff --git a/DJC-Firmware/src/djc/ManualInput.hpp b/DJC-Firmware/src/djc/ManualInput.hpp index cb3e56c..719f3c4 100644 --- a/DJC-Firmware/src/djc/ManualInput.hpp +++ b/DJC-Firmware/src/djc/ManualInput.hpp @@ -15,6 +15,7 @@ struct ManualInput final { using Unit = kf::i16; /// @brief Scale factor - normalised float [‑1, +1] is multiplied by this value + /// @note Mavlink manual control compatable scaling static constexpr Unit scale_factor{1000}; Unit left_x, left_y, right_x, right_y;///< Joystick axes diff --git a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp index 16eced6..f42ffa3 100644 --- a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp +++ b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp @@ -23,7 +23,9 @@ struct MavlinkTelemetryRegistry final : kf::mixin::NonCopyable { explicit constexpr Entry(MessageDecoder message_decoder) noexcept : _message_decoder{message_decoder} {} - [[nodiscard]] const ValueType &value() const noexcept { return _value; } + [[nodiscard]] const ValueType &value() const noexcept { + return _value; + } /// @brief Check if the entry was updated after the given timestamp [[nodiscard]] bool updatedSince(kf::math::Milliseconds since) const noexcept { diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index bc0c496..8dea4c4 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -7,11 +7,11 @@ #include #include -#include -#include #include +#include #include #include +#include #include "djc/transport/PeerAddress.hpp" @@ -45,41 +45,41 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } }; - explicit constexpr PeerFavoritesRegistry(kf::Slice> entries) noexcept : _entries{entries} {} + explicit constexpr PeerFavoritesRegistry(kf::Slice> entries) noexcept : _entries{entries} {} /// @brief Return the entire slot array, including empty slots. - [[nodiscard]] kf::Slice> all() const noexcept { + [[nodiscard]] kf::Slice> all() const noexcept { return {_entries.data(), _active_count}; } /// @brief Obtain a const pointer to an entry by address. /// @param address Peer address to search for. - [[nodiscard]] const kf::Option &get(const transport::PeerAddress &address) const noexcept { - static constexpr kf::Option none{}; - - if (const auto index = indexOf(address); index.hasValue()) { - if (const auto &option = _entries[index.value()]; option.hasValue()) { return option; } + [[nodiscard]] kf::Option get(const transport::PeerAddress &address) const noexcept { + if (const auto index = indexOf(address); index.isSome()) { + if (const auto &option = _entries[index.unwrap()]; option.isSome()) { + return kf::someRef(option.unwrap()); + } } - return none; + return kf::none; } /// @brief Check exists to an entry by address [[nodiscard]] bool exists(const transport::PeerAddress &address) const noexcept { - return indexOf(address).hasValue(); + return indexOf(address).isSome(); } /// @brief Update already existed or Add a new entry /// @return true, or false if the list is full. [[nodiscard]] bool put(const Entry &entry_to_add) noexcept { - if (auto index = indexOf(entry_to_add.address); index.hasValue()) { - _entries[index.value()].value(entry_to_add); + if (auto index = indexOf(entry_to_add.address); index.isSome()) { + _entries[index.unwrap()] = kf::someTrivial(entry_to_add); return true; } const bool can_add = (_active_count < _entries.size()); if (can_add) { - _entries[_active_count].value(entry_to_add); + _entries[_active_count] = kf::someTrivial(entry_to_add); _active_count += 1; } @@ -93,10 +93,10 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable const auto index = indexOf(address); const auto last_index = _active_count - 1u; - if (not index.hasValue()) { return false; } + if (index.isNone()) { return false; } - if (index.value() != last_index) { - _entries[index.value()] = std::move(_entries[last_index]); + if (index.unwrap() != last_index) { + _entries[index.unwrap()] = _entries[last_index]; } _entries[last_index] = {}; @@ -106,7 +106,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } private: - kf::Slice> _entries; + kf::Slice> _entries; kf::usize _active_count{0}; /// @brief Find the index of an entry by address. @@ -115,11 +115,11 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable [[nodiscard]] kf::Option indexOf(const transport::PeerAddress &address) const noexcept { for (auto index = 0u; index < _entries.size(); index += 1) { const auto &item = _entries[index]; - if (item.hasValue() and item.value().address == address) { - return {index}; + if (item.isSome() and item.unwrap().address == address) { + return kf::some(index); } } - return {}; + return kf::none; } // impl @@ -127,7 +127,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable void initImpl() noexcept { _active_count = 0; for (const auto &entry: _entries) { - _active_count += static_cast(entry.hasValue()); + _active_count += static_cast(entry.isSome()); } } }; diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index 164e935..585d604 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -9,10 +9,10 @@ #include #include -#include #include #include #include +#include #include "djc/prelude.hpp" diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 831fe80..a6d8028 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -3,13 +3,16 @@ #pragma once +#include #include #include #include +#include #include #include #include #include +#include #include "djc/input/VirtualKeyboard.hpp" #include "djc/transport/TransportLink.hpp" @@ -33,10 +36,35 @@ template struct DisplayManager final : explicit DisplayManager(DisplayDriverImpl &display_driver, const transport::TransportLink &transport_link) noexcept : _display_driver{display_driver}, _transport_link{transport_link} {} + [[nodiscard]] kf::usize rowsTotal() const noexcept { + return _canvas.unwrap().heightInGlyphs() - 1; + } + + [[nodiscard]] kf::usize colsTotal() const noexcept { + return _canvas.unwrap().widthInGlyphs(); + } + void showConnectionStatusOverlay(bool show) noexcept { _show_connection_status_overlay = show; } + void onRender(kf::memory::StringView str) noexcept { + if (_canvas.isNone()) { return; } + + _canvas.unwrap().background(Palette::black); + _canvas.unwrap().foreground(Palette::white); + + _canvas.unwrap().fill(); + + if (virtual_keyboard.active()) { + renderVirtualKeyboard(); + } else { + renderUi(str); + } + + (void) _display_driver.send(); + } + private: using P = typename DisplayDriverImpl::PixelImpl; using Palette = kf::gfx::Palette

; @@ -47,51 +75,42 @@ template struct DisplayManager final : const transport::TransportLink &_transport_link; bool _show_connection_status_overlay{false}; - kf::gfx::Canvas

_canvas{}; - - void onRender(kf::memory::StringView str) noexcept { - _canvas.background(Palette::black); - _canvas.foreground(Palette::white); - - _canvas.fill(); - - if (virtual_keyboard.active()) { - renderVirtualKeyboard(); - } else { - renderUi(str); - } - } + kf::Option> _canvas{}; void renderUi(kf::memory::StringView str) noexcept { + auto &canvas = _canvas.unwrap(); + if (_show_connection_status_overlay) { - const auto y = static_cast(_canvas.maxY() - _canvas.glyphHeight()); - const auto overlay = _transport_link.connected() ? _transport_link.activePeerAddress().value().toString().data() : "Disconnected"; + const auto y = static_cast(canvas.maxY() - canvas.font().heightTotal()); + const auto overlay = _transport_link.connected() ? _transport_link.activePeerAddress().unwrap().toString().data() : "Disconnected"; - _canvas.background(Palette::bright_blue); - _canvas.foreground(Palette::black); - _canvas.text(0, y, overlay); + canvas.background(Palette::bright_blue); + canvas.foreground(Palette::black); + canvas.text(0, y, overlay); } - _canvas.background(Palette::black); - _canvas.foreground(Palette::white); - _canvas.text(0, 0, str.data()); + canvas.background(Palette::black); + canvas.foreground(Palette::white); + canvas.text(0, 0, str.data()); } void renderVirtualKeyboard() noexcept { + auto &canvas = _canvas.unwrap(); + const auto longest_row = input::VirtualKeyboard::rows[0].size(); - const auto key_width = _canvas.width() / longest_row; - const auto key_height = _canvas.glyphHeight(); - const auto keyboard_offset_y = _canvas.maxY() - key_height * virtual_keyboard.rowsTotal(); - const auto glyph_offset_x = (key_width - _canvas.glyphWidth()) / 2; + const auto key_width = canvas.width() / longest_row; + const auto key_height = canvas.font().heightTotal(); + const auto keyboard_offset_y = canvas.maxY() - key_height * virtual_keyboard.rowsTotal(); + const auto glyph_offset_x = (key_width - canvas.font().widthTotal()) / 2; char c[2]{0, 0}; - _canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", virtual_keyboard.available(), virtual_keyboard.text().size()).data()); - _canvas.text(0, _canvas.glyphHeight(), virtual_keyboard.text().data()); + canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", virtual_keyboard.available(), virtual_keyboard.text().size()).data()); + canvas.text(0, canvas.font().heightTotal(), virtual_keyboard.text().data()); - _canvas.background(Palette::bright_black); - _canvas.foreground(Palette::bright_black); - _canvas.rect(0, keyboard_offset_y, _canvas.maxX(), _canvas.maxY(), true); + canvas.background(Palette::bright_black); + canvas.foreground(Palette::bright_black); + canvas.rect(0, keyboard_offset_y, canvas.maxX(), canvas.maxY(), true); for (auto row = 0; row < virtual_keyboard.rowsTotal(); row += 1) { const auto y = keyboard_offset_y + row * key_height; @@ -103,14 +122,14 @@ template struct DisplayManager final : const auto x = col * key_width + x_offset; if (row == virtual_keyboard.cursorRow() and col == virtual_keyboard.cursorCol()) { - _canvas.foreground(Palette::blue); - _canvas.rect(x, y, x + key_width, y + key_height - 1, true); + canvas.foreground(Palette::blue); + canvas.rect(x, y, x + key_width, y + key_height - 1, true); - _canvas.background(Palette::blue); - _canvas.foreground(Palette::bright_white); + canvas.background(Palette::blue); + canvas.foreground(Palette::bright_white); } else { - _canvas.background(Palette::bright_black); - _canvas.foreground(Palette::black); + canvas.background(Palette::bright_black); + canvas.foreground(Palette::black); } const auto &key = input::VirtualKeyboard::keyAt(row, col); @@ -120,7 +139,7 @@ template struct DisplayManager final : c[0] = '?'; } - _canvas.text(x + glyph_offset_x, y, c); + canvas.text(x + glyph_offset_x, y, c); } } } @@ -130,19 +149,14 @@ template struct DisplayManager final : KF_IMPL_INITABLE(This, void); void initImpl() noexcept { - _canvas = kf::gfx::Canvas

{ + kf::gfx::Canvas

canvas{ kf::image::DynamicImage

{_display_driver.image()}, - kf::gfx::fonts::gyver_5x7_en, + typename kf::gfx::Canvas

::State{ + .active_font = kf::someRef(kf::gfx::fonts::gyver_5x7_en), + .auto_next_line = true, + }, }; - _canvas.autoNextLine(true); - - auto &config = ui::UI::instance().renderConfig(); - config.callback([this](kf::memory::StringView str) { - onRender(str); - (void) _display_driver.send(); - }); - config.row_max_length = _canvas.widthInGlyphs(); - config.rows_total = _canvas.heightInGlyphs() - 1; + _canvas = kf::some(std::move(canvas)); } KF_IMPL_TIMED_POLLABLE(This); diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp index e46645e..9e7e90e 100644 --- a/DJC-Firmware/src/djc/transport/PeerAddress.hpp +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -24,6 +24,8 @@ namespace djc::transport { /// The active kind is stored in a tag field; the union contains the actual address. struct PeerAddress : kf::mixin::StringRepresentable { + using StringType = internal::PeerAddressStringType; + /// @brief create an ESP‑NOW peer address from a MAC. /// @param mac 6‑byte MAC address (EspNow::Mac). static constexpr PeerAddress fromEspnowMac(const kf::network::MacAddress &mac) noexcept { @@ -69,17 +71,15 @@ struct PeerAddress : kf::mixin::StringRepresentable { explicit ConfigPage( + UI &ui, UI::Page &root, djc::ConfigManager &config_manager, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : - Page{"Config"}, + Page{ui, "Config"}, + _ui{ui}, _config_manager{config_manager}, + _peer_favorite_page{ui, *this, _peer_favoriter_registry}, _peer_favoriter_registry{peer_favoriter_registry}, _layout{{ &root.link(), @@ -58,7 +61,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _favorite_peers_fold_toggle_button.callback([this]() { show_favorites = not show_favorites; this->onEntry(); - UI::instance().addEvent(UI::Event::update()); + update(); }); _default_protocol_mode_selector.callback([this](Mode mode) { @@ -74,7 +77,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _peer_favorite_displays[i].callback([this](const transport::PeerAddress &address) -> void { _peer_favorite_page.bindPeer(address); - UI::instance().bindPage(_peer_favorite_page); + _ui.bindPage(_peer_favorite_page); }); } } @@ -92,12 +95,12 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { if (show_favorites) { for (auto i = 0u; i < all_favorites.size(); i += 1) { const auto &favorite = all_favorites[i]; - if (favorite.hasValue()) { - _peer_favorite_displays[i].state({widgets::PeerDisplay::State{ - .address = favorite.value().address, - .name = {{favorite.value().name.data(), favorite.value().name.size()}}, - .label_color = widgets::PeerDisplay::Color::Normal, - }}); + if (favorite.isSome()) { + _peer_favorite_displays[i].state(kf::some(widgets::PeerDisplay::State{ + .address = favorite.unwrap().address, + .name = kf::some(kf::memory::StringView{favorite.unwrap().name.data(), favorite.unwrap().name.size()}), + })); + _peer_favorite_displays[i].foreground(kf::ui::Color::Primary); } } } @@ -115,6 +118,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // state + UI &_ui; djc::ConfigManager &_config_manager; PeerFavoritesRegistry &_peer_favoriter_registry; kf::memory::StaticString<32> _label_favorites_buffer{}; @@ -164,7 +168,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // child pages - PeerFavoritePage _peer_favorite_page{*this, _peer_favoriter_registry}; + PeerFavoritePage _peer_favorite_page; kf::Slice layout(kf::usize displayed_peers) noexcept { return kf::Slice{_layout.data(), _layout.size()}.first(layout_regular_widgets + displayed_peers); diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 2010e74..e349691 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -22,11 +22,12 @@ namespace djc::ui::pages { /// @brief MAVLink telemetry page struct MavlinkTelemetryPage : UI::Page { explicit MavlinkTelemetryPage( + UI& ui, UI::Page &root, protocol::ProtocolRegistry &protocol_registry, protocol::ProtocolLink &protocol_link, MavlinkTelemetryRegistry &mavlink_telemetry_registry) noexcept : - Page{"Mavlink: Telemetry"}, + Page{ui, "Mavlink: Telemetry"}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, _mavlink_telemetry_registry{mavlink_telemetry_registry}, @@ -84,7 +85,7 @@ struct MavlinkTelemetryPage : UI::Page { } if (need_update) { - UI::instance().addEvent(UI::Event::update()); + update(); } } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index 2ad515e..08f96e9 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -17,10 +17,14 @@ namespace djc::ui::pages { struct PeerDetailPage final : UI::Page { explicit PeerDetailPage( + UI &ui, UI::Page &root, transport::TransportLink &transport_link, PeerFavoritesRegistry &peer_favorites_registry) noexcept : - Page{{}}, + Page{ui, {}}, + _ui{ui}, + _root{root}, + _transport_link{transport_link}, _peer_favorites_registry{peer_favorites_registry}, _layout{{ &root.link(), @@ -31,55 +35,58 @@ struct PeerDetailPage final : UI::Page { { widgets({_layout.data(), _layout.size()}); - _connection_button.callback([this, &transport_link, &root]() -> void { - if (not _peer_address.hasValue()) { return; } + _connection_button.callback([this]() -> void { + if (_peer_address.isNone()) { return; } - if (transport_link.connect(_peer_address.value())) { - UI::instance().bindPage(root); + if (_transport_link.connect(_peer_address.unwrap())) { + _ui.bindPage(_root); } else { _connection_button.label("Failed to connect"); } }); _peer_favorite_button.callback([this]() -> void { - if (not _peer_address.hasValue()) { return; } + if (_peer_address.isNone()) { return; } this->label("Back"); - _peer_favorite_page.bindPeer(_peer_address.value()); - UI::instance().bindPage(_peer_favorite_page); - UI::instance().addEvent(UI::Event::update()); + _peer_favorite_page.bindPeer(_peer_address.unwrap()); + _ui.bindPage(_peer_favorite_page); + update(); }); } void bindPeer(const transport::PeerAddress &address) noexcept { - _peer_address.value(address); + _peer_address = kf::someTrivial(address); } void onEntry() noexcept override { _connection_button.label("Connect"); - if (_peer_address.hasValue()) { - _label_buffer = _peer_address.value().toString(); + if (_peer_address.isSome()) { + _label_buffer = _peer_address.unwrap().toString(); this->label(_label_buffer.view()); - - _peer_favorite_button.label(_peer_favorites_registry.exists(_peer_address.value()) ? "Edit" : "Add to favorites"); + + _peer_favorite_button.label(_peer_favorites_registry.exists(_peer_address.unwrap()) ? "Edit" : "Add to favorites"); } } private: // state - kf::Option _peer_address{}; + UI &_ui; + UI::Page &_root; + transport::TransportLink &_transport_link; + kf::TrivialOption _peer_address{}; PeerFavoritesRegistry &_peer_favorites_registry; // widgets - transport::PeerAddress::ReprString _label_buffer{}; + transport::PeerAddress::StringType _label_buffer{}; UI::Button _connection_button{{}}, _peer_favorite_button{{}}; kf::memory::Array _layout; // child pages - PeerFavoritePage _peer_favorite_page{*this, _peer_favorites_registry}; + PeerFavoritePage _peer_favorite_page{_ui, *this, _peer_favorites_registry}; }; }// namespace djc::ui::pages diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 82885bb..1ea2c5e 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -3,11 +3,11 @@ #pragma once +#include #include #include #include #include -#include #include "djc/PeerFavoritesRegistry.hpp" #include "djc/service/PeerScanningService.hpp" @@ -22,14 +22,17 @@ namespace djc::ui::pages { struct PeerExplorerPage : UI::Page { explicit PeerExplorerPage( + UI &ui, UI::Page &root, transport::TransportLink &transport_link, service::PeerScanningService &peer_scanner, PeerFavoritesRegistry &peer_favorites_registry) noexcept : - Page{"Peer Explorer"}, + Page{ui, "Peer Explorer"}, + _ui{ui}, _transport_link{transport_link}, _peer_scanner{peer_scanner}, _peer_favorites_registry{peer_favorites_registry}, + _peer_detail_page{ui, *this, _transport_link, _peer_favorites_registry}, _layout{{ &root.link(), &_primary_connection_status_button, @@ -38,7 +41,7 @@ struct PeerExplorerPage : UI::Page { for (auto i = 0u; i < _peer_displays.size(); i += 1) { _peer_displays[i].callback([this](const transport::PeerAddress &address) -> void { _peer_detail_page.bindPeer(address); - UI::instance().bindPage(_peer_detail_page); + _ui.bindPage(_peer_detail_page); }); _layout[i + peer_display_start_index] = &_peer_displays[i]; @@ -59,8 +62,8 @@ struct PeerExplorerPage : UI::Page { if (not _redraw_timer.expired(now)) { return; } _redraw_timer.start(now); - if (_transport_link.activePeerAddress().hasValue()) { - (void) _connection_button_buffer.format("\xFC%s\x80", _transport_link.activePeerAddress().value().toString().data()); + if (_transport_link.activePeerAddress().isSome()) { + (void) _connection_button_buffer.format("\xFC%s\x80", _transport_link.activePeerAddress().unwrap().toString().data()); _primary_connection_status_button.label(_connection_button_buffer.view()); } else { _primary_connection_status_button.label( @@ -73,20 +76,35 @@ struct PeerExplorerPage : UI::Page { _available_label.value(_available_label_buffer.view()); for (auto i = 0u; i < available_peers.size(); i += 1) { - _peer_displays[i].state(createPeerDisplayState(available_peers[i], now)); + const auto &entry = available_peers[i]; + _peer_displays[i].state(createPeerDisplayState(entry, now)); + + if (entry.isSome()) { + constexpr auto extreme_age_factor{0.75f}; + const auto extreme_age = _peer_scanner.config().entry_max_life_time * extreme_age_factor; + const auto age = now - entry.unwrap().last_seen; + + using C = kf::ui::Color; + _peer_displays[i].background((age < extreme_age) ? C::Success : C::Warning); + } + } widgets(layout(available_peers.size())); - UI::instance().addEvent(UI::Event::update()); + update(); } private: static constexpr auto peer_display_start_index{3u}; + UI &_ui; transport::TransportLink &_transport_link; service::PeerScanningService &_peer_scanner; PeerFavoritesRegistry &_peer_favorites_registry; - kf::math::Timer _redraw_timer{static_cast(500)}; + kf::math::Timer::Config _redraw_timer_config{ + .period = 500, + }; + kf::math::Timer _redraw_timer{_redraw_timer_config}; kf::memory::StaticString<64> _available_label_buffer{}, _connection_button_buffer{}; @@ -97,35 +115,31 @@ struct PeerExplorerPage : UI::Page { kf::memory::Array _layout; // child pages - PeerDetailPage _peer_detail_page{*this, _transport_link, _peer_favorites_registry}; + PeerDetailPage _peer_detail_page; kf::Slice layout(kf::usize displayed_peers) noexcept { return kf::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); } - kf::Option createPeerDisplayState(const kf::Option &entry, kf::math::Milliseconds now) const noexcept { + kf::Option createPeerDisplayState(const kf::TrivialOption &entry, kf::math::Milliseconds now) const noexcept { using P = widgets::PeerDisplay; - constexpr auto extreme_age_factor{0.75f}; - const auto map_record = [](const kf::Option &record) -> kf::Option { - if (record.hasValue()) { - const auto &name = record.value().name; - return {{name.data(), name.size()}}; + const auto map_record = [](kf::Option record) -> kf::Option { + if (record.isSome()) { + const auto &name = record.unwrap().name; + return kf::some(kf::memory::StringView{name.data(), name.size()}); } else { - return {}; + return kf::none; } }; - if (entry.hasValue()) { - const auto age = now - entry.value().last_seen; - const auto extreme_age = _peer_scanner.config().entry_max_life_time * extreme_age_factor; - return {P::State{ - .address = entry.value().address, - .name = map_record(_peer_favorites_registry.get(entry.value().address)), - .label_color = (age < extreme_age) ? P::Color::Normal : P::Color::Warn, - }}; + if (entry.isSome()) { + return kf::some(P::State{ + .address = entry.unwrap().address, + .name = map_record(_peer_favorites_registry.get(entry.unwrap().address)), + }); } else { - return {}; + return kf::none; } } }; diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index f719014..f8d5ad3 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -16,8 +16,10 @@ namespace djc::ui::pages { struct PeerFavoritePage final : UI::Page { - explicit PeerFavoritePage(UI::Page &root, PeerFavoritesRegistry &peer_favorites_registry) noexcept : - Page{{}}, + explicit PeerFavoritePage(UI &ui, UI::Page &root, PeerFavoritesRegistry &peer_favorites_registry) noexcept : + Page{ui, {}}, + _ui{ui}, + _root{root}, _peer_favorites_registry{peer_favorites_registry}, _layout{{ // address and transport shows in title @@ -30,43 +32,47 @@ struct PeerFavoritePage final : UI::Page { { _confirm_button.callback([this]() -> void { - if (not _temp_entry.hasValue()) { return; } - _temp_entry.value().trust = _trust_input.value(); + if (_temp_entry.isNone()) { return; } + _temp_entry.unwrap().trust = _trust_input.value(); - const auto result = _peer_favorites_registry.put(_temp_entry.value()); + const auto result = _peer_favorites_registry.put(_temp_entry.unwrap()); _confirm_button.label(result ? "Writed" : "Write failed"); - UI::instance().addEvent(UI::Event::update()); + update(); }); + _confirm_button.foreground(kf::ui::Color::Primary); - _delete_button.callback([this, &root]() -> void { - if (not _temp_entry.hasValue()) { return; } + _delete_button.callback([this]() -> void { + if (_temp_entry.isNone()) { return; } - (void) _peer_favorites_registry.remove(_temp_entry.value().address); + (void) _peer_favorites_registry.remove(_temp_entry.unwrap().address); - UI::instance().bindPage(root); - UI::instance().addEvent(UI::Event::update()); + _ui.bindPage(_root); + update(); }); + _delete_button.background(kf::ui::Color::Error); } void bindPeer(const transport::PeerAddress &address) noexcept { const auto &entry_option = _peer_favorites_registry.get(address); - _temp_entry.value(entry_option.valueOr(PeerFavoritesRegistry::Entry::create(address))); + _temp_entry = kf::someTrivial(entry_option.unwrapOr(PeerFavoritesRegistry::Entry::create(address))); - (void) _label_buffer.format("%s Peer favorite\n%s", (entry_option.hasValue() ? "Edit" : "Add"), address.toString().data()); + (void) _label_buffer.format("%s Peer favorite\n%s", (entry_option.isSome() ? "Edit" : "Add"), address.toString().data()); this->label(_label_buffer.view()); - _description_input.source({_temp_entry.value().name.data(), _temp_entry.value().name.size()}); - _trust_input.value(_temp_entry.value().trust); + _description_input.source({_temp_entry.unwrap().name.data(), _temp_entry.unwrap().name.size()}); + _trust_input.value(_temp_entry.unwrap().trust); _confirm_button.label("Confirm"); - widgets(kf::Slice{_layout.data(), _layout.size()}.first(_layout.size() - (entry_option.hasValue() ? 0 : 1))); + widgets(kf::Slice{_layout.data(), _layout.size()}.first(_layout.size() - (entry_option.isSome() ? 0 : 1))); } private: + UI &_ui; + UI::Page &_root; PeerFavoritesRegistry &_peer_favorites_registry; - kf::Option _temp_entry{}; + kf::TrivialOption _temp_entry{}; kf::memory::StaticString<64> _label_buffer{}; @@ -76,7 +82,7 @@ struct PeerFavoritePage final : UI::Page { .value_range = PeerFavoritesRegistry::Entry::trust_range, .default_value = PeerFavoritesRegistry::Entry::trust_range.start, .step = static_cast(1), - .placement = UI::Placement::Outside, + .placement = kf::ui::Placement::Outside, .init_show_value = true, }; @@ -85,8 +91,7 @@ struct PeerFavoritePage final : UI::Page { UI::Labeled _labeled_trust_input{"Trust", _trust_input}; UI::Labeled _labeled_description_input{"Name", _description_input}; - UI::Button _confirm_button{{}}, _delete_button{"\xF9" - "Delete\x80"}; + UI::Button _confirm_button{{}}, _delete_button{"Delete"}; kf::memory::Array _layout; }; diff --git a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index da166b2..43a874c 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -18,11 +18,12 @@ namespace djc::ui::pages { struct RawProtocolPage : UI::Page { explicit RawProtocolPage( + UI &ui, UI::Page &root, protocol::ProtocolRegistry &protocol_registry, protocol::ProtocolLink &protocol_link, transport::TransportLink &transport_link) noexcept : - Page{"Raw Protocol"}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, _transport_link{transport_link}, + Page{ui, "Raw Protocol"}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, _transport_link{transport_link}, _layout{{ &root.link(), &_message_input, @@ -32,7 +33,7 @@ struct RawProtocolPage : UI::Page { _send_button.callback([this]() { kf::memory::StringView s{_message.data(), _message.size()}; - s = s.sub(0, s.find('\0').valueOr(s.size())); + s = s.sub(0, s.find('\0').unwrapOr(s.size())); logger.debug(s); @@ -53,7 +54,7 @@ struct RawProtocolPage : UI::Page { } void onExit() noexcept override { - _protocol_registry.raw().callback(protocol::RawProtocol::CallbackType{}); + _protocol_registry.raw().callback(kf::none); } private: diff --git a/DJC-Firmware/src/djc/ui/pages/RootPage.hpp b/DJC-Firmware/src/djc/ui/pages/RootPage.hpp index c29a5b7..95c4be9 100644 --- a/DJC-Firmware/src/djc/ui/pages/RootPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RootPage.hpp @@ -13,7 +13,7 @@ namespace djc::ui::pages { struct RootPage : UI::Page { static constexpr auto max_items{4}; - explicit constexpr RootPage() noexcept : Page{"Main"} {} + explicit constexpr RootPage(UI &ui) noexcept : Page{ui, "Main"} {} void attach(UI::Page &page) noexcept { if (_items >= _layout.size()) { return; } diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index c971ff6..f3eca26 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -15,43 +15,32 @@ namespace djc::ui::widgets { struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked { - enum class Color : char { - Normal = '\xFC', - Warn = '\xF9', - }; - struct State final { transport::PeerAddress address; kf::Option name; - Color label_color; kf::memory::StringView displayName() const noexcept { - return name.hasValue() ? name.value().data() : address.toString().data(); + return name.isSome() ? name.unwrap().data() : address.toString().data(); } }; - void state(const kf::Option &new_state) noexcept { _state = new_state; } + void state(const kf::Option &new_state) noexcept { + _state = new_state; + } - void doRender(UI::RenderImpl &render) const noexcept override { + void doRender(UI::Traits::RenderImpl &render) const noexcept override { render.beginAltBlock(); - - if (_state.hasValue()) { - render.value( - kf::memory::StaticString<64>::formatted( - "%c%s\x80", - static_cast(_state.value().label_color), - _state.value().displayName()) - .view()); + if (_state.isSome()) { + render.value(_state.unwrap().displayName()); } - render.endAltBlock(); } bool onClick() noexcept override { - if (_state.hasValue()) { - this->invoke(_state.value().address); + if (_state.isSome()) { + this->invoke(_state.unwrap().address); } - return _state.hasValue(); + return _state.isSome(); } private: diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index 56100d7..89f354f 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -3,8 +3,8 @@ #pragma once -#include #include +#include #include #include "djc/input/VirtualKeyboard.hpp" @@ -18,11 +18,15 @@ struct TextInput final : UI::Widget { explicit constexpr TextInput(kf::Slice source) noexcept : _text_source{source} {} - void source(kf::Slice new_source) noexcept { _text_source = new_source; } + void source(kf::Slice new_source) noexcept { + _text_source = new_source; + } - bool available() const noexcept { return nullptr != _text_source.data(); } + bool available() const noexcept { + return _text_source.empty(); + } - void doRender(UI::RenderImpl &render) const noexcept override { + void doRender(UI::Traits::RenderImpl &render) const noexcept override { render.value(kf::memory::StringView{"\xFC'"}); render.value(string()); render.value(kf::memory::StringView{"'\x80"}); @@ -40,7 +44,7 @@ struct TextInput final : UI::Widget { return true; } - bool onEventValue(UI::Event::Value event_value) noexcept { + bool onEventValue(UI::Traits::EventImpl::Value event_value) noexcept { if (not virtual_keyboard.active()) { return false; } @@ -63,7 +67,7 @@ struct TextInput final : UI::Widget { [[nodiscard]] kf::memory::StringView string() const noexcept { if (available()) { const kf::memory::StringView s{_text_source.data(), _text_source.size()}; - return s.sub(0, s.find('\0').valueOr(s.size())); + return s.sub(0, s.find('\0').unwrapOr(s.size())); } else { return kf::memory::StringView{"not available"}; } diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 82ee30f..ac0a166 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -3,6 +3,7 @@ // framework #include +#include // lib #include @@ -39,6 +40,9 @@ #include "djc/ui/pages/RawProtocolPage.hpp" #include "djc/ui/pages/RootPage.hpp" +using UiRender = djc::ui::UI::Traits::RenderImpl; +using UiEvent = djc::ui::UI::Traits::EventImpl; + static constexpr auto logger{kf::Logger::create("main")}; static djc::ConfigManager config_manager{}; @@ -98,13 +102,26 @@ static djc::service::DisplayManager display_manager{ transport_link, }; -static djc::ui::UI ui{}; +static UiRender::Config ui_render_config{ + UiRender::Config::defaults(), +}; + +static UiRender ui_render{ + ui_render_config, +}; + +static djc::ui::UI ui{ + ui_render, +}; // pages -static djc::ui::pages::RootPage root_page{}; +static djc::ui::pages::RootPage root_page{ + ui, +}; static djc::ui::pages::PeerExplorerPage peer_explorer_page{ + ui, root_page, transport_link, peer_scanner, @@ -112,6 +129,7 @@ static djc::ui::pages::PeerExplorerPage peer_explorer_page{ }; static djc::ui::pages::MavlinkTelemetryPage mavlink_telemetry_page{ + ui, root_page, protocol_registry, protocol_link, @@ -119,6 +137,7 @@ static djc::ui::pages::MavlinkTelemetryPage mavlink_telemetry_page{ }; static djc::ui::pages::RawProtocolPage raw_protocol_page{ + ui, root_page, protocol_registry, protocol_link, @@ -126,6 +145,7 @@ static djc::ui::pages::RawProtocolPage raw_protocol_page{ }; static djc::ui::pages::ConfigPage config_page{ + ui, root_page, config_manager, peer_favoriter_registry, @@ -152,6 +172,13 @@ void setup() { } display_manager.init(); + ui_render_config.text.row_max_length = display_manager.colsTotal(); + ui_render_config.text.rows_total = display_manager.rowsTotal(); + ui_render.callback([](auto str) -> void { + display_manager.onRender(str); + }); + + WiFi.mode(WIFI_MODE_STA); if (not transport_registry.espnow().init()) { logger.error("failed to initialize espnow transport"); @@ -177,8 +204,6 @@ void setup() { }); { - using E = djc::ui::UI::Event; - input_handler.onLeftButton([]() { if (virtual_keyboard.active()) { virtual_keyboard.quit(); @@ -187,28 +212,28 @@ void setup() { display_manager.showConnectionStatusOverlay(control.enabled()); } - ui.addEvent(E::update()); + ui.addEvent(UiEvent::update()); }); input_handler.onRightButton([]() { if (control.enabled()) { return; } - ui.addEvent(E::widgetClick()); + ui.addEvent(UiEvent::widgetClick()); }); input_handler.onDirection([](djc::service::InputHandler::JoystickListener::Direction direction) { - static constexpr E navigation_event_from_direction[4] = { - E::pageCursorMove(-1),// Up - E::pageCursorMove(+1),// Down - E::widgetValue(-1), // Left - E::widgetValue(+1), // Right + static constexpr UiEvent navigation_event_from_direction[4] = { + UiEvent::pageCursorMove(-1),// Up + UiEvent::pageCursorMove(+1),// Down + UiEvent::widgetValue(-1), // Left + UiEvent::widgetValue(+1), // Right }; - static constexpr E virtual_keyboard_event_from_direction[4] = { - E::widgetValue(0),// Up - E::widgetValue(1),// Down - E::widgetValue(2),// Left - E::widgetValue(3),// Right + static constexpr UiEvent virtual_keyboard_event_from_direction[4] = { + UiEvent::widgetValue(0),// Up + UiEvent::widgetValue(1),// Down + UiEvent::widgetValue(2),// Left + UiEvent::widgetValue(3),// Right }; if (control.enabled()) { return; } @@ -224,7 +249,7 @@ void setup() { root_page.attach(config_page); ui.bindPage(root_page); - ui.addEvent(E::update()); + ui.addEvent(UiEvent::update()); } if (config_manager.modified()) { config_manager.save(); } @@ -239,22 +264,22 @@ void loop() { transport_link.poll(now); peer_scanner.poll(now); - if (auto_connect_service.config().enabled and not auto_connect_service.target().hasValue()) { + if (auto_connect_service.config().enabled and auto_connect_service.target().isNone()) { const auto favorites = peer_favoriter_registry.all(); if (favorites.size() > 0) { auto most_trusted_favorite_index = 0u; for (auto index = 1u; index < favorites.size(); index += 1) { - if (favorites[index].hasValue() and favorites[index].value().trust > favorites[most_trusted_favorite_index].value().trust) { + if (favorites[index].isSome() and favorites[index].unwrap().trust > favorites[most_trusted_favorite_index].unwrap().trust) { most_trusted_favorite_index = index; } } - if (const auto &most_trusted = favorites[most_trusted_favorite_index]; most_trusted.hasValue()) { + if (const auto &most_trusted = favorites[most_trusted_favorite_index]; most_trusted.isSome()) { for (const auto &peer: peer_scanner.peers()) { - if (peer.hasValue() and peer.value().address == most_trusted.value().address) { - auto_connect_service.target(most_trusted.value().address); + if (peer.isSome() and peer.unwrap().address == most_trusted.unwrap().address) { + auto_connect_service.target(most_trusted.unwrap().address); break; } } From 5c4dae388542a7e1e6d7698ed8d4b42df68ee724 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 23:49:15 +0300 Subject: [PATCH 150/326] fix(ui/TextInput): check available via null compare --- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index 89f354f..15de992 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -23,7 +23,7 @@ struct TextInput final : UI::Widget { } bool available() const noexcept { - return _text_source.empty(); + return nullptr != _text_source.data(); } void doRender(UI::Traits::RenderImpl &render) const noexcept override { From 36085ec9c5a8748369e2e5d6e33d804f9fff59e8 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 23:51:34 +0300 Subject: [PATCH 151/326] fix(transport/espnow): use manual dispatching bypasses KiraFlux-Toolkit's espnow issue --- .../src/djc/transport/EspNowTransport.hpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 23db984..f259b28 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -48,9 +48,11 @@ struct EspNowTransport : Transport, kf::mixin::Initable { _active_peer = addPeer(address.mac()); if (_active_peer.isNone()) { return false; } - _active_peer.unwrap().callback([this](kf::Slice buffer) { - invokeReceive(buffer); - }); + // KF-toolkit issue with callback dispatching + // _active_peer.unwrap().callback([this](kf::Slice buffer) { + // invokeReceive(buffer); + // logger.debug("unicast"); + // }); logger.info("Connected: OK"); return true; @@ -72,7 +74,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { delPeer(peer); - _active_peer = {}; + _active_peer.reset(); logger.info("Disconnected: OK"); } @@ -123,7 +125,11 @@ struct EspNowTransport : Transport, kf::mixin::Initable { } espnow.callback([this](const kf::network::MacAddress &mac, kf::Slice buffer) { - invokeReceiveForeign(PeerAddress::fromEspnowMac(mac), buffer); + if (_active_peer.isSome() and _active_peer.unwrap().mac() == mac) { + invokeReceive(buffer); + } else { + invokeReceiveForeign(PeerAddress::fromEspnowMac(mac), buffer); + } }); _broadcast_peer = addPeer(broadcast_mac_address); From 6ff3739581767470273d841749f46c88f332550e Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 11 Jun 2026 23:52:36 +0300 Subject: [PATCH 152/326] fix(main target peer finder): add isSome check --- DJC-Firmware/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index ac0a166..5eaf54e 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -271,7 +271,7 @@ void loop() { auto most_trusted_favorite_index = 0u; for (auto index = 1u; index < favorites.size(); index += 1) { - if (favorites[index].isSome() and favorites[index].unwrap().trust > favorites[most_trusted_favorite_index].unwrap().trust) { + if (favorites[index].isSome() and favorites[most_trusted_favorite_index].isSome() and favorites[index].unwrap().trust > favorites[most_trusted_favorite_index].unwrap().trust) { most_trusted_favorite_index = index; } } From 57fd1f65732b88758115354afc7615bbbc55be12 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 00:09:30 +0300 Subject: [PATCH 153/326] feat: make djc::UI as extension of kf::ui::UI --- DJC-Firmware/src/djc/ui/UI.hpp | 25 +++++++++++++++---- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 4 +-- .../src/djc/ui/pages/PeerExplorerPage.hpp | 6 ++--- .../src/djc/ui/widgets/PeerDisplay.hpp | 11 +++++--- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index f505de2..9793490 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -8,13 +8,28 @@ #include #include -namespace djc::ui { +#include "djc/ui/widgets/PeerDisplay.hpp" + +namespace djc::internal { -// KiraFlux-Toolkit UI specialization for ESP32-DJC -using UI = kf::ui::UI< - kf::ui::UiTraits< +using UiBase = kf::ui::UI,// Render Engine: Buffered Colored Text UI render engine kf::ui::Event<6> // Event: 6-bit Event value encoding >>; -}// namespace djc \ No newline at end of file +} + +namespace djc::ui { + +/// @brief KiraFlux-Toolkit UI expended specializalization for ESP32-DJC +struct UI : internal::UiBase { + using internal::UiBase::UiBase; + + using Widget = internal::UiBase::Widget; + + struct PeerDisplay : widgets::PeerDisplay { + using widgets::PeerDisplay::PeerDisplay; + }; +}; + +}// namespace djc::ui \ No newline at end of file diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index e9028f8..e8edd9d 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -96,7 +96,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { for (auto i = 0u; i < all_favorites.size(); i += 1) { const auto &favorite = all_favorites[i]; if (favorite.isSome()) { - _peer_favorite_displays[i].state(kf::some(widgets::PeerDisplay::State{ + _peer_favorite_displays[i].state(kf::some(UI::PeerDisplay::State{ .address = favorite.unwrap().address, .name = kf::some(kf::memory::StringView{favorite.unwrap().name.data(), favorite.unwrap().name.size()}), })); @@ -157,7 +157,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config}; UI::Labeled _labeled_default_protocol_mode_selector{"Init Protocol", _default_protocol_mode_selector}; - kf::memory::Array _peer_favorite_displays{}; + kf::memory::Array _peer_favorite_displays{}; UI::CheckBox _autoconnect_enabled_input{false}; UI::Labeled _labeled_autoconnect_enabled_input{"Autoconnect", _autoconnect_enabled_input}; diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 1ea2c5e..b3c4dca 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -110,7 +110,7 @@ struct PeerExplorerPage : UI::Page { UI::Button _primary_connection_status_button{{}}; UI::Display _available_label{_available_label_buffer.view()}; - kf::memory::Array _peer_displays{}; + kf::memory::Array _peer_displays{}; kf::memory::Array _layout; @@ -121,8 +121,8 @@ struct PeerExplorerPage : UI::Page { return kf::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); } - kf::Option createPeerDisplayState(const kf::TrivialOption &entry, kf::math::Milliseconds now) const noexcept { - using P = widgets::PeerDisplay; + kf::Option createPeerDisplayState(const kf::TrivialOption &entry, kf::math::Milliseconds now) const noexcept { + using P = UI::PeerDisplay; const auto map_record = [](kf::Option record) -> kf::Option { if (record.isSome()) { diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index f3eca26..d3dd956 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -4,17 +4,20 @@ #pragma once #include -#include #include #include +#include #include "djc/transport/PeerAddress.hpp" -#include "djc/ui/UI.hpp" namespace djc::ui::widgets { -struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked { +template struct PeerDisplay : + kf::ui::widgets::Widget, + kf::mixin::Callbacked + +{ struct State final { transport::PeerAddress address; kf::Option name; @@ -28,7 +31,7 @@ struct PeerDisplay final : UI::Widget, kf::mixin::Callbacked Date: Fri, 12 Jun 2026 00:23:35 +0300 Subject: [PATCH 154/326] feat(ui): using semantic color --- DJC-Firmware/src/djc/ui/UI.hpp | 3 +++ DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 11 +++++------ DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 4 ++-- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 14 ++++++++++---- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index 9793490..6df4c55 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "djc/ui/widgets/PeerDisplay.hpp" @@ -25,6 +26,8 @@ namespace djc::ui { struct UI : internal::UiBase { using internal::UiBase::UiBase; + using Color = kf::ui::Color; + using Widget = internal::UiBase::Widget; struct PeerDisplay : widgets::PeerDisplay { diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index e8edd9d..ee5f25f 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -100,7 +100,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { .address = favorite.unwrap().address, .name = kf::some(kf::memory::StringView{favorite.unwrap().name.data(), favorite.unwrap().name.size()}), })); - _peer_favorite_displays[i].foreground(kf::ui::Color::Primary); + _peer_favorite_displays[i].foreground(UI::Color::Primary); } } } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index b3c4dca..711dea1 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -63,12 +63,12 @@ struct PeerExplorerPage : UI::Page { _redraw_timer.start(now); if (_transport_link.activePeerAddress().isSome()) { - (void) _connection_button_buffer.format("\xFC%s\x80", _transport_link.activePeerAddress().unwrap().toString().data()); + (void) _connection_button_buffer.format("%s", _transport_link.activePeerAddress().unwrap().toString().data()); _primary_connection_status_button.label(_connection_button_buffer.view()); + _primary_connection_status_button.background(UI::Color::Success); } else { - _primary_connection_status_button.label( - "\xF9" - "Disconnected\x80"); + _primary_connection_status_button.label("Disconnected"); + _primary_connection_status_button.foreground(UI::Color::Disabled); } const auto available_peers = _peer_scanner.peers(); @@ -84,8 +84,7 @@ struct PeerExplorerPage : UI::Page { const auto extreme_age = _peer_scanner.config().entry_max_life_time * extreme_age_factor; const auto age = now - entry.unwrap().last_seen; - using C = kf::ui::Color; - _peer_displays[i].background((age < extreme_age) ? C::Success : C::Warning); + _peer_displays[i].foreground((age < extreme_age) ? UI::Color::Primary : UI::Color::Warning); } } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index f8d5ad3..1b54179 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -40,7 +40,7 @@ struct PeerFavoritePage final : UI::Page { update(); }); - _confirm_button.foreground(kf::ui::Color::Primary); + _confirm_button.foreground(UI::Color::Primary); _delete_button.callback([this]() -> void { if (_temp_entry.isNone()) { return; } @@ -50,7 +50,7 @@ struct PeerFavoritePage final : UI::Page { _ui.bindPage(_root); update(); }); - _delete_button.background(kf::ui::Color::Error); + _delete_button.background(UI::Color::Error); } void bindPeer(const transport::PeerAddress &address) noexcept { diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index 15de992..03b2037 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -14,9 +14,13 @@ namespace djc::ui::widgets { struct TextInput final : UI::Widget { - constexpr TextInput() noexcept : _text_source{} {} + TextInput() noexcept : _text_source{} { + initStyle(); + } - explicit constexpr TextInput(kf::Slice source) noexcept : _text_source{source} {} + explicit TextInput(kf::Slice source) noexcept : _text_source{source} { + initStyle(); + } void source(kf::Slice new_source) noexcept { _text_source = new_source; @@ -27,9 +31,7 @@ struct TextInput final : UI::Widget { } void doRender(UI::Traits::RenderImpl &render) const noexcept override { - render.value(kf::memory::StringView{"\xFC'"}); render.value(string()); - render.value(kf::memory::StringView{"'\x80"}); } bool onClick() noexcept override { @@ -60,6 +62,10 @@ struct TextInput final : UI::Widget { } private: + void initStyle() { + foreground(UI::Color::Info); + } + inline static auto &virtual_keyboard{input::VirtualKeyboard::instance()}; kf::Slice _text_source; From 939a52135170dc4f8beb7a5285847492cdabf8bb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 00:26:34 +0300 Subject: [PATCH 155/326] fix(ui/peer-explorer): set background and foreground for connection status button --- DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 711dea1..8b36dbd 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -65,10 +65,12 @@ struct PeerExplorerPage : UI::Page { if (_transport_link.activePeerAddress().isSome()) { (void) _connection_button_buffer.format("%s", _transport_link.activePeerAddress().unwrap().toString().data()); _primary_connection_status_button.label(_connection_button_buffer.view()); + _primary_connection_status_button.foreground(UI::Color::Normal); _primary_connection_status_button.background(UI::Color::Success); } else { _primary_connection_status_button.label("Disconnected"); _primary_connection_status_button.foreground(UI::Color::Disabled); + _primary_connection_status_button.background(UI::Color::Normal); } const auto available_peers = _peer_scanner.peers(); From a4c2f07f8cd5d387aee4fbdd6d3ee4f5475cb6ef Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 00:30:40 +0300 Subject: [PATCH 156/326] feat(ui/config): initialize selectors with actual config values --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index ee5f25f..18c4399 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -177,9 +177,10 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // impl KF_IMPL_INITABLE(ConfigPage, void); void initImpl() noexcept { - // _default_protocol_mode_selector.value(storage.config().init_protocol_mode); // todo: Combobox::value(T) - // _default_transport_kind_selector.value(storage.config().init_transport_kind); // todo: Combobox::value(T) - _autoconnect_enabled_input.value(_config_manager.config().auto_connect_service.enabled); + const auto &config = _config_manager.config(); + _default_protocol_mode_selector.value(config.init_protocol_mode); + _default_transport_kind_selector.value(config.init_transport_kind); + _autoconnect_enabled_input.value(config.auto_connect_service.enabled); } }; From 591b11f1b8b974a6dafcdc032ed7c44f16d5691b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 01:21:41 +0300 Subject: [PATCH 157/326] chore(transport/PeerAddress): remove espnow include --- DJC-Firmware/src/djc/transport/PeerAddress.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/DJC-Firmware/src/djc/transport/PeerAddress.hpp b/DJC-Firmware/src/djc/transport/PeerAddress.hpp index 9e7e90e..87080b0 100644 --- a/DJC-Firmware/src/djc/transport/PeerAddress.hpp +++ b/DJC-Firmware/src/djc/transport/PeerAddress.hpp @@ -5,7 +5,6 @@ #include #include -#include #include #include "djc/transport/Kind.hpp" From 362f583d864b51c5fb9dc0f36a9523741ad0f4b0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 01:22:18 +0300 Subject: [PATCH 158/326] feat: add patched version of kf::network::EspNow --- DJC-Firmware/src/kf-patch/EspNow.hpp | 297 +++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 DJC-Firmware/src/kf-patch/EspNow.hpp diff --git a/DJC-Firmware/src/kf-patch/EspNow.hpp b/DJC-Firmware/src/kf-patch/EspNow.hpp new file mode 100644 index 0000000..e467904 --- /dev/null +++ b/DJC-Firmware/src/kf-patch/EspNow.hpp @@ -0,0 +1,297 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: MIT + +#pragma once + +// std +#include + +// esp +#include +#include + +// lib +#include "kf/Option.hpp" +#include "kf/Result.hpp" +#include "kf/Slice.hpp" +#include "kf/io/Writable.hpp" +#include "kf/memory/Array.hpp" +#include "kf/memory/StringView.hpp" +#include "kf/mixin/Callbacked.hpp" +#include "kf/mixin/Initable.hpp" +#include "kf/mixin/MacAddressed.hpp" +#include "kf/mixin/NonCopyable.hpp" +#include "kf/mixin/Quitable.hpp" +#include "kf/mixin/Singleton.hpp" +#include "kf/mixin/StringRepresentable.hpp" +#include "kf/network/MacAddress.hpp" +#include "kf/primitives.hpp" + +namespace kf::internal { + +struct EspNowError final : mixin::StringRepresentable { + + enum Kind : u8 { + InternalError, ///< ESP-NOW internal API error + UnknownError, ///< Unknown ESP API error + NotInitialized, ///< ESP-NOW protocol not initialized + IncorrectWiFiMode,///< Incorrect WiFi interface mode set + PeerListIsFull, ///< Peer list is at maximum capacity + InvalidArg, ///< Invalid argument passed to API + NoMemory, ///< Insufficient memory for peer addition + PeerAlreadyExists,///< Peer already exists in list + PeerNotFound, ///< Peer not found in peer list + TooBigMessage, ///< Message size exceeds ESP_NOW_MAX_DATA_LEN + } kind; + + static constexpr EspNowError create(Kind kind) noexcept { + return EspNowError{.kind = kind}; + } + +private: + KF_IMPL_STRING_REPRESENTABLE(EspNowError, memory::StringView); + memory::StringView toStringImpl() const noexcept { + +#define CASE_RETURN(__v) \ + case __v: return #__v + + switch (kind) { + CASE_RETURN(EspNowError::NotInitialized); + CASE_RETURN(EspNowError::InternalError); + CASE_RETURN(EspNowError::TooBigMessage); + CASE_RETURN(EspNowError::InvalidArg); + CASE_RETURN(EspNowError::NoMemory); + CASE_RETURN(EspNowError::PeerNotFound); + CASE_RETURN(EspNowError::IncorrectWiFiMode); + CASE_RETURN(EspNowError::PeerListIsFull); + CASE_RETURN(EspNowError::PeerAlreadyExists); + default: + CASE_RETURN(EspNowError::UnknownError); + } + +#undef CASE_RETURN + } +}; + +}// namespace kf::internal + +namespace kf::network { + +/// @brief Encapsulates ESP-NOW protocol in safe C++ abstractions +/// @note Singleton wrapper for ESP-NOW API with peer management and callbacks +struct EspNow final : + + kf::mixin::Singleton, + mixin::MacAddressed, + kf::mixin::Initable>, + kf::mixin::Quitable, + kf::mixin::Callbacked> + +{ + /// @brief Maximum number of peers that can be stored in the registry + static constexpr usize max_peers{20}; + + /// @brief ESP-NOW operation error codes + using Error = internal::EspNowError; + + /// @brief ESP‑NOW peer representation with communication capabilities + /// @note Manages a single peer: registration, sending, and receive callback + /// @note Move‑only, non‑copyable + struct Peer final : + + mixin::NonCopyable, + mixin::MacAddressed, + io::Writable> + + { + friend struct EspNow; + + /// @brief Default configuration (station interface) + struct Config final { + wifi_interface_t wifi_interface;///< WiFi interface (STA or AP) + + [[nodiscard]] static constexpr Config defaults() noexcept { + return Config{ + .wifi_interface = WIFI_IF_STA, + }; + } + }; + + /// @brief Move constructor + /// @param other Source peer (becomes inactive) + Peer(Peer &&other) noexcept : mixin::MacAddressed{std::move(other)}, _owns{other._owns} { + other._owns = false; + } + + /// @brief Move assignment + /// @param other Source peer (becomes inactive) + /// @return Reference to this + Peer &operator=(Peer &&other) noexcept { + if (this != &other) { + if (_owns) { + (void) del(); + } + + static_cast(*this) = std::move(other); + _owns = other._owns; + other._owns = false; + } + + return *this; + } + + /// @brief Add a new peer to ESP‑NOW network + /// @param mac MAC address of the peer + /// @param config Configuration parameters (optional) + /// @return Peer object on success, Error on failure + /// @note Automatically registers the peer with the ESP‑NOW subsystem + [[nodiscard]] static Result create(const MacAddress &mac, Config config = Config::defaults()) noexcept { + esp_now_peer_info_t peer_info{ + .channel = 0, + .ifidx = config.wifi_interface, + .encrypt = false, + }; + std::copy(mac.begin(), mac.end(), peer_info.peer_addr); + + const auto result = esp_now_add_peer(&peer_info); + + if (ESP_OK == result) { + return ok(std::move(Peer{mac})); + } else { + return error(translateEspError(result)); + } + } + + /// @brief Remove peer from ESP-NOW network + /// @return Success or Error + /// @note Also removes any associated receive handler + [[nodiscard]] Result del() noexcept { + const auto result = esp_now_del_peer(_mac_address.data()); + + if (ESP_OK == result) { + return ok(); + } else { + return error(translateEspError(result)); + } + } + + /// @brief Check if peer exists in ESP-NOW network + /// @return true if peer is registered with ESP-NOW + [[nodiscard]] bool exist() noexcept { + return esp_now_is_peer_exist(_mac_address.data()); + } + + /// @brief Destructor - automatically removes the peer if owned + ~Peer() noexcept { + if (_owns) { + (void) del(); + } + } + + private: + using WriteResult = Result; + + bool _owns; + + /// @brief Private constructor – creates a peer owned by this object + /// @param mac MAC address + /// @note Called only by Peer::create + explicit Peer(const MacAddress &mac) noexcept : mixin::MacAddressed{mac}, _owns{true} {} + + /// @brief Internal send implementation + /// @param data Pointer to data buffer + /// @param len Length in bytes + /// @return Success or translated ESP‑NOW error + [[nodiscard]] WriteResult processSend(const void *data, usize len) noexcept { + const auto result = esp_now_send(_mac_address.data(), static_cast(data), len); + + if (ESP_OK == result) { + return ok(); + } else { + return error(translateEspError(result)); + } + } + + KF_IMPL_WRITABLE(Peer, WriteResult); + + WriteResult writeBufferImpl(Slice buffer) noexcept { + if (buffer.size() > ESP_NOW_MAX_DATA_LEN) { return error(Error::create(Error::TooBigMessage)); } + return processSend(buffer.data(), buffer.size()); + } + + template WriteResult writePacketImpl(T &&packet) noexcept { + static_assert(sizeof(T) < ESP_NOW_MAX_DATA_LEN, "Message is too big!"); + return processSend(static_cast(&packet), sizeof(T)); + } + + template WriteResult writeMixedImpl(T &&header, Slice buffer) noexcept { + const auto mixed_size = sizeof(T) + buffer.size(); + u8 mixed[mixed_size]; + + const auto header_data = reinterpret_cast(&header); + std::copy(header_data, header_data + sizeof(T), mixed); + std::copy(buffer.begin(), buffer.end(), mixed + sizeof(T)); + + return processSend(static_cast(mixed), mixed_size); + } + }; + +private: + friend struct ::kf::mixin::Singleton; + + /// @brief Private constructor (singleton) + constexpr EspNow() noexcept : MacAddressed{{}} {} + + /// @brief ESP‑NOW receive callback (static wrapper) + /// @param raw_mac_address Source MAC address (6 bytes) + /// @param data Received data buffer + /// @param size Size of received data + static void onReceive(const u8 *raw_mac_address, const u8 *data, int size) noexcept { + auto &self = EspNow::instance(); + + MacAddress source_mac_address; + std::copy(raw_mac_address, raw_mac_address + ESP_NOW_ETH_ALEN, source_mac_address.begin()); + + const Slice buffer{data, static_cast(size)}; + + self.invoke(source_mac_address, buffer); + } + + /// @brief Translate ESP error code to Error enum + /// @param result ESP error code + /// @return Corresponding Error enum value + [[nodiscard]] static Error translateEspError(esp_err_t result) noexcept { + switch (result) { + case ESP_ERR_ESPNOW_INTERNAL: return Error::create(Error::InternalError); + case ESP_ERR_ESPNOW_NOT_INIT: return Error::create(Error::NotInitialized); + case ESP_ERR_ESPNOW_ARG: return Error::create(Error::InvalidArg); + case ESP_ERR_ESPNOW_NO_MEM: return Error::create(Error::NoMemory); + case ESP_ERR_ESPNOW_NOT_FOUND: return Error::create(Error::PeerNotFound); + case ESP_ERR_ESPNOW_IF: return Error::create(Error::IncorrectWiFiMode); + case ESP_ERR_ESPNOW_FULL: return Error::create(Error::PeerListIsFull); + case ESP_ERR_ESPNOW_EXIST: return Error::create(Error::PeerAlreadyExists); + default: return Error::create(Error::UnknownError); + } + } + + KF_IMPL_INITABLE(EspNow, Result); + Result initImpl() noexcept { + (void) esp_read_mac(_mac_address.data(), ESP_MAC_WIFI_STA); + + const auto init_result = esp_now_init(); + if (ESP_OK != init_result) { return error(translateEspError(init_result)); } + + const auto handler_result = esp_now_register_recv_cb(onReceive); + if (ESP_OK != handler_result) { return error(translateEspError(handler_result)); } + + return ok(); + } + + KF_IMPL_QUITABLE(EspNow); + void quitImpl() noexcept { + (void) esp_now_unregister_recv_cb(); + (void) esp_now_deinit(); + } +}; + +}// namespace kf::network \ No newline at end of file From 7281bfeac2b04a98a3f83dcf012a21c91b3731a6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 01:22:35 +0300 Subject: [PATCH 159/326] fix: using patched espnow --- DJC-Firmware/src/djc/transport/EspNowTransport.hpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index f259b28..6b25f72 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -8,9 +8,10 @@ #include #include #include -#include #include +#include "kf-patch/EspNow.hpp"// patched version + #include "djc/transport/PeerAddress.hpp" #include "djc/transport/Transport.hpp" @@ -48,12 +49,6 @@ struct EspNowTransport : Transport, kf::mixin::Initable { _active_peer = addPeer(address.mac()); if (_active_peer.isNone()) { return false; } - // KF-toolkit issue with callback dispatching - // _active_peer.unwrap().callback([this](kf::Slice buffer) { - // invokeReceive(buffer); - // logger.debug("unicast"); - // }); - logger.info("Connected: OK"); return true; } @@ -72,8 +67,6 @@ struct EspNowTransport : Transport, kf::mixin::Initable { return; } - delPeer(peer); - _active_peer.reset(); logger.info("Disconnected: OK"); } From 6ea835c56d591f011a790d6931c7382ff50707a0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 01:29:23 +0300 Subject: [PATCH 160/326] style: using trailing return type for long types, breakdown some single-line methods --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 2 +- DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp | 8 ++++++-- DJC-Firmware/src/djc/transport/Transport.hpp | 2 +- DJC-Firmware/src/djc/transport/TransportLink.hpp | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index 8dea4c4..05dd91e 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -54,7 +54,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable /// @brief Obtain a const pointer to an entry by address. /// @param address Peer address to search for. - [[nodiscard]] kf::Option get(const transport::PeerAddress &address) const noexcept { + [[nodiscard]] auto get(const transport::PeerAddress &address) const noexcept -> kf::Option { if (const auto index = indexOf(address); index.isSome()) { if (const auto &option = _entries[index.unwrap()]; option.isSome()) { return kf::someRef(option.unwrap()); diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp index b31f478..4344dd6 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -60,10 +60,14 @@ struct ProtocolRegistry final : } /// @brief Direct access to the Raw protocol instance. - [[nodiscard]] RawProtocol &raw() noexcept { return _raw_protocol; } + [[nodiscard]] RawProtocol &raw() noexcept { + return _raw_protocol; + } /// @brief Direct access to the MAVLink protocol instance. - [[nodiscard]] MavlinkProtocol &mavlink() noexcept { return _mavlink_protocol; } + [[nodiscard]] MavlinkProtocol &mavlink() noexcept { + return _mavlink_protocol; + } private: RawProtocol _raw_protocol{}; diff --git a/DJC-Firmware/src/djc/transport/Transport.hpp b/DJC-Firmware/src/djc/transport/Transport.hpp index 0d0e0ec..1f92ec2 100644 --- a/DJC-Firmware/src/djc/transport/Transport.hpp +++ b/DJC-Firmware/src/djc/transport/Transport.hpp @@ -62,7 +62,7 @@ struct Transport : kf::mixin::NonCopyable { /// @brief Get the address of the currently connected peer. /// @return Option containing the peer address if connected, empty Option otherwise. - [[nodiscard]] kf::Option activePeerAddress() const noexcept { + [[nodiscard]] auto activePeerAddress() const noexcept -> kf::Option { return _active_peer_address.isNone() ? kf::none : kf::someRef(_active_peer_address.unwrap()); } diff --git a/DJC-Firmware/src/djc/transport/TransportLink.hpp b/DJC-Firmware/src/djc/transport/TransportLink.hpp index 4e96354..274fa6c 100644 --- a/DJC-Firmware/src/djc/transport/TransportLink.hpp +++ b/DJC-Firmware/src/djc/transport/TransportLink.hpp @@ -104,7 +104,7 @@ struct TransportLink final : /// @brief Return the address of the active peer, if any. /// @return Reference to an empty option when no transport is set. - [[nodiscard]] kf::Option activePeerAddress() const noexcept { + [[nodiscard]] auto activePeerAddress() const noexcept -> kf::Option { return _transport.isNone() ? kf::none : _transport.unwrap().activePeerAddress(); } From dcafda18fdf9f46f5b37e2f600a3a29ce27ef731 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 18:22:43 +0300 Subject: [PATCH 161/326] fix(kf-patch): update espnow wrapper cleanup, etc. --- DJC-Firmware/src/kf-patch/EspNow.hpp | 213 +++++++++++---------------- 1 file changed, 85 insertions(+), 128 deletions(-) diff --git a/DJC-Firmware/src/kf-patch/EspNow.hpp b/DJC-Firmware/src/kf-patch/EspNow.hpp index e467904..ab409de 100644 --- a/DJC-Firmware/src/kf-patch/EspNow.hpp +++ b/DJC-Firmware/src/kf-patch/EspNow.hpp @@ -4,7 +4,8 @@ #pragma once // std -#include +#include // std::copy +#include // std::move // esp #include @@ -15,7 +16,6 @@ #include "kf/Result.hpp" #include "kf/Slice.hpp" #include "kf/io/Writable.hpp" -#include "kf/memory/Array.hpp" #include "kf/memory/StringView.hpp" #include "kf/mixin/Callbacked.hpp" #include "kf/mixin/Initable.hpp" @@ -29,92 +29,91 @@ namespace kf::internal { +#define EVAL_KIND(__esp_error__) (__esp_error__ - ESP_ERR_ESPNOW_BASE) + +#define CASE_RETURN(__v) \ + case __v: return #__v + struct EspNowError final : mixin::StringRepresentable { enum Kind : u8 { - InternalError, ///< ESP-NOW internal API error - UnknownError, ///< Unknown ESP API error - NotInitialized, ///< ESP-NOW protocol not initialized - IncorrectWiFiMode,///< Incorrect WiFi interface mode set - PeerListIsFull, ///< Peer list is at maximum capacity - InvalidArg, ///< Invalid argument passed to API - NoMemory, ///< Insufficient memory for peer addition - PeerAlreadyExists,///< Peer already exists in list - PeerNotFound, ///< Peer not found in peer list - TooBigMessage, ///< Message size exceeds ESP_NOW_MAX_DATA_LEN + UnknownError = EVAL_KIND(ESP_ERR_ESPNOW_BASE), ///< Unknown ESP API error + NotInitialized = EVAL_KIND(ESP_ERR_ESPNOW_NOT_INIT),///< ESP-NOW protocol not initialized + InvalidArg = EVAL_KIND(ESP_ERR_ESPNOW_ARG), ///< Invalid argument passed to API + NoMemory = EVAL_KIND(ESP_ERR_ESPNOW_NO_MEM), ///< Insufficient memory for peer addition + PeerListIsFull = EVAL_KIND(ESP_ERR_ESPNOW_FULL), ///< Peer list is at maximum capacity + PeerNotFound = EVAL_KIND(ESP_ERR_ESPNOW_NOT_FOUND), ///< Peer not found in peer list + InternalError = EVAL_KIND(ESP_ERR_ESPNOW_INTERNAL), ///< ESP-NOW internal API error + PeerAlreadyExists = EVAL_KIND(ESP_ERR_ESPNOW_EXIST),///< Peer already exists in list + IncorrectWiFiMode = EVAL_KIND(ESP_ERR_ESPNOW_IF), ///< Incorrect WiFi interface mode set + TooBigMessage, ///< Message size exceeds ESP_NOW_MAX_DATA_LEN } kind; - static constexpr EspNowError create(Kind kind) noexcept { - return EspNowError{.kind = kind}; + [[nodiscard]] static constexpr auto create(Kind kind) noexcept -> ErrorWrapper { + return error(EspNowError{.kind = kind}); + } + + [[nodiscard]] static constexpr auto fromEspErr(esp_err_t e) noexcept -> ErrorWrapper { + return create((ESP_ERR_ESPNOW_BASE <= e and e <= ESP_ERR_ESPNOW_IF) ? UnknownError : static_cast(e - ESP_ERR_ESPNOW_BASE)); } private: KF_IMPL_STRING_REPRESENTABLE(EspNowError, memory::StringView); memory::StringView toStringImpl() const noexcept { -#define CASE_RETURN(__v) \ - case __v: return #__v - switch (kind) { CASE_RETURN(EspNowError::NotInitialized); - CASE_RETURN(EspNowError::InternalError); - CASE_RETURN(EspNowError::TooBigMessage); CASE_RETURN(EspNowError::InvalidArg); CASE_RETURN(EspNowError::NoMemory); - CASE_RETURN(EspNowError::PeerNotFound); - CASE_RETURN(EspNowError::IncorrectWiFiMode); CASE_RETURN(EspNowError::PeerListIsFull); + CASE_RETURN(EspNowError::PeerNotFound); + CASE_RETURN(EspNowError::InternalError); CASE_RETURN(EspNowError::PeerAlreadyExists); + CASE_RETURN(EspNowError::IncorrectWiFiMode); + CASE_RETURN(EspNowError::TooBigMessage); default: CASE_RETURN(EspNowError::UnknownError); } - -#undef CASE_RETURN } }; +#undef EVAL_KIND +#undef CASE_RETURN + }// namespace kf::internal namespace kf::network { -/// @brief Encapsulates ESP-NOW protocol in safe C++ abstractions +/// @brief ESP-NOW protocol encapsulataion /// @note Singleton wrapper for ESP-NOW API with peer management and callbacks struct EspNow final : - kf::mixin::Singleton, + mixin::Singleton, mixin::MacAddressed, - kf::mixin::Initable>, - kf::mixin::Quitable, - kf::mixin::Callbacked> + mixin::Initable>, + mixin::Quitable, + mixin::Callbacked> { - /// @brief Maximum number of peers that can be stored in the registry - static constexpr usize max_peers{20}; - - /// @brief ESP-NOW operation error codes + /// @brief ESP-NOW operation error type using Error = internal::EspNowError; + using VoidResult = Result; + /// @brief ESP‑NOW peer representation with communication capabilities - /// @note Manages a single peer: registration, sending, and receive callback + /// @note Manages a single peer: registration, sending /// @note Move‑only, non‑copyable struct Peer final : mixin::NonCopyable, mixin::MacAddressed, - io::Writable> + io::Writable { - friend struct EspNow; - - /// @brief Default configuration (station interface) + /// @brief Peer configuration struct Config final { - wifi_interface_t wifi_interface;///< WiFi interface (STA or AP) - - [[nodiscard]] static constexpr Config defaults() noexcept { - return Config{ - .wifi_interface = WIFI_IF_STA, - }; - } + MacAddress mac_address; + bool wifi_interface_sta;///< (true - STA, false - AP) }; /// @brief Move constructor @@ -123,116 +122,97 @@ struct EspNow final : other._owns = false; } + /// @brief Destructor - automatically removes the peer if owned + ~Peer() noexcept { + if (_owns) { (void) del(); } + } + /// @brief Move assignment /// @param other Source peer (becomes inactive) /// @return Reference to this Peer &operator=(Peer &&other) noexcept { if (this != &other) { - if (_owns) { - (void) del(); - } + if (_owns) { (void) del(); } static_cast(*this) = std::move(other); _owns = other._owns; other._owns = false; } - return *this; } /// @brief Add a new peer to ESP‑NOW network - /// @param mac MAC address of the peer - /// @param config Configuration parameters (optional) + /// @param config Peer Configuration /// @return Peer object on success, Error on failure - /// @note Automatically registers the peer with the ESP‑NOW subsystem - [[nodiscard]] static Result create(const MacAddress &mac, Config config = Config::defaults()) noexcept { + [[nodiscard]] static auto create(const Config &config) noexcept -> Result { esp_now_peer_info_t peer_info{ .channel = 0, - .ifidx = config.wifi_interface, + .ifidx = static_cast(config.wifi_interface_sta ? WIFI_IF_STA : WIFI_IF_AP), .encrypt = false, }; - std::copy(mac.begin(), mac.end(), peer_info.peer_addr); - - const auto result = esp_now_add_peer(&peer_info); + std::copy(config.mac_address.begin(), config.mac_address.end(), peer_info.peer_addr); - if (ESP_OK == result) { - return ok(std::move(Peer{mac})); + const auto e = esp_now_add_peer(&peer_info); + if (ESP_OK == e) { + return ok(std::move(Peer{config.mac_address})); } else { - return error(translateEspError(result)); + return Error::fromEspErr(e); } } /// @brief Remove peer from ESP-NOW network - /// @return Success or Error - /// @note Also removes any associated receive handler - [[nodiscard]] Result del() noexcept { - const auto result = esp_now_del_peer(_mac_address.data()); - - if (ESP_OK == result) { + [[nodiscard]] VoidResult del() noexcept { + const auto e = esp_now_del_peer(_mac_address.data()); + if (ESP_OK == e) { return ok(); } else { - return error(translateEspError(result)); + return Error::fromEspErr(e); } } /// @brief Check if peer exists in ESP-NOW network - /// @return true if peer is registered with ESP-NOW [[nodiscard]] bool exist() noexcept { return esp_now_is_peer_exist(_mac_address.data()); } - /// @brief Destructor - automatically removes the peer if owned - ~Peer() noexcept { - if (_owns) { - (void) del(); - } - } - private: - using WriteResult = Result; - - bool _owns; + bool _owns{true}; - /// @brief Private constructor – creates a peer owned by this object - /// @param mac MAC address /// @note Called only by Peer::create - explicit Peer(const MacAddress &mac) noexcept : mixin::MacAddressed{mac}, _owns{true} {} + explicit Peer(const MacAddress &mac) noexcept : mixin::MacAddressed{mac} {} /// @brief Internal send implementation - /// @param data Pointer to data buffer - /// @param len Length in bytes - /// @return Success or translated ESP‑NOW error - [[nodiscard]] WriteResult processSend(const void *data, usize len) noexcept { - const auto result = esp_now_send(_mac_address.data(), static_cast(data), len); - - if (ESP_OK == result) { + [[nodiscard]] VoidResult send(const u8 *data, usize len) noexcept { + const auto e = esp_now_send(_mac_address.data(), data, len); + if (ESP_OK == e) { return ok(); } else { - return error(translateEspError(result)); + return Error::fromEspErr(e); } } - KF_IMPL_WRITABLE(Peer, WriteResult); + KF_IMPL_WRITABLE(Peer, VoidResult); - WriteResult writeBufferImpl(Slice buffer) noexcept { - if (buffer.size() > ESP_NOW_MAX_DATA_LEN) { return error(Error::create(Error::TooBigMessage)); } - return processSend(buffer.data(), buffer.size()); + VoidResult writeBufferImpl(Slice buffer) noexcept { + if (buffer.size() > ESP_NOW_MAX_DATA_LEN) { return Error::create(Error::TooBigMessage); } + return send(buffer.data(), buffer.size()); } - template WriteResult writePacketImpl(T &&packet) noexcept { + template VoidResult writePacketImpl(T &&packet) noexcept { static_assert(sizeof(T) < ESP_NOW_MAX_DATA_LEN, "Message is too big!"); - return processSend(static_cast(&packet), sizeof(T)); + return send(reinterpret_cast(&packet), sizeof(T)); } - template WriteResult writeMixedImpl(T &&header, Slice buffer) noexcept { + template VoidResult writeMixedImpl(T &&header, Slice buffer) noexcept { const auto mixed_size = sizeof(T) + buffer.size(); + if (mixed_size > ESP_NOW_MAX_DATA_LEN) { return Error::create(Error::TooBigMessage); } u8 mixed[mixed_size]; const auto header_data = reinterpret_cast(&header); std::copy(header_data, header_data + sizeof(T), mixed); std::copy(buffer.begin(), buffer.end(), mixed + sizeof(T)); - return processSend(static_cast(mixed), mixed_size); + return send(mixed, mixed_size); } }; @@ -243,48 +223,25 @@ struct EspNow final : constexpr EspNow() noexcept : MacAddressed{{}} {} /// @brief ESP‑NOW receive callback (static wrapper) - /// @param raw_mac_address Source MAC address (6 bytes) - /// @param data Received data buffer - /// @param size Size of received data static void onReceive(const u8 *raw_mac_address, const u8 *data, int size) noexcept { - auto &self = EspNow::instance(); - MacAddress source_mac_address; std::copy(raw_mac_address, raw_mac_address + ESP_NOW_ETH_ALEN, source_mac_address.begin()); - const Slice buffer{data, static_cast(size)}; - - self.invoke(source_mac_address, buffer); + EspNow::instance().invoke(source_mac_address, Slice{data, static_cast(size)}); } - /// @brief Translate ESP error code to Error enum - /// @param result ESP error code - /// @return Corresponding Error enum value - [[nodiscard]] static Error translateEspError(esp_err_t result) noexcept { - switch (result) { - case ESP_ERR_ESPNOW_INTERNAL: return Error::create(Error::InternalError); - case ESP_ERR_ESPNOW_NOT_INIT: return Error::create(Error::NotInitialized); - case ESP_ERR_ESPNOW_ARG: return Error::create(Error::InvalidArg); - case ESP_ERR_ESPNOW_NO_MEM: return Error::create(Error::NoMemory); - case ESP_ERR_ESPNOW_NOT_FOUND: return Error::create(Error::PeerNotFound); - case ESP_ERR_ESPNOW_IF: return Error::create(Error::IncorrectWiFiMode); - case ESP_ERR_ESPNOW_FULL: return Error::create(Error::PeerListIsFull); - case ESP_ERR_ESPNOW_EXIST: return Error::create(Error::PeerAlreadyExists); - default: return Error::create(Error::UnknownError); - } - } + KF_IMPL_INITABLE(EspNow, VoidResult); + VoidResult initImpl() noexcept { + esp_err_t e; - KF_IMPL_INITABLE(EspNow, Result); - Result initImpl() noexcept { - (void) esp_read_mac(_mac_address.data(), ESP_MAC_WIFI_STA); + if (e = esp_now_init(); ESP_OK != e) { goto fail; } + if (e = esp_now_register_recv_cb(onReceive); ESP_OK != e) { goto fail; } - const auto init_result = esp_now_init(); - if (ESP_OK != init_result) { return error(translateEspError(init_result)); } - - const auto handler_result = esp_now_register_recv_cb(onReceive); - if (ESP_OK != handler_result) { return error(translateEspError(handler_result)); } + (void) esp_read_mac(_mac_address.data(), ESP_MAC_WIFI_STA); return ok(); + fail: + return Error::fromEspErr(e); } KF_IMPL_QUITABLE(EspNow); From b78f2732b7fd8c10a8c7ce4901bbb4afb93266b2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 18:32:58 +0300 Subject: [PATCH 162/326] wip(transport/espnow): update up to patched. remove broadcast peer --- .../src/djc/transport/EspNowTransport.hpp | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 6b25f72..ed17274 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -18,7 +18,7 @@ namespace djc::transport { /// @brief ESP‑NOW transport implementation. -/// @note Manages ESP‑NOW peer connections. Uses a broadcast peer for discovery and a dedicated active peer for communication. +/// @note Manages ESP‑NOW peer connections. uses dedicated active peer for communication. struct EspNowTransport : Transport, kf::mixin::Initable { [[nodiscard]] bool send(kf::Slice buffer) noexcept override { @@ -72,36 +72,27 @@ struct EspNowTransport : Transport, kf::mixin::Initable { } private: - kf::Option - _broadcast_peer{},///< Broadcast peer for discovery. - _active_peer{}; ///< Currently connected peer + kf::Option _active_peer{}; static auto addPeer(const kf::network::MacAddress &mac) noexcept -> kf::Option { - auto peer_result = EspNow::Peer::create(mac); - if (peer_result.isError()) { - logger.error( - LogString::formatted( - "Failed to add peer [%s] :%s", - mac.toString().data(), - peer_result.error().toString().data()) - .view()); + auto peer_result = EspNow::Peer::create(EspNow::Peer::Config{ + .mac_address = mac, + .wifi_interface_sta = true, + }); + + if (peer_result.isOk()) { + logger.info(LogString::formatted("Peer '%s' added", mac.toString().data()).view()); + return kf::some(std::move(peer_result.ok())); + } else { + logger.error(LogString::formatted("Failed to add peer [%s] :%s", mac.toString().data(), peer_result.error().toString().data()).view()); return kf::none; } - - logger.info(LogString::formatted("Peer '%s' added", mac.toString().data()).view()); - return kf::some(std::move(peer_result.ok())); } static void delPeer(EspNow::Peer &peer) noexcept { const auto result = peer.del(); if (result.isError()) { - logger.error( - LogString::formatted( - "Failed to delete peer [%s] : %s", - peer.mac().toString().data(), - result.error().toString().data()) - .view()); - return; + logger.error(LogString::formatted("Failed to delete peer [%s] : %s", peer.mac().toString().data(), result.error().toString().data()).view()); } } @@ -125,8 +116,6 @@ struct EspNowTransport : Transport, kf::mixin::Initable { } }); - _broadcast_peer = addPeer(broadcast_mac_address); - logger.debug("init: ok"); return true; } From f63ba8dcc5f5f38456212e5e017261b8dea618cd Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 18:34:06 +0300 Subject: [PATCH 163/326] chore(transport/espnow): remove broadcast mac address declaration --- DJC-Firmware/src/djc/transport/EspNowTransport.hpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index ed17274..a034875 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -35,9 +35,6 @@ struct EspNowTransport : Transport, kf::mixin::Initable { static constexpr auto logger{kf::Logger::create("EspNowTransport")}; - /// @brief MAC address used for ESP‑NOW broadcast. - static constexpr kf::network::MacAddress broadcast_mac_address{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; - protected: /// @brief Establish a connection to a peer. /// @param address The peer's address (must be of kind `EspNow`). From 89f330f64c0bf0ccdd38b0eedc9d9d846b947c99 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 18:46:07 +0300 Subject: [PATCH 164/326] refactor(config): add ui render system config --- DJC-Firmware/src/djc/Config.hpp | 5 ++++- DJC-Firmware/src/main.cpp | 17 ++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 2949a3f..0cc01b7 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -24,13 +24,14 @@ #include "djc/service/AutoConnectService.hpp" #include "djc/service/InputHandler.hpp" #include "djc/service/PeerScanningService.hpp" +#include "djc/ui/UI.hpp" namespace djc { /// @brief Persistent configuration structure stored in NVS. struct Config { static constexpr auto - latest_version{9u}, + latest_version{10u}, max_peer_favorites{8u}; kf::u16 version; @@ -55,6 +56,7 @@ struct Config { service::InputHandler::Config input_handler; service::PeerScanningService::Config peer_scanner; service::AutoConnectService::Config auto_connect_service; + ui::UI::Traits::RenderImpl::Config render_system; [[nodiscard]] bool isLatestVersion() const noexcept { return version == latest_version; @@ -79,6 +81,7 @@ struct Config { .input_handler = service::InputHandler::Config::defaults(), .peer_scanner = service::PeerScanningService::Config::defaults(), .auto_connect_service = service::AutoConnectService::Config::defaults(), + .render_system = ui::UI::Traits::RenderImpl::Config::defaults(), }; } }; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 5eaf54e..d59631e 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -33,6 +33,9 @@ #include "djc/service/InputHandler.hpp" #include "djc/service/PeerScanningService.hpp" +// djc::ui +#include "djc/ui/UI.hpp" + // djc::ui::pages #include "djc/ui/pages/ConfigPage.hpp" #include "djc/ui/pages/MavlinkTelemetryPage.hpp" @@ -40,9 +43,6 @@ #include "djc/ui/pages/RawProtocolPage.hpp" #include "djc/ui/pages/RootPage.hpp" -using UiRender = djc::ui::UI::Traits::RenderImpl; -using UiEvent = djc::ui::UI::Traits::EventImpl; - static constexpr auto logger{kf::Logger::create("main")}; static djc::ConfigManager config_manager{}; @@ -102,12 +102,8 @@ static djc::service::DisplayManager display_manager{ transport_link, }; -static UiRender::Config ui_render_config{ - UiRender::Config::defaults(), -}; - -static UiRender ui_render{ - ui_render_config, +static djc::ui::UI::Traits::RenderImpl ui_render{ + config_manager.config().render_system, }; static djc::ui::UI ui{ @@ -172,6 +168,7 @@ void setup() { } display_manager.init(); + auto &ui_render_config = config_manager.config().render_system; ui_render_config.text.row_max_length = display_manager.colsTotal(); ui_render_config.text.rows_total = display_manager.rowsTotal(); ui_render.callback([](auto str) -> void { @@ -204,6 +201,8 @@ void setup() { }); { + using UiEvent = djc::ui::UI::Traits::EventImpl; + input_handler.onLeftButton([]() { if (virtual_keyboard.active()) { virtual_keyboard.quit(); From 5ce962a0920cb82e7dd7f626e01763e4df8ce2f5 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 12 Jun 2026 18:46:56 +0300 Subject: [PATCH 165/326] chore(main): include cleanup, using auto in lambda arguments --- DJC-Firmware/src/main.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index d59631e..142f1b8 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -7,7 +7,6 @@ // lib #include -#include // djc #include "djc/ConfigManager.hpp" @@ -149,7 +148,7 @@ static djc::ui::pages::ConfigPage config_page{ void setup() { Serial.begin(115200); - kf::Logger::writer = [](kf::memory::StringView str) { Serial.write(str.data(), str.size()); }; + kf::Logger::writer = [](auto str) { Serial.write(str.data(), str.size()); }; config_manager.load(); peer_favoriter_registry.init(); @@ -183,11 +182,11 @@ void setup() { transport_link.transport(transport_registry.get(config_manager.config().init_transport_kind)); - transport_link.onReceive([](const djc::transport::PeerAddress &, kf::Slice buffer) { + transport_link.onReceive([](const auto &, auto buffer) { protocol_link.receive(buffer); }); - protocol_registry.mavlink().callback([](const mavlink_message_t &message) { + protocol_registry.mavlink().callback([](const auto &message) { mavlink_telemetry_registry.update(static_cast(millis()), message); }); @@ -195,7 +194,7 @@ void setup() { peer_scanner.init(); - auto_connect_service.callback([](const djc::transport::PeerAddress &address) -> void { + auto_connect_service.callback([](const auto &address) -> void { logger.info("Auto Connect"); (void) transport_link.connect(address); }); @@ -220,7 +219,7 @@ void setup() { ui.addEvent(UiEvent::widgetClick()); }); - input_handler.onDirection([](djc::service::InputHandler::JoystickListener::Direction direction) { + input_handler.onDirection([](auto direction) { static constexpr UiEvent navigation_event_from_direction[4] = { UiEvent::pageCursorMove(-1),// Up UiEvent::pageCursorMove(+1),// Down From f46d5b9ad9a8507bd20837d92379e768a5b4dc44 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 10:36:52 +0300 Subject: [PATCH 166/326] feat(ui): make virtual keyboard as ui component (no longer singleton) --- .../src/djc/service/DisplayManager.hpp | 32 +++++++------- DJC-Firmware/src/djc/ui/UI.hpp | 30 ++++++++++--- DJC-Firmware/src/djc/ui/UiTraits.hpp | 22 ++++++++++ .../src/djc/{input => ui}/VirtualKeyboard.hpp | 9 ++-- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 4 +- .../src/djc/ui/pages/PeerExplorerPage.hpp | 4 +- .../src/djc/ui/pages/PeerFavoritePage.hpp | 3 +- .../src/djc/ui/pages/RawProtocolPage.hpp | 7 ++-- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 42 ++++++++----------- DJC-Firmware/src/main.cpp | 8 ++-- 10 files changed, 95 insertions(+), 66 deletions(-) create mode 100644 DJC-Firmware/src/djc/ui/UiTraits.hpp rename DJC-Firmware/src/djc/{input => ui}/VirtualKeyboard.hpp (97%) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index a6d8028..4c4d99a 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -14,9 +14,8 @@ #include #include -#include "djc/input/VirtualKeyboard.hpp" #include "djc/transport/TransportLink.hpp" -#include "djc/ui/UI.hpp" +#include "djc/ui/VirtualKeyboard.hpp" #include "djc/service/Service.hpp" @@ -33,8 +32,8 @@ template struct DisplayManager final : using DisplayDriverImpl = I; - explicit DisplayManager(DisplayDriverImpl &display_driver, const transport::TransportLink &transport_link) noexcept : - _display_driver{display_driver}, _transport_link{transport_link} {} + explicit DisplayManager(DisplayDriverImpl &display_driver, const transport::TransportLink &transport_link, const ui::VirtualKeyboard &virtual_keyboard) noexcept : + _display_driver{display_driver}, _transport_link{transport_link}, _virtual_keyboard{virtual_keyboard} {} [[nodiscard]] kf::usize rowsTotal() const noexcept { return _canvas.unwrap().heightInGlyphs() - 1; @@ -56,7 +55,7 @@ template struct DisplayManager final : _canvas.unwrap().fill(); - if (virtual_keyboard.active()) { + if (_virtual_keyboard.active()) { renderVirtualKeyboard(); } else { renderUi(str); @@ -69,10 +68,9 @@ template struct DisplayManager final : using P = typename DisplayDriverImpl::PixelImpl; using Palette = kf::gfx::Palette

; - inline static const auto &virtual_keyboard = input::VirtualKeyboard::instance(); - DisplayDriverImpl &_display_driver; const transport::TransportLink &_transport_link; + const ui::VirtualKeyboard &_virtual_keyboard; bool _show_connection_status_overlay{false}; kf::Option> _canvas{}; @@ -97,31 +95,31 @@ template struct DisplayManager final : void renderVirtualKeyboard() noexcept { auto &canvas = _canvas.unwrap(); - const auto longest_row = input::VirtualKeyboard::rows[0].size(); + const auto longest_row = ui::VirtualKeyboard::rows[0].size(); const auto key_width = canvas.width() / longest_row; const auto key_height = canvas.font().heightTotal(); - const auto keyboard_offset_y = canvas.maxY() - key_height * virtual_keyboard.rowsTotal(); + const auto keyboard_offset_y = canvas.maxY() - key_height * _virtual_keyboard.rowsTotal(); const auto glyph_offset_x = (key_width - canvas.font().widthTotal()) / 2; char c[2]{0, 0}; - canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", virtual_keyboard.available(), virtual_keyboard.text().size()).data()); - canvas.text(0, canvas.font().heightTotal(), virtual_keyboard.text().data()); + canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", _virtual_keyboard.available(), _virtual_keyboard.text().size()).data()); + canvas.text(0, canvas.font().heightTotal(), _virtual_keyboard.text().data()); canvas.background(Palette::bright_black); canvas.foreground(Palette::bright_black); canvas.rect(0, keyboard_offset_y, canvas.maxX(), canvas.maxY(), true); - for (auto row = 0; row < virtual_keyboard.rowsTotal(); row += 1) { + for (auto row = 0; row < _virtual_keyboard.rowsTotal(); row += 1) { const auto y = keyboard_offset_y + row * key_height; - const auto cols = input::VirtualKeyboard::rows[row].size(); + const auto cols = ui::VirtualKeyboard::rows[row].size(); const auto x_offset = ((longest_row - cols) * key_width) / 2; for (auto col = 0; col < cols; col += 1) { const auto x = col * key_width + x_offset; - if (row == virtual_keyboard.cursorRow() and col == virtual_keyboard.cursorCol()) { + if (row == _virtual_keyboard.cursorRow() and col == _virtual_keyboard.cursorCol()) { canvas.foreground(Palette::blue); canvas.rect(x, y, x + key_width, y + key_height - 1, true); @@ -132,9 +130,9 @@ template struct DisplayManager final : canvas.foreground(Palette::black); } - const auto &key = input::VirtualKeyboard::keyAt(row, col); - if (key.kind == input::VirtualKeyboard::Key::Kind::Common) { - c[0] = key.value(virtual_keyboard.shifted()); + const auto &key = ui::VirtualKeyboard::keyAt(row, col); + if (key.kind == ui::VirtualKeyboard::Key::Kind::Common) { + c[0] = key.value(_virtual_keyboard.shifted()); } else { c[0] = '?'; } diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index 6df4c55..6afb3d7 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -3,17 +3,20 @@ #pragma once +#include +#include #include #include -#include #include -#include +#include "djc/ui/UiTraits.hpp" +#include "djc/ui/VirtualKeyboard.hpp" #include "djc/ui/widgets/PeerDisplay.hpp" +#include "djc/ui/widgets/TextInput.hpp" namespace djc::internal { -using UiBase = kf::ui::UI,// Render Engine: Buffered Colored Text UI render engine kf::ui::Event<6> // Event: 6-bit Event value encoding >>; @@ -22,9 +25,11 @@ using UiBase = kf::ui::UI { using widgets::PeerDisplay::PeerDisplay; }; + + struct TextInput : widgets::TextInput { + using widgets::TextInput::TextInput; + }; + + /// @brief Create text input widget with virtual keyboard binding + [[nodiscard]] TextInput createTextInput(kf::Slice source = {}) noexcept { + return TextInput{ + _virtual_keyboard, + source, + }; + } + +private: + VirtualKeyboard &_virtual_keyboard; }; }// namespace djc::ui \ No newline at end of file diff --git a/DJC-Firmware/src/djc/ui/UiTraits.hpp b/DJC-Firmware/src/djc/ui/UiTraits.hpp new file mode 100644 index 0000000..7ee9d2f --- /dev/null +++ b/DJC-Firmware/src/djc/ui/UiTraits.hpp @@ -0,0 +1,22 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace djc::ui { + +struct UiTraitsTag {}; + +/// @brief ESP32-DJC extended UI Traits specializalization +/// @tparam R Render implementation +/// @tparam R Event implementation +template struct UiTraits : + + UiTraitsTag, + kf::ui::UiTraits + +{}; + +}// namespace djc::ui \ No newline at end of file diff --git a/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp similarity index 97% rename from DJC-Firmware/src/djc/input/VirtualKeyboard.hpp rename to DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp index f1a291c..c38f1da 100644 --- a/DJC-Firmware/src/djc/input/VirtualKeyboard.hpp +++ b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp @@ -8,12 +8,11 @@ #include #include #include -#include #include namespace djc::internal { -struct Key { +struct Key : kf::mixin::NonCopyable { enum class Kind : kf::u8 { Common, @@ -37,9 +36,9 @@ struct Key { }// namespace djc::internal -namespace djc::input { +namespace djc::ui { -struct VirtualKeyboard final : kf::mixin::Singleton { +struct VirtualKeyboard final : kf::mixin::NonCopyable { enum class State : kf::u8 { Normal, @@ -220,4 +219,4 @@ struct VirtualKeyboard final : kf::mixin::Singleton { } }; -}// namespace djc::input +}// namespace djc::ui diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 18c4399..cc49f92 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -14,8 +14,6 @@ #include "djc/transport/Kind.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/pages/PeerFavoritePage.hpp" -#include "djc/ui/widgets/PeerDisplay.hpp" -#include "djc/ui/widgets/TextInput.hpp" namespace djc::ui::pages { @@ -143,7 +141,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { .items = {_control_mode_options.data(), _control_mode_options.size()}, }; - widgets::TextInput _device_name_input{}; + UI::TextInput _device_name_input{_ui.createTextInput()}; UI::Button _save_config_button{"Save"}, diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 8b36dbd..6e3f3d8 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -15,7 +15,6 @@ #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/pages/PeerDetailPage.hpp" -#include "djc/ui/widgets/PeerDisplay.hpp" namespace djc::ui::pages { @@ -85,10 +84,9 @@ struct PeerExplorerPage : UI::Page { constexpr auto extreme_age_factor{0.75f}; const auto extreme_age = _peer_scanner.config().entry_max_life_time * extreme_age_factor; const auto age = now - entry.unwrap().last_seen; - + _peer_displays[i].foreground((age < extreme_age) ? UI::Color::Primary : UI::Color::Warning); } - } widgets(layout(available_peers.size())); diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 1b54179..6083b68 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -10,7 +10,6 @@ #include "djc/PeerFavoritesRegistry.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" -#include "djc/ui/widgets/TextInput.hpp" namespace djc::ui::pages { @@ -87,7 +86,7 @@ struct PeerFavoritePage final : UI::Page { }; TrustInput _trust_input{_trust_input_config}; - widgets::TextInput _description_input{}; + UI::TextInput _description_input{_ui.createTextInput()}; UI::Labeled _labeled_trust_input{"Trust", _trust_input}; UI::Labeled _labeled_description_input{"Name", _description_input}; diff --git a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index 43a874c..304a75d 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -4,15 +4,13 @@ #pragma once #include -#include #include +#include #include "djc/protocol/ProtocolLink.hpp" #include "djc/protocol/ProtocolRegistry.hpp" -#include "djc/protocol/RawProtocol.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/ui/UI.hpp" -#include "djc/ui/widgets/TextInput.hpp" namespace djc::ui::pages { @@ -24,6 +22,7 @@ struct RawProtocolPage : UI::Page { protocol::ProtocolLink &protocol_link, transport::TransportLink &transport_link) noexcept : Page{ui, "Raw Protocol"}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, _transport_link{transport_link}, + _message_input{ui.createTextInput({_message.data(), _message.size()})}, _layout{{ &root.link(), &_message_input, @@ -67,7 +66,7 @@ struct RawProtocolPage : UI::Page { // widgets - widgets::TextInput _message_input{{_message.data(), _message.size()}}; + UI::TextInput _message_input; UI::Button _send_button{"Send"}; kf::memory::Array _layout; diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index 03b2037..88418ae 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -6,20 +6,18 @@ #include #include #include +#include +#include -#include "djc/input/VirtualKeyboard.hpp" -#include "djc/ui/UI.hpp" +#include "djc/ui/VirtualKeyboard.hpp" namespace djc::ui::widgets { -struct TextInput final : UI::Widget { +template struct TextInput : kf::ui::widgets::Widget { - TextInput() noexcept : _text_source{} { - initStyle(); - } - - explicit TextInput(kf::Slice source) noexcept : _text_source{source} { - initStyle(); + explicit TextInput(VirtualKeyboard &virtual_keyboard, kf::Slice source) noexcept : + _virtual_keyboard{virtual_keyboard}, _text_source{source} { + this->foreground(kf::ui::Color::Info); } void source(kf::Slice new_source) noexcept { @@ -30,43 +28,39 @@ struct TextInput final : UI::Widget { return nullptr != _text_source.data(); } - void doRender(UI::Traits::RenderImpl &render) const noexcept override { + void doRender(typename U::RenderImpl &render) const noexcept override { render.value(string()); } bool onClick() noexcept override { if (not available()) { return false; } - if (virtual_keyboard.active()) { - virtual_keyboard.click(); + if (_virtual_keyboard.active()) { + _virtual_keyboard.click(); } else { - virtual_keyboard.begin(_text_source); + _virtual_keyboard.begin(_text_source); } return true; } - bool onEventValue(UI::Traits::EventImpl::Value event_value) noexcept { - if (not virtual_keyboard.active()) { + bool onEventValue(typename U::EventImpl::Value event_value) noexcept { + if (not _virtual_keyboard.active()) { return false; } switch (event_value) { - case 0: virtual_keyboard.moveCursorRow(-1); break; - case 1: virtual_keyboard.moveCursorRow(+1); break; - case 2: virtual_keyboard.moveCursorCol(-1); break; - case 3: virtual_keyboard.moveCursorCol(+1); break; + case 0: _virtual_keyboard.moveCursorRow(-1); break; + case 1: _virtual_keyboard.moveCursorRow(+1); break; + case 2: _virtual_keyboard.moveCursorCol(-1); break; + case 3: _virtual_keyboard.moveCursorCol(+1); break; } return true; } private: - void initStyle() { - foreground(UI::Color::Info); - } - - inline static auto &virtual_keyboard{input::VirtualKeyboard::instance()}; + VirtualKeyboard &_virtual_keyboard; kf::Slice _text_source; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 142f1b8..1aacbff 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -14,7 +14,6 @@ #include "djc/MavlinkTelemetryRegistry.hpp" #include "djc/PeerFavoritesRegistry.hpp" #include "djc/Periphery.hpp" -#include "djc/input/VirtualKeyboard.hpp" #include "djc/prelude.hpp" // djc::transport @@ -34,6 +33,7 @@ // djc::ui #include "djc/ui/UI.hpp" +#include "djc/ui/VirtualKeyboard.hpp" // djc::ui::pages #include "djc/ui/pages/ConfigPage.hpp" @@ -46,8 +46,6 @@ static constexpr auto logger{kf::Logger::create("main")}; static djc::ConfigManager config_manager{}; -static auto &virtual_keyboard{djc::input::VirtualKeyboard::instance()}; - static djc::Periphery periphery{ config_manager.config().periphery, }; @@ -72,6 +70,8 @@ static djc::PeerFavoritesRegistry peer_favoriter_registry{ {config_manager.config().peer_favorites.data(), config_manager.config().peer_favorites.size()}, }; +static djc::ui::VirtualKeyboard virtual_keyboard{}; + // services static djc::service::InputHandler input_handler{ @@ -99,6 +99,7 @@ static djc::service::Control control{ static djc::service::DisplayManager display_manager{ periphery.display, transport_link, + virtual_keyboard, }; static djc::ui::UI::Traits::RenderImpl ui_render{ @@ -107,6 +108,7 @@ static djc::ui::UI::Traits::RenderImpl ui_render{ static djc::ui::UI ui{ ui_render, + virtual_keyboard, }; // pages From bc771f3b274a633052a84d88ca23663912c9d384 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 11:39:14 +0300 Subject: [PATCH 167/326] docs(ui traits): add note --- DJC-Firmware/src/djc/ui/UiTraits.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/DJC-Firmware/src/djc/ui/UiTraits.hpp b/DJC-Firmware/src/djc/ui/UiTraits.hpp index 7ee9d2f..ab193b6 100644 --- a/DJC-Firmware/src/djc/ui/UiTraits.hpp +++ b/DJC-Firmware/src/djc/ui/UiTraits.hpp @@ -12,6 +12,7 @@ struct UiTraitsTag {}; /// @brief ESP32-DJC extended UI Traits specializalization /// @tparam R Render implementation /// @tparam R Event implementation +/// @note Any Widget from `djc::widgets` should be noted that `` implements `djc::ui::UiTraits` template struct UiTraits : UiTraitsTag, From 594cdb72614041f8e31b9c8474c8e57350a3c406 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 11:40:16 +0300 Subject: [PATCH 168/326] docs(ui): add briefs --- DJC-Firmware/src/djc/ui/UI.hpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index 6afb3d7..25f9315 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -16,9 +17,9 @@ namespace djc::internal { -using UiBase = kf::ui::UI,// Render Engine: Buffered Colored Text UI render engine - kf::ui::Event<6> // Event: 6-bit Event value encoding +using UiBase = ::kf::ui::UI<::djc::ui::UiTraits< + ::kf::ui::render::ColoredTextRender<256>,// Render Engine: Buffered Colored Text UI render engine + ::kf::ui::Event<6> // Event: 6-bit Event value encoding >>; } @@ -26,19 +27,27 @@ using UiBase = kf::ui::UI { using widgets::PeerDisplay::PeerDisplay; }; + /// @brief Text input area via Virtual Keyboard struct TextInput : widgets::TextInput { using widgets::TextInput::TextInput; }; From 96f97e675c96b8934245179ac5971c8275df9bf4 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 11:41:33 +0300 Subject: [PATCH 169/326] fix(ui/text-input): add double quote mark wrapping --- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index 88418ae..bd6578c 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -29,7 +29,9 @@ template struct TextInput : kf::ui::widgets::Widget { } void doRender(typename U::RenderImpl &render) const noexcept override { + render.value(kf::memory::StringView{"\""}); render.value(string()); + render.value(kf::memory::StringView{"\""}); } bool onClick() noexcept override { From ca8dceb9b5d915ae8583832c919c70c910126e96 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 11:42:08 +0300 Subject: [PATCH 170/326] feat(ui/pages): add color styling for several widgets --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 6 +++++- DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp | 4 ++++ DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 13 ++++++++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index cc49f92..c4109b4 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -45,16 +45,19 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _device_name_input.source({_config_manager.config().device_name.data(), _config_manager.config().device_name.size()}); _save_config_button.callback([this]() { _config_manager.save(); }); + _save_config_button.foreground(UI::Color::Primary); _load_config_button.callback([this]() { _config_manager.load(); this->init(); }); + _load_config_button.foreground(UI::Color::Secondary); _reset_config_button.callback([this]() { _config_manager.reset(); this->init(); }); + _reset_config_button.foreground(UI::Color::Warning); _favorite_peers_fold_toggle_button.callback([this]() { show_favorites = not show_favorites; @@ -85,10 +88,11 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { (void) _label_favorites_buffer.format( "[%c] Peer Favorites (%d/%d)", - ((show_favorites) ? 'V' : '>'), + (show_favorites ? 'V' : '>'), all_favorites.size(), Config::max_peer_favorites); _favorite_peers_fold_toggle_button.label(_label_favorites_buffer.view()); + _favorite_peers_fold_toggle_button.background(show_favorites ? UI::Color::Secondary : UI::Color::Primary); if (show_favorites) { for (auto i = 0u; i < all_favorites.size(); i += 1) { diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index 08f96e9..cec9cea 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -42,6 +42,8 @@ struct PeerDetailPage final : UI::Page { _ui.bindPage(_root); } else { _connection_button.label("Failed to connect"); + _connection_button.foreground(UI::Color::Normal); + _connection_button.background(UI::Color::Error); } }); @@ -60,6 +62,8 @@ struct PeerDetailPage final : UI::Page { void onEntry() noexcept override { _connection_button.label("Connect"); + _connection_button.foreground(UI::Color::Primary); + _connection_button.background(UI::Color::Normal); if (_peer_address.isSome()) { _label_buffer = _peer_address.unwrap().toString(); this->label(_label_buffer.view()); diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 6083b68..2fd4bf2 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -34,12 +34,13 @@ struct PeerFavoritePage final : UI::Page { if (_temp_entry.isNone()) { return; } _temp_entry.unwrap().trust = _trust_input.value(); - const auto result = _peer_favorites_registry.put(_temp_entry.unwrap()); - _confirm_button.label(result ? "Writed" : "Write failed"); + const bool write_ok = _peer_favorites_registry.put(_temp_entry.unwrap()); + _confirm_button.label(write_ok ? "Writed" : "Write failed"); + _confirm_button.background(write_ok ? UI::Color::Success : UI::Color::Error); + _confirm_button.foreground(UI::Color::Normal); update(); }); - _confirm_button.foreground(UI::Color::Primary); _delete_button.callback([this]() -> void { if (_temp_entry.isNone()) { return; } @@ -49,7 +50,7 @@ struct PeerFavoritePage final : UI::Page { _ui.bindPage(_root); update(); }); - _delete_button.background(UI::Color::Error); + _delete_button.background(UI::Color::Warning); } void bindPeer(const transport::PeerAddress &address) noexcept { @@ -63,6 +64,8 @@ struct PeerFavoritePage final : UI::Page { _description_input.source({_temp_entry.unwrap().name.data(), _temp_entry.unwrap().name.size()}); _trust_input.value(_temp_entry.unwrap().trust); _confirm_button.label("Confirm"); + _confirm_button.foreground(UI::Color::Primary); + _confirm_button.background(UI::Color::Normal); widgets(kf::Slice{_layout.data(), _layout.size()}.first(_layout.size() - (entry_option.isSome() ? 0 : 1))); } @@ -81,7 +84,7 @@ struct PeerFavoritePage final : UI::Page { .value_range = PeerFavoritesRegistry::Entry::trust_range, .default_value = PeerFavoritesRegistry::Entry::trust_range.start, .step = static_cast(1), - .placement = kf::ui::Placement::Outside, + .placement = UI::Placement::Outside, .init_show_value = true, }; From d1c12f3ce3e7ade3c07a9c109891970ed3705c1c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 11:57:42 +0300 Subject: [PATCH 171/326] feat: update display manager --- .../src/djc/service/DisplayManager.hpp | 32 ++++++------------- DJC-Firmware/src/main.cpp | 9 ++++-- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 4c4d99a..58318cb 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -31,16 +31,15 @@ template struct DisplayManager final : KF_CHECK_IMPL(I, ::kf::drivers::display::DisplayDriverTag); using DisplayDriverImpl = I; + using Pixel = typename DisplayDriverImpl::PixelImpl; + using Canvas = typename kf::gfx::Canvas; + using Palette = kf::gfx::Palette; explicit DisplayManager(DisplayDriverImpl &display_driver, const transport::TransportLink &transport_link, const ui::VirtualKeyboard &virtual_keyboard) noexcept : _display_driver{display_driver}, _transport_link{transport_link}, _virtual_keyboard{virtual_keyboard} {} - [[nodiscard]] kf::usize rowsTotal() const noexcept { - return _canvas.unwrap().heightInGlyphs() - 1; - } - - [[nodiscard]] kf::usize colsTotal() const noexcept { - return _canvas.unwrap().widthInGlyphs(); + [[nodiscard]] auto canvas() const noexcept -> const kf::Option & { + return _canvas; } void showConnectionStatusOverlay(bool show) noexcept { @@ -65,16 +64,12 @@ template struct DisplayManager final : } private: - using P = typename DisplayDriverImpl::PixelImpl; - using Palette = kf::gfx::Palette

; - DisplayDriverImpl &_display_driver; const transport::TransportLink &_transport_link; const ui::VirtualKeyboard &_virtual_keyboard; + kf::Option _canvas{kf::none}; bool _show_connection_status_overlay{false}; - kf::Option> _canvas{}; - void renderUi(kf::memory::StringView str) noexcept { auto &canvas = _canvas.unwrap(); @@ -101,8 +96,6 @@ template struct DisplayManager final : const auto keyboard_offset_y = canvas.maxY() - key_height * _virtual_keyboard.rowsTotal(); const auto glyph_offset_x = (key_width - canvas.font().widthTotal()) / 2; - char c[2]{0, 0}; - canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", _virtual_keyboard.available(), _virtual_keyboard.text().size()).data()); canvas.text(0, canvas.font().heightTotal(), _virtual_keyboard.text().data()); @@ -131,13 +124,8 @@ template struct DisplayManager final : } const auto &key = ui::VirtualKeyboard::keyAt(row, col); - if (key.kind == ui::VirtualKeyboard::Key::Kind::Common) { - c[0] = key.value(_virtual_keyboard.shifted()); - } else { - c[0] = '?'; - } - canvas.text(x + glyph_offset_x, y, c); + canvas.glyph(x + glyph_offset_x, y, (key.kind == ui::VirtualKeyboard::Key::Kind::Common) ? key.value(_virtual_keyboard.shifted()) : '?'); } } } @@ -147,9 +135,9 @@ template struct DisplayManager final : KF_IMPL_INITABLE(This, void); void initImpl() noexcept { - kf::gfx::Canvas

canvas{ - kf::image::DynamicImage

{_display_driver.image()}, - typename kf::gfx::Canvas

::State{ + Canvas canvas{ + kf::image::DynamicImage{_display_driver.image()}, + typename Canvas::State{ .active_font = kf::someRef(kf::gfx::fonts::gyver_5x7_en), .auto_next_line = true, }, diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 1aacbff..e280edb 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -169,9 +169,12 @@ void setup() { } display_manager.init(); - auto &ui_render_config = config_manager.config().render_system; - ui_render_config.text.row_max_length = display_manager.colsTotal(); - ui_render_config.text.rows_total = display_manager.rowsTotal(); + if (const auto &canvas = display_manager.canvas(); canvas.isSome()) { + auto &ui_render_config = config_manager.config().render_system; + ui_render_config.text.row_max_length = canvas.unwrap().widthInGlyphs(); + ui_render_config.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; + } + ui_render.callback([](auto str) -> void { display_manager.onRender(str); }); From fae5017e1a53579540c5868f5e63167c11a9e1b2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 12:01:00 +0300 Subject: [PATCH 172/326] feat: rename Bus -> SpiBus --- DJC-Firmware/src/djc/Periphery.hpp | 12 ++++++------ DJC-Firmware/src/djc/prelude.hpp | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index 585d604..b5049a6 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -26,8 +26,8 @@ struct PeripheryConfig final : kf::mixin::NonCopyable { AxisInput::FilterImpl::Config axis_filter; Joystick::Config left_joystick, right_joystick; - Bus::Config bus; - Bus::Node::Config bus_node; + SpiBus::Config bus; + SpiBus::Node::Config bus_node; DisplayDriver::Config display; kf::u16 joystick_axes_tune_samples; @@ -50,9 +50,9 @@ struct PeripheryConfig final : kf::mixin::NonCopyable { .y = axisDefaults(true), }, // SPI default pins: MOSI=23, MISO=19, SCK=18 - .bus = djc::Bus::Config::create(), + .bus = djc::SpiBus::Config::create(), // CS, SPI frequency - .bus_node = djc::Bus::Node::Config::create(GPIO_NUM_5, 27000000), + .bus_node = djc::SpiBus::Node::Config::create(GPIO_NUM_5, 27000000), .display = { .init_orientation = kf::drivers::display::Orientation::ClockWise, }, @@ -110,7 +110,7 @@ struct Periphery final : kf::mixin::NonCopyable, kf::mixin::Initableconfig().bus, SPI, }; @@ -154,7 +154,7 @@ struct Periphery final : kf::mixin::NonCopyable, kf::mixin::Initable; using Joystick = kf::drivers::sensors::Joystick; -using Bus = kf::bus::spi::ArduinoSPI; +using SpiBus = kf::bus::spi::ArduinoSPI; -using DisplayDriver = kf::drivers::display::ST7735; +using DisplayDriver = kf::drivers::display::ST7735; }// namespace djc \ No newline at end of file From 1b5de86776969f6ee7c46ac72eaddc5ec4d0097a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 12:09:24 +0300 Subject: [PATCH 173/326] feat(ui/virtual-keyboard/key): add isCommon() -> bool --- DJC-Firmware/src/djc/service/DisplayManager.hpp | 2 +- DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 58318cb..cadebab 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -125,7 +125,7 @@ template struct DisplayManager final : const auto &key = ui::VirtualKeyboard::keyAt(row, col); - canvas.glyph(x + glyph_offset_x, y, (key.kind == ui::VirtualKeyboard::Key::Kind::Common) ? key.value(_virtual_keyboard.shifted()) : '?'); + canvas.glyph(x + glyph_offset_x, y, key.isCommon() ? key.value(_virtual_keyboard.shifted()) : '?'); } } } diff --git a/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp index c38f1da..78ebd5d 100644 --- a/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp +++ b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp @@ -32,6 +32,10 @@ struct Key : kf::mixin::NonCopyable { constexpr char value(bool shifted = false) const noexcept { return shifted ? shift_value : normal_value; } + + constexpr bool isCommon() const noexcept { + return kind == Kind::Common; + } }; }// namespace djc::internal From 96bda541453eb7eb51dac887c78de8424efe2d33 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 12:24:56 +0300 Subject: [PATCH 174/326] wip(kf-patch): add patched kf::ui::UI --- DJC-Firmware/src/kf-patch/UI.hpp | 273 +++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 DJC-Firmware/src/kf-patch/UI.hpp diff --git a/DJC-Firmware/src/kf-patch/UI.hpp b/DJC-Firmware/src/kf-patch/UI.hpp new file mode 100644 index 0000000..8bda5ab --- /dev/null +++ b/DJC-Firmware/src/kf-patch/UI.hpp @@ -0,0 +1,273 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: MIT + +#pragma once + +#include "kf/Option.hpp" +#include "kf/algorithm.hpp" +#include "kf/math/units.hpp" +#include "kf/memory/Queue.hpp" +#include "kf/memory/StringView.hpp" +#include "kf/mixin/Labeled.hpp" +#include "kf/mixin/NonCopyable.hpp" +#include "kf/mixin/TimedPollable.hpp" +#include "kf/primitives.hpp" + +#include "kf/ui/UiTraits.hpp" +#include "kf/ui/render/Render.hpp" +#include "kf/ui/widgets/Button.hpp" +#include "kf/ui/widgets/CheckBox.hpp" +#include "kf/ui/widgets/ComboBox.hpp" +#include "kf/ui/widgets/Display.hpp" +#include "kf/ui/widgets/Labeled.hpp" +#include "kf/ui/widgets/Slider.hpp" +#include "kf/ui/widgets/SpinBox.hpp" +#include "kf/ui/widgets/Widget.hpp" + +namespace kf::ui { + +/// @brief User interface framework with widget-based rendering +/// @tparam U UI Traits implementation +template struct UI : + + mixin::NonCopyable, + mixin::TimedPollable> + +{ + KF_CHECK_IMPL(U, ::kf::ui::UiTraitsTag); + + /// @brief UI Traits implementation + struct Traits : U {}; + + using Widget = widgets::Widget; + + /// @brief Button widget with click handler + struct Button : widgets::Button { + using widgets::Button::Button; + }; + + /// @brief Checkbox with boolean state and change handler + struct CheckBox : widgets::CheckBox { + using widgets::CheckBox::CheckBox; + }; + + /// @brief Combo box for selecting from a fixed set of options + /// @tparam T Value type of options + template struct ComboBox : widgets::ComboBox { + using widgets::ComboBox::ComboBox; + }; + + /// @brief Read-only display of a value (by reference) + /// @tparam T Type of displayed value (must outlive the widget) + template struct Display : widgets::Display { + using widgets::Display::Display; + }; + + /// @brief Wrapper that adds a label to another widget + struct Labeled : widgets::Labeled { + using widgets::Labeled::Labeled; + }; + + /// @brief Spin box for numeric adjustment with configurable step mode + /// @tparam T Arithmetic type (int, float, etc.) + /// @tparam A Adjuster (Arithmetic, ArithmeticPositiveOnly, Geometric) + template struct SpinBox : widgets::SpinBox { + using widgets::SpinBox::SpinBox; + }; + + /// @brief Slider for numeric adjustment with constraints + /// @tparam T Arithmetic type (int, float, etc.) + template struct Slider : widgets::Slider { + using widgets::Slider::Slider; + }; + + struct Page;// forward declaration + + explicit constexpr UI(typename Traits::RenderImpl &render_system) noexcept : _render_system{render_system} {} + + /// @brief Set active page + /// @param page Page to make active (must remain valid) + void activePage(Page &page) noexcept { + if (_active_page.isSome()) { + _active_page.unwrap().onExit(); + } + page.onEntry(); + _active_page = someRef(page); + } + + /// @brief Add event to processing queue + /// @note If the queue is non‑empty and the last event is Update, adding another Update event is a no‑op. + void addEvent(typename Traits::EventImpl event) { + using Kind = typename Traits::EventImpl::Kind; + if (not _events.empty() and Kind::Update == event.kind() and Kind::Update == _events.back().kind()) { return; } + _events.push(event); + } + +private: + /// @brief Special widget for creating page navigation buttons + /// @note Internal use only - use Page::link() for page navigation + struct PageSetter : Widget { + explicit PageSetter(Page &target) noexcept : _target{target} { + this->hint("Navigate to page.."); + } + + /// @brief Set target page as active on click + [[nodiscard]] bool onClick() noexcept override { + _target._ui.activePage(_target); + return true;// redraw always required after page change + } + + void doRender(typename Traits::RenderImpl &render) const noexcept override { + render.arrow(); + render.value(_target.label()); + } + + private: + Page &_target;///< Target page for navigation + }; + +public: + /// @brief UI page containing widgets and label + struct Page : mixin::NonCopyable, mixin::Labeled { + friend struct PageSetter; + + explicit constexpr Page(UI &ui, memory::StringView label) noexcept : mixin::Labeled::Labeled{label}, _ui{ui} {} + + /// @brief Page behavior on entry + virtual void onEntry() noexcept {} + + /// @brief Page behavior on leave + virtual void onExit() noexcept {} + + /// @brief Page behavior on UI polling + virtual void onUpdate(math::Milliseconds now) noexcept {} + + /// @brief Get 'go to this page' Widget + [[nodiscard]] constexpr Widget &link() noexcept { + return _to_this; + } + + /// @brief Get selected widget + [[nodiscard]] constexpr auto selectedWidget() const noexcept -> Option { + return _widgets.empty() ? none : someRef(_widgets[_cursor]); + } + + /// @brief Render page content to display. + /// @param render Renderer instance. + /// @note Handles cursor positioning and widget focus. + void render(typename Traits::RenderImpl &render) noexcept { + render.title(this->label()); + + if (nullptr == _widgets.data()) { return; } + + const usize available = render.widgetsAvailable(); + const usize start = (_widgets.size() > available) ? kf::min(static_cast(_cursor), _widgets.size() - available) : 0; + const usize end = kf::min(start + available, _widgets.size()); + + for (auto i = start; i < end; i += 1) { + auto widget = _widgets[i]; + + if (nullptr == widget) { continue; } + + widget->render(render, i, (i == _cursor)); + } + } + + /// @brief Add event to processing queue. + /// @param event The event to be processed. + [[nodiscard]] bool onEvent(typename Traits::EventImpl event) noexcept { + using Kind = typename Traits::EventImpl::Kind; + switch (event.kind()) { + case Kind::Update: { + return true; + } + case Kind::PageCursorMove: { + return moveCursor(event.value()); + } + case Kind::WidgetClick: { + if (_widgets.size() > 0) { + return _widgets[_cursor]->onClick(); + } + } + case Kind::WidgetValue: { + if (_widgets.size() > 0) { + return _widgets[_cursor]->onEventValue(event.value()); + } + } + } + return false; + } + + /// @brief Get widgets on page + [[nodiscard]] auto widgets() noexcept -> Slice { + return _widgets; + } + + protected: + /// @brief Set widgets on page + void widgets(Slice new_widgets) noexcept { + _widgets = new_widgets; + _cursor = clamp(_cursor, 0, _widgets.size() - 1); + } + + /// @brief Add Update event + void update() noexcept { + _ui.addEvent(Traits::EventImpl::update()); + } + + private: + PageSetter _to_this{*this};///< Navigation widget to this page + UI &_ui; + Slice _widgets{};///< Widgets on this page + isize _cursor{0}; ///< Current widget cursor position (focused widget index) + + /// @brief Move cursor within page bounds + /// @param delta Cursor movement delta (positive/negative) + /// @return true if cursor position changed (redraw required) + [[nodiscard]] bool moveCursor(isize delta) noexcept { + const auto n = _widgets.size(); + if (n > 1) { + _cursor = (_cursor + delta + n) % n; + return true; + } else { + return false; + } + } + }; + +private: + memory::Queue _events{};///< Event queue for pending UI events + typename Traits::RenderImpl &_render_system; ///< Renderer system implementation + Option _active_page{none}; ///< Currently active page for rendering + + using This = UI; + + KF_IMPL_TIMED_POLLABLE(This); + // Process active page update, pending events and render if needed + void pollImpl(math::Milliseconds now) noexcept { + if (_active_page.isNone()) { return; } + + _active_page.unwrap().onUpdate(now); + + if (_events.empty()) { return; } + + constexpr usize max_events_per_poll{20}; + usize events_processed{0}; + + bool render_required{false}; + + while (not _events.empty() and events_processed < max_events_per_poll) { + render_required |= _active_page.unwrap().onEvent(_events.front()); + events_processed += 1; + _events.pop(); + } + + if (render_required) { + _render_system.beginFrame(); + _active_page.unwrap().render(_render_system); + _render_system.endFrame(); + } + } +}; + +}// namespace kf::ui \ No newline at end of file From f81473cd8d1b64454e7a74383c586e4f23ca9fbb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 12:25:53 +0300 Subject: [PATCH 175/326] chore: replace original kf/ui/UI.hpp with patched --- DJC-Firmware/src/djc/ui/UI.hpp | 3 ++- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp | 4 ++-- DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 2 +- DJC-Firmware/src/main.cpp | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index 25f9315..f980cff 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -7,9 +7,10 @@ #include #include #include -#include #include +#include "kf-patch/UI.hpp" // patched version + #include "djc/ui/UiTraits.hpp" #include "djc/ui/VirtualKeyboard.hpp" #include "djc/ui/widgets/PeerDisplay.hpp" diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index c4109b4..85906ce 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -78,7 +78,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _peer_favorite_displays[i].callback([this](const transport::PeerAddress &address) -> void { _peer_favorite_page.bindPeer(address); - _ui.bindPage(_peer_favorite_page); + _ui.activePage(_peer_favorite_page); }); } } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index cec9cea..deabde6 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -39,7 +39,7 @@ struct PeerDetailPage final : UI::Page { if (_peer_address.isNone()) { return; } if (_transport_link.connect(_peer_address.unwrap())) { - _ui.bindPage(_root); + _ui.activePage(_root); } else { _connection_button.label("Failed to connect"); _connection_button.foreground(UI::Color::Normal); @@ -51,7 +51,7 @@ struct PeerDetailPage final : UI::Page { if (_peer_address.isNone()) { return; } this->label("Back"); _peer_favorite_page.bindPeer(_peer_address.unwrap()); - _ui.bindPage(_peer_favorite_page); + _ui.activePage(_peer_favorite_page); update(); }); } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 6e3f3d8..9573033 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -40,7 +40,7 @@ struct PeerExplorerPage : UI::Page { for (auto i = 0u; i < _peer_displays.size(); i += 1) { _peer_displays[i].callback([this](const transport::PeerAddress &address) -> void { _peer_detail_page.bindPeer(address); - _ui.bindPage(_peer_detail_page); + _ui.activePage(_peer_detail_page); }); _layout[i + peer_display_start_index] = &_peer_displays[i]; diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 2fd4bf2..d4c83e5 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -47,7 +47,7 @@ struct PeerFavoritePage final : UI::Page { (void) _peer_favorites_registry.remove(_temp_entry.unwrap().address); - _ui.bindPage(_root); + _ui.activePage(_root); update(); }); _delete_button.background(UI::Color::Warning); diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index e280edb..20b7567 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -251,7 +251,7 @@ void setup() { root_page.attach(raw_protocol_page); root_page.attach(config_page); - ui.bindPage(root_page); + ui.activePage(root_page); ui.addEvent(UiEvent::update()); } From c455847a6d845945718c50be0344fdbbc2b298c3 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 12:47:38 +0300 Subject: [PATCH 176/326] fix(kf-patch/ui/page/selected-widget): add explicit someref template parameter fixes const ref issue --- DJC-Firmware/src/kf-patch/UI.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/kf-patch/UI.hpp b/DJC-Firmware/src/kf-patch/UI.hpp index 8bda5ab..e95a318 100644 --- a/DJC-Firmware/src/kf-patch/UI.hpp +++ b/DJC-Firmware/src/kf-patch/UI.hpp @@ -149,7 +149,7 @@ template struct UI : /// @brief Get selected widget [[nodiscard]] constexpr auto selectedWidget() const noexcept -> Option { - return _widgets.empty() ? none : someRef(_widgets[_cursor]); + return _widgets.empty() ? none : someRef(*_widgets[_cursor]); } /// @brief Render page content to display. From 58f0f3a376d1beb9e2916eb4efe2d59db21bb779 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 12:48:03 +0300 Subject: [PATCH 177/326] feat(kf-patch/ui): add active page getter --- DJC-Firmware/src/kf-patch/UI.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DJC-Firmware/src/kf-patch/UI.hpp b/DJC-Firmware/src/kf-patch/UI.hpp index e95a318..6a0ac3c 100644 --- a/DJC-Firmware/src/kf-patch/UI.hpp +++ b/DJC-Firmware/src/kf-patch/UI.hpp @@ -95,6 +95,11 @@ template struct UI : _active_page = someRef(page); } + /// @brief Get readobly access to active page + auto activePage() const noexcept -> kf::Option { + return _active_page; + } + /// @brief Add event to processing queue /// @note If the queue is non‑empty and the last event is Update, adding another Update event is a no‑op. void addEvent(typename Traits::EventImpl event) { From 2dbd28a6df24502b0aa72d50b7553f3dcf94dc21 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 12:50:04 +0300 Subject: [PATCH 178/326] feat(service/display-manager): replace boolean transport link hard-coded overlay with overlay setter --- .../src/djc/service/DisplayManager.hpp | 19 +++++++------------ DJC-Firmware/src/main.cpp | 12 ++++++++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index cadebab..5774b11 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -14,7 +14,6 @@ #include #include -#include "djc/transport/TransportLink.hpp" #include "djc/ui/VirtualKeyboard.hpp" #include "djc/service/Service.hpp" @@ -35,15 +34,15 @@ template struct DisplayManager final : using Canvas = typename kf::gfx::Canvas; using Palette = kf::gfx::Palette; - explicit DisplayManager(DisplayDriverImpl &display_driver, const transport::TransportLink &transport_link, const ui::VirtualKeyboard &virtual_keyboard) noexcept : - _display_driver{display_driver}, _transport_link{transport_link}, _virtual_keyboard{virtual_keyboard} {} + explicit DisplayManager(DisplayDriverImpl &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : + _display_driver{display_driver}, _virtual_keyboard{virtual_keyboard} {} [[nodiscard]] auto canvas() const noexcept -> const kf::Option & { return _canvas; } - void showConnectionStatusOverlay(bool show) noexcept { - _show_connection_status_overlay = show; + void overlay(kf::memory::StringView new_overlay) noexcept { + _overlay = new_overlay; } void onRender(kf::memory::StringView str) noexcept { @@ -65,21 +64,17 @@ template struct DisplayManager final : private: DisplayDriverImpl &_display_driver; - const transport::TransportLink &_transport_link; const ui::VirtualKeyboard &_virtual_keyboard; kf::Option _canvas{kf::none}; - bool _show_connection_status_overlay{false}; + kf::memory::StringView _overlay{}; void renderUi(kf::memory::StringView str) noexcept { auto &canvas = _canvas.unwrap(); - if (_show_connection_status_overlay) { - const auto y = static_cast(canvas.maxY() - canvas.font().heightTotal()); - const auto overlay = _transport_link.connected() ? _transport_link.activePeerAddress().unwrap().toString().data() : "Disconnected"; - + if (not _overlay.empty()) { canvas.background(Palette::bright_blue); canvas.foreground(Palette::black); - canvas.text(0, y, overlay); + canvas.text(0, static_cast(canvas.maxY() - canvas.font().heightTotal()), _overlay); } canvas.background(Palette::black); diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 20b7567..0870a59 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -98,7 +98,6 @@ static djc::service::Control control{ static djc::service::DisplayManager display_manager{ periphery.display, - transport_link, virtual_keyboard, }; @@ -176,6 +175,16 @@ void setup() { } ui_render.callback([](auto str) -> void { + if (control.enabled()) { + display_manager.overlay(transport_link.connected() ? transport_link.activePeerAddress().unwrap().toString().view() : kf::memory::StringView{"Disconnected"}); + } else { + if (const auto &p = ui.activePage(); p.isSome()) { + if (const auto &widget = p.unwrap().selectedWidget(); widget.isSome()) { + display_manager.overlay(widget.unwrap().hint()); + } + } + } + display_manager.onRender(str); }); @@ -212,7 +221,6 @@ void setup() { virtual_keyboard.quit(); } else { control.enabled(not control.enabled()); - display_manager.showConnectionStatusOverlay(control.enabled()); } ui.addEvent(UiEvent::update()); From de0bee832ea951d7fcd37307ac1c1004607ccdbc Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 12:59:28 +0300 Subject: [PATCH 179/326] fix(kf-patch/ui/page): make _ui protected instead of private --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 5 ++--- DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp | 7 +++---- DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 2 -- DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 5 ++--- DJC-Firmware/src/kf-patch/UI.hpp | 3 ++- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 85906ce..c472686 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -25,10 +25,10 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { djc::ConfigManager &config_manager, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : Page{ui, "Config"}, - _ui{ui}, _config_manager{config_manager}, _peer_favorite_page{ui, *this, _peer_favoriter_registry}, _peer_favoriter_registry{peer_favoriter_registry}, + _device_name_input{ui.createTextInput()}, _layout{{ &root.link(), &_device_name_input, @@ -120,7 +120,6 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // state - UI &_ui; djc::ConfigManager &_config_manager; PeerFavoritesRegistry &_peer_favoriter_registry; kf::memory::StaticString<32> _label_favorites_buffer{}; @@ -145,7 +144,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { .items = {_control_mode_options.data(), _control_mode_options.size()}, }; - UI::TextInput _device_name_input{_ui.createTextInput()}; + UI::TextInput _device_name_input; UI::Button _save_config_button{"Save"}, diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index deabde6..b689742 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -22,7 +22,6 @@ struct PeerDetailPage final : UI::Page { transport::TransportLink &transport_link, PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{ui, {}}, - _ui{ui}, _root{root}, _transport_link{transport_link}, _peer_favorites_registry{peer_favorites_registry}, @@ -30,7 +29,8 @@ struct PeerDetailPage final : UI::Page { &root.link(), &_connection_button, &_peer_favorite_button, - }} + }}, + _peer_favorite_page{ui, *this, peer_favorites_registry} { widgets({_layout.data(), _layout.size()}); @@ -75,7 +75,6 @@ struct PeerDetailPage final : UI::Page { private: // state - UI &_ui; UI::Page &_root; transport::TransportLink &_transport_link; kf::TrivialOption _peer_address{}; @@ -90,7 +89,7 @@ struct PeerDetailPage final : UI::Page { // child pages - PeerFavoritePage _peer_favorite_page{_ui, *this, _peer_favorites_registry}; + PeerFavoritePage _peer_favorite_page; }; }// namespace djc::ui::pages diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 9573033..2c96817 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -27,7 +27,6 @@ struct PeerExplorerPage : UI::Page { service::PeerScanningService &peer_scanner, PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{ui, "Peer Explorer"}, - _ui{ui}, _transport_link{transport_link}, _peer_scanner{peer_scanner}, _peer_favorites_registry{peer_favorites_registry}, @@ -96,7 +95,6 @@ struct PeerExplorerPage : UI::Page { private: static constexpr auto peer_display_start_index{3u}; - UI &_ui; transport::TransportLink &_transport_link; service::PeerScanningService &_peer_scanner; PeerFavoritesRegistry &_peer_favorites_registry; diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index d4c83e5..4bfaeae 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -17,9 +17,9 @@ struct PeerFavoritePage final : UI::Page { explicit PeerFavoritePage(UI &ui, UI::Page &root, PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{ui, {}}, - _ui{ui}, _root{root}, _peer_favorites_registry{peer_favorites_registry}, + _description_input{ui.createTextInput()}, _layout{{ // address and transport shows in title &_labeled_trust_input, @@ -71,7 +71,6 @@ struct PeerFavoritePage final : UI::Page { } private: - UI &_ui; UI::Page &_root; PeerFavoritesRegistry &_peer_favorites_registry; kf::TrivialOption _temp_entry{}; @@ -89,7 +88,7 @@ struct PeerFavoritePage final : UI::Page { }; TrustInput _trust_input{_trust_input_config}; - UI::TextInput _description_input{_ui.createTextInput()}; + UI::TextInput _description_input; UI::Labeled _labeled_trust_input{"Trust", _trust_input}; UI::Labeled _labeled_description_input{"Name", _description_input}; diff --git a/DJC-Firmware/src/kf-patch/UI.hpp b/DJC-Firmware/src/kf-patch/UI.hpp index 6a0ac3c..f42b2b7 100644 --- a/DJC-Firmware/src/kf-patch/UI.hpp +++ b/DJC-Firmware/src/kf-patch/UI.hpp @@ -220,9 +220,10 @@ template struct UI : _ui.addEvent(Traits::EventImpl::update()); } + UI &_ui; + private: PageSetter _to_this{*this};///< Navigation widget to this page - UI &_ui; Slice _widgets{};///< Widgets on this page isize _cursor{0}; ///< Current widget cursor position (focused widget index) From e7b0ff509edb76f255f7fe17c72c635a02211e90 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 13:15:24 +0300 Subject: [PATCH 180/326] feat(servive/display-manager): add overlay color parameter --- DJC-Firmware/src/djc/service/DisplayManager.hpp | 7 +++++-- DJC-Firmware/src/main.cpp | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 5774b11..30c4a2a 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -31,6 +31,7 @@ template struct DisplayManager final : using DisplayDriverImpl = I; using Pixel = typename DisplayDriverImpl::PixelImpl; + using Color = typename Pixel::ColorType; using Canvas = typename kf::gfx::Canvas; using Palette = kf::gfx::Palette; @@ -41,8 +42,9 @@ template struct DisplayManager final : return _canvas; } - void overlay(kf::memory::StringView new_overlay) noexcept { + void overlay(kf::memory::StringView new_overlay, Color color) noexcept { _overlay = new_overlay; + _overlay_color = color; } void onRender(kf::memory::StringView str) noexcept { @@ -67,12 +69,13 @@ template struct DisplayManager final : const ui::VirtualKeyboard &_virtual_keyboard; kf::Option _canvas{kf::none}; kf::memory::StringView _overlay{}; + Color _overlay_color{}; void renderUi(kf::memory::StringView str) noexcept { auto &canvas = _canvas.unwrap(); if (not _overlay.empty()) { - canvas.background(Palette::bright_blue); + canvas.background(_overlay_color); canvas.foreground(Palette::black); canvas.text(0, static_cast(canvas.maxY() - canvas.font().heightTotal()), _overlay); } diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 0870a59..0ad31b1 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -175,12 +175,15 @@ void setup() { } ui_render.callback([](auto str) -> void { + using Palette = decltype(display_manager)::Palette; + if (control.enabled()) { - display_manager.overlay(transport_link.connected() ? transport_link.activePeerAddress().unwrap().toString().view() : kf::memory::StringView{"Disconnected"}); + const auto status = transport_link.connected() ? transport_link.activePeerAddress().unwrap().toString().view() : kf::memory::StringView{"Disconnected"}; + display_manager.overlay(status, Palette::bright_yellow); } else { if (const auto &p = ui.activePage(); p.isSome()) { if (const auto &widget = p.unwrap().selectedWidget(); widget.isSome()) { - display_manager.overlay(widget.unwrap().hint()); + display_manager.overlay(widget.unwrap().hint(), Palette::white); } } } From 4193db43d61732477f4ffa7268574609a4e6827a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 13:53:10 +0300 Subject: [PATCH 181/326] feat(service/display-manager): make overlay y depend on overlay lenght --- DJC-Firmware/src/djc/service/DisplayManager.hpp | 4 +++- DJC-Firmware/src/main.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 30c4a2a..31c7247 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -77,7 +77,9 @@ template struct DisplayManager final : if (not _overlay.empty()) { canvas.background(_overlay_color); canvas.foreground(Palette::black); - canvas.text(0, static_cast(canvas.maxY() - canvas.font().heightTotal()), _overlay); + const auto rows = 1 + (_overlay.size() / canvas.widthInGlyphs()); + const auto y = static_cast(canvas.maxY() - rows * canvas.font().heightTotal()); + canvas.text(0, y, _overlay); } canvas.background(Palette::black); diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 0ad31b1..c7e4625 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -171,7 +171,7 @@ void setup() { if (const auto &canvas = display_manager.canvas(); canvas.isSome()) { auto &ui_render_config = config_manager.config().render_system; ui_render_config.text.row_max_length = canvas.unwrap().widthInGlyphs(); - ui_render_config.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; + ui_render_config.text.rows_total = canvas.unwrap().heightInGlyphs() - 2; // two rows for overlay } ui_render.callback([](auto str) -> void { From 2734265e179c546ece112a13853f85d867072ef0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 13:54:01 +0300 Subject: [PATCH 182/326] feat(ui/pages): add hints where needed --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 26 ++++++++++++++----- .../src/djc/ui/pages/MavlinkTelemetryPage.hpp | 5 +++- .../src/djc/ui/pages/PeerDetailPage.hpp | 1 + .../src/djc/ui/pages/PeerExplorerPage.hpp | 12 ++++++--- .../src/djc/ui/pages/PeerFavoritePage.hpp | 5 ++++ .../src/djc/ui/pages/RawProtocolPage.hpp | 3 +++ 6 files changed, 42 insertions(+), 10 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index c472686..3d79420 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -31,55 +31,69 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _device_name_input{ui.createTextInput()}, _layout{{ &root.link(), + &_save_config_button, + &_load_config_button, + &_reset_config_button, &_device_name_input, &_labeled_autoconnect_enabled_input, &_labeled_default_transport_kind_selector, &_labeled_default_protocol_mode_selector, - &_save_config_button, - &_load_config_button, - &_reset_config_button, &_favorite_peers_fold_toggle_button, }} { widgets(layout(0)); _device_name_input.source({_config_manager.config().device_name.data(), _config_manager.config().device_name.size()}); + _device_name_input.hint("Device name"); _save_config_button.callback([this]() { _config_manager.save(); }); _save_config_button.foreground(UI::Color::Primary); + _save_config_button.hint("Write config from RAM into NVS"); _load_config_button.callback([this]() { _config_manager.load(); this->init(); }); _load_config_button.foreground(UI::Color::Secondary); + _load_config_button.hint("Load config from NVS into RAM"); _reset_config_button.callback([this]() { _config_manager.reset(); this->init(); }); _reset_config_button.foreground(UI::Color::Warning); + _reset_config_button.hint("Set RAM config as detaults"); _favorite_peers_fold_toggle_button.callback([this]() { show_favorites = not show_favorites; this->onEntry(); update(); }); + _favorite_peers_fold_toggle_button.hint("Toggle folding"); + + _default_transport_kind_selector.callback([this](transport::Kind kind) { + _config_manager.config().init_transport_kind = kind; + }); + _labeled_default_transport_kind_selector.hint("Define transport select after init"); _default_protocol_mode_selector.callback([this](Mode mode) { _config_manager.config().init_protocol_mode = mode; }); + _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); _autoconnect_enabled_input.callback([this](bool value) { _config_manager.config().auto_connect_service.enabled = value; }); + _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { - _layout[layout_regular_widgets + i] = &_peer_favorite_displays[i]; + auto &display =_peer_favorite_displays[i]; + _layout[layout_regular_widgets + i] = &display; - _peer_favorite_displays[i].callback([this](const transport::PeerAddress &address) -> void { + display.callback([this](const transport::PeerAddress &address) -> void { _peer_favorite_page.bindPeer(address); _ui.activePage(_peer_favorite_page); }); + display.hint("Open peer config"); } } @@ -149,7 +163,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { UI::Button _save_config_button{"Save"}, _load_config_button{"Load"}, - _reset_config_button{"Reset (RAM cache)"}, + _reset_config_button{"Reset"}, _favorite_peers_fold_toggle_button{{}}; TransportKindSelector _default_transport_kind_selector{_transport_kind_config}; diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index e349691..6dc81a5 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -22,7 +22,7 @@ namespace djc::ui::pages { /// @brief MAVLink telemetry page struct MavlinkTelemetryPage : UI::Page { explicit MavlinkTelemetryPage( - UI& ui, + UI &ui, UI::Page &root, protocol::ProtocolRegistry &protocol_registry, protocol::ProtocolLink &protocol_link, @@ -38,6 +38,9 @@ struct MavlinkTelemetryPage : UI::Page { &_attitude_display, }} { widgets({_layout.data(), _layout.size()}); + + _imu_display.hint("IMU accel vertor"); + _attitude_display.hint("attitude quaternion"); } void onEntry() noexcept override { diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index b689742..ed5ee09 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -54,6 +54,7 @@ struct PeerDetailPage final : UI::Page { _ui.activePage(_peer_favorite_page); update(); }); + _peer_favorite_button.hint("Open peer favorites edit"); } void bindPeer(const transport::PeerAddress &address) noexcept { diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 2c96817..359c1db 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -36,13 +36,17 @@ struct PeerExplorerPage : UI::Page { &_primary_connection_status_button, &_available_label, }} { + _available_label.hint("Available peer will show below"); + for (auto i = 0u; i < _peer_displays.size(); i += 1) { - _peer_displays[i].callback([this](const transport::PeerAddress &address) -> void { + auto &display = _peer_displays[i]; + _layout[i + peer_display_start_index] = &display; + + display.callback([this](const transport::PeerAddress &address) -> void { _peer_detail_page.bindPeer(address); _ui.activePage(_peer_detail_page); }); - - _layout[i + peer_display_start_index] = &_peer_displays[i]; + display.hint("Click for details"); } _primary_connection_status_button.callback([this]() { @@ -65,10 +69,12 @@ struct PeerExplorerPage : UI::Page { _primary_connection_status_button.label(_connection_button_buffer.view()); _primary_connection_status_button.foreground(UI::Color::Normal); _primary_connection_status_button.background(UI::Color::Success); + _primary_connection_status_button.hint("Click to disconnect"); } else { _primary_connection_status_button.label("Disconnected"); _primary_connection_status_button.foreground(UI::Color::Disabled); _primary_connection_status_button.background(UI::Color::Normal); + _primary_connection_status_button.hint("Primary peer not set"); } const auto available_peers = _peer_scanner.peers(); diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 4bfaeae..3c45fe3 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -30,6 +30,9 @@ struct PeerFavoritePage final : UI::Page { }} { + _labeled_trust_input.hint("Set priority for auto connect"); + _labeled_description_input.hint("Will shown as human-readable alias"); + _confirm_button.callback([this]() -> void { if (_temp_entry.isNone()) { return; } _temp_entry.unwrap().trust = _trust_input.value(); @@ -41,6 +44,7 @@ struct PeerFavoritePage final : UI::Page { update(); }); + _confirm_button.hint("Write to registry"); _delete_button.callback([this]() -> void { if (_temp_entry.isNone()) { return; } @@ -50,6 +54,7 @@ struct PeerFavoritePage final : UI::Page { _ui.activePage(_root); update(); }); + _delete_button.hint("Remove from registry"); _delete_button.background(UI::Color::Warning); } diff --git a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index 304a75d..5df5d3d 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -30,6 +30,8 @@ struct RawProtocolPage : UI::Page { }} { widgets({_layout.data(), _layout.size()}); + _message_input.hint("Edit message"); + _send_button.callback([this]() { kf::memory::StringView s{_message.data(), _message.size()}; s = s.sub(0, s.find('\0').unwrapOr(s.size())); @@ -38,6 +40,7 @@ struct RawProtocolPage : UI::Page { (void) _transport_link.send({reinterpret_cast(s.data()), s.size()}); }); + _send_button.hint("Send raw buffer as is"); } void onEntry() noexcept override { From 2fd14c4eb2192a8f95fa03dbc1a2cf74851eff63 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 13 Jun 2026 14:08:23 +0300 Subject: [PATCH 183/326] chore: remove unused includes --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 3 --- DJC-Firmware/src/djc/Periphery.hpp | 13 ++++++++----- DJC-Firmware/src/djc/service/AutoConnectService.hpp | 2 -- DJC-Firmware/src/djc/service/Control.hpp | 2 -- DJC-Firmware/src/djc/service/DisplayManager.hpp | 4 +--- DJC-Firmware/src/kf-patch/EspNow.hpp | 1 - 6 files changed, 9 insertions(+), 16 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index 05dd91e..e8dd871 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -3,12 +3,9 @@ #pragma once -#include - #include #include #include -#include #include #include #include diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index b5049a6..3730e17 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -3,12 +3,9 @@ #pragma once -#include - #include // for delay #include -#include #include #include #include @@ -20,7 +17,7 @@ namespace djc { namespace internal { -struct PeripheryConfig final : kf::mixin::NonCopyable { +struct PeripheryConfig final { ButtonListener::Config button; AxisInput::FilterImpl::Config axis_filter; @@ -75,7 +72,13 @@ struct PeripheryConfig final : kf::mixin::NonCopyable { }// namespace internal /// @brief ESP32-DJC Hardware Periphery -struct Periphery final : kf::mixin::NonCopyable, kf::mixin::Initable, kf::mixin::Configurable { +struct Periphery final : + + kf::mixin::NonCopyable, + kf::mixin::Initable, + kf::mixin::Configurable + +{ using Config = internal::PeripheryConfig; using Configurable::Configurable; diff --git a/DJC-Firmware/src/djc/service/AutoConnectService.hpp b/DJC-Firmware/src/djc/service/AutoConnectService.hpp index 4d37e74..e491b9f 100644 --- a/DJC-Firmware/src/djc/service/AutoConnectService.hpp +++ b/DJC-Firmware/src/djc/service/AutoConnectService.hpp @@ -8,8 +8,6 @@ #include #include #include -#include -#include #include "djc/service/Service.hpp" #include "djc/transport/PeerAddress.hpp" diff --git a/DJC-Firmware/src/djc/service/Control.hpp b/DJC-Firmware/src/djc/service/Control.hpp index c75ce85..ca349ed 100644 --- a/DJC-Firmware/src/djc/service/Control.hpp +++ b/DJC-Firmware/src/djc/service/Control.hpp @@ -4,8 +4,6 @@ #pragma once #include -#include -#include #include "djc/ManualInput.hpp" #include "djc/protocol/ProtocolLink.hpp" diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 31c7247..88f1fd9 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -12,11 +12,9 @@ #include #include #include -#include - -#include "djc/ui/VirtualKeyboard.hpp" #include "djc/service/Service.hpp" +#include "djc/ui/VirtualKeyboard.hpp" namespace djc::service { diff --git a/DJC-Firmware/src/kf-patch/EspNow.hpp b/DJC-Firmware/src/kf-patch/EspNow.hpp index ab409de..464de7d 100644 --- a/DJC-Firmware/src/kf-patch/EspNow.hpp +++ b/DJC-Firmware/src/kf-patch/EspNow.hpp @@ -12,7 +12,6 @@ #include // lib -#include "kf/Option.hpp" #include "kf/Result.hpp" #include "kf/Slice.hpp" #include "kf/io/Writable.hpp" From 2de4aa03f0c063d8ce5044b0848718b0731f4461 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 12:37:34 +0300 Subject: [PATCH 184/326] chore: remove src/kf-patch --- DJC-Firmware/src/kf-patch/EspNow.hpp | 253 ------------------------ DJC-Firmware/src/kf-patch/UI.hpp | 279 --------------------------- 2 files changed, 532 deletions(-) delete mode 100644 DJC-Firmware/src/kf-patch/EspNow.hpp delete mode 100644 DJC-Firmware/src/kf-patch/UI.hpp diff --git a/DJC-Firmware/src/kf-patch/EspNow.hpp b/DJC-Firmware/src/kf-patch/EspNow.hpp deleted file mode 100644 index 464de7d..0000000 --- a/DJC-Firmware/src/kf-patch/EspNow.hpp +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright (c) 2026 KiraFlux -// SPDX-License-Identifier: MIT - -#pragma once - -// std -#include // std::copy -#include // std::move - -// esp -#include -#include - -// lib -#include "kf/Result.hpp" -#include "kf/Slice.hpp" -#include "kf/io/Writable.hpp" -#include "kf/memory/StringView.hpp" -#include "kf/mixin/Callbacked.hpp" -#include "kf/mixin/Initable.hpp" -#include "kf/mixin/MacAddressed.hpp" -#include "kf/mixin/NonCopyable.hpp" -#include "kf/mixin/Quitable.hpp" -#include "kf/mixin/Singleton.hpp" -#include "kf/mixin/StringRepresentable.hpp" -#include "kf/network/MacAddress.hpp" -#include "kf/primitives.hpp" - -namespace kf::internal { - -#define EVAL_KIND(__esp_error__) (__esp_error__ - ESP_ERR_ESPNOW_BASE) - -#define CASE_RETURN(__v) \ - case __v: return #__v - -struct EspNowError final : mixin::StringRepresentable { - - enum Kind : u8 { - UnknownError = EVAL_KIND(ESP_ERR_ESPNOW_BASE), ///< Unknown ESP API error - NotInitialized = EVAL_KIND(ESP_ERR_ESPNOW_NOT_INIT),///< ESP-NOW protocol not initialized - InvalidArg = EVAL_KIND(ESP_ERR_ESPNOW_ARG), ///< Invalid argument passed to API - NoMemory = EVAL_KIND(ESP_ERR_ESPNOW_NO_MEM), ///< Insufficient memory for peer addition - PeerListIsFull = EVAL_KIND(ESP_ERR_ESPNOW_FULL), ///< Peer list is at maximum capacity - PeerNotFound = EVAL_KIND(ESP_ERR_ESPNOW_NOT_FOUND), ///< Peer not found in peer list - InternalError = EVAL_KIND(ESP_ERR_ESPNOW_INTERNAL), ///< ESP-NOW internal API error - PeerAlreadyExists = EVAL_KIND(ESP_ERR_ESPNOW_EXIST),///< Peer already exists in list - IncorrectWiFiMode = EVAL_KIND(ESP_ERR_ESPNOW_IF), ///< Incorrect WiFi interface mode set - TooBigMessage, ///< Message size exceeds ESP_NOW_MAX_DATA_LEN - } kind; - - [[nodiscard]] static constexpr auto create(Kind kind) noexcept -> ErrorWrapper { - return error(EspNowError{.kind = kind}); - } - - [[nodiscard]] static constexpr auto fromEspErr(esp_err_t e) noexcept -> ErrorWrapper { - return create((ESP_ERR_ESPNOW_BASE <= e and e <= ESP_ERR_ESPNOW_IF) ? UnknownError : static_cast(e - ESP_ERR_ESPNOW_BASE)); - } - -private: - KF_IMPL_STRING_REPRESENTABLE(EspNowError, memory::StringView); - memory::StringView toStringImpl() const noexcept { - - switch (kind) { - CASE_RETURN(EspNowError::NotInitialized); - CASE_RETURN(EspNowError::InvalidArg); - CASE_RETURN(EspNowError::NoMemory); - CASE_RETURN(EspNowError::PeerListIsFull); - CASE_RETURN(EspNowError::PeerNotFound); - CASE_RETURN(EspNowError::InternalError); - CASE_RETURN(EspNowError::PeerAlreadyExists); - CASE_RETURN(EspNowError::IncorrectWiFiMode); - CASE_RETURN(EspNowError::TooBigMessage); - default: - CASE_RETURN(EspNowError::UnknownError); - } - } -}; - -#undef EVAL_KIND -#undef CASE_RETURN - -}// namespace kf::internal - -namespace kf::network { - -/// @brief ESP-NOW protocol encapsulataion -/// @note Singleton wrapper for ESP-NOW API with peer management and callbacks -struct EspNow final : - - mixin::Singleton, - mixin::MacAddressed, - mixin::Initable>, - mixin::Quitable, - mixin::Callbacked> - -{ - /// @brief ESP-NOW operation error type - using Error = internal::EspNowError; - - using VoidResult = Result; - - /// @brief ESP‑NOW peer representation with communication capabilities - /// @note Manages a single peer: registration, sending - /// @note Move‑only, non‑copyable - struct Peer final : - - mixin::NonCopyable, - mixin::MacAddressed, - io::Writable - - { - /// @brief Peer configuration - struct Config final { - MacAddress mac_address; - bool wifi_interface_sta;///< (true - STA, false - AP) - }; - - /// @brief Move constructor - /// @param other Source peer (becomes inactive) - Peer(Peer &&other) noexcept : mixin::MacAddressed{std::move(other)}, _owns{other._owns} { - other._owns = false; - } - - /// @brief Destructor - automatically removes the peer if owned - ~Peer() noexcept { - if (_owns) { (void) del(); } - } - - /// @brief Move assignment - /// @param other Source peer (becomes inactive) - /// @return Reference to this - Peer &operator=(Peer &&other) noexcept { - if (this != &other) { - if (_owns) { (void) del(); } - - static_cast(*this) = std::move(other); - _owns = other._owns; - other._owns = false; - } - return *this; - } - - /// @brief Add a new peer to ESP‑NOW network - /// @param config Peer Configuration - /// @return Peer object on success, Error on failure - [[nodiscard]] static auto create(const Config &config) noexcept -> Result { - esp_now_peer_info_t peer_info{ - .channel = 0, - .ifidx = static_cast(config.wifi_interface_sta ? WIFI_IF_STA : WIFI_IF_AP), - .encrypt = false, - }; - std::copy(config.mac_address.begin(), config.mac_address.end(), peer_info.peer_addr); - - const auto e = esp_now_add_peer(&peer_info); - if (ESP_OK == e) { - return ok(std::move(Peer{config.mac_address})); - } else { - return Error::fromEspErr(e); - } - } - - /// @brief Remove peer from ESP-NOW network - [[nodiscard]] VoidResult del() noexcept { - const auto e = esp_now_del_peer(_mac_address.data()); - if (ESP_OK == e) { - return ok(); - } else { - return Error::fromEspErr(e); - } - } - - /// @brief Check if peer exists in ESP-NOW network - [[nodiscard]] bool exist() noexcept { - return esp_now_is_peer_exist(_mac_address.data()); - } - - private: - bool _owns{true}; - - /// @note Called only by Peer::create - explicit Peer(const MacAddress &mac) noexcept : mixin::MacAddressed{mac} {} - - /// @brief Internal send implementation - [[nodiscard]] VoidResult send(const u8 *data, usize len) noexcept { - const auto e = esp_now_send(_mac_address.data(), data, len); - if (ESP_OK == e) { - return ok(); - } else { - return Error::fromEspErr(e); - } - } - - KF_IMPL_WRITABLE(Peer, VoidResult); - - VoidResult writeBufferImpl(Slice buffer) noexcept { - if (buffer.size() > ESP_NOW_MAX_DATA_LEN) { return Error::create(Error::TooBigMessage); } - return send(buffer.data(), buffer.size()); - } - - template VoidResult writePacketImpl(T &&packet) noexcept { - static_assert(sizeof(T) < ESP_NOW_MAX_DATA_LEN, "Message is too big!"); - return send(reinterpret_cast(&packet), sizeof(T)); - } - - template VoidResult writeMixedImpl(T &&header, Slice buffer) noexcept { - const auto mixed_size = sizeof(T) + buffer.size(); - if (mixed_size > ESP_NOW_MAX_DATA_LEN) { return Error::create(Error::TooBigMessage); } - u8 mixed[mixed_size]; - - const auto header_data = reinterpret_cast(&header); - std::copy(header_data, header_data + sizeof(T), mixed); - std::copy(buffer.begin(), buffer.end(), mixed + sizeof(T)); - - return send(mixed, mixed_size); - } - }; - -private: - friend struct ::kf::mixin::Singleton; - - /// @brief Private constructor (singleton) - constexpr EspNow() noexcept : MacAddressed{{}} {} - - /// @brief ESP‑NOW receive callback (static wrapper) - static void onReceive(const u8 *raw_mac_address, const u8 *data, int size) noexcept { - MacAddress source_mac_address; - std::copy(raw_mac_address, raw_mac_address + ESP_NOW_ETH_ALEN, source_mac_address.begin()); - - EspNow::instance().invoke(source_mac_address, Slice{data, static_cast(size)}); - } - - KF_IMPL_INITABLE(EspNow, VoidResult); - VoidResult initImpl() noexcept { - esp_err_t e; - - if (e = esp_now_init(); ESP_OK != e) { goto fail; } - if (e = esp_now_register_recv_cb(onReceive); ESP_OK != e) { goto fail; } - - (void) esp_read_mac(_mac_address.data(), ESP_MAC_WIFI_STA); - - return ok(); - fail: - return Error::fromEspErr(e); - } - - KF_IMPL_QUITABLE(EspNow); - void quitImpl() noexcept { - (void) esp_now_unregister_recv_cb(); - (void) esp_now_deinit(); - } -}; - -}// namespace kf::network \ No newline at end of file diff --git a/DJC-Firmware/src/kf-patch/UI.hpp b/DJC-Firmware/src/kf-patch/UI.hpp deleted file mode 100644 index f42b2b7..0000000 --- a/DJC-Firmware/src/kf-patch/UI.hpp +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) 2026 KiraFlux -// SPDX-License-Identifier: MIT - -#pragma once - -#include "kf/Option.hpp" -#include "kf/algorithm.hpp" -#include "kf/math/units.hpp" -#include "kf/memory/Queue.hpp" -#include "kf/memory/StringView.hpp" -#include "kf/mixin/Labeled.hpp" -#include "kf/mixin/NonCopyable.hpp" -#include "kf/mixin/TimedPollable.hpp" -#include "kf/primitives.hpp" - -#include "kf/ui/UiTraits.hpp" -#include "kf/ui/render/Render.hpp" -#include "kf/ui/widgets/Button.hpp" -#include "kf/ui/widgets/CheckBox.hpp" -#include "kf/ui/widgets/ComboBox.hpp" -#include "kf/ui/widgets/Display.hpp" -#include "kf/ui/widgets/Labeled.hpp" -#include "kf/ui/widgets/Slider.hpp" -#include "kf/ui/widgets/SpinBox.hpp" -#include "kf/ui/widgets/Widget.hpp" - -namespace kf::ui { - -/// @brief User interface framework with widget-based rendering -/// @tparam U UI Traits implementation -template struct UI : - - mixin::NonCopyable, - mixin::TimedPollable> - -{ - KF_CHECK_IMPL(U, ::kf::ui::UiTraitsTag); - - /// @brief UI Traits implementation - struct Traits : U {}; - - using Widget = widgets::Widget; - - /// @brief Button widget with click handler - struct Button : widgets::Button { - using widgets::Button::Button; - }; - - /// @brief Checkbox with boolean state and change handler - struct CheckBox : widgets::CheckBox { - using widgets::CheckBox::CheckBox; - }; - - /// @brief Combo box for selecting from a fixed set of options - /// @tparam T Value type of options - template struct ComboBox : widgets::ComboBox { - using widgets::ComboBox::ComboBox; - }; - - /// @brief Read-only display of a value (by reference) - /// @tparam T Type of displayed value (must outlive the widget) - template struct Display : widgets::Display { - using widgets::Display::Display; - }; - - /// @brief Wrapper that adds a label to another widget - struct Labeled : widgets::Labeled { - using widgets::Labeled::Labeled; - }; - - /// @brief Spin box for numeric adjustment with configurable step mode - /// @tparam T Arithmetic type (int, float, etc.) - /// @tparam A Adjuster (Arithmetic, ArithmeticPositiveOnly, Geometric) - template struct SpinBox : widgets::SpinBox { - using widgets::SpinBox::SpinBox; - }; - - /// @brief Slider for numeric adjustment with constraints - /// @tparam T Arithmetic type (int, float, etc.) - template struct Slider : widgets::Slider { - using widgets::Slider::Slider; - }; - - struct Page;// forward declaration - - explicit constexpr UI(typename Traits::RenderImpl &render_system) noexcept : _render_system{render_system} {} - - /// @brief Set active page - /// @param page Page to make active (must remain valid) - void activePage(Page &page) noexcept { - if (_active_page.isSome()) { - _active_page.unwrap().onExit(); - } - page.onEntry(); - _active_page = someRef(page); - } - - /// @brief Get readobly access to active page - auto activePage() const noexcept -> kf::Option { - return _active_page; - } - - /// @brief Add event to processing queue - /// @note If the queue is non‑empty and the last event is Update, adding another Update event is a no‑op. - void addEvent(typename Traits::EventImpl event) { - using Kind = typename Traits::EventImpl::Kind; - if (not _events.empty() and Kind::Update == event.kind() and Kind::Update == _events.back().kind()) { return; } - _events.push(event); - } - -private: - /// @brief Special widget for creating page navigation buttons - /// @note Internal use only - use Page::link() for page navigation - struct PageSetter : Widget { - explicit PageSetter(Page &target) noexcept : _target{target} { - this->hint("Navigate to page.."); - } - - /// @brief Set target page as active on click - [[nodiscard]] bool onClick() noexcept override { - _target._ui.activePage(_target); - return true;// redraw always required after page change - } - - void doRender(typename Traits::RenderImpl &render) const noexcept override { - render.arrow(); - render.value(_target.label()); - } - - private: - Page &_target;///< Target page for navigation - }; - -public: - /// @brief UI page containing widgets and label - struct Page : mixin::NonCopyable, mixin::Labeled { - friend struct PageSetter; - - explicit constexpr Page(UI &ui, memory::StringView label) noexcept : mixin::Labeled::Labeled{label}, _ui{ui} {} - - /// @brief Page behavior on entry - virtual void onEntry() noexcept {} - - /// @brief Page behavior on leave - virtual void onExit() noexcept {} - - /// @brief Page behavior on UI polling - virtual void onUpdate(math::Milliseconds now) noexcept {} - - /// @brief Get 'go to this page' Widget - [[nodiscard]] constexpr Widget &link() noexcept { - return _to_this; - } - - /// @brief Get selected widget - [[nodiscard]] constexpr auto selectedWidget() const noexcept -> Option { - return _widgets.empty() ? none : someRef(*_widgets[_cursor]); - } - - /// @brief Render page content to display. - /// @param render Renderer instance. - /// @note Handles cursor positioning and widget focus. - void render(typename Traits::RenderImpl &render) noexcept { - render.title(this->label()); - - if (nullptr == _widgets.data()) { return; } - - const usize available = render.widgetsAvailable(); - const usize start = (_widgets.size() > available) ? kf::min(static_cast(_cursor), _widgets.size() - available) : 0; - const usize end = kf::min(start + available, _widgets.size()); - - for (auto i = start; i < end; i += 1) { - auto widget = _widgets[i]; - - if (nullptr == widget) { continue; } - - widget->render(render, i, (i == _cursor)); - } - } - - /// @brief Add event to processing queue. - /// @param event The event to be processed. - [[nodiscard]] bool onEvent(typename Traits::EventImpl event) noexcept { - using Kind = typename Traits::EventImpl::Kind; - switch (event.kind()) { - case Kind::Update: { - return true; - } - case Kind::PageCursorMove: { - return moveCursor(event.value()); - } - case Kind::WidgetClick: { - if (_widgets.size() > 0) { - return _widgets[_cursor]->onClick(); - } - } - case Kind::WidgetValue: { - if (_widgets.size() > 0) { - return _widgets[_cursor]->onEventValue(event.value()); - } - } - } - return false; - } - - /// @brief Get widgets on page - [[nodiscard]] auto widgets() noexcept -> Slice { - return _widgets; - } - - protected: - /// @brief Set widgets on page - void widgets(Slice new_widgets) noexcept { - _widgets = new_widgets; - _cursor = clamp(_cursor, 0, _widgets.size() - 1); - } - - /// @brief Add Update event - void update() noexcept { - _ui.addEvent(Traits::EventImpl::update()); - } - - UI &_ui; - - private: - PageSetter _to_this{*this};///< Navigation widget to this page - Slice _widgets{};///< Widgets on this page - isize _cursor{0}; ///< Current widget cursor position (focused widget index) - - /// @brief Move cursor within page bounds - /// @param delta Cursor movement delta (positive/negative) - /// @return true if cursor position changed (redraw required) - [[nodiscard]] bool moveCursor(isize delta) noexcept { - const auto n = _widgets.size(); - if (n > 1) { - _cursor = (_cursor + delta + n) % n; - return true; - } else { - return false; - } - } - }; - -private: - memory::Queue _events{};///< Event queue for pending UI events - typename Traits::RenderImpl &_render_system; ///< Renderer system implementation - Option _active_page{none}; ///< Currently active page for rendering - - using This = UI; - - KF_IMPL_TIMED_POLLABLE(This); - // Process active page update, pending events and render if needed - void pollImpl(math::Milliseconds now) noexcept { - if (_active_page.isNone()) { return; } - - _active_page.unwrap().onUpdate(now); - - if (_events.empty()) { return; } - - constexpr usize max_events_per_poll{20}; - usize events_processed{0}; - - bool render_required{false}; - - while (not _events.empty() and events_processed < max_events_per_poll) { - render_required |= _active_page.unwrap().onEvent(_events.front()); - events_processed += 1; - _events.pop(); - } - - if (render_required) { - _render_system.beginFrame(); - _active_page.unwrap().render(_render_system); - _render_system.endFrame(); - } - } -}; - -}// namespace kf::ui \ No newline at end of file From b08a2917efa07ca91199b4063b403bfce957dc21 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 12:38:13 +0300 Subject: [PATCH 185/326] wip: use kf lib --- DJC-Firmware/src/djc/transport/EspNowTransport.hpp | 3 +-- DJC-Firmware/src/djc/ui/UI.hpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index a034875..71b1039 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -9,8 +9,7 @@ #include #include #include - -#include "kf-patch/EspNow.hpp"// patched version +#include #include "djc/transport/PeerAddress.hpp" #include "djc/transport/Transport.hpp" diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index f980cff..25f9315 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -7,10 +7,9 @@ #include #include #include +#include #include -#include "kf-patch/UI.hpp" // patched version - #include "djc/ui/UiTraits.hpp" #include "djc/ui/VirtualKeyboard.hpp" #include "djc/ui/widgets/PeerDisplay.hpp" From 19e0a4561425000a5a6a476a9d8ecf51eabb2a91 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:02:09 +0300 Subject: [PATCH 186/326] fix: use new toolkit color names --- DJC-Firmware/src/djc/service/DisplayManager.hpp | 12 ++++++------ DJC-Firmware/src/main.cpp | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 88f1fd9..4791490 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -97,8 +97,8 @@ template struct DisplayManager final : canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", _virtual_keyboard.available(), _virtual_keyboard.text().size()).data()); canvas.text(0, canvas.font().heightTotal(), _virtual_keyboard.text().data()); - canvas.background(Palette::bright_black); - canvas.foreground(Palette::bright_black); + canvas.background(Palette::dark_gray); + canvas.foreground(Palette::dark_gray); canvas.rect(0, keyboard_offset_y, canvas.maxX(), canvas.maxY(), true); for (auto row = 0; row < _virtual_keyboard.rowsTotal(); row += 1) { @@ -111,13 +111,13 @@ template struct DisplayManager final : const auto x = col * key_width + x_offset; if (row == _virtual_keyboard.cursorRow() and col == _virtual_keyboard.cursorCol()) { - canvas.foreground(Palette::blue); + canvas.foreground(Palette::dark_blue); canvas.rect(x, y, x + key_width, y + key_height - 1, true); - canvas.background(Palette::blue); - canvas.foreground(Palette::bright_white); + canvas.background(Palette::dark_blue); + canvas.foreground(Palette::white); } else { - canvas.background(Palette::bright_black); + canvas.background(Palette::dark_gray); canvas.foreground(Palette::black); } diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index c7e4625..464aee9 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -179,11 +179,11 @@ void setup() { if (control.enabled()) { const auto status = transport_link.connected() ? transport_link.activePeerAddress().unwrap().toString().view() : kf::memory::StringView{"Disconnected"}; - display_manager.overlay(status, Palette::bright_yellow); + display_manager.overlay(status, Palette::light_yellow); } else { if (const auto &p = ui.activePage(); p.isSome()) { if (const auto &widget = p.unwrap().selectedWidget(); widget.isSome()) { - display_manager.overlay(widget.unwrap().hint(), Palette::white); + display_manager.overlay(widget.unwrap().hint(), Palette::light_gray); } } } From b5eb1c1647b2463b23d4c3c0664be7402bfe15c6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:03:02 +0300 Subject: [PATCH 187/326] fix(periphery/init): use isError() Display driver init method signature update --- DJC-Firmware/src/djc/Periphery.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index 3730e17..17e313c 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -160,7 +160,7 @@ struct Periphery final : logger.error("SpiBus initialization failed"); } - if (not display.init()) { + if (display.init().isError()) { logger.error("Display driver initialization failed"); } From 50076b93b93f34266e819076c44e81c742a02eed Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:03:51 +0300 Subject: [PATCH 188/326] fix(ui/config-page): use config item type item alias removed --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 3d79420..4016e42 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -141,7 +141,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // widgets - kf::memory::Array _transport_kind_options{{ + kf::memory::Array _transport_kind_options{{ {"EspNow", transport::Kind::EspNow}, }}; @@ -149,7 +149,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { .items = {_transport_kind_options.data(), _transport_kind_options.size()}, }; - kf::memory::Array _control_mode_options{{ + kf::memory::Array _control_mode_options{{ {"Mavlink", Mode::Mavlink}, {"Raw", Mode::Raw}, }}; From 8fafcbf6158c6e81aa666ab1f8ddcd456a164bb7 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:04:53 +0300 Subject: [PATCH 189/326] fix(pages): use onPoll onUpdate renamed --- DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 6dc81a5..182e555 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -48,7 +48,7 @@ struct MavlinkTelemetryPage : UI::Page { _last_imu = _last_attitude = _last_serial_control = 0; } - void onUpdate(kf::math::Milliseconds now) noexcept override { + void onPoll(kf::math::Milliseconds now) noexcept override { bool need_update{false}; if (_mavlink_telemetry_registry.scaled_imu.updatedSince(_last_imu)) { diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 359c1db..e7e971e 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -60,7 +60,7 @@ struct PeerExplorerPage : UI::Page { _redraw_timer.start(0);// enable timer } - void onUpdate(kf::math::Milliseconds now) noexcept override { + void onPoll(kf::math::Milliseconds now) noexcept override { if (not _redraw_timer.expired(now)) { return; } _redraw_timer.start(now); From 160a9719e45bea669056a4731e844f06dc19c130 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:05:52 +0300 Subject: [PATCH 190/326] fix(ui/traits): update template params toolkit's ui traits template parameters changed --- DJC-Firmware/src/djc/ui/UiTraits.hpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/UiTraits.hpp b/DJC-Firmware/src/djc/ui/UiTraits.hpp index ab193b6..88436d4 100644 --- a/DJC-Firmware/src/djc/ui/UiTraits.hpp +++ b/DJC-Firmware/src/djc/ui/UiTraits.hpp @@ -10,13 +10,12 @@ namespace djc::ui { struct UiTraitsTag {}; /// @brief ESP32-DJC extended UI Traits specializalization -/// @tparam R Render implementation -/// @tparam R Event implementation +/// @tparam W Widget base class /// @note Any Widget from `djc::widgets` should be noted that `` implements `djc::ui::UiTraits` -template struct UiTraits : +template struct UiTraits : UiTraitsTag, - kf::ui::UiTraits + kf::ui::UiTraits {}; From 06dfa79043c6b67c9886e0b551c3e23d5d11d9ab Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:07:34 +0300 Subject: [PATCH 191/326] fix(ui/service): pass widget base into ui traits --- DJC-Firmware/src/djc/ui/UI.hpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index 25f9315..f219980 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include "djc/ui/UiTraits.hpp" #include "djc/ui/VirtualKeyboard.hpp" @@ -17,12 +18,14 @@ namespace djc::internal { -using UiBase = ::kf::ui::UI<::djc::ui::UiTraits< +using WidgetBase = ::kf::ui::widgets::Widget< ::kf::ui::render::ColoredTextRender<256>,// Render Engine: Buffered Colored Text UI render engine ::kf::ui::Event<6> // Event: 6-bit Event value encoding - >>; + >; -} +using UiBase = ::kf::ui::UI<::djc::ui::UiTraits>; + +}// namespace djc::internal namespace djc::ui { From 06607be476e7e93f9c30b63b6238f4042c7e8f24 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:08:56 +0300 Subject: [PATCH 192/326] fix(ui/widgets): update peer-display up to new kf lib --- DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index d3dd956..a802b35 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -6,7 +6,8 @@ #include #include #include -#include +#include +#include #include "djc/transport/PeerAddress.hpp" @@ -14,7 +15,7 @@ namespace djc::ui::widgets { template struct PeerDisplay : - kf::ui::widgets::Widget, + U::Widget, kf::mixin::Callbacked { @@ -27,16 +28,19 @@ template struct PeerDisplay : } }; + explicit constexpr PeerDisplay(kf::Option state = kf::none, kf::ui::Style style = kf::ui::Style::defaults()) noexcept : + U::Widget{style}, _state{state} {} + void state(const kf::Option &new_state) noexcept { _state = new_state; } void doRender(typename U::RenderImpl &render) const noexcept override { - render.beginAltBlock(); + render.beginBlock(kf::ui::Block::Alternative); if (_state.isSome()) { render.value(_state.unwrap().displayName()); } - render.endAltBlock(); + render.endBlock(kf::ui::Block::Alternative); } bool onClick() noexcept override { @@ -47,7 +51,7 @@ template struct PeerDisplay : } private: - kf::Option _state{}; + kf::Option _state; }; }// namespace djc::ui::widgets \ No newline at end of file From 28c88f31372136eef71d1aeb74d52f5948c92e99 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:09:30 +0300 Subject: [PATCH 193/326] fix(ui/widgets): text input: update up to kf api --- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index bd6578c..f5df7c3 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -4,19 +4,21 @@ #pragma once #include -#include #include -#include #include +#include #include "djc/ui/VirtualKeyboard.hpp" namespace djc::ui::widgets { -template struct TextInput : kf::ui::widgets::Widget { +template struct TextInput : - explicit TextInput(VirtualKeyboard &virtual_keyboard, kf::Slice source) noexcept : - _virtual_keyboard{virtual_keyboard}, _text_source{source} { + U::Widget + +{ + explicit TextInput(VirtualKeyboard &virtual_keyboard, kf::Slice source, kf::ui::Style style = kf::ui::Style::defaults()) noexcept : + U::Widget{style}, _virtual_keyboard{virtual_keyboard}, _text_source{source} { this->foreground(kf::ui::Color::Info); } @@ -29,9 +31,9 @@ template struct TextInput : kf::ui::widgets::Widget { } void doRender(typename U::RenderImpl &render) const noexcept override { - render.value(kf::memory::StringView{"\""}); + render.value('\"'); render.value(string()); - render.value(kf::memory::StringView{"\""}); + render.value('\"'); } bool onClick() noexcept override { @@ -63,7 +65,6 @@ template struct TextInput : kf::ui::widgets::Widget { private: VirtualKeyboard &_virtual_keyboard; - kf::Slice _text_source; [[nodiscard]] kf::memory::StringView string() const noexcept { From 18189c3086481c8166de415740534d6d69141172 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:09:47 +0300 Subject: [PATCH 194/326] feat(ui/service): add style alias --- DJC-Firmware/src/djc/ui/UI.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index f219980..daf094c 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,9 @@ struct UI : internal::UiBase { /// @brief UI Semantic Color using Color = kf::ui::Color; + /// @brief UI Widget Style + using Style = kf::ui::Style; + /// @brief UI Widget value Placement using Placement = kf::ui::Placement; From 43baf001e217c54ef6c92a38d1c19516b0ecb172 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:31:55 +0300 Subject: [PATCH 195/326] fix(ui): extend buffer --- DJC-Firmware/src/djc/ui/UI.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index daf094c..c57fcb7 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -20,7 +20,7 @@ namespace djc::internal { using WidgetBase = ::kf::ui::widgets::Widget< - ::kf::ui::render::ColoredTextRender<256>,// Render Engine: Buffered Colored Text UI render engine + ::kf::ui::render::ColoredTextRender<512>,// Render Engine: Buffered Colored Text UI render engine ::kf::ui::Event<6> // Event: 6-bit Event value encoding >; From 3d3991c43ba88db9ad327c4d102d5d5a56edab66 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:32:32 +0300 Subject: [PATCH 196/326] feat(ui/pages): update styling --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 44 ++++++++++++++----- .../src/djc/ui/pages/PeerDetailPage.hpp | 11 +++-- .../src/djc/ui/pages/PeerExplorerPage.hpp | 6 +-- .../src/djc/ui/pages/PeerFavoritePage.hpp | 11 ++--- 4 files changed, 49 insertions(+), 23 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 4016e42..390820c 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -46,21 +46,18 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _device_name_input.hint("Device name"); _save_config_button.callback([this]() { _config_manager.save(); }); - _save_config_button.foreground(UI::Color::Primary); _save_config_button.hint("Write config from RAM into NVS"); _load_config_button.callback([this]() { _config_manager.load(); this->init(); }); - _load_config_button.foreground(UI::Color::Secondary); _load_config_button.hint("Load config from NVS into RAM"); _reset_config_button.callback([this]() { _config_manager.reset(); this->init(); }); - _reset_config_button.foreground(UI::Color::Warning); _reset_config_button.hint("Set RAM config as detaults"); _favorite_peers_fold_toggle_button.callback([this]() { @@ -86,7 +83,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { - auto &display =_peer_favorite_displays[i]; + auto &display = _peer_favorite_displays[i]; _layout[layout_regular_widgets + i] = &display; display.callback([this](const transport::PeerAddress &address) -> void { @@ -142,7 +139,13 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // widgets kf::memory::Array _transport_kind_options{{ - {"EspNow", transport::Kind::EspNow}, + { + "EspNow", + transport::Kind::EspNow, + UI::Style{ + .foreground_color = UI::Color::Highlight, + }, + }, }}; TransportKindSelector::Config _transport_kind_config{ @@ -150,8 +153,17 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { }; kf::memory::Array _control_mode_options{{ - {"Mavlink", Mode::Mavlink}, - {"Raw", Mode::Raw}, + { + "Mavlink", + Mode::Mavlink, + UI::Style{ + .foreground_color = UI::Color::Highlight, + }, + }, + { + "Raw", + Mode::Raw, + }, }}; ProtocolModeSelector::Config _control_mode_config{ @@ -161,9 +173,21 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { UI::TextInput _device_name_input; UI::Button - _save_config_button{"Save"}, - _load_config_button{"Load"}, - _reset_config_button{"Reset"}, + _save_config_button{ + "Save", + UI::Style{ + .foreground_color = UI::Color::Primary, + }, + }, + _load_config_button{ + "Load", + }, + _reset_config_button{ + "Reset", + UI::Style{ + .foreground_color = UI::Color::Danger, + }, + }, _favorite_peers_fold_toggle_button{{}}; TransportKindSelector _default_transport_kind_selector{_transport_kind_config}; diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index ed5ee09..55f439e 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -42,8 +42,9 @@ struct PeerDetailPage final : UI::Page { _ui.activePage(_root); } else { _connection_button.label("Failed to connect"); - _connection_button.foreground(UI::Color::Normal); - _connection_button.background(UI::Color::Error); + _connection_button.style(UI::Style{ + .background_color = UI::Color::Error, + }); } }); @@ -63,8 +64,10 @@ struct PeerDetailPage final : UI::Page { void onEntry() noexcept override { _connection_button.label("Connect"); - _connection_button.foreground(UI::Color::Primary); - _connection_button.background(UI::Color::Normal); + _connection_button.style(UI::Style{ + .foreground_color = UI::Color::Primary, + }); + if (_peer_address.isSome()) { _label_buffer = _peer_address.unwrap().toString(); this->label(_label_buffer.view()); diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index e7e971e..5254254 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -67,13 +67,11 @@ struct PeerExplorerPage : UI::Page { if (_transport_link.activePeerAddress().isSome()) { (void) _connection_button_buffer.format("%s", _transport_link.activePeerAddress().unwrap().toString().data()); _primary_connection_status_button.label(_connection_button_buffer.view()); - _primary_connection_status_button.foreground(UI::Color::Normal); - _primary_connection_status_button.background(UI::Color::Success); + _primary_connection_status_button.style({UI::Color::Normal, UI::Color::Success}); _primary_connection_status_button.hint("Click to disconnect"); } else { _primary_connection_status_button.label("Disconnected"); - _primary_connection_status_button.foreground(UI::Color::Disabled); - _primary_connection_status_button.background(UI::Color::Normal); + _primary_connection_status_button.style({UI::Color::Disabled, UI::Color::Normal}); _primary_connection_status_button.hint("Primary peer not set"); } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 3c45fe3..3ecc676 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -39,8 +39,10 @@ struct PeerFavoritePage final : UI::Page { const bool write_ok = _peer_favorites_registry.put(_temp_entry.unwrap()); _confirm_button.label(write_ok ? "Writed" : "Write failed"); - _confirm_button.background(write_ok ? UI::Color::Success : UI::Color::Error); - _confirm_button.foreground(UI::Color::Normal); + _confirm_button.style(UI::Style{ + .foreground_color = UI::Color::Normal, + .background_color = (write_ok ? UI::Color::Success : UI::Color::Error), + }); update(); }); @@ -55,7 +57,7 @@ struct PeerFavoritePage final : UI::Page { update(); }); _delete_button.hint("Remove from registry"); - _delete_button.background(UI::Color::Warning); + _delete_button.background(UI::Color::Danger); } void bindPeer(const transport::PeerAddress &address) noexcept { @@ -69,8 +71,7 @@ struct PeerFavoritePage final : UI::Page { _description_input.source({_temp_entry.unwrap().name.data(), _temp_entry.unwrap().name.size()}); _trust_input.value(_temp_entry.unwrap().trust); _confirm_button.label("Confirm"); - _confirm_button.foreground(UI::Color::Primary); - _confirm_button.background(UI::Color::Normal); + _confirm_button.style({UI::Color::Primary, UI::Color::Normal}); widgets(kf::Slice{_layout.data(), _layout.size()}.first(_layout.size() - (entry_option.isSome() ? 0 : 1))); } From be7cc38c18714d8757902d3fba41130dfa948e43 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 13:53:46 +0300 Subject: [PATCH 197/326] fix(main/render-config): add 1 available row --- DJC-Firmware/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 464aee9..90b77e2 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -171,7 +171,7 @@ void setup() { if (const auto &canvas = display_manager.canvas(); canvas.isSome()) { auto &ui_render_config = config_manager.config().render_system; ui_render_config.text.row_max_length = canvas.unwrap().widthInGlyphs(); - ui_render_config.text.rows_total = canvas.unwrap().heightInGlyphs() - 2; // two rows for overlay + ui_render_config.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; } ui_render.callback([](auto str) -> void { From a7fb74c9e874cb7a8c02941da8bc09d816b9df06 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 15:27:58 +0300 Subject: [PATCH 198/326] fix(main): remove outdated dead code Fixes merge issue --- DJC-Firmware/src/main.cpp | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 633ef11..1070446 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -176,7 +176,7 @@ void setup() { ui_render.callback([](auto str) -> void { using Palette = decltype(display_manager)::Palette; - + if (control.enabled()) { const auto status = transport_link.connected() ? transport_link.activePeerAddress().unwrap().toString().view() : kf::memory::StringView{"Disconnected"}; display_manager.overlay(status, Palette::light_yellow); @@ -269,26 +269,6 @@ void setup() { if (config_manager.modified()) { config_manager.save(); } } -kf::Option getTargetPeer() noexcept { - const auto favorites = peer_favoriter_registry.all(); - const auto peers = peer_scanner.peers(); - - if (favorites.size() * peers.size() == 0) { return {}; } - - // peers.all() -> filter: favorites -> max: (.trust) -> .address - - auto most_trusted_peer_index{0u}; - - for (auto index = 1u; index < peers.size(); index += 1) { - if (not peers[index].hasValue()) { continue; } - - const auto &trusted = peer_favoriter_registry.get(peers[index].value().address); - if (not trusted.hasValue()) { continue; } - } - - return {}; -} - void loop() { constexpr kf::math::Milliseconds loop_period{1000 / 50};// 50 Hz delay(loop_period); From d26fd5a94b71557e3174fd4610735abefe7a3a6c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 18:18:55 +0300 Subject: [PATCH 199/326] wip(main): break down setup & loop into procedures --- DJC-Firmware/src/main.cpp | 232 +++++++++++++++++++++++--------------- 1 file changed, 138 insertions(+), 94 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 1070446..efd428b 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -42,6 +42,8 @@ #include "djc/ui/pages/RawProtocolPage.hpp" #include "djc/ui/pages/RootPage.hpp" +using UiEvent = djc::ui::UI::Traits::EventImpl; + static constexpr auto logger{kf::Logger::create("main")}; static djc::ConfigManager config_manager{}; @@ -147,50 +149,7 @@ static djc::ui::pages::ConfigPage config_page{ peer_favoriter_registry, }; -void setup() { - Serial.begin(115200); - kf::Logger::writer = [](auto str) { Serial.write(str.data(), str.size()); }; - - config_manager.load(); - peer_favoriter_registry.init(); - config_page.init(); - - if (not periphery.init()) { - logger.error("Periphery init failed. Resseting periphery config to defaults"); - config_manager.config().periphery = djc::Periphery::Config::defaults(); - config_manager.modified(true); - } - - if (not config_manager.config().periphery.joystick_axes_tuned) { - logger.debug("Tunning axes.."); - periphery.tune(config_manager.config().periphery); - config_manager.modified(true); - } - - display_manager.init(); - if (const auto &canvas = display_manager.canvas(); canvas.isSome()) { - auto &ui_render_config = config_manager.config().render_system; - ui_render_config.text.row_max_length = canvas.unwrap().widthInGlyphs(); - ui_render_config.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; - } - - ui_render.callback([](auto str) -> void { - using Palette = decltype(display_manager)::Palette; - - if (control.enabled()) { - const auto status = transport_link.connected() ? transport_link.activePeerAddress().unwrap().toString().view() : kf::memory::StringView{"Disconnected"}; - display_manager.overlay(status, Palette::light_yellow); - } else { - if (const auto &p = ui.activePage(); p.isSome()) { - if (const auto &widget = p.unwrap().selectedWidget(); widget.isSome()) { - display_manager.overlay(widget.unwrap().hint(), Palette::light_gray); - } - } - } - - display_manager.onRender(str); - }); - +static void setupTransport() noexcept { WiFi.mode(WIFI_MODE_STA); if (not transport_registry.espnow().init()) { @@ -202,82 +161,134 @@ void setup() { transport_link.onReceive([](const auto &, auto buffer) { protocol_link.receive(buffer); }); +} +static void setupProtocol() noexcept { protocol_registry.mavlink().callback([](const auto &message) { mavlink_telemetry_registry.update(static_cast(millis()), message); }); protocol_link.protocol(protocol_registry.get(config_manager.config().init_protocol_mode)); +} - peer_scanner.init(); +static void setupInput() noexcept { + static constexpr UiEvent navigation_event_from_direction[4] = { + UiEvent::pageCursorMove(-1),// Up + UiEvent::pageCursorMove(+1),// Down + UiEvent::widgetValue(-1), // Left + UiEvent::widgetValue(+1), // Right + }; + + static constexpr UiEvent virtual_keyboard_event_from_direction[4] = { + UiEvent::widgetValue(0),// Up + UiEvent::widgetValue(1),// Down + UiEvent::widgetValue(2),// Left + UiEvent::widgetValue(3),// Right + }; + + input_handler.onLeftButton([]() { + if (virtual_keyboard.active()) { + virtual_keyboard.quit(); + } else { + control.enabled(not control.enabled()); + } - auto_connect_service.callback([](const auto &address) -> void { - logger.info("Auto Connect"); - (void) transport_link.connect(address); + ui.addEvent(UiEvent::update()); }); - { - using UiEvent = djc::ui::UI::Traits::EventImpl; + input_handler.onRightButton([]() { + if (control.enabled()) { return; } - input_handler.onLeftButton([]() { - if (virtual_keyboard.active()) { - virtual_keyboard.quit(); - } else { - control.enabled(not control.enabled()); - } + ui.addEvent(UiEvent::widgetClick()); + }); - ui.addEvent(UiEvent::update()); - }); + input_handler.onDirection([](auto direction) { + if (control.enabled()) { return; } - input_handler.onRightButton([]() { - if (control.enabled()) { return; } + const auto table = virtual_keyboard.active() ? virtual_keyboard_event_from_direction : navigation_event_from_direction; + ui.addEvent(table[static_cast(direction)]); + }); +} - ui.addEvent(UiEvent::widgetClick()); - }); +static void setupUI() noexcept { + config_page.init(); - input_handler.onDirection([](auto direction) { - static constexpr UiEvent navigation_event_from_direction[4] = { - UiEvent::pageCursorMove(-1),// Up - UiEvent::pageCursorMove(+1),// Down - UiEvent::widgetValue(-1), // Left - UiEvent::widgetValue(+1), // Right - }; + // apply page links + root_page.attach(peer_explorer_page); + root_page.attach(mavlink_telemetry_page); + root_page.attach(raw_protocol_page); + root_page.attach(config_page); - static constexpr UiEvent virtual_keyboard_event_from_direction[4] = { - UiEvent::widgetValue(0),// Up - UiEvent::widgetValue(1),// Down - UiEvent::widgetValue(2),// Left - UiEvent::widgetValue(3),// Right - }; + ui_render.callback([](auto str) -> void { + using Palette = decltype(display_manager)::Palette; - if (control.enabled()) { return; } + if (control.enabled()) { + const auto status = transport_link.connected() ? transport_link.activePeerAddress().unwrap().toString().view() : kf::memory::StringView{"Disconnected"}; + display_manager.overlay(status, Palette::light_yellow); + } else { + if (const auto &p = ui.activePage(); p.isSome()) { + if (const auto &widget = p.unwrap().selectedWidget(); widget.isSome()) { + display_manager.overlay(widget.unwrap().hint(), Palette::light_gray); + } + } + } - const auto table = virtual_keyboard.active() ? virtual_keyboard_event_from_direction : navigation_event_from_direction; - ui.addEvent(table[static_cast(direction)]); - }); + display_manager.onRender(str); + }); - // apply page links - root_page.attach(peer_explorer_page); - root_page.attach(mavlink_telemetry_page); - root_page.attach(raw_protocol_page); - root_page.attach(config_page); + ui.activePage(root_page); + ui.addEvent(UiEvent::update()); +} - ui.activePage(root_page); - ui.addEvent(UiEvent::update()); +static void setupGraphics() noexcept { + display_manager.init(); + + if (const auto &canvas = display_manager.canvas(); canvas.isSome()) { + auto &ui_render_config = config_manager.config().render_system; + ui_render_config.text.row_max_length = canvas.unwrap().widthInGlyphs(); + ui_render_config.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; + config_manager.modified(true); + } +} + +static void setupPeriphery() noexcept { + if (not periphery.init()) { + logger.error("Periphery init failed. Resseting periphery config to defaults"); + config_manager.config().periphery = djc::Periphery::Config::defaults(); + config_manager.modified(true); } - if (config_manager.modified()) { config_manager.save(); } + if (not config_manager.config().periphery.joystick_axes_tuned) { + logger.debug("Tunning axes.."); + periphery.tune(config_manager.config().periphery); + config_manager.modified(true); + } } -void loop() { - constexpr kf::math::Milliseconds loop_period{1000 / 50};// 50 Hz - delay(loop_period); +static void setupBase() noexcept { + Serial.begin(115200); + kf::Logger::writer = [](auto str) { Serial.write(str.data(), str.size()); }; - const auto now = static_cast(millis()); - input_handler.poll(now); - transport_link.poll(now); - peer_scanner.poll(now); + config_manager.load(); + peer_favoriter_registry.init(); +} +static void finalizeBase() noexcept { + if (config_manager.modified()) { + config_manager.save(); + } +} + +static void setupServices() noexcept { + peer_scanner.init(); + + auto_connect_service.callback([](const auto &address) -> void { + logger.info("Auto Connect"); + (void) transport_link.connect(address); + }); +} + +static void updateAutoConnect(kf::math::Milliseconds now) noexcept { if (auto_connect_service.config().enabled and auto_connect_service.target().isNone()) { const auto favorites = peer_favoriter_registry.all(); @@ -301,7 +312,9 @@ void loop() { } } auto_connect_service.poll(now); +} +static void updateControl(kf::math::Microseconds now) noexcept { if (control.enabled()) { using I = djc::ManualInput; @@ -317,6 +330,37 @@ void loop() { control.input(control_input); } control.poll(now); +} + +static void pollServices(kf::math::Milliseconds now) noexcept { + input_handler.poll(now); + transport_link.poll(now); + peer_scanner.poll(now); + updateAutoConnect(now); + updateControl(now); ui.poll(now); display_manager.poll(now); +} + +void setup() { + setupBase(); + + setupPeriphery(); + setupInput(); + setupGraphics(); + + setupTransport(); + setupProtocol(); + + setupServices(); + setupUI(); + + finalizeBase(); +} + +void loop() { + constexpr kf::math::Milliseconds loop_period{1000 / 50};// 50 Hz + delay(loop_period); + + pollServices(static_cast(millis())); } \ No newline at end of file From c7a9ca0d7141a8df6258049a855156dfb86c5ae7 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 18:51:01 +0300 Subject: [PATCH 200/326] feat: add 'system' static interface --- DJC-Firmware/src/djc/system/System.hpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/System.hpp diff --git a/DJC-Firmware/src/djc/system/System.hpp b/DJC-Firmware/src/djc/system/System.hpp new file mode 100644 index 0000000..39dc2f8 --- /dev/null +++ b/DJC-Firmware/src/djc/system/System.hpp @@ -0,0 +1,23 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace djc::system { + +struct SystemTag {}; + +/// @brief System static inteface +/// @note System owns a group of related services and components, provides `init()` and `poll(now)`, and is orchestrated from main. +template struct System : + + SystemTag, + kf::mixin::NonCopyable, + kf::mixin::Initable, + kf::mixin::TimedPollable {}; + +}// namespace djc::system \ No newline at end of file From 7475708539a6943e11bc6a44ce81fd418a43817c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 20:05:03 +0300 Subject: [PATCH 201/326] feat(service): add config service will replace config service --- .../src/djc/service/ConfigService.hpp | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 DJC-Firmware/src/djc/service/ConfigService.hpp diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp new file mode 100644 index 0000000..5e8940f --- /dev/null +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -0,0 +1,105 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include "djc/Config.hpp" +#include "djc/service/Service.hpp" + +namespace djc::service { + +/// @brief Config service with delayed NVS operations +/// @note Requests are batched and executed on a 5-second timer from the main loop. +struct ConfigService : Service { + + /// @brief Get readonly reference to the current configuration + [[nodiscard]] constexpr const Config &config() const noexcept { + return _storage.config; + } + + /// @brief Get mutable reference to the current configuration + [[nodiscard]] Config &config() noexcept { + return _storage.config; + } + + /// @brief Requests an deferred save of the current config to NVS + void requestSave() noexcept { + _save_requested = true; + logger.debug("save requested"); + } + + /// @brief Requests an deferred load of the config from NVS + void requestLoad() noexcept { + _load_requested = true; + logger.debug("load requested"); + } + + /// @brief Requests an deferred reset of the config to defaults + void requestReset() noexcept { + _reset_requested = true; + logger.debug("reset requested"); + } + +private: + static constexpr auto logger{kf::Logger::create("ConfigService")}; + + static constexpr kf::math::Timer::Config sync_timer_config{ + .period = 5'000, + }; + + kf::memory::Storage _storage{ + .key = "DC", + .config = djc::Config::defaults(), + }; + + kf::math::Timer _sync_timer{sync_timer_config}; + + bool _save_requested{false}, _load_requested{false}, _reset_requested{false}; + + KF_IMPL_TIMED_POLLABLE(ConfigService); + void pollImpl(kf::math::Milliseconds now) noexcept { + if (not _sync_timer.expired(now)) { return; } + _sync_timer.start(now); + + if (_load_requested) { + _load_requested = false; + + logger.info("Loading config from NVS"); + + if (not _storage.load()) { + logger.error("Failed to load config"); + requestReset(); + requestSave(); + } + + if (not _storage.config.isLatestVersion()) { + logger.error("Config version is outdated"); + requestReset(); + requestSave(); + } + } + + if (_reset_requested) { + _reset_requested = false; + + logger.info("Resetting RAM config cache to defaults"); + _storage.config = djc::Config::defaults(); + } + + if (_save_requested) { + _save_requested = false; + + logger.info("Saving config to NVS"); + + if (not _storage.save()) { + logger.error("Failed to save config into NVS"); + } + } + } +}; + +}// namespace djc::service \ No newline at end of file From 26bf66725a8f80f6b7aca3c0daa29cf0fc5efb3d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 20:10:16 +0300 Subject: [PATCH 202/326] feat(system): add config system --- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/ConfigSystem.hpp diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp new file mode 100644 index 0000000..5fcc5db --- /dev/null +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -0,0 +1,30 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "djc/service/ConfigService.hpp" +#include "djc/system/System.hpp" + +namespace djc::system { + +/// @brief System owning the config service, handling deferred NVS operations +/// @note Wraps ConfigService, requests load on init, and polls it periodically +struct ConfigSystem : System { +private: + service::ConfigService _config_service{}; + + KF_IMPL_INITABLE(ConfigSystem, bool); + bool initImpl() noexcept { + _config_service.requestLoad(); + + return true; + } + + KF_IMPL_TIMED_POLLABLE(ConfigSystem); + void poll(kf::math::Milliseconds now) noexcept { + _config_service.poll(now); + } +}; + +}// namespace djc::system \ No newline at end of file From de65fd9f1f69b22a0b6cd0980d995f8bdf7268a5 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 20:49:20 +0300 Subject: [PATCH 203/326] fix(system/config): add config service getter --- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index 5fcc5db..cc702a8 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -11,6 +11,17 @@ namespace djc::system { /// @brief System owning the config service, handling deferred NVS operations /// @note Wraps ConfigService, requests load on init, and polls it periodically struct ConfigSystem : System { + + /// @brief Get mutable access to config service component + service::ConfigService &configService() noexcept { + return _config_service; + } + + /// @brief Get readonly access to config service component + constexpr const service::ConfigService &configService() const noexcept { + return _config_service; + } + private: service::ConfigService _config_service{}; From 9457b7901b1d1146133d2212f49934a37e392456 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 20:51:13 +0300 Subject: [PATCH 204/326] feat(system): add periphery system --- .../src/djc/system/PeripherySystem.hpp | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/PeripherySystem.hpp diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp new file mode 100644 index 0000000..c07f1fa --- /dev/null +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -0,0 +1,60 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "djc/Periphery.hpp" +#include "djc/service/ConfigService.hpp" +#include "djc/system/System.hpp" + +namespace djc::system { + +/// @brief System that owns and initializes hardware peripherals +/// @note Provides access to peripherals for other systems via getters; no periodic polling needed. +struct PeripherySystem : System { + + explicit PeripherySystem(service::ConfigService &config_service) noexcept : + _config_service{config_service} {} + + /// @brief Get mutable access to periphery component + Periphery &periphery() noexcept { + return _periphery; + } + + /// @brief Get readonly access to periphery component + constexpr const Periphery &periphery() const noexcept { + return _periphery; + } + +private: + static constexpr auto logger{kf::Logger::create("PeripherySystem")}; + + service::ConfigService &_config_service; + Periphery _periphery{_config_service.config().periphery}; + + KF_IMPL_INITABLE(PeripherySystem, bool); + bool initImpl() noexcept { + if (not _periphery.init()) { + logger.error("Periphery init failed. Resseting periphery config to defaults"); + _config_service.config().periphery = djc::Periphery::Config::defaults(); + _config_service.requestSave(); + + return false; + } + + if (not _config_service.config().periphery.joystick_axes_tuned) { + logger.debug("Tunning axes.."); + _periphery.tune(_config_service.config().periphery); + _config_service.requestSave(); + } + + return true; + } + + KF_IMPL_TIMED_POLLABLE(PeripherySystem); + void poll(kf::math::Milliseconds now) noexcept {} +}; + +}// namespace djc::system \ No newline at end of file From d4bd19b8af9a9ae5946298b83cb15fba3c3398fa Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 21:28:25 +0300 Subject: [PATCH 205/326] refactor(system/config): rename config service getter --- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index cc702a8..9ef2e2e 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -12,13 +12,13 @@ namespace djc::system { /// @note Wraps ConfigService, requests load on init, and polls it periodically struct ConfigSystem : System { - /// @brief Get mutable access to config service component - service::ConfigService &configService() noexcept { + /// @brief Get mutable access to config service + service::ConfigService &service() noexcept { return _config_service; } - /// @brief Get readonly access to config service component - constexpr const service::ConfigService &configService() const noexcept { + /// @brief Get readonly access to config service + constexpr const service::ConfigService &service() const noexcept { return _config_service; } From aadf5af521f6ed350f9fb1a8215d7b3995d81067 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 22:32:40 +0300 Subject: [PATCH 206/326] feat(system): add transport system --- .../src/djc/system/TransportSystem.hpp | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/TransportSystem.hpp diff --git a/DJC-Firmware/src/djc/system/TransportSystem.hpp b/DJC-Firmware/src/djc/system/TransportSystem.hpp new file mode 100644 index 0000000..9acd94f --- /dev/null +++ b/DJC-Firmware/src/djc/system/TransportSystem.hpp @@ -0,0 +1,69 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include + +#include "djc/Config.hpp" +#include "djc/system/System.hpp" +#include "djc/transport/TransportLink.hpp" +#include "djc/transport/TransportRegistry.hpp" + +namespace djc::system { + +/// @brief System managing transport, Wraps TransportRegistry and TransportLink +/// @note Initializes WiFi STA mode, and polls the link for connection timeouts. +struct TransportSystem : System, kf::mixin::Configurable { + + using kf::mixin::Configurable::Configurable; + + /// @brief Get mutable access to transport link component + transport::TransportLink &link() noexcept { + return _transport_link; + } + + /// @brief Get readonly access to transport link component + constexpr const transport::TransportLink &link() const noexcept { + return _transport_link; + } + + /// @brief Get mutable access to transport registry component + transport::TransportRegistry ®istry() noexcept { + return _transport_registry; + } + + /// @brief Get readonly access to transport registry component + constexpr const transport::TransportRegistry ®istry() const noexcept { + return _transport_registry; + } + +private: + static constexpr auto logger{kf::Logger::create("TransportSystem")}; + + transport::TransportRegistry _transport_registry{}; + transport::TransportLink _transport_link{this->config().transport_link}; + + KF_IMPL_INITABLE(TransportSystem, bool); + bool initImpl() noexcept { + WiFi.mode(WIFI_MODE_STA); + + if (not _transport_registry.espnow().init()) { + logger.error("failed to initialize espnow transport"); + } + + _transport_link.transport(_transport_registry.get(this->config().init_transport_kind)); + + return true; + } + + KF_IMPL_TIMED_POLLABLE(TransportSystem); + void poll(kf::math::Milliseconds now) noexcept { + _transport_link.poll(now); + } +}; + +}// namespace djc::system \ No newline at end of file From 3b7db90506e43176513f2fbf7018f4460dcb7b26 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 22:51:40 +0300 Subject: [PATCH 207/326] feat(system): add protocol system --- .../src/djc/system/ProtocolSystem.hpp | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/ProtocolSystem.hpp diff --git a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp new file mode 100644 index 0000000..6dd8481 --- /dev/null +++ b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp @@ -0,0 +1,82 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "djc/Config.hpp" +#include "djc/MavlinkTelemetryRegistry.hpp" +#include "djc/protocol/ProtocolLink.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" +#include "djc/system/System.hpp" + +namespace djc::system { + +/// @brief System managing communication protocols and telemetry registry. +/// @note Owns ProtocolRegistry, ProtocolLink, and MavlinkTelemetryRegistry. +/// @note Protocol polling is handled externally. +/// @note MAVLink callback is configured in init(). +struct ProtocolSystem : System, kf::mixin::Configurable { + + using kf::mixin::Configurable::Configurable; + + /// @brief Get mutable access to protocol link component + protocol::ProtocolLink &link() noexcept { + return _protocol_link; + } + + /// @brief Get readonly access to protocol link component + constexpr const protocol::ProtocolLink &link() const noexcept { + return _protocol_link; + } + + /// @brief Get mutable access to protocol registry component + protocol::ProtocolRegistry &protocolRegistry() noexcept { + return _protocol_registry; + } + + /// @brief Get readonly access to protocol registry component + constexpr const protocol::ProtocolRegistry &protocolRegistry() const noexcept { + return _protocol_registry; + } + + /// @brief Get mutable access to MAVLink telemetry registry component + MavlinkTelemetryRegistry &mavlinkTelemetryRegistry() noexcept { + return _mavlink_telemetry_registry; + } + + /// @brief Get readonly access to MAVLink telemetry registry component + constexpr const MavlinkTelemetryRegistry &mavlinkTelemetryRegistry() const noexcept { + return _mavlink_telemetry_registry; + } + +private: + static constexpr auto logger{kf::Logger::create("ProtocolSystem")}; + + MavlinkTelemetryRegistry _mavlink_telemetry_registry{}; + protocol::ProtocolRegistry _protocol_registry{this->config().protocol_registry}; + protocol::ProtocolLink _protocol_link{this->config().protocol_link}; + kf::math::Milliseconds _poll_time{}; + + KF_IMPL_INITABLE(ProtocolSystem, bool); + bool initImpl() noexcept { + _protocol_registry.mavlink().callback([this](const auto &message) { + _mavlink_telemetry_registry.update(static_cast(_poll_time), message); + }); + + _protocol_link.protocol(_protocol_registry.get(this->config().init_protocol_mode)); + + return true; + } + + KF_IMPL_TIMED_POLLABLE(ProtocolSystem); + void poll(kf::math::Milliseconds now) noexcept { + _poll_time = now; + + // protocol link polling in control component + } +}; + +}// namespace djc::system \ No newline at end of file From 263e4c9e9735ae1fd27583edf53eff15c6ceedcd Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 23:17:46 +0300 Subject: [PATCH 208/326] feat(system): add peer system --- DJC-Firmware/src/djc/system/PeerSystem.hpp | 115 +++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/PeerSystem.hpp diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp new file mode 100644 index 0000000..f3e69bf --- /dev/null +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -0,0 +1,115 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include + +#include "djc/Config.hpp" +#include "djc/PeerFavoritesRegistry.hpp" +#include "djc/service/AutoConnectService.hpp" +#include "djc/service/PeerScanningService.hpp" +#include "djc/system/System.hpp" +#include "djc/transport/TransportLink.hpp" + +namespace djc::system { + +/// @brief System managing peer favorites, scanning and auto-connection. +/// @note Owns PeerFavoritesRegistry, PeerScanningService, AutoConnectService. +/// Depends on Config (settings) and TransportLink (for scanning and connection). +/// On each poll, scans visible peers and triggers auto-connection to the most trusted visible favorite. +struct PeerSystem : System, kf::mixin::Configurable { + + explicit PeerSystem(Config &config, transport::TransportLink &transport_link) noexcept : + kf::mixin::Configurable{config}, + _transport_link{transport_link}, + _peer_favorites_registry{{config.peer_favorites.data(), config.peer_favorites.size()}}, + _peer_scanning_service{config.peer_scanner, transport_link}, + _auto_connect_service{config.auto_connect_service, transport_link} {} + + /// @brief Get mutable access to peer favorites registry component + PeerFavoritesRegistry &favoritesRegistry() noexcept { + return _peer_favorites_registry; + } + + /// @brief Get readonly access to peer favorites registry component + constexpr const PeerFavoritesRegistry &favoritesRegistry() const noexcept { + return _peer_favorites_registry; + } + + /// @brief Get mutable access to peer scanning service component + service::PeerScanningService &scanningService() noexcept { + return _peer_scanning_service; + } + + /// @brief Get readonly access to peer scanning service component + constexpr const service::PeerScanningService &scanningService() const noexcept { + return _peer_scanning_service; + } + + /// @brief Get mutable access to auto‑connect service component + service::AutoConnectService &autoConnectService() noexcept { + return _auto_connect_service; + } + + /// @brief Get readonly access to auto‑connect service component + constexpr const service::AutoConnectService &autoConnectService() const noexcept { + return _auto_connect_service; + } + +private: + static constexpr auto logger{kf::Logger::create("PeerSystem")}; + + transport::TransportLink &_transport_link; + PeerFavoritesRegistry _peer_favorites_registry; + service::PeerScanningService _peer_scanning_service; + service::AutoConnectService _auto_connect_service; + + KF_IMPL_INITABLE(PeerSystem, bool); + bool initImpl() noexcept { + _peer_favorites_registry.init(); + _peer_scanning_service.init(); + + _auto_connect_service.callback([this](const auto &address) -> void { + logger.info("Auto Connect"); + (void) _transport_link.connect(address); + }); + + return true; + } + + KF_IMPL_TIMED_POLLABLE(PeerSystem); + void poll(kf::math::Milliseconds now) noexcept { + _peer_scanning_service.poll(now); + + if (_auto_connect_service.config().enabled and _auto_connect_service.target().isNone()) { + const auto favorites = _peer_favorites_registry.all(); + + if (favorites.size() > 0) { + auto most_trusted_favorite_index = 0u; + + for (auto index = 1u; index < favorites.size(); index += 1) { + if (favorites[index].isSome() and favorites[most_trusted_favorite_index].isSome() and favorites[index].unwrap().trust > favorites[most_trusted_favorite_index].unwrap().trust) { + most_trusted_favorite_index = index; + } + } + + if (const auto &most_trusted = favorites[most_trusted_favorite_index]; most_trusted.isSome()) { + for (const auto &peer: _peer_scanning_service.peers()) { + if (peer.isSome() and peer.unwrap().address == most_trusted.unwrap().address) { + _auto_connect_service.target(most_trusted.unwrap().address); + break; + } + } + } + } + } + + _auto_connect_service.poll(now); + } +}; + +}// namespace djc::system \ No newline at end of file From ac0591f8bf49d97cc8d55ed957c362c2e6e23b0a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 23:23:55 +0300 Subject: [PATCH 209/326] feat(system): add input system --- DJC-Firmware/src/djc/system/InputSystem.hpp | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/InputSystem.hpp diff --git a/DJC-Firmware/src/djc/system/InputSystem.hpp b/DJC-Firmware/src/djc/system/InputSystem.hpp new file mode 100644 index 0000000..0fad883 --- /dev/null +++ b/DJC-Firmware/src/djc/system/InputSystem.hpp @@ -0,0 +1,53 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "djc/Config.hpp" +#include "djc/Periphery.hpp" +#include "djc/service/InputHandler.hpp" +#include "djc/system/System.hpp" + +namespace djc::system { + +/// @brief System managing user input +/// @note Owns InputHandler service. Depends on Config (read‑only) and Periphery. +struct InputSystem : System, kf::mixin::Configurable { + + explicit InputSystem(const Config &config, Periphery &periphery) noexcept : + kf::mixin::Configurable{config}, + _input_handler{ + config.input_handler, + periphery.right_joystick,// primary joystick for navigation/control + periphery.left_button_listener, + periphery.right_button_listener, + } {} + + /// @brief Get mutable access to the input handler service (for callback setup). + service::InputHandler &inputHandler() noexcept { + return _input_handler; + } + + /// @brief Get readonly access to the input handler service. + constexpr const service::InputHandler &inputHandler() const noexcept { + return _input_handler; + } + +private: + service::InputHandler _input_handler; + + KF_IMPL_INITABLE(InputSystem, bool); + bool initImpl() noexcept { + // For now, nothing to do. + return true; + } + + KF_IMPL_TIMED_POLLABLE(InputSystem); + void pollImpl(kf::math::Milliseconds now) noexcept { + _input_handler.poll(now); + } +}; + +}// namespace djc::system \ No newline at end of file From d063eb98af636b35c64ab92026f0fc7e9c56ef87 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 23:45:58 +0300 Subject: [PATCH 210/326] refactor(system): remove ConfigService dependency from PeripherySystem --- .../src/djc/system/PeripherySystem.hpp | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp index c07f1fa..bec1a1c 100644 --- a/DJC-Firmware/src/djc/system/PeripherySystem.hpp +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -4,19 +4,19 @@ #pragma once #include +#include +#include "djc/Config.hpp" #include "djc/Periphery.hpp" -#include "djc/service/ConfigService.hpp" #include "djc/system/System.hpp" namespace djc::system { /// @brief System that owns and initializes hardware peripherals /// @note Provides access to peripherals for other systems via getters; no periodic polling needed. -struct PeripherySystem : System { +struct PeripherySystem : System, kf::mixin::Configurable { - explicit PeripherySystem(service::ConfigService &config_service) noexcept : - _config_service{config_service} {} + using kf::mixin::Configurable::Configurable; /// @brief Get mutable access to periphery component Periphery &periphery() noexcept { @@ -28,29 +28,29 @@ struct PeripherySystem : System { return _periphery; } + /// @brief Tune periphery + void tune(Periphery::Config &mutable_periphery_config) noexcept { + if (mutable_periphery_config.joystick_axes_tuned) { + logger.debug("Nothing to do: axes already tuned"); + } else { + logger.debug("Tunning axes.."); + _periphery.tune(mutable_periphery_config); + } + } + private: static constexpr auto logger{kf::Logger::create("PeripherySystem")}; - service::ConfigService &_config_service; - Periphery _periphery{_config_service.config().periphery}; + Periphery _periphery{this->config().periphery}; KF_IMPL_INITABLE(PeripherySystem, bool); bool initImpl() noexcept { - if (not _periphery.init()) { - logger.error("Periphery init failed. Resseting periphery config to defaults"); - _config_service.config().periphery = djc::Periphery::Config::defaults(); - _config_service.requestSave(); - + if (_periphery.init()) { + return true; + } else { + logger.error("Periphery init failed"); return false; } - - if (not _config_service.config().periphery.joystick_axes_tuned) { - logger.debug("Tunning axes.."); - _periphery.tune(_config_service.config().periphery); - _config_service.requestSave(); - } - - return true; } KF_IMPL_TIMED_POLLABLE(PeripherySystem); From def73df203b4d1a6082ea8fe662321ea75cd5ce7 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sun, 14 Jun 2026 23:48:13 +0300 Subject: [PATCH 211/326] chore: update gitignore --- DJC-Firmware/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DJC-Firmware/.gitignore b/DJC-Firmware/.gitignore index 89cc49c..fdcb730 100644 --- a/DJC-Firmware/.gitignore +++ b/DJC-Firmware/.gitignore @@ -3,3 +3,5 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch + +**ignore* \ No newline at end of file From 6d31905fe0a7c55581a0f22b4597dd6325db34bc Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 00:01:31 +0300 Subject: [PATCH 212/326] refactor(peer-favorites-registry): add entries source setter --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index e8dd871..cc9eae9 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -42,7 +42,12 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } }; - explicit constexpr PeerFavoritesRegistry(kf::Slice> entries) noexcept : _entries{entries} {} + explicit constexpr PeerFavoritesRegistry(kf::Slice> entries = {}) noexcept : _entries{entries} {} + + /// @brief Set entries source + void entries(kf::Slice> new_entries) noexcept { + _entries = new_entries; + } /// @brief Return the entire slot array, including empty slots. [[nodiscard]] kf::Slice> all() const noexcept { From e1539f59bb303c5b80c3a03ebfbd78b841935147 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 00:02:16 +0300 Subject: [PATCH 213/326] refactor(peer-favorites-registry.all): use trailing return type --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index cc9eae9..65d0616 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -50,7 +50,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } /// @brief Return the entire slot array, including empty slots. - [[nodiscard]] kf::Slice> all() const noexcept { + [[nodiscard]] auto all() const noexcept -> kf::Slice> { return {_entries.data(), _active_count}; } From 2b7d784699e5b00c343afd604f7d96f0bcf114cc Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 00:14:15 +0300 Subject: [PATCH 214/326] refactor(peer-favorites-registry): move active entries count to source setter Initable no longer needed --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index 65d0616..884237a 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -20,7 +20,7 @@ namespace djc { /// The registry does not own the memory, only manipulates it. /// /// @note All methods are safe to call from different UI callbacks as long as the underlying storage is exclusively owned by the registry. -struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable { +struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable { // TODO: remove initable /// @brief A single favorite‑peer record. struct Entry final { @@ -47,6 +47,11 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable /// @brief Set entries source void entries(kf::Slice> new_entries) noexcept { _entries = new_entries; + + _active_count = 0; + for (const auto &entry: _entries) { + _active_count += static_cast(entry.isSome()); + } } /// @brief Return the entire slot array, including empty slots. @@ -124,14 +129,9 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable return kf::none; } - // impl + // TODO: remove initable KF_IMPL_INITABLE(PeerFavoritesRegistry, void); - void initImpl() noexcept { - _active_count = 0; - for (const auto &entry: _entries) { - _active_count += static_cast(entry.isSome()); - } - } + void initImpl() noexcept {} }; }// namespace djc \ No newline at end of file From 8550359f80705bbb2fc4ebb5f4eddb6f298b4e75 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 00:14:45 +0300 Subject: [PATCH 215/326] fix(service/peer): use readonly config reference --- DJC-Firmware/src/djc/system/PeerSystem.hpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp index f3e69bf..2a0abbd 100644 --- a/DJC-Firmware/src/djc/system/PeerSystem.hpp +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -19,16 +19,13 @@ namespace djc::system { /// @brief System managing peer favorites, scanning and auto-connection. /// @note Owns PeerFavoritesRegistry, PeerScanningService, AutoConnectService. -/// Depends on Config (settings) and TransportLink (for scanning and connection). -/// On each poll, scans visible peers and triggers auto-connection to the most trusted visible favorite. +/// @note Depends on Config (readolny) and TransportLink (for scanning and connection). +/// @note On each poll, scans visible peers and triggers auto-connection to the most trusted visible favorite. +/// @note Peer favorites registry entries source should set extenally struct PeerSystem : System, kf::mixin::Configurable { - explicit PeerSystem(Config &config, transport::TransportLink &transport_link) noexcept : - kf::mixin::Configurable{config}, - _transport_link{transport_link}, - _peer_favorites_registry{{config.peer_favorites.data(), config.peer_favorites.size()}}, - _peer_scanning_service{config.peer_scanner, transport_link}, - _auto_connect_service{config.auto_connect_service, transport_link} {} + explicit PeerSystem(const Config &config, transport::TransportLink &transport_link) noexcept : + kf::mixin::Configurable{config}, _transport_link{transport_link} {} /// @brief Get mutable access to peer favorites registry component PeerFavoritesRegistry &favoritesRegistry() noexcept { @@ -64,9 +61,9 @@ struct PeerSystem : System, kf::mixin::Configurable { static constexpr auto logger{kf::Logger::create("PeerSystem")}; transport::TransportLink &_transport_link; - PeerFavoritesRegistry _peer_favorites_registry; - service::PeerScanningService _peer_scanning_service; - service::AutoConnectService _auto_connect_service; + PeerFavoritesRegistry _peer_favorites_registry{}; + service::PeerScanningService _peer_scanning_service{this->config().peer_scanner, _transport_link}; + service::AutoConnectService _auto_connect_service{this->config().auto_connect_service, _transport_link}; KF_IMPL_INITABLE(PeerSystem, bool); bool initImpl() noexcept { From 211dbe6498a9ece971d206a8f0bf925d20b402a0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 00:35:19 +0300 Subject: [PATCH 216/326] feat(system): add graphics system --- .../src/djc/system/GraphicsSystem.hpp | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/GraphicsSystem.hpp diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp new file mode 100644 index 0000000..c1ae0e3 --- /dev/null +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -0,0 +1,62 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "djc/service/DisplayManager.hpp" +#include "djc/system/System.hpp" +#include "djc/ui/VirtualKeyboard.hpp" + +namespace djc::system { + +/// @brief System managing display output, canvas, and virtual keyboard. +template struct GraphicsSystem : System> { + using DisplayManagerImpl = service::DisplayManager; + + explicit GraphicsSystem(I &display_driver) noexcept : + _display_manager{_display_driver, _virtual_keyboard} {} + + /// @brief Get mutable access to virtual keyboard component. + ui::VirtualKeyboard &virtualKeyboard() noexcept { + return _virtual_keyboard; + } + + /// @brief Get readonly access to virtual keyboard component. + constexpr const ui::VirtualKeyboard &virtualKeyboard() const noexcept { + return _virtual_keyboard; + } + + /// @brief Get mutable access to display manager service. + DisplayManagerImpl &displayManager() noexcept { + return _display_manager; + } + + /// @brief Get readonly access to display manager service. + constexpr const DisplayManagerImpl &displayManager() const noexcept { + return _display_manager; + } + +private: + static constexpr auto logger = kf::Logger::create("GraphicsSystem"); + + ui::VirtualKeyboard _virtual_keyboard{}; + DisplayManagerImpl _display_manager; + + using This = GraphicsSystem; + + KF_IMPL_INITABLE(This, bool); + bool initImpl() noexcept { + _display_manager.init(); + return true; + } + + KF_IMPL_TIMED_POLLABLE(This); + void pollImpl(kf::math::Milliseconds now) noexcept { + _display_manager.poll(now); + } +}; + +}// namespace djc::system \ No newline at end of file From d75554f6ce2275cf417d766e2be317e336e8d0af Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 00:43:10 +0300 Subject: [PATCH 217/326] feat(system): add control system --- DJC-Firmware/src/djc/system/ControlSystem.hpp | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/ControlSystem.hpp diff --git a/DJC-Firmware/src/djc/system/ControlSystem.hpp b/DJC-Firmware/src/djc/system/ControlSystem.hpp new file mode 100644 index 0000000..c8482ad --- /dev/null +++ b/DJC-Firmware/src/djc/system/ControlSystem.hpp @@ -0,0 +1,58 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "djc/ManualInput.hpp" +#include "djc/Periphery.hpp" +#include "djc/protocol/ProtocolLink.hpp" +#include "djc/service/Control.hpp" +#include "djc/system/System.hpp" +#include "djc/transport/TransportLink.hpp" + +namespace djc::system { + +/// @brief System managing manual control output. +/// @note Owns Control service. Depends Periphery (joysticks), TransportLink and ProtocolLink. +struct ControlSystem : System { + + explicit ControlSystem(Periphery &periphery, transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : + _periphery{periphery}, _control{transport_link, protocol_link} {} + + /// @brief Get mutable access to the control service. + service::Control &control() noexcept { + return _control; + } + + /// @brief Get readonly access to the control service. + constexpr const service::Control &control() const noexcept { + return _control; + } + +private: + Periphery &_periphery; + service::Control _control; + + KF_IMPL_INITABLE(ControlSystem, bool); + bool initImpl() noexcept { + return true; + } + + KF_IMPL_TIMED_POLLABLE(ControlSystem); + void pollImpl(kf::math::Milliseconds now) noexcept { + if (_control.enabled()) { + using I = ManualInput; + + _control.input(I{ + .left_x = I::fromNormalized(_periphery.left_joystick.axis_x.read()), + .left_y = I::fromNormalized(_periphery.left_joystick.axis_y.read()), + .right_x = I::fromNormalized(_periphery.right_joystick.axis_x.read()), + .right_y = I::fromNormalized(_periphery.right_joystick.axis_y.read()), + }); + } + + _control.poll(now); + } +}; + +}// namespace djc::system \ No newline at end of file From c07bef7cd2fa1283b93fdbc3d114fe3443d191d9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 01:13:16 +0300 Subject: [PATCH 218/326] refactor(system/graphics): remove virtual keyboard, logger --- .../src/djc/system/GraphicsSystem.hpp | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index c1ae0e3..665d756 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -3,9 +3,6 @@ #pragma once -#include -#include - #include "djc/service/DisplayManager.hpp" #include "djc/system/System.hpp" #include "djc/ui/VirtualKeyboard.hpp" @@ -16,18 +13,8 @@ namespace djc::system { template struct GraphicsSystem : System> { using DisplayManagerImpl = service::DisplayManager; - explicit GraphicsSystem(I &display_driver) noexcept : - _display_manager{_display_driver, _virtual_keyboard} {} - - /// @brief Get mutable access to virtual keyboard component. - ui::VirtualKeyboard &virtualKeyboard() noexcept { - return _virtual_keyboard; - } - - /// @brief Get readonly access to virtual keyboard component. - constexpr const ui::VirtualKeyboard &virtualKeyboard() const noexcept { - return _virtual_keyboard; - } + explicit GraphicsSystem(I &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : + _display_manager{_display_driver, virtual_keyboard} {} /// @brief Get mutable access to display manager service. DisplayManagerImpl &displayManager() noexcept { @@ -40,9 +27,6 @@ template struct GraphicsSystem : System> { } private: - static constexpr auto logger = kf::Logger::create("GraphicsSystem"); - - ui::VirtualKeyboard _virtual_keyboard{}; DisplayManagerImpl _display_manager; using This = GraphicsSystem; From a661d97a68819868687c6c20d77aae305195488e Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 01:18:52 +0300 Subject: [PATCH 219/326] feat(system): add ui system --- DJC-Firmware/src/djc/system/UiSystem.hpp | 87 ++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 DJC-Firmware/src/djc/system/UiSystem.hpp diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp new file mode 100644 index 0000000..7a36956 --- /dev/null +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -0,0 +1,87 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "djc/Config.hpp" +#include "djc/system/System.hpp" +#include "djc/ui/UI.hpp" +#include "djc/ui/VirtualKeyboard.hpp" +#include "djc/ui/pages/RootPage.hpp" + +namespace djc::system { + +/// @brief System that owns UI components: renderer, virtual keyboard, UI instance and root page +/// @note Initialises the UI with the root page and triggers an initial update event. Polls the UI on every main loop iteration +struct UiSystem : System { + + using Renderer = ui::UI::Traits::RenderImpl; + + explicit UiSystem(const Config &config) noexcept : + _renderer{config.render_system} {} + + /// @brief Get mutable access to renderer component + Renderer &renderer() noexcept { + return _renderer; + } + + /// @brief Get readonly access to renderer component + constexpr const Renderer &renderer() const noexcept { + return _renderer; + } + + /// @brief Get mutable access to virtual keyboard component + ui::VirtualKeyboard &virtualKeyboard() noexcept { + return _virtual_keyboard; + } + + /// @brief Get readonly access to virtual keyboard component + constexpr const ui::VirtualKeyboard &virtualKeyboard() const noexcept { + return _virtual_keyboard; + } + + /// @brief Get mutable access to ui service + ui::UI &ui() noexcept { + return _ui_service; + } + + /// @brief Get readonly access to ui service + constexpr const ui::UI &ui() const noexcept { + return _ui_service; + } + + /// @brief Get mutable access to ui service + ui::pages::RootPage &rootPage() noexcept { + return _root_page; + } + + /// @brief Get readonly access to ui service + constexpr const ui::pages::RootPage &rootPage() const noexcept { + return _root_page; + } + +private: + static constexpr auto logger{kf::Logger::create("UiSystem")}; + + Renderer _renderer; + ui::VirtualKeyboard _virtual_keyboard{}; + ui::UI _ui_service{_renderer, _virtual_keyboard}; + ui::pages::RootPage _root_page{_ui_service}; + + KF_IMPL_INITABLE(UiSystem, bool); + bool initImpl() noexcept { + _ui_service.activePage(_root_page); + _ui_service.addEvent(ui::UI::Traits::EventImpl::update()); + + return true; + } + + KF_IMPL_TIMED_POLLABLE(UiSystem); + void poll(kf::math::Milliseconds now) noexcept { + _ui_service.poll(now); + } +}; + +}// namespace djc::system \ No newline at end of file From 1160e33a314ca0a2b01532c0058bb1c5120969bb Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 12:09:55 +0300 Subject: [PATCH 220/326] refactor(pages/config): use config service instead of config manager --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 30 +++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 390820c..52f7344 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -8,9 +8,9 @@ #include #include "djc/Config.hpp" -#include "djc/ConfigManager.hpp" #include "djc/PeerFavoritesRegistry.hpp" #include "djc/protocol/ProtocolRegistry.hpp" +#include "djc/service/ConfigService.hpp" #include "djc/transport/Kind.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/pages/PeerFavoritePage.hpp" @@ -22,10 +22,10 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { explicit ConfigPage( UI &ui, UI::Page &root, - djc::ConfigManager &config_manager, + djc::service::ConfigService &config_service, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : Page{ui, "Config"}, - _config_manager{config_manager}, + _config_service{config_service}, _peer_favorite_page{ui, *this, _peer_favoriter_registry}, _peer_favoriter_registry{peer_favoriter_registry}, _device_name_input{ui.createTextInput()}, @@ -42,21 +42,21 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { }} { widgets(layout(0)); - _device_name_input.source({_config_manager.config().device_name.data(), _config_manager.config().device_name.size()}); + _device_name_input.source({_config_service.config().device_name.data(), _config_service.config().device_name.size()}); _device_name_input.hint("Device name"); - _save_config_button.callback([this]() { _config_manager.save(); }); + _save_config_button.callback([this]() { + _config_service.requestSave(); + }); _save_config_button.hint("Write config from RAM into NVS"); _load_config_button.callback([this]() { - _config_manager.load(); - this->init(); + _config_service.requestLoad(); }); _load_config_button.hint("Load config from NVS into RAM"); _reset_config_button.callback([this]() { - _config_manager.reset(); - this->init(); + _config_service.requestReset(); }); _reset_config_button.hint("Set RAM config as detaults"); @@ -68,17 +68,17 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { _favorite_peers_fold_toggle_button.hint("Toggle folding"); _default_transport_kind_selector.callback([this](transport::Kind kind) { - _config_manager.config().init_transport_kind = kind; + _config_service.config().init_transport_kind = kind; }); _labeled_default_transport_kind_selector.hint("Define transport select after init"); _default_protocol_mode_selector.callback([this](Mode mode) { - _config_manager.config().init_protocol_mode = mode; + _config_service.config().init_protocol_mode = mode; }); _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); _autoconnect_enabled_input.callback([this](bool value) { - _config_manager.config().auto_connect_service.enabled = value; + _config_service.config().auto_connect_service.enabled = value; }); _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); @@ -94,6 +94,8 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { } } + // TODO: add onPoll with sync + void onEntry() noexcept override { const auto all_favorites = _peer_favoriter_registry.all(); @@ -131,7 +133,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // state - djc::ConfigManager &_config_manager; + djc::service::ConfigService &_config_service; PeerFavoritesRegistry &_peer_favoriter_registry; kf::memory::StaticString<32> _label_favorites_buffer{}; bool show_favorites{true}; @@ -216,7 +218,7 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { // impl KF_IMPL_INITABLE(ConfigPage, void); void initImpl() noexcept { - const auto &config = _config_manager.config(); + const auto &config = _config_service.config(); _default_protocol_mode_selector.value(config.init_protocol_mode); _default_transport_kind_selector.value(config.init_transport_kind); _autoconnect_enabled_input.value(config.auto_connect_service.enabled); From b49d1cc6b2443f6aa5b62318104d1539f53de015 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 12:11:17 +0300 Subject: [PATCH 221/326] chore: remove config manager --- DJC-Firmware/src/djc/ConfigManager.hpp | 74 -------------------------- 1 file changed, 74 deletions(-) delete mode 100644 DJC-Firmware/src/djc/ConfigManager.hpp diff --git a/DJC-Firmware/src/djc/ConfigManager.hpp b/DJC-Firmware/src/djc/ConfigManager.hpp deleted file mode 100644 index 0d65a3b..0000000 --- a/DJC-Firmware/src/djc/ConfigManager.hpp +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 KiraFlux -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include - -#include "djc/Config.hpp" - -namespace djc { - -struct ConfigManager final : kf::mixin::NonCopyable { - - [[nodiscard]] constexpr const Config &config() const noexcept { - return _storage.config; - } - - [[nodiscard]] Config &config() noexcept { - return _storage.config; - } - - [[nodiscard]] bool modified() const noexcept { - return _modified; - } - - void modified(bool is_modified) noexcept { - _modified = is_modified; - } - - void save() noexcept { - logger.info("Saving config to NVS"); - - if (not _storage.save()) { - logger.error("Failed to save config into NVS"); - } - } - - void load() noexcept { - logger.info("Loading config from NVS"); - - if (not _storage.load()) { - logger.error("Failed to load config"); - reset(); - save(); - } - - if (not _storage.config.isLatestVersion()) { - logger.error("Config version is outdated"); - reset(); - save(); - } - } - - void reset() noexcept { - logger.info("Resetting RAM config cache to defaults"); - - _storage.config = djc::Config::defaults(); - modified(true); - } - -private: - static constexpr auto logger{kf::Logger::create("ConfigManager")}; - - kf::memory::Storage _storage{ - .key = "DC", - .config = djc::Config::defaults(), - }; - - bool _modified{false}; -}; - -}// namespace djc \ No newline at end of file From 871437d076f8c3e3dd1d34195942a61616603a06 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 12:14:07 +0300 Subject: [PATCH 222/326] refactor(system/ui): rename ui service property --- DJC-Firmware/src/djc/system/UiSystem.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp index 7a36956..e7574c0 100644 --- a/DJC-Firmware/src/djc/system/UiSystem.hpp +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -43,12 +43,12 @@ struct UiSystem : System { } /// @brief Get mutable access to ui service - ui::UI &ui() noexcept { + ui::UI &service() noexcept { return _ui_service; } /// @brief Get readonly access to ui service - constexpr const ui::UI &ui() const noexcept { + constexpr const ui::UI &service() const noexcept { return _ui_service; } From 27071a0462b8361dcb3592f45446bfa016330e1d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 12:15:49 +0300 Subject: [PATCH 223/326] refactor(system/graphics): rename display service property --- DJC-Firmware/src/djc/system/GraphicsSystem.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index 665d756..3440070 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -16,13 +16,13 @@ template struct GraphicsSystem : System> { explicit GraphicsSystem(I &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : _display_manager{_display_driver, virtual_keyboard} {} - /// @brief Get mutable access to display manager service. - DisplayManagerImpl &displayManager() noexcept { + /// @brief Get mutable access to display service + DisplayManagerImpl &service() noexcept { return _display_manager; } - /// @brief Get readonly access to display manager service. - constexpr const DisplayManagerImpl &displayManager() const noexcept { + /// @brief Get readonly access to display service + constexpr const DisplayManagerImpl &service() const noexcept { return _display_manager; } From 1c2b78afaa68ee4800531296522c507f10365c75 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 12:19:05 +0300 Subject: [PATCH 224/326] refactor(system/input): rename service getter --- DJC-Firmware/src/djc/system/InputSystem.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/system/InputSystem.hpp b/DJC-Firmware/src/djc/system/InputSystem.hpp index 0fad883..63674c3 100644 --- a/DJC-Firmware/src/djc/system/InputSystem.hpp +++ b/DJC-Firmware/src/djc/system/InputSystem.hpp @@ -25,13 +25,13 @@ struct InputSystem : System, kf::mixin::Configurable { periphery.right_button_listener, } {} - /// @brief Get mutable access to the input handler service (for callback setup). - service::InputHandler &inputHandler() noexcept { + /// @brief Get mutable access to the input handler service + service::InputHandler &service() noexcept { return _input_handler; } - /// @brief Get readonly access to the input handler service. - constexpr const service::InputHandler &inputHandler() const noexcept { + /// @brief Get readonly access to the input handler service + constexpr const service::InputHandler &service() const noexcept { return _input_handler; } From ce134818a7d0e09f4a3fd7506452b337465ce9cd Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 12:23:49 +0300 Subject: [PATCH 225/326] refactor(system/control) --- DJC-Firmware/src/djc/system/ControlSystem.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/system/ControlSystem.hpp b/DJC-Firmware/src/djc/system/ControlSystem.hpp index c8482ad..41eed52 100644 --- a/DJC-Firmware/src/djc/system/ControlSystem.hpp +++ b/DJC-Firmware/src/djc/system/ControlSystem.hpp @@ -19,13 +19,13 @@ struct ControlSystem : System { explicit ControlSystem(Periphery &periphery, transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : _periphery{periphery}, _control{transport_link, protocol_link} {} - /// @brief Get mutable access to the control service. - service::Control &control() noexcept { + /// @brief Get mutable access to the control service + service::Control &service() noexcept { return _control; } - /// @brief Get readonly access to the control service. - constexpr const service::Control &control() const noexcept { + /// @brief Get readonly access to the control service + constexpr const service::Control &service() const noexcept { return _control; } From 04e0edceac293d7977680f6d548a6e8f931a6700 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 13:02:01 +0300 Subject: [PATCH 226/326] refactor(system/periphery): remove tune method wrapper --- DJC-Firmware/src/djc/system/PeripherySystem.hpp | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp index bec1a1c..e47b83f 100644 --- a/DJC-Firmware/src/djc/system/PeripherySystem.hpp +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -28,16 +28,6 @@ struct PeripherySystem : System, kf::mixin::Configurable Date: Mon, 15 Jun 2026 13:18:46 +0300 Subject: [PATCH 227/326] refactor(service/config): extract sync method --- .../src/djc/service/ConfigService.hpp | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 5e8940f..e82a90c 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -44,27 +44,8 @@ struct ConfigService : Service { logger.debug("reset requested"); } -private: - static constexpr auto logger{kf::Logger::create("ConfigService")}; - - static constexpr kf::math::Timer::Config sync_timer_config{ - .period = 5'000, - }; - - kf::memory::Storage _storage{ - .key = "DC", - .config = djc::Config::defaults(), - }; - - kf::math::Timer _sync_timer{sync_timer_config}; - - bool _save_requested{false}, _load_requested{false}, _reset_requested{false}; - - KF_IMPL_TIMED_POLLABLE(ConfigService); - void pollImpl(kf::math::Milliseconds now) noexcept { - if (not _sync_timer.expired(now)) { return; } - _sync_timer.start(now); - + /// @brief Force sync now + void sync() noexcept { if (_load_requested) { _load_requested = false; @@ -100,6 +81,30 @@ struct ConfigService : Service { } } } + +private: + static constexpr auto logger{kf::Logger::create("ConfigService")}; + + static constexpr kf::math::Timer::Config sync_timer_config{ + .period = 5'000, + }; + + kf::memory::Storage _storage{ + .key = "DC", + .config = djc::Config::defaults(), + }; + + kf::math::Timer _sync_timer{sync_timer_config}; + + bool _save_requested{false}, _load_requested{false}, _reset_requested{false}; + + KF_IMPL_TIMED_POLLABLE(ConfigService); + void pollImpl(kf::math::Milliseconds now) noexcept { + if (_sync_timer.expired(now)) { + _sync_timer.start(now); + sync(); + } + } }; }// namespace djc::service \ No newline at end of file From c54d84a5a36161c244a9926a773210ce0ac35712 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 13:21:45 +0300 Subject: [PATCH 228/326] feat(mixin): add service owner mixin --- DJC-Firmware/src/djc/mixin/ServiceOwner.hpp | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 DJC-Firmware/src/djc/mixin/ServiceOwner.hpp diff --git a/DJC-Firmware/src/djc/mixin/ServiceOwner.hpp b/DJC-Firmware/src/djc/mixin/ServiceOwner.hpp new file mode 100644 index 0000000..c59e698 --- /dev/null +++ b/DJC-Firmware/src/djc/mixin/ServiceOwner.hpp @@ -0,0 +1,37 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include + +#include "djc/service/Service.hpp" + +namespace djc::mixin { + +struct ServiceOwnerTag {}; + +/// @brief Mixin represents that class owns one service +template struct ServiceOwner : ServiceOwnerTag { + KF_CHECK_IMPL(ServiceType, ::djc::service::ServiceTag); + + explicit ServiceOwner(ServiceType &&service) noexcept : + _service{std::move(service)} {} + + /// @brief Get mutable access to the service + ServiceType &service() noexcept { + return _service; + } + + /// @brief Get readonly access to the service + constexpr const ServiceType &service() const noexcept { + return _service; + } + +private: + ServiceType _service; +}; + +}// namespace djc::mixin \ No newline at end of file From e2c4a261cf9951cd552b0df5746d228d3d9b8d72 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 13:25:25 +0300 Subject: [PATCH 229/326] refactor(system/config): use service owner mixin --- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 25 +++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index 9ef2e2e..49efecc 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -3,38 +3,35 @@ #pragma once +#include "djc/mixin/ServiceOwner.hpp" #include "djc/service/ConfigService.hpp" #include "djc/system/System.hpp" namespace djc::system { /// @brief System owning the config service, handling deferred NVS operations -/// @note Wraps ConfigService, requests load on init, and polls it periodically -struct ConfigSystem : System { +/// @note Wraps ConfigService, load on init, and polls it periodically +struct ConfigSystem : - /// @brief Get mutable access to config service - service::ConfigService &service() noexcept { - return _config_service; - } + System, + mixin::ServiceOwner - /// @brief Get readonly access to config service - constexpr const service::ConfigService &service() const noexcept { - return _config_service; - } +{ + explicit ConfigSystem() noexcept : + mixin::ServiceOwner{{}} {} private: - service::ConfigService _config_service{}; - KF_IMPL_INITABLE(ConfigSystem, bool); bool initImpl() noexcept { - _config_service.requestLoad(); + this->service().requestLoad(); + this->service().sync(); return true; } KF_IMPL_TIMED_POLLABLE(ConfigSystem); void poll(kf::math::Milliseconds now) noexcept { - _config_service.poll(now); + this->service().poll(now); } }; From 022dbd315e8f00d437b761fdd28671186c50fe67 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 13:32:31 +0300 Subject: [PATCH 230/326] refactor(system/control): use service owner mixin --- DJC-Firmware/src/djc/system/ControlSystem.hpp | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/DJC-Firmware/src/djc/system/ControlSystem.hpp b/DJC-Firmware/src/djc/system/ControlSystem.hpp index 41eed52..a089581 100644 --- a/DJC-Firmware/src/djc/system/ControlSystem.hpp +++ b/DJC-Firmware/src/djc/system/ControlSystem.hpp @@ -5,6 +5,7 @@ #include "djc/ManualInput.hpp" #include "djc/Periphery.hpp" +#include "djc/mixin/ServiceOwner.hpp" #include "djc/protocol/ProtocolLink.hpp" #include "djc/service/Control.hpp" #include "djc/system/System.hpp" @@ -12,26 +13,19 @@ namespace djc::system { -/// @brief System managing manual control output. +/// @brief System managing manual control output /// @note Owns Control service. Depends Periphery (joysticks), TransportLink and ProtocolLink. -struct ControlSystem : System { +struct ControlSystem : - explicit ControlSystem(Periphery &periphery, transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : - _periphery{periphery}, _control{transport_link, protocol_link} {} - - /// @brief Get mutable access to the control service - service::Control &service() noexcept { - return _control; - } + System, + mixin::ServiceOwner - /// @brief Get readonly access to the control service - constexpr const service::Control &service() const noexcept { - return _control; - } +{ + explicit ControlSystem(Periphery &periphery, transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : + mixin::ServiceOwner{service::Control{transport_link, protocol_link}}, _periphery{periphery} {} private: Periphery &_periphery; - service::Control _control; KF_IMPL_INITABLE(ControlSystem, bool); bool initImpl() noexcept { @@ -40,10 +34,10 @@ struct ControlSystem : System { KF_IMPL_TIMED_POLLABLE(ControlSystem); void pollImpl(kf::math::Milliseconds now) noexcept { - if (_control.enabled()) { + if (this->service().enabled()) { using I = ManualInput; - _control.input(I{ + this->service().input(I{ .left_x = I::fromNormalized(_periphery.left_joystick.axis_x.read()), .left_y = I::fromNormalized(_periphery.left_joystick.axis_y.read()), .right_x = I::fromNormalized(_periphery.right_joystick.axis_x.read()), @@ -51,7 +45,7 @@ struct ControlSystem : System { }); } - _control.poll(now); + this->service().poll(now); } }; From 912cc6ff90c40ecdf1bd0dd6db78c65ffd6166e9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 13:35:15 +0300 Subject: [PATCH 231/326] refactor(system/graphics): use service owner mixin --- .../src/djc/system/GraphicsSystem.hpp | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index 3440070..1264cc4 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -3,6 +3,7 @@ #pragma once +#include "djc/mixin/ServiceOwner.hpp" #include "djc/service/DisplayManager.hpp" #include "djc/system/System.hpp" #include "djc/ui/VirtualKeyboard.hpp" @@ -10,25 +11,18 @@ namespace djc::system { /// @brief System managing display output, canvas, and virtual keyboard. -template struct GraphicsSystem : System> { - using DisplayManagerImpl = service::DisplayManager; +template struct GraphicsSystem : - explicit GraphicsSystem(I &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : - _display_manager{_display_driver, virtual_keyboard} {} + System>, + mixin::ServiceOwner> - /// @brief Get mutable access to display service - DisplayManagerImpl &service() noexcept { - return _display_manager; - } +{ + using DisplayManagerImpl = service::DisplayManager; - /// @brief Get readonly access to display service - constexpr const DisplayManagerImpl &service() const noexcept { - return _display_manager; - } + explicit GraphicsSystem(I &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : + mixin::ServiceOwner{DisplayManagerImpl{_display_driver, virtual_keyboard}} {} private: - DisplayManagerImpl _display_manager; - using This = GraphicsSystem; KF_IMPL_INITABLE(This, bool); From 78662b11e998897877f46adc2c6b4f136f3515f8 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 13:39:45 +0300 Subject: [PATCH 232/326] refactor(system/input): use service owner mixin --- DJC-Firmware/src/djc/system/InputSystem.hpp | 28 +++++++-------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/DJC-Firmware/src/djc/system/InputSystem.hpp b/DJC-Firmware/src/djc/system/InputSystem.hpp index 63674c3..23b5b9b 100644 --- a/DJC-Firmware/src/djc/system/InputSystem.hpp +++ b/DJC-Firmware/src/djc/system/InputSystem.hpp @@ -3,10 +3,9 @@ #pragma once -#include - #include "djc/Config.hpp" #include "djc/Periphery.hpp" +#include "djc/mixin/ServiceOwner.hpp" #include "djc/service/InputHandler.hpp" #include "djc/system/System.hpp" @@ -14,30 +13,21 @@ namespace djc::system { /// @brief System managing user input /// @note Owns InputHandler service. Depends on Config (read‑only) and Periphery. -struct InputSystem : System, kf::mixin::Configurable { +struct InputSystem : + + System, + mixin::ServiceOwner +{ explicit InputSystem(const Config &config, Periphery &periphery) noexcept : - kf::mixin::Configurable{config}, - _input_handler{ + mixin::ServiceOwner{service::InputHandler{ config.input_handler, periphery.right_joystick,// primary joystick for navigation/control periphery.left_button_listener, periphery.right_button_listener, - } {} - - /// @brief Get mutable access to the input handler service - service::InputHandler &service() noexcept { - return _input_handler; - } - - /// @brief Get readonly access to the input handler service - constexpr const service::InputHandler &service() const noexcept { - return _input_handler; - } + }} {} private: - service::InputHandler _input_handler; - KF_IMPL_INITABLE(InputSystem, bool); bool initImpl() noexcept { // For now, nothing to do. @@ -46,7 +36,7 @@ struct InputSystem : System, kf::mixin::Configurable { KF_IMPL_TIMED_POLLABLE(InputSystem); void pollImpl(kf::math::Milliseconds now) noexcept { - _input_handler.poll(now); + this->service().poll(now); } }; From 581b545746f23fd4638be64e56a4d8a0a110153b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 13:43:26 +0300 Subject: [PATCH 233/326] refactor(system/ui): use servic owner mixin --- DJC-Firmware/src/djc/system/UiSystem.hpp | 27 ++++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp index e7574c0..74230ba 100644 --- a/DJC-Firmware/src/djc/system/UiSystem.hpp +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -5,6 +5,7 @@ #include +#include "djc/mixin/ServiceOwner.hpp" #include "djc/Config.hpp" #include "djc/system/System.hpp" #include "djc/ui/UI.hpp" @@ -15,11 +16,16 @@ namespace djc::system { /// @brief System that owns UI components: renderer, virtual keyboard, UI instance and root page /// @note Initialises the UI with the root page and triggers an initial update event. Polls the UI on every main loop iteration -struct UiSystem : System { +struct UiSystem : +System, +mixin::ServiceOwner + +{ using Renderer = ui::UI::Traits::RenderImpl; explicit UiSystem(const Config &config) noexcept : + mixin::ServiceOwner{djc::ui::UI{_renderer, _virtual_keyboard}}, _renderer{config.render_system} {} /// @brief Get mutable access to renderer component @@ -42,16 +48,6 @@ struct UiSystem : System { return _virtual_keyboard; } - /// @brief Get mutable access to ui service - ui::UI &service() noexcept { - return _ui_service; - } - - /// @brief Get readonly access to ui service - constexpr const ui::UI &service() const noexcept { - return _ui_service; - } - /// @brief Get mutable access to ui service ui::pages::RootPage &rootPage() noexcept { return _root_page; @@ -67,20 +63,19 @@ struct UiSystem : System { Renderer _renderer; ui::VirtualKeyboard _virtual_keyboard{}; - ui::UI _ui_service{_renderer, _virtual_keyboard}; - ui::pages::RootPage _root_page{_ui_service}; + ui::pages::RootPage _root_page{this->service()}; KF_IMPL_INITABLE(UiSystem, bool); bool initImpl() noexcept { - _ui_service.activePage(_root_page); - _ui_service.addEvent(ui::UI::Traits::EventImpl::update()); + this->service().activePage(_root_page); + this->service().addEvent(ui::UI::Traits::EventImpl::update()); return true; } KF_IMPL_TIMED_POLLABLE(UiSystem); void poll(kf::math::Milliseconds now) noexcept { - _ui_service.poll(now); + this->service().poll(now); } }; From 909f4fcef1d7e8df2718a23a27af49efab7162ec Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:03:52 +0300 Subject: [PATCH 234/326] feat(mixin): add initable mixin with init arguments --- DJC-Firmware/src/djc/mixin/Initable.hpp | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 DJC-Firmware/src/djc/mixin/Initable.hpp diff --git a/DJC-Firmware/src/djc/mixin/Initable.hpp b/DJC-Firmware/src/djc/mixin/Initable.hpp new file mode 100644 index 0000000..fb2d99f --- /dev/null +++ b/DJC-Firmware/src/djc/mixin/Initable.hpp @@ -0,0 +1,35 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace djc::mixin { + +/// @brief CRTP mixin that adds an `init(Args...)` method forwarding arguments to `initImpl(Args...)` +/// @tparam Impl Derived class (must implement `initImpl(Args...)`) +template struct Initable; + +template struct Initable { + + /// @brief Initializes the object with given arguments + /// @param args Arguments forwarded to `initImpl` + /// @return init result + [[nodiscard]] R init(Args... args) noexcept { + return static_cast(this)->initImpl(std::forward(args)...); + } +}; + +template struct Initable { + + /// @brief Initializes the object with given arguments + /// @param args Arguments forwarded to `initImpl` + void init(Args... args) noexcept { + static_cast(this)->initImpl(std::forward(args)...); + } +}; + +#define DJC_IMPL_INITABLE(__impl__, ...) friend struct ::djc::mixin::Initable<__impl__, __VA_ARGS__> + +}// namespace djc::mixin \ No newline at end of file From e4863ac322d21d1d750b5a67bf81ce0e00131188 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:28:11 +0300 Subject: [PATCH 235/326] refactor(system): use djc::Initable --- DJC-Firmware/src/djc/system/System.hpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/system/System.hpp b/DJC-Firmware/src/djc/system/System.hpp index 39dc2f8..44e4ad4 100644 --- a/DJC-Firmware/src/djc/system/System.hpp +++ b/DJC-Firmware/src/djc/system/System.hpp @@ -3,21 +3,22 @@ #pragma once -#include #include #include +#include "djc/mixin/Initable.hpp" + namespace djc::system { struct SystemTag {}; /// @brief System static inteface /// @note System owns a group of related services and components, provides `init()` and `poll(now)`, and is orchestrated from main. -template struct System : +template struct System : SystemTag, - kf::mixin::NonCopyable, - kf::mixin::Initable, + djc::mixin::NonCopyable, + kf::mixin::Initable, kf::mixin::TimedPollable {}; }// namespace djc::system \ No newline at end of file From 82db7babc71d0ac5ab077782c971cd383fe7d8f3 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:29:36 +0300 Subject: [PATCH 236/326] wip(system): use djc::Initable --- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 8 +++----- DJC-Firmware/src/djc/system/ControlSystem.hpp | 8 +++----- DJC-Firmware/src/djc/system/GraphicsSystem.hpp | 7 +++---- DJC-Firmware/src/djc/system/InputSystem.hpp | 9 +++------ DJC-Firmware/src/djc/system/PeripherySystem.hpp | 10 +++++----- DJC-Firmware/src/djc/system/UiSystem.hpp | 8 +++----- 6 files changed, 20 insertions(+), 30 deletions(-) diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index 49efecc..e3f8c37 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -13,7 +13,7 @@ namespace djc::system { /// @note Wraps ConfigService, load on init, and polls it periodically struct ConfigSystem : - System, + System, mixin::ServiceOwner { @@ -21,12 +21,10 @@ struct ConfigSystem : mixin::ServiceOwner{{}} {} private: - KF_IMPL_INITABLE(ConfigSystem, bool); - bool initImpl() noexcept { + DJC_IMPL_INITABLE(ConfigSystem, void()); + void initImpl() noexcept { this->service().requestLoad(); this->service().sync(); - - return true; } KF_IMPL_TIMED_POLLABLE(ConfigSystem); diff --git a/DJC-Firmware/src/djc/system/ControlSystem.hpp b/DJC-Firmware/src/djc/system/ControlSystem.hpp index a089581..94a985a 100644 --- a/DJC-Firmware/src/djc/system/ControlSystem.hpp +++ b/DJC-Firmware/src/djc/system/ControlSystem.hpp @@ -17,7 +17,7 @@ namespace djc::system { /// @note Owns Control service. Depends Periphery (joysticks), TransportLink and ProtocolLink. struct ControlSystem : - System, + System, mixin::ServiceOwner { @@ -27,10 +27,8 @@ struct ControlSystem : private: Periphery &_periphery; - KF_IMPL_INITABLE(ControlSystem, bool); - bool initImpl() noexcept { - return true; - } + DJC_IMPL_INITABLE(ControlSystem, void()); + void initImpl() noexcept {} KF_IMPL_TIMED_POLLABLE(ControlSystem); void pollImpl(kf::math::Milliseconds now) noexcept { diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index 1264cc4..f990415 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -13,7 +13,7 @@ namespace djc::system { /// @brief System managing display output, canvas, and virtual keyboard. template struct GraphicsSystem : - System>, + System, void()>, mixin::ServiceOwner> { @@ -25,10 +25,9 @@ template struct GraphicsSystem : private: using This = GraphicsSystem; - KF_IMPL_INITABLE(This, bool); - bool initImpl() noexcept { + DJC_IMPL_INITABLE(This, void()); + void initImpl() noexcept { _display_manager.init(); - return true; } KF_IMPL_TIMED_POLLABLE(This); diff --git a/DJC-Firmware/src/djc/system/InputSystem.hpp b/DJC-Firmware/src/djc/system/InputSystem.hpp index 23b5b9b..eff6205 100644 --- a/DJC-Firmware/src/djc/system/InputSystem.hpp +++ b/DJC-Firmware/src/djc/system/InputSystem.hpp @@ -15,7 +15,7 @@ namespace djc::system { /// @note Owns InputHandler service. Depends on Config (read‑only) and Periphery. struct InputSystem : - System, + System, mixin::ServiceOwner { @@ -28,11 +28,8 @@ struct InputSystem : }} {} private: - KF_IMPL_INITABLE(InputSystem, bool); - bool initImpl() noexcept { - // For now, nothing to do. - return true; - } + DJC_IMPL_INITABLE(InputSystem, void()); + void initImpl() noexcept {} KF_IMPL_TIMED_POLLABLE(InputSystem); void pollImpl(kf::math::Milliseconds now) noexcept { diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp index e47b83f..912befe 100644 --- a/DJC-Firmware/src/djc/system/PeripherySystem.hpp +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -4,7 +4,6 @@ #pragma once #include -#include #include "djc/Config.hpp" #include "djc/Periphery.hpp" @@ -14,9 +13,10 @@ namespace djc::system { /// @brief System that owns and initializes hardware peripherals /// @note Provides access to peripherals for other systems via getters; no periodic polling needed. -struct PeripherySystem : System, kf::mixin::Configurable { +struct PeripherySystem : System { - using kf::mixin::Configurable::Configurable; + explicit PeripherySystem(const Config &config) noexcept : + _periphery{config.periphery} {} /// @brief Get mutable access to periphery component Periphery &periphery() noexcept { @@ -31,9 +31,9 @@ struct PeripherySystem : System, kf::mixin::Configurableconfig().periphery}; + Periphery _periphery; - KF_IMPL_INITABLE(PeripherySystem, bool); + DJC_IMPL_INITABLE(PeripherySystem, bool()); bool initImpl() noexcept { if (_periphery.init()) { return true; diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp index 74230ba..85b4423 100644 --- a/DJC-Firmware/src/djc/system/UiSystem.hpp +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -18,7 +18,7 @@ namespace djc::system { /// @note Initialises the UI with the root page and triggers an initial update event. Polls the UI on every main loop iteration struct UiSystem : -System, +System, mixin::ServiceOwner { @@ -65,12 +65,10 @@ mixin::ServiceOwner ui::VirtualKeyboard _virtual_keyboard{}; ui::pages::RootPage _root_page{this->service()}; - KF_IMPL_INITABLE(UiSystem, bool); - bool initImpl() noexcept { + DJC_IMPL_INITABLE(UiSystem, void()); + void initImpl() noexcept { this->service().activePage(_root_page); this->service().addEvent(ui::UI::Traits::EventImpl::update()); - - return true; } KF_IMPL_TIMED_POLLABLE(UiSystem); From 8da4bd4aa0b852816ea1e1debe0d93a6d78d0e83 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:30:40 +0300 Subject: [PATCH 237/326] refactor(system): use djc::Initable --- DJC-Firmware/src/djc/system/PeerSystem.hpp | 26 +++++++++---------- .../src/djc/system/ProtocolSystem.hpp | 19 +++++++------- .../src/djc/system/TransportSystem.hpp | 16 +++++------- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp index 2a0abbd..f300f5e 100644 --- a/DJC-Firmware/src/djc/system/PeerSystem.hpp +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -6,7 +6,6 @@ #include #include -#include #include "djc/Config.hpp" #include "djc/PeerFavoritesRegistry.hpp" @@ -17,15 +16,19 @@ namespace djc::system { -/// @brief System managing peer favorites, scanning and auto-connection. +/// @brief System managing peer favorites, scanning and auto-connection /// @note Owns PeerFavoritesRegistry, PeerScanningService, AutoConnectService. /// @note Depends on Config (readolny) and TransportLink (for scanning and connection). /// @note On each poll, scans visible peers and triggers auto-connection to the most trusted visible favorite. /// @note Peer favorites registry entries source should set extenally -struct PeerSystem : System, kf::mixin::Configurable { +struct PeerSystem : + System + +{ explicit PeerSystem(const Config &config, transport::TransportLink &transport_link) noexcept : - kf::mixin::Configurable{config}, _transport_link{transport_link} {} + _peer_scanning_service{config.peer_scanner, transport_link}, + _auto_connect_service{config.auto_connect_service, transport_link} {} /// @brief Get mutable access to peer favorites registry component PeerFavoritesRegistry &favoritesRegistry() noexcept { @@ -60,22 +63,19 @@ struct PeerSystem : System, kf::mixin::Configurable { private: static constexpr auto logger{kf::Logger::create("PeerSystem")}; - transport::TransportLink &_transport_link; PeerFavoritesRegistry _peer_favorites_registry{}; - service::PeerScanningService _peer_scanning_service{this->config().peer_scanner, _transport_link}; - service::AutoConnectService _auto_connect_service{this->config().auto_connect_service, _transport_link}; + service::PeerScanningService _peer_scanning_service; + service::AutoConnectService _auto_connect_service; - KF_IMPL_INITABLE(PeerSystem, bool); - bool initImpl() noexcept { + DJC_IMPL_INITABLE(PeerSystem, void(transport::TransportLink &)); + void initImpl(transport::TransportLink &transport_link) noexcept { _peer_favorites_registry.init(); _peer_scanning_service.init(); - _auto_connect_service.callback([this](const auto &address) -> void { + _auto_connect_service.callback([&transport_link](const auto &address) -> void { logger.info("Auto Connect"); - (void) _transport_link.connect(address); + (void) transport_link.connect(address); }); - - return true; } KF_IMPL_TIMED_POLLABLE(PeerSystem); diff --git a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp index 6dd8481..c8582d0 100644 --- a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp +++ b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp @@ -4,7 +4,6 @@ #pragma once #include -#include #include "djc/Config.hpp" #include "djc/MavlinkTelemetryRegistry.hpp" @@ -18,9 +17,11 @@ namespace djc::system { /// @note Owns ProtocolRegistry, ProtocolLink, and MavlinkTelemetryRegistry. /// @note Protocol polling is handled externally. /// @note MAVLink callback is configured in init(). -struct ProtocolSystem : System, kf::mixin::Configurable { +struct ProtocolSystem : System { - using kf::mixin::Configurable::Configurable; + explicit ProtocolSystem(const Config &config) noexcept : + _protocol_registry{config.protocol_registry}, + _protocol_link{config.protocol_link} {} /// @brief Get mutable access to protocol link component protocol::ProtocolLink &link() noexcept { @@ -56,19 +57,17 @@ struct ProtocolSystem : System, kf::mixin::Configurable static constexpr auto logger{kf::Logger::create("ProtocolSystem")}; MavlinkTelemetryRegistry _mavlink_telemetry_registry{}; - protocol::ProtocolRegistry _protocol_registry{this->config().protocol_registry}; - protocol::ProtocolLink _protocol_link{this->config().protocol_link}; + protocol::ProtocolRegistry _protocol_registry; + protocol::ProtocolLink _protocol_link; kf::math::Milliseconds _poll_time{}; - KF_IMPL_INITABLE(ProtocolSystem, bool); - bool initImpl() noexcept { + DJC_IMPL_INITABLE(ProtocolSystem, void(protocol::ProtocolRegistry::Mode)); + void initImpl(protocol::ProtocolRegistry::Mode mode) noexcept { _protocol_registry.mavlink().callback([this](const auto &message) { _mavlink_telemetry_registry.update(static_cast(_poll_time), message); }); - _protocol_link.protocol(_protocol_registry.get(this->config().init_protocol_mode)); - - return true; + _protocol_link.protocol(_protocol_registry.get(mode)); } KF_IMPL_TIMED_POLLABLE(ProtocolSystem); diff --git a/DJC-Firmware/src/djc/system/TransportSystem.hpp b/DJC-Firmware/src/djc/system/TransportSystem.hpp index 9acd94f..6265e8e 100644 --- a/DJC-Firmware/src/djc/system/TransportSystem.hpp +++ b/DJC-Firmware/src/djc/system/TransportSystem.hpp @@ -6,10 +6,10 @@ #include #include -#include #include "djc/Config.hpp" #include "djc/system/System.hpp" +#include "djc/transport/Kind.hpp" #include "djc/transport/TransportLink.hpp" #include "djc/transport/TransportRegistry.hpp" @@ -17,9 +17,9 @@ namespace djc::system { /// @brief System managing transport, Wraps TransportRegistry and TransportLink /// @note Initializes WiFi STA mode, and polls the link for connection timeouts. -struct TransportSystem : System, kf::mixin::Configurable { +struct TransportSystem : System { - using kf::mixin::Configurable::Configurable; + explicit TransportSystem(const Config& config) noexcept {} /// @brief Get mutable access to transport link component transport::TransportLink &link() noexcept { @@ -45,19 +45,17 @@ struct TransportSystem : System, kf::mixin::Configurableconfig().transport_link}; + transport::TransportLink _transport_link{config.transport_link}; - KF_IMPL_INITABLE(TransportSystem, bool); - bool initImpl() noexcept { + DJC_IMPL_INITABLE(TransportSystem, void(transport::Kind)); + void initImpl(transport::Kind kind) noexcept { WiFi.mode(WIFI_MODE_STA); if (not _transport_registry.espnow().init()) { logger.error("failed to initialize espnow transport"); } - _transport_link.transport(_transport_registry.get(this->config().init_transport_kind)); - - return true; + _transport_link.transport(_transport_registry.get(kind)); } KF_IMPL_TIMED_POLLABLE(TransportSystem); From c2f89abc2bd7695a17959946230eed6171da6dfe Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:53:12 +0300 Subject: [PATCH 238/326] fix(ui): UI now implements service tag --- DJC-Firmware/src/djc/ui/UI.hpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index c57fcb7..3b9d95b 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -12,6 +12,7 @@ #include #include +#include "djc/service/Service.hpp" #include "djc/ui/UiTraits.hpp" #include "djc/ui/VirtualKeyboard.hpp" #include "djc/ui/widgets/PeerDisplay.hpp" @@ -32,8 +33,12 @@ namespace djc::ui { /// @brief ESP32-DJC extended UI specializalization /// @note djc::pages must use fields from this service (`UI::Color`, `UI::Widget`, etc.) -struct UI : internal::UiBase { +struct UI : + service::ServiceTag, + internal::UiBase + +{ explicit constexpr UI(Traits::RenderImpl &render_system, VirtualKeyboard &virtual_keyboard) noexcept : internal::UiBase{render_system}, _virtual_keyboard{virtual_keyboard} {} From cbe50e752e67b398c1a16e50e8de0301f7697e00 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:53:46 +0300 Subject: [PATCH 239/326] fix(mixin/initable): move macro to buttom --- DJC-Firmware/src/djc/mixin/Initable.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/mixin/Initable.hpp b/DJC-Firmware/src/djc/mixin/Initable.hpp index fb2d99f..22f99ce 100644 --- a/DJC-Firmware/src/djc/mixin/Initable.hpp +++ b/DJC-Firmware/src/djc/mixin/Initable.hpp @@ -30,6 +30,6 @@ template struct Initable { } }; -#define DJC_IMPL_INITABLE(__impl__, ...) friend struct ::djc::mixin::Initable<__impl__, __VA_ARGS__> +}// namespace djc::mixin -}// namespace djc::mixin \ No newline at end of file +#define DJC_IMPL_INITABLE(__impl__, ...) friend struct ::djc::mixin::Initable<__impl__, __VA_ARGS__> \ No newline at end of file From ca4501331d63411810bc204862249d275659fdf8 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:54:06 +0300 Subject: [PATCH 240/326] fix(system): use initable from djc --- DJC-Firmware/src/djc/system/System.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/system/System.hpp b/DJC-Firmware/src/djc/system/System.hpp index 44e4ad4..65c3979 100644 --- a/DJC-Firmware/src/djc/system/System.hpp +++ b/DJC-Firmware/src/djc/system/System.hpp @@ -17,8 +17,8 @@ struct SystemTag {}; template struct System : SystemTag, - djc::mixin::NonCopyable, - kf::mixin::Initable, + kf::mixin::NonCopyable, + djc::mixin::Initable, kf::mixin::TimedPollable {}; }// namespace djc::system \ No newline at end of file From 45db8803895f1a62ef54775c40db7bf4ad3fd917 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:55:14 +0300 Subject: [PATCH 241/326] fix(service): fix syntax errors --- DJC-Firmware/src/djc/system/GraphicsSystem.hpp | 6 +++--- DJC-Firmware/src/djc/system/TransportSystem.hpp | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index f990415..71ef6a6 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -20,19 +20,19 @@ template struct GraphicsSystem : using DisplayManagerImpl = service::DisplayManager; explicit GraphicsSystem(I &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : - mixin::ServiceOwner{DisplayManagerImpl{_display_driver, virtual_keyboard}} {} + mixin::ServiceOwner{DisplayManagerImpl{display_driver, virtual_keyboard}} {} private: using This = GraphicsSystem; DJC_IMPL_INITABLE(This, void()); void initImpl() noexcept { - _display_manager.init(); + this->service().init(); } KF_IMPL_TIMED_POLLABLE(This); void pollImpl(kf::math::Milliseconds now) noexcept { - _display_manager.poll(now); + this->service().poll(now); } }; diff --git a/DJC-Firmware/src/djc/system/TransportSystem.hpp b/DJC-Firmware/src/djc/system/TransportSystem.hpp index 6265e8e..f0df78b 100644 --- a/DJC-Firmware/src/djc/system/TransportSystem.hpp +++ b/DJC-Firmware/src/djc/system/TransportSystem.hpp @@ -19,7 +19,8 @@ namespace djc::system { /// @note Initializes WiFi STA mode, and polls the link for connection timeouts. struct TransportSystem : System { - explicit TransportSystem(const Config& config) noexcept {} + explicit TransportSystem(const Config &config) noexcept : + _transport_link{config.transport_link} {} /// @brief Get mutable access to transport link component transport::TransportLink &link() noexcept { @@ -45,7 +46,7 @@ struct TransportSystem : System { static constexpr auto logger{kf::Logger::create("TransportSystem")}; transport::TransportRegistry _transport_registry{}; - transport::TransportLink _transport_link{config.transport_link}; + transport::TransportLink _transport_link; DJC_IMPL_INITABLE(TransportSystem, void(transport::Kind)); void initImpl(transport::Kind kind) noexcept { From 6b274a7aa36a16b6d1c9afa5da1114d509a5c133 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:55:44 +0300 Subject: [PATCH 242/326] refactor(system/ui): add pages list as init argument --- DJC-Firmware/src/djc/system/UiSystem.hpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp index 85b4423..b8d5c8c 100644 --- a/DJC-Firmware/src/djc/system/UiSystem.hpp +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -3,10 +3,12 @@ #pragma once +#include + #include -#include "djc/mixin/ServiceOwner.hpp" #include "djc/Config.hpp" +#include "djc/mixin/ServiceOwner.hpp" #include "djc/system/System.hpp" #include "djc/ui/UI.hpp" #include "djc/ui/VirtualKeyboard.hpp" @@ -16,16 +18,16 @@ namespace djc::system { /// @brief System that owns UI components: renderer, virtual keyboard, UI instance and root page /// @note Initialises the UI with the root page and triggers an initial update event. Polls the UI on every main loop iteration -struct UiSystem : +struct UiSystem : -System, -mixin::ServiceOwner + System)>, + mixin::ServiceOwner { using Renderer = ui::UI::Traits::RenderImpl; explicit UiSystem(const Config &config) noexcept : - mixin::ServiceOwner{djc::ui::UI{_renderer, _virtual_keyboard}}, + mixin::ServiceOwner{djc::ui::UI{_renderer, _virtual_keyboard}}, _renderer{config.render_system} {} /// @brief Get mutable access to renderer component @@ -65,8 +67,12 @@ mixin::ServiceOwner ui::VirtualKeyboard _virtual_keyboard{}; ui::pages::RootPage _root_page{this->service()}; - DJC_IMPL_INITABLE(UiSystem, void()); - void initImpl() noexcept { + DJC_IMPL_INITABLE(UiSystem, void(std::initializer_list)); + void initImpl(std::initializer_list pages) noexcept { + for (auto page : pages) { + _root_page.attach(*page); + } + this->service().activePage(_root_page); this->service().addEvent(ui::UI::Traits::EventImpl::update()); } From 7560c22dabd675728e456ea2d873bc800c9e7a14 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 14:57:49 +0300 Subject: [PATCH 243/326] refactor(service): rename Control to ControlService --- .../src/djc/service/{Control.hpp => ControlService.hpp} | 6 +++--- DJC-Firmware/src/djc/system/ControlSystem.hpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename DJC-Firmware/src/djc/service/{Control.hpp => ControlService.hpp} (88%) diff --git a/DJC-Firmware/src/djc/service/Control.hpp b/DJC-Firmware/src/djc/service/ControlService.hpp similarity index 88% rename from DJC-Firmware/src/djc/service/Control.hpp rename to DJC-Firmware/src/djc/service/ControlService.hpp index ca349ed..1e0fa64 100644 --- a/DJC-Firmware/src/djc/service/Control.hpp +++ b/DJC-Firmware/src/djc/service/ControlService.hpp @@ -16,9 +16,9 @@ namespace djc::service { /// @note /// Stores the current joystick input and a flag that enables/disables output. /// When enabled and the transport is connected, `pollImpl()` forwards the input to the active protocol via `ProtocolLink::poll()`. -struct Control final : Service { +struct ControlService final : Service { - explicit Control(transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : + explicit ControlService(transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : _transport_link{transport_link}, _protocol_link{protocol_link} {} /// @brief Returns the current manual input values. @@ -47,7 +47,7 @@ struct Control final : Service { ManualInput _manual_input{}; bool _enabled{false}; - KF_IMPL_TIMED_POLLABLE(Control); + KF_IMPL_TIMED_POLLABLE(ControlService); void pollImpl(kf::math::Milliseconds now) noexcept { if (_enabled and _transport_link.connected()) { _protocol_link.poll(now, _manual_input, _transport_link); diff --git a/DJC-Firmware/src/djc/system/ControlSystem.hpp b/DJC-Firmware/src/djc/system/ControlSystem.hpp index 94a985a..4659b02 100644 --- a/DJC-Firmware/src/djc/system/ControlSystem.hpp +++ b/DJC-Firmware/src/djc/system/ControlSystem.hpp @@ -18,11 +18,11 @@ namespace djc::system { struct ControlSystem : System, - mixin::ServiceOwner + mixin::ServiceOwner { explicit ControlSystem(Periphery &periphery, transport::TransportLink &transport_link, protocol::ProtocolLink &protocol_link) noexcept : - mixin::ServiceOwner{service::Control{transport_link, protocol_link}}, _periphery{periphery} {} + mixin::ServiceOwner{service::ControlService{transport_link, protocol_link}}, _periphery{periphery} {} private: Periphery &_periphery; From b615106aac9cec33ca4a13a4ebd1005fe8a5646a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 15:10:16 +0300 Subject: [PATCH 244/326] chore: set build cache dir --- DJC-Firmware/platformio.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/DJC-Firmware/platformio.ini b/DJC-Firmware/platformio.ini index c3e667d..900684e 100644 --- a/DJC-Firmware/platformio.ini +++ b/DJC-Firmware/platformio.ini @@ -1,3 +1,6 @@ +[platformio] +build_cache_dir = .pio/build_cache + [env:esp32dev] platform = espressif32 board = esp32dev From 87be3595e1c9114f0cd11a26026581d2ca9a893f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 15:10:49 +0300 Subject: [PATCH 245/326] fix(system/control): fix include path --- DJC-Firmware/src/djc/system/ControlSystem.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/system/ControlSystem.hpp b/DJC-Firmware/src/djc/system/ControlSystem.hpp index 4659b02..250ff33 100644 --- a/DJC-Firmware/src/djc/system/ControlSystem.hpp +++ b/DJC-Firmware/src/djc/system/ControlSystem.hpp @@ -7,7 +7,7 @@ #include "djc/Periphery.hpp" #include "djc/mixin/ServiceOwner.hpp" #include "djc/protocol/ProtocolLink.hpp" -#include "djc/service/Control.hpp" +#include "djc/service/ControlService.hpp" #include "djc/system/System.hpp" #include "djc/transport/TransportLink.hpp" From 61a0139a17fa21163f205690616712fa985f5859 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 15:40:49 +0300 Subject: [PATCH 246/326] refactor(ui/pages): config page no longer initable --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 52f7344..2fabafb 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -5,7 +5,6 @@ #include #include -#include #include "djc/Config.hpp" #include "djc/PeerFavoritesRegistry.hpp" @@ -17,7 +16,7 @@ namespace djc::ui::pages { -struct ConfigPage : UI::Page, kf::mixin::Initable { +struct ConfigPage : UI::Page { explicit ConfigPage( UI &ui, @@ -215,14 +214,14 @@ struct ConfigPage : UI::Page, kf::mixin::Initable { return kf::Slice{_layout.data(), _layout.size()}.first(layout_regular_widgets + displayed_peers); } - // impl - KF_IMPL_INITABLE(ConfigPage, void); - void initImpl() noexcept { - const auto &config = _config_service.config(); - _default_protocol_mode_selector.value(config.init_protocol_mode); - _default_transport_kind_selector.value(config.init_transport_kind); - _autoconnect_enabled_input.value(config.auto_connect_service.enabled); - } + // TODO: fetch config when needed + // KF_IMPL_INITABLE(ConfigPage, void); + // void initImpl() noexcept { + // const auto &config = _config_service.config(); + // _default_protocol_mode_selector.value(config.init_protocol_mode); + // _default_transport_kind_selector.value(config.init_transport_kind); + // _autoconnect_enabled_input.value(config.auto_connect_service.enabled); + // } }; }// namespace djc::ui::pages \ No newline at end of file From f12e3a6b2a6fa28667ac9ff644e60748373dc775 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 15:41:09 +0300 Subject: [PATCH 247/326] fix(system): use pollImpl --- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 2 +- DJC-Firmware/src/djc/system/PeerSystem.hpp | 2 +- DJC-Firmware/src/djc/system/PeripherySystem.hpp | 2 +- DJC-Firmware/src/djc/system/ProtocolSystem.hpp | 2 +- DJC-Firmware/src/djc/system/TransportSystem.hpp | 2 +- DJC-Firmware/src/djc/system/UiSystem.hpp | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index e3f8c37..6506748 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -28,7 +28,7 @@ struct ConfigSystem : } KF_IMPL_TIMED_POLLABLE(ConfigSystem); - void poll(kf::math::Milliseconds now) noexcept { + void pollImpl(kf::math::Milliseconds now) noexcept { this->service().poll(now); } }; diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp index f300f5e..7a213ab 100644 --- a/DJC-Firmware/src/djc/system/PeerSystem.hpp +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -79,7 +79,7 @@ struct PeerSystem : } KF_IMPL_TIMED_POLLABLE(PeerSystem); - void poll(kf::math::Milliseconds now) noexcept { + void pollImpl(kf::math::Milliseconds now) noexcept { _peer_scanning_service.poll(now); if (_auto_connect_service.config().enabled and _auto_connect_service.target().isNone()) { diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp index 912befe..1412c6e 100644 --- a/DJC-Firmware/src/djc/system/PeripherySystem.hpp +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -44,7 +44,7 @@ struct PeripherySystem : System { } KF_IMPL_TIMED_POLLABLE(PeripherySystem); - void poll(kf::math::Milliseconds now) noexcept {} + void pollImpl(kf::math::Milliseconds now) noexcept {} }; }// namespace djc::system \ No newline at end of file diff --git a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp index c8582d0..f2a4561 100644 --- a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp +++ b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp @@ -71,7 +71,7 @@ struct ProtocolSystem : System { } KF_IMPL_TIMED_POLLABLE(TransportSystem); - void poll(kf::math::Milliseconds now) noexcept { + void pollImpl(kf::math::Milliseconds now) noexcept { _transport_link.poll(now); } }; diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp index b8d5c8c..d89592b 100644 --- a/DJC-Firmware/src/djc/system/UiSystem.hpp +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -78,7 +78,7 @@ struct UiSystem : } KF_IMPL_TIMED_POLLABLE(UiSystem); - void poll(kf::math::Milliseconds now) noexcept { + void pollImpl(kf::math::Milliseconds now) noexcept { this->service().poll(now); } }; From 548421f6eaac06cd9889808519718cfad596c8df Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 16:09:44 +0300 Subject: [PATCH 248/326] perf(system/periphery): remove logger and wrapper --- DJC-Firmware/src/djc/system/PeripherySystem.hpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp index 1412c6e..0063c66 100644 --- a/DJC-Firmware/src/djc/system/PeripherySystem.hpp +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -3,8 +3,6 @@ #pragma once -#include - #include "djc/Config.hpp" #include "djc/Periphery.hpp" #include "djc/system/System.hpp" @@ -29,18 +27,11 @@ struct PeripherySystem : System { } private: - static constexpr auto logger{kf::Logger::create("PeripherySystem")}; - Periphery _periphery; DJC_IMPL_INITABLE(PeripherySystem, bool()); bool initImpl() noexcept { - if (_periphery.init()) { - return true; - } else { - logger.error("Periphery init failed"); - return false; - } + return _periphery.init(); } KF_IMPL_TIMED_POLLABLE(PeripherySystem); From 574005259df25bda5f343c5d974549fb44769f15 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 16:17:50 +0300 Subject: [PATCH 249/326] refactor: periphery.init now returns void --- DJC-Firmware/src/djc/Periphery.hpp | 7 ++----- DJC-Firmware/src/djc/system/PeripherySystem.hpp | 8 ++++---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index 17e313c..f40d518 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -147,8 +147,8 @@ struct Periphery final : static constexpr auto logger = kf::Logger::create("Periphery"); // impl - KF_IMPL_INITABLE(Periphery, bool); - bool initImpl() noexcept { + KF_IMPL_INITABLE(Periphery, void); + void initImpl() noexcept { logger.info("Initializing peripherals"); left_joystick.init(); @@ -163,9 +163,6 @@ struct Periphery final : if (display.init().isError()) { logger.error("Display driver initialization failed"); } - - logger.info("Peripherals initialized successfully"); - return true; } }; diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp index 0063c66..500c9d8 100644 --- a/DJC-Firmware/src/djc/system/PeripherySystem.hpp +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -11,7 +11,7 @@ namespace djc::system { /// @brief System that owns and initializes hardware peripherals /// @note Provides access to peripherals for other systems via getters; no periodic polling needed. -struct PeripherySystem : System { +struct PeripherySystem : System { explicit PeripherySystem(const Config &config) noexcept : _periphery{config.periphery} {} @@ -29,9 +29,9 @@ struct PeripherySystem : System { private: Periphery _periphery; - DJC_IMPL_INITABLE(PeripherySystem, bool()); - bool initImpl() noexcept { - return _periphery.init(); + DJC_IMPL_INITABLE(PeripherySystem, void()); + void initImpl() noexcept { + _periphery.init(); } KF_IMPL_TIMED_POLLABLE(PeripherySystem); From d33769ccb70a628398bf3bc2f4fe158ea243d5e9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 16:26:58 +0300 Subject: [PATCH 250/326] fix(periphery): use initable --- DJC-Firmware/src/djc/Periphery.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index f40d518..4b84b1b 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -75,7 +75,7 @@ struct PeripheryConfig final { struct Periphery final : kf::mixin::NonCopyable, - kf::mixin::Initable, + kf::mixin::Initable, kf::mixin::Configurable { From a6fdab06f4c25bb1995dd92c138e2535bb09bef4 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 16:37:10 +0300 Subject: [PATCH 251/326] refactor(peer-favorites-registry): remove initable mixin --- DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp | 15 ++++----------- DJC-Firmware/src/djc/system/PeerSystem.hpp | 1 - 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp index 884237a..8fcbbe0 100644 --- a/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp +++ b/DJC-Firmware/src/djc/PeerFavoritesRegistry.hpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -14,13 +13,13 @@ namespace djc { -/// @brief Registry of favorite peers (bookmarks). +/// @brief Registry of favorite peers /// /// Maintains a list of peer entries inside an externally‑provided array of slots. /// The registry does not own the memory, only manipulates it. /// /// @note All methods are safe to call from different UI callbacks as long as the underlying storage is exclusively owned by the registry. -struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable { // TODO: remove initable +struct PeerFavoritesRegistry final : kf::mixin::NonCopyable { /// @brief A single favorite‑peer record. struct Entry final { @@ -42,8 +41,6 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } }; - explicit constexpr PeerFavoritesRegistry(kf::Slice> entries = {}) noexcept : _entries{entries} {} - /// @brief Set entries source void entries(kf::Slice> new_entries) noexcept { _entries = new_entries; @@ -54,7 +51,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } } - /// @brief Return the entire slot array, including empty slots. + /// @brief Return all non-empty entries [[nodiscard]] auto all() const noexcept -> kf::Slice> { return {_entries.data(), _active_count}; } @@ -113,7 +110,7 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } private: - kf::Slice> _entries; + kf::Slice> _entries{}; kf::usize _active_count{0}; /// @brief Find the index of an entry by address. @@ -128,10 +125,6 @@ struct PeerFavoritesRegistry final : kf::mixin::NonCopyable, kf::mixin::Initable } return kf::none; } - - // TODO: remove initable - KF_IMPL_INITABLE(PeerFavoritesRegistry, void); - void initImpl() noexcept {} }; }// namespace djc \ No newline at end of file diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp index 7a213ab..7e98950 100644 --- a/DJC-Firmware/src/djc/system/PeerSystem.hpp +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -69,7 +69,6 @@ struct PeerSystem : DJC_IMPL_INITABLE(PeerSystem, void(transport::TransportLink &)); void initImpl(transport::TransportLink &transport_link) noexcept { - _peer_favorites_registry.init(); _peer_scanning_service.init(); _auto_connect_service.callback([&transport_link](const auto &address) -> void { From a45a5860c1a49555ee7f191995e82bee60a85be2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 16:52:43 +0300 Subject: [PATCH 252/326] fix(ui/pages/config): add config into widget valued fetch on entry --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 2fabafb..a2f30df 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -73,11 +73,13 @@ struct ConfigPage : UI::Page { _default_protocol_mode_selector.callback([this](Mode mode) { _config_service.config().init_protocol_mode = mode; + _config_service.requestSave(); }); _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); _autoconnect_enabled_input.callback([this](bool value) { _config_service.config().auto_connect_service.enabled = value; + _config_service.requestSave(); }); _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); @@ -93,9 +95,12 @@ struct ConfigPage : UI::Page { } } - // TODO: add onPoll with sync - void onEntry() noexcept override { + const auto &config = _config_service.config(); + _default_protocol_mode_selector.value(config.init_protocol_mode); + _default_transport_kind_selector.value(config.init_transport_kind); + _autoconnect_enabled_input.value(config.auto_connect_service.enabled); + const auto all_favorites = _peer_favoriter_registry.all(); (void) _label_favorites_buffer.format( @@ -213,15 +218,6 @@ struct ConfigPage : UI::Page { kf::Slice layout(kf::usize displayed_peers) noexcept { return kf::Slice{_layout.data(), _layout.size()}.first(layout_regular_widgets + displayed_peers); } - - // TODO: fetch config when needed - // KF_IMPL_INITABLE(ConfigPage, void); - // void initImpl() noexcept { - // const auto &config = _config_service.config(); - // _default_protocol_mode_selector.value(config.init_protocol_mode); - // _default_transport_kind_selector.value(config.init_transport_kind); - // _autoconnect_enabled_input.value(config.auto_connect_service.enabled); - // } }; }// namespace djc::ui::pages \ No newline at end of file From b8359d3294983a51d8c001365ac2fa90461abb05 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 16:59:58 +0300 Subject: [PATCH 253/326] fix(service/config): set sync period to 20s --- DJC-Firmware/src/djc/service/ConfigService.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index e82a90c..3297b66 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -86,7 +86,7 @@ struct ConfigService : Service { static constexpr auto logger{kf::Logger::create("ConfigService")}; static constexpr kf::math::Timer::Config sync_timer_config{ - .period = 5'000, + .period = 20'000, }; kf::memory::Storage _storage{ From 9f113a3ca21c73fb13d018bf4900c02332a87828 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 17:00:36 +0300 Subject: [PATCH 254/326] fix(ui/pages/config): save button now forced config sync --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index a2f30df..98a6c02 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -46,6 +46,7 @@ struct ConfigPage : UI::Page { _save_config_button.callback([this]() { _config_service.requestSave(); + _config_service.sync(); }); _save_config_button.hint("Write config from RAM into NVS"); From b0dadbe4444cd1874b27e09d3214d01c836a7715 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 17:01:50 +0300 Subject: [PATCH 255/326] refactor(main): use systems --- DJC-Firmware/src/main.cpp | 425 ++++++++++++++------------------------ 1 file changed, 157 insertions(+), 268 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index efd428b..a1587fe 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -3,364 +3,253 @@ // framework #include -#include -// lib +// toolkit #include +#include +#include // djc -#include "djc/ConfigManager.hpp" -#include "djc/ManualInput.hpp" -#include "djc/MavlinkTelemetryRegistry.hpp" -#include "djc/PeerFavoritesRegistry.hpp" -#include "djc/Periphery.hpp" #include "djc/prelude.hpp" -// djc::transport -#include "djc/transport/TransportLink.hpp" -#include "djc/transport/TransportRegistry.hpp" - -// djc::protocol -#include "djc/protocol/ProtocolLink.hpp" -#include "djc/protocol/ProtocolRegistry.hpp" - -// djc::service -#include "djc/service/AutoConnectService.hpp" -#include "djc/service/Control.hpp" -#include "djc/service/DisplayManager.hpp" -#include "djc/service/InputHandler.hpp" -#include "djc/service/PeerScanningService.hpp" +// djc::system +#include "djc/system/ConfigSystem.hpp" +#include "djc/system/ControlSystem.hpp" +#include "djc/system/GraphicsSystem.hpp" +#include "djc/system/InputSystem.hpp" +#include "djc/system/PeerSystem.hpp" +#include "djc/system/PeripherySystem.hpp" +#include "djc/system/ProtocolSystem.hpp" +#include "djc/system/TransportSystem.hpp" +#include "djc/system/UiSystem.hpp" // djc::ui #include "djc/ui/UI.hpp" -#include "djc/ui/VirtualKeyboard.hpp" - -// djc::ui::pages #include "djc/ui/pages/ConfigPage.hpp" #include "djc/ui/pages/MavlinkTelemetryPage.hpp" #include "djc/ui/pages/PeerExplorerPage.hpp" #include "djc/ui/pages/RawProtocolPage.hpp" #include "djc/ui/pages/RootPage.hpp" -using UiEvent = djc::ui::UI::Traits::EventImpl; +static constexpr auto loop_rate_hz{50}; static constexpr auto logger{kf::Logger::create("main")}; -static djc::ConfigManager config_manager{}; - -static djc::Periphery periphery{ - config_manager.config().periphery, -}; - -static djc::transport::TransportLink transport_link{ - config_manager.config().transport_link, -}; - -static djc::transport::TransportRegistry transport_registry{}; - -static djc::protocol::ProtocolLink protocol_link{ - config_manager.config().protocol_link, -}; - -static djc::protocol::ProtocolRegistry protocol_registry{ - config_manager.config().protocol_registry, -}; - -static djc::MavlinkTelemetryRegistry mavlink_telemetry_registry{}; +// systems -static djc::PeerFavoritesRegistry peer_favoriter_registry{ - {config_manager.config().peer_favorites.data(), config_manager.config().peer_favorites.size()}, -}; +static djc::system::ConfigSystem config_system{}; -static djc::ui::VirtualKeyboard virtual_keyboard{}; +static auto &config{config_system.service().config()}; -// services +static djc::system::PeripherySystem periphery_system{config}; -static djc::service::InputHandler input_handler{ - config_manager.config().input_handler, - periphery.right_joystick, - periphery.left_button_listener, - periphery.right_button_listener, -}; +static djc::system::TransportSystem transport_system{config}; -static djc::service::PeerScanningService peer_scanner{ - config_manager.config().peer_scanner, - transport_link, -}; +static djc::system::ProtocolSystem protocol_system{config}; -static djc::service::AutoConnectService auto_connect_service{ - config_manager.config().auto_connect_service, - transport_link, +static djc::system::ControlSystem control_system{ + periphery_system.periphery(), + transport_system.link(), + protocol_system.link(), }; -static djc::service::Control control{ - transport_link, - protocol_link, +static djc::system::PeerSystem peer_system{ + config, + transport_system.link(), }; -static djc::service::DisplayManager display_manager{ - periphery.display, - virtual_keyboard, +static djc::system::InputSystem input_system{ + config, + periphery_system.periphery(), }; -static djc::ui::UI::Traits::RenderImpl ui_render{ - config_manager.config().render_system, -}; +static djc::system::UiSystem ui_system{config}; -static djc::ui::UI ui{ - ui_render, - virtual_keyboard, +static djc::system::GraphicsSystem graphics_system{ + periphery_system.periphery().display, + ui_system.virtualKeyboard(), }; -// pages - -static djc::ui::pages::RootPage root_page{ - ui, -}; +// ui pages static djc::ui::pages::PeerExplorerPage peer_explorer_page{ - ui, - root_page, - transport_link, - peer_scanner, - peer_favoriter_registry, + ui_system.service(), + ui_system.rootPage(), + transport_system.link(), + peer_system.scanningService(), + peer_system.favoritesRegistry(), }; static djc::ui::pages::MavlinkTelemetryPage mavlink_telemetry_page{ - ui, - root_page, - protocol_registry, - protocol_link, - mavlink_telemetry_registry, + ui_system.service(), + ui_system.rootPage(), + protocol_system.protocolRegistry(), + protocol_system.link(), + protocol_system.mavlinkTelemetryRegistry(), }; static djc::ui::pages::RawProtocolPage raw_protocol_page{ - ui, - root_page, - protocol_registry, - protocol_link, - transport_link, + ui_system.service(), + ui_system.rootPage(), + protocol_system.protocolRegistry(), + protocol_system.link(), + transport_system.link(), }; static djc::ui::pages::ConfigPage config_page{ - ui, - root_page, - config_manager, - peer_favoriter_registry, + ui_system.service(), + ui_system.rootPage(), + config_system.service(), + peer_system.favoritesRegistry(), }; -static void setupTransport() noexcept { - WiFi.mode(WIFI_MODE_STA); +// navigation to event maps - if (not transport_registry.espnow().init()) { - logger.error("failed to initialize espnow transport"); - } +using UiEvent = djc::ui::UI::Traits::EventImpl; - transport_link.transport(transport_registry.get(config_manager.config().init_transport_kind)); +static constexpr UiEvent navigation_event_from_direction[4]{ + UiEvent::pageCursorMove(-1),// Up + UiEvent::pageCursorMove(+1),// Down + UiEvent::widgetValue(-1), // Left + UiEvent::widgetValue(+1), // Right +}; - transport_link.onReceive([](const auto &, auto buffer) { - protocol_link.receive(buffer); - }); -} +static constexpr UiEvent virtual_keyboard_event_from_direction[4]{ + UiEvent::widgetValue(0),// Up + UiEvent::widgetValue(1),// Down + UiEvent::widgetValue(2),// Left + UiEvent::widgetValue(3),// Right +}; -static void setupProtocol() noexcept { - protocol_registry.mavlink().callback([](const auto &message) { - mavlink_telemetry_registry.update(static_cast(millis()), message); - }); +// callbacks - protocol_link.protocol(protocol_registry.get(config_manager.config().init_protocol_mode)); -} +static void onReceiveFromPeer(const djc::transport::PeerAddress &address, kf::Slice buffer) noexcept { + (void) address; -static void setupInput() noexcept { - static constexpr UiEvent navigation_event_from_direction[4] = { - UiEvent::pageCursorMove(-1),// Up - UiEvent::pageCursorMove(+1),// Down - UiEvent::widgetValue(-1), // Left - UiEvent::widgetValue(+1), // Right - }; - - static constexpr UiEvent virtual_keyboard_event_from_direction[4] = { - UiEvent::widgetValue(0),// Up - UiEvent::widgetValue(1),// Down - UiEvent::widgetValue(2),// Left - UiEvent::widgetValue(3),// Right - }; - - input_handler.onLeftButton([]() { - if (virtual_keyboard.active()) { - virtual_keyboard.quit(); - } else { - control.enabled(not control.enabled()); - } + protocol_system.link().receive(buffer); +} - ui.addEvent(UiEvent::update()); - }); +static void onReceiveFromLogger(kf::memory::StringView str) noexcept { + Serial.write(str.data(), str.size()); +} - input_handler.onRightButton([]() { - if (control.enabled()) { return; } +static void onPrimaryButtonClick() noexcept { + if (control_system.service().enabled()) { return; } - ui.addEvent(UiEvent::widgetClick()); - }); + ui_system.service().addEvent(UiEvent::widgetClick()); +} - input_handler.onDirection([](auto direction) { - if (control.enabled()) { return; } +static void onSecondaryButtonClick() noexcept { + if (ui_system.virtualKeyboard().active()) { + ui_system.virtualKeyboard().quit(); + } else { + control_system.service().enabled(not control_system.service().enabled()); + } - const auto table = virtual_keyboard.active() ? virtual_keyboard_event_from_direction : navigation_event_from_direction; - ui.addEvent(table[static_cast(direction)]); - }); + ui_system.service().addEvent(UiEvent::update()); } -static void setupUI() noexcept { - config_page.init(); +static void onPrimaryJoystickDirection(djc::service::InputHandler::JoystickListener::Direction direction) noexcept { + if (control_system.service().enabled()) { return; } - // apply page links - root_page.attach(peer_explorer_page); - root_page.attach(mavlink_telemetry_page); - root_page.attach(raw_protocol_page); - root_page.attach(config_page); + const auto table = ui_system.virtualKeyboard().active() ? virtual_keyboard_event_from_direction : navigation_event_from_direction; + ui_system.service().addEvent(table[static_cast(direction)]); +} - ui_render.callback([](auto str) -> void { - using Palette = decltype(display_manager)::Palette; +static void onUiRendered(kf::memory::StringView str) { + using Palette = std::decay_t::Palette; - if (control.enabled()) { - const auto status = transport_link.connected() ? transport_link.activePeerAddress().unwrap().toString().view() : kf::memory::StringView{"Disconnected"}; - display_manager.overlay(status, Palette::light_yellow); + if (control_system.service().enabled()) { + if (transport_system.link().connected()) { + graphics_system.service().overlay(transport_system.link().activePeerAddress().unwrap().toString().view(), Palette::light_green); } else { - if (const auto &p = ui.activePage(); p.isSome()) { - if (const auto &widget = p.unwrap().selectedWidget(); widget.isSome()) { - display_manager.overlay(widget.unwrap().hint(), Palette::light_gray); - } + graphics_system.service().overlay("Disconnected", Palette::light_yellow); + } + } else { + if (const auto &p = ui_system.service().activePage(); p.isSome()) { + if (const auto &widget = p.unwrap().selectedWidget(); widget.isSome()) { + graphics_system.service().overlay(widget.unwrap().hint(), Palette::light_gray); } } - - display_manager.onRender(str); - }); - - ui.activePage(root_page); - ui.addEvent(UiEvent::update()); -} - -static void setupGraphics() noexcept { - display_manager.init(); - - if (const auto &canvas = display_manager.canvas(); canvas.isSome()) { - auto &ui_render_config = config_manager.config().render_system; - ui_render_config.text.row_max_length = canvas.unwrap().widthInGlyphs(); - ui_render_config.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; - config_manager.modified(true); } + + graphics_system.service().onRender(str); } -static void setupPeriphery() noexcept { - if (not periphery.init()) { - logger.error("Periphery init failed. Resseting periphery config to defaults"); - config_manager.config().periphery = djc::Periphery::Config::defaults(); - config_manager.modified(true); - } +// setups - if (not config_manager.config().periphery.joystick_axes_tuned) { +static bool setupPeriphery(djc::Config &config) noexcept { + if (not config.periphery.joystick_axes_tuned) { logger.debug("Tunning axes.."); - periphery.tune(config_manager.config().periphery); - config_manager.modified(true); + periphery_system.periphery().tune(config.periphery); + return true; } -} - -static void setupBase() noexcept { - Serial.begin(115200); - kf::Logger::writer = [](auto str) { Serial.write(str.data(), str.size()); }; - config_manager.load(); - peer_favoriter_registry.init(); + return false; } -static void finalizeBase() noexcept { - if (config_manager.modified()) { - config_manager.save(); +static bool setupGraphics(djc::Config &config) noexcept { + if (const auto &canvas = graphics_system.service().canvas(); canvas.isSome()) { + config.render_system.text.row_max_length = canvas.unwrap().widthInGlyphs(); + config.render_system.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; + return true; } -} - -static void setupServices() noexcept { - peer_scanner.init(); - auto_connect_service.callback([](const auto &address) -> void { - logger.info("Auto Connect"); - (void) transport_link.connect(address); - }); + return false; } -static void updateAutoConnect(kf::math::Milliseconds now) noexcept { - if (auto_connect_service.config().enabled and auto_connect_service.target().isNone()) { - const auto favorites = peer_favoriter_registry.all(); +#define DJC_SYSTEM_INIT(__system_instance__, ...) \ + __system_instance__.init(__VA_ARGS__); \ + logger.info("done: '" #__system_instance__ "'"); - if (favorites.size() > 0) { - auto most_trusted_favorite_index = 0u; - - for (auto index = 1u; index < favorites.size(); index += 1) { - if (favorites[index].isSome() and favorites[most_trusted_favorite_index].isSome() and favorites[index].unwrap().trust > favorites[most_trusted_favorite_index].unwrap().trust) { - most_trusted_favorite_index = index; - } - } +#define DJC_DO_SETUP(__setup_function__) \ + if (__setup_function__) { config_system.service().requestSave(); } - if (const auto &most_trusted = favorites[most_trusted_favorite_index]; most_trusted.isSome()) { - for (const auto &peer: peer_scanner.peers()) { - if (peer.isSome() and peer.unwrap().address == most_trusted.unwrap().address) { - auto_connect_service.target(most_trusted.unwrap().address); - break; - } - } - } - } - } - auto_connect_service.poll(now); -} +void setup() { + Serial.begin(115200); + kf::Logger::writer = onReceiveFromLogger; -static void updateControl(kf::math::Microseconds now) noexcept { - if (control.enabled()) { - using I = djc::ManualInput; + // init - const I control_input{ - .left_x = I::fromNormalized(periphery.left_joystick.axis_x.read()), - .left_y = I::fromNormalized(periphery.left_joystick.axis_y.read()), - .right_x = I::fromNormalized(periphery.right_joystick.axis_x.read()), - .right_y = I::fromNormalized(periphery.right_joystick.axis_y.read()), - }; + DJC_SYSTEM_INIT(config_system); + DJC_SYSTEM_INIT(periphery_system); + DJC_SYSTEM_INIT(transport_system, config.init_transport_kind); + DJC_SYSTEM_INIT(protocol_system, config.init_protocol_mode); + DJC_SYSTEM_INIT(peer_system, transport_system.link()); + DJC_SYSTEM_INIT(input_system); + DJC_SYSTEM_INIT(graphics_system); + DJC_SYSTEM_INIT(control_system); + DJC_SYSTEM_INIT(ui_system, {&peer_explorer_page, &mavlink_telemetry_page, &raw_protocol_page, &config_page}); - // todo debug log + // orcestre - control.input(control_input); - } - control.poll(now); -} + transport_system.link().onReceive(onReceiveFromPeer); + peer_system.favoritesRegistry().entries({config.peer_favorites.data(), config.peer_favorites.size()}); -static void pollServices(kf::math::Milliseconds now) noexcept { - input_handler.poll(now); - transport_link.poll(now); - peer_scanner.poll(now); - updateAutoConnect(now); - updateControl(now); - ui.poll(now); - display_manager.poll(now); -} + input_system.service().onLeftButton(onSecondaryButtonClick); + input_system.service().onRightButton(onPrimaryButtonClick); + input_system.service().onDirection(onPrimaryJoystickDirection); -void setup() { - setupBase(); + ui_system.renderer().callback(onUiRendered); - setupPeriphery(); - setupInput(); - setupGraphics(); + DJC_DO_SETUP(setupPeriphery(config)); + DJC_DO_SETUP(setupGraphics(config)); +} - setupTransport(); - setupProtocol(); +void loop() { + constexpr kf::math::Milliseconds loop_period{1000 / loop_rate_hz}; - setupServices(); - setupUI(); + const auto now = static_cast(millis()); - finalizeBase(); -} + config_system.poll(now); + periphery_system.poll(now); + transport_system.poll(now); + protocol_system.poll(now); + peer_system.poll(now); + input_system.poll(now); + control_system.poll(now); + ui_system.poll(now); + graphics_system.poll(now); -void loop() { - constexpr kf::math::Milliseconds loop_period{1000 / 50};// 50 Hz delay(loop_period); - - pollServices(static_cast(millis())); } \ No newline at end of file From 640218a6f30bb8f2ef1fcee538bc83c45128e329 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 17:08:07 +0300 Subject: [PATCH 256/326] fix(ui/pages/config): add config force sync at reset and load buttons --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 98a6c02..1458681 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -52,11 +52,13 @@ struct ConfigPage : UI::Page { _load_config_button.callback([this]() { _config_service.requestLoad(); + _config_service.sync(); }); _load_config_button.hint("Load config from NVS into RAM"); _reset_config_button.callback([this]() { _config_service.requestReset(); + _config_service.sync(); }); _reset_config_button.hint("Set RAM config as detaults"); From 203bc892942d6102dad855d865467e910920e516 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 15 Jun 2026 17:12:34 +0300 Subject: [PATCH 257/326] style(ui/pages): move widget hint above callback set --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 18 +++++++++--------- .../src/djc/ui/pages/PeerDetailPage.hpp | 2 +- .../src/djc/ui/pages/PeerExplorerPage.hpp | 6 +++--- .../src/djc/ui/pages/PeerFavoritePage.hpp | 7 +++---- .../src/djc/ui/pages/RawProtocolPage.hpp | 2 +- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 1458681..52b3e24 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -41,60 +41,60 @@ struct ConfigPage : UI::Page { }} { widgets(layout(0)); - _device_name_input.source({_config_service.config().device_name.data(), _config_service.config().device_name.size()}); _device_name_input.hint("Device name"); + _device_name_input.source({_config_service.config().device_name.data(), _config_service.config().device_name.size()}); + _save_config_button.hint("Write config from RAM into NVS"); _save_config_button.callback([this]() { _config_service.requestSave(); _config_service.sync(); }); - _save_config_button.hint("Write config from RAM into NVS"); + _load_config_button.hint("Load config from NVS into RAM"); _load_config_button.callback([this]() { _config_service.requestLoad(); _config_service.sync(); }); - _load_config_button.hint("Load config from NVS into RAM"); + _reset_config_button.hint("Set RAM config as detaults"); _reset_config_button.callback([this]() { _config_service.requestReset(); _config_service.sync(); }); - _reset_config_button.hint("Set RAM config as detaults"); + _favorite_peers_fold_toggle_button.hint("Toggle folding"); _favorite_peers_fold_toggle_button.callback([this]() { show_favorites = not show_favorites; this->onEntry(); update(); }); - _favorite_peers_fold_toggle_button.hint("Toggle folding"); + _labeled_default_transport_kind_selector.hint("Define transport select after init"); _default_transport_kind_selector.callback([this](transport::Kind kind) { _config_service.config().init_transport_kind = kind; }); - _labeled_default_transport_kind_selector.hint("Define transport select after init"); + _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); _default_protocol_mode_selector.callback([this](Mode mode) { _config_service.config().init_protocol_mode = mode; _config_service.requestSave(); }); - _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); + _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); _autoconnect_enabled_input.callback([this](bool value) { _config_service.config().auto_connect_service.enabled = value; _config_service.requestSave(); }); - _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { auto &display = _peer_favorite_displays[i]; _layout[layout_regular_widgets + i] = &display; + display.hint("Open peer config"); display.callback([this](const transport::PeerAddress &address) -> void { _peer_favorite_page.bindPeer(address); _ui.activePage(_peer_favorite_page); }); - display.hint("Open peer config"); } } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index 55f439e..a19fa65 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -48,6 +48,7 @@ struct PeerDetailPage final : UI::Page { } }); + _peer_favorite_button.hint("Open peer favorites edit"); _peer_favorite_button.callback([this]() -> void { if (_peer_address.isNone()) { return; } this->label("Back"); @@ -55,7 +56,6 @@ struct PeerDetailPage final : UI::Page { _ui.activePage(_peer_favorite_page); update(); }); - _peer_favorite_button.hint("Open peer favorites edit"); } void bindPeer(const transport::PeerAddress &address) noexcept { diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 5254254..312fb85 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -42,11 +42,11 @@ struct PeerExplorerPage : UI::Page { auto &display = _peer_displays[i]; _layout[i + peer_display_start_index] = &display; + display.hint("Click for details"); display.callback([this](const transport::PeerAddress &address) -> void { _peer_detail_page.bindPeer(address); _ui.activePage(_peer_detail_page); }); - display.hint("Click for details"); } _primary_connection_status_button.callback([this]() { @@ -67,12 +67,12 @@ struct PeerExplorerPage : UI::Page { if (_transport_link.activePeerAddress().isSome()) { (void) _connection_button_buffer.format("%s", _transport_link.activePeerAddress().unwrap().toString().data()); _primary_connection_status_button.label(_connection_button_buffer.view()); - _primary_connection_status_button.style({UI::Color::Normal, UI::Color::Success}); _primary_connection_status_button.hint("Click to disconnect"); + _primary_connection_status_button.style({UI::Color::Normal, UI::Color::Success}); } else { _primary_connection_status_button.label("Disconnected"); - _primary_connection_status_button.style({UI::Color::Disabled, UI::Color::Normal}); _primary_connection_status_button.hint("Primary peer not set"); + _primary_connection_status_button.style({UI::Color::Disabled, UI::Color::Normal}); } const auto available_peers = _peer_scanner.peers(); diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 3ecc676..3bc3b4f 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -33,6 +33,7 @@ struct PeerFavoritePage final : UI::Page { _labeled_trust_input.hint("Set priority for auto connect"); _labeled_description_input.hint("Will shown as human-readable alias"); + _confirm_button.hint("Write to registry"); _confirm_button.callback([this]() -> void { if (_temp_entry.isNone()) { return; } _temp_entry.unwrap().trust = _trust_input.value(); @@ -46,8 +47,8 @@ struct PeerFavoritePage final : UI::Page { update(); }); - _confirm_button.hint("Write to registry"); + _delete_button.hint("Remove from registry"); _delete_button.callback([this]() -> void { if (_temp_entry.isNone()) { return; } @@ -56,8 +57,6 @@ struct PeerFavoritePage final : UI::Page { _ui.activePage(_root); update(); }); - _delete_button.hint("Remove from registry"); - _delete_button.background(UI::Color::Danger); } void bindPeer(const transport::PeerAddress &address) noexcept { @@ -98,7 +97,7 @@ struct PeerFavoritePage final : UI::Page { UI::Labeled _labeled_trust_input{"Trust", _trust_input}; UI::Labeled _labeled_description_input{"Name", _description_input}; - UI::Button _confirm_button{{}}, _delete_button{"Delete"}; + UI::Button _confirm_button{{}}, _delete_button{"Delete", UI::Style{.background_color = UI::Color::Danger}}; kf::memory::Array _layout; }; diff --git a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index 5df5d3d..6837896 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -32,6 +32,7 @@ struct RawProtocolPage : UI::Page { _message_input.hint("Edit message"); + _send_button.hint("Send raw buffer as is"); _send_button.callback([this]() { kf::memory::StringView s{_message.data(), _message.size()}; s = s.sub(0, s.find('\0').unwrapOr(s.size())); @@ -40,7 +41,6 @@ struct RawProtocolPage : UI::Page { (void) _transport_link.send({reinterpret_cast(s.data()), s.size()}); }); - _send_button.hint("Send raw buffer as is"); } void onEntry() noexcept override { From 184daa1a41ad1fd7f8aed3e1b548abfae80899ae Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 18 Jun 2026 17:50:05 +0300 Subject: [PATCH 258/326] refactor(transport/espnow): inline peerAdd static method, dead code remove. --- .../src/djc/transport/EspNowTransport.hpp | 65 +++++++------------ 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 71b1039..97dfcc2 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -8,16 +8,16 @@ #include #include #include -#include #include +#include #include "djc/transport/PeerAddress.hpp" #include "djc/transport/Transport.hpp" namespace djc::transport { -/// @brief ESP‑NOW transport implementation. -/// @note Manages ESP‑NOW peer connections. uses dedicated active peer for communication. +/// @brief ESP‑NOW transport implementation +/// @note Manages ESP‑NOW peer connections. uses dedicated active peer for communication struct EspNowTransport : Transport, kf::mixin::Initable { [[nodiscard]] bool send(kf::Slice buffer) noexcept override { @@ -28,29 +28,32 @@ struct EspNowTransport : Transport, kf::mixin::Initable { } } -private: - using EspNow = kf::network::EspNow; - using LogString = kf::memory::StaticString<128>; - - static constexpr auto logger{kf::Logger::create("EspNowTransport")}; - protected: - /// @brief Establish a connection to a peer. - /// @param address The peer's address (must be of kind `EspNow`). - /// @return true on success, false on failure. - /// @note Adds the peer to ESP‑NOW and sets up a receive callback. + /// @brief Establish a connection to a peer + /// @param address The peer's address (must be of kind `EspNow`) + /// @return true on success, false on failure + /// @note Adds the peer to ESP‑NOW and sets up a receive callback [[nodiscard]] bool doConnect(const PeerAddress &address) noexcept override { if (address.kind() != Kind::EspNow) { return false; } - _active_peer = addPeer(address.mac()); - if (_active_peer.isNone()) { return false; } + auto peer_result = EspNow::Peer::create(EspNow::Peer::Config{ + .mac_address = address.mac(), + .wifi_interface_sta = true, + }); + + if (peer_result.isError()) { + logger.error(LogString::formatted("Connect to '%s' failed: %s", address.mac().toString().data(), peer_result.error().toString().data()).view()); + return false; + } + + _active_peer = kf::some(std::move(peer_result.ok())); - logger.info("Connected: OK"); + logger.info(LogString::formatted("Connected: primary peer set '%s'", address.mac().toString().data()).view()); return true; } - /// @brief Disconnect from the current peer. - /// @note Removes the peer from ESP‑NOW and clears internal state. + /// @brief Disconnect from the current peer + /// @note Removes the peer from ESP‑NOW and clears internal state void doDisconnect() noexcept override { if (not connected()) { logger.warn("Disconnect failed: No active peer"); @@ -68,29 +71,12 @@ struct EspNowTransport : Transport, kf::mixin::Initable { } private: - kf::Option _active_peer{}; - - static auto addPeer(const kf::network::MacAddress &mac) noexcept -> kf::Option { - auto peer_result = EspNow::Peer::create(EspNow::Peer::Config{ - .mac_address = mac, - .wifi_interface_sta = true, - }); + using EspNow = kf::network::EspNow; + using LogString = kf::memory::StaticString<128>; - if (peer_result.isOk()) { - logger.info(LogString::formatted("Peer '%s' added", mac.toString().data()).view()); - return kf::some(std::move(peer_result.ok())); - } else { - logger.error(LogString::formatted("Failed to add peer [%s] :%s", mac.toString().data(), peer_result.error().toString().data()).view()); - return kf::none; - } - } + static constexpr auto logger{kf::Logger::create("EspNowTransport")}; - static void delPeer(EspNow::Peer &peer) noexcept { - const auto result = peer.del(); - if (result.isError()) { - logger.error(LogString::formatted("Failed to delete peer [%s] : %s", peer.mac().toString().data(), result.error().toString().data()).view()); - } - } + kf::Option _active_peer{kf::none}; KF_IMPL_INITABLE(EspNowTransport, bool); bool initImpl() noexcept { @@ -112,7 +98,6 @@ struct EspNowTransport : Transport, kf::mixin::Initable { } }); - logger.debug("init: ok"); return true; } }; From 4ab5c1d9374afffea14192c181bbc57bbf266c19 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 18 Jun 2026 18:01:58 +0300 Subject: [PATCH 259/326] fix(service/display): draw overlay after ui. fill overlay background --- .../src/djc/service/DisplayManager.hpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 4791490..01a6a81 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -18,7 +18,7 @@ namespace djc::service { -/// @brief Service that manages display rendering, including UI and virtual keyboard overlay. +/// @brief Service that manages display rendering, including UI and virtual keyboard overlay template struct DisplayManager final : Service>, @@ -72,17 +72,21 @@ template struct DisplayManager final : void renderUi(kf::memory::StringView str) noexcept { auto &canvas = _canvas.unwrap(); + canvas.background(Palette::black); + canvas.foreground(Palette::white); + canvas.text(0, 0, str.data()); + if (not _overlay.empty()) { - canvas.background(_overlay_color); - canvas.foreground(Palette::black); const auto rows = 1 + (_overlay.size() / canvas.widthInGlyphs()); const auto y = static_cast(canvas.maxY() - rows * canvas.font().heightTotal()); + + canvas.foreground(_overlay_color); + canvas.rect(0, y, canvas.maxX(), canvas.maxY(), true); + + canvas.background(_overlay_color); + canvas.foreground(Palette::black); canvas.text(0, y, _overlay); } - - canvas.background(Palette::black); - canvas.foreground(Palette::white); - canvas.text(0, 0, str.data()); } void renderVirtualKeyboard() noexcept { @@ -128,7 +132,6 @@ template struct DisplayManager final : } } - // impl using This = DisplayManager; KF_IMPL_INITABLE(This, void); From 41f030b3ef3f4d8fcdd29f7b295c7681a5182f3d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 18 Jun 2026 18:15:08 +0300 Subject: [PATCH 260/326] refactor(service/display): use canvas as method parameter instead of unwrapped canvas --- DJC-Firmware/src/djc/service/DisplayManager.hpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/DisplayManager.hpp index 01a6a81..26d8225 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/DisplayManager.hpp @@ -47,6 +47,7 @@ template struct DisplayManager final : void onRender(kf::memory::StringView str) noexcept { if (_canvas.isNone()) { return; } + auto &canvas = _canvas.unwrap(); _canvas.unwrap().background(Palette::black); _canvas.unwrap().foreground(Palette::white); @@ -54,9 +55,9 @@ template struct DisplayManager final : _canvas.unwrap().fill(); if (_virtual_keyboard.active()) { - renderVirtualKeyboard(); + renderVirtualKeyboard(canvas); } else { - renderUi(str); + renderUi(canvas, str); } (void) _display_driver.send(); @@ -69,9 +70,7 @@ template struct DisplayManager final : kf::memory::StringView _overlay{}; Color _overlay_color{}; - void renderUi(kf::memory::StringView str) noexcept { - auto &canvas = _canvas.unwrap(); - + void renderUi(Canvas &canvas, kf::memory::StringView str) noexcept { canvas.background(Palette::black); canvas.foreground(Palette::white); canvas.text(0, 0, str.data()); @@ -79,19 +78,17 @@ template struct DisplayManager final : if (not _overlay.empty()) { const auto rows = 1 + (_overlay.size() / canvas.widthInGlyphs()); const auto y = static_cast(canvas.maxY() - rows * canvas.font().heightTotal()); - + canvas.foreground(_overlay_color); canvas.rect(0, y, canvas.maxX(), canvas.maxY(), true); - + canvas.background(_overlay_color); canvas.foreground(Palette::black); canvas.text(0, y, _overlay); } } - void renderVirtualKeyboard() noexcept { - auto &canvas = _canvas.unwrap(); - + void renderVirtualKeyboard(Canvas &canvas) noexcept { const auto longest_row = ui::VirtualKeyboard::rows[0].size(); const auto key_width = canvas.width() / longest_row; const auto key_height = canvas.font().heightTotal(); From b5df7db4f1e443d747368a887c46db70a72e65bd Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 18 Jun 2026 18:26:14 +0300 Subject: [PATCH 261/326] refactor(service): rename DisplayManager -> GraphicsService --- .../{DisplayManager.hpp => GraphicsService.hpp} | 10 +++++----- DJC-Firmware/src/djc/system/GraphicsSystem.hpp | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) rename DJC-Firmware/src/djc/service/{DisplayManager.hpp => GraphicsService.hpp} (94%) diff --git a/DJC-Firmware/src/djc/service/DisplayManager.hpp b/DJC-Firmware/src/djc/service/GraphicsService.hpp similarity index 94% rename from DJC-Firmware/src/djc/service/DisplayManager.hpp rename to DJC-Firmware/src/djc/service/GraphicsService.hpp index 26d8225..03c5ab3 100644 --- a/DJC-Firmware/src/djc/service/DisplayManager.hpp +++ b/DJC-Firmware/src/djc/service/GraphicsService.hpp @@ -19,10 +19,10 @@ namespace djc::service { /// @brief Service that manages display rendering, including UI and virtual keyboard overlay -template struct DisplayManager final : +template struct GraphicsService final : - Service>, - kf::mixin::Initable, void> + Service>, + kf::mixin::Initable, void> { KF_CHECK_IMPL(I, ::kf::drivers::display::DisplayDriverTag); @@ -33,7 +33,7 @@ template struct DisplayManager final : using Canvas = typename kf::gfx::Canvas; using Palette = kf::gfx::Palette; - explicit DisplayManager(DisplayDriverImpl &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : + explicit GraphicsService(DisplayDriverImpl &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : _display_driver{display_driver}, _virtual_keyboard{virtual_keyboard} {} [[nodiscard]] auto canvas() const noexcept -> const kf::Option & { @@ -129,7 +129,7 @@ template struct DisplayManager final : } } - using This = DisplayManager; + using This = GraphicsService; KF_IMPL_INITABLE(This, void); void initImpl() noexcept { diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index 71ef6a6..4de6c16 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -4,7 +4,7 @@ #pragma once #include "djc/mixin/ServiceOwner.hpp" -#include "djc/service/DisplayManager.hpp" +#include "djc/service/GraphicsService.hpp" #include "djc/system/System.hpp" #include "djc/ui/VirtualKeyboard.hpp" @@ -14,13 +14,13 @@ namespace djc::system { template struct GraphicsSystem : System, void()>, - mixin::ServiceOwner> + mixin::ServiceOwner> { - using DisplayManagerImpl = service::DisplayManager; + using GraphicsServiceImpl = service::GraphicsService; explicit GraphicsSystem(I &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : - mixin::ServiceOwner{DisplayManagerImpl{display_driver, virtual_keyboard}} {} + mixin::ServiceOwner{GraphicsServiceImpl{display_driver, virtual_keyboard}} {} private: using This = GraphicsSystem; From 6815ce356477ed05c3c1a36aca2f62ad54b15b9b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 12:31:24 +0300 Subject: [PATCH 262/326] perf(service/graphics): use defer request flag --- DJC-Firmware/src/djc/service/GraphicsService.hpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/service/GraphicsService.hpp b/DJC-Firmware/src/djc/service/GraphicsService.hpp index 03c5ab3..9c0495c 100644 --- a/DJC-Firmware/src/djc/service/GraphicsService.hpp +++ b/DJC-Firmware/src/djc/service/GraphicsService.hpp @@ -43,6 +43,7 @@ template struct GraphicsService final : void overlay(kf::memory::StringView new_overlay, Color color) noexcept { _overlay = new_overlay; _overlay_color = color; + requestSend(); } void onRender(kf::memory::StringView str) noexcept { @@ -60,7 +61,7 @@ template struct GraphicsService final : renderUi(canvas, str); } - (void) _display_driver.send(); + requestSend(); } private: @@ -69,6 +70,11 @@ template struct GraphicsService final : kf::Option _canvas{kf::none}; kf::memory::StringView _overlay{}; Color _overlay_color{}; + bool _update_requested{false}; + + void requestSend() noexcept { + _update_requested = true; + } void renderUi(Canvas &canvas, kf::memory::StringView str) noexcept { canvas.background(Palette::black); @@ -144,7 +150,12 @@ template struct GraphicsService final : } KF_IMPL_TIMED_POLLABLE(This); - void pollImpl(kf::math::Milliseconds now) noexcept {} + void pollImpl(kf::math::Milliseconds now) noexcept { + if (_update_requested) { + _update_requested = false; + (void) _display_driver.send(); + } + } }; }// namespace djc::service \ No newline at end of file From c58a401d9b2a78c22765b69ccc3859f467ecb31d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 12:32:49 +0300 Subject: [PATCH 263/326] fix(main/setup-graphics): request config if canvas sizes changed --- DJC-Firmware/src/main.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index a1587fe..bb2b733 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -189,13 +189,24 @@ static bool setupPeriphery(djc::Config &config) noexcept { } static bool setupGraphics(djc::Config &config) noexcept { - if (const auto &canvas = graphics_system.service().canvas(); canvas.isSome()) { - config.render_system.text.row_max_length = canvas.unwrap().widthInGlyphs(); - config.render_system.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; - return true; + const auto &canvas = graphics_system.service().canvas(); + if (canvas.isNone()) { + return false; } - return false; + const auto available_width = canvas.unwrap().widthInGlyphs(); + const auto available_height = canvas.unwrap().heightInGlyphs() - 1; + + auto &render_config = config.render_system.text; + + if (available_width == render_config.row_max_length and available_height == render_config.rows_total) { + return false; + } + + render_config.row_max_length = available_width; + render_config.rows_total = available_height; + + return true; } #define DJC_SYSTEM_INIT(__system_instance__, ...) \ From 45a3c5c038e67622da65fd664fba602bb41fbb52 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 12:35:02 +0300 Subject: [PATCH 264/326] refactor(main): remove DJC_DO_SETUP macro --- DJC-Firmware/src/main.cpp | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index bb2b733..ddbfe4f 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -178,44 +178,35 @@ static void onUiRendered(kf::memory::StringView str) { // setups -static bool setupPeriphery(djc::Config &config) noexcept { +static void setupPeriphery(djc::Config &config) noexcept { if (not config.periphery.joystick_axes_tuned) { logger.debug("Tunning axes.."); periphery_system.periphery().tune(config.periphery); - return true; + config_system.service().requestSave(); } - - return false; } -static bool setupGraphics(djc::Config &config) noexcept { +static void setupGraphics(djc::Config &config) noexcept { const auto &canvas = graphics_system.service().canvas(); - if (canvas.isNone()) { - return false; - } + if (canvas.isNone()) { return; } const auto available_width = canvas.unwrap().widthInGlyphs(); const auto available_height = canvas.unwrap().heightInGlyphs() - 1; auto &render_config = config.render_system.text; - if (available_width == render_config.row_max_length and available_height == render_config.rows_total) { - return false; - } + if (available_width == render_config.row_max_length and available_height == render_config.rows_total) { return; } render_config.row_max_length = available_width; render_config.rows_total = available_height; - return true; + config_system.service().requestSave(); } #define DJC_SYSTEM_INIT(__system_instance__, ...) \ __system_instance__.init(__VA_ARGS__); \ logger.info("done: '" #__system_instance__ "'"); -#define DJC_DO_SETUP(__setup_function__) \ - if (__setup_function__) { config_system.service().requestSave(); } - void setup() { Serial.begin(115200); kf::Logger::writer = onReceiveFromLogger; @@ -243,8 +234,8 @@ void setup() { ui_system.renderer().callback(onUiRendered); - DJC_DO_SETUP(setupPeriphery(config)); - DJC_DO_SETUP(setupGraphics(config)); + setupPeriphery(config); + setupGraphics(config); } void loop() { From b3236fd364779cdcdc8c41fd61fe874312f1e83f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 19:17:15 +0300 Subject: [PATCH 265/326] refactor(graphics): extract draw methods into graphics system --- .../src/djc/service/GraphicsService.hpp | 147 ++---------------- .../src/djc/system/GraphicsSystem.hpp | 126 ++++++++++++++- DJC-Firmware/src/main.cpp | 14 +- 3 files changed, 138 insertions(+), 149 deletions(-) diff --git a/DJC-Firmware/src/djc/service/GraphicsService.hpp b/DJC-Firmware/src/djc/service/GraphicsService.hpp index 9c0495c..9654585 100644 --- a/DJC-Firmware/src/djc/service/GraphicsService.hpp +++ b/DJC-Firmware/src/djc/service/GraphicsService.hpp @@ -3,156 +3,31 @@ #pragma once -#include #include -#include -#include -#include -#include -#include -#include -#include #include "djc/service/Service.hpp" -#include "djc/ui/VirtualKeyboard.hpp" namespace djc::service { -/// @brief Service that manages display rendering, including UI and virtual keyboard overlay -template struct GraphicsService final : - - Service>, - kf::mixin::Initable, void> - -{ +/// @brief Service that manages display sending +template struct GraphicsService : Service> { KF_CHECK_IMPL(I, ::kf::drivers::display::DisplayDriverTag); - using DisplayDriverImpl = I; - using Pixel = typename DisplayDriverImpl::PixelImpl; - using Color = typename Pixel::ColorType; - using Canvas = typename kf::gfx::Canvas; - using Palette = kf::gfx::Palette; - - explicit GraphicsService(DisplayDriverImpl &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : - _display_driver{display_driver}, _virtual_keyboard{virtual_keyboard} {} - - [[nodiscard]] auto canvas() const noexcept -> const kf::Option & { - return _canvas; - } - - void overlay(kf::memory::StringView new_overlay, Color color) noexcept { - _overlay = new_overlay; - _overlay_color = color; - requestSend(); - } - - void onRender(kf::memory::StringView str) noexcept { - if (_canvas.isNone()) { return; } - auto &canvas = _canvas.unwrap(); - - _canvas.unwrap().background(Palette::black); - _canvas.unwrap().foreground(Palette::white); - - _canvas.unwrap().fill(); - - if (_virtual_keyboard.active()) { - renderVirtualKeyboard(canvas); - } else { - renderUi(canvas, str); - } - - requestSend(); - } - -private: - DisplayDriverImpl &_display_driver; - const ui::VirtualKeyboard &_virtual_keyboard; - kf::Option _canvas{kf::none}; - kf::memory::StringView _overlay{}; - Color _overlay_color{}; - bool _update_requested{false}; + explicit constexpr GraphicsService(I &display_driver) noexcept : + _display_driver{display_driver} {} void requestSend() noexcept { - _update_requested = true; + _send_requested = true; } - void renderUi(Canvas &canvas, kf::memory::StringView str) noexcept { - canvas.background(Palette::black); - canvas.foreground(Palette::white); - canvas.text(0, 0, str.data()); - - if (not _overlay.empty()) { - const auto rows = 1 + (_overlay.size() / canvas.widthInGlyphs()); - const auto y = static_cast(canvas.maxY() - rows * canvas.font().heightTotal()); - - canvas.foreground(_overlay_color); - canvas.rect(0, y, canvas.maxX(), canvas.maxY(), true); - - canvas.background(_overlay_color); - canvas.foreground(Palette::black); - canvas.text(0, y, _overlay); - } - } - - void renderVirtualKeyboard(Canvas &canvas) noexcept { - const auto longest_row = ui::VirtualKeyboard::rows[0].size(); - const auto key_width = canvas.width() / longest_row; - const auto key_height = canvas.font().heightTotal(); - const auto keyboard_offset_y = canvas.maxY() - key_height * _virtual_keyboard.rowsTotal(); - const auto glyph_offset_x = (key_width - canvas.font().widthTotal()) / 2; - - canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", _virtual_keyboard.available(), _virtual_keyboard.text().size()).data()); - canvas.text(0, canvas.font().heightTotal(), _virtual_keyboard.text().data()); - - canvas.background(Palette::dark_gray); - canvas.foreground(Palette::dark_gray); - canvas.rect(0, keyboard_offset_y, canvas.maxX(), canvas.maxY(), true); - - for (auto row = 0; row < _virtual_keyboard.rowsTotal(); row += 1) { - const auto y = keyboard_offset_y + row * key_height; - const auto cols = ui::VirtualKeyboard::rows[row].size(); - - const auto x_offset = ((longest_row - cols) * key_width) / 2; - - for (auto col = 0; col < cols; col += 1) { - const auto x = col * key_width + x_offset; - - if (row == _virtual_keyboard.cursorRow() and col == _virtual_keyboard.cursorCol()) { - canvas.foreground(Palette::dark_blue); - canvas.rect(x, y, x + key_width, y + key_height - 1, true); - - canvas.background(Palette::dark_blue); - canvas.foreground(Palette::white); - } else { - canvas.background(Palette::dark_gray); - canvas.foreground(Palette::black); - } - - const auto &key = ui::VirtualKeyboard::keyAt(row, col); - - canvas.glyph(x + glyph_offset_x, y, key.isCommon() ? key.value(_virtual_keyboard.shifted()) : '?'); - } - } - } - - using This = GraphicsService; - - KF_IMPL_INITABLE(This, void); - void initImpl() noexcept { - Canvas canvas{ - kf::image::DynamicImage{_display_driver.image()}, - typename Canvas::State{ - .active_font = kf::someRef(kf::gfx::fonts::gyver_5x7_en), - .auto_next_line = true, - }, - }; - _canvas = kf::some(std::move(canvas)); - } +private: + I &_display_driver; + bool _send_requested{false}; - KF_IMPL_TIMED_POLLABLE(This); + KF_IMPL_TIMED_POLLABLE(GraphicsService); void pollImpl(kf::math::Milliseconds now) noexcept { - if (_update_requested) { - _update_requested = false; + if (_send_requested) { + _send_requested = false; (void) _display_driver.send(); } } diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index 4de6c16..e282cb0 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -3,6 +3,16 @@ #pragma once +#include "djc/ui/VirtualKeyboard.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + #include "djc/mixin/ServiceOwner.hpp" #include "djc/service/GraphicsService.hpp" #include "djc/system/System.hpp" @@ -13,21 +23,125 @@ namespace djc::system { /// @brief System managing display output, canvas, and virtual keyboard. template struct GraphicsSystem : - System, void()>, + System, void(I &)>, mixin::ServiceOwner> { - using GraphicsServiceImpl = service::GraphicsService; + using DisplayDriverImpl = I; + using GraphicsServiceImpl = service::GraphicsService; + + using Pixel = typename DisplayDriverImpl::PixelImpl; + using Color = typename Pixel::ColorType; + using Canvas = typename kf::gfx::Canvas; + using Palette = kf::gfx::Palette; explicit GraphicsSystem(I &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : - mixin::ServiceOwner{GraphicsServiceImpl{display_driver, virtual_keyboard}} {} + mixin::ServiceOwner{GraphicsServiceImpl{display_driver}}, _virtual_keyboard{virtual_keyboard} {} + + [[nodiscard]] auto canvas() const noexcept -> const kf::Option & { + return _canvas; + } + + void overlay(kf::memory::StringView new_overlay, Color color) noexcept { + _overlay = new_overlay; + _overlay_color = color; + this->service().requestSend(); + } + + void onRender(kf::memory::StringView str) noexcept { + if (_canvas.isNone()) { return; } + auto &canvas = _canvas.unwrap(); + + _canvas.unwrap().background(Palette::black); + _canvas.unwrap().foreground(Palette::white); + + _canvas.unwrap().fill(); + + if (_virtual_keyboard.active()) { + renderVirtualKeyboard(canvas); + } else { + renderUi(canvas, str); + } + + this->service().requestSend(); + } private: + const ui::VirtualKeyboard &_virtual_keyboard; + kf::Option _canvas{kf::none}; + kf::memory::StringView _overlay{}; + Color _overlay_color{}; + + void renderUi(Canvas &canvas, kf::memory::StringView str) noexcept { + canvas.background(Palette::black); + canvas.foreground(Palette::white); + canvas.text(0, 0, str.data()); + + if (not _overlay.empty()) { + const auto rows = 1 + (_overlay.size() / canvas.widthInGlyphs()); + const auto y = static_cast(canvas.maxY() - rows * canvas.font().heightTotal()); + + canvas.foreground(_overlay_color); + canvas.rect(0, y, canvas.maxX(), canvas.maxY(), true); + + canvas.background(_overlay_color); + canvas.foreground(Palette::black); + canvas.text(0, y, _overlay); + } + } + + void renderVirtualKeyboard(Canvas &canvas) noexcept { + const auto longest_row = ui::VirtualKeyboard::rows[0].size(); + const auto key_width = canvas.width() / longest_row; + const auto key_height = canvas.font().heightTotal(); + const auto keyboard_offset_y = canvas.maxY() - key_height * _virtual_keyboard.rowsTotal(); + const auto glyph_offset_x = (key_width - canvas.font().widthTotal()) / 2; + + canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", _virtual_keyboard.available(), _virtual_keyboard.text().size()).data()); + canvas.text(0, canvas.font().heightTotal(), _virtual_keyboard.text().data()); + + canvas.background(Palette::dark_gray); + canvas.foreground(Palette::dark_gray); + canvas.rect(0, keyboard_offset_y, canvas.maxX(), canvas.maxY(), true); + + for (auto row = 0; row < _virtual_keyboard.rowsTotal(); row += 1) { + const auto y = keyboard_offset_y + row * key_height; + const auto cols = ui::VirtualKeyboard::rows[row].size(); + + const auto x_offset = ((longest_row - cols) * key_width) / 2; + + for (auto col = 0; col < cols; col += 1) { + const auto x = col * key_width + x_offset; + + if (row == _virtual_keyboard.cursorRow() and col == _virtual_keyboard.cursorCol()) { + canvas.foreground(Palette::dark_blue); + canvas.rect(x, y, x + key_width, y + key_height - 1, true); + + canvas.background(Palette::dark_blue); + canvas.foreground(Palette::white); + } else { + canvas.background(Palette::dark_gray); + canvas.foreground(Palette::black); + } + + const auto &key = ui::VirtualKeyboard::keyAt(row, col); + + canvas.glyph(x + glyph_offset_x, y, key.isCommon() ? key.value(_virtual_keyboard.shifted()) : '?'); + } + } + } + using This = GraphicsSystem; - DJC_IMPL_INITABLE(This, void()); - void initImpl() noexcept { - this->service().init(); + DJC_IMPL_INITABLE(This, void(I &)); + void initImpl(DisplayDriver &display_driver) noexcept { + _canvas = kf::some(Canvas{ + kf::image::DynamicImage{display_driver.image()}, + typename Canvas::State{ + .active_font = kf::someRef(kf::gfx::fonts::gyver_5x7_en), + .auto_next_line = true, + }, + }); } KF_IMPL_TIMED_POLLABLE(This); diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index ddbfe4f..6c851d2 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -157,23 +157,23 @@ static void onPrimaryJoystickDirection(djc::service::InputHandler::JoystickListe } static void onUiRendered(kf::memory::StringView str) { - using Palette = std::decay_t::Palette; + using Palette = std::decay_t::Palette; if (control_system.service().enabled()) { if (transport_system.link().connected()) { - graphics_system.service().overlay(transport_system.link().activePeerAddress().unwrap().toString().view(), Palette::light_green); + graphics_system.overlay(transport_system.link().activePeerAddress().unwrap().toString().view(), Palette::light_green); } else { - graphics_system.service().overlay("Disconnected", Palette::light_yellow); + graphics_system.overlay("Disconnected", Palette::light_yellow); } } else { if (const auto &p = ui_system.service().activePage(); p.isSome()) { if (const auto &widget = p.unwrap().selectedWidget(); widget.isSome()) { - graphics_system.service().overlay(widget.unwrap().hint(), Palette::light_gray); + graphics_system.overlay(widget.unwrap().hint(), Palette::light_gray); } } } - graphics_system.service().onRender(str); + graphics_system.onRender(str); } // setups @@ -187,7 +187,7 @@ static void setupPeriphery(djc::Config &config) noexcept { } static void setupGraphics(djc::Config &config) noexcept { - const auto &canvas = graphics_system.service().canvas(); + const auto &canvas = graphics_system.canvas(); if (canvas.isNone()) { return; } const auto available_width = canvas.unwrap().widthInGlyphs(); @@ -219,7 +219,7 @@ void setup() { DJC_SYSTEM_INIT(protocol_system, config.init_protocol_mode); DJC_SYSTEM_INIT(peer_system, transport_system.link()); DJC_SYSTEM_INIT(input_system); - DJC_SYSTEM_INIT(graphics_system); + DJC_SYSTEM_INIT(graphics_system, periphery_system.periphery().display); DJC_SYSTEM_INIT(control_system); DJC_SYSTEM_INIT(ui_system, {&peer_explorer_page, &mavlink_telemetry_page, &raw_protocol_page, &config_page}); From 4fb17fe1ade898d9db3b2dd1a137e6d2c9069321 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 19:21:41 +0300 Subject: [PATCH 266/326] chore(graphics): style, format, docs --- DJC-Firmware/src/djc/service/GraphicsService.hpp | 2 ++ DJC-Firmware/src/djc/system/GraphicsSystem.hpp | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/service/GraphicsService.hpp b/DJC-Firmware/src/djc/service/GraphicsService.hpp index 9654585..f11fa29 100644 --- a/DJC-Firmware/src/djc/service/GraphicsService.hpp +++ b/DJC-Firmware/src/djc/service/GraphicsService.hpp @@ -10,12 +10,14 @@ namespace djc::service { /// @brief Service that manages display sending +/// @tparam I DisplayDriver Implementation class template struct GraphicsService : Service> { KF_CHECK_IMPL(I, ::kf::drivers::display::DisplayDriverTag); explicit constexpr GraphicsService(I &display_driver) noexcept : _display_driver{display_driver} {} + /// @brief Requests an deferred send of RAM image to display via driver void requestSend() noexcept { _send_requested = true; } diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index e282cb0..4833b64 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -3,15 +3,12 @@ #pragma once -#include "djc/ui/VirtualKeyboard.hpp" #include #include #include #include #include -#include #include -#include #include "djc/mixin/ServiceOwner.hpp" #include "djc/service/GraphicsService.hpp" From 68df3a08a3c4837cf2a4bcefbb93c54c10815bfe Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 19:25:53 +0300 Subject: [PATCH 267/326] refactor: rename GraphicsService -> DisplayService --- .../service/{GraphicsService.hpp => DisplayService.hpp} | 6 +++--- DJC-Firmware/src/djc/system/GraphicsSystem.hpp | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) rename DJC-Firmware/src/djc/service/{GraphicsService.hpp => DisplayService.hpp} (81%) diff --git a/DJC-Firmware/src/djc/service/GraphicsService.hpp b/DJC-Firmware/src/djc/service/DisplayService.hpp similarity index 81% rename from DJC-Firmware/src/djc/service/GraphicsService.hpp rename to DJC-Firmware/src/djc/service/DisplayService.hpp index f11fa29..e521866 100644 --- a/DJC-Firmware/src/djc/service/GraphicsService.hpp +++ b/DJC-Firmware/src/djc/service/DisplayService.hpp @@ -11,10 +11,10 @@ namespace djc::service { /// @brief Service that manages display sending /// @tparam I DisplayDriver Implementation class -template struct GraphicsService : Service> { +template struct DisplayService : Service> { KF_CHECK_IMPL(I, ::kf::drivers::display::DisplayDriverTag); - explicit constexpr GraphicsService(I &display_driver) noexcept : + explicit constexpr DisplayService(I &display_driver) noexcept : _display_driver{display_driver} {} /// @brief Requests an deferred send of RAM image to display via driver @@ -26,7 +26,7 @@ template struct GraphicsService : Service> { I &_display_driver; bool _send_requested{false}; - KF_IMPL_TIMED_POLLABLE(GraphicsService); + KF_IMPL_TIMED_POLLABLE(DisplayService); void pollImpl(kf::math::Milliseconds now) noexcept { if (_send_requested) { _send_requested = false; diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index 4833b64..b14e2ab 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -11,7 +11,7 @@ #include #include "djc/mixin/ServiceOwner.hpp" -#include "djc/service/GraphicsService.hpp" +#include "djc/service/DisplayService.hpp" #include "djc/system/System.hpp" #include "djc/ui/VirtualKeyboard.hpp" @@ -21,11 +21,11 @@ namespace djc::system { template struct GraphicsSystem : System, void(I &)>, - mixin::ServiceOwner> + mixin::ServiceOwner> { using DisplayDriverImpl = I; - using GraphicsServiceImpl = service::GraphicsService; + using DisplayServiceImpl = service::DisplayService; using Pixel = typename DisplayDriverImpl::PixelImpl; using Color = typename Pixel::ColorType; @@ -33,7 +33,7 @@ template struct GraphicsSystem : using Palette = kf::gfx::Palette; explicit GraphicsSystem(I &display_driver, const ui::VirtualKeyboard &virtual_keyboard) noexcept : - mixin::ServiceOwner{GraphicsServiceImpl{display_driver}}, _virtual_keyboard{virtual_keyboard} {} + mixin::ServiceOwner{DisplayServiceImpl{display_driver}}, _virtual_keyboard{virtual_keyboard} {} [[nodiscard]] auto canvas() const noexcept -> const kf::Option & { return _canvas; From b46b64895160badddbe068af1501c471f5f74202 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 19:42:42 +0300 Subject: [PATCH 268/326] feat(ui/pages): set custom hints for page links --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 2 ++ DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp | 2 ++ DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 2 ++ DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp | 2 ++ DJC-Firmware/src/djc/ui/pages/RootPage.hpp | 4 +++- 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 52b3e24..ed160af 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -41,6 +41,8 @@ struct ConfigPage : UI::Page { }} { widgets(layout(0)); + this->link().hint("Open Configuration page"); + _device_name_input.hint("Device name"); _device_name_input.source({_config_service.config().device_name.data(), _config_service.config().device_name.size()}); diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 182e555..9cc37ff 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -39,6 +39,8 @@ struct MavlinkTelemetryPage : UI::Page { }} { widgets({_layout.data(), _layout.size()}); + this->link().hint("Open MAVLink page"); + _imu_display.hint("IMU accel vertor"); _attitude_display.hint("attitude quaternion"); } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 312fb85..ec587ad 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -36,6 +36,8 @@ struct PeerExplorerPage : UI::Page { &_primary_connection_status_button, &_available_label, }} { + this->link().hint("Open Peer explorer"); + _available_label.hint("Available peer will show below"); for (auto i = 0u; i < _peer_displays.size(); i += 1) { diff --git a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index 6837896..3aaf3a2 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -30,6 +30,8 @@ struct RawProtocolPage : UI::Page { }} { widgets({_layout.data(), _layout.size()}); + this->link().hint("Open raw protocol page"); + _message_input.hint("Edit message"); _send_button.hint("Send raw buffer as is"); diff --git a/DJC-Firmware/src/djc/ui/pages/RootPage.hpp b/DJC-Firmware/src/djc/ui/pages/RootPage.hpp index 95c4be9..04149d6 100644 --- a/DJC-Firmware/src/djc/ui/pages/RootPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RootPage.hpp @@ -13,7 +13,9 @@ namespace djc::ui::pages { struct RootPage : UI::Page { static constexpr auto max_items{4}; - explicit constexpr RootPage(UI &ui) noexcept : Page{ui, "Main"} {} + explicit RootPage(UI &ui) noexcept : Page{ui, "Main"} { + this->link().hint("Return to Main page"); + } void attach(UI::Page &page) noexcept { if (_items >= _layout.size()) { return; } From 6f2fcf4e5e2bff145d89332635dc915ebe26dd00 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 19:48:22 +0300 Subject: [PATCH 269/326] fix(system/gfx): add padding for overlay --- DJC-Firmware/src/djc/system/GraphicsSystem.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index b14e2ab..b8ccee4 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -64,6 +64,8 @@ template struct GraphicsSystem : } private: + static constexpr auto overlay_text_padding{1}; + const ui::VirtualKeyboard &_virtual_keyboard; kf::Option _canvas{kf::none}; kf::memory::StringView _overlay{}; @@ -83,7 +85,7 @@ template struct GraphicsSystem : canvas.background(_overlay_color); canvas.foreground(Palette::black); - canvas.text(0, y, _overlay); + canvas.text(overlay_text_padding, y, _overlay); } } From 549a8e3f90c920c9e814a520ee99983a64eb81c6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 19:50:03 +0300 Subject: [PATCH 270/326] fix(system/gfx): pass string vite to canvas instead of implicit stringview from pointer --- DJC-Firmware/src/djc/system/GraphicsSystem.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index b8ccee4..a1c5291 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -74,7 +74,7 @@ template struct GraphicsSystem : void renderUi(Canvas &canvas, kf::memory::StringView str) noexcept { canvas.background(Palette::black); canvas.foreground(Palette::white); - canvas.text(0, 0, str.data()); + canvas.text(0, 0, str); if (not _overlay.empty()) { const auto rows = 1 + (_overlay.size() / canvas.widthInGlyphs()); @@ -97,7 +97,7 @@ template struct GraphicsSystem : const auto glyph_offset_x = (key_width - canvas.font().widthTotal()) / 2; canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", _virtual_keyboard.available(), _virtual_keyboard.text().size()).data()); - canvas.text(0, canvas.font().heightTotal(), _virtual_keyboard.text().data()); + canvas.text(0, canvas.font().heightTotal(), _virtual_keyboard.text()); canvas.background(Palette::dark_gray); canvas.foreground(Palette::dark_gray); From dbc43e8608798bf382a710c3584ebfdb3077237d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 19:56:29 +0300 Subject: [PATCH 271/326] fix(ui/widget/text-input): update default style --- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index f5df7c3..a352f72 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -17,10 +17,8 @@ template struct TextInput : U::Widget { - explicit TextInput(VirtualKeyboard &virtual_keyboard, kf::Slice source, kf::ui::Style style = kf::ui::Style::defaults()) noexcept : - U::Widget{style}, _virtual_keyboard{virtual_keyboard}, _text_source{source} { - this->foreground(kf::ui::Color::Info); - } + explicit TextInput(VirtualKeyboard &virtual_keyboard, kf::Slice source, kf::ui::Style style = kf::ui::Style{.foreground_color = kf::ui::Color::Info}) noexcept : + U::Widget{style}, _virtual_keyboard{virtual_keyboard}, _text_source{source} {} void source(kf::Slice new_source) noexcept { _text_source = new_source; From 9cb8436f06c9a9d9f06c3afc4b369e63cdf5c35a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 19 Jun 2026 20:22:17 +0300 Subject: [PATCH 272/326] fix(ui/virtual-keyboard): correct keyboard logic and indicator --- .../src/djc/system/GraphicsSystem.hpp | 2 +- DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp | 22 ++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index a1c5291..fdf6bce 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -96,7 +96,7 @@ template struct GraphicsSystem : const auto keyboard_offset_y = canvas.maxY() - key_height * _virtual_keyboard.rowsTotal(); const auto glyph_offset_x = (key_width - canvas.font().widthTotal()) / 2; - canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", _virtual_keyboard.available(), _virtual_keyboard.text().size()).data()); + canvas.text(0, 0, kf::memory::StaticString<32>::formatted("\xBC\xF0Text Input: %d / %d\x80\n", _virtual_keyboard.capacity(), _virtual_keyboard.text().size()).data()); canvas.text(0, canvas.font().heightTotal(), _virtual_keyboard.text()); canvas.background(Palette::dark_gray); diff --git a/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp index 78ebd5d..822ab5a 100644 --- a/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp +++ b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp @@ -139,40 +139,46 @@ struct VirtualKeyboard final : kf::mixin::NonCopyable { [[nodiscard]] bool active() const noexcept { return _active; } - [[nodiscard]] kf::memory::StringView text() const noexcept { return {_text_source.data(), _text_source.size()}; } + [[nodiscard]] kf::memory::StringView text() const noexcept { + return kf::memory::StringView{_text_source.data(), static_cast(_text_cursor)}; + } [[nodiscard]] static const Key &keyAt(kf::i8 row, kf::i8 col) noexcept { return rows[row][col]; } + [[nodiscard]] kf::usize capacity() const noexcept { + return _text_source.size(); + } + [[nodiscard]] kf::usize available() const noexcept { if (_text_cursor < _text_source.size()) { return _text_source.size() - _text_cursor; - } else { - return 0; } + return 0; } void begin(kf::Slice text_source) noexcept { _active = true; - _text_source = text_source; - _text_cursor = text().find('\0').unwrapOr(0); + _text_cursor = 0; + while (_text_cursor < _text_source.size() and _text_source[_text_cursor] != '\0') { + _text_cursor += 1; + } } - void quit() noexcept { _active = false; } void click() noexcept { - if (available() == 0) { return; } - const auto &key = rows[_cursor_row][_cursor_row_index]; switch (key.kind) { case Key::Kind::Space: case Key::Kind::Enter: case Key::Kind::Common: { + if (available() == 0) { return; } + _text_source[_text_cursor] = key.value(shifted()); _text_cursor += 1; _text_source[_text_cursor] = '\0'; From a59a7ced0f27ef8a2d417f5ca4b3a25f62141cd3 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 18:45:44 +0300 Subject: [PATCH 273/326] fix: replace djc::mixin::Initable with kf::mixin::Initable --- DJC-Firmware/src/djc/mixin/Initable.hpp | 35 ------------------- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 2 +- DJC-Firmware/src/djc/system/ControlSystem.hpp | 2 +- .../src/djc/system/GraphicsSystem.hpp | 2 +- DJC-Firmware/src/djc/system/InputSystem.hpp | 2 +- DJC-Firmware/src/djc/system/PeerSystem.hpp | 2 +- .../src/djc/system/PeripherySystem.hpp | 2 +- .../src/djc/system/ProtocolSystem.hpp | 2 +- DJC-Firmware/src/djc/system/System.hpp | 5 ++- .../src/djc/system/TransportSystem.hpp | 2 +- DJC-Firmware/src/djc/system/UiSystem.hpp | 2 +- 11 files changed, 11 insertions(+), 47 deletions(-) delete mode 100644 DJC-Firmware/src/djc/mixin/Initable.hpp diff --git a/DJC-Firmware/src/djc/mixin/Initable.hpp b/DJC-Firmware/src/djc/mixin/Initable.hpp deleted file mode 100644 index 22f99ce..0000000 --- a/DJC-Firmware/src/djc/mixin/Initable.hpp +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2026 KiraFlux -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -namespace djc::mixin { - -/// @brief CRTP mixin that adds an `init(Args...)` method forwarding arguments to `initImpl(Args...)` -/// @tparam Impl Derived class (must implement `initImpl(Args...)`) -template struct Initable; - -template struct Initable { - - /// @brief Initializes the object with given arguments - /// @param args Arguments forwarded to `initImpl` - /// @return init result - [[nodiscard]] R init(Args... args) noexcept { - return static_cast(this)->initImpl(std::forward(args)...); - } -}; - -template struct Initable { - - /// @brief Initializes the object with given arguments - /// @param args Arguments forwarded to `initImpl` - void init(Args... args) noexcept { - static_cast(this)->initImpl(std::forward(args)...); - } -}; - -}// namespace djc::mixin - -#define DJC_IMPL_INITABLE(__impl__, ...) friend struct ::djc::mixin::Initable<__impl__, __VA_ARGS__> \ No newline at end of file diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index 6506748..3cfe22f 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -21,7 +21,7 @@ struct ConfigSystem : mixin::ServiceOwner{{}} {} private: - DJC_IMPL_INITABLE(ConfigSystem, void()); + KF_IMPL_INITABLE(ConfigSystem, void()); void initImpl() noexcept { this->service().requestLoad(); this->service().sync(); diff --git a/DJC-Firmware/src/djc/system/ControlSystem.hpp b/DJC-Firmware/src/djc/system/ControlSystem.hpp index 250ff33..feb4f75 100644 --- a/DJC-Firmware/src/djc/system/ControlSystem.hpp +++ b/DJC-Firmware/src/djc/system/ControlSystem.hpp @@ -27,7 +27,7 @@ struct ControlSystem : private: Periphery &_periphery; - DJC_IMPL_INITABLE(ControlSystem, void()); + KF_IMPL_INITABLE(ControlSystem, void()); void initImpl() noexcept {} KF_IMPL_TIMED_POLLABLE(ControlSystem); diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index fdf6bce..4c2f231 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -132,7 +132,7 @@ template struct GraphicsSystem : using This = GraphicsSystem; - DJC_IMPL_INITABLE(This, void(I &)); + KF_IMPL_INITABLE(This, void(I &)); void initImpl(DisplayDriver &display_driver) noexcept { _canvas = kf::some(Canvas{ kf::image::DynamicImage{display_driver.image()}, diff --git a/DJC-Firmware/src/djc/system/InputSystem.hpp b/DJC-Firmware/src/djc/system/InputSystem.hpp index eff6205..e7b652d 100644 --- a/DJC-Firmware/src/djc/system/InputSystem.hpp +++ b/DJC-Firmware/src/djc/system/InputSystem.hpp @@ -28,7 +28,7 @@ struct InputSystem : }} {} private: - DJC_IMPL_INITABLE(InputSystem, void()); + KF_IMPL_INITABLE(InputSystem, void()); void initImpl() noexcept {} KF_IMPL_TIMED_POLLABLE(InputSystem); diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp index 7e98950..36bd4b9 100644 --- a/DJC-Firmware/src/djc/system/PeerSystem.hpp +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -67,7 +67,7 @@ struct PeerSystem : service::PeerScanningService _peer_scanning_service; service::AutoConnectService _auto_connect_service; - DJC_IMPL_INITABLE(PeerSystem, void(transport::TransportLink &)); + KF_IMPL_INITABLE(PeerSystem, void(transport::TransportLink &)); void initImpl(transport::TransportLink &transport_link) noexcept { _peer_scanning_service.init(); diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp index 500c9d8..7a304c6 100644 --- a/DJC-Firmware/src/djc/system/PeripherySystem.hpp +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -29,7 +29,7 @@ struct PeripherySystem : System { private: Periphery _periphery; - DJC_IMPL_INITABLE(PeripherySystem, void()); + KF_IMPL_INITABLE(PeripherySystem, void()); void initImpl() noexcept { _periphery.init(); } diff --git a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp index f2a4561..cf682c6 100644 --- a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp +++ b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp @@ -61,7 +61,7 @@ struct ProtocolSystem : System(_poll_time), message); diff --git a/DJC-Firmware/src/djc/system/System.hpp b/DJC-Firmware/src/djc/system/System.hpp index 65c3979..910ec48 100644 --- a/DJC-Firmware/src/djc/system/System.hpp +++ b/DJC-Firmware/src/djc/system/System.hpp @@ -3,11 +3,10 @@ #pragma once +#include #include #include -#include "djc/mixin/Initable.hpp" - namespace djc::system { struct SystemTag {}; @@ -18,7 +17,7 @@ template struct System : SystemTag, kf::mixin::NonCopyable, - djc::mixin::Initable, + kf::mixin::Initable, kf::mixin::TimedPollable {}; }// namespace djc::system \ No newline at end of file diff --git a/DJC-Firmware/src/djc/system/TransportSystem.hpp b/DJC-Firmware/src/djc/system/TransportSystem.hpp index a82adff..2898377 100644 --- a/DJC-Firmware/src/djc/system/TransportSystem.hpp +++ b/DJC-Firmware/src/djc/system/TransportSystem.hpp @@ -48,7 +48,7 @@ struct TransportSystem : System { transport::TransportRegistry _transport_registry{}; transport::TransportLink _transport_link; - DJC_IMPL_INITABLE(TransportSystem, void(transport::Kind)); + KF_IMPL_INITABLE(TransportSystem, void(transport::Kind)); void initImpl(transport::Kind kind) noexcept { WiFi.mode(WIFI_MODE_STA); diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp index d89592b..1bb64a1 100644 --- a/DJC-Firmware/src/djc/system/UiSystem.hpp +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -67,7 +67,7 @@ struct UiSystem : ui::VirtualKeyboard _virtual_keyboard{}; ui::pages::RootPage _root_page{this->service()}; - DJC_IMPL_INITABLE(UiSystem, void(std::initializer_list)); + KF_IMPL_INITABLE(UiSystem, void(std::initializer_list)); void initImpl(std::initializer_list pages) noexcept { for (auto page : pages) { _root_page.attach(*page); From a55611c77dd054e6bce6ec452c19b94f47eb9a78 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 19:30:39 +0300 Subject: [PATCH 274/326] wip: migration to kiraflux-toolkit v0.3.4 --- DJC-Firmware/src/djc/Config.hpp | 4 ++-- DJC-Firmware/src/djc/Periphery.hpp | 5 ++-- .../src/djc/service/PeerScanningService.hpp | 6 ++--- DJC-Firmware/src/djc/system/UiSystem.hpp | 12 ++++++---- .../src/djc/transport/EspNowTransport.hpp | 4 ++-- DJC-Firmware/src/djc/ui/UI.hpp | 12 ++++------ DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp | 24 +++++++++---------- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 21 ++++++++-------- .../src/djc/ui/pages/MavlinkTelemetryPage.hpp | 5 ++-- .../src/djc/ui/pages/PeerDetailPage.hpp | 4 ++-- .../src/djc/ui/pages/PeerExplorerPage.hpp | 5 ++-- .../src/djc/ui/pages/PeerFavoritePage.hpp | 7 +++--- .../src/djc/ui/pages/RawProtocolPage.hpp | 3 ++- DJC-Firmware/src/djc/ui/pages/RootPage.hpp | 3 ++- .../src/djc/ui/widgets/PeerDisplay.hpp | 2 +- DJC-Firmware/src/djc/ui/widgets/TextInput.hpp | 2 +- DJC-Firmware/src/main.cpp | 4 ++-- 17 files changed, 62 insertions(+), 61 deletions(-) diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp index 0cc01b7..9d098b7 100644 --- a/DJC-Firmware/src/djc/Config.hpp +++ b/DJC-Firmware/src/djc/Config.hpp @@ -56,7 +56,7 @@ struct Config { service::InputHandler::Config input_handler; service::PeerScanningService::Config peer_scanner; service::AutoConnectService::Config auto_connect_service; - ui::UI::Traits::RenderImpl::Config render_system; + ui::UI::Traits::RendererImpl::Config ui_renderer; [[nodiscard]] bool isLatestVersion() const noexcept { return version == latest_version; @@ -81,7 +81,7 @@ struct Config { .input_handler = service::InputHandler::Config::defaults(), .peer_scanner = service::PeerScanningService::Config::defaults(), .auto_connect_service = service::AutoConnectService::Config::defaults(), - .render_system = ui::UI::Traits::RenderImpl::Config::defaults(), + .ui_renderer = ui::UI::Traits::RendererImpl::Config::defaults(), }; } }; diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index 4b84b1b..56232f0 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -75,7 +75,7 @@ struct PeripheryConfig final { struct Periphery final : kf::mixin::NonCopyable, - kf::mixin::Initable, + kf::mixin::Initable, kf::mixin::Configurable { @@ -146,8 +146,7 @@ struct Periphery final : private: static constexpr auto logger = kf::Logger::create("Periphery"); - // impl - KF_IMPL_INITABLE(Periphery, void); + KF_IMPL_INITABLE(Periphery, void()); void initImpl() noexcept { logger.info("Initializing peripherals"); diff --git a/DJC-Firmware/src/djc/service/PeerScanningService.hpp b/DJC-Firmware/src/djc/service/PeerScanningService.hpp index 4731a39..4667b50 100644 --- a/DJC-Firmware/src/djc/service/PeerScanningService.hpp +++ b/DJC-Firmware/src/djc/service/PeerScanningService.hpp @@ -19,7 +19,7 @@ namespace djc::internal { /// @brief Configuration parameters for the PeerScanningService service. -struct PeerScannerConfig final : kf::mixin::NonCopyable { +struct PeerScannerConfig final { kf::math::Milliseconds entry_max_life_time; ///< How long an entry stays in the list without being refreshed. kf::math::Timer::Config entries_list_update_timer;///< Interval between periodic clean-ups and list compaction. @@ -45,7 +45,7 @@ namespace djc::service { struct PeerScanningService final : Service, - kf::mixin::Initable, + kf::mixin::Initable, kf::mixin::Configurable { @@ -78,7 +78,7 @@ struct PeerScanningService final : kf::math::Milliseconds _last_poll_time{0}; kf::usize _active_count{0}; - KF_IMPL_INITABLE(PeerScanningService, void); + KF_IMPL_INITABLE(PeerScanningService, void()); void initImpl() noexcept { _update_poll_timer.start(0);// enable timer diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp index 1bb64a1..1c72d3d 100644 --- a/DJC-Firmware/src/djc/system/UiSystem.hpp +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -6,6 +6,7 @@ #include #include +#include #include "djc/Config.hpp" #include "djc/mixin/ServiceOwner.hpp" @@ -24,11 +25,11 @@ struct UiSystem : mixin::ServiceOwner { - using Renderer = ui::UI::Traits::RenderImpl; + using Renderer = ui::UI::Traits::RendererImpl; explicit UiSystem(const Config &config) noexcept : mixin::ServiceOwner{djc::ui::UI{_renderer, _virtual_keyboard}}, - _renderer{config.render_system} {} + _renderer{config.ui_renderer, _buffer.slice()} {} /// @brief Get mutable access to renderer component Renderer &renderer() noexcept { @@ -63,18 +64,19 @@ struct UiSystem : private: static constexpr auto logger{kf::Logger::create("UiSystem")}; + kf::memory::Array _buffer; Renderer _renderer; ui::VirtualKeyboard _virtual_keyboard{}; ui::pages::RootPage _root_page{this->service()}; KF_IMPL_INITABLE(UiSystem, void(std::initializer_list)); void initImpl(std::initializer_list pages) noexcept { - for (auto page : pages) { + for (auto page: pages) { _root_page.attach(*page); } - + this->service().activePage(_root_page); - this->service().addEvent(ui::UI::Traits::EventImpl::update()); + this->service().requestRender(); } KF_IMPL_TIMED_POLLABLE(UiSystem); diff --git a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp index 97dfcc2..52c6a0e 100644 --- a/DJC-Firmware/src/djc/transport/EspNowTransport.hpp +++ b/DJC-Firmware/src/djc/transport/EspNowTransport.hpp @@ -18,7 +18,7 @@ namespace djc::transport { /// @brief ESP‑NOW transport implementation /// @note Manages ESP‑NOW peer connections. uses dedicated active peer for communication -struct EspNowTransport : Transport, kf::mixin::Initable { +struct EspNowTransport : Transport, kf::mixin::Initable { [[nodiscard]] bool send(kf::Slice buffer) noexcept override { if (_active_peer.isSome()) { @@ -78,7 +78,7 @@ struct EspNowTransport : Transport, kf::mixin::Initable { kf::Option _active_peer{kf::none}; - KF_IMPL_INITABLE(EspNowTransport, bool); + KF_IMPL_INITABLE(EspNowTransport, bool()); bool initImpl() noexcept { logger.info("init"); diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index 3b9d95b..c196ad1 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -6,10 +6,9 @@ #include #include #include -#include #include #include -#include +#include #include #include "djc/service/Service.hpp" @@ -21,8 +20,8 @@ namespace djc::internal { using WidgetBase = ::kf::ui::widgets::Widget< - ::kf::ui::render::ColoredTextRender<512>,// Render Engine: Buffered Colored Text UI render engine - ::kf::ui::Event<6> // Event: 6-bit Event value encoding + ::kf::ui::render::ColoredTextRenderer,// Render Engine: Buffered Colored Text UI render engine + ::kf::ui::Event<6> // Event: 6-bit Event value encoding >; using UiBase = ::kf::ui::UI<::djc::ui::UiTraits>; @@ -39,7 +38,7 @@ struct UI : internal::UiBase { - explicit constexpr UI(Traits::RenderImpl &render_system, VirtualKeyboard &virtual_keyboard) noexcept : + explicit constexpr UI(Traits::RendererImpl &render_system, VirtualKeyboard &virtual_keyboard) noexcept : internal::UiBase{render_system}, _virtual_keyboard{virtual_keyboard} {} /// @brief UI Semantic Color @@ -48,9 +47,6 @@ struct UI : /// @brief UI Widget Style using Style = kf::ui::Style; - /// @brief UI Widget value Placement - using Placement = kf::ui::Placement; - /// @brief UI Widget Base using Widget = internal::UiBase::Widget; diff --git a/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp index 822ab5a..c069968 100644 --- a/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp +++ b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp @@ -54,7 +54,7 @@ struct VirtualKeyboard final : kf::mixin::NonCopyable { template using KeyRow = kf::memory::Array; - static constexpr KeyRow<14> row_0{{ + static constexpr KeyRow<14> row_0{{{ {'`', '~'}, {'1', '!'}, {'2', '@'}, @@ -69,9 +69,9 @@ struct VirtualKeyboard final : kf::mixin::NonCopyable { {'-', '_'}, {'=', '+'}, {Key::Kind::Backspace, 0}, - }}; + }}}; - static constexpr KeyRow<13> row_1{{ + static constexpr KeyRow<13> row_1{{{ {'q', 'Q'}, {'w', 'W'}, {'e', 'E'}, @@ -85,9 +85,9 @@ struct VirtualKeyboard final : kf::mixin::NonCopyable { {'[', '{'}, {']', '}'}, {'\\', '|'}, - }}; + }}}; - static constexpr KeyRow<12> row_2{{ + static constexpr KeyRow<12> row_2{{{ {'a', 'A'}, {'s', 'S'}, {'d', 'D'}, @@ -100,9 +100,9 @@ struct VirtualKeyboard final : kf::mixin::NonCopyable { {';', ':'}, {'\'', '"'}, {Key::Kind::Enter, '\n'}, - }}; + }}}; - static constexpr KeyRow<10> row_3{{ + static constexpr KeyRow<10> row_3{{{ {Key::Kind::Shift, 0}, {'z', 'Z'}, {'x', 'X'}, @@ -113,19 +113,19 @@ struct VirtualKeyboard final : kf::mixin::NonCopyable { {'m', 'M'}, {',', '<'}, {'.', '>'}, - }}; + }}}; - static constexpr KeyRow<1> row_4{{ + static constexpr KeyRow<1> row_4{{{ {Key::Kind::Space, ' '}, - }}; + }}}; - static constexpr kf::memory::Array, 5> rows{{ + static constexpr kf::memory::Array, 5> rows{{{ {row_0.data(), row_0.size()}, {row_1.data(), row_1.size()}, {row_2.data(), row_2.size()}, {row_3.data(), row_3.size()}, {row_4.data(), row_4.size()}, - }}; + }}}; [[nodiscard]] kf::u8 rowsTotal() const noexcept { return rows.size(); } diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index ed160af..0fa401a 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -23,7 +23,7 @@ struct ConfigPage : UI::Page { UI::Page &root, djc::service::ConfigService &config_service, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : - Page{ui, "Config"}, + Page{ui}, _config_service{config_service}, _peer_favorite_page{ui, *this, _peer_favoriter_registry}, _peer_favoriter_registry{peer_favoriter_registry}, @@ -39,6 +39,7 @@ struct ConfigPage : UI::Page { &_labeled_default_protocol_mode_selector, &_favorite_peers_fold_toggle_button, }} { + this->label("Config"); widgets(layout(0)); this->link().hint("Open Configuration page"); @@ -68,17 +69,17 @@ struct ConfigPage : UI::Page { _favorite_peers_fold_toggle_button.callback([this]() { show_favorites = not show_favorites; this->onEntry(); - update(); + _ui.requestRender(); }); _labeled_default_transport_kind_selector.hint("Define transport select after init"); - _default_transport_kind_selector.callback([this](transport::Kind kind) { - _config_service.config().init_transport_kind = kind; + _default_transport_kind_selector.callback([this](auto item) { + _config_service.config().init_transport_kind = item.value(); }); _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); - _default_protocol_mode_selector.callback([this](Mode mode) { - _config_service.config().init_protocol_mode = mode; + _default_protocol_mode_selector.callback([this](auto item) { + _config_service.config().init_protocol_mode = item.value(); _config_service.requestSave(); }); @@ -149,7 +150,7 @@ struct ConfigPage : UI::Page { // widgets - kf::memory::Array _transport_kind_options{{ + kf::memory::Array _transport_kind_options{{{ { "EspNow", transport::Kind::EspNow, @@ -157,13 +158,13 @@ struct ConfigPage : UI::Page { .foreground_color = UI::Color::Highlight, }, }, - }}; + }}}; TransportKindSelector::Config _transport_kind_config{ .items = {_transport_kind_options.data(), _transport_kind_options.size()}, }; - kf::memory::Array _control_mode_options{{ + kf::memory::Array _control_mode_options{{{ { "Mavlink", Mode::Mavlink, @@ -175,7 +176,7 @@ struct ConfigPage : UI::Page { "Raw", Mode::Raw, }, - }}; + }}}; ProtocolModeSelector::Config _control_mode_config{ .items = {_control_mode_options.data(), _control_mode_options.size()}, diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 9cc37ff..725c8b6 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -27,7 +27,7 @@ struct MavlinkTelemetryPage : UI::Page { protocol::ProtocolRegistry &protocol_registry, protocol::ProtocolLink &protocol_link, MavlinkTelemetryRegistry &mavlink_telemetry_registry) noexcept : - Page{ui, "Mavlink: Telemetry"}, + Page{ui}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, _mavlink_telemetry_registry{mavlink_telemetry_registry}, @@ -37,6 +37,7 @@ struct MavlinkTelemetryPage : UI::Page { &_imu_display, &_attitude_display, }} { + this->label("Mavlink: Telemetry"); widgets({_layout.data(), _layout.size()}); this->link().hint("Open MAVLink page"); @@ -90,7 +91,7 @@ struct MavlinkTelemetryPage : UI::Page { } if (need_update) { - update(); + _ui.requestRender(); } } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index a19fa65..4edb852 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -21,7 +21,7 @@ struct PeerDetailPage final : UI::Page { UI::Page &root, transport::TransportLink &transport_link, PeerFavoritesRegistry &peer_favorites_registry) noexcept : - Page{ui, {}}, + Page{ui}, _root{root}, _transport_link{transport_link}, _peer_favorites_registry{peer_favorites_registry}, @@ -54,7 +54,7 @@ struct PeerDetailPage final : UI::Page { this->label("Back"); _peer_favorite_page.bindPeer(_peer_address.unwrap()); _ui.activePage(_peer_favorite_page); - update(); + _ui.requestRender(); }); } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index ec587ad..95552c3 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -26,7 +26,7 @@ struct PeerExplorerPage : UI::Page { transport::TransportLink &transport_link, service::PeerScanningService &peer_scanner, PeerFavoritesRegistry &peer_favorites_registry) noexcept : - Page{ui, "Peer Explorer"}, + Page{ui}, _transport_link{transport_link}, _peer_scanner{peer_scanner}, _peer_favorites_registry{peer_favorites_registry}, @@ -36,6 +36,7 @@ struct PeerExplorerPage : UI::Page { &_primary_connection_status_button, &_available_label, }} { + this->label("Peer Explorer"); this->link().hint("Open Peer explorer"); _available_label.hint("Available peer will show below"); @@ -95,7 +96,7 @@ struct PeerExplorerPage : UI::Page { } widgets(layout(available_peers.size())); - update(); + _ui.requestRender(); } private: diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 3bc3b4f..3aa8863 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -16,7 +16,7 @@ namespace djc::ui::pages { struct PeerFavoritePage final : UI::Page { explicit PeerFavoritePage(UI &ui, UI::Page &root, PeerFavoritesRegistry &peer_favorites_registry) noexcept : - Page{ui, {}}, + Page{ui}, _root{root}, _peer_favorites_registry{peer_favorites_registry}, _description_input{ui.createTextInput()}, @@ -45,7 +45,7 @@ struct PeerFavoritePage final : UI::Page { .background_color = (write_ok ? UI::Color::Success : UI::Color::Error), }); - update(); + _ui.requestRender(); }); _delete_button.hint("Remove from registry"); @@ -55,7 +55,7 @@ struct PeerFavoritePage final : UI::Page { (void) _peer_favorites_registry.remove(_temp_entry.unwrap().address); _ui.activePage(_root); - update(); + _ui.requestRender(); }); } @@ -88,7 +88,6 @@ struct PeerFavoritePage final : UI::Page { .value_range = PeerFavoritesRegistry::Entry::trust_range, .default_value = PeerFavoritesRegistry::Entry::trust_range.start, .step = static_cast(1), - .placement = UI::Placement::Outside, .init_show_value = true, }; diff --git a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index 3aaf3a2..0d8e573 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -21,13 +21,14 @@ struct RawProtocolPage : UI::Page { protocol::ProtocolRegistry &protocol_registry, protocol::ProtocolLink &protocol_link, transport::TransportLink &transport_link) noexcept : - Page{ui, "Raw Protocol"}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, _transport_link{transport_link}, + Page{ui}, _protocol_registry{protocol_registry}, _protocol_link{protocol_link}, _transport_link{transport_link}, _message_input{ui.createTextInput({_message.data(), _message.size()})}, _layout{{ &root.link(), &_message_input, &_send_button, }} { + this->label("Raw Protocol"); widgets({_layout.data(), _layout.size()}); this->link().hint("Open raw protocol page"); diff --git a/DJC-Firmware/src/djc/ui/pages/RootPage.hpp b/DJC-Firmware/src/djc/ui/pages/RootPage.hpp index 04149d6..ee1cf0d 100644 --- a/DJC-Firmware/src/djc/ui/pages/RootPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RootPage.hpp @@ -13,7 +13,8 @@ namespace djc::ui::pages { struct RootPage : UI::Page { static constexpr auto max_items{4}; - explicit RootPage(UI &ui) noexcept : Page{ui, "Main"} { + explicit RootPage(UI &ui) noexcept : Page{ui} { + this->label("Main"); this->link().hint("Return to Main page"); } diff --git a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp index a802b35..eac817a 100644 --- a/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/PeerDisplay.hpp @@ -35,7 +35,7 @@ template struct PeerDisplay : _state = new_state; } - void doRender(typename U::RenderImpl &render) const noexcept override { + void doRender(typename U::RendererImpl &render) const noexcept override { render.beginBlock(kf::ui::Block::Alternative); if (_state.isSome()) { render.value(_state.unwrap().displayName()); diff --git a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp index a352f72..bc38667 100644 --- a/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp +++ b/DJC-Firmware/src/djc/ui/widgets/TextInput.hpp @@ -28,7 +28,7 @@ template struct TextInput : return nullptr != _text_source.data(); } - void doRender(typename U::RenderImpl &render) const noexcept override { + void doRender(typename U::RendererImpl &render) const noexcept override { render.value('\"'); render.value(string()); render.value('\"'); diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 6c851d2..2ac97e5 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -146,7 +146,7 @@ static void onSecondaryButtonClick() noexcept { control_system.service().enabled(not control_system.service().enabled()); } - ui_system.service().addEvent(UiEvent::update()); + ui_system.service().requestRender(); } static void onPrimaryJoystickDirection(djc::service::InputHandler::JoystickListener::Direction direction) noexcept { @@ -193,7 +193,7 @@ static void setupGraphics(djc::Config &config) noexcept { const auto available_width = canvas.unwrap().widthInGlyphs(); const auto available_height = canvas.unwrap().heightInGlyphs() - 1; - auto &render_config = config.render_system.text; + auto &render_config = config.ui_renderer.text; if (available_width == render_config.row_max_length and available_height == render_config.rows_total) { return; } From 0e3591da1a959122a4e256524ad2575e72075adf Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 19:33:04 +0300 Subject: [PATCH 275/326] feat(ui): add layout enum alias --- DJC-Firmware/src/djc/ui/UI.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index c196ad1..ec1a7df 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +48,9 @@ struct UI : /// @brief UI Widget Style using Style = kf::ui::Style; + /// @brief UI Page Layout + using Layout = kf::ui::Layout; + /// @brief UI Widget Base using Widget = internal::UiBase::Widget; From 29f0a1438e8a0218234ca07cf9ec0d1ab8b60689 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 19:53:51 +0300 Subject: [PATCH 276/326] refactor: use Array::slice() --- DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp | 10 +++++----- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 3 +-- DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp | 2 +- DJC-Firmware/src/main.cpp | 2 +- 8 files changed, 12 insertions(+), 13 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp index c069968..c650fd4 100644 --- a/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp +++ b/DJC-Firmware/src/djc/ui/VirtualKeyboard.hpp @@ -120,11 +120,11 @@ struct VirtualKeyboard final : kf::mixin::NonCopyable { }}}; static constexpr kf::memory::Array, 5> rows{{{ - {row_0.data(), row_0.size()}, - {row_1.data(), row_1.size()}, - {row_2.data(), row_2.size()}, - {row_3.data(), row_3.size()}, - {row_4.data(), row_4.size()}, + row_0.slice(), + row_1.slice(), + row_2.slice(), + row_3.slice(), + row_4.slice(), }}}; [[nodiscard]] kf::u8 rowsTotal() const noexcept { return rows.size(); } diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 0fa401a..eb6b440 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -222,7 +222,7 @@ struct ConfigPage : UI::Page { PeerFavoritePage _peer_favorite_page; kf::Slice layout(kf::usize displayed_peers) noexcept { - return kf::Slice{_layout.data(), _layout.size()}.first(layout_regular_widgets + displayed_peers); + return _layout.slice().first(layout_regular_widgets + displayed_peers); } }; diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 725c8b6..3af3662 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -38,7 +38,7 @@ struct MavlinkTelemetryPage : UI::Page { &_attitude_display, }} { this->label("Mavlink: Telemetry"); - widgets({_layout.data(), _layout.size()}); + widgets(_layout.slice()); this->link().hint("Open MAVLink page"); diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index 4edb852..1184504 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -33,7 +33,7 @@ struct PeerDetailPage final : UI::Page { _peer_favorite_page{ui, *this, peer_favorites_registry} { - widgets({_layout.data(), _layout.size()}); + widgets(_layout.slice()); _connection_button.callback([this]() -> void { if (_peer_address.isNone()) { return; } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp index 95552c3..5970b92 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerExplorerPage.hpp @@ -122,7 +122,7 @@ struct PeerExplorerPage : UI::Page { PeerDetailPage _peer_detail_page; kf::Slice layout(kf::usize displayed_peers) noexcept { - return kf::Slice{_layout.data(), _layout.size()}.first(peer_display_start_index + displayed_peers); + return _layout.slice().first(peer_display_start_index + displayed_peers); } kf::Option createPeerDisplayState(const kf::TrivialOption &entry, kf::math::Milliseconds now) const noexcept { diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 3aa8863..ed58326 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -71,8 +71,7 @@ struct PeerFavoritePage final : UI::Page { _trust_input.value(_temp_entry.unwrap().trust); _confirm_button.label("Confirm"); _confirm_button.style({UI::Color::Primary, UI::Color::Normal}); - - widgets(kf::Slice{_layout.data(), _layout.size()}.first(_layout.size() - (entry_option.isSome() ? 0 : 1))); + widgets(_layout.slice().first(_layout.size() - (entry_option.isSome() ? 0 : 1))); } private: diff --git a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp index 0d8e573..759b725 100644 --- a/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/RawProtocolPage.hpp @@ -29,7 +29,7 @@ struct RawProtocolPage : UI::Page { &_send_button, }} { this->label("Raw Protocol"); - widgets({_layout.data(), _layout.size()}); + widgets(_layout.slice()); this->link().hint("Open raw protocol page"); diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 2ac97e5..ab78bac 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -226,7 +226,7 @@ void setup() { // orcestre transport_system.link().onReceive(onReceiveFromPeer); - peer_system.favoritesRegistry().entries({config.peer_favorites.data(), config.peer_favorites.size()}); + peer_system.favoritesRegistry().entries(config.peer_favorites.slice()); input_system.service().onLeftButton(onSecondaryButtonClick); input_system.service().onRightButton(onPrimaryButtonClick); From d395824c77cd5888c1cac4e84725a5fef7d0574a Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 20:00:53 +0300 Subject: [PATCH 277/326] refactor(ui/pages): move some fields to lambda capture --- DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp | 6 ++---- DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp index 1184504..8d51757 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerDetailPage.hpp @@ -22,7 +22,6 @@ struct PeerDetailPage final : UI::Page { transport::TransportLink &transport_link, PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{ui}, - _root{root}, _transport_link{transport_link}, _peer_favorites_registry{peer_favorites_registry}, _layout{{ @@ -35,11 +34,11 @@ struct PeerDetailPage final : UI::Page { { widgets(_layout.slice()); - _connection_button.callback([this]() -> void { + _connection_button.callback([this, &root]() -> void { if (_peer_address.isNone()) { return; } if (_transport_link.connect(_peer_address.unwrap())) { - _ui.activePage(_root); + _ui.activePage(root); } else { _connection_button.label("Failed to connect"); _connection_button.style(UI::Style{ @@ -79,7 +78,6 @@ struct PeerDetailPage final : UI::Page { private: // state - UI::Page &_root; transport::TransportLink &_transport_link; kf::TrivialOption _peer_address{}; PeerFavoritesRegistry &_peer_favorites_registry; diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index ed58326..48d0e53 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -17,7 +17,6 @@ struct PeerFavoritePage final : UI::Page { explicit PeerFavoritePage(UI &ui, UI::Page &root, PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{ui}, - _root{root}, _peer_favorites_registry{peer_favorites_registry}, _description_input{ui.createTextInput()}, _layout{{ @@ -49,12 +48,12 @@ struct PeerFavoritePage final : UI::Page { }); _delete_button.hint("Remove from registry"); - _delete_button.callback([this]() -> void { + _delete_button.callback([this, &root]() -> void { if (_temp_entry.isNone()) { return; } (void) _peer_favorites_registry.remove(_temp_entry.unwrap().address); - _ui.activePage(_root); + _ui.activePage(root); _ui.requestRender(); }); } @@ -75,7 +74,6 @@ struct PeerFavoritePage final : UI::Page { } private: - UI::Page &_root; PeerFavoritesRegistry &_peer_favorites_registry; kf::TrivialOption _temp_entry{}; From 437aa7d88db3090dfd609036b83acaa4ce274a26 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 20:31:34 +0300 Subject: [PATCH 278/326] feat: add math module with crc32 function --- DJC-Firmware/src/djc/math.hpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 DJC-Firmware/src/djc/math.hpp diff --git a/DJC-Firmware/src/djc/math.hpp b/DJC-Firmware/src/djc/math.hpp new file mode 100644 index 0000000..7571cde --- /dev/null +++ b/DJC-Firmware/src/djc/math.hpp @@ -0,0 +1,32 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace djc::math { + +/// @brief CRC-32 (IEEE 802.3) checksum +[[nodiscard]] kf::u32 crc32(kf::Slice data) { + constexpr auto reflected_polynomial{0xEDB88320u}; + + auto crc = static_cast(-1); + + for (auto byte: data) { + crc ^= byte; + + for (auto j = 0u; j < 8; j += 1) { + if (crc & 1) { + crc = (crc >> 1) ^ reflected_polynomial; + } else { + crc >>= 1; + } + } + } + + return ~crc; +} + +}// namespace djc::math \ No newline at end of file From c4f37d08c2bf4f4d5572ad3fdfe5ba264a36a439 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 21:04:06 +0300 Subject: [PATCH 279/326] perf(service/config): skip redundant NVS writes using CRC32 --- .../src/djc/service/ConfigService.hpp | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 3297b66..288d973 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -5,9 +5,12 @@ #include #include +#include #include +#include #include "djc/Config.hpp" +#include "djc/math.hpp" #include "djc/service/Service.hpp" namespace djc::service { @@ -29,36 +32,41 @@ struct ConfigService : Service { /// @brief Requests an deferred save of the current config to NVS void requestSave() noexcept { _save_requested = true; - logger.debug("save requested"); + logger.debug("Save requested"); } /// @brief Requests an deferred load of the config from NVS void requestLoad() noexcept { _load_requested = true; - logger.debug("load requested"); + logger.debug("Load requested"); } /// @brief Requests an deferred reset of the config to defaults void requestReset() noexcept { _reset_requested = true; - logger.debug("reset requested"); + logger.debug("Reset requested"); } /// @brief Force sync now void sync() noexcept { + using LogString = kf::memory::StaticString<64>; + if (_load_requested) { _load_requested = false; - logger.info("Loading config from NVS"); + logger.info("Loading config from NVS..."); - if (not _storage.load()) { - logger.error("Failed to load config"); - requestReset(); - requestSave(); - } + if (_storage.load()) { + _stored_crc = crc(); + logger.info(LogString::formatted("Config loaded from NVS (CRC: %u)", _stored_crc).view()); - if (not _storage.config.isLatestVersion()) { - logger.error("Config version is outdated"); + if (not _storage.config.isLatestVersion()) { + logger.warn("Config version is outdated"); + requestReset(); + requestSave(); + } + } else { + logger.error("Config load failed"); requestReset(); requestSave(); } @@ -66,18 +74,27 @@ struct ConfigService : Service { if (_reset_requested) { _reset_requested = false; + logger.info("Config reset to defaults"); - logger.info("Resetting RAM config cache to defaults"); _storage.config = djc::Config::defaults(); } if (_save_requested) { _save_requested = false; - logger.info("Saving config to NVS"); + const auto current_crc = crc(); - if (not _storage.save()) { - logger.error("Failed to save config into NVS"); + if (current_crc == _stored_crc) { + logger.debug("Config unchanged, save skipped"); + } else { + logger.info(LogString::formatted("Config changed, saving (CRC: %u -> %u)...", _stored_crc, current_crc).view()); + + if (_storage.save()) { + _stored_crc = current_crc; + logger.info("Config saved, CRC updated"); + } else { + logger.error("Config save failed"); + } } } } @@ -86,7 +103,7 @@ struct ConfigService : Service { static constexpr auto logger{kf::Logger::create("ConfigService")}; static constexpr kf::math::Timer::Config sync_timer_config{ - .period = 20'000, + .period = 5'000, }; kf::memory::Storage _storage{ @@ -96,8 +113,14 @@ struct ConfigService : Service { kf::math::Timer _sync_timer{sync_timer_config}; + kf::u32 _stored_crc{}; + bool _save_requested{false}, _load_requested{false}, _reset_requested{false}; + [[nodiscard]] kf::u32 crc() const noexcept { + return djc::math::crc32({reinterpret_cast(&_storage.config), sizeof(Config)}); + } + KF_IMPL_TIMED_POLLABLE(ConfigService); void pollImpl(kf::math::Milliseconds now) noexcept { if (_sync_timer.expired(now)) { From b9ebe67452617c5d944a5b93b86430fd5f0c4650 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 21:15:20 +0300 Subject: [PATCH 280/326] refactor(service/config): remove explicit save requests, rely on periodic sync --- .../src/djc/service/ConfigService.hpp | 32 +++++-------------- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 13 +++----- DJC-Firmware/src/main.cpp | 3 -- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 288d973..14ba65a 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -29,12 +29,6 @@ struct ConfigService : Service { return _storage.config; } - /// @brief Requests an deferred save of the current config to NVS - void requestSave() noexcept { - _save_requested = true; - logger.debug("Save requested"); - } - /// @brief Requests an deferred load of the config from NVS void requestLoad() noexcept { _load_requested = true; @@ -63,12 +57,10 @@ struct ConfigService : Service { if (not _storage.config.isLatestVersion()) { logger.warn("Config version is outdated"); requestReset(); - requestSave(); } } else { logger.error("Config load failed"); requestReset(); - requestSave(); } } @@ -79,22 +71,14 @@ struct ConfigService : Service { _storage.config = djc::Config::defaults(); } - if (_save_requested) { - _save_requested = false; - - const auto current_crc = crc(); + if (const auto current_crc = crc(); current_crc != _stored_crc) { + logger.info(LogString::formatted("Config changed, saving (CRC: %u -> %u)...", _stored_crc, current_crc).view()); - if (current_crc == _stored_crc) { - logger.debug("Config unchanged, save skipped"); + if (_storage.save()) { + _stored_crc = current_crc; + logger.info("Config saved, CRC updated"); } else { - logger.info(LogString::formatted("Config changed, saving (CRC: %u -> %u)...", _stored_crc, current_crc).view()); - - if (_storage.save()) { - _stored_crc = current_crc; - logger.info("Config saved, CRC updated"); - } else { - logger.error("Config save failed"); - } + logger.error("Config save failed"); } } } @@ -103,7 +87,7 @@ struct ConfigService : Service { static constexpr auto logger{kf::Logger::create("ConfigService")}; static constexpr kf::math::Timer::Config sync_timer_config{ - .period = 5'000, + .period = 10'000, }; kf::memory::Storage _storage{ @@ -115,7 +99,7 @@ struct ConfigService : Service { kf::u32 _stored_crc{}; - bool _save_requested{false}, _load_requested{false}, _reset_requested{false}; + bool _load_requested{false}, _reset_requested{false}; [[nodiscard]] kf::u32 crc() const noexcept { return djc::math::crc32({reinterpret_cast(&_storage.config), sizeof(Config)}); diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index eb6b440..dc8f269 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -47,22 +47,19 @@ struct ConfigPage : UI::Page { _device_name_input.hint("Device name"); _device_name_input.source({_config_service.config().device_name.data(), _config_service.config().device_name.size()}); - _save_config_button.hint("Write config from RAM into NVS"); + _save_config_button.hint("Force to sync now"); _save_config_button.callback([this]() { - _config_service.requestSave(); _config_service.sync(); }); - _load_config_button.hint("Load config from NVS into RAM"); + _load_config_button.hint("Request Load config from NVS into RAM"); _load_config_button.callback([this]() { _config_service.requestLoad(); - _config_service.sync(); }); - _reset_config_button.hint("Set RAM config as detaults"); + _reset_config_button.hint("Request reset RAM config"); _reset_config_button.callback([this]() { _config_service.requestReset(); - _config_service.sync(); }); _favorite_peers_fold_toggle_button.hint("Toggle folding"); @@ -80,13 +77,11 @@ struct ConfigPage : UI::Page { _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); _default_protocol_mode_selector.callback([this](auto item) { _config_service.config().init_protocol_mode = item.value(); - _config_service.requestSave(); }); _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); _autoconnect_enabled_input.callback([this](bool value) { _config_service.config().auto_connect_service.enabled = value; - _config_service.requestSave(); }); for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { @@ -186,7 +181,7 @@ struct ConfigPage : UI::Page { UI::Button _save_config_button{ - "Save", + "Sync now", UI::Style{ .foreground_color = UI::Color::Primary, }, diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index ab78bac..f6bc4d4 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -182,7 +182,6 @@ static void setupPeriphery(djc::Config &config) noexcept { if (not config.periphery.joystick_axes_tuned) { logger.debug("Tunning axes.."); periphery_system.periphery().tune(config.periphery); - config_system.service().requestSave(); } } @@ -199,8 +198,6 @@ static void setupGraphics(djc::Config &config) noexcept { render_config.row_max_length = available_width; render_config.rows_total = available_height; - - config_system.service().requestSave(); } #define DJC_SYSTEM_INIT(__system_instance__, ...) \ From e7675cd3305d13ab1833476ebdfd68b9b9220138 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 21:17:23 +0300 Subject: [PATCH 281/326] refactor(main): simplify graphics setup --- DJC-Firmware/src/main.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index f6bc4d4..f3ee839 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -186,18 +186,10 @@ static void setupPeriphery(djc::Config &config) noexcept { } static void setupGraphics(djc::Config &config) noexcept { - const auto &canvas = graphics_system.canvas(); - if (canvas.isNone()) { return; } - - const auto available_width = canvas.unwrap().widthInGlyphs(); - const auto available_height = canvas.unwrap().heightInGlyphs() - 1; - - auto &render_config = config.ui_renderer.text; - - if (available_width == render_config.row_max_length and available_height == render_config.rows_total) { return; } - - render_config.row_max_length = available_width; - render_config.rows_total = available_height; + if (const auto &canvas = graphics_system.canvas(); canvas.isSome()) { + config.ui_renderer.text.row_max_length = canvas.unwrap().widthInGlyphs(); + config.ui_renderer.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; + } } #define DJC_SYSTEM_INIT(__system_instance__, ...) \ From d6c62ab2d79f111f698080078b36c505b776a2d6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 21:32:44 +0300 Subject: [PATCH 282/326] refactor: adjust periphery fields names --- DJC-Firmware/src/djc/Periphery.hpp | 26 +++++++++++++------------- DJC-Firmware/src/main.cpp | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index 56232f0..bb64066 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -23,10 +23,10 @@ struct PeripheryConfig final { AxisInput::FilterImpl::Config axis_filter; Joystick::Config left_joystick, right_joystick; - SpiBus::Config bus; - SpiBus::Node::Config bus_node; + SpiBus::Config spi_bus; + SpiBus::Node::Config display_spi_node; - DisplayDriver::Config display; + DisplayDriver::Config display_driver; kf::u16 joystick_axes_tune_samples; bool joystick_axes_tuned; @@ -47,10 +47,10 @@ struct PeripheryConfig final { .y = axisDefaults(true), }, // SPI default pins: MOSI=23, MISO=19, SCK=18 - .bus = djc::SpiBus::Config::create(), + .spi_bus = djc::SpiBus::Config::create(), // CS, SPI frequency - .bus_node = djc::SpiBus::Node::Config::create(GPIO_NUM_5, 27000000), - .display = { + .display_spi_node = djc::SpiBus::Node::Config::create(GPIO_NUM_5, 27000000), + .display_driver = { .init_orientation = kf::drivers::display::Orientation::ClockWise, }, .joystick_axes_tune_samples = 100, @@ -113,14 +113,14 @@ struct Periphery final : GPIO::AdcInput{GPIO_NUM_35}, }; - SpiBus bus{ - this->config().bus, + SpiBus spi_bus{ + this->config().spi_bus, SPI, }; - DisplayDriver display{ - this->config().display, - bus.createNode(this->config().bus_node), + DisplayDriver display_driver{ + this->config().display_driver, + spi_bus.createNode(this->config().display_spi_node), GPIO::DigitalOutput{GPIO_NUM_22},// DC GPIO::DigitalOutput{GPIO_NUM_17},// RESET }; @@ -155,11 +155,11 @@ struct Periphery final : left_button_listener.init(); right_button_listener.init(); - if (bus.init().isError()) { + if (spi_bus.init().isError()) { logger.error("SpiBus initialization failed"); } - if (display.init().isError()) { + if (display_driver.init().isError()) { logger.error("Display driver initialization failed"); } } diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index f3ee839..1ea32b9 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -66,7 +66,7 @@ static djc::system::InputSystem input_system{ static djc::system::UiSystem ui_system{config}; static djc::system::GraphicsSystem graphics_system{ - periphery_system.periphery().display, + periphery_system.periphery().display_driver, ui_system.virtualKeyboard(), }; @@ -208,7 +208,7 @@ void setup() { DJC_SYSTEM_INIT(protocol_system, config.init_protocol_mode); DJC_SYSTEM_INIT(peer_system, transport_system.link()); DJC_SYSTEM_INIT(input_system); - DJC_SYSTEM_INIT(graphics_system, periphery_system.periphery().display); + DJC_SYSTEM_INIT(graphics_system, periphery_system.periphery().display_driver); DJC_SYSTEM_INIT(control_system); DJC_SYSTEM_INIT(ui_system, {&peer_explorer_page, &mavlink_telemetry_page, &raw_protocol_page, &config_page}); From 9d3c2d519594d67941c054175dd54e96dc3b7730 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 21:48:51 +0300 Subject: [PATCH 283/326] refactor: extract gpio nums to prelude --- DJC-Firmware/src/djc/Periphery.hpp | 22 ++++++++++------------ DJC-Firmware/src/djc/prelude.hpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index bb64066..a5b1c0c 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -46,10 +46,8 @@ struct PeripheryConfig final { .x = axisDefaults(false), .y = axisDefaults(true), }, - // SPI default pins: MOSI=23, MISO=19, SCK=18 - .spi_bus = djc::SpiBus::Config::create(), - // CS, SPI frequency - .display_spi_node = djc::SpiBus::Node::Config::create(GPIO_NUM_5, 27000000), + .spi_bus = djc::SpiBus::Config::create(gpio_spi_mosi, gpio_spi_miso, gpio_spi_sck), + .display_spi_node = djc::SpiBus::Node::Config::create(gpio_display_st7735_spi_cs, 27000000), .display_driver = { .init_orientation = kf::drivers::display::Orientation::ClockWise, }, @@ -86,7 +84,7 @@ struct Periphery final : ButtonListener left_button_listener{ this->config().button, GPIO::DigitalInput{ - GPIO_NUM_14, + gpio_button_left, GPIO::DigitalInput::Pull::InternalUp, }, }; @@ -94,14 +92,14 @@ struct Periphery final : Joystick left_joystick{ this->config().left_joystick, this->config().axis_filter, - GPIO::AdcInput{GPIO_NUM_32}, - GPIO::AdcInput{GPIO_NUM_33}, + GPIO::AdcInput{gpio_joystick_left_x}, + GPIO::AdcInput{gpio_joystick_left_y}, }; ButtonListener right_button_listener{ this->config().button, GPIO::DigitalInput{ - GPIO_NUM_4, + gpio_button_right, GPIO::DigitalInput::Pull::InternalUp, }, }; @@ -109,8 +107,8 @@ struct Periphery final : Joystick right_joystick{ this->config().right_joystick, this->config().axis_filter, - GPIO::AdcInput{GPIO_NUM_34}, - GPIO::AdcInput{GPIO_NUM_35}, + GPIO::AdcInput{gpio_joystick_right_x}, + GPIO::AdcInput{gpio_joystick_right_y}, }; SpiBus spi_bus{ @@ -121,8 +119,8 @@ struct Periphery final : DisplayDriver display_driver{ this->config().display_driver, spi_bus.createNode(this->config().display_spi_node), - GPIO::DigitalOutput{GPIO_NUM_22},// DC - GPIO::DigitalOutput{GPIO_NUM_17},// RESET + GPIO::DigitalOutput{gpio_display_st7735_data_command}, + GPIO::DigitalOutput{gpio_display_st7735_reset}, }; // Analog axis calibration diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp index b954bb7..fd8af46 100644 --- a/DJC-Firmware/src/djc/prelude.hpp +++ b/DJC-Firmware/src/djc/prelude.hpp @@ -3,6 +3,8 @@ #pragma once +#include + #include #include #include @@ -12,6 +14,33 @@ namespace djc { +constexpr gpio_num_t + + // buttons + + gpio_button_left{GPIO_NUM_14}, + gpio_button_right{GPIO_NUM_4}, + + // joystick axis + + gpio_joystick_left_x{GPIO_NUM_32}, + gpio_joystick_left_y{GPIO_NUM_33}, + gpio_joystick_right_x{GPIO_NUM_34}, + gpio_joystick_right_y{GPIO_NUM_35}, + + // spi + + gpio_spi_mosi{GPIO_NUM_23}, + gpio_spi_miso{GPIO_NUM_19}, + gpio_spi_sck{GPIO_NUM_18}, + + // display + + gpio_display_st7735_spi_cs{GPIO_NUM_5}, + gpio_display_st7735_data_command{GPIO_NUM_22}, + gpio_display_st7735_reset{GPIO_NUM_17} +; + using GPIO = kf::gpio::ArduinoGPIO; using ButtonListener = kf::input::LogicalLevelListener; From 196cef59b658ce1d62bd05aa3e8bd17b7f59fc3f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Mon, 22 Jun 2026 22:06:30 +0300 Subject: [PATCH 284/326] feat(platformio.ini): add sub-envs --- DJC-Firmware/platformio.ini | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/platformio.ini b/DJC-Firmware/platformio.ini index 900684e..fab3037 100644 --- a/DJC-Firmware/platformio.ini +++ b/DJC-Firmware/platformio.ini @@ -1,4 +1,5 @@ [platformio] +default_envs = esp32dev_st7735 build_cache_dir = .pio/build_cache [env:esp32dev] @@ -16,6 +17,13 @@ lib_deps = build_flags = -std=gnu++17 - build_unflags = -std=gnu++11 + +[env:esp32dev_st7735] +extends = env:esp32dev +build_flags = ${env:esp32dev.build_flags} -DDJC_DISPLAY_DRIVER_ST7735 + +[env:esp32dev_ssd1306] +extends = env:esp32dev +build_flags = ${env:esp32dev.build_flags} -DDJC_DISPLAY_DRIVER_SSD1306 \ No newline at end of file From fe91d4464feba3879717bc392524e9727bab9fec Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 16:41:12 +0300 Subject: [PATCH 285/326] feat(memory): add NVS wrapper --- DJC-Firmware/src/djc/memory/NVS.hpp | 142 ++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 DJC-Firmware/src/djc/memory/NVS.hpp diff --git a/DJC-Firmware/src/djc/memory/NVS.hpp b/DJC-Firmware/src/djc/memory/NVS.hpp new file mode 100644 index 0000000..c8cdbdb --- /dev/null +++ b/DJC-Firmware/src/djc/memory/NVS.hpp @@ -0,0 +1,142 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace djc::internal { + +struct NvsError { + enum Kind : char { + InitFailed, + ReadFailed, + WriteFailed, + CommitFailed, + InvalidLength, + NotFound, + NotEnoughSpace, + Unknown, + } kind; + + static constexpr auto fromEsp(const auto e) -> kf::internal::ResultErrorWrapper { + switch (e) { + case ESP_ERR_NVS_NOT_FOUND: + return {NotFound}; + + case ESP_ERR_NVS_INVALID_LENGTH: + return {InvalidLength}; + + case ESP_ERR_NVS_NOT_ENOUGH_SPACE: + return {NotEnoughSpace}; + + case ESP_ERR_NVS_INVALID_HANDLE: + case ESP_ERR_NVS_READ_ONLY: + case ESP_ERR_NVS_INVALID_NAME: + default: + return {Unknown}; + } + } +}; + +}// namespace djc::internal + +namespace djc::memory { + +/// @brief NVS wrapper +struct NVS final : + + kf::mixin::NonCopyable, + kf::mixin::Initable()>, + kf::mixin::Quitable + +{ + + using Error = internal::NvsError; + + using ResultType = kf::Result; + + /// @brief Construct NVS storage component + constexpr explicit NVS(const char *nvs_namespace) noexcept : + _namespace{nvs_namespace} {} + + [[nodiscard]] ResultType load(kf::Slice buffer) noexcept { + auto len = buffer.size(); + return wrap(nvs_get_blob(_handle.unwrap(), blob_key, static_cast(buffer.data()), &len)); + } + + [[nodiscard]] ResultType dump(kf::Slice buffer) noexcept { + return wrap(nvs_set_blob(_handle.unwrap(), blob_key, buffer.data(), buffer.size())); + } + + [[nodiscard]] ResultType commit() noexcept { + return wrap(nvs_commit(_handle.unwrap())); + } + +private: + static constexpr auto blob_key{"blob"}; + + const char *_namespace; + kf::TrivialOption _handle{kf::none}; + + [[nodiscard]] static ResultType wrap(esp_err_t e) noexcept { + if (ESP_OK == e) { + return kf::ok(); + } else { + return Error::fromEsp(e); + } + } + + KF_IMPL_INITABLE(NVS, kf::Result()); + auto initImpl() noexcept -> kf::Result { + if (_handle.isSome()) { + // Already initialised + return kf::ok(); + } + + esp_err_t e; + nvs_handle_t handle; + + // Ensure NVS flash is initialised (idempotent) + e = nvs_flash_init(); + if (e == ESP_ERR_NVS_NO_FREE_PAGES or e == ESP_ERR_NVS_NEW_VERSION_FOUND) { + // NVS partition was truncated, need to erase and retry + e = nvs_flash_erase(); + if (e != ESP_OK) { + return Error::fromEsp(e); + } + e = nvs_flash_init(); + } + + if (e != ESP_OK) { + return Error::fromEsp(e); + } + + e = nvs_open(_namespace, NVS_READWRITE, &handle); + if (ESP_OK != e) { + return Error::fromEsp(e); + } + + _handle = kf::someTrivial(handle); + return kf::ok(); + } + + KF_IMPL_QUITABLE(NVS); + void quitImpl() noexcept { + if (_handle.isSome()) { + nvs_close(_handle.unwrap()); + _handle.reset(); + } + } +}; + +}// namespace djc::memory \ No newline at end of file From 9b6de19e3ddef7226b9d0e9760898d1b7cc56a72 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 17:03:36 +0300 Subject: [PATCH 286/326] refactor(service/config): use djc's NVS wrapper --- .../src/djc/service/ConfigService.hpp | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 14ba65a..10461c6 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -6,11 +6,11 @@ #include #include #include -#include #include #include "djc/Config.hpp" #include "djc/math.hpp" +#include "djc/memory/NVS.hpp" #include "djc/service/Service.hpp" namespace djc::service { @@ -21,12 +21,12 @@ struct ConfigService : Service { /// @brief Get readonly reference to the current configuration [[nodiscard]] constexpr const Config &config() const noexcept { - return _storage.config; + return _config; } /// @brief Get mutable reference to the current configuration [[nodiscard]] Config &config() noexcept { - return _storage.config; + return _config; } /// @brief Requests an deferred load of the config from NVS @@ -45,16 +45,21 @@ struct ConfigService : Service { void sync() noexcept { using LogString = kf::memory::StaticString<64>; + // init is idempotent + if (_nvs_entry.init().isError()) { + logger.error("NVS init failed"); + } + if (_load_requested) { _load_requested = false; logger.info("Loading config from NVS..."); - if (_storage.load()) { + if (_nvs_entry.load(view()).isOk()) { _stored_crc = crc(); logger.info(LogString::formatted("Config loaded from NVS (CRC: %u)", _stored_crc).view()); - if (not _storage.config.isLatestVersion()) { + if (not _config.isLatestVersion()) { logger.warn("Config version is outdated"); requestReset(); } @@ -68,13 +73,13 @@ struct ConfigService : Service { _reset_requested = false; logger.info("Config reset to defaults"); - _storage.config = djc::Config::defaults(); + _config = djc::Config::defaults(); } if (const auto current_crc = crc(); current_crc != _stored_crc) { logger.info(LogString::formatted("Config changed, saving (CRC: %u -> %u)...", _stored_crc, current_crc).view()); - if (_storage.save()) { + if (_nvs_entry.dump(view()).isOk() and _nvs_entry.commit().isOk()) { _stored_crc = current_crc; logger.info("Config saved, CRC updated"); } else { @@ -90,10 +95,8 @@ struct ConfigService : Service { .period = 10'000, }; - kf::memory::Storage _storage{ - .key = "DC", - .config = djc::Config::defaults(), - }; + Config _config{djc::Config::defaults()}; + memory::NVS _nvs_entry{"djc"}; kf::math::Timer _sync_timer{sync_timer_config}; @@ -101,8 +104,16 @@ struct ConfigService : Service { bool _load_requested{false}, _reset_requested{false}; + [[nodiscard]] kf::Slice view() noexcept { + return {reinterpret_cast(&_config), sizeof(Config)}; + } + + [[nodiscard]] kf::Slice view() const noexcept { + return const_cast(this)->view(); + } + [[nodiscard]] kf::u32 crc() const noexcept { - return djc::math::crc32({reinterpret_cast(&_storage.config), sizeof(Config)}); + return djc::math::crc32(view()); } KF_IMPL_TIMED_POLLABLE(ConfigService); From 6fa53c6eaa011f6f86bfc5b5d639f11ffb469274 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 17:13:09 +0300 Subject: [PATCH 287/326] refactor(service/config): pass sync timer config as constructor parameter --- DJC-Firmware/src/djc/service/ConfigService.hpp | 14 +++++--------- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 6 +++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 10461c6..548f5e0 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -19,6 +19,9 @@ namespace djc::service { /// @note Requests are batched and executed on a 5-second timer from the main loop. struct ConfigService : Service { + explicit constexpr ConfigService(const kf::math::Timer::Config &sync_timer_config) noexcept : + _sync_timer{sync_timer_config} {} + /// @brief Get readonly reference to the current configuration [[nodiscard]] constexpr const Config &config() const noexcept { return _config; @@ -90,18 +93,11 @@ struct ConfigService : Service { private: static constexpr auto logger{kf::Logger::create("ConfigService")}; - - static constexpr kf::math::Timer::Config sync_timer_config{ - .period = 10'000, - }; - + Config _config{djc::Config::defaults()}; memory::NVS _nvs_entry{"djc"}; - - kf::math::Timer _sync_timer{sync_timer_config}; - + kf::math::Timer _sync_timer; kf::u32 _stored_crc{}; - bool _load_requested{false}, _reset_requested{false}; [[nodiscard]] kf::Slice view() noexcept { diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index 3cfe22f..53e4097 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -18,9 +18,13 @@ struct ConfigSystem : { explicit ConfigSystem() noexcept : - mixin::ServiceOwner{{}} {} + mixin::ServiceOwner{service::ConfigService{sync_timer_config}} {} private: + static constexpr kf::math::Timer::Config sync_timer_config{ + .period = 10'000, + }; + KF_IMPL_INITABLE(ConfigSystem, void()); void initImpl() noexcept { this->service().requestLoad(); From e41bfe4742e81ed6f64bfe17f74ed606b4f4beb8 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 17:14:04 +0300 Subject: [PATCH 288/326] fix(memory/nvs/error/from-esp): replace parameter type from auto to esp_err_t --- DJC-Firmware/src/djc/memory/NVS.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/memory/NVS.hpp b/DJC-Firmware/src/djc/memory/NVS.hpp index c8cdbdb..a3a752e 100644 --- a/DJC-Firmware/src/djc/memory/NVS.hpp +++ b/DJC-Firmware/src/djc/memory/NVS.hpp @@ -28,7 +28,7 @@ struct NvsError { Unknown, } kind; - static constexpr auto fromEsp(const auto e) -> kf::internal::ResultErrorWrapper { + static constexpr auto fromEsp(const esp_err_t e) -> kf::internal::ResultErrorWrapper { switch (e) { case ESP_ERR_NVS_NOT_FOUND: return {NotFound}; From d9e9abed2f0e1db1bf5ee4e5602053f90322321c Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 17:14:48 +0300 Subject: [PATCH 289/326] style(memory/nvs): use explicit before constexpr --- DJC-Firmware/src/djc/memory/NVS.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/memory/NVS.hpp b/DJC-Firmware/src/djc/memory/NVS.hpp index a3a752e..df5af68 100644 --- a/DJC-Firmware/src/djc/memory/NVS.hpp +++ b/DJC-Firmware/src/djc/memory/NVS.hpp @@ -66,7 +66,7 @@ struct NVS final : using ResultType = kf::Result; /// @brief Construct NVS storage component - constexpr explicit NVS(const char *nvs_namespace) noexcept : + explicit constexpr NVS(const char *nvs_namespace) noexcept : _namespace{nvs_namespace} {} [[nodiscard]] ResultType load(kf::Slice buffer) noexcept { From 2bddf7c5514ee1bdc457123d18923aa317cc20b0 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 18:14:42 +0300 Subject: [PATCH 290/326] refactor: make config service type-eraised --- .../src/djc/service/ConfigService.hpp | 85 ++++++++++++------- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 32 ++++++- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 18 ++-- DJC-Firmware/src/main.cpp | 3 +- 4 files changed, 96 insertions(+), 42 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 548f5e0..1b7b287 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -3,34 +3,65 @@ #pragma once +#include + +#include #include +#include #include #include +#include #include -#include "djc/Config.hpp" #include "djc/math.hpp" #include "djc/memory/NVS.hpp" #include "djc/service/Service.hpp" -namespace djc::service { +namespace djc::internal { -/// @brief Config service with delayed NVS operations -/// @note Requests are batched and executed on a 5-second timer from the main loop. -struct ConfigService : Service { +using ConfigView = kf::Slice; + +using CallbackedByConfigView = kf::mixin::Callbacked; + +struct ConfigServiceOnLoadCallbacked : private CallbackedByConfigView { + + /// @brief Set config service behavior on load + template void onLoad(F &&f) noexcept { + this->callback(std::forward(f)); + } + + void invokeOnLoad(ConfigView view) noexcept { + this->invoke(view); + } +}; - explicit constexpr ConfigService(const kf::math::Timer::Config &sync_timer_config) noexcept : - _sync_timer{sync_timer_config} {} +struct ConfigServiceResettingStrategy : private CallbackedByConfigView { - /// @brief Get readonly reference to the current configuration - [[nodiscard]] constexpr const Config &config() const noexcept { - return _config; + /// @brief Set config service resetting strategy + template void resettingStrategy(F &&f) noexcept { + this->callback(std::forward(f)); } - /// @brief Get mutable reference to the current configuration - [[nodiscard]] Config &config() noexcept { - return _config; + void invokeResetStrategy(ConfigView view) noexcept { + this->invoke(view); } +}; + +}// namespace djc::internal + +namespace djc::service { + +/// @brief Config service with delayed NVS operations +/// @note Requests are batched and executed on a 5-second timer from the main loop. +struct ConfigService : + + Service, + internal::ConfigServiceOnLoadCallbacked, + internal::ConfigServiceResettingStrategy + +{ + explicit constexpr ConfigService(const kf::math::Timer::Config &sync_timer_config, kf::Slice config_view) noexcept : + _sync_timer{sync_timer_config}, _config_view{config_view} {} /// @brief Requests an deferred load of the config from NVS void requestLoad() noexcept { @@ -58,14 +89,12 @@ struct ConfigService : Service { logger.info("Loading config from NVS..."); - if (_nvs_entry.load(view()).isOk()) { + if (_nvs_entry.load(_config_view).isOk()) { _stored_crc = crc(); logger.info(LogString::formatted("Config loaded from NVS (CRC: %u)", _stored_crc).view()); - if (not _config.isLatestVersion()) { - logger.warn("Config version is outdated"); - requestReset(); - } + this->invokeOnLoad(_config_view); + } else { logger.error("Config load failed"); requestReset(); @@ -74,15 +103,15 @@ struct ConfigService : Service { if (_reset_requested) { _reset_requested = false; - logger.info("Config reset to defaults"); - _config = djc::Config::defaults(); + this->invokeResetStrategy(_config_view); + logger.info("Config reset to defaults"); } if (const auto current_crc = crc(); current_crc != _stored_crc) { logger.info(LogString::formatted("Config changed, saving (CRC: %u -> %u)...", _stored_crc, current_crc).view()); - if (_nvs_entry.dump(view()).isOk() and _nvs_entry.commit().isOk()) { + if (_nvs_entry.dump(_config_view).isOk() and _nvs_entry.commit().isOk()) { _stored_crc = current_crc; logger.info("Config saved, CRC updated"); } else { @@ -93,23 +122,15 @@ struct ConfigService : Service { private: static constexpr auto logger{kf::Logger::create("ConfigService")}; - - Config _config{djc::Config::defaults()}; + memory::NVS _nvs_entry{"djc"}; + kf::Slice _config_view; kf::math::Timer _sync_timer; kf::u32 _stored_crc{}; bool _load_requested{false}, _reset_requested{false}; - [[nodiscard]] kf::Slice view() noexcept { - return {reinterpret_cast(&_config), sizeof(Config)}; - } - - [[nodiscard]] kf::Slice view() const noexcept { - return const_cast(this)->view(); - } - [[nodiscard]] kf::u32 crc() const noexcept { - return djc::math::crc32(view()); + return djc::math::crc32(_config_view); } KF_IMPL_TIMED_POLLABLE(ConfigService); diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index 53e4097..2509529 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -3,6 +3,7 @@ #pragma once +#include "djc/Config.hpp" #include "djc/mixin/ServiceOwner.hpp" #include "djc/service/ConfigService.hpp" #include "djc/system/System.hpp" @@ -18,15 +19,44 @@ struct ConfigSystem : { explicit ConfigSystem() noexcept : - mixin::ServiceOwner{service::ConfigService{sync_timer_config}} {} + mixin::ServiceOwner{service::ConfigService{ + sync_timer_config, + {reinterpret_cast(&_config), sizeof(Config)}, + }} {} + + /// @brief Get readonly reference to the current configuration + [[nodiscard]] constexpr const Config &config() const noexcept { + return _config; + } + + /// @brief Get mutable reference to the current configuration + [[nodiscard]] Config &config() noexcept { + return _config; + } private: + Config _config{djc::Config::defaults()}; + static constexpr kf::math::Timer::Config sync_timer_config{ .period = 10'000, }; KF_IMPL_INITABLE(ConfigSystem, void()); void initImpl() noexcept { + this->service().resettingStrategy([](kf::Slice view) { + auto &config = *reinterpret_cast(view.data()); + + config = Config::defaults(); + }); + + this->service().onLoad([this](kf::Slice view) { + auto &config = *reinterpret_cast(view.data()); + + if (not _config.isLatestVersion()) { + this->service().requestReset(); + } + }); + this->service().requestLoad(); this->service().sync(); } diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index dc8f269..b8f7232 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -21,9 +21,11 @@ struct ConfigPage : UI::Page { explicit ConfigPage( UI &ui, UI::Page &root, + Config &config, djc::service::ConfigService &config_service, PeerFavoritesRegistry &peer_favoriter_registry) noexcept : Page{ui}, + _config{config}, _config_service{config_service}, _peer_favorite_page{ui, *this, _peer_favoriter_registry}, _peer_favoriter_registry{peer_favoriter_registry}, @@ -45,7 +47,7 @@ struct ConfigPage : UI::Page { this->link().hint("Open Configuration page"); _device_name_input.hint("Device name"); - _device_name_input.source({_config_service.config().device_name.data(), _config_service.config().device_name.size()}); + _device_name_input.source(_config.device_name.slice()); _save_config_button.hint("Force to sync now"); _save_config_button.callback([this]() { @@ -71,17 +73,17 @@ struct ConfigPage : UI::Page { _labeled_default_transport_kind_selector.hint("Define transport select after init"); _default_transport_kind_selector.callback([this](auto item) { - _config_service.config().init_transport_kind = item.value(); + _config.init_transport_kind = item.value(); }); _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); _default_protocol_mode_selector.callback([this](auto item) { - _config_service.config().init_protocol_mode = item.value(); + _config.init_protocol_mode = item.value(); }); _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); _autoconnect_enabled_input.callback([this](bool value) { - _config_service.config().auto_connect_service.enabled = value; + _config.auto_connect_service.enabled = value; }); for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { @@ -97,10 +99,9 @@ struct ConfigPage : UI::Page { } void onEntry() noexcept override { - const auto &config = _config_service.config(); - _default_protocol_mode_selector.value(config.init_protocol_mode); - _default_transport_kind_selector.value(config.init_transport_kind); - _autoconnect_enabled_input.value(config.auto_connect_service.enabled); + _default_protocol_mode_selector.value(_config.init_protocol_mode); + _default_transport_kind_selector.value(_config.init_transport_kind); + _autoconnect_enabled_input.value(_config.auto_connect_service.enabled); const auto all_favorites = _peer_favoriter_registry.all(); @@ -138,6 +139,7 @@ struct ConfigPage : UI::Page { // state + Config &_config; djc::service::ConfigService &_config_service; PeerFavoritesRegistry &_peer_favoriter_registry; kf::memory::StaticString<32> _label_favorites_buffer{}; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 1ea32b9..0ae2db7 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -39,7 +39,7 @@ static constexpr auto logger{kf::Logger::create("main")}; static djc::system::ConfigSystem config_system{}; -static auto &config{config_system.service().config()}; +static auto &config{config_system.config()}; static djc::system::PeripherySystem periphery_system{config}; @@ -99,6 +99,7 @@ static djc::ui::pages::RawProtocolPage raw_protocol_page{ static djc::ui::pages::ConfigPage config_page{ ui_system.service(), ui_system.rootPage(), + config, config_system.service(), peer_system.favoritesRegistry(), }; From 86c458f3816faa4c7264d17099e7445ce3796447 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 18:20:39 +0300 Subject: [PATCH 291/326] fix(ui/pages/config): fix typo --- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index b8f7232..4483946 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -23,12 +23,12 @@ struct ConfigPage : UI::Page { UI::Page &root, Config &config, djc::service::ConfigService &config_service, - PeerFavoritesRegistry &peer_favoriter_registry) noexcept : + PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{ui}, _config{config}, _config_service{config_service}, - _peer_favorite_page{ui, *this, _peer_favoriter_registry}, - _peer_favoriter_registry{peer_favoriter_registry}, + _peer_favorite_page{ui, *this, _peer_favorites_registry}, + _peer_favorites_registry{peer_favorites_registry}, _device_name_input{ui.createTextInput()}, _layout{{ &root.link(), @@ -103,7 +103,7 @@ struct ConfigPage : UI::Page { _default_transport_kind_selector.value(_config.init_transport_kind); _autoconnect_enabled_input.value(_config.auto_connect_service.enabled); - const auto all_favorites = _peer_favoriter_registry.all(); + const auto all_favorites = _peer_favorites_registry.all(); (void) _label_favorites_buffer.format( "[%c] Peer Favorites (%d/%d)", @@ -141,7 +141,7 @@ struct ConfigPage : UI::Page { Config &_config; djc::service::ConfigService &_config_service; - PeerFavoritesRegistry &_peer_favoriter_registry; + PeerFavoritesRegistry &_peer_favorites_registry; kf::memory::StaticString<32> _label_favorites_buffer{}; bool show_favorites{true}; From 1ed1459719c6ee146386e957b417bb4c9d0ae3e7 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 18:29:43 +0300 Subject: [PATCH 292/326] fix(service/config/internal): make invoke..(..) methods protected --- DJC-Firmware/src/djc/service/ConfigService.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 1b7b287..2775be6 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -30,6 +30,7 @@ struct ConfigServiceOnLoadCallbacked : private CallbackedByConfigView { this->callback(std::forward(f)); } +protected: void invokeOnLoad(ConfigView view) noexcept { this->invoke(view); } @@ -42,6 +43,7 @@ struct ConfigServiceResettingStrategy : private CallbackedByConfigView { this->callback(std::forward(f)); } +protected: void invokeResetStrategy(ConfigView view) noexcept { this->invoke(view); } From 169e598c8db68a9056e3d12cd0caa6232e8f32d1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 18:36:33 +0300 Subject: [PATCH 293/326] chore: fix typo --- DJC-Firmware/src/djc/ManualInput.hpp | 6 +++--- DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp | 2 +- DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp | 8 ++++---- DJC-Firmware/src/djc/protocol/Protocol.hpp | 2 +- DJC-Firmware/src/djc/system/PeerSystem.hpp | 4 ++-- DJC-Firmware/src/djc/ui/UI.hpp | 2 +- DJC-Firmware/src/djc/ui/UiTraits.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp | 2 +- DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/DJC-Firmware/src/djc/ManualInput.hpp b/DJC-Firmware/src/djc/ManualInput.hpp index 719f3c4..31b6bdb 100644 --- a/DJC-Firmware/src/djc/ManualInput.hpp +++ b/DJC-Firmware/src/djc/ManualInput.hpp @@ -7,20 +7,20 @@ namespace djc { -/// @brief Normalised manual control input from joysticks +/// @brief Normalized manual control input from joysticks /// @note /// Holds four stick channels (left/right, X/Y) in a fixed‑point format. /// The values are sent to the peer by the active protocol. struct ManualInput final { using Unit = kf::i16; - /// @brief Scale factor - normalised float [‑1, +1] is multiplied by this value + /// @brief Scale factor - normalized float [‑1, +1] is multiplied by this value /// @note Mavlink manual control compatable scaling static constexpr Unit scale_factor{1000}; Unit left_x, left_y, right_x, right_y;///< Joystick axes - /// @brief Convert a normalised float value to the internal unit representation + /// @brief Convert a normalized float value to the internal unit representation [[nodiscard]] static constexpr Unit fromNormalized(kf::f32 value) noexcept { return static_cast(value * scale_factor); } diff --git a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp index f42ffa3..08d58cc 100644 --- a/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp +++ b/DJC-Firmware/src/djc/MavlinkTelemetryRegistry.hpp @@ -10,7 +10,7 @@ namespace djc { -/// @brief Centralised storage for received MAVLink telemetry messages +/// @brief Centralized storage for received MAVLink telemetry messages /// @note Holds a set of typed entries, one for each supported MAVLink message type. struct MavlinkTelemetryRegistry final : kf::mixin::NonCopyable { diff --git a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp index 4e65f1b..c587fd8 100644 --- a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp @@ -62,7 +62,7 @@ struct MavlinkProtocol : using kf::mixin::Configurable::Configurable; - /// @brief Serialise and send a MAVLink message through the given transport. + /// @brief Serialize and send a MAVLink message through the given transport. /// @param transport_link Transport to use for sending. /// @param message The message to send. /// @return true if the transport reported success, false otherwise. @@ -78,9 +78,9 @@ struct MavlinkProtocol : // impl dynamic void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept override { - if (_heartbear_timer.expired(now) or _heartbeat_timer_reset_required) { + if (_heartbeat_timer.expired(now) or _heartbeat_timer_reset_required) { _heartbeat_timer_reset_required = false; - _heartbear_timer.start(now); + _heartbeat_timer.start(now); (void) sendHeartbeat(transport_link); } @@ -100,7 +100,7 @@ struct MavlinkProtocol : } private: - kf::math::Timer _heartbear_timer{this->config().heartbeat_timer}; + kf::math::Timer _heartbeat_timer{this->config().heartbeat_timer}; bool _heartbeat_timer_reset_required{true}; [[nodiscard]] bool sendHeartbeat(transport::TransportLink &transport_link) const noexcept { diff --git a/DJC-Firmware/src/djc/protocol/Protocol.hpp b/DJC-Firmware/src/djc/protocol/Protocol.hpp index f3b6843..b6fcd0c 100644 --- a/DJC-Firmware/src/djc/protocol/Protocol.hpp +++ b/DJC-Firmware/src/djc/protocol/Protocol.hpp @@ -18,7 +18,7 @@ struct Protocol : kf::mixin::NonCopyable { /// @brief Called periodically when the protocol is active and the link is connected /// @note - /// The implementation must serialise the current stick values and send them through the transport. + /// The implementation must serialize the current stick values and send them through the transport. /// Periodic tasks such as heartbeat or keep‑alive messages are also handled here, using `now` to maintain internal timers. virtual void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept = 0; diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp index 36bd4b9..66b290a 100644 --- a/DJC-Firmware/src/djc/system/PeerSystem.hpp +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -18,9 +18,9 @@ namespace djc::system { /// @brief System managing peer favorites, scanning and auto-connection /// @note Owns PeerFavoritesRegistry, PeerScanningService, AutoConnectService. -/// @note Depends on Config (readolny) and TransportLink (for scanning and connection). +/// @note Depends on Config (readonly) and TransportLink (for scanning and connection). /// @note On each poll, scans visible peers and triggers auto-connection to the most trusted visible favorite. -/// @note Peer favorites registry entries source should set extenally +/// @note Peer favorites registry entries source should set externally struct PeerSystem : System diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index ec1a7df..c90a430 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -31,7 +31,7 @@ using UiBase = ::kf::ui::UI<::djc::ui::UiTraits>; namespace djc::ui { -/// @brief ESP32-DJC extended UI specializalization +/// @brief ESP32-DJC extended UI specialization /// @note djc::pages must use fields from this service (`UI::Color`, `UI::Widget`, etc.) struct UI : diff --git a/DJC-Firmware/src/djc/ui/UiTraits.hpp b/DJC-Firmware/src/djc/ui/UiTraits.hpp index 88436d4..6c5cc6f 100644 --- a/DJC-Firmware/src/djc/ui/UiTraits.hpp +++ b/DJC-Firmware/src/djc/ui/UiTraits.hpp @@ -9,7 +9,7 @@ namespace djc::ui { struct UiTraitsTag {}; -/// @brief ESP32-DJC extended UI Traits specializalization +/// @brief ESP32-DJC extended UI Traits specialization /// @tparam W Widget base class /// @note Any Widget from `djc::widgets` should be noted that `` implements `djc::ui::UiTraits` template struct UiTraits : diff --git a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp index 3af3662..d716ccb 100644 --- a/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/MavlinkTelemetryPage.hpp @@ -42,7 +42,7 @@ struct MavlinkTelemetryPage : UI::Page { this->link().hint("Open MAVLink page"); - _imu_display.hint("IMU accel vertor"); + _imu_display.hint("IMU accel vector"); _attitude_display.hint("attitude quaternion"); } diff --git a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp index 48d0e53..4ebd0df 100644 --- a/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/PeerFavoritePage.hpp @@ -38,7 +38,7 @@ struct PeerFavoritePage final : UI::Page { _temp_entry.unwrap().trust = _trust_input.value(); const bool write_ok = _peer_favorites_registry.put(_temp_entry.unwrap()); - _confirm_button.label(write_ok ? "Writed" : "Write failed"); + _confirm_button.label(write_ok ? "Written" : "Write failed"); _confirm_button.style(UI::Style{ .foreground_color = UI::Color::Normal, .background_color = (write_ok ? UI::Color::Success : UI::Color::Error), From 592f5978ece7d6b22431c20d2848845408d1348b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 22:50:21 +0300 Subject: [PATCH 294/326] feat(config): add separated configs and CRTP config base --- DJC-Firmware/src/djc/config/Config.hpp | 48 ++++++++++++++++++ DJC-Firmware/src/djc/config/DeviceConfig.hpp | 51 ++++++++++++++++++++ DJC-Firmware/src/djc/config/UserConfig.hpp | 51 ++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 DJC-Firmware/src/djc/config/Config.hpp create mode 100644 DJC-Firmware/src/djc/config/DeviceConfig.hpp create mode 100644 DJC-Firmware/src/djc/config/UserConfig.hpp diff --git a/DJC-Firmware/src/djc/config/Config.hpp b/DJC-Firmware/src/djc/config/Config.hpp new file mode 100644 index 0000000..14caec8 --- /dev/null +++ b/DJC-Firmware/src/djc/config/Config.hpp @@ -0,0 +1,48 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include "djc/math.hpp" + +namespace djc::config { + +/// @brief Persistent configuration structure +template struct Config : kf::mixin::Resettable { + + kf::u8 version; + + [[nodiscard]] bool isLatest() const noexcept { + return version == latest_version; + } + + [[nodiscard]] constexpr auto view() noexcept -> kf::Slice { + return { + reinterpret_cast(this), + sizeof(Impl), + }; + } + + [[nodiscard]] constexpr auto view() const noexcept -> kf::Slice { + return const_cast(this)->view(); + } + + [[nodiscard]] kf::u32 crc32() const noexcept { + return math::crc32(view()); + } + + [[nodiscard]] static constexpr auto defaults() noexcept { + Impl ret; + + ret.version = latest_version; + ret.reset(); + + return ret; + } +}; + +}// namespace djc::config \ No newline at end of file diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp new file mode 100644 index 0000000..4b384ac --- /dev/null +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -0,0 +1,51 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "djc/Periphery.hpp" +#include "djc/config/Config.hpp" +#include "djc/protocol/ProtocolLink.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" +#include "djc/service/AutoConnectService.hpp" +#include "djc/service/InputHandler.hpp" +#include "djc/service/PeerScanningService.hpp" +#include "djc/transport/TransportLink.hpp" + +namespace djc::config { + +/// @brief Device-related configutation +struct DeviceConfig : Config { + + // periphery + + Periphery::Config periphery; + + // network + + transport::TransportLink::Config transport_link; + protocol::ProtocolLink::Config protocol_link; + protocol::ProtocolRegistry::Config protocol_registry; + + // service configs + + service::InputHandler::Config input_handler; + service::PeerScanningService::Config peer_scanner; + service::AutoConnectService::Config auto_connect_service; + +private: + KF_IMPL_RESETTABLE(DeviceConfig); + void resetImpl() noexcept { + periphery = Periphery::Config::defaults(); + + transport_link = transport::TransportLink::Config::defaults(); + protocol_link = protocol::ProtocolLink::Config::defaults(); + protocol_registry = protocol::ProtocolRegistry::Config::defaults(); + + input_handler = service::InputHandler::Config::defaults(); + peer_scanner = service::PeerScanningService::Config::defaults(); + auto_connect_service = service::AutoConnectService::Config::defaults(); + } +}; + +}// namespace djc::config \ No newline at end of file diff --git a/DJC-Firmware/src/djc/config/UserConfig.hpp b/DJC-Firmware/src/djc/config/UserConfig.hpp new file mode 100644 index 0000000..687d40e --- /dev/null +++ b/DJC-Firmware/src/djc/config/UserConfig.hpp @@ -0,0 +1,51 @@ +// Copyright (c) 2026 KiraFlux +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "djc/PeerFavoritesRegistry.hpp" +#include "djc/config/Config.hpp" +#include "djc/protocol/ProtocolRegistry.hpp" +#include "djc/transport/Kind.hpp" +#include "djc/ui/UI.hpp" + +namespace djc::config { + +/// @brief User Configuration +struct UserConfig : Config { + + static constexpr auto max_peer_favorites{8u}; + + /// @brief Protocol mode that sent after init + protocol::ProtocolRegistry::Mode init_protocol_mode; + + /// @brief Transport kind that sent after init + transport::Kind init_transport_kind; + + /// @brief Human-readable string + kf::memory::Array device_name; + + /// @brief Peer favorites registry entries + kf::memory::Array, max_peer_favorites> peer_favorites; + + /// @brief UI Renderer mics config + ui::UI::Traits::RendererImpl::Config ui_renderer; + +private: + KF_IMPL_RESETTABLE(UserConfig); + void resetImpl() noexcept { + init_protocol_mode = protocol::ProtocolRegistry::Mode::Mavlink; + init_transport_kind = transport::Kind::EspNow; + + device_name = {"ESP32-DJC"}; + + peer_favorites = {}; + + ui_renderer = ui::UI::Traits::RendererImpl::Config::defaults(); + } +}; + +}// namespace djc::config \ No newline at end of file From 745f96c22dee465d1a79737fa5a601cee5351059 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 23:35:27 +0300 Subject: [PATCH 295/326] feat(config): add intepret from slice method --- DJC-Firmware/src/djc/config/Config.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DJC-Firmware/src/djc/config/Config.hpp b/DJC-Firmware/src/djc/config/Config.hpp index 14caec8..6230587 100644 --- a/DJC-Firmware/src/djc/config/Config.hpp +++ b/DJC-Firmware/src/djc/config/Config.hpp @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -20,6 +21,10 @@ template struct Config : kf::mixin::Resett return version == latest_version; } + [[nodiscard]] static constexpr auto interpret(kf::Slice view) noexcept -> kf::Option { + return (view.size() == sizeof(Impl)) ? kf::someRef(*reinterpret_cast(view.data())) : kf::none; + } + [[nodiscard]] constexpr auto view() noexcept -> kf::Slice { return { reinterpret_cast(this), From c2a1858043cd8f3edc1d4a346b33d55e8b35ac43 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 23:35:46 +0300 Subject: [PATCH 296/326] feat(config): add tag base class --- DJC-Firmware/src/djc/config/Config.hpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/config/Config.hpp b/DJC-Firmware/src/djc/config/Config.hpp index 6230587..dbcb37d 100644 --- a/DJC-Firmware/src/djc/config/Config.hpp +++ b/DJC-Firmware/src/djc/config/Config.hpp @@ -12,9 +12,15 @@ namespace djc::config { +struct ConfigTag {}; + /// @brief Persistent configuration structure -template struct Config : kf::mixin::Resettable { +template struct Config : + + ConfigTag, + kf::mixin::Resettable +{ kf::u8 version; [[nodiscard]] bool isLatest() const noexcept { From e60b73d4947d91c67264c754dbd56d38d6c8b5ab Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 23:38:20 +0300 Subject: [PATCH 297/326] refactor!(system/config): make parametrized by config impl --- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 40 ++++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index 2509529..b7245a6 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -3,7 +3,8 @@ #pragma once -#include "djc/Config.hpp" +#include "djc/config/DeviceConfig.hpp" +#include "djc/config/UserConfig.hpp" #include "djc/mixin/ServiceOwner.hpp" #include "djc/service/ConfigService.hpp" #include "djc/system/System.hpp" @@ -11,49 +12,46 @@ namespace djc::system { /// @brief System owning the config service, handling deferred NVS operations +/// @tparam I Config Implementation (Must inherit from `::djc::config::ConfigTag`) /// @note Wraps ConfigService, load on init, and polls it periodically -struct ConfigSystem : +template struct ConfigSystem : System, mixin::ServiceOwner { - explicit ConfigSystem() noexcept : - mixin::ServiceOwner{service::ConfigService{ - sync_timer_config, - {reinterpret_cast(&_config), sizeof(Config)}, - }} {} + KF_CHECK_IMPL(I, ::djc::config::ConfigTag); + using ConfigImpl = I; + + explicit ConfigSystem(const kf::math::Timer::Config &sync_timer_config) noexcept : + mixin::ServiceOwner{service::ConfigService{sync_timer_config, _config.view()}} {} /// @brief Get readonly reference to the current configuration - [[nodiscard]] constexpr const Config &config() const noexcept { + [[nodiscard]] constexpr const ConfigImpl &config() const noexcept { return _config; } /// @brief Get mutable reference to the current configuration - [[nodiscard]] Config &config() noexcept { + [[nodiscard]] ConfigImpl &config() noexcept { return _config; } private: - Config _config{djc::Config::defaults()}; - - static constexpr kf::math::Timer::Config sync_timer_config{ - .period = 10'000, - }; + ConfigImpl _config{ConfigImpl::defaults()}; KF_IMPL_INITABLE(ConfigSystem, void()); void initImpl() noexcept { this->service().resettingStrategy([](kf::Slice view) { - auto &config = *reinterpret_cast(view.data()); - - config = Config::defaults(); + if (auto c = ConfigImpl::interpret(view); c.isSome()) { + c.unwrap().reset(); + } }); this->service().onLoad([this](kf::Slice view) { - auto &config = *reinterpret_cast(view.data()); - - if (not _config.isLatestVersion()) { - this->service().requestReset(); + if (auto c = ConfigImpl::interpret(view); c.isSome()) { + if (not c.unwrap().isLatest()) { + this->service().requestReset(); + } } }); From a068d202c0d856001fd160d1e1e8834f16e9740f Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Wed, 24 Jun 2026 23:39:15 +0300 Subject: [PATCH 298/326] chore!: delete djc/Config.hpp --- DJC-Firmware/src/djc/Config.hpp | 89 --------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 DJC-Firmware/src/djc/Config.hpp diff --git a/DJC-Firmware/src/djc/Config.hpp b/DJC-Firmware/src/djc/Config.hpp deleted file mode 100644 index 9d098b7..0000000 --- a/DJC-Firmware/src/djc/Config.hpp +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) 2026 KiraFlux -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -// lib -#include -#include -#include - -#include "djc/PeerFavoritesRegistry.hpp" -#include "djc/Periphery.hpp" - -// transport -#include "djc/transport/Kind.hpp" -#include "djc/transport/PeerAddress.hpp" -#include "djc/transport/TransportLink.hpp" - -// protocol -#include "djc/protocol/ProtocolLink.hpp" -#include "djc/protocol/ProtocolRegistry.hpp" - -// services -#include "djc/service/AutoConnectService.hpp" -#include "djc/service/InputHandler.hpp" -#include "djc/service/PeerScanningService.hpp" -#include "djc/ui/UI.hpp" - -namespace djc { - -/// @brief Persistent configuration structure stored in NVS. -struct Config { - static constexpr auto - latest_version{10u}, - max_peer_favorites{8u}; - - kf::u16 version; - - // user - protocol::ProtocolRegistry::Mode init_protocol_mode; - transport::Kind init_transport_kind; - kf::memory::Array device_name; - kf::memory::Array, max_peer_favorites> peer_favorites; - - // periphery - Periphery::Config periphery; - - // transport - transport::TransportLink::Config transport_link; - - // protocol - protocol::ProtocolLink::Config protocol_link; - protocol::ProtocolRegistry::Config protocol_registry; - - // services - service::InputHandler::Config input_handler; - service::PeerScanningService::Config peer_scanner; - service::AutoConnectService::Config auto_connect_service; - ui::UI::Traits::RendererImpl::Config ui_renderer; - - [[nodiscard]] bool isLatestVersion() const noexcept { - return version == latest_version; - } - - static constexpr Config defaults() noexcept { - return Config{ - .version = latest_version, - - .init_protocol_mode = protocol::ProtocolRegistry::Mode::Mavlink, - .init_transport_kind = transport::Kind::EspNow, - .device_name = {"ESP32-DJC"}, - .peer_favorites = {}, - - .periphery = Periphery::Config::defaults(), - - .transport_link = transport::TransportLink::Config::defaults(), - - .protocol_link = protocol::ProtocolLink::Config::defaults(), - .protocol_registry = protocol::ProtocolRegistry::Config::defaults(), - - .input_handler = service::InputHandler::Config::defaults(), - .peer_scanner = service::PeerScanningService::Config::defaults(), - .auto_connect_service = service::AutoConnectService::Config::defaults(), - .ui_renderer = ui::UI::Traits::RendererImpl::Config::defaults(), - }; - } -}; - -}// namespace djc \ No newline at end of file From 68330b149478d34b003f2b4b5850aad54b1a35b2 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:35:07 +0300 Subject: [PATCH 299/326] feat(memory/nvs): add namespace getter --- DJC-Firmware/src/djc/memory/NVS.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/DJC-Firmware/src/djc/memory/NVS.hpp b/DJC-Firmware/src/djc/memory/NVS.hpp index df5af68..d8da1ab 100644 --- a/DJC-Firmware/src/djc/memory/NVS.hpp +++ b/DJC-Firmware/src/djc/memory/NVS.hpp @@ -69,6 +69,10 @@ struct NVS final : explicit constexpr NVS(const char *nvs_namespace) noexcept : _namespace{nvs_namespace} {} + [[nodiscard]] constexpr const char *name() const noexcept { + return _namespace; + } + [[nodiscard]] ResultType load(kf::Slice buffer) noexcept { auto len = buffer.size(); return wrap(nvs_get_blob(_handle.unwrap(), blob_key, static_cast(buffer.data()), &len)); From fdf06db4e04672eed549af5b392660511c6a00b9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:35:52 +0300 Subject: [PATCH 300/326] fix(config): remove crc32 method --- DJC-Firmware/src/djc/config/Config.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/DJC-Firmware/src/djc/config/Config.hpp b/DJC-Firmware/src/djc/config/Config.hpp index dbcb37d..4fd7df4 100644 --- a/DJC-Firmware/src/djc/config/Config.hpp +++ b/DJC-Firmware/src/djc/config/Config.hpp @@ -8,8 +8,6 @@ #include #include -#include "djc/math.hpp" - namespace djc::config { struct ConfigTag {}; From a320e479df2aaccf8ee610e2ba520f15edb5037d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:36:14 +0300 Subject: [PATCH 301/326] fix(config): use {} init --- DJC-Firmware/src/djc/config/Config.hpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/DJC-Firmware/src/djc/config/Config.hpp b/DJC-Firmware/src/djc/config/Config.hpp index 4fd7df4..9aac539 100644 --- a/DJC-Firmware/src/djc/config/Config.hpp +++ b/DJC-Firmware/src/djc/config/Config.hpp @@ -40,12 +40,8 @@ template struct Config : return const_cast(this)->view(); } - [[nodiscard]] kf::u32 crc32() const noexcept { - return math::crc32(view()); - } - [[nodiscard]] static constexpr auto defaults() noexcept { - Impl ret; + Impl ret{}; ret.version = latest_version; ret.reset(); From 6068fc30aa944204ff07882a7e6115ea66e176a1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:37:18 +0300 Subject: [PATCH 302/326] fix(config/user): use array from array assignment operator --- DJC-Firmware/src/djc/config/UserConfig.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/config/UserConfig.hpp b/DJC-Firmware/src/djc/config/UserConfig.hpp index 687d40e..7ce14a2 100644 --- a/DJC-Firmware/src/djc/config/UserConfig.hpp +++ b/DJC-Firmware/src/djc/config/UserConfig.hpp @@ -40,7 +40,7 @@ struct UserConfig : Config { init_protocol_mode = protocol::ProtocolRegistry::Mode::Mavlink; init_transport_kind = transport::Kind::EspNow; - device_name = {"ESP32-DJC"}; + device_name = decltype(device_name){"ESP32-DJC"}; peer_favorites = {}; From 92f9c14f14fc62dd2d6742ea1a5ab128e105d5d1 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:38:51 +0300 Subject: [PATCH 303/326] fix(service/config): add nvs namespace argument --- DJC-Firmware/src/djc/service/ConfigService.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 2775be6..6c696ab 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -62,8 +62,8 @@ struct ConfigService : internal::ConfigServiceResettingStrategy { - explicit constexpr ConfigService(const kf::math::Timer::Config &sync_timer_config, kf::Slice config_view) noexcept : - _sync_timer{sync_timer_config}, _config_view{config_view} {} + explicit constexpr ConfigService(const char *nvs_namespace, const kf::math::Timer::Config &sync_timer_config, kf::Slice config_view) noexcept : + _nvs_entry{nvs_namespace}, _sync_timer{sync_timer_config}, _config_view{config_view} {} /// @brief Requests an deferred load of the config from NVS void requestLoad() noexcept { @@ -125,7 +125,7 @@ struct ConfigService : private: static constexpr auto logger{kf::Logger::create("ConfigService")}; - memory::NVS _nvs_entry{"djc"}; + memory::NVS _nvs_entry; kf::Slice _config_view; kf::math::Timer _sync_timer; kf::u32 _stored_crc{}; From dea74c31f1e01bdb8ceb8577bd4ab4394a81a48d Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:39:31 +0300 Subject: [PATCH 304/326] fix(system/config): solve template issues --- DJC-Firmware/src/djc/system/ConfigSystem.hpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/DJC-Firmware/src/djc/system/ConfigSystem.hpp b/DJC-Firmware/src/djc/system/ConfigSystem.hpp index b7245a6..919d806 100644 --- a/DJC-Firmware/src/djc/system/ConfigSystem.hpp +++ b/DJC-Firmware/src/djc/system/ConfigSystem.hpp @@ -16,15 +16,15 @@ namespace djc::system { /// @note Wraps ConfigService, load on init, and polls it periodically template struct ConfigSystem : - System, + System, void()>, mixin::ServiceOwner { KF_CHECK_IMPL(I, ::djc::config::ConfigTag); using ConfigImpl = I; - explicit ConfigSystem(const kf::math::Timer::Config &sync_timer_config) noexcept : - mixin::ServiceOwner{service::ConfigService{sync_timer_config, _config.view()}} {} + explicit ConfigSystem(const char *nvs_namespace) noexcept : + mixin::ServiceOwner{service::ConfigService{nvs_namespace, _sync_timer_config, _config.view()}} {} /// @brief Get readonly reference to the current configuration [[nodiscard]] constexpr const ConfigImpl &config() const noexcept { @@ -37,9 +37,15 @@ template struct ConfigSystem : } private: + static constexpr kf::math::Timer::Config _sync_timer_config{ + .period = 10'000, + }; + ConfigImpl _config{ConfigImpl::defaults()}; - KF_IMPL_INITABLE(ConfigSystem, void()); + using This = ConfigSystem; + + KF_IMPL_INITABLE(This, void()); void initImpl() noexcept { this->service().resettingStrategy([](kf::Slice view) { if (auto c = ConfigImpl::interpret(view); c.isSome()) { @@ -59,7 +65,7 @@ template struct ConfigSystem : this->service().sync(); } - KF_IMPL_TIMED_POLLABLE(ConfigSystem); + KF_IMPL_TIMED_POLLABLE(This); void pollImpl(kf::math::Milliseconds now) noexcept { this->service().poll(now); } From 6e35ff44d6a78da72caee878cea5e3a8b2b6f6c9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:40:12 +0300 Subject: [PATCH 305/326] feat(service/config): add nvs namespace to logging --- DJC-Firmware/src/djc/service/ConfigService.hpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 6c696ab..3f626d8 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -83,22 +83,22 @@ struct ConfigService : // init is idempotent if (_nvs_entry.init().isError()) { - logger.error("NVS init failed"); + logger.error(LogString::formatted("NVS(%s) init failed", _nvs_entry.name())); } if (_load_requested) { _load_requested = false; - logger.info("Loading config from NVS..."); + logger.info(LogString::formatted("Loading config '%s' from NVS...", _nvs_entry.name())); if (_nvs_entry.load(_config_view).isOk()) { _stored_crc = crc(); - logger.info(LogString::formatted("Config loaded from NVS (CRC: %u)", _stored_crc).view()); + logger.info(LogString::formatted("Config '%s' loaded from NVS (CRC: %u)", _nvs_entry.name(), _stored_crc).view()); this->invokeOnLoad(_config_view); } else { - logger.error("Config load failed"); + logger.error(LogString::formatted("Config '%s' load failed", _nvs_entry.name())); requestReset(); } } @@ -107,17 +107,17 @@ struct ConfigService : _reset_requested = false; this->invokeResetStrategy(_config_view); - logger.info("Config reset to defaults"); + logger.info(LogString::formatted("Config reset '%s' to defaults", _nvs_entry.name())); } if (const auto current_crc = crc(); current_crc != _stored_crc) { - logger.info(LogString::formatted("Config changed, saving (CRC: %u -> %u)...", _stored_crc, current_crc).view()); + logger.info(LogString::formatted("Config '%s' changed, saving (CRC: %u -> %u)...", _nvs_entry.name(), _stored_crc, current_crc).view()); if (_nvs_entry.dump(_config_view).isOk() and _nvs_entry.commit().isOk()) { _stored_crc = current_crc; - logger.info("Config saved, CRC updated"); + logger.info(LogString::formatted("Config '%s' saved, CRC updated", _nvs_entry.name())); } else { - logger.error("Config save failed"); + logger.error(LogString::formatted("Config '%s' save failed", _nvs_entry.name())); } } } From bcf653ebd69bf9f17573f8c9c2e5d6ced6a09be4 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:41:02 +0300 Subject: [PATCH 306/326] refactor: firmware uses new config api --- DJC-Firmware/src/djc/system/InputSystem.hpp | 6 +- DJC-Firmware/src/djc/system/PeerSystem.hpp | 7 ++- .../src/djc/system/PeripherySystem.hpp | 4 +- .../src/djc/system/ProtocolSystem.hpp | 4 +- .../src/djc/system/TransportSystem.hpp | 4 +- DJC-Firmware/src/djc/system/UiSystem.hpp | 4 +- DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp | 52 ++++++++++------ DJC-Firmware/src/main.cpp | 61 ++++++++++++------- 8 files changed, 86 insertions(+), 56 deletions(-) diff --git a/DJC-Firmware/src/djc/system/InputSystem.hpp b/DJC-Firmware/src/djc/system/InputSystem.hpp index e7b652d..9bde92b 100644 --- a/DJC-Firmware/src/djc/system/InputSystem.hpp +++ b/DJC-Firmware/src/djc/system/InputSystem.hpp @@ -3,8 +3,8 @@ #pragma once -#include "djc/Config.hpp" #include "djc/Periphery.hpp" +#include "djc/config/DeviceConfig.hpp" #include "djc/mixin/ServiceOwner.hpp" #include "djc/service/InputHandler.hpp" #include "djc/system/System.hpp" @@ -12,14 +12,14 @@ namespace djc::system { /// @brief System managing user input -/// @note Owns InputHandler service. Depends on Config (read‑only) and Periphery. +/// @note Owns InputHandler service. Depends on DeviceConfig (read‑only) and Periphery. struct InputSystem : System, mixin::ServiceOwner { - explicit InputSystem(const Config &config, Periphery &periphery) noexcept : + explicit InputSystem(const config::DeviceConfig &config, Periphery &periphery) noexcept : mixin::ServiceOwner{service::InputHandler{ config.input_handler, periphery.right_joystick,// primary joystick for navigation/control diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp index 66b290a..09ab579 100644 --- a/DJC-Firmware/src/djc/system/PeerSystem.hpp +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -7,8 +7,8 @@ #include -#include "djc/Config.hpp" #include "djc/PeerFavoritesRegistry.hpp" +#include "djc/config/DeviceConfig.hpp" #include "djc/service/AutoConnectService.hpp" #include "djc/service/PeerScanningService.hpp" #include "djc/system/System.hpp" @@ -18,7 +18,7 @@ namespace djc::system { /// @brief System managing peer favorites, scanning and auto-connection /// @note Owns PeerFavoritesRegistry, PeerScanningService, AutoConnectService. -/// @note Depends on Config (readonly) and TransportLink (for scanning and connection). +/// @note Depends on DeviceConfig (readonly) and TransportLink (for scanning and connection). /// @note On each poll, scans visible peers and triggers auto-connection to the most trusted visible favorite. /// @note Peer favorites registry entries source should set externally struct PeerSystem : @@ -26,7 +26,7 @@ struct PeerSystem : System { - explicit PeerSystem(const Config &config, transport::TransportLink &transport_link) noexcept : + explicit PeerSystem(const config::DeviceConfig &config, transport::TransportLink &transport_link) noexcept : _peer_scanning_service{config.peer_scanner, transport_link}, _auto_connect_service{config.auto_connect_service, transport_link} {} @@ -71,6 +71,7 @@ struct PeerSystem : void initImpl(transport::TransportLink &transport_link) noexcept { _peer_scanning_service.init(); + // TODO: move to main _auto_connect_service.callback([&transport_link](const auto &address) -> void { logger.info("Auto Connect"); (void) transport_link.connect(address); diff --git a/DJC-Firmware/src/djc/system/PeripherySystem.hpp b/DJC-Firmware/src/djc/system/PeripherySystem.hpp index 7a304c6..0d76f70 100644 --- a/DJC-Firmware/src/djc/system/PeripherySystem.hpp +++ b/DJC-Firmware/src/djc/system/PeripherySystem.hpp @@ -3,8 +3,8 @@ #pragma once -#include "djc/Config.hpp" #include "djc/Periphery.hpp" +#include "djc/config/DeviceConfig.hpp" #include "djc/system/System.hpp" namespace djc::system { @@ -13,7 +13,7 @@ namespace djc::system { /// @note Provides access to peripherals for other systems via getters; no periodic polling needed. struct PeripherySystem : System { - explicit PeripherySystem(const Config &config) noexcept : + explicit PeripherySystem(const config::DeviceConfig &config) noexcept : _periphery{config.periphery} {} /// @brief Get mutable access to periphery component diff --git a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp index cf682c6..3db0aa4 100644 --- a/DJC-Firmware/src/djc/system/ProtocolSystem.hpp +++ b/DJC-Firmware/src/djc/system/ProtocolSystem.hpp @@ -5,8 +5,8 @@ #include -#include "djc/Config.hpp" #include "djc/MavlinkTelemetryRegistry.hpp" +#include "djc/config/DeviceConfig.hpp" #include "djc/protocol/ProtocolLink.hpp" #include "djc/protocol/ProtocolRegistry.hpp" #include "djc/system/System.hpp" @@ -19,7 +19,7 @@ namespace djc::system { /// @note MAVLink callback is configured in init(). struct ProtocolSystem : System { - explicit ProtocolSystem(const Config &config) noexcept : + explicit ProtocolSystem(const config::DeviceConfig &config) noexcept : _protocol_registry{config.protocol_registry}, _protocol_link{config.protocol_link} {} diff --git a/DJC-Firmware/src/djc/system/TransportSystem.hpp b/DJC-Firmware/src/djc/system/TransportSystem.hpp index 2898377..9d0b750 100644 --- a/DJC-Firmware/src/djc/system/TransportSystem.hpp +++ b/DJC-Firmware/src/djc/system/TransportSystem.hpp @@ -7,7 +7,7 @@ #include -#include "djc/Config.hpp" +#include "djc/config/DeviceConfig.hpp" #include "djc/system/System.hpp" #include "djc/transport/Kind.hpp" #include "djc/transport/TransportLink.hpp" @@ -19,7 +19,7 @@ namespace djc::system { /// @note Initializes WiFi STA mode, and polls the link for connection timeouts. struct TransportSystem : System { - explicit TransportSystem(const Config &config) noexcept : + explicit TransportSystem(const config::DeviceConfig &config) noexcept : _transport_link{config.transport_link} {} /// @brief Get mutable access to transport link component diff --git a/DJC-Firmware/src/djc/system/UiSystem.hpp b/DJC-Firmware/src/djc/system/UiSystem.hpp index 1c72d3d..57b2aa9 100644 --- a/DJC-Firmware/src/djc/system/UiSystem.hpp +++ b/DJC-Firmware/src/djc/system/UiSystem.hpp @@ -8,7 +8,7 @@ #include #include -#include "djc/Config.hpp" +#include "djc/config/UserConfig.hpp" #include "djc/mixin/ServiceOwner.hpp" #include "djc/system/System.hpp" #include "djc/ui/UI.hpp" @@ -27,7 +27,7 @@ struct UiSystem : { using Renderer = ui::UI::Traits::RendererImpl; - explicit UiSystem(const Config &config) noexcept : + explicit UiSystem(const config::UserConfig &config) noexcept : mixin::ServiceOwner{djc::ui::UI{_renderer, _virtual_keyboard}}, _renderer{config.ui_renderer, _buffer.slice()} {} diff --git a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp index 4483946..b418dd2 100644 --- a/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp +++ b/DJC-Firmware/src/djc/ui/pages/ConfigPage.hpp @@ -6,8 +6,9 @@ #include #include -#include "djc/Config.hpp" #include "djc/PeerFavoritesRegistry.hpp" +#include "djc/config/DeviceConfig.hpp" +#include "djc/config/UserConfig.hpp" #include "djc/protocol/ProtocolRegistry.hpp" #include "djc/service/ConfigService.hpp" #include "djc/transport/Kind.hpp" @@ -21,12 +22,16 @@ struct ConfigPage : UI::Page { explicit ConfigPage( UI &ui, UI::Page &root, - Config &config, - djc::service::ConfigService &config_service, + config::DeviceConfig &device_config, + djc::service::ConfigService &device_config_service, + config::UserConfig &user_config, + djc::service::ConfigService &user_config_service, PeerFavoritesRegistry &peer_favorites_registry) noexcept : Page{ui}, - _config{config}, - _config_service{config_service}, + _device_config{device_config}, + _device_config_service{device_config_service}, + _user_config{user_config}, + _user_config_service{user_config_service}, _peer_favorite_page{ui, *this, _peer_favorites_registry}, _peer_favorites_registry{peer_favorites_registry}, _device_name_input{ui.createTextInput()}, @@ -47,21 +52,23 @@ struct ConfigPage : UI::Page { this->link().hint("Open Configuration page"); _device_name_input.hint("Device name"); - _device_name_input.source(_config.device_name.slice()); + _device_name_input.source(_user_config.device_name.slice()); _save_config_button.hint("Force to sync now"); _save_config_button.callback([this]() { - _config_service.sync(); + _device_config_service.sync(); + _user_config_service.sync(); }); _load_config_button.hint("Request Load config from NVS into RAM"); _load_config_button.callback([this]() { - _config_service.requestLoad(); + _device_config_service.requestLoad(); + _user_config_service.requestLoad(); }); _reset_config_button.hint("Request reset RAM config"); _reset_config_button.callback([this]() { - _config_service.requestReset(); + _device_config_service.requestReset(); }); _favorite_peers_fold_toggle_button.hint("Toggle folding"); @@ -73,17 +80,17 @@ struct ConfigPage : UI::Page { _labeled_default_transport_kind_selector.hint("Define transport select after init"); _default_transport_kind_selector.callback([this](auto item) { - _config.init_transport_kind = item.value(); + _user_config.init_transport_kind = item.value(); }); _labeled_default_protocol_mode_selector.hint("Define protocol select after init"); _default_protocol_mode_selector.callback([this](auto item) { - _config.init_protocol_mode = item.value(); + _user_config.init_protocol_mode = item.value(); }); _labeled_autoconnect_enabled_input.hint("Auto connect to most trusted peer"); _autoconnect_enabled_input.callback([this](bool value) { - _config.auto_connect_service.enabled = value; + _device_config.auto_connect_service.enabled = value; }); for (auto i = 0u; i < _peer_favorite_displays.size(); i += 1) { @@ -99,9 +106,9 @@ struct ConfigPage : UI::Page { } void onEntry() noexcept override { - _default_protocol_mode_selector.value(_config.init_protocol_mode); - _default_transport_kind_selector.value(_config.init_transport_kind); - _autoconnect_enabled_input.value(_config.auto_connect_service.enabled); + _default_protocol_mode_selector.value(_user_config.init_protocol_mode); + _default_transport_kind_selector.value(_user_config.init_transport_kind); + _autoconnect_enabled_input.value(_device_config.auto_connect_service.enabled); const auto all_favorites = _peer_favorites_registry.all(); @@ -109,7 +116,7 @@ struct ConfigPage : UI::Page { "[%c] Peer Favorites (%d/%d)", (show_favorites ? 'V' : '>'), all_favorites.size(), - Config::max_peer_favorites); + config::UserConfig::max_peer_favorites); _favorite_peers_fold_toggle_button.label(_label_favorites_buffer.view()); _favorite_peers_fold_toggle_button.background(show_favorites ? UI::Color::Secondary : UI::Color::Primary); @@ -139,9 +146,14 @@ struct ConfigPage : UI::Page { // state - Config &_config; - djc::service::ConfigService &_config_service; + config::DeviceConfig &_device_config; + djc::service::ConfigService &_device_config_service; + + config::UserConfig &_user_config; + djc::service::ConfigService &_user_config_service; + PeerFavoritesRegistry &_peer_favorites_registry; + kf::memory::StaticString<32> _label_favorites_buffer{}; bool show_favorites{true}; @@ -205,14 +217,14 @@ struct ConfigPage : UI::Page { ProtocolModeSelector _default_protocol_mode_selector{_control_mode_config}; UI::Labeled _labeled_default_protocol_mode_selector{"Init Protocol", _default_protocol_mode_selector}; - kf::memory::Array _peer_favorite_displays{}; + kf::memory::Array _peer_favorite_displays{}; UI::CheckBox _autoconnect_enabled_input{false}; UI::Labeled _labeled_autoconnect_enabled_input{"Autoconnect", _autoconnect_enabled_input}; // layout - kf::memory::Array _layout; + kf::memory::Array _layout; // child pages diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 0ae2db7..65b26b7 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -12,6 +12,10 @@ // djc #include "djc/prelude.hpp" +// djc::config +#include "djc/config/DeviceConfig.hpp" +#include "djc/config/UserConfig.hpp" + // djc::system #include "djc/system/ConfigSystem.hpp" #include "djc/system/ControlSystem.hpp" @@ -37,15 +41,23 @@ static constexpr auto logger{kf::Logger::create("main")}; // systems -static djc::system::ConfigSystem config_system{}; +static djc::system::ConfigSystem device_config_system{ + "device", +}; + +static auto &device_config{device_config_system.config()}; -static auto &config{config_system.config()}; +static djc::system::ConfigSystem user_config_system{ + "user", +}; -static djc::system::PeripherySystem periphery_system{config}; +static auto &user_config{user_config_system.config()}; -static djc::system::TransportSystem transport_system{config}; +static djc::system::PeripherySystem periphery_system{device_config}; -static djc::system::ProtocolSystem protocol_system{config}; +static djc::system::TransportSystem transport_system{device_config}; + +static djc::system::ProtocolSystem protocol_system{device_config}; static djc::system::ControlSystem control_system{ periphery_system.periphery(), @@ -54,16 +66,16 @@ static djc::system::ControlSystem control_system{ }; static djc::system::PeerSystem peer_system{ - config, + device_config, transport_system.link(), }; static djc::system::InputSystem input_system{ - config, + device_config, periphery_system.periphery(), }; -static djc::system::UiSystem ui_system{config}; +static djc::system::UiSystem ui_system{user_config}; static djc::system::GraphicsSystem graphics_system{ periphery_system.periphery().display_driver, @@ -99,8 +111,10 @@ static djc::ui::pages::RawProtocolPage raw_protocol_page{ static djc::ui::pages::ConfigPage config_page{ ui_system.service(), ui_system.rootPage(), - config, - config_system.service(), + device_config, + device_config_system.service(), + user_config, + user_config_system.service(), peer_system.favoritesRegistry(), }; @@ -179,23 +193,25 @@ static void onUiRendered(kf::memory::StringView str) { // setups -static void setupPeriphery(djc::Config &config) noexcept { +static void setupPeriphery(djc::config::DeviceConfig &config) noexcept { if (not config.periphery.joystick_axes_tuned) { logger.debug("Tunning axes.."); periphery_system.periphery().tune(config.periphery); } } -static void setupGraphics(djc::Config &config) noexcept { +static void setupGraphics(djc::config::UserConfig &config) noexcept { if (const auto &canvas = graphics_system.canvas(); canvas.isSome()) { config.ui_renderer.text.row_max_length = canvas.unwrap().widthInGlyphs(); config.ui_renderer.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; } } +// TODO: add check: __system_instance__ implements System + #define DJC_SYSTEM_INIT(__system_instance__, ...) \ __system_instance__.init(__VA_ARGS__); \ - logger.info("done: '" #__system_instance__ "'"); + logger.info("done: '" #__system_instance__ "'") void setup() { Serial.begin(115200); @@ -203,29 +219,29 @@ void setup() { // init - DJC_SYSTEM_INIT(config_system); + DJC_SYSTEM_INIT(device_config_system); + DJC_SYSTEM_INIT(user_config_system); DJC_SYSTEM_INIT(periphery_system); - DJC_SYSTEM_INIT(transport_system, config.init_transport_kind); - DJC_SYSTEM_INIT(protocol_system, config.init_protocol_mode); + DJC_SYSTEM_INIT(transport_system, user_config.init_transport_kind); + DJC_SYSTEM_INIT(protocol_system, user_config.init_protocol_mode); DJC_SYSTEM_INIT(peer_system, transport_system.link()); DJC_SYSTEM_INIT(input_system); DJC_SYSTEM_INIT(graphics_system, periphery_system.periphery().display_driver); DJC_SYSTEM_INIT(control_system); DJC_SYSTEM_INIT(ui_system, {&peer_explorer_page, &mavlink_telemetry_page, &raw_protocol_page, &config_page}); - // orcestre + // orchestration transport_system.link().onReceive(onReceiveFromPeer); - peer_system.favoritesRegistry().entries(config.peer_favorites.slice()); - + peer_system.favoritesRegistry().entries(user_config.peer_favorites.slice()); input_system.service().onLeftButton(onSecondaryButtonClick); input_system.service().onRightButton(onPrimaryButtonClick); input_system.service().onDirection(onPrimaryJoystickDirection); ui_system.renderer().callback(onUiRendered); - setupPeriphery(config); - setupGraphics(config); + setupPeriphery(device_config); + setupGraphics(user_config); } void loop() { @@ -233,7 +249,8 @@ void loop() { const auto now = static_cast(millis()); - config_system.poll(now); + device_config_system.poll(now); + user_config_system.poll(now); periphery_system.poll(now); transport_system.poll(now); protocol_system.poll(now); From 5133a4a44c1bb833c000ecb3a1035d67c06e6097 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:43:15 +0300 Subject: [PATCH 307/326] feat(main): add implementation check to init macro --- DJC-Firmware/src/main.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 65b26b7..efe6c39 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -207,10 +207,9 @@ static void setupGraphics(djc::config::UserConfig &config) noexcept { } } -// TODO: add check: __system_instance__ implements System - -#define DJC_SYSTEM_INIT(__system_instance__, ...) \ - __system_instance__.init(__VA_ARGS__); \ +#define DJC_SYSTEM_INIT(__system_instance__, ...) \ + KF_CHECK_IMPL(decltype(__system_instance__), ::djc::system::SystemTag); \ + __system_instance__.init(__VA_ARGS__); \ logger.info("done: '" #__system_instance__ "'") void setup() { From 1d93b97fb6c883c7bed02eb5ea2c390fdf86bed8 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Thu, 25 Jun 2026 00:52:12 +0300 Subject: [PATCH 308/326] refactor(system/peer): move callback function to orchestration state --- DJC-Firmware/src/djc/system/PeerSystem.hpp | 12 +++--------- DJC-Firmware/src/main.cpp | 11 ++++++++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/DJC-Firmware/src/djc/system/PeerSystem.hpp b/DJC-Firmware/src/djc/system/PeerSystem.hpp index 09ab579..903fd57 100644 --- a/DJC-Firmware/src/djc/system/PeerSystem.hpp +++ b/DJC-Firmware/src/djc/system/PeerSystem.hpp @@ -23,7 +23,7 @@ namespace djc::system { /// @note Peer favorites registry entries source should set externally struct PeerSystem : - System + System { explicit PeerSystem(const config::DeviceConfig &config, transport::TransportLink &transport_link) noexcept : @@ -67,15 +67,9 @@ struct PeerSystem : service::PeerScanningService _peer_scanning_service; service::AutoConnectService _auto_connect_service; - KF_IMPL_INITABLE(PeerSystem, void(transport::TransportLink &)); - void initImpl(transport::TransportLink &transport_link) noexcept { + KF_IMPL_INITABLE(PeerSystem, void()); + void initImpl() noexcept { _peer_scanning_service.init(); - - // TODO: move to main - _auto_connect_service.callback([&transport_link](const auto &address) -> void { - logger.info("Auto Connect"); - (void) transport_link.connect(address); - }); } KF_IMPL_TIMED_POLLABLE(PeerSystem); diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index efe6c39..8e6f147 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -144,6 +144,12 @@ static void onReceiveFromPeer(const djc::transport::PeerAddress &address, kf::Sl protocol_system.link().receive(buffer); } +static void onTrustedPeerDiscovered(const djc::transport::PeerAddress &address) noexcept { + logger.info("Auto Connect"); + + (void) transport_system.link().connect(address); +} + static void onReceiveFromLogger(kf::memory::StringView str) noexcept { Serial.write(str.data(), str.size()); } @@ -223,7 +229,7 @@ void setup() { DJC_SYSTEM_INIT(periphery_system); DJC_SYSTEM_INIT(transport_system, user_config.init_transport_kind); DJC_SYSTEM_INIT(protocol_system, user_config.init_protocol_mode); - DJC_SYSTEM_INIT(peer_system, transport_system.link()); + DJC_SYSTEM_INIT(peer_system); DJC_SYSTEM_INIT(input_system); DJC_SYSTEM_INIT(graphics_system, periphery_system.periphery().display_driver); DJC_SYSTEM_INIT(control_system); @@ -232,7 +238,10 @@ void setup() { // orchestration transport_system.link().onReceive(onReceiveFromPeer); + + peer_system.autoConnectService().callback(onTrustedPeerDiscovered); peer_system.favoritesRegistry().entries(user_config.peer_favorites.slice()); + input_system.service().onLeftButton(onSecondaryButtonClick); input_system.service().onRightButton(onPrimaryButtonClick); input_system.service().onDirection(onPrimaryJoystickDirection); From 7361845e0e18f959b234f2a750369918a4d72be9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 26 Jun 2026 18:20:43 +0300 Subject: [PATCH 309/326] refactor(prelude): change st7735 dc pin to GPIO_NUM_16 --- DJC-Firmware/src/djc/prelude.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp index fd8af46..6cd3b0a 100644 --- a/DJC-Firmware/src/djc/prelude.hpp +++ b/DJC-Firmware/src/djc/prelude.hpp @@ -37,8 +37,9 @@ constexpr gpio_num_t // display gpio_display_st7735_spi_cs{GPIO_NUM_5}, - gpio_display_st7735_data_command{GPIO_NUM_22}, + gpio_display_st7735_data_command{GPIO_NUM_16}, gpio_display_st7735_reset{GPIO_NUM_17} + ; using GPIO = kf::gpio::ArduinoGPIO; From dfe874199c1d9c89ac164cdf919b7eb6b66a27d4 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 26 Jun 2026 18:21:02 +0300 Subject: [PATCH 310/326] feat(prelude): add I2C bus --- DJC-Firmware/src/djc/prelude.hpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp index 6cd3b0a..007b7fd 100644 --- a/DJC-Firmware/src/djc/prelude.hpp +++ b/DJC-Firmware/src/djc/prelude.hpp @@ -5,16 +5,20 @@ #include +#include + +#include #include + #include #include #include -#include + #include namespace djc { -constexpr gpio_num_t +constexpr gpio_num_t // buttons @@ -28,6 +32,11 @@ constexpr gpio_num_t gpio_joystick_right_x{GPIO_NUM_34}, gpio_joystick_right_y{GPIO_NUM_35}, + // i2c + + gpio_i2c_sda{GPIO_NUM_21}, + gpio_i2c_scl{GPIO_NUM_22}, + // spi gpio_spi_mosi{GPIO_NUM_23}, @@ -50,6 +59,8 @@ using AxisInput = kf::drivers::sensors::NormalizedAdcInput; using Joystick = kf::drivers::sensors::Joystick; +using IicBus = kf::bus::iic::ArduinoIIC; + using SpiBus = kf::bus::spi::ArduinoSPI; using DisplayDriver = kf::drivers::display::ST7735; From 35c112a7d94ac3113caec9009c22588f0c3e14e4 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 26 Jun 2026 18:29:56 +0300 Subject: [PATCH 311/326] refactor(prelude): change gpio num for buttons --- DJC-Firmware/src/djc/prelude.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp index 007b7fd..56a623e 100644 --- a/DJC-Firmware/src/djc/prelude.hpp +++ b/DJC-Firmware/src/djc/prelude.hpp @@ -22,8 +22,8 @@ constexpr gpio_num_t // buttons - gpio_button_left{GPIO_NUM_14}, - gpio_button_right{GPIO_NUM_4}, + gpio_button_left{GPIO_NUM_26}, + gpio_button_right{GPIO_NUM_25}, // joystick axis From d14d79a4c5ee87d4f3292310b73694d4f229a743 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 26 Jun 2026 18:35:50 +0300 Subject: [PATCH 312/326] feat(prelude): add battery level gpio --- DJC-Firmware/src/djc/prelude.hpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp index 56a623e..7fa932f 100644 --- a/DJC-Firmware/src/djc/prelude.hpp +++ b/DJC-Firmware/src/djc/prelude.hpp @@ -20,25 +20,23 @@ namespace djc { constexpr gpio_num_t - // buttons + // inputs gpio_button_left{GPIO_NUM_26}, gpio_button_right{GPIO_NUM_25}, - // joystick axis - gpio_joystick_left_x{GPIO_NUM_32}, gpio_joystick_left_y{GPIO_NUM_33}, gpio_joystick_right_x{GPIO_NUM_34}, gpio_joystick_right_y{GPIO_NUM_35}, - // i2c + gpio_battery_level{GPIO_NUM_39}, + + // bus gpio_i2c_sda{GPIO_NUM_21}, gpio_i2c_scl{GPIO_NUM_22}, - // spi - gpio_spi_mosi{GPIO_NUM_23}, gpio_spi_miso{GPIO_NUM_19}, gpio_spi_sck{GPIO_NUM_18}, From 4583b5c05d5f7af4bb568e138f20a7ba0ac8cda9 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Fri, 26 Jun 2026 18:47:50 +0300 Subject: [PATCH 313/326] feat(periphery): add i2c bus instance Update DeviceConfig version --- DJC-Firmware/src/djc/Periphery.hpp | 16 ++++++++++++++-- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index a5b1c0c..af98d8e 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -24,6 +24,8 @@ struct PeripheryConfig final { Joystick::Config left_joystick, right_joystick; SpiBus::Config spi_bus; + IicBus::Config iic_bus; + SpiBus::Node::Config display_spi_node; DisplayDriver::Config display_driver; @@ -47,6 +49,7 @@ struct PeripheryConfig final { .y = axisDefaults(true), }, .spi_bus = djc::SpiBus::Config::create(gpio_spi_mosi, gpio_spi_miso, gpio_spi_sck), + .iic_bus = djc::IicBus::Config::create(/* clock: */ 400'000, /* timeout: (=default) */ 0, /* buffer_size: */ 0, gpio_i2c_sda, gpio_i2c_scl), .display_spi_node = djc::SpiBus::Node::Config::create(gpio_display_st7735_spi_cs, 27000000), .display_driver = { .init_orientation = kf::drivers::display::Orientation::ClockWise, @@ -116,6 +119,11 @@ struct Periphery final : SPI, }; + IicBus iic_bus{ + this->config().iic_bus, + Wire, + }; + DisplayDriver display_driver{ this->config().display_driver, spi_bus.createNode(this->config().display_spi_node), @@ -154,11 +162,15 @@ struct Periphery final : right_button_listener.init(); if (spi_bus.init().isError()) { - logger.error("SpiBus initialization failed"); + logger.error("SPI bus init failed"); + } + + if (iic_bus.init().isError()) { + logger.error("I2C bus init failed"); } if (display_driver.init().isError()) { - logger.error("Display driver initialization failed"); + logger.error("Display driver init failed"); } } }; diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index 4b384ac..1e403f9 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -15,7 +15,7 @@ namespace djc::config { /// @brief Device-related configutation -struct DeviceConfig : Config { +struct DeviceConfig : Config { // periphery From 6ac9e018730b30420a64cd1ab0da6ae1161a3037 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 00:49:34 +0300 Subject: [PATCH 314/326] refactor!: move prelude content into periphery --- DJC-Firmware/src/djc/Periphery.hpp | 213 +++++++++++------- DJC-Firmware/src/djc/prelude.hpp | 66 ------ DJC-Firmware/src/djc/service/InputHandler.hpp | 18 +- .../src/djc/system/GraphicsSystem.hpp | 2 +- DJC-Firmware/src/djc/system/InputSystem.hpp | 7 +- DJC-Firmware/src/main.cpp | 5 +- 6 files changed, 140 insertions(+), 171 deletions(-) delete mode 100644 DJC-Firmware/src/djc/prelude.hpp diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index af98d8e..8e49c20 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -3,138 +3,185 @@ #pragma once -#include // for delay +#include // delay, gpio_num_t #include -#include #include #include #include -#include "djc/prelude.hpp" +#include +#include +#include +#include +#include +#include +#include namespace djc { -namespace internal { - -struct PeripheryConfig final { - ButtonListener::Config button; - - AxisInput::FilterImpl::Config axis_filter; - Joystick::Config left_joystick, right_joystick; - - SpiBus::Config spi_bus; - IicBus::Config iic_bus; - - SpiBus::Node::Config display_spi_node; - - DisplayDriver::Config display_driver; - kf::u16 joystick_axes_tune_samples; - bool joystick_axes_tuned; - - static constexpr PeripheryConfig defaults() noexcept { - return PeripheryConfig{ - .button = { - .debounce = 50,// ms - }, - .axis_filter = { - .factor = 0.5f, - }, - .left_joystick = { - .x = axisDefaults(true), - .y = axisDefaults(false), - }, - .right_joystick = { - .x = axisDefaults(false), - .y = axisDefaults(true), - }, - .spi_bus = djc::SpiBus::Config::create(gpio_spi_mosi, gpio_spi_miso, gpio_spi_sck), - .iic_bus = djc::IicBus::Config::create(/* clock: */ 400'000, /* timeout: (=default) */ 0, /* buffer_size: */ 0, gpio_i2c_sda, gpio_i2c_scl), - .display_spi_node = djc::SpiBus::Node::Config::create(gpio_display_st7735_spi_cs, 27000000), - .display_driver = { - .init_orientation = kf::drivers::display::Orientation::ClockWise, - }, - .joystick_axes_tune_samples = 100, - .joystick_axes_tuned = false, - }; - } - -private: - static constexpr AxisInput::Config axisDefaults(bool inverted) noexcept { - return AxisInput::Config{ - .inverted = inverted, - .dead_zone = 200, - .range_positive = 2000, - .range_negative = 2000, - }; - } -}; - -}// namespace internal - /// @brief ESP32-DJC Hardware Periphery struct Periphery final : kf::mixin::NonCopyable, - kf::mixin::Initable, - kf::mixin::Configurable + kf::mixin::Initable { - using Config = internal::PeripheryConfig; - using Configurable::Configurable; + using GPIO = kf::gpio::ArduinoGPIO; + + using ButtonListener = kf::input::LogicalLevelListener; + + using AxisInput = kf::drivers::sensors::NormalizedAdcInput; + + using Joystick = kf::drivers::sensors::Joystick; + + using IicBus = kf::bus::iic::ArduinoIIC; + + using SpiBus = kf::bus::spi::ArduinoSPI; + + using DisplayDriver = kf::drivers::display::ST7735; + + static constexpr gpio_num_t + + // inputs + + gpio_button_left{GPIO_NUM_26}, + gpio_button_right{GPIO_NUM_25}, + + gpio_joystick_left_x{GPIO_NUM_32}, + gpio_joystick_left_y{GPIO_NUM_33}, + gpio_joystick_right_x{GPIO_NUM_34}, + gpio_joystick_right_y{GPIO_NUM_35}, + + gpio_battery_level{GPIO_NUM_39}, + + // bus + + gpio_i2c_sda{GPIO_NUM_21}, + gpio_i2c_scl{GPIO_NUM_22}, + + gpio_spi_mosi{GPIO_NUM_23}, + gpio_spi_miso{GPIO_NUM_19}, + gpio_spi_sck{GPIO_NUM_18}, + + // display + + gpio_display_st7735_spi_cs{GPIO_NUM_5}, + gpio_display_st7735_data_command{GPIO_NUM_16}, + gpio_display_st7735_reset{GPIO_NUM_17} + + ; + + struct Config { + ButtonListener::Config button; + + AxisInput::FilterImpl::Config axis_filter; + Joystick::Config left_joystick, right_joystick; + + SpiBus::Config spi_bus; + IicBus::Config iic_bus; + + SpiBus::Node::Config display_spi_node; + + DisplayDriver::Config display_driver; + kf::u16 joystick_axes_tune_samples; + bool joystick_axes_tuned; + + static constexpr auto defaults() noexcept { + return Config{ + .button = { + .debounce = 50,// ms + }, + .axis_filter = { + .factor = 0.5f, + }, + .left_joystick = { + .x = axisDefaults(true), + .y = axisDefaults(false), + }, + .right_joystick = { + .x = axisDefaults(false), + .y = axisDefaults(true), + }, + .spi_bus = SpiBus::Config::create(gpio_spi_mosi, gpio_spi_miso, gpio_spi_sck), + .iic_bus = IicBus::Config::create(/* clock: */ 400'000, /* timeout: (=default) */ 0, /* buffer_size: */ 0, gpio_i2c_sda, gpio_i2c_scl), + .display_spi_node = SpiBus::Node::Config::create(gpio_display_st7735_spi_cs, 27000000), + .display_driver = { + .init_orientation = kf::drivers::display::Orientation::ClockWise, + }, + .joystick_axes_tune_samples = 100, + .joystick_axes_tuned = false, + }; + } + + private: + static constexpr AxisInput::Config axisDefaults(bool inverted) noexcept { + return AxisInput::Config{ + .inverted = inverted, + .dead_zone = 200, + .range_positive = 2000, + .range_negative = 2000, + }; + } + }; + + explicit Periphery(const Config &config) noexcept : + config{config} {} + + const Config &config; ButtonListener left_button_listener{ - this->config().button, + config.button, GPIO::DigitalInput{ gpio_button_left, GPIO::DigitalInput::Pull::InternalUp, }, }; - Joystick left_joystick{ - this->config().left_joystick, - this->config().axis_filter, - GPIO::AdcInput{gpio_joystick_left_x}, - GPIO::AdcInput{gpio_joystick_left_y}, - }; - ButtonListener right_button_listener{ - this->config().button, + config.button, GPIO::DigitalInput{ gpio_button_right, GPIO::DigitalInput::Pull::InternalUp, }, }; + Joystick left_joystick{ + config.left_joystick, + config.axis_filter, + GPIO::AdcInput{gpio_joystick_left_x}, + GPIO::AdcInput{gpio_joystick_left_y}, + }; + Joystick right_joystick{ - this->config().right_joystick, - this->config().axis_filter, + config.right_joystick, + config.axis_filter, GPIO::AdcInput{gpio_joystick_right_x}, GPIO::AdcInput{gpio_joystick_right_y}, }; SpiBus spi_bus{ - this->config().spi_bus, + config.spi_bus, SPI, }; IicBus iic_bus{ - this->config().iic_bus, + config.iic_bus, Wire, }; DisplayDriver display_driver{ - this->config().display_driver, - spi_bus.createNode(this->config().display_spi_node), + config.display_driver, + spi_bus.createNode(config.display_spi_node), GPIO::DigitalOutput{gpio_display_st7735_data_command}, GPIO::DigitalOutput{gpio_display_st7735_reset}, }; // Analog axis calibration - void tune(Config &mut_config) noexcept { - Joystick::Tuner left_tuner{mut_config.left_joystick, left_joystick, mut_config.joystick_axes_tune_samples}; - Joystick::Tuner right_tuner{mut_config.right_joystick, right_joystick, mut_config.joystick_axes_tune_samples}; + void tune(Config &mutable_config) noexcept { + Joystick::Tuner left_tuner{mutable_config.left_joystick, left_joystick, mutable_config.joystick_axes_tune_samples}; + Joystick::Tuner right_tuner{mutable_config.right_joystick, right_joystick, mutable_config.joystick_axes_tune_samples}; left_tuner.reset(); right_tuner.reset(); @@ -146,7 +193,7 @@ struct Periphery final : delay(1); } - mut_config.joystick_axes_tuned = true; + mutable_config.joystick_axes_tuned = true; } private: diff --git a/DJC-Firmware/src/djc/prelude.hpp b/DJC-Firmware/src/djc/prelude.hpp deleted file mode 100644 index 7fa932f..0000000 --- a/DJC-Firmware/src/djc/prelude.hpp +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2026 KiraFlux -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include - -#include - -#include -#include - -#include -#include -#include - -#include - -namespace djc { - -constexpr gpio_num_t - - // inputs - - gpio_button_left{GPIO_NUM_26}, - gpio_button_right{GPIO_NUM_25}, - - gpio_joystick_left_x{GPIO_NUM_32}, - gpio_joystick_left_y{GPIO_NUM_33}, - gpio_joystick_right_x{GPIO_NUM_34}, - gpio_joystick_right_y{GPIO_NUM_35}, - - gpio_battery_level{GPIO_NUM_39}, - - // bus - - gpio_i2c_sda{GPIO_NUM_21}, - gpio_i2c_scl{GPIO_NUM_22}, - - gpio_spi_mosi{GPIO_NUM_23}, - gpio_spi_miso{GPIO_NUM_19}, - gpio_spi_sck{GPIO_NUM_18}, - - // display - - gpio_display_st7735_spi_cs{GPIO_NUM_5}, - gpio_display_st7735_data_command{GPIO_NUM_16}, - gpio_display_st7735_reset{GPIO_NUM_17} - -; - -using GPIO = kf::gpio::ArduinoGPIO; - -using ButtonListener = kf::input::LogicalLevelListener; - -using AxisInput = kf::drivers::sensors::NormalizedAdcInput; - -using Joystick = kf::drivers::sensors::Joystick; - -using IicBus = kf::bus::iic::ArduinoIIC; - -using SpiBus = kf::bus::spi::ArduinoSPI; - -using DisplayDriver = kf::drivers::display::ST7735; - -}// namespace djc \ No newline at end of file diff --git a/DJC-Firmware/src/djc/service/InputHandler.hpp b/DJC-Firmware/src/djc/service/InputHandler.hpp index da88be7..1c579df 100644 --- a/DJC-Firmware/src/djc/service/InputHandler.hpp +++ b/DJC-Firmware/src/djc/service/InputHandler.hpp @@ -11,13 +11,13 @@ #include #include -#include "djc/prelude.hpp" +#include "djc/Periphery.hpp" #include "djc/service/Service.hpp" namespace djc::service { struct InputHandler final : Service { - using JoystickListener = kf::input::JoystickListener; + using JoystickListener = kf::input::JoystickListener; using ClickCallback = kf::Function; using DirectionCallback = kf::Function; @@ -40,14 +40,10 @@ struct InputHandler final : Service { } }; - explicit InputHandler( - const Config &config, - Joystick &primary_joystick, - ButtonListener &left_button_listener, - ButtonListener &right_button_listener) noexcept : - _joystick_listener{primary_joystick, config.joystick_listener}, - _left_button_listener{left_button_listener}, - _right_button_listener{right_button_listener} {} + explicit InputHandler(const Config &config, Periphery &periphery) noexcept : + _joystick_listener{periphery.right_joystick, config.joystick_listener}, + _left_button_listener{periphery.left_button_listener}, + _right_button_listener{periphery.right_button_listener} {} template void onRightButton(F &&callback) noexcept { _right_click_callback = kf::some(ClickCallback{std::forward(callback)}); @@ -75,7 +71,7 @@ struct InputHandler final : Service { private: JoystickListener _joystick_listener; - ButtonListener &_left_button_listener, &_right_button_listener; + Periphery::ButtonListener &_left_button_listener, &_right_button_listener; kf::Option _direction_callback{kf::none}; kf::Option _left_click_callback{kf::none}, _right_click_callback{kf::none}; diff --git a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp index 4c2f231..0e63d43 100644 --- a/DJC-Firmware/src/djc/system/GraphicsSystem.hpp +++ b/DJC-Firmware/src/djc/system/GraphicsSystem.hpp @@ -133,7 +133,7 @@ template struct GraphicsSystem : using This = GraphicsSystem; KF_IMPL_INITABLE(This, void(I &)); - void initImpl(DisplayDriver &display_driver) noexcept { + void initImpl(I &display_driver) noexcept { _canvas = kf::some(Canvas{ kf::image::DynamicImage{display_driver.image()}, typename Canvas::State{ diff --git a/DJC-Firmware/src/djc/system/InputSystem.hpp b/DJC-Firmware/src/djc/system/InputSystem.hpp index 9bde92b..802de06 100644 --- a/DJC-Firmware/src/djc/system/InputSystem.hpp +++ b/DJC-Firmware/src/djc/system/InputSystem.hpp @@ -20,12 +20,7 @@ struct InputSystem : { explicit InputSystem(const config::DeviceConfig &config, Periphery &periphery) noexcept : - mixin::ServiceOwner{service::InputHandler{ - config.input_handler, - periphery.right_joystick,// primary joystick for navigation/control - periphery.left_button_listener, - periphery.right_button_listener, - }} {} + mixin::ServiceOwner{service::InputHandler{config.input_handler, periphery}} {} private: KF_IMPL_INITABLE(InputSystem, void()); diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 8e6f147..2b728b5 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -9,9 +9,6 @@ #include #include -// djc -#include "djc/prelude.hpp" - // djc::config #include "djc/config/DeviceConfig.hpp" #include "djc/config/UserConfig.hpp" @@ -77,7 +74,7 @@ static djc::system::InputSystem input_system{ static djc::system::UiSystem ui_system{user_config}; -static djc::system::GraphicsSystem graphics_system{ +static djc::system::GraphicsSystem graphics_system{ periphery_system.periphery().display_driver, ui_system.virtualKeyboard(), }; From e8e65d060b32976aa4761a00170d82c1df1ea480 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 01:49:33 +0300 Subject: [PATCH 315/326] epic: Compile-time Display selection Fixes #41 --- DJC-Firmware/platformio.ini | 2 +- DJC-Firmware/src/djc/Periphery.hpp | 71 +++++++++++++++++--- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 2 +- DJC-Firmware/src/djc/ui/UI.hpp | 30 +++++++-- DJC-Firmware/src/main.cpp | 15 ++++- 5 files changed, 100 insertions(+), 20 deletions(-) diff --git a/DJC-Firmware/platformio.ini b/DJC-Firmware/platformio.ini index fab3037..9b4e360 100644 --- a/DJC-Firmware/platformio.ini +++ b/DJC-Firmware/platformio.ini @@ -22,7 +22,7 @@ build_unflags = [env:esp32dev_st7735] extends = env:esp32dev -build_flags = ${env:esp32dev.build_flags} -DDJC_DISPLAY_DRIVER_ST7735 +build_flags = ${env:esp32dev.build_flags} -DDJC_DISPLAY_DRIVER_ST7735 -DDJC_UI_RENDERER_IMPL_TEXTUAL_COLORED [env:esp32dev_ssd1306] extends = env:esp32dev diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index 8e49c20..f723d51 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -10,13 +10,16 @@ #include #include +#include +#include // TODO: move to InputHandler + #include #include + +#include #include #include #include -#include -#include namespace djc { @@ -38,9 +41,25 @@ struct Periphery final : using IicBus = kf::bus::iic::ArduinoIIC; + using SSD1306 = kf::drivers::display::SSD1306; + using SpiBus = kf::bus::spi::ArduinoSPI; - using DisplayDriver = kf::drivers::display::ST7735; + using ST7735 = kf::drivers::display::ST7735; + +#if defined(DJC_DISPLAY_DRIVER_ST7735) + + using DisplayDriver = ST7735; + +#elif defined(DJC_DISPLAY_DRIVER_SSD1306) + + using DisplayDriver = SSD1306; + +#else + +#error DJC_DISPLAY_DRIVER_* Must be defined! + +#endif static constexpr gpio_num_t @@ -74,18 +93,33 @@ struct Periphery final : ; struct Config { + + // Input + ButtonListener::Config button; AxisInput::FilterImpl::Config axis_filter; + Joystick::Config left_joystick, right_joystick; - SpiBus::Config spi_bus; + // I2C + IicBus::Config iic_bus; - SpiBus::Node::Config display_spi_node; + IicBus::Node::Config ssd1306_iic_node; + + // SPI + + SpiBus::Config spi_bus; + + SpiBus::Node::Config st7735_spi_node; + + ST7735::Config st7735; + + // Other - DisplayDriver::Config display_driver; kf::u16 joystick_axes_tune_samples; + bool joystick_axes_tuned; static constexpr auto defaults() noexcept { @@ -104,10 +138,13 @@ struct Periphery final : .x = axisDefaults(false), .y = axisDefaults(true), }, + .iic_bus = IicBus::Config::create(/* clock: */ 400'000, /* timeout: (=default) */ 0, /* buffer_size: */ 0 /* , gpio_i2c_sda, gpio_i2c_scl */), + .ssd1306_iic_node = { + .address = SSD1306::default_address, + }, .spi_bus = SpiBus::Config::create(gpio_spi_mosi, gpio_spi_miso, gpio_spi_sck), - .iic_bus = IicBus::Config::create(/* clock: */ 400'000, /* timeout: (=default) */ 0, /* buffer_size: */ 0, gpio_i2c_sda, gpio_i2c_scl), - .display_spi_node = SpiBus::Node::Config::create(gpio_display_st7735_spi_cs, 27000000), - .display_driver = { + .st7735_spi_node = SpiBus::Node::Config::create(gpio_display_st7735_spi_cs, /* clock: */ 27'000'000), + .st7735 = { .init_orientation = kf::drivers::display::Orientation::ClockWise, }, .joystick_axes_tune_samples = 100, @@ -172,10 +209,22 @@ struct Periphery final : }; DisplayDriver display_driver{ - config.display_driver, - spi_bus.createNode(config.display_spi_node), + +#if defined(DJC_DISPLAY_DRIVER_ST7735) + + config.st7735, + spi_bus.createNode(config.st7735_spi_node), GPIO::DigitalOutput{gpio_display_st7735_data_command}, GPIO::DigitalOutput{gpio_display_st7735_reset}, + +#elif defined(DJC_DISPLAY_DRIVER_SSD1306) + + iic_bus.createNode(config.ssd1306_iic_node), + +#else + +#endif + }; // Analog axis calibration diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index 1e403f9..4b384ac 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -15,7 +15,7 @@ namespace djc::config { /// @brief Device-related configutation -struct DeviceConfig : Config { +struct DeviceConfig : Config { // periphery diff --git a/DJC-Firmware/src/djc/ui/UI.hpp b/DJC-Firmware/src/djc/ui/UI.hpp index c90a430..eb0fb80 100644 --- a/DJC-Firmware/src/djc/ui/UI.hpp +++ b/DJC-Firmware/src/djc/ui/UI.hpp @@ -6,10 +6,9 @@ #include #include #include -#include #include +#include #include -#include #include #include "djc/service/Service.hpp" @@ -18,11 +17,34 @@ #include "djc/ui/widgets/PeerDisplay.hpp" #include "djc/ui/widgets/TextInput.hpp" +#ifdef DJC_UI_RENDERER_IMPL_TEXTUAL_COLORED + +#include + +namespace djc::internal { + +/// @brief Render Engine: Buffered Colored Text UI render engine +using Renderer = ::kf::ui::render::ColoredTextRenderer; + +}// namespace djc::internal + +#else + +#include + +namespace djc::internal { + +using Renderer = ::kf::ui::render::PlainTextRenderer; + +} + +#endif + namespace djc::internal { using WidgetBase = ::kf::ui::widgets::Widget< - ::kf::ui::render::ColoredTextRenderer,// Render Engine: Buffered Colored Text UI render engine - ::kf::ui::Event<6> // Event: 6-bit Event value encoding + Renderer, + ::kf::ui::Event<6>// Event: 6-bit Event value encoding >; using UiBase = ::kf::ui::UI<::djc::ui::UiTraits>; diff --git a/DJC-Firmware/src/main.cpp b/DJC-Firmware/src/main.cpp index 2b728b5..83ac8cf 100644 --- a/DJC-Firmware/src/main.cpp +++ b/DJC-Firmware/src/main.cpp @@ -205,8 +205,17 @@ static void setupPeriphery(djc::config::DeviceConfig &config) noexcept { static void setupGraphics(djc::config::UserConfig &config) noexcept { if (const auto &canvas = graphics_system.canvas(); canvas.isSome()) { - config.ui_renderer.text.row_max_length = canvas.unwrap().widthInGlyphs(); - config.ui_renderer.text.rows_total = canvas.unwrap().heightInGlyphs() - 1; + + auto &textual_renderer_config{ +#ifdef DJC_UI_RENDERER_IMPL_TEXTUAL_COLORED + config.ui_renderer.text +#else + config.ui_renderer +#endif + }; + + textual_renderer_config.row_max_length = canvas.unwrap().widthInGlyphs(); + textual_renderer_config.rows_total = canvas.unwrap().heightInGlyphs() - 1; } } @@ -238,7 +247,7 @@ void setup() { peer_system.autoConnectService().callback(onTrustedPeerDiscovered); peer_system.favoritesRegistry().entries(user_config.peer_favorites.slice()); - + input_system.service().onLeftButton(onSecondaryButtonClick); input_system.service().onRightButton(onPrimaryButtonClick); input_system.service().onDirection(onPrimaryJoystickDirection); From 4fafe896460bbf8efcd0b6cfaebe46788706d77b Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 02:16:24 +0300 Subject: [PATCH 316/326] refactor(periphery/config)!: use resettable mixin --- DJC-Firmware/src/djc/Periphery.hpp | 78 ++++++++++---------- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 2 +- 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/DJC-Firmware/src/djc/Periphery.hpp b/DJC-Firmware/src/djc/Periphery.hpp index f723d51..4353c25 100644 --- a/DJC-Firmware/src/djc/Periphery.hpp +++ b/DJC-Firmware/src/djc/Periphery.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -92,7 +93,7 @@ struct Periphery final : ; - struct Config { + struct Config : kf::mixin::Resettable { // Input @@ -122,44 +123,47 @@ struct Periphery final : bool joystick_axes_tuned; - static constexpr auto defaults() noexcept { - return Config{ - .button = { - .debounce = 50,// ms - }, - .axis_filter = { - .factor = 0.5f, - }, - .left_joystick = { - .x = axisDefaults(true), - .y = axisDefaults(false), - }, - .right_joystick = { - .x = axisDefaults(false), - .y = axisDefaults(true), - }, - .iic_bus = IicBus::Config::create(/* clock: */ 400'000, /* timeout: (=default) */ 0, /* buffer_size: */ 0 /* , gpio_i2c_sda, gpio_i2c_scl */), - .ssd1306_iic_node = { - .address = SSD1306::default_address, - }, - .spi_bus = SpiBus::Config::create(gpio_spi_mosi, gpio_spi_miso, gpio_spi_sck), - .st7735_spi_node = SpiBus::Node::Config::create(gpio_display_st7735_spi_cs, /* clock: */ 27'000'000), - .st7735 = { - .init_orientation = kf::drivers::display::Orientation::ClockWise, - }, - .joystick_axes_tune_samples = 100, - .joystick_axes_tuned = false, - }; + private: + static void resetAxis(AxisInput::Config &axis, bool inverted) noexcept { + axis.inverted = inverted; + axis.dead_zone = 200; + axis.range_positive = 2000; + axis.range_negative = 2000; } - private: - static constexpr AxisInput::Config axisDefaults(bool inverted) noexcept { - return AxisInput::Config{ - .inverted = inverted, - .dead_zone = 200, - .range_positive = 2000, - .range_negative = 2000, - }; + KF_IMPL_RESETTABLE(Config); + void resetImpl() noexcept { + button.debounce = 0; + + axis_filter.factor = 0.5f; + + resetAxis(left_joystick.x, true); + resetAxis(left_joystick.y, false); + + resetAxis(right_joystick.x, false); + resetAxis(right_joystick.y, true); + + iic_bus.clock_hz = 400'000; + iic_bus.timeout = 0; // wire default + iic_bus.buffer_size = 0;// + iic_bus.pin_sda = -1; // + iic_bus.pin_scl = -1; // + + ssd1306_iic_node.address = SSD1306::default_address; + + spi_bus.pin_mosi = gpio_spi_mosi; + spi_bus.pin_miso = gpio_spi_miso; + spi_bus.pin_sck = gpio_spi_sck; + + st7735_spi_node.clock_hz = 27'000'000; + st7735_spi_node.pin_cs = gpio_display_st7735_spi_cs; + st7735_spi_node.bit_order = SpiBus::Node::Config::BitOrder::MostSignificant; + st7735_spi_node.clock_bits = SpiBus::Node::Config::ClockBits::None; + + st7735.init_orientation = kf::drivers::display::Orientation::ClockWise; + + joystick_axes_tune_samples = 100; + joystick_axes_tuned = false; } }; diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index 4b384ac..d85f300 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -36,7 +36,7 @@ struct DeviceConfig : Config { private: KF_IMPL_RESETTABLE(DeviceConfig); void resetImpl() noexcept { - periphery = Periphery::Config::defaults(); + periphery.reset(); transport_link = transport::TransportLink::Config::defaults(); protocol_link = protocol::ProtocolLink::Config::defaults(); From ef73cebb146206513978b2ef155de16bd59d7262 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 15:55:40 +0300 Subject: [PATCH 317/326] refactor!(transport/link/config): use resettable mixin --- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 4 ++-- .../src/djc/transport/TransportLink.hpp | 19 +++++++++---------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index d85f300..42fd1c8 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -14,7 +14,7 @@ namespace djc::config { -/// @brief Device-related configutation +/// @brief Device-related configutation struct DeviceConfig : Config { // periphery @@ -38,7 +38,7 @@ struct DeviceConfig : Config { void resetImpl() noexcept { periphery.reset(); - transport_link = transport::TransportLink::Config::defaults(); + transport_link.reset(); protocol_link = protocol::ProtocolLink::Config::defaults(); protocol_registry = protocol::ProtocolRegistry::Config::defaults(); diff --git a/DJC-Firmware/src/djc/transport/TransportLink.hpp b/DJC-Firmware/src/djc/transport/TransportLink.hpp index 274fa6c..4f737ca 100644 --- a/DJC-Firmware/src/djc/transport/TransportLink.hpp +++ b/DJC-Firmware/src/djc/transport/TransportLink.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "djc/transport/PeerAddress.hpp" @@ -15,16 +16,14 @@ namespace djc::internal { -/// @brief Configuration parameters for the Transport Link. -struct TransportLinkConfig final : kf::mixin::NonCopyable { +/// @brief Configuration parameters for the Transport Link +struct TransportLinkConfig : kf::mixin::Resettable { kf::math::Timer::Config disconnect_timer; - [[nodiscard]] static constexpr TransportLinkConfig defaults() noexcept { - return TransportLinkConfig{ - .disconnect_timer = { - .period = 15'000, - }, - }; +private: + KF_IMPL_RESETTABLE(TransportLinkConfig); + void resetImpl() noexcept { + disconnect_timer.period = 15'000; } }; @@ -32,10 +31,10 @@ struct TransportLinkConfig final : kf::mixin::NonCopyable { namespace djc::transport { -/// @brief Connection manager for a single transport. +/// @brief Connection manager for a single transport /// @note Separates connection lifecycle and inactivity timeout from higher‑level logic. /// Keeps the transport abstract: the rest of the firmware only talks to `TransportLink`, never to a concrete transport. -struct TransportLink final : +struct TransportLink : kf::mixin::NonCopyable, kf::mixin::Configurable, From 78b4b251d63ef48da30dea934fea4c2fcd1140ce Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 15:59:42 +0300 Subject: [PATCH 318/326] refactor!(protocol/link/config): use resettable mixin --- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 2 +- .../src/djc/protocol/ProtocolLink.hpp | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index 42fd1c8..f1ee02e 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -39,7 +39,7 @@ struct DeviceConfig : Config { periphery.reset(); transport_link.reset(); - protocol_link = protocol::ProtocolLink::Config::defaults(); + protocol_link.reset(); protocol_registry = protocol::ProtocolRegistry::Config::defaults(); input_handler = service::InputHandler::Config::defaults(); diff --git a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp index 32a6797..8930a1e 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolLink.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "djc/ManualInput.hpp" @@ -18,17 +19,16 @@ namespace djc::internal { -/// @brief Configuration for the ProtocolLink. -struct ProtocolLinkConfig final { +/// @brief Configuration for the ProtocolLink +struct ProtocolLinkConfig : kf::mixin::Resettable { - kf::math::Timer::Config poll_timer;///< Interval between calls to the active protocol's `poll()` method. + ///@brief Interval between calls to the active protocol's `poll()` method + kf::math::Timer::Config poll_timer; - [[nodiscard]] static constexpr auto defaults() noexcept { - return ProtocolLinkConfig{ - .poll_timer = { - .period = static_cast(1000 / 50), - }, - }; +private: + KF_IMPL_RESETTABLE(ProtocolLinkConfig); + void resetImpl() noexcept { + poll_timer.period = static_cast(1000 / 50); } }; @@ -36,7 +36,7 @@ struct ProtocolLinkConfig final { namespace djc::protocol { -/// @brief Manages the active protocol and calls its `poll()` method at fixed intervals. +/// @brief Manages the active protocol and calls its `poll()` method at fixed intervals /// @note /// Holds a pointer to a `Protocol` instance. /// On every `poll()` call, checks a timer and invokes `_protocol.unwrap().poll()` if the period has expired. From 81af7e969f1e4c2dc4713f65d5ddd5185fee45d6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 16:07:10 +0300 Subject: [PATCH 319/326] refactor!(protocol/registry): use resettable mixin --- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 2 +- .../src/djc/protocol/ProtocolRegistry.hpp | 26 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index f1ee02e..3ff8c22 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -40,7 +40,7 @@ struct DeviceConfig : Config { transport_link.reset(); protocol_link.reset(); - protocol_registry = protocol::ProtocolRegistry::Config::defaults(); + protocol_registry.reset(); input_handler = service::InputHandler::Config::defaults(); peer_scanner = service::PeerScanningService::Config::defaults(); diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp index 4344dd6..1335091 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -5,6 +5,7 @@ #include #include +#include #include "djc/protocol/MavlinkProtocol.hpp" #include "djc/protocol/Protocol.hpp" @@ -12,15 +13,16 @@ namespace djc::internal { -/// @brief Configuration container for the ProtocolRegistry. -struct ProtocolRegistryConfig final { +/// @brief Configuration container for the ProtocolRegistry +struct ProtocolRegistryConfig : kf::mixin::Resettable { - protocol::MavlinkProtocol::Config mavlink;///< MAVLink protocol configuration + /// @brief MAVLink protocol configuration + protocol::MavlinkProtocol::Config mavlink; - [[nodiscard]] static constexpr auto defaults() noexcept { - return ProtocolRegistryConfig{ - .mavlink = protocol::MavlinkProtocol::Config::defaults(), - }; +private: + KF_IMPL_RESETTABLE(ProtocolRegistryConfig); + void resetImpl() noexcept { + mavlink = protocol::MavlinkProtocol::Config::defaults(); } }; @@ -28,7 +30,7 @@ struct ProtocolRegistryConfig final { namespace djc::protocol { -/// @brief Storage for all available protocol implementations. +/// @brief Storage for all available protocol implementations struct ProtocolRegistry final : kf::mixin::NonCopyable, @@ -37,7 +39,7 @@ struct ProtocolRegistry final : { using Config = internal::ProtocolRegistryConfig; - /// @brief Available protocol modes. + /// @brief Available protocol modes enum class Mode : char { Raw = 0x00, ///< Raw binary protocol (sends ManualInput as-is) Mavlink = 0x01,///< MAVLink protocol (sends MANUAL_CONTROL and HEARTBEAT) @@ -45,7 +47,7 @@ struct ProtocolRegistry final : using kf::mixin::Configurable::Configurable; - /// @brief Retrieve a protocol instance by mode. + /// @brief Retrieve a protocol instance by mode /// @param mode Requested protocol mode /// @return Reference to the corresponding protocol object. [[nodiscard]] Protocol &get(Mode mode) noexcept { @@ -59,12 +61,12 @@ struct ProtocolRegistry final : } } - /// @brief Direct access to the Raw protocol instance. + /// @brief Direct access to the Raw protocol instance [[nodiscard]] RawProtocol &raw() noexcept { return _raw_protocol; } - /// @brief Direct access to the MAVLink protocol instance. + /// @brief Direct access to the MAVLink protocol instance [[nodiscard]] MavlinkProtocol &mavlink() noexcept { return _mavlink_protocol; } From 5e7661c0ab9c9dc1b188d80d5e114cdf0f1f8dc6 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 16:21:25 +0300 Subject: [PATCH 320/326] refactor!(protocol/mavlink/config): use resettable mixin --- .../src/djc/protocol/MavlinkProtocol.hpp | 52 +++++++++++-------- .../src/djc/protocol/ProtocolRegistry.hpp | 2 +- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp index c587fd8..f6f96a4 100644 --- a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "djc/ManualInput.hpp" @@ -19,26 +20,33 @@ namespace djc::internal { /// @brief Configuration for the MAVLink protocol -struct MavlinkProtocolConfig final { - - using IdType = kf::u8; - - kf::math::Timer::Config heartbeat_timer;///< Timer for HEARTBEAT messages (ms) - IdType system_id_self; ///< MAVLink system ID of this controller - IdType system_id_target; ///< MAVLink system ID of the target drone (0 = broadcast) - IdType component_id_heartbeat; - IdType component_id_manual_control; - - [[nodiscard]] static constexpr auto defaults() noexcept { - return MavlinkProtocolConfig{ - .heartbeat_timer = { - .period = 2'000,// ms - }, - .system_id_self = 0x7f, - .system_id_target = 0x01, - .component_id_heartbeat = MAV_COMP_ID_USER1, - .component_id_manual_control = MAV_COMP_ID_USER2, - }; +struct MavlinkProtocolConfig : kf::mixin::Resettable { + + /// @brief Timer for HEARTBEAT messages (ms) + kf::math::Timer::Config heartbeat_timer; + + kf::u8 + + /// @brief MAVLink system ID of this controller + system_id_self, + + /// @brief MAVLink system ID of the target drone (0 = broadcast) + system_id_target, + + // components + + component_id_heartbeat, + component_id_manual_control; + +private: + KF_IMPL_RESETTABLE(MavlinkProtocolConfig); + void resetImpl() noexcept { + heartbeat_timer.period = 2'000;// ms + + system_id_self = 0x7f; + system_id_target = 0x01; + component_id_heartbeat = MAV_COMP_ID_USER1; + component_id_manual_control = MAV_COMP_ID_USER2; } }; @@ -54,7 +62,7 @@ namespace djc::protocol { struct MavlinkProtocol : Protocol, - kf::mixin::Callbacked, + kf::mixin::Callbacked,// TODO: add mavlink_chan_t to callback argument for multi-channel work kf::mixin::Configurable { @@ -78,7 +86,7 @@ struct MavlinkProtocol : // impl dynamic void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept override { - if (_heartbeat_timer.expired(now) or _heartbeat_timer_reset_required) { + if (_heartbeat_timer.expired(now) or _heartbeat_timer_reset_required) {// TODO: remove flag _heartbeat_timer_reset_required = false; _heartbeat_timer.start(now); diff --git a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp index 1335091..8b8d5d5 100644 --- a/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp +++ b/DJC-Firmware/src/djc/protocol/ProtocolRegistry.hpp @@ -22,7 +22,7 @@ struct ProtocolRegistryConfig : kf::mixin::Resettable { private: KF_IMPL_RESETTABLE(ProtocolRegistryConfig); void resetImpl() noexcept { - mavlink = protocol::MavlinkProtocol::Config::defaults(); + mavlink.reset(); } }; From 276f78025dc125913d31fa229219604f1e02e846 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 16:33:27 +0300 Subject: [PATCH 321/326] refactor!(service/peer-scanning/config): use resettable mixin --- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 2 +- .../src/djc/service/PeerScanningService.hpp | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index 3ff8c22..bd86abe 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -43,7 +43,7 @@ struct DeviceConfig : Config { protocol_registry.reset(); input_handler = service::InputHandler::Config::defaults(); - peer_scanner = service::PeerScanningService::Config::defaults(); + peer_scanner.reset(); auto_connect_service = service::AutoConnectService::Config::defaults(); } }; diff --git a/DJC-Firmware/src/djc/service/PeerScanningService.hpp b/DJC-Firmware/src/djc/service/PeerScanningService.hpp index 4667b50..c402a33 100644 --- a/DJC-Firmware/src/djc/service/PeerScanningService.hpp +++ b/DJC-Firmware/src/djc/service/PeerScanningService.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "djc/service/Service.hpp" @@ -18,18 +19,20 @@ namespace djc::internal { -/// @brief Configuration parameters for the PeerScanningService service. -struct PeerScannerConfig final { - kf::math::Milliseconds entry_max_life_time; ///< How long an entry stays in the list without being refreshed. - kf::math::Timer::Config entries_list_update_timer;///< Interval between periodic clean-ups and list compaction. - - [[nodiscard]] static constexpr PeerScannerConfig defaults() noexcept { - return PeerScannerConfig{ - .entry_max_life_time = 8'000, - .entries_list_update_timer = { - .period = 100, - }, - }; +/// @brief Configuration parameters for the PeerScanningService service +struct PeerScannerConfig : kf::mixin::Resettable { + + /// @brief How long an entry stays in the list without being refreshed + kf::math::Milliseconds entry_max_life_time; + + /// @brief Interval between periodic clean-ups and list compaction + kf::math::Timer::Config entries_list_update_timer; + +private: + KF_IMPL_RESETTABLE(PeerScannerConfig); + void resetImpl() noexcept { + entry_max_life_time = 8'000; + entries_list_update_timer.period = 100; } }; @@ -37,12 +40,12 @@ struct PeerScannerConfig final { namespace djc::service { -/// @brief Background service that listens for foreign (broadcast) packets and maintains a list of visible peers. +/// @brief Background service that listens for foreign (broadcast) packets and maintains a list of visible peers /// @note /// The scanner subscribes to `TransportLink::onReceiveForeign` and keeps up to `max_entries` entries /// Entries are refreshed every time a packet from the corresponding peer is received. /// Periodically, expired entries are removed and the list is compacted so that the first `peers().size()` elements are always valid. -struct PeerScanningService final : +struct PeerScanningService : Service, kf::mixin::Initable, From 8ecdf41b79a16131127108610af0df803ad62e79 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 16:38:24 +0300 Subject: [PATCH 322/326] refactor!(service/input-handler/config): use resettable mixin --- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 2 +- DJC-Firmware/src/djc/service/InputHandler.hpp | 24 ++++++++----------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index bd86abe..99649dc 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -42,7 +42,7 @@ struct DeviceConfig : Config { protocol_link.reset(); protocol_registry.reset(); - input_handler = service::InputHandler::Config::defaults(); + input_handler.reset(); peer_scanner.reset(); auto_connect_service = service::AutoConnectService::Config::defaults(); } diff --git a/DJC-Firmware/src/djc/service/InputHandler.hpp b/DJC-Firmware/src/djc/service/InputHandler.hpp index 1c579df..558ae1f 100644 --- a/DJC-Firmware/src/djc/service/InputHandler.hpp +++ b/DJC-Firmware/src/djc/service/InputHandler.hpp @@ -10,33 +10,29 @@ #include #include #include +#include #include "djc/Periphery.hpp" #include "djc/service/Service.hpp" namespace djc::service { -struct InputHandler final : Service { +// TODO: make implements 3x callbacked +struct InputHandler : Service { using JoystickListener = kf::input::JoystickListener; using ClickCallback = kf::Function; using DirectionCallback = kf::Function; - struct Config final { + struct Config : kf::mixin::Resettable { JoystickListener::Config joystick_listener; - static constexpr Config defaults() noexcept { - return Config{ - .joystick_listener = JoystickListener::Config{ - .repeat_timer = { - .period = 100,// ms - }, - .delay_timer = { - .period = 400,// ms - }, - .threshold = 0.6f, - }, - }; + private: + KF_IMPL_RESETTABLE(Config); + void resetImpl() noexcept { + joystick_listener.repeat_timer.period = 100;// ms + joystick_listener.delay_timer.period = 400; // ms + joystick_listener.threshold = 0.6f; } }; From 80b8dde67ec9656cacb1f7e426f132d140ffcb9e Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 16:41:23 +0300 Subject: [PATCH 323/326] refactor!(service/auto-connect/config): use resettable mixin --- DJC-Firmware/src/djc/config/DeviceConfig.hpp | 2 +- .../src/djc/service/AutoConnectService.hpp | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/DJC-Firmware/src/djc/config/DeviceConfig.hpp b/DJC-Firmware/src/djc/config/DeviceConfig.hpp index 99649dc..0c1870c 100644 --- a/DJC-Firmware/src/djc/config/DeviceConfig.hpp +++ b/DJC-Firmware/src/djc/config/DeviceConfig.hpp @@ -44,7 +44,7 @@ struct DeviceConfig : Config { input_handler.reset(); peer_scanner.reset(); - auto_connect_service = service::AutoConnectService::Config::defaults(); + auto_connect_service.reset(); } }; diff --git a/DJC-Firmware/src/djc/service/AutoConnectService.hpp b/DJC-Firmware/src/djc/service/AutoConnectService.hpp index e491b9f..a08c250 100644 --- a/DJC-Firmware/src/djc/service/AutoConnectService.hpp +++ b/DJC-Firmware/src/djc/service/AutoConnectService.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "djc/service/Service.hpp" #include "djc/transport/PeerAddress.hpp" @@ -15,18 +16,19 @@ namespace djc::internal { -struct AutoConnectServiceConfig final { +struct AutoConnectServiceConfig : kf::mixin::Resettable { - kf::math::Timer::Config cooldown_timer;///< Delay before the service reacts to a new target. - bool enabled; ///< Whether the service is active. + /// @brief Delay before the service reacts to a new target + kf::math::Timer::Config cooldown_timer; - [[nodiscard]] static constexpr AutoConnectServiceConfig defaults() noexcept { - return AutoConnectServiceConfig{ - .cooldown_timer = { - .period = 10'000, - }, - .enabled = true, - }; + /// @brief Whether the service is active + bool enabled; + +private: + KF_IMPL_RESETTABLE(AutoConnectServiceConfig); + void resetImpl() noexcept { + cooldown_timer.period = 10'000; + enabled = true; } }; From cf0047a163cfa7a895e2ec66ec2528bea30cd233 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 16:43:51 +0300 Subject: [PATCH 324/326] fix(protocol/mavlink): remove timer flag Not needed after toolkit v0.3.3 --- DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp index f6f96a4..5ee7369 100644 --- a/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp +++ b/DJC-Firmware/src/djc/protocol/MavlinkProtocol.hpp @@ -62,7 +62,7 @@ namespace djc::protocol { struct MavlinkProtocol : Protocol, - kf::mixin::Callbacked,// TODO: add mavlink_chan_t to callback argument for multi-channel work + kf::mixin::Callbacked, kf::mixin::Configurable { @@ -86,8 +86,7 @@ struct MavlinkProtocol : // impl dynamic void poll(kf::math::Milliseconds now, const ManualInput &input, transport::TransportLink &transport_link) noexcept override { - if (_heartbeat_timer.expired(now) or _heartbeat_timer_reset_required) {// TODO: remove flag - _heartbeat_timer_reset_required = false; + if (_heartbeat_timer.expired(now)) { _heartbeat_timer.start(now); (void) sendHeartbeat(transport_link); @@ -109,7 +108,6 @@ struct MavlinkProtocol : private: kf::math::Timer _heartbeat_timer{this->config().heartbeat_timer}; - bool _heartbeat_timer_reset_required{true}; [[nodiscard]] bool sendHeartbeat(transport::TransportLink &transport_link) const noexcept { mavlink_message_t message; From 3b27d5501206f72e81cf7a14c5668f10fbf9cd05 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 16:53:47 +0300 Subject: [PATCH 325/326] refactor(memory/nvs): add key parameter to set/get blob methods --- DJC-Firmware/src/djc/memory/NVS.hpp | 10 +++---- .../src/djc/service/ConfigService.hpp | 28 ++++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/DJC-Firmware/src/djc/memory/NVS.hpp b/DJC-Firmware/src/djc/memory/NVS.hpp index d8da1ab..ae5762e 100644 --- a/DJC-Firmware/src/djc/memory/NVS.hpp +++ b/DJC-Firmware/src/djc/memory/NVS.hpp @@ -73,13 +73,13 @@ struct NVS final : return _namespace; } - [[nodiscard]] ResultType load(kf::Slice buffer) noexcept { + [[nodiscard]] ResultType getBlob(const char *key, kf::Slice buffer) noexcept { auto len = buffer.size(); - return wrap(nvs_get_blob(_handle.unwrap(), blob_key, static_cast(buffer.data()), &len)); + return wrap(nvs_get_blob(_handle.unwrap(), key, static_cast(buffer.data()), &len)); } - [[nodiscard]] ResultType dump(kf::Slice buffer) noexcept { - return wrap(nvs_set_blob(_handle.unwrap(), blob_key, buffer.data(), buffer.size())); + [[nodiscard]] ResultType setBlob(const char *key, kf::Slice buffer) noexcept { + return wrap(nvs_set_blob(_handle.unwrap(), key, buffer.data(), buffer.size())); } [[nodiscard]] ResultType commit() noexcept { @@ -87,8 +87,6 @@ struct NVS final : } private: - static constexpr auto blob_key{"blob"}; - const char *_namespace; kf::TrivialOption _handle{kf::none}; diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 3f626d8..503b266 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -63,7 +63,7 @@ struct ConfigService : { explicit constexpr ConfigService(const char *nvs_namespace, const kf::math::Timer::Config &sync_timer_config, kf::Slice config_view) noexcept : - _nvs_entry{nvs_namespace}, _sync_timer{sync_timer_config}, _config_view{config_view} {} + _nvs{nvs_namespace}, _sync_timer{sync_timer_config}, _config_view{config_view} {} /// @brief Requests an deferred load of the config from NVS void requestLoad() noexcept { @@ -82,23 +82,23 @@ struct ConfigService : using LogString = kf::memory::StaticString<64>; // init is idempotent - if (_nvs_entry.init().isError()) { - logger.error(LogString::formatted("NVS(%s) init failed", _nvs_entry.name())); + if (_nvs.init().isError()) { + logger.error(LogString::formatted("NVS(%s) init failed", _nvs.name())); } if (_load_requested) { _load_requested = false; - logger.info(LogString::formatted("Loading config '%s' from NVS...", _nvs_entry.name())); + logger.info(LogString::formatted("Loading config '%s' from NVS...", _nvs.name())); - if (_nvs_entry.load(_config_view).isOk()) { + if (_nvs.getBlob(blob_key, _config_view).isOk()) { _stored_crc = crc(); - logger.info(LogString::formatted("Config '%s' loaded from NVS (CRC: %u)", _nvs_entry.name(), _stored_crc).view()); + logger.info(LogString::formatted("Config '%s' loaded from NVS (CRC: %u)", _nvs.name(), _stored_crc).view()); this->invokeOnLoad(_config_view); } else { - logger.error(LogString::formatted("Config '%s' load failed", _nvs_entry.name())); + logger.error(LogString::formatted("Config '%s' load failed", _nvs.name())); requestReset(); } } @@ -107,17 +107,17 @@ struct ConfigService : _reset_requested = false; this->invokeResetStrategy(_config_view); - logger.info(LogString::formatted("Config reset '%s' to defaults", _nvs_entry.name())); + logger.info(LogString::formatted("Config reset '%s' to defaults", _nvs.name())); } if (const auto current_crc = crc(); current_crc != _stored_crc) { - logger.info(LogString::formatted("Config '%s' changed, saving (CRC: %u -> %u)...", _nvs_entry.name(), _stored_crc, current_crc).view()); + logger.info(LogString::formatted("Config '%s' changed, saving (CRC: %u -> %u)...", _nvs.name(), _stored_crc, current_crc).view()); - if (_nvs_entry.dump(_config_view).isOk() and _nvs_entry.commit().isOk()) { + if (_nvs.setBlob(blob_key, _config_view).isOk() and _nvs.commit().isOk()) { _stored_crc = current_crc; - logger.info(LogString::formatted("Config '%s' saved, CRC updated", _nvs_entry.name())); + logger.info(LogString::formatted("Config '%s' saved, CRC updated", _nvs.name())); } else { - logger.error(LogString::formatted("Config '%s' save failed", _nvs_entry.name())); + logger.error(LogString::formatted("Config '%s' save failed", _nvs.name())); } } } @@ -125,7 +125,9 @@ struct ConfigService : private: static constexpr auto logger{kf::Logger::create("ConfigService")}; - memory::NVS _nvs_entry; + static constexpr auto blob_key{"blob"}; + + memory::NVS _nvs; kf::Slice _config_view; kf::math::Timer _sync_timer; kf::u32 _stored_crc{}; From 5eba654d141034533f6490954e68e22323384b12 Mon Sep 17 00:00:00 2001 From: KiraFlux Date: Sat, 27 Jun 2026 17:55:05 +0300 Subject: [PATCH 326/326] refactor(service/config): make crc32 public --- DJC-Firmware/src/djc/service/ConfigService.hpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/DJC-Firmware/src/djc/service/ConfigService.hpp b/DJC-Firmware/src/djc/service/ConfigService.hpp index 503b266..8c38d3d 100644 --- a/DJC-Firmware/src/djc/service/ConfigService.hpp +++ b/DJC-Firmware/src/djc/service/ConfigService.hpp @@ -77,6 +77,11 @@ struct ConfigService : logger.debug("Reset requested"); } + /// @brief Calculate CRC32 for config view + [[nodiscard]] kf::u32 crc() const noexcept { + return djc::math::crc32(_config_view); + } + /// @brief Force sync now void sync() noexcept { using LogString = kf::memory::StaticString<64>; @@ -133,10 +138,6 @@ struct ConfigService : kf::u32 _stored_crc{}; bool _load_requested{false}, _reset_requested{false}; - [[nodiscard]] kf::u32 crc() const noexcept { - return djc::math::crc32(_config_view); - } - KF_IMPL_TIMED_POLLABLE(ConfigService); void pollImpl(kf::math::Milliseconds now) noexcept { if (_sync_timer.expired(now)) {