Skip to content

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.

pip install 'strands-robots[sim-mujoco,mesh]'

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

Left, a dashed layer, one LAN with mTLS by default, holding two dashed hosts. Host A: so101 real, Robot("so101", mode="real", mesh=True); so101_sim, Robot("so101"); the router chip tcp/127.0.0.1:7447, the first process listens and the rest connect. Host B: the dashboard, one more peer with the fleet page, the consent card and e-stop; g1 real, Robot("unitree_g1", mode="real"); the chip ZENOH_CONNECT=tcp/10.0.0.1:7447, how a second host joins. A two-headed wire between the hosts carries presence, state and RPC. Under the LAN the one green element, the safety topics: strands/safety/estop locks every peer that hears it, strands/safety/resume carries a proof. Right, across sites: a bridge peer, STRANDS_MESH_BACKEND=bridge, relays the same topics to AWS IoT Core, mTLS and a policy per thing; a wire claim never becomes a local fact. Footnote: the mesh is enrichment.Left, a dashed layer, one LAN with mTLS by default, holding two dashed hosts. Host A: so101 real, Robot("so101", mode="real", mesh=True); so101_sim, Robot("so101"); the router chip tcp/127.0.0.1:7447, the first process listens and the rest connect. Host B: the dashboard, one more peer with the fleet page, the consent card and e-stop; g1 real, Robot("unitree_g1", mode="real"); the chip ZENOH_CONNECT=tcp/10.0.0.1:7447, how a second host joins. A two-headed wire between the hosts carries presence, state and RPC. Under the LAN the one green element, the safety topics: strands/safety/estop locks every peer that hears it, strands/safety/resume carries a proof. Right, across sites: a bridge peer, STRANDS_MESH_BACKEND=bridge, relays the same topics to AWS IoT Core, mTLS and a policy per thing; a wire claim never becomes a local fact. Footnote: the mesh is enrichment.

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.
Edit page