kodeOS internals
Kody's pack
How Kody's animations ship as a folder of files: the manifest, the limits every file passes, which pack plays, and how to install one.
What it is
Kody's animations are files, not part of the kodeOS image, except for one
scene. The folder of files is a pack, and a manifest, kode.json, names the scenes of the face and the animations of the other screens. A pack lets you add a scene without building a new kodeOS. The official pack is on the internal FAT of every Kode Dot as it ships and updates with assets_update. TBD: kodeOS updating the pack by itself after an OS update is in progress; this page says how once it lands. You can put a pack of your own on the microSD card.
kodeOS checks every file against a set of limits before it plays it, and plays one source only. A bad file is refused with a reason in the log. The checks judge the form of a file. A file that passes them and still crashes the renderer crashes the board each time Kody draws it.
How it is connected
flowchart LR subgraph media["Media"] sd["/sdcard/kode"] fat["/storage/kode"] end sd --> stg["AO_Storage"] fat --> stg stg --> assets["kgui_assets.c"] net["AO_Net<br/>assets_update"] --> assets assets -- "kui_pack_parse<br/>kui_pack_check_anim" --> player["kui_kody players"] class sd store class fat store class stg ao class net ao
Who does what
A pack is one folder named kode at the root of a medium, flat, with no
subfolders. Two pieces of code handle it:
kui_pack.cin the kit parses and checks a manifest and a single Lottie, and is host-tested.kgui_assets.cin kgui does the rest. It reads the files through AO_Storage and AO_Net one operation at a time, chooses the source, guards it and installs it.
kgui_assets.c runs inside AO_Gui's steps and reaches AO_Gui through one
callback.
The files
A file is a Lottie in one of two forms:
.json, as an animation program exports it. This is the form to drop on a card..json.z, the same JSON minified and deflated into one zlib stream. The official pack uses this form.
The manifest never repeats an animation's width, height, frame count or rate.
They come from the Lottie's w, h, op - ip and fr.
One source plays
kodeOS picks the pack on the card if a card is mounted and its manifest passes,
then the pack on the internal FAT, then the built-in reloj scene. It never
mixes files from two sources. A card pack that passes hides the whole FAT pack
even if it has fewer scenes. A broken manifest on the card is no error: the log
says why and the FAT pack plays.
kgui chooses the source in four cases:
- When the face starts.
- On
assets_reload. - After an install commits.
- When a card-pack file is missing and
kode.jsonis missing with it.
Dead scenes
A manifest that passes makes its source usable, and files are checked later, when they are about to play. A scene whose wake-up or loop is refused is dead.
- A step over scenes skips a dead scene.
- The face shows the built-in scene while it wants a dead one.
- A refused reaction leaves its trigger's list.
- A refused role file shows nothing.
When files are read
kgui never opens a file. It asks AO_Storage, which owns both media, with the same reading conversation the launcher uses, and copies each chunk into PSRAM.
- At the choice, it reads the manifest and the files of the two roles a screen plays,
emptyandloading. They stay in memory while the source is active. The files ofwritingandgoodbyeare named in the manifest and are not read. - A scene's wake-up and loop are read when Kody is about to play them.
- A reaction is read when its trigger fires.
Each file is judged once, when it arrives. A deflated one is inflated into a scratch block for the checks and freed, and the player inflates it again at each play.
AO_Storage refuses an open the same way when the reader is taken and when the
file is empty, so kgui asks a stat which it was. An empty file is refused
(why=size). A taken reader is asked again a few times and then waits, never
dead for that.
An install
An install writes the internal FAT only. It stages, then commits.
- Stage. Files go into
/storage/kode.new. - Commit. kodeOS reads the staged manifest, checks that it passes and
carries a digest for every file it names, checks every file is there,
matches its digest and passes the limits, and writes
/storage/kode.new/.ok. Then it removes the files the active manifest names from/storage/kode, moves the new ones in, the manifest last, removes.okand chooses the source again.
A failed check leaves /storage/kode as it was. If the power fails during the
moves, .ok is still there at the next boot, kgui finishes the moves and logs
assets: the interrupted install is finished.
A staged folder without .ok stays as it is, and the next install overwrites
the files it names. A file that no manifest names is never read and never
removed.
Specifications
The manifest
The top-level keys of kode.json.
| Key | Needed | Meaning |
|---|---|---|
format | yes | The number 1. Anything else refuses the manifest |
name, version | no | Text of at most 31 characters, shown by About and assets_info; - when absent |
scenes | yes | The scenes of the face, 1 to 32. The row Kody's scene on the Settings list steps through them in this order, after auto, and wraps. config key="scene" picks one by name |
face | no | How the hour picks a scene (below) |
empty, writing, goodbye, loading | no | One file for each role outside the face |
sha256 | no | A table from file name to lower-case hex SHA-256. The installer needs it; reading a pack ignores it |
The keys of one scene:
| Scene key | Needed | Meaning |
|---|---|---|
name | yes | 1 to 15 characters of a-z, 0-9 and _, unique in the pack. The store keeps it in kgui/scene |
wake | yes | Plays once when Kody wakes in the scene. Its last frame is frame 0 of the loop |
loop | yes | Repeats while Kody is awake |
anchor | no | Frames between two anchors of the loop, 1 to 255. Absent or 0 means no anchors, and a scene with no anchors has no reactions even if on lists files |
on | no | A table from a trigger name to a list of files. The triggers are look, hour, notice, plug and joke (internals/gui/face). With several files kodeOS picks one at random each time |
The face key has two parts:
defaultis the scene the hour picks outside every window (the first scene when absent or unknown).hoursis a list of windows{from, to, scene}with hours 0 to 23. A window starts atfromand ends beforeto,fromis less thanto, and none crosses midnight.
The first window that holds the local hour wins, and a window naming no scene is dropped with a log line.
More rules of the manifest:
- An unknown key, role or trigger is ignored at every level and reported at debug level, so a pack made for a newer kodeOS plays on an older one.
- At most 128 reactions may be named across all scenes.
- One file can fill two places only when both places have the same canvas and the same anchor.
The roles
The canvas each role needs and where it plays.
| Role | Canvas | Plays on |
|---|---|---|
wake, loop, look files | 410 x 300 | The face (internals/gui/face) |
empty | 410 x 369 | The empty launcher (internals/gui/launcher) |
writing | 410 x 160 | No screen plays it: the write screen shows an edge and a figure (internals/gui/sheets) |
goodbye | 410 x 160 | No screen plays it: an app runs when the edge has closed |
loading | 264 x 264 | The loading screen of the guided tests and the self-test |
A role the pack leaves out changes the screen:
- With no
empty, the empty launcher is the QR alone. - The pack format still names
writingandgoodbye, but nothing on the board plays them, so a pack without them loses nothing. - With no
loading, the loading screen is its line.
The limits
A file over a limit is refused and the log names the file and the reason word:
size, depth, json, format, schema, name, file, hash, canvas,
frames, fps, precomp or memory.
| What | Limit |
|---|---|
kode.json | 256 KB (KUI_PACK_MANIFEST_MAX) |
| Scenes in a manifest | 32 (KUI_PACK_SCENES_MAX) |
| Reactions, all scenes together | 128 (KUI_PACK_REACT_MAX) |
| A scene name | 1 to 15 characters of a-z, 0-9, _ |
| A file name | 1 to 40 characters of A-Z, a-z, 0-9, _, ., -. It does not start with . and ends in .json or .json.z |
| A file as stored | 64 KB (KUI_PACK_FILE_MAX) |
| A file's JSON, inflated | 96 KB (KUI_PACK_JSON_MAX) |
| Canvas | Exactly the canvas of its role |
| Frames | 1 to 1,800, and a multiple of the anchor when the file is a loop with one |
| Frame rate | A whole number, 1 to 60 |
| JSON nesting | 32 levels (KUI_PACK_MAX_DEPTH) |
| Precomposition chain | 2 levels, no cycle |
A precomposition is a layer that holds a group of layers. The Lottie lists the groups in assets and a layer points to one with refId.
The nesting limit protects AO_Gui's stack, because ThorVG recurses as deep as the Lottie nests. The depth is counted first by a scan that does not recurse and counts brackets outside strings, so the checker cannot overflow the stack it protects.
The official pack
The pack that ships on every Kode Dot.
| Thing | Value |
|---|---|
| Location on a new board | /storage/kode, staged by the root CMakeLists.txt from components/kui/kody/pack/ into the storage image |
| Version | 2026.11.05.2 |
| Files | 69 animations plus kode.json |
| Size | 195,878 bytes of streams; 207,389 bytes with the manifest |
| Scenes, in order | tren, parque, camping, azotea, madrugada, desayuno, taller, lluvia, playa, reloj |
| The hour | No windows, default tren. kgui draws the hour's scene itself (internals/gui/face) |
| Anchors | 45 frames on tren and reloj, 135 on the others. Every scene has one file each for look, hour, notice and plug; the first five also have joke |
| Roles | empty is duda, writing is portatil, goodbye is despide, loading is cargando |
| Source | components/kui/kody/assets, written by components/kui/tools/kodygen.py |
The console functions
kgui registers seven functions. All answer in records: each line a job says is the record >|assets, and the job's end closes the line with >ok or >err. While a line is open every other command answers >err code=4 msg=busy.
| Function | What it does | Line stays open | Records |
|---|---|---|---|
assets_info | Shows the active pack | 5 s | source=sd|fat|builtin name version scenes, scene=… anchor= look= dead= per scene, role=… file= per role |
assets_check arg=sd or arg=fat | Checks every file on that medium; activates nothing | 60 s | file=… result=ok|refused|missing|bad_hash, then op=check result=done ok= refused= |
assets_reload | Chooses the source again | 60 s | The assets_info source record, after source=… result=refused why= for each refused source |
assets_begin arg=<bytes> | Stages an install of that many bytes | 60 s | op=begin result=ready or refused why=room |
assets_commit | Checks the staged pack and makes it the FAT pack | 120 s | file=… result=ok|bad_hash|missing|refused, then op=commit result=done |
assets_update, optional URL | Fetches, stages and commits | 240 s | file=… result=fetched|kept|bad_hash, then the commit records |
assets_try arg="<file>" | Plays one file of the active pack on the face in a loop | 60 s | op=try file=… result=playing or refused why= |
Errors and refusals:
- A bad argument answers
>err code=3 msg=arg: a word other thansdorfat, a byte count outside 1 to 2147483647, a URL that does not fit (127 characters) or a name that cannot be a pack file. - A full queue answers
>err code=5. - Any other refusal is a record followed by
>err code=5 msg=<function>. - Another job, a pack not yet read at boot, or an image being written refuse everything but
assets_infowithop=<function> result=refused why=busy, and the host tools ask again. - A step that gets no answer for 20 s ends with
result=timeout. assets_tryneeds the face on the glass with Kody awake (why=face), refuses under the built-in scene (why=builtin), and choosing a scene (on the Settings list or withconfig key="scene") or the hour moving the scene ends it.- Install refusals use the words
busy,offline,url,room,download,hash,missing,writeandmove, or a limit's name such asdepth,canvasorframes.
gui_info logs assets: source=<word> scenes=<n> ready=<yes|no> and one scene: line per scene, with (dead) on a dead one.
Gotchas
Example code
Install a pack of your own on the Kode Dot from a computer:
- Put
kode.jsonand the files it names in one folder,DIR. Name the scenes witha-z,0-9and_, and draw each animation for its role's canvas. - Run
python3 tools/kcli.py -p PORT assets-install DIRover the USB-C cable, orpython3 tools/kble.py assets-install DIRover Bluetooth. The tool adds the digests the manifest lacks and refuses a file name the board would refuse. - Watch the records.
assets_beginanswersop=begin result=ready, eachfile_putfollows, andassets_commitends withop=commit result=done. - Run
assets_info. It reportssource=fatand your pack's name and version. - Put the Kode Dot on the face. Kody wakes in the scene of the hour, or in the one you pick with
config key="scene" value="<name>".
In the SDK
components/kui/pack:kui_pack.candkui_pack.h, the manifest parser and the Lottie checks.components/kui/kody/pack: the official pack's files.kgui_assets.c: reading, choosing, guarding and installing.