From 01a068be7d2dcbdf521cfb54a5d9e274cc5a7a9b Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:08:31 +0200 Subject: [PATCH] refactor(companion): shared shutdown steps, SD channel key by secret, fetch race, small UI fixes - UITaskBase holds what both UIs' shutdown() and low-battery check share: the saves before the goodbye sound, the power-down (GPS through its provider) and the battery EMA and threshold. - L2 SD archive: a channel's file is keyed by its secret only, so renaming keeps the history; old files are renamed once at boot. - L2 fetch: abandoning a request and the task finishing it are one critical section (it could stay DONE for good); TileCache gets the read-side barrier. - Text from a repeater or sensor no longer rings on the device (nothing to open); a settings switch reads any non-zero value as on; the message toast never cuts a letter in half; ui-new's GPS helpers ask UiCore. Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/AbstractUITask.h | 31 +++++++++++ .../companion_radio/ui-core/SettingsSchema.h | 1 + examples/companion_radio/ui-core/UiCore.h | 3 + .../companion_radio/ui-lvgl/HistoryStore.h | 23 +++++++- examples/companion_radio/ui-lvgl/LvglPort.h | 10 +++- examples/companion_radio/ui-lvgl/UITask.cpp | 21 ++----- .../companion_radio/ui-lvgl/map/TileCache.h | 3 + examples/companion_radio/ui-new/UITask.cpp | 55 +++---------------- 8 files changed, 82 insertions(+), 65 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 4e32fb23..486b84e9 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -47,6 +47,37 @@ protected: // of its loop() (MyMesh::loop() used to push it at the end of its own). void pollConnection() { setHasConnection(_serial->isBLEConnected()); } + // ── The parts of shutdown() and the low-battery check every UI shares ── + // Before the goodbye sound: whatever a screen not yet left still holds in + // NodePrefs (they save on exit), the clock, the contacts' lazy write. + static void saveBeforeShutdown() { + the_mesh.savePrefs(); + the_mesh.saveRTCTime(); + the_mesh.flushDirtyContacts(); + } + // After it: reboot, or the display, radio and GPS off and the board down. + // The GPS goes through its provider: pins keep their state in nRF52 + // SYSTEMOFF, so the module would go on draining the battery. + void powerDown(bool restart, DisplayDriver* display, SensorManager* sensors) { + if (restart) { _board->reboot(); return; } + if (display) display->turnOff(); + radio_driver.powerOff(); + if (sensors) { + LocationProvider* loc = sensors->getLocationProvider(); + if (loc) loc->stop(); + } + _board->powerOff(); + } + // Settings > Power > Battery shutdown: `mv` smoothed with battEma() every + // 8 s, under the threshold and not on USB power (charging would loop). + static uint16_t battEma(uint16_t prev, uint16_t raw) { + return prev == 0 ? raw : (uint16_t)((prev * 4u + raw) / 5u); // alpha 0.2: the ADC jitters under load + } + bool lowBatteryShutdownDue(uint16_t mv, const NodePrefs* prefs) const { + uint16_t low = prefs ? prefs->low_batt_mv : 0; + return low > 0 && mv > 0 && mv < low && !_board->isExternalPowered(); + } + public: void setHasConnection(bool connected) { bool prev = _connected; diff --git a/examples/companion_radio/ui-core/SettingsSchema.h b/examples/companion_radio/ui-core/SettingsSchema.h index 7d01e001..e8f5589f 100644 --- a/examples/companion_radio/ui-core/SettingsSchema.h +++ b/examples/companion_radio/ui-core/SettingsSchema.h @@ -391,6 +391,7 @@ static void writeRaw(NodePrefs& p, const Setting& s, int32_t v) { // (e.g. an auto-off time set on another frontend). static uint8_t get(const NodePrefs& p, const Setting& s) { int32_t raw = readRaw(p, s); + if (!s.values && s.count == 2 && !s.option) return raw != 0; // a switch: any non-zero is on (advert_loc_policy 2 from the app) if (!s.values) return raw >= 0 && raw < s.count ? (uint8_t)raw : 0; uint8_t best = 0; int32_t best_d = INT32_MAX; diff --git a/examples/companion_radio/ui-core/UiCore.h b/examples/companion_radio/ui-core/UiCore.h index 21827d4a..bd8d2d30 100644 --- a/examples/companion_radio/ui-core/UiCore.h +++ b/examples/companion_radio/ui-core/UiCore.h @@ -205,6 +205,9 @@ public: const uint8_t* extra, int extra_len, const char* text) override { // we only want to show text messages on display, not cli data if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + // Only chats and rooms have a conversation on the device: text from a + // repeater or sensor would ring with nothing to open (the app still gets it). + if (from.type != ADV_TYPE_CHAT && from.type != ADV_TYPE_ROOM) return; if (from.type == ADV_TYPE_ROOM && _room_unread < _queue_len) _room_unread++; if (from.type == ADV_TYPE_CHAT) dm_unread.onIncoming(from.id.pub_key); // before the ring insert below UiEvent& ev = pushMessageArrived(from.type == ADV_TYPE_ROOM ? UIEventType::roomMessage : UIEventType::contactMessage, diff --git a/examples/companion_radio/ui-lvgl/HistoryStore.h b/examples/companion_radio/ui-lvgl/HistoryStore.h index f2a8d616..1ac00be7 100644 --- a/examples/companion_radio/ui-lvgl/HistoryStore.h +++ b/examples/companion_radio/ui-lvgl/HistoryStore.h @@ -46,8 +46,14 @@ struct Hdr { uint32_t cap, head, count, next_id; }; -// FNV-1a over the channel's name and secret. +// FNV-1a over the channel's secret: renaming a channel keeps its archive. static uint32_t chKey(const ChannelDetails& ch) { + uint32_t h = 2166136261u; + for (size_t i = 0; i < sizeof(ch.channel.secret); i++) { h ^= ch.channel.secret[i]; h *= 16777619u; } + return h; +} +// Earlier builds hashed the name in too (see migrateKeys()). +static uint32_t chKeyNamed(const ChannelDetails& ch) { uint32_t h = 2166136261u; for (const char* p = ch.name; *p; p++) { h ^= (uint8_t)*p; h *= 16777619u; } for (size_t i = 0; i < sizeof(ch.channel.secret); i++) { h ^= ch.channel.secret[i]; h *= 16777619u; } @@ -148,6 +154,7 @@ public: // skipped; a channel's entries get its current slot. void restore(MessageHistory& hist) { if (!ready()) return; + migrateKeys(); uint32_t keys[MAX_GROUP_CHANNELS]; bool have[MAX_GROUP_CHANNELS]; for (int i = 0; i < MAX_GROUP_CHANNELS; i++) { @@ -174,6 +181,20 @@ public: } private: + // Files named by the old name+secret key move to the secret-only name, once + // (a file already there under the new name wins). + void migrateKeys() { + for (int i = 0; i < MAX_GROUP_CHANNELS; i++) { + ChannelDetails ch; + if (!the_mesh.getChannel(i, ch) || !ch.name[0]) continue; + char from[64], to[64]; + snprintf(from, sizeof(from), "%s/c_%08lx.bin", DIR_PATH, (unsigned long)chKeyNamed(ch)); + snprintf(to, sizeof(to), "%s/c_%08lx.bin", DIR_PATH, (unsigned long)chKey(ch)); + struct stat st; + if (stat(from, &st) == 0 && stat(to, &st) != 0) rename(from, to); + } + } + bool _ok = false, _tried = false; uint32_t _last_try = 0; uint32_t _keep = KEEP[KEEP_DEFAULT]; diff --git a/examples/companion_radio/ui-lvgl/LvglPort.h b/examples/companion_radio/ui-lvgl/LvglPort.h index 7562a157..4953cb5a 100644 --- a/examples/companion_radio/ui-lvgl/LvglPort.h +++ b/examples/companion_radio/ui-lvgl/LvglPort.h @@ -647,6 +647,7 @@ static uint8_t* s_fbuf = nullptr; static size_t s_fcap = 0; static volatile size_t s_flen = 0; static volatile bool s_fabandoned = false; +static portMUX_TYPE s_fmux = portMUX_INITIALIZER_UNLOCKED; static TaskHandle_t s_ftask = nullptr; static uint32_t s_fstart_ms = 0; static const uint32_t FETCH_MAX_MS = 30000; // a body still trickling in after this is cut off @@ -727,8 +728,10 @@ static void fetchTask(void*) { snprintf(s_ferr, sizeof(s_ferr), "bad URL"); } s_fcode = code; + portENTER_CRITICAL(&s_fmux); // against fetchAbandon() on the UI core: test and set as one if (s_fabandoned) { s_fabandoned = false; s_fstate = F_IDLE; } // nobody wants it any more else s_fstate = code == 200 ? F_DONE : F_FAILED; + portEXIT_CRITICAL(&s_fmux); } } @@ -756,8 +759,13 @@ static const uint8_t* fetchData(size_t& len) { len = s_flen; return s_fbuf; } static void fetchRelease() { if (s_fstate == F_DONE || s_fstate == F_FAILED) s_fstate = F_IDLE; } // Stop caring about the request in flight; it completes (or fails) on its own. static void fetchAbandon() { + // Locked with the task's finish: a request ending between the test and the + // set would otherwise stay DONE with nobody to release it (fetchStart() busy + // for good) and leave the flag to drop the next request's answer. + portENTER_CRITICAL(&s_fmux); if (s_fstate == F_REQUESTED || s_fstate == F_BUSY) s_fabandoned = true; - else fetchRelease(); + else if (s_fstate == F_DONE || s_fstate == F_FAILED) s_fstate = F_IDLE; + portEXIT_CRITICAL(&s_fmux); } static const char* fetchError() { return s_ferr; } // WiFi off: the response buffer (grown to the biggest tile or update chunk, diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 3268f695..15d3d19d 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -382,9 +382,7 @@ uint32_t UITask::idleMillis(uint32_t lv_next) { void UITask::shutdown(bool restart) { s_prefs_dirty_ms = s_prefs_save_at = 0; - the_mesh.savePrefs(); - the_mesh.saveRTCTime(); - the_mesh.flushDirtyContacts(); + saveBeforeShutdown(); if (!trailJournalTick(true)) _core->trail.onShutdown(); // no card: the internal slot, if enabled #ifdef PIN_BUZZER _buzzer.shutdown(); // the goodbye sound, unless muted @@ -392,17 +390,7 @@ void UITask::shutdown(bool restart) { for (uint32_t t0 = millis(); _buzzer.isPlaying() && millis() - t0 < 2500; ) { _buzzer.loop(); delay(10); } #endif #endif - if (restart) { - _board->reboot(); - } else { - if (_display) _display->turnOff(); - radio_driver.powerOff(); - if (_sensors) { - LocationProvider* loc = _sensors->getLocationProvider(); - if (loc) loc->stop(); - } - _board->powerOff(); - } + powerDown(restart, _display, _sensors); } // Settings > Power > Battery shutdown (NodePrefs::low_batt_mv), as @@ -411,9 +399,8 @@ void UITask::checkLowBattery() { if ((int32_t)(millis() - _next_batt_ms) < 0) return; _next_batt_ms = millis() + 8000; uint16_t raw = getBattMilliVolts(); - if (raw > 0) _batt_mv = _batt_mv == 0 ? raw : (uint16_t)((_batt_mv * 4u + raw) / 5u); // EMA, alpha 0.2 - uint16_t low = _prefs ? _prefs->low_batt_mv : 0; - if (low == 0 || _batt_mv == 0 || _batt_mv >= low || _board->isExternalPowered()) return; + if (raw > 0) _batt_mv = battEma(_batt_mv, raw); + if (!lowBatteryShutdownDue(_batt_mv, _prefs)) return; wake(); showToast("Low battery - shutting down", 3000); lv_timer_handler(); diff --git a/examples/companion_radio/ui-lvgl/map/TileCache.h b/examples/companion_radio/ui-lvgl/map/TileCache.h index f558242e..4da4b79b 100644 --- a/examples/companion_radio/ui-lvgl/map/TileCache.h +++ b/examples/companion_radio/ui-lvgl/map/TileCache.h @@ -73,6 +73,9 @@ public: struct Done { int z; int32_t x, y; TileProvider* src; bool overlay_missed; }; bool poll(Done& done) { if (_job_state != JOB_DONE) return false; +#if defined(ESP32) + __sync_synchronize(); // pairs with runJob()'s: the state before the results it covers +#endif done = { _job.z, _job.x, _job.y, _job.src, _job.overlay_missed }; bool keep = !_job_stale; if (keep && !_job.present) { // remember the miss without holding a buffer slot diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index b8d5e4d3..106198a0 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1,3 +1,4 @@ +#include #include "UITask.h" #include "../ui-core/SoundNotifier.h" #include "../ui-core/SoundControl.h" @@ -623,7 +624,8 @@ void UITask::onMessageArrived(const UiEvent& ev) { _alert_expiry = (millis() + BADGE_MS) | 1; } else if (!in_view && style == NodePrefs::MSG_ALERT_NORMAL) { char alert_buf[80]; - snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", ev.text); + int cut = (int)mesh::validUtf8PrefixLength(ev.text, 20); // never half a letter + snprintf(alert_buf, sizeof(alert_buf), "Msg: %.*s", cut, ev.text); showAlert(alert_buf, 2000); } @@ -835,9 +837,7 @@ void UITask::shutdown(bool restart){ // reboot." Unconditional and cheap: an unchanged NodePrefs still writes // identical bytes, same as this codebase's many other direct // the_mesh.savePrefs() call sites already do without a dirty check. - the_mesh.savePrefs(); - the_mesh.saveRTCTime(); - the_mesh.flushDirtyContacts(); + saveBeforeShutdown(); // Auto-save the live GPS trail if enabled (covers low-battery auto-shutdown). _core->trail.onShutdown(); @@ -863,22 +863,7 @@ void UITask::shutdown(bool restart){ #endif // PIN_BUZZER - if (restart) { - _board->reboot(); - } else { - _display->turnOff(); - radio_driver.powerOff(); - // Power GPS down through its provider before SYSTEMOFF — GPIO pins retain - // state in NRF52 SYSTEMOFF, so otherwise the module keeps draining the - // battery. The provider handles the enable + reset pins and the correct - // active level. gps_enabled is persisted; applyGpsPrefs() restores it on - // the next boot. - if (_sensors) { - LocationProvider* loc = _sensors->getLocationProvider(); - if (loc) loc->stop(); - } - _board->powerOff(); - } + powerDown(restart, _display, _sensors); } bool UITask::isButtonPressed() const { @@ -1476,13 +1461,10 @@ void UITask::loop() { // laggy for no benefit the sim actually needs. _batt_mv = raw; #else - // EMA filter: alpha=0.2 (80% old, 20% new) — smooths ADC noise from uneven load - _batt_mv = (_batt_mv == 0) ? raw : (uint16_t)((_batt_mv * 4u + raw) / 5u); + _batt_mv = battEma(_batt_mv, raw); #endif } - uint16_t low_mv = _node_prefs ? _node_prefs->low_batt_mv : 0; - // Don't shut down while on external power (charging) — avoids a shutdown loop. - if (low_mv > 0 && _batt_mv > 0 && _batt_mv < low_mv && !board.isExternalPowered()) { + if (lowBatteryShutdownDue(_batt_mv, _node_prefs)) { if (_display != NULL) { _display->startFrame(); _display->setTextSize(1); @@ -1668,27 +1650,8 @@ char UITask::handleTripleClick(char c) { return 0; } -bool UITask::getGPSState() { - if (_sensors != NULL) { - int num = _sensors->getNumSettings(); - for (int i = 0; i < num; i++) { - if (strcmp(_sensors->getSettingName(i), "gps") == 0) { - return !strcmp(_sensors->getSettingValue(i), "1"); - } - } - } - return false; -} - -bool UITask::hasGPS() { - if (_sensors != NULL) { - int num = _sensors->getNumSettings(); - for (int i = 0; i < num; i++) { - if (strcmp(_sensors->getSettingName(i), "gps") == 0) return true; - } - } - return false; -} +bool UITask::getGPSState() { return _core && _core->gpsEnabled(); } +bool UITask::hasGPS() { return _core && _core->gpsAvailable(); } void UITask::toggleGPS(bool announce) { if (_node_prefs) applyGpsState(_node_prefs->gps_enabled == 0, announce);