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
Who drives the LED

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
AO_LedMgr states

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:

SignalDuring the row
LED_SET_SIG, LED_TOGGLE_SIG, LED_RGB_SIG, the heartbeat tickDropped, because by the end of the row they would be seconds old
LED_HEARTBEAT_ENABLE_SIG, LED_BLINK_PERIOD_SIGRecorded 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

SignalEventFromWhat AO_LedMgr does
LED_SET_SIGLedEvt: onfacade, AO_Consolekdrv_led_set(on)
LED_TOGGLE_SIGLedEvt, payload unusedfacade, AO_Consolekdrv_led_toggle()
LED_RGB_SIGLedRgbEvt: r, g, bfacade, AO_Console, AO_Powerkdrv_led_rgb(r, g, b). (0, 0, 0) is off
LED_BLINK_PERIOD_SIGLedBlinkEvt: period_msAO_ConsoleStores the period; 10 ms minimum
LED_HEARTBEAT_ENABLE_SIGLedEvt: onAO_ConsoleArms or disarms the heartbeat
LED_HEARTBEAT_SIGnoneself, periodicToggles the LED. Private, numbered from MAX_PUB_SIG
LED_TEST_REQ_SIGnoneAO_AutotestEnters the production row
LED_TEST_WAIT_SIGnoneself, one-shotThe deadline of one colour
LOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIGLowPowerRoundEvt, LowPowerStateEvtin, subscribedWith CONFIG_KODE_LOWPOWER: dark and still on a round (busy led test during the row); the heartbeat back on state AWAKE
BUTTON_EVT_SIGButtonEvtAO_ButtonsOnly during the row: the answers
LED_TEST_SIGLedTestEvt: step, total, phase, colourout, publishedPhases LED_TEST_ASK, _PASS, _FAIL, _TIMEOUT, _DONE. A question and its outcome share step
TEST_RESULT_SIGnamed ledout, publishedThe verdict

Numbers, calls and verbs

ThingValue
Heartbeat default500 ms (KODE_LED_BLINK_MS), off at boot
Heartbeat floor10 ms (KODE_LED_BLINK_MIN_MS)
Write pulse500 ms lit, 500 ms dark (KODE_LED_PULSE_MS), from AO_Power, while led_activity is on
Row3 colours, 10 s each, console budget 32 s (KODE_LED_TEST_BUDGET_MS)
Facadekode_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
Callled_test_request(), defined in every build
Consoleled_on, led_off, led_toggle, led_blink, led_hb, led_rgb, test component=led
Stored settingsNone. The led_activity key belongs to AO_Power
KconfigNone

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