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
From files to Kody

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:

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:

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:

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.

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.

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.

  1. Stage. Files go into /storage/kode.new.
  2. 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 .ok and 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.

KeyNeededMeaning
formatyesThe number 1. Anything else refuses the manifest
name, versionnoText of at most 31 characters, shown by About and assets_info; - when absent
scenesyesThe 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
facenoHow the hour picks a scene (below)
empty, writing, goodbye, loadingnoOne file for each role outside the face
sha256noA 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 keyNeededMeaning
nameyes1 to 15 characters of a-z, 0-9 and _, unique in the pack. The store keeps it in kgui/scene
wakeyesPlays once when Kody wakes in the scene. Its last frame is frame 0 of the loop
loopyesRepeats while Kody is awake
anchornoFrames 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
onnoA 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:

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:

The roles

The canvas each role needs and where it plays.

RoleCanvasPlays on
wake, loop, look files410 x 300The face (internals/gui/face)
empty410 x 369The empty launcher (internals/gui/launcher)
writing410 x 160No screen plays it: the write screen shows an edge and a figure (internals/gui/sheets)
goodbye410 x 160No screen plays it: an app runs when the edge has closed
loading264 x 264The loading screen of the guided tests and the self-test

A role the pack leaves out changes the screen:

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.

WhatLimit
kode.json256 KB (KUI_PACK_MANIFEST_MAX)
Scenes in a manifest32 (KUI_PACK_SCENES_MAX)
Reactions, all scenes together128 (KUI_PACK_REACT_MAX)
A scene name1 to 15 characters of a-z, 0-9, _
A file name1 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 stored64 KB (KUI_PACK_FILE_MAX)
A file's JSON, inflated96 KB (KUI_PACK_JSON_MAX)
CanvasExactly the canvas of its role
Frames1 to 1,800, and a multiple of the anchor when the file is a loop with one
Frame rateA whole number, 1 to 60
JSON nesting32 levels (KUI_PACK_MAX_DEPTH)
Precomposition chain2 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.

ThingValue
Location on a new board/storage/kode, staged by the root CMakeLists.txt from components/kui/kody/pack/ into the storage image
Version2026.11.05.2
Files69 animations plus kode.json
Size195,878 bytes of streams; 207,389 bytes with the manifest
Scenes, in ordertren, parque, camping, azotea, madrugada, desayuno, taller, lluvia, playa, reloj
The hourNo windows, default tren. kgui draws the hour's scene itself (internals/gui/face)
Anchors45 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
Rolesempty is duda, writing is portatil, goodbye is despide, loading is cargando
Sourcecomponents/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.

FunctionWhat it doesLine stays openRecords
assets_infoShows the active pack5 ssource=sd|fat|builtin name version scenes, scene=… anchor= look= dead= per scene, role=… file= per role
assets_check arg=sd or arg=fatChecks every file on that medium; activates nothing60 sfile=… result=ok|refused|missing|bad_hash, then op=check result=done ok= refused=
assets_reloadChooses the source again60 sThe assets_info source record, after source=… result=refused why= for each refused source
assets_begin arg=<bytes>Stages an install of that many bytes60 sop=begin result=ready or refused why=room
assets_commitChecks the staged pack and makes it the FAT pack120 sfile=… result=ok|bad_hash|missing|refused, then op=commit result=done
assets_update, optional URLFetches, stages and commits240 sfile=… 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 loop60 sop=try file=… result=playing or refused why=

Errors and refusals:

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:

  1. Put kode.json and the files it names in one folder, DIR. Name the scenes with a-z, 0-9 and _, and draw each animation for its role's canvas.
  2. Run python3 tools/kcli.py -p PORT assets-install DIR over the USB-C cable, or python3 tools/kble.py assets-install DIR over Bluetooth. The tool adds the digests the manifest lacks and refuses a file name the board would refuse.
  3. Watch the records. assets_begin answers op=begin result=ready, each file_put follows, and assets_commit ends with op=commit result=done.
  4. Run assets_info. It reports source=fat and your pack's name and version.
  5. 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