# WebSocket API

`go run . -port /dev/ttyUSB0 serve` exposes the WebSocket enpoint

| URL | Port (default) | Role |
|-----|----------------|------|
| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **input** push stream |

---

## External API (`:8081/ws`)

### Connection flow

1. Connect → server sends **`hello`** (receive off; lists available commands).
2. Send JSON commands → server replies with a matching `*_status` or `client_list` message (one reply per command).
3. After `set_stream` with `enable: true`, the server may send **`input`** messages **without** a prior command (push stream).

Commands and stream pushes are multiplexed on one socket. While streaming, always parse `type` and branch (status vs sample vs error).

### Two layers (firmware vs host)

| Layer | Commands | Effect |
|-------|----------|--------|
| **Firmware (ESP-NOW)** | `set_input_stream`, `set_tap_notify` | Per `client_id`: slave sends accel samples and/or tap events to the master |
| **This connection (host)** | `set_stream` | Whether **you** receive push JSON, at what rate (`interval_ms`, 1 ms … 10 s), and how early the UART read starts (`pre_fetch`) |

- **UART polling** runs only if at least one connection has `receive_input: true` (`set_stream`) **and** at least one slave streams input (`set_input_stream`) or has tap notify enabled (`set_tap_notify`).
- **`set_tap_notify` alone** configures which tap kinds the slave reports; it does **not** enable host push by itself — you still need `set_stream`.

Typical sequence:

1. `list_clients` → slave IDs
2. Per slave: `set_input_stream` and/or `set_tap_notify` as needed
3. `set_stream` with `"enable": true`
4. Read **`input`** messages in a loop

There is **no per-slave filter** on push messages: each `input` contains all cached slaves. Filter by `client_id` in your app.

---

## Push stream messages

These are the samples you get after enabling receive. Timing is per WebSocket connection:

- **`interval_ms`** — minimum time between consecutive `input` pushes on this socket.
- **`pre_fetch`** — milliseconds **before** each scheduled push when the host sends the UART cache read, so the master has time to collect data from all slaves before the JSON goes out.

The server UART poll uses the **minimum** `interval_ms` among all subscribers with `receive_input: true`.

### `input` (type `"input"`)

Sent when `set_stream` has `enable: true` and the poll tick fires for this connection (after the UART read started `pre_fetch` ms earlier). Each message combines the latest accel cache and visible tap state for every slave slot on the master.

**Success** — all slaves with a cache entry (not only those with `valid: true`):

```json
{
  "type": "input",
  "t": 1716900123456789012,
  "success": true,
  "clients": [
    {
      "client_id": 16,
      "valid": true,
      "x": 12,
      "y": -34,
      "z": 16384,
      "accel_age_ms": 8,
      "tap_kind": "single",
      "tap_age_ms": 3
    },
    {
      "client_id": 42,
      "valid": false,
      "tap_kind": "none"
    }
  ]
}
```

| Field | Meaning |
|-------|---------|
| `t` | Unix timestamp in **nanoseconds** when the host read the cache |
| `success` | `true` if `CACHE_STATUS` succeeded |
| `clients[]` | One entry per slave slot in the master cache |
| `client_id` | ESP-NOW client id (same as `list_clients`) |
| `valid` | `false` if no accel sample yet or stale; omit `x`/`y`/`z` when false |
| `x`, `y`, `z` | Raw accelerometer LSB (BMA456, ±2 g scale on the pod) |
| `accel_age_ms` | Milliseconds since the master received this accel sample |
| `tap_kind` | `"none"`, `"single"`, `"double"`, or `"triple"` |
| `tap_age_ms` | Milliseconds since the tap was seen in the master cache; omit when `tap_kind` is `"none"` |

Tap events stay visible for **`tap_display_min_ms`** (2000 ms, also in `hello`) after the API first saw them, even if the hardware age grows.

**Failure** (e.g. UART busy):

```json
{
  "type": "input",
  "t": 1716900123456789012,
  "success": false,
  "error": "uart busy"
}
```

No `clients` array on failure.

---

## Commands (request → response)

Send one JSON object per message. Field `type` selects the command.

### `hello` (server → client, on connect)

```json
{
  "type": "hello",
  "serial_port": "/dev/ttyUSB0",
  "interval_ms": 16,
  "pre_fetch_ms": 2,
  "tap_display_min_ms": 2000,
  "note": "set_tap_notify configures slave S/D/T only; set_stream enables input polling/push on this connection",
  "commands": [
    "list_clients",
    "set_stream", "get_stream",
    "set_input_stream", "get_input_stream",
    "set_tap_notify", "get_tap_notify",
    "set_led_ring", "get_battery"
  ]
}
```

### `list_clients`

Request: `{"type":"list_clients"}`

Response `client_list`:

```json
{
  "type": "client_list",
  "success": true,
  "clients": [
    {
      "id": 16,
      "mac": "aa:bb:cc:dd:ee:10",
      "version": 1,
      "available": true,
      "used": true,
      "last_ping": 1234,
      "last_success_ping": 1200,
      "input_stream": false,
      "tap_notify_single": false,
      "tap_notify_double": false,
      "tap_notify_triple": false
    }
  ]
}
```

### `set_stream` / `get_stream` (receive input on this connection)

```json
{"type":"set_stream","enable":true,"interval_ms":32,"pre_fetch":2}
{"type":"get_stream"}
```

| Field | Meaning |
|-------|---------|
| `enable` | Turn push stream on/off for this connection |
| `interval_ms` | Minimum time between `input` pushes (1 … 10000) |
| `pre_fetch` | Milliseconds before each push when the host starts the UART cache read; optional, default in `hello` (`pre_fetch_ms`) |

Response `stream_status`:

```json
{"type":"stream_status","receive_input":true,"interval_ms":32,"pre_fetch":2,"success":true}
```

### `set_input_stream` / `get_input_stream` (firmware, per slave)

`client_id` required (> 0). Enables accel streaming from the slave to the master.

```json
{"type":"set_input_stream","client_id":16,"enable":true}
{"type":"get_input_stream","client_id":16}
```

Response `input_stream_status`:

```json
{"type":"input_stream_status","client_id":16,"enabled":true,"success":true}
```

### `set_tap_notify` / `get_tap_notify` (firmware, per slave)

Per client: `single`, `double_tap`, `triple` required on set.

```json
{"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false}
```

Broadcast: `"all_clients": true` with the three booleans.

Response `tap_notify_status`:

```json
{
  "type": "tap_notify_status",
  "client_id": 16,
  "success": true,
  "single": true,
  "double_tap": false,
  "triple": false
}
```

### `set_led_ring`

Control the LED ring on the master or a slave.

```json
{"type":"set_led_ring","mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":128}
{"type":"set_led_ring","mode":"digit","client_id":0,"digit":3,"r":0,"g":255,"b":0}
{"type":"set_led_ring","mode":"find-me","all_clients":true,"slaves_only":true}
```

| `mode` | Notes |
|--------|--------|
| `clear` | Turn off |
| `color` | Full ring RGB + `intensity` |
| `progress` | `progress` 0–100 |
| `digit` | `digit` 0–10 |
| `blink` | `blink_ms`, `blink_count` |
| `find-me` | Locate pod |

Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast.

Response `led_ring_status`:

```json
{"type":"led_ring_status","success":true,"mode":5,"client_id":16,"slaves_updated":1}
```

### `get_battery`

Read cached battery samples from the master. Slaves push battery every **30 s**; this command reads the master cache.

```json
{"type":"get_battery","all_clients":true}
{"type":"get_battery","client_id":16}
```

Default if omitted: all clients.

Response `battery_status`:

```json
{
  "type": "battery_status",
  "success": true,
  "samples": [
    {
      "client_id": 16,
      "lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71},
      "lipo2": {"valid": false},
      "age_ms": 1200
    }
  ]
}
```
