feat(nav): WaypointStore — persistent /waypoints table

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 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-03 08:23:11 +02:00
parent 11c0b7c4a5
commit b1dfde5fcb
3 changed files with 126 additions and 0 deletions

View File

@@ -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 <Arduino.h>
#include <string.h>
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 <typename F>
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 <typename F>
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';
}
}
};

View File

@@ -1161,6 +1161,15 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
_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;
_ping_tag = 0;
@@ -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()) {

View File

@@ -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;