Skip to content

Robot and factory

Every keyword the Robot factory accepts and every method the engine or hardware robot it returns has.

Robot(name, mode=...) is a factory function, not a class: it returns a simulation engine in mode="sim" (the default) or a strands_robots.hardware_robot.Robot in mode="real"; both expose send_action, get_observation, run_policy and cleanup, but run_policy differs: hardware takes a Policy first, sim a robot name and a provider, or a built Policy as policy_object=. Below: every factory keyword and method of the returned object.

The factory

strands_robots.robot.Robot

Robot(name: str, mode: Literal['sim'] = ..., backend: str = ..., urdf_path: str | None = ..., cameras: dict[str, dict[str, Any]] | None = ..., position: list[float] | None = ..., data_config: str | None = ..., *, orientation: list[float] | None = ..., keyframe: str | int | None = ..., driver: str = ..., tool_name: str | None = ..., **kwargs: Any) -> Simulation
Robot(name: str, mode: Literal['real'], backend: str = ..., urdf_path: str | None = ..., cameras: dict[str, dict[str, Any]] | None = ..., position: list[float] | None = ..., data_config: str | None = ..., *, orientation: list[float] | None = ..., keyframe: str | int | None = ..., driver: Literal['strands'], tool_name: str | None = ..., **kwargs: Any) -> HardwareDriver
Robot(name: str, mode: Literal['real'], backend: str = ..., urdf_path: str | None = ..., cameras: dict[str, dict[str, Any]] | None = ..., position: list[float] | None = ..., data_config: str | None = ..., *, orientation: list[float] | None = ..., keyframe: str | int | None = ..., driver: str = ..., tool_name: str | None = ..., **kwargs: Any) -> HardwareRobot
Robot(name: str, mode: Literal['auto'] | str = ..., backend: str = ..., urdf_path: str | None = ..., cameras: dict[str, dict[str, Any]] | None = ..., position: list[float] | None = ..., data_config: str | None = ..., *, orientation: list[float] | None = ..., keyframe: str | int | None = ..., driver: str = ..., tool_name: str | None = ..., **kwargs: Any) -> Simulation | HardwareRobot
Robot(name: str, mode: str = 'sim', backend: str = 'mujoco', urdf_path: str | None = None, cameras: dict[str, dict[str, Any]] | None = None, position: list[float] | None = None, data_config: str | None = None, mesh: bool | None = None, peer_id: str | None = None, *, orientation: list[float] | None = None, keyframe: str | int | None = None, driver: str = 'auto', tool_name: str | None = None, **kwargs: Any) -> Simulation | HardwareRobot | HardwareDriver

Create a robot - returns a Simulation or HardwareRobot instance.

This is a convenience factory, NOT a wrapper class. You get the real backend instance back - with full access to all its methods.

Defaults to simulation mode so that Robot("so100") never accidentally sends commands to physical hardware. Use mode="real" to explicitly opt into hardware control.

Parameters:

Name Type Description Default
name str

Robot name ("so100", "aloha", "unitree_g1", "panda", ...) Accepts any alias defined in registry/robots.json.

required
mode str

"sim" (default - safe), "real" (explicit hardware), or "auto" (probes USB for servo controllers, falls back to sim). Case-insensitive; surrounding whitespace ignored.

'sim'
backend str

Simulation backend name or alias, resolved through strands_robots.simulation.create_simulation - the same registry that powers create_simulation() directly. Built-in: "mujoco" (CPU, default; aliases "mj"/"mjc"/"mjx") and "newton" (GPU, warp-lang based; alias "nt"). Heavy out-of-tree backends ("isaac") ship in the strands-robots-sim plugin package and resolve once installed. An unavailable backend surfaces the factory's actionable install hint. Backend-specific kwargs (e.g. num_envs, device) are forwarded to the backend constructor. Only applies to mode="sim"; ignored for mode="real".

'mujoco'
urdf_path str | None

Explicit path to URDF/MJCF file. If not provided, resolved via strands_robots.simulation.model_registry (asset manager or STRANDS_ASSETS_DIR search paths). Only applies to mode="sim"; in mode="real" it is ignored and reported at debug level.

None
cameras dict[str, dict[str, Any]] | None

Camera config for real hardware. Example::

{"wrist": {"type": "opencv", "index_or_path": "/dev/video0", "fps": 30}}

Note: In mode="sim", cameras must be added after creation via the simulation tool (add_camera action). They cannot be passed to the factory yet.

None
position list[float] | None

Robot base position in the sim world, [x, y, z]. Passed through to the backend's add_robot verbatim, so the backend's contract governs: omitting it spawns at the origin (or the registry's spawn_position for a model authored below the ground), and a wrong-length, non-numeric or non-finite vector is refused with an actionable message (surfaced here as RuntimeError) instead of being replaced by the origin. Only applies to mode="sim"; in mode="real" it is ignored and reported at debug level.

None
data_config str | None

Data configuration name for observation/action schema. Honoured in both modes: mode="sim" defaults it to the canonical robot name, and mode="real" forwards it verbatim to strands_robots.hardware_robot.Robot, which carries it into the policy_config a policy is built with.

None
orientation list[float] | None

Robot base orientation in the sim world as a quaternion [w, x, y, z]. Forwarded to the backend's add_robot verbatim on the same terms as position: omitting it spawns unrotated, and a wrong-length, non-numeric or non-finite quaternion is refused with an actionable message (surfaced here as RuntimeError) rather than being replaced by the identity rotation. Only applies to mode="sim"; in mode="real" it is ignored and reported at debug level.

None
keyframe str | int | None

Spawn the robot in a canonical pose declared by a <keyframe> in its source model (e.g. panda "home", aloha "neutral_pose") instead of the default all-zero configuration. Accepts the keyframe name (str) or index (int) and is forwarded to the backend's add_robot verbatim, so that method's contract governs: the pose is sticky across reset() and an unknown keyframe is a hard error naming the available keyframes (surfaced here as RuntimeError) instead of a silent zero-pose spawn. Only applies to mode="sim"; in mode="real" it is ignored and reported at debug level.

None
mesh bool | None

Attach a Zenoh fleet-coordination mesh. None (default) keeps mesh OFF for a quiet bare Robot(...) but honours the STRANDS_MESH=true opt-in. Pass True to force it on or False to force it off; an explicit value always wins over the env default. STRANDS_MESH=false is a hard kill switch.

None
peer_id str | None

Optional mesh peer identifier. Auto-generated when omitted.

None
driver str

Which implementation drives the robot in mode="real", one of :data:~strands_robots.registry.DRIVER_CHOICES. "auto" (default) states no preference: it honours the robot's registry hardware.driver, otherwise builds the native driver registered for this robot, and falls back to the lerobot driver only for a robot this package has no driver for. "lerobot" pins that path explicitly. "strands" builds the native driver registered for this robot via :func:~strands_robots.drivers.register_native_driver, and is refused by name when none is - never quietly served the lerobot one. The value is checked in every mode, but only mode="real" acts on it; mode="sim" refuses any value but "auto" with TypeError, as it refuses port=.

'auto'
tool_name str | None

The name the agent sees this robot under. Defaults to "<name>_sim" in simulation and to the canonical robot name on hardware - which is why two Robot("so101") in one Agent used to fail at registration ("Tool name 'so101_sim' already exists"). Name each one (tool_name="left_arm") to put a bimanual pair, or a real arm beside its sim twin, in one agent. Must be letters, digits, _ or -, at most 64 characters - what a model provider accepts as a tool name. Anything else is refused here, before the backend builds, rather than at the first model call as a validation error about the request body.

None
**kwargs Any

Forwarded to the underlying backend constructor.

{}

Returns:

Type Description
Simulation | Robot | HardwareDriver

strands_robots.simulation.Simulation (sim) or

Simulation | Robot | HardwareDriver

strands_robots.hardware_robot.Robot (real hardware).

Simulation | Robot | HardwareDriver

Either one reports name back as robot_name, the string its

Simulation | Robot | HardwareDriver

methods take; tool_name is the agent-tool name.

Raises:

Type Description
ValueError

If mode is not 'sim'/'real'/'auto', if driver is not one of :data:~strands_robots.registry.DRIVER_CHOICES, if driver="strands" names a robot with no registered native driver, if cameras= is passed in sim mode, if the robot name is empty or not in the registry (and no urdf_path= given), or if backend is not a known backend (the message lists the available backends and any install hint).

ImportError

If a known backend's optional dependency is missing (e.g. newton without warp); the message names the pip extra to install.

RuntimeError

If the sim world or robot fails to initialize.

Examples::

# Simulation (default - safe)
sim = Robot("so100")

# Explicit MJCF model path
sim = Robot("my_arm", urdf_path="path/to/robot.xml")

# Real hardware (explicit opt-in)
hw = Robot("so100", mode="real", cameras={...})

# Auto-detect (probes USB, falls back to sim)
robot = Robot("so100", mode="auto")

# The 5-line promise (defaults to sim - safe, no hardware needed)
from strands_robots import Robot
from strands import Agent
robot = Robot("so100")  # mode="sim" (default)
agent = Agent(tools=[robot])
agent("Pick up the red cube")

The hardware robot

Returned by Robot(name, mode="real"). Also usable directly when you already hold a driver.

strands_robots.hardware_robot.Robot

Robot(tool_name: str, robot: Robot | RobotConfig | str, cameras: dict[str, dict[str, Any]] | None = None, action_horizon: int = 8, data_config: str | Any | None = None, control_frequency: float = 50.0, ros2_bridge: bool = False, ros2_domain: int = 0, ros2_commands: bool = True, ros2_transport: str = 'rclpy', joint_limits: dict[str, tuple[float, float]] | None = None, dds_security_config: dict[str, str] | None = None, foxglove: bool | str = False, foxglove_mcap: str | PathLike[str] | None = None, foxglove_services: bool = False, **kwargs: Any)

Bases: TeleopMixin, AgentTool

Universal robot control with async task execution and status reporting.

Initialize Robot with async capabilities.

Parameters:

Name Type Description Default
tool_name str

Name for this robot tool

required
robot Robot | RobotConfig | str

LeRobot Robot instance, RobotConfig, or robot type string

required
cameras dict[str, dict[str, Any]] | None

Camera configuration dict: {"wrist": {"type": "opencv", "index_or_path": "/dev/video0", "fps": 30}} Each key names one camera and must be a bare token of letters, digits, _ or -: it is the identity every consumer keys that camera's frames by - a level of the mesh topic they are published on, a segment of the S3 key they are offloaded to, and the observation.images.<name> feature key a recording writes them under - so a name carrying punctuation any of those reserves is refused here (:func:~strands_robots.utils.camera_token_error).

None
action_horizon int

Actions consumed from each inferred policy chunk before re-querying. Must be a positive integer - it is a lower bound on the chunk slice the task loop applies (resolve_chunk_length returns max(action_horizon, policy.execution_horizon)), and a 0/negative/float/bool/non-numeric value raises ValueError here rather than being silently clamped to a horizon the caller never asked for, or aborting the task mid-run once the arm is already connected.

8
data_config str | Any | None

Data configuration (for GR00T compatibility)

None
control_frequency float

Control loop frequency in Hz (default: 50Hz). Must be a positive finite number - it is the divisor of the loop's per-action period (1 / control_frequency), the only throttle between two servo commands. A 0, negative, nan or inf rate raises ValueError here rather than leaving the loop unthrottled against real hardware.

50.0
ros2_bridge bool

When True, publish this robot's live observation (joint_states + one image_raw per camera) on a ROS 2 domain so external ROS 2 nodes can subscribe to the physical robot, and the agent's own use_ros calls reach the same graph. The symmetric counterpart of SimEngine(ros2_bridge=...) for real hardware. Requires rclpy (system ROS 2 / the official docker image); an ImportError is raised here if it is missing. Defaults to False - the robot never touches ROS 2, so disabling the bridge is simply the default (opt-in).

False
ros2_domain int

ROS 2 domain id (ROS_DOMAIN_ID) to publish on. Only an int in [0, 232] names a domain: RTPS derives its discovery ports from it, and 233 lands past the end of the port space.

0
ros2_commands bool

When True (default), the bridge also subscribes to /<robot>/joint_command and forwards inbound messages to send_action so an external ROS 2 stack can drive the real arm (full duplex). Set False for a read-only telemetry bridge. Ignored unless ros2_bridge=True. Only a boolean names a posture: the value is checked, not read by truthiness, so "false" cannot open the surface it asks to close.

True
ros2_transport str

Which ROS 2 backend the bridge uses: "rclpy" (default) - full sensor_msgs fidelity, needs a sourced ROS 2 distro; "rtps" - pure cyclonedds (a single pip wheel, no rclpy / no sourced distro), type coverage bounded by the local IDL bundle (joint_states + image_raw). Both emit the same topics with byte-identical payloads; only the rclpy bridge appears on the ROS 2 graph as a node, since a bare DDS participant carries no node name and no type hash (see docs/reference/ros2/rtps-robot.md). Ignored unless ros2_bridge=True.

'rclpy'
joint_limits dict[str, tuple[float, float]] | None

Optional {"<motor>.pos": (min, max)} clamp ranges threaded into the ROS 2 bridge, keyed by the joint name as it arrives on the wire (the same <motor>.pos names the bridge publishes in joint_states). When set, an inbound joint_command whose ANY joint is outside its declared range is rejected whole (no partial application). Requires ros2_bridge=True.

None
dds_security_config dict[str, str] | None

Optional DDS Security credentials (identity_ca, certificate, private_key, governance, permissions; permissions_ca optional, each a non-empty string) for the pure-RTPS bridge. When ros2_commands=True on the "rtps" transport this (or the STRANDS_ROS2_BRIDGE_I_KNOW_THIS_IS_INSECURE=1 opt-out) is REQUIRED - the bridge refuses to drive the arm over an unsecured DDS graph. Only the "rtps" transport consumes it (rclpy DDS Security is configured via the ROS 2 RMW keystore/env); passing it with ros2_transport="rclpy" raises. Requires ros2_bridge=True.

None
foxglove bool | str

True serves this robot's live observation to Foxglove on ws://127.0.0.1:8765 (the next free port when busy) as per-joint states and one JPEG stream per camera; "host:port" picks the address. The real-arm counterpart of Simulation(foxglove=...), on the same telemetry path as ros2_bridge, and both may be on at once. STRANDS_ROBOTS_FOXGLOVE=1 switches it on for a caller that left this False. Needs the [foxglove] extra. Defaults to False - no socket is opened.

False
foxglove_mcap str | PathLike[str] | None

Path of a new MCAP file the same channels are recorded to. Requires foxglove; an existing file is refused rather than overwritten.

None
foxglove_services bool

When True, advertise the gated strands/set_joint_positions service; each call is refused until STRANDS_FOXGLOVE_COMMAND_ALLOW names it. Default False: the server advertises no capability.

False
**kwargs Any

Robot-specific parameters (port, etc.)

{}

foxglove_url property

foxglove_url: str | None

The live Foxglove WebSocket URL, or None when no Foxglove bridge runs.

foxglove_link: str | None

A foxglove:// deep link to this robot's server, or None.

tool_name property

tool_name: str

The Strands agent-tool name this robot registers itself under.

tool_type property

tool_type: str

The Strands tool category for this device (always "robot").

tool_spec property

tool_spec: ToolSpec

Get tool specification with async actions.

publish_ros_observation

publish_ros_observation(*, skip_images: bool = False) -> dict[str, Any]

Read the robot's current observation once and publish it on ROS 2.

On-demand counterpart to the per-step publishing inside a running task: lets an agent turn an idle, connected robot into a live ROS 2 device without starting a control task. Requires ros2_bridge=True at construction.

Parameters:

Name Type Description Default
skip_images bool

When True, publish joint_states only (opt out of the heavier camera image_raw topics).

False

Returns:

Type Description
dict[str, Any]

{"status": "success", ...} on publish, or

dict[str, Any]

{"status": "error", "content": [...]} when the bridge is

dict[str, Any]

disabled - tools never raise past dispatch.

start_task

start_task(instruction: str, policy_port: int | None = None, policy_host: str = 'localhost', policy_provider: str = 'lerobot_local', duration: float = 30.0, **policy_kwargs: Any) -> dict[str, Any]

Start robot task asynchronously and return immediately.

Parameters:

Name Type Description Default
instruction str

Natural-language instruction passed to the policy.

required
policy_port int | None

Port of the policy server to query. Required when policy_provider dials one: this entry point takes no pre-built policy, so the port is the only thing a policy can be built from. A provider that builds in process declares no port, and supplying one anyway is refused rather than forwarded and dropped. Validated on the shared :func:~strands_robots.utils.tcp_port_error domain before the task is submitted, so a port no policy can be built from is reported here instead of as a started task that connects the arm and then fails on the executor thread.

None
policy_host str

Host of the policy server.

'localhost'
policy_provider str

Provider name used to build the policy.

'lerobot_local'
duration float

Wall-clock budget in seconds. Must be positive and finite; it is validated before the task is submitted, so a budget the loop cannot honor is reported here instead of as a started task that commands nothing (or never ends).

30.0
**policy_kwargs Any

Checkpoint/provider keywords (model_path, policy_type, pretrained_name_or_path, server_address, ...) forwarded to create_policy via :meth:_get_policy. This is the vocabulary the mesh dispatch collects from the wire command; per-provider validity is create_policy's contract, so an unknown keyword is the provider's own refusal to make.

{}

Returns:

Type Description
dict[str, Any]

Tool-shaped result confirming the task started, or an error naming

dict[str, Any]

the offending parameter. A second task is refused while another

dict[str, Any]

rollout is in flight - including during its connect/policy-build

dict[str, Any]

bring-up - because the arm has a single command bus. A robot that

dict[str, Any]

cleanup / stop has shut down is refused permanently: the

dict[str, Any]

executor and bridges are gone, so a rollout started now would

dict[str, Any]

command the arm zero times.

run_policy

run_policy(policy_object: Policy, instruction: str = '', duration: float = 30.0, n_steps: int | None = None) -> dict[str, Any]

Run a pre-built policy object on the real robot (blocking).

Hardware counterpart of Simulation.run_policy(policy_object=...): drive a policy already constructed in-process (e.g. via create_policy(...) around a local checkpoint) without standing up a policy server on a port. Reuses the exact start_task control loop - connect (with half-open rollback), state-key initialization, the RTC control-frequency / observed-delay contract, and resolve_chunk_length chunk consumption - so a pre-built object and a server-backed provider behave identically on the wire.

Blocking: returns when duration elapses, after n_steps applied actions, or when stop_task() is called from another thread. For the server-backed provider path (and for fire-and-forget execution) use start_task.

Exclusive: the arm has a single command bus, so this is refused while another rollout is in flight - including while that rollout is still connecting or building its policy, before it reports RUNNING. Two rollouts admitted at once would interleave send_action writes on one half-duplex bus and share the single task-state slot.

Terminal after shutdown: cleanup / stop release the task executor, the mesh and the ROS bridges and cannot be undone, so a rollout requested afterwards is refused rather than admitted to command the arm zero times. A rollout already running when the shutdown lands is reported STOPPED - a shutdown truncates a task exactly as stop_task does, so its step count is a partial one.

Parameters:

Name Type Description Default
policy_object Policy

A constructed Policy instance. The object's own device / embodiment / chunking configuration is honored; the loop only injects the robot state keys and the RTC control rate, exactly as it does for server-backed policies. Its per-episode state is cleared via Policy.reset at the start of every task, so one object can be reused across tasks without the previous task's cached action chunk being driven.

required
instruction str

Natural-language instruction passed to the policy on every get_actions call.

''
duration float

Wall-clock budget in seconds (same default as start_task). Must be positive and finite; the loop bounds the rollout by it even when n_steps is given, so a value it cannot honor is refused rather than reported as a rollout that commanded nothing.

30.0
n_steps int | None

Optional cap on applied actions (mirrors the sim run_policy parameter); the loop stops at whichever of duration / n_steps comes first. None (the default) requests no cap and leaves duration the sole horizon. Any other value must be a positive integer, the domain the sim horizon and this loop's action_horizon already share: the loop bounds the rollout by step_count < n_steps, so a cap it cannot count against is refused rather than spent on the arm.

None

Returns:

Type Description
dict[str, Any]

Tool-shaped result: a text summary plus a {"json": ...} block

dict[str, Any]

carrying status / steps / duration_s / instruction

dict[str, Any]

/ policy (and error when one occurred), or an error naming

dict[str, Any]

the rollout already in flight.

get_task_status

get_task_status() -> dict[str, Any]

Report the task state, then the device it would drive.

The task machine used to be the whole answer: an arm whose port does not exist on this host read Robot Status: IDLE, byte-identical to a connected, healthy arm at rest, and the difference only surfaced as a connect failure inside the first task. The measured facts were already gathered by :meth:get_status for Python callers; the tool action now prints the same facts under the task state and carries them as a json block.

Returns:

Type Description
dict[str, Any]

status=success with the task state on the first line (as

dict[str, Any]

before), the device lines from :meth:_device_facts after it,

dict[str, Any]

and a second content block holding those facts as JSON.

stop_task

stop_task() -> dict[str, Any]

Stop the current task, including one that is still connecting.

This is the interrupt an operator (or the fleet {"action": "stop"} dispatch, via :class:~strands_robots.mesh.core.Mesh) reaches for, so it has to hold for a task in ANY stage that can still command the arm - not only the one stage whose status happens to be RUNNING.

_execute_task_async sits in CONNECTING for the whole hardware bring-up: a motors-bus handshake plus warmup_s per camera, seconds on a real arm and longer on a multi-camera rig, followed by the policy build. A stop pressed in that window used to be answered with status="success" and "No task running to stop", and the arm then moved anyway once the bring-up finished - the operator was told the interrupt was handled while the rollout it was meant to cancel was still pending.

A status write cannot express the request on its own, because _execute_task_async writes RUNNING once bring-up completes and that overwrites any STOPPED recorded before it. So the request is latched in an event that is set here FIRST, before the status is even read, and cleared only when a new task starts. The rollout honors the latch at each stage boundary and in its loop condition.

Returns:

Type Description
dict[str, Any]

A tool-shaped result confirming the stop, or - for a robot that is

dict[str, Any]

genuinely idle or already in a terminal state - reporting that

dict[str, Any]

there was nothing to stop. Both are status="success": asking an

dict[str, Any]

idle robot to stop is satisfied, not an error.

stream async

stream(tool_use: ToolUse, invocation_state: dict[str, Any], **kwargs: Any) -> AsyncGenerator[ToolResultEvent | ToolInterruptEvent, None]

Stream robot task execution with async actions.

cleanup

cleanup() -> None

Cleanup resources and stop any running tasks.

Terminal: this latches a shutdown, releases the task executor, tears down the mesh and ROS bridges, and disconnects the robot -- the motors bus and every camera -- so no device node stays held. The one exception is a teleop loop that did not join: the devices are left open rather than closed under a live writer, and the reason is recorded at ERROR with the remedy. There is no restart, so run_policy / start_task / the execute action refuse permanently afterwards rather than admit a rollout that would command the arm zero times, and a rollout still in flight when this runs is abandoned at its next stage check -- rather than finishing a bring-up this teardown has already made pointless -- and reported STOPPED rather than COMPLETED. Construct a new Robot to run another task.

get_status async

get_status() -> dict[str, Any]

Report the device's measured state plus this robot's task state.

Two fields answer different questions about cameras, and the pair is what makes an unhealthy device attributable:

  • cameras enumerates the camera names the device is configured for - which image streams exist at all.
  • cameras_connected maps each live camera to its own is_connected reading - which of those streams are actually up.

is_connected on a lerobot arm is bus and all(cameras), so a single dropped camera pulls it to False without naming which of the N+1 facts fell. Reported beside the per-camera readings, a False becomes attributable: a camera reading False is the culprit, and every camera reading True leaves the motor bus as the one by elimination.

Best-effort per camera, mirroring the observation path: a camera whose is_connected probe raises is omitted from cameras_connected rather than failing the whole probe, so a name present in cameras and absent from cameras_connected is one whose state could not be read. That same raise also reaches the arm's aggregate is_connected, which reports None for it rather than degrading this probe to its error shape - so the attribution survives the failure it exists to attribute. A device exposing no live camera objects reports an empty map.

Returns:

Type Description
dict[str, Any]

Status dict of measured facts. An unexpected failure anywhere in

dict[str, Any]

the probe degrades to ``{"robot_name", "error", "is_connected":

dict[str, Any]

False, "task_status": "error"}`` rather than propagating, so a

dict[str, Any]

supervising agent reads a verdict instead of taking an exception.

get_observation

get_observation(robot_name: str | None = None) -> dict[str, Any]

Read the arm once through lerobot: joints and camera frames.

The hardware half of the call mode="sim" answers, so a loop written against the simulation keeps its shape when mode="real". The keys are lerobot's ("<motor>.pos" plus one per camera), not the simulation model's joint names. Connects on first use, as :meth:send_action does, and reads under the bus lock the mesh shares.

Parameters:

Name Type Description Default
robot_name str | None

Ignored (single robot). Present for parity with sim.

None

Returns:

Type Description
dict[str, Any]

The lerobot robot's observation dict.

Raises:

Type Description
Exception

Whatever the connect or the read raised, unchanged - a port that will not open is the caller's to see, not an empty observation.

send_action

send_action(action: dict[str, Any], robot_name: str | None = None) -> dict[str, Any]

Apply a single action to the hardware robot (TeleopMixin contract).

Synchronous so it can be driven from the :class:TeleopMixin teleop loop thread. Ensures the underlying lerobot robot is connected, then delegates through :func:strands_robots.bus_access.write_action, so a teleop or ROS 2 command shares the bus with the mesh's readers instead of racing them. robot_name is accepted for parity with the multi-robot simulation host but ignored here - a hardware Robot wraps exactly one device.

Parameters:

Name Type Description Default
action dict[str, Any]

Flat {motor.pos: float} action dict (lerobot shape).

required
robot_name str | None

Ignored (single robot). Present for mixin parity.

None

Returns:

Type Description
dict[str, Any]

Status dict (success/error) so the teleop loop can count

dict[str, Any]

errors without exceptions tearing down the hot loop.

stop async

stop() -> None

Stop the robot and release everything it holds.

Terminal, exactly as :meth:cleanup is -- this is the async spelling of it, and it delegates every step rather than performing any itself.

The disconnect in particular belongs to that cleanup rather than ahead of it. lerobot gates Robot.disconnect() on is_connected (bus.is_connected and all(cam.is_connected ...)) and raises DeviceNotConnectedError when it is false, so disconnecting here first meant that stopping a robot which was never connected -- or one left half-open by a failed bring-up -- raised before :meth:cleanup was reached. Every terminal guarantee was then silently skipped: no shutdown latch, a task executor still accepting work, and any device a half-open connect had opened still held, with no entry point left that would close it.

Ordering matters as much as reachability. :meth:cleanup closes the devices last, once the teleop loop, the task executor, the mesh and the ROS bridge are all down, because :meth:send_action re-opens the robot lazily on a command that finds it disconnected. Disconnecting here put that close ahead of every one of those command sources.

Runs off the event loop: :meth:cleanup joins the task executor and closes a serial port, both of which block.

start_teleop_publish

start_teleop_publish(teleoperator: Any, device_name: str = 'leader', method: str = 'arm', hz: float = 50.0) -> dict[str, Any]

Start publishing teleoperator actions to the mesh.

This makes the robot a teleop source: another peer on the mesh can call start_teleop_receive(source_peer_id=self.peer_id) to have its hardware follow along.

Parameters:

Name Type Description Default
teleoperator Any

Any object with a callable get_action() -> dict. Typically a lerobot Teleoperator (SOLeader, GamepadTeleop, KeyboardTeleop, Phone). The publish loop polls it every tick, so a device that does not satisfy that contract is refused here rather than on the loop thread - the same domain :meth:attach_teleop grades a locally attached device against.

required
device_name str

Name for this input stream (e.g. "leader", "gamepad").

'leader'
method str

Input method label ("arm", "gamepad", "keyboard", "phone").

'arm'
hz float

Publishing frequency in Hz. Must be a positive finite number; the publish loop's period is 1 / hz.

50.0

Returns:

Type Description
dict[str, Any]

Status dict with topic and peer_id for the receiver to use, or an

dict[str, Any]

error dict when the mesh is inactive, teleoperator cannot be

dict[str, Any]

polled, device_name is not a valid mesh identifier, or hz is

dict[str, Any]

not a rate the publish loop can honor.

stop_teleop

stop_teleop(device_name: str | None = None) -> dict[str, Any]

Stop all or a specific teleop publisher/receiver.

Parameters:

Name Type Description Default
device_name str | None

If provided, stop only the named publisher/receiver. If None, stop all.

None

Returns:

Type Description
dict[str, Any]

Stats from stopped sessions.

strands_robots.hardware_robot.TaskStatus

Bases: Enum

Robot task execution status

strands_robots.hardware_robot.RobotTaskState dataclass

RobotTaskState(status: TaskStatus = TaskStatus.IDLE, instruction: str = '', start_mono: float = 0.0, duration: float = 0.0, step_count: int = 0, error_message: str = '', task_future: Future | None = None, policy: Any = None)

Robot task execution state

Teleoperator

strands_robots.teleoperator.Teleoperator

Teleoperator(name: str, *, id: str | None = None, **kwargs: Any) -> LeRobotTeleoperator

Create a teleoperator (input device) - returns a raw lerobot Teleoperator.

Convenience factory, NOT a wrapper. You get the real lerobot Teleoperator instance back with full access to all its methods.

Unlike :func:Robot, a teleoperator has no "sim" mode - it is always a real input device (or a mock). It is NOT connected here; call .connect() (or attach it to a robot and call robot.teleoperate(), which connects lazily).

Parameters:

Name Type Description Default
name str

Teleoperator type ("so101_leader", "so100_leader", "gamepad", "keyboard", "phone", "koch_leader", ...). Any type registered via lerobot's @TeleoperatorConfig.register_subclass.

required
id str | None

Optional instance identifier. Namespaces the calibration file (e.g. id="blue" -> blue.json). Lets two same-type leaders keep separate calibration.

None
**kwargs Any

Device-specific config forwarded to the resolved lerobot TeleoperatorConfig dataclass iff it declares the field. Common: port= (serial leaders), use_gripper= (gamepad). An unknown kwarg (typo) raises ValueError.

{}

Returns:

Type Description
Teleoperator

A connected-on-demand lerobot Teleoperator instance.

Raises:

Type Description
ValueError

If name is not a registered teleoperator type, or a kwarg is unknown to both the allowlist and the dataclass.

TypeError

If lerobot returns a non-dataclass config class.

Examples::

leader = Teleoperator("so101_leader", port="/dev/ttyACM1", id="blue")
pad = Teleoperator("gamepad", use_gripper=True)
kb = Teleoperator("keyboard")
Edit page