From 6cd081add1a85891a960bde8a25a026f3378a282 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Fri, 2 Oct 2026 13:49:16 +0200 Subject: [PATCH] fix(l1): wrap-safe UI timers, a failed send keeps the text, bars stop at the ends - The redraw, alert, splash and sensor timers compare millis() wrap-safe (refreshDue(), alertShowing(); 0 = now / none), so the screen keeps refreshing past the 49.7-day rollover. - A failed send leaves the keyboard with its text ("Not sent" in the field label, as alerts don't show over it) or the quick-message list with "Send failed", instead of dropping to Home. No "Sent" echo. - The dashboard's Sats field shows "--" with the GPS off, not a stale count. - Brightness and volume in Settings stop at both ends instead of wrapping. Co-Authored-By: Claude Opus 5.5 --- .../companion_radio/ui-new/MessagesScreen.h | 23 +++++++----- .../companion_radio/ui-new/SettingsScreen.h | 24 +++++++++--- examples/companion_radio/ui-new/UITask.cpp | 37 ++++++++++--------- examples/companion_radio/ui-new/UITask.h | 4 ++ 4 files changed, 55 insertions(+), 33 deletions(-) diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index a805d431..5888ee68 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -570,25 +570,28 @@ class MessagesScreen : public UIScreen { drawButton(d, d.width() - buttonWidth(d, ctxt), cby, ctxt, sel); } - void afterSend(bool ok, const char* msg) { + // A failed send leaves the screen as it was -- the keyboard with the text + // still in it, or the quick-message list -- so it can be sent again. + // Alerts don't show over the keyboard, so there its field label says it. + void afterSend(bool ok) { + if (!ok) { + if (_phase == KEYBOARD) _kb->prompt = "Not sent"; + else _task->showAlert("Send failed", 1500); + return; + } _reply_mode = false; _share_mode = false; - if (ok && _sending_to_channel) { + if (_sending_to_channel) { _hist_sel = 0; _hist_scroll = 0; // Filed at index 0 by sendText(): the unread index range is stale, and // the user is active in this channel -- fully read. _unread_at_entry = 0; _viewing_max_seen = 0; - _task->showAlert("Sent", 600); - } else if (ok) { + } else { _dm_hist_sel = 0; _dm_hist_scroll = 0; _phase = DM_HIST; - _task->showAlert("Sent", 600); - } else { - _task->showAlert("Send failed", 1500); - _task->gotoHomeScreen(); } } @@ -2485,7 +2488,7 @@ public: expandMsg(_kb->buf, expanded, sizeof(expanded)); } bool ok = sendText(expanded); - afterSend(ok, expanded); + afterSend(ok); } } return true; @@ -2520,7 +2523,7 @@ public: expandMsg(tmpl, msg, sizeof(msg)); } bool ok = sendText(msg); - afterSend(ok, msg); + afterSend(ok); return true; } } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 8b9f4651..a171ebe9 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -216,18 +216,24 @@ class SettingsScreen : public UIScreen { return true; } - // A schema setting: its short label, the value in the value column (a - // switch as ON / OFF, brightness and volume as bars). A value too long for - // the column starts further left, clear of the label, else scrolls. + // The number pad for the lock PIN, with what it's asking for. void beginPinEntry(const char* prompt) { _kb->beginPin("", KeyboardWidget::PIN_MAX_LEN, false, prompt); _kb->clearPlaceholders(); // a PIN is literal, not a template message } + // Brightness and volume: a row of level boxes, stopping at both ends. + static bool isBar(const settings::Setting& st) { + return st.offset == offsetof(NodePrefs, display_brightness) || st.offset == offsetof(NodePrefs, buzzer_volume); + } + + // A schema setting: its short label, the value flush right (a switch for + // on / off, brightness and volume as bars); a value too long for the room + // beside the label is cut, and scrolls while selected. int renderSchema(DisplayDriver& display, const settings::Setting& st, NodePrefs* p, int y, bool sel) { const char* label = settings::shortLabel(st); if (!p) { display.setCursor(2, y); display.print(label); return 0; } - if (st.offset == offsetof(NodePrefs, display_brightness) || st.offset == offsetof(NodePrefs, buzzer_volume)) { + if (isBar(st)) { display.setCursor(2, y); display.print(label); renderBar(display, valCol(display), y, settings::get(*p, st) + 1, st.count); @@ -899,7 +905,15 @@ public: if (_selected >= SCHEMA_ITEM) { if (!p || !(left || right || enter)) return false; - settings::step(*p, settings::ALL[_selected - SCHEMA_ITEM], left ? -1 : 1, _task->core()); + const settings::Setting& st = settings::ALL[_selected - SCHEMA_ITEM]; + if (isBar(st)) { + const int v = settings::get(*p, st) + (left ? -1 : 1); + if (v < 0 || v >= st.count) return true; + settings::set(*p, st, (uint8_t)v); + if (st.changed) st.changed(_task->core()); + } else { + settings::step(*p, st, left ? -1 : 1, _task->core()); + } _dirty = true; return true; } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 08a33ded..96066b2f 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -131,7 +131,7 @@ public: } void poll() override { - if (millis() >= dismiss_after) { + if ((int32_t)(millis() - dismiss_after) >= 0) { _task->gotoHomeScreen(); } } @@ -805,10 +805,10 @@ class HomeScreen : public UIScreen { CayenneLPP sensors_lpp; int sensors_nb = 0; - int next_sensors_refresh = 0; + unsigned long next_sensors_refresh = 0; // 0: read on the next call void refresh_sensors() { - if (millis() > next_sensors_refresh) { + if (!next_sensors_refresh || (int32_t)(millis() - next_sensors_refresh) >= 0) { sensors_lpp.reset(); sensors_nb = 0; sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); @@ -820,9 +820,9 @@ class HomeScreen : public UIScreen { sensors_nb ++; } #if AUTO_OFF_MILLIS > 0 - next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec + next_sensors_refresh = (millis() + 5000) | 1; // refresh sensor values every 5 sec #else - next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min + next_sensors_refresh = (millis() + 60000) | 1; // refresh sensor values every 1 min #endif } } @@ -2045,7 +2045,7 @@ void UITask::clearAllDMUnread() { _core->clearAllDMUnread(); } void UITask::showAlert(const char* text, int duration_millis) { snprintf(_alert, sizeof(_alert), "%s", text); - _alert_expiry = millis() + duration_millis; + _alert_expiry = (millis() + duration_millis) | 1; // 0 means none } void UITask::notify(UIEventType t) { @@ -2117,7 +2117,7 @@ void UITask::onMessageArrived(const UiEvent& ev) { if (_display->isOn()) { uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; - _next_refresh = 100; + _next_refresh = 0; } } notify(ev.kind); @@ -2173,7 +2173,7 @@ void UITask::setCurrScreen(UIScreen* c) { if (!c) return; curr = c; c->onShow(); // central per-visit reset hook (see UIScreen::onShow) - _next_refresh = 100; + _next_refresh = 0; } void UITask::syncLockToHome() { @@ -2203,7 +2203,7 @@ void UITask::cancelUnlockPrompt() { _kb.buf[0] = '\0'; // clear input _kb.len = 0; _kb.cursor_pos = 0; - _next_refresh = 100; + _next_refresh = 0; } void UITask::handleUnlockKey(char c) { @@ -2359,6 +2359,7 @@ static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_m in.imperial = imperial; #if ENV_INCLUDE_GPS == 1 in.loc = sensors.getLocationProvider(); + { NodePrefs* np = the_mesh.getNodePrefs(); in.gps_on = np && np->gps_enabled; } // off: no stale satellite count #endif if (telemetry::isSensor(field)) { if (!lpp) { static CayenneLPP s_lpp(200); s_lpp.reset(); sensors.querySensors(0xFF, s_lpp); lpp = &s_lpp; } @@ -3031,7 +3032,7 @@ void UITask::loop() { // Lock-screen password keyboard: Consume every key press char k; while (dequeueKey(k)) handleUnlockKey(k); - _next_refresh = 100; // redraw immediately after key press + _next_refresh = 0; // redraw immediately after key press } else if (!_locked && curr) { // Apply the whole queued burst, then redraw once — N taps captured during // a blocking refresh become N navigation steps at the cost of one refresh. @@ -3041,7 +3042,7 @@ void UITask::loop() { // Note timing no longer depends on render cadence (TIMER1 IRQ advances // notes directly — see buzzer.cpp), so a redraw right after a keypress // can't clip a note; no need to hold it back while buzzer.isPlaying(). - _next_refresh = 100; // trigger refresh immediately + _next_refresh = 0; // trigger refresh immediately } else { _kq_head = _kq_tail = 0; // locked or no screen: eat all queued keys // Locked: wake window is set only when display first turns on @@ -3068,18 +3069,18 @@ void UITask::loop() { } if (_locked && !_unlock_kb && (int32_t)(millis() - _lock_wake_until) >= 0) { _display->turnOff(); - } else if (_locked && _unlock_kb && millis() >= _next_refresh) { + } else if (_locked && _unlock_kb && refreshDue()) { // While the prompt is up the password keyboard replaces the lockscreen view PERF_T0(); _display->startFrame(); _kb.beginFrame(); int delay_millis = _kb.render(*_display); - if (millis() < _alert_expiry) renderAlertOverlay(); // "Wrong PIN", and a ringing alarm + if (alertShowing()) renderAlertOverlay(); // "Wrong PIN", and a ringing alarm PERF_T1(); _display->endFrame(); PERF_T2(); _next_refresh = millis() + delay_millis; - } else if (_locked && millis() >= _next_refresh && home) { + } else if (_locked && refreshDue() && home) { PERF_T0(); _display->startFrame(); if (curr && curr != home && (millis() - ui_started_at < BOOT_SCREEN_MILLIS)) { @@ -3091,11 +3092,11 @@ void UITask::loop() { } // Alert overlay on top — without this a ringing alarm on a locked device // played its melody against a screen that never said what was ringing. - if (millis() < _alert_expiry) renderAlertOverlay(); + if (alertShowing()) renderAlertOverlay(); PERF_T1(); _display->endFrame(); PERF_T2(); - } else if (!_locked && millis() >= _next_refresh && curr) { + } else if (!_locked && refreshDue() && curr) { PERF_T0(); _display->startFrame(); _kb.beginFrame(); @@ -3106,14 +3107,14 @@ void UITask::loop() { // full-screen text entry, not just message compose. Otherwise a message // arriving mid-typing blanks out the letter grid for 3s with no way to // see what's being typed. - if (millis() < _alert_expiry && !_kb.isVisible()) { // alert overlay on top of any (non-keyboard) screen + if (alertShowing() && !_kb.isVisible()) { // alert overlay on top of any (non-keyboard) screen renderAlertOverlay(); // Keep refreshing the underlying screen at its own cadence (capped at the // alert's expiry) so layouts that settle over a frame — e.g. the message- // history scrollbar reserve — don't stay stuck behind the alert. Unchanged // frames are skipped by the display CRC, so e-ink isn't thrashed. _next_refresh = millis() + delay_millis; - if (_next_refresh > _alert_expiry) _next_refresh = _alert_expiry; + if ((int32_t)(_next_refresh - _alert_expiry) > 0) _next_refresh = _alert_expiry; } else { _next_refresh = millis() + delay_millis; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 967b08ca..7ad6f417 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -344,6 +344,10 @@ public: void wakeForAlarm(); // Clear any active alert overlay early (alarm dismiss). void clearAlert() { _alert_expiry = 0; } + // Wrap-safe (millis() rolls over after ~49.7 days). _next_refresh 0 = draw + // on the next pass; _alert_expiry 0 = no alert. + bool refreshDue() const { return !_next_refresh || (int32_t)(millis() - _next_refresh) >= 0; } + bool alertShowing() const { return _alert_expiry && (int32_t)(_alert_expiry - millis()) > 0; } // Clock tools engine API (ui-core/ClockEngine.h) — ClockToolsScreen drives // these; the engine runs from tickCore() so it fires regardless of the screen. void onAlarmChanged();