在ROS1中,nodelet软件包旨在提供在同一进程中运行多个算法(节点)的方式,不同算法之间通过传递指向数据的指针来代替了数据本身的传输(类似于编程传值与传址的区别),从而实现零成本的数据拷贝。在ROS2中,nodelet升级为更强大的Components。
nodelet和Components都是基于class_loader实现的。nodelet是对pluginlib的进一步封装,Components本质是有ROS2 接口的plugins。
nodelet能够实现以下功能:
nodelet:Nodelet,它将用于动态加载。所有的节点都将继承这个基类,并且可以使用pluginlib动态加载nodelet:NodeletLoader加载一个或多个节点。它们之间的任何通信都可以使用boost共享指针实现零拷贝roscpp发布调用,如果希望no-copy pub/sub工作,必须将消息发布为shared_ptr。ros::Publisher pub = nh.advertise<std_msgs::String>("topic_name", 5);
std_msgs::StringPtr str(new std_msgs::String);
str->data = "hello world";
pub.publish(str);
注意,当用这种方式发布,你和roscpp之间的隐性契约:你不可以修改你发送后的消息,因为指针会直接传递到用户的任何进程内。如果你想发送另一条消息,你必须分配一个新的。
onInit()函数中使用std::shared_ptr而非boost::shared_ptr导致[FATAL] [1673255629.474497531]: Failed to load nodelet '/fastlio_plugin_loader` of type `fast_lio/fastlio_plugin` to manager `nodelet_manager'
[nodelet_manager-1] process has died [pid 1170639, exit code -6, cmd /opt/ros/noetic/lib/nodelet/nodelet manager __name:=nodelet_manager __log:=/home/long/.ros/log/1496afea-8ffa-11ed-913a-07d0641c7442/nodelet_manager-1.log].
plugins.xml中没有加name属性导致[ERROR] [1673261540.209231901]: Failed to load nodelet [/fastlio_plugin_loader] of type [fast_lio/fastlio_plugin] even after refreshing the cache: According to the loaded plugin descriptions the class fast_lio/fastlio_plugin with base class type nodelet::Nodelet does not exist.
# Launch a nodelet manager node
nodelet manager
# Launch a nodelet of type pkg/Type on m_name manager
nodelet load pkg/Type m_name
# Launch a nodelet of type pkg/Type in a standalone node
nodelet standalone pkg/Type
# Unload a nodelet a nodelet by name from manager
nodelet unload name manager
rosrun nodelet nodelet manager __name:=mymanager
__name:= 用于设置管理器名称。
添加第一个节点
rosrun nodelet nodelet load nodelet_tutorial_math/Plus mymanager __name:=n1 _value:=100
nodelet 的节点名称是: /n1
添加的参数名称是: /n1/value。
添加第二个节点
rosrun nodelet nodelet load nodelet_tutorial_math/Plus mymanager __name:=n2 _value:=-50 /n2/in:=/n1/out
原始独立节点的main函数在main.cpp文件中,将其main函数更改名称(除main外的任意名称),比如mainFunction,同时还需新建头文件main.h,main.h中声明mainFunction函数
//main.h
#ifndef MAIN_H
#define MAIN_H
int mainFunction();
#endif
//main.h
//main.cpp
#define NODELET //切换独立节点与Nodelet节点的宏定义
#ifdef NODELET
#include "main.h"
#endif
#endif
#ifdef NODELET
int mainFunction()
{
int argc; char** argv;
ros::init(argc, argv, "laserMapping");
#else
int main(int argc, char** argv)
{
ros::init(argc, argv, "laserMapping");
#endif
//main.cpp
新建文件plugin.h与plugin.cpp
//plugin.h
#ifndef PLUGIN_H
#define PLUGIN_H
#include #继承Nodelet基类
namespace custom
{
class Plugin: public nodelet::Nodelet
{
public:
Plugin();
virtual void onInit();
};
}
#endif
//plugin.h
//plugin.cpp
#include "plugin.h"
#include "main.h"
#include
#include
namespace custom
{
Plugin::Plugin(){
}
void Plugin::onInit(){
NODELET_INFO("plugin - %s", __FUNCTION__);
boost::shared_ptr<boost::thread> spinThread;
spinThread = boost::make_shared<boost::thread>(&mainFunction);
}
}
PLUGINLIB_EXPORT_CLASS(custom::Plugin,nodelet::Nodelet);
//plugin.cpp
在CMakeLists.txt文件中添加库
# CMakeLists.txt
cmake_minimum_required(VERSION 2.8.3)
project(sample_nodelet)
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
nodelet
)
catkin_package(
INCLUDE_DIRS include
LIBRARIES ${PROJECT_NAME}
CATKIN_DEPENDS nodelet roscpp rospy
# DEPENDS system_lib
)
include_directories(
include
${catkin_INCLUDE_DIRS}
)
add_library(main_function src/main.cpp)
target_link_libraries(main_funtion ${catkin_LIBRARIES})
add_library(custom_plugin
src/plugin.cpp
)
target_link_libraries(custom_plugin main_function ${catkin_LIBRARIES})
if(catkin_EXPORTED_LIBRARIES)
add_dependencies(custom_plugin ${catkin_EXPORTED_LIBRARIES})
endif()
插件描述文件是存储插件所有重要信息的机器可读文件,包含插件所在库的名字,插件的名字,插件的类型及其继承的基类名。
ROS1中功能包生成的库保存在devel/lib目录下,ROS2中功能包生存的库保存在install/目录下。
创建plugins.xml文件
<class_libraries>
<library path="lib/libcustom_plugin">
<class name="custom/Plugin" type="custom::Plugin" base_class_type="nodelet::Nodelet">
<description>This is a nodelet plugin.description>
class>
library>
class_libraries>
class name必须与nodelet load 相同,否则会报错。
为了让pluginlib能够找到ROS包中的所有可用的插件,每个包必须显式指定其导出的插件和包含这些插件的库。
ROS1是通过package.xml导出的。
在package.xml中添加以下语句,其中polygon_base是基类的包名,plugin= xml描述文件
<export>
<nodelet plugin="${prefix}/plugins.xml">
export>
为了使上述导出命令正常工作,提供插件的包必须直接依赖于包含插件基类的包,还要在package.xml文件中加油
<build_depend>nodeletbuild_depend>
<run_depend>nodeletrun_depend>
或
<depend>nodeletdepend>
<launch>
<node pkg="nodelet" type="nodelet" name="nodelet_manager" args="manager" output="screen">
<param name="num_worker_threads" value="4"/>
node>
<node pkg="nodelet" type="nodelet" name="SampleNodelet" args="load custom/Plugin nodelet_manager" output="screen">
node>
launch>
load 后跟的名称必须与class name相同,否则会报错。
ROS2 Wiki
Components可以实现以下功能:
对于传统插件,基类可以是用户定义的任何东西。唯一的约束是派生类必须可以默认构造(因此,根据C++定律,基类也必须可以默认构造)。通常对于Components,插件类通常会继承rclcpp::Node类,但这不是必须的。实际上,将类作为Components导出的要求是:
rclcpp::NodeOptions实例。rclcpp::node_interfaces::NodeBaseInterface::SharedPtr get_node_base_interface(void)
满足要求的有rclcpp::Node和rclcpp_lifecycle::LifecycleNode等。这意味着Components可以从满足这些要求的任何类中继承,但它也可以自身满足要求并完全跳过继承。
传统上,插件在注册到class_loader factory scheme时必须声明他们的基类。这允许工厂将派生对象实例化为基类的智能指针
#include
// Registering a plugin
PLUGINLIB_EXPORT_CLASS(BaseClass, DerivedClass)
//
...
// Instantiating a plugin
// We have access to all of BaseClass API
std::shared_ptr<BaseClass> base_ptr = plugin_loader.createSharedInstance("DerivedClass");
Components基类注册依赖于void(智能)指针的类型擦除,这样就可以将一个简单的包装器类作为所有组件的公共基类。这种设计意味着开发人员不需要自己为新实例查询工厂。
#include
// Registering a component
RCLCPP_COMPONENTS_REGISTER_NODE(DerivedNode)
//
...
// NodeInstanceWrapper is a wrapper around both a
// std::shared_ptr and a
// rclcpp::node_interfaces::NodeBaseInterface::SharedPtr
// Not much to do with those!
rclcpp_components::NodeInstanceWrapper component_wrapper = component_loader->create_node_instance(options);
在大多数情况下,Component继承自rclcpp::Node,因为它是满足上述需求的最简单方法。因此,我们假设Component是一个ROS 2节点,包含它所涉及的一切,可能包括parameters, listeners/publishers, services, action等等。
在CMakeLists.txt
查看已经注册过的components
ros2 component types
启动component container,重映射容器名和命名空间
ros2 run rclcpp_components component_container --ros_args --remap __node:=MyContainer --remap __ns:=/ns
component container的名字将会是
/ns/MyContainer
容器的命名空间不会影响加载的components
加载component到容器
# ros2 component load [component_container_name] [package_name] [class_name]
ros2 component load /ns/MyContainer composition composition::Listener
return the unique ID of the loaded component as well as the node name:
终端输出
Loaded component 1 into '/ns/MyContainer' container node as '/listener'`
重映射component的节点名和命名空间
ros2 component load /ns/MyContainer composition composition::Talker --node-name talker3 --node-namespace /ns1
终端输出
Loaded component 2 into '/ns/MyContainer' container node as '/ns1/talker3'`
使用component id卸载组件
ros2 component unload /ns/MyContainer 1 2
终端输出
Unloaded component 1 from '/ns/MyContainer' container
Unloaded component 2 from '/ns/MyContainer' container
与ROS1 nodelet相似,将节点编译成共享库,与ROS1在package.xml中导出插件不同,ROS2在CMakeLists.txt中使用rclcpp_components_register_nodes注册组件节点。
find_package(rclcpp_components REQUIRED)
add_library([shared_lib_name] SHARED [cppFile...])
rclcpp_components_register_nodes([shared_lib_name] "component_class_name")
import launch
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
def generate_launch_description():
"""Generate launch description with multiple components."""
container = ComposableNodeContainer(
name='my_container',
namespace='',
package='rclcpp_components',
executable='component_container',
composable_node_descriptions=[
ComposableNode(
package='composition',
plugin='composition::Talker',
name='talker'),
ComposableNode(
package='composition',
plugin='composition::Listener',
name='listener')
],
output='screen',
)
return launch.LaunchDescription([container])
#include "class_loader/class_loader.hpp"
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_components/node_factory.hpp"
int main(int argc, char * argv[])
{
if (argc < 2) {
fprintf(stderr, "Requires at least one argument to be passed with the library to load\n");
return 1;
}
rclcpp::init(argc, argv);
rclcpp::Logger logger = rclcpp::get_logger("composition");
rclcpp::executors::SingleThreadedExecutor exec;
rclcpp::NodeOptions options;
std::vector<std::unique_ptr<class_loader::ClassLoader>> loaders;
std::vector<rclcpp_components::NodeInstanceWrapper> node_wrappers;
std::vector<std::string> libraries;
for (int i = 1; i < argc; ++i) {
libraries.push_back(argv[i]);
}
for (auto library : libraries) {
RCLCPP_INFO(logger, "Load library %s", library.c_str());
std::unique_ptr<class_loader::ClassLoader> loader = std::make_unique<class_loader::ClassLoader>(library);
auto classes = loader->getAvailableClasses<rclcpp_components::NodeFactory>();
for (auto clazz : classes) {
RCLCPP_INFO(logger, "Instantiate class %s", clazz.c_str());
rclcpp_components::NodeFactory::SharedPtr node_factory = loader->createInstance<rclcpp_components::NodeFactory>(clazz);
rclcpp_components::NodeInstanceWrapper wrapper = node_factory->create_node_instance(options);
node_wrappers.push_back(wrapper);
// 获取节点基类指针
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr node = wrapper.get_node_base_interface();
// 将component节点加到执行器中
exec.add_node(node);
}
loaders.push_back(std::move(loader));
}
exec.spin();
// 卸载components节点
for (auto wrapper : node_wrappers) {
exec.remove_node(wrapper.get_node_base_interface());
}
node_wrappers.clear();
rclcpp::shutdown();
return 0;
}