Notes on heap fragmentation, UDP saturation, throttling/coalescing, buffer reuse, yielding, and payload sizing for reliable streaming/broadcast.
99 lines
3.1 KiB
Markdown
99 lines
3.1 KiB
Markdown
## 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 ≥50–100 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.
|
||
|