fix(prefs): flush settings on every reboot/shutdown path, not just per-screen exit

Root cause of "settings don't survive a reboot": every screen that edits
NodePrefs (Settings, Bot, Trail, Locator, GPS sharing, etc.) only persisted
via its own savePrefsIfDirty(_dirty) call on its own Cancel/exit path.
UITask::shutdown() -- the one function behind every real power-off/reboot,
including the low-battery auto-shutdown that fires directly from
UITask::loop() regardless of which screen is on-screen -- never called
the_mesh.savePrefs() at all. A user who changed a setting and then hit any
of those paths without first explicitly backing out of the screen (e.g.
display auto-off mid-edit, then a low-battery auto-shutdown) silently lost
the change.

Also closes two direct board.reboot() bypasses that skipped shutdown()
entirely: the phone-app CMD_REBOOT command and the serial CLI "reboot"
command. The factory-reset reboot path is deliberately left alone -- it's
supposed to wipe everything.

Verified with a real regression test (not just "it compiles"): set a pref
in memory without flushing it, cross the real low_batt_mv threshold to
trigger the actual production low-battery-shutdown code path, then a real
page.reload(), then confirm behaviourally (does an incoming message wake
the screen or not) whether the setting survived. Temporarily reverted the
fix and re-ran to confirm the test actually fails without it (screen woke,
setting lost) before restoring it and confirming it passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-08 08:33:46 +02:00
co-authored by Claude Sonnet 5
parent 00c3279fa6
commit 531eec2cd3
2 changed files with 15 additions and 0 deletions
+2
View File
@@ -2383,6 +2383,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed? if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
saveContacts(); saveContacts();
} }
savePrefs(); // flush any on-device setting change not yet persisted -- see UITask::shutdown()'s comment
board.reboot(); board.reboot();
} else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) { } else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) {
uint8_t reply[11]; uint8_t reply[11];
@@ -3191,6 +3192,7 @@ void MyMesh::checkCLIRescueCmd() {
} }
} else if (strcmp(cli_command, "reboot") == 0) { } else if (strcmp(cli_command, "reboot") == 0) {
savePrefs(); // flush any on-device setting change not yet persisted -- see UITask::shutdown()'s comment
board.reboot(); // doesn't return board.reboot(); // doesn't return
} else { } else {
Serial.println(" Error: unknown command"); Serial.println(" Error: unknown command");
@@ -2012,6 +2012,19 @@ bool UITask::savePrefsIfDirty(bool& dirty) {
hardware-agnostic pre-shutdown activity should be done here hardware-agnostic pre-shutdown activity should be done here
*/ */
void UITask::shutdown(bool restart){ void UITask::shutdown(bool restart){
// Every screen that edits NodePrefs (Settings, Bot, Trail, Locator, GPS
// sharing, etc.) only persists on its OWN "Cancel"/exit path (see each
// screen's own savePrefsIfDirty(_dirty) call) -- there was previously no
// flush here at all. A user who edits a setting and then triggers a
// reboot/power-off WITHOUT first backing out of that screen (e.g. the
// display auto-offs while still inside Settings, then the device is
// later hard-reset or its battery pulled; or a low-battery auto-shutdown
// fires mid-edit) silently lost that change on the next boot -- this was
// the actual mechanism behind reports of "settings don't survive a
// 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.saveRTCTime();
// Auto-save the live GPS trail before power-off when the user enabled it // Auto-save the live GPS trail before power-off when the user enabled it