feat(companion): track-back — retrace the recorded trail to its start

Add Tools › Trail → Hold Enter → Track back (shown when the trail has ≥2
points). It reuses NavView but, instead of a single fixed target, snaps onto
the route at the nearest recorded breadcrumb and walks the points in reverse,
auto-advancing the target as each is reached (within 20 m). The header shows
points remaining ("Back: 12 pt" → "Trail start"); reaching the start toasts
"Back at start" and exits. Cancel leaves at any time.

Lives in WaypointsView as a new sub-mode; advancement runs in poll()
(forwarded from TrailScreen) so it tracks GPS independently of the render
cadence. An EtaTracker drives the approach/ETA line and is reset on each
breadcrumb advance to avoid a bogus closing-speed spike.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-29 19:27:54 +02:00
parent c60a7a7f64
commit 4d795ade51
3 changed files with 78 additions and 6 deletions

View File

@@ -107,6 +107,7 @@ Cycle views with **LEFT / RIGHT**:
| Start / Stop tracking | Begin or end a recording session. If **GPS is off**, choosing Start asks **"GPS is off — Enable GPS & start"** so a session can't silently run with nothing to record |
| Mark here | Drop a waypoint at the current GPS fix (see below) |
| Waypoints… | Open the waypoint list / navigation / add-by-coords |
| Track back | Retrace the recorded route back to its start (needs ≥2 points; see below) |
| Share my pos | Send your current position as a one-shot `[LOC]` message — pick a contact or channel (see **Live Share**) |
| Trail file… | Open the file submenu (below) |
| Settings… | Open the settings submenu (below) |
@@ -135,6 +136,10 @@ Cycle views with **LEFT / RIGHT**:
**Auto-pause** — when set, a recording trail automatically **pauses** after the device has stayed within ~15 m of one spot for the chosen delay: the elapsed timer and point sampling both freeze, and the map line breaks across the idle gap. It **resumes on its own** as soon as you move again. This keeps a stop (a break, a meal, parking) out of your distance and average-speed stats without you having to remember to stop and restart tracking. A paused trail is still "on" (the **G** marker keeps blinking) — the Summary **Status** row shows `paused`. The stop is detected with its own coarse movement gate, independent of **Min dist**, so GPS jitter while you're parked doesn't keep it awake.
### Track back
**Hold Enter → Track back** retraces the trail you just recorded, back to where you started — useful for returning the same way in poor visibility or unfamiliar ground. It reuses the navigation view (distance + two absolute bearings; see *Waypoints Navigating*), but instead of a single fixed target it walks the recorded breadcrumbs in reverse: it snaps onto the route at the **nearest recorded point**, guides you to it, then automatically advances to the next earlier point as you reach each one (within ~20 m). The header shows how many points remain (`Back: 12 pt`), reading `Trail start` on the final leg; arriving there shows `Back at start` and exits. **Cancel** leaves track-back at any time. It needs a trail with at least two points and a GPS fix; it doesn't require tracking to still be running.
### Waypoints
A waypoint is a saved spot — your car, camp, a water source — that you can navigate back to later. Waypoints are **independent of the trail**: they live in their own flash file (`/waypoints`), survive a reboot, and are **not** cleared by *Reset trail*. Up to 16 can be stored — the Waypoints list header shows how many are in use (e.g. `WAYPOINTS 3/16`).

View File

@@ -73,7 +73,7 @@ class TrailScreen : public UIScreen {
// 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_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 };
ACT_SHARE_NOW, ACT_TRACKBACK };
// 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
// correctly (settings rows cycle with LEFT/RIGHT; everything else is Enter).
@@ -192,6 +192,7 @@ public:
break;
case ACT_MARK: _wp.markHere(); break;
case ACT_WAYPOINTS: _wp.openList(); break;
case ACT_TRACKBACK: _wp.startTrackBack(); break;
case ACT_SAVE: handleSave(); break;
case ACT_LOAD: handleLoad(); break;
case ACT_RESET: handleReset(); break;
@@ -353,6 +354,8 @@ private:
// "Waypoints" opens the nav list — always available; it hosts the list,
// backtrack (Trail-start row) and the "+ Add by coords" entry.
pushAction(ACT_WAYPOINTS, "Waypoints...");
// Track back retraces the recorded route to its start; needs ≥2 points.
if (_store->count() >= 2) pushAction(ACT_TRACKBACK, "Track back");
pushAction(ACT_SHARE_NOW, "Share my pos");
if (fileMenuHasItems()) pushAction(ACT_FILE, "Trail file...");
pushAction(ACT_SETTINGS, "Settings...");

View File

@@ -20,11 +20,18 @@ class WaypointsView {
TrailStore* _store;
// Sub-modes layered over the trail views. OFF = the component is dormant.
enum Mode { OFF, LIST, NAV, ADD, AVG };
enum Mode { OFF, LIST, NAV, ADD, AVG, TRACKBACK };
uint8_t _mode = OFF;
int _sel = 0;
int _scroll = 0;
// Track-back (Tools Trail Track back): retrace the recorded trail in
// reverse using NavView. _tb_idx is the current target breadcrumb; it walks
// down to 0 (the start) as each is reached within TB_ARRIVE_M.
static const int TB_ARRIVE_M = 20;
int _tb_idx = 0;
navview::EtaTracker _tb_eta;
// 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
@@ -226,6 +233,20 @@ class WaypointsView {
navview::draw(display, have, mylat, mylon, tlat, tlon, label, cogv, cog, useImperial());
}
// Navigate to the current track-back breadcrumb. The header doubles as the
// progress readout: "Trail start" on the last leg, else points still to go.
void renderTrackBack(DisplayDriver& display) {
if (_tb_idx < 0 || _tb_idx >= _store->count()) { _mode = OFF; return; }
const TrailPoint& t = _store->at(_tb_idx);
int32_t mylat, mylon; bool have = ownPos(mylat, mylon);
int cog; bool cogv = _task->currentCourse(cog);
char label[20];
if (_tb_idx == 0) snprintf(label, sizeof(label), "Trail start");
else snprintf(label, sizeof(label), "Back: %d pt", _tb_idx);
navview::draw(display, have, mylat, mylon, t.lat_1e6, t.lon_1e6,
label, cogv, cog, useImperial(), &_tb_eta);
}
// Resolve a combined-list row to a nav target. Row 0 is the trail start when
// a trail exists; the rest are saved waypoints.
bool rowTarget(int row, int32_t& lat, int32_t& lon, const char*& label) {
@@ -271,11 +292,34 @@ public:
_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.
// Retrace the recorded trail back to its start. Snaps onto the route at the
// nearest recorded point, then NavView guides to each earlier breadcrumb in
// turn (poll() advances the target) until the start is reached.
void startTrackBack() {
if (_store->count() < 2) { _task->showAlert("No trail", 1000); return; }
int idx = _store->count() - 1; // default: the newest end of the trail
int32_t lat, lon;
if (ownPos(lat, lon)) { // else snap to the nearest recorded point
float best = 1e30f;
for (int i = 0; i < _store->count(); i++) {
float d = geo::haversineKm(lat, lon, _store->at(i).lat_1e6, _store->at(i).lon_1e6);
if (d < best) { best = d; idx = i; }
}
}
_tb_idx = idx;
_tb_eta.reset();
_mode = TRACKBACK;
}
// Driven from TrailScreen::poll() so the position-tracking sub-modes tick on
// the main loop, not on the (slow on e-ink) render cadence.
void poll() {
if (_mode != AVG) return;
if (_mode == AVG) pollAvg();
else if (_mode == TRACKBACK) pollTrackBack();
}
// Accumulate GPS fixes while averaging; mark the mean when the window closes.
void pollAvg() {
uint32_t now = millis();
if ((int32_t)(now - _avg_next_ms) >= 0) {
int32_t lat, lon;
@@ -290,6 +334,18 @@ public:
}
}
// Advance the track-back target when the current breadcrumb is reached; once
// at the start (index 0) and within range, announce arrival and exit.
void pollTrackBack() {
int32_t lat, lon;
if (!ownPos(lat, lon)) return;
float d_m = geo::haversineKm(lat, lon, _store->at(_tb_idx).lat_1e6,
_store->at(_tb_idx).lon_1e6) * 1000.0f;
if (d_m > (float)TB_ARRIVE_M) return;
if (_tb_idx > 0) { _tb_idx--; _tb_eta.reset(); }
else { _task->showAlert("Back at start", 1500); _mode = OFF; }
}
// Only called while active().
int render(DisplayDriver& display) {
display.setTextSize(1);
@@ -297,6 +353,7 @@ public:
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 == TRACKBACK) { renderTrackBack(display); return 1000; }
if (_mode == NAV) { renderWpNav(display); return 1000; }
renderWpList(display); // LIST
if (_ctx.active) _ctx.render(display);
@@ -400,6 +457,13 @@ public:
return true;
}
// Track-back — launched from the action menu, so Cancel returns to the
// trail views (not the waypoint list).
if (_mode == TRACKBACK) {
if (c == KEY_CANCEL) _mode = OFF;
return true;
}
// List (row 0 is the synthetic "Trail start" when a trail exists; the final
// row is "+ Add by coords"). Cancel returns control to the trail views.
int n = wpListCount();