Conteúdo detalhado
🧬 O que é o micro-ROS
O ESP32 tem pouca memória e não roda Linux, então não cabe o ROS 2 inteiro nele. O micro-ROS é uma versão enxuta, feita para microcontroladores: o ESP32 cria nós, publishers e subscribers com a biblioteca rclc (a API em C), e um programa no PC ou no Pi, o micro-ROS Agent, faz a ponte com o resto do grafo. Para os outros nós, o ESP32 aparece como mais um nó ROS 2, com tópicos que você lê com ros2 topic echo.
🌱 Novo aqui?
O ESP32 não fala DDS direto: fala XRCE-DDS (DDS para recursos extremamente limitados), um protocolo leve em que ele é o cliente e o agente é o servidor que traduz tudo para DDS. Por isso o agente precisa estar rodando antes: sem ele, o ESP32 não tem com quem conversar. O executor é o laço do firmware que chama os seus callbacks quando chega mensagem ou o timer vence.
Como ler: o caminho tem dois trechos. À esquerda, ESP32 e agente conversam pelo protocolo leve (cabo ou Wi-Fi). À direita, o agente entrega tudo ao DDS, e os nós comuns nem sabem que existe um microcontrolador. Se o agente (caixa com brilho) cair, o ESP32 fica isolado.
✓ Deixe no ESP32
- ✓PWM dos motores e leitura dos encoders.
- ✓A parada de segurança quando o comando some.
- ✓Leitura rápida de IMU e sensores simples.
✗ Não ponha no ESP32
- ✗SLAM, mapa ou planejamento de rota.
- ✗Mensagens grandes (imagem, nuvem de pontos).
- ✗Dezenas de tópicos: cada um gasta memória.
Por que aprender
É a mesma divisão da Trilha 3 (o Pi pensa, o ESP32 age), só que agora sem inventar protocolo: o ESP32 fala ROS. O que funciona com o robô do Gazebo (teleop, SLAM, Nav2) passa a funcionar com o robô real trocando só quem publica e assina os tópicos.
Conceitos-chave
🛰️ Instalar o micro-ROS Agent
O jeito mais simples de rodar o agente é a imagem Docker oficial microros/micro-ros-agent:<distro>: nada para compilar. A distro do agente, a da biblioteca no ESP32 e a do seu ROS 2 precisam ser a mesma. O agente escuta em um transporte: serial (o cabo USB do ESP32) ou UDP (Wi-Fi, na porta que você escolher, por convenção 8888). Comece pelo cabo: é mais fácil de diagnosticar.
# descubra a porta do ESP32 (DevKit: ttyUSB0; S3 com USB nativo: ttyACM0)
ls /dev/ttyUSB* /dev/ttyACM*
# agente pela porta serial (cabo USB)
docker run -it --rm -v /dev:/dev --privileged --net=host \
microros/micro-ros-agent:<distro> serial --dev /dev/ttyUSB0 -b 115200
# OU agente por Wi-Fi (UDP), para o ESP32 sem cabo
docker run -it --rm --net=host microros/micro-ros-agent:<distro> udp4 --port 8888
Como verificar
- ✓O agente imprime "TermiosAgentLinux.cpp | init | running..." (serial) ou "UDPv4AgentLinux.cpp | init | running..." (UDP) e fica esperando.
- ✓Quando o ESP32 conectar, aparecem linhas "create_client", "create_topic", "create_publisher".
- ✗"Unable to find image"? A tag da sua distro ainda não foi publicada: confira no Docker Hub.
Como ler: a coluna âmbar é o cabo; a ciano, o Wi-Fi. No robô da Trilha 3 o melhor dos dois mundos é o cabo USB entre ESP32 e Raspberry Pi, com o agente rodando no Pi: estável e o robô continua livre. O Wi-Fi faz sentido quando não há Pi a bordo.
Por que aprender
Quase todo "o ESP32 não aparece no ROS" é agente desligado, porta errada ou distro diferente. Saber subir o agente e ler as mensagens dele resolve a maior parte dos problemas antes de abrir o firmware.
Conceitos-chave
📤 ESP32 publicando dados (encoders, IMU)
O firmware abaixo é o do robô inteiro: este tópico explica a parte que publica, o próximo explica a que assina. A cada 50 ms um timer copia a contagem dos dois encoders para uma mensagem std_msgs/msg/Int32MultiArray e publica em /ticks. A conta da posição fica no PC (tópico 5): o ESP32 só mede, rápido e sem errar pulso.
Passo a passo da biblioteca
Baixe a release da sua distro
Na página de releases do repositório micro-ROS/micro_ros_arduino no GitHub, baixe o .zip marcado com a sua distro (há releases para Jazzy; para Lyrical, confira se já saiu).
Instale na Arduino IDE 2.x
Sketch → Incluir Biblioteca → Adicionar biblioteca .ZIP. O README da release diz quais versões do pacote esp32 ela suporta; se não compilar com o pacote 3.x instalado, siga o que ele indicar ou use o componente ESP-IDF micro_ros_espidf_component (Jazzy = 24.x, Lyrical = 26.x).
Ligue e grave
Fiação do TB6612FNG como na Trilha 1 (pinos no topo do código), encoders nos GPIO 18 e 19, GND comum. Grave com o agente DESLIGADO (ele ocupa a porta), depois ligue o agente.
Objetivo: ESP32 como nó /esp32_base, publicando /ticks e obedecendo /cmd_vel, com parada automática. PWM na API 3.x (ledcAttach e ledcWrite recebem o pino).
// Base do robô com micro-ROS (biblioteca micro_ros_arduino da SUA distro, pacote esp32 3.x)
// Publica /ticks (encoders) a cada 50 ms e assina /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 esquerdo (TB6612FNG)
const int BIN1 = 16, BIN2 = 17, PWMB = 4; // motor direito
const int STBY = 23;
const int ENC_ESQ = 18, ENC_DIR = 19; // canal A de cada encoder
const float SEPARACAO = 0.15; // m entre as rodas: MEÇA o seu robô
const float V_MAX = 0.40; // m/s com PWM 255: meça com fita métrica
const unsigned long TIMEOUT_MS = 500; // sem comando por 0,5 s = para
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 piscando rápido = falha ao criar 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 em m/s (negativo = ré) vira sentido + 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 um canal: sentido vem do comando
}
// Tópico 4: chega um Twist em /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: a cada 50 ms publica os ticks e confere o 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 (não use Serial.print!)
int saidas[] = {AIN1, AIN2, BIN1, BIN2, STBY};
for (int p : saidas) pinMode(p, OUTPUT);
digitalWrite(STBY, HIGH); // tira o driver do modo 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); // tempo para o agente atender
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 e Lyrical usam a versão "2" do timer (distros antigas: 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; // a mensagem aponta para o vetor fixo de 2
msgTicks.data.size = 2;
msgTicks.data.capacity = 2;
digitalWrite(PINO_LED, HIGH); // LED aceso fixo = conectado
}
void loop() {
RCSOFT(rclc_executor_spin_some(&executor, RCL_MS_TO_NS(10)));
}
Como verificar
- ✓LED do GPIO 2 aceso fixo e o agente mostra "create_publisher" e "create_subscriber".
- ✓ros2 node list mostra /esp32_base; ros2 topic hz /ticks mostra cerca de 20 Hz.
- ✓ros2 topic echo /ticks: gire uma roda com a mão e o número correspondente em data muda.
- ✗LED piscando rápido: o ESP32 não achou o agente. Ligue o agente e aperte EN (reset) no ESP32.
💡 E a IMU?
Para publicar a MPU-6050 da Trilha 2, use o tipo sensor_msgs/msg/Imu (aceleração linear em m/s², velocidade angular em rad/s) num segundo publisher no mesmo timer, com header.frame_id apontando para um link imu_link que você acrescenta ao URDF. Comece só com os encoders: eles bastam para mapear.
Por que aprender
O padrão "timer copia o dado, executor publica" serve para qualquer sensor: distância, bateria, botão de emergência. E contar pulso no ESP32, por interrupção, é o que garante que nenhum giro da roda se perde enquanto o Pi está ocupado com o mapa.
Conceitos-chave
📥 ESP32 ouvindo /cmd_vel e girando os motores
O /cmd_vel diz como o robô inteiro deve se mover: v (metros por segundo para a frente) e ω (radianos por segundo de giro). O ESP32 precisa converter isso em velocidade de cada roda. É a cinemática diferencial: para girar à esquerda, a roda direita anda mais que a esquerda. Na função aoReceberCmd do firmware isso são duas linhas.
Como ler: visto de cima, com a frente para a direita; a roda de cima é a esquerda do robô. As setas ciano têm tamanho proporcional à velocidade de cada roda: a de baixo (roda direita) é maior, então ele curva para a esquerda, para cima na figura (seta âmbar). L é a distância entre as rodas, a constante SEPARACAO do firmware.
🌱 Novo aqui?
Watchdog de comando é a regra "se não chegar ordem nova em 0,5 s, pare". Teleop e Nav2 publicam várias vezes por segundo; se o Wi-Fi cair ou o PC travar, as mensagens param, e sem watchdog o robô seguiria com a última velocidade até bater. No firmware, ele está no fim de aoTimer.
Objetivo: testar a assinatura com o robô SUSPENSO (rodas no ar), primeiro com um comando fixo e depois com o teclado.
# 1) rodas iguais para a frente a 0,15 m/s, por 3 segundos (10 mensagens/s)
timeout 3 ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.15}}"
# 2) girar no lugar: rodas em sentidos opostos
timeout 3 ros2 topic pub --rate 10 /cmd_vel geometry_msgs/msg/Twist "{angular: {z: 1.0}}"
# 3) dirigir com o teclado, igual ao Gazebo do 4.2
ros2 run teleop_twist_keyboard teleop_twist_keyboard
Como verificar
- ✓Comando 1: as duas rodas giram para a frente e PARAM sozinhas cerca de meio segundo depois que o timeout encerra o publish.
- ✓Comando 2: a roda direita para a frente, a esquerda para trás.
- ✗Uma roda gira ao contrário? Troque os fios daquele motor no driver (ou os pinos IN1/IN2 no código).
✓ Segurança no primeiro teste
- ✓Robô sobre um suporte, rodas sem tocar a mesa.
- ✓Velocidades baixas (0,1 a 0,2 m/s) até confiar.
- ✓Mão perto da chave da bateria dos motores.
✗ Nunca
- ✗Remover o watchdog "porque atrapalha".
- ✗Testar no chão, perto da escada, na primeira vez.
- ✗Alimentar motores pela USB do ESP32.
Por que aprender
Este é o momento em que o robô real passa a obedecer ao ROS 2. Daqui em diante, qualquer nó que publique em /cmd_vel, do teclado ao Nav2, dirige o seu robô sem nenhuma linha nova no firmware.
Conceitos-chave
📍 Odometria do robô real no ROS 2
No Gazebo, o plugin DiffDrive entregava /odom e a TF odom → base_link de graça. No robô real, quem faz isso é você: um nó Python no Pi ou no PC assina /ticks, converte pulsos em metros e acumula a posição (x, y, θ), exatamente a odometria da Trilha 2, agora publicada no formato que o SLAM e o Nav2 esperam.
Como ler: pulsos entram à esquerda; no meio, três contas transformam pulsos em deslocamento e o deslocamento em posição acumulada; à direita saem as duas coisas que o Gazebo fornecia. É por isso que o slam_toolbox do 4.4 funciona igual no simulado e no real.
Objetivo: acrescentar o nó odometria ao pacote meu_robo do 4.1. Os três parâmetros vêm do seu robô: pulsos por volta da roda (veja o datasheet do motor, ou conte girando uma volta à mão), raio da roda e distância entre rodas.
# Converte /ticks (ESP32) em /odom e na 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 ao 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: # primeira mensagem: só 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 # quanto o centro andou
dth = (d_dir - d_esq) / self.sep # quanto girou
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) no setup.py do meu_robo, acrescente em console_scripts:
# 'odometria = meu_robo.odometria:main',
# 2) no package.xml, acrescente exec_depend de nav_msgs, geometry_msgs e 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
Como verificar
- ✓Empurre o robô 1 m em linha reta no chão: ros2 topic echo /odom --field pose.pose.position deve mostrar x perto de 1,0. Se der 0,9 ou 1,1, ajuste raio.
- ✓Gire o robô uma volta no lugar: θ deve voltar perto do início. Se não, ajuste separacao.
- ✓No RViz, Fixed Frame odom e um display TF: o quadro base_link anda junto com o robô.
Por que aprender
Odometria calibrada é metade de um bom mapa. O SLAM do 4.4 corrige o erro aos poucos, mas parte de onde a odometria diz que o robô está: se ela erra 10% a cada metro, o mapa entorta antes de qualquer correção.
Conceitos-chave
🩺 Problemas comuns (Wi-Fi x serial, sincronizar tempo)
Quase todo defeito de micro-ROS cai em quatro grupos: conexão (agente, porta, Wi-Fi), versão (distro diferente entre as três pontas), memória (tópicos demais no ESP32) e tempo (relógio do ESP32 diferente do PC). Siga a tabela de cima para baixo antes de mexer no código.
| Sintoma | Causa provável e conserto |
|---|---|
| Upload falha: "port busy" | O agente está segurando a porta. Pare o agente, grave, ligue de novo. |
| LED piscando rápido | Agente desligado, porta errada ou baud diferente de 115200. Depois de ligar o agente, aperte EN. |
| Nó aparece, tópicos não | Distro da biblioteca diferente da do agente e do ROS. Use as três iguais. |
| Para de responder depois de reiniciar o agente | O firmware não reconecta sozinho. Aperte EN, ou implemente a reconexão (abaixo). |
| "create_publisher" falha no 5º tópico | Limite de memória da biblioteca pré-compilada. Junte dados numa mensagem só (como o /ticks). |
| Wi-Fi: comandos chegam aos trancos | Sinal fraco ou rede cheia. Teste com o cabo; prefira o agente no Pi a bordo. |
Trocar para Wi-Fi: no firmware, troque a linha do transporte serial por esta, preenchendo os três valores entre os sinais de menor e maior; no PC, use o agente udp4 --port 8888 do tópico 2. O IP é o do computador onde o agente roda (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); // no lugar de set_microros_transports()
Como verificar: o agente UDP mostra "create_client" com o IP do ESP32; ros2 topic hz /ticks continua perto de 20 Hz com o robô longe do PC.
🌱 Novo aqui?
Sincronizar o tempo: o ESP32 liga com o relógio em zero, e o PC usa a hora real. Se o ESP32 carimbar mensagens com o próprio relógio, o ROS acha que elas são de 1970 e a TF as descarta. Por isso, neste módulo, quem carimba é o nó de odometria no PC, na chegada. Se precisar carimbar no ESP32 (IMU rápida, por exemplo), chame rmw_uros_sync_session(1000) depois de conectar e use rmw_uros_epoch_nanos() como hora, com #include <rmw_microros/rmw_microros.h>.
💡 Reconexão automática
Para o robô sobreviver a um agente que reinicia, o padrão é uma máquina de estados: esperando agente (testa com rmw_uros_ping_agent(100, 1) a cada meio segundo), criar entidades, conectado (spin; se o ping falhar, motores em zero) e destruir entidades, voltando ao início. O exemplo "micro-ros_reconnection_example" da biblioteca mostra o esqueleto completo; acrescente nele as partes deste firmware.
✓ Ordem de diagnóstico
- ✓1. O agente está rodando e mostra "running"?
- ✓2. ros2 node list mostra /esp32_base?
- ✓3. ros2 topic hz nos tópicos do ESP32?
✗ Perda de tempo
- ✗Mudar o firmware antes de conferir o agente.
- ✗Depurar com Serial.print: a serial é do micro-ROS.
- ✗Começar pelo Wi-Fi sem ter funcionado no cabo.
Por que aprender
No projeto 4.6 o robô vai rodar longe do PC, ligado por horas. Saber diagnosticar conexão, versão e tempo é o que separa um robô que funciona na bancada de um que funciona na sala.
Conceitos-chave
✅ Resumo do módulo
Para ir além
- micro.ros.org — visão geral, conceitos e tutoriais oficiais
- micro_ros_arduino — releases por distro e exemplos (inclusive reconexão)
- micro_ros_espidf_component — caminho pelo ESP-IDF
- Imagem do agente no Docker Hub — confira a tag da sua distro
- docs.ros.org — mensagens nav_msgs, geometry_msgs e tf2
Próximo módulo
4.4 · LiDAR e mapas (SLAM): o laser entra no robô e o slam_toolbox desenha o mapa enquanto descobre onde ele está.