PTENES
MÓDULO 4.3 · PRÁTICA

🔌 micro-ROS no ESP32

Até aqui o robô vivia no Gazebo. Agora o ESP32 das trilhas anteriores vira um nó ROS 2 de verdade: publica os pulsos dos encoders, obedece ao mesmo /cmd_vel do teleop e para sozinho se a conexão cair. Um nó Python no PC fecha o ciclo com a odometria do robô real.

6
Tópicos
Intermediário
Nível
Prática
Tipo
ESP32
+ motores com encoder
0%
0 de 0 tópicos lidos · Seção 1 de 6

Conteúdo detalhado

1

🧬 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.

ESP32seu firmwarerclccliente XRCE AgentPC ou Pi grafo ROS 2teleopodometriaRViz, Nav2 USB ou Wi-FiDDS para o grafo, o ESP32 é só mais um nó: /esp32_base

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

micro-ROS
ROS 2 para microcontrolador
Agent
ponte para o DDS
rclc
API em C do firmware
Executor
chama os callbacks
2

🛰️ 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.

terminal (PC ou Pi)
# 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.
serial (cabo USB) + estável, sem configurar rede+ ideal para o primeiro teste+ com o Pi a bordo, fica no robô− com o PC, robô preso no fio− porta ocupada = sem upload UDP (Wi-Fi) + robô livre, sem cabo+ agente pode ficar no PC− precisa do IP do agente− rede ruim = comando atrasado− o rádio gasta mais bateria

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

Transporte
serial ou UDP
/dev/ttyUSB0
a porta do cabo
udp4 8888
agente na rede
Mesma distro
agente, ESP32 e ROS
3

📤 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

1

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).

2

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).

3

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).

robo_microros.ino
// 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

/ticks
contagem dos encoders
Timer 50 ms
publica a 20 Hz
Interrupção
não perde pulso
Int32MultiArray
vetor com memória fixa
4

📥 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.

L v, ω roda esquerda: lentaroda direita: rápida v_esq = v − ω·L/2 v_dir = v + ω·L/2 ω > 0: curva para a esquerda · v = 0 e ω ≠ 0: gira no lugar

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.

terminal (agente rodando, ESP32 conectado)
# 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

Cinemática diferencial
v, ω → duas rodas
Subscriber rclc
callback no executor
Watchdog
sem ordem, para
V_MAX
m/s → PWM, calibrado
5

📍 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.

ESP32/ticks nó odometria Δticks × 2πr / ticks_por_voltad = média; Δθ = diferença / Lx += d·cos θ · y += d·sin θ /odom TF odom→base mesmas saídas do Gazebo: o resto do robô não percebe a troca

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.

meu_robo/meu_robo/odometria.py
# 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()
terminal
# 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

nav_msgs/Odometry
pose + velocidade
TransformBroadcaster
publica na TF
Quaternion
ângulo no formato ROS
Calibração
1 m reto, 1 volta
6

🩺 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.

SintomaCausa provável e conserto
Upload falha: "port busy"O agente está segurando a porta. Pare o agente, grave, ligue de novo.
LED piscando rápidoAgente desligado, porta errada ou baud diferente de 115200. Depois de ligar o agente, aperte EN.
Nó aparece, tópicos nãoDistro da biblioteca diferente da do agente e do ROS. Use as três iguais.
Para de responder depois de reiniciar o agenteO firmware não reconecta sozinho. Aperte EN, ou implemente a reconexão (abaixo).
"create_publisher" falha no 5º tópicoLimite de memória da biblioteca pré-compilada. Junte dados numa mensagem só (como o /ticks).
Wi-Fi: comandos chegam aos trancosSinal 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).

robo_microros.ino (trecho do setup)
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

Porta ocupada
agente x upload
Ping do agente
base da reconexão
Sync de tempo
carimbo confiável
Wi-Fi UDP
depois do cabo

✅ Resumo do módulo

✓
micro-ROS põe o ESP32 no grafo; o agente (Docker microros/micro-ros-agent:<distro>) faz a ponte para o DDS.
✓
Mesma distro nas três pontas: biblioteca do ESP32, agente e ROS 2.
✓
ESP32 mede e age: publica /ticks, assina /cmd_vel, converte v e ω em rodas e para sem comando.
✓
PC calcula: o nó odometria publica /odom e a TF odom → base_link, como o Gazebo fazia.
✓
Diagnóstico: agente → node list → topic hz; cabo antes do Wi-Fi; carimbo de tempo no PC.

Para ir além

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á.