demo-game/API_WEBSOCKET.md
simon 84827c9782 Add main menu, WebSocket API demo, and multi-scene game hub.
Replace the single-entry Pong flow with a menu launcher, a per-client API demo on :8081/ws with stream controls and tap flash feedback, and bump the viewport to 1920×1080.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 21:43:44 +02:00

349 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WebSocket API
`go run . -port /dev/ttyUSB0 serve` exposes two WebSocket endpoints. They share the same UART link but serve different purposes.
| URL | Port (default) | Role |
|-----|----------------|------|
| `ws://localhost:8080/ws` | Dashboard (`-addr`) | Server → client only: full `DashboardState` JSON (~2 s poll + live-stream accel/tap) |
| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **accel** / **tap** push streams |
Disable the external server with `-api-addr ""`.
CLI overview and UART commands: [`../README.md`](../README.md). HTTP endpoints: [`API_REST.md`](API_REST.md).
---
## 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` / `set_tap_stream` with `enable: true`, the server may send **`accel`** and/or **`tap`** 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 (accel and tap)
| Layer | Commands | Effect |
|-------|----------|--------|
| **Firmware (ESP-NOW)** | `set_accel_stream`, `set_tap_notify` | Per `client_id`: slave sends accel or tap kinds to the master |
| **This connection (host)** | `set_stream`, `set_tap_stream` | Whether **you** receive push JSON and at what rate (`interval_ms`, 1 ms … 10 s) |
- **Accel UART polling** runs only if at least one connection has `receive_accel: true` **and** at least one slave streams accel (`set_accel_stream` or dashboard).
- **Tap UART polling** runs only if at least one connection has `receive_tap: true` (`set_tap_stream`). `set_tap_notify` alone does **not** poll.
Typical sequence:
1. `list_clients` → slave IDs
2. Per slave: `set_accel_stream` / `set_tap_notify` as needed
3. `set_stream` and/or `set_tap_stream` with `"enable": true`
4. Read push messages in a loop
There is **no per-slave filter** on push messages: each `accel` contains all cached slaves; each `tap` contains all visible events. Filter by `client_id` in your app.
---
## Push stream messages
These are the samples you get after enabling receive. Interval is per WebSocket connection; the server UART poll uses the **minimum** `interval_ms` among all subscribers that want accel or tap.
### `accel` (type `"accel"`)
Sent only when `set_stream` has `enable: true`, a slave streams accel, and the poll tick fires for this connection.
**Success** — all slaves with a cache entry on the master (not only those with `valid: true`):
```json
{
"type": "accel",
"t": 1716900123456789012,
"success": true,
"clients": [
{
"client_id": 16,
"valid": true,
"x": 12,
"y": -34,
"z": 16384,
"age_ms": 8
},
{
"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 in the master cache |
| `client_id` | ESP-NOW client id (same as `list_clients`) |
| `valid` | `false` if no sample yet or stale; omit `x`/`y`/`z` when false |
| `x`, `y`, `z` | Raw accelerometer LSB (BMA456, ±2 g scale on the pod) |
| `age_ms` | Milliseconds since the master received this sample |
**Failure** (e.g. UART busy):
```json
{
"type": "accel",
"t": 1716900123456789012,
"success": false,
"error": "uart busy"
}
```
No `clients` array on failure.
### `tap` (type `"tap"`)
Sent only when `set_tap_stream` has `enable: true` and there is at least one event to show.
Events appear when the master cache reports a new tap. Each event stays in push payloads for **`tap_display_min_ms`** (2000 ms, also in `hello`) after the API first saw it, even if the hardware age grows.
**Success**:
```json
{
"type": "tap",
"t": 1716900123456789012,
"success": true,
"events": [
{
"client_id": 16,
"valid": true,
"kind": "single",
"age_ms": 3,
"shown_at_ms": 1717000000123
}
]
}
```
| Field | Meaning |
|-------|---------|
| `t` | Unix timestamp in **nanoseconds** (poll time) |
| `events[]` | All taps currently “on screen” for the API |
| `client_id` | Slave that tapped |
| `kind` | `"single"`, `"double"`, or `"triple"` |
| `age_ms` | Age in the master cache when read |
| `shown_at_ms` | Unix **milliseconds** when this host first included the event |
If no events are visible, **no** `tap` message is sent on that tick (unlike accel, which can send empty `clients` only on success with cache data).
**Failure**:
```json
{
"type": "tap",
"t": 1716900123456789012,
"success": false,
"error": "uart busy"
}
```
---
## Commands (request → response)
Send one JSON object per message. Field `type` selects the command.
### `hello` (server → client, on connect)
```json
{
"type": "hello",
"serial_port": "/dev/ttyUSB0",
"interval_ms": 16,
"tap_display_min_ms": 2000,
"note": "set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push",
"commands": [
"list_clients",
"set_stream", "get_stream",
"set_accel_stream", "get_accel_stream",
"set_tap_stream", "get_tap_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",
"version": 1,
"available": true,
"used": true,
"last_ping": 1234,
"last_success_ping": 1200,
"accel_stream": false,
"tap_notify_single": false,
"tap_notify_double": false,
"tap_notify_triple": false
}
]
}
```
### `set_stream` / `get_stream` (receive accel on this connection)
```json
{"type":"set_stream","enable":true,"interval_ms":32}
{"type":"get_stream"}
```
Response `stream_status`:
```json
{"type":"stream_status","receive_accel":true,"interval_ms":32,"success":true}
```
### `set_accel_stream` / `get_accel_stream` (firmware, per slave)
`client_id` required (> 0).
```json
{"type":"set_accel_stream","client_id":16,"enable":true}
{"type":"get_accel_stream","client_id":16}
```
Response `accel_stream_status`:
```json
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true}
```
### `set_tap_stream` / `get_tap_stream` (receive tap on this connection)
```json
{"type":"set_tap_stream","enable":true,"interval_ms":16}
{"type":"get_tap_stream"}
```
Response `tap_stream_status`:
```json
{"type":"tap_stream_status","receive_tap":true,"interval_ms":16,"success":true}
```
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
Per client: `single`, `double_tap`, `triple` required on set.
```json
{"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false}
```
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
}
```
### `set_led_ring`
Same JSON body as [`POST /api/led-ring`](API_REST.md#led-ring) with `"type":"set_led_ring"` added. Reply: `led_ring_status`.
### `get_battery`
Body: `{"type":"get_battery","all_clients":true}` or `"client_id":16`. Default if omitted: all clients.
Reply: `battery_status` with `samples[]` (see REST doc).
---
## Examples
### Accel stream
```python
import asyncio, json, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws:
print(await ws.recv()) # hello
await ws.send(json.dumps({"type": "list_clients"}))
clients = json.loads(await ws.recv())["clients"]
for c in clients:
if not c.get("available"):
continue
await ws.send(json.dumps({
"type": "set_accel_stream", "client_id": c["id"], "enable": True
}))
await ws.recv() # accel_stream_status
await ws.send(json.dumps({"type": "set_stream", "enable": True, "interval_ms": 16}))
await ws.recv() # stream_status
while True:
msg = json.loads(await ws.recv())
if msg.get("type") != "accel":
continue
if not msg.get("success"):
print("error:", msg.get("error"))
continue
for c in msg.get("clients", []):
if c.get("valid"):
print(c["client_id"], c["x"], c["y"], c["z"], "age", c.get("age_ms"))
asyncio.run(main())
```
### Tap stream
```python
import asyncio, json, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws:
print(await ws.recv()) # hello
await ws.send(json.dumps({
"type": "set_tap_notify", "client_id": 16,
"single": True, "double_tap": False, "triple": False
}))
await ws.recv() # tap_notify_status
await ws.send(json.dumps({"type": "set_tap_stream", "enable": True, "interval_ms": 16}))
await ws.recv() # tap_stream_status
while True:
msg = json.loads(await ws.recv())
if msg.get("type") == "tap" and msg.get("events"):
for e in msg["events"]:
print(e["client_id"], e["kind"], "age", e.get("age_ms"))
asyncio.run(main())
```
---
## Dashboard WebSocket (`:8080/ws`)
Read-only from the browsers perspective: the server pushes JSON whenever state changes. Clients do not send commands on this socket (messages are ignored).
Payload shape: `DashboardState``updated_at`, `serial_port`, `uart_connected`, `live_stream`, `master`, `clients[]` (id, mac, accel, tap notify flags, battery, etc.). Accel/tap samples appear here when **Live stream** is enabled in the UI (`PUT /api/live-stream`).
During OTA, additional messages with `"type":"ota_progress"` may appear on the same socket.
Configure slaves via REST on `:8080` ([`API_REST.md`](API_REST.md)), not via this WebSocket.