S4: Interfaces de Bajo Nivel — Control Directo, Mensajes y Librerías¶
Objetivos¶
- Leer estados de motores e IMU desde
LowStatea 500 Hz - Comprender la estructura de
LowCmdy su campomotor_cmd - Implementar un monitor de articulaciones con alarmas de rango
- Entender cuándo el control de bajo nivel es necesario vs peligroso
¿Qué es el control de bajo nivel?¶
El SDK2 tiene dos niveles de abstracción para controlar el B2:
Alto nivel ── LocoClient / SportClient ── "Muévete 0.5 m/s adelante"
│
Bajo nivel ── LowCmd / LowState ── "Motor 0: τ=2.5 Nm, kp=40, kd=1"
El control de bajo nivel publica LowCmd directamente al controlador de cada motor (12 en el B2). Permite:
- Implementar controladores de posición / impedancia personalizados
- Acceder a datos de sensores a máxima frecuencia (500 Hz)
- Desarrollar algoritmos de contacto y estimación de estado
- Leer corriente, temperatura y torque de cada motor
Úsalo con precaución
Publicar en rt/lowcmd incorrectamente puede causar caídas del robot o daño mecánico. En este módulo nos limitamos a leer LowState y solo publicamos LowCmd en el ejercicio avanzado con el robot en reposo.
Estructura de LowState¶
# Campos relevantes de LowState (simplificado)
class LowState:
motor_state: list[MotorState] # 12 motores
imu_state: IMUState
battery_capacity_pct: int # Batería 0-100
class MotorState:
q: float # Posición (rad)
dq: float # Velocidad (rad/s)
ddq: float # Aceleración (rad/s²)
tau_est: float # Torque estimado (Nm)
temperature: float # Temperatura (°C)
class IMUState:
quaternion: list[float] # [w, x, y, z]
gyroscope: list[float] # [x, y, z] (rad/s)
accelerometer: list[float] # [x, y, z] (m/s²)
rpy: list[float] # [roll, pitch, yaw] (rad)
Mapa de motores del B2¶
| ID | Pierna | Articulación | Rango (rad) |
|---|---|---|---|
| 0 | FL | Hip Roll | [-1.0, 1.0] |
| 1 | FL | Hip Pitch | [-1.5, 3.4] |
| 2 | FL | Knee | [-2.7, -0.8] |
| 3 | FR | Hip Roll | [-1.0, 1.0] |
| 4 | FR | Hip Pitch | [-1.5, 3.4] |
| 5 | FR | Knee | [-2.7, -0.8] |
| 6 | RL | Hip Roll | [-1.0, 1.0] |
| 7 | RL | Hip Pitch | [-3.4, 1.5] |
| 8 | RL | Knee | [0.8, 2.7] |
| 9 | RR | Hip Roll | [-1.0, 1.0] |
| 10 | RR | Hip Pitch | [-3.4, 1.5] |
| 11 | RR | Knee | [0.8, 2.7] |
FL=Frontal-Izquierda, FR=Frontal-Derecha, RL=Trasera-Izquierda, RR=Trasera-Derecha
Código: monitor de estado en tiempo real¶
#!/usr/bin/env python3
"""
Monitor de articulaciones y IMU via SDK2 LowState.
Muestra posición, velocidad y temperatura de cada motor.
"""
import time
import rclpy
from sdk2_bridge_sim import SDK2Bridge # (1)!
# Robot real:
# from unitree_sdk2py.core.channel import ChannelFactory, ChannelSubscriber
# from unitree_sdk2py.idl.unitree_go.msg.dds_ import LowState_
# ChannelFactory.Instance().Init(0)
TEMP_WARN = 60.0 # °C — alerta de temperatura # (2)!
def print_motor_table(state):
import os; os.system("clear")
print(f"{'Motor':<6} {'Pierna':<5} {'Articulación':<14} {'q (rad)':>8} {'dq (r/s)':>9} {'τ (Nm)':>7} {'Temp °C':>8}")
print("-" * 60)
labels = [
("FL", "Hip Roll"), ("FL", "Hip Pitch"), ("FL", "Knee"),
("FR", "Hip Roll"), ("FR", "Hip Pitch"), ("FR", "Knee"),
("RL", "Hip Roll"), ("RL", "Hip Pitch"), ("RL", "Knee"),
("RR", "Hip Roll"), ("RR", "Hip Pitch"), ("RR", "Knee"),
]
for i, (leg, joint) in enumerate(labels):
m = state.motor_state[i]
flag = " ⚠" if m.temperature > TEMP_WARN else ""
print(f"{i:<6} {leg:<5} {joint:<14} {m.q:>8.3f} {m.dq:>9.3f} {m.tau_est:>7.2f} {m.temperature:>7.1f}{flag}")
imu = state.imu_state
print(f"\nIMU — Roll:{imu.rpy[0]:>6.3f} Pitch:{imu.rpy[1]:>6.3f} Yaw:{imu.rpy[2]:>6.3f} rad")
print(f" Acc: {imu.accelerometer}")
def main():
rclpy.init()
bridge = SDK2Bridge()
try:
while True:
state = bridge.get_low_state() # (3)!
if state:
print_motor_table(state)
time.sleep(0.1) # 10 Hz para la visualización
except KeyboardInterrupt:
pass
finally:
bridge.destroy()
rclpy.shutdown()
if __name__ == '__main__':
main()
- En robot real: usar
ChannelSubscriber("rt/lowstate", LowState_) - El B2 permite operación hasta ~80 °C; por encima de 90 °C el controlador reducirá torque
get_low_state()en el bridge devuelve el últimoLowStaterecibido
LowCmd: estructura (referencia, no ejecutar en sim)¶
# Solo para referencia — no enviar en simulación
class MotorCmd:
mode: int # 0=idle, 1=torque, 10=posición PD
q: float # Posición objetivo (rad)
dq: float # Velocidad objetivo (rad/s)
tau: float # Torque feed-forward (Nm)
kp: float # Ganancia proporcional
kd: float # Ganancia derivativa
Ejemplo de modo PD (solo robot real en reposo)¶
# Mantener posición actual (ganancia baja = modo suave)
cmd = LowCmd_()
for i in range(12):
cmd.motor_cmd[i].mode = 10 # Modo PD
cmd.motor_cmd[i].q = current_q[i] # Posición actual
cmd.motor_cmd[i].kp = 20.0 # Ganancia proporcional
cmd.motor_cmd[i].kd = 0.5 # Ganancia derivativa
cmd.motor_cmd[i].tau = 0.0 # Sin feedforward
cmd.crc = CRC().Crc(cmd)
publisher.Write(cmd)
Ejercicios¶
Ejercicio 1 — Monitor básico¶
Ejecuta s4_monitor_lowstate.py con sdk2_bridge_sim. Desplaza el robot (mueve, gira) y observa cómo cambian las lecturas de q y dq de los motores. ¿Qué motores cambian más durante una traslación frontal?
Ejercicio 2 — Extractor de IMU¶
Extrae solo los datos del IMU (roll, pitch, yaw + acelerómetro) y publícalos como geometry_msgs/TwistStamped en un tópico ROS 2 /imu_viz. Verifica con ros2 topic echo.
Ejercicio 3 — Alarma de temperatura¶
Extiende el monitor para que emita una alerta de ROS 2 (get_logger().warn) cuando cualquier motor supere TEMP_WARN. Encapsula la lógica en una función check_thermal(state) -> list[int] que devuelva los IDs de motores en alerta.
Ejercicio 4 — Análisis de simetría¶
Compara los valores de q de las articulaciones FL vs FR (índices 0,1,2 vs 3,4,5) durante el caminar. ¿Son simétricas? ¿Qué implica una asimetría significativa?
Ejercicio 5 — LowState a CSV (robot real)¶
Durante una sesión de robot real, registra LowState a 100 Hz durante 10 segundos mientras el B2 camina. Guarda en CSV. Calcula la amplitud de oscilación de cada articulación de rodilla.
Pasa al robot real¶
Habilitación de LowState en hardware
En robot real, rt/lowstate se publica automáticamente a 500 Hz sin necesidad de configuración adicional. El primer mensaje llega en ~10 ms tras ChannelFactory.Instance().Init().
# Leer LowState en robot real
from unitree_sdk2py.core.channel import ChannelFactory, ChannelSubscriber
from unitree_sdk2py.idl.unitree_go.msg.dds_ import LowState_
ChannelFactory.Instance().Init(0) # 0 = dominio DDS local
latest = None
def handler(msg): global latest; latest = msg
sub = ChannelSubscriber("rt/lowstate", LowState_)
sub.Init(handler, 10)