483 lines
12 KiB
Markdown
483 lines
12 KiB
Markdown
# 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/<pkg>/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/<pkg>/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 文件路径:`<install>/share/<pkg>/launch/`。**launch 文件必须装到 share/<pkg>/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(<other_launch.py>),
|
|
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 <pkg> <file>
|
|
|
|
# 看可用参数
|
|
ros2 launch <pkg> <file> --show-args
|
|
|
|
# 调试(详细日志)
|
|
ros2 launch -d <pkg> <file>
|
|
|
|
# 传参
|
|
ros2 launch <pkg> <file> topic:=hello period_ms:=200
|
|
```
|
|
|
|
---
|
|
|
|
## 10. 常见坑
|
|
|
|
### 10.1 launch 文件找不到
|
|
- `setup.py` 的 `data_files` 必须包含 `'share/<pkg>/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/<pkg>/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)
|