refactor: dedup persistence header, harden action menu, tidy small items

- Persist.h: shared writeHeader()/readHeader() for the magic+version+count
  on-disk header; Waypoint and Trail snapshots now share one implementation
  (byte layout unchanged; each keeps its own count clamp/reject + extra fields).
- TrailScreen: pushAction() guards _act_map / PopupMenu capacity, so adding a
  new action can't silently overrun the fixed array.
- PopupMenu: add count()/setSelected() accessors; replace the remaining direct
  _sel/_count field pokes in NearbyScreen, TrailScreen and QuickMsgScreen.
- Trail summary: unit-suffixed time ("1h 05m" / "5m 03s") so hours can't be
  misread as minutes.
- Trail::haversineMeters delegates to geo::haversineKm — single Haversine
  implementation (also trims a little flash).

Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo builds compile clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-14 13:37:03 +02:00
parent 151892e1ce
commit b8a7b5dcc0
7 changed files with 85 additions and 83 deletions

View File

@@ -0,0 +1,40 @@
#pragma once
// Shared on-disk header for the small fixed-table snapshots (waypoints, trail).
// Layout: magic(4) + version(1) + reserved(1) + count(2), little-endian, then
// table-specific payload (records, and for the trail an extra accumulated-ms
// field). Keeps the read/write boilerplate — and its short-read checks — in one
// place so both stores can't drift apart.
#include <Arduino.h>
namespace persist {
// Write magic(4) + version(1) + reserved(1) + count(2). Returns true on a full write.
template <typename F>
inline bool writeHeader(F& file, uint32_t magic, uint8_t version, uint16_t count) {
uint8_t res = 0;
return file.write((uint8_t*)&magic, sizeof(magic)) == sizeof(magic)
&& file.write(&version, 1) == 1
&& file.write(&res, 1) == 1
&& file.write((uint8_t*)&count, sizeof(count)) == sizeof(count);
}
// Read + validate the header. Returns true and sets count_out only when magic
// and version match. The caller decides how to treat count_out (clamp or
// reject against its own capacity) and reads any table-specific fields that
// follow the count.
template <typename F>
inline bool readHeader(F& file, uint32_t expect_magic, uint8_t expect_version, uint16_t& count_out) {
uint32_t magic = 0;
if (file.read((uint8_t*)&magic, sizeof(magic)) != (int)sizeof(magic)) return false;
if (magic != expect_magic) return false;
uint8_t ver = 0, res = 0;
uint16_t cnt = 0;
if (file.read(&ver, 1) != 1) return false;
if (file.read(&res, 1) != 1) return false;
if (file.read((uint8_t*)&cnt, sizeof(cnt)) != (int)sizeof(cnt)) return false;
if (ver != expect_version) return false;
count_out = cnt;
return true;
}
} // namespace persist

View File

@@ -4,6 +4,8 @@
#include <math.h>
#include <stdint.h>
#include <time.h>
#include "Persist.h"
#include "GeoUtils.h"
// RAM-only GPS trail ring buffer.
// Storage cost: CAPACITY(512) × sizeof(TrailPoint)(16 B, padded) = 8 KB,
@@ -177,16 +179,9 @@ public:
// cleanly. The file is left open for the caller to close.
template <typename F>
bool writeTo(F& file) {
uint32_t magic = SAVE_MAGIC;
uint8_t ver = SAVE_VERSION;
uint8_t res = 0;
uint16_t cnt = (uint16_t)_count;
if (!persist::writeHeader(file, SAVE_MAGIC, SAVE_VERSION, (uint16_t)_count)) return false;
uint32_t accum = currentAccumulatedMs();
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;
if (file.write((uint8_t*)&accum, sizeof(accum)) != sizeof(accum)) return false;
if (file.write((uint8_t*)&accum, sizeof(accum)) != sizeof(accum)) return false;
for (int i = 0; i < _count; i++) {
if (file.write((uint8_t*)&at(i), sizeof(TrailPoint)) != sizeof(TrailPoint)) return false;
}
@@ -195,17 +190,11 @@ public:
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;
uint32_t accum = 0;
if (file.read(&ver, 1) != 1) return false;
if (file.read(&res, 1) != 1) return false;
if (file.read((uint8_t*)&cnt, sizeof(cnt)) != (int)sizeof(cnt)) return false;
if (!persist::readHeader(file, SAVE_MAGIC, SAVE_VERSION, cnt)) return false;
if (file.read((uint8_t*)&accum, sizeof(accum)) != (int)sizeof(accum)) return false;
if (ver != SAVE_VERSION || cnt > CAPACITY) return false;
if (cnt > CAPACITY) return false;
if (_active) {
_active = false;
_session_start_ms = 0;
@@ -338,17 +327,11 @@ public:
// touching the live RAM ring. Returns 0 on format mismatch.
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;
uint8_t ver = 0, res = 0;
uint16_t cnt = 0;
uint32_t accum = 0;
if (file.read(&ver, 1) != 1) return 0;
if (file.read(&res, 1) != 1) return 0;
if (file.read((uint8_t*)&cnt, sizeof(cnt)) != (int)sizeof(cnt)) return 0;
if (!persist::readHeader(file, SAVE_MAGIC, SAVE_VERSION, cnt)) return 0;
if (file.read((uint8_t*)&accum, sizeof(accum)) != (int)sizeof(accum)) return 0;
if (ver != SAVE_VERSION || cnt > CAPACITY) return 0;
if (cnt > CAPACITY) return 0;
size_t total = gpxHeader(out);
total += gpxWaypoints(out, wpts);
@@ -370,19 +353,10 @@ public:
return ms;
}
// Approximate great-circle distance in metres (Haversine).
// Approximate great-circle distance in metres. Delegates to the shared
// geo:: Haversine (km) so there is a single implementation.
static float haversineMeters(int32_t la1, int32_t lo1, int32_t la2, int32_t lo2) {
const float R = 6371000.0f;
const float D2R = (float)M_PI / 180.0f;
float lat1 = (la1 / 1.0e6f) * D2R;
float lat2 = (la2 / 1.0e6f) * D2R;
float dlat = ((la2 - la1) / 1.0e6f) * D2R;
float dlon = ((lo2 - lo1) / 1.0e6f) * D2R;
float sdl = sinf(dlat * 0.5f);
float sdo = sinf(dlon * 0.5f);
float a = sdl * sdl + cosf(lat1) * cosf(lat2) * sdo * sdo;
float c = 2.0f * atan2f(sqrtf(a), sqrtf(1.0f - a));
return R * c;
return geo::haversineKm(la1, lo1, la2, lo2) * 1000.0f;
}
private:

View File

@@ -6,6 +6,7 @@
#include <Arduino.h>
#include <string.h>
#include "Persist.h"
static const uint8_t WAYPOINT_LABEL_LEN = 12; // incl. NUL → 11 visible chars
@@ -54,13 +55,7 @@ public:
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;
if (!persist::writeHeader(file, SAVE_MAGIC, SAVE_VERSION, (uint16_t)_count)) return false;
for (int i = 0; i < _count; i++) {
if (file.write((uint8_t*)&_wp[i], sizeof(Waypoint)) != sizeof(Waypoint)) return false;
}
@@ -69,15 +64,8 @@ public:
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;
if (file.read(&ver, 1) != 1) return false;
if (file.read(&res, 1) != 1) return false;
if (file.read((uint8_t*)&cnt, sizeof(cnt)) != (int)sizeof(cnt)) return false;
if (ver != SAVE_VERSION) return false;
if (!persist::readHeader(file, SAVE_MAGIC, SAVE_VERSION, cnt)) return false;
if (cnt > CAPACITY) cnt = CAPACITY;
_count = 0;
for (uint16_t i = 0; i < cnt; i++) {

View File

@@ -196,13 +196,13 @@ class NearbyScreen : public UIScreen {
}
void rebuildPingMenu() {
int keep = _ping_menu._sel; // preserve selection across a rebuild
int keep = _ping_menu.selectedIndex(); // preserve selection across a rebuild
_ping_menu.begin("Ping", 4);
_ping_menu.addItem("Send");
if (_ping_time_str[0]) _ping_menu.addItem(_ping_time_str);
if (_ping_snr_out_str[0]) _ping_menu.addItem(_ping_snr_out_str);
if (_ping_snr_back_str[0]) _ping_menu.addItem(_ping_snr_back_str);
if (keep > 0 && keep < _ping_menu._count) _ping_menu._sel = keep;
_ping_menu.setSelected(keep);
}
void openPingMenu() { rebuildPingMenu(); }
@@ -261,7 +261,7 @@ class NearbyScreen : public UIScreen {
}
// Keep the menu in sync with the populated result lines (grows as the
// reply arrives; never shows blank rows).
if (pingRowCount() != _ping_menu._count) rebuildPingMenu();
if (pingRowCount() != _ping_menu.count()) rebuildPingMenu();
}
bool handlePingMenuInput(char c, const uint8_t* pub_key, bool allow_enter_to_open = false) {

View File

@@ -94,4 +94,6 @@ struct PopupMenu {
}
int selectedIndex() const { return _sel; }
int count() const { return _count; }
void setSelected(int i) { if (i >= 0 && i < _count) _sel = i; }
};

View File

@@ -174,7 +174,8 @@ class QuickMsgScreen : public UIScreen {
// Dispatch the selected fullscreen-options row. `channel` picks which
// fullscreen view to close when starting a reply.
void dispatchFsAction(bool channel) {
FsAct a = (FsAct)_fs_act[(_ctx_menu._sel >= 0 && _ctx_menu._sel < _fs_act_n) ? _ctx_menu._sel : 0];
int csel = _ctx_menu.selectedIndex();
FsAct a = (FsAct)_fs_act[(csel >= 0 && csel < _fs_act_n) ? csel : 0];
_ctx_menu.active = false;
if (a == FS_REPLY) {
(channel ? _fs : _dm_fs).active = false;

View File

@@ -352,38 +352,34 @@ private:
"%s tracking", _store->isActive() ? "Stop" : "Start");
}
// Append an action row, guarding both the id map and the popup capacity so
// adding a new action can never silently overrun _act_map / PopupMenu.
void pushAction(ActionId id, const char* label) {
if (_act_count >= (int)sizeof(_act_map)) return;
_act_map[_act_count++] = (uint8_t)id;
_action_menu.addItem(label);
}
void openActionMenu() {
refreshActionLabels();
_act_count = 0;
_action_menu.begin("Trail", 4);
_act_map[_act_count++] = ACT_MIN_DIST; _action_menu.addItem(_act_min_dist_label);
_act_map[_act_count++] = ACT_UNITS; _action_menu.addItem(_act_units_label);
_act_map[_act_count++] = ACT_GRID; _action_menu.addItem(_act_grid_label);
_act_map[_act_count++] = ACT_TOGGLE; _action_menu.addItem(_act_toggle_label);
pushAction(ACT_MIN_DIST, _act_min_dist_label);
pushAction(ACT_UNITS, _act_units_label);
pushAction(ACT_GRID, _act_grid_label);
pushAction(ACT_TOGGLE, _act_toggle_label);
// Waypoints: mark the current spot, and browse/navigate saved ones.
_act_map[_act_count++] = ACT_MARK; _action_menu.addItem("Mark here");
pushAction(ACT_MARK, "Mark here");
// "Waypoints" opens the nav list — always available; it hosts the list,
// backtrack (Trail-start row) and the "+ Add by coords" entry.
_act_map[_act_count++] = ACT_WAYPOINTS; _action_menu.addItem("Waypoints");
if (_task->waypoints().count() > 0) {
_act_map[_act_count++] = ACT_WP_CLEAR; _action_menu.addItem("Clear waypoints");
}
if (!_store->empty()) {
_act_map[_act_count++] = ACT_SAVE; _action_menu.addItem("Save trail");
}
pushAction(ACT_WAYPOINTS, "Waypoints");
if (_task->waypoints().count() > 0) pushAction(ACT_WP_CLEAR, "Clear waypoints");
if (!_store->empty()) pushAction(ACT_SAVE, "Save 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 (live)");
}
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");
}
if (saved) pushAction(ACT_LOAD, "Load trail");
if (!_store->empty()) pushAction(ACT_EXPORT, "Export (live)");
if (saved) pushAction(ACT_EXPORT_SAVED, "Export (saved)");
if (!_store->empty()) pushAction(ACT_RESET, "Reset trail");
}
static bool savedTrailExists() {
@@ -584,7 +580,7 @@ private:
// continue cycling.
void reopenAt(int sel) {
openActionMenu();
if (sel >= 0 && sel < _act_count) _action_menu._sel = sel;
_action_menu.setSelected(sel);
}
void cycleMinDelta(NodePrefs* p, int dir) {
@@ -644,9 +640,10 @@ private:
}
case 3: {
uint32_t es = _store->elapsedSeconds();
if (es < 3600) snprintf(buf, n, "Time: %lu:%02lu",
// Unit-suffixed so "1h 05m" can't be misread as "1m 05s".
if (es < 3600) snprintf(buf, n, "Time: %lum %02lus",
(unsigned long)(es / 60), (unsigned long)(es % 60));
else snprintf(buf, n, "Time: %lu:%02lu",
else snprintf(buf, n, "Time: %luh %02lum",
(unsigned long)(es / 3600), (unsigned long)((es % 3600) / 60));
break;
}