# WebSocket API External API: `ws://localhost:8081/ws` (default `-api-addr`, disable with empty string). Start with `go run . -port /dev/ttyUSB0 serve`. --- ## Connection flow 1. Connect → server sends `hello` (push off; defaults and command list). 2. Send JSON commands → one reply per message (`*_status` or `client_list`). 3. After `set_stream` with `enable: true`, server may push `input` messages without a prior command. Commands and pushes share one socket — always branch on `type`. On disconnect, `set_stream` state for that socket is dropped. Firmware settings (`set_input_stream`, `set_tap_notify`) stay on the master until changed. --- ## Two layers (firmware vs host) | Layer | Commands | Effect | |-------|----------|--------| | Firmware (ESP-NOW) | `set_input_stream`, `set_tap_notify` | Per `client_id`: slave sends accel and/or tap events to the master | | This connection | `set_stream` | Whether you receive push JSON on this socket | UART polling runs only when at least one connection has `receive_input: true` **and** at least one slave streams input or has tap notify enabled. `set_tap_notify` alone does not enable push — you still need `set_stream`. ### Push timing (per connection) | Field | Where | Meaning | |-------|-------|---------| | `interval_ms` | `hello`, `set_stream`, `stream_status` | Minimum ms between `input` pushes on this socket (1 … 10000) | | `pre_fetch` | `set_stream`, `stream_status` | Ms before each push when the host starts the UART cache read | Global UART poll interval = minimum `interval_ms` among all connections with push enabled. Typical sequence: 1. `list_clients` → slave IDs 2. Per slave: `set_input_stream` and/or `set_tap_notify` 3. `set_stream` with `"enable": true` 4. Read `input` messages; filter by `client_id` in your app (no per-slave filter on the wire) --- ## Push: `input` Combines latest accel cache and visible tap state for every slave slot on the master. **Success:** ```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 } ] } ``` | 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 (includes invalid/stale entries) | | `client_id` | Same id as in `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) | | `accel_age_ms` | Ms since the master received this accel sample | | `tap_kind` | `"single"`, `"double"`, or `"triple"`; omit when no recent tap | | `tap_age_ms` | Ms since tap in master cache; omit with `tap_kind` | Tap events stay visible for `tap_display_min_ms` (2000, in `hello`) after the API first saw them. **Failure** (no `clients` array): ```json {"type":"input","t":1716900123456789012,"success":false,"error":"uart busy"} ``` --- ## Commands One JSON object per message; field `type` selects the command. **Errors:** Replies use the matching response `type`. On failure: `success: false` (or omitted) and `"error": "…"`. Malformed JSON or unknown `type` → `stream_status` with `error`. ### `hello` (server → client) ```json { "type": "hello", "serial_port": "/dev/ttyUSB0", "interval_ms": 16, "pre_fetch_ms": 2, "tap_display_min_ms": 2000, "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", "available": true, "input_stream": false, "tap_notify_single": false, "tap_notify_double": false, "tap_notify_triple": false } ] } ``` Also per client: `version`, `used`, `last_ping`, `last_success_ping`. ### `set_stream` / `get_stream` ```json {"type":"set_stream","enable":true,"interval_ms":32,"pre_fetch":2} {"type":"get_stream"} ``` 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) `client_id` required (> 0). ```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) Set requires `single`, `double_tap`, `triple` per client, or `"all_clients": true` for broadcast. ```json {"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false} {"type":"get_tap_notify","client_id":16} ``` Response `tap_notify_status`: ```json {"type":"tap_notify_status","client_id":16,"success":true,"single":true,"double_tap":false,"triple":false} ``` ### `set_led_ring` ```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} ``` | Request `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 | Target: `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`). Response `led_ring_status` — `mode` is numeric: 0=clear, 1=progress, 2=digit, 3=blink, 4=find-me, 5=color. ```json {"type":"led_ring_status","success":true,"mode":5,"client_id":16,"slaves_updated":1} ``` ### `get_battery` Slaves push battery every 30 s; this reads the master cache. Default: all clients. ```json {"type":"get_battery","all_clients":true} {"type":"get_battery","client_id":16} ``` 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 } ] } ```