From b1dfde5fcb7c282ce6f3a96f65623f25a931f2b8 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 3 Jun 2026 08:23:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(nav):=20WaypointStore=20=E2=80=94=20persis?= =?UTF-8?q?tent=20/waypoints=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3. Fixed 16-entry table of (lat, lon, ts, label[12]) persisted to /waypoints with a magic+version header (mirrors TrailStore's format). Unlike the RAM-only trail, waypoints are loaded in UITask::begin() and rewritten on change via UITask::saveWaypoints(), so they survive reboots. add/remove/rename/clear operate on the in-RAM table; the screen layer (phase 5) drives persistence after each edit. Co-Authored-By: Claude Opus 4.8 --- examples/companion_radio/Waypoint.h | 104 +++++++++++++++++++++ examples/companion_radio/ui-new/UITask.cpp | 18 ++++ examples/companion_radio/ui-new/UITask.h | 4 + 3 files changed, 126 insertions(+) create mode 100644 examples/companion_radio/Waypoint.h diff --git a/examples/companion_radio/Waypoint.h b/examples/companion_radio/Waypoint.h new file mode 100644 index 00000000..23c58cbd --- /dev/null +++ b/examples/companion_radio/Waypoint.h @@ -0,0 +1,104 @@ +#pragma once +// Saved GPS waypoints — small fixed table persisted to /waypoints. Unlike the +// trail (a RAM ring), waypoints are long-lived: loaded at boot, rewritten on +// every add / delete / rename. Used by the navigation feature to mark a spot +// (car, camp, water…) and later get bearing + distance back to it. + +#include +#include + +static const uint8_t WAYPOINT_LABEL_LEN = 12; // incl. NUL → 11 visible chars + +struct Waypoint { + int32_t lat_1e6, lon_1e6; + uint32_t ts; // RTC time when marked + char label[WAYPOINT_LABEL_LEN]; +}; + +class WaypointStore { +public: + static const int CAPACITY = 16; + + int count() const { return _count; } + bool full() const { return _count >= CAPACITY; } + const Waypoint& at(int i) const { return _wp[i]; } + + // Add a waypoint; label may be empty (caller can auto-name). Returns false + // if the table is full. Trims/copies the label safely. + bool add(int32_t lat, int32_t lon, uint32_t ts, const char* label) { + if (_count >= CAPACITY) return false; + Waypoint& w = _wp[_count++]; + w.lat_1e6 = lat; w.lon_1e6 = lon; w.ts = ts; + setLabel(w, label); + return true; + } + + void remove(int i) { + if (i < 0 || i >= _count) return; + for (int j = i; j < _count - 1; j++) _wp[j] = _wp[j + 1]; + _count--; + } + + void rename(int i, const char* label) { + if (i < 0 || i >= _count) return; + setLabel(_wp[i], label); + } + + void clear() { _count = 0; } + + // ── persistence ────────────────────────────────────────────────────────── + // Layout: 4-byte magic "WYPT", uint8 version, uint8 reserved, uint16 count, + // then `count` raw Waypoint records. + static const uint32_t SAVE_MAGIC = 0x54505957; // "WYPT" little-endian + static const uint8_t SAVE_VERSION = 1; + + template + bool writeTo(F& file) { + uint32_t magic = SAVE_MAGIC; + uint8_t ver = SAVE_VERSION, res = 0; + uint16_t cnt = (uint16_t)_count; + if (file.write((uint8_t*)&magic, sizeof(magic)) != sizeof(magic)) return false; + if (file.write(&ver, 1) != 1) return false; + if (file.write(&res, 1) != 1) return false; + if (file.write((uint8_t*)&cnt, sizeof(cnt)) != sizeof(cnt)) return false; + for (int i = 0; i < _count; i++) { + if (file.write((uint8_t*)&_wp[i], sizeof(Waypoint)) != sizeof(Waypoint)) return false; + } + return true; + } + + template + bool readFrom(F& file) { + uint32_t magic = 0; + if (file.read((uint8_t*)&magic, sizeof(magic)) != (int)sizeof(magic)) return false; + if (magic != SAVE_MAGIC) return false; + uint8_t ver = 0, res = 0; + uint16_t cnt = 0; + file.read(&ver, 1); + file.read(&res, 1); + file.read((uint8_t*)&cnt, sizeof(cnt)); + if (ver != SAVE_VERSION) return false; + if (cnt > CAPACITY) cnt = CAPACITY; + _count = 0; + for (uint16_t i = 0; i < cnt; i++) { + Waypoint w; + if (file.read((uint8_t*)&w, sizeof(Waypoint)) != (int)sizeof(Waypoint)) break; + w.label[WAYPOINT_LABEL_LEN - 1] = '\0'; // defensive + _wp[_count++] = w; + } + return true; + } + +private: + Waypoint _wp[CAPACITY]; + int _count = 0; + + static void setLabel(Waypoint& w, const char* label) { + if (label && label[0]) { + strncpy(w.label, label, WAYPOINT_LABEL_LEN - 1); + w.label[WAYPOINT_LABEL_LEN - 1] = '\0'; + } else { + w.label[0] = '\0'; + } + } +}; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 199b7ac8..c5627f46 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1160,6 +1160,15 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no ui_started_at = millis(); _alert_expiry = 0; _batt_mv = AbstractUITask::getBattMilliVolts(); // seed EMA with first reading + + // Load persisted waypoints (table survives reboots, unlike the RAM trail). + { + DataStore* ds = the_mesh.getDataStore(); + if (ds) { + File f = ds->openRead("/waypoints"); + if (f) { _waypoints.readFrom(f); f.close(); } + } + } // Initialize ping state _ping_active = false; @@ -1962,6 +1971,15 @@ bool UITask::currentCourse(int& deg_out) const { return true; } +void UITask::saveWaypoints() { + DataStore* ds = the_mesh.getDataStore(); + if (!ds) return; + File f = ds->openWrite("/waypoints"); + if (!f) return; + _waypoints.writeTo(f); + f.close(); +} + char UITask::checkDisplayOn(char c) { if (_display != NULL) { if (!_display->isOn()) { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 5cb8d955..b97178e8 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -23,6 +23,7 @@ #include "../AbstractUITask.h" #include "../NodePrefs.h" #include "../Trail.h" +#include "../Waypoint.h" class UITask : public AbstractUITask { DisplayDriver* _display; @@ -78,6 +79,7 @@ class UITask : public AbstractUITask { UIScreen* curr; CayenneLPP _dash_lpp; TrailStore _trail; + WaypointStore _waypoints; uint32_t _next_trail_sample_ms = 0; // Course-over-ground ring — a heading source independent of trail recording. @@ -143,6 +145,8 @@ public: void gotoAutoAdvertScreen(); void gotoTrailScreen(); TrailStore& trail() { return _trail; } + WaypointStore& waypoints() { return _waypoints; } + void saveWaypoints(); // persist the table to /waypoints // Current course over ground in degrees (0..359), or false if not enough // recent movement to derive a stable heading. Independent of trail logging. bool currentCourse(int& deg_out) const;