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

ThingValue
Runpython3 tools/factory/ui/test_dot_ui.py -p PORT for the operator's page, or python3 tools/factory/test_dot_final.py alone
ReportsOne 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 status0 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 Dotpython3 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

RuleValue
PortsThe 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
GrammarA 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 endEvery verb ends with >ok or >err code=<n> msg=<word>, once
One at a timeA second line while one is in flight answers >err code=4 msg=busy
Watchdog295 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 optiontest_dot_* answers >err code=1; test component=all answers >err code=5 msg=not_built
After the lockEvery 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 sendsThe Kode Dot doesEnds with
1ping, then infoAnswers. The computer reads the serial, the MAC and the versions>ok
2test_dot_infoSays where AO_TestDot is. Refuses a locked unit>|test_dot ... >ok, or locked
3test_dot_screen phase=connectingShows the connecting screen>|screen phase=connecting ok=1 >ok
4test_dot_placeWakes the station and looks for the four marks until the screen sits inside the limits>|place result=ok ... >ok
5test component=allRuns sd, imu and the camera part on the open line>ok at the camera's end
6test_dot_poll, about every 150 msGives the operator's steps and the other rows>|poll state=done lane=idle
6aBeside the operator, while lane=idle: test_dot_relays, test_dot_adc, test_dot_loop, test_dot_boost, test_dot_radioDrive the pads and check the C5Each its own >ok
7bat_info, test component=pmic, test component=gauge, tempGive the exit record>ok each
8test_dot_result verdict=pass|failShows PASS or FAIL. A pass runs the factory reset first>ok, or >err with the step that stopped
9test_dot_photos, then test_dot_photos only= for each one that arrived brokenSends the kept photos as blobs>|photos n= ... >ok
10test_dot_lock confirm=on, after a PASSStores 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.

VerbThe Kode Dot's boundThe console's lineThe computer waits
test_dot_result verdict=pass290.15 s, the reset's steps at their budgets (KODE_TEST_DOT_RESET_MS)295.15 s300.15 s
test_dot_radio33 s (KODE_TEST_DOT_RADIO_MS)295.15 s38 s
test_dot_radio discover=on148 s (KODE_TEST_DOT_DISCOVER_MS)295.15 s153 s
test_dot_loop30 s (KODE_TEST_DOT_LOOP_MS)295.15 s35 s
test_dot_end50 s (KODE_TEST_DOT_END_MS)295.15 s55 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

VerbKeysNotes
test component=allnonetest alone means the same. The refusals are in order: not_built, the port, locked, busy, camera, station_i2c
test_dot_placewin 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 5win is the window looked at first, and the last good one is kept. cam_ms is read and bounds nothing
test_dot_pollnone
test_dot_screenphase one of connecting, result; verdict one of pass, failverdict is required with result (code 2)
test_dot_photosonly uint 1 to 255A photo's number, counted from 1. A number the pass did not keep sends no photo (photos n=0)
test_dot_blobname string "location" or "colours"Any other name is code 3
test_dot_relaysstate a byte, 0x hex or decimal, bareA 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_adcch uint 0 to 3
test_dot_loopcount uint 1 to 100, default 20
test_dot_radiossid24, ssid5 strings of 32 bytes at most; ble string of 29 at most; ch24, ch5 uint 0 to 255; discover boolLonger 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_booststate one of on, off
test_dot_resultverdict one of pass, fail
test_dot_lockconfirm=onconfirm=off is code 3
test_dot_endnoneGives 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_infonone
test_dot_cameraonly one of framing, pixels, lines, zones, colour, ledWithout 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.

RecordFields
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=8The 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.8The 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

RecordFields
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=timeoutNobody 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=timeoutNo 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:

RecordFields
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

VerbRecords
test_dot_infotest_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_placeplace 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_screenscreen phase=<connecting|result> ok=<0|1>. ok=0 means the host did not answer in 2 s
test_dot_relaysrelays 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_adcadc 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_looploop 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_radioThree 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_boostboost 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_photosphotos 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_cameraThe records of that part as above, its blobs and timing=camera
test_dot_result, test_dot_lock, test_dot_endNone. 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.

RecordFields
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

CodeWordWhen
1the verbThe build has no KODE_TEST_DOT
5not_builttest component=all in a build without it
5cable, net, schedThe line came from Bluetooth, the Kode channel or the scheduler
5lockedThe unit carries the lock, or the session took it and the verb is not test_dot_end
4busyAnother 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
4cameraJ3's supply or the camera did not answer
4station_i2cThe relay expander or the ADC did not answer
4no_memoryThe worker or the PSRAM could not be had
4no_runtest_dot_result before a run reached its end, or verdict=fail before any run
5failedverdict=pass after a FAIL
4reset_find, reset_install, reset_apps, reset_link, reset_wipe, reset_nvs, reset_requestThe factory reset of a PASS stopped at that step. FAIL is on the glass
6reset_timeoutThe reset did not answer within its budget
4nvsThe lock could not be written
4no_blobThe blob was not kept
4boostThe boost was refused or failed
4dockAO_Log would not lend the dock's pins
6timeoutAn owner, the worker or the radio check did not answer in its bound. The unit can be busy until the worker answers
5testtest component=all ended early (see the ring's test=all record)
2, 3the keyA 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.

  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