demo-game/API_WEBSOCKET.md
simon 70e66f4d60 Adapt game clients to the unified WebSocket input stream.
Replace separate accel/tap push handling with input messages, set_input_stream, and a single set_stream on port 8081.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 13:47:41 +02:00

7.8 KiB
Raw Permalink Blame History

WebSocket API

go run . -port /dev/ttyUSB0 serve exposes the WebSocket enpoint

URL Port (default) Role
ws://localhost:8081/ws External API (-api-addr) Request/response commands + optional input push stream

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)

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)
  • 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.

Typical sequence:

  1. list_clients → slave IDs
  2. Per slave: set_input_stream and/or set_tap_notify as needed
  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.


Push stream messages

These are the samples you get after enabling receive. Timing is per WebSocket connection:

  • 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):

{
  "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,
      "tap_kind": "none"
    }
  ]
}
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 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"

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.

Failure (e.g. UART busy):

{
  "type": "input",
  "t": 1716900123456789012,
  "success": false,
  "error": "uart busy"
}

No clients array on failure.


Commands (request → response)

Send one JSON object per message. Field type selects the command.

hello (server → client, on connect)

{
  "type": "hello",
  "serial_port": "/dev/ttyUSB0",
  "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",
    "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:

{
  "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,
      "input_stream": false,
      "tap_notify_single": false,
      "tap_notify_double": false,
      "tap_notify_triple": false
    }
  ]
}

set_stream / get_stream (receive input on this connection)

{"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:

{"type":"stream_status","receive_input":true,"interval_ms":32,"pre_fetch":2,"success":true}

set_input_stream / get_input_stream (firmware, per slave)

client_id required (> 0). Enables accel streaming from the slave to the master.

{"type":"set_input_stream","client_id":16,"enable":true}
{"type":"get_input_stream","client_id":16}

Response input_stream_status:

{"type":"input_stream_status","client_id":16,"enabled":true,"success":true}

set_tap_notify / get_tap_notify (firmware, per slave)

Per client: single, double_tap, triple required on set.

{"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:

{
  "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.

{"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
clear Turn off
color Full ring RGB + intensity
progress progress 0100
digit digit 010
blink blink_ms, blink_count
find-me Locate pod

Use client_id (0 = master) or all_clients (+ optional slaves_only) for broadcast.

Response led_ring_status:

{"type":"led_ring_status","success":true,"mode":5,"client_id":16,"slaves_updated":1}

get_battery

Read cached battery samples from the master. Slaves push battery every 30 s; this command reads the master cache.

{"type":"get_battery","all_clients":true}
{"type":"get_battery","client_id":16}

Default if omitted: all clients.

Response battery_status:

{
  "type": "battery_status",
  "success": true,
  "samples": [
    {
      "client_id": 16,
      "lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71},
      "lipo2": {"valid": false},
      "age_ms": 1200
    }
  ]
}