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.

A continuación, se muestran todas las entradas correspondientes a los blogs de prácticas realizados durante el transcurso de las mismas, cuyo contenido se divide en las 20 semanas que ha durado mi periodo de Prácticas Externas.

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, 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 intenta configurar el entorno de desarrollo. Al ejecutar este script por primera vez, aparecen varios errores, ya que faltan dependencias por instalar (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 que se ejecutan a continuación se encargan 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

IMPORTANTE: Hay que ejecutar el comando con sudo.

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>

IMPORTANTE: Cada vez que se genera un nuevo RADI no se borra el anterior, por lo que el espacio disponible en disco se irá llenando y llenando cada vez que se genere un nuevo RADI 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 generado:

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 (sin sudo) 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.

IMPORTANTE: En el caso de que los cambios se hayan realizado únicamente en el fichero database/universes.sql, sólo sería necesario realizar el último de todos los pasos que componen este bucle de trabajo, escrito a continuación.

./scripts/develop_academy.sh

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

IMPORTANTE: 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

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

IMPORTANTE: Este comando se ejecuta ÚNICAMENTE la primera vez que se lance el RoboticsDatabase junto a RoboticsAcademy.

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

IMPORTANTE: 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

Por último, 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 02: 29-09-2025 al 03-10-2025

Ejercicio de calentamiento: Cambiar el mundo a un ejercicio

Para ir familiarizándome con el software de RoboticsInfrastructure, se me ha pedido como primer ejercicio cambiar el mundo 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 mundos ya migrados a Gazebo Harmonic.

IMPORTANTE: La forma más sencilla de identificar los mundos 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 mundo todavía se encuentra en Gazebo 11 y no se ha migrado. Pero si aparece escrito gz, significa que ese mundo ya se ha migrado a Gazebo Harmonic.

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

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 mundos correspondientes a los ejercicios de Laser Mapping (número 12) y Marker Based Visual Loc (número 25). El mundo del Laser Mapping es un almacén tipo Amazon, mientras que el mundo 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 la ventana 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 (MUNDO ORIGINAL)
world_file_name = "marker_visual_loc.world"

# DESPUÉS (MUNDO 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 generar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

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

IMPORTANTE: Cada vez que se genera un nuevo RADI no se borra el anterior, por lo que el espacio disponible en disco se irá llenando y llenando cada vez que se genere un nuevo RADI 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 generado:

docker system prune -af

IMPORTANTE: 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, hay 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 03: 06-10-2025 al 10-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

ENLACE AL ENUNCIADO DEL EJERCICIO: https://jderobot.github.io/RoboticsAcademy/exercises/AutonomousCars/obstacle_avoidance

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 diff-drive-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, sólo se añaden los topics correspondientes al publicador y al subscriptor del plugin del diff-drive-system y aquellos relativos al publicador del plugin del láser:

# 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

En este caso, 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 sólamente habría que 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, 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, lo único que habría que hacer es 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 mundo 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

IMPORTANTE: 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, lo único que habría que realizar es un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todas 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, lo único que habría que realizar es 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

Este es uno de los cambios más sencillos de realizar en todo el proceso de migración. Lo único que habría que hacer es 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 generar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

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

IMPORTANTE: Cada vez que se genera un nuevo RADI no se borra el anterior, por lo que el espacio disponible en disco se irá llenando y llenando cada vez que se genere un nuevo RADI 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 generado:

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, hay 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 escenario 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 04: 13-10-2025 al 17-10-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

ENLACE AL ENUNCIADO DEL EJERCICIO: https://jderobot.github.io/RoboticsAcademy/exercises/AutonomousCars/global_navigation/

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:

# 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

En este caso, 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, 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 sólamente habría que 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, 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, lo único que habría que hacer es 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 mundo 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

IMPORTANTE: 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, lo único que habría que realizar es un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todas 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="taxi_navigation_city_large_harmonic">
<physics name="4ms" type="ignored">
<max_step_size>0.004</max_step_size>
<real_time_factor>1.0</real_time_factor>
</physics>
<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>

<gui fullscreen="0">
<!-- 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.0 1.0 1.0</ambient_light>
<background_color>0.4 0.6 1.0</background_color>
<camera_pose>53.5 -8.0 1.5 0 0 -1.57</camera_pose>
</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="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>

<!-- World control -->
<plugin filename="WorldControl" name="World control">
<gz-gui>
<title>World control</title>
<property type="bool" key="showTitleBar">false</property>
<property type="bool" key="resizable">false</property>
<property type="double" key="height">72</property>
<property type="double" key="z">1</property>

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

<play_pause>true</play_pause>
<step>true</step>
<start_paused>true</start_paused>
<use_event>true</use_event>

</plugin>
<camera name='user_camera'>
<pose>0 0 80 3.14 1.5529944 0</pose>
<view_controller>orbit</view_controller>
<projection_type>perspective</projection_type>
</camera>
</gui>

<light type="directional"
name="sun">
<cast_shadows>true</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>
<attenuation>
<range>1000</range>
<constant>0.9</constant>
<linear>0.01</linear>
<quadratic>0.001</quadratic>
</attenuation>
<direction>-0.5 0.1 -0.9</direction>
</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>1000 1000</size>
</plane>
</geometry>
</collision>
<visual name="visual">
<geometry>
<plane>
<normal>0 0 1</normal>
<size>1000 1000</size>
</plane>
</geometry>
<material>
<ambient>0.1 0.1 0.1 1</ambient>
<diffuse>0.1 0.1 0.1 1</diffuse>
<specular>0.1 0.1 0.1 1</specular>
</material>
</visual>
</link>
</model>
<!-- My city -->
<include>
<pose>0 0 -0.5 0 0 3.14</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/database/universes.sql

Este es uno de los cambios más sencillos de realizar en todo el proceso de migración. Lo único que habría que hacer es 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 None 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 generar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

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

IMPORTANTE: Cada vez que se genera un nuevo RADI no se borra el anterior, por lo que el espacio disponible en disco se irá llenando y llenando cada vez que se genere un nuevo RADI 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 generado:

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, hay 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 el escenario 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 05: 20-10-2025 al 24-10-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

ENLACE AL ENUNCIADO DEL EJERCICIO: https://jderobot.github.io/RoboticsAcademy/exercises/AutonomousCars/autoparking/

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.

IMPORTANTE: 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/images/semana05/model.sdf

FICHERO RoboticsInfrastructure/CustomRobots/autopark_harmonic/params/prius_autoparking_3laser_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 al plugin subscriptor del DiffDrive, y los plugin publicadores del DiffDrive y de los 3 sensores láser LIDAR.

# 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/worldspara 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, 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 sólamente habría que 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, lo único que habría que hacer es 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 mundo 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

IMPORTANTE: 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, lo único que habría que realizar es un Ctrl+C Ctrl+V de cualquier fichero con el mismo formato de nombre que ya haya sido migrado a Gazebo Harmonic y sustituir todas 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="autopark">
<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>

<!-- Carretera -->
<include>

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

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

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

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

<include>
<uri>model://prius_autoparking_3laser_harmonic</uri>
<pose>17 -2 0.1 0 -0.1 1.55</pose>
</include>

<include>
<uri>model://opel</uri>
<name>opel_1</name>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_line.world -->
<pose>-11 2 0 1.55 0 -1.55</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_battery.world -->
<pose>1 0.8 0 1.55 0 0</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_sideways.world -->
<pose>1 0.8 0 1.55 0 0.35</pose>

<static>false</static>
</include>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_line.world (COMENTAR OPEL_2) -->
<include>
<uri>model://opel</uri>
<name>opel_2</name>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_line.world -->
<pose>-4 1 0 1.55 0 -1.55</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_battery.world -->
<pose>-3 0.8 0 1.55 0 0</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_sideways.world -->
<pose>-3 0.8 0 1.55 0 0.35</pose>

<static>false</static>
</include>

<include>
<uri>model://opel</uri>
<name>opel_3</name>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_line.world -->
<pose>3 2 0 1.55 0 -1.55</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_battery.world -->
<pose>-11.2 0.8 0 1.55 0 0</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_sideways.world -->
<pose>-11.2 0.8 0 1.55 0 0.35</pose>

<static>false</static>
</include>

<include>
<uri>model://opel</uri>
<name>opel_4</name>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_line.world -->
<pose>10 2 0 1.55 0 -1.55</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_battery.world -->
<pose>9.3 0.8 0 1.55 0 0</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_sideways.world -->
<pose>9.3 0.8 0 1.55 0 0.35</pose>

<static>false</static>
</include>

<include>
<uri>model://opel</uri>
<name>opel_5</name>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_line.world -->
<pose>17 2 0 1.55 0 -1.55</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_battery.world -->
<pose>13.5 0.8 0 1.55 0 0</pose>

<!-- FICHERO RoboticsInfrastructure/Worlds/autopark_sideways.world -->
<pose>13.5 0.8 0 1.55 0 0.35</pose>

<static>false</static>
</include>

<include>
<uri>model://lamp_post</uri>
<name>farola_0</name>
<pose>20 4.2 0 0 0 0</pose>
<static>false</static>
</include>

<include>
<uri>model://lamp_post</uri>
<name>farola_1</name>
<pose>15 4.2 0 0 0 0</pose>
<static>false</static>
</include>

<include>
<uri>model://lamp_post</uri>
<name>farola_2</name>
<pose>10 4.2 0 0 0 0</pose>
<static>false</static>
</include>

<include>
<uri>model://lamp_post</uri>
<name>farola_3</name>
<pose>5 4.2 0 0 0 0</pose>
<static>false</static>
</include>

<include>
<uri>model://lamp_post</uri>
<name>farola_4</name>
<pose>0 4.2 0 0 0 0</pose>
<static>false</static>
</include>

<include>
<uri>model://lamp_post</uri>
<name>farola_5</name>
<pose>-5 4.2 0 0 0 0</pose>
<static>false</static>
</include>

<include>
<uri>model://lamp_post</uri>
<name>farola_6</name>
<pose>-10 4.2 0 0 0 0</pose>
<static>false</static>
</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/database/universes.sql

Este es uno de los cambios más sencillos de realizar en todo el proceso de migración. Lo único que habría que hacer es 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 None ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
40 Autopark_battery /opt/jderobot/Launchers/autopark_battery.launch.py None ROS2 gz {0.0,0.0,0.0,0.0,0.0,0.0}
41 Autopark_sideways /opt/jderobot/Launchers/autopark_sideways.launch.py None 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 generar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

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

IMPORTANTE: Cada vez que se genera un nuevo RADI no se borra el anterior, por lo que el espacio disponible en disco se irá llenando y llenando cada vez que se genere un nuevo RADI 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 generado:

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, hay 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 el escenario 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 06: 27-10-2025 al 31-10-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

ENLACE AL ENUNCIADO DEL EJERCICIO: https://jderobot.github.io/RoboticsAcademy/exercises/AutonomousCars/follow_line/

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/<robot_model_name>/models/<robot_model_name>/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:

CODE

FICHERO RoboticsInfrastructure/CustomRobots/<robot_model_name>/params/<robot_model_name>.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 plugin subscriptor del DiffDrive, y los plugin publicadores del DiffDrive, del sensor láser LIDAR y de la cámara.

CODE

FICHERO RoboticsInfrastructure/CustomRobots/CMakeLists.txt

En este caso, la única modificación realizada en este fichero es la adición de las líneas [...], para que el fichero que se acaba de crear (<robot_model_name>.yaml), se tenga en cuenta a la hora de lanzar el Docker:

CODE

FICHERO RoboticsInfrastructure/Launchers/<exercise_name>/spawn_robot.launch.py

Una vez creado el directorio <exercise_name>/, 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 sólamente habría que 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, <robot_model_name>.yaml):

CODE

FICHERO RoboticsInfrastructure/Launchers/<robot_model_name>.launch.py

Para este fichero, lo único que habría que hacer es 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 mundo que utiliza este ejercicio.

CODE

IMPORTANTE: En caso de que no se haya creado dentro del directorio <exercise_name>/ 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/<world_name>.world

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

CODE

FICHERO RoboticsInfrastructure/database/universes.sql

Este es uno de los cambios más sencillos de realizar en todo el proceso de migración. Lo único que habría que hacer es 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:

CODE

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 generar un nuevo RADI. Para ello, se deben ejecutar los siguientes comandos en la terminal:

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

IMPORTANTE: Cada vez que se genera un nuevo RADI no se borra el anterior, por lo que el espacio disponible en disco se irá llenando y llenando cada vez que se genere un nuevo RADI 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 generado:

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, hay 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 FOLLOW LINE

Además, para verificar que tanto el escenario 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 / GIF COCHE MOVIÉNDOSE POR EL ESCENARIO

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

...

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

...

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

...

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

...

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

...

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

...

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

...

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

...

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

...

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

...

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

...

SEMANA 18: 19-01-2026 al 23-01-2026

...

SEMANA 19: 26-01-2026 al 30-01-2026

...

SEMANA 20: 02-02-2026 al 06-02-2026

...