ROS专题 · 2024年3月11日 0

ROS 2基础概念#10:启动系统(Launch)| ROS 2学习笔记

ROS2的启动系统(launch)是一种用于启动和配置ROS2节点、参数和其他组件的XML格式文件。与ROS1中的launch文件类似,ROS2的启动文件允许用户以可组合和灵活的方式启动多个节点,并为它们设置参数和调整配置。

启动系统简介

ROS 2 中的启动系统负责帮助用户描述其系统的配置,然后按照描述执行它。 系统的配置包括运行哪些程序、在哪里运行它们、传递哪些参数以及特定于 ROS 的约定,这些约定可以通过为每个组件提供不同的配置来轻松地在整个系统中重用组件。 它还负责监视启动的进程的状态,并对这些进程的状态变化进行报告和/或做出反应。

用Python、XML或YAML编写的启动文件可以启动和停止不同的节点以及触发和操作各种事件。 有关不同格式的说明,请参阅对使用 Python、XML 和 YAML 的 ROS 2 启动文件的描述。 提供这个框架的包是launch_ros。

如下是一个python格式的 ROS2 启动文件的基本结构:

import launch
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='my_package',
            executable='my_node_executable',
            name='my_node',
            output='screen',
            parameters=[{'my_parameter': '123'}]  # 设置节点参数
        )
    ])

if __name__ == '__main__':
    generate_launch_description()

通过ROS2 启动文件,用户可以轻松地组织和管理ROS2节点及其配置,从而实现更复杂的机器人系统的启动和管理。

接下来的内容我们都以python为例。

创建启动文件

1. 环境设置

创建一个目录,用于存储启动文件:

mkdir launch

2. 撰写一个启动文件:

让我们使用 turtlesim 包及其可执行文件来组合一个 ROS 2 启动文件,以 python 为例。在 launch 目录下创建一个名为 turtlesim_mimic_launch.py 的文件,并输入一下内容保存。

from launch import LaunchDescription
from launch_ros.actions import Node

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

3. 检查启动文件

上面的启动文件会启动一个由三个节点组成的系统,全部来自 turtlesim 包。 该系统的目标是启动两个 turtle 模拟窗口,并让一只 turtle 模仿另一只 turtle 的动作。

启动两个 turtlesim 节点时,它们之间的唯一区别是它们的命名空间值。 唯一的命名空间允许系统启动两个节点,而不会出现节点名称或主题名称冲突。 该系统中的两只 turtle 都接收同一主题的命令并发布它们在同一主题上的姿势。 通过独特的命名空间,可以区分针对不同 turtle 的消息。

最后一个节点也来自 turtlesim 包,但是是一个不同的可执行文件:mimic。 该节点以重新映射的形式添加了配置详细信息。 mimic /input/pose 主题被重新映射到 /turtlesim1/turtle1/pose ,它的 /output/cmd_vel 主题被重新映射到 /turtlesim2/turtle1/cmd_vel 。 这意味着 mimic 将订阅 /turtlesim1/sim 的姿势主题,并重新发布它以供 /turtlesim2/sim 的速度命令主题订阅。 换句话说,turtlesim2 将模仿 turtlesim1 的动作。

# These import statements pull in some Python launch modules
from launch import LaunchDescription
from launch_ros.actions import Node

# Next, the launch description itself begins:
def generate_launch_description():
   return LaunchDescription([

   ])

# The first two actions in the launch description launch the two turtlesim windows:
Node(
    package='turtlesim',
    namespace='turtlesim1',
    executable='turtlesim_node',
    name='sim'
),
Node(
    package='turtlesim',
    namespace='turtlesim2',
    executable='turtlesim_node',
    name='sim'
),

# The final action launches the mimic node with the remaps:
Node(
    package='turtlesim',
    executable='mimic',
    name='mimic',
    remappings=[
      ('/input/pose', '/turtlesim1/turtle1/pose'),
      ('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
    ]
)

4. 通过 ros2 launch 启动

要运行上面创建的启动文件,请进入之前创建的 launch 目录并运行以下命令:

cd launch
ros2 launch turtlesim_mimic_launch.py

将打开两个turtlesim窗口,您将看到以下 [INFO] 消息,告诉您启动文件已启动哪些节点:

[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [turtlesim_node-1]: process started with pid [11714]
[INFO] [turtlesim_node-2]: process started with pid [11715]
[INFO] [mimic-3]: process started with pid [11716]

要查看系统的运行情况,请打开一个新终端并在 /turtlesim1/turtle1/cmd_vel 主题上运行 ros2 topic pub 命令以使第一只 turtle 移动:

ros2 topic pub -r 1 /turtlesim1/turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: -1.8}}"

您会看到两只 turtle 走相同的路径。

5. 使用 rqt_graph

当系统仍在运行时,打开一个新终端并运行 rqt_graph 以更好地了解启动文件中节点之间的关系。

运行命令:

rqt_graph

隐藏的节点(您运行的 ros2 topic pub 命令)正在将数据发布到左侧的 /turtlesim1/turtle1/cmd_vel 主题,/turtlesim1/sim 节点已订阅该主题。 该图的其余部分显示了前面描述的内容:mimic 订阅了 /turtlesim1/sim 的姿势主题,并发布到 /turtlesim2/sim 的速度命令主题。

把启动文件集成到ROS 2软件包里

1. 创建一个ROS 2软件包

运行如下的命令,为新软件包创建一个 workspace

mkdir -p launch_ws/src
cd launch_ws/src
ros2 pkg create py_launch_example --build-type ament_python

2. 创建保存启动文件的结构

按照惯例,包的所有启动文件都存储在包内的启动目录中。 确保在上面创建的包的顶层创建一个启动目录。

对于 Python 包,包含的目录应如下所示:

src/
  py_launch_example/
    launch/
    package.xml
    py_launch_example/
    resource/
    setup.cfg
    setup.py
    test/

为了让 colcon 找到启动文件,我们需要使用 setup data_files 参数告知 Python 的安装工具我们的启动文件。

在我们的 setup.py 文件中:

import os
from glob import glob
from setuptools import setup

package_name = 'py_launch_example'

setup(
    # Other parameters ...
    data_files=[
        # ... Other data files
        # Include all launch files.
        (os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*launch.[pxy][yma]*')))
    ]
)

3. 撰写启动文件

在启动目录中,创建一个名为 my_script_launch.py 的新启动文件。 建议使用 _launch.py 作为 Python 启动文件的文件后缀,但不是必需的。 但是,启动文件名需要以 launch.py 结尾才能被 ros2 launch 识别并自动完成。

您的启动文件应定义 generate_launch_description() 函数,该函数返回由 ros2 启动动词使用的launch.LaunchDescription()

import launch
import launch_ros.actions

def generate_launch_description():
    return launch.LaunchDescription([
        launch_ros.actions.Node(
            package='demo_nodes_cpp',
            executable='talker',
            name='talker'),
  ])

4. 构建并运行启动文件

转到 workspace 的顶层目录,并构建此启动文件:

colcon build

colcon 构建成功并且您 source 了此 workspace 之后,您应该能够运行启动文件,如下所示:

ros2 launch py_launch_example my_script_launch.py

使用替换(substitution)

启动文件用于启动节点、服务和执行进程。 这组操作可能有参数,这会影响他们的行为。 可以在参数中使用替换,以便在描述可重用启动文件时提供更大的灵活性。 替换是仅在执行启动描述期间评估的变量,可用于获取特定信息(例如启动配置、环境变量)或评估任意 Python 表达式。

本章节展示了 ROS 2 启动文件中替换的使用示例,并使用到 turtlesim 软件包。

1. 创建并安装软件包

创建一个新的 build_type ament_python 包,名为 launch_tutorial

ros2 pkg create launch_tutorial --build-type ament_python

然后,确保添加对包的 setup.py 的更改,以便安装启动文件:

import os
from glob import glob
from setuptools import setup

package_name = 'launch_tutorial'

setup(
    # Other parameters ...
    data_files=[
        # ... Other data files
        # Include all launch files.
        (os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*launch.[pxy][yma]*')))
    ]
)

2. 父启动文件

我们创建一个启动文件,它将调用另一个启动文件并将参数传递给另一个启动文件。 为此,请在 launch_tutorial 包的启动文件夹中创建 example_main.launch.py 文件。

from launch_ros.substitutions import FindPackageShare

from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import PathJoinSubstitution, TextSubstitution


def generate_launch_description():
    colors = {
        'background_r': '200'
    }

    return LaunchDescription([
        IncludeLaunchDescription(
            PythonLaunchDescriptionSource([
                PathJoinSubstitution([
                    FindPackageShare('launch_tutorial'),
                    'launch',
                    'example_substitutions.launch.py'
                ])
            ]),
            launch_arguments={
                'turtlesim_ns': 'turtlesim2',
                'use_provided_red': 'True',
                'new_background_r': TextSubstitution(text=str(colors['background_r']))
            }.items()
        )
    ])

example_main.launch.py 文件中,FindPackageShare 替换用于查找 launch_tutorial 包的路径。 然后使用 PathJoinSubstitution 替换将该包路径的路径与 example_substitutions.launch.py 文件名连接起来。

PathJoinSubstitution([
    FindPackageShare('launch_tutorial'),
    'launch',
    'example_substitutions.launch.py'
])

带有 turtlesim_ns use_provided_red 参数的 launch_arguments 字典被传递到 IncludeLaunchDescription 操作。 TextSubstitution 替换用于使用颜色字典中的 background_r 键的值来定义 new_background_r 参数。

launch_arguments={
    'turtlesim_ns': 'turtlesim2',
    'use_provided_red': 'True',
    'new_background_r': TextSubstitution(text=str(colors['background_r']))
}.items()

3. 使用替换的启动文件示例

现在在同一文件夹中创建一个 example_substitutions.launch.py 文件。

from launch_ros.actions import Node

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, ExecuteProcess, TimerAction
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration, PythonExpression


def generate_launch_description():
    turtlesim_ns = LaunchConfiguration('turtlesim_ns')
    use_provided_red = LaunchConfiguration('use_provided_red')
    new_background_r = LaunchConfiguration('new_background_r')

    turtlesim_ns_launch_arg = DeclareLaunchArgument(
        'turtlesim_ns',
        default_value='turtlesim1'
    )
    use_provided_red_launch_arg = DeclareLaunchArgument(
        'use_provided_red',
        default_value='False'
    )
    new_background_r_launch_arg = DeclareLaunchArgument(
        'new_background_r',
        default_value='200'
    )

    turtlesim_node = Node(
        package='turtlesim',
        namespace=turtlesim_ns,
        executable='turtlesim_node',
        name='sim'
    )
    spawn_turtle = ExecuteProcess(
        cmd=[[
            'ros2 service call ',
            turtlesim_ns,
            '/spawn ',
            'turtlesim/srv/Spawn ',
            '"{x: 2, y: 2, theta: 0.2}"'
        ]],
        shell=True
    )
    change_background_r = ExecuteProcess(
        cmd=[[
            'ros2 param set ',
            turtlesim_ns,
            '/sim background_r ',
            '120'
        ]],
        shell=True
    )
    change_background_r_conditioned = ExecuteProcess(
        condition=IfCondition(
            PythonExpression([
                new_background_r,
                ' == 200',
                ' and ',
                use_provided_red
            ])
        ),
        cmd=[[
            'ros2 param set ',
            turtlesim_ns,
            '/sim background_r ',
            new_background_r
        ]],
        shell=True
    )

    return LaunchDescription([
        turtlesim_ns_launch_arg,
        use_provided_red_launch_arg,
        new_background_r_launch_arg,
        turtlesim_node,
        spawn_turtle,
        change_background_r,
        TimerAction(
            period=2.0,
            actions=[change_background_r_conditioned],
        )
    ])

example_substitutions.launch.py 文件中,定义了turtlesim_nsuse_provided_rednew_background_r 启动配置。 它们用于将启动参数的值存储在上述变量中,并将它们传递给所需的操作。 这些 LaunchConfiguration 替换允许我们获取启动描述的任何部分中的启动参数的值。

DeclareLaunchArgument 用于定义可以从上述启动文件或控制台传递的启动参数。

turtlesim_ns = LaunchConfiguration('turtlesim_ns')
use_provided_red = LaunchConfiguration('use_provided_red')
new_background_r = LaunchConfiguration('new_background_r')

turtlesim_ns_launch_arg = DeclareLaunchArgument(
    'turtlesim_ns',
    default_value='turtlesim1'
)
use_provided_red_launch_arg = DeclareLaunchArgument(
    'use_provided_red',
    default_value='False'
)
new_background_r_launch_arg = DeclareLaunchArgument(
    'new_background_r',
    default_value='200'
)

如下的代码定义了一个名为 turtlesim_node 的节点。此节点的软件包名为 turtlesim ,命名空间设置为turtlesim_ns ,并且有 LaunchConfiguration 替换。

turtlesim_node = Node(
    package='turtlesim',
    namespace=turtlesim_ns,
    executable='turtlesim_node',
    name='sim'
)

然后,使用相应的 cmd 参数定义名为 spawn_turtleExecuteProcess 操作。 该命令调用 turtlesim 节点的 spawn 服务。

此外,LaunchConfiguration 替换用于获取 turtlesim_ns 启动参数的值以构造命令字符串。

spawn_turtle = ExecuteProcess(
    cmd=[[
        'ros2 service call ',
        turtlesim_ns,
        '/spawn ',
        'turtlesim/srv/Spawn ',
        '"{x: 2, y: 2, theta: 0.2}"'
    ]],
    shell=True
)

相同的方法用于更改 turtlesim 背景的红色参数的 change_background_r change_background_r_condition 操作。 不同之处在于,仅当提供的 new_background_r 参数等于 200 并且 use_provided_red 启动参数设置为 True 时,才会执行 change_background_r_condition 操作。 IfCondition 内的评估是使用 PythonExpression 替换完成的。

change_background_r = ExecuteProcess(
    cmd=[[
        'ros2 param set ',
        turtlesim_ns,
        '/sim background_r ',
        '120'
    ]],
    shell=True
)
change_background_r_conditioned = ExecuteProcess(
    condition=IfCondition(
        PythonExpression([
            new_background_r,
            ' == 200',
            ' and ',
            use_provided_red
        ])
    ),
    cmd=[[
        'ros2 param set ',
        turtlesim_ns,
        '/sim background_r ',
        new_background_r
    ]],
    shell=True
)

4. 构建软件包

进入 workspace 的根目录,运行如下命令来构建软件包:

colcon build

构建完成后请记得 source 这个 workspace

5. 启动示例

现在您可以使用 ros2 launch 命令启动 example_main.launch.py 文件。

ros2 launch launch_tutorial example_main.launch.py

这将执行以下操作:

  • 启动一个蓝色背景的 turtlesim 节点
  • 生成第二个 turtle
  • 将颜色更改为紫色
  • 如果提供的 background_r 参数为 200 并且 use_provided_red 参数为 True,则两秒后将颜色更改为粉红色

6. 更改启动参数

如果要更改提供的启动参数,可以在 example_main.launch.pylaunch_arguments 字典中更新它们,或者使用首选参数启动 example_substitutions.launch.py。 要查看可能提供给启动文件的参数,请运行以下命令:

ros2 launch launch_tutorial example_substitutions.launch.py --show-args

这将显示可能提供给启动文件的参数及其默认值。

Arguments (pass arguments as '<name>:=<value>'):

    'turtlesim_ns':
        no description given
        (default: 'turtlesim1')

    'use_provided_red':
        no description given
        (default: 'False')

    'new_background_r':
        no description given
        (default: '200')

现在您可以将所需的参数传递给启动文件,如下所示:

ros2 launch launch_tutorial example_substitutions.launch.py turtlesim_ns:='turtlesim3' use_provided_red:='True' new_background_r:=200

使用事件处理程序

ROS 2 中的 Launch 是一个执行和管理用户定义流程的系统。 它负责监视其启动的进程的状态,以及报告这些进程状态的变化并做出反应。 这些更改称为事件,可以通过向启动系统注册事件处理程序来处理。 事件处理程序可以针对特定事件进行注册,并且对于监视进程的状态很有用。 此外,它们还可用于定义一组复杂的规则,这些规则可用于动态修改启动文件。

本章节展示了 ROS 2 启动文件中事件处理程序的使用示例。

1. 事件处理程序示例启动文件

launch_tutorial 包的启动文件夹中创建一个名为 example_event_handlers.launch.py 的新文件。

from launch_ros.actions import Node

from launch import LaunchDescription
from launch.actions import (DeclareLaunchArgument, EmitEvent, ExecuteProcess,
                            LogInfo, RegisterEventHandler, TimerAction)
from launch.conditions import IfCondition
from launch.event_handlers import (OnExecutionComplete, OnProcessExit,
                                OnProcessIO, OnProcessStart, OnShutdown)
from launch.events import Shutdown
from launch.substitutions import (EnvironmentVariable, FindExecutable,
                                LaunchConfiguration, LocalSubstitution,
                                PythonExpression)


def generate_launch_description():
    turtlesim_ns = LaunchConfiguration('turtlesim_ns')
    use_provided_red = LaunchConfiguration('use_provided_red')
    new_background_r = LaunchConfiguration('new_background_r')

    turtlesim_ns_launch_arg = DeclareLaunchArgument(
        'turtlesim_ns',
        default_value='turtlesim1'
    )
    use_provided_red_launch_arg = DeclareLaunchArgument(
        'use_provided_red',
        default_value='False'
    )
    new_background_r_launch_arg = DeclareLaunchArgument(
        'new_background_r',
        default_value='200'
    )

    turtlesim_node = Node(
        package='turtlesim',
        namespace=turtlesim_ns,
        executable='turtlesim_node',
        name='sim'
    )
    spawn_turtle = ExecuteProcess(
        cmd=[[
            FindExecutable(name='ros2'),
            ' service call ',
            turtlesim_ns,
            '/spawn ',
            'turtlesim/srv/Spawn ',
            '"{x: 2, y: 2, theta: 0.2}"'
        ]],
        shell=True
    )
    change_background_r = ExecuteProcess(
        cmd=[[
            FindExecutable(name='ros2'),
            ' param set ',
            turtlesim_ns,
            '/sim background_r ',
            '120'
        ]],
        shell=True
    )
    change_background_r_conditioned = ExecuteProcess(
        condition=IfCondition(
            PythonExpression([
                new_background_r,
                ' == 200',
                ' and ',
                use_provided_red
            ])
        ),
        cmd=[[
            FindExecutable(name='ros2'),
            ' param set ',
            turtlesim_ns,
            '/sim background_r ',
            new_background_r
        ]],
        shell=True
    )

    return LaunchDescription([
        turtlesim_ns_launch_arg,
        use_provided_red_launch_arg,
        new_background_r_launch_arg,
        turtlesim_node,
        RegisterEventHandler(
            OnProcessStart(
                target_action=turtlesim_node,
                on_start=[
                    LogInfo(msg='Turtlesim started, spawning turtle'),
                    spawn_turtle
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessIO(
                target_action=spawn_turtle,
                on_stdout=lambda event: LogInfo(
                    msg='Spawn request says "{}"'.format(
                        event.text.decode().strip())
                )
            )
        ),
        RegisterEventHandler(
            OnExecutionComplete(
                target_action=spawn_turtle,
                on_completion=[
                    LogInfo(msg='Spawn finished'),
                    change_background_r,
                    TimerAction(
                        period=2.0,
                        actions=[change_background_r_conditioned],
                    )
                ]
            )
        ),
        RegisterEventHandler(
            OnProcessExit(
                target_action=turtlesim_node,
                on_exit=[
                    LogInfo(msg=(EnvironmentVariable(name='USER'),
                            ' closed the turtlesim window')),
                    EmitEvent(event=Shutdown(
                        reason='Window closed'))
                ]
            )
        ),
        RegisterEventHandler(
            OnShutdown(
                on_shutdown=[LogInfo(
                    msg=['Launch was asked to shutdown: ',
                        LocalSubstitution('event.reason')]
                )]
            )
        ),
    ])

OnProcessStartOnProcessIOOnExecutionCompleteOnProcessExit OnShutdown 事件的 RegisterEventHandler 操作在启动描述中定义。

OnProcessStart 事件处理程序用于注册在 turtlesim 节点启动时执行的回调函数。 当 turtlesim 节点启动时,它会向控制台记录一条消息并执行 spawn_turtle 操作。

RegisterEventHandler(
    OnProcessStart(
        target_action=turtlesim_node,
        on_start=[
            LogInfo(msg='Turtlesim started, spawning turtle'),
            spawn_turtle
        ]
    )
),

OnProcessIO 事件处理程序用于注册当 spawn_turtle 操作写入其标准输出时执行的回调函数。 它记录生成请求的结果。

RegisterEventHandler(
    OnProcessIO(
        target_action=spawn_turtle,
        on_stdout=lambda event: LogInfo(
            msg='Spawn request says "{}"'.format(
                event.text.decode().strip())
        )
    )
),

OnExecutionComplete 事件处理程序用于注册在 spawn_turtle 操作完成时执行的回调函数。 它会向控制台记录一条消息,并在生成操作完成时执行 change_background_r change_background_r_conditioned 操作。

RegisterEventHandler(
    OnExecutionComplete(
        target_action=spawn_turtle,
        on_completion=[
            LogInfo(msg='Spawn finished'),
            change_background_r,
            TimerAction(
                period=2.0,
                actions=[change_background_r_conditioned],
            )
        ]
    )
),

OnProcessExit 事件处理程序用于注册当 turtlesim 节点退出时执行的回调函数。 它将一条消息记录到控制台并执行 EmitEvent 操作,以便在 turtlesim 节点退出时发出 Shutdown 事件。 这意味着当 turtlesim窗口关闭时,启动进程将关闭。

RegisterEventHandler(
    OnProcessExit(
        target_action=turtlesim_node,
        on_exit=[
            LogInfo(msg=(EnvironmentVariable(name='USER'),
                    ' closed the turtlesim window')),
            EmitEvent(event=Shutdown(
                reason='Window closed'))
        ]
    )
),

最后,OnShutdown 事件处理程序用于注册一个回调函数,该函数在启动文件被要求关闭时执行。 它会向控制台记录一条消息,说明为什么要求关闭启动文件。 它会记录带有关闭原因的消息,例如关闭 turtlesim窗口或用户发出的 ctrl-c 信号。

RegisterEventHandler(
    OnShutdown(
        on_shutdown=[LogInfo(
            msg=['Launch was asked to shutdown: ',
                LocalSubstitution('event.reason')]
        )]
    )
),

2. 构建软件包

进入 workspace 的根目录,运行如下命令来构建包:

colcon build

构建完成后记得 source workspace

3. 启动示例

现在您可以使用 ros2 launch 命令启动 example_event_handlers.launch.py 文件。

ros2 launch launch_tutorial example_event_handlers.launch.py turtlesim_ns:='turtlesim3' use_provided_red:='True' new_background_r:=200

这将执行以下操作:

  • 启动一个蓝色背景的 turtlesim 节点
  • 生成第二只 turtle
  • 将颜色更改为紫色
  • 如果提供的 background_r 参数为 200 并且 use_provided_red 参数为 True,则两秒后将颜色更改为粉红色
  • turtlesim 窗口关闭时关闭启动文件

此外,它会在以下情况下将消息记录到控制台:

  • Turtlesim 节点启动
  • 执行 spawn 动作
  • 执行 change_background_r 操作
  • 执行 change_background_r_condition 操作
  • turtlesim 节点退出
  • 启动进程被要求关闭。

管理大型项目

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

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

1. 编写启动文件

1.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 节点。

1.2 在启动文件中设置参数

我们将首先编写一个启动文件来启动我们的第一个 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 模拟。

1.3 从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 文件中定义实参和参数可以轻松存储和加载大量变量。 此外,还可以轻松地从当前 ros2 参数列表中导出 YAML 文件。

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

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

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

1.4 在 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_bbackground_g和background_r 参数分配给 turtlesim3/simturtlesim2/sim 节点中的指定值。

1.5 命名空间

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

1.6 重用节点

现在创建一个名为 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 参数。

1.7 覆盖参数

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

1.8 重新映射

创建一个名为 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'),
         ]
      )
   ])

该启动文件将启动模拟节点,该节点将命令一个 turtlesim 跟随另一个 turtlesim 。 该节点旨在接收主题 /input/pose 上的目标姿势。 在我们的例子中,我们想要从 /turtle2/pose 主题重新映射目标姿势。 最后,我们将 /output/cmd_vel 主题重新映射到 /turtlesim2/turtle1/cmd_vel。 这样,我们的 turtlesim2 模拟世界中的 turtle1 将跟随我们最初的 turtlesim 世界中的 turtle2

1.9 配置文件

创建一个名为 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

1.10 环境变量

现在让我们在包中创建最后一个名为 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'],
      ),
   ])

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

2. 运行启动文件

2.1 更新 setup.py

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

2.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

您将看到类似下图的结果:

另外,RViz 应该已经开始运行了。 它将显示相对于世界坐标系的所有海龟坐标系,其原点位于左下角。