SDK
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
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
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
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
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:
- A wanted service.
- A direct ask (
link_asked, set byradio_up, the row or a C5 update). - A job in flight.
- 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:
- The link, if it is down.
- A fresh
kdrv_c5snapshot. - A Wi-Fi scan.
- The BLE start.
- A BLE listen.
It judges, in order, and names the first failure:
- The snapshot is valid.
- The chip id is an ESP32-C5.
- The descriptor answered (else "stock or old slave").
- The project name is
kode_c5(else "stock esp-hosted slave"). - The esp-hosted versions are equal.
- SDIO runs at
KBSP_C5_SDIO_WIDTHlines andKBSP_C5_SDIO_KHZ. - 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
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.
/*
* 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:radio_ao.c, the state machine, the workers, the reconcile, the update and the row.kaos.h:kradio_link_tand theradio_*,wifi_*,ble_*andc5_*calls.components/kdrv/radio: the esp-hosted link, Wi-Fi and BLE drivers; seesdk/kdrv/radioandsdk/kdrv/c5.