Commit Graph

72 Commits

Author SHA1 Message Date
Jakub
afdfca6f9b feat(keyboard): selectable main/additional script for the on-screen keyboard
Page 0 was hardcoded to Latin -- Cyrillic/Greek could only ever be the
second, cycled-to page. Settings > Keyboard's Alphabet row splits into
Main (which script the keyboard opens on by default) and Additional
(the second one reached via #@/abc), so a Cyrillic/Greek typist can make
their own script the default instead of always landing on Latin first.
Setting Additional equal to Main collapses back to a 2-page cycle (that
script + Symbols), same rule the old Latin-hardcoded design already used
implicitly.

KeyboardWidget.h: cellStr()/t9GroupStr() now dispatch through
scriptCellStr()/scriptT9GroupStr(), treating Latin as an ordinary peer of
Cyrillic/Greek instead of a special case; scriptHint() replaces
altAlphabetHint() so the #@/abc key's "next page" hint is correct
regardless of which script that lands on; the accent popup's gating
checks the current page's actual script instead of assuming page 0 is
always Latin. Removed the now-dead pageIsAltAlphabet().

NodePrefs gains keyboard_main_alphabet (schema sentinel 0xC0DE001F ->
0xC0DE0020, same append-at-tail/clamp-on-load pattern as every prior
schema growth this file uses). Verified via a real build that the new
field lands in existing tail padding -- sizeof(NodePrefs) is unchanged
at 2712.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 20:50:41 +02:00
Jakub
453dc5e570 feat(admin): typed Radio/Routing field editors; pre-release audit fixes
Tools > Admin: split the Radio tab's single "f,bw,sf,cr" comma-string
field (free-text keyboard) into 4 independent, type-appropriate rows --
Frequency (same digit-cursor DigitEditor Settings/Repeater use locally),
Bandwidth/Spreading factor/Coding rate (discrete-set LEFT/RIGHT stepping),
plus TX power and the 3 Routing numeric fields as number steppers and
Repeat as an ON/OFF toggle. Edits happen locally (zero mesh traffic per
keystroke); only Enter sends one combined `set`, Cancel sends nothing.

Full pre-release audit of all 16 commits since v1.22 (5 parallel focus
areas: display/font, keyboard/messages, Nodes/Admin, Bot/channels,
UITask+NodePrefs schema) turned up and fixed:

- Admin: fetched FK_NUMBER values weren't clamped to the field's range,
  so a value already out-of-range could get stuck unreachable; fixed
  commit-time float format noise (%.6f -> %.3f); Cancel/failed-login
  always returned to the Nodes picker even when Admin was opened
  directly from a node's Hold-Enter action -- now returns to wherever
  it was actually opened from (AdminScreen::_from_picker).
- Keyboard: one-shot Shift was consumed after the *first* T9 multi-tap,
  so cycling to the 2nd/3rd candidate always came out lowercase --
  fixed by caching the cycle's caps state (t9_caps).
- Messages: history is numbered newest-first, so a message arriving
  while scrolled up to an older one silently relabeled the view onto a
  different message -- selection now shifts with the insert.
- Channels: onChannelRemoved() didn't clear ch_notif_override/
  ch_notif_muted/ch_fav_bitmask despite its own contract comment
  requiring it (now far more reachable via the on-device Delete);
  an all-zero hex secret silently self-deleted the channel it was
  just saved into (collides with the empty-slot sentinel) -- rejected.
- Room login: isRoomLoggedIn() indexed the login-tracking ring
  directly instead of via its head offset, silently wrong once the
  ring wraps (8+ rooms/session) -- the new on-device Logout depends
  on this being right.
- Font/display: removed the now fully-inert Settings > Display > Font
  toggle and the dead LemonFont.h (retired by the earlier misc-fixed
  font unification, zero remaining includes); fixed a copy-pasted
  "5x7" comment (font is 6x9), two meaningless dead ternaries, and an
  OLED/e-ink inconsistency in the undefined-glyph fallback box offset.
- AdminField's `kind`/bounds fields no longer rely on default member
  initializers inside aggregate-init: this toolchain's actual nRF52
  build (unlike env:native) has no explicit -std= override, so it
  predates C++14's aggregate-with-default-member-initializer rule.
  Given an explicit constructor instead -- portable regardless of
  standard, all existing field-table literals unchanged.

Docs updated to match: tools_screen.md (Diagnostics as a 3-tab
carousel, was documented as one flat screen), message_screen.md
(chat bubbles, newest-at-bottom, cursor mode, secret validation),
settings_screen.md (dropped the dead Font row), solo_ui_framework.md
(header menu_hint signatures, icon priority-drop, KeyboardWidget's
T9/alphabets/cursor-mode). release-notes.md gains the v1.23 section
covering all of the above plus the other 15 commits since v1.22.

Build-verified on WioTrackerL1_companion_solo_dual.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 19:38:51 +02:00
Jakub
56aaff1f76 refactor(admin): make Admin remote-only, move device settings to Settings
A device administering itself via the Admin tool read as awkward, so the
local mode is gone and its options relocated to Settings.

- AdminScreen is remote-only: drop the CHOOSER phase, _local_mode, the
  LocalField/LocalKind model and activateLocalField/commitLocalField.
  Phases collapse to LOGIN/COMMAND/REPLY, entered only via startFor().
- Tools > Admin opens straight to the Nodes picker (ACT_ADMIN ->
  pickAdminTarget()). Backing out of a command screen returns to that
  picker; the picker's Cancel returns to Tools. gotoAdminScreen() removed.
- Settings > System gains Name (keyboard-edited node_name) and Reboot
  (action row, placed last so it isn't the default cursor). Radio + TX
  power were already in Settings; Send advert is the home ADVERT page.

Docs: tools_screen.md Admin section rewritten remote-only; settings_screen.md
System table gains Name + Reboot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 09:10:24 +02:00
Jakub
dfb993de53 feat(clock,keyboard): alarm repeat + non-Latin keyboard alphabets
Alarm repeat (Clock Tools):
- NodePrefs::alarm_repeat_mask (weekday bitmask, struct tm::tm_wday
  convention) — new Repeat row cycles Off/Daily/Weekdays/Weekends.
  computeAlarmNextFire() scans the next 7 days for a matching weekday when
  set; evaluateAlarm() only clears alarm_on (one-shot) when the mask is
  empty, otherwise re-arms. mask==0 is byte-for-byte the original one-shot
  behaviour, so existing users see no change.

On-screen keyboard alphabets (Settings > Keyboard > Alphabet):
- KeyboardWidget reworked from single-byte ASCII cells to UTF-8 codepoints
  (insertion, backspace and T9 in-place cycling all codepoint-aware now, so a
  multi-byte character is never split) — see kbApplyCapsUtf8/kbUtf8Len/
  kbUtf8CharAt/kbUtf8LastCharBytes.
- Cyrillic, Greek and an Extended Latin set (Polish/Czech/Slovak/German/
  French/Spanish/Nordic diacritics) join the keyboard's existing #@/abc page
  cycle (Latin -> alt alphabet -> Symbols -> Latin) — no new key needed.
  NodePrefs::keyboard_alt_alphabet picks which one, if any, is active.
- Lemon font is forced on transiently inside KeyboardWidget::render() (saved
  and restored every call) whenever the alt-alphabet page is showing or
  already-typed text has non-ASCII bytes, so composing is visible even if
  Font is set to Default.
- Each script's caps-shift rule is distinct and documented in
  kbApplyCapsUtf8: flat -0x20 for ASCII/Cyrillic/Greek (with the ё/ς
  exceptions), flat -0x20 for Latin-1 (à-þ), and an adjacent-pair -1 for
  Latin Extended-A (verified against every character actually used, not a
  blanket rule for that whole Unicode block).

NodePrefs schema: two tail-appended fields this session (alarm_repeat_mask,
keyboard_alt_alphabet), SCHEMA_SENTINEL 0xC0DE001B -> 0xC0DE001D,
sizeof(NodePrefs) 2496 -> 2504 (verified via a standalone host compile).

Docs: Clock Tools' Repeat row, Settings > Keyboard > Alphabet, and the
Rooms keyboard note (no longer ASCII-only once an alphabet is enabled).

Not build-verified — no PlatformIO toolchain available this session. Caps
mappings cross-checked against Python's Unicode case tables; UTF-8 literal
bytes verified at the byte level.
2026-07-10 16:01:42 +02:00
Jakub
a56d7079dd feat(ui): retire Recent adverts home page, folded into Nodes
The Recent adverts carousel page was a read-only name+age list of recently-heard
nodes — now covered by the Nodes screen (which folds getRecentlyHeard() non-contacts
into its list). Remove it from the UX:

- isPageVisible() hard-hides HomePage::RECENT, so it drops out of the carousel for
  everyone (default and custom page orders, existing users included).
- Delete the dead RECENT render block, the nav "Recent adverts" alert and the unused
  AdvertPath recent[] member.
- Drop HOME_RECENT from Settings > Home Pages so there's no dead toggle.

HPB_RECENT bit index is left intact, so persisted page_order / home_pages_mask stay
valid (a stored RECENT entry is simply skipped) — no migration, no schema bump.

Both solo envs build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 01:53:02 +02:00
Jakub
e63e260eab fix(ui): drop {loc}/{time} placeholders when naming a radio preset
The shared on-screen keyboard seeds {loc}/{time} placeholders on begin()
for message composition, but they make no sense in a radio-preset name.
Clear them right after opening the keyboard from the "+ Save current..."
flow in both Settings > Radio and Tools > Repeater; the message-slot
editor still keeps them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 11:02:43 +02:00
Jakub
3419e590fe fix(keyboard): rename QWERTY label to ABC (layout is alphabetical, not QWERTY)
The on-screen keyboard grid has been a-b-c...z alphabetical order since
it was first introduced, never an actual QWERTY row layout. The
"QWERTY" name only appeared with the T9 option (needed something to
contrast it against) and was inaccurate from the start.
2026-07-02 18:33:40 +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
cc88e89a64 refactor(companion): extract drawRowSelection() for the canonical list-row bar
Every drawList/AccordionList row opened with the same hand-written
display.drawSelectionRow(0, y-1, width-reserve, lineStep-1, sel) line —
14 copies of the same geometry and magic offsets. Add a drawRowSelection(d,
y, sel, reserve) helper in icons.h next to drawList and route the canonical
sites through it (Bot/LiveShare/Locator/Repeater/Settings/Tools/Nearby/
QuickMsg/Waypoints).

Rows that intentionally differ (full-width DashboardConfig/QuickMsg, the
keyboard/ringtone grids) keep their explicit drawSelectionRow() call — the
helper is opt-in rather than baked into drawList, so legitimate variants
aren't forced into one geometry. Framework doc updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:53:18 +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
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
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
a2af1a7409 refactor(ui): share radio params editor between Settings and Repeater
The manual Freq/SF/BW/CR editing was duplicated near-verbatim between
Settings › Radio and Tools › Repeater — same digit-by-digit Freq editor,
same SF/BW/CR step bounds, differing only in the target fields and the
apply call. Extract RadioParamsEditor (freq DigitEditor + static SF/BW/CR
steppers), with no dependency on UITask or the radio driver: the screen
passes the freq bounds in, the same way RadioPresetPicker stays decoupled.
Completes the preset-picker consolidation.

Behaviour preserved: a step at the range limit changes nothing and isn't
consumed (falls through), as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:09:35 +02:00
MarekZegare4
0e333aeb5e refactor(ui): share radio preset picker between Settings and Repeater
Settings › Radio and Tools › Repeater had near-identical preset-picker
logic (name/list-index lookup, save/delete, popup build, selection
handling) plus their own copy of the nearest-bandwidth search — only the
target fields (companion params vs. dedicated repeater profile) and the
apply call differed.

Extract the shared logic into RadioPresetPicker (a Target of field
pointers + a Result the screen acts on), with no dependency on UITask so
each screen keeps its own apply/dirty/keyboard/alert handling. Move the
nearest-bandwidth search to nearestBwIndex() in RadioPresets.h. Removes
~6 duplicated methods and 4 state fields from each screen.

Verified: WioTrackerL1_companion_solo_dual builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 22:32:47 +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
92643d1721 fix(ui): scale trail-map markers for e-ink, add wrap-around nav
Trail map dots/diamonds/cross/start markers and the north indicator were
drawn as raw unscaled pixels, so they shrank to near-invisible specks on
landscape e-ink's larger font scale; they now route through the mini-icon
framework (icons.h) like the rest of the UI, and the grid intersection dots
scale with it too. The keyboard's preview/grid separator had the same bug
(hardcoded 1px) and now uses display.sepH().

Also makes UP/DOWN (and the keyboard's LEFT/RIGHT/UP/DOWN) wrap top<->bottom
across every list, menu and form selector, matching the behavior PopupMenu
already had.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 09:16:57 +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
9da730e3b6 refactor(ui): shared drawList + header + key-decode helpers
- drawList(): scrollable list skeleton (visible window, keep-sel-in-view,
  scroll-column reserve, per-row callback, indicator). Migrate Tools, Bot,
  Nearby, Waypoints list and the three QuickMsg lists onto it; centralises the
  scroll-clamp/reserve/indicator that each had open-coded (and inconsistently).
- drawCenteredHeader(): centred title + separator, replacing the duplicated
  two-liner across ~10 screens (incl. QuickMsg, which used width()/2 spacing and
  was missed before); unifies the separator at headerH()-sepH().
- keyIsPrev()/keyIsNext() in UIScreen.h for the LEFT||PREV / RIGHT||NEXT idiom;
  applied to 6 screens, behaviour-preserving. Cancel/context-menu stay
  per-screen (they mean different things on different screens).
- rotLabel() dedups the 0/90/180/270 array in Settings.
- Unify selection-row height to lineStep()-1 (Tools/Bot matched the rest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 21:23:00 +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
6e4177f3d6 feat(ui): proportional scrollbar with mini-icon caps; reclaim row space
Replace the text ^/v scroll arrows with a font-scaling indicator drawn in
icons.h: a proportional thumb between fixed up/down triangle mini-icon caps.
drawScrollIndicator() lives in a ~5px right-edge column and scales via
miniIconScale (1x OLED, 2x landscape e-ink).

- Mini-icons: ICON_SCROLL_UP/DOWN; miniIconDrawTop (exact placement) and
  miniIconDrawHalo (shape-hugging 1px DARK halo, clip_top to spare the header
  separator) so caps stay visible on the LIGHT selection bar without a box.
- Thumb is 3*s wide so it centres on the triangle column; caps are static end
  markers (always drawn) so neither vanishes at the extremes.
- scrollIndicatorReserve(): right-edge gutter (0 when the list fits). Content,
  the selection bar and the message-history bubbles are all narrowed by it so
  nothing renders under the scrollbar. Portrait e-ink wrap width subtracts the
  reserve in both the box-height pass and render so wrapped text can't spill.
- Drop the redundant ">" selection marker (the highlight bar already shows
  selection) and shift rows to x=2, reclaiming left-edge space.
- Wire the indicator into every scrollable list (Bot, Settings, Nearby, Tools,
  Trail, QuickMsg lists + DM/channel history) and add one to WaypointsView,
  which scrolled with no indicator before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 14:41:46 +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
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
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
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
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
daaa5b1503 fix(messages): allow full-length text on the compose/send paths too
After widening the history buffers, the compose side was still the
bottleneck: the keyboard capped input at 139 bytes and the quick-message
expand buffers were 140. Raise KB_MAX_LEN to 160 (MeshCore's MAX_TEXT_LEN)
and size the expand/compose buffers to MSG_TEXT_BUF, so a message can be
typed, expanded, sent, stored and displayed at the full protocol length on
every path.

The custom-message editor now passes an explicit max bound to its 140-byte
store, so the wider keyboard buffer can't overflow it; the bot editor
already passed per-field maxima.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 12:15:30 +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
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
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>
2026-05-26 15:49:50 +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
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
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
30b81b6d51 feat(ui): Trail phase 5 — Settings › GPS gains trail interval + min-delta
Two new entries in the existing GPS section of SettingsScreen, gated by
the same ENV_INCLUDE_GPS guard. LEFT/RIGHT cycles through the option
table from Trail.h:
- "Trail int" : 30 s / 10 s / 20 s / 1 min / 5 min / 15 min
- "Trail dist": 5 m / 10 m / 25 m / 100 m

Changes are stored to p->trail_interval_idx / trail_min_delta_idx and
take effect on the next sampling tick in UITask::loop (already gated on
those fields), so no separate apply* helper is needed. _dirty triggers
the normal savePrefs on settings exit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 10:19:47 +02:00
Jakub
aedb7f6f5d fix(ui): Favourites migration even when page_order is full
ensurePageOrderInit only inserted FAVOURITES when len < PAGE_ORDER_LEN.
Older saves with all 11 slots filled (default order with GPS + SENSORS)
hit the full-array branch and were left without FAVOURITES in the
explicit order, so the page was shown via the missing-page fallback at
end of nav but movePageInOrder couldn't reorder it.

Now drop the last entry (typically SHUTDOWN, which the same fallback
already re-appends at end of nav) to make room. End result: FAVOURITES
sits after CLOCK, reorderable; nav layout matches the new default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 23:24:13 +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
900f2d59e9 refactor(ui): tag SettingsScreen ifdefs with semantic FEAT_* macros
Extends Features.h with preprocessor flags (FEAT_BRIGHTNESS_SETTING,
FEAT_CLOCK_SECONDS_SETTING, FEAT_DISPLAY_ROTATION_SETTING,
FEAT_JOYSTICK_ROTATION_SETTING, FEAT_FULL_REFRESH_SETTING) since
constexpr can't gate enum members or class fields. SettingsScreen.h
now uses #if FEAT_X instead of #if defined(EINK_DISPLAY_MODEL) — grep
on the feature name finds the toggle and every use site, and the
condition names *what* the gate controls.

Driver headers (DisplayDriver.h, GxEPDDisplay.h) keep raw
EINK_DISPLAY_MODEL — they're below the companion_radio layer and the
ifdef there gates on the build's panel type, not a UI feature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 22:20:42 +02:00
Jakub
e16ecbd690 style(ui): sprintf → snprintf with sizeof in UI render paths
Mechanical sweep across UITask.cpp and SettingsScreen.h. All current
format strings already fit their buffers, but a future format change
would silently overflow with sprintf. snprintf with sizeof makes the
buffer bound explicit and traceable to the array declaration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:35:49 +02:00
Jakub
d981f93e6e refactor(ui): drawSelectionRow helper for the 5-line invert/restore pattern
Eight screens repeated the same if/else block to invert the row
background for selected items. Add DisplayDriver::drawSelectionRow that
sets LIGHT, optionally fills the rect, and leaves the colour as DARK
when sel (so the caller's text renders inverted). 61 lines removed
across SettingsScreen, KeyboardWidget (cells + special row), ToolsScreen,
DashboardConfigScreen, BotScreen, RingtoneEditorScreen (note slots +
menu), NearbyScreen, QuickMsgScreen (4 list views).

Card-style rows (QuickMsg hist, NearbyScreen discover, RingtoneEditor
notes display) keep their original drawRect/partial-fill outline for
unselected — they don't match the simple-invert pattern.

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