FTH 4 · Operating a real humanoid|INEMA.CLUB-PRO

Lesson 2 · 20 min

Inside the software: DDS, SDK, high and low level

Your program does not call a robot function: it publishes and subscribes to messages on a DDS bus. In this lesson you set up the manufacturer's bench (simulator, SDK, and CycloneDDS 0.10.2), bring up the simulated G1, and read the first state message, through the same path you would use with the real robot.

Start the lesson
Laptop on a bench connected by a network cable to a blurred humanoid robot in the background, with a network switch with subtle lights.

In this lesson you will

  1. 1The path: your program → SDK → DDS → robot or simulator
  2. 2The bench in ~/fth with pinned versions, including the compiled CycloneDDS
  3. 3High level (ready-made locomotion) × low level (motor by motor)
  4. 4Domain and interface: the mistake that makes the robot look dead

Goal: set up the manufacturer's bench (simulator, SDK, CycloneDDS 0.10.2) and read the G1's first state message over DDS.

01From your program to the motor

Click each block.

Click each block in the diagram to see what it does.

Swapping the simulator for the robot changes the domain and the interface, not your code.

02Three things to know before installing

Click each one.

1. High × low

High level: the manufacturer's locomotion controller walks and you ask for a velocity (LocoClient in the "sport" service: SetVelocity, Move, Damp, ZeroTorque).

Low level: you command each motor through rt/lowcmd. First, the official example releases the internal controller (MotionSwitcherClient, CheckMode and ReleaseMode), otherwise the two fight over the motors.

03Set up the bench (once)

A venv in ~/fth (the same one from course 0, if you took it), the simulator and SDK pinned to commits, and CycloneDDS 0.10.2 compiled: pip alone fails on aarch64 without the C library. You need python3.12 (with venv), git, cmake, and a C compiler.

Terminal · simulator and SDK
F=${FTH:-$HOME/fth}; mkdir -p $F && cd $F
[ -d .venv ] || python3.12 -m venv .venv; . .venv/bin/activate
pip install -q mujoco==3.16.0 numpy pygame==2.6.1 mcap==1.6.0
[ -d operar ] || git clone -q --depth 1 https://github.com/inematds/formacao-fth-operar-v72 operar
[ -d unitree_mujoco ] || git clone -q --filter=blob:none --sparse https://github.com/unitreerobotics/unitree_mujoco
(cd unitree_mujoco && git sparse-checkout set simulate_python unitree_robots/g1 && git checkout -q 1eb6642)
[ -d unitree_sdk2_python ] || git clone -q https://github.com/unitreerobotics/unitree_sdk2_python
(cd unitree_sdk2_python && git checkout -q 814556d)
for r in unitree_mujoco unitree_sdk2_python; do git -C $r log -1 --format="$r %h (%ad)" --date=short; done
Terminal · CycloneDDS 0.10.2 and SDK
F=${FTH:-$HOME/fth}; cd $F && . .venv/bin/activate
[ -d cyclonedds ] || git clone -q --depth 1 -b 0.10.2 https://github.com/eclipse-cyclonedds/cyclonedds 2>/dev/null
cmake -S cyclonedds -B cyclonedds/build -DCMAKE_INSTALL_PREFIX=$F/cyclonedds/install -DBUILD_EXAMPLES=OFF > /dev/null 2>&1
cmake --build cyclonedds/build --target install -j 4 > /dev/null 2>&1 && echo "cyclonedds C 0.10.2 instalado em $F/cyclonedds/install"
export CYCLONEDDS_HOME=$F/cyclonedds/install
pip install -q cyclonedds==0.10.2 && pip install -q -e unitree_sdk2_python
python3 -c "from importlib.metadata import version as v; import unitree_sdk2py; print('cyclonedds', v('cyclonedds'), '· unitree_sdk2py', v('unitree_sdk2py'), '· mujoco', v('mujoco'))"
Example from the repository
unitree_mujoco 1eb6642 (2026-09-07)
unitree_sdk2_python 814556d (2026-09-21)
cyclonedds C 0.10.2 instalado em ~/fth/cyclonedds/install
cyclonedds 0.10.2 · unitree_sdk2py 1.0.1 · mujoco 3.16.0

The two commits and the three versions. The build takes a few minutes; the compiler warning is hidden in /dev/null.

04Bring up the simulated G1 and read the state

kit/sim.py runs the unitree_mujoco bridge with no window, in the background, for 8 s; ler_estado.py is another process that only subscribes to rt/lowstate. The G1 hangs from the elastic band (the simulator's gantry).

Terminal · simulator + reading
F=${FTH:-$HOME/fth}; cd $F/operar && . $F/.venv/bin/activate
python3 kit/sim.py --s 8 --suspenso &
python3 kit/ler_estado.py; echo "saída: $?"
wait
Excerpt · kit/op.py
class Estado:
    """Assina rt/lowstate e guarda a última mensagem e quantas chegaram."""

    def __init__(self):
        from unitree_sdk2py.core.channel import ChannelSubscriber
        from unitree_sdk2py.idl.unitree_hg.msg.dds_ import LowState_
        self.ultima, self.n, self.t_ultima = None, 0, 0.0
        self.sub = ChannelSubscriber("rt/lowstate", LowState_)
        self.sub.Init(self._chegou, 10)
Shown · only with the real robot, after the checklist
# Trecho de example/g1/low_level/g1_low_level_example.py (unitree_sdk2_python 814556d)
ChannelFactoryInitialize(0, sys.argv[1])   # domínio 0 + nome da placa ligada ao robô
self.msc = MotionSwitcherClient()
self.msc.SetTimeout(5.0)
self.msc.Init()
status, result = self.msc.CheckMode()
while result['name']:                      # libera o controle interno antes do baixo nível
    self.msc.ReleaseMode()
    status, result = self.msc.CheckMode()
    time.sleep(1)
Example from the repository
simulador: G1 29 motores · DDS domínio 1 em lo · 8 s · suspenso: sim
tópico: rt/lowstate · tipo: unitree_hg LowState_
IMU quatérnio (w, x, y, z): [1.0, 0.0, 0.1, 0.0] · tronco em pé (inclinação < 20°): sim
junta 0 (left_hip_pitch): q = 0.0 rad · torque estimado = 0 N·m
motores no pacote: 35 (o G1 de 29 GDL usa os índices 0 a 28)
saída: 0
simulador: fim em t = 8.0 s · robô suspenso (pés fora do chão)

The packet has 35 motor slots; the 29-DoF G1 uses the first 29. Loose motors: zero torque.

05Practice

Bench ready and first message read.

E1 · Set up the bench

15 min

Run the two setup blocks. If cmake complains, install cmake and a C compiler with your system's package manager and run the second block again.

Done when: the output shows cyclonedds 0.10.2 · unitree_sdk2py 1.0.1 · mujoco 3.16.0.

E2 · Read the G1

5 min

Run the simulator block with the reading and explain why the packet has 35 motors if the G1 has 29.

Done when: python3 kit/ler_estado.py; echo "saída: $?" (with sim.py running) → motores no pacote: 35 (o G1 de 29 GDL usa os índices 0 a 28) and saída: 0.

E3 · See the domain error

3 min

Run only ler_estado.py, without the simulator, and wait the 10 s.

Done when: ERRO: nenhuma mensagem em rt/lowstate em 10 s and saída: 1.

06Lesson quiz

The explanation appears after your answer.

Question 1 of 4

ler_estado.py waited 10 s and then failed. What is the most likely cause?skill

07In three sentences

BusProgram and robot talk over DDS: rt/lowstate and rt/lowcmd.
DomainSimulator on domain 1 on lo; robot on 0 on the network card.
Benchunitree_mujoco 1eb6642, SDK 814556d and CycloneDDS 0.10.2.

Going further

Previous lessonNext lesson