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>
7.8 KiB
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
- Connect → server sends
hello(receive off; lists available commands). - Send JSON commands → server replies with a matching
*_statusorclient_listmessage (one reply per command). - After
set_streamwithenable: true, the server may sendinputmessages 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_notifyalone configures which tap kinds the slave reports; it does not enable host push by itself — you still needset_stream.
Typical sequence:
list_clients→ slave IDs- Per slave:
set_input_streamand/orset_tap_notifyas needed set_streamwith"enable": true- Read
inputmessages 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 consecutiveinputpushes 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 0–100 |
digit |
digit 0–10 |
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
}
]
}