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"); } }