aboutsummaryrefslogtreecommitdiff
path: root/docs/DEVELOPER_GUIDE.md
blob: 7b56f3bed0244ecfca1ab8a4c11d4d7b97fe1833 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!--
  Author:  cirakg & claude ai
  Summary: How to build, test and debug OTP Wallet in Docker, how the tooling works, and the coding conventions.
-->
# Developer guide

Everything runs in Docker through the `Makefile`; the host needs only Docker and make. This guide covers the daily workflow, how the tooling works underneath, and the conventions every change follows. The design and the build plan are in the [README](../README.md).

## First-time setup

1. Docker access: your user must be in the `docker` group (log out and in after joining, or run `newgrp docker` in a terminal).
2. `make image` builds the development image (about 1.1 GB; it needs the network).
3. `make sdk` is interactive: you accept Garmin's agreements, log in, and the SDK (9.2.0) plus the device files of all 72 products download into `.cache/home` (about 1.1 GB). The login token is deleted at the end.
4. `make key` creates the developer key in `.cache/keys/`. Back that folder up; see [README 15.6](../README.md#156-developer-key).

## Daily workflow

| Task | Command |
| --- | --- |
| Build for one device | `make build DEVICE=fr70` |
| Run the app and watch it in your browser | `make sim DEVICE=fr70`, then open http://127.0.0.1:6080/vnc.html |
| Unit tests (Python tools and Monkey C, 4 devices) | `make test` |
| 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` |
| 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)) |
| Everything, in order | `make ci` |
| A shell in the container | `make shell` |

Use other devices with `TEST_DEVICES="fr70 venu3"` on any target that takes a device list. Every target runs serially, even under `make -j`.

## Repository layout

| Path | Content |
| --- | --- |
| `source/` | The watch app (Monkey C), one folder per module (README section 5) |
| `test/` | Monkey C unit tests (`(:test)` functions), compiled only into test builds |
| `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 |
| `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 |
| `.cache/`, `build/` | Git-ignored: SDK, device files, key; build outputs |

## How the tooling works

### Builds

`docker/scripts/build-app.sh` compiles one device in one mode:

- `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.
- `release`: `monkeyc -r` removes every `(:debug)` declaration; used by `make package`.
- `test`: adds `test.jungle` and `--unit-test`.

Every build uses strict type checking (`-l 3`) and fails on any compiler warning. The launcher icon is the classic source of warnings: the compiler warns when it has to scale an icon, so every product gets an icon of exactly its size through `icons.jungle`. After adding a product to the manifest, run `make sdk` (device files) and then `make icons`.

### The headless simulator

The simulator is a GTK program. The scripts start it on a virtual display (Xvfb on `:99`) with the openbox window manager, so its window can be found for screenshots. Behaviour verified with SDK 9.2.0:

- It accepts `monkeydo` on TCP port 1234; the scripts wait until that port listens (read from `/proc/net/tcp`).
- It keeps a single-instance lock file, `~/Sim-dev`, holding its process ID. That file survives in `.cache/home`, and a later container often reuses the process ID for something else, so the simulator would believe it's already running. `ciq_start_simulator` deletes the stale lock first.
- `monkeydo` exits with 1 even when every unit test passed, so `tools/ciq_test_report.py` judges the console output instead.
- For a normal (non-test) run, `monkeydo` blocks while the app runs and streams the app's `System.println` output. Quotes in that output arrive escaped, which is why the diagnostics use plain `key=value` pairs.
- **It starts apps that have a glance in the glance.** `ciq_launch_app` waits for the glance's `DIAG shown glance` line, then opens the app the way the simulator accepts. On touch screens that's a tap on the glance; on button-only watches it's DOWN (which selects the glance) and then START. START alone does nothing in the simulator. The press is retried if the app doesn't open.
- **A second simulator in the same container opens a second window**, and input may go to the wrong one. `ciq_start_simulator` reuses one that's already listening.
- Both `monkeydo` and the harness append to the app log; with `>` one would overwrite the other's lines.

### Monkey C facts that matter here (verified with SDK 9.2.0)

- `Long` is 64-bit and wraps on overflow; `>>` is an arithmetic shift, so logical shifts and rotations mask off the copied sign bits (see `source/crypto/Sha512.mc`).
- Assigning a value above 255 to a ByteArray element throws; mask with `& 0xFF`.
- Native HMAC supports only SHA-256; HMAC-SHA1 and HMAC-SHA512 go through `Hmac.generic`.
- Function calls are expensive in the VM; hot loops inline small helpers (SHA-512 went 37 % faster that way).
- **The watchdog** ("Code Executed Too Long") is not enforced in unit-test runs, but it is in normal simulator runs, with the device's `watchdogCount` per event: 120,000 bytecodes on the Forerunner 255/255S, 240,000 on the others. M4 measured what it means. 200 byte-wise PBKDF2 iterations in one event tripped it on the 255S, and so did two SHA-512 codes in one draw. So:
  - 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.
- `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>`).
- `Application.Properties.getValue` on a property that isn't in `properties.xml` stops the app with an uncatchable "Unexpected Type Error". Every option is therefore declared in `resources/settings/properties.xml`.
- **On touch screens, a tap is delivered as the `onSelect` behaviour first**, and `onTap` only runs if `onSelect` returns false. Delegates that need the tap position (the list, the PIN keypad) return false from `onSelect` on touch screens, and handle the START button in `onKey`.
- **Glances:** because `getGlanceView` is `(:glance)`, the compiler checks the whole app class against the glance scope. Methods that never run in glance mode opt out with `(:typecheck(disableGlanceCheck))`.
  - `AppBase.initialize` runs before non-glance code is loaded, so `Diag` (which it calls) is `(:glance)`.
  - `onStop` also runs after the glance, so it only touches `AppState` once the full app has started.
- `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`.

### Screenshots and the UI flow

`make screenshots` builds the UI-test variant and runs `tools/ui_flow.py` on every device in `TEST_DEVICES` and `SPOT_DEVICES`.

1. **Input.** The flow clicks the buttons where the device's `simulator.json` draws them. It taps and swipes on touch screens, and long-presses for the menu where there's no MENU button.
2. **Waiting.** After each input it waits for the screen's `DIAG shown <screen>` line. The list and the OTP screen print it only once every visible code is drawn.
3. **Touch targets.** It reads where to tap from `DIAG list_focus` (the focused row) and `DIAG pin_keys` (the keypad).
4. **Comparison.** Each screen is cut out of the window capture by `tools/sim_screenshot.py` and compared with `test/golden/<device>/<screen>.png`. That tool finds the device drawing by its bezel, so it doesn't assume a window layout. Differences go to `build/screenshots/<device>/<screen>-diff.png`.

Because the entry IDs, the clock and the codes are fixed in the UI-test build, every run is pixel-identical. Native `Menu2` menus print nothing, so the flow waits a fixed 1.5 s for them. Touch-only watches show menus as a freely scrolling list without focus, so About is reached only on watches with buttons.

### 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.

## Conventions

- **Headers**: every file starts with `Author: cirakg & claude ai` and a one-line `Summary:` in its comment syntax. JSON files use `author`/`_author` and `description`/`_summary` keys, and PNG images use Author and Description text chunks. Only `docker/docs/package-lock.json`, which npm rewrites, is exempt. `make lint` enforces this (rule HDR001).
- **Comments**: every module, class and non-trivial function explains its purpose, inputs, outputs, errors and security implications.
- **No logging**: `System.println` is allowed only in `source/util/Diag.mc`, and only in `(:debug)` code. Secrets, codes and entry names never go to a log.
- **No network**: `manifest.xml` requests no permissions and no code uses `Toybox.Communications`. `make lint` enforces both.
- **Secrets**: no real secrets anywhere in the repository; test data is fake and says so.
- **Small functions, named constants**, strict typing in Monkey C (`-l 3`) and Python (`mypy --strict`), and warnings treated as errors in the compiler, pytest and the linters.
- **Commits**: [Conventional Commits](https://www.conventionalcommits.org/) on `main`, one finished and verified unit per commit, with `CHANGELOG.md` updated.

## 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 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.
- **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** (from M6): one encoder module under `source/codes/`, its validator, unit tests with reference patterns, and the checks in [README 12.4](../README.md#124-how-each-symbology-is-proven-scannable).