init: ROS2 learning suite

This commit is contained in:
xs
2026-08-03 18:09:35 +08:00
commit 5ef38ab508
95 changed files with 13322 additions and 0 deletions
+468
View File
@@ -0,0 +1,468 @@
# 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) |