SDK

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

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:

ThingValue
Priority13
Queue16 events
Stack3072 bytes
ClaimsKBSP_RES_BTN_TOP; the expander belongs to AO_IoExp
StatesOne, 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.

ArrivalWhat AO_Buttons does
IOEXP_INPUT_SIG with okRemembers the byte, reads GPIO35 now, decodes the six buttons with kdrv_buttons_decode() and runs the debounce
IOEXP_INPUT_SIG without okNot 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_SIGAn 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_SIGRe-arms GPIO35 and calls ioexp_read_request(). The answer is the settle sample
BUTTONS_HOLD_TMO_SIGPublishes 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.

TurnUP publishesDOWN publishesLEFT publishesRIGHT publishes
0UPDOWNLEFTRIGHT
90RIGHTLEFTUPDOWN
180DOWNUPRIGHTLEFT
270LEFTRIGHTDOWNUP

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

SignalEventFrom
IOEXP_INPUT_SIGIoexpInputEvtAO_IoExp, subscribed
BUTTONS_TOP_INT_SIGnoneThe GPIO35 interrupt, through kqp_post_from_isr()
BUTTONS_DUMP_REQ_SIGnonebuttons_dump_request(), for btn_read. Marks a snapshot pending and asks AO_IoExp for a reading
BUTTONS_TEST_REQ_SIGnonebuttons_test_request()
BUTTONS_ROTATE_SIGButtonsRotateEvt (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_SIGnone (timers)Itself
BUTTONS_PAD_IDLE_SIGnonePrivate: an injected release left the pad idle, so a waiting turn may apply

Signals out

SignalEventRangeSubscribers
BUTTON_EVT_SIGButtonEvt (id, pressed, t_ms)publishAO_Gui; AO_LedMgr and AO_Display during their production rows; your AO
BUTTON_HOLD_SIGButtonHoldEvt (id)publishAO_Boot, AO_Gui
BUTTON_DOUBLE_SIGButtonDoubleEvt (id, t_ms)publishYour AO. id is always the top button
BTN_STATE_SIGButtonsStateEvt (pressed, ok)publishCloses btn_read with the raw sample and not the debounced belief
BTN_TEST_SIGButtonsTestEvt (phase, step, total, button, pressed, origin)publishThe console prints it and kgui shows the prompt
TEST_RESULT_SIGTestResultEvt named buttonspublishAO_Autotest
IOEXP_READ_REQ_SIGnonepost to AO_IoExpWhen 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

TimerSignalValueArmed by
settleBUTTONS_SETTLE_SIG30 ms, one-shotA sample that leaves a bit unconfirmed. Disarmed when everything agrees
holdBUTTONS_HOLD_TMO_SIG600 ms, one-shotA confirmed press of TOP or BOT. Disarmed by the release or the start of a guided run
test_tmoBUTTONS_TEST_TMO_SIG20 s for a press, 3 s for a release, one-shotEach prompt, and again for the release

The calls

CallWhat 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

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.

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