Libérer le potentiel du middleware Fast DDS [contribution de la communauté]

Objectif : Ce didacticiel vous montrera comment utiliser les capacités de configuration étendues de Fast DDS dans ROS 2.

Niveau du didacticiel : Avancé

Durée : 20 minutes

Arrière-plan

L’interface entre la pile ROS 2 et Fast DDS est fournie par l’implémentation du middleware ROS 2 rmw_fastrtps. Cette implémentation est disponible dans toutes les distributions ROS 2, à la fois des binaires et des sources.

ROS 2 RMW ne permet que la configuration de certains middleware QoS (voir Politiques de QoS ROS 2). Cependant, rmw_fastrtps offre des capacités de configuration étendues pour tirer pleinement parti des fonctionnalités de Fast DDS. Ce didacticiel vous guidera à travers une série d’exemples expliquant comment utiliser des fichiers XML pour déverrouiller cette configuration étendue.

Afin d’obtenir plus d’informations sur l’utilisation de Fast DDS sur ROS 2, veuillez consulter la documentation suivante __.

Conditions préalables

Ce didacticiel suppose que vous savez comment créer un package. Cela suppose également que vous savez écrire un simple publisher and subscriber et un simple service et client. Bien que les exemples soient implémentés en C++, les mêmes concepts s’appliquent aux packages Python.

Mélanger des publications synchrones et asynchrones dans un même nœud

Dans ce premier exemple, un nœud avec deux éditeurs, l’un avec un mode de publication synchrone et l’autre avec un mode de publication asynchrone, sera créé.

rmw_fastrtps utilise le mode de publication asynchrone par défaut. Lorsque l’éditeur invoque l’opération d’écriture, les données sont copiées dans une file d’attente, un thread d’arrière-plan (thread asynchrone) est informé de l’ajout à la file d’attente et le contrôle du thread est rendu à l’utilisateur avant que les données ne soient réellement envoyées. Le thread d’arrière-plan est chargé de consommer la file d’attente et d’envoyer les données à chaque lecteur correspondant.

En revanche, avec le mode de publication synchrone, les données sont envoyées directement dans le contexte du thread utilisateur. Cela implique que tout appel bloquant survenant pendant l’opération d’écriture bloquerait le thread utilisateur, empêchant ainsi l’application de continuer son fonctionnement. Cependant, ce mode donne généralement des débits plus élevés à des latences plus faibles, car il n’y a pas de notification ni de changement de contexte entre les threads.

Créer le nœud avec les éditeurs

Tout d’abord, créez un nouveau package nommé sync_async_node_example_cpp sur un nouvel espace de travail :

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --dependencies rclcpp std_msgs -- sync_async_node_example_cpp

Ensuite, ajoutez un fichier nommé src/sync_async_writer.cpp au package, avec le contenu suivant. Notez que l’éditeur synchrone publiera sur le sujet sync_topic, tandis que l’éditeur asynchrone publiera sur le sujet async_topic.

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

class SyncAsyncPublisher : public rclcpp::Node
{
public:
    SyncAsyncPublisher()
        : Node("sync_async_publisher"), count_(0)
    {
        // Create the synchronous publisher on topic 'sync_topic'
        sync_publisher_ = this->create_publisher<std_msgs::msg::String>("sync_topic", 10);

        // Create the asynchronous publisher on topic 'async_topic'
        async_publisher_ = this->create_publisher<std_msgs::msg::String>("async_topic", 10);

        // This timer will trigger the publication of new data every half a second
        timer_ = this->create_wall_timer(
                500ms, std::bind(&SyncAsyncPublisher::timer_callback, this));
    }

private:
    /**
     * Actions to run every time the timer expires
     */
    void timer_callback()
    {
        // Create a new message to be sent
        auto sync_message = std_msgs::msg::String();
        sync_message.data = "SYNC: Hello, world! " + std::to_string(count_);

        // Log the message to the console to show progress
        RCLCPP_INFO(this->get_logger(), "Synchronously publishing: '%s'", sync_message.data.c_str());

        // Publish the message using the synchronous publisher
        sync_publisher_->publish(sync_message);

        // Create a new message to be sent
        auto async_message = std_msgs::msg::String();
        async_message.data = "ASYNC: Hello, world! " + std::to_string(count_);

        // Log the message to the console to show progress
        RCLCPP_INFO(this->get_logger(), "Asynchronously publishing: '%s'", async_message.data.c_str());

        // Publish the message using the asynchronous publisher
        async_publisher_->publish(async_message);

        // Prepare the count for the next message
        count_++;
    }

    // This timer will trigger the publication of new data every half a second
    rclcpp::TimerBase::SharedPtr timer_;

    // A publisher that publishes asynchronously
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr async_publisher_;

    // A publisher that publishes synchronously
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr sync_publisher_;

    // Number of messages sent so far
    size_t count_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<SyncAsyncPublisher>());
    rclcpp::shutdown();
    return 0;
}

Ouvrez maintenant le fichier CMakeLists.txt et ajoutez un nouvel exécutable et nommez-le SyncAsyncWriter afin que vous puissiez exécuter votre nœud en utilisant ros2 run :

add_executable(SyncAsyncWriter src/sync_async_writer.cpp)
ament_target_dependencies(SyncAsyncWriter rclcpp std_msgs)

Enfin, ajoutez la section install(TARGETS…) pour que ros2 run puisse trouver votre exécutable :

install(TARGETS
    SyncAsyncWriter
    DESTINATION lib/${PROJECT_NAME})

Vous pouvez nettoyer votre CMakeLists.txt en supprimant certaines sections et commentaires inutiles, il ressemble donc à ceci :

cmake_minimum_required(VERSION 3.8)
project(sync_async_node_example_cpp)

# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 14)
endif()

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(SyncAsyncWriter src/sync_async_writer.cpp)
ament_target_dependencies(SyncAsyncWriter rclcpp std_msgs)

install(TARGETS
    SyncAsyncWriter
    DESTINATION lib/${PROJECT_NAME})

ament_package()

Si ce nœud est créé et exécuté maintenant, les deux éditeurs se comporteront de la même manière, en publiant de manière asynchrone dans les deux rubriques, car il s’agit du mode de publication par défaut. La configuration par défaut du mode de publication peut être modifiée en runtime lors du lancement du nœud, à l’aide d’un fichier XML.

Créer le fichier XML avec la configuration du profil

Créez un fichier avec le nom SyncAsync.xml et le contenu suivant :

<?xml version="1.0" encoding="UTF-8" ?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">

    <!-- default publisher profile -->
    <publisher profile_name="default_publisher" is_default_profile="true">
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
    </publisher>

    <!-- publisher profile for topic sync_topic -->
    <publisher profile_name="/sync_topic">
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
        <qos>
            <publishMode>
                <kind>SYNCHRONOUS</kind>
            </publishMode>
        </qos>
    </publisher>

    <!-- publisher profile for topic async_topic -->
    <publisher profile_name="/async_topic">
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
        <qos>
            <publishMode>
                <kind>ASYNCHRONOUS</kind>
            </publishMode>
        </qos>
    </publisher>

 </profiles>

Notez que plusieurs profils d’éditeurs sont définis. Un profil par défaut qui est défini en définissant is_default_profile sur true, et deux profils avec des noms qui coïncident avec ceux des sujets précédemment définis : sync_topic et un autre pour async_topic. Ces deux derniers profils définissent le mode de publication sur SYNCHRONOUS ou ASYNCHRONOUS selon le cas. Notez également que tous les profils spécifient une valeur historyMemoryPolicy, qui est nécessaire pour que l’exemple fonctionne, et la raison sera expliquée plus tard dans ce tutoriel.

Exécuter le nœud de l’éditeur

Vous devrez exporter les variables d’environnement suivantes pour que le XML soit chargé :

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export RMW_FASTRTPS_USE_QOS_FROM_XML=1
export FASTRTPS_DEFAULT_PROFILES_FILE=path/to/SyncAsync.xml

Enfin, assurez-vous d’avoir sourcé vos fichiers de configuration et exécutez le nœud :

source install/setup.bash
ros2 run sync_async_node_example_cpp SyncAsyncWriter

Vous devriez voir les éditeurs envoyer les données depuis le nœud de publication, comme suit :

[INFO] [1612972049.994630332] [sync_async_publisher]: Synchronously publishing: 'SYNC: Hello, world! 0'
[INFO] [1612972049.995097767] [sync_async_publisher]: Asynchronously publishing: 'ASYNC: Hello, world! 0'
[INFO] [1612972050.494478706] [sync_async_publisher]: Synchronously publishing: 'SYNC: Hello, world! 1'
[INFO] [1612972050.494664334] [sync_async_publisher]: Asynchronously publishing: 'ASYNC: Hello, world! 1'
[INFO] [1612972050.994368474] [sync_async_publisher]: Synchronously publishing: 'SYNC: Hello, world! 2'
[INFO] [1612972050.994549851] [sync_async_publisher]: Asynchronously publishing: 'ASYNC: Hello, world! 2'

Vous avez maintenant un éditeur synchrone et un éditeur asynchrone exécutés dans le même nœud.

Créer un nœud avec les abonnés

Ensuite, un nouveau nœud avec les abonnés qui écouteront les publications sync_topic et async_topic va être créé. Dans un nouveau fichier source nommé src/sync_async_reader.cpp écrivez le contenu suivant :

#include <functional>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

using std::placeholders::_1;

class SyncAsyncSubscriber : public rclcpp::Node
{
public:

    SyncAsyncSubscriber()
        : Node("sync_async_subscriber")
    {
        // Create the synchronous subscriber on topic 'sync_topic'
        // and tie it to the topic_callback
        sync_subscription_ = this->create_subscription<std_msgs::msg::String>(
            "sync_topic", 10, std::bind(&SyncAsyncSubscriber::topic_callback, this, _1));

        // Create the asynchronous subscriber on topic 'async_topic'
        // and tie it to the topic_callback
        async_subscription_ = this->create_subscription<std_msgs::msg::String>(
            "async_topic", 10, std::bind(&SyncAsyncSubscriber::topic_callback, this, _1));
    }

private:

    /**
     * Actions to run every time a new message is received
     */
    void topic_callback(const std_msgs::msg::String & msg) const
    {
        RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
    }

    // A subscriber that listens to topic 'sync_topic'
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sync_subscription_;

    // A subscriber that listens to topic 'async_topic'
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr async_subscription_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<SyncAsyncSubscriber>());
    rclcpp::shutdown();
    return 0;
}

Ouvrez le fichier CMakeLists.txt et ajoutez un nouvel exécutable et nommez-le SyncAsyncReader sous le précédent SyncAsyncWriter :

add_executable(SyncAsyncReader src/sync_async_reader.cpp)
ament_target_dependencies(SyncAsyncReader rclcpp std_msgs)

install(TARGETS
    SyncAsyncReader
    DESTINATION lib/${PROJECT_NAME})

Ajouter les profils d’abonnés au XML

Les profils de configuration des abonnés peuvent être ajoutés dans le même fichier XML SyncAsync.xml. Pour le moment, seul le profil par défaut sera défini. Les profils d’abonnés seront étendus ultérieurement. Ouvrez le fichier SyncAsync.xml et ajoutez les profils suivants dans la balise <profiles> :

<!-- subscriber profile for topic sync_topic -->
<subscriber profile_name="default_subscriber" is_default_profile="true">
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
</subscriber>

Exécuter le nœud d’abonné

Avec le nœud d’éditeur en cours d’exécution dans un terminal, ouvrez-en un autre et exportez les variables d’environnement requises pour que le XML soit chargé :

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export RMW_FASTRTPS_USE_QOS_FROM_XML=1
export FASTRTPS_DEFAULT_PROFILES_FILE=path/to/SyncAsync.xml

Enfin, assurez-vous d’avoir sourcé vos fichiers de configuration et exécutez le nœud :

source install/setup.bash
ros2 run sync_async_node_example_cpp SyncAsyncReader

Vous devriez voir les abonnés recevoir les données du nœud de publication, comme suit :

[INFO] [1612972054.495429090] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 10'
[INFO] [1612972054.995410057] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 10'
[INFO] [1612972055.495453494] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 11'
[INFO] [1612972055.995396561] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 11'
[INFO] [1612972056.495534818] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 12'
[INFO] [1612972056.995473953] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 12'

Analyse de l’exemple

Profils de configuration XML

Le fichier XML définit plusieurs configurations pour les éditeurs et les abonnés. Vous pouvez avoir un profil de configuration d’éditeur par défaut et plusieurs profils d’éditeur spécifiques à un sujet. La seule exigence est que tous les profils d’éditeur aient un nom différent et qu’il n’y ait qu’un seul profil par défaut. Il en va de même pour les abonnés.

Afin de définir une configuration pour un sujet spécifique, nommez simplement le profil après le nom du sujet ROS 2 (comme /sync_topic et /async_topic dans l’exemple), et rmw_fastrtps s’appliquera ce profil à tous les éditeurs et abonnés pour ce sujet. Le profil de configuration par défaut est identifié par l’attribut is_default_profile défini sur true, et agit comme un profil de secours lorsqu’il n’y en a pas d’autre avec un nom correspondant au nom du sujet.

La variable d’environnement FASTRTPS_DEFAULT_PROFILES_FILE est utilisée pour informer Fast DDS du chemin vers le fichier XML avec les profils de configuration à charger.

RMW_FASTRTPS_USE_QOS_FROM_XML

Parmi tous les attributs configurables, rmw_fastrtps traite publishMode et historyMemoryPolicy différemment. Par défaut, ces valeurs sont définies sur ASYNCHRONOUS et PREALLOCATED_WITH_REALLOC dans l’implémentation rmw_fastrtps, et les valeurs définies sur le fichier XML sont ignorées. Afin d’utiliser les valeurs du fichier XML, la variable d’environnement RMW_FASTRTPS_USE_QOS_FROM_XML doit être définie sur 1.

Cependant, cela implique une autre mise en garde : si RMW_FASTRTPS_USE_QOS_FROM_XML est défini, mais que le fichier XML ne définit pas publishMode ou historyMemoryPolicy, ces attributs prennent la valeur par défaut Fast DDS à la place de la valeur par défaut de rmw_fastrtps. Ceci est important, en particulier pour historyMemoryPolicy, car la valeur par défaut Fast DDS est PREALLOCATED qui ne fonctionne pas avec les types de données de sujet ROS2. Par conséquent, dans l’exemple, une valeur valide pour cette stratégie a été explicitement définie (DYNAMIC).

Priorisation de rmw_qos_profile_t

La qualité de service ROS 2 contenue dans rmw_qos_profile_t est toujours respectée, sauf si elle est définie sur *_SYSTEM_DEFAULT. Dans ce cas, les valeurs XML (ou les valeurs par défaut Fast DDS en l’absence de XML) sont appliquées. Cela signifie que si une QoS dans rmw_qos_profile_t est définie sur autre chose que *_SYSTEM_DEFAULT, la valeur correspondante dans le XML est ignorée.

Utilisation d’autres fonctionnalités FastDDS avec XML

Bien que nous ayons créé un nœud avec deux éditeurs avec une configuration différente, il n’est pas facile de vérifier qu’ils se comportent différemment. Maintenant que les bases des profils XML ont été couvertes, utilisons-les pour configurer quelque chose qui a un effet visuel sur les nœuds. Plus précisément, un nombre maximal d’abonnés correspondants sur l’un des éditeurs et une définition de partition sur l’autre seront définis. Notez que ce ne sont que des exemples très simples parmi tous les attributs de configuration qui peuvent être réglés sur rmw_fastrtps via des fichiers XML. Veuillez vous référer à *Fast DDS* documentation pour voir la liste complète des attributs qui peut être configuré via des fichiers XML.

Limiter le nombre d’abonnés correspondants

Ajoutez un nombre maximum d’abonnés correspondants au profil d’éditeur /async_topic. Ça devrait ressembler à ça:

<!-- publisher profile for topic async_topic -->
<publisher profile_name="/async_topic">
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
    <qos>
        <publishMode>
            <kind>ASYNCHRONOUS</kind>
        </publishMode>
    </qos>
    <matchedSubscribersAllocation>
        <initial>0</initial>
        <maximum>1</maximum>
        <increment>1</increment>
    </matchedSubscribersAllocation>
</publisher>

Le nombre d’abonnés correspondants est limité à un.

Ouvrez maintenant trois terminaux et n’oubliez pas de sourcer les fichiers d’installation et de définir les variables d’environnement requises. Sur le premier terminal, exécutez le nœud éditeur et le nœud abonné sur les deux autres. Vous devriez voir que seul le premier nœud d’abonné reçoit les messages des deux sujets. Le second n’a pas pu terminer le processus de correspondance dans le /async_topic car l’éditeur l’en a empêché, car il avait déjà atteint son maximum d’éditeurs correspondants. Par conséquent, seuls les messages du /sync_topic vont être reçus dans ce troisième terminal :

[INFO] [1613127657.088860890] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 18'
[INFO] [1613127657.588896594] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 19'
[INFO] [1613127658.088849401] [sync_async_subscriber]: I heard: 'SYNC: Hello, world! 20'

Utilisation de partitions dans le sujet

La fonction de partitions peut être utilisée pour contrôler quels éditeurs et abonnés échangent des informations dans le même sujet.

Les partitions introduisent un concept de niveau d’isolation d’entité logique à l’intérieur de l’isolation physique induite par un ID de domaine. Pour qu’un éditeur puisse communiquer avec un abonné, il doit appartenir au moins à une partition commune. Les partitions représentent un autre niveau pour séparer les éditeurs et les abonnés au-delà du domaine et du sujet. Contrairement au domaine et au sujet, un point de terminaison peut appartenir à plusieurs partitions en même temps. Pour que certaines données soient partagées sur différents domaines ou sujets, il doit y avoir un éditeur différent pour chacun, partageant son propre historique des modifications. Cependant, un seul éditeur peut partager le même échantillon de données sur différentes partitions à l’aide d’un seul changement de données de sujet, réduisant ainsi la surcharge du réseau.

Changeons l’éditeur /sync_topic pour partitionner part1 et créons un nouvel abonné /sync_topic qui utilise la partition part2. Leurs profils devraient maintenant ressembler à ceci :

<!-- publisher profile for topic sync_topic -->
<publisher profile_name="/sync_topic">
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
    <qos>
        <publishMode>
            <kind>SYNCHRONOUS</kind>
        </publishMode>
        <partition>
            <names>
                <name>part1</name>
            </names>
        </partition>
    </qos>
</publisher>

<!-- subscriber profile for topic sync_topic -->
<subscriber profile_name="/sync_topic">
    <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
    <qos>
        <partition>
            <names>
                <name>part2</name>
            </names>
        </partition>
    </qos>
</subscriber>

Assurez-vous également de supprimer le profil d’abonné « default_subscriber ». Ouvrez deux terminaux. N’oubliez pas de sourcer les fichiers d’installation et de définir les variables d’environnement requises. Sur le premier terminal, exécutez le nœud éditeur et le nœud abonné sur l’autre. Vous devriez voir que seuls les messages /async_topic parviennent à l’abonné. L’abonné /sync_topic ne reçoit pas les données car elles se trouvent dans une partition différente de l’éditeur correspondant.

[INFO] [1612972054.995410057] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 10'
[INFO] [1612972055.995396561] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 11'
[INFO] [1612972056.995473953] [sync_async_subscriber]: I heard: 'ASYNC: Hello, world! 12'

Configurer un service et un client

Les services et les clients ont chacun un éditeur et un abonné, qui communiquent via deux sujets différents. Par exemple, pour un service nommé ping il y a :

  • Un abonné au service écoutant les requêtes sur /rq/ping.

  • Un éditeur de service envoyant des réponses sur /rr/ping.

  • Un éditeur client envoyant des requêtes sur /rq/ping.

  • Un abonné client écoutant les réponses sur /rr/ping.

Bien que vous puissiez utiliser ces noms de rubrique pour définir les profils de configuration sur le XML, vous pouvez parfois souhaiter appliquer le même profil à tous les services ou clients sur un nœud. Au lieu de copier le même profil avec tous les noms de sujets générés pour tous les services, vous pouvez simplement créer une paire de profils d’éditeur et d’abonné nommée service. La même chose peut être faite pour les clients créant une paire nommée client.

Créer les nœuds avec le service et le client

Commencez à créer le nœud avec le service. Ajoutez un nouveau fichier source nommé src/ping_service.cpp sur votre package avec le contenu suivant :

#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/trigger.hpp"

/**
 * Service action: responds with success=true and prints the request on the console
 */
void ping(const std::shared_ptr<example_interfaces::srv::Trigger::Request> request,
        std::shared_ptr<example_interfaces::srv::Trigger::Response> response)
{
    // The request data is unused
    (void) request;

    // Build the response
    response->success = true;

    // Log to the console
    RCLCPP_INFO(rclcpp::get_logger("ping_server"), "Incoming request");
    RCLCPP_INFO(rclcpp::get_logger("ping_server"), "Sending back response");
}

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);

    // Create the node and the service
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("ping_server");
    rclcpp::Service<example_interfaces::srv::Trigger>::SharedPtr service =
        node->create_service<example_interfaces::srv::Trigger>("ping", &ping);

    // Log that the service is ready
    RCLCPP_INFO(rclcpp::get_logger("ping_server"), "Ready to serve.");

    // run the node
    rclcpp::spin(node);
    rclcpp::shutdown();
}

Créez le client dans un fichier nommé src/ping_client.cpp avec le contenu suivant :

#include <chrono>
#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/trigger.hpp"

using namespace std::chrono_literals;

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);

    // Create the node and the client
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("ping_client");
    rclcpp::Client<example_interfaces::srv::Trigger>::SharedPtr client =
        node->create_client<example_interfaces::srv::Trigger>("ping");

    // Create a request
    auto request = std::make_shared<example_interfaces::srv::Trigger::Request>();

    // Wait for the service to be available
    while (!client->wait_for_service(1s)) {
        if (!rclcpp::ok()) {
            RCLCPP_ERROR(rclcpp::get_logger("ping_client"), "Interrupted while waiting for the service. Exiting.");
            return 0;
        }
        RCLCPP_INFO(rclcpp::get_logger("ping_client"), "Service not available, waiting again...");
    }

    // Now that the service is available, send the request
    RCLCPP_INFO(rclcpp::get_logger("ping_client"), "Sending request");
    auto result = client->async_send_request(request);

    // Wait for the result and log it to the console
    if (rclcpp::spin_until_future_complete(node, result) ==
        rclcpp::FutureReturnCode::SUCCESS)
    {
        RCLCPP_INFO(rclcpp::get_logger("ping_client"), "Response received");
    } else {
        RCLCPP_ERROR(rclcpp::get_logger("ping_client"), "Failed to call service ping");
    }

    rclcpp::shutdown();
    return 0;
}

Ouvrez le fichier CMakeLists.txt et ajoutez deux nouveaux exécutables ping_service et ping_client :

find_package(example_interfaces REQUIRED)

add_executable(ping_service src/ping_service.cpp)
ament_target_dependencies(ping_service example_interfaces rclcpp)

add_executable(ping_client src/ping_client.cpp)
ament_target_dependencies(ping_client example_interfaces rclcpp)

install(TARGETS
    ping_service
    DESTINATION lib/${PROJECT_NAME})

install(TARGETS
    ping_client
    DESTINATION lib/${PROJECT_NAME})

Enfin, construisez le package.

Créer les profils XML pour le service et le client

Créez un fichier avec le nom ping.xml avec le contenu suivant :

<?xml version="1.0" encoding="UTF-8" ?>
<profiles xmlns="http://www.eprosima.com/XMLSchemas/fastRTPS_Profiles">

    <!-- default publisher profile -->
    <publisher profile_name="default_publisher" is_default_profile="true">
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
    </publisher>

    <!-- default subscriber profile -->
    <subscriber profile_name="default_subscriber" is_default_profile="true">
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
    </subscriber>

    <!-- service publisher is SYNC -->
    <publisher profile_name="service">
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
        <qos>
            <publishMode>
                <kind>SYNCHRONOUS</kind>
            </publishMode>
        </qos>
    </publisher>

    <!-- client publisher is ASYNC -->
    <publisher profile_name="client">
        <historyMemoryPolicy>DYNAMIC</historyMemoryPolicy>
        <qos>
            <publishMode>
                <kind>ASYNCHRONOUS</kind>
            </publishMode>
        </qos>
    </publisher>

</profiles>

Ce fichier de configuration définit le mode de publication sur SYNCHRONOUS sur le service et sur ASYNCHRONOUS sur le client. Notez que nous ne définissons que les profils d’éditeur pour le service et le client, mais des profils d’abonnés peuvent également être fournis.

Exécuter les nœuds

Ouvrez deux terminaux et sourcez les fichiers de configuration sur chacun d’eux. Définissez ensuite les variables d’environnement requises pour le XML à charger :

export RMW_IMPLEMENTATION=rmw_fastrtps_cpp
export RMW_FASTRTPS_USE_QOS_FROM_XML=1
export FASTRTPS_DEFAULT_PROFILES_FILE=path/to/ping.xml

Sur le premier terminal, exécutez le nœud de service.

ros2 run sync_async_node_example_cpp ping_service

Vous devriez voir le service en attente de demandes :

[INFO] [1612977403.805799037] [ping_server]: Ready to serve

Sur le deuxième terminal, exécutez le nœud client.

ros2 run sync_async_node_example_cpp ping_client

Vous devriez voir le client envoyer la requête et recevoir la réponse :

[INFO] [1612977404.805799037] [ping_client]: Sending request
[INFO] [1612977404.825473835] [ping_client]: Response received

Dans le même temps, la sortie dans la console du serveur a été mise à jour :

[INFO] [1612977403.805799037] [ping_server]: Ready to serve
[INFO] [1612977404.807314904] [ping_server]: Incoming request
[INFO] [1612977404.836405125] [ping_server]: Sending back response