这篇博客是 B 站《古月·ROS2入门21讲》的第十五个视频的图文记录,主要介绍了 launch 文件如何实现多个节点加载、命名空间隔离、参数启动加载、多个launch嵌套 的功能。原始视频链接如下:
- B 站视频链接:15. Launch:多节点启动与配置脚本
【Note】:ROS2 的 launch 文件编写规则和 ROS1 之间存在巨大差异,ROS2 更像是在写 python 脚本,而 ROS1 则是在写 xml。尽管也可以用 launch 或 yaml 编写,但官方建议用 python 格式撰写。
1. 多个节点启动
使用下面的命令运行 learning_launch 功能包下的 simple.launch.py 文件:
bash
$ ros2 launch learning_launch simple.launch.py
运行后可以看见同时启动了两个节点 topic_helloworld_pub-1 和 topic_helloworld_sub-2,这两个节点有各自的 PID,说明是两个独立的进程:

2. launch 文件内容
在 learning_launch 功能包下有 launch/simple.launch.py 文件,具体位置为 src/ros2_21_tutorials/learning_launch/launch:
【Note】:虽然没有强制要求 launch 文件必须放在 launch 文件夹中,但这已经成为了一个约定俗成的习惯。
从该文件的内容中可以发现其本身是 python 写法,这和 ROS1 中的 launch 文件编写规则有较大差异:
python
from launch import LaunchDescription # launch文件的描述类
from launch_ros.actions import Node # 节点启动的描述类
def generate_launch_description(): # 自动生成launch文件的函数
return LaunchDescription([ # 返回launch文件的描述信息
Node( # 配置一个节点的启动
package='learning_topic', # 节点所在的功能包
executable='topic_helloworld_pub', # 节点的可执行文件
),
Node( # 配置一个节点的启动
package='learning_topic', # 节点所在的功能包
executable='topic_helloworld_sub', # 节点的可执行文件名
),
])

3. 加载 rviz 配置
3.1 launch 加载配置
launch 文件可以用来在打开 rviz 时加载配置好的信息,使用下面的命令运行一个 launch 文件:
bash
$ ros2 launch learning_launch rviz.launch.py
如果你此时再开一个终端运行一个空的 rviz 就可以看见两者之间的差异:
bash
$ ros2 run rviz2 rviz2
下图中左边的是通过 rviz.launch.py 启动的 rviz 窗口,右边的是 rviz2 启动的窗口,可以看见在视角上有明显不同。当你进入开发后期有时候需要展现多个内容的时候这种自动加载配置的方式就非常方便。

3.2 命令行加载配置
当然,也不是说命令行就不能在启动 rviz 的时候加载配置文件,你仍然可以用 -d 参数的形式指明配置文件路径,如:
bash
$ ros2 run rviz2 rviz2 -d \
src/ros2_21_tutorials/learning_launch/rviz/turtle_rviz.rviz
这种方式启动的依旧是上面 rviz.launch.py 相同的节点:

3.3 launch 文件内容
打开 learning_launch 功能包中 launch/rviz.launch.py 文件,具体位置在 src/ros2_21_tutorials/learning_launch/launch/rviz.launch.py 处:
python
import os
from ament_index_python.packages import get_package_share_directory # 查询功能包路径的方法
from launch import LaunchDescription # launch文件的描述类
from launch_ros.actions import Node # 节点启动的描述类
def generate_launch_description(): # 自动生成launch文件的函数
rviz_config = os.path.join( # 找到配置文件的完整路径
get_package_share_directory('learning_launch'),
'rviz',
'turtle_rviz.rviz'
)
return LaunchDescription([ # 返回launch文件的描述信息
Node( # 配置一个节点的启动
package='rviz2', # 节点所在的功能包
executable='rviz2', # 节点的可执行文件名
name='rviz2', # 对节点重新命名
arguments=['-d', rviz_config] # 加载命令行参数
)
])
从文件内容上可以看出其本质依旧是使用 -d 参数实现加载,和命令行加载方式完全一致。

另一方面,由于 ROS中不允许有重名节点,当你有一个节点功能需要服用的时候就可以以 launch 文件的形式加载节点,加载时会 使用 name 值强制覆盖节点脚本中的节点名。这种情况在复合机器人中十分常见,例如一台机器人上可能有多个相机,如果给每个相机都写一个脚本既不通用也不优雅,但用 launch 文件重命名就可以很便捷地修改节点名。

4. namespace 命名空间
4.1 话题前的 namespace
使用下面命令启动 learning_launch 功能包中的 launch/remapping.launch.py 文件,具体位置为 src/ros2_21_tutorials/learning_launch/launch/remapping.launch.py:
bash
$ ros2 launch learning_launch remapping.launch.py
启动后可以发现有两个小海龟仿真器:

此时可以看到可用的话题有以下几个,在话题前有两个额外的字段 /turtlesim1 和 /turtlesim2,这表示话题在不同的命名空间:
bash
$ ros2 topic list

4.2 launch 文件中 namespace 规则
打开 learning_launch 功能包中的 launch/remapping.launch.py 文件,具体位置为 src/ros2_21_tutorials/learning_launch/launch/remapping.launch.py 内容如下:
python
from launch import LaunchDescription # launch文件的描述类
from launch_ros.actions import Node # 节点启动的描述类
def generate_launch_description(): # 自动生成launch文件的函数
return LaunchDescription([ # 返回launch文件的描述信息
Node( # 配置一个节点的启动
package='turtlesim', # 节点所在的功能包
namespace='turtlesim1', # 节点所在的命名空间
executable='turtlesim_node', # 节点的可执行文件名
name='sim' # 对节点重新命名
),
Node( # 配置一个节点的启动
package='turtlesim', # 节点所在的功能包
namespace='turtlesim2', # 节点所在的命名空间
executable='turtlesim_node', # 节点的可执行文件名
name='sim' # 对节点重新命名
),
Node( # 配置一个节点的启动
package='turtlesim', # 节点所在的功能包
executable='mimic', # 节点的可执行文件名
name='mimic', # 对节点重新命名
remappings=[ # 资源重映射列表
('/input/pose', '/turtlesim1/turtle1/pose'), # 将/input/pose话题名修改为/turtlesim1/turtle1/pose
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'), # 将/output/cmd_vel话题名修改为/turtlesim2/turtle1/cmd_vel
]
)
])

命名空间的概念实际上就是在强化 隔离 的思想。想象一下,在一个空间中有两台机器人,这两台机器人的硬件完全一样,假设每台都有 camera_head、camera_left、camera_right 三个相机,如果仅重命名节点的形式规避节点重名,那么你会得到以下几个话题:
- Robot1:
camera_head_1、camera_left_1、camera_right_1; - Robot2:
camera_head_2、camera_left_2、camera_right_2;
难道我们每个话题和服务都要以后缀数字的形式进行编号么,这显然过于繁琐,且对算法而言要区分的就太多了。如果有需要修改一个机器人的信息则要全局范围修改。命名空间则可以帮助我们简化这一流程,通过命名空间可以将话题转化为:
- Robot1:
Robot1/camera_head、Robot1/camera_left、Robot1/camera_right; - Robot2:
Robot2/camera_head、Robot2/camera_left、Robot2/camera_right;
当需要对机器人进行重命名或者将机器人移动到其他组中,则可以直接修改命名空间即可。
4.3 同时控制两只海龟
在 remapping.launch.py 文件中还有一个名为 minic 的节点,该节点实现的功能是 将一个海龟的位置转换成另一个海龟的控制指令:

此时新开一个终端向话题 `` 发送控制指令会看到两只海龟同步运动:
bash
$ ros2 topic pub /turtlesim1/turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.5, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.3}}"

体验完后就可以关闭这两个仿真器。
5. param 参数加载
5.1 直接在 launch 文件中加载参数
同样可以通过 launch 文件在启动的时候将参数加载进去,打开 learning_launch 功能包中 launch/parameters.launch.py 文件,具体位置在 src/ros2_21_tutorials/learning_launch/launch/parameters.launch.py 处:
python
from launch import LaunchDescription # launch文件的描述类
from launch.actions import DeclareLaunchArgument # 声明launch文件内使用的Argument类
from launch.substitutions import LaunchConfiguration, TextSubstitution
from launch_ros.actions import Node # 节点启动的描述类
def generate_launch_description(): # 自动生成launch文件的函数
background_r_launch_arg = DeclareLaunchArgument(
'background_r', default_value=TextSubstitution(text='0') # 创建一个Launch文件内参数(arg)background_r
)
background_g_launch_arg = DeclareLaunchArgument(
'background_g', default_value=TextSubstitution(text='84') # 创建一个Launch文件内参数(arg)background_g
)
background_b_launch_arg = DeclareLaunchArgument(
'background_b', default_value=TextSubstitution(text='122') # 创建一个Launch文件内参数(arg)background_b
)
return LaunchDescription([ # 返回launch文件的描述信息
background_r_launch_arg, # 调用以上创建的参数(arg)
background_g_launch_arg,
background_b_launch_arg,
Node( # 配置一个节点的启动
package='turtlesim',
executable='turtlesim_node', # 节点所在的功能包
name='sim', # 对节点重新命名
parameters=[{ # ROS参数列表
'background_r': LaunchConfiguration('background_r'), # 创建参数background_r
'background_g': LaunchConfiguration('background_g'), # 创建参数background_g
'background_b': LaunchConfiguration('background_b'), # 创建参数background_b
}]
),
])

使用下面命令运行该 launch 文件:
bash
$ ros2 launch learning_launch parameters.launch.py

5.2 通过参数文件形式加载
在之前讲 param 的博客中提到了参数可以写成 yaml 文件的形式然后用 dump 和 load 命令进行加载,launch 也可以实现相同的功能。
参数配置文件 yaml 在 src/ros2_21_tutorials/learning_launch/config/turtlesim.yaml 处:
yaml
/turtlesim2/sim:
ros__parameters:
background_b: 0
background_g: 0
background_r: 0
启动的 launch 文件在 src/ros2_21_tutorials/learning_launch/launch/parameters_yaml.launch.py 处:
python
import os
from ament_index_python.packages import get_package_share_directory # 查询功能包路径的方法
from launch import LaunchDescription # launch文件的描述类
from launch_ros.actions import Node # 节点启动的描述类
def generate_launch_description(): # 自动生成launch文件的函数
config = os.path.join( # 找到参数文件的完整路径
get_package_share_directory('learning_launch'),
'config',
'turtlesim.yaml'
)
return LaunchDescription([ # 返回launch文件的描述信息
Node( # 配置一个节点的启动
package='turtlesim', # 节点所在的功能包
executable='turtlesim_node', # 节点的可执行文件名
namespace='turtlesim2', # 节点所在的命名空间
name='sim', # 对节点重新命名
parameters=[config] # 加载参数文件
)
])

使用下面的命令启动这个 launch 文件:
bash
$ ros2 launch learning_launch parameters_yaml.launch.py

6. launch 文件嵌套
在复杂机器人系统中经常会出现 launch 文件嵌套的情况,例如一个 launch 文件只包含各种算法、一个 launch 文件只包含各种硬件驱动、一个 launch 文件只包含和上层的交互功能。
打开 learning_launch 功能包中的 launch/namespaces.launch.py 文件,具体位置在 src/ros2_21_tutorials/learning_launch/launch/namespaces.launch.py:
python
import os
from ament_index_python.packages import get_package_share_directory # 查询功能包路径的方法
from launch import LaunchDescription # launch文件的描述类
from launch.actions import IncludeLaunchDescription # 节点启动的描述类
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.actions import GroupAction # launch文件中的执行动作
from launch_ros.actions import PushRosNamespace # ROS命名空间配置
def generate_launch_description(): # 自动生成launch文件的函数
parameter_yaml = IncludeLaunchDescription( # 包含指定路径下的另外一个launch文件
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('learning_launch'), 'launch'),
'/parameters_nonamespace.launch.py'])
)
parameter_yaml_with_namespace = GroupAction( # 对指定launch文件中启动的功能加上命名空间
actions=[
PushRosNamespace('turtlesim2'),
parameter_yaml]
)
return LaunchDescription([ # 返回launch文件的描述信息
parameter_yaml_with_namespace
])

在上面的代码中还需要注意的是 PushRosNamespace 函数,该函数的功能是给新包含的 launch 文件添加一个命名空间,这样做的意义在于 当你不知道嵌套的 launch 文件中是否存在冲突的名字时,多套一层命名空间准没错:

该 launch 文件嵌套了一个 parameters_nonamespace.launch.py 文件:
python
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
config = os.path.join(
get_package_share_directory('learning_launch'),
'config',
'turtlesim.yaml'
)
return LaunchDescription([
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim',
parameters=[config]
)
])

7. 配置 launch 编译条件
在编写好 launch 文件后需要告诉编译器将这些文件安装到 install 目录下,和之前博客中添加节点一样,同样需要在 setup.py 文件中添加这些 launch 文件,具体位置在 src/ros2_21_tutorials/learning_launch/setup.py
处:
python
from setuptools import setup
import os
from glob import glob
package_name = 'learning_launch'
setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
(os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*.launch.py'))),
(os.path.join('share', package_name, 'config'), glob(os.path.join('config', '*.*'))),
(os.path.join('share', package_name, 'rviz'), glob(os.path.join('rviz', '*.*'))),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='hcx',
maintainer_email='huchunxu@guyuehome.com',
description='TODO: Package description',
license='TODO: License declaration',
tests_require=['pytest'],
entry_points={
'console_scripts': [
],
},
)
