Skip to content

Add touch-friendly SeedQR transcription viewer - #369

Open
schnuartz-ai wants to merge 1 commit into
cryptoadvance:masterfrom
schnuartz-ai:feature/seedqr-transcription-viewer
Open

Add touch-friendly SeedQR transcription viewer#369
schnuartz-ai wants to merge 1 commit into
cryptoadvance:masterfrom
schnuartz-ai:feature/seedqr-transcription-viewer

Conversation

@schnuartz-ai

@schnuartz-ai schnuartz-ai commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements a guided, zone-based SeedQR transcription viewer, adapted from
SeedSigner's SeedQR transcription workflow to Specter DIY's 480x800
touchscreen (see SeedSigner's docs/seed_qr/README.md for the reference UX).

Selecting SeedQR (digits) or Compact SeedQR (binary) from the
mnemonic export menu now opens a dedicated overview screen: the complete
QR code, split into labeled sections, with a coordinate axis (row letters
/ column numbers) around it and a "Tap a section to enlarge" hint. Tapping
a section opens it hugely enlarged, with crisp square modules, visible
grid lines, and Left/Right/Up/Down navigation to neighboring sections —
making it practical to copy a SeedQR onto paper or a metal backup plate by
hand. Returning to the overview preserves the section you were last
looking at. Plaintext QR export is unchanged.

(I'll attach a few simulator screenshots to this description separately.)

Design

  • src/seedqr.py: pure matrix/geometry helpers (matrix generation +
    validation, section geometry, touch-to-section mapping, navigation) with
    no lvgl dependency, so this logic is unit-testable on a desktop
    Python interpreter without a display or firmware build.
  • src/gui/screens/seedqr.py: SeedQROverviewScreen + SeedQRZoomScreen,
    and the show_seedqr() overview<->zoom navigation loop. Both screens
    render the same canonical module matrix (generated once via
    seedqr.generate_matrix()), so the overview and every zoomed section
    can never disagree about module data.
  • src/keystore/ram.py: routes the Standard/Compact SeedQR menu choices
    to show_seedqr(); the Plaintext QR path is unchanged except it now
    passes sensitive=True.
  • Section sizing matches SeedSigner: a 21x21 QR groups into 7x7-module
    sections (3x3 grid); every larger SeedQR size groups into 5x5-module
    sections. A non-square-divisible size (e.g. 29x29) has a smaller
    trailing row/column, which is rendered at its true, smaller size (not
    padded out with filler modules).

Security

This handles wallet recovery material, so:

  • The mnemonic, SeedQR digit string, Compact SeedQR bytes, and the QR
    module matrix are never printed, logged, or persisted.
  • While reviewing gui/components/qrcode.py I found a pre-existing
    unconditional print(text) that logged the full QR payload even on
    real hardware
    , not just the simulator. Removed it, and added a
    sensitive flag (threaded through add_qrcode/QRAlert) so simulator
    debug logging can be suppressed per-QR; it's now set for every
    mnemonic-related QR, including the pre-existing Plaintext export path.
  • Both new screens drop their references to the secret matrix on LVGL
    DELETE.

Companion fix (separate repo)

While building the matrix-generation helper I found a pre-existing bug in
the f469-disco submodule's qrcode usermod: _qrcode_encode() decided
text-vs-binary mode by scanning for an embedded 0x00 byte, so a bytes
payload with no zero byte (common for Compact SeedQR's raw entropy) fell
through to qrcodegen_encodeText(), which expects a NUL-terminated string
and could read past the intended data into uninitialized stack memory.
Fixed at diybitcoinhardware/f469-disco#43 (using the actual Python object
type instead of content-sniffing). I don't have write access to that
repo's upstream, so it's a separate PR; the f469-disco submodule pointer
in this PR is unchanged (still upstream's commit) — once #43 merges,
the submodule pointer here should be bumped in a follow-up commit.

Test plan

  • python3 run_native_tests.py — 41/41 tests pass (encoding, section
    geometry, touch mapping, navigation, and print/log security
    regressions for the pure seedqr module).
  • python3 -m compileall src test — clean.
  • make unix (real firmware build, including this submodule fix) —
    builds successfully; confirmed the compiled qrcode.encode_to_string()
    produces the correct matrix size for all 4 SeedQR combinations
    (12/24-word x Standard/Compact), including entropy with no embedded
    zero byte.
  • Manually exercised all 4 combinations in the unix simulator: full
    overview, tapping into first/middle/last/partial sections, all four
    nav directions, back-to-overview section
  • Not tested on physical hardware.

This PR implements the SeedQR transcription mode requested in #212, including the sectioned overview and enlarged grid-based navigation for copying the QR to paper or metal.

Fixes #212

Screenshot 2026-07-28 112219 Screenshot 2026-07-28 112239 Screenshot 2026-07-28 114854 Screenshot 2026-07-28 122510 Screenshot 2026-07-28 122524 Screenshot 2026-07-28 122503

Selecting Standard SeedQR (digits) or Compact SeedQR (binary) from the
mnemonic export menu now opens a dedicated overview screen: the complete
QR code, split into labeled sections (matching SeedSigner's SeedQR
transcription workflow), with a coordinate axis (row letters / column
numbers) around it. Tapping a section opens it hugely enlarged, with
crisp square modules, visible grid lines, and Left/Right/Up/Down
navigation to neighboring sections -- making it practical to copy a
SeedQR onto paper or a metal backup plate by hand.

- src/seedqr.py: pure matrix/geometry helpers (no lvgl dependency), so
  section geometry, touch-to-section mapping, and navigation logic are
  unit-testable without a display or firmware build.
- src/gui/screens/seedqr.py: the two new screens (SeedQROverviewScreen,
  SeedQRZoomScreen) and the overview<->zoom navigation loop.
- src/keystore/ram.py: routes Standard/Compact SeedQR selections to the
  new viewer; Plaintext QR export is unchanged except it's now marked
  sensitive so it stops being logged in the simulator.
- src/gui/components/qrcode.py, gui/common.py, gui/screens/qralert.py:
  add a `sensitive` flag and remove an unconditional print() that leaked
  QR contents (including on real hardware, not just the simulator).
- test/tests_native/test_seedqr.py: encoding, section geometry, touch
  mapping, navigation, and security-regression tests (41 total) for the
  pure logic module.

The corresponding fix to the f469-disco submodule's qrcode usermod (a
pre-existing bug where compact/binary QR payloads could read past their
buffer into uninitialized stack memory) is proposed separately at
diybitcoinhardware/f469-disco#43, since this repo has no write access to
that submodule's upstream. Once that merges, the f469-disco submodule
pointer here should be bumped in a follow-up commit.
@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for specter-diy-docs ready!

Name Link
🔨 Latest commit 3d48e6f
🔍 Latest deploy log https://app.netlify.com/projects/specter-diy-docs/deploys/6a6882ab0b3643000863ba93
😎 Deploy Preview https://deploy-preview-369--specter-diy-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@Schnuartz

Copy link
Copy Markdown
Contributor

@al-munazzim could you review that?

@al-munazzim al-munazzim left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough implementation and test coverage. I reviewed the SeedQR viewer changes, including the pure seedqr helpers, the LVGL overview/zoom screens, the mnemonic export routing, and the QR logging/sensitive handling.

What I checked locally:

  • python3 -m compileall -q src test
  • python test/run_native_tests.py in a fresh uv venv with test/integration/requirements.txt installed: 41/41 tests passed
  • Diff/read-through of the security-sensitive paths that handle mnemonic / Standard SeedQR / Compact SeedQR material

The design looks sound to me:

  • Standard and Compact SeedQR now route to the transcription viewer without rendering the raw digit string / entropy bytes as text.
  • The viewer uses one canonical QR module matrix for overview and zoom, so the two views cannot diverge.
  • Section sizing and partial trailing sections are covered by tests.
  • The previous QR payload logging issue is addressed with the sensitive flag and the plaintext mnemonic QR path is marked sensitive too.
  • CI is green.

No blocking findings from my review. Approving.

schnuartz-ai pushed a commit to Schnuartz/specter-diy that referenced this pull request Aug 9, 2026
Extends the SeedQR transcription viewer (PR cryptoadvance#369) with a verification
step: after copying a Standard/Compact SeedQR by hand, the user presses
Done, scans the copy with the device's existing QR scanner, and gets a
clear success/mismatch/unreadable result.

- src/hosts/qr.py's scanner (GM65/M3Y over UART) only ever exposes the
  decoded payload, never a raw module grid or image -- there is no
  hardware/firmware path to a true module-by-module diff of the physical
  drawing today. Verification is therefore payload-only: a successful
  scan that decodes to the expected SeedQR is reported as "contents
  verified", explicitly not as "every module was drawn perfectly" (QR
  error correction could mask real drawing mistakes). A decoded-but-wrong
  payload is a hard failure; no decode is reported as unreadable.
- src/seedqr.py: verify_payload/verify_scanned_payload implement that
  payload-only check. compare_matrices/mismatch_stats/matrix_diff/
  diff_grid implement true module-level comparison and are fully unit
  tested, but nothing currently feeds them a physically-scanned matrix
  (that would require regenerating a matrix from the decoded payload,
  silently hiding whatever error correction papered over) -- they're
  ready for a future scanner that can expose raw module data.
- src/gui/screens/seedqr.py: the overview screen gains a Done button
  (only when a scanner is wired in) that starts
  View/copy -> Done -> Scan copied QR -> result. Success is a clear
  green state (with the verified matrix rendered in green); mismatch
  and unreadable are clearly marked failure states with Redraw/Try
  again actions. The scanned bytes are only ever held long enough to
  classify them and are never logged, displayed as text, or persisted.
- src/specter.py: Specter.scan_qr() reuses the existing QRHost/get_data
  scanning infrastructure (host selection, progress screen, cancel
  handling) instead of reimplementing scanning.
- src/keystore/ram.py: threads the scan_qr callable from
  show_mnemonic() into show_seedqr().
- test/tests_native/test_seedqr.py: payload verification (Standard/
  Compact match, mismatch, truncated, wrong-type-scan), matrix
  comparison (identical, missing/extra modules, multiple mismatches,
  counts/percentages, size mismatch, invalid input, 21x21/25x25/29x29),
  and security regressions (no unconditional prints in the new
  verification code paths, no scanned-data interpolation into labels).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WfSHkd5CGgU4SsR79yrpKg
schnuartz-ai added a commit to Schnuartz/specter-diy that referenced this pull request Aug 9, 2026
Extends the SeedQR transcription viewer (PR cryptoadvance#369) with a verification
step: after copying a Standard/Compact SeedQR by hand, the user presses
Done, scans the copy with the device's existing QR scanner, and gets a
clear success/mismatch/unreadable result.

- src/hosts/qr.py's scanner (GM65/M3Y over UART) only ever exposes the
  decoded payload, never a raw module grid or image -- there is no
  hardware/firmware path to a true module-by-module diff of the physical
  drawing today. Verification is therefore payload-only: a successful
  scan that decodes to the expected SeedQR is reported as "contents
  verified", explicitly not as "every module was drawn perfectly" (QR
  error correction could mask real drawing mistakes). A decoded-but-wrong
  payload is a hard failure; no decode is reported as unreadable.
- src/seedqr.py: verify_payload/verify_scanned_payload implement that
  payload-only check. compare_matrices/mismatch_stats/matrix_diff/
  diff_grid implement true module-level comparison and are fully unit
  tested, but nothing currently feeds them a physically-scanned matrix
  (that would require regenerating a matrix from the decoded payload,
  silently hiding whatever error correction papered over) -- they're
  ready for a future scanner that can expose raw module data.
- src/gui/screens/seedqr.py: the overview screen gains a Done button
  (only when a scanner is wired in) that starts
  View/copy -> Done -> Scan copied QR -> result. Success is a clear
  green state (with the verified matrix rendered in green); mismatch
  and unreadable are clearly marked failure states with Redraw/Try
  again actions. The scanned bytes are only ever held long enough to
  classify them and are never logged, displayed as text, or persisted.
- src/specter.py: Specter.scan_qr() reuses the existing QRHost/get_data
  scanning infrastructure (host selection, progress screen, cancel
  handling) instead of reimplementing scanning.
- src/keystore/ram.py: threads the scan_qr callable from
  show_mnemonic() into show_seedqr().
- test/tests_native/test_seedqr.py: payload verification (Standard/
  Compact match, mismatch, truncated, wrong-type-scan), matrix
  comparison (identical, missing/extra modules, multiple mismatches,
  counts/percentages, size mismatch, invalid input, 21x21/25x25/29x29),
  and security regressions (no unconditional prints in the new
  verification code paths, no scanned-data interpolation into labels).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add transcription mode to SeedQR

3 participants