kodeOS internals

Production test

The rows that check every part of a finished Kode Dot: the fixture, what each row judges, how the E5 final test runs them and how to read a result.

What it is

The production test checks every part of a finished Kode Dot, one row per part, one row at a time. At the factory the final test is test component=all, the E5 test: a station's computer starts it over the cable, and the Kode Dot runs thirteen of the rows, adds a camera's check of the glass and the LED and an operator's presses and touches, and the computer decides whether the unit ships (sdk/kaos/test-dot, internals/e5-wire-contract). Every row also runs by name, and test component=main runs the six that a bare main PCB answers.

A row is an entry in the table TESTS[] in components/kaos/autotest/src/autotest_ao.c, or in the table of the owner that judges it. The Active Object that owns the part judges its row. AO_Autotest walks the table for a group or a name, and AO_TestDot asks it for one row at a time in the E5 run. sdk/kaos/autotest has the design. This page is for the person who tests units on the line and for the engineer who changes a row.

How it is connected

flowchart LR
console["test component=main<br/>or a row"] --> ao["AO_Autotest"]
e5["test component=all"] --> td["AO_TestDot"]
td -- "one row at a time" --> ao
ao -- "asks the owner" --> owner["owner AO<br/>AO_Power, AO_Imu, ..."]
owner -- "TEST_RESULT_SIG" --> ao
ao -- "AUTOTEST_ROW_SIG" --> out["AO_Console<br/>or AO_TestDot"]
out --> reply["test=NAME<br/>result=pass, fail or timeout"]
class console,e5,reply ext
class ao,td,owner,out ao
How a row reaches a verdict

A row has two independent flags:

The rows clocks, psram and mem have no owner Active Object:

An asynchronous row has a budget, the worst case of its script, held in kqp_config.h. The budget becomes the console's watchdog while the answer is owed. A row still silent after it prints result=timeout and the run goes on, so one owner that hangs cannot stop the count of the others.

Specifications

The command

FormRuns
test component=allThe default. The E5 final test on a station build: thirteen rows, the camera, the operator, from the cable alone. A build without KODE_TEST_DOT answers >err code=5 msg=not_built
test component=mainpmic, gauge, ioexp, clocks, imu, mag, for a main PCB on its own
test component=userThe owner's profile, run from the Self-test page of the settings screen. Not the production test
test component=NAMEOne row by name, for example test component=imu

From a computer: python3 tools/kcli.py -p PORT --settle 5 -v cmd --timeout 600 "test component=main". The default wait is 5 s, so --timeout 600 is needed for a long run. Over Bluetooth use kble.py. See internals/bench-tools.

Each row prints its readings as ! log lines, then one record >|test=NAME result=pass|fail|timeout. A run of a group or a row ends with >|summary pass=N fail=N timeout=N (a skipped=N field appears only when a row was skipped) and >ok. fail= counts every row that did not pass, timeouts included. When fewer rows answered than were promised, the run ends with an error instead of >ok.

The E5 run prints no summary. Each row ends in >|test=NAME result=<word> ms=N, with the word pass, fail, timeout or skipped, or absent or busy when the sequencer would not take it. The station's computer counts them and decides the verdict.

Word after result=Meaning
passEvery criterion of the row held
failA check failed; the record names it with its numbers
timeoutThe owner stayed silent after its budget, or nobody answered a step. A step answered wrong keeps the row a fail
absentThe part does not answer. A failed verdict. Expected for haptic on a unit with no Antenna PCB
noverdictThe row could not measure; why= says mute, short or loud. The verdict is timeout
stuckA button is still held a whole step after it was judged; a defect of the unit

The fixture

Without an item, the row that needs it fails or ends in timeout.

ItemWhy
The battery on J7, not fullThe cell powers the gauge, and with no pack the thermistor input floats to cold and forbids charging. The ammeter is proven only when current flows
A USB adapter, not a computer portA computer port is detected as 500 mA, which leaves too little charge current to prove the gauge's ammeter
Room temperature, 10 to 45 °CThe pack's charging window
A microSD card in the slotAny card. The row judges the slot, reads and writes 1 MB, and deletes its file. An empty slot fails the row
An NFC tag on the readerBlank, or carrying a kodeOS serial (an NDEF text starting KODE). The row writes this unit's serial and reads it back
An EM4100 tag on the 125 kHz antennaThe row passes on any ID that decodes twice in a row and logs the ID, so an operator can compare it with the fixture's tag
A BLE advertiser and an access point within a few metresThe c5 row needs one advertiser in a 5 s listen, and the strongest network at or above -70 dBm
A fresh bootAfter radio_down or install dst=c5 the C5 link cannot be rebuilt until the board resets
No wifi flag storedWith it, an autojoin round can hold the channel past the c5 row's budget
A room below -20 dBFSThe audio row refuses to judge above it and says so about the room
A still benchimu judges rest, mag compares two readings, haptic divides a driven window by a still one. A table already carrying 235 Hz, such as another unit buzzing on it, makes haptic refuse to judge
A person at the benchFor buttons, led, display and tp when run by name. With nobody there they end in timeout. In the E5 run an operator answers buttons and tp, and the camera judges the glass and the LED in place of display and led

The rows

The groups below put chips and crystals first, then the card and the radio, then the rows that make noise, then the four that need an operator, and the memory rows last, because mem judges what every row before it did to internal RAM.

The E5 run takes its own order. sd and imu run first, while the unit is still, and then the camera part. The rows after it run beside the operator, in this order: audio, haptic, mag, pmic, gauge, ioexp, clocks, ir, nfc, rfid and mem. The run does not take c5, which the station's radio check covers, nor psram, which stays a row of its own.

Rows: chips and crystals

The power chips, the expander, the crystal, the motion parts and the haptic driver.

RowOwnerWhat passes
pmicAO_PowerThe BQ25896 answers with its part number; the boot policy reads back as written; the input is USB at 4400 to 5500 mV; the ADC converted a cell between 3000 and 4280 mV; SYS at or above its minimum; the thermistor in the charging band; no live fault; the charger is charging. Budget 5 s
gaugeAO_PowerThe CW2217 answers (version byte 0xA0, algorithm ready); its cell voltage agrees with the charger's within 100 mV; with at least 200 mA of charge, its current agrees with the charger's within 50 mA plus a quarter. Budget 5 s
ioexpAO_IoExpThe TCA6408A answers, P0 to P6 are inputs, and INT on GPIO6 is high after an input read. Budget 5 s
clocksclocks driverThe RTC slow-clock mux is on X3 and X3 measures 32768 Hz within 1 %. This proves the crystal is fitted and roughly right, not that it is in spec
imuAO_ImuWHO_AM_I is 0x05; the manufacturer self-test shifts each accelerometer axis by at least 200 mg and each gyroscope axis by at least 300 dps; at rest the acceleration is within 150 mg of 1 g, rotation below 20 dps, die temperature 0 to 60 °C. Budget 2.5 s
magAO_ImuThe QMC5883P chip id is 0x80 and it converts. The self-test shift and the field at rest are logged and not judged. Budget 5 s
hapticAO_HapticThe AW86233 answers, and the IMU as witness measures the motor. After a 140 ms still floor (refused above 1.2 mg at 235 Hz), buzzes of 200 ms at 25, 50 and 80 % gain, the first to reach 5.2 times the floor passes

Rows: the card and the radio

The microSD slot and the C5.

RowOwnerWhat passes
sdAO_StorageIdentify with four data lines; read 1 MB of raw sectors on four lines; those two carry the verdict. A one-line read, a 1 MB write and a read-back are measured and reported but never fail the row
c5AO_RadioThe link is up within 10 s; the chip is an ESP32-C5 on the same esp-hosted version; the image names the project kode_c5; SDIO runs four lines at 40 MHz; the GPIO service answers; a Wi-Fi scan hears a network at -70 dBm or stronger; the BLE stack starts; a 5 s BLE listen hears an advertiser

Rows: infrared, NFC, 125 kHz and audio

The rows that send or make noise.

RowOwnerWhat passes
irAO_IrStep 1: the receiver recovers on its own, nothing sent. Step 2: a NEC frame with a random 16-bit command is echoed back exactly, up to three attempts with a fresh command each
nfcAO_NfcThe ST25R3916 identifies itself, the field turns on and a tag is presented within three tries; on a writable tag the unit's serial is written and read back, otherwise the same UID is read twice
rfidAO_RfidA stable EM4100 ID decodes twice in a row within 16 windows of 150 ms. Budget 3.4 s
audioAO_AudioRegister 0x44 reads 0x08; a silence window is not dead, railed or too loud; two different tones drawn from eight detector frequencies each return above the other seven and above the floor, up to three attempts

Rows: operator

The four rows judged by a person at the unit.

RowOwnerWhat passes
buttonsAO_ButtonsSix prompts, 20 s each: pad-left, bottom, pad-down, top, pad-right, pad-up. Each must read as exactly that button and be released within 3 s. A wrong press or a double is forgiven and the same button is asked again; the third wrong press ends the run
ledAO_LedMgrRed, green and blue at level 96, 10 s each; TOP confirms, BOT refuses. Stops at the first colour not confirmed
displayAO_DisplayWhite, red, green, blue and black over the whole glass at 100 %, 10 s each; TOP confirms, BOT refuses. With no touch controller answering at 0x15, the row answers panel=absent at once
tpAO_TouchThe CST820 identifies itself; two taps, one on each side of the glass, each land within 64 px of the target, 20 s each. A tap off the target is forgiven and the same target is asked again; the third on one target fails it. Nothing is stored

Rows: memory

The last two rows, which test the PSRAM and the internal RAM.

RowOwnerWhat passes
psramAO_AutotestThe free PSRAM less 2 MB, in 1 MB blocks, four passes (each word's own address and the complement, read back). Budget 15 s. A record >|psram tested_kb= free_kb= errors= first= wrote= read= precedes the verdict
memAO_Autotestint_low since boot is at least 64 KB and alloc_fail is 0. It tests the firmware, not a part, and a unit cannot repair it

Rows that no group runs: screenshot, jpeg, fs, boot, apps, kfw, id, ble_console, net, and the user-profile rows sd_user, c5_user, nfc_field, rfid_field, power_plug, nfc_card, rfid_card. They run by name, as every other row does. ble_console needs the BLE console build, net needs the network, and the c5 rows need the radio. sd_verify is a separate verb that judges the card itself and wipes it; the sd row leaves the card as it found it.

Gotchas

Example code

Run the rows on a unit on the bench. The E5 run needs the station and its computer, and internals/e5-wire-contract has its example.

  1. Fit the battery, plug in a USB adapter, put a microSD card in the slot, an NFC tag on the reader and an EM4100 tag on the 125 kHz antenna.
  2. Run python3 tools/kcli.py -p PORT --settle 5 -v cmd --timeout 600 "test component=sd". The row prints ! readings and one >|test=sd result=pass record, then the summary and >ok.
  3. Run the rows that need hands one at a time: test component=buttons, test component=led, test component=display, test component=tp. Press the named button, then TOP for yes and BOT for no on the colours and fields, then tap the two targets.
  4. Run test component=mem last. >|summary pass=1 fail=0 timeout=0 and >ok mean the internal pool held its floor.
  5. For a main PCB with no panel, card or tags, run test component=main. It ends with its own summary and needs nobody at the glass.

In the SDK