机器人系统ros2-开发实践04-ROS 2 启动文件管理大型项目的最佳实践

发布于:2024-05-03 ⋅ 阅读:(21) ⋅ 点赞:(0)

机器人上的大型应用通常涉及多个互连的节点,每个节点可以有许多参数。海龟模拟器中模拟多只海龟就是一个很好的例子。海龟模拟由多个海龟节点、世界配置以及 TF 广播器和监听器节点组成。在所有节点之间,存在大量影响这些节点的行为和外观的 ROS 参数。 ROS 2启动文件允许我们在一个地方启动所有节点并设置相应的参数。

通俗点就是将机器人各个功能节点启动放在代码里去配置和组合启动,并设置启动节点的一些初始化参数

1 编写启动文件

编写启动文件过程的目标之一应该是使它们尽可能可重用。这可以通过将相关节点和配置聚集到单独的启动文件中来完成。之后,可以编写专用于特定配置的顶级启动文件。这将允许在相同的机器人之间移动而无需更改启动文件。即使是从真实机器人转移到模拟机器人等改变也只需进行一些更改即可完成。

我们现在将回顾使这成为可能的顶级启动文件结构。首先,我们将创建一个启动文件,该文件将调用单独的启动文件。为此,我们在包的文件夹中创建一个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 在launch文件中设置参数

我们将首先编写一个启动文件来启动我们的第一个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 文件中定义实参和参数可以轻松存储和加载大量变量。此外,还可以轻松地从当前列表导出 YAML 文件。

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

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

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

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

因此,我们可以使用通配符语法,而不是为使用相同参数的同一节点创建新配置。 /**将分配每个节点中的所有参数,尽管节点名称和命名空间存在差异。

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

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

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

3 命名空间

您可能已经注意到,我们在turtlesim_world_2.launch.py文件中定义了 turlesim 世界的命名空间。独特的命名空间允许系统启动两个相似的节点,而不会出现节点名称或主题名称冲突。

namespace='turtlesim2',

但是,如果启动文件包含大量节点,则为每个节点定义命名空间可能会变得乏味。为了解决这个问题,PushRosNamespace可以使用该操作为每个启动文件描述定义全局命名空间。每个嵌套节点都会自动继承该名称空间。

为此,首先,我们需要从文件namespace='turtlesim2’中删除该行turtlesim_world_2.launch.py。之后,我们需要更新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,
      ]
   )

最后,我们将语句中的turtlesim_world_2to替换掉。因此,启动描述中的每个节点都将有一个命名空间。turtlesim_world_2_with_namespacereturn LaunchDescriptionturtlesim_world_2.launch.pyturtlesim2

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发出命令以跟随另一个。该节点旨在接收主题上的目标姿势/input/pose。在我们的例子中,我们想要从/turtle2/pose主题重新映射目标姿势。最后,我们将/output/cmd_vel主题重新映射到/turtlesim2/turtle1/cmd_vel。我们的模拟世界turtle1中的这种方式将遵循我们最初的turtlesim世界。turtlesim2turtle2

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

此启动文件将使用包中定义的配置文件启动 RViz turtle_tf2_py。此 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'],
      ),
   ])

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

9.运行启动文件

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

9.2 构建并运行

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

ros2 launch launch_tutorial launch_turtlesim.launch.py

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

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

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

ros2 run turtlesim turtle_teleop_key

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

在这里插入图片描述

除此之外,RViz 应该已经开始了。它将显示相对于world原点位于左下角的框架的所有海龟框架。

在这里插入图片描述