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:
MarekZegare4
2026-06-29 19:19:43 +02:00
co-authored by Claude Opus 4.8
parent 35622b7693
commit c60a7a7f64
5 changed files with 119 additions and 6 deletions
@@ -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