mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-06 20:26:12 +00:00
feat(ui): Trail phase 4 — Save/Load to flash + GPX export over USB
Single-slot persistence as agreed. Action popup grows three entries
(conditional on what makes sense for the current state):
- "Save trail" — writes the current RAM ring to /trail in LittleFS.
- "Load trail" — only shown when /trail exists; replaces the live ring
with the snapshot (any active session ends first).
- "Export GPX" — dumps the trail as a minimal GPX 1.1 document over
Serial so the user can capture it from a USB host.
Plumbing:
- TrailStore gains writeTo / readFrom (template on the file handle, so
the platform-specific File flavour stays out of Trail.h) plus
exportGpx(Stream&) for the GPX serializer. Header carries magic
"TRAL", a version byte, point count, and the accumulated active time
so elapsed survives the round trip.
- DataStore::openWrite is exposed publicly so callers outside DataStore
don't have to duplicate the platform open-mode dance.
- MyMesh gains a public getDataStore() accessor.
After Load, _pending_seg_break is set so any subsequent Start opens a
new segment instead of joining the loaded last point.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
bb33a23543
commit
2671e3f764
@@ -23,7 +23,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_TOGGLE, ACT_RESET };
|
||||
enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT };
|
||||
PopupMenu _action_menu;
|
||||
uint8_t _act_map[8];
|
||||
uint8_t _act_count = 0;
|
||||
@@ -87,7 +87,10 @@ public:
|
||||
if (sel >= 0 && sel < _act_count) {
|
||||
ActionId act = (ActionId)_act_map[sel];
|
||||
if (act == ACT_TOGGLE) { handleToggle(); }
|
||||
else if (act == ACT_SAVE) { handleSave(); }
|
||||
else if (act == ACT_LOAD) { handleLoad(); }
|
||||
else if (act == ACT_RESET) { handleReset(); }
|
||||
else if (act == ACT_EXPORT) { handleExport(); }
|
||||
else /* MIN_DIST / UNITS */ { reopenAt(sel); return true; } // re-open keeping focus
|
||||
}
|
||||
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
|
||||
@@ -143,6 +146,33 @@ private:
|
||||
_store->clear();
|
||||
_task->showAlert("Trail reset", 800);
|
||||
}
|
||||
void handleSave() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) { _task->showAlert("FS unavailable", 800); return; }
|
||||
File f = ds->openWrite(TRAIL_FILE);
|
||||
if (!f) { _task->showAlert("Save failed", 800); return; }
|
||||
bool ok = _store->writeTo(f);
|
||||
f.close();
|
||||
_task->showAlert(ok ? "Trail saved" : "Save failed", 800);
|
||||
}
|
||||
void handleLoad() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) { _task->showAlert("FS unavailable", 800); return; }
|
||||
File f = ds->openRead(TRAIL_FILE);
|
||||
if (!f) { _task->showAlert("No saved trail", 800); return; }
|
||||
bool ok = _store->readFrom(f);
|
||||
f.close();
|
||||
_task->showAlert(ok ? "Trail loaded" : "Load failed", 800);
|
||||
}
|
||||
void handleExport() {
|
||||
if (!Serial) { _task->showAlert("Connect USB first", 1000); return; }
|
||||
size_t n = _store->exportGpx(Serial);
|
||||
char alert[24];
|
||||
snprintf(alert, sizeof(alert), "GPX %u B sent", (unsigned)n);
|
||||
_task->showAlert(alert, 1200);
|
||||
}
|
||||
|
||||
static constexpr const char* TRAIL_FILE = "/trail";
|
||||
|
||||
// Refresh the three settings/action labels in-place. Pointer identity is
|
||||
// preserved, so PopupMenu picks up the new text on the next render.
|
||||
@@ -164,10 +194,26 @@ private:
|
||||
_act_map[_act_count++] = ACT_UNITS; _action_menu.addItem(_act_units_label);
|
||||
_act_map[_act_count++] = ACT_TOGGLE; _action_menu.addItem(_act_toggle_label);
|
||||
if (!_store->empty()) {
|
||||
_act_map[_act_count++] = ACT_SAVE; _action_menu.addItem("Save trail");
|
||||
}
|
||||
if (savedTrailExists()) {
|
||||
_act_map[_act_count++] = ACT_LOAD; _action_menu.addItem("Load trail");
|
||||
}
|
||||
if (!_store->empty()) {
|
||||
_act_map[_act_count++] = ACT_EXPORT; _action_menu.addItem("Export GPX");
|
||||
_act_map[_act_count++] = ACT_RESET; _action_menu.addItem("Reset trail");
|
||||
}
|
||||
}
|
||||
|
||||
static bool savedTrailExists() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return false;
|
||||
File f = ds->openRead(TRAIL_FILE);
|
||||
bool ok = (bool)f;
|
||||
if (f) f.close();
|
||||
return ok;
|
||||
}
|
||||
|
||||
// After Enter on a settings row, the popup auto-closes per PopupMenu's
|
||||
// semantics. Re-open it with focus restored to that row so the user can
|
||||
// continue cycling.
|
||||
|
||||
Reference in New Issue
Block a user