Skip to content

Bridges

Presence, commands and safety events reach AWS IoT Core over MQTT5; joint state stays on the LAN.

At the end of this page your fleet's presence, commands and safety events reach AWS IoT Core over MQTT5 with per-robot X.509 identities while joint state and camera frames stay on the LAN, and you can write the ACL that separates operators from robots on Zenoh.

pip install 'strands-robots[mesh-iot]'          # awsiotsdk, awscrt, boto3 on top of [mesh]
from strands_robots.mesh.iot import bootstrap_account, provision_robot

bootstrap_account()                                   # once per account and region: Rules, Lambda, DynamoDB audit, Fleet Provisioning template
p = provision_robot("so101-arm-01")                   # per robot: cert + policy + Thing named after the peer
print(p.env_vars())                                   # STRANDS_IOT_THING_NAME, STRANDS_IOT_ENDPOINT, STRANDS_IOT_CERT_DIR, STRANDS_MESH_BACKEND=iot

Export those variables, set STRANDS_MESH_BACKEND=bridge, and Robot("so101", mode="real", port=..., mesh=True, peer_id="so101-arm-01") publishes on both wires.

Three backends

STRANDS_MESH_BACKEND transport for
zenoh (default) ZenohTransport: one LAN session per process, mTLS and ACL from STRANDS_MESH_* a room, a lab
iot IotMqttTransport: MQTT5 over mTLS to AWS IoT Core, no Zenoh a robot whose only peers are in the cloud
bridge BridgeTransport: one of each; every put fans out, subscriptions fan in production: LAN peers plus operator dashboards, audit and fleet ops in AWS

The bridge degrades rather than fails: Zenoh down means pure IoT, IoT down pure Zenoh, both down is_alive() false and every put a no-op.

What the bridge forwards

The MQTT side is filtered by topic suffix. Default to both wires: presence, health, cmd, response, broadcast, safety/event, safety/estop, safety/resume. LAN-only: state, pose, imu, odom, camera, input, hand, stream, lidar, map. STRANDS_MESH_BRIDGE_TOPICS, a comma-separated suffix list, replaces the default. Inbound duplicates (a presence that arrived on both wires) are dropped by sender_id and turn_id; STRANDS_MESH_BRIDGE_DEDUP_STRICT tightens that.

Keys are unchanged on MQTT (strands/<peer>/cmd is a valid MQTT topic); wildcards map * to + and ** to #.

The IoT trust model

Each robot is a Thing whose name equals its mesh peer_id and its cert CN; the MQTT client_id is set to it so ${iot:Connection.Thing.ThingName} in the IoT policy scopes every robot to its own topics. provision_robot(thing_name, region=, cert_dir=, attributes=, allow_estop_publish=False) generates the key locally, has AWS sign a CSR with CN=<thing>, O=strands-robots, and writes <thing>.cert.pem, <thing>.private.key and AmazonRootCA1.pem under STRANDS_IOT_CERT_DIR (default ~/.strands_robots/iot). Re-running publishes a changed policy document as the new default.

variable meaning
STRANDS_IOT_ENDPOINT the account's ATS endpoint
STRANDS_IOT_THING_NAME the Thing, equal to the cert CN and the peer id
STRANDS_IOT_CERT_DIR where the three PEM files live
STRANDS_IOT_CA_FILE overrides the root CA path
STRANDS_IOT_DIRECT_AUTH x509 (default with a cert) or sigv4 (IAM credentials) for direct messages

Missing awsiotsdk, endpoint or cert files make connect() return False with an ERROR line; the mesh stays off. A reconnect is a clean session, so the transport re-subscribes every topic filter and WARNs with the list.

Cloud mirrors: shadow.enable_for_mesh(mesh) keeps a Device Shadow (presence) per Thing; camera_offload.enable_for_mesh(mesh) puts camera frames in S3 (STRANDS_MESH_CAMERA_S3_BUCKET, STRANDS_MESH_CAMERA_S3_PREFIX) and publishes presigned URLs (STRANDS_MESH_CAMERA_PRESIGN_TTL).

The Zenoh ACL

Under STRANDS_MESH_AUTH_MODE=mtls the built-in ACL lets any CA-signed peer publish and subscribe anywhere, and Mesh.start refuses that posture until you set STRANDS_MESH_ACCEPT_PERMISSIVE_ACL=1 or point STRANDS_MESH_ACL_FILE at a JSON5 file. The template is examples/mesh/mesh_acl_example.json5: default_permission: "deny", named rules, subjects keyed by literal cert_common_names, and policies binding the two. Its three roles:

subject may
robot_peer publish telemetry; subscribe to presence, broadcast, safety and its own cmd and response
operator_peer publish commands; subscribe to **
dashboard_peer subscribe to observe; publish presence

Zenoh 1.x facts the loader verified live: enabled: true is required or the block is a no-op; cert_common_names match literally (no globs), so you enumerate every CN; omitting interfaces matches every link and [] is rejected; key_exprs see the key without the namespace, so **/cmd works and strands/*/cmd matches nothing; both nodes check the remote peer: grant every key set both ways. No ${cn} interpolation: strict per-peer isolation on Zenoh means one rule per robot CN (mesh_acl_strict_per_peer.json5); the IoT transport gives that isolation by construction. A file is one of two shapes: default_permission: "deny" plus allow rules is a whitelist, where a gap denies; default_permission: "allow" plus rules is a blacklist, where a key expression nobody named is open on the wire. _parse_acl_bytes (mesh/_acl_config.py) refuses the blacklist shape with PermissiveACLError unless STRANDS_MESH_ACCEPT_PERMISSIVE_ACL is 1, true or yes. The token has two more readers: Mesh._refuse_under_permissive_default_acl (mesh/core.py), the start gate under the built-in permissive default, and session._build_config (mesh/session.py), whose per-session WARNING that no ACL file is set is suppressed by it; check_mesh (doctor.py) reports all of this. A token set to load a blacklist in CI also waives the start gate: drop the file later and the fleet runs wire-open with no log signal. Do not set it on a production fleet.

STRANDS_MESH_CA_PINS pins CA fingerprints; STRANDS_MESH_DISABLE_CA_PIN turns pinning off in a lab.

ROS 2 as a peer

RosBridgedRobot, RosbridgeRobot and RtpsRobot in strands_robots.mesh wrap a ROS 2 graph as a mesh peer, so a rover on /cmd_vel appears in peers and answers execute and stop like any robot. Choosing one: ROS 2.

Edit page