Commit Graph

1237 Commits

Author SHA1 Message Date
MarekZegare4
c917bf59fd fix(companion): atomic save for contacts/channels/prefs
saveContacts/saveChannels/savePrefs truncated the live file up front and
then wrote in place, so a crash, reset or full flash mid-save wiped the
whole file — observed as all contacts disappearing after a UI hang.

Write to a temp file and only swap it in (via LittleFS atomic rename) once
every record has written cleanly; on any failure keep the previous good
file and drop the temp. savePrefs gates the swap on the tail-sentinel write
succeeding (writes return 0 once flash fills, so a good sentinel means the
whole record fit). Contacts/channels live on the multi-MB QSPIFlash, so the
transient second copy fits easily.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:39:46 +02:00
MarekZegare4
e5887f1d01 feat(companion): highlight the active target on the map
Setting a Locator/Nav target had no spatial feedback — you could arm a
destination but never see where it is. Draw it as a flag marker (ICON_MAP_TARGET)
on both the home Map preview and the full Trail map, folded into each view's
bounding box so a destination you set never sits off-frame, and drawn last so it
stays legible atop a coincident waypoint or contact.

activeTargetPos() resolves the target once (waypoint coords, or a person via
resolvePersonPos), gated only on a target being set — independent of whether the
Locator alert is enabled — so it doubles as the engine's resolver
(locatorDistance now delegates to it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:56:45 +02:00
MarekZegare4
83d139a62d refactor(companion): unify the active target across Locator/Nav entry points
Three notions of "set a target" had drifted apart: LocatorScreen's picker
hand-wrote the prefs fields and deferred the save, while the Nearby/Waypoints
popups wrote the same fields again and saved immediately, and the live>last-
advertised position precedence was duplicated between the engine and the picker.

- setTarget() is now the single definition of the active target (fields +
  re-arm). setTargetNow() wraps it for popups (immediate save + confirm); the
  Locator screen keeps its deferred save so LEFT/RIGHT cycling doesn't thrash
  flash. applyTarget() delegates instead of re-writing the fields.
- resolvePersonPos() is the single live-share / last-advertised precedence,
  used by both locatorDistance() and the picker.
- Consistent action verb "Set as target" in Nearby and Waypoints menus.

No behaviour change for the user beyond the relabel; pure consolidation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:34:53 +02:00
MarekZegare4
f481e3669f feat(ui): add a status-bar indicator for active Live Share
Live Share was the only "leave it on and forget" broadcast mode with no
at-a-glance cue, unlike auto-advert/trail/repeater which all blink in the top
bar. Reuses the diamond already used for a live-tracked contact on the map,
so the glyph already means "sharing position" elsewhere in the UI. Also
fixes the Tools-list "Live Share" row to use ICON_GPS instead of ICON_ADVERT
(which collided with the Auto-Advert row) — currently inert since per-row
icons are disabled there, but correct for whenever they're re-enabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 17:08:03 +02:00
MarekZegare4
fee2d32bd3 perf(companion): stop Locator target scan once TARGET_MAX is reached
buildTargets() kept iterating every contact after _targets filled up, doing a
lookup + dedup pass per row with no effect (addPersonTarget already caps at
TARGET_MAX). Bound the loop instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:01:55 +02:00
MarekZegare4
a67bf9e220 fix(companion): bound trail simplification error on gentle curves + live distance
The streaming simplifier tested the previous candidate against a line that
re-aimed at the newest sample each step. That test point always sits beside
the moving chord endpoint, where cross-track is ~0, so a long gentle curve
slipped through one sub-min_delta step at a time and collapsed into a single
chord deviating arbitrarily far from the real track.

Switch to a fixed-corridor (Reumann-Witkam) test: anchor the line at the last
committed vertex aimed at the first sample of the run (_dir), and test each
*newest* sample against that fixed direction. Deviation is now bounded to
~min_delta, while a straight road still costs only its two endpoints.

Also count the uncommitted candidate (_pending) in totalDistanceMeters() so
the live distance/speed readout no longer stalls across a long straight run,
where the whole stretch is held as one pending point until a bend commits it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 17:01:50 +02:00
MarekZegare4
0f475969c7 feat(companion): stream-simplify GPS trail points on straight runs
addPoint() now holds the latest accepted sample as a pending candidate
instead of committing it immediately. It only becomes a stored vertex once a
later sample proves it was a real bend (cross-track deviation from the last
vertex exceeds the min-delta tolerance) — so a straight stretch costs just
its two endpoints no matter how long it is, while turns still get a vertex
roughly every min-delta of deviation. This stretches the 1024-point buffer
over much longer routes without losing shape on curves.

The pending candidate is flushed before any segment break (setActive(false),
setPaused(true), writeTo()) so the last stretch of a run is never silently
dropped just because no bend forced a commit; readFrom()/clear() drop a
leftover candidate that belonged to the session being replaced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 16:27:15 +02:00
MarekZegare4
4f175bda3c feat(companion): double the GPS trail buffer (512 -> 1024 points)
RAM headroom allows it: +8KB resident (TrailStore::_buf), pushing static RAM
to 73.0%/75.0% on GAT562/WioTrackerL1Eink respectively. On-disk snapshot
format is unaffected (count is a uint16_t, already asserted to fit far past
this); an old 512-point snapshot still loads fine on this firmware.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 16:18:45 +02:00
MarekZegare4
b7e7e945d2 feat(companion): widen Locator targeting beyond favourites/live-share
Locator's person target now resolves any known contact's position, not just
an active live [LOC] share: locatorDistance() falls back to the contact's
last-advertised gps_lat/gps_lon when there's no current live share, so a
rarely-updating but stationary node (a repeater, or someone who shared a fix
once) still works as a target. LocatorScreen's target picker is widened to
match (favourites always offered, plus every other contact with a currently
resolvable position), and shows a compact age tag for non-live entries so
staleness is visible.

Also adds a quick "Set Locator target" action directly from Nearby Nodes' and
Waypoints' own popup menus (UITask::setLocatorTarget()), so picking a target
doesn't require a detour through Tools > Locator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 16:18:39 +02:00
MarekZegare4
57f5346427 refactor: rename Geo Alert feature to Locator
Pure rename, no behavior change: GeoAlertScreen -> LocatorScreen (file +
class), NodePrefs geo_alert_* fields -> locator_*, UITask engine functions
(evaluateGeoAlert/fireGeoAlert/geoAlertDistance/geoProximityBeeper) and state
-> evaluateLocator/fireLocator/locatorDistance/locatorProximityBeeper, Tools
menu entry and screen header text -> "Locator", plus matching docs/README
updates. NodePrefs field renames don't affect on-disk layout (DataStore
serializes by position, not name).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 23:37:30 +02:00
MarekZegare4
3c94aca602 feat(ui): show nearest-tracked-contact distance as an arrow icon on the map status line
Replaces the bottom-left "->dist" text inside the map preview with a proper
arrow mini-icon, moved onto the status line alongside the tracked-node count
(e.g. "Track:3 ->120m"). The map's bottom-left corner now always shows the
scale tick instead of conditionally falling back to it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 15:37:57 +02:00
MarekZegare4
cc68f77031 fix(companion): null-guard geo-alert picker, clamp loc-share channel idx on load
GeoAlertScreen::openPicker() is reachable directly from handleInput()'s
Target row, bypassing the _prefs null-check every other path in this
screen goes through via activate(). DataStore's loc_share_channel_idx
was the only loc_share_* field without a clamp-on-load, unlike its
sibling index fields (harmless today since getChannel() bounds-checks
it, but inconsistent with the established defensive pattern).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 12:05:36 +02:00
MarekZegare4
4c37bc1b5d Merge remote-tracking branch 'origin/main' into feat/location-beacon-alerts-autopause 2026-06-23 12:02:24 +02:00
MarekZegare4
02cfb9da86 fix(ui): hide GPS status icon when GPS setting is disabled
The status-bar GPS icon appeared whenever the board had GPS hardware,
even if the user had turned GPS off in settings, implying a search
that wasn't actually happening.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 11:24:06 +02:00
MarekZegare4
687d126334 fix: bound Serial.write() to prevent indefinite hang on stalled USB host
Adafruit_USBD_CDC::write() blocks as long as DTR is asserted, even if
the host stopped draining the FIFO. Every writeFrame() call and the
GPX trail export wrote directly to Serial, so a serial monitor left
open but idle could stall the main loop until the 30s watchdog reset
the device.

streamWriteUntil() caps the wait with an absolute deadline shared
across chained writes, and only enters the wait loop when
availableForWrite() is actually exhausted, so a healthy host sees no
behavior change. The GPX export latches after the first stall to
avoid paying the timeout once per write call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 11:24:02 +02:00
MarekZegare4
5bb3111bb0 feat(ui): GPS fix icon on status bar, minimap distance label, hide Tools icons
- Status bar: add a boxed/plain GPS reticle icon reflecting LocationProvider
  fix state, shown only on boards that have a location provider. Drop the
  now-redundant "Fix:y/n" text from the map page's info line.
- Home minimap: replace the bottom-left scale bar with a "->dist" label to
  the nearest live-tracked point when one is available, falling back to a
  smaller single-tick scale indicator otherwise. Less likely to be covered
  by a plotted point than the old full-width bar, and more useful at a
  glance. Drop the now-duplicate nearest-distance text from the map page's
  info line.
- Tools screen: comment out per-row/section icons (kept for easy restore) —
  they don't fit visually in that list for now.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 10:37:46 +02:00
MarekZegare4
79ebad58ae feat(companion): parse [LOC] in room messages + wire roomMessage event
Two receive→UI wiring gaps of the same class as the room-history fix:

- onSignedMessageRecv (room posts) didn't parse [LOC] live-location shares,
  while DM (onMessageRecv) and channel (onChannelMessageRecv) both do — so a
  guest sharing position in a room never appeared on the map/nearby. Resolve
  the signed sender prefix to a name and track by name, mirroring the channel
  path (unverified: only a 4-byte prefix is available here).

- UIEventType::roomMessage was defined but never emitted; room notifications
  rode on contactMessage and always played the global DM melody. Emit
  roomMessage for room posts and handle it explicitly (default DM melody, no
  per-sender lookup since the author varies); drop the now-duplicate dead case.

Build verified: WioTrackerL1 solo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:28:37 +02:00
MarekZegare4
c935287627 fix(companion): reset NaN/inf radio params before constrain() on load
constrain() is a min/max macro and NaN compares false against both bounds,
so a corrupted or layout-shifted prefs file that decodes a float field as
NaN/inf would pass it straight through to setParams() and could hang the
radio at boot (stuck after "Loading...", recoverable only by erasing flash).
Reset freq/bw/airtime_factor/rx_delay_base to safe defaults before the
existing constrain() so a non-finite value can never reach the driver.

Note: a defensive hardening of the boot path, not a confirmed root cause for
the reported brick.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:03:19 +02:00
MarekZegare4
8adc313cc5 feat(home): page-indicator icons instead of dots
Replace the home carousel's page-indicator dots with a small per-page glyph
set (uniform 5x5, smaller than the menu/status icons), the current page
underlined rather than inverted (an inverted box read too much like the top
status bar). Icons follow buildVisibleOrder, so custom page order and
hide/show toggles from Settings carry over automatically; the row pitch
shrinks to fit when many pages are enabled, and the band geometry scales with
the font (identical layout to the old dots at 1x, clears header/content on 2x
e-ink).

Adds 13 ICON_PG_* glyphs (clock, star, recent, radio, bluetooth, advert,
gps, sensors, settings, map, tools, msg, power).

Builds verified: WioTrackerL1 + e-ink solo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 19:56:38 +02:00
MarekZegare4
a10b4c3123 Merge branch 'main' into feat/location-beacon-alerts-autopause 2026-06-22 18:33:57 +02:00
MarekZegare4
9b4c8431c9 fix(companion): show room-server messages on device, attributed to sender
Incoming room-server posts fired the notification and reached the app via
the offline queue, but never appeared when the room was opened directly on
the device: the on-device history (addDMMsg) was gated to ADV_TYPE_CHAT only,
while rooms (ADV_TYPE_ROOM) share that same history list (keyed by the
server's pubkey). Include ADV_TYPE_ROOM so room posts are stored too.

A room carries many guests, so prefix each stored post with its author —
resolved from the signed message's sender pubkey prefix (fallback: short
hex) — and have the DM history view split "Sender: text" for room contacts
so each line is attributed to the guest who wrote it, instead of showing the
room server's name for every message.

Builds verified: WioTrackerL1 + GAT562 30S solo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:31:47 +02:00
MarekZegare4
b8760f7e41 feat(ui): collapsible Tools, shared AccordionList helper
The flat 10-item Tools list grew crowded, so tools are now grouped into
collapsible sections (Location / Comms / System) that fold in place — the
same model as Settings.

Extracted that fold-in-place behaviour into a reusable AccordionList helper
(owns the collapse mask, flattened visible-row list, selection and scroll;
host supplies section sizes + two row painters) and migrated SettingsScreen
onto it, dropping its hand-rolled buildVis/visIndexOf/sectionIndex and five
state fields. AccordionList joins drawList/DigitEditor/NavView as a shared
UI element used by two screens.

Added four mini-icons (bot, note, chart, gear) so every Tools row and
section header carries a glyph.

Builds verified: WioTrackerL1 + e-ink solo (Flash 61.0%).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 18:13:33 +02:00
MarekZegare4
f49b7dd8bd fix(live-share): name the sender in DM position shares
A channel [LOC] broadcast already carries the sender (sendGroupMessage
prepends "<name>: "), but the DM path sent a bare "[LOC]<lat>,<lon>" with
no name in it. Embed the node name as a trailing token after the
coordinate so a DM share is self-describing in any chat client;
parseLocShare ignores the trailing text, so receivers are unaffected.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:30:44 +02:00
MarekZegare4
df894eba16 fix(map): label live contacts before waypoints so the person's name shows
The label collision-avoidance reserved waypoint label slots first, so a
tracked contact's name was dropped whenever it sat near a waypoint —
leaving only the waypoint label. Label live-tracked contacts first: on a
live map the moving person's name should win the slot over a nearby static
waypoint.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:25:09 +02:00
MarekZegare4
3ffdfd64b6 feat(map): north + scale on home mini-map; avoid overlapping labels
Home Map page mini-preview gains a north mini-icon and a fillRect scale
bar with a round-distance label (metric/imperial), and its status line now
shows the distance to the nearest tracked contact alongside fix/track.

Trail map labels no longer smear in dense clusters: each placed label
reserves a rect and a new one tries four offsets around its marker,
skipping (marker still shown) when none fit without overlap.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 14:00:04 +02:00
MarekZegare4
92d4a7ae16 fix(nearby): refresh distance-sorted list so live shares bubble up
The stored list only re-merged + re-sorted periodically under the "recent"
sort; under distance sort it refreshed only on entry. So a [LOC] share
arriving while the list was open (sorted by distance) wasn't re-sorted and
the now-nearest node never moved to the top. Refresh on a timer under both
sorts, keeping the highlighted node selected across the rebuild. Factor the
identity-preserving reselection into refreshKeepingSelection(), shared by
the list and the detail/nav refresh paths.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:53:39 +02:00
MarekZegare4
440a222e4b feat(geo-alert): pick target from a list (favourites first)
Enter on the Target row now opens a picker instead of only cycling: it
lists favourites first (the quick path), then active verified live
senders, then waypoints — UP/DOWN + Enter to choose. This lets you arm an
alert against a person ahead of time by pubkey, without waiting for them
to share a [LOC] (the previous cycle only offered currently-active live
senders, so a person you hadn't received a fresh share from couldn't be
selected at all). LEFT/RIGHT still quick-cycles the same unified set.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:45:47 +02:00
MarekZegare4
bcb7eb59d5 fix(nearby): live [LOC] senders respect the type filter + sort by shared pos
A position-sharing node that isn't a stored contact was appended to the
Nearby list regardless of the active type filter, so a companion would
show up (and, having a live distance, sort to the top) under the Repeater
/ Room / Sensor filters. Gate the append on the type filter — a non-contact
[LOC] sender is treated as a companion, so it only appears under All/Comp.

Also drop the "share must be newer than the advert" guard when overlaying
a share onto a stored contact: a [LOC] message is an explicit current
position, so it always defines the pin and the distance used for proximity
sorting (recency for the time sort stays the most recent of advert/share).

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:33:18 +02:00
MarekZegare4
0fee3bf92c feat(trail): scroll-editor for add-waypoint coordinates
Replace the text keyboard for the "Add by coords" Lat/Lon fields with the
shared digit-by-digit DigitEditor (the radio-frequency widget): Enter
opens the editor on the magnitude (LEFT/RIGHT move the cursor, UP/DOWN
change the digit), and LEFT/RIGHT on the row still toggles the hemisphere
when the editor is closed. The keyboard never fit numeric entry — no
minus, easy to type stray letters. Label still uses the keyboard. Docs
updated.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:25:24 +02:00
MarekZegare4
16c634bd6e feat(companion): geo-alert beeper overrides buzzer mute
The proximity beeper is its own opt-in toggle, so enabling it is an
explicit "I want to hear this" — drop the isBuzzerQuiet() guard so it
ticks even when the global buzzer is muted (manual Off or BT auto-mute).
The arrive/leave crossing alert has no dedicated toggle and keeps
respecting the mute. Docs updated.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:15:07 +02:00
MarekZegare4
dcc4379f63 feat(companion): follow live contacts — nav, geo-alert, ETA, quick-share
Builds on live location sharing:
- Nearby "Navigate" now follows a moving live target: the navigate view
  re-reads the shared position every second and re-locates the entry by
  name for non-contact senders, so it no longer freezes or drops out
  (previously only refreshed for stored contacts entered via detail).
- NavView gains a closing-speed / ETA line (EtaTracker), shown in the
  Nearby navigate view while approaching.
- Geo Alert target can be a live contact, not just a waypoint: the engine
  re-reads the contact's latest [LOC] position each evaluation (keyed by
  pubkey prefix) so the geofence follows a moving person; alert wording
  switches to Near/Away. The picker cycles waypoints + verified live
  contacts (shown with an '@' prefix). Schema bumped 0xC0DE0014 -> 0x15.
- Nearby marks a position-broadcasting node with the map's ♦ diamond
  (was a cryptic '*'/'~' prefix) and spells out "Sharing pos:" in detail.
- Home Map page Hold-Enter is a one-shot position share: pushes to the
  Live Share target while auto-sharing is on, else opens the picker.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:09:51 +02:00
MarekZegare4
659ecec5a4 refactor(companion): tidy geo-alert / live-share; dedicated auto-pause gate
Review cleanup of the new location features:
- GeoAlertScreen + LiveShareScreen use the shared drawList() helper instead
  of hand-rolling the scroll/clamp/scrollbar maths, matching every other
  list screen.
- Extract UITask::geoAlertDistance() — one place for the target-distance
  maths, shared by the crossing evaluator and the proximity beeper.
- Geo Alert re-seeds its crossing engine after every edit, so changing the
  target/radius while armed can't fire a stale arrive/leave before exit.
- Trail auto-pause gets its own coarse movement gate
  (NodePrefs::TRAIL_AUTOPAUSE_MOVE_M = 15 m) instead of reusing the trail
  min-delta, so GPS jitter while parked doesn't keep the idle timer alive.
- Fix stale include comments.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 12:38:59 +02:00
MarekZegare4
408dfd494b feat(companion): geo-alert geofence + trail auto-pause
Geo Alert (Tools › Geo Alert): a single geofence around a snapshotted
waypoint. Beeps + alerts on crossing into (arrive) / out of (leave) the
radius per mode, with edge hysteresis and silent first-eval seeding. Adds
a proximity Beeper that ticks faster the closer to the target while inside
the radius — independent of the discrete arrive/leave alert.

Trail auto-pause (Trail › Settings): freezes the trail timer + sampling
after the device sits still for a configurable delay, resuming on the next
real movement. New paused state in TrailStore (distinct from Stop); Summary
shows "paused".

Persisted via new NodePrefs fields; schema bumped 0xC0DE0012 → 0xC0DE0014
with clamps so upgraders start with both features off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:45:00 +02:00
MarekZegare4
1d2f2beaba fix(companion): Live Share UX + map labels
- Live Share tool: add scrollbar, drop non-clickable section headers and
  the one-shot "Share now" (keep Auto-share toggle; manual share stays on
  the Map). Ellipsize the "To" target so long names don't wrap.
- Move "Track loc" (receive) from Settings into Live Share.
- Map: 2-char contact labels (distinguishable); drop "press" hint from the
  Home Map page status line so it fits on OLED.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:23:22 +02:00
MarekZegare4
d78e74d4cd feat(companion): live location sharing via [LOC] messages
Share and track positions over the mesh through chat messages, separate
from (and parallel to) the 0-hop auto-advert.

Receive / tracking:
- geo::parseLocShare + [LOC] tag (sibling to [WAY]); LiveTrackStore (16
  slots, DM=pubkey/verified, channel=name/best-effort, 20-min expiry).
- MyMesh parses [LOC] on DM + channel receive → UITask live-track table.
- NearbyScreen overlays shares onto contacts (fresher pin) and lists
  non-contact senders; markers (*=DM, ~=chan) + "Loc:" age/verified line.

Send:
- Map menu "Share my pos" (one-shot) → Messages recipient picker.
- New Tools > Live Share tool: Receive (Track loc) + Send (Auto, Target,
  Move/Min gap/Heartbeat, Share now). Target chosen via the Messages
  picker (channel/DM). Movement-gated auto-send engine in UITask loop.

Map / navigation:
- Trail map shows tracked contacts as ◆ markers (ICON_MAP_CONTACT).
- Home "Map" page: mini-map preview + status line; ENTER opens the tool,
  Cancel returns Home.

Prefs: track_shared_loc + loc_share_* (NodePrefs schema 0xC0DE0012).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:13:09 +02:00
MarekZegare4
29deaaad44 fix(companion): channel save/lookup hardening + trail count guard
Address the open items from the FEATURES.md audit backlog:
- saveChannels() now skips unused slots (all-zero secret) instead of
  writing all 40 every time, so /channels2 holds only configured channels
  (was always ~2.7 KB) and wears the flash less. loadChannels() already
  compacted empties on read, so the loaded result is unchanged.
- findChannelIdx() returns -1 for an all-zero secret, so a corrupted/empty
  channel can't match an unused all-zero slot and misroute messages.
- TrailStore gains a static_assert that CAPACITY fits the uint16_t save
  header count, failing the build instead of silently truncating.

Also re-classify two audit items verified to be non-issues in current
code: the bot strstr "199-char" truncation (BOT_SCRATCH=200 >= MAX_TEXT_LEN
=160, unreachable) and the MSG_PICK reply title (rlen clamped to 20, so
title[24] never overflows).

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:25:11 +02:00
MarekZegare4
a2af1a7409 refactor(ui): share radio params editor between Settings and Repeater
The manual Freq/SF/BW/CR editing was duplicated near-verbatim between
Settings › Radio and Tools › Repeater — same digit-by-digit Freq editor,
same SF/BW/CR step bounds, differing only in the target fields and the
apply call. Extract RadioParamsEditor (freq DigitEditor + static SF/BW/CR
steppers), with no dependency on UITask or the radio driver: the screen
passes the freq bounds in, the same way RadioPresetPicker stays decoupled.
Completes the preset-picker consolidation.

Behaviour preserved: a step at the range limit changes nothing and isn't
consumed (falls through), as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:09:35 +02:00
MarekZegare4
4369a0cc74 fix(repeater): validate profile against chip freq bounds; fix digit-editor padding
isValidRepeaterProfile() hard-coded the 150-960 MHz SX1262 range, which
would wrongly reject legal frequencies on any other chip. Take the freq
bounds as parameters and pass radio_driver.getFreqBounds() at both call
sites (repeaterProfileValid() and the load-time migration), so the chip's
own validated range is the single source of truth. DataStore.cpp gains a
target.h include for radio_driver (declared extern there).

DigitEditor::render() now zero-pads the integer part to int_digits: the
cursor addresses place values (100/10/1/0.1…), so a value with fewer
integer digits than int_digits would shift every glyph and misplace the
highlight. No visual change for the frequency field (always 3 digits).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:09:27 +02:00
MarekZegare4
52e65b960f docs(repeater): rename "politeness" -> "forwarding filters", fix stale refs
Rename the repeater filter knobs from "politeness" to "forwarding
filters" across code comments, FEATURES.md and the docs, and correct
references that hadn't followed the controls when they moved from
Settings › Radio to the dedicated Tools › Repeater screen.

Comment/doc accuracy fixes:
- NodePrefs: user_radio_presets are now written by both Settings and
  Repeater (shared picker), not Settings only.
- DiagnosticsScreen: class header listed only some rows; spell out the
  full set (forwarded, signal, pool/queue, error flags, reset gesture).
- RepeaterScreen header no longer claims to show live forwarding stats
  (they live on Diagnostics).
- tools_screen.md: drop the stale paragraph claiming the Repeater screen
  shows live stats at the bottom — it is config-only.

Comments/docs only; no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:32:59 +02:00
MarekZegare4
0e333aeb5e refactor(ui): share radio preset picker between Settings and Repeater
Settings › Radio and Tools › Repeater had near-identical preset-picker
logic (name/list-index lookup, save/delete, popup build, selection
handling) plus their own copy of the nearest-bandwidth search — only the
target fields (companion params vs. dedicated repeater profile) and the
apply call differed.

Extract the shared logic into RadioPresetPicker (a Target of field
pointers + a Result the screen acts on), with no dependency on UITask so
each screen keeps its own apply/dirty/keyboard/alert handling. Move the
nearest-bandwidth search to nearestBwIndex() in RadioPresets.h. Removes
~6 duplicated methods and 4 state fields from each screen.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:32:47 +02:00
Jakub
a8c7a200dc feat(repeater): save/delete radio profile as a named preset
The Repeater screen's preset picker only let you pick an existing
preset, not save the current profile as a new one or remove a saved
one — the only way to manage user presets was from Settings > Radio,
even though the repeater profile is a separate set of freq/sf/bw/cr
values.

Ports Settings' "+ Save current..." / "- Delete preset..." popup flow
onto the repeater profile fields, reusing the same shared on-screen
keyboard (UITask::keyboard()) and the same 4 NodePrefs::user_radio_presets
slots — a preset saved from either screen shows up in both, since it's
the same underlying array.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:55:10 +02:00
Jakub
32c50d1cfe feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.

Four opt-in politeness knobs (skip-advert, max-hops, yield, min-SNR),
all flood-only and off by default, plus overhear suppression that
cancels a queued retransmit if a peer relays the same packet first.
Adaptive Power Control is suppressed while relaying (pins TX power to
the ceiling) and duty-cycle RX is forced off, since a repeater needs
to hear and relay at consistent power.

A dedicated Tools > Repeater screen consolidates the toggle, the
politeness knobs, and live forwarding stats, plus an optional "Custom"
radio profile — a dedicated frequency/SF/BW/CR for relaying, separate
from the companion's own network, band-matched to the companion
frequency by default and used everywhere a radio change can happen
(boot, on-device toggle, app-driven CMD_SET_RADIO_PARAMS, Settings
radio edits) so the device never silently falls back to the wrong
params mid-relay.

Diagnostics gains the actually-forwarded packet count, real heap-free
via mallinfo(), a reset-counters popup, and hardened loop-detect
bounds. Also: a frequency-floor fix and a float-equality preset-match
fix in the repeater profile logic, deduped BW-table/valCol/profile-
seeding helpers shared between Settings and the Repeater screen, and
a build.sh fix tolerating control characters in `pio project config`'s
JSON dump.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:07:00 +02:00
Jakub
327e659d51 feat(ui): add on-device diagnostics screen (Tools > Diagnostics)
Shows packet counts by category (RX/TX), radio noise floor/RSSI/SNR,
packet-pool free count and outbound queue length, uptime, and heap/stack
headroom on a single scrollable screen.

Adds generic per-payload-type RX/TX counters to Dispatcher (mesh layer),
plus pool-free/queue-length getters, and a new DeviceDiag helper for
nRF52 heap (linker-symbol + sbrk) and stack (FreeRTOS high-water-mark)
stats — the only platform needed for the 3 in-scope boards (Wio Tracker
L1 OLED/Eink, GAT562 30S), all nRF52840.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:34:52 +02:00
Jakub
99cbe51797 fix(ui): popup selection bar stops before the scroll-indicator gutter
Matches every other list (drawList's row width - reserve): the highlight
shouldn't paint over the indicator's column, just frame it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:06:59 +02:00
Jakub
2560ec4cef fix(ui): scale compass tape ticks/pointer for e-ink
Tick marks, the scale baseline and the travel-direction pointer triangle
were hardcoded pixel constants, so they shrank to near-invisible slivers
next to the 2x cardinal labels and numeric readout on landscape e-ink.
Same fix pattern as the trail map / keyboard separator: derive sizes from
miniIconScale()/sepH() instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:00:14 +02:00
Jakub
b85522b2c2 fix(ui): popup menu uses the shared scroll indicator, not raw ^/v glyphs
PopupMenu drew its own unscaled ^/v text markers instead of the
track+thumb indicator the rest of the UI uses, so it didn't show position/
proportion and looked inconsistent on e-ink. drawScrollIndicator(Px) assumed
a full-width list (anchored to d.width()), which doesn't hold for a centred,
narrower popup box — added a right_x-aware overload so it can anchor to the
box's own edge instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:00:06 +02:00
Jakub
1bf90ed2fb fix(ui): drop popup menu's header gap, tighten row tiling
PopupMenu is compact and stands alone (no full-screen header to separate
from), so the gap meant for listStart() just wasted vertical space and let
the selection bar's edges miss the box border by a few px.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 15:33:18 +02:00
Jakub
92643d1721 fix(ui): scale trail-map markers for e-ink, add wrap-around nav
Trail map dots/diamonds/cross/start markers and the north indicator were
drawn as raw unscaled pixels, so they shrank to near-invisible specks on
landscape e-ink's larger font scale; they now route through the mini-icon
framework (icons.h) like the rest of the UI, and the grid intersection dots
scale with it too. The keyboard's preview/grid separator had the same bug
(hardcoded 1px) and now uses display.sepH().

Also makes UP/DOWN (and the keyboard's LEFT/RIGHT/UP/DOWN) wrap top<->bottom
across every list, menu and form selector, matching the behavior PopupMenu
already had.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 09:16:57 +02:00
Jakub
2136e959d4 fix(ui): gap below header separator, one method everywhere
Content drawn under the title separator touched the line on every standard
screen, because listStart() == headerH() (the row right after the separator).
Graphical screens worked around it with a hand-rolled hdr+2.

Bake a 2px breathing gap into listStart() so every list gains it at once, and
switch the screens that hand-rolled the offset (Compass, Nav, Nearby detail) to
listStart() so the content top is computed the same way everywhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:35:51 +02:00
Jakub
e5ff2de3e4 fix(ui): self-sizing popup menu, scaled for landscape e-ink
The popup menu used fixed pixel constants (box width, item height, text
positions) sized for the OLED 128x64. On landscape e-ink the font is ~2x, so
rows overlapped, text ran past the box edge, and the box never adapted to its
contents.

Rebuild render() from live font metrics: box width fits the widest title/item
(clamped to the screen, centred), row height and title bar derive from the line
height, items and title are ellipsized so nothing overflows, and the scroll
arrows live in a reserved gutter. The title separator uses the scaled sepH()
with a 2px gap before the first row so content no longer touches the line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:35:40 +02:00