kodeOS internals

Apps region

How kodeOS stores installed apps in flash: the region, the mailbox, install and eviction, and the app data folder with the icon and colour of each app.

What it is

The apps region is the part of the flash where kodeOS keeps the apps you install. It is the apps partition, 0xC40000, 11.75 MB, and it holds up to 64 apps. Each app is a complete firmware image. The bootloader starts one in place, without copying it, so installing an app is writing it once and starting it is a reset.

Three things sit around the region.

sdk/kdrv/apps has the byte formats: the directory, the request, the .kfw container and the manifest. This page is how kodeOS uses them. AO_AppMgr owns the content of the region and the mailbox (KBSP_RES_APPS_REGION, KBSP_RES_APP_PENDING). AO_Boot owns the boot request sector (KBSP_RES_APPS_BOOTREQ). AO_Storage reads the app data folder.

How it is connected

flowchart LR
host["Computer<br/>esptool"]
subgraph board["On the Kode Dot"]
  src["Launcher, console,<br/>URL"]
  mb["app_pending"]
  am["AO_AppMgr"]
  reg["apps region"]
  boot["AO_Boot<br/>boot request"]
  bl["Bootloader<br/>override"]
  fat["FAT<br/>.kode/appsdata"]
  mb -- "adopt" --> am
  src -- "install" --> am
  am -- "write" --> reg
  am -- "request" --> boot
  boot --> bl
  bl -- "jump" --> reg
  am --> fat
end
host -- "write-flash" --> mb
class host ext
class mb,reg,fat store
class am,boot ao
How an image reaches the region and starts

How an install behaves

An install works by name. The identity of an app is the project_name in its image header. An install with the name of an installed app replaces it in one directory commit, so a second install does not use a second entry. A new name is added. The installs that come from a file, the card, a URL, the console or the mailbox all end in the same code in AO_AppMgr.

The allocator is next-fit and the eviction is first in, first out. kdrv_apps_plan() puts a new image right after the newest one by install order, searching to the end of the region and then wrapping to the start. When nothing fits, the old copy of the app being replaced goes first, then the oldest apps until the image fits. So a new version of an installed app costs no other app unless it is larger than the room its old copy leaves. kodeOS keeps no record of which app you used last, so it has nothing else to rank by.

When all 64 directory entries hold other names, the oldest entry goes too. AO_AppMgr logs each eviction, evicting '<name>' to make room; its copy is on the card as a notice or evicting '<name>' to make room; no copy kept as a warning, publishes APP_EVICTED_SIG for it, and commits the directory, with the victims' flags cleared, before it touches the flash. The old copy of the app being replaced is not an eviction: the log says the old copy of the app being replaced makes room for its new version.

An eviction keeps a copy on the card when it can. If an install from a file, the card, a URL or the mailbox must evict, AO_AppMgr first copies each victim to /sdcard/<project>.bin (skipping one that is already there) and then restarts the install. The victim shows in the launcher as a file you can install again. If a copy fails and the install came from the card, the install fails and nothing is evicted. A terminal install cannot pause for this, so its victims are evicted without a copy.

The write is paced. AO_AppMgr writes an image in 32 KB chunks, spaced by a 10 ms gap, so the lower priorities (the screen included) get the CPU between erases. A write holds the cache down while it erases, and nothing in flash runs then.

Adoption copies the mailbox into the region at boot. In its initial transition AO_AppMgr asks whether the mailbox holds something worth copying. If it does, it posts APP_ADOPT_SIG to itself, because a copy of several megabytes must not run inside an initial transition. The copy goes through a static 4 KB buffer.

The app data folder is read by the owner of the media. AO_Storage finds the folder, reads app.json and the icon and posts the answer to kgui as events. A row in the launcher resolves each field on its own. For each one it takes the first source that has a value.

Specifications

The region

ThingValue
Partition appsData, subtype 0x40, 0xC40000, size 0xBC0000 (11.75 MB), up to the FAT at 0x1800000
Partition app_pendingData, subtype 0x41, 0x1A00000, 6 MB, non-executable
Directory copiesA at region offset 0x0000, B at 0x1000, one 4 KB sector each
Boot requestRegion offset 0x2000, 20 bytes in a 4 KB sector
First imageRegion offset 0x10000 (KAPPS_DATA_OFF); every image starts on a 64 KB boundary (KAPPS_ALIGN)
AppsUp to 64 (KAPPS_MAX_ENTRIES); the launcher lists up to 64 of each kind (KODE_APP_LIST_MAX)
Largest installThe data area: 11.75 MB less the 64 KB in front of it
Largest mailbox push0x5FF000 bytes: 6 MB less the sector that holds the intent word
EvictionThe old copy of the app being replaced first, then the oldest install, by seq
Directory commitWrites the copy not in use with gen plus 1 and a new CRC, then swaps. A power cut mid-commit leaves the old directory

The region ends where the FAT begins and crosses the 16 MB mark on the way. That is legal because the cache maps 32-bit addresses (CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH). To grow the region, space has to come from storage.

Adoption of the mailbox

AO_AppMgr consumes the mailbox (it erases the first sector and the intent sector) only when the outcome is final, so a failed directory write is retried at the next boot.

CaseWhat AO_AppMgr does
The image fails esp_image_verify() in the regionThe result repeats every time, so it consumes the mailbox and does not retry
The intent sector holds KOTANo directory is involved. It checks the image fits the free kodeOS slot, writes it there, consumes the mailbox before the reset, then asks AO_Boot to switch slot
A plain appOne install by name in one commit, evicting the oldest app first if the 64 entries are full. It consumes the mailbox after the commit, then launches the app with a sticky request (KBOOT_RUN_STICKY, state ALWAYS). This is the only place that state is written

install src="pending" dst=os|app runs the same adoption with the destination named by the command. For dst=app, run=on writes a NEW request, never ALWAYS.

Verification

CheckWhenWhereWhat it checks
kdrv_apps_pending_present()At boot, before the copyThe mailboxThe length only: the segment headers of a bare image, or the header of a .kfw container. It catches a torn upload. No hash
kdrv_apps_pending_verify()Only when the install must evict an installed appThe mailboxThe full esp_image_verify(), so a broken upload costs nobody an app
esp_image_verify() in kdrv_apps_write_end()After the copyThe regionThe full check with the hash. An app starts only after this passes

The app data folder

ThingValue
Path on the FAT/storage/.kode/appsdata/<project>/
Path on the card/sdcard/.kode/appsdata/<project>/
<project>The project_name of the app, up to 31 characters, from the characters A-Z a-z 0-9 _ - . only. Any other name has no folder
Which medium winsThe FAT first, then the card. The first folder found is used whole; fields are never merged across media
Filesapp.json (at most 1024 bytes) and the icon icon.k64 (exactly 12288 bytes)
Cost on the FAT20 KB of 4 KB clusters for a folder with an icon: one for the folder, one for app.json, three for the icon
Console writesTo the FAT only. A microSD card shipped with some units fails on write, and one failed write breaks every later read on that volume
Icon format64 by 64 RGB565 little-endian (8192 bytes), then a 64 by 64 alpha plane (4096 bytes). No header. A row draws it at the 80 px disc; the details draw it at 64 px
DecodingThe board decodes no PNG or JPEG. tools/kappdata.py converts on the computer

app.json is one flat object of string values. These are its keys:

KeyLimit
kodeRequired, "1"
name31 characters
category63 characters, a comma list lowercased on read
version15 characters
description52 characters, in the details of the app
icon40 characters, a relative path with no ..
color#RRGGBB

Unknown keys are ignored. The whole file is refused with size, syntax or revision on any fault, and the launcher then draws the app as if it had no folder. internals/app-icon has the steps.

Who writes the folder, and what an install does to it:

What is installedThe folder
A .kfw container with an app data block (KFW_FLAG_APPDATA)Replaced whole by the block's app.json and icon
A container without a blockDeleted
A bare .bin image (it starts with 0xE9)Left as it is

The install ends only after AO_Storage has answered, so a launch never runs before the folder exists. A write that fails is one warning and the install still succeeds. An erased app takes its folder with it. An install never touches a folder on the card.

Which source gives each field in the launcher:

Field1st2nd3rdDefault
NameFolderImage manifestProject name
CategoryFolderapp_cat override tableImage manifestEmpty (the app is a loose row at the root of Apps)
VersionFolderThe image's esp_app_desc_t.version (installed apps only)Empty
DescriptionFolderEmpty
IconFolder icon pathFolder icon.k64The apps glyph on the app's colour (the console calls it monogram)
ColourFolderMedium Grey

The identity of a row is always the project name. The folder changes only its label.

The answer to a request for one folder is three posted events, in this order:

  1. APPDATA_FIELDS_SIG with every field and two statuses (json_status: ok, none, size, syntax, revision; icon_status: ok, none, size, path, busy).
  2. Zero or three APPDATA_ICON_SIG blocks of 4096 bytes each.
  3. APPDATA_END_SIG.

AO_Storage takes the three icon blocks before it posts anything, so an icon arrives complete or not at all. While a console upload holds the jumbo pool, the fields event still goes out with icon_status=busy and kgui asks again at the next scan.

kgui keeps one 12 KB PSRAM buffer for each of its 128 list entries (64 installed apps and 64 files), about 1.5 MB, taken once.

Console

CommandWhat it does
install src="terminal" dst=app run=on name="<project>"Streams a .bin or .kfw over the console into the region (kcli.py install FILE --dst app). name is the image's project name, which the board cannot read before the room is made; kcli and kble send it. Each app the install evicts prints >|evicted name="<project>" kept=off before >|ready
install src="pending" dst=appAdopts the mailbox now
app_list, app_run name="<project>", app_erase name="<project>"List, start with a NEW request, erase
appdata_ls, appdata_info name=, appdata_rm name=List every folder and orphan, show what the board resolved for one app, delete the FAT folder of one app
kcli.py appdata put NAME DIRValidate on the computer, convert the icon, create the folder and upload it in one session

Gotchas

Example code

Install an app over the cable, start it and read the board's list. Each command prints what it did.

    1. Install and start: python3 tools/kcli.py -p <port> --settle 5 install build/my_app.bin --dst app. The tool prints installed and booting and the app starts.
    2. Hold BOT and LEFT for about one second to go back to kodeOS. Apps opens with the app in focus, in its folder if it names a category.
    3. List the region: python3 tools/kcli.py -p <port> --settle 5 cmd app_list. One record per installed app comes back, then ok.
    4. Start it from the console: python3 tools/kcli.py -p <port> --settle 5 cmd 'app_run name="my_app"'. The name is the project() name of the build.

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

In the SDK