Créer et utiliser des plugins (C++)

Objectif : Apprendre à créer et à charger un plugin simple à l’aide de pluginlib

Niveau du tutoriel : Débutant

Durée : 20 minutes

Arrière-plan

Ce tutoriel est dérivé de http://wiki.ros.org/pluginlib et Writing and Using a Simple Plugin Tutorial.

pluginlib est une bibliothèque C++ pour charger et décharger des plugins à partir d’un package ROS. Les plugins sont des classes chargeables dynamiquement qui sont chargées à partir d’une bibliothèque d’exécution (c’est-à-dire un objet partagé, une bibliothèque liée dynamiquement). Avec pluginlib, il n’est pas nécessaire de lier explicitement leur application à la bibliothèque contenant les classes - à la place, pluginlib peut ouvrir une bibliothèque contenant des classes exportées à tout moment sans que l’application ait connaissance préalable de la bibliothèque ou du fichier d’en-tête contenant la définition de classe . Les plugins sont utiles pour étendre/modifier le comportement de l’application sans avoir besoin du code source de l’application.

Conditions préalables

Ce tutoriel suppose des connaissances de base en C++ et que vous avez installé pluginlib.

sudo apt-get install ros-rolling-pluginlib

Tâches

Dans ce didacticiel, vous allez créer deux nouveaux packages, l’un définissant la classe de base et l’autre fournissant les plugins. La classe de base définira une classe de polygone générique, puis nos plugins définiront des formes spécifiques.

1 Créer le package de classe de base

Créez un nouveau package vide dans votre dossier ros2_ws/src avec la commande de terminal suivante.

ros2 pkg create --build-type ament_cmake polygon_base --dependencies pluginlib --node-name area_node

Ouvrez votre éditeur préféré, éditez ros2_ws/src/polygon_base/include/polygon_base/regular_polygon.hpp et collez-y ce qui suit :

#ifndef POLYGON_BASE_REGULAR_POLYGON_HPP
#define POLYGON_BASE_REGULAR_POLYGON_HPP

namespace polygon_base
{
  class RegularPolygon
  {
    public:
      virtual void initialize(double side_length) = 0;
      virtual double area() = 0;
      virtual ~RegularPolygon(){}

    protected:
      RegularPolygon(){}
  };
}  // namespace polygon_base

#endif  // POLYGON_BASE_REGULAR_POLYGON_HPP

Ce code ci-dessus devrait être assez explicite… nous créons une classe abstraite appelée RegularPolygon. Une chose à noter est la présence de la méthode initialize. Avec pluginlib, un constructeur sans paramètres est requis pour les classes donc, si des paramètres sont requis, nous utilisons la méthode initialize pour initialiser l’objet.

Nous devons rendre cet en-tête disponible pour les autres classes, alors ouvrez ros2_ws/src/polygon_base/CMakeLists.txt pour l’éditer. Ajoutez les lignes suivantes après la commande ament_target_dependencies.

install(
  DIRECTORY include/
  DESTINATION include
)

Et ajoutez cette commande avant la commande ament_package

ament_export_include_directories(
  include
)

Nous reviendrons sur ce package plus tard pour écrire notre nœud de test.

2 Créer le package de plug-in

Nous allons maintenant écrire deux implémentations non virtuelles de notre classe abstraite. Créez un deuxième package vide dans votre dossier ros2_ws/src avec la commande de terminal suivante.

ros2 pkg create --build-type ament_cmake polygon_plugins --dependencies polygon_base pluginlib --library-name polygon_plugins

2.1 Code source des plugins

Ouvrez ros2_ws/src/polygon_plugins/src/polygon_plugins.cpp pour le modifier et collez-y ce qui suit :

#include <polygon_base/regular_polygon.hpp>
#include <cmath>

namespace polygon_plugins
{
  class Square : public polygon_base::RegularPolygon
  {
    public:
      void initialize(double side_length) override
      {
        side_length_ = side_length;
      }

      double area() override
      {
        return side_length_ * side_length_;
      }

    protected:
      double side_length_;
  };

  class Triangle : public polygon_base::RegularPolygon
  {
    public:
      void initialize(double side_length) override
      {
        side_length_ = side_length;
      }

      double area() override
      {
        return 0.5 * side_length_ * getHeight();
      }

      double getHeight()
      {
        return sqrt((side_length_ * side_length_) - ((side_length_ / 2) * (side_length_ / 2)));
      }

    protected:
      double side_length_;
  };
}

#include <pluginlib/class_list_macros.hpp>

PLUGINLIB_EXPORT_CLASS(polygon_plugins::Square, polygon_base::RegularPolygon)
PLUGINLIB_EXPORT_CLASS(polygon_plugins::Triangle, polygon_base::RegularPolygon)

L’implémentation des classes Square et Triangle devrait être assez simple : enregistrez la longueur du côté et utilisez-la pour calculer l’aire. Le seul élément spécifique à pluginlib est les trois dernières lignes, qui invoquent des macros magiques qui enregistrent les classes en tant que plugins réels. Passons en revue les arguments de la macro PLUGINLIB_EXPORT_CLASS :

  1. Le type complet de la classe de plugin, dans ce cas, polygon_plugins::Square.

  2. Le type complet de la classe de base, dans ce cas, polygon_base::RegularPolygon.

2.2 XML de déclaration de plugin

Les étapes ci-dessus permettent de créer des instances de nos plugins une fois que la bibliothèque dans laquelle ils existent est chargée, mais le chargeur de plugins a toujours besoin d’un moyen de trouver cette bibliothèque et de savoir quoi référencer dans cette bibliothèque. À cette fin, nous allons également créer un fichier XML qui, avec une ligne d’exportation spéciale dans le manifeste du package, met toutes les informations nécessaires sur nos plugins à la disposition de la chaîne d’outils ROS.

Créez ros2_ws/src/polygon_plugins/plugins.xml avec le code suivant :

<library path="polygon_plugins">
  <class type="polygon_plugins::Square" base_class_type="polygon_base::RegularPolygon">
    <description>This is a square plugin.</description>
  </class>
  <class type="polygon_plugins::Triangle" base_class_type="polygon_base::RegularPolygon">
    <description>This is a triangle plugin.</description>
  </class>
</library>

Quelques points à noter :

  1. La balise library donne le chemin relatif vers une bibliothèque qui contient les plugins que nous voulons exporter. Dans ROS 2, c’est juste le nom de la bibliothèque. Dans ROS 1, il contenait le préfixe lib ou parfois lib/lib (c’est-à-dire lib/libpolygon_plugins) mais ici c’est plus simple.

  2. La balise class déclare un plugin que nous voulons exporter depuis notre bibliothèque. Passons en revue ses paramètres :

  • type : le type complet du plugin. Pour nous, c’est polygon_plugins::Square.

  • base_class : Le type de classe de base complet pour le plugin. Pour nous, c’est polygon_base::RegularPolygon.

  • description : une description du plugin et de ce qu’il fait.

  • name : il y avait un attribut de nom, mais il n’est plus nécessaire.

2.3 Déclaration du plugin CMake

La dernière étape consiste à exporter vos plugins via CMakeLists.txt. C’est un changement par rapport à ROS 1, où l’exportation se faisait via package.xml. Ajoutez le bloc suivant à votre ros2_ws/src/polygon_plugins/CMakeLists.txt après la ligne indiquant find_package(pluginlib REQUIRED)

add_library(polygon_plugins src/polygon_plugins.cpp)
target_include_directories(polygon_plugins PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
ament_target_dependencies(
  polygon_plugins
  polygon_base
  pluginlib
)

pluginlib_export_plugin_description_file(polygon_base plugins.xml)

install(
  TARGETS polygon_plugins
  EXPORT export_${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin
)

Et avant la commande ament_package, ajoutez

ament_export_libraries(
  polygon_plugins
)
ament_export_targets(
  export_${PROJECT_NAME}
)

Les arguments de cette commande CMake sont

  1. Le package pour la classe de base, c’est-à-dire polygon_base

  2. Le chemin relatif vers la déclaration de plugin xml, c’est-à-dire plugins.xml

3 Utilisez les plugins

Il est maintenant temps d’utiliser les plugins. Cela peut être fait dans n’importe quel package, mais ici nous allons le faire dans le package de base. Modifiez ros2_ws/src/polygon_base/src/area_node.cpp pour contenir les éléments suivants :

#include <pluginlib/class_loader.hpp>
#include <polygon_base/regular_polygon.hpp>

int main(int argc, char** argv)
{
  // To avoid unused parameter warnings
  (void) argc;
  (void) argv;

  pluginlib::ClassLoader<polygon_base::RegularPolygon> poly_loader("polygon_base", "polygon_base::RegularPolygon");

  try
  {
    std::shared_ptr<polygon_base::RegularPolygon> triangle = poly_loader.createSharedInstance("polygon_plugins::Triangle");
    triangle->initialize(10.0);

    std::shared_ptr<polygon_base::RegularPolygon> square = poly_loader.createSharedInstance("polygon_plugins::Square");
    square->initialize(10.0);

    printf("Triangle area: %.2f\n", triangle->area());
    printf("Square area: %.2f\n", square->area());
  }
  catch(pluginlib::PluginlibException& ex)
  {
    printf("The plugin failed to load for some reason. Error: %s\n", ex.what());
  }

  return 0;
}

Le ClassLoader est la classe clé à comprendre, définie dans le class_loader.hpp header.

  • Il est modélisé avec la classe de base, c’est-à-dire polygon_base::RegularPolygon

  • Le premier argument est une chaîne pour le nom du package de la classe de base, c’est-à-dire polygon_base

  • Le deuxième argument est une chaîne avec le type de classe de base entièrement qualifié pour le plugin, c’est-à-dire polygon_base::RegularPolygon

Il existe plusieurs façons d’instancier une instance de la classe. Dans cet exemple, nous utilisons des pointeurs partagés. Nous avons juste besoin d’appeler createSharedInstance avec le type complet de la classe du plugin, dans ce cas, polygon_plugins::Square.

Remarque importante : le package polygon_base dans lequel ce nœud est défini ne dépend PAS de la classe polygon_plugins. Les plugins seront chargés dynamiquement sans qu’aucune dépendance n’ait besoin d’être déclarée. De plus, nous instancions les classes avec des noms de plug-ins codés en dur, mais vous pouvez également le faire de manière dynamique avec des paramètres, etc.

4 Construire et exécuter

Revenez à la racine de votre espace de travail, ros2_ws, et créez vos nouveaux packages :

colcon build --packages-select polygon_base polygon_plugins

À partir de ros2_ws, assurez-vous de sourcer les fichiers d’installation :

. install/setup.bash

Exécutez maintenant le nœud :

ros2 run polygon_base area_node

Il devrait imprimer

Triangle area: 43.30
Square area: 100.00

Résumé

Toutes nos félicitations! Vous venez d’écrire et d’utiliser vos premiers plugins.