SDK

AO_LowPower

The Active Object that decides whether a Kode Dot is awake, asleep or off: the round every owner answers, the wakes, the radio wake and the refusals.

What it is

A Kode Dot is in one of three states: AWAKE, ASLEEP or OFF. AO_LowPower decides which. It owns no device and claims no resource. It asks every owner of a device to put that device at rest, and it changes the state only when all of them have said they are ready. Then it stops the QP tick and lets go of the lock that keeps the P4 awake. From there the P4 light-sleeps by itself whenever every task is idle.

StateThe screenThe devicesThe P4The C5The radios
AWAKEAs the interface draws itEach as its owner runs itRunningRunningAs configured
ASLEEPDimmed, showing the timeEach at restLight sleep whenever every task is idleLight sleep, with the P4Kept: BLE advertises or stays connected, Wi-Fi stays associated, the Kode channel keeps its session
OFFOffEach at restDeep sleepHeld in resetOff

Ship mode is not a state. The BQ25896 switch that cuts the battery has one door, the console verb ship_mode, and it belongs to AO_Power (sdk/kaos/power). internals/power-and-sleep puts the four levels side by side.

AO_LowPower exists only with CONFIG_KODE_LOWPOWER. The option is off in the SDK and on in kodeOS. CONFIG_KODE_LOWPOWER_IDLE (on by default with it) makes AO_LowPower count the idle time and offer sleep_after. kodeOS turns it off, because its own Rest decides when the board rests: there AO_LowPower counts no idle time, config has no sleep_after row, and ASLEEP comes from a request or the interface. With it off, kode_power_sleep() and kode_power_off() still exist, log low power not built and do nothing, so an app's source compiles either way.

How it is connected

stateDiagram-v2
[*] --> AWAKE : khal_power_init, the lock is held
AWAKE --> Resting : idle time ran out, or a request
Resting --> AWAKE : one owner busy or silent, or a person at the board (never an order for OFF)
Resting --> ASLEEP : every owner ready, target ASLEEP
Resting --> OFF : every owner ready, target OFF, or an order for OFF out of budget
ASLEEP --> AWAKE : BOT, MQTT, BLE line, cable line, VBUS, request, a plain owner's time
ASLEEP --> RadioWake : a radio owner's time
RadioWake --> ASLEEP : the radio owners are done and rested
RadioWake --> AWAKE : real use of the board, or a radio owner busy
ASLEEP --> Resting : off_after reached, or a request for OFF
RadioWake --> Resting : off_after reached, or a request for OFF
OFF --> [*] : after 100 ms, deep sleep
The states of AO_LowPower

Resting is the time of a round. The board is still AWAKE while the round runs. The states in the code are LowPower_awake, LowPower_resting, LowPower_asleep and LowPower_off, under LowPower_on, which takes the answers and timers that arrive late. LowPower_radio is a substate of LowPower_asleep.

Priority and start

AO_LowPower runs at priority 24 (KODE_PRIO_LOWPOWER), the highest of the Active Objects. A wake has to be decided before the owners it wakes run, and a sleep has to be able to ask them all to rest. Its steps are short: a round is a publish and a count, and the sleep itself happens in the idle task. The queue holds 48 events (KODE_LOWPOWER_QUEUE_LEN) and the stack is the small tier, 3072 bytes.

kode_init() calls kode_boot_gate() before any Active Object exists, then starts AO_Log, AO_Boot and AO_LowPower in that order. It starts third so that every owner started later can join in its own initial transition. The initial transition runs khal_power_init(), which configures power management and takes the lock. The lock starts held, which is AWAKE.

The Active Object subscribes to IOEXP_INPUT_SIG, BUTTON_EVT_SIG, TOUCH_EVT_SIG, POWER_EVT_SIG, MQTT_MSG_SIG and CONFIG_REQ_SIG.

The transitions

FromToWhat causes it
AWAKEASLEEPThe idle time with no input (sleep_after), or a request: the console verb sleep, or kode_power_sleep() from an app
AWAKEOFFA request: the console verb power_off, kode_power_off(), or the answer of the Turn off card
ASLEEPOFFThe time spent asleep (off_after), or a request for OFF
ASLEEPAWAKEBOT pressed; a message from the Kode channel; a console line on BLE or on the cable; a cable seen at the minute; a request for AWAKE; the work of an owner that is not a radio owner, when it falls due
ASLEEPradio wakeThe work of a radio owner falls due: the renewal of the Kode channel's session, its retry, or the mqtt check. The glass stays asleep and the board returns to ASLEEP when the work is done
OFFAWAKEBOT held for about a second. The wake is a deep-sleep wake, which is a boot: kodeOS starts again and lands on the face

Three more facts hold for every row:

A round of readiness

The board changes state only through a round:

    1. AO_LowPower numbers the round, publishes LOWPOWER_ROUND_SIG with the target state and arms a one-shot of KODE_LOWPOWER_ROUND_MS (2000 ms).
    2. Each owner that joined puts its device at rest and answers once with lowpower_ready(): ready, or busy with a reason of at most 23 characters. The answer carries the number of the round. AO_LowPower drops and logs at debug level an answer for another round, one from an id that never joined, and a second one from the same owner.
    3. When every answer is ready, the state changes and AO_LowPower publishes LOWPOWER_STATE_SIG.
    4. One busy answer, or one owner that does not answer in 2000 ms, abandons the round. The board stays AWAKE, the log names the owner and the reason, and the refusal is published as LOWPOWER_REFUSED_SIG.

There is no round back to AWAKE: nobody may refuse a wake, so asking would only delay it. An abandoned round publishes LOWPOWER_STATE_SIG with state AWAKE and prev AWAKE (ASLEEP for an OFF round that began from ASLEEP). This is how an owner that already rested learns to restore. If no event is left to carry a state AWAKE, AO_LowPower publishes it again every KODE_LOWPOWER_STATE_RETRY_MS (20 ms) until one goes out.

A person at the board wins over a sleep nobody asked for:

A request that meets a busy owner

An owner is busy for a moment more often than not: the radio still stopping a job, the Kode channel in the middle of its join. So a round that a request for ASLEEP began is not given up at the first busy answer or the first silence. KODE_LOWPOWER_REQUEST_RETRY_MS (1000 ms) later the same request begins a new round, until KODE_LOWPOWER_REQUEST_RETRY_WINDOW_MS (10000 ms), counted from the request, has run out. The board is AWAKE in the wait. The requester is answered once: by the new state when a round goes through, or by LOWPOWER_REFUSED_SIG with the reason of the last round when the window is over.

Only the two answers of an owner, busy and none, are asked again. Everything else ends the request at once: a cable (the refusal is AO_LowPower's own and is made before any round), BOT, a request for AWAKE, a new request that takes the place of the waiting one, and a request that cannot be taken (no such state, power management did not start, a round is running). A round that the idle time began, or a radio wake's, has no requester and is never asked again.

A request for OFF is an order

Off is what the person asked for: the Turn off card, the console's power_off, an app's kode_power_off(). No owner can refuse it, so its round is never abandoned and never begun again from AWAKE.

A round for OFF that AO_LowPower began itself, when off_after was reached, has no requester and keeps the rules of the idle time.

The order on the way down

For ASLEEP, AO_LowPower publishes the state and logs at NOTICE what it enters and why. Only then does it stop the QP tick and let go of the lock. The P4 sleeps only when every task is idle, so AO_Log, which runs below, writes those lines first.

OFF has no such margin, because deep sleep is immediate. After LOWPOWER_STATE_SIG with state OFF, AO_LowPower waits KODE_LOWPOWER_OFF_GRACE_MS (100 ms) for the log and the owners to finish, and then khal_power_off_enter() runs.

The Active Object supervisor (kqp_watch, sdk/kqp) cannot read a sleep as a hang. The tick stops only after every owner answered ready, and an owner answers ready when the work in hand is finished. On the way up the tick restarts first and the lock follows, both before the state AWAKE is published, so an owner that restores its device finds timers that run.

Who takes part

An owner joins with lowpower_join() in its own initial transition. QP runs that transition in the thread that starts the Active Object, so the join needs no lock. The table holds KODE_LOWPOWER_PARTICIPANTS (24) places. A join after the first round is refused and logged, and that owner is never asked.

OwnerJoins asIn ASLEEPAnswers busy while
AO_DisplaydisplayDims the glass, takes the DSI link down, keeps the panel's memory, writes the time each minuteA screenshot is lent or the mirror runs (glass lent, mirror running); the renderer handed no glyphs (renderer not parked); the link or the panel would not rest (display link, display panel, glass not restored)
AO_TouchtouchThe CST820 interrupt off and auto-sleep on, one second after the last touchThe touch test runs (touch test)
AO_ImuimuThe IMU and the magnetometer at standbyA window, a row or a read waits (imu busy)
AO_HaptichapticThe AW86233 at standbyA buzz or the row runs (haptic busy)
AO_AudioaudioThe codec is closed. The amplifier is the C5's pinA sound, a recording, a stream or the row (audio playing, audio recording, audio test, audio busy)
AO_NfcnfcThe field down, the ST25R3916B asleepA read, an emulation, a sweep, a dump, the row or a held carrier (nfc busy)
AO_RfidrfidThe receiver's pull releasedA request or a job is with the worker (rfid busy)
AO_LedMgrledThe LED off, the heartbeat stoppedThe led row runs (led test)
AO_StoragestorageFiles closed, the card idleA write or another transfer is open (file write, storage busy)
AO_PowerpowerThe PMIC's ADC back to one-shot. The gauge stays readableA flash write, the plug row or a ship countdown (flash write, power plug test, ship countdown)
AO_IoExpioexpArms the expander's INT as the light-sleep wakeA failed read waits for its retry (expander retry) or the wake line would not arm (no wake line)
AO_RadioradioStarts the sleep sentinel: the C5 sleeps with the P4A job holds the channel, a C5 update, a park for a jump, the production row, a link that starts or stops, or the sentinel refused
AO_NetnetKeeps the sessions and starts nothing newA job, a transfer, a screen upload, a leave for a jump, or the dwell of the mqtt check (check dwell)
AO_Gui (kgui)guiStops every timer and draws the face for the glass to holdA flash write is in flight (write in flight) or the screen is not up yet (starting)
AO_ConsoleconsoleNothing to restA payload crosses the wire (console transfer)
AO_SchedulerschedulerGives the time of its earliest entry with lowpower_wake_at()A line waits for the console, or an event of code is within its bound (sched work); no event was left for the time (sched wake)
AO_Boot, AO_AppMgr, AO_Autotestboot, appmgr, autotestNothing to restA restart is on its way (home pending, restart pending), the factory reset runs (factory reset), an app is being written (app write), or a self-test run is in flight (test row)

AO_Ir has no device of its own and does not join, since the C5 serves it. AO_Buttons does not join either: it listens to the events of AO_IoExp. Each owner's part is on its own page.

The contract of a participant

An owner that joins keeps four rules, and a fifth when it is a radio owner:

  1. It rests on the round and answers once. The owner subscribes to LOWPOWER_ROUND_SIG and LOWPOWER_STATE_SIG itself, puts its device at rest or refuses, and calls lowpower_ready() exactly once.
  2. It restores on state AWAKE. An owner whose device wakes for whoever asks next has nothing to restore and listens to the rounds only.
  3. The restore is idempotent. A state AWAKE can arrive twice, because an abandoned round publishes it with prev AWAKE.
  4. It arms no QTimeEvt in response to state ASLEEP. The tick is stopped by then and the timer would never fire. Work with a deadline that can fall due asleep is measured in real time (khal_time_us()) and handed to AO_LowPower with lowpower_wake_at() before the owner answers a round for ASLEEP.
  5. A radio owner restores on state RADIO and ends its work with lowpower_radio_done(). Every other owner tests for state AWAKE and never sees the radio wake.

An Active Object of your own follows the same rules. kode_ao.h brings the registration calls and the signals.

ASLEEP: the tick, the lock and the minute

ASLEEP has no QP tick, because a tick every millisecond would keep the P4 out of light sleep. kqp_tick_rest(true) stops it, and the supervisor does not judge while it is stopped. So AO_LowPower counts the time asleep on khal_timer_once(), a one-shot timer whose callback posts LOWPOWER_MINUTE_SIG. At most KHAL_TIMER_ONCE_SLOTS (4) can be pending, and a one-shot cannot be cancelled, so the Active Object keeps the due time and the number of timers outstanding and obeys only a due time that has come.

One timer serves every count. It is armed for the next minute of the wall clock, the off_after deadline or an owner's time, whichever comes first. When the clock is not set, the minute is a whole 60 s. At each minute AO_LowPower does three things:

  1. It checks off_after. When the time asleep has reached it, the minute becomes a round for OFF. The count starts at the ASLEEP that follows a wake somebody used, and a wake for an owner's time that nobody used does not start it again.
  2. It asks AO_Power for a quiet reading with power_peek_request(). The charger's interrupt reaches no pin the P4 can sleep on, so this is how a cable that appeared is found, within 60 s. A cable wakes the board. Without a cable, the reading is the minute's sample of the summary of the last rest.
  3. It writes the previous minute's sample into the store, then posts DISPLAY_MINUTE_REQ_SIG to AO_Display, which writes the time on the glass. The store comes first so the two writes never overlap.

The lock is one esp_pm lock of type "no light sleep", taken at boot. AWAKE is "the lock is held". There is no forced esp_light_sleep_start(): power management sleeps by itself once the lock is let go and nothing else holds one. The P4 runs at a fixed 400 MHz, so light sleep is the saving and scaling is not used. The P4 also wakes briefly for its own housekeeping, such as the MQTT keepalive or a TCP acknowledgement. These wakes are not transitions: the state stays ASLEEP and the screen does not change.

ASLEEP: what wakes the board

Cause (the word in lowpower_info)What it is
botA fresh press of BOT, read from the expander's input event
mqttMQTT_MSG_SIG: a message from the Kode channel or from an MQTT client of yours
bleA console line that arrives on BLE
cableA console line on the serial port or the dock, or a POWER_EVT_SIG that says VBUS is present
timerAn owner's time: LOWPOWER_WAKE_AT_SIG named a time and it has come. A radio wake for a radio owner, a whole wake for AO_Scheduler
requestA request for AWAKE

Every console line from any source calls lowpower_input_note() before the busy check. For the cable, BLE and the Kode channel it starts the idle time over, and in ASLEEP it wakes the board: a refused line is a person at the other end as much as an accepted one. A line with the source SCHED, which AO_Scheduler runs on its own account, is not input.

ASLEEP: work that falls due

A QTimeEvt does not count while the tick is stopped. An owner whose work has a deadline in hours keeps it in real time, khal_time_us(), which runs through light sleep. The Kode channel does this for the renewal of its session and its back-off, and AO_Scheduler for the minute of its earliest entry.

Before an owner answers a round for ASLEEP, it calls lowpower_wake_at(who, due_us), which posts LOWPOWER_WAKE_AT_SIG. AO_LowPower keeps two times: the earliest a radio owner gave, and the earliest any other owner gave. They are kept apart because a radio wake asks the radio owners alone to say their times again. The time folds into the one timer of the minute. A time that has passed is due at once, with the timer armed for 1 µs.

When the timer comes and the time has come, the board wakes with the cause timer. off_after is judged first, so a time that falls due together with it goes to OFF. After that:

The times are forgotten when a round begins and when the board wakes, and an owner says its time again in every round. A time that no longer matters costs one wake that finds nothing to do.

The radio wake

A renewal of the Kode channel's session needs the network and not the glass, so the wake for it does not light the board. A radio wake is a phase of ASLEEP, and lowpower_info still says state=asleep.

StepWhat happens
BeginA radio owner's time comes. AO_LowPower forgets that time, arms the minute again, takes the tick and the lock, counts the wake in wakes_timer, publishes LOWPOWER_STATE_SIG with state KODE_LP_RADIO (cause timer, prev ASLEEP) and arms the bound KODE_LOWPOWER_RADIO_MAX_MS (60000 ms)
WorkThe radio owners restore on state RADIO. AO_Radio stops the sleep sentinel so the bus is awake, and AO_Net does what the channel has due. The display, the touch panel, the interface and every other owner test for AWAKE, stay at rest, and the minute keeps being written on the glass
DoneEach radio owner calls lowpower_radio_done() when its work is finished
RestWhen every radio owner is done, or when the bound runs out, AO_LowPower posts a round for ASLEEP to the radio owners alone and arms the round's one-shot. A published round would ask every owner to rest again, and the others never woke
BackWhen every answer is ready, AO_LowPower publishes LOWPOWER_STATE_SIG with state ASLEEP (cause timer, prev RADIO), stops the tick and lets go of the lock. No glass was lit

Entering and leaving the radio wake never enters or exits LowPower_asleep itself, because QP does not exit or enter a parent for a transition between it and its own child. So the time asleep, the off_after count, the summary of the rest and the one-shots of the minute run on through it.

A radio wake becomes a whole AWAKE when something real uses the board: BOT, a message from the broker, a console line on BLE or the cable, a cable, or a request for AWAKE. A radio owner that answers the closing round busy, or does not answer in 2000 ms, does the same, and the refusal is published. A radio wake that reaches off_after goes to OFF as the ASLEEP rest would. Another owner's time survives a radio wake in its own slot, so a radio time that is earlier never hides it.

The rest after the work of a timer wake

A whole wake for an owner's time should end when the owner's work does, and not after the idle time. The owner cannot judge that alone, because it does not know who else is at the board, so it calls lowpower_request_rest(src) when its work is done. AO_Scheduler is the caller, with src equal to KODE_LOWPOWER_SRC_SCHED (0xFD).

AO_LowPower keeps one flag, used. It is false after a wake whose cause is timer, and true after every other wake, the boot included. It turns true at the first real input since the wake: a button, a touch, a console line from the cable, BLE or the Kode channel, a fresh press of BOT, or a cable that appears.

OFF

OFF closes the sessions first. AO_Net closes the Kode channel and any MQTT session, then tells AO_Radio with radio_net_left(). AO_Radio waits for that, stops the sleep sentinel if it runs and answers. The C5 link and the BLE host stay up and nothing is taken down: deep sleep stops the P4 whole, khal_power_off_enter() holds the C5 in reset, and tearing the link down while the BLE host still runs would panic that host.

khal_power_off_enter() holds five pins at their rest levels through deep sleep: the card's supply (off), the NFC chip's select (high), the pull of the LF front end, the reset of the panel and touch, and the C5's enable (low), which is written last. It arms the expander's interrupt line, GPIO6, as the one wake source: ext1 on a low level.

The boot from OFF

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. kode_init() calls kode_boot_gate() right after kode_log_init(), before any Active Object starts:

The gate reads BOT's live level about one second after the press, because the boot takes that long to reach kode_init(). The expander keeps no latch, so a tap that is released before the gate reads sends the board back to OFF. To wake the board from OFF, hold BOT for about a second.

This gate is a documented exception to design rules 1 and 3 (sdk/design-rules). It calls kdrv_tca6408a_init() and kdrv_tca6408a_get() itself, although AO_IoExp owns the expander, and it reads the chip with no event. No Active Object runs yet at that point, and the pad button has to decide the boot before the first of them starts.

The screen in ASLEEP

The CO5300 keeps the picture in a memory of its own, the GRAM, and shows it with nobody driving it. So the DSI link can go while the screen stays lit. AO_Display takes the link down, and with it the video scan-out, both framebuffers and the rail of the DPHY. Each minute it brings a small link up, writes the rectangle of the time into the GRAM and takes the link down again (sdk/kdrv/co5300).

The SDK cannot draw, because lv_* lives only in kgui, kui and kscreen. So the pixels come from the owner of the screen. On the round, the renderer draws the page the glass holds while it sleeps and renders the ten digits and the colon once into a strip in PSRAM. It hands the strip to AO_Display with display_glyphs_hand(), in a DisplayGlyphsEvt that also carries the position of the time, the dimming level and whether the face shows a 12 h dial. AO_Display composes hh:mm from the strip in the local time of the face's zone and writes it. LVGL does not run. A renderer with nothing to hand over passes a NULL strip. The glass is then dimmed to KODE_LOWPOWER_DIM_PCT (20 %) and stays dark, and the minute writes nothing.

The strip is a pointer, the one exception to design rule 7 that this event makes. Eleven glyphs of a large font are far more than an event can carry. The strip stays the renderer's, valid from the hand-over until the board is AWAKE again, and AO_Display never frees or writes it.

The link going down frees both framebuffers, and the link coming up allocates a new pair at addresses that can differ. The contract with a renderer is:

  1. On the round, the renderer stops drawing and flushes its page. AO_Display lets flushes through while it waits for the glyphs and drops them afterwards.
  2. The glyphs are the renderer's word that it has stopped. AO_Display takes the link down only after DISPLAY_GLYPHS_SIG arrives. If none arrives in KODE_LOWPOWER_GLYPHS_WAIT_MS (1500 ms, under the 2000 ms of the round), it answers busy with renderer not parked and the round is abandoned safely.
  3. On AWAKE, AO_Display brings the link up and publishes DISPLAY_REPAINT_SIG on every exit from a rest it began, a busy refusal included. The renderer fetches both buffers again from kdrv_display_back_buffer() and kdrv_display_front_buffer() and paints the whole glass. kdrv_display_buffers_epoch() changes with every new pair.

The radios while the P4 sleeps

ASLEEP keeps the radios, so the P4 and the C5 sleep together, with work on both sides of the SDIO link.

On the P4, the sleep sentinel of sdk/kdrv/c5 keeps the esp-hosted link alive. AO_Radio starts it on the round with a job of its radio worker, and rule 9 holds: the sentinel runs the handshake and reports what happened, and decides no state. Going down, it marks the bus silent, arms the wake line KBSP_PIN_C5_WAKE_P4 (GPIO2), announces power save to the C5, waits 20 ms and suspends esp-hosted's three SDIO tasks, which would otherwise poll every 10 ms and wake the P4 a hundred times a second. If the C5 refuses, the line does not arm or the tasks never settle, the bus stays up and the round is answered busy with the reason.

Coming up starts on the C5's line or on a frame the P4 queued. A hook on esp_hosted_tx() catches every outgoing frame, and the sentinel knocks the C5 awake before the frame is sent. After a wake the P4 stays up for 100 ms for the traffic to land, and for another 3 s each time the BLE GAP count has moved. A held BLE connection does not keep the P4 awake: the controller in the C5 keeps the connection and the P4 sleeps between its events.

On the C5, the host-sleep code acts only while the P4 has announced power save:

IPv6 neighbour solicitations are dropped while the P4 sleeps, so the P4 is unreachable over IPv6 until it wakes.

The summary of the last rest

A rest on the battery cannot be watched on the cable, because the cable keeps the board awake and opening the serial port resets it. So AO_LowPower keeps one record of the last rest in the blob lp_last of the kset namespace, written only by AO_LowPower. It is a lowpower_last_t: 32 bytes of totals and a ring of KODE_LOWPOWER_LAST_SAMPLES (60) samples of 8 bytes, 512 bytes in all, with a _Static_assert on the size. A record of another version or size reads as none.

FieldWhat it holds
start_sThe wall clock when the rest began, in Unix seconds. 0 when the clock was not set
duration_sHow long the rest had lasted at the last write
slept_pct, sleepsThe share of the rest spent in light sleep, and the light sleeps in it
wakes_c5, wakes_tx, deafThe wakes of the P4 by the C5's line and by its own traffic for the C5, and the wakes the C5 never answered
end_causeThe cause that ended the rest, or KODE_LOWPOWER_LAST_OPEN while no end was written
samplesOne sample for each minute: seconds from the start, the cell's current in mA (into the cell positive) and its voltage in mV

The totals are those of the rest and not of the boot. The sample is the answer to the minute's peek of AO_Power. The P4 is awake for the minute anyway, so the gauge is read for a few milliseconds at about 75 mA, and the sample carries that bias. A reading with a cable in it is no sample, because the cable wakes the board.

The working copy is 512 bytes in PSRAM, taken when the rest begins and given back when it ends. Each sample is kept in it when it arrives and the whole record is written as one blob at the start of the next minute, before AO_Display is asked for the time. Erasing a page of the store stops the flash cache on both cores for milliseconds, and a GRAM write that runs then fails, so the order matters. One more write follows when the rest ends. The record a reset finds is at most one minute behind, and a rest that ends before its first minute leaves the earlier summary alone. A board that sleeps all day writes 1,440 times a day. If PSRAM has no room, the rest goes on without a summary and the log says so.

Specifications

Settings

AO_LowPower owns two stored settings of config, each a 4-byte blob of seconds in the kset namespace. A stored value that is missing, of the wrong size or not one of the listed words reads as the default. Each set logs <key> <word> (stored) at NOTICE.

KeyValuesDefaultMeaning
sleep_after30s, 1m, 2m, 5m, 10m, never2mThe idle time in AWAKE. A button, a touch or a console line starts it over. It does not depend on the auto-lock of the face or on Kody's sleep
off_after1h, 6h, 24h, 3d, 7d, never1hThe time spent in ASLEEP before OFF

A change of sleep_after starts the idle time over at once. never counts nothing. Without CONFIG_KODE_LOWPOWER_IDLE the row does not exist.

Refusals

A refused request answers the verb with >err code=4 msg="<who>: <why>". Code 4 is "busy". lowpower_last_refusal() keeps the last one and lowpower_info prints it as last_busy.

TextMeaning
cable: external powerVBUS is present. The board neither sleeps nor powers off
<owner>: <why>A participant answered busy, for example storage: file write
<owner>: no answerA participant did not answer in 2000 ms
request: a round is runningA second request while a round is open
request: request pendingA rest was asked while another request waits to begin again
request: cancelledA request for AWAKE ended the round
request: board not AWAKEA rest was asked while the board was not AWAKE
request: no such stateThe target is not one of the three states
request: power management did not startkhal_power_init() failed. The board stays AWAKE
BOT: pressedThe thumb of a person at the board ended a round that was requested
input: board usedAn input ended a round that the idle time began, or the board was used since its last wake

With the option off, sleep and power_off answer >err code=4 msg=not_built and lowpower_info does not exist.

Console verbs and calls

The syntax of the verbs is in internals/command-set.

Verb or callWhat it does
sleepTakes the board to ASLEEP. >ok when the state changed
power_offTakes the board to OFF
ship_modeOpens the battery switch. It is AO_Power's, the only door to ship mode
lowpower_infoTwo records, >|lowpower (state, seconds in it, cause, the two times, wakes by bot, ble, mqtt and timer, last_busy) and >|lowpower_stats (light-sleep share, sleeps, the C5's wakes and its frames, deaf), then >ok
lowpower_info last=onThe summary of the last rest: a >|lowpower_last header and one >|lp_sample per minute, oldest first, in slices of KAOS_WIRE_SLICE (12). end=asleep means no end was written because a reset ended the rest. A board with no summary prints >|lowpower_last present=no
kode_power_sleep(), kode_power_off()The facade calls. Both return at once. The answer is LOWPOWER_STATE_SIG, or LOWPOWER_REFUSED_SIG when the board will not (sdk/kode)
lowpower_request(target, src)Asks for a state. src is the console source, or KODE_LOWPOWER_SRC_APP (0xFE) for an app
lowpower_request_rest(src)Asks for ASLEEP once an owner's work is done. Returns false when no event was left to post it
lowpower_input_note(src)Says a console line came in. Safe from any task
lowpower_join(ao, name), lowpower_join_radio(ao, name)Join the rounds. They return KODE_LOWPOWER_WHO_NONE when the table is full
lowpower_ready(who, round, busy, why)Answers a round
lowpower_wake_at(who, due_us)Gives the real time at which the owner's work falls due. False when no event was left
lowpower_radio_done(who)A radio owner's work in a radio wake is finished. Dropped outside a radio wake
lowpower_last_read(out), lowpower_last_sample(l, i)Read the summary and walk its samples. Safe from any task

The cause word in lowpower_info is request, idle, off_after, bot, ble, mqtt, cable, timer or boot. The wakes_ fields count wakes from ASLEEP by the cause that began them. A radio wake that BOT then turns into a whole AWAKE counts in wakes_timer and in wakes_bot.

Signals

AO_LowPower publishes LOWPOWER_ROUND_SIG (LowPowerRoundEvt: target, round), LOWPOWER_STATE_SIG (LowPowerStateEvt: state, cause, prev) and LOWPOWER_REFUSED_SIG (LowPowerRefusedEvt: the source and the text). It posts DISPLAY_MINUTE_REQ_SIG to AO_Display and POWER_PEEK_REQ_SIG to AO_Power. It takes the posted signals LOWPOWER_REQ_SIG, LOWPOWER_READY_SIG, LOWPOWER_REST_SIG, LOWPOWER_INPUT_SIG, LOWPOWER_WAKE_AT_SIG, LOWPOWER_RADIO_DONE_SIG and LOWPOWER_MINUTE_SIG, and five private one-shots. sdk/events lists each one.

Timers

TimerWhat it times
LOWPOWER_ROUND_TMO_SIGA round, KODE_LOWPOWER_ROUND_MS (2000 ms). After state OFF, the grace before deep sleep, KODE_LOWPOWER_OFF_GRACE_MS (100 ms)
LOWPOWER_IDLE_TMO_SIGThe idle time in AWAKE, the value of sleep_after
LOWPOWER_RETRY_TMO_SIGThe wait before a requested round that met a busy or silent owner begins again, 1000 ms
LOWPOWER_RADIO_TMO_SIGThe bound of a radio wake, 60000 ms
LOWPOWER_STATE_RETRY_SIGThe wait before a state change that found no event is published again, 20 ms
khal_timer_once()The minute, the off_after deadline and an owner's time in ASLEEP, where the tick is stopped

Gotchas

Example code

An app that asks for ASLEEP ten seconds after it starts. On battery the log shows entering ASLEEP (request) and the Kode Dot sleeps until BOT, a message or a cable wakes it. With a cable in, the log shows request for ASLEEP (source 254) refused: cable: external power and the app keeps running.

/*
 * sleep_later.c - 2026-11-03 - from Kode with love
 *
 * Starts the system, waits ten seconds and asks AO_LowPower for ASLEEP. On
 * battery the log shows "entering ASLEEP (request)", the screen dims to the
 * time and the Kode Dot sleeps until BOT, a Kode channel message or a cable
 * wakes it. With USB plugged in, the log shows that the request was refused
 * because of the cable, and the app goes on running.
 */
#include "kode.h"
 
#define TAG              "sleep_later"
#define BEFORE_SLEEP_MS  10000U
 
/* Wait, then ask for the rest. The call returns at once whatever the answer. */
void app_main(void)
{
    kode_init();
    KLOG_I(TAG, "asking for ASLEEP in %u ms", (unsigned)BEFORE_SLEEP_MS);
    kode_delay_ms(BEFORE_SLEEP_MS);
 
    kode_power_sleep();
 
    for (;;) {
        kode_delay_ms(1000U);
    }
}

Not compiled against the SDK. TBD: a build and a run on a Kode Dot on battery.

In the SDK