Parte 4: Services
Introducción¶
Ejercicios: 6
Tiempo estimado de finalización: 2 horas
Nivel de dificultad: Intermedio
Objetivos¶
En esta parte aprenderás sobre los Services: un método de comunicación alternativo que se puede usar para transmitir datos/información o invocar acciones en una red ROS. Aprenderás cómo funciona esto y por qué puede ser útil. También explorarás algunas aplicaciones prácticas de esto.
Resultados de Aprendizaje Esperados¶
Al finalizar esta sesión serás capaz de:
- Reconocer cómo los ROS Services difieren del enfoque estándar de publisher-subscriber basado en topics, e identificar casos de uso apropiados para este tipo de sistema de mensajería.
- Identificar los services disponibles en una red ROS, y usar las herramientas de línea de comandos de ROS para interrogarlos y llamarlos.
- Desarrollar Nodos Python de tipo Service Client.
- Invocar diferentes services usando diversas interfaces de tipo service.
Enlaces Rápidos¶
Ejercicios¶
- Ejercicio 1: Usar Herramientas de Línea de Comandos para Interrogar un Service y su Interface
- Ejercicio 2: Jugar el Juego de Números (desde la Línea de Comandos)
- Ejercicio 3: Crear una Interface de Service
- Ejercicio 4: Adaptar el Server del Juego de Números
- Ejercicio 5: Crear un Python Service Client
- Ejercicio 6: Desarrollar un Map Saver Service Client
Recursos Adicionales¶
Primeros Pasos¶
Paso 1: Iniciar tu Entorno ROS2
Si aún no lo has hecho, inicia tu entorno ROS ahora. Una vez hecho esto, deberías tener acceso a una instancia de terminal de Linux (también conocida como TERMINAL 1).
Paso 2: Restaurar tu trabajo (Solo para Usuarios de Escritorio Administrado 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 copias de seguridad de tu trabajo en tu unidad U:\ regularmente. Cuando se te solicite (al primer inicio de WSL-ROS2 en TERMINAL 1) ingresa Y+Enter para restaurar tus datos1.
It looks like you already have a backup from a previous session:
U:\wsl-ros\ros2-backup-XXX.tar.gz
Do you want to restore this now? [y/n]
Paso 3: Iniciar VS Code
También vale la pena iniciar VS Code ahora.
Usuarios de WSL...
Es importante iniciar VS Code dentro de tu entorno ROS usando la extensión "WSL". Recuerda siempre verificar esto:
Paso 4: Asegurarse de que el Repositorio del Curso Esté Actualizado
Una vez más, vale la pena verificar rápidamente que el Repositorio del Curso esté actualizado antes de comenzar con los ejercicios de la Parte 4. Vuelve a la Parte 1 si aún no lo has instalado (¿en serio?!) o, alternativamente, consulta aquí cómo actualizarlo.
Introducción a los Services¶
Hasta ahora, hemos aprendido sobre los topics de ROS y las interfaces de tipo message que usamos para transmitir datos en ellos. También hemos aprendido cómo los nodos individuales pueden acceder a datos de un robot simplemente suscribiéndose a topics en los que otro nodo de la red ROS está publicando mensajes. Además, sabemos que cualquier nodo puede publicar mensajes en cualquier topic, lo que transmite datos a través de la red ROS, poniéndolos a disposición de cualquier otro nodo de la red que desee acceder a ellos.
Otra forma de pasar datos entre Nodos ROS es usando Services. Estos se basan en un tipo de comunicación de llamada y respuesta:
- Un Client de Service envía una Request a un Server de Service.
- El Server de Service procesa esa request y envía de vuelta una Response.
Esto es similar a una transacción: un nodo solicita algo, y otro nodo cumple esa solicitud y responde. Esto es útil para tareas rápidas y de corta duración, por ejemplo:
- Encender o apagar un dispositivo.
- Obtener algunos datos y guardarlos en un archivo (un mapa, por ejemplo).
- Realizar un cálculo y devolver un resultado.
- Emitir un sonido2.
Un único service puede tener muchos clients, pero solo puede haber un único Server proporcionando ese service en particular en un momento dado.
Veamos ahora cómo funciona todo esto en la práctica, ¡jugando un juego de números! No necesitamos una simulación en ejecución para este, así que en TERMINAL 1 usa el siguiente comando para iniciar el Service Adivina el Número:
Habiendo iniciado el service exitosamente, deberías ver lo siguiente:
[INFO] [#####] [number_game_service]: The '/guess_the_number' service is active.
[INFO] [#####] [number_game_service]: A secret number has been set... Game on!
Ahora necesitamos interrogar esto para averiguar cómo jugar el juego...
Interrogar un Service¶
Ejercicio 1: Usar Herramientas de Línea de Comandos para Interrogar un Service y su Interface¶
-
Abre una nueva instancia de terminal (TERMINAL 2) y usa el comando
ros2 servicepara listar todos los ROS services activos:Habrá algunos elementos en esta lista, la mayoría con el prefijo:
/number_game_service. Este es el nombre del nodo que está proporcionando el service (es decir, el Server), y estos elementos son generados automáticamente por ROS. Lo que realmente nos interesa es el service en sí, que debería aparecer en la lista como/guess_the_number. -
A continuación, necesitamos encontrar el tipo de interface utilizado por este service, lo cual podemos hacer de un par de maneras:
-
Usar el subcomando
type: -
Usar el subcomando
listnuevamente, pero con la bandera-t:Este último proporcionará la misma lista de services que antes, pero ahora cada uno tendrá su tipo de interface listado junto a él.
-
-
Independientemente del método que hayas usado arriba, deberías haber descubierto que el tipo de interface utilizado por el service
/guess_the_numberes:Observa cómo (de manera muy similar a las interfaces de tipo message usadas por los topics), hay tres campos en esta definición de tipo:
rigsa_interfaces: el nombre del package de ROS al que pertenece esta interface.srv: que esto es una interface de tipo service, el segundo tipo de interface que hemos visto (aprenderemos sobre el tercero y último en la Parte 5).NumberGame: la estructura de datos.
Necesitamos conocer la estructura de datos para poder hacer una llamada al service, así que vamos a identificarla a continuación.
-
Podemos usar el comando
ros2 interface listpara listar todos los tipos de interface disponibles en nuestro sistema ROS, ¡pero esto nos dará una lista muy larga!-
Podemos usar la bandera
-mpara filtrar por interfaces de tipo message, o la bandera-spara filtrar por interfaces de tipo service. Prueba la última: -
¡Todavía hay bastante ahí, verdad!? Filtremos esto aún más con Grep para identificar solo las interfaces que pertenecen al package
rigsa_interfaces:Con suerte, la interface
srv/NumberGameahora aparece en la lista. -
Usa el subcomando
showpara mostrar la estructura del mensaje:
La estructura de la interface debería mostrarse de la siguiente manera:
-
El Formato de las Interfaces de Service¶
Las interfaces de service tienen dos partes, separadas por tres guiones (---). Sobre el separador está la Request del Service, y debajo está la Response del Service:
int32 guess <-- Request
---
int32 guesses <-- Response (1 de 3)
string hint <-- Response (2 de 3)
bool success <-- Response (3 de 3)
Para llamar a un service, necesitamos proporcionarle datos en el formato especificado en la sección de Request de la interface. El Server del service enviará datos de vuelta al llamante en el formato especificado en la sección de Response de la interface.
La interface de service rigsa_interfaces/srv/NumberGame tiene solo un parámetro de request:
guess: unint32(entero de 32 bits)
...que es lo único que necesitamos enviar al Service Server/number_game_servicepara llamarlo.
Luego hay tres parámetros de response:
- Un entero de 32 bits llamado
guesses - Una cadena de texto llamada
hint -
Una bandera booleana llamada
success...todos los cuales serán devueltos por el server una vez que haya procesado nuestra request.
Ejercicio 2: Jugar el Juego de Números (desde la Línea de Comandos)¶
Ahora estamos listos para hacer una llamada al service, y podemos hacerlo usando el comando ros2 service nuevamente (desde TERMINAL 2):
-
Para empezar, enviemos una suposición inicial de
0y veamos qué sucede:La request nos será devuelta como eco, seguida de una response, que probablemente se verá algo así, y que nos muestra el valor de los tres parámetros de response que identificamos arriba:
guesses: nos dice cuántas veces hemos intentado adivinar el número en total (solo una vez hasta ahora)hint: nos dice si deberíamos ir "más alto" o "más bajo" en nuestra próxima suposición para acercarnos al número secretosuccess: nos dice si adivinamos el número correcto o no (¡poco probable en el primer intento!)
-
Realiza otra llamada al service, esta vez cambiando el valor de tu
guess, por ejemplo: -
Intenta hacer una suposición de 500 ahora.
El service debería responder con el hint
'Error'ahora. Vuelve a mirar en TERMINAL 1 (donde el Server está corriendo) para obtener más información sobre esto. -
Continúa hasta que adivines el número mágico, ¿cuántos intentos te toma?!
-
Detén el server, ingresando Ctrl+C en TERMINAL 1.
Crear Nuestros Propios Services¶
A lo largo de los próximos tres ejercicios, aprenderemos cómo crear una interface de service propia, y construir un Server y un Client (en Python) que la usen.
Primero, sin embargo, necesitamos crear un nuevo package, así que sigue el mismo procedimiento que has seguido en las partes anteriores de este curso para crear otro nuevo package llamado part4_services.
En TERMINAL 1:
-
Dirígete a la carpeta
srcdel workspace de ROS 2: -
Clona la plantilla del package:
-
Ejecuta el script
init_pkg.shy especifica el nombre de tu package:
Ejercicio 3: Crear una Interface de Service¶
Creemos ahora una interface de service que tenga una estructura similar a la usada por el service /guess_the_number, pero esta vez con dos parámetros de request, en lugar de solo uno...
-
En TERMINAL 1, navega hasta la raíz del directorio de tu package
part4_services: -
Crea un nuevo directorio llamado
srv: -
Crea un nuevo archivo en este directorio llamado
MyNumberGame.srv:Aquí es donde definiremos la estructura de nuestra propia interface de service
MyNumberGame. -
Abre este archivo en VS Code, ingresa el siguiente contenido y guarda el archivo:
La Request tendrá por tanto dos campos ahora:
# Nombre del Campo Tipo de Dato 1 guessint322 cheatbool -
El resto del proceso es ahora muy similar a crear una interface de tipo message, como hicimos en la Parte 1.
Primero, necesitamos declarar la interface en nuestro archivo
CMakeLists.txt, agregando lo siguiente encima de la líneaament_package(): -
A continuación, necesitamos modificar el archivo
package.xml. Agrega las siguientes líneas a este, justo encima de la línea<export>: -
Y finalmente, usamos Colcon para generar el código fuente necesario para la interface de service:
-
Navega hasta la raíz del Workspace de ROS2:
-
Ejecuta
colcon build: -
Y vuelve a cargar el
.bashrc:
-
-
Verifiquemos que esto funcionó, usando el CLI de
ros2(de la misma manera que hicimos antes cuando interrogamosrigsa_interfaces/srv/NumberGame):-
Primero, lista todas las interfaces de service de ROS disponibles en el sistema (¡recuerda usar
-spara filtrar por tipos de interfaces de service!):Desplázate por esta lista y mira si puedes encontrar
part4_services/srv/MyNumberGame(o, usagrepnuevamente). -
Si está ahí, usa el subcomando
showpara mostrar la estructura de datos:
¿Coincide con la definición en nuestro archivo
MyNumberGame.srv? -
Ejercicio 4: Adaptar el Server del Juego de Números¶
Vamos a tomar una copia del nodo server rigsa_examples/number_game.py ahora, y adaptarlo para usar la interface de service que creamos arriba.
-
Todavía en TERMINAL 1, navega hasta el directorio
part4_services/scripts: -
Copia el script
number_game.pydel repositorio del curso aquí, renombrándolo comomy_number_game.pyal mismo tiempo:Este archivo ya debería tener permisos de ejecución, pero siempre vale la pena verificarlo...
-
Declara esto como un ejecutable del package volviendo al archivo
CMakeLists.txty agregandomy_number_game.pya tu lista de ejecutables Python: -
Ahora, veamos el código y qué necesita ser adaptado:
-
Abre el nodo
my_number_game.pyen VS Code y revísalo. -
Tal como está, el nodo importa la interface de service
NumberGamederigsa_interfaces, así que necesitarás cambiar esto para usar la interface de tu propio package ahora:También necesitarás cambiar la definición de
srv_type, cuando el service se crea en__init__(): -
Todo lo que este service hace (cuando se le envía una Request) está contenido dentro del método
srv_callback().Aquí, los parámetros de
requestson procesados, los parámetros deresponseson definidos y laresponsegeneral es devuelta una vez que las tareas del callback han sido completadas. -
Es posible que hayas notado que cuando creamos nuestra interface
MyNumberGameen el ejercicio anterior, los parámetros de Response eran casi los mismos que los de los Ejercicios 1 y 2, excepto que algunos de sus nombres habían cambiado ligeramente.Trabaja a través del método
srv_callback()y asegúrate de que todos los atributos deresponsesean renombrados para coincidir con los nuevos nombres que les hemos dado en nuestra interfacepart4_services/srv/MyNumberGame. -
Recuerda que nuestra interface
MyNumberGametambién tiene un parámetro de Request adicional:... es decir, una bandera booleana con el nombre de atributo
cheat.Adapta el método
srv_callback()aún más ahora para leer este valor también.-
Si (cuando se realiza una request al server) el valor de
cheatesTrue, el hint que devuelve el server debería contener ¡el número secreto real! Por ejemplo: -
En tales situaciones, el valor de
response.num_guessesaún debería incrementarse en uno, y la banderacorrectaún debería devolverFalse.
-
-
-
Una vez que hayas adaptado el nodo, pruébalo ejecutándolo:
TERMINAL 1:
Luego deberías poder hacer llamadas a este desde TERMINAL 2 usando el subcomando
ros2 service call, como hicimos en el Ejercicio 2.Hint
Ahora hay dos parámetros de request, así que al enviar una request desde la línea de comandos ambos deben ser suministrados. Haz esto incluyéndolos a ambos dentro de las llaves (
{}) separados por una coma, por ejemplo:
Ejercicio 5: Crear un Python Service Client¶
Hasta ahora hemos estado haciendo llamadas a services desde la línea de comandos, pero también podemos llamar services desde dentro de Nodos Python. Cuando un nodo llama (es decir, solicita) un service, se convierte en un "Client" de Service.
-
Asegúrate de que tu nodo
my_number_game.pysiga activo en TERMINAL 1 para este ejercicio. -
En TERMINAL 2, crea un nuevo archivo en el directorio
part4_services/scriptsllamadonumber_game_client.py(usando el comandotouch). -
Agrégalo a la lista de ejecutables Python de tu package en el archivo
CMakeLists.txt: -
Vuelve a compilar tu package (como antes), recordando que hay tres pasos para esto:
- Navega hasta la raíz del workspace de ROS2,
- Ejecuta
colcon build(con los argumentos adicionales necesarios), - Vuelve a cargar tu
~/.bashrc.
-
Ahora, abre el archivo
number_game_client.pyen VS Code, y luego echa un vistazo a lo siguiente:El Nodo number_game_client.pyRevisa el código anterior cuidadosamente (incluyendo todas las anotaciones), antes de tomar una copia y pegarlo en tu propio archivo
number_game_client.py. -
Ahora deberías poder ejecutar el código con
ros2 run. Para empezar, usa una llamada estándar deros2 runde la siguiente manera:Luego deberías obtener una salida como esta:
[INFO] [#####] [number_game_client]: Sending the request: - guess: 0 - cheat: False Awaiting response... [INFO] [#####] [number_game_client]: The server responded with: - Incorrect guess :( - Number of attempts so far: 1 - A hint: 'Higher'.¿Notas cómo los parámetros de request
guessycheathan tomado los valores predeterminados0yFalserespectivamente? -
La razón principal de usar parámetros en nuestro nodo
number_game_clientes para poder cambiar el valor de la suposición desde la línea de comandos. Podemos hacer esto agregando algunos argumentos adicionales a la llamada deros2 run:... ¡reemplaza
Xcon un número real! -
Intenta hacer trampa también ahora:
-
También podemos establecer los dos parámetros simultáneamente usando múltiples banderas
-p:Note
El
\de arriba nos permite dividir el comando en dos líneas separadas, ¡útil para cuando estas cosas empiezan a ser un poco largas!
El Map Saver Service¶
Claramente, los ejemplos con los que hemos estado trabajando hasta ahora han sido bastante triviales: ¡es poco probable que alguna vez necesites programar un robot para jugar el juego de números! Sin embargo, el objetivo ha sido ilustrar cómo funcionan los ROS Services y cómo desarrollar los propios.
Sin embargo, una aplicación que puede resultarte útil es el guardado de mapas. En la Parte 3 aprendimos sobre SLAM, donde condujimos un robot por un entorno mientras los algoritmos SLAM trabajaban en segundo plano para generar un mapa del mundo usando datos del sensor LiDAR del robot y su sistema de odometría:
Habiendo mapeado el entorno, llamamos al nodo map_saver_cli del package nav2_map_server, para guardar una copia de ese mapa en un archivo:
... ¿no sería conveniente tener una manera de poder hacer esto programáticamente (es decir, desde dentro de un nodo Python, por ejemplo) en lugar de tener que ejecutar el comando anterior manualmente? Bueno, hay una manera, ¿y adivina qué? ... ¡Involucra Services!
Ejercicio 6: Desarrollar un Map Saver Service Client¶
-
Asegúrate de que todo en TERMINALES 1 y 2 de los ejercicios anteriores esté cerrado ahora.
-
En TERMINAL 1, volvamos a iniciar el Nav World, como lo hicimos en la Parte 3:
-
En TERMINAL 2, también volvamos a iniciar Cartographer (los algoritmos SLAM):
-
Abre otra instancia de terminal ahora (TERMINAL 3), y úsala para iniciar el Map Saver Service (¿no sería conveniente poder iniciar todos estos launch files a la vez?3):
Esto agregará una serie de services
/map_savera nuestra red ROS. -
En otra nueva instancia de terminal (TERMINAL 4), usa un subcomando de
ros2 servicepara identificar todos los services/map_saver(como lo hicimos en el Ejercicio 1).Question
¿Ves un elemento en esta lista que podría estar relacionado con guardar un mapa? (¡Tiene el prefijo
/map_saver!4) -
Usa otro subcomando de
ros2 servicepara determinar el tipo de interface utilizada por este service (nuevamente, como lo hicimos en el Ejercicio 1). -
A continuación, usa un comando
ros2 interfacepara descubrir la estructura de esta interface de service.Preguntas
- ¿Cuántos parámetros de Request tiene esta interface?
- ¿Cuántos parámetros de Response hay también?5
- ¿Cuáles son sus tipos de datos?
-
Desarrolla un Python Service Client para hacer llamadas a este service:
-
Crea un nuevo Nodo en tu package
part4_servicesllamadomap_saver_client.pypara esto.Cosas a recordar al hacer esto:
- Créalo en tu directorio
part4_services/scripts. - Asegúrate de que tenga permisos de ejecución.
- Declárao como un ejecutable del package en tu
CMakeLists.txt. - Vuelve a compilar tu package con
colcon, asegurándote de seguir el proceso completo de tres pasos
- Créalo en tu directorio
-
Usa el Nodo
number_game_client.pydel Ejercicio 5 como punto de partida al construir tu nuevo nodomap_saver_client.py... todos los mismos principios aplicarán aquí, solo los estás aplicando a un service diferente (y por lo tanto necesitas tener en cuenta una interface de service diferente). -
Declara un parámetro para tu nodo para permitirte especificar un nombre de archivo para tu mapa cuando se llame al nodo
map_saver_client.py, por ejemplo:Sigue el mismo enfoque que en el Ejercicio 5 para esto, y asegúrate de especificar un valor predeterminado, para situaciones en las que el valor del parámetro no se defina explícitamente cuando se llame al nodo.
-
Al construir requests de service, considera los siguientes consejos:
- Los datos del mapa SLAM (generados por Cartographer) se publican en un topic llamado
/map. -
Al proporcionar un nombre para el archivo del mapa:
- No necesitas incluir una extensión de archivo
-
Los nombres de archivo se interpretan en relación con tu directorio home, así que:
my_amazing_mapresultaría en un archivo de mapa en~/my_amazing_map.yamlmy/amazing/mapresultaría en un archivo de mapa en~/my/amazing/map.yaml(¡asumiendo que la estructura de directorios ya existe!)
-
Para más orientación consulta aquí para un ejemplo de uso.
- El server aplicará sus propios valores predeterminados a ciertos atributos de la request, si no son definidos explícitamente por un client.
- Los datos del mapa SLAM (generados por Cartographer) se publican en un topic llamado
-
Resumen del Map Saver Service¶
De regreso en la Parte 3 guardamos nuestro mapa SLAM una vez mediante una llamada de línea de comandos después de haber explorado y mapeado completamente el entorno:
... lo que resultó en algo como:
MAP_NAME.pgmSin embargo, en tareas del mundo real (¡es decir, tareas que podrías necesitar completar en el laboratorio!), tu robot podría estar explorando un entorno de forma autónoma, y no necesariamente sabes cuándo se ha explorado el entorno completo, ¡ni siempre estarás ahí para ejecutar el nodo map_saver_cli manualmente! Por lo tanto, es posible que desees programar tu robot con la capacidad de guardar un mapa de forma incremental y periódica a medida que se explora más y más del entorno.
¡El proceso que exploramos en el ejercicio anterior te permite hacer exactamente eso! En el ejemplo, nuestro nodo client fue programado para hacer solo una request al server y luego detenerse. Sin embargo, podría ser programado para hacer requests de service regulares (digamos, una vez cada 5 o 10 segundos) para actualizar continuamente su mapa a medida que el robot explora más y más.
Piensa en cómo podrías adaptar el nodo map_saver_client.py para lograr esto, apoyándote en otros ejercicios que has trabajado en partes anteriores de este curso.
Conclusión¶
En la Parte 4 hemos aprendido sobre los ROS Services y por qué pueden ser útiles para aplicaciones robóticas:
- Los Services difieren de los métodos de comunicación estándar basados en topics en ROS en que son un tipo de comunicación de llamada y respuesta, que tiene lugar entre un nodo y otro.
- Típicamente, un Caller de service hará una request a un service, y luego esperará una response (aunque es posible hacer otras cosas mientras tanto).
- En general, sin embargo, los Services son útiles para controlar tareas rápidas, de corta duración o cálculos.
¡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:
- En VS Code, haz clic derecho sobre la carpeta
srcen el explorador de archivos → Descargar… - O desde la terminal integrada:
Luego descarga el archivo
mi_codigo_parte4.zipdesde el explorador de VS Code.
WSL-ROS2 (Computadoras del laboratorio con escritorio administrado)
Ejecuta el siguiente script en cualquier instancia de Terminal WSL-ROS2 inactiva:
Luego podrás restaurarlo a un entorno WSL-ROS2 nuevo la próxima vez que lo inicies (wsl_ros restore).
-
Recuerda: también puedes usar el comando
wsl_ros restoreen cualquier momento. ↩ -
En los robots reales del laboratorio, puede existir un service de audio. Échale un vistazo la próxima vez que estés en el laboratorio... Una vez que hayas trabajado toda la Parte 4, sabrás exactamente cómo interrogar services y aprovechar la funcionalidad que proporcionan. ↩
-
¡Puedes! Podemos usar launch files para iniciar otros launch files, consulta aquí para más información. ↩
-
Debería haber uno en la lista llamado
/map_saver/save_map↩ -
La Interface de Service
nav2_msgs/srv/SaveMaptiene 6 Requests (map_topic,map_url,image_format,map_mode,free_thresh,occupied_thresh) y 1 Response (result). ↩