aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 18:36:54 +0200
commit6039571d70b1f6de1ae384e58e11879869be356c (patch)
tree9f389b8aa234224599f5bed5c47d3f9450317d45
parentf466977249d9ac10fe8b75180c30cb90dbcc22ca (diff)
docs: record M7 (settings forms) results; bump to 0.7.0v0.7.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--CHANGELOG.md53
-rw-r--r--README.md25
-rw-r--r--docs/DEVELOPER_GUIDE.md9
-rw-r--r--docs/FEASIBILITY.md5
-rw-r--r--docs/PERFORMANCE.md13
-rw-r--r--docs/SECURITY.md18
-rw-r--r--docs/TESTING.md29
-rw-r--r--source/app/AppInfo.mc2
8 files changed, 139 insertions, 15 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e3882e..7ac63f2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,59 @@ All notable changes to this project are documented here. The format follows
## [Unreleased]
+## [0.7.0] - 2026-10-20
+
+Milestone M7: the phone settings forms. Entries can now be added and changed
+from Garmin Connect on the phone, one form at a time.
+
+### Added
+
+- The full settings form (`resources/settings/`): Add OTP, Add code, Manage
+ entry, Import, Options and a read-only About group, with every property of
+ README 10.2 and its strings.
+- Processing (`source/settings/SettingsJob.mc`) in README 10.3 order: Import,
+ Add OTP, Add code, Manage, then the About fields. Each form is applied only
+ when its switch is on. On success the form is cleared (secret first) and
+ its keyed hash remembered, so the same form sent again does nothing. After a
+ problem only the secret is cleared, so the rest can be corrected. Every form
+ writes a short result into its Result field and the results screen.
+- Add OTP (`OtpForm.mc`): TOTP, HOTP and Steam, validated as README 10.4 says
+ (Base32 errors name the character and position, a warning below 80 bits).
+- Add code (`CodeForm.mc`): all 12 symbologies, validated by the real encoder
+ (`CodeCheck`, a job for 2D codes), with a warning when the code doesn't fit
+ this screen. 2D codes are cached encoded on the way in, so the first
+ opening is instant.
+- Manage entry (`ManageForm.mc`): edit details, move, require or drop the
+ PIN, replace the secret or contents, set a HOTP counter, delete. Entry IDs
+ are case-insensitive.
+- Changes to locked entries wait for the PIN (README 7.9,
+ `ChangeQueue.mc`): they are sealed with the device key under `q`, listed
+ under "Phone changes" in the menu and applied only after the PIN is
+ entered on the watch, or rejected.
+- About fields (`AboutScan.mc`): the entry catalog (IDs and names, at most
+ 800 characters; IDs and types only with "Encrypt names"), the version and
+ the storage use, rewritten only when something changed.
+- "Encrypt names" now works: new entries follow it, and existing ones are
+ re-sealed (locked ones after the next PIN entry).
+- Options are applied as soon as the phone saves them.
+- `tools/check_resources.py` (in `make lint`): every resource file validates
+ against the SDK's `resources.xsd`, and settings, properties and strings
+ agree. The dev image gains `xmlschema` for it.
+- Tests: 34 new Monkey C tests (154 per device), including every symbology
+ valid and invalid, every Manage action, the queue, idempotency and a
+ check that the form defaults match `properties.xml`. The UI flow sends the
+ forms the way the phone does and checks the results: a TOTP with its known
+ code, a Code 128 card that must decode, and a queued change confirmed with
+ the PIN.
+
+### Changed
+
+- Form hashes are keyed (HMAC-SHA256 with a key derived from the device
+ key) instead of plain SHA-256 of the content, because the content includes
+ secrets. Fields are fed to the native HMAC one by one: copying a large
+ import form into one buffer and zeroing it tripped the watchdog on the
+ Forerunner 255S during M7.
+
## [0.6.0] - 2026-10-20
Milestone M6: barcodes. Code entries now show their barcode: all 12
diff --git a/README.md b/README.md
index ffe2f91..87c3a46 100644
--- a/README.md
+++ b/README.md
@@ -6,13 +6,14 @@
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: M6 (barcodes) done; next is M7, the phone settings forms; 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). M6 added the code screen and all 12 symbologies, encoded on the watch:
+> **Status: M7 (phone settings forms) done; next is M8, polish and release; M1 (settings spike) and the M7 phone check wait for you.** 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, M5 import (`otpauth://` and Aegis JSON via the phone settings) and M6 all 12 barcode symbologies, encoded on the watch. M7 added the rest of the phone settings:
>
-> - **Linear codes** are encoded directly.
-> - **QR, Data Matrix, PDF417 and Aztec** are encoded as resumable jobs and cached encrypted.
-> - **Checks:** each is compared with a reference encoder (zxing-cpp, segno, ppf.datamatrix, pdf417gen, aztec_code_generator) and decoded with zxing-cpp. Screenshots of five kinds of code decode on seven simulated watches.
+> - **Add OTP and Add code forms**, validated on the watch (codes by the real encoders), with a short result written back to each form.
+> - **Manage entry** by ID: edit, move, require or drop the PIN, replace the secret or contents, set a HOTP counter, delete. Changes to locked entries wait until you confirm them with the PIN on the watch.
+> - **Options** applied as soon as you save, and a read-only **About** group with the entry IDs.
+> - **Checks:** the resource XML validates against the SDK schema; the UI flow sends the forms the way the phone does and checks the new entries on screenshots.
>
-> 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).
+> The settings spike (`make spike`, [docs/SETTINGS_SPIKE.md](docs/SETTINGS_SPIKE.md)) and the M7 check with the Store beta wait for your phone. 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.
@@ -207,7 +208,7 @@ Design rules:
| Key | Content | Protected by |
| --- | --- | --- |
-| `m` | Metadata: schema version, entry index (the order of IDs), pending IDs, hashes of applied forms (M7) | Not secret |
+| `m` | Metadata: schema version, entry index (the order of IDs), pending IDs | Not secret |
| `dk` | Device key, 32 random bytes | Stored as is ([7.4](#74-what-stays-readable)) |
| `w` | KDF salt, iteration count and the wrapped locked master key | The PIN key |
| `lk` | Wrong-PIN count and delay | Not secret |
@@ -215,7 +216,8 @@ Design rules:
| `x.<id>` | Cached barcode matrix for a code entry | The same key as its entry |
| `p.<id>` | Pending locked entry, waiting for your PIN | Device key ([7.7](#77-pending-locked-entries)) |
| `imp`, `imp.<n>` | Partial import text, kept between syncs, in chunks below 8 KB | Device key |
-| `res`, `fh`, `st` | Latest settings results; hashes of applied forms; date of the last self-test | Not secret |
+| `q` | Phone changes to locked entries, waiting for your PIN ([7.9](#79-integrity-of-changes-coming-from-the-phone)) | Device key |
+| `res`, `fh`, `cs`, `st` | Latest settings results; keyed hashes of applied forms; signature of the last About update; date of the last self-test | Not secret |
Limits: at most 8 KB per value and about 100 KB in total (the stricter of Garmin's two documented figures). M3 measured 239 bytes for a typical OTP record, so the entry cap stays at 150 (byte formats in [docs/SECURITY.md](docs/SECURITY.md)). A full store produces a clear message, never a crash.
@@ -304,7 +306,7 @@ A locked entry can arrive from settings or an import while the watch is locked,
Anyone who controls your phone or Garmin account can change the settings. So:
-- Actions on locked entries (edit, replace secret, unlock, delete) wait in a queue until you confirm them with the PIN on the watch.
+- Actions on locked entries (edit, replace secret, unlock, set the counter, delete) wait in a queue until you confirm them with the PIN on the watch, even if the watch happens to be unlocked. The menu lists them under "Phone changes", where you confirm (the PIN is always asked) or reject them. "Require PIN" waits too while the watch is locked, because it needs the master key.
- Deleting or editing *unlocked* entries applies directly, and the watch shows what changed.
- New entries always apply, and they're visible in the list.
@@ -423,8 +425,8 @@ Garmin's "array settings" (growable lists) are broken today. A failed save reset
### 10.3 Processing rules
1. **When**: from `onSettingsChanged` while the app runs, and at every app start. Processing runs *after* the first screen is drawn, so it never slows start-up.
-2. **Switches**: a form is processed only when its switch is on. Afterwards the watch turns the switch off and clears the form, secret fields first.
-3. **Idempotency**: Garmin doesn't document whether the phone shows those cleared values ([FEASIBILITY 3e](docs/FEASIBILITY.md#3e-writing-values-back-unknown)). So the watch also remembers a hash of each of the last 32 applied forms and ignores a form it has already applied. Re-sending the same form does nothing; changing anything makes it a new request. Manage actions use stable IDs and absolute positions, so applying one twice can't hit a different entry.
+2. **Switches**: a form is processed only when its switch is on. Afterwards the watch turns the switch off and clears the form, secret fields first. After a problem only the secret field is cleared, so the other fields can be corrected and sent again.
+3. **Idempotency**: Garmin doesn't document whether the phone shows those cleared values ([FEASIBILITY 3e](docs/FEASIBILITY.md#3e-writing-values-back-unknown)). So the watch also remembers a keyed hash of each of the last 32 applied forms and ignores a form it has already applied. Re-sending the same form does nothing; changing anything makes it a new request. Manage actions use stable IDs and absolute positions, so applying one twice can't hit a different entry.
4. **Results** go to the form's status field and to a "Settings results" screen on the watch. That screen opens by itself after processing if the app is open, and is always available from the menu.
5. **Status texts are short and specific**, for example:
- "Added K7F2 GitHub (alice)"
@@ -445,7 +447,7 @@ Code validation, per symbology, is in [section 12.1](#121-symbologies).
### 10.5 XML excerpt
-The full files are written in M7.
+The full files are in `resources/settings/` (written in M7). `make lint` validates them against the SDK's `resources.xsd` and checks that the form, the properties and the strings agree (`tools/check_resources.py`).
```xml
<!-- resources/settings/properties.xml (excerpt) -->
@@ -879,6 +881,7 @@ Done when: each symbology is either marked implemented with every check green, o
- Manage actions on unlocked and locked entries.
- End-to-end: a manually entered entry, set through a debug fixture that takes the same path as the phone, shows the right TOTP code or a barcode that decodes correctly, on screenshots.
- **Done when**: everything passes, the XML validates against the SDK schema, and you've confirmed it on your phone and watch with the beta.
+- **Status (2026-10-20)**: everything passes in Docker and the XML validates; the phone and watch check with the beta is yours ([docs/TESTING.md](docs/TESTING.md#real-watch-checklist)).
### M8: Polish, performance, release
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index 2646e1b..395cf00 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -95,6 +95,9 @@ The simulator is a GTK program. The scripts start it on a virtual display (Xvfb
- `Attention.vibrate` makes the simulator shake the device drawing, which spoils screenshots; the UI-test build doesn't vibrate.
- A `ConfirmationDelegate` closes its dialog after `onResponse`, so a view switch from `onResponse` must wait for a short timer.
- `Attention has :vibrate` inside a class that has a private `vibrate` method warns (the symbol lookup finds the method); the helper is called `buzz`.
+- `has` and `hidden` are reserved words; they can't name a function or a variable.
+- Under strict typing, a property value (`Application.Properties.ValueType`) can't be compared with `==` to a Boolean or Number: narrow it with `instanceof` first (`Forms.flag`, `Forms.number`).
+- Zeroing a ByteArray is a byte loop in the VM: zeroing a 10 KB import text in one event tripped the watchdog on the Forerunner 255S. The native HMAC takes data in pieces (`update`), so large content is hashed without copying it into one buffer.
### Screenshots and the UI flow
@@ -127,7 +130,11 @@ Debug builds print `DIAG first_frame startup_ms=... mem_used=... mem_total=...`
- **A test device**: add it to `TEST_DEVICES` (unit tests, perf and screenshots) or `SPOT_DEVICES` (screenshots only) in the `Makefile`. Then run `make screenshots`, review `build/screenshots/<device>/`, and run `make screenshots-approve`.
- **A screen in the UI flow**: print `Diag.shown("<name>")` after its first complete draw, add the steps to `run_flow` in `tools/ui_flow.py`, and approve the new goldens after reviewing them.
- **Import inputs**: new parser cases go into `test/import/` (fixtures are built in `ImportSupport.mc`, all fake). To widen the fuzz corpus, change the seeds or mutations in `tools/gen_fuzz_corpus.py` and run `make corpus`. The Python tests check that the committed corpus matches the generator.
-- **A settings form** (M7 adds the rest): declare its properties in `resources/settings/properties.xml` (reading an undeclared one crashes), add the form to `resources/settings/settings.xml` with its strings, and handle it in `source/settings/SettingsProcessor.mc`. The form is applied only when its switch is on. The processor clears the fields afterwards and remembers the form's hash.
+- **A settings form or field**:
+ 1. Declare each property in `resources/settings/properties.xml`. Reading an undeclared property crashes the app in a way that can't be caught.
+ 2. Add the setting to `resources/settings/settings.xml` and its strings to `resources/strings/strings.xml`. `make lint` validates both against the SDK schema and checks that they agree (`tools/check_resources.py`).
+ 3. Read the fields through a `Forms.Store` in a reader like `source/settings/OtpForm.mc`, so tests can use `Forms.MemoryStore`. Put every field in the form's hash content and its default in the form's `defaults()`; the test `settingsDefaultsMatchProperties` checks those against `properties.xml`.
+ 4. Add a phase to `SettingsJob` in README 10.3 order. Heavy work (encoding, many records) goes into its own steps: one step is one timer event under the watchdog.
- **On-watch numbers**: sideload a debug build (`make build DEVICE=fr70`, copy `build/debug/fr70.prg` to the watch's `GARMIN/APPS` folder over USB). Then open the menu and choose "Debug: benchmark".
- **A barcode symbology**: add an encoder module under `source/codes/` and dispatch to it in `Codes.encode`. Then:
- Add reference rows in `tools/gen_code_vectors.py` (zxing-cpp's writer, or segno for QR) and run `make codes`.
diff --git a/docs/FEASIBILITY.md b/docs/FEASIBILITY.md
index f586f10..08e9461 100644
--- a/docs/FEASIBILITY.md
+++ b/docs/FEASIBILITY.md
@@ -274,6 +274,11 @@ With SDK 9.2.0 and the device files of all 72 products installed:
- **Properties:** reading a property that isn't declared in `properties.xml` stops the app with an error `try` can't catch. The option properties are declared ahead of M7's form.
- **Touch input:** on watches with touch and buttons, a tap arrives first as the select behaviour. The Venu X1 has no MENU button, and the Venu Sq 2 has no UP/DOWN.
+## Verified during M7 (2026-10-20)
+
+- **Settings schema.** The SDK's `bin/resources.xsd` allows property types `string`, `number`, `long`, `float`, `double`, `boolean` and `array`, and setting types `list`, `boolean`, `numeric`, `alphaNumeric`, `phone`, `email`, `url`, `date`, `password` and `array`, with `readonly`, `min`, `max`, `maxLength` and `errorMessage`. The form uses `long` for HOTP counters and `readonly` for the Result and About fields. Every resource file validates against it (`make lint`).
+- **Still open (3d, 3e):** how the phone shows values the watch writes back (Result fields, cleared forms, the About catalog), and how long a text field may be. The catalog is capped at 800 characters, the Garmin Express limit reported in 3d. Your M1 spike and the M7 beta check answer these.
+
## Decisions
Answered on 2026-10-18:
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
index 0fbb2f0..9e89e40 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
+### M7, 2026-10-20 (settings forms, version 0.7.0)
+
+- **Memory is rising with the code.** The app's code is loaded into the same memory as its data, so every milestone adds to it:
+
+ | Forerunner 255S (512 KB) | M5 | M6 | M7 | Budget |
+ | --- | --- | --- | --- | --- |
+ | First frame (`make memtest`) | | 26 % | 31.4 % | 70 % |
+ | Peak during the 100-entry import (UI flow) | 41.5 % | | 56.2 % | 70 % |
+
+ The 768 KB watches use 21 % at the first frame and about 38 % at the import's peak. The 255S still has room, but M8 should claw some back, for example by keeping rarely used encoders and the settings code out of memory until needed, before adding more.
+- **Settings run.** The forms are applied in timer steps after the first frame, so start-up is unchanged (`startup_ms` 1 to 17 ms). Each form takes one step (a 2D code more, while it is encoded); the About scan reads four headers per step, so 110 entries take 28 steps, at least 1.4 s at 50 ms per step. It runs only when the entry list changed.
+- **Package:** 12 MB for all 72 products (9.5 MB in M6).
+
### M6, 2026-10-20 (barcodes, in 0.6.0)
- **Linear codes** encode in milliseconds while the screen opens; nothing is cached.
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 31723d5..f52a983 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -21,7 +21,9 @@ Everything lives in the app's private `Application.Storage`:
| `x.<id>` | 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.<n>` | dictionary, bytes | Waiting multi-part import: `imp` = `{"n": chunks, "t": saved at}`, `imp.<n>` = 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` |
+| `fh` | array | Keyed hashes of the last 32 applied settings forms (below) | `Forms` |
+| `q` | array | Phone changes to locked entries waiting for the PIN: one SealedBox per change (device key, aad `OTPW-Q-1`), at most 16 | `ChangeQueue` |
+| `cs` | string | Keyed hash of what the About fields were last written from (IDs, pending IDs, "Encrypt names", waiting changes, version); absent while names still need converting | `AboutScan` |
| `st` | array | Version and day of the last passed self-test | `AppState` |
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.
@@ -176,3 +178,17 @@ Kfp = HKDF-SHA256(device key, info = "OTPW fp v1")
- **Secrets in the phone settings.** Text pasted into the Import fields sits in Garmin Connect's settings until the watch processes it. The watch then clears the fields (they hold secrets) and turns "Import now" off. Whether the phone shows the cleared fields is the open question of the M1 spike.
- **Unfinished imports.** A multi-part import waiting for its next part is kept sealed with the device key under `imp`/`imp.<n>`. It is deleted when the import completes, fails, is cancelled or is 24 hours old. A test checks that the secret's text isn't readable in storage (`importerJoinsMultiPartJson`).
- **What the parsers touch.** They never decode icons or notes: long strings are skipped unread, and notes aren't kept. Parsed secrets are wiped once stored or rejected.
+
+## Settings forms
+
+- **Form hashes.** The watch remembers the last 32 applied forms (README 10.3) as keyed hashes, because a form's content includes its secret:
+
+ ```text
+ hash = first 8 bytes of HMAC-SHA256(Kfp, "OTPW-FH-1" || 0x1F || form name || 0x1F || field 1 || 0x1F || ...)
+ ```
+
+ Kfp is the fingerprint key above. Without the device key a stored hash reveals nothing. With it, an attacker can only test guesses of a whole form, as with fingerprints. The fields go to the native HMAC one by one; the short ones (every secret field) are zeroed afterwards. An import form's text is too long to zero byte by byte within the watchdog limit; it stays in memory as the property's string anyway.
+- **Secrets in the forms.** Add OTP's secret, Add code's contents and Manage entry's new secret are cleared first when the watch processes a form, also after a problem (the other fields stay so you can correct them). Messages never quote a secret.
+- **Changes from the phone to locked entries** (README 7.9). Edit, replace, set counter, "Don't require PIN" and delete of a locked entry are not applied when they arrive. Each one is sealed with the device key under `q`, which is no weaker than the settings text it came in. They are applied only after someone enters the PIN on the watch under "Phone changes", which always asks for the PIN, even while the session is unlocked. Moving an entry is applied at once: the order isn't protected, and positions are absolute. Changes to unlocked entries apply at once and are listed on the results screen (`manageQueuesLockedEntryChanges`, `manageQueueIsSealedAndLimited`).
+- **The About catalog** sends entry IDs and names to the phone's settings, where your entries came from in the first place. With "Encrypt names" on it sends only IDs and types.
+- **"Encrypt names"** puts names and accounts into the encrypted part of each record. Turning it on or off re-seals every entry the watch can reach: unlocked ones at once, locked ones the next time the PIN is entered (`settingsAppliesEncryptNames`). The barcode cache is kept.
diff --git a/docs/TESTING.md b/docs/TESTING.md
index d8302af..b77ab5d 100644
--- a/docs/TESTING.md
+++ b/docs/TESTING.md
@@ -12,7 +12,7 @@ The full test plan is in [README section 17](../README.md#17-test-plan). This do
| --- | --- | --- |
| Python tool tests | `make test` (first part) | The tools that judge everything else are correct: test-log parser, screenshot checker, budget checker, product rule, link checker, linter, icon generator. Includes Hypothesis fuzzing of the log parser and integration checks against the real docs and sources |
| Monkey C unit tests | `make test` (second part) | Garmin's Run No Evil tests, run in the headless simulator on each test device; judged from the console output, because `monkeydo` always exits with 1 |
-| Static checks | `make lint` | Project policy (headers, no logging, no network, no secret-like literals), ruff and strict mypy on the tools, the product rule for all 72 products, and strict debug and release builds with zero compiler warnings |
+| Static checks | `make lint` | Project policy (headers, no logging, no network, no secret-like literals), ruff and strict mypy on the tools, the product rule for all 72 products, the resource XML against the SDK's `resources.xsd` with the settings cross-check (`tools/check_resources.py`), and strict debug and release builds with zero compiler warnings |
| Screenshots | `make screenshots` | Each test device's screen matches its approved image in `test/golden/` (at most 0.01 % of pixels may differ by more than 24 levels, less than one changed character) |
| Performance and memory | `make perf`, `make memtest` | First-frame time and memory use within `tools/budgets.json` ([PERFORMANCE.md](PERFORMANCE.md)) |
| Packaging | `make package` | The signed `.iq` builds for all 72 products with zero warnings |
@@ -26,6 +26,25 @@ Test devices (`TEST_DEVICES`): Forerunner 255S (weakest: 218 px round MIP, butto
## Results
+### M7, 2026-10-20 (phone settings forms, version 0.7.0)
+
+Monkey C unit tests: 154 per device (34 new, in `test/settings/`), all passing on the four test devices.
+
+- **Add OTP:** every type (TOTP, HOTP with its counter, Steam ignoring algorithm, digits and period), and each validation message of README 10.4: Base32 character and position, `=` padding, missing secret, digits, period, algorithm, type, counter, control characters, length in bytes, a missing name, the warning below 80 bits. After a problem only the secret is cleared; after success the whole form, with the switch off.
+- **Add code:** a valid payload for each of the 12 symbologies (2D codes come out cached and decodable) and an invalid one for each, with the encoder's message (check digit, length, character and position, capacity). Options and "Fix if wrong" are stored as sent, and codes too large for the screen are added with the README 12.2 warning.
+- **Manage entry:** every action on unlocked entries; locked entries' changes queued even while unlocked, then applied by confirmation; "Require PIN" applied at once or queued; unknown, malformed, pending and damaged IDs; a queued change whose entry vanished; changes arriving while the queue is applied; tampered queue items dropped; the 16-item limit.
+- **Idempotency:** the same Add OTP, Manage and Import form twice is applied once; any change makes a new request.
+- **The whole run:** all forms at once in README order; the About catalog, its 800-character cut and the storage line; "Encrypt names" on and off, with locked entries converted after the PIN; work detected only when needed.
+- **The contract with `properties.xml`:** every key the forms read exists and every form default equals its property's default (`settingsDefaultsMatchProperties`). The phone's side, all 13 resource files against the SDK's `resources.xsd` plus the settings, property and string cross-check, runs in `make lint` (`tools/check_resources.py`, 4 Python tests of its own).
+
+Scripted UI flow (end-to-end): the UI-test build fills in the Add OTP, Add code and Manage entry forms next to the import, as the phone would, so they take the real path: properties, `onSettingsChanged`-style processing after the first frame, the timer-driven job and the results screen. 20 screens per watch (17 on the touch-only ones), on seven watches:
+
+- `otp_form`: the TOTP added through the form shows 309 848 and next 489 290, the RFC 6238 values for its fake key at the frozen time, computed in Python.
+- `code_form`: the Code 128 card added through the form decodes from the screenshot on every watch.
+- `queue`, `queue_results` (watches with buttons): the Manage form's edit of the locked Bank entry waits under "Phone changes"; "Confirm all" asks for the PIN and then applies it.
+
+The flow caught one watchdog trip on the Forerunner 255S that the unit tests can't: hashing the 100-entry import form by copying it into one buffer and zeroing it. The fields now go to the native HMAC one by one.
+
### M6d, 2026-10-20 (Aztec; M6 complete, version 0.6.0)
Monkey C unit tests: 120 per device.
@@ -197,3 +216,11 @@ Things the simulator can't verify, done by you on the Forerunner 70. The checkli
- [ ] 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).
- [ ] M6c: scan the "Parcel" (Data Matrix) and "Ticket" (PDF417) demo cards with a phone app that reads them, for example the zxing-based "Barcode Scanner".
- [ ] M6d: scan the "Rail pass" (Aztec) demo card with a phone app that reads Aztec.
+- [ ] M7, with the Store beta, using a test account (never a real one):
+ - **Add OTP:** fill in the form with the account's Base32 secret, turn on "Add this entry" and save. The watch shows the results; the code must match the one in your phone's authenticator app. Check the form's Result field and whether the form is emptied on the phone.
+ - **Add code:** add a test EAN-13 (for example `5901234123457`) and a short QR text; scan both from the watch.
+ - **Manage entry:** with the IDs from the About group on the phone (or "Entry IDs" on the watch), rename one entry, move it to position 1 and delete it.
+ - **Locked entries:** set "Require PIN" on an entry, then send "Don't require PIN" for it. The watch must not apply it until you open the menu, choose "Phone changes", "Confirm all" and enter the PIN.
+ - **Repeats:** send the same Add OTP form twice (turn the switch on again without changing anything). The second time the result is "Already added".
+ - **Options:** change Theme and Code size; the watch changes as soon as the phone saves.
+ - **Limits:** does the About catalog (up to 800 characters) show completely on the phone?
diff --git a/source/app/AppInfo.mc b/source/app/AppInfo.mc
index 497414e..21411dd 100644
--- a/source/app/AppInfo.mc
+++ b/source/app/AppInfo.mc
@@ -12,5 +12,5 @@ import Toybox.Lang;
module AppInfo {
// Semantic version of the app: MAJOR.MINOR.PATCH.
- const VERSION = "0.6.0";
+ const VERSION = "0.7.0";
}