Leader is the base class for the machine that drives. Subclass it to read
your own controller, and the SDK handles the session, the lease, video sync and
recording.
Constructor
str | None
The room the follower is in. Both ends must join the same room.
str
default:"VIDEOSDK_TOKEN"
VideoSDK token.
int
default:"50"
The rate
run() paces at. Heartbeats flow at this rate whether or not the
deadman is held.int
default:"10"
Heartbeat rate. Must be comfortably faster than the follower’s
watchdog_timeout_s. Heartbeats flow whether or not the deadman is held, so
the follower can tell “operator let go” from “link died”.float
default:"120.0"
How long each video frame is held so state samples can arrive either side of
it.
0 means align on arrival, never “skip alignment”. See
Sync.bool
default:"True"
Receive the follower’s video.
False sends and receives joints only, for a
controller that never shows a frame, and saves the cost of decoding video.
observation() still pairs joints with frames whenever there
are frames, and returns images == {} when there are none.float | None
default:"None"
Refuse to arm while any joint differs from the follower by more than this, in
descriptor units. Set it on real hardware. See
arm_when_aligned().float
default:"1.0"
How often this leader publishes its own stats.
0 stops publishing them, and
get_stats() still works. Mirrors the follower’s stats_hz.str
default:"operator"
Display name in the meeting.
object
Use a transport of your own instead of joining a room. This is how the
loopback harness runs a whole session with no network.
Running it
Call one of these from your own program. Both pace the loop for you.leader.run(hz=30) is the same three steps in one call, for when there is
nothing to read each period. On the leader there usually is, so ticks() is
the one you want.
Already have a scheduler, such as a ROS 2 timer? Call
leader.tick() from it
instead and skip both.Methods
You implement
returns RobotDescriptor
The joint schema, matching the follower’s. Called once, at construction.
See
RobotDescriptor.returns Mapping[str, float]
Where the operator’s controller is right now. Called every tick.
read_joints() is whatever the controller is: a backdrivable arm, a VR headset
pose, a simulator, or a policy’s output.
You call
Provided by the base class.Taking control
returns bool
Ask the follower for control and wait for its answer.
True means you have
it, False means you were refused or nobody answered.returns None
Give control back. The follower stops applying your commands.
returns None
True starts sending commands, False stops. Releasing is never refused.With max_misalignment set, hold_deadman(True) raises NotAligned when the
worst joint is further out than that, or when no follower pose has arrived
yet. The check lives here as well as in arm_when_aligned().returns None
Stop the follower now, on a channel of its own so it cannot queue behind
video.
Starting to drive
returns None
Wait until the two arms match, hold still for
hold_s, then start sending.
Holding the pose is the trigger, since a leader arm falls the moment the
operator lets go to press a key.Every argument is keyword only. on_status runs each poll if you want to show
progress, and timeout raises NotAligned instead of waiting forever.returns dict[str, float] | None
How far each joint is from the follower’s, leader minus follower, in
descriptor units.
None until the follower’s pose is known.These compare positions, so they fit an arm driven by another arm, a VR
hand or a wearable controller. For a base or a drone, which report speed
rather than position, leave
max_misalignment unset and call
hold_deadman(True) directly.NotAligned, so catch it if you want to tell the operator rather
than exit:
Observing
returns SyncedObservation
The latest frame from the follower with the joint positions from the instant
that frame was taken. Until video is up you get joints and
images == {}, which is how you know it is not ready yet. See
Observation.Recording
returns EpisodeRecorder
Attach a recorder. Does not open an episode.
returns str | None
Begin one recorded demonstration, ending any open episode first.
None if
there is no recorder.returns None
Close the episode.
success is True, False, or None for “not judged”.returns None
Close the recorder and detach it.
Episodes do not open on their own here, so call
start_episode() and
end_episode() yourself. The follower does that from the deadman.Monitoring
returns list[dict]
This leader’s own health report: transport, latency and session. Same shape as
the follower’s, so a collector scrapes both unchanged.Named
get_stats() here and stats() on the
Follower. Two spellings, one
contract. leader.stats() does not exist.Tracking error, clamps and loop jitter are not in here. They belong to the
machine with the motors and arrive on
remote_stats from its own report,
rather than being guessed at from this end.Properties
Acknowledgement and sync quality:
seq - applied_seq is how many commands are in flight or were dropped. Both
numbers come from this side, so the gap needs no clock sync.
cameras stays empty on a follower that has no cameras, so it is a “the far
machine spoke” signal only when there are cameras to announce.
on_observation is the unconditional one.Events
once, at the end of start()
The transport is up.
descriptor is this leader’s own, not the follower’s,
so it says nothing about the far machine yet.For that, watch on_observation for the first state frame, or read
cameras, which stays empty until the follower’s first
announce arrives.on grant
You have control.
on each report from the follower
The arm published its stats. The same list
remote_stats holds.per frame joined to state
A video frame was matched to the joint values from its own capture instant.This one runs on the media thread, so stash the frame and return. Decoding or
writing to disk here holds up all video. Unlike
observation(), it does not
stamp the next command.on refusal
Your claim was refused, or the joints did not match. It does not fire when
claim_control() simply times out, so a False return covers both refused
and never answered.per new observation
A newer observation arrived from the follower.
Keep callbacks short. Anything slow in one holds up the session, so hand the
work off rather than doing it here.
Example
A controller on a generic device, driving one follower.- Open your device before
super().__init__(). The leader has noconnect()hook, so this is the only place to do it. max_misalignmentstops the follower moving suddenly to meet a controller that is somewhere else.- Check what
claim_control()returns.Falsemeans you do not have control, and commands go nowhere. arm_when_aligned()is what starts the commands. Nothing moves before it.
Related
Follower
The other half of a session.
Observation
What observation() hands back, field by field.
Adapters
The LeRobot and ROS 2 subclasses that ship with the SDK.
Sync
How joints and frames are matched to the same instant.