# E5 wire contract

The console contract between the E5 station's computer and a Kode Dot: every verb and record, the order of a run, the refusals and the error codes.

## What it is

The E5 station tests a finished Kode Dot and decides whether it ships. A computer drives the unit over the USB-C console, in the same text protocol as any other console client, with a set of verbs that only a kodeOS built with `KODE_TEST_DOT` knows. The Kode Dot measures and the computer judges. This page is the contract between the two halves: the verbs the computer sends, the records it reads back and the order a run follows.

The Kode Dot's side is the Active Object AO_TestDot (`sdk/kaos/test-dot`). The computer's side is `tools/factory/test_dot_final.py` in the KodeOS repository. Both change in the same KodeOS release. The line grammar, the terminators and the error codes are those of `internals/console-protocol`, and the rows a run asks for are on `internals/production-test`.

## How it is connected

```mermaid
sequenceDiagram
  autonumber
  participant PC as Station computer
  participant K as Kode Dot
  PC->>K: info, test_dot_info
  K-->>PC: serial, versions, test_dot state
  PC->>K: test_dot_screen phase=connecting
  PC->>K: test_dot_place
  K-->>PC: place records, location blob
  PC->>K: test component=all
  K-->>PC: rows, camera records, blobs, ok
  loop until poll state=done
    PC->>K: test_dot_poll
    K-->>PC: operator steps, rows, poll
    PC->>K: pads, NFC, RFID, radio
  end
  PC->>K: test_dot_result verdict=pass
  K-->>PC: ok after the factory reset
  PC->>K: test_dot_photos, test_dot_lock
```

*One run, as the computer sees it*

### One session

The computer opens the port once and keeps it open from `test_dot_place` to `test_dot_lock`. Opening the port resets the board, and what a run keeps (the PASS screen, the ship countdown) lives in RAM, so a second open would drop it. A run that must stop sends `test_dot_end`, or closes the port and lets the next open start from a boot.

### The station's program

The program is `tools/factory/test_dot_final.py`, with its page in `tools/factory/ui/`. It identifies the Kode Dot, places it under the camera, runs the rows, does its own part (the rear pads, NFC, RFID, the radio) beside the operator, judges, tells the Kode Dot the verdict, saves the photos, locks a PASS and writes the report. It refuses a kodeOS that answers `test_dot_info` with code 1, and writes nothing for it.

| Thing | Value |
| --- | --- |
| Run | `python3 tools/factory/ui/test_dot_ui.py -p PORT` for the operator's page, or `python3 tools/factory/test_dot_final.py` alone |
| Reports | One folder per run under `~/kode-factory/reports`, never inside the repository: `report.html`, `results.json`, the pictures and the photos. `record_<station>.csv` gets one line per unit whose exit record was read |
| Settings | `~/kode-factory/test_dot_station.json`, copied from `tools/factory/test_dot_station.json` on the first run: the station's name, the two networks and their channels, the BLE beacon, the placement limits |
| Exit status | 0 PASS; 1 any other verdict; 2 no report (the port held, no answer, a kodeOS without the verbs); 3 everything passed but the lock was not confirmed, and the unit must not leave |
| Bench runs | `--no-place`, `--no-write` and `--operator-sim` make a BENCH verdict: no reset, no lock, nothing in the CSV, and `test_dot_end` at the end |
| Without a Kode Dot | `python3 tools/factory/ui/test_dot_sim.py --case pass` runs the program against a fake Kode Dot that answers this contract |

## Specifications

### The rules of the wire

| Rule | Value |
| --- | --- |
| Ports | The cable and the charging base only. A line from Bluetooth answers `>err code=5 msg=cable`, one from the Kode channel `msg=net`, one from the scheduler `msg=sched` |
| Grammar | A string is quoted, every other value is bare (`internals/console-protocol`). `win="704,628"` and `ssid24="lab"` are strings. `state=0xFE`, `ch=2` and `count=20` are not |
| Records | `>\|<name> key=value ...`, at most 192 bytes of text. Text values are quoted, numbers and closed words are bare, `-` is none |
| The end | Every verb ends with `>ok` or `>err code=<n> msg=<word>`, once |
| One at a time | A second line while one is in flight answers `>err code=4 msg=busy` |
| Watchdog | 295 s for the station verbs (`KODE_TEST_DOT_LINE_MS`), instead of the console's 5 s. The computer waits longer still: see the waits below |
| Log lines | `!` lines interleave with the records and carry nothing of the contract |
| Without the option | `test_dot_*` answers `>err code=1`; `test component=all` answers `>err code=5 msg=not_built` |
| After the lock | Every verb answers `>err code=5 msg=locked`. In the session that took the lock, `test_dot_end` still answers |

### The order of a run

| # | The computer sends | The Kode Dot does | Ends with |
| --- | --- | --- | --- |
| 1 | `ping`, then `info` | Answers. The computer reads the serial, the MAC and the versions | `>ok` |
| 2 | `test_dot_info` | Says where AO_TestDot is. Refuses a locked unit | `>\|test_dot ...` `>ok`, or `locked` |
| 3 | `test_dot_screen phase=connecting` | Shows the connecting screen | `>\|screen phase=connecting ok=1` `>ok` |
| 4 | `test_dot_place` | Wakes the station and looks for the four marks until the screen sits inside the limits | `>\|place result=ok ...` `>ok` |
| 5 | `test component=all` | Runs `sd`, `imu` and the camera part on the open line | `>ok` at the camera's end |
| 6 | `test_dot_poll`, about every 150 ms | Gives the operator's steps and the other rows | `>\|poll state=done lane=idle` |
| 6a | Beside the operator, while `lane=idle`: `test_dot_relays`, `test_dot_adc`, `test_dot_loop`, `test_dot_boost`, `test_dot_radio` | Drive the pads and check the C5 | Each its own `>ok` |
| 7 | `bat_info`, `test component=pmic`, `test component=gauge`, `temp` | Give the exit record | `>ok` each |
| 8 | `test_dot_result verdict=pass\|fail` | Shows PASS or FAIL. A pass runs the factory reset first | `>ok`, or `>err` with the step that stopped |
| 9 | `test_dot_photos`, then `test_dot_photos only=` for each one that arrived broken | Sends the kept photos as blobs | `>\|photos n= ...` `>ok` |
| 10 | `test_dot_lock confirm=on`, after a PASS | Stores the lock | `>ok` |

The computer waits at step 6 until the Kode Dot says the run is done, with a budget of 300 s. The Kode Dot's own watchdog for the run is 240 s. A bench run sends `test_dot_end` where a PASS sends the lock. The unit is unplugged at the end: 15 s after the cable leaves, a unit that passed switches off. A cable put back during the count cancels the count and nothing else. The Kode Dot stays armed, and the next unplug counts 15 s again.

### The waits

Every verb that can take long has three bounds, and each sits above the one before. The Kode Dot's bound is the longest it waits for an owner. The console's line is the longest it keeps the verb open. The computer's wait is the longest it listens. The order guarantees that the Kode Dot's own answer, a `timeout` included, reaches the computer before either of the others gives up.

| Verb | The Kode Dot's bound | The console's line | The computer waits |
| --- | --- | --- | --- |
| `test_dot_result verdict=pass` | 290.15 s, the reset's steps at their budgets (`KODE_TEST_DOT_RESET_MS`) | 295.15 s | 300.15 s |
| `test_dot_radio` | 33 s (`KODE_TEST_DOT_RADIO_MS`) | 295.15 s | 38 s |
| `test_dot_radio discover=on` | 148 s (`KODE_TEST_DOT_DISCOVER_MS`) | 295.15 s | 153 s |
| `test_dot_loop` | 30 s (`KODE_TEST_DOT_LOOP_MS`) | 295.15 s | 35 s |
| `test_dot_end` | 50 s (`KODE_TEST_DOT_END_MS`) | 295.15 s | 55 s |

The line is the reset's bound plus 5 s (`KODE_TEST_DOT_LINE_MS`), and the computer adds 5 s to each Kode Dot bound. The station's program holds a copy of each bound under the name `KODE_*_S`, and a test of the program checks every copy against `kqp_config.h` in the SDK.

### The verbs

| Verb | Keys | Notes |
| --- | --- | --- |
| `test component=all` | none | `test` alone means the same. The refusals are in order: `not_built`, the port, `locked`, `busy`, `camera`, `station_i2c` |
| `test_dot_place` | `win` string `"x,y"` (columns and rows of the sensor, 0 to 2592); `cam_ms` uint 1 to 60000; `mark_ms` uint 1 to 60000, default 8000; `dx_mm` and `deg`, a number with at most one decimal, defaults 15 and 5 | `win` is the window looked at first, and the last good one is kept. `cam_ms` is read and bounds nothing |
| `test_dot_poll` | none | |
| `test_dot_screen` | `phase` one of `connecting`, `result`; `verdict` one of `pass`, `fail` | `verdict` is required with `result` (code 2) |
| `test_dot_photos` | `only` uint 1 to 255 | A photo's number, counted from 1. A number the pass did not keep sends no photo (`photos n=0`) |
| `test_dot_blob` | `name` string `"location"` or `"colours"` | Any other name is code 3 |
| `test_dot_relays` | `state` a byte, `0x` hex or decimal, bare | A value over 255 or not a number is code 3 `state`. The byte whose two low bits read 01 (R2 on, R1 at rest) is code 5 `forbidden` |
| `test_dot_adc` | `ch` uint 0 to 3 | |
| `test_dot_loop` | `count` uint 1 to 100, default 20 | |
| `test_dot_radio` | `ssid24`, `ssid5` strings of 32 bytes at most; `ble` string of 29 at most; `ch24`, `ch5` uint 0 to 255; `discover` bool | Longer text is cut. An empty name or a zero channel leaves that part out. `discover=on` sweeps every channel once and reports where the two networks were heard instead of probing the channels given. A sweep has its own bound, 148 s |
| `test_dot_boost` | `state` one of `on`, `off` | |
| `test_dot_result` | `verdict` one of `pass`, `fail` | |
| `test_dot_lock` | `confirm=on` | `confirm=off` is code 3 |
| `test_dot_end` | none | Gives back what the test took. On a dormant Kode Dot nothing was taken, and it still gives the glass back, so a screen shown by `test_dot_screen` goes |
| `test_dot_info` | none | |
| `test_dot_camera` | `only` one of `framing`, `pixels`, `lines`, `zones`, `colour`, `led` | Without `only`, every part |

### The records of the open line

`test component=all` prints these before its `>ok`, in the order the run reaches them. A record the run reaches later goes into the ring and arrives through `test_dot_poll` with the same text.

| Record | Fields |
| --- | --- |
| `test=<row> result=<word> ms=<n>` | `row` is one of `sd`, `imu`, `audio`, `haptic`, `mag`, `pmic`, `gauge`, `ioexp`, `clocks`, `ir`, `nfc`, `rfid`, `mem`. `result` is `pass`, `fail`, `timeout` or `skipped`. A row that AO_Autotest would not take prints `result=absent` or `busy` with no `ms` |
| `test=cam_framing result=<w> marks=<n> inside=<0\|1> dx_mm= dy_mm= scale= nominal=<0\|1> min_marks=4 rescued=<0\|1> recentred=0 win="x,y"` | The four marks and where the screen sits. `why=marks_not_found_in_field` or `mark_cut_by_window` is added when it could not judge |
| `test=cam_straight result=<w> angle_deg= persp_pct= max_angle=7 max_persp_pct=8` | The turn and the perspective |
| `test=cam_pixels result=<w> dead=<n> stuck=<n> warn=<n> max_dead=2 max_stuck=1 [why=reflection_or_light] [at="x,y;on:x,y;"]` | Dead and stuck-on pixels. A `noverdict` carries `why=not_located`, `saturated`, `noise` or `no_reference` |
| `pixwarn x= y= dw= dg=` | A weaker spot, logged and never failed. Up to 4 |
| `test=cam_lines result=<w> lines=<n> x= y0= y1= row_y= level= hlevel= max_level=-0.20 wlevel= whlevel=` | Dead columns and rows |
| `test=cam_zones result=<w> zones=<n> min_ratio= limit_ratio=0.86 bright=<n> [at="x0,y0-x1,y1;"]` | Dark zones and bright patches |
| `colour_info dark=r,g,b white=... grey=... red=... green=... blue=...` | What the colour photos measured |
| `test=cam_colour result=<w> dist_grey= dist_red= dist_green= dist_blue= wchroma= ripple= expratio= tol=0.250 retakes=<n> [white_dead=1]` | The colours against the white |
| `test=led result=<w> flux_red= dist_red= flux_green= dist_green= flux_blue= dist_blue= at=x,y min_flux=4000 max_dist=0.8` | The RGB LED. A `noverdict` is `test=led result=noverdict why=<word>` |
| `timing=camera photos_ms= analysis_ms= led_ms= total_ms=` | The last record of the line |

`<w>` is `pass`, `fail` or `noverdict`. A `noverdict` means the Kode Dot could not measure, and the computer reports it as NO VERDICT, not as a failure.

### The records of the ring

| Record | Fields |
| --- | --- |
| `step=<n> of=6 press=<button>` | The operator is asked for a button. `button` is `pad-left`, `bottom`, `pad-down`, `top`, `pad-right` or `pad-up`, in that order |
| `step=<n> button=<name> result=pass ms=<n>` | The asked button, pressed alone |
| `step=<n> button=<name> result=timeout` | Nobody pressed in 20 s. The record has no `ms` |
| `step=<n> button=<name> result=<w> pressed=<names> ms=<n>` | `w` is `wrong` or `double` (the same button is asked again), `doublefail` (a second double), `stuck` (a button still down after the release window), `held` (a button already down when the step began, with no prompt) or `giveup` (the third wrong press). `pressed` lists the buttons that were down, joined with `+` (`pad-left+top`) |
| `test=buttons result=<pass\|fail\|timeout> ms=<n>` | The button test's verdict. `fail` after a give-up or a failed step, `timeout` when a step ran out and the test was not given up |
| `step=<n> of=2 target=<x>,<y>` | The operator is asked for a touch |
| `step=<n> target=<x>,<y> got=<x>,<y> result=<w>` | `w` is `pass`, `fail` or `miss` (a tap off the target, forgiven twice). `got` is where the tap landed |
| `step=<n> target=<x>,<y> result=timeout` | No tap in 20 s. The record has no `got` |
| `test=tp result=<pass\|fail\|timeout> ms=<n>` | The touch test's verdict, in the words of the button test |
| `timing=all total_ms= cam_end_ms= sd_ms= imu_ms= audio_ms= haptic_ms= mag_ms= pmic_ms= gauge_ms= ioexp_ms= clocks_ms= ir_ms= nfc_ms= rfid_ms= mem_ms=` | The last record of the run |
| `test=all result=<aborted\|timeout> why=<word>` | The run ended early: `sd_missing`, `watchdog`, `camera` or `verdict`. When the line is still open it ends with `>err code=5 msg=test` (`sd_missing`, `verdict`) or `code=6 msg=test` (`watchdog`, `camera`) |

The buttons are asked for 20 s each, with 3 s to let go. A wrong button is forgiven twice, and the third wrong press gives the test up. A double press is asked again once and then fails the step. AO_Buttons and AO_Touch write these records, with `buttons_test_record()` and `touch_test_record()`, and the console prints the same text for `test component=buttons` and `test component=tp`. The touch targets are at (80, 215) and (335, 285) of the 410 x 502 glass, 20 s each, within 64 px, and a tap off the target is forgiven twice.

`test_dot_poll` answers the ring in order, then `poll dropped=<n>` when newer records pushed older ones out of its 64 places, then:

| Record | Fields |
| --- | --- |
| `poll state=<running\|done> lane=<idle\|busy>` | `done` is any time no run is going, so it is also what a poll before a run reads. `busy` while a job or a blob is out |

### The records of the other verbs

| Verb | Records |
| --- | --- |
| `test_dot_info` | `test_dot state=<dormant\|waking\|awake\|ending> locked=<0\|1> run=<none\|running\|done\|aborted> psram_free=<bytes> j3=<free\|lent>`. `locked` reads the lock taken in this session; a lock stored at boot is the refusal itself |
| `test_dot_place` | `place mem psram_free= largest=`, `place init ok=1 ms= why=-`, `place state=searching`, then one `place check n=<k> marks= dx= dy= ang= win="x,y"` per look, with `k` counted from 1 (`place check n=<k> why=no_frame` when the camera gave no frame, `why=camera` when it failed, and `place result=cam_missing` follows). It ends with `place result=ok dx_mm= dy_mm= ang_deg= marks= tries= win="x,y" ms=` and the `location` blob; or, when the time is up, the last `place check`, `place state=misplaced` and `place result=misplaced marks= dx_mm= dy_mm= ang_deg= limit_mm= limit_deg=`; or, when the camera does not answer at the wake, `place state=no_camera` and `place result=cam_missing`. All of them end with `>ok` |
| `test_dot_screen` | `screen phase=<connecting\|result> ok=<0\|1>`. `ok=0` means the host did not answer in 2 s |
| `test_dot_relays` | `relays state=0x<HH> ok=<0\|1>`. When the chip did not take the byte the record reads `ok=0` and the verb ends `>err code=4 msg=station_i2c` |
| `test_dot_adc` | `adc ch=<n> mv=<int>`; no record and `>err code=4 msg=station_i2c` when the ADC did not answer. `mv` is the voltage at the ADS1115's pin, on its 4.096 V range. A divider in front of the pin is the station's and the computer's to undo |
| `test_dot_loop` | `loop ok=<0\|1> b9600=<n>/<count> b115200=<n>/<count> rev=<n>/<count> ms=<n> why=<->\|no_echo\|corrupt\|uart_busy>`. The frames are 16 bytes. `b9600` and `b115200` send on pad 2 and read on pad 3, and `rev` sends on pad 3 at 115200 |
| `test_dot_radio` | Three records, then `>ok`: `radio_card ok=<0\|1> chip=<esp32c5\|other> id=<n> kode=<0\|1> fw=<a.b.c> host=<a.b.c> mac=<aa:bb:cc:dd:ee:ff> sdio=<w>x<khz>kHz`, then `radio_time card_ms=<n> w24_ms=<n> w5_ms=<n> ble_ms=<n> ms=<n>`, then `radio ok=<0\|1> seen24= rssi24= ch24= seen5= rssi5= ch5= passive5= ble_adv= ble_rx= rssi_rx= why=<->\|link\|busy\|timeout\|error>`. `radio_card ok=1` means the card was read, the chip is a C5, it runs Kode's slave, its esp-hosted version `fw` equals the Kode Dot's own `host`, and the SDIO bus is more than 0 bits wide. `id` is the chip's number. The four times are how long the card read, the 2.4 GHz part, the 5 GHz part and the Bluetooth part took (the find and the advert together), and `ms` is the whole check. A check that outlasts its bound prints `radio ok=0 why=timeout` alone and ends `>err code=6 msg=timeout` |
| `test_dot_boost` | `boost on=<0\|1> fault=0x<HH> current_ma=<int>`. A negative current is the cell discharging. A refusal (a cable in, a cell under 3.6 V) still prints the record and ends `>err code=4 msg=boost` |
| `test_dot_photos` | `photos total=<n>`, then one blob per photo, `photo n=<i> why=encode` for one that did not encode, and `photos n=<sent> bytes=<total> ms=<n>`. `i` counts from 1. With `only=` there is no `photos total=` |
| `test_dot_camera` | The records of that part as above, its blobs and `timing=camera` |
| `test_dot_result`, `test_dot_lock`, `test_dot_end` | None. The end is the answer |

### Blobs and photos

A blob is a JPEG that crosses the wire as records. It is sent as one header and a run of data records, with no end marker. The receiver counts `len`.

| Record | Fields |
| --- | --- |
| `blob name="<name>" len=<bytes> crc=<n>` | The header. `crc` is the zlib CRC-32 of all the bytes, in decimal |
| `d=<base64>` | The bytes, 120 at a time (160 base64 characters), in order |

The names a pass keeps are `location` (the marks photo, 352 x 256), `colours` (a strip of five 96 x 64 crops: red, green, blue, white, grey), `dead_<x>_<y>` and `on_<x>_<y>` (64 px crops of a defect) and `zone_<x0>_<y0>` (a 128 x 96 crop), eight at most. `location` arrives after the placement and again after the first camera step. The camera part lends `colours` and the crops after its verdict. `test_dot_blob` asks again for `location` or `colours`.

`test_dot_photos` names a photo `photo_<NN>_<what>` where `NN` is its number from 1 and `what` is the pattern: `place`, `black_marks`, `panel_off`, `white`, `grey`, `red`, `green`, `blue`, `led_off`, `led_red`, `led_green` or `led_blue`. A pass keeps 14 at most. The computer checks length and CRC and asks again with `only=` for one that arrived short or wrong. The photos and blobs of a pass live until `test_dot_end` or a reset, and a PASS does not free them.

### The refusals

| Code | Word | When |
| --- | --- | --- |
| 1 | the verb | The build has no `KODE_TEST_DOT` |
| 5 | `not_built` | `test component=all` in a build without it |
| 5 | `cable`, `net`, `sched` | The line came from Bluetooth, the Kode channel or the scheduler |
| 5 | `locked` | The unit carries the lock, or the session took it and the verb is not `test_dot_end` |
| 4 | `busy` | Another verb, a run or the end is in progress, or a run verb while a run goes. The factory reset on the console is also `busy` while a run goes |
| 4 | `camera` | J3's supply or the camera did not answer |
| 4 | `station_i2c` | The relay expander or the ADC did not answer |
| 4 | `no_memory` | The worker or the PSRAM could not be had |
| 4 | `no_run` | `test_dot_result` before a run reached its end, or `verdict=fail` before any run |
| 5 | `failed` | `verdict=pass` after a FAIL |
| 4 | `reset_find`, `reset_install`, `reset_apps`, `reset_link`, `reset_wipe`, `reset_nvs`, `reset_request` | The factory reset of a PASS stopped at that step. FAIL is on the glass |
| 6 | `reset_timeout` | The reset did not answer within its budget |
| 4 | `nvs` | The lock could not be written |
| 4 | `no_blob` | The blob was not kept |
| 4 | `boost` | The boost was refused or failed |
| 4 | `dock` | AO_Log would not lend the dock's pins |
| 6 | `timeout` | An owner, the worker or the radio check did not answer in its bound. The unit can be `busy` until the worker answers |
| 5 | `test` | `test component=all` ended early (see the ring's `test=all` record) |
| 2, 3 | the key | A key is missing or wrong, as for any verb |

## Gotchas

> **DANGER**
>
> **`test_dot_result verdict=pass` erases the unit.** The factory reset removes the apps, the saved networks and the settings, and the unit then waits for the cable to leave to switch itself off. Send it once, for a unit that finished a run, and send nothing else except the photos and the lock.

> **WARNING**
>
> **`test_dot_lock` closes the verbs for good.** After `>ok`, even in the same session, every verb but `test_dot_end` answers `locked`, and after a reboot `test_dot_end` does too. Read the photos first. A `locked` answer to `test_dot_info` means the unit already passed a station.

> **WARNING**
>
> **Never close and reopen the port during a run.** Opening the port resets the board. The PASS screen and the countdown to ship mode are gone, and a unit that passed is back at the start with its apps already erased.

> **WARNING**
>
> **A cable put back does not cancel the ship.** After a PASS, 15 s with the cable out switches the unit off. A replug during those 15 s only cancels the count. The Kode Dot stays armed and erased, and the next unplug counts 15 s again. A unit that is plugged in and left there never switches off.

> **NOTE**
>
> **`test_dot_relays` takes the raw byte.** The Kode Dot refuses only the forbidden pair, so a byte that closes a relay the station does not expect is accepted. The computer's own guard refuses it before the wire, and the delivery state `0xFC` is entered only from the charging state `0xFE`.

## Example code

Run the whole program against a fake Kode Dot, with no board and no station.

1. Run `python3 tools/factory/ui/test_dot_sim.py --case pass`. The program speaks this contract to the fake Kode Dot: placement, rows, camera, operator steps, pads, radio, verdict, photos and lock.
2. Read the last line: `PASS` with the time and the path of `report.html`. Open it to see the steps and the pictures.
3. Run it again with `--case fail`. The last line reads `FAIL` and the report names the step.
4. With a Kode Dot built with the option and the cable in, run `python3 tools/kcli.py -p PORT --settle 5 cmd 'test_dot_info'`. The answer is the `test_dot` record and `>ok`.

## In the SDK

- [`test_dot_verbs.c`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/test_dot/src/test_dot_verbs.c): the verbs, their keys and the blob writer.
- [`test_dot_ao.c`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/test_dot/src/test_dot_ao.c): the records of every verb and the order of a run.
- [`test_dot_cam.c`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/test_dot/src/test_dot_cam.c): the camera part's records.
- [`tools/factory`](https://github.com/kodediy/KodeOS/tree/main/tools/factory): the station's program, its page, its simulator and `ui/EVENTS.md`, the events it prints for the page.
