# 70 · Launch 文件系统(完全指南) > **目标**:能写 ROS2 Python launch 文件,理解参数覆盖、嵌套、生命周期、条件启动。 --- ## 目录 - [1. 什么是 launch](#1-什么是-launch) - [2. 最小 launch 文件](#2-最小-launch-文件) - [3. 启动方式](#3-启动方式) - [4. 顶层 Actions](#4-顶层-actions) - [5. 事件处理 / 生命周期](#5-事件处理--生命周期) - [6. 条件启动](#6-条件启动) - [7. 命名空间 / 多机器人](#7-命名空间--多机器人) - [8. 常用模式(实战)](#8-常用模式实战) - [9. ROS2 launch CLI](#9-ros2-launch-cli) - [10. 常见坑](#10-常见坑) - [11. 在本仓库里跑](#11-在本仓库里跑) - [12. 进阶:复杂 launch + 自定义 Action](#12-进阶复杂-launch--自定义-action) --- ## 1. 什么是 launch ROS1 用 XML 启动多节点繁琐。ROS2 的 launch = Python 脚本: - 一键启多个节点 + 设参数 - 跨包嵌套复用 - 条件启动、按机器切换配置 - 生命周期管理(启动顺序、超时、失败重试) --- ## 2. 最小 launch 文件 ```python # src//launch/my_launch.py from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): """ROS2 launch 框架会调这个函数生成 LaunchDescription 实例。""" return LaunchDescription([ Node( package='py_pubsub', # 包名 executable='talker', # 入口名(setup.py entry_points) name='talker_py', # 节点名(可重命名避免冲突) output='screen', # stdout/stderr 直打到终端 parameters=[ # 参数 dict {'period_ms': 500, 'topic': 'chatter'}, ], remappings=[ # topic 重映射 ('/chatter', '/my_chatter'), ], ), ]) ``` **入口约定**:函数名必须是 `generate_launch_description()`,返回 `LaunchDescription`。 --- ## 3. 启动方式 ```bash # 装包后,ROS 自动索引 launch 到 share//launch/ colcon build --packages-select py_pubsub # 用包名 + launch 文件名(去 .py 后缀) ros2 launch py_pubsub my_launch.py # 加参数(覆盖 LaunchConfiguration) ros2 launch py_pubsub my_launch.py topic:=hello period_ms:=200 ``` ROS2 找 launch 文件路径:`/share//launch/`。**launch 文件必须装到 share//launch/**,否则报 "file not found"。 --- ## 4. 顶层 Actions ### 4.1 `Node` ```python Node( package=..., executable=..., name=..., namespace='/', # 命名空间(可分多机器人) output='screen', # 'screen' / 'log' / 'both' parameters=[{...}], # 参数 dict remappings=[(...)], # topic 重映射 arguments=[...], # 透传给 executable 的命令行参数 respawn=False, # 崩溃是否重启 respawn_delay=0, ) ``` ### 4.2 `DeclareLaunchArgument` 声明命令行参数 ```python from launch.actions import DeclareLaunchArgument DeclareLaunchArgument( 'topic', # 参数名 default_value='chatter', # 默认值 description='Topic name', # 说明(ros2 launch --help 显示) choices=['chatter', 'hello'], # (可选)限定取值 ) ``` ### 4.3 `LaunchConfiguration` 取值 ```python from launch.substitutions import LaunchConfiguration topic = LaunchConfiguration('topic') Node( ..., parameters=[{'topic': topic}], ) ``` ### 4.4 `IncludeLaunchDescription` 嵌套 ```python from launch.actions import IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource IncludeLaunchDescription( PythonLaunchDescriptionSource(), launch_arguments={ 'topic': 'chatter', 'period_ms': '500', }.items(), ) ``` **用 FindPackageShare + PathJoinSubstitution**(跨包推荐): ```python from launch_ros.substitutions import FindPackageShare from launch.substitutions import PathJoinSubstitution pkg_share = FindPackageShare('py_pubsub') launch_path = PathJoinSubstitution([pkg_share, 'launch', 'pubsub_launch.py']) IncludeLaunchDescription(PythonLaunchDescriptionSource(launch_path)) ``` ### 4.5 `ExecuteProcess` 跑 shell 命令 ```python from launch.actions import ExecuteProcess ExecuteProcess( cmd=['xacro', 'arm.xacro'], output='screen', ) ``` > ⚠️ `Command(['cat', '/path'])` 偶尔会丢空格变成 `'cat/path'`, > 建议**launch 启动前 `open().read()` 读文件**,不要 cat。 ### 4.6 `OpaqueFunction` 任意函数 ```python from launch.actions import OpaqueFunction def my_setup(context, *args, **kwargs): # 自定义逻辑 return [...] LaunchDescription([ OpaqueFunction(function=my_setup), Node(...), ]) ``` ### 4.7 `TimerAction` 延迟启动 ```python from launch.actions import TimerAction LaunchDescription([ Node(package='srv', executable='server'), TimerAction(period=2.0, actions=[ Node(package='client', executable='client'), ]), ]) ``` --- ## 5. 事件处理 / 生命周期 ### 5.1 启动顺序 ROS2 launch 按 LaunchDescription 里 action 的顺序**同步**启动(默认)。 ### 5.2 事件 handler ```python from launch import LaunchDescription from launch.event_handlers import OnProcessExit from launch.actions import RegisterEventHandler, LogInfo def generate_launch_description(): server = Node(package='srv', executable='server') return LaunchDescription([ server, RegisterEventHandler( OnProcessExit( target_action=server, on_exit=[LogInfo(msg='server exited, shutting down')], ) ), ]) ``` 支持的事件: `OnProcessStart` / `OnProcessExit` / `OnProcessIO` 等。 --- ## 6. 条件启动 ```python from launch.conditions import IfCondition, UnlessCondition Node( ..., condition=IfCondition(LaunchConfiguration('use_camera')), ) ``` CLI 用 `:=true` / `:=false`: ```bash ros2 launch my_pkg my.launch.py use_camera:=true ``` --- ## 7. 命名空间 / 多机器人 ### 7.1 namespace 隔离 ```python Node( package='py_pubsub', executable='talker', namespace='robot1', # → topic /robot1/chatter name='talker', # → 节点名 /robot1/talker ) ``` ### 7.2 push_ros_namespace ```python from launch.actions import PushRosNamespace LaunchDescription([ PushRosNamespace('robot1'), Node(...), ]) ``` --- ## 8. 常用模式(实战) ### 8.1 参数文件加载 ```python from ament_index_python.packages import get_package_share_directory import os params_file = os.path.join( get_package_share_directory('my_pkg'), 'config', 'params.yaml' ) Node( package='my_pkg', executable='node', parameters=[params_file], ) ``` ### 8.2 多节点同包 ```python nodes = [ Node(package='py_pubsub', executable='talker', name='talker_a', parameters=[{'topic': 'a'}]), Node(package='py_pubsub', executable='listener', name='listener_a', parameters=[{'topic': 'a'}]), ] ``` ### 8.3 跨包组合(`bringup` 模式) 本仓库 [`src/bringup/launch/full_demo_launch.py`](../src/bringup/launch/full_demo_launch.py): ```python def _include(pkg_name, launch_file): pkg = FindPackageShare(pkg_name) path = PathJoinSubstitution([pkg, 'launch', launch_file]) return IncludeLaunchDescription(PythonLaunchDescriptionSource(path)) def generate_launch_description(): return LaunchDescription([ _include('py_pubsub', 'pubsub_launch.py'), _include('cpp_pubsub', 'pubsub_launch.py'), _include('py_srv', 'srv_launch.py'), _include('py_action_demo', 'action_launch.py'), _include('cpp_robot_tf2', 'robot_tf2_launch.py'), _include('py_vision_demo', 'vision_launch.py'), ]) ``` ### 8.4 读 URDF 文件内容 ```python import os from ament_index_python.packages import get_package_share_directory pkg_share = get_package_share_directory('cpp_robot_tf2') urdf_path = os.path.join(pkg_share, 'urdf', 'simple_arm.urdf') with open(urdf_path, 'r', encoding='utf-8') as f: robot_description = f.read() Node( package='robot_state_publisher', executable='robot_state_publisher', parameters=[{'robot_description': robot_description}], ) ``` --- ## 9. ROS2 launch CLI ```bash # 列出所有 launch 文件 ros2 launch --show-args # 看可用参数 ros2 launch --show-args # 调试(详细日志) ros2 launch -d # 传参 ros2 launch topic:=hello period_ms:=200 ``` --- ## 10. 常见坑 ### 10.1 launch 文件找不到 - `setup.py` 的 `data_files` 必须包含 `'share//launch': ['launch/*.py']` - colcon build 后 launch 文件没复制到 install/share → 检查 build log ### 10.2 节点名冲突 同一 ROS Domain 内**节点名必须唯一**。launch 里 `name=` 重命名,或用 `namespace=` 隔离。 ### 10.3 参数失效 `parameters=[{'topic': 'chatter'}]` 里 topic 是字符串;如果想"取自 LaunchConfiguration": ```python parameters=[{'topic': LaunchConfiguration('topic')}] ``` ### 10.4 `Command(['cat', path])` 空格丢了 如前所述,**改用 launch 启动前 `open().read()`**。 ### 10.5 launch 启动后节点秒退 - 节点 main 函数异常(看日志) - 参数名拼错(节点没收到参数) - launch `output='log'` 看不到错误 → 改 `output='screen'` ### 10.6 launch 嵌套找不到文件 `IncludeLaunchDescription` 引用的 launch 文件必须在被包含包的 `share//launch/` 下被 colcon 实际安装。 --- ## 11. 在本仓库里跑 ### 11.1 各包 launch 文件清单 ``` src/ ├── py_pubsub/launch/pubsub_launch.py (2 节点,纯 Python) ├── cpp_pubsub/launch/pubsub_launch.py (2 节点,C++ + topic 命令行参数) ├── py_srv/launch/srv_launch.py (1 service) ├── py_action_demo/launch/action_launch.py (1 action server) ├── cpp_robot_tf2/launch/robot_tf2_launch.py (3 节点,URDF + TF) ├── py_vision_demo/launch/vision_launch.py (2 节点,cv_bridge) └── bringup/launch/ ├── pubsub_launch.py (4 节点,Topic 跨包) ├── service_launch.py ├── action_launch.py ├── robot_launch.py (嵌套 cpp_robot_tf2) ├── vision_launch.py (嵌套 py_vision_demo) └── full_demo_launch.py (11 节点,跨所有包) ``` ### 11.2 看可用参数 ```bash docker exec ros2_dev bash -lc "source /root/ros2_ws/install/setup.bash && ros2 launch bringup full_demo_launch.py --show-args" ``` --- ## 12. 进阶:复杂 launch + 自定义 Action ### 12.1 复杂 launch 模板 ```python import os from ament_index_python.packages import get_package_share_directory from launch import LaunchDescription from launch.actions import ( DeclareLaunchArgument, OpaqueFunction, RegisterEventHandler, LogInfo, ) from launch.conditions import IfCondition from launch.event_handlers import OnProcessExit from launch.substitutions import ( LaunchConfiguration, Command, FindExecutable, PathJoinSubstitution, ) from launch_ros.actions import Node from launch_ros.substitutions import FindPackageShare def launch_setup(context, *args, **kwargs): # 任何动态逻辑 config_file = LaunchConfiguration('config_file').perform(context) if not os.path.exists(config_file): return [LogInfo(msg=f'config not found: {config_file}')] return [...] def generate_launch_description(): return LaunchDescription([ DeclareLaunchArgument('config_file', default_value='config.yaml'), DeclareLaunchArgument('enable_ai', default_value='true'), Node( package='my_pkg', executable='main_node', parameters=[LaunchConfiguration('config_file')], condition=IfCondition(LaunchConfiguration('enable_ai')), ), OpaqueFunction(function=launch_setup), ]) ``` --- ## 接下来读 | 主题 | 文档 | |---|---| | Topic | [`20-topics.md`](20-topics.md) | | Service | [`30-services.md`](30-services.md) | | Action | [`40-actions.md`](40-actions.md) | | colcon / ament 包构建 | [`80-package-build.md`](80-package-build.md) | | 测试策略 | [`90-testing.md`](90-testing.md) | --- --- ## 📖 阅读路径导航 > 💡 这是仓库 `doc/` 下所有文档的推荐阅读顺序。[返回 README 总导航](../README.md#-23-篇文档怎么读) > > ⏱ **本文预计阅读时间**: 50 分钟 > 📍 **当前位置**: 第 18 / 24 篇 - ⏮ **上一篇**: [机器人模型描述](60-urdf.md) - ⏭ **下一篇**: [colcon / ament 包构建](80-package-build.md)