# 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

```mermaid
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

| 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:

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

| 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

> **WARNING**
>
> **You asked for sleep or off and the Kode Dot kept running.** A USB cable is in, and the answer was `cable: external power`. Unplug the cable. The same cable makes ship mode open the battery path and leave the board running: the log says `battery path open; VBUS is keeping the board up`, and the board goes dark when the cable is pulled.

> **WARNING**
>
> **A tap of BOT does not wake the Kode Dot from off.** The boot gate reads the button about a second after the press, and the expander keeps no latch. Hold BOT for about a second.

> **NOTE**
>
> **An app that calls `kode_power_sleep()` finds the app still running.** The call returns at once and the Kode Dot may refuse: with a cable in, or while an owner is busy. `kode_power_off()` is refused only by the cable. Subscribe to `LOWPOWER_STATE_SIG` and `LOWPOWER_REFUSED_SIG` with an Active Object of your own to know. Without one, the log says who refused and why.

> **NOTE**
>
> **The real switch-off is only verified with a person, a battery and no cable.** On USB you can check the event path, the I2C write and the `KODE_OK` log line, and the refusals of sleep and off. Nothing logs the real switch-off, because the console dies with the board, and asleep cannot be watched on the cable. **TBD**: a run of ship mode, off and asleep on a Kode Dot with this page's example.

## 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.

```c
/*
 * 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`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/lowpower): AO_LowPower, the states and the rounds.
- [`components/kaos/power/src/power_ao.c`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/power/src/power_ao.c): AO_Power, the handler of `POWER_SHIP_REQ_SIG` and the battery poll.
- [`components/kdrv/power/src/kdrv_bq25896.c`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/power/src/kdrv_bq25896.c): `kdrv_bq25896_ship_mode()`.
- [`components/khal/power`](https://github.com/kodediy/kode-sdk/tree/main/components/khal/power): the awake lock, the wake lines and deep sleep.
- [`components/kgui/kgui_ao.c`](https://github.com/kodediy/KodeOS/blob/main/components/kgui/kgui_ao.c): the Turn off card and `gui_power_off()`.
