aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-21 03:55:15 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-21 03:55:15 +0200
commitf439beec17419ccbe0dc7643cc039079f15e04a0 (patch)
tree02863a95ba3d8af263a68586370319e5559b00a8
parent56e0c351bb95cfdf63aa76ab8c5d725a694bad63 (diff)
docs: user guide, known limitations and M8 results; bump to 0.8.0v0.8.0
0.8.0 is the release candidate: 1.0.0 is tagged after the on-watch checklist (docs/TESTING.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--CHANGELOG.md56
-rw-r--r--README.md29
-rw-r--r--docs/DEVELOPER_GUIDE.md17
-rw-r--r--docs/FEASIBILITY.md9
-rw-r--r--docs/PERFORMANCE.md47
-rw-r--r--docs/SECURITY.md26
-rw-r--r--docs/TESTING.md29
-rw-r--r--docs/USER_GUIDE.md194
-rw-r--r--source/app/AppInfo.mc2
9 files changed, 386 insertions, 23 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ac63f2..892d7a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,62 @@ All notable changes to this project are documented here. The format follows
## [Unreleased]
+## [0.8.0] - 2026-10-21
+
+Milestone M8: polish, performance and the release candidate. 1.0.0 follows
+once the on-watch checklist is done (docs/TESTING.md).
+
+### Added
+
+- The favourite code in the glance (opt-in, "Show the favourite code in the
+ glance"): the first favourite TOTP or Steam entry without the PIN, with a
+ countdown bar. The app seals that entry for the glance under `g` after
+ every change (`source/app/GlanceCode.mc`); the glance checks the option
+ itself, so turning it off hides the code at once.
+- Manage entry: "Mark as favourite (glance)" and "Remove favourite".
+- Brightness: full brightness on AMOLED watches and the backlight on MIP
+ watches while a barcode is shown, renewed for up to a minute; a tap starts
+ the minute again.
+- `make watchdog` (in `make ci`): a stress build (`stress.jungle`,
+ `source/util/StressTest.mc`) encodes the largest code of every 2D
+ symbology, packs it, finds its runs and draws it, with two steps per
+ event, on the Forerunner 255S and 70. The last code stays on the screen
+ and must decode.
+- `make memtest` checks the glance too (at most 70 % of its 64 KB).
+- `make products` checks each product's watchdog limit against the job
+ runner's assumption.
+- `docs/USER_GUIDE.md`, with the list of known limitations.
+
+### Changed
+
+- Imports use far less memory: parsed items are kept packed (about 110
+ bytes each instead of 760 as objects), and the pasted text is held once.
+ The peak of a 100-entry import on the Forerunner 255S fell from 56 % to
+ 45 % of its memory.
+- QR mask scoring works on rows and columns packed 16 modules per word
+ (the same penalties as before, checked against the per-module rules on
+ every mask): a version 10 QR takes 40 % less compute.
+- Watches with 768 KB (every target except the Forerunner 255 and 255S) run
+ two job steps per timer tick, which halves import and encoding times.
+- Large 2D codes are drawn as runs (one rectangle per run), from a bitmap
+ painted in steps when there are too many runs for one frame.
+- The barcode cache has a new format with 16-bit sizes; caches from 0.7 are
+ re-encoded once.
+
+### Fixed
+
+- Large 2D codes could stop the app with "Watchdog Tripped" on every watch:
+ the QR segmentation, Reed-Solomon blocks, function patterns and the final
+ mask; Data Matrix encodation, placement and drawing; Aztec bit encoding,
+ stuffing, the GF(4096) tables and generator; PDF417 compaction; the cache
+ packing; and drawing a code module by module. Every step is now bounded
+ and measured (`make watchdog`). An entry like that added from the phone
+ would have crashed the app during settings processing, at every start.
+- A PDF417 code wider than 255 modules (big screens) was cached with a
+ wrong width.
+- The Data Matrix placement grid used up to 90 KB; it now uses a byte
+ array (about 35 KB for the largest symbol).
+
## [0.7.0] - 2026-10-20
Milestone M7: the phone settings forms. Entries can now be added and changed
diff --git a/README.md b/README.md
index 87c3a46..1045ec6 100644
--- a/README.md
+++ b/README.md
@@ -6,14 +6,15 @@
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: 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:
+> **Status: M8 (polish, performance, release candidate) done in Docker as version 0.8.0; 1.0.0 is tagged after your on-watch checklist.** 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), M6 all 12 barcode symbologies and M7 the phone settings forms. M8 added:
>
-> - **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 favourite code in the glance** (opt-in), and favourite marks in Manage entry.
+> - **Brightness** for barcodes: full brightness on AMOLED watches, the backlight on MIP watches, for about a minute.
+> - **Memory and speed:** a 100-entry import peaks at 45 % of the Forerunner 255S's memory instead of 56 %; QR mask scoring on packed rows; two job steps per tick on the 768 KB watches.
+> - **Watchdog safety:** every 2D encoder now handles the largest inputs the app accepts without being stopped by the watchdog, proven by `make watchdog` at twice the normal work per event.
+> - **Docs:** the [user guide](docs/USER_GUIDE.md) with the [known limitations](docs/USER_GUIDE.md#known-limitations).
>
-> 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).
+> Waiting for you: the settings spike (`make spike`, [docs/SETTINGS_SPIKE.md](docs/SETTINGS_SPIKE.md)) and the [real-watch checklist](docs/TESTING.md#real-watch-checklist) with the 0.8.0 beta. 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.
@@ -388,7 +389,7 @@ Garmin's "array settings" (growable lists) are broken today. A failed save reset
| Property | Type | Field | Notes |
| --- | --- | --- | --- |
| `mgId` | string | alphaNumeric | The entry ID shown on the watch, for example `K7F2` (case-insensitive) |
-| `mgAction` | number | list | Edit details, Move to position, Require PIN, Don't require PIN, Replace secret or contents, Set HOTP counter, Delete |
+| `mgAction` | number | list | Edit details, Move to position, Require PIN, Don't require PIN, Replace secret or contents, Set HOTP counter, Delete, Mark as favourite (glance), Remove favourite (M8) |
| `mgName`, `mgAccount`, `mgGroup` | string | alphaNumeric | Blank means unchanged. `-` clears the group |
| `mgPosition` | number | numeric (≥ 1) | |
| `mgSecret` | string | password | New secret (Base32) or new code contents |
@@ -620,7 +621,7 @@ A symbology counts as implemented only when checks 1 to 3 pass. Otherwise it's l
- **PIN**: a keypad on touch screens; a digit selector on button-only watches. Digits show as dots. A wrong PIN vibrates (where available) and shows any waiting time.
- **Create or change PIN**: enter it twice. Warns below 6 digits.
- **Menu**: Lock now, Change PIN, Reset PIN (erases locked entries), Settings results, Entry IDs, Clock status, About (version, storage use, self-test result).
-- **Glance**: "OTP Wallet · 12 entries". If you turn it on, it shows the favourite unlocked TOTP code with a progress bar instead.
+- **Glance**: "OTP Wallet · 12 entries". If you turn it on, it shows the code of the first favourite TOTP or Steam entry without the PIN, with a progress bar, instead ([docs/SECURITY.md](docs/SECURITY.md#glance-code)).
### 13.2 Flow
@@ -741,11 +742,12 @@ The Garmin SDK and device files are never baked into an image (Garmin's licence)
| `make screenshots-approve` | Save the current screens as the approved images, after you've reviewed them | no |
| `make icons` | Regenerate the per-size launcher icons after the product list changes | no |
| `make perf` | Performance measurements against the budgets | no |
-| `make memtest` | Peak memory on Forerunner 255S, app and glance | no |
+| `make memtest` | Memory after the first frame and in the glance, on `TEST_DEVICES` | no |
+| `make watchdog` | The largest code of every 2D symbology encoded, packed and drawn at twice the steps per event, on `WATCHDOG_DEVICES` (Forerunner 255S and 70); fails on a watchdog trip or if the last code doesn't decode | no |
| `make lint` | Strict type check (`-l 3`) with warnings as errors, project lint rules, settings XML schema check, Python lint for the tools | no |
| `make package` | Signed `.iq` for the Store beta, plus per-device `.prg` files | no |
| `make docs` | Markdown lint, link check, Mermaid render check | no (yes when building the docs image) |
-| `make ci` | lint, test, screenshots, perf, memtest, package, docs | no |
+| `make ci` | lint, test, screenshots, perf, memtest, watchdog, package, docs | no |
| `make products` | Check the manifest's product list against the downloaded device files | no |
| `make lock` | Regenerate the Python and npm lock files (only releases older than the cutoff date) | yes |
| `make vectors` | Regenerate the cross-checked crypto and OTP test vectors | no |
@@ -890,6 +892,7 @@ Done when: each symbology is either marked implemented with every check green, o
- Final `docs/` and the manual test checklist.
- `make ci` green; signed `.iq` and per-device `.prg` files; `CHANGELOG` 1.0.0; the final list of known limitations.
- **Done when**: `make ci` is green from a clean checkout, you've completed the on-watch checklist, and 1.0.0 is tagged.
+- **Status (2026-10-21)**: done in Docker as 0.8.0, the release candidate: `make ci` is green, also from a clean checkout; the signed `.iq` and the release `.prg` files are built by `make package`; the known limitations are in [docs/USER_GUIDE.md](docs/USER_GUIDE.md#known-limitations). Your on-watch checklist ([docs/TESTING.md](docs/TESTING.md#real-watch-checklist)) comes before the 1.0.0 tag.
## 17. Test plan
@@ -948,7 +951,7 @@ Tests are written alongside the code, never after.
### 17.6 Performance and memory
-See [section 18](#18-performance-budgets). `make perf` and `make memtest` fail the build when a budget is exceeded.
+See [section 18](#18-performance-budgets). `make perf` and `make memtest` fail the build when a budget is exceeded. `make watchdog` (M8) runs the largest input of every 2D encoder with twice the normal steps per event, so each step is proven to use at most half the watchdog budget, and draws the largest codes each screen fits.
### 17.7 Static checks
@@ -963,7 +966,7 @@ See [section 18](#18-performance-budgets). `make perf` and `make memtest` fail t
### 17.8 `make ci`
-Runs lint, test, screenshots, perf, memtest, package and docs in that order, and stops at the first failure.
+Runs lint, test, screenshots, perf, memtest, watchdog, package and docs in that order, and stops at the first failure.
### 17.9 Manual checklist on the real watch
@@ -1038,7 +1041,7 @@ The simulator runs on your PC's CPU, so its timings don't represent a watch. In
├── CHANGELOG.md
├── Makefile host entry point; every target runs in Docker
├── manifest.xml
-├── monkey.jungle debug, release and test configurations
+├── monkey.jungle debug, release and test configurations (+ test, uitest, stress .jungle)
├── docker/
│ ├── Dockerfile garmin-otp/dev
│ ├── docs.Dockerfile garmin-otp/docs
diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md
index 395cf00..77d2f02 100644
--- a/docs/DEVELOPER_GUIDE.md
+++ b/docs/DEVELOPER_GUIDE.md
@@ -23,7 +23,8 @@ Everything runs in Docker through the `Makefile`; the host needs only Docker and
| All static checks | `make lint` |
| Screenshots against the approved images | `make screenshots` |
| Accept changed screens after reviewing build/screenshots/ | `make screenshots-approve` |
-| Start-up time and memory budgets | `make perf`, `make memtest` |
+| Start-up time and memory budgets (app and glance) | `make perf`, `make memtest` |
+| Largest 2D codes at twice the steps per event (watchdog) | `make watchdog` |
| Signed package for the Store beta | `make package` |
| Regenerate the crypto/OTP test vectors | `make vectors` |
| Build the M1 settings spike | `make spike` (instructions: [SETTINGS_SPIKE.md](SETTINGS_SPIKE.md)) |
@@ -41,7 +42,7 @@ Use other devices with `TEST_DEVICES="fr70 venu3"` on any target that takes a de
| `test/golden/` | Approved simulator screenshots: one folder per device, one image per screen of the UI flow |
| `resources/` | Strings, the default launcher icon, the option properties and, from M7, the settings form |
| `resources-icons/`, `icons.jungle` | Launcher icons at each size the products need, generated by `make icons` |
-| `manifest.xml`, `monkey.jungle`, `test.jungle`, `uitest.jungle` | App manifest and build configuration |
+| `manifest.xml`, `monkey.jungle`, `test.jungle`, `uitest.jungle`, `stress.jungle` | App manifest and build configuration |
| `docker/` | Images (`Dockerfile`, `docs.Dockerfile`), hash-locked dependencies, and the container scripts |
| `tools/` | Python tools (test-log judge, screenshot checker, linters, icon generator) and their tests |
| `docs/` | This guide and the other documents |
@@ -55,6 +56,7 @@ Use other devices with `TEST_DEVICES="fr70 venu3"` on any target that takes a de
- `debug`: what `make build`, `make sim` and `make perf` use. It contains the `(:debug)` diagnostics, the debug menu items (demo entries, performance overlay, on-watch benchmark) and the fixtures.
- `uitest`: a debug build plus `uitest.jungle`, for `make screenshots`. It keeps the `(:uitest)` half of `source/util/TestMode.mc`, so the app starts with a wiped store, fake entries, the PIN 123456, a frozen clock and a 10 s auto-lock.
+- `stress`: a debug build plus `stress.jungle`, for `make watchdog`. It keeps the `(:stress)` half of `source/util/StressTest.mc`: the app starts straight into the watchdog stress test instead of the list.
- `release`: `monkeyc -r` removes every `(:debug)` declaration; used by `make package`.
- `test`: adds `test.jungle` and `--unit-test`.
@@ -84,6 +86,9 @@ The simulator is a GTK program. The scripts start it on a virtual display (Xvfb
- Anything long runs as a `Jobs.Job` with a bounded step (`Kdf.ITERATIONS_PER_STEP` = 100).
- Screens compute at most one SHA-512 code per event, in a timer event separate from drawing.
- The scripted UI flow runs on the 255S profile, so a regression trips there.
+ - **M8 found that step sizes guessed from "bytecodes per loop" were 2 to 3 times too large**: a simple loop iteration costs 50 to 70 counted bytecodes, not 10 to 20. Every step of the 2D encoders, the cache packer and the run finder is now sized by measurement in `make watchdog`, which runs the largest input of each with *two* steps per event: a pass means every step uses at most half the budget. Do the same for any new job: add its worst input to `StressTest.start()`.
+ - `Jobs.JobRunner` runs one step per 50 ms tick on the 512 KB watches and two on the others (`Jobs.stepsPerTick`, by app memory). `tools/gen_products.py --check` (part of `make lint`) fails if a product's `watchdogCount` doesn't match that rule.
+ - Drawing counts too: one `fillRectangle` per run in `onUpdate` is fine up to about 1,500 runs per 120,000 bytecodes. `RunsPainter` paints larger codes into a buffered bitmap in steps first (the Venu X1 fits a QR with 8,000 runs).
- `StringUtil.convertEncodedString` stops the app with an uncatchable "System Error" on invalid UTF-8; `try` doesn't help. Decode untrusted bytes with `Bytes.toUtf8`, which validates first.
- `Application.Storage` stores ByteArrays (its `ValueType` includes them, unlike `Application.PropertyValueType`), so records are stored as single ByteArrays of known size. Dictionaries passed to it must be typed `Dictionary<Application.Storage.KeyType, Application.Storage.ValueType>`; `Dictionary<String, ...>` doesn't type-check.
- `Array.slice` returns `Array<Object or Null>`; cast it back (`as Array<String>`).
@@ -97,6 +102,10 @@ The simulator is a GTK program. The scripts start it on a virtual display (Xvfb
- `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`).
+- **Memory of objects** (measured on the Forerunner 255S): an `Entry` takes about 490 bytes even when empty, an import item with its entry about 760, and an 80-byte ByteArray about 110. Hold many small records packed in ByteArrays (`ImportItem.pack`) rather than as objects, and grids in ByteArrays rather than `Array<Number>` (a 132 x 132 grid of Numbers took about 90 KB).
+- The app's code and constant data are loaded whole at start: about 156 KB of the 255S's 512 KB before anything runs (release and debug builds alike). Only resources (`WatchUi.loadResource`) load on demand.
+- `module` is a reserved word too.
+- **The glance** loads every `(:glance)` module; the crypto it needs for the favourite code (`Hkdf`, `Hmac`, `Hashes`, `Sha512`, `Aes`, `SealedBox`, `Otp`, `Bytes`, `Clock`, `TestMode`) took it from 19 % to 37 % of its 64 KB. A glance module can't name app-only classes (the compiler checks the glance scope), hence `GlanceCode` (glance) and `GlanceStore` (app) in one file. In the simulator, an app that exits does not return to its glance; a new launch starts there (`screenshots.sh` launches again for `glance_code.png`).
- 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
@@ -112,7 +121,7 @@ Because the entry IDs, the clock and the codes are fixed in the UI-test build, e
### Performance and memory
-Debug builds print `DIAG first_frame startup_ms=... mem_used=... mem_total=...` after their first frame (`source/util/Diag.mc`). `docker/scripts/app-diag.sh` adds a `HARNESS first_frame wall_ms=...` line, the wall-clock time from launching `monkeydo` until that first frame. `tools/check_budgets.py` compares both with `tools/budgets.json`. See [PERFORMANCE.md](PERFORMANCE.md) for the numbers and their limits.
+Debug builds print `DIAG first_frame startup_ms=... mem_used=... mem_total=...` after their first frame, and the glance `DIAG glance mem_used=... mem_total=...` (`source/util/Diag.mc`). `docker/scripts/app-diag.sh` adds a `HARNESS first_frame wall_ms=...` line, the wall-clock time from launching `monkeydo` until that first frame. `tools/check_budgets.py` compares both with `tools/budgets.json`. See [PERFORMANCE.md](PERFORMANCE.md) for the numbers and their limits.
## Conventions
@@ -126,7 +135,7 @@ Debug builds print `DIAG first_frame startup_ms=... mem_used=... mem_total=...`
## Adding things
-- **A device**: add `<iq:product id="..."/>` to `manifest.xml`, then `make sdk` (downloads its files), `make icons` and `make products` (checks it meets the target rule: a watch, API 5.0 or newer, at least 512 KB).
+- **A device**: add `<iq:product id="..."/>` to `manifest.xml`, then `make sdk` (downloads its files), `make icons` and `make products` (checks it meets the target rule: a watch, API 5.0 or newer, at least 512 KB, and the watchdog the job runner assumes for its memory).
- **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.
diff --git a/docs/FEASIBILITY.md b/docs/FEASIBILITY.md
index 08e9461..920a65c 100644
--- a/docs/FEASIBILITY.md
+++ b/docs/FEASIBILITY.md
@@ -279,6 +279,15 @@ With SDK 9.2.0 and the device files of all 72 products installed:
- **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.
+## Verified during M8 (2026-10-21)
+
+- **Watchdog limits come from the device files** (`simulator.json`, `watchdogCount`): 120,000 bytecodes per event on the Forerunner 255 and 255S, the only 512 KB targets, and 240,000 on the other 70. The job runner relies on that pairing, and `make products` checks it.
+- **Counted bytecodes are far more than the source suggests**: simple loop iterations cost 50 to 70. The M6 step sizes had only been exercised with small codes. With large ones, every 2D encoder could trip the watchdog. `make watchdog` now proves each step stays below half the limit.
+- **Glances can load the crypto they need**: the opt-in favourite code (AES, HMAC, HKDF, SHA-512, TOTP) runs in the glance at about 37 % of its 64 KB. In the simulator the glance's 1 s timer runs and the glance redraws while it is shown; how often a real watch redraws a visible glance is on the real-watch checklist.
+- **Buffered bitmaps** with a two-colour palette (API 4.0+) work in the simulator on the Forerunner 255S, 70 and 965 and the Venu X1 (`make watchdog` decodes one), so very large codes are painted in steps and drawn as one bitmap.
+- **The simulator doesn't return to the glance** when an app exits; a new launch starts in the glance.
+- **Code and constant data load whole at start**: about 156 KB of the Forerunner 255S's 512 KB, the same in release and debug builds.
+
## Decisions
Answered on 2026-10-18:
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
index 9e89e40..4c35be6 100644
--- a/docs/PERFORMANCE.md
+++ b/docs/PERFORMANCE.md
@@ -22,9 +22,56 @@ The targets are in [README section 18](../README.md#18-performance-budgets). Thi
| `startup_ms` | 100 ms | M4 measured 1 to 18 ms; the margin absorbs simulator jitter |
| `first_frame_wall_ms` | 36000 ms | Regression guard only: about 2× M4's 16-18 s, which include loading the glance and opening the app from it |
| `mem_used_fraction` | 70 % | Hard limit, leaves room for spikes on the 512 KB watches |
+| `glance_mem_used_fraction` | 70 % | Hard limit on the glance's 64 KB (README 18), measured after its first draw |
+| Watchdog | Each job step at most half the per-event limit | `make watchdog`: the largest inputs at two steps per event (M8) |
## Results
+### M8, 2026-10-21 (polish and release candidate, version 0.8.0)
+
+- **Memory, Forerunner 255S (512 KB):**
+
+ | | M7 | M8 | Budget |
+ | --- | --- | --- | --- |
+ | First frame (`make memtest`) | 31.4 % | 34.4 % | 70 % |
+ | Peak during the 100-entry import (UI flow) | 56.2 % | 45.3 % | 70 % |
+ | Glance, entry count / favourite code | about 19 % (12.5 KB) | 36.7 % / 42 % | 70 % |
+
+ The import peak fell because parsed items are kept packed (an `Entry` object takes about 490 bytes, an item with its entry about 760; packed, about 110) and the pasted text is held once. The first frame rose by the code M8 added. Most of the 156 KB the app occupies before it draws anything is its own code and constant data. The glance now loads the crypto for the favourite code. The 768 KB watches: 22.9 % at the first frame, 30.3 % at the import's peak (37.6 % in M7), 37 % in the glance.
+- **Import:** 401 ms of compute for 100 entries in the unit-test benchmark (332 ms in M5, before packing). In the simulated watches it took 7.65 s on the 255S and 3.8 s on the 768 KB watches, which now run two job steps per tick (7.6 s in M7).
+- **QR compute** (unit-test benchmark, simulator, one step at a time):
+
+ | QR | M6 | M8 |
+ | --- | --- | --- |
+ | Version 3 | 81 ms, 28 steps | 61 ms, 54 steps |
+ | Version 10 | 312 ms, 105 steps | 139 ms, 106 steps |
+ | Version 26 | 1.4 s, 407 steps | 482 ms, 391 steps |
+
+ Mask scoring now works on packed rows and columns (16 modules per word). The step counts didn't fall, because M8 found that M6's steps were too large for big codes (below) and sized every step by measurement. At 50 ms per tick, a version 10 QR needs at least 5.3 s on the 255S and 2.7 s on the 768 KB watches, plus the watch's compute, which only the on-watch benchmark can tell. README 18's 3 s target is met on the 768 KB watches at best; the cache makes every later opening instant.
+- **The demo cards in the simulated watches** (first encoding, `make screenshots`):
+
+ | Code | Forerunner 255S | 768 KB watches |
+ | --- | --- | --- |
+ | QR version 6 | 3.9 s (2.8 s in M6) | 2.0 s |
+ | Data Matrix 18 x 18 | 0.8 s | 0.4 s |
+ | PDF417 | 1.3 s | 0.4 to 0.7 s |
+ | Aztec 23 x 23 | 0.5 s | 0.25 s |
+
+ The 255S is slower than in M6 because its steps are smaller: M6's were sized by estimate and only exercised with these small codes.
+- **Watchdog (`make watchdog`).** M6's step sizes were never tried with large codes. With the largest inputs the app accepts, every 2D encoder tripped the watchdog on the 255S. Going by the measured costs, the biggest steps (QR version 40's single-step mask application, about 31,000 modules) were far over the 768 KB watches' limit too. A code like that, added from the phone, would have stopped the app during settings processing at every start. A loop iteration turned out to cost 50 to 70 counted bytecodes, 2 to 3 times the estimate. Every step is now sized so that two steps fit in one event. Results on the 255S (steps, and peak memory in the stress build, which uses 204 KB before the first code):
+
+ | Input | Symbol | Steps | Peak |
+ | --- | --- | --- | --- |
+ | QR, 2,900 letters, level L | version 40, 177 x 177 | 1,041 | 316 KB (62 %) |
+ | QR, 800 digits, level H | 93 x 93 | 255 | 237 KB |
+ | Data Matrix, 1,500 letters | 144 x 144 | 446 | 276 KB (326 KB before the byte-array grid) |
+ | Aztec, 1,400 letters | 95 x 95 | 254 | 231 KB |
+ | PDF417, the most this screen takes | 120 x 48 | 50 | 222 KB |
+
+ In the real app, which uses about 178 KB at the list, the largest QR would peak at about 58 %. At 50 ms per step it takes about 52 s on the 255S and 26 s on the 768 KB watches, once, and it can't be shown on the 255S's screen anyway.
+- **Drawing.** Codes are drawn as runs of dark modules, one rectangle each. Up to 1,500 runs per watchdog unit are drawn directly in `onUpdate`. Larger codes (the Venu X1 fits a version 39 QR with 8,111 runs) are painted into a buffered bitmap in steps first; `make watchdog` decodes one from the screen.
+- **Start-up:** `startup_ms` 1 to 17 ms and launch to first frame 16 to 18 s, unchanged.
+
### 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:
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index f52a983..0deacc0 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -1,6 +1,6 @@
<!--
Author: cirakg & claude ai
- Summary: How stored data is protected: storage keys, the record and PIN-wrap byte formats, lockout, session, migration and limits.
+ Summary: How stored data is protected: storage keys, the record and PIN-wrap byte formats, lockout, session, migration, the glance code and limits.
-->
# Security and storage format
@@ -18,12 +18,13 @@ 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.<id>` | bytes | One entry record (format below) | `Vault` |
| `p.<id>` | bytes | Pending locked entry: a record sealed with the device key | `Vault` |
-| `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` |
+| `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 the packed matrix, format 2: `2`, width and height (2 bytes each), quiet zone, row height, then the modules 8 per byte. Format 1 (0.6 and 0.7, one byte of width) is no longer read: such a cache is re-encoded once. Deleted when the entry is updated, removed or erased | `Vault`, `Codes.Packed` |
+| `g` | bytes | The glance code (below): a SealedBox (device key, aad `OTPW-GL-1`) of one favourite entry's label, type, algorithm, digits, period and secret. Only while "Show the favourite code in the glance" is on | `GlanceStore` |
| `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 | 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` |
+| `cs` | string | Keyed hash of what the About fields and the glance code were last written from (IDs, pending IDs, "Encrypt names", the glance option, 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.
@@ -189,6 +190,23 @@ Kfp = HKDF-SHA256(device key, info = "OTPW fp v1")
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`).
+- **Changes from the phone to locked entries** (README 7.9). Edit, replace, set counter, "Don't require PIN", the favourite mark 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.
+
+## Glance code
+
+With "Show the favourite code in the glance" on (off by default), the glance in the watch's glance loop shows the current code of one entry without opening the app:
+
+- **Which entry.** The first favourite (from Aegis, or "Mark as favourite" in Manage entry) that is a TOTP or Steam entry and doesn't require the PIN. Locked entries never appear, whatever their mark; HOTP entries don't either (showing a code would need the counter to advance).
+- **Why a copy.** The glance gets 64 KB and can't open the vault, so after every change to the entries or the option the app's About scan seals that one entry for it under `g`:
+
+ ```text
+ g = SealedBox(device keys, aad "OTPW-GL-1",
+ 1 | type | algorithm | digits | period (2 bytes) | label length | label (UTF-8) | secret)
+ ```
+
+ This is the protection the entry itself has (unlocked entries are sealed with the same device key), so the copy adds no new way in. The glance opens it with the same SealedBox code and checks its MAC first (`glanceItemRefusesTamperingAndOtherKeys`).
+- **Turning it off.** The glance reads the option itself, so the code disappears as soon as the phone saves the change; the app deletes `g` at its next start or settings change. Removing the favourite mark, requiring the PIN for the entry, or deleting it also removes or replaces `g` (`glanceScanChoosesTheFirstQualifyingFavourite`).
+- **What it means for you.** Anyone who can see your watch's glance loop sees that one code, exactly as if they opened the app to that entry. Choose an entry for which that's acceptable, or leave the option off.
+- **Memory.** The glance computes the code once per period and redraws the countdown every second; the secret is wiped when the glance is hidden.
diff --git a/docs/TESTING.md b/docs/TESTING.md
index b77ab5d..73f89d5 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
+### M8, 2026-10-21 (polish and release candidate, version 0.8.0)
+
+- **Unit tests:** 163 per device on the Forerunner 255S, Venu Sq 2, Forerunner 70 and 965, all passing. New ones:
+ - **Import:** packed items round-trip every field and wipe the parsed secret (`importItemsPackAndUnpack`).
+ - **QR:** the packed mask scorer gives every mask the same penalty as the per-module rules, for versions from 1 to 21 and every level (`qrPackedScoresMatchReference`). segno's automatic version and mask choice still matches (`qrMatchesSegno`).
+ - **Cache and drawing:** the new cache format holds widths over 255; a 0.7 cache is refused (`codeCachePackedFormat`). The drawn runs are exactly the dark modules (`codeRunsCoverExactlyTheDarkModules`).
+ - **Glance:**
+ - The sealed item gives the entry's codes (TOTP with SHA-256, Steam) and refuses tampering and other keys.
+ - The About scan picks the first favourite TOTP or Steam entry without the PIN, and removes the item when the mark or the option goes.
+ - The favourite actions check the mark and queue for locked entries.
+- **Barcode decoding:** 222 encoder dumps per device decode with zxing-cpp at 3 sizes.
+- **Screenshots:** 21 screens on 7 watches (18 on the touch-only ones) match their goldens, including the new glance with the favourite's code (768 147 for GitHub at the frozen time, the value Python's `hmac` computes). The glance is a second launch, because the simulator doesn't return to it when the app exits.
+- **Watchdog stress (`make watchdog`, new):**
+ - On the Forerunner 255S and 70, the largest code of every 2D symbology is encoded, packed, turned into runs and drawn with two steps per event, and the last code on the screen must decode.
+ - It also passed on the Forerunner 965 and the Venu X1, which have the largest screens. The X1 draws a version 39 QR from a bitmap.
+ - Writing it found watchdog trips in every 2D encoder with large inputs (see PERFORMANCE.md), all fixed.
+- **Budgets:** start-up 1 to 17 ms; memory at the first frame 34.4 % (255S) and 22.9 % (768 KB watches); glance 37 %; the 100-entry import peaks at 45.3 % on the 255S.
+- **Tools:** 75 Python tests, among them the watchdog check of the product list, the glance memory budget and barcode reading from screenshots.
+
### 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.
@@ -205,7 +224,7 @@ Tests never use real secrets or personal data. Fixtures are generated or hand-wr
## Real-watch checklist
-Things the simulator can't verify, done by you on the Forerunner 70. The checklist grows with each milestone; it starts with M1.
+Things the simulator can't verify, done by you on the Forerunner 70, with fake or test accounts only. 1.0.0 is tagged once every box is ticked (or a remaining item is recorded as a known limitation). The items were added milestone by milestone; M8 adds the release checks at the end.
- [ ] M1: install the settings spike from the Store beta and run tests A to H in [SETTINGS_SPIKE.md](SETTINGS_SPIKE.md); record the results there.
- [ ] M4: sideload a debug build (`make build DEVICE=fr70`, then copy `build/debug/fr70.prg` to `GARMIN/APPS` over USB). Open the menu, choose "Debug: benchmark", and send me the numbers. They set the real PBKDF2 iteration count and the SHA-512 cost on your watch.
@@ -224,3 +243,11 @@ Things the simulator can't verify, done by you on the Forerunner 70. The checkli
- **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?
+- [ ] M8, with the 0.8.0 Store beta:
+ - **Update keeps data:** install 0.8.0 over the 0.7 beta (or whichever you have); the entries, the PIN and the order must still be there. Open a 2D code you had opened before: it is re-encoded once (new cache format), then instant again.
+ - **Glance:** mark a test TOTP entry as favourite (Manage entry, "Mark as favourite (glance)"), turn on "Show the favourite code in the glance" and open the app once. The glance must show the same code as the app and count down. Turn the option off on the phone: the glance must go back to "N entries" without opening the app. Lock the entry ("Require PIN"): it must leave the glance.
+ - **Brightness:** open a barcode in a dim room. The screen should go to full brightness and stay bright for about a minute; a tap brightens it again. Turn "Full brightness while showing a barcode" off and check that it no longer does.
+ - **Large code timing:** add a QR with about 300 characters of text (Add code) and note how long the first opening takes ("Encoding…"). On the Forerunner 70 it runs two steps per tick.
+ - **Benchmark:** menu, "Debug: benchmark" on a sideloaded debug build; send me the numbers (they replace the simulator estimates in PERFORMANCE.md).
+ - **Uninstall:** remove the app from the watch, install it again: it must start empty (no entries, no PIN).
+ - **Scanning:** scan the demo cards (Gym card, Member QR, Parcel, Ticket, Rail pass) with your phone, indoors and outdoors; a shop scanner if you can.
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
new file mode 100644
index 0000000..f4fe5f8
--- /dev/null
+++ b/docs/USER_GUIDE.md
@@ -0,0 +1,194 @@
+<!--
+ Author: cirakg & claude ai
+ Summary: How to install and use OTP Wallet: the Store beta, adding and importing entries, the PIN, daily use, the glance, moving watches, troubleshooting and known limitations.
+-->
+# OTP Wallet user guide
+
+OTP Wallet keeps one-time passwords (TOTP, HOTP and Steam Guard) and barcodes (loyalty cards, tickets, QR codes) on your Garmin watch, encrypted. Nothing leaves the watch: there is no account, no cloud and no network access. You add entries from the Garmin Connect app on your phone; the watch is the only place they are stored.
+
+## Contents
+
+- [Install](#install)
+- [Where the settings are](#where-the-settings-are)
+- [Add an authenticator code](#add-an-authenticator-code)
+- [Import from Aegis or otpauth links](#import-from-aegis-or-otpauth-links)
+- [Add a barcode or QR code](#add-a-barcode-or-qr-code)
+- [Change, move or delete an entry](#change-move-or-delete-an-entry)
+- [The PIN and locked entries](#the-pin-and-locked-entries)
+- [Daily use on the watch](#daily-use-on-the-watch)
+- [The glance](#the-glance)
+- [Options](#options)
+- [Moving to a new watch, and backups](#moving-to-a-new-watch-and-backups)
+- [Troubleshooting](#troubleshooting)
+- [Known limitations](#known-limitations)
+
+## Install
+
+Garmin's app settings only work for apps installed from the Connect IQ Store, so OTP Wallet is installed as a private beta that only your Garmin account can see:
+
+1. The developer uploads the signed package (`make package` builds `build/package/otp-wallet-<version>.iq`) on Garmin's developer site as a beta app.
+2. On your phone, open the Connect IQ Store app, find the beta under your apps, and install it on your watch.
+3. Each new version is a new upload; the Store updates the watch, and your entries stay (check this once with the [real-watch checklist](TESTING.md#real-watch-checklist)).
+
+A sideloaded build (copied over USB) runs, but gets no phone settings, so you can't add entries to it. Sideloaded debug builds are for testing with fake demo entries only.
+
+## Where the settings are
+
+In the Garmin Connect app: your watch, then **Connect IQ apps**, then **OTP Wallet**, then **Settings**. The settings are a set of forms. Each form has a switch at the bottom ("Add this entry", "Import now", "Apply"); the watch acts on a form only when its switch is on, and only when it receives the settings:
+
+- If OTP Wallet is open on the watch, it applies the forms within seconds of saving.
+- Otherwise it applies them the next time you open it.
+
+Each form has a **Result** field the watch writes back, and the watch shows the same lines on its **Settings results** screen (also in its menu). After a form is applied the watch turns its switch off and clears it, the secret field first. After a problem only the secret field is cleared, so you can correct the rest and send it again. Sending exactly the same form twice does nothing the second time.
+
+Your phone may or may not show the cleared fields and the Result text before you reopen the settings page; that depends on Garmin Connect (see [SETTINGS_SPIKE.md](SETTINGS_SPIKE.md)).
+
+## Add an authenticator code
+
+Use the **Add OTP** form with the details the website shows when you set up two-factor authentication (choose "can't scan the code?" or "enter the key manually" to see them):
+
+| Field | What to enter |
+| --- | --- |
+| Type | TOTP (almost every site), HOTP (counter based) or Steam |
+| Issuer (service) and Account | For example "GitHub" and your user name. At least one of them |
+| Secret | The key in Base32 (letters A–Z and digits 2–7). Spaces, hyphens and lower case are fine |
+| Algorithm, Digits, Period | Leave SHA1, 6 and 30 unless the site says otherwise |
+| Counter | HOTP only: the starting counter, usually 0 |
+| Group | Optional, shown in the list |
+| Require PIN to display | The code is only shown after you enter the app PIN |
+
+Turn on **Add this entry** and save. The result says, for example, "Added K7F2 GitHub (alice)", or exactly what to fix ("Secret: '1' is not a Base32 character (position 5)"). Check that the watch shows the same code as the website or your phone authenticator before you rely on it.
+
+## Import from Aegis or otpauth links
+
+The **Import** form takes the text of an **unencrypted Aegis export** (Aegis: Settings, Import & Export, Export, plain JSON) or any number of `otpauth://` links:
+
+1. Paste the text into **Text, part 1**. If it is longer than one field takes, continue in the next part; the watch joins parts 1 to 8 in order.
+2. Choose the mode: **Add to my entries** (default), or **Replace imported entries** (removes entries that came from earlier imports first; entries you added by hand are never touched).
+3. Turn on **Require PIN for these entries** if they should all be locked.
+4. Turn on **Import now** and save.
+
+The report lists how many entries were imported and every one that wasn't, with the reason: duplicates of entries you already have, unsupported types (mOTP, Yandex), or bad values. Aegis favourites stay favourites (see [the glance](#the-glance)); icons and notes are not imported.
+
+**Long exports.** If the text is still incomplete after joining the fields, the watch keeps what it got (encrypted) and says "Waiting for more: received ... characters". Paste the next part into the fields, turn **Import now** on again, and save. The waiting part expires after 24 hours and is limited to 32 KB; choose the mode **Cancel waiting import** to discard it. A compact export takes about 190 characters per entry; remove icons from the export if it is much larger.
+
+**Afterwards**, delete the exported JSON file from your phone, and keep such files out of cloud-synced folders. The import fields are cleared on the phone by the watch.
+
+**iPhone:** if Garmin Connect refuses the `&` in a link, write `;` instead; the watch accepts both.
+
+Google Authenticator's export links (`otpauth-migration://`) are not supported. Import them into Aegis first and export from there.
+
+## Add a barcode or QR code
+
+Use the **Add code** form:
+
+| Field | What to enter |
+| --- | --- |
+| Card name | For example "Gym card" |
+| Barcode type | QR, Code 128, Code 39, EAN-13, EAN-8, UPC-A, UPC-E, ITF, Codabar, Data Matrix, PDF417 or Aztec |
+| Contents | The text or digits the code holds (as printed under the code, or what a scanner app reads from it) |
+| Error correction | 2D codes: Medium is right for most uses |
+| Check digit | EAN, UPC and ITF: "Add or verify" checks the last digit; "Fix if wrong" corrects it |
+| Show the text under the bars | The digits under linear codes |
+| Group, Require PIN to display | As for OTP entries |
+
+The watch encodes the code to check it, and warns if it won't fit your screen at a scannable size. 2D codes are drawn the first time you open them ("Encoding… 40%"); after that they open at once.
+
+## Change, move or delete an entry
+
+Use the **Manage entry** form with the entry's **ID**: four characters such as `K7F2`, shown at the bottom of each entry on the watch, under **Entry IDs** in the watch menu, and in the **About** group of the settings (the catalog). Case doesn't matter.
+
+| Action | Fields used |
+| --- | --- |
+| Edit details | New name, account or group; blank keeps a field, `-` clears it |
+| Move to position | The new position, 1 = top |
+| Require PIN / Don't require PIN | — |
+| Replace secret or contents | The new secret (Base32) or code contents |
+| Set HOTP counter | The counter |
+| Delete | — |
+| Mark as favourite (glance) / Remove favourite | — |
+
+**Changes to locked entries wait for you.** Anything that would change or reveal a locked entry (everything except moving it) is not applied when it arrives. The watch shows "Phone changes: N to confirm" at the top of its menu and in the list title. Open it, check the list, and choose **Confirm all** (it asks for the PIN, even if the app is unlocked) or **Reject all**. This way, someone with access to your phone alone can't unlock or rewrite your locked entries.
+
+## The PIN and locked entries
+
+- **Set PIN** in the watch menu creates it (4 to 10 digits; the watch warns below 6). You are also asked for one the first time an entry needs it.
+- Entering the PIN unlocks all locked entries until the app locks again: when you leave the app, after the auto-lock time (option), or with **Lock now** in the menu.
+- **Wrong PINs:** after 5 wrong PINs the watch makes you wait, longer with each further mistake. With the option **Erase locked entries after 10 wrong PINs**, the tenth wrong PIN erases the locked entries (unlocked entries stay).
+- **Change PIN** keeps your entries. **Reset PIN**, for a forgotten PIN, erases the locked entries and lets you set a new PIN; there is no other way back into them.
+- Locked entries are encrypted with a key that only your PIN opens. A short PIN can still be guessed by someone who copies the watch's storage, so choose 6 or more digits for anything important.
+
+## Daily use on the watch
+
+- **List:** your entries in your order. Unlocked TOTP and HOTP entries show the current code and a small countdown; locked entries show a lock; barcodes show an icon.
+- **Open an entry** (tap it, or START): the code in large digits with a countdown ring; in the last 5 seconds the next code is shown too. For HOTP, tap the code (or START) for the next one.
+- **Barcodes** open black on white, at the largest size that fits. They are shown at full brightness for about a minute on AMOLED watches (the screen's own limit; tap to brighten it again), and with the backlight on MIP watches. You can turn this off under Options.
+- **Menu** (hold MENU, or long-press the list on touch-only watches): Phone changes, Lock now, Set/Change/Reset PIN, Settings results, Entry IDs, Clock status, About (version, storage, self-test result).
+- **BACK** on the list leaves the app and locks everything.
+
+## The glance
+
+In the watch's glance loop, OTP Wallet shows the number of entries. With the option **Show the favourite code in the glance** on, it shows instead the current code of your first **favourite** TOTP or Steam entry that doesn't require the PIN, with a bar for the seconds left:
+
+- Mark an entry as favourite with Manage entry, "Mark as favourite (glance)" (Aegis favourites already are).
+- Locked entries and HOTP entries never appear in the glance.
+- The glance updates the code once you've opened the app after changing your favourites; turning the option off hides the code at once.
+- Anyone who can see your glance loop sees that code, so choose an entry for which that's acceptable ([SECURITY.md](SECURITY.md#glance-code)).
+
+## Options
+
+| Option | Default | Meaning |
+| --- | --- | --- |
+| Lock after | 1 min | How long locked entries stay open without input: immediately, 15 s, 30 s, 1, 2 or 5 minutes |
+| Erase locked entries after 10 wrong PINs | Off | See [the PIN](#the-pin-and-locked-entries) |
+| Time offset in seconds | 0 | Seconds added to the watch clock if your codes are consistently early or late (−120 to 120) |
+| Code size | Large | Size of the digits on the detail screen |
+| Theme | Dark | Dark or light screens (barcodes are always black on white) |
+| Show groups | On | The group in front of the account in the list |
+| Encrypt names on the watch | Off | Keeps names and accounts in the encrypted part too; locked entries then show only "Locked entry" and the About catalog only IDs |
+| Show the favourite code in the glance | Off | See [the glance](#the-glance) |
+| Full brightness while showing a barcode | On | See [daily use](#daily-use-on-the-watch) |
+
+## Moving to a new watch, and backups
+
+OTP Wallet never exports your secrets: they only flow into the watch. Your backup is your authenticator app or Aegis vault (and the recovery codes the websites gave you). To move to a new watch, install OTP Wallet there and import or add your entries again, then delete the app from the old watch (uninstalling removes all its data).
+
+## Troubleshooting
+
+| Problem | What to do |
+| --- | --- |
+| Codes are rejected | Open **Clock status** in the menu. The watch clock must be right to within a few seconds; sync the watch (GPS activity or phone sync). If it is always off by the same amount, set **Time offset** |
+| Nothing happens after saving a form | Open OTP Wallet on the watch; forms are applied when the app runs. Check that the form's switch was on |
+| "Already added" or "Already imported" | The watch has already applied exactly that form; change something to send a new one |
+| "Waiting for more" after an import | The JSON was incomplete: paste the rest and send again, or choose the mode "Cancel waiting import" |
+| "Damaged entry K7F2" | That entry's stored data can't be read. Delete it with Manage entry and add it again |
+| A barcode says "Too large for this screen" | The contents are too long for a scannable code on this watch. Use a shorter text, or a 2D symbology for long text |
+| A barcode doesn't scan | Turn brightness up (the option is on by default), hold the watch steady and flat, and try both closer and further away. Laser scanners at tills often can't read screens |
+| "Data from a newer version" | The app was downgraded; install the newest version again |
+| "Crypto self-test failed" | The watch's crypto didn't give the expected results; codes are hidden to be safe. Update the watch firmware and report it |
+
+## Known limitations
+
+- **Connect IQ limits**
+ - No unlocking with the watch's own passcode; the app has its own PIN.
+ - App settings only work with the Store version, which is why the app is a private beta rather than sideloaded.
+ - The phone may not show what the watch writes back (Result fields, cleared forms, the About catalog) until you reopen the settings.
+- **Import**
+ - Encrypted Aegis exports can't be decrypted on a watch (it would need 32 MiB of memory); export unencrypted.
+ - Google Authenticator export links aren't supported; mOTP and Yandex entries are skipped.
+ - Icons and notes aren't imported.
+ - Pasted text is limited to the eight import fields per sync, and a waiting import to 32 KB.
+- **Storage**
+ - At most 150 entries and about 100 KB in total.
+ - At most 16 phone changes can wait for the PIN at once.
+ - The About catalog shows up to 800 characters; the rest are under Entry IDs on the watch.
+- **Security**
+ - Locked entries are only as strong as the PIN against someone who copies the watch's storage; unlocked entries rely on the watch keeping its storage private.
+ - Secrets and the import text pass through Garmin Connect's settings on your phone.
+ - See [SECURITY.md](SECURITY.md) and [README section 8](../README.md#8-threat-model).
+- **Barcodes**
+ - Data Matrix uses ASCII and Base256 encoding only (no C40, Text, X12 or EDIFACT), and Aztec a simple mode choice. Their symbols are valid, but sometimes larger than another encoder's.
+ - PDF417 only fits short contents on small round screens (about 170 codewords on a 218-pixel screen).
+ - Large 2D codes take a while to encode the first time: seconds for typical cards, up to about a minute for the largest QR codes on the Forerunner 255 and 255S. After that they open from the cache.
+- **Glance:** only the first favourite TOTP or Steam entry without the PIN.
+- **No backup or export** from the watch, by design.
diff --git a/source/app/AppInfo.mc b/source/app/AppInfo.mc
index 21411dd..163486c 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.7.0";
+ const VERSION = "0.8.0";
}