SDK
Apps region and containers
How a Kode Dot stores installed apps and kodeOS updates in flash: the A/B directory, the boot request, the mailbox, .kfw containers and the app manifest.
What it is
An app on a Kode Dot is a whole firmware, not a plugin. kodeOS writes its .bin into the flash region apps, and a small bootloader override jumps to it in place. Any reset brings the board back to kodeOS, because the app never touches the partition that says which kodeOS to start.
The module kdrv/apps is everything that handles those images:
| Part | File | Job |
|---|---|---|
| The apps region | kdrv_apps.c | The A/B directory, the allocator, image writes and checks, the boot request |
| The mailbox | kdrv_apps.c | app_pending: an image a computer drops with esptool, adopted at the next boot |
| kodeOS slots | kdrv_slot.c | factory, ota_0, ota_1 and otadata: where kodeOS itself lives |
| Containers | kdrv_kfw.c | The .kfw format and a streaming decoder in front of any of these destinations |
| The manifest | kapp_desc.c | The display name and categories an app declares inside its image |
| Category overrides | kapp_cat.c | The board's own category table, kept in NVS |
| The app folder | kapp_data.c | The reader for an app's app.json |
Two Active Objects use it. AO_AppMgr owns the directory, the images, the mailbox and the write to the free kodeOS slot (sdk/kaos/appmgr). AO_Boot owns the boot request and otadata (sdk/kaos/boot). The design of the region and the boot model are on internals/apps-region and internals/boot-model.
How it is connected
flowchart LR subgraph aos["Active Objects"] am["AO_AppMgr"] boot["AO_Boot"] end subgraph drv["kdrv/apps"] kfw["kdrv_kfw<br/>decoder"] end subgraph flash["Flash destinations"] apps["kdrv_apps<br/>apps region"] slot["kdrv_slot<br/>free OTA slot"] mb["kdrv_apps<br/>app_pending"] end am --> kfw kfw --> apps kfw --> slot kfw --> mb boot --> apps boot --> slot apps -- "kapps_fmt.h" --> bl["bootloader override"] class am ao class boot ao class apps store class slot store class mb store
kapps_fmt.h is the one header the driver and the bootloader share, word for word, so it includes nothing from ESP-IDF. It holds the magic numbers, the layout, the boot request and the rules both sides apply to them, as static inline functions. The bootloader and kodeOS are updated apart, so that layout never moves.
Why the directory has two copies
Flash only clears bits. An erase sets every bit of a 4 KB sector to 1, and a write can only change a 1 to a 0. Two designs follow:
- The directory is kept twice, because a power cut in the middle of one sector write must leave a whole copy.
- The boot request moves from one state to the next with a single 4-byte write and no erase, because each new state only clears more bits.
The directory has two copies, A and B. kdrv_apps_dir_commit() never touches the copy believed valid. It writes the other one with gen one higher and a fresh CRC, waits for the write, and only then swaps its RAM copy. Inside a copy, the magic word is written last, so a cut leaves a sector that reads blank or corrupt and never one that looks whole. The commit refuses (KODE_DENIED) a directory that kapps_dir_sane() rejects, so the driver never seals a bad directory with a good CRC.
Init sorts each copy into one of four kinds. kdrv_apps_init() is idempotent: AO_Boot and AO_AppMgr both call it and whoever runs first pays the read. It also starts the category table first, so overrides are visible from the first manifest read.
| Kind | Meaning |
|---|---|
| ok | Magic, version and CRC hold, count fits the array of 64, and every live entry is an image the region could boot |
| blank | The magic reads as erased flash |
| corrupt | Another magic, a CRC that fails, or a copy that fails kapps_dir_sane() |
| foreign | The sector could not be read, or has the region's magic and another format version, as a different kodeOS would write |
If either copy is foreign, the driver uses neither and formats nothing. It logs apps region directory unreadable or of another format, returns KODE_ERROR and leaves the region shut: every call that needs the partition refuses. A later init looks again. Otherwise the ok copy with the highest gen wins (A wins a tie). When neither is ok, the region is formatted: an empty directory with gen 1 goes to A and B is erased so it cannot pass for newer.
Placing and writing an image
The allocator is log-structured and next-fit. kdrv_apps_plan() touches no flash and keeps no state, so the host tests run it without a board.
- Round the image size up to 64 KB. An image that cannot fit in an empty region is refused (
KODE_ERROR). - Collect the live entries and sort them by offset.
- Start the search right after the newest install (highest
seq), rounded up to 64 KB. - Take the first gap that fits, looking from the search start to the end of the region, then from
KAPPS_DATA_OFFup to the search start. - If nothing fits, evict the entry named by
replacingfirst, then the oldest live entry (lowestseq), add its extent to the search and go again. The order ofevict[]is the order apps are freed in.
The eviction is first in, first out by install order. kodeOS has no notion of use to rank by. The one exception is replacing, the entry of the app the image is a new version of (KAPPS_NO_VICTIM for a new app): its old copy goes before any other app, because the install replaces it anyway. When evict[] holds it, replaced names it, and the caller does not count it as lost.
The plan also predicts one more victim, slot_victim: when all 64 directory slots would still be live after the evictions, AO_AppMgr removes the oldest remaining entry after the write. With replacing live there is none, because the new version takes its old copy's slot. Without it the plan is a prediction: an image with the same name as an installed app still replaces that app in its own slot, and only the written image tells. Replacing by name is AO_AppMgr's job. The driver's directory calls are generic: read, plan, commit.
A write session erases as it goes. kdrv_apps_write_begin(offset, size) opens a session. kdrv_apps_write() erases a 64 KB block only when the write reaches it, so the GUI keeps running between AO_AppMgr's 32 KB chunks.
The bytes reach flash through 2 KB of internal RAM (KDRV_BOUNCE_BYTES, borrowed per call from the heap). esp_flash_write() takes its fast path only from internal memory, and from PSRAM it programs 32 bytes at a time with the cache off.
Every image arrives in PSRAM. A 1.5 MB app from the card installs in about 8 s this way, against about 14 s straight from PSRAM, measured on a Kode Dot. If the heap has no 2 KB to lend, the write goes straight from the caller's buffer, slower and still correct. The mailbox and the kodeOS slot use the same helper (kdrv_bounce.h).
kdrv_apps_write_end() closes the session and runs kdrv_apps_verify() over exactly the bytes written, a plain esp_image_verify(). kdrv_apps_write_abort() only closes the session. The partial bytes stay in flash, unchecked and never entered in the directory.
Launching an app
The boot request says what the bootloader runs next. kdrv_apps_req_set() accepts only the states NEW and ALWAYS. It checks kapps_req_bounds_ok() first, so the refusal happens while there is still a console to explain it, then erases the request sector and writes the 20-byte record.
sequenceDiagram autonumber box "kodeOS" participant AM as AO_AppMgr participant B0 as AO_Boot end participant R as apps region participant BL as bootloader AM->>B0: BOOT_APP_RUN_SIG offset size mode B0->>R: req_set: erase, write KRUN NEW or ALWAYS B0->>B0: reset the chip BL->>R: read request, check CRC and bounds BL->>R: NEW becomes TRIED, ALWAYS becomes ARMED BL->>BL: jump to the app image Note over B0,R: next kodeOS boot: req_tried() yes, then req_clear()
NEW means one boot. ALWAYS is the sticky request that only mailbox adoption writes, so an uploaded sketch survives the Serial Monitor and the debugger. The bootloader marks it ARMED on the first boot, and from then on keeps it only across the resets in the table under Specifications. Back in kodeOS, kdrv_apps_req_tried() is how AO_Boot knows it is "back from the user app". kdrv_apps_req_armed() asks the same about the sticky state.
Adopting a mailbox image
The mailbox is checked in two phases. app_pending (6 MB at 0x1A00000, subtype 0x41) is non-executable, holds one image or one container at offset 0 and has no directory. A host tool writes the image with esptool, and a separate word at 0x5FF000 says what it is for (see Specifications).
- At boot,
kdrv_apps_pending_present()reads one byte.0xE9means a bare image, andpending_walk_len()walks its header chain withesp_partition_read()to find its length. This is a length check only: it catches a torn or garbage upload, not a wrong hash. A walk that does not hold together consumes the mailbox at once, so a broken upload is not re-checked on every boot. - Any other first byte may start a
.kfwcontainer, whose header states its own length. A container claiming more payload than the mailbox holds is discarded. - When an install must evict an app,
kdrv_apps_pending_verify()runs the fullesp_image_verify()on the mailbox in place, so a corrupt upload cannot cost anyone an installed app. A container is not verified here. - AO_AppMgr copies the image into the region, and
kdrv_apps_write_end()runs the full check on the copy.
The walk does not trust the length a segment header states. It bounds the inputs of each addition, not its result, so a value near UINT32_MAX cannot wrap the offset around the partition.
The in-place check and the check on the copy need the 32-bit flash cache mapping (CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH), because both app_pending and the upper half of the region lie above 16 MB. Nothing boots on the strength of the length check alone.
kdrv_apps_pending_present() reports two lengths. img_len is what the mailbox holds, which is what a copy reads. decoded_len is what the image occupies once decoded, which is what an eviction plan is made for. They are equal for a bare image. A deflated container holds fewer bytes than it places.
The mailbox sink keeps a container verbatim, header and compression included, so reading it back checks it again. This is the one place where the decoder is not in the path, and kdrv_apps_pending_write_end() checks only the byte count. kdrv_apps_pending_consume() erases the first sector and the intent sector every time, so an intent flag never outlives its image.
Decoding a container
A container is decoded as a stream. kdrv_kfw.c holds the format, a decoder and the contract of a sink, the destination of a decoded image:
| Sink call | Meaning |
|---|---|
begin(ctx, img_len) | Open a session for img_len decompressed bytes, or 0 when the source cannot say. A sink that needs the length refuses here |
write(ctx, data, len) | Append one chunk. May block for as long as the flash takes |
end(ctx) | Close and verify |
abort(ctx) | Abandon. Always safe, even before begin() |
Four sinks exist: kdrv_slot_sink() (with a size pre-check), kdrv_apps_sink() for the region, kdrv_apps_pending_sink() for the mailbox and kfw_buf_sink(), which fills a buffer the caller owns. The ESP32-C5 update uses the buffer sink, because a synchronous sink would block AO_Radio for a round trip per chunk.
The decoder is one static session with five states.
stateDiagram-v2 direction LR [*] --> IDLE IDLE --> SNIFF: kfw_begin SNIFF --> RAW: first byte 0xE9 SNIFF --> BODY: 72 header bytes accepted SNIFF --> DEAD: bad header, wrong target, wrong chip RAW --> DEAD: sink error BODY --> DEAD: overrun, bad deflate, sink error RAW --> IDLE: kfw_end or kfw_abort BODY --> IDLE: kfw_end or kfw_abort DEAD --> IDLE: kfw_end or kfw_abort
- SNIFF. One byte settles the raw case, since a raw image has no length to wait for. A raw image names its chip at byte 12 of its header, and one built for another chip than the target needs is refused before anything is placed. The P4 is the chip for the OS and app targets, the C5 for the C5 target. Any other first byte accumulates until 72 bytes are in, then
kfw_hdr_parse()runs. A container that declares a different target than the caller asked for is refused: the command decides where an image goes and the file only agrees or not. - Deflate. For a deflated payload the decoder takes a decompressor and a 32 KB dictionary from PSRAM. The inflater is
tinfl_decompressin the P4's ROM, so compression costs the image no flash. A failed allocation frees whatever half succeeded and returnsKODE_NO_RESOURCES. - Digest. A SHA-256 (PSA Crypto) runs over the decoded output. At
kfw_end()the count must equalimage_lenand the digest must match, both checked beforesink->end(), so a bad image is abandoned and never committed. A raw image has no digest, and its sink's own check is the only one. - Header callback. The callback fires with the image length and the container's name before the sink opens, so a caller that must place the image first, like the region's allocator, can.
- One session.
kfw_begin()while one is open returnsKODE_DENIED. AO_AppMgr and AO_Radio share the decoder, and each ends its own session withkfw_abort_owned(ctx).
kodeOS slots and the app guard
The kodeOS slots. kdrv_slot.c is the only file that calls the esp_ota_* functions. "The slot" means whichever of ota_0 and ota_1 kodeOS is not running (from factory it is ota_0).
kdrv_slot_begin() opens the write with OTA_WITH_SEQUENTIAL_WRITES, so the erase is spread over the writes and the console does not freeze for seconds. kdrv_slot_confirm_self() must run at the top of every boot from an OTA slot: an unconfirmed image keeps esp_ota_begin() refusing, and no later update can start. The slot sink refuses an image bigger than the slot before the first byte, when the length is known.
An app is built with a flash guard. In a firmware built with CONFIG_KODEOS_BOOT_ROLE_APP, an app runs in place from a data partition, and ESP-IDF's write guard finds no running app partition and aborts every persistent write. kdrv_slot.c then wraps esp_ota_get_running_partition() to answer with a synthetic app partition covering the running image (subtype APP_TEST, so kdrv_running_is_factory() stays false) and esp_ota_get_next_update_partition() to answer NULL. The --wrap flags sit under the same role guard in the layer's CMakeLists.txt. kodeOS itself is built without them.
Manifest, categories and app folder
The manifest and categories. An app may declare a display name and up to four categories in a kapp_desc_t placed at image offset 0x120: IDF's own esp_app_desc_t starts at 0x20 and is 256 bytes, and a section placed right behind it lands at a fixed, computable address. kdrv_apps_desc() reads the first KAPP_PREFIX_LEN bytes of an image with one flash read and extracts both descriptors. A missing manifest is the common case: the launcher then shows the app as a loose row at the root, in no folder.
After every parse, kapp_cat_resolve() looks for a category the board set for the app, and an override beats the image, silence included. The board's table is one NVS blob in PSRAM, written only by AO_AppMgr.
The app folder. An app can carry an app.json and a 64 by 64 icon in a folder next to its image, under .kode/appsdata/<project> on either medium. kapp_data_parse() reads the file into fixed-size fields, allocates nothing and refuses the whole file on any fault, so a caller never draws half a folder. How an app gets its icon is on internals/app-icon.
Specifications
The region
| Thing | Value |
|---|---|
Partition apps | Data, subtype 0x40 (KAPPS_PART_SUBTYPE), 0xC40000 to 0x1800000, size 0xBC0000 (11.75 MB) |
Partition app_pending | Data, subtype 0x41, fixed at 0x1A00000, 6 MB, non-executable |
| Byte order | Little-endian; every field is a uint32_t. The magic 0x5050414B is stored as 4B 41 50 50, the letters "KAPP" |
| Image alignment | 64 KB (KAPPS_ALIGN, 0x10000): the MMU page, since an image maps for execution only from a page boundary |
| Directory capacity | 64 entries (KAPPS_MAX_ENTRIES) |
| Executing above 16 MB | The cache maps 32-bit addresses, so images run across the 16 MB mark |
Offsets inside the region, counted from its start:
| Offset | Contents |
|---|---|
0x0000 (KAPPS_DIR_A_OFF) | Directory copy A, one 4 KB sector |
0x1000 (KAPPS_DIR_B_OFF) | Directory copy B |
0x2000 (KAPPS_REQ_OFF) | Boot request, one 4 KB sector |
0x3000 | Reserved |
0x10000 (KAPPS_DATA_OFF) | First allocatable byte. Every image starts on a 64 KB boundary |
The directory
kapps_dir_t is 1044 bytes in each copy. The CRC32 covers every byte before it. The valid copy with the highest gen wins.
| Offset | Size | Field | Meaning |
|---|---|---|---|
0x000 | 4 | magic | KAPPS_DIR_MAGIC, 0x5050414B |
0x004 | 4 | version | KAPPS_FMT_VERSION, 1 |
0x008 | 4 | gen | Generation; grows with every commit |
0x00C | 4 | count | Entries in use |
0x010 | 1024 | e[64] | 64 entries of 16 bytes |
0x410 | 4 | crc | CRC32 of every byte above |
Each entry (kapps_entry_t):
| Offset | Size | Field | Meaning |
|---|---|---|---|
0x0 | 4 | offset | From the region start; a multiple of KAPPS_ALIGN |
0x4 | 4 | size | Exact image length in bytes |
0x8 | 4 | seq | Install order, only grows: this is the FIFO |
0xC | 4 | flags | Bit 0 is KAPPS_ENTRY_VALID; bits 1 to 31 are reserved |
The boot request
kapps_req_t is 20 bytes (KAPPS_REQ_LEN), frozen by a _Static_assert. The CRC32 covers magic to size only. The field state sits after the CRC, so the bootloader can advance it with a 4-byte write and keep the record valid.
| Offset | Size | Field | Meaning |
|---|---|---|---|
0x00 | 4 | magic | KAPPS_REQ_MAGIC, 0x4E55524B ("KRUN") |
0x04 | 4 | offset | Region-relative offset of the image |
0x08 | 4 | size | Exact image length |
0x0C | 4 | crc | CRC32 of magic to size |
0x10 | 4 | state | One of the words below |
| State | Word | Written by |
|---|---|---|
EMPTY | 0xFFFFFFFF | Erased flash |
NEW | 0xAAAAAAAA | kodeOS: boot this once |
ALWAYS | 0x55555555 | Mailbox adoption only: boot this across USB resets |
ARMED | 0x00005555 | The bootloader, on the first boot of an ALWAYS request |
TRIED | 0x00000000 | The bootloader, when it retires a request |
Every step only clears bits: NEW to TRIED, ALWAYS to ARMED to TRIED. kapps_req_bounds_ok(offset, size, region_size) accepts an image only when it is at or past KAPPS_DATA_OFF, on a 64 KB boundary, not empty, not past the end of the region and fully inside it. The writer refuses a request that fails it, and the bootloader checks again, since flash is not safe only because a trusted writer wrote it once. The design is incompatible with flash encryption, which is not enabled.
An ARMED request survives these resets (kapps_req_cause_keeps(), a closed list, ROM numbering) while the mailbox is empty:
| Code | Cause |
|---|---|
0x03 | Core software reset: OpenOCD's first reset |
0x05 | Deep-sleep wake |
0x0C | CPU software reset: esp_restart(), a panic's reboot |
0x16 | USB-JTAG: OpenOCD over USB |
0x17 | USB-UART: a host opened the port |
0x18 | CPU JTAG: OpenOCD's reset halt |
Power-on (which BOT+LEFT is), the watchdogs, a brownout, a full mailbox and any other cause retire it to TRIED, and kodeOS boots.
The mailbox
| Offset | Contents |
|---|---|
0x000000 | The image or container, up to KAPPS_PENDING_INTENT_OFF bytes |
0x5FF000 (KAPPS_PENDING_INTENT_OFF) | The intent word, in the last 4 KB sector |
The word KAPPS_OS_INTENT_MAGIC (0x41544F4B, "KOTA") there means "adopt this as a kodeOS update, into the free OTA slot". Anything else, erased flash included, means "adopt this as an app", the default. A host tool writes the word separately, before the image. Nothing infers intent from the image's bytes. The constant is the contract with host tools that do not read the partition table, and a host test pins it to partitions.csv.
The container
A .kfw file is a 72-byte header and a payload. Everything in the header describes the decompressed image: the region allocator needs its length before the first byte is written, and the digest has to vouch for the image, since a digest of the payload would pass a correct transfer of a corrupt file.
| Offset | Size | Field | Meaning |
|---|---|---|---|
0x00 | 4 | magic | KFW_MAGIC, 0x3157464B ("KFW1") |
0x04 | 2 | hdr_version | 1 |
0x06 | 2 | hdr_len | 72 |
0x08 | 1 | target | 0 KFW_TARGET_OS (free kodeOS slot), 1 KFW_TARGET_APP (apps region), 2 KFW_TARGET_C5 |
0x09 | 1 | comp | 0 KFW_COMP_NONE, 1 KFW_COMP_DEFLATE |
0x0A | 2 | flags | Must be 0 |
0x0C | 4 | payload_len | Bytes after the header |
0x10 | 4 | image_len | Bytes after decompression |
0x14 | 32 | sha256 | Of the decompressed image |
0x34 | 4 | crc | CRC32 of the 52 bytes before it |
0x38 | 16 | name | NUL-terminated (KFW_NAME_MAX) |
kfw_hdr_parse() checks the magic (a mismatch is a quiet KODE_ERROR: not a container), the CRC, the version and length, the target and compression ranges, flags == 0, both lengths non-zero, payload_len == image_len when uncompressed, and a terminated name. It fills its output only on KODE_OK. KAPPS_KFW_MAGIC in kapps_fmt.h is a second copy of KFW_MAGIC, held equal by a _Static_assert, so the bootloader can tell a parked container from erased flash without the decoder. The host-side producer is tools/mkkfw.py in the kodeOS repository.
The manifest
kapp_desc_t is 136 bytes at image offset 0x120 (KAPP_DESC_OFF), checked at compile time against KAPP_IDF_DESC_OFF plus sizeof(esp_app_desc_t). Its offsets below count from the manifest's start.
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 4 | magic | KAPP_DESC_MAGIC, 0x4450414B ("KAPD") |
| 4 | 4 | version | KAPP_DESC_VERSION, 2: the struct's version, not the app's |
| 8 | 32 | name | Display name, terminator included (KAPP_NAME_MAX) |
| 40 | 64 | category | Comma-separated list (KAPP_CATEGORY_MAX) |
| 104 | 32 | reserved | Eight words, zero |
An app declares it once, at file scope: KODE_APP_MANIFEST("Kode Runner", "game,demo");. Static asserts refuse a name or list that is too long. The magic is the only thing that tells a manifest from whatever else the linker put at 0x120 in an app that declared none.
kapp_desc_parse() is pure and trusts nothing, since every byte may have come off a card. Bad magic or an unterminated string is KODE_ERROR, which means "unmanifested" and is never a failure to report. A version 1 manifest (88 bytes, KAPP_DESC_V1_SIZE, one 16-byte category) is read by its own bound and never by sizeof(kapp_desc_t). Any other version is KODE_ERROR. Three more readers work on an image prefix: kapp_project_name(), kapp_idf_desc() (project, version, build date and time) and kapp_image_chip(), which reads the chip id at byte 12 of the image header and keeps a P4 image out of the C5.
| Constant | Value | Meaning |
|---|---|---|
KAPP_CATEGORY_ITEM_MAX | 16 | One category, terminator included |
KAPP_CATEGORIES_MAX | 4 | Categories for each app |
KAPP_CATEGORY_MAX | 64 | 4 times 15, plus 3 commas, plus the terminator |
KAPP_CAT_ROWS_MAX | 64 | Apps that may carry an override |
KAPP_PREFIX_LEN | 0x120 plus 136 | Image bytes a reader needs to hold |
The list has one form everywhere: lowercase words separated by commas. kapp_cat_normalise() lowercases, trims spaces, drops duplicates, caps the list at 4 and logs anything it drops. kapp_cat_has() matches a whole item, so game does not match games.
The override table is one NVS blob, namespace kode, key app_cats:
| Offset | Size | Field | Meaning |
|---|---|---|---|
| 0 | 4 | magic | KAPP_CAT_BLOB_MAGIC, 0x5441434B ("KCAT") |
| 4 | 4 | version | 1: the blob layout |
| 8 | 4 | count | Rows |
| 12 | 96 each | rows | project[32] then category[64]; at most 64 rows, 6,156 bytes in all |
kapp_cat_deserialise() accepts a table only if all of it is valid. Magic, version, count and length must agree, and every project[] and category[] must carry a NUL inside its array. Each row's category is normalised on the way in. The rows live in PSRAM, taken once by kapp_cat_init(). A change is made in RAM first and persisted after, so a failed NVS write leaves the new table in RAM until the next boot, and the caller hears the failure.
The app folder
app.json is one flat JSON object of string values, at most 1024 bytes (KAPP_DATA_JSON_MAX). Escapes are limited to \" and \\. No numbers, arrays or nesting.
| Key | Rule |
|---|---|
kode | Required, and must be "1" |
name | Display name, up to 31 characters |
category | A list, normalised like a manifest's |
version | Up to 15 characters |
description | Up to 52 characters |
icon | A relative path of up to 40 characters, with no leading /, no \ and no .. component |
color | "#rrggbb" |
A key the reader does not know is ignored whatever its length, so a newer tool's file still works on an older board. kapp_data_parse() returns KAPP_JSON_OK, _NONE (no file), _SIZE (over 1024 bytes), _SYNTAX (outside the subset or a field over its limit) or _REVISION (kode missing or not "1"), and zeroes its output on anything but OK. The icon file icon.k64 is 12288 bytes (KAPP_ICON_BYTES): 64 by 64 pixels of RGB565 (8192 bytes) followed by 64 by 64 of alpha (4096 bytes).
The calls
| Call | What it does |
|---|---|
kdrv_apps_init(), _present(), _region_size(), _address() | Open the region and describe it |
kdrv_apps_dir() | The working copy of the directory in RAM. Never NULL |
kdrv_apps_dir_commit(d) | Write the other copy, then swap. KODE_DENIED for an insane directory |
kdrv_apps_plan(d, region_size, img_size, replacing, &plan) | Where the image goes and who is evicted, the app being replaced first. Valid only for the directory it ran on, so apply the evictions and commit before writing |
kdrv_apps_write_begin(off, size) | KODE_DENIED if the region is absent or a session is open. KODE_ERROR for an offset below KAPPS_DATA_OFF, a misaligned one or an extent past the partition |
kdrv_apps_write(), _write_end(), _write_abort(), _sink() | Stream the image in, verify it, or walk away. The sink needs kdrv_apps_sink_ctx_t{offset} and refuses img_len == 0 |
kdrv_apps_read(off, buf, len) | Read from the data area. No session. KODE_ERROR below KAPPS_DATA_OFF or past the partition |
kdrv_apps_desc(entry, &desc, &manifest) | One flash read, both descriptors, then the override |
kdrv_apps_verify(off, size, &len) | esp_image_verify() at the region's absolute address; optionally the true image length |
kdrv_apps_erase_header(off) | Erase the first 4 KB of an extent: the cheap uninstall |
kdrv_apps_req_set(), _req_tried(), _req_armed(), _req_present(), _req_clear() | The boot request |
kdrv_apps_pending_present(), _verify(), _read(), _os_intent(), _consume() | The mailbox |
kdrv_apps_pending_write_begin(), _write(), _write_end(), _write_abort(), _sink() | Write it from kodeOS. Capped at KAPPS_PENDING_INTENT_OFF bytes |
kfw_begin(), kfw_feed(), kfw_end() | One decode session. kfw_begin takes the sink, its ctx, the wanted target, the source length, a header callback |
kfw_abort(), kfw_abort_owned(ctx), kfw_busy(), kfw_produced() | Abandon, ask, count the image bytes handed to the sink (not the bytes fed in) |
kfw_hdr_parse(), kfw_sha256(), kfw_crc32(), kfw_selftest() | Header check, digest, CRC, and the self-test behind test component=kfw |
kdrv_slot_present(), _size(), _address(), _desc(), _has_image(), _info(id, &info) | The free slot, and any of the three kodeOS partitions by id |
kdrv_slot_begin(), _write(), _end(), _abort(), _sink(), _erase() | Write the free slot. erase clears its first 64 KB |
kdrv_slot_confirm_self() | Confirm the running image if it is still PENDING_VERIFY |
kdrv_otadata_blank(), _clear(), _select_slot(), kdrv_slot_state() | Read, reset and point otadata. select_slot verifies the image first |
kdrv_slot_coredump(), kdrv_slot_coredump_reason() | Whether a core dump waits in flash and whose it is |
kdrv_running_info(), _desc(), _is_factory(), kdrv_reset_reason() | The running partition and the chip's reset reason |
The partitions behind the slot driver:
| Partition | Offset | Size |
|---|---|---|
otadata | 0x1B000 | 0x2000 |
factory | 0x20000 | 0x400000 (4 MB), the rescue kodeOS, reachable by cable only |
ota_0 | 0x420000 | 0x400000 (4 MB) |
ota_1 | 0x820000 | 0x400000 (4 MB) |
coredump | 0xC20000 | 0x20000 |
The files
| File | What it does |
|---|---|
include/kapps_fmt.h | The on-flash format and the rules shared with the bootloader. No ESP-IDF |
include/kdrv_apps.h, src/kdrv_apps.c | The region, the allocator, the write session, the boot request, the mailbox |
include/kdrv_slot.h, src/kdrv_slot.c | kodeOS slots, otadata, self-confirm, the core dump query, the app-role flash guard |
include/kdrv_kfw.h, src/kdrv_kfw.c | The container header, the streaming decoder, the sinks contract, the buffer sink, the self-test |
include/kapp_desc.h, src/kapp_desc.c | The manifest, the image-prefix readers, KODE_APP_MANIFEST |
include/kapp_cat.h, src/kapp_cat.c | Category lists and the NVS override table |
include/kapp_data.h, src/kapp_data.c | The app.json reader |
src/kdrv_bounce.h | Flash writes through 2 KB of internal RAM. Private |
src/kdrv_desc_fill.h | IDF's app descriptor into kdrv_slot_desc_t, each string read with its array's width. Private |
Gotchas
Example code
An app that declares its name and category, which is the part of this layer an app author writes. Install it, and the launcher shows "Hello" in the folder examples.
/*
* manifest.c - 2026-10-28 - from Kode with love
*
* Declares a display name and a category in the app's image, at offset
* 0x120, then logs a line. After the install the launcher lists the app as
* "Hello" in the folder "examples", and the log shows: hello from the apps
* region.
*/
#include "kode.h"
#define TAG "hello"
/* What the launcher shows, and the category it files this app under. */
KODE_APP_MANIFEST("Hello", "example");
/* Bring the system up, say hello, then idle so the console stays up. */
void app_main(void)
{
kode_init();
KLOG_N(TAG, "hello from the apps region");
while (1) {
kode_delay_ms(1000);
}
}Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.
In the SDK
components/kdrv/apps: the region, slot, container, manifest, category and app-folder code.kapps_fmt.h: the on-flash format, shared with the bootloader.bootloader_start.c: the other reader ofkapps_fmt.h, in the kodeOS repository.