14 KiB
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, driven by call sites inkb2040_hidden_signer.ino. - Grayscale levels in use:
- dim (headers / outlines):
0x04 - bar fill:
0x08 - bright text:
0x0F
- dim (headers / outlines):
- Each screen historically ended with a bottom status line at
y=118, drawn bydrawStatusLine(). 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.
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.
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.
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.
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.
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 LETTERwith linesFirst letter/Letter: aINVALIDwithBad mnemonic/NEXT to returnERRORwithGenerate failed/Try again
6. Signer Ready
Source: drawSignerReady() in display.cpp.
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 selectboth in the body and in the status line). With status writes disabled, that redundancy is gone. PICK FIRST LETTERreuses 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.