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
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
| Tool | What it does | Main commands |
|---|---|---|
kcli.py | The 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.py | The same console over Bluetooth. A bridge process keeps the link open between two commands | term, cmd "app_list", install FILE --dst D (D is app, os or pending), screenshot -o FILE, file-get NAME, assets-install DIR, detach |
kmirror.py | The 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.py | Shared code: quote(), fields(), the base64 record decoder and the PNG writer. Standard library only | Imported, never run |
kappdata.py | Validates an app.json and converts an icon to the board's .k64 format | Imported by kcli.py and mkkfw.py |
kblebundle.py | What Bluetooth needs on macOS: a Python environment with bleak and a signed .app that declares the Bluetooth permission | Imported by kble.py and kmirror.py |
mkkfw.py | Wraps 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:
-p PORT(required).-vto show logs and echo while it waits.--settle Sto wait S seconds after the open so a rebooting board finishes.--stealto terminate whatever holds a busy port and retry.
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
| Behaviour | Value |
|---|---|
| Payload commands | install 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 size | Read from chunk= in the ready record; 4096 when absent |
Wait for >|ready | READY_TIMEOUT 12 s, then one reopen of the port and a repeat of the request |
| Wait for the close after the last byte | CLOSE_TIMEOUT 30 s: CRC, image verify and directory commit |
install --dst c5 | A deadline on silence, C5_CLOSE_IDLE 45 s. Cable only. >ok means the C5 activated the image, not that it runs it |
install --dst os | The board reboots into the new slot right after >ok; nothing answers for about 2 s |
Screenshot and file-get | Base64 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-import | Reads 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 bridge | Unix 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 stream | A 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
| Script | What it does | Page |
|---|---|---|
jtag_flash.sh | Writes the flash over JTAG with one OpenOCD that exits | internals/building-and-flashing |
build_c5.sh | Builds the ESP32-C5 firmware into storage_image/c5_slave.bin and stamps its commit | internals/release-pipeline |
assemble_release.sh, validate_release_tag.sh, check_on_main.sh | Make the release files, check a tag, check a commit is on main | internals/release-pipeline |
check_layers.sh | The include rules of every layer | internals/building-and-flashing |
host_tests.sh, tool_tests.sh | The SDK's host suites; the tools' own tests | internals/host-tests |
check_os_size.sh, ci_report.sh, check_todo_refs.sh | Image size against its ceiling, one Markdown table of the build's four results, the to-do marker rule | internals/test-pipeline |
ram_budget.txt, misra_baseline.txt | The internal RAM budget and the MISRA baseline that the build's checks read | internals/test-pipeline |
release/ | flash.sh, FACTORY_README.txt and THIRD_PARTY_NOTICES.txt for the factory pack | internals/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:
inforeportscoredump=withowner=kodeos: usebuild/kodeos.elf.owner=other: the dump belongs to a sketch or another build, and you need that image's ELF.
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.
- 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, theapp.jsonand the icon. - Install it:
python3 tools/kcli.py -p PORT --settle 5 install scope.kfw --dst app. The last line readsinstalled and booting. - Capture the glass:
python3 tools/kcli.py -p PORT --steal --settle 5 screenshot -o shot.png. It printsshot.png: WIDTHxHEIGHT, N bytes. - Watch it live:
python3 tools/kmirror.py -p PORT --open. A browser tab opens onhttp://127.0.0.1:8420/; a click is a tap. - Run the tools' own tests, which need no board:
tools/tool_tests.sh. It ends withtool tests, all passing.
In the SDK
tools: every script on this page, in the kodeOS repository.tools/tests: the tests for the tools. They need no board and no ESP-IDF.components/kaos/console: AO_Console, the board side of everything the clients send.