Commit Graph

103 Commits

Author SHA1 Message Date
Jakub
9ee199e641 feat(trail): auto-save GPS trail on shutdown (Trail settings toggle)
Losing the whole route on a low-battery auto-shutdown was the worst solo-mode
failure. New NodePrefs::trail_autosave_lowbatt toggle (Tools > Trail >
Settings > Auto-save, default OFF): UITask::shutdown() writes the live trail
to /trail when the toggle is on and _trail.count() > 0. The count guard stops
an empty trail from wiping a previously saved one; it overwrites the same
/trail file the manual Trail > Save uses.

Schema bumped 0xC0DE001A -> 0xC0DE001B: append-only tail field with a load
clamp, so pre-0x1B saves default to off. sizeof(NodePrefs) is unchanged (the
byte fits existing padding after keyboard_type), so the tripwire assert stays
2496.

Builds green: WioTrackerL1_companion_solo_dual (RAM 69.9%, Flash 62.8%).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 22:21:42 +02:00
Jakub
b576de75b6 fix(prefs): bump schema sentinel for keyboard_type field (PR #21)
keyboard_type was inserted into the append-only NodePrefs layout without
bumping SCHEMA_SENTINEL, leaving it at 0xC0DE0019 — the same value plain
main already uses without this field. A device already on that schema
would silently misalign every field read after alarm_min once updated
to a build containing this change. Bump to 0xC0DE001A and fix the
migration comment's stale version label (it said 0xC0DE0018, which was
already taken by the MAP home-page migration).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-02 18:02:48 +02:00
Marcin Marczyk
5ed0a05265 Merge branch 'main' into feat/t9-onscreen-keyboard 2026-07-02 13:49:46 +02:00
marcin
3363867d98 Added T9 on-screen keyboard as an option 2026-07-02 12:21:52 +02:00
MarekZegare4
948076560b feat(home-pages): make Map and Shutdown reorderable; v1.22 notes
The Map page had no Settings entry and Shutdown was pinned to the end of the
carousel because page_order held only 11 slots — full in the common
GPS+SENSORS config, so both pages fell back to being appended at nav time and
couldn't be moved. Grow page_order to 13 (== HPB_COUNT) so every page has a
reorderable slot, and add Map + Shutdown to the default order and the
required-append list.

To keep the persisted format backward-compatible (page_order sits mid-record,
not at the tail), the on-disk head stays the original 11 bytes at its existing
offset and the 2 new slots are appended at the file tail, matching the
append-only schema design. Bump SCHEMA_SENTINEL to 0xC0DE0019; on a pre-0x19
save the tail bytes are the old sentinel/EOF, so they're clamped to 0 and
ensurePageOrderInit re-appends Map/Shutdown into the freed slots on first use.

Drop the now-dead Shutdown-eviction path (13 slots fit all pages).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:01:40 +02:00
MarekZegare4
088bbe61ba fix(home-pages): stop Favourites re-enabling on update; add Map visibility toggle
The prefs schema-migration block runs on every SCHEMA_SENTINEL mismatch, and
the "turn Favourites on" step was ungated. Since each firmware release bumps
the sentinel, every update re-applied `home_pages_mask |= HP_FAVOURITES`,
clobbering a user who had hidden it. Gate it to the transition that added
Favourites (sentinel < 0xC0DE0002), matching the trail_units_idx migration.

The Map/Trail carousel page shipped with no visibility toggle: pageBit(MAP)
returned -1 so it was always visible with no Settings row. Add a proper "Map"
toggle (new HPB_MAP/HP_MAP bit, pageBit/bitToPage cases, HOME_MAP settings
row) plus a gated one-time migration (sentinel < 0xC0DE0018) that keeps it
visible for upgraders. Bump SCHEMA_SENTINEL to 0xC0DE0018; page_order[] stays
a literal 11 so the persisted layout is unchanged.

Also fix the fresh-install seed, which used a stale 0x01FF literal that left
Favourites hidden on new installs despite its "all pages visible" comment;
seed NodePrefs::HP_ALL so fresh installs match upgraders (Favourites + Map).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:34:38 +02:00
MarekZegare4
9ce04835da feat(companion): clock tools — alarm, countdown timer, stopwatch
Add a Clock Tools screen (Enter on the home Clock page) with three time
utilities, plus the engine that drives them from UITask::loop() so they
work regardless of the current screen / display state:

- Alarm: a single one-shot wake alarm. Persisted in NodePrefs as a local
  time-of-day; UITask schedules it as an ABSOLUTE fire instant recomputed
  from that time, so it survives RTC re-syncs (mesh/app/GPS/CLI all jump
  the clock) — small corrections still fire on time, a jump over the
  target still fires (late, up to 6 h). Disarms after firing. A bell
  icon signals an armed alarm on the clock face and the top status bar.
- Timer: a millis-based countdown (sync-immune), big HH:MM:SS readout,
  rings even when off-screen.
- Stopwatch: millis-based, keeps running in the background.

Numeric fields use the shared framework DigitEditor (digit-by-digit,
min/max enforced). The alarm/timer/ring engine lives in UITask alongside
the locator/live-share engines (no screen-cast for time-critical logic).

E-ink: live readouts refresh coarsely (and on any key); timing is exact
regardless, and the countdown's buzzer fires on time. Rings override mute
and are dismissed by any key (auto-stop after 1 min). Cannot wake from a
full Shutdown (CPU/RAM powered down).

NodePrefs: +alarm_on/alarm_hour/alarm_min (fit existing tail padding,
sizeof unchanged 2488), SCHEMA_SENTINEL 0xC0DE0016 -> 0xC0DE0017,
DataStore read/clamp/write in lockstep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 09:01:03 +02:00
MarekZegare4
c60a7a7f64 feat(companion): GPS averaging for waypoint marking (Mark avg)
Add a "Mark avg" trail setting (Off/5/10/30 s). When set, Tools › Trail ›
Mark here samples the GPS fix once a second for the chosen window and stores
the mean position instead of one instantaneous fix — a steadier, more
accurate mark for a precise spot. A progress screen shows time left + sample
count; Cancel aborts; the window then opens the label keyboard as usual.
Off (default) keeps marking instant.

Sampling runs in WaypointsView::poll() (forwarded from TrailScreen::poll())
so it ticks on the main loop, independent of the slow e-ink render cadence;
int64 accumulators avoid overflow over 30 samples.

Persisted as NodePrefs::gps_avg_idx (schema 0xC0DE0016): struct field +
option table, DataStore rd/write/clamp in lockstep. sizeof unchanged (the
uint8_t fits existing tail padding) so the 2488 tripwire still holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 19:19:43 +02:00
MarekZegare4
3c34809af7 fix(companion): stable per-slot channel indices (channels2 -> channels3)
CMD_SET_CHANNEL saves immediately after clearing a channel slot, and
loadChannels() reassigned indices 0,1,2... sequentially on every load,
skipping gaps. Removing a channel and rebooting would then silently shift
every later channel down a slot -- anything that remembers a channel by
index (Live Share's target, the bot's channel, per-channel melody bitmasks)
could end up pointing at the wrong channel.

channels3 records now carry their original slot index instead of padding,
so a removed channel just leaves a hole. One-time migration: if channels3
doesn't exist yet, load the legacy channels2 with its old sequential
semantics, then resave as channels3 so the fallback never runs again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 17:48:16 +02:00
MarekZegare4
be9f3db8c6 feat(companion): on-device room login with saved passwords
Log in to a room server from the device UI with no phone app: picking a
room prompts for its password (blank allowed for open rooms), and a
context-menu "Login..." allows re-login. Successful passwords are
persisted to a dedicated /room_pw file so a previously-used room logs
back in after reboot without retyping; a failed login forgets the
(now-stale) saved password so the next attempt prompts again.

The room-password file is written via a temp file + atomic rename
(new DataStore::commitFile helper), matching the crash-safety of
contacts/channels persistence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 10:55:24 +02:00
MarekZegare4
57774d41f3 feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21).

Features:
- Live Location Sharing — broadcast position as movement-gated [LOC]
  messages to a channel or contact; live shares show as map pins with
  distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is
  parsed in DMs, channel messages and room messages; DM shares name the
  sender.
- Locator (geofence) — arm a geofence around a saved waypoint or a person
  (live [LOC] or last-known position), alert on arrive/leave or near/far,
  with an optional homing beeper (gated to arrive/both modes). Arm from
  Tools > Locator, Nearby Nodes, or Waypoints; target picker lists
  favourites first, clearable via a "None" entry. Active target is drawn
  as a flag on the map.
- One active target shared across Locator / Navigate / Map via a single
  resolver (resolvePersonPos / activeTargetPos) that prefers a live
  [LOC] share over the last-advertised GPS fix.
- Follow live contacts — Navigate to a live-sharing contact follows them
  as they move and adds an ETA line; quick-share your own position from
  the Map.
- Map & status-bar upgrades — home mini-map gets a north marker, scale
  tick, and a connected trail line (was disconnected dots); status line
  shows tracked-node count, an arrow + distance to the active Locator/Nav
  target (falling back to the nearest live-tracked contact); GPS fix icon
  in the top status bar, shown only on GPS boards with GPS enabled.
- Trail auto-pause — recording freezes on stops (banking elapsed time,
  breaking the map line across the idle gap) and resumes on movement
  without ending the session.
- Streaming trail simplification — GPS points are simplified in-stream
  via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight
  runs collapse to their endpoints, curves stay bounded to the Min-dist
  tolerance, so the 512-point buffer covers a far longer route than a
  flat point budget would suggest.
- Collapsible Tools (Location / Comms / System sections, fold-in-place
  like Settings) and page-indicator icons on the home carousel.
- Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon
  digit by digit.

Fixes:
- Critical: low-heap hang and contact loss on RAM-tight builds. Halved
  message-history scrollback rings (recovering ~14 KB free heap) and
  made contacts/channels/prefs persistence atomic (temp file + rename),
  so an interrupted save can no longer corrupt or wipe the store.
- Serial.write() bounded so a stalled USB host can't hang the device.
- Nearby Nodes: live [LOC] senders respect the type filter, sort by
  shared position, and the list refreshes so live shares bubble up.
- Map: live contacts are labelled before waypoints.
- GPS status icon hidden when GPS is off in Settings.
- Splash screen no longer truncates a pre-release tag's own dash (e.g.
  v1.21-rc1) when stripping the build's commit-hash suffix.
- Null-guarded the Locator target picker; clamped loc-share channel
  index on load.

Under the hood:
- -Os size optimisation on the e-ink and GAT562 30S solo envs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:04:21 +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
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
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
fee4182ffa fix(prefs): persist joystick rotation on e-ink builds
The joystick rotation setting never survived a reboot: loadPrefsInt cleared it
on every load. The post-read override guarded on FEAT_JOYSTICK_ROTATION_SETTING
(and JOYSTICK_ROTATION), but DataStore.cpp included neither Features.h nor the
macro's header, so both were undefined — `#if !FEAT_JOYSTICK_ROTATION_SETTING`
was always true and the stored value was forced back to 0 every time.

Include Features.h so the flag resolves per build, and only force the default
when the setting isn't user-editable (OLED). On e-ink the stored value is now
kept; stale values migrated from another build are still corrected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:35:30 +02:00
Jakub
f591cddf4f refactor(ui): audit-pass fixes + dead-code/redundancy cleanup
Fixes from a full ui-new audit (OLED + e-ink both build green):
- remove dead DisplayDriver::drawScrollArrows (all lists use drawScrollIndicator)
- SettingsScreen selection bar -> lineStep()-1, matching the drawList screens
- HomeScreen: don't force the 1s blink refresh on the CLOCK page (status icons
  are hidden there anyway)
- FullscreenMsgView: clamp KEY_DOWN scroll to a cached _max_scroll instead of
  over-incrementing and leaning on the next render to clamp
- DataStore: clamp use_lemon_font (>1 -> 0) on load, like the other enum fields
- move the ~1.5KB wrap scratch (trans/lines) off the render stack into shared
  file-scope statics in FullscreenMsgView (single-threaded UI; the fullscreen
  view and history list never lay out in the same frame)

Cleanup:
- drop dead members RingtoneEditorScreen::DUR_VALS and HomeScreen::sensors_scroll
- delete redundant manual scroll-clamps in handleInput across the drawList
  screens (drawList already reclamps each render); remove the now-unused _visible
  from QuickMsgScreen/NearbyScreen/BotScreen and BotScreen::scrollToSel()

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:54:11 +02:00
MarekZegare4
8bd6fbf1cb feat(bot): hardening + auto-responder, query commands, quiet hours
Reply-bot overhaul on top of the trigger/reply auto-reply:

Robustness
- single botTriggerMatches() shared by DM/channel paths; named constants
  (BOT_REPLY_COOLDOWN_MS / BOT_SCRATCH) replace magic numbers.
- per-contact DM throttle (8-entry ring) so a second sender isn't starved
  while one contact is on cooldown.
- channel anti-loop: skip only when an incoming message equals our own reply
  (was: any reply containing the trigger word — which silently killed channel
  replies for the common "trigger word in reply" setup).

Features
- away / reply-to-all: a lone "*" trigger matches every message.
- independent DM vs channel triggers (bot_trigger / bot_trigger_ch); "*" works
  on the channel too, bounded by cooldown + echo guard.
- query commands: a DM or monitored-channel message is scanned for "!word"
  tokens — !ping/!batt/!loc/!time/!temp/!hops/!status/!help — merged into one
  combined reply. DM = per-contact throttle, ignores quiet hours; channel =
  broadcast, per-channel cooldown, respects quiet hours. !hops uses
  getPathHashCount() (0 = direct).
- quiet hours (bot_quiet_start/end, local, wraps midnight) silence push replies.
- reply counter shown in the BotScreen header.

UI / storage
- BotScreen: 9 rows, scrolling list (no more cramming), field I/O via
  fieldBuf()/fieldCap().
- prefs bot_commands_enabled / bot_quiet_start / bot_quiet_end / bot_trigger_ch
  persisted; schema 0xC0DE000A → 0xC0DE000C with clamping + migration
  (bot_trigger_ch seeded from bot_trigger on upgrade).
- docs: tools_screen bot section rewritten; FEATURES.md refreshed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 11:57:27 +02:00
MarekZegare4
bcf8774962 feat(ui): message delivery status (DM ACK + channel relay)
End-to-end delivery indicators on the device UI, drawn next to outgoing
messages and auto-scaled to the font (legible on landscape e-ink).

DM (and room servers, which share the DM path):
- Pending / delivered / failed marker driven by the real end-to-end ACK.
  Pending shows a row of dots — one per send — so auto-resend progress is
  visible before it resolves to ✓ / ✗.
- Auto-resend: new pref dm_resend_count (0-5, default 2), Settings ›
  Messages › Resend. A pending DM whose ACK times out is re-sent (reusing
  the original timestamp) until resends run out, then ✗. Driven from
  UITask::loop so it completes in the background, independent of screen.
- Incoming dedup: a retry reuses the sender timestamp + text but carries a
  fresh packet hash, so addDMMsg drops copies matching prefix+ts+text.

Channels (flood, no recipient ACK):
- ✓ only once a repeater echo confirms the send was relayed into the mesh;
  no echo is normal, not a failure (no pending/fail shown). A small ring
  tracks a burst of sends so each matches its echo. Receive-path hashing is
  gated so the hot flood path is untouched when idle.

Shared:
- Markers shown in both the history list and the fullscreen message view.
- Reusable scalable mini-icon facility in icons.h (bitmap + auto-scale);
  adding a new status icon is a bitmap plus one draw call.

No changes to the upstream mesh library (src/).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 23:33:16 +02:00
MarekZegare4
2766a098a5 fix(bot): preserve trigger case, lowercase only at match time
Trigger is now stored as typed (e.g. "Ping" stays "Ping").
Case-insensitive matching is preserved by lowercasing both
the trigger and incoming message at comparison time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 09:53:09 +02:00
Jakub
5b58049139 feat(power): battery saving — hardware duty-cycle RX + adaptive TX power
Two independent, default-off toggles under Settings › Radio.

Pwr save: hardware RX duty-cycle (SX126x SetRxDutyCycle via
startReceiveDutyCycleAuto). The chip cycles RX↔sleep and wakes on a preamble —
no MCU state machine; recvRaw reads the packet exactly as in continuous RX.
Falls back to continuous RX on non-SX126x. (Replaces an earlier software-CAD
state machine that fought the hardware: polling a warm-sleeping chip gave a
phantom-busy channel that stalled TX ~4 s and dropped ACKs in the scan gaps.)

Auto pwr: Adaptive Power Control. tx_power_dbm becomes a ceiling; actual TX
power tracks the reverse-link SNR margin (measured above the per-SF demod floor,
EWMA-smoothed, proportional step with a deadband). Feedback comes from direct /
room-server ACKs and, for channels (no ACK), from hearing a repeater rebroadcast
our own flood; a lost confirmation ramps power back up so channel sends can't get
stranded below what the repeaters can hear.

Prefs schema 0xC0DE0009 (rx_powersave, tx_apc). Radio page / name bar show the
live TX power; noise floor reads n/a while duty-cycling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 08:15:42 +02:00
Jakub
cd4c8e3ce6 fix(DataStore): clamp notif_melody_dm/ch to valid range [0-3]
After PR #16 added None (slot=3) to melody options, only notif_melody_ad
had a bounds check. Add matching clamps for dm and ch to prevent stray
bytes from an older prefs file triggering buildMelodyFromPrefs with an
out-of-range slot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 16:13:16 +02:00
vanous
8e1352b24a Settings - Sound: Add Advert scope 2026-06-07 10:53:17 +02:00
vanous
7703b3ae36 Settings - Sound: Allow to set items to None 2026-06-07 09:13:02 +02:00
Jakub
f5b3213f80 Merge upstream/main (v1.16.0) into wio-unified
Merge 254 upstream commits. Highlights relevant to the Wio fork: native
NRF52 companion power-saving (sleep when !hasPendingWork), preamble 16->32
for SF<9, generic sensor-registration model, CMD_SEND_RAW_PACKET, isEink()
display abstraction, KEEP_DISPLAY_ON_USB, contacts-sync/transient fixes.

Conflicts resolved (11 files):
- buzzer.h/.cpp: keep NRF52 include guard + our begin()->startup(); fold in
  upstream doc comment.
- platformio (wio + eink): keep our slim env layout, add upstream kiss_modem
  env (+ debug_tool=stlink on eink ble).
- GxEPDDisplay.h: take upstream isEink() override.
- EnvironmentSensorManager.h: take upstream's wider #if guard.
- DataStore.cpp: take upstream saveContacts(filter) signature, keep our
  ::openWrite (member openWrite would shadow the global 2-arg overload).
- main.cpp: keep our watchdog init + add board.onBootComplete(); adopt
  upstream's `if(!hasPendingWork()) sleep` loop.
- MyMesh.h: version -> v1.16-solo.0, build date 6 Jun 2026.
- MyMesh.cpp: keep both CMD codes (SCREENSHOT 66 + RAW_PACKET 65), both field
  inits, both handlers, our screenshot fns + upstream saveContacts/save_filter,
  upstream hasPendingWork() + our MyMeshBot include.
- UITask.cpp: keep our solo splash/clock page/snprintf/buzzer init; merge
  auto-off (+ opt-in KEEP_DISPLAY_ON_USB); merge low-batt shutdown (our EMA +
  prefs threshold + upstream's !isExternalPowered() guard and eink-aware delay).

Post-merge drift fixes (upstream API/architecture changes, not conflicts):
- applyTxPower(): radio_set_tx_power() removed upstream -> radio_driver.setTxPower().
- Sensor refactor dropped the per-sensor *_initialized flags our
  getAvailableLPPTypes() relied on; removed that override and the SENSORS page
  now derives available LPP types from the populated sensors_lpp buffer.

README kept ours via .gitattributes merge=ours. Builds verified on all 5 Wio
envs (dual, eink dual, radio_ble, dual_dev, eink_dual_dev). Not yet
hardware-verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:21:41 +02:00
Jakub
2bbdb7fe6c Merge remote-tracking branch 'origin/wio-unified' into wio-unified
# Conflicts:
#	examples/companion_radio/DataStore.cpp
2026-06-04 17:13:49 +02:00
vanous
0630e127d4 Add Advert sound and settings 2026-06-04 09:28:41 +02:00
Jakub
359b5fccd7 feat(units): global metric/imperial setting for all distance displays
Add a single "Units" choice (Settings › System: Metric / Imperial) that
governs every distance and speed shown in Tools — Nearby Nodes, the Trail
views, and the navigate-to-point screen.

- NodePrefs: units_imperial (global) + trail_show_pace (Trail readout mode),
  schema bumped 0xC0DE0005 → 0006. The old combined trail_units_idx is kept
  for file-layout stability but no longer drives anything; upgraders default
  to metric + speed.
- geo::fmtDist gains an imperial mode (ft below ~1000 ft, then miles).
- NavView / Nearby / Trail (waypoint list, Summary distance, List deltas,
  map scale-bar label) all format through the global preference.
- Trail's action-menu "Units" row becomes a Speed/Pace readout toggle; the
  km-vs-mi unit now follows the global setting instead of being one of four
  combined options.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:44:56 +02:00
Scott Powell
d9df8307ca * first draft 2026-06-01 16:49:31 +10:00
Jakub
9113b7f12e fix: guard findChannelIdx == -1 + scope trail_units reset
C1/C2: onChannelMessageRecv and onChannelDataRecv both passed
findChannelIdx() through (uint8_t), so an unknown-secret packet
turned -1 into 255 and the message flowed into the offline queue,
UI history, notification and bot reply with a bogus channel index.
Drop such packets at the recv path; also harden addChannelMsg with
an MAX_GROUP_CHANNELS bounds check so a future caller can't poison
the channel-history ring buffer either.

H4: loadPrefsInt always reset trail_units_idx on sentinel mismatch,
even for jumps where the field was saved correctly (e.g. 0xC0DE0004
→ 0xC0DE0005). Scope the reset to sentinel == 0xC0DE0003, which is
the only case where the byte at that offset is actually the stale
sentinel low byte.

FEATURES.md: mark these three items as  in the audit backlog.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:57:12 +02:00
Jakub
cb37d23eb0 fix(prefs): skip channels with empty secret in loadChannels
An all-zero secret in /channels2 means the entry is uninitialised or the
file format was corrupted by a previous firmware (e.g. different on-disk
layout from an older fork). Loading such a channel makes findChannelIdx()
match the wrong slot for incoming messages — they end up associated with
the bogus channel index and never display, even when the companion app
receives them fine over BLE.

Skip empty-secret entries during load and log how many were dropped.
Also defensively null-terminate the loaded name so callers can treat it
as a C string regardless of how the file was written.

Reported via user feedback: "only public visible, hashtag channels not;
public was empty; phone could read messages; works after wipe".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 19:56:25 +02:00
Jakub
c92264c21e fix(ui): OLED joystick rotation — enforce reset after prefs rd()
The previous fix set joystick_rotation=0 before reading from file, but
the subsequent rd() call overwrote it with whatever was stored (e.g. 2
from a prior e-ink firmware). Move the hardware-enforce block to after
rd() so stale values in flash are always corrected on OLED builds
(FEAT_JOYSTICK_ROTATION_SETTING=0). Remove the now-redundant pre-read
block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 23:49:19 +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
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>
2026-05-26 15:49:50 +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
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
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
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
66122e782d fix(ui): Favourites page polish + upgrade migration
Render issues:
- Drop the "FAVOURITES" title — it overlapped the node-name/battery top
  bar; the page-dots indicator already identifies the page
- Use the locally-computed content_y (matches other home pages) so tiles
  sit below the dots row instead of starting at headerH() + sepH()
- Add 2 px top/bottom margin inside the grid so cells don't hug the dots
  or the panel edge

Upgrade migration (visibility + position):
- Newly-loaded prefs with an older sentinel OR a non-default mask had
  HP_FAVOURITES off and could not be enabled — DataStore::loadPrefsInt
  now ORs HP_FAVOURITES into home_pages_mask whenever the sentinel
  doesn't match the current schema
- Existing page_order_set users were stuck because FAVOURITES wasn't in
  page_order, so movePageInOrder bailed — ensurePageOrderInit now
  detects the missing FAVOURITES entry and inserts it right after CLOCK,
  shifting the tail; if the array is already full the page still
  appears via the buildVisibleOrder fallback

Bug fix in ensurePageOrderInit verification loop: iterated to HPB_COUNT
(12) over a PAGE_ORDER_LEN (11) array → one OOB read per call. Now
bounded to PAGE_ORDER_LEN.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:17:48 +02:00
Jakub
21c02c7114 feat(ui): Favourites home page (phase 1: storage + read-only grid)
New home page rendering 6 pinned-contact slots. Grid transposes to
orientation: landscape → 3×2, portrait → 2×3. Joystick claims UP/DOWN
and inner LEFT/RIGHT for grid nav; edge LEFT/RIGHT fall through to
page navigation. Selected tile inverts via drawSelectionRow, filled
tile shows contact name + unread badge, empty tile shows "+".

Storage:
- NodePrefs::favourite_contacts[6][6] (6-byte pub_key prefix per slot,
  all-zero = empty; collision probability with a real key is 2^-48)
- HomePageBit gains HPB_FAVOURITES = 11 (HPB_COUNT = 12), with a new
  HP_FAVOURITES mask bit so the page is toggleable in Settings
- New PAGE_ORDER_LEN constant decouples the persisted array length (11)
  from the page-type count; loops over page_order now use it, value-range
  checks still use HPB_COUNT
- homePageBit returns 0 for HPB_SETTINGS / HPB_QUICK_MSG explicitly
  rather than by bit-index range, so HPB_FAVOURITES (bit 11) is correctly
  treated as toggleable
- Default order inserts FAVOURITES after CLOCK; SHUTDOWN drops to the
  fallback "missing pages" append (still reachable, lands at end)

Schema sentinel bumped to 0xC0DE0002. DataStore::loadPrefsInt and
savePrefs read/write the new field. Older saves leave the field
zero-initialised (all slots empty), which is the natural starting state.

Phase 2 (Pin from Contact options) and Phase 3 (Pin from empty-slot
mini picker) wire interactions; this commit handles render + nav only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:00:27 +02:00
Jakub
a4b4362b1b refactor(prefs): centralise home-page bit numbering in NodePrefs::HomePageBit
pageBit/bitToPage in HomeScreen and homePageBitIndex in SettingsScreen
each spelled out the bit assignment with magic numbers 0..10. Lift the
numbering into a NodePrefs::HomePageBit enum (HPB_CLOCK … HPB_QUICK_MSG,
HPB_COUNT) and route every reference through it: mapping functions, the
HP_* bitmasks, the homePageLabel table sizing, loop bounds over
page_order, the bounds check on page_order[0], the "always-visible"
discriminator for SETTINGS/QUICK_MSG.

Per-class enums (HomeScreen::HomePage, SettingsScreen::SettingItem)
stay — they index different concerns (navigation page vs. settings
row). The mapping functions still exist, but now reference a single
source of truth for the bit values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:26:56 +02:00
Jakub
87bdc5cb9f feat(prefs): tail schema sentinel for layout-change detection
Old saves (without sentinel) load unchanged — rd() lambda zero-inits
anything not on disk. New saves append a 4-byte sentinel that encodes
the schema revision; the low byte is bumped on every layout change in
loadPrefsInt/savePrefs. Mismatch on load logs a warning and continues
with defaults; the next savePrefs writes the current sentinel so the
file self-heals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:18:50 +02:00
Jakub
e2d59b9a3d fix(prefs): use magic sentinel to validate page_order
Previously page_order_set status was inferred from page_order[0] being
in 1..11, so a single junk byte in that range falsely triggered
custom-order mode. Add explicit page_order_set magic (0xA5) written
alongside the array; pre-magic saves are migrated once on load when
their first byte still passes the legacy range check.

Also fix(ui): keep "@[nick] " prefix out of placeholder expansion in
QuickMsgScreen reply path, so a nick containing a token like {loc}
isn't substituted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:53:05 +02:00
Jakub
a6bf3ee08f feat(eink): full refresh interval setting in Settings › Display
Add "Full rfsh" setting (e-ink only) with options off/5/10/20/30
partial refreshes between full refreshes. Prevents ghosting on panels
that accumulate artifacts after many partial updates.

GxEPDDisplay counts partial refreshes and calls display.display(false)
every N frames when the interval is set. Persisted in DataStore and
applied at startup via applyFullRefreshInterval().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 10:22:03 +02:00
Jakub
b279b95b2d refactor(ui): apply code review fixes — bugs, risk, optimisations, cleanup
Fixes 7 bugs (timing UB on unsigned long, abs() no-op, XBM CRC PROGMEM
scan, CayenneLPP heap thrash ×2, UTF-8 reply prefix, lock-seq reset on
wake), 3 risk items (notif melody stop guard, bot trigger pre-lowercase,
lock-seq cleared on display wake), plus optimisations (fmtMsgAge/
getTextWidth caching, RTTTL builder consolidated to NodePrefs, HP_*
constants and homePageLabel table moved to NodePrefs, DataStore flat
lambda schema, translateUTF8Static factored out) and minor cleanup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 20:03:45 +02:00
Jakub
fea1be8e81 feat(ui): runtime joystick rotation independent of display rotation
Add NodePrefs::joystick_rotation (0-3 steps CW), persisted in DataStore
alongside display_rotation. rotateJoystickKey() now takes a runtime rot
parameter instead of compile-time JOYSTICK_ROTATION macro. UITask::loop()
reads joystick_rotation from NodePrefs each frame. Settings screen exposes
"Joystick" rotation item on any build with UI_HAS_JOYSTICK. JOYSTICK_ROTATION
macro still works as compile-time default for legacy/non-prefs builds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 15:20:40 +02:00
Jakub
bad9fae419 merge: bring home page ordering feature and CI dual-build from wio-tracker-l1-improvements
Resolved conflicts to preserve eink-ui layout helpers (valCol, lineStep, sepH)
while integrating the page_order persistence, buildVisibleOrder(), and
Settings/Messages ordering UI from the wio-tracker-l1-improvements branch.
display_rotation is now written before page_order in the save/load sequence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:46:35 +02:00
Jakub
bb94a6f81f feat: home screen ordering — reorder all screens from Settings
NodePrefs: add page_order[11] (1-indexed bit-index, 0=end/legacy).
DataStore: persist page_order appended after use_lemon_font.

UITask/HomeScreen:
- bitToPage(): maps stable bit indices 0-10 to HomePage enum values
- buildVisibleOrder(): respects page_order when initialised, else
  falls back to default enum sequence
- navPage() and navigation dots use buildVisibleOrder()

SettingsScreen Home Pages section:
- Add Settings (bit 9) and Messages (bit 10) — show as "always"
- LEFT/RIGHT on any home page item moves it earlier/later in order
- ENTER toggles ON/OFF for pages that can be disabled
- Position number shown before label once custom order is active
- ensurePageOrderInit() seeds default order on first reorder action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 23:33:06 +02:00
Jakub
6a3b8e3135 feat: add runtime display rotation setting for e-ink builds
Adds Settings > Display > Rotation (0°/90°/180°/270°) that persists to
NodePrefs and is applied on startup. Falls back to compile-time
DISPLAY_ROTATION when the field is missing from an older prefs file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 09:58:33 +02:00