kodeOS internals

Bench tools

The computer-side tools that talk to a Kode Dot: the console over cable and Bluetooth, the live mirror, the app container and the release scripts.

What it is

The bench is the desk where you test a Kode Dot by hand, with a cable and a computer. The bench tools are the Python programs in the tools/ folder of the kodeOS repository. They run on the computer, and they talk to a board that runs kodeOS. With them you send console commands, install an app, take a screenshot, copy a file, change a setting and watch the screen live.

Each console client follows the console protocol exactly: one line is one command, every answer line starts with >, a record starts with >|, a terminator is >ok or >err code=N msg=token, and every other line is a ! log the computer did not ask for. A script of your own can copy what a client does instead of rewriting the line framing, the echo removal and the ANSI stripping. internals/console-protocol has the exact form.

How it is connected

flowchart LR
subgraph pc["On your computer"]
  kcli["kcli.py"]
  kble["kble.py"]
  kmirror["kmirror.py"]
  kpayload["kpayload.py"]
  kappdata["kappdata.py"]
  mkkfw["mkkfw.py"]
  kcli --> kpayload
  kble --> kpayload
  kcli --> kappdata
  mkkfw --> kappdata
end
subgraph dot["On the Kode Dot"]
  board["AO_Console"]
end
kcli -- "USB-C or charging base" --> board
kble -- "Bluetooth LE" --> board
kmirror -- "either road" --> board
class kcli,kble,kmirror,mkkfw app
class board ao
Which tool reaches the board by which road

kpayload.py and kappdata.py are modules, not commands.

kcli.py needs pyserial, which every ESP-IDF environment carries, so run it in a terminal where ESP-IDF is loaded. kble.py and the Bluetooth mode of kmirror.py build a private Python environment with bleak the first time they run. Both Bluetooth tools are written for macOS.

Specifications

The tools

ToolWhat it doesMain commands
kcli.pyThe console over the USB-C cable or the charging base. The reference client of the protocol-p PORT ping, cmd "app_list", install FILE --dst D [--no-run] (D is app, os, c5 or pending), screenshot -o FILE, file-get NAME -o FILE, file-put LOCAL REMOTE, assets-install DIR, config, config-export -o FILE, config-import FILE, appdata with ls, info, rm or put
kble.pyThe same console over Bluetooth. A bridge process keeps the link open between two commandsterm, cmd "app_list", install FILE --dst D (D is app, os or pending), screenshot -o FILE, file-get NAME, assets-install DIR, detach
kmirror.pyThe glass in a browser tab, live, with click-to-touch and a pad of six buttons-p PORT, --ble, --scale N (1 or 2), --q N (1 to 100), --open, --http-port N
kpayload.pyShared code: quote(), fields(), the base64 record decoder and the PNG writer. Standard library onlyImported, never run
kappdata.pyValidates an app.json and converts an icon to the board's .k64 formatImported by kcli.py and mkkfw.py
kblebundle.pyWhat Bluetooth needs on macOS: a Python environment with bleak and a signed .app that declares the Bluetooth permissionImported by kble.py and kmirror.py
mkkfw.pyWraps an ESP-IDF image in a .kfw container, optionally with an app's app.json and icon--target T (T is os, app or c5) [--name N] [--no-compress] [--appdata DIR] IN OUT

Options every kcli.py command takes:

kcli.py cmd takes several lines, --timeout S (default 5 s) and --gap SECONDS for a verb that answers >ok and then works for a while. kmirror.py finds the port itself when exactly one /dev/cu.usbmodem* exists.

The wire behaviour a script can rely on

BehaviourValue
Payload commandsinstall and file-put share one conversation: after >|ready chunk=N, one chunk of raw bytes per >|ack. The cable has no flow control of its own, so the sender never sends ahead of an acknowledgement
Chunk sizeRead from chunk= in the ready record; 4096 when absent
Wait for >|readyREADY_TIMEOUT 12 s, then one reopen of the port and a repeat of the request
Wait for the close after the last byteCLOSE_TIMEOUT 30 s: CRC, image verify and directory commit
install --dst c5A deadline on silence, C5_CLOSE_IDLE 45 s. Cable only. >ok means the C5 activated the image, not that it runs it
install --dst osThe board reboots into the new slot right after >ok; nothing answers for about 2 s
Screenshot and file-getBase64 inside ordinary >|d= records, so a ! log line in the middle does no harm and the verbs work over Bluetooth too. The idle limit is 15 s on the cable and 30 s over Bluetooth
config-importReads the board's records first in the same session, then sends config key= value= only for keys that differ, and prints key: old -> new
Bluetooth bridgeUnix socket under ~/Library/Caches/kode-kble/. First attach about 2.3 s, later ones about 0.18 s. It ends on detach, when the board drops the link, or when no board is found, never on idle
Mirror streamA 14-byte little-endian header (seq, x, y, w, h as u16, len as u32) then that many JPEG bytes. The viewer sends mirror_ack seq=N for every frame, or the stream stops after four

The scripts around them

ScriptWhat it doesPage
jtag_flash.shWrites the flash over JTAG with one OpenOCD that exitsinternals/building-and-flashing
build_c5.shBuilds the ESP32-C5 firmware into storage_image/c5_slave.bin and stamps its commitinternals/release-pipeline
assemble_release.sh, validate_release_tag.sh, check_on_main.shMake the release files, check a tag, check a commit is on maininternals/release-pipeline
check_layers.shThe include rules of every layerinternals/building-and-flashing
host_tests.sh, tool_tests.shThe SDK's host suites; the tools' own testsinternals/host-tests
check_os_size.sh, ci_report.sh, check_todo_refs.shImage size against its ceiling, one Markdown table of the build's four results, the to-do marker ruleinternals/test-pipeline
ram_budget.txt, misra_baseline.txtThe internal RAM budget and the MISRA baseline that the build's checks readinternals/test-pipeline
release/flash.sh, FACTORY_README.txt and THIRD_PARTY_NOTICES.txt for the factory packinternals/release-pipeline

Reading a core dump

idf.py -p PORT coredump-info reads the coredump partition at 0xC20000 (128 KB) over esptool. It needs the ELF of the image that crashed:

For a QP/C assert, pc= points inside panic_abort, and the backtrace names the failing code. Clear a dump after reading it with esptool erase-region 0xC20000 0x20000.

Gotchas

Example code

Install an app with its icon and name, then look at the glass. Run each line from the root of the kodeOS repository, with PORT replaced by your port.

  1. Wrap the image with its metadata: python3 tools/mkkfw.py --target app --name scope --appdata ./scope-folder build/scope.bin scope.kfw. One line prints the sizes of the image, the container, the app.json and the icon.
  2. Install it: python3 tools/kcli.py -p PORT --settle 5 install scope.kfw --dst app. The last line reads installed and booting.
  3. Capture the glass: python3 tools/kcli.py -p PORT --steal --settle 5 screenshot -o shot.png. It prints shot.png: WIDTHxHEIGHT, N bytes.
  4. Watch it live: python3 tools/kmirror.py -p PORT --open. A browser tab opens on http://127.0.0.1:8420/; a click is a tap.
  5. Run the tools' own tests, which need no board: tools/tool_tests.sh. It ends with tool tests, all passing.

In the SDK