Skip to content

Parte 5: Actions

Introducción

Ejercicios: 6 (5 básicos, 1 avanzado)
Tiempo Estimado de Finalización: 3 horas (solo ejercicios básicos)
Nivel de Dificultad: Avanzado

Objetivos

En esta parte del curso aprenderemos sobre un tercer método de comunicación disponible en ROS: los Actions. Los Actions son esencialmente una versión avanzada de los Services, y veremos exactamente en qué difieren estos dos y por qué podrías elegir emplear un action en lugar de un service para ciertas tareas.

Resultados de Aprendizaje Esperados

Al finalizar esta sesión podrás:

  1. Reconocer cómo los ROS Actions difieren de los ROS Services y explicar dónde este método podría ser útil en aplicaciones robóticas.
  2. Explicar la estructura de las interfaces de Action e identificar la información relevante dentro de ellas, permitiéndote construir Action Servers y Clients.
  3. Implementar nodes Python Action Client que puedan manejar feedback y results y también puedan cancelar un action a mitad de camino.
  4. Desarrollar nodes Action Server y Client que podrían usarse como base para una estrategia de exploración robótica.

Enlaces Rápidos

Ejercicios

Recursos Adicionales

Primeros Pasos

Paso 1: Lanza tu Entorno de ROS

Lanza tu entorno de ROS ahora para que tengas acceso a una instancia de terminal Linux (también conocida como TERMINAL 1).

Paso 2: Restaura tu trabajo (Solo para Usuarios de Computadoras del Laboratorio con WSL-ROS2)

Recuerda que cualquier trabajo que realices dentro del Entorno WSL-ROS2 no se conservará entre sesiones ni en diferentes computadoras del laboratorio, por lo que debes hacer una copia de seguridad de tu trabajo en tu unidad U:\ regularmente. Cuando se te solicite (al primer lanzamiento de WSL-ROS2 en TERMINAL 1) ingresa Y para restaurarlo1.

Paso 3: Lanza VS Code

Usuarios de WSL recuerda verificar esto.

Paso 4: Asegúrate de que el Repositorio del Curso esté Actualizado

Verifica que el Repositorio del Curso esté actualizado antes de comenzar con estos ejercicios. Consulta aquí cómo instalar y/o actualizar.

Llamar a un Action Server

Antes de hablar sobre qué son realmente los actions, vamos a ir directo al grano y ver uno en action (perdona el juego de palabras).

Ejercicio 1: Lanzar un Action Server y llamarlo desde la línea de comandos

Vamos a jugar un pequeño juego aquí. Vamos a lanzar el robot de simulación en un entorno misterioso ahora, y lo haremos lanzando Gazebo en modo headless, es decir, Gazebo se ejecutará en segundo plano, pero no habrá Interfaz Gráfica de Usuario (GUI) para mostrarnos cómo es realmente el entorno. Luego, usaremos un action server para hacer que nuestro robot escanee el entorno y tome fotos por nosotros, ¡para revelar su entorno!

  1. Para lanzar el robot de simulación en este entorno misterioso, usa el siguiente comando ros2 launch en TERMINAL 1:

    ros2 launch rigsa_simulations mystery_world.launch.py
    

    ... ¡aparentemente no ocurrirá nada (por ahora)!

  2. A continuación, abre una nueva terminal (TERMINAL 2) y echa un vistazo a todos los topics que están actualmente activos en la red de ROS (¡ya deberías saber exactamente cómo hacer esto!).

    La salida de esto debería confirmar que ROS y nuestro robot están efectivamente activos...

    ¿Cómo?

    Cuando el robot está activo, la salida del comando ros2 topic list debería proporcionar una larga lista de topics, varios de los cuales hemos estado trabajando a lo largo de este curso, como cmd_vel, odom, scan, y así sucesivamente. Si la simulación del robot no está activa, se nos presentaría una lista mucho más pequeña, que contiene solo los topics básicos de ROS:


    TERMINAL 2:

    $ ros2 topic list
    /parameter_events
    /rosout
    


  3. A continuación (aún en TERMINAL 2), ejecuta el siguiente comando para lanzar un Action Server en la red:

    ros2 run rigsa_examples camera_sweep_action_server.py
    
  4. Ahora, abre otra nueva instancia de terminal (TERMINAL 3), de manera que puedas ver esta y la TERMINAL 2 lado a lado. Ingresa el siguiente comando para listar todos los actions que están activos en la red de ROS:

    ros2 action list
    

    Debería haber un elemento aquí llamado /camera_sweep. Usa el comando info para obtener más información sobre este:

    ros2 action info /camera_sweep
    

    Esto nos indica el nombre del action: Action: /camera_sweep, así como el número de nodes client y server que tiene este action. Actualmente, el action debería tener 0 clients y 1 server, y el node que actúa como servidor aquí debería aparecer como /camera_sweep_action_server_node (el node que acabamos de lanzar con el comando ros2 run en TERMINAL 2).

    Finalmente, llama al comando ros2 action info nuevamente, pero esta vez proporcionando un argumento adicional:

    ros2 action info -t /camera_sweep
    
    Action: /camera_sweep
    Action clients: 0
    Action servers: 1
        /camera_sweep_action_server [rigsa_interfaces/action/CameraSweep]
    

    El argumento -t muestra adicionalmente el tipo de action frente al node servidor, indicándonos el tipo de interface utilizada por el servidor.

  5. Ahora busquemos más información sobre la interface en sí. Al igual que con cualquier interface (mensaje, service o action) podemos usar el comando ros2 interface para esto. En TERMINAL 3 ingresa lo siguiente:

    ros2 interface show rigsa_interfaces/action/CameraSweep
    

    Lo cual debería presentarnos lo siguiente:

    
    

    Hay tres partes en una interface de action, y hablaremos sobre estas con más detalle en breve, pero por ahora, todo lo que necesitamos saber es que para llamar a un action, necesitamos enviar al action server un Goal.

    Comparación con ROS Services

    Esto es un poco como enviar una Request a un ROS Service Server, como hicimos en la sesión anterior.

  6. Podemos emitir un goal a un action server desde la línea de comandos usando el comando ros2 action nuevamente. Vamos a intentarlo en TERMINAL 3.

    Primero, identifiquemos el sub-comando correcto de ros2 action:

    ros2 action --help
    
    Commands:
      info       Print information about an action
      list       Output a list of action names
      send_goal  Send an action goal
    

    Como se muestra arriba, hay tres sub-comandos para elegir, ¡y ya hemos usado los dos primeros! Claramente, el comando send_goal es el que necesitamos ahora.

    Obtengamos ayuda sobre este:

    ros2 action send_goal --help
    

    A partir de esto, aprendemos que hay tres argumentos posicionales, que deben suministrarse en el orden correcto:

    ros2 action send_goal <action_name> <action_type> <goal>
    

    Sabemos por nuestra investigación anterior con los comandos ros2 action list, info y ros2 interface show cómo proporcionar los datos correctos aquí:

    1. action_name: /camera_sweep
    2. action_type: rigsa_interfaces/action/CameraSweep
    3. goal: un paquete de datos (en formato YAML) que contiene dos parámetros:
      1. sweep_angle: el ángulo (en grados) que el robot rotará en su lugar (es decir, el 'barrido')
      2. image_count: el número de imágenes que capturará desde su cámara frontal mientras realiza el 'barrido'
  7. Ahora, de nuevo en TERMINAL 3, intenta usar el comando ros2 action send_goal, pero vigila la TERMINAL 2 mientras lo haces:

    ros2 action send_goal /camera_sweep rigsa_interfaces/action/CameraSweep \
        "{sweep_angle: 0, image_count: 0}"
    

    Después de llamar al action, se te debería presentar un mensaje (en TERMINAL 3) indicando que el Goal was rejected. En TERMINAL 2 (donde se está ejecutando el action server), deberíamos ver información adicional sobre por qué fue así. Lee esto y luego regresa a TERMINAL 3 e intenta de nuevo enviar un goal al action server, ¡esta vez suministrando entradas válidas!

    Una vez que se hayan suministrado parámetros de goal válidos, el action server (en TERMINAL 2) responderá para informarte de lo que va a hacer. Luego deberás esperar a que complete su tarea...

  8. Una vez que el action haya completado (podría tardar hasta 20 segundos), debería aparecer un mensaje en TERMINAL 3 para informarnos del resultado:

    Result:
        image_paths: 
    - ~/ros_action_examples/img01.jpg
    - ~/ros_action_examples/img02.jpg
    - ~/ros_action_examples/img03.jpg
    - ...
    
    Goal finished with status: SUCCEEDED
    

    Adicionalmente, también deberíamos ver algo más de texto en TERMINAL 2:

    [INFO] [#####] [camera_sweep_action_server_node]: camera_sweep_action_server_node completed successfully:
      - Angular sweep = # degrees
      - Images captured = #
      - Time taken = # seconds
    
    1. El result del action (presentado en TERMINAL 3) es una serie de rutas de archivos, que ilustran las imágenes que han sido capturadas y dónde se han guardado en el sistema de archivos. Navega a este directorio en TERMINAL 3 (usando cd) y echa un vistazo al contenido usando ll (un alias útil para el comando ls):

      cd ~/ros_action_examples
      
      ll
      

      Deberías ver el mismo número de archivos de imagen que el solicitado con el parámetro image_count.

    2. Lanza eog en este directorio y navega por todas las imágenes para revelar el entorno misterioso de tu robot:

      eog .
      
  9. Hagamos esto una vez más. Cierra la ventana de eog, regresa a TERMINAL 3 y emite el comando ros2 action send_goal nuevamente, pero esta vez usa el flag opcional -f:

    ros2 action send_goal -f /camera_sweep rigsa_interfaces/action/CameraSweep \
        "{sweep_angle: 0, image_count: 0}" 
    

    Tip

    ¡No olvides suministrar nuevamente parámetros de goal válidos!

    Ahora, además de recibir un result una vez que el action haya completado, ¡también recibimos algunas actualizaciones regulares mientras el action está en progreso (también conocido como "feedback")!

  10. Para terminar, cierra el action server en TERMINAL 2 y el proceso de Gazebo headless en TERMINAL 1 ingresando Ctrl+C en cada terminal.

¿Qué es un ROS Action?

En el ejercicio anterior lanzamos un action server y luego lo llamamos desde la línea de comandos usando el sub-comando ros2 action send_goal. De la misma manera que un ROS Service, el action también nos proporcionó un result una vez que la tarea se completó.

Usando el flag -f pudimos pedir al servidor que nos proporcionara feedback en tiempo real sobre cómo iba (en TERMINAL 3). El Feedback es una de las características clave que diferencia a un ROS Action de un ROS Service: un Action Server proporciona feedback a intervalos regulares mientras trabaja hacia su goal. Otra característica de los ROS Actions es que pueden cancelarse a mitad de camino (con lo que jugaremos en breve).

En última instancia, los Actions usan una combinación de comunicación basada tanto en Topics como en Services, para crear un protocolo de mensajería más avanzado. Basándose en los ROS Services, los Actions están diseñados para ser usados en tareas de larga duración, debido a la provisión de feedback y la capacidad de cancelar un proceso a mitad de camino. Puedes leer más sobre Actions en la documentación oficial de ROS 2 aquí (que también incluye una animación muy buena para explicar cómo funcionan).

El Formato de las Interfaces de Action

Al igual que los Services, las Interfaces de Action tienen múltiples partes, y necesitamos saber qué formato tienen estos mensajes de action para poder usarlos.

Ejecutamos ros2 interface show en el ejercicio anterior, para interrogar la interface de action utilizada por el action server /camera_sweep:

ros2 interface show rigsa_interfaces/action/CameraSweep

Como sabemos del Ejercicio 1, para llamar a este action server necesitamos enviar un goal, y (en este caso) hay dos parámetros de goal que deben proporcionarse:

  1. sweep_angle: un valor de punto flotante de 32 bits
  2. image_count: un entero de 32 bits

Preguntas

  • ¿Cuáles son los nombres de los parámetros de interface de result y feedback? (Hay tres en total.)
  • ¿Qué tipos de datos usan estos parámetros?

Aprenderás cómo usamos esta información para desarrollar nodes Python Action Server y Client en los siguientes ejercicios.

Creación de Python Action Clients

En el ejercicio anterior llamamos a un Action Server preexistente desde la línea de comandos, enviándole un goal. Veamos ahora cómo podemos hacer esto desde dentro de un node Python de ROS.

Ejercicio 2: Construir un Node Python Action Client

  1. En TERMINAL 1 lanza la simulación del mundo misterioso nuevamente, pero esta vez con un argumento adicional:

    ros2 launch rigsa_simulations mystery_world.launch.py with_gui:=true
    

    Con el switch with_gui configurado en true, ahora deberíamos poder ver realmente el mundo simulado esta vez.

    (¡No es muy emocionante que digamos!)

  2. Luego, en TERMINAL 2, lanza el Camera Sweep Action Server nuevamente:

    ros2 run rigsa_examples camera_sweep_action_server.py
    

Parte 1: Un Action Client Mínimo

  1. Ahora, en TERMINAL 3, crea un nuevo paquete llamado part5_actions usando el enfoque que hemos estado usando para crear paquetes a lo largo de este curso.

  2. Navega a la carpeta scripts de tu paquete usando el comando cd:

    cd ~/ros2_ws/src/part5_actions/scripts/
    
  3. Aquí, crea un nuevo archivo Python llamado camera_sweep_action_client.py (usando el comando touch) y hazlo ejecutable (usando chmod).

  4. Luego, declara esto como un ejecutable en el CMakeLists.txt del paquete:

    CMakeLists.txt
    # Install Python executables
    install(PROGRAMS
      scripts/basic_velocity_control.py
      scripts/stop_me.py
      scripts/camera_sweep_action_client.py
      DESTINATION lib/${PROJECT_NAME}
    )
    
  5. Como mínimo absoluto, el Action Client puede construirse de la siguiente manera:

    El Node camera_sweep_action_client.py

  6. Construyamos el Node ahora, para poder ejecutarlo. Regresa a TERMINAL 3 y usa Colcon para construir el paquete:

    cd ~/ros2_ws/ 
    
    colcon build --packages-select part5_actions --symlink-install
    
  7. Vuelve a cargar el .bashrc:

    source ~/.bashrc
    
  8. Ejecuta tu node con ros2 run:

    ros2 run part5_actions camera_sweep_action_client.py
    

    Con el comando anterior tal como está, el servidor rechazará el goal (vuelve a TERMINAL 2 para averiguar por qué). ¿Cómo puedes modificar el comando anterior (agregando algunos argumentos adicionales), para que el node camera_sweep_action_client.py envíe realmente un goal válido?2

  9. Habiendo modificado el comando ros2 run para enviar exitosamente un goal válido al action server camera_sweep, el robot debería comenzar a girar, capturando imágenes a medida que avanza.

    El Client que hemos creado aquí hace una llamada al action server y luego termina, ¡ni siquiera espera a que el servidor complete su tarea! La única manera de saber si el action fue exitoso sería vigilar TERMINAL 2, para ver al server del action responder al goal que se le envió... El client en sí no proporciona feedback durante el action, ni el result al final. Veamos cómo incorporar eso ahora...

Parte 2: Manejo de un Result

  1. Regresa al archivo camera_sweep_action_client.py en VS Code.

  2. Para poder manejar el result que se envía desde un action server, primero necesitamos manejar la respuesta que el servidor envía al goal en sí.

    Dentro del método send_goal() de la clase CameraSweepActionClient(), encuentra la línea que dice:

    return self.actionclient.send_goal_async(goal)
    

    y cámbiala a:

    self.send_goal_future = self.actionclient.send_goal_async(goal)
    self.send_goal_future.add_done_callback(self.goal_response_callback)
    

    Este método ya no retorna el future que se envía desde send_goal_async(), sino que ahora lo maneja y le agrega un callback: goal_response_callback. Este callback ahora puede usarse para informar al client de si el servidor ha aceptado el goal o no.

  3. Por lo tanto, necesitamos definir este callback ahora. Defínelo como un nuevo método de clase de la clase CameraSweepActionClient() (es decir, debajo del método de clase send_goal() que ya ha sido definido)...

    def goal_response_callback(self, future):
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().warn("The goal was rejected by the server.")
            return
    
        self.get_logger().info("The goal was accepted by the server.")
    
        self.get_result_future = goal_handle.get_result_async()
        self.get_result_future.add_done_callback(self.get_result_callback)
    

    La entrada a este método será el future que crea la llamada send_goal_async(). Lo asignamos a un atributo llamado goal_handle aquí, y podemos usarlo para dos propósitos:

    1. Verificar si el goal que enviamos fue aceptado por el servidor.
    2. Si fue aceptado, entonces podemos obtener el result (usando get_result_async()) y podemos adjuntar otro callback para procesar realmente ese result: get_result_callback.
  4. Ahora también necesitamos definir este callback. Define get_result_callback como otro nuevo método de la clase CameraSweepActionClient() (es decir, debajo del método de clase goal_response_callback() que acabamos de definir)...

    def get_result_callback(self, future):
        result = future.result().result
        self.get_logger().info(
            f"The action has completed.\n"
            f"Result (Image Paths):\n  "
            + "\n  ".join(result.image_paths)
        )
        rclpy.shutdown()
    

    La entrada a este método de clase es otro objeto future que contiene el result real enviado desde el servidor. Lo asignamos a result y usamos una llamada get_logger().info() para imprimirlo en el terminal cuando el action haya finalizado.

    Como sabemos de nuestro trabajo anterior, la interface de Action CameraSweep contiene un parámetro de Result llamado image_paths.

  5. Finalmente, en el método main, cambia esto:

    future = action_client.send_goal()
    
    a solo esto:
    action_client.send_goal()
    
    (porque send_goal() ya no retorna un future)

  6. Y luego también cambia:

    rclpy.spin_until_future_complete(action_client, future)
    

    a esto:

    rclpy.spin(action_client)
    
  7. ¡Guarda todos tus cambios!

  8. Ejecuta este node nuevamente ahora (con el comando ros2 run) y observa los cambios en acción.

    Nuestro node client ahora nos presenta el result que envía el servidor al completarse el action, pero ¿no sería bueno poder ver el feedback en tiempo real mientras el action tiene lugar? Agreguemos esto ahora...

Parte 3: Manejo de Feedback

  1. Regresa al archivo camera_sweep_action_client.py en VS Code.

  2. Para poder manejar el feedback que se envía desde un action server, ¡necesitamos agregar otro callback! Regresa al método send_goal() y a la línea donde realmente estamos enviando el goal al servidor:

    self.send_goal_future = self.actionclient.send_goal_async(goal)
    

    Tal como está, todo lo que hacemos aquí es enviar el goal, pero también podemos agregar un feedback callback a esto:

    self.send_goal_future = self.actionclient.send_goal_async(
        goal=goal, 
        feedback_callback=self.feedback_callback
    )
    

    El feedback_callback se ejecutará cada vez que se reciba un nuevo mensaje de feedback del servidor.

  3. Para definir qué queremos hacer con estos mensajes de feedback, necesitamos agregar otro nuevo método a la clase CameraSweepActionClient(). Debajo del método de clase get_result_callback() que definimos antes, agrega también este nuevo:

    def feedback_callback(self, feedback_msg):
        feedback = feedback_msg.feedback
        fdbk_current_angle = feedback.current_angle
        fdbk_current_image = feedback.current_image
        self.get_logger().info(
            f"\nFEEDBACK:\n"
            f"  - Current angular position = {fdbk_current_angle:.1f} degrees.\n"
            f"  - Image(s) captured so far = {fdbk_current_image}."
        )
    

    Como sabemos de nuestro trabajo anterior, la interface CameraSweep contiene dos parámetros de feedback: current_angle y current_image.

  4. Guarda todos tus cambios nuevamente, ejecuta el node nuevamente con el comando ros2 run y observa los cambios en acción.

    El node que hemos construido ahora puede enviar un goal a un action server, procesar el feedback devuelto por el servidor mientras el action está en progreso, y presentarnos el result una vez que todo esté completo.

    Como se discutió anteriormente, la otra característica clave de los Actions es la capacidad de cancelarlos a mitad de camino. Veamos también cómo incorporar esto ahora.

Parte 4: Cancelar un Action

  1. Primero, crea una copia de tu node camera_sweep_action_client.py y llámala cancel_camera_sweep.py. Asegúrate de que TERMINAL 3 esté ubicada en el directorio scripts de tu paquete part5_actions antes de ejecutar lo siguiente:

    cp camera_sweep_action_client.py cancel_camera_sweep.py
    
  2. No olvides declarar esto como un ejecutable adicional en el CMakeLists.txt del paquete. También necesitarás reconstruir el paquete con colcon build (regresa para un recordatorio).

  3. Abre el archivo cancel_camera_sweep.py en VS Code.

  4. Queremos que este client pueda cancelar el goal en dos circunstancias diferentes:

    1. El client en sí es apagado por el usuario (mediante un Ctrl+C en el terminal).
    2. Un evento condicional que ocurre mientras el action está en curso.

    Para abordar el punto 1 primero, necesitamos recurrir a parte del trabajo que hicimos en la Parte 2 en la implementación de procedimientos de apagado seguro...

  5. Como esperamos que recuerdes, primero necesitamos importar SignalHandlerOptions en nuestro node, así que agrega esto como una importación adicional al comienzo del código:

    from rclpy.signals import SignalHandlerOptions
    

    Luego, en la función del node main(), modifica la llamada rclpy.init():

    rclpy.init(
        args=args,
        signal_handler_options=SignalHandlerOptions.NO
    )
    
  6. Dentro del método __init__() de nuestra clase CameraSweepActionClient() ahora necesitamos agregar algunos flags adicionales:

    self.goal_succeeded = False
    self.goal_cancelled = False
    self.stop = False
    

    En el método de clase get_result_callback(), podemos entonces asegurarnos de que el flag self.goal_succeeded se establezca en True cuando se recibe un result. En este método de clase, localiza la línea rclpy.shutdown() y agrega la siguiente línea adicional justo encima de ella:

    self.goal_succeeded = True
    
  7. Los Actions pueden cancelarse usando un método cancel_goal_async() del goal_handle que se obtiene del goal_response_callback(). Por ello, necesitamos hacerlo accesible en toda nuestra clase CameraSweepActionClient(). Localiza el método de clase goal_response_callback() y agrega esta línea al final como la última línea del método goal_response_callback():

    self.goal_handle = goal_handle
    

    Esto hace que goal_handle sea accesible en toda la clase CameraSweepActionClient() como self.goal_handle.

  8. Solo podemos intentar cancelar un Action cuando está en progreso, por lo tanto, el feedback callback es el mejor lugar para activar esto. Localiza el método de clase feedback_callback() y coloca lo siguiente al final de él:

    if self.stop:
        future = self.goal_handle.cancel_goal_async()
        future.add_done_callback(self.cancel_goal)
    

    Aquí, llamamos al método cancel_goal_async() desde self.goal_handle y agregamos otro nuevo callback (cancel_goal()) a él (es decir, para encapsular lo que queremos que suceda cuando el action sea cancelado).

  9. Ahora, definamos esto como otro nuevo (¡y final!) método de clase:

    def cancel_goal(self, future):
        cancel_response = future.result()
        if len(cancel_response.goals_canceling) > 0:
            self.get_logger().info('Goal successfully canceled')
            self.goal_cancelled = True
        else:
            self.get_logger().info('Goal failed to cancel')
    

    La entrada a este callback es otro future, que podemos usar para determinar si el goal ha sido cancelado (como se muestra arriba). Si lo ha sido, entonces establecemos nuestro flag self.goal_cancelled en True.

  10. Finalmente, regresa a la función main() del node. Vamos a reemplazar la línea rclpy.spin(action_client) ahora, con un rclpy.spin_once(), envuelto dentro de un try - except, ¡envuelto dentro de un loop while!

    while not action_client.goal_succeeded:
        try:
            rclpy.spin_once(action_client)
            if action_client.goal_cancelled:
                break
        except KeyboardInterrupt:
            print("Ctrl+C")
            action_client.stop = True
    

    El loop while se ejecutará hasta que el action complete exitosamente, o hasta que el goal sea cancelado, o apaguemos el node con una interrupción Ctrl+C.

    Revisa el node para ver cómo todo esto fluirá a través de tu clase.

  11. Es posible que deseemos cancelar un goal condicionalmente si, por ejemplo, ha transcurrido demasiado tiempo desde que se realizó la llamada, o si el caller ha sido informado de algo más que ha ocurrido mientras tanto (¡quizás nos estamos quedando sin espacio de almacenamiento en el robot y no podemos guardar más imágenes!).

    Para los propósitos de este ejercicio, queremos modificar nuestro node para que el action siempre sea cancelado después de que se hayan capturado un total de 5 imágenes. Esto puede hacerse realizando una modificación bastante pequeña al feedback_callback(). Intenta implementar esto ahora.

    • Intenta ahora introducir una llamada condicional al método cancel_goal() una vez que se hayan capturado un total de 5 imágenes.
    • Podrías usar el atributo current_image del mensaje CameraSweepFeedback para activar esto.

Un Resumen de los ROS Actions

Los ROS Actions funcionan de manera muy similar a los ROS Services, pero tienen las siguientes diferencias clave:

  1. Pueden cancelarse: Si algo tarda demasiado, o si ha ocurrido algo más, entonces un Action Client puede cancelar un Action cuando lo necesite.
  2. Proporcionan feedback: para que un client pueda monitorear lo que está sucediendo y actuar en consecuencia (es decir, cancelar el action, si es necesario).

Por lo tanto, este mecanismo es útil para operaciones que pueden tardar mucho en ejecutarse, y donde podría ser necesaria una intervención.

Creación de tus Propios Action Servers, Clients e Interfaces

Importante

Cancela todos los procesos activos que puedas tener en ejecución antes de continuar.

Hasta ahora hemos visto cómo llamar a un action server preexistente, pero ¿qué pasa si realmente queremos configurar el nuestro propio, y también usar nuestras propias Interfaces de Action personalizadas?

Para empezar, echa un vistazo al Action Server con el que has estado trabajando en los ejercicios anteriores. Has estado lanzándolo con el siguiente comando:

ros2 run rigsa_examples camera_sweep_action_server.py

Preguntas

  • ¿A qué paquete pertenece el node del action server?
  • ¿Dónde (en el directorio de ese paquete) es probable que esté ubicado este node?

Una vez que hayas identificado el nombre y la ubicación del código fuente, ábrelo en VS Code y revísalo para ver cómo funciona todo. No te preocupes demasiado por todo el contenido relacionado con la obtención y manipulación de imágenes de cámara, aprenderemos más sobre esto en la siguiente parte de este curso. En cambio, enfócate en la estructura general del código y en la manera en que se implementa el action server.

Algunas cosas a revisar:

  1. La manera en que el servidor se inicializa y los numerosos callbacks que se le adjuntan:

    self.actionserver = ActionServer(
        node=self, 
        action_type=CameraSweep,
        action_name="camera_sweep",
        execute_callback=self.server_execution_callback, # (1)!
        callback_group=ReentrantCallbackGroup(), # (2)!
        goal_callback=self.goal_callback, # (3)!
        cancel_callback=self.cancel_callback # (4)!
    )
    
    1. Este callback contiene todo el código que ejecutará el servidor una vez que se le envíe un goal válido (es decir, la funcionalidad principal del Action).
    2. Este node servidor se configura como un Executor Multi-hilo (consulta la configuración en main()), para controlar la ejecución de los diversos callbacks que necesitamos. Aquí, asignamos el Action Server a un Reentrant Callback Group, permitiendo que todos sus callbacks se ejecuten en paralelo, así como otros callbacks de subscriber también.
    3. Este callback se usa para verificar los parámetros del goal que se han enviado al servidor, para decidir si aceptar o rechazar una solicitud.
    4. Este callback contiene todo lo que necesita suceder en caso de que el Action sea cancelado a mitad de camino.
  2. Echa un vistazo a los diversos callbacks de Action para ver qué sucede en cada uno:

    1. ¿Cómo se verifican los parámetros de goal y, consecuentemente, se aceptan o rechazan?
    2. ¿Cómo se implementan las cancelaciones y cómo se monitorea esto en el server_execution_callback() principal?
    3. ¿Cómo se maneja y publica el feedback?
    4. ¿Cómo se maneja y publica también el result?
  3. Finalmente, considera las operaciones de apagado.

Implementación de una Estrategia de "Exploración" usando el Marco de Actions

Una estrategia de exploración permite a un robot navegar autónomamente por un entorno desconocido mientras simultáneamente evita colisiones. Una manera de lograr esto es utilizar dos estados de movimiento distintos: moverse hacia adelante y girar en su lugar, alternando repetidamente entre ellos. El Movimiento Browniano y el Levy Flight son ejemplos de este tipo de enfoque. Aleatorizar el tiempo pasado en uno o ambos de estos dos estados resultará en una estrategia de navegación que permite al robot explorar lenta y aleatoriamente un entorno. El movimiento hacia adelante podría realizarse hasta que, por ejemplo, se haya recorrido cierta distancia, haya transcurrido un tiempo determinado o algo se interponga en el camino. De manera similar, la dirección, velocidad y/o duración del giro también podría aleatorizarse para lograr esto.

En los próximos ejercicios construiremos nuestra propia Interface de Action, y nodes Server y Client con el objetivo de crear la base para un comportamiento de exploración básico. Habiendo completado estos ejercicios tendrás algo que, con mayor desarrollo, podría convertirse en un tipo de comportamiento de exploración como el descrito arriba.

Ejercicio 3: Crear una Interface de Action

En el Ejercicio 2 creamos el paquete part5_actions. Dentro de este paquete ahora definiremos una Interface de Action para ser usada por el Action Server y Client subsiguientes que crearemos en los ejercicios posteriores.

  1. Las interfaces de action deben definirse dentro de una carpeta action en la raíz del directorio del paquete, así que vamos a crearla ahora en TERMINAL 1:

    1. Primero, navega a tu paquete:

      cd ~/ros2_ws/src/part5_actions
      
    2. Luego usa mkdir para crear un nuevo directorio:

      mkdir action
      
  2. Aquí ahora definiremos una Interface de Action llamada ExploreForward:

    touch action/ExploreForward.action
    
  3. Ábrela en VS Code y define la estructura de datos de la Interface de la siguiente manera:

    ExploreForward.action
    #goal
    float32 fwd_velocity               # The speed at which the robot should move forwards (m/s)
    float32 stopping_distance          # Minimum distance of an approaching obstacle before the robot stops (meters) 
    ---
    #result
    float32 total_distance_travelled   # Total distance travelled during the action (meters)
    float32 closest_obstacle           # LaserScan distance to the closest detected obstacle up ahead (meters)
    ---
    #feedback
    float32 current_distance_travelled # Distance travelled so far during the action (meters)
    

    Esta interface tiene por lo tanto dos parámetros de goal, uno de feedback y dos de result:

    Goal:

    1. fwd_velocity: La velocidad (en m/s) a la que el robot debe moverse hacia adelante cuando se llama al action server.
    2. stopping_distance: La distancia (en metros) a la que el robot debe detenerse frente a cualquier objeto o pared límite que esté delante de él (estos datos provendrán de los datos LaserScan del sensor LiDAR del robot).

    Feedback:

    1. current_distance_travelled: La distancia recorrida (en metros) desde que se inició el action actual (basada en datos de Odometry).

    Result:

    1. total_distance_travelled: La distancia total recorrida (en metros) durante el transcurso del action (basada en datos de Odometry).
    2. closest_obstacle: La distancia al obstáculo más cercano por delante del robot cuando el action se completa.
  4. Ahora, declara esta interface en el archivo CMakeLists.txt del paquete, para que se pueda crear el código Python necesario:

    CMakeLists.txt
    find_package(rosidl_default_generators REQUIRED)
    rosidl_generate_interfaces(${PROJECT_NAME}
      "action/ExploreForward.action" 
    )
    
  5. También modifica el archivo package.xml (encima de la línea <export>) para incluir las dependencias rosidl necesarias:

    package.xml
    <buildtool_depend>rosidl_default_generators</buildtool_depend>
    <exec_depend>rosidl_default_runtime</exec_depend>
    <member_of_group>rosidl_interface_packages</member_of_group>
    
  6. Ahora ejecuta colcon para generar el código fuente necesario para esta nueva interface:

    1. Primero, siempre asegúrate de estar en la raíz del Workspace:

      cd ~/ros2_ws/
      
    2. Luego ejecuta colcon build:

      colcon build --packages-select part5_actions --symlink-install 
      
    3. Y finalmente vuelve a cargar el .bashrc:

      source ~/.bashrc
      
  7. Ahora podemos verificar que funcionó.

    1. Usa ros2 interface para listar todos los tipos de interface disponibles, pero usa la opción -a para mostrar solo las interfaces de tipo action:

      ros2 interface list -a
      

      Busca un mensaje con el prefijo "part5_actions".

    2. A continuación, muestra la estructura de datos de la interface:

      ros2 interface show part5_actions/action/ExploreForward
      

      Esto debería coincidir con el contenido del archivo part5_actions/action/ExploreForward.action que creamos arriba.

Ejercicio 4: Construir el Action Server "ExploreForward"

  1. En TERMINAL 1 navega a la carpeta scripts de tu paquete part5_actions, luego:

    1. Crea un script Python llamado explore_server.py
    2. Hazlo ejecutable
    3. Decláralo como un ejecutable en CMakeLists.txt
  2. Abre el archivo en VS Code.

  3. La tarea del node Server es la siguiente:

    • El action server debe hacer que el robot se mueva hacia adelante hasta detectar un obstáculo por delante.
    • Debe usar la Interface ExploreForward.action que creamos en el ejercicio anterior.
    • El Server deberá configurarse para aceptar dos parámetros de goal:

      1. La velocidad (en m/s) a la que el robot debe moverse hacia adelante cuando se llama al action server.
      2. La distancia (en metros) a la que el robot debe detenerse frente a cualquier objeto o pared límite que esté delante de él.

        Para hacer esto necesitarás suscribirte al topic /scan. Ten en cuenta que un objeto no estará necesariamente directamente frente al robot, por lo que puede que necesites monitorear un rango de puntos de datos de LaserScan (dentro del arreglo ranges) para que la evasión de colisiones sea efectiva (recuerda el ejemplo de callback de LaserScan de la Parte 3).

    • Asegúrate de hacer algunas verificaciones de errores en los parámetros del goal para garantizar que se realice una solicitud válida. Esto se hace adjuntando un goal_callback al Action Server.

    • Mientras tu servidor realiza su tarea debe proporcionar el siguiente feedback a un Client:

      1. La distancia recorrida (en metros) desde que se inició el action actual.

        Para hacer esto necesitarás suscribirte al topic /odom. Recuerda el trabajo que hiciste en la Parte 2 sobre esto.

        Consejos

        • La orientación del robot no debería cambiar durante el transcurso de una sola llamada al action, solo sus posiciones linear.x y linear.y deberían variar.
        • Ten en cuenta, sin embargo, que el robot no necesariamente se moverá a lo largo del eje X o Y, por lo que necesitarás considerar la distancia total recorrida en el plano X-Y.
        • Hicimos esto en el ejercicio move_square de la Parte 2, así que consúltalo si necesitas un recordatorio.
    • Finalmente, al completarse el action, tu servidor debe estar configurado para proporcionar los siguientes dos parámetros de result:

      1. La distancia total recorrida (en metros) durante el transcurso del action.
      2. La distancia al obstáculo que hizo que el robot se detuviera (si el action server ha hecho su trabajo correctamente, esto debería ser muy similar al stopping_distance que proporcionó el Action Client en el goal).
  4. Hay código de plantilla para ayudarte con esto:

    La Plantilla explore_server.py

    También puede que quieras echar un vistazo al código de camera_sweep_action_server.py de los ejercicios anteriores para ayudarte a construir esto: muchas de las técnicas usadas aquí serán similares (excluyendo todo lo relacionado con la cámara).

Pruebas

Cuando quieras probar las cosas, ¿por qué no usar el entorno Nav World de la Parte 3 (en TERMINAL 1)?:

ros2 launch rigsa_simulations nav_world.launch.py

No olvides que para lanzar el servidor necesitarás haber construido todo con colcon siguiendo el proceso habitual de tres etapas.

Haz esto en TERMINAL 2 y luego podrás ejecutarlo:

ros2 run part5_actions explore_server.py

Además, no olvides que no necesitas haber desarrollado un Node Python Client para probar el servidor. Usa la herramienta CLI ros2 action send_goal para hacer llamadas al servidor (como lo hicimos en el Ejercicio 1).

Ejercicio 5: Construir un Client Básico "ExploreForward"

  1. En TERMINAL 3 navega a la carpeta scripts de tu paquete part5_actions, crea un script Python llamado explore_client.py, hazlo ejecutable y agrégalo a tu CMakeLists.txt.

  2. Ejecuta colcon build ahora, para que no tengas que preocuparte por ello más tarde (de nuevo, siguiendo el proceso de tres etapas como arriba)...

    Aquí está de nuevo, por si acaso:

    1. Paso 1:

      cd ~/ros2_ws
      
    2. Paso 2:

      colcon build --packages-select part5_actions --symlink-install
      
    3. Paso 3:

      source ~/.bashrc
      
  3. Abre el archivo explore_client.py en VS Code.

  4. La tarea del Action Client es la siguiente:

    • El client necesita emitir un goal correctamente formateado al servidor.
    • El client debe programarse para monitorear los datos de feedback del Server. Si detecta (a partir del feedback) que el robot ha recorrido una distancia mayor de 2 metros sin detectar un obstáculo, entonces debe cancelar la llamada al action actual.
  5. Usa las técnicas que usamos en el node Client del Ejercicio 2 como guía para ayudarte con esto.

  6. Una vez que tengas todo en su lugar, lanza el action client con ros2 run como se muestra a continuación:

    ros2 run part5_actions explore_client.py
    

    Si todo está bien, este node client debería llamar al action server, que (a su vez) hará que el robot se mueva hacia adelante hasta que esté a cierta distancia de un obstáculo por delante (stopping_distance), momento en el que el robot se detendrá, y tu node client también debería detenerse. Una vez que esto suceda, reorienta tu robot (usando el node teleop_keyboard) y lanza el node client nuevamente para asegurarte de que esté deteniéndose de manera robusta frente a los obstáculos repetidamente, y cuando se aproxime desde diferentes ángulos.

    Importante

    Asegúrate de que tu funcionalidad de cancelación también funcione correctamente, garantizando que:

    1. El robot nunca se mueva más de 2 metros durante una llamada al action determinada.
    2. Un action se aborta a mitad de camino si el node client se apaga con Ctrl+C.

Ejercicio 6 (Avanzado): Implementar una Estrategia de Exploración

Hasta ahora, tu node Action Client debería tener la capacidad de llamar al servidor ExploreForward.action para hacer que el robot se mueva hacia adelante 2 metros, o hasta que llegue a un obstáculo (lo que ocurra primero), pero podrías construir sobre esto ahora y convertirlo en un comportamiento de exploración completo:

  • Entre llamadas al action, tu node client podría hacer que el robot gire en su lugar para enfrentar una dirección diferente y luego emitir otra llamada al action para hacer que el robot se mueva hacia adelante nuevamente.
  • El proceso de giro podría hacerse aleatoriamente (idealmente), o en una cantidad fija cada vez.
  • Programando tu node client para repetir este proceso una y otra vez, el robot (de manera algo aleatoria) recorrería su entorno de forma segura, deteniéndose antes de chocar con cualquier obstáculo y reorientándose cada vez que deje de moverse hacia adelante.

Conclusión

En la Parte 5 de este curso has aprendido:

  • Cómo funcionan los ROS Actions y por qué pueden ser útiles.
  • Cómo desarrollar Action Client Nodes en Python que pueden monitorear el action en tiempo real (a través de feedback), y que también pueden cancelar el action solicitado, si es necesario.
  • Cómo usar las herramientas estándar de ROS para interrogar interfaces de action, permitiéndonos así construir clients para llamarlas.
  • Cómo construir action servers, clients e interfaces personalizados.
  • Cómo aprovechar este marco para implementar una estrategia de exploración.

Topics, Services o Actions: ¿Cuál Elegir?

Ahora deberías haber desarrollado una buena comprensión de los tres métodos de comunicación disponibles dentro de ROS para facilitar la comunicación entre ROS Nodes:

  1. Mensajería basada en Topics.
  2. ROS Services.
  3. ROS Actions.

A través de este curso has adquirido algo de experiencia práctica usando los tres, pero puede que aún te preguntes cómo seleccionar el apropiado para cierta tarea robótica...

Esta página web de ROS.org resume todo esto muy bien (y brevemente), así que deberías leerla para asegurarte de saber qué es qué. En resumen:

  • Topics: Son más apropiados para transmitir flujos de datos continuos como datos de sensores e información del estado del robot, y para publicar datos que probablemente sean requeridos por una variedad de Nodes en una red de ROS.
  • Services: Son más apropiados para procedimientos muy cortos como cálculos rápidos (cinemática inversa, etc.) y para realizar acciones discretas cortas que es poco probable que salgan mal o que no necesitarán intervención (p. ej., encender un LED de advertencia cuando la batería está baja).
  • Actions: Son más apropiados para tareas de larga duración (como mover un robot), o para operaciones donde podríamos necesitar cambiar de opinión y hacer algo diferente o cancelar un comportamiento invocado a mitad de camino.

¡Guarda tu trabajo!

Sandbox del Laboratorio (rigsa.io)

Tu código se guarda en /home/student/ros2_ws/src dentro de la sesión. Antes de cerrar la sesión, descárgalo para no perderlo:

  1. En VS Code, haz clic derecho sobre la carpeta src en el explorador de archivos → Descargar…
  2. O desde la terminal integrada:
    zip -r ~/mi_codigo_parte5.zip ~/ros2_ws/src && echo "¡Listo! ✓"
    
    Luego descarga el archivo mi_codigo_parte5.zip desde el explorador de VS Code.

WSL-ROS2 (Computadoras del laboratorio)

Ejecuta el siguiente script en cualquier instancia de Terminal WSL-ROS2 inactiva:

wsl_ros backup

Luego podrás restaurarlo en un entorno WSL-ROS2 nuevo la próxima vez que lo inicies (wsl_ros restore).


  1. Recuerda: también puedes usar el comando wsl_ros restore en cualquier momento. ↩

  2. Necesitas establecer explícitamente los parámetros goal_images y goal_angle en tiempo de ejecución, de manera similar a como lo hicimos aquí con el service client number_game_client.py. ↩