> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroruntime.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# LeRobot

> Two adapters that wrap a LeRobot SO-101 robot and teleoperator.

`zeroruntime.teleops.adapters.lerobot` gives you two subclasses, one per machine:

| Class | Runs on | Wraps |
| - | - | - |
| `SO101FollowerAdapter` | the robot host | a LeRobot `Robot`, the arm that moves |
| `SO101LeaderAdapter` | the operator host | a LeRobot `Teleoperator`, the arm a human backdrives |

## Install

```bash theme={null}
pip install "zeroruntime-teleops[lerobot]"
```

That brings in [LeRobot](https://huggingface.co/docs/lerobot/index) and the
servo backend for the SO-101. For LeRobot hardware on a different bus, install
that backend from LeRobot too.

This extra needs Python 3.12 or later, which is stricter than the base package.

You also need a [access token](https://app.videosdk.live/signup) and one room
id both machines join. `MEETING_ID` below is that room id, read from the
environment along with the token:

```bash theme={null}
export ZERORUNTIME_AUTH_TOKEN="your-token-here"
export ZERORUNTIME_MEETING_ID="abcd-efgh-ijkl"
```

Every constructor argument on both adapters is in the
[Adapters reference](/robotics/api-reference/python/adapters/lerobot).

## Robot host

The machine with the follower arm.

```python theme={null}
import zeroruntime.teleops as teleop
from zeroruntime.teleops.adapters.lerobot import SO101FollowerAdapter
from lerobot.cameras.opencv import OpenCVCameraConfig
from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig

arm = SO101Follower(SO101FollowerConfig(
    id="so101_follower", port="/dev/ttyACM0",
    cameras={"wrist": OpenCVCameraConfig(index_or_path=0,
                                         fps=30, width=640, height=480),
             "front": OpenCVCameraConfig(index_or_path=2,
                                         fps=30, width=640, height=480)}))

follower = SO101FollowerAdapter(
    MEETING_ID, robot=arm,
    safety=teleop.SafetyConfig(slew=12.0, watchdog_timeout_s=0.5),
    record="./sessions")

@follower.on_safe_state
def _(state, reason):
    print(f"[safety] {state.value}" + (f" ({reason.value})" if reason else ""))

follower.run(hz=30)
```

Nothing moves until an operator claims control and holds the deadman.

<Note>
  Configure cameras on the LeRobot object, not on the adapter. LeRobot's
  `get_observation()` returns joints and images in one call, and the adapter
  serves both from it.
</Note>

## Operator host

The machine with the leader arm.

```python theme={null}
from zeroruntime.teleops.adapters.lerobot import SO101LeaderAdapter
from lerobot.teleoperators.so_leader import SO101Leader, SO101LeaderConfig

leader = SO101LeaderAdapter(
    MEETING_ID,
    teleop=SO101Leader(SO101LeaderConfig(id="so101_leader",
                                         port="/dev/ttyACM1")),
    sync_buffer_ms=120.0, max_misalignment=20.0)

leader.start()
leader.claim_control()
leader.arm_when_aligned(hold_s=2.0)

for i in leader.ticks(hz=30):       # ticks() paces AND ticks for you
    obs = leader.observation()      # obs.joints and obs.images, one instant
    render(obs.images.get("wrist"))
```

`leader.run(hz=30)` does the same when the operator does not need to see the
frames.

<Warning>
  **`limits()` returns `{}` on these adapters, so position clamping is off and
  `slew` is the only shaping the SDK applies.** Tune it on your own hardware,
  and see [Safety](/robotics/concepts/safety) for what the remaining checks cover.
</Warning>

## Cameras

Each camera travels as its own track, so `observation()` returns one array per
camera:

```python theme={null}
obs.images    # {"wrist": ndarray(480, 640, 3), "front": ndarray(480, 640, 3)}
obs.joints    # {"shoulder_pan": 12.4, ...}
```

Both describe the same instant: the joints are interpolated to that frame's own
capture time. See [Sync](/robotics/concepts/sync).

`get_observation()` is called once per observe cycle and cached, so joints and
images always come from the same read.

## Joint names

Use plain joint names, `elbow_flex` rather than `elbow_flex.pos`. LeRobot adds
the `.pos` suffix, the adapter takes it off, and everything you touch, safety
limits, dataset rows and callbacks, uses the plain name.

## Units

Every number you set is read in the arm's own units, since nothing converts
between them. A `slew` of `12.0` at 30 Hz lets a joint travel 360 of those units
each second.

<Warning>
  Set it too low and the dataset records the clamp instead of the operator.
  Pass a per joint mapping when an arm reports its joints in more than one
  unit, as a calibrated [SO-101](/robotics/integrations/lerobot/so101#units-and-telemetry) does.
</Warning>

## Related

<CardGroup cols={2}>
  <Card title="SO-101" icon="gears" href="/robotics/integrations/lerobot/so101">
    Calibrate the arms, and set slew for degrees and gripper units.
  </Card>

  <Card title="Safety" icon="shield-halved" href="/robotics/concepts/safety">
    The checks a command passes before it reaches the motors.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.