diff --git a/docs/solo_features/tools_screen/tools_screen.md b/docs/solo_features/tools_screen/tools_screen.md index 65778505..e901f209 100644 --- a/docs/solo_features/tools_screen/tools_screen.md +++ b/docs/solo_features/tools_screen/tools_screen.md @@ -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`). diff --git a/examples/companion_radio/ui-new/TrailScreen.h b/examples/companion_radio/ui-new/TrailScreen.h index 1ad33722..3b967e41 100644 --- a/examples/companion_radio/ui-new/TrailScreen.h +++ b/examples/companion_radio/ui-new/TrailScreen.h @@ -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..."); diff --git a/examples/companion_radio/ui-new/WaypointsView.h b/examples/companion_radio/ui-new/WaypointsView.h index 5d03b405..9c10f976 100644 --- a/examples/companion_radio/ui-new/WaypointsView.h +++ b/examples/companion_radio/ui-new/WaypointsView.h @@ -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();