diff --git a/examples/companion_radio/Trail.h b/examples/companion_radio/Trail.h
index 8dce4f98..cbf6a317 100644
--- a/examples/companion_radio/Trail.h
+++ b/examples/companion_radio/Trail.h
@@ -215,40 +215,89 @@ public:
return true;
}
- // Dump the trail as a minimal GPX / document over the given
- // Stream. Segments split on SEG_START flags. Returns bytes written.
- // Caller must ensure the stream is ready (e.g. USB serial connected).
+ // GPX writers — shared helpers so we can dump from RAM and from flash
+ // through the same formatting code.
+
+ template
+ static size_t gpxHeader(S& out, const char* name) {
+ size_t n = 0;
+ n += out.print(F("\n"));
+ n += out.print(F("\n"));
+ n += out.print(F(""));
+ n += out.print(name);
+ n += out.print(F("\n"));
+ return n;
+ }
+
+ template
+ static size_t gpxFooter(S& out, bool in_segment) {
+ size_t n = 0;
+ if (in_segment) n += out.print(F("\n"));
+ n += out.print(F("\n"));
+ return n;
+ }
+
+ // Emit a single ; opens a on a segment boundary. Updates
+ // `in_segment` to track open/close pairing.
+ template
+ static size_t gpxPoint(S& out, const TrailPoint& p, bool first, bool& in_segment) {
+ size_t n = 0;
+ bool seg_start = first || (p.flags & TRAIL_FLAG_SEG_START);
+ if (seg_start) {
+ if (in_segment) n += out.print(F("\n"));
+ n += out.print(F("\n"));
+ in_segment = true;
+ }
+ char buf[120];
+ time_t t = (time_t)p.ts;
+ struct tm* gt = gmtime(&t);
+ int len = snprintf(buf, sizeof(buf),
+ "\n",
+ p.lat_1e6 / 1.0e6, p.lon_1e6 / 1.0e6,
+ gt->tm_year + 1900, gt->tm_mon + 1, gt->tm_mday,
+ gt->tm_hour, gt->tm_min, gt->tm_sec);
+ n += out.write((const uint8_t*)buf, len);
+ return n;
+ }
+
+ // Dump the live RAM ring as GPX. Returns bytes written.
template
size_t exportGpx(S& out, const char* trk_name = "MeshCore Trail") {
- size_t total = 0;
- total += out.print(F("\n"));
- total += out.print(F("\n"));
- total += out.print(F(""));
- total += out.print(trk_name);
- total += out.print(F("\n"));
+ size_t total = gpxHeader(out, trk_name);
bool in_segment = false;
for (int i = 0; i < _count; i++) {
- const auto& p = at(i);
- bool seg_start = (i == 0) || (p.flags & TRAIL_FLAG_SEG_START);
- if (seg_start) {
- if (in_segment) total += out.print(F("\n"));
- total += out.print(F("\n"));
- in_segment = true;
- }
- char buf[120];
- // RFC 3339 timestamp from UNIX seconds.
- time_t t = (time_t)p.ts;
- struct tm* gt = gmtime(&t);
- int n = snprintf(buf, sizeof(buf),
- "\n",
- p.lat_1e6 / 1.0e6, p.lon_1e6 / 1.0e6,
- gt->tm_year + 1900, gt->tm_mon + 1, gt->tm_mday,
- gt->tm_hour, gt->tm_min, gt->tm_sec);
- total += out.write((const uint8_t*)buf, n);
+ total += gpxPoint(out, at(i), i == 0, in_segment);
}
- if (in_segment) total += out.print(F("\n"));
- total += out.print(F("\n"));
+ total += gpxFooter(out, in_segment);
+ return total;
+ }
+
+ // Stream a saved trail straight from the open file as GPX without
+ // touching the live RAM ring. Returns 0 on format mismatch.
+ template
+ static size_t exportGpxFromFile(F& file, S& out, const char* trk_name = "MeshCore Trail") {
+ uint32_t magic = 0;
+ if (file.read((uint8_t*)&magic, sizeof(magic)) != (int)sizeof(magic)) return 0;
+ if (magic != SAVE_MAGIC) return 0;
+ uint8_t ver = 0, res = 0;
+ uint16_t cnt = 0;
+ uint32_t accum = 0;
+ file.read(&ver, 1);
+ file.read(&res, 1);
+ file.read((uint8_t*)&cnt, sizeof(cnt));
+ file.read((uint8_t*)&accum, sizeof(accum));
+ if (ver != SAVE_VERSION || cnt > CAPACITY) return 0;
+
+ size_t total = gpxHeader(out, trk_name);
+ bool in_segment = false;
+ for (uint16_t i = 0; i < cnt; i++) {
+ TrailPoint p;
+ int n = file.read((uint8_t*)&p, sizeof(TrailPoint));
+ if (n != (int)sizeof(TrailPoint)) break;
+ total += gpxPoint(out, p, i == 0, in_segment);
+ }
+ total += gpxFooter(out, in_segment);
return total;
}
diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h
index 87a34f5e..fbfb888d 100644
--- a/examples/companion_radio/ui-new/TrailScreen.h
+++ b/examples/companion_radio/ui-new/TrailScreen.h
@@ -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_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT };
+ enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT, ACT_EXPORT_SAVED };
PopupMenu _action_menu;
uint8_t _act_map[8];
uint8_t _act_count = 0;
@@ -90,8 +90,9 @@ public:
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
+ else if (act == ACT_EXPORT) { handleExport(); }
+ else if (act == ACT_EXPORT_SAVED) { handleExportSaved(); }
+ else /* MIN_DIST / UNITS */ { reopenAt(sel); return true; } // re-open keeping focus
}
if (_cfg_dirty) { the_mesh.savePrefs(); _cfg_dirty = false; }
} else if (res == PopupMenu::CANCELLED) {
@@ -166,18 +167,30 @@ private:
}
void handleExport() {
if (!Serial) { _task->showAlert("Connect USB first", 1200); return; }
+ size_t n = _store->exportGpx(Serial);
+ showExportAlert(n);
+ }
+
+ void handleExportSaved() {
+ if (!Serial) { _task->showAlert("Connect USB first", 1200); return; }
+ 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; }
+ size_t n = TrailStore::exportGpxFromFile(f, Serial);
+ f.close();
+ if (n == 0) { _task->showAlert("Bad saved file", 1000); return; }
+ showExportAlert(n);
+ }
+
+ void showExportAlert(size_t n) {
// The companion app multiplexes BLE + USB on the same frame protocol.
// When BLE is connected, USB-receive is ignored and the dump can't
- // collide. Without a BLE link the app might be on USB itself, in
- // which case the raw GPX would land mid-frame and confuse it — warn
- // the user but proceed (they may have no app attached at all).
- size_t n = _store->exportGpx(Serial);
+ // collide. Without a BLE link the app might be on USB itself, so warn
+ // the user — they may need to reconnect their app afterwards.
char alert[28];
- if (_task->hasConnection()) {
- snprintf(alert, sizeof(alert), "GPX %u B (USB)", (unsigned)n);
- } else {
- snprintf(alert, sizeof(alert), "GPX %u B - disc. app", (unsigned)n);
- }
+ if (_task->hasConnection()) snprintf(alert, sizeof(alert), "GPX %u B (USB)", (unsigned)n);
+ else snprintf(alert, sizeof(alert), "GPX %u B - disc. app", (unsigned)n);
_task->showAlert(alert, 1500);
}
@@ -205,12 +218,18 @@ private:
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");
+ bool saved = savedTrailExists();
+ if (saved) {
+ _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");
+ _act_map[_act_count++] = ACT_EXPORT; _action_menu.addItem("Export GPX");
+ }
+ if (saved) {
+ _act_map[_act_count++] = ACT_EXPORT_SAVED; _action_menu.addItem("Export saved");
+ }
+ if (!_store->empty()) {
+ _act_map[_act_count++] = ACT_RESET; _action_menu.addItem("Reset trail");
}
}