Mesh¶
Two robots see each other on the mesh; three switches decide whether it is on, its security, its wire.
At the end of this page two robots in one process see each other on the mesh, one asks the other for status and hands it a task, and you know its three switches.
No hardware needed; without the [mesh] extra mesh.alive stays False.
STRANDS_MESH_LOCAL_DEV=true is the single-machine preset (no TLS, no ACL, loud warnings); set it before the first Robot(mesh=True).
import os, time
os.environ.setdefault("STRANDS_MESH_LOCAL_DEV", "true")
from strands_robots import Robot
a = Robot("so101", mesh=True, peer_id="arm-a")
b = Robot("so101", mesh=True, peer_id="arm-b", tool_name="so101_b")
time.sleep(1.5) # two heartbeats
print(a.mesh.alive, sorted(p["peer_id"] for p in a.mesh.peers))
# True ['arm-a__so101', 'arm-b', 'arm-b__so101']
print(a.mesh.send("arm-b", {"action": "status"}, timeout=5.0)["result"])
# {'status': 'idle', 'robots_running': []}
print(a.mesh.tell("arm-b", "wave", policy_provider="mock", duration=1.0)["result"]["status"])
# success
a.mesh.stop(); b.mesh.stop()
A simulation appears twice: the session peer (arm-b) and one child peer per robot in its world (arm-b__so101). Another mesh process on this machine shows its peers too.
What it is¶
Every Robot and simulation can own a Mesh: a peer that broadcasts presence, publishes state and sensors, answers RPC commands and relays teleoperation frames. The wire is Zenoh on the LAN, optionally bridged to AWS IoT Core (bridges). Every message is JSON on a key like strands/<peer>/state (topics).
The mesh is enrichment: a Zenoh session that fails to open leaves the robot working without it, never crashing the host.
Three switches¶
| variable | values | effect |
|---|---|---|
STRANDS_MESH |
unset (default), true, false |
Robot(mesh=None) follows this; false is a hard kill switch even over mesh=True |
STRANDS_MESH_AUTH_MODE |
mtls (default), none |
none also needs STRANDS_MESH_I_KNOW_THIS_IS_INSECURE=1, or STRANDS_MESH_LOCAL_DEV=true on loopback |
STRANDS_MESH_BACKEND |
zenoh (default), iot, bridge |
which transport carries the topics; a typo falls back to zenoh and is reported once |
Robot(..., mesh=True) forces it on for one robot, mesh=False off; init_mesh(robot, peer_id=...) attaches one to anything with send_action and stop.
Postures¶
| posture | set | for |
|---|---|---|
| local dev | STRANDS_MESH_LOCAL_DEV=true |
one machine; multicast stays off, every process on the host meets at tcp/127.0.0.1:7447 |
| trusted lab | STRANDS_MESH_AUTH_MODE=mtls, STRANDS_MESH_TLS_CA, _TLS_CERT, _TLS_KEY, STRANDS_MESH_ACCEPT_PERMISSIVE_ACL=1 |
any CA-signed peer may publish anywhere; Mesh.start refuses without the acknowledgement |
| production | the mTLS trio plus STRANDS_MESH_ACL_FILE with default_permission: "deny" |
role-separated operators and robots (bridges covers the ACL file) |
Under mtls with no ACL file and no acknowledgement, Mesh.start refuses and prints the four ways out, as does strands-robots doctor.
The mTLS trio is required together: with any of the three unset, a missing file or a symlink, session open refuses naming the variables; the loader never downgrades to plain TCP. The key file must be mode 0600 on POSIX, checked on the real file. Windows skips the mode check and logs one WARNING per key file, so restrict the key with an NTFS ACL instead.
Discovery: the first process on a host listens on tls/127.0.0.1:<STRANDS_MESH_PORT> (default 7447) and later ones connect to it, so every mesh process on a machine sees every other, forgotten dashboards included. Across hosts set ZENOH_CONNECT=tls/10.0.0.1:7447 (comma-separated) or ZENOH_LISTEN. STRANDS_MESH_MULTICAST=true opens UDP 224.0.0.224:7446 so any LAN device can find your fleet; off by default, it logs a warning when on.
Signed wire identity¶
mTLS admits a peer; it does not say which peer wrote a message. The Zenoh SourceInfo label is the publisher's own, so an admitted peer could copy a robot's and answer an e-stop as it. A certificate holder signs what it publishes (strands_robots.mesh.wire_identity): a sig block with its DER leaf, time, nonce and the signature over the body; the mTLS pair under mtls, the IoT device certificate on iot and bridge.
STRANDS_MESH_REQUIRE_SIGNED_IDENTITY: auto (default) requires one when the auth mode is mtls and STRANDS_MESH_TLS_CA loads; 1 always; 0 keeps the session-id path. When required, a presence binds robot_id to a leaf whose common name is that id (or its parent, for a <peer>__<robot> child), a reply counts once per nonce, a command names its target inside what is signed, and a motion command is attributed to its signer's peer id, the id dashboards deposit grants for. STRANDS_ROBOT_COMMAND_ALLOW=reset@lab-op pre-approves one peer, *@lab-op every verb for it; a bare verb admits any verified peer, warning once. A leaf no certificate in the STRANDS_MESH_TLS_CA bundle issued directly (an AWS-generated IoT certificate beside an mTLS fleet) counts as unsigned: the peer is dropped from the roster, its replies and motion commands refused; it can still stop a robot and read state. Teleop frames are unsigned: an approved stream follows the session its leader announced from, a hint rather than proof.
Rates and caps¶
Presence at 2 Hz, state at 10 Hz, camera off until STRANDS_MESH_CAMERA_HZ is set. Commands are capped at 20 Hz and 16 KiB per message, safety topics at 2 Hz and 4 KiB, camera frames at 1 MiB, sessions at 256; each cap has a STRANDS_MESH_* override in reference/configuration.
Pages¶
- fleet: join, discover,
tell,send,broadcast, RPC,robot_mesh. - safety and e-stop:
emergency_stop, lockout, signed resume, audit trail. - topics: every key and its rate.
- bridges: the IoT and bridge transports, the ACL file.
- direct messaging: point-to-point commands over AWS IoT Core.