Conteúdo detalhado
🏗️ 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.
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.
# 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
🚀 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.
# 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()),
])
# 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
🔄 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.
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.
| Item | Simulado | Real |
|---|---|---|
| /scan | plugin do Gazebo | driver do LiDAR |
| /odom + TF odom | plugin de rodas | nó odometria (ticks do ESP32) |
| /cmd_vel | move o modelo | micro-ROS → ponte H → motores |
| use_sim_time | True | False |
| Erro | quase zero | patina, 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.
# 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
🗺️ 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
Bateria cheia, área preparada
Bateria baixa muda a velocidade dos motores e piora a odometria. Escadas bloqueadas, chão livre, pessoas avisadas.
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.
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.
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.
# 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
🚪 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).
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.
#!/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
🛡️ 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.
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
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.
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.
Junte a detecção da Trilha 3: em cada waypoint, tire uma foto e procure um objeto com YOLO26.
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
✅ Resumo do módulo e da Trilha 4
Para ir além
- Nav2: First-Time Robot Setup Guide — TF, URDF, odometria e sensores do seu próprio robô
- Nav2: Keepout Zones — zonas proibidas sem pintar o mapa
- ROS 2: tutoriais de launch — argumentos, condições e inclusões (troque por jazzy se for a sua)
- slam_toolbox — modo lifelong para mapas que mudam com o tempo
- micro-ROS — agente, transportes e exemplos para ESP32
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.