kodeOS internals
App icon
Give an installed app an icon, a colour, a name, a version and a description with a small folder next to the app, sent over the cable or copied to the card.
What it is
In Apps, the launcher, every app is one row: a round disc on the left and the name of the app. The disc shows the app's picture. Without one it shows the apps glyph on the app's colour, or on Medium Grey when the app gave no colour. The version is in the details of the app, which TOP held opens.
You change all of that with a folder named after the app. The folder holds a text file, app.json, and a picture. Nothing is rebuilt: the launcher reads the folder at its next scan.
The folder sets six things: name, category, version, description, icon and color. Only kode, the format revision, is required. A key you leave out takes its value from the next source, the app's manifest or the board's defaults, and never becomes empty. internals/apps-region covers how the board reads the folder, with the events and the memory.
You can put the folder in the file that installs the app, a .kfw container, or send it to an app that is already installed. Both ways end in the same place on the board: /storage/.kode/appsdata/<project>/ on the internal FAT. With a card reader you can copy the same folder to /.kode/appsdata/<project>/ on the microSD card.
How it is connected
flowchart LR subgraph pc["On your computer"] dir["app.json<br/>+ icon.png"] -- "cable" --> put["kcli.py<br/>appdata put"] dir -- "container" --> kfw["mkkfw.py<br/>--appdata"] end subgraph board["On the Kode Dot"] am["AO_AppMgr<br/>install"] fat["FAT: /storage/.kode/<br/>appsdata/scope"] st["AO_Storage"] gui["AO_Gui<br/>the row"] am --> fat fat --> st --> gui end put --> fat kfw -- "install" --> am class dir app class am,st,gui ao class fat store
<project> is the project name of the app, the project_name that ESP-IDF writes into the .bin from the project() line of its CMakeLists.txt. It is the identity of the app, and it is not the name on the row. kcli.py cmd app_list and kcli.py appdata ls print it.
Two apps can share a display name, so the board finds the folder by the project name and nothing else.
The converter runs on the computer. The board decodes no PNG or JPEG, because decoding needs memory it does not have to spare.
tools/kappdata.py checks app.json against the limits below, cuts the picture to a square, scales it to 64 by 64 keeping its transparency, and writes the raw .k64 file. It needs Pillow (python3 -m pip install pillow) in the Python that runs kcli.py, and only for a PNG or JPEG.
The board looks for the folder on the FAT first and on the card second, and uses the first one it finds as a whole. It never takes a missing value from the card's folder. The console writes only to the FAT, because a microSD card that fails on write breaks every later read on that volume.
Specifications
app.json
One flat JSON object, every value a string, ASCII only (0x20 to 0x7E), at most 1024 bytes. The only escapes are \" and \\. There are no numbers, arrays or nesting. The board refuses the whole file on any fault and never applies part of it.
| Key | Limit | Used for |
|---|---|---|
kode | Required, must be "1" | The format revision. Anything else is refused with revision |
name | 31 characters | The row and the details |
category | 63 characters, a comma list, lowercased on read | The folder the app is filed in: the first item, in the plural |
version | 15 characters | The details of the app |
description | 52 characters | The details of the app |
icon | 40 characters, a relative path inside the folder, no leading /, no \, no .. | The round disc. Without this key the board reads icon.k64 in the folder. The host tools (appdata put, mkkfw.py) take the first file that exists of icon.png, icon.jpg, icon.jpeg, icon.k64, convert it and write the key as icon.k64 |
color | Exactly #RRGGBB | The ground of the disc when the app has no picture, in place of Medium Grey |
A key the reader does not know is ignored whatever its length, so a file from a newer tool works on an older board. A description of 53 characters makes the board refuse the whole file with syntax. The limit comes from the app sheet, and the parser enforces it.
json= status | Meaning |
|---|---|
ok | Parsed |
none | No app.json in the folder |
size | Longer than 1024 bytes |
syntax | Outside the subset, or a field over its limit |
revision | kode missing, or not "1" |
The icon
| Thing | Value |
|---|---|
| File | icon.k64, exactly 12288 bytes, no header |
| Colour plane | 64 by 64 pixels of RGB565, little-endian, row after row: 8192 bytes |
| Alpha plane | 64 by 64 bytes of transparency: 4096 bytes |
| Transfer | The console sends it in 4096-byte parts, so exactly three |
| On screen | A row draws it at the disc's size, 80 px; the details draw it at 64 px |
| Any other length | Refused with size |
| No icon, or refused | The apps glyph on the app's colour. The launcher draws it itself. The console still calls this value=monogram |
icon= status in appdata ls and status= on the icon line of appdata info: ok, none (no picture), size (not a 64 by 64 .k64), path (the icon key names a file that is not there), busy (the board was in the middle of a transfer; it asks again at the next scan).
Putting the folder on the board
| Way | Command | What happens |
|---|---|---|
| In the container | python3 tools/mkkfw.py --target app --name <project> --appdata <DIR> build/<project>.bin <project>.kfw, then python3 tools/kcli.py -p <port> install <project>.kfw --dst app | The tool validates the JSON and converts the picture on the computer, naming the field and writing no file if one is wrong, and rewrites the icon key as icon.k64. On install the board replaces the FAT folder whole. A container with no folder inside removes the app's folder, so an update never leaves the old icon. A bare .bin leaves it as it is |
| Over the cable | python3 tools/kcli.py -p <port> appdata put <project> <DIR> | Validates, converts, creates the FAT folder, uploads app.json and icon.k64 and prints what the board resolved. All in one session, because opening the port resets the board |
| With a card reader | Copy app.json and icon.k64 to /.kode/appsdata/<project>/ on the card, put the card in, open and close the settings screen or restart | The picture must already be .k64. put writes no .k64 on your computer, so read it back with kcli file-get from /storage/.kode/appsdata/<project>/ |
If the board cannot read the folder in a container it installs the app anyway and logs one warning.
Checking and removing
| Command | Console verb | What it prints |
|---|---|---|
kcli.py appdata ls | appdata_ls | One record per project: project=, medium=, json=, icon=, app=. A folder with no app is app=orphan |
kcli.py appdata info <project> | appdata_info name= | `project=... medium=fs |
kcli.py appdata rm <project> | appdata_rm name= | Deletes the FAT folder and its files. Leaves the card alone. Refuses a name that does not lead into /storage/.kode/appsdata/ |
Each field= line has src=: folder (your app.json), binary (the app's manifest or build), table (the board's category override table) or default (no source had a value). The icon line says value=icon or value=monogram (no picture), and never the path.
When no source has a category, appdata info prints other, and the launcher files the app in no folder: it is a loose row at the root of Apps, under the folders. An app whose first category is other is a loose row too.
Gotchas
Example code
An app.json for an installed app whose project name is scope. The key icon is left out, so the host tool takes icon.png from the same folder.
{
"kode": "1",
"name": "Scope",
"category": "tools",
"version": "0.4",
"description": "Two-channel oscilloscope on the expansion header.",
"color": "#8E8E93"
}Put the folder on the board and check it. The first command validates, converts icon.png and uploads both files. The answer of the second shows src=folder on every field and status=ok on the icon line.
# app_icon.sh - 2026-10-28 - from Kode with love
#
# Sends the folder scope-folder (app.json and icon.png) to a Kode Dot for the
# installed app "scope", then asks the board what it resolved. Every field
# line of the answer ends with src=folder and the icon line with status=ok.
# Opening the port resets the board, so each command opens its own session.
python3 tools/kcli.py -p /dev/cu.usbmodem101 --settle 5 appdata put scope ./scope-folder
python3 tools/kcli.py -p /dev/cu.usbmodem101 --settle 5 appdata info scopeTBD: these commands have not been run on a Kode Dot from this page.
In the SDK
components/kdrv/apps/src/kapp_data.c: theapp.jsonreader on the board.tools/kappdata.py: validation and the.k64converter.tools/mkkfw.py: packs a folder into a.kfwcontainer.