This repository provides a Dockerized ROS 2 Humble environment for collecting real-robot system identification data on a Unitree Go2 and then fitting robot parameters from those datasets.
The setup is built around two mounted repositories:
unitree-ros2-dlshandles the ROS 2, CycloneDDS, and Unitree hardware bridge.sim2real-robot-identificationhandles trajectory generation, dataset recording, replay, and parameter fitting.
The container is designed to feel close to a local ROS install:
- you edit code on the host
- both repos are bind-mounted into the container
- Python scripts can be run directly from the mounted source tree
- C++ packages can be built inside the container without copying files around
In practice:
- Python edits in
sim2real-robot-identificationcan be run immediately - C++ edits in
unitree-ros2-dls/ros2_wsneed a rebuild withcolcon build
We set the Kp and Kd gains to be the same as the deploy.yaml seen in the deploy code for both unitree_rl_mjlab and our MPC-Injection project:
stiffness: [20, 20, 40, 20, 20, 40, 20, 20, 40, 20, 20, 40]
damping: [ 1, 1, 2, 1, 1, 2, 1, 1, 2, 1, 1, 2]
default_joint_pos: [-0.1,0.9,-1.8, 0.1,0.9,-1.8, -0.1,0.9,-1.8, 0.1,0.9,-1.8]# Build the per-joint PD gain arrays like MjLab Go2 actuator config
# Hip joints: Kp=20, Kd=1
# Thigh joints: Kp=20, Kd=1
# Calf joints: Kp=40, Kd=2
default_kp_map = {'hip_joint': 20.0, 'thigh_joint': 20.0, 'calf_joint': 40.0}
default_kd_map = {'hip_joint': 1.0, 'thigh_joint': 1.0, 'calf_joint': 2.0}Dockerfile: builds the ROS 2 Humble image and installs the Python dependencies needed for collection and MuJoCo-based fittingMakefile: starts or attaches to the long-lived interactive containerunitree-ros2-dls: Unitree DDS workspace plus the DLS HAL bridgesim2real-robot-identification: collection scripts, datasets, MuJoCo sysid flow, and IsaacLab/PACE flow
The Dockerfile:
- starts from
osrf/ros:humble-desktop - installs the ROS 2 packages needed by the Unitree/CycloneDDS workflow
- installs Python packages used by collection and MuJoCo fitting
- auto-sources
/opt/ros/humble/setup.bash - auto-sources
unitree-ros2-dls/ros2_connect.bashin new shells once the Unitree workspace has been built
The Makefile is what makes the container feel like a local development environment.
make buildbuilds the Docker imagemake runstarts the interactive container with:--network host--privileged- X11 forwarding for MuJoCo rendering
- bind mounts for both repos
- a shared shell history file
- the working directory set to
/root/sim2real-robot-identification
make attachopens another shell in the already-running container
Because the repos are bind-mounted, any edits you make on the host are visible immediately in the container.
Do not merge both repositories into one colcon workspace.
Both repos contain a dls2_interface package:
unitree-ros2-dls/ros2_ws/src/dls2_interfacesim2real-robot-identification/ros2_ws/src/dls2_interface
The correct layout is:
- build and source only the ROS 2 workspace in
unitree-ros2-dls - treat
sim2real-robot-identificationas a mounted Python project that importsdls2_interfacefrom the sourced ROS environment
If you build both ROS workspaces, you can end up with duplicate message packages and confusing ROS environment resolution.
unitree-ros2-dls is responsible for:
- sourcing the Unitree CycloneDDS workspace
- building and sourcing its own
ros2_ws - exposing DLS-compatible ROS topics
- translating between Unitree low-level messages and DLS messages
Its main entry points are:
unitree-ros2-dls/ros2_connect.bashunitree-ros2-dls/launch_quadruped_hal.pyunitree-ros2-dls/ros2_ws/src/quadruped_hal/quadruped_hal.cpp
The HAL:
- subscribes to Unitree
/lowstate - republishes DLS
/imu - republishes DLS
/blind_state - subscribes to DLS
/trajectory_generator - subscribes to DLS
/control_signal - publishes Unitree
/lowcmd
sim2real-robot-identification is responsible for:
- generating reference trajectories from the MuJoCo robot model
- publishing desired joint trajectories on
/trajectory_generator - recording measured joint states from
/blind_state - saving datasets to
datasets/<robot>/traj_<N>.pt - replaying datasets in MuJoCo
- fitting actuator parameters with MuJoCo sysid
- optionally running the IsaacLab/PACE identification path in a separate environment
For the Go2 workflow, the most important script is:
The two repos communicate through the DLS message package, especially:
BlindState.msgTrajectoryGenerator.msg
The collection script imports those messages from the sourced ROS environment built in unitree-ros2-dls.
This is one of the most important details in the whole setup.
Unitree low-level joint order is:
FRFLRRRL
The DLS/sim2real side expects:
FLFRRLRR
quadruped_hal.cpp performs that remapping in both directions so the Go2 collection script can stay in DLS order while the robot still receives the correct Unitree motor indices.
Before running on a real Go2, make sure you have:
- a Linux host with Docker installed
- X11 available if you want MuJoCo rendering inside the container
- an Ethernet connection from the host to the robot
- the host Ethernet interface configured for the Unitree subnet
- the robot safely suspended in the air for data collection
Recommended host network settings for the Ethernet interface connected to the robot:
- IPv4 mode: manual
- address:
192.168.123.99 - netmask:
255.255.255.0
If DDS traffic is blocked, check your firewall rules for UDP traffic in the CycloneDDS range.
The Unitree repo relies on git submodules. From the host:
git -C unitree-ros2-dls submodule update --init --recursivemake buildmake runThis opens a shell in /root/sim2real-robot-identification.
Inside the container:
cd /root/unitree-ros2-dls/unitree_ros2/cyclonedds_ws
colcon buildAfter this succeeds, new shells in the container will auto-source ros2_connect.bash.
If you later modify C++ code in unitree-ros2-dls/ros2_ws, rebuild that workspace manually with:
cd /root/unitree-ros2-dls/ros2_ws
colcon buildEdit unitree-ros2-dls/ros2_connect.bash and replace the hardcoded interface name in CYCLONEDDS_URI with the Ethernet NIC connected to the robot.
For example, change:
<NetworkInterface name="enp98s0" priority="default" multicast="default" />to your actual NIC name such as:
<NetworkInterface name="enp3s0" priority="default" multicast="default" />Inside the container:
source /root/unitree-ros2-dls/ros2_connect.bash
ros2 topic listIf the connection is correct, you should see Unitree topics such as /lf/lowstate.
This is the recommended operator sequence for a first real Go2 run.
- connect the robot by Ethernet
- set the host NIC to the Unitree subnet
- suspend the robot safely so the legs can move freely
- make sure the area around the robot is clear
On the host:
make build
make runInside the container:
cd /root/unitree-ros2-dls/unitree_ros2/cyclonedds_ws
colcon build
source /root/unitree-ros2-dls/ros2_connect.bashBefore running that source command, make sure you already edited unitree-ros2-dls/ros2_connect.bash to use the Ethernet interface that is physically connected to the robot.
Inside the container:
ros2 topic listYou should see the Unitree low-level topics. If you do not, the problem is usually one of:
- wrong Ethernet interface name
- host IP configuration
- firewall rules
- robot not reachable on the cable
Keep the first shell open, then from the host start a second shell:
make attachIn both terminals, make sure the Unitree environment is sourced:
source /root/unitree-ros2-dls/ros2_connect.bashIn terminal 1:
cd /root/unitree-ros2-dls
python3 launch_quadruped_hal.pyThis is the process that bridges:
- Unitree
/lowstateto DLS/blind_stateand/imu - DLS
/trajectory_generatorand/control_signalto Unitree/lowcmd
launch_quadruped_hal.py expects to find ros2_connect.bash relative to the current working directory, so do not launch it from /root/sim2real-robot-identification.
If you prefer to run the HAL manually instead of using the launcher script:
cd /root/unitree-ros2-dls
source ros2_connect.bash
source ros2_ws/install/setup.bash
./ros2_ws/install/quadruped_hal/bin/quadruped_halBefore running the collection script, edit two settings in sim2real-robot-identification/config.py and sim2real-robot-identification/run_collection_quadruped_ros2.py:
- set
robot = 'go2'inconfig.py - keep the
KpandKdvalues inconfig.pyset to the gains you want to use for both collection and fitting - set
USE_MUJOCO_SIMULATION = Falsefor the real robot - optionally set
USE_MUJOCO_RENDER = Falseif you do not want the MuJoCo viewer window
In terminal 2:
cd /root/sim2real-robot-identification
python3 run_collection_quadruped_ros2.pyNotes:
- the Go2 XML already contains the required
down,sys_id_1, andsys_id_2keyframes run_collection_quadruped_ros2.pydefaults to MuJoCo simulation until you change that flag in the file
When the collection script starts, use:
startCollection
Then choose:
trajectory
The script will generate the chirp-based joint motion, publish the desired trajectory on /trajectory_generator, and record the measured state coming back from /blind_state.
The console help/autocomplete text in the upstream repo is a little stale. The command you need for this workflow is still startCollection.
Collected trajectories are saved in:
sim2real-robot-identification/datasets/go2/traj_<N>.pt
For a first run, the file is typically:
datasets/go2/traj_0.pt
Inside the container:
cd /root/sim2real-robot-identification
python3 datasets/replay_dataset_quadruped.pyThis replays the recorded joint positions in MuJoCo so you can visually inspect the trajectory.
Inside the container:
cd /root/sim2real-robot-identification
python3 sysid_mujoco/my_fit.py --dataset datasets/go2/traj_0.ptThis identifies per-joint:
dampingarmaturefrictionloss
The fit output is written under:
sim2real-robot-identification/sysid_mujoco/results/go2/<timestamp>/
Before running my_fit.py, keep config.py aligned with the dataset you collected:
robotshould still bego2KpandKdshould still match the gains used during collection
my_fit.py does not automatically overwrite the normal Go2 robot XML you use for simulation.
What it identifies are the per-joint mechanical parameters:
dampingarmaturefrictionloss
The generated sysid model is only an internal fitting model. It is not your final robot model, because the sysid pipeline converts the robot to a fixed-base version, removes some elements, rewrites actuators, and disables collisions.
So after step 11, the practical workflow is:
- Open the generated report:
sim2real-robot-identification/sysid_mujoco/results/go2/<timestamp>/report.html
-
Read off the optimized values for each Go2 joint.
-
Copy those values into the full Go2 MJCF used for normal simulation:
sim2real-robot-identification/robot_model/go2/go2.xml
In go2.xml, each actuated joint already has these attributes. For example:
<joint name="FL_hip_joint" class="abduction" damping="0.1531" armature="0.0010" frictionloss="0.2884"/>After fitting, replace those numbers with the identified values from the report for every actuated joint:
FL_hip_jointFL_thigh_jointFL_calf_jointFR_hip_jointFR_thigh_jointFR_calf_jointRL_hip_jointRL_thigh_jointRL_calf_jointRR_hip_jointRR_thigh_jointRR_calf_joint
Example:
<joint name="FL_hip_joint" class="abduction" damping="NEW_VALUE" armature="NEW_VALUE" frictionloss="NEW_VALUE"/>Recommended practice:
- keep the original
go2.xmlas your nominal model - save a calibrated copy such as
go2_sysid.xmlif you want to compare nominal vs identified parameters - if you make a calibrated copy, make sure the scene file or downstream code points to that calibrated XML
If you also use gym-quadruped, update the Go2 XML inside that repository too. gym-quadruped loads its own robot XML from its own package, so changing the XML in this repository does not automatically update the gym-quadruped copy.
Important:
my_fit.pyidentifies mechanical joint parameters, not new PD gains- the
KpandKdused during collection are part of the fitting setup, but the values you copy back into the XML aredamping,armature, andfrictionloss
The real-robot collection loop is:
run_collection_quadruped_ros2.pyloads the Go2 MuJoCo model- it reads the
sys_id_1andsys_id_2keyframes - it generates a chirp-like joint position trajectory
- it publishes desired joint position, velocity,
kp, andkdon/trajectory_generator quadruped_halconverts that command into Unitree low-level motor commands- the Go2 executes the command and publishes
/lowstate quadruped_halremaps Unitree joint order into DLS order and republishes/blind_state- the collection script records actual position and velocity plus the desired reference
- the trajectory is saved as a
.ptdataset - the MuJoCo sysid scripts use that dataset to fit actuator parameters
The Docker image in this repo is intended to support:
- ROS 2 collection
- dataset replay
- MuJoCo-based fitting with
sysid_mujoco/my_fit.py
The IsaacLab/PACE identification flow in sim2real-robot-identification/sysid_isaaclab is a separate environment and is not fully provided by this Docker image.
Use that path only if you also set up the upstream IsaacLab dependencies.
- Build and source only
unitree-ros2-dls/ros2_ws. Do not buildsim2real-robot-identification/ros2_wsas a second workspace. - The hardcoded NIC name inside
unitree-ros2-dls/ros2_connect.bashmust match the Ethernet interface connected to the robot. make runsetsROS_DOMAIN_ID=100, butros2_connect.bashlater setsROS_DOMAIN_ID=0, so0is the effective domain once you source the Unitree environment.make runmounts the local repos directly, so host-side edits appear instantly inside the container.- The Go2 collection path assumes the robot is suspended off the ground.
- If the MuJoCo viewer is not available, set
USE_MUJOCO_RENDER=Falseinrun_collection_quadruped_ros2.py. - If
ros2 topic listdoes not show Unitree topics, check the NIC, host IP, cable, and firewall before debugging the Python scripts.
The intended workflow is:
- use the root Docker image and
maketargets for the runtime environment - use
unitree-ros2-dlsas the only ROS 2 workspace you build and source - run the Go2 HAL from
unitree-ros2-dls - run the collection and fitting scripts from
sim2real-robot-identification - save datasets from the real robot and fit the model parameters from those recordings