容器 & 进程
概念
介绍容器之前,必须回顾ROS的节点间是什么关系
节点是ROS2对功能的划分,进程是操作系统运行程序的单位。一个进程可以运行一个节点,也可以同时运行多个节点。
一般来说我们希望一个节点实现一个功能,这样从功能的角度上实现了代码解耦,互相之间通常不会调用代码,方便独立开发、测试和替换
容器的一个优势在于,它让多个main()对应的节点们,方便的组合(请忽略launch,它不一样~)
如果不用容器,想调整同一进程中的节点组合,就需要修改 main() 并重新编译,很不方便。
操作系统运行一个可执行程序时,会创建一个进程并从 main() 开始执行,而进程间通信通常需要更多的数据复制、序列化和调度开销。对于图像、点云等大数据,这些开销可能比较明显。论坛参考
容器就是ROS2提供的通用解决方案。它作为整个进程唯一的主程序,负责加载和运行被编译成组件的节点
容器进程
├── 相机节点
├── 图像识别节点
└── 目标跟踪节点那这么好用的东西为啥放在后面讲?
单进程风险:一个组件的故障(or 崩溃)可能让整个进程重启,有时得不偿失(比如串口比特错位,而你尝试古法重启解决,你想等整个视觉模块加载么)
动手写一个小容器
下面还是用最熟悉的发布者和订阅者作为例子,不过这次不为它们编写main(),而是把它们编译成组件,再交给容器运行。
创建功能包
进入工作空间的src目录,创建一个名为component_demo的功能包
cd ~/ros2_ws/srcros2 pkg create component_demo --build-type ament_cmake --dependencies rclcpp rclcpp_components std_msgs
mkdir component_demo/launch文件结构如下:
component_demo
── CMakeLists.txt
├── launch
│ └── component_demo.launch.py
├── package.xml
└── src
├── publisher_component.cpp
└── subscriber_component.cpp编写发布组件
#include <chrono>
#include <cstddef>
#include <functional>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_components/register_node_macro.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
namespace component_demo
{
class Publisher : public rclcpp::Node
{
public:
// 组件的构造函数必须接收NodeOptions,容器会通过它传入节点配置
explicit Publisher(const rclcpp::NodeOptions & options)
: Node("component_publisher", options), count_(1)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("component_chatter", 10);
timer_ = this->create_wall_timer(1s, std::bind(&Publisher::timer_callback, this));
}
private:
void timer_callback()
{
std_msgs::msg::String message;
message.data = "Hello from component: " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
size_t count_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
};
} // namespace component_demo
// 组件没有main(),用这个宏把节点类注册为可加载的组件
RCLCPP_COMPONENTS_REGISTER_NODE(component_demo::Publisher)编写订阅组件
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_components/register_node_macro.hpp"
#include "std_msgs/msg/string.hpp"
namespace component_demo
{
class Subscriber : public rclcpp::Node
{
public:
explicit Subscriber(const rclcpp::NodeOptions & options)
: Node("component_subscriber", options)
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"component_chatter", 10,
std::bind(&Subscriber::topic_callback, this, std::placeholders::_1));
}
private:
void topic_callback(const std_msgs::msg::String::SharedPtr message) const
{
RCLCPP_INFO(this->get_logger(), "Received: '%s'", message->data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
} // namespace component_demo
RCLCPP_COMPONENTS_REGISTER_NODE(component_demo::Subscriber)和普通节点相比,这两个组件都没有main()。容器加载组件时会创建节点,所以构造函数还需要接收一个rclcpp::NodeOptions参数。
注册组件
修改功能包中的CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(component_demo)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(std_msgs REQUIRED)
# 组件需要编译成共享库,而不是带main()的可执行程序
add_library(publisher_component SHARED src/publisher_component.cpp)
ament_target_dependencies(publisher_component rclcpp rclcpp_components std_msgs)
rclcpp_components_register_nodes(publisher_component "component_demo::Publisher")
add_library(subscriber_component SHARED src/subscriber_component.cpp)
ament_target_dependencies(subscriber_component rclcpp rclcpp_components std_msgs)
rclcpp_components_register_nodes(subscriber_component "component_demo::Subscriber")
install(TARGETS
publisher_component
subscriber_component
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY launch
DESTINATION share/${PROJECT_NAME}
)
ament_package()RCLCPP_COMPONENTS_REGISTER_NODE在C++代码中注册节点类,rclcpp_components_register_nodes在CMake中登记组件及其所在的共享库,两处都不能漏掉。
为了后面能用launch启动,还要在package.xml中加入运行依赖
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>component_demo</name>
<version>0.0.0</version>
<description>ROS2 component demo</description>
<maintainer email="[email protected]">example</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>rclcpp_components</depend>
<depend>std_msgs</depend>
<exec_depend>launch_ros</exec_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>编译功能包
回到工作空间目录编译,并刷新当前终端的环境
colcon build --packages-select component_demo
source install/setup.bash可以用下面的命令检查组件是否注册成功
ros2 component types输出中应该能找到
component_demo
component_demo::Publisher
component_demo::Subscriber启动容器并加载组件
先启动一个容器,并把它命名为component_container
ros2 run rclcpp_components component_container --ros-args -r __node:=component_container再打开一个终端,加载发布和订阅组件
source install/setup.bash
ros2 component load /component_container component_demo component_demo::Publisher
ros2 component load /component_container component_demo component_demo::Subscriber提示
这里启动的两个节点都没有自己的main(),真正运行的可执行程序只有component_container。
此时容器所在的终端应该会不断输出
[INFO] [component_publisher]: Publishing: 'Hello from component: 1'
[INFO] [component_subscriber]: Received: 'Hello from component: 1'
[INFO] [component_publisher]: Publishing: 'Hello from component: 2'
[INFO] [component_subscriber]: Received: 'Hello from component: 2'输入下面的命令,还可以查看容器中已经加载的组件
ros2 component list使用launch加载
实际使用时,一般不会每次手动输入两条ros2 component load,可以通过launch文件同时创建容器并加载组件。
新建component_demo/launch/component_demo.launch.py
from launch import LaunchDescription
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
def generate_launch_description():
container = ComposableNodeContainer(
name="component_container",
namespace="",
package="rclcpp_components",
executable="component_container",
composable_node_descriptions=[
ComposableNode(
package="component_demo",
plugin="component_demo::Publisher",
name="component_publisher",
extra_arguments=[{"use_intra_process_comms": True}],
),
ComposableNode(
package="component_demo",
plugin="component_demo::Subscriber",
name="component_subscriber",
extra_arguments=[{"use_intra_process_comms": True}],
),
],
output="screen",
)
return LaunchDescription([container])文件结构如下:
component_demo
├── CMakeLists.txt
├── launch
│ └── component_demo.launch.py
├── package.xml
└── src
├── publisher_component.cpp
└── subscriber_component.cpp重新编译并刷新环境后运行
colcon build --packages-select component_demo
source install/setup.bash
ros2 launch component_demo component_demo.launch.py这里的launch只负责按照配置启动容器和加载组件,发布者和订阅者仍然运行在同一个容器进程中。use_intra_process_comms则会为它们启用进程内通信。