Commit Graph

3376 Commits

Author SHA1 Message Date
Jakub
82fa152a94 Merge pull request #11 from vanous/voltage_percentage
Dashboard Clock field: Add Battery percentage
2026-05-27 23:47:22 +02:00
Nick Dunklee
59cee5c8a0 feat: Integrate BME680 Bosch BSEC support for RAK4631
This is a consolidation of my changes for BME680 on RAK4631 nodes.
I will close my other PRs related to this and link back to this one.

*Background on change:*

This change replaces the Adafruit BME680 driver on RAK4631 with the
Bosch BSEC library. Other boards continue to use the existing
Adafruit path via ENV_INCLUDE_BME680.

This makes the IAQ portion of the sensor functional, and more accurate.
It also contains the math and/or CayenneLPP fixes from my other PRs.
The Bosch code also appears to handle calibrating sensor aging as well,
whereas the Adafruit code is just looking at blind values that can drift
with time. Pretty cool to see this shooting out useful data!

RAK4631 platform.io is set to override to ENV_INCLUDE_BME680_BSEC while
leaving the Adafruit code for other node types. (If this becomes applicable
for other node types in future, awesome! I just don't have hardware
to test against.)

Using the BSEC library introduces IAQ sensor calibration, and saves
the calibration state periodically so it does not have to calibrate
again later.

At startup the IAQ sensor takes 30 minutes to heat and to hit a baseline,
then starts calibrating. Once calibrated, it will save those settings
and will only write settings again if calibration falls back and restores
back to state 3.

This fix also has the gas resistance math fix that was in
[pull 2146](https://github.com/meshcore-dev/MeshCore/pull/2146) so
the adafruit path also can at least show accurate values instead
of looping negative.

Also includes the fix from [pull 2149](https://github.com/meshcore-dev/MeshCore/pull/2149) so the pressure output isn't truncated to 1hPa steps.

*Fixes/Changes:*
- Add bsec_config_iaq[] with the 3.3V/3s-LP/28d calibration profile
- BSEC init applies setConfig() for voltage-correct heater targeting
- IAQ, heat-compensated temperature/humidity, pressure, and altitude
  reported over CayenneLPP
- IAQ accuracy reported as analog input over CayenneLPP (0,1,2,3)
- Calibration state persisted to /bsec_state.bin on nRF52 internal
  flash; written only when iaqAccuracy improves to >= 2, should
  keep write frequency well within flash endurance over device lifetime
- Fix non-BSEC query_bme680: float pressure division, addGenericSensor
  for gas resistance (was addAnalogInput, overflows at > 327 Ohm)
- loop() correctly gated for both GPS and BSEC-only builds
- Add fix_bsec_lib.py extra_script to resolve nRF52840 hard-float ABI
  mismatch in Bosch's PlatformIO packaging, silly Bosch

One general note outside of this code change: I noticed while BME680
_functions_ in companion nodes, since companion nodes run Bluetooth,
BLE preempts the CPU, and can do so mid-I2C-transaction.

This can cause the BME680 to see an anomaly and drop calibration and
start a recalibrate. This is behavior that will exist (and has existed)
regardless of using the Adafruit or Bosch paths.

This particular companion behavior does not seem to occur in sensor
or repeater nodes since their BLE is off. Probably affects other
I2C devices as well.

*Tests:*

- RAK19003
- RAK19007
- RAK19001
- repeater, sensor, companion
2026-05-27 15:33:48 -06:00
vanous
3f98060c82 Dashboard Clock field: Add Battery percentage 2026-05-27 22:57:17 +02:00
Jakub
f9167a01bf Merge pull request #10 from vanous/screenshot_tool
Add Screenshot tool and a build flag for conditional screenshot functionality
2026-05-27 22:45:43 +02:00
vanous
7fc8b4b5a5 Screenshot tool and a build flag for conditional screenshot functionality:
- Adds the possibility to capture the device screen and save it as a PNG
  image
- Wrap the code behind ENABLE_SCREENSHOT build flag, as per instructions
  in README
2026-05-27 22:08:28 +02:00
Jakub
5b40e91df3 feat(contacts): channel favourites — filter in Settings + context menu
Add per-channel favourite marking and a Settings filter (all/fav) for
the channel list, symmetric with the existing DM and Room filters.

NodePrefs:
- ch_fav_bitmask (uint64_t): bit i = channel i is marked favourite
- ch_fav_only (uint8_t): 0=show all channels, 1=favourites only
- SCHEMA_SENTINEL bumped to 0xC0DE0005

DataStore: save/load both new fields before the sentinel.

SettingsScreen: new CH_FILTER item ('Channels: all/fav') in the
Contacts section, positioned between DM_FILTER and ROOM_FILTER.

QuickMsgScreen:
- buildChannelList() skips non-favourite channels when ch_fav_only=1
- Channel context menu gains a 4th option 'Fav'/'Unfav' that toggles
  ch_fav_bitmask for the selected channel and rebuilds the list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 17:33:52 +02:00
Jakub
dd9c75d4f4 fix(ui/input): first melody note duration; OLED joystick reset
UITask: when a key is pressed while the buzzer is playing, defer the
next screen refresh by 300 ms instead of triggering it immediately
(via _next_refresh = 100). The blocking e-ink endFrame() was extending
the first note by the full refresh duration (~150-200 ms). Subsequent
notes were unaffected because _next_refresh was already future-dated
after the first render.

DataStore: for builds where JOYSTICK_ROTATION is not defined and
FEAT_JOYSTICK_ROTATION_SETTING is 0 (OLED), always reset
joystick_rotation to 0 on prefs load. Without this, stale e-ink prefs
(which allow changing joystick rotation in Settings) could silently
leave a non-zero value that reversed all joystick directions on OLED.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 17:33:41 +02:00
Jakub
2e3d6381a4 fix(ui): e-ink block char position in default-font path
In GxEPDDisplay::print(), the 0xDB fallback block was drawn at
cy - 8*scale, which assumed the Lemon baseline convention
(cy = original_y + 8*scale). For the default GFX font fontAscender
returns 0, so cy = original_y (top of cell) — the block landed
16 px above the character on e-ink (scale=2).

Fix: draw at cy (top of cell) for the non-Lemon path.

Also remove the unused forceNextRefreshFull() virtual from
DisplayDriver — it was added based on a wrong hypothesis (partial
refresh ghosting) and was never wired into endFrame().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 17:33:31 +02:00
Jakub
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
Jakub
b9814e48c0 docs: fix three README inaccuracies vs code
- Auto-Advert: add missing 30 s interval option (code has 8 opts: off/30s/1min/2min/5min/10min/30min/1h)
- Ringtone Editor: duration labels are 1/4/1/8/1/16/1/32, not whole/half/quarter/eighth
- Lemon font: OLED line height is 9 px, e-ink is 10 px (not uniformly 10 px)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 21:35:13 +02:00
Jakub
4c2da2f364 fix(ui): home pages — unconditional SHUTDOWN eviction when required page absent
Previous fix only evicted SHUTDOWN when cur_len == PAGE_ORDER_LEN (full).
If the array was not yet full but had SHUTDOWN + 2 missing pages (TOOLS and
QUICK_MSG), TOOLS would fill the last slot but QUICK_MSG would be left out.

Remove the cur_len guard: evict SHUTDOWN whenever any required page is
missing, regardless of array fullness. This frees one slot before the
append loop so all missing required pages can be added in a single pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:18:17 +02:00
Jakub
7c8f5895cd fix(ui): home pages — evict SHUTDOWN from full page_order to fit missing pages
Previous fix appended missing pages only when there was room (cur_len <
PAGE_ORDER_LEN). On e-ink builds with GPS+SENSORS the default order fills
all 11 slots; old saves that included SHUTDOWN instead of TOOLS/QUICK_MSG
had no room to append the missing entries.

Now, if the array is full and a required page is absent, SHUTDOWN is evicted
first (it is appended at runtime by buildVisibleOrder's fallback and does not
need to be in the explicit list). This frees a slot for the missing page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 16:13:07 +02:00
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
Wessel Nieboer
62f1b11d5b Increase MAX_FRAME_SIZE by 4 bytes
It's the safest fix for now to fix region scoped messages from being
dropped early
2026-05-26 11:51:14 +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
Liam Cottle
d2c2a6ef0b Merge pull request #2621 from AtlavoxDev/feature/mainboard-boot-complete-hook
Feature/mainboard boot complete hook
2026-05-26 02:04:49 +12:00
AtlavoxDev
e5dab6b999 Rename bootComplete() to onBootComplete() for naming consistency
Matches the existing event-style lifecycle hooks on MainBoard
(onBeforeTransmit, onAfterTransmit) per @liamcottle's review feedback.
2026-05-25 09:50:28 -04:00
AtlavoxDev
39a69b86c3 Add MainBoard::bootComplete() hook for boot-indicator LED feedback
Framework for upcoming variant-specific PRs that add LED feedback during boot. The hook gives users visual cues that the device is busy and
shouldn't be interacted with until startup completes.
2026-05-25 09:04:47 -04: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