From 99956e33622ac79d8be8b791989002256f7362bd Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 6 Jun 2026 12:26:31 +0200 Subject: [PATCH] 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. --- goTool/README.md | 2 +- goTool/api_stream.go | 3 +- goTool/docs/API_WEBSOCKET.md | 153 ++++++++++++++--------------------- 3 files changed, 63 insertions(+), 95 deletions(-) diff --git a/goTool/README.md b/goTool/README.md index 44399f0..c0aa137 100644 --- a/goTool/README.md +++ b/goTool/README.md @@ -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: diff --git a/goTool/api_stream.go b/goTool/api_stream.go index 0a17bb3..4792304 100644 --- a/goTool/api_stream.go +++ b/goTool/api_stream.go @@ -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() diff --git a/goTool/docs/API_WEBSOCKET.md b/goTool/docs/API_WEBSOCKET.md index c263071..f5b4f1e 100644 --- a/goTool/docs/API_WEBSOCKET.md +++ b/goTool/docs/API_WEBSOCKET.md @@ -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` 0–100 | @@ -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