# AO_Buttons

The Active Object that owns the Kode Dot's six buttons: debounce, presses, holds and double presses, and the guided button test.

## What it is

The Kode Dot has six buttons: the pad (up, down, left, right), the top button and the bottom button. **AO_Buttons** owns them and turns the clicks of a tactile switch into clean events. A switch bounces, so its contact opens and closes several times when a finger presses it.

AO_Buttons accepts a change only when it holds for a settle time, and then publishes one `BUTTON_EVT_SIG` per button that changed, with the button and whether it is pressed. A subscriber never has to know about bounce.

It has three more jobs. It publishes `BUTTON_HOLD_SIG` when the top or bottom button stays down for 600 ms. It publishes `BUTTON_DOUBLE_SIG` when the top button is pressed twice within 800 ms. It turns the pad with the glass for an app that rotates its screen. It also runs the guided button test of the production line.

## How it is connected

```mermaid
flowchart LR
  pad["Pad and BOT<br/>expander P0 to P4"] --> exp["AO_IoExp"]
  exp -- "IOEXP_INPUT_SIG" --> btn["AO_Buttons"]
  top["TOP<br/>GPIO35"] -- "edge interrupt" --> btn
  btn -- "IOEXP_READ_REQ_SIG<br/>settle sample" --> exp
  btn -- "BUTTON_EVT_SIG" --> subs["AO_Gui, your AO"]
  btn -- "BUTTON_HOLD_SIG" --> boot["AO_Boot"]
  class exp,btn,boot ao
  class pad,top chip
```

*Where the six buttons come from*

Five buttons are on pins P0 to P4 of the TCA6408A expander and the top button is on GPIO35. AO_Buttons reads nothing while nobody presses a button. AO_IoExp owns the expander and publishes the levels of its eight pins on each interrupt (`sdk/kaos/ioexp`), and the edge of GPIO35 posts an event straight to AO_Buttons. Each arrival is one sample of the six buttons.

AO_Buttons in numbers:

| Thing | Value |
| --- | --- |
| Priority | 13 |
| Queue | 16 events |
| Stack | 3072 bytes |
| Claims | `KBSP_RES_BTN_TOP`; the expander belongs to AO_IoExp |
| States | One, `Buttons_watching` |

Its initial transition subscribes to `IOEXP_INPUT_SIG`, configures the pin with `kdrv_buttons_init()` and arms the edge with `kdrv_buttons_top_attach()`.

When the pin does not configure it logs `no top button; the other five still arrive from the expander` and carries on (`sdk/kdrv/buttons`).

### Debounce

The AO keeps one bit per button in three masks: `stable` (what it believes), `candidate` (the sample before) and the new `sample`. A press is confirmed when the bit reads high in both the candidate and the sample, and a release when it reads low in both. A change moves only on the settle sample, the one asked for `KODE_BUTTONS_SETTLE_MS` (30 ms) after the change that armed the settle timer, and only when it agrees with the sample before.

Other samples, such as every expander publish and every edge of GPIO35, only update the candidate.

The settle sample exists because of measurements. The bottom button, held up only by the 200 k pull of the PMIC's QON pin, rings as it is released. Before the rule, one release published a release, a press nobody made and a second release within 1 ms. GPIO35 is also a strapping pin of the chip.

After each sample, if the candidate still disagrees with `stable` and the settle timer is not armed, AO_Buttons arms it. A later sample does not restart it. A finger on the glass makes the expander publish every few milliseconds, and a restarted timer would never fire.

### Where samples come from

The table lists what can arrive and what AO_Buttons does with it.

| Arrival | What AO_Buttons does |
| --- | --- |
| `IOEXP_INPUT_SIG` with `ok` | Remembers the byte, reads GPIO35 now, decodes the six buttons with `kdrv_buttons_decode()` and runs the debounce |
| `IOEXP_INPUT_SIG` without `ok` | Not a sample. A failed read decoded as "every button released" would publish releases nobody made. A pending `btn_read` is answered `ok = false` |
| `BUTTONS_TOP_INT_SIG` | An ordinary sample of GPIO35 and the last expander byte. The pin's interrupt stays off until the settle timer re-arms it, so a bouncing contact posts one event per arming |
| `BUTTONS_SETTLE_SIG` | Re-arms GPIO35 and calls `ioexp_read_request()`. The answer is the settle sample |
| `BUTTONS_HOLD_TMO_SIG` | Publishes `BUTTON_HOLD_SIG` if that button is still down |

Every confirmed edge logs one INFO line naming the button and the sample that confirmed it, and every raw edge of GPIO35 logs its own line, so a launch nobody made can be traced.

### Hold and double

A confirmed press of the top or the bottom button arms the one `hold` timer for 600 ms (`KODE_BUTTON_HOLD_MS`). The release disarms it, and a press of the other button restarts it. `BUTTON_HOLD_SIG` carries `KDRV_BTN_TOP` or `KDRV_BTN_BOT`, never a pad button. AO_Boot turns the bottom button's hold into `HOME_SIG` (`sdk/kaos/boot`).

A top press confirmed within `KODE_BUTTON_DOUBLE_MS` (800 ms) of the previous top release publishes `BUTTON_DOUBLE_SIG` right after its own `BUTTON_EVT_SIG`. A press that reached hold arms nothing, and a press that completes a double is spent, so three quick presses make one double. The detector is fed only by confirmed edges, so an injected press never reaches it: `btn id=top state=double` injects one.

### The pad follows the glass

An app that turns its screen with `kscreen_set_rotation()` gets its pad turned with it when `CONFIG_KSCREEN_ROTATE_PAD` is on, the default (`sdk/kscreen`). kscreen calls `buttons_rotation_request(quarter_turns)`, and each pad button then publishes the direction it points on the turned glass. TOP and BOT never turn.

A turn that arrives while a pad button is down waits until the last one is released, so a press and its release always name the same button.

The table lists the button each pad button publishes for each turn of the glass.

| Turn | UP publishes | DOWN publishes | LEFT publishes | RIGHT publishes |
| --- | --- | --- | --- | --- |
| 0 | UP | DOWN | LEFT | RIGHT |
| 90 | RIGHT | LEFT | UP | DOWN |
| 180 | DOWN | UP | RIGHT | LEFT |
| 270 | LEFT | RIGHT | DOWN | UP |

Injected presses are turned like real ones. The hardware checks do not turn: `btn_read`, `BTN_STATE_SIG` and the guided test report the buttons as wired, so while a turn is in force `btn_read` and `buttons_down()` can name different buttons.

### The guided test

`test component=buttons` runs six steps: pad-left, bottom, pad-down, top, pad-right, pad-up. Two faults show as a wrong name in a record: a short circuit between two pad lines, and a pin mapped to the wrong button.

Each step publishes a `BTN_TEST_SIG` prompt. The first confirmed sample with at least one button down answers it, and it passes only when exactly the asked button is down. A step has `KODE_BUTTONS_TEST_STEP_MS` (20 s) to press and `KODE_BUTTONS_TEST_RELEASE_MS` (3 s) to release. The next prompt waits until every button is up, because the bottom button and pad-left held together reset the main chip in hardware.

A button still held after the release window is `BTN_TEST_STUCK`, a failed step that names it. A button already down when a step begins fails that step as `BTN_TEST_HELD`, with no prompt.

The operator's slips are forgiven. A wrong button (`BTN_TEST_WRONG`) or the asked button together with another (`BTN_TEST_DOUBLE`) asks for the same button again. The third wrong press on a step (`KODE_BUTTONS_TEST_TRIES`) is `BTN_TEST_GIVEUP`: the test fails and the run ends. The second double on a step (`KODE_BUTTONS_TEST_DOUBLE_TRIES`) is `BTN_TEST_DOUBLEFAIL` and fails the step. Any other failure does not stop the run. The verdict is a published `TEST_RESULT_SIG` named `buttons`. The worst case is 557 s (`KODE_BUTTONS_TEST_BUDGET_MS`): six steps, four attempts each, 23 s an attempt, plus 5 s.

**The origin of a run.** `buttons_test_request_from(origin)` starts a run for a client other than AO_Autotest. Every `BTN_TEST_SIG` step and the verdict carry the `origin` of the run (`TEST_ORIGIN_AUTOTEST`, 0, or the client's own number), so AO_Autotest and the console count only the steps and the verdict of a run they asked for. A request that arrives while a run is up is refused: it gets a failing verdict in its own origin, and the run in flight keeps its origin.

## Specifications

### Signals in

| Signal | Event | From |
| --- | --- | --- |
| `IOEXP_INPUT_SIG` | `IoexpInputEvt` | AO_IoExp, subscribed |
| `BUTTONS_TOP_INT_SIG` | none | The GPIO35 interrupt, through `kqp_post_from_isr()` |
| `BUTTONS_DUMP_REQ_SIG` | none | `buttons_dump_request()`, for `btn_read`. Marks a snapshot pending and asks AO_IoExp for a reading |
| `BUTTONS_TEST_REQ_SIG` | none | `buttons_test_request()` |
| `BUTTONS_ROTATE_SIG` | `ButtonsRotateEvt` (`quarter_turns`, 0 to 3) | `buttons_rotation_request()`. A larger value is ignored with a warning |
| `BUTTONS_SETTLE_SIG`, `BUTTONS_TEST_TMO_SIG`, `BUTTONS_HOLD_TMO_SIG` | none (timers) | Itself |
| `BUTTONS_PAD_IDLE_SIG` | none | Private: an injected release left the pad idle, so a waiting turn may apply |

### Signals out

| Signal | Event | Range | Subscribers |
| --- | --- | --- | --- |
| `BUTTON_EVT_SIG` | `ButtonEvt` (`id`, `pressed`, `t_ms`) | publish | AO_Gui; AO_LedMgr and AO_Display during their production rows; your AO |
| `BUTTON_HOLD_SIG` | `ButtonHoldEvt` (`id`) | publish | AO_Boot, AO_Gui |
| `BUTTON_DOUBLE_SIG` | `ButtonDoubleEvt` (`id`, `t_ms`) | publish | Your AO. `id` is always the top button |
| `BTN_STATE_SIG` | `ButtonsStateEvt` (`pressed`, `ok`) | publish | Closes `btn_read` with the raw sample and not the debounced belief |
| `BTN_TEST_SIG` | `ButtonsTestEvt` (`phase`, `step`, `total`, `button`, `pressed`, `origin`) | publish | The console prints it and kgui shows the prompt |
| `TEST_RESULT_SIG` | `TestResultEvt` named `buttons` | publish | AO_Autotest |
| `IOEXP_READ_REQ_SIG` | none | post to AO_IoExp | When the settle timer fires and on `btn_read` |

The event allocation is bounded: when the pool is dry a press is dropped and never asserted.

### The timers

| Timer | Signal | Value | Armed by |
| --- | --- | --- | --- |
| `settle` | `BUTTONS_SETTLE_SIG` | 30 ms, one-shot | A sample that leaves a bit unconfirmed. Disarmed when everything agrees |
| `hold` | `BUTTONS_HOLD_TMO_SIG` | 600 ms, one-shot | A confirmed press of TOP or BOT. Disarmed by the release or the start of a guided run |
| `test_tmo` | `BUTTONS_TEST_TMO_SIG` | 20 s for a press, 3 s for a release, one-shot | Each prompt, and again for the release |

### The calls

| Call | What it does |
| --- | --- |
| `kode_buttons_read()` | The mask of buttons down: bit `1 << kdrv_button_t` for `KDRV_BTN_UP` (0), `_DOWN`, `_LEFT`, `_RIGHT`, `_TOP` (4), `_BOT` (5). It reads one word, touches no bus, never blocks and is safe from any task. It follows the `BUTTON_EVT_SIG` stream, so a polling app and a subscribing one agree |
| `buttons_down()` | The same word, from `kaos.h` |
| `buttons_inject(id, pressed)` | Publishes one change as if AO_Buttons had confirmed it. Used by the `btn` verb. False for an `id` out of range or a dry pool |
| `buttons_dump_request()`, `buttons_test_request()`, `buttons_rotation_request()` | The three posts above |

The console verbs are `btn_read`, `btn`, and `test component=buttons`. `btn id=top|bot state=hold` publishes the hold a thumb would. The constants are `KODE_BUTTONS_SETTLE_MS`, `KODE_BUTTON_HOLD_MS`, `KODE_BUTTON_DOUBLE_MS`, `KODE_BUTTONS_TEST_STEP_MS` and `KODE_BUTTONS_TEST_RELEASE_MS`. AO_Buttons has no option and no stored setting.

## Gotchas

> **WARNING**
>
> **Bottom button plus pad-left held for about a second resets the main chip in hardware.** An OR gate pulls the reset input low. AO_Buttons publishes two presses and then the board reboots, and nothing in firmware can stop it. A hold of the bottom button alone reaches 600 ms well before that, so `BUTTON_HOLD_SIG` is out first.

> **NOTE**
>
> **A tap shorter than the settle time shows nowhere.** The debounce is symmetric: a press and release faster than 30 ms is invisible. A polling app finds an edge by comparing with the previous call to `kode_buttons_read()`, and does not catch a bit set.

> **NOTE**
>
> **`btn_read` and `kode_buttons_read()` can disagree under a turned pad.** `btn_read` reports the wiring, and `kode_buttons_read()` reports the published ids.

> **NOTE**
>
> **The settle time is also the debounce time.** A bounce or an opening that outlasts 30 ms gets through. A thumb on the bottom button has opened the contact for 12 ms, which the settle hides, and for 31 to 48 ms, which reads as a release and a new press and restarts a hold.

## Example code

Poll the buttons and show them on the LED. With the app running, the LED turns green while the top button is down, red while the bottom button is down and off otherwise.

```c
/*
 * button_led.c - 2026-10-28 - from Kode with love
 *
 * Polls kode_buttons_read(), which AO_Buttons keeps up to date, and sets the
 * status LED to match: green for the top button, red for the bottom button,
 * off when neither is down. On a Kode Dot the colour follows the buttons.
 */
#include "kode.h"

/* Choose the colour for the buttons that are down. */
static void show(uint32_t down)
{
    if ((down & (1U << KDRV_BTN_TOP)) != 0U) {
        (void)kode_led_rgb(0, 64, 0);
    } else if ((down & (1U << KDRV_BTN_BOT)) != 0U) {
        (void)kode_led_rgb(64, 0, 0);
    } else {
        (void)kode_led_rgb(0, 0, 0);
    }
}

/* Poll every 20 ms and publish a colour only when the buttons change. */
void app_main(void)
{
    uint32_t last = 0U;

    kode_init();
    for (;;) {
        uint32_t const down = kode_buttons_read();
        if (down != last) {
            show(down);
            last = down;
        }
        kode_delay_ms(20);
    }
}
```

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/buttons`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/buttons): `buttons_ao.c`, the debounce, the hold and the test.
- [`components/kdrv/buttons`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/buttons): the decoding and GPIO35 (`sdk/kdrv/buttons`).
- [`kdrv_buttons.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/buttons/include/kdrv_buttons.h): the button ids.
