diff --git a/docs/solo_features/tools_screen/tools_screen.md b/docs/solo_features/tools_screen/tools_screen.md index 1b36f159..7e2d0b41 100644 --- a/docs/solo_features/tools_screen/tools_screen.md +++ b/docs/solo_features/tools_screen/tools_screen.md @@ -147,7 +147,7 @@ Then on the device: **Tools › Trail** → **Hold Enter** → **Export (live)** - **macOS/Linux** — `cat /dev/tty.usbmodem* > track.gpx` (stop with Ctrl-C after the dump finishes) - **Windows** — PuTTY (Serial, 115200) or Arduino IDE Serial Monitor with no line ending; copy the text from `` into a `.gpx` file -Either way, the resulting file imports into OsmAnd, Garmin BaseCamp, GPX Studio, Google Earth, etc. +Saved **waypoints are included** in the export as GPX `` elements (with their label as ``), alongside the track — so they show as pins in OsmAnd, Garmin BaseCamp, GPX Studio, Google Earth, etc. Either way, the resulting file imports into all of those. > [!NOTE] > If the companion app is connected via **BLE**, the export is safe — BLE and USB operate independently. If connected via **USB**, disconnect the app before exporting. diff --git a/examples/companion_radio/Trail.h b/examples/companion_radio/Trail.h index 6832d3df..356666ae 100644 --- a/examples/companion_radio/Trail.h +++ b/examples/companion_radio/Trail.h @@ -226,17 +226,65 @@ public: // through the same formatting code. template - static size_t gpxHeader(S& out, const char* name) { + static size_t gpxHeader(S& out) { size_t n = 0; n += out.print(F("\n")); n += out.print(F("\n")); + return n; + } + + template + static size_t gpxTrackOpen(S& out, const char* name) { + size_t n = 0; n += out.print(F("")); n += out.print(name); n += out.print(F("\n")); return n; } + // Emit saved waypoints as elements. In GPX 1.1 these must precede the + // . Duck-typed over any store exposing count()/at(i) whose entries have + // lat_1e6 / lon_1e6 / ts / label, so Trail.h stays decoupled from Waypoint.h. + template + static size_t gpxWaypoints(S& out, WP& store) { + size_t n = 0; + for (int i = 0; i < store.count(); i++) { + const auto& w = store.at(i); + // XML-escape the user label (&, <, > only). + char esc[64]; int e = 0; + for (const char* p = w.label; *p && e < (int)sizeof(esc) - 6; p++) { + if (*p == '&') { memcpy(esc + e, "&", 5); e += 5; } + else if (*p == '<') { memcpy(esc + e, "<", 4); e += 4; } + else if (*p == '>') { memcpy(esc + e, ">", 4); e += 4; } + else esc[e++] = *p; + } + esc[e] = '\0'; + char buf[160]; + int len = snprintf(buf, sizeof(buf), + "%s", + w.lat_1e6 / 1.0e6, w.lon_1e6 / 1.0e6, esc); + if (len > 0) { + if ((size_t)len > sizeof(buf)) len = sizeof(buf); + n += out.write((const uint8_t*)buf, (size_t)len); + } + if (w.ts > 1000000000UL) { // append \n")); + } + return n; + } + template static size_t gpxFooter(S& out, bool in_segment) { size_t n = 0; @@ -271,10 +319,12 @@ public: 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 = gpxHeader(out, trk_name); + // Dump the live RAM ring as GPX (with saved waypoints). Returns bytes written. + template + size_t exportGpx(S& out, WP& wpts, const char* trk_name = "MeshCore Trail") { + size_t total = gpxHeader(out); + total += gpxWaypoints(out, wpts); + total += gpxTrackOpen(out, trk_name); bool in_segment = false; for (int i = 0; i < _count; i++) { total += gpxPoint(out, at(i), i == 0, in_segment); @@ -285,8 +335,8 @@ public: // 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") { + template + static size_t exportGpxFromFile(F& file, S& out, WP& wpts, 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; @@ -299,7 +349,9 @@ public: file.read((uint8_t*)&accum, sizeof(accum)); if (ver != SAVE_VERSION || cnt > CAPACITY) return 0; - size_t total = gpxHeader(out, trk_name); + size_t total = gpxHeader(out); + total += gpxWaypoints(out, wpts); + total += gpxTrackOpen(out, trk_name); bool in_segment = false; for (uint16_t i = 0; i < cnt; i++) { TrailPoint p; diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 34109ec2..f6bee90d 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -306,7 +306,7 @@ private: } void handleExport() { if (!Serial) { _task->showAlert("Connect USB first", 1200); return; } - size_t n = _store->exportGpx(Serial); + size_t n = _store->exportGpx(Serial, _task->waypoints()); showExportAlert(n); } @@ -316,7 +316,7 @@ private: 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); + size_t n = TrailStore::exportGpxFromFile(f, Serial, _task->waypoints()); f.close(); if (n == 0) { _task->showAlert("Bad saved file", 1000); return; } showExportAlert(n);