A clean, minimal starting point for learning to control the Unitree G1 humanoid robot using the Unitree Python SDK. The repo decouples low-level DDS communication and policy execution so you can create new policy classes without worrying about DDS topics, motor mode bytes, or threading for quick testing of your policy in sim and real.
Write a policy once, validate it across multiple simulators (MuJoCo, Isaac Lab) and deploy to the real robot with no code changes. All communication goes through DDS, the same protocol the real robot uses, so sim and real share the same code path.
- A
UnitreeG1Robotclass that handles DDS plumbing, the motion-service handshake, mode_machine matching, the 500Hz control loop, and shutdown safety. - A
Policyabstract base class. Subclass it, implementresetandstep, and the robot runs your policy at whatever rate you specify. - Body-only mode (29 motors) and body + Dex3 hand mode (43 motors), switched by a single constructor argument.
- An
AnkleSwingPolicyexample that recreates Unitree's official low-level demo through the new architecture, useful as a sanity check end-to-end. - A two-thread design: a policy thread at your chosen rate and a control thread at 500Hz. Plug in slow learned policies (10-50Hz) without losing motor command rate.
G1-Playground/
├── scripts/
│ └── run_ankle_swing_demo.py # entry point that runs the demo
├── src/g1_playground/
│ ├── __init__.py
│ ├── action.py # JointAction dataclass + Mode constants
│ ├── dds.py # DDS channel initialization
│ ├── robot.py # UnitreeG1Robot + joint indices + gain defaults
│ ├── state.py # G1State dataclass (body + hand state bundle)
│ └── policies/
│ ├── __init__.py
│ ├── policy.py # Policy abstract base class
│ └── ankle_swing.py # example policy
├── pyproject.toml
├── uv.lock
└── README.md
This project uses uv for Python dependency management. If you don't have it:
curl -LsSf https://astral.sh/uv/install.sh | shgit clone https://github.com/AlexandreBrown/G1-Playground.git
cd G1-PlaygroundThe upstream unitree_sdk2_python ships precompiled .so files that get stripped out when uv builds a wheel from a git source. The fix is an editable install pointing at a local clone next to this repo:
git clone https://github.com/unitreerobotics/unitree_sdk2_python.git ../unitree_sdk2_python
uv add --editable ../unitree_sdk2_pythonuv sync
source .venv/bin/activateThis installs mujoco, numpy, and any other declared dependencies into .venv/. The source command activates the virtual environment so you can use python directly instead of uv run for the rest of the session.
python -c "from g1_playground.robot import UnitreeG1Robot; from g1_playground.policies.ankle_swing import AnkleSwingPolicy; print('ok')"Should print ok with no traceback.
Start in simulation. Always. Real hardware has consequences and the sim catches most bugs.
MuJoCo is already installed as a Python dependency (mujoco in pyproject.toml), so no manual download is needed.
This is Unitree's official MuJoCo simulator. It runs as a separate process and communicates with your control code over DDS, exactly as the real robot does. Clone it next to this repo:
git clone https://github.com/unitreerobotics/unitree_mujoco.git ../unitree_mujocoEdit ../unitree_mujoco/simulate_python/config.py:
import os
ROBOT = "g1"
ROBOT_SCENE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "unitree_robots", "g1", "scene_29dof.xml")
DOMAIN_ID = 1
INTERFACE = "lo"
USE_JOYSTICK = 0
JOYSTICK_TYPE = "xbox"
JOYSTICK_DEVICE = 0
PRINT_SCENE_INFORMATION = True
ENABLE_ELASTIC_BAND = True # virtual strap so the robot doesn't collapse
SIMULATE_DT = 0.002
VIEWER_DT = 0.02The important settings:
DOMAIN_ID = 1matches the script's default DDS channel ID. We use 1 for simulation so that running a script without explicit flags never accidentally sends commands to a real robot (which uses channel 0). If they differ, the two processes can't see each other and you'll getNo LowState received within timeout.INTERFACE = "lo"uses loopback so sim and script can talk on the same machine.ENABLE_ELASTIC_BAND = Truehangs the robot from a virtual strap. TheAnkleSwingPolicyramps to zero pose, which is not a stable standing posture, so without the strap the robot collapses.
Terminal 1:
Ensure env is activate :
source .venv/bin/activateRun the sim :
python ../unitree_mujoco/simulate_python/unitree_mujoco.pyA MuJoCo viewer window opens with G1 loaded.
Terminal 2:
Ensure env is activate :
source .venv/bin/activateRun your code :
python scripts/run_ankle_swing_demo.py --network_interface lo --dds_channel_id 1You should see:
- Warning message and an Enter prompt.
- Press Enter.
- Within ~1 second the policy will start running on the robot.
- In the sim window: robot ramps to zero pose (first 3 seconds), then ankles swing in PR mode (3 seconds), then AB mode with wrist roll.
Stop with Ctrl+C. The robot's stop() releases hands if present and the threads die on process exit.
| Symptom | Cause | Fix |
|---|---|---|
No LowState received within timeout |
Domain ID or interface mismatch between sim and script | Verify DOMAIN_ID = 1 and INTERFACE = "lo" in config.py, and pass lo as the script argument |
| Robot falls immediately and twitches on the ground | ENABLE_ELASTIC_BAND = False (no virtual strap) |
Set True, restart sim, press 9 after it loads |
crc_amd64.so: cannot open shared object file |
unitree_sdk2py installed as a non-editable wheel from git, which dropped the precompiled libs |
Use editable install from a local clone (see setup step 3) |
Like unitree_mujoco, the unitree_sim_isaaclab simulator runs as a separate process and communicates with your control code over DDS, so the existing scripts work without modification.
Requires an NVIDIA GPU with recent drivers. If you don't have one, stick with the MuJoCo setup above.
Create a virtual environment with Python 3.11 and install Isaac Sim:
cd ../
uv venv unitree_sim_env --python 3.11
source unitree_sim_env/bin/activate
uv pip install torch==2.7.0 torchvision==0.22.0 torchaudio==2.7.0 --index-url https://download.pytorch.org/whl/cu126
uv pip install "isaacsim[all,extscache]==5.1.0" --extra-index-url https://pypi.nvidia.comVerify by running isaacsim (first run will ask you to accept the EULA).
git clone https://github.com/isaac-sim/IsaacLab.git ./IsaacLab
sudo apt install cmake build-essential
cd ../IsaacLab
./isaaclab.sh --install
cd ../G1-PlaygroundVerify:
python ../IsaacLab/scripts/tutorials/00_sim/create_empty.pyIt's gonna take a little while but you should end up seeing "[INFO]: Setup complete..." in the terminal.
git clone https://github.com/unitreerobotics/unitree_sim_isaaclab.git ../unitree_sim_isaaclab
cd ../unitree_sim_isaaclab
git submodule update --init --depth 1Build CycloneDDS from source (needed for the unitree_sdk2_python install):
git clone https://github.com/eclipse-cyclonedds/cyclonedds -b releases/0.10.x ../cyclonedds
cd ../cyclonedds && mkdir build install && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=../install
cmake --build . --target install
cd ../../unitree_sim_isaaclabInstall its dependencies:
export CYCLONEDDS_HOME=$(realpath ../cyclonedds/install)
uv pip install wheel
CYCLONEDDS_HOME=$CYCLONEDDS_HOME uv pip install --no-build-isolation -e ../unitree_sdk2_python
uv pip install -e ./teleimager
uv pip install -r requirements.txtFetch the robot assets (requires git-lfs):
bash fetch_assets.shThis downloads USDA/URDF models from HuggingFace and places them in ../assets/.
Return to the project directory:
cd ../G1-PlaygroundBoth unitree_sim_isaaclab and unitree_mujoco use DDS channel 1, which matches the script's default --dds_channel_id.
Terminal 1 (with the unitree_sim_env venv active):
source ../unitree_sim_env/bin/activate
cd ../unitree_sim_isaaclab
python sim_main.py --device cuda --enable_cameras --task Isaac-PickPlace-Cylinder-G129-Dex3-Joint --enable_dex3_dds --robot_type g129Available G1 tasks:
| Task | Hand type | Flag |
|---|---|---|
Isaac-PickPlace-Cylinder-G129-Dex3-Joint |
Dex3 | --enable_dex3_dds |
Isaac-PickPlace-RedBlock-G129-Dex3-Joint |
Dex3 | --enable_dex3_dds |
Isaac-Stack-RgyBlock-G129-Dex3-Joint |
Dex3 | --enable_dex3_dds |
Isaac-Move-Cylinder-G129-Dex3-Wholebody |
Dex3 | --enable_dex3_dds |
Only one hand flag can be active at a time. Joint tasks fix the robot's base in place (good for arm/hand testing), while Wholebody tasks allow full locomotion. Use a Wholebody task when testing policies that move the legs. You can also register your own custom Isaac Lab tasks for deployment testing.
Terminal 2 (with the G1-Playground venv active):
source .venv/bin/activate
python scripts/run_ankle_swing_demo.py --dds_channel_id 1No --network_interface is needed since Isaac Lab communicates via shared memory on the same machine.
| Symptom | Cause | Fix |
|---|---|---|
No LowState received within timeout |
DDS channel mismatch | Ensure --dds_channel_id is 1 (the default) to match the Isaac Lab simulator |
libstdc++.so.6: version GLIBCXX_3.4.30 not found |
System libstdc++ too old for Isaac Sim | sudo apt install libstdc++-12-dev or update your distro's gcc/g++ package |
| EULA prompt blocks startup | First-time Isaac Sim launch | Run isaacsim once manually and accept the EULA |
Could not locate cyclonedds |
Missing CycloneDDS dev libs | sudo apt install cyclonedds-dev |
Hardware is unforgiving. Read this section in full before plugging anything in.
- Hang the robot from an overhead gantry or strap. The
AnkleSwingPolicyramps to zero pose, which from a standing posture means the robot collapses to a crouch and falls. Standing self-balance is not implemented in this repo. - Wireless controller and E-stop within reach. Know which button it is before you start.
- Clear area. No people, no objects, no cables in reach of the robot.
- Charged battery. Low battery causes erratic behavior under load.
- Body-only first. Default to
num_motors=29. Do not attemptnum_motors=43(Dex3 hands) until body-only is solid.
The G1 has an ethernet port for SDK communication. Connect it to your laptop's wired adapter.
Find the interface name:
ifconfigLook for the wired adapter connected to the robot. Common names: enp2s0, enp3s0, eth0. Configure that adapter's IP to be in the same subnet as the robot. The default robot onboard PC IP is 192.168.123.161, this one is private and cannot be ssh-ed into and the dev computer on the robot is 192.168.123.164 (that one we can ssh into).
Verify the connection:
ping 192.168.123.161If ping fails, the network isn't set up correctly. Fix that before going further. The Unitree quick-start docs cover network configuration in detail.
- Power on the G1. It boots through its startup sequence.
- Enter Developer mode : HOLD L2 + CLICK R2
- Go DAMP : HOLD L2 + CLICK B
- Go DEBUG POSE : HOLD L2 + CLICK A
- Go DAMP : HOLD L2 + CLICK B
Do not press L2 + UP for locked standing. Stay in damping mode for the first test.
python scripts/run_ankle_swing_demo.py --network_interface enp2s0 --dds_channel_id 0The real robot uses DDS channel 0 by default. Replace enp2s0 with your actual ethernet interface.
What you should see:
- Script prints the warning and waits for Enter.
- Press Enter.
_release_motion_modeshuts down the onboard sport service. You may hear a small click from the robot or feel it briefly lose stiffness.- State arrives in milliseconds and the wait completes.
- The 500Hz control thread starts publishing commands. The robot ramps to zero pose, then ankles swing, etc.
- Unusual motor noise, vibration, or buzzing → likely a gain mismatch
- A joint jerking or moving unexpectedly during the ramp →
_initial_qmay be wrong - Any joint hitting its mechanical limit → zero pose is outside the safe range for that joint configuration
- Motors getting hot → don't leave it running long-term with stiff gains
Ctrl+C the script. The robot's stop() injects a release action; the threads die when the process exits. Alternatively use an emergency stop if you have one attached to the G1.
Sim and hardware behave differently in two important ways:
-
Hardware exposes timing jitter. The Python control loop at 500Hz is near its CPU budget on a laptop. You may see a tick-tick sound during fast motion that wasn't audible in sim. Drop
control_dtto0.004(250Hz) or run withsudo chrt -f 80 python ...for realtime scheduling priority. Either fixes most of it. -
The default PD gains are starting points, not tuned values.
ARMS_Kp = [40] * 7is uniform across shoulder, elbow, and wrist, but the wrist has much lower inertia and benefits from softerkpand higherkdto avoid ringing. Override per joint in your policy:
kp = DEFAULT_KP.copy()
kd = DEFAULT_KD.copy()
for idx in (G129DofJointIndex.LeftWristRoll, G129DofJointIndex.RightWristRoll):
kp[idx] = 25.0
kd[idx] = 2.0
return JointAction(q=q, kp=kp, kd=kd, mode_pr=Mode.AB)A short explanation of why the code is structured the way it is.
Strategy pattern. UnitreeG1Robot knows nothing about specific behaviors. It owns DDS, threading, CRC, mode_machine handshake, and the motor command layout. The Policy knows nothing about DDS or threading; it consumes a G1State and returns a JointAction. They communicate through a single dataclass boundary, which makes policies trivial to unit-test without a robot.
Two threads The DDS subscriber callback updates state. The policy thread reads state and writes target actions. The control thread reads target actions and publishes commands. Splitting compute from publish further would add lock overhead for no benefit, since they run at the same rate.
G1State bundles body and hand state. When num_motors=43, hand states are populated; otherwise they are None. Policies that don't need hands simply ignore those fields.
Mode encoding differs between body and Dex3 hands. Body motors take mode = 1 for enable. Dex3 motors take a packed RIS_Mode_t byte. This is handled in _apply_hand_actions and _encode_dex3_motor_mode. A subtle bug here is silent on hardware (hands just don't move), so verify carefully when first enabling Dex3.
Stop is best-effort, not clean. RecurrentThread in unitree_sdk2py has no shutdown API. In our stop(), we inject a release action so the hands go limp, waits 50ms for the control thread to publish it, then returns. The threads die when the process exits via sys.exit(0).
MIT.
Built on top of unitree_sdk2_python, unitree_mujoco, and unitree_sim_isaaclab. The architecture borrows the Init/Start lifecycle pattern from Unitree's C++ examples.

