mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
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:
@@ -482,6 +482,19 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
rd(&_prefs.trail_autosave_lowbatt, sizeof(_prefs.trail_autosave_lowbatt));
|
||||
if (_prefs.trail_autosave_lowbatt > 1) _prefs.trail_autosave_lowbatt = 0;
|
||||
|
||||
// → 0xC0DE001C: append alarm_repeat_mask at the tail. A pre-0x1C file has the
|
||||
// old sentinel bytes / EOF here; any value that isn't one of the four presets
|
||||
// clamps to 0 (no repeat / one-shot), matching the original alarm behaviour
|
||||
// upgraders already had.
|
||||
rd(&_prefs.alarm_repeat_mask, sizeof(_prefs.alarm_repeat_mask));
|
||||
if (NodePrefs::alarmRepeatIdxForMask(_prefs.alarm_repeat_mask) == 0) _prefs.alarm_repeat_mask = 0;
|
||||
|
||||
// → 0xC0DE001D: append keyboard_alt_alphabet at the tail. A pre-0x1D file has
|
||||
// the old sentinel bytes / EOF here; clamp anything out of range to 0 (Latin
|
||||
// only), matching the keyboard's original (Latin-only) behaviour.
|
||||
rd(&_prefs.keyboard_alt_alphabet, sizeof(_prefs.keyboard_alt_alphabet));
|
||||
if (_prefs.keyboard_alt_alphabet >= NodePrefs::KB_ALPHABET_COUNT) _prefs.keyboard_alt_alphabet = 0;
|
||||
|
||||
// Schema sentinel: bumped on layout changes. Mismatch means an older file
|
||||
// (or a different schema); rd() already zero-inits any fields not present,
|
||||
// so we just log it — next savePrefs writes the current sentinel.
|
||||
@@ -668,6 +681,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.page_order[NodePrefs::PAGE_ORDER_LEN_V1],
|
||||
NodePrefs::PAGE_ORDER_LEN - NodePrefs::PAGE_ORDER_LEN_V1);
|
||||
file.write((uint8_t *)&_prefs.trail_autosave_lowbatt, sizeof(_prefs.trail_autosave_lowbatt));
|
||||
file.write((uint8_t *)&_prefs.alarm_repeat_mask, sizeof(_prefs.alarm_repeat_mask));
|
||||
file.write((uint8_t *)&_prefs.keyboard_alt_alphabet, sizeof(_prefs.keyboard_alt_alphabet));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is
|
||||
// the one we check: once the flash fills, writes return 0, so a good
|
||||
|
||||
@@ -272,29 +272,89 @@ struct NodePrefs { // persisted to file
|
||||
// Index into gpsAvgSecs(). [Tools › Trail › Settings › Mark avg]
|
||||
uint8_t gps_avg_idx;
|
||||
|
||||
// Alarm clock — a single one-shot wake alarm, configured from the Clock page
|
||||
// (Enter › Alarm). Stored as a local time-of-day; the actual fire instant is
|
||||
// (re)computed as an absolute time in tickBackground(), which is what makes it
|
||||
// robust to RTC re-syncs: the mesh (every inbound packet), the companion app,
|
||||
// GPS and the CLI can all jump getCurrentTime() at any moment, but an absolute
|
||||
// target instant stays correct across small corrections and still fires (late)
|
||||
// if the clock jumps over it. Fires once, then alarm_on clears. The minutnik
|
||||
// (countdown) and stoper (stopwatch) are runtime-only and not persisted.
|
||||
uint8_t alarm_on; // 0=off (default), 1=armed (one-shot)
|
||||
// Alarm clock — a wake alarm, configured from the Clock page (Enter › Alarm).
|
||||
// Stored as a local time-of-day; the actual fire instant is (re)computed as an
|
||||
// absolute time in tickBackground(), which is what makes it robust to RTC
|
||||
// re-syncs: the mesh (every inbound packet), the companion app, GPS and the
|
||||
// CLI can all jump getCurrentTime() at any moment, but an absolute target
|
||||
// instant stays correct across small corrections and still fires (late) if
|
||||
// the clock jumps over it. With alarm_repeat_mask == 0 it's one-shot: fires
|
||||
// once, then alarm_on clears. A non-zero mask instead re-arms it for the next
|
||||
// matching weekday (see UITask::computeAlarmNextFire/evaluateAlarm) and
|
||||
// alarm_on stays set. The minutnik (countdown) and stoper (stopwatch) are
|
||||
// runtime-only and not persisted.
|
||||
uint8_t alarm_on; // 0=off (default), 1=armed
|
||||
uint8_t alarm_hour; // 0-23, local time
|
||||
uint8_t alarm_min; // 0-59
|
||||
// Repeat days as a bitmask, bit i = 1<<tm_wday (Sunday=0 .. Saturday=6, per
|
||||
// struct tm — matches what computeAlarmNextFire() already reads from
|
||||
// gmtime()). 0 = no repeat (one-shot, the original/default behaviour).
|
||||
// Cycled through the presets below via the alarm's Repeat row.
|
||||
static const uint8_t ALARM_REPEAT_NONE = 0x00; // one-shot (default)
|
||||
static const uint8_t ALARM_REPEAT_DAILY = 0x7F; // every day
|
||||
static const uint8_t ALARM_REPEAT_WEEKDAYS = 0x3E; // Mon-Fri
|
||||
static const uint8_t ALARM_REPEAT_WEEKENDS = 0x41; // Sat+Sun
|
||||
static const uint8_t ALARM_REPEAT_COUNT = 4;
|
||||
static uint8_t alarmRepeatMaskForIdx(uint8_t idx) {
|
||||
static const uint8_t M[ALARM_REPEAT_COUNT] =
|
||||
{ ALARM_REPEAT_NONE, ALARM_REPEAT_DAILY, ALARM_REPEAT_WEEKDAYS, ALARM_REPEAT_WEEKENDS };
|
||||
return M[idx < ALARM_REPEAT_COUNT ? idx : 0];
|
||||
}
|
||||
// Reverse lookup for display: an arbitrary mask (e.g. loaded from a future
|
||||
// custom-day picker) that doesn't match a preset just reads as "Off" here —
|
||||
// it still fires correctly, computeAlarmNextFire() reads the raw mask.
|
||||
static uint8_t alarmRepeatIdxForMask(uint8_t mask) {
|
||||
switch (mask) {
|
||||
case ALARM_REPEAT_DAILY: return 1;
|
||||
case ALARM_REPEAT_WEEKDAYS: return 2;
|
||||
case ALARM_REPEAT_WEEKENDS: return 3;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
static const char* alarmRepeatLabel(uint8_t idx) {
|
||||
static const char* L[ALARM_REPEAT_COUNT] = { "Off", "Daily", "Weekdays", "Weekends" };
|
||||
return L[idx < ALARM_REPEAT_COUNT ? idx : 0];
|
||||
}
|
||||
|
||||
// On-screen keyboard layout, shared across every text-entry screen (Settings >
|
||||
// Keyboard). 0=ABC grid, alphabetical order (default), 1=T9 multi-tap
|
||||
// (phone-keypad groups, cycled with repeated Enter presses — see KeyboardWidget.h).
|
||||
uint8_t keyboard_type;
|
||||
|
||||
// Additional (non-Latin) keyboard alphabet, orthogonal to keyboard_type above
|
||||
// — either layout style (ABC grid or T9) can show any alphabet's characters.
|
||||
// 0 = Latin only (default): the keyboard's existing #@/abc page-cycle key
|
||||
// toggles between just the Latin letters page and the symbols page, as
|
||||
// today. A non-zero value adds that alphabet's own page to the same cycle
|
||||
// key (Latin → alphabet → symbols → Latin), so composing in it needs no new
|
||||
// key, just Settings › Keyboard › Alphabet to pick which one is available.
|
||||
// See KeyboardWidget.h for the per-alphabet grids (KB_CYRILLIC_CHARS etc.)
|
||||
// and Lemon font (src/helpers/ui/LemonFont.h) for on-screen rendering —
|
||||
// every alphabet here must be in Lemon's U+0020-04FF range.
|
||||
static const uint8_t KB_ALPHABET_LATIN_ONLY = 0;
|
||||
static const uint8_t KB_ALPHABET_CYRILLIC = 1;
|
||||
static const uint8_t KB_ALPHABET_GREEK = 2;
|
||||
static const uint8_t KB_ALPHABET_EXT_LATIN = 3; // Polish/Czech/Slovak/German/French/etc. diacritics
|
||||
static const uint8_t KB_ALPHABET_COUNT = 4;
|
||||
static const char* keyboardAlphabetLabel(uint8_t idx) {
|
||||
static const char* L[KB_ALPHABET_COUNT] = { "Latin", "Cyrillic", "Greek", "Ext.Latin" };
|
||||
return L[idx < KB_ALPHABET_COUNT ? idx : 0];
|
||||
}
|
||||
|
||||
// Auto-save the live GPS trail to /trail on shutdown (covers the low-battery
|
||||
// auto-shutdown, which otherwise loses the whole route). Only writes when the
|
||||
// trail has points and the toggle is on. 0 = off (default), 1 = on.
|
||||
// [Tools › Trail › Settings › Auto-save]
|
||||
uint8_t trail_autosave_lowbatt;
|
||||
|
||||
// Appended at the tail (see the alarm doc comment above for the field itself
|
||||
// and the serialization tripwire below for why new fields land here, not
|
||||
// inline with alarm_on/hour/min).
|
||||
uint8_t alarm_repeat_mask;
|
||||
|
||||
// Appended at the tail (see the keyboard_alt_alphabet doc comment above).
|
||||
uint8_t keyboard_alt_alphabet;
|
||||
|
||||
// Single source of truth for the live-share option tables (shared by the Map
|
||||
// UI labels and the auto-send engine in UITask).
|
||||
static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
|
||||
@@ -357,7 +417,7 @@ struct NodePrefs { // persisted to file
|
||||
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
|
||||
// older saves are detected on load and skipped (zero-init defaults kept).
|
||||
// High 24 bits identify the file format; low byte is the schema revision.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE001B;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE001D;
|
||||
|
||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||
@@ -454,7 +514,7 @@ struct NodePrefs { // persisted to file
|
||||
// 3. clamp it on load (an upgrader's file lacks it → stray bytes)
|
||||
// 4. bump SCHEMA_SENTINEL's low byte
|
||||
// (Padding can also shift sizeof; a "false" trip just means re-check + rebump.)
|
||||
static_assert(sizeof(NodePrefs) == 2496,
|
||||
static_assert(sizeof(NodePrefs) == 2504,
|
||||
"NodePrefs layout changed — sync DataStore save/load + clamp, bump "
|
||||
"SCHEMA_SENTINEL, then update this size (see steps above).");
|
||||
|
||||
|
||||
@@ -145,15 +145,16 @@ class ClockToolsScreen : public UIScreen {
|
||||
int renderAlarm(DisplayDriver& d) {
|
||||
d.drawCenteredHeader("ALARM");
|
||||
if (!_prefs) return 60000;
|
||||
const char* rows[3] = { "Hour", "Minute", "Armed" };
|
||||
if (_sel > 2) _sel = 2;
|
||||
const char* rows[4] = { "Hour", "Minute", "Repeat", "Armed" };
|
||||
if (_sel > 3) _sel = 3;
|
||||
const int valx = d.width() / 2 + 6;
|
||||
drawList(d, 3, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
drawList(d, 4, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
drawRowSelection(d, y, sel, reserve);
|
||||
d.setCursor(4, y); d.print(rows[i]);
|
||||
char b[6];
|
||||
char b[10];
|
||||
if (i == 0) snprintf(b, sizeof(b), "%02u", _prefs->alarm_hour);
|
||||
else if (i == 1) snprintf(b, sizeof(b), "%02u", _prefs->alarm_min);
|
||||
else if (i == 2) snprintf(b, sizeof(b), "%s", NodePrefs::alarmRepeatLabel(NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask)));
|
||||
else snprintf(b, sizeof(b), "%s", _prefs->alarm_on ? "ON" : "OFF");
|
||||
EditKind k = (i == 0) ? EK_A_HOUR : (i == 1) ? EK_A_MIN : EK_NONE;
|
||||
drawValue(d, y, valx, reserve, sel, k, b);
|
||||
@@ -226,12 +227,16 @@ class ClockToolsScreen : public UIScreen {
|
||||
_view = V_MENU; _sel = 0; _scroll = 0;
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 2; return true; }
|
||||
if (c == KEY_DOWN) { _sel = (_sel < 2) ? _sel + 1 : 0; return true; }
|
||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 3; return true; }
|
||||
if (c == KEY_DOWN) { _sel = (_sel < 3) ? _sel + 1 : 0; return true; }
|
||||
if (c == KEY_ENTER) {
|
||||
if (_sel == 0) editField(EK_A_HOUR, _prefs->alarm_hour, 23);
|
||||
else if (_sel == 1) editField(EK_A_MIN, _prefs->alarm_min, 59);
|
||||
else { _prefs->alarm_on ^= 1; _alarm_dirty = true; _task->onAlarmChanged(); }
|
||||
else if (_sel == 2) { // Repeat: cycle Off -> Daily -> Weekdays -> Weekends -> Off
|
||||
uint8_t idx = (NodePrefs::alarmRepeatIdxForMask(_prefs->alarm_repeat_mask) + 1) % NodePrefs::ALARM_REPEAT_COUNT;
|
||||
_prefs->alarm_repeat_mask = NodePrefs::alarmRepeatMaskForIdx(idx);
|
||||
_alarm_dirty = true; _task->onAlarmChanged();
|
||||
} else { _prefs->alarm_on ^= 1; _alarm_dirty = true; _task->onAlarmChanged(); }
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -42,6 +42,71 @@ static const char* const KB_T9_GROUPS[KB_PAGES][9] = {
|
||||
{ ".,!?'-", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }, // page 0 — letters
|
||||
{ "@#&", "*()", "-_+", "=/\\", ":;'\"", "<>[]", "{}|~", "^$%`", ",." }, // page 1 — symbols
|
||||
};
|
||||
|
||||
// Additional (non-Latin) keyboard alphabets — NodePrefs::keyboard_alt_alphabet
|
||||
// picks which one (if any) joins the Latin/symbols page cycle. Every alphabet
|
||||
// here must fall inside Lemon's U+0020-04FF range (src/helpers/ui/LemonFont.h)
|
||||
// since that's what actually draws these glyphs on-screen.
|
||||
//
|
||||
// Unlike KB_CHARS (single ASCII byte per cell), these hold UTF-8 strings —
|
||||
// Cyrillic is 2 bytes/codepoint — so cells are `const char*`, not `char`.
|
||||
// KeyboardWidget's insertion/backspace/T9-cycle logic works in codepoints via
|
||||
// the kbUtf8*() helpers below, not raw bytes, to stay correct for either.
|
||||
|
||||
// Cyrillic ABC grid: rows 0-2 hold the 30 letters that fit alphabetically
|
||||
// (а-э); row 3 holds the remaining 3 (ё, ю, я) plus basic punctuation. No
|
||||
// digit row on this page (digits are already reachable via the Symbols page,
|
||||
// same as the Latin page's own row 3 duplicates them there).
|
||||
static const char* const KB_CYRILLIC_CHARS[4][10] = {
|
||||
{ "а","б","в","г","д","е","ж","з","и","й" },
|
||||
{ "к","л","м","н","о","п","р","с","т","у" },
|
||||
{ "ф","х","ц","ч","ш","щ","ъ","ы","ь","э" },
|
||||
{ "ю","я","ё",".",",","!","?","-","'","\"" },
|
||||
};
|
||||
// Cyrillic T9 groups: the classic Russian phone-keypad distribution. Cell 0
|
||||
// (digit '1') is punctuation, matching KB_T9_GROUPS' page-0 convention;
|
||||
// cells 1-8 (digits '2'-'9') hold the 33 letters, 3-5 per key.
|
||||
static const char* const KB_T9_GROUPS_CYRILLIC[9] = {
|
||||
".,!?'-", "абвг", "деёжз", "ийкл", "мноп", "рсту", "фхцч", "шщъыь", "эюя"
|
||||
};
|
||||
|
||||
// Greek ABC grid: the 24-letter modern alphabet plus final sigma (ς, used
|
||||
// only at the end of a word — σ is the regular form) = 25 letters, fitting
|
||||
// rows 0-2 with 5 basic punctuation marks to spare; row 3 keeps the digit row
|
||||
// (unlike Cyrillic/Ext.Latin, Greek has room left over).
|
||||
// NOTE: monotonic Modern Greek normally marks stress with a tonos accent
|
||||
// (ά έ ή ί ό ύ ώ) — omitted here to keep this a single, simple page. Fine for
|
||||
// informal/transliteration-style typing; flag if proper accented Greek
|
||||
// composition turns out to matter and we can add a second Greek page for them.
|
||||
static const char* const KB_GREEK_CHARS[4][10] = {
|
||||
{ "α","β","γ","δ","ε","ζ","η","θ","ι","κ" },
|
||||
{ "λ","μ","ν","ξ","ο","π","ρ","σ","ς","τ" },
|
||||
{ "υ","φ","χ","ψ","ω",".",",","!","?","'" },
|
||||
{ "1","2","3","4","5","6","7","8","9","0" },
|
||||
};
|
||||
// Greek T9 groups: cell 0 (digit '1') is punctuation; cells 1-8 (digits
|
||||
// '2'-'9') split the 25 letters roughly evenly, final sigma grouped with the
|
||||
// regular sigma it's a variant of.
|
||||
static const char* const KB_T9_GROUPS_GREEK[9] = {
|
||||
".,!?'-", "αβγ", "δεζ", "ηθι", "κλμ", "νξο", "πρσς", "τυφ", "χψω"
|
||||
};
|
||||
|
||||
// Extended Latin ABC grid: diacritics for Polish, Czech/Slovak, German,
|
||||
// French, Spanish/Portuguese and Nordic — the common non-ASCII Latin letters,
|
||||
// not full Unicode coverage. 34 letters + 6 basic punctuation marks fill all
|
||||
// 40 cells; no digit row on this page (digits are reachable via the Symbols
|
||||
// page, same as Cyrillic).
|
||||
static const char* const KB_EXTLATIN_CHARS[4][10] = {
|
||||
{ "ą","ć","ę","ł","ń","ó","ś","ź","ż","ö" }, // Polish (9) + German ö
|
||||
{ "ü","ä","ß","č","ď","ě","ň","ř","š","ť" }, // German (3) + Czech/Slovak (7)
|
||||
{ "ů","ž","é","è","ê","ë","à","ç","ù","ñ" }, // Czech/Slovak (2) + French (6) + Spanish ñ
|
||||
{ "å","ø","æ","õ",".",",","!","?","-","'" }, // Nordic (3) + Portuguese õ + punctuation
|
||||
};
|
||||
// Extended Latin T9 groups: cell 0 (digit '1') is punctuation; cells 1-8
|
||||
// (digits '2'-'9') cluster by language for memorability.
|
||||
static const char* const KB_T9_GROUPS_EXTLATIN[9] = {
|
||||
".,!?'-", "ąćęł", "ńóśźż", "äöüß", "čďěň", "řšťůž", "éèêë", "àçùñ", "åøæõ"
|
||||
};
|
||||
// Buffer cap for typed text, in bytes. Matches MeshCore's MAX_TEXT_LEN
|
||||
// (10*CIPHER_BLOCK_SIZE = 160) so a full-length message can be composed; each
|
||||
// field passes its own smaller max to begin() where its store is smaller.
|
||||
@@ -51,11 +116,94 @@ static const int KB_MAX_LEN = 160;
|
||||
// very wide display (small font → many chars per line) can't overrun them.
|
||||
static const int KB_PREVIEW_CAP = 46;
|
||||
|
||||
// Uppercase one char when Shift (caps) is on. The keyboard applies this at every
|
||||
// draw and commit site — a-z only, everything else (digits, punctuation) passes
|
||||
// through unchanged.
|
||||
static inline char kbApplyCaps(char ch, bool caps) {
|
||||
return (caps && ch >= 'a' && ch <= 'z') ? (char)(ch - 'a' + 'A') : ch;
|
||||
// ── UTF-8 helpers for the alt-alphabet pages ─────────────────────────────────
|
||||
// KB_CHARS/KB_T9_GROUPS content is ASCII (1 byte/char); KB_CYRILLIC_CHARS and
|
||||
// KB_T9_GROUPS_CYRILLIC are UTF-8 (2 bytes/char in this range). These helpers
|
||||
// let insertion, backspace and T9 cycling work in codepoints for either,
|
||||
// instead of assuming 1 byte == 1 character.
|
||||
|
||||
// Apply Shift/caps to every codepoint in a UTF-8 string, writing the result
|
||||
// (same codepoint count, each independently shifted) into `out`. Every script
|
||||
// here pairs lower/uppercase differently, so each gets its own rule:
|
||||
// - ASCII a-z and Cyrillic а-я: flat -0x20 codepoint offset. ё/Ё (U+0451/
|
||||
// U+0401) break the Cyrillic pattern by 0x50 and are special-cased.
|
||||
// - Latin-1 Supplement à-þ (U+00E0-00FE, Ext.Latin's ö/ü/é/etc.): also a flat
|
||||
// -0x20 offset, same as ASCII — the block was designed as parallel case
|
||||
// pairs. U+00F7 (÷, division sign) sits in that numeric range but isn't a
|
||||
// letter; excluded. ß (U+00DF) has no simple uppercase in this range
|
||||
// (its uppercase ẞ is U+1E9E, outside Lemon's U+0020-04FF) — left as-is.
|
||||
// - Latin Extended-A (Ext.Latin's ą/č/etc., U+0100-017F): NOT a flat offset
|
||||
// like Latin-1 — this block alternates even=uppercase/odd=lowercase in
|
||||
// adjacent pairs, so lowercase - 1 = uppercase. Verified true for every
|
||||
// character actually in KB_EXTLATIN_CHARS/KB_T9_GROUPS_EXTLATIN; NOT a
|
||||
// universal rule for the whole block (it has a handful of unpaired/
|
||||
// irregular codepoints elsewhere) — recheck before adding more from it.
|
||||
// - Greek α-ω (U+03B1-03C9): flat -0x20 offset, same shape as Cyrillic/ASCII.
|
||||
// Final sigma ς (U+03C2) is the one exception — it has no uppercase of its
|
||||
// own; -0x20 would land on U+03A2, which is unassigned. It capitalizes to
|
||||
// regular Σ (U+03A3) instead, same as σ, and is special-cased before the
|
||||
// general range rule (03C2 falls inside 03B1-03C9, so order matters here).
|
||||
// Used both for a single cell (one codepoint) and a whole T9 group label/string.
|
||||
static void kbApplyCapsUtf8(const char* in, bool caps, char* out, size_t out_size) {
|
||||
size_t o = 0;
|
||||
const uint8_t* p = (const uint8_t*)in;
|
||||
while (*p && o + 2 < out_size) {
|
||||
uint32_t cp = DisplayDriver::decodeCodepoint(p);
|
||||
if (caps) {
|
||||
if (cp == 0x0451) cp = 0x0401; // ё -> Ё
|
||||
else if (cp == 0x03C2) cp = 0x03A3; // ς -> Σ
|
||||
else if (cp >= 0x0430 && cp <= 0x044F) cp -= 0x20; // а-я -> А-Я
|
||||
else if (cp >= 0x03B1 && cp <= 0x03C9) cp -= 0x20; // α-ω -> Α-Ω
|
||||
else if (cp >= 0x00E0 && cp <= 0x00FE && cp != 0x00F7) cp -= 0x20; // à-þ -> À-Þ
|
||||
else if (cp >= 0x0100 && cp < 0x0180 && (cp & 1) == 1) cp -= 1; // ą-ż (odd=lower) -> Ą-Ż
|
||||
else if (cp >= 'a' && cp <= 'z') cp -= 0x20; // a-z -> A-Z
|
||||
}
|
||||
if (cp < 0x80) {
|
||||
out[o++] = (char)cp;
|
||||
} else {
|
||||
out[o++] = (char)(0xC0 | (cp >> 6));
|
||||
out[o++] = (char)(0x80 | (cp & 0x3F));
|
||||
}
|
||||
}
|
||||
out[o] = '\0';
|
||||
}
|
||||
|
||||
// Codepoint count of a UTF-8 string (byte length overcounts once a 2-byte
|
||||
// alphabet is involved — this is what T9 cycling needs instead of strlen()).
|
||||
static int kbUtf8Len(const char* s) {
|
||||
int n = 0;
|
||||
const uint8_t* p = (const uint8_t*)s;
|
||||
while (*p) { DisplayDriver::decodeCodepoint(p); n++; }
|
||||
return n;
|
||||
}
|
||||
|
||||
// Extract the idx-th codepoint of a UTF-8 string as its own NUL-terminated
|
||||
// UTF-8 bytes in `out` (>= 5 bytes). Empty string if idx is out of range.
|
||||
static void kbUtf8CharAt(const char* s, int idx, char* out) {
|
||||
const uint8_t* p = (const uint8_t*)s;
|
||||
for (int i = 0; *p; i++) {
|
||||
const uint8_t* start = p;
|
||||
DisplayDriver::decodeCodepoint(p);
|
||||
if (i == idx) {
|
||||
int n = (int)(p - start);
|
||||
memcpy(out, start, n);
|
||||
out[n] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
out[0] = '\0';
|
||||
}
|
||||
|
||||
// Byte width of the LAST codepoint in buf[0..len) — for backspace and the T9
|
||||
// in-place replace, which must remove/overwrite a whole codepoint, not one
|
||||
// byte (a lone trailing continuation byte would otherwise corrupt a 2-byte
|
||||
// character). Capped at 4 (UTF-8's max), though this codebase's alphabets are
|
||||
// all <= 2 bytes today.
|
||||
static int kbUtf8LastCharBytes(const char* buf, int len) {
|
||||
if (len <= 0) return 0;
|
||||
int n = 1;
|
||||
while (n < len && n < 4 && ((uint8_t)buf[len - n] & 0xC0) == 0x80) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
static const int KB_PH_MAX = 12; // max placeholders in list
|
||||
@@ -67,7 +215,7 @@ struct KeyboardWidget {
|
||||
int len;
|
||||
int max_len;
|
||||
int row, col;
|
||||
int page; // 0 = letters, 1 = symbols
|
||||
int page; // see totalPages()/pageIsAltAlphabet()/pageIsSymbols() below
|
||||
bool caps;
|
||||
char _ph_buf[KB_PH_MAX][KB_PH_LEN];
|
||||
int _ph_count;
|
||||
@@ -75,9 +223,12 @@ struct KeyboardWidget {
|
||||
|
||||
// Live setting lookup — set once by UITask::begin(). NULL only in tests/tools
|
||||
// that construct a KeyboardWidget standalone, in which case isT9() defaults
|
||||
// to ABC.
|
||||
// to ABC and hasAltAlphabet() defaults to Latin-only.
|
||||
NodePrefs* prefs = nullptr;
|
||||
bool isT9() const { return prefs && prefs->keyboard_type == 1; }
|
||||
bool hasAltAlphabet() const {
|
||||
return prefs && prefs->keyboard_alt_alphabet != NodePrefs::KB_ALPHABET_LATIN_ONLY;
|
||||
}
|
||||
|
||||
// T9 multi-tap state: which grid cell is mid-cycle (-1 = none), its cycle
|
||||
// position, and when the last Enter landed on it (for the timeout).
|
||||
@@ -88,6 +239,61 @@ struct KeyboardWidget {
|
||||
int gridRows() const { return isT9() ? KB_T9_ROWS : KB_ROWS_CHAR; }
|
||||
int gridCols() const { return isT9() ? KB_T9_COLS : KB_COLS_CHAR; }
|
||||
|
||||
// ── Page model ────────────────────────────────────────────────────────────
|
||||
// Logical page order: 0 = Latin letters, [1 = the enabled alt alphabet],
|
||||
// last = symbols. Without an alt alphabet this is exactly the original
|
||||
// 2-page Latin/symbols cycle; enabling one inserts its page in the middle,
|
||||
// so the #@/abc key's existing cycle (case 4 below) reaches it for free.
|
||||
int totalPages() const { return hasAltAlphabet() ? 3 : 2; }
|
||||
bool pageIsAltAlphabet(int pg) const { return hasAltAlphabet() && pg == 1; }
|
||||
bool pageIsSymbols(int pg) const { return pg == totalPages() - 1; }
|
||||
|
||||
// ABC-grid cell content as a NUL-terminated UTF-8 string (1 codepoint).
|
||||
// Latin/symbols cells come back through a small scratch buffer since
|
||||
// KB_CHARS stores single ASCII bytes, not strings; alt-alphabet cells are
|
||||
// literal string-table entries, returned directly.
|
||||
const char* cellStr(int r, int c) const {
|
||||
if (pageIsAltAlphabet(page)) {
|
||||
switch (prefs->keyboard_alt_alphabet) {
|
||||
case NodePrefs::KB_ALPHABET_CYRILLIC: return KB_CYRILLIC_CHARS[r][c];
|
||||
case NodePrefs::KB_ALPHABET_GREEK: return KB_GREEK_CHARS[r][c];
|
||||
case NodePrefs::KB_ALPHABET_EXT_LATIN: return KB_EXTLATIN_CHARS[r][c];
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
static char single[2];
|
||||
single[0] = KB_CHARS[pageIsSymbols(page) ? 1 : 0][r][c];
|
||||
single[1] = '\0';
|
||||
return single;
|
||||
}
|
||||
|
||||
// T9 group string (UTF-8) for the given cell (0-8), page-aware like cellStr.
|
||||
const char* t9GroupStr(int cell) const {
|
||||
if (pageIsAltAlphabet(page)) {
|
||||
switch (prefs->keyboard_alt_alphabet) {
|
||||
case NodePrefs::KB_ALPHABET_CYRILLIC: return KB_T9_GROUPS_CYRILLIC[cell];
|
||||
case NodePrefs::KB_ALPHABET_GREEK: return KB_T9_GROUPS_GREEK[cell];
|
||||
case NodePrefs::KB_ALPHABET_EXT_LATIN: return KB_T9_GROUPS_EXTLATIN[cell];
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
return KB_T9_GROUPS[pageIsSymbols(page) ? 1 : 0][cell];
|
||||
}
|
||||
|
||||
// Compact ASCII hint for the #@/abc key when it's about to switch to the
|
||||
// alt-alphabet page. Deliberately ASCII (not the alphabet's own script) so
|
||||
// it renders correctly even when Lemon isn't the user's current font choice
|
||||
// — unlike the alphabet's own grid, this hint is visible from the Latin/
|
||||
// symbols pages too, where render() doesn't force Lemon on (see render()).
|
||||
static const char* altAlphabetHint(uint8_t alt) {
|
||||
switch (alt) {
|
||||
case NodePrefs::KB_ALPHABET_CYRILLIC: return "CY";
|
||||
case NodePrefs::KB_ALPHABET_GREEK: return "GR";
|
||||
case NodePrefs::KB_ALPHABET_EXT_LATIN: return "EL";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
enum Result { NONE, DONE, CANCELLED };
|
||||
|
||||
void begin(const char* initial = "", int max = KB_MAX_LEN) {
|
||||
@@ -123,6 +329,17 @@ struct KeyboardWidget {
|
||||
// on the same cell from being treated as a continued cycle.
|
||||
if (t9_cell >= 0 && millis() - t9_last_ms > KB_T9_TIMEOUT_MS) t9_cell = -1;
|
||||
|
||||
// Lemon is the only font that can draw the alt-alphabet page (or any
|
||||
// already-typed non-ASCII bytes sitting in buf, e.g. Cyrillic composed
|
||||
// earlier, now viewed from the Latin/symbols page) — force it on for just
|
||||
// this render() call if the user's own font choice wouldn't otherwise show
|
||||
// it, then restore exactly the state found on entry. Self-contained: never
|
||||
// leaks a forced font into whatever renders next frame.
|
||||
bool want_lemon = pageIsAltAlphabet(page);
|
||||
if (!want_lemon) for (int i = 0; i < len; i++) if ((uint8_t)buf[i] >= 0x80) { want_lemon = true; break; }
|
||||
bool had_lemon = display.isLemonFont();
|
||||
if (want_lemon && !had_lemon) display.setLemonFont(true);
|
||||
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
@@ -179,10 +396,12 @@ struct KeyboardWidget {
|
||||
// Label the cell "<digit><group>" so it reads like a phone keypad. The
|
||||
// digit is what the multi-tap cycle lands on after the letters (see
|
||||
// handleInput: '1'+cell). No separator space — the widest group
|
||||
// (".,!?'-", 6 chars) + digit already fills a narrow OLED cell.
|
||||
char label[10];
|
||||
snprintf(label, sizeof(label), "%c%s", (char)('1' + cell), KB_T9_GROUPS[page][cell]);
|
||||
if (caps) for (char* p = label; *p; p++) *p = kbApplyCaps(*p, caps);
|
||||
// (Cyrillic "деёжз"/"шщъыь", 5 letters x up to 2 UTF-8 bytes) + digit
|
||||
// still fits with room to spare.
|
||||
char group_shown[12];
|
||||
kbApplyCapsUtf8(t9GroupStr(cell), caps, group_shown, sizeof(group_shown));
|
||||
char label[14];
|
||||
snprintf(label, sizeof(label), "%c%s", (char)('1' + cell), group_shown);
|
||||
int cx = c * cell_w;
|
||||
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
|
||||
int tw = display.getTextWidth(label);
|
||||
@@ -195,11 +414,13 @@ struct KeyboardWidget {
|
||||
int y = chars_y + r * cell_h;
|
||||
for (int c = 0; c < cols; c++) {
|
||||
bool sel = (row == r && col == c);
|
||||
char ch = kbApplyCaps(KB_CHARS[page][r][c], caps);
|
||||
char ch_buf[2] = { ch == ' ' ? '_' : ch, '\0' };
|
||||
char ch_buf[3];
|
||||
kbApplyCapsUtf8(cellStr(r, c), caps, ch_buf, sizeof(ch_buf));
|
||||
if (ch_buf[0] == ' ' && ch_buf[1] == '\0') ch_buf[0] = '_';
|
||||
int cx = c * cell_w;
|
||||
display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel);
|
||||
display.setCursor(cx + (cell_w - cw) / 2, y);
|
||||
int tw = display.getTextWidth(ch_buf);
|
||||
display.setCursor(cx + (cell_w - tw) / 2, y);
|
||||
display.print(ch_buf);
|
||||
}
|
||||
}
|
||||
@@ -214,7 +435,18 @@ struct KeyboardWidget {
|
||||
int sx = i * spec_w;
|
||||
display.drawSelectionRow(sx, spec_y - 1, spec_w - 1, cell_h, sel || active);
|
||||
if (i == 3 || i == 4) { // text keys: {} picker, page toggle
|
||||
const char* lbl = (i == 3) ? "{}" : (page == 0 ? "#@" : "abc");
|
||||
// Shows what pressing it lands on next, same "reads as the
|
||||
// destination" convention as the original 2-page abc<->#@ toggle,
|
||||
// generalized to however many pages are in the cycle right now.
|
||||
const char* lbl;
|
||||
if (i == 3) {
|
||||
lbl = "{}";
|
||||
} else {
|
||||
int next = (page + 1) % totalPages();
|
||||
lbl = pageIsSymbols(next) ? "#@"
|
||||
: pageIsAltAlphabet(next) ? altAlphabetHint(prefs->keyboard_alt_alphabet)
|
||||
: "abc";
|
||||
}
|
||||
int tw = display.getTextWidth(lbl);
|
||||
display.setCursor(sx + (spec_w - tw) / 2, spec_y);
|
||||
display.print(lbl);
|
||||
@@ -236,6 +468,7 @@ struct KeyboardWidget {
|
||||
// placeholder picker overlay (drawn on top of keyboard)
|
||||
if (_ph_menu.active) _ph_menu.render(display);
|
||||
|
||||
if (want_lemon && !had_lemon) display.setLemonFont(false); // restore exactly what we found
|
||||
return 50;
|
||||
}
|
||||
|
||||
@@ -300,28 +533,47 @@ struct KeyboardWidget {
|
||||
if (c == KEY_ENTER) {
|
||||
if (row < rows && isT9()) {
|
||||
int cell = row * cols + col;
|
||||
const char* group = KB_T9_GROUPS[page][cell];
|
||||
int glen = (int)strlen(group);
|
||||
const char* group = t9GroupStr(cell);
|
||||
int glen = kbUtf8Len(group); // codepoint count, not byte length
|
||||
int total = glen + 1; // + the cell's own digit, at the end of the cycle
|
||||
bool cycling = (t9_cell == cell) && (millis() - t9_last_ms < KB_T9_TIMEOUT_MS);
|
||||
if (cycling) {
|
||||
t9_cycle = (t9_cycle + 1) % total;
|
||||
if (len > 0) {
|
||||
char ch = kbApplyCaps((t9_cycle < glen) ? group[t9_cycle] : (char)('1' + cell), caps);
|
||||
buf[len - 1] = ch;
|
||||
char one[5];
|
||||
if (t9_cycle < glen) kbUtf8CharAt(group, t9_cycle, one);
|
||||
else { one[0] = (char)('1' + cell); one[1] = '\0'; }
|
||||
char shown[5];
|
||||
kbApplyCapsUtf8(one, caps, shown, sizeof(shown));
|
||||
int old_n = kbUtf8LastCharBytes(buf, len);
|
||||
int new_len = len - old_n;
|
||||
int n = (int)strlen(shown);
|
||||
if (new_len + n <= max_len) {
|
||||
memcpy(buf + new_len, shown, n);
|
||||
len = new_len + n;
|
||||
buf[len] = '\0';
|
||||
}
|
||||
}
|
||||
} else if (len < max_len) {
|
||||
char ch = kbApplyCaps(group[0], caps);
|
||||
buf[len++] = ch;
|
||||
buf[len] = '\0';
|
||||
t9_cell = cell;
|
||||
t9_cycle = 0;
|
||||
char one[5]; kbUtf8CharAt(group, 0, one);
|
||||
char shown[5]; kbApplyCapsUtf8(one, caps, shown, sizeof(shown));
|
||||
int n = (int)strlen(shown);
|
||||
if (len + n <= max_len) {
|
||||
memcpy(buf + len, shown, n);
|
||||
len += n;
|
||||
buf[len] = '\0';
|
||||
t9_cell = cell;
|
||||
t9_cycle = 0;
|
||||
}
|
||||
}
|
||||
t9_last_ms = millis();
|
||||
} else if (row < rows) {
|
||||
if (len < max_len) {
|
||||
char ch = kbApplyCaps(KB_CHARS[page][row][col], caps);
|
||||
buf[len++] = ch;
|
||||
char shown[3];
|
||||
kbApplyCapsUtf8(cellStr(row, col), caps, shown, sizeof(shown));
|
||||
int n = (int)strlen(shown);
|
||||
if (len + n <= max_len) {
|
||||
memcpy(buf + len, shown, n);
|
||||
len += n;
|
||||
buf[len] = '\0';
|
||||
}
|
||||
} else {
|
||||
@@ -332,14 +584,14 @@ struct KeyboardWidget {
|
||||
if (len < max_len) { buf[len++] = ' '; buf[len] = '\0'; }
|
||||
break;
|
||||
case 2:
|
||||
if (len > 0) buf[--len] = '\0';
|
||||
if (len > 0) { len -= kbUtf8LastCharBytes(buf, len); buf[len] = '\0'; }
|
||||
break;
|
||||
case 3:
|
||||
_ph_menu.begin("Placeholder:", KB_PH_VISIBLE);
|
||||
for (int i = 0; i < _ph_count; i++) _ph_menu.addItem(_ph_buf[i]);
|
||||
break;
|
||||
case 4:
|
||||
page ^= 1; // toggle letters ⇄ symbols
|
||||
page = (page + 1) % totalPages(); // cycle letters -> [alt alphabet] -> symbols
|
||||
break;
|
||||
case 5:
|
||||
return DONE;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1496,14 +1496,30 @@ void UITask::wakeForAlarm() {
|
||||
|
||||
// Next absolute wall instant matching alarm_hour:alarm_min in local time,
|
||||
// strictly after now_wall (an alarm set to the current minute waits a day).
|
||||
// With alarm_repeat_mask == 0 that's just tomorrow's occurrence (one-shot).
|
||||
// With a repeat mask set, scan today..+6 days for the next weekday whose bit
|
||||
// is set (struct tm's tm_wday convention, same as the mask) — today counts
|
||||
// only if its time hasn't already passed.
|
||||
uint32_t UITask::computeAlarmNextFire(uint32_t now_wall) const {
|
||||
int tz = _node_prefs ? _node_prefs->tz_offset_hours : 0;
|
||||
int64_t now_local = (int64_t)now_wall + (int64_t)tz * 3600;
|
||||
time_t t = (time_t)now_local;
|
||||
struct tm* ti = gmtime(&t);
|
||||
int64_t sod = ti->tm_hour * 3600 + ti->tm_min * 60 + ti->tm_sec; // secs since local midnight
|
||||
int64_t target = (now_local - sod)
|
||||
+ (int64_t)_node_prefs->alarm_hour * 3600 + (int64_t)_node_prefs->alarm_min * 60;
|
||||
int64_t midnight = now_local - sod;
|
||||
int64_t time_of_day = (int64_t)_node_prefs->alarm_hour * 3600 + (int64_t)_node_prefs->alarm_min * 60;
|
||||
uint8_t mask = _node_prefs->alarm_repeat_mask;
|
||||
if (mask != 0) {
|
||||
for (int d = 0; d < 7; d++) {
|
||||
if (mask & (1 << ((ti->tm_wday + d) % 7))) {
|
||||
int64_t target = midnight + (int64_t)d * 86400 + time_of_day;
|
||||
if (target > now_local) return (uint32_t)(target - (int64_t)tz * 3600);
|
||||
}
|
||||
}
|
||||
// Mask had no bit set (shouldn't happen — the UI only offers non-empty
|
||||
// presets) — fall through to the one-shot calculation so it still fires.
|
||||
}
|
||||
int64_t target = midnight + time_of_day;
|
||||
if (target <= now_local) target += 86400;
|
||||
return (uint32_t)(target - (int64_t)tz * 3600);
|
||||
}
|
||||
@@ -1529,8 +1545,12 @@ void UITask::evaluateAlarm() {
|
||||
if (now_wall - _alarm_next_fire < CLOCK_ALARM_CATCHUP_SECS) {
|
||||
char lbl[20];
|
||||
snprintf(lbl, sizeof(lbl), "Alarm %02d:%02d", _node_prefs->alarm_hour, _node_prefs->alarm_min);
|
||||
_node_prefs->alarm_on = 0; // one-shot
|
||||
bool dirty = true; savePrefsIfDirty(dirty);
|
||||
if (_node_prefs->alarm_repeat_mask == 0) {
|
||||
_node_prefs->alarm_on = 0; // one-shot
|
||||
bool dirty = true; savePrefsIfDirty(dirty);
|
||||
}
|
||||
// Repeating: alarm_on stays set: computeAlarmNextFire() re-arms it for the
|
||||
// next matching weekday below.
|
||||
_alarm_next_fire = 0;
|
||||
fireClockAlert(lbl);
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user