Tutorial: Run the gravity-compensation demo
Drive the Lite arms from a host-side Python process — no rclpy, no
colcon overlay, no --system-site-packages. You'll bring up the stack
(MuJoCo or real), switch to the REMOTE control mode, and run an
external gravity-compensation loop that reads joint states and publishes
MITCommand back over raw DDS. By the end you'll understand the
Tier-3 external-client path, the REMOTE control mode,
and the difference between the torque-mode and PD-mode gravity loops.
This is the worked, end-to-end companion to How-to → Talk to Humanoid Control from Python: that page is the recipe, this is the lesson.
Time + materials
- 20 minutes
- A working workspace build (the bringup side)
- The
Lite-Gravity-Compensationdemo, checked out anywhere outsidehumanoid_control_ws(it's a standalone Tier-3 project with its ownuvenvironment — it only needs to reach the robot's DDS bus, not the colcon workspace) uvon PATH for the demo's own env- No real hardware required — the whole lesson runs in MuJoCo
The mental model
The gravity-comp runner is not a controller and not a ROS node.
It's an ordinary Python process that joins the same DDS network the
bringup uses. The in-process humanoid_control::RemotePolicyController is the only
thing that touches the command interfaces; the external loop just feeds
it MITCommands:
host-side process (pure pip, no rclpy) Humanoid Control bringup (System 0, RT)
┌───────────────────────────────┐ ┌──────────────────────────────┐
│ run_ros2_torque.py │ │ joint_state_broadcaster │
│ each tick @ COMMAND_HZ: │ rt/lite/ │ publishes /lite/joint_states│
│ 1. drain /lite/joint_states │◀───joint_ │ │
│ → 14-joint snapshot │ states │ │
│ 2. mirror into MuJoCo; │ │ │
│ mj_forward + subtree │ │ │
│ COM → gravity torques │ │ RemotePolicyController │
│ (model only, no stepping)│ rt/remote_│ (REMOTE mode) │
│ 3. build MITCommand │ policy_ │ reads ~/command, writes the │
│ 4. pub /remote_policy_ │──controller│ 5 MIT command interfaces │
│ controller/command │ /command ▶│ per joint │
└───────────────────────────────┘ └───────────────┬──────────────┘
▼
MujocoSystem (sim) or
RobstrideSystem (real)
applies τ = K·err + D·errṙ + ff
In the sim bringup, MujocoSystem simulates physics and applies the
torques. The demo's own MuJoCo is a dynamics model only — it never
steps; it just computes gravity-cancelling generalized forces from the
live joint snapshot. They are independent.
Step 1 — Install the demo
The demo has its own uv environment, fully separate from the pixi /
colcon workspace. From the demo checkout:
cd Lite-Gravity-Compensation
uv sync
uv sync resolves the two host-side packages straight from their git
repos — lite_sdk2 (the
publisher/subscriber + QoS registry) and humanoid_control_msgs_dds (the
ROS-wire-compatible CycloneDDS message types, pulled in transitively).
Nothing here links against rclpy.
rclpy?ROS 2 messages are just CDR-serialized DDS types. lite_sdk2 reproduces
the three rmw conventions — the rt/ topic prefix, the
pkg::msg::dds_::Name_ type-name mangling, and RELIABLE/KEEP_LAST QoS —
so cyclonedds-python pairs directly with rmw_cyclonedds_cpp or
rmw_fastrtps_cpp on the bringup. See
How-to → Talk to Humanoid Control from Python.
Step 2 — Bring up the stack
In the bringup terminal (inside pixi shell), start MuJoCo:
cd humanoid_control_ws && pixi shell
ros2 launch humanoid_bringup_lite mujoco.launch.py
Wait for zero_torque_controller to come active. (For real hardware,
use real.launch.py instead — everything downstream is identical.)
Step 3 — Switch to REMOTE
RemotePolicyController accepts commands only while it is the active
controller — i.e. in the REMOTE mode. Switching is flat, so you can
enter REMOTE directly from ZERO_TORQUE (or any mode), with no DAMPING or
STANDBY step first. Press R1+B on the gamepad, or, headless, make the
same switch_controllers call yourself (in a second terminal, inside
pixi shell):
ros2 control switch_controllers \
--activate remote_policy_controller \
--deactivate zero_torque_controller
On the gamepad, R1+A starts LOCOMOTION (the in-process
rl_policy_controller) and R1+B starts REMOTE
(remote_policy_controller). Use the B combo for this demo.
Confirm REMOTE is active before running the loop:
ros2 control list_controllers
# remote_policy_controller humanoid_control/RemotePolicyController active
Step 4 — Run the gravity-comp loop (torque mode)
Back in the demo terminal (the uv env, not pixi). Match
ROS_DOMAIN_ID to the bringup (both default to 0):
cd Lite-Gravity-Compensation
source .venv/bin/activate
python run_ros2_torque.py
You should see the banner and, once the first full joint snapshot arrives, a periodic torque readout:
DDS domain=0 mode=torque joints=14 rate=... Hz model=...
First complete /lite/joint_states received.
[hh:mm:ss] torque mean=0.412 max=1.083
In the MuJoCo viewer the arms now hold against gravity — nudge a
link and it stays where you leave it instead of falling. Torque mode
sends effort = clipped gravity torque, K=0, D=TORQUE_DAMPING, so
the actuator's MIT formula reduces to a gravity-cancelling feedforward
plus a passive viscous brake.
Step 5 — Try PD mode
Stop the torque loop (Ctrl-C — see Safe stop) and run the PD variant instead:
python run_ros2_pd.py
PD mode encodes the same gravity term as a position offset
(position = q + gravity/Kp) with non-zero stiffness/damping
(K=PD_POSITION_KP, D=PD_VELOCITY_KD). The arm feels stiffer and
self-centering rather than free-floating. Both publish in
LITE_ARM_JOINTS order (the arm_joints list in
humanoid_bringup_lite/config/lite_hardware.yaml); RemotePolicyController
rejects a joint-order mismatch.
python run_mujoco.py opens the demo's MuJoCo model in a passive viewer
with the same gravity compensation applied in-process — no DDS, no
bringup. Handy for sanity-checking the model alone.
Step 6 — Safe stop
Ctrl-C publishes one final passive command (K=0,
D=TORQUE_DAMPING, effort=0) — the actuator coasts under damping —
then the process exits. That is not a real stop: the controller stays
in REMOTE, and RemotePolicyController's stale-command fallback (~100 ms
of silence) treats the now-quiet topic as a fault and falls back to
damping.
For a deliberate stop, switch out of REMOTE first — press X on the gamepad, or headless:
ros2 control switch_controllers \
--activate damping_controller \
--deactivate remote_policy_controller
Then Ctrl-C the runner, then the bringup launch.
What you came away with
| Skill | Page where it's documented in detail |
|---|---|
The Tier-3 external-client (no-rclpy) DDS path | How-to → Talk to Humanoid Control from Python |
lite_sdk2 / humanoid_control_msgs_dds topic + QoS registry | Reference → Packages |
| The REMOTE mode + the R1+B controller switch | Concepts → Five control modes |
RemotePolicyController and the MIT command write | Reference → Controllers |
| The five MIT command interfaces (torque vs PD encoding) | Concepts → MIT command surface |
Next
- Tutorials → Run a tracking policy — the in-process counterpart: a policy that runs inside the RT cycle instead of over DDS.
- Concepts → MIT command surface —
why
position/velocity/effort/stiffness/dampingis the whole command surface, and how torque- and PD-mode gravity comp map onto it. - How-to → Switch controllers manually
— more on the
ros2 control switch_controllerspath used above.