Commit Graph

227 Commits

Author SHA1 Message Date
MarekZegare4
f6baa5c38c refactor(companion): extract MessageHistory store from QuickMsgScreen
Move the two RAM history rings (channel + DM) out of the 1988-line
QuickMsgScreen into a self-contained MessageHistory.h component, mirroring
the WaypointsView extraction from TrailScreen. The store owns the ring
buffers, per-entry delivery state (channel relay echo, DM end-to-end ACK +
auto-resend) and the per-channel unread counters; the screen keeps all
view state (selection, scroll, fullscreen readers, the unread "viewing
session" bookkeeping) and reaches entries through accessors.

Behaviour-preserving:
- The shared types (AckState, ChHistEntry, DmHistEntry, MSG_TEXT_BUF) are
  file-scope in MessageHistory.h, so the phase machine still refers to them
  unqualified — only data + storage logic moved.
- addChannelMsg keeps a thin screen forwarder that computes the "viewing"
  flag (a phase fact the store can't see) and returns the ring pos;
  afterSend uses armChannelRelay(pos, seq) instead of poking the ring.
- The public API called via UITask/MyMesh/the bot (addChannelMsg, addDMMsg,
  markDmDelivered, markChannelRelayed, tickDmResends, clearAllChannelUnread,
  getTotalChannelUnread, getRecentDMContacts) stays on QuickMsgScreen as
  forwarders, so no caller changes.

QuickMsgScreen 1988 -> 1738 lines; MessageHistory.h 324. Both solo boards
build. Single-TU fragment: included by UITask.cpp before QuickMsgScreen.h.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 22:41:01 +02:00
MarekZegare4
0e6bd743a6 refactor(companion): unify screen save-on-exit via UITask::savePrefsIfDirty()
The "_dirty bool, then if (_dirty) the_mesh.savePrefs() on exit" pattern was
duplicated across 9 screens with subtle divergence: some cleared the flag
after saving, some left it set and relied on onShow() to reset. Replace all
12 exit sites with a single _task->savePrefsIfDirty(flag) helper that saves
once only if dirty and always clears the flag, so the "did we touch flash?"
answer lives in one place and the reset is consistent.

Edit sites still mark the flag manually (inherent to change tracking); only
the persist-on-exit boilerplate is centralised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:47:51 +02:00
MarekZegare4
15716d2b03 fix(companion): nullptr-init screen pointers so a forgotten new() fails safe
Adding a screen touches 4 sites; 3 (member decl, gotoX decl, gotoX def) are
compile-checked, but a missed `new XScreen()` in begin() left the pointer
uninitialised and crashed at first navigation. Give every screen member an
in-class nullptr initialiser and bail early in setCurrScreen(nullptr) so the
mistake is an inert no-op instead of a null deref. Document the 4-site
registration contract on the member block.

A full registry table was considered and rejected: the named gotoXScreen()
methods are a depended-upon API (~30 call sites, menu dispatch + back-nav),
so a table would add an enum + indirection without removing them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:42:59 +02:00
MarekZegare4
eedd47d1e1 refactor(companion): hoist screen entry into virtual UIScreen::onShow()
Replace the ad-hoc enter()/markClean() entry methods (which lived outside
the UIScreen interface and were invoked via casts from each gotoX) with a
virtual onShow() lifecycle hook called centrally by setCurrScreen().

This removes the "forgot to call enter() in a new navigator" footgun and
the unchecked cast smell: 12 navigators collapse to one-line
setCurrScreen(x) calls, and override enforces signature match. Two entries
that carry a parameter/variant keep an explicit typed call after
setCurrScreen(): RingtoneEditor::selectSlot(slot) and TrailScreen::showMapView().

Behaviour-preserving: only screens that previously had enter() get an
onShow() override; Splash/Home/QuickMsg/Diag keep no reset as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:37:04 +02:00
MarekZegare4
bcca97a848 docs(companion): document the screen-fragment single-TU contract
The ui-new/*.h screens are header fragments compiled only as part of
UITask.cpp, in include order. Two implicit rules a contributor can trip on:
include order (a static inline helper / shared scratch is visible only to
later fragments) and single-TU-only (some fragments define external-linkage
symbols at file scope, e.g. NearbyScreen::FILTER_LABELS, so reusing one from a
second .cpp is a duplicate-symbol link error).

Spell both out in a contract comment at the inclusion block, and raise the
single-TU note to a callout in the framework guide. Comments only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:20:47 +02:00
MarekZegare4
b624d03e96 docs(companion): make the entity-reference cleanup contract explicit
The onContactRemoved/onChannelRemoved hooks already clear every NodePrefs
field keyed on a contact pubkey or channel index (verified complete), but the
convention was opt-in with no reminder — new per-entity state could silently
forget to register, the class of bug fixed earlier this session.

Add a CONTRACT comment above each hook listing what it covers and instructing
additions to land there, plus a terse [del->onContactRemoved] /
[del->onChannelRemoved] tag on each of the eight keyed fields in NodePrefs.h.
Now editing either side (struct field or hook) points at the other. Comments
only; no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:17:38 +02:00
MarekZegare4
7a5247d5d4 fix(companion): show the sender's own timestamp for synced/queued messages
DM, room and channel history always stamped a message with receipt time
(rtc_clock.getCurrentTime()), even though the sender's real timestamp was
already available (and, for DMs/rooms, already threaded through to
addDMMsg -- just never used for display). Live-received messages hid this
because receipt lags origination by only seconds, but a room-sync replay or
an offline-queued message held by a repeater can arrive long after it was
sent, so a burst of backlog messages all showed as "just now".

storeDMMsg() now prefers msg_ts (falling back to receipt time only when
unknown). addChannelMsg() gained a timestamp parameter, threaded from
onChannelMessageRecv() down through AbstractUITask/UITask, with the same
fallback. The DM dedup check and outgoing-message timestamps are unaffected
(they use msg_ts directly, already correct).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 11:00:45 +02:00
MarekZegare4
6a394e2d7a fix(companion): clear stale channel-index refs when a channel is removed
CMD_SET_CHANNEL clearing a slot (empty secret) left bot_channel_idx,
loc_share_channel_idx, and the per-channel melody bitmasks pointing at that
index. A new channel added later at the same slot would then silently
inherit the old one's bot target, Live Share target, or notification
melody. New onChannelRemoved() hook, mirroring onContactRemoved(), turns
the bot/Live Share channel target off (fail closed) and clears the melody
bits for that index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 00:39:10 +02:00
MarekZegare4
5c4607cca9 fix(companion): clear stale Locator/Live Share/favourite refs on delete
Deleting a waypoint left the Locator pointed at coordinates that no longer
existed (it's a coordinate snapshot, so nothing noticed). Removing a contact
was worse: nothing cleared its favourite slot, its Locator/Live Share target,
or its per-contact mute/melody entry, so all four kept referencing a pubkey
that no longer resolved to anything.

- WaypointsView's Delete now calls UITask::clearTargetIfWaypoint() first.
- New AbstractUITask::onContactRemoved() hook, called from MyMesh.cpp's
  CMD_REMOVE_CONTACT handler, clears the favourite slot, the Locator target,
  and dm_notif/dm_melody entries for that pubkey. Live Share's DM target
  turns auto-share off instead of guessing a new recipient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 17:48:05 +02:00
MarekZegare4
5d71fde980 fix(companion): prompt to enable GPS when starting a trail with GPS off
Starting trail recording with GPS switched off used to call setActive(true)
immediately -- the session timer ran while the screen sat on "Waiting for
GPS fix" forever and recorded nothing, with no hint that GPS was the
problem. Choosing "Start tracking" now opens a "GPS is off" confirmation
(Enable GPS & start / Cancel); confirming enables GPS and starts the
session. Behaviour is unchanged when GPS is already on.

Gated on a new UITask::hasGPS() so the prompt only appears on boards that
expose a toggleable GPS, not where GPS is simply absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 14:11:48 +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
Jakub
32c50d1cfe feat(repeater): politeness controls, dedicated radio profile, and diagnostics
Settings > Radio gains a repeater toggle, 16 community-suggested radio
presets plus manual Freq/SF/BW/CR tuning (digit-by-digit Freq editor),
4 persisted user preset slots, and the packet pool bumped 16->32 so
queued retransmits stop starving incoming-packet allocation while
relaying.

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 22:34:52 +02:00
Jakub
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
Jakub
da161d49d2 fix(ui): message-history scrollbar reserve stuck behind alert overlay
The DM/channel history views size the scroll-column reserve from the previous
frame's visible count. A new message bumps the count so the reserve briefly
appears until the next frame settles it — normally invisible, but the alert
overlay froze the screen (_next_refresh = _alert_expiry) for the alert's whole
duration, leaving the content shifted as if a scrollbar were needed. Keep the
underlying screen refreshing at its own cadence (capped at the alert expiry);
the display CRC skips unchanged frames so e-ink isn't thrashed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +02:00
Jakub
98af868e68 refactor(ui): dedup status-bar indicators
- Extract drawBoxedIcon()/drawSlotIcon() in icons.h; collapse the four
  near-identical mute/BT/advert/trail blocks in renderBatteryIndicator.
- Centre the glyph on the actual indicator box (box_h) instead of the text
  line, fixing the 1px vertical offset in Lemon mode.
- Extract blinkOn() for the shared advert/trail blink cadence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 18:17:34 +02:00
Jakub
353dee5878 feat(ui): status-bar mini-icons + settings fixes
- Replace single-letter M/B/A/G top-bar indicators with scalable mini-icons:
  ICON_MUTE (speaker+cross), ICON_BLUETOOTH (rune), ICON_ADVERT (broadcast
  mast+waves), ICON_TRAIL (map pin). Centred in the cw+2 indicator box;
  disconnected BT shows the plain glyph instead of lowercase b.
- fix(settings): right-side values used display.valCol() without the scrollbar
  reserve, so after expanding a section they rendered under the indicator.
  Route all value cursors through valCol(display) = display.valCol() - _reserve.
- fix(settings): font toggle used ^=1, which on a stale value of 2 (older Hybrid
  build) flips 2<->3 — both nonzero, locking applyFont() on Lemon with no way
  back. Normalise: use_lemon_font = use_lemon_font ? 0 : 1.
- docs(settings): document Messages > Resend setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 16:45:28 +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
151892e1ce refactor(ui): extract shared helpers, dedup screens, GPS shutdown via provider
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>
2026-06-14 13:21:59 +02:00
MarekZegare4
fcdacaa3bc fix: GPS shutdown active-level, buzzer octave-8 consistency, waypoint header read checks
- 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>
2026-06-14 12:49:15 +02:00
MarekZegare4
6c5a40a8ce fix(power): power off GPS before SYSTEMOFF to prevent battery drain on shutdown
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>
2026-06-14 12:29:33 +02:00
MarekZegare4
5ec61477e6 feat(ui): simplify solo splash label, remove outdated WIOTRACKER_L1.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-12 09:46:45 +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
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
Jakub
e6f8fb99c7 feat(NearbyScreen): waypoint/navigate from list, TIME filter, addWaypoint helper
NearbyScreen:
- Hold Enter on a list row now shows Navigate and Save waypoint directly,
  without entering the detail view first
- TIME added as the last filter slot (LEFT/RIGHT cycles Fav→ALL→…→Snsr→TIME);
  time-sort shows age in right column and sorts by last-heard descending
- Save waypoint also available from detail view Options menu

UITask:
- Extract addWaypoint() helper (two overloads: with/without explicit timestamp)
  consolidating the full()+add()+saveWaypoints()+alert pattern; TrailScreen
  and QuickMsgScreen updated to use it

Fixes:
- SoundNotifier.h: correct NodePrefs.h include path (../NodePrefs.h)
- kbAddSensorPlaceholders: dereference _kb pointer in SettingsScreen,
  QuickMsgScreen and BotScreen (signature changed to KeyboardWidget&)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 15:09:57 +02:00
Jakub
51ce15bbdc refactor: extract SoundNotifier helper from UITask::notify()
Move all buzzer/melody dispatch (DM, channel, advert) into a new
SoundNotifier class in SoundNotifier.h, isolating fork-specific sound
code so upstream changes to notify() no longer cause merge conflicts.
The shared playSlot() helper also removes the custom_played
duplication that existed across the three cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 21:51:03 +02:00
Jakub
ca9b3ff726 cleanup: remove dead UIEventType::advertReceived
The enum value and its case label were never emitted after PR #16 —
both dispatch sites in MyMesh.cpp now use advertReceivedFlood or
advertReceivedZeroHop exclusively.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:47:37 +02:00
Jakub
c339dc7d97 refactor(ui): share single KeyboardWidget instance across screens
Moves KeyboardWidget from a per-screen value member to a single instance
owned by UITask, passed to SettingsScreen, QuickMsgScreen, and BotScreen
as a pointer. Only one screen is ever active at a time, so the shared
state is safe. Saves ~1.5 KB of always-resident heap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:42:18 +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
cacc4e6206 fix(ui): keep hasConnection() BLE-specific; use isClientConnected() only for mute/wake
Self-review caught a regression I'd just introduced: feeding hasConnection()
from isClientConnected() (BLE *or* USB) broke the GPX-export collision warning
in TrailScreen, which relies on hasConnection() meaning "BLE app connected"
(BLE → USB dump is safe; otherwise the app may be on USB → warn).

Correct split:
- hasConnection() ← isBLEConnected() — BLE-specific consumers: BT status
  indicator, pairing PIN, GPX-export warning.
- isClientConnected() (BLE bonded OR open USB-CDC port) used directly only by
  the Auto buzzer mute and message-wake, which should react to either transport.

Builds clean on dual (OLED + e-ink) and pure-BLE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:42:14 +02:00
Jakub
3b795f7abc feat(ui): detect USB-CDC connection for Auto mute (BLE or USB)
USB client presence IS detectable on nRF52 after all: (bool)Serial ==
tud_cdc_n_connected() (DTR — a host has the CDC port open). Add
isClientConnected() = BLE-bonded OR USB-CDC-open; DualSerialInterface
overrides it, base defaults to isConnected() (single-transport unchanged).

Wire it so each consumer gets the right signal:
- hasConnection() ← isClientConnected(): Auto buzzer mute + message-wake now
  trigger on BLE or an open USB port (PR #14's intent, done correctly), but
  not on charging-only (no host → DTR low).
- BT status indicator + pairing PIN stay on isBLEConnected() (BLE-specific).

Caveat: a plain serial monitor also asserts DTR, so it counts as connected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:36:49 +02:00
Jakub
a6ae9e80c8 fix(ble): show pairing PIN on dual (BLE+USB) builds
The Bluetooth-page PIN prompt was gated on hasConnection(), which is fed by
the serial interface's isConnected(). DualSerialInterface::isConnected()
always returns true (USB always "connected"), so waiting_for_pair was always
false and the PIN was never drawn — on every dual build, i.e. the published
OLED + e-ink firmware. Pure-BLE builds were unaffected (there isConnected()
reflects bond state).

Gate the PIN on actual BLE-bonded state instead: add
AbstractUITask::isBLEConnected() (forwards to BaseSerialInterface::
isBLEConnected(), which DualSerialInterface overrides with the real BLE state
and pure-BLE inherits as its bond-aware isConnected()). The PIN now shows on
the Bluetooth page while BLE is on and not yet bonded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:03:38 +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
Jakub
75f9ccb34a feat(nav): share a waypoint as a message
Waypoints list → Hold Enter → Send hands the point to the Messages screen
as "[WAY]<lat>,<lon> <label>" (same text format geo::parseLatLon already
reads). The user picks a contact or channel, the text lands prefilled in
the keyboard to confirm/edit, then sends — closing the loop with the
Navigate / Save waypoint actions on the receiving end.

- QuickMsgScreen: share-compose mode (startShare/beginShareCompose). Picking
  a recipient jumps straight to the prefilled keyboard; cancel returns home;
  afterSend clears the mode.
- UITask::shareToMessage hands off from TrailScreen to the Messages screen.
- TrailScreen: "Send" added to the waypoint Rename/Delete popup; builds the
  [WAY] payload at the {loc} precision (5 dp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:38:12 +02:00
vanous
0630e127d4 Add Advert sound and settings 2026-06-04 09:28:41 +02:00
Liam Cottle
b83355e702 Merge pull request #2671 from sefinek/fix/typos-grammar-formatting
Fix typos and formatting in documentation and comments
2026-06-04 15:57:48 +12:00
Jakub
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
Jakub
55d164c074 refactor(nav): single GPS-position accessor + reset COG window after a gap
UITask::currentLocation() becomes the one place that reads the
LocationProvider fix. TrailScreen::ownPos, CompassScreen::gpsValid and
NearbyScreen::refresh now delegate to it instead of each duplicating the
getLocationProvider/isValid dance (and the ENV_INCLUDE_GPS guard).

pushCogFix now restarts the course-over-ground ring when the gap since the
previous fix exceeds 15 s: after losing GPS for a while, a window spanning
the stale old fixes would compute a bogus "teleport" heading. The
last-good heading is kept for display continuity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 00:12:15 +02:00
Jakub
8bf050ddb4 feat(nav): standalone Compass in Tools
Tools › Compass shows the device's course over ground (from the GPS COG
ring — no magnetometer) as a rotating arrow in a ring with a north tick,
plus a large degrees + cardinal readout. "No GPS fix" without a fix;
"move to set heading" when stationary (course undefined). Reuses
UITask::currentCourse, so it works without recording a trail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:07:53 +02:00
Jakub
b1dfde5fcb feat(nav): WaypointStore — persistent /waypoints table
Phase 3. Fixed 16-entry table of (lat, lon, ts, label[12]) persisted to
/waypoints with a magic+version header (mirrors TrailStore's format).
Unlike the RAM-only trail, waypoints are loaded in UITask::begin() and
rewritten on change via UITask::saveWaypoints(), so they survive reboots.

add/remove/rename/clear operate on the in-RAM table; the screen layer
(phase 5) drives persistence after each edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:23:11 +02:00
Jakub
11c0b7c4a5 feat(nav): course-over-ground ring in UITask (compass without magnetometer)
Phase 2. A 5-fix GPS ring sampled every ~1 s in UITask::loop, filled
whenever there's a valid fix — independent of trail recording, so the
heading is available to navigation at all times, not only while logging
a trail.

- pushCogFix() rejects gross outliers (a fix implying > 50 m/s since the
  previous one) so a single bad GPS reading can't swing the heading.
- currentCourse(deg) returns the bearing across the window (oldest→newest)
  once the window spans ≥ 6 m of real movement; below that it holds the
  last good heading, and returns false only until the first valid heading
  exists. This is the "two absolute bearings (To / Hdg)" heading source
  for the upcoming nav view.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 08:20:59 +02:00
Jakub
5b25d1eb32 feat(ui/clock): stacked big-digit clock on portrait e-ink
On a tall portrait e-ink panel the clock and lock screens wasted most of
the vertical space on a small inline "HH:MM". Render HH and MM on two
lines in a new size-4 font (built-in GFX scaled 7×, ~42×56 px) so the
digits roughly double in height and fill the narrow width. Wide panels
(OLED, landscape e-ink) keep the classic single-line size-2 layout.

- New shared drawClockTime() helper used by both the Clock home page and
  the lock screen; returns the y below the time block so the date and
  dashboard rows flow beneath it.
- GxEPDDisplay: size 4 = built-in font × BIG_TEXT_SCALE (7); getCharWidth /
  getLineHeight / setTextSize handle it; fontAscender stays 0 (built-in is
  top-left origin).
- Centre the big digits on their visible glyph width — the built-in font
  advances 6 px per char but the glyph is 5 px wide, so getTextWidth over-
  reports by one trailing column and the digits would sit ~half a column
  left of centre. AM/PM rendered one size larger (size 2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 23:32:27 +02:00
Sefinek
cc05ae555a docs: fix typos, grammar, and formatting in FAQ and code comments 2026-06-02 22:12:18 +02:00
taco
e449af1723 bypass auto-shutdown delay for e-ink devices 2026-06-02 18:37:18 +10:00
taco
3ce1cf404e NRF52: disable autoshutdown if powered, add auto-shutdown warning for oled 2026-06-02 00:55:25 +10:00
vanous
8c3622b879 Nearby Nodes: Add Ping 2026-05-30 16:32:46 +02:00
Jakub
fb700bbaad docs+ci: pre-merge polish — README, doc accuracy, workflow
README:
- Fix typo "offical" → "official"
- File names in Firmware Variants table now match workflow output
  (solo-<version>-oled.uf2 / solo-<version>-eink.uf2)
- Un-comment the E-ink Display section (now fully supported)
- Clock Screen: "sensor values" → "data fields" (covers Batt%, Nodes, Msgs)
- Tools Screen: mention nearby nodes and auto-advert in the highlight
- Promote Screenshot Tool to its own top-level section (was buried under
  Contributing); fix `uv run tools/screenshot.py` invocation path

Docs:
- favourites_dial: document the 3rd pin-picker tier (all chat contacts
  fallback) added in 0e0e5b93
- clock_screen: Altitude source clarified — onboard sensor (GPS or
  barometric), not always GPS
- message_screen: blank lines around image tables that were inlined

Code:
- UITask.cpp: rename leftover plus_y/plus_label locals to solo_y/solo_label
  in the splash-screen Solo banner

Workflow:
- workflow_dispatch builds previously labelled firmware with the branch
  name (GITHUB_REF_NAME). Resolve BUILD_VERSION to the tag when run on a
  tag push, else dev-<short-sha> so manual runs produce
  solo-dev-abc1234-oled.uf2 etc.

Cleanup:
- remove stray docs/solo_features/settings_screen/set_scr_1_eink copy.png

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 12:46:29 +02:00