管理大型项目

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

教程级别:中级

时间:20 分钟

背景

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

先决条件

本教程使用 turtlesimturtle_tf2_py 包。 本教程还假设您已经:doc:创建了一个名为``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 节点。

Note

Design Tip: Top-level launch files should be short, consist of includes to other files corresponding to subcomponents of the application, and commonly changed parameters.

按照以下方式编写启动文件可以轻松更换系统的一部分,我们稍后会看到。 但是,在某些情况下,由于性能和使用原因,某些节点或启动文件必须单独启动。

Note

Design tip: Be aware of the tradeoffs when deciding how many top-level launch files your application requires.

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

此启动文件将启动相同的 turtlesim_node,其参数值直接从 YAML 配置文件中加载。 在 YAML 文件中定义参数和参数可以轻松存储和加载大量变量。 此外,可以从当前 ros2 param 列表中轻松导出 YAML 文件。 要了解如何执行此操作,请参阅 了解参数 教程。

现在让我们在包的 /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.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_b
   background_g
   background_r

因此,我们可以使用通配符语法,而不必为使用相同参数的同一节点创建新配置。

“/**”将分配每个节点中的所有参数,尽管节点名称和命名空间存在差异。

我们现在将以以下方式更新“/config”文件夹中的“turtlesim.yaml”:

/**:
   ros__parameters:
      background_b: 255
      background_g: 86
      background_r: 150

现在将“turtlesim_world_3.launch.py​​”启动描述包含在我们的主要启动文件中。 在我们的启动描述中使用该配置文件将把“background_b”、“background_g”和“background_r”参数分配给“turtlesim3/sim”和“turtlesim2/sim”节点中的指定值。

3 命名空间

您可能已经注意到,我们在“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”命名空间。

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

在这个文件中,我们声明了 target_frame 启动参数,其默认值为 turtle1。 默认值意味着启动文件可以接收一个参数以转发给其节点,或者在未提供参数的情况下,它将默认值传递给其节点。 之后,我们在启动期间使用不同的名称和参数两次使用 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'],
      ),
   ])

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

Note

If you are running the launch file where the USER environment variable is not defined (like in the ROS docker file), then you can replace the EnvironmentVariable('USER') above with any other word of your liking.

运行启动文件

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'))),
      (os.path.join('share', package_name, 'rviz'),
         glob(os.path.join('config', '*.rviz'))),
   ],

2 构建并运行

要最终看到代码的结果,请使用以下命令构建包并启动顶级启动文件:

ros2 launch launch_tutorial launch_turtlesim.launch.py

现在您将看到两个 turtlesim 模拟已启动。 第一个中有两只海龟,第二个中有一只。 在第一个模拟中,“turtle2”在世界的左下角生成。 它的目标是到达“carrot1”框架,该框架在 x 轴上相对于“turtle1”框架有五米远。

第二个中的“turtlesim2/turtle1”旨在模仿“turtle2”的行为。

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

ros2 run turtlesim turtle_teleop_key

结果你会看到类似的图片:

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

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

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

摘要

在本教程中,您了解了使用 ROS 2 启动文件管理大型项目的各种技巧和做法。