Adyansh04/grove-g1

An autonomy stack for the Unitree G1 humanoid, built on ROS 2.

59

stars

524

commits

C++

primary language

Aug 26, 2026

updated

behavior-tree
humanoid-robotics
moveit
mujoco
nav2
robotics
ros2
ros2-humble
slam
unitree-g1
Browse cluster: Rust for Robotics and ROS2

README

Grove-G1

Grove-G1

CI License: BSD-3-Clause

The earlier Humble line, where Unitree's own internal leg policy walks the robot and this stack only does arm manipulation on top, is kept on humble-unitree.

An autonomy stack for the Unitree G1 humanoid, built on ROS 2 Jazzy and developed simulation-first against unitree_mujoco.

The simulator speaks the same DDS channels as the real robot, so the hardware interface, the navigation stack and the control-authority logic all carry over to hardware without code changes. Moving to the physical G1 is a domain-ID and interface change, not a rewrite.

What it does today

The robot maps a facility with SLAM Toolbox, localizes against the saved map, and drives itself to a goal pose under Nav2. Balance is ours: a learned locomotion policy runs at 50 Hz inside a ros2_control controller and commands all 29 body motors over rt/lowcmd, with no onboard controller underneath. Arm trajectories are an ordinary JointTrajectoryController claiming the 14 arm joints on the same component, so MoveIt executes while the policy keeps the robot up.

MoveIt plans for either arm or both together, collision-checked against a live octomap built from the LiDAR, and each Dex3-1 hand is its own planning group with open and closed postures.

On top of that, pick and place are served as actions, and a BehaviorTree.CPP behaviour tree sequences them with navigation into a mission that runs end to end in the facility world: drive to a workbench, walk the last half metre under closed-loop control, pick a cube up, carry it across the building, and put it down on a bench. Object poses come from a source that refuses to run on hardware, because there is no object-detection pipeline yet; a real one replaces it without the skills changing.

Nav2 parks within 0.5 m of a goal and the arm's usable window is about 0.2 m wide, so a base approach skill closes the gap against the measured object rather than against the map. The tree is editable in Groot2 against a generated node palette.

Learned manipulation is wired up on top of that: a vision-language-action policy proposes joint targets and every chunk is checked against the planning scene before it runs. The pipeline works; the pretrained policy does not grasp reliably, which needs fine-tuning on demonstrations from this robot.

Nav2 Demo

MoveIt Demo

MoveIt Demo

Pick & Place Demo

Pick & Place Demo

Architecture

Grove-G1 architecture

On hardware the simulation card becomes the physical G1 and the LiDAR front end becomes livox_ros_driver2. Everything above the DDS rail is unchanged.

Two rules shape the design, and both apply in simulation so the habits transfer:

  • Only one publisher ever commands rt/lowcmd. Control-mode ownership is explicit, and the hardware component leaves any joint no controller claims unpowered, so "who owns this joint" is also "does this joint hold".
  • Commanding rt/lowcmd means owning balance. There is no onboard controller left underneath to catch a mistake.

Packages

PackageWhat it does
g1_bringupThe entry point. Launch files, scenes and config that compose everything below.
g1_descriptionVendored G1 URDF plus the ros2_control xacro wrapper.
g1_hand_interfaceros2_control plugin for one Dex3-1 hand, over the hand's own SDK channels.
g1_controllersThe locomotion policy, its chained safety controller and the freeze controllers.
g1_hardware_interfaceros2_control plugin owning all 29 body motors over rt/lowcmd.
g1_locomotionWalks the base into arm's reach of a measured object, and backs it out again.
g1_manipulationPick and place as actions, and the object-pose source behind them.
g1_moveit_configMoveIt config: arm and hand planning groups, kinematics, the octomap.
g1_msgsThe mission's own interfaces: pick, place, approach, retreat, arm posture, grasp.
g1_navigationSLAM Toolbox mapping, AMCL localization and Nav2.
g1_orchestrationThe behaviour tree that sequences navigation and manipulation into a mission.
g1_sensor_relayPublishes LiDAR and depth frames sampled inside the simulator.
g1_state_estimationPublishes odom to base_footprint and the TF chain Nav2 needs.
g1_vlaLearned grasping: a policy's action chunks, checked against the planning scene before they run.

There is no demonstration recorder yet, so the learned-grasp path runs a pretrained policy and cannot be fine-tuned on this robot; a recorder, and the LeRobot dataset and policy tooling that would sit beside it, are both still open.

Quick start

The ROS build and runtime commands run inside the dev container, which pins the ROS 2 Jazzy and Ubuntu 24.04 toolchain the stack is built and tested against.

Prerequisites

Install Docker Engine with Docker Compose v2, the NVIDIA driver and the NVIDIA Container Toolkit on the host. The simulator uses the GPU exposed by docker-compose.yml. For the GUI modes, run from an X11 desktop session; manage.sh start grants the container local X11 access.

Install vcstool on the host as well, because the import script uses its vcs command before the development container exists:

sudo apt install python3-vcstool

Start the development container

From the repository root, on the host:

cp .env.example .env
./scripts/import-externals.sh
./scripts/manage.sh start
./scripts/manage.sh exec

import-externals.sh pulls the third-party packages listed in workspace.repos into workspace/src and puts the two that ship a non-standard layout into a buildable one. Run it again whenever workspace.repos changes.

Build

Inside the container:

cd /root/workspace
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
source install/setup.bash

Then pick a demo. The run commands live in their own guides so this page stays short:

GuideWhat it covers
Navigation and arm planningMapping, localization, Nav2 goals, and MoveIt planning against the LiDAR octomap.
Pick and placeThe manipulation skills and the behaviour tree that sequences them with navigation.
Learned graspingA vision-language-action policy behind the planning-scene gate. Runs; does not grasp yet.

Each guide lists the launch arguments it uses and why, and ros2 launch g1_bringup bringup.launch.py --show-args prints the full set.

Development environment

Docker Compose is the runtime source of truth; .devcontainer/devcontainer.json is the VS Code layer on top. In VS Code, use Dev Containers: Reopen in Container.

SettingValue
ROS distroJazzy, pinned.
Middlewarermw_fastrtps_cpp image-wide. The SDK carries its own CycloneDDS, pinned to loopback, and ROS must not load a second one.
ROS_DOMAIN_ID1
Robot overrideGROVE_G1_ROS_DOMAIN_ID, GROVE_G1_CYCLONEDDS_URI, GROVE_G1_ROBOT_NIC
C++ standardC++20 on GCC 13.3
Workspace/root/workspace
Shared data/root/data

The container runs privileged with network_mode: host and a /dev bind mount. That is deliberate for local robotics development: DDS discovery between the bare-DDS simulator and the ROS graph happens over loopback, and device access has to work.

Pointing the container at a real G1 is three environment variables, not an image rebuild: GROVE_G1_CYCLONEDDS_URI=file:///etc/cyclonedds/cyclonedds.hardware.xml (baked in beside the loopback one, differing only in the interface), GROVE_G1_ROBOT_NIC for the NIC that reaches the robot, and GROVE_G1_ROS_DOMAIN_ID for its domain. They are prefixed because the base image's own /etc/profile.d/10-ros-env.sh rewrites the unprefixed names. sim.launch.py refuses to start unless CYCLONEDDS_URI names a profile that pins lo, so the simulator cannot be brought up pointing at a robot.

Lifecycle:

./scripts/manage.sh start | stop | restart | recreate | logs | exec

Use exec-as-me instead of exec for anything that rewrites source files in place, such as clang-tidy --fix or clang-format -i. It runs as your host user, so the files do not come back owned by root.

Project dependencies belong in .devcontainer/Dockerfile, followed by ./scripts/manage.sh recreate. Do not install into a running container and forget about it.

Tests

colcon test --packages-select g1_description g1_locomotion g1_navigation
colcon test-result --all

Leftover nodes from a previous run are the most productive source of phantom failures here: several copies of the stack on one DDS graph look like bugs everywhere except where they are. Clear them first, and note it verifies the graph rather than the process table:

./scripts/clean-stack.sh

Suites that launch a simulator are timing-sensitive and serialize on a shared ctest resource lock. Run them one package at a time, and check nothing is left over from a previous run (pgrep -x unitree_mujoco) before trusting a result: a stray simulator is the usual explanation for a batch of failures that all pass on a clean rerun. Each package README says which of its tests need a simulator.

Those suites carry the ctest label simulator, so the rest can be run on their own:

colcon test --ctest-args -LE simulator   # everything that needs no simulator
colcon test --ctest-args -L  simulator   # only the simulator suites

Continuous integration

Every pull request, and every push to main, builds the workspace and runs the tests that need no simulator, in the image built by .github/ci.Dockerfile. Lint runs as part of colcon test, not separately.

The simulator suites are excluded: they are CPU-time-sensitive and measure a shared runner rather than the stack. Run them locally before merging anything that touches locomotion, navigation or the sensor path.

A per-package C++ coverage table is printed to each run's summary. It covers only the tests CI runs, so the node and launch layer reads low there by construction. It is a signal on the pure logic, not a figure for the repository, which is why there is no badge for it.

Repository layout

.devcontainer/     derived dev image
workspace/src/     ROS 2 packages
workspace/patches/ patches applied to vendored sources at image build
workspace/vendor/  our source compiled into the vendored simulator
scripts/           container lifecycle, and stack teardown

Contributors

Adyansh04

523 commits

lentin-grazen

1 commits

Adyansh04/grove-g1

An autonomy stack for the Unitree G1 humanoid, built on ROS 2.

59

stars

524

commits

C++

primary language

Aug 26, 2026

updated

behavior-tree
humanoid-robotics
moveit
mujoco
nav2
robotics
ros2
ros2-humble
slam
unitree-g1
Browse cluster: Rust for Robotics and ROS2

README

Grove-G1

Grove-G1

CI License: BSD-3-Clause

The earlier Humble line, where Unitree's own internal leg policy walks the robot and this stack only does arm manipulation on top, is kept on humble-unitree.

An autonomy stack for the Unitree G1 humanoid, built on ROS 2 Jazzy and developed simulation-first against unitree_mujoco.

The simulator speaks the same DDS channels as the real robot, so the hardware interface, the navigation stack and the control-authority logic all carry over to hardware without code changes. Moving to the physical G1 is a domain-ID and interface change, not a rewrite.

What it does today

The robot maps a facility with SLAM Toolbox, localizes against the saved map, and drives itself to a goal pose under Nav2. Balance is ours: a learned locomotion policy runs at 50 Hz inside a ros2_control controller and commands all 29 body motors over rt/lowcmd, with no onboard controller underneath. Arm trajectories are an ordinary JointTrajectoryController claiming the 14 arm joints on the same component, so MoveIt executes while the policy keeps the robot up.

MoveIt plans for either arm or both together, collision-checked against a live octomap built from the LiDAR, and each Dex3-1 hand is its own planning group with open and closed postures.

On top of that, pick and place are served as actions, and a BehaviorTree.CPP behaviour tree sequences them with navigation into a mission that runs end to end in the facility world: drive to a workbench, walk the last half metre under closed-loop control, pick a cube up, carry it across the building, and put it down on a bench. Object poses come from a source that refuses to run on hardware, because there is no object-detection pipeline yet; a real one replaces it without the skills changing.

Nav2 parks within 0.5 m of a goal and the arm's usable window is about 0.2 m wide, so a base approach skill closes the gap against the measured object rather than against the map. The tree is editable in Groot2 against a generated node palette.

Learned manipulation is wired up on top of that: a vision-language-action policy proposes joint targets and every chunk is checked against the planning scene before it runs. The pipeline works; the pretrained policy does not grasp reliably, which needs fine-tuning on demonstrations from this robot.

Nav2 Demo

MoveIt Demo

MoveIt Demo

Pick & Place Demo

Pick & Place Demo

Architecture

Grove-G1 architecture

On hardware the simulation card becomes the physical G1 and the LiDAR front end becomes livox_ros_driver2. Everything above the DDS rail is unchanged.

Two rules shape the design, and both apply in simulation so the habits transfer:

  • Only one publisher ever commands rt/lowcmd. Control-mode ownership is explicit, and the hardware component leaves any joint no controller claims unpowered, so "who owns this joint" is also "does this joint hold".
  • Commanding rt/lowcmd means owning balance. There is no onboard controller left underneath to catch a mistake.

Packages

PackageWhat it does
g1_bringupThe entry point. Launch files, scenes and config that compose everything below.
g1_descriptionVendored G1 URDF plus the ros2_control xacro wrapper.
g1_hand_interfaceros2_control plugin for one Dex3-1 hand, over the hand's own SDK channels.
g1_controllersThe locomotion policy, its chained safety controller and the freeze controllers.
g1_hardware_interfaceros2_control plugin owning all 29 body motors over rt/lowcmd.
g1_locomotionWalks the base into arm's reach of a measured object, and backs it out again.
g1_manipulationPick and place as actions, and the object-pose source behind them.
g1_moveit_configMoveIt config: arm and hand planning groups, kinematics, the octomap.
g1_msgsThe mission's own interfaces: pick, place, approach, retreat, arm posture, grasp.
g1_navigationSLAM Toolbox mapping, AMCL localization and Nav2.
g1_orchestrationThe behaviour tree that sequences navigation and manipulation into a mission.
g1_sensor_relayPublishes LiDAR and depth frames sampled inside the simulator.
g1_state_estimationPublishes odom to base_footprint and the TF chain Nav2 needs.
g1_vlaLearned grasping: a policy's action chunks, checked against the planning scene before they run.

There is no demonstration recorder yet, so the learned-grasp path runs a pretrained policy and cannot be fine-tuned on this robot; a recorder, and the LeRobot dataset and policy tooling that would sit beside it, are both still open.

Quick start

The ROS build and runtime commands run inside the dev container, which pins the ROS 2 Jazzy and Ubuntu 24.04 toolchain the stack is built and tested against.

Prerequisites

Install Docker Engine with Docker Compose v2, the NVIDIA driver and the NVIDIA Container Toolkit on the host. The simulator uses the GPU exposed by docker-compose.yml. For the GUI modes, run from an X11 desktop session; manage.sh start grants the container local X11 access.

Install vcstool on the host as well, because the import script uses its vcs command before the development container exists:

sudo apt install python3-vcstool

Start the development container

From the repository root, on the host:

cp .env.example .env
./scripts/import-externals.sh
./scripts/manage.sh start
./scripts/manage.sh exec

import-externals.sh pulls the third-party packages listed in workspace.repos into workspace/src and puts the two that ship a non-standard layout into a buildable one. Run it again whenever workspace.repos changes.

Build

Inside the container:

cd /root/workspace
colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
source install/setup.bash

Then pick a demo. The run commands live in their own guides so this page stays short:

GuideWhat it covers
Navigation and arm planningMapping, localization, Nav2 goals, and MoveIt planning against the LiDAR octomap.
Pick and placeThe manipulation skills and the behaviour tree that sequences them with navigation.
Learned graspingA vision-language-action policy behind the planning-scene gate. Runs; does not grasp yet.

Each guide lists the launch arguments it uses and why, and ros2 launch g1_bringup bringup.launch.py --show-args prints the full set.

Development environment

Docker Compose is the runtime source of truth; .devcontainer/devcontainer.json is the VS Code layer on top. In VS Code, use Dev Containers: Reopen in Container.

SettingValue
ROS distroJazzy, pinned.
Middlewarermw_fastrtps_cpp image-wide. The SDK carries its own CycloneDDS, pinned to loopback, and ROS must not load a second one.
ROS_DOMAIN_ID1
Robot overrideGROVE_G1_ROS_DOMAIN_ID, GROVE_G1_CYCLONEDDS_URI, GROVE_G1_ROBOT_NIC
C++ standardC++20 on GCC 13.3
Workspace/root/workspace
Shared data/root/data

The container runs privileged with network_mode: host and a /dev bind mount. That is deliberate for local robotics development: DDS discovery between the bare-DDS simulator and the ROS graph happens over loopback, and device access has to work.

Pointing the container at a real G1 is three environment variables, not an image rebuild: GROVE_G1_CYCLONEDDS_URI=file:///etc/cyclonedds/cyclonedds.hardware.xml (baked in beside the loopback one, differing only in the interface), GROVE_G1_ROBOT_NIC for the NIC that reaches the robot, and GROVE_G1_ROS_DOMAIN_ID for its domain. They are prefixed because the base image's own /etc/profile.d/10-ros-env.sh rewrites the unprefixed names. sim.launch.py refuses to start unless CYCLONEDDS_URI names a profile that pins lo, so the simulator cannot be brought up pointing at a robot.

Lifecycle:

./scripts/manage.sh start | stop | restart | recreate | logs | exec

Use exec-as-me instead of exec for anything that rewrites source files in place, such as clang-tidy --fix or clang-format -i. It runs as your host user, so the files do not come back owned by root.

Project dependencies belong in .devcontainer/Dockerfile, followed by ./scripts/manage.sh recreate. Do not install into a running container and forget about it.

Tests

colcon test --packages-select g1_description g1_locomotion g1_navigation
colcon test-result --all

Leftover nodes from a previous run are the most productive source of phantom failures here: several copies of the stack on one DDS graph look like bugs everywhere except where they are. Clear them first, and note it verifies the graph rather than the process table:

./scripts/clean-stack.sh

Suites that launch a simulator are timing-sensitive and serialize on a shared ctest resource lock. Run them one package at a time, and check nothing is left over from a previous run (pgrep -x unitree_mujoco) before trusting a result: a stray simulator is the usual explanation for a batch of failures that all pass on a clean rerun. Each package README says which of its tests need a simulator.

Those suites carry the ctest label simulator, so the rest can be run on their own:

colcon test --ctest-args -LE simulator   # everything that needs no simulator
colcon test --ctest-args -L  simulator   # only the simulator suites

Continuous integration

Every pull request, and every push to main, builds the workspace and runs the tests that need no simulator, in the image built by .github/ci.Dockerfile. Lint runs as part of colcon test, not separately.

The simulator suites are excluded: they are CPU-time-sensitive and measure a shared runner rather than the stack. Run them locally before merging anything that touches locomotion, navigation or the sensor path.

A per-package C++ coverage table is printed to each run's summary. It covers only the tests CI runs, so the node and launch layer reads low there by construction. It is a signal on the pure logic, not a figure for the repository, which is why there is no badge for it.

Repository layout

.devcontainer/     derived dev image
workspace/src/     ROS 2 packages
workspace/patches/ patches applied to vendored sources at image build
workspace/vendor/  our source compiled into the vendored simulator
scripts/           container lifecycle, and stack teardown

Contributors

Adyansh04

523 commits

lentin-grazen

1 commits

Languages

C++

61.6%

Python

29.8%

CMake

4.9%

Shell

2.5%

Dockerfile

1.1%