From 29f60c5bcd664bd00b628b6bf8ab9341e85efc5d Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:37:04 +1000 Subject: [PATCH] Contacts: cap chunked save to twice a day; flush on low battery Throttle the ESP32 lazy contact save from a 5s debounce to a 12h interval (CONTACTS_SAVE_INTERVAL). A new _nextContactSaveDue anchors the next flush 12h out on the first dirty pass and only writes once reached, clearing after each save so the next dirty event re-anchors. Cuts the recurring ~95KB LittleFS rewrites on busy meshes. nRF52/STM32 blocking-save path unchanged. Flush contacts (blocking) in the AUTO_SHUTDOWN_MILLIVOLTS path before shutdown() so low-battery power-off doesn't drop up to 12h of contact learning. Manual power-off left untouched. Gate the SHUTDOWN home page out on the watch (MECK_TWATCH): removed from the HomePage enum with render/input/isOnShutdownPage guarded. The AXP2101 PWR-button long-press is the shutdown path there. Files: MyMesh.h, MyMesh.cpp, UITask.cpp --- examples/companion_radio/MyMesh.cpp | 13 +++++++++++-- examples/companion_radio/MyMesh.h | 1 + examples/companion_radio/ui-new/UITask.cpp | 17 ++++++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ca6faaf7..e7386fef 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -119,6 +119,7 @@ #define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250 #define LAZY_CONTACTS_WRITE_DELAY 5000 #define USER_IDLE_SAVE_THRESHOLD 15000 // Defer saves until 15s after last keypress +#define CONTACTS_SAVE_INTERVAL 43200000 // 12h: cap chunked contact flush to twice a day #define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg==" @@ -1495,6 +1496,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe next_ack_idx = 0; sign_data = NULL; dirty_contacts_expiry = 0; + _nextContactSaveDue = 0; advert_paths = nullptr; // PSRAM-allocated in begin() _rxlog = nullptr; // PSRAM-allocated in begin() _rxlog_head = 0; @@ -3714,8 +3716,15 @@ void MyMesh::loop() { // Voice session or active keyboard use -- push save forward dirty_contacts_expiry = futureMillis(2000); } else if (!_store->isSaveInProgress()) { - _store->beginSaveContacts(this); - dirty_contacts_expiry = 0; + // Cap the chunked flush to CONTACTS_SAVE_INTERVAL (twice a day): anchor + // the next-due time on the first dirty pass, then only write once it is + // reached. Cleared after a save so the next dirty event re-anchors. + if (_nextContactSaveDue == 0) _nextContactSaveDue = futureMillis(CONTACTS_SAVE_INTERVAL); + if (millisHasNowPassed(_nextContactSaveDue)) { + _store->beginSaveContacts(this); + dirty_contacts_expiry = 0; + _nextContactSaveDue = 0; + } } #endif } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 163f7685..314cda59 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -346,6 +346,7 @@ private: uint8_t *sign_data; uint32_t sign_data_len; unsigned long dirty_contacts_expiry; + unsigned long _nextContactSaveDue; // ESP32 chunked save: next scheduled flush (0 = unscheduled) TransportKey send_scope; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index c755b4bc..d7607d37 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -183,7 +183,9 @@ class HomeScreen : public UIScreen { #if HAS_BQ27220 BATTERY, #endif +#if !defined(MECK_TWATCH) SHUTDOWN, +#endif Count // keep as last }; @@ -366,7 +368,11 @@ public: bool isEditingUTC() const { return _editing_utc; } bool isFirstPage() const { return _page == HomePage::FIRST; } bool isOnRecentPage() const { return _page == HomePage::RECENT; } +#if defined(MECK_TWATCH) + bool isOnShutdownPage() const { return false; } +#else bool isOnShutdownPage() const { return _page == HomePage::SHUTDOWN; } +#endif void cancelEditing() { if (_editing_utc) { _node_prefs->utc_offset_hours = _saved_utc_offset; @@ -1260,7 +1266,9 @@ public: sprintf(buf, "%d.%d C", battTemp / 10, abs(battTemp % 10)); display.drawTextRightAlign(display.width()-1-EINK_X_OFFSET, y, buf); #endif - } else if (_page == HomePage::SHUTDOWN) { + } +#if !defined(MECK_TWATCH) + else if (_page == HomePage::SHUTDOWN) { display.setColor(DisplayDriver::GREEN); display.setTextSize(1); if (_shutdown_init) { @@ -1304,6 +1312,7 @@ public: display.drawTextCentered(display.width() / 2, y2, line2); } } +#endif return _editing_utc ? 700 : 5000; } @@ -1342,6 +1351,7 @@ public: return true; // Consume all other keys while editing } +#if !defined(MECK_TWATCH) // SHUTDOWN page -- intercept up/down and Enter before page cycling if (_page == HomePage::SHUTDOWN) { if (_poweroff_confirm) { @@ -1376,6 +1386,7 @@ public: } // Left/right fall through to page cycling below } +#endif if (c == KEY_LEFT || c == KEY_PREV || c == 'a') { _page = (_page + HomePage::Count - 1) % HomePage::Count; @@ -2894,6 +2905,10 @@ if (curr) curr->poll(); } #endif + // Flush contacts before the battery dies -- the chunked lazy save may not + // have run for hours. Blocking save (tmp-then-rename, brownout-safe). + the_mesh.saveContacts(); + shutdown(); } } else {