> ## 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.

# SO-101

> Set up two SO-101 arms on ROS 2 and control one from the other.

This guide sets up two SO-101 arms with a ready-made example. You move the
**leader** arm by hand on the operator machine, and the **follower** arm on the
robot machine copies it. You don't need to write any code.

<Card title="Example project" icon="github" href="https://github.com/ZeroRuntimeAI/zrt-teleops-examples/tree/main/ros2" horizontal>
  Launch files, configs, and shortcut commands for two SO-101 arms.
</Card>

## Prerequisites

* Two SO-101 arms, calibrated with
  [LeRobot](https://huggingface.co/docs/lerobot/so101). `lerobot-calibrate`
  comes from a separate LeRobot install. The id you give each arm there goes
  in `.env`.
* Ubuntu 24.04 and
  [ROS 2 Jazzy](https://docs.ros.org/en/jazzy/Installation.html) on each
  machine. On other 64-bit Linux (Raspberry Pi OS, Debian, Ubuntu 22.04,
  Jetson), run this example in Docker instead: see the
  [example's Docker section](https://github.com/ZeroRuntimeAI/zrt-teleops-examples/tree/main/ros2#docker-any-64-bit-linux).
* An [access token](https://app.videosdk.live/signup). You create a room id in
  step 4.

You can run one arm per machine, or both arms on one machine.

Do the first four steps on each machine.

<Steps>
  <Step title="Install the ROS packages" icon="download">
    ```bash theme={null}
    sudo apt install ros-jazzy-ros2-control ros-jazzy-ros2-controllers \
        ros-jazzy-controller-manager ros-jazzy-feetech-ros2-driver \
        ros-jazzy-xacro ros-jazzy-robot-state-publisher ros-dev-tools \
        python3-venv git
    sudo usermod -aG dialout $USER     # USB access; log out and back in after
    ```
  </Step>

  <Step title="Install the SDK" icon="box">
    ```bash theme={null}
    source /opt/ros/jazzy/setup.bash
    python3 -m venv --system-site-packages ~/ros2-venv
    source ~/ros2-venv/bin/activate
    pip install "zeroruntime-teleops[ros2]"
    ```

    Check the install: `which zrt-teleops-ros2-leader` should print a path.
  </Step>

  <Step title="Get and build the example" icon="github">
    ```bash theme={null}
    source /opt/ros/jazzy/setup.bash
    cd ~
    git clone https://github.com/ZeroRuntimeAI/zrt-teleops-examples.git
    cd ~/zrt-teleops-examples/ros2
    sudo rosdep init && rosdep update       # first time only
    rosdep install --from-paths . --ignore-src -y
    colcon build --symlink-install
    ```
  </Step>

  <Step title="Fill in .env" icon="key">
    ```bash theme={null}
    cp .env.example .env
    ```

    Add your token, the room id, and the port and calibration id of the arm on
    this machine. If both arms are on one machine, fill in both.

    No room id yet? Create one on either machine, and use the same id on both:

    ```bash theme={null}
    source ~/ros2-venv/bin/activate
    export ZERORUNTIME_AUTH_TOKEN="your-token-here"
    python3 -c "from zeroruntime.teleops.rooms import create_room; print(create_room())"
    ```

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

    ZERORUNTIME_FOLLOWER_PORT=/dev/serial/by-id/<follower-port>
    ZERORUNTIME_FOLLOWER_ID=my_follower

    ZERORUNTIME_LEADER_PORT=/dev/serial/by-id/<leader-port>
    ZERORUNTIME_LEADER_ID=my_leader
    ```

    * **Ports:** find them with `ls /dev/serial/by-id/`. Don't use
      `/dev/ttyACM0`: those names can change after a replug or reboot.
    * **Ids:** the names you gave the arms when you calibrated them. The
      calibration files must be on the machine that runs that arm, under
      `~/.cache/huggingface/lerobot/calibration/` (`robots/so_follower/` for
      the follower, `teleoperators/so_leader/` for the leader). If you
      calibrated on another machine, copy them there.
  </Step>

  <Step title="Start the follower" icon="robot">
    Plug in the follower arm and its power supply first. Then, on the robot
    machine:

    ```bash theme={null}
    cd ~/zrt-teleops-examples/ros2
    source /opt/ros/jazzy/setup.bash
    source install/setup.bash
    source ~/ros2-venv/bin/activate
    set -a; source .env; set +a
    ros2 launch so101_teleop_bringup follower.launch.py
    ```

    The follower arm becomes stiff and holds its position. This is the
    follower's **launch terminal**: keep it open.
  </Step>

  <Step title="Start the leader" icon="hand">
    On the operator machine, the same way:

    ```bash theme={null}
    cd ~/zrt-teleops-examples/ros2
    source /opt/ros/jazzy/setup.bash
    source install/setup.bash
    source ~/ros2-venv/bin/activate
    set -a; source .env; set +a
    ros2 launch so101_teleop_bringup leader.launch.py
    ```

    The leader arm stays loose, so you can move it by hand. Before going on,
    wait until both launch terminals show the bridge's `topics: ...` line.
  </Step>

  <Step title="Take control" icon="gamepad">
    Open a **command terminal** on **each** machine and load the shortcut commands.
    `env.sh` loads ROS and `.env` for you.

    ```bash theme={null}
    cd ~/zrt-teleops-examples/ros2
    source env.sh
    ```

    **Optional first-run check**, on the operator machine: hold the leader arm
    in its middle pose (the pose you used during calibration) and run:

    ```bash theme={null}
    ros2 topic echo /leader/joint_states --once
    ```

    Every joint should read close to 0 rad. A value near 3.14 means that
    joint's `offset` (2048 in the URDF) is missing, or the arm wasn't
    calibrated with LeRobot. Fix that before `take_control`: the example sets
    `max_misalignment: 0.0`, so nothing stops the follower from moving to a
    wrong pose.

    Then, on the operator machine:

    ```bash theme={null}
    take_control
    ```

    The reply should say `armed, lease …`. The follower moves to match the
    leader, then follows it. Keep the space around the follower clear.

    If the reply says `no grant within 5.0s`, see
    [Troubleshooting](#troubleshooting).
  </Step>
</Steps>

## Stopping

* **`release_control`** on the operator machine stops the follower. It holds
  its position.
* **`Ctrl-C` in the leader's launch terminal** is safe. The leader is loose anyway.
* **`follower_leave`** on the robot machine disconnects the follower while the
  arm keeps holding.

<Warning>
  `Ctrl-C` in the follower's launch terminal stops the servo
  driver, which **turns the servos' torque off**. The arm is then held only by
  its gears: it often stays where it is, but a stretched-out pose can sag or
  drop. Hold the arm first. To leave the room with the arm actively holding,
  use `follower_leave` instead. `Ctrl-C` in the command terminal, or on the
  leader, is harmless.
</Warning>

## Commands

Run these in the command terminal, after `source env.sh`. Leader commands go on
the operator machine, follower commands on the robot machine; run one on the
wrong machine and it tells you where to run it.

| Command | Run on | What it does |
| - | - | - |
| `take_control` / `release_control` | operator | Take control, or stop and hold position. |
| `estop` | either | Emergency stop. The robot stays stopped until it's cleared. |
| `clear_estop` | either | Clear an emergency stop. Then run `take_control` again. From the operator machine it calls the robot's bridge directly, so both machines must be on the same network. |
| `start_recording` / `stop_recording` | robot | Start or stop recording. |
| `start_episode` / `end_episode` | either | Start or end an episode by hand. On the operator, this asks the robot through the room, so it works across networks. It only takes effect while the robot is recording. |
| `start_episode "task"` | either | Start an episode with this task, for example `start_episode "pick up the orange cube"`. Later `start_episode` calls on that machine keep it until you give another. Episodes started by `take_control` get no task. |
| `end_episode success` / `end_episode fail` | either | End the episode and mark it a success or a failure. The data is kept either way. |
| `follower_leave` / `follower_join` | robot | Leave or rejoin the room. Run `release_control` first. |
| `leader_leave` / `leader_join` | operator | Leave or rejoin the room. |
| `follower_events` / `leader_events` | robot / operator | Watch events as they happen. |
| `watch_follower` | robot | Watch the commands sent to the follower arm. |

To keep both sides running after you close the terminal, see the
[example's README](https://github.com/ZeroRuntimeAI/zrt-teleops-examples/tree/main/ros2).
In background mode, `follower_up` and `leader_up` refuse to start while a
`ros2 launch` of the same arm is running.

## Troubleshooting

| Problem | Fix |
| - | - |
| `take_control` says `no grant within 5.0s` | The follower isn't running or is still joining, the two machines use different room ids, or the token and room id are from different environments. |
| `usb_port ... does not exist` | The arm is unplugged, or the port in `.env` is wrong. |
| A command says it runs on the leader or follower machine | Run it in that machine's command terminal. |
| Every servo shows `Read timeout` at startup | The arm's power supply isn't connected. |
| `Read timeout` mid-session, and the follower goes limp | The driver stopped. Hold the arm and restart the follower. If it happens again, lower `update_rate` in both `*_controllers.yaml` files and `control_hz` in `bridge.yaml` to 100, and double `max_norm_step` in `bridge.yaml` to keep the speed. |
| Joints read about 3.14 at the middle pose | The joint `offset` of 2048 is missing from the URDF. |
| `command size ... does not match`, then the follower stops | Run `ros2 control switch_controllers -c /follower/controller_manager --activate forward_controller`. |
| The wrist turns half a turn when you take control | Recalibrate both arms with the wrist turned the same way. |
| The follower lags behind the leader | Raise `max_norm_step` in `bringup/config/bridge.yaml`. It applies at the next launch, without a rebuild. |

## What's next

<CardGroup cols={2}>
  <Card title="Reference" icon="sliders" href="/robotics/integrations/ros2/reference">
    Recording, cameras, and all settings.
  </Card>

  <Card title="SO-101 on LeRobot" icon="gears" href="/robotics/integrations/lerobot/so101">
    The same arms without ROS.
  </Card>
</CardGroup>


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