![]() |
![]() |
We are building a new bimanual arm platform for the Berkeley Humanoid Lite project!
This version improves upon the original BHL with a more robust hardware stack and refined mechanical design.
Key improvements include:
- Actuator upgrade: Transition from custom M6C12 / 5010 actuators to more reliable and accessible Robstride actuators.
- Refined mechanical design: Redesigned the CAD geometry with smoother contours resembling human arms for a more aesthetically pleasing form.
- LeRobot integration: Provides LeRobot-compatible robot and teleoperator plugins for real-robot control, calibration, and teleoperation workflows.
While making these upgrades, our goal remains the same: building open-source, accessible, and customizable humanoid robots for researchers, makers, and the community.
Note: this repository is under active development. API-breaking changes are likely.
Note: we are still refining the actuator selection and the design of the head, so that part is not open-sourced yet.
Install dependencies:
uv syncThis package provides third-party LeRobot plugins. The robot and teleoperator
types become available after Python imports lerobot_robot_bhl_arm and
lerobot_teleoperator_bhl_arm.
The example scripts in this repository import those packages for you.
After those imports, the robot config is available in LeRobot as:
berkeley_humanoid_lite_arm
And the different teleoperator configs are available as:
bhl_arm_keyboard
bhl_arm_keyboard_ik
bhl_arm_steamvr_ik
bhl_arm_spacemouse_ik
Bring up the default CAN interfaces:
source ./examples/canup.shThe default mapping in this package is:
left arm -> can0right arm -> can1
To connect and print measured joint positions:
uv run ./examples/connect_bhl_arm.py --count 3 --period 0.5For the full observation dictionary:
uv run ./examples/connect_bhl_arm.py --count 1 --full-observationRun:
uv run ./examples/connect_bhl_arm.py --calibrate --count 1This will connect to the robot, run calibration if no saved LeRobot calibration exists yet, and write the calibration file under LeRobot's calibration directory:
~/.cache/huggingface/lerobot/calibration/
The exact filename is resolved by LeRobot from the robot configuration, typically incorporating the robot id when one is set. If you want to inspect the resolved path directly in Python, print robot.calibration_fpath from a BHLArm instance.
During calibration, the robot temporarily sets kP=0 and kD=0 so the joints stay backdrivable while you manually move each joint through its full range of motion and try to reach both the lower and upper limits.
The program continuously records the measured joint positions returned by the actuators, tracks the minimum and maximum value reached for each joint, and compares those sampled extrema against the default joint limits in BHLArmConfig. From that difference, it computes a per-joint homing offset so the actuator-reported positions line up with the expected robot joint ranges.
For best results, make sure each joint reaches both ends of its intended range before stopping the calibration.
After calibration, stop the program by pressing Ctrl + C. The offset will be automatically calculated and stored.
This repository currently includes a keyboard teleop sanity check for:
left_wrist_yawright_wrist_yaw
Default keys:
q/a: increase / decreaseleft_wrist_yaww/s: increase / decreaseright_wrist_yawx: request exit
Run:
uv run ./examples/teleop_bhl_arm_keyboard.py \
--stiffness 8 \
--damping 1 \
--soft-start-duration 2 \
--step-deg 2Behavior:
- On startup, the script ramps
kP/kDwhile holding the current pose. - Press
xonce to enter damping mode. - Press
xagain, orCtrl+Cagain, to disconnect and fully exit.
You can invoke the same teleoperator through the official LeRobot CLI by preloading this repository's plugin packages:
uv run python -c 'import lerobot_robot_bhl_arm, lerobot_teleoperator_bhl_arm; from lerobot.scripts.lerobot_teleoperate import main; main()' \
--robot.type=berkeley_humanoid_lite_arm \
--robot.stiffness=8 \
--robot.damping=1 \
--robot.soft_start_enabled=true \
--robot.soft_start_duration=2.0 \
--robot.soft_start_show_progress=true \
--teleop.type=bhl_arm_keyboard \
--teleop.control_frequency=20 \
--teleop.position_step=0.034906585Notes:
--robot.soft_start_enabled=trueis recommended so the robot syncs to its current pose before keyboard deltas are applied while smoothly applying the configured gains.- The keyboard teleoperator uses per-joint
*.deltacommands. BHLArmaccepts both absolute commands (positions,*.pos) and incremental commands (deltas,*.delta).- The preloaded CLI path above uses the standard LeRobot teleoperate lifecycle and does not implement the manual script's two-stage damping-mode exit loop.
This repository also includes a second keyboard teleoperator that moves an end-effector target in Cartesian space and uses MuJoCo + Mink IK to solve joint positions for the real robot.
Default keys:
- Left arm:
w/s:+x / -xforward/backward translationa/d:+y / -yleft/right translationr/f:+z / -zup/down translationq/e:+xroll /-xrollz/x:+zyaw /-zyaw- Right arm:
i/k:+x / -xforward/backward translationj/l:+y / -yleft/right translationy/h:+z / -zup/down translationu/o:+xroll /-xrolln/m:+zyaw /-zyawc: reset both targets to the current measured end-effector posesp: request exit
Run the manual script with the MuJoCo viewer and frame markers:
uv run ./examples/teleop_bhl_arm_keyboard_ik.py \
--stiffness 8 \
--damping 1 \
--soft-start-duration 2 \
--ee-step 0.01 \
--viewerThe viewer shows both the current end-effector frame and the commanded target frame for each arm. By default, the teleoperator looks for the BHL Arm MJCF at:
./data/bhl_arm/mjcf/bhl_arm.xml
relative to this repository's parent workspace. If your MJCF lives elsewhere, pass --xml-path /abs/path/to/bhl_arm.xml.
This repository also includes a SteamVR teleoperator that listens for controller pose deltas over UDP, rebases those deltas to the robot's current end-effector pose, and solves dual-arm IK with MuJoCo + Mink.
The expected controller payload per side is:
relative_location:[x, y, z]relative_orientation:[qw, qx, qy, qz]
Run the manual real-robot teleop:
uv run ./examples/teleop_bhl_arm_steamvr_ik.py \
--stiffness 8 \
--damping 1 \
--soft-start-duration 2 \
--frequency 30 \
--steamvr-host 0.0.0.0 \
--steamvr-port 11005 \
--viewerThis listens for SteamVR packets on UDP 0.0.0.0:11005 by default. The viewer shows current and target end-effector frames for both arms.
This repository also includes a dual-SpaceMouse teleoperator that uses one 3Dconnexion SpaceMouse per arm, integrates each device's 6-DoF deflection into end-effector pose targets, and solves dual-arm IK with MuJoCo + Mink.
On Linux, the matching hidraw devices must be readable by your user. A typical udev rule for the SpaceMouse Compact is:
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="256f", ATTRS{idProduct}=="c635", MODE="0660", GROUP="plugdev", TAG+="uaccess"
Reload rules and replug the devices after adding the rule:
sudo udevadm control --reload-rules
sudo udevadm triggerTo verify raw device input before starting teleop:
uv run ./examples/test_spacemouse.py --device /dev/hidraw1
uv run ./examples/test_spacemouse.py --device /dev/hidraw2Run the manual real-robot teleop:
uv run ./examples/teleop_bhl_arm_spacemouse_ik.py \
--stiffness 8 \
--damping 1 \
--soft-start-duration 2 \
--frequency 30 \
--left-device /dev/hidraw1 \
--right-device /dev/hidraw2 \
--translation-speed 0.12 \
--rotation-speed 1.2 \
--viewerBehavior:
- The first SpaceMouse controls the left end effector.
- The second SpaceMouse controls the right end effector.
- The left button on either device resets that arm target to the current measured end-effector pose.
left_translation_signs,right_translation_signs,left_rotation_signs, andright_rotation_signsinBHLArmSpaceMouseIKTeleopConfigcan be adjusted if your physical device orientation does not match the robot frame convention.
To scan the default CAN buses and list detected actuator IDs:
uv run ./examples/discover_bhl_arm_actuators.pyTo limit the scanned ID range:
uv run ./examples/discover_bhl_arm_actuators.py --start-id 1 --end-id 50Reusable controller-facing safety helpers live on BHLArm:
prepare_for_control(...)enter_damping_mode(...)hold_current_position()hold_joint_positions(...)
These helpers are intended for future controllers that need the same startup/shutdown mechanics without duplicating robot-side logic.
Consider joining our Discord channel.
We would like to thank DeepCybo and Zhongguancun Academy (ZGCA) for the collaboration and support.


