kodeOS internals
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
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 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
Example code
Run the whole program against a fake Kode Dot, with no board and no station.
- 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. - Read the last line:
PASSwith the time and the path ofreport.html. Open it to see the steps and the pictures. - Run it again with
--case fail. The last line readsFAILand the report names the step. - 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 thetest_dotrecord and>ok.
In the SDK
test_dot_verbs.c: the verbs, their keys and the blob writer.test_dot_ao.c: the records of every verb and the order of a run.test_dot_cam.c: the camera part's records.tools/factory: the station's program, its page, its simulator andui/EVENTS.md, the events it prints for the page.