Files
n_signer/firmware/kb2040_hidden_signer/DISPLAY_SCREENS.md

14 KiB
Raw Permalink Blame History

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 in 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. 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
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 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. 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.