E1 · A imagem
10 minConstrua 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.
Aula 6 · 20 min
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
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.
Clique em cada bloco.
Testado em Linux aarch64 com --network host e a interface lo. Em Windows e macOS a rede do Docker é outra: não testado.
Clique em cada uma.
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).
O robô fala CycloneDDS 0.10.2. O README do unitree_ros2 manda compilar o CycloneDDS no Foxy, mas diz que no Humble esse passo pode ser pulado: o rmw_cyclonedds_cpp do apt serve.
As variáveis RMW_IMPLEMENTATION, ROS_DOMAIN_ID e CYCLONEDDS_URI já vêm no Dockerfile do kit.
A prosa do README do unitree_ros2 ainda descreve o LowState do Go2. Para o G1, valem os arquivos de unitree_hg/msg/ (LowState, LowCmd, MotorCmd).
No robô real, o comando também precisa do CRC: o unitree_ros2 traz motor_crc_hg.cpp; o simulador não confere.
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.
# 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"]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'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}'
waitimagem 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.
/parameter_events /rosout
Sem a rede do host, o contêiner fica numa rede própria e não enxerga o DDS do simulador: só os tópicos internos do ROS 2.
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.
"""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()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: $?"
waitnó 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.
ROS 2 do robô, isolado e medido.
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.
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.
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.
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
Pergunta 2 de 4
No ROS 2, qual é o nome do tópico rt/lowstate do SDK?
Pergunta 3 de 4
O contêiner listou só /parameter_events e /rosout. O que faltou?
Pergunta 4 de 4
O nó publica /lowcmd a 50 Hz. O que falta para isso ir ao robô real?
Escolha uma alternativa. A explicação aparece depois da sua tentativa.