FTH 4 · Operar um humanoide real|INEMA.CLUB-PRO

Aula 6 · 20 min

A política como nó ROS 2

O ROS 2 atual é o Lyrical, mas o pacote oficial da Unitree pede o Humble. A saída é um contêiner por robô: Humble lá dentro, só para falar com o G1, e o resto do seu sistema onde quiser. Nesta aula o contêiner enxerga os tópicos do simulador, mede a taxa e roda um nó de política a 50 Hz.

Começar a aula
Dois monitores numa mesa escura, um com janelas de terminal desfocadas e outro com um robô humanoide em simulação, e um pequeno computador entre eles.

Nesta aula você vai

  1. 1Por que Humble em contêiner, se o ROS 2 atual é o Lyrical
  2. 2rt/lowstate no DDS é /lowstate no ROS 2: sem ponte
  3. 3ros2 topic hz contra o simulador do fabricante
  4. 4O esqueleto de um nó de política: assina estado, publica comando

Objetivo: ver os tópicos do G1 no ROS 2 Humble em contêiner, medir a taxa e rodar um nó de política que publica a 50 Hz.

01Um contêiner por robô

Clique em cada bloco.

Clique em cada bloco do diagrama para ver o que ele faz.

Testado em Linux aarch64 com --network host e a interface lo. Em Windows e macOS a rede do Docker é outra: não testado.

02Três decisões do contêiner

Clique em cada uma.

1. Humble isolado

O Humble entra só no contêiner, para falar com o robô. O host fica livre para o ROS 2 atual e para outros robôs, cada um com o seu contêiner.

Suporte oficial a Jazzy ou Lyrical no unitree_ros2: não há (há relato de projeto que fez ponte do G1 no Jazzy em 2026).

03Construir a imagem e ver os tópicos

A imagem leva alguns minutos na primeira vez (apt e colcon). Depois, o simulador sobe em segundo plano e o contêiner lista os tópicos e mede a taxa.

Arquivo · kit/ros2/Dockerfile
# Contêiner ROS 2 Humble só para falar com o robô Unitree (o unitree_ros2 suporta Humble/Foxy, não Jazzy/Lyrical).
# Testado em 19/10/2026: ros:humble-ros-base (arm64) + unitree_ros2 668d1ec (02/07/2026).
FROM ros:humble-ros-base
RUN apt-get update && apt-get install -y --no-install-recommends \
      ros-humble-rmw-cyclonedds-cpp ros-humble-rosidl-generator-dds-idl \
      ros-humble-rosbag2-storage-mcap git python3-colcon-common-extensions \
    && rm -rf /var/lib/apt/lists/*
WORKDIR /unitree
RUN git clone -q https://github.com/unitreerobotics/unitree_ros2 && cd unitree_ros2 && git checkout -q 668d1ec
RUN . /opt/ros/humble/setup.sh && cd unitree_ros2/cyclonedds_ws && \
    colcon build --packages-select unitree_api unitree_go unitree_hg > /dev/null
# Mesmo domínio e mesma interface do simulador (unitree_mujoco: DOMAIN_ID=1, INTERFACE="lo")
ENV RMW_IMPLEMENTATION=rmw_cyclonedds_cpp ROS_DOMAIN_ID=1 \
    CYCLONEDDS_URI='<CycloneDDS><Domain><General><Interfaces><NetworkInterface name="lo"/></Interfaces></General></Domain></CycloneDDS>'
COPY entrada.sh /entrada.sh
ENTRYPOINT ["/bin/bash", "/entrada.sh"]
Terminal · construir a imagem
F=${FTH:-$HOME/fth}; cd $F/operar
docker build -q -t fth-humble kit/ros2 > /dev/null && echo "imagem fth-humble pronta"
docker run --rm fth-humble bash -c 'echo "ROS 2 $ROS_DISTRO · RMW $RMW_IMPLEMENTATION · domínio $ROS_DOMAIN_ID"; ros2 pkg list | grep unitree'
Terminal · tópicos e taxa
F=${FTH:-$HOME/fth}; cd $F/operar && . $F/.venv/bin/activate
python3 kit/sim.py --s 20 --suspenso > /dev/null &
until python3 kit/monitor.py --s 0.5 > /dev/null; do sleep 0.5; done
docker run --rm --network host fth-humble ros2 topic list 2>/dev/null
docker run --rm --network host fth-humble timeout -s INT 8 ros2 topic hz /lowstate --window 1000 2>/dev/null | awk '/average rate/ {r=$3} END {printf "average rate ≈ %d Hz\n", int((r+5)/10)*10}'
wait
Exemplo do repositório
imagem fth-humble pronta
ROS 2 humble · RMW rmw_cyclonedds_cpp · domínio 1
unitree_api
unitree_go
unitree_hg
/lowcmd
/lowstate
/parameter_events
/rosout
/sportmodestate
/wirelesscontroller
average rate ≈ 200 Hz

Humble, CycloneDDS, domínio 1 e as três famílias de mensagem da Unitree. Os tópicos do simulador aparecem sem o rt/ e o /lowstate chega a cerca de 200 Hz.

04Um nó de política a 50 Hz

O nó assina /lowstate, roda a "política" a cada 20 ms e publica /lowcmd. A política daqui é a mais segura possível (amortecimento); troque o miolo de passo() pela sua rede.

Arquivo · kit/ros2/no_politica.py
"""no_politica.py — o esqueleto de uma política como nó ROS 2 (Humble, dentro do contêiner fth-humble).

Assina /lowstate (unitree_hg/msg/LowState), roda a "política" a 50 Hz e publica /lowcmd (unitree_hg/msg/LowCmd).
A política daqui é a mais segura possível: amortecimento (kp = 0, kd = 2) em todos os motores. Troque o miolo
de passo() pela sua rede (ONNX, PyTorch) — o resto do nó fica igual.
Para o robô real faltam duas coisas que o simulador não cobra: o CRC do comando (os exemplos oficiais
calculam: motor_crc_hg.cpp no unitree_ros2, CRC().Crc no SDK Python; o simulador não confere) e o laço
de 2 ms do exemplo oficial entre a política e o motor.
"""
import math
import sys

import rclpy
from rclpy.node import Node
from unitree_hg.msg import LowCmd, LowState


class Politica(Node):
    def __init__(self, segundos):
        super().__init__("fth_politica")
        self.estado, self.recebidos, self.enviados, self.incl = None, 0, 0, 0.0
        self.create_subscription(LowState, "lowstate", self.chegou, 10)
        self.pub = self.create_publisher(LowCmd, "lowcmd", 10)
        self.create_timer(0.02, self.passo)  # 50 Hz
        self.fim = self.get_clock().now().nanoseconds + int(segundos * 1e9)

    def chegou(self, msg):
        self.estado, self.recebidos = msg, self.recebidos + 1
        w, x, y, z = msg.imu_state.quaternion
        self.incl = math.degrees(math.acos(max(-1.0, min(1.0, 1 - 2 * (x * x + y * y)))))

    def passo(self):
        if self.estado is None:
            return  # sem estado, sem comando
        cmd = LowCmd()
        cmd.mode_pr, cmd.mode_machine = 0, self.estado.mode_machine
        for i in range(29):
            m = cmd.motor_cmd[i]
            m.mode, m.q, m.dq, m.tau, m.kp, m.kd = 1, 0.0, 0.0, 0.0, 0.0, 2.0
        self.pub.publish(cmd)
        self.enviados += 1


def main():
    segundos = float(sys.argv[1]) if len(sys.argv) > 1 else 5.0
    rclpy.init()
    no = Politica(segundos)
    while rclpy.ok() and no.get_clock().now().nanoseconds < no.fim:
        rclpy.spin_once(no, timeout_sec=0.05)
    taxa = no.enviados / segundos
    print(f"nó fth_politica: {segundos:g} s · /lowstate recebido: {'sim' if no.recebidos else 'NÃO'}"
          f" · tronco em pé (inclinação < 20°): {'sim' if no.incl < 20 else 'não'}")
    print(f"/lowcmd publicado a ≈ {round(taxa):.0f} Hz (alvo 50 Hz) · comando: amortecimento")
    no.destroy_node()
    rclpy.shutdown()
    sys.exit(0 if no.recebidos and 45 <= taxa <= 55 else 1)


if __name__ == "__main__":
    main()
Terminal · rodar o nó contra o simulador
F=${FTH:-$HOME/fth}; cd $F/operar && . $F/.venv/bin/activate
python3 kit/sim.py --s 15 --suspenso > /dev/null &
until python3 kit/monitor.py --s 0.5 > /dev/null; do sleep 0.5; done
docker run --rm --network host -v $PWD/kit/ros2:/kit:ro fth-humble python3 /kit/no_politica.py 5 2>/dev/null; echo "saída: $?"
wait
Exemplo do repositório
nó fth_politica: 5 s · /lowstate recebido: sim · tronco em pé (inclinação < 20°): sim
/lowcmd publicado a ≈ 50 Hz (alvo 50 Hz) · comando: amortecimento
saída: 0

O nó recebeu o estado, publicou a 50 Hz e o robô suspenso ficou de tronco reto, amortecido.

05Prática

ROS 2 do robô, isolado e medido.

E1 · A imagem

10 min

Construa a imagem fth-humble com o bloco e confira as mensagens da Unitree.

Pronto quando: a saída lista unitree_api, unitree_go e unitree_hg depois de ROS 2 humble · RMW rmw_cyclonedds_cpp · domínio 1.

E2 · Tópicos e taxa

5 min

Rode o bloco dos tópicos. Depois rode só o ros2 topic list sem --network host e compare.

Pronto quando: /lowstate e /lowcmd na lista e average rate ≈ 200 Hz; sem a rede do host, só /parameter_events e /rosout.

E3 · O nó de política

10 min

Rode o nó contra o simulador. Depois mude o timer para 0,01 s e preveja a nova taxa antes de rodar.

Pronto quando: python3 /kit/no_politica.py 5 no contêiner → /lowcmd publicado a ≈ 50 Hz (alvo 50 Hz) e saída: 0.

06Teste da aula

A explicação aparece depois da sua resposta.

Pergunta 1 de 4

Por que o curso usa ROS 2 Humble se o atual é o Lyrical?competência

07Em três frases

ContêinerHumble só no contêiner do robô; o host fica no ROS 2 atual.
Rede--network host, CycloneDDS, domínio e interface do robô.
NóAssina /lowstate, publica /lowcmd; a política é o miolo.

Para ir além

Aula anteriorPróxima aula