Two scripts, one per machine, joined by a VideoSDK room. The follower runs on the robot. The leader drives it. Already using LeRobot or ROS 2? See the integration quickstarts instead.

Install

Credentials

Both machines need a token and the same room id.
Get a token from app.videosdk.live. See Installation for the full list.

Follower

Runs on the machine with the robot. Three methods: what the joints are called, how to read them, and how to write them.
follower.py
run() joins the room and paces the loop for you. On every pass it applies whatever command arrived, and holds position if the leader goes quiet. Ten times a second it sends the joint positions along with a camera frame. control_hz is the loop rate and observation_hz the publish rate, so an observation goes out every third tick here. slew caps how far a joint may move in one tick. See Configuration for the rest.
Swap the two dictionary lines for your own driver calls and nothing else changes. write_joints() receives values that have already passed the limits and speed cap in SafetyConfig.
Every constructor argument and callback is in the Follower reference, and the safety fields in Configuration.

Leader

Runs on the machine with the controller. Two methods, and no writes.
leader.py
ticks() paces the loop and sends a command on every pass, using whatever read_joints() returns. observation() gives you the latest from the follower: a camera frame, and the joint positions from the moment that frame was taken.
The follower applies nothing until claim_control() succeeds and the deadman is held. On real hardware, set max_misalignment so the arm refuses to arm while the two poses disagree.
Control, arming and the fields on a synced observation are in the Leader reference.

Run it

Two terminals, both with the token and the room id exported.

Output

  • holding (starvation) is the starting state. The follower applies nothing until the leader claims control.
  • applied 40 <- observation 302 pairs each command with the observation the operator was acting on. That pairing is what a recording keeps.
  • joints={} on the first tick means nothing has arrived from the follower yet. It fills in on the next observation.
  • cameras=[] because this run had no camera. With one configured you see its label here, and obs.images carries a frame for each.
Seeing holding (starvation) again mid-run is the watchdog. Commands stopped arriving for longer than watchdog_timeout_s, so the arm held position until they resumed. Stop the leader entirely and it stays there.
New to the terms used here? Concepts explains how a session works, and the Python SDK reference lists every class and argument.

Record a dataset

Add a path and the session is written to disk as you drive.
record= attaches a recorder when the session starts, and stop() closes it. To control that yourself, leave it out and call start_recording() and stop_recording() instead. See Recording for the format and the LeRobot export.

Integration quickstarts

If your robot is already on one of these, start there instead.

LeRobot

Pass your existing LeRobot robot and teleoperator to the adapters. They work out the joints and units for you, so there is no class to write.

ROS 2

Run a bridge node on each host, beside the drivers you already have. Your drivers keep the hardware.

Next steps

Follower

Every constructor argument, callback and method on the robot side.

Leader

Control, arming, and the synced observation.

Safety

The checks a command passes before it reaches the motors.

Custom robot

The full version of what this page showed, including cameras and telemetry.