Building a Server in Python
The MeshSocket repo ships a Python client (socketCore.MeshSocket) and a relay
(socket_server.MeshServer). This is the lowest-effort way to integrate. For
other languages, see Building a Server in Any Language (Raw Protocol); for the concepts, Data Flow — sync & actions.
Getting the library
The client lives in the MeshSocket repo under Python/
(socketCore.py, socket_server.py). It is not yet published to PyPI, so vendor
it or add it to your path:
import sys
sys.path.insert(0, "/path/to/MeshSocket/Python")
from socketCore import MeshSocket # the client
from socket_server import MeshServer # the relay (for self-hosting)
Dependencies: websockets (the only hard requirement for the client).
The client API
MeshSocket(
url="ws://host:8765", # relay URL
name="My Server", # display name / routing handle
auth_token="", # gateway token; "" for self-hosted
channel="home", # channel to join
role="hub", # role (sets capability defaults)
can_broadcast=True, # may send broadcast_request
can_route=True, # may send route_msg / route_msg_noreply
can_monitor=False, # receive roster / call get_nodes
)
| Method | Purpose |
|---|---|
await socket.start() |
Connect + run the receive loop (run it as a task) |
await socket.wait_until_ready() |
Resolve once identified (welcome received) |
@socket.on("type") |
Register a handler for incoming frames of type |
await socket.emit("type", payload) |
Fire-and-forget send (alias of send) |
await socket.request("type", payload, timeout=5.0) |
Send and await the reply |
A handler that returns a value answers a request (the library wraps it into a
reply_to); returning None is fire-and-forget.
Minimal server: push telemetry + handle a command
import asyncio, random, sys
sys.path.insert(0, "/path/to/MeshSocket/Python")
from socketCore import MeshSocket
RELAY = "ws://192.168.1.50:8765" # your relay; wss + auth_token for Connect+
CHANNEL = "home"
socket = MeshSocket(url=RELAY, name="Demo Server", channel=CHANNEL,
role="hub", can_broadcast=True, can_route=True)
# --- device → server: actions arrive as `broadcast`, demux on msg_type ---
@socket.on("broadcast")
async def on_broadcast(payload):
if not isinstance(payload, dict):
return
if payload.get("msg_type") == "command":
print("command:", payload.get("command"), payload)
elif payload.get("msg_type") == "set":
print("set:", payload.get("target"), "→", payload.get("value"))
# no return value → fire-and-forget
# --- server → device: broadcast metrics every second ---
async def telemetry_loop():
while True:
await socket.emit("broadcast_request", {
"msg_type": "metrics",
"cpu": random.randint(0, 100),
"mem": random.randint(0, 100),
})
await asyncio.sleep(1)
async def main():
asyncio.create_task(socket.start())
await socket.wait_until_ready()
print("connected to", RELAY, "on", CHANNEL)
await telemetry_loop()
asyncio.run(main())
Pair this with the tiny layout (gauge filtered on msg_type: "metrics",
button broadcasting msg_type: "command") and the gauge moves while taps print.
Answering a request (routed RPC)
When a control uses mode: "request" (e.g. a graph node fetching content via
route_msg), the frame is routed to you as a request of the inner type.
Register a handler that returns the response object:
@socket.on("get_doc")
async def on_get_doc(payload):
path = (payload or {}).get("path", "")
return { "title": path, "content": load_markdown(path) } # becomes the reply
See Recipe: Graph + Request/Reply Content for the matching layout side.
Running a relay (self-hosting)
For LAN use, run the bundled relay — no gateway, no token (it accepts any client
when MESH_AUTH_TOKEN is unset):
import asyncio, sys
sys.path.insert(0, "/path/to/MeshSocket/Python")
from socket_server import MeshServer
# Binds 0.0.0.0:8765; point both your server and the phone's QR/layout at
# ws://<this-machine-LAN-IP>:8765
asyncio.run(MeshServer(host="0.0.0.0", port=8765).start())
MeshServer also accepts auth_handler=lambda token, ip: ... if you want simple
token gating without the full Connect+ gateway, and on_startup / on_authenticated
hooks. On macOS you may get a one-time firewall prompt to accept incoming
connections — allow it.
Discovering the device (roster)
If you need to wait for a phone to appear (e.g. before pushing), poll rather than rely on the membership push:
async def wait_for_peer(timeout=30):
import time; deadline = time.time() + timeout
while time.time() < deadline:
resp = await socket.request("get_nodes", timeout=3.0)
peers = [c for c in (resp or {}).get("clients", []) if c["name"] != socket.name]
if peers:
return peers
await asyncio.sleep(1)
return []
(The roster is pushed as server_client_list on changes, but that push can miss
peers that join after you — get_nodes reflects the live roster at call time. See
Message Reference.)
Production notes
- Connect+ gateway: use
wss://…and pass a validauth_token. Without a valid token you'll be parked at "awaiting gateway" (see Connection & Pairing). - Reconnects: wrap
start()so a dropped socket reconnects; the client supports offline buffering (max_offline_buffer) if you want sends to survive brief drops. - Rate: a few broadcasts/sec/metric is plenty; the device coalesces UI updates and the gateway rate-limits.
Recipes: Recipe: Telemetry Dashboard, Recipe: Interactive Controls, Recipe: Graph + Request/Reply Content, Recipe: Server-Driven (Dynamic) UI.