# 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

```mermaid
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
```

*AO_Boot's two states*

```mermaid
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()
```

*The leave before a reset*

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:

1. **Confirm.** `kdrv_slot_confirm_self()` clears the `PENDING_VERIFY` mark of a freshly updated image. This is mandatory: `esp_ota_begin()` refuses to open an update session from an unconfirmed partition.
2. **Work out why the board started.** The bootloader marks a request `TRIED` before it jumps to an app, so a `TRIED` request means the board is back from an app. AO_Boot sets the reason `KBOOT_FROM_APP`, remembers the app's offset and clears the request. Any other state sets `KBOOT_COLD`, and a request still present is cleared as stale.
3. **Detect a failed update.** Running from `factory` while otadata is not blank means an update died before it confirmed. AO_Boot sets `KBOOT_UNEXPECTED` and clears otadata, so the board stays on `factory`.
4. **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:

1. Ask AO_Net to stop the Kode channel cleanly.
2. Ask AO_Radio to hang up Bluetooth and park the C5.
3. Wait 150 ms, so the reply to the command that caused the reset leaves the console.
4. 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 pending` while an app's deferral of the hold is running (`kode_home_defer()`), which is a restart a moment away;
- `restart pending` in `Boot_leaving`, from the write of the request to the reset;
- `factory reset` while 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

> **WARNING**
>
> **An app that resets comes home to kodeOS and does not restart itself.** In the app role AO_Boot never writes otadata, and a one-shot request is already `TRIED` when the app starts. A crash on the first line still returns to kodeOS. Only a sticky request, which the mailbox writes, keeps an app across the resets its host produces (`internals/boot-model`). `kode_exit_to_os()` in a sticky app resets the board and the app starts again.

> **WARNING**
>
> **`kode_home_defer()` answers `KODE_DENIED` and the board resets anyway.** A deferral counts only while the 200 ms grace is running, and only once. Call it from the handler of `HOME_SIG` or `BUTTON_HOLD_SIG`, save what you must, and call `kode_home_now()`. Three seconds after the hold the board resets whatever the app does.

> **NOTE**
>
> **A second restart request does nothing.** In `Boot_leaving` the log reads `already leaving; this restart request is dropped`. A double tap on Open, or the bottom button held during `app_run`, cannot write a second boot request or select a second slot.

> **NOTE**
>
> **A reset takes up to about 600 ms from the request.** The two leave steps take up to 450 ms when a neighbour is silent, and the drain adds 150 ms. A script that resets the board and reads the log straight away sees the leave lines first.

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

```c
/*
 * 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`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/boot): `boot_ao.c`, the state machine, the leave and the hold.
- [`components/kdrv/apps`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/apps): `kdrv_slot` and `kdrv_apps`, the otadata and boot-request primitives AO_Boot calls.
- [`kode.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kode/include/kode.h): `kode_exit_to_os()`, `kode_home_defer()` and `kode_home_now()`.
