TP 3 — Services et paramètres ============================= Objectifs --------- - offrir un **service** : une requête, une réponse ; - régler les **limites** de la bibliothèque micro-ROS, et la reconstruire ; - exposer des **paramètres**, modifiables depuis ROS 2 sans recompiler, et les **valider**. Durée indicative : une séance de 3 h. Prérequis : :doc:`tp02_recevoir`, dont on reprend le projet. Ce qu'il faut savoir -------------------- Topic, service ou paramètre ? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 :widths: 18 42 40 * - Mécanisme - Principe - Exemple * - **topic** - flux de messages, sans réponse, à n'importe quel nombre d'abonnés - une mesure, une consigne de vitesse * - **service** - une requête, une réponse, à un seul serveur - remettre l'odométrie à zéro, déclencher un bip * - **paramètre** - une valeur nommée, lue et modifiée par ``ros2 param`` - un réglage : un rayon de roue, un seuil Un service est fait pour une action **ponctuelle**, dont on veut savoir si elle a réussi. Un paramètre, pour un **réglage**, qu'on change rarement. Le robot en offre de chaque sorte : ``base/reset_odom`` est un service ; les rayons de roue et l'entraxe sont des paramètres de sa carte. Les limites de micro-ROS ~~~~~~~~~~~~~~~~~~~~~~~~ micro-ROS n'alloue pas de mémoire au fil de l'eau : le nombre de nœuds, d'éditeurs, d'abonnés, de services est **fixé à la compilation de la bibliothèque**, dans le fichier ``colcon.meta``. Par défaut : .. code-block:: text RMW_UXRCE_MAX_PUBLISHERS = 10 RMW_UXRCE_MAX_SUBSCRIPTIONS = 5 RMW_UXRCE_MAX_SERVICES = 1 RMW_UXRCE_MAX_CLIENTS = 1 Au-delà, la création échoue — et rien d'autre ne l'indique que le code de retour. Étape 1 — Un service : le buzzer -------------------------------- #. Configurez **PC13** en sortie (le buzzer actif, TP 3 de la partie 1). #. Créez un service ``buzzer`` de type ``std_srvs/srv/SetBool`` : ``data: true`` met le buzzer en marche, ``false`` l'arrête. La réponse indique ``success`` et un court ``message``. .. code-block:: c rclc_service_init_default(&srv_buzzer, &node, ROSIDL_GET_SRV_TYPE_SUPPORT(std_srvs, srv, SetBool), "buzzer"); rclc_executor_add_service(&executor, &srv_buzzer, &req_buzzer, &res_buzzer, buzzer_cb); #. Le champ ``message`` de la réponse est une chaîne : comme au TP 2, c'est à vous de fournir sa mémoire. #. Un élément de plus pour l'exécuteur. #. Depuis le PC : .. code-block:: bash ros2 service list ros2 service call /tp/buzzer std_srvs/srv/SetBool "{data: true}" .. admonition:: Questions 1. Que renvoie l'appel ? Que se passe-t-il si la carte est débranchée ? 2. Pourquoi un service plutôt qu'un topic ``std_msgs/msg/Bool`` ? .. dropdown:: Solution de l'étape 1 :icon: code :color: secondary .. code-block:: c #include static rcl_service_t srv_buzzer; static std_srvs__srv__SetBool_Request req_buzzer; /* static : vivent */ static std_srvs__srv__SetBool_Response res_buzzer; /* avec le service */ static char res_buzzer_text[32]; /* Copie un texte dans une chaîne micro-ROS dont la mémoire est à nous. */ static void set_text(rosidl_runtime_c__String *s, char *buf, size_t cap, const char *txt) { strncpy(buf, txt, cap - 1u); buf[cap - 1u] = '\0'; s->data = buf; s->size = strlen(buf); s->capacity = cap; } static void buzzer_cb(const void *req_in, void *res_out) { const std_srvs__srv__SetBool_Request *req = (const std_srvs__srv__SetBool_Request *)req_in; std_srvs__srv__SetBool_Response *res = (std_srvs__srv__SetBool_Response *)res_out; HAL_GPIO_WritePin(BUZZER_GPIO_Port, BUZZER_Pin, req->data ? GPIO_PIN_SET : GPIO_PIN_RESET); res->success = true; set_text(&res->message, res_buzzer_text, sizeof res_buzzer_text, req->data ? "buzzer on" : "buzzer off"); } et après la création du nœud : .. code-block:: c rclc_service_init_default(&srv_buzzer, &node, ROSIDL_GET_SRV_TYPE_SUPPORT(std_srvs, srv, SetBool), "buzzer"); rclc_executor_add_service(&executor, &srv_buzzer, &req_buzzer, &res_buzzer, buzzer_cb); **Le rappel de service** reçoit la requête (déjà décodée par l'exécuteur) et **remplit la réponse** ; l'exécuteur l'envoie quand le rappel se termine. Un service est donc **synchrone** du point de vue du client : il attend que le rappel ait fini. **strncpy et sa borne.** ``strncpy`` copie au plus ``cap − 1`` caractères ; on ajoute le ``'\0'`` final à la main, car ``strncpy`` ne le met pas si le texte est trop long. La capacité déclarée doit être celle du tampon, pas celle du texte. .. dropdown:: Réponses aux questions 1 et 2 :icon: light-bulb :color: info **1. La réponse.** ``ros2 service call`` affiche la requête, puis la réponse : ``success=True, message='buzzer on'``. Si la carte est débranchée, le service **disparaît** du graphe (l'agent le retire quand la session meurt, ou il n'existe plus après un redémarrage de l'agent), et l'appel affiche « waiting for service to become available… » indéfiniment. Un programme qui appelle un service doit donc toujours prévoir un **délai d'attente**. **2. Service ou topic.** Avec un topic, l'émetteur publie et ne sait rien de la suite : le message est-il arrivé ? quelqu'un l'a-t-il traité ? a-t-il réussi ? Un service répond à ces trois questions : il y a **un** destinataire, il exécute l'ordre, et il dit si ça a marché (``success``) et pourquoi (``message``). Pour une action ponctuelle qu'on veut confirmer, c'est le bon outil. Pour un flux continu (une consigne de vitesse, une mesure), le topic reste le bon choix : une réponse à chaque message serait inutile et coûteuse. Étape 2 — Un second service, et la limite ----------------------------------------- #. Ajoutez un service ``reset_counter`` de type ``std_srvs/srv/Trigger``, qui remet à zéro le compteur du TP 1 (réintégrez-le s'il a disparu). #. Vérifiez le code de retour de sa création : que se passe-t-il ? #. Ouvrez ``micro_ros_stm32cubemx_utils/microros_static_library_ide/ library_generation/colcon.meta``, et portez ``RMW_UXRCE_MAX_SERVICES`` à **8**. #. Supprimez le dossier ``micro_ros_stm32cubemx_utils/microros_static_library_ide/libmicroros``, pour forcer la reconstruction de la bibliothèque, et recompilez. .. admonition:: Question 3. Pourquoi ces limites sont-elles fixées à la compilation, et pas choisies par le programme ? .. dropdown:: Solution de l'étape 2 :icon: code :color: secondary Le second service, sur le même modèle : .. code-block:: c #include static void reset_cb(const void *req_in, void *res_out) { std_srvs__srv__Trigger_Response *res = (std_srvs__srv__Trigger_Response *)res_out; (void)req_in; /* une requête Trigger est vide */ g_counter = 0; res->success = true; set_text(&res->message, res_reset_text, sizeof res_reset_text, "counter reset"); } Avec la bibliothèque par défaut, sa création **échoue** : ``rclc_service_init_default`` renvoie une erreur, car ``RMW_UXRCE_MAX_SERVICES`` vaut 1 et le service ``buzzer`` occupe déjà la seule place. Dans ``colcon.meta`` : .. code-block:: text "-DRMW_UXRCE_MAX_SERVICES=8", puis on supprime ``microros_static_library_ide/libmicroros`` : l'étape de pré-compilation voit qu'elle manque, et reconstruit la bibliothèque (une dizaine de minutes). **Pourquoi 8** : 2 services à nous, 5 pour le serveur de paramètres de l'étape 3, et une place de marge. .. dropdown:: Réponse à la question 3 :icon: light-bulb :color: info **3. Des limites fixées à la compilation.** Le client micro-ROS réserve **toute sa mémoire à l'avance**, dans des tableaux de taille fixe : un tableau de 10 éditeurs, de 5 abonnés, de 1 service… C'est un choix délibéré pour un microcontrôleur : - **pas d'allocation** au fil de l'eau, donc pas de fragmentation de la mémoire au bout de plusieurs jours de fonctionnement ; - une **empreinte mémoire connue** à la compilation : si le programme se lie, il a la mémoire qu'il lui faut ; pas de panne de mémoire en plein fonctionnement. Le prix : il faut dimensionner ces tableaux pour le programme, et reconstruire la bibliothèque quand on les change. Le firmware du robot l'a fait (24 éditeurs, 16 abonnés, 16 services). Étape 3 — Des paramètres ------------------------ Trois réglages de la carte deviennent des paramètres : .. list-table:: :header-rows: 1 :widths: 30 15 55 * - Nom - Type - Rôle * - ``blink_period_ms`` - entier - période de clignotement de LED_2, de 50 à 5 000 ms * - ``temp_alarm`` - réel - seuil d'alarme de température, en °C : LED_3 allumée au-delà * - ``buzzer_enabled`` - booléen - autorise ou non le service ``buzzer`` #. Créez le **serveur de paramètres**, avec une option pour en accepter plusieurs (par défaut : quatre au plus) : .. code-block:: c rclc_parameter_options_t o = { .notify_changed_over_dds = true, .max_params = 4, .allow_undeclared_parameters = false, .low_mem_mode = false, }; rclc_parameter_server_init_with_option(¶ms, &node, &o); rclc_executor_add_parameter_server(&executor, ¶ms, on_param_changed); Le serveur utilise **cinq services** et un éditeur : comptez-les dans les limites de l'étape 2, et ajoutez ``RCLC_EXECUTOR_PARAMETER_SERVER_HANDLES`` (5) au nombre d'éléments de l'exécuteur. #. Déclarez chaque paramètre, puis donnez-lui sa valeur de départ : .. code-block:: c rclc_add_parameter(¶ms, "blink_period_ms", RCLC_PARAMETER_INT); rclc_parameter_set_int(¶ms, "blink_period_ms", 500); #. Écrivez ``on_param_changed`` : elle reçoit l'ancienne et la nouvelle valeur, et renvoie ``true`` pour **accepter** la modification, ``false`` pour la **refuser**. Refusez une période hors de 50–5 000 ms. .. important:: ``old_param`` vaut ``NULL`` à la **création** d'un paramètre (premier ``rclc_parameter_set_*``) : ce n'est pas une erreur, il faut accepter. Sinon, tous les paramètres restent à zéro. #. Dans la boucle, lisez les paramètres (``rclc_parameter_get_int``…) et appliquez-les. #. Depuis le PC : .. code-block:: bash ros2 param list /tp/ratbot_lab ros2 param get /tp/ratbot_lab blink_period_ms ros2 param set /tp/ratbot_lab blink_period_ms 100 ros2 param set /tp/ratbot_lab blink_period_ms 10 .. Capture d'écran à insérer : le terminal : ros2 param list, ros2 param get, puis le refus de la valeur 10. Déposez _static/captures/ros/tp03-param-refus.png puis décommentez le bloc ci-dessous (retirez le préfixe « .. » devant chaque ligne) : .. .. image:: ../_static/captures/ros/tp03-param-refus.png .. :align: center .. :width: 90% .. admonition:: Questions 4. Que répond ``ros2 param set`` pour la valeur 10 ? Où ce refus a-t-il été décidé ? 5. Redémarrez la carte : que valent les paramètres ? Comment le firmware du robot garde-t-il les siens d'un démarrage à l'autre ? 6. Pourquoi le serveur de paramètres demande-t-il cinq services ? Lesquels (``ros2 service list``) ? .. dropdown:: Solution de l'étape 3 :icon: code :color: secondary .. code-block:: c #include static rclc_parameter_server_t params; /* old_param NULL : création ; new_param NULL : suppression. */ static bool on_param_changed(const Parameter *old_param, const Parameter *new_param, void *context) { (void)context; if (old_param == NULL) { return true; } /* création : on accepte */ if (new_param == NULL) { return false; } /* suppression : on refuse */ if (strcmp(new_param->name.data, "blink_period_ms") == 0) { int64_t v = new_param->value.integer_value; return (v >= 50) && (v <= 5000); } if (strcmp(new_param->name.data, "temp_alarm") == 0) { double v = new_param->value.double_value; return (v > 0.0) && (v < 100.0); } return true; /* buzzer_enabled : tout booléen */ } la création (l'exécuteur compte maintenant 2 + 5 éléments) : .. code-block:: c rclc_parameter_options_t o = { .notify_changed_over_dds = true, .max_params = 4, .allow_undeclared_parameters = false, .low_mem_mode = false, }; rclc_parameter_server_init_with_option(¶ms, &node, &o); rclc_executor_init(&executor, &support.context, 2u + RCLC_EXECUTOR_PARAMETER_SERVER_HANDLES, &allocator); /* … les deux services … */ rclc_executor_add_parameter_server(&executor, ¶ms, on_param_changed); rclc_add_parameter(¶ms, "blink_period_ms", RCLC_PARAMETER_INT); rclc_parameter_set_int(¶ms, "blink_period_ms", 500); rclc_add_parameter(¶ms, "temp_alarm", RCLC_PARAMETER_DOUBLE); rclc_parameter_set_double(¶ms, "temp_alarm", 35.0); rclc_add_parameter(¶ms, "buzzer_enabled", RCLC_PARAMETER_BOOL); rclc_parameter_set_bool(¶ms, "buzzer_enabled", true); et dans la boucle : .. code-block:: c int64_t period = 500; double alarm = 35.0; (void)rclc_parameter_get_int(¶ms, "blink_period_ms", &period); (void)rclc_parameter_get_double(¶ms, "temp_alarm", &alarm); /* … LED_2 clignote à « period », LED_3 s'allume au-delà de « alarm » … */ Le service ``buzzer`` consulte ``buzzer_enabled`` avec ``rclc_parameter_get_bool`` avant d'agir, et répond ``success = false`` s'il est désactivé. **Pourquoi l'ordre déclarer, puis valeur.** ``rclc_add_parameter`` crée le paramètre, sans valeur ; le premier ``rclc_parameter_set_*`` la lui donne, et passe **par le rappel de validation** avec ``old_param`` à ``NULL``. Un rappel qui refuse ce cas laisserait tous les paramètres à zéro, sans aucun message : c'est un piège rencontré sur le firmware du robot. **max_params** : par défaut, le serveur n'accepte que **quatre** paramètres ; au-delà, ``rclc_add_parameter`` échoue en silence si l'on ne teste pas son retour. Le robot en déclare treize, d'où l'option. **La validation sur la carte.** C'est la carte qui sait quelles valeurs sont dangereuses pour elle. Valider côté PC serait insuffisant : n'importe quel outil ROS 2 peut appeler le service ``set_parameters``. .. dropdown:: Réponses aux questions 4 à 6 :icon: light-bulb :color: info **4. La valeur 10.** ``ros2 param set`` répond **« Setting parameter failed »**, et ``ros2 param get`` montre que la valeur est restée à 100. Le refus a été décidé **sur la carte**, par ``on_param_changed``, qui a renvoyé ``false`` : la requête a traversé l'agent, le serveur de paramètres l'a soumise au rappel, et a renvoyé le refus au PC. **5. Après un redémarrage.** Les paramètres reviennent à leurs **valeurs de départ** (500, 35,0, vrai) : ils vivent en **RAM**. Le firmware du robot garde les siens (rayons des roues, entraxe, échelle du gyroscope…) dans sa **flash de données** SPI : il les y relit au démarrage, avant de créer son nœud, et les y réécrit à la demande (``ratbot config save``). Un réglage d'étalonnage qui disparaît à chaque coupure serait inutilisable. **6. Les cinq services.** ``ros2 service list`` montre, sous ``/tp/ratbot_lab/`` : .. code-block:: text describe_parameters get_parameter_types get_parameters list_parameters set_parameters Ce sont les services **standard** que tout nœud ROS 2 expose pour ses paramètres ; c'est par eux que passent ``ros2 param``, ``rqt`` ou tout autre outil. Le serveur ajoute un éditeur, ``parameter_events``, qui annonce chaque changement. rclc n'implémente pas le sixième service des nœuds ROS 2 complets, ``set_parameters_atomically``, pour économiser la mémoire. .. dropdown:: Tout le code de la correction :icon: download :color: secondary :download:`taches.c <../../corrections/ros/tp03/taches.c>` — compilé sans avertissement contre les en-têtes de micro-ROS Humble, la HAL et FreeRTOS du firmware RatBot (sans édition de liens ni essai sur carte). Le fichier contient les inclusions, les déclarations et la fonction de la tâche micro-ROS, qui remplace le squelette ``StartDefaultTask`` généré par CubeMX. La configuration attendue est rappelée en tête du fichier, et les réponses aux questions à la fin. Exercices supplémentaires ------------------------- Ces exercices n'ont pas de correction. - **Mémoriser les réglages** : un service ``save`` écrit les paramètres dans un registre de sauvegarde de la RTC (TP 9 de la partie 1) ; au démarrage, ils y sont relus. - **Un fichier de paramètres** : ``ros2 param dump`` écrit les paramètres dans un fichier YAML, ``ros2 param load`` les recharge. Essayez. - **Les contraintes déclarées** : utilisez ``rclc_add_parameter_constraint_integer`` pour déclarer la plage de ``blink_period_ms``, et observez ``ros2 param describe``. Après le TP ----------- La flash externe contient votre programme : réinstallez celui du robot, voir :ref:`etat-ratbot`.