PTENES
MÓDULO 4.6 · PROJETO

🏁 Projeto: robô que mapeia e navega

Hora de juntar a trilha inteira num robô de verdade: ESP32 com micro-ROS nos motores, Raspberry Pi 5 com ROS 2 no comando e um LiDAR 2D nos olhos. Um único launch file sobe tudo; o robô mapeia a casa e depois vai sozinho de um cômodo a outro, com travas de segurança.

6
Tópicos
Avançado
Nível
Projeto
Tipo
4.1 a 4.5
Pré-requisitos
0%
0 de 0 tópicos lidos · Seção 1 de 6

Conteúdo detalhado

1

🏗️ Arquitetura do robô real com ROS 2

O robô do projeto é o da Trilha 3 (chassi, motores com encoder, ESP32 e Raspberry Pi 5) com um LiDAR 2D no topo. A divisão de trabalho segue a regra do curso: o computador pensa, o microcontrolador controla. O ESP32 lê encoders e IMU, gira os motores e corta tudo se parar de receber comandos. O Pi 5 roda o ROS 2: driver do LiDAR, agente micro-ROS, SLAM e Nav2. O seu PC entra só para olhar pelo RViz2, pela rede.

Raspberry Pi 5 · ROS 2 micro_ros_agent driver do LiDAR → /scan robot_state_publisher (TF) slam_toolbox ou AMCL Nav2 → /cmd_vel ESP32micro-ROSpub: /odom, /imusub: /cmd_velmotores + encoders LiDAR 2D PCRViz2 USBUSBWi-Fi bateria dos motores separada, GND comum, chave de corte física

Como ler: o quadro âmbar é o Pi 5, onde fica toda a "inteligência" ROS 2. As peças ciano à esquerda são o hardware que conversa por USB. O PC tracejado é opcional: o robô funciona sem ele, você só perde a tela do RViz2.

🌱 Novo aqui?

Com dois adaptadores USB-serial, o Linux pode chamar o LiDAR de /dev/ttyUSB0 hoje e de ttyUSB1 amanhã. Uma regra udev cria um nome fixo (/dev/lidar, /dev/esp32) a partir do número de série de cada adaptador. E o ROS_DOMAIN_ID é o "canal" do ROS 2 na rede: Pi e PC precisam do mesmo número para se enxergar, e robôs vizinhos usam números diferentes.

Objetivo: dar nomes fixos às portas no Pi 5 e combinar o canal ROS 2 com o PC. Os números <idVendor>, <idProduct> e <serial> você lê com o primeiro comando, um adaptador por vez.

terminal (no Pi 5)
# 1) descubra os dados do adaptador ligado agora
udevadm info -a -n /dev/ttyUSB0 | grep -m3 -E "idVendor|idProduct|serial"

# 2) crie a regra (uma linha por dispositivo) e recarregue
sudo nano /etc/udev/rules.d/99-robo.rules
#  SUBSYSTEM=="tty", ATTRS{idVendor}=="<idVendor>", ATTRS{idProduct}=="<idProduct>", ATTRS{serial}=="<serial>", SYMLINK+="lidar"
#  SUBSYSTEM=="tty", ATTRS{idVendor}=="<idVendor>", ATTRS{idProduct}=="<idProduct>", ATTRS{serial}=="<serial>", SYMLINK+="esp32"
sudo udevadm control --reload-rules && sudo udevadm trigger

# 3) mesmo canal ROS 2 no Pi e no PC (escolha um número de 0 a 101)
echo "export ROS_DOMAIN_ID=42" >> ~/.bashrc && source ~/.bashrc
ls -l /dev/lidar /dev/esp32

Como verificar: o ls mostra /dev/lidar e /dev/esp32 apontando para algum ttyUSB; troque os cabos de porta USB, reinicie e os nomes continuam certos. No PC, com o mesmo ROS_DOMAIN_ID, ros2 topic list mostra os tópicos do robô.

Por que aprender

Robô que funciona na bancada e falha "às vezes" quase sempre tem uma destas causas: porta trocada, energia do motor derrubando o Pi ou rede misturando robôs. Uma arquitetura clara evita as três antes de aparecerem.

Conceitos-chave

Pi pensa
ROS 2, SLAM, Nav2
ESP32 age
motores e parada
Regra udev
nome fixo da porta
ROS_DOMAIN_ID
canal na rede
2

🚀 Launch files que sobem tudo

Abrir cinco terminais a cada teste cansa e gera erro. Um launch file em Python descreve todos os nós, seus parâmetros e as inclusões de outros launch files, e sobe tudo com um comando. Este aqui tem um interruptor: slam:=True para mapear, slam:=False para navegar no mapa salvo. Ele mora na pasta launch/ do pacote meu_robo que você criou no módulo 4.1 (o URDF vem do meu_robo_description, do 4.2).

Objetivo: um launch que sobe descrição do robô, agente micro-ROS, LiDAR e Nav2 (com SLAM ou com mapa). Ajuste o nome do arquivo xacro, a velocidade da serial do seu LiDAR (o C1 e o A1 usam valores diferentes: confira no launch do driver) e o caminho do mapa.

meu_robo/launch/robo_completo.launch.py
# Sobe o robô real inteiro: TF, ESP32 (micro-ROS), LiDAR e Nav2 com SLAM ou mapa
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import Command, LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue


def generate_launch_description():
    pkg = get_package_share_directory('meu_robo')
    nav2 = get_package_share_directory('nav2_bringup')
    desc = get_package_share_directory('meu_robo_description')   # pacote do módulo 4.2
    xacro_arq = os.path.join(desc, 'urdf', 'robo.urdf.xacro')
    descricao = ParameterValue(Command(['xacro ', xacro_arq]), value_type=str)

    return LaunchDescription([
        DeclareLaunchArgument('slam', default_value='True',
                              description='True = mapear; False = navegar no mapa salvo'),
        DeclareLaunchArgument('mapa', default_value=os.path.expanduser('~/mapas/casa.yaml')),
        DeclareLaunchArgument('params', default_value=os.path.join(pkg, 'config', 'nav2_robo.yaml')),

        # 1) URDF -> TF fixos (base_link -> laser, base_link -> imu)
        Node(package='robot_state_publisher', executable='robot_state_publisher',
             parameters=[{'robot_description': descricao}]),

        # 2) ponte com o ESP32: /ticks e /cmd_vel passam por aqui (módulo 4.3)
        Node(package='micro_ros_agent', executable='micro_ros_agent',
             arguments=['serial', '--dev', '/dev/esp32', '-b', '115200']),

        # 3) LiDAR (RPLIDAR; para o LD19 troque pacote, executável e parâmetros)
        Node(package='rplidar_ros', executable='rplidar_node',
             parameters=[{'serial_port': '/dev/lidar', 'serial_baudrate': 460800,
                          'frame_id': 'laser'}]),

        # 3b) nó odometria do módulo 4.3: /ticks -> /odom e TF odom -> base_link
        Node(package='meu_robo', executable='odometria'),

        # 4) Nav2: com slam:=True ele também sobe o slam_toolbox
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource(os.path.join(nav2, 'launch', 'bringup_launch.py')),
            launch_arguments={'slam': LaunchConfiguration('slam'),
                              'map': LaunchConfiguration('mapa'),
                              'params_file': LaunchConfiguration('params'),
                              'use_sim_time': 'False'}.items()),
    ])
terminal (no Pi 5)
# no setup.py do meu_robo (pacote ament_python), dentro de data_files, acrescente:
#   (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
#   (os.path.join('share', package_name, 'config'), glob('config/*.yaml')),
# (e "import os" + "from glob import glob" no topo do setup.py)
mkdir -p ~/ros2_ws/src/meu_robo/config
cp ~/meu_nav2.yaml ~/ros2_ws/src/meu_robo/config/nav2_robo.yaml   # do módulo 4.5
cd ~/ros2_ws && colcon build --symlink-install && source install/setup.bash
ros2 launch meu_robo robo_completo.launch.py slam:=True

Como verificar

  • ✓O LiDAR gira, o terminal mostra o agente micro-ROS criando a sessão com o ESP32 e o Nav2 termina com os servidores "active".
  • ✓ros2 topic list mostra /scan, /odom, /map e /cmd_vel.
  • ✗"executable not found" no LiDAR: abra o launch do driver e copie de lá o nome do executável e dos parâmetros.

Por que aprender

O launch file é a "receita" do robô: quem pega o seu projeto (um colega, um aluno ou você daqui a seis meses) sobe tudo com uma linha, em vez de adivinhar a ordem dos terminais.

Conceitos-chave

Launch file
sobe tudo junto
Argumento
slam:=True/False
Include
reusa o do Nav2
data_files
instala launch e config
3

🔄 Do simulado ao real: o que muda

No Gazebo tudo era perfeito: rodas que não patinam, LiDAR sem ruído, relógio sincronizado. No robô real, o SLAM e o Nav2 são os mesmos; o que muda é quem publica os dados e a qualidade deles. A maior parte dos problemas da passagem para o real está na odometria e na árvore de frames, não no Nav2.

map odom base_link laser imu slam_toolbox ou AMCL (corrige a deriva) nó odometria (ticks do ESP32, 4.3) robot_state_publisher (URDF, fixo)

Como ler: cada seta é uma transformação, e o texto ao lado diz quem a publica. Cada seta precisa de exatamente um publicador. O erro clássico é a odometria e outro nó publicarem odom → base_link ao mesmo tempo: o robô "pula" no RViz2.

ItemSimuladoReal
/scanplugin do Gazebodriver do LiDAR
/odom + TF odomplugin de rodasnó odometria (ticks do ESP32)
/cmd_velmove o modelomicro-ROS → ponte H → motores
use_sim_timeTrueFalse
Erroquase zeropatina, ruído, bateria caindo

Objetivo: conferir as três fontes do robô real antes de mapear. Rode com o launch do tópico 2 no ar e o robô com as rodas suspensas para o teste de comando.

terminal
# 1) taxas: /scan perto da taxa do LiDAR, /odom estável
ros2 topic hz /scan
ros2 topic hz /odom

# 2) a odometria anda junto com o robô? (empurre 1 m com a mão e compare)
ros2 run tf2_ros tf2_echo odom base_link

# 3) o comando chega aos motores? (rodas no ar; Ctrl+C para parar)
ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1}}"

Como verificar

  • ✓Empurrando o robô 1 m em linha reta, o x do tf2_echo passa perto de 1,0 (erro de poucos centímetros).
  • ✓Com o pub, as duas rodas giram para a frente; ao parar de publicar, param em menos de um segundo (watchdog do ESP32).
  • ✗x sai negativo ou o dobro: sinal do encoder invertido ou raio da roda errado no código da odometria.

Por que aprender

Sim-to-real é uma habilidade que você vai usar de novo na Trilha 5, com o braço. A lição é a mesma: mantenha o "cérebro" igual e valide cada fonte de dado do mundo real, uma de cada vez.

Conceitos-chave

Um publicador
por transformação
tf2_echo
lê uma transformação
Rodas no ar
1º teste de motor
Sim-to-real
mesmo cérebro
4

🗺️ Mapear o ambiente

Com as fontes validadas, mapeie de verdade. O roteiro é o do módulo 4.4, agora no robô completo: launch com slam:=True, teleop pelo PC, mapa salvo com o nome do lugar. Faça um mapa por ambiente (casa, sala de aula, laboratório) e guarde todos em ~/mapas.

Passo a passo

1

Bateria cheia, área preparada

Bateria baixa muda a velocidade dos motores e piora a odometria. Escadas bloqueadas, chão livre, pessoas avisadas.

2

Suba o robô em modo SLAM

No Pi (por SSH): ros2 launch meu_robo robo_completo.launch.py slam:=True. No PC: RViz2 com Fixed Frame = map.

3

Dirija devagar e feche laços

Teleop no PC, velocidade baixa, giros lentos, volta completa em cada cômodo e retorno ao ponto de partida.

4

Salve e marque a área proibida

Salve com o map_saver. Depois abra o casa.pgm num editor de imagem e pinte de preto uma faixa larga diante da escada: para o Nav2, aquilo vira parede.

Objetivo: os comandos da sessão de mapeamento, um terminal por bloco.

terminal
# Pi 5 (SSH): robô completo em modo mapeamento
ros2 launch meu_robo robo_completo.launch.py slam:=True

# PC: ver e dirigir (mesmo ROS_DOMAIN_ID)
rviz2 &
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# Pi 5 (outro SSH), com o mapa bom na tela: salvar
mkdir -p ~/mapas && ros2 run nav2_map_server map_saver_cli -f ~/mapas/casa

Como verificar: ~/mapas/casa.pgm mostra a planta sem paredes duplicadas; corredores retos saem retos. Se uma parede aparece "dobrada", refaça passando de novo pelo trecho e fechando o laço antes de salvar.

💡 Área proibida de verdade

Pintar o PGM é o jeito simples. O Nav2 também tem zonas de exclusão (keepout) com uma máscara separada do mapa, o que permite mudar a área sem refazer o mapa; procure "Keepout Zones" em docs.nav2.org. Em qualquer caso, a trava principal perto de escada é física: portão, barreira ou porta fechada.

Por que aprender

É o mesmo processo que um técnico faz ao instalar um robô de limpeza ou de entrega num prédio: mapear, revisar o mapa e marcar onde o robô não pode ir.

Conceitos-chave

slam:=True
modo mapeamento
SSH + RViz2
Pi trabalha, PC mostra
Um mapa
por ambiente
Keepout
zona proibida
5

🚪 Navegar entre cômodos

Coordenadas como (3,0; 1,5) não dizem nada para quem usa o robô. O passo final é dar nomes aos lugares: um dicionário que liga "cozinha" à pose do mapa. Assim, "vai para a cozinha" vira um comando simples, que mais tarde pode vir de um botão, de uma mensagem ou da sua voz (Trilha 5).

"cozinha"pedido LUGARESbase: 0,0 · 0°cozinha: 3,0 1,5 · 90°quarto: 0,5 −1,5 goToPoseNav2 robô vai nome desconhecido → o script recusa e lista os nomes válidos

Como ler: o pedido (ciano) só vira movimento se o nome estiver na tabela âmbar. Essa "lista fechada" é a mesma ideia de segurança que a Trilha 5 usa com modelos de linguagem: o pedido escolhe entre opções permitidas, nunca inventa coordenadas.

Objetivo: um comando python3 vai_para.py cozinha que leva o robô ao cômodo pelo nome. Antes, suba o robô com slam:=False e dê a posição inicial no RViz2. Meça as poses do seu mapa passando o mouse no RViz2.

vai_para.py
#!/usr/bin/env python3
# Uso: python3 vai_para.py cozinha   (só aceita nomes da lista LUGARES)
import math
import sys
import rclpy
from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult

LUGARES = {                       # nome: (x, y, ângulo em graus) no mapa casa.yaml
    'base': (0.0, 0.0, 0.0),
    'sala': (1.2, 0.8, 0.0),
    'cozinha': (3.0, 1.5, 90.0),
    'quarto': (0.5, -1.5, 180.0),
}


def main():
    nome = sys.argv[1].lower() if len(sys.argv) > 1 else ''
    if nome not in LUGARES:
        print('Lugar desconhecido. Use um destes:', ', '.join(LUGARES))
        return
    x, y, graus = LUGARES[nome]

    rclpy.init()
    nav = BasicNavigator()
    nav.waitUntilNav2Active()
    alvo = PoseStamped()
    alvo.header.frame_id = 'map'
    alvo.header.stamp = nav.get_clock().now().to_msg()
    alvo.pose.position.x, alvo.pose.position.y = x, y
    alvo.pose.orientation.z = math.sin(math.radians(graus) / 2)
    alvo.pose.orientation.w = math.cos(math.radians(graus) / 2)

    print(f'Indo para {nome}...')
    nav.goToPose(alvo)
    while not nav.isTaskComplete():
        pass
    ok = nav.getResult() == TaskResult.SUCCEEDED
    print('Cheguei.' if ok else 'Não consegui chegar.')
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Como verificar

  • ✓python3 vai_para.py cozinha imprime "Indo para cozinha...", o robô atravessa a porta e para virado como você definiu; depois "Cheguei.".
  • ✓python3 vai_para.py garagem não move nada e lista os nomes válidos.
  • ✗Trava na porta? Veja o costmap no RViz2: se a inflação fecha a passagem, reduza o inflation_radius (módulo 4.5).

Por que aprender

É aqui que o robô deixa de ser uma demonstração técnica e vira algo útil: levar um objeto para a cozinha, fazer a ronda da sala, voltar para a base. E a lista fechada de lugares é o primeiro passo para ligar um "cérebro" de IA com segurança.

Conceitos-chave

Lugares nomeados
nome → pose
Lista fechada
recusa o resto
slam:=False
navega no mapa
Porta
teste da inflação
6

🛡️ Checklist de segurança e próximos passos

Um robô que anda sozinho perto de pessoas precisa de travas em várias camadas, porque cada uma falha de um jeito diferente. O software pode travar, a rede pode cair, o LiDAR não vê degrau. A regra é: nenhuma camada sozinha é a sua segurança, e a mais forte é sempre a física.

pessoa supervisionando, área preparada Nav2: velocidade baixa, zonas proibidas ESP32: para se não chegar /cmd_vel chave física corta os motores

Como ler: de fora para dentro, cada camada é mais simples e mais confiável. Se o Nav2 errar, o watchdog do ESP32 ainda para o robô quando os comandos somem; se o ESP32 travar, a chave física corta a energia. A moldura tracejada é você: nas primeiras semanas, ninguém sai de perto.

✓ Antes de cada navegação

  • ✓Chave de corte dos motores ao alcance da mão e testada.
  • ✓Watchdog de comando no ESP32: sem /cmd_vel por meio segundo, motores parados.
  • ✓Velocidade máxima baixa no velocity_smoother; suba só depois de muitos testes.
  • ✓Escada fechada fisicamente e marcada como proibida no mapa.
  • ✓Bateria com BMS, presa no chassi, fios longe das rodas.

✗ Nunca

  • ✗Deixar o robô navegando sozinho com crianças, idosos ou animais por perto.
  • ✗Confiar no LiDAR 2D para ver degrau, vidro ou objeto baixo no chão.
  • ✗Testar velocidade nova perto de escada, mesa de vidro ou móvel frágil.
  • ✗Deixar a bateria de lítio carregando no robô sem ninguém olhando.

Desafios para ir além

🧹 Sensor de degrau

Dois sensores infravermelhos apontados para o chão (módulo 1.4) no ESP32: se o chão sumir, os motores param antes de qualquer comando do Pi.

🔋 Volta para a base

Publique a tensão da bateria pelo micro-ROS e faça o script mandar o robô para "base" quando ela cair abaixo de um limite.

📷 Câmera na ronda

Junte a detecção da Trilha 3: em cada waypoint, tire uma foto e procure um objeto com YOLO26.

📱 Botões no celular

Uma página simples (Trilha 2) com um botão por cômodo, chamando o mesmo dicionário LUGARES.

🌱 Novo aqui?

Um watchdog de comando é um temporizador no microcontrolador que é "zerado" a cada comando recebido. Se o tempo estourar (rede caiu, Pi travou, programa fechou), ele para os motores por conta própria. Você viu a ideia no controle por Wi-Fi do módulo 2.1; no robô com ROS 2 ela vale ainda mais, porque são muitos programas entre você e a roda.

Por que aprender

Robô móvel é a primeira máquina do curso que pode machucar alguém sem você tocar nela. Criar o hábito das camadas de segurança agora é o que vai permitir, na Trilha 5, colocar um braço robótico e um modelo de IA no comando sem medo.

Conceitos-chave

Camadas
nenhuma basta sozinha
Watchdog
sem comando, para
Chave física
a trava final
Supervisão
você por perto

✅ Resumo do módulo e da Trilha 4

✓
Arquitetura: ESP32 com micro-ROS nos motores, Pi 5 com ROS 2, LiDAR por USB, portas com nome fixo.
✓
Um launch file sobe o robô inteiro, com slam:=True para mapear e False para navegar.
✓
Do simulado ao real: mesmo SLAM e Nav2; valide /scan, odometria e TF uma fonte por vez.
✓
Lugares nomeados: lista fechada de destinos, o mesmo princípio de segurança da IA na Trilha 5.
✓
Segurança em camadas: Nav2 lento, watchdog no ESP32, chave física e você por perto.

Para ir além

Próxima trilha

Trilha 5 · IA moderna e braço robótico: um modelo de linguagem escolhe entre ações permitidas, voz local, o braço SO-101 com LeRobot e, no projeto final, a sua base móvel desta trilha com um braço em cima.