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 : TP 2 — Recevoir des ordres, dont on reprend le projet.

Ce qu’il faut savoir

Topic, service ou paramètre ?

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 :

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

  1. Configurez PC13 en sortie (le buzzer actif, TP 3 de la partie 1).

  2. 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.

    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);
    
  3. Le champ message de la réponse est une chaîne : comme au TP 2, c’est à vous de fournir sa mémoire.

  4. Un élément de plus pour l’exécuteur.

  5. Depuis le PC :

    ros2 service list
    ros2 service call /tp/buzzer std_srvs/srv/SetBool "{data: true}"
    

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 ?

Solution de l’étape 1
#include <std_srvs/srv/set_bool.h>

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 :

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.

Réponses aux questions 1 et 2

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

  1. 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).

  2. Vérifiez le code de retour de sa création : que se passe-t-il ?

  3. Ouvrez micro_ros_stm32cubemx_utils/microros_static_library_ide/ library_generation/colcon.meta, et portez RMW_UXRCE_MAX_SERVICES à 8.

  4. Supprimez le dossier micro_ros_stm32cubemx_utils/microros_static_library_ide/libmicroros, pour forcer la reconstruction de la bibliothèque, et recompilez.

Question

  1. Pourquoi ces limites sont-elles fixées à la compilation, et pas choisies par le programme ?

Solution de l’étape 2

Le second service, sur le même modèle :

#include <std_srvs/srv/trigger.h>

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 :

"-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.

Réponse à la question 3

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 :

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

  1. Créez le serveur de paramètres, avec une option pour en accepter plusieurs (par défaut : quatre au plus) :

    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(&params, &node, &o);
    rclc_executor_add_parameter_server(&executor, &params, 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.

  2. Déclarez chaque paramètre, puis donnez-lui sa valeur de départ :

    rclc_add_parameter(&params, "blink_period_ms", RCLC_PARAMETER_INT);
    rclc_parameter_set_int(&params, "blink_period_ms", 500);
    
  3. É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.

  4. Dans la boucle, lisez les paramètres (rclc_parameter_get_int…) et appliquez-les.

  5. Depuis le PC :

    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
    

Questions

  1. Que répond ros2 param set pour la valeur 10 ? Où ce refus a-t-il été décidé ?

  2. 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 ?

  3. Pourquoi le serveur de paramètres demande-t-il cinq services ? Lesquels (ros2 service list) ?

Solution de l’étape 3
#include <rclc_parameter/rclc_parameter.h>

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) :

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(&params, &node, &o);
rclc_executor_init(&executor, &support.context,
                   2u + RCLC_EXECUTOR_PARAMETER_SERVER_HANDLES, &allocator);
/* … les deux services … */
rclc_executor_add_parameter_server(&executor, &params, on_param_changed);

rclc_add_parameter(&params, "blink_period_ms", RCLC_PARAMETER_INT);
rclc_parameter_set_int(&params, "blink_period_ms", 500);
rclc_add_parameter(&params, "temp_alarm", RCLC_PARAMETER_DOUBLE);
rclc_parameter_set_double(&params, "temp_alarm", 35.0);
rclc_add_parameter(&params, "buzzer_enabled", RCLC_PARAMETER_BOOL);
rclc_parameter_set_bool(&params, "buzzer_enabled", true);

et dans la boucle :

int64_t period = 500;
double  alarm  = 35.0;

(void)rclc_parameter_get_int(&params, "blink_period_ms", &period);
(void)rclc_parameter_get_double(&params, "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.

Réponses aux questions 4 à 6

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/ :

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.

Tout le code de la correction

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 Remettre la carte en état RatBot.