Logo de JdeRobot

BLOG DE PRÁCTICAS EXTERNAS JDEROBOT
ALBERTO LEÓN LUENGO

Foto de Perfil

¡Bienvenido! En esta web se encuentra todo el contenido visto y aprendido en mis Prácticas Externas realizadas con la empresa JdeRobot durante el curso 2025-2026 en el Grado en Ingeniería de Robótica Software en la Universidad Rey Juan Carlos.

A continuación, se muestran todas las entradas correspondientes a los blogs realizados, cuyo contenido se divide en las 20 semanas que han durado mis Prácticas.

SEMANA 01: 22-09-2025 al 26-09-2025

Instalación de Docker en Ubuntu 24.04

En primer lugar, se abre una terminal por defecto en el directorio /home/username, a través de la cual se irán ejecutando los siguientes comandos:

PASO 1: Acceso al escritorio

cd Escritorio/

PASO 2: Clonación del repositorio de RoboticsAcademy

git clone --recurse-submodules https://github.com/JdeRobot/RoboticsAcademy.git

PASO 3: Acceso al repositorio de RoboticsAcademy

cd RoboticsAcademy/

PASO 4: Ejecución del script de preparación

Este script configurará el entorno de desarrollo. Sin embargo, al intentar ejecutar este script por primera vez, aparecerán varios errores debido a algunas dependencias que están pendientes de instalar, concretamente yarn y docker.

./scripts/develop_academy.sh
./scripts/develop_academy.sh: linea 146: yarn: orden no encontrada
./scripts/develop_academy.sh: linea 147: yarn: orden no encontrada
./scripts/develop_academy.sh: linea 166: docker: orden no encontrada
Cleaning up...
./scripts/develop_academy.sh: linea 28: docker: orden no encontrada

PASO 5: Ejecución de comandos de configuración

Todos los comandos mostrados a continuación se encargarán de actualizar los repositorios de APT, instalar las herramientas necesarias (certificados y curl), crear el directorio para las claves de Docker, descargar la clave GPG de Docker, dar permisos de lectura a dicha clave, añadir el repositorio de Docker a APT y volver a actualizar la lista de paquetes.

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

PASO 6: Instalación de Docker y todos sus componentes

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

PASO 7: Comprobación de la activación de Docker

sudo systemctl status docker

PASO 8: Instalación de Docker Compose

sudo apt install docker-compose

PASO 9: Verificación de la versión de Docker instalada

docker --version

PASO 10: Creación del grupo de usuarios de Docker

sudo groupadd docker

PASO 11: Adición del usuario actual al grupo Docker

sudo usermod -aG docker $USER

PASO 12: Cambio de grupo

newgrp docker

PASO 13: Reejecución del script de preparación

sudo ./scripts/develop_academy.sh

SEMANA 02: 29-09-2025 al 03-10-2025

Configuración y uso de Docker en Visual Studio Code

PASO 1: Apertura de RoboticsAcademy en Visual Studio Code

cd Escritorio/RoboticsAcademy/
code .

PASO 2: Instalación de las extensiones de Docker

Tras instalar ambas extensiones, es necesario reiniciar el equipo para que Docker y Visual Studio Code se integren correctamente.

Container Tools Extension
Project Manager Extension

PASO 3: Bucle de trabajo a seguir en Visual Studio Code

- Hacer los cambios necesarios dentro del directorio RoboticsInfrastructure/.
- Hacer commit y push de dichos cambios en una nueva rama (Publish Branch).
- Acceder al directorio RoboticsAcademy/scripts/RADI/.

cd scripts/RADI/

- Ejecutar el script build.sh dentro de la nueva rama.

./build.sh -i <branch_name>

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

- Volver al directorio principal.

cd ../..

- Cambiar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml.

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

- Ejecutar el script de preparación para lanzar el Docker de RoboticsAcademy con todos los cambios realizados.

./scripts/develop_academy.sh

- Acceder a la dirección web http://0.0.0.0:7164/ que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

SEMANA 03: 06-10-2025 al 10-10-2025

Lanzamiento del RoboticsBackend en local

PASO 1

Para lanzar el RoboticsBackend y poder trabajar en Unibotics en local, se debe ejecutar el siguiente comando en la terminal:

docker run --rm -it --device /dev/dri -p 6080:6080 -p 1108:1108 -p 7163:7163 jderobot/robotics-backend:latest

PASO 2

Se deja ejecutando el comando anterior en la terminal y se accede a la página web de Unibotics.

PASO 3

Una vez dentro de Unibotics, se selecciona el ejercicio en el que se quiera trabajar.

NOTA: Para que todo funcione correctamente, se debe seleccionar la opción 'Local ROS2 (RoboticsBackend 4)', que aparece en la esquina superior derecha al hacer clic en la foto de perfil.

Robotics Backend Option

PASO 4

Verificar que, tanto la simulación en Gazebo como en la terminal se han lanzado correctamente.

Robotics Backend Local

SEMANA 04: 13-10-2025 al 17-10-2025

Lanzamiento del RoboticsDatabase junto a RoboticsAcademy en local

PASO 1

Para poder lanzar RoboticsDatabase junto a RoboticsAcademy, se debe ejecutar el siguiente comando en la terminal:

docker run --hostname my-postgres --name academy_db -d\
-e POSTGRES_DB=academy_db \
-e POSTGRES_USER=user-dev \
-e POSTGRES_PASSWORD=robotics-academy-dev \
-e POSTGRES_PORT=5432 \
-d -p 5432:5432 \
jderobot/robotics-database:latest

PASO 2

Una vez ejecutado el comando anterior, ya no será necesario ejecutarlo más veces, ya que lo que ha hecho este comando es crear un nuevo contenedor para RoboticsDatabase, al cual se llamará con el flag --link cuando se vaya a lanzar RoboticsAcademy. A continuación se muestran 3 formas diferentes de lanzar RoboticsDatabase junto a RoboticsAcademy:

Lanzamiento de RoboticsDatabase + RoboticsAcademy (NVIDIA + GPU)

docker run --rm -it $(nvidia-smi >/dev/null 2>&1 && echo "--gpus all" || echo "") --device /dev/dri -p 6080:6080 -p 6081:6081 -p 1108:1108 -p 7163:7163 -p 7164:7164 --link academy_db jderobot/robotics-academy:latest

NOTA: Para que este comando funcione, es necesario tener instalados los drivers de NVIDIA en Linux.

Lanzamiento de RoboticsDatabase + RoboticsAcademy (GPU)

docker run --rm -it --device /dev/dri -p 6080:6080 -p 6081:6081 -p 1108:1108 -p 7163:7163 -p 7164:7164 --link academy_db jderobot/robotics-academy:latest

Lanzamiento de RoboticsDatabase + RoboticsAcademy (CPU)

docker run --rm -it -p 6080:6080 -p 6081:6081 -p 1108:1108 -p 7163:7163 -p 7164:7164 --link academy_db jderobot/robotics-academy:latest

PASO 3

Para comprobar que todos los Dockers que se van a utilizar se hayan descargado y configurado correctamente, se debe ejecutar el siguiente comando en la terminal:

sudo docker images

Cuyo resultado debe ser el siguiente:

REPOSITORY                   TAG       IMAGE ID       CREATED       SIZE
jderobot/robotics-backend latest 0e2a10ccfaf4 5 days ago 27.2GB
jderobot/robotics-academy latest 1c67a50c79c2 13 days ago 28.5GB
jderobot/robotics-database latest 3a1d0407347e 13 days ago 483MB

SEMANA 05: 20-10-2025 al 24-10-2025

Ejercicio de calentamiento: Cambiar universo

Para ir familiarizándome con el software de RoboticsInfrastructure, se me ha pedido como primer ejercicio cambiar el universo de Gazebo de uno de los ejercicios que ya se haya migrado a Gazebo Harmonic.

En primer lugar, he accedido al fichero RoboticsInfrastructure/database/universes.sql para localizar aquellos universos ya migrados a Gazebo Harmonic.

NOTA: La forma más sencilla de identificar los universos migrados a Gazebo Harmonic, es mirando si en su columna type aparece escrito gazebo o gz. En caso de que aparezca escrito gazebo, significa que ese universo todavía se encuentra en Gazebo 11 y no se ha migrado. Pero si aparece escrito gz, significa que ese universo ya se ha migrado a Gazebo Harmonic.

A continuación se listan todos los universos que tienen escrito gz en su columna type, y que podrían utilizarse para llevar a cabo este ejercicio de calentamiento:

12	Laser Mapping Warehouse	/opt/jderobot/Launchers/laser_mapping.launch.py	{"gzsim":"/opt/jderobot/Launchers/visualization/laser_mapping.config"}	ROS2	gz	{0.0,0.0,0.0,0.0,0.0,0.0}
25 Vacuums House Markers /opt/jderobot/Launchers/marker_visual_loc.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/marker_visual_loc.config"} ROS2 gz {1,-1.5,0.6,0,0,0}
31 Rescue People Harmonic /opt/jderobot/Launchers/rescue_people.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/rescue_people.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
32 Follow Road Harmonic /opt/jderobot/Launchers/follow_road.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_road.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
33 Small Laser Mapping Warehouse /opt/jderobot/Launchers/small_laser_mapping.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/small_laser_mapping.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
34 Pick And Place Arm /home/dev_ws/src/IndustrialRobots/ros2_SimRealRobotControl/ros2srrc_launch/moveit2/moveit2.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
35 Car Junction /opt/jderobot/Launchers/car_junction.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/car_junction.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
36 Drone Gymkhana Harmonic /opt/jderobot/Launchers/drone_gymkhana.launch.py None ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
37 Tower Inspection Harmonic /opt/jderobot/Launchers/power_tower_inspection.launch.py None ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

En mi caso, he seleccionado los universos correspondientes a los ejercicios de Laser Mapping (número 12) y Marker Based Visual Loc (número 25). El universo del Laser Mapping es un almacén tipo Amazon, mientras que el universo del Marker Based Visual Loc es una casa de dos plantas.

El resultado final de este ejercicio visualizará el almacén del ejercicio Laser Mapping en el visor de Gazebo del ejercicio Marker Based Visual Loc.

A continuación se muestra cómo se ve inicialmente el ejercicio Marker Based Visual Loc al lanzar el Docker de RoboticsAcademy antes de realizar cualquier cambio en el código:

Original World

El único cambio que he realizado ha sido en el fichero RoboticsInfrastructure/Launchers/marker_visual_loc.launch.py en la siguiente línea:

# ANTES (UNIVERSO ORIGINAL)
world_file_name = "marker_visual_loc.world"

# DESPUÉS (UNIVERSO NUEVO)
world_file_name = "laser_mapping.world"

Una vez realizado este cambio, he hecho commit y push en una nueva rama que he creado y publicado llamada test-aleon2020. Es importante hacer esto siempre para evitar así cualquier tipo de conflicto con la rama principal.

Como los cambios se han realizado en un fichero que se encuentra dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i test-aleon2020

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

NOTA: Si es la primera vez que se ejecuta este script, el tiempo que tardará en ejecutarse por completo será considerablemente largo, ya que debe configurar todo el entorno. Si es la primera vez que se ejecuta tardará unos 35-45 minutos, de lo contrario, tardará unos 2-3 minutos.

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Marker Based Visual Loc al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados en el código:

New World

SEMANA 06: 27-10-2025 al 31-10-2025

Migración del ejercicio Obstacle Avoidance a Gazebo Harmonic

Una vez realizado este primer ejercicio de calentamiento para irme familiarizando con el código de RoboticsInfrastructure, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic del ejercicio Obstacle Avoidance, que introduce de forma práctica la navegación local mediante el uso de campos de fuerza virtuales (VFF, Virtual Force Fields).

Obstacle Avoidance Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/f1/models/f1_result_laser_no_cam/model.sdf

En este fichero se han modificado las partes correspondientes a las etiquetas <plugin> y <sensor>, donde el plugin pasa de estar declarado dentro del sensor a integrarse en él. Además, es importante añadir al final de la nueva versión los plugins correspondientes al velocity-control-system y al odometry-publisher-system:

<?xml version='1.0'?>
<sdf version="1.9">
<model name="f1_renault">
<pose>0 0 0 0 0 0</pose>
<static>false</static>

<link name="f1">
<pose>0 0 0 0 0 0</pose>
<inertial>
<mass>10</mass>
<inertia>
<ixx>1</ixx>
<ixy>0.0</ixy>
<iyy>1</iyy>
<ixz>0.0</ixz>
<iyz>0.0</iyz>
<izz>1.0</izz>
</inertia>
</inertial>
<collision name="collision">
<geometry>
<mesh>
<uri>model://f1_renault_laser_no_cam/Renault/Car.obj</uri>
<scale>0.2 0.2 0.2</scale>
</mesh>
</geometry>
</collision>
<visual name="visual">
<geometry>
<mesh>
<uri>model://f1_renault_laser_no_cam/Renault/Car.obj</uri>
<scale>0.2 0.2 0.2</scale>
</mesh>
</geometry>
</visual>
</link>

<link name='laser_body'>
<pose>0.500000 0.000000 0.072000 0.000000 0.000000 0.00000</pose>
<visual name="visual_laser">
<geometry>
<mesh>
<uri>model://f1_renault_laser_no_cam/meshes/hokuyo.dae</uri>
</mesh>
</geometry>
</visual>

<sensor name="laser" type="gpu_lidar">
<always_on>1</always_on>
<visualize>1</visualize>
<pose>0.500000 0.000000 0.072000 0.000000 0.000000 0.00000</pose>
<update_rate>20.000000</update_rate>
<topic>f1/laser/scan</topic>
<frame>base_scan</frame>
<lidar>
<scan>
<horizontal>
<samples>180</samples>
<resolution>1.000000</resolution>
<min_angle>-1.570000</min_angle>
<max_angle>1.570000</max_angle>
</horizontal>
</scan>
<range>
<min>0.080000</min>
<max>10.000000</max>
<resolution>0.010000</resolution>
</range>
</lidar>
</sensor>
</link>

<!-- <velocity_decay>
<linear>0.000000</linear>
<angular>0.000000</angular>
</velocity_decay>
<self_collide>0</self_collide>
<kinematic>0</kinematic>
</link> -->

<joint type="fixed" name="laser_fix">
<pose>0 0 0 0 0 0</pose>
<child>laser_body</child>
<parent>f1</parent>
<!-- <axis>
<xyz>0 0 0</xyz>
</axis> -->
</joint>

<plugin filename="gz-sim-velocity-control-system" name="gz::sim::systems::VelocityControl">
<topic>/F1ROS/cmd_vel</topic>
</plugin>

<plugin filename="gz-sim-odometry-publisher-system" name="gz::sim::systems::OdometryPublisher">
<odom_frame>odom</odom_frame>
<robot_base_frame>xf1</robot_base_frame>
<odom_publish_frequency>20.0</odom_publish_frequency>
<odom_topic>F1ROS/odom</odom_topic>
<dimensions>3</dimensions>
</plugin>

</model>
</sdf>

FICHERO RoboticsInfrastructure/CustomRobots/f1/params/f1_result_laser_no_cam.yaml

Este fichero se crea de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes al publicador y al subscriptor del plugin del DiffDrive (F1ROS/odom y /F1ROS/cmd_vel) y aquellos relativos al publicador del plugin del láser (f1/laser/scan):

# gz topic published by DiffDrive plugin
- ros_topic_name: "odom"
gz_topic_name: "F1ROS/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS

# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "cmd_vel"
gz_topic_name: "/F1ROS/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "f1/laser/scan"
gz_topic_name: "f1/laser/scan"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

La única modificación realizada en este fichero es la adición de la línea f1/params para que el fichero que se acaba de crear (f1_result_laser_no_cam.yaml) se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# F1
f1/models
f1/launch
f1/worlds
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# F1
f1/models
f1/launch
f1/worlds
f1/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/obstacle_avoidance/spawn_robot.launch.py

Una vez creado el directorio obstacle_avoidance/, y a su vez dentro de él el fichero spawn_robot.launch.py, lo único que habría que hacer es coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a las variables start_gazebo_ros_image_bridge_cmd y start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a la variable bridge_params (en este caso, f1_renault_laser_no_cam.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "f1_renault_laser_no_cam"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "f1_renault_laser_no_cam.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

# start_gazebo_ros_image_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/image_raw"],
# output="screen",
# )

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
# ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/simple_circuit_obstacles_followingcam.launch.py

Para este fichero, habría que hacer un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

robot_launch_dir = "/opt/jderobot/Launchers/obstacle_avoidance"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")
world_file_name = "simple_circuit_obstacles_followingcam.world"
worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro del directorio obstacle_avoidance/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/simple_circuit_obstacles_followingcam.world

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

<?xml version="1.0" ?>

<sdf version="1.9">
<world name="default">

<plugin
filename="gz-sim-physics-system"
name="gz::sim::systems::Physics">
</plugin>
<plugin
filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster">
</plugin>
<plugin
filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands">
</plugin>
<plugin
filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>

<scene>
<shadows>false</shadows>
</scene>

<light type="directional" name="sun">
<!-- <cast_shadows>true</cast_shadows> -->
<pose>0 0 20 -1.3 0 0.5</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.01 0.01 0.01 1</specular>
<intensity>2</intensity>
<visualize>false</visualize>
</light>
<light type="point" name="point_light">
<pose>0.73 0.09 8.77 0 0 0</pose>
<diffuse>1 1 1 1</diffuse>
<specular>.01 .01 .01 1</specular>
<attenuation>
<range>20</range>
<linear>0.2</linear>
<constant>0.8</constant>
<quadratic>0.01</quadratic>
</attenuation>
<!-- <cast_shadows>true</cast_shadows> -->
<visualize>false</visualize>
</light>
<light type="point" name="point_light_01">
<pose>3.482 -4.28 8.87 0 0 0</pose>
<diffuse>1 1 1 1</diffuse>
<specular>0.01 0.01 0.01 1</specular>
<attenuation>
<range>10</range>
<linear>0.5</linear>
<constant>0.8</constant>
<quadratic>0.001</quadratic>
</attenuation>
<cast_shadows>false</cast_shadows>
<visualize>false</visualize>
</light>
<light type="point" name="point_light_02">
<pose>0.13 0.46 11.60 0 0 0</pose>
<diffuse>1 1 1 1</diffuse>
<specular>0.01 0.01 0.01 1</specular>
<attenuation>
<range>10</range>
<linear>0.2</linear>
<constant>0.5</constant>
<quadratic>0.001</quadratic>
</attenuation>
<cast_shadows>false</cast_shadows>
<visualize>false</visualize>
</light>
<light type="point" name="point_light_03">
<pose>4.27 -1.27 11.21 0 0 0</pose>
<diffuse>1 1 1 1</diffuse>
<specular>0.01 0.01 0.01 1</specular>
<attenuation>
<range>10</range>
<linear>0.5</linear>
<constant>0.8</constant>
<quadratic>0.001</quadratic>
</attenuation>
<cast_shadows>false</cast_shadows>
<visualize>false</visualize>
</light>
<light type="point" name="point_light_04">
<pose>-0.31 -3.78 8.61 0 0 0</pose>
<diffuse>1 1 1 1</diffuse>
<specular>.01 .01 .01 1</specular>
<attenuation>
<range>10</range>
<linear>0.5</linear>
<constant>0.8</constant>
<quadratic>0.001</quadratic>
</attenuation>
<cast_shadows>false</cast_shadows>
<visualize>false</visualize>
</light>

<model name='ground_plane'>
<static>true</static>
<link name='link'>
<collision name='collision'>
<geometry>
<plane>
<normal>0.0 0.0 1</normal>
<size>1000 1000</size>
</plane>
</geometry>
</collision>
</link>
<pose>0 0 0 0 0 0</pose>
</model>

<include>
<uri>model://simple_circuit</uri>
<pose>-53.46 11.50 0 0 0 0</pose>
</include>
<include>
<uri>model://f1_renault_laser_no_cam</uri>
<pose>0.04 0.68 0 0 0 -1.57</pose>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_1</name>
<pose>-0.1 -33 0 0 0 0</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_2</name>
<pose>-7 -40 0 0 0 -1.67</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_3</name>
<pose>-13.2 -18.8 0 0 0 -3</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_4</name>
<pose>-40 -11.5 0 0 0 -1.57</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_5</name>
<pose>-66.83 -34.15 0 0 0 -1.03</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_6</name>
<pose>-70.8 -34.2 0 0 0 -1.33</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_7</name>
<pose>-103 14.6 0 0 0 2.47</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_8</name>
<pose>-52.76 42.3 0 0 0 2.15</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_9</name>
<pose>-33.4 57.3 0 0 0 0</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_10</name>
<pose>-3.5 59 0 0 0 0.785</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_11</name>
<pose>1 26.2 0 0 0 0</pose>
<static>true</static>
</include>
<include>
<uri>model://f1_dummy_harmonic</uri>
<name>f1_dummy_12</name>
<pose>-1 24.5 0 0 0 0</pose>
<static>true</static>
</include>

</world>
</sdf>

FICHERO RoboticsInfrastructure/Launchers/visualization/obstacle_avoidance.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Dark" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#f3f3f3" toolbar_text_color_light="#111111" toolbar_color_dark="#414141" toolbar_text_color_dark="#f3f3f3" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>1 1 1</ambient_light>
<background_color>0.2 0.2 0.2</background_color>
<camera_pose>0 3 2 0 0.5 -1.57</camera_pose>
</plugin>

<plugin filename="CameraTracking" name="Camera tracking">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
<follow_target>f1_renault</follow_target>
<follow_offset>0 0 1</follow_offset>
<follow_pgain>0.1</follow_pgain>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin>

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Obstacle Avoidance) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
19 Obstacle Avoidance Default /opt/jderobot/Launchers/simple_circuit_obstacles_followingcam.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
19 Obstacle Avoidance Default /opt/jderobot/Launchers/simple_circuit_obstacles_followingcam.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/obstacle_avoidance.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada obstacle-avoidance-harmonic. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i obstacle-avoidance-harmonic

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Obstacle Avoidance al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

Obstacle Avoidance Image

Además, para verificar que tanto el circuito como el coche han sido migrados correctamente, se muestra una pequeña animación del coche moviéndose en línea recta para verificar que todo el proceso se ha realizado correctamente:

SEMANA 07: 03-11-2025 al 07-11-2025

Migración del ejercicio Global Navigation a Gazebo Harmonic

Con el ejercicio de Obstacle Avoidance ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un segundo ejercicio, en este caso, Global Navigation, que introduce de forma práctica la navegación global mediante el uso y la implementación de la lógica del algoritmo de planificación de ruta del gradiente (GPP, Gradient Path Planning).

Global Navigation Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/taxi_navigator/models/taxi_holo_ROS_harmonic/model.sdf

En este caso, no ha sido necesario llevar a cabo ninguna modificación en este fichero, ya que el coche utilizado para esta versión ya se encuentra migrado a Gazebo Harmonic:

<?xml version="1.0"?>
<sdf version="1.9">
<model name="taxi_holo_ROS_harmonic">
<pose>0 0 0 0 0 0</pose>
<static>false</static>

<!-- Chassis link -->
<link name="taxi_holo">
<inertial>
<mass>750.0</mass>
<inertia>
<ixx>1</ixx><ixy>0.0</ixy><ixz>0.0</ixz>
<iyy>1</iyy><iyz>0.0</iyz>
<izz>1</izz>
</inertia>
</inertial>

<!-- collision & visual both use the same mesh -->
<collision name="collision">
<geometry>
<mesh>
<uri>model://taxi_holo_ROS_harmonic/meshes/taxi_holo.obj</uri>
</mesh>
</geometry>
</collision>
<visual name="visual">
<geometry>
<mesh>
<uri>model://taxi_holo_ROS_harmonic/meshes/taxi_holo.obj</uri>
</mesh>
</geometry>
</visual>
</link>

<!-- VelocityControl plugin: applies Twist directly to the chassis link -->
<plugin
filename="gz-sim-velocity-control-system"
name="gz::sim::systems::VelocityControl">

<topic>/taxi_holo/cmd_vel</topic>

</plugin>

<plugin
filename="gz-sim-odometry-publisher-system"
name="gz::sim::systems::OdometryPublisher">
<odom_topic>/taxi_holo/odom</odom_topic>
<robot_base_frame>taxi_holo</robot_base_frame>
<odom_frame>odom</odom_frame>
<odom_publish_frequency>20</odom_publish_frequency>
<dimensions>3</dimensions>
</plugin>

</model>
</sdf>

FICHERO RoboticsInfrastructure/CustomRobots/taxi_navigator/params/taxi_holo_ROS_harmonic.yaml

Este fichero se crea de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes a los plugins publicador y subscriptor del DiffDrive (/taxi_holo/odom y /taxi_holo/cmd_vel):

# gz topic published by DiffDrive plugin
- ros_topic_name: "odom"
gz_topic_name: "/taxi_holo/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS


# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "cmd_vel"
gz_topic_name: "/taxi_holo/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

La única modificación realizada en este fichero es la adición de las líneas taxi_navigator/launch, taxi_navigator/params y taxi_navigator/worlds, para que el fichero que se acaba de crear (taxi_holo_ROS_harmonic.yaml), se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# GLOBAL_NAVIGATION
taxi_navigator/models
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# GLOBAL_NAVIGATION
taxi_navigator/launch
taxi_navigator/models
taxi_navigator/worlds
taxi_navigator/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/global_navigation/spawn_robot.launch.py

Una vez creado el directorio global_navigation/, y a su vez dentro de él el fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a las variables start_gazebo_ros_image_bridge_cmd y start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a la variable bridge_params (en este caso, taxi_holo_ROS_harmonic.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "taxi_holo_ROS_harmonic"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "taxi_holo_ROS_harmonic.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

# start_gazebo_ros_image_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/image_raw"],
# output="screen",
# )

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
# ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/taxi_navigator.launch.py

Para este fichero, habría que hacer un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

robot_launch_dir = "/opt/jderobot/Launchers/global_navigation"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")
world_file_name = "taxi_navigation_city_large.world"
worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro del directorio global_navigation/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/taxi_navigation_city_large.world

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

<?xml version="1.0"?>

<sdf version="1.9">
<world name="default">
<plugin
filename="gz-sim-physics-system"
name="gz::sim::systems::Physics">
</plugin>
<plugin
filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster">
</plugin>
<plugin
filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands">
</plugin>
<plugin
filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>

<scene>
<background>0.008 0.008 0.012</background>
<shadows>true</shadows>
<grid>false</grid>
</scene>

<light type="directional" name="sun">
<cast_shadows>0</cast_shadows>
<pose>0 0 100 0 0 0</pose>
<diffuse>1 1 1 1</diffuse>
<specular>1 1 1 1</specular>
<direction>0 0 -1</direction>
<visualize>false</visualize>
</light>

<!-- Black Ground Plane -->
<model name="ground_plane">
<static>true</static>
<link name="link">
<collision name="collision">
<geometry>
<plane>
<normal>0 0 1</normal>
<size>500 500</size>
</plane>
</geometry>
</collision>
<visual name="visual">
<geometry>
<plane>
<normal>0 0 1</normal>
<size>500 500</size>
</plane>
</geometry>
<material>
<ambient>0 0 0 1</ambient>
<diffuse>0 0 0 1</diffuse>
</material>
</visual>
</link>
</model>
<!-- My city -->
<include>
<pose>0 0 -0.5 0 0 0</pose>
<uri>model://cityLarge_harmonic</uri>
</include>
<!-- My robots -->
<include>
<pose>0 0 0.1 1.5529944 0 -1.5529944</pose>
<uri>model://taxi_holo_ROS_harmonic</uri>
</include>

</world>
</sdf>

FICHERO RoboticsInfrastructure/Launchers/visualization/global_nav.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Dark" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#ffa726" toolbar_text_color_light="#000000" toolbar_color_dark="#ffa726" toolbar_text_color_dark="#000000" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>

<scene>scene</scene>

<ambient_light>1 1 1</ambient_light>
<background_color>0 0 0</background_color>
<camera_pose>0 0 80 3.14 1.5529944 0</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<!-- <plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin> -->

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Global Navigation) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
8 City Large /opt/jderobot/Launchers/taxi_navigator.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
8 City Large /opt/jderobot/Launchers/taxi_navigator.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/global_nav.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada global-navigation-migration. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i global-navigation-migration

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Global Navigation al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

Global Navigation Image

Además, para verificar que tanto la ciudad como el taxi han sido migrados correctamente, se muestra una pequeña animación del taxi moviéndose en línea recta para verificar que todo el proceso se ha realizado correctamente:

SEMANA 08: 10-11-2025 al 14-11-2025

Migración del ejercicio Autoparking a Gazebo Harmonic

Con el ejercicio de Global Navigation ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un tercer ejercicio, en este caso, Autoparking, que consiste en la implementación de la lógica de un algoritmo de navegación en un vehículo autónomo que se encuentra buscando aparcamiento.

Autoparking Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/autopark_harmonic/models/prius_autoparking_3laser_harmonic/model.sdf

En este caso, no ha sido necesario llevar a cabo ninguna modificación en este fichero, ya que el coche utilizado para esta versión ya se encuentra migrado a Gazebo Harmonic.

NOTA: Dada la gran extensión en cuanto a líneas se refiere de este fichero, adjunto a continuación un enlace a dicho fichero en el que se puede visualizar todo su contenido.

ENLACE: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana08/model.sdf

FICHERO RoboticsInfrastructure/CustomRobots/autopark_harmonic/params/prius_autoparking_3laser_harmonic.yaml

Estos ficheros se crean de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes al plugin publicador y subscriptor del DiffDrive (/prius_autoparking/odom y /prius_autoparking/cmd_vel), y los plugin publicadores de los 3 sensores láser LIDAR (/prius_autoparking/scan_front, /prius_autoparking/scan_side y /prius_autoparking/scan_back).

# gz topic published by DiffDrive plugin
- ros_topic_name: "/prius_autoparking/odom"
gz_topic_name: "/prius_autoparking/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS

# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "/prius_autoparking/cmd_vel"
gz_topic_name: "/prius_autoparking/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/prius_autoparking/scan_front"
gz_topic_name: "/prius_autoparking/scan_front"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/prius_autoparking/scan_side"
gz_topic_name: "/prius_autoparking/scan_side"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/prius_autoparking/scan_back"
gz_topic_name: "/prius_autoparking/scan_back"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de las líneas autopark_harmonic/launch, autopark_harmonic/models, autopark_harmonic/params y autopark_harmonic/worlds para que el fichero que se acaba de crear (prius_autoparking_3laser_harmonic.yaml), se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# AUTOPARK_HARMONIC
autopark_harmonic/launch
autopark_harmonic/models
autopark_harmonic/params
autopark_harmonic/worlds
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/autopark_line/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/autopark_battery/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/autopark_sideways/spawn_robot.launch.py

Una vez creados los directorios autopark_line/, autopark_battery/ y autopark_sideways/, y a su vez dentro de cada uno de ellos el mismo fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a las variables start_gazebo_ros_image_bridge_cmd y start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a bridge_params (en este caso, prius_autoparking_3laser_harmonic.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "prius_autoparking_3laser_harmonic"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "prius_autoparking_3laser_harmonic.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

# start_gazebo_ros_image_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/image_raw"],
# output="screen",
# )

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
# ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/autopark_line.launch.py
FICHERO RoboticsInfrastructure/Launchers/autopark_battery.launch.py
FICHERO RoboticsInfrastructure/Launchers/autopark_sideways.launch.py

Para estos ficheros, habría que hacer un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentra el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

# FICHERO RoboticsInfrastructure/Launchers/autopark_line.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/autopark_line"

# FICHERO RoboticsInfrastructure/Launchers/autopark_battery.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/autopark_battery"

# FICHERO RoboticsInfrastructure/Launchers/autopark_sideways.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/autopark_sideways"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")

# FICHERO RoboticsInfrastructure/Launchers/autopark_line.launch.py
world_file_name = "autopark_line.world"

# FICHERO RoboticsInfrastructure/Launchers/autopark_battery.launch.py
world_file_name = "autopark_battery.world"

# FICHERO RoboticsInfrastructure/Launchers/autopark_sideways.launch.py
world_file_name = "autopark_sideways.world"

worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se hayan creado dentro de los directorios autopark_line/, autopark_battery/ y autopark_sideways/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/autopark_line.world
FICHERO RoboticsInfrastructure/Worlds/autopark_battery.world
FICHERO RoboticsInfrastructure/Worlds/autopark_sideways.world

En estos ficheros, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

NOTA: Dada la gran extensión en cuanto a líneas se refiere de este fichero, adjunto a continuación un enlace a dicho fichero en el que se puede visualizar todo su contenido.

ENLACE: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana08/autopark.world

FICHERO RoboticsInfrastructure/Launchers/visualization/autoparking.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Light" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#ffa726" toolbar_text_color_light="#000000" toolbar_color_dark="#ffa726" toolbar_text_color_dark="#000000" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="true">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>0.4 0.4 0.4</ambient_light>
<background_color>0.8 0.8 0.8</background_color>
<camera_pose>-17 0 10 0 0.7138 0.09599</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<!-- <plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin> -->

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Autoparking) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
39 Autopark_line /opt/jderobot/Launchers/autopark_line.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
40 Autopark_battery /opt/jderobot/Launchers/autopark_battery.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
41 Autopark_sideways /opt/jderobot/Launchers/autopark_sideways.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
39 Autopark_line /opt/jderobot/Launchers/autopark_line.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/autoparking.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
40 Autopark_battery /opt/jderobot/Launchers/autopark_battery.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/autoparking.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
41 Autopark_sideways /opt/jderobot/Launchers/autopark_sideways.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/autoparking.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada autopark_harmonic. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i autopark_harmonic

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Autoparking al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

IMAGEN UNIBOTICS GAZEBO AUTOPARK_LINE

Autoparking Line Image

IMAGEN UNIBOTICS GAZEBO AUTOPARK_BATTERY

Autoparking Battery Image

IMAGEN UNIBOTICS GAZEBO AUTOPARK_SIDEWAYS

Autoparking Sideways Image

Además, para verificar que tanto los diferentes parkings como el coche han sido migrados correctamente, se muestra una pequeña animación del coche moviéndose en línea recta para verificar que todo el proceso se ha realizado correctamente:

VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO AUTOPARK_LINE


VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO AUTOPARK_BATTERY


VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO AUTOPARK_SIDEWAYS

SEMANA 09: 17-11-2025 al 21-11-2025

Migración del ejercicio Follow Line a Gazebo Harmonic

Con el ejercicio de Autoparking ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un cuarto ejercicio, en este caso, Follow Line, que consiste en la implementación de un controlador PID reactivo que sea capaz de seguir la línea roja pintada en el suelo del circuito de carreras.

Follow Line Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/f1/models/f1_renault/model.sdf
FICHERO RoboticsInfrastructure/CustomRobots/ackermann_cars/models/f1_renault_camera/model.sdf

En este caso, no ha sido necesario llevar a cabo ninguna modificación en ninguno de estos ficheros, ya que los dos coches utilizados para esta versión (tanto el coche holonómico como el coche Ackermann) ya se encuentran migrados a Gazebo Harmonic.

NOTA: Dada la gran extensión en cuanto a líneas se refiere de estos ficheros, adjunto a continuación un enlace a cada uno de estos ficheros en el que se puede visualizar todo su contenido.

ENLACE MODELO COCHE HOLONÓMICO: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana09/model_holonomic.sdf

ENLACE MODELO COCHE ACKERMANN: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana09/model_ackermann.sdf

FICHERO RoboticsInfrastructure/CustomRobots/f1/params/f1_renault.yaml
FICHERO RoboticsInfrastructure/CustomRobots/ackermann_cars/params/f1_renault_camera.yaml

Estos ficheros se crean de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia.

En el caso de ambos ficheros (fichero f1_renault.yaml y f1_renault_camera.yaml), se añaden los topics correspondientes a los plugins publicadores y subscriptores del DiffDrive (F1ROS/odom y /F1ROS/cmd_vel) y de la cámara (/cam_f1_left/camera_info).

# gz topic published by DiffDrive plugin
- ros_topic_name: "odom"
gz_topic_name: "F1ROS/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS

# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "cmd_vel"
gz_topic_name: "/F1ROS/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

# gz topic published by Sensors plugin (Camera)
- ros_topic_name: "/cam_f1_left/camera_info"
gz_topic_name: "/cam_f1_left/camera_info"
ros_type_name: "sensor_msgs/msg/CameraInfo"
gz_type_name: "gz.msgs.CameraInfo"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de las línea f1/params y ackermann_cars/params para que los ficheros que se acaban de crear (f1_result_laser_no_cam.yaml y f1_renault_camera.yaml) se tengan en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# F1
f1/models
f1/launch
f1/worlds
...
# ACKERMAN CAR
ackermann_cars/models
ackermann_cars/launch
ackermann_cars/worlds
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# F1
f1/models
f1/launch
f1/worlds
f1/params
...
# ACKERMAN CAR
ackermann_cars/models
ackermann_cars/launch
ackermann_cars/worlds
ackermann_cars/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/monaco_circuit/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/montreal_circuit/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/simple_circuit/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/monaco_circuit_ackermann/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit_ackermann/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/montreal_circuit_ackermann/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit_ackermann/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/simple_circuit_ackermann/spawn_robot.launch.py

Una vez creados los directorios monaco_circuit/, montmelo_circuit/, montreal_circuit/, nurburgring_circuit/, simple_circuit/, monaco_circuit_ackermann/, montmelo_circuit_ackermann/, montreal_circuit_ackermann/, nurburgring_circuit_ackermann/ y simple_circuit_ackermann/, y a su vez dentro de cada uno de ellos el fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a la variable start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a bridge_params (en este caso, f1_renault.yaml y f1_renault_camera.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file

# FICHERO RoboticsInfrastructure/Launchers/monaco_circuit/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/montreal_circuit/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/simple_circuit/spawn_robot.launch.py
model_folder = "f1_renault"

# FICHERO RoboticsInfrastructure/Launchers/monaco_circuit_ackermann/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit_ackermann/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/montreal_circuit_ackermann/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit_ackermann/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/simple_circuit_ackermann/spawn_robot.launch.py
model_folder = "f1_renault_camera"

urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(

# FICHERO RoboticsInfrastructure/Launchers/monaco_circuit/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/montreal_circuit/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/simple_circuit/spawn_robot.launch.py
get_package_share_directory("custom_robots"), "params", "f1_renault.yaml"

# FICHERO RoboticsInfrastructure/Launchers/monaco_circuit_ackermann/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit_ackermann/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/montreal_circuit_ackermann/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit_ackermann/spawn_robot.launch.py
# FICHERO RoboticsInfrastructure/Launchers/simple_circuit_ackermann/spawn_robot.launch.py
get_package_share_directory("custom_robots"), "params", "f1_renault_camera.yaml"

)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

start_gazebo_ros_image_bridge_cmd = Node(
package="ros_gz_image",
executable="image_bridge",
arguments=["/cam_f1_left/image_raw"],
output="screen",
)

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/monaco_circuit.launch.py
FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit.launch.py
FICHERO RoboticsInfrastructure/Launchers/montreal_circuit.launch.py
FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit.launch.py
FICHERO RoboticsInfrastructure/Launchers/simple_circuit.launch.py
FICHERO RoboticsInfrastructure/Launchers/monaco_circuit_ackermann.launch.py
FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit_ackermann.launch.py
FICHERO RoboticsInfrastructure/Launchers/montreal_circuit_ackermann.launch.py
FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit_ackermann.launch.py
FICHERO RoboticsInfrastructure/Launchers/simple_circuit_ackermann.launch.py

Para estos ficheros, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

# FICHERO RoboticsInfrastructure/Launchers/monaco_circuit.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/monaco_circuit"

# FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/montmelo_circuit"

# FICHERO RoboticsInfrastructure/Launchers/montreal_circuit.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/montreal_circuit"

# FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/nurburgring_circuit"

# FICHERO RoboticsInfrastructure/Launchers/simple_circuit.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/simple_circuit"

# FICHERO RoboticsInfrastructure/Launchers/monaco_circuit_ackermann.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/monaco_circuit_ackermann"

# FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit_ackermann.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/montmelo_circuit_ackermann"

# FICHERO RoboticsInfrastructure/Launchers/montreal_circuit_ackermann.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/montreal_circuit_ackermann"

# FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit_ackermann.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/nurburgring_circuit_ackermann"

# FICHERO RoboticsInfrastructure/Launchers/simple_circuit_ackermann.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/simple_circuit_ackermann"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")

# FICHERO RoboticsInfrastructure/Launchers/monaco_circuit.launch.py
world_file_name = "monaco_line.world"

# FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit.launch.py
world_file_name = "montmelo_line.world"

# FICHERO RoboticsInfrastructure/Launchers/montreal_circuit.launch.py
world_file_name = "montreal_line.world"

# FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit.launch.py
world_file_name = "nurburgring_line.world"

# FICHERO RoboticsInfrastructure/Launchers/simple_circuit.launch.py
world_file_name = "simple_circuit.world"

# FICHERO RoboticsInfrastructure/Launchers/monaco_circuit_ackermann.launch.py
world_file_name = "monaco_line_ackermann.world"

# FICHERO RoboticsInfrastructure/Launchers/montmelo_circuit_ackermann.launch.py
world_file_name = "montmelo_line_ackermann.world"

# FICHERO RoboticsInfrastructure/Launchers/montreal_circuit_ackermann.launch.py
world_file_name = "montreal_line_ackermann.world"

# FICHERO RoboticsInfrastructure/Launchers/nurburgring_circuit_ackermann.launch.py
world_file_name = "nurburgring_line_ackermann.world"

# FICHERO RoboticsInfrastructure/Launchers/simple_circuit_ackermann.launch.py
world_file_name = "simple_circuit_ackermann.world"

worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro de los directorios monaco_circuit/, montmelo_circuit/, montreal_circuit/, nurburgring_circuit/, simple_circuit/, monaco_circuit_ackermann/ montmelo_circuit_ackermann/, montreal_circuit_ackermann/, nurburgring_circuit_ackermann/ o simple_circuit_ackermann/, el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/monaco_line.world
FICHERO RoboticsInfrastructure/Worlds/montmelo_line.world
FICHERO RoboticsInfrastructure/Worlds/montreal_line.world
FICHERO RoboticsInfrastructure/Worlds/nurburgring_line.world
FICHERO RoboticsInfrastructure/Worlds/simple_circuit.world
FICHERO RoboticsInfrastructure/Worlds/monaco_line_ackermann.world
FICHERO RoboticsInfrastructure/Worlds/montmelo_line_ackermann.world
FICHERO RoboticsInfrastructure/Worlds/montreal_line_ackermann.world
FICHERO RoboticsInfrastructure/Worlds/nurburgring_line_ackermann.world
FICHERO RoboticsInfrastructure/Worlds/simple_circuit_ackermann.world

En estos ficheros, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

<?xml version="1.0" ?>
<sdf version="1.10">
<world name="default">

<plugin name="gz::sim::systems::Physics" filename="gz-sim-physics-system"/>
<plugin name="gz::sim::systems::SceneBroadcaster" filename="gz-sim-scene-broadcaster-system"/>
<plugin
filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands">
</plugin>
<plugin
filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>

<gravity>0 0 -9.8</gravity>
<atmosphere type="adiabatic"/>

<scene>
<ambient>0.4 0.4 0.4 1</ambient>
<background>0.7 0.7 0.7 1</background>
<shadows>true</shadows>
</scene>

<!-- Circuito -->
<include>

<!-- FICHERO RoboticsInfrastructure/Worlds/monaco_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/monaco_line_ackermann.world -->
<uri>model://monaco</uri>

<!-- FICHERO RoboticsInfrastructure/Worlds/montmelo_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/montmelo_line_ackermann.world -->
<uri>model://montmelo_line</uri>

<!-- FICHERO RoboticsInfrastructure/Worlds/montreal_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/montreal_line_ackermann.world -->
<uri>model://montreal_line</uri>

<!-- FICHERO RoboticsInfrastructure/Worlds/nurburgring_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/nurburgring_line_ackermann.world -->
<uri>model://nurburgring_line</uri>

<!-- FICHERO RoboticsInfrastructure/Worlds/simple_circuit.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/simple_circuit_ackermann.world -->
<uri>model://simple_circuit</uri>

<pose>0 0 0 0 0 0</pose>
</include>

<!-- Coche -->
<include>

<!-- FICHERO RoboticsInfrastructure/Worlds/monaco_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/montmelo_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/montreal_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/nurburgring_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/simple_circuit.world -->
<uri>model://f1_renault</uri>

<!-- FICHERO RoboticsInfrastructure/Worlds/monaco_line_ackermann.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/montmelo_line_ackermann.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/montreal_line_ackermann.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/nurburgring_line_ackermann.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/simple_circuit_ackermann.world -->
<uri>model://f1_renault_camera</uri>

<!-- FICHERO RoboticsInfrastructure/Worlds/monaco_line.world -->
<pose>-105.223 -70.77 -1.8 0 0 1.69</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/monaco_line_ackermann.world -->
<pose>-105.223 -70.77 -1.8 0 0 0.12</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/montmelo_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/montmelo_line_ackermann.world -->
<pose>27.18 -31.55 0.00 0.00 0.01 -3.12</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/montreal_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/montreal_line_ackermann.world -->
<pose>-200.88 -90.72 0.00 0.00 0.00 -2.83</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/nurburgring_line.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/nurburgring_line_ackermann.world -->
<pose>-74.29 37.74 0 0 0 -0.51</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/simple_circuit.world -->
<!-- FICHERO RoboticsInfrastructure/Worlds/simple_circuit_ackermann.world -->
<pose>53.462 -10.734 0.004 0 0 -1.57</pose>

</include>

<!-- Luz -->
<light name="sun" type="directional">
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<direction>-0.5 0.1 -0.9</direction>
<cast_shadows>true</cast_shadows>
</light>

</world>
</sdf>

FICHERO RoboticsInfrastructure/Launchers/visualization/follow_line.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Light" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#ffa726" toolbar_text_color_light="#000000" toolbar_color_dark="#ffa726" toolbar_text_color_dark="#000000" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>0.4 0.4 0.4</ambient_light>
<background_color>0.8 0.8 0.8</background_color>
<camera_pose>-2 0 2 0 0.4 0</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin>

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Follow Line) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
13 Montmelo Ackermann Circuit /opt/jderobot/Launchers/montmelo_circuit_ackermann.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
14 Montmelo Circuit /opt/jderobot/Launchers/montmelo_circuit.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
15 Montreal Ackermann Circuit /opt/jderobot/Launchers/montreal_circuit_ackermann.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
16 Montreal Circuit /opt/jderobot/Launchers/montreal_circuit.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
17 Nurburgring Ackermann Circuit /opt/jderobot/Launchers/nurburgring_circuit_ackermann.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
18 Nurburgring Circuit /opt/jderobot/Launchers/nurburgring_circuit.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
22 Simple Ackermann Circuit /opt/jderobot/Launchers/simple_circuit_ackermann.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
23 Simple Circuit /opt/jderobot/Launchers/simple_circuit.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
41 Monaco Ackermann Circuit /opt/jderobot/Launchers/monaco_circuit_ackermann.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
42 Monaco Circuit /opt/jderobot/Launchers/monaco_circuit.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
13 Montmelo Ackermann Circuit /opt/jderobot/Launchers/montmelo_circuit_ackermann.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
14 Montmelo Circuit /opt/jderobot/Launchers/montmelo_circuit.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
15 Montreal Ackermann Circuit /opt/jderobot/Launchers/montreal_circuit_ackermann.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
16 Montreal Circuit /opt/jderobot/Launchers/montreal_circuit.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
17 Nurburgring Ackermann Circuit /opt/jderobot/Launchers/nurburgring_circuit_ackermann.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
18 Nurburgring Circuit /opt/jderobot/Launchers/nurburgring_circuit.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
22 Simple Ackermann Circuit /opt/jderobot/Launchers/simple_circuit_ackermann.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
23 Simple Circuit /opt/jderobot/Launchers/simple_circuit.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
41 Monaco Ackermann Circuit /opt/jderobot/Launchers/monaco_circuit_ackermann.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
42 Monaco Circuit /opt/jderobot/Launchers/monaco_circuit.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_line.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada harmonic-follow-line-tests. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i harmonic-follow-line-tests

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Follow Line al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

IMAGEN UNIBOTICS GAZEBO MONACO_CIRCUIT
IMAGEN UNIBOTICS GAZEBO MONACO_CIRCUIT_ACKERMANN

Monaco Circuit Image

IMAGEN UNIBOTICS GAZEBO MONTMELO_CIRCUIT
IMAGEN UNIBOTICS GAZEBO MONTMELO_CIRCUIT_ACKERMANN

Montmelo Circuit Image

IMAGEN UNIBOTICS GAZEBO MONTREAL_CIRCUIT
IMAGEN UNIBOTICS GAZEBO MONTREAL_CIRCUIT_ACKERMANN

Montreal Circuit Image

IMAGEN UNIBOTICS GAZEBO NURBURGRING_CIRCUIT
IMAGEN UNIBOTICS GAZEBO NURBURGRING_CIRCUIT_ACKERMANN

Nurburgring Circuit Image

IMAGEN UNIBOTICS GAZEBO SIMPLE_CIRCUIT
IMAGEN UNIBOTICS GAZEBO SIMPLE_CIRCUIT_ACKERMANN

Simple Circuit Image

Además, para verificar que tanto los circuitos como los coches han sido migrados correctamente, se muestra una pequeña animación del coche moviéndose en línea recta para verificar que todo el proceso se ha realizado correctamente:

VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO MONACO_CIRCUIT

VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO MONTMELO_CIRCUIT


VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO MONTREAL_CIRCUIT


VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO NURBURGRING_CIRCUIT


VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO SIMPLE_CIRCUIT

VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO MONACO_CIRCUIT_ACKERMANN

VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO MONTMELO_CIRCUIT_ACKERMANN


VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO MONTREAL_CIRCUIT_ACKERMANN


VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO NURBURGRING_CIRCUIT_ACKERMANN


VÍDEO DEL COCHE MOVIÉNDOSE POR EL ESCENARIO SIMPLE_CIRCUIT_ACKERMANN

SEMANA 10: 24-11-2025 al 28-11-2025

Migración del ejercicio Basic Vacuum Cleaner a Gazebo Harmonic

Con el ejercicio de Follow Line ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un quinto ejercicio, en este caso, Basic Vacuum Cleaner, que consiste en la implementación de la lógica de un algoritmo de navegación para una aspiradora autónoma, cuyo objetivo principal será cubrir la mayor superficie posible de una casa utilizando el algoritmo programado.

Basic Vacuum Cleaner Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/roomba_robot/models/roombaROS/model.sdf

En este fichero se han modificado las partes correspondientes a las etiquetas <plugin> y <sensor>, donde el plugin pasa de estar declarado dentro del sensor a integrarse en él. Además, es importante añadir al final de la nueva versión los plugins correspondientes al velocity-control-system, al odometry-publisher-system y al contact-system:

NOTA: Dada la gran extensión en cuanto a líneas se refiere de este fichero, adjunto a continuación un enlace a dicho fichero en el que se puede visualizar todo su contenido.

ENLACE: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana10/model.sdf

FICHERO RoboticsInfrastructure/CustomRobots/roomba_robot/params/roombaROS.yaml

Este fichero se crea de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes a los plugins publicadores y subscriptores del diff-drive (/roombaROS/odom y /roombaROS/cmd_vel), del sensor láser LIDAR (/roombaROS/laser/scan) y de los 3 sensores bumper de contacto (/roombaROS/events/center_bumper, /roombaROS/events/right_bumper y /roombaROS/events/left_bumper):

# gz topic published by DiffDrive plugin
- ros_topic_name: "odom"
gz_topic_name: "/roombaROS/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS


# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "cmd_vel"
gz_topic_name: "/roombaROS/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/roombaROS/laser/scan"
gz_topic_name: "/roombaROS/laser/scan"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/center_bumper"
gz_topic_name: "/roombaROS/events/center_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/right_bumper"
gz_topic_name: "/roombaROS/events/right_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/left_bumper"
gz_topic_name: "/roombaROS/events/left_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de la línea roomba_robot/params para que el fichero que se acaba de crear (roombaROS.yaml) se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# ROOMBA
roomba_robot/models
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# ROOMBA
roomba_robot/models
roomba_robot/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/basic_vacuum_cleaner/spawn_robot.launch.py

Una vez creado el directorio basic_vacuum_cleaner/, y a su vez dentro de él el fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a las variables start_gazebo_ros_image_bridge_cmd y start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a bridge_params (en este caso, roombaROS.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "roombaROS"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "roombaROS.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

# start_gazebo_ros_image_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/image_raw"],
# output="screen",
# )

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
# ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/vacuum_cleaner.launch.py

Para este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

robot_launch_dir = "/opt/jderobot/Launchers/basic_vacuum_cleaner"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")
world_file_name = "roomba_1_house_harmonic.world"
worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro del directorio basic_vacuum_cleaner/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/roomba_1_house_harmonic.world

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

<?xml version="1.0" ?>

<sdf version="1.8">
<world name="default">
<plugin
filename="gz-sim-physics-system"
name="gz::sim::systems::Physics">
</plugin>
<plugin
filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands">
</plugin>
<plugin
filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster">
</plugin>
<plugin
filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>

<scene>
<ambient>0.4 0.4 0.4 1</ambient>
<background>0.7 0.7 0.7 1</background>
<shadows>true</shadows>
<grid>false</grid>
</scene>

<include>
<uri>model://roombaROS</uri>
<pose>-1 1.5 0 0 0 0</pose>
</include>

<include>
<uri>model://house_int2</uri>
<pose>0 0 0 0 0 0</pose>
</include>

<light type="directional" name="sun">
<cast_shadows>1</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<direction>-0.5 0.1 -0.9</direction>
<visualize>false</visualize>
</light>

</world>
</sdf>

FICHERO RoboticsInfrastructure/Launchers/visualization/vacuum_house.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Light" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#f3f3f3" toolbar_text_color_light="#111111" toolbar_color_dark="#414141" toolbar_text_color_dark="#f3f3f3" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>0.4 0.4 0.4</ambient_light>
<background_color>0.8 0.8 0.8</background_color>
<camera_pose>0 3.5 19 0.00 1.57 -1.57</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin>

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Basic Vacuum Cleaner) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
24 Vacuums House /opt/jderobot/Launchers/vacuum_cleaner.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
24 Vacuums House /opt/jderobot/Launchers/vacuum_cleaner.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/vacuum_house.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada basic-vacuum-cleaner-harmonic. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i basic-vacuum-cleaner-harmonic

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Basic Vacuum Cleaner al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

Basic Vacuum Cleaner Image

Además, para verificar que tanto la casa como la aspiradora han sido migrados correctamente, se muestra una pequeña animación de la aspiradora moviéndose para verificar que todo el proceso se ha realizado correctamente:

SEMANA 11: 01-12-2025 al 05-12-2025

Migración del ejercicio Localized Vacuum Cleaner a Gazebo Harmonic

Con el ejercicio de Basic Vacuum Cleaner ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un sexto ejercicio, en este caso, Localized Vacuum Cleaner, que consiste en implementar la lógica de un algoritmo de navegación para una aspiradora autónoma utilizando la ubicación del robot, donde el robot está equipado con un mapa y conoce su ubicación actual en él, cuyo objetivo principal será cubrir la mayor superficie posible de una casa utilizando el algoritmo programado.

Localized Vacuum Cleaner Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/roomba_robot/models/roombaROS/model.sdf

En este fichero se han modificado las partes correspondientes a las etiquetas <plugin> y <sensor>, donde el plugin pasa de estar declarado dentro del sensor a integrarse en él. Además, es importante añadir al final de la nueva versión los plugins correspondientes al velocity-control-system, al odometry-publisher-system y al contact-system:

NOTA: Dada la gran extensión en cuanto a líneas se refiere de este fichero, adjunto a continuación un enlace a dicho fichero en el que se puede visualizar todo su contenido.

ENLACE: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana11/model.sdf

FICHERO RoboticsInfrastructure/CustomRobots/roomba_robot/params/roombaROS.yaml

Este fichero se crea de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes a los plugins publicadores y subscriptores del DiffDrive (/roombaROS/odom y /roombaROS/cmd_vel), del sensor láser LIDAR (/roombaROS/laser/scan) y de los 3 sensores bumper de contacto (/roombaROS/events/center_bumper, /roombaROS/events/right_bumper y /roombaROS/events/left_bumper):

# gz topic published by DiffDrive plugin
- ros_topic_name: "odom"
gz_topic_name: "/roombaROS/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS


# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "cmd_vel"
gz_topic_name: "/roombaROS/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/roombaROS/laser/scan"
gz_topic_name: "/roombaROS/laser/scan"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/center_bumper"
gz_topic_name: "/roombaROS/events/center_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/right_bumper"
gz_topic_name: "/roombaROS/events/right_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/left_bumper"
gz_topic_name: "/roombaROS/events/left_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de la línea roomba_robot/params para que el fichero que se acaba de crear (roombaROS.yaml) se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# ROOMBA
roomba_robot/models
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# ROOMBA
roomba_robot/models
roomba_robot/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/localized_vacuum_cleaner/spawn_robot.launch.py

Una vez creado el directorio localized_vacuum_cleaner/, y a su vez dentro de él el fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a las variables start_gazebo_ros_image_bridge_cmd y start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a bridge_params (en este caso, roombaROS.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "roombaROS"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "roombaROS.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

# start_gazebo_ros_image_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/image_raw"],
# output="screen",
# )

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
# ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/vacuum_cleaner.launch.py

Para este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

robot_launch_dir = "/opt/jderobot/Launchers/localized_vacuum_cleaner"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")
world_file_name = "roomba_1_house_harmonic.world"
worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro del directorio localized_vacuum_cleaner/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/roomba_1_house_harmonic.world

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

<?xml version="1.0" ?>

<sdf version="1.8">
<world name="default">
<plugin
filename="gz-sim-physics-system"
name="gz::sim::systems::Physics">
</plugin>
<plugin
filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands">
</plugin>
<plugin
filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster">
</plugin>
<plugin
filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>

<scene>
<ambient>0.4 0.4 0.4 1</ambient>
<background>0.7 0.7 0.7 1</background>
<shadows>true</shadows>
<grid>false</grid>
</scene>

<include>
<uri>model://roombaROS</uri>
<pose>-1 1.5 0 0 0 0</pose>
</include>

<include>
<uri>model://house_int2</uri>
<pose>0 0 0 0 0 0</pose>
</include>

<light type="directional" name="sun">
<cast_shadows>1</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<direction>-0.5 0.1 -0.9</direction>
<visualize>false</visualize>
</light>

</world>
</sdf>

FICHERO RoboticsInfrastructure/Launchers/visualization/vacuum_house.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Light" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#f3f3f3" toolbar_text_color_light="#111111" toolbar_color_dark="#414141" toolbar_text_color_dark="#f3f3f3" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>0.4 0.4 0.4</ambient_light>
<background_color>0.8 0.8 0.8</background_color>
<camera_pose>0 3.5 19 0.00 1.57 -1.57</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin>

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Localized Vacuum Cleaner) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
24 Vacuums House /opt/jderobot/Launchers/vacuum_cleaner.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
24 Vacuums House /opt/jderobot/Launchers/vacuum_cleaner.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/vacuum_house.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada localized-vacuum-cleaner-harmonic. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i localized-vacuum-cleaner-harmonic

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Localized Vacuum Cleaner al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

Localized Vacuum Cleaner Image

Además, para verificar que tanto la casa como la aspiradora han sido migrados correctamente, se muestra una pequeña animación de la aspiradora moviéndose para verificar que todo el proceso se ha realizado correctamente:

SEMANA 12: 08-12-2025 al 12-12-2025

Migración del ejercicio Montecarlo Laser Localization a Gazebo Harmonic

Con el ejercicio de Localized Vacuum Cleaner ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un séptimo ejercicio, en este caso, Montecarlo Laser Localization, que consiste en desarrollar un algoritmo de localización basado en el filtro de partículas utilizando el láser del robot.

Montecarlo Laser Localization Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/roomba_robot/models/roombaROS/model.sdf

En este fichero se han modificado las partes correspondientes a las etiquetas <plugin> y <sensor>, donde el plugin pasa de estar declarado dentro del sensor a integrarse en él. Además, es importante añadir al final de la nueva versión los plugins correspondientes al velocity-control-system, al odometry-publisher-system y al contact-system:

NOTA: Dada la gran extensión en cuanto a líneas se refiere de este fichero, adjunto a continuación un enlace a dicho fichero en el que se puede visualizar todo su contenido.

ENLACE: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana12/model.sdf

FICHERO RoboticsInfrastructure/CustomRobots/roomba_robot/params/roombaROS.yaml

Este fichero se crea de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes a los plugins publicadores y subscriptores del DiffDrive (/roombaROS/odom y /roombaROS/cmd_vel), del sensor láser LIDAR (/roombaROS/laser/scan) y de los 3 sensores bumper de contacto (/roombaROS/events/center_bumper, /roombaROS/events/right_bumper y /roombaROS/events/left_bumper):

# gz topic published by DiffDrive plugin
- ros_topic_name: "odom"
gz_topic_name: "/roombaROS/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS


# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "cmd_vel"
gz_topic_name: "/roombaROS/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/roombaROS/laser/scan"
gz_topic_name: "/roombaROS/laser/scan"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/center_bumper"
gz_topic_name: "/roombaROS/events/center_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/right_bumper"
gz_topic_name: "/roombaROS/events/right_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/left_bumper"
gz_topic_name: "/roombaROS/events/left_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de la línea roomba_robot/params para que el fichero que se acaba de crear (roombaROS.yaml) se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# ROOMBA
roomba_robot/models
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# ROOMBA
roomba_robot/models
roomba_robot/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/montecarlo_laser_localization/spawn_robot.launch.py

Una vez creado el directorio montecarlo_laser_localization/, y a su vez dentro de él el fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a las variables start_gazebo_ros_image_bridge_cmd y start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a bridge_params (en este caso, roombaROS.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "roombaROS"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "roombaROS.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

# start_gazebo_ros_image_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/image_raw"],
# output="screen",
# )

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
# ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/vacuum_cleaner.launch.py

Para este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar únicamente las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

robot_launch_dir = "/opt/jderobot/Launchers/montecarlo_laser_localization"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")
world_file_name = "roomba_1_house_harmonic.world"
worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro del directorio montecarlo_laser_localization/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/roomba_1_house_harmonic.world

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

<?xml version="1.0" ?>

<sdf version="1.8">
<world name="default">
<plugin
filename="gz-sim-physics-system"
name="gz::sim::systems::Physics">
</plugin>
<plugin
filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands">
</plugin>
<plugin
filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster">
</plugin>
<plugin
filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>

<scene>
<ambient>0.4 0.4 0.4 1</ambient>
<background>0.7 0.7 0.7 1</background>
<shadows>true</shadows>
<grid>false</grid>
</scene>

<include>
<uri>model://roombaROS</uri>
<pose>-1 1.5 0 0 0 0</pose>
</include>

<include>
<uri>model://house_int2</uri>
<pose>0 0 0 0 0 0</pose>
</include>

<light type="directional" name="sun">
<cast_shadows>1</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<direction>-0.5 0.1 -0.9</direction>
<visualize>false</visualize>
</light>

</world>
</sdf>

FICHERO RoboticsInfrastructure/Launchers/visualization/vacuum_house.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Light" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#f3f3f3" toolbar_text_color_light="#111111" toolbar_color_dark="#414141" toolbar_text_color_dark="#f3f3f3" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>0.4 0.4 0.4</ambient_light>
<background_color>0.8 0.8 0.8</background_color>
<camera_pose>0 3.5 19 0.00 1.57 -1.57</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin>

FICHERO RoboticsInfrastructure/database/universes.sql

En ets fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Montecarlo Laser Localization) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
24 Vacuums House /opt/jderobot/Launchers/vacuum_cleaner.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
24 Vacuums House /opt/jderobot/Launchers/vacuum_cleaner.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/vacuum_house.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada montecarlo-laser-localization-harmonic. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i montecarlo-laser-localization-harmonic

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Montecarlo Laser Localization al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

Montecarlo Laser Localization Image

Además, para verificar que tanto la casa como la aspiradora han sido migrados correctamente, se muestra una pequeña animación de la aspiradora moviéndose para verificar que todo el proceso se ha realizado correctamente:

SEMANA 13: 15-12-2025 al 19-12-2025

Migración del ejercicio Montecarlo Visual Localization a Gazebo Harmonic

Con el ejercicio de Montecarlo Laser Localization ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un octavo ejercicio, en este caso, Montecarlo Visual Localization, que consiste en desarrollar un algoritmo de localización visual basado en el filtro de partículas.

Montecarlo Visual Localization Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/roomba_robot/models/roombaROS_cam/model.sdf

En este fichero se han modificado las partes correspondientes a las etiquetas <plugin> y <sensor>, donde el plugin pasa de estar declarado dentro del sensor a integrarse en él. Además, es importante añadir al final de la nueva versión los plugins correspondientes al velocity-control-system, al odometry-publisher-system y al contact-system:

NOTA: Dada la gran extensión en cuanto a líneas se refiere de este fichero, adjunto a continuación un enlace a dicho fichero en el que se puede visualizar todo su contenido.

ENLACE: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana13/model.sdf

FICHERO RoboticsInfrastructure/CustomRobots/roomba_robot/params/roombaROS_cam.yaml

Este fichero se crea de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes a los plugins publicadores y subscriptores del DiffDrive (/roombaROS/odom y /roombaROS/cmd_vel), del sensor láser LIDAR (/roombaROS/laser/scan) de los 3 sensores bumper de contacto (/roombaROS/events/center_bumper, /roombaROS/events/right_bumper y /roombaROS/events/left_bumper) y de la cámara (/camera/camera_info):

# gz topic published by DiffDrive plugin
- ros_topic_name: "odom"
gz_topic_name: "/roombaROS/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS


# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "cmd_vel"
gz_topic_name: "/roombaROS/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/roombaROS/laser/scan"
gz_topic_name: "/roombaROS/laser/scan"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/center_bumper"
gz_topic_name: "/roombaROS/events/center_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/right_bumper"
gz_topic_name: "/roombaROS/events/right_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

- ros_topic_name: "/roombaROS/events/left_bumper"
gz_topic_name: "/roombaROS/events/left_bumper"
ros_type_name: "ros_gz_interfaces/msg/Contacts"
gz_type_name: "gz.msgs.Contacts"
direction: GZ_TO_ROS

# gz topic published by Sensors plugin (Camera)
- ros_topic_name: "/camera/camera_info"
gz_topic_name: "/camera/camera_info"
ros_type_name: "sensor_msgs/msg/CameraInfo"
gz_type_name: "gz.msgs.CameraInfo"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de la línea roomba_robot/params para que el fichero que se acaba de crear (roombaROS_cam.yaml) se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# ROOMBA
roomba_robot/models
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# ROOMBA
roomba_robot/models
roomba_robot/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/montecarlo_visual_localization/spawn_robot.launch.py

Una vez creado el directorio montecarlo_visual_localization/, y a su vez dentro de él el fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a la variable start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a bridge_params (en este caso, roombaROS_cam.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "roombaROS_cam"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "roombaROS_cam.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

start_gazebo_ros_image_bridge_cmd = Node(
package="ros_gz_image",
executable="image_bridge",
arguments=["/camera/image_raw"],
output="screen",
)

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/montecarlo_visual_loc.launch.py

Para este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

robot_launch_dir = "/opt/jderobot/Launchers/montecarlo_visual_localization"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")
world_file_name = "roomba_1_house_montecarlo_visual_loc.world"
worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro del directorio montecarlo_visual_localization/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/roomba_1_house_montecarlo_visual_loc.world

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

<?xml version="1.0" ?>

<sdf version="1.8">
<world name="default">
<plugin
filename="gz-sim-physics-system"
name="gz::sim::systems::Physics">
</plugin>
<plugin
filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands">
</plugin>
<plugin
filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster">
</plugin>
<plugin
filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>

<!-- <gui fullscreen=1>
<camera name='user_camera'>
<pose frame=''>-2.41 5.36 2.04 0.00 0.254 -1.14</pose>
<view_controller>orbit</view_controller>
<projection_type>perspective</projection_type>
</camera>
</gui> -->

<gravity>0 0 -9.8</gravity>
<atmosphere type="adiabatic"/>

<scene>
<ambient>0.4 0.4 0.4 1</ambient>
<background>0.7 0.7 0.7 1</background>
<shadows>true</shadows>
<grid>false</grid>
</scene>

<include>
<uri>model://roombaROS_cam</uri>
<pose>-1 1.5 0 0 0 0</pose>
</include>

<include>
<uri>model://house_int2_roof</uri>
<pose>0 0 0 0 0 0</pose>
</include>

<include>
<uri>model://ground_plane_sincolor</uri>
</include>

<light type="directional" name="sun">
<cast_shadows>1</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<direction>-0.5 0.1 -0.9</direction>
<visualize>false</visualize>
</light>

</world>
</sdf>

FICHERO RoboticsInfrastructure/Launchers/visualization/montecarlo_visual_localization.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Light" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#ffa726" toolbar_text_color_light="#000000" toolbar_color_dark="#ffa726" toolbar_text_color_dark="#000000" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>0.4 0.4 0.4</ambient_light>
<background_color>0.8 0.8 0.8</background_color>
<camera_pose>-2 0 2 0 0.4 0</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin>

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Montecarlo Visual Localization) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
26 Vacuums House Roof /opt/jderobot/Launchers/montecarlo_visual_loc.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
26 Vacuums House Roof /opt/jderobot/Launchers/montecarlo_visual_loc.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/montecarlo_visual_localization.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada montecarlo-visual-localization-harmonic. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i montecarlo-visual-localization-harmonic

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Montecarlo Visual Localization al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

Montecarlo Visual Localization Image

Además, para verificar que tanto la casa como la aspiradora han sido migrados correctamente, se muestra una pequeña animación de la aspiradora moviéndose para verificar que todo el proceso se ha realizado correctamente:

SEMANA 14: 22-12-2025 al 26-12-2025

Christmas Holiday Board

SEMANA 15: 29-12-2025 al 02-01-2026

Christmas Holiday Board

SEMANA 16: 05-01-2026 al 09-01-2026

Christmas Holiday Board

SEMANA 17: 12-01-2026 al 16-01-2026

Migración del ejercicio 3D Reconstruction a Gazebo Harmonic

Con el ejercicio de Montecarlo Visual Localization ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un noveno ejercicio, en este caso, 3D Reconstruction, que consiste en programar la lógica necesaria para permitir que el robot Kobuki genere una reconstrucción 3D de la escena que recibe a través de sus cámaras, tanto de la izquierda como de la derecha.

3D Reconstruction Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/3d_reconstruction/models/turtlebotROS/model.sdf

En este fichero se han modificado las partes correspondientes a las etiquetas <plugin> y <sensor>, donde el plugin pasa de estar declarado dentro del sensor a integrarse en él.

NOTA: Dada la gran extensión en cuanto a líneas se refiere de este fichero, adjunto a continuación un enlace a dicho fichero en el que se puede visualizar todo su contenido.

ENLACE: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana17/model.sdf

FICHERO RoboticsInfrastructure/CustomRobots/3d_reconstruction/params/turtlebotROS.yaml

Este fichero se crea de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes al publicador y al subscriptor del sensor láser LIDAR (/turtlebotROS/laser/scan) y de las cámaras (/cam_turtlebot_left/camera_info y /cam_turtlebot_right/camera_info):

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/turtlebotROS/laser/scan"
gz_topic_name: "/turtlebotROS/laser/scan"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS


# gz topic published by Sensors plugin (Camera)
- ros_topic_name: "/cam_turtlebot_left/camera_info"
gz_topic_name: "/cam_turtlebot_left/camera_info"
ros_type_name: "sensor_msgs/msg/CameraInfo"
gz_type_name: "gz.msgs.CameraInfo"
direction: GZ_TO_ROS

- ros_topic_name: "/cam_turtlebot_right/camera_info"
gz_topic_name: "/cam_turtlebot_right/camera_info"
ros_type_name: "sensor_msgs/msg/CameraInfo"
gz_type_name: "gz.msgs.CameraInfo"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de la línea 3d_reconstruction/params para que el fichero que se acaba de crear (turtlebotROS.yaml) se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# 3D RECONSTRUCTION
3d_reconstruction/models
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# 3D RECONSTRUCTION
3d_reconstruction/models
3d_reconstruction/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/3d_reconstruction/spawn_robot.launch.py

Una vez creado el directorio 3d_reconstruction/, y a su vez dentro de él el fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic, copiar su contenido y comentar todo lo relativo a la variable start_gazebo_ros_depth_bridge_cmd, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a bridge_params (en este caso, turtlebotROS.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "turtlebotROS"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "turtlebotROS.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

start_gazebo_ros_image_bridge_cmd_left = Node(
package="ros_gz_image",
executable="image_bridge",
arguments=["/cam_turtlebot_left/image_raw"],
output="screen",
)

start_gazebo_ros_image_bridge_cmd_right = Node(
package="ros_gz_image",
executable="image_bridge",
arguments=["/cam_turtlebot_right/image_raw"],
output="screen",
)

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
ld.add_action(start_gazebo_ros_image_bridge_cmd_left)
ld.add_action(start_gazebo_ros_image_bridge_cmd_right)
# ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/3d_reconstruction.launch.py

Para este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar únicamente las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

robot_launch_dir = "/opt/jderobot/Launchers/3d_reconstruction"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")
world_file_name = "kobuki_1_reconstruction3d.world"
worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro del directorio 3d_reconstruction/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/kobuki_1_reconstruction3d.world

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

<?xml version="1.0" ?>

<sdf version="1.8">
<world name="default">
<plugin
filename="gz-sim-user-commands-system"
name="gz::sim::systems::UserCommands">
</plugin>
<plugin
filename="gz-sim-scene-broadcaster-system"
name="gz::sim::systems::SceneBroadcaster">
</plugin>
<plugin
filename="gz-sim-sensors-system"
name="gz::sim::systems::Sensors">
<render_engine>ogre2</render_engine>
</plugin>

<scene>
<ambient>0.4 0.4 0.4 1</ambient>
<background>0.7 0.7 0.7 1</background>
<shadows>true</shadows>
<grid>true</grid>
</scene>

<light name='point_light1' type='point'>
<pose frame=''>-3.0 -2.0 4.0 0 0 0</pose>
<visualize>false</visualize>
</light>

<light name='point_light2' type='point'>
<pose frame=''>-3.0 2.0 4.0 0 0 0</pose>
<visualize>false</visualize>
</light>

<light name='point_light3' type='directional'>
<pose frame=''>0 0 1.0 0 -1.57 0</pose>
<visualize>false</visualize>
</light>

<include>
<uri>model://ground_plane_sincolor</uri>
</include>

<!-- Pioneer2dx model -->
<include>
<uri>model://turtlebotROS</uri>
<pose>0 0 0 0 0 0</pose>
</include>

<!-- Duck -->
<include>
<uri>model://duck</uri>
<pose>4.7 3 0 1.57 0 1.57</pose>
</include>

<!-- Cereales -->
<include>
<uri>model://cereales</uri>
<pose>5.3 2.3 0 0 0 0</pose>
</include>

<!-- Blocks -->
<include>
<uri>model://blocks</uri>
<pose>4.66 -4 0 0 0 1.57</pose>
</include>

<!-- charactersMario -->
<include>
<uri>model://charactersMario</uri>
<pose>4.1 0.5 0 0 0 -1.57</pose>
</include>

<light type="directional" name="sun">
<cast_shadows>1</cast_shadows>
<pose>0 0 10 0 0 0</pose>
<diffuse>0.8 0.8 0.8 1</diffuse>
<specular>0.2 0.2 0.2 1</specular>
<direction>-0.5 0.1 -0.9</direction>
<visualize>false</visualize>
</light>

</world>
</sdf>

FICHERO RoboticsInfrastructure/Launchers/visualization/3d_reconstruction.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Light" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#ffa726" toolbar_text_color_light="#000000" toolbar_color_dark="#ffa726" toolbar_text_color_dark="#000000" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>0.4 0.4 0.4</ambient_light>
<background_color>0.8 0.8 0.8</background_color>
<camera_pose>-2 0 2 0 0.4 0</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin>

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, 3D Reconstruction) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
1 3d Reconstruction /opt/jderobot/Launchers/3d_reconstruction.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
1 3d Reconstruction /opt/jderobot/Launchers/3d_reconstruction.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/3d_reconstruction.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada 3d-reconstruction-harmonic. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i 3d-reconstruction-harmonic

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio 3D Reconstruction al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

3D Reconstruction Image

Además, para verificar que tanto los objetos como el Kobuki han sido migrados correctamente, se muestra una pequeña animación de la solución de este ejercicio para verificar que todo el proceso se ha realizado correctamente:

SEMANA 18: 19-01-2026 al 23-01-2026

Migración del ejercicio Follow Person a Gazebo Harmonic

Con el ejercicio de 3D Reconstruction ya migrado por completo a Gazebo Harmonic, se me ha pedido realizar la migración completa de Gazebo 11 a Gazebo Harmonic de un décimo ejercicio, en este caso, Follow Person, que consiste en implementar la lógica de un algoritmo de navegación que se utilizará para seguir a una persona en un hospital utilizando una R-CNN (Red Neuronal Convolucional basada en Regiones) llamada SSD (Detector de Disparo Único).

Follow Person Exercise

A continuación, se encuentran todos los ficheros que se han modificado y / o creado (y de qué forma) para poder llevar a cabo la migración completa de este ejercicio de Gazebo 11 a Gazebo Harmonic:

FICHERO RoboticsInfrastructure/CustomRobots/3d_reconstruction/models/turtlebotROS/model.sdf

En este fichero se han modificado las partes correspondientes a las etiquetas <plugin> y <sensor>, donde el plugin pasa de estar declarado dentro del sensor a integrarse en él. Además, es importante añadir al final de la nueva versión los plugins correspondientes al velocity-control-system y al odometry-publisher-system:

NOTA: Dada la gran extensión en cuanto a líneas se refiere de este fichero, adjunto a continuación un enlace a dicho fichero en el que se puede visualizar todo su contenido.

ENLACE: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana18/model.sdf

FICHERO RoboticsInfrastructure/CustomRobots/3d_reconstruction/params/turtlebotROS.yaml

Este fichero se crea de cero en la ruta especificada, aunque se puede coger cualquier fichero del tipo robot_params.yaml como referencia. En este caso, se añaden los topics correspondientes a los plugins publicadores y subscriptores del DiffDrive (/odom y /cmd_vel), del sensor láser LIDAR (/scan) y de la cámara (/depth_camera/camera_info):

# gz topic published by DiffDrive plugin
- ros_topic_name: "odom"
gz_topic_name: "/odom"
ros_type_name: "nav_msgs/msg/Odometry"
gz_type_name: "gz.msgs.Odometry"
direction: GZ_TO_ROS

# gz topic subscribed to by DiffDrive plugin
- ros_topic_name: "cmd_vel"
gz_topic_name: "/cmd_vel"
ros_type_name: "geometry_msgs/msg/Twist"
gz_type_name: "gz.msgs.Twist"
direction: ROS_TO_GZ

# gz topic published by Sensors plugin (LIDAR)
- ros_topic_name: "/scan"
gz_topic_name: "/scan"
ros_type_name: "sensor_msgs/msg/LaserScan"
gz_type_name: "gz.msgs.LaserScan"
direction: GZ_TO_ROS

# gz topic published by Sensors plugin (Camera)
- ros_topic_name: "/depth_camera/camera_info"
gz_topic_name: "/depth_camera/camera_info"
ros_type_name: "sensor_msgs/msg/CameraInfo"
gz_type_name: "gz.msgs.CameraInfo"
direction: GZ_TO_ROS

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de la línea 3d_reconstruction/params para que el fichero que se acaba de crear (turtlebotROS.yaml) se tenga en cuenta a la hora de lanzar el Docker:

# GAZEBO 11

install(
DIRECTORY
...
# 3D RECONSTRUCTION
3d_reconstruction/models
...
DESTINATION share/${PROJECT_NAME})
# GAZEBO HARMONIC

install(
DIRECTORY
...
# 3D RECONSTRUCTION
3d_reconstruction/models
3d_reconstruction/params
...
DESTINATION share/${PROJECT_NAME})

FICHERO RoboticsInfrastructure/Launchers/follow_person/spawn_robot.launch.py
FICHERO RoboticsInfrastructure/Launchers/follow_person_teleop/spawn_robot.launch.py

Una vez creados los directorios follow_person/ y follow_person_teleop/, y a su vez dentro de ellos el fichero spawn_robot.launch.py, habría que coger cualquier fichero con el mismo nombre de otro ejercicio que ya esté migrado a Gazebo Harmonic y copiar su contenido, y lo más importante, modificar el nombre del fichero que se le pasa como argumento a bridge_params (en este caso, turtlebotROS.yaml):

# Copyright 2019 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
# Get the urdf file
model_folder = "turtlebotROS"
urdf_path = os.path.join(
get_package_share_directory("custom_robots"),
"models",
model_folder,
"model.sdf",
)

# Launch configuration variables specific to simulation
# x_pose = LaunchConfiguration('x_pose', default='1.0')
# y_pose = LaunchConfiguration('y_pose', default='-1.5')
# z_pose = LaunchConfiguration('z_pose', default='7.1')

# Declare the launch arguments
# declare_x_position_cmd = DeclareLaunchArgument(
# 'x_pose', default_value='1.0',
# description='Specify namespace of the robot')

# declare_y_position_cmd = DeclareLaunchArgument(
# 'y_pose', default_value='-1.5',
# description='Specify namespace of the robot')

# declare_z_position_cmd = DeclareLaunchArgument(
# 'z_pose', default_value='7.1',
# description='Specify namespace of the robot')

# start_gazebo_ros_spawner_cmd = Node(
# package='ros_gz_sim',
# executable='create',
# arguments=[
# '-name', 'waffle',
# '-file', urdf_path,
# '-x', x_pose,
# '-y', y_pose,
# '-z', z_pose
# ],
# output='screen',
# )

bridge_params = os.path.join(
get_package_share_directory("custom_robots"), "params", "turtlebotROS.yaml"
)

start_gazebo_ros_bridge_cmd = Node(
package="ros_gz_bridge",
executable="parameter_bridge",
arguments=[
"--ros-args",
"-p",
f"config_file:={bridge_params}",
],
output="screen",
)

start_gazebo_ros_image_bridge_cmd_left = Node(
package="ros_gz_image",
executable="image_bridge",
arguments=["/cam_turtlebot_left/image_raw"],
output="screen",
)

start_gazebo_ros_image_bridge_cmd_right = Node(
package="ros_gz_image",
executable="image_bridge",
arguments=["/cam_turtlebot_right/image_raw"],
output="screen",
)

# start_gazebo_ros_depth_bridge_cmd = Node(
# package="ros_gz_image",
# executable="image_bridge",
# arguments=["/turtlebot3/camera/depth"],
# output="screen",
# )

ld = LaunchDescription()

# Declare the launch options
# ld.add_action(declare_x_position_cmd)
# ld.add_action(declare_y_position_cmd)
# ld.add_action(declare_z_position_cmd)

# Add any conditioned actions
# ld.add_action(start_gazebo_ros_spawner_cmd)
ld.add_action(start_gazebo_ros_bridge_cmd)
ld.add_action(start_gazebo_ros_image_bridge_cmd_left)
ld.add_action(start_gazebo_ros_image_bridge_cmd_right)
# ld.add_action(start_gazebo_ros_image_bridge_cmd)
# ld.add_action(start_gazebo_ros_depth_bridge_cmd)

return ld

FICHERO RoboticsInfrastructure/Launchers/follow_person.launch.py
FICHERO RoboticsInfrastructure/Launchers/follow_person_teleop.launch.py

Para estos ficheros, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y modificar las líneas correspondientes a las variables robot_launch_dir y world_file_name, correspondientes a las rutas en las que se encuentran el directorio que almacena el fichero spawn_robot.launch.py y el fichero del universo que utiliza este ejercicio.

import os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
IncludeLaunchDescription,
SetEnvironmentVariable,
AppendEnvironmentVariable,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, Command
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():

x = LaunchConfiguration("x")
y = LaunchConfiguration("y")
z = LaunchConfiguration("z")
roll = LaunchConfiguration("R")
pitch = LaunchConfiguration("P")
yaw = LaunchConfiguration("Y")

package_dir = get_package_share_directory("custom_robots")
ros_gz_sim = get_package_share_directory("ros_gz_sim")

gazebo_models_path = os.path.join(package_dir, "models")

# FICHERO RoboticsInfrastructure/Launchers/follow_person.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/follow_person"

# FICHERO RoboticsInfrastructure/Launchers/follow_person_teleop.launch.py
robot_launch_dir = "/opt/jderobot/Launchers/follow_person_teleop"

use_sim_time = LaunchConfiguration("use_sim_time", default="true")
x_pose = LaunchConfiguration("x_pose", default="1.0")
y_pose = LaunchConfiguration("y_pose", default="-1.5")
z_pose = LaunchConfiguration("z_pose", default="7.1")

# FICHERO RoboticsInfrastructure/Launchers/follow_person.launch.py
world_file_name = "hospital_follow_person.world"

# FICHERO RoboticsInfrastructure/Launchers/follow_person_teleop.launch.py
world_file_name = "hospital_follow_person_teleop.world"

worlds_dir = "/opt/jderobot/Worlds"
world_path = os.path.join(worlds_dir, world_file_name)

gazebo_server = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(ros_gz_sim, "launch", "gz_sim.launch.py")
),
launch_arguments={
"gz_args": ["-r -s -v4 ", world_path],
"on_exit_shutdown": "true",
}.items(),
)

declare_x_cmd = DeclareLaunchArgument("x", default_value="1.0")

declare_y_cmd = DeclareLaunchArgument("y", default_value="-1.5")

declare_z_cmd = DeclareLaunchArgument("z", default_value="7.1")

declare_roll_cmd = DeclareLaunchArgument("R", default_value="0.0")

declare_pitch_cmd = DeclareLaunchArgument("P", default_value="0.0")

declare_yaw_cmd = DeclareLaunchArgument("Y", default_value="1.57079")

# robot_state_publisher_cmd = IncludeLaunchDescription(
# PythonLaunchDescriptionSource(
# os.path.join(robot_launch_dir, "robot_state_publisher.launch.py")
# ),
# launch_arguments={"use_sim_time": use_sim_time}.items(),
# )

spawn_robot_cmd = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(robot_launch_dir, "spawn_robot.launch.py")
),
launch_arguments={"x_pose": x_pose, "y_pose": y_pose, "z_pose": z_pose}.items(),
)

world_entity_cmd = Node(
package="ros_gz_sim",
executable="create",
arguments=["-name", "world", "-file", world_path],
output="screen",
)

ld = LaunchDescription()

ld.add_action(SetEnvironmentVariable("GZ_SIM_RESOURCE_PATH", gazebo_models_path))
set_env_vars_resources = AppendEnvironmentVariable(
"GZ_SIM_RESOURCE_PATH", os.path.join(package_dir, "models")
)
ld.add_action(set_env_vars_resources)
ld.add_action(gazebo_server)
# ld.add_action(gazebo_client)
ld.add_action(declare_x_cmd)
ld.add_action(declare_y_cmd)
ld.add_action(declare_z_cmd)
ld.add_action(declare_roll_cmd)
ld.add_action(declare_pitch_cmd)
ld.add_action(declare_yaw_cmd)
ld.add_action(world_entity_cmd)
# ld.add_action(robot_state_publisher_cmd)
ld.add_action(spawn_robot_cmd)

return ld

NOTA: En caso de que no se haya creado dentro de los directorios follow_person/ y follow_person_teleop/ el fichero robot_state_publisher.launch.py, es obligatorio comentar y / o eliminar del código todo lo relacionado con la variable robot_state_publisher_cmd.

FICHERO RoboticsInfrastructure/Worlds/hospital_follow_person.world
FICHERO RoboticsInfrastructure/Worlds/hospital_follow_person_teleop.world

En estos ficheros, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todos aquellos flags <include> de la versión antigua por aquellos que aparezcan en la versión nueva.

NOTA: Dada la gran extensión en cuanto a líneas se refiere de estos ficheros, adjunto a continuación un enlace a dichos ficheros en el que se puede visualizar todo su contenido.

ENLACE UNIVERSO NORMAL: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana18/hospital_follow_person.world

ENLACE UNIVERSO TELEOPERADO: https://github.com/TheRoboticsClub/2025-upe-alberto-leon/blob/main/code/semana18/hospital_follow_person_teleop.world

FICHERO RoboticsInfrastructure/Launchers/visualization/follow_person.config

En este fichero, habría que realizar un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic, ya que el contenido de todos los ficheros existentes en este directorio y de este formato son exactamente iguales.

<?xml version="1.0"?>
<!-- Quick start dialog -->
<dialog name="quick_start" show_again="false"/>

<!-- Window -->
<window>
<width>1024</width>
<height>768</height>
<style material_theme="Light" material_primary="DeepOrange" material_accent="LightBlue" toolbar_color_light="#ffa726" toolbar_text_color_light="#000000" toolbar_color_dark="#ffa726" toolbar_text_color_dark="#000000" plugin_toolbar_color_light="#bbdefb" plugin_toolbar_text_color_light="#111111" plugin_toolbar_color_dark="#607d8b" plugin_toolbar_text_color_dark="#eeeeee"/>
<menus>
<drawer visible="false">
</drawer>
<plugins visible="false">
</plugins>
</menus>
<dialog_on_exit>false</dialog_on_exit>
</window>

<!-- GUI plugins -->
<!-- 3D scene -->
<plugin filename="MinimalScene" name="3D View">
<gz-gui>
<title>3D View</title>
<property type="bool" key="showTitleBar">false</property>
<property type="string" key="state">docked</property>
</gz-gui>
<engine>ogre2</engine>
<scene>scene</scene>
<ambient_light>0.4 0.4 0.4</ambient_light>
<background_color>0.8 0.8 0.8</background_color>
<camera_pose>-2 0 2 0 0.4 0</camera_pose>
</plugin>

<!-- Plugins that add functionality to the scene -->
<plugin filename="GzSceneManager" name="Scene Manager">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="InteractiveViewControl" name="Interactive view control">
<gz-gui>
<property key="resizable" type="bool">false</property>
<property key="width" type="double">5</property>
<property key="height" type="double">5</property>
<property key="state" type="string">floating</property>
<property key="showTitleBar" type="bool">false</property>
</gz-gui>
</plugin>

<plugin filename="WorldStats" name="World stats">
<gz-gui>
<title>World stats</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">110</property>
<property type="double" key="width">290</property>
<property type="double" key="z">1</property>

<property type="string" key="state">floating</property>
<anchors target="3D View">
<line own="right" target="right"/>
<line own="bottom" target="bottom"/>
</anchors>
</gz-gui>

<sim_time>true</sim_time>
<real_time>true</real_time>
<real_time_factor>true</real_time_factor>
<iterations>true</iterations>
<topic>/world/world_demo/stats</topic>

</plugin>

FICHERO RoboticsInfrastructure/database/universes.sql

En este fichero, habría que identificar el ejercicio que se quiere migrar de Gazebo 11 a Gazebo Harmonic (en este caso, Follow Person) y cambiar el valor de la columna type de gazebo a gz:

# GAZEBO 11
10 Follow Person /opt/jderobot/Launchers/follow_person.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
11 Follow Person Teleop /opt/jderobot/Launchers/follow_person_teleop.launch.py None ROS2 gazebo {0.0,0.0,0.0,0.0,0.0,0.0}
# GAZEBO HARMONIC
10 Follow Person /opt/jderobot/Launchers/follow_person.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_person.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
11 Follow Person Teleop /opt/jderobot/Launchers/follow_person_teleop.launch.py {"gzsim":"/opt/jderobot/Launchers/visualization/follow_person.config"} ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}

Una vez realizados todos estos cambios en todos los ficheros mencionados, he hecho commit y push en una nueva rama que he creado y publicado llamada follow-person-harmonic. Es importante hacer esto siempre para así evitar cualquier tipo de conflicto con la rama principal.

Como todos los cambios se han realizado en ficheros que se encuentran dentro de RoboticsInfrastructure, es obligatorio compilar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

cd scripts/RADI/
./build.sh -i follow-person-harmonic

NOTA: Cada vez que se compile un nuevo RADI no se borrará el anterior, por lo que el espacio disponible en disco se irá llenando y llenando hasta que no quede espacio disponible en disco. Para evitar que esto ocurra, se debe ejecutar el siguiente comando en la terminal, el cual se encargará de borrar todo el espacio de memoria que se ha ido llenando por cada RADI compilado:

docker system prune -af

Una vez finalizada la ejecución del script build.sh, se debe regresar al repositorio principal:

cd ../..

Pero antes de ejecutar el script develop_academy.sh, habría que modificar la siguiente línea del fichero RoboticsAcademy/compose_cfg/dev_humble_cpu.yaml:

# ANTES
robotics-academy:
image: jderobot/robotics-academy:latest

# DESPUÉS
robotics-academy:
image: jderobot/robotics-academy:test

Con este cambio ya realizado, ya se puede lanzar el script develop_academy.sh:

./scripts/develop_academy.sh

Y por último, sólo quedaría acceder a la dirección web `http://0.0.0.0:7164/` que aparece en la terminal al ejecutar el comando anterior para poder entrar a Unibotics en local y verificar que los cambios se han realizado correctamente.

A continuación se muestra cómo se ve el ejercicio Follow Person al lanzar el Docker de RoboticsAcademy con todos estos cambios realizados:

Follow Person Image

Además, para verificar que tanto el escenario como el robot han sido migrados correctamente, se muestra una pequeña animación del robot moviéndose para verificar que todo el proceso se ha realizado correctamente:

SEMANA 19: 26-01-2026 al 30-01-2026

Internship Report Writing

SEMANA 20: 02-02-2026 al 06-02-2026

Internship Report Writing