From 9ee199e641fdeeca0256b51dfa987888aeb598de Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sun, 5 Jul 2026 22:21:42 +0200 Subject: [PATCH] feat(trail): auto-save GPS trail on shutdown (Trail settings toggle) Losing the whole route on a low-battery auto-shutdown was the worst solo-mode failure. New NodePrefs::trail_autosave_lowbatt toggle (Tools > Trail > Settings > Auto-save, default OFF): UITask::shutdown() writes the live trail to /trail when the toggle is on and _trail.count() > 0. The count guard stops an empty trail from wiping a previously saved one; it overwrites the same /trail file the manual Trail > Save uses. Schema bumped 0xC0DE001A -> 0xC0DE001B: append-only tail field with a load clamp, so pre-0x1B saves default to off. sizeof(NodePrefs) is unchanged (the byte fits existing padding after keyboard_type), so the tripwire assert stays 2496. Builds green: WioTrackerL1_companion_solo_dual (RAM 69.9%, Flash 62.8%). Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/DataStore.cpp | 7 +++++++ examples/companion_radio/NodePrefs.h | 8 +++++++- examples/companion_radio/ui-new/TrailScreen.h | 8 +++++++- examples/companion_radio/ui-new/UITask.cpp | 13 +++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 818523b4..e36b4a36 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -476,6 +476,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no if (_prefs.page_order[i] > NodePrefs::HPB_COUNT) _prefs.page_order[i] = 0; } + // → 0xC0DE001B: append trail_autosave_lowbatt at the tail. A pre-0x1B file has + // the old sentinel bytes / EOF here; rd() zero-inits when absent and the clamp + // below turns any stray value into 0 (off), so upgraders default to off. + rd(&_prefs.trail_autosave_lowbatt, sizeof(_prefs.trail_autosave_lowbatt)); + if (_prefs.trail_autosave_lowbatt > 1) _prefs.trail_autosave_lowbatt = 0; + // Schema sentinel: bumped on layout changes. Mismatch means an older file // (or a different schema); rd() already zero-inits any fields not present, // so we just log it — next savePrefs writes the current sentinel. @@ -661,6 +667,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ // appended here so the on-disk head stays the original 11 bytes. file.write((uint8_t *)&_prefs.page_order[NodePrefs::PAGE_ORDER_LEN_V1], NodePrefs::PAGE_ORDER_LEN - NodePrefs::PAGE_ORDER_LEN_V1); + file.write((uint8_t *)&_prefs.trail_autosave_lowbatt, sizeof(_prefs.trail_autosave_lowbatt)); // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is // the one we check: once the flash fills, writes return 0, so a good diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index e726cc4e..c358e238 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -289,6 +289,12 @@ struct NodePrefs { // persisted to file // (phone-keypad groups, cycled with repeated Enter presses — see KeyboardWidget.h). uint8_t keyboard_type; + // Auto-save the live GPS trail to /trail on shutdown (covers the low-battery + // auto-shutdown, which otherwise loses the whole route). Only writes when the + // trail has points and the toggle is on. 0 = off (default), 1 = on. + // [Tools › Trail › Settings › Auto-save] + uint8_t trail_autosave_lowbatt; + // Single source of truth for the live-share option tables (shared by the Map // UI labels and the auto-send engine in UITask). static const uint8_t LOC_SHARE_MOVE_COUNT = 4; @@ -351,7 +357,7 @@ struct NodePrefs { // persisted to file // adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so // older saves are detected on load and skipped (zero-init defaults kept). // High 24 bits identify the file format; low byte is the schema revision. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE001A; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE001B; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 3b967e41..2a06eff6 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -72,7 +72,7 @@ class TrailScreen : public UIScreen { // (Start/Stop tracking, Reset). PopupMenu stores label pointers verbatim, so // the labels live in member buffers that get refreshed in openActionMenu() // and after every LEFT/RIGHT cycle. - enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_GRID, ACT_AUTOPAUSE, ACT_MARK_AVG, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT, ACT_EXPORT_SAVED, ACT_MARK, ACT_WAYPOINTS, ACT_FILE, ACT_SETTINGS, + enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_GRID, ACT_AUTOPAUSE, ACT_MARK_AVG, ACT_AUTOSAVE, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT, ACT_EXPORT_SAVED, ACT_MARK, ACT_WAYPOINTS, ACT_FILE, ACT_SETTINGS, ACT_SHARE_NOW, ACT_TRACKBACK }; // The action popup is multi-level: a short main menu, plus "Trail file…" and // "Settings…" submenus. _menu_level tracks which is open so input is routed @@ -89,6 +89,7 @@ class TrailScreen : public UIScreen { char _act_grid_label[16]; char _act_autopause_label[24]; char _act_mark_avg_label[24]; + char _act_autosave_label[24]; char _act_toggle_label[20]; static const int SUMMARY_ITEM_COUNT = 5; @@ -178,6 +179,7 @@ public: case ACT_UNITS: case ACT_GRID: case ACT_MARK_AVG: + case ACT_AUTOSAVE: case ACT_AUTOPAUSE: cycleSetting(act, 1); reopenSettingsAt(sel); return true; case ACT_SHARE_NOW: shareMyLocationNow(); break; case ACT_TOGGLE: @@ -327,6 +329,8 @@ private: "Auto-pause: %s", NodePrefs::trailAutoPauseLabel(p ? p->trail_autopause_idx : 0)); snprintf(_act_mark_avg_label, sizeof(_act_mark_avg_label), "Mark avg: %s", NodePrefs::gpsAvgLabel(p ? p->gps_avg_idx : 0)); + snprintf(_act_autosave_label, sizeof(_act_autosave_label), + "Auto-save: %s", (p && p->trail_autosave_lowbatt) ? "ON" : "OFF"); snprintf(_act_toggle_label, sizeof(_act_toggle_label), "%s tracking", _store->isActive() ? "Stop" : "Start"); } @@ -394,6 +398,7 @@ private: pushAction(ACT_MIN_DIST, _act_min_dist_label); pushAction(ACT_AUTOPAUSE, _act_autopause_label); pushAction(ACT_MARK_AVG, _act_mark_avg_label); + pushAction(ACT_AUTOSAVE, _act_autosave_label); if (_view == V_SUMMARY) pushAction(ACT_UNITS, _act_units_label); if (_view == V_MAP) pushAction(ACT_GRID, _act_grid_label); } @@ -406,6 +411,7 @@ private: if (act == ACT_GRID) { _map_grid = !_map_grid; refreshActionLabels(); return true; } if (act == ACT_AUTOPAUSE && p){ cycleAutoPause(p, dir); _cfg_dirty = true; refreshActionLabels(); return true; } if (act == ACT_MARK_AVG && p){ cycleMarkAvg(p, dir); _cfg_dirty = true; refreshActionLabels(); return true; } + if (act == ACT_AUTOSAVE && p){ p->trail_autosave_lowbatt = !p->trail_autosave_lowbatt; _cfg_dirty = true; refreshActionLabels(); return true; } return false; } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index f9e5b84a..19bca57b 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1927,6 +1927,19 @@ bool UITask::savePrefsIfDirty(bool& dirty) { void UITask::shutdown(bool restart){ the_mesh.saveRTCTime(); + // Auto-save the live GPS trail before power-off when the user enabled it + // (Tools › Trail › Settings › Auto-save). This covers the low-battery + // auto-shutdown, which otherwise loses the whole route. Overwrites /trail + // (same file as the manual Trail › Save); guarded on count()>0 so an empty + // trail can't wipe a previously saved one. + if (_node_prefs && _node_prefs->trail_autosave_lowbatt && _trail.count() > 0) { + DataStore* ds = the_mesh.getDataStore(); + if (ds) { + File f = ds->openWrite("/trail"); + if (f) { _trail.writeTo(f); f.close(); } + } + } + #ifdef PIN_BUZZER /* note: we have a choice here - we can do a blocking buzzer.loop() with non-deterministic consequences