From 93d0becabf76aaa568989f295ca9b8a2c387f0cc Mon Sep 17 00:00:00 2001 From: cirakg Date: Tue, 20 Oct 2026 06:56:58 +0200 Subject: docs: record M6b (QR) results Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 ++++++++ README.md | 6 ++++-- docs/PERFORMANCE.md | 13 +++++++++++++ docs/SECURITY.md | 3 +-- docs/TESTING.md | 20 ++++++++++++++++++++ 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b4b4f..e1d8912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,14 @@ Milestone M6 in progress (released as 0.6.0 when all four parts are done). Barcodes from the unit tests are decoded with zxing-cpp in every test run (`tools/check_codes.py`), and the UI flow decodes a screenshot of the code screen. +- M6b, QR Code (`source/codes/Qr.mc`, `ReedSolomon.mc`, `QrTables.mc`): + - Versions 1-40 and levels L-H, with optimal numeric, alphanumeric and byte + segmentation and all eight masks scored as segno does. + - Encoded as a watchdog-safe job with progress, and cached sealed with the + entry's key (`x.`). + - Drawn in the largest centred square at 2 px per module or more. + - Checked against segno (`tools/gen_qr_tables.py`, `make codes`), decoded + with zxing-cpp, and decoded from a screenshot of a version 6 card. ## [0.5.0] - 2026-10-20 diff --git a/README.md b/README.md index a9ec6af..9655d4f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Garmin Connect IQ watch app that shows one-time passwords (TOTP, HOTP, and optional Steam Guard codes) and stores barcodes and QR codes (loyalty cards, tickets) to show at a scanner. Everything lives encrypted on the watch. There is no cloud, no backend and no analytics, and the app requests no permissions, so it cannot use the network. -> **Status: M6a (linear barcodes) done; M6b-M6d (QR, Data Matrix, PDF417, Aztec) next; M1 (settings spike) ready for your phone test.** The plan below was approved on 2026-10-18. M0 built the Docker toolchain and pipeline, M2 the crypto and OTP core, M3 the encrypted vault ([docs/SECURITY.md](docs/SECURITY.md)), M4 the screens and M5 import (`otpauth://` and Aegis JSON via the phone settings). M6a added the code screen and the EAN-13, EAN-8, UPC-A, UPC-E, Code 128, Code 39, ITF and Codabar encoders. They're checked against zxing-cpp reference rows and decoded at 1 to 3 px per module, and a screenshot of the code screen decodes on seven simulated watches. The settings spike (`make spike`, [docs/SETTINGS_SPIKE.md](docs/SETTINGS_SPIKE.md)) waits for your test. The Phase 0 research, with sources, is in [docs/FEASIBILITY.md](docs/FEASIBILITY.md); the daily workflow is in [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md). +> **Status: M6a (linear barcodes) and M6b (QR) done; M6c-M6d (Data Matrix, PDF417, Aztec) next; M1 (settings spike) ready for your phone test.** The plan below was approved on 2026-10-18. M0 built the Docker toolchain and pipeline, M2 the crypto and OTP core, M3 the encrypted vault ([docs/SECURITY.md](docs/SECURITY.md)), M4 the screens and M5 import (`otpauth://` and Aegis JSON via the phone settings). M6a added the code screen and the eight linear symbologies. M6b added QR Code: versions 1 to 40, levels L to H, optimal mode segmentation and all eight masks, encoded on the watch as a resumable job and cached encrypted. QR is checked against segno module for module and decoded with zxing-cpp, and screenshots of both kinds of code decode on seven simulated watches. The settings spike (`make spike`, [docs/SETTINGS_SPIKE.md](docs/SETTINGS_SPIKE.md)) waits for your test. The Phase 0 research, with sources, is in [docs/FEASIBILITY.md](docs/FEASIBILITY.md); the daily workflow is in [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md). "OTP Wallet" is a working title. @@ -587,7 +587,9 @@ Linear: EAN-13 needs 113 modules, which means 1 px per module on a 218 px screen ### 12.4 How each symbology is proven scannable -1. **Reference comparison**: Python reference encoders in Docker (zxing-cpp's writer, segno for QR with forced version and mask, and others) generate expected bar patterns and matrices for fixed parameters. The watch encoder must match them exactly wherever the output is deterministic: the EAN/UPC family, Code 39, ITF, Codabar, QR with forced version and mask, and Data Matrix in ASCII mode. Code 128 is the exception, because it has several equally short encodings. zxing-cpp 3 writes through zint, which breaks those ties differently from the zxing rules the watch uses. For Code 128 the module count must match (it did for 5,000 random strings), and the code must decode (check 2). +1. **Reference comparison**: Python reference encoders in Docker (zxing-cpp's writer, segno for QR with forced version and mask, and others) generate expected bar patterns and matrices for fixed parameters. The watch encoder must match them exactly wherever the output is deterministic: the EAN/UPC family, Code 39, ITF, Codabar, QR with forced version and mask, and Data Matrix in ASCII mode. Code 128 is the exception, because it has several equally short encodings. zxing-cpp 3 writes through zint, which breaks those ties differently from the zxing rules the watch uses. For Code 128 the module count must match (it did for 5,000 random strings), and the code must decode (check 2). For QR there are two deliberate differences from segno: + - **Padding.** segno adds a whole zero byte when the bit stream already ends on a codeword boundary (always the case in byte mode). ISO/IEC 18004 7.4.10 adds no padding there, and the watch follows the standard. The exact references therefore use payloads that segno pads correctly. + - **Segmentation.** segno encodes one mode per symbol, while the watch segments optimally. The exact references use single-mode payloads, and mixed content is covered by decoding. 2. **Decoding**: the simulator tests dump every encoder output. It's rendered at each device's module size and decoded with zxing-cpp, which reads all 12 symbologies. The decoded text must equal the input, for hundreds of seeded random payloads per symbology. 3. **Screenshots**: simulator screenshots of the code screen on Forerunner 255S, Venu Sq 2 and Forerunner 965 must decode with zxing-cpp. 4. **Real world** (manual checklist): your phone's camera, plus any real shop scanner you can try, indoors and outdoors, on MIP and AMOLED. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 9a5bcbf..9c9a31a 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -25,6 +25,19 @@ The targets are in [README section 18](../README.md#18-performance-budgets). Thi ## Results +### M6, 2026-10-20 (barcodes, in 0.6.0) + +- **Linear codes** encode in milliseconds while the screen opens; nothing is cached. +- **QR is encoded as a job** in steps sized for the Forerunner 255S's watchdog: at most 2,500 modules to place, 500 to score as rows, 1,000 as columns, or one Reed-Solomon block. Simulator compute time and steps: + + | QR | Compute | Steps | At 50 ms per step on a watch | + | --- | --- | --- | --- | + | Version 3 (29 x 29) | 81 ms | 28 | at least 1.4 s | + | Version 10 (57 x 57) | 312 ms | 105 | at least 5.3 s | + | Version 26 (121 x 121) | 1.4 s | 407 | at least 20 s | + + Version 10 is over README 18's 3 s target. Most of the time is the pauses between steps, and mask scoring (eight masks, every module) is most of the steps. The cache makes every later opening instant. M8 can cut the steps, for example by scoring masks on packed rows. The demo card (version 6) encodes in 2.8 s in the simulated watches (`make screenshots` prints it). + ### M5, 2026-10-20 (version 0.5.0, import) - **Compute.** An Aegis export with 100 entries (19 KB, compact) takes 332 ms of compute in the unit-test benchmark, spread over 159 job steps. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 45faba6..31723d5 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -18,13 +18,12 @@ Everything lives in the app's private `Application.Storage`: | `lk` | dictionary | Wrong-PIN state: `f` failures, `d` delay in seconds, `t` trusted time of the last attempt; absent after a success | `Lockout` | | `e.` | bytes | One entry record (format below) | `Vault` | | `p.` | bytes | Pending locked entry: a record sealed with the device key | `Vault` | +| `x.` | bytes | Encoded 2D barcode: a SealedBox (aad `OTPW-X-1` + ID) with the entry's own key (device key, or the master key for locked entries) of width, height, quiet zone and the packed modules. Deleted when the entry is updated, removed or erased | `Vault` | | `imp`, `imp.` | dictionary, bytes | Waiting multi-part import: `imp` = `{"n": chunks, "t": saved at}`, `imp.` = chunks of one SealedBox (device key, aad `OTPW-IMP-1`) of the text | `ImportBuffer` | | `res` | dictionary | Latest settings results (`l` lines, `u` unseen) | `Results` | | `fh` | array | Hashes (SHA-256, first 8 bytes) of the last 32 applied settings forms | `SettingsProcessor` | | `st` | array | Version and day of the last passed self-test | `AppState` | -`x.` (barcode cache) arrives with M6. - The PIN wrap and the lockout state have their own keys instead of living in `m` as README 6.2 first planned. Each wrong PIN rewrites the lockout state before the check, and a small separate value makes that write cheap and keeps it from risking the entry index. ## Keys diff --git a/docs/TESTING.md b/docs/TESTING.md index 29cec49..ca72af3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -26,6 +26,25 @@ Test devices (`TEST_DEVICES`): Forerunner 255S (weakest: 218 px round MIP, butto ## Results +### M6b, 2026-10-20 (QR, in 0.6.0) + +Monkey C unit tests: 111 per device. + +1. **Reference comparison with segno** (`make codes`). + - **31 matrices identical module for module.** They cover versions 1 to 40 and levels L, M, Q and H, numeric, alphanumeric and UTF-8 byte data, forced versions and masks, and automatic choices. The watch picks the same version and mask as segno with `boost_error=False`. + - **Two deliberate differences**, listed in README 12.4: + - segno pads a stream that already ends on a codeword boundary with an extra zero byte; the watch follows ISO 18004. + - segno uses a single mode per symbol; the watch segments optimally. +2. **Decoding.** 30 QR codes from the test run are decoded with zxing-cpp at 2 and 3 px per module. They include mixed segmented content, Unicode, emoji, a Wi-Fi string and all four levels. +3. **Screenshots.** The UI flow opens the fixture's QR card (version 6, its digit run in numeric mode), encoded on the watch as a job the first time (2.8 s in the simulated watches). Its screenshot decodes on all seven watches, as does the EAN-13 card's. + +Also tested: + +- **The encrypted matrix cache:** round trip; tampering ignored; cleared by update and remove; a locked entry's cache unreadable while locked. +- **Capacity errors.** + +What the UI flow found: scoring the eight masks tripped the watchdog, first on the Forerunner 70 (2,500 modules per step) and then on the 255S (1,200). Each job step now works on at most 500 modules when scoring rows and 1,000 when scoring columns. + ### M6a, 2026-10-20 (linear barcodes, in 0.6.0) Monkey C unit tests: 105 per device, with the checks of README 12.4. @@ -147,3 +166,4 @@ Things the simulator can't verify, done by you on the Forerunner 70. The checkli - [ ] M4: check that leaving the app and coming back asks for the PIN again for the locked demo entry. Check that the glance in the glance loop shows the number of entries. - [ ] M5: in Garmin Connect, paste a few `otpauth://` links of test accounts (not your real ones) into the Import fields, turn "Import now" on and save. Check the result on the watch and in the Result field. Also check that the text fields are emptied afterwards: this answers part of the M1 question. - [ ] M6a: with "Debug: demo entries", open "Gym card" (an EAN-13). Scan it with your phone's camera, or a shop scanner if you can, indoors and outdoors. Tell me whether it reads. +- [ ] M6b: open "Member QR" (a demo entry) and scan it with your phone's camera. The first opening encodes it; note roughly how long that takes. The second opening should be instant (cache). -- cgit v1.2.3