docs(features): waypoints + node navigation design (compass via GPS COG)

This commit is contained in:
Jakub
2026-06-02 23:49:21 +02:00
parent 5b25d1eb32
commit e019afcfc0

View File

@@ -153,11 +153,146 @@ Edge cases:
## Backlog (not yet prioritised) ## Backlog (not yet prioritised)
### Compass to contact ### Waypoints — mark a spot, navigate back ⭐ (next candidate)
Select contact in Nearby → Enter → fullscreen compass. Arrow points to last-known position of contact relative to my GPS heading (need accel/mag or compute heading from movement). Distance below. Updates every 1 s. Turns Solo from a comms device into a basic GPS navigator. Mark the current
position with a short label, then later get bearing + distance back to it —
ideal for off-grid use (car, camp, trailhead, water source).
Joystick: Enter from Nearby toggles, Back exits. Heading-from-GPS requires moving; for stationary use, would need magnetometer that the L1 board does or doesn't have — check. **Storage** — dedicated flash file `/waypoints` (separate from prefs, like
`/trail`). Fixed table, no schema-sentinel impact:
```
struct Waypoint {
int32_t lat_1e6, lon_1e6; // saved fix
uint32_t ts; // when marked (RTC)
char label[12]; // short name, NUL-terminated ("CAR", "CAMP", "H2O"…)
};
static const int WAYPOINT_MAX = 16; // 16 × 24 B = 384 B file
```
**Marking** — from the GPS or Trail screen: Hold Enter → "Mark here".
- Requires a GPS fix; otherwise alert "No GPS fix".
- Opens the existing `KeyboardWidget` to type the label (≤11 chars). Empty
input auto-labels `WP<n>`.
- Saves the current fix + label, appends to the file.
**Visible on the trail Map** — waypoints render on the existing Map view as a
distinct marker (e.g. a hollow diamond or a small flag) so they show in
context with the recorded track:
- Fold waypoint coords into the map's bounding box so off-track waypoints
stay in frame. Today `renderMap()` derives the box from
`TrailStore::boundingBox()`; extend it to also span the waypoint table
(and handle the "waypoints but empty trail" case — map still renders).
- Generalise the `project()` lambda to take raw (lat, lon) instead of a
`TrailPoint&` so the same projection draws both track points and waypoints.
- Marker shows the label's first character beside it when there's room
(122 px is tight with many waypoints); the full label lives in the list /
nav view.
**Trail workflow integration** — waypoints live *inside* the Trail screen, not
a separate Tools entry, because marking points of interest happens while you
are recording:
- **Mark**: Trail → Hold Enter → "Mark here" (new action-menu row). Opens the
keyboard for the label, saves the current fix. Works whether or not tracking
is active — a waypoint is independent of trail recording state.
- **Manage / navigate**: Trail → Hold Enter → "Waypoints" → a PopupMenu list
of saved waypoints (label + distance). Selecting one:
- Enter → fullscreen nav (see below).
- Hold Enter → Rename / Delete (later: **Share over mesh**).
- Waypoints persist across trail Reset / reboot (separate `/waypoints` file),
unlike the RAM trail ring.
**Navigation view — two bearings, no compass needed**
The L1 has no magnetometer, so we can't show "you are facing X". Instead the
nav view shows two *absolute* bearings and lets the user do the comparison —
robust against GPS jitter, no relative-turn maths:
```
CAMP ← waypoint label
1.4 km ← distance to target
To: 145° SE ← absolute bearing target-from-me (haversine/bearingDeg)
Hdg: 090° E ← my course over ground, derived from recent GPS movement
```
Reading it: target is at 145°, I'm travelling at 90° → I need to bear right.
When standing still (course undefined) the Hdg line shows `--` instead of a
stale value.
**Course over ground (COG)** — a small `TrailStore` helper:
`bool currentCourse(int& deg)` walks back from the newest fix until the
cumulative distance from it exceeds a threshold (~1015 m so GPS noise
doesn't dominate) and returns the bearing from that older fix to the newest.
Returns false (→ `--`) when there isn't enough recent movement. Refreshed ~1 s.
`bearingDeg` / `bearingCardinal` are currently private statics in NearbyScreen;
lift them into a shared header (or TrailStore) so both screens use them.
This COG value can later back a standalone "heading" trail view if wanted, but
the two-bearing nav view already covers the practical need.
**Shared nav view — also navigate to a node**
The nav view's target is just `(lat, lon, label)` — it doesn't care whether
that came from a waypoint, the trail start (Backtrack), or a node's
last-known advert position. Make it a small reusable component
(`NavView` / `drawNavTo(display, target_lat, target_lon, label)`) and wire it
in from **Nearby Nodes** too: node detail → "Navigate" → same To/Hdg/distance
screen, retargeting the selected node. This folds the backlog "Compass to
contact" idea into one screen and means Nearby stops being a static snapshot —
you can actually walk toward a person.
Node target is the contact's `gps_lat/gps_lon` from its last advert (already
read in NearbyScreen). It's a *last-known* fix, not live, so the label could
show the advert age (e.g. `Alice (5m)`); pair it with Auto-Advert on the other
device for a moving target.
**COG source must be decoupled from trail recording.** Deriving the heading
from `TrailStore` only works while a trail is actively being recorded — but
node/waypoint navigation shouldn't require the user to start trail logging.
Fix: a tiny independent COG ring in UITask (≈4 fixes), maintained on every GPS
poll regardless of trail state. The trail ring stays a separate concern, so
the Hdg line is available everywhere (waypoints, nodes, backtrack).
The COG ring is **time-sampled with guards**, not raw displacement-gated:
push every GPS fix at the normal poll cadence (~1 s) into a short rolling
window (a few seconds of fixes), and compute the heading as the bearing
across the window (oldest→newest), optionally low-pass smoothed. Time-based
sampling gives a steadier, averaged course than bearing between just two
points, and tracks slow movement without lagging.
Two guards keep it honest:
- **Gross-error rejection** — drop a fix before it enters the window if it
implies an impossible jump (implied speed over a sane cap, e.g. > ~50 m/s
between consecutive fixes), or if the provider exposes a usable
validity/HDOP signal. One bad fix shouldn't swing the heading.
- **Minimum displacement over the window** — only emit a heading once the
total movement across the window exceeds a small threshold (a few m). Below
that the user is effectively stationary: hold the last good heading, and
show `--` until the window has cleared the threshold at least once. This
is what stops a standing user from getting a spinning bearing.
Threshold(s) can be fixed constants to start; expose in Settings later if it
proves worth tuning.
Open question: whether the nav view should also be reachable as a 4th Map
overlay state (cycle a "highlighted" waypoint with LEFT/RIGHT on the Map) or
stay list-driven only. Start list-driven; add map cycling later if wanted.
### Backtrack — navigate home along the recorded trail
Pairs with Waypoints but needs zero new storage: read the live `TrailStore`
ring and show bearing + distance back to the trail start (or the nearest
recorded point). "Get me back to where I started." A Trail action-menu entry
"Navigate to start" opens the same fullscreen nav view as Waypoints.
### Compass to contact — folded into the Waypoints nav view
Superseded by the shared nav view described under **Waypoints** above:
navigate to a node = open that nav view with the contact's last-advert
`gps_lat/gps_lon` as the target. No separate compass screen needed; the
"two absolute bearings (To / Hdg)" approach replaces the relative-heading
arrow this entry originally assumed. Kept here only as a cross-reference.
### SOS broadcast ### SOS broadcast