From 06e1dcdbfc44a3c34a5945a12da397631ed1a9af Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Sun, 6 Sep 2026 00:26:28 +0200 Subject: [PATCH 1/8] feat: Add lockpass setting to configure lock screen password --- examples/companion_radio/DataStore.cpp | 10 ++++- examples/companion_radio/NodePrefs.h | 11 +++++- .../companion_radio/ui-new/SettingsScreen.h | 37 ++++++++++++++++++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index da67ecb0..ee714f89 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -90,7 +90,7 @@ void DataStore::begin() { #include #elif defined(EXTRAFS) #include - #else + #else #include #endif #endif @@ -622,6 +622,11 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no rd(&_prefs.loc_share_duration_idx, sizeof(_prefs.loc_share_duration_idx)); if (_prefs.loc_share_duration_idx >= NodePrefs::LOC_SHARE_DURATION_COUNT) _prefs.loc_share_duration_idx = 0; + // append the lock-screen password. Should be empty by default + // since struct was zero initialized in begin(), meaning password is disabled + rd(_prefs.lock_screen_password, sizeof(_prefs.lock_screen_password)); + _prefs.lock_screen_password[sizeof(_prefs.lock_screen_password) - 1] = '\0'; + // Schema sentinel: bumped on layout changes. Mismatch means an older file // (or a different schema); rd() and the clamps above already keep every // field within its valid range regardless, so we just log it here — @@ -819,6 +824,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.contact_expiry_idx, sizeof(_prefs.contact_expiry_idx)); file.write((uint8_t *)&_prefs.loc_share_scope, sizeof(_prefs.loc_share_scope)); file.write((uint8_t *)&_prefs.loc_share_duration_idx, sizeof(_prefs.loc_share_duration_idx)); + file.write((uint8_t *)_prefs.lock_screen_password, sizeof(_prefs.lock_screen_password)); // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is // the one we check: once the flash fills, writes return 0, so a good @@ -1360,7 +1366,7 @@ bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { makeBlobPath(key, key_len, path, sizeof(path)); _fs->remove(path); - + return true; // return true even if file did not exist } #endif diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 8ee65e4c..8dbc7ba6 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -536,6 +536,11 @@ struct NodePrefs { // persisted to file // ── Custom messages ──────────────────────────────────────────────────── char custom_msgs[10][140]; // user-defined quick messages (supports {loc}, {time}) + // Lock-screen password, empty by default. If set, a password is required to + // unlock the lock screen. + static const uint8_t LOCK_PASSWORD_MAX_LEN = 32; + char lock_screen_password[LOCK_PASSWORD_MAX_LEN]; + // Single source of truth for the live-share option tables (shared by the Map // UI labels and the auto-send engine in UITask). static const uint8_t LOC_SHARE_MOVE_COUNT = 4; @@ -623,7 +628,7 @@ struct NodePrefs { // persisted to file // repeat_* fields) instead of at the tail, which shifted every field after // them by 25 bytes when loading an older file. Never released, but a dev // build wrote it, so the number must not be reused for anything else. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE002E; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE002F; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage @@ -763,6 +768,7 @@ struct NodePrefs { // persisted to file // msg_wake_screen_off (0xC0DE002A) landed in the 1 byte of padding the // 0xC0DE0029 bump left over -- confirmed via a real sim_companion_radio // (native) build, sizeof unchanged at 2760. + // repeat_extra_scope_mask + ch_scope_idx[64] (0xC0DE002B) added 64 bytes, // not 66 -- the struct had 2 bytes of spare tail padding left over from an // earlier bump -- confirmed via a real sim_companion_radio (native) build @@ -777,7 +783,8 @@ struct NodePrefs { // persisted to file // WioTrackerL1_companion_solo_dual (nRF52/ARM) and Heltec_v3_companion_radio_ble // (ESP32) builds, sizeof unchanged at 2824. loc_share_duration_idx (0xC0DE002E) // likewise (sim build; see the check below). -static_assert(sizeof(NodePrefs) == 2824, +// 0xC0DE002F 32 byte bump for lock_screen_password +static_assert(sizeof(NodePrefs) == 2856, "NodePrefs layout changed — sync DataStore save/load + clamp, bump " "SCHEMA_SENTINEL, then update this size (see steps above)."); diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 9bb7bb14..2ae38c42 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -22,6 +22,7 @@ class SettingsScreen : public UIScreen { AUTO_OFF, #endif AUTO_LOCK, + LOCK_PASSWORD, BATT_DISPLAY, #if FEAT_CLOCK_SECONDS_SETTING CLOCK_SECONDS, @@ -589,6 +590,10 @@ class SettingsScreen : public UIScreen { display.print("Auto lock"); display.setCursor(valCol(display), y); display.print((p && p->auto_lock) ? "ON" : "OFF"); + } else if (item == LOCK_PASSWORD) { + display.print("LockPass"); + display.setCursor(valCol(display), y); + display.print((p && p->lock_screen_password[0]) ? "ON" : "OFF"); } else if (item == TIMEZONE) { display.print("Time zone"); char buf[8]; @@ -715,6 +720,7 @@ class SettingsScreen : public UIScreen { // Keyboard state for editing message slots int _edit_slot = -1; // -1 = not editing, 0..9 = slot being edited bool _edit_name = false; // editing DEVICE_NAME via the keyboard + bool _edit_lock_pass = false; // editing the lock-screen password via the keyboard KeyboardWidget* _kb; // Scope list management (SCOPE_NAME row -> a full-screen add/rename/ @@ -786,6 +792,7 @@ public: _scope_action_menu.active = false; _scope_delete_confirm_active = false; _prune_confirm.active = false; + _edit_lock_pass = false; resetList(); _editor.freq.active = false; } @@ -793,7 +800,7 @@ public: int render(DisplayDriver& display) override { display.setTextSize(1); - if (_edit_slot >= 0 || _edit_name || _scope_rename_idx != -2 || _picker.saving) { + if (_edit_slot >= 0 || _edit_name || _edit_lock_pass || _scope_rename_idx != -2 || _picker.saving) { return _kb->render(display); } @@ -861,6 +868,22 @@ public: return true; } + // Keyboard editing mode for the lock-screen password + if (_edit_lock_pass) { + auto res = _kb->handleInput(c); + if (res == KeyboardWidget::DONE) { + if (p) { + strncpy(p->lock_screen_password, _kb->buf, sizeof(p->lock_screen_password) - 1); + p->lock_screen_password[sizeof(p->lock_screen_password) - 1] = '\0'; + _dirty = true; // savePrefsIfDirty persists new password + } + _edit_lock_pass = false; + } else if (res == KeyboardWidget::CANCELLED) { + _edit_lock_pass = false; + } + return true; + } + // Keyboard editing mode for adding/renaming a scope-list entry if (_scope_rename_idx != -2) { auto res = _kb->handleInput(c); @@ -1111,6 +1134,18 @@ public: _dirty = true; return true; } + // LockPass: Clear password if defined or get input from keyboard + if (_selected == LOCK_PASSWORD && p && enter) { + if (p->lock_screen_password[0]) { + p->lock_screen_password[0] = '\0'; + _dirty = true; + } else { + _edit_lock_pass = true; + _kb->begin("", (int)sizeof(p->lock_screen_password) - 1); + _kb->clearPlaceholders(); // a password is literal, not a template message + } + return true; + } if (_selected == TIMEZONE && p) { if (right && p->tz_offset_hours < 14) { p->tz_offset_hours++; _dirty = true; return true; } if (left && p->tz_offset_hours > -12) { p->tz_offset_hours--; _dirty = true; return true; } From b7109102259bc72637dd48d455a3b7d8ffcaba51 Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Sun, 6 Sep 2026 16:25:03 +0200 Subject: [PATCH 2/8] feat: Add password input for screen unlock --- docs/solo_features/screen_lock/screen_lock.md | 12 ++ examples/companion_radio/ui-new/UITask.cpp | 112 ++++++++++++++---- examples/companion_radio/ui-new/UITask.h | 13 +- 3 files changed, 116 insertions(+), 21 deletions(-) diff --git a/docs/solo_features/screen_lock/screen_lock.md b/docs/solo_features/screen_lock/screen_lock.md index 656a34c8..339eaa79 100644 --- a/docs/solo_features/screen_lock/screen_lock.md +++ b/docs/solo_features/screen_lock/screen_lock.md @@ -63,3 +63,15 @@ Fully autonomous, independent of Auto-lock and of any key combo: - **Magnet near (cover closed)** — locks and blanks the display immediately, no wake grace. - **Magnet away (cover opened)** — unlocks and wakes the display right away. + +--- + +### Lock-screen password + +Enable **LockPass** in **Settings › Display** to require a password when unlocking the device. + +Upon enabling the setting the keyboard will show, requiring you to submit a new password. +Attempting to unlock the device will then require the same password to be typed and submitted to reach the home screen. +Submitting an incorrect password clears the keyboard input. + +Disabling the setting clears the stored password and allows an instant unlock again. diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0146ef4c..56a0b465 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2212,6 +2212,73 @@ void UITask::syncLockToHome() { if (home) static_cast(home)->setLocked(_locked); } +bool UITask::passwordLockEnabled() const { + // Is lock_screen_password setting not empty? + return _node_prefs && _node_prefs->lock_screen_password[0] != '\0'; +} + +void UITask::beginUnlockPrompt() { + _unlock_kb = true; // Track that keyboard is visible and is waiting for input + int max_len = _node_prefs ? (int)sizeof(_node_prefs->lock_screen_password) - 1 : 32; + _kb.begin("", max_len); + _kb.clearPlaceholders(); + _lock_wake_until = millis() + 5000; // keep the display on while typing + _next_refresh = 0; +} + +void UITask::cancelUnlockPrompt() { + _unlock_kb = false; + _next_refresh = 100; +} + +void UITask::handleUnlockKey(char c) { + auto res = _kb.handleInput(c); + if (res == KeyboardWidget::DONE) { // Process input on submit + if (_node_prefs && strcmp(_kb.buf, _node_prefs->lock_screen_password) == 0) { + // Match: Unlock + _unlock_kb = false; + _locked = false; + if (_display && !_display->isOn()) _display->turnOn(); + uint32_t aoff = autoOffMillis(); + if (aoff > 0) _auto_off = millis() + aoff; + syncLockToHome(); + } else { + // Invalid: Clear input and reset keyboard + _kb.len = 0; + _kb.buf[0] = '\0'; + _kb.cursor_pos = 0; + _kb.page = _kb.row = _kb.col = 0; + _kb.caps = _kb.caps_lock = false; + _next_refresh = 0; + } + } else if (res == KeyboardWidget::CANCELLED) { + cancelUnlockPrompt(); + } + _lock_wake_until = millis() + 5000; // keep the display on while typing +} + +void UITask::toggleLock() { + if (_unlock_kb) { + cancelUnlockPrompt(); + return; + } + if (_locked) { + if (passwordLockEnabled()) { // Prompt for password + beginUnlockPrompt(); + } else { // ...or unlock instantly + _locked = false; + if (_display && !_display->isOn()) _display->turnOn(); + uint32_t aoff = autoOffMillis(); + if (aoff > 0) _auto_off = millis() + aoff; + } + } else { // Device is currently unlocked -> lock it + _locked = true; + _lock_wake_until = millis() + 2000; // Briefly show lockscreen before blanking + } + syncLockToHome(); + _next_refresh = 0; +} + bool UITask::savePrefsIfDirty(bool& dirty) { if (!dirty) return false; the_mesh.savePrefs(); @@ -2561,15 +2628,7 @@ void UITask::pollCardKB() { // two-key combo, so it doesn't need the physical combo's extra 3x // repetition to guard against accidental triggering. if (_display && !_display->isOn()) _display->turnOn(); - _locked = !_locked; - if (_locked) { - _lock_wake_until = millis() + 2000; - } else { - uint32_t aoff = autoOffMillis(); - if (aoff > 0) _auto_off = millis() + aoff; - } - syncLockToHome(); - _next_refresh = 0; + toggleLock(); return; } else if (raw >= 0x80 && raw <= 0xAF) { // Fn+ -- open its accent popup char base = CARDKB_FN_BASE[raw - 0x80]; @@ -2623,6 +2682,7 @@ void UITask::pollHallSensor() { _hall_magnet_present = present; if (present) { // cover closed + cancelUnlockPrompt(); // a cover can't be over the password prompt _locked = true; syncLockToHome(); _lock_wake_until = 0; @@ -2663,15 +2723,7 @@ void UITask::loop() { if (_lock_seq_count >= 3) { _lock_seq_count = 0; _lock_seq_used = true; // suppress Back release click - _locked = !_locked; - if (_locked) { - _lock_wake_until = millis() + 2000; - } else { - if (_display && !_display->isOn()) _display->turnOn(); - uint32_t aoff = autoOffMillis(); - if (aoff > 0) _auto_off = millis() + aoff; - } - syncLockToHome(); + toggleLock(); } // eat the Enter — don't pass to curr } else { @@ -2864,7 +2916,12 @@ void UITask::loop() { } if (_kq_head != _kq_tail) { - if (!_locked && curr) { + if (_unlock_kb) { + // Lock-screen password keyboard: Consume every key press + char k; + while (dequeueKey(k)) handleUnlockKey(k); + _next_refresh = 100; // 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. char k; @@ -2901,8 +2958,22 @@ void UITask::loop() { tickClockTools(); if (_display != NULL && _display->isOn()) { - if (_locked && (int32_t)(millis() - _lock_wake_until) >= 0) { + // Lock-screen password prompt + if (_locked && _unlock_kb && (int32_t)(millis() - _lock_wake_until) >= 0) { + cancelUnlockPrompt(); // Cancel unlock attempt on idle + _next_refresh = 0; + } + if (_locked && !_unlock_kb && (int32_t)(millis() - _lock_wake_until) >= 0) { _display->turnOff(); + } else if (_locked && _unlock_kb && millis() >= _next_refresh) { + // While the prompt is up the password keyboard replaces the lockscreen view + _display->startFrame(); + _kb.beginFrame(); + int delay_millis = _kb.render(*_display); + // Alert overlay on top // TODO: Is this necessary here? + // if (millis() < _alert_expiry) renderAlertOverlay(); + _display->endFrame(); + _next_refresh = millis() + delay_millis; } else if (_locked && millis() >= _next_refresh && home) { _display->startFrame(); home->render(*_display); @@ -2950,6 +3021,7 @@ void UITask::loop() { digitalWrite(PIN_LED, LOW); // turn off status LED with display to save power #endif if (_node_prefs && _node_prefs->auto_lock) { + cancelUnlockPrompt(); // idle-lock isn't a password prompt _locked = true; _lock_wake_until = 0; syncLockToHome(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 52f5b020..8fe7056d 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -58,6 +58,8 @@ class UITask : public AbstractUITask { int _lock_seq_count; // Enter presses while Back held (lock/unlock sequence) unsigned long _lock_seq_ms; // millis() of last lock-sequence press (for timeout) bool _lock_seq_used; // true = suppress next back_btn CLICK (post-sequence release) + // True while the lock screen shows the on-screen keyboard and waits for submission + bool _unlock_kb = false; char _alert[80]; char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies // Persistent RTTTL buffer for the bot !buzz command (see botBuzz()) -- sized @@ -288,6 +290,15 @@ private: // dedicated lock-screen code path in loop(). void syncLockToHome(); + // Whether a lockscreen password is set + bool passwordLockEnabled() const; + // Handles (un)locking and requesting password input when one is set + void toggleLock(); + void beginUnlockPrompt(); + void cancelUnlockPrompt(); + // Handles shortcuts during lockscreen password input + void handleUnlockKey(char c); + // Centred alert overlay (the showAlert() box). Wraps long text to up to // three lines inside the box instead of letting it overflow the border. // Shared by the normal render path and the lock screen (so a ringing @@ -566,7 +577,7 @@ public: #endif } - bool isBuzzerQuiet() { + bool isBuzzerQuiet() { #ifdef PIN_BUZZER return buzzer.isQuiet(); #else From 6404622ebf424d24d15ec429b9d0fab30474ea47 Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Sun, 6 Sep 2026 17:52:41 +0200 Subject: [PATCH 3/8] feat: Lock devices with password set on boot --- docs/solo_features/screen_lock/screen_lock.md | 2 +- examples/companion_radio/ui-new/UITask.cpp | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/solo_features/screen_lock/screen_lock.md b/docs/solo_features/screen_lock/screen_lock.md index 339eaa79..0297a6f6 100644 --- a/docs/solo_features/screen_lock/screen_lock.md +++ b/docs/solo_features/screen_lock/screen_lock.md @@ -68,7 +68,7 @@ Fully autonomous, independent of Auto-lock and of any key combo: ### Lock-screen password -Enable **LockPass** in **Settings › Display** to require a password when unlocking the device. +Enable **LockPass** in **Settings › Display** to require a password when unlocking or powering up the device. Upon enabling the setting the keyboard will show, requiring you to submit a new password. Attempting to unlock the device will then require the same password to be typed and submitted to reach the home screen. diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 56a0b465..7171e2fb 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1592,6 +1592,13 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no } #endif + // Lock device on boot if password is enabled to prevent bypassing it by resetting device + if (passwordLockEnabled()) { + _locked = true; + // Add BOOT_SCREEN_MILLIS to make sure splash screen still shows + _lock_wake_until = millis() + BOOT_SCREEN_MILLIS + 5000; + } + #if defined(PIN_USER_BTN) user_btn.begin(); #endif @@ -2976,12 +2983,17 @@ void UITask::loop() { _next_refresh = millis() + delay_millis; } else if (_locked && millis() >= _next_refresh && home) { _display->startFrame(); - home->render(*_display); + if (curr && curr != home) { + // Boot splash is still up on a boot-locked device + _next_refresh = millis() + curr->render(*_display); + } else { + home->render(*_display); + _next_refresh = millis() + Features::LOCKSCREEN_REFRESH_MS; + } // 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(); _display->endFrame(); - _next_refresh = millis() + Features::LOCKSCREEN_REFRESH_MS; } else if (!_locked && millis() >= _next_refresh && curr) { _display->startFrame(); _kb.beginFrame(); From f2209c338d1c2c4bfa7d55d1f8c7802b1113af67 Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Sun, 6 Sep 2026 19:49:07 +0200 Subject: [PATCH 4/8] feat: Hash & salt lock screen password --- examples/companion_radio/DataStore.cpp | 22 +++++++++++- examples/companion_radio/NodePrefs.h | 9 +++-- .../companion_radio/ui-new/SettingsScreen.h | 5 ++- examples/companion_radio/ui-new/UITask.cpp | 35 ++++++++++++++++++- examples/companion_radio/ui-new/UITask.h | 5 +++ 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index ee714f89..ab53ec5b 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -1,3 +1,20 @@ +/* + * File: DataStore.cpp + * Project: companion_radio + * Created Date: 2026-09-26 12:03:18 + * Author: 3urobeat + * + * Last Modified: 2026-09-26 12:16:49 + * Modified By: 3urobeat + * + * Copyright (c) 2026 3urobeat + * + * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + * You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + */ + + #include #include "DataStore.h" #include "Features.h" // FEAT_JOYSTICK_ROTATION_SETTING (else `#if !FEAT_…` is always true) @@ -624,8 +641,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no // append the lock-screen password. Should be empty by default // since struct was zero initialized in begin(), meaning password is disabled + // → 0xC0DE002F: append the lock-screen password + // → 0xC0DE0030: append the per-device password salt rd(_prefs.lock_screen_password, sizeof(_prefs.lock_screen_password)); - _prefs.lock_screen_password[sizeof(_prefs.lock_screen_password) - 1] = '\0'; + rd(_prefs.lock_screen_password_salt, sizeof(_prefs.lock_screen_password_salt)); // Schema sentinel: bumped on layout changes. Mismatch means an older file // (or a different schema); rd() and the clamps above already keep every @@ -825,6 +844,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.loc_share_scope, sizeof(_prefs.loc_share_scope)); file.write((uint8_t *)&_prefs.loc_share_duration_idx, sizeof(_prefs.loc_share_duration_idx)); file.write((uint8_t *)_prefs.lock_screen_password, sizeof(_prefs.lock_screen_password)); + file.write((uint8_t *)_prefs.lock_screen_password_salt, sizeof(_prefs.lock_screen_password_salt)); // Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL. Its write is // the one we check: once the flash fills, writes return 0, so a good diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 8dbc7ba6..d426b8ef 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -539,7 +539,9 @@ struct NodePrefs { // persisted to file // Lock-screen password, empty by default. If set, a password is required to // unlock the lock screen. static const uint8_t LOCK_PASSWORD_MAX_LEN = 32; - char lock_screen_password[LOCK_PASSWORD_MAX_LEN]; + static const uint8_t lock_screen_password_salt_LEN = 16; + uint8_t lock_screen_password[LOCK_PASSWORD_MAX_LEN]; + uint8_t lock_screen_password_salt[lock_screen_password_salt_LEN]; // Single source of truth for the live-share option tables (shared by the Map // UI labels and the auto-send engine in UITask). @@ -628,7 +630,7 @@ struct NodePrefs { // persisted to file // repeat_* fields) instead of at the tail, which shifted every field after // them by 25 bytes when loading an older file. Never released, but a dev // build wrote it, so the number must not be reused for anything else. - static const uint32_t SCHEMA_SENTINEL = 0xC0DE002F; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE0030; // Bit-index for each home page. Used by page_order (entries store bit+1) and // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage @@ -784,7 +786,8 @@ struct NodePrefs { // persisted to file // (ESP32) builds, sizeof unchanged at 2824. loc_share_duration_idx (0xC0DE002E) // likewise (sim build; see the check below). // 0xC0DE002F 32 byte bump for lock_screen_password -static_assert(sizeof(NodePrefs) == 2856, +// 0xC0DE0030 16 byte bump for lock_screen_password_salt +static_assert(sizeof(NodePrefs) == 2872, "NodePrefs layout changed — sync DataStore save/load + clamp, bump " "SCHEMA_SENTINEL, then update this size (see steps above)."); diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 2ae38c42..3b74b680 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -873,8 +873,7 @@ public: auto res = _kb->handleInput(c); if (res == KeyboardWidget::DONE) { if (p) { - strncpy(p->lock_screen_password, _kb->buf, sizeof(p->lock_screen_password) - 1); - p->lock_screen_password[sizeof(p->lock_screen_password) - 1] = '\0'; + _task->setNodeLockPassword(_kb->buf); _dirty = true; // savePrefsIfDirty persists new password } _edit_lock_pass = false; @@ -1137,7 +1136,7 @@ public: // LockPass: Clear password if defined or get input from keyboard if (_selected == LOCK_PASSWORD && p && enter) { if (p->lock_screen_password[0]) { - p->lock_screen_password[0] = '\0'; + _task->setNodeLockPassword(""); _dirty = true; } else { _edit_lock_pass = true; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 7171e2fb..9ba6da76 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2224,6 +2224,39 @@ bool UITask::passwordLockEnabled() const { return _node_prefs && _node_prefs->lock_screen_password[0] != '\0'; } +void UITask::setNodeLockPassword(const char* plain) { + if (!_node_prefs || !plain) return; + if (plain[0] == '\0') { // Clear the password + memset(_node_prefs->lock_screen_password, 0, sizeof(_node_prefs->lock_screen_password)); + memset(_node_prefs->lock_screen_password_salt, 0, sizeof(_node_prefs->lock_screen_password_salt)); + return; + } + // Generate a fresh random salt and store salted SHA-256 digest + mesh::RNG* rng = the_mesh.getRNG(); + if (rng) { + rng->random(_node_prefs->lock_screen_password_salt, sizeof(_node_prefs->lock_screen_password_salt)); + } else { + // Time as fallback entropy source + uint32_t t = (uint32_t)millis() ^ (uint32_t)rtc_clock.getCurrentTime(); + memcpy(_node_prefs->lock_screen_password_salt, &t, sizeof(t)); + } + mesh::Utils::sha256((uint8_t*)_node_prefs->lock_screen_password, + sizeof(_node_prefs->lock_screen_password), + _node_prefs->lock_screen_password_salt, + sizeof(_node_prefs->lock_screen_password_salt), + (const uint8_t*)plain, (int)strlen(plain)); +} + +bool UITask::checkNodeLockPassword(const char* entered) const { + if (!_node_prefs || !entered) return false; + uint8_t digest[NodePrefs::LOCK_PASSWORD_MAX_LEN]; + mesh::Utils::sha256(digest, sizeof(digest), + _node_prefs->lock_screen_password_salt, + sizeof(_node_prefs->lock_screen_password_salt), + (const uint8_t*)entered, (int)strlen(entered)); + return memcmp(digest, _node_prefs->lock_screen_password, sizeof(digest)) == 0; +} + void UITask::beginUnlockPrompt() { _unlock_kb = true; // Track that keyboard is visible and is waiting for input int max_len = _node_prefs ? (int)sizeof(_node_prefs->lock_screen_password) - 1 : 32; @@ -2241,7 +2274,7 @@ void UITask::cancelUnlockPrompt() { void UITask::handleUnlockKey(char c) { auto res = _kb.handleInput(c); if (res == KeyboardWidget::DONE) { // Process input on submit - if (_node_prefs && strcmp(_kb.buf, _node_prefs->lock_screen_password) == 0) { + if (_node_prefs && checkNodeLockPassword(_kb.buf)) { // Match: Unlock _unlock_kb = false; _locked = false; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 8fe7056d..964842fe 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -306,6 +306,11 @@ private: void renderAlertOverlay(); public: + // Stores new lock screen salted SHA-256 password + // `plain` is the raw user input, passing "" clears currently set password + void setNodeLockPassword(const char* plain); + // Verifies entered against the stored password hash. Returns match as bool + bool checkNodeLockPassword(const char* entered) const; UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) { next_batt_chck = _next_refresh = 0; From 47981da98c17de6d3db3691d30f73cb52cfc9982 Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Sun, 6 Sep 2026 20:25:50 +0200 Subject: [PATCH 5/8] fix: Fix cover open being able to bypass lock (whoops) and hopefully fix flickering during password input --- examples/companion_radio/ui-new/UITask.cpp | 23 ++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 9ba6da76..6aaec80f 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2731,11 +2731,17 @@ void UITask::pollHallSensor() { digitalWrite(PIN_LED, LOW); // same as the auto-off path -- one less thing lit under a closed cover #endif } else { // cover opened - _locked = false; - syncLockToHome(); - if (_display && !_display->isOn()) _display->turnOn(); - uint32_t aoff = autoOffMillis(); - if (aoff > 0) _auto_off = millis() + aoff; + if (passwordLockEnabled()) { // Redirect flow to usual unlock prompt when password is enabled + _locked = true; + if (_display) _display->turnOn(); + beginUnlockPrompt(); + } else { + _locked = false; + syncLockToHome(); + if (_display && !_display->isOn()) _display->turnOn(); + uint32_t aoff = autoOffMillis(); + if (aoff > 0) _auto_off = millis() + aoff; + } } _next_refresh = 0; #endif @@ -2750,7 +2756,7 @@ void UITask::loop() { uint8_t joy_rot = _node_prefs ? _node_prefs->joystick_rotation : JOYSTICK_ROTATION; int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { - if (back_btn.isPressed()) { + if (back_btn.isPressed() && !_unlock_kb) { // Enter clicked while Back is held — lock/unlock sequence if (_display && !_display->isOn()) { _display->turnOn(); // turn on display so hints are visible @@ -2767,6 +2773,11 @@ void UITask::loop() { } // eat the Enter — don't pass to curr } else { + // While the password keyboard is open, Back+Enter is just typing + if (_unlock_kb) { + _lock_seq_count = 0; + _lock_seq_ms = 0; + } enqueueKey(checkDisplayOn(KEY_ENTER)); } } else if (ev == BUTTON_EVENT_LONG_PRESS) { From a16c9bf2da88772d56dc6f14e7695b2195137ee9 Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Tue, 8 Sep 2026 21:13:52 +0200 Subject: [PATCH 6/8] chore: Make sure lockscreen always wins --- examples/companion_radio/ui-new/UITask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6aaec80f..6dc9a293 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -3027,7 +3027,7 @@ void UITask::loop() { _next_refresh = millis() + delay_millis; } else if (_locked && millis() >= _next_refresh && home) { _display->startFrame(); - if (curr && curr != home) { + if (curr && curr != home && (millis() - ui_started_at < BOOT_SCREEN_MILLIS)) { // Boot splash is still up on a boot-locked device _next_refresh = millis() + curr->render(*_display); } else { From 6f8703842ce343fb4fd4708c6f272df5be53f146 Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Tue, 8 Sep 2026 22:25:39 +0200 Subject: [PATCH 7/8] feat: Add PIN input keyboard and use it on lockscreen --- examples/companion_radio/NodePrefs.h | 3 +- .../companion_radio/ui-new/KeyboardWidget.h | 122 +++++++++++++++++- .../companion_radio/ui-new/SettingsScreen.h | 6 +- examples/companion_radio/ui-new/UITask.cpp | 8 +- examples/companion_radio/ui-new/UITask.h | 1 + examples/companion_radio/ui-new/icons.h | 5 + 6 files changed, 139 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index d426b8ef..c4ea4061 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -214,7 +214,8 @@ struct NodePrefs { // persisted to file static const uint8_t PAGE_ORDER_MAGIC = 0xA5; // On-screen keyboard layout, shared across every text-entry screen (Settings > // Keyboard). 0=ABC grid, alphabetical order (default), 1=T9 multi-tap - // (phone-keypad groups, cycled with repeated Enter presses — see KeyboardWidget.h). + // (phone-keypad groups, cycled with repeated Enter presses — see KeyboardWidget.h), + // 3=PIN keyboard. uint8_t keyboard_type; // Additional (non-Latin) keyboard alphabet, orthogonal to keyboard_type above // — either layout style (ABC grid or T9) can show any alphabet's characters. diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 1b907ba3..34f70e4e 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -43,6 +43,17 @@ static const char* const KB_T9_GROUPS[KB_PAGES][9] = { { "@#&", "*()", "-_+", "=/\\", ":;'\"", "<>[]", "{}|~", "^$%`", ",." }, // page 1 — symbols }; +// PIN input layout: Classic number pad +static const int KB_PIN_ROWS = 4; +static const int KB_PIN_COLS = 3; +static const int KB_PIN_SPECIAL = 3; // backspace, abc kb switch & submit +static const char KB_PIN_DIGITS[KB_PIN_ROWS][KB_PIN_COLS] = { + {'1','2','3'}, + {'4','5','6'}, + {'7','8','9'}, + {' ','0',' '}, +}; + // Non-Latin keyboard scripts. NodePrefs::keyboard_main_alphabet/ // keyboard_alt_alphabet (Settings > Keyboard's Main/Additional rows) pick // which script occupies page 0 (the keyboard's default/opening page) and @@ -325,6 +336,7 @@ struct KeyboardWidget { // to ABC and mainScript()/altScript() default to Latin-only. NodePrefs* prefs = nullptr; bool isT9() const { return prefs && prefs->keyboard_type == 1; } + bool isPin() const { return prefs && prefs->keyboard_type == 2; } // Which script occupies page 0 (the keyboard's default/opening page) and // which occupies page 1 (reached by the #@/abc cycle key) -- Settings > // Keyboard's Main/Additional rows. Additional equal to Main collapses to no @@ -346,8 +358,8 @@ struct KeyboardWidget { // lowercase regardless of Shift. bool t9_caps = false; - int gridRows() const { return isT9() ? KB_T9_ROWS : KB_ROWS_CHAR; } - int gridCols() const { return isT9() ? KB_T9_COLS : KB_COLS_CHAR; } + int gridRows() const { return isPin() ? KB_PIN_ROWS : (isT9() ? KB_T9_ROWS : KB_ROWS_CHAR); } + int gridCols() const { return isPin() ? KB_PIN_COLS : (isT9() ? KB_T9_COLS : KB_COLS_CHAR); } // ── Page model ──────────────────────────────────────────────────────────── // Logical page order: 0 = mainScript(), [1 = altScript(), if it differs], @@ -438,6 +450,12 @@ struct KeyboardWidget { addPlaceholder("{time}"); } + // Open keyboard in PIN mode + void beginPin(const char* initial = "", int max = KB_MAX_LEN) { + if (prefs) prefs->keyboard_type = 2; // Force number input + begin(initial, max); + } + // Insert one UTF-8 codepoint (a grid cell's own glyph, or a picked accent // variant) at cursor_pos, applying Shift/caps-lock the same way every cell // commit does. Shared by the plain-Latin-cell commit below and the accent @@ -628,6 +646,41 @@ struct KeyboardWidget { return 50; } + // PIN mode + if (isPin()) { + const int s = miniIconScale(display); + for (int r = 0; r < rows; r++) { + int y = chars_y + r * cell_h; + for (int c = 0; c < cols; c++) { + bool sel = (row == r && col == c); + int cx = c * cell_w; + display.drawSelectionRow(cx, y - 1, cell_w - 1, cell_h, sel); + char ch = KB_PIN_DIGITS[r][c]; + if (ch == ' ') continue; // blank cells beside 0 + char ch_buf[2]; + ch_buf[0] = ch; + ch_buf[1] = '\0'; + int tw = display.getTextWidth(ch_buf); + display.setCursor(cx + (cell_w - tw) / 2, y); + display.print(ch_buf); + } + } + // special row with backspace, abc kb switch & submit + const int psw = display.width() / KB_PIN_SPECIAL; + const int icy = spec_y + (cell_h - lh) / 2; + for (int i = 0; i < KB_PIN_SPECIAL; i++) { + bool sel = (row == rows && col == i); + int sx = i * psw; + display.drawSelectionRow(sx, spec_y - 1, psw - 1, cell_h, sel); + const MiniIcon& ic = (i == 0) ? ICON_BACKSPACE + : (i == 1) ? ICON_KEYBOARD + : ICON_CHECK; + int ix = sx + (psw - ic.w * s) / 2; + miniIconDraw(display, ix, icy, ic); + } + return 50; + } + // Compact mode (Settings > Keyboard's "Ext. KB" row): an external-keyboard // typist never looks at the letter grid or special-row icons, so skip // drawing them entirely -- no status line either, since nothing it could @@ -791,7 +844,7 @@ struct KeyboardWidget { // just fine -- so this only checks that no other exclusive input mode // (popup/cursor-move) is already in progress, same as inPlainGridState(). bool openAccentFor(char base) { - if (!isVisible() || _ph_menu.active || cursor_mode || accent_active) return false; + if (!isVisible() || isPin() || _ph_menu.active || cursor_mode || accent_active) return false; int gi = findAccentGroup(base); if (gi < 0) return false; accent_active = true; @@ -908,6 +961,69 @@ struct KeyboardWidget { return NONE; } + // PIN mode: a dedicated numeric keypad + if (isPin()) { + const int rows = gridRows(); + const int cols = gridCols(); + if (c == KEY_CONTEXT_MENU) { + if (row == rows && col == 0) { // Backspace + len = 0; buf[0] = '\0'; + cursor_pos = 0; + t9_cell = -1; + } + return NONE; + } + if (c == KEY_UP) { // Navigation + row = (row > 0) ? row - 1 : rows; + return NONE; + } + if (c == KEY_DOWN) { + row = (row < rows) ? row + 1 : 0; + return NONE; + } + if (c == KEY_LEFT) { + int max_col = (row == rows) ? KB_PIN_SPECIAL - 1 : cols - 1; + col = (col > 0) ? col - 1 : max_col; + return NONE; + } + if (c == KEY_RIGHT) { + int max_col = (row == rows) ? KB_PIN_SPECIAL - 1 : cols - 1; + col = (col < max_col) ? col + 1 : 0; + return NONE; + } + if (c == KEY_ENTER) { + if (row < rows) { + char d = KB_PIN_DIGITS[row][col]; + if (d == ' ') return NONE; // ignore blanks + if (len < max_len) { // Digit + memmove(buf + cursor_pos + 1, buf + cursor_pos, len - cursor_pos); + buf[cursor_pos] = d; + len++; cursor_pos++; + buf[len] = '\0'; + } + return NONE; + } + // special row + if (col == 0) { + t9_cell = -1; + if (cursor_pos > 0) { + int n = kbUtf8LastCharBytes(buf, cursor_pos); + memmove(buf + cursor_pos - n, buf + cursor_pos, len - cursor_pos); + len -= n; cursor_pos -= n; + buf[len] = '\0'; + } + } else if (col == 1) { + if (prefs) prefs->keyboard_type = 0; // ABC switch + page = 0; + t9_cell = -1; + } else { + return DONE; // Submit + } + return NONE; + } + return NONE; + } + const int rows = gridRows(); const int cols = gridCols(); diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 3b74b680..b855d480 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -721,6 +721,7 @@ class SettingsScreen : public UIScreen { int _edit_slot = -1; // -1 = not editing, 0..9 = slot being edited bool _edit_name = false; // editing DEVICE_NAME via the keyboard bool _edit_lock_pass = false; // editing the lock-screen password via the keyboard + uint8_t _lock_pass_saved_type = 0; // remember keyboard set to restore setting after pin entry KeyboardWidget* _kb; // Scope list management (SCOPE_NAME row -> a full-screen add/rename/ @@ -877,8 +878,10 @@ public: _dirty = true; // savePrefsIfDirty persists new password } _edit_lock_pass = false; + if (p) p->keyboard_type = _lock_pass_saved_type; // restore user setting } else if (res == KeyboardWidget::CANCELLED) { _edit_lock_pass = false; + if (p) p->keyboard_type = _lock_pass_saved_type; // restore user setting } return true; } @@ -1140,7 +1143,8 @@ public: _dirty = true; } else { _edit_lock_pass = true; - _kb->begin("", (int)sizeof(p->lock_screen_password) - 1); + _lock_pass_saved_type = p ? p->keyboard_type : 0; // remember setting to restore after + _kb->beginPin("", (int)sizeof(p->lock_screen_password) - 1); _kb->clearPlaceholders(); // a password is literal, not a template message } return true; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6dc9a293..3ab78ac4 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2260,7 +2260,8 @@ bool UITask::checkNodeLockPassword(const char* entered) const { void UITask::beginUnlockPrompt() { _unlock_kb = true; // Track that keyboard is visible and is waiting for input int max_len = _node_prefs ? (int)sizeof(_node_prefs->lock_screen_password) - 1 : 32; - _kb.begin("", max_len); + _lock_pin_restore_kb_type = _node_prefs ? _node_prefs->keyboard_type : 0; + _kb.beginPin("", max_len); _kb.clearPlaceholders(); _lock_wake_until = millis() + 5000; // keep the display on while typing _next_refresh = 0; @@ -2268,6 +2269,10 @@ void UITask::beginUnlockPrompt() { void UITask::cancelUnlockPrompt() { _unlock_kb = false; + if (_node_prefs) _node_prefs->keyboard_type = _lock_pin_restore_kb_type; // restore user setting + _kb.buf[0] = '\0'; // clear input + _kb.len = 0; + _kb.cursor_pos = 0; _next_refresh = 100; } @@ -2278,6 +2283,7 @@ void UITask::handleUnlockKey(char c) { // Match: Unlock _unlock_kb = false; _locked = false; + if (_node_prefs) _node_prefs->keyboard_type = _lock_pin_restore_kb_type; // restore user setting if (_display && !_display->isOn()) _display->turnOn(); uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 964842fe..b59ea80e 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -60,6 +60,7 @@ class UITask : public AbstractUITask { bool _lock_seq_used; // true = suppress next back_btn CLICK (post-sequence release) // True while the lock screen shows the on-screen keyboard and waits for submission bool _unlock_kb = false; + uint8_t _lock_pin_restore_kb_type = 0; // remember keyboard set to restore setting after pin entry char _alert[80]; char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies // Persistent RTTTL buffer for the bot !buzz command (see botBuzz()) -- sized diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index 8a05e853..89813de4 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -451,6 +451,11 @@ MINI_ICON(ICON_MAP_TARGET, 5, // ⚑ flag on a pole — the active Locator/N packRow("#....")); // Keyboard special-key glyphs. +MINI_ICON(ICON_KEYBOARD, 7, // PIN keyboard to ABC keyboard switch icon + packRow("#.#.#.#"), + packRow("#######"), + packRow("#.#.#.#"), + packRow("#######")); MINI_ICON(ICON_SHIFT, 7, // ⇧ caps packRow("...#..."), packRow("..###.."), From 02726edcdcac4bb1c41b0e0e8cbf2d4ea5babd7a Mon Sep 17 00:00:00 2001 From: 3urobeat Date: Wed, 16 Sep 2026 18:44:24 +0200 Subject: [PATCH 8/8] feat: Mask PIN input on lockscreen --- .../companion_radio/ui-new/KeyboardWidget.h | 18 +++++++++++++++++- examples/companion_radio/ui-new/UITask.cpp | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/ui-new/KeyboardWidget.h b/examples/companion_radio/ui-new/KeyboardWidget.h index 34f70e4e..9d73965a 100644 --- a/examples/companion_radio/ui-new/KeyboardWidget.h +++ b/examples/companion_radio/ui-new/KeyboardWidget.h @@ -297,6 +297,7 @@ struct KeyboardWidget { bool accent_active = false; // true while the Hold-Enter accent popup is open int accent_group = -1; // index into KB_ACCENT_VARIANTS for the held cell's base letter int accent_sel = 0; // selected variant within that group + bool pin_kb_mask_enabled = false; // Whether pin keyboard should mask input int row, col; int page; // see totalPages()/scriptAt()/pageIsSymbols() below bool caps; @@ -438,6 +439,7 @@ struct KeyboardWidget { page = 0; caps = false; caps_lock = false; + pin_kb_mask_enabled = false; t9_cell = -1; t9_cycle = 0; _ph_menu.active = false; @@ -451,9 +453,10 @@ struct KeyboardWidget { } // Open keyboard in PIN mode - void beginPin(const char* initial = "", int max = KB_MAX_LEN) { + void beginPin(const char* initial = "", int max = KB_MAX_LEN, bool mask = false) { if (prefs) prefs->keyboard_type = 2; // Force number input begin(initial, max); + pin_kb_mask_enabled = mask; // Mask input of number field } // Insert one UTF-8 codepoint (a grid cell's own glyph, or a picked accent @@ -622,6 +625,19 @@ struct KeyboardWidget { } else { linebuf[0] = '\0'; } + // Mask input when pin_kb_mask is enabled + if (pin_kb_mask_enabled) { + char masked[KB_PREVIEW_BYTES + 2]; + int mi = 0; + int blen = (int)strlen(linebuf); + for (int bi = 0; bi < blen; ) { + int u = kbUtf8CharBytesAt(linebuf, bi, blen); + masked[mi++] = (u == 1 && linebuf[bi] == '_') ? '_' : '*'; + bi += u; + } + masked[mi] = '\0'; + strncpy(linebuf, masked, sizeof(linebuf)); + } char linebuf_t[KB_PREVIEW_BYTES + 2]; display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t)); display.setCursor(0, pl * lh); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 3ab78ac4..5d4b6fd0 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2261,7 +2261,7 @@ void UITask::beginUnlockPrompt() { _unlock_kb = true; // Track that keyboard is visible and is waiting for input int max_len = _node_prefs ? (int)sizeof(_node_prefs->lock_screen_password) - 1 : 32; _lock_pin_restore_kb_type = _node_prefs ? _node_prefs->keyboard_type : 0; - _kb.beginPin("", max_len); + _kb.beginPin("", max_len, true); // Pass true to hide input _kb.clearPlaceholders(); _lock_wake_until = millis() + 5000; // keep the display on while typing _next_refresh = 0;