# AO_Touch

Owns the touch controller of a Kode Dot: how a finger becomes a TOUCH_EVT_SIG, the polling rules, the two-target test and the rest of the controller.

## What it is

**AO_Touch** turns a finger on the glass into events. It publishes each press, each release and each move of more than 2 px as `TOUCH_EVT_SIG`, with the position pinned to the glass. AO_Gui in kodeOS and the render pass of `sdk/kscreen` in an app both feed on it.

It publishes changes only, so a finger resting on the screen costs nobody any work. The position is the controller's own, clamped to the 410 by 502 glass: nothing is calibrated, and no setting is stored. A guided test of two taps proves that the panel answers where a finger lands.

The console reaches every register of the chip through AO_Touch (`tp`). The chip, its registers and its habit of falling asleep are on `sdk/kdrv/cst820`.

## How it is connected

```mermaid
flowchart LR
  chip["CST820<br/>INT line"] -- "falls" --> tca["TCA6408A<br/>pin P6"]
  tca --> ioexp["AO_IoExp"]
  ioexp -- "IOEXP_INPUT_SIG" --> ao["AO_Touch"]
  ao -- "kdrv_cst820_read()<br/>I2C 0x15" --> chip
  ao -- "TOUCH_EVT_SIG" --> gui["AO_Gui<br/>or kscreen"]
  disp["AO_Display"] -- "DISPLAY_STATE_SIG" --> ao
  class chip,tca chip
  class ao,ioexp,disp ao
```

*From a finger to TOUCH_EVT_SIG*

### Priority and the interrupt line

AO_Touch runs at priority 14 (`KODE_PRIO_TOUCH`), with a 16-event queue and a 4096-byte stack. The polls are short and need low jitter, so the interface follows the finger. It claims `KBSP_RES_TOUCH`, and `kode_init()` starts it right before AO_Display.

The controller's interrupt line does not reach a pin of the P4. It goes to P6 of the TCA6408A expander, which AO_IoExp owns. AO_IoExp reads the expander on its own interrupt and publishes all eight pins as `IOEXP_INPUT_SIG`. AO_Touch subscribes to that signal and reads nothing from the controller until P6 falls.

### States

```mermaid
stateDiagram-v2
  [*] --> Touch_idle: claim, subscribe
  Touch_idle --> Touch_armed: DISPLAY_STATE_SIG up, CST820 answers
  Touch_armed --> Touch_tracking: IOEXP_INPUT_SIG, P6 changed and low
  Touch_tracking --> Touch_tracking: TOUCH_POLL_SIG, sample and publish on change
  Touch_tracking --> Touch_armed: release published, or 3 empty polls
```

*AO_Touch states*

### Idle

The touch controller and the panel share one reset line on GPIO7. Starting the panel resets the controller, so AO_Touch waits for `DISPLAY_STATE_SIG`. If the panel is not up it logs `no panel; touch shares its reset line, so no touch` and stays idle. A unit with the other supplier's panel does this. If the panel is up but `kdrv_cst820_init()` fails, it logs `no CST820` and stays idle. In idle, a field request is answered with `KODE_DENIED` so the console does not wait for its watchdog, and a test request fails with no prompt.

AO_Touch does not call `display_state_request()` in its initial transition. `kode_init()` starts it before AO_Display, so the announcement is still ahead, and a request would post to an AO with no queue.

### Armed and tracking

`Touch_up` is the parent of both and serves the field and test requests. In armed, AO_Touch reads nothing. On the falling edge it enters tracking, which arms a periodic 10 ms timer (`KODE_TOUCH_POLL_MS`) and leaves it disarmed on exit. Each `TOUCH_POLL_SIG` reads the chip and does one of three things:

- **Finger down.** The controller's point is clamped to the glass. AO_Touch publishes it when the finger lands, or when it is more than 2 px (`TOUCH_MOVE_EPSILON`) from the last **published** position on either axis. Measured against the last published point, a slow drag adds up to a publish. Measured against the last sample, every step would stay under the threshold and the last event a subscriber saw would be the press.
- **Finger lifted after a press.** AO_Touch publishes a release at the last position and returns to armed. A read the chip did not answer counts as a release, so a dead controller never leaves the GUI pressed.
- **Nothing, and no press since the edge.** After `TOUCH_EMPTY_POLLS` (3) such polls it returns to armed, so a glitch cannot start a poll that never ends.

Position and pressed flag move only with an event that went out. If the event pool is dry, the next poll says the same thing again, so a release is never lost.

The polling exists because CST816-family firmwares differ: some pulse INT once per report, others once per touch. A board lying on a desk uses no I2C at all.

### No calibration

A published point is the controller's own, pinned to the glass: a finger on the bezel can read past the last pixel of the 410 by 502 panel, so a coordinate below 0 becomes 0 and one past the edge becomes the last pixel. Nothing else is applied. Rotation and mirroring are design facts of the panel and are not stored. A `kset/tcal` blob that an older kodeOS left in the store is never read.

### The guided test

`test component=tp` needs a person. It checks the chip's identity from the copy the driver took at start-up, then shows two targets, one on each side of the glass in the clear band between the printed zones of the screen protector:

- The first at (80, 215).
- The second at (335, 285).

Each prompt is a published `TOUCH_TEST_SIG`. The tap is the first release after the prompt, and AO_Touch judges the average of every raw sample while the finger was down, within 64 px (`KODE_TOUCH_TEST_TOL_PX`) of the target on each axis. A step has 20 s (`KODE_TOUCH_TEST_STEP_MS`).

A tap off the target is a slip and is forgiven: `TP_TEST_MISS` names where it landed and the same target is asked again. The third tap off one target (`KODE_TOUCH_TEST_TRIES`) fails the step with `TP_TEST_FAIL`, and the run goes on to the next target. When both targets have been judged the verdict is published. Nothing is fitted or stored: the test proves the panel answers, and it does not calibrate it.

**The origin of a run.** `touch_test_request_from(origin)` starts a run for a client other than AO_Autotest. Every `TOUCH_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 a run they asked for. A request that arrives while a run is up is refused with a failing verdict in its own origin, and the run in flight keeps its origin.

### Resting with the board

With `CONFIG_KODE_LOWPOWER`, AO_Touch joins the rounds of AO_LowPower as `touch` (`sdk/kaos/lowpower`). Right after the bring-up, the one moment the chip is known to answer, it reads the three settings the rest changes: the interrupt control, the auto-sleep switch and the auto-sleep time. If any of them does not read, the rest leaves the chip alone.

On a round, AO_Touch switches the interrupt off and auto-sleep on, with a time of `KODE_TOUCH_REST_SLEEP_S` (1 s), so the chip stops scanning a second after the last touch. The writes may be refused by a chip that is already asleep, which is the rest itself, so their results are not judged. Once asleep, the chip NAKs every access until a finger lands or the panel is reset. The reset line is the panel's too, so there is no reset.

On `LOWPOWER_STATE_SIG` with state AWAKE, AO_Touch writes the awake settings back. A chip that NAKs the write is tried again every `KODE_TOUCH_RESTORE_RETRY_MS` (200 ms), on `TOUCH_RESTORE_SIG`, until it answers, which it does as soon as a finger lands. So the first touch after a wake can be lost and the next one is not. The guided test makes AO_Touch answer a round busy (`touch test`). A touch does not wake the board from ASLEEP or OFF.

## Specifications

### Signals

| Signal | Event | Direction | Meaning |
| --- | --- | --- | --- |
| `DISPLAY_STATE_SIG` | `DisplayStateEvt` | in, subscribed | Gate out of idle |
| `IOEXP_INPUT_SIG` | `IoexpInputEvt`: `levels`, `changed`, `ok` | in, subscribed | P6 low with its bit in `changed` is the edge. Every other one is ignored |
| `TOUCH_FIELD_REQ_SIG` | `TouchFieldReqEvt`: `who`, `op`, `field`, `value` | in, posted by AO_Console | Get, set or dump a CST820 field. After a set AO_Touch reads back |
| `TOUCH_TEST_REQ_SIG`, `TOUCH_TEST_REQ_FROM_SIG` | none, or `TestReqEvt`: `origin` | in, posted by AO_Autotest or another client | Start the guided test. A second request during a run is refused with a failing verdict in the asker's origin |
| `TOUCH_POLL_SIG` | none | self, periodic | Only while tracking |
| `TOUCH_TEST_TMO_SIG` | none | self, one-shot | One per prompt |
| `TOUCH_EVT_SIG` | `TouchEvt`: `x`, `y`, `pressed`, `t_ms` | out, published | A sample, pinned to the glass. `t_ms` is the read time, so a subscriber can measure a drag |
| `TOUCH_TEST_SIG` | `TouchTestEvt`: `phase`, `step`, `total`, `tx`, `ty`, `gx`, `gy`, `origin` | out, published | `TP_TEST_PROMPT`, `_PASS`, `_MISS`, `_FAIL`, `_TIMEOUT`, `_DONE`. `gx`, `gy` are where the tap read |
| `TOUCH_FIELD_RSP_SIG` | `TouchFieldEvt`: `op`, `field`, `rc`, `value` | out, posted to `who` | `KODE_DENIED` for a read-only field, a value out of range or no controller. `KODE_ERROR` when the chip did not answer, which on this part usually means asleep |
| `TEST_RESULT_SIG` | named `tp`, with the `origin` of the run | out, published | The verdict |
| `LOWPOWER_ROUND_SIG`, `LOWPOWER_STATE_SIG` | `LowPowerRoundEvt`, `LowPowerStateEvt` | in, subscribed | With `CONFIG_KODE_LOWPOWER`: rest the controller, and put it back on state AWAKE |
| `TOUCH_RESTORE_SIG` | none | self, one-shot | With `CONFIG_KODE_LOWPOWER`: the retry of a write-back the sleeping chip refused |

### Calls

| Call | What it does |
| --- | --- |
| `touch_field_request(who, op, field, value)` | `op` is `FIELD_OP_GET`, `FIELD_OP_SET` or `FIELD_OP_DUMP`; `field` is a `kdrv_cst820_field_t`. A `NULL` `who` is served and not answered |
| `touch_test_request()`, `touch_test_request_from(origin)` | Start the guided test, for AO_Autotest or for the client with that origin |

### Numbers, verbs and settings

| Thing | Value |
| --- | --- |
| Poll period | 10 ms (`KODE_TOUCH_POLL_MS`); the QP tick is 1 ms |
| Move hysteresis | 2 px per axis (`TOUCH_MOVE_EPSILON`) |
| Empty polls before giving up an edge | 3 (`TOUCH_EMPTY_POLLS`) |
| Test | 2 targets (`KODE_TOUCH_TEST_STEPS`), 20 s a tap (`KODE_TOUCH_TEST_STEP_MS`), 3 tries a target (`KODE_TOUCH_TEST_TRIES`); console budget 125 s (`KODE_TOUCH_TEST_BUDGET_MS`) |
| Test tolerance | 64 px on each raw axis (`KODE_TOUCH_TEST_TOL_PX`) |
| Rest | Auto-sleep after 1 s (`KODE_TOUCH_REST_SLEEP_S`); the write-back retries every 200 ms (`KODE_TOUCH_RESTORE_RETRY_MS`) |
| Console | `tp`, `tp field="fingers"`, `test component=tp`. `touch`, `tap` and `swipe` inject fingers but never pass through AO_Touch, so they cannot pass the test |
| Setting | None. A `kset/tcal` blob left by an older kodeOS is ignored |
| Kconfig | None for the touch itself, which is always built. The rest needs `CONFIG_KODE_LOWPOWER` |

## Gotchas

> **WARNING**
>
> **There is no touch when the panel is down.** The reset line is shared, so a controller started before the panel is reset by it. A board whose panel failed, and a unit with the other supplier's panel, have console input only.

> **NOTE**
>
> **A published release is not guaranteed on the next poll.** If the small event pool is dry, AO_Touch stays in tracking and publishes it on the next poll. A subscriber must not assume a release arrives exactly once per 10 ms.

> **NOTE**
>
> **The test judges where the controller read the tap.** A sensor a little off the glass passes while the error stays within 64 px, 5 mm on this glass. A mirrored axis puts a tap at least about 70 px off, and a broken mapping hundreds, so both fail. The two targets sit mid-glass for that reason.

## Example code

This is an Active Object of your own that subscribes to the touch events and logs them. Touch the glass and the console prints one line per press, move and release. The log level must be `i` or lower: `config key="log" value=i`.

```c
/*
 * touch_log.c - 2026-10-28 - from Kode with love
 * Logs every TOUCH_EVT_SIG of the Kode Dot. Touch the glass and the console
 * prints the position of each press, move and release.
 */
#include "kode_ao.h"
static QActive     l_ao;
static QEvtPtr     l_queue[KQP_AO_QUEUE_LEN];
static StackType_t l_stack[KQP_AO_STACK_BYTES / sizeof(StackType_t)];

/* The one state: log each touch sample, pass the rest to the top state. */
static QState TouchLog_running(QActive * const me, QEvt const * const e)
{
    QState status = Q_SUPER(&QHsm_top);
    (void)me;
    if (e->sig == TOUCH_EVT_SIG) {
        TouchEvt const * const t = (TouchEvt const *)e;
        KLOG_I("touch_log", "x=%d y=%d %s", (int)t->x, (int)t->y,
               t->pressed ? "down" : "up");
        status = Q_HANDLED();
    }
    return status;
}

/* Subscribe to the touch events, then run. */
static QState TouchLog_initial(QActive * const me, void const * const par)
{
    (void)par;
    QActive_subscribe(me, TOUCH_EVT_SIG);
    return Q_TRAN(&TouchLog_running);
}

/* Bring the system up, then start the Active Object. */
void app_main(void)
{
    kode_init();
    QActive_ctor(&l_ao, Q_STATE_CAST(&TouchLog_initial));
    QACTIVE_START(&l_ao, KODE_PRIO_USER_LOW, l_queue, Q_DIM(l_queue),
                  l_stack, sizeof(l_stack), (void *)0);
}
```

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/touch`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/touch): `touch_ao.c`, the state machine, the polling, the guided test and the rest.
- [`kaos.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kaos/core/include/kaos.h): `touch_field_request()`, `touch_test_request()` and `touch_test_request_from()`.
- [`components/kdrv/cst820`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/cst820): the driver; see `sdk/kdrv/cst820`.
