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
A row has two independent flags:
TEST_SYNCrows are judged on AO_Autotest's own thread and publish at once (screenshot,jpeg,clocks,mem,boot,apps,kfw,id,ble_console).TEST_ASYNCrows belong to another Active Object, which answers later withTEST_RESULT_SIG, orTEST_SKIP_SIGwhen what the row needs is not there.
The rows clocks, psram and mem have no owner Active Object:
clocksis judged by the clocks driver.psramis walked by AO_Autotest in steps with the patterns ofautotest_psram.c.memreads the two numberskhal_systemkeeps about internal RAM.
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
| Form | Runs |
|---|---|
test component=all | The 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=main | pmic, gauge, ioexp, clocks, imu, mag, for a main PCB on its own |
test component=user | The owner's profile, run from the Self-test page of the settings screen. Not the production test |
test component=NAME | One 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 |
|---|---|
pass | Every criterion of the row held |
fail | A check failed; the record names it with its numbers |
timeout | The owner stayed silent after its budget, or nobody answered a step. A step answered wrong keeps the row a fail |
absent | The part does not answer. A failed verdict. Expected for haptic on a unit with no Antenna PCB |
noverdict | The row could not measure; why= says mute, short or loud. The verdict is timeout |
stuck | A 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.
| Item | Why |
|---|---|
| The battery on J7, not full | The 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 port | A computer port is detected as 500 mA, which leaves too little charge current to prove the gauge's ammeter |
| Room temperature, 10 to 45 °C | The pack's charging window |
| A microSD card in the slot | Any 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 reader | Blank, 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 antenna | The 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 metres | The c5 row needs one advertiser in a 5 s listen, and the strongest network at or above -70 dBm |
| A fresh boot | After radio_down or install dst=c5 the C5 link cannot be rebuilt until the board resets |
No wifi flag stored | With it, an autojoin round can hold the channel past the c5 row's budget |
| A room below -20 dBFS | The audio row refuses to judge above it and says so about the room |
| A still bench | imu 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 bench | For 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.
| Row | Owner | What passes |
|---|---|---|
pmic | AO_Power | The 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 |
gauge | AO_Power | The 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 |
ioexp | AO_IoExp | The TCA6408A answers, P0 to P6 are inputs, and INT on GPIO6 is high after an input read. Budget 5 s |
clocks | clocks driver | The 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 |
imu | AO_Imu | WHO_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 |
mag | AO_Imu | The 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 |
haptic | AO_Haptic | The 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.
| Row | Owner | What passes |
|---|---|---|
sd | AO_Storage | Identify 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 |
c5 | AO_Radio | The 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.
| Row | Owner | What passes |
|---|---|---|
ir | AO_Ir | Step 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 |
nfc | AO_Nfc | The 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 |
rfid | AO_Rfid | A stable EM4100 ID decodes twice in a row within 16 windows of 150 ms. Budget 3.4 s |
audio | AO_Audio | Register 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.
| Row | Owner | What passes |
|---|---|---|
buttons | AO_Buttons | Six 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 |
led | AO_LedMgr | Red, green and blue at level 96, 10 s each; TOP confirms, BOT refuses. Stops at the first colour not confirmed |
display | AO_Display | White, 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 |
tp | AO_Touch | The 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.
| Row | Owner | What passes |
|---|---|---|
psram | AO_Autotest | The 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 |
mem | AO_Autotest | int_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.
- 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.
- 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=passrecord, then the summary and>ok. - 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. - Run
test component=memlast.>|summary pass=1 fail=0 timeout=0and>okmean the internal pool held its floor. - 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
components/kaos/autotest: AO_Autotest, the table of rows and the PSRAM walk.kqp_config.h: the budgets and constants of every row.components/kaos/console: thetestverb and the records it prints.components/kaos/test_dot: AO_TestDot, which runstest component=all.