# Buttons

The Kode Dot's D-pad and two side buttons: which pin each one is on, how a press is debounced, and the press, hold and double-press events your code receives.

## What it is

The Kode Dot has six buttons: a D-pad of four (up, down, left, right) and two side buttons, top and bottom. The driver names each one by what it is for, not by where it is wired, and turns the state of all six into one number, the **button mask**, with one bit per button.

Your code never sees the wiring or the bounce. A press arrives as one `BUTTON_EVT_SIG` event that says which button changed and whether it is down, already filtered. Two gestures are timed for you as well: a hold of the top or bottom button, and a double press of the top button. If you prefer to poll, `kode_buttons_read()` returns the mask.

## How it is connected

```mermaid
flowchart LR
  subgraph board["On the Kode Dot"]
    pad["pad and<br/>bottom button"] --> tca["TCA6408A<br/>P0 to P4"]
    tca -- "INT, GPIO6" --> ioexp["AO_IoExp"]
    ioexp -- "IOEXP_INPUT_SIG" --> btn["AO_Buttons"]
    top["top button<br/>GPIO35"] -- "edge" --> btn
  end
  btn -- "BUTTON_EVT_SIG" --> app["your AO"]
  class pad chip
  class tca chip
  class top chip
  class ioexp ao
  class btn ao
  class app app
```

*From a finger to a button event*

### The wiring

Five buttons hang off the TCA6408A expander (`sdk/kdrv/tca6408a`) and one is on a pin of the P4. All six switches are SKRMAAE010 and all are active low: a pressed button pulls its net to ground and reads 0.

| Enum | Button | Where | Pull-up |
| --- | --- | --- | --- |
| `KDRV_BTN_UP` | D-pad up | Expander P1 | R39, 10 kΩ |
| `KDRV_BTN_DOWN` | D-pad down | Expander P3 | R41, 10 kΩ |
| `KDRV_BTN_LEFT` | D-pad left | Expander P2 | R40, 10 kΩ |
| `KDRV_BTN_RIGHT` | D-pad right | Expander P4 | R42, 10 kΩ |
| `KDRV_BTN_TOP` | The upper side button | GPIO35 (`KBSP_PIN_BUTTON_TOP`) | R38, 10 kΩ, and the P4's internal pull-up |
| `KDRV_BTN_BOT` | The lower side button | Expander P0 | None of its own: the charger's internal `QON` pull-up, 200 kΩ |

The bottom button shares its net with the `QON` pin of the BQ25896 (`sdk/kdrv/bq25896`), so it is the button that wakes the board from ship mode, and the one that wakes it from OFF. A tap does not wake it from OFF: the boot gate wants BOT held for about a second (`sdk/kaos/lowpower`). Two hardware behaviours act with no software. The bottom button and D-pad left held together for about a second reset the P4. The bottom button held 16 to 23 s with no USB power-cycles the board. A script that tests buttons must never hold those two together.

### Owners and debounce

**Two owners.** The expander belongs to AO_IoExp, which reads it on its interrupt and publishes all eight pins. AO_Buttons owns the driver, claims `KBSP_RES_BTN_TOP` (GPIO35), runs at priority 13 and reads nothing while nobody presses. An expander publish and an edge on GPIO35 each count as one sample of the six buttons.

The driver itself holds only GPIO35: it sets the input with the pull-up, arms an any-edge interrupt, and decodes a sample. The interrupt disables itself before its callback runs, so a bouncing contact posts once per arming.

**The debounce is AO_Buttons'.** A change arms a one-shot of 30 ms (`KODE_BUTTONS_SETTLE_MS`). When it fires, AO_Buttons re-arms GPIO35 and asks AO_IoExp for a fresh reading. That reading is the only sample that can confirm a change, for all six buttons, and only when it agrees with the sample before it. Earlier samples, which arrive microseconds apart, never confirm. There are two reasons:

- GPIO35 is a strapping pin that has carried a false level across a reset.
- The bottom button, held up only by `QON`'s 200 kΩ, rings as it is let go. Measured on a Kode Dot, with two close samples enough, one release published a release, a press nobody made and a second release within 1 ms.

A failed expander read is not a sample, since decoding a dead bus as "every button released" would publish releases nobody made.

### The events

AO_Buttons publishes on change only, so a resting finger costs nothing downstream. Every confirmed edge logs one line at INFO: the button, `pressed` or `released`, and which sample confirmed it.

| Signal | Payload | When |
| --- | --- | --- |
| `BUTTON_EVT_SIG` | `ButtonEvt`: `id` (a `kdrv_button_t`), `pressed`, `t_ms` (`khal_time_ms()` at confirmation) | Each confirmed press and release |
| `BUTTON_HOLD_SIG` | `ButtonHoldEvt`: `id`, top or bottom | 600 ms (`KODE_BUTTON_HOLD_MS`) after a press was confirmed, if the button is still down. Once per press. Never armed during the guided test |
| `BUTTON_DOUBLE_SIG` | `ButtonDoubleEvt`: `id` (always top), `t_ms` | Right after the second press's `BUTTON_EVT_SIG`, when it is confirmed within 800 ms (`KODE_BUTTON_DOUBLE_MS`) of the first press's release |

A press that reached hold does not start a double, and the second press of a double does not start another, so three quick taps make one double. The detector is fed only by real presses; console-injected ones are left out on purpose.

### The pad follows the glass

An app that rotates its screen with `kscreen_set_rotation()` gets the D-pad rotated with it when `CONFIG_KSCREEN_ROTATE_PAD` is on, which is the default. kscreen calls `buttons_rotation_request(quarter_turns)`, and each pad button then publishes the direction it points on the turned glass. The top and bottom buttons never turn. A turn that arrives while a pad button is down waits for its release, so a press and its release always name the same button.

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

### The guided test

`test component=buttons` asks for six presses: left, bottom, down, top, right, up. A short between two D-pad tracks or a pin mapped to the wrong button shows as a wrong name.

- The first confirmed sample with a button down is the answer: exactly the asked button passes. A wrong button, or the asked button with another, is forgiven and asked again; the third wrong press on a step ends the test as a failure, and the second double fails the step. Each names what was pressed.
- Each prompt waits until every button is released, so the operator is never asked for the second half of the reset combination.
- A step times out after 20 s, a button still held 3 s after being judged is reported stuck, and the run goes on.
- The verdict is `TEST_RESULT_SIG` named `buttons`.

## Specifications

### The driver

| Call | What it does |
| --- | --- |
| `kdrv_buttons_init()` | Makes GPIO35 an input with the P4's pull-up |
| `kdrv_buttons_top_attach(cb, arg)` | Arms GPIO35's any-edge interrupt. `cb` runs in interrupt context and may post one static event and nothing else. `KODE_DENIED` for a NULL callback |
| `kdrv_buttons_top_rearm()` | Enables the interrupt again after the owner has sampled the pin |
| `kdrv_buttons_top_pressed()` | True when GPIO35 reads low |
| `kdrv_buttons_decode(exp_levels, top_pressed)` | Pure function. Takes the expander's input byte (bit n is Pn, high at rest) and the top button's state; returns the mask with bit `1 << kdrv_button_t` set for each button that is down. P5 to P7 are ignored |
| `kdrv_button_name(b)` | `pad-up`, `pad-down`, `pad-left`, `pad-right`, `top`, `bottom`; `?` out of range. The `pad-` prefix keeps a log line such as `pad-down pressed` from reading as a state |

The driver does no debouncing. A caller that used it directly would see every bounce.

### The owner

| Call | What it does |
| --- | --- |
| `kode_buttons_read()` | The mask of buttons down, as the `BUTTON_EVT_SIG` stream last described them, so a polling app and a subscribing one agree. A tap shorter than the 30 ms settle shows nowhere; compare with the previous call to find an edge. 0 before `kode_init()` |
| `buttons_down()` | The same word, from kaos. Never blocks, any task may call it |
| `buttons_inject(id, pressed)` | Publishes one change as if AO_Buttons had confirmed it; what the console's `btn` uses. False for an `id` out of range or a dry event pool |
| `buttons_dump_request()` | Asks for one raw reading, answered by `BTN_STATE_SIG`; the console's `btn_read` |
| `buttons_test_request()` | Starts the guided test |
| `buttons_rotation_request(quarter_turns)` | Turns the D-pad, 0 to 3. Anything else is ignored with a warning |

### The timings

| Constant | Value | Meaning |
| --- | --- | --- |
| `KODE_BUTTONS_SETTLE_MS` | 30 ms | Time from a change to its confirming sample |
| `KODE_BUTTON_HOLD_MS` | 600 ms | A press this long is a hold |
| `KODE_BUTTON_DOUBLE_MS` | 800 ms | Longest gap from the first release to the second press |
| `KODE_BUTTONS_TEST_STEP_MS` | 20 s | Time allowed for each press in the guided test |
| `KODE_BUTTONS_TEST_RELEASE_MS` | 3 s | Time allowed to let a judged press go, or the button is stuck |
| `KODE_PRIO_BUTTONS` | 13 | AO_Buttons' priority |

## Gotchas

> **WARNING**
>
> **Holding the bottom button leaves your app.** In an app, AO_Boot resets into kodeOS when the bottom button is held for 600 ms (`CONFIG_KODEOS_HOME_HOLD`, on by default), so no app can trap the user. If you need to save first, call `kode_home_defer()` inside the `BUTTON_HOLD_SIG` handler, write what you have, then call `kode_home_now()`. Do not build a long-press on the bottom button.

> **WARNING**
>
> **Do not hold the bottom button and D-pad left together.** About a second of both resets the P4 in hardware, and no firmware can stop it. It is also why the guided test never asks for the two in a row without a release between.

> **NOTE**
>
> **A press shows 30 ms late, and a tap shorter than that shows nowhere.** The confirming sample is asked for 30 ms after the change. If you poll with `kode_buttons_read()`, compare with the previous mask to catch an edge. A subscriber to `BUTTON_EVT_SIG` gets both edges.

> **NOTE**
>
> **`BUTTON_EVT_SIG` carries `id` as a byte.** The enum does not travel with the event. Cast it: `(uint8_t)KDRV_BTN_TOP == event->id`.

## Example code

Poll the buttons and show them on the LED: green while the top button is down, red for the bottom button, blue for any D-pad button. You see the colour follow your thumb.

```c
/*
 * buttons_led.c - 2026-10-28 - from Kode with love
 *
 * Reads the Kode Dot's six buttons every 20 ms and shows them on the RGB
 * LED: green while the top button is down, red for the bottom button and
 * blue for any D-pad button. You see the colour follow your thumb. The
 * bottom button held for 600 ms returns to kodeOS, so tap it.
 */
#include "kode.h"

#define LEVEL    32U
#define PAD_MASK ((1UL << (uint32_t)KDRV_BTN_TOP) - 1UL)

/* Bring the system up, then map the button mask to a colour forever. */
void app_main(void)
{
    kode_init();

    while (1) {
        uint32_t const down = kode_buttons_read();
        uint8_t red = 0U;
        uint8_t green = 0U;
        uint8_t blue = 0U;

        if ((down & (1UL << (uint32_t)KDRV_BTN_TOP)) != 0UL) {
            green = LEVEL;
        }
        if ((down & (1UL << (uint32_t)KDRV_BTN_BOT)) != 0UL) {
            red = LEVEL;
        }
        if ((down & PAD_MASK) != 0UL) {
            blue = LEVEL;
        }
        (void)kode_led_rgb(red, green, blue);
        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/kdrv/buttons`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/buttons): the driver, `kdrv_buttons.c`.
- [`kdrv_buttons.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/buttons/include/kdrv_buttons.h): the button enum and the decode call.
- [`components/kaos/buttons`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/buttons): AO_Buttons, the owner, with the debounce, hold, double press and guided test.
