mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
Merge PR #37: lock screen PIN (3urobeat), adapted
The contributor's feature -- a PIN asked before the screen unlocks, on
boot, and when the magnet cover opens, stored as a salted SHA-256 -- on
top of this branch, with:
- ui-core/ScreenLock.h: set / check / isSet and the tries limit, shared by
L1 and L2. isSet looks at every byte (the PR tested only the first, so one
PIN in 256 silently disabled the lock). 5 misses pause entry for 30 s.
- NodePrefs: the fields at the tail after quiet hours, sentinel 0xC0DE0031,
sizeof 2880; an older file's bytes there are cleared. A sentinel bump no
longer resets display_brightness_pct from a file that already has it.
- DataStore: no AGPL header (the project is MIT), no whitespace churn.
- Keyboard: the number pad is a widget flag (pin_mode) with a prompt in the
preview ("New PIN", "Again", "PIN"), not keyboard_type = 2 written into the
prefs and restored after.
- L1 Settings: "Lock PIN" right after the schema's "Lock screen" row, typed
twice, at least 4 characters, saved at once (a power-off right after
setting it kept no PIN). Home is told about a boot-time lock once it
exists (also fixes booting with the cover closed).
- L2: the screen PIN moves from plain text in NVS to the same hash in
NodePrefs; the old one is migrated on first boot and removed. The pad
unlocks as soon as the hash matches from 4 digits on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -63,3 +63,23 @@ 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 near (cover closed)** — locks and blanks the display immediately, no wake grace.
|
||||||
- **Magnet away (cover opened)** — unlocks and wakes the display right away.
|
- **Magnet away (cover opened)** — unlocks and wakes the display right away.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Lock PIN
|
||||||
|
|
||||||
|
**Settings › Display › Lock PIN** asks for a PIN before the screen unlocks,
|
||||||
|
also right after power-up, and when the magnet cover opens.
|
||||||
|
|
||||||
|
- Enter on the row opens a number pad: type the PIN (at least 4 characters),
|
||||||
|
confirm with ✓, then type it again. The pad's keyboard key switches to the
|
||||||
|
normal keyboard for a PIN with letters.
|
||||||
|
- Unlocking (Back + Enter three times, or opening the cover) shows the pad
|
||||||
|
with the input masked. A wrong PIN says how many tries are left; after 5 in
|
||||||
|
a row, entry pauses for 30 seconds.
|
||||||
|
- Enter on the row again removes the PIN.
|
||||||
|
|
||||||
|
The PIN is kept as a salted SHA-256 hash, never as the PIN itself. It locks
|
||||||
|
the screen only: messages still arrive and the phone app still connects.
|
||||||
|
On the Wio Tracker L2 it's **Settings › Display & power › Screen PIN**
|
||||||
|
(digits only).
|
||||||
|
|||||||
@@ -636,6 +636,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
|||||||
if (_prefs.quiet_from > 23) _prefs.quiet_from = 22;
|
if (_prefs.quiet_from > 23) _prefs.quiet_from = 22;
|
||||||
if (_prefs.quiet_to > 23) _prefs.quiet_to = 7;
|
if (_prefs.quiet_to > 23) _prefs.quiet_to = 7;
|
||||||
|
|
||||||
|
// lock_screen_password / _salt: a salted SHA-256 of the screen PIN, all
|
||||||
|
// zeros = no PIN. An older file's sentinel bytes landing here are cleared
|
||||||
|
// below on the mismatch.
|
||||||
|
rd(_prefs.lock_screen_password, sizeof(_prefs.lock_screen_password));
|
||||||
|
rd(_prefs.lock_screen_password_salt, sizeof(_prefs.lock_screen_password_salt));
|
||||||
|
|
||||||
// Schema sentinel: bumped on layout changes. Mismatch means an older file
|
// Schema sentinel: bumped on layout changes. Mismatch means an older file
|
||||||
// (or a different schema); rd() and the clamps above already keep every
|
// (or a different schema); rd() and the clamps above already keep every
|
||||||
// field within its valid range regardless, so we just log it here —
|
// field within its valid range regardless, so we just log it here —
|
||||||
@@ -645,12 +651,18 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
|||||||
if (sentinel != NodePrefs::SCHEMA_SENTINEL) {
|
if (sentinel != NodePrefs::SCHEMA_SENTINEL) {
|
||||||
MESH_DEBUG_PRINTLN("prefs schema sentinel mismatch: got 0x%08X, expected 0x%08X — re-saving on next change",
|
MESH_DEBUG_PRINTLN("prefs schema sentinel mismatch: got 0x%08X, expected 0x%08X — re-saving on next change",
|
||||||
(unsigned)sentinel, (unsigned)NodePrefs::SCHEMA_SENTINEL);
|
(unsigned)sentinel, (unsigned)NodePrefs::SCHEMA_SENTINEL);
|
||||||
|
// 0xC0DE0030 → 0xC0DE0031: the lock-screen PIN appended; from an older
|
||||||
|
// file it holds its sentinel's bytes, which would read as a PIN nobody knows.
|
||||||
|
if (sentinel < 0xC0DE0031) {
|
||||||
|
memset(_prefs.lock_screen_password, 0, sizeof(_prefs.lock_screen_password));
|
||||||
|
memset(_prefs.lock_screen_password_salt, 0, sizeof(_prefs.lock_screen_password_salt));
|
||||||
|
}
|
||||||
// 0xC0DE002F → 0xC0DE0030: quiet hours appended; from an older file they
|
// 0xC0DE002F → 0xC0DE0030: quiet hours appended; from an older file they
|
||||||
// hold its sentinel's bytes, so start off, 22:00-07:00.
|
// hold its sentinel's bytes, so start off, 22:00-07:00.
|
||||||
if (sentinel < 0xC0DE0030) { _prefs.quiet_hours = 0; _prefs.quiet_from = 22; _prefs.quiet_to = 7; }
|
if (sentinel < 0xC0DE0030) { _prefs.quiet_hours = 0; _prefs.quiet_from = 22; _prefs.quiet_to = 7; }
|
||||||
// 0xC0DE002E → 0xC0DE002F: display_brightness_pct appended; from an older
|
// 0xC0DE002E → 0xC0DE002F: display_brightness_pct appended; from an older
|
||||||
// file it holds a stray sentinel byte (0x2E = "46 %"), so start from the level.
|
// file it holds a stray sentinel byte (0x2E = "46 %"), so start from the level.
|
||||||
_prefs.display_brightness_pct = 0;
|
if (sentinel < 0xC0DE002F) _prefs.display_brightness_pct = 0;
|
||||||
// 0xC0DE002A (v1.27) → 0xC0DE002B: repeat_extra_scope_mask + ch_scope_idx
|
// 0xC0DE002A (v1.27) → 0xC0DE002B: repeat_extra_scope_mask + ch_scope_idx
|
||||||
// appended. Unlike the range-clamped fields, these can't be left with whatever
|
// appended. Unlike the range-clamped fields, these can't be left with whatever
|
||||||
// stray bytes rd() picked up from a pre-0x2B file's own sentinel tail:
|
// stray bytes rd() picked up from a pre-0x2B file's own sentinel tail:
|
||||||
@@ -843,6 +855,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
|||||||
file.write((uint8_t *)&_prefs.quiet_hours, sizeof(_prefs.quiet_hours));
|
file.write((uint8_t *)&_prefs.quiet_hours, sizeof(_prefs.quiet_hours));
|
||||||
file.write((uint8_t *)&_prefs.quiet_from, sizeof(_prefs.quiet_from));
|
file.write((uint8_t *)&_prefs.quiet_from, sizeof(_prefs.quiet_from));
|
||||||
file.write((uint8_t *)&_prefs.quiet_to, sizeof(_prefs.quiet_to));
|
file.write((uint8_t *)&_prefs.quiet_to, sizeof(_prefs.quiet_to));
|
||||||
|
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
|
// 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
|
// the one we check: once the flash fills, writes return 0, so a good
|
||||||
|
|||||||
@@ -548,6 +548,14 @@ struct NodePrefs { // persisted to file
|
|||||||
// ── Custom messages ────────────────────────────────────────────────────
|
// ── Custom messages ────────────────────────────────────────────────────
|
||||||
char custom_msgs[10][140]; // user-defined quick messages (supports {loc}, {time})
|
char custom_msgs[10][140]; // user-defined quick messages (supports {loc}, {time})
|
||||||
|
|
||||||
|
// ── Screen lock PIN (0xC0DE0031) ───────────────────────────────────────
|
||||||
|
// SHA-256 of salt + PIN (see ScreenLock.h); all zeros = no PIN. It locks the
|
||||||
|
// screen only: the radio and the app link keep working.
|
||||||
|
static const uint8_t LOCK_HASH_LEN = 32;
|
||||||
|
static const uint8_t LOCK_SALT_LEN = 16;
|
||||||
|
uint8_t lock_screen_password[LOCK_HASH_LEN];
|
||||||
|
uint8_t lock_screen_password_salt[LOCK_SALT_LEN];
|
||||||
|
|
||||||
// Single source of truth for the live-share option tables (shared by the Map
|
// Single source of truth for the live-share option tables (shared by the Map
|
||||||
// UI labels and the auto-send engine in UITask).
|
// UI labels and the auto-send engine in UITask).
|
||||||
static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
|
static const uint8_t LOC_SHARE_MOVE_COUNT = 4;
|
||||||
@@ -635,7 +643,7 @@ struct NodePrefs { // persisted to file
|
|||||||
// repeat_* fields) instead of at the tail, which shifted every field after
|
// 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
|
// 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.
|
// build wrote it, so the number must not be reused for anything else.
|
||||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0030;
|
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0031;
|
||||||
|
|
||||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
// 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
|
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||||
@@ -819,7 +827,8 @@ static inline bool inQuietHours(const NodePrefs& p, uint32_t utc) {
|
|||||||
// quiet_hours / quiet_from / quiet_to (0xC0DE0030) added 8 bytes, not 3 (next
|
// quiet_hours / quiet_from / quiet_to (0xC0DE0030) added 8 bytes, not 3 (next
|
||||||
// to buzzer_auto, rounded up to the alignment) -- sizeof 2832, confirmed via real
|
// to buzzer_auto, rounded up to the alignment) -- sizeof 2832, confirmed via real
|
||||||
// WioTrackerL1_companion_solo_dual (nRF52/ARM) and L2 (ESP32) builds.
|
// WioTrackerL1_companion_solo_dual (nRF52/ARM) and L2 (ESP32) builds.
|
||||||
static_assert(sizeof(NodePrefs) == 2832,
|
// lock_screen_password / _salt (0xC0DE0031) added 48 bytes -- sizeof 2880.
|
||||||
|
static_assert(sizeof(NodePrefs) == 2880,
|
||||||
"NodePrefs layout changed — sync DataStore save/load + clamp, bump "
|
"NodePrefs layout changed — sync DataStore save/load + clamp, bump "
|
||||||
"SCHEMA_SENTINEL, then update this size (see steps above).");
|
"SCHEMA_SENTINEL, then update this size (see steps above).");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#pragma once
|
||||||
|
// The screen lock's PIN, for every frontend: stored in NodePrefs as a salted
|
||||||
|
// SHA-256 (lock_screen_password / _salt), never as the PIN itself. It locks
|
||||||
|
// the screen only -- the radio and the app link keep working.
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Utils.h>
|
||||||
|
#include "../NodePrefs.h"
|
||||||
|
|
||||||
|
namespace screenlock {
|
||||||
|
|
||||||
|
static const uint8_t TRIES = 5; // misses before a pause
|
||||||
|
static const uint32_t PAUSE_MS = 30000;
|
||||||
|
|
||||||
|
// Any byte set: a digest can start with a zero byte.
|
||||||
|
static bool isSet(const NodePrefs& p) {
|
||||||
|
for (uint8_t b : p.lock_screen_password) if (b) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear(NodePrefs& p) {
|
||||||
|
memset(p.lock_screen_password, 0, sizeof(p.lock_screen_password));
|
||||||
|
memset(p.lock_screen_password_salt, 0, sizeof(p.lock_screen_password_salt));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A new PIN under a fresh salt; "" clears it. The caller saves the prefs.
|
||||||
|
static void set(NodePrefs& p, const char* pin, mesh::RNG* rng) {
|
||||||
|
if (!pin || !pin[0]) { clear(p); return; }
|
||||||
|
if (rng) {
|
||||||
|
rng->random(p.lock_screen_password_salt, sizeof(p.lock_screen_password_salt));
|
||||||
|
} else {
|
||||||
|
uint32_t t = micros() ^ (millis() << 16);
|
||||||
|
memcpy(p.lock_screen_password_salt, &t, sizeof(t));
|
||||||
|
}
|
||||||
|
mesh::Utils::sha256(p.lock_screen_password, sizeof(p.lock_screen_password),
|
||||||
|
p.lock_screen_password_salt, sizeof(p.lock_screen_password_salt),
|
||||||
|
(const uint8_t*)pin, (int)strlen(pin));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool check(const NodePrefs& p, const char* pin) {
|
||||||
|
if (!pin) return false;
|
||||||
|
uint8_t digest[NodePrefs::LOCK_HASH_LEN];
|
||||||
|
mesh::Utils::sha256(digest, sizeof(digest),
|
||||||
|
p.lock_screen_password_salt, sizeof(p.lock_screen_password_salt),
|
||||||
|
(const uint8_t*)pin, (int)strlen(pin));
|
||||||
|
return memcmp(digest, p.lock_screen_password, sizeof(digest)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrong tries: TRIES misses in a row, then PAUSE_MS before the next one.
|
||||||
|
struct Attempts {
|
||||||
|
uint8_t fails = 0;
|
||||||
|
uint32_t block_until = 0; // millis(); 0 = not blocked
|
||||||
|
|
||||||
|
bool blocked() {
|
||||||
|
if (block_until && (int32_t)(block_until - millis()) > 0) return true;
|
||||||
|
block_until = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t secondsLeft() const {
|
||||||
|
int32_t left = (int32_t)(block_until - millis());
|
||||||
|
return block_until && left > 0 ? (left + 999) / 1000 : 0;
|
||||||
|
}
|
||||||
|
void ok() { fails = 0; block_until = 0; }
|
||||||
|
// A miss; true when it starts the pause.
|
||||||
|
bool miss() {
|
||||||
|
if (++fails < TRIES) return false;
|
||||||
|
fails = 0;
|
||||||
|
block_until = (millis() + PAUSE_MS) | 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
uint8_t left() const { return TRIES - fails; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace screenlock
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
// - lock screen (Settings > Display & power > Lock screen, NodePrefs::auto_lock):
|
// - lock screen (Settings > Display & power > Lock screen, NodePrefs::auto_lock):
|
||||||
// once the screen turns off, waking shows a clock card and nothing reacts
|
// once the screen turns off, waking shows a clock card and nothing reacts
|
||||||
// until "slide to unlock" -- against touches in a pocket; with a screen PIN
|
// until "slide to unlock" -- against touches in a pocket; with a screen PIN
|
||||||
// (NVS, lvport::loadPin) the card always comes up, also after a reboot, and
|
// (NodePrefs, ui-core/ScreenLock.h) the card always comes up, also after a reboot, and
|
||||||
// asks for the PIN on a keypad instead of the slider;
|
// asks for the PIN on a keypad instead of the slider;
|
||||||
// - favourites dial (Home > Favourites, NodePrefs::favourite_contacts): six
|
// - favourites dial (Home > Favourites, NodePrefs::favourite_contacts): six
|
||||||
// slots holding a contact, room or channel; tap opens it, hold changes /
|
// slots holding a contact, room or channel; tap opens it, hold changes /
|
||||||
@@ -24,8 +24,6 @@ static lv_obj_t* s_lock_slider = nullptr;
|
|||||||
static lv_obj_t* s_pin_dots = nullptr; // lock card / setup popup: one dot per digit typed
|
static lv_obj_t* s_pin_dots = nullptr; // lock card / setup popup: one dot per digit typed
|
||||||
static lv_obj_t* s_pin_msg = nullptr; // "Wrong PIN" / "Try again in 30 s" / setup step
|
static lv_obj_t* s_pin_msg = nullptr; // "Wrong PIN" / "Try again in 30 s" / setup step
|
||||||
static const uint8_t PIN_MIN = 4, PIN_MAX = 8;
|
static const uint8_t PIN_MIN = 4, PIN_MAX = 8;
|
||||||
static const uint8_t PIN_TRIES = 5; // misses before a pause
|
|
||||||
static const uint32_t PIN_PAUSE_MS = 30000;
|
|
||||||
static const char* const PIN_MAP[] = { "1", "2", "3", "\n", "4", "5", "6", "\n", "7", "8", "9", "\n",
|
static const char* const PIN_MAP[] = { "1", "2", "3", "\n", "4", "5", "6", "\n", "7", "8", "9", "\n",
|
||||||
LV_SYMBOL_BACKSPACE, "0", LV_SYMBOL_OK, "" };
|
LV_SYMBOL_BACKSPACE, "0", LV_SYMBOL_OK, "" };
|
||||||
|
|
||||||
@@ -174,7 +172,7 @@ void UITask::lockScreen() {
|
|||||||
lv_obj_set_flex_align(s_lock, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
lv_obj_set_flex_align(s_lock, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
lv_obj_set_style_pad_top(s_lock, 18, 0);
|
lv_obj_set_style_pad_top(s_lock, 18, 0);
|
||||||
lv_obj_set_style_pad_row(s_lock, 4, 0);
|
lv_obj_set_style_pad_row(s_lock, 4, 0);
|
||||||
if (_pin[0]) { // compact card: the time and unread on one line, the keypad below
|
if (pinSet()) { // compact card: the time and unread on one line, the keypad below
|
||||||
lv_obj_set_style_pad_top(s_lock, 4, 0);
|
lv_obj_set_style_pad_top(s_lock, 4, 0);
|
||||||
lv_obj_set_style_pad_row(s_lock, 2, 0);
|
lv_obj_set_style_pad_row(s_lock, 2, 0);
|
||||||
lv_obj_t* top = lv_obj_create(s_lock);
|
lv_obj_t* top = lv_obj_create(s_lock);
|
||||||
@@ -297,9 +295,9 @@ void UITask::refreshLock() {
|
|||||||
if (unread > 0) lv_label_set_text_fmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d", unread);
|
if (unread > 0) lv_label_set_text_fmt(s_lock_unread, LV_SYMBOL_ENVELOPE " %d", unread);
|
||||||
else lv_label_set_text(s_lock_unread, "");
|
else lv_label_set_text(s_lock_unread, "");
|
||||||
if (!s_pin_msg) return;
|
if (!s_pin_msg) return;
|
||||||
int32_t left = (int32_t)(_pin_block_until - millis());
|
bool was = _pin_tries.block_until != 0;
|
||||||
if (_pin_block_until && left > 0) lv_label_set_text_fmt(s_pin_msg, "Too many tries - wait %ld s", (long)((left + 999) / 1000));
|
if (_pin_tries.blocked()) lv_label_set_text_fmt(s_pin_msg, "Too many tries - wait %lu s", (unsigned long)_pin_tries.secondsLeft());
|
||||||
else if (_pin_block_until) { _pin_block_until = 0; lv_label_set_text(s_pin_msg, "Enter PIN"); }
|
else if (was) lv_label_set_text(s_pin_msg, "Enter PIN");
|
||||||
else if (!lv_label_get_text(s_pin_msg)[0]) lv_label_set_text(s_pin_msg, "Enter PIN");
|
else if (!lv_label_get_text(s_pin_msg)[0]) lv_label_set_text(s_pin_msg, "Enter PIN");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -309,28 +307,28 @@ void UITask::refreshLock() {
|
|||||||
|
|
||||||
// ── Screen PIN ────────────────────────────────────────────────────────────────
|
// ── Screen PIN ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Lock card keypad: the PIN unlocks as soon as it's complete (OK checks a
|
// Lock card keypad: the PIN unlocks as soon as it matches (from 4 digits on;
|
||||||
// shorter entry); five misses pause entry for 30 s.
|
// only its hash is kept, so not its length); OK or a full 8 digits that
|
||||||
|
// don't match count a miss, and five misses pause entry for 30 s.
|
||||||
void UITask::pinKey(const char* key) {
|
void UITask::pinKey(const char* key) {
|
||||||
using namespace devview;
|
using namespace devview;
|
||||||
if (!s_lock || !_pin[0]) return;
|
if (!s_lock || !pinSet()) return;
|
||||||
if (_pin_block_until && (int32_t)(_pin_block_until - millis()) > 0) return;
|
if (_pin_tries.blocked()) return;
|
||||||
bool ok = pinEdit(_pin_entry, key);
|
bool ok = pinEdit(_pin_entry, key);
|
||||||
size_t n = strlen(_pin_entry);
|
size_t n = strlen(_pin_entry);
|
||||||
if (!ok && n < strlen(_pin)) {
|
if (n >= PIN_MIN && screenlock::check(*_prefs, _pin_entry)) { _pin_tries.ok(); unlockScreen(); return; }
|
||||||
|
if (!ok && n < PIN_MAX) {
|
||||||
if (n && s_pin_msg) lv_label_set_text(s_pin_msg, "Enter PIN");
|
if (n && s_pin_msg) lv_label_set_text(s_pin_msg, "Enter PIN");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!n) return;
|
if (!n) return;
|
||||||
if (!strcmp(_pin_entry, _pin)) { _pin_fails = 0; unlockScreen(); return; }
|
|
||||||
_pin_entry[0] = '\0';
|
_pin_entry[0] = '\0';
|
||||||
showDots(_pin_entry);
|
showDots(_pin_entry);
|
||||||
if (++_pin_fails >= PIN_TRIES) {
|
if (_pin_tries.miss()) {
|
||||||
_pin_fails = 0;
|
|
||||||
_pin_block_until = (millis() + PIN_PAUSE_MS) | 1;
|
|
||||||
refreshLock();
|
refreshLock();
|
||||||
} else if (s_pin_msg) {
|
} else if (s_pin_msg) {
|
||||||
lv_label_set_text_fmt(s_pin_msg, "Wrong PIN - %d tr%s left", PIN_TRIES - _pin_fails, PIN_TRIES - _pin_fails == 1 ? "y" : "ies");
|
int left = _pin_tries.left();
|
||||||
|
lv_label_set_text_fmt(s_pin_msg, "Wrong PIN - %d tr%s left", left, left == 1 ? "y" : "ies");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,10 +337,10 @@ void UITask::pinKey(const char* key) {
|
|||||||
void UITask::pinSetupPopup() {
|
void UITask::pinSetupPopup() {
|
||||||
using namespace devview;
|
using namespace devview;
|
||||||
_pin_entry[0] = _pin_new[0] = '\0';
|
_pin_entry[0] = _pin_new[0] = '\0';
|
||||||
lv_obj_t* panel = navPopupPanel(_pin[0] ? "Change PIN" : "Set screen PIN", true);
|
lv_obj_t* panel = navPopupPanel(pinSet() ? "Change PIN" : "Set screen PIN", true);
|
||||||
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
lv_obj_set_style_pad_row(panel, 2, 0);
|
lv_obj_set_style_pad_row(panel, 2, 0);
|
||||||
if (_pin[0]) { // next to the close button
|
if (pinSet()) { // next to the close button
|
||||||
lv_obj_t* l;
|
lv_obj_t* l;
|
||||||
headerButton(lv_obj_get_child(panel, 0), LV_SYMBOL_TRASH " Remove", onPinRemove, 44, &l);
|
headerButton(lv_obj_get_child(panel, 0), LV_SYMBOL_TRASH " Remove", onPinRemove, 44, &l);
|
||||||
lv_obj_set_style_text_color(l, lv_color_hex(theme::FAIL), 0);
|
lv_obj_set_style_text_color(l, lv_color_hex(theme::FAIL), 0);
|
||||||
@@ -369,8 +367,10 @@ void UITask::pinSetupKey(const char* key) {
|
|||||||
lv_label_set_text(s_pin_msg, "Didn't match - new PIN again");
|
lv_label_set_text(s_pin_msg, "Didn't match - new PIN again");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
strcpy(_pin, _pin_new);
|
if (_prefs) {
|
||||||
lvport::savePin(_pin);
|
screenlock::set(*_prefs, _pin_new, the_mesh.getRNG());
|
||||||
|
the_mesh.savePrefs();
|
||||||
|
}
|
||||||
_pin_new[0] = _pin_entry[0] = '\0';
|
_pin_new[0] = _pin_entry[0] = '\0';
|
||||||
navClosePopup();
|
navClosePopup();
|
||||||
showToast("PIN set - asked each time the screen wakes", 3000);
|
showToast("PIN set - asked each time the screen wakes", 3000);
|
||||||
@@ -378,8 +378,10 @@ void UITask::pinSetupKey(const char* key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UITask::pinRemove() {
|
void UITask::pinRemove() {
|
||||||
_pin[0] = '\0';
|
if (_prefs) {
|
||||||
lvport::savePin("");
|
screenlock::clear(*_prefs);
|
||||||
|
the_mesh.savePrefs();
|
||||||
|
}
|
||||||
navClosePopup();
|
navClosePopup();
|
||||||
showToast("PIN removed");
|
showToast("PIN removed");
|
||||||
pinRowRefresh();
|
pinRowRefresh();
|
||||||
|
|||||||
@@ -680,6 +680,7 @@ static int getU8(const char* ns, const char* key, int def) { return getInt(ns,
|
|||||||
static void putU8(const char* ns, const char* key, int v) { putInt(ns, key, v); }
|
static void putU8(const char* ns, const char* key, int v) { putInt(ns, key, v); }
|
||||||
static void getStr(const char* ns, const char* key, char* out, size_t n) { Kv* k = find(ns, key, false); snprintf(out, n, "%s", k ? k->val : ""); }
|
static void getStr(const char* ns, const char* key, char* out, size_t n) { Kv* k = find(ns, key, false); snprintf(out, n, "%s", k ? k->val : ""); }
|
||||||
static void putStr(const char* ns, const char* key, const char* v) { if (Kv* k = find(ns, key, true)) snprintf(k->val, sizeof(k->val), "%s", v); }
|
static void putStr(const char* ns, const char* key, const char* v) { if (Kv* k = find(ns, key, true)) snprintf(k->val, sizeof(k->val), "%s", v); }
|
||||||
|
static void remove(const char* ns, const char* key) { if (Kv* k = find(ns, key, false)) k->val[0] = '\0'; }
|
||||||
} // namespace nvs
|
} // namespace nvs
|
||||||
|
|
||||||
static bool s_swallow = false;
|
static bool s_swallow = false;
|
||||||
@@ -848,8 +849,12 @@ static bool vectorOn() { return nvs::getBool("mc_ui", "vector", false); }
|
|||||||
static void setVectorOn(bool on) { nvs::putBool("mc_ui", "vector", on); }
|
static void setVectorOn(bool on) { nvs::putBool("mc_ui", "vector", on); }
|
||||||
|
|
||||||
// Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none.
|
// Screen-lock PIN (Settings > Display & power > Screen PIN): digits, "" = none.
|
||||||
static void loadPin(char* out, size_t n) { nvs::getStr("mc_lock", "pin", out, n); }
|
// The screen PIN as older builds kept it (plain text): read once and removed;
|
||||||
static void savePin(const char* pin) { nvs::putStr("mc_lock", "pin", pin); }
|
// it lives hashed in NodePrefs now (ui-core/ScreenLock.h).
|
||||||
|
static void takeOldPin(char* out, size_t n) {
|
||||||
|
nvs::getStr("mc_lock", "pin", out, n);
|
||||||
|
if (out[0]) nvs::remove("mc_lock", "pin");
|
||||||
|
}
|
||||||
|
|
||||||
// Accent colour (Settings > Display & power): an index into theme::ACCENTS.
|
// Accent colour (Settings > Display & power): an index into theme::ACCENTS.
|
||||||
static int loadAccent() { return nvs::getU8("mc_ui", "accent", 0); }
|
static int loadAccent() { return nvs::getU8("mc_ui", "accent", 0); }
|
||||||
|
|||||||
@@ -898,8 +898,15 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs
|
|||||||
buildStatusBar();
|
buildStatusBar();
|
||||||
applyDisplayPrefs(); // a slider percentage overrides the level main.cpp set
|
applyDisplayPrefs(); // a slider percentage overrides the level main.cpp set
|
||||||
showHome();
|
showHome();
|
||||||
lvport::loadPin(_pin, sizeof(_pin));
|
// Before the PIN moved into NodePrefs it was kept in NVS in plain text.
|
||||||
if (_pin[0]) lockScreen(); // a reboot doesn't get round the PIN
|
char old_pin[9];
|
||||||
|
lvport::takeOldPin(old_pin, sizeof(old_pin));
|
||||||
|
if (old_pin[0] && _prefs && !pinSet()) {
|
||||||
|
screenlock::set(*_prefs, old_pin, the_mesh.getRNG());
|
||||||
|
the_mesh.savePrefs();
|
||||||
|
}
|
||||||
|
memset(old_pin, 0, sizeof(old_pin));
|
||||||
|
if (pinSet()) lockScreen(); // a reboot doesn't get round the PIN
|
||||||
showSplash(); // over both; fades out by itself
|
showSplash(); // over both; fades out by itself
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1175,7 +1182,7 @@ void UITask::sleep() {
|
|||||||
if (_display) _display->turnOff();
|
if (_display) _display->turnOff();
|
||||||
prefsSaveSoon(0); // nothing to stall now
|
prefsSaveSoon(0); // nothing to stall now
|
||||||
lvport::powerSave(true, _tap_wake);
|
lvport::powerSave(true, _tap_wake);
|
||||||
if ((_prefs && _prefs->auto_lock) || _pin[0]) lockScreen(); // Lock screen, or a screen PIN
|
if ((_prefs && _prefs->auto_lock) || pinSet()) lockScreen(); // Lock screen, or a screen PIN
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::wake() {
|
void UITask::wake() {
|
||||||
@@ -3212,7 +3219,7 @@ void UITask::buildSchemaSettings() {
|
|||||||
lv_obj_add_event_cb(tw, onTapWake, LV_EVENT_VALUE_CHANGED, NULL);
|
lv_obj_add_event_cb(tw, onTapWake, LV_EVENT_VALUE_CHANGED, NULL);
|
||||||
g = group(body, "LOCK");
|
g = group(body, "LOCK");
|
||||||
schemaRow(g, SETTING(auto_lock));
|
schemaRow(g, SETTING(auto_lock));
|
||||||
listRow(g, "Screen PIN", _pin[0] ? "On - asked when the screen wakes" : "Off", onPinSetup, NULL);
|
listRow(g, "Screen PIN", pinSet() ? "On - asked when the screen wakes" : "Off", onPinSetup, NULL);
|
||||||
accentRow(group(body, "LOOK")); // DeviceScreen.h
|
accentRow(group(body, "LOOK")); // DeviceScreen.h
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "../AbstractUITask.h"
|
#include "../AbstractUITask.h"
|
||||||
#include "../NodePrefs.h"
|
#include "../NodePrefs.h"
|
||||||
#include "../ui-core/UiCoreHost.h"
|
#include "../ui-core/UiCoreHost.h"
|
||||||
|
#include "../ui-core/ScreenLock.h"
|
||||||
|
|
||||||
class UiCore;
|
class UiCore;
|
||||||
class NearbyModel;
|
class NearbyModel;
|
||||||
@@ -458,13 +459,12 @@ private:
|
|||||||
uint16_t _batt_mv = 0; // smoothed, read every 8 s: the status bar and the low-battery shutdown
|
uint16_t _batt_mv = 0; // smoothed, read every 8 s: the status bar and the low-battery shutdown
|
||||||
uint32_t _next_batt_ms = 0;
|
uint32_t _next_batt_ms = 0;
|
||||||
lv_obj_t* _prune_lbl = nullptr;
|
lv_obj_t* _prune_lbl = nullptr;
|
||||||
// Screen PIN (DeviceScreen.h); stored in NVS (lvport::loadPin)
|
// Screen PIN (DeviceScreen.h): a salted hash in NodePrefs (ui-core/ScreenLock.h)
|
||||||
char _pin[9] = ""; // "" = no PIN
|
bool pinSet() const { return _prefs && screenlock::isSet(*_prefs); }
|
||||||
bool _tap_wake = true; // a touch wakes the dark screen (NVS)
|
bool _tap_wake = true; // a touch wakes the dark screen (NVS)
|
||||||
char _pin_entry[9] = ""; // digits typed so far
|
char _pin_entry[9] = ""; // digits typed so far
|
||||||
char _pin_new[9] = ""; // setup: the first entry, waiting for its confirmation
|
char _pin_new[9] = ""; // setup: the first entry, waiting for its confirmation
|
||||||
uint8_t _pin_fails = 0;
|
screenlock::Attempts _pin_tries;
|
||||||
uint32_t _pin_block_until = 0; // millis() until which entry is refused after 5 misses
|
|
||||||
|
|
||||||
// Open conversation (SCR_THREAD)
|
// Open conversation (SCR_THREAD)
|
||||||
bool _thread_is_channel = false;
|
bool _thread_is_channel = false;
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ static const char* const KB_T9_GROUPS[KB_PAGES][9] = {
|
|||||||
{ "@#&", "*()", "-_+", "=/\\", ":;'\"", "<>[]", "{}|~", "^$%`", ",." }, // page 1 — symbols
|
{ "@#&", "*()", "-_+", "=/\\", ":;'\"", "<>[]", "{}|~", "^$%`", ",." }, // 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/
|
// Non-Latin keyboard scripts. NodePrefs::keyboard_main_alphabet/
|
||||||
// keyboard_alt_alphabet (Settings > Keyboard's Main/Additional rows) pick
|
// keyboard_alt_alphabet (Settings > Keyboard's Main/Additional rows) pick
|
||||||
// which script occupies page 0 (the keyboard's default/opening page) and
|
// which script occupies page 0 (the keyboard's default/opening page) and
|
||||||
@@ -250,6 +261,12 @@ struct KeyboardWidget {
|
|||||||
bool accent_active = false; // true while the Hold-Enter accent popup is open
|
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_group = -1; // index into KB_ACCENT_VARIANTS for the held cell's base letter
|
||||||
int accent_sel = 0; // selected variant within that group
|
int accent_sel = 0; // selected variant within that group
|
||||||
|
bool pin_kb_mask_enabled = false; // Whether pin keyboard should mask input
|
||||||
|
// The number pad (beginPin) instead of the user's ABC / T9 layout; its ABC
|
||||||
|
// key drops back to that layout for a password with letters.
|
||||||
|
bool pin_mode = false;
|
||||||
|
const char* pin_prompt = nullptr; // e.g. "New PIN", shown in the preview; kept past the ABC switch
|
||||||
|
static const int PIN_MAX_LEN = 16;
|
||||||
int row, col;
|
int row, col;
|
||||||
int page; // see totalPages()/scriptAt()/pageIsSymbols() below
|
int page; // see totalPages()/scriptAt()/pageIsSymbols() below
|
||||||
bool caps;
|
bool caps;
|
||||||
@@ -289,6 +306,7 @@ struct KeyboardWidget {
|
|||||||
// to ABC and mainScript()/altScript() default to Latin-only.
|
// to ABC and mainScript()/altScript() default to Latin-only.
|
||||||
NodePrefs* prefs = nullptr;
|
NodePrefs* prefs = nullptr;
|
||||||
bool isT9() const { return prefs && prefs->keyboard_type == 1; }
|
bool isT9() const { return prefs && prefs->keyboard_type == 1; }
|
||||||
|
bool isPin() const { return pin_mode; }
|
||||||
// Which script occupies page 0 (the keyboard's default/opening page) and
|
// Which script occupies page 0 (the keyboard's default/opening page) and
|
||||||
// which occupies page 1 (reached by the #@/abc cycle key) -- Settings >
|
// which occupies page 1 (reached by the #@/abc cycle key) -- Settings >
|
||||||
// Keyboard's Main/Additional rows. Additional equal to Main collapses to no
|
// Keyboard's Main/Additional rows. Additional equal to Main collapses to no
|
||||||
@@ -310,8 +328,8 @@ struct KeyboardWidget {
|
|||||||
// lowercase regardless of Shift.
|
// lowercase regardless of Shift.
|
||||||
bool t9_caps = false;
|
bool t9_caps = false;
|
||||||
|
|
||||||
int gridRows() const { return isT9() ? KB_T9_ROWS : KB_ROWS_CHAR; }
|
int gridRows() const { return isPin() ? KB_PIN_ROWS : (isT9() ? KB_T9_ROWS : KB_ROWS_CHAR); }
|
||||||
int gridCols() const { return isT9() ? KB_T9_COLS : KB_COLS_CHAR; }
|
int gridCols() const { return isPin() ? KB_PIN_COLS : (isT9() ? KB_T9_COLS : KB_COLS_CHAR); }
|
||||||
|
|
||||||
// ── Page model ────────────────────────────────────────────────────────────
|
// ── Page model ────────────────────────────────────────────────────────────
|
||||||
// Logical page order: 0 = mainScript(), [1 = altScript(), if it differs],
|
// Logical page order: 0 = mainScript(), [1 = altScript(), if it differs],
|
||||||
@@ -390,6 +408,9 @@ struct KeyboardWidget {
|
|||||||
page = 0;
|
page = 0;
|
||||||
caps = false;
|
caps = false;
|
||||||
caps_lock = false;
|
caps_lock = false;
|
||||||
|
pin_kb_mask_enabled = false;
|
||||||
|
pin_mode = false;
|
||||||
|
pin_prompt = nullptr;
|
||||||
t9_cell = -1;
|
t9_cell = -1;
|
||||||
t9_cycle = 0;
|
t9_cycle = 0;
|
||||||
_ph_menu.active = false;
|
_ph_menu.active = false;
|
||||||
@@ -402,6 +423,14 @@ struct KeyboardWidget {
|
|||||||
addPlaceholder("{time}");
|
addPlaceholder("{time}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Open keyboard in PIN mode
|
||||||
|
void beginPin(const char* initial = "", int max = PIN_MAX_LEN, bool mask = false, const char* prompt = nullptr) {
|
||||||
|
begin(initial, max);
|
||||||
|
pin_mode = true;
|
||||||
|
pin_prompt = prompt;
|
||||||
|
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
|
// 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
|
// 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
|
// commit does. Shared by the plain-Latin-cell commit below and the accent
|
||||||
@@ -568,12 +597,29 @@ struct KeyboardWidget {
|
|||||||
} else {
|
} else {
|
||||||
linebuf[0] = '\0';
|
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];
|
char linebuf_t[KB_PREVIEW_BYTES + 2];
|
||||||
display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t));
|
display.translateUTF8ToBlocks(linebuf_t, linebuf, sizeof(linebuf_t));
|
||||||
display.setCursor(0, pl * lh);
|
display.setCursor(0, pl * lh);
|
||||||
display.print(linebuf_t);
|
display.print(linebuf_t);
|
||||||
ps = pe;
|
ps = pe;
|
||||||
}
|
}
|
||||||
|
if (pin_prompt) { // what the PIN is for, bottom-right of the preview (alerts don't show over the keyboard)
|
||||||
|
display.setCursor(display.width() - display.getTextWidth(pin_prompt), (prev_lines - 1) * lh);
|
||||||
|
display.print(pin_prompt);
|
||||||
|
}
|
||||||
display.fillRect(0, sep_y, display.width(), display.sepH());
|
display.fillRect(0, sep_y, display.width(), display.sepH());
|
||||||
|
|
||||||
// Cursor-positioning mode: LEFT/RIGHT/UP/DOWN now drive the text cursor
|
// Cursor-positioning mode: LEFT/RIGHT/UP/DOWN now drive the text cursor
|
||||||
@@ -592,6 +638,41 @@ struct KeyboardWidget {
|
|||||||
return 50;
|
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
|
// Compact mode (Settings > Keyboard's "Ext. KB" row): an external-keyboard
|
||||||
// typist never looks at the letter grid or special-row icons, so skip
|
// typist never looks at the letter grid or special-row icons, so skip
|
||||||
// drawing them entirely -- no status line either, since nothing it could
|
// drawing them entirely -- no status line either, since nothing it could
|
||||||
@@ -755,7 +836,7 @@ struct KeyboardWidget {
|
|||||||
// just fine -- so this only checks that no other exclusive input mode
|
// just fine -- so this only checks that no other exclusive input mode
|
||||||
// (popup/cursor-move) is already in progress, same as inPlainGridState().
|
// (popup/cursor-move) is already in progress, same as inPlainGridState().
|
||||||
bool openAccentFor(char base) {
|
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);
|
int gi = findAccentGroup(base);
|
||||||
if (gi < 0) return false;
|
if (gi < 0) return false;
|
||||||
accent_active = true;
|
accent_active = true;
|
||||||
@@ -872,6 +953,70 @@ struct KeyboardWidget {
|
|||||||
return NONE;
|
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) {
|
||||||
|
pin_mode = false; // ABC switch: the user's own layout
|
||||||
|
row = col = 0;
|
||||||
|
page = 0;
|
||||||
|
t9_cell = -1;
|
||||||
|
} else {
|
||||||
|
return DONE; // Submit
|
||||||
|
}
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
|
||||||
const int rows = gridRows();
|
const int rows = gridRows();
|
||||||
const int cols = gridCols();
|
const int cols = gridCols();
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ class SettingsScreen : public UIScreen {
|
|||||||
SCHEMA_MESSAGES,
|
SCHEMA_MESSAGES,
|
||||||
MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4,
|
MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4,
|
||||||
MSG_SLOT_5, MSG_SLOT_6, MSG_SLOT_7, MSG_SLOT_8, MSG_SLOT_9,
|
MSG_SLOT_5, MSG_SLOT_6, MSG_SLOT_7, MSG_SLOT_8, MSG_SLOT_9,
|
||||||
Count
|
Count,
|
||||||
|
// Not walked from the enum: placed right after the schema's "Lock screen" row.
|
||||||
|
LOCK_PIN
|
||||||
};
|
};
|
||||||
// A schema setting's row: SCHEMA_ITEM + its index in settings::ALL.
|
// A schema setting's row: SCHEMA_ITEM + its index in settings::ALL.
|
||||||
static const int SCHEMA_ITEM = 128;
|
static const int SCHEMA_ITEM = 128;
|
||||||
@@ -161,8 +163,11 @@ class SettingsScreen : public UIScreen {
|
|||||||
_sec_count[cur] = 0;
|
_sec_count[cur] = 0;
|
||||||
} else if (cur >= 0 && isSchemaGroup(i)) {
|
} else if (cur >= 0 && isSchemaGroup(i)) {
|
||||||
for (int k = 0; k < settings::COUNT && _sec_count[cur] < MAX_PER_SEC; k++)
|
for (int k = 0; k < settings::COUNT && _sec_count[cur] < MAX_PER_SEC; k++)
|
||||||
if (schemaIn(i, settings::ALL[k].section) && schemaShown(settings::ALL[k]))
|
if (schemaIn(i, settings::ALL[k].section) && schemaShown(settings::ALL[k])) {
|
||||||
_sec_items[cur][_sec_count[cur]++] = (uint8_t)(SCHEMA_ITEM + k);
|
_sec_items[cur][_sec_count[cur]++] = (uint8_t)(SCHEMA_ITEM + k);
|
||||||
|
if (settings::ALL[k].offset == offsetof(NodePrefs, auto_lock) && _sec_count[cur] < MAX_PER_SEC)
|
||||||
|
_sec_items[cur][_sec_count[cur]++] = (uint8_t)LOCK_PIN;
|
||||||
|
}
|
||||||
} else if (cur >= 0 && _sec_count[cur] < MAX_PER_SEC) {
|
} else if (cur >= 0 && _sec_count[cur] < MAX_PER_SEC) {
|
||||||
_sec_items[cur][_sec_count[cur]++] = (uint8_t)i;
|
_sec_items[cur][_sec_count[cur]++] = (uint8_t)i;
|
||||||
}
|
}
|
||||||
@@ -213,6 +218,11 @@ class SettingsScreen : public UIScreen {
|
|||||||
// A schema setting: its short label, the value in the value column (a
|
// A schema setting: its short label, the value in the value column (a
|
||||||
// switch as ON / OFF, brightness and volume as bars). A value too long 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.
|
// the column starts further left, clear of the label, else scrolls.
|
||||||
|
void beginPinEntry(const char* prompt) {
|
||||||
|
_kb->beginPin("", KeyboardWidget::PIN_MAX_LEN, false, prompt);
|
||||||
|
_kb->clearPlaceholders(); // a PIN is literal, not a template message
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
display.print(label);
|
display.print(label);
|
||||||
@@ -545,6 +555,10 @@ class SettingsScreen : public UIScreen {
|
|||||||
int r = display.drawTextEllipsized(vx, y, display.width() - vx - _reserve,
|
int r = display.drawTextEllipsized(vx, y, display.width() - vx - _reserve,
|
||||||
sl.name(sl.default_idx), sel);
|
sl.name(sl.default_idx), sel);
|
||||||
if (sel && r > 0) mq_delay = r;
|
if (sel && r > 0) mq_delay = r;
|
||||||
|
} else if (item == LOCK_PIN) {
|
||||||
|
display.print("Lock PIN");
|
||||||
|
display.setCursor(valCol(display), y);
|
||||||
|
display.print(_task->passwordLockEnabled() ? "ON" : "OFF");
|
||||||
} else if (item == DEVICE_NAME) {
|
} else if (item == DEVICE_NAME) {
|
||||||
display.print("Name");
|
display.print("Name");
|
||||||
int vx = valCol(display);
|
int vx = valCol(display);
|
||||||
@@ -620,6 +634,8 @@ class SettingsScreen : public UIScreen {
|
|||||||
// Keyboard state for editing message slots
|
// Keyboard state for editing message slots
|
||||||
int _edit_slot = -1; // -1 = not editing, 0..9 = slot being edited
|
int _edit_slot = -1; // -1 = not editing, 0..9 = slot being edited
|
||||||
bool _edit_name = false; // editing DEVICE_NAME via the keyboard
|
bool _edit_name = false; // editing DEVICE_NAME via the keyboard
|
||||||
|
bool _edit_lock_pass = false; // setting the screen PIN via the keyboard
|
||||||
|
char _lock_pass_first[KeyboardWidget::PIN_MAX_LEN + 1] = ""; // the first entry, awaiting its repeat
|
||||||
KeyboardWidget* _kb;
|
KeyboardWidget* _kb;
|
||||||
|
|
||||||
// Scope list management (SCOPE_NAME row -> a full-screen add/rename/
|
// Scope list management (SCOPE_NAME row -> a full-screen add/rename/
|
||||||
@@ -691,6 +707,8 @@ public:
|
|||||||
_scope_action_menu.active = false;
|
_scope_action_menu.active = false;
|
||||||
_scope_delete_confirm_active = false;
|
_scope_delete_confirm_active = false;
|
||||||
_prune_confirm.active = false;
|
_prune_confirm.active = false;
|
||||||
|
_edit_lock_pass = false;
|
||||||
|
_lock_pass_first[0] = '\0';
|
||||||
resetList();
|
resetList();
|
||||||
_editor.freq.active = false;
|
_editor.freq.active = false;
|
||||||
}
|
}
|
||||||
@@ -698,7 +716,7 @@ public:
|
|||||||
int render(DisplayDriver& display) override {
|
int render(DisplayDriver& display) override {
|
||||||
display.setTextSize(1);
|
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);
|
return _kb->render(display);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -766,6 +784,36 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keyboard editing mode for the lock-screen password
|
||||||
|
if (_edit_lock_pass) {
|
||||||
|
auto res = _kb->handleInput(c);
|
||||||
|
if (res == KeyboardWidget::DONE) {
|
||||||
|
// Entered twice: a typo here would lock the owner out.
|
||||||
|
if (strlen(_kb->buf) < 4) {
|
||||||
|
_kb->pin_prompt = "Min 4";
|
||||||
|
} else if (!_lock_pass_first[0]) {
|
||||||
|
strncpy(_lock_pass_first, _kb->buf, sizeof(_lock_pass_first) - 1);
|
||||||
|
_lock_pass_first[sizeof(_lock_pass_first) - 1] = '\0';
|
||||||
|
beginPinEntry("Again");
|
||||||
|
} else if (strcmp(_lock_pass_first, _kb->buf) != 0) {
|
||||||
|
_lock_pass_first[0] = '\0';
|
||||||
|
beginPinEntry("No match");
|
||||||
|
} else {
|
||||||
|
if (p) {
|
||||||
|
_task->setNodeLockPassword(_kb->buf);
|
||||||
|
the_mesh.savePrefs(); // now, not on leaving: a PIN must survive a power-off right after
|
||||||
|
}
|
||||||
|
_edit_lock_pass = false;
|
||||||
|
_lock_pass_first[0] = '\0';
|
||||||
|
_task->showAlert("PIN set", 900);
|
||||||
|
}
|
||||||
|
} else if (res == KeyboardWidget::CANCELLED) {
|
||||||
|
_edit_lock_pass = false;
|
||||||
|
_lock_pass_first[0] = '\0';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Keyboard editing mode for adding/renaming a scope-list entry
|
// Keyboard editing mode for adding/renaming a scope-list entry
|
||||||
if (_scope_rename_idx != -2) {
|
if (_scope_rename_idx != -2) {
|
||||||
auto res = _kb->handleInput(c);
|
auto res = _kb->handleInput(c);
|
||||||
@@ -974,6 +1022,19 @@ public:
|
|||||||
_dirty = true;
|
_dirty = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Lock PIN: Enter sets one, or clears the one set
|
||||||
|
if (_selected == LOCK_PIN && p && enter) {
|
||||||
|
if (_task->passwordLockEnabled()) {
|
||||||
|
_task->setNodeLockPassword("");
|
||||||
|
the_mesh.savePrefs();
|
||||||
|
_task->showAlert("PIN cleared", 900);
|
||||||
|
} else {
|
||||||
|
_edit_lock_pass = true;
|
||||||
|
_lock_pass_first[0] = '\0';
|
||||||
|
beginPinEntry("New PIN");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (_selected == DEVICE_NAME && p && enter) {
|
if (_selected == DEVICE_NAME && p && enter) {
|
||||||
_edit_name = true;
|
_edit_name = true;
|
||||||
_kb->begin(the_mesh.getNodeName(), (int)sizeof(p->node_name) - 1);
|
_kb->begin(the_mesh.getNodeName(), (int)sizeof(p->node_name) - 1);
|
||||||
|
|||||||
@@ -1464,6 +1464,13 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
|||||||
}
|
}
|
||||||
#endif
|
#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)
|
#if defined(PIN_USER_BTN)
|
||||||
user_btn.begin();
|
user_btn.begin();
|
||||||
#endif
|
#endif
|
||||||
@@ -1537,6 +1544,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
|||||||
applyRotation();
|
applyRotation();
|
||||||
applyFullRefreshInterval();
|
applyFullRefreshInterval();
|
||||||
applyAllGpioModes(); // restore persisted pin modes to hardware before any UI/bot use
|
applyAllGpioModes(); // restore persisted pin modes to hardware before any UI/bot use
|
||||||
|
// Locked above (a PIN, or the cover closed) before Home existed to be told.
|
||||||
|
if (_locked) syncLockToHome();
|
||||||
setCurrScreen(splash);
|
setCurrScreen(splash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1924,6 +1933,89 @@ void UITask::syncLockToHome() {
|
|||||||
if (home) static_cast<HomeScreen*>(home)->setLocked(_locked);
|
if (home) static_cast<HomeScreen*>(home)->setLocked(_locked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool UITask::passwordLockEnabled() const {
|
||||||
|
return _node_prefs && screenlock::isSet(*_node_prefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::setNodeLockPassword(const char* plain) {
|
||||||
|
if (_node_prefs) screenlock::set(*_node_prefs, plain, the_mesh.getRNG());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::beginUnlockPrompt() {
|
||||||
|
_unlock_kb = true; // Track that keyboard is visible and is waiting for input
|
||||||
|
_kb.beginPin("", KeyboardWidget::PIN_MAX_LEN, true, "PIN"); // masked
|
||||||
|
_kb.clearPlaceholders();
|
||||||
|
_lock_wake_until = millis() + 5000; // keep the display on while typing
|
||||||
|
_next_refresh = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::cancelUnlockPrompt() {
|
||||||
|
_unlock_kb = false;
|
||||||
|
_kb.pin_mode = false;
|
||||||
|
_kb.buf[0] = '\0'; // clear input
|
||||||
|
_kb.len = 0;
|
||||||
|
_kb.cursor_pos = 0;
|
||||||
|
_next_refresh = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UITask::handleUnlockKey(char c) {
|
||||||
|
auto res = _kb.handleInput(c);
|
||||||
|
if (res == KeyboardWidget::DONE && _pin_tries.blocked()) {
|
||||||
|
char msg[32];
|
||||||
|
snprintf(msg, sizeof(msg), "Wait %lu s", (unsigned long)_pin_tries.secondsLeft());
|
||||||
|
showAlert(msg, 1200);
|
||||||
|
} else if (res == KeyboardWidget::DONE) { // Process input on submit
|
||||||
|
if (_node_prefs && screenlock::check(*_node_prefs, _kb.buf)) {
|
||||||
|
// Match: Unlock
|
||||||
|
_pin_tries.ok();
|
||||||
|
_unlock_kb = false;
|
||||||
|
_kb.pin_mode = 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; TRIES misses pause the entry
|
||||||
|
char msg[32];
|
||||||
|
if (_pin_tries.miss()) snprintf(msg, sizeof(msg), "Wait %lu s", (unsigned long)(screenlock::PAUSE_MS / 1000));
|
||||||
|
else snprintf(msg, sizeof(msg), "Wrong PIN, %u left", (unsigned)_pin_tries.left());
|
||||||
|
showAlert(msg, 1200);
|
||||||
|
_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) {
|
bool UITask::savePrefsIfDirty(bool& dirty) {
|
||||||
if (!dirty) return false;
|
if (!dirty) return false;
|
||||||
the_mesh.savePrefs();
|
the_mesh.savePrefs();
|
||||||
@@ -2209,15 +2301,7 @@ void UITask::pollCardKB() {
|
|||||||
// two-key combo, so it doesn't need the physical combo's extra 3x
|
// two-key combo, so it doesn't need the physical combo's extra 3x
|
||||||
// repetition to guard against accidental triggering.
|
// repetition to guard against accidental triggering.
|
||||||
if (_display && !_display->isOn()) _display->turnOn();
|
if (_display && !_display->isOn()) _display->turnOn();
|
||||||
_locked = !_locked;
|
toggleLock();
|
||||||
if (_locked) {
|
|
||||||
_lock_wake_until = millis() + 2000;
|
|
||||||
} else {
|
|
||||||
uint32_t aoff = autoOffMillis();
|
|
||||||
if (aoff > 0) _auto_off = millis() + aoff;
|
|
||||||
}
|
|
||||||
syncLockToHome();
|
|
||||||
_next_refresh = 0;
|
|
||||||
return;
|
return;
|
||||||
} else if (raw >= 0x80 && raw <= 0xAF) { // Fn+<letter> -- open its accent popup
|
} else if (raw >= 0x80 && raw <= 0xAF) { // Fn+<letter> -- open its accent popup
|
||||||
char base = CARDKB_FN_BASE[raw - 0x80];
|
char base = CARDKB_FN_BASE[raw - 0x80];
|
||||||
@@ -2271,6 +2355,7 @@ void UITask::pollHallSensor() {
|
|||||||
_hall_magnet_present = present;
|
_hall_magnet_present = present;
|
||||||
|
|
||||||
if (present) { // cover closed
|
if (present) { // cover closed
|
||||||
|
cancelUnlockPrompt(); // a cover can't be over the password prompt
|
||||||
_locked = true;
|
_locked = true;
|
||||||
syncLockToHome();
|
syncLockToHome();
|
||||||
_lock_wake_until = 0;
|
_lock_wake_until = 0;
|
||||||
@@ -2279,11 +2364,17 @@ void UITask::pollHallSensor() {
|
|||||||
digitalWrite(PIN_LED, LOW); // same as the auto-off path -- one less thing lit under a closed cover
|
digitalWrite(PIN_LED, LOW); // same as the auto-off path -- one less thing lit under a closed cover
|
||||||
#endif
|
#endif
|
||||||
} else { // cover opened
|
} else { // cover opened
|
||||||
_locked = false;
|
if (passwordLockEnabled()) { // Redirect flow to usual unlock prompt when password is enabled
|
||||||
syncLockToHome();
|
_locked = true;
|
||||||
if (_display && !_display->isOn()) _display->turnOn();
|
if (_display) _display->turnOn();
|
||||||
uint32_t aoff = autoOffMillis();
|
beginUnlockPrompt();
|
||||||
if (aoff > 0) _auto_off = millis() + aoff;
|
} else {
|
||||||
|
_locked = false;
|
||||||
|
syncLockToHome();
|
||||||
|
if (_display && !_display->isOn()) _display->turnOn();
|
||||||
|
uint32_t aoff = autoOffMillis();
|
||||||
|
if (aoff > 0) _auto_off = millis() + aoff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_next_refresh = 0;
|
_next_refresh = 0;
|
||||||
#endif
|
#endif
|
||||||
@@ -2296,7 +2387,7 @@ void UITask::loop() {
|
|||||||
uint8_t joy_rot = _node_prefs ? _node_prefs->joystick_rotation : JOYSTICK_ROTATION;
|
uint8_t joy_rot = _node_prefs ? _node_prefs->joystick_rotation : JOYSTICK_ROTATION;
|
||||||
int ev = user_btn.check();
|
int ev = user_btn.check();
|
||||||
if (ev == BUTTON_EVENT_CLICK) {
|
if (ev == BUTTON_EVENT_CLICK) {
|
||||||
if (back_btn.isPressed()) {
|
if (back_btn.isPressed() && !_unlock_kb) {
|
||||||
// Enter clicked while Back is held — lock/unlock sequence
|
// Enter clicked while Back is held — lock/unlock sequence
|
||||||
if (_display && !_display->isOn()) {
|
if (_display && !_display->isOn()) {
|
||||||
_display->turnOn(); // turn on display so hints are visible
|
_display->turnOn(); // turn on display so hints are visible
|
||||||
@@ -2309,18 +2400,15 @@ void UITask::loop() {
|
|||||||
if (_lock_seq_count >= 3) {
|
if (_lock_seq_count >= 3) {
|
||||||
_lock_seq_count = 0;
|
_lock_seq_count = 0;
|
||||||
_lock_seq_used = true; // suppress Back release click
|
_lock_seq_used = true; // suppress Back release click
|
||||||
_locked = !_locked;
|
toggleLock();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
// eat the Enter — don't pass to curr
|
// eat the Enter — don't pass to curr
|
||||||
} else {
|
} 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));
|
enqueueKey(checkDisplayOn(KEY_ENTER));
|
||||||
}
|
}
|
||||||
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) {
|
||||||
@@ -2510,7 +2598,12 @@ void UITask::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_kq_head != _kq_tail) {
|
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
|
// 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.
|
||||||
char k;
|
char k;
|
||||||
@@ -2539,16 +2632,34 @@ void UITask::loop() {
|
|||||||
|
|
||||||
|
|
||||||
if (_display != NULL && _display->isOn()) {
|
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();
|
_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);
|
||||||
|
if (millis() < _alert_expiry) renderAlertOverlay(); // "Wrong PIN", and a ringing alarm
|
||||||
|
_display->endFrame();
|
||||||
|
_next_refresh = millis() + delay_millis;
|
||||||
} else if (_locked && millis() >= _next_refresh && home) {
|
} else if (_locked && millis() >= _next_refresh && home) {
|
||||||
_display->startFrame();
|
_display->startFrame();
|
||||||
home->render(*_display);
|
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 {
|
||||||
|
home->render(*_display);
|
||||||
|
_next_refresh = millis() + Features::LOCKSCREEN_REFRESH_MS;
|
||||||
|
}
|
||||||
// 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 (millis() < _alert_expiry) renderAlertOverlay();
|
||||||
_display->endFrame();
|
_display->endFrame();
|
||||||
_next_refresh = millis() + Features::LOCKSCREEN_REFRESH_MS;
|
|
||||||
} else if (!_locked && millis() >= _next_refresh && curr) {
|
} else if (!_locked && millis() >= _next_refresh && curr) {
|
||||||
_display->startFrame();
|
_display->startFrame();
|
||||||
_kb.beginFrame();
|
_kb.beginFrame();
|
||||||
@@ -2588,6 +2699,7 @@ void UITask::loop() {
|
|||||||
digitalWrite(PIN_LED, LOW); // turn off status LED with display to save power
|
digitalWrite(PIN_LED, LOW); // turn off status LED with display to save power
|
||||||
#endif
|
#endif
|
||||||
if (_node_prefs && _node_prefs->auto_lock) {
|
if (_node_prefs && _node_prefs->auto_lock) {
|
||||||
|
cancelUnlockPrompt(); // idle-lock isn't a password prompt
|
||||||
_locked = true;
|
_locked = true;
|
||||||
_lock_wake_until = 0;
|
_lock_wake_until = 0;
|
||||||
syncLockToHome();
|
syncLockToHome();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "../AbstractUITask.h"
|
#include "../AbstractUITask.h"
|
||||||
#include "../NodePrefs.h"
|
#include "../NodePrefs.h"
|
||||||
#include "../Trail.h"
|
#include "../Trail.h"
|
||||||
|
#include "../ui-core/ScreenLock.h"
|
||||||
|
|
||||||
// Optional M5Stack CardKB (I2C keyboard, addr 0x5F). CARDKB_I2C names which
|
// Optional M5Stack CardKB (I2C keyboard, addr 0x5F). CARDKB_I2C names which
|
||||||
// TwoWire it lives on -- set at file scope (not inside the class body, and
|
// TwoWire it lives on -- set at file scope (not inside the class body, and
|
||||||
@@ -65,6 +66,9 @@ class UITask : public UITaskBase, public UiCoreHost {
|
|||||||
int _lock_seq_count; // Enter presses while Back held (lock/unlock sequence)
|
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)
|
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)
|
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;
|
||||||
|
screenlock::Attempts _pin_tries;
|
||||||
char _alert[80];
|
char _alert[80];
|
||||||
char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies
|
char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies
|
||||||
// Persistent RTTTL buffer for the bot !buzz command (see botBuzz()) -- sized
|
// Persistent RTTTL buffer for the bot !buzz command (see botBuzz()) -- sized
|
||||||
@@ -225,6 +229,13 @@ private:
|
|||||||
// dedicated lock-screen code path in loop().
|
// dedicated lock-screen code path in loop().
|
||||||
void syncLockToHome();
|
void syncLockToHome();
|
||||||
|
|
||||||
|
// 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
|
// Centred alert overlay (the showAlert() box). Wraps long text to up to
|
||||||
// three lines inside the box instead of letting it overflow the border.
|
// 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
|
// Shared by the normal render path and the lock screen (so a ringing
|
||||||
@@ -232,6 +243,9 @@ private:
|
|||||||
void renderAlertOverlay();
|
void renderAlertOverlay();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// A new screen PIN (see ui-core/ScreenLock.h); "" clears it. The caller saves the prefs.
|
||||||
|
void setNodeLockPassword(const char* plain);
|
||||||
|
bool passwordLockEnabled() const;
|
||||||
|
|
||||||
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) {
|
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : UITaskBase(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) {
|
||||||
next_batt_chck = _next_refresh = 0;
|
next_batt_chck = _next_refresh = 0;
|
||||||
@@ -411,7 +425,7 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isBuzzerQuiet() {
|
bool isBuzzerQuiet() {
|
||||||
#ifdef PIN_BUZZER
|
#ifdef PIN_BUZZER
|
||||||
return buzzer.isQuiet();
|
return buzzer.isQuiet();
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -451,6 +451,11 @@ MINI_ICON(ICON_MAP_TARGET, 5, // ⚑ flag on a pole — the active Locator/N
|
|||||||
packRow("#...."));
|
packRow("#...."));
|
||||||
|
|
||||||
// Keyboard special-key glyphs.
|
// 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
|
MINI_ICON(ICON_SHIFT, 7, // ⇧ caps
|
||||||
packRow("...#..."),
|
packRow("...#..."),
|
||||||
packRow("..###.."),
|
packRow("..###.."),
|
||||||
|
|||||||
Reference in New Issue
Block a user