Conteúdo detalhado
🗂️ Workspace, pacotes e colcon
No 4.1 você criou ~/ros2_ws sem pensar muito. Vale entender o que acontece ali. O workspace é uma pasta com src/ (o seu código) e, depois do colcon build, mais três: build/ (rascunho), install/ (o que roda) e log/. Quando você faz source install/setup.bash, o seu workspace passa a ser uma camada por cima do ROS instalado em /opt/ros.
🌱 Novo aqui?
Underlay é a camada de baixo (o ROS em /opt/ros/<distro>); overlay é a de cima (o seu install/). Se um pacote existe nas duas, vale o do overlay. colcon é a ferramenta que compila todos os pacotes de src/ na ordem certa. rosdep lê o package.xml e instala pelo apt as dependências que faltam.
Como ler: o ROS instalado é o chão; o seu workspace se apoia nele. A seta ciano mostra a ordem do source: primeiro /opt/ros (já está no .bashrc), depois o install/ do seu workspace, em todo terminal novo.
Objetivo: criar o pacote meu_robo_description, que vai guardar a descrição do robô, o mundo do Gazebo e o launch. Ele é do tipo ament_python, como o do 4.1.
# ferramentas que este módulo usa (desktop já traz RViz2 e robot_state_publisher)
sudo apt install -y ros-<distro>-ros-gz ros-<distro>-xacro ros-<distro>-tf2-tools \
ros-<distro>-joint-state-publisher-gui ros-<distro>-teleop-twist-keyboard
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --license MIT meu_robo_description
mkdir -p meu_robo_description/urdf meu_robo_description/launch meu_robo_description/worlds
# depois de criar os arquivos dos tópicos 2 e 5:
cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -y
colcon build --symlink-install --packages-select meu_robo_description
source install/setup.bash
import os
from glob import glob
from setuptools import setup
package_name = 'meu_robo_description'
share = os.path.join('share', package_name)
setup(
name=package_name,
version='0.1.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages', ['resource/' + package_name]),
(share, ['package.xml']),
(os.path.join(share, 'urdf'), glob('urdf/*.xacro')), # instala a descrição
(os.path.join(share, 'launch'), glob('launch/*.launch.py')),
(os.path.join(share, 'worlds'), glob('worlds/*.sdf')),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='voce',
maintainer_email='voce@exemplo.com',
description='Descricao e simulacao do robo do curso',
license='MIT',
)
Como verificar: no package.xml gerado, acrescente uma linha <exec_depend> para cada pacote usado: robot_state_publisher, xacro, ros_gz_sim, ros_gz_bridge, rviz2. Depois do build, ros2 pkg prefix meu_robo_description mostra o caminho em install/, e ls install/meu_robo_description/share/meu_robo_description/urdf lista o arquivo do robô.
✓ Hábitos que poupam tempo
- ✓Compilar sempre na raiz do workspace.
- ✓Usar --symlink-install: Python e launch mudam sem recompilar.
- ✓Rodar rosdep install ao baixar pacote de terceiros.
✗ Erros clássicos
- ✗Rodar colcon build dentro de src/.
- ✗Esquecer o source install/setup.bash no terminal novo.
- ✗Criar arquivo novo (URDF, launch) e não registrá-lo no setup.py.
Por que aprender
Metade das perguntas de iniciante em fóruns de ROS é workspace sem source, arquivo não instalado ou dependência faltando. Entender as camadas transforma esses erros em um minuto de conserto.
Conceitos-chave
📐 URDF/xacro: descrever o robô
O URDF (Unified Robot Description Format) descreve o robô como peças rígidas, os links, ligadas por joints (juntas). Cada link tem três partes: visual (como aparece), collision (a forma que bate) e inertial (massa e inércia, que o simulador exige). O xacro é um pré-processador que acrescenta variáveis, contas e macros, para você escrever a roda uma vez e usar duas.
Como ler: o retângulo âmbar é o base_link, a peça-mãe. As rodas (ciano) giram sem limite: junta continuous. O rodízio (tracejado) e o LiDAR (círculo âmbar) não se mexem em relação à base: junta fixed. Pela convenção do ROS, x aponta para a frente e y para a esquerda; o z sai da tela.
Objetivo: descrever um robô diferencial de 25 × 18 cm, com rodas de 4 cm de raio, rodízio e LiDAR, já com os plugins do Gazebo (tração diferencial, estado das juntas e laser). Meça o seu robô e troque as propriedades do topo do arquivo.
<?xml version="1.0"?>
<robot name="robo" xmlns:xacro="http://www.ros.org/wiki/xacro">
<!-- medidas em metros e kg: troque pelas do seu robô -->
<xacro:property name="base_x" value="0.25"/> <xacro:property name="base_y" value="0.18"/>
<xacro:property name="base_z" value="0.06"/> <xacro:property name="raio" value="0.04"/>
<xacro:property name="larg_roda" value="0.025"/> <xacro:property name="sep" value="0.21"/>
<material name="ambar"><color rgba="0.96 0.62 0.04 1"/></material>
<material name="preto"><color rgba="0.1 0.1 0.1 1"/></material>
<link name="base_link">
<visual><geometry><box size="${base_x} ${base_y} ${base_z}"/></geometry><material name="ambar"/></visual>
<collision><geometry><box size="${base_x} ${base_y} ${base_z}"/></geometry></collision>
<inertial><mass value="1.0"/><inertia ixx="0.003" ixy="0" ixz="0" iyy="0.0055" iyz="0" izz="0.008"/></inertial>
</link>
<!-- macro: uma roda; lado = esq ou dir, y = posição lateral -->
<xacro:macro name="roda" params="lado y">
<link name="roda_${lado}">
<visual><geometry><cylinder radius="${raio}" length="${larg_roda}"/></geometry><material name="preto"/></visual>
<collision><geometry><cylinder radius="${raio}" length="${larg_roda}"/></geometry></collision>
<inertial><mass value="0.1"/><inertia ixx="4.5e-5" ixy="0" ixz="0" iyy="4.5e-5" iyz="0" izz="8e-5"/></inertial>
</link>
<joint name="junta_roda_${lado}" type="continuous">
<parent link="base_link"/><child link="roda_${lado}"/>
<origin xyz="0.05 ${y} 0" rpy="-1.5708 0 0"/><axis xyz="0 0 1"/>
</joint>
</xacro:macro>
<xacro:roda lado="esq" y="${sep/2}"/>
<xacro:roda lado="dir" y="${-sep/2}"/>
<link name="rodizio">
<visual><geometry><sphere radius="0.02"/></geometry><material name="preto"/></visual>
<collision><geometry><sphere radius="0.02"/></geometry></collision>
<inertial><mass value="0.05"/><inertia ixx="8e-6" ixy="0" ixz="0" iyy="8e-6" iyz="0" izz="8e-6"/></inertial>
</link>
<joint name="junta_rodizio" type="fixed">
<parent link="base_link"/><child link="rodizio"/><origin xyz="-0.09 0 -0.02"/>
</joint>
<link name="lidar_link">
<visual><geometry><cylinder radius="0.035" length="0.04"/></geometry><material name="preto"/></visual>
<collision><geometry><cylinder radius="0.035" length="0.04"/></geometry></collision>
<inertial><mass value="0.1"/><inertia ixx="5e-5" ixy="0" ixz="0" iyy="5e-5" iyz="0" izz="6e-5"/></inertial>
</link>
<joint name="junta_lidar" type="fixed">
<parent link="base_link"/><child link="lidar_link"/><origin xyz="0.05 0 0.05"/>
</joint>
<!-- Gazebo (Harmonic ou Jetty): rodízio sem atrito, tração, juntas e laser -->
<gazebo reference="rodizio"><mu1>0.0</mu1><mu2>0.0</mu2></gazebo>
<gazebo>
<plugin filename="gz-sim-diff-drive-system" name="gz::sim::systems::DiffDrive">
<left_joint>junta_roda_esq</left_joint><right_joint>junta_roda_dir</right_joint>
<wheel_separation>${sep}</wheel_separation><wheel_radius>${raio}</wheel_radius>
<topic>cmd_vel</topic><odom_topic>odom</odom_topic><tf_topic>tf</tf_topic>
<frame_id>odom</frame_id><child_frame_id>base_link</child_frame_id>
<odom_publish_frequency>30</odom_publish_frequency>
</plugin>
<plugin filename="gz-sim-joint-state-publisher-system" name="gz::sim::systems::JointStatePublisher">
<topic>joint_states</topic>
</plugin>
</gazebo>
<gazebo reference="lidar_link">
<sensor name="lidar" type="gpu_lidar">
<topic>scan</topic><gz_frame_id>lidar_link</gz_frame_id>
<update_rate>10</update_rate><always_on>1</always_on><visualize>true</visualize>
<lidar>
<scan><horizontal><samples>360</samples><resolution>1</resolution>
<min_angle>-3.14159</min_angle><max_angle>3.14159</max_angle></horizontal></scan>
<range><min>0.12</min><max>12.0</max><resolution>0.01</resolution></range>
</lidar>
</sensor>
</gazebo>
</robot>
Como verificar
- ✓xacro robo.urdf.xacro > /tmp/robo.urdf não mostra erro.
- ✓check_urdf /tmp/robo.urdf imprime a árvore: base_link com 4 filhos (pacote liburdfdom-tools).
- ✗"Undefined substitution argument"? Faltou declarar alguma xacro:property.
Por que aprender
Um arquivo serve a todos: o RViz desenha o robô com ele, o Gazebo simula a física, o robot_state_publisher calcula a posição de cada peça e o Nav2 usa as medidas para não bater. Se a distância entre rodas estiver errada aqui, a odometria e o mapa saem tortos lá na frente.
Conceitos-chave
🌳 TF2: quadros de coordenadas
O LiDAR diz "tem parede a 1 m, no ângulo 30°", mas 1 m de onde? Do próprio LiDAR. Para pôr essa parede no mapa, o ROS precisa encadear: LiDAR → base do robô → ponto onde o robô começou → mapa. Cada um desses é um quadro de coordenadas (frame), e a biblioteca TF2 guarda a árvore deles com o tempo, para responder a qualquer momento "onde está A em relação a B".
Como ler: cada caixa é um quadro, cada linha uma transformação, e cada transformação tem um único publicador (texto cinza). O robot_state_publisher publica as linhas âmbar a partir do URDF. A odometria publica odom → base_link. A linha tracejada map → odom só aparece quando entra o SLAM: é a correção do erro acumulado da odometria.
🌱 Novo aqui?
odom é o ponto onde o robô ligou: a posição em relação a ele muda suave, mas acumula erro (a roda patina). map é o referencial fixo do mapa: pode dar pequenos saltos quando o SLAM corrige, mas não deriva. Essa divisão segue a convenção REP 105, que todo pacote de navegação respeita.
# onde está o LiDAR em relação à base? (translação e rotação)
ros2 run tf2_ros tf2_echo base_link lidar_link
# gera frames_<data>.pdf com a árvore inteira
ros2 run tf2_tools view_frames
Como verificar: o tf2_echo imprime "Translation: [0.050, 0.000, 0.050]", exatamente o origin da junta do LiDAR no URDF. O PDF mostra odom → base_link → quatro filhos.
Por que aprender
TF errada é o defeito mais traiçoeiro da navegação: tudo roda, mas o laser aparece girado, o mapa duplica paredes, o robô acha que está atravessando o sofá. Quem sabe ler a árvore acha o problema em minutos.
Conceitos-chave
👁️ RViz2: ver o robô e os sensores
O RViz2 não simula nada: ele desenha o que os tópicos dizem. É a janela para dentro da cabeça do robô. Antes de levar o modelo ao Gazebo, confira-o no RViz com dois nós: o robot_state_publisher, que lê o URDF e publica a TF das peças, e o joint_state_publisher_gui, uma janela com controles deslizantes para girar as rodas à mão.
Passo a passo: o modelo no RViz2
Publique a descrição
Rode os três comandos do bloco abaixo, cada um em um terminal com o workspace ativado.
Ajuste o Fixed Frame
No painel "Global Options", troque Fixed Frame para base_link (sem simulador não existe odom).
Adicione as visualizações
Botão "Add": RobotModel (em Description Topic escolha /robot_description) e TF. Salve com File → Save Config As em ~/ros2_ws/robo.rviz.
Mexa nas rodas
Arraste os controles da janela do joint_state_publisher_gui e veja as rodas girarem no RViz.
cd ~/ros2_ws/src/meu_robo_description/urdf
# terminal 1: lê o URDF e publica a TF das peças
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(xacro robo.urdf.xacro)"
# terminal 2: controles deslizantes para as juntas das rodas
ros2 run joint_state_publisher_gui joint_state_publisher_gui
# terminal 3
rviz2
Como verificar: aparece uma caixa âmbar com duas rodas pretas, o rodízio atrás e o LiDAR em cima. Os eixos da TF mostram x (vermelho) para a frente. ros2 topic echo /joint_states muda quando você arrasta um controle.
✓ O que conferir no RViz
- ✓A frente do robô coincide com o eixo vermelho (x).
- ✓As rodas tocam o chão na mesma altura do rodízio.
- ✓O LiDAR está onde ele fica no robô real.
✗ Sinais de problema
- ✗"No transform from roda_esq": falta /joint_states.
- ✗Tela vazia: Fixed Frame aponta para um quadro que não existe.
- ✗Rodas deitadas: o rpy da junta está errado.
Por que aprender
Todo o resto da trilha passa pelo RViz: o laser no 4.4, o mapa e o caminho planejado no 4.5, o clique de "vá até ali". Aprender a montar e salvar uma configuração agora poupa refazer tudo a cada vez.
Conceitos-chave
🌐 Gazebo (Harmonic/Jetty) e ros_gz
O Gazebo simula física, colisões e sensores. A versão casa com a distro: Harmonic com Jazzy e Jetty com Lyrical; o pacote ros-<distro>-ros-gz já traz o par certo. O Gazebo tem a sua própria rede de mensagens; quem liga as duas é a ponte ros_gz_bridge, que traduz tópico a tópico. O antigo "Gazebo Classic" acabou em janeiro de 2025: tutoriais com gazebo_ros e arquivos .world antigos não servem mais.
Como ler: a seta âmbar de cima é o comando que sai do ROS e entra no simulador (velocidade). A seta ciano de baixo é o que o simulador "mede" e devolve ao ROS. Só passa pela ponte o que você listar nela: tópico esquecido na ponte é tópico que não existe do outro lado.
Objetivo: um mundo com chão, luz, o sistema de sensores (sem ele o LiDAR não publica) e duas caixas para o laser enxergar.
<?xml version="1.0"?>
<sdf version="1.9">
<world name="sala">
<plugin filename="gz-sim-physics-system" name="gz::sim::systems::Physics"/>
<plugin filename="gz-sim-user-commands-system" name="gz::sim::systems::UserCommands"/>
<plugin filename="gz-sim-scene-broadcaster-system" name="gz::sim::systems::SceneBroadcaster"/>
<plugin filename="gz-sim-sensors-system" name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>
<light type="directional" name="sol">
<pose>0 0 10 0 0 0</pose><diffuse>0.9 0.9 0.9 1</diffuse><direction>-0.5 0.1 -0.9</direction>
</light>
<model name="chao"><static>true</static><link name="link">
<collision name="c"><geometry><plane><normal>0 0 1</normal><size>20 20</size></plane></geometry></collision>
<visual name="v"><geometry><plane><normal>0 0 1</normal><size>20 20</size></plane></geometry></visual>
</link></model>
<model name="caixa1"><static>true</static><pose>1.5 0 0.25 0 0 0</pose><link name="link">
<collision name="c"><geometry><box><size>0.5 0.5 0.5</size></box></geometry></collision>
<visual name="v"><geometry><box><size>0.5 0.5 0.5</size></box></geometry></visual>
</link></model>
<model name="caixa2"><static>true</static><pose>-1 1.2 0.25 0 0 0.5</pose><link name="link">
<collision name="c"><geometry><box><size>1 0.3 0.5</size></box></geometry></collision>
<visual name="v"><geometry><box><size>1 0.3 0.5</size></box></geometry></visual>
</link></model>
</world>
</sdf>
Objetivo: um launch que abre o Gazebo com o mundo, publica a descrição, coloca o robô no mundo, liga a ponte e abre o RViz2. Na ponte, ] quer dizer ROS → Gazebo e [ quer dizer Gazebo → ROS.
import os
import xacro
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
def generate_launch_description():
pkg = get_package_share_directory('meu_robo_description')
urdf = xacro.process_file(os.path.join(pkg, 'urdf', 'robo.urdf.xacro')).toxml()
mundo = os.path.join(pkg, 'worlds', 'sala.sdf')
sim = {'use_sim_time': True} # todos seguem o relógio do Gazebo
gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource(os.path.join(
get_package_share_directory('ros_gz_sim'), 'launch', 'gz_sim.launch.py')),
launch_arguments={'gz_args': f'-r {mundo}'}.items()) # -r: já começa rodando
rsp = Node(package='robot_state_publisher', executable='robot_state_publisher',
parameters=[{'robot_description': urdf}, sim])
nascer = Node(package='ros_gz_sim', executable='create',
arguments=['-topic', 'robot_description', '-name', 'robo', '-z', '0.05'])
ponte = Node(package='ros_gz_bridge', executable='parameter_bridge', parameters=[sim],
arguments=[
'/cmd_vel@geometry_msgs/msg/Twist]gz.msgs.Twist',
'/odom@nav_msgs/msg/Odometry[gz.msgs.Odometry',
'/scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan',
'/tf@tf2_msgs/msg/TFMessage[gz.msgs.Pose_V',
'/joint_states@sensor_msgs/msg/JointState[gz.msgs.Model',
'/clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock',
])
rviz = Node(package='rviz2', executable='rviz2', parameters=[sim])
return LaunchDescription([gazebo, rsp, nascer, ponte, rviz])
cd ~/ros2_ws
colcon build --symlink-install --packages-select meu_robo_description
source install/setup.bash
ros2 launch meu_robo_description sim.launch.py
Como verificar
- ✓O Gazebo abre com o robô âmbar no centro e duas caixas; o laser aparece como raios azuis.
- ✓No RViz, com Fixed Frame odom, adicione RobotModel e LaserScan (/scan): os pontos do laser desenham a borda das caixas.
- ✓ros2 topic hz /scan mostra cerca de 10 Hz.
- ✗Gazebo fecha logo ao abrir em máquina virtual? Rode antes export LIBGL_ALWAYS_SOFTWARE=1 (fica lento, mas funciona).
Por que aprender
O mesmo launch, com outra ponte, vira o do robô real no 4.6. Mapear e navegar primeiro na simulação mostra se um problema é do algoritmo ou do hardware, e deixa você estudar a trilha toda sem LiDAR.
Conceitos-chave
🎮 Dirigir o robô simulado com teleop
Com a simulação aberta, falta alguém publicar em /cmd_vel. O teleop_twist_keyboard transforma teclas em mensagens Twist. É o teste de fumaça de toda a trilha: se o robô anda, o laser gira junto e a odometria acompanha, a cadeia comando → simulador → sensores → ROS está inteira. No 4.3, este mesmo comando vai mover o robô real.
# deixe ESTE terminal em foco enquanto dirige
ros2 run teleop_twist_keyboard teleop_twist_keyboard
# i = frente , = ré j / l = girar k = parar q / z = mais / menos velocidade
# em outro terminal, confira o que está acontecendo
ros2 topic echo /cmd_vel
ros2 topic echo /odom --field pose.pose.position
Como verificar
- ✓Com i, o robô anda para a frente no Gazebo e no RViz ao mesmo tempo.
- ✓O echo /cmd_vel mostra linear.x: 0.5; o x da odometria cresce.
- ✓Ao se aproximar de uma caixa, os pontos do LaserScan no RViz ficam parados no mundo (o robô é que se move).
- ✗Robô anda de ré com i? O eixo das juntas das rodas está invertido no URDF.
💡 Twist ou TwistStamped?
O teleop publica Twist por padrão, que é o que a ponte acima espera. Pacotes mais novos, como o Nav2 do módulo 4.5, podem publicar TwistStamped (Twist com carimbo de tempo). Quando chegar lá, ajuste o tipo na ponte ou no parâmetro do Nav2 para os dois lados combinarem; tipo diferente no mesmo tópico simplesmente não conecta.
🌱 Novo aqui?
Repare que, se você dirigir em círculo e voltar ao ponto de partida, a odometria do Gazebo diz exatamente 0. No robô real ela não vai dizer: a roda patina e o erro se acumula. É esse erro que o SLAM do módulo 4.4 corrige com o laser.
Por que aprender
Dirigir à mão é como você vai mapear a casa no 4.4 e testar o robô real no 4.3. E, quando algo der errado com o Nav2, voltar ao teleop separa "o robô não obedece" de "o planejador não planeja".
Conceitos-chave
✅ Resumo do módulo
Para ir além
- Tutoriais de URDF (docs.ros.org) — links, juntas e xacro passo a passo
- Sobre o TF2 — conceitos e ferramentas
- Documentação do Gazebo — escolha Harmonic ou Jetty no topo
- Gazebo com ROS (ros_gz) — qual par instalar com cada distro
- REP 105 — a convenção map, odom e base_link
Próximo módulo
4.3 · micro-ROS no ESP32: o robô real entra no ROS 2, obedece ao mesmo /cmd_vel e devolve odometria.