PTENES
MÓDULO 4.2 · PRÁCTICA

🧱 Describir y simular el robot

Antes de mapear y navegar, ROS necesita saber cómo es tu robot: dónde están las ruedas, el LiDAR y el centro. Describes esto en URDF/xacro, lo verificas en RViz2 y pones al robot a andar en Gazebo , dirigido por el teclado. Todo en la PC, sin ninguna pieza.

6
Temas
Intermedio
Nivel
Práctica
Tipo
Solo la PC
Hardware necesario
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

🗂️ Workspace, paquetes y colcon

En el 4.1 creaste ~/ros2_ws sin pensarlo mucho. Vale la pena entender qué pasa ahí. El workspace es una carpeta con src/ (tu código) y, después del colcon build, otros tres: build/ (borrador), install/ (lo que se ejecuta) y log/. Cuando ejecutas source install/setup.bash, tu workspace pasa a ser una capa sobre ROS instalado en /opt/ros.

🌱 ¿Eres nuevo aquí?

Underlay es la capa de abajo (ROS en /opt/ros/<distro>); overlay es la de arriba (tu install/). Si un paquete existe en ambas, prevalece el del overlay. colcon es la herramienta que compila todos los paquetes de src/ en el orden correcto. rosdep lee el package.xml e instala con apt las dependencias que faltan.

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

Cómo leer: ROS instalado es el suelo; tu workspace se apoya en él. La flecha cian muestra el orden de source: primero /opt/ros (ya está en .bashrc), después el install/ de tu workspace, en cada terminal nuevo.

Objetivo: crear el paquete meu_robo_description, que guardará la descripción del robot, el mundo de Gazebo y el launch. Es del tipo ament_python, como el del 4.1.

terminal
# herramientas que usa este módulo (desktop ya incluye RViz2 y 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
# después de crear los archivos de los temas 2 y 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 la descripción
        (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='Descripcion e simulacao do robo do curso',
    license='MIT',
)

Cómo verificar: en package.xml generado, agrega una línea <exec_depend> por cada paquete usado: robot_state_publisher, xacro, ros_gz_sim, ros_gz_bridge, rviz2. Después del build, ros2 pkg prefix meu_robo_description muestra la ruta en install/, e ls install/meu_robo_description/share/meu_robo_description/urdf lista el archivo del robot.

✓ Hábitos que ahorran tiempo

  • ✓Compila siempre en la raíz del workspace.
  • ✓Usar --symlink-install: Python y launch cambian sin recompilar.
  • ✓Ejecutar rosdep install al descargar un paquete de terceros.

✗ Errores clásicos

  • ✗Ejecutar colcon build dentro de src/.
  • ✗Olvidar el source install/setup.bash en la nueva terminal.
  • ✗Crear un archivo nuevo (URDF, launch) y no registrarlo en setup.py.

Por qué aprender

La mitad de las preguntas de principiantes en foros de ROS son sobre un workspace sin source, un archivo que no se instaló o una dependencia faltante. Entender las capas convierte estos errores en un arreglo de un minuto.

Conceptos clave

Workspace
src, build, install, log
Superposición
tu capa de arriba
rosdep
instala dependencias
data_files
lo que va a share/
2

📐 URDF/xacro: describir el robot

O URDF (Unified Robot Description Format) describe el robot como piezas rígidas, los links, unidas por joints (articulaciones). Cada link tiene tres partes: visual (cómo se ve), collision (la forma que choca) e inertial (masa e inercia, que el simulador exige). xacro es un preprocesador que agrega variables, cálculos y macros para que escribas la rueda una vez y la uses dos veces.

base_link rueda_izq · continuous rueda_der · continuous ruedita · fixed lidar_link · fixed x (frente)y (izquierda)

Cómo leer: el rectángulo ámbar es el base_link, la pieza principal. Las ruedas (cian) giran sin límite: articulación continuous. La ruedita (línea discontinua) y el LiDAR (círculo ámbar) no se mueven con respecto a la base: articulación fixed. Según la convención de ROS, x apunta hacia adelante e y hacia la izquierda; z sale de la pantalla.

Objetivo: describir un robot diferencial de 25 × 18 cm, con ruedas de 4 cm de radio, ruedita y LiDAR, ya con los plugins de Gazebo (tracción diferencial, estado de las articulaciones y láser). Mide tu robot y cambia las propiedades al inicio del archivo.

meu_robo_description/urdf/robo.urdf.xacro
<?xml version="1.0"?>
<robot name="robo" xmlns:xacro="http://www.ros.org/wiki/xacro">
  <!-- medidas en metros y kg: cámbialas por las de tu robot -->
  <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: una rueda; lado = izq o der, y = posición 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 o Jetty): ruedita sin fricción, tracción, articulaciones y láser -->
  <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>

Cómo verificar

  • ✓xacro robo.urdf.xacro > /tmp/robo.urdf no muestra errores.
  • ✓check_urdf /tmp/robo.urdf imprime el árbol: base_link con 4 hijos (paquete liburdfdom-tools).
  • ✗"Undefined substitution argument"? ¿Faltó declarar algún xacro:property.

Por qué aprender

Un archivo sirve para todo: RViz dibuja el robot con él, Gazebo simula la física, robot_state_publisher calcula la posición de cada pieza y Nav2 usa las medidas para evitar choques. Si la distancia entre las ruedas está mal aquí, más adelante la odometría y el mapa quedarán deformados.

Conceptos clave

Link
pieza rígida
Joint
fixed, continuous, revolute
Inertial
masa que Gazebo exige
xacro:macro
escribe una vez, úsala dos veces
3

🌳 TF2: marcos de coordenadas

El LiDAR dice «hay una pared a 1 m, en el ángulo de 30°», pero 1 m ¿desde dónde? Desde el propio LiDAR. Para ubicar esa pared en el mapa, ROS necesita encadenar: LiDAR → base del robot → punto donde el robot comenzó → mapa. Cada uno de estos es un marco de coordenadas (frame), y la biblioteca TF2 guarda su árbol a lo largo del tiempo para responder en cualquier momento «dónde está A con respecto a B».

map odom base_link roda_esqroda_dirrodiziolidar_link SLAM o AMCL (4.4, 4.5)odometría (Gazebo o 4.3)robot_state_publisher

Cómo leer: cada caja es un frame, cada línea una transformación y cada transformación tiene un único publicador (texto gris). robot_state_publisher publica las líneas ámbar a partir del URDF. La odometría publica odom → base_link. La línea discontinua map → odom solo aparece cuando se activa el SLAM: es la corrección del error acumulado de la odometría.

🌱 ¿Eres nuevo aquí?

odom es el punto donde se encendió el robot: su posición relativa cambia suavemente, pero acumula error (la rueda patina). map es el marco de referencia fijo del mapa: puede dar pequeños saltos cuando el SLAM corrige, pero no deriva. Esta división sigue la convención REP 105, que respeta todo paquete de navegación.

terminal (con la simulación del tema 5 en marcha)
# ¿dónde está el LiDAR con respecto a la base? (traslación y rotación)
ros2 run tf2_ros tf2_echo base_link lidar_link
# genera frames_<data>.pdf con el árbol completo
ros2 run tf2_tools view_frames

Cómo verificar: o tf2_echo imprime "Translation: [0.050, 0.000, 0.050]", exactamente el origin de la articulación del LiDAR en el URDF. El PDF muestra odom → base_link → cuatro hijos.

Por qué aprender

Una TF incorrecta es el defecto más engañoso de la navegación: todo funciona, pero el láser aparece girado, el mapa duplica las paredes y el robot cree que atraviesa el sofá. Quien sabe leer el árbol encuentra el problema en minutos.

Conceptos clave

Frame
marco de coordenadas
map / odom
fijo / suave con deriva
base_link
el centro del robot
tf2_echo
consulta una conexión
4

👁️ RViz2: ver el robot y los sensores

O RViz2 no simula nada: dibuja lo que indican los tópicos. Es la ventana a la mente del robot. Antes de llevar el modelo a Gazebo, revísalo en RViz con dos nodos: el robot_state_publisher, que lee el URDF y publica la TF de las piezas, y el joint_state_publisher_gui, una ventana con controles deslizantes para girar las ruedas manualmente.

Paso a paso: el modelo en RViz2

1

Publica la descripción

Ejecuta los tres comandos del bloque de abajo, cada uno en una terminal con el workspace activado.

2

Ajusta el Fixed Frame

En el panel "Global Options", cambia Fixed Frame a base_link (sin simulador no existe odom).

3

Agrega las visualizaciones

Botón "Add": RobotModel (en Description Topic elige /robot_description) e TF. Guarda con File → Save Config As en ~/ros2_ws/robo.rviz.

4

Mueve las ruedas

Arrastra los controles de la ventana de joint_state_publisher_gui y observa cómo giran las ruedas en RViz.

terminal
cd ~/ros2_ws/src/meu_robo_description/urdf
# terminal 1: lee el URDF y publica la TF de las piezas
ros2 run robot_state_publisher robot_state_publisher --ros-args -p robot_description:="$(xacro robo.urdf.xacro)"
# terminal 2: controles deslizantes para las articulaciones de las ruedas
ros2 run joint_state_publisher_gui joint_state_publisher_gui
# terminal 3
rviz2

Cómo verificar: aparece una caja ámbar con dos ruedas negras, la rueda giratoria atrás y el LiDAR arriba. Los ejes de la TF muestran x (rojo) hacia adelante. ros2 topic echo /joint_states cambia cuando arrastras un control.

✓ Qué revisar en RViz

  • ✓La parte frontal del robot coincide con el eje rojo (x).
  • ✓Las ruedas tocan el suelo a la misma altura que la rueda giratoria.
  • ✓El LiDAR está donde se encuentra en el robot real.

✗ Señales de problemas

  • ✗"No hay transformación desde roda_esq": falta /joint_states.
  • ✗Pantalla vacía: Fixed Frame apunta a un frame que no existe.
  • ✗Ruedas acostadas: el rpy de la articulación es incorrecto.

Por qué aprender

Todo lo demás del recorrido pasa por RViz: el láser en el 4.4, el mapa y la ruta planificada en el 4.5, el clic de "ve hasta allí". Aprender a crear y guardar una configuración ahora te evita tener que rehacerlo todo cada vez.

Conceptos clave

Fixed Frame
marco de referencia de la pantalla
RobotModel
dibuja el URDF
/joint_states
ángulo de cada articulación
.rviz
configuración guardada
5

🌐 Gazebo (Harmonic/Jetty) y ros_gz

O Gazebo simula la física, las colisiones y los sensores. La versión corresponde a la distro: Harmonic con Jazzy y Jetty con Lyrical; el paquete ros-<distro>-ros-gz ya incluye el par correcto. Gazebo tiene su propia red de mensajes; quien conecta las dos es el puente ros_gz_bridge, que traduce cada tópico. El antiguo "Gazebo Classic" dejó de funcionar en enero de 2025: los tutoriales con gazebo_ros y archivos .world antiguos ya no sirven.

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

Cómo leer: la flecha ámbar de arriba es el comando que sale de ROS y entra al simulador (velocidad). La flecha cian de abajo es lo que el simulador "mide" y devuelve a ROS. Solo pasa por el puente lo que incluyas en él: un tópico olvidado en el puente es un tópico que no existe del otro lado.

Objetivo: un mundo con suelo, luz, el sistema de sensores (sin él el LiDAR no publica) y dos cajas para que el láser las detecte.

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: un launch que abre Gazebo con el mundo, publica la descripción, coloca el robot en el mundo, conecta el puente y abre RViz2. En el puente, ] significa ROS → Gazebo y [ significa 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 siguen el reloj de 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: empieza a funcionar de inmediato

    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

Cómo verificar

  • ✓Gazebo se abre con el robot ámbar en el centro y dos cajas; el láser aparece como rayos azules.
  • ✓En RViz, con Fixed Frame odom, agrega RobotModel y LaserScan (/scan): los puntos del láser dibujan el borde de las cajas.
  • ✓ros2 topic hz /scan muestra cerca de 10 Hz.
  • ✗¿Gazebo se cierra justo después de abrir en una máquina virtual? Ejecuta antes export LIBGL_ALWAYS_SOFTWARE=1 (funciona más lento, pero funciona).

Por qué aprender

El mismo launch, con otro puente, se convierte en el del robot real del 4.6. Mapear y navegar primero en la simulación te permite ver si un problema es del algoritmo o del hardware, y te permite estudiar todo el recorrido sin LiDAR.

Conceptos clave

Harmonic / Jetty
Gazebo de cada distro
parameter_bridge
traduce cada tópico
use_sim_time
reloj del simulador
launch
inicia todo de una vez
6

🎮 Conduce el robot simulado con teleop

Con la simulación abierta, falta alguien que publique en /cmd_vel. O teleop_twist_keyboard transforma teclas em mensagens Twist. Es la prueba de humo de toda la ruta: si el robot se mueve, el láser gira al mismo tiempo y la odometría lo sigue, la cadena comando → simulador → sensores → ROS está completa. En el 4.3, este mismo comando moverá al robot real.

terminal (con sim.launch.py en ejecución)
# deja ESTE terminal enfocado mientras conduces
ros2 run teleop_twist_keyboard teleop_twist_keyboard
#   i = adelante   , = atrás   j / l = girar   k = parar   q / z = más / menos velocidad
# en otra terminal, comprueba qué está pasando
ros2 topic echo /cmd_vel
ros2 topic echo /odom --field pose.pose.position

Cómo verificar

  • ✓Con i, el robot avanza en Gazebo y en RViz al mismo tiempo.
  • ✓O echo /cmd_vel muestra linear.x: 0.5; o x de la odometría aumenta.
  • ✓Al acercarte a una caja, los puntos del LaserScan en RViz se quedan quietos en el mundo (es el robot el que se mueve).
  • ✗¿El robot avanza hacia atrás con i? El eje de las juntas de las ruedas está invertido en el URDF.

💡 ¿Twist o TwistStamped?

El teleop publica Twist por defecto, que es lo que espera el puente de arriba. Paquetes más nuevos, como Nav2 del módulo 4.5, pueden publicar TwistStamped (Twist con marca de tiempo). Cuando llegues a ese punto, ajusta el tipo en el puente o en el parámetro de Nav2 para que ambos lados coincidan; si el tipo es distinto en el mismo tópico, simplemente no se conectan.

🌱 ¿Eres nuevo aquí?

Observa que, si conduces en círculo y vuelves al punto de partida, la odometría de Gazebo indica exactamente 0. En el robot real no será así: la rueda patina y el error se acumula. Ese es el error que el SLAM del módulo 4.4 corrige con el láser.

Por qué aprender

Conducir manualmente es como vas a mapear la casa en el 4.4 y probar el robot real en el 4.3. Y, cuando algo salga mal con Nav2, volver al teleop te permite distinguir entre «el robot no obedece» y «el planificador no planifica».

Conceptos clave

teleop
teclado → /cmd_vel
/odom
posición estimada
/scan
lecturas del láser
Prueba de humo
¿la cadena está completa?

✅ Resumen del módulo

✓
Workspace en capas: /opt/ros abajo, tu install/ encima; colcon en la raíz, source en cada terminal.
✓
URDF/xacro describe links y juntas una sola vez para RViz, Gazebo, TF y Nav2.
✓
TF2: map → odom → base_link → sensores, cada conexión con un único publicador.
✓
Gazebo Harmonic (Jazzy) o Jetty (Lyrical) conectado a ROS mediante ros_gz_bridge, tópico por tópico.
✓
teleop_twist_keyboard cierra el ciclo y sirve como prueba de humo.

Para profundizar

Siguiente módulo

4.3 · micro-ROS en ESP32: el robot real se integra a ROS 2, obedece al mismo /cmd_vel y devuelve la odometría.