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.
This commit is contained in:
Jakub
2026-07-10 16:01:42 +02:00
parent cf3c2c0353
commit dfb993de53
9 changed files with 424 additions and 56 deletions
@@ -70,6 +70,7 @@ class SettingsScreen : public UIScreen {
// Keyboard section
SECTION_KEYBOARD,
KEYBOARD_TYPE,
KEYBOARD_ALPHABET,
// Contacts section
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER,
// Messages section
@@ -557,6 +558,10 @@ class SettingsScreen : public UIScreen {
display.print("Type");
display.setCursor(valCol(display), y);
display.print((p && p->keyboard_type) ? "T9" : "ABC");
} else if (item == KEYBOARD_ALPHABET) {
display.print("Alphabet");
display.setCursor(valCol(display), y);
display.print(NodePrefs::keyboardAlphabetLabel(p ? p->keyboard_alt_alphabet : 0));
} else if (item == BATT_DISPLAY) {
display.print("BattDisp");
display.setCursor(valCol(display), y);
@@ -886,6 +891,14 @@ public:
_dirty = true;
return true;
}
if (_selected == KEYBOARD_ALPHABET && p && (left || right || enter)) {
int idx = p->keyboard_alt_alphabet;
if (right || enter) idx = (idx + 1) % NodePrefs::KB_ALPHABET_COUNT;
else if (left) idx = (idx + NodePrefs::KB_ALPHABET_COUNT - 1) % NodePrefs::KB_ALPHABET_COUNT;
p->keyboard_alt_alphabet = (uint8_t)idx;
_dirty = true;
return true;
}
if (_selected == DM_RESEND && p) {
int n = p->dm_resend_count;
if (right || enter) n = (n + 1) % 6; // 0..5, wraps