ros2 从零开始14 插件

前言

背景

本教程源自http://wiki.ros.org/pluginlib
pluginlib 是C++ 库, ROS 包内加载和卸载插件的。 插件是可动态加载的类,从运行时库(即共享对象、动态链接库)加载出来。 使用 pluginlib,你不必显式地将应用与包含类的库关联——而是可以在任何时刻打开包含导出类的库。 插件对于扩展/修改应用行为非常有用,无需应用源代码。
简单来说,pluginlib是ros节点运行时动态加载上的。有点类似于平时我们用dl_open来动态加载,只不过ros2把它封装了,使用比较方便些。
在我们的工程高度抽象时,我们更新pluginlib库,可以在不动基础代码的情况下,就可以完成功能升级。

实践

1.创建一个包

进入工作区的src目录,用如下指令创建我们的基类包

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

polygon_base 是我们的本次的包名,area_node是它里面的一个节点。

root@bc2bf85b2e4a:~/ros2_ws# cd ~/ros2_ws/src/
root@bc2bf85b2e4a:~/ros2_ws/src# ros2 pkg create --build-type ament_cmake --license Apache-2.0 --dependencies pluginlib --node-name area_node polygon_base
going to create a new package
package name: polygon_base
destination directory: /root/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['root <****@****>']
licenses: ['Apache-2.0']
build type: ament_cmake
dependencies: ['pluginlib']
node_name: area_node
creating folder ./polygon_base
creating ./polygon_base/package.xml
creating source and include folder
creating folder ./polygon_base/src
creating folder ./polygon_base/include/polygon_base
creating ./polygon_base/CMakeLists.txt
creating ./polygon_base/src/area_node.cpp

2. 创建pluginlib库

2.1 新增基类头文件

polygon_base/include/polygon_base/目录新建文件regular_polygon.hpp,并且内容添加如下

#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

以上代码定义一个命名空间是polygon_base的基类RegularPolygon, 基类有个虚方法initialize用于初始化。同时有个无参构造函数RegularPolygon(){}, 无参构造函数是pluginlib要求的。如果想要给对象传替参数,就调用initialize即可。

2.2 修改CMakeLists.txt

我们依赖pluginlib,CMakeLists.txt理应有find_package(pluginlib REQUIRED)选项(如果在创建包时没有–dependencies pluginlib,则需要手动添加),在find_package(pluginlib REQUIRED)之后,添加如下内容

# 创建接口库和别名
add_library(${PROJECT_NAME} INTERFACE)
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
# 设置编译标准
target_compile_features(${PROJECT_NAME} INTERFACE c_std_99 cxx_std_17)
# 设置头文件包含路径(使用生成器表达式)
target_include_directories(${PROJECT_NAME} INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include/${PROJECT_NAME}>
)
# 链接依赖库
target_link_libraries(${PROJECT_NAME} INTERFACE ${pluginlib_TARGETS})

# 安装头文件
install(DIRECTORY include/
  DESTINATION include/${PROJECT_NAME}
)

# 安装库并导出目标
install(TARGETS ${PROJECT_NAME}
  EXPORT export_${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin
)
# 安装导出文件
install(EXPORT export_${PROJECT_NAME}
  NAMESPACE ${PROJECT_NAME}::
  DESTINATION share/${PROJECT_NAME}/cmake
)

同时,在ament_package 之前添加如下内容:

# 导出旧式 CMake 变量
ament_export_include_directories(
  include
)

# 导出现代 CMake 目标
ament_export_targets(
  export_${PROJECT_NAME}
)

完整的CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.8)
project(polygon_base)

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

# find dependencies
find_package(ament_cmake REQUIRED)
find_package(pluginlib REQUIRED)
# 创建接口库和别名
add_library(${PROJECT_NAME} INTERFACE)
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
# 设置编译标准
target_compile_features(${PROJECT_NAME} INTERFACE c_std_99 cxx_std_17)
# 设置头文件包含路径(使用生成器表达式)
target_include_directories(${PROJECT_NAME} INTERFACE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include/${PROJECT_NAME}>
)
# 链接依赖库
target_link_libraries(${PROJECT_NAME} INTERFACE ${pluginlib_TARGETS})

# 安装头文件
install(DIRECTORY include/
  DESTINATION include/${PROJECT_NAME}
)

# 安装库并导出目标
install(TARGETS ${PROJECT_NAME}
  EXPORT export_${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin
)
# 安装导出文件
install(EXPORT export_${PROJECT_NAME}
  NAMESPACE ${PROJECT_NAME}::
  DESTINATION share/${PROJECT_NAME}/cmake
)


add_executable(area_node src/area_node.cpp)
target_include_directories(area_node PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
target_compile_features(area_node PUBLIC c_std_99 cxx_std_17)  # Require C99 and C++17
ament_target_dependencies(
  area_node
  "pluginlib"
)

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

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

# 导出旧式 CMake 变量
ament_export_include_directories(
  include
)

# 导出现代 CMake 目标
ament_export_targets(
  export_${PROJECT_NAME}
)

ament_package()

3. 实现抽象类(实现插件具体代码)

现在我们将写抽象类的两个非虚实现。 在文件夹src里用以下命令创建第二个包
ros2 pkg create --build-type ament_cmake --license Apache-2.0 --dependencies polygon_base pluginlib --library-name polygon_plugins polygon_plugins,注意看,我们第二个包polygon_plugins依赖于包polygon_basepluginlib
执行如下:

root@bc2bf85b2e4a:~/ros2_ws/src# ros2 pkg create --build-type ament_cmake --license Apache-2.0 --dependencies polygon_base pluginlib --library-name polygon_plugins polygon_plugins
  going to create a new package
  package name: polygon_plugins
  destination directory: /root/ros2_ws/src
  package format: 3
  version: 0.0.0
  description: TODO: Package description
  maintainer: ['root <****@****>']
  licenses: ['Apache-2.0']
  build type: ament_cmake
  dependencies: ['polygon_base', 'pluginlib']
  library_name: polygon_plugins
  creating folder ./polygon_plugins
  creating ./polygon_plugins/package.xml
  creating source and include folder
  creating folder ./polygon_plugins/src
  creating folder ./polygon_plugins/include/polygon_plugins
  creating ./polygon_plugins/CMakeLists.txt
  creating ./polygon_plugins/include/polygon_plugins/polygon_plugins.hpp
  creating ./polygon_plugins/src/polygon_plugins.cpp
  creating ./polygon_plugins/include/polygon_plugins/visibility_control.h
3.1 修改实现

打开进行编辑文件polygon_plugins/src/polygon_plugins.cpp,并粘贴以下内容:

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

解析:
正方形和三角形的实现相当简单:保存边长,并用它来计算面积。 跟插件库的有关的部分是最后三行,调用了一些神奇的宏PLUGINLIB_EXPORT_CLASS,将类注册为真正的插件。PLUGINLIB_EXPORT_CLASS(class_type, base_class_type)

  • class_type-插件类的全限定类型,在此例中为polygon_plugins::Squarepolygon_plugins::Triangle
  • base_class_type-基类的全限定类型,在此例中为polygon_base::RegularPolygon
3.2 新增plugins.xml

上述步骤允许在加载包含库时创建插件实例,但插件加载器仍需找到该库并知道在库中引用的内容。 为此,我们新增plugins.xml文件(在polygon_plugins包目录下),配合包清单中的特殊导出行,使插件的所有必要信息都能在 ROS 工具链中获得。

<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" name="awesome_triangle">
    <description>This is a triangle plugin.</description>
  </class>
</library>

有几点需要注意:
标签library给出了包含我们想导出插件的库的相对路径。 在ROS2中,那只是包的名字。 但是在ROS1中,它包含前缀lib或者lib/libname(例如:lib/libpolygon_plugins),但在这里更简单。

标签class 声明了一个我们想从库导出的插件。 让我们来看看它的参数:

  • type:插件的完全限定类型。 对我们来说,就是polygon_plugins::Square
  • base_class:插件的完全限定基类类型。 对我们来说,就是polygon_base::RegularPolygon
  • description: 插件及其功能描述。
  • name(可选):类加载器使用的查找名称。
3.3 修改CMakeLists.txt

最后一步是通过CMakeLists.txt导出你的插件。在你的行find_package(pluginlib REQUIRED)后加上以下一句:

pluginlib_export_plugin_description_file(polygon_base plugins.xml)

命令pluginlib_export_plugin_description_file的参数如下:

  • 带有基类的包,本例是polygon_base
  • 插件声明XML的相对路径,本例是plugins.xml

4. 使用插件

现在我们可以使用插件了,可以在任意的包中使用,但是本例我们在之前的基类包polygon_base中使用,编辑文件polygon_base/src/area_node.cpp,将如下内容贴上:

#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("awesome_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;
}

ClassLoader是需要理解的关键类,定义在头文件pluginlib/class_loader.hpp中:

  • 它采用了基类模板polygon_base::RegularPolygon
  • 第一个参数是基类包名的字符串polygon_base
  • 第二个参数是一个带有插件完全限定基类类型的字符串 polygon_base::RegularPolygon(命名空间+基类名)。

有多种方式可以实例化该类的实例。 在这个例子中,我们调用poly_loader.createSharedInstance("awesome_triangle");poly_loader.createSharedInstance("polygon_plugins::Square"); 。入参可以是插件类的全限定类型type(声明XML文件的属性,本例是polygon_plugins::Square),也可以是可选的名称name(声明XML文件的属性,本例是awesome_triangle)。

5. 编译

进入工作区,用colcon build ,等待编译完成。

root@bc2bf85b2e4a:~/ros2_ws# colcon build --packages-select polygon_base polygon_plugins
Starting >>> polygon_base
Finished <<< polygon_base [0.11s]                  
Starting >>> polygon_plugins
Finished <<< polygon_plugins [0.71s]                    

Summary: 2 packages finished [0.98s]
root@bc2bf85b2e4a:~/ros2_ws# 

6. 运行查看

编译完成后,我们需要安装后才能运行,使用

source install/setup.sh

使用ros2 plugin list查看插件:

root@bc2bf85b2e4a:~/ros2_ws# ros2 plugin list
polygon_plugins:
        Plugin(name='polygon_plugins::Square', type='polygon_plugins::Square', base='polygon_base::RegularPolygon')
        Plugin(name='awesome_triangle', type='polygon_plugins::Triangle', base='polygon_base::RegularPolygon')

接下来,输入如下命令ros2 run polygon_base area_node,运行节点, 如下:

root@bc2bf85b2e4a:~/ros2_ws# ros2 run polygon_base area_node
Triangle area: 43.30
Square area: 100.00

可以看到,节点正常运行,并正确计算了面积。

总结

完成了插件的介绍,之后就是中级教程了。中级教程会涉及到动作actions ,敬请期待。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐