domingo, 17 de abril de 2022

ROS2: Executables, Plugins and Components

    Introduction

     In ROS-2, we have basically three types of code-run generation:  an executable, a plugin and a component. 

    An executable is a code that will be compiled into a fully functional application, which means it must have a int main () { } function. 

    A plugin is a dynamically loadable class that is loaded from a runtime library, like a shared object or a DLL. It doesn't require that this class is linked to an application to be used. It will be linked and accessed in runtime.

    A component is a code that, like plugins, is intent to be loaded at runtime. It is built into a shared library and will register itself with special macros from the package rclcpp_components. In fact, they are special plugins.


    Plugins x Components

    A plugin must declare his base class when registering to class_loader factory scheme, to allow the factory to be able to instantiate that class as a shared object pointer that can be accessed by the running program.

    Therefore, we start creating a plugin by making a base class package an then use it to build our plugin package

    Also, we need to export our classes (base and plugin) by using PLUGINLIB_EXPORT_CLASS

    Finally, we need to setup a ".xml" file with the plugin meta-configuration so the plugin is available to the ROS toolchain and therefore the plugin loader will be able to find our library.

    We also need to export that .xml file in the package.xml with the a tag <(classname) plugin='(xml file)' />

    A component, on the other hand, relies on void pointers, therefore allowing for a simpler structure, working as a wrapper class to a common base class that is the "base class" for all components.

    Thus, we're not expected to call for a factory to instantiate the component.


Creating a Simple Executable

    We've already done this many times. If you take a look at the hello world example code, you'll see that all we have to do to build an executable is to have one and only one file with main (). 

    This is important: although we could have many cpp files to define our code, we do need one and only one of them to have the int main () { }  method. That cpp file will be the root for our application.

    Also, most of the time we need to properly specify the compile directives on CMakeList.txt file, such as


add_executable(our_application_binary_name src/our_application_code.cpp)



Creating and Using a Simple Plugin

    First, make sure you have the pluginlib installed:

sudo apt-get install ros-galactic-pluginlib
    Then, as we said, let's create a base class for our plugin. This post follows the instructions in ROS-2 documentation plugin tutorial.

    We start by defining a package called polygon_base

ros2 pkg create --build-type ament_cmake polygon_base --dependencies pluginlib
    After that, we create a file called include/polygon_base/regular_polygon.hpp with the following code:


#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(){}
};
}

#endif


    Then, we add the following to CMakeLists.txt so the include files are properly generated


install(
DIRECTORY include/
DESTINATION include
)

ament_export_include_directories(
include
)


    We're can build our base package already
colcon build --packages-select polygon_base
   
    Next, let's create the plugin package:

cd src/
ros2 pkg create --build-type ament_cmake polygon_plugins --dependencies polygon_base pluginlib --library-name polygon_plugins
cd ..

    Notice that we are pointing the base package as a dependency to our plugin package. That is correct. A dependency does exist between the plugin and its base package. What is not necessary is to have a dependency between the plugin package and the application package that will use the plugin.

    Now, we can see that ros2 generated a class for us, with a constructor and a destructor.


#include "polygon_plugins/polygon_plugins.hpp"

namespace polygon_plugins
{

PolygonPlugins::PolygonPlugins()
{
}

PolygonPlugins::~PolygonPlugins()
{
}

} // namespace polygon_plugins


    We'll modify this file and the hpp to create two classes, a Triangle and a Square, that implement a Regular Polygon:


HEADER FILE


#ifndef POLYGON_PLUGINS__POLYGON_PLUGINS_HPP_
#define POLYGON_PLUGINS__POLYGON_PLUGINS_HPP_

#include <cmath>
#include "polygon_base/regular_polygon.hpp"
#include "polygon_plugins/visibility_control.h"

namespace polygon_plugins
{
class Triangle : public polygon_base::RegularPolygon
{
protected:
double side;

public:
void initialize(double side_length);
double area();
double height();
};

class Square : public polygon_base::RegularPolygon
{
protected:
double side;

public:
void initialize(double side_length);
double area();
};

} // namespace polygon_plugins

#endif // POLYGON_PLUGINS__POLYGON_PLUGINS_HPP_


CODE FILE


#include "polygon_plugins/polygon_plugins.hpp"

namespace polygon_plugins
{
void Square::initialize(double side_length)
{
this->side = side_length;
}

double Square::area() {
return this->side * this->side;
}
void Triangle::initialize(double side_length)
{
this->side = side_length;
}

double Triangle::height() {
return sqrt((this->side * this->side) - ((this->side / 2) * (this->side / 2)));
}

double Triangle::area() {
return 0.5 * this->side * this->height();
}

} // namespace polygon_plugins

#include <pluginlib/class_list_macros.hpp>

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




    Now, create a plugins.xml file on the base path of your package with:

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


    Now, we need to add the following line to the CMakeLists.txt to make it see our plugins.xml config file. Please add it just below target_include_directories:


pluginlib_export_plugin_description_file(polygon_base plugins.xml)


    We're can build our plugin package now
colcon build --packages-select polygon_plugins

    Finally, let's create a third package that will use the plugin we've build in the polygon_plugin package:

cd src/
ros2 pkg create --build-type ament_cmake polygon_calc --dependencies pluginlib polygon_base --node-name area_node
cd ..

    Edit the file src/area_node.cpp to:

#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;
}

    Now we can compile it and run our area_node application:

colcon build --packages-select polygon_calc
ros2 run polygon_calc area_node
Triangle area: 43.30
Square area: 100.00


Creating and Using a Simple Component

    Let's create two components, a publisher and a subscriber for a hello world string message

cd src/
ros2 pkg create --build-type ament_cmake comp_pub --dependencies rclcpp
ros2 pkg create --build-type ament_cmake comp_subs --dependencies rclcpp
    Now, for the message format, let's create another package:

ros2 pkg create --build-type ament_cmake comp_msg --dependencies rclcpp
    Lets setup the message format first:

    string message
    uint8 order

    Then, for the CMakeLists.txt, add before ament_package():


find_package(rosidl_default_generators REQUIRED)

set(msg_format_file "msg/PubSubExample.msg")

rosidl_generate_interfaces(${PROJECT_NAME} ${msg_format_file})

ament_export_dependencies(rosidl_default_generators)


    For the package.xml, add
   
  <buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>




    Now, for the comp_pub package, let's create a file called src/message_publisher.cpp


#include <memory>
#include <string>
#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_components/register_node_macro.hpp>

#include "comp_msg/msg/pub_sub_example.hpp"

namespace comp_pub
{
class MessagePublisher : public rclcpp::Node
{
private:
rclcpp::Publisher<comp_msg::msg::PubSubExample>::SharedPtr publisher;
rclcpp::TimerBase::SharedPtr timer;
int order;

public:
MessagePublisher(const rclcpp::NodeOptions &options =
rclcpp::NodeOptions()) :
                             Node("MessagePublisher", options)
{
publisher = this->create_publisher<comp_msg::msg::PubSubExample>(
                "message_pub_topic", 10);

timer = this->create_wall_timer(
std::chrono::seconds(1),
[this]() -> void //
{ //
auto msg = comp_msg::msg::PubSubExample();
msg.message = "A simple publisher message";
msg.order = ++this->order;
RCLCPP_INFO(this->get_logger(), "publishing message\n");
this->publisher->publish(msg);
} //
);
}
};
}

RCLCPP_COMPONENTS_REGISTER_NODE(comp_pub::MessagePublisher)



The main difference here is that the constructor must receive a parameter NodeOption and we must call a macro to register the component on ROS

Next, let's setup CMakeList.txt file to add compile information to build our component.

We need to add find_package to include the components module and our message format package.


find_package(rclcpp_components REQUIRED)
find_package(comp_msg REQUIRED)


    We also need to add our code as a library, with the corresponding dependencies and node registration. Notice that we're using comp_sub as a namespace on our code, so we need to put it here also, when registering it.


add_library(msg_publisher SHARED src/message_publisher.cpp)

target_include_directories(msg_publisher PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)

ament_target_dependencies(msg_publisher
"comp_msg"
"rclcpp"
"rclcpp_components")

rclcpp_components_register_node(msg_publisher PLUGIN
"comp_pub::MessagePublisher"
EXECUTABLE msg_publisher_server)

install(TARGETS
msg_publisher
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)




Finally, make sure you add the following lines to package.xml



<depend>comp_msg</depend>

<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>comp_msg</buildtool_depend>
<buildtool_depend>rclcpp</buildtool_depend>
<buildtool_depend>rclcpp_components</buildtool_depend>



Next, let's build our subscriber, which is, as we saw in earlier posts, a client for the publisher. On the comp_subs package, edit the file src/message_subscriber.cpp with the following code:


#include <memory>
#include <string>
#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_components/register_node_macro.hpp>

#include "comp_msg/msg/pub_sub_example.hpp"

using std::placeholders::_1;

namespace comp_sub
{
class MessageSubscriber : public rclcpp::Node
{
private:
rclcpp::Subscription<comp_msg::msg::PubSubExample>::SharedPtr subscriber;

public:
MessageSubscriber (const rclcpp::NodeOptions &options =
rclcpp::NodeOptions()) :
                             Node("message_subscriber_client", options)
{
subscriber = this->create_subscription<comp_msg::msg::PubSubExample>(
"message_pub_topic", 10,
            std::bind(&MessageSubscriber::read_message, this, _1));
}

void read_message(const comp_msg::msg::PubSubExample &msg)
{
RCLCPP_INFO(this->get_logger(),
"received data: { message: '%s', order: '%u' }",
msg.message.c_str(),
msg.order);
}
};
}

RCLCPP_COMPONENTS_REGISTER_NODE(comp_sub::MessageSubscriber)


Now we need to follow the same steps as we did for the publisher on CMakeLists.txt and package.xml

Add to CMakeLists.txt before ament_package()

find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(comp_msg REQUIRED)

add_library(msg_subscriber SHARED src/message_subscriber.cpp)

target_include_directories(msg_subscriber PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)

ament_target_dependencies(msg_subscriber
"comp_msg"
"rclcpp"
"rclcpp_components")

rclcpp_components_register_node(msg_subscriber PLUGIN
"comp_sub::MessageSubscriber"
EXECUTABLE msg_subscriber_client)

install(TARGETS
msg_subscriber
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)



Add the following to packages.xml


<depend>comp_msg</depend>

<buildtool_depend>ament_cmake</buildtool_depend>
<buildtool_depend>comp_msg</buildtool_depend>
<buildtool_depend>rclcpp</buildtool_depend>
<buildtool_depend>rclcpp_components</buildtool_depend>



    And we're done creating two components, each running from a different package. We could put them into the same package as well, following the same instructions, but instead of creating another package, we put the configs and code in the same one. 

    Now, we can run our components by typing:

Publisher
. install/local_setup.bash
ros2 run comp_pub msg_publisher_server    
Subscriber
. install/local_setup.bash
ros2 run comp_sub msg_subscriber_client    


Nenhum comentário:

Postar um comentário

ROS2: Executables, Plugins and Components

    Introduction       In ROS-2, we have basically three types of code-run generation:  an executable, a plugin and a component.       An ex...