Hare WebSocket stream¶
The Hare WebSocket endpoint streams live PV updates. Clients subscribe to sets of PVs and receive an initial snapshot (values + display metadata) followed by batched diffs as values change.
Connecting¶
Endpoint:
ws://<host>/hare/v1/ws/pvs
Use wss:// in production (TLS).
Authentication¶
The API key (with read scope) can be provided in two ways:
- Header (preferred) — set
X-API-Keyduring the handshake - Query parameter — append
?api_key=<token>to the URL, for clients (e.g. the browserWebSocketAPI) that cannot set custom headers
If authentication fails, the server closes the connection with WebSocket
status code 1008 (Policy Violation).
Example — JavaScript¶
const ws = new WebSocket("ws://localhost:8000/hare/v1/ws/pvs?api_key=your-api-key");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "subscribe",
pvNames: ["TEST:SINE", "pva://TEST:COUNTER"]
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === "diff") {
for (const [pv, value] of Object.entries(msg.data)) {
console.log(pv, value.value, value.severity);
}
}
};
Example — Python (websockets)¶
import asyncio
import json
import websockets
async def main():
uri = "ws://localhost:8000/hare/v1/ws/pvs"
headers = {"X-API-Key": "your-api-key"}
async with websockets.connect(uri, additional_headers=headers) as ws:
await ws.send(json.dumps({
"type": "subscribe",
"pvNames": ["TEST:SINE", "TEST:WAVEFORM"],
}))
async for raw in ws:
msg = json.loads(raw)
print(msg["type"], msg)
asyncio.run(main())
Client --> server messages¶
subscribe¶
{
"type": "subscribe",
"pvNames": ["TEST:SINE", "pva://TEST:COUNTER"]
}
pvNames must be a list of strings. Subscribing is idempotent — names the
client already holds are accepted again at no cost. Invalid names in the batch
are rejected individually (see error frames below); the valid names
still subscribe.
unsubscribe¶
{
"type": "unsubscribe",
"pvNames": ["TEST:SINE"]
}
ping¶
{
"type": "ping"
}
The server responds with pong.
Server --> client messages¶
initial¶
Sent after a subscribe, once the new PVs have been seeded. data maps PV
name to its current value; meta maps PV name to its static display metadata
(units, precision, enumChoices, limits) — read meta once, then rely on
diffs for the values.
{
"type": "initial",
"data": {
"TEST:SINE": {
"pv": "TEST:SINE",
"value": 3.27,
"valueKind": "scalar",
"severity": "NO_ALARM",
"alarmStatus": 0,
"tsNs": 1700000000123456789,
"connected": true,
"error": null
}
},
"meta": {
"TEST:SINE": {
"pv": "TEST:SINE",
"valueKind": "scalar",
"units": "degC",
"precision": 2,
"displayLow": -10.0,
"displayHigh": 10.0,
"writable": true
}
},
"count": 1
}
A PV that is valid but has not produced a value yet (e.g. a disconnected
channel) may be absent from initial — its first diff arrives when the
channel connects. If no PV in the batch yields a value or metadata, the
initial frame is skipped entirely, so do not treat it as a subscribe
acknowledgement.
diff¶
The workhorse frame. Updates are buffered and flushed on a fixed cadence
(CANOPY_HARE__WS_BATCH_INTERVAL_MS, default 100 ms); each flush carries only
the PVs whose values changed since the last one, and only the dynamic
PVValue payload — metadata is never re-sent.
{
"type": "diff",
"data": {
"TEST:SINE": {
"pv": "TEST:SINE",
"value": 3.31,
"valueKind": "scalar",
"severity": "NO_ALARM",
"alarmStatus": 0,
"tsNs": 1700000000223456789,
"connected": true,
"error": null
}
},
"count": 1,
"timestamp": 1700000000.25
}
value is a number, a string, an enum index, a 1-D array, or a
{labels, columns} table, per valueKind — see
values and metadata. alarmStatus is the EPICS
menuAlarmStat code behind severity (see the
value fields).
heartbeat¶
Sent every CANOPY_HARE__HEARTBEAT_INTERVAL_S seconds (default 5) so clients
know the broker is alive even when no values are changing.
{
"type": "heartbeat",
"timestamp": 1700000005.0,
"backend": "simulation"
}
backend names the active EPICS backend — a frontend can surface a warning
when it is unexpectedly simulation.
pong¶
{
"type": "pong",
"timestamp": 1700000000.5
}
error¶
Errors arrive as frames on the open connection — a bad message never drops the socket (only an authentication failure closes it).
A malformed frame (invalid JSON, unknown type, pvNames not a list of
strings) produces:
{
"type": "error",
"code": "invalid_message",
"message": "pvNames: Input should be a valid list"
}
A subscribe containing malformed PV names produces an error frame listing
exactly which names were rejected — the rest of the batch subscribes
normally:
{
"type": "error",
"code": "invalid_pv_name",
"message": "rejected 2 invalid PV name(s)",
"pvNames": ["", "BAD NAME"]
}
This lets a client distinguish "rejected at subscribe time" from "valid PV that has not produced a value yet."
Lifecycle¶
sequenceDiagram
participant Client
participant Hare
Client->>Hare: subscribe {pvNames: [A, B]}
Hare->>Client: initial {data: {A, B}, meta: {A, B}}
Hare->>Client: diff {data: {A}}
Hare->>Client: diff {data: {A, B}}
Note right of Hare: batched every 100 ms,<br/>changed PVs only
Hare->>Client: heartbeat {backend}
Client->>Hare: unsubscribe {pvNames: [B]}
Hare->>Client: diff {data: {A}}
On disconnect, all of the client's subscriptions are cleaned up automatically; monitors for PVs no other client watches are released.
Limits¶
The broker enforces per-client caps (configurable — see configuration):
| Cap | Default | Behavior at the limit |
|---|---|---|
| PVs per subscribe message | 5000 | Excess names in the frame are dropped |
| Subscriptions per client | 20000 | New PVs beyond the budget are dropped; existing ones unaffected |
Dropped names are logged server-side but do not produce an error frame — stay under the caps by batching subscriptions sensibly. Re-subscribing to already-held PVs never counts against the budget.