LeRobot documentation
Unitree G1
Unitree G1
The Unitree G1 humanoid is now supported in LeRobot! You can teleoperate, train locomanipulation policies, test in sim, and more. Both 29 and 23 DoF variants are supported.
Part 1: Getting Started
Install the Unitree SDK
Follow the unitree_sdk2_python installation guide. Tested with unitree_sdk2py==1.0.1 and cyclonedds==0.10.2:
conda create -y -n lerobot python=3.12
conda activate lerobot
git clone https://github.com/unitreerobotics/unitree_sdk2_python.git
cd unitree_sdk2_python
pip install -e .
cd ..Install LeRobot
conda install ffmpeg -c conda-forge
conda install -c conda-forge "pinocchio>=3.0.0,<4.0.0"
git clone https://github.com/huggingface/lerobot.git
cd lerobot
pip install -e '.[unitree_g1]'For now, pinocchio must be installed from conda-forge (not pip) to include the CasADi bindings needed for arm IK.
Test the Installation (Simulation)
The simulation environment has its own dependencies. Check the Simulation environment dependencies: Unitree G1 Mujoco EnvHub.
pip install mujoco loguru msgpack msgpack-numpy
lerobot-teleoperate \
--robot.type=unitree_g1 \
--robot.is_simulation=true \
--teleop.type=unitree_g1 \
--teleop.id=wbc_unitree \
--robot.cameras='{"global_view": {"type": "zmq", "server_address": "localhost", "port": 5555, "camera_name": "head_camera", "width": 640, "height": 480, "fps": 30, "warmup_s": 5}}' \
--display_data=true \
--robot.controller=GrootLocomotionControllerThis will launch a MuJoCo sim instance for the G1. You can connect a gamepad to your machine before launching in order to control the robot’s locomotion in sim. We support both HolosomaLocomotionController and GrootLocomotionController via --robot.controller.
- Press
9to release the robot - Press
7/8to increase / decrease waist height
Connect to the Physical Robot
The G1’s Ethernet IP is fixed at 192.168.123.164. Your machine must have a static IP on the same subnet: 192.168.123.x where x ≠ 164.
# Replace 'enp131s0' with your ethernet interface name (check with `ip a`)
sudo ip addr flush dev enp131s0
sudo ip addr add 192.168.123.200/24 dev enp131s0
sudo ip link set enp131s0 upSSH into the Robot
ssh unitree@192.168.123.164
# Password: 123Share Internet via Ethernet
The G1 needs internet access to clone repos and install packages. Share your laptop’s connection over Ethernet:
On your laptop:
sudo sysctl -w net.ipv4.ip_forward=1
# Replace wlp132s0f0 with your WiFi interface name
sudo iptables -t nat -A POSTROUTING -o wlp132s0f0 -s 192.168.123.0/24 -j MASQUERADE
sudo iptables -A FORWARD -i wlp132s0f0 -o enp131s0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i enp131s0 -o wlp132s0f0 -j ACCEPTOn the G1:
sudo ip route del default 2>/dev/null || true
sudo ip route add default via 192.168.123.200 dev eth0
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
# Verify
ping -c 3 8.8.8.8Install the Unitree SDK on the G1
Follow the unitree_sdk2_python installation guide:
conda create -y -n lerobot python=3.12
conda activate lerobot
git clone https://github.com/unitreerobotics/unitree_sdk2_python.git
cd unitree_sdk2_python
python -m pip install -e .
cd ..Install LeRobot on the G1
git clone https://github.com/huggingface/lerobot.git
cd lerobot
conda install -c conda-forge "pinocchio>=3.0.0,<4.0.0"
python -m pip install -e '.[unitree_g1]'For now, pinocchio must be installed from conda-forge (not pip) to include the CasADi bindings needed for arm IK.
(Optional) Enable WiFi on the Robot
For wireless SSH access, you can enable WiFi on the G1 (it’s blocked by default):
sudo rfkill unblock all
sudo ip link set wlan0 up
sudo nmcli radio wifi on
sudo nmcli device set wlan0 managed yes
sudo systemctl restart NetworkManagerConnect to a WiFi network:
nmcli device wifi list
sudo nmcli connection add type wifi ifname wlan0 con-name "YourNetwork" ssid "YourNetwork"
sudo nmcli connection modify "YourNetwork" wifi-sec.key-mgmt wpa-psk
sudo nmcli connection modify "YourNetwork" wifi-sec.psk "YourPassword"
sudo nmcli connection modify "YourNetwork" connection.autoconnect yes
sudo nmcli connection up "YourNetwork"
ip a show wlan0You can then SSH over WiFi instead of Ethernet:
ssh unitree@<ROBOT_WIFI_IP>
# Password: 123Part 2: Teleoperation & Locomotion
Run the Robot Server
On the robot (from ~/lerobot):
cd ~/lerobot
python src/lerobot/robots/unitree_g1/run_g1_server.py --cameraRun the Locomotion Policy
You can run the teleoperation client from your laptop over Ethernet, over WiFi (experimental), or directly on the robot itself. Mind potential latency introduced by your network.
From your laptop:
lerobot-teleoperate \
--robot.type=unitree_g1 \
--robot.is_simulation=false \
--robot.robot_ip=<ROBOT_IP> \
--teleop.type=unitree_g1 \
--teleop.id=wbc_unitree \
--robot.cameras='{"global_view": {"type": "zmq", "server_address": "<ROBOT_IP>", "port": 5555, "camera_name": "head_camera", "width": 640, "height": 480, "fps": 30}}' \
--display_data=true \
--robot.controller=HolosomaLocomotionControllerWe support both GrootLocomotionController and HolosomaLocomotionController via --robot.controller.
Part 3: Loco-Manipulation with the Homunculus Exoskeleton
We provide a loco-manipulation solution via the Homunculus Exoskeleton — an open-source 7 DoF exoskeleton for whole-body control. Check it out here.
Calibrate
lerobot-calibrate \ --teleop.type=unitree_g1 \ --teleop.left_arm_config.port=/dev/ttyACM1 \ --teleop.right_arm_config.port=/dev/ttyACM0 \ --teleop.id=exo
During calibration move each joint through its entire range. After fitting, move the joint in a neutral position and press n to advance.
Record a Dataset
lerobot-record \
--robot.type=unitree_g1 \
--robot.is_simulation=true \
--robot.cameras='{"global_view": {"type": "zmq", "server_address": "localhost", "port": 5555, "camera_name": "head_camera", "width": 640, "height": 480, "fps": 30}}' \
--teleop.type=unitree_g1 \
--teleop.left_arm_config.port=/dev/ttyACM1 \
--teleop.right_arm_config.port=/dev/ttyACM0 \
--teleop.id=exo \
--dataset.repo_id=your-username/dataset-name \
--dataset.single_task="Test" \
--dataset.num_episodes=2 \
--dataset.episode_time_s=5 \
--dataset.reset_time_s=5 \
--dataset.push_to_hub=true \
--dataset.streaming_encoding=true \
--dataset.encoder_threads=2Note: Omit
--teleop.left_arm_config.portand--teleop.right_arm_config.portif you’re only using the joystick.
Example dataset: nepyope/unitree_box_move_blue_full
Part 4: Training & Inference
Train
python src/lerobot/scripts/lerobot_train.py \
--dataset.repo_id=your-username/dataset-name \
--policy.type=pi05 \
--output_dir=./outputs/pi05_training \
--job_name=pi05_training \
--policy.repo_id=your-username/your-repo-id \
--policy.pretrained_path=lerobot/pi05_base \
--policy.compile_model=true \
--policy.gradient_checkpointing=true \
--wandb.enable=true \
--policy.dtype=bfloat16 \
--policy.freeze_vision_encoder=false \
--policy.train_expert_only=false \
--steps=3000 \
--policy.device=cuda \
--batch_size=32Inference with RTC
Once trained, we recommend deploying policies using inference-time RTC:
lerobot-rollout \
--strategy.type=base \
--policy.path=your-username/your-repo-id \
--policy.device=cuda \
--robot.type=unitree_g1 \
--robot.is_simulation=false \
--robot.controller=HolosomaLocomotionController \
--robot.cameras='{"global_view": {"type": "zmq", "server_address": "<ROBOT_IP>", "port": 5555, "camera_name": "head_camera", "width": 640, "height": 480, "fps": 30}}' \
--task="task_description" \
--duration=1000 \
--fps=30 \
--inference.type=rtcPart 5: SONIC Whole-Body Control (Latent-Token Policies)
LeRobot now ports the decoder half of NVIDIA’s SONIC whole-body deploy stack as the SonicWholeBodyController. Instead of commanding joints, the policy outputs a 64-D latent motion token (motion_token.{i}.pos) each tick; the SONIC decoder maps that token plus recent proprioception history onto a residual action that is scaled and added to the standing pose, producing 50 Hz joint-position targets for all 29 DoF.
The controller loads its ONNX decoder and all deploy constants (PD gains, default_angles, action_scale, and the neutral token) from lerobot/sonic_decoder. The repo also ships a distilled low-latency decoder, selectable with the controller’s policy_type="low_latency" argument (defaults to "default").
Test in Simulation
Any token-output policy trained on SONIC motion tokens (e.g. nepyope/sonic_walk) can drive it:
lerobot-rollout \
--strategy.type=base \
--policy.path=nepyope/sonic_walk \
--policy.device=cuda \
--robot.type=unitree_g1 \
--robot.is_simulation=true \
--robot.controller=SonicWholeBodyController \
--robot.cameras='{"ego_view": {"type": "zmq", "server_address": "localhost", "port": 5555, "camera_name": "head_camera", "width": 640, "height": 480, "fps": 30, "warmup_s": 15}}'Run on the Physical Robot
On hardware, run the policy with async inference rather than a single lerobot-rollout loop. A rollout driven from your laptop puts the network in the middle of the control loop: every step blocks on inference, and one hiccup stalls the 50 Hz balance loop while the robot is standing on it. Async inference splits the two — the policy runs on your GPU machine, while the robot client runs on the G1’s Jetson, so the SONIC controller thread and rt/lowcmd stay onboard and only 64-D token chunks cross the network.
Three processes, with the robot server from Part 2 running the whole time:
GPU machine G1 (Jetson)
policy_server ◄── gRPC ──► robot_client: UnitreeG1 + SONIC decoder @ 50 Hz
checkpoint │ ZMQ (lowcmd 6000, lowstate 6001, camera 5555)
▼
run_g1_server.py ◄──► rt/lowcmd, rt/lowstate (DDS)Start the policy server on the GPU machine first:
python -m lerobot.async_inference.policy_server \ --host=0.0.0.0 \ --port=8080 \ --fps=30
On the robot, start the server (bridge + camera):
python src/lerobot/robots/unitree_g1/run_g1_server.py --camera
Then, in a second terminal on the robot, start the client. Everything robot-side is loopback, since the bridge runs on the same machine:
python -m lerobot.async_inference.robot_client \
--server_address=<GPU_MACHINE_IP>:8080 \
--policy_type=pi05 \
--pretrained_name_or_path=nepyope/sonic_walk \
--policy_device=cuda \
--actions_per_chunk=50 \
--fps=30 \
--task="walk forward" \
--robot.type=unitree_g1 \
--robot.is_simulation=false \
--robot.robot_ip=127.0.0.1 \
--robot.controller=SonicWholeBodyController \
--robot.cameras='{"ego_view": {"type": "zmq", "server_address": "127.0.0.1", "port": 5555, "camera_name": "head_camera", "width": 640, "height": 480, "fps": 30, "warmup_s": 5}}'A few things to keep in mind:
--policy_deviceis the device the server loads the policy onto — the client sends it during the handshake.--actions_per_chunkmust not exceed the checkpoint’s chunk size (50 fornepyope/sonic_walk).- With
SonicWholeBodyControllerthe observation state is the 64-D token echo instead of the 29 joint positions, so the checkpoint has to be trained against that same state.
On connect, the robot eases every joint to the decoder’s default_angles before the controller takes over, so the first policy commands don’t snap from the connect-time pose.
Additional Resources
Last updated: July 2026
Update on GitHub