# 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

```mermaid
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:

- `kui_pack.c` in the kit parses and checks a manifest and a single Lottie, and is host-tested.
- `kgui_assets.c` in 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.json` is 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, `empty` and `loading`. They stay in memory while the source is active. The files of `writing` and `goodbye` are 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.

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`.

| 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:

- `default` is the scene the hour picks outside every window (the first scene when absent or unknown).
- `hours` is a list of windows `{from, to, scene}` with hours 0 to 23. A window starts at `from` and ends before `to`, `from` is less than `to`, 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 `writing` and `goodbye`, 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 than `sd` or `fat`, 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_info` with `op=<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_try` needs 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 with `config key="scene"`) or the hour moving the scene ends it.
- Install refusals use the words `busy`, `offline`, `url`, `room`, `download`, `hash`, `missing`, `write` and `move`, or a limit's name such as `depth`, `canvas` or `frames`.

`gui_info` logs `assets: source=<word> scenes=<n> ready=<yes|no>` and one `scene:` line per scene, with `(dead)` on a dead one.

## Gotchas

> **WARNING**
>
> **A pack that passes every check crashes the board whenever Kody draws it.** The checks judge form, not whether the renderer survives. `info` then reports the
> core dump, such as `coredump=present owner=kodeos task="gui"`. A pack on the card
> goes with the card. A pack on the FAT goes with `idf.py storage-flash`, which writes the FAT again with the pack that kodeOS ships.

> **NOTE**
>
> **A pack on the card hides the official pack completely.** Sources are never mixed, so the card's pack needs every scene and role you want, even if the FAT pack has more. A broken manifest on the card is skipped with a log line and the FAT pack plays.

> **NOTE**
>
> **The Update button is missing from the Kody page.** It needs Wi-Fi with an address and no image being written, and it updates the FAT, never the card. The small print on the page says which condition is unmet.

> **NOTE**
>
> **A scene name in `kgui/scene` is not in the active pack.** kodeOS lets the hour
> pick and keeps the key for a pack that has the scene. `config key="scene"` with a
> name the pack lacks answers `>err code=5 msg=value`.

## 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

- [`components/kui/pack`](https://github.com/kodediy/kode-sdk/tree/main/components/kui/pack): `kui_pack.c` and `kui_pack.h`, the manifest parser and the Lottie checks.
- [`components/kui/kody/pack`](https://github.com/kodediy/kode-sdk/tree/main/components/kui/kody/pack): the official pack's files.
- [`kgui_assets.c`](https://github.com/kodediy/KodeOS/blob/main/components/kgui/kgui_assets.c): reading, choosing, guarding and installing.
