feat: screen lock — hold Back + 3×Enter to lock/unlock

- 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 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-17 09:55:51 +02:00
parent b068318bc5
commit 9ff42a9296
4 changed files with 126 additions and 8 deletions

View File

@@ -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

View File

@@ -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();

View File

@@ -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;

View File

@@ -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));