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's true/false and 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 a target_id/target_name and (optionally) mode: "request" to get a reply — see The MeshSocket Protocol.

Need request/reply and content panels? Recipe: Graph + Request/Reply Content.