Tidies the Trail screen end to end, with no functional change beyond the
intended UX cleanups. Squashed from refactor/trail-screen.
Popup:
- Two-level action menu (Hold Enter): short main menu (Start/Stop, Mark
here, Waypoints…, Trail file…, Settings…) plus "Trail file…" and
"Settings…" submenus, replacing one flat ~12-item list that mixed
settings and actions. One interaction pattern per level; Reset lives in
Trail file…, away from a stray Enter. Settings submenu is view-aware
(Grid only on Map, Readout only on Summary).
Map:
- MapProjection (geo→pixel) built once and shared by the map, grid and
marker drawing — removes the duplicated projection math and renderGrid's
11 scalar params.
- renderMap split into computeBounds() and drawMarkers() helpers.
Grid:
- Square cells fitted to the frame: the shorter side is divided into a
whole number of equal cells (grid touches that pair of borders), and the
longer side centres the whole cells that fit — square, symmetric, no
one-sided drift. Drawn as one dot per intersection (legible on OLED).
Waypoints:
- Extracted the whole waypoint management UI (list / navigate / add-by-
coords / mark / rename / delete / send) into a self-contained
WaypointsView component that TrailScreen owns and delegates to.
- Dropped the redundant bulk "Clear all" (per-waypoint Delete covers it).
Docs updated (tools_screen.md) + design rationale (docs/design/
trail_redesign.md). Builds clean on solo envs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
End-to-end delivery indicators on the device UI, drawn next to outgoing
messages and auto-scaled to the font (legible on landscape e-ink).
DM (and room servers, which share the DM path):
- Pending / delivered / failed marker driven by the real end-to-end ACK.
Pending shows a row of dots — one per send — so auto-resend progress is
visible before it resolves to ✓ / ✗.
- Auto-resend: new pref dm_resend_count (0-5, default 2), Settings ›
Messages › Resend. A pending DM whose ACK times out is re-sent (reusing
the original timestamp) until resends run out, then ✗. Driven from
UITask::loop so it completes in the background, independent of screen.
- Incoming dedup: a retry reuses the sender timestamp + text but carries a
fresh packet hash, so addDMMsg drops copies matching prefix+ts+text.
Channels (flood, no recipient ACK):
- ✓ only once a repeater echo confirms the send was relayed into the mesh;
no echo is normal, not a failure (no pending/fail shown). A small ring
tracks a burst of sends so each matches its echo. Receive-path hashing is
gated so the hot flood path is untouched when idle.
Shared:
- Markers shown in both the history list and the fullscreen message view.
- Reusable scalable mini-icon facility in icons.h (bitmap + auto-scale);
adding a new status icon is a bitmap plus one draw call.
No changes to the upstream mesh library (src/).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Persist.h: shared writeHeader()/readHeader() for the magic+version+count
on-disk header; Waypoint and Trail snapshots now share one implementation
(byte layout unchanged; each keeps its own count clamp/reject + extra fields).
- TrailScreen: pushAction() guards _act_map / PopupMenu capacity, so adding a
new action can't silently overrun the fixed array.
- PopupMenu: add count()/setSelected() accessors; replace the remaining direct
_sel/_count field pokes in NearbyScreen, TrailScreen and QuickMsgScreen.
- Trail summary: unit-suffixed time ("1h 05m" / "5m 03s") so hours can't be
misread as minutes.
- Trail::haversineMeters delegates to geo::haversineKm — single Haversine
implementation (also trims a little flash).
Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo builds compile clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull repeated UI idioms into DisplayDriver and remove duplicated logic:
- DisplayDriver: add drawScrollArrows() and drawInvertedHeader(); replaces 11
copy-pasted scroll-arrow blocks across 5 screens and 3 inverted title-bar
blocks (NearbyScreen x2, NavView). Standardises the detail-view separator.
- useImperial(): single source on UITask; NearbyScreen/TrailScreen delegate
instead of each re-reading NodePrefs.units_imperial.
- NearbyScreen: extract saveSelectedWaypoint() (two identical blocks -> one);
drop the redundant local label buffer (WaypointStore truncates).
- UITask::shutdown(): power GPS off through LocationProvider::stop() instead of
poking PIN_GPS_EN directly — centralises enable/reset-pin + active-level logic.
Net -58 lines. Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo
builds compile clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirrors the Waypoint::readFrom() hardening — reject a truncated trail
snapshot instead of proceeding with garbage header fields. Consistency
with the waypoint store's persistence path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UITask::shutdown(): power GPS off using !PIN_GPS_EN_ACTIVE instead of hardcoded
LOW, matching MicroNMEALocationProvider::stop() (still LOW on current active-high
devices, but correct for a future active-low GPS).
- buzzer _noteFreq(): clamp octave to 8 instead of 7 so the parser's accepted range
(4-8) is fully consumed; previously an octave-8 digit was clamped away and could
leak into the stream as the next note's duration.
- Waypoint::readFrom(): check read() return for version/reserved/count header bytes
so a truncated file is rejected instead of using garbage count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NRF52 GPIO pins retain their last state in SYSTEMOFF. If GPS was active,
PIN_GPS_EN stays HIGH and the module keeps drawing ~20mA. gps_enabled is
already persisted to flash so applyGpsPrefs() restores the correct state
on next boot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trigger is now stored as typed (e.g. "Ping" stays "Ping").
Case-insensitive matching is preserved by lowercasing both
the trigger and incoming message at comparison time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UITask.onBLEDisconnected() sets _next_refresh=0 so the BLE icon
updates instantly instead of waiting for the next polling cycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add build-solo-firmwares.yml: auto-detects all _solo_dual envs from platformio.ini, builds in parallel, publishes uf2 files to draft release
- Remove build-wio-tracker-l1-firmwares.yml (replaced by solo workflow)
- Add WioTrackerL1_companion_solo_dual and WioTrackerL1Eink_companion_solo_dual envs
- Wire build-solo-firmwares command in build.sh using _solo_dual suffix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>