mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -570,25 +570,28 @@ class MessagesScreen : public UIScreen {
|
|||||||
drawButton(d, d.width() - buttonWidth(d, ctxt), cby, ctxt, sel);
|
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;
|
_reply_mode = false;
|
||||||
_share_mode = false;
|
_share_mode = false;
|
||||||
if (ok && _sending_to_channel) {
|
if (_sending_to_channel) {
|
||||||
_hist_sel = 0;
|
_hist_sel = 0;
|
||||||
_hist_scroll = 0;
|
_hist_scroll = 0;
|
||||||
// Filed at index 0 by sendText(): the unread index range is stale, and
|
// Filed at index 0 by sendText(): the unread index range is stale, and
|
||||||
// the user is active in this channel -- fully read.
|
// the user is active in this channel -- fully read.
|
||||||
_unread_at_entry = 0;
|
_unread_at_entry = 0;
|
||||||
_viewing_max_seen = 0;
|
_viewing_max_seen = 0;
|
||||||
_task->showAlert("Sent", 600);
|
} else {
|
||||||
} else if (ok) {
|
|
||||||
_dm_hist_sel = 0;
|
_dm_hist_sel = 0;
|
||||||
_dm_hist_scroll = 0;
|
_dm_hist_scroll = 0;
|
||||||
_phase = DM_HIST;
|
_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));
|
expandMsg(_kb->buf, expanded, sizeof(expanded));
|
||||||
}
|
}
|
||||||
bool ok = sendText(expanded);
|
bool ok = sendText(expanded);
|
||||||
afterSend(ok, expanded);
|
afterSend(ok);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -2520,7 +2523,7 @@ public:
|
|||||||
expandMsg(tmpl, msg, sizeof(msg));
|
expandMsg(tmpl, msg, sizeof(msg));
|
||||||
}
|
}
|
||||||
bool ok = sendText(msg);
|
bool ok = sendText(msg);
|
||||||
afterSend(ok, msg);
|
afterSend(ok);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,18 +216,24 @@ class SettingsScreen : public UIScreen {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A schema setting: its short label, the value in the value column (a
|
// The number pad for the lock PIN, with what it's asking for.
|
||||||
// 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.
|
|
||||||
void beginPinEntry(const char* prompt) {
|
void beginPinEntry(const char* prompt) {
|
||||||
_kb->beginPin("", KeyboardWidget::PIN_MAX_LEN, false, prompt);
|
_kb->beginPin("", KeyboardWidget::PIN_MAX_LEN, false, prompt);
|
||||||
_kb->clearPlaceholders(); // a PIN is literal, not a template message
|
_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) {
|
int renderSchema(DisplayDriver& display, const settings::Setting& st, NodePrefs* p, int y, bool sel) {
|
||||||
const char* label = settings::shortLabel(st);
|
const char* label = settings::shortLabel(st);
|
||||||
if (!p) { display.setCursor(2, y); display.print(label); return 0; }
|
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.setCursor(2, y);
|
||||||
display.print(label);
|
display.print(label);
|
||||||
renderBar(display, valCol(display), y, settings::get(*p, st) + 1, st.count);
|
renderBar(display, valCol(display), y, settings::get(*p, st) + 1, st.count);
|
||||||
@@ -899,7 +905,15 @@ public:
|
|||||||
|
|
||||||
if (_selected >= SCHEMA_ITEM) {
|
if (_selected >= SCHEMA_ITEM) {
|
||||||
if (!p || !(left || right || enter)) return false;
|
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;
|
_dirty = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void poll() override {
|
void poll() override {
|
||||||
if (millis() >= dismiss_after) {
|
if ((int32_t)(millis() - dismiss_after) >= 0) {
|
||||||
_task->gotoHomeScreen();
|
_task->gotoHomeScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -805,10 +805,10 @@ class HomeScreen : public UIScreen {
|
|||||||
|
|
||||||
CayenneLPP sensors_lpp;
|
CayenneLPP sensors_lpp;
|
||||||
int sensors_nb = 0;
|
int sensors_nb = 0;
|
||||||
int next_sensors_refresh = 0;
|
unsigned long next_sensors_refresh = 0; // 0: read on the next call
|
||||||
|
|
||||||
void refresh_sensors() {
|
void refresh_sensors() {
|
||||||
if (millis() > next_sensors_refresh) {
|
if (!next_sensors_refresh || (int32_t)(millis() - next_sensors_refresh) >= 0) {
|
||||||
sensors_lpp.reset();
|
sensors_lpp.reset();
|
||||||
sensors_nb = 0;
|
sensors_nb = 0;
|
||||||
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f);
|
||||||
@@ -820,9 +820,9 @@ class HomeScreen : public UIScreen {
|
|||||||
sensors_nb ++;
|
sensors_nb ++;
|
||||||
}
|
}
|
||||||
#if AUTO_OFF_MILLIS > 0
|
#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
|
#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
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2045,7 +2045,7 @@ void UITask::clearAllDMUnread() { _core->clearAllDMUnread(); }
|
|||||||
|
|
||||||
void UITask::showAlert(const char* text, int duration_millis) {
|
void UITask::showAlert(const char* text, int duration_millis) {
|
||||||
snprintf(_alert, sizeof(_alert), "%s", text);
|
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) {
|
void UITask::notify(UIEventType t) {
|
||||||
@@ -2117,7 +2117,7 @@ void UITask::onMessageArrived(const UiEvent& ev) {
|
|||||||
if (_display->isOn()) {
|
if (_display->isOn()) {
|
||||||
uint32_t aoff = autoOffMillis();
|
uint32_t aoff = autoOffMillis();
|
||||||
if (aoff > 0) _auto_off = millis() + aoff;
|
if (aoff > 0) _auto_off = millis() + aoff;
|
||||||
_next_refresh = 100;
|
_next_refresh = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
notify(ev.kind);
|
notify(ev.kind);
|
||||||
@@ -2173,7 +2173,7 @@ void UITask::setCurrScreen(UIScreen* c) {
|
|||||||
if (!c) return;
|
if (!c) return;
|
||||||
curr = c;
|
curr = c;
|
||||||
c->onShow(); // central per-visit reset hook (see UIScreen::onShow)
|
c->onShow(); // central per-visit reset hook (see UIScreen::onShow)
|
||||||
_next_refresh = 100;
|
_next_refresh = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::syncLockToHome() {
|
void UITask::syncLockToHome() {
|
||||||
@@ -2203,7 +2203,7 @@ void UITask::cancelUnlockPrompt() {
|
|||||||
_kb.buf[0] = '\0'; // clear input
|
_kb.buf[0] = '\0'; // clear input
|
||||||
_kb.len = 0;
|
_kb.len = 0;
|
||||||
_kb.cursor_pos = 0;
|
_kb.cursor_pos = 0;
|
||||||
_next_refresh = 100;
|
_next_refresh = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::handleUnlockKey(char c) {
|
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;
|
in.imperial = imperial;
|
||||||
#if ENV_INCLUDE_GPS == 1
|
#if ENV_INCLUDE_GPS == 1
|
||||||
in.loc = sensors.getLocationProvider();
|
in.loc = sensors.getLocationProvider();
|
||||||
|
{ NodePrefs* np = the_mesh.getNodePrefs(); in.gps_on = np && np->gps_enabled; } // off: no stale satellite count
|
||||||
#endif
|
#endif
|
||||||
if (telemetry::isSensor(field)) {
|
if (telemetry::isSensor(field)) {
|
||||||
if (!lpp) { static CayenneLPP s_lpp(200); s_lpp.reset(); sensors.querySensors(0xFF, s_lpp); lpp = &s_lpp; }
|
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
|
// Lock-screen password keyboard: Consume every key press
|
||||||
char k;
|
char k;
|
||||||
while (dequeueKey(k)) handleUnlockKey(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) {
|
} else if (!_locked && curr) {
|
||||||
// Apply the whole queued burst, then redraw once — N taps captured during
|
// 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.
|
// 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
|
// Note timing no longer depends on render cadence (TIMER1 IRQ advances
|
||||||
// notes directly — see buzzer.cpp), so a redraw right after a keypress
|
// 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().
|
// 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 {
|
} else {
|
||||||
_kq_head = _kq_tail = 0; // locked or no screen: eat all queued keys
|
_kq_head = _kq_tail = 0; // locked or no screen: eat all queued keys
|
||||||
// Locked: wake window is set only when display first turns on
|
// 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) {
|
if (_locked && !_unlock_kb && (int32_t)(millis() - _lock_wake_until) >= 0) {
|
||||||
_display->turnOff();
|
_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
|
// While the prompt is up the password keyboard replaces the lockscreen view
|
||||||
PERF_T0();
|
PERF_T0();
|
||||||
_display->startFrame();
|
_display->startFrame();
|
||||||
_kb.beginFrame();
|
_kb.beginFrame();
|
||||||
int delay_millis = _kb.render(*_display);
|
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();
|
PERF_T1();
|
||||||
_display->endFrame();
|
_display->endFrame();
|
||||||
PERF_T2();
|
PERF_T2();
|
||||||
_next_refresh = millis() + delay_millis;
|
_next_refresh = millis() + delay_millis;
|
||||||
} else if (_locked && millis() >= _next_refresh && home) {
|
} else if (_locked && refreshDue() && home) {
|
||||||
PERF_T0();
|
PERF_T0();
|
||||||
_display->startFrame();
|
_display->startFrame();
|
||||||
if (curr && curr != home && (millis() - ui_started_at < BOOT_SCREEN_MILLIS)) {
|
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
|
// 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.
|
// played its melody against a screen that never said what was ringing.
|
||||||
if (millis() < _alert_expiry) renderAlertOverlay();
|
if (alertShowing()) renderAlertOverlay();
|
||||||
PERF_T1();
|
PERF_T1();
|
||||||
_display->endFrame();
|
_display->endFrame();
|
||||||
PERF_T2();
|
PERF_T2();
|
||||||
} else if (!_locked && millis() >= _next_refresh && curr) {
|
} else if (!_locked && refreshDue() && curr) {
|
||||||
PERF_T0();
|
PERF_T0();
|
||||||
_display->startFrame();
|
_display->startFrame();
|
||||||
_kb.beginFrame();
|
_kb.beginFrame();
|
||||||
@@ -3106,14 +3107,14 @@ void UITask::loop() {
|
|||||||
// full-screen text entry, not just message compose. Otherwise a message
|
// 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
|
// arriving mid-typing blanks out the letter grid for 3s with no way to
|
||||||
// see what's being typed.
|
// 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();
|
renderAlertOverlay();
|
||||||
// Keep refreshing the underlying screen at its own cadence (capped at the
|
// 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-
|
// 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
|
// history scrollbar reserve — don't stay stuck behind the alert. Unchanged
|
||||||
// frames are skipped by the display CRC, so e-ink isn't thrashed.
|
// frames are skipped by the display CRC, so e-ink isn't thrashed.
|
||||||
_next_refresh = millis() + delay_millis;
|
_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 {
|
} else {
|
||||||
_next_refresh = millis() + delay_millis;
|
_next_refresh = millis() + delay_millis;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,6 +344,10 @@ public:
|
|||||||
void wakeForAlarm();
|
void wakeForAlarm();
|
||||||
// Clear any active alert overlay early (alarm dismiss).
|
// Clear any active alert overlay early (alarm dismiss).
|
||||||
void clearAlert() { _alert_expiry = 0; }
|
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
|
// Clock tools engine API (ui-core/ClockEngine.h) — ClockToolsScreen drives
|
||||||
// these; the engine runs from tickCore() so it fires regardless of the screen.
|
// these; the engine runs from tickCore() so it fires regardless of the screen.
|
||||||
void onAlarmChanged();
|
void onAlarmChanged();
|
||||||
|
|||||||
Reference in New Issue
Block a user