# 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

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

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

| 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

> **WARNING**
>
> **An app that set a colour sees it vanish during `test component=led`.** The row drops every colour request while it runs and turns the LED off at its exit. The driver then remembers the last colour the row lit, so a heartbeat that resumes blinks blue after a full pass. It stays blue until somebody asks for another colour.

> **NOTE**
>
> **A colour set while the heartbeat runs is toggled away.** The heartbeat owns the on and off of the LED. Disable it first (`led_hb`), which also turns the LED off, then set the colour.

> **NOTE**
>
> **A blink period under 10 ms is refused.** The log says `blink period %ums too short; ignored` and the old period stays. Arming a zero-tick `QTimeEvt` would be a QP assert and a reset, so any code that arms a timer from a run-time value checks it first.

> **NOTE**
>
> **A flash write overwrites your colour with a blue pulse.** AO_Power publishes `LED_RGB_SIG` for as long as the write reports progress. `config key="led_activity" value=off` stops it.

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

```c
/*
 * 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`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/led): `led_mgr.c`, the state machine, the heartbeat and the production row.
- [`kode.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kode/include/kode.h): `kode_led_set()`, `kode_led_toggle()` and `kode_led_rgb()`.
- [`components/kdrv/led`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/led): the KTD2026 driver; see `sdk/kdrv/ktd2026`.
