管理大型项目

目标: 学习使用ROS 2启动文件管理大型项目的最佳实践。

教程级别: 中级

**时间:**20分钟

背景

本教程介绍了编写大型项目启动文件的一些技巧。重点是如何结构化启动文件,使其在不同情况下尽可能地被重用。此外,还介绍了不同的ROS 2启动工具的使用示例,例如参数、YAML文件、重映射、命名空间、默认参数和RViz配置。

先决条件

本教程使用了 turtlesimturtle_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 os

from ament_index_python.packages import get_package_share_directory

from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource


def 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广播器、TF监听器、模仿器、固定帧广播器和RViz节点。

注解

设计提示:顶层启动文件应该简短,由包含到应用程序的子组件对应文件的包含项组成,并包含常常更改的参数。

按照以下方式编写启动文件可以轻松地替换系统的某个部分,我们稍后将会看到。然而,出于性能和使用原因,有时需要单独启动某些节点或启动文件。

注解

设计提示:在决定应用程序需要多少个顶层启动文件时,请注意权衡。

2 参数

2.1 在启动文件中设置参数

我们将首先编写一个启动文件,用于启动我们的第一个turtlesim仿真。首先,创建一个名为``turtlesim_world_1.launch.py``的新文件。

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, TextSubstitution

from launch_ros.actions import Node


def 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仿真,并将定义和传递给节点的仿真配置参数。

2.2 从YAML文件中加载参数

在第二个启动文件中,我们将使用不同的配置启动第二个turtlesim仿真。现在创建一个``turtlesim_world_2.launch.py``文件。

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('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 param``列表导出。要了解如何做到这一点,请参考 了解参数 教程。

现在让我们在包的``/config``文件夹中创建一个名为``turtlesim.yaml``的配置文件,这个文件将被我们的启动文件加载。

/turtlesim2/sim:
   ros__parameters:
      background_b: 255
      background_g: 86
      background_r: 150

如果我们现在启动``turtlesim_world_2.launch.py``启动文件,我们将启动带有预配置背景颜色的``turtlesim_node``。

要了解更多关于使用参数和使用YAML文件的信息,请查看 了解参数 教程。

2.3 在YAML文件中使用通配符

有些情况下,我们希望在多个节点中设置相同的参数。这些节点可能有不同的命名空间或名称,但仍具有相同的参数。定义单独的YAML文件来显式定义命名空间和节点名称是不高效的。解决方案是使用通配符字符,它们作为未知字符的替代,将参数应用于多个不同的节点。

现在让我们创建一个新的 turtlesim_world_3.launch.py 文件,与 turtlesim_world_2.launch.py 类似,添加一个额外的 turtlesim_node 节点。

...
Node(
   package='turtlesim',
   executable='turtlesim_node',
   namespace='turtlesim3',
   name='sim',
   parameters=[config]
)

然而,加载相同的 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.py 启动描述。在我们的启动描述中使用该配置文件将为 turtlesim3/simturtlesim2/sim 节点分配 background_bbackground_gbackground_r 参数指定的值。

3 个命名空间

正如您可能已经注意到的那样,我们在``turtlesim_world_2.launch.py``文件中定义了turtlesim世界的命名空间。唯一的命名空间可以让系统在没有节点名称或主题名称冲突的情况下启动两个相似的节点。

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``命名空间。

4 重复使用节点

现在创建一个``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():
   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')}
         ]
      ),
   ])

在该文件中,我们使用默认值``turtle1``声明了``target_frame``的启动参数。默认值意味着启动文件可以接收一个参数并传递给其节点,或者在未提供参数的情况下,将默认值传递给其节点。

之后,我们在启动过程中两次使用了不同名称和参数的``turtle_tf2_broadcaster``节点。这样可以在不冲突的情况下复制相同的节点。

我们还启动了一个``turtle_tf2_listener``节点,并设置了先前声明和获取的``target_frame``参数。

5 参数覆盖

回想一下,在我们的顶层启动文件中调用了``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``。

6 重映射

现在创建一个 mimic.launch.py 文件。

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
   return LaunchDescription([
      Node(
         package='turtlesim',
         executable='mimic',
         name='mimic',
         remappings=[
            ('/input/pose', '/turtle2/pose'),
            ('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
         ]
      )
   ])

这个启动文件将启动 mimic 节点,该节点将向一个 turtlesim 发送指令,让它跟随另一个 turtlesim。该节点设计用于在主题 /input/pose 上接收目标姿态。在我们的情况下,我们希望将目标姿态重映射到 /turtle2/pose 主题上。最后,我们将 /output/cmd_vel 主题重映射到 /turtlesim2/turtle1/cmd_vel。这样,在我们的 turtlesim2 模拟世界中的 turtle1 将跟随我们初始 turtlesim 世界中的 turtle2

7 配置文件

现在让我们创建一个名为 turtlesim_rviz.launch.py 的文件。

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():
   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。

8 环境变量

现在我们来创建最后一个启动文件,名为``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():
   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'],
      ),
   ])

该启动文件展示了在启动文件中如何调用环境变量。环境变量可用于定义或推送命名空间,以区分不同计算机或机器人上的节点。

运行启动文件

1 更新 setup.py

打开``setup.py``并添加以下行,以便安装``launch/``文件夹中的启动文件和``config/``中的配置文件。``data_files``字段现在应该如下所示:

import os
from glob import glob
from setuptools import setup
...

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'))),
   ],

2 构建和运行

为了最终看到我们代码的结果,请使用以下命令构建包并启动顶层启动文件:

ros2 launch launch_tutorial launch_turtlesim.launch.py

现在您将看到两个turtlesim模拟开始运行。第一个模拟中有两只乌龟,第二个模拟中只有一只乌龟。在第一个模拟中,“turtle2”被生成在世界的左下角。它的目标是相对于“turtle1”框架在x轴上距离为五米的“carrot1”框架。

第二个模拟中的“turtlesim2/turtle1”被设计成模仿“turtle2”的行为。

如果您想控制“turtle1”,请运行teleop节点。

ros2 run turtlesim turtle_teleop_key

结果,您将看到类似的画面:

../../../_images/turtlesim_worlds.png

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

../../../_images/turtlesim_rviz.png

总结

在本教程中,您学习了使用ROS 2 launch文件管理大型项目的各种提示和实践。