En esta guía vas a convertir una ESP32 en un nodo de ROS 2 de verdad usando micro-ROS, conectarla al PC con solo un cable USB y controlar un motor DC a través de un L298N publicando en un topic de ROS 2. El firmware se reconecta solo si el agente se cae, y para el motor si dejan de llegar comandos.
Motivación
La mayoría de los robots tienen un ordenador con ROS 2 y un microcontrolador que hace el trabajo de bajo nivel: PWM, encoders, sensores. La forma clásica de unirlos es un protocolo serie propio y un nodo puente que tienes que escribir y mantener tú. micro-ROS elimina esa capa: el microcontrolador es un nodo de ROS 2, con sus propios topics, y aparece en ros2 node list como cualquier otro.
Cómo funciona micro-ROS
Un microcontrolador no puede ejecutar una pila DDS completa, así que micro-ROS divide el trabajo en dos:
ESP32 (cliente micro-ROS) ⇄ serie USB ⇄ Agente micro-ROS (PC) ⇄ DDS ⇄ nodos ROS 2
- El cliente corre en la ESP32 y habla un protocolo ligero (XRCE-DDS) sobre un transporte: serie, UDP, etc.
- El agente corre en el PC, recibe ese tráfico y crea las entidades DDS reales en nombre de la ESP32.
Para el resto de ROS 2, la ESP32 es un nodo normal.
Materiales necesarios
- ESP32 DevKit (cualquier placa ESP32-WROOM con chip USB-serie).
- Módulo controlador de motores L298N.
- Motor DC (un motorreductor pequeño de 6–12 V es perfecto).
- Fuente externa para el motor (7–12 V; baterías o fuente de laboratorio).
- Cable USB de datos (no uno solo de carga).
- Cables Dupont.
- Un PC con Ubuntu 22.04 y ROS 2 Humble instalado.
- Arduino IDE 2.x.
1. Conexiones
| L298N | Conectar a |
|---|---|
| IN1 | ESP32 GPIO2 |
| IN2 | ESP32 GPIO4 |
| ENA | Dejar el jumper puesto (por defecto) |
| OUT1 / OUT2 | Bornes del motor |
| 12V | + de la fuente del motor |
| GND | – de la fuente y GND de la ESP32 |
La tierra debe ser común. Sin GND compartido entre la ESP32 y el L298N, las señales lógicas no tienen referencia y el motor no se moverá (o lo hará de forma errática).
Dos formas de controlar la velocidad, seleccionadas con USE_ENA en el sketch:
USE_ENA 0(jumper en ENA): ENA siempre habilitado y el PWM va directo a IN1 o IN2. Solo hacen falta dos cables.USE_ENA 1(sin jumper): IN1/IN2 marcan la dirección y el PWM va a ENA, conectado al GPIO25.
El GPIO2 es un strapping pin y además maneja el LED de la placa en la mayoría de DevKits. Aquí funciona bien, pero si falla la subida del firmware, mira Solución de problemas.
2. Configuración de Arduino IDE
- Instala el paquete de placas ESP32: Boards Manager → busca
esp32(de Espressif) → instalar. El sketch compila con core 2.x y 3.x. - Descarga la librería micro_ros_arduino para Humble. No está en el Library Manager: ve a la página de releases y descarga el ZIP de la última versión
-humble. - En Arduino IDE: Sketch → Include Library → Add .ZIP Library… y selecciona el ZIP.
- Elige tu placa (ESP32 Dev Module) y el puerto.
3. El firmware, pieza a pieza
Vamos a desmontar el sketch bloque a bloque. El archivo completo, listo para copiar, está al final de esta sección.
3.1 Cabeceras
#include <micro_ros_arduino.h>
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <rmw_microros/rmw_microros.h>
#include <std_msgs/msg/float32.h>
micro_ros_arduino.h: el pegamento del transporte para Arduino; aportaset_microros_transports().rcl/rcl.h: el núcleo de la librería cliente de ROS 2 (la misma capa en C sobre la que se apoyanrclcppyrclpy).rclc/rclc.hyrclc/executor.h: rclc, una capa fina en C pensada para microcontroladores. Te ahorra casi todo el código repetitivo dercly te da un executor que despacha los callbacks.rmw_microros/rmw_microros.h: extras propios de micro-ROS, aquírmw_uros_ping_agent()para saber si el agente está vivo.std_msgs/msg/float32.h: el tipo en C del mensaje que vamos a recibir,std_msgs__msg__Float32. Cada tipo de mensaje de ROS tiene una cabecera como esta.
3.2 Configuración
#define PIN_IN1 2 // D2 -> IN1
#define PIN_IN2 4 // D4 -> IN2
#define USE_ENA 0
#define PIN_ENA 25
#define PWM_FREQ 1000 // el L298N es lento; 1 kHz va bien
#define PWM_RES 8 // 0-255
#define CH_A 0 // canales (solo core 2.x)
#define CH_B 1
#define TIMEOUT_MS 500 // sin comandos durante este tiempo -> motor parado
Todo lo que puedas querer cambiar está aquí:
- Pines: IN1/IN2 en GPIO2/GPIO4, y ENA en GPIO25 si lo usas.
USE_ENA: elige uno de los dos modos de control de la sección de Conexiones. Al ser un#define, el modo que no usas ni siquiera se compila.PWM_FREQ: el L298N usa transistores bipolares que conmutan despacio. A decenas de kHz pasan demasiado tiempo a medio conducir y se calientan; 1 kHz es un valor seguro.PWM_RES: 8 bits, así que el duty va de 0 a 255.CH_A/CH_B: canales LEDC, solo los usa el core 2.x (más abajo se explica).TIMEOUT_MS: cuánto tiempo sigue el motor sin recibir comandos nuevos.
3.3 Estado global
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
rcl_subscription_t sub;
rclc_executor_t executor;
std_msgs__msg__Float32 msg;
unsigned long last_cmd = 0;
enum AgentState { WAITING_AGENT, AGENT_AVAILABLE, AGENT_CONNECTED, AGENT_DISCONNECTED };
AgentState state = WAITING_AGENT;
Estos son los objetos de ROS. Son globales porque tienen que sobrevivir a setup() y ser accesibles desde loop():
support: guarda el contexto de micro-ROS (la sesión con el agente).allocator: cómo obtiene memoria micro-ROS. El de por defecto usamalloc/free.node,sub,executor: el nodo, la suscripción y el despachador de callbacks.msg: el buffer donde se escriben los mensajes entrantes. El executor lo rellena antes de llamar al callback, así que tiene que ser global, no una variable local.last_cmd: elmillis()del último comando, para el timeout de seguridad.state: el estado actual de la máquina de estados de conexión.
3.4 Ayudante de temporización
#define EXECUTE_EVERY_N_MS(MS, X) do { \
static int64_t t_init = -1; \
if (t_init == -1) t_init = uxr_millis(); \
if ((int64_t)uxr_millis() - t_init > MS) { X; t_init = uxr_millis(); } \
} while (0)
Un “ejecuta X cada MS milisegundos” que no bloquea, sacado de los ejemplos oficiales de micro-ROS. El truco está en la variable static: cada sitio donde se usa la macro tiene su propio t_init, así que los pings de 500 ms y de 200 ms de loop() llevan temporizadores independientes. El envoltorio do { ... } while (0) hace que la macro se comporte como una sola sentencia tras un if o dentro de un case. uxr_millis() es el reloj en milisegundos de micro-ROS.
3.5 PWM en core 2.x y 3.x
void pwm_attach(int pin, int ch) {
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcAttach(pin, PWM_FREQ, PWM_RES);
#else
ledcSetup(ch, PWM_FREQ, PWM_RES);
ledcAttachPin(pin, ch);
#endif
}
void pwm_write(int pin, int ch, int duty) {
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcWrite(pin, duty);
#else
ledcWrite(ch, duty);
#endif
}
La ESP32 genera PWM con su periférico LEDC, y el core 3.x cambió su API:
| Core 2.x | Core 3.x | |
|---|---|---|
| Configurar | ledcSetup(canal, freq, res) + ledcAttachPin(pin, canal) | ledcAttach(pin, freq, res) |
| Escribir | ledcWrite(canal, duty) | ledcWrite(pin, duty) |
Los dos ayudantes reciben tanto el pin como el canal y usan el que necesite el core instalado. ESP_ARDUINO_VERSION_MAJOR lo define el propio core, así que la elección se hace al compilar.
3.6 Mover el motor
void set_motor(float v) {
v = constrain(v, -1.0f, 1.0f);
int duty = (int)(fabs(v) * 255);
#if USE_ENA
digitalWrite(PIN_IN1, v > 0);
digitalWrite(PIN_IN2, v < 0);
pwm_write(PIN_ENA, CH_A, duty);
#else
// Jumper en ENA: PWM en el pin de un lado, 0 en el otro
if (v > 0) { pwm_write(PIN_IN1, CH_A, duty); pwm_write(PIN_IN2, CH_B, 0); }
else if (v < 0) { pwm_write(PIN_IN1, CH_A, 0); pwm_write(PIN_IN2, CH_B, duty); }
else { pwm_write(PIN_IN1, CH_A, 0); pwm_write(PIN_IN2, CH_B, 0); }
#endif
}
La única función que toca el hardware:
constrainlimita el comando a [-1, 1], así que un mensaje erróneo como{data: 7.0}no puede generar un duty inválido.fabs(v) * 255convierte la magnitud en ciclo de trabajo; el signo es la dirección.- Luego, según el modo:
USE_ENA 1: IN1/IN2 son pines digitales normales.v > 0da IN1=1, IN2=0 (adelante);v < 0lo contrario;v == 0ambos a 0 (parada libre). El PWM va en ENA.USE_ENA 0: ENA siempre está habilitado, así que la velocidad sale de aplicar PWM a una entrada mientras la otra se queda a 0. La entrada que recibe el PWM marca la dirección.
Nunca se ponen las dos entradas en alto a la vez, lo que en el L298N sería un frenado activo.
3.7 El callback de la suscripción
void cmd_callback(const void *msgin) {
const std_msgs__msg__Float32 *m = (const std_msgs__msg__Float32 *)msgin;
set_motor(m->data);
last_cmd = millis();
}
Los callbacks de rclc son C puro, así que el mensaje llega como const void * y hay que convertirlo al tipo real. Después aplica la velocidad y actualiza last_cmd, que es lo que evita que salte el timeout de seguridad. Mantén los callbacks cortos como este: se ejecutan dentro de rclc_executor_spin_some(), en mitad de loop().
3.8 Crear las entidades de ROS
bool create_entities() {
allocator = rcl_get_default_allocator();
if (rclc_support_init(&support, 0, NULL, &allocator) != RCL_RET_OK) return false;
if (rclc_node_init_default(&node, "esp32_motor", "", &support) != RCL_RET_OK) return false;
if (rclc_subscription_init_default(
&sub, &node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float32),
"motor_cmd") != RCL_RET_OK) return false;
executor = rclc_executor_get_zero_initialized_executor();
if (rclc_executor_init(&executor, &support.context, 1, &allocator) != RCL_RET_OK) return false;
if (rclc_executor_add_subscription(&executor, &sub, &msg, &cmd_callback, ON_NEW_DATA) != RCL_RET_OK) return false;
return true;
}
El equivalente en micro-ROS de rclcpp::init() + crear un nodo + create_subscription(), paso a paso:
rclc_support_init: abre la sesión con el agente. El0, NULLsonargc/argv, que aquí no se usan. Es el paso que falla si el agente no está.rclc_node_init_default: crea el nodoesp32_motorcon namespace vacío ("").rclc_subscription_init_default: se suscribe amotor_cmd.ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float32)le dice a micro-ROS cómo deserializar el tipo. La variante_defaultusa QoS fiable (reliable). El nombre del topic no lleva/delante, así que se resuelve relativo al namespace del nodo; con namespace vacío queda/motor_cmd.rclc_executor_init: crea un executor con hueco para 1 handle. Cada suscripción, timer o servicio que añadas después necesita un hueco, así que si añades un timer para publicar, esto pasa a 2. Olvidarlo es el fallo más común en micro-ROS.rclc_executor_add_subscription: une suscripción, buffer del mensaje y callback.ON_NEW_DATAsignifica “llama al callback solo cuando de verdad llegue un mensaje”.
Cada paso devuelve false si falla en vez de colgarse, así la máquina de estados puede reintentarlo más tarde.
3.9 Destruir las entidades de ROS
void destroy_entities() {
rmw_context_t *rmw_context = rcl_context_get_rmw_context(&support.context);
(void)rmw_uros_set_context_entity_destroy_session_timeout(rmw_context, 0);
rcl_subscription_fini(&sub, &node);
rclc_executor_fini(&executor);
rcl_node_fini(&node);
rclc_support_fini(&support);
}
Libera todo en orden inverso a la creación. Las dos primeras líneas son importantes: normalmente, destruir una entidad envía un mensaje al agente y espera su respuesta. Si el agente ya no está, cada fini se quedaría bloqueado hasta agotar su timeout. Poner el timeout de destrucción a 0 hace que la limpieza sea instantánea y puramente local, que es justo lo que quieres tras una desconexión.
3.10 setup()
void setup() {
#if USE_ENA
pinMode(PIN_IN1, OUTPUT);
pinMode(PIN_IN2, OUTPUT);
pwm_attach(PIN_ENA, CH_A);
#else
pwm_attach(PIN_IN1, CH_A);
pwm_attach(PIN_IN2, CH_B);
#endif
set_motor(0);
set_microros_transports(); // serie por USB a 115200
}
Configura los pines según el modo elegido, se asegura de que el motor arranque parado y llama a set_microros_transports(), que deja Serial a 115200 como transporte de micro-ROS.
Fíjate en lo que no está: en setup() no se crea ninguna entidad de ROS. Puede que el agente todavía no esté corriendo, así que eso se deja a la máquina de estados. Por eso tampoco hay ningún Serial.print() en el sketch: el puerto transporta tramas de micro-ROS y cualquier texto suelto las corrompería.
3.11 loop(): la máquina de estados
void loop() {
switch (state) {
case WAITING_AGENT:
EXECUTE_EVERY_N_MS(500,
state = (RMW_RET_OK == rmw_uros_ping_agent(100, 1)) ? AGENT_AVAILABLE : WAITING_AGENT;);
break;
case AGENT_AVAILABLE:
state = create_entities() ? AGENT_CONNECTED : WAITING_AGENT;
if (state == WAITING_AGENT) destroy_entities();
break;
case AGENT_CONNECTED:
EXECUTE_EVERY_N_MS(200,
state = (RMW_RET_OK == rmw_uros_ping_agent(100, 1)) ? AGENT_CONNECTED : AGENT_DISCONNECTED;);
if (state == AGENT_CONNECTED) {
rclc_executor_spin_some(&executor, RCL_MS_TO_NS(10));
}
break;
case AGENT_DISCONNECTED:
set_motor(0);
destroy_entities();
state = WAITING_AGENT;
break;
}
// Seguridad: sin comandos recientes o sin agente -> motor parado
if (state != AGENT_CONNECTED || millis() - last_cmd > TIMEOUT_MS) set_motor(0);
}
Esto es lo que permite al firmware sobrevivir a que el agente vaya y venga:
WAITING_AGENT --ping OK--> AGENT_AVAILABLE --entidades creadas--> AGENT_CONNECTED
^ | |
+---- falla la creación ---+ falla el ping
| v
+------------------------------------------------------- AGENT_DISCONNECTED
WAITING_AGENT: cada 500 ms,rmw_uros_ping_agent(100, 1)envía un ping y espera hasta 100 ms la respuesta.AGENT_AVAILABLE: el agente respondió, así que se crean las entidades. Si falla a medias, se limpia lo que se haya creado y se vuelve a esperar.AGENT_CONNECTED: funcionamiento normal.rclc_executor_spin_someespera hasta 10 ms por datos entrantes y ejecutacmd_callbacksi llegó un mensaje. Cada 200 ms vuelve a hacer ping para detectar un agente caído.AGENT_DISCONNECTED: para el motor, libera todo y empieza de nuevo.
Resultado: da igual si arrancas el agente antes o después de la ESP32, o si lo reinicias a mitad; la placa se reconecta sola.
La última línea es la red de seguridad. Se ejecuta en cada iteración, sea cual sea el estado: si no estamos conectados, o han pasado más de TIMEOUT_MS desde el último comando, el motor se para. Si el nodo que publica los comandos se cae o desconectas el PC, el motor no se queda girando. Por eso la prueba de abajo publica a 10 Hz (-r 10) en vez de una sola vez. millis() - last_cmd usa aritmética sin signo, así que sigue siendo correcto incluso cuando millis() se desborda tras ~49 días.
Sketch completo
Ver el sketch completo
// micro-ROS en ESP32 por SERIE (USB): controla un motor DC con L298N desde ROS 2
// Topic: /motor_cmd (std_msgs/Float32) -> de -1.0 (atrás) a 1.0 (adelante)
// Reconecta solo si el agente se cae o arranca después que la ESP32.
// Requiere: librería micro_ros_arduino (rama humble). Compila con ESP32 Arduino core 2.x y 3.x
//
// Agente en el PC:
// ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200
// Prueba:
// ros2 topic pub -r 10 /motor_cmd std_msgs/msg/Float32 "{data: 0.5}"
//
// IMPORTANTE: no uses Serial.print() en este sketch. El puerto serie lo usa micro-ROS.
#include <micro_ros_arduino.h>
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <rmw_microros/rmw_microros.h>
#include <std_msgs/msg/float32.h>
// ---- Conexión al L298N ----
#define PIN_IN1 2 // D2 -> IN1
#define PIN_IN2 4 // D4 -> IN2
// USE_ENA = 0 -> el puente (jumper) de ENA está puesto: la velocidad se controla con PWM en IN1/IN2
// USE_ENA = 1 -> quitaste el jumper y conectaste ENA a PIN_ENA
#define USE_ENA 0
#define PIN_ENA 25
#define PWM_FREQ 1000 // el L298N es lento; 1 kHz va bien
#define PWM_RES 8 // 0-255
#define CH_A 0 // canales (solo core 2.x)
#define CH_B 1
#define TIMEOUT_MS 500 // sin comandos durante este tiempo -> motor parado
// ---------------------------
rclc_support_t support;
rcl_allocator_t allocator;
rcl_node_t node;
rcl_subscription_t sub;
rclc_executor_t executor;
std_msgs__msg__Float32 msg;
unsigned long last_cmd = 0;
enum AgentState { WAITING_AGENT, AGENT_AVAILABLE, AGENT_CONNECTED, AGENT_DISCONNECTED };
AgentState state = WAITING_AGENT;
#define EXECUTE_EVERY_N_MS(MS, X) do { \
static int64_t t_init = -1; \
if (t_init == -1) t_init = uxr_millis(); \
if ((int64_t)uxr_millis() - t_init > MS) { X; t_init = uxr_millis(); } \
} while (0)
// ---- PWM compatible con core 2.x y 3.x ----
void pwm_attach(int pin, int ch) {
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcAttach(pin, PWM_FREQ, PWM_RES);
#else
ledcSetup(ch, PWM_FREQ, PWM_RES);
ledcAttachPin(pin, ch);
#endif
}
void pwm_write(int pin, int ch, int duty) {
#if ESP_ARDUINO_VERSION_MAJOR >= 3
ledcWrite(pin, duty);
#else
ledcWrite(ch, duty);
#endif
}
void set_motor(float v) {
v = constrain(v, -1.0f, 1.0f);
int duty = (int)(fabs(v) * 255);
#if USE_ENA
digitalWrite(PIN_IN1, v > 0);
digitalWrite(PIN_IN2, v < 0);
pwm_write(PIN_ENA, CH_A, duty);
#else
// Jumper en ENA: PWM en el pin de un lado, 0 en el otro
if (v > 0) { pwm_write(PIN_IN1, CH_A, duty); pwm_write(PIN_IN2, CH_B, 0); }
else if (v < 0) { pwm_write(PIN_IN1, CH_A, 0); pwm_write(PIN_IN2, CH_B, duty); }
else { pwm_write(PIN_IN1, CH_A, 0); pwm_write(PIN_IN2, CH_B, 0); }
#endif
}
void cmd_callback(const void *msgin) {
const std_msgs__msg__Float32 *m = (const std_msgs__msg__Float32 *)msgin;
set_motor(m->data);
last_cmd = millis();
}
bool create_entities() {
allocator = rcl_get_default_allocator();
if (rclc_support_init(&support, 0, NULL, &allocator) != RCL_RET_OK) return false;
if (rclc_node_init_default(&node, "esp32_motor", "", &support) != RCL_RET_OK) return false;
if (rclc_subscription_init_default(
&sub, &node,
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float32),
"motor_cmd") != RCL_RET_OK) return false;
executor = rclc_executor_get_zero_initialized_executor();
if (rclc_executor_init(&executor, &support.context, 1, &allocator) != RCL_RET_OK) return false;
if (rclc_executor_add_subscription(&executor, &sub, &msg, &cmd_callback, ON_NEW_DATA) != RCL_RET_OK) return false;
return true;
}
void destroy_entities() {
rmw_context_t *rmw_context = rcl_context_get_rmw_context(&support.context);
(void)rmw_uros_set_context_entity_destroy_session_timeout(rmw_context, 0);
rcl_subscription_fini(&sub, &node);
rclc_executor_fini(&executor);
rcl_node_fini(&node);
rclc_support_fini(&support);
}
void setup() {
#if USE_ENA
pinMode(PIN_IN1, OUTPUT);
pinMode(PIN_IN2, OUTPUT);
pwm_attach(PIN_ENA, CH_A);
#else
pwm_attach(PIN_IN1, CH_A);
pwm_attach(PIN_IN2, CH_B);
#endif
set_motor(0);
set_microros_transports(); // serie por USB a 115200
}
void loop() {
switch (state) {
case WAITING_AGENT:
EXECUTE_EVERY_N_MS(500,
state = (RMW_RET_OK == rmw_uros_ping_agent(100, 1)) ? AGENT_AVAILABLE : WAITING_AGENT;);
break;
case AGENT_AVAILABLE:
state = create_entities() ? AGENT_CONNECTED : WAITING_AGENT;
if (state == WAITING_AGENT) destroy_entities();
break;
case AGENT_CONNECTED:
EXECUTE_EVERY_N_MS(200,
state = (RMW_RET_OK == rmw_uros_ping_agent(100, 1)) ? AGENT_CONNECTED : AGENT_DISCONNECTED;);
if (state == AGENT_CONNECTED) {
rclc_executor_spin_some(&executor, RCL_MS_TO_NS(10));
}
break;
case AGENT_DISCONNECTED:
set_motor(0);
destroy_entities();
state = WAITING_AGENT;
break;
}
// Seguridad: sin comandos recientes o sin agente -> motor parado
if (state != AGENT_CONNECTED || millis() - last_cmd > TIMEOUT_MS) set_motor(0);
}
Súbelo. Cierra el Monitor Serie si está abierto: a partir de ahora el puerto es de micro-ROS.
4. Ejecutar el agente micro-ROS
Opción A: compilar el agente (nativo)
source /opt/ros/humble/setup.bash
mkdir -p ~/microros_ws/src && cd ~/microros_ws
git clone -b humble https://github.com/micro-ROS/micro_ros_setup.git src/micro_ros_setup
rosdep update && rosdep install --from-paths src --ignore-src -y
colcon build
source install/local_setup.bash
ros2 run micro_ros_setup create_agent_ws.sh
ros2 run micro_ros_setup build_agent.sh
source install/local_setup.bash
Y arráncalo:
ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0 -b 115200
Opción B: Docker
Si no quieres compilar nada:
docker run -it --rm -v /dev:/dev --privileged --net=host microros/micro-ros-agent:humble serial --dev /dev/ttyUSB0 -b 115200
Cuando la ESP32 se conecte verás en el agente session established y la creación del participante, el topic y el suscriptor. Si no aparece nada en unos segundos, pulsa el botón EN de la placa.
5. Probar desde ROS 2
En otra terminal (con source /opt/ros/humble/setup.bash):
ros2 node list
Debe aparecer /esp32_motor. Ahora mándale comandos:
ros2 topic pub -r 10 /motor_cmd std_msgs/msg/Float32 "{data: 0.5}"
El motor gira hacia adelante a media velocidad. Prueba en reversa:
ros2 topic pub -r 10 /motor_cmd std_msgs/msg/Float32 "{data: -1.0}"
Cosas a comprobar:
- Timeout: para el
ros2 topic pubcon Ctrl+C. El motor se detiene en menos de 500 ms. - Reconexión: para el agente con Ctrl+C y vuelve a arrancarlo. El motor se para, y unos segundos después
/esp32_motorvuelve a aparecer enros2 node listsin tocar la placa.
6. Ajustes
- El motor no arranca con valores bajos. El L298N tiene una caída de ~2 V y todo motor tiene rozamiento estático, así que por debajo de cierto duty solo zumba. Puedes añadir un duty mínimo en
set_motor(p. ej. mapear 0–1 a 80–255) o simplemente evitar valores pequeños. - Pitido audible. Prueba a subir
PWM_FREQ. El L298N pierde eficiencia por encima de unos pocos kHz, así que no te pases. - Parada más lenta o más rápida. Ajusta
TIMEOUT_MSa la frecuencia con la que publicas comandos (al menos 2–3 periodos).
7. Solución de problemas
Error opening /dev/ttyUSB0/ permiso denegado. Añade tu usuario al grupodialouty cierra sesión y vuelve a entrar:
sudo usermod -aG dialout $USER
- El puerto está ocupado. Cierra el Monitor Serie de Arduino IDE (o cualquier otro programa que use el puerto).
- El puerto no es
/dev/ttyUSB0. Placas con USB nativo o chips CH9102/CDC pueden aparecer como/dev/ttyACM0. Compruébalo conls /dev/tty{USB,ACM}*. - El agente no muestra
session established. Pulsa EN en la ESP32. Revisa que el baudrate sea 115200 y que no quede ningúnSerial.print()en el sketch. - El nodo aparece pero el motor no se mueve. Revisa la GND común, la alimentación del motor y que
USE_ENAcoincida con el jumper de ENA. - Falla la subida (“Failed to connect” / “Wrong boot mode”). El GPIO2 es un strapping pin y debe estar en bajo al arrancar para entrar en modo descarga. Desconecta el cable de IN1 mientras flasheas, o mueve IN1 a otro pin (p. ej. GPIO26) y cambia
PIN_IN1. - Errores de compilación con core 3.x. Si la librería precompilada de micro-ROS da errores de enlazado con tu versión del core, instala el core ESP32 2.0.17 desde el Boards Manager.
- El nodo no aparece en
ros2 node listpero el agente está conectado. Asegúrate de que el agente y tu terminal usan el mismoROS_DOMAIN_ID(micro-ROS usa el dominio 0 por defecto).
Siguientes pasos
- Controlar el motor con
teleop_twist_keyboard, usando un pequeño nodo que conviertageometry_msgs/TwistenFloat32. - Añadir un encoder y publicar la velocidad desde la ESP32 como realimentación.
- Pasar al transporte WiFi (
set_microros_wifi_transports) y olvidarte del cable.