mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(companion): GPS averaging for waypoint marking (Mark avg)
Add a "Mark avg" trail setting (Off/5/10/30 s). When set, Tools › Trail › Mark here samples the GPS fix once a second for the chosen window and stores the mean position instead of one instantaneous fix — a steadier, more accurate mark for a precise spot. A progress screen shows time left + sample count; Cancel aborts; the window then opens the label keyboard as usual. Off (default) keeps marking instant. Sampling runs in WaypointsView::poll() (forwarded from TrailScreen::poll()) so it ticks on the main loop, independent of the slow e-ink render cadence; int64 accumulators avoid overflow over 30 samples. Persisted as NodePrefs::gps_avg_idx (schema 0xC0DE0016): struct field + option table, DataStore rd/write/clamp in lockstep. sizeof unchanged (the uint8_t fits existing tail padding) so the 2488 tripwire still holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,7 @@ Cycle views with **LEFT / RIGHT**:
|
||||
| ---------- | --------- | ------------------------------------------------------- |
|
||||
| Min dist | always | Sample gate, 4 levels — metric: 5/10/25/100 m, imperial: 15/30/75/300 ft |
|
||||
| Auto-pause | always | Off / 1 / 2 / 5 min — auto-freeze the trail after a stop, resume on movement (see below) |
|
||||
| Mark avg | always | Off / 5 / 10 / 30 s — GPS averaging for **Mark here** (see Waypoints below) |
|
||||
| Readout | Summary view | Summary shows Speed or Pace (in the global unit system) |
|
||||
| Grid | Map view | Toggle scale grid on the map |
|
||||
|
||||
@@ -140,6 +141,8 @@ A waypoint is a saved spot — your car, camp, a water source — that you can n
|
||||
|
||||
**Dropping a waypoint** — **Hold Enter → Mark here**. This captures the current GPS fix and opens the on-screen keyboard for a short label (up to 11 characters — e.g. `CAR`, `CAMP`, `H2O`). Leaving it blank auto-names it `WP1`, `WP2`, … Marking works whether or not the trail is being recorded; it needs a GPS fix (otherwise it reports *No GPS fix*).
|
||||
|
||||
**GPS averaging** — with **Settings → Mark avg** set (5 / 10 / 30 s), *Mark here* doesn't snapshot a single fix; it samples the GPS once a second for that window and stores the **mean** position, for a steadier mark than one instantaneous reading (handy for a precise spot — a cache, a car, a trailhead). A short screen shows the time left and the sample count while it runs; **Cancel** aborts. When the window closes it opens the label keyboard as usual. With **Mark avg = Off** (the default) marking is instant.
|
||||
|
||||
**Adding by coordinates** — open **Hold Enter → Waypoints** and select the **+ Add by coords** row (always the last entry in the list). This creates a waypoint without being there — no GPS fix required (handy for a meeting point or a spot read off a map). It opens a small form with three editable rows plus **Save**:
|
||||
|
||||
| OLED | E-Ink |
|
||||
|
||||
@@ -418,6 +418,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
rd(&_prefs.locator_target_kind, sizeof(_prefs.locator_target_kind));
|
||||
rd(_prefs.locator_key, sizeof(_prefs.locator_key));
|
||||
if (_prefs.locator_target_kind > 1) _prefs.locator_target_kind = 0;
|
||||
// → 0xC0DE0016: GPS-averaging duration for waypoint marking.
|
||||
rd(&_prefs.gps_avg_idx, sizeof(_prefs.gps_avg_idx));
|
||||
if (_prefs.gps_avg_idx >= NodePrefs::GPS_AVG_COUNT) _prefs.gps_avg_idx = 0;
|
||||
// Pre-0x10 files leave stray sentinel bytes here, same as a never-configured
|
||||
// device. Either way there's no valid saved profile, so default to a profile
|
||||
// in the same band as the companion's own network (_prefs.freq, already read
|
||||
@@ -618,6 +621,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.locator_beeper, sizeof(_prefs.locator_beeper));
|
||||
file.write((uint8_t *)&_prefs.locator_target_kind, sizeof(_prefs.locator_target_kind));
|
||||
file.write((uint8_t *)_prefs.locator_key, sizeof(_prefs.locator_key));
|
||||
file.write((uint8_t *)&_prefs.gps_avg_idx, sizeof(_prefs.gps_avg_idx));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is
|
||||
// the one we check: once the flash fills, writes return 0, so a good
|
||||
|
||||
@@ -264,6 +264,12 @@ struct NodePrefs { // persisted to file
|
||||
// discrete arrive/leave alert (locator_mode).
|
||||
uint8_t locator_beeper; // 0=off (default), 1=on
|
||||
|
||||
// GPS averaging for waypoint marking — when set, "Mark here" samples the GPS
|
||||
// fix for this many seconds and stores the mean position, for a more accurate
|
||||
// mark than a single instantaneous fix. 0 = off (instant mark, the default).
|
||||
// Index into gpsAvgSecs(). [Tools › Trail › Settings › Mark avg]
|
||||
uint8_t gps_avg_idx;
|
||||
|
||||
// Single source of truth for the live-share option tables (shared by the Map
|
||||
// UI labels and the auto-send engine in UITask).
|
||||
static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
|
||||
@@ -295,6 +301,17 @@ struct NodePrefs { // persisted to file
|
||||
return L[m < LOCATOR_MODE_COUNT ? m : 0];
|
||||
}
|
||||
|
||||
// GPS-averaging durations for waypoint marking (seconds). 0 = off (instant).
|
||||
static const uint8_t GPS_AVG_COUNT = 4;
|
||||
static uint16_t gpsAvgSecs(uint8_t idx) {
|
||||
static const uint16_t S[GPS_AVG_COUNT] = { 0, 5, 10, 30 };
|
||||
return S[idx < GPS_AVG_COUNT ? idx : 0];
|
||||
}
|
||||
static const char* gpsAvgLabel(uint8_t idx) {
|
||||
static const char* L[GPS_AVG_COUNT] = { "Off", "5s", "10s", "30s" };
|
||||
return L[idx < GPS_AVG_COUNT ? idx : 0];
|
||||
}
|
||||
|
||||
// Trail auto-pause delays (seconds). 0 = off.
|
||||
static const uint8_t TRAIL_AUTOPAUSE_COUNT = 4;
|
||||
static uint16_t trailAutoPauseSecs(uint8_t idx) {
|
||||
@@ -315,7 +332,7 @@ struct NodePrefs { // persisted to file
|
||||
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
|
||||
// older saves are detected on load and skipped (zero-init defaults kept).
|
||||
// High 24 bits identify the file format; low byte is the schema revision.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0015;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0016;
|
||||
|
||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||
|
||||
@@ -72,7 +72,7 @@ class TrailScreen : public UIScreen {
|
||||
// (Start/Stop tracking, Reset). PopupMenu stores label pointers verbatim, so
|
||||
// the labels live in member buffers that get refreshed in openActionMenu()
|
||||
// and after every LEFT/RIGHT cycle.
|
||||
enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_GRID, ACT_AUTOPAUSE, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT, ACT_EXPORT_SAVED, ACT_MARK, ACT_WAYPOINTS, ACT_FILE, ACT_SETTINGS,
|
||||
enum ActionId { ACT_MIN_DIST, ACT_UNITS, ACT_GRID, ACT_AUTOPAUSE, ACT_MARK_AVG, ACT_TOGGLE, ACT_SAVE, ACT_LOAD, ACT_RESET, ACT_EXPORT, ACT_EXPORT_SAVED, ACT_MARK, ACT_WAYPOINTS, ACT_FILE, ACT_SETTINGS,
|
||||
ACT_SHARE_NOW };
|
||||
// The action popup is multi-level: a short main menu, plus "Trail file…" and
|
||||
// "Settings…" submenus. _menu_level tracks which is open so input is routed
|
||||
@@ -88,6 +88,7 @@ class TrailScreen : public UIScreen {
|
||||
char _act_units_label[24];
|
||||
char _act_grid_label[16];
|
||||
char _act_autopause_label[24];
|
||||
char _act_mark_avg_label[24];
|
||||
char _act_toggle_label[20];
|
||||
|
||||
static const int SUMMARY_ITEM_COUNT = 5;
|
||||
@@ -136,6 +137,10 @@ public:
|
||||
return _store->isActive() ? 1000 : 5000;
|
||||
}
|
||||
|
||||
// Forward the loop tick to the waypoint UI so GPS averaging (Mark avg) can
|
||||
// sample independently of the render cadence (slow on e-ink).
|
||||
void poll() override { _wp.poll(); }
|
||||
|
||||
bool handleInput(char c) override {
|
||||
// Waypoint management UI consumes all input while active.
|
||||
if (_wp.active()) return _wp.handleInput(c);
|
||||
@@ -172,6 +177,7 @@ public:
|
||||
case ACT_MIN_DIST:
|
||||
case ACT_UNITS:
|
||||
case ACT_GRID:
|
||||
case ACT_MARK_AVG:
|
||||
case ACT_AUTOPAUSE: cycleSetting(act, 1); reopenSettingsAt(sel); return true;
|
||||
case ACT_SHARE_NOW: shareMyLocationNow(); break;
|
||||
case ACT_TOGGLE:
|
||||
@@ -318,6 +324,8 @@ private:
|
||||
"Grid: %s", _map_grid ? "ON" : "OFF");
|
||||
snprintf(_act_autopause_label, sizeof(_act_autopause_label),
|
||||
"Auto-pause: %s", NodePrefs::trailAutoPauseLabel(p ? p->trail_autopause_idx : 0));
|
||||
snprintf(_act_mark_avg_label, sizeof(_act_mark_avg_label),
|
||||
"Mark avg: %s", NodePrefs::gpsAvgLabel(p ? p->gps_avg_idx : 0));
|
||||
snprintf(_act_toggle_label, sizeof(_act_toggle_label),
|
||||
"%s tracking", _store->isActive() ? "Stop" : "Start");
|
||||
}
|
||||
@@ -382,6 +390,7 @@ private:
|
||||
_action_menu.begin("Settings", 4);
|
||||
pushAction(ACT_MIN_DIST, _act_min_dist_label);
|
||||
pushAction(ACT_AUTOPAUSE, _act_autopause_label);
|
||||
pushAction(ACT_MARK_AVG, _act_mark_avg_label);
|
||||
if (_view == V_SUMMARY) pushAction(ACT_UNITS, _act_units_label);
|
||||
if (_view == V_MAP) pushAction(ACT_GRID, _act_grid_label);
|
||||
}
|
||||
@@ -393,6 +402,7 @@ private:
|
||||
if (act == ACT_UNITS && p) { cycleUnits(p, dir); _cfg_dirty = true; refreshActionLabels(); return true; }
|
||||
if (act == ACT_GRID) { _map_grid = !_map_grid; refreshActionLabels(); return true; }
|
||||
if (act == ACT_AUTOPAUSE && p){ cycleAutoPause(p, dir); _cfg_dirty = true; refreshActionLabels(); return true; }
|
||||
if (act == ACT_MARK_AVG && p){ cycleMarkAvg(p, dir); _cfg_dirty = true; refreshActionLabels(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -435,6 +445,13 @@ private:
|
||||
: (idx + NodePrefs::TRAIL_AUTOPAUSE_COUNT - 1) % NodePrefs::TRAIL_AUTOPAUSE_COUNT;
|
||||
p->trail_autopause_idx = idx;
|
||||
}
|
||||
void cycleMarkAvg(NodePrefs* p, int dir) {
|
||||
uint8_t idx = p->gps_avg_idx;
|
||||
if (idx >= NodePrefs::GPS_AVG_COUNT) idx = 0;
|
||||
idx = (dir > 0) ? (idx + 1) % NodePrefs::GPS_AVG_COUNT
|
||||
: (idx + NodePrefs::GPS_AVG_COUNT - 1) % NodePrefs::GPS_AVG_COUNT;
|
||||
p->gps_avg_idx = idx;
|
||||
}
|
||||
|
||||
// One-shot manual share: build "[LOC]lat,lon" and hand it to the Messages
|
||||
// screen, where the user picks a DM or channel recipient. (Auto live-share
|
||||
|
||||
@@ -20,11 +20,22 @@ class WaypointsView {
|
||||
TrailStore* _store;
|
||||
|
||||
// Sub-modes layered over the trail views. OFF = the component is dormant.
|
||||
enum Mode { OFF, LIST, NAV, ADD };
|
||||
enum Mode { OFF, LIST, NAV, ADD, AVG };
|
||||
uint8_t _mode = OFF;
|
||||
int _sel = 0;
|
||||
int _scroll = 0;
|
||||
|
||||
// GPS averaging (Tools › Trail › Settings › Mark avg). When enabled, markHere()
|
||||
// accumulates fixes for gps_avg_idx seconds and marks the mean position — a
|
||||
// steadier mark than one instantaneous fix. Sampling runs in poll() on a 1 s
|
||||
// gate, independent of (slow, on e-ink) redraws. int64 sums: 30 samples ×
|
||||
// ~180e6 overflows int32.
|
||||
long long _avg_sum_lat = 0, _avg_sum_lon = 0;
|
||||
uint32_t _avg_n = 0; // fixes accumulated so far
|
||||
uint32_t _avg_end_ms = 0; // millis() when the averaging window closes
|
||||
uint32_t _avg_next_ms = 0; // millis() of the next sample
|
||||
uint16_t _avg_total_s = 0; // configured window length (for the readout)
|
||||
|
||||
PopupMenu _ctx; // Rename / Delete / Send on a selected waypoint
|
||||
bool _kb_active = false;
|
||||
int _kb_rename_idx = -1; // -1 = marking new, ≥0 = renaming that index
|
||||
@@ -99,6 +110,17 @@ class WaypointsView {
|
||||
_kb_active = true;
|
||||
}
|
||||
|
||||
// Capture a mark position and open the keyboard for its label. Shared by the
|
||||
// instant "Mark here" and the end of GPS averaging. _mode goes OFF: the
|
||||
// keyboard takes over the screen, and there's no sub-view to return to once
|
||||
// the label is committed (active() then tracks _kb_active alone).
|
||||
void beginLabel(int32_t lat, int32_t lon) {
|
||||
_mode = OFF;
|
||||
_mark_lat = lat; _mark_lon = lon; _mark_ts = (uint32_t)rtc_clock.getCurrentTime();
|
||||
_kb_rename_idx = -1;
|
||||
openKb("", WAYPOINT_LABEL_LEN - 1);
|
||||
}
|
||||
|
||||
// Commit a mark-here / rename label from the keyboard.
|
||||
void commitLabel(const char* buf) {
|
||||
if (_kb_rename_idx >= 0) {
|
||||
@@ -144,6 +166,23 @@ class WaypointsView {
|
||||
}
|
||||
}
|
||||
|
||||
// GPS-averaging progress screen. Sampling itself happens in poll(); this only
|
||||
// reports remaining time and the running sample count.
|
||||
void renderAvg(DisplayDriver& display) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawCenteredHeader("AVERAGING GPS");
|
||||
const int top = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
int remain = (int)((int32_t)(_avg_end_ms - millis()) / 1000);
|
||||
if (remain < 0) remain = 0;
|
||||
char line[28];
|
||||
snprintf(line, sizeof(line), "%ds left (of %us)", remain, (unsigned)_avg_total_s);
|
||||
display.setCursor(2, top); display.print(line);
|
||||
snprintf(line, sizeof(line), "Samples: %u", (unsigned)_avg_n);
|
||||
display.setCursor(2, top + step); display.print(line);
|
||||
display.setCursor(2, top + 2 * step); display.print("Cancel to abort");
|
||||
}
|
||||
|
||||
void renderWpList(DisplayDriver& display) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
char title[24];
|
||||
@@ -220,9 +259,35 @@ public:
|
||||
int32_t lat, lon;
|
||||
if (!ownPos(lat, lon)) { _task->showAlert("No GPS fix", 1000); return; }
|
||||
if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
|
||||
_mark_lat = lat; _mark_lon = lon; _mark_ts = (uint32_t)rtc_clock.getCurrentTime();
|
||||
_kb_rename_idx = -1;
|
||||
openKb("", WAYPOINT_LABEL_LEN - 1);
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
uint8_t avg_idx = p ? p->gps_avg_idx : 0;
|
||||
if (avg_idx == 0) { beginLabel(lat, lon); return; } // instant mark (default)
|
||||
// Averaging: seed with the current fix, then sample for N more seconds.
|
||||
_avg_total_s = NodePrefs::gpsAvgSecs(avg_idx);
|
||||
_avg_sum_lat = lat; _avg_sum_lon = lon; _avg_n = 1;
|
||||
uint32_t now = millis();
|
||||
_avg_end_ms = now + (uint32_t)_avg_total_s * 1000;
|
||||
_avg_next_ms = now + 1000;
|
||||
_mode = AVG;
|
||||
}
|
||||
|
||||
// Accumulate GPS fixes while averaging. Driven from TrailScreen::poll() so it
|
||||
// ticks on the main loop, not on the (slow on e-ink) render cadence. No-op
|
||||
// unless an averaging window is open.
|
||||
void poll() {
|
||||
if (_mode != AVG) return;
|
||||
uint32_t now = millis();
|
||||
if ((int32_t)(now - _avg_next_ms) >= 0) {
|
||||
int32_t lat, lon;
|
||||
if (ownPos(lat, lon)) { _avg_sum_lat += lat; _avg_sum_lon += lon; _avg_n++; }
|
||||
_avg_next_ms = now + 1000;
|
||||
}
|
||||
if ((int32_t)(now - _avg_end_ms) >= 0) { // window closed → mark the mean
|
||||
if (_avg_n == 0) { _task->showAlert("No GPS fix", 1000); _mode = OFF; return; }
|
||||
int32_t mlat = (int32_t)(_avg_sum_lat / (long long)_avg_n);
|
||||
int32_t mlon = (int32_t)(_avg_sum_lon / (long long)_avg_n);
|
||||
beginLabel(mlat, mlon); // opens the label keyboard
|
||||
}
|
||||
}
|
||||
|
||||
// Only called while active().
|
||||
@@ -231,6 +296,7 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_kb_active) return _task->keyboard().render(display); // keyboard owns the screen
|
||||
if (_mode == ADD) { renderAddForm(display); return 1000; }
|
||||
if (_mode == AVG) { renderAvg(display); return display.isEink() ? 1000 : 300; }
|
||||
if (_mode == NAV) { renderWpNav(display); return 1000; }
|
||||
renderWpList(display); // LIST
|
||||
if (_ctx.active) _ctx.render(display);
|
||||
@@ -293,6 +359,12 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Averaging window — any cancel aborts the mark; otherwise just wait it out.
|
||||
if (_mode == AVG) {
|
||||
if (c == KEY_CANCEL) _mode = OFF;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ADD form: scroll-edit lat/lon (magnitude) + hemisphere toggle + label.
|
||||
if (_mode == ADD) {
|
||||
// The digit editor, while open, consumes all input (UP/DOWN change the
|
||||
|
||||
Reference in New Issue
Block a user