在ROS开发中,难免出现大规模的节点,如何管理这些节点,而构造出成规模的项目?且看ROS2的开发团队的建议。
本教程介绍了为大型项目编写启动文件的一些技巧。重点是如何构建启动文件,以便它们可以在不同情况下尽可能多地重复使用。此外,它还涵盖了不同 ROS 2 启动工具的使用示例,例如参数、YAML 文件、重新映射、命名空间、默认参数和 RViz 配置。
本教程使用 turtlesim 和 turtle_tf2_py 包。本教程还假设您已经创建了一个名为 launch_tutorial 的构建类型为 ament_python 的新包。
机器人上的大型应用程序通常涉及多个互连节点,每个节点都可以有很多参数。在海龟模拟器中模拟多只海龟可以作为一个很好的例子。海龟模拟由多个海龟节点、世界配置以及 TF 广播器和侦听器节点组成。在所有节点之间,存在大量影响这些节点的行为和外观的 ROS 参数。 ROS 2 启动文件允许我们启动所有节点并在一个地方设置相应的参数。在教程结束时,您将在 launch_tutorial 包中构建 launch_turtlesim.launch.py 启动文件。这个启动文件将调出负责模拟两个 turtlesim 模拟的不同节点,启动 TF 广播器和监听器,加载参数,并启动 RViz 配置。在本教程中,我们将介绍此启动文件和使用的所有相关功能。
1)顶层管理
编写启动文件过程中的目标之一应该是使它们尽可能可重用。这可以通过将相关节点和配置集群到单独的启动文件中来完成。之后,可以编写专用于特定配置的顶级启动文件。这将允许在完全不更改启动文件的情况下在相同的机器人之间移动。即使是从真实机器人转移到模拟机器人这样的变化,也只需进行少量更改即可完成。
我们现在将讨论使这成为可能的顶级启动文件结构。首先,我们将创建一个启动文件,它将调用单独的启动文件。为此,让我们在 launch_tutorial 包的 /launch 文件夹中创建一个 launch_turtlesim.launch.py 文件。
import osfrom ament_index_python.packages import get_package_share_directoryfrom launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSourcedef generate_launch_description():turtlesim_world_1 = IncludeLaunchDescription(PythonLaunchDescriptionSource([os.path.join(get_package_share_directory('launch_tutorial'), 'launch'),'/turtlesim_world_1.launch.py']))turtlesim_world_2 = IncludeLaunchDescription(PythonLaunchDescriptionSource([os.path.join(get_package_share_directory('launch_tutorial'), 'launch'),'/turtlesim_world_2.launch.py']))broadcaster_listener_nodes = IncludeLaunchDescription(PythonLaunchDescriptionSource([os.path.join(get_package_share_directory('launch_tutorial'), 'launch'),'/broadcaster_listener.launch.py']),launch_arguments={'target_frame': 'carrot1'}.items(),)mimic_node = IncludeLaunchDescription(PythonLaunchDescriptionSource([os.path.join(get_package_share_directory('launch_tutorial'), 'launch'),'/mimic.launch.py']))fixed_frame_node = IncludeLaunchDescription(PythonLaunchDescriptionSource([os.path.join(get_package_share_directory('launch_tutorial'), 'launch'),'/fixed_broadcaster.launch.py']))rviz_node = IncludeLaunchDescription(PythonLaunchDescriptionSource([os.path.join(get_package_share_directory('launch_tutorial'), 'launch'),'/turtlesim_rviz.launch.py']))return LaunchDescription([turtlesim_world_1,turtlesim_world_2,broadcaster_listener_nodes,mimic_node,fixed_frame_node,rviz_node])
此启动文件包括一组其他启动文件。这些包含的启动文件中的每一个都包含节点、参数,并且可能还包含嵌套的包含,这些包含属于系统的一部分。确切地说,我们启动了两个 turtlesim 模拟世界,TF broadcaster、TF listener、mimic、fixed frame broadcaster 和 RViz 节点。
注意1
设计提示:顶层启动文件应该简短,包含与应用程序子组件对应的其他文件的包含,以及经常更改的参数。
以下列方式编写启动文件可以很容易地换出系统的一部分,我们稍后会看到。但是,有时由于性能和使用原因,某些节点或启动文件必须单独启动。
注意2
设计提示:在决定您的应用程序需要多少顶级启动文件时,请注意权衡。
我们将从编写一个启动文件开始我们的第一个 turtlesim 模拟。首先,创建一个名为 turtlesim_world_1.launch.py 的新文件。
我们将从编写一个启动文件开始我们的第一个 turtlesim 模拟。首先,创建一个名为 turtlesim_world_1.launch.py 的新文件。
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, TextSubstitutionfrom launch_ros.actions import Nodedef generate_launch_description():background_r_launch_arg = DeclareLaunchArgument('background_r', default_value=TextSubstitution(text='0'))background_g_launch_arg = DeclareLaunchArgument('background_g', default_value=TextSubstitution(text='84'))background_b_launch_arg = DeclareLaunchArgument('background_b', default_value=TextSubstitution(text='122'))return LaunchDescription([background_r_launch_arg,background_g_launch_arg,background_b_launch_arg,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 模拟,具有定义并传递给节点的模拟配置参数。
在第二次启动时,我们将使用不同的配置启动第二个 turtlesim 模拟。现在创建一个 turtlesim_world_2.launch.py 文件。
import osfrom ament_index_python.packages import get_package_share_directoryfrom launch import LaunchDescription
from launch_ros.actions import Nodedef generate_launch_description():config = os.path.join(get_package_share_directory('launch_tutorial'),'config','turtlesim.yaml')return LaunchDescription([Node(package='turtlesim',executable='turtlesim_node',namespace='turtlesim2',name='sim',parameters=[config])])
此启动文件将使用直接从 YAML 配置文件加载的参数值启动相同的 turtlesim_node。在 YAML 文件中定义参数和参数可以方便地存储和加载大量变量。此外,YAML 文件可以很容易地从当前的 ros2 参数列表中导出。要了解如何执行此操作,请参阅了解参数教程。
现在让我们在包的 /config 文件夹中创建一个配置文件 turtlesim.yaml,它将由我们的启动文件加载。
/turtlesim2/sim:ros__parameters:background_b: 255background_g: 86background_r: 150
If we now start theturtlesim_world_2.launch.py
launch file, we will start theturtlesim_node
with preconfigured background colors.
要了解有关使用参数和使用 YAML 文件的更多信息,请查看了解参数教程。
有时我们想在多个节点中设置相同的参数。这些节点可以有不同的名称空间或名称,但仍具有相同的参数。定义明确定义命名空间和节点名称的单独 YAML 文件效率不高。一种解决方案是使用通配符来替代文本值中的未知字符,以将参数应用于多个不同的节点。
现在让我们创建一个类似于 turtlesim_world_2.launch.py 的新 turtlesim_world_3.launch.py 文件以包含一个 turtlesim_node 节点。
...
Node(package='turtlesim',executable='turtlesim_node',namespace='turtlesim3',name='sim',parameters=[config]
)
然而,加载相同的 YAML 文件不会影响第三个 turtlesim 世界的外观。原因是它的参数存储在另一个命名空间下,如下所示:
/turtlesim3/sim:background_bbackground_gbackground_r
因此,我们可以使用通配符语法,而不是为使用相同参数的同一节点创建新配置。/**
将在每个节点中分配所有参数,尽管节点名称和名称空间不同。我们现在将按以下方式更新 /config 文件夹中的 turtlesim.yaml:
/**:ros__parameters:background_b: 255background_g: 86background_r: 150
现在在我们的主启动文件中包含 turtlesim_world_3.launch.py 启动描述。在我们的启动描述中使用该配置文件会将 background_b、background_g 和 background_r 参数分配给 turtlesim3/sim 和 turtlesim2/sim 节点中的指定值。
您可能已经注意到,我们在 turtlesim_world_2.launch.py 文件中定义了 turlesim 世界的命名空间。独特的命名空间允许系统启动两个相似的节点,而不会出现节点名称或主题名称冲突。
namespace='turtlesim2',
但是,如果启动文件包含大量节点,则为每个节点定义命名空间可能会变得乏味。为解决该问题,可以使用 PushRosNamespace 操作为每个启动文件描述定义全局命名空间。每个嵌套节点都将自动继承该命名空间。
为此,首先,我们需要从 turtlesim_world_2.launch.py 文件中删除 namespace='turtlesim2' 行。之后,我们需要更新 launch_turtlesim.launch.py 以包含以下几行:
from launch.actions import GroupAction
from launch_ros.actions import PushRosNamespace...turtlesim_world_2 = IncludeLaunchDescription(PythonLaunchDescriptionSource([os.path.join(get_package_share_directory('launch_tutorial'), 'launch'),'/turtlesim_world_2.launch.py']))turtlesim_world_2_with_namespace = GroupAction(actions=[PushRosNamespace('turtlesim2'),turtlesim_world_2,])
最后,我们将 return LaunchDescription 语句中的 turtlesim_world_2 替换为 turtlesim_world_2_with_namespace。
因此,turtlesim_world_2.launch.py 启动描述中的每个节点都将有一个 turtlesim2 命名空间。
现在创建一个 broadcaster_listener.launch.py 文件。
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfigurationfrom launch_ros.actions import Nodedef generate_launch_description():return LaunchDescription([DeclareLaunchArgument('target_frame', default_value='turtle1',description='Target frame 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 参数。
回想一下,我们在顶级启动文件中调用了 broadcaster_listener.launch.py 文件。除此之外,我们还向它传递了 target_frame 启动参数,如下所示:
broadcaster_listener_nodes = IncludeLaunchDescription(PythonLaunchDescriptionSource([os.path.join(get_package_share_directory('launch_tutorial'), 'launch'),'/broadcaster_listener.launch.py']),launch_arguments={'target_frame': 'carrot1'}.items(),)
此语法允许我们将默认目标目标框架更改为 carrot1。如果您希望 turtle2 跟随 turtle1 而不是 carrot1,只需删除定义 launch_arguments 的行。这将为 target_frame 分配其默认值,即 turtle1。
现在创建一个 mimic.launch.py 文件。
from launch import LaunchDescription
from launch_ros.actions import Nodedef generate_launch_description():return LaunchDescription([Node(package='turtlesim',executable='mimic',name='mimic',remappings=[('/input/pose', '/turtle2/pose'),('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),])])
此启动文件将启动模拟节点,它将向一个海龟发出命令以跟随另一个。该节点旨在接收关于主题 /input/pose 的目标姿势。在我们的例子中,我们想要从 /turtle2/pose 主题重新映射目标姿势。最后,我们将 /output/cmd_vel 主题重新映射到 /turtlesim2/turtle1/cmd_vel。这样,我们的 turtlesim2 模拟世界中的 turtle1 将跟随我们初始 turtlesim 世界中的 turtle2。
现在让我们创建一个名为 turtlesim_rviz.launch.py 的文件。
import osfrom ament_index_python.packages import get_package_share_directoryfrom launch import LaunchDescription
from launch_ros.actions import Nodedef generate_launch_description():rviz_config = os.path.join(get_package_share_directory('turtle_tf2_py'),'rviz','turtle_rviz.rviz')return LaunchDescription([Node(package='rviz2',executable='rviz2',name='rviz2',arguments=['-d', rviz_config])])
此启动文件将使用 turtle_tf2_py 包中定义的配置文件启动 RViz。此 RViz 配置将设置世界坐标系,启用 TF 可视化,并以自上而下的视图启动 RViz。
现在让我们在我们的包中创建最后一个名为 fixed_broadcaster.launch.py 的启动文件。
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import EnvironmentVariable, LaunchConfiguration
from launch_ros.actions import Nodedef generate_launch_description():return LaunchDescription([DeclareLaunchArgument('node_prefix',default_value=[EnvironmentVariable('USER'), '_'],description='prefix for node name'),Node(package='turtle_tf2_py',executable='fixed_frame_tf2_broadcaster',name=[LaunchConfiguration('node_prefix'), 'fixed_broadcaster'],),])
此启动文件显示了在启动文件中调用环境变量的方式。环境变量可用于定义或推送命名空间,以区分不同计算机或机器人上的节点。
打开 setup.py 并添加以下行,以便安装 launch/ 文件夹中的启动文件和 config/ 中的配置文件。 data_files 字段现在应如下所示:
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', '*.yaml'))),],
要最终查看代码的结果,请使用以下命令构建包并启动顶级启动文件:
ros2 launch launch_tutorial launch_turtlesim.launch.py
您现在将看到两个 turtlesim 模拟已启动。第一个有两只乌龟,第二个有一只。在第一个模拟中,turtle2 生成在世界的左下角。它的目标是到达相对于 turtle1 框架在 x 轴上五米远的 carrot1 框架。
第二个中的 turtlesim2/turtle1 旨在模仿 turtle2 的行为。
如果您想控制 turtle1,请运行 teleop 节点。
ros2 run turtlesim turtle_teleop_key
结果,您将看到类似的图片:
除此之外,RViz 应该已经启动。它将显示相对于原点位于左下角的世界坐标系的所有海龟坐标系。
在本教程中,您了解了使用 ROS 2 启动文件管理大型项目的各种技巧和实践。