v0.0.33 - Implement in-place mnemonic entry grid with staged prefix selection and fix stage-3 candidate rendering
This commit is contained in:
@@ -160,6 +160,58 @@ Includes:
|
||||
- missing policy -> deny path
|
||||
- missing `QREXEC_REMOTE_DOMAIN` -> fallback identity path
|
||||
|
||||
## 5.5 USB keyboard firmware workflows in a PCI-USB-controller qube
|
||||
|
||||
When a full USB controller is assigned directly to a qube via PCI passthrough, HID input does **not** flow through `sys-usb` input proxy policy. In this setup, keyboard-class USB blocking is usually caused by `usbguard` inside the owning qube.
|
||||
|
||||
### Scope check (dom0)
|
||||
|
||||
```bash
|
||||
qvm-prefs AI klass
|
||||
```
|
||||
|
||||
- `StandaloneVM`: service state changes in the qube root filesystem persist.
|
||||
- `AppVM`: root filesystem state resets at reboot; use `/rw/config/rc.local` for per-qube persistence.
|
||||
|
||||
### Disable usbguard immediately (inside AI qube)
|
||||
|
||||
```bash
|
||||
sudo systemctl disable --now usbguard
|
||||
sudo systemctl mask usbguard
|
||||
```
|
||||
|
||||
### Persist behavior across reboot
|
||||
|
||||
- **StandaloneVM**: the mask persists as-is.
|
||||
- **AppVM**: persist with `/rw/config/rc.local`:
|
||||
|
||||
```bash
|
||||
sudo tee /rw/config/rc.local >/dev/null <<'EOF'
|
||||
#!/bin/sh
|
||||
systemctl stop usbguard 2>/dev/null || true
|
||||
systemctl mask usbguard 2>/dev/null || true
|
||||
EOF
|
||||
sudo chmod +x /rw/config/rc.local
|
||||
```
|
||||
|
||||
### Restart + verify
|
||||
|
||||
```bash
|
||||
# dom0
|
||||
qvm-shutdown --wait AI && qvm-start AI
|
||||
|
||||
# inside AI qube
|
||||
systemctl is-enabled usbguard
|
||||
lsusb
|
||||
ls /dev/ttyACM*
|
||||
```
|
||||
|
||||
Expected result: `usbguard` is `masked`, the keyboard-class device enumerates, and serial/WebUSB workflows remain usable.
|
||||
|
||||
### Security trade-off
|
||||
|
||||
Disabling `usbguard` removes an important BadUSB defense for that qube. Keep this configuration limited to a dedicated hardware-development qube and avoid using it for general browsing or untrusted USB devices.
|
||||
|
||||
---
|
||||
|
||||
## 6. Security requirements
|
||||
|
||||
340
firmware/kb2040_hidden_signer/DISPLAY_SCREENS.md
Normal file
340
firmware/kb2040_hidden_signer/DISPLAY_SCREENS.md
Normal file
@@ -0,0 +1,340 @@
|
||||
# KB2040 Hidden Signer — Display Screens
|
||||
|
||||
Reference for the on-device UI so we can iterate on screen designs.
|
||||
|
||||
- Panel: **SSD1327 128x128 grayscale OLED** (I2C, STEMMA QT, addr `0x3D`).
|
||||
- Rendering source: [`display.cpp`](./display.cpp), driven by call sites in [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino).
|
||||
- Grayscale levels in use:
|
||||
- dim (headers / outlines): `0x04`
|
||||
- bar fill: `0x08`
|
||||
- bright text: `0x0F`
|
||||
- Each screen historically ended with a bottom **status line** at `y=118`, drawn by `drawStatusLine()`. The status-line writes are currently commented out in the sketch (they were being overwritten by debug/diagnostic prints), so that line now stays effectively empty unless re-enabled.
|
||||
|
||||
The boxes below are schematic (the panel is 128x128 px, not character-aligned). They show layout intent, relative position, and the text/labels actually printed.
|
||||
|
||||
---
|
||||
|
||||
## 1. Volume View
|
||||
Source: `drawVolumeView()` in [`display.cpp`](./display.cpp:60).
|
||||
Top-level "media mode" screen. Header switches between `MODE: MEDIA` / `MODE: SIGNER`.
|
||||
|
||||
```
|
||||
+--------------------------------+
|
||||
| MODE: MEDIA | <- tiny, dim (size 1)
|
||||
| |
|
||||
| VOL | <- size 2, dim
|
||||
| |
|
||||
| |
|
||||
| #### #### |
|
||||
| ## ## # ## | <- "50" centered, size 6
|
||||
| ## ## # ## | (huge digits)
|
||||
| ## ## # ## |
|
||||
| #### #### |
|
||||
| |
|
||||
| +------------------------+ | <- volume bar outline
|
||||
| |########## | | fill = volume %
|
||||
| +------------------------+ |
|
||||
| |
|
||||
| [status line] | <- currently disabled
|
||||
+--------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Signer Menu
|
||||
Source: `drawSignerMenu()` in [`display.cpp`](./display.cpp:102).
|
||||
Two items; `>` marks the current selection (shown here on index 0).
|
||||
|
||||
```
|
||||
+--------------------------------+
|
||||
| SIGNER MENU | <- size 1, dim
|
||||
| |
|
||||
| |
|
||||
| > Generate new | <- size 2, selected
|
||||
| |
|
||||
| |
|
||||
| Enter mnemonic | <- size 2
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| [status line] | <- currently disabled
|
||||
+--------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Enter Mnemonic (in-place 2-column grid)
|
||||
Source (planned): `drawEnterMnemonic()` in [`display.cpp`](./display.cpp).
|
||||
Replaces the old separate `PICK FIRST LETTER` modal + `WORD PICK` modal with a
|
||||
single grid that looks like the Generated Phrase screen but is filled in by the
|
||||
user. Header reads `ENTER MNEMONIC`.
|
||||
|
||||
Layout: 2 columns x 6 rows (slots 1-6 left, 7-12 right), same geometry as the
|
||||
Generated Phrase screen. All 12 numbers are shown; words are blank until entered.
|
||||
A cursor marks the active slot (starts at slot 1).
|
||||
|
||||
```
|
||||
+--------------------------------+
|
||||
| ENTER MNEMONIC | <- size 1, dim
|
||||
|> 1 m 7 | <- cursor '>' on active slot
|
||||
| 2 8 | slot 1 building: "m"
|
||||
| 3 9 |
|
||||
| 4 10 |
|
||||
| 5 11 |
|
||||
| 6 12 |
|
||||
| |
|
||||
| back letter | <- footer legend (stage-dependent)
|
||||
+--------------------------------+
|
||||
```
|
||||
|
||||
### Per-word 3-stage entry (in the active slot)
|
||||
|
||||
Only the letters chosen so far are shown; a full word appears only at stage 3.
|
||||
|
||||
| Stage | Scroll wheel cycles | Active slot shows | PLAY does |
|
||||
| ----- | ---------------------------------- | ----------------- | -------------------------- |
|
||||
| 1 | first letter `a`..`z` | `m` | lock letter 1 -> stage 2 |
|
||||
| 2 | valid 2nd letters for letter 1 | `mo` | lock letter 2 -> stage 3 |
|
||||
| 3 | words matching first two letters | `mosaic` | select word -> next slot |
|
||||
|
||||
- After slot 12's word is selected: build phrase, run
|
||||
`mnemonic_validate_basic()`, then go to Signer Ready (or INVALID modal).
|
||||
|
||||
### BACK behavior (context-sensitive erase)
|
||||
|
||||
`BACK` = the NEXT button (PREV stays unused here), consistent with other entry
|
||||
screens.
|
||||
|
||||
| Position | BACK action |
|
||||
| -------------------------------- | --------------------------------------------- |
|
||||
| Stage 3 (choosing word) | erase 2nd letter -> return to stage 2 |
|
||||
| Stage 2 (choosing 2nd letter) | erase 1st letter -> return to stage 1 |
|
||||
| Stage 1, slot N > 1 | clear previous slot's word, move cursor back to slot N-1 at stage 1 |
|
||||
| Stage 1, slot 1 | exit to Signer Menu |
|
||||
|
||||
### Footer legend (stage-dependent)
|
||||
|
||||
| Stage | PREV (left) | PLAY (center) | NEXT (right) |
|
||||
| ------- | ----------- | ------------- | ------------ |
|
||||
| Stage 1 | `-` | `letter` | `back` |
|
||||
| Stage 2 | `-` | `letter` | `back` |
|
||||
| Stage 3 | `-` | `select` | `back` |
|
||||
|
||||
### New wordlist helpers required (in `mnemonic_kb.cpp` / `.h`)
|
||||
|
||||
- `mnemonic_second_letters(char first, char *out, uint8_t *count)`
|
||||
-> set of valid 2nd letters given the 1st letter.
|
||||
- `mnemonic_prefix_range(char first, char second, uint16_t *start, uint16_t *count)`
|
||||
-> wordlist index range for all words starting with those two letters.
|
||||
|
||||
(`mnemonic_letter_range()` already provides the first-letter range and can back
|
||||
the stage-1 -> stage-2 transition.)
|
||||
|
||||
---
|
||||
|
||||
## 4. Mnemonic Page (Generated Phrase)
|
||||
Source: `drawMnemonicPage()` in [`display.cpp`](./display.cpp:152).
|
||||
Shows generated phrase in one screen: 2 columns × 6 rows (all 12 words, no spacer rows).
|
||||
|
||||
```
|
||||
+--------------------------------+
|
||||
| GENERATED PHRASE | <- size 1, dim
|
||||
| 1 abandon 7 issue | <- size 1, tight rows
|
||||
| 2 ability 8 jungle |
|
||||
| 3 able 9 kitchen |
|
||||
| 4 about 10 lemon |
|
||||
| 5 above 11 mosquito |
|
||||
| 6 absent 12 napkin |
|
||||
| |
|
||||
| PLAY confirm NEXT cancel | <- size 1
|
||||
| |
|
||||
| [status line] | <- currently disabled
|
||||
+--------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Message (generic 2-line modal)
|
||||
Source: `drawMessage()` in [`display.cpp`](./display.cpp:193).
|
||||
Generic title + 2 lines. Reused for several prompts.
|
||||
|
||||
Example — **Approval prompt** (title `APPROVAL`):
|
||||
|
||||
```
|
||||
+--------------------------------+
|
||||
| APPROVAL | <- size 1, dim
|
||||
| |
|
||||
| |
|
||||
| PLAY=Allow | <- size 2
|
||||
| |
|
||||
| PREV=Deny | <- size 2
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| |
|
||||
| [status line] | <- currently disabled
|
||||
+--------------------------------+
|
||||
```
|
||||
|
||||
Other current uses of this same screen:
|
||||
- `PICK FIRST LETTER` with lines `First letter` / `Letter: a`
|
||||
- `INVALID` with `Bad mnemonic` / `NEXT to return`
|
||||
- `ERROR` with `Generate failed` / `Try again`
|
||||
|
||||
---
|
||||
|
||||
## 6. Signer Ready
|
||||
Source: `drawSignerReady()` in [`display.cpp`](./display.cpp:210).
|
||||
Shown while waiting for the host to request signing.
|
||||
|
||||
```
|
||||
+--------------------------------+
|
||||
| SIGNER READY | <- size 1, dim
|
||||
| |
|
||||
| +------------------------+ |
|
||||
| | Awaiting | | <- size 2
|
||||
| | | |
|
||||
| | host | | <- size 2
|
||||
| +------------------------+ |
|
||||
| |
|
||||
| NEXT/PLAY: menu | <- size 1
|
||||
| |
|
||||
| [status line] | <- currently disabled
|
||||
+--------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes / observations for redesign
|
||||
|
||||
- The bottom status line frequently **duplicated** the in-body hint text (e.g. Word Pick showed `ENC scroll PLAY select` both in the body and in the status line). With status writes disabled, that redundancy is gone.
|
||||
- `PICK FIRST LETTER` reuses the generic Message screen rather than having a dedicated layout — a candidate for a purpose-built screen.
|
||||
- All hint text (`ENC` / `PLAY` / `NEXT` / `PREV`) refers to the physical controls: rotary encoder + the PLAY/NEXT/PREV buttons.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Screen ↔ render-function map
|
||||
|
||||
| Screen | Render function | display.cpp |
|
||||
| --------------- | --------------------- | ----------- |
|
||||
| Volume View | `drawVolumeView()` | [60](./display.cpp:60) |
|
||||
| Signer Menu | `drawSignerMenu()` | [102](./display.cpp:102) |
|
||||
| Word Pick | `drawWordPick()` | [123](./display.cpp:123) |
|
||||
| Mnemonic Page | `drawMnemonicPage()` | [152](./display.cpp:152) |
|
||||
| Message (modal) | `drawMessage()` | [193](./display.cpp:193) |
|
||||
| Signer Ready | `drawSignerReady()` | [210](./display.cpp:210) |
|
||||
| Status line | `drawStatusLine()` | [53](./display.cpp:53) |
|
||||
|
||||
---
|
||||
|
||||
# PROPOSAL: three-zone button legend footer
|
||||
|
||||
The three physical buttons sit directly below the screen in this order:
|
||||
|
||||
```
|
||||
PREV PLAY/PAUSE NEXT
|
||||
^ ^ ^
|
||||
(left) (center) (right)
|
||||
```
|
||||
|
||||
Idea: replace the free-text status line with a fixed **3-zone legend** along the
|
||||
bottom of every screen. Each zone shows a single short verb for what that button
|
||||
does on the current screen — left-aligned, center-aligned, right-aligned so each
|
||||
label sits over its physical button. No need to print the button name; position
|
||||
implies it. An empty zone means that button does nothing on that screen.
|
||||
|
||||
### Layout math (size 1 font, 6 px/char on a 128 px panel)
|
||||
|
||||
- Left zone: left-aligned at `x = 2`.
|
||||
- Center zone: centered around `x = 64` (use `getTextBounds` to center).
|
||||
- Right zone: right-aligned to `x = 126` (compute `x = 126 - width`).
|
||||
- Budget ~7 chars per zone worst case (7 x 6 = 42 px x 3 = 126 px). Keep verbs
|
||||
<= 7 chars (`select`, `confirm`, `cancel`, `back`, `deny`, `allow`, `next`,
|
||||
`prev`) to guarantee no overlap.
|
||||
- Optional: a thin divider line above the legend at `y ~= 112`, and 1 px vertical
|
||||
ticks between zones to reinforce the 3-button mapping.
|
||||
|
||||
```
|
||||
+--------------------------------+
|
||||
| ...screen body... |
|
||||
| |
|
||||
| -------------------------------| <- optional divider (y~112)
|
||||
| prev confirm next | <- 3 zones (left/center/right)
|
||||
+--------------------------------+
|
||||
^PREV ^PLAY ^NEXT (physical buttons below glass)
|
||||
```
|
||||
|
||||
### Per-screen legend mapping (derived from `signer_mode_tick()`)
|
||||
|
||||
Encoder still scrolls/selects on every screen; the legend only describes the 3
|
||||
buttons. `-` = button does nothing on that screen.
|
||||
|
||||
| Screen / state | PREV (left) | PLAY (center) | NEXT (right) |
|
||||
| -------------------------------------- | ----------- | ------------- | ------------ |
|
||||
| Volume / Media view | `prev` | `play` | `next` |
|
||||
| Signer Menu (`SIGNER_UI_MENU`) | `-` | `select` | `-` |
|
||||
| Generated Phrase (`GENERATE_REVIEW`) | `cancel` | `confirm` | `new` |
|
||||
| Signer Ready (`READY`) | `-` | `-` | `menu` |
|
||||
| Letter Pick (`ENTER_LETTER`) | `-` | `confirm` | `back` |
|
||||
| Word Pick (`ENTER_PICK`) | `-` | `select` | `back` |
|
||||
| Approval prompt (Message modal) | `deny` | `allow` | `-` |
|
||||
|
||||
Notes:
|
||||
- `PREV` is currently unused in most signer entry screens; it is meaningful in
|
||||
Media view (`prev` track), the Approval prompt (`deny`), and now the Generated
|
||||
Phrase screen (`cancel`).
|
||||
|
||||
### Behavior change required: Generated Phrase (`SIGNER_UI_GENERATE_REVIEW`)
|
||||
|
||||
The legend wording above implies a **functional change** in
|
||||
[`signer_mode_tick()`](./kb2040_hidden_signer.ino:657), not just labels:
|
||||
|
||||
| Button | Current behavior | Proposed behavior |
|
||||
| ------ | --------------------------- | ------------------------------------------ |
|
||||
| PREV | (unused) | `cancel` -> return to Signer Menu |
|
||||
| PLAY | confirm -> save seed | `confirm` -> save seed (unchanged) |
|
||||
| NEXT | cancel -> Signer Menu | `new` -> regenerate a fresh 12-word phrase and refresh this screen |
|
||||
|
||||
Implementation detail for `new`:
|
||||
- On `s_evt_next_pressed`, call `generate_mnemonic_12(s_generated_mnemonic, ...)`
|
||||
again, then `signer_enter_review_screen()` to redraw with the new words.
|
||||
- On `s_evt_prev_pressed`, call `signer_enter_menu_screen()` (the old NEXT path).
|
||||
|
||||
> Dependency check (resolved): `s_evt_prev_pressed` already exists and is wired
|
||||
> the same way as PLAY/NEXT — declared at [line 112](./kb2040_hidden_signer.ino:112),
|
||||
> reset in [`update_button_events()`](./kb2040_hidden_signer.ino:560), set on PREV
|
||||
> press at [line 581](./kb2040_hidden_signer.ino:581). No new plumbing needed.
|
||||
|
||||
### What this replaces on each screen
|
||||
|
||||
- Word Pick body lines `ENC scroll PLAY select` / `NEXT back` -> removed; the
|
||||
encoder action stays implicit, and PLAY/NEXT move to the footer legend.
|
||||
- Mnemonic Page body line `PLAY confirm NEXT cancel` -> moves to the footer
|
||||
legend (frees a body row).
|
||||
- Signer Ready body line `NEXT/PLAY: menu` -> becomes a single `menu` in the
|
||||
NEXT zone.
|
||||
- Approval modal `PLAY=Allow` / `PREV=Deny` body lines -> footer legend
|
||||
(`deny` left / `allow` center).
|
||||
|
||||
### Implementation sketch
|
||||
|
||||
1. Add a helper `drawButtonLegend(const char *left, const char *center, const char *right)`
|
||||
in [`display.cpp`](./display.cpp) that renders the 3 aligned zones (plus the
|
||||
optional divider/ticks) at the bottom.
|
||||
2. Give each screen a small static legend (or pass it in) and call
|
||||
`drawButtonLegend(...)` instead of `drawStatusLine()`.
|
||||
3. Remove the now-redundant in-body hint rows listed above.
|
||||
4. Keep `drawStatusLine()` available (commented/disabled) for transient debug.
|
||||
|
||||
### Open design decisions (need your call)
|
||||
|
||||
1. **NEXT "back" wording**: use the word `back` vs a glyph like `<` / `<<`.
|
||||
2. **Divider/ticks**: include the thin divider line + inter-zone ticks, or keep
|
||||
it text-only for a cleaner look.
|
||||
3. **Empty zones**: render nothing, or a subtle `·` placeholder so the user can
|
||||
tell the button is intentionally inactive.
|
||||
4. **Media view**: apply the same legend (`prev` / `play` / `next`), or leave the
|
||||
Volume view visually as-is since its meaning is already obvious.
|
||||
@@ -35,7 +35,7 @@ This directory contains an Arduino firmware scaffold that implements the `kb2040
|
||||
3. Wire exact `keyboard_modules` pin mapping and display abstraction.
|
||||
4. Add secure memory wiping and persistent policy storage if required.
|
||||
|
||||
## Arduino setup
|
||||
## Arduino setup (IDE)
|
||||
|
||||
1. Install Adafruit board index in Arduino IDE.
|
||||
2. Install **Adafruit RP2040** boards package.
|
||||
@@ -44,6 +44,25 @@ This directory contains an Arduino firmware scaffold that implements the `kb2040
|
||||
5. Install library **Adafruit TinyUSB Library**.
|
||||
6. Open and flash [`kb2040_hidden_signer.ino`](./kb2040_hidden_signer.ino).
|
||||
|
||||
## Arduino CLI flashing (recommended repeatable command)
|
||||
|
||||
Use this exact command when flashing from CLI:
|
||||
|
||||
```bash
|
||||
arduino-cli compile --upload -p /dev/ttyACM0 \
|
||||
--fqbn rp2040:rp2040:adafruit_kb2040:usbstack=tinyusb \
|
||||
firmware/kb2040_hidden_signer
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `usbstack=tinyusb` is **required** for this firmware (TinyUSB composite device).
|
||||
- If you omit that option, build fails with `TinyUSB is not selected`.
|
||||
- If `/dev/ttyACM0` changes, find the current port with:
|
||||
|
||||
```bash
|
||||
arduino-cli board list
|
||||
```
|
||||
|
||||
## Host validation
|
||||
|
||||
Install dependency:
|
||||
|
||||
@@ -26,19 +26,20 @@ bool g_signerMode = false;
|
||||
char g_status[24] = "BOOT";
|
||||
|
||||
uint8_t g_menuSelected = 0;
|
||||
uint8_t g_wordSlot = 0;
|
||||
uint8_t g_wordTotal = 12;
|
||||
uint16_t g_wordIndex = 0;
|
||||
char g_word[16] = "abandon";
|
||||
uint8_t g_mnemonicPage = 0;
|
||||
char g_mnemonic[256] = "";
|
||||
char g_msgTitle[18] = "";
|
||||
char g_msgLine1[22] = "";
|
||||
char g_msgLine2[22] = "";
|
||||
char g_enterSlots[12][16] = {{0}};
|
||||
uint8_t g_enterActiveSlot = 0;
|
||||
uint8_t g_enterStage = 1;
|
||||
char g_enterBuilding[16] = "";
|
||||
uint8_t g_enterCursorPos = 0;
|
||||
|
||||
enum signer_screen_t {
|
||||
SIGNER_SCREEN_MENU = 0,
|
||||
SIGNER_SCREEN_WORD_PICK = 1,
|
||||
SIGNER_SCREEN_ENTER_GRID = 1,
|
||||
SIGNER_SCREEN_MNEMONIC_PAGE = 2,
|
||||
SIGNER_SCREEN_MESSAGE = 3,
|
||||
SIGNER_SCREEN_READY = 4,
|
||||
@@ -57,12 +58,38 @@ void drawStatusLine() {
|
||||
g_display.print(g_status);
|
||||
}
|
||||
|
||||
void drawButtonLegend(const char *left, const char *center, const char *right) {
|
||||
g_display.setTextSize(1);
|
||||
g_display.setTextColor(kGrayText);
|
||||
g_display.setTextWrap(false);
|
||||
|
||||
if (left && left[0] != '\0') {
|
||||
g_display.setCursor(2, 118);
|
||||
g_display.print(left);
|
||||
}
|
||||
|
||||
int16_t x1 = 0;
|
||||
int16_t y1 = 0;
|
||||
uint16_t w = 0;
|
||||
uint16_t h = 0;
|
||||
|
||||
if (center && center[0] != '\0') {
|
||||
g_display.getTextBounds(center, 0, 0, &x1, &y1, &w, &h);
|
||||
const int16_t x = static_cast<int16_t>((kDisplayWidth - w) / 2);
|
||||
g_display.setCursor(x, 118);
|
||||
g_display.print(center);
|
||||
}
|
||||
|
||||
if (right && right[0] != '\0') {
|
||||
g_display.getTextBounds(right, 0, 0, &x1, &y1, &w, &h);
|
||||
const int16_t x = static_cast<int16_t>(126 - w);
|
||||
g_display.setCursor(x, 118);
|
||||
g_display.print(right);
|
||||
}
|
||||
}
|
||||
|
||||
void drawVolumeView() {
|
||||
g_display.setTextWrap(false);
|
||||
g_display.setTextSize(1);
|
||||
g_display.setTextColor(kGrayDim);
|
||||
g_display.setCursor(6, 4);
|
||||
g_display.print(g_signerMode ? "MODE: SIGNER" : "MODE: MEDIA");
|
||||
|
||||
g_display.setTextColor(kGrayDim);
|
||||
g_display.setTextSize(2);
|
||||
@@ -96,7 +123,7 @@ void drawVolumeView() {
|
||||
g_display.fillRect(barX + 1, barY + 1, fillW, barH - 2, kGrayBar);
|
||||
}
|
||||
|
||||
drawStatusLine();
|
||||
drawButtonLegend("prev", "play", "next");
|
||||
}
|
||||
|
||||
void drawSignerMenu() {
|
||||
@@ -117,36 +144,76 @@ void drawSignerMenu() {
|
||||
g_display.print(items[i]);
|
||||
}
|
||||
|
||||
drawStatusLine();
|
||||
drawButtonLegend("", "select", "");
|
||||
}
|
||||
|
||||
void drawWordPick() {
|
||||
void drawEnterMnemonic() {
|
||||
g_display.setTextWrap(false);
|
||||
g_display.setTextSize(1);
|
||||
g_display.setTextColor(kGrayDim);
|
||||
g_display.setCursor(2, 2);
|
||||
char hdr[20];
|
||||
snprintf(hdr, sizeof(hdr), "WORD %u/%u", (unsigned)(g_wordSlot + 1), (unsigned)g_wordTotal);
|
||||
g_display.print(hdr);
|
||||
g_display.print("ENTER MNEMONIC");
|
||||
|
||||
char idxBuf[20];
|
||||
snprintf(idxBuf, sizeof(idxBuf), "IDX %u", (unsigned)g_wordIndex);
|
||||
g_display.setCursor(82, 2);
|
||||
g_display.print(idxBuf);
|
||||
|
||||
g_display.drawRect(4, 24, 120, 56, kGrayDim);
|
||||
g_display.setTextSize(2);
|
||||
g_display.setTextColor(kGrayText);
|
||||
g_display.setCursor(10, 44);
|
||||
g_display.print(g_word);
|
||||
for (uint8_t i = 0; i < 12; ++i) {
|
||||
const uint8_t row = (i < 6) ? i : (i - 6);
|
||||
const int16_t x = (i < 6) ? 0 : 64;
|
||||
const int16_t y = static_cast<int16_t>(18 + row * 9);
|
||||
|
||||
g_display.setTextSize(1);
|
||||
g_display.setCursor(6, 88);
|
||||
g_display.print("ENC scroll PLAY select");
|
||||
g_display.setCursor(6, 98);
|
||||
g_display.print("NEXT back");
|
||||
const bool isActive = (i == g_enterActiveSlot);
|
||||
const char cursor = isActive ? '>' : ' ';
|
||||
|
||||
drawStatusLine();
|
||||
const char *slotWord = g_enterSlots[i];
|
||||
if (!slotWord) {
|
||||
slotWord = "";
|
||||
}
|
||||
|
||||
char activeBuf[16];
|
||||
const char *shownWord = slotWord;
|
||||
if (isActive) {
|
||||
const char *src = g_enterBuilding;
|
||||
if (!src) {
|
||||
src = "";
|
||||
}
|
||||
|
||||
if (g_enterStage < 3) {
|
||||
size_t srcLen = strlen(src);
|
||||
if (srcLen > 14) {
|
||||
srcLen = 14;
|
||||
}
|
||||
size_t pos = g_enterCursorPos;
|
||||
if (pos > srcLen) {
|
||||
pos = srcLen;
|
||||
}
|
||||
|
||||
size_t out = 0;
|
||||
for (size_t j = 0; j < pos && out < sizeof(activeBuf) - 1; ++j) {
|
||||
activeBuf[out++] = src[j];
|
||||
}
|
||||
if (out < sizeof(activeBuf) - 1) {
|
||||
activeBuf[out++] = '_';
|
||||
}
|
||||
for (size_t j = pos; j < srcLen && out < sizeof(activeBuf) - 1; ++j) {
|
||||
activeBuf[out++] = src[j];
|
||||
}
|
||||
activeBuf[out] = '\0';
|
||||
shownWord = activeBuf;
|
||||
} else if (src[0] != '\0') {
|
||||
shownWord = src;
|
||||
}
|
||||
}
|
||||
|
||||
char lineBuf[22];
|
||||
snprintf(lineBuf, sizeof(lineBuf), "%c%2u %s", cursor, (unsigned)(i + 1), shownWord);
|
||||
g_display.setCursor(x, y);
|
||||
g_display.print(lineBuf);
|
||||
}
|
||||
|
||||
if (g_enterStage == 3) {
|
||||
drawButtonLegend("", "select", "back");
|
||||
} else {
|
||||
drawButtonLegend("", "letter", "back");
|
||||
}
|
||||
}
|
||||
|
||||
void drawMnemonicPage() {
|
||||
@@ -160,34 +227,28 @@ void drawMnemonicPage() {
|
||||
strncpy(copy, g_mnemonic, sizeof(copy) - 1);
|
||||
copy[sizeof(copy) - 1] = '\0';
|
||||
|
||||
const uint8_t perPage = 4;
|
||||
const uint8_t startWord = g_mnemonicPage * perPage;
|
||||
const uint8_t endWord = startWord + perPage;
|
||||
|
||||
uint8_t wordNum = 0;
|
||||
char *words[12] = {nullptr};
|
||||
uint8_t count = 0;
|
||||
char *saveptr = nullptr;
|
||||
char *tok = strtok_r(copy, " ", &saveptr);
|
||||
uint8_t line = 0;
|
||||
while (tok) {
|
||||
if (wordNum >= startWord && wordNum < endWord) {
|
||||
char lineBuf[32];
|
||||
snprintf(lineBuf, sizeof(lineBuf), "%2u %s", (unsigned)(wordNum + 1), tok);
|
||||
g_display.setTextColor(kGrayText);
|
||||
g_display.setCursor(8, 24 + line * 18);
|
||||
g_display.print(lineBuf);
|
||||
line++;
|
||||
}
|
||||
wordNum++;
|
||||
while (tok && count < 12) {
|
||||
words[count++] = tok;
|
||||
tok = strtok_r(nullptr, " ", &saveptr);
|
||||
}
|
||||
|
||||
g_display.setTextSize(1);
|
||||
g_display.setCursor(6, 98);
|
||||
g_display.print("ENC page PLAY confirm");
|
||||
g_display.setCursor(6, 108);
|
||||
g_display.print("NEXT cancel");
|
||||
g_display.setTextColor(kGrayText);
|
||||
for (uint8_t i = 0; i < count; ++i) {
|
||||
const uint8_t row = (i < 6) ? i : (i - 6);
|
||||
const int16_t x = (i < 6) ? 0 : 64;
|
||||
const int16_t y = static_cast<int16_t>(18 + row * 9);
|
||||
|
||||
drawStatusLine();
|
||||
char lineBuf[20];
|
||||
snprintf(lineBuf, sizeof(lineBuf), "%2u %s", (unsigned)(i + 1), words[i]);
|
||||
g_display.setCursor(x, y);
|
||||
g_display.print(lineBuf);
|
||||
}
|
||||
|
||||
drawButtonLegend("cancel", "confirm", "new");
|
||||
}
|
||||
|
||||
void drawMessage() {
|
||||
@@ -204,7 +265,21 @@ void drawMessage() {
|
||||
g_display.setCursor(6, 62);
|
||||
g_display.print(g_msgLine2);
|
||||
|
||||
drawStatusLine();
|
||||
const char *left = "";
|
||||
const char *center = "";
|
||||
const char *right = "";
|
||||
|
||||
if (strcmp(g_msgTitle, "APPROVAL") == 0) {
|
||||
left = "deny";
|
||||
center = "allow";
|
||||
} else if (strcmp(g_msgTitle, "INVALID") == 0) {
|
||||
center = "select";
|
||||
right = "back";
|
||||
} else if (strcmp(g_msgTitle, "ERROR") == 0) {
|
||||
center = "select";
|
||||
}
|
||||
|
||||
drawButtonLegend(left, center, right);
|
||||
}
|
||||
|
||||
void drawSignerReady() {
|
||||
@@ -223,11 +298,7 @@ void drawSignerReady() {
|
||||
g_display.setCursor(30, 58);
|
||||
g_display.print("host");
|
||||
|
||||
g_display.setTextSize(1);
|
||||
g_display.setCursor(6, 96);
|
||||
g_display.print("NEXT/PLAY: menu");
|
||||
|
||||
drawStatusLine();
|
||||
drawButtonLegend("", "", "menu");
|
||||
}
|
||||
|
||||
void renderFrame() {
|
||||
@@ -244,8 +315,8 @@ void renderFrame() {
|
||||
case SIGNER_SCREEN_MENU:
|
||||
drawSignerMenu();
|
||||
break;
|
||||
case SIGNER_SCREEN_WORD_PICK:
|
||||
drawWordPick();
|
||||
case SIGNER_SCREEN_ENTER_GRID:
|
||||
drawEnterMnemonic();
|
||||
break;
|
||||
case SIGNER_SCREEN_MNEMONIC_PAGE:
|
||||
drawMnemonicPage();
|
||||
@@ -335,12 +406,22 @@ void display_show_signer_menu(uint8_t selected_index) {
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
void display_show_word_pick(uint8_t slot_index, uint8_t total_slots, const char *word, uint16_t word_index) {
|
||||
g_signerScreen = SIGNER_SCREEN_WORD_PICK;
|
||||
g_wordSlot = slot_index;
|
||||
g_wordTotal = total_slots;
|
||||
g_wordIndex = word_index;
|
||||
copy_text(g_word, sizeof(g_word), word);
|
||||
void display_show_enter_mnemonic(const char *const slots[12], uint8_t active_slot, uint8_t stage,
|
||||
const char *building_text, uint8_t cursor_pos) {
|
||||
g_signerScreen = SIGNER_SCREEN_ENTER_GRID;
|
||||
g_enterActiveSlot = (active_slot > 11) ? 11 : active_slot;
|
||||
g_enterStage = stage;
|
||||
g_enterCursorPos = cursor_pos;
|
||||
copy_text(g_enterBuilding, sizeof(g_enterBuilding), building_text);
|
||||
|
||||
for (uint8_t i = 0; i < 12; ++i) {
|
||||
const char *src = "";
|
||||
if (slots && slots[i]) {
|
||||
src = slots[i];
|
||||
}
|
||||
copy_text(g_enterSlots[i], sizeof(g_enterSlots[i]), src);
|
||||
}
|
||||
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
@@ -364,6 +445,9 @@ void display_show_signer_ready() {
|
||||
g_dirty = true;
|
||||
}
|
||||
|
||||
// Note: status-line updates are currently disabled at call sites in
|
||||
// kb2040_hidden_signer.ino by commenting out display_set_status(...)
|
||||
// invocations to keep the bottom status line from being overwritten.
|
||||
void display_set_status(const char *status) {
|
||||
if (!status) {
|
||||
return;
|
||||
|
||||
@@ -25,7 +25,8 @@ void display_set_volume(uint8_t volume);
|
||||
|
||||
// Signer UI rendering helpers.
|
||||
void display_show_signer_menu(uint8_t selected_index);
|
||||
void display_show_word_pick(uint8_t slot_index, uint8_t total_slots, const char *word, uint16_t word_index);
|
||||
void display_show_enter_mnemonic(const char *const slots[12], uint8_t active_slot, uint8_t stage,
|
||||
const char *building_text, uint8_t cursor_pos);
|
||||
void display_show_mnemonic_page(const char *mnemonic, uint8_t page_index);
|
||||
void display_show_message(const char *title, const char *line1, const char *line2);
|
||||
void display_show_signer_ready();
|
||||
|
||||
@@ -52,8 +52,7 @@ enum signer_ui_state_t {
|
||||
SIGNER_UI_MENU = 0,
|
||||
SIGNER_UI_READY = 1,
|
||||
SIGNER_UI_GENERATE_REVIEW = 2,
|
||||
SIGNER_UI_ENTER_LETTER = 3,
|
||||
SIGNER_UI_ENTER_PICK = 4,
|
||||
SIGNER_UI_ENTER_GRID = 3,
|
||||
};
|
||||
|
||||
static device_mode_t s_mode = MODE_MEDIA;
|
||||
@@ -67,12 +66,19 @@ static bool s_auto_approve = false;
|
||||
|
||||
// Signer UI state
|
||||
static uint8_t s_signer_menu_selected = 0; // 0=Generate, 1=Enter
|
||||
static uint8_t s_signer_review_page = 0; // 0..2 for 12 words at 4/page
|
||||
static uint8_t s_signer_review_page = 0; // legacy placeholder (review now renders all 12 words at once)
|
||||
static char s_generated_mnemonic[256] = "";
|
||||
static uint8_t s_enter_slot = 0; // 0..11
|
||||
static uint8_t s_enter_letter_index = 0; // 0..25 => a..z
|
||||
static uint16_t s_enter_word_index = 0; // current picker index
|
||||
static uint8_t s_enter_slot = 0; // 0..11
|
||||
static uint8_t s_enter_stage = 1; // 1=first letter, 2=second letter, 3=word select
|
||||
static uint8_t s_enter_first_letter_index = 0; // 0..25 => a..z
|
||||
static char s_enter_second_letters[26] = {0};
|
||||
static uint8_t s_enter_second_count = 0;
|
||||
static uint8_t s_enter_second_index = 0;
|
||||
static uint16_t s_enter_prefix_start = 0;
|
||||
static uint16_t s_enter_prefix_count = 0;
|
||||
static uint16_t s_enter_candidate_offset = 0;
|
||||
static uint16_t s_enter_selected_indices[12] = {0};
|
||||
static bool s_enter_committed[12] = {false};
|
||||
|
||||
static void signer_enter_ready_screen();
|
||||
|
||||
@@ -128,8 +134,9 @@ static void led_blink(int n, int on_ms, int off_ms) {
|
||||
static void send_consumer_key(uint16_t usage) {
|
||||
if (!usb_hid.ready()) return;
|
||||
usb_hid.sendReport16(1, usage);
|
||||
delay(2);
|
||||
delay(8); // restore hold for better throughput while keeping reliability
|
||||
usb_hid.sendReport16(1, 0);
|
||||
delay(4); // restore smaller inter-key gap for faster throughput
|
||||
}
|
||||
|
||||
static int8_t read_ab() {
|
||||
@@ -252,30 +259,30 @@ static void signer_apply_seed(const char *mnemonic) {
|
||||
strncpy(s_mnemonic, mnemonic, sizeof(s_mnemonic) - 1);
|
||||
s_mnemonic[sizeof(s_mnemonic) - 1] = '\0';
|
||||
s_seed_loaded = strlen(s_mnemonic) > 0;
|
||||
display_set_status(s_seed_loaded ? "Seed loaded" : "Seed cleared");
|
||||
// display_set_status(s_seed_loaded ? "Seed loaded" : "Seed cleared");
|
||||
}
|
||||
|
||||
static bool wait_for_user_approval(uint32_t timeout_ms) {
|
||||
display_show_message("APPROVAL", "PLAY=Allow", "PREV=Deny");
|
||||
display_set_status("Waiting approval");
|
||||
display_show_message("APPROVAL", "Host requests", "signature");
|
||||
// display_set_status("Waiting approval");
|
||||
|
||||
uint32_t start = millis();
|
||||
while ((millis() - start) < timeout_ms) {
|
||||
if (btn_pressed(PIN_BTN_PLAY)) {
|
||||
while (btn_pressed(PIN_BTN_PLAY)) delay(5);
|
||||
display_set_status("Approved");
|
||||
// display_set_status("Approved");
|
||||
return true;
|
||||
}
|
||||
if (btn_pressed(PIN_BTN_PREV)) {
|
||||
while (btn_pressed(PIN_BTN_PREV)) delay(5);
|
||||
display_set_status("Denied");
|
||||
// display_set_status("Denied");
|
||||
return false;
|
||||
}
|
||||
display_tick();
|
||||
delay(5);
|
||||
}
|
||||
|
||||
display_set_status("Approval timeout");
|
||||
// display_set_status("Approval timeout");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -293,13 +300,13 @@ static void handle_rpc(const char *req_json) {
|
||||
char method[64] = {0};
|
||||
if (!json_extract_string(req_json, "method", method, sizeof(method))) {
|
||||
send_json_response("{\"error\":{\"code\":-32600,\"message\":\"invalid request\"}}");
|
||||
display_set_status("RPC invalid request");
|
||||
// display_set_status("RPC invalid request");
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(method, "ping") == 0) {
|
||||
send_json_response("{\"result\":\"pong\"}");
|
||||
display_set_status("RPC ping");
|
||||
// display_set_status("RPC ping");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -311,7 +318,7 @@ static void handle_rpc(const char *req_json) {
|
||||
s_seed_loaded ? "true" : "false",
|
||||
s_auto_approve ? "true" : "false");
|
||||
send_json_response(resp);
|
||||
display_set_status("RPC get_status");
|
||||
// display_set_status("RPC get_status");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -319,7 +326,7 @@ static void handle_rpc(const char *req_json) {
|
||||
char phrase[256] = {0};
|
||||
if (!json_extract_string(req_json, "mnemonic", phrase, sizeof(phrase))) {
|
||||
send_json_response("{\"error\":{\"code\":-32602,\"message\":\"missing mnemonic\"}}");
|
||||
display_set_status("Mnemonic missing");
|
||||
// display_set_status("Mnemonic missing");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -335,14 +342,14 @@ static void handle_rpc(const char *req_json) {
|
||||
if (strstr(req_json, "\"value\":true") != nullptr) {
|
||||
s_auto_approve = true;
|
||||
send_json_response("{\"result\":true}");
|
||||
display_set_status("Auto-approve ON");
|
||||
// display_set_status("Auto-approve ON");
|
||||
} else if (strstr(req_json, "\"value\":false") != nullptr) {
|
||||
s_auto_approve = false;
|
||||
send_json_response("{\"result\":false}");
|
||||
display_set_status("Auto-approve OFF");
|
||||
// display_set_status("Auto-approve OFF");
|
||||
} else {
|
||||
send_json_response("{\"error\":{\"code\":-32602,\"message\":\"missing value\"}}");
|
||||
display_set_status("Auto-approve invalid");
|
||||
// display_set_status("Auto-approve invalid");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -350,7 +357,7 @@ static void handle_rpc(const char *req_json) {
|
||||
if (strcmp(method, "get_public_key") == 0) {
|
||||
if (!s_seed_loaded) {
|
||||
send_json_response("{\"error\":{\"code\":2014,\"message\":\"seed not loaded\"}}");
|
||||
display_set_status("No seed");
|
||||
// display_set_status("No seed");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -360,20 +367,20 @@ static void handle_rpc(const char *req_json) {
|
||||
char resp[192];
|
||||
snprintf(resp, sizeof(resp), "{\"result\":{\"pubkey\":\"%s\"}}", pubhex);
|
||||
send_json_response(resp);
|
||||
display_set_status("Pubkey served");
|
||||
// display_set_status("Pubkey served");
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(method, "sign_event") == 0) {
|
||||
if (!s_seed_loaded) {
|
||||
send_json_response("{\"error\":{\"code\":2014,\"message\":\"seed not loaded\"}}");
|
||||
display_set_status("Sign blocked:no seed");
|
||||
// display_set_status("Sign blocked:no seed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_mode != MODE_SIGNER) {
|
||||
send_json_response("{\"error\":{\"code\":2015,\"message\":\"not in signer mode\"}}");
|
||||
display_set_status("Sign blocked:mode");
|
||||
// display_set_status("Sign blocked:mode");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -390,7 +397,7 @@ static void handle_rpc(const char *req_json) {
|
||||
}
|
||||
|
||||
send_json_response("{\"result\":{\"sig\":\"dev_signature_placeholder\"}}");
|
||||
display_set_status("Event signed");
|
||||
// display_set_status("Event signed");
|
||||
if (s_signer_ui_state == SIGNER_UI_MENU) {
|
||||
display_show_signer_menu(s_signer_menu_selected);
|
||||
} else if (s_signer_ui_state == SIGNER_UI_READY) {
|
||||
@@ -400,7 +407,7 @@ static void handle_rpc(const char *req_json) {
|
||||
}
|
||||
|
||||
send_json_response("{\"error\":{\"code\":-32601,\"message\":\"method not found\"}}");
|
||||
display_set_status("RPC method missing");
|
||||
// display_set_status("RPC method missing");
|
||||
}
|
||||
|
||||
static void webusb_pump_frames() {
|
||||
@@ -426,7 +433,7 @@ static void webusb_pump_frames() {
|
||||
if (body_len > (sizeof(s_rx_buf) - 4)) {
|
||||
s_rx_len = 0;
|
||||
send_json_response("{\"error\":{\"code\":-32000,\"message\":\"frame too large\"}}");
|
||||
display_set_status("Frame too large");
|
||||
// display_set_status("Frame too large");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -452,56 +459,96 @@ static void signer_enter_menu_screen() {
|
||||
s_signer_ui_state = SIGNER_UI_MENU;
|
||||
display_set_view(DISPLAY_VIEW_SIGNER);
|
||||
display_show_signer_menu(s_signer_menu_selected);
|
||||
display_set_status("ENC scroll PLAY select");
|
||||
// display_set_status("ENC scroll PLAY select");
|
||||
}
|
||||
|
||||
static void signer_enter_ready_screen() {
|
||||
s_signer_ui_state = SIGNER_UI_READY;
|
||||
display_set_view(DISPLAY_VIEW_SIGNER);
|
||||
display_show_signer_ready();
|
||||
display_set_status("Ready to sign");
|
||||
// display_set_status("Ready to sign");
|
||||
}
|
||||
|
||||
static char signer_current_letter() {
|
||||
return (char)('a' + (s_enter_letter_index % 26));
|
||||
static char signer_first_letter() {
|
||||
return (char)('a' + (s_enter_first_letter_index % 26));
|
||||
}
|
||||
|
||||
static void signer_enter_letter_pick_screen() {
|
||||
s_signer_ui_state = SIGNER_UI_ENTER_LETTER;
|
||||
|
||||
char line1[20];
|
||||
char line2[24];
|
||||
snprintf(line1, sizeof(line1), "Word %u/12", (unsigned)(s_enter_slot + 1));
|
||||
snprintf(line2, sizeof(line2), "Letter: %c", signer_current_letter());
|
||||
display_show_message("PICK FIRST LETTER", line1, line2);
|
||||
display_set_status("ENC letter PLAY confirm");
|
||||
static char signer_second_letter() {
|
||||
if (s_enter_second_count == 0 || s_enter_second_index >= s_enter_second_count) {
|
||||
return 'a';
|
||||
}
|
||||
return s_enter_second_letters[s_enter_second_index];
|
||||
}
|
||||
|
||||
static void signer_enter_word_pick_screen() {
|
||||
s_signer_ui_state = SIGNER_UI_ENTER_PICK;
|
||||
static void signer_reset_active_slot_state() {
|
||||
s_enter_stage = 1;
|
||||
s_enter_first_letter_index = 0;
|
||||
s_enter_second_count = 0;
|
||||
s_enter_second_index = 0;
|
||||
s_enter_prefix_start = 0;
|
||||
s_enter_prefix_count = 0;
|
||||
s_enter_candidate_offset = 0;
|
||||
}
|
||||
|
||||
uint16_t start = 0;
|
||||
uint16_t count = 0;
|
||||
if (!mnemonic_letter_range(signer_current_letter(), &start, &count) || count == 0) {
|
||||
start = 0;
|
||||
count = 2048;
|
||||
static bool signer_prepare_second_letters() {
|
||||
s_enter_second_count = 0;
|
||||
s_enter_second_index = 0;
|
||||
return mnemonic_second_letters(signer_first_letter(), s_enter_second_letters, &s_enter_second_count) && s_enter_second_count > 0;
|
||||
}
|
||||
|
||||
static bool signer_prepare_prefix_candidates() {
|
||||
s_enter_prefix_start = 0;
|
||||
s_enter_prefix_count = 0;
|
||||
s_enter_candidate_offset = 0;
|
||||
return mnemonic_prefix_range(signer_first_letter(), signer_second_letter(), &s_enter_prefix_start, &s_enter_prefix_count) &&
|
||||
s_enter_prefix_count > 0;
|
||||
}
|
||||
|
||||
static void signer_render_enter_grid() {
|
||||
const char *slot_words[12] = {nullptr};
|
||||
for (uint8_t i = 0; i < 12; ++i) {
|
||||
if (s_enter_committed[i]) {
|
||||
slot_words[i] = mnemonic_word_at(s_enter_selected_indices[i]);
|
||||
} else {
|
||||
slot_words[i] = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (s_enter_word_index < start || s_enter_word_index >= (uint16_t)(start + count)) {
|
||||
s_enter_word_index = start;
|
||||
char building[16] = "";
|
||||
uint8_t cursor_pos = 0;
|
||||
|
||||
if (s_enter_stage == 1) {
|
||||
building[0] = signer_first_letter();
|
||||
building[1] = '\0';
|
||||
cursor_pos = 1;
|
||||
} else if (s_enter_stage == 2) {
|
||||
building[0] = signer_first_letter();
|
||||
building[1] = signer_second_letter();
|
||||
building[2] = '\0';
|
||||
cursor_pos = 2;
|
||||
} else {
|
||||
const uint16_t idx = (uint16_t)(s_enter_prefix_start + s_enter_candidate_offset);
|
||||
const char *word = mnemonic_word_at(idx);
|
||||
if (word) {
|
||||
strncpy(building, word, sizeof(building) - 1);
|
||||
building[sizeof(building) - 1] = '\0';
|
||||
}
|
||||
cursor_pos = (uint8_t)strlen(building);
|
||||
}
|
||||
|
||||
const char *word = mnemonic_word_at(s_enter_word_index);
|
||||
if (!word) word = "abandon";
|
||||
display_show_word_pick(s_enter_slot, 12, word, s_enter_word_index);
|
||||
display_set_status("ENC word PLAY select");
|
||||
display_show_enter_mnemonic(slot_words, s_enter_slot, s_enter_stage, building, cursor_pos);
|
||||
}
|
||||
|
||||
static void signer_enter_grid_screen() {
|
||||
s_signer_ui_state = SIGNER_UI_ENTER_GRID;
|
||||
display_set_view(DISPLAY_VIEW_SIGNER);
|
||||
signer_render_enter_grid();
|
||||
}
|
||||
|
||||
static void signer_enter_review_screen() {
|
||||
s_signer_ui_state = SIGNER_UI_GENERATE_REVIEW;
|
||||
if (s_signer_review_page > 2) s_signer_review_page = 2;
|
||||
display_show_mnemonic_page(s_generated_mnemonic, s_signer_review_page);
|
||||
display_set_status("PLAY confirm NEXT cancel");
|
||||
display_show_mnemonic_page(s_generated_mnemonic, 0);
|
||||
// display_set_status("PLAY confirm NEXT cancel");
|
||||
}
|
||||
|
||||
static void build_entered_mnemonic(char *out, size_t out_sz) {
|
||||
@@ -513,6 +560,10 @@ static void build_entered_mnemonic(char *out, size_t out_sz) {
|
||||
size_t cursor = 0;
|
||||
|
||||
for (uint8_t i = 0; i < 12; ++i) {
|
||||
if (!s_enter_committed[i]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char *w = mnemonic_word_at(s_enter_selected_indices[i]);
|
||||
if (!w) w = "abandon";
|
||||
|
||||
@@ -544,10 +595,10 @@ static void handle_mode_switch_chord() {
|
||||
if (s_mode == MODE_SIGNER) {
|
||||
display_set_view(DISPLAY_VIEW_SIGNER);
|
||||
signer_enter_menu_screen();
|
||||
display_set_status("Signer mode");
|
||||
// display_set_status("Signer mode");
|
||||
} else {
|
||||
display_set_view(DISPLAY_VIEW_VOLUME);
|
||||
display_set_status("Media mode");
|
||||
// display_set_status("Media mode");
|
||||
}
|
||||
|
||||
led_blink(s_mode == MODE_SIGNER ? 3 : 1, 80, 80);
|
||||
@@ -588,36 +639,32 @@ static void media_mode_tick() {
|
||||
const int32_t steps = encoder_take_steps();
|
||||
if (steps > 0) {
|
||||
for (int32_t i = 0; i < steps; ++i) {
|
||||
const int8_t applied = volume_change_by(+1);
|
||||
if (applied > 0) {
|
||||
send_consumer_key(HID_USAGE_CONSUMER_VOLUME_INCREMENT);
|
||||
display_set_volume(s_local_volume);
|
||||
}
|
||||
volume_change_by(+1); // keep display clamped to 0..100
|
||||
send_consumer_key(HID_USAGE_CONSUMER_VOLUME_INCREMENT); // always emit HID step
|
||||
display_set_volume(s_local_volume);
|
||||
}
|
||||
display_set_status("Volume up");
|
||||
// display_set_status("Volume up");
|
||||
} else if (steps < 0) {
|
||||
for (int32_t i = 0; i < -steps; ++i) {
|
||||
const int8_t applied = volume_change_by(-1);
|
||||
if (applied < 0) {
|
||||
send_consumer_key(HID_USAGE_CONSUMER_VOLUME_DECREMENT);
|
||||
display_set_volume(s_local_volume);
|
||||
}
|
||||
volume_change_by(-1); // keep display clamped to 0..100
|
||||
send_consumer_key(HID_USAGE_CONSUMER_VOLUME_DECREMENT); // always emit HID step
|
||||
display_set_volume(s_local_volume);
|
||||
}
|
||||
display_set_status("Volume down");
|
||||
// display_set_status("Volume down");
|
||||
}
|
||||
|
||||
if (s_evt_play_pressed) {
|
||||
send_consumer_key(HID_USAGE_CONSUMER_PLAY_PAUSE);
|
||||
display_set_status("Play/Pause");
|
||||
// display_set_status("Play/Pause");
|
||||
}
|
||||
if (s_evt_next_pressed) {
|
||||
// Preserve original hardware remap: NEXT wiring is swapped.
|
||||
send_consumer_key(HID_USAGE_CONSUMER_SCAN_PREVIOUS_TRACK);
|
||||
display_set_status("Prev track");
|
||||
// display_set_status("Prev track");
|
||||
}
|
||||
if (s_evt_prev_pressed) {
|
||||
send_consumer_key(HID_USAGE_CONSUMER_SCAN_NEXT_TRACK);
|
||||
display_set_status("Next track");
|
||||
// display_set_status("Next track");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,27 +693,22 @@ static void signer_mode_tick() {
|
||||
signer_enter_review_screen();
|
||||
} else {
|
||||
display_show_message("ERROR", "Generate failed", "Try again");
|
||||
display_set_status("Generate failed");
|
||||
// display_set_status("Generate failed");
|
||||
}
|
||||
} else {
|
||||
s_enter_slot = 0;
|
||||
s_enter_letter_index = 0;
|
||||
s_enter_word_index = s_enter_selected_indices[s_enter_slot];
|
||||
signer_enter_letter_pick_screen();
|
||||
for (uint8_t i = 0; i < 12; ++i) {
|
||||
s_enter_committed[i] = false;
|
||||
s_enter_selected_indices[i] = 0;
|
||||
}
|
||||
signer_reset_active_slot_state();
|
||||
signer_enter_grid_screen();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SIGNER_UI_GENERATE_REVIEW: {
|
||||
if (steps != 0) {
|
||||
int16_t p = (int16_t)s_signer_review_page + (steps > 0 ? 1 : -1);
|
||||
if (p < 0) p = 0;
|
||||
if (p > 2) p = 2;
|
||||
s_signer_review_page = (uint8_t)p;
|
||||
signer_enter_review_screen();
|
||||
}
|
||||
|
||||
if (s_evt_play_pressed) {
|
||||
signer_apply_seed(s_generated_mnemonic);
|
||||
signer_enter_ready_screen();
|
||||
@@ -674,7 +716,15 @@ static void signer_mode_tick() {
|
||||
|
||||
if (s_evt_next_pressed) {
|
||||
signer_enter_menu_screen();
|
||||
display_set_status("Generate canceled");
|
||||
// display_set_status("Generate canceled");
|
||||
}
|
||||
|
||||
if (s_evt_prev_pressed) {
|
||||
if (generate_mnemonic_12(s_generated_mnemonic, sizeof(s_generated_mnemonic)) == 0) {
|
||||
signer_enter_review_screen();
|
||||
} else {
|
||||
display_show_message("ERROR", "Generate failed", "Try again");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -686,85 +736,80 @@ static void signer_mode_tick() {
|
||||
break;
|
||||
}
|
||||
|
||||
case SIGNER_UI_ENTER_LETTER: {
|
||||
case SIGNER_UI_ENTER_GRID: {
|
||||
if (steps != 0) {
|
||||
int16_t idx = (int16_t)s_enter_letter_index + (steps > 0 ? 1 : -1);
|
||||
while (idx < 0) idx += 26;
|
||||
while (idx >= 26) idx -= 26;
|
||||
s_enter_letter_index = (uint8_t)idx;
|
||||
signer_enter_letter_pick_screen();
|
||||
}
|
||||
|
||||
if (s_evt_play_pressed) {
|
||||
uint16_t start = 0;
|
||||
uint16_t count = 0;
|
||||
if (mnemonic_letter_range(signer_current_letter(), &start, &count) && count > 0) {
|
||||
s_enter_word_index = start;
|
||||
} else {
|
||||
s_enter_word_index = 0;
|
||||
}
|
||||
signer_enter_word_pick_screen();
|
||||
}
|
||||
|
||||
if (s_evt_next_pressed) {
|
||||
if (s_enter_slot == 0) {
|
||||
signer_enter_menu_screen();
|
||||
display_set_status("Entry canceled");
|
||||
} else {
|
||||
s_enter_slot--;
|
||||
const char *prev_word = mnemonic_word_at(s_enter_selected_indices[s_enter_slot]);
|
||||
if (prev_word && prev_word[0] >= 'a' && prev_word[0] <= 'z') {
|
||||
s_enter_letter_index = (uint8_t)(prev_word[0] - 'a');
|
||||
if (s_enter_stage == 1) {
|
||||
int16_t idx = (int16_t)s_enter_first_letter_index + (steps > 0 ? 1 : -1);
|
||||
while (idx < 0) idx += 26;
|
||||
while (idx >= 26) idx -= 26;
|
||||
s_enter_first_letter_index = (uint8_t)idx;
|
||||
} else if (s_enter_stage == 2) {
|
||||
if (s_enter_second_count > 0) {
|
||||
int16_t idx = (int16_t)s_enter_second_index + (steps > 0 ? 1 : -1);
|
||||
while (idx < 0) idx += s_enter_second_count;
|
||||
while (idx >= s_enter_second_count) idx -= s_enter_second_count;
|
||||
s_enter_second_index = (uint8_t)idx;
|
||||
}
|
||||
s_enter_word_index = s_enter_selected_indices[s_enter_slot];
|
||||
signer_enter_word_pick_screen();
|
||||
display_set_status("Back one step");
|
||||
} else if (s_enter_prefix_count > 0) {
|
||||
int32_t rel = (int32_t)s_enter_candidate_offset + steps;
|
||||
while (rel < 0) rel += s_enter_prefix_count;
|
||||
while (rel >= s_enter_prefix_count) rel -= s_enter_prefix_count;
|
||||
s_enter_candidate_offset = (uint16_t)rel;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SIGNER_UI_ENTER_PICK: {
|
||||
uint16_t start = 0;
|
||||
uint16_t count = 0;
|
||||
if (!mnemonic_letter_range(signer_current_letter(), &start, &count) || count == 0) {
|
||||
start = 0;
|
||||
count = 2048;
|
||||
}
|
||||
|
||||
if (steps != 0) {
|
||||
int32_t rel = (int32_t)s_enter_word_index - (int32_t)start;
|
||||
rel += steps;
|
||||
while (rel < 0) rel += count;
|
||||
while (rel >= count) rel -= count;
|
||||
s_enter_word_index = (uint16_t)(start + rel);
|
||||
signer_enter_word_pick_screen();
|
||||
signer_render_enter_grid();
|
||||
}
|
||||
|
||||
if (s_evt_play_pressed) {
|
||||
s_enter_selected_indices[s_enter_slot] = s_enter_word_index;
|
||||
|
||||
if (s_enter_slot < 11) {
|
||||
s_enter_slot++;
|
||||
s_enter_letter_index = 0;
|
||||
s_enter_word_index = s_enter_selected_indices[s_enter_slot];
|
||||
signer_enter_letter_pick_screen();
|
||||
if (s_enter_stage == 1) {
|
||||
if (signer_prepare_second_letters()) {
|
||||
s_enter_stage = 2;
|
||||
signer_render_enter_grid();
|
||||
}
|
||||
} else if (s_enter_stage == 2) {
|
||||
if (signer_prepare_prefix_candidates()) {
|
||||
s_enter_stage = 3;
|
||||
signer_render_enter_grid();
|
||||
}
|
||||
} else {
|
||||
char phrase[256];
|
||||
build_entered_mnemonic(phrase, sizeof(phrase));
|
||||
if (mnemonic_validate_basic(phrase)) {
|
||||
signer_apply_seed(phrase);
|
||||
signer_enter_ready_screen();
|
||||
s_enter_selected_indices[s_enter_slot] = (uint16_t)(s_enter_prefix_start + s_enter_candidate_offset);
|
||||
s_enter_committed[s_enter_slot] = true;
|
||||
|
||||
if (s_enter_slot < 11) {
|
||||
s_enter_slot++;
|
||||
signer_reset_active_slot_state();
|
||||
signer_render_enter_grid();
|
||||
} else {
|
||||
display_show_message("INVALID", "Bad mnemonic", "NEXT to return");
|
||||
display_set_status("Validation failed");
|
||||
char phrase[256];
|
||||
build_entered_mnemonic(phrase, sizeof(phrase));
|
||||
if (mnemonic_validate_basic(phrase)) {
|
||||
signer_apply_seed(phrase);
|
||||
signer_enter_ready_screen();
|
||||
} else {
|
||||
display_show_message("INVALID", "Bad mnemonic", "Fix and retry");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s_evt_next_pressed) {
|
||||
signer_enter_letter_pick_screen();
|
||||
display_set_status("Back to letter");
|
||||
if (s_enter_stage == 3) {
|
||||
s_enter_stage = 2;
|
||||
signer_render_enter_grid();
|
||||
} else if (s_enter_stage == 2) {
|
||||
s_enter_stage = 1;
|
||||
signer_render_enter_grid();
|
||||
} else {
|
||||
if (s_enter_slot == 0) {
|
||||
signer_enter_menu_screen();
|
||||
} else {
|
||||
s_enter_slot--;
|
||||
s_enter_committed[s_enter_slot] = false;
|
||||
s_enter_selected_indices[s_enter_slot] = 0;
|
||||
signer_reset_active_slot_state();
|
||||
signer_render_enter_grid();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -808,7 +853,7 @@ void setup() {
|
||||
|
||||
display_set_mode(false);
|
||||
display_set_view(DISPLAY_VIEW_VOLUME);
|
||||
display_set_status("USB mounted");
|
||||
// display_set_status("USB mounted");
|
||||
|
||||
digitalWrite(LED_BUILTIN, HIGH);
|
||||
led_blink(2, 60, 60);
|
||||
@@ -821,7 +866,7 @@ void loop() {
|
||||
display_set_mode(s_mode == MODE_SIGNER);
|
||||
display_set_volume(s_local_volume);
|
||||
display_set_view(s_mode == MODE_SIGNER ? DISPLAY_VIEW_SIGNER : DISPLAY_VIEW_VOLUME);
|
||||
display_set_status("Ready");
|
||||
// display_set_status("Ready");
|
||||
if (s_mode == MODE_SIGNER) {
|
||||
signer_enter_menu_screen();
|
||||
}
|
||||
@@ -860,7 +905,7 @@ void loop() {
|
||||
(unsigned long)s_diag_bytes_rx,
|
||||
(unsigned long)s_diag_frames_rx,
|
||||
(unsigned long)s_diag_responses_tx);
|
||||
display_set_status(diag);
|
||||
// display_set_status(diag);
|
||||
|
||||
// Idle LED state: ON (HIGH) when connected, OFF otherwise. The RX path
|
||||
// drives it LOW briefly on each byte, so a flicker == bytes arriving.
|
||||
|
||||
@@ -84,6 +84,98 @@ bool mnemonic_letter_range(char letter, uint16_t *out_start, uint16_t *out_count
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mnemonic_second_letters(char first, char *out, uint8_t *count) {
|
||||
if (!out || !count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*count = 0;
|
||||
|
||||
if (first >= 'A' && first <= 'Z') {
|
||||
first = (char)(first - 'A' + 'a');
|
||||
}
|
||||
if (first < 'a' || first > 'z') {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t start = 0;
|
||||
uint16_t range_count = 0;
|
||||
if (!mnemonic_letter_range(first, &start, &range_count) || range_count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool seen[26] = {false};
|
||||
uint8_t found = 0;
|
||||
for (uint16_t i = 0; i < range_count; ++i) {
|
||||
const char* w = BIP39_WORDLIST[start + i];
|
||||
char c = w[1];
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c = (char)(c - 'A' + 'a');
|
||||
}
|
||||
if (c < 'a' || c > 'z') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t idx = (uint8_t)(c - 'a');
|
||||
if (!seen[idx]) {
|
||||
seen[idx] = true;
|
||||
out[found++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
*count = found;
|
||||
return found > 0;
|
||||
}
|
||||
|
||||
bool mnemonic_prefix_range(char first, char second, uint16_t *out_start, uint16_t *out_count) {
|
||||
if (!out_start || !out_count) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first >= 'A' && first <= 'Z') {
|
||||
first = (char)(first - 'A' + 'a');
|
||||
}
|
||||
if (second >= 'A' && second <= 'Z') {
|
||||
second = (char)(second - 'A' + 'a');
|
||||
}
|
||||
if (first < 'a' || first > 'z' || second < 'a' || second > 'z') {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t start = 0;
|
||||
uint16_t range_count = 0;
|
||||
if (!mnemonic_letter_range(first, &start, &range_count) || range_count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int first_match = -1;
|
||||
int last_match = -1;
|
||||
for (uint16_t i = 0; i < range_count; ++i) {
|
||||
const char* w = BIP39_WORDLIST[start + i];
|
||||
char c = w[1];
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
c = (char)(c - 'A' + 'a');
|
||||
}
|
||||
|
||||
if (c == second) {
|
||||
if (first_match < 0) {
|
||||
first_match = (int)(start + i);
|
||||
}
|
||||
last_match = (int)(start + i);
|
||||
} else if (first_match >= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (first_match < 0 || last_match < first_match) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*out_start = (uint16_t)first_match;
|
||||
*out_count = (uint16_t)(last_match - first_match + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* mnemonic_word_at(uint16_t index) {
|
||||
if (index >= 2048) {
|
||||
return nullptr;
|
||||
|
||||
@@ -7,5 +7,7 @@
|
||||
const char* mnemonic_word_at(uint16_t index);
|
||||
int mnemonic_word_index(const char* word);
|
||||
bool mnemonic_letter_range(char letter, uint16_t *out_start, uint16_t *out_count);
|
||||
bool mnemonic_second_letters(char first, char *out, uint8_t *count);
|
||||
bool mnemonic_prefix_range(char first, char second, uint16_t *out_start, uint16_t *out_count);
|
||||
int generate_mnemonic_12(char* out, size_t out_len);
|
||||
bool mnemonic_validate_basic(const char* mnemonic);
|
||||
|
||||
@@ -491,8 +491,8 @@ int socket_name_random(char *out, size_t out_len);
|
||||
/* Version information (auto-updated by build/version tooling) */
|
||||
#define NSIGNER_VERSION_MAJOR 0
|
||||
#define NSIGNER_VERSION_MINOR 0
|
||||
#define NSIGNER_VERSION_PATCH 32
|
||||
#define NSIGNER_VERSION "v0.0.32"
|
||||
#define NSIGNER_VERSION_PATCH 33
|
||||
#define NSIGNER_VERSION "v0.0.33"
|
||||
|
||||
|
||||
/* NSIGNER_HEADERLESS_DECLS_END */
|
||||
|
||||
Reference in New Issue
Block a user