Zoom-to-transcribe for seed QRs + BIP-85 encoding choice#354
Open
polto wants to merge 2 commits into
Open
Conversation
When the device shows a seed-class QR (main mnemonic, BIP-85 derived
mnemonic, SLIP-77 blinding key) the QR codes are dense enough that
copying them by hand is error-prone. This commit adds a "Transcribe"
button to the QRAlert that opens a full-screen zoom view of one
adaptive grid zone at a time.
Architecture
------------
src/qr_transcribe_logic.py is a pure module (no LVGL imports) with
six small functions that drive the screen: adaptive_grid_size picks
an N x N grid based on the QR size so each zone holds ~12-18 modules
per side, zone_bounds slices the bitmap, module_at reads a single bit
from the packed qrcode.encode buffer, next_zone clamps directional
moves to the grid edges, iter_zone_modules yields per-cell coords for
the screen renderer, and clamp_zone snaps the current zone back into
range when the user shrinks N. All six are covered by unit tests in
test/tests/test_qr_transcribe.py.
The screen src/gui/screens/qr_transcribe.py is an LVGL Screen that
renders one zone as a grid of small lv.obj cells (black for dark
modules, white for light) inside a fixed 400x400 white panel. Each
cell carries a 1-pixel gap so adjacent dark modules read as discrete
dots rather than a connected blob, which is the property the user
needs when copying onto paper. Axis labels along the top and left
edges show the absolute QR row / column indices (1..S) so the user
always knows where the zone sits in the full matrix. A widget grid
is used rather than lv.canvas because lv.canvas is not rendered by
the SDL backend in the unix simulator build.
The bottom area carries:
- a D-pad (50 x 50 buttons) on the left for zone navigation, with
edge buttons greyed out via lv.btn.STATE.INA when the user is at
a grid edge
- a mini-map on the right that highlights the current zone within
the N x N grid
- a centred Done button at y=720 that returns the user to the
underlying QRAlert
A "-" / "+" pair in the title row lets the user pick N between 2 and
6; clamp_zone keeps the (zone_r, zone_c) coordinate valid when N
drops, and the mini-map rebuilds to the new grid size.
All button callbacks go through the existing on_release decorator so
each press feeds the touch point into the RNG entropy pool, matching
the rest of the codebase.
Wiring
------
QRAlert (src/gui/screens/qralert.py) replaces the previous "Toggle
transcribe" spacing toggle with a "Transcribe" button. Tapping it
schedules an asyncio task that constructs QRTranscribeScreen with the
underlying payload (via QRCode.get_text, which returns the original
message rather than the current bcur frame), swaps to it, and on
Done restores the QRAlert. transcribe=True is enabled on the SLIP-77
blinding-key export site (apps/blindingkeys/app.py); the main-seed
QRAlert in keystore/ram.py already passed it before this commit so
no change there. The BIP-85 export site is wired in the next commit
together with an encoding-format menu.
When the user exports a BIP-85 derived mnemonic as a QR, the device used to render the plaintext words straight away. The main recovery phrase flow in keystore/ram.py has long offered three encoding formats — SeedQR (4-digit-zero-padded word indices), Compact SeedQR (the raw 16/24/32 entropy bytes, displayed as hex on screen and encoded as binary in the QR), and Plaintext (the mnemonic string) — so derived mnemonics now get the same menu before the QR is shown. The encoding choice also passes through to the underlying QRAlert as transcribe=True so the new Transcribe button (introduced in the previous commit) is reachable for any of the three formats. If the user backs out of the encoding menu, the BIP-85 flow returns to its own derivation menu instead of producing an empty QR. The encoding logic is byte-for-byte identical to show_mnemonic in keystore/ram.py so the resulting payloads round-trip the same way as the main seed; a 12-word SeedQR is 48 numeric chars, a 12-word Compact SeedQR is 16 bytes (32 hex chars on screen), etc.
❌ Deploy Preview for specter-diy-docs failed.
|
1 similar comment
❌ Deploy Preview for specter-diy-docs failed.
|
Contributor
|
I think we should keep it exactly like with SeedSigner So no custom n but the blocks SeedSigner Uses. A1, B3 Like on the preset I have. In this turn we should also show the standard SeedQR in a grid like SeedSigner uses. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Adds a Transcribe button to the seed-class QR exports (main mnemonic, BIP-85 derived mnemonic, SLIP-77 blinding key) that opens a full-screen zoomed view of one zone of the QR at a time, so the user can hand-copy the code onto paper. Also adds the SeedQR / Compact SeedQR / Plaintext encoding menu to BIP-85 mnemonic exports, matching what the main recovery-phrase flow already offers.
Why
Seed QRs are dense and copying one onto paper without a magnifier is error-prone. The previous "Toggle transcribe" feature only widened the cell spacing slightly; the user still had to read the whole code from a single 320 px display.
The new screen splits the QR into an adaptive N×N grid (default chosen so each zone holds ~12-18 modules per side) and renders one zone at a time as discrete black-on-white dots with row/column axis labels. The user pans through the grid with a D-pad and tracks position via a mini-map.
For BIP-85 specifically, the QR export used to show only the plaintext mnemonic — the main seed flow already offers SeedQR (digits) and Compact SeedQR (binary) as alternatives. The PR brings the BIP-85 flow to parity.
What changes
Commit 1:
Add QR zoom-to-transcribe screen for hand-copying seed-class QRssrc/qr_transcribe_logic.py(new) — pure logic, no LVGL imports:adaptive_grid_size,zone_bounds,module_at,next_zone,iter_zone_modules,clamp_zone. All six covered by unit tests intest/tests/test_qr_transcribe.py.src/gui/screens/qr_transcribe.py(new) —QRTranscribeScreen: 400×400 zone area rendered as anlv.objwidget grid (1 px gap per cell so adjacent dark modules appear as discrete dots), absolute QR row/col axis labels (1..S), D-pad navigation with edge-clamping, mini-map, N controls (2..6), Done button.src/gui/screens/qralert.py— replaces "Toggle transcribe" with a "Transcribe" button that swaps to the new screen and restores the QRAlert on Done.src/apps/blindingkeys/app.py— passestranscribe=Trueto the SLIP-77 QRAlert.Commit 2:
BIP-85: offer SeedQR / Compact SeedQR / Plaintext on the QR exportsrc/apps/bip85.py— prompts the user to pick an encoding format (same menu and same encoding math askeystore/ram.py:show_mnemonic), then renders the resulting QR withtranscribe=Trueso the new Transcribe button is reachable on any format.Implementation notes
lv.objwidget grid (one widget per QR module) is used instead oflv.canvasbecauselv.canvasis not rendered by the SDL backend in the unix simulator build. The widget approach works on both SDL and the F469 LTDC framebuffer.on_releasedecorator, so every press still feeds touch entropy into the RNG pool.keystore/ram.pyalready passedtranscribe=Truebefore this PR, so no change there.Screenshots
Known limitations / discussion
qrcode.encode()raises on a payload that doesn't fit in v40. Today only seed-class flows passtranscribe=Trueand those always fit, but a guard + user-facing Alert would be a nice safety net._transcribe_loopcallslv.scr_loaddirectly rather than going throughAsyncGUI.show_screen, sogui.scrstays pointed at the underlying QRAlert while the transcribe screen is on top. Harmless for the seed-class flows (no concurrent UI activity) but happy to refactor if maintainers prefer.Built on top of
masterat v1.10.3.