Commit Graph

3094 Commits

Author SHA1 Message Date
Jakub
2e0263bdf9 fix(ui): home pages — append missing pages to stale page_order on load
ensurePageOrderInit returned early after inserting FAVOURITES without
checking whether TOOLS/QUICK_MSG were present. Old page_order saves that
predated those pages had CLOCK+FAVOURITES but no Messages entry, so
movePageInOrder always found cur==-1 and silently did nothing.

Now recount valid entries after any insertion, then walk the full list of
required page bits and append any that are absent (if room exists).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:08:01 +02:00
Jakub
439c726f1e feat(ui): v1.13 — collapsible Settings, Favourites Dial, GPX trail, schema fix
- SettingsScreen: collapsible sections (Enter toggles); all collapsed by default;
  vis[] filtered list drives render and UP/DOWN/Enter navigation
- NodePrefs: bump SCHEMA_SENTINEL 0xC0DE0003 → 0xC0DE0004; reset trail_units_idx
  in mismatch handler (was corrupted on upgrade from 0003 saves)
- UITask: shorten "No contacts available" → "No fav contacts" (OLED alert overflow)
- README: document Favourites Dial, GPS Trail + GPX download instructions,
  Mark-all-read, collapsible Settings; update USB/BLE export note

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
v1.13
2026-05-26 15:49:50 +02:00
Jakub
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
Jakub
cea3c30f31 fix(build): trail — :: prefixes on DataStore's openWrite + Trail.h gmtime
Adding the public DataStore::openWrite(const char*) member shadowed
the file-static openWrite(FILESYSTEM*, const char*) helper inside
DataStore.cpp's method bodies, so every internal call broke ("no
matching function" with two args). Qualify each internal call with
::openWrite so name lookup picks the static helper. The new public
member's own implementation already does that.

Trail.h's gpxPoint template called gmtime unqualified; ADL couldn't
find it from the template definition because no argument was
template-dependent. Include <time.h> and call ::gmtime — both passes
the dependency rule and avoids the -fpermissive-only acceptance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 08:05:05 +02:00
Jakub
9fe37d39ce docs(features): trail — note "Export saved" path for flash trails
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 08:02:41 +02:00
Jakub
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
Jakub
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
Jakub
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
Jakub
39f055ab45 docs(features): mark trail phase 4 done (Save/Load/GPX)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 07:49:34 +02:00
Jakub
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
Jakub
bb33a23543 refactor(ui): disable GPS toggle in sensors page 2026-05-25 19:26:02 +02:00
Jakub
ad4a4ccba8 docs(features): trail — N/3 in title, hint row dropped, popup-only toggle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:59:15 +02:00
Jakub
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
Jakub
652e1f0979 docs(features): trail — settings + actions in single Hold-Enter popup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:38:06 +02:00
Jakub
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
Jakub
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
Jakub
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
Jakub
65984d47a5 docs(features): mark favourites badge overlap fixed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:30:31 +02:00
Jakub
0d1fd345a8 fix(ui): favourites tile — reserve room for unread badge, share baseline with name
The badge was rendered at cy + 1 while the name centered around cy + ~8;
on OLED with line_h = 8 the two ended up touching, and the name's
drawTextEllipsized used the full cell width so any "..." would land
under the digit anyway.

Compute the badge width up-front, subtract it (plus a 3 px gap) from
the name's max width, and render both on the same baseline (name_y).
The name now shortens to "Nam…" before the badge instead of running
through it. E-ink with its wider tiles already had room, so this is a
strict improvement there too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:30:12 +02:00
Jakub
8c8f60391c docs(features): trail — add Units to Config view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:17:17 +02:00
Jakub
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
Jakub
72fbbf2400 docs(features): trail — config-screen entry point replaces popup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:08:46 +02:00
Jakub
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
Jakub
e32f0ba34d fix(build): drop SECTION_GPS references after the GPS section was removed
Earlier commits deleted the SECTION_GPS enum entry along with its only
remaining child (Trail dist moved to the Trail tool), but isSection()
and sectionName() still referenced it under the ENV_INCLUDE_GPS guard.
Both helpers no longer need a GPS case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:01:06 +02:00
Jakub
d55ae5c2c5 fix(ui): home-page nav buffer must size to HomePage::Count after FAVOURITES
Both call sites of buildVisibleOrder wrote into a fixed-size 11-int
stack buffer. After FAVOURITES landed, HomePage::Count became 12; in
the common case where all pages are visible, buildVisibleOrder writes
12 entries — one past the end. That OOB write trashed whatever lived
next on the stack (typically the loop variable in navPage), so LEFT
wrap-around behaved randomly: sometimes locked at the first page,
sometimes jumped to a wrong one. User-visible as "menu doesn't wrap
to the left".

Size the buffer to `(int)Count` so it tracks the enum and grows with
any future home page additions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:59:32 +02:00
Jakub
183abcba32 docs(features): trail — min-dist now tool-local, millis-based elapsed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:55:15 +02:00
Jakub
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
Jakub
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
Jakub
237dcbf524 feat(ui): bump TrailStore capacity 256 → 512
8.5 min of raw 1 s samples (vs 4.25 min) — with the typical 5 m
min-delta gate that's ~30 min of walking or ~10 min of cycling worth
of points before the ring wraps. Cost is one 16-byte TrailPoint per
slot = +4 KB RAM; nRF52840 build was at 64.7 % so ample headroom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:50:51 +02:00
Jakub
d227670059 docs(features): update trail phase 5 — interval removed, 1 s fixed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:48:23 +02:00
Jakub
b574d7829a feat(ui): trail — fixed 1 s sampling; drop GPS upd + Trail int from settings
GPS NMEA polling is cheap, and a slower cadence just hides motion from
the trail; the min-delta gate already filters jitter without throttling
the GPS. Both knobs that exposed cadence-style controls to the user are
removed:

- Trail sampling is fixed at TrailStore::SAMPLING_SECS = 1 s, matching
  the sensor manager's built-in GPS update default. The
  intervalSecs / intervalLabel / INTERVAL_COUNT helpers go with it.
- GPS update rate ("GPS upd" entry) leaves the sensor manager on its
  built-in 1 s default. applyGPSInterval and the GPS_INTERVAL_OPTS /
  LABELS tables / gpsIntervalIndex helper are deleted.

Both NodePrefs fields (gps_interval, trail_interval_idx) are retained
as reserved so the schema sentinel doesn't have to bump — older saves
load cleanly, the bytes are just ignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:48:03 +02:00
Jakub
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
Jakub
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
Jakub
2538751c27 docs(features): mark trail phase 5 done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:20:02 +02:00
Jakub
30b81b6d51 feat(ui): Trail phase 5 — Settings › GPS gains trail interval + min-delta
Two new entries in the existing GPS section of SettingsScreen, gated by
the same ENV_INCLUDE_GPS guard. LEFT/RIGHT cycles through the option
table from Trail.h:
- "Trail int" : 30 s / 10 s / 20 s / 1 min / 5 min / 15 min
- "Trail dist": 5 m / 10 m / 25 m / 100 m

Changes are stored to p->trail_interval_idx / trail_min_delta_idx and
take effect on the next sampling tick in UITask::loop (already gated on
those fields), so no separate apply* helper is needed. _dirty triggers
the normal savePrefs on settings exit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:19:47 +02:00
Jakub
81766650cc docs(features): note OLED Favourites name/badge overlap as follow-up
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:17:03 +02:00
Jakub
0a11d54b61 docs(features): mark trail phase 3 done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:14:39 +02:00
Jakub
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
Jakub
26278e433d docs(features): note trail phase 2 follow-ups (scroll, blink, defaults, segments)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:10:57 +02:00
Jakub
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
Jakub
9d038730df fix(ui): force 1 s home-refresh while trail tracking, matching auto-advert
The status-bar G/A indicators both flip every 2 s (millis() % 4000 <
2000), but with only auto-advert forcing the 1 s refresh, a screen with
G alone repainted every 5 s — the blink looked sparse vs A. Treat any
blinking indicator (auto_adv OR trail active) as needing the 1 s
cadence on OLED.

E-ink builds keep the 30 s home refresh — blink is gated to "always on"
there via Features::BLINK_INDICATORS = false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:24:36 +02:00
Jakub
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
Jakub
2ff27a7178 docs(features): rename Breadcrumb section to Trail, mark phase 2 done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 09:14:39 +02:00
Jakub
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
Jakub
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
Jakub
adef2fceb9 feat(ui): GPS breadcrumb phase 1 — storage, sampling, Summary view
Adds Tools › Breadcrumb. Enter on the screen toggles tracking; while
active, UITask::loop samples GPS at the cadence chosen in settings
(default 1 min) and drops the point into a 256-entry RAM ring guarded
by a min-distance gate (default 25 m). A "G" indicator joins "A" in
the status bar with the same blink convention.

New storage:
- examples/companion_radio/Breadcrumb.h — BreadcrumbStore class
- 256 × 12 B = 3 KB RAM (survives auto-off, lost on reboot — explicit
  flash-slot save comes in phase 4)
- Haversine min-delta gate, total distance, elapsed seconds, current
  km/h, bounding-box helper

New screen:
- examples/companion_radio/ui-new/BreadcrumbScreen.h — Summary view
  with Status, Points, Dist (m or km), Time (h:mm), Speed; Enter
  toggles tracking, Esc returns to Tools

Schema bump 0xC0DE0002 → 0xC0DE0003 for two new NodePrefs bytes:
breadcrumb_interval_idx (1min default) and breadcrumb_min_delta_idx
(25m default). Older saves leave both at zero, which matches the new
defaults. DataStore::loadPrefsInt/savePrefs read/write the pair.

Logging on/off is a runtime flag inside BreadcrumbStore — not a
preference — so reboot returns to the off state cleanly; the user
will be able to persist a snapshot to a slot in phase 4.

Phase 2 (map view), 3 (point list), 4 (slot save/load, GPX export over
USB), and 5 (settings UI) follow incrementally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:22:12 +02:00
Jakub
ccf45f0c0c docs(features): Favourites dial complete (all 3 phases)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:08:32 +02:00
Jakub
e1cbd959b2 feat(ui): Favourites phase 3 — in-place pin picker on empty tile
Enter on an empty Favourites slot opens a PopupMenu listing candidate
contacts. Sourced first from upstream-favourited chat contacts (those
flagged with c.flags & 0x01), then from recent DM contacts (resolved
from the 4-byte _dm_hist prefix to a full 6-byte pub_key via the contact
list), deduped against the favourites list. Capped at 12 entries.

Selection writes the chosen 6-byte prefix into the target slot; if the
contact is already pinned elsewhere, the previous slot is vacated first
(one contact per dial, per the agreed pin-only semantics). savePrefs
commits to flash and a brief alert confirms.

Plumbing:
- New QuickMsgScreen::getRecentDMContacts() resolves recent DM senders
  to 6-byte pub_key prefixes, deduped, newest first
- UITask::getRecentDMContacts() wraps it so HomeScreen doesn't reach
  into the QuickMsg screen directly
- HomeScreen gains its own PopupMenu + per-entry pub_key / label buffers
  (12 × 6 B keys + 12 × 22 B labels), no shared state with QuickMsg's
  Contact-options menu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:08:12 +02:00
Jakub
c5faaf0afe docs(features): Favourites phase 2 complete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:00:52 +02:00
Jakub
221885373b fix(ui): Cancel from Favourites-opened DM returns home, not to contact list
QuickMsgScreen::enterDM (used by openContactDM from the Favourites dial)
now sets _dm_direct_entry so KEY_CANCEL in DM_HIST jumps back to the
home screen instead of falling through CONTACT_PICK → MODE_SELECT. The
flag clears on every reset() and after the cancel returns.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 07:58:45 +02:00