# AO_Display

Owns the Kode Dot's AMOLED panel: powers it, takes frames from a renderer, sets brightness and lends screenshots and JPEGs.

## What it is

**AO_Display** is the only code that sends commands and pixels to the screen of a Kode Dot. Everything else asks it with an event. It has two jobs. The control path switches the panel on, sets the brightness and tells the rest of the system whether the panel started. The pixel path takes rectangles of a finished picture from a renderer and flushes them to the glass.

A renderer draws into memory and sends one rectangle at a time with `DISPLAY_FLUSH_SIG`. In kodeOS the renderer is AO_Gui. In your own app it is the render pass of `sdk/kscreen`, or code of yours. AO_Display never draws anything itself.

It also keeps a shadow copy of what the panel shows. The shadow feeds the console's `screenshot` and the live mirror (`internals/mirror`). A JPEG encoder on the same copy serves the screen upload of `sdk/kaos/net`. The chip itself, its bring-up and the flip that makes a frame tear-free are on `sdk/kdrv/co5300`.

## How it is connected

```mermaid
flowchart LR
  gui["AO_Gui or kscreen"] -- "DISPLAY_FLUSH_SIG" --> ao["AO_Display"]
  ao -- "kdrv_display_flush()" --> drv["kdrv_display"]
  ioexp["AO_IoExp"] -- "IOEXP_FIELD_RSP_SIG" --> ao
  ao -- "IOEXP_FIELD_REQ_SIG<br/>for P7" --> ioexp
  ao -- "DISPLAY_STATE_SIG" --> subs["AO_Gui, AO_Touch,<br/>kscreen"]
  console["AO_Console, AO_Net"] -- "capture, shot, mirror" --> ao
  drv -. "DMA done: semaphore" .-> gui
  class ao,ioexp,gui,console ao
```

*AO_Display and its neighbours*

AO_Display in numbers:

| Thing | Value |
| --- | --- |
| Priority | 15 (`KODE_PRIO_DISPLAY`) |
| Queue | 16 events |
| Stack | 6144 bytes |
| Claims | `KBSP_RES_PANEL`; with `KODEOS_MIRROR` also `KBSP_RES_JPEG` and `KBSP_RES_PPA`, the P4's JPEG codec and its pixel scaler |
| Subscribes to | `CONFIG_REQ_SIG`, in its initial transition |

It sits above AO_Gui, AO_Touch and AO_Buttons on purpose: AO_Gui draws a frame and then waits for AO_Display to send it, so a display that could only run after the renderer finished would deadlock against it.

`kode_init()` starts it last of all, because bringing the panel up costs about 245 ms of mandated waits. It does them in its own thread, so nobody else waits. A part that starts later subscribes to `DISPLAY_STATE_SIG` and calls `display_state_request()`.

```mermaid
stateDiagram-v2
  [*] --> Display_powering: claim the panel, ask for P7 level
  Display_powering --> Display_powering: first IOEXP_FIELD_RSP_SIG, ask for P7 direction
  Display_powering --> Display_active: second answer or a refusal, panel up, DISPLAY_STATE_SIG
  Display_active --> Display_testing: DISPLAY_TEST_REQ_SIG with the panel up
  Display_testing --> Display_active: verdict published
```

*AO_Display states*

### Powering

The panel's supply, `VCI_EN`, is pin P7 of the TCA6408A, which AO_IoExp owns (`sdk/kaos/ioexp`). AO_Display asks for the output level first (`o7` = 1) and for the direction after (`c7` = 0), so the pin never drives low on its way to high. State requests that arrive meanwhile are deferred into a four-event queue and recalled when `Display_active` is entered, because the asker cannot tell a late panel from a missing one.

### Coming up

`panel_up()` initialises the driver, then reads `kset/bright`, clamps it to 5 to 100 and hands it to the driver before the announcement. The glass is still black, and the driver writes the value with the first frame, so the panel lights once at the level where it stays. With the mirror compiled in, the JPEG codec and the scaler are initialised here too; neither is fatal if it fails. AO_Display then publishes `DISPLAY_STATE_SIG` whether or not the panel came up, since a renderer that hears nothing cannot tell a failed panel from a slow one.

### Active

`Display_active` handles every request in the Specifications tables. `Display_testing` is a substate of it, so everything `Display_active` does for other parts keeps working while the production row runs.

### The flush is not an event

The flush is the one signal that does not come back as an event. The renderer waits for the end of a transfer inside LVGL's own render call, where it reads no queue. The DMA interrupt signals a semaphore that `kdrv_display` owns, and the renderer waits on it with `kdrv_display_flush_wait()`, for 500 ms at most. A flush that fails to start is released by hand with `kdrv_display_flush_abort()`. This is one of the three documented exceptions to "everything is an event" in `sdk/design-rules`.

### Capture and shot are loans

A screenshot is a 402 KB BMP and a shot is a JPEG of up to 64 KB (`KSHOT_JPEG_CAP`). Neither fits in an event. The requester gets a response event, reads the buffer through `display_capture_buf()` or `display_shot_buf()`, and gives it back with the matching release. A capture freezes the shadow until the release. The shot encodes the front framebuffer in one band into a PSRAM buffer that the first shot allocates and keeps.

**A shot request can go unanswered.** A request that arrives before the panel is up gets no response, and a pool with no free event block cannot carry one. Whoever asks sets a deadline of its own; AO_Net gives the display 2 s. A lent shot or capture also refuses every other request until it is released.

### The rest of the screen

With `CONFIG_KODE_LOWPOWER`, AO_Display joins the rounds of AO_LowPower as `display` (`sdk/kaos/lowpower`). The DSI link and both framebuffers go while the board sleeps, so the renderer is parked before the link goes. AO_Display cannot tell a renderer that draws from one that does not. It knows only that somebody has flushed, and then it waits for the renderer's word that it has stopped.

- **A round opens from AWAKE.** If a screenshot or a shot is lent, or the mirror runs, AO_Display answers busy (`glass lent`, `mirror running`). Otherwise it parks. With no renderer ever seen, it rests at once. With one, the screen stays lit and the link up, flushes still reach the glass (the renderer draws its last page), and a one-shot of `KODE_LOWPOWER_GLYPHS_WAIT_MS` (1500 ms) starts.
- **`DISPLAY_GLYPHS_SIG` arrives.** This is the renderer's word that it has stopped: a strip of glyphs, or a NULL strip for "stopped, with nothing to give". Flushes are dropped from now on. For ASLEEP, AO_Display sets the brightness to the strip's `rest_pct` (or `KODE_LOWPOWER_DIM_PCT`, 20 %), takes the link down, writes the time into the panel's memory at once and answers ready. For OFF, it puts the panel to sleep, takes the link down and answers ready. A rest from ASLEEP to OFF brings the link up for the sleep-in first and takes it down again.
- **The one-shot ends first.** The renderer did not park. AO_Display answers busy `renderer not parked` and the screen stays lit.
- **A step that fails** answers busy with its reason (`display link`, `display panel`), and `glass not restored` answers a round that finds the glass half restored.
- **`DISPLAY_MINUTE_REQ_SIG`.** Each minute while ASLEEP, AO_LowPower asks for the time. AO_Display reads the wall clock, converts it to local time with the zone the face keeps in `TZ`, composes `hh:mm` from the strip (`display_clock.c`, with the 12 h dial when `h12` is set) and writes the rectangle with `kdrv_display_gram_write()`. It writes nothing when the clock is not set or no glyphs were handed. A write that fails is tried once more after `KODE_LOWPOWER_MINUTE_RETRY_MS` (150 ms), in the same step, because the QP tick is stopped while the board sleeps. A task that stops the flash cache is the usual cause, and it is over by then.
- **AWAKE.** `LOWPOWER_STATE_SIG` with state AWAKE brings the link up, wakes the panel if it was asleep, sets the stored brightness and publishes `DISPLAY_REPAINT_SIG`. The restore is idempotent, and the repaint goes out on every exit from a rest it began, a busy refusal included, because the renderer parked for the round and waits for it. A restore that fails is tried again every `KODE_DISPLAY_RESTORE_RETRY_MS` (200 ms), since a board that stays awake hears no further AWAKE.

The contract of the framebuffers is in `sdk/kaos/lowpower`, and the calls that move the link are on `sdk/kdrv/co5300`.

## Specifications

### Signals in

| Signal | Event | From | What AO_Display does |
| --- | --- | --- | --- |
| `DISPLAY_FLUSH_SIG` | `FlushEvt`: `x1`, `y1`, `x2`, `y2`, `pixels`, `last` | AO_Gui, kscreen | `kdrv_display_flush()`; on failure `kdrv_display_flush_abort()`. `last` marks the final chunk, where the driver flips the framebuffer |
| `DISPLAY_BRIGHTNESS_SIG` | `BrightnessEvt`: `percent`, `persist` | AO_Gui, any app | Drives the panel and updates the announced `bright`. With `persist` it clamps to 5 to 100 and stores `kset/bright`. Without it there is no floor and nothing is stored |
| `CONFIG_REQ_SIG` | `KodeConfigReqEvt` | AO_Console | Answers the key `bright`; a set applies and stores like a persisted slider release |
| `DISPLAY_STATE_REQ_SIG` | none | any part that starts late | Publishes `DISPLAY_STATE_SIG` again. Deferred while powering |
| `DISPLAY_CAPTURE_REQ_SIG` | `DisplayCaptureReqEvt`: `who` | AO_Console | Freezes the shadow and posts `DISPLAY_CAPTURE_RSP_SIG` to `who` |
| `DISPLAY_CAPTURE_END_SIG` | none | the requester | Releases the shadow |
| `DISPLAY_SHOT_REQ_SIG` | `DisplayShotReqEvt`: `who`, `quality` | AO_Net | Encodes the glass as one JPEG and posts `DISPLAY_SHOT_RSP_SIG` to `who` |
| `DISPLAY_SHOT_END_SIG` | none | the requester | Ends the loan |
| `DISPLAY_MIRROR_START_SIG`, `_STOP_`, `_ACK_`, `_INFO_REQ_` | `MirrorStartEvt`, `MirrorAckEvt` | AO_Console | The live mirror (`KODEOS_MIRROR` only) |
| `DISPLAY_TEST_REQ_SIG` | none | AO_Autotest | Starts the production row |
| `IOEXP_FIELD_RSP_SIG` | `IoexpFieldEvt` | AO_IoExp | Only in `Display_powering`: the two answers about P7 |
| `LOWPOWER_ROUND_SIG`, `LOWPOWER_STATE_SIG` | `LowPowerRoundEvt`, `LowPowerStateEvt` | AO_LowPower | With `CONFIG_KODE_LOWPOWER`: rest the glass, and put it back on state AWAKE |
| `DISPLAY_MINUTE_REQ_SIG` | none | AO_LowPower | With `CONFIG_KODE_LOWPOWER`: write the time into the panel's memory |
| `DISPLAY_GLYPHS_SIG` | `DisplayGlyphsEvt` | The renderer, through `display_glyphs_hand()` | With `CONFIG_KODE_LOWPOWER`: the renderer has stopped, and here are the digits of the time. The strip is a pointer into the renderer's PSRAM and AO_Display never writes it |
| `DISPLAY_GLYPHS_TMO_SIG`, `DISPLAY_RESTORE_SIG` | none (timers) | Itself | The wait for the glyphs, and the retry of a restore that failed |

### Signals out

| Signal | Event | Meaning |
| --- | --- | --- |
| `DISPLAY_STATE_SIG` | `DisplayStateEvt`: `up`, `bright` | Published once when the panel comes up, and again on every state request. A static event, so republishing is safe. `bright` seeds the settings slider and means nothing when `up` is false |
| `DISPLAY_CAPTURE_RSP_SIG` | `DisplayCaptureRspEvt`: `len`, `w`, `h`, `status`, `painted` | Posted to the asker. `painted` false means nothing was flushed since boot, so the image is black |
| `DISPLAY_SHOT_RSP_SIG` | `DisplayShotRspEvt`: `len`, `status` | `KODE_OK` lends the buffer. `KODE_DENIED`: codec busy or a shot already lent. `KODE_NO_RESOURCES`: no codec, no framebuffer or no PSRAM. `KODE_ERROR`: larger than 64 KB |
| `DISPLAY_MIRROR_INFO_SIG` | `MirrorInfoEvt` | The mirror's live state |
| `DISPLAY_TEST_SIG` | `DisplayTestEvt`: `step`, `total`, `phase`, `field` | One record of the production row |
| `TEST_RESULT_SIG` | named `display` | The row's verdict |
| `DISPLAY_REPAINT_SIG` | none | On every exit from the row, and with `CONFIG_KODE_LOWPOWER` on every exit from a rest of the glass: the link has new framebuffers, and the renderer fetches them again and invalidates the whole screen |

### Calls

| Call | What it does |
| --- | --- |
| `display_state_request()` | Posts `DISPLAY_STATE_REQ_SIG`. Only after `kode_init()` returns: earlier, AO_Display has no queue and the post is a FreeRTOS assert and a boot loop |
| `display_brightness_request(pct, persist)` | Posts `DISPLAY_BRIGHTNESS_SIG` |
| `display_capture_request(who)`, `display_capture_buf()`, `display_capture_release()` | The capture loan. The buffer is valid between a `KODE_OK` response and the release |
| `display_shot_request(who, quality)`, `display_shot_buf()`, `display_shot_release()` | The shot loan. `quality` is 1 to 100. `display_shot_buf()` is `NULL` when nothing is lent |
| `display_mirror_start(scale, quality)`, `_stop()`, `_ack(seq)`, `_info_request()` | The mirror. `scale` is 1 or 2; the quality is capped at 70 |
| `display_test_request()` | Defined in every build. With no panel the row is one record and a failed verdict |
| `display_glyphs_hand(glyphs)` | With `CONFIG_KODE_LOWPOWER`: the renderer's hand-over of the time's digits, which also says it has stopped drawing. Posts `DISPLAY_GLYPHS_SIG` |

### The production row

`test component=display` paints five full-screen fields in turn: white, red, green, blue and black (`DISPLAY_FIELD_WHITE` to `DISPLAY_FIELD_BLACK`, five in all). Nothing on the board can see the glass, so the person at the bench judges each one: the top button says yes, the bottom button says no.

Each field waits `KODE_DISPLAY_TEST_STEP_MS` (10 s). The row ends at the first field that is not confirmed, and its console budget is 53 s (`KODE_DISPLAY_TEST_BUDGET_MS`, five steps plus 3 s).

| Thing | Value |
| --- | --- |
| Brightness during the row | 100 % (`KODE_DISPLAY_TEST_BRIGHT`), through the driver, so nothing reaches the store |
| Painting | Bands of 16 lines (`KODE_DISPLAY_TEST_BAND_ROWS`) from a PSRAM buffer, one band per `DISPLAY_TEST_STEP_SIG`. 502 lines do not divide by 16, so the last of the 32 bands is short |
| Phases | `DISPLAY_TEST_ASK`, `_PASS`, `_FAIL`, `_TIMEOUT`, `_DONE`, `_ABSENT` |
| The pad | `Display_testing` subscribes to `BUTTON_EVT_SIG` on entry and unsubscribes on exit. A press while a field is still going up is dropped |
| The renderer meanwhile | Every `DISPLAY_FLUSH_SIG` is completed with `kdrv_display_flush_abort()` and dropped, so the launcher never paints over the field |
| Exit | Every way out goes through `Q_EXIT_SIG`: stops the wait timer, frees the pad and the band, restores the user's brightness and publishes `DISPLAY_REPAINT_SIG` |
| No panel | `>\|step=1 panel=absent result=absent` and a failed verdict |

### Options, verbs and settings

| Thing | Value |
| --- | --- |
| `KODEOS_SCREENSHOT` | Compiles `screenshot` |
| `KODEOS_MIRROR` | Depends on `KODEOS_SCREENSHOT`. Compiles the mirror, the JPEG codec and the scaler, the two extra claims and the shot path |
| `KODEOS_SHOT_HTTP` | Depends on the Kode channel and `KODEOS_MIRROR`. Lets `sdk/kaos/net` ask for shots |
| Console | `config key="bright" value=N`, `screenshot`, `test component=display`, `mirror_start`, `mirror_stop`, `mirror_ack`, `mirror_info` |
| Setting | `kset/bright`, a `u8`, 5 to 100, default 100 (`KSET_BRIGHT_DEF`, `KSET_BRIGHT_MIN`). AO_Display is the only writer |
| Timers | `DISPLAY_TEST_WAIT_SIG`, one-shot, re-armed per field. `DISPLAY_MIRROR_TICK_SIG`, one-shot of 10 ms, re-armed after every pass and left disarmed while a session is paused |

## Gotchas

> **WARNING**
>
> **The board boots into an assert when you call `display_state_request()` too early.** Before `kode_init()` returns, AO_Display has no queue, and a post to it fails inside FreeRTOS (`xQueueGenericSend`). Call it only after `kode_init()`. An AO that `kode_init()` itself starts needs no request, since it subscribes before the announcement.

> **WARNING**
>
> **The screen stays black although the panel is up.** The panel is announced dark. The first frame a renderer flushes lights it, at the announced `bright`. A firmware that sets brightness and never draws leaves the glass black.

> **NOTE**
>
> **A subscriber that reads only the first `DISPLAY_STATE_SIG` holds an old brightness.** `bright` is the one field that changes after the announcement, whenever a slider is released or `config key="bright"` sets it. Read every `DISPLAY_STATE_SIG`.

> **NOTE**
>
> **`config key="bright"` reports the stored value, not the one in force.** A transient `display_brightness_request(pct, false)` dims the glass and the console still reads the stored number. A persisted brightness never goes below 5, because 0 is black on this panel and the control that turns it up lives on the glass.

## Example code

This sets the brightness to 20 % and stores it. The glass dims once a frame is on it, and `config key="bright"` on the console answers `value=20 stored=yes`. Restore it with `config key="bright" value=100`.

```c
/*
 * display_dim.c - 2026-10-28 - from Kode with love
 *
 * Asks AO_Display, the owner of the Kode Dot's panel, for a stored brightness
 * of 20 %, then idles so the console stays up. Type config key="bright" on
 * the console and the answer reads value=20 stored=yes.
 */
#include "kode_ao.h"

/* Bring the system up, ask for the new brightness, then idle. */
void app_main(void)
{
    kode_init();
    display_brightness_request(20, true);

    while (1) {
        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/display`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/display): `display_ao.c`, the state machine, the loans, the mirror and the production row.
- [`kaos.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/core/include/kaos.h): the public calls and the contract of the flush semaphore.
- [`components/kdrv/display`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/display): the CO5300 driver the AO calls; see `sdk/kdrv/co5300`.
