kodeOS internals
Power and sleep
The four levels of rest of a Kode Dot (awake, asleep, off and ship mode), who asks for each, what wakes it, and why a cable keeps it awake.
What it is
A Kode Dot rests at four levels. Awake is everything running. Asleep keeps the radios and shows the time. Off is deep sleep with the radios stopped. Ship mode opens one switch inside the charger chip and cuts the battery from the board. Each level is deeper than the one before, and each has its own door.
One Active Object decides the first three: AO_LowPower (sdk/kaos/lowpower). It asks every owner of a device to put that device at rest and changes the state only when all of them say they are ready. The fourth level belongs to AO_Power, the owner of the charger (sdk/kaos/power). Ship mode is not a state of the board and it asks nobody.
In this page and in the SDK, "sleep" means the rest of the first three levels. The console verb sleep and the call kode_power_sleep() take the Kode Dot to asleep. The verb ship_mode is the only way to ship mode. The face has a Rest of its own, a dimmed face while the board stays awake, set by kody_sleep (internals/gui/face). It is a look of the screen and not a state of AO_LowPower, and sleep_after is a different time.
A cable keeps the Kode Dot awake. While USB power is present, charging or not, a request for asleep or off is refused with cable: external power. This is the wiring and the design: the cable also keeps the USB console, the dock console and the debugger alive.
How it is connected
flowchart TD idle["idle time: sleep_after"] --> lp["AO_LowPower"] offafter["time asleep: off_after"] --> lp con["console: sleep, power_off"] --> lp app["app: kode_power_sleep, kode_power_off"] --> lp card["Turn off card"] --> lp lp -- "round, state" --> owners["every owner that joined"] lp --> result["ASLEEP or OFF, or a refusal with a reason"] ship["console: ship_mode"] --> pw["AO_Power"] pw --> bq["BQ25896: BATFET_DIS"] class lp ao class pw ao class bq chip class con ext class app app
The four levels
| Level | What it is | Doors in | Doors out |
|---|---|---|---|
| Awake | Everything runs. The P4 holds a lock that keeps it out of light sleep | Boot, any wake | The idle time, or a request |
| Asleep | The screen is dimmed and shows the time. Every owner has put its device at rest. The P4 and the C5 sleep light whenever they are idle. Wi-Fi, Bluetooth and the Kode channel stay up | sleep_after, the verb sleep, kode_power_sleep() | BOT, a message, a console line, a cable, an owner's time |
| Off | The screen is off. The P4 is in deep sleep and the C5 is held in reset. The radios are stopped | off_after, the verb power_off, kode_power_off(), the Turn off card | BOT held for about a second. The wake is a boot |
| Ship mode | The BATFET is open. Nothing runs, the Kode Dot draws microamps, and only the charger stays awake | The verb ship_mode | BOT (the QON pin) or USB |
The transitions, the rounds, the retries and the refusals are on sdk/kaos/lowpower. This page keeps the view of the whole and the part that touches the battery.
The ways to ask
| Way | What it asks for |
|---|---|
The idle time (sleep_after, 2 min by default), in a firmware built with CONFIG_KODE_LOWPOWER_IDLE. kodeOS has none: its Rest decides | Asleep |
The time asleep (off_after, 1 h by default) | Off |
The console verb sleep, or kode_power_sleep() from an app | Asleep |
The console verb power_off, kode_power_off() from an app, or the answer of the Turn off card | Off |
The console verb ship_mode | Ship mode. Nothing else reaches it |
sleep and power_off answer >ok when the Kode Dot has changed state, and >err code=4 msg="..." when it will not. ship_mode answers >ok at once, because on battery nothing would be left to answer with. The syntax of the three verbs is in internals/command-set.
The Turn off card asks for off. The face's bottom button held fills a red arc, and the Turn off circle of the Settings screen does the same job: both raise a card that says what comes after, that the Kode Dot switches off and the red button turns it on again. On a cable the card says it cannot and its button is grey. A tap on the card's button, or the top button, calls kode_power_off(). BOT or a swipe down says no and switches nothing off. The card is described on internals/gui/sheets.
A round, owner by owner
Every owner that joined puts its own device at rest on the round. Nothing else does it for them.
| Owner | Asleep | Off |
|---|---|---|
| AO_Display | Dims the glass, takes the DSI link down, keeps the panel's memory and writes the time into it each minute | The panel in sleep-in, the link down |
| AO_Gui | Draws the face with Kody on the first frame of his scene and hands the digits of the time to AO_Display | Draws a black page |
| AO_Touch | The controller's interrupt off and auto-sleep on | The same |
| AO_Imu, AO_Haptic, AO_Nfc, AO_Rfid, AO_LedMgr | The device at standby or off | The same |
| AO_Audio | The codec closed. The C5 lowers the amplifier's enable | The codec closed. The C5 is in reset, so the amplifier stays off |
| AO_Storage | Files closed, the card idle | The same. Deep sleep holds the card's supply off |
| AO_Power | The ADC back to one-shot. The gauge stays readable | The same |
| AO_IoExp | Arms the expander's interrupt as the light-sleep wake | The same line, GPIO6, is the wake source of deep sleep |
| AO_Radio | Starts the sleep sentinel, so the C5 sleeps with the P4 | Stops the sentinel and leaves the link up. Deep sleep then holds the C5 in reset |
| AO_Net | Keeps the sessions | Closes the Kode channel and the MQTT session first |
| AO_Scheduler | Tells AO_LowPower when its earliest entry falls due | Nothing to rest |
| AO_Console, AO_Boot, AO_AppMgr, AO_Autotest | Nothing to rest | Nothing to rest |
An owner that has work it must not cut answers the round busy. For asleep the Kode Dot then stays awake. For off, which is an order, the owner is given up to 10 s to finish and the Kode Dot goes off after it, finished or not. The reasons (file write, audio playing, flash write and so on) are on sdk/kaos/lowpower, and each owner's own page says when it answers them.
What wakes the Kode Dot
| From | Wake | What happens |
|---|---|---|
| Asleep | BOT pressed | Awake, no boot |
| Asleep | A message from the Kode channel, or from an MQTT client of yours | Awake, no boot |
| Asleep | A console line on BLE or on the cable | Awake, no boot |
| Asleep | A cable, seen at the minute | Awake within 60 s. The charger is read once a minute |
| Asleep | An owner's time falls due | A radio wake for the Kode channel, which does not light the screen. A whole awake for AO_Scheduler |
| Off | BOT held for about a second | A boot: kodeOS starts again and lands on the face |
| Ship mode | BOT held on the QON pin, or USB power | A cold power-on: kodeOS starts and lands on the face |
Nothing else wakes asleep or off. TOP, the pad, the touch panel and motion do not.
From off, a tap is not enough. The expander's interrupt line is low when any of the five buttons on the expander is down, so the wake alone does not say that BOT was pressed. The gate at the top of kode_init() reads the expander about a second after the press, and a tap released before that sends the Kode Dot back to deep sleep. Hold BOT for about a second.
The Kode channel does not light the screen. While the Kode Dot is asleep, the channel's renewal and its tries wake the radio alone: the P4 runs, the C5 link is awake, the glass and the other devices stay at rest, and it is asleep again as soon as the work is done. A broker that cannot be reached costs these short wakes and never a lit screen.
The cable
While VBUS is present the Kode Dot neither sleeps nor powers off, and a request is refused before any round begins. Asleep reads the charger once a minute, and a cable that appeared wakes it within 60 s. Off stays off until BOT is held, with the cable in or not, and the charger charges the cell either way.
TBD: OFF waking every few minutes to look for a cable is being built on the SDK branch feat/off-cable-check. This paragraph says what it does once that branch lands.
Ship mode
power_ship_request() posts one event that is made once, at the start of the program, so it cannot fail to allocate. The console verb ship_mode is its only caller. No app can reach ship mode: the facade has no call for it, so a request to switch the Kode Dot off cannot cut the battery by mistake.
AO_Power says everything before the write. On battery the board is off as soon as the register is written, and a line printed after it would never leave the console. The handler of POWER_SHIP_REQ_SIG runs in this order:
- Publish
POWER_SHIP_SIG, a warning with no payload that every subscriber receives before the write. - Save the wall clock to NVS (
kdrv_clocks_wall_keep(true)), so the time survives the loss of supply. It comes back marked approximate. - Log two notices:
ship mode: opening the BATFETandwake with BUTTON_BOT (QON) or by plugging USB. - Call
kdrv_bq25896_ship_mode(false). - If the call returns
KODE_OK, a cable is feeding the board: logbattery path open; VBUS is keeping the board up - it powers off when USB is unplugged. Any other result logsship mode FAILED (<code>): the board is NOT going to switch off.
AO_Power does not retry and does not hide a failure. A board with no charger still starts: AO_Power logs that bat_info will have nothing to report, and it refuses a ship request with the error line.
AO_Audio hushes the codec in the warning step. The ES8311 has no idle timer, so ship mode and a round of rest are the only events that close it. AO_Audio silences the converter with one register write, then asks its worker to close the codec. The close matters when the board stays up on USB. Ship mode is not a round: the owners of AO_LowPower are not asked to rest first.
On USB the Kode Dot cannot switch off. The USB power line feeds the rail directly, so opening the BATFET only takes the battery out of the path. The board keeps running and goes dark when someone unplugs the cable.
Specifications
| Thing | Value |
|---|---|
| Console verbs | sleep (asleep), power_off (off), ship_mode (ship mode), lowpower_info (state and counters). The grammar is in internals/command-set |
| Facade calls | kode_power_sleep() and kode_power_off(), both void. They return at once, and the answer is LOWPOWER_STATE_SIG or LOWPOWER_REFUSED_SIG |
| Stored settings | sleep_after: 30s, 1m, 2m (default), 5m, 10m, never. off_after: 1h (default), 6h, 24h, 3d, 7d, never. kodeOS has no sleep_after (CONFIG_KODE_LOWPOWER_IDLE off) |
| Events of the rest | LOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIG, LOWPOWER_REFUSED_SIG (sdk/events) |
| Ship mode events | POWER_SHIP_REQ_SIG is posted to AO_Power only. POWER_SHIP_SIG is published before the write |
| Register write | One read-modify-write of REG09: BATFET_DIS (bit 5) set; BATFET_DLY (bit 3) cleared when delayed is false, so the cut is immediate; BATFET_RST_EN (bit 2) left as it was |
| Driver result | KODE_DENIED when the driver was never started, KODE_ERROR when the read or write fails. On battery it never returns; KODE_OK means USB is present |
| Ship mode current | 12 µA from the cell (sdk/kdrv/bq25896) |
| Ship mode wake | BOT held 0.9 to 1.3 s on the QON pin, or USB power |
| Off wake | BOT held about a second. The expander's interrupt, GPIO6, is the one wake source of deep sleep (ext1, low) |
pmic field="batfet_dis" value=1 | Refused with KODE_DENIED: it would reach ship mode without saying why. AO_Power points to ship_mode |
| Build options | CONFIG_KODE_LOWPOWER for asleep and off, off in the SDK and on in kodeOS. Without it the two facade calls log low power not built and sleep and power_off answer not_built. Ship mode needs no option |
bat_info takes a fresh reading, publishes it, logs a summary and dumps the registers of both chips. It prints the fault register twice: the first value is everything since the last read, the second is what is true now. When the charger does not answer it publishes nothing and the command times out.
Gotchas
Example code
An app that asks for off five seconds after it starts. On battery the log shows entering OFF (request) and the Kode Dot goes dark until BOT is held. With a cable in, the log shows request for OFF (source 254) refused: cable: external power and the app keeps running.
/*
* power_off.c - 2026-11-03 - from Kode with love
*
* Starts the system, waits five seconds and asks AO_LowPower for OFF. On
* battery the log shows "entering OFF (request)" and the Kode Dot goes dark;
* hold BOT for about a second to turn it on. With USB plugged in, the log
* shows that the request was refused because of the cable, and after three
* more seconds the app hands the Kode Dot back to kodeOS.
*/
#include "kode.h"
#define TAG "power_off"
#define BEFORE_OFF_MS 5000U
#define USB_GRACE_MS 3000U
/* Wait, ask for OFF, and go home if the cable kept the Kode Dot awake. */
void app_main(void)
{
kode_init();
KLOG_I(TAG, "asking for OFF in %u ms", (unsigned)BEFORE_OFF_MS);
kode_delay_ms(BEFORE_OFF_MS);
kode_power_off();
kode_delay_ms(USB_GRACE_MS);
KLOG_I(TAG, "still running: the request was refused, going home");
kode_exit_to_os();
}Not compiled against the SDK. TBD: a build and a run on a Kode Dot on battery.
In the SDK
components/kaos/lowpower: AO_LowPower, the states and the rounds.components/kaos/power/src/power_ao.c: AO_Power, the handler ofPOWER_SHIP_REQ_SIGand the battery poll.components/kdrv/power/src/kdrv_bq25896.c:kdrv_bq25896_ship_mode().components/khal/power: the awake lock, the wake lines and deep sleep.components/kgui/kgui_ao.c: the Turn off card andgui_power_off().