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
How a folder becomes a row

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

KeyLimitUsed for
kodeRequired, must be "1"The format revision. Anything else is refused with revision
name31 charactersThe row and the details
category63 characters, a comma list, lowercased on readThe folder the app is filed in: the first item, in the plural
version15 charactersThe details of the app
description52 charactersThe details of the app
icon40 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
colorExactly #RRGGBBThe 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= statusMeaning
okParsed
noneNo app.json in the folder
sizeLonger than 1024 bytes
syntaxOutside the subset, or a field over its limit
revisionkode missing, or not "1"

The icon

ThingValue
Fileicon.k64, exactly 12288 bytes, no header
Colour plane64 by 64 pixels of RGB565, little-endian, row after row: 8192 bytes
Alpha plane64 by 64 bytes of transparency: 4096 bytes
TransferThe console sends it in 4096-byte parts, so exactly three
On screenA row draws it at the disc's size, 80 px; the details draw it at 64 px
Any other lengthRefused with size
No icon, or refusedThe 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

WayCommandWhat happens
In the containerpython3 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 appThe 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 cablepython3 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 readerCopy app.json and icon.k64 to /.kode/appsdata/<project>/ on the card, put the card in, open and close the settings screen or restartThe 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

CommandConsole verbWhat it prints
kcli.py appdata lsappdata_lsOne 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 scope

TBD: these commands have not been run on a Kode Dot from this page.

In the SDK