SDK
AO_Boot
The Active Object that decides which firmware starts after a reset: otadata, the boot request, the short leave before every reset, and the bottom-button hold.
What it is
After every reset the bootloader picks a firmware. Two small records in the flash hold that choice. otadata names the kodeOS slot that starts when nothing else is asked. The boot request, a sector of the apps region, names one app to start instead. AO_Boot owns both records and is the only code that writes them. Which images the flash holds is AO_AppMgr's business (sdk/kaos/appmgr). Which one starts is AO_Boot's.
It runs in kodeOS and inside every app, and the build role decides what it writes. In an app it claims the records and writes nothing, so any reset comes back to kodeOS. In kodeOS it confirms a new update, works out why the board started, and carries out four requests: switch slot, run an app, report, exit. Every restart it makes passes through a short leave first. A hold of the bottom button publishes HOME_SIG and, in an app, resets the board home even when the app is hung.
How it is connected
stateDiagram-v2 [*] --> Boot_active : claim, subscribe BUTTON_HOLD_SIG, act on the role Boot_active --> Boot_leaving : slot switch, app run, exit, home Boot_active --> Boot_active : info, hold, defer Boot_leaving --> [*] : khal_restart() after 150 ms
sequenceDiagram autonumber box "kodeOS" participant B as AO_Boot participant N as AO_Net participant R as AO_Radio end B->>N: NET_LEAVE_REQ_SIG, 200 ms bound N-->>B: BOOT_NET_LEFT_SIG B->>R: RADIO_PARK_REQ_SIG, 250 ms bound R-->>B: BOOT_RADIO_PARKED_SIG Note over B: wait 150 ms, then khal_restart()
AO_Boot in numbers:
| Thing | Value |
|---|---|
| Priority | 12 |
| Queue | 8 events |
| Stack | 4096 bytes |
| Claims | KBSP_RES_OTADATA, KBSP_RES_APPS_BOOTREQ |
| Reads and writes | kdrv_slot and kdrv_apps (sdk/kdrv/apps) |
| Subscribes to | BUTTON_HOLD_SIG, from AO_Buttons (sdk/kaos/buttons) |
In both roles its initial transition also restores the wall clock from the RTC or from the last value AO_Power kept (sdk/kaos/power).
The role
KODEOS_BOOT_ROLE_APP is the default, so a firmware that forgets to say what it is behaves like an app and cannot keep the board. KODEOS_BOOT_ROLE_OS is kodeOS itself. The initial transition then does four things in order:
- Confirm.
kdrv_slot_confirm_self()clears thePENDING_VERIFYmark of a freshly updated image. This is mandatory:esp_ota_begin()refuses to open an update session from an unconfirmed partition. - Work out why the board started. The bootloader marks a request
TRIEDbefore it jumps to an app, so aTRIEDrequest means the board is back from an app. AO_Boot sets the reasonKBOOT_FROM_APP, remembers the app's offset and clears the request. Any other state setsKBOOT_COLD, and a request still present is cleared as stale. - Detect a failed update. Running from
factorywhile otadata is not blank means an update died before it confirmed. AO_Boot setsKBOOT_UNEXPECTEDand clears otadata, so the board stays onfactory. - Classify how the app ended, from the chip's reset reason (
boot_app_end()).
The leave
Every deliberate restart does its own write first (the slot, the boot request, or nothing), records the role of the firmware it goes to and enters Boot_leaving. There AO_Boot runs these steps:
- Ask AO_Net to stop the Kode channel cleanly.
- Ask AO_Radio to hang up Bluetooth and park the C5.
- Wait 150 ms, so the reply to the command that caused the reset leaves the console.
- Call
khal_restart().
Each of the first two steps has its own bound on the leave timer. A silent neighbour delays the reset by its bound and no more.
In Boot_leaving every other restart request and every home gesture is logged and dropped: the first request wins and the board resets once.
The hold
With the bottom button held for KODE_BUTTON_HOLD_MS, AO_Buttons publishes BUTTON_HOLD_SIG. AO_Boot is the only owner of that gesture. It publishes HOME_SIG, which the launcher of kodeOS or an AO of yours answers.
With KODEOS_HOME_HOLD (app role) it then arms the home timer for 200 ms and resets the board home when it expires. An app that needs the time to save calls kode_home_defer() from its HOME_SIG handler, writes, and calls kode_home_now(). The wait is capped at 3 s from the hold, once.
Resting with the board
AO_Boot owns no device that sleeps, but it joins the rounds of AO_LowPower as boot (sdk/kaos/lowpower). A board that went to sleep between the write of a boot request and the reset would wake into the old firmware, so AO_Boot answers a round busy while a restart is on its way and ready otherwise:
home pendingwhile an app's deferral of the hold is running (kode_home_defer()), which is a restart a moment away;restart pendinginBoot_leaving, from the write of the request to the reset;factory resetwhile a factory reset runs, so the board cannot rest in the middle of it.
AO_Boot restores nothing when the board wakes. A new kodeOS slot confirms itself in the initial transition, before any round can begin, so no confirmation is ever pending to wait for.
The time zone
AO_Boot owns the setting zone (kset/zone, internals/settings). It applies the stored zone in its initial transition, before an app runs, so the face, the FAT timestamps and an app's localtime_r() read the same local time from the first second.
A zone must be a POSIX TZ string, std offset [dst [offset] [,start[/time],end[/time]]], of at most 32 characters. config key="zone" refuses anything else with >err code=5 msg=value and stores nothing, so text the C library would read as no zone, or as an offset of days, never reaches the face. A stored zone that is not one, left by an older build, is ignored at boot: the clock runs in UTC and the log says zone: UTC0 (none stored). A zone the C library has no memory for is not stored either, and the log names the zone that stays.
Specifications
The requests
| Signal | Event | From | What AO_Boot does |
|---|---|---|---|
BOOT_OS_SWITCH_SIG | none | AO_AppMgr, after a verified install dst=os | Refuses with a log line if the inactive slot is absent or holds no valid image. Otherwise kdrv_otadata_select_slot() verifies the image and writes otadata, then the leave and the reset |
BOOT_APP_RUN_SIG | BootRunEvt (offset, size, mode) | AO_AppMgr: app_run, install and run, mailbox adoption | kdrv_apps_req_set() writes the request, then the leave. KBOOT_RUN_ONCE writes NEW, KBOOT_RUN_STICKY writes ALWAYS. A failed write logs and does not reset |
BOOT_INFO_REQ_SIG | none | AO_Console (boot_info), AO_Autotest (test component=boot) | Publishes BOOT_STATE_SIG and logs the inactive slot's image |
BOOT_EXIT_REQ_SIG | none | kode_exit_to_os() | The leave, then the reset. Writes nothing |
BOOT_HOME_DEFER_SIG | none | kode_home_defer() | Honoured only while the grace runs. Extends it once |
BOOT_HOME_NOW_SIG | none | kode_home_now() | Disarms the home timer, then the leave |
BUTTON_HOLD_SIG | ButtonHoldEvt | AO_Buttons, subscribed | Only the bottom button counts: publishes HOME_SIG and arms the grace |
BOOT_NET_LEFT_SIG, BOOT_RADIO_PARKED_SIG | none | AO_Net, AO_Radio | The answers to the two steps of the leave |
LOWPOWER_ROUND_SIG | LowPowerRoundEvt | AO_LowPower, subscribed with CONFIG_KODE_LOWPOWER | Answers ready, or busy while a restart is on its way |
The private signals are BOOT_LEAVE_TMO_SIG, the bound of the leave step that is running, and BOOT_HOME_GRACE_SIG, the grace and then the cap. A build without KODEOS_NET or KODEOS_RADIO skips that step of the leave.
What it sends
| Signal | Event | Range | When |
|---|---|---|---|
BOOT_STATE_SIG | BootStateEvt: running[20], address, size, reason, otadata_blank, app_end | publish | In answer to BOOT_INFO_REQ_SIG. The console closes boot_info with it |
HOME_SIG | none | publish | On every bottom-button hold, in both roles |
NET_LEAVE_REQ_SIG | none | post to AO_Net | First step of a leave |
RADIO_PARK_REQ_SIG | RadioParkEvt (target: the role of the firmware the jump goes to) | post to AO_Radio | Second step of a leave |
The states of a boot
| Value | Meaning |
|---|---|
KBOOT_COLD | Power-on or a plain reset, and no app was running |
KBOOT_FROM_APP | The bootloader tried an app and the board is back |
KBOOT_UNEXPECTED | A kodeOS update never confirmed; the rescue copy on factory took over |
KBOOT_APP_END_NONE | This boot is not back from an app |
KBOOT_APP_END_CLEAN | Any way home that is not one of the next three: a restart, BOT+LEFT, the USB port opening |
KBOOT_APP_END_PANIC | Reset reason panic or CPU lockup |
KBOOT_APP_END_WATCHDOG | Interrupt, task or other watchdog |
KBOOT_APP_END_BROWNOUT | Brownout or power glitch |
An unknown reset reason reads as clean and never as a crash.
The timings
| Constant | Value | What it bounds |
|---|---|---|
KODE_HOME_GRACE_MS | 200 ms | From the hold to the reset, if no app asks for more |
KODE_HOME_DEFER_MAX_MS | 3000 ms | The longest an app can hold the reset, counted from the hold |
KODE_LEAVE_NET_MS | 200 ms | AO_Net's clean stop of the Kode channel |
KODE_LEAVE_RADIO_MS | 250 ms | AO_Radio parking the C5 |
KODE_RESTART_DRAIN_MS | 150 ms | The pause before khal_restart() |
KODE_LEAVE_BUDGET_MS | 500 ms | A _Static_assert keeps the two leave bounds under it |
The calls
| Call | What it does |
|---|---|
kode_exit_to_os() | Posts BOOT_EXIT_REQ_SIG. The polite way home |
kode_home_defer() | KODE_OK once noted, KODE_DENIED when no hold is pending. Call it from the HOME_SIG handler, in the same tick |
kode_home_now() | The app is ready: go home now |
boot_reason(), boot_app_end(), boot_app_offset() | Why this boot happened, how the app before it ended, and which app it was (KODE_APP_OFFSET_NONE when none) |
boot_os_switch_request(), boot_app_run_request(), boot_info_request(), boot_exit_request() | The four posts above, from kaos.h |
boot_home_pending() | True while a deferral would still be honoured. Always false without KODEOS_HOME_HOLD |
AO_Boot has one stored setting, zone, and no console verb of its own besides boot_info. The options are KODEOS_BOOT_ROLE_APP or _OS and KODEOS_HOME_HOLD, which depends on the app role.
Gotchas
Example code
Light the LED blue for three seconds, then hand the machine back to kodeOS. With the app running the LED turns blue, and after three seconds the screen shows the launcher of kodeOS again.
/*
* home_after_3s.c - 2026-10-28 - from Kode with love
*
* Lights the status LED blue, waits three seconds, then asks AO_Boot to
* leave the app. AO_Boot runs its short leave and resets, and the bootloader
* brings kodeOS back. On a Kode Dot the LED goes blue, then the launcher
* returns.
*/
#include "kode.h"
/* Bring the system up, show a colour, wait, then go home. */
void app_main(void)
{
kode_init();
kode_led_rgb(0, 0, 64);
kode_delay_ms(3000);
kode_exit_to_os();
}Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.
In the SDK
components/kaos/boot:boot_ao.c, the state machine, the leave and the hold.components/kdrv/apps:kdrv_slotandkdrv_apps, the otadata and boot-request primitives AO_Boot calls.kode.h:kode_exit_to_os(),kode_home_defer()andkode_home_now().