Data Flow: sync and action
Two directions, two mechanisms. A control receives data via sync and sends
data via action. Both are declared on the control in the layout
JSON, and both ride the The MeshSocket Protocol verbs. Get this page right and the
rest is detail.
Server → device: sync (listening)
A control subscribes by declaring one or more sync entries:
"sync": [{
"method": "meshsocket",
"type": "listen",
"event": "broadcast",
"filter": { "msg_type": "metrics" },
"valuePath": "cpu"
}]
When a matching frame arrives, the control's value updates. Three fields control matching and extraction:
event — which frames to listen to
The device registers a listener for frames of this type. There is one special,
recommended value:
-
"broadcast"— the shared, multiplexed channel. Your server sends abroadcast_request(see The MeshSocket Protocol); the relay fans it out as abroadcastframe to every channel member. This is the idiomatic telemetry path because one broadcast reaches every viewer. Multiple controls listen on"broadcast"and tell themselves apart withfilter. -
Any other name (e.g.
"telemetry") — the device listens for frames whosetypeequals that name. The relay's fan-out always usestype: "broadcast", so to hit a custom event your server must route a frame of that exact type to the device (route_msg_noreplywith"type": "telemetry"). Prefer"broadcast"+filterunless you specifically need point-to-point delivery.
filter — which broadcasts are mine
Because everyone shares the "broadcast" channel, filter selects the frames a
control cares about. It is a shallow match: every key in filter must be
present in the payload with an equal value.
"filter": { "msg_type": "metrics" } // matches {"msg_type":"metrics", ...}
"filter": { "event": "ups", "field": "load" } // both must match
No filter ⇒ the control reacts to every broadcast frame. Choose a stable
discriminator (msg_type is the convention) so unrelated traffic is ignored.
valuePath — what value to pull out
A dot path into the payload. Objects are indexed by key, arrays by integer index:
"valuePath": "cpu" // {"cpu": 73} → 73
"valuePath": "stats.cpu" // {"stats": {"cpu": 73}} → 73
"valuePath": "cores.0.load" // {"cores": [{"load": 12}]} → 12
- An empty / omitted
valuePathuses the payload itself (must be a scalar). - The resolved value must be a scalar (
number,bool, orstring) for ordinary controls. A gauge/slider/progress wants a number; a toggle a bool; a label/segmented a string. - If the path doesn't resolve (missing key, wrong type), the update is skipped — no crash, the control just keeps its last value.
Special receivers
A few controls interpret the synced data specially:
sparkline— each incoming number is appended to a rolling buffer (length =sparklinePoints, default 50). Stream raw numbers; the control keeps the history.list—valuePathshould resolve to an array of objects; the rows replace the list contents.logConsole— accepts a string, an object{ "text", "level" }, or an array of either; each becomes a log line (capped tomaxLines).graph—valuePathresolves to the graph data (nodes/edges); see Recipe: Graph + Request/Reply Content.
Worked example (gauge)
Layout:
{ "type": "gauge", "id": "cpu", "min": 0, "max": 100,
"sync": [{ "method":"meshsocket", "type":"listen",
"event":"broadcast", "filter":{"msg_type":"metrics"}, "valuePath":"cpu" }] }
Server broadcasts (one frame moves it for every connected viewer):
{ "type": "broadcast_request", "payload": { "msg_type": "metrics", "cpu": 73, "mem": 41 } }
→ the cpu gauge reads 73. A second gauge with valuePath: "mem" and the same
filter reads 41 from the same frame.
Device → server: action (sending)
A control emits when the user interacts with it:
"action": {
"method": "meshsocket",
"mode": "broadcast",
"event": "broadcast_request",
"payload": { "msg_type": "command", "command": "reboot", "level": "{{value}}" }
}
mode — fire-and-forget vs request/reply
"broadcast"(and anything not"request") → the device sendseventwithpayloadand does not wait. Use for commands and telemetry from the device."request"→ the device sendseventand awaits a reply. Use when the action needs an answer (e.g. a node tap that fetches content to display).
event — the MeshSocket verb
event is the wire verb, not an app-level name:
"broadcast_request"— fan the action out to the channel. Put an app-level discriminator in the payload (msg_type) so your server can route it. Every channel member (including your server) receives it as abroadcastframe."route_msg"— send to a specific member and (withmode: "request") await its reply. The payload carries{ "target_id" | "target_name", "type", "payload" }."route_msg_noreply"— targeted fire-and-forget.
payload and {{value}}
The control's current value is substituted into the payload wherever {{value}}
appears, recursively through objects and arrays:
- Exact token — a string that is exactly
"{{value}}"is replaced with the value's native type. A slider at 72 sends the number72, a toggle sends the booltrue. (This matters: numeric/bool receivers reject a stringified copy.) - Embedded token —
"level {{value}}%"becomes a string with the value interpolated ("level 72%").
What your server receives
For a broadcast_request action, your server — sitting on the channel — receives
a broadcast frame with the payload, and demuxes on msg_type exactly like a
control does on the receive side. For a route_msg action, register a handler for
the routed type and (if it was a request) return a value to answer it.
Worked example (button + slider)
{ "type": "slider", "id": "bright", "min": 0, "max": 100,
"action": { "method":"meshsocket", "mode":"broadcast", "event":"broadcast_request",
"payload": { "msg_type":"set", "target":"lamp", "value":"{{value}}" } } }
Dragging to 72 sends:
{ "type":"broadcast_request", "payload": { "msg_type":"set", "target":"lamp", "value": 72 } }
Your server sees { msg_type:"set", target:"lamp", value:72 } and acts on it.
The round trip
A typical interactive layout uses both directions at once: controls listen on
"broadcast" (filtered by msg_type) for state your server pushes, and emit
broadcast_request actions (tagged with a msg_type) that your server handles —
often updating state and broadcasting the new value straight back, so the UI
reflects reality rather than optimistic guesses.
Build it for real in Recipe: Telemetry Dashboard and Recipe: Interactive Controls. Pick a language in Anatomy of a CAR-TER Server.