# AO_Radio

Owns the ESP32-C5 of a Kode Dot: the link, Wi-Fi and BLE as services, two workers, C5 updates and parking the radio for a jump.

## What it is

A Kode Dot has two processors. The P4 runs kodeOS. The ESP32-C5 holds the only radio. **AO_Radio** owns the C5 as a whole: its EN line, the esp-hosted link to it over SDIO, its GPIOs (reached by RPC), the Wi-Fi and the BLE controller that run on its radio, its flash, and its row of the production test.

Everything reaches the C5 through one RPC channel that carries one request at a time. So AO_Radio runs **one job at a time** and refuses every other request, and each refusal comes with an answer. Nothing in AO_Radio blocks: a Wi-Fi scan takes about 38 s and a C5 update more than a minute, and both run as jobs on worker tasks that AO_Radio owns.

Wi-Fi and BLE are services, each with a desired state kept as a flag. Both use the link, and switching one on or off in the settings does not start or end the link by itself. Link, driver and chip are on `sdk/kdrv/radio` and `sdk/kdrv/c5`. The network on top of Wi-Fi is `sdk/kaos/net`.

## How it is connected

```mermaid
flowchart LR
  req["Console, facade<br/>AO_Net, AO_Boot"]
  subgraph p4["On the P4"]
    ao["AO_Radio<br/>priority 4"] -- "kdrv_radio_run()" --> w1["radio_worker"]
    ao -- "kdrv_c5svc_send()" --> w2["radio_svc"]
    sto["AO_Storage"]
  end
  req -- "WIFI_*, BLE_*,<br/>RADIO_*_REQ_SIG" --> ao
  w1 -- "esp-hosted, SDIO" --> c5["ESP32-C5"]
  w2 -- "esp-hosted, SDIO" --> c5
  ao -- "unmount, remount" --> sto
  ao -- "RADIO_STATE_SIG,<br/>WIFI_STATE_SIG,<br/>BLE_STATE_SIG" --> subs["Subscribers"]
  class ao,sto ao
  class c5 chip
  class subs app
```

*AO_Radio, its workers and the C5*

### Priority and start

AO_Radio claims `KBSP_RES_C5`. It runs at priority 4 (`KODE_PRIO_RADIO`) with an 8-event queue and a 6144-byte stack, and it shares that level with the console reader task and its own two workers. All of them move data and block, so all sit low, round-robin by FreeRTOS. Only AO_Net (2) and AO_Gui (3) are lower.

`kode_init()` starts it after AO_Storage, since the link walks the SD card between its mount paths by posting to AO_Storage, and before AO_Net, whose initial transition posts a question to it. The Kconfig options for AO_Radio:

| Option | What it does |
| --- | --- |
| `KODEOS_RADIO` | Compiles AO_Radio |
| `KODEOS_WIFI`, `KODEOS_BLE` | Switch the services |
| `KODEOS_WIFI_AUTOJOIN` | The join at link-up |
| `KODEOS_C5_HOLD` | The reset hold |

### The shared SD controller

The C5 and the microSD card share one SDMMC controller of the P4. With the link up the card is mounted in hosted mode, without it on the plain path. So AO_Radio asks AO_Storage to unmount before it touches the C5 and to mount again after, and each unmount that must come first is asked with an answer.

### States

```mermaid
stateDiagram-v2
  [*] --> Radio_off
  [*] --> Radio_starting: a service is wanted
  Radio_off --> Radio_starting: radio_up, a service start, the row
  Radio_starting --> Radio_up: CONNECT_DONE ok, card remounted
  Radio_starting --> Radio_failed: CONNECT_DONE failed
  Radio_up --> Radio_stopping: radio_down, or nothing uses the link
  Radio_stopping --> Radio_off: DISCONNECT_DONE
  Radio_up --> Radio_failed: LINK_LOST
  Radio_failed --> Radio_starting: radio_up
  Radio_up --> Radio_autojoin: WIFI_AUTO_REQ_SIG
  Radio_autojoin --> Radio_up: round over
  Radio_up --> Radio_updating: dst=c5 install
  Radio_updating --> Radio_off: C5 activated, link gone
  Radio_updating --> Radio_up: failure
```

*AO_Radio states*

`Radio_down` is the parent of `Radio_off`, `Radio_starting`, `Radio_stopping` and `Radio_failed`. `Radio_up` is the parent of `Radio_updating` and `Radio_autojoin`.

A request that needs the link has one right answer when it is down, written once in `Radio_down`: a scan with no link gets an empty scan end with `ok=false`, and never a timeout. The states mirror `kradio_link_t` (`KRADIO_OFF`, `_STARTING`, `_UP`, `_STOPPING`, `_FAILED`), and every change publishes `RADIO_STATE_SIG`.

### The workers

The two workers are `kqp_worker` tasks at `KODE_PRIO_RADIO`, and a worker runs one job and decides nothing.

| Worker | Stack | Runs | Done event |
| --- | --- | --- | --- |
| `radio_worker` | 6144 bytes, internal RAM | `kdrv_radio_run()`: link, scans, joins, firmware pushes, BLE calls | `RADIO_JOB_SIG` |
| `radio_svc` | 8192 bytes, PSRAM | `kdrv_c5svc_send()` to the C5's KODE services: vault, identity, BLE keys, `INFO`. Queue of two requests per service | `RADIO_SVC_JOB_SIG` |

The stack of `radio_worker` is in internal RAM because link bring-up calls `khal_nvs_init()` and the BLE jobs use NimBLE's bond store in NVS, and a PSRAM stack must never touch the internal flash. Sends have their own worker because a send is one RPC and the C5 serves RPCs one at a time. Behind a 37 s dual-band scan, an identity answer would arrive that much late.

`winflight` is set at submit and cleared only by the done event with the matching sequence number, so a job is never submitted over one still inside the worker. `busy` is policy: the channel is occupied by a job or a chain of jobs.

Every job has a guard of 180 s (`KODE_RADIO_GUARD_MS`), and a BLE listen adds its length. A KODE send has its own 60 s guard (`KODE_RADIO_SVC_SEND_GUARD_MS`) and then a 3 s wait for its answer. An expired guard answers `KODE_TIMEOUT` and leaves the worker busy. The loss of the link arrives as `RADIO_LINK_LOST_SIG` and is no job's outcome.

`DISCONNECT` is never submitted while a send is inside `radio_svc`, because tearing down destroys the semaphore the send sleeps on. It is owed, and the last send back pays it.

### Starting the link

`radio_begin_connect()` switches `kdrv_sd` to hosted mode, asks AO_Storage to unmount with an answer, and only then submits `KDRV_RADIO_JOB_CONNECT`. A card still mounted at the answer (a transfer holds it) is asked again every 500 ms for 10 s (`RADIO_UNMOUNT_RETRY_MS`, `RADIO_UNMOUNT_WAIT_MS`). After that the link does not start, and the C5 is untouched.

After `CONNECT_DONE` the card is remounted, and AO_Radio waits for `STORAGE_CMD_RSP_SIG` before it enters `Radio_up`. An RPC to the C5 while the card is being probed leaves the C5 mute for the rest of the boot. So "up" means link up and card back.

### Autostart and the flags

At start `Radio_initial` reads `kset/wifi` and `kset/ble`. A flag never stored takes the build default: Wi-Fi off (`KODEOS_WIFI_DEFAULT_ON`), BLE on (`KODEOS_BLE_DEFAULT_ON`). `KODEOS_BLE_AT_BOOT` forces BLE on, and without `KODEOS_WIFI` the Wi-Fi flag is false. If either is wanted, AO_Radio goes straight to `Radio_starting`.

AO_Radio is the only writer of the flags, through `radio_flag_set()`, and only a change is stored. A refusal is about the present and the flag about every moment after, so a busy check never decides a flag. A refused request becomes a debt: `wifi_stop_pending`, `wifi_start_pending`, `ble_stop_pending` and `ble_start_pending`, paid by `radio_settle()` when a job ends.

Setting a flag cancels the opposite debt, since someone often switches a service off and on during one job. `Radio_off` and `Radio_failed` clear all four.

### The link ends when nothing uses it

Four things use the link:

1. A wanted service.
2. A direct ask (`link_asked`, set by `radio_up`, the row or a C5 update).
3. A job in flight.
4. With `KODEOS_C5_HOLD`, a sound.

When a job ends and none holds, `radio_settle()` posts `RADIO_UNUSED_SIG` to itself and `Radio_up` takes the `radio_down` path.

`wifi_down` stops the station. `ble_down` makes the host quiet, and the host keeps running because NimBLE starts once per boot. Switching a service on in `Radio_off` starts the link again in about 3 s. Code that needs the link without a service must call `radio_up_request()`.

`LINK_LOST` comes as `KODE_ERROR` (transport died) or `KODE_TIMEOUT` (the C5 went mute). Both unmount the card and order a `DISCONNECT`, so `Radio_failed` always means a cleanly torn-down link. A mute C5 is reset once by a rebuild without a request (`relink_owed`).

### Holding the C5 in reset

`KODEOS_C5_HOLD` is on in kodeOS. The entry of `Radio_off` holds the C5 when neither service is wanted and no sound asked for it.

In reset it draws nothing and the amplifier its firmware enables is shut down with it. `C5_RUN_REQ_SIG`, which AO_Audio publishes before a boot's first sound, lets it go and starts a one-shot `KODE_C5_BOOT_MS` (600 ms), then publishes `C5_RUNNING_SIG`. After that the C5 is not held again in that boot. `info` reports `power=held` or `power=running`.

### The reconcile

The entry of `Radio_up` starts a chain that starts what is wanted: first, after an adoption (below), nothing; then BLE (stack, then advert); then Wi-Fi through `wifi_auto_request()`. With autojoin compiled out or `wifi_join` off it only publishes the state, because "enabled and idle until told which network" is a real answer.

A step's `*_DONE` passes the turn only while `recon == RECON_BLE`, and `radio_reconcile_retry()` unsticks a chain whose BLE start was refused because a scan held the channel. An entry action never starts a job, so the entry posts requests to AO_Radio itself.

### Keeping BLE on the air

`radio_ble_adv_reconcile()` keeps one rule: BLE flag on, nobody connected, stack idle, therefore advertising. It runs on `BLE_LINK_SIG` (after the state is published), when a BLE start job ends, and last after every settled job. No timer turns Bluetooth off.

The rule also decides how the board shows itself. A board with no bond is visible. A board with a bond is hidden: the advert stays connectable for the devices that know the board, but it carries no name and no discoverable flag, and a new pairing is refused. `ble_pair_open_request()` (the Bluetooth page's Pair new device, the verb `ble_pair`) makes it visible for 5 minutes (`RADIO_BLE_PAIR_MS`), until a device pairs, or until Forget all leaves it with no bond. An advert on the air that shows the board the wrong way is stopped, and its `IDLE` brings the right one. A lab build (`KODEOS_BLE_AT_BOOT`) and an app that serves Bluetooth of its own never hide.

The advert runs in legs. A leg is fast (`KDRV_BLE_ITVL_FAST`, about 150 ms) for 30 s (`RADIO_BLE_FAST_MS`) after somebody may be looking: Bluetooth switched on, Pair new device, or a bonded device that just left. Otherwise it is slow (`KDRV_BLE_ITVL_SLOW`, about 1 s) and has no end, except while Pair new device's time is what shows the board: then the leg ends with that time. At rest the advert stays slow.

A BLE scan listens `ms` long: 0 means 5000 and the range is 500 to 30000 (`KODE_BLE_SCAN_MIN_MS`, `_MAX_MS`). A length outside the range is refused with an end marker `ok = false`.

### Autojoin

Autojoin scans first and joins after, since a join without a scan costs 20 s per network that is elsewhere. The remembered networks are the C5's vault, and AO_Radio keeps their names only. `auto_pick()` chooses the strongest remembered network among the strongest 24 the sweep heard, skipping those already tried this round (`tried` is a bitmask, so `KDRV_WIFI_SCAN_MAX` stays under 32).

A join is three steps: ready the P4 interface, `KODE_VAULT_JOIN` on the C5, wait for the address. Credentials are stored only after a join worked and `remember` is set, and AO_Radio clears its copy of the password. Every outcome checks `wifi_want` first, and every round ends in `auto_finish()`, which publishes the state and settles.

The settings `wifi_join` (default is the build option) and `wifi_ps` (default on) are stored as `kset/wifi_join` and `kset/wifi_ps`.

### Fast mode

`wifi_fast_request(on)` posts a static event, so it needs no pool block. `on` means no Wi-Fi power save on the C5, which AO_Net asks for while commands arrive on the Kode channel. AO_Radio keeps the wish in `fast_want` and what it believes in `ps_fast`, and submits one `WIFI_PS` job when they differ and the channel is free.

A job the C5 refuses logs `the C5 would not change the power save`. Leaving `Radio_up` clears both, since a reset C5 boots on the default.

### Parking the C5 for a jump

Before a deliberate restart (an app, kodeOS, an update), AO_Boot sends `RADIO_PARK_REQ_SIG` with the target role. AO_Radio answers exactly once, in every state, with `BOOT_RADIO_PARKED_SIG`, which means it has done with the C5, parked or not. With the link up, `PARK_QUIET` submits `BLE_QUIET` so a phone sees the disconnect now. `PARK_ASKED` sends `SELF/PARK` to the C5, and then `radio_park_end()` answers.

The 3 s service deadline applies.

Only a `KODE_ST_OK`, the station associated, leaves a handover note (`kdrv_radio_note_leave(target)`) and calls `kdrv_wifi_keep_station_on_restart()`. Any other ending leaves none, and the next firmware resets the C5. `PARK_DONE` is terminal until the reset, and AO_Radio then asks the C5 nothing.

At the next boot with a note, `kdrv_radio_adopted()` is true and, if Wi-Fi is wanted, `KDRV_RADIO_JOB_WIFI_ADOPT` is the first reconcile step. A failed adoption logs `adoption failed (…): joining` and the usual round follows.

### KODE services on the C5

`ID_SIGN_REQ_SIG` signs the cloud's challenge. `ID_REQ_SIG` reads the certificate pair, makes the CSR, puts a part of the pair or locks. `C5_WIPE_REQ_SIG` forgets the vault, the BLE keys and the bonds in three steps, and the identity stays.

Each is answered always, a refusal included: `ID_SIGN_RSP_SIG`, `ID_PART_SIG` records of up to 144 bytes with an `ID_END_SIG`, `C5_WIPE_RSP_SIG`. At every link AO_Radio asks the C5 for `INFO` and then the vault's `LIST`.

### Updating the C5

With `install dst=c5` the image is decoded whole into PSRAM, up to 1920 KB (`C5_IMAGE_MAX`, one slot of the C5), before the C5 is asked for anything. It is then pushed in 32 KB chunks (`C5_PUSH_CHUNK`) as `KDRV_RADIO_JOB_C5_WRITE`. Phases are `OPENING`, `DECODING`, `BEGIN`, `WRITING`, `FINISH` and `TEARDOWN`.

`C5_BEGIN` opens the session only after `kfw_end()` verified the digest and `kdrv_radio_image_header_ok()` accepted the header, which also checks the chip id. The card is unmounted first.

`FINISH_DONE` orders a `DISCONNECT`, and the update ends in `Radio_off` with the link off for the rest of the boot. Send `radio_up` and read `c5` to see the new image.

The C5 slave has rollback disabled, so a bad activated image leaves it unreachable. During an update AO_Radio refuses every Wi-Fi and BLE verb from an explicit list, because `busy` is false while it waits for storage.

Progress is `APP_FLASH_PROGRESS_SIG` with `what = KWRITE_C5`, at 0 % and then every percent, then one `APP_FLASH_DONE_SIG` or `APP_FLASH_FAIL_SIG` with reason `KFAIL_WRITE`. From the cable (`install src="terminal" dst=c5`) the receive needs neither the link nor the channel. AO_Radio asks AO_Log for raw mode, computes the CRC of each chunk, feeds the same decoder and acks each whole 4 KB chunk with `RECV_ACK_SIG`.

At the end it checks the CRC, `kfw_end()`, the header and `kapp_idf_desc()`. Its `>ok` means the C5 activated the image, and the C5 may still not run it.

### The production row

`test component=c5` has five steps on the one channel:

1. The link, if it is down.
2. A fresh `kdrv_c5` snapshot.
3. A Wi-Fi scan.
4. The BLE start.
5. A BLE listen.

It judges, in order, and names the first failure:

1. The snapshot is valid.
2. The chip id is an ESP32-C5.
3. The descriptor answered (else "stock or old slave").
4. The project name is `kode_c5` (else "stock esp-hosted slave").
5. The esp-hosted versions are equal.
6. SDIO runs at `KBSP_C5_SDIO_WIDTH` lines and `KBSP_C5_SDIO_KHZ`.
7. The GPIO service answers on the card-detect pin.

The scan must find a network at or above -70 dBm (`KODE_C5_TEST_RSSI_MIN_DBM`). NimBLE must have synced and own an address, and the listen must hear an advertiser. A link that is up is left alone. A row that arrives while a job holds the channel becomes a debt (`test_pending`), paid after the services' debts.

The budget is 118 s (`KODE_C5_TEST_BUDGET_MS`). The owner's variant (`c5_test_request_user()`, for `sdk/kaos/autotest`) drops the scan floor and the advertiser requirement. The BLE stack stays up afterwards.

### The rest of the radios

With `CONFIG_KODE_LOWPOWER`, AO_Radio joins the rounds of AO_LowPower as `radio`, with `lowpower_join_radio()` because it takes part in radio wakes (`sdk/kaos/lowpower`). ASLEEP keeps the radios, so the P4 and the C5 sleep together. OFF leaves the link up.

**For ASLEEP**, with the link up and nothing in flight, AO_Radio hands the start of the sleep sentinel to its radio worker as a job, `KDRV_RADIO_JOB_SLEEP`, which runs `kdrv_radio_sleep()` (`sdk/kdrv/c5`). The round is answered ready when the sentinel is up. A busy job, a C5 update, a park for a jump, the production row, or a link that is starting or stopping makes AO_Radio answer busy (`radio busy`, `c5 update`, `c5 park`, `c5 test`, `link starting`, `link stopping`). A refusal of the start is also a busy answer with the reason: `c5 refused sleep`, `sdio not idle`, `wake line` or `radio sleep failed`. With no link up there is nothing to keep, and the answer is ready.

The sentinel reports each wake as a note, which reaches AO_Radio as `RADIO_SLEEP_NOTE_SIG`. The note decides nothing. A note that says the sentinel could not go back down (`STOOD_DOWN`) makes AO_Radio ask AO_LowPower for AWAKE with `lowpower_request()`, so that the state of the system matches the hardware. A note with a deaf C5 is counted. On AWAKE, AO_Radio stops the sentinel with another job, and the stop waits for the job in flight. A round that arrives while that stop is with the worker is owed, and AO_Radio decides it when the stop returns: ready for an OFF round once AO_Net has left, a new start for an ASLEEP round, and `c5 would not wake` when the stop fails. A round that arrives while any other sentinel job is out, a start or the stop for an OFF round, is answered busy (`radio busy`) at once.

**For a radio wake**, AO_Radio hears `LOWPOWER_STATE_SIG` with state RADIO: the channel's work fell due while the board sleeps. The work needs the bus and nothing else. AO_Radio stops the sentinel as it does for AWAKE and calls `lowpower_radio_done()` once the stop has returned, whatever its outcome, or at once when no sentinel is armed or the link is not up. The round for ASLEEP that ends the radio wake reaches AO_Radio and AO_Net alone, and AO_Radio answers it by starting the sentinel again.

**For OFF**, AO_Net leaves first. It closes its sessions and calls `radio_net_left()`, and it tells AO_Radio from its initial transition with `radio_lp_expect_net()` whether it takes part, so that AO_Radio does not wait for a call nobody would make. Then AO_Radio stops the sentinel if it runs, so that the bus is awake and nothing is suspended, and answers. If the stop fails, the answer is busy `c5 would not wake`. AO_Radio takes nothing down: no teardown job, no unmount, no hold of the C5 from here. Deep sleep stops the P4 whole and `khal_power_off_enter()` holds `C5_EN` low, which resets the C5. The link is built once per boot, and tearing esp-hosted down while the NimBLE host runs panics that host, so an OFF round leaves both alone. An OFF round that is abandoned finds the link as it was. A link that is already down, or lost while the round waits for AO_Net, is answered ready once AO_Net has left. A link lost while the stop for an OFF round is still with the worker refuses the round with `link lost`.

## Specifications

### Signals in

| Group | Signals | Meaning |
| --- | --- | --- |
| Link | `RADIO_UP_REQ_SIG`, `RADIO_DOWN_REQ_SIG`, `RADIO_STATE_REQ_SIG` | Start, end, republish. `RADIO_PARK_REQ_SIG` (`RadioParkEvt`: `target`) is AO_Boot's |
| Chip | `RADIO_TEST_REQ_SIG` (`RadioTestReqEvt`: `user`), `RADIO_FIELD_REQ_SIG` (`RadioFieldReqEvt`: `who`, `op`, `field`, `value`) | The row; one field of the `kdrv_c5` table, served from the snapshot in every state |
| Wi-Fi | `WIFI_SCAN_REQ_SIG`, `WIFI_JOIN_REQ_SIG` (`WifiJoinEvt`: `ssid[33]`, `pass[65]`, `remember`), `WIFI_LEAVE_REQ_SIG`, `WIFI_STATE_REQ_SIG`, `WIFI_RSSI_REQ_SIG`, `WIFI_FAST_REQ_SIG` (`WifiFastEvt`: `on`), `WIFI_AUTO_REQ_SIG`, `WIFI_LIST_REQ_SIG`, `WIFI_FORGET_REQ_SIG`, `WIFI_START_REQ_SIG`, `WIFI_STOP_REQ_SIG` | A join with an empty `pass` of a vault network is a vault join: the C5 uses the password it keeps. Leave is not forget. An accepted join also sets the Wi-Fi flag |
| BLE | `BLE_START_REQ_SIG`, `BLE_SCAN_REQ_SIG` (`BleScanReqEvt`: `ms`), `BLE_STOP_REQ_SIG`, `BLE_STATE_REQ_SIG`, `BLE_UNPAIR_REQ_SIG`, `BLE_PAIR_OPEN_REQ_SIG`, `BLE_LINK_SIG`, `BLE_PAIR_TAKE_SIG`, `BLE_PAIR_REJECT_REQ_SIG` | `BLE_UNPAIR_REQ_SIG` is refused while a console central is attached. The pairing pair exists with `KODEOS_BLE_PAIRING_PASSKEY` |
| Identity | `ID_SIGN_REQ_SIG` (`IdSignReqEvt`: `who`, `nonce[201]`), `ID_REQ_SIG` (`IdReqEvt`), `C5_WIPE_REQ_SIG` (`C5WipeReqEvt`), `KODE_SVC_RSP_SIG` (`KodeSvcEvt`), `KODE_SVC_TMO_SIG` | A second identity request while one is in flight is refused at once |
| Update | `FLASH_REQ_SIG`, `RADIO_RECV_REQ_SIG` (`AppRecvReqEvt`), `RADIO_INSTALL_GO_SIG`, `RECV_DATA_SIG`, `RADIO_RECV_IDLE_SIG`, `STORAGE_*`, `NET_STREAM_*` | The medium and the terminal paths |
| Internal | `RADIO_JOB_SIG`, `RADIO_SVC_JOB_SIG` (`KqpWorkerDoneEvt`), `RADIO_LINK_LOST_SIG` (`RadioLostEvt`), `RADIO_JOB_GUARD_SIG`, `RADIO_UNMOUNT_RETRY_SIG`, `RADIO_UNUSED_SIG`, `RADIO_TEST_STEP_SIG`, `RADIO_SCAN_MORE_SIG` | Workers, guards and self-posts |
| Rest | `LOWPOWER_ROUND_SIG`, `LOWPOWER_STATE_SIG`, `RADIO_SLEEP_NOTE_SIG` (`RadioSleepNoteEvt`: `kind`, `knocks`, `tx_sta`, `tx_serial`, `tx_hci`), `RADIO_NET_LEFT_SIG` (`RadioNetLeftEvt`: `round`) | With `CONFIG_KODE_LOWPOWER`: the rounds of the board's rest, a wake of the P4 that involved the C5, and AO_Net's word that its sessions are closed |
| Other | `CONFIG_REQ_SIG` (`wifi`, `ble`, `wifi_join`, `wifi_ps`), `C5_RUN_REQ_SIG` | Settings; the C5 asked to run |

### Signals out

| Signal | Event | Meaning |
| --- | --- | --- |
| `RADIO_STATE_SIG` | `RadioStateEvt`: `link`, `fw_known`, `fw_major/minor/patch`, `sd_hosted` | The link, on every change and on request |
| `WIFI_STATE_SIG` | `WifiStateEvt`: `state`, `ip4`, `ssid`, `rssi`, `enabled` | `enabled` is the service flag, not the association. AO_Net reads online from this |
| `WIFI_SCAN_ENTRY_SIG`, `WIFI_SCAN_END_SIG` | | One per network as the sweep produces it, then the end with the count; `ok=false` for a sweep cut short or a link lost |
| `WIFI_SAVED_ENTRY_SIG`, `WIFI_SAVED_END_SIG` | `WifiSavedEvt`: `ssid` | Names only; a stored password never comes back |
| `BLE_STATE_SIG` | `BleStateEvt` | Reports OFF from the flag when `ble_want` is false, whatever the driver says |
| `BLE_SCAN_ENTRY_SIG`, `BLE_SCAN_END_SIG`, `BLE_PAIR_SIG` | | The BLE listing and the pairing record |
| `RECV_READY_SIG`, `RECV_ACK_SIG`, `RECV_DONE_SIG`, `RECV_FAIL_SIG`, `RECV_STALL_SIG` | static events | The terminal install protocol; they cannot fail to allocate |
| `APP_FLASH_PROGRESS_SIG`, `APP_FLASH_DONE_SIG`, `APP_FLASH_FAIL_SIG` | | A C5 update, for the interface and AO_Power |
| `C5_RUNNING_SIG`, `TEST_RESULT_SIG` | named `c5` | The C5 has booted; the verdict |
| Posted | `RADIO_FIELD_RSP_SIG` to the asker, `ID_*` answers, `BOOT_RADIO_PARKED_SIG` to AO_Boot, `STORAGE_CMD_SIG` and the `STORAGE_*` loan to AO_Storage, `CONSOLE_RAW_REQ_SIG` to AO_Log, `NET_STREAM_*` to AO_Net | |

### Calls, numbers and settings

| Thing | Value |
| --- | --- |
| Calls | `radio_up_request()`, `radio_down_request()`, `radio_state_request()`, `radio_park_request(target)`, `wifi_scan_request()`, `wifi_join_request(ssid, pass, remember)` (returns false when the pool is dry), `wifi_leave_request()`, `wifi_start_request()`, `wifi_stop_request()`, `wifi_state_request()`, `wifi_rssi_request()`, `wifi_fast_request(on)`, `wifi_auto_request()`, `wifi_list_request()`, `wifi_forget_request(ssid)`, `ble_start_request()`, `ble_scan_request(ms)`, `ble_stop_request()`, `ble_state_request()`, `ble_unpair_request()`, `ble_pair_open_request()`, `ble_pair_reject_request()`, `c5_flash_request()`, `c5_test_request()`, `c5_field_request()`. With `CONFIG_KODE_LOWPOWER` also `radio_net_left(round)`, `radio_lp_expect_net(expected)` and `radio_sleep_last_note(out)`, which AO_Net and the console use. There is no Wi-Fi or BLE call in `kode.h`, so an app includes `kode_ao.h`. `*_state_request()` only after `kode_init()` returns |
| Stored settings | `kset/wifi`, `kset/ble` (the wishes, `on` or `off`), `kset/wifi_join`, `kset/wifi_ps`. A read gives the wish and `stored=yes`, not the state of the link |
| Console | `radio_up`, `radio_down`, `radio_info`, `c5`, `c5_help`, `c5_wipe`, `wifi_up`, `wifi_down`, `wifi_scan`, `wifi_join`, `wifi_auto`, `wifi_list`, `wifi_forget`, `wifi_leave`, `wifi_info`, `ble_up`, `ble_down`, `ble_scan`, `ble_info`, `ble_unpair`, `ble_pair`, `test component=c5` |
| Timers | `guard`, `unmount_retry`, one per KODE service, the C5-boot one-shot, and the receive idle timer (5 s between chunks, 30 s while raw mode is requested) |

## Gotchas

> **DANGER**
>
> **A bad C5 image can leave the radio unreachable.** The C5 slave has rollback disabled, and there is no cable to it. `install dst=c5` checks the digest, the header and the chip id before it writes, but an image that is the right chip and does not boot cannot be undone from the P4.

> **WARNING**
>
> **Your join returns nothing, or an empty answer, with the link off.** In `Radio_down` the first answer to a join is a refusal. Call `wifi_start_request()` (or `radio_up_request()`) first, and watch `WIFI_STATE_SIG`: `wifi_join_request()` is answered there once the attempt resolves.

> **WARNING**
>
> **Switching off the last service takes the link with it.** Code that needs the link with no service must call `radio_up_request()`, or the next service that goes off ends the link. A start again costs about 3 s.

> **NOTE**
>
> **The row can outwait its budget behind an autojoin round.** A round is a scan plus up to 20 s per join, and the row's budget is 118 s. The console prints `result=timeout` and drops the late verdict. The log says `waiting for the channel`.

## Example code

This turns Wi-Fi on, which joins a remembered network, and waits until a name resolves. The Kode Dot must remember a network already (`wifi_join` from the console once). You see `online` in the log within a few seconds of the join. The log level must be `i`: `config key="log" value=i`.

```c
/*
 * wifi_online.c - 2026-10-28 - from Kode with love
 *
 * Asks AO_Radio to switch Wi-Fi on, which brings the C5 link up and joins a
 * network the Kode Dot already remembers. Then it retries a name lookup until
 * it works and logs the address. The log shows online once the join is done.
 */
#include "kode_ao.h"

/* Bring the system up, switch Wi-Fi on, then wait for a working lookup. */
void app_main(void)
{
    uint32_t ip4 = 0;

    kode_init();
    wifi_start_request();

    while (kode_net_lookup("kode.diy", &ip4) != KODE_OK) {
        kode_delay_ms(1000);
    }
    KLOG_I("wifi_online", "online, kode.diy is %08X", (unsigned)ip4);

    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/kaos/radio`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/radio): `radio_ao.c`, the state machine, the workers, the reconcile, the update and the row.
- [`kaos.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/core/include/kaos.h): `kradio_link_t` and the `radio_*`, `wifi_*`, `ble_*` and `c5_*` calls.
- [`components/kdrv/radio`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/radio): the esp-hosted link, Wi-Fi and BLE drivers; see `sdk/kdrv/radio` and `sdk/kdrv/c5`.
