Contenido detallado
🗂️ 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.
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.
# 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
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
📐 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.
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.
<?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
🌳 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».
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.
# ¿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
👁️ 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
Publica la descripción
Ejecuta los tres comandos del bloque de abajo, cada uno en una terminal con el workspace activado.
Ajusta el Fixed Frame
En el panel "Global Options", cambia Fixed Frame a base_link (sin simulador no existe odom).
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.
Mueve las ruedas
Arrastra los controles de la ventana de joint_state_publisher_gui y observa cómo giran las ruedas en RViz.
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
🌐 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.
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.
<?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.
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])
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
🎮 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.
# 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
✅ Resumen del módulo
Para profundizar
- Tutoriales de URDF (docs.ros.org) — links, juntas y xacro paso a paso
- Acerca de TF2 — conceptos y herramientas
- Documentación de Gazebo — elige Harmonic o Jetty en la parte superior
- Gazebo con ROS (ros_gz) — qué pareja instalar con cada distro
- REP 105 — la convención map, odom y base_link
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.