Complete integration of OpenArm bimanual robots with HuggingFace LeRobot for robotic learning and teleoperation.
This repository contains a modified fork of LeRobot with OpenArm-specific enhancements for bimanual manipulation, gamepad teleoperation, and hardware calibration synchronization.
- Bimanual OpenArm Support: Control two OpenArm follower robots simultaneously
- Gamepad Teleoperation: PlayStation/Xbox controller support for intuitive joint control
- Leader Arm Teleoperation: Bimanual leader arm control for high-quality demonstration collection
- Hardware Calibration Sync: Proper integration with OpenArm's hardware calibration system
- Velocity & Torque Support: Full action space including position, velocity, and torque
- Recording & Replay: Compatible with LeRobot's dataset recording and policy training
- Ubuntu 22.04 or 24.04
- Python 3.10+
- Two OpenArm follower robots with CAN bus connectivity
- PlayStation DualSense or Xbox controller
- Root access for CAN bus configuration
This module requires the AD-SDL LeRobot fork with OpenArm support:
# In your project directory clone the LeRobot fork
git clone https://github.com/AD-SDL/lerobot.git
cd lerobot
# Install with PDM (recommended - uses lock file)
pdm install
# OR install with pip
pip install -e .
cd ..# Clone this repository
git clone https://github.com/AD-SDL/openarm_module.git
cd openarm_module
# Run system setup (installs OpenArm packages and configures auto CAN setup)
bash setup_system.shThis script will:
- Install OpenArm system packages (
openarm-can-utils, CLI tools) - Configure udev rules for automatic CAN configuration on USB connection
- Set up CAN interfaces for current session
# Install with PDM
pdm install
# OR install with pip
pip install -e .IMPORTANT: The robots are already calibrated at the factory. Only recalibrate if you suspect the zero positions are incorrect (e.g., arms don't return to proper zero position).
To verify calibration:
# Move arms to zero to check if calibration is correct
python scripts/move_to_zero.pyIf the arms move to a centered, symmetric position (hanging down naturally), calibration is good. Skip to Sync Calibration below.
If the arms move to awkward or asymmetric positions, proceed with recalibration.
WARNING: This will overwrite the existing calibration. Only do this if verification above failed.
# Calibrate follower right arm (can0)
openarm-can-zero-position-calibration --canport can0 --arm-side right_arm
# Calibrate follower left arm (can1)
openarm-can-zero-position-calibration --canport can1 --arm-side left_arm
# Calibrate leader right arm (can2)
openarm-can-zero-position-calibration --canport can2 --arm-side right_arm
# Calibrate leader left arm (can3)
openarm-can-zero-position-calibration --canport can3 --arm-side left_armREQUIRED: After verifying or recalibrating hardware, sync LeRobot's calibration files:
# Delete old LeRobot calibration files (if any)
rm -rf ~/.cache/huggingface/lerobot/calibration/
# Calibrate follower arms
lerobot-calibrate \
--robot.type=openarm_follower \
--robot.port=can0 \
--robot.side=right \
--robot.id=my_openarm_follower_right
lerobot-calibrate \
--robot.type=openarm_follower \
--robot.port=can1 \
--robot.side=left \
--robot.id=my_openarm_follower_left
# Calibrate leader arms
lerobot-calibrate \
--teleop.type=openarm_leader \
--teleop.port=can2 \
--teleop.id=my_openarm_leader_right
lerobot-calibrate \
--teleop.type=openarm_leader \
--teleop.port=can3 \
--teleop.id=my_openarm_leader_leftAfter plugging in USB-CAN adapters, verify they're configured:
ip link show can0 can1 can2 can3All four interfaces should show as UP. If not, unplug and replug the USB-CAN adapters.
CAN port mapping:
can0— follower right armcan1— follower left armcan2— leader right armcan3— leader left arm
USB wrist cameras are identified by physical USB port using udev symlinks. After plugging in cameras:
# Verify symlinks are created
ls -la /dev/video-wrist-*
# Expected:
# /dev/video-wrist-left -> videoX
# /dev/video-wrist-right -> videoXIf symlinks are missing, check the udev rules file at /etc/udev/rules.d/99-openarm-cameras.rules.
Get the RealSense chest camera serial number:
source ~/humanoids/lerobot_env/bin/activate
python3 -c "import pyrealsense2 as rs; ctx = rs.context(); print([d.get_info(rs.camera_info.serial_number) for d in ctx.devices])"If auto-configuration doesn't work:
openarm-can-configure-socketcan can0 -fd -b 1000000 -d 5000000
openarm-can-configure-socketcan can1 -fd -b 1000000 -d 5000000After installation and calibration:
# Test teleoperation
lerobot-teleoperate \
--robot.type=bi_openarm_follower \
--robot.left_arm_config.port=can1 \
--robot.left_arm_config.side=left \
--robot.right_arm_config.port=can0 \
--robot.right_arm_config.side=right \
--teleop.type=openarm_bi_gamepad_joints \
--teleop.joint_velocity_scale=60.0Use the gamepad to control the robots. Press Square/X to toggle between left/right arm!
lerobot-teleoperate \
--robot.type=bi_openarm_follower \
--robot.left_arm_config.port=can1 \
--robot.left_arm_config.side=left \
--robot.right_arm_config.port=can0 \
--robot.right_arm_config.side=right \
--teleop.type=openarm_bi_gamepad_joints \
--teleop.joint_velocity_scale=60.0Important: Use --teleop.type=openarm_bi_gamepad_joints for bimanual control (two arms), or --teleop.type=openarm_gamepad_joints for single arm control.
- Left Stick: Control joint 1-2
- Right Stick: Control joint 3-4
- D-Pad: Control joint 5-7
- L1/R1: Open/close gripper
- PS/Xbox Button: Return active arm to zero position
- Square/X: Toggle between left/right arm (bimanual mode)
- Triangle/Y: Print current arm position
- Circle/B: Exit teleoperation
Leader arm teleoperation produces significantly smoother demonstrations than gamepad control and is recommended for high-quality data collection.
CAN port mapping for leader arm setup:
can0/can1— follower right/left armscan2/can3— leader right/left arms
Set camera formats before starting:
v4l2-ctl --device=/dev/video-wrist-left --set-fmt-video=width=640,height=480,pixelformat=MJPG
v4l2-ctl --device=/dev/video-wrist-right --set-fmt-video=width=640,height=480,pixelformat=MJPGTeleoperate with cameras:
lerobot-teleoperate \
--robot.type=bi_openarm_follower \
--robot.left_arm_config.port=can1 \
--robot.left_arm_config.side=left \
--robot.left_arm_config.cameras="{ \
chest: {type: intelrealsense, serial_number_or_name: YOUR_SERIAL, width: 848, height: 480, fps: 30}, \
wrist_left: {type: opencv, index_or_path: /dev/video-wrist-left, width: 640, height: 480, fps: 30, fourcc: MJPG} \
}" \
--robot.right_arm_config.port=can0 \
--robot.right_arm_config.side=right \
--robot.right_arm_config.cameras="{ \
wrist_right: {type: opencv, index_or_path: /dev/video-wrist-right, width: 640, height: 480, fps: 30, fourcc: MJPG} \
}" \
--robot.id=my_bimanual_follower \
--teleop.type=bi_openarm_leader \
--teleop.left_arm_config.port=can3 \
--teleop.right_arm_config.port=can2 \
--teleop.id=my_bimanual_leader \
--teleop.left_arm_config.position_kp="[120,120,60,20,12,15,12,2]" \
--teleop.left_arm_config.position_kd="[2,2,1.0,0.5,0.1,0.1,0.1,0.02]" \
--teleop.right_arm_config.position_kp="[120,120,60,20,12,15,12,2]" \
--teleop.right_arm_config.position_kd="[2,2,1.0,0.5,0.1,0.1,0.1,0.02]" \
--robot.left_arm_config.position_kp="[240,240,120,40,24,31,25,5]" \
--robot.left_arm_config.position_kd="[5,5,1.5,0.3,0.3,0.3,0.3,0.05]" \
--robot.right_arm_config.position_kp="[240,240,120,40,24,31,25,5]" \
--robot.right_arm_config.position_kd="[5,5,1.5,0.3,0.3,0.3,0.3,0.05]" \
--display_data=trueTuning notes:
teleopkp/kd values control leader arm stiffness — lower values make the leader easier to backdriverobotkp/kd values control follower arm responsiveness
Controller not responding:
- Disconnect and reconnect the controller
- Check controller is detected:
ls /dev/input/js* - Restart the teleoperation script
Arms move too fast/slow:
- Adjust
--teleop.joint_velocity_scale(default: 60.0) - Lower values = slower movement
- Higher values = faster movement
export HF_HUB_OFFLINE=1
lerobot-record \
--robot.type=bi_openarm_follower \
--robot.left_arm_config.port=can1 \
--robot.left_arm_config.side=left \
--robot.right_arm_config.port=can0 \
--robot.right_arm_config.side=right \
--teleop.type=openarm_bi_gamepad_joints \
--teleop.joint_velocity_scale=60.0 \
--dataset.repo_id=local/my_task \
--dataset.single_task="Task description" \
--dataset.fps=30 \
--dataset.num_episodes=50 \
--dataset.episode_time_s=30 \
--dataset.reset_time_s=10 \
--dataset.push_to_hub=falseexport HF_HUB_OFFLINE=1
v4l2-ctl --device=/dev/video-wrist-left --set-fmt-video=width=640,height=480,pixelformat=MJPG
v4l2-ctl --device=/dev/video-wrist-right --set-fmt-video=width=640,height=480,pixelformat=MJPG
lerobot-record \
--robot.type=bi_openarm_follower \
--robot.left_arm_config.port=can1 \
--robot.left_arm_config.side=left \
--robot.left_arm_config.cameras="{ \
chest: {type: intelrealsense, serial_number_or_name: YOUR_SERIAL, width: 848, height: 480, fps: 30, use_depth: true}, \
wrist_left: {type: opencv, index_or_path: /dev/video-wrist-left, width: 640, height: 480, fps: 30, fourcc: MJPG} \
}" \
--robot.right_arm_config.port=can0 \
--robot.right_arm_config.side=right \
--robot.right_arm_config.cameras="{ \
wrist_right: {type: opencv, index_or_path: /dev/video-wrist-right, width: 640, height: 480, fps: 30, fourcc: MJPG} \
}" \
--robot.id=my_bimanual_follower \
--teleop.type=bi_openarm_leader \
--teleop.left_arm_config.port=can3 \
--teleop.right_arm_config.port=can2 \
--teleop.id=my_bimanual_leader \
--teleop.left_arm_config.position_kp="[120,120,60,20,12,15,12,2]" \
--teleop.left_arm_config.position_kd="[2,2,1.0,0.5,0.1,0.1,0.1,0.02]" \
--teleop.right_arm_config.position_kp="[120,120,60,20,12,15,12,2]" \
--teleop.right_arm_config.position_kd="[2,2,1.0,0.5,0.1,0.1,0.1,0.02]" \
--dataset.repo_id=local/my_task \
--dataset.single_task="Task description" \
--dataset.fps=30 \
--dataset.num_episodes=100 \
--dataset.episode_time_s=40 \
--dataset.reset_time_s=10 \
--dataset.push_to_hub=false \
--display_data=falseTo resume adding episodes to an existing dataset add --resume=true to the command.
Recording Parameters:
--dataset.fps: Recording framerate (30 fps recommended for leader arm)--dataset.num_episodes: Number of demonstrations to collect--dataset.episode_time_s: Maximum episode duration in seconds--dataset.reset_time_s: Time between episodes for scene reset--dataset.push_to_hub: Set to false for local-only storage--resume: Set to true to append episodes to an existing dataset
Dataset location: ~/.cache/huggingface/lerobot/local/
lerobot-replay \
--robot.type=bi_openarm_follower \
--robot.left_arm_config.port=can1 \
--robot.left_arm_config.side=left \
--robot.right_arm_config.port=can0 \
--robot.right_arm_config.side=right \
--dataset.fps=60 \
--dataset.repo_id=local/my_task \
--dataset.episode=0Train an ACT policy on recorded demonstrations:
python lerobot/scripts/train.py \
policy=act \
env=bi_openarm_follower \
dataset_repo_id=local/my_task \
training.offline_steps=100000 \
training.batch_size=8 \
training.eval_freq=10000This module depends on the lerobot fork which contains the following modifications from upstream LeRobot:
openarm_follower.py:
- Removed
set_zero_position()call fromconnect()to preserve hardware calibration - Added full action space support (position, velocity, torque)
- Optimized
get_observation()to read all motor states in one CAN refresh cycle
config_openarm_follower.py:
- Configured motor IDs and types for 7-DOF + gripper
- Set appropriate kp/kd gains per joint
- Defined joint limits for left/right arm configurations
- Created bimanual robot wrapper for dual OpenArm control
- Synchronized action/observation spaces across both arms
- Added proper torque enable/disable on connect/disconnect
openarm_teleop_gamepad.py:
- Direct joint control mode for OpenArm
- PlayStation controller button mapping
- Return-to-zero functionality
openarm_bi_teleop_gamepad.py:
- Bimanual control with arm toggle
- Independent left/right arm control
- Synchronized gripper control
gamepad_utils.py modifications:
- Added
get_button()method toGamepadController - Enhanced button state tracking
- Improved analog stick dead zone handling
teleop_bi_gamepad_joints.py:
- Added velocity and torque fields to action dictionary
- Fixed action registration for bimanual recording
- Proper integration with LeRobot's dataset format
openarm_module/
├── README.md
├── pyproject.toml # Python package configuration
├── scripts/
│ ├── setup_system.sh # System setup (OpenArm packages + CAN)
│ ├── sync_calibration.py # Sync hardware calibration with LeRobot
│ └── move_to_zero.py # Utility to move arms to zero
├── src/
│ └── openarm_module/ # Main module code
│ ├── __init__.py
│ └── ... # TODO
└── tests/
Note: This module depends on the lerobot fork, which should be installed separately (see Installation).
# Check CAN interfaces are up
ip link show can0
ip link show can1
# Restart CAN interfaces
sudo ip link set down can0
sudo ip link set down can1
sudo ip link set can0 type can bitrate 1000000 dbitrate 5000000 fd on
sudo ip link set up can0
sudo ip link set can1 type can bitrate 1000000 dbitrate 5000000 fd on
sudo ip link set up can1
# Check for CAN errors
candump can0
candump can1# Test motor communication
openarm-can-motor-check --canport can0
openarm-can-motor-check --canport can1
# Check motor IDs match configuration
openarm-can-diagnosis --canport can0If arms don't return to correct zero position:
-
Re-run hardware calibration:
openarm-can-zero-position-calibration --canport can0 --arm-side right_arm openarm-can-zero-position-calibration --canport can1 --arm-side left_arm
-
Delete LeRobot calibration and re-sync:
rm -rf ~/.cache/huggingface/lerobot/calibration/ python scripts/sync_calibration.py -
Verify zero position:
python scripts/move_to_zero.py
Dataset not saving:
- Check disk space:
df -h - Verify HF_HUB_OFFLINE is set:
echo $HF_HUB_OFFLINE - Check dataset path exists:
ls ~/.cache/huggingface/lerobot/
Missing velocity/torque in observations:
- This was a bug in earlier versions
- Update to latest version from this repository
- Observations should include
.pos,.vel, and.torquefor all joints
Contributions are welcome! Please:
- Fork this repository
- Create a feature branch:
git checkout -b feature-name - Make your changes and test thoroughly
- Submit a pull request with a clear description
This project inherits the Apache 2.0 license from HuggingFace LeRobot.
- HuggingFace LeRobot Team for the excellent robotic learning framework
- Enactic/OpenArm for the robot hardware and CAN library
- AD-SDL/Argonne National Laboratory for supporting this integration
For questions and support:
- Open an issue in this repository: AD-SDL/openarm_module
- Contact: Rapid Prototyping Laboratory, Argonne National Laboratory
- lerobot-rpl - LeRobot fork with OpenArm support
- LeRobot - Original LeRobot framework
- OpenArm CAN - OpenArm CAN library
Note: This is a research project. Use at your own risk and always ensure safety when operating robotic systems.