# WebSocket API External API: `ws://localhost:8081/ws` (default `-api-addr`, disable with empty string). Start with `go run . serve` (optional `-port` / `-ble` to pre-connect). Host can be opened later via `host_connect` / dashboard REST. --- ## Connection flow 1. Connect → server sends `hello` (host status, push off; defaults and command list). 2. Optionally `ble_scan` / `list_serial_ports` then `host_connect` if not already linked. 3. Send JSON commands → one reply per message (`*_status`, `client_list`, `ble_scan_result`, …). 4. 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 | Host polling / BLE ticks run 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`. On **BLE**, live `input` prefers firmware `BLE_STREAM_TICK` pushes; otherwise falls back to `CACHE_STATUS` like UART. ### 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 host 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", "transport": "uart", "endpoint": "/dev/ttyUSB0", "connected": true, "interval_ms": 16, "pre_fetch_ms": 2, "tap_display_min_ms": 2000, "commands": [ "get_host", "list_serial_ports", "ble_scan", "host_connect", "host_disconnect", "list_clients", "set_stream", "get_stream", "set_input_stream", "get_input_stream", "set_tap_notify", "get_tap_notify", "set_led_ring", "get_battery" ] } ``` ### Host link | Client `type` | Reply `type` | Notes | |---------------|--------------|--------| | `get_host` | `host_status` | `transport`, `endpoint`, `connected` | | `list_serial_ports` | `serial_ports` | `{ports:[…]}` | | `ble_scan` | `ble_scan_result` | optional `timeout_s`, `name_filter`; disconnects active BLE first | | `host_connect` | `host_status` | `transport:"uart","port"` or `transport:"ble","address","name?"` | | `host_disconnect` | `host_status` | | ```json {"type":"host_connect","transport":"ble","address":"AA:BB:CC:DD:EE:FF","name":"Powerpod"} {"type":"host_status","transport":"ble","endpoint":"AA:BB:…","connected":true,"success":true} ``` If the host is not connected, other commands that need the master return their usual error in the status reply. ### `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 | | `battery-low` | UV indicator (4 LEDs, 5 s) | 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, 6=battery-low. ```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 } ] } ```