nav2官方的bringup文件阅读笔记-命名空间,composition

nav2官方的bringup文件阅读笔记-命名空间,composition

最近开始学习nav2的源码,开始读了nav2自己提供的快速启动的bringup文件,本笔记用来记录从这个文件中学到了什么。

nav2的源码地址放在这里:GitHub链接

结构概述

nav2提供的bringup文件可以分为三个部分去理解:

  1. 参数声明
  2. 通过参数条件控制节点的启动
  3. 启动响应的动作

前面的参数声明阶段,该文件声明了各种参数

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_vel or cmd_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时,有两个与组合有关的参数:

  1. use_composition:这个参数决定了是否使用组合方式运行导航相关
  2. container_name:这个参数指定了容器节点的名称,即nav2_container。当使用组合方式时,所有的组件节点都会被加载到这个容器中运行。

当多个节点之间的通信比较频繁,同时数据量较大时,如相机驱动节点获取图像,并将图像数据传递给图像处理节点进行处理,这种情况下,如果每个节点都运行在独立的进程中,数据传输会涉及到进程间通信,这会带来额外的开销,可能导致延迟增加。而如果将这些节点组合在同一个进程中运行,它们之间的数据传输可以通过内存共享的方式进行,从而减少了进程间通信的开销,提高了通信效率。