Servicios ROS2

ROS2. Aprende cómo usar servicios con Python

1 Introducción

Además de mediante topics, otra forma de hacer que los nodos se comuniquen entre sí es mediante el uso de servicios. Los servicios están basados en el modelo cliente-servidor según el cual un nodo (cliente) envía una petición a otro nodo (servidor) que responde proporcionado el resultado de la solicitud.

La comunicación entre el cliente y el servidor puede realizarse de formas:

 

      • Síncrona. El cliente, tras enviar la petición, detiene su ejecución hasta recibir la respuesta.
      • Asíncrona. El cliente no detiene su ejecución cuando envía la petición al servidor.
Tanto la solicitud como la respuesta están definidas en un archivo con extensión .srv que tiene una estructura como la siguiente:
int64 a
int64 b
---
int64 sum

Donde cada línea indica un dato que se deben pasar o que devuelve el servicio. Siendo los que se encuentran antes de  “–  –  –”  los correspondientes a la petición y los que se encuentran después los asociados a la respuesta. Para cada uno de los datos se indica su tipo y el nombre.

La estructura mostrada forma parte de un servicio definido en el paquete example_interfaces con el nombre AddTwoInts, el cual recibe dos valores enteros (a y b) y devuelve el resultado de su suma (sum).

En este artículo vamos a generar un nuevo paquete python que llamaremos py_srvcli en el cual crearemos un nodo servidor que va a implementar este servicio y un nodo cliente que realizará las peticiones.

2 Generando el paquete python

Para crear el paquete vamos a la carpeta src de nuestro espacio de trabajo y ejecutamos el siguiente comando:

ros2 pkg create --build-type ament_python py_srvcli --dependencies rclpy example_interfaces

Cómo en el comando anterior ya hemos indicado las dependencias estas ya se incluyen automáticamente en el archivo package.xml del paquete por lo que no es necesario editarlo para añadirlas manualmente.

Si quieres conocer los argumentos que se pueden usar a la hora de generar un paquete puedes consultar el artículo “ROS2. Cómo crear un paquete Python”.

3 Creando el nodo servidor que proporciona el servicio

Para ello, en nuestro paquete dentro de la carpeta que tiene su mismo nombre, creamos un archivo al que vamos a llamar server_node.py:

cd py_srvcli/py_srvcli
touch server_node.py

Abrimos el archivo y pegamos una de las dos versiones de código que se muestran a continuación. La diferencia entre ellas es que en una se ha utilizado programación orientada a objetos y en la otra no.

El código se encuentra explicado en los comentarios.

3.1 Usando una función local

# Importa el tipo de servicio AddTwoInts del paquete example_interface. 
from example_interfaces.srv import AddTwoInts

# Importa la biblioteca cliente Python de ROS 2.
import rclpy

# Crea una variable global que se usará más adelante para almacenar la 
# instancia del nodo.
node = None

# Define la función que se ejecuta al recibir una petición y que devuelve
# la respuesta del servicio.
def add_two_ints_callback(request, response):
    global node

    # Asigna el resultado a la respuesta del servicio.
    response.sum = request.a + request.b

    # Muestra en la consola la información de la petición recibida.
    node.get_logger().info(
        'Incoming request\na: %d b: %d' % (request.a, request.b))

    # Devuelve la respuesta del servicio.
    return response


def main(args=None):
    global node

    # Inicializa la librería Python de ROS2.
    rclpy.init(args=args)

    # Crea el nodo que va a proporcionar el servicio.
    node = rclpy.create_node('minimal_service')

    # Crea el servicio y define el tipo, su nombre y el de la función que se
    # ejecutara al recibir una petición.
    srv = node.create_service(AddTwoInts, 'add_two_ints', add_two_ints_callback)

    # Mantiene corriendo el bucle interno del nodo para poder gestionar las
    # peticiones del cliente.
    rclpy.spin(node)

    # Destruye explícitamente el servicio. Es opcional ya que el recolector de
    # basura realiza esta acción cuandop destruye el objeto nodo.
    node.destroy_service(srv)

    # Destruye el nodo.
    node.destroy_node()

    rclpy.shutdown()


if __name__ == '__main__':
    main()

3.2 Usando una clase

# Importa el tipo de servicio AddTwoInts del paquete example_interface. 
from example_interfaces.srv import AddTwoInts

# Importa la biblioteca cliente Python de ROS 2 y, específicamente, la clase Node.
import rclpy
from rclpy.node import Node


class MinimalService(Node):

    # El constructor de la clase inicializa el nodo con el nombre service_node.
    # Luego, crea un servicio y define el tipo, su nombre y el de la función que
    # se ejecutará al recibir una petición.
    def __init__(self):
        super().__init__('service_node')
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

    # Define la función que se ejecuta al recibir una petición y que devuelve
    # la respuesta del servicio.
    def add_two_ints_callback(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))

        return response


def main(args=None):

    # Inicializa la librería Python de ROS2.
    rclpy.init(args=args)

    # Crea una instancia de la clase.
    node = MinimalService()

    # Mantiene corriendo el bucle interno del nodo para poder gestionar las
    # peticiones del cliente.
    rclpy.spin(node)

    rclpy.shutdown()


if __name__ == '__main__':
    main()

4 Creando el nodo cliente

Creamos mediante el siguiente comando, en la misma ubicación,  el archivo client_node.py que va a contener el código de nuestro nodo cliente. 

touch client_node.py

Abrimos el archivo y al igual que hemos hecho con el nodo servidor pegamos una de las versiones de código que se muestran a continuación, en función de que queramos realizar la petición al servicio de forma síncrona o asíncrona y de que queramos usar una clase o no.

El código se encuentra explicado en los comentarios.

4.1 Cliente con comunicación síncrona

En este caso el cliente realiza la petición al servicio y su ejecución se detiene hasta recibir la respuesta.

Hay que destacar que, aunque el objetivo de este apartado es ver un ejemplo de cliente con comunicación síncrona con el servidor, las dos versiones de código que se muestran a continuación realmente realizan una llamada asíncrona al servicio mediante .call_async() y luego esperan a recibir la respuesta mediante .spin_until_future_complete().

Aunque rclpy tiene la función .call() para realizar una verdadera llamada síncrona al servicio su uso puede causar interbloqueos, por ejemplo con rclpy.spin() si este último no se lanza en un hilo diferente. Es por ello que se recomienda el uso de .call_async(), que es totalmente seguro.

Si de todas formas estas interesado en ver cómo usar .call() sin provocar interbloqueos puedes echarle un ojo a Synchronous vs. asynchronous service clients en la documentación de ROS2 Humble Hawksbill.

4.1.1 Sin usar clases
# Importa el tipo de servicio AddTwoInts del paquete example_interface. 
from example_interfaces.srv import AddTwoInts

# Importa la libreria Python de ROS 2.
import rclpy


def main(args=None):

    # Inicializa la librería Python de ROS2.
    rclpy.init(args=args)

    # Crea el nodo.
    node = rclpy.create_node('minimal_client')

    # Crea un cliente y define el tipo, su nombre.
    cli = node.create_client(AddTwoInts, 'add_two_ints')

    # Crea una variable de tipo petición y almacena en ella la información
    # que se proporcionará al servicio.
    req = AddTwoInts.Request()
    req.a = 41
    req.b = 1

    # Espera a que el servicio esté disponible.
    while not cli.wait_for_service(timeout_sec=1.0):
        node.get_logger().info('service not available, waiting again...')

    # Envía la petición al servicio.
    future = cli.call_async(req)

    # Espera hasta que el servicio haya proporcionado la respuesta.
    rclpy.spin_until_future_complete(node, future)

    # Obtiene la información de la respuesta.
    result = future.result()

    # Muestra en la consola los valores suministrados al servicio
    # y el resultado recibido como respuesta.
    node.get_logger().info(
        'Result of add_two_ints: for %d + %d = %d' %
        (req.a, req.b, result.sum))

    # Destruye explícitamente el servicio. Es opcional ya que el recolector de
    # basura realiza esta acción cuandop destruye el objeto nodo.
    node.destroy_service(srv)

    # Destruye el nodo.
    node.destroy_node()

    rclpy.shutdown()


if __name__ == '__main__':
    main()
4.1.1 Usando una clase
# Importa el tipo de servicio AddTwoInts del paquete example_interface. 
from example_interfaces.srv import AddTwoInts

# Importa la libreria Python de ROS 2 y, específicamente, la clase node.
import rclpy
from rclpy.node import Node

# Definimos la clase que va a crear el nodo y contiene la función que realizará
# la petición al servicio.
class MinimalClient(Node):

    # El constructor de la clase inicializa el nodo con el nombre minimal_client_async.
    # Luego, crea un servicio y define el tipo, su nombre y el de la función que
    # se ejecutará al recibir una petición. Y finalmente espera a que el servicio
    # esté disponible.
    def __init__(self):
        # El constructor de la clase inicializa el nodo y le asigna el nombre 
        # minimal_client_async.
        super().__init__('minimal_client_async')

        # Crea un servicio y define el tipo, su nombre y el de la función que
        # se ejecutará al recibir una petición.
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')

        # Espera a que el servicio esté disponible.
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')

        # Crea la variable que almacenará los datos que se enviarán en la petición
        # al servicio.
        self.req = AddTwoInts.Request()

    # Define la función que realiza la petición al servicio.
    def send_request(self):
        self.req.a = 41
        self.req.b = 1
        self.future = self.cli.call_async(self.req)


def main(args=None):

    # Inicializa la librería Python de ROS2.
    rclpy.init(args=args)

    # Crea el nodo.
    node = MinimalClient()

    # Realiza la petición al servicio.
    node.send_request()

    # Espera hasta que el servicio haya proporcionado la respuesta.
    rclpy.spin_until_future_complete(node, node.future)

    # Lee la información de la respuesta.
    result = node.future.result()

    # Muestra la información en la consola.
    node.get_logger().info(
        'Result of add_two_ints: for %d + %d = %d' %
        (node.req.a, node.req.b, result.sum))


    # Destruye el nodo.
    node.destroy_node()
    
    rclpy.shutdown()


if __name__ == '__main__':
    main()

4.2 Cliente con comunicación asíncrona

En este caso el cliente realiza la petición al servicio pero no detiene su ejecución, por lo que es necesario en el código crear un bucle y comprobar que se ha recibido la respuesta usando .done() antes de acceder al resultado con .result().
4.2.1 Sin usar clases
# Importa el tipo de servicio AddTwoInts del paquete example_interface. 
from example_interfaces.srv import AddTwoInts

# Importa la libreria Python de ROS 2.
import rclpy


def main(args=None):

    # Inicializa la librería Python de ROS2.
    rclpy.init(args=args)

    # Crea el nodo.
    node = rclpy.create_node('minimal_client_async')

    # Crea un cliente y define el tipo, su nombre.
    cli = node.create_client(AddTwoInts, 'add_two_ints')

    # Crea una variable de tipo petición y almacena en ella la información
    # que se proporcionará al servicio.
    req = AddTwoInts.Request()
    req.a = 41
    req.b = 1

    # Espera a que el servicio esté disponible.
    while not cli.wait_for_service(timeout_sec=1.0):
        node.get_logger().info('service not available, waiting again...')

    # Envía la petición al servicio.
    future = cli.call_async(req)

    # Bucle infinito hasta que se reciba la respuesta del servicio.
    while rclpy.ok():

        # Ejecuta una sola vez el bucle interno del nodo, actualizando los 
        # buffers de entrada (llegada de topics, respuestas de servicios...).  
        rclpy.spin_once(node)

        # Comprueba si el servicio ha proporcionado la respuesta.
        if future.done():

            # Lee la información de la respuesta.
            result = future.result()

            # Muestra la información en la consola.
            node.get_logger().info(
                'Result of add_two_ints: for %d + %d = %d' %
                (req.a, req.b, result.sum))

            # Interrumpe el bucle.
            break


    # Destruye explícitamente el servicio. Es opcional ya que el recolector de
    # basura realiza esta acción cuandop destruye el objeto nodo.
    node.destroy_service(srv)

    # Destruye el nodo.
    node.destroy_node()

    rclpy.shutdown()


if __name__ == '__main__':
    main()
4.2.2 Usando una clase
# Importa el tipo de servicio AddTwoInts del paquete example_interface. 
from example_interfaces.srv import AddTwoInts

# Importa la libreria Python de ROS 2 y, específicamente, la clase node.
import rclpy
from rclpy.node import Node

# Definimos la clase que va a crear el nodo y contiene la función que realizará
# la petición al servicio.
class MinimalClientAsync(Node):

    # El constructor de la clase inicializa el nodo con el nombre minimal_client_async.
    # Luego, crea un servicio y define el tipo, su nombre y el de la función que
    # se ejecutará al recibir una petición. Y finalmente espera a que el servicio
    # esté disponible.
    def __init__(self):
        # El constructor de la clase inicializa el nodo y le asigna el nombre 
        # minimal_client_async.
        super().__init__('minimal_client_async')

        # Crea un servicio y define el tipo, su nombre y el de la función que
        # se ejecutará al recibir una petición.
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')

        # Espera a que el servicio esté disponible.
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')

        # Crea la variable que almacenará los datos que se enviarán en la petición
        # al servicio.
        self.req = AddTwoInts.Request()

    # Define la función que realiza la petición al servicio.
    def send_request(self):
        self.req.a = 41
        self.req.b = 1
        self.future = self.cli.call_async(self.req)


def main(args=None):

    # Inicializa la librería Python de ROS2.
    rclpy.init(args=args)

    # Crea el nodo.
    node = MinimalClientAsync()

    # Realiza la petición al servicio.
    node.send_request()

    # Bucle infinito hasta que se reciba la respuesta del servicio.
    while rclpy.ok():

        # Ejecuta una sola vez el bucle interno del nodo, actualizando los 
        # buffers de entrada (llegada de topics, respuestas de servicios...). 
        rclpy.spin_once(node)

        # Comprueba si el servicio ha proporcionado la respuesta.
        if node.future.done():

            # Lee la información de la respuesta.
            result = node.future.result()

            # Muestra la información en la consola.
            node.get_logger().info(
                'Result of add_two_ints: for %d + %d = %d' %
                (node.req.a, node.req.b, result.sum))

            # Interrumpe el bucle.
            break

    # Destruye el nodo.
    node.destroy_node()
    
    rclpy.shutdown()


if __name__ == '__main__':
    main()

5 Incorporando los puntos de entrada en setup.py

El último paso es editar el archivo setup.py para añadir al array entry_points los puntos de entrada para de los scripts de los nodos servidor y cliente, de modo que puedan ejecutarse mediante ros2 run

from setuptools import setup

package_name = 'py_srvcli'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='ros',
    maintainer_email='TODO@gmail.com',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
              'server = py_srvcli.server_node:main',
              'client = py_srvcli.client_node:main',
        ],
    },
)

6 Ejecutando los nodos

Una vez que ya tenemos preparado nuestro paquete lo compilamos desde el directorio raíz de nuestro espacio de trabajo:

colcon build --packages-select py_srvcli
. install/setup.bash

Seguidamente ejecutamos el nodo servidor mediante el siguiente comando:

ros2 run py_srvcli server

Y finalmente abrimos otro terminal y ejecutamos el nodo cliente:

ros2 run py_srvcli client

Si hemos seguido los pasos correctamente deberíamos ver en un terminal los datos enviados por el cliente y en el otro la respuesta del servidor.

Servicios - Salida por consola del nodo servidor
Servicios - Salida por consola del nodo cliente
Comparte este artículo:

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *