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

Clase 6 · 20 min

La policy como nodo ROS 2

El ROS 2 actual es Lyrical, pero el paquete oficial de Unitree pide Humble. La salida es un contenedor por robot: Humble adentro, solo para hablar con el G1, y el resto de tu sistema donde quieras. En esta clase el contenedor ve los tópicos del simulador, mide la tasa y ejecuta un nodo de policy a 50 Hz.

Empezar la clase
Dos monitores sobre un escritorio oscuro: uno con ventanas de terminal desenfocadas y otro con un robot humanoide en simulación, y una pequeña computadora entre ellos.

En esta clase vas a

  1. 1Por qué Humble en contenedor, si el ROS 2 actual es Lyrical
  2. 2rt/lowstate en DDS es /lowstate en ROS 2: sin puente
  3. 3ros2 topic hz contra el simulador del fabricante
  4. 4El esqueleto de un nodo de policy: se suscribe al estado, publica comando

Objetivo: ver los tópicos del G1 en ROS 2 Humble en contenedor, medir la tasa y ejecutar un nodo de policy que publica a 50 Hz.

01Un contenedor por robot

Haz clic en cada bloque.

Haz clic en cada bloque del diagrama para ver qué hace.

Probado en Linux aarch64 con --network host y la interfaz lo. En Windows y macOS la red de Docker es distinta: no probado.

02Tres decisiones del contenedor

Haz clic en cada una.

1. Humble aislado

Humble entra solo en el contenedor, para hablar con el robot. El host queda libre para el ROS 2 actual y para otros robots, cada uno con su contenedor.

Soporte oficial de Jazzy o Lyrical en unitree_ros2: no hay (hay un proyecto reportado que hizo un puente del G1 en Jazzy en 2026).

03Construir la imagen y ver los tópicos

La imagen tarda unos minutos la primera vez (apt y colcon). Después, el simulador arranca en segundo plano y el contenedor lista los tópicos y mide la tasa.

Archivo · 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 la imagen
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 y tasa
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
Ejemplo del repositorio
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, dominio 1 y las tres familias de mensajes de Unitree. Los tópicos del simulador aparecen sin el rt/ y /lowstate llega a unos 200 Hz.

04Un nodo de policy a 50 Hz

El nodo se suscribe a /lowstate, ejecuta la "policy" cada 20 ms y publica /lowcmd. La policy de aquí es la más segura posible (amortiguación); cambia el núcleo de passo() por tu red.

Archivo · 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 · ejecutar el nodo contra el 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
Ejemplo del repositorio
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

El nodo recibió el estado, publicó a 50 Hz y el robot suspendido quedó con el tronco recto, amortiguado.

05Práctica

El ROS 2 del robot, aislado y medido.

E1 · La imagen

10 min

Construye la imagen fth-humble con el bloque y revisa los mensajes de Unitree.

Listo cuando: la salida lista unitree_api, unitree_go y unitree_hg después de ROS 2 humble · RMW rmw_cyclonedds_cpp · domínio 1.

E2 · Tópicos y frecuencia

5 min

Ejecuta el bloque de los tópicos. Después ejecuta solo el ros2 topic list sin --network host y compara.

Listo cuando: /lowstate y /lowcmd en la lista y average rate ≈ 200 Hz; sin la red del host, solo /parameter_events y /rosout.

E3 · El nodo de política

10 min

Ejecuta el nodo contra el simulador. Después cambia el timer a 0,01 s y predice la nueva frecuencia antes de ejecutar.

Listo cuando: python3 /kit/no_politica.py 5 en el contenedor → /lowcmd publicado a ≈ 50 Hz (alvo 50 Hz) y saída: 0.

06Prueba de la clase

La explicación aparece después de tu respuesta.

Pregunta 1 de 4

¿Por qué el curso usa ROS 2 Humble si el actual es Lyrical?competencia

07En tres frases

ContenedorHumble solo en el contenedor del robot; el host se queda con el ROS 2 actual.
Red--network host, CycloneDDS, dominio e interfaz del robot.
NodoSe suscribe a /lowstate, publica /lowcmd; la política es el núcleo.

Para ir más allá

Clase anteriorSiguiente clase