Launch启动文件可以一次性启动多个节点文件,是ROS系统中多节点启动与配置的一种脚本。
ROS2中的Launch文件就是基于Python描述的,其核心目的是启动节点,我们在命令行中输入的各种参数,在Launch文件中,通过类似这样的很多代码模版,也可以进行配置,甚至还可以使用Python原有的编程功能,大大丰富了启动过程中的多样化配置。它就像粘合剂一样,可以自由组装和配置各个节点。
可以在命令行内使用ros2中的launch命令来启动一个launch文件:
ros2 launch learning_launch simple.launch.py
该launch文件内容如下:
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', # 节点的可执行文件名
),
])
我们使用ros2命令在终端中启动节点时,还可以在命令后配置一些传入程序的参数,使用launch文件一样可以做到。
传递其他参数的代码如下:
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', # 对节点重新命名,不用初始化节点时的默认名
# 会替换掉节点程序内本身初始化设定的名字
# 以打开多个相同节点时区分名字(ros2不允许同名节点)
arguments=['-d', rviz_config] # 加载命令行参数
)
])
config部分就是利用了python代码的特性,配置了具体变量路径。
launch还可以实现资源重映射,在使用别人代码的时候,对类似的资源重新命名,提高软件的复用性。如下,时借助turtle的一个launch的示例代码:
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
]
)
])
namespace和c++的命名空间类似,会在节点的名字前添加一个父目录。此时通过ros2 topic list查看话题可以看到:
/parameter_events
/rosout
/turtlesim1/turtle1/cmd_vel
/turtlesim1/turtle1/color_sensor
/turtlesim1/turtle1/pose
/turtlesim2/turtle1/cmd_vel
/turtlesim2/turtle1/color_sensor
/turtlesim2/turtle1/pose
从命名空间角度避免了节点重名问题(ros2不允许同名节点存在)。
mimic节点的作用是实现一个海龟的位置变成另一个海龟的指令,本身订阅话题/input/pose并接收受一个海龟运动后的位置参数,然后再将这些参数发布到话题/output/cmd_vel。remappings参数则是对资源的重映射,将原本订阅的话题和发布的话题地址都重定向了。于是实现了,用turtlesim1命名空间内的海龟位置控制turtlesim2命名空间内的海龟位置。
在使用别人的代码时,若别人代码设置的节点名、话题名等不适合自己的项目时,可以用这种方法去调整,也不需要修改源码。
通过launch还可以修改一些执行参数,仍举海龟例子,修改其打开时的背景颜色:
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
}]
),
])
LaunchConfiguration是去系统内读取参数的值(background_r、background_g、background_b参数),这几个参数的值则是上面的代码进行定义的:
background_r_launch_arg = DeclareLaunchArgument(
'background_r', default_value=TextSubstitution(text='0') # 创建一个Launch文件内参数(arg)background_r
)
LaunchDescription生成launch描述信息的时候再传递入background_r_launch_arg等三个参数以实现保存和调用。
传入这三个参数还是显得有些冗余,在launch内可以用yaml配置文件完成参数传递的配置,上述代码可优化为:
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] # 加载参数文件
)
])
对应的turtlesim.yaml文件内容如下:
/turtlesim2/sim:
ros__parameters:
background_b: 0
background_g: 0
background_r: 0
第一行的/turtlesim2/sim对应命名空间,不可随意修改。
在复杂的机器人系统中,launch文件也会有很多,此时我们可以使用类似编程中的include机制,让launch文件互相包含。如下:
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文件中启动的功能加上命名空间,即在其前面加一个父目录,避免原本的命名和当前launch的命名冲突
actions=[
PushRosNamespace('turtlesim2'),
parameter_yaml]
)
return LaunchDescription([ # 返回launch文件的描述信息
parameter_yaml_with_namespace # return得到launch描述的时候会得到上面配置的parameter_yaml_with_namespace
])
最后,所有的launch文件都必须拷贝到install的安装空间内。为了实现这个,必须在setup.py配置文件的data_files部分里添加如下内容:
(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', '*.*'))),
完整的setup.py如下:
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': [
],
},
)
这样配置后,colcon编译时才会将launch文件拷贝到install文件夹内,在运行时找到并成功运行。否则虽然可以正常编译,但运行时会无法找到相应文件。