PTENES
MÓDULO 4.2 · PRÁTICA

🧱 Descrever e simular o robô

Antes de mapear e navegar, o ROS precisa saber como o seu robô é: onde ficam as rodas, o LiDAR, o centro. Você descreve isso em URDF/xacro, confere no RViz2 e coloca o robô para andar no Gazebo, dirigido pelo teclado. Tudo no PC, sem nenhuma peça.

6
Tópicos
Intermediário
Nível
Prática
Tipo
Só o PC
Hardware exigido
0%
0 de 0 tópicos lidos · Seção 1 de 6

Conteúdo detalhado

1

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

Ubuntu 26.04 ou 24.04 underlay: /opt/ros/<distro>rclpy, RViz2, ros_gz, xacro... overlay: ~/ros2_ws/installmeu_robo, meu_robo_description source 2ºsource 1º

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.

terminal
# 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
meu_robo_description/setup.py
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

Workspace
src, build, install, log
Overlay
sua camada por cima
rosdep
instala dependências
data_files
o que vai para share/
2

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

base_link roda_esq · continuous roda_dir · continuous rodizio · fixed lidar_link · fixed x (frente)y (esquerda)

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.

meu_robo_description/urdf/robo.urdf.xacro
<?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

Link
peça rígida
Joint
fixed, continuous, revolute
Inertial
massa que o Gazebo exige
xacro:macro
escreva uma vez, use duas
3

🌳 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".

map odom base_link roda_esqroda_dirrodiziolidar_link SLAM ou AMCL (4.4, 4.5)odometria (Gazebo ou 4.3)robot_state_publisher

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.

terminal (com a simulação do tópico 5 rodando)
# 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

Frame
quadro de coordenadas
map / odom
fixo / suave com deriva
base_link
o centro do robô
tf2_echo
consulta uma ligação
4

👁️ 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

1

Publique a descrição

Rode os três comandos do bloco abaixo, cada um em um terminal com o workspace ativado.

2

Ajuste o Fixed Frame

No painel "Global Options", troque Fixed Frame para base_link (sem simulador não existe odom).

3

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.

4

Mexa nas rodas

Arraste os controles da janela do joint_state_publisher_gui e veja as rodas girarem no RViz.

terminal
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

Fixed Frame
referencial da tela
RobotModel
desenha o URDF
/joint_states
ângulo de cada junta
.rviz
configuração salva
5

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

Gazebo mundo sala.sdfDiffDrivegpu_lidarJointStatefísica, relógio ros_gzbridge ROS 2 teleoprobot_state_pub.RViz2SLAM, Nav2 (4.4+) /cmd_vel/odom /scan /tf/joint_states /clock

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.

meu_robo_description/worlds/sala.sdf
<?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.

meu_robo_description/launch/sim.launch.py
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])
terminal
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

Harmonic / Jetty
Gazebo de cada distro
parameter_bridge
traduz tópico a tópico
use_sim_time
relógio do simulador
launch
sobe tudo de uma vez
6

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

terminal (com sim.launch.py rodando)
# 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

teleop
teclado → /cmd_vel
/odom
posição estimada
/scan
leituras do laser
Teste de fumaça
a cadeia está inteira?

✅ Resumo do módulo

✓
Workspace em camadas: /opt/ros embaixo, o seu install/ por cima; colcon na raiz, source em todo terminal.
✓
URDF/xacro descreve links e juntas uma vez para RViz, Gazebo, TF e Nav2.
✓
TF2: map → odom → base_link → sensores, cada ligação com um único publicador.
✓
Gazebo Harmonic (Jazzy) ou Jetty (Lyrical) ligado ao ROS pela ros_gz_bridge, tópico a tópico.
✓
teleop_twist_keyboard fecha o ciclo e serve de teste de fumaça.

Para ir além

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.