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 a broadcast_request (see The MeshSocket Protocol); the relay fans it out as a broadcast frame 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 with filter.

  • Any other name (e.g. "telemetry") — the device listens for frames whose type equals that name. The relay's fan-out always uses type: "broadcast", so to hit a custom event your server must route a frame of that exact type to the device (route_msg_noreply with "type": "telemetry"). Prefer "broadcast" + filter unless 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 valuePath uses the payload itself (must be a scalar).
  • The resolved value must be a scalar (number, bool, or string) 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.
  • listvaluePath should 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 to maxLines).
  • graphvaluePath resolves 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 sends event with payload and does not wait. Use for commands and telemetry from the device.
  • "request" → the device sends event and 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 a broadcast frame.
  • "route_msg" — send to a specific member and (with mode: "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 number 72, a toggle sends the bool true. (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.