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¶
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_statusreportscalibration_source stopreleases torque on every motor and names any that stayed driventransport="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-calibratewrote; the driver does not rewrite EEPROM - a reply whose error byte carries an error number is dropped; the hardware-alert bit alone is not
stopreleases 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_actionrefuses unless the high-level FSM id is one of500,501,801(HANDSHAKE_FSMS)send_actionrefuses under the battery floor, as a separate refusalrun_policyrolls a built policy on a 500 Hz thread with a per-step re-gate and a zero-torque frame on exit;start_taskrefuses 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_actionrefuses untilrelease_sport_mode()has confirmed the onboard sport service is releasedsend_actionrefuses under the battery floorrt/lowcmdframes areunitree_gostructs; aunitree_hgframe 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_eagerlyprobesGET /api/daemon/status, which also reports Lite or Wireless hardware_imu,_poseand_batteryare 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:
robotdexposes no per-joint write, sorun_policyandstart_taskrefuse and name the intent path; the on-robot policy is the samealpha_walking.onnxthe 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_eagerlyactivates the gripper and waits forgSTA == ACTIVE; a 2F-85 ignores every position command until thensend_actionrefuses while the gripper is not activatedstart_taskandrun_policyrefuse: 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_actionrefuses untilenable_upper_body()has handed the upper body to the host- every non-upper-body slot is sent
q=0, kp=0, kd=0so 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
stopcancels 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_towould - 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_STOPaccepts a connection and performs no motion send_actionmaps ontoservoJ, 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;
cleanuppowers 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_actionisset_servo_angle_j, refused past the reported joint speed limitstophalts 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_actionbecomes 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,POSITIONmode, 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_hzbecause the firmware cuts thrust when the stream goes quiet send_actionreturns 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 /controlcarries one twist frame;GET /datais the telemetry snapshot;GET /v2/frontand/v2/rearare the camerasturn_sign=-1.0corrects 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_velwrite passes the operator gate: approved by the agent's operator or pre-approved withSTRANDS_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_observationreturns{}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