Files
spore/docs/StreamingAPI.md
Patrick Balsiger 2aa887a037 docs(streaming): add 'Things to consider' on high-frequency WS usage and throttling
Notes on heap fragmentation, UDP saturation, throttling/coalescing, buffer reuse, yielding, and payload sizing for reliable streaming/broadcast.
2025-09-28 22:10:18 +02:00

99 lines
3.1 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.

## Streaming API (WebSocket)
### Overview
The streaming API exposes an event-driven WebSocket at `/ws`. It bridges between external clients and the internal event bus:
- Incoming WebSocket JSON `{ event, payload }``ctx.fire(event, payload)`
- Local events → broadcasted to all connected WebSocket clients as `{ event, payload }`
This allows real-time control and observation of the system without polling.
### URL
- `ws://<device-ip>/ws`
### Message Format
- Client → Device
```json
{
"event": "<event-name>",
"payload": "<json-string>" | { /* inline JSON */ }
}
```
- Device → Client
```json
{
"event": "<event-name>",
"payload": "<json-string>"
}
```
Notes:
- The device accepts `payload` as a string or a JSON object/array. Objects are serialized into a string before dispatching to local subscribers to keep a consistent downstream contract.
- A minimal ack `{ "ok": true }` is sent after a valid inbound message.
#### Echo suppression (origin tagging)
- To prevent the sender from receiving an immediate echo of its own message, the server injects a private field into JSON payloads:
- `_origin: "ws:<clientId>"`
- When re-broadcasting local events to WebSocket clients, the server:
- Strips the `_origin` field from the outgoing payload
- Skips the originating `clientId` so only other clients receive the message
- If a payload is not valid JSON (plain string), no origin tag is injected and the message may be echoed
### Event Bus Integration
- The WebSocket registers an `onAny` subscriber to `NodeContext` so that all local events are mirrored to clients.
- Services should subscribe to specific events via `ctx.on("<name>", ...)`.
### Examples
1) Set a solid color on NeoPattern:
```json
{
"event": "api/neopattern/color",
"payload": { "color": "#FF0000", "brightness": 128 }
}
```
2) Broadcast a cluster event (delegated to core):
```json
{
"event": "cluster/broadcast",
"payload": {
"event": "api/neopattern/color",
"data": { "color": "#00FF00", "brightness": 128 }
}
}
```
### Reference Implementation
- WebSocket setup and bridging are implemented in `ApiServer`.
- Global event subscription uses `NodeContext::onAny`.
Related docs:
- [`ClusterBroadcast.md`](./ClusterBroadcast.md) — centralized UDP broadcasting and CLUSTER_EVENT format
### Things to consider
- High-frequency updates can overwhelm ESP8266:
- Frequent JSON parse/serialize and `String` allocations fragment heap and may cause resets (e.g., Exception(3)).
- UDP broadcast on every message amplifies load; WiFi/UDP buffers can back up.
- Prefer ≥50100 ms intervals; microbursts at 10 ms are risky.
- Throttle and coalesce:
- Add a minimum interval in the core `cluster/broadcast` handler.
- Optionally drop redundant updates (e.g., same color as previous).
- Reduce allocations:
- Reuse `StaticJsonDocument`/preallocated buffers in hot paths.
- Avoid re-serializing when possible; pass-through payload strings.
- Reserve `String` capacity when reuse is needed.
- Yielding:
- Call `yield()` in long-running or bursty paths to avoid WDT.
- Packet size:
- Keep payloads small to fit `ClusterProtocol::UDP_BUF_SIZE` and reduce airtime.