SDK

AO_AppMgr

The Active Object that owns the apps region: installs an app from a file, a URL, the console or the mailbox, runs and erases apps, updates kodeOS.

What it is

An app on a Kode Dot is a whole firmware image, written into the apps region, a part of the flash. AO_AppMgr owns that region. It installs an app, starts an installed app by name, and erases one by name. It also writes the inactive kodeOS slot, which is how install dst=os updates kodeOS.

It reads no file itself: AO_Storage reads and lends the bytes in chunks (sdk/kaos/storage). It does not decide which firmware starts either. AO_Boot does (sdk/kaos/boot), and the two AOs write different regions of the flash.

An image can come from four places: a file on a medium, a URL, the console, or the mailbox, the app_pending partition that a computer writes with esptool. Every install ends the same way. The directory, the list of installed apps, is updated in one commit after the written image is verified. When an install must evict apps to make room, AO_AppMgr first copies each victim to the card, then commits the eviction, then erases.

How it is connected

flowchart LR
file["File on a medium"] --> sto["AO_Storage"]
url["URL"] --> net["AO_Net"]
sto --> ao["AO_AppMgr"]
net --> ao
con["Console"] --> ao
mbx["Mailbox<br/>app_pending"] --> ao
ao -- "install, erase" --> region["Apps region"]
ao -- "install dst=os" --> slot["Inactive<br/>kodeOS slot"]
ao -- "run request" --> boot["AO_Boot"]
class ao,sto,net,boot ao
class region,slot,mbx store
Where an image comes from and where it goes
stateDiagram-v2
[*] --> idle : claim, kdrv_apps_init, mailbox check
idle --> opening : FLASH_REQ_SIG from a file, card or URL
opening --> flashing : source open, both buffers primed
flashing --> idle : eof verified and installed, or a failure
idle --> receiving : APP_RECV_REQ_SIG, console armed first
receiving --> launching : installed, run requested
idle --> adopting : APP_ADOPT_SIG, mailbox holds an image
flashing --> keeping : the plan evicts apps the card lacks
adopting --> keeping : the same, for the mailbox
keeping --> idle : copies done, the request is posted again
idle --> asking : APP_EVICT_REQ_SIG
launching --> idle : APP_RECV_LAUNCH_SIG
AO_AppMgr's states
sequenceDiagram
autonumber
box "kodeOS"
  participant A as AO_AppMgr
  participant S as AO_Storage
end
A->>S: STORAGE_OPEN_REQ_SIG
S-->>A: STORAGE_OPEN_RSP_SIG, size
A->>S: READ_REQ x2, both buffers
S-->>A: STORAGE_DATA_SIG, buffer 0
Note over A: writes chunk N to flash while S fills buffer 1
A->>S: STORAGE_NEXT_SIG, buffer 0 back
An install from a file, two buffers

AO_AppMgr in numbers:

ThingValue
Priority9
Queue16 events
Stack8192 bytes, the default tier
ClaimsKBSP_RES_APPS_REGION, KBSP_RES_APP_PENDING, KBSP_RES_OTA_SLOT
Compiled underKODEOS_APPMGR
Flash accesskdrv_apps, kdrv_slot and kdrv_kfw (sdk/kdrv/apps)

The stack is that large because the install path holds two directory copies of about 1 KB on it, and at 4 KB a streamed install overflowed and hung with no panic.

The hierarchy

idle is the only state that accepts work. Every other working state sits inside a parent, AppMgr_busy, which answers a question about the next install ("what would you evict?") with a busy end marker and settles late answers from AO_Storage. A new write, run, erase or list request that arrives in a working state is refused and never queued, so two images cannot mix in one place of the flash.

opening, flashing, adopting and keeping log the refusal. receiving and launching ignore it silently. app_cat is the exception: the console waits for its answer, so a refused one is answered with KODE_DENIED.

The copy

FLASH_REQ_SIG carries three things:

A URL moves the whole conversation from AO_Storage to the stream loan of AO_Net; the protocol is the same. Each chunk goes to the decoder of kdrv_kfw, which tells a bare 0xE9 image from a KFW1 container and inflates compressed ones, so progress counts the image and not the bytes that carried it. A URL holds up to 256 characters and travels whole. The progress events and the log of AO_AppMgr name it by its last path segment, and no log line prints its query, where a token usually rides. A file name holds up to 63.

The image is placed in the decoder's header callback, the only moment its true length is known. For dst=app that is kdrv_apps_plan() on a copy of the directory, told which installed app the header names. When there is no room it evicts that app's old copy first, then the oldest apps by sequence number, so a new version costs no other app while it fits the room of the old one. For dst=os it is a size check against the slot, and launch is forced off because the switch of AO_Boot is the launch. dst=c5 never reaches AO_AppMgr: the console sends it to AO_Radio.

The pacer

After a chunk is written, AO_AppMgr waits one 10 ms tick before it hands the buffer back. Erasing flash holds the cache down and nothing in flash runs, the GUI included, so the gap gives the interface a slot.

The returned buffers collect in the to_return bitmask and one pacer returns them all. Arming it once per buffer would produce two "next" requests for one loan, AO_Storage would refill the same buffer twice, and the image would be corrupt with a byte count that still adds up. The gap costs about 1.6 s on a 5 MB image.

Verify, then commit

xfer_end() verifies the written extent in place with the real image verifier. For dst=app, region_install() then runs these steps:

  1. Zero the entry of an app with the same name (installing replaces).
  2. Take the next sequence number, before compacting.
  3. Compact the directory.
  4. Evict the oldest entry when the directory holds KAPPS_MAX_ENTRIES (64).
  5. Append the new entry.
  6. Make one kdrv_apps_dir_commit().

AO_AppMgr then publishes APP_FLASH_DONE_SIG with the project name. When launch is set, region_launch() resolves the extent out of the committed directory by name, the same path APP_RUN_REQ_SIG takes, and asks AO_Boot for a one-shot run.

Keeping an evicted app

A victim that came from a URL, the console, the mailbox or the internal FAT has no other copy. Before AO_AppMgr commits an eviction it copies each victim to /sdcard/<project>.bin, byte for byte. The extent is a verified, bootable image with its own manifest, so the launcher shows the file as an app to install again. A victim is already kept when that file exists, whatever its content.

The install runs in a round. The first plan that names victims abandons the transfer and enters keeping, which checks the card with a STAT and, if the file is absent, reads the extent from the region into PSRAM 32 KB per tick and hands it to AO_Storage as a whole-file write. When every victim is tried, AO_AppMgr posts the same request again at the front of its queue, and the second plan only evicts.

A failed copy decides by origin:

Install from the console

install src="terminal" is its own request, APP_RECV_REQ_SIG. In idle AO_AppMgr checks that the region or the slot exists and fits, then asks AO_Log for raw mode first, before any flash work, because an erase stops the reader task and an arming that lands inside it cannot tell the command's newline from the first payload byte.

In receiving, the transfer runs in four steps:

  1. AO_AppMgr makes room, opens the sink, tightens the watchdog from the 30 s long-stop to the 5 s gap and publishes RECV_READY_SIG.
  2. The host sends 4096-byte chunks (RECV_DATA_SIG, posted by the reader task).
  3. AO_AppMgr stages each chunk, takes its CRC, writes it, and publishes RECV_ACK_SIG for each whole chunk.
  4. At the full count it compares the CRC, verifies, installs and publishes one of three static outcome events: RECV_DONE_SIG, RECV_FAIL_SIG or RECV_STALL_SIG.

The room is made before the first byte arrives, so the plan cannot read the image's name. The request carries it instead, from install … name="<project>", which kcli and kble send, and the old copy of that app goes first. Without name the plan treats the image as a new app. Each app evicted is published as APP_EVICTED_SIG before RECV_READY_SIG, and the console prints it as >|evicted.

The acks are the entire flow control, because the USB driver drops bytes silently when its ring is full. A failed write stops neither the draining nor the acks, since the host still owes the full count.

A failure after the 0 % report also publishes APP_FLASH_FAIL_SIG, so the Opening card ends as a failure. The launching state waits 50 ms so the >ok leaves the console before the board resets.

Mailbox adoption

At boot, and for install src="pending", AO_AppMgr checks kdrv_apps_pending_present() and posts APP_ADOPT_SIG to itself, because a copy of several MB must not run inside an initial transition. It reads the destination from the OS-intent sector (unless dst= was typed), reads the project name from the head of the mailbox, plans with it, and copies 4 KB per tick in adopting.

A deterministic failure (the image does not verify, the directory is full and nothing can be evicted) consumes the mailbox so no later boot adopts it again. A directory commit failure or an I/O error leaves it, and the next boot tries again. On success the mailbox is consumed after the commit.

A boot-time adoption then starts the app with KBOOT_RUN_STICKY, the one sticky request kodeOS writes, so the app survives the reset that the host's port opening causes. A typed install src="pending" starts it once, and only with run=on.

Resting with the board

With CONFIG_KODE_LOWPOWER, AO_AppMgr joins the rounds of AO_LowPower as appmgr (sdk/kaos/lowpower). It owns no device that sleeps. It owns work that must not be cut: an image being written, kept or received, a launch on its way and the asks of an eviction are every state but idle. Each of those answers a round busy (app write), and idle answers ready. Nothing is restored when the board wakes.

Specifications

Requests

SignalEventFromWhat AO_AppMgr does
FLASH_REQ_SIGFlashReqEvt (src_kind, dst_kind, launch, src)kode_flash(), the console, the launcherThe only way to ask for a copy. appmgr_flash_request(file, launch) is the form src="fs:…" dst=app
APP_RECV_REQ_SIGAppRecvReqEvt (dst, size, crc32, launch, src, name)appmgr_recv_request()An image arrives on the console
APP_RUN_REQ_SIGAppFlashEvt (file)appmgr_run_request()Resolves the name in the directory and asks AO_Boot for a one-shot run. Logs '<name>' is not installed otherwise
APP_ERASE_REQ_SIGAppFlashEvtappmgr_erase_request()Zeroes the entry, compacts, commits, then kdrv_apps_erase_header() clears the first 4 KB of the extent
APP_CAT_REQ_SIGAppCatReqEvt (project, cats, clear)appmgr_cat_request()Sets or clears the category override in NVS. AO_AppMgr is the only writer of that table
APP_LIST_REQ_SIGAppListReqEvt (skip, who)appmgr_list_page()Posts up to 24 APP_ENTRY_SIG and an end marker to who
APP_EVICT_REQ_SIGAppEvictReqEvt (size, project, who)appmgr_evict_query()Plans an install on a copy of the directory and posts up to 8 APP_EVICT_ENTRY_SIG, then APP_EVICT_END_SIG (victims, fits, busy)
STORAGE_OPEN_RSP_SIG, STORAGE_DATA_SIG, NET_STREAM_OPEN_RSP_SIG, NET_STREAM_DATA_SIGStorage and stream eventsAO_Storage, AO_NetThe loan
STORAGE_PATH_RSP_SIG, STORAGE_WRITE_RSP_SIGStoragePathRspEvt, StorageWriteRspEvtAO_StorageThe answers of keeping and asking
CONSOLE_RAW_RSP_SIG, RECV_DATA_SIGConsoleRawRspEvt, RecvDataEvtAO_Log, the reader taskThe console install
LOWPOWER_ROUND_SIGLowPowerRoundEvtAO_LowPower, subscribed with CONFIG_KODE_LOWPOWERAnswers ready in idle, busy app write in every other state

Private: APP_ADOPT_SIG, and the QTimeEvts APP_FLASH_CHUNK_SIG, APP_RECV_IDLE_SIG and APP_RECV_LAUNCH_SIG.

What it sends

SignalEventRangeMeaning
APP_FLASH_PROGRESS_SIGAppProgressEvt (percent, what, file)publish0 % when a write starts, then each whole percent. what is KWRITE_APP, _APP_RUN, _OS or _C5. The Opening card and the LED pulse of AO_Power follow it. The log keeps a 10 % step
APP_FLASH_DONE_SIGAppFlashDoneEvt (name)publishThe app installed; name is empty when the descriptor could not be read, and a static event goes out when the pool is dry
APP_EVICTED_SIGAppEvictedEvt (name, kept)publishOne per app an install evicts, before the write starts. kept says a copy is on the card. The old copy of the app being replaced is not one
APP_FLASH_FAIL_SIGAppFlashFailEvt (why)publishKFAIL_WRITE, or KFAIL_KEEP when the card could not keep an app. One static event per reason
APP_CAT_RSP_SIGAppCatRspEvtpublishThe outcome of APP_CAT_REQ_SIG
RECV_READY_SIG, RECV_ACK_SIG, RECV_DONE_SIG, RECV_FAIL_SIG, RECV_STALL_SIGnonepublishThe receive protocol. The three outcomes are static and cannot fail to allocate
APP_ENTRY_SIG, APP_LIST_END_SIG, APP_EVICT_ENTRY_SIG, APP_EVICT_END_SIGAppEntryEvt, ListEndEvt, AppEvictEntryEvt, AppEvictEndEvtpost to the askerPages and eviction answers
BOOT_APP_RUN_SIG, BOOT_OS_SWITCH_SIGBootRunEvt, nonepost to AO_BootAfter a verified write, app_run, or an adoption
CONSOLE_RAW_REQ_SIG, CONSOLE_RAW_END_SIGConsoleRawReqEvt, nonepost to AO_LogArm raw mode; abandon it on a stall

Timers and limits

NameValueBounds
pacer (APPMGR_CHUNK_GAP_TICKS)10 msThe pause before a buffer goes back, and each 4 KB or 32 KB step of adoption and keeping
idle, APPMGR_RECV_IDLE_TICKS5 s (KODE_RECV_IDLE_MS)The gap between two chunks of a console install
idle, APPMGR_RECV_LONGSTOP_TICKS30 sThe wait for the answer that arms raw mode
idle, APPMGR_OPEN_TICKS30 sopening, and the STAT answer in keeping
idle, APPMGR_CHUNK_TICKS30 sThe gap between chunks in flashing. Expiry is KFAIL_WRITE
idle, APPMGR_KEEP_WRITE_TICKS60 s plus 1 s per 128 KBOne copy to the card
pacer, APPMGR_KEEP_BEAT_TICKS1 sRe-publishes the percent while the card writes, so AO_Power keeps the LED pulsing
answer, APPMGR_ANSWER_DRAIN_TICKS50 msFrom "installed" to the reset
KAPPS_MAX_ENTRIES64Apps in the directory. A full listing is three pages
KODE_APP_EVICT_MAX8Victims in one eviction answer
KODE_RECV_CHUNK4096Console chunk, and the static adoption buffer
KODE_FLASH_SRC_MAX257A source with its terminator: a URL of up to 256 characters. A console line carries fewer

The idle timer is shared by opening, flashing, receiving and keeping. These states exclude each other and each disarms it on exit. The timer idle and the state idle are two things.

Console verbs, calls and settings

app_list, app_run name="…", app_erase name="…", app_cat, install src=… dst=os|app|c5|pending, with name="<project>" for src="terminal" dst=app only. The facade call is kode_flash(src, dst, run): src is "fs:<file>", "sd:<file>", a bare name or an http:// or https:// URL, dst is "os" or "app", and it posts and returns. AO_AppMgr owns the category override table of kapp_cat, an NVS blob. The format of the region and the directory is on internals/apps-region.

Gotchas

Example code

Install an app from the card and start it. With blinky.bin on the card, the board writes it, resets and runs it.

/*
 * install_from_card.c - 2026-10-28 - from Kode with love
 *
 * Asks AO_AppMgr to install /sdcard/blinky.bin into the apps region and run
 * it. kode_flash() posts the request and returns; AO_AppMgr reads the file
 * through AO_Storage, verifies it, commits the directory and resets into the
 * new app. On a Kode Dot the Opening card shows, then the app starts.
 */
#include "kode.h"
 
#define TAG "install"
 
/* Post the install, then wait for the reset. */
void app_main(void)
{
    kode_init();
 
    if (kode_flash("sd:blinky.bin", "app", true) != KODE_OK) {
        KLOG_E(TAG, "no installer in this firmware, or a bad argument");
        return;
    }
    KLOG_I(TAG, "install requested");
    for (;;) {
        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