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
Ways to ask, two owners

The four levels

LevelWhat it isDoors inDoors out
AwakeEverything runs. The P4 holds a lock that keeps it out of light sleepBoot, any wakeThe idle time, or a request
AsleepThe 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 upsleep_after, the verb sleep, kode_power_sleep()BOT, a message, a console line, a cable, an owner's time
OffThe screen is off. The P4 is in deep sleep and the C5 is held in reset. The radios are stoppedoff_after, the verb power_off, kode_power_off(), the Turn off cardBOT held for about a second. The wake is a boot
Ship modeThe BATFET is open. Nothing runs, the Kode Dot draws microamps, and only the charger stays awakeThe verb ship_modeBOT (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

WayWhat 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 decidesAsleep
The time asleep (off_after, 1 h by default)Off
The console verb sleep, or kode_power_sleep() from an appAsleep
The console verb power_off, kode_power_off() from an app, or the answer of the Turn off cardOff
The console verb ship_modeShip 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.

OwnerAsleepOff
AO_DisplayDims the glass, takes the DSI link down, keeps the panel's memory and writes the time into it each minuteThe panel in sleep-in, the link down
AO_GuiDraws the face with Kody on the first frame of his scene and hands the digits of the time to AO_DisplayDraws a black page
AO_TouchThe controller's interrupt off and auto-sleep onThe same
AO_Imu, AO_Haptic, AO_Nfc, AO_Rfid, AO_LedMgrThe device at standby or offThe same
AO_AudioThe codec closed. The C5 lowers the amplifier's enableThe codec closed. The C5 is in reset, so the amplifier stays off
AO_StorageFiles closed, the card idleThe same. Deep sleep holds the card's supply off
AO_PowerThe ADC back to one-shot. The gauge stays readableThe same
AO_IoExpArms the expander's interrupt as the light-sleep wakeThe same line, GPIO6, is the wake source of deep sleep
AO_RadioStarts the sleep sentinel, so the C5 sleeps with the P4Stops the sentinel and leaves the link up. Deep sleep then holds the C5 in reset
AO_NetKeeps the sessionsCloses the Kode channel and the MQTT session first
AO_SchedulerTells AO_LowPower when its earliest entry falls dueNothing to rest
AO_Console, AO_Boot, AO_AppMgr, AO_AutotestNothing to restNothing 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

FromWakeWhat happens
AsleepBOT pressedAwake, no boot
AsleepA message from the Kode channel, or from an MQTT client of yoursAwake, no boot
AsleepA console line on BLE or on the cableAwake, no boot
AsleepA cable, seen at the minuteAwake within 60 s. The charger is read once a minute
AsleepAn owner's time falls dueA radio wake for the Kode channel, which does not light the screen. A whole awake for AO_Scheduler
OffBOT held for about a secondA boot: kodeOS starts again and lands on the face
Ship modeBOT held on the QON pin, or USB powerA 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:

    1. Publish POWER_SHIP_SIG, a warning with no payload that every subscriber receives before the write.
    2. Save the wall clock to NVS (kdrv_clocks_wall_keep(true)), so the time survives the loss of supply. It comes back marked approximate.
    3. Log two notices: ship mode: opening the BATFET and wake with BUTTON_BOT (QON) or by plugging USB.
    4. Call kdrv_bq25896_ship_mode(false).
    5. If the call returns KODE_OK, a cable is feeding the board: log battery path open; VBUS is keeping the board up - it powers off when USB is unplugged. Any other result logs ship 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

ThingValue
Console verbssleep (asleep), power_off (off), ship_mode (ship mode), lowpower_info (state and counters). The grammar is in internals/command-set
Facade callskode_power_sleep() and kode_power_off(), both void. They return at once, and the answer is LOWPOWER_STATE_SIG or LOWPOWER_REFUSED_SIG
Stored settingssleep_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 restLOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIG, LOWPOWER_REFUSED_SIG (sdk/events)
Ship mode eventsPOWER_SHIP_REQ_SIG is posted to AO_Power only. POWER_SHIP_SIG is published before the write
Register writeOne 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 resultKODE_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 current12 µA from the cell (sdk/kdrv/bq25896)
Ship mode wakeBOT held 0.9 to 1.3 s on the QON pin, or USB power
Off wakeBOT held about a second. The expander's interrupt, GPIO6, is the one wake source of deep sleep (ext1, low)
pmic field="batfet_dis" value=1Refused with KODE_DENIED: it would reach ship mode without saying why. AO_Power points to ship_mode
Build optionsCONFIG_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