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.
Two duplications surfaced by a framework-consistency pass:
- KeyboardWidget applied the a-z shift-uppercase at five draw/commit sites
with an inline `if (caps && ch >= 'a' && ch <= 'z')`. Fold them into one
`kbApplyCaps(ch, caps)` helper.
- The favourites grid drew the empty "+" tile from two branches (the gone-slot
prune and the always-empty slot). Route both through a single `has_contact`
flag so the "+" and the trailing selection-colour reset each live at one site.
No behaviour change. Builds green: WioTrackerL1_companion_solo_dual.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
T9 keys now read like a phone keypad — each cell is labelled <digit><group>
(e.g. 2abc), with the digit matching what the multi-tap cycle lands on after
the letters. No separator space: the widest group (".,!?'-") plus the digit
already fills a narrow 128px OLED cell at 3 columns.
Also fills in the v1.22 release notes (T9 keyboard, trail auto-save, Clock
Tools in Tools, Map/Shutdown reorderable, battery-read fix, the UI-audit fix
batch, discover-dedup, favourites self-heal, preset-name placeholder, OLED
frame-skip, e-ink full-refresh) and syncs the solo-feature docs: new Keyboard
settings section, Trail Auto-save row, and Clock Tools reachable from Tools.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
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>
- Unify on the shared keyboard: WaypointsView drives UITask::keyboard()
instead of its own instance; openKb() clears {loc}/{time} for literal
fields (labels, coordinates). Reclaims the duplicate widget from the heap.
- Icon glyphs on the special row: caps ⇧, space ⎵ (two halves), delete ⌫,
OK ✓ (reuses ICON_CHECK); {} and the page toggle stay text.
- Second character page (symbols), toggled by a "#@"/"abc" key. Tidy the
letters page: drop the duplicate grid space, add the comma, group
punctuation as . , ! ?
- Harden the preview buffers (KB_PREVIEW_CAP) against very wide displays.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
KeyboardWidget printed the text preview directly via display.print()
without calling translateUTF8ToBlocks first. When a reply prefix
contained a UTF-8 sender name (e.g. Cyrillic), raw multi-byte sequences
reached the display driver on non-Lemon renders.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GxEPDDisplay: fontAscender only applies to sz=1 with Lemon; sz=2 (NULL font)
was incorrectly getting a 16px cursor offset, causing clock text to render
16px lower than expected and overlap the date line
- KeyboardWidget: compute cell_h from available screen height instead of lh+1;
tighten sep_y to lh so the grid fits within 122px even with Lemon font (lh=20)
- SettingsScreen renderBar: constrain box size to available width so the 5th
buzzer-volume square doesn't overflow past the right edge with Lemon font
- NearbyScreen discover detail: manually truncate b64 public key by charWidth
to guarantee one-line rendering; use dynamic step = (height-hdr)/5 so
Status line doesn't fall off the bottom of the screen
- NearbyScreen contacts detail: merge dist+az into one line and use dynamic
step to fit 5 lines within the display; removes the off-screen Seen: row
- AutoAdvertScreen: replace lineStep() gap before hints with 4px fixed gap
so both hint lines fit within the display with large fonts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KeyboardWidget: replace inline placeholder overlay with PopupMenu
- UITask QuickMsgScreen: replace inline CONTACT_PICK/CHANNEL_PICK context
menus with PopupMenu; notif label stored in _ctx_notif_item at open time
- BotScreen: clear placeholder list for trigger field (expansion tokens
would never match incoming literal text)
- MsgExpand: fix APPEND macro off-by-one (oi+_l < out_len-1 → < out_len)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
expandMsg now expands {temp}, {hum}, {pres}, {batt}, {alt}, {lux}, {dist},
{co2} from live sensor readings. The [{}] picker in the on-screen keyboard
shows only sensors currently detected by SensorManager and scrolls when
more than 3 items fit the overlay.
New SensorPlaceholders.h helper keeps sensor-to-keyboard wiring out of
UITask.cpp. MsgExpand.h handles all expansion logic centrally; MyMeshBot
and QuickMsgScreen pass &sensors and batt_volts to each call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three keyboard instances (Settings, QuickMsg, Bot) now show plain
text + cursor without any field-name prefix. Removes the label field
from KeyboardWidget entirely — the screen title already indicates context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- preview[] increased from 32 to 40 bytes to safely fit max label (15 chars)
+ 20 chars text + cursor + null without snprintf truncation
- KEY_CONTEXT_MENU (long-press ENTER) now treated as CANCEL in keyboard
input handler, restoring BotScreen behavior lost in the refactor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three keyboard implementations (SettingsScreen, QuickMsgScreen, BotScreen)
now share a single KeyboardWidget struct defined in KeyboardWidget.h, eliminating
~480 lines of duplicated render/input logic. Also fixes the SettingsScreen bug
where navigation keys were checked before the placeholder overlay, causing cursor
movement when the overlay was open.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>