Update InputClientSample struct to make TapKind optional and improve WebSocket API documentation. Adjust README to reflect changes in input push stream format and clarify connection flow. Enhance API_WEBSOCKET.md with detailed command and push message descriptions.

This commit is contained in:
simon 2026-06-06 12:26:31 +02:00
parent ab1844ac32
commit 99956e3362
3 changed files with 63 additions and 95 deletions

View File

@ -86,7 +86,7 @@ If the UART device is unplugged or the port disappears, `serve` keeps running an
| Doc | Content |
|-----|---------|
| **[docs/API_WEBSOCKET.md](docs/API_WEBSOCKET.md)** | `ws://…:8081/ws` commands, **`accel` / `tap` push stream** format, dashboard `ws://…:8080/ws` |
| **[docs/API_WEBSOCKET.md](docs/API_WEBSOCKET.md)** | `ws://…:8081/ws` commands and **`input` push stream** (accel + tap) |
| **[docs/API_REST.md](docs/API_REST.md)** | REST on `:8080` (dashboard) and `:8081` (battery, LED, service info) |
CLI:

View File

@ -31,7 +31,7 @@ type InputClientSample struct {
Y int32 `json:"y,omitempty"`
Z int32 `json:"z,omitempty"`
AccelAgeMs uint32 `json:"accel_age_ms,omitempty"`
TapKind string `json:"tap_kind"`
TapKind string `json:"tap_kind,omitempty"`
TapAgeMs uint32 `json:"tap_age_ms,omitempty"`
}
@ -389,7 +389,6 @@ func (h *accelStreamHub) inputClientsFromCacheLocked(cache *pb.CacheStatusRespon
for _, c := range cache.GetClients() {
sample := InputClientSample{
ClientID: c.GetClientId(),
TapKind: "none",
}
if a := c.GetAccel(); a != nil {
sample.Valid = a.GetValid()

View File

@ -1,58 +1,54 @@
# WebSocket API
`go run . -port /dev/ttyUSB0 serve` exposes the WebSocket enpoint
External API: `ws://localhost:8081/ws` (default `-api-addr`, disable with empty string).
| URL | Port (default) | Role |
|-----|----------------|------|
| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **input** push stream |
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.
---
## 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)
## 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`) |
| 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 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`.
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` as needed
2. Per slave: `set_input_stream` and/or `set_tap_notify`
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.
4. Read `input` messages; filter by `client_id` in your app (no per-slave filter on the wire)
---
## Push stream messages
## Push: `input`
These are the samples you get after enabling receive. Timing is per WebSocket connection:
Combines latest accel cache and visible tap state for every slave slot on the master.
- **`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`):
**Success:**
```json
{
@ -72,8 +68,7 @@ Sent when `set_stream` has `enable: true` and the poll tick fires for this conne
},
{
"client_id": 42,
"valid": false,
"tap_kind": "none"
"valid": false
}
]
}
@ -81,38 +76,33 @@ Sent when `set_stream` has `enable: true` and the poll tick fires for this conne
| Field | Meaning |
|-------|---------|
| `t` | Unix timestamp in **nanoseconds** when the host read the cache |
| `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`) |
| `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 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"` |
| `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 ms, also in `hello`) after the API first saw them, even if the hardware age grows.
Tap events stay visible for `tap_display_min_ms` (2000, in `hello`) after the API first saw them.
**Failure** (e.g. UART busy):
**Failure** (no `clients` array):
```json
{
"type": "input",
"t": 1716900123456789012,
"success": false,
"error": "uart busy"
}
{"type":"input","t":1716900123456789012,"success":false,"error":"uart busy"}
```
No `clients` array on failure.
---
## Commands (request → response)
## Commands
Send one JSON object per message. Field `type` selects the command.
One JSON object per message; field `type` selects the command.
### `hello` (server → client, on connect)
**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
{
@ -121,7 +111,6 @@ Send one JSON object per message. Field `type` selects the command.
"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",
@ -146,11 +135,7 @@ Response `client_list`:
{
"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,
@ -160,28 +145,24 @@ Response `client_list`:
}
```
### `set_stream` / `get_stream` (receive input on this connection)
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"}
```
| 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)
### `set_input_stream` / `get_input_stream` (firmware)
`client_id` required (> 0). Enables accel streaming from the slave to the master.
`client_id` required (> 0).
```json
{"type":"set_input_stream","client_id":16,"enable":true}
@ -194,41 +175,31 @@ Response `input_stream_status`:
{"type":"input_stream_status","client_id":16,"enabled":true,"success":true}
```
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
### `set_tap_notify` / `get_tap_notify` (firmware)
Per client: `single`, `double_tap`, `triple` required on set.
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}
```
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
}
{"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 |
|--------|--------|
| Request `mode` | Notes |
|----------------|--------|
| `clear` | Turn off |
| `color` | Full ring RGB + `intensity` |
| `progress` | `progress` 0100 |
@ -236,9 +207,9 @@ Control the LED ring on the master or a slave.
| `blink` | `blink_ms`, `blink_count` |
| `find-me` | Locate pod |
Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast.
Target: `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`).
Response `led_ring_status`:
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}
@ -246,15 +217,13 @@ Response `led_ring_status`:
### `get_battery`
Read cached battery samples from the master. Slaves push battery every **30 s**; this command reads the master cache.
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}
```
Default if omitted: all clients.
Response `battery_status`:
```json