fix(sos): one click on a locked L1 no longer starts and cancels an SOS; the hold always works; the contact picker doesn't stall

The Back hold started at millis() | 2, up to 2 ms ahead of the clock. With the
screen off the loop comes round in under that, the unsigned difference wrapped
and read as 3 s held: the countdown started and the release cancelled it. The
same "| 1" sentinel ahead of millis() is fixed in the other places it was used
(L1 status screen, L2 prefs flush, WiFi test, live tiles, home edge flip, SD
retry).

A lock sequence left half done by a long Back hold (no click on release) kept
the SOS hold off until the next click: cleared 500 ms after Back is let go.

Picking an SOS contact no longer saves prefs on the way there and back (a flash
write stalls the L1); leaving the SOS screen saves it with the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-11-02 12:54:08 +01:00
co-authored by Claude Opus 5.5
parent ada5b8e545
commit 9bbaedccce
10 changed files with 30 additions and 15 deletions
@@ -331,7 +331,7 @@ static void onEditTile(lv_event_t* e) {
if (!side) previewSlot(t, slotAt(p));
int page = s_page + side;
if (!side || page < APPS || page >= pageCount()) { s_edge_since = 0; return; }
if (!s_edge_since) { s_edge_since = millis() | 1; return; }
if (!s_edge_since) { s_edge_since = (millis() - 1) | 1; return; } // never 0, never ahead of millis()
if (millis() - s_edge_since < EDGE_MS) return;
// Onto that page's nearest slot; the finger lifts to drag it on from there.
int to = side > 0 ? (page - APPS) * PER_PAGE : (page - APPS) * PER_PAGE + PER_PAGE - 1;
@@ -621,7 +621,7 @@ void UITask::homeFieldText(uint8_t f, char* v, int n) {
in.gps_on = _core->gpsEnabled();
if (telemetry::isSensor(f)) { // the sensors on the bus: read every few seconds, not per field
if (_sensors && (s_lpp_ms == 0 || millis() - s_lpp_ms > 5000)) {
s_lpp_ms = millis() | 1;
s_lpp_ms = (millis() - 1) | 1; // never 0, never ahead of millis()
s_lpp.reset();
queryLocalSensors(*_sensors, board.getMCUTemperature(), s_lpp);
}
+1 -1
View File
@@ -310,7 +310,7 @@ static bool mountStorage() {
if (s_sd_lent) return false;
if (s_sd_mounted) return true;
if (s_sd_last_try && millis() - s_sd_last_try < 5000) return false;
s_sd_last_try = millis() | 1;
s_sd_last_try = (millis() - 1) | 1; // never 0, never ahead of millis()
SD_MMC.setPins(2, 3, 1);
s_sd_mounted = SD_MMC.begin("/sdcard", true /* 1-bit */);
return s_sd_mounted;
+1 -1
View File
@@ -1807,7 +1807,7 @@ void UITask::mapDownloadTick() {
// The WiFi goes 30 s after the map was left or the screen went off (a
// quick look elsewhere keeps it).
if (_screen != SCR_MAP || _asleep) {
if (!_map_left_ms) _map_left_ms = millis() | 1;
if (!_map_left_ms) _map_left_ms = (millis() - 1) | 1; // never 0, never ahead of millis()
else if (millis() - _map_left_ms > 30000) dl.liveEnd();
}
}
+1 -1
View File
@@ -1257,7 +1257,7 @@ static void fmtDate(char* b, size_t n, const struct tm& ti) {
// itself changes at once; shutdown() saves whatever is pending.
static uint32_t s_prefs_dirty_ms = 0; // when the first unsaved change came (0: none)
static uint32_t s_prefs_save_at = 0; // the screen was left: save from then (0: not yet)
static void prefsSave() { if (!s_prefs_dirty_ms) s_prefs_dirty_ms = millis() | 1; }
static void prefsSave() { if (!s_prefs_dirty_ms) s_prefs_dirty_ms = (millis() - 1) | 1; } // never 0, never ahead of millis()
static void prefsSaveSoon(uint32_t delay_ms) {
if (s_prefs_dirty_ms && !s_prefs_save_at) s_prefs_save_at = (millis() + delay_ms) | 1;
}
@@ -307,7 +307,7 @@ void UITask::wifiSave() {
}
lvport::netEnd(); // a scan might have left the radio on
lvport::netJoin(s_wifi_test_ssid, s_wifi_test_pass);
s_wifi_test_ms = millis() | 1;
s_wifi_test_ms = (millis() - 1) | 1; // never 0, never ahead of millis()
snprintf(msg, sizeof(msg), LV_SYMBOL_REFRESH " Connecting to %s...", s_wifi_test_ssid);
wifiTestShow(msg, nullptr, nullptr);
}
@@ -1433,10 +1433,7 @@ public:
void commitPickSos(const ContactInfo& ci) {
NodePrefs* p = _task->getNodePrefs();
if (p) {
memcpy(p->sos_dm[_pick_sos_slot], ci.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN);
the_mesh.savePrefs();
}
if (p) memcpy(p->sos_dm[_pick_sos_slot], ci.id.pub_key, NodePrefs::FAVOURITE_PREFIX_LEN); // saved on leaving SOS
_pick_sos_slot = -1;
_task->gotoSosScreen();
}
+10 -2
View File
@@ -13,6 +13,7 @@ class SosScreen : public UIScreen {
NodePrefs* _prefs;
KeyboardWidget* _kb;
bool _dirty = false;
bool _picking = false; // off to the contact picker: the changes stay unsaved until we leave
int _sel = 0;
int _scroll = 0;
int _kb_row = -1; // >=0: the keyboard is open on that row
@@ -135,7 +136,9 @@ class SosScreen : public UIScreen {
if (enter) openChannels();
break;
case K_DM1: case K_DM2: case K_DM3:
if (enter) { _task->savePrefsIfDirty(_dirty); _task->pickSosContact(k - K_DM1); }
// No save on the way: a prefs write stalls the L1 for a moment, and
// leaving this screen saves the pick with the rest.
if (enter) { _picking = true; _task->pickSosContact(k - K_DM1); }
else { memset(_prefs->sos_dm[k - K_DM1], 0, NodePrefs::FAVOURITE_PREFIX_LEN); _dirty = true; }
break;
case K_ACK: case K_NOTE:
@@ -156,7 +159,12 @@ class SosScreen : public UIScreen {
public:
SosScreen(UITask* task, NodePrefs* prefs, KeyboardWidget* kb) : _task(task), _prefs(prefs), _kb(kb) {}
void onShow() override { _dirty = false; _kb_row = -1; _ch_list = false; } // the row stays: the contact list comes back here
void onShow() override { // the row stays: the contact list comes back here
_dirty = _picking; // back from the picker: what was changed, and the pick, still to save
_picking = false;
_kb_row = -1;
_ch_list = false;
}
int render(DisplayDriver& display) override {
if (_kb_row >= 0) return _kb->render(display);
@@ -242,7 +242,7 @@ private:
if (!_lpp_at || millis() - _lpp_at > 5000) { // both passes (measure, draw) read one query
_lpp.reset();
{ CardKBBusLock bus_lock; queryLocalSensors(sensors, board.getMCUTemperature(), _lpp); } // shares the CardKB's bus
_lpp_at = millis() | 1;
_lpp_at = (millis() - 1) | 1; // never 0, never ahead of millis()
}
static const struct { uint8_t type; const char* name; } NAMES[] = {
{ LPP_TEMPERATURE, "Temperature" }, { LPP_RELATIVE_HUMIDITY, "Humidity" },
@@ -428,7 +428,7 @@ public:
static unsigned long at = 0;
static int n = 0;
if (at && millis() - at < 10000) return n;
at = millis() | 1;
at = (millis() - 1) | 1; // never 0, never ahead of millis()
n = 0;
const uint32_t now = rtc_clock.getCurrentTime();
const int nc = the_mesh.getNumContacts();
+10 -1
View File
@@ -1456,12 +1456,21 @@ void UITask::loop() {
if (_lock_seq_used && millis() - _lock_seq_ms > 5000) {
_lock_seq_used = false; // safety reset if Back release event was missed
}
// The lock sequence lives while Back is held. A long hold gives no click on
// release, so a sequence left half done stayed and kept the SOS hold off:
// cleared once Back has been up past the click window.
if (back_btn.isPressed()) _back_up_at = 0;
else if (!_back_up_at) _back_up_at = (millis() - 1) | 1;
else if ((_lock_seq_count > 0 || _lock_seq_used) && millis() - _back_up_at > 500) {
_lock_seq_count = 0;
_lock_seq_used = false;
}
// Back held on its own: past the button's long press, so no click comes of it.
// Enter during the hold is the lock sequence, not this.
if (!back_btn.isPressed() || user_btn.isPressed() || _lock_seq_count > 0 || _lock_seq_used) {
_back_held_at = back_btn.isPressed() ? 1 : 0; // 1: spoken for until released
} else if (_back_held_at == 0) {
_back_held_at = millis() | 2;
_back_held_at = (millis() - 2) | 2; // never 0 or 1, never ahead of millis(): ahead, the next pass read 3 s held
} else if (_back_held_at != 1 && millis() - _back_held_at >= SOS_HOLD_MS) {
_back_held_at = 1;
onBackHeld();
+1
View File
@@ -158,6 +158,7 @@ class UITask : public UITaskBase, public UiCoreHost {
unsigned long _sos_view_until = 0; // 0 = closed
int _sos_left = 0; // countdown second last beeped
unsigned long _back_held_at = 0; // 0 = not held, or already acted on
unsigned long _back_up_at = 0; // when Back was let go (0: held)