Recipe: Interactive Controls
Goal: buttons, toggles, and sliders that do something — and stay in sync across viewers because the server echoes authoritative state back. Concepts: Data Flow — sync & actions.
The pattern: command out, state back
sequenceDiagram
participant C as Control (device)
participant R as Relay
participant S as Server
participant V as All viewers
C->>R: broadcast_request {msg_type cmd}
R-->>S: broadcast (cmd)
S->>S: apply change
S->>R: broadcast_request {msg_type state}
R-->>V: broadcast (state) → controls update
Don't let a control assume its tap "worked" — broadcast the resulting state so the UI mirrors reality and every device agrees.
The layout
{
"name": "Lab Bench", "version": 1,
"connection": { "url":"ws://192.168.1.50:8765",
"identity": { "name":"Bench", "channel":"lab", "role":"controller", "canBroadcast": true } },
"tabs": [{
"title":"Bench", "icon":"slider.horizontal.3",
"grid": { "columns": 4, "rows": 4 },
"children": [
{ "type":"toggle", "id":"power", "position":[0,0], "span":[1,2], "label":"Power",
"action": { "method":"meshsocket","mode":"broadcast","event":"broadcast_request",
"payload": { "msg_type":"cmd","target":"power","value":"{{value}}" } },
"sync": [{ "method":"meshsocket","type":"listen","event":"broadcast",
"filter":{"msg_type":"state","target":"power"}, "valuePath":"value" }] },
{ "type":"slider", "id":"volts", "position":[1,0], "span":[1,4], "label":"Volts",
"min":0, "max":24, "step":0.5,
"action": { "method":"meshsocket","mode":"broadcast","event":"broadcast_request",
"payload": { "msg_type":"cmd","target":"volts","value":"{{value}}" } },
"sync": [{ "method":"meshsocket","type":"listen","event":"broadcast",
"filter":{"msg_type":"state","target":"volts"}, "valuePath":"value" }] },
{ "type":"button", "id":"estop", "position":[2,0], "span":[1,2], "label":"E-STOP",
"tint":"#FF3B30", "haptic":"error",
"action": { "method":"meshsocket","mode":"broadcast","event":"broadcast_request",
"payload": { "msg_type":"cmd","target":"estop" } } }
]
}]
}
Each interactive control both sends (action) and listens (sync) on a
state frame scoped to its target, so a change from anywhere (another phone,
the server's own logic, a physical knob) reflects everywhere.
The server (Python)
import asyncio, sys
sys.path.insert(0, "/path/to/MeshSocket/Python")
from socketCore import MeshSocket
socket = MeshSocket(url="ws://192.168.1.50:8765", name="Bench Server",
channel="lab", role="hub", can_broadcast=True)
state = {"power": False, "volts": 0.0}
async def push_state(target):
await socket.emit("broadcast_request",
{"msg_type": "state", "target": target, "value": state[target]})
@socket.on("broadcast")
async def on_broadcast(payload):
if not isinstance(payload, dict) or payload.get("msg_type") != "cmd":
return
target = payload.get("target")
if target == "estop":
state["power"] = False; state["volts"] = 0.0
await push_state("power"); await push_state("volts")
elif target in state:
state[target] = payload.get("value")
await push_state(target) # echo authoritative value back
async def main():
asyncio.create_task(socket.start())
await socket.wait_until_ready()
for t in state: await push_state(t) # send initial state to new viewers
await asyncio.Future() # run forever
asyncio.run(main())
Notes
{{value}}types. The exact-token rule sends the toggle'strue/falseand the slider's number natively — your handler gets real bools/numbers (Data Flow — sync & actions).- Buttons often carry a fixed payload (no
{{value}}) — they're "fire this command." - Echo, don't assume. The
sync-back is what keeps multiple phones consistent and makes the UI authoritative. - Targeted instead of broadcast? If only one server should receive a command,
use
event: "route_msg"with atarget_id/target_nameand (optionally)mode: "request"to get a reply — see The MeshSocket Protocol.
Need request/reply and content panels? Recipe: Graph + Request/Reply Content.