Skip to content

Drivers

How Robot(name, mode="real") picks its driver, the native drivers that ship, and the contract a driver implements.

How Robot(name, mode="real") picks a driver, the 19 native drivers that ship and their wires, and the contract a driver implements.

from strands_robots.drivers import list_native_drivers, list_driver_coverage

print(list_native_drivers()["so101"])            # FeetechDriver
print(list_driver_coverage()["so101"])           # ('lerobot', 'strands')
print(list_driver_coverage()["omx"])             # ('lerobot',)
print(list_driver_coverage()["ability_hand"])    # ()

Two driver families

Top, the one green element: one interface, get_observation, send_action, run_policy and the agent tool. Two dashed layers under it. mode="sim": MuJoCo on the CPU, the default; Newton and Isaac Sim on a GPU; one registry entry and the same MJCF assets, nothing gated in simulation; a twin card, transport="twin", the native driver steps the MuJoCo model instead of a bus. mode="real": the lerobot driver, the default, or a native driver; under them the transports serial, TCP, DDS and ROS 2, and under those the motors, the only thing the gate protects. Footnote: the call does not change; what refuses it does.Top, the one green element: one interface, get_observation, send_action, run_policy and the agent tool. Two dashed layers under it. mode="sim": MuJoCo on the CPU, the default; Newton and Isaac Sim on a GPU; one registry entry and the same MJCF assets, nothing gated in simulation; a twin card, transport="twin", the native driver steps the MuJoCo model instead of a bus. mode="real": the lerobot driver, the default, or a native driver; under them the transports serial, TCP, DDS and ROS 2, and under those the motors, the only thing the gate protects. Footnote: the call does not change; what refuses it does.
driver= builds for
"strands" (NATIVE_DRIVER, what auto picks when one is registered) the native driver class registered for the robot the robots in the table below
"lerobot" (DEFAULT_DRIVER, what auto falls back to) strands_robots.hardware_robot.Robot around a lerobot robot class any robot whose registry entry has hardware.lerobot_type (so101_follower, koch_follower, lekiwi, bi_so_follower, ...)
"auto" (the default) the registry's hardware.driver if set, else a registered native driver, else lerobot everything

Robots lerobot has no type for (unitree_go2, robotiq_2f85, reachy_mini, microduck, booster_t1, crazyflie, yahboom_m3pro) declare hardware.driver = "strands"; other robots in the table below need none: Robot("so101", mode="real", port="/dev/ttyACM0") builds FeetechDriver with no lerobot extra, with the arm's lerobot calibration. omx, openarm and reachy2 have no native driver and use lerobot; driver="lerobot" pins that path, and earthrover declares it for teleop reads. driver="strands" on a robot with no native driver is refused by name.

port= is a Feetech serial path, a controller IP, a radio:// URI for a Crazyflie, host:port for a daemon. A keyword the driver does not declare is refused.

Shipped native drivers

Generated from _SHIPPED_DRIVERS and each module's SUPPORTED_ROBOTS:

robot driver class transport install setup
so100 FeetechDriver serial (Feetech SCS bus) [serial] feetech-arms
so101 FeetechDriver serial (Feetech SCS bus) [serial] feetech-arms
hope_jr FeetechDriver serial (Feetech SCS bus) [serial] feetech-arms
open_duck_mini FeetechDriver serial (Feetech SCS bus) [serial] feetech-arms
koch DynamixelDriver serial (Dynamixel Protocol 2.0) [serial] feetech-arms
panda FrankaDriver ethernet (FCI, libfranka) panda-py, vendor wheel franka
fr3 FrankaDriver ethernet (FCI, libfranka) panda-py, vendor wheel franka
fr3_v2 FrankaDriver ethernet (FCI, libfranka) panda-py, vendor wheel franka
g1 G1Driver DDS (CycloneDDS) [ros2] + unitree_sdk2_python unitree
unitree_g1 G1Driver DDS (CycloneDDS) [ros2] + unitree_sdk2_python unitree
unitree_go2 Go2Driver DDS (CycloneDDS) [ros2] + unitree_sdk2_python unitree
unitree_h1 Go2Driver DDS (CycloneDDS) [ros2] + unitree_sdk2_python unitree
unitree_h1_2 Go2Driver DDS (CycloneDDS) [ros2] + unitree_sdk2_python unitree
b2 Go2Driver DDS (CycloneDDS) [ros2] + unitree_sdk2_python unitree
reachy_mini ReachyDriver http + WebSocket (reachy daemon) pip install websockets reachy-mini
microduck MicroduckDriver unix socket, JSON-RPC (robotd) base install, ssh for a remote duck microduck
robotiq_2f85 RobotiqDriver ethernet (Modbus TCP) base install drivers
robotiq_2f85_v4 RobotiqDriver ethernet (Modbus TCP) base install drivers
booster_t1 BoosterDriver DDS (vendor SDK) booster_robotics_sdk_python, vendor wheel booster-t1
rby1 RBY1Driver gRPC (rby1-sdk) [rby1] drivers
ur3e URDriver ethernet (RTDE, port 30004) [ur] ur
ur5e URDriver ethernet (RTDE, port 30004) [ur] ur
ur7e URDriver ethernet (RTDE, port 30004) [ur] ur
ur10e URDriver ethernet (RTDE, port 30004) [ur] ur
ur12e URDriver ethernet (RTDE, port 30004) [ur] ur
ur16e URDriver ethernet (RTDE, port 30004) [ur] ur
ur8long URDriver ethernet (RTDE, port 30004) [ur] ur
ur15 URDriver ethernet (RTDE, port 30004) [ur] ur
ur18 URDriver ethernet (RTDE, port 30004) [ur] ur
ur20 URDriver ethernet (RTDE, port 30004) [ur] ur
ur30 URDriver ethernet (RTDE, port 30004) [ur] ur
xarm7 XArmDriver ethernet (xArm TCP) [xarm] drivers
kinova_gen3 KinovaDriver ethernet (Kortex TCP) kortex_api, vendor wheel drivers
kuka_iiwa KukaDriver ethernet (FRI over UDP 30200) pyfri, built from source drivers
stretch StretchDriver USB on the robot (stretch_body) [stretch], on the robot drivers
stretch3 StretchDriver USB on the robot (stretch_body) [stretch], on the robot drivers
spot SpotDriver gRPC (bosdyn-client) [spot] drivers
crazyflie CrazyflieDriver radio (CRTP over Crazyradio) [crazyflie] drivers
earthrover EarthRoverDriver http (earth-rovers-sdk) [earthrover] drivers
yahboom_m3pro YahboomM3ProDriver rosbridge WebSocket / rclpy / twin [rosbridge] or [ros2] drivers

FeetechDriver

Speaks Feetech STS/SMS serial bus. Source: strands_robots/drivers/feetech/driver.py.

Port, SDK, kwargs and checks
port= serial device of the SCS bus, for example "/dev/ttyACM0" or "/dev/tty.usbserial-*"
SDK pip install 'strands-robots[serial]'; calibration file from lerobot-calibrate
Other kwargs baud_rate=1_000_000, calibration=<path or records>, motor_ids=(), timeout=1.0, transport="serial" or "twin"
Action keys degrees per joint, gripper in percent open; keys shoulder_pan or shoulder_pan.pos

Checks before it writes:

  • the bus opens the port and discovers the servo ids on connect
  • without calibration= the driver reads and commands the servo's full travel, not the arm's measured travel; get_status reports calibration_source
  • stop releases torque on every motor and names any that stayed driven
  • transport="twin" answers the same verbs from the arm's MuJoCo model

DynamixelDriver

Speaks Dynamixel Protocol 2.0 serial bus. Source: strands_robots/drivers/dynamixel/driver.py.

Port, SDK, kwargs and checks
port= serial device of the U2D2 or bus adapter, for example "/dev/ttyUSB0"
SDK pip install 'strands-robots[serial]'; calibration file from lerobot-calibrate (koch_follower)
Other kwargs baud_rate=1_000_000, calibration=<path or records>, motor_ids=(), timeout=1.0
Action keys degrees per joint, gripper in percent open; keys shoulder_pan or shoulder_pan.pos

Checks before it writes:

  • the arm keeps the operating modes lerobot-calibrate wrote; the driver does not rewrite EEPROM
  • a reply whose error byte carries an error number is dropped; the hardware-alert bit alone is not
  • stop releases torque on every motor and names any that stayed driven

FrankaDriver

Speaks Franka Control Interface (FCI) through panda-py. Source: strands_robots/drivers/franka/driver.py.

Port, SDK, kwargs and checks
port= IP address of the arm's control box
SDK panda-py (pip install panda-python); resolved on connect, never at import
Other kwargs speed_factor=0.2, stream_rate_hz=30.0
Action keys radians; Panda joints joint1..joint7, FR3 fr3_joint1.., FR3 v2 fr3v2_joint1.., the names the arm's own MuJoCo asset uses

Checks before it writes:

  • a motion command is refused unless the driver is connected, every joint named belongs to this arm, every value is finite, and all seven joints are given
  • no 1 kHz torque loop: joint motion goes through panda-py's guarded motion generator, which owns the realtime context
  • state is sourced at 1000 Hz and downsampled to stream_rate_hz; the stride is reported

G1Driver

Speaks CycloneDDS through unitree_sdk2py. Source: strands_robots/drivers/g1.py.

Port, SDK, kwargs and checks
port= the robot's IP, recorded for logging; DDS binds to network_interface
SDK pip install 'strands-robots[ros2]' then git clone https://github.com/unitreerobotics/unitree_sdk2_python and pip install --no-deps -e ./unitree_sdk2_python
Other kwargs network_interface="eth0", battery_floor_pct=15.0
Action keys radians, keyed by the 29 joint names of the unitree_g1 model

Checks before it writes:

  • send_action refuses unless the high-level FSM id is one of 500, 501, 801 (HANDSHAKE_FSMS)
  • send_action refuses under the battery floor, as a separate refusal
  • run_policy rolls a built policy on a 500 Hz thread with a per-step re-gate and a zero-torque frame on exit; start_task refuses by name

Go2Driver

Speaks CycloneDDS through unitree_sdk2py. Source: strands_robots/drivers/go2.py.

Port, SDK, kwargs and checks
port= the robot's IP, recorded for logging; DDS binds to network_interface
SDK pip install 'strands-robots[ros2]' then git clone https://github.com/unitreerobotics/unitree_sdk2_python and pip install --no-deps -e ./unitree_sdk2_python
Other kwargs network_interface="eth0", battery_floor_pct=15.0
Action keys radians, keyed by joint name (GO2_JOINT_INDEX, H1_JOINT_INDEX, H1_2_JOINT_INDEX); an index is never accepted, because the SDK's motor order differs from the model's

Checks before it writes:

  • send_action refuses until release_sport_mode() has confirmed the onboard sport service is released
  • send_action refuses under the battery floor
  • rt/lowcmd frames are unitree_go structs; a unitree_hg frame fails CRC and is dropped by the robot

ReachyDriver

Speaks Reachy daemon REST API plus its real-time link. Source: strands_robots/drivers/reachy.py.

Port, SDK, kwargs and checks
port= daemon host, optionally with a port: "reachy-a.local" or "reachy-a.local:8000"
SDK none; REACHY_HOST/REACHY_PORT are read when port is omitted, then localhost and reachy-mini.local are probed
Other kwargs api_port=8000, media_port=8443, tts_url=None
Action keys head pose and antennas inside the shared envelope; a write outside it is refused naming the limit

Checks before it writes:

  • connect_eagerly probes GET /api/daemon/status, which also reports Lite or Wireless hardware
  • _imu, _pose and _battery are cached from the daemon link and published by the mesh when present

MicroduckDriver

Speaks robotd JSON-RPC over a unix socket. Source: strands_robots/drivers/microduck.py.

Port, SDK, kwargs and checks
port= a unix socket path, or "ssh://[user@]host" to have the driver forward the duck's socket
SDK none; MICRODUCK_SOCKET, then MICRODUCK_HOST, then /run/robotd.sock are tried when port is omitted
Other kwargs api_version=31, timeout=5.0, subscribe_hz=None
Action keys intents: robot.move (twist), robot.head, robot.pose, robot.do (skills), robot.enable/robot.relax

Checks before it writes:

  • robotd exposes no per-joint write, so run_policy and start_task refuse and name the intent path; the on-robot policy is the same alpha_walking.onnx the sim runs
  • continuous intents go as JSON-RPC notifications; discrete ones wait for a reply

RobotiqDriver

Speaks Modbus TCP. Source: strands_robots/drivers/robotiq/driver.py.

Port, SDK, kwargs and checks
port= the gripper's IP address or hostname
SDK none; the codec is strands_robots.drivers.robotiq.protocol
Other kwargs tcp_port=502, unit_id=9 (often 0 behind a UR controller), stroke_mm=85.0, speed=1.0, force=1.0
Action keys gripper or gripper.pos as a closed fraction 0.0 to 1.0, or position/aperture_mm in millimetres

Checks before it writes:

  • connect_eagerly activates the gripper and waits for gSTA == ACTIVE; a 2F-85 ignores every position command until then
  • send_action refuses while the gripper is not activated
  • start_task and run_policy refuse: a 1-DOF end effector is commanded as one dimension of the arm's action

BoosterDriver

Speaks Booster SDK (booster_robotics_sdk_python, DDS). Source: strands_robots/drivers/booster.py.

Port, SDK, kwargs and checks
port= the robot's IP address; an empty string discovers on the default interface
SDK pip install booster_robotics_sdk_python (vendor wheel, imported on connect)
Other kwargs domain_id=0, robot_name=None, cmd_type="parallel"
Action keys the eight upper-body joints only; head and legs through rotate_head and move

Checks before it writes:

  • send_action refuses until enable_upper_body() has handed the upper body to the host
  • every non-upper-body slot is sent q=0, kp=0, kd=0 so the onboard controller keeps the legs
  • a write is refused while the fall state is anything but IS_READY

RBY1Driver

Speaks gRPC through rby1-sdk. Source: strands_robots/drivers/rby1.py.

Port, SDK, kwargs and checks
port= the robot's ip:port
SDK pip install 'strands-robots[rby1]'
Other kwargs control_frequency=50.0, priority=1
Action keys radians, torso, arms and head; no wheels or grippers

Checks before it writes:

  • a pressed e-stop or faulted control manager is refused before power-on
  • range and per-step speed are gated on the robot's own dynamics model
  • stop cancels control; the next write opens a new stream

StretchDriver

Speaks USB through stretch_body, on the robot's own computer. Source: strands_robots/drivers/stretch.py.

Port, SDK, kwargs and checks
port= none: the SDK opens the robot's /dev/hello-* devices; a port is refused
SDK pip install 'strands-robots[stretch]' (preinstalled on the robot)
Other kwargs control_frequency=15.0
Action keys lift, arm in m; wrist and head in rad; the base through set_twist(vx, wz); no gripper

Checks before it writes:

  • a goal outside the joint's current soft limits is refused, not clipped as move_to would
  • writes are refused while the runstop is latched or the robot is not homed
  • a twist that would clamp a wheel, strafe, or run without the wheels' watchdog is refused

URDriver

Speaks RTDE through ur_rtde. Source: strands_robots/drivers/ur.py.

Port, SDK, kwargs and checks
port= controller IP or hostname, optionally with ":30004"
SDK pip install 'strands-robots[ur]' (rtde_control, rtde_receive)
Other kwargs model=None, control_frequency=125.0, rtde_frequency=None
Action keys radians, shoulder_pan_joint .. wrist_3_joint, the order the MuJoCo assets and the RTDE wire share

Checks before it writes:

  • the receive interface opens first: a controller in PROTECTIVE_STOP accepts a connection and performs no motion
  • send_action maps onto servoJ, gated on the controller mode and on the size of the step
  • off hardware every read returns its cache and every write refuses not connected

SpotDriver

Speaks gRPC through bosdyn-client. Source: strands_robots/drivers/spot.py.

Port, SDK, kwargs and checks
port= robot hostname or IP; credentials from BOSDYN_CLIENT_USERNAME / BOSDYN_CLIENT_PASSWORD
SDK pip install 'strands-robots[spot]'
Other kwargs control_frequency=10.0
Action keys arm arm_sh0 .. arm_wr1 and claw arm_f1x in rad; the base through set_twist(vx, vy, wz); legs read only

Checks before it writes:

  • every command is refused while E-stopped or with the motors off; stand() powers on
  • a leg joint is refused (the locomotion controller owns it), and an arm target outside its limits
  • a twist expires after 0.6 s; cleanup powers off safely and returns the lease

XArmDriver

Speaks TCP through xarm-python-sdk. Source: strands_robots/drivers/xarm.py.

Port, SDK, kwargs and checks
port= controller IP
SDK pip install 'strands-robots[xarm]'
Other kwargs control_frequency=100.0
Action keys radians, joint1 .. joint7; no gripper yet

Checks before it writes:

  • a controller holding an error code is refused before it is energised
  • send_action is set_servo_angle_j, refused past the reported joint speed limit
  • stop halts and re-arms servo mode

KinovaDriver

Speaks Kortex TCP through kortex_api. Source: strands_robots/drivers/kinova.py.

Port, SDK, kwargs and checks
port= base IP; username=/password= for the session
SDK Kinova's kortex_api wheel, pip install --no-deps, run with PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
Other kwargs control_frequency=40.0
Action keys radians, joint_1 .. joint_7; no gripper

Checks before it writes:

  • a base in fault, or not ARMSTATE_SERVOING_READY, is refused at connect and on every write
  • send_action becomes joint speeds, refused past 0.8727 rad/s in one period
  • a stream quiet for three periods is stopped: Kortex joint speeds never expire

KukaDriver

Speaks FRI over UDP through pyfri, in its own process. Source: strands_robots/drivers/kuka.py.

Port, SDK, kwargs and checks
port= controller (KONI) IP to accept, or none for any; fri_port=30200
SDK lbr-stack's pyfri, built from source against pybind11 2.13 or newer
Other kwargs control_frequency=50.0, connect_timeout=5.0
Action keys radians, joint1 .. joint7

Checks before it writes:

  • writes only in COMMANDING_ACTIVE, POSITION mode, normal safety, drives active
  • targets outside the iiwa 14 range or past its joint speed in one period are refused
  • each FRI cycle moves at most joint speed times sample time; a halt holds the last command

CrazyflieDriver

Speaks CRTP over a Crazyradio through cflib. Source: strands_robots/drivers/crazyflie.py.

Port, SDK, kwargs and checks
port= "radio://<dongle>/<channel>/<rate>/<address>" or "usb://0"
SDK pip install 'strands-robots[crazyflie]'
Other kwargs setpoint_hz=20
Action keys twists in SI (wz in rad/s, converted to the wire's degrees per second in one place)

Checks before it writes:

  • a setpoint is a subscription: the driver re-sends the last accepted setpoint at setpoint_hz because the firmware cuts thrust when the stream goes quiet
  • send_action returns once the setpoint is latched, not when motion ends
  • stopping and landing are different verbs

EarthRoverDriver

Speaks HTTP to the vendor earth-rovers-sdk. Source: strands_robots/drivers/earthrover.py.

Port, SDK, kwargs and checks
port= "http://host:8000" or a bare "host:port"
SDK pip install 'strands-robots[earthrover]' plus the SDK process on the host
Other kwargs timeout_s=10.0, turn_sign=1.0
Action keys linear, angular, lamp, each normalised to [-1, 1]

Checks before it writes:

  • POST /control carries one twist frame; GET /data is the telemetry snapshot; GET /v2/front and /v2/rear are the cameras
  • turn_sign=-1.0 corrects a rover observed turning the wrong way, at the call site

YahboomM3ProDriver

Speaks the robot's ROS 2 graph, over rosbridge or in-process rclpy. Source: strands_robots/drivers/yahboom_m3pro.py.

Port, SDK, kwargs and checks
port= "host[:port]" of rosbridge_server, default "localhost:9090"; ignored by transport="ros2"
SDK pip install 'strands-robots[rosbridge]' from any host, or rclpy on the robot (ROS_DOMAIN_ID=30 on the shipped image)
Other kwargs transport="rosbridge", "ros2" or "twin"; timeout_s=5.0, joint_signs=(1.0,)*5, move_time_ms=1500
Action keys arm1.pos .. arm5.pos and gripper.pos in radians (the model's vocabulary), converted to servo degrees at the wire; base twists in SI

Checks before it writes:

  • every /cmd_vel write passes the operator gate: approved by the agent's operator or pre-approved with STRANDS_ROS2_COMMAND_ALLOW=/cmd_vel
  • the firmware zeroes the base after 200 to 500 ms without a message, so a held move is a 10 Hz stream and an explicit zero
  • get_observation returns {} on the robot: the board publishes no arm joint-state topic

Native drivers import their SDK in connect_eagerly(), naming the install line if missing; unconnected, get_observation() logs why it is {}.

The contract

A native driver is anything with these members (HardwareDriver is a runtime_checkable Protocol; no inheritance needed):

member role
tool_name, tool_type, tool_spec, stream the Strands AgentTool surface, so Agent(tools=[robot]) works
send_action(action, robot_name=None) one command, keyed by this driver's joint names; returns a status envelope
run_policy(policy, ...), get_task_status(), stop_task(); start_task(instruction, policy_provider=...) builds the policy in the driver and is removed in 0.8 the policy rollout path
get_status() (async), stop() (async) health and de-energise
cleanup() release the transport

Constructor: driver_cls(tool_name=..., cameras=..., data_config=..., **kwargs). A driver that wants a cameras= dict sets reads_cameras = True; otherwise a non-empty cameras= is refused, not dropped.

Optional: get_observation and the sensor attributes (_pose, _imu, _battery, _lidar_state), which the mesh reads with getattr, so a driver without an IMU publishes no IMU topic. Joint telemetry needs a bus with sync_read or a get_observation, plus is_connected.

A refusal has the same envelope shape as a success:

from strands_robots import Robot

sim = Robot("so101")
arm = Robot("so101", mode="real", driver="strands", transport="twin", sim=sim)
arm.connect_eagerly()
print(arm.send_action({"shoulder_pan": 10.0}))
# {'status': 'success', 'content': [{'json': {'commanded': {'shoulder_pan': 10.0}, 'unit': 'degrees (gripper: percent open)'}}]}
arm.cleanup()

The real FeetechDriver, its MuJoCo model on the bus: verbs, units and refusals without a serial port.

Register your own

from strands_robots.drivers import register_native_driver

register_native_driver("koch_follower", MyKochDriver)   # refuses a class missing a contract member
robot = Robot("koch_follower", mode="real", driver="strands", port="/dev/ttyUSB0")

register_native_driver binds a driver class to a registry robot name, as its default, after missing_driver_members(cls) passes; re-registering needs overwrite=True. For an unregistered robot, call register_robot("my_arm", model_xml=..., hardware={"driver": "strands"}) first.

Where the gates are

A driver refuses before writing: the Feetech bus past servo travel, the G1 outside its FSM handshake or under 15% battery, the Go2 before sport release, the Booster T1 before upper-body control, the Robotiq before activation, the UR in PROTECTIVE_STOP, the xArm or Gen3 on a fault, the iiwa outside FRI commanding, the Stretch past SDK clipping. Above them sits the operator gate.

Next: feetech-arms, teleoperation, cameras, calibration.

Edit page