ROS插件系统pluginlib
我们在玩机器人的时候,经常会遇到各种各样的选择问题。就以双目摄像头来说吧,市场上有kinect, realsense等等,针对不同的传感器都有一套开发环境和驱动。 为了确定哪个更好用,我们常常会每一种都试一试。类似的还有执行器、算法的选择。如果让我们为每一种选择都搭建一个独立的系统,肯定会有很多重复工作, 而且那么多种组合需要搭建多少个系统呢,每种选择所碰到的坑可能都要再重新爬一边,想一想都是一个很繁琐的过程。
如果能够实现代码或者设备的复用,每种模块只需要实现一次,在构建系统的时候根据选择做些简单的配置,那将是一种多么幸福的体验。 ROS提供了一套插件系统pluginlib来实现这一功能, 它是一个C++库,用于在ROS的Node中动态的装载或者卸载插件。所谓的插件,实际上就是一个动态链接库中为完成某一项功能而编写的一套函数和类结构。 这样我们在编写Node的代码时候就不必立即决定使用那种传感器、执行器和算法了,只要先对这些选择的接口进行实现。具体的设备驱动和算法库就可以写成插件, 在使用的时候通过ROS的基础设施(roslaunch,rosrun等工具)配置系统的选择就可以了。
在本文中,我们根据官方的教程介绍如何通过pluginlib为Node建立插件系统。 这个插件系统只是为我们提供了实现各种可能的基础,如何能够让系统更具有扩展性,降低各个模块之间的耦合性, 我们还需要在设计模式上下一些功夫。
1. pluginlib的工作原理
让我们来假想一种情况,做为以后写设计模式系列文章的准备。我们要制造一只简单的小黄鸭, 和一只快乐的小绿鸭。虽然外观上看起来它们只是颜色不一样,但是它们的内在也是很不相同的。 不过好在它们都是鸭子,有很多共同的标签和特性。按照大多数C++的套路,我们应该先定义一个叫做Duck的基类,然后通过继承这个基类派生出两个子类SimpleDuck和HappyDuck。 小黄鸭是SimpleDuck的一个实现,或者说是对象,小绿鸭则是HappyDuck的一个对象。
本质上,插件和基类都是接口。插件是应用程序的接口,它通过特定的函数加载用户编写的功能模块。同样的插件需要通过调用应用程序暴露出来的API来访问应用程序的资源和原生的功能。 如果应用程序是一个屋子,那么用于加载插件的函数就好像是屋里面的插座,插件就是各种家用电器。冰箱也好,电视机也罢都要满足用电的标准。插件的开发也一样, 需要满足应用程序的各种约定。
基类是程序中对象的接口。在C++的世界中,我们通过class描述了各种各样的事物。有些事物具有一些共同的特性,但在细微之处还是有些微的不一样。 我们把这些共同的特性提取出来,就定义了基类。派生而来的子类,则体现了个性的地方。这样我们在程序中就可以对一类事物进行操作,而不必为每一个具体的类型重复工作。 这要感谢C++中的多态、继承等高级特性。
在基于pluginlib的插件系统中,插件是各个可以运行时动态装载的类,它们保存在动态链接库中。在应用程序中,我们通过pluginlib动态加载插件,但是由于我们并不知道插件的具体实现是什么, 所以需要提供一个基类,这样我们只需要针对这个基类编程就好了。这也就是很多人推崇的面向接口而不是面向实现的编程,具有更强的可扩展性。
上图是pluginlib应用的一个例子,在应用程序(Application)中定义Duck。通过成员函数initilize()实现对象的初始化工作,get_type()获取鸭子的具体类型,get_name()则返回具体对象的名字。 而我们的两个子类SimpleDuck和HappyDuck均派生自Duck类,并各自在一个Plugin中实现。实现只是建立插件系统的第一步,为了让整个系统得以运行还需要告知ROS系统存在这么两个插件, 并且在应用程序中加载这些插件。
下面我们对官方教程稍作修改,以这里的鸭子为例再讲一遍。
2. 插件的实现
首先我们在'~/catkin_ws/src'中创建一个'learning_plugin'的包,它依赖于两个包。roscpp是ROS中C++语言的基础,pluginlib则是这里插件系统的主角。
$ cd ~/catkin_ws/src
$ catkin_create_pkg learning_pluginlib roscpp pluginlib
然后在learning_plugin的include子目录下创建duck.h头文件:
$ cd learning_pluginlib/include/learning_pluginlib
$ vim duck.h
并在其中定义Duck基类,这是一个抽象类,其中initialize是一个抽象函数。所以我们不可能直接建立一个Duck的对象,只能从其派生而来的子类开始。
我们把默认构造函数定义为protected的权限,这是pluginlib的要求,必须提供一个不需要参数的构造函数。如果需要配置任何参数都是通过虚函数initialize()传递的。
其它的成员函数和变量都有充分的自解释能力,不再详述了。
#ifndef LEARNING_PLUGINLIB_DUCK_H
#define LEARNING_PLUGINLIB_DUCK_H
#include <string>
namespace learning_pluginlib {
class Duck {
public:
virtual void initialize(std::string const & name) = 0;
virtual std::string const & get_name(void) const { return mName; }
virtual std::string const & get_type(void) const { return mType; }
virtual ~Duck() {}
protected:
Duck() {}
protected:
std::string mName;
std::string mType;
};
};
#endif
我们再分别创建两个头文件simple_duck.h和happy_duck.h,并实现了SimpleDuck和HappyDuck。这里我们只实现它们的虚函数initialize()用来根据参数name设定从Duck继承而来的成员变量mName和mType。
|
|
3. 插件的注册
现在我们已经成功的创建了基类Duck以及两个派生子类SimpleDuck和HappyDuck。现在我们需要通过ROS的各种基础设施和pluginlib的框架约定注册插件。 首先,我们分别为两个插件创建一个cpp的源文件,命名为simple_duck.cpp和happy_duck.cpp:
$ cd ~/catkin_ws/src/learning_pluginlib/src
$ touch simple_duck.cpp
$ touch happy_duck.cpp
并分别写入如下内容。在第一行中,我们引入了头文件<pluginlib/class_list_macros.h>,这样就可以在文件的最后使用宏PLUGINLIB_EXPORT_CLASS声明。
这个宏有两个参数,第一个是插件的类,第二个则是基类。
|
|
include_directories(include)
add_library(simple_duck_plugin src/simple_duck.cpp)
add_library(happy_duck_plugin src/happy_duck.cpp)
如果程序中没有什么问题的话,就可以通过catkin_make编译插件了。
$ cd ~/catkin_ws
$ catkin_make
在使用这些插件的时候,pluginlib还需要知道从哪里找到动态链接库,以及从库中装载哪些插件。ROS通过一个xml文件描述需要加载插件的各种属性,
我们在learning_plugin的根目录中创建一个duck_plugins.xml文件:
$ cd ~/catkin_ws/src/learning_pluginlib
$ touch duck_plugins.xml
这个文件必须与CMakeLists.txt和package.xml两个文件在一个目录下。在这个文件中描述了两个插件,每个插件主要有library和class两个标签。
其中library中的path属性指示了动态链接库的路径,这里是相对于~/catkin_ws/devel的路径。class标签则描述了需要加载的子类类型和基类类型。
<library path="lib/libsimple_duck_plugin">
<class type="learning_pluginlib::SimpleDuck" base_class_type="learning_pluginlib::Duck">
<description>简单的鸭子</description>
</class>
</library>
<library path="lib/libhappy_duck_plugin">
<class type="learning_pluginlib::HappyDuck" base_class_type="learning_pluginlib::Duck">
<description>快乐的鸭子</description>
</class>
</library>
接着需要在package.xml中添加如下语句,其中标签名称"learning_pluginlib"应当与插件的基类所在的包名称一致。这里基类以及派生出来的插件子类都在一个包里,
但很多实际情况它们都是分属于不同的包。属性plugin则指出了插件描述文件。
<export>
<learning_pluginlib plugin="${prefix}/duck_plugins.xml" />
</export>
为了确认ROS系统能够正确的找到插件描述文件,我们可以通过如下的指令,先编译工作空间并导入工作环境,最后通过rospack指令查询插件描述文件:
$ cd ~/catkin_ws
$ catkin_make
$ source devel/setup.bash
$ rospack plugins --attrib=plugin learning_pluginlib
learning_pluginlib /home/gyc/catkin_ws/src/learning_pluginlib/duck_plugins.xml
4. 使用插件
现在我们已经创建并注册了插件,下面我们用一个简单的例子介绍一下如何在程序中使用插件。下面我们在learning_pluginlib/src目录下创建一个duck_loader.cpp的文件,首先inlude两个头文件。 class_loader.h是pluginlib的一个基础设施,用于在应用程序中装载插件。duck.h则是我们之前定义的基类文件。
#include <pluginlib/class_loader.h>
#include <learning_pluginlib/duck.h>
接下来创建一个main()函数,并在函数的一开始构建一个duck_loader对象,它的构造函数有两个参数,第一个参数用于指引基类所在的ROS包,第二个参数则说明了插件系统的基类。
int main(int argc, char* argv[])
{
pluginlib::ClassLoader duck_loader("learning_pluginlib", "learning_pluginlib::Duck");
在try-catch语句块中,我们通过duck_loader先后创建了yellow_duck和green_duck。它们分别是SimpleDuck和HappyDuck的对象,通过initialize函数指定对象的名称并完成一些初始化工作。
最后通过ROS_INFO打印两只鸭子的名称和类型。如果没有成功装载插件,可以通过PluginlibException捕获到。
try {
boost::shared_ptr yellow_duck = duck_loader.createInstance("learning_pluginlib::SimpleDuck");
yellow_duck->initialize("YellowDuck");
boost::shared_ptr green_duck = duck_loader.createInstance("learning_pluginlib::HappyDuck");
green_duck->initialize("GreenDuck");
ROS_INFO("%s is a %s", yellow_duck->get_name().c_str(), yellow_duck->get_type().c_str());
ROS_INFO("%s is a %s", green_duck->get_name().c_str(), green_duck->get_type().c_str());
} catch(pluginlib::PluginlibException & ex) {
ROS_ERROR("Failed to load plugin. Error: %s", ex.what());
}
最后返回退出。
return 0;
}
要编译运行这个例程,我们需要先在CMakeLists.txt文件中添加如下语句:
add_executable(duck_loader src/duck_loader.cpp)
target_link_libraries(duck_loader ${catkin_LIBRARIES})
然后切换到工作空间的根目录中编译运行:
$ cd ~/catkin_ws
$ catkin_make
$ rosrun learning_pluginlib duck_loader
[ INFO] [1543151120.866535342]: YellowDuck is a SimpleDuck
[ INFO] [1543151120.866585747]: GreenDuck is a HappyDuck
5. 总结
pluginlib为ROS应用程序提供了插件系统,使得我们可以在应用程序中针对一个基类也就是插件的接口编程。借助pluginlib我们可以动态的加载C++类型, 但需要提供一个描述插件的存放目录和欲装载的类型及其基类名称的xml文件,并在包描述文件package.xml中添加插件描述文件的包和路径。