SDK
ESP32-C5 coprocessor
The second processor that is the only radio of a Kode Dot: its wiring, the esp-hosted link, the 21-row status table and the handover across a reset.
What it is
The ESP32-P4 that runs your app has no radio. Every wireless feature of a Kode Dot lives in a second chip beside it, an ESP32-C5 from Espressif: Wi-Fi on both bands and Bluetooth LE (it has no classic Bluetooth). The C5 also does four jobs that are not radio jobs, because it owns the pins behind them:
IO4enables the speaker amplifier.IO6is the card-detect switch of the microSD slot.IO5enables the +3.3V_EXT supply of the expansion header J3.IO24drives the two infrared emitters andIO23reads the infrared receiver.
The C5 runs its own firmware, the kode_c5 project in the SDK's c5/ submodule. It talks to the P4 over esp-hosted, an Espressif protocol that lets one chip use the radio and the pins of another. In esp-hosted's words the C5 is the slave and the P4 the host. Both sides run esp-hosted 2.12.9 and must stay on the same version.
The C5 also keeps the secrets of the board: the saved Wi-Fi passwords, the key that proves which unit this is, and the keys of the Bluetooth bonds. The P4 never holds them.
How it is connected
flowchart LR p4["ESP32-P4<br/>kdrv_radio"] -- "SDIO, 4 bits, 40 MHz" --> c5["ESP32-C5<br/>kode_c5"] p4 -- "GPIO23, C5_EN" --> c5 subgraph pins["Pins the C5 owns"] amp["speaker<br/>amplifier"] cd["microSD<br/>card detect"] ext["J3<br/>+3.3V_EXT"] ir["IR emitters,<br/>receiver"] end c5 -- "IO4" --> amp c5 -- "IO6" --> cd c5 -- "IO5" --> ext c5 -- "IO24, IO23" --> ir class p4 chip class c5 chip
The C5 is U6 on the main PCB, with its own crystal and antenna. TBD: the exact module part number, from the bill of materials.
Six wires join the two chips, all on the P4's SDMMC controller (slot 1). The reset line is C5_EN, GPIO23 of the P4 (KBSP_PIN_C5_EN), pulled up by R20 (10 kΩ), so a C5 that nobody holds starts by itself at power-up.
The board file sdkconfig.board states every one of these facts to esp-hosted, and kdrv_c5.c holds the clock and the width to KBSP_C5_SDIO_KHZ and KBSP_C5_SDIO_WIDTH with _Static_assert, so the two sources cannot disagree and still build.
Who owns the link
AO_Radio owns the C5 (KBSP_RES_C5: the enable line, the link, its pins and its flash). It hands every blocking job to its own worker, radio_worker, which calls kdrv_radio_run(). No other part of the system touches the link. The driver keeps no task, queue or flag of its own.
The pieces, from the bottom:
kdrv_radiobuilds and ends the link and runs the jobs on it, one at a time. The link carries one RPC at a time, so a Wi-Fi scan of about 38 s keeps it busy and nothing else gets through.kdrv_c5is the table of what the C5 says about itself. One pass of RPCs fills a snapshot; reading the table afterwards never touches the link.kdrv_c5svcsends the KODE requests (vault, identity, Bluetooth keys, park) to the C5 and routes the answers.- The handover note lets a C5 stay running across a deliberate reset of the P4.
Reset, hold and the SD controller
Starting the link pulses C5_EN low, so starting the link reboots the C5, unless the handover note below keeps it. esp-hosted also takes the P4's one SDMMC controller, which the microSD slot shares. AO_Radio unmounts the card before every start, and the card mounts again through a second path while the link is up. The card's own supply is the P4's pin (KBSP_PIN_SD_PWR, GPIO22), so a C5 reboot never affects it.
With CONFIG_KODEOS_C5_HOLD (off by default, on in kodeOS), AO_Radio drives C5_EN low at the start of a boot where neither Wi-Fi nor Bluetooth is wanted. A held C5 is off, and so is the speaker amplifier, because R16 (10 kΩ) pulls its enable to ground. The C5 is released when the link starts, or when AO_Audio asks for it before the first sound of a boot. When the last user of the radio goes away, AO_Radio ends the link and holds the C5 again.
The handover note
A deliberate restart can leave the C5 parked with its Wi-Fi station still associated, so the next firmware skips the join. The note is one 32-bit word in LP_SYSTEM_REG_LP_STORE15_REG, a register of the low-power domain that survives esp_restart() and every reset except a power-on. The high 16 bits are the magic 0x4B50 ("KP"), the low 16 bits are the role of the firmware the restart goes to: KDRV_RADIO_NOTE_OS (1) or KDRV_RADIO_NOTE_APP (2).
The first CONNECT of a boot reads the note and erases it. The C5 is kept only when every condition holds:
- The magic is there.
- The reset reason is a software reset.
- The target is this firmware's own role.
- This boot wants Wi-Fi (
req.wants_wifi).
Any other case pulses C5_EN and starts from a fresh C5: a panic, a watchdog, a power cut, a boot that does not want Wi-Fi, or a firmware built before the note. Only the first CONNECT of a boot can keep the C5. kdrv_radio_adopted() tells whether it did. The rule itself is kradio_note_keeps_c5(), a pure function in kdrv_radio_note.c that the host tests run.
The sleep sentinel
The sentinel keeps the esp-hosted link alive while the P4 light-sleeps. It exists only with CONFIG_KODE_LOWPOWER (the files kdrv_radio_sleep.c and kdrv_radio_sleep_policy.c). While the P4 sleeps, the C5 is told, esp-hosted's SDIO tasks are suspended, and the C5's wake line (KBSP_PIN_C5_WAKE_P4, GPIO2) or the P4's own outgoing traffic ends the sleep. Design rule 9 holds: the sentinel is a worker. It runs the handshake and reports what happened, and it decides no state of the system. The job is KDRV_RADIO_JOB_SLEEP. It runs through kdrv_radio_sleep() on the worker of AO_Radio (sdk/kaos/radio), and kdrv_radio_run() refuses it, because the note of a wake must reach the owner's own post.
kdrv_radio_sleep(true, note, arg) goes down. One task of the sentinel does all of it, so the bus has one conductor. The order is fixed:
- It marks the bus silent, before the C5 is told anything. A frame that the P4 queues from then on must wake the sentinel, or its caller would wait for an answer that cannot come. This step also arms the C5's wake line.
- It announces power save to the C5 and waits 20 ms (
KRADIO_SLEEP_PS_WAIT_MS), so that the C5 finishes what it was sending. - It suspends the three SDIO tasks of esp-hosted (
sdio_write,sdio_read,sdio_process_rx), but only when all of them are blocked. A task that stops in the middle of an SDIO transaction would hold the mutex of the bus, and the way back up would wait for it for ever. The sentinel looks up to 21 times, 5 ms apart (KRADIO_SLEEP_SETTLE_TRIES,KRADIO_SLEEP_SETTLE_MS).
If the wake line does not arm, the C5 does not take the announcement, or the SDIO tasks never settle, the bus is left as it was (a C5 that was told is told again that power save is off) and the call returns KODE_ERROR, KODE_IO_ERROR or KODE_DENIED. A bus that nobody told the C5 about must not go quiet.
The way up starts on the C5's line or on a frame the P4 queued. A hook on esp_hosted_tx() (the linker's --wrap) catches every outgoing frame and counts it by interface: station, serial (RPC) and HCI (BLE). The sentinel knocks the C5 awake first, because a message to a sleeping C5 is not answered and an SDIO write that reaches it times out, which makes esp-hosted restart the board. The first power-save-off message only wakes the C5, which wakes on its CMD line, so it is repeated up to 100 more times, 10 ms apart, until the C5 answers (KRADIO_SLEEP_OFF_RETRIES, KRADIO_SLEEP_OFF_RETRY_MS). Then the sentinel waits 50 ms (KRADIO_SLEEP_RESUME_WAIT_MS) and resumes the SDIO tasks. A short awake window follows: 100 ms (KRADIO_SLEEP_AWAKE_MS), and another 3 s (KRADIO_SLEEP_GAP_MS) each time the BLE GAP count of kdrv_ble_activity() has moved. A connect and its encryption need the board up longer than the traffic that announced them. A command that waits for the sentinel's task ends the repeated window, so the wake does not wait for the link to fall silent. Then the note is called. The bus goes down again, unless a command waits: then it stays up and the command decides.
A C5 that never answers leaves the SDIO tasks suspended, and the wake is noted as KDRV_RADIO_WOKE_DEAF. The transport is never resumed against a deaf slave. If the bus cannot go down again, the sentinel stands down: the bus stays up, its power-management lock stays held so the P4 does not light-sleep over a live bus, and the note is KDRV_RADIO_WOKE_STOOD_DOWN. The next start or stop of the sentinel lets the lock go. kdrv_radio_sleep(false, ...) serves the C5 one last time and leaves the bus awake. A stop returns KODE_IO_ERROR when the C5 did not answer, and the sentinel stays armed.
The note (kdrv_radio_sleep_note_t) has the kind (WOKE_C5, WOKE_TX, WOKE_DEAF or WOKE_STOOD_DOWN), the knocks that the wake needed, and the totals of frames queued while the bus was silent (tx_sta, tx_serial, tx_hci). It is called in the sentinel's task and must be short. kdrv_radio_sleep_stats() gives the counters since boot, which lowpower_info prints. The rules (the order, the retries, the windows) are in kdrv_radio_sleep_policy.c, free of ESP-IDF and driven through a table of functions, so the host tests run the real rules over fakes.
The C5's half of the sleep is in its firmware: light sleep on the host's announcement, a wake on its SDIO CMD line, a frame filter and the amplifier's enable (sdk/kaos/lowpower).
The C5 firmware
The firmware is the slave example of esp-hosted-mcu 2.12.9 with Kode's additions, built by tools/build_c5.sh in the kodeOS repository (IDF 6.0.2) and shipped as c5_slave.bin in the factory FAT. The additions are the card-detect input, the amplifier enable, the infrared service, the KODE services (vault, identity, Bluetooth keys, park) and a measurement of the 32.768 kHz crystal at boot. The image's project name is kode_c5; stock esp-hosted says network_adapter.
Kode's additions live in c5/main/kode_*. The esp-hosted files keep their names and Espressif's style, so a new esp-hosted release still merges.
| File | What it does |
|---|---|
kode_svc.c | The KODE services' dispatcher. esp-hosted's receive thread copies a request and queues it, and one task answers the requests one at a time, so no service needs a lock. It answers INFO and PARK itself and hands every other request to its service's handler |
kode_svc_frame.c | The frame check, and the rule that the first well-formed request marks an updated C5 image valid |
kode_vault.c, kode_vault_list.c | The Wi-Fi vault. The host tells it a password once, after a join it made itself worked (REMEMBER). From then on the host asks it to JOIN a network by name and the C5 sets its own station configuration. LIST answers names only |
kode_id.c, kode_id_nonce.c, kode_id_serial.c | The unit's identity: the key, the CSR, the certificate, the signed challenge and the serial |
kode_btkeys.c, kode_btkeys_cache.c, kode_hci_filter.c | Bluetooth bond keys behind tokens, and the swap on the way into the controller |
kode_ir.c | The infrared block: one task owns the RMT on IO24 and IO23 |
kode_park.c | The park |
kode_wire.h, kir_wire.h | The two wires, identical copies of the SDK's (kdrv/radio/include/, kdrv/ir/include/) |
Six of them hold no ESP-IDF at all (kode_vault_list.c, kode_id_nonce.c, kode_id_serial.c, kode_svc_frame.c, kode_btkeys_cache.c, kode_hci_filter.c), and c5/test/run.sh tests them on the computer.
The vault keeps one NVS blob, kvault/nets, behind a magic and a version. Any other blob reads as an empty vault. A password lives there and nowhere else on the Kode Dot.
The identity key is born in the C5's Key Manager, in random mode. NVS keeps the recovery information that loads the key again on this chip (kid/ri), never the key. Every signature goes through a PSA opaque key on the ECDSA peripheral, so mbedTLS never sees the scalar. A new key is the Key Manager's second in a row: its first random key after a C5 reset comes from a small set of values that is the same on every chip.
The key signs two things: the CSR the C5 builds itself, and the cloud's challenge as kode-auth/v1\n plus the nonce. The C5 adds the prefix, so no program on the P4 can have the key sign anything else. LOCK, sent by the factory after the unit's first login, closes CSR and CERT_SET for good. The identity has an NVS partition of its own, fctry, because app_main() erases the default one when its format changes or it fills, and that would lose the key. Nothing in it is secret, so it is not encrypted. The serial in the CSR is the C5's MAC in the encoding khal_identity_encode() uses on the P4, and the host tests pin the same vectors on both sides.
The Bluetooth keys. NimBLE on the P4 keeps a 16-byte token where a bond's key would be. A key reaches the controller in two HCI commands only, LE Long Term Key Request Reply and LE Add Device To Resolving List. The filter swaps the token in either for the key just before the packet enters the controller. A sketch's own keys pass untouched. The C5 keeps the keys in NVS namespace kbtkeys and in a RAM table of twelve slots: three bonds on the P4 (CONFIG_BT_NIMBLE_MAX_BONDS), each with at most the Kode Dot's own LTK, the peer's LTK and the peer's IRK. One short spinlock guards the table, because the receive task reads it and the service task writes it.
The infrared block opens nothing until the host first asks, so a C5 whose host never asks leaves IO24 and IO23 as they reset. The carrier duty is 0.50, the most a TSOP-class receiver is tuned for. R22 and R23 (39 Ω) set the peak current, about 69 mA per emitter, so the duty raises only the average. TBD: the range at 0.50 has not been measured against Kode-Dot-Val/10-IR, which was taken at 0.33.
A park keeps the Wi-Fi station associated, so the next firmware adopts the association instead of joining again. It gives up everything that belonged to the host that is leaving: Bluetooth (if the host started it), the infrared channels, the drive of the J3 supply pin, the station frames and the SDIO link. The C5 answers the park first, then parks. The next ESP_OPEN_DATA_PATH un-parks it and hands the station back.
The files of the component:
| File | What it does |
|---|---|
include/kdrv_radio.h, src/kdrv_radio.c | The link: CONNECT, DISCONNECT, every job (Wi-Fi, BLE, C5 update, status snapshot, J3 supply), the peer-data seam, C5_EN hold, the handover note, the link-lost report |
src/kdrv_radio_sleep.c, src/kdrv_radio_sleep.h, src/kdrv_radio_sleep_policy.c | The sleep sentinel and its rules, with CONFIG_KODE_LOWPOWER |
src/kdrv_radio_note.h, src/kdrv_radio_note.c | The note's word layout and the rule that decides whether the C5 stays. No ESP-IDF in it |
include/kdrv_c5.h, src/kdrv_c5.c | The status table, its snapshot, the string getters, the J3 supply call |
include/kdrv_c5svc.h, src/kdrv_c5svc.c | The P4's end of the KODE services: send, call, listen, abandon, forget |
include/kode_wire.h | The KODE wire format. The same file lives in the C5 firmware |
Specifications
The board
| Thing | Value |
|---|---|
| Chip | ESP32-C5, target esp32c5, esp_chip_model_t 23 |
| Link | esp-hosted 2.12.9 on both sides, SDIO slot 1, 4 data lines, 40 MHz |
| SDIO pins (P4) | CLK GPIO50, CMD GPIO51, D0 GPIO49, D1 GPIO48, D2 GPIO53, D3 GPIO52 |
| Reset line | C5_EN = GPIO23 (KBSP_PIN_C5_EN), R20 pull-up, high runs, low resets |
| C5 pins used for the P4 | IO4 amplifier enable (output, high from boot), IO6 card detect (input, pull-up), IO5 J3 supply enable (R72 pull-up holds the rail on), IO24 IR transmit, IO23 IR receive |
| Reaching the pins | RPC GPIO service, about 66 edges per second. Never use it where timing matters. The IR pins go through the C5's own infrared service instead |
| Link-up wait | 10 s for CP_INIT (RADIO_CP_INIT_MS) |
| Reset pulse | C5_EN low for 10 ms, then 1500 ms for the C5 to boot (the same figures esp-hosted uses) |
| Current | Measured on a Kode Dot: 46.5 mA with the C5 held in reset, 73.8 mA with the link up and the station idle, 141.1 mA while the C5 scans |
The status table
kdrv_c5_get(field, &value) reads one row, in the unit of kdrv_c5_field_info(). Every row is read-only; kdrv_c5_set() refuses all of them. A row is refused with KODE_DENIED when its group was not filled by the last refresh. Seven rows always answer: those marked "own, always".
| Field | Group | Unit and range | What it is |
|---|---|---|---|
valid | own, always | bool | A refresh has succeeded since the last forget |
chip_id | identity | code 0..65535 | esp_chip_model_t; 23 is an ESP32-C5 |
fw_major, fw_minor, fw_patch | identity | code 0..255 | The esp-hosted version the slave runs. The slave sets the finer fields to -1 |
host_major, host_minor, host_patch | own, always | code 0..255 | The esp-hosted version this firmware was built against |
app_desc | own, always | bool | The slave answered the image-descriptor RPC |
project_kode | descriptor | bool | The image's project name is kode_c5 |
mac_hi, mac_mid, mac_lo | identity | code 0..65535 | The station MAC, two bytes a row |
reset_reason | own, always | code 0..255 | esp_reset_reason_t of the C5's last boot, carried by CP_INIT; 0 before any link-up |
link_ms | own, always | ms 0..60000 | Connect to CP_INIT at the last start; 0 before any |
heap_free_kb, heap_largest_kb | heap | KB | Free internal heap of the C5 and its largest free block |
sdio_khz | identity | kHz | The clock the transport negotiated |
sdio_width | identity | bits 0..8 | Data lines in use, 1 or 4. A link on one line has three that do not reach the C5 |
sd_cd | pins | bool | IO6 as read: the card-detect level. Its polarity is not confirmed on the board, so it is a reading and never a verdict |
pwr_ext | pins | bool | IO5 as read: the J3 supply enable, 1 is on |
The identity group (chip, version, MAC, SDIO configuration) fails together: if any of its four RPCs does not answer, kdrv_c5_refresh() returns KODE_ERROR and the snapshot stays invalid. The descriptor, heap and pin groups are independent. One that does not answer is recorded as absent and the refresh still succeeds, which is how an older slave without the descriptor RPC is told apart from a missing one.
What a table cannot hold has its own getter, each refused with KODE_DENIED when the slave did not send it: kdrv_c5_target(), _project_name(), _app_version(), _idf_ver(), _build_date(), _build_time(), _elf_sha() (32 bytes) and _mac() (6 bytes).
The driver
| Call | What it does |
|---|---|
kdrv_radio_run(req) | Runs one job to its end and returns its status. Blocks. Worker only. KODE_DENIED for a NULL request or a job this build lacks |
kdrv_radio_link_up() | True from a finished CONNECT until a drop, a DISCONNECT or a C5 image activation |
kdrv_radio_c5_hold(hold), _c5_held() | Drive C5_EN low or high. KODE_DENIED once esp-hosted has the line, from CONNECT until its DISCONNECT ends |
kdrv_radio_note_leave(target) | Write the handover note. Call it only after the C5 answered the park with OK, right before the restart |
kdrv_radio_adopted() | True when the last CONNECT kept a parked C5 |
kdrv_radio_fwversion(&ver) | The version cached by the last CONNECT; KODE_DENIED before one |
kdrv_radio_image_header_ok(head, len) | Cheap gate before a C5 update: length, ESP image magic, and chip id ESP_CHIP_ID_ESP32C5. A kodeOS or app image is refused by name. Not a verification |
kdrv_radio_on_link_lost(cb) | One callback for a dropped transport (KODE_ERROR) or a mute C5 (KODE_TIMEOUT). It must post and return |
kdrv_radio_sleep(sleep, note, arg), kdrv_radio_sleep_stats(&out) | Start or stop the sleep sentinel, and read its counters. Only with CONFIG_KODE_LOWPOWER. The call blocks until the sentinel's task has done it, so it belongs to a worker, one caller at a time |
kdrv_radio_peer_listen(id, cb, up, ctx), _peer_send(id, data, len) | The seam for another driver's own service on the C5 (infrared uses it). Three listener slots. The callback runs on esp-hosted's thread |
kdrv_c5_refresh(), _forget() | Take or drop the snapshot. Refresh blocks: worker only |
kdrv_c5_exp_power(on) | Drive IO5 over the RPC GPIO service and read it back. Worker only. The C5 holds the level until told again or rebooted |
The jobs kdrv_radio_run() knows, by group:
| Group | Jobs |
|---|---|
| Link | CONNECT, DISCONNECT |
| Wi-Fi | WIFI_SCAN, WIFI_JOIN, WIFI_LEAVE, WIFI_STOP, WIFI_RSSI, WIFI_PS, WIFI_PREPARE, WIFI_AWAIT, WIFI_ADOPT |
| Bluetooth | BLE_START, BLE_ADV, BLE_SCAN, BLE_QUIET, BLE_STOP, BLE_UNPAIR |
| The C5 itself | C5_BEGIN, C5_WRITE, C5_FINISH, C5_ABORT, C5_INFO, EXP_POWER |
| Sleep | SLEEP: start (req.on true) or stop the sentinel. Only with CONFIG_KODE_LOWPOWER |
CONNECT runs these steps:
- Platform setup once (NVS and the default event loop).
- Drain a stale
CP_INIT. - Apply the handover rule.
- Give the reset line to esp-hosted.
esp_hosted_init()andesp_hosted_connect_to_slave().- Wait for
CP_INIT. - Read the slave version.
- Take the snapshot.
- Register the peer listeners.
A snapshot failure is never fatal. If CP_INIT does not arrive in 10 s the job returns KODE_TIMEOUT and logs a question about whether the C5 carries the slave firmware at all.
DISCONNECT stops Wi-Fi and BLE first, while a channel still exists to tell them. It then calls esp_hosted_deinit() with the loss report masked, and clears the local state of kdrv_c5, kdrv_wifi and kdrv_ble.
A C5 update is C5_BEGIN, then C5_WRITE, then C5_FINISH, which ends the session and activates the image. C5_WRITE sends pieces of 1500 bytes (the size of the vendor's example; esp-hosted sleeps 10 ms in each write, so this also sets the pace). After activation the C5 reboots into the new slot and cannot go back, so the driver marks the link down at once. The data pointer of C5_WRITE is borrowed from AO_Storage's loan buffer; it is the one pointer in the whole interface that is not a copy.
CP_INIT carries the reason the C5 booted. The event task of esp-hosted records it with kdrv_c5_note_reset(). The worker records the connect time with kdrv_c5_note_link_ms().
The KODE services
Four services share one pair of message ids over esp-hosted's peer data: KODE_MSG_REQ 0x4B4F4401 out, KODE_MSG_RSP 0x4B4F4481 back. Every message starts with an 8-byte header (version, service, op, seq, status, reserved, len, little-endian). The P4 drops an answer whose version is not KODE_WIRE_VERSION (3) or whose seq does not match. The largest payload is 2048 bytes.
| Service | Operations | What it holds |
|---|---|---|
SELF (0) | INFO (1), PARK (2) | INFO answers a 24-byte kode_info_t: vault count, key count, identity flags, heap figures, token swaps and misses. PARK keeps the Wi-Fi station and drops the host |
VAULT (1) | REMEMBER, JOIN, LIST, FORGET, WIPE | Up to 8 networks (KODE_VAULT_NETS_MAX), SSID up to 32 bytes, password up to 64. After a join that worked, the P4 sends the name and password once; afterwards it knows only the names |
ID (2) | CSR, CERT_SET, CERT_GET, SIGN_AUTH, LOCK, STATE | The unit's key lives in the C5's Key Manager. It signs its own certificate request and a cloud challenge (a nonce of 1 to 200 characters of [A-Za-z0-9._-], checked on both chips). Nothing signs bytes the P4 chose. LOCK closes CSR and CERT_SET for good. A certificate pair is at most 1536 bytes |
BTKEYS (3) | PUT, DEL, WIPE, HAS | Bond keys behind 16-byte tokens (sdk/kdrv/radio explains the tokens) |
Status bytes: OK 0, REFUSED 1, BAD_REQUEST 2, NOT_FOUND 3, STORAGE 4, LOCKED 5. PARK answers OK when a station is associated, REFUSED when there is nothing to keep, and BAD_REQUEST from an image that predates the park.
kdrv_c5svc has two ways to ask:
kdrv_c5svc_send()returns after one RPC and the answer arrives later through the listener, on esp-hosted's thread. It is the call for an Active Object's worker, which posts the AO an event from the listener.kdrv_c5svc_call()waits for the answer and is for the NimBLE host task and the radio worker, never an AO.
One request may be in flight per service. kdrv_c5svc_abandon() gives up on one, and a late answer is dropped. kdrv_c5svc_forget() clears everything in flight when the link goes down.
Gotchas
Example code
An app that drives a module on the expansion header powers it through the C5's IO5. This one switches the supply on for five seconds and off again. With a multimeter on pins 2 and 19 of J3, you read 3.3 V for five seconds, then 0 V.
/*
* module_power.c - 2026-10-28 - from Kode with love
*
* Switches the +3.3V_EXT supply of the expansion header J3 on for five
* seconds, then off. The supply is enabled by pin IO5 of the ESP32-C5, so
* the request goes to that chip. With a multimeter on pins 2 and 19 of J3
* you read 3.3 V for five seconds, then 0 V.
*/
#include "kode.h"
/* Bring the system up, run the supply for five seconds, then idle. */
void app_main(void)
{
kode_init();
/* Posts to the owner of the C5 and returns at once. */
kode_exp_power(true);
kode_delay_ms(5000);
kode_exp_power(false);
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/kdrv/radio: the link, the table, the services and the Wi-Fi, Bluetooth and network drivers (sdk/kdrv/radio).kdrv_c5.h: every field of the table and the getters.kdrv_radio.h: the jobs, the hold and the handover note.components/kaos/radio: AO_Radio, the owner (sdk/kaos/radio).