nav2官方的bringup文件阅读笔记-命名空间,composition
最近开始学习nav2的源码,开始读了nav2自己提供的快速启动的bringup文件,本笔记用来记录从这个文件中学到了什么。
nav2的源码地址放在这里:GitHub链接
结构概述
nav2提供的bringup文件可以分为三个部分去理解:
- 参数声明
- 通过参数条件控制节点的启动
- 启动响应的动作
前面的参数声明阶段,该文件声明了各种参数
python
namespace = LaunchConfiguration('namespace')
use_namespace = LaunchConfiguration('use_namespace')
slam = LaunchConfiguration('slam')
map_yaml_file = LaunchConfiguration('map')
use_sim_time = LaunchConfiguration('use_sim_time')
params_file = LaunchConfiguration('params_file')
autostart = LaunchConfiguration('autostart')
use_composition = LaunchConfiguration('use_composition')
use_respawn = LaunchConfiguration('use_respawn')
log_level = LaunchConfiguration('log_level')
use_localization = LaunchConfiguration('use_localization')
其中各处参数的作用如下:
| 参数 | 默认值 | 作用 |
|---|---|---|
namespace |
空字符串 | 设置节点所在的命名空间,例如 robot1。 |
use_namespace |
false |
是否启用 namespace。为 true 时,节点和相对话题名会放到该命名空间下。 |
slam |
False |
是否选择 SLAM 模式。为真且定位也启用时,启动 slam_launch.py。 |
map |
空字符串 | 地图 YAML 文件的完整路径,传给定位 launch 使用。 |
use_localization |
True |
是否启动定位相关部分。关闭后,SLAM 和定位 launch 都不会启动。 |
use_sim_time |
false |
是否使用仿真时钟,例如 Gazebo 发布的 /clock。 |
params_file |
Nav2 默认参数文件 | 所有相关节点使用的 ROS 参数 YAML 文件路径。 |
autostart |
true |
是否自动启动 Nav2 的生命周期节点,无需再手动逐个切换到 active 状态。 |
use_composition |
True |
是否使用组合节点方式运行 Nav2。启用时会启动 nav2_container;关闭时通常由各 launch 分别启动节点进程。 |
use_respawn |
False |
进程退出后是否自动重启;本文件说明它用于非组合模式。 |
log_level |
info |
日志级别,例如 debug、info、warn、error。 |
Ros2中的namespace
类比与C++中的命名空间,ROS2也提供了这样类似的机制,通过命名空间,使得同名的话题、节点等可以在不同的命名空间下共存,避免冲突。
在应用命名空间之前,我们先熟悉一下Ros2中的命名机制:
- 全局名称:结构为
/加话题名,如/cmd_vel - 相对名称:这种话题相较于上面的区别就是它没有以
/开头,直接就是话题名,如demo/cmd_velorcmd_vel - 私有名称:结构为
~/加话题名,如~/cmd_vel
假设节点名为nd_name,其命名空间为/ns_name,待解析的话题名为tp_name,三种名称的解析结果如下:
| 名称类型 | 写法 | 解析规则 | 最终话题名 |
|---|---|---|---|
| 全局名称 | /tp_name |
以/开头,保持绝对路径,不受节点命名空间影响 |
/tp_name |
| 相对名称 | tp_name |
在节点命名空间后追加话题名 | /ns_name/tp_name |
| 多级相对名称 | demo/tp_name |
在节点命名空间后追加整个相对路径 | /ns_name/demo/tp_name |
| 私有名称 | ~/tp_name |
将~展开为"节点命名空间/节点名",再追加话题名 |
/ns_name/nd_name/tp_name |
我们可以用一个简单的节点来演示命名空间的作用,我们在该节点中发布三个不同类型的话题hello,即/hello、hello和~/hello。并使用launch文件启动时的namespace参数来设置节点的命名空间。
cpp
#include <chrono>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<rclcpp::Node>("multi_pub");
auto global_pub = node->create_publisher<std_msgs::msg::String>("/hello", 10);
auto relative_pub = node->create_publisher<std_msgs::msg::String>("hello", 10);
auto private_pub = node->create_publisher<std_msgs::msg::String>("~/hello", 10);
auto timer = node->create_wall_timer(std::chrono::seconds(1), [&]() {
std_msgs::msg::String msg;
const std::string origin = node->get_fully_qualified_name();
msg.data = origin + ": global";
global_pub->publish(msg);
msg.data = origin + ": relative";
relative_pub->publish(msg);
msg.data = origin + ": private";
private_pub->publish(msg);
});
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
python
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
DeclareLaunchArgument(
'namespace', default_value='robot1',
description='Namespace of the publisher node',
),
Node(
package='namespace_demo',
executable='multi_pub',
namespace=LaunchConfiguration('namespace'),
output='screen',
),
])
编译启动launch文件后,此时节点就会解析出对应的命名空间,并发布相应的话题。此时使用ros2 topic list命令可以看到各个话题的实际名称,验证命名空间的作用。

可以看到,结果确实如上所述。如果在启动的时候,我们将launch文件中的namespace参数设置为空值呢?
python
# in launch文件
'namespace', default_value='robot1' -> 'namespace', default_value=''
此时的运行结果如下

可以看到当不指定命名空间时,命名空间占据的部分会被省略,/robot1/hello 变成了 /hello,/robot1/multi_pub/hello 变成了 /multi_pub/hello。
此时虽然只有一个/hello话题,但实际上他是两个话题重名,分别是全局话题/hello和相对话题hello。我们可以使用ros2 topic info /hello命令来查看该话题的详细信息,验证其实际来源。

可以看到他有两个发布来源,也就是确实是重名话题。
Bringup文件中的应用
在文件中,有下面的语句
python
remappings = [('/tf', 'tf'), ('/tf_static', 'tf_static')]
这行代码的作用是将全局话题/tf和/tf_static重映射为相对话题tf和tf_static。由于很多节点默认使用全局话题,通过这种方式可以将它们改为相对话题,而相对话题又会受到节点命名空间的影响,这样就使得原先可能冲突的TF发布变得不再冲突。
以及还有下面的语句
python
params_file = ReplaceString(
source_file=params_file,
replacements={'<robot_namespace>': ('/', namespace)},
condition=IfCondition(use_namespace),
)
configured_params = ParameterFile(
RewrittenYaml(
source_file=params_file,
root_key=namespace,
param_rewrites={},
convert_types=True,
),
allow_substs=True,
)
第一个是ReplaceString,它的作用是将参数文件中的<robot_namespace>替换为实际的命名空间值。
第二个是RewrittenYaml,它的作用是将修改后的参数文件重新写入YAML格式,并指定根键为实际的命名空间值。
第二个操作结束后,如果有命名空间的话,那么参数文件中的根键就会被修改为实际的命名空间值,从而使得所有的参数都位于该命名空间下。比如
yaml
controller_manager:
ros__parameters:
update_rate: 100
use_sim_time: true
变为
yaml
robot1:
controller_manager:
ros__parameters:
update_rate: 100
use_sim_time: true
这样保证遵守了ROS2参数文件的命名空间规范,这样参数传递时会去找到正确的/robot1/controller_manager/ros__parameters路径,而不是之前的/controller_manager/ros__parameters。
下面是ROS2文档给出的使用YAML文件初始化参数的格式:

分组控制与PushNameSpace
在bringup文件中,有下面的语句:
python
bringup_cmd_group = GroupAction(
[
PushROSNamespace(condition=IfCondition(use_namespace), namespace=namespace),
.......# 省略后面的Node初始化
]
)
这一段使用了GroupAction和PushROSNamespace来实现对一组节点的命名空间控制。
在ROS2中,
PushROSNamespace是一个用于设置命名空间的动作,正如其名,它会将指定的命名空间"Push"到当前的命名空间栈中(当然这也意味着你可以Push多个命名空间,形成嵌套的命名空间),在其之后的作用域中的所有节点和话题都会自动继承这个命名空间。
而GroupAction是一个用于将多个动作组合在一起的动作,它可以将一组相关的动作放在一起,使得它们可以作为一个整体来执行,同时在GroupAction内使用PushROSNamespace,使得PushROSNamespace的作用范围仅限于该组动作内,从而实现对一组节点的命名空间控制。
当然,在ROS2中并没有PopROSNamespace的实现,因此想要实现分组控制的命名空间,只能使用GroupAction的策略,这也与源码的描述相符合。

上面的源码注释意思如下 :
一个用于将 ROS 命名空间压入当前上下文的 Action。
如果在具有作用域的
GroupAction中使用,它会在组作用域结束时自动弹出。除此之外,没有其他方式可以将它弹出。
第二部分,参数控制
在bringup文件中,参数控制主要是通过条件语句来决定是否启动某些节点或launch文件。在ROS2的launch系统中,条件语句通常使用IfCondition和UnlessCondition来实现。
python
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(launch_dir, 'slam_launch.py')
),
condition=IfCondition(PythonExpression([slam, ' and ', use_localization])),
launch_arguments={
'namespace': namespace,
'use_sim_time': use_sim_time,
'autostart': autostart,
'use_respawn': use_respawn,
'params_file': params_file,
}.items(),
)
上面是bringup文件中启动slam_launch.py的部分,即负责启动slam_toolbox和map_server节点的launch文件。在上面的代码中,IncludeLaunchDescription用来在该launch文件中包含另一个launch文件。condition参数指定了一个条件表达式,只有当该条件为真时,才会执行包含的launch文件。而判断的条件是slam和use_localization两个参数的逻辑与运算,即只有当这两个参数都为真时,才会启动slam_launch.py。
python
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(launch_dir, 'localization_launch.py')
),
condition=IfCondition(PythonExpression(['not ', slam, ' and ', use_localization])),
launch_arguments={
'namespace': namespace,
'map': map_yaml_file,
'use_sim_time': use_sim_time,
'autostart': autostart,
'params_file': params_file,
'use_composition': use_composition,
'use_respawn': use_respawn,
'container_name': 'nav2_container',
}.items(),
),
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(launch_dir, 'navigation_launch.py')
),
launch_arguments={
'namespace': namespace,
'use_sim_time': use_sim_time,
'autostart': autostart,
'params_file': params_file,
'use_composition': use_composition,
'use_respawn': use_respawn,
'container_name': 'nav2_container',
}.items(),
),
上面是调用后面两个launch文件的部分,分别是localization_launch.py和navigation_launch.py。前者用来启动AMCL定位相关的节点,后者用来启动导航相关的节点。可以看到,当且仅当PythonExpression(['not ', slam, ' and ', use_localization])为真,也就是当slam为假且use_localization为真时,即此时没有slam建图的需求,但需要定位功能时,才会启动localization_launch.py。而navigation_launch.py则没有条件限制,会在任何情况下启动。
ROS2中的container
python
Node(
condition=IfCondition(use_composition),
name='nav2_container',
package='rclcpp_components',
executable='component_container_isolated',
parameters=[configured_params, {'autostart': autostart}],
arguments=['--ros-args', '--log-level', log_level],
remappings=remappings,
output='screen',
),
在bringup文件中,有着上面的代码,这个段代码初始化了一个Node action,用来启动一个名为nav2_container的节点。
在ROS2中,允许我们将多个节点组合在一个进程中运行,这种方式叫做
组合(Composition),组合中的节点被称为组件(Component)。而容纳这些组件的进程就叫做容器(Container)。在nav2中,nav2_container就是一个容器节点,它可以容纳多个导航相关的组件节点。
同时看后面的代码
python
IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(launch_dir, 'localization_launch.py')
),
condition=IfCondition(PythonExpression(['not ', slam, ' and ', use_localization])),
launch_arguments={
'namespace': namespace,
'map': map_yaml_file,
'use_sim_time': use_sim_time,
'autostart': autostart,
'params_file': params_file,
'use_composition': use_composition,
'use_respawn': use_respawn,
'container_name': 'nav2_container',
}.items(),
),
在启动localization_launch.py时,有两个与组合有关的参数:
use_composition:这个参数决定了是否使用组合方式运行导航相关container_name:这个参数指定了容器节点的名称,即nav2_container。当使用组合方式时,所有的组件节点都会被加载到这个容器中运行。
当多个节点之间的通信比较频繁,同时数据量较大时,如相机驱动节点获取图像,并将图像数据传递给图像处理节点进行处理,这种情况下,如果每个节点都运行在独立的进程中,数据传输会涉及到进程间通信,这会带来额外的开销,可能导致延迟增加。而如果将这些节点组合在同一个进程中运行,它们之间的数据传输可以通过内存共享的方式进行,从而减少了进程间通信的开销,提高了通信效率。