diff --git a/examples/companion_radio/Persist.h b/examples/companion_radio/Persist.h new file mode 100644 index 00000000..aba508c2 --- /dev/null +++ b/examples/companion_radio/Persist.h @@ -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 + +namespace persist { + +// Write magic(4) + version(1) + reserved(1) + count(2). Returns true on a full write. +template +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 +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 diff --git a/examples/companion_radio/Trail.h b/examples/companion_radio/Trail.h index 9646b9f4..5dc71117 100644 --- a/examples/companion_radio/Trail.h +++ b/examples/companion_radio/Trail.h @@ -4,6 +4,8 @@ #include #include #include +#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 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 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 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: diff --git a/examples/companion_radio/Waypoint.h b/examples/companion_radio/Waypoint.h index ba57d1cb..5a7259b4 100644 --- a/examples/companion_radio/Waypoint.h +++ b/examples/companion_radio/Waypoint.h @@ -6,6 +6,7 @@ #include #include +#include "Persist.h" static const uint8_t WAYPOINT_LABEL_LEN = 12; // incl. NUL → 11 visible chars @@ -54,13 +55,7 @@ public: 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; + 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 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++) { diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 24cdeaee..6f96548f 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -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) { diff --git a/examples/companion_radio/ui-new/PopupMenu.h b/examples/companion_radio/ui-new/PopupMenu.h index 59cdfd57..f339d665 100644 --- a/examples/companion_radio/ui-new/PopupMenu.h +++ b/examples/companion_radio/ui-new/PopupMenu.h @@ -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; } }; diff --git a/examples/companion_radio/ui-new/QuickMsgScreen.h b/examples/companion_radio/ui-new/QuickMsgScreen.h index 3bef5d50..314246b8 100644 --- a/examples/companion_radio/ui-new/QuickMsgScreen.h +++ b/examples/companion_radio/ui-new/QuickMsgScreen.h @@ -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; diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 631d8473..95b25e21 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -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; }