feat: Add PIN input keyboard and use it on lockscreen

This commit is contained in:
3urobeat
2026-09-08 22:25:39 +02:00
parent a16c9bf2da
commit 6f8703842c
6 changed files with 139 additions and 6 deletions
+2 -1
View File
@@ -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.
@@ -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();
@@ -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;
+7 -1
View File
@@ -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;
+1
View File
@@ -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
+5
View File
@@ -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("..###.."),