我们已经学会了如何用
ros2 run
去运行一个ROS2的可执行程序。可是当你需要同时运行多个程序时,就不得不开多个终端来启动程序。比如一个SLAM小车,需要开一个终端启动激光雷达驱动程序发布laserscan,另外需要开一个终端启动底盘发布里程计和imu,还需一个终端启动slam程序订阅激光和传感器数据并发布地图和定位。整个操作就会变得十分繁琐。所以ROS提供了启动系统(Launch System)。ROS中的启动系统负责帮助用户描述系统的配置,然后按照描述执行。系统的配置包括运行什么程序,在哪里运行,传递什么参数,以及特定于ROS的约定,这些约定通过给组件提供不同的配置,使得在整个系统中重用组件变得容易。此外,因为启动系统是执行用户进程的进程(或一组进程),所以它负责监视它启动的流程的状态,以及对这些进程的状态变化进行报告或反应。
我们需要编写启动文件(launch file)。ROS2的启动文件可以用Python、XML和YAML三种方法编写,如果有用过ROS1的话可能最熟悉的就是XML方式。但是XML或YAML无法实现Python的灵活性。因为使用Python有以下两个优点:
本教程的launch file是用Python编写的,它可以启动和停止不同的节点,以及触发和处理各种事件。
本教程适用于最新的ROS2 Galactic版本,而且仅介绍C++ package。
我将由简到烦逐步教大家写出一个可以用于运行复杂的ROS2系统的launch file。
可以在任何你喜欢的目录下创建启动文件,但正规的方式为了能让ros2管理到launch file,我们把launch file创建在包目录里面。
先创建一个包,比如在工作空间的src目录下创建一个包叫my_launch_test
ros2 pkg create --build-type ament_cmake my_launch_test
然后在包目录里创建launch目录来存储launch file。由于是python launch,所以我们需要创建一个.py
结尾的文件,主流方式命名为xxx.launch.py或者xxx_launch.py。这里我们把它叫做test.launch.py
mkdir launch
cd launch
touch test.launch.py
可以用任何你喜欢的文本编辑器来打开编辑launch file。
我们以运行海龟模拟器turtlesim为例,来编写启动文件。
在test.launch.py中加入以下代码:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim1'
),
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim2'
)
])
来看看代码,这个launch file的作用是启动两个turtlesim窗口
首先需要import两个模块。
from launch import LaunchDescription
from launch_ros.actions import Node
启动描述,launch file最基本的就是将内容编写在启动描述中。
def generate_launch_description():
return LaunchDescription([
])
launch file最基本的功能就是启动node,LaunchDescription
中包含了两个Node,
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim1'
),
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim2'
)
Node中一般需要3个属性,package、executable和name,package为包的名称,executable为包内的可执行文件名,name为节点运行时的节点名。可能还会用到:
namespace: 命名空间。使节点名称前增加命名空间前缀,命名空间不同使系统允许两个相同节点名和主题名不冲突。如果没有唯一的命名空间,当topic消息相同时就无法区分是哪个节点的。
respawn: 复位。设为‘true’时,节点停止时自动重启。默认为‘false’
output: 输出。设为‘screen’时,将节点的输出打印到中终端屏幕。
arguments: 节点需要输入的参数。
remappings: 重映射,将默认节点属性(如节点名称、主题名称、服务名称等),重映射为其它名称。
可以直接在launch目录下启动launch file
ros2 launch test.launch.py
但我们一般对包进行编译后调用。返回到my_launch_test目录,
在package.xml中添加依赖:
<exec_depend>ros2launchexec_depend>
在CMakeListe.txt中添加:
# Install launch files.
install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)
然后返回工作空间根目录进行colcon build
。之后只要source 设置文件. install/setup.bash
,就能使用ros2 launch
的方式来启动了。
出现两个turtlesim窗口。
当启动了上一节的示例时,我们查看一下当前的话题列表
会看到,虽然运行了两个海龟模拟器,但看到的话题列表跟只有一个模拟器一样。因为两个模拟器都发布和订阅同样的话题,混在一起了。
这时候运行海龟操控键盘turtle_teleop_key
,试着操作一下箭头,发现两只海龟同时都会动,因为两个节点都订阅了/turtle1/cmdvel
。
如果要区分两个海龟模拟器的话,就得加入命名空间(namespace)属性,将launch file代码改为:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
namespace='turtlesim1',
executable='turtlesim_node',
name='sim1'
),
Node(
package='turtlesim',
namespace='turtlesim2',
executable='turtlesim_node',
name='sim2'
)
])
重新启动launch file。再查看话题列表:
可以看到两个模拟器节点的话题就区分出来了。
增加了命名空间后我们再次运行turtle_teleop_key
,操作一下方向按键,发现海龟动不了了。来看看话题列表:
马上就发现,虽然话题类型都是geometry_msgs/msg/Twist
,但turtle_teleop_key
发布的话题是/turtle1/cmd_vel
,而两个turtlesim
节点订阅的话题是有带命名空间前缀的。所以并不能兼容。
这时候我们就可以使用重映射,给话题重映射个名字,比如把/turtlesim1/turtle1/cmd_vel
重映射为/turtle1/cmd_vel
,来改一下launch file:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
namespace='turtlesim1',
executable='turtlesim_node',
name='sim1',
remappings=[
('/turtlesim1/turtle1/cmd_vel', '/turtle1/cmd_vel'),
]
),
Node(
package='turtlesim',
namespace='turtlesim2',
executable='turtlesim_node',
name='sim2'
),
Node(
package='turtlesim',
executable='mimic',
name='mimic',
remappings=[
('/input/pose', '/turtlesim1/turtle1/pose'),
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
]
)
])
turtlesim1节点的/turtlesim1/turtle1/cmd_vel
话题,映射为/turtle1/cmd_vel
。
我们还增加了mimic
节点,把mimic
的 /input/pose
话题映射为 /turtlesim1/turtle1/pose
,把 /output/cmd_vel
话题映射为 /turtlesim2/turtle1/cmd_vel
。mimic
节点的作用就是将会订阅turtlesim1节点发布的位姿(pose)话题,并重新发布为turtlesim2节点的速度控制。也就是说turtlesim2
将会模仿 turtlesim1
的动作。
turtle_teleop_key
控制turtlesim1
,而turtlesim2
模仿turtlesim1
,所以操作方向键时,两只海龟一起动了起来。
turtlesim节点提供背景颜色参数可以让我们设置,我们可以在终端里输入命令去设置修改某一个参数。launch file也提供了可以在文件里直接调用命令的功能。见如下代码:
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import ExecuteProcess
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
namespace='turtlesim1',
executable='turtlesim_node',
name='sim1',
remappings=[
('/turtlesim1/turtle1/cmd_vel', '/turtle1/cmd_vel'),
]
),
Node(
package='turtlesim',
namespace='turtlesim2',
executable='turtlesim_node',
name='sim2'
),
Node(
package='turtlesim',
executable='mimic',
name='mimic',
remappings=[
('/input/pose', '/turtlesim1/turtle1/pose'),
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
]
),
ExecuteProcess(
cmd=[[
'ros2 param set ',
'/turtlesim1/sim1 ',
'background_b ',
'0'
]],
shell=True
)
])
首先需要import一个模块:
from launch.actions import ExecuteProcess
然后就可以使用了:
ExecuteProcess(
cmd=[[
'ros2 param set ',
'/turtlesim1/sim1 ',
'background_b ',
'0'
]],
shell=True
)
我们调用了ros2 param set /turtlesim1/sim1 background_b 0
这个命令,将/turtlesim1/sim1
的background_b
设置为0。来看下效果:
可以看到turtlesim1
的背景色变了。
前面几节我们在launch file里的参数都是直接写死的。但为了使启动更灵活,比如我们想在启动时改变某个参数,而无需每次去修改launch file,就可以使用参数配置的功能。
一般需要用到两个模块:launch.substitutions.LaunchConfiguration
和launch.actions.DeclareLaunchArgument
。
LaunchConfiguration
用来增加一个启动文件的传递参数,而DeclareLaunchArgument
用于定义可以从上述启动文件或控制台传递的启动参数。
我们将上一节的修改背景颜色改为使用参数配置的方式。见以下代码:
from launch import LaunchDescription
from launch_ros.actions import Node
from launch.substitutions import LaunchConfiguration
from launch.actions import ExecuteProcess, DeclareLaunchArgument
def generate_launch_description():
background_blue_arg = LaunchConfiguration('background_blue_arg')
background_blue_launch_arg = DeclareLaunchArgument(
'background_blue_arg',
default_value='255',
description='Configure the blue value of turtlesim1 background color.'
)
return LaunchDescription([
background_blue_launch_arg,
Node(
package='turtlesim',
namespace='turtlesim1',
executable='turtlesim_node',
name='sim1',
remappings=[
('/turtlesim1/turtle1/cmd_vel', '/turtle1/cmd_vel'),
]
),
Node(
package='turtlesim',
namespace='turtlesim2',
executable='turtlesim_node',
name='sim2'
),
Node(
package='turtlesim',
executable='mimic',
name='mimic',
remappings=[
('/input/pose', '/turtlesim1/turtle1/pose'),
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
]
),
ExecuteProcess(
cmd=[[
'ros2 param set ',
'/turtlesim1/sim1 ',
'background_b ',
background_blue_arg
]],
shell=True
)
])
定义了background_blue_arg
,默认值为255。
可以用以下命令来查看launch file提供了哪些参数及其默认值:
ros2 launch my_launch_test test.launch.py --show-args
返回:
现在可以将所需的参数传递给启动文件,例如调用以下命令将背景色蓝色值设置为150:
ros2 launch my_launch_test test.launch.py background_blue_arg:=150
当我们有了一个个完善的launch file,比如这launch file用于启动深度相机,另一个launch file用于启动导航,而我们需要写一个launch file同时包含深度相机和导航的启动功能。launch系统为我们提供给了嵌套复用的功能,直接可以launch file中包含其它launch file,这就使得launch file更灵活,更方便。
编写launch file过程中的目标之一应该是使它们尽可能可重用。这可以通过将相关节点和配置集群到单独的启动文件中来完成。之后,可以编写专用于特定配置的顶层launch file。这将允许在完全不更改启动文件的情况下在相同的机器人之间移动。即使是从真正的机器人移动到模拟机器人这样的变化,也只需进行少量更改即可完成。
我们另外编写个launch file简单命名为top_level_test.launch.py。里面将直接复用前一节的launch file,同时运行一个命令发布速度让乌龟转圈。代码如下:
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import ExecuteProcess, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
def generate_launch_description():
two_turtle_sim = IncludeLaunchDescription(
PythonLaunchDescriptionSource([os.path.join(
get_package_share_directory('my_launch_test'), 'launch'),
'/test.launch.py']),
launch_arguments={'background_blue_arg': '150'}.items()
)
cmd_execute = ExecuteProcess(
cmd=[[
'ros2 topic pub -r 1 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: -1.8}}"'
]],
shell=True
)
return LaunchDescription([
two_turtle_sim,
cmd_execute
])
运行效果如下:
本篇介绍了一下launch比较常用的功能,描述了为大型项目编写启动文件的一些技巧。重点是如何构建启动文件,以便在不同情况下尽可能多地重用它们。可以看到ROS2使用python实现的launch非常实用且灵活。
当我们编写一个大型的ROS2项目的launch file时,主要就是进行模块化launch file的实现,把每个模块的launch file做到尽可能可重用,最后再编写个顶层的launch file来调用各个模块launch file。
其实还有很多的功能并没有在文档中描述出来,大家可以在实践中多多参考别人的代码,不断完善相关知识。