From 9ff42a9296d6fe3ca9394703b4b011248609a8e0 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Sun, 17 May 2026 09:55:51 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20screen=20lock=20=E2=80=94=20hold=20Back?= =?UTF-8?q?=20+=203=C3=97Enter=20to=20lock/unlock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Long press Back starts lock sequence; 3 Enter presses within 3s toggle lock - Locked screen wakes for 5 seconds on any key, showing clock and unlock hint - Incoming messages do not wake the display while locked - Auto Lock setting in Display section: locks automatically when screen turns off - Lock state persisted in UITask; NodePrefs gains auto_lock flag Co-Authored-By: Claude Sonnet 4.6 --- examples/companion_radio/NodePrefs.h | 1 + .../companion_radio/ui-new/SettingsScreen.h | 10 ++ examples/companion_radio/ui-new/UITask.cpp | 115 ++++++++++++++++-- examples/companion_radio/ui-new/UITask.h | 8 ++ 4 files changed, 126 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 3f70e34e..e40f99a1 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -37,6 +37,7 @@ struct NodePrefs { // persisted to file uint8_t default_scope_key[16]; uint8_t display_brightness; // 0=min..4=max, default 2 (medium) uint16_t auto_off_secs; // display auto-off: 0=never, else seconds (default 15) + uint8_t auto_lock; // 0=disabled, 1=lock screen when display turns off int8_t tz_offset_hours; // timezone offset from UTC, -12..+14 (default 0) uint16_t low_batt_mv; // auto-shutdown threshold: 0=disabled, 3000-3500 mV uint8_t batt_display_mode; // 0=icon, 1=percent, 2=voltage diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index e5e8c6ed..ecd59b83 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -10,6 +10,7 @@ class SettingsScreen : public UIScreen { SECTION_DISPLAY, BRIGHTNESS, AUTO_OFF, + AUTO_LOCK, BATT_DISPLAY, CLOCK_SECONDS, // Sound section @@ -279,6 +280,10 @@ class SettingsScreen : public UIScreen { display.print("AutoOff"); display.setCursor(VAL_X, y); display.print(AUTO_OFF_LABELS[autoOffIndex()]); + } else if (item == AUTO_LOCK) { + display.print("AutoLock"); + display.setCursor(VAL_X, y); + display.print((p && p->auto_lock) ? "ON" : "OFF"); #if ENV_INCLUDE_GPS == 1 } else if (item == GPS_INTERVAL) { display.print("GPS upd"); @@ -452,6 +457,11 @@ public: if (left) idx = (idx + AUTO_OFF_COUNT - 1) % AUTO_OFF_COUNT; if (left || right) { p->auto_off_secs = AUTO_OFF_OPTS[idx]; _dirty = true; return true; } } + if (_selected == AUTO_LOCK && p && (left || right || enter)) { + p->auto_lock ^= 1; + _dirty = true; + return true; + } #if ENV_INCLUDE_GPS == 1 if (_selected == GPS_INTERVAL && p) { int idx = gpsIntervalIndex(); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0c5c1f50..0142e62c 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1049,7 +1049,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i snprintf(alert_buf, sizeof(alert_buf), "Msg: %.20s", from_name); showAlert(alert_buf, 3000); - if (_display != NULL) { + if (_display != NULL && !_locked) { if (!_display->isOn() && !hasConnection()) { _display->turnOn(); } @@ -1157,6 +1157,13 @@ void UITask::loop() { ev = back_btn.check(); if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_CANCEL); + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + if (_display != NULL && _display->isOn()) { + _lock_seq = 1; + _lock_seq_count = 0; + _lock_seq_ms = millis(); + _next_refresh = 0; + } } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { c = handleTripleClick(KEY_SELECT); } @@ -1199,10 +1206,48 @@ void UITask::loop() { } #endif - if (c != 0 && curr) { - curr->handleInput(c); - { uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } // extend auto-off timer - _next_refresh = 100; // trigger refresh + // Lock sequence timeout check + if (_lock_seq == 1 && millis() - _lock_seq_ms > 3000) { + _lock_seq = 0; + _lock_seq_count = 0; + } + + if (c != 0) { + // Intercept Enter presses when a lock/unlock sequence is active + if (_lock_seq == 1 && c == KEY_ENTER) { + _lock_seq_count++; + if (_lock_seq_count >= 3) { + _lock_seq = 0; + _lock_seq_count = 0; + _locked = !_locked; + if (_locked) { + _lock_wake_until = millis() + 2000; // brief "locked" display, then blank + } else { + uint32_t aoff = autoOffMillis(); + if (aoff > 0) _auto_off = millis() + aoff; + } + } + _next_refresh = 0; + } else if (_lock_seq == 1) { + // Non-Enter key cancels the sequence; process key normally below + _lock_seq = 0; + _lock_seq_count = 0; + if (!_locked && curr) { + curr->handleInput(c); + { uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } + _next_refresh = 100; + } + } else if (!_locked && curr) { + curr->handleInput(c); + { uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } // extend auto-off timer + _next_refresh = 100; // trigger refresh + } else if (_locked) { + // Locked: eat all keys except unlock sequence (handled above) + // Any key extends the wake window if display is already on + if (_display != NULL && _display->isOn()) + _lock_wake_until = millis() + 5000; + _next_refresh = 0; + } } userLedHandler(); @@ -1221,7 +1266,49 @@ void UITask::loop() { if (curr) curr->poll(); if (_display != NULL && _display->isOn()) { - if (millis() >= _next_refresh && curr) { + if (_locked && millis() > _lock_wake_until) { + _display->turnOff(); + } else if (_locked && millis() >= _next_refresh) { + _display->startFrame(); + // Lock screen: clock + unlock hint popup + uint32_t unix_ts = rtc_clock.getCurrentTime(); + _display->setColor(DisplayDriver::LIGHT); + if (unix_ts < 1000000000UL) { + _display->setTextSize(1); + _display->drawTextCentered(_display->width() / 2, 20, "No time sync"); + } else { + int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; + unix_ts += (int32_t)tz * 3600; + time_t t = (time_t)unix_ts; + struct tm* ti = gmtime(&t); + char buf[12]; + _display->setTextSize(2); + sprintf(buf, "%02d:%02d", ti->tm_hour, ti->tm_min); + _display->drawTextCentered(_display->width() / 2, 8, buf); + _display->setTextSize(1); + static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; + static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; + sprintf(buf, "%s %d %s", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon]); + _display->drawTextCentered(_display->width() / 2, 26, buf); + } + // Hint popup at bottom (like alert style) + _display->setTextSize(1); + const char* hint = _lock_seq == 1 + ? (_lock_seq_count == 0 ? "Enter x3 to unlock" : + _lock_seq_count == 1 ? "Enter x2 more..." : "Enter x1 more...") + : "Hold Back + 3xEnter"; + int p = 3; + int hy = _display->height() - 13; + int hw = _display->getTextWidth(hint); + int hx = (_display->width() - hw) / 2; + _display->setColor(DisplayDriver::LIGHT); + _display->fillRect(hx - p, hy - p, hw + p*2, 8 + p*2); + _display->setColor(DisplayDriver::DARK); + _display->setCursor(hx, hy); + _display->print(hint); + _display->endFrame(); + _next_refresh = millis() + 1000; + } else if (!_locked && millis() >= _next_refresh && curr) { _display->startFrame(); int delay_millis = curr->render(*_display); if (millis() < _alert_expiry && curr == home) { // render alert only on home screen @@ -1240,11 +1327,15 @@ void UITask::loop() { _display->endFrame(); } #if AUTO_OFF_MILLIS > 0 - if (autoOffMillis() > 0 && millis() > _auto_off) { + if (!_locked && autoOffMillis() > 0 && millis() > _auto_off) { _display->turnOff(); #ifdef PIN_LED digitalWrite(PIN_LED, LOW); // turn off status LED with display to save power #endif + if (_node_prefs && _node_prefs->auto_lock) { + _locked = true; + _lock_wake_until = 0; + } } #endif } @@ -1283,9 +1374,17 @@ char UITask::checkDisplayOn(char c) { #ifdef PIN_LED digitalWrite(PIN_LED, LOW); // ensure LED is off when waking display (userLedHandler takes over) #endif + if (_locked) { + _lock_wake_until = millis() + 5000; + _next_refresh = 0; + return 0; // eat the waking key press + } c = 0; } - { uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } // extend auto-off timer + if (!_locked) { + uint32_t aoff = autoOffMillis(); + if (aoff > 0) _auto_off = millis() + aoff; // extend auto-off timer + } _next_refresh = 0; // trigger refresh } return c; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index af8f87ad..5c6fadea 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -33,6 +33,11 @@ class UITask : public AbstractUITask { #endif unsigned long _next_refresh, _auto_off; NodePrefs* _node_prefs; + bool _locked; + unsigned long _lock_wake_until; // when to blank screen again after locked wake (5s) + int _lock_seq; // 0=idle, 1=active (long-Back pressed, awaiting Enters) + int _lock_seq_count; // Enter presses so far in current sequence + unsigned long _lock_seq_ms; // millis() when sequence started char _alert[80]; char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies unsigned long _alert_expiry; @@ -86,6 +91,9 @@ public: ui_started_at = 0; _batt_mv = 0; _msgcount = _room_unread = 0; + _locked = false; + _lock_wake_until = 0; + _lock_seq = 0; _lock_seq_count = 0; _lock_seq_ms = 0; _last_notif_ch_idx = -1; _last_notif_dm_valid = false; memset(_last_notif_dm_prefix, 0, sizeof(_last_notif_dm_prefix));