Commit Graph
21 Commits
Author SHA1 Message Date
JakubandClaude Sonnet 4.6 64b924a9c7 feat(ui): GPS trail — scale grid on map view
Add an optional dot/intersection scale grid to the trail map view:
- renderGrid() draws labelled grid lines anchored to display edges
- Grid step auto-selected for ~3 intervals in the shorter dimension
- MIN_GRID_PX=22 prevents density on small/OLED screens; clamp ensures
  minimum 2 intervals on large trails
- Intersections inside the label bbox and north-arrow bbox are suppressed
- Toggle via Hold-Enter action menu (Grid: ON/OFF item in ActionId enum)
- Map title shows 'TRAIL MAP+' when grid is active
- README updated to mention grid toggle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 17:33:20 +02:00
JakubandClaude Sonnet 4.6 755525761e fix(ui): post-Trail review — alert visible everywhere + writeTo error checks
Four findings from a code review pass on the Trail feature:

- showAlert overlay was gated to `curr == home`, so feedback like
  "Tracking started", "Trail saved", "GPX N B (USB)" never rendered on
  the Trail screen (or QuickMsg / Settings / Auto-Advert, for that
  matter). Drop the gate — the overlay shows on whichever screen the
  caller invoked from.
- TrailStore::writeTo only verified the first write; subsequent point
  writes could partially fail (full filesystem, mid-write power loss)
  and the method would still return true. Check every write return
  value so handleSave's "Trail saved" only fires on actual success.
- gpxPoint: clamp snprintf's intended-length return against the buffer
  size and skip the point if gmtime returns null. Avoids any chance of
  out.write reading past buf[120].
- TrailScreen::_act_map sized exactly to today's 8 actions — bump to 12
  so adding another popup item later doesn't cause an OOB write into
  the surrounding members.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 08:15:53 +02:00
JakubandClaude Sonnet 4.6 129af614a2 feat(ui): trail — export saved trail straight from flash without Load
Adds an "Export saved" item in the action popup (shown when /trail
exists). Streams GPX directly from the saved file to USB Serial,
skipping the RAM ring entirely — useful for dumping an old recording
after reboot when the live ring is empty (or busy with another
recording you don't want to overwrite via Load).

Refactors exportGpx into three helpers (gpxHeader, gpxPoint, gpxFooter)
shared by the RAM and file paths so the markup stays consistent. The
file path validates the same TRAL magic + version it accepts on Load.

handleExport and the new handleExportSaved both route through
showExportAlert so the "GPX N B (USB)" / "GPX N B - disc. app" hint
keeps reflecting the BLE-app-collision state described last time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 08:02:16 +02:00
JakubandClaude Sonnet 4.6 c290722633 refactor(ui): trail GPX — dump always, vary the confirm to reflect risk
Blocking the export when BLE isn't connected also blocked the no-app
case (user attached only a terminal). Dump unconditionally and let the
confirm alert tell the user what just happened:

- BLE connected → "GPX N B (USB)" — USB receive is parked, no collision
- BLE absent   → "GPX N B - disc. app" — reminder that the same USB
                  pipe the app would use just carried raw XML, so
                  reconnect/restart the app if you had one running

User can also reach the "no app, terminal-only" workflow naturally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 07:55:56 +02:00
JakubandClaude Sonnet 4.6 fb32df37c8 fix(ui): trail GPX export — guard against USB-app collision
The Wio L1 DualSerialInterface multiplexes BLE + USB on the same frame
protocol: when BLE is connected, the USB receive path is ignored, so we
can safely push raw GPX bytes to USB without confusing the companion
app. When BLE is *not* connected the app is talking over USB itself —
the dump would land mid-frame and the app would parse garbage.

Block the export when hasConnection() (BLE link to the app) is false
and show "Connect BLE or disconnect app". When it's true the dump runs
and the confirm alert reads "GPX N B over USB" so the user knows which
pipe to read from.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 07:54:53 +02:00
JakubandClaude Sonnet 4.6 2671e3f764 feat(ui): Trail phase 4 — Save/Load to flash + GPX export over USB
Single-slot persistence as agreed. Action popup grows three entries
(conditional on what makes sense for the current state):

- "Save trail"  — writes the current RAM ring to /trail in LittleFS.
- "Load trail"  — only shown when /trail exists; replaces the live ring
                  with the snapshot (any active session ends first).
- "Export GPX"  — dumps the trail as a minimal GPX 1.1 document over
                  Serial so the user can capture it from a USB host.

Plumbing:
- TrailStore gains writeTo / readFrom (template on the file handle, so
  the platform-specific File flavour stays out of Trail.h) plus
  exportGpx(Stream&) for the GPX serializer. Header carries magic
  "TRAL", a version byte, point count, and the accumulated active time
  so elapsed survives the round trip.
- DataStore::openWrite is exposed publicly so callers outside DataStore
  don't have to duplicate the platform open-mode dance.
- MyMesh gains a public getDataStore() accessor.

After Load, _pending_seg_break is set so any subsequent Start opens a
new segment instead of joining the loaded last point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 07:49:17 +02:00
JakubandClaude Sonnet 4.6 dfc9d96ce5 refactor(ui): trail — drop bottom hint, N/3 in title, popup-only start
The hint row plus its separator ate ~12 px on OLED. View counter moves
into the title bar ("TRAIL 1/3", "TRAIL MAP 2/3", "TRAIL LIST 3/3") so
the same context is available without a dedicated row, and the freed
space goes to whatever the view is rendering.

Short Enter now never toggles tracking. Both start and stop are only
reachable through the Hold-Enter popup — symmetric, no stray-tap risk.
Short Enter on the screen just shows "Hold Enter for menu" so the user
who tapped finds out where to look.

Available area for Summary / List recomputed without the hint
reservation (avail = height − y0 − 2); map's bottom edge moves down to
height − 2 too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:58:56 +02:00
JakubandClaude Sonnet 4.6 a7eb0eca7a refactor(ui): trail — settings back in popup, hold-Enter to stop
Per feedback the dedicated Config view added too much navigation. Roll
back to three views (Summary, Map, List) cyclable with LEFT/RIGHT, and
move every knob and action into the Hold-Enter (KEY_CONTEXT_MENU)
popup:

  Min dist: <value>    ← LEFT/RIGHT cycles, popup stays open
  Units: <value>       ← same
  Start/Stop tracking  ← Enter executes, popup closes
  Reset trail          ← Enter executes, only listed when not empty

Short Enter only starts a stopped trail; on an active trail it shows
"Hold Enter for menu" instead of stopping. Stop is therefore reachable
only through the popup, so a stray tap can never end a recording.

PopupMenu lacks LEFT/RIGHT semantics, so the screen intercepts those
keys when the focused row is a settings row, refreshes the label
in-place (same buffer pointer the menu already holds), and keeps the
popup open. Enter on a settings row re-opens the popup with focus
preserved on that row so the user can keep cycling with Enter too if
they prefer. Settings edits batch in _cfg_dirty and commit once on
popup close, Start, or Back.

Hint line shows current view N/3, the short-Enter action ("start" or
"act" depending on state), and a [Hold]menu reminder. Separator above
the hint keeps the row clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:37:43 +02:00
JakubandClaude Sonnet 4.6 c07d7fed50 refactor(ui): trail — 4 views always cyclable, hint sep, action popup w/ Reset
User feedback drove three changes:

1. Browsing after stop. The old V_CONFIG-snap pinned the user to the
   settings screen when tracking ended; you could see the map only while
   active. Drop the snap: Summary/Map/List are reachable from Config
   and from each other at any time, LEFT/RIGHT cycles all four. Enter
   from any view toggles tracking; the alert reads "Tracking
   started/stopped" / "Waiting for GPS fix" as before. Hint shows
   `<>N/4` so the user knows which view they're on.

2. Hint overlap. A 1-px horizontal separator sits two pixels above the
   hint line; Summary, List and Map are sized to leave that band clear
   (`avail` shrinks by an extra 2 px; the map's bottom moves up by 2).
   No more text touching the hint on OLED.

3. KEY_CONTEXT_MENU (Hold Enter) splits off from KEY_CANCEL and now
   opens an action popup instead of going back to Tools. Cancel keeps
   the back-to-Tools role. First action available: "Reset trail" (only
   when the ring isn't empty). Phase 4 will add Save / Load / Export
   GPX to the same menu.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:29:00 +02:00
JakubandClaude Sonnet 4.6 e6d9b8449b feat(ui): trail map — small N arrow in the top-right corner
The projection is already north-up (positive latitude maps to smaller
y), but until now nothing on the screen confirmed which way is which.
Add a tiny 5×7 arrowhead + shaft with an "N" label above, anchored in
the top-right of the map area. Drawn after the polyline so it sits
above any path that crosses the corner.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:54:54 +02:00
JakubandClaude Sonnet 4.6 46ac292b36 feat(ui): trail Units setting — km/h, mph, min/km, min/mi
Second row in the Config view: UP/DOWN moves focus between Min dist
and Units, LEFT/RIGHT cycles the focused row's value. Units propagates
to the Summary view, which now shows either "Avg: N km/h" / "N mph"
or "Pace: M:SS /km" / "/mi" depending on the choice.

Pace renders as "--:-- /unit" when distance or elapsed time is zero
(avoids divide-by-zero glitches with one-point trails). mph conversion
uses the 0.621371 ratio with rounding; pace works in floating point
off raw metres and seconds for accuracy at low speeds.

NodePrefs gains trail_units_idx as another reserved-ish byte; old saves
load with 0 = km/h, no schema bump needed since the rd() lambda already
defaults missing fields to zero.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:16:02 +02:00
JakubandClaude Sonnet 4.6 b0aff8ecf9 refactor(ui): trail Min-dist on a Config screen, not a popup
The Hold-Enter popup was inconsistent with how every other Tool screen
(Auto-Advert, Ringtone Editor, etc.) presents its settings. Reshape
the entry point:

- New V_CONFIG view is the resting screen. Shows "Min dist: <value>",
  the stored points count and distance (so the user can see what
  pressing Enter will resume), and a hint "<>dist [Ent] start/resume".
  LEFT/RIGHT cycles min-dist; Enter saves any pending change and
  starts/resumes tracking. Tracks open straight into V_SUMMARY.
- The three live views (Summary, Map, List) are only reachable while
  tracking. Enter on any of them stops and drops back to V_CONFIG;
  LEFT/RIGHT cycles between the three.
- Esc/Hold from any view writes the pending min-dist change (if any)
  and returns to Tools.

Popup state (_opts_open / _opts_dirty / renderOptions) is gone, along
with all writes-on-every-cycle. Min-dist now batches in _cfg_dirty and
commits once on Start or Back.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:08:27 +02:00
JakubandClaude Sonnet 4.6 08c09163ca refactor(ui): move trail Min-dist setting out of Settings into the tool itself
Per-tool config belongs in the tool, matching Auto-Advert's pattern.
SettingsScreen drops:
- TRAIL_MIN_DELTA enum entry + the entire SECTION_GPS gate (it was its
  only remaining item after GPS upd / Trail int were removed)
- Render and input handler blocks

TrailScreen gains a small modal popup opened with Hold Enter (the
KEY_CONTEXT_MENU input). Enter cycles through the four min-dist values
(5/10/25/100 m), Esc closes the popup. The popup overlays whichever
view is active; savePrefs runs on each cycle so the value persists.

NodePrefs::trail_min_delta_idx and the TrailStore::MIN_DELTA_* helpers
are unchanged — only the entry point shifted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:54:51 +02:00
JakubandClaude Sonnet 4.6 178a34d1ec fix(ui): trail elapsed/avg-speed driven by millis(), not RTC
The previous implementation derived "Time" and "Avg speed" from the
RTC: first().ts and rtc_clock.getCurrentTime(). When the RTC isn't
synced yet (no GPS time fix, no host sync), every point ts is 0, the
current time is 0 too, and elapsed stays at 0 — Time never moves and
Avg speed reads 0 even with the map filling in. Real-world breakage.

Switch the elapsed/avg-speed timebase to millis():
- TrailStore gains _session_start_ms (millis() when the current
  start→stop window opened) and _accumulated_ms (banked time across
  previous windows).
- setActive(true) records the start; setActive(false) banks the
  delta into the accumulator.
- elapsedSeconds = (_accumulated_ms + active session delta) / 1000.
- clear() zeroes both.

Per-point ts stays RTC-derived (used for the HH:MM labels in List view
where a wallclock is what the user actually wants).

TrailScreen drops the now_ts argument it was passing in, and bumps the
refresh interval to 1 s while active so the m:ss counter actually
ticks every second.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:51:37 +02:00
JakubandClaude Sonnet 4.6 4eb4e78a3f feat(ui): trail map — filled/open dot markers around segment breaks
A 3×3 filled square is drawn at the last point of each segment before
a break, and a 3×3 outline square at the first point after the break.
The user can now see where tracking paused and resumed at a glance,
without confusing the gap with sparse sampling.

Start (`+`) and current (`×`) markers unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:41:09 +02:00
JakubandClaude Sonnet 4.6 b12d5a8705 feat(ui): trail — surface "waiting for GPS fix" state on start
Two visible cues when the user starts tracking without a fix:

1. The Enter alert now reads "Waiting for GPS fix" instead of
   "Tracking started" when the LocationProvider isn't valid at the
   moment of toggling. Tracking is still armed — sampling starts as
   soon as the GPS lock comes up.

2. The Summary Status field shows "waiting fix" while active and the
   ring is still empty (no point ever sampled). It flips to "tracking"
   once the first point lands. The detection is "any point recorded
   yet?" rather than "is the fix valid right this second", which is
   stable across the cadence of UITask::loop sampling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:26:33 +02:00
JakubandClaude Sonnet 4.6 0c4e2e3f47 feat(ui): Trail phase 3 — per-point list view
Third view in TrailScreen, reached by LEFT/RIGHT from Summary/Map.
Newest-first, each row shows `HH:MM  +N m` (or `+N.N km` past 1 km).
The first point of each segment shows `start` instead of a delta —
matches the map renderer's segment-aware drawing.

UP/DOWN scrolls within the list; ^/v arrows on the right edge appear
when more rows hide above/below. Time is local (tz_offset_hours
applied via gmtime on (ts + offset)).

Empty trail shows "No trail yet" centred in the content area.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:14:14 +02:00
JakubandClaude Sonnet 4.6 7f0985c63f fix(ui): trail sampling, avg speed, RTC elapsed, segment-aware map
Test feedback revealed four issues:

1. Sampling too sparse — defaults were 60 s interval + 25 m min-delta,
   so a walking pace dropped ~80% of samples. New defaults: 30 s + 5 m.
   Interval options expanded to { 30, 10, 20, 60, 300, 900 } s and
   min-delta to { 5, 10, 25, 100 } m so the user can dial it further
   from Settings (phase 5).

2. "Speed" was current-from-last-pair, misleading next to a Time field
   that grows monotonically. Switch to avgSpeedKmh = total / elapsed,
   labeled "Avg speed".

3. Time advanced only when a new sample landed (it used
   last().ts - first().ts). Now elapsedSeconds takes an optional
   now_ts and uses it whenever the trail is active, so the Time field
   in Summary ticks every render cycle. Sub-1h shown as m:ss for
   visible seconds.

4. Stop → start drew a straight line across the dead time. TrailPoint
   gains a flags byte; addPoint flags the first point and the first
   point after a re-arm as SEG_START. The map renderer skips the line
   from the predecessor for SEG_START points (still draws the start
   pixel). totalDistanceMeters also skips segment boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:10:03 +02:00
JakubandClaude Sonnet 4.6 f50859b4ac fix(ui): scrollable Summary in TrailScreen so OLED hint stops overlapping
OLED (64 px height) couldn't fit 5 summary items plus the bottom hint
row — Time/Speed lines overlapped the [Ent]/[Esc] hint. Convert Summary
to a scrolling list: visible-count derived from the available area
(height - listStart - hint - margin) divided by lineStep, scroll arrows
"^"/"v" on the right edge when more items hide above/below. UP/DOWN
scroll within Summary; LEFT/RIGHT still switch views.

E-ink portrait and OLED both benefit; e-ink landscape fits all 5 items
without ever showing the arrows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:23:06 +02:00
JakubandClaude Sonnet 4.6 984f348898 feat(ui): Trail phase 2 — auto-fit map view + view switching
LEFT/RIGHT cycles between Summary (existing) and the new Map view.
Pixel-by-pixel polyline auto-fits the bounding box of the points to
the visible area, with longitude scaled by cos(avg_lat) so trails at
higher latitudes don't smear east/west. Picks the limiting axis and
centres the non-limiting dimension, so the trail fills as much screen
as possible without distorting.

Markers:
- Start point: a 5×5 plus sign
- Current/last point: a 5×5 diagonal cross

Edge cases:
- Empty trail → "No trail yet" centred
- Single point or all samples colocated → single current marker at centre
- Degenerate area_w/h (very small screen) → no draw

Lines are Bresenham via 1×1 fillRect — a few-tens-of-ms render which is
fine for an on-demand screen. A drawPixel override on the two drivers
would speed this up, deferred until the cost becomes visible.

Bottom hint shows `<>` for view nav, "N/2" current view, and the
Enter-toggle label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:13:52 +02:00
JakubandClaude Sonnet 4.6 ae783edc47 refactor(ui): rename "Breadcrumb" to "Trail" across the feature
User-facing label change requested for the GPS trail viewer. Carries
through internally for consistency:
- Files: Breadcrumb.h → Trail.h, BreadcrumbScreen.h → TrailScreen.h
- Types: BreadcrumbPoint/Store/Screen → Trail*
- NodePrefs fields: breadcrumb_*_idx → trail_*_idx (same byte layout, no
  sentinel bump needed)
- UITask: _breadcrumb, breadcrumb_screen, gotoBreadcrumbScreen,
  _next_breadcrumb_sample_ms → trail-prefixed equivalents
- Tools menu label "Breadcrumb" → "Trail", screen title "TRAIL"

Status bar indicator stays "G" — semantics ("GPS logging active") match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:10:40 +02:00