From 311a3484e2e1a23db47bcf167784ea440b6a32ff Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:05:39 +0200 Subject: [PATCH] refactor(ui-core): move ping state into PingEngine Single in-flight ping + last result move from UITask to ui-core/PingEngine.h; MyMesh's ping callback routes to the engine instead of casting the Listener back to UITask. UITask keeps thin wrappers (and the "not supported" alert), NearbyScreen is unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/development/ui-core.md | 4 +- examples/companion_radio/ui-core/PingEngine.h | 76 +++++++++++++++++++ examples/companion_radio/ui-core/UiCore.h | 3 + examples/companion_radio/ui-new/UITask.cpp | 65 ++-------------- examples/companion_radio/ui-new/UITask.h | 18 +---- 5 files changed, 92 insertions(+), 74 deletions(-) create mode 100644 examples/companion_radio/ui-core/PingEngine.h diff --git a/docs/development/ui-core.md b/docs/development/ui-core.md index f78c0c4f..a99bfa76 100644 --- a/docs/development/ui-core.md +++ b/docs/development/ui-core.md @@ -39,7 +39,7 @@ interleaved. Classified: - Trail — sampling, auto-pause, low-battery auto-save. - Course over ground — `pushCogFix`, `currentCourse`, `currentLocation`. - Clock tools — alarm / countdown / ring ✅ `ui-core/ClockEngine.h`. -- Ping — `startPing`, `handlePingResult`. +- Ping ✅ `ui-core/PingEngine.h` (timeout still decided by `NearbyScreen`). - Device controls — GPS on/off, GPIO (`setGpioMode`, bot GPIO), buzzer mode/volume, brightness, radio apply (`applyTxPower`, `applyApc`, `applyRadioParams`, …). - Bot hooks — `botSetGPS`, `botBuzz`, `botSetGPIO`, … @@ -151,7 +151,7 @@ checked in the sim plus on L1 hardware before the next one. 0. **Listener boundary** ✅ (upstream `MyMesh::Listener` ported; `MyMesh` has no UI calls left). 1. **Skeleton** ✅. `examples/companion_radio/ui-core/` with `UiCore.h` (facade), `MessageHistory.h`, `DmUnreadTable.h`. Header-only for now, reached from `ui-new/UITask.cpp` by relative include, so none of the 66 variant `platformio.ini` files that build `ui-new` change. `UITask` heap-allocates one `UiCore` in `begin()` (before the screens, as `MessagesScreen` used to own the history on the heap); `MessagesScreen` binds to `core().history` by reference. When the Core grows real `.cpp` files, compile them through a unity `.cpp` inside each frontend directory. -2. **Engines, one per commit.** Clock tools ✅ (also introduced `ui-core/UiEvents.h`, the Core → frontend event queue; `UITask::tickCore()` runs `UiCore::loop()` and drains it) → ping → course-over-ground → live share → locator → trail → notifications. `UITask` shrinks to screen management + drawing. +2. **Engines, one per commit.** Clock tools ✅ (also introduced `ui-core/UiEvents.h`, the Core → frontend event queue; `UITask::tickCore()` runs `UiCore::loop()` and drains it) → ping ✅ → course-over-ground → live share → locator → trail → notifications. `UITask` shrinks to screen management + drawing. 3. **Flip the interface.** `UiCore` implements `AbstractUITask`; `ui-new`'s `UITask` becomes a frontend fed by events. 4. **Settings schema.** Convert `SettingsScreen` section by section. 5. **`ui-lvgl` skeleton** for L2 + sim target: boot, home, message list/conversation, keyboard. Then screens by priority. diff --git a/examples/companion_radio/ui-core/PingEngine.h b/examples/companion_radio/ui-core/PingEngine.h new file mode 100644 index 00000000..c6d4f80f --- /dev/null +++ b/examples/companion_radio/ui-core/PingEngine.h @@ -0,0 +1,76 @@ +#pragma once +// Ping engine: one on-device ping in flight at a time (MyMesh::sendPing), its +// result (RTT + SNR both ways) kept after the in-flight slot is released so the +// view can keep showing it. The reply arrives through MyMesh's ping callback, +// which is a plain function pointer -- routed here via a single static instance. +// +// The ping timeout is still decided by the view (NearbyScreen's +// PING_TIMEOUT_MS), which calls clear() when it gives up. + +class PingEngine { +public: + enum StartResult : uint8_t { STARTED = 0, BUSY, UNSUPPORTED, SEND_FAILED }; + + void begin(NodePrefs* prefs) { _prefs = prefs; s_instance = this; } + + StartResult start(const uint8_t* pub_key) { + if (_active || !pub_key) return BUSY; + if (_prefs && _prefs->path_hash_mode > 1) return UNSUPPORTED; // no 3-byte path hash support + + _active = true; + _tag = 0; + _snr_out_x4 = 0; + _snr_back_x4 = 0; + _rtt_ms = 0; + + // Always install the callback before sending so the response cannot race it. + the_mesh.setPingCallback(onPingResult, NULL); + _tag = the_mesh.sendPing(pub_key, _prefs ? _prefs->path_hash_mode + 1 : 1); + if (_tag == 0) { + clear(); + return SEND_FAILED; + } + return STARTED; + } + + // Release the in-flight slot (result values are kept). + void clear() { + if (_tag != 0) the_mesh.clearPingResult(_tag); + _active = false; + _tag = 0; + } + + bool isActive() const { return _active; } + void getResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const { + snr_out_x4 = _snr_out_x4; + snr_back_x4 = _snr_back_x4; + rtt_ms = _rtt_ms; + } + +private: + void handleResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) { + if (_active && _tag == tag) { + _snr_out_x4 = snr_out_x4; + _snr_back_x4 = snr_back_x4; + _rtt_ms = rtt_ms; + // Release the in-flight slot immediately; the view keeps the result values. + clear(); + } + } + + static void onPingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) { + if (s_instance) s_instance->handleResult(tag, snr_out_x4, snr_back_x4, rtt_ms); + } + + static PingEngine* s_instance; + + NodePrefs* _prefs = nullptr; + bool _active = false; + uint32_t _tag = 0; + int16_t _snr_out_x4 = 0; + int16_t _snr_back_x4 = 0; + uint32_t _rtt_ms = 0; +}; + +// Header-only Core, single TU (see UiCore.h) -- the one definition lives here. +PingEngine* PingEngine::s_instance = nullptr; diff --git a/examples/companion_radio/ui-core/UiCore.h b/examples/companion_radio/ui-core/UiCore.h index fcf24720..0b9a8251 100644 --- a/examples/companion_radio/ui-core/UiCore.h +++ b/examples/companion_radio/ui-core/UiCore.h @@ -11,11 +11,13 @@ #include "DmUnreadTable.h" #include "UiEvents.h" #include "ClockEngine.h" +#include "PingEngine.h" class UiCore { public: void begin(NodePrefs* prefs) { clock.begin(prefs, &events); + ping.begin(prefs); } // Driven from the frontend's loop(), before it drains `events`. @@ -27,6 +29,7 @@ public: // ── Engines ─────────────────────────────────────────────────────────────── ClockEngine clock; // alarm / countdown / ring + PingEngine ping; // single in-flight ping + last result // ── Models ──────────────────────────────────────────────────────────────── MessageHistory history; // channel + DM rings, delivery state, channel unread diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8ca4498b..c78b8a71 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1646,14 +1646,6 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no } } - // Initialize ping state - _ping_active = false; - _ping_tag = 0; - _ping_sent_ms = 0; - _ping_snr_out_x4 = 0; - _ping_snr_back_x4 = 0; - _ping_rtt_ms = 0; - _core = new UiCore(); // before any screen -- MessagesScreen binds to its history _core->begin(node_prefs); splash = new SplashScreen(this); @@ -1775,57 +1767,16 @@ void UITask::gotoMapScreen() { void UITask::gotoLocatorScreen() { setCurrScreen(locator_screen); } void UITask::gotoAutoAdvertScreen() { setCurrScreen(auto_advert_screen); } -// Public method to handle ping result callback -void UITask::handlePingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) { - if (_ping_active && _ping_tag == tag) { - _ping_snr_out_x4 = snr_out_x4; - _ping_snr_back_x4 = snr_back_x4; - _ping_rtt_ms = rtt_ms; - // Release the in-flight slot immediately; the UI keeps the result values. - clearPing(); - } -} - -// Static ping callback (for MyMesh) -static void onPingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) { - MyMesh::Listener* ui = the_mesh.getListener(); - if (ui) { - UITask* task = static_cast(ui); - task->handlePingResult(tag, snr_out_x4, snr_back_x4, rtt_ms); - } -} - -void UITask::clearPing() { - if (_ping_tag != 0) { - the_mesh.clearPingResult(_ping_tag); - } - _ping_active = false; - _ping_tag = 0; -} - bool UITask::startPing(const uint8_t* pub_key) { - if (_ping_active || !pub_key) return false; - if (_node_prefs && _node_prefs->path_hash_mode > 1) { - showAlert("Ping not supported with 3-byte path hashes", 3000); - return false; - } - - _ping_active = true; - _ping_tag = 0; - _ping_sent_ms = millis(); - _ping_snr_out_x4 = 0; - _ping_snr_back_x4 = 0; - _ping_rtt_ms = 0; - - // Always install the callback before sending so the response cannot race it. - the_mesh.setPingCallback(onPingResult, NULL); - _ping_tag = the_mesh.sendPing(pub_key, _node_prefs ? _node_prefs->path_hash_mode + 1 : 1); - if (_ping_tag == 0) { - clearPing(); - return false; - } - return true; + PingEngine::StartResult r = _core->ping.start(pub_key); + if (r == PingEngine::UNSUPPORTED) showAlert("Ping not supported with 3-byte path hashes", 3000); + return r == PingEngine::STARTED; } +bool UITask::isPingActive() const { return _core->ping.isActive(); } +void UITask::getPingResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const { + _core->ping.getResult(snr_out_x4, snr_back_x4, rtt_ms); +} +void UITask::clearPing() { _core->ping.clear(); } void UITask::playMelody(const char* melody) { #ifdef PIN_BUZZER diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index c72e182e..5e5e6677 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -168,13 +168,6 @@ class UITask : public AbstractUITask { uint32_t _next_cog_sample_ms = 0; void pushCogFix(int32_t lat, int32_t lon); - // Ping state - bool _ping_active = false; - uint32_t _ping_tag = 0; - unsigned long _ping_sent_ms = 0; - int16_t _ping_snr_out_x4 = 0; - int16_t _ping_snr_back_x4 = 0; - uint32_t _ping_rtt_ms = 0; void userLedHandler(); @@ -447,16 +440,11 @@ public: bool hasDisplay() const { return _display != NULL; } DisplayDriver* getDisplay() const { return _display; } - // Ping helpers + // Ping helpers (engine in ui-core/PingEngine.h) bool startPing(const uint8_t* pub_key); - bool isPingActive() const { return _ping_active; } - void getPingResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const { - snr_out_x4 = _ping_snr_out_x4; - snr_back_x4 = _ping_snr_back_x4; - rtt_ms = _ping_rtt_ms; - } + bool isPingActive() const; + void getPingResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const; void clearPing(); - void handlePingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms); // Favourites dial helpers. Slot index 0..FAVOURITES_COUNT-1. A slot holds // either a contact/room (pubkey prefix) or a channel (index), per