PTENES
MÓDULO 4.6 · PROYECTO

🏁 Proyecto: robot que mapea y navega

Es hora de reunir todo el recorrido en un robot de verdad: ESP32 con micro-ROS en los motores, Raspberry Pi 5 con ROS 2 al mando y un LiDAR 2D en los ojos. Un solo launch file inicia todo; el robot mapea la casa y después va por sí solo de una habitación a otra, con bloqueos de seguridad.

6
Temas
Avanzado
Nivel
Proyecto
Tipo
4.1 a 4.5
Requisitos previos
0%
0 de 0 temas leídos · Sección 1 de 6

Contenido detallado

1

🏗️ Arquitectura del robot real con ROS 2

El robot del proyecto es el de la Ruta 3 (chasis, motores con encoder, ESP32 y Raspberry Pi 5) con un LiDAR 2D en la parte superior. La división del trabajo sigue la regla del curso: la computadora piensa, el microcontrolador controla. El ESP32 lee los encoders y la IMU, hace girar los motores y corta todo si deja de recibir comandos. La Pi 5 ejecuta ROS 2: el driver del LiDAR, el agente micro-ROS, SLAM y Nav2. Tu PC solo se conecta para visualizar en RViz2, a través de la red.

Raspberry Pi 5 · ROS 2 micro_ros_agent driver del LiDAR → /scan robot_state_publisher (TF) slam_toolbox o AMCL Nav2 → /cmd_vel ESP32micro-ROSpub: /odom, /imusub: /cmd_velmotores + encoders LiDAR 2D PCRViz2 USBUSBWi-Fi batería de los motores separada, GND común, interruptor físico de corte

Cómo leer: el recuadro ámbar es la Pi 5, donde reside toda la «inteligencia» de ROS 2. Las piezas cian de la izquierda son el hardware que se conecta por USB. El PC con línea discontinua es opcional: el robot funciona sin él; solo pierdes la pantalla de RViz2.

🌱 ¿Eres nuevo aquí?

Con dos adaptadores USB-serial, Linux puede llamar al LiDAR /dev/ttyUSB0 hoy y ttyUSB1 mañana. Una regla udev crea un nombre fijo (/dev/lidar, /dev/esp32) a partir del número de serie de cada adaptador. Y ROS_DOMAIN_ID es el «canal» de ROS 2 en la red: la Pi y el PC necesitan el mismo número para verse, y los robots cercanos usan números diferentes.

Objetivo: asignar nombres fijos a los puertos de la Pi 5 y configurar el canal ROS 2 junto con el PC. Los números <idVendor>, <idProduct> e <serial> los lees con el primer comando, un adaptador a la vez.

terminal (en la Pi 5)
# 1) descubre los datos del adaptador conectado ahora
udevadm info -a -n /dev/ttyUSB0 | grep -m3 -E "idVendor|idProduct|serial"

# 2) crea la regla (una línea por dispositivo) y vuelve a cargarla
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) mismo canal ROS 2 en la Pi y en el PC (elige un número del 0 al 101)
echo "export ROS_DOMAIN_ID=42" >> ~/.bashrc && source ~/.bashrc
ls -l /dev/lidar /dev/esp32

Cómo verificar: o ls muestra /dev/lidar e /dev/esp32 apuntando a algún ttyUSB; cambia los cables de puerto USB, reinicia y los nombres seguirán siendo correctos. En la PC, con el mismo ROS_DOMAIN_ID, ros2 topic list muestra los tópicos del robot.

Por qué aprender

Un robot que funciona en la mesa de trabajo y falla «a veces» casi siempre tiene una de estas causas: cambio de puerto, energía del motor que hace caer el Pi o una red que mezcla robots. Una arquitectura clara evita las tres antes de que aparezcan.

Conceptos clave

Pi piensa
ROS 2, SLAM, Nav2
ESP32 actúa
motores y parada
Regla udev
nombre fijo del puerto
ROS_DOMAIN_ID
canal en la red
2

🚀 Launch files que lo ponen todo en marcha

Abrir cinco terminales en cada prueba cansa y genera errores. Un launch file en Python describe todos los nodos, sus parámetros y las inclusiones de otros launch files, y lo pone todo en marcha con un comando. Este tiene un interruptor: slam:=True para mapear, slam:=False para navegar por el mapa guardado. Está en la carpeta launch/ del paquete meu_robo que creaste en el módulo 4.1 (el URDF viene de meu_robo_description, del 4.2).

Objetivo: un launch que pone en marcha la descripción del robot, el agente micro-ROS, el LiDAR y Nav2 (con SLAM o con un mapa). Ajusta el nombre del archivo xacro, la velocidad del puerto serial de tu LiDAR (el C1 y el A1 usan valores diferentes: compruébalo en el launch del driver) y la ruta del mapa.

meu_robo/launch/robo_completo.launch.py
# Pone en marcha todo el robot real: TF, ESP32 (micro-ROS), LiDAR y Nav2 con SLAM o 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')   # paquete del 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 por el mapa guardado'),
        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 fijos (base_link -> laser, base_link -> imu)
        Node(package='robot_state_publisher', executable='robot_state_publisher',
             parameters=[{'robot_description': descricao}]),

        # 2) puente con el ESP32: /ticks y /cmd_vel pasan por aquí (módulo 4.3)
        Node(package='micro_ros_agent', executable='micro_ros_agent',
             arguments=['serial', '--dev', '/dev/esp32', '-b', '115200']),

        # 3) LiDAR (RPLIDAR; para el LD19 cambia el paquete, el ejecutable y los parámetros)
        Node(package='rplidar_ros', executable='rplidar_node',
             parameters=[{'serial_port': '/dev/lidar', 'serial_baudrate': 460800,
                          'frame_id': 'laser'}]),

        # 3b) nodo de odometría del módulo 4.3: /ticks -> /odom y TF odom -> base_link
        Node(package='meu_robo', executable='odometria'),

        # 4) Nav2: con slam:=True también pone en marcha 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()),
    ])
terminal (en la Pi 5)
# en el setup.py de meu_robo (paquete ament_python), agrega esto dentro de data_files:
#   (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')),
#   (os.path.join('share', package_name, 'config'), glob('config/*.yaml')),
# (y "import os" + "from glob import glob" al principio de setup.py)
mkdir -p ~/ros2_ws/src/meu_robo/config
cp ~/meu_nav2.yaml ~/ros2_ws/src/meu_robo/config/nav2_robo.yaml   # del 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

Cómo verificar

  • ✓El LiDAR gira, la terminal muestra al agente micro-ROS creando la sesión con el ESP32 y Nav2 termina con los servidores en estado «active».
  • ✓ros2 topic list muestra /scan, /odom, /map e /cmd_vel.
  • ✗«executable not found» en el LiDAR: abre el launch del driver y copia de allí el nombre del ejecutable y los parámetros.

Por qué aprender

El launch file es la «receta» del robot: quien tome tu proyecto (un colega, un alumno o tú dentro de seis meses) lo pone todo en marcha con una línea, en vez de adivinar el orden de las terminales.

Conceptos clave

Launch file
lo pone todo en marcha
Argumento
slam:=True/False
Include
reutiliza el de Nav2
data_files
instala launch y config
3

🔄 Del simulador al robot real: qué cambia

En Gazebo todo era perfecto: ruedas que no patinan, LiDAR sin ruido, reloj sincronizado. En el robot real, SLAM y Nav2 son los mismos; lo que cambia es quién publica los datos e a su calidad . La mayoría de los problemas al pasar al robot real están en la odometría y el árbol de frames, no en Nav2.

map odom base_link laser imu slam_toolbox o AMCL (corrige la deriva) nodo de odometría (ticks del ESP32, 4.3) robot_state_publisher (URDF, fijo)

Cómo leer: cada flecha es una transformación y el texto a su lado indica quién la publica. Cada flecha necesita exactamente un publicador. El error clásico es que la odometría y otro nodo publiquen odom → base_link al mismo tiempo: el robot «salta» en RViz2.

ElementoSimuladoReal
/scanplugin de Gazebodriver del LiDAR
/odom + TF odomplugin de ruedasnodo de odometría (ticks del ESP32)
/cmd_velmueve el modelomicro-ROS → puente H → motores
use_sim_timeTrueFalse
Errorcasi ceropatinaje, ruido, batería que se descarga

Objetivo: verifica las tres fuentes del robot real antes de mapear. Ejecuta el launch del tema 2 y mantén el robot con las ruedas suspendidas para probar el comando.

terminal
# 1) frecuencias: /scan cerca de la frecuencia del LiDAR, /odom estable
ros2 topic hz /scan
ros2 topic hz /odom

# 2) ¿La odometría avanza junto con el robot? (empújalo 1 m con la mano y compara)
ros2 run tf2_ros tf2_echo odom base_link

# 3) ¿El comando llega a los motores? (ruedas en el aire; Ctrl+C para parar)
ros2 topic pub -r 10 /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1}}"

Cómo verificar

  • ✓Al empujar el robot 1 m en línea recta, el valor x de tf2_echo se acerca a 1,0 (con un error de unos pocos centímetros).
  • ✓Con el pub, las dos ruedas giran hacia adelante; cuando dejas de publicar, se detienen en menos de un segundo (watchdog del ESP32).
  • ✗x da negativo o el doble: el signo del encoder está invertido o el radio de la rueda es incorrecto en el código de odometría.

Por qué aprender

Sim-to-real es una habilidad que volverás a usar en la Trilha 5, con el brazo. La lección es la misma: mantén igual el «cerebro» y valida cada fuente de datos del mundo real, una por una.

Conceptos clave

Un publicador
por transformación
tf2_echo
lee una transformación
Ruedas en el aire
Primera prueba del motor
Sim-to-real
el mismo cerebro
4

🗺️ Mapea el entorno

Con las fuentes validadas, haz un mapa real. Sigue el procedimiento del módulo 4.4, ahora con el robot completo: launch con slam:=True, teleop desde la PC y mapa guardado con el nombre del lugar. Haz un mapa por entorno (casa, aula, laboratorio) y guárdalos todos en ~/mapas.

Paso a paso

1

Batería cargada, área preparada

Una batería baja cambia la velocidad de los motores y empeora la odometría. Bloquea las escaleras, despeja el piso y avisa a las personas.

2

Pon en marcha el robot en modo SLAM

En el Pi (por SSH): ros2 launch meu_robo robo_completo.launch.py slam:=True. En la PC: RViz2 con Fixed Frame = map.

3

Conduce despacio y cierra los ciclos

Teleop desde la PC, velocidad baja, giros lentos, da una vuelta completa en cada habitación y regresa al punto de partida.

4

Guarda y marca el área prohibida

Guarda con map_saver. Después abre casa.pgm en un editor de imágenes y pinta de negro una franja ancha frente a la escalera: para Nav2, eso se convierte en una pared.

Objetivo: los comandos de la sesión de mapeo, un terminal por bloque.

terminal
# Pi 5 (SSH): robot completo en modo de mapeo
ros2 launch meu_robo robo_completo.launch.py slam:=True

# PC: ver y conducir (mismo ROS_DOMAIN_ID)
rviz2 &
ros2 run teleop_twist_keyboard teleop_twist_keyboard

# Pi 5 (otro SSH), con el mapa correcto en pantalla: guardar
mkdir -p ~/mapas && ros2 run nav2_map_server map_saver_cli -f ~/mapas/casa

Cómo verificar: ~/mapas/casa.pgm muestra el plano sin paredes duplicadas; los pasillos rectos quedan rectos. Si una pared aparece «doblada», vuelve a recorrer ese tramo y cierra el circuito antes de guardar.

💡 Área realmente prohibida

Pintar el PGM es la forma sencilla. Nav2 también tiene zonas de exclusión (keepout) con una máscara separada del mapa, lo que permite cambiar el área sin rehacer el mapa; busca «Keepout Zones» en docs.nav2.org. En cualquier caso, la barrera principal cerca de una escalera es física: una reja, una barrera o una puerta cerrada.

Por qué aprender

Es el mismo proceso que sigue un técnico al instalar un robot de limpieza o de reparto en un edificio: mapear, revisar el mapa y marcar dónde no puede ir el robot.

Conceptos clave

slam:=True
modo de mapeo
SSH + RViz2
La Pi trabaja, la PC muestra
Un mapa
por ambiente
Keepout
zona prohibida
5

🚪 Navegar entre habitaciones

Las coordenadas como (3,0; 1,5) no significan nada para quien usa el robot. El paso final es asignar nombres a los lugares: un diccionario que vincula «cocina» con la pose del mapa. Así, «ve a la cocina» se convierte en un comando sencillo, que más adelante puede venir de un botón, un mensaje o tu voz (Ruta 5).

«cocina»solicitud LUGARESbase: 0,0 · 0°cocina: 3,0 1,5 · 90°habitación: 0,5 −1,5 goToPoseNav2 el robot va nombre desconocido → el script lo rechaza y muestra los nombres válidos

Cómo leer: el pedido (cian) solo se convierte en movimiento si el nombre está en la tabla ámbar. Esta «lista cerrada» es la misma idea de seguridad que la Ruta 5 usa con modelos de lenguaje: el pedido elige entre opciones permitidas, nunca inventa coordenadas.

Objetivo: un comando python3 vai_para.py cozinha que lleva al robot a la habitación usando su nombre. Antes, inicia el robot con slam:=False y establece la posición inicial en RViz2. Mide las poses de tu mapa pasando el mouse sobre RViz2.

vai_para.py
#!/usr/bin/env python3
# Uso: python3 vai_para.py cozinha   (solo acepta nombres de la lista LUGARES)
import math
import sys
import rclpy
from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult

LUGARES = {                       # nombre: (x, y, ángulo en grados) en el 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 desconocido. Usa uno de estos:', ', '.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'Yendo a {nome}...')
    nav.goToPose(alvo)
    while not nav.isTaskComplete():
        pass
    ok = nav.getResult() == TaskResult.SUCCEEDED
    print('Cheguei.' if ok else 'No pude llegar.')
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Cómo verificar

  • ✓python3 vai_para.py cozinha imprime «Yendo a cocina...», el robot cruza la puerta y se detiene orientado como definiste; después, «Llegué.»
  • ✓python3 vai_para.py garagem no mueve nada y muestra los nombres válidos.
  • ✗¿Se atasca en la puerta? Mira el costmap en RViz2: si la inflación bloquea el paso, reduce el inflation_radius (módulo 4.5).

Por qué aprender

Aquí es donde el robot deja de ser una demostración técnica y se vuelve útil: llevar un objeto a la cocina, recorrer la sala y volver a la base. Y la lista cerrada de lugares es el primer paso para conectar un «cerebro» de IA de forma segura.

Conceptos clave

Lugares con nombre
nombre → pose
Lista cerrada
rechaza lo demás
slam:=False
navega en el mapa
Puerta
prueba de inflación
6

🛡️ Lista de seguridad y próximos pasos

Un robot que se mueve solo cerca de personas necesita barreras en varias capas, porque cada una falla de una manera diferente. El software puede bloquearse, la red puede caerse y el LiDAR no detecta escalones. La regla es: ninguna capa por sí sola garantiza tu seguridad, y la más fuerte siempre es la física.

una persona supervisando, área preparada Nav2: velocidad baja, zonas prohibidas ESP32: se detiene si no recibe /cmd_vel un interruptor físico corta la energía de los motores

Cómo leer: de afuera hacia adentro, cada capa es más sencilla y confiable. Si Nav2 se equivoca, el watchdog del ESP32 todavía detiene el robot cuando dejan de llegar los comandos; si el ESP32 se bloquea, el interruptor físico corta la energía. Tú eres el marco punteado: durante las primeras semanas, no te alejes.

✓ Antes de cada navegación

  • ✓Interruptor para detener los motores al alcance de la mano y probado.
  • ✓Watchdog de comandos en el ESP32: si no recibe /cmd_vel durante medio segundo, los motores se detienen.
  • ✓Velocidad máxima baja en velocity_smoother; súbela solo después de muchas pruebas.
  • ✓Escalera bloqueada físicamente y marcada como prohibida en el mapa.
  • ✓Batería con BMS, sujeta al chasis y con los cables lejos de las ruedas.

✗ Nunca

  • ✗Dejar que el robot navegue solo con niños, personas mayores o animales cerca.
  • ✗Confiar en el LiDAR 2D para detectar escalones, vidrio u objetos bajos en el suelo.
  • ✗Probar una velocidad nueva cerca de una escalera, una mesa de vidrio o un mueble frágil.
  • ✗Dejar la batería de litio cargándose en el robot sin supervisión.

Desafíos para avanzar

🧹 Sensor de escalones

Dos sensores infrarrojos apuntando al suelo (módulo 1.4) conectados al ESP32: si el suelo desaparece, los motores se detienen antes de recibir cualquier comando de la Pi.

🔋 Regreso a la base

Publica el voltaje de la batería mediante micro-ROS y haz que el script envíe el robot a «base» cuando caiga por debajo de un límite.

📷 Cámara durante el recorrido

Integra la detección de la Ruta 3: en cada waypoint, toma una foto y busca un objeto con YOLO26.

📱 Botones en el celular

Una página sencilla (Ruta 2) con un botón por habitación que use el mismo diccionario LUGARES.

🌱 ¿Eres nuevo aquí?

Un watchdog de comandos es un temporizador en el microcontrolador que se «reinicia» con cada comando recibido. Si se agota el tiempo (se cayó la red, la Pi se bloqueó o se cerró el programa), detiene los motores por su cuenta. Ya viste la idea en el control por Wi-Fi del módulo 2.1; en el robot con ROS 2 es aún más importante, porque hay muchos programas entre tú y la rueda.

Por qué aprender

Un robot móvil es la primera máquina del curso que puede lastimar a alguien sin que la toques. Crear ahora el hábito de aplicar capas de seguridad es lo que te permitirá, en la Trilha 5, poner un brazo robótico y un modelo de IA al mando sin miedo.

Conceptos clave

Capas
ninguna basta por sí sola
Watchdog
sin comando, se detiene
Interruptor físico
el bloqueo final
Supervisión
tú cerca

✅ Resumen del módulo y de la Trilha 4

✓
Arquitectura: ESP32 con micro-ROS en los motores, Pi 5 con ROS 2, LiDAR por USB, puertos con nombre fijo.
✓
Un launch file inicia todo el robot, con slam:=True para mapear y False para navegar.
✓
De la simulación a lo real: mismo SLAM y Nav2; valida /scan, la odometría y TF de una fuente a la vez.
✓
Lugares con nombre: lista cerrada de destinos, el mismo principio de seguridad de la IA en la Trilha 5.
✓
Seguridad en capas: Nav2 lento, watchdog en el ESP32, interruptor físico y tú cerca.

Para profundizar

Siguiente ruta

Trilha 5 · IA moderna y brazo robótico: un modelo de lenguaje elige entre acciones permitidas, voz local, el brazo SO-101 con LeRobot y, en el proyecto final, tu base móvil de esta trilha con un brazo encima.