🚀 第07课:启动文件(Launch)

ROS2基础 ✅ Docker验证通过

📋 课程目标

🧠 为什么需要Launch文件?

当机器人系统需要同时启动多个节点时,逐个在终端中运行 ros2 run 既繁琐又容易出错。Launch文件可以一次性启动多个节点,并配置它们的参数、重映射、命名空间等。

🔑 Launch文件的优势

📐 Launch系统架构

ros2 launch my_pkg my_launch.py arg1:=value1 ┌───────────────────────────────────────────────────────┐ │ Launch System │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Launch Arg │───►│ Substitution│───► 值替换 │ │ │ (声明参数) │ │ (值替换) │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Node │ │ Condition │ │ │ │ (启动节点) │ │ (条件判断) │ │ │ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────────────────────┐ │ │ │ Launch Description │ │ │ │ Node1 ── Node2 ── Node3 ── Include │ │ │ │ (params) (remap) (ns) (sub-launch) │ │ │ └──────────────────────────────────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Event │ │ Action │ │ │ │ (事件处理) │ │ (执行动作) │ │ │ └─────────────┘ └─────────────┘ │ └───────────────────────────────────────────────────────┘

🐍 基础Launch文件

#!/usr/bin/env python3
"""基础Launch文件 - 启动传感器模拟系统"""

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, LogInfo
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
    """生成Launch描述"""

    # ====== 声明Launch参数 ======
    use_sim_arg = DeclareLaunchArgument(
        'use_sim',
        default_value='true',
        description='是否使用仿真模式'
    )

    sensor_rate_arg = DeclareLaunchArgument(
        'sensor_rate',
        default_value='10.0',
        description='传感器发布频率(Hz)'
    )

    use_rviz_arg = DeclareLaunchArgument(
        'use_rviz',
        default_value='true',
        description='是否启动RViz2'
    )

    # 获取参数值
    use_sim = LaunchConfiguration('use_sim')
    sensor_rate = LaunchConfiguration('sensor_rate')
    use_rviz = LaunchConfiguration('use_rviz')

    # ====== 定义节点 ======

    # 激光雷达节点
    lidar_node = Node(
        package='my_sensors_pkg',
        executable='lidar_simulator',
        name='lidar_sensor',
        namespace='sensors',
        parameters=[{
            'scan_frequency': sensor_rate,
            'num_readings': 360,
            'obstacle_distance': 3.0,
        }],
        output='screen',
        emulate_tty=True,
    )

    # 温度传感器节点
    temp_node = Node(
        package='my_sensors_pkg',
        executable='temperature_sensor',
        name='temp_sensor',
        namespace='sensors',
        parameters=[{
            'base_temp': 25.0,
            'noise_amplitude': 2.0,
            'publish_rate': sensor_rate,
        }],
        output='screen',
    )

    # 障碍物检测节点
    obstacle_node = Node(
        package='my_nav_pkg',
        executable='obstacle_detector',
        name='obstacle_detector',
        parameters=[{
            'safe_distance': 1.0,
            'stop_distance': 0.5,
        }],
        remappings=[
            ('scan', '/sensors/scan'),
            ('cmd_vel', '/cmd_vel'),
        ],
        output='screen',
    )

    # 温度监控节点
    monitor_node = Node(
        package='my_nav_pkg',
        executable='temperature_monitor',
        name='temp_monitor',
        parameters=[{
            'warning_threshold': 30.0,
            'critical_threshold': 35.0,
        }],
        remappings=[
            ('temperature', '/sensors/temperature'),
        ],
        output='screen',
    )

    # RViz2(条件启动)
    rviz_config = PathJoinSubstitution([
        FindPackageShare('my_nav_pkg'), 'config', 'nav.rviz'
    ])

    rviz_node = Node(
        condition=IfCondition(use_rviz),
        package='rviz2',
        executable='rviz2',
        name='rviz2',
        arguments=['-d', rviz_config],
        output='screen',
    )

    # 启动日志
    launch_log = LogInfo(msg='🚀 传感器系统启动中...')

    return LaunchDescription([
        # 声明参数
        use_sim_arg,
        sensor_rate_arg,
        use_rviz_arg,
        launch_log,
        # 启动节点
        lidar_node,
        temp_node,
        obstacle_node,
        monitor_node,
        rviz_node,
    ])

🔄 高级Launch技巧

1. 子Launch包含(Include)

from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.substitutions import FindPackageShare

# 包含另一个Launch文件
sensor_launch = IncludeLaunchDescription(
    PythonLaunchDescriptionSource([
        PathJoinSubstitution([
            FindPackageShare('my_sensors_pkg'),
            'launch', 'sensors.launch.py'
        ])
    ]),
    launch_arguments={
        'sensor_rate': '20.0',
        'use_sim': 'true',
    }.items()
)

2. 分组与命名空间

from launch.actions import GroupAction
from launch_ros.actions import PushROSNamespace

# 为一组节点设置命名空间
robot1_group = GroupAction(actions=[
    PushROSNamespace('robot1'),
    Node(package='my_pkg', executable='sensor_node', name='lidar'),
    Node(package='my_pkg', executable='nav_node', name='navigator'),
    Node(package='my_pkg', executable='ctrl_node', name='controller'),
])

robot2_group = GroupAction(actions=[
    PushROSNamespace('robot2'),
    Node(package='my_pkg', executable='sensor_node', name='lidar'),
    Node(package='my_pkg', executable='nav_node', name='navigator'),
    Node(package='my_pkg', executable='ctrl_node', name='controller'),
])

3. 事件与动作

from launch.actions import RegisterEventHandler, EmitEvent
from launch.event_handlers import OnProcessExit, OnProcessStart
from launch.events import Shutdown

# 节点退出时自动关闭
shutdown_on_exit = RegisterEventHandler(
    event_handler=OnProcessExit(
        target_action=lidar_node,
        on_exit=[
            LogInfo(msg='⚠️ 激光雷达节点退出,关闭系统...'),
            EmitEvent(event=Shutdown(reason='关键节点退出'))
        ]
    )
)

# 节点启动后执行动作
on_start_log = RegisterEventHandler(
    event_handler=OnProcessStart(
        target_action=nav_node,
        on_start=[
            LogInfo(msg='✅ 导航节点已启动')
        ]
    )
)

4. 参数文件加载

import os
from ament_index_python.packages import get_package_share_directory

pkg_dir = get_package_share_directory('my_nav_pkg')

# 方式1:加载整个YAML文件
nav_params = os.path.join(pkg_dir, 'config', 'nav_params.yaml')

nav_node = Node(
    package='nav2_controller',
    executable='controller_server',
    parameters=[nav_params],
)

# 方式2:加载多个参数文件
nav_node = Node(
    package='nav2_controller',
    executable='controller_server',
    parameters=[
        nav_params,
        os.path.join(pkg_dir, 'config', 'costmap_params.yaml'),
        {'use_sim_time': True},  # 同时传入字典
    ],
)

5. 完整的导航系统Launch

#!/usr/bin/env python3
"""完整导航系统Launch - Nav2 + 仿真"""

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


def generate_launch_description():
    pkg_dir = get_package_share_directory('my_robot_nav')
    nav2_launch_dir = get_package_share_directory('nav2_bringup')

    # 参数声明
    use_sim_time = DeclareLaunchArgument(
        'use_sim_time', default_value='true')
    map_file = DeclareLaunchArgument(
        'map', default_value=os.path.join(pkg_dir, 'maps', 'my_map.yaml'))
    params_file = DeclareLaunchArgument(
        'params_file',
        default_value=os.path.join(pkg_dir, 'config', 'nav2_params.yaml'))

    # 仿真环境
    gazebo = IncludeLaunchDescription(
        PythonLaunchDescriptionSource([
            os.path.join(get_package_share_directory('gazebo_ros'),
                        'launch', 'gazebo.launch.py')
        ]),
        launch_arguments={'world': os.path.join(
            pkg_dir, 'worlds', 'my_world.world')}.items()
    )

    # 机器人Spawn
    spawn_robot = Node(
        package='gazebo_ros',
        executable='spawn_entity.py',
        arguments=[
            '-entity', 'my_robot',
            '-robot_namespace', 'robot',
            '-file', os.path.join(pkg_dir, 'urdf', 'robot.urdf'),
            '-x', '0', '-y', '0', '-z', '0.1',
        ],
        output='screen',
    )

    # Nav2导航栈
    navigation = IncludeLaunchDescription(
        PythonLaunchDescriptionSource([
            os.path.join(nav2_launch_dir, 'launch', 'navigation_launch.py')
        ]),
        launch_arguments={
            'use_sim_time': LaunchConfiguration('use_sim_time'),
            'params_file': LaunchConfiguration('params_file'),
        }.items()
    )

    # SLAM
    slam = IncludeLaunchDescription(
        PythonLaunchDescriptionSource([
            os.path.join(nav2_launch_dir, 'launch', 'slam_launch.py')
        ]),
        launch_arguments={
            'use_sim_time': LaunchConfiguration('use_sim_time'),
            'slam_params_file': LaunchConfiguration('params_file'),
        }.items()
    )

    # RViz
    rviz = Node(
        package='rviz2',
        executable='rviz2',
        arguments=['-d', os.path.join(pkg_dir, 'rviz', 'nav2.rviz')],
        parameters=[{'use_sim_time': LaunchConfiguration('use_sim_time')}],
        output='screen',
    )

    return LaunchDescription([
        use_sim_time, map_file, params_file,
        gazebo, spawn_robot, navigation, slam, rviz,
    ])

🖥️ Launch命令行

# 运行Launch文件
ros2 launch my_pkg my_launch.py

# 传递参数
ros2 launch my_pkg my_launch.py use_sim:=false sensor_rate:=20.0

# 查看Launch文件内容
ros2 launch my_pkg my_launch.py --show-args

# 从包中查找Launch文件
ros2 pkg prefix my_pkg
find $(ros2 pkg prefix my_pkg) -name "*.launch.py"

🎯 练习题

📝 练习1:多传感器Launch

创建一个Launch文件,同时启动激光雷达、IMU和摄像头三个传感器节点,每个节点使用不同频率参数。

📝 练习2:条件启动

创建Launch文件,通过 use_rviz 参数控制是否启动RViz,通过 use_sim 参数选择真实传感器还是仿真节点。

📝 练习3:多机器人Launch

创建多机器人Launch,使用GroupAction为两个机器人设置不同命名空间,各自运行独立的导航栈。

🏆 成就解锁

🏅 Launch编排专家

经验值:+200 XP