ROS2中launch文件编写——基础篇

一、launch文件存在的意义

在学习古月居ROS2入门教程时,启动一个小海龟仿真示例程序,需要启动多个节点(小海龟、键盘控制等),每次都需要打开一个新的终端,然后使用下面这种方式来启动节点:

ros2 run package-name  execute-name

在小海龟仿真示例中,我们运行了以下两个命令:

ros2 run turtlesim turtlesim_node
ros2 run turtlesim turtle_teleop_key

如果我们需要启动更多的节点,就需要打开非常多的窗口,非常的不优雅。此时就轮到launch文件发挥作用啦:

  • Launch启动文件可以一次性启动所有的节点;
  • 它是ROS系统中多节点启动与配置的一种脚本。
  • ROS2官方推荐的是python方式编写launch文件。

二、如何编写第一个launch文件

1. 基本框架

以下代码片段是每个launch启动文件所需的基本框架:

from launch import LaunchDescription
 
def generate_launch_description():
    return LaunchDescription([
        # add your actions here...
    ])
  • 每个启动文件都有一个generate_launch_description函数,返回LaunchDescription对象。

2. 节点填充

为了使launch启动文件执行特定操作,我们需要向LaunchDescription对象执行节点填充操作。
以小海龟仿真为例,需要向其中添加turtlesim_node小海龟仿真器节点和turtle_teleop_key键盘控制节点,完整的launch文件如下:

这块,我是直接在古月居的ros2_21_tutorials/learning_launch/launch下面新建了simple_turtlesim.launch.py

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import ExecuteProcess
 
 
def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='sim'
        ),
        ExecuteProcess(
            cmd=['xterm', '-e', 'ros2', 'run', 'turtlesim', 'turtle_teleop_key'],
            name='teleop_key',
        ),
    ])
  • Node用于在启动文件中定义一个节点。它的参数包括:

    • package:这是一个字符串,表示节点所在的ROS包的名称。
    • executable:这是一个字符串,表示在给定包中的可执行文件的名称。
    • name:这是一个可选的字符串,用于指定节点的名称。如果未指定,将使用可执行文件的名称。
    • namespace:这是一个可选的字符串,用于指定节点的命名空间。命名空间是一种组织节点的方式,可以帮助避免节点名称的冲突。
    • output:这是一个可选的字符串,用于指定节点的输出应该如何处理。例如,可以将其设置为"screen",以便将节点的输出打印到屏幕上。
    • parameters:这是一个可选的列表,用于指定节点的参数。每个参数都是一个字典,其中包含参数的名称和值。
    • remappings:这是一个可选的列表,用于指定主题的重新映射。每个重新映射都是一个元组,其中包含原始主题的名称和新主题的名称。
    • arguments:这是一个可选的列表,用于指定传递给可执行文件的命令行参数。
  • ExecuteProcess用于在launch文件中启动一个进程。

    • 其中最重要的参数是cmd,它是一个包含要执行的命令及其参数的列表。例如,cmd=['ls', '-l']将执行ls -l命令。
    • 在上面的launch示例文件中,需要在你的系统中安装xterm才能使用这个功能。
      sudo apt-get update
      sudo apt-get install xterm
      
    • 为什么没有将turtle_teleop_keyNode的方式进行启动?我查阅了很多资料问答帖,大概问题是,launch启动文件无法从终端获取用户输入,所以使用ExecuteProcess替代,这将会自动运行一个xterm终端模拟器来获取键盘输入。

3. 节点填充的另一种方式(add_action)

创建启动文件的另一种方式是创建一个 LaunchDescription 对象ld,然后通过add_action方法填充节点:

from launch import LaunchDescription
from launch_ros.actions import Node
from launch.actions import ExecuteProcess
 
 
def generate_launch_description():
    ld = LaunchDescription()
    
    turtlesim_node = Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='sim'
        )
    
    teleop_key = ExecuteProcess(
            cmd=['xterm', '-e', 'ros2', 'run', 'turtlesim', 'turtle_teleop_key'],
            name='teleop_key',
        )
        
    ld.add_action(turtlesim_node)
    ld.add_action(teleop_key)
    return ld

4. 编译测试

  • 添加launch启动文件和配置文件(三里面会讲到),这块我是直接在配置好的古月居的ros2_21_tutorials/learning_launch/setup.py里面修改的,这里面直接用了*通配符,所以不需要进行修改。

    cd ~/dev_ws/src/ros2_21_tutorials/learning_launch/
    vim setup.py
    
    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'))),
       ],
    
  • 编译

    cd ~/dev_ws/
    colcon build
    source install/local_setup.bash
    
  • 启动launch文件

    ros2 launch learning_launch simple_turtlesim.launch.py
    

三、使用ROS2启动文件管理大型项目实践

翻译自:https://docs.ros.org/en/foxy/Tutorials/Intermediate/Launch/Using-ROS2-Launch-For-Large-Projects.html#id3

机器人上的大型应用通常涉及多个互连的节点,每个节点可以有许多参数。海龟模拟器中模拟多只海龟就是一个很好的例子。海龟模拟由多个海龟节点、世界配置以及 TF 广播器和监听器节点组成。在所有节点之间,存在大量影响这些节点的行为和外观的 ROS 参数。ROS2启动文件允许我们在一个地方启动所有节点并设置相应的参数。在教程结束时,运行launch_turtlesim.launch.py启动文件将调出不同的节点,负责模拟两个turtlesim 模拟、启动 TF 广播器和监听器、加载参数以及启动 RViz 配置。

1. 顶层启动文件

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

首先,我们将创建一个顶层启动文件,该文件将调用单独的启动文件。此处我还是在古月居的ros2_21_tutorials/learning_launch/launch下面新建一个名为launch_turtlesim.launch.py的launch文件。

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('learning_launch'), 'launch'),
         '/turtlesim_world_1.launch.py'])
      )
   turtlesim_world_2 = IncludeLaunchDescription(
      PythonLaunchDescriptionSource([os.path.join(
         get_package_share_directory('learning_launch'), 'launch'),
         '/turtlesim_world_2.launch.py'])
      )
   broadcaster_listener_nodes = IncludeLaunchDescription(
      PythonLaunchDescriptionSource([os.path.join(
         get_package_share_directory('learning_launch'), 'launch'),
         '/broadcaster_listener.launch.py']),
      launch_arguments={
    
    'target_frame': 'carrot1'}.items(),
      )
   mimic_node = IncludeLaunchDescription(
      PythonLaunchDescriptionSource([os.path.join(
         get_package_share_directory('learning_launch'), 'launch'),
         '/mimic.launch.py'])
      )
   fixed_frame_node = IncludeLaunchDescription(
      PythonLaunchDescriptionSource([os.path.join(
         get_package_share_directory('learning_launch'), 'launch'),
         '/fixed_broadcaster.launch.py'])
      )
   rviz_node = IncludeLaunchDescription(
      PythonLaunchDescriptionSource([os.path.join(
         get_package_share_directory('learning_launch'), 'launch'),
         '/turtlesim_rviz.launch.py'])
      )

   return LaunchDescription([
      turtlesim_world_1,
      turtlesim_world_2,
      broadcaster_listener_nodes,
      mimic_node,
      fixed_frame_node,
      rviz_node
   ])
  • 该启动文件包括一组其他的launch启动文件。启动了两个turtlesim模拟世界,TF broadcaster, TF listener, mimic, fixed frame broadcaster, and RViz nodes

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_node节点。

2.2 从YAML文件加载参数

在第二个模拟器launch启动文件中,我们将使用不同的参数配置方式。现在创建一个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('learning_launch'),
      'config',
      'turtlesim.yaml'
      )

   return LaunchDescription([
      Node(
         package='turtlesim',
         executable='turtlesim_node',
         namespace='turtlesim2',
         name='sim',
         parameters=[config]
      )
   ])
  • 此启动文件使用从 YAML 配置文件中加载的参数启动turtlesim_node节点。

  • 在 YAML 文件中定义ParameterArgument,可以轻松存储和加载大量变量。

    • Parameter是形参,是函数声明时写的参数;而Argument是实参,是调用函数时传递的参数值。
  • 此外,还可以轻松地从当前列表导出 YAML 文件。要了解如何执行此操作,请参阅了解参数教程

  • 现在在文件夹ros2_21_tutorials/learning_launch/config中创建一个配置文件turtlesim.yaml,它将由上述launch启动文件加载。

    /turtlesim2/sim:
       ros__parameters:
          background_b: 255
          background_g: 86
          background_r: 150
    
  • 此时启动turtlesim_world_2.launch.py文件,将以预先配置的背景颜色启动turtlesim_node

2.3 在YAML文件中使用通配符

有时我们想要在多个节点中设置相同的参数。这些节点具有不同的命名空间或名称,但仍然具有相同的参数。此时创建多个YAML文件显得十分低效,解决方案是使用通配符(其充当文本中未知字符的替换)将参数应用于多个不同的节点。

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

3. 命名空间

我们在turtlesim_world_2.launch.py文件中定义了 turlesim 世界模拟器的命名空间。独一无二的命名空间允许系统启动两个相同的节点,而不会出现节点名称或主题名称冲突。可以看到我们在turtlesim_world_1.launch.pyturtlesim_world_2.launch.py中的都使用了name='sim',就是因为命名空间的差异,才不会出现冲突。

但是,如果启动文件包含大量节点,则为每个节点定义命名空间同样十分低效,也不太方便管理。为了解决这个问题,可以使用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('learning_launch'), 'launch'),
         '/turtlesim_world_2.launch.py'])
      )
   turtlesim_world_2_with_namespace = GroupAction(
     actions=[
         PushRosNamespace('turtlesim2'),
         turtlesim_world_2,
      ]
   )

最后,将LaunchDescription语句中的turtlesim_world_2替换为turtlesim_world_2_with_namespacereturn。As a result, each node in the turtlesim_world_2.launch.py launch description will have a turtlesim2 namespace。

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_listener节点;
  • 之后,在启动期间,turtle_tf2_broadcaster节点被使用不同的名称和参数两次调用。

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(),
   )
  • 此语法将target_frame默认参数更改为carrot1

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发出命令以跟随另一个。
  • The node is designed to receive the target pose on the topic /input/pose. In our case, we want to remap the target pose from /turtle2/pose topic. Finally, we remap the /output/cmd_vel topic to /turtlesim2/turtle1/cmd_vel. This way turtle1 in our turtlesim2 simulation world will follow turtle2 in our initial turtlesim world.

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
  • This RViz configuration will set the world frame, enable TF visualization, and start RViz with a top-down view.

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. 运行启动文件

  • 按照《二、4. 编译测试》中的内容进行编译,并运行如下启动命令:

    ros2 launch learning_launch launch_turtlesim.launch.py
    
  • 现在将看到两个turtlesim 模拟已启动。第一个有两只乌龟,第二个有一只乌龟。Its aim is to reach the carrot1 frame which is five meters away on the x-axis relative to the turtle1 frame.

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

    ros2 run turtlesim turtle_teleop_key
    
  • 结果,你会看到类似的图片:
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/xijuezhu8128/article/details/131818608