mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(trail): include waypoints in the GPX export
GPX dumps previously contained only the <trk>. Emit saved waypoints as <wpt> elements (label → <name>, plus <time> when the RTC was set) before the track, per GPX 1.1 ordering, in both live and saved-file exports. The emitter is duck-typed over the waypoint store so Trail.h stays decoupled from Waypoint.h; labels are XML-escaped. They now import as pins in OsmAnd / Garmin / GPX Studio alongside the track. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<?xml` to `</gpx>` 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 `<wpt>` elements (with their label as `<name>`), 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.
|
||||
|
||||
@@ -226,17 +226,65 @@ public:
|
||||
// through the same formatting code.
|
||||
|
||||
template <typename S>
|
||||
static size_t gpxHeader(S& out, const char* name) {
|
||||
static size_t gpxHeader(S& out) {
|
||||
size_t n = 0;
|
||||
n += out.print(F("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"));
|
||||
n += out.print(F("<gpx version=\"1.1\" creator=\"MeshCore\" "
|
||||
"xmlns=\"http://www.topografix.com/GPX/1/1\">\n"));
|
||||
return n;
|
||||
}
|
||||
|
||||
template <typename S>
|
||||
static size_t gpxTrackOpen(S& out, const char* name) {
|
||||
size_t n = 0;
|
||||
n += out.print(F("<trk><name>"));
|
||||
n += out.print(name);
|
||||
n += out.print(F("</name>\n"));
|
||||
return n;
|
||||
}
|
||||
|
||||
// Emit saved waypoints as <wpt> elements. In GPX 1.1 these must precede the
|
||||
// <trk>. 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 <typename S, typename WP>
|
||||
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),
|
||||
"<wpt lat=\"%.6f\" lon=\"%.6f\"><name>%s</name>",
|
||||
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 <time> when the RTC was set
|
||||
time_t t = (time_t)w.ts;
|
||||
struct tm* gt = ::gmtime(&t);
|
||||
if (gt) {
|
||||
len = snprintf(buf, sizeof(buf),
|
||||
"<time>%04d-%02d-%02dT%02d:%02d:%02dZ</time>",
|
||||
gt->tm_year + 1900, gt->tm_mon + 1, gt->tm_mday,
|
||||
gt->tm_hour, gt->tm_min, gt->tm_sec);
|
||||
if (len > 0) { if ((size_t)len > sizeof(buf)) len = sizeof(buf);
|
||||
n += out.write((const uint8_t*)buf, (size_t)len); }
|
||||
}
|
||||
}
|
||||
n += out.print(F("</wpt>\n"));
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
template <typename S>
|
||||
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 <typename S>
|
||||
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 <typename S, typename WP>
|
||||
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 <typename F, typename S>
|
||||
static size_t exportGpxFromFile(F& file, S& out, const char* trk_name = "MeshCore Trail") {
|
||||
template <typename F, typename S, typename WP>
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user