目标:学习使用 ROS 2 启动文件管理大型项目的最佳实践。
教程级别:中级时间: 20 分钟
目录
·背景
·前置条件
·引言
·编写启动文件
o1 顶层组织
o2 参数
o3 命名空间
o4 重用节点
o5 参数覆盖
o6 重映射
o7 配置文件
o8 环境变量
·运行启动文件
o1 更新setup.py
o2 构建并运行
·总结
背景
本教程描述了为大型项目编写启动文件的一些技巧。重点是如何结构化启动文件,以便它们能够在不同情况下尽可能多地重用。此外,它还涵盖了不同 ROS 2 启动工具的使用示例,如参数、YAML 文件、重映射、命名空间、默认参数和 RViz 配置。
前置条件
本教程使用turtlesim和turtle_tf2_py包。本教程还假定您已经创建了一个名为launch_tutorial、构建类型为ament_python的新包。
引言
机器人上的大型应用程序通常涉及多个相互连接的节点,每个节点可以有许多参数。在乌龟模拟器中模拟多只乌龟可以作为一个很好的例子。乌龟模拟由多个乌龟节点、世界配置、TF 广播器和监听器节点组成。在所有节点之间,有大量影响这些节点行为和外观的 ROS 参数。ROS 2 启动文件允许我们在一个地方启动所有节点并设置相应的参数。在本教程结束时,您将在launch_tutorial包中构建launch_turtlesim_launch启动文件。该启动文件将启动负责两个turtlesim模拟的不同节点,启动 TF 广播器和监听器,加载参数,并启动 RViz 配置。在本教程中,我们将详细介绍这个启动文件及其使用的所有相关功能。
注意:启动文件可以用 XML、YAML 或 Python 格式编写。在本教程中,使用选项卡以所有三种格式显示启动文件。您可以选择您喜欢的任何格式——它们在功能上是等效的。无论您在哪里看到文件名launch_turtlesim_launch,请确保为您的启动文件类型使用正确的文件扩展名(即 Python 使用launch_turtlesim_launch.py,XML 使用launch_turtlesim_launch.xml,YAML 使用launch_turtlesim_launch.yaml)。
编写启动文件
1 顶层组织
编写启动文件过程中的目标之一是使它们尽可能可重用。这可以通过将相关节点和配置聚类到单独的启动文件中来实现。之后,可以编写一个专用于特定配置的顶层启动文件。这将允许在相同的机器人之间移动而无需更改启动文件。即使是像从真实机器人切换到模拟机器人这样的更改,也只需少量更改即可完成。
我们现在将介绍使这成为可能的顶层启动文件结构。首先,我们将创建一个将调用单独启动文件的启动文件。为此,让我们在launch_tutorial包的/launch文件夹中创建一个launch_turtlesim_launch文件。
注意:较早期的启动系统版本可能不支持在include语句内部使用let,而需要使用arg。语法相同:name和value属性保持不变(例如,<arg name="target_frame" value="carrot1" />)。
Python:将完整代码复制粘贴到launch/launch_turtlesim_launch.py文件中:
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.substitutions import PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
launch_dir = PathJoinSubstitution([FindPackageShare('launch_tutorial'), 'launch'])
returnLaunchDescription([
IncludeLaunchDescription(
PathJoinSubstitution([launch_dir, 'turtlesim_world_1_launch.py'])
),IncludeLaunchDescription(
PathJoinSubstitution([launch_dir, 'turtlesim_world_2_launch.py'])
),IncludeLaunchDescription(
PathJoinSubstitution([launch_dir, 'broadcaster_listener_launch.py']),
launch_arguments={'target_frame': 'carrot1'}.items()
),IncludeLaunchDescription(
PathJoinSubstitution([launch_dir, 'mimic_launch.py'])
),IncludeLaunchDescription(
PathJoinSubstitution([launch_dir, 'fixed_broadcaster_launch.py'])
),IncludeLaunchDescription(
PathJoinSubstitution([launch_dir, 'turtlesim_rviz_launch.py'])
),])此启动文件包含一组其他启动文件。这些被包含的启动文件中的每一个都包含节点、参数,以及可能的嵌套包含,这些都属于系统的一个部分。准确地说,我们启动了两种turtlesim模拟世界、TF 广播器、TF 监听器、模仿节点、固定框架广播器和 RViz 节点。
设计提示:顶层启动文件应该简短,包含指向应用程序子组件对应其他文件的包含,以及经常更改的参数。
以后面这种方式编写启动文件可以很容易地替换系统的某个部分,我们稍后会看到。然而,在某些情况下,由于性能和使用原因,某些节点或启动文件必须单独启动。
设计提示:在决定您的应用程序需要多少个顶层启动文件时,要注意权衡。
2 参数
2.1 在启动文件中设置参数
我们将开始编写一个启动文件,用于启动我们的第一个turtlesim模拟。首先,创建一个名为turtlesim_world_1_launch的新文件。
Python:将完整代码复制粘贴到launch/turtlesim_world_1_launch.py文件中:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
returnLaunchDescription([
DeclareLaunchArgument('background_r', default_value='0'),
DeclareLaunchArgument('background_g', default_value='84'),
DeclareLaunchArgument('background_b', default_value='122'),
Node(
package='turtlesim',
executable='turtlesim_node',
name='sim',parameters=[{
'background_r': LaunchConfiguration('background_r'),
'background_g': LaunchConfiguration('background_g'),
'background_b': LaunchConfiguration('background_b'),
}]),])
此启动文件启动turtlesim_node节点,该节点启动turtlesim模拟,并带有定义并传递给节点的模拟配置参数。
2.2 从 YAML 文件加载参数
在第二个启动文件中,我们将启动第二个具有不同配置的turtlesim模拟。现在创建一个turtlesim_world_2_launch文件。
Python:将完整代码复制粘贴到launch/turtlesim_world_2_launch.py文件中:
from launch import LaunchDescription
from launch.substitutions import PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
returnLaunchDescription([
Node(
package='turtlesim',
executable='turtlesim_node',
namespace='turtlesim2',name='sim',parameters=[PathJoinSubstitution([
FindPackageShare('launch_tutorial'), 'config', 'turtlesim.yaml'])
],),])
此启动文件将启动相同的turtlesim_node,其参数值直接从 YAML 配置文件加载。在 YAML 文件中定义参数和变量使得存储和加载大量变量变得容易。同样值得注意的是,这个 YAML 文件不是另一个启动文件,而是为turtlesim_node设置节点参数的配置文件。此外,YAML 文件可以轻松地从当前的ros2 param list导出。要学习如何做到这一点,请参考理解参数教程。
现在让我们在包的/config文件夹中创建一个配置文件turtlesim.yaml,该文件将由我们的启动文件加载。
/turtlesim2/sim:ros__parameters:
background_b: 255
background_g: 86
background_r: 150
要了解有关使用参数和使用 YAML 文件的更多信息,请查看理解参数教程。
2.3 在 YAML 文件中使用通配符
有些情况下,我们希望在多个节点中设置相同的参数。这些节点可能有不同的命名空间或名称,但仍然具有相同的参数。定义明确指定命名空间和节点名称的单独 YAML 文件效率不高。一种解决方案是使用通配符,它们作为文本值中未知字符的替换,将参数应用于多个不同的节点。
现在让我们创建一个类似于turtlesim_world_2_launch的新turtlesim_world_3_launch文件,以在命名空间turtlesim3中包含一个turtlesim_node节点:
Python:将完整代码复制粘贴到launch/turtlesim_world_3_launch.py文件中:
from launch import LaunchDescription
from launch.substitutions import PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
returnLaunchDescription([
Node(
package='turtlesim',
executable='turtlesim_node',
namespace='turtlesim3',name='sim',parameters=[
PathJoinSubstitution([
FindPackageShare('launch_tutorial'), 'config', 'turtlesim.yaml'
]),],),])然而,加载相同的 YAML 文件不会影响第三个turtlesim世界的外观。原因在于其参数存储在另一个命名空间下,如下所示:
/turtlesim3/sim:background_b
background_g
background_r
因此,与其为使用相同参数的同一节点创建新配置,不如使用通配符语法。/**将分配所有节点中的所有参数,尽管节点名称和命名空间存在差异。
我们现在将以以下方式更新/config文件夹中的turtlesim.yaml:
/**:ros__parameters:
background_b: 255
background_g: 86
background_r: 150
现在将turtlesim_world_3_launch启动描述包含到我们的主启动文件中。在我们的启动描述中使用该配置文件将把background_b、background_g和background_r参数设置为turtlesim3/sim和turtlesim2/sim节点中的指定值。
3 命名空间
正如您可能已经注意到的,我们在turtlesim_world_2_launch文件中为turtlesim世界定义了命名空间。唯一的命名空间允许系统启动两个相似的节点而不会发生节点名称或话题名称冲突。
namespace='turtlesim2',然而,如果启动文件包含大量节点,为每个节点定义命名空间可能变得繁琐。为了解决这个问题,可以使用PushROSNamespace操作为每个启动文件描述定义全局命名空间。每个嵌套节点将自动继承该命名空间。
注意:PushROSNamespace必须是列表中的第一个操作,以便后续操作应用该命名空间。
要做到这一点,首先,我们需要从turtlesim_world_2_launch文件中删除namespace='turtlesim2'行。然后,我们需要更新launch_turtlesim_launch以将 include 语句更改为以下内容:
Python:
from launch.actions import GroupAction
from launch_ros.actions import PushROSNamespace
...GroupAction(actions=[
PushROSNamespace('turtlesim2'),
IncludeLaunchDescription(PathJoinSubstitution([launch_dir, 'turtlesim_world_2_launch.py'])),
]),因此,turtlesim_world_2_launch启动描述中的每个节点都将具有turtlesim2命名空间。
4 重用节点
现在创建一个broadcaster_listener_launch文件。
Python:将完整代码复制粘贴到launch/broadcaster_listener_launch.py文件中:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
returnLaunchDescription([
DeclareLaunchArgument(
'target_frame', default_value='turtle1',
description='Targetframe name.',),Node(
package='turtle_tf2_py',executable='turtle_tf2_broadcaster',name='broadcaster1',parameters=[
{'turtlename': 'turtle1'}
],),Node(
package='turtle_tf2_py',executable='turtle_tf2_broadcaster',name='broadcaster2',parameters=[
{'turtlename': 'turtle2'}
],),Node(
package='turtle_tf2_py',executable='turtle_tf2_listener',name='listener',parameters=[
{'target_frame': LaunchConfiguration('target_frame')}
],),])在此文件中,我们声明了target_frame启动参数,默认值为turtle1。默认值意味着启动文件可以接收参数以转发给其节点,或者如果没有提供参数,它将把默认值传递给其节点。
然后,我们在启动期间使用不同的名称和参数两次使用turtle_tf2_broadcaster节点。这使我们能够复制同一节点而不会发生冲突。
我们还启动了一个turtle_tf2_listener节点,并设置了我们上面声明和获取的target_frame参数。
5 参数覆盖
回想一下,我们在顶层启动文件中调用了broadcaster_listener_launch文件。除此之外,我们还向它传递了target_frame启动参数,如下所示:
Python:
IncludeLaunchDescription(PathJoinSubstitution([launch_dir, 'broadcaster_listener_launch.py']),
launch_arguments={'target_frame': 'carrot1'}.items()
),这种语法允许我们将默认目标框架更改为carrot1。如果您希望turtle2跟随turtle1而不是carrot1,只需删除传递target_frame参数的行。这会将target_frame分配为其默认值turtle1。
6 重映射
现在创建一个mimic_launch文件。
Python:将完整代码复制粘贴到launch/mimic_launch.py文件中:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
returnLaunchDescription([
Node(
package='turtlesim',
executable='mimic',name='mimic',remappings=[
('/input/pose','/turtle2/pose'),('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
])])此启动文件将启动mimic节点,该节点将向一个turtlesim发出命令以跟随另一个。该节点设计用于在/input/pose话题上接收目标位姿。在我们的例子中,我们希望将目标位姿从/turtle2/pose话题重映射。最后,我们将/output/cmd_vel话题重映射到/turtlesim2/turtle1/cmd_vel。这样,我们turtlesim2模拟世界中的turtle1将跟随我们初始turtlesim世界中的turtle2。
7 配置文件
现在让我们创建一个名为turtlesim_rviz_launch的文件。
Python:将完整代码复制粘贴到launch/turtlesim_rviz_launch.py文件中:
from launch import LaunchDescription
from launch.substitutions import PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description():
returnLaunchDescription([
Node(
package='rviz2',executable='rviz2',name='rviz2',arguments=['-d', PathJoinSubstitution([
FindPackageShare('turtle_tf2_py'), 'rviz', 'turtle_rviz.rviz'
])],),])此启动文件将启动 RViz,并使用turtle_tf2_py包中定义的配置文件。此 RViz 配置将设置世界框架,启用 TF 可视化,并以俯视图启动 RViz。
8 环境变量
现在让我们在我们的包中创建最后一个名为fixed_broadcaster_launch的启动文件。
Python:将完整代码复制粘贴到launch/fixed_broadcaster_launch.py文件中:
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import EnvironmentVariable, LaunchConfiguration
from launch_ros.actions import Node
def generate_launch_description():
returnLaunchDescription([
DeclareLaunchArgument(
'node_prefix',
default_value=[EnvironmentVariable('USER'), '_'],
description='prefixfor node name'),Node(
package='turtle_tf2_py',executable='fixed_frame_tf2_broadcaster',name=[LaunchConfiguration('node_prefix'), 'fixed_broadcaster'],
),])此启动文件展示了可以在启动文件内部调用环境变量的方式。环境变量可用于定义或推送命名空间,以区分不同计算机或机器人上的节点。
注意:如果您在未定义USER环境变量的情况下运行启动文件(如在 ROS Docker 文件中),则可以将上面的环境变量引用替换为您喜欢的任何其他词。
运行启动文件
1 更新 setup.py
打开setup.py并添加以下行,以便安装来自launch/文件夹的启动文件和来自config/的配置文件。data_files字段现在应如下所示:
import os
from glob import globfrom setuptools import setup
...data_files=[
...(os.path.join('share', package_name, 'launch'),
glob('launch/*')),(os.path.join('share', package_name, 'config'),
glob('config/*.yaml')),
(os.path.join('share', package_name, 'rviz'),
glob('config/*.rviz')),
],2 构建并运行
最后查看我们代码的结果,使用以下命令构建包并启动顶层启动文件:
Python:
$ ros2 launch launch_tutorial launch_turtlesim_launch.py
您现在将看到两个turtlesim模拟启动。第一个中有两只乌龟,第二个中有一只。在第一个模拟中,turtle2在世界左下角生成。它的目标是到达carrot1框架,该框架相对于turtle1框架在 x 轴上五米远。
第二个中的turtlesim2/turtle1旨在模仿turtle2的行为。
如果您想控制turtle1,请运行teleop节点。
$ ros2 run turtlesimturtle_teleop_key
结果,您将看到类似的画面:

除此之外,RViz 应该已经启动。它将显示相对于world框架的所有乌龟框架,其原点位于左下角。

总结
在本教程中,您学习了使用 ROS 2 启动文件管理大型项目的各种技巧和实践。
本文档完整翻译自 ROS 2 Lyrical 官方文档,仅用于学习交流。
夜雨聆风