Salta el contingut

ROS 2 — Robot Operating System 2

Què és ROS 2?

ROS 2 (Robot Operating System 2) no és un sistema operatiu en el sentit convencional: és un middleware de codi obert que proporciona infraestructura de comunicació, eines i biblioteques per al desenvolupament de software robòtic. És l'estàndard de facto en recerca i cada cop més en producció.

ROS 2 és la versió reescrita de ROS 1, amb millores fonamentals: - Temps real: suport per a comunicació hard real-time (DDS - Data Distribution Service) - Multi-robot: dissenyat per a flotes de robots des del principi - Seguretat: autenticació i xifratge de comunicacions - Portabilitat: suporta Linux, Windows i macOS

Arquitectura de ROS 2

graph TD
    subgraph Nodes ROS 2
        N1[Node Càmera\ncamera_node]
        N2[Node Deteccio\nyolo_detector]
        N3[Node Planificacio\npath_planner]
        N4[Node Control\nrobot_controller]
    end

    N1 -- Topic: /camera/image_raw --> N2
    N2 -- Topic: /deteccions/objects --> N3
    N3 -- Topic: /cmd_vel --> N4
    N3 -- Service: /get_plan --> N4
    N4 -- Action: /navigate_to_pose --> N3

Conceptes clau:

Nodes: processsos independents que realitzen una tasca específica. Cada node pot ser en Python o C++.

Topics: canals de comunicació pub/sub asíncrons. Un node publica missatges a un topic i altres nodes es subscriuen. Exemple: el node de la càmera publica imatges al topic /camera/image_raw.

Services: comunicació request-response síncrona. El client envia una petició i espera la resposta. Exemple: servei /get_map que retorna el mapa actual.

Actions: com els serveis però per a tasques llargues, amb feedback periòdic. Exemple: acció /navigate_to_pose que navega a una posició i informa del progrés.

Messages: estructures de dades tipades que viatgen per topics i serveis. ROS 2 defineix centenars de tipus estàndard (imatges, poses, nucs de punts, etc.).

Exemple de node ROS 2 en Python

#!/usr/bin/env python3
"""
Node ROS 2 exemple: detector d'obstacles simplificat
Subscriu a les lectures del làser i publica si hi ha un obstacle proper
"""

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from std_msgs.msg import Bool, String


class DetectorObstacles(Node):
    """Node que detecta obstacles usant un sensor làser (LiDAR)."""

    def __init__(self):
        super().__init__('detector_obstacles')

        # Paràmetre configurable: distància mínima de seguretat (metres)
        self.declare_parameter('distancia_minima', 0.5)
        self.distancia_minima = self.get_parameter('distancia_minima').value

        # Subscriptor: llegeix les dades del LiDAR
        self.subscripcio_laser = self.create_subscription(
            LaserScan,
            '/scan',
            self.callback_laser,
            10  # Longitud de la cua de missatges
        )

        # Publicadors: avís d'obstacle i missatge descriptiu
        self.pub_obstacle = self.create_publisher(Bool, '/obstacle_detectat', 10)
        self.pub_avis = self.create_publisher(String, '/avis_obstacle', 10)

        self.get_logger().info(
            f'Node detector_obstacles iniciat. '
            f'Distancia minima de seguretat: {self.distancia_minima}m'
        )

    def callback_laser(self, msg: LaserScan):
        """S'executa cada vegada que arriben noves dades del làser."""
        # Filtrem les lectures invàlides (inf, nan, 0)
        lectures_valides = [
            r for r in msg.ranges
            if msg.range_min < r < msg.range_max
        ]

        if not lectures_valides:
            return

        distancia_minima_llegida = min(lectures_valides)
        hi_ha_obstacle = distancia_minima_llegida < self.distancia_minima

        # Publiquem si hi ha obstacle
        msg_bool = Bool()
        msg_bool.data = hi_ha_obstacle
        self.pub_obstacle.publish(msg_bool)

        if hi_ha_obstacle:
            msg_avis = String()
            msg_avis.data = (
                f'ALERTA: Obstacle a {distancia_minima_llegida:.2f}m '
                f'(minim: {self.distancia_minima}m)'
            )
            self.pub_avis.publish(msg_avis)
            self.get_logger().warning(msg_avis.data)


def main(args=None):
    rclpy.init(args=args)
    node = DetectorObstacles()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Executar el node amb Docker:

# Córrer ROS 2 Humble amb Docker (sense necessitat d'instal·lar ROS localment)
docker run -it --rm \
  --name ros2-joan-garcia \
  --network host \
  osrf/ros:humble-desktop-full \
  bash

# Dins del contenidor, crear i executar el node
mkdir -p /workspace/src/detector_obstacles/detector_obstacles
# ... (copiar el fitxer Python)
cd /workspace
colcon build
source install/setup.bash
ros2 run detector_obstacles detector_obstacles

Eines de ROS 2

rviz2: visualitzador 3D per a dades robòtiques (mapes, nucs de punts, trajectòries, marcs de referència)

rqt: conjunt d'eines GUI per a depuració i monitorització (gràfics de topics, visualització d'imatges, etc.)

rosbag2: gravació i reproducció de tots els missatges de ROS 2. Indispensable per a depuració i entrenament d'IA.

ros2 launch: arxius de llançament que inicien múltiples nodes simultàniament amb la seva configuració.

Estructura d'un paquet ROS 2

El codi de ROS 2 s'organitza en paquets (packages) dins d'un workspace. Un workspace típic té aquesta estructura:

robot_ws/
└── src/
    └── detector_obstacles/
        ├── package.xml           # Metadades i dependencies del paquet
        ├── setup.py              # Configuracio d'instal-lacio (paquets Python)
        ├── setup.cfg
        ├── resource/
        │   └── detector_obstacles
        ├── launch/
        │   └── detector.launch.py
        └── detector_obstacles/
            ├── __init__.py
            └── detector_node.py  # El node vist més amunt
# Crear un paquet Python nou dins del workspace
cd robot_ws/src
ros2 pkg create --build-type ament_python detector_obstacles \
  --dependencies rclpy sensor_msgs std_msgs

# Compilar tot el workspace
cd ~/robot_ws
colcon build --symlink-install
source install/setup.bash

Services i Actions en Python

Els topics (vistos més amunt) són ideals per a fluxos continus de dades, però quan cal una resposta puntual a una petició concreta (un servei) o una tasca llarga amb feedback (una acció), ROS 2 ofereix mecanismes dedicats.

# Servei ROS 2: retorna si una posicio es segura per navegar-hi
import rclpy
from rclpy.node import Node
from std_srvs.srv import SetBool


class ServeiValidacioPosicio(Node):
    def __init__(self):
        super().__init__('validacio_posicio')
        self.srv = self.create_service(
            SetBool, '/validar_posicio', self.callback_validar
        )

    def callback_validar(self, request, response):
        # request.data: True si es vol activar la validacio estricta
        response.success = True
        response.message = 'Posicio validada correctament' if request.data else 'Validacio desactivada'
        return response


def main(args=None):
    rclpy.init(args=args)
    rclpy.spin(ServeiValidacioPosicio())
    rclpy.shutdown()
# Cridar el servei des de la terminal, sense escriure cap client
ros2 service call /validar_posicio std_srvs/srv/SetBool "{data: true}"

Les actions afegeixen feedback periòdic i la possibilitat de cancel·lar tasques llargues (per exemple, /navigate_to_pose, que informa del percentatge de trajecte completat mentre el robot es desplaça). Es defineixen amb tres missatges: Goal (objectiu), Feedback (progrés) i Result (resultat final).

Launch files i paràmetres

Un launch file permet arrencar diversos nodes alhora amb la seva configuració, en lloc d'obrir una terminal per node:

# launch/detector.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package='detector_obstacles',
            executable='detector_node',
            name='detector_obstacles',
            parameters=[{'distancia_minima': 0.4}],
            output='screen',
        ),
        Node(
            package='detector_obstacles',
            executable='camera_node',
            name='camera_node',
        ),
    ])
ros2 launch detector_obstacles detector.launch.py

Miniactivitat — AC5071/04/03 — Paquet ROS 2 propi

Crea un paquet ROS 2 nou (ros2 pkg create) amb un node que publiqui un missatge propi cada segon (per exemple, la temperatura simulada d'un sensor). Afegeix-hi un servei que permeti activar/desactivar les publicacions, i un launch file que arrenqui el node amb un paràmetre configurable. Comprova amb ros2 topic echo i ros2 service call que tot funciona.