Commit Graph
22 Commits
Author SHA1 Message Date
MarekZegare4andClaude Opus 4.8 362d4e9baa tune(trail): tighten the simplification corridor for more fidelity
Field test: 4 km at the 10 m min-delta gate cost only 38 of the 512-point
trail capacity, leaving plenty of headroom. The straight-run corridor had
been tied 1:1 to the min-delta gate; add a CORRIDOR_FACTOR (0.5) so the
corridor is tighter than the gate, committing more vertices per route (less
reduction, a polyline that hugs the real track more closely) while leaving
the gate itself — the noise floor for rejecting too-close samples —
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:02:42 +02:00
MarekZegare4andClaude Opus 4.8 81f38566a6 revert(companion): trail buffer back to 512 points
The 1024-point trail buffer (16 KB in .bss, since ui_task is a global)
doubled the static footprint of the GPS trail for little practical gain on
a RAM-tight nRF52 build. Revert CAPACITY to 512 (8 KB) and sync the Tools
docs. Heap headroom is better spent elsewhere; in-stream simplification
already lets 512 vertices cover a long route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 21:39:46 +02:00
MarekZegare4andClaude Opus 4.8 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
MarekZegare4andClaude Sonnet 4.6 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
MarekZegare4andClaude Sonnet 4.6 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
MarekZegare4andClaude Opus 4.8 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
MarekZegare4andClaude Opus 4.8 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
MarekZegare4andClaude Sonnet 4.6 b8a7b5dcc0 refactor: dedup persistence header, harden action menu, tidy small items
- 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>
2026-06-14 13:37:03 +02:00
MarekZegare4andClaude Sonnet 4.6 1521be3803 fix(trail): check read() return for header bytes in readFrom/exportGpxFromFile
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>
2026-06-14 12:53:40 +02:00
JakubandClaude Sonnet 4.6 2cc68fb987 fix(trail,batt): GPX NUL in stream + battery curve top at 4170 mV
- Trail.h gpxWaypoints/gpxPoint: snprintf truncation clamp was `> sizeof`
  (writing the NUL terminator into the XML stream); corrected to
  `>= sizeof - 1` at all 3 sites (<wpt>, <time>, <trkpt>).
- UITask.cpp battMvToPercent: raise 100% ceiling from 4200 → 4170 mV —
  the board never charges to 4200, so the indicator was stuck at ~97%.
- Trail.h header comment: corrected RAM cost from "256×12 B=3 KB" to
  actual CAPACITY(512)×16 B=8 KB.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 17:00:00 +02:00
JakubandClaude Opus 4.8 26097d7d68 feat(trail): include waypoints in the GPX export
GPX dumps previously contained only the <trk>. Emit saved waypoints as
<wpt> elements (label → <name>, plus <time> when the RTC was set) before
the track, per GPX 1.1 ordering, in both live and saved-file exports. The
emitter is duck-typed over the waypoint store so Trail.h stays decoupled
from Waypoint.h; labels are XML-escaped. They now import as pins in OsmAnd /
Garmin / GPX Studio alongside the track.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:28:21 +02:00
JakubandClaude Opus 4.8 d0d20989a0 feat(units): imperial min-distance gate + imperial map grid
Follow the global Units preference in the two remaining Trail distance
spots that were still metric-only:

- Min dist gate: the index is now a unit-agnostic level (0=finest..3).
  Metric stays 5/10/25/100 m; imperial uses round 15/30/75/300 ft (applied
  as their metre equivalents in the sampling gate).
- Map scale grid: imperial mode uses round ft/mi steps (10 ft … 100 mi)
  instead of metric steps with a converted label. Step storage moved to
  float metres so sub-metre imperial steps keep their geometry; labels come
  from a parallel table so the scale bar reads an exact round value.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:50:42 +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 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
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 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 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 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 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
JakubandClaude Sonnet 4.6 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
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 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