# ROS2基础教程 - 10 Launch

在 ROS 中,launch 系统用于启动和管理多个节点及其配置。

例如我们启动小海龟程序,要开启两个终端,在后面我们的程序可能要启动更多的节点,使用 ros2 run 命令,会非常的麻烦,使用 launch 我们可以同时启动多个节点,并进行一些参数的配置。

和 ROS1 不同, ROS2 中 launch 文件就是一个 XML 文件,在 ROS2 中,launch 文件可以使用 XML、YAML 或 Python 脚本,但是推荐使用 Python 脚本(通常是 .py 文件)作为启动文件。这些文件定义了要启动的节点、参数、条件等。

# 10.1 使用launch启动多个节点

# 1 创建功能包

我们可以在任意功能包下创建 launch 文件,为了方便演示,我这里单独创建一个功能包:

ros2 pkg create --build-type ament_python launch_python
1

launch 文件需要放在功能包下的 launch 目录下,如果没有该目录,则需要先创建。

# 2 编写launch文件

在 ROS2 中有一个发布订阅的demo,使用topic通信,可以分别使用如下的命令来分别启动发布者和订阅者:

# 启动发布者
ros2 run demo_nodes_py talker

# 启动订阅者
ros2 run demo_nodes_py listener
1
2
3
4
5

我们可以使用 launch 同时启动上面两个节点。


首先需要编写 launch 文件,定义需要启动的节点、参数、条件等。

在功能包的 launch 目录下创建一个 launch 文件了。

例如我创建一个 demo_launch.pylaunch 文件,内容如下:

# demo_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        
        Node(
            package='demo_nodes_py',
            executable='talker',
            output='screen'
        ),
        Node(
            package='demo_nodes_py',
            executable='listener',
            output='screen'
        ),
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

在上面的代码中,使用 Node 定义了两个节点,同时启动了发布者和订阅者。

  • 使用LaunchDescription对象定义一系列要执行的启动的节点;

  • package 表示的是功能包的名称;

  • executable 表示的是节点的名称。

  • output 指定节点的日志输出方式,如 screenlog

# 3 配置launch文件(重要)

setup.py新增 launch 相关配置:

from setuptools import find_packages, setup
import os
from glob import glob

package_name = 'launch_python'

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','*.*'))),
        (os.path.join('share', package_name, 'rviz'), glob(os.path.join('rviz', '*.*'))),
    ],
    # ...其他配置
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面新增了三行配置,主要作用是安装 launch、config、rviz 这三个目录中的文件到编译的 install 目录中,这样在运行的时候,能找到这些目录下的文件。(否则运行launch文件找不到

注意,后面使用了通配符进行了文件的匹配,你需要按照规则命令launch文件,我这里launch文件需要以 _launch.py 结尾。

# 4 构建项目

工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成功能包的文件。

# 5 运行launch

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。


然后启动launch:

ros2 launch launch_python demo_launch.py
1

运行后,发现同时启动了发布者和订阅者:

# 10.2 Node的参数

# 1 name参数

为节点指定一个唯一的名称。如果不指定,ROS 2将使用可执行文件的名称作为节点的默认名称。但是,如果多个相同名称的节点被启动,可能会导致冲突。

例如下面启动两个小海龟的窗口:

# turtle_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='turtle_a',
            output='screen'
        ),
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='turtle_b',
            output='screen'
        ),
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

运行 launch 后,使用 ros2 node list 查看节点,可以看到两个节点:

原来如果不通过 name 设置节点名称,直接使用 ros2 run turtlesim turtlesim_node , 节点名称是 turtlesim

# 2 namespace参数

命名空间可以用于将一组相关的节点、主题、服务等资源进行分组,避免名称冲突。例如,在一个复杂的机器人系统中,可能有多个相同类型的传感器,为它们设置不同的命名空间可以方便地区分和管理。

如果有两个相同的激光雷达节点,一个用于前部感知,一个用于后部感知,可以为它们分别设置命名空间。假设前部激光雷达节点的namespace='front_lidar',那么这个节点发布的主题可能将会变成 /front_lidar/scan(假设主题名称是scan),而后部激光雷达节点如果namespace='rear_lidar',那么这个节点发布的主题将会变成 /rear_lidar/scan

举个栗子:

# turtle_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            namespace='turtle',
            name='turtle_a',
            output='screen'
        ),
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            namespace='turtle',
            name='turtle_b',
            output='screen'
        ),
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上面为两个节点设置了命名空间,此时查看节点列表,发现节点前面多了命名空间:

此时查看话题信息,可以看到节点发布和订阅的主题,前面也多了命名空间:

如果单独运行 ros2 run turtlesim turtlesim_node ,是没有命名空间的:

# 3 remappings参数

上面在同一个命名空间下运行了两个小海龟,但是有一个问题的,两个节点订阅和发布的 topic 是相同的,能否让两个节点发布和订阅的主题不同呢?可以使用 remappings 参数。

remappings是用于主题(Topic)和服务(Service)的重命名的机制。比如,一个节点发布的主题名称为/image_raw,而另一个节点期望接收的主题名称为/camera/image。通过remappings,可以将/image_raw重映射为/camera/image,使得两个节点能够正常通信。

举个栗子:

# turtle_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            namespace='turtle',
            name='turtle_a',
            output='screen',
            remappings=[
                ('/turtle/turtle1/cmd_vel', '/turtle/cmd_vel_a'),
                ('/turtle/turtle1/pose', '/turtle/pose_a')
            ]
        ),
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            namespace='turtle',
            name='turtle_b',
            output='screen',
            remappings=[
                ('/turtle/turtle1/cmd_vel', '/turtle/cmd_vel_b'),
                ('/turtle/turtle1/pose', '/turtle/pose_b')
            ]
        ),
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

在上面使用 remappings 分别修改了节点订阅和发布的 topic,此时使用 ros2 topic list 查看topic,显示如下:

# 4 parameters参数

节点会定义一些参数,我们在使用 launch 启动节点的时候,可以传递参数。

举个栗子:

以小海龟为例,可以传递窗口的背景颜色(当然,这些参数是节点中定义的)。

# turtle_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            namespace='turtle',
            name='turtle_a',
            output='screen',
            parameters=[
                {'background_r' : 100},
                {'background_g' : 100},
                {'background_b' : 100},
            ]
        ),
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            namespace='turtle',
            name='turtle_b',
            output='screen',
            parameters=[
                {'background_r' : 255},  
            ]
        ),
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

在上面使用 parameters 传递参数。

但是对于上面的方式,但是是固定写死的,无法在运行 launch 文件的时候,动态的传递参数。如果想在运行 ros2 launch 命令的时候动态的传递参数,我们可以使用 DeclareLaunchArgumentLaunchConfiguration 来定义 ,举个栗子:

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():
    background_r_arg = DeclareLaunchArgument(
        'background_r', default_value='100', description='Red component of the background color'
    )
    background_g_arg = DeclareLaunchArgument(
        'background_g', default_value='100', description='Green component of the background color'
    )
    background_b_arg = DeclareLaunchArgument(
        'background_b', default_value='100', description='Blue component of the background color'
    )

    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        namespace='turtle',
        name='turtle_a',
        output='screen',
        parameters=[{
            'background_r': LaunchConfiguration('background_r'),
            'background_g': LaunchConfiguration('background_g'),
            'background_b': LaunchConfiguration('background_b'),
        }]
    )

    # 声明并使用参数
    return LaunchDescription([background_r_arg, background_g_arg, background_b_arg, node])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

上面 DeclareLaunchArgumentNode 都是 launch 中的 action,所以在使用 LaunchDescription 启动的时候,都需要加载。

那么在运行 launch 文件时动态指定参数值,如:

ros2 launch my_package my_launch_file.launch.py background_r:=200 background_g:=150 background_b:=50
1

上面配置和读取参数的方式有点复杂,如果参数很多,我们可以将参数放在参数文件中尽心配置,然后读取参数文件。

举个栗子:

首先在功能包的 config 目录创建并定义参数文件:

/turtle/turtle_a:  # 对应指定的命名空间和节点名称
  ros__parameters:	# 此部分用于定义节点的参数
    background_r: 255
    background_g: 0
    background_b: 0
1
2
3
4
5

然后加载并使用参数文件,需要指定上面参数文件的路径:

# turtle_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node
import os
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():

    # 得到配置文件的路径
    # 查找launch_python功能包下config目录下的turtle.yaml配置文件
    param_path = os.path.join(
        get_package_share_directory('launch_python'),
        'config',
        'turtle.yaml'
    )

    return LaunchDescription([
        Node(   # 启动rviz
            package='turtlesim',
            executable='turtlesim_node',
            namespace='turtle',
            name='turtle_a',
            output='screen',
            parameters=[param_path]  # 指定配置文件
        )
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 5 arguments参数

arguments参数用于向节点的可执行文件传递命令行参数。

打个比方,如果 turtlesim 节点支持参数,例如 ros2 run turtlesim turtlesim_node -debug -frequency 10.0 ,那么在 launch 文件中配置如下:

# turtle_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            namespace='turtle',
            name='turtle_a',
            output='screen',
            arguments=['-debug', '-frequency 10.0']
        )
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

有些节点的启动可以自定参数,你可以在 launch 文件中使用 arguments 参数进行配置。


例如,后面在启动 rviz 软件的时候,可以通过这个 arguments 参数来指定使用的配置文件。

在 launch 文件中启动 rviz

# turtle_launch.py

from launch import LaunchDescription
from launch_ros.actions import Node
import os
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():

    # 得到配置文件的路径
    # 查找launch_python功能包下rviz目录下的rviz_config.rviz配置文件
    rviz_config_file_path = os.path.join(
        get_package_share_directory('launch_python'),
        'rviz',
        'rviz_config.rviz'
    )

    return LaunchDescription([
        Node(   # 启动rviz
            package='rviz2',
            executable='rviz2',
            name='rviz2',
            output='screen',
            arguments=['-d', rviz_config_file_path]  # 指定配置文件的路径
        )
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

rviz 后面再介绍。

# 10.3 condition

ROS2 的 launch 框架支持在启动节点时使用条件判断,例如,你想满足指定条件才启动指定的节点,

那么你可以使用 launch.conditions 模块中的条件类来控制节点的启动。

launch.conditions 主要提供了两种种条件类型,比如:

  • IfCondition:如果条件为真,则执行该动作。
  • UnlessCondition:如果条件为假,则执行该动作。

# 1 IfCondition

举个栗子:

下面使用 IfCondition 来根据某个参数的值决定是否启动节点。

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch_ros.actions import Node
from launch.conditions import IfCondition
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    # 声明一个布尔参数,用于控制节点的启动
    enable_node_arg = DeclareLaunchArgument('enable_node', default_value='false')
    
    # 使用 IfCondition 判断是否启动节点
    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        namespace='turtle',
        name='turtle_a',
        output='screen',
        condition=IfCondition(LaunchConfiguration('enable_node'))  # 根据参数决定是否启动
    )

    return LaunchDescription([
        enable_node_arg,
        node
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 使用 DeclareLaunchArgument 声明一个名为 enable_node 的参数,默认值为 'false'

  • 在定义节点时,使用 IfCondition 来判断 enable_node 的值。如果这个参数为 true,则启动 turtlesim_node,所以上面不会启动节点。

# 2 UnlessCondition

如果想在某个条件不成立时启动节点,可以使用 UnlessCondition

举个栗子:

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch_ros.actions import Node
from launch.conditions import UnlessCondition
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    # 声明一个布尔参数,用于控制节点的启动
    enable_node_arg = DeclareLaunchArgument('enable_node', default_value='false')
    
    # 使用 IfCondition 判断是否启动节点
    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        namespace='turtle',
        name='turtle_a',
        output='screen',
        condition=UnlessCondition(LaunchConfiguration('enable_node'))  # 根据参数决定是否启动
    )

    return LaunchDescription([
        enable_node_arg,
        node
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在上面的代码中,只有在 enable_nodefalse 时,才会启动节点。

# 10.4 包含其他launch文件

有什么功能很复杂,所有的节点都写在一个 launch 文件中,会很臃肿和难以维护,我们可以将需要启动的节点和配置放在多个 launch 文件中,这样可以将复杂的系统分解为更小、可复用的 launch 文件,提升维护性和可扩展性。

下面介绍一下如何包含其他的 launch 文件,并传递参数。

假设有两个 launch 文件:

  • child_launch.py:定义一个简单的节点和一些参数。
  • main_launch.py:包含 child_launch.py 并传递参数。

# 1 定义子launch文件

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node

def generate_launch_description():
    param_arg = DeclareLaunchArgument(
        'param_value', default_value='100', description='A parameter value for the node'
    )

    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        namespace='turtle',
        name='turtle_b',
        output='screen',
        parameters=[{'background_r': LaunchConfiguration('param_value')}]
    )

    return LaunchDescription([param_arg, node])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

launch 文件和之前定义 parameters 参数一样。

# 2 定义父launch文件

在父 launch 文件中,使用 IncludeLaunchDescription 包含其他的 launch 文件。

from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, DeclareLaunchArgument
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
import os
from ament_index_python.packages import get_package_share_directory
from launch_ros.actions import Node

def generate_launch_description():
    # 获取 child_launch.py 的路径
    child_launch_path = os.path.join(
        get_package_share_directory('launch_python'),  # 指定功能包的名字
        'launch',
        'child_launch.py'
    )

    # 声明一个参数,供 child_launch.py 使用
    param_arg = DeclareLaunchArgument(
        'param_value', default_value='255', description='Parameter to pass to child launch'
    )

    # 包含 child_launch.py 并传递参数
    include_child = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(child_launch_path),
        launch_arguments={'param_value': LaunchConfiguration('param_value')}.items()
    )

    # 可以再创建一个node
    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        namespace='turtle',
        name='turtle_a',
        output='screen',
    )

    return LaunchDescription([param_arg, include_child, node])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

在上面的代码中,定义了参数并传给包含的 launch 文件。

如果要包含多个 launch 文件,需要定义多个 IncludeLaunchDescription

举个栗子:

from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
import os
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
    # 获取子 launch 文件的路径
    launch_a_path = os.path.join(
        get_package_share_directory('launch_python'),  # 指定功能包的名字
        'launch',
        'launch_a.py'
    )
    launch_b_path = os.path.join(
        get_package_share_directory('launch_python'),
        'launch',
        'launch_b.py'
    )

    # 包含 launch_a.py
    include_launch_a = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(launch_a_path),
        launch_arguments={'param1': 'value1'}.items()  # 可选:传递参数
    )

    # 包含 launch_b.py
    include_launch_b = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(launch_b_path),
        launch_arguments={'param2': 'value2'}.items()  # 可选:传递参数
    )

    return LaunchDescription([include_launch_a, include_launch_b])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 10.5 环境变量

我们在实际开发的时候,可以在系统的环境变量中配置一些变量,这样可以在代码中读取,根据不同的值处理不同的逻辑,不用每次修改代码进行重新编译。

在 launch 文件中,可以读取环境变量和修改环境变量。

# 1 读取环境变量

在 launch 文件中,可以使用 EnvironmentVariable 读取环境变量的值。

举个栗子:

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

def generate_launch_description():
    param_value = EnvironmentVariable('MY_ENV_VAR', default_value='255')
    
    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        output='screen',
        parameters=[{'background_r': param_value}]
    )
    
    return LaunchDescription([node])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

EnvironmentVariable('MY_ENV_VAR'):此行代码将读取环境变量 MY_ENV_VAR 的值,并将其作为参数传递给节点。

使用下面的方式也可以读取环境变量的值:

import os
my_env_value = os.getenv('MY_ENV_VAR', 'default_value')
1
2

两者的区别:

  • EnvironmentVariable() :是 ROS 2 launch 文件中用于读取环境变量值的类,适合在 ROS 2 launch 文件中定义和传递参数时使用,能够在 LaunchDescription 中用作参数或条件的一部分。当使用 launch 文件运行时,它会动态读取环境变量的值,这意味着如果环境变量在运行时被更改,这个修改会生效。
  • os.getenv() :是 Python 内置的函数,用于在任何 Python 脚本中读取环境变量,如果你在 launch 文件之外编写 Python 脚本并需要读取环境变量,那么使用这个。os.getenv() 在 Python 脚本启动时会读取环境变量的值。如果环境变量在脚本运行后被更改,该修改不会反映在已经启动的进程中。

# 2 修改环境变量

SetEnvironmentVariablelaunch.actions 模块中的一个类,它可以在 launch 文件中使用,来改变启动上下文中的环境变量。

举个栗子:

from launch import LaunchDescription
from launch.actions import SetEnvironmentVariable
from launch.substitutions import EnvironmentVariable
from launch_ros.actions import Node

def generate_launch_description():
    # 设置环境变量
    set_env_var = SetEnvironmentVariable('MY_ENV_VAR', '100')

    # 读取环境变量
    param_value = EnvironmentVariable('MY_ENV_VAR', default_value='255')

    # 定义节点
    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        output='screen',
        parameters=[{'background_r': param_value}]
    )
    
    return LaunchDescription([
        set_env_var,
        node
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

SetEnvironmentVariableNode 都是 launch 中的 action,所以都需要使用 LaunchDescription 加载,并且在 Node 之前加载,作为启动 Node 必要的环境变量,使节点能够读取这些变量并按照预期运行。

# 10.6 打印日志

在 ROS2 launch 框架中,launch.actions 提供了一组用于定义和控制启动行为的类,这些类被称为“动作” (actions)。每个 actionlaunch 文件中用于执行特定的任务,像前面的声明参数(DeclareLaunchArgument)、设置环境变量(SetEnvironmentVariable)、启动节点(Node)等都是 action

同样,在 launch 中打印日志,也有对应的 action 来处理。

举个栗子:

from launch import LaunchDescription
from launch.actions import LogInfo
from launch.actions import DeclareLaunchArgument
from launch.substitutions import EnvironmentVariable
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    # 打印简单的值
    log_action1 = LogInfo(msg="Hello")
    
    # 打印拼接
    log_action2 = LogInfo(msg=["Hello ", str(123)])  # 拼接字符串和整数
    
    # 打印环境变量
    env_var_value = EnvironmentVariable('MY_ENV_VAR', default_value='foooor')
    log_action3 = LogInfo(msg=["The value of MY_ENV_VAR is: ", env_var_value])
    
    # 打印参数值
    param_arg = DeclareLaunchArgument('my_param', default_value='default_value')
    log_action4 = LogInfo(msg=["The value of my_param is: ", LaunchConfiguration('my_param')])

    return LaunchDescription([
        param_arg,
        log_action1, 
        log_action2, 
        log_action3, 
        log_action4
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在上面的代码中,通过 LogInfo 打印日志,演示了各种打印情况。

launch 文件中,还可以使用以下动作来打印日志:

  • LogInfo:用于打印一般信息。
  • LogWarn:用于打印警告信息。
  • LogError:用于打印错误信息。
  • LogDebug:用于打印调试信息。

# 10.7 GroupAction

GroupAction 可以在 launch 文件中将一组动作组合在一起。它允许你对这些动作应用共享的上下文设置,如命名空间、条件、参数等。

GroupAction 的主要功能和优势:

  1. 组织和分组:将一组相关的 launch 动作(如节点启动、参数声明等)组合在一起。
  2. 共享设置:可以为组内的动作应用相同的设置,比如命名空间、条件等。
  3. 条件执行:可设置条件,只有在满足条件时,组内的动作才会被执行。
  4. 作用域管理:可以在同一作用域内执行多个动作。

# 1 基本用法示例

下面是一个使用 GroupAction 的简单示例,展示如何将多个节点放置在同一命名空间下:

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

def generate_launch_description():
    # 使用 GroupAction 将多个节点放置在同一命名空间中
    group = GroupAction([
        PushRosNamespace('turtle'), 
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='turtle_a',
            output='screen'
        ),
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='turtle_b',
            output='screen'
        )
    ])  # 为组内所有节点设置命名空间

    return LaunchDescription([
        group
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • 在上述示例中,turtle1turtle2 节点都被放置在 turtle 命名空间下。

运行后查看节点信息:

# 2 使用条件执行

GroupAction 还可以用于条件执行,使组内动作只有在特定条件下才会被执行,和之前 Nodecondition 一样。

举个栗子:

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, GroupAction, LogInfo
from launch.conditions import IfCondition
from launch_ros.actions import Node
from launch_ros.actions import PushRosNamespace
from launch.substitutions import LaunchConfiguration

def generate_launch_description():
    # 声明用于控制组执行的参数
    condition_arg = DeclareLaunchArgument('run_group', default_value='true')

    group = GroupAction(
        actions=[
            PushRosNamespace('turtle'),    # 设置命名空间
            Node(
                package='turtlesim',
                executable='turtlesim_node',
                name='turtle_a',
                output='screen'
            ),
            Node(
                package='turtlesim',
                executable='turtlesim_node',
                name='turtle_b',
                output='screen'
            ),
            LogInfo(msg='Launching the turtle node group')    # 打印日志
        ],
        condition=IfCondition(LaunchConfiguration('run_group'))    # 判断条件是否启动
    )

    return LaunchDescription([
        condition_arg, 
        group
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
  • IfCondition:设置条件,只有当 run_group 参数为 true 时,组内的动作才会被执行。
  • LaunchConfiguration:用于获取 launch 文件中的参数值。
  • 同样可以使用 from launch.conditions import UnlessCondition ,和前面介绍 condition 的时候一样,这里就不介绍了。

# 3 嵌套使用GroupAction

还可以嵌套使用 GroupAction 来构建更复杂的层次结构。

举个栗子:

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

def generate_launch_description():

    # 子分组
    child_group = GroupAction([
        PushRosNamespace('child_turtle'),    # 设置命名空间
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='turtle_a',
            output='screen'
        ),
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='turtle_b',
            output='screen'
        )
    ])

    # 父分组,包含子分组
    super_group = GroupAction([
        PushRosNamespace('super_turtle'),    # 设置命名空间
        child_group,
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='turtle_c',
            output='screen'
        )
    ])

    return LaunchDescription([
        super_group
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

上面使用分组打开了三个小海龟窗口,turtle_aturtle_b 被放置在 child_turtle 命名空间下,而 turtle_c 和整个 child_turtle 被放置在 super_turtle 命名空间下。

运行后,查看节点信息:

查看话题信息:

可以看到,节点和话题都在指定的命令空间下。

# 4 配合其他Launch Actions使用

GroupAction 可以和其他 launch 动作(如 IncludeLaunchDescriptionSetParameter 等)一起使用,实现更丰富的功能。

举个栗子,下面演示一下包含其他的launch文件。

首先定义 child_launch.py

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, LogInfo
from launch.substitutions import LaunchConfiguration, EnvironmentVariable
from launch_ros.actions import Node

def generate_launch_description():
    param_arg = DeclareLaunchArgument(
        'param_value', default_value='100', description='A parameter value for the node'
    )

    env_value = EnvironmentVariable('MY_ENV_VAR', default_value='100')
    login_action = LogInfo(msg=['MY_ENV_VAR:', env_value])

    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        namespace='turtle',
        name='turtle_b',
        output='screen'
        parameters=[{'background_r': LaunchConfiguration('param_value')}]
    )

    return LaunchDescription([
        param_arg, 
        login_action,
        node
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

定义一个子 launch 文件,在启动一个节点,并接收传递的参数,读取环境变量等操作。


然后定义一个 main_launch.py

from launch import LaunchDescription
from launch.actions import GroupAction, IncludeLaunchDescription, SetEnvironmentVariable, DeclareLaunchArgument
from launch.launch_description_sources import PythonLaunchDescriptionSource
import os
from ament_index_python.packages import get_package_share_directory
from launch_ros.actions import Node, PushRosNamespace

def generate_launch_description():

    param_arg = DeclareLaunchArgument(
        'param_value', default_value='100', description='A parameter value for the node'
    )

    included_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            os.path.join(get_package_share_directory('launch_python'), 'launch', 'child_launch.py')
        )
    )

    # 创建一个node
    node = Node(
        package='turtlesim',
        executable='turtlesim_node',
        name='turtle_a',
        output='screen'
    )

    # 通过分组设置
    group = GroupAction([
        PushRosNamespace('turtle'),  # 设置命名空间
        param_arg,  # 设置参数
        SetEnvironmentVariable('MY_ENV_VAR', '200'),  # 设置环境变量
        node,  # 设置启动节点
        included_launch  # 包含其他launch文件
    ])

    return LaunchDescription([
        group
    ])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

在上面的 launch 文件中,GroupAction 中包含了设置命名空间、参数、环境变量、节点、包含另一个 launch 文件。

总结 :通过使用 GroupAction,可以有效组织和控制 launch 文件中多个动作的执行,使其更具可维护性和可扩展性。