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>
This commit is contained in:
Jakub
2026-07-17 19:38:51 +02:00
parent e439dd0fbe
commit 453dc5e570
20 changed files with 396 additions and 1605 deletions

View File

@@ -95,8 +95,14 @@ pixel sizes** — derive everything from these:
Drawing helpers (all clip/measure for you):
- `drawCenteredHeader(title)` — plain centered title + separator.
- `drawInvertedHeader(label)` — filled title bar (used by detail views).
- `drawCenteredHeader(title, menu_hint=false, menu_open=false)` — plain centered
title + separator.
- `drawInvertedHeader(label, menu_hint=false, menu_open=false)` — filled title
bar (used by detail views).
- Both take an optional `menu_hint`: pass `true` on a screen with a Hold-Enter
context menu to reserve a `` glyph (`menuHintWidth()`/`drawContextMenuHint()`)
in the header, so the menu is discoverable without already knowing the
shortcut; `menu_open` highlights it while the menu is actually up.
- `drawSelectionRow(x, y, w, h, sel)` — the highlight bar behind a list row.
- `drawTextEllipsized(x, y, max_w, str)` — truncates with ``; **use this for
any user string** (names, labels) so long/UTF-8 text can't overrun.
@@ -167,6 +173,19 @@ sensor tokens) via `addPlaceholder()` / `clearPlaceholders()`; the shared
`kbAddSensorPlaceholders()` (`ui-new/SensorPlaceholders.h`) adds only the tokens
the board's sensors actually provide. Expand them with `expandMsg()` at send time.
Two layouts share every grid: **ABC** (one key per letter) and **T9**
(phone-keypad multi-tap — repeated Enter within `KB_T9_TIMEOUT_MS` cycles a
cell's letter group, then its digit). `NodePrefs::keyboard_alt_alphabet` adds a
non-Latin alphabet's own page to the cycle (Latin → alphabet → Symbols →
Latin); each alphabet defines both its ABC grid (`KB_*_CHARS`) and T9 group
table (`KB_T9_GROUPS_*`) so the two layouts always offer the same letters.
Shift is one-shot by default (capitalises the next letter, including whichever
candidate a T9 multi-tap cycle settles on) or Hold-Enter to toggle caps-lock;
Hold-Clear erases the whole field. Hold-Enter elsewhere in the field enters
**cursor mode** (LEFT/RIGHT move the insertion point, UP/DOWN jump to
start/end) so edits/inserts can target any point in the typed text, not just
the end.
`FullscreenMsgView::wrapLines()` is a standalone pixel-accurate word-wrapper
(O(n), variable-width-font aware) reusable by any multi-line layout; it writes
into the shared `s_wrap_trans` / `s_wrap_lines` scratch (single-threaded render,
@@ -223,6 +242,17 @@ with a `blinkOn()` cadence for "leave it on and forget" broadcasts (auto-advert,
Live Share, trail, repeater) — follow that pattern when adding an indicator:
always shown on e-ink, blinking on OLED.
Icons are drawn from a fixed priority-ordered table (`HomeScreen::renderBatteryIndicator()`,
`UITask.cpp`); once the row runs out of horizontal space the loop just stops,
so the lowest-priority icons silently drop first rather than the whole bar
crushing the node name. A blinking icon still reserves its width on the
off-phase of its blink, so the row's layout can't visibly shift width as icons
blink in and out.
Screens with a Hold-Enter context menu (Nodes, Bot, Admin, Diagnostics, …) pass
`menu_hint=true` to their header call (see §2) so a `` glyph advertises the
menu; `KEY_CONTEXT_MENU` (Hold-Enter) opens it.
---
## 7. Input

View File

@@ -18,11 +18,13 @@ The Messages screen is split into three modes — **DMs**, **Channels**, and **R
| :-----------------------: | :-----------------------: |
| ![](./compose_oled.png) | ![](./compose_eink.png) |
Press **Enter** on a contact or channel to open its history, then press **Enter** again (or select an empty send row) to compose a message. Choose between:
Press **Enter** on a contact or channel to open its history, then press **Enter** again (or select the **[+ send]** button, anchored at the right edge of the history) to compose a message. Choose between:
- **Custom message** — opens the on-screen keyboard
- **Q1Q10** — quick reply templates editable in Settings Messages
While typing, **Hold Enter** enters cursor mode (LEFT/RIGHT move the insertion point, UP/DOWN jump to start/end, Enter/Cancel exit) so you can edit or insert in the middle of what you've typed instead of only at the end — see the on-screen keyboard section of the [UI framework guide](../../design/solo_ui_framework.md) for the full key set (Shift, T9 multi-tap, alternate alphabets).
The keyboard supports placeholders that insert live data at send time:
| Placeholder | Value | Availability |
@@ -60,7 +62,7 @@ Posting to a **room server** requires a login handshake first, so the device can
| :-----------------------: | :-----------------------: |
| ![](./history_oled.png) | ![](./history_eink.png) |
Each entry in the history list shows the sender name and a compact age indicator (`3m`, `2h`, `>1d`) in the top-right corner.
Messages are drawn as chat bubbles sized to fit their content, anchored **right** for your own outgoing messages and **left** for incoming ones (like a typical messenger), with the sender name and a compact age indicator (`3m`, `2h`, `>1d`) in the top-right corner of each bubble. The list runs **newest at the bottom** — opening a history starts you at the latest message, and scrolling **up** goes further into the past.
**Short Enter** on a message opens it in fullscreen. **Hold Enter** — on a history row or in fullscreen — opens the same options menu: Reply, plus **Navigate** / **Save waypoint** when the message contains a location (see Fullscreen message view). You don't need to open the message first.
@@ -144,7 +146,7 @@ Joining a new community channel, or creating one to share with others, no longer
| Secret | **LEFT/RIGHT** toggles between two entry modes; **Enter** opens the keyboard for whichever is selected |
- **Passphrase** (default) — type any text; the device hashes it down to the channel's 16-byte secret. Easiest to agree on verbally, the same idea as a room password — two people who type the same passphrase end up on the same channel.
- **Hex key** — type the exact 32-hex-character secret (the format used by channel QR codes, see [QR Codes](../../qr_codes.md)), for joining a channel whose precise secret you were given rather than agreeing on a new passphrase.
- **Hex key** — type the exact 32-hex-character secret (the format used by channel QR codes, see [QR Codes](../../qr_codes.md)), for joining a channel whose precise secret you were given rather than agreeing on a new passphrase. An all-zero secret (`00…0`) is rejected ("Invalid secret") — that value is reserved internally to mark an empty channel slot.
Select **[Save]** to commit. The secret can't be redisplayed once saved (only the derived key is kept) — editing it later means typing a new passphrase or hex key, the same as re-logging into a room with a new password.

View File

@@ -24,7 +24,6 @@ Press **Cancel/Back** to save and return to the home screen.
| Battery | icon / % / V | Display mode for the top-bar battery indicator |
| Clock seconds | show / hide | Hiding reduces OLED refresh from 1 s to 60 s |
| Clock format | 24 h / 12 h | 12 h appends AM/PM |
| Font | Default / Lemon | Default: 5×7 Adafruit with ASCII transliteration; Lemon: native Unicode with pixel-accurate line wrap |
| Display rotation _(e-ink only)_ | 0° / 90° / 180° / 270° | Applied immediately |
| Joystick rotation _(e-ink only)_ | 0° / 90° / 180° / 270° | Rotates input mapping independently of display rotation; useful for custom enclosures |
| Full refresh interval _(e-ink only)_ | off / 5 / 10 / 20 / 30 | Partial refreshes between full clears; reduces ghosting on long sessions |
@@ -101,7 +100,7 @@ The **repeater** mode and its flood filters live on their own screen — see **T
| Setting | Options | Notes |
| -------- | ---------- | -------------------------------------------------------------------------------------------------- |
| Layout | ABC / T9 | On-screen keyboard style. **ABC**: an a-b-c…z grid, one key per letter (the original layout). **T9**: phone-keypad multi-tap — each key is labelled with its **digit** and a letter group (e.g. `2abc`); repeated **Enter** presses cycle through the letters and then the digit itself. Applies to whichever alphabet page is active (see Alphabet below), not just Latin. |
| Alphabet | Latin / Cyrillic / Greek / Polish / Czech / Slovak / German / French / Spanish / Portuguese / Nordic | Which extra (non-Latin) alphabet, if any, joins the keyboard's page cycle. **Latin** (default): only the Latin letters and Symbols pages, as before. Any other choice adds that alphabet's own page to the same **#@/abc** key's cycle (Latin → alphabet → Symbols → Latin) — no separate key to switch scripts. Each Latin-diacritic language (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic) is its own separate page with that language's full, correct set of non-ASCII letters — e.g. Czech gets `á č ď é ě í ň ó ř š ť ú ů ý ž`, German just `ä ö ü ß`. Nordic covers `å ä æ ö ø`, shared across Danish/Norwegian/Swedish since their alphabets only differ in which of those five each one uses. **Greek** covers the 24-letter alphabet plus final sigma (`ς`) but not the tonos stress accents used in proper Modern Greek spelling. Typing in the chosen alphabet needs the **Lemon** font to actually display (Settings Display Font) — the keyboard forces it on temporarily while that page is open even if Font is set to Default, but received messages and everything else still follow your own Font choice. |
| Alphabet | Latin / Cyrillic / Greek / Polish / Czech / Slovak / German / French / Spanish / Portuguese / Nordic | Which extra (non-Latin) alphabet, if any, joins the keyboard's page cycle. **Latin** (default): only the Latin letters and Symbols pages, as before. Any other choice adds that alphabet's own page to the same **#@/abc** key's cycle (Latin → alphabet → Symbols → Latin) — no separate key to switch scripts. Each Latin-diacritic language (Polish, Czech, Slovak, German, French, Spanish, Portuguese, Nordic) is its own separate page with that language's full, correct set of non-ASCII letters — e.g. Czech gets `á č ď é ě í ň ó ř š ť ú ů ý ž`, German just `ä ö ü ß`. Nordic covers `å ä æ ö ø`, shared across Danish/Norwegian/Swedish since their alphabets only differ in which of those five each one uses. **Greek** covers the 24-letter alphabet plus final sigma (`ς`) but not the tonos stress accents used in proper Modern Greek spelling. Every alphabet's letters render natively — the display font (a single unified Unicode font used everywhere on-screen) covers all of them, no separate toggle needed. |
Applies to every on-screen text field (messages, waypoint labels, room passwords, preset names). Earlier releases labelled the grid *QWERTY*; the layout has always been alphabetical, so it is now named **ABC**.

View File

@@ -453,7 +453,15 @@ Each target's Commands toggle is independent — e.g. answer `!ping` in DMs but
<!-- screenshot pending: Diagnostics — live device/mesh stats rows (uptime, rx/tx counters, heap, RSSI/SNR, queue, errors) -->
A single read-only screen of live device and mesh stats, refreshed once a second. On a small OLED the rows scroll with **UP/DOWN**; on a larger e-ink display they all fit at once.
A circular tab carousel of live device and mesh stats, refreshed once a second (same tab idiom as Auto-Reply Bot / Nodes). **LEFT/RIGHT** switches tab; **UP/DOWN** scrolls within it on a small OLED — on a larger e-ink display a tab's rows all fit at once.
| Tab | Shows |
| --- | ----- |
| **Live** | Live counters — see table below. |
| **System** | Static device identity: firmware version + build date, device model, node name, and the active radio parameters. |
| **Font** | A rendering test card — one sample line per script the on-device font claims to cover (Latin, diacritics, Greek, Cyrillic, digits, symbols), so its coverage can be eyeballed directly. |
**Live** tab rows:
| Row | Shows |
| ------------ | -------------------------------------------------------------------------------------------------- |
@@ -472,7 +480,7 @@ A single read-only screen of live device and mesh stats, refreshed once a second
| Queue | Packets waiting in the outbound queue |
| Errors | Radio error flags since boot/reset — `OK`, or tokens `F` (queue full), `C` (CAD timeout), `R` (RX-start timeout) |
The packet counters, **Forwarded** and **Errors** are cumulative since boot. **Hold Enter** opens a one-item *Reset counters* menu (Back dismisses it); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. **Cancel/Back** returns to the Tools list.
The packet counters, **Forwarded** and **Errors** are cumulative since boot. On the **Live** tab, **Hold Enter** opens a one-item *Reset counters* menu (Back dismisses it); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. **Cancel/Back** returns to the Tools list.
The counters make the repeater behaviour observable: **Forwarded** confirms the node is actually relaying (not just configured to), and **Pool free** / **Queue** show whether forwarding is exhausting the packet pool. See **Tools Repeater** for the relaying options.
@@ -526,12 +534,13 @@ Send commands to a **repeater/room server you have admin permission on** — the
| Tab | Rows |
| --- | ---- |
| **System** | Name, Owner info, Admin password |
| **Radio** | Radio (freq, bandwidth, spreading factor, coding rate), TX power |
| **Radio** | Frequency, Bandwidth, Spreading factor, Coding rate, TX power |
| **Routing** | Repeat, Advert interval, Flood advert interval, Max hops |
| **Actions** | Send advert, Send zero-hop advert, Sync clock, Reboot, **Custom command...** |
**Enter** on a row does one of three things, depending on the field:
- Most **System/Radio/Routing** rows first **fetch** the node's current value, then open the keyboard **pre-filled** with it to edit — submitting sends the change. If the fetch fails or times out, the keyboard still opens (blank), so the value can be set blind.
**Enter** on a row does one of four things, depending on the field:
- **Name / Owner info** first **fetch** the node's current value, then open the keyboard **pre-filled** with it to edit — submitting sends the change. If the fetch fails or times out, the keyboard still opens (blank), so the value can be set blind.
- **Radio and Routing rows** are typed, not free text: **Repeat** is an ON/OFF toggle; **Advert interval / Flood advert interval / Max hops / TX power** are number steppers (**LEFT/RIGHT** to adjust, within that field's valid range); **Frequency** uses the same digit-by-digit cursor editor as Settings' own Radio screen (**LEFT/RIGHT** moves between digits, **UP/DOWN** changes the selected one); **Bandwidth / Spreading factor / Coding rate** step through their valid discrete LoRa values with **LEFT/RIGHT**. All four Radio-tuple fields (Frequency/Bandwidth/SF/Coding rate) fetch and re-send the same underlying `radio` value together — editing any one of them still only overwrites that one, the other three round-trip unchanged. **Enter** sends the change; **Cancel** discards it and returns to the row list without sending anything.
- **Admin password** has no fetch (there's no way to read a password back) — it opens straight to a blank keyboard.
- **Actions** (Reboot, Send advert, …) send immediately, no editing step.
- **Custom command...** (last row of Actions) opens the same free-text entry for anything not covered above — up to 160 characters, see the linked reference for the full grammar. The keyboard's **{}** key doubles as command completion here: it lists commands matching whatever's typed since the last space (narrowing as you type), and picking one completes that word instead of just inserting after it.