SDK
AO_LedMgr
The Active Object that owns the RGB LED of a Kode Dot: the five signals that drive it, the heartbeat timer and the production row an operator judges.
What it is
AO_LedMgr is the only code that drives the RGB LED of a Kode Dot. Nothing calls it. A part that wants a colour publishes an event, and AO_LedMgr handles one event from start to end before it takes the next, so two parts never use the LED at once and no lock exists.
It does three things. It sets the LED on, off, toggled or to an RGB colour. It runs a heartbeat, a timer that toggles the LED at a chosen period. And it runs the LED row of the production test, where the person at the bench says whether the colours are right.
The LED chip, a KTD2026, and its channels are on sdk/kdrv/ktd2026.
How it is connected
flowchart LR subgraph board["On the Kode Dot"] ao["AO_LedMgr"] -- "kdrv_led_*()" --> drv["kdrv_led"] --> led["KTD2026<br/>I2C 0x30"] end app["Your app<br/>kode_led_rgb()"] -- "LED_RGB_SIG" --> ao console["AO_Console<br/>led_* verbs"] -- "LED_SET_SIG,<br/>LED_RGB_SIG..." --> ao power["AO_Power<br/>write pulse"] -- "LED_RGB_SIG" --> ao class ao ao class console,power ao class app app class led chip
AO_LedMgr runs at priority 7 (KODE_PRIO_LED) with a 16-event queue and a 3072-byte stack. It claims KBSP_RES_LED, the KTD2026 at I2C address 0x30, calls kdrv_led_init() so the LED starts off, and subscribes to five signals in its initial transition. kode_init() starts it right after AO_Net and before AO_Ir. Nothing waits for it.
The command signals are published, never posted, so a publisher needs no reference to the AO. kode_led_set(), kode_led_toggle() and kode_led_rgb() in kode.h publish them, and the console verbs publish the same events.
Publishers
AO_Power is the one permanent publisher besides the facade. While a flash write reports progress it publishes LED_RGB_SIG to pulse the LED blue, 500 ms lit and 500 ms dark (KODE_LED_PULSE_MS), whether the screen is on or off. It does this only while the led_activity setting is on.
AO_LedMgr does not arbitrate between publishers. kgui never drives the LED.
States
stateDiagram-v2 [*] --> LedMgr_active: claim, kdrv_led_init(), subscribe LedMgr_active --> LedMgr_active: set, toggle, rgb, blink period, heartbeat LedMgr_active --> LedMgr_testing: LED_TEST_REQ_SIG LedMgr_testing --> LedMgr_active: verdict published
LedMgr_testing is a substate of LedMgr_active: whatever it does not handle goes to the parent, so the parent's work for other parts keeps running during the row. The row takes away exactly one thing, the LED.
The heartbeat
One periodic QTimeEvt sends the private LED_HEARTBEAT_SIG, and each beat calls kdrv_led_toggle(). The timer is disarmed at boot and stays so until LED_HEARTBEAT_ENABLE_SIG enables it. The enable path disarms before it arms, since arming an armed QTimeEvt corrupts the time-event list.
Disabling disarms and forces the LED off. While the heartbeat runs, it decides whether the LED is lit, so a colour set meanwhile is toggled on the next beat.
The period
LED_BLINK_PERIOD_SIG carries milliseconds. Below KODE_LED_BLINK_MIN_MS (10 ms) AO_LedMgr logs blink period %ums too short; ignored and keeps the old period, because each half of a blink is an I2C write. A valid period re-arms a running heartbeat at once.
The production row
test component=led lights red, green and blue in turn (LED_COLOUR_RED, _GREEN, _BLUE) at KODE_LED_TEST_LEVEL, 96 of 255. It lights the colour before it publishes the question, so the reader already sees it. The top button confirms, the bottom button refuses, and the four pad directions are ignored.
A colour nobody answers in 10 s (KODE_LED_TEST_STEP_MS) is a timeout. The row ends at the first colour not confirmed. LedMgr_testing subscribes to BUTTON_EVT_SIG on entry and unsubscribes on exit.
While the row runs, requests from other parts are handled in one of two ways:
| Signal | During the row |
|---|---|
LED_SET_SIG, LED_TOGGLE_SIG, LED_RGB_SIG, the heartbeat tick | Dropped, because by the end of the row they would be seconds old |
LED_HEARTBEAT_ENABLE_SIG, LED_BLINK_PERIOD_SIG | Recorded and applied at the exit, because they describe what happens from now on |
Every way out goes through Q_EXIT_SIG: the pad is released, the LED goes dark, and the heartbeat is re-armed from what was last asked. A second LED_TEST_REQ_SIG during a run is ignored with a warning.
Resting with the board
With CONFIG_KODE_LOWPOWER, AO_LedMgr joins the rounds of AO_LowPower as led (sdk/kaos/lowpower). On a round it stops the heartbeat, switches the LED off and answers ready. While the production row runs it answers busy with led test, because the operator is looking at the LED and the pad is the answer.
On LOWPOWER_STATE_SIG with state AWAKE it arms the heartbeat again if one was asked for. It does not restore a colour: AO_LedMgr does not know which colour an app had set, so the LED stays dark until a new colour or the heartbeat lights it. A beat queued before the rest does not light the LED after it.
Specifications
Signals
| Signal | Event | From | What AO_LedMgr does |
|---|---|---|---|
LED_SET_SIG | LedEvt: on | facade, AO_Console | kdrv_led_set(on) |
LED_TOGGLE_SIG | LedEvt, payload unused | facade, AO_Console | kdrv_led_toggle() |
LED_RGB_SIG | LedRgbEvt: r, g, b | facade, AO_Console, AO_Power | kdrv_led_rgb(r, g, b). (0, 0, 0) is off |
LED_BLINK_PERIOD_SIG | LedBlinkEvt: period_ms | AO_Console | Stores the period; 10 ms minimum |
LED_HEARTBEAT_ENABLE_SIG | LedEvt: on | AO_Console | Arms or disarms the heartbeat |
LED_HEARTBEAT_SIG | none | self, periodic | Toggles the LED. Private, numbered from MAX_PUB_SIG |
LED_TEST_REQ_SIG | none | AO_Autotest | Enters the production row |
LED_TEST_WAIT_SIG | none | self, one-shot | The deadline of one colour |
LOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIG | LowPowerRoundEvt, LowPowerStateEvt | in, subscribed | With CONFIG_KODE_LOWPOWER: dark and still on a round (busy led test during the row); the heartbeat back on state AWAKE |
BUTTON_EVT_SIG | ButtonEvt | AO_Buttons | Only during the row: the answers |
LED_TEST_SIG | LedTestEvt: step, total, phase, colour | out, published | Phases LED_TEST_ASK, _PASS, _FAIL, _TIMEOUT, _DONE. A question and its outcome share step |
TEST_RESULT_SIG | named led | out, published | The verdict |
Numbers, calls and verbs
| Thing | Value |
|---|---|
| Heartbeat default | 500 ms (KODE_LED_BLINK_MS), off at boot |
| Heartbeat floor | 10 ms (KODE_LED_BLINK_MIN_MS) |
| Write pulse | 500 ms lit, 500 ms dark (KODE_LED_PULSE_MS), from AO_Power, while led_activity is on |
| Row | 3 colours, 10 s each, console budget 32 s (KODE_LED_TEST_BUDGET_MS) |
| Facade | kode_led_set(on), kode_led_toggle(), kode_led_rgb(r, g, b). Each returns KODE_OK or KODE_NO_RESOURCES when the event pool is dry; none asserts |
| Call | led_test_request(), defined in every build |
| Console | led_on, led_off, led_toggle, led_blink, led_hb, led_rgb, test component=led |
| Stored settings | None. The led_activity key belongs to AO_Power |
| Kconfig | None |
Gotchas
Example code
This cycles the LED through red, green and blue, one colour a second, at a low level because the LED is bright. You see the colours change once a second.
/*
* led_cycle.c - 2026-10-28 - from Kode with love
*
* Asks AO_LedMgr, the owner of the Kode Dot's LED, for red, green and blue in
* turn, one second each, at a low level. You see the LED change colour once a
* second for as long as the app runs.
*/
#include "kode.h"
#define LEVEL 32
/* Bring the system up, then step through the three colours forever. */
void app_main(void)
{
kode_init();
while (1) {
kode_led_rgb(LEVEL, 0, 0);
kode_delay_ms(1000);
kode_led_rgb(0, LEVEL, 0);
kode_delay_ms(1000);
kode_led_rgb(0, 0, LEVEL);
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/kaos/led:led_mgr.c, the state machine, the heartbeat and the production row.kode.h:kode_led_set(),kode_led_toggle()andkode_led_rgb().components/kdrv/led: the KTD2026 driver; seesdk/kdrv/ktd2026.