253 lines
7.4 KiB
Markdown
253 lines
7.4 KiB
Markdown
# 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
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|