Contenido detallado
🧬 Qué es micro-ROS
El ESP32 tiene poca memoria y no ejecuta Linux, así que no puede ejecutar ROS 2 completo. El micro-ROS es una versión ligera, diseñada para microcontroladores: el ESP32 crea nodos, publishers y subscribers con la biblioteca rclc (la API en C), y un programa en la PC o en la Pi, el micro-ROS Agent, hace de puente con el resto del grafo. Para los demás nodos, el ESP32 aparece como un nodo ROS 2 más, con tópicos que puedes leer con ros2 topic echo.
🌱 ¿Eres nuevo aquí?
El ESP32 no se comunica directamente mediante DDS: usa XRCE-DDS (DDS para recursos extremadamente limitados), un protocolo ligero en el que el ESP32 es el cliente y el agente es el servidor que traduce todo a DDS. Por eso el agente debe estar en ejecución antes: sin él, el ESP32 no tiene con quién comunicarse. El executor es el bucle del firmware que llama a tus callbacks cuando llega un mensaje o se cumple el tiempo del timer.
Cómo leer: El camino tiene dos tramos. A la izquierda, el ESP32 y el agente se comunican mediante el protocolo ligero (cable o Wi-Fi). A la derecha, el agente entrega todo al DDS y los nodos comunes ni siquiera saben que existe un microcontrolador. Si el agente (la caja brillante) falla, el ESP32 queda aislado.
✓ Deja en el ESP32
- ✓PWM de los motores y lectura de los encoders.
- ✓La parada de seguridad cuando desaparece el comando.
- ✓Lectura rápida de IMU y sensores simples.
✗ No pongas en el ESP32
- ✗SLAM, mapa o planificación de rutas.
- ✗Mensajes grandes (imagen, nube de puntos).
- ✗Decenas de tópicos: cada uno consume memoria.
Por qué aprender
Es la misma división de la Trilha 3 (el Pi piensa, el ESP32 actúa), pero ahora sin inventar un protocolo: el ESP32 habla ROS. Lo que funciona con el robot de Gazebo (teleop, SLAM, Nav2) también funciona con el robot real; solo cambia quién publica y se suscribe a los tópicos.
Conceptos clave
🛰️ Instalar el micro-ROS Agent
La forma más sencilla de ejecutar el agente es con la imagen oficial de Docker microros/micro-ros-agent:<distro>: no hay nada que compilar. La distro del agente, la de la biblioteca en el ESP32 y la de tu ROS 2 deben ser la misma. El agente escucha por un transporte: serial (el cable USB del ESP32) o por UDP (Wi-Fi, en el puerto que elijas; por convención, 8888). Empieza con el cable: es más fácil de diagnosticar.
# descubre el puerto del ESP32 (DevKit: ttyUSB0; S3 con USB nativo: ttyACM0)
ls /dev/ttyUSB* /dev/ttyACM*
# agente por el puerto serial (cable USB)
docker run -it --rm -v /dev:/dev --privileged --net=host \
microros/micro-ros-agent:<distro> serial --dev /dev/ttyUSB0 -b 115200
# O agente por Wi-Fi (UDP), para el ESP32 sin cable
docker run -it --rm --net=host microros/micro-ros-agent:<distro> udp4 --port 8888
Cómo verificar
- ✓El agente imprime "TermiosAgentLinux.cpp | init | running..." (serial) o "UDPv4AgentLinux.cpp | init | running..." (UDP) y queda esperando.
- ✓Cuando el ESP32 se conecta, aparecen líneas "create_client", "create_topic", "create_publisher".
- ✗¿Aparece "Unable to find image"? La etiqueta de tu distro aún no se publicó: compruébalo en Docker Hub.
Cómo leer: la columna ámbar es el cable; la cian, el Wi-Fi. En el robot de la Trilha 3, lo mejor de ambos mundos es el cable USB entre el ESP32 y la Raspberry Pi, con el agente ejecutándose en el Pi: es estable y el robot sigue libre. El Wi-Fi tiene sentido cuando no hay un Pi a bordo.
Por qué aprender
Casi siempre, si «el ESP32 no aparece en ROS», se debe a que el agente está apagado, el puerto es incorrecto o la distro es distinta. Saber iniciar el agente y leer sus mensajes resuelve la mayoría de los problemas antes de abrir el firmware.
Conceptos clave
📤 El ESP32 publica datos (encoders, IMU)
El firmware de abajo es el del robot completo: este tópico explica la parte que publica, el siguiente explica la que se suscribe. Cada 50 ms, un temporizador copia el conteo de los dos encoders en un mensaje std_msgs/msg/Int32MultiArray y lo publica en /ticks. El cálculo de la posición se hace en la PC (tópico 5): el ESP32 solo mide, rápido y sin perder pulsos.
Paso a paso de la biblioteca
Descarga la versión de tu distro
En la página de versiones del repositorio micro-ROS/micro_ros_arduino en GitHub, descarga el .zip marcado con tu distro (hay versiones para Jazzy; para Lyrical, comprueba si ya está disponible).
Instálala en Arduino IDE 2.x
Sketch → Incluir biblioteca → Añadir biblioteca .ZIP. El README de la versión indica qué versiones del paquete esp32 admite; si no compila con el paquete 3.x instalado, sigue sus indicaciones o usa el componente ESP-IDF micro_ros_espidf_component (Jazzy = 24.x, Lyrical = 26.x).
Conecta y carga el programa
Conecta el TB6612FNG como en la Trilha 1 (los pines están al principio del código), los encoders a GPIO 18 y 19 y conecta las GND. Carga el programa con el agente APAGADO (ocupa el puerto); después, inicia el agente.
Objetivo: ESP32 como nodo /esp32_base, publicando /ticks y obedeciendo /cmd_vel, con parada automática. PWM con la API 3.x (ledcAttach e ledcWrite reciben el pin).
// Base del robot con micro-ROS (biblioteca micro_ros_arduino de TU distro, paquete esp32 3.x)
// Publica /ticks (encoders) cada 50 ms y se suscribe a /cmd_vel (geometry_msgs/Twist).
#include <micro_ros_arduino.h>
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <std_msgs/msg/int32_multi_array.h>
#include <geometry_msgs/msg/twist.h>
const int PINO_LED = 2;
const int AIN1 = 26, AIN2 = 27, PWMA = 25; // motor izquierdo (TB6612FNG)
const int BIN1 = 16, BIN2 = 17, PWMB = 4; // motor derecho
const int STBY = 23;
const int ENC_ESQ = 18, ENC_DIR = 19; // canal A de cada encoder
const float SEPARACAO = 0.15; // distancia entre ruedas, en m: MÍDELA en tu robot
const float V_MAX = 0.40; // m/s con PWM 255: mídela con una cinta métrica
const unsigned long TIMEOUT_MS = 500; // sin comando durante 0,5 s = se detiene
volatile int32_t ticksEsq = 0, ticksDir = 0;
volatile int sentidoEsq = 1, sentidoDir = 1;
unsigned long ultimoCmd = 0;
rcl_publisher_t pubTicks;
rcl_subscription_t subCmd;
std_msgs__msg__Int32MultiArray msgTicks;
int32_t bufTicks[2];
geometry_msgs__msg__Twist msgCmd;
rclc_executor_t executor;
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
rcl_timer_t timer;
#define RCCHECK(fn) { rcl_ret_t rc = fn; if (rc != RCL_RET_OK) erro(); }
#define RCSOFT(fn) { rcl_ret_t rc = fn; (void)rc; }
void erro() { // LED parpadeando rápido = error al crear algo
ledcWrite(PWMA, 0); ledcWrite(PWMB, 0);
while (true) { digitalWrite(PINO_LED, !digitalRead(PINO_LED)); delay(100); }
}
void IRAM_ATTR isrEsq() { ticksEsq += sentidoEsq; }
void IRAM_ATTR isrDir() { ticksDir += sentidoDir; }
// v en m/s (negativo = marcha atrás) se convierte en dirección + PWM de 0 a 255
void motor(int in1, int in2, int pwm, float v, volatile int &sentido) {
int duty = constrain((int)(fabs(v) / V_MAX * 255.0), 0, 255);
digitalWrite(in1, v >= 0 ? HIGH : LOW);
digitalWrite(in2, v >= 0 ? LOW : HIGH);
ledcWrite(pwm, duty);
sentido = (v >= 0) ? 1 : -1; // encoder de un canal: la dirección viene del comando
}
// Tópico 4: llega un Twist en /cmd_vel
void aoReceberCmd(const void *msgin) {
const geometry_msgs__msg__Twist *cmd = (const geometry_msgs__msg__Twist *)msgin;
float v = cmd->linear.x, w = cmd->angular.z;
motor(AIN1, AIN2, PWMA, v - w * SEPARACAO / 2.0, sentidoEsq);
motor(BIN1, BIN2, PWMB, v + w * SEPARACAO / 2.0, sentidoDir);
ultimoCmd = millis();
}
// Tópico 3: cada 50 ms publica los ticks y comprueba el watchdog
void aoTimer(rcl_timer_t *t, int64_t ultimo) {
(void)ultimo; if (t == NULL) return;
bufTicks[0] = ticksEsq; bufTicks[1] = ticksDir;
RCSOFT(rcl_publish(&pubTicks, &msgTicks, NULL));
if (millis() - ultimoCmd > TIMEOUT_MS) { ledcWrite(PWMA, 0); ledcWrite(PWMB, 0); }
}
void setup() {
pinMode(PINO_LED, OUTPUT);
set_microros_transports(); // serial USB a 115200 (¡no uses Serial.print!)
int saidas[] = {AIN1, AIN2, BIN1, BIN2, STBY};
for (int p : saidas) pinMode(p, OUTPUT);
digitalWrite(STBY, HIGH); // saca el controlador del modo de espera
ledcAttach(PWMA, 20000, 8); // 20 kHz, 8 bits (API 3.x)
ledcAttach(PWMB, 20000, 8);
pinMode(ENC_ESQ, INPUT_PULLUP); pinMode(ENC_DIR, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(ENC_ESQ), isrEsq, RISING);
attachInterrupt(digitalPinToInterrupt(ENC_DIR), isrDir, RISING);
delay(2000); // tiempo para que el agente atienda
allocator = rcl_get_default_allocator();
RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));
RCCHECK(rclc_node_init_default(&node, "esp32_base", "", &support));
RCCHECK(rclc_publisher_init_default(&pubTicks, &node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32MultiArray), "ticks"));
RCCHECK(rclc_subscription_init_default(&subCmd, &node,
ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist), "cmd_vel"));
// Jazzy y Lyrical usan la versión "2" del temporizador (distros anteriores: rclc_timer_init_default)
RCCHECK(rclc_timer_init_default2(&timer, &support, RCL_MS_TO_NS(50), aoTimer, true));
RCCHECK(rclc_executor_init(&executor, &support.context, 2, &allocator));
RCCHECK(rclc_executor_add_subscription(&executor, &subCmd, &msgCmd, &aoReceberCmd, ON_NEW_DATA));
RCCHECK(rclc_executor_add_timer(&executor, &timer));
msgTicks.data.data = bufTicks; // el mensaje apunta al vector fijo de 2
msgTicks.data.size = 2;
msgTicks.data.capacity = 2;
digitalWrite(PINO_LED, HIGH); // LED encendido fijo = conectado
}
void loop() {
RCSOFT(rclc_executor_spin_some(&executor, RCL_MS_TO_NS(10)));
}
Cómo verificar
- ✓El LED de GPIO 2 queda encendido y el agente muestra "create_publisher" y "create_subscriber".
- ✓ros2 node list muestra /esp32_base; ros2 topic hz /ticks muestra cerca de 20 Hz.
- ✓ros2 topic echo /ticks: gira una rueda con la mano y el número correspondiente en data cambia.
- ✗LED parpadea rápido: el ESP32 no encontró el agente. Inicia el agente y presiona EN (reset) en el ESP32.
💡 ¿Y la IMU?
Para publicar la MPU-6050 de la Ruta 2, usa el tipo sensor_msgs/msg/Imu (aceleración lineal en m/s², velocidad angular en rad/s) en un segundo publisher dentro del mismo timer, con header.frame_id apuntando a un link imu_link que agregas al URDF. Comienza solo con los encoders: son suficientes para mapear.
Por qué aprender
El patrón «el timer copia el dato, el executor publica» sirve para cualquier sensor: distancia, batería, botón de emergencia. Y contar pulsos en el ESP32 mediante interrupciones garantiza que no se pierda ningún giro de la rueda mientras la Pi está ocupada con el mapa.
Conceptos clave
📥 ESP32 escuchando /cmd_vel y haciendo girar los motores
O /cmd_vel indica cómo debe moverse el robot completo: v (metros por segundo hacia adelante) y ω (radianes por segundo de giro). El ESP32 necesita convertir esto en la velocidad de cada rueda. Es la cinemática diferencial: para girar a la izquierda, la rueda derecha avanza más que la izquierda. En la función aoReceberCmd del firmware, esto se resuelve con dos líneas.
Cómo leer: vista desde arriba, con el frente hacia la derecha; la rueda de arriba es la izquierda del robot. Las flechas cian tienen un tamaño proporcional a la velocidad de cada rueda: la de abajo (rueda derecha) es más grande, así que gira a la izquierda, hacia arriba en la figura (flecha ámbar). L es la distancia entre las ruedas, la constante SEPARACAO del firmware.
🌱 ¿Eres nuevo aquí?
Watchdog de comandos es la regla «si no llega una orden nueva en 0,5 s, detente». Teleop y Nav2 publican varias veces por segundo; si se cae el Wi-Fi o la PC se bloquea, los mensajes dejan de llegar y, sin watchdog, el robot seguiría con la última velocidad hasta chocar. En el firmware, está al final de aoTimer.
Objetivo: prueba la suscripción con el robot SUSPENDIDO (ruedas en el aire), primero con un comando fijo y después con el teclado.
# 1) ambas ruedas hacia adelante a 0,15 m/s durante 3 segundos (10 mensajes/s)
timeout 3 ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.15}}"
# 2) girar sobre su eje: ruedas en sentidos opuestos
timeout 3 ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist "{angular: {z: 1.0}}"
# 3) conducir con el teclado, igual que en Gazebo en 4.2
ros2 run teleop_twist_keyboard teleop_twist_keyboard
Cómo verificar
- ✓Comando 1: ambas ruedas giran hacia adelante y se DETIENEN solas aproximadamente medio segundo después de que timeout finaliza la publicación.
- ✓Comando 2: la rueda derecha avanza y la izquierda retrocede.
- ✗¿Una rueda gira al revés? Invierte los cables de ese motor en el driver (o los pines IN1/IN2 en el código).
✓ Seguridad en la primera prueba
- ✓Robot sobre un soporte, con las ruedas sin tocar la mesa.
- ✓Velocidades bajas (0,1 a 0,2 m/s) hasta que tengas confianza.
- ✓Mantén la mano cerca del interruptor de la batería de los motores.
✗ Nunca
- ✗Quitar el watchdog «porque estorba».
- ✗Probarlo en el suelo, cerca de las escaleras, la primera vez.
- ✗Alimentar los motores por el USB del ESP32.
Por qué aprender
Este es el momento en que el robot real empieza a obedecer a ROS 2. De aquí en adelante, cualquier nodo que publique en /cmd_vel, desde el teclado hasta Nav2, controla tu robot sin agregar ninguna línea al firmware.
Conceptos clave
📍 Odometría del robot real en ROS 2
En Gazebo, el plugin DiffDrive proporcionaba /odom y la TF odom → base_link gratis. En el robot real, te toca hacerlo: un nodo Python en la Pi o en la PC se suscribe a /ticks, convierte pulsos en metros y acumula la posición (x, y, θ), exactamente la odometría de la Ruta 2, ahora publicada en el formato que esperan SLAM y Nav2.
Cómo leer: los pulsos entran por la izquierda; en el medio, tres cálculos convierten los pulsos en desplazamiento y el desplazamiento en posición acumulada; a la derecha salen las dos cosas que proporcionaba Gazebo. Por eso, slam_toolbox de 4.4 funciona igual en el simulador y en el robot real.
Objetivo: agregar el nodo odometría al paquete meu_robo de 4.1. Los tres parámetros dependen de tu robot: pulsos por vuelta de la rueda (consulta la hoja de datos del motor o cuenta los pulsos al girarla una vuelta a mano), radio de la rueda y distancia entre ruedas.
# Convierte /ticks (ESP32) en /odom y en la TF odom -> base_link
import math
import rclpy
from rclpy.node import Node
from rclpy.executors import ExternalShutdownException
from std_msgs.msg import Int32MultiArray
from nav_msgs.msg import Odometry
from geometry_msgs.msg import TransformStamped
from tf2_ros import TransformBroadcaster
class Odometria(Node):
def __init__(self):
super().__init__('odometria')
self.tpv = self.declare_parameter('ticks_por_volta', 360).value
self.raio = self.declare_parameter('raio', 0.033).value # m
self.sep = self.declare_parameter('separacao', 0.15).value # m, igual que en el firmware
self.x = self.y = self.th = 0.0
self.anterior, self.t_anterior = None, None
self.pub = self.create_publisher(Odometry, 'odom', 10)
self.tf = TransformBroadcaster(self)
self.create_subscription(Int32MultiArray, 'ticks', self.ao_receber, 10)
def ao_receber(self, msg):
if len(msg.data) < 2:
return
agora, ticks = self.get_clock().now(), list(msg.data[:2])
if self.anterior is None: # primer mensaje: solo guarda
self.anterior, self.t_anterior = ticks, agora
return
m_por_tick = 2 * math.pi * self.raio / self.tpv
d_esq = (ticks[0] - self.anterior[0]) * m_por_tick
d_dir = (ticks[1] - self.anterior[1]) * m_por_tick
dt = (agora - self.t_anterior).nanoseconds / 1e9
self.anterior, self.t_anterior = ticks, agora
d = (d_esq + d_dir) / 2 # cuánto avanzó el centro
dth = (d_dir - d_esq) / self.sep # cuánto giró
self.x += d * math.cos(self.th + dth / 2)
self.y += d * math.sin(self.th + dth / 2)
self.th += dth
qz, qw = math.sin(self.th / 2), math.cos(self.th / 2) # ángulo -> quaternion
stamp = agora.to_msg()
odom = Odometry()
odom.header.stamp, odom.header.frame_id, odom.child_frame_id = stamp, 'odom', 'base_link'
odom.pose.pose.position.x, odom.pose.pose.position.y = self.x, self.y
odom.pose.pose.orientation.z, odom.pose.pose.orientation.w = qz, qw
if dt > 0:
odom.twist.twist.linear.x, odom.twist.twist.angular.z = d / dt, dth / dt
self.pub.publish(odom)
t = TransformStamped()
t.header.stamp, t.header.frame_id, t.child_frame_id = stamp, 'odom', 'base_link'
t.transform.translation.x, t.transform.translation.y = self.x, self.y
t.transform.rotation.z, t.transform.rotation.w = qz, qw
self.tf.sendTransform(t)
def main(args=None):
rclpy.init(args=args)
no = Odometria()
try:
rclpy.spin(no)
except (KeyboardInterrupt, ExternalShutdownException):
pass
finally:
no.destroy_node()
rclpy.try_shutdown()
if __name__ == '__main__':
main()
# 1) en setup.py de meu_robo, agrega en console_scripts:
# 'odometria = meu_robo.odometria:main',
# 2) en package.xml, agrega exec_depend para nav_msgs, geometry_msgs y tf2_ros
cd ~/ros2_ws && colcon build --symlink-install --packages-select meu_robo
source install/setup.bash
ros2 run meu_robo odometria --ros-args -p ticks_por_volta:=<pulsos> -p raio:=0.033 -p separacao:=0.15
Cómo verificar
- ✓Empuja el robot 1 m en línea recta sobre el suelo: ros2 topic echo /odom --field pose.pose.position debe mostrar x cerca de 1,0. Si da 0,9 o 1,1, ajusta radio.
- ✓Gira el robot una vuelta sobre su eje: θ debe volver cerca del valor inicial. Si no, ajusta separacao.
- ✓En RViz, Fixed Frame odom y un display TF: el cuadro base_link se mueve junto con el robot.
Por qué aprender
Una odometría calibrada es la mitad de un buen mapa. El SLAM del 4.4 corrige el error poco a poco, pero parte de donde la odometría indica que está el robot: si se equivoca un 10% por cada metro, el mapa se deforma antes de que haya cualquier corrección.
Conceptos clave
🩺 Problemas comunes (Wi-Fi x serial, sincronizar el tiempo)
Casi todos los problemas de micro-ROS pertenecen a cuatro grupos: conexión (agente, puerto, Wi-Fi), versión (distribución diferente entre los tres extremos), memoria (demasiados tópicos en el ESP32) y tiempo (el reloj del ESP32 es diferente al de la PC). Sigue la tabla de arriba abajo antes de modificar el código.
| Síntoma | Causa probable y solución |
|---|---|
| La carga falla: "port busy" | El agente está ocupando el puerto. Detén el agente, carga el programa y vuelve a iniciarlo. |
| El LED parpadea rápido | El agente está apagado, el puerto es incorrecto o la velocidad en baudios es distinta de 115200. Después de iniciar el agente, presiona EN. |
| Aparece el nodo, pero no los tópicos | La distribución de la biblioteca es diferente a la del agente y ROS. Usa la misma en los tres. |
| Deja de responder después de reiniciar el agente | El firmware no se reconecta por sí solo. Presiona EN o implementa la reconexión (abajo). |
| "create_publisher" falla en el 5.º tópico | Límite de memoria de la biblioteca precompilada. Agrupa los datos en un solo mensaje (como /ticks). |
| Wi-Fi: los comandos llegan a tirones | La señal es débil o la red está saturada. Haz la prueba con el cable; es mejor que el agente esté en el Pi a bordo. |
Cambiar a Wi-Fi: en el firmware, cambia la línea del transporte serial por esta y completa los tres valores entre los signos menor y mayor; en la PC, usa el agente udp4 --port 8888 del tema 2. La IP es la de la computadora donde se ejecuta el agente (hostname -I).
char rede[] = "<sua_rede_wifi>";
char senha[] = "<sua_senha>";
char ip_agente[] = "<ip_do_pc_ou_pi>";
set_microros_wifi_transports(rede, senha, ip_agente, 8888); // en lugar de set_microros_transports()
Cómo verificar: el agente UDP muestra "create_client" con la IP del ESP32; ros2 topic hz /ticks se mantiene cerca de 20 Hz con el robot lejos de la PC.
🌱 ¿Eres nuevo aquí?
Sincronizar el tiempo: el ESP32 se inicia con el reloj en cero, y la PC usa la hora real. Si el ESP32 marca los mensajes con su propio reloj, ROS cree que son de 1970 y descarta la TF. Por eso, en este módulo, quien pone la marca de tiempo es el nodo de odometría en la PC, al recibirlos. Si necesitas poner la marca de tiempo en el ESP32 (por ejemplo, para una IMU rápida), llama a rmw_uros_sync_session(1000) después de conectarte y usa rmw_uros_epoch_nanos() como hora, con #include <rmw_microros/rmw_microros.h>.
💡 Reconexión automática
Para que el robot siga funcionando si el agente se reinicia, se usa una máquina de estados: esperando al agente (prueba con rmw_uros_ping_agent(100, 1) cada medio segundo), crear entidades, conectado (spin; si falla el ping, los motores quedan en cero) y destruir entidades, y volver al inicio. El ejemplo "micro-ros_reconnection_example" de la biblioteca muestra la estructura completa; agrega allí las partes de este firmware.
✓ Orden de diagnóstico
- ✓1. ¿El agente está en ejecución y muestra "running"?
- ✓2. ros2 node list ¿muestra /esp32_base?
- ✓3. ros2 topic hz ¿en los tópicos del ESP32?
✗ Pérdida de tiempo
- ✗Modificar el firmware antes de comprobar el agente.
- ✗Depurar con Serial.print: el puerto serial es para micro-ROS.
- ✗Empezar por Wi-Fi sin haber logrado que funcione con el cable.
Por qué aprender
En el proyecto 4.6, el robot funcionará lejos de la PC y estará encendido durante horas. Saber diagnosticar la conexión, la versión y el tiempo es lo que permite pasar de un robot que funciona en la mesa a uno que funciona en la sala.
Conceptos clave
✅ Resumen del módulo
Para profundizar
- micro.ros.org — descripción general, conceptos y tutoriales oficiales
- micro_ros_arduino — versiones por distribución y ejemplos (incluida la reconexión)
- micro_ros_espidf_component — ruta con ESP-IDF
- Imagen del agente en Docker Hub — comprueba la etiqueta de tu distribución
- docs.ros.org — mensajes nav_msgs, geometry_msgs y tf2
Siguiente módulo
4.4 · LiDAR y mapas (SLAM): el láser se integra al robot y slam_toolbox dibuja el mapa mientras descubre dónde está.