- 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>
Pull repeated UI idioms into DisplayDriver and remove duplicated logic:
- DisplayDriver: add drawScrollArrows() and drawInvertedHeader(); replaces 11
copy-pasted scroll-arrow blocks across 5 screens and 3 inverted title-bar
blocks (NearbyScreen x2, NavView). Standardises the detail-view separator.
- useImperial(): single source on UITask; NearbyScreen/TrailScreen delegate
instead of each re-reading NodePrefs.units_imperial.
- NearbyScreen: extract saveSelectedWaypoint() (two identical blocks -> one);
drop the redundant local label buffer (WaypointStore truncates).
- UITask::shutdown(): power GPS off through LocationProvider::stop() instead of
poking PIN_GPS_EN directly — centralises enable/reset-pin + active-level logic.
Net -58 lines. Verified: GAT562 (SSD1306) and WioTrackerL1Eink (GxEPD) solo
builds compile clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- UITask::shutdown(): power GPS off using !PIN_GPS_EN_ACTIVE instead of hardcoded
LOW, matching MicroNMEALocationProvider::stop() (still LOW on current active-high
devices, but correct for a future active-low GPS).
- buzzer _noteFreq(): clamp octave to 8 instead of 7 so the parser's accepted range
(4-8) is fully consumed; previously an octave-8 digit was clamped away and could
leak into the stream as the next note's duration.
- Waypoint::readFrom(): check read() return for version/reserved/count header bytes
so a truncated file is rejected instead of using garbage count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NRF52 GPIO pins retain their last state in SYSTEMOFF. If GPS was active,
PIN_GPS_EN stays HIGH and the module keeps drawing ~20mA. gps_enabled is
already persisted to flash so applyGpsPrefs() restores the correct state
on next boot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
UITask.onBLEDisconnected() sets _next_refresh=0 so the BLE icon
updates instantly instead of waiting for the next polling cycle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add build-solo-firmwares.yml: auto-detects all _solo_dual envs from platformio.ini, builds in parallel, publishes uf2 files to draft release
- Remove build-wio-tracker-l1-firmwares.yml (replaced by solo workflow)
- Add WioTrackerL1_companion_solo_dual and WioTrackerL1Eink_companion_solo_dual envs
- Wire build-solo-firmwares command in build.sh using _solo_dual suffix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Restore commented-out EXTERNAL_FLASH_DEVICES/USE_QSPI for reference;
board has no QSPI flash chip — defines were RAK4631 leftovers causing
framework to lock P0.28/P0.30 (joystick UP/LEFT) as QSPI outputs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Board has no QSPI flash chip — EXTERNAL_FLASH_DEVICES/USE_QSPI were
copied from RAK4631. The framework was initialising the QSPI peripheral
on startup, locking P0.28/P0.30 (joystick UP/LEFT) as outputs. Removing
these defines prevents QSPI init entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
QSPI IO0=P0.30 and IO2=P0.28 are wired to joystick LEFT and UP on this
board. QSPI init configures those pins as OUTPUT, blocking button reads.
Disable QSPIFLASH so the joystick works; firmware falls back to internal
flash storage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Contacts with lastmod=0 or lastmod>now (RTC not synced after reboot but
contacts have timestamps from a prior session) were sorted to the top
because their raw lastmod > 0. Now both cases are normalised to 0 before
the comparison, so unknown-time entries always appear at the bottom.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TIME mode: call refresh() every 3 s so newly-heard contacts bubble up
to the correct position; previously the list froze at the order captured
when the filter was selected.
- All filters: right-align the distance/age value within the right column
(width - textWidth - 2 px) instead of pinning it to a fixed left edge,
so short strings like "5s" or "3m" sit flush with the longer ones.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>