ESC
输入关键词搜索文章标题和内容

ROS2 Launch文件编排:从单节点启动到多节点自动化管理

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

ROS2 Launch文件编排:从单节点启动到多节点自动化管理

Launch是ROS2里管理多节点启动的核心工具。手动逐个ros2 run启动节点,三个以上就容易乱——Launch文件一次编排,一键拉起整套系统。本文基于ROS2 Jazzy Jalisco LTS,覆盖Launch参数、条件启动、嵌套引用、生命周期管理等实战场景。

一、Launch系统概述

Launch是什么

Launch是ROS2的多节点启动编排工具,解决三个问题:

  1. 一键启动:一个命令拉起多个节点,不用逐个终端手动ros2 run
  2. 参数传递:统一管理节点参数,支持命令行覆盖和配置文件加载
  3. 依赖编排:控制节点启动条件、重映射、命名空间,管理节点间依赖关系

三种格式对比

ROS2支持Python、XML、YAML三种Launch文件格式:

特性 Python XML YAML
灵活性 最高,可写任意Python逻辑 中等,标签式声明 最低,纯数据描述
可读性 中等,需懂Python 高,结构清晰 高,简洁直观
条件启动 支持 支持 有限
自定义逻辑 支持 不支持 不支持
官方推荐 首选推荐 适合简单场景 适合纯参数场景
文件后缀 .launch.py .launch.xml .launch.yaml

Python格式最灵活,是实际项目中的首选。 本文全部使用Python格式。

ROS1 Launch vs ROS2 Launch

对比项 ROS1 Launch ROS2 Launch
格式 仅XML Python/XML/YAML
参数机制 <arg> + <param> DeclareLaunchArgument + LaunchConfiguration
条件启动 <if> / <unless> IfCondition / UnlessCondition
嵌套引用 <include> IncludeLaunchDescription
事件系统 无 支持on_exit等事件
生命周期管理 无 支持自动configure/activate
替换机制 $(find pkg) FindPackageShare + PathJoinSubstitution

ROS2 Launch比ROS1强在可编程性和事件驱动,不再是纯声明式配置。

二、Python Launch基础

最简Launch文件

启动demo_nodes_cpp的talker和listener:

# demo_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        # 发布节点
        Node(
            package='demo_nodes_cpp',
            executable='talker',
            name='talker_node',
            output='screen',
        ),
        # 订阅节点
        Node(
            package='demo_nodes_cpp',
            executable='listener',
            name='listener_node',
            output='screen',
        ),
    ])

运行方式:

# 方式1:通过包名启动(需要安装到workspace)
ros2 launch demo_launch demo_launch.py

# 方式2:直接指定文件路径(开发调试用)
ros2 launch /path/to/demo_launch.py

LaunchDescription + Node核心参数

Node是最常用的Action,关键参数:

参数 类型 说明
package str ROS2包名
executable str 可执行文件名
name str 节点名(覆盖代码中的名字)
namespace str 命名空间
parameters list/dict 参数列表或字典
remappings list 话题重映射
output str 输出目标:screen/log
arguments list 传递给可执行文件的命令行参数
condition Condition 启动条件

三、Launch参数

声明与读取参数

# param_launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration


def generate_launch_description():
    # 声明参数,带默认值和描述
    declare_freq_arg = DeclareLaunchArgument(
        'publish_freq',
        default_value='10',
        description='发布频率(Hz)',
    )

    declare_use_sim_arg = DeclareLaunchArgument(
        'use_sim_time',
        default_value='false',
        description='是否使用仿真时间',
    )

    return LaunchDescription([
        # 声明必须放在LaunchDescription中才能被识别
        declare_freq_arg,
        declare_use_sim_arg,

        Node(
            package='demo_nodes_cpp',
            executable='talker',
            name='talker_node',
            output='screen',
            # LaunchConfiguration读取参数值
            parameters=[{
                'publish_freq': LaunchConfiguration('publish_freq'),
                'use_sim_time': LaunchConfiguration('use_sim_time'),
            }],
        ),
    ])

命令行传参

# 传递单个参数
ros2 launch my_pkg param_launch.py publish_freq:=20

# 传递多个参数
ros2 launch my_pkg param_launch.py publish_freq:=20 use_sim_time:=true

# 查看Launch文件支持的所有参数
ros2 launch my_pkg param_launch.py --show-args

--show-args输出示例:

Arguments (pass arguments as 'name:=value'):
    'publish_freq':
        发布频率(Hz)
        (default: '10')
    'use_sim_time':
        是否使用仿真时间
        (default: 'false')

关键点:DeclareLaunchArgument的description字段就是--show-args显示的说明,务必写清楚。

四、条件启动

IfCondition / UnlessCondition

# conditional_launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node


def generate_launch_description():
    declare_sim_arg = DeclareLaunchArgument(
        'use_sim',
        default_value='false',
        description='是否启动仿真环境',
    )

    return LaunchDescription([
        declare_sim_arg,

        # IfCondition:条件为真时启动
        Node(
            package='gazebo_ros',
            executable='gazebo',
            name='gazebo',
            output='screen',
            condition=IfCondition(LaunchConfiguration('use_sim')),
        ),

        # UnlessCondition:条件为假时启动(即use_sim=false时启动真机驱动)
        Node(
            package='my_robot_drivers',
            executable='lidar_driver',
            name='lidar_driver',
            output='screen',
            condition=UnlessCondition(LaunchConfiguration('use_sim')),
        ),

        # 两种条件下都启动的节点
        Node(
            package='my_robot_nav',
            executable='nav2_controller',
            name='nav2_controller',
            output='screen',
        ),
    ])

运行效果:

# 启动仿真模式:gazebo启动,lidar_driver不启动
ros2 launch my_pkg conditional_launch.py use_sim:=true

# 真机模式:lidar_driver启动,gazebo不启动
ros2 launch my_pkg conditional_launch.py use_sim:=false

IfCondition和UnlessCondition互为补充,一个场景用哪个更直观就用哪个。

五、参数文件加载

加载YAML参数文件

实际项目中参数多且需要按场景切换,YAML文件是标准做法。

参数文件config/robot_params.yaml:

robot_controller:
  ros__parameters:
    control_freq: 50
    max_speed: 1.5
    pid_kp: 0.8
    pid_ki: 0.01
    pid_kd: 0.05

Launch文件加载:

# params_file_launch.py
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
    # 方式1:硬编码路径(不推荐,可移植性差)
    # params_file = '/path/to/config/robot_params.yaml'

    # 方式2:通过FindPackageShare定位包内文件(推荐)
    params_file = PathJoinSubstitution([
        FindPackageShare('my_robot_nav'),  # 包名
        'config',                           # 子目录
        'robot_params.yaml',                # 文件名
    ])

    return LaunchDescription([
        Node(
            package='my_robot_nav',
            executable='robot_controller',
            name='robot_controller',
            output='screen',
            # 加载YAML参数文件
            parameters=[params_file],
        ),
    ])

混合加载:文件参数 + 命令行覆盖

# 混合参数加载:YAML文件做基础,命令行参数覆盖特定值
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
    declare_speed_arg = DeclareLaunchArgument(
        'max_speed',
        default_value='2.0',
        description='最大速度覆盖值(m/s)',
    )

    params_file = PathJoinSubstitution([
        FindPackageShare('my_robot_nav'),
        'config',
        'robot_params.yaml',
    ])

    return LaunchDescription([
        declare_speed_arg,

        Node(
            package='my_robot_nav',
            executable='robot_controller',
            name='robot_controller',
            output='screen',
            parameters=[
                params_file,  # 先加载YAML文件
                {             # 再用字典覆盖特定参数
                    'max_speed': LaunchConfiguration('max_speed'),
                },
            ],
        ),
    ])

注意:parameters列表中后面的字典会覆盖前面YAML文件中的同名参数。

六、节点重映射与命名空间

remappings:话题重映射

节点代码写死了话题名,但实际部署需要改——不改代码,用重映射解决。

# remap_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    return LaunchDescription([
        Node(
            package='demo_nodes_cpp',
            executable='talker',
            name='talker_node',
            output='screen',
            # 将默认的/chatter重映射到/sensor/lidar_data
            remappings=[
                ('/chatter', '/sensor/lidar_data'),
            ],
        ),
        Node(
            package='demo_nodes_cpp',
            executable='listener',
            name='listener_node',
            output='screen',
            # listener也要对应重映射
            remappings=[
                ('/chatter', '/sensor/lidar_data'),
            ],
        ),
    ])

namespace:命名空间隔离

多机器人场景下,同一套驱动节点需要隔离话题和TF:

# namespace_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    # 机器人A的命名空间
    robot_a_nodes = [
        Node(
            package='my_robot_drivers',
            executable='lidar_driver',
            name='lidar_driver',
            namespace='robot_a',  # 话题变为 /robot_a/scan
            output='screen',
        ),
        Node(
            package='my_robot_nav',
            executable='nav2_controller',
            name='nav2_controller',
            namespace='robot_a',  # 话题变为 /robot_a/cmd_vel
            output='screen',
        ),
    ]

    # 机器人B的命名空间
    robot_b_nodes = [
        Node(
            package='my_robot_drivers',
            executable='lidar_driver',
            name='lidar_driver',
            namespace='robot_b',  # 话题变为 /robot_b/scan
            output='screen',
        ),
        Node(
            package='my_robot_nav',
            executable='nav2_controller',
            name='nav2_controller',
            namespace='robot_b',  # 话题变为 /robot_b/cmd_vel
            output='screen',
        ),
    ]

    return LaunchDescription(robot_a_nodes + robot_b_nodes)

命名空间效果:话题/scan → /robot_a/scan,TF的base_link → /robot_a/base_link。

来自 linuxros.cn · linuxROS

七、IncludeLaunchDescription

嵌套Launch文件

把复杂系统拆成多个Launch文件,再用IncludeLaunchDescription组合:

# robot_bringup_launch.py(顶层编排文件)
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
    declare_sim_arg = DeclareLaunchArgument(
        'use_sim',
        default_value='false',
        description='是否使用仿真',
    )

    # 引用sensor_drivers包的Launch文件
    sensor_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            PathJoinSubstitution([
                FindPackageShare('my_robot_drivers'),
                'launch',
                'sensors.launch.py',
            ])
        ),
        # 向子Launch文件传递参数
        launch_arguments={
            'use_sim': LaunchConfiguration('use_sim'),
        }.items(),
    )

    # 引用navigation包的Launch文件
    nav_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            PathJoinSubstitution([
                FindPackageShare('my_robot_nav'),
                'launch',
                'navigation.launch.py',
            ])
        ),
        launch_arguments={
            'use_sim': LaunchConfiguration('use_sim'),
        }.items(),
    )

    return LaunchDescription([
        declare_sim_arg,
        sensor_launch,
        nav_launch,
    ])

好处:每个子系统独立维护自己的Launch文件,顶层只做编排,不关心内部细节。

八、生命周期节点自动管理

在Launch中触发configure + activate

生命周期节点(Lifecycle Node)启动后处于Unconfigured状态,需要手动触发状态转换。Launch提供了EmitEvent + RegisterEventHandler来自动完成:

# lifecycle_launch.py
from launch import LaunchDescription
from launch.actions import EmitEvent, RegisterEventHandler
from launch.events import matches_action
from launch.lifecycle_event import LifecycleEventMatch
from launch_ros.actions import LifecycleNode
from launch_ros.events import ChangeState
from launch_ros.event_handlers import OnStateTransition
from lifecycle_msgs.msg import Transition


def generate_launch_description():
    # 声明生命周期节点
    lifecycle_node = LifecycleNode(
        package='my_robot_nav',
        executable='map_saver',
        name='map_saver',
        namespace='',
        output='screen',
    )

    # 事件1:节点启动后自动触发configure
    configure_event = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=matches_action(lifecycle_node),
            transition_id=Transition.TRANSITION_CONFIGURE,
        )
    )

    # 事件2:configure完成后自动触发activate
    activate_after_configure = RegisterEventHandler(
        OnStateTransition(
            target_lifecycle_node=lifecycle_node,
            goal_state='inactive',  # configure成功后进入inactive
            entities=[
                EmitEvent(
                    event=ChangeState(
                        lifecycle_node_matcher=matches_action(lifecycle_node),
                        transition_id=Transition.TRANSITION_ACTIVATE,
                    )
                )
            ],
        )
    )

    return LaunchDescription([
        lifecycle_node,
        configure_event,
        activate_after_configure,
    ])

状态转换链:Unconfigured → Inactive(configure)→ Active(activate)。节点进入Active状态后才开始正常工作。

九、Launch事件系统

on_exit事件:节点退出时触发动作

某个关键节点崩溃后,需要自动重启或清理资源:

# event_launch.py
from launch import LaunchDescription
from launch.actions import ExecuteProcess, RegisterEventHandler
from launch.event_handlers import OnProcessExit
from launch_ros.actions import Node


def generate_launch_description():
    # 核心节点
    core_node = Node(
        package='my_robot_nav',
        executable='nav2_controller',
        name='nav2_controller',
        output='screen',
    )

    # 辅助节点:等核心节点退出后再启动(比如做清理)
    cleanup_node = ExecuteProcess(
        cmd=['ros2', 'topic', 'pub', '/system/status',
             'std_msgs/msg/String', '{data: "nav_shutdown"}', '--once'],
        output='screen',
    )

    # 注册事件:core_node退出时启动cleanup_node
    on_core_exit = RegisterEventHandler(
        OnProcessExit(
            target_action=core_node,
            on_exit=[cleanup_node],
        )
    )

    return LaunchDescription([
        core_node,
        on_core_exit,
    ])

OnProcessExit的on_exit列表里可以放任意Action,不限于启动节点——发通知、写日志、执行脚本都行。

十、完整实战:机器人启动编排

综合运用参数、条件启动、命名空间、生命周期管理、事件系统,编排一个完整的机器人启动流程:

# robot_bringup.launch.py
import os
from launch import LaunchDescription
from launch.actions import (
    DeclareLaunchArgument,
    EmitEvent,
    IncludeLaunchDescription,
    RegisterEventHandler,
)
from launch.conditions import IfCondition, UnlessCondition
from launch.event_handlers import OnProcessExit
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import LifecycleNode, Node
from launch_ros.events import ChangeState
from launch_ros.event_handlers import OnStateTransition
from launch_ros.substitutions import FindPackageShare
from lifecycle_msgs.msg import Transition


def generate_launch_description():
    # ========== 参数声明 ==========
    declare_sim_arg = DeclareLaunchArgument(
        'use_sim',
        default_value='false',
        description='是否使用仿真环境',
    )
    declare_robot_ns_arg = DeclareLaunchArgument(
        'robot_namespace',
        default_value='robot_a',
        description='机器人命名空间',
    )

    # ========== 参数文件路径 ==========
    nav_params = PathJoinSubstitution([
        FindPackageShare('my_robot_nav'),
        'config',
        'nav_params.yaml',
    ])

    # ========== 仿真环境(条件启动) ==========
    gazebo_launch = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            PathJoinSubstitution([
                FindPackageShare('gazebo_ros'),
                'launch',
                'gazebo.launch.py',
            ])
        ),
        condition=IfCondition(LaunchConfiguration('use_sim')),
    )

    # ========== 真机传感器驱动(条件启动)==========
    lidar_driver = Node(
        package='my_robot_drivers',
        executable='lidar_driver',
        name='lidar_driver',
        namespace=LaunchConfiguration('robot_namespace'),
        output='screen',
        condition=UnlessCondition(LaunchConfiguration('use_sim')),
    )

    camera_driver = Node(
        package='my_robot_drivers',
        executable='camera_driver',
        name='camera_driver',
        namespace=LaunchConfiguration('robot_namespace'),
        output='screen',
        condition=UnlessCondition(LaunchConfiguration('use_sim')),
    )

    # ========== 控制器节点 ==========
    controller = Node(
        package='my_robot_nav',
        executable='nav2_controller',
        name='nav2_controller',
        namespace=LaunchConfiguration('robot_namespace'),
        output='screen',
        parameters=[nav_params],
    )

    # ========== 生命周期节点:地图保存 ==========
    map_saver = LifecycleNode(
        package='my_robot_nav',
        executable='map_saver',
        name='map_saver',
        namespace=LaunchConfiguration('robot_namespace'),
        output='screen',
    )

    # 自动触发 configure 和 activate
    configure_map_saver = EmitEvent(
        event=ChangeState(
            lifecycle_node_matcher=lambda node: node.node_name == 'map_saver',
            transition_id=Transition.TRANSITION_CONFIGURE,
        )
    )

    activate_after_configure = RegisterEventHandler(
        OnStateTransition(
            target_lifecycle_node=map_saver,
            goal_state='inactive',
            entities=[
                EmitEvent(
                    event=ChangeState(
                        lifecycle_node_matcher=lambda node: node.node_name == 'map_saver',
                        transition_id=Transition.TRANSITION_ACTIVATE,
                    )
                )
            ],
        )
    )

    # ========== 事件:控制器退出时记录日志 ==========
    on_controller_exit = RegisterEventHandler(
        OnProcessExit(
            target_action=controller,
            on_exit=[
                ExecuteProcess(
                    cmd=['ros2', 'topic', 'pub', '/system/status',
                         'std_msgs/msg/String',
                         '{data: "controller_shutdown"}', '--once'],
                    output='screen',
                ),
            ],
        )
    )

    return LaunchDescription([
        # 参数声明
        declare_sim_arg,
        declare_robot_ns_arg,
        # 仿真环境
        gazebo_launch,
        # 真机驱动
        lidar_driver,
        camera_driver,
        # 控制器
        controller,
        # 生命周期节点
        map_saver,
        configure_map_saver,
        activate_after_configure,
        # 事件处理
        on_controller_exit,
    ])

启动依赖关系图:

flowchart TB A(["开始"]) --> B{"use_sim?"} B -->|"true"| C["启动Gazebo仿真"] B -->|"false"| D["启动真机驱动"] D --> D1["LiDAR驱动"] D --> D2["Camera驱动"] C --> E["启动控制器"] D1 --> E D2 --> E E --> F["地图保存节点<br/>configure"] F --> G["地图保存节点<br/>activate"] G --> H(["系统就绪"]) E -->|"控制器退出"| I["发布状态通知"] I --> J(["结束"])

十一、常见问题

Q1:Launch文件修改后不生效?

每次修改Launch文件都要重新colcon build太慢。开发阶段用--symlink-install创建符号链接,修改后直接生效:

colcon build --symlink-install
source install/setup.bash
ros2 launch my_pkg my_launch.py

Q2:节点启动顺序问题?

Launch不保证节点按声明顺序启动——所有节点几乎同时启动。如果B依赖A先就绪,有两种方案:

  • 方案1:在B的代码里用wait_for_service或wait_for_topic等待A就绪
  • 方案2:用OnProcessExit事件,A退出或A输出特定内容后再启动B

不要依赖Launch的声明顺序来控制启动顺序。

Q3:参数文件路径找不到?

FindPackageShare找不到包,报错package not found。排查步骤:

# 1. 确认包已编译安装
colcon list  # 查看workspace中的包

# 2. 确认install目录中有share文件夹
ls install/my_robot_nav/share/my_robot_nav/

# 3. 确认source了setup.bash
echo $ROS_PACKAGE_PATH

# 4. 用ros2 pkg prefix确认路径
ros2 pkg prefix my_robot_nav

最常见的原因是忘了source install/setup.bash。

十二、总结

Launch文件是ROS2系统集成的核心工具,掌握以下要点就够了:

功能 关键API 一句话
启动节点 Node 指定package+executable
声明参数 DeclareLaunchArgument 带默认值和描述
读取参数 LaunchConfiguration 运行时替换
条件启动 IfCondition / UnlessCondition 根据参数决定是否启动
加载参数文件 PathJoinSubstitution + FindPackageShare 定位包内YAML文件
话题重映射 remappings 不改代码改话题名
命名空间 namespace 多机器人隔离
嵌套引用 IncludeLaunchDescription 组合子Launch文件
生命周期管理 EmitEvent + ChangeState 自动configure/activate
事件处理 OnProcessExit 节点退出时触发动作

速查命令:

# 启动Launch文件
ros2 launch <pkg> <file.launch.py>

# 直接指定路径启动
ros2 launch /path/to/file.launch.py

# 传参
ros2 launch <pkg> <file.launch.py> arg:=value

# 查看参数
ros2 launch <pkg> <file.launch.py> --show-args

# 开发阶段符号链接
colcon build --symlink-install

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页