Software Setup
Plugin installation, leader/follower arm configuration, ROS2 bimanual control, LeRobot DK1 integration, Python API, and troubleshooting. From a fresh Ubuntu install to a moving bimanual system.
Jump to a section:
SDK Installation
The DK1 SDK is distributed as a LeRobot plugin via the trlc-dk1 repository. It registers four device types: dk1_leader, dk1_follower, bi_dk1_leader, and bi_dk1_follower.
Install LeRobot first
pip install lerobot
Clone and install the DK1 plugin
git clone https://github.com/TRLC-AI/trlc-dk1.git
cd trlc-dk1
uv pip install -e .
Verify the installation
python3 -c "from lerobot.common.robots import make_robot; print('DK1 plugin OK')"
# Check that DK1 device types are registered
python3 -c "
from lerobot.common.robots.factory import get_robot_types
types = get_robot_types()
for t in types:
if 'dk1' in t:
print(t)
"
You should see dk1_leader, dk1_follower, bi_dk1_leader, bi_dk1_follower printed. If not, ensure the plugin installed correctly with uv pip show trlc-dk1.
Install from a specific commit (for reproducibility)
cd trlc-dk1
git checkout v0.3.0
uv pip install -e .
Leader/Follower Arm Configuration
The DK1's bimanual system relies on port assignment to distinguish the leader arm (Dynamixel XL330) from the follower arm (DM4340 + DM4310). Incorrect port assignments are the most common setup error.
Detect USB serial ports
Run the LeRobot port-detection utility with one arm connected at a time:
# Connect ONLY the leader arm (Dynamixel XL330)
python -m lerobot.scripts.find_motors_bus_port
# Note the reported port, e.g. /dev/ttyACM0
# Disconnect leader, connect ONLY the follower arm (DM series)
python -m lerobot.scripts.find_motors_bus_port
# Note the reported port, e.g. /dev/ttyACM1
Create the bimanual robot config
Create a YAML configuration file for the bimanual pair. LeRobot uses this to route commands to the correct arm:
# ~/.lerobot/robots/dk1_bimanual.yaml
robot_type: bi_dk1_follower
leader_arms:
left:
port: /dev/ttyACM0
motors: [shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper_left, gripper_right]
follower_arms:
left:
port: /dev/ttyACM1
motors: [shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper_left, gripper_right]
cameras:
wrist_left:
type: opencv
index: 0
fps: 30
width: 640
height: 480
overhead:
type: opencv
index: 2
fps: 30
width: 640
height: 480
Verify the configuration
python -m lerobot.scripts.control_robot \
--robot.type=bi_dk1_follower \
--robot.config=~/.lerobot/robots/dk1_bimanual.yaml \
--control.type=none
This connects to both arms without moving them. Check for connection errors. If either arm fails to connect, re-run port detection or swap port assignments.
Port persistence: USB serial ports can change between reboots. Use udev rules to bind a port to a specific arm by USB serial number. See the Setup Guide for the udev rule template.
ROS2 Bimanual Control
ROS2 Humble provides a higher-level control layer for the DK1 with full MoveIt2 bimanual planning support. This is optional for LeRobot-only data collection workflows.
Install ROS2 Humble and bimanual packages
sudo apt update && sudo apt install ros-humble-desktop \
ros-humble-ros2-control ros-humble-ros2-controllers \
ros-humble-moveit ros-humble-joint-state-publisher-gui -y
Clone and build the DK1 ROS2 package
mkdir -p ~/dk1_ws/src && cd ~/dk1_ws/src
git clone https://github.com/TRLC-AI/trlc-dk1-ros2.git
cd ~/dk1_ws
source /opt/ros/humble/setup.bash
colcon build --symlink-install
Launch in bimanual mode
source ~/dk1_ws/install/setup.bash
# Launch both arms (use_fake_hardware for testing without hardware)
ros2 launch trlc_dk1_ros2 dk1_bimanual.launch.py \
use_fake_hardware:=false \
leader_port:=/dev/ttyACM0 \
follower_port:=/dev/ttyACM1
Test with fake hardware (no arms needed)
ros2 launch trlc_dk1_ros2 dk1_bimanual.launch.py \
use_fake_hardware:=true
Send a bimanual trajectory
ros2 topic pub /follower_left/joint_trajectory_controller/joint_trajectory \
trajectory_msgs/msg/JointTrajectory \
'{joint_names: ["shoulder_pan"], points: [{positions: [0.3], time_from_start: {sec: 2}}]}'
LeRobot DK1 Configuration
LeRobot with the DK1 plugin handles bimanual teleoperation recording natively. The bi_dk1_follower device type records from both arms and all cameras simultaneously.
Calibrate both arms
# Calibrate the leader arm
python -m lerobot.scripts.control_robot \
--robot.type=dk1_leader \
--robot.port=/dev/ttyACM0 \
--control.type=calibrate
# Calibrate the follower arm
python -m lerobot.scripts.control_robot \
--robot.type=dk1_follower \
--robot.port=/dev/ttyACM1 \
--control.type=calibrate
Start bimanual teleoperation
python -m lerobot.scripts.control_robot \
--robot.type=bi_dk1_follower \
--robot.config=~/.lerobot/robots/dk1_bimanual.yaml \
--control.type=teleoperate
Record a bimanual dataset
python -m lerobot.scripts.control_robot \
--robot.type=bi_dk1_follower \
--robot.config=~/.lerobot/robots/dk1_bimanual.yaml \
--control.type=record \
--control.fps=30 \
--control.repo_id=your-username/dk1-bimanual-pick-place-v1 \
--control.num_episodes=50 \
--control.single_task="Bimanual: pick up the block with left arm, transfer to right arm" \
--control.warmup_time_s=5 \
--control.reset_time_s=10
Push to HuggingFace Hub
huggingface-cli login
python -m lerobot.scripts.push_dataset_to_hub \
--repo_id=your-username/dk1-bimanual-pick-place-v1
Python API for Bimanual Control
The DK1 Python API provides direct access to both arms via serial. No ROS2 required for basic control and data logging.
Connect both arms
from trlc_dk1 import DK1Leader, DK1Follower, BimanualDK1
# Connect individually
leader = DK1Leader(port="/dev/ttyACM0")
follower = DK1Follower(port="/dev/ttyACM1")
leader.connect()
follower.connect()
# Or use the bimanual controller (recommended)
robot = BimanualDK1(
leader_port="/dev/ttyACM0",
follower_port="/dev/ttyACM1"
)
robot.connect()
Read joint states from both arms
import time
from trlc_dk1 import BimanualDK1
robot = BimanualDK1(leader_port="/dev/ttyACM0", follower_port="/dev/ttyACM1")
robot.connect()
for _ in range(100):
leader_state = robot.get_leader_state()
follower_state = robot.get_follower_state()
print(f"Leader: {leader_state.positions}")
print(f"Follower: {follower_state.positions}")
time.sleep(0.033) # 30 Hz
robot.disconnect()
Run leader-follower loop manually
from trlc_dk1 import BimanualDK1
import time
robot = BimanualDK1(leader_port="/dev/ttyACM0", follower_port="/dev/ttyACM1")
robot.connect()
robot.enable_follower()
try:
while True:
leader_state = robot.get_leader_state()
# Apply leader positions to follower (scaled if needed)
robot.set_follower_positions(leader_state.positions)
time.sleep(0.01) # 100 Hz control loop
finally:
robot.disable_follower()
robot.disconnect()
Simulation Support
The DK1 supports MuJoCo bimanual simulation with a calibrated model that mirrors real-hardware kinematics. Train policies in simulation before deploying to hardware.
MuJoCo bimanual simulation
pip install mujoco
git clone https://github.com/TRLC-AI/trlc-dk1-mujoco.git
cd trlc-dk1-mujoco
# Run the bimanual simulation with leader/follower
python examples/bimanual_sim.py
# Run with keyboard teleop
python examples/bimanual_sim.py --teleop keyboard
Train a policy against the MuJoCo environment
python -m lerobot.scripts.train \
--policy.type=act \
--env.type=dk1_bimanual_sim \
--policy.chunk_size=100 \
--training.num_epochs=5000 \
--output_dir=outputs/dk1-act-sim
Sim-to-Real alignment: The DK1 MuJoCo model uses the TRLC-DK1-Follower_v0.3.0 STEP file geometry and measured DM4340/DM4310 motor torque curves. Policies trained in sim transfer to real hardware with minimal tuning for structured pick-and-place tasks.
Top 3 Bimanual-Specific Issues
Both arms connected to the same /dev/ttyACM* port, or port assignments swapped. The leader arm (Dynamixel XL330) and follower arm (DM series) use different protocols; wrong assignment causes immediate control failure.
Fix:
# 1. Unplug both arms
# 2. Connect ONLY the leader arm (XL330 servos)
python -m lerobot.scripts.find_motors_bus_port
# Note: leader_port = /dev/ttyACM?
# 3. Disconnect leader, connect ONLY the follower arm (DM servos)
python -m lerobot.scripts.find_motors_bus_port
# Note: follower_port = /dev/ttyACM?
# 4. Update your YAML config with the correct ports
# 5. Create udev rules to make assignments permanent
The follower arm's PD gains are too high for the current payload or arm configuration. This is especially common when the arms are loaded with end-effectors or when operating at full extension.
Fix:
# Reduce follower PD gains in the DK1 config
# Edit trlc-dk1/configs/follower_gains.yaml:
joint_gains:
default:
kp: 30 # reduce from default 50
kd: 0.5 # reduce from default 1.0
wrist:
kp: 15 # wrist joints need lower gains
kd: 0.3
# Apply and restart teleoperation
python -m lerobot.scripts.control_robot \
--robot.type=bi_dk1_follower \
--control.type=teleoperate
USB bandwidth contention with two cameras plus two USB serial arms on the same bus controller. LeRobot timestamp skew between camera streams and joint state readings exceeds acceptable limits.
Fix:
# 1. Check which USB bus each device is on
lsusb -t
# 2. Spread devices across separate USB bus controllers
# - Cameras: use a powered USB hub on one controller
# - Arms: connect directly on a different controller
# 3. Reduce camera resolution if bandwidth is still tight
# In dk1_bimanual.yaml:
cameras:
wrist_left:
width: 480
height: 320 # lower resolution reduces USB bandwidth
# 4. Verify timestamp skew is acceptable
python -m trlc_dk1.tools.check_sync \
--config ~/.lerobot/robots/dk1_bimanual.yaml
# Target: < 5ms skew between all streams
Still stuck? Ask on the DK1 Forum or check existing GitHub issues.