feat(level1): ROS2 完全体 12 包 / 80 测试 / 23 文档 / 工程化 / Docker 分组
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
# Level 1 ~ Level 4 全方位 ROS2 学习路线图
|
||||
|
||||
> **目标读者**: 想从零学到能用 ROS2 + 机械臂 + VLA (Vision-Language-Action) 做真实具身智能系统的开发者。
|
||||
>
|
||||
> **学习承诺**: 本仓库把 ROS2 从基础到 VLA 部署分成 4 级,每级有可运行的代码包 + 教科书级别文档 + 自动化测试。
|
||||
>
|
||||
> **预计总时长**: 每天 2-3 小时,Level 1 约 4-6 周,Level 2 约 6-8 周,Level 3 约 8-12 周,Level 4 持续学习。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [学习哲学](#学习哲学)
|
||||
- [Level 1: ROS2 基础机制 (Foundation)](#level-1-ros2-基础机制-foundation)
|
||||
- [Level 2: 仿真 + 工具链 (Simulation & Tooling)](#level-2-仿真--工具链-simulation--tooling)
|
||||
- [Level 3: 机械臂实战 (Real Robot)](#level-3-机械臂实战-real-robot)
|
||||
- [Level 4: 具身智能 VLA (Embodied AI)](#level-4-具身智能-vla-embodied-ai)
|
||||
- [硬件清单](#硬件清单)
|
||||
- [时间估算](#时间估算)
|
||||
- [推荐阅读 + 引用](#推荐阅读--引用)
|
||||
|
||||
---
|
||||
|
||||
## 学习哲学
|
||||
|
||||
### 三条铁律
|
||||
|
||||
1. **跑通 > 读完** — 每学一个机制,先在仓库里跑通最小 demo,再读设计文档。
|
||||
2. **测一次 > 看一次** — 每个包都带 pytest/gtest,跑通测试比"看懂了"更可信。
|
||||
3. **文档带引用** — 每篇深度文档末尾都列 ROS2 官方文档 + 设计稿链接 + DDS 规范 + 论文。
|
||||
|
||||
### 设计思想: 双层解耦
|
||||
|
||||
- **本机 venv** — 开发工具链(ruff/black/mypy/pytest),不污染系统 Python,只装 IDE / 编辑器需要的。
|
||||
- **Docker 容器** — ROS2 运行时(`osrf/ros:humble-desktop`),跨 Win/macOS/Linux 一致,apt 装 ROS2 包。
|
||||
|
||||
理由: ROS2 原生依赖大量 C++ 库(DDS / FastRTPS / Cyclone / MoveIt2),在 Windows 本机 pip 装 rclpy 经常飘红。容器化后,所有人跑同一镜像,问题统一在镜像里修复。
|
||||
|
||||
### 设计思想: 教科书写法
|
||||
|
||||
每篇文档**不重复 ROS2 官方教程**,而是:
|
||||
|
||||
1. **讲什么 (What)** — 这个机制解决什么问题。
|
||||
2. **为什么 (Why)** — 为什么 ROS2 这样设计,设计稿原文引用。
|
||||
3. **怎么用 (How)** — API 列表 + 最小可运行示例。
|
||||
4. **怎么测 (Test)** — 单元测试 + 集成测试代码。
|
||||
5. **怎么错 (Pitfalls)** — 踩过的坑 + 故障排查表。
|
||||
6. **学什么 (Next)** — 进阶阅读 + 相关论文。
|
||||
|
||||
---
|
||||
|
||||
## Level 1: ROS2 基础机制 (Foundation)
|
||||
|
||||
> **核心目标**: 完整理解 ROS2 的 12 大基础机制,能独立写节点 + launch 文件 + 自定义接口 + 生命周期管理。
|
||||
|
||||
### 12 大机制清单
|
||||
|
||||
| # | 机制 | 当前包 | 文档 |
|
||||
|---|---|---|---|
|
||||
| 1 | 节点 (Node) | `py_pubsub` / `cpp_pubsub` | `10-concepts.md` |
|
||||
| 2 | Topic (发布订阅) | `py_pubsub` / `cpp_pubsub` | `20-topics.md` |
|
||||
| 3 | Service (请求响应) | `py_srv` | `30-services.md` |
|
||||
| 4 | Action (目标-反馈-结果) | `py_action_demo` | `40-actions.md` |
|
||||
| 5 | TF2 (坐标变换) | `cpp_robot_tf2` | `50-tf2.md` |
|
||||
| 6 | URDF (机器人模型) | `cpp_robot_tf2` | `60-urdf.md` |
|
||||
| 7 | Launch (启动编排) | `bringup` | `70-launch.md` |
|
||||
| 8 | 参数 (Parameter) | `py_params` ⭐新增 | `15-params.md` ⭐新增 |
|
||||
| 9 | 自定义接口 (.msg/.srv/.action) | `cpp_custom_interface` ⭐新增 | `16-custom-interfaces.md` ⭐新增 |
|
||||
| 10 | 生命周期 (Lifecycle Node) | `py_lifecycle_composable` ⭐新增 | `17-lifecycle.md` ⭐新增 |
|
||||
| 11 | 组合节点 (Composable Node) | `py_lifecycle_composable` ⭐新增 | `18-composable.md` ⭐新增 |
|
||||
| 12 | QoS (服务质量) | `cpp_qos_demo` ⭐新增 | `19-qos.md` ⭐新增 |
|
||||
| 13 | ros2 bag (录制回放) | `cpp_qos_demo` ⭐新增 | `20-bag.md` ⭐新增 |
|
||||
| 14 | colcon overlay (混合工作空间) | `py_overlay_dds` ⭐新增 | `21-overlay-dds.md` ⭐新增 |
|
||||
| 15 | DDS / RMW (中间件配置) | `py_overlay_dds` ⭐新增 | `21-overlay-dds.md` ⭐新增 |
|
||||
|
||||
⭐新增 = Level 1 完成补齐的 5 个包 + 7 篇文档。
|
||||
|
||||
### Level 1 子目标
|
||||
|
||||
- ✅ 能独立写发布者、订阅者、服务器、客户端、Action server、Action client。
|
||||
- ✅ 能用 TF2 监听 / 广播坐标变换,理解 `lookupTransform` vs `buffer.lookup_transform_async`。
|
||||
- ✅ 能写简单 URDF,理解 `<link>` / `<joint>` / `<inertial>` / `<visual>` / `<collision>`。
|
||||
- ✅ 能写 Python launch 文件,理解 `Node` / `IncludeLaunchDescription` / `LaunchConfiguration` / `PathJoinSubstitution`。
|
||||
- ⭐ **新增** 能声明参数、读参数、参数变化回调、从 YAML 加载。
|
||||
- ⭐ **新增** 能自定义 .msg / .srv / .action 并在节点里使用。
|
||||
- ⭐ **新增** 能用 Lifecycle Node 管理节点状态(configure / activate / cleanup / shutdown)。
|
||||
- ⭐ **新增** 能用 Composable Node 把多个节点装到一个进程。
|
||||
- ⭐ **新增** 能配置 QoS(Reliability / Durability / History / Depth / Deadline / Lifeliness)。
|
||||
- ⭐ **新增** 能用 `ros2 bag` 录制 / 回放 / 信息查询。
|
||||
- ⭐ **新增** 能用 `colcon build --packages-up-to` 做混合工作空间。
|
||||
- ⭐ **新增** 能配 domain ID / static peers / QoS XML,理解 DDS 中间件。
|
||||
|
||||
### Level 1 测试覆盖
|
||||
|
||||
```
|
||||
py_pubsub 4 pytest ✅
|
||||
cpp_pubsub 2 gtest ✅
|
||||
py_srv 1 pytest ✅
|
||||
py_action_demo 1 pytest ✅
|
||||
cpp_robot_tf2 2 gtest ✅
|
||||
py_vision_demo 2 pytest ✅
|
||||
bringup 6 launch ✅
|
||||
py_params 3 pytest ⭐(新增)
|
||||
cpp_custom_interface 3 gtest ⭐(新增)
|
||||
py_lifecycle_composable 3 pytest ⭐(新增)
|
||||
cpp_qos_demo 3 gtest ⭐(新增)
|
||||
py_overlay_dds 2 pytest ⭐(新增)
|
||||
|
||||
总计: 32 用例, 目标 100% 通过
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Level 2: 仿真 + 工具链 (Simulation & Tooling)
|
||||
|
||||
> **核心目标**: 用 MoveIt2 + Nav2 + Gazebo 把 Level 1 的基础机制用在真实仿真机器人上,学会调试 / 监控 / 测试金字塔。
|
||||
|
||||
### Level 2 计划包(下一阶段)
|
||||
|
||||
| 包 | 主题 | 关键概念 |
|
||||
|---|---|---|
|
||||
| `moveit2_demo` | MoveIt2 机械臂运动规划 | SRDF / PlanningScene / OMPL / CartesianPath |
|
||||
| `nav2_demo` | Nav2 移动底盘导航 | Costmap / BT / SLAM / Recovery Behavior |
|
||||
| `gazebo_sim` | Gazebo 仿真器集成 | SDF / Plugins / Sensors / ros2_control |
|
||||
| `ros2_controllers` | ros2_control 控制器 | JointTrajectoryController / GripperController / DiffDriveController |
|
||||
| `ros2_test_demo` | 测试金字塔 | launch_testing / pytest fixtures / gtest mock |
|
||||
|
||||
### Level 2 文档
|
||||
|
||||
- `22-moveit2.md` — MoveIt2 概念 + 编程 API + 调试技巧
|
||||
- `23-nav2.md` — Nav2 概念 + 配置 + 行为树
|
||||
- `24-gazebo.md` — Gazebo Harmonic 集成
|
||||
- `25-ros2-control.md` — ros2_control 硬件抽象
|
||||
- `26-testing-pyramid.md` — 单元 / 集成 / E2E 测试策略
|
||||
|
||||
### Level 2 子目标
|
||||
|
||||
- ✅ 能用 MoveIt2 给 6 自由度机械臂做运动规划。
|
||||
- ✅ 能用 Nav2 给移动底盘做 SLAM + 路径规划。
|
||||
- ✅ 能在 Gazebo 里仿真传感器(Lidar / Camera / IMU)。
|
||||
- ✅ 理解 ros2_control 硬件抽象层。
|
||||
- ✅ 写完整的测试金字塔: 单元 (60%) + 集成 (30%) + E2E (10%)。
|
||||
|
||||
### 推荐硬件(仿真阶段无需)
|
||||
|
||||
仿真阶段**不需要任何硬件**。Gazebo + RViz 足够。
|
||||
|
||||
---
|
||||
|
||||
## Level 3: 机械臂实战 (Real Robot)
|
||||
|
||||
> **核心目标**: 把仿真代码搬到真机,做手眼标定 + 视觉抓取 + 力控,实现"看得见、抓得起、放得稳"。
|
||||
|
||||
### Level 3 计划包(中后期)
|
||||
|
||||
| 包 | 主题 | 关键概念 |
|
||||
|---|---|---|
|
||||
| `arm_hardware` | 真实机械臂驱动 | UR5e / xArm / Franka / Realman 驱动 |
|
||||
| `arm_calibration` | 手眼标定 | Eye-in-Hand / Eye-to-Hand / Tsai-Lenz / Park |
|
||||
| `arm_perception` | 视觉感知 | RGB-D / 点云 / 6D 位姿 / GraspNet |
|
||||
| `arm_grasp` | 抓取规划 | 6-DoF Grasp / suction / parallel jaw |
|
||||
| `arm_trajectory` | 轨迹优化 | 时间最优 / 能量最优 / STOMP / CHOMP |
|
||||
| `arm_force` | 力控 | impedance / admittance / hybrid position-force |
|
||||
|
||||
### Level 3 文档
|
||||
|
||||
- `27-arm-drivers.md` — 主流机械臂驱动对比 + 选型
|
||||
- `28-hand-eye-calib.md` — 手眼标定原理 + 实践
|
||||
- `29-6d-pose.md` — 6D 位姿估计(FoundationPose / GraspNet)
|
||||
- `30-grasp-planning.md` — 抓取规划算法
|
||||
- `31-force-control.md` — 力控原理 + 实践
|
||||
|
||||
### Level 3 推荐硬件(最低配置)
|
||||
|
||||
| 设备 | 型号 | 预算 |
|
||||
|---|---|---|
|
||||
| 机械臂 | xArm6 / UR5e / Realman RM75 | ¥15,000-50,000 |
|
||||
| 夹爪 | 大寰 DH-3 / Robotiq 2F-85 | ¥3,000-8,000 |
|
||||
| RGB-D 相机 | Intel RealSense D435 / Orbbec Gemini 2 | ¥1,500-3,000 |
|
||||
| 标定板 | A4 ArUco 6×6 | ¥50 |
|
||||
| 工控机 | NUC / RDK X5 | ¥3,000-5,000 |
|
||||
|
||||
总计: ¥25,000-70,000(可选配)
|
||||
|
||||
### Level 3 子目标
|
||||
|
||||
- ✅ 能驱动真实机械臂做点到点运动 + 直线运动。
|
||||
- ✅ 做完手眼标定,误差 < 2mm。
|
||||
- ✅ 视觉检测物体 6D 位姿,精度 < 5mm / 5°。
|
||||
- ✅ 抓取成功率 > 80% (已知物体集)。
|
||||
- ✅ 力控下能完成插孔 / 装配任务。
|
||||
|
||||
---
|
||||
|
||||
## Level 4: 具身智能 VLA (Embodied AI)
|
||||
|
||||
> **核心目标**: 用大模型(LLM / VLM)让机器人理解自然语言指令,自主决策动作序列,完成开放域任务。
|
||||
|
||||
### Level 4 计划包(终极目标)
|
||||
|
||||
| 包 | 主题 | 关键概念 |
|
||||
|---|---|---|
|
||||
| `vla_data` | 数据采集 + 处理 | DROID / Open X-Embodiment / RT-1 数据格式 |
|
||||
| `vla_model` | VLA 模型 | OpenVLA / π0 / RT-2 / RoboFlamingo |
|
||||
| `vla_inference` | 推理优化 | RKNN / TensorRT / ONNX / 量化 |
|
||||
| `vla_deployment` | 端到端部署 | PC + RDK X5 + RK3506 三机协同 |
|
||||
|
||||
### Level 4 文档
|
||||
|
||||
- `32-vla-intro.md` — VLA 概念 + 主流模型对比
|
||||
- `33-vla-training.md` — VLA 模型微调 + 数据工程
|
||||
- `34-vla-inference.md` — 边缘推理 + NPU 加速
|
||||
- `35-vla-deployment.md` — 真机部署 + 三机协同
|
||||
|
||||
### Level 4 关键论文(引用)
|
||||
|
||||
1. **RT-2** — Google DeepMind, 2023, "RT-2: Vision-Language-Action Models Transfer Web Knowledge to Robotic Control"
|
||||
2. **OpenVLA** — Stanford / UC Berkeley / Toyota Research, 2024, "OpenVLA: An Open-Source Vision-Language-Action Model"
|
||||
3. **π0** — Physical Intelligence, 2024, "π0: A Foundation Model for Robots"
|
||||
4. **DROID** Stanford, 2024, "DROID: A Large-Scale In-the-Wild Robot Manipulation Dataset"
|
||||
5. **Open X-Embodiment** — Google DeepMind et al., 2023, "Scaling Up Learning Across Many Different Robot Types"
|
||||
|
||||
### Level 4 子目标
|
||||
|
||||
- ✅ 能用 OpenVLA / π0 完成"把红杯子放到桌子左边"这类指令。
|
||||
- ✅ 能用 RDK X5 NPU (5 TOPS) 跑 VLA 推理,延迟 < 500ms。
|
||||
- ✅ 三机协同: PC 跑大模型 / RDK X5 跑感知 / RK3506 跑实时控制。
|
||||
- ✅ 在真实机械臂上完成 10+ 类自然语言指令。
|
||||
|
||||
### Level 4 终极硬件配置
|
||||
|
||||
| 设备 | 角色 | 关键算力 |
|
||||
|---|---|---|
|
||||
| PC (RTX 4090) | 训练 + 大模型推理 | 100+ TOPS |
|
||||
| RDK X5 | 边缘 VLA 推理 | 5 TOPS NPU |
|
||||
| RK3506 ×2 | 实时控制 + 通讯 | 3 核 ARM + 512MB RAM |
|
||||
|
||||
---
|
||||
|
||||
## 硬件清单(汇总)
|
||||
|
||||
| 阶段 | 设备 | 必需 / 可选 |
|
||||
|---|---|---|
|
||||
| L1 | PC + Docker | 必需 |
|
||||
| L2 | PC + Docker | 必需 |
|
||||
| L3 | PC + xArm + RealSense + 标定板 | 必需 |
|
||||
| L3 进阶 | + 夹爪 + 工装 | 可选 |
|
||||
| L4 | PC (RTX 4090) + RDK X5 + RK3506 ×2 + xArm | 必需 |
|
||||
|
||||
---
|
||||
|
||||
## 时间估算
|
||||
|
||||
| Level | 学习时长(每天 2-3 小时) | 关键里程碑 |
|
||||
|---|---|---|
|
||||
| L1 | 4-6 周 | 写自定义接口 + Lifecycle Node |
|
||||
| L2 | 6-8 周 | MoveIt2 给真臂规划 + Gazebo 仿真 |
|
||||
| L3 | 8-12 周 | 手眼标定 + 视觉抓取 + 力控 |
|
||||
| L4 | 12+ 周 | VLA 真机部署 |
|
||||
|
||||
总计: **6-9 个月** 从零到 VLA 真机部署。
|
||||
|
||||
---
|
||||
|
||||
## 推荐阅读 + 引用
|
||||
|
||||
### ROS2 官方文档(权威)
|
||||
|
||||
- [ROS2 Humble 官方文档](https://docs.ros.org/en/humble/index.html)
|
||||
- [ROS2 设计稿 (design.ros2.org)](https://design.ros2.org/) — 必读,讲解为什么这样设计
|
||||
- [ROS2 Concepts](https://docs.ros.org/en/humble/Concepts.html) — 概念总览
|
||||
- [ROS2 Tutorials](https://docs.ros.org/en/humble/Tutorials.html) — 入门教程
|
||||
- [ROS2 QoS 文档](https://docs.ros.org/en/humble/Concepts/About-Quality-of-Service.html)
|
||||
- [ROS2 Lifecycle Node](https://design.ros2.org/articles/node_lifecycle.html)
|
||||
- [ROS2 Composable Node](https://docs.ros.org/en/humble/Concepts/About-Composition.html)
|
||||
- [ROS2 Launch](https://docs.ros.org/en/humble/Tutorials/Launch-system.html)
|
||||
|
||||
### DDS 规范
|
||||
|
||||
- [OMG DDS 规范 v1.4](https://www.omg.org/spec/DDS/1.4/) — QoS 源头
|
||||
- [FastRTPS 文档](https://fast-rtps.docs.eprosima.com/) — 默认 RMW
|
||||
- [Cyclone DDS 文档](https://cyclonedds.io/docs/) — 备选 RMW
|
||||
|
||||
### 机器人学
|
||||
|
||||
- [Probabilistic Robotics (Thrun et al.)](http://www.probabilistic-robotics.org/) — SLAM 基础
|
||||
- [Modern Robotics (Lynch & Park)](https://hades.mech.northwestern.edu/index.php/Modern_Robotics.html) — 运动学 / 动力学
|
||||
|
||||
### 具身智能 / VLA
|
||||
|
||||
- [RT-2 论文](https://arxiv.org/abs/2307.15818)
|
||||
- [OpenVLA 论文](https://arxiv.org/abs/2406.09246)
|
||||
- [π0 论文](https://arxiv.org/abs/2410.24164)
|
||||
- [DROID 数据集](https://droid-dataset.github.io/)
|
||||
- [Open X-Embodiment 数据集](https://robotics-transformer-x.github.io/)
|
||||
|
||||
---
|
||||
|
||||
## 学习方法建议
|
||||
|
||||
### 每日节奏(2-3 小时)
|
||||
|
||||
```
|
||||
30 min: 读本级文档(理解概念)
|
||||
60 min: 跑包内 demo(动手验证)
|
||||
30 min: 改 demo 试错(巩固)
|
||||
30 min: 写学习笔记(记录坑点)
|
||||
30 min: 复习 + 下一节
|
||||
```
|
||||
|
||||
### 提问技巧
|
||||
|
||||
学 ROS2 时遇到问题,优先:
|
||||
1. 看本仓库对应文档末尾"故障排查"表
|
||||
2. 看 ROS2 官方文档对应章节
|
||||
3. `ros2 doctor` 输出
|
||||
4. `rclpy` / `rclcpp` 源码 + 调用栈
|
||||
5. 最后才 Google / Stack Overflow
|
||||
|
||||
### 贡献方式
|
||||
|
||||
学完一段后,欢迎:
|
||||
- 修 bug(测试用例失败的)
|
||||
- 补文档(没说清楚的)
|
||||
- 加 demo(新机制的最小示例)
|
||||
- 翻译(英文版)
|
||||
|
||||
详见 `CONTRIBUTING.md`。
|
||||
|
||||
---
|
||||
|
||||
## 路线图完成度
|
||||
|
||||
```
|
||||
[Level 1] ████████████░░ 95% (5 包 5 文档待补,1 个 todo)
|
||||
[Level 2] ░░░░░░░░░░░░░░░ 0% (规划完成)
|
||||
[Level 3] ░░░░░░░░░░░░░░░ 0% (规划完成)
|
||||
[Level 4] ░░░░░░░░░░░░░░░ 0% (规划完成)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**继续**: 看 `doc/01-quickstart.md` 跑通第一个 demo → `doc/10-concepts.md` 理解 ROS2 概念 → `doc/15-params.md` Level 1 深度内容。
|
||||
@@ -0,0 +1,628 @@
|
||||
# 参数系统 (Parameter) 全解
|
||||
|
||||
> **目标**: 彻底理解 ROS2 参数系统的设计原理、API、配置方式、回调机制,能独立设计参数化节点。
|
||||
>
|
||||
> **阅读时间**: 60-90 分钟
|
||||
>
|
||||
> **前置知识**: 已完成 `py_pubsub`(理解节点 + Topic),已阅读 `10-concepts.md`。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 是什么 (What)](#1-是什么-what)
|
||||
- [2. 为什么需要参数 (Why)](#2-为什么需要参数-why)
|
||||
- [3. 设计原理 (Design)](#3-设计原理-design)
|
||||
- [4. API 全解 (API)](#4-api-全解-api)
|
||||
- [5. 实战代码 (Code)](#5-实战代码-code)
|
||||
- [6. 测试策略 (Test)](#6-测试策略-test)
|
||||
- [7. 进阶玩法 (Advanced)](#7-进阶玩法-advanced)
|
||||
- [8. 故障排查 (Pitfalls)](#8-故障排查-pitfalls)
|
||||
- [9. 推荐阅读 (Further)](#9-推荐阅读-further)
|
||||
|
||||
---
|
||||
|
||||
## 1. 是什么 (What)
|
||||
|
||||
**参数 (Parameter)** 是 ROS2 节点的**运行时配置项**,它有以下特点:
|
||||
|
||||
| 特性 | 说明 |
|
||||
|---|---|
|
||||
| **类型** | 7 种基本类型 + 数组 + 字节数组 |
|
||||
| **生命周期** | 节点启动时声明,运行时可改,节点关闭时销毁 |
|
||||
| **作用范围** | 节点级(每个节点独立) / 全局(全局参数服务) |
|
||||
| **持久化** | 可选,持久化参数在节点重启后恢复 |
|
||||
| **原子性** | `set_parameters_atomically` 保证多参数同时生效 |
|
||||
|
||||
### 7 种基本类型
|
||||
|
||||
```python
|
||||
ParameterType.PARAMETER_BOOL # bool
|
||||
ParameterType.PARAMETER_INTEGER # int
|
||||
ParameterType.PARAMETER_DOUBLE # float
|
||||
ParameterType.PARAMETER_STRING # str
|
||||
ParameterType.PARAMETER_BYTE_ARRAY # bytes
|
||||
ParameterType.PARAMETER_BOOL_ARRAY # List[bool]
|
||||
ParameterType.PARAMETER_INTEGER_ARRAY # List[int]
|
||||
ParameterType.PARAMETER_DOUBLE_ARRAY # List[float]
|
||||
ParameterType.PARAMETER_STRING_ARRAY # List[str]
|
||||
```
|
||||
|
||||
### 与 Topic / Service 的区别
|
||||
|
||||
| 维度 | Parameter | Topic | Service |
|
||||
|---|---|---|---|
|
||||
| **用途** | 配置 | 流式数据 | 请求-响应 |
|
||||
| **频率** | 低(偶尔改) | 高(传感器 ~100Hz) | 单次 |
|
||||
| **持久化** | 可选 | 否 | 否 |
|
||||
| **回调** | on_set_parameters | on_message | on_request |
|
||||
|
||||
---
|
||||
|
||||
## 2. 为什么需要参数 (Why)
|
||||
|
||||
### 2.1 没有参数会怎样
|
||||
|
||||
假设你写了一个相机驱动节点,分辨率硬编码为 `640×480`。换相机后想用 `1920×1080`,只能改源码重编译。
|
||||
|
||||
### 2.2 有参数的好处
|
||||
|
||||
```python
|
||||
class CameraDriver(Node):
|
||||
def __init__(self):
|
||||
super().__init__('camera_driver')
|
||||
self.declare_parameter('width', 640)
|
||||
self.declare_parameter('height', 480)
|
||||
self.declare_parameter('frame_rate', 30)
|
||||
# ...
|
||||
```
|
||||
|
||||
换相机 / 调分辨率,不用改代码,只要改 launch 文件 / YAML / CLI:
|
||||
|
||||
```bash
|
||||
ros2 param set /camera_driver width 1920
|
||||
```
|
||||
|
||||
### 2.3 ROS2 参数的设计目标
|
||||
|
||||
引用 [ROS2 Design: Parameter](https://design.ros2.org/articles/ros_parameters.html):
|
||||
|
||||
> "Parameters are intended to be a way to configure nodes at startup or during runtime, without changing code."
|
||||
|
||||
关键点:
|
||||
|
||||
1. **无代码修改** — 配置与代码解耦
|
||||
2. **支持运行时修改** — 不重启节点也能改
|
||||
3. **类型安全** — 7 种类型 + 校验
|
||||
4. **可回调** — 节点能响应参数变化
|
||||
5. **可序列化** — YAML / 命令行
|
||||
|
||||
---
|
||||
|
||||
## 3. 设计原理 (Design)
|
||||
|
||||
### 3.1 架构图
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Parameter Server │ ← 全局服务 /set_parameters, /list_parameters, /describe_parameters, /get_parameters
|
||||
│ (每个进程内置,无独立进程) │
|
||||
└──────────────────────────────────────┘
|
||||
▲ ▲
|
||||
│ set_parameters │ get_parameters
|
||||
│ │
|
||||
┌──────┴──────┐ ┌──────┴──────┐
|
||||
│ Node A │ │ Node B │
|
||||
│ params: │ │ params: │
|
||||
│ - rate=10 │ │ - topic= │
|
||||
│ - topic=X │ │ - format= │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
注意:**Parameter Server 不是独立进程**,它运行在每个节点进程内的 `rcl` 层(具体是 `rcl_params`)。每个节点都有自己的参数副本,通过 DDS 同步。
|
||||
|
||||
### 3.2 关键概念
|
||||
|
||||
#### a) Declare vs Use
|
||||
|
||||
```python
|
||||
# 第一步:声明(必须)
|
||||
self.declare_parameter('rate', 1.0)
|
||||
|
||||
# 第二步:使用
|
||||
rate = self.get_parameter('rate').value
|
||||
```
|
||||
|
||||
不声明就 `get_parameter` → 抛 `ParameterNotDeclaredException`。
|
||||
|
||||
#### b) On-Set Callback
|
||||
|
||||
```python
|
||||
self.add_on_set_parameters_callback(self._on_change)
|
||||
```
|
||||
|
||||
回调签名:`Callable[[List[Parameter]], SetParametersResult]`。
|
||||
|
||||
返回 `SetParametersResult(successful=True)` → 接受;返回 `(False, reason)` → 拒绝。
|
||||
|
||||
**注意**: 回调是**同步阻塞**的,执行慢的回调会卡住参数设置。
|
||||
|
||||
#### c) Parameter Override
|
||||
|
||||
启动顺序(优先级从高到低):
|
||||
|
||||
```
|
||||
1. CLI: --params-file /path/to/file.yaml
|
||||
2. CLI: -p param_name:=value
|
||||
3. Launch: Node(parameters=[yaml_file])
|
||||
4. YAML 文件: config/params.yaml
|
||||
5. 代码: declare_parameter('name', default_value)
|
||||
```
|
||||
|
||||
最右的默认值优先级最低,CLI / Launch 覆盖它。
|
||||
|
||||
#### d) 持久化参数 (YAML I/O)
|
||||
|
||||
```bash
|
||||
# 保存当前参数
|
||||
ros2 param dump /node_name > saved.yaml
|
||||
|
||||
# 恢复
|
||||
ros2 param load /node_name saved.yaml
|
||||
```
|
||||
|
||||
格式:
|
||||
|
||||
```yaml
|
||||
/node_name:
|
||||
ros__parameters:
|
||||
rate: 5.0
|
||||
topic: "/chatter"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API 全解 (API)
|
||||
|
||||
### 4.1 rclpy API
|
||||
|
||||
#### 声明
|
||||
|
||||
```python
|
||||
declare_parameter(
|
||||
name: str,
|
||||
value: Any = None, # 推断类型
|
||||
descriptor: str = '',
|
||||
ignore_override: bool = False,
|
||||
) -> Parameter
|
||||
```
|
||||
|
||||
#### 读取
|
||||
|
||||
```python
|
||||
get_parameter(name: str) -> Parameter
|
||||
# or
|
||||
get_parameters(names: List[str]) -> List[Parameter]
|
||||
```
|
||||
|
||||
`Parameter` 对象的属性:
|
||||
|
||||
- `.name`: 参数名
|
||||
- `.value`: 参数值(类型推断)
|
||||
- `.type`: ParameterType 枚举
|
||||
- `.descriptor`: 描述符
|
||||
|
||||
#### 设置
|
||||
|
||||
```python
|
||||
set_parameters(parameters: List[Parameter]) -> List[SetParametersResult]
|
||||
```
|
||||
|
||||
或原子性:
|
||||
|
||||
```python
|
||||
set_parameters_atomically(parameters: List[Parameter]) -> SetParametersResult
|
||||
```
|
||||
|
||||
#### 回调
|
||||
|
||||
```python
|
||||
add_on_set_parameters_callback(
|
||||
callback: Callable[[List[Parameter]], SetParametersResult],
|
||||
prepend: bool = False,
|
||||
) -> None
|
||||
|
||||
remove_on_set_parameters_callback(callback) -> None
|
||||
```
|
||||
|
||||
#### 列出 / 描述
|
||||
|
||||
```python
|
||||
list_parameters() -> List[str]
|
||||
describe_parameters(names: List[str]) -> List[ParameterDescriptor]
|
||||
```
|
||||
|
||||
### 4.2 CLI 工具
|
||||
|
||||
```bash
|
||||
# 列出某节点所有参数
|
||||
ros2 param list /node_name
|
||||
|
||||
# 读参数
|
||||
ros2 param get /node_name param_name
|
||||
|
||||
# 设参数
|
||||
ros2 param set /node_name param_name value
|
||||
|
||||
# 导出参数
|
||||
ros2 param dump /node_name
|
||||
|
||||
# 加载参数
|
||||
ros2 param load /node_name saved.yaml
|
||||
```
|
||||
|
||||
### 4.3 Launch 文件传参
|
||||
|
||||
#### 方式 A: 直接传值
|
||||
|
||||
```python
|
||||
Node(
|
||||
package='my_pkg',
|
||||
executable='my_node',
|
||||
parameters=[{
|
||||
'rate': 10.0,
|
||||
'topic': '/chatter',
|
||||
}],
|
||||
)
|
||||
```
|
||||
|
||||
#### 方式 B: 加载 YAML
|
||||
|
||||
```python
|
||||
from launch.substitutions import PathJoinSubstitution
|
||||
from launch_ros.substitutions import FindPackageShare
|
||||
|
||||
config = PathJoinSubstitution([
|
||||
FindPackageShare('my_pkg'),
|
||||
'config',
|
||||
'params.yaml',
|
||||
])
|
||||
|
||||
Node(
|
||||
package='my_pkg',
|
||||
executable='my_node',
|
||||
parameters=[config],
|
||||
)
|
||||
```
|
||||
|
||||
#### 方式 C: CLI 覆盖
|
||||
|
||||
```bash
|
||||
ros2 run my_pkg my_node --ros-args -p rate:=10.0 -p topic:=/new_topic
|
||||
```
|
||||
|
||||
#### 方式 D: 全局参数
|
||||
|
||||
```python
|
||||
from launch_ros.actions import PushRosNamespace
|
||||
|
||||
# 启动时把所有参数推到 /my_ns
|
||||
PushRosNamespace('my_ns')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 实战代码 (Code)
|
||||
|
||||
完整示例见 `src/py_params/`,这里讲关键设计:
|
||||
|
||||
### 5.1 主节点: `param_node.py`
|
||||
|
||||
```python
|
||||
class ParamNode(Node):
|
||||
def __init__(self):
|
||||
super().__init__('param_node')
|
||||
|
||||
# 1. 声明三个参数(类型自动推断)
|
||||
self.declare_parameter('publish_rate', 1.0)
|
||||
self.declare_parameter('topic_name', 'params_chatter')
|
||||
self.declare_parameter('message_prefix', 'Params:')
|
||||
|
||||
# 2. 读取参数
|
||||
topic_name = self.get_parameter('topic_name').value
|
||||
|
||||
# 3. 用参数构造发布者
|
||||
self._pub = self.create_publisher(String, topic_name, 10)
|
||||
|
||||
# 4. 用参数构造定时器
|
||||
rate = self.get_parameter('publish_rate').value
|
||||
self._timer = self.create_timer(1.0 / rate, self._cb)
|
||||
|
||||
# 5. 注册回调
|
||||
self.add_on_set_parameters_callback(self._on_change)
|
||||
```
|
||||
|
||||
### 5.2 回调: 拒绝非法值
|
||||
|
||||
```python
|
||||
def _on_change(self, params):
|
||||
for p in params:
|
||||
if p.name == 'publish_rate' and p.value <= 0.0:
|
||||
return SetParametersResult(
|
||||
successful=False,
|
||||
reason='publish_rate 必须 > 0'
|
||||
)
|
||||
return SetParametersResult(successful=True)
|
||||
```
|
||||
|
||||
### 5.3 YAML 配置: `config/params.yaml`
|
||||
|
||||
```yaml
|
||||
param_node:
|
||||
ros__parameters:
|
||||
publish_rate: 2.0
|
||||
topic_name: "params_chatter"
|
||||
message_prefix: "Configured:"
|
||||
```
|
||||
|
||||
### 5.4 Launch 文件
|
||||
|
||||
```python
|
||||
from launch import LaunchDescription
|
||||
from launch_ros.actions import Node
|
||||
from launch.substitutions import PathJoinSubstitution
|
||||
from launch_ros.substitutions import FindPackageShare
|
||||
|
||||
def generate_launch_description():
|
||||
cfg = PathJoinSubstitution([
|
||||
FindPackageShare('py_params'),
|
||||
'config',
|
||||
'params.yaml',
|
||||
])
|
||||
return LaunchDescription([
|
||||
Node(
|
||||
package='py_params',
|
||||
executable='param_node',
|
||||
parameters=[cfg],
|
||||
output='screen',
|
||||
),
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试策略 (Test)
|
||||
|
||||
### 6.1 单元测试: 声明默认值
|
||||
|
||||
```python
|
||||
def test_param_declaration():
|
||||
rclpy.init()
|
||||
node = ParamNode()
|
||||
assert node.get_parameter('publish_rate').value == 1.0
|
||||
```
|
||||
|
||||
### 6.2 单元测试: 合法 set
|
||||
|
||||
```python
|
||||
new_param = Parameter(
|
||||
name='publish_rate',
|
||||
value=ParameterValue(type=ParameterType.PARAMETER_DOUBLE, double_value=5.0),
|
||||
)
|
||||
result = node.set_parameters([new_param])
|
||||
assert result[0].successful is True
|
||||
```
|
||||
|
||||
### 6.3 单元测试: 非法值被拒绝
|
||||
|
||||
```python
|
||||
bad = Parameter(
|
||||
name='publish_rate',
|
||||
value=ParameterValue(type=ParameterType.PARAMETER_DOUBLE, double_value=-1.0),
|
||||
)
|
||||
result = node.set_parameters([bad])
|
||||
assert result[0].successful is False
|
||||
assert '必须 > 0' in result[0].reason
|
||||
```
|
||||
|
||||
### 6.4 YAML 集成测试
|
||||
|
||||
```python
|
||||
def test_yaml_loadable():
|
||||
with open('config/params.yaml') as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
assert cfg['param_node']['ros__parameters']['publish_rate'] == 2.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 进阶玩法 (Advanced)
|
||||
|
||||
### 7.1 动态重配置 (Dynamic Reconfigure)
|
||||
|
||||
ROS1 时代的 dynamic_reconfigure,在 ROS2 里被参数 + 回调取代。
|
||||
|
||||
例: 实时调 PID 参数:
|
||||
|
||||
```python
|
||||
def _on_change(self, params):
|
||||
for p in params:
|
||||
if p.name == 'kp':
|
||||
self._pid.set_kp(p.value)
|
||||
elif p.name == 'kd':
|
||||
self._pid.set_kd(p.value)
|
||||
return SetParametersResult(successful=True)
|
||||
```
|
||||
|
||||
外部通过 `ros2 param set` 实时调:
|
||||
|
||||
```bash
|
||||
ros2 param set /controller kp 0.5
|
||||
```
|
||||
|
||||
### 7.2 参数回调链 (Callback Chain)
|
||||
|
||||
多个回调按顺序执行,任何一个返回 False 整链失败:
|
||||
|
||||
```python
|
||||
node.add_on_set_parameters_callback(cb_validate) # 校验
|
||||
node.add_on_set_parameters_callback(cb_propagate) # 传给内部子系统
|
||||
```
|
||||
|
||||
### 7.3 全局参数 (Global Parameter)
|
||||
|
||||
通过 `PushRosNamespace` 把所有参数推到命名空间:
|
||||
|
||||
```python
|
||||
from launch_ros.actions import PushRosNamespace
|
||||
|
||||
LaunchDescription([
|
||||
PushRosNamespace('robot1'),
|
||||
Node(package='cam', executable='driver'),
|
||||
])
|
||||
# 启动后参数命名:/robot1/cam/driver/...
|
||||
```
|
||||
|
||||
### 7.4 参数文件覆盖
|
||||
|
||||
启动顺序优先级(从高到低):
|
||||
|
||||
```
|
||||
1. --params-file (CLI)
|
||||
2. -p name:=value (CLI)
|
||||
3. Launch(parameters=...)
|
||||
4. YAML 文件
|
||||
5. declare_parameter 默认值
|
||||
```
|
||||
|
||||
多个 YAML 文件,后面的覆盖前面的(数组传参)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 故障排查 (Pitfalls)
|
||||
|
||||
### 8.1 `ParameterNotDeclaredException`
|
||||
|
||||
**症状**: `get_parameter` 抛异常。
|
||||
|
||||
**原因**: 没 `declare_parameter` 就 `get_parameter`。
|
||||
|
||||
**解决**:
|
||||
|
||||
```python
|
||||
# 错误
|
||||
def __init__(self):
|
||||
rate = self.get_parameter('rate').value # 💥
|
||||
|
||||
# 正确
|
||||
def __init__(self):
|
||||
self.declare_parameter('rate', 1.0) # 先声明
|
||||
rate = self.get_parameter('rate').value # 再 get
|
||||
```
|
||||
|
||||
### 8.2 回调未触发
|
||||
|
||||
**症状**: `ros2 param set` 后回调没反应。
|
||||
|
||||
**原因**: 多个回调,前一个返回 False 短路了。
|
||||
|
||||
**解决**: 检查每个回调的返回值,所有都要 `successful=True`。
|
||||
|
||||
### 8.3 YAML 没加载
|
||||
|
||||
**症状**: 启动后参数是默认值,不是 YAML 里的值。
|
||||
|
||||
**原因**: YAML 路径错 / 节点名不匹配。
|
||||
|
||||
**解决**:
|
||||
|
||||
```bash
|
||||
# 检查 YAML 是否被安装到 share/
|
||||
ros2 pkg prefix py_params
|
||||
# 查看 install/py_params/share/py_params/config/params.yaml
|
||||
|
||||
# 启动时打印实际加载的参数
|
||||
ros2 param list /param_node # 看实际值
|
||||
```
|
||||
|
||||
### 8.4 浮点精度
|
||||
|
||||
**症状**: `ros2 param set rate 0.1` 后实际值是 `0.10000000149...`。
|
||||
|
||||
**原因**: IEEE 754 浮点表示。
|
||||
|
||||
**解决**: 容忍误差,或在回调里 `round(p.value, 3)`。
|
||||
|
||||
### 8.5 数组参数类型不匹配
|
||||
|
||||
**症状**: `get_parameter` 抛 `ParameterTypeMismatchException`。
|
||||
|
||||
**原因**: YAML 写 `'topic'`(字符串)但代码期望 `topic_name` 是字符串数组。
|
||||
|
||||
**解决**: 检查 YAML 缩进和 `[]` / 引号:
|
||||
|
||||
```yaml
|
||||
# 字符串数组
|
||||
names: ["a", "b", "c"]
|
||||
# 或
|
||||
names: ['a', 'b', 'c']
|
||||
```
|
||||
|
||||
### 8.6 Callback 阻塞导致 hang
|
||||
|
||||
**症状**: `ros2 param set` 卡死。
|
||||
|
||||
**原因**: 回调里有阻塞 I/O(网络 / 大文件)。
|
||||
|
||||
**解决**: 把阻塞操作放后台线程,回调只做校验。
|
||||
|
||||
### 8.7 Atomically vs 普通 set
|
||||
|
||||
**症状**: 多参数同时设置,部分生效部分失败。
|
||||
|
||||
**解决**: 用 `set_parameters_atomically`,要么全成功要么全失败。
|
||||
|
||||
```python
|
||||
result = node.set_parameters_atomically([
|
||||
Parameter(name='kp', value=...),
|
||||
Parameter(name='kd', value=...),
|
||||
])
|
||||
if not result.successful:
|
||||
self.get_logger().error(f'set failed: {result.reason}')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 推荐阅读 (Further)
|
||||
|
||||
### 官方文档
|
||||
|
||||
- [ROS2 Parameter Design](https://design.ros2.org/articles/ros_parameters.html) — 设计稿,必读
|
||||
- [ROS2 Humble Parameter Tutorial](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Parameters/Understanding-ROS2-Parameters.html)
|
||||
- [ROS2 CLI: ros2 param](https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Using-ROS2-CLI-Tools.html)
|
||||
- [rclpy: Parameter class](https://docs.ros2.org/en/latest/rclpy_api/rclpy.parameter.html)
|
||||
- [rcl_interfaces.msg](https://github.com/ros2/rcl_interfaces) — Parameter / ParameterValue / SetParametersResult 消息定义
|
||||
|
||||
### 相关 RFC / 设计稿
|
||||
|
||||
- [ROS2 Design: Parameter Validation](https://design.ros2.org/articles/ros_parameters.html#parameter-validation)
|
||||
- [ROS2 Design: On-Set Parameters Callback](https://design.ros2.org/articles/ros_parameters.html#on-set-parameters-callback)
|
||||
|
||||
### 进阶话题
|
||||
|
||||
- **与 ros2_control 集成**: hardware_interface 启动时从 YAML 加载控制器参数
|
||||
- **与 MoveIt2 集成**: PlanningScene 用参数配置避障
|
||||
- **与 Nav2 集成**: 行为树参数、Costmap 参数全部走 ROS2 参数
|
||||
|
||||
### 实战例子
|
||||
|
||||
- [ros2/demos: topic_monitor](https://github.com/ros2/demos/blob/humble/demo_nodes_py/demo_nodes_py/topics/topic_monitor.py)
|
||||
- [turtlebot3: 参数化](https://github.com/ROBOTIS-GIT/turtlebot3/blob/humble/turtlebot3_node/src/turtlebot3_node.cpp)
|
||||
|
||||
---
|
||||
|
||||
## 一句话总结
|
||||
|
||||
> **ROS2 参数 = 节点的"配置项",从 launch / YAML / CLI 传入,运行时可改,回调里能拒绝非法值。设计目标是"配置与代码解耦"。**
|
||||
|
||||
下一节: `doc/16-custom-interfaces.md` 学习自定义 .msg / .srv / .action。
|
||||
@@ -0,0 +1,196 @@
|
||||
# 16 · 自定义接口 (.msg / .srv / .action) 完全指南
|
||||
|
||||
> **目标**: 理解 ROS2 自定义接口的设计原理、文件格式、rosidl 工具链、能独立写 .msg / .srv / .action。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 为什么需要自定义](#1-为什么需要自定义)
|
||||
- [2. .msg 文件格式](#2-msg-文件格式)
|
||||
- [3. .srv 文件格式](#3-srv-文件格式)
|
||||
- [4. .action 文件格式](#4-action-文件格式)
|
||||
- [5. rosidl_generate_interfaces](#5-rosidl_generate_interfaces)
|
||||
- [6. CMakeLists.txt + package.xml 配置](#6-cmakeliststxt--packagexml-配置)
|
||||
- [7. C++ / Python 使用](#7-c--python-使用)
|
||||
- [8. 设计原则](#8-设计原则)
|
||||
- [9. 推荐阅读](#9-推荐阅读)
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么需要自定义
|
||||
|
||||
ROS2 标准消息(`std_msgs` / `sensor_msgs` / `geometry_msgs`)覆盖了 80% 场景,但有些项目特定需求:
|
||||
|
||||
- 工业传感器读数(温度 + 压力 + 校准日期)
|
||||
- 机器人状态(电量 + 温度 + 故障码)
|
||||
- 业务动作(下单 + 支付 + 物流)
|
||||
|
||||
这些都需要**项目特定**的自定义接口。
|
||||
|
||||
## 2. .msg 文件格式
|
||||
|
||||
```
|
||||
# 注释(以 # 开头)
|
||||
<type> <field_name> # 字段
|
||||
```
|
||||
|
||||
字段类型:
|
||||
|
||||
| ROS 类型 | C++ 类型 | Python 类型 |
|
||||
|---|---|---|
|
||||
| `bool` | `bool` | `bool` |
|
||||
| `int8`/`int16`/`int32`/`int64` | `int8_t`/... | `int` |
|
||||
| `uint8`/.../`uint64` | `uint8_t`/... | `int` |
|
||||
| `float32`/`float64` | `float`/`double` | `float` |
|
||||
| `string` | `std::string` | `str` |
|
||||
| `time`/`duration` | `builtin_interfaces::msg::Time` | `Time` |
|
||||
| 其他消息类型 | `pkg::msg::Type` | `pkg.msg.Type` |
|
||||
| `type[]` | `std::vector<T>` | `List[T]` |
|
||||
| `type[3]` | `std::array<T,3>` | `Tuple[T,T,T]` |
|
||||
|
||||
示例: `msg/SensorReading.msg`
|
||||
|
||||
```
|
||||
std_msgs/Header header
|
||||
string sensor_id
|
||||
string unit
|
||||
float64 value
|
||||
```
|
||||
|
||||
## 3. .srv 文件格式
|
||||
|
||||
```
|
||||
# Request 字段(--- 上)
|
||||
<type> <field_name>
|
||||
---
|
||||
# Response 字段(--- 下)
|
||||
<type> <field_name>
|
||||
```
|
||||
|
||||
示例: `srv/GetCalibration.srv`
|
||||
|
||||
```
|
||||
string sensor_id
|
||||
---
|
||||
float64[9] intrinsic_matrix
|
||||
float64[3] bias
|
||||
string calibration_date
|
||||
bool valid
|
||||
```
|
||||
|
||||
## 4. .action 文件格式
|
||||
|
||||
```
|
||||
# Goal 字段(第 1 段)
|
||||
<type> <field_name>
|
||||
---
|
||||
# Result 字段(第 2 段)
|
||||
<type> <field_name>
|
||||
---
|
||||
# Feedback 字段(第 3 段)
|
||||
<type> <field_name>
|
||||
```
|
||||
|
||||
示例: `action/MoveArm.action`
|
||||
|
||||
```
|
||||
geometry_msgs/PoseStamped target_pose
|
||||
string[] joint_names
|
||||
float32 max_velocity_scaling
|
||||
float32 max_acceleration_scaling
|
||||
---
|
||||
bool success
|
||||
string error_message
|
||||
float64 total_time_sec
|
||||
---
|
||||
float32 progress
|
||||
string current_state
|
||||
```
|
||||
|
||||
## 5. rosidl_generate_interfaces
|
||||
|
||||
ROS2 用 `rosidl` 自动从 .msg/.srv/.action 生成 C++ / Python 代码:
|
||||
|
||||
```cmake
|
||||
find_package(rosidl_default_generators REQUIRED)
|
||||
|
||||
rosidl_generate_interfaces(${PROJECT_NAME}
|
||||
"msg/SensorReading.msg"
|
||||
"srv/GetCalibration.srv"
|
||||
"action/MoveArm.action"
|
||||
DEPENDENCIES std_msgs geometry_msgs
|
||||
ADD_LINTER_TESTS
|
||||
)
|
||||
```
|
||||
|
||||
生成位置:
|
||||
- C++ 头文件:`install/<pkg>/include/<pkg>/msg/<msg_name>.hpp`
|
||||
- Python 模块:`install/<pkg>/lib/python3.10/site-packages/<pkg>/msg/<msg_name>.py`
|
||||
|
||||
## 6. CMakeLists.txt + package.xml 配置
|
||||
|
||||
**`package.xml` 必备**:
|
||||
|
||||
```xml
|
||||
<buildtool_depend>rosidl_default_generators</buildtool_depend>
|
||||
<exec_depend>rosidl_default_runtime</exec_depend>
|
||||
<member_of_group>rosidl_interface_packages</member_of_group>
|
||||
```
|
||||
|
||||
**`CMakeLists.txt` 顺序**:
|
||||
|
||||
```cmake
|
||||
find_package(ament_cmake REQUIRED)
|
||||
find_package(rosidl_default_generators REQUIRED)
|
||||
find_package(std_msgs REQUIRED)
|
||||
find_package(geometry_msgs REQUIRED)
|
||||
|
||||
rosidl_generate_interfaces(${PROJECT_NAME} ...)
|
||||
|
||||
ament_target_dependencies(${PROJECT_NAME}_core rclcpp "rosidl_typesupport_cpp")
|
||||
rosidl_target_interfaces(${PROJECT_NAME}_core ${PROJECT_NAME} "rosidl_typesupport_cpp")
|
||||
```
|
||||
|
||||
## 7. C++ / Python 使用
|
||||
|
||||
### C++
|
||||
|
||||
```cpp
|
||||
#include "cpp_custom_interface/msg/sensor_reading.hpp"
|
||||
|
||||
auto msg = cpp_custom_interface::msg::SensorReading();
|
||||
msg.sensor_id = "imu_0";
|
||||
msg.value = 1.234;
|
||||
publisher_->publish(msg);
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
from cpp_custom_interface.msg import SensorReading
|
||||
|
||||
msg = SensorReading()
|
||||
msg.sensor_id = 'imu_0'
|
||||
msg.value = 1.234
|
||||
publisher.publish(msg)
|
||||
```
|
||||
|
||||
## 8. 设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|---|---|
|
||||
| **优先标准接口** | 90% 场景用 `std_msgs` / `sensor_msgs` |
|
||||
| **字段 snake_case + 单位后缀** | `velocity_mps` 而不是 `v` |
|
||||
| **Header 必备** | 任何有"时间戳"的消息都加 `std_msgs/Header` |
|
||||
| **数组 vs 单值** | `float64[]` 用于多维数据,单值用 `float64` |
|
||||
| **不要嵌指针** | 用 ID (`string object_id`) 而非 `string&` |
|
||||
| **字段命名清晰** | `target_pose` 不是 `tp`,`max_velocity_scaling` 不是 `v_max` |
|
||||
| **Result 加 success + error_message** | client 知道成功还是失败 |
|
||||
|
||||
## 9. 推荐阅读
|
||||
|
||||
- [ROS2 自定义接口官方教程](https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Custom-ROS2-Interfaces.html)
|
||||
- [REP-127: ROS Message 标准](https://www.ros.org/reps/rep-0127.html)
|
||||
- [rosidl 文档](https://design.ros2.org/articles/legacy_interface_definition.html)
|
||||
- [`cpp_custom_interface` 包](../src/cpp_custom_interface/README.md) — 本仓库的演示
|
||||
@@ -0,0 +1,249 @@
|
||||
# 17 · 生命周期节点 (Lifecycle Node) 完全指南
|
||||
|
||||
> **目标**: 理解 ROS2 Lifecycle Node 的设计、状态机、转换机制,能用 Lifecycle Node 管理资源密集型节点。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 是什么](#1-是什么)
|
||||
- [2. 为什么需要](#2-为什么需要)
|
||||
- [3. 状态机](#3-状态机)
|
||||
- [4. 转换回调](#4-转换回调)
|
||||
- [5. Service 接口](#5-service-接口)
|
||||
- [6. Python 实现](#6-python-实现)
|
||||
- [7. C++ 实现](#7-c-实现)
|
||||
- [8. CLI 控制](#8-cli-控制)
|
||||
- [9. 实战模式](#9-实战模式)
|
||||
- [10. 推荐阅读](#10-推荐阅读)
|
||||
|
||||
---
|
||||
|
||||
## 1. 是什么
|
||||
|
||||
**Lifecycle Node** 是 rclcpp / rclpy 的特殊节点基类,提供:
|
||||
|
||||
- **受控状态切换**(configure → activate → deactivate → cleanup → shutdown)
|
||||
- **每种状态都有自己的回调**(on_configure / on_activate 等)
|
||||
- **外部触发转换**(通过 service 调用)
|
||||
|
||||
适用场景:
|
||||
|
||||
- 资源密集型节点(加载 ML 模型、连接硬件、订阅话题)
|
||||
- 需要明确"启动顺序"的复杂系统(相机先 ready,处理节点再 activate)
|
||||
- 安全敏感系统(切换前确认硬件 OK)
|
||||
|
||||
## 2. 为什么需要
|
||||
|
||||
普通节点的缺点:
|
||||
|
||||
```python
|
||||
# ❌ 普通节点 — 启动即订阅 / 订阅即消费 / 死了就完了
|
||||
class MyNode(Node):
|
||||
def __init__(self):
|
||||
super().__init__('my_node')
|
||||
self._sub = self.create_subscription(...) # 一启动就开始消费
|
||||
```
|
||||
|
||||
问题:
|
||||
- 想"暂停"接收?做不到
|
||||
- 想"重新初始化"?得 kill 重启
|
||||
- 想"加载模型失败就停"?只能异常退出
|
||||
|
||||
Lifecycle Node 解决:
|
||||
|
||||
```python
|
||||
# ✅ Lifecycle Node — 显式状态切换
|
||||
class MyLifecycleNode(LifecycleNode):
|
||||
def on_configure(self, state):
|
||||
# 加载模型、分配资源
|
||||
# 失败 → return FAILURE,不会进入 active
|
||||
|
||||
def on_activate(self, state):
|
||||
# 订阅话题、启动定时器
|
||||
# 失败 → return FAILURE,自动 cleanup
|
||||
|
||||
def on_deactivate(self, state):
|
||||
# 停止订阅、暂停定时器(但资源仍在)
|
||||
|
||||
def on_cleanup(self, state):
|
||||
# 释放模型、断开连接(回到 unconfigured)
|
||||
```
|
||||
|
||||
## 3. 状态机
|
||||
|
||||
```
|
||||
configure
|
||||
unconfigured ───────→ inactive
|
||||
▲ │ │ activate
|
||||
│ │ cleanup ▼
|
||||
│ └────────────── active
|
||||
│ │ deactivate
|
||||
└──────────────────────┘
|
||||
|
||||
shutdown(任何状态都可触发)→ finalized
|
||||
```
|
||||
|
||||
四个主要状态:
|
||||
- `unconfigured`: 已创建但未配置
|
||||
- `inactive`: 已配置但未激活
|
||||
- `active`: 完全运行(处理数据)
|
||||
- `finalized`: 终止(不可逆)
|
||||
|
||||
转换事件(transition):
|
||||
- `configure`: unconfigured → inactive
|
||||
- `activate`: inactive → active
|
||||
- `deactivate`: active → inactive
|
||||
- `cleanup`: inactive → unconfigured
|
||||
- `shutdown`: 任何 → finalized
|
||||
|
||||
## 4. 转换回调
|
||||
|
||||
每个转换回调签名: `(state: State) -> TransitionCallbackReturn`
|
||||
|
||||
返回:
|
||||
- `SUCCESS`: 转换成功,进入目标状态
|
||||
- `FAILURE`: 转换失败,回到原状态
|
||||
- `ERROR`: 转换错误,直接进 finalized
|
||||
|
||||
**必须重写**:
|
||||
- `on_configure(state)`
|
||||
- `on_activate(state)`
|
||||
- `on_deactivate(state)`
|
||||
- `on_cleanup(state)`
|
||||
- `on_shutdown(state)`
|
||||
|
||||
**可选重写**:
|
||||
- `on_error(state)`: 错误处理
|
||||
|
||||
## 5. Service 接口
|
||||
|
||||
每个 Lifecycle Node 自动注册两个 service:
|
||||
|
||||
- `/<node>/change_state` (`lifecycle_msgs/srv/ChangeState`)— 触发转换
|
||||
- `/<node>/get_state` (`lifecycle_msgs/srv/GetState`)— 查询状态
|
||||
|
||||
```bash
|
||||
# 查状态
|
||||
ros2 service call /lifecycle_demo_node/get_state lifecycle_msgs/srv/GetState
|
||||
|
||||
# 触发 configure (transition id = 1)
|
||||
ros2 service call /lifecycle_demo_node/change_state \
|
||||
lifecycle_msgs/srv/ChangeState "{transition: {id: 1}}"
|
||||
```
|
||||
|
||||
Transition IDs:
|
||||
|
||||
| ID | 转换 |
|
||||
|---|---|
|
||||
| 0 | configure |
|
||||
| 1 | cleanup |
|
||||
| 2 | activate |
|
||||
| 3 | deactivate |
|
||||
| 4 | shutdown |
|
||||
|
||||
## 6. Python 实现
|
||||
|
||||
```python
|
||||
from rclpy.lifecycle import LifecycleNode, TransitionCallbackReturn
|
||||
|
||||
class LifecycleDemoNode(LifecycleNode):
|
||||
def on_configure(self, state):
|
||||
self._publisher = self.create_lifecycle_publisher(String, 'topic', 10)
|
||||
return TransitionCallbackReturn.SUCCESS
|
||||
|
||||
def on_activate(self, state):
|
||||
self._timer = self.create_timer(0.5, self._publish)
|
||||
return super().on_activate(state)
|
||||
|
||||
def on_deactivate(self, state):
|
||||
self.destroy_timer(self._timer)
|
||||
return super().on_deactivate(state)
|
||||
|
||||
def on_cleanup(self, state):
|
||||
self.destroy_lifecycle_publisher(self._publisher)
|
||||
return TransitionCallbackReturn.SUCCESS
|
||||
```
|
||||
|
||||
## 7. C++ 实现
|
||||
|
||||
```cpp
|
||||
#include "rclcpp_lifecycle/lifecycle_node.hpp"
|
||||
|
||||
class LifecycleDemoNode : public rclcpp_lifecycle::LifecycleNode
|
||||
{
|
||||
public:
|
||||
LifecycleDemoNode() : rclcpp_lifecycle::LifecycleNode("demo") {}
|
||||
|
||||
rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn
|
||||
on_configure(const rclcpp_lifecycle::State &)
|
||||
{
|
||||
publisher_ = this->create_publisher<String>("topic", 10);
|
||||
return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 8. CLI 控制
|
||||
|
||||
```bash
|
||||
# 1. 启 Lifecycle Node
|
||||
ros2 launch my_pkg lifecycle_demo.py
|
||||
|
||||
# 2. 触发 configure
|
||||
ros2 lifecycle set /lifecycle_node configure
|
||||
# (Humble 后 ros2 lifecycle set 直接用,而不是 service call)
|
||||
|
||||
# 3. 触发 activate
|
||||
ros2 lifecycle set /lifecycle_node activate
|
||||
|
||||
# 4. 看状态
|
||||
ros2 lifecycle get /lifecycle_node
|
||||
# 输出:active [3]
|
||||
```
|
||||
|
||||
## 9. 实战模式
|
||||
|
||||
### 模式 1: ML 模型加载
|
||||
|
||||
```python
|
||||
def on_configure(self, state):
|
||||
try:
|
||||
self._model = torch.load('model.pt') # 加载 ML 模型
|
||||
return TransitionCallbackReturn.SUCCESS
|
||||
except FileNotFoundError:
|
||||
return TransitionCallbackReturn.FAILURE # 配置失败,节点不可用
|
||||
|
||||
def on_activate(self, state):
|
||||
self._sub = self.create_subscription(Image, 'image_raw', self._infer, 10)
|
||||
return super().on_activate(state)
|
||||
|
||||
def _infer(self, msg):
|
||||
if self._model is None: return # 不会到这里(因为 activate 前要 configure 成功)
|
||||
result = self._model(msg)
|
||||
...
|
||||
```
|
||||
|
||||
### 模式 2: 硬件连接(相机)
|
||||
|
||||
```python
|
||||
def on_configure(self, state):
|
||||
try:
|
||||
self._camera = cv2.VideoCapture(0)
|
||||
if not self._camera.isOpened():
|
||||
return TransitionCallbackReturn.FAILURE
|
||||
return TransitionCallbackReturn.SUCCESS
|
||||
except Exception:
|
||||
return TransitionCallbackReturn.FAILURE
|
||||
|
||||
def on_cleanup(self, state):
|
||||
if self._camera:
|
||||
self._camera.release()
|
||||
return TransitionCallbackReturn.SUCCESS
|
||||
```
|
||||
|
||||
## 10. 推荐阅读
|
||||
|
||||
- [ROS2 Lifecycle 设计稿](https://design.ros2.org/articles/node_lifecycle.html)
|
||||
- [ROS2 Lifecycle Humble 教程](https://docs.ros.org/en/humble/Tutorials/Intermediate/Launch/Using-Event-Handlers.html)
|
||||
- [`py_lifecycle_composable` 包](../src/py_lifecycle_composable/README.md)
|
||||
@@ -0,0 +1,188 @@
|
||||
# 18 · 组合节点 (Composable Node) 完全指南
|
||||
|
||||
> **目标**: 理解 ROS2 Composable Node 设计,能把多个节点合并到同一进程,降低延迟与开销。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 是什么](#1-是什么)
|
||||
- [2. 为什么需要](#2-为什么需要)
|
||||
- [3. 工作原理](#3-工作原理)
|
||||
- [4. C++ 实现(.so 库)](#4-c-实现so-库)
|
||||
- [5. Container 启动](#5-container-启动)
|
||||
- [6. Python 等价做法](#6-python-等价做法)
|
||||
- [7. 性能对比](#7-性能对比)
|
||||
- [8. 何时用](#8-何时用)
|
||||
- [9. 推荐阅读](#9-推荐阅读)
|
||||
|
||||
---
|
||||
|
||||
## 1. 是什么
|
||||
|
||||
**Composable Node** = 把多个 ROS 节点装到**同一个进程**(共享内存)。
|
||||
|
||||
```
|
||||
传统模式:
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Process1 │ │ Process2 │ │ Process3 │
|
||||
│ Node A │ │ Node B │ │ Node C │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
│ DDS │ │ DDS │ │ DDS │
|
||||
└─────┴────────┴────────┴────────┘
|
||||
(跨进程通信,微秒级延迟)
|
||||
|
||||
Composable 模式:
|
||||
┌────────────────────────────┐
|
||||
│ Single Process │
|
||||
│ Node A │ Node B │ Node C │
|
||||
│ (shared memory) │
|
||||
└────────────────────────────┘
|
||||
│ DDS │
|
||||
(只有出/入本进程时走 DDS)
|
||||
```
|
||||
|
||||
## 2. 为什么需要
|
||||
|
||||
| 场景 | 传统模式延迟 | Composable 延迟 | 提升 |
|
||||
|---|---|---|---|
|
||||
| 5 节点 pipeline | ~500μs | ~50μs | **10x** |
|
||||
| 高频 sensor fusion | ~1ms | ~100μs | **10x** |
|
||||
| 大量小消息 | 频繁拷贝 | 共享指针 | **CPU 降 30%** |
|
||||
|
||||
其他好处:
|
||||
- 启动快(避免 fork)
|
||||
- 内存共享(零拷贝)
|
||||
- 调试简单(单进程,单 gdb)
|
||||
|
||||
## 3. 工作原理
|
||||
|
||||
ROS2 Composable = C++ **共享库(.so)** + **Container 进程**。
|
||||
|
||||
1. 把节点代码编译成 .so 库(`libmy_component.so`)
|
||||
2. Container 进程(`component_container_mt`)加载 .so
|
||||
3. Container 实例化组件(无需 fork)
|
||||
4. 组件间用**进程内 publish/subscribe**(不经过 DDS)
|
||||
|
||||
## 4. C++ 实现(.so 库)
|
||||
|
||||
**`my_pkg/src/my_component.cpp`**:
|
||||
|
||||
```cpp
|
||||
#include "rclcpp_components/register_node_macro.hpp"
|
||||
|
||||
class MyComponent : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
explicit MyComponent(const rclcpp::NodeOptions & options)
|
||||
: Node("my_component", options) {}
|
||||
};
|
||||
|
||||
RCLCPP_COMPONENTS_REGISTER_NODE(MyComponent)
|
||||
```
|
||||
|
||||
**`CMakeLists.txt`**:
|
||||
|
||||
```cmake
|
||||
add_library(my_component SHARED src/my_component.cpp)
|
||||
ament_target_dependencies(my_component rclcpp)
|
||||
rclcpp_components_register_node(my_component "my_component")
|
||||
|
||||
install(TARGETS my_component
|
||||
ARCHIVE DESTINATION lib
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
```
|
||||
|
||||
## 5. Container 启动
|
||||
|
||||
```bash
|
||||
# 1. 单线程 container(调试用)
|
||||
ros2 component standalone --container-type standalone
|
||||
|
||||
# 2. 多线程 container(生产用)
|
||||
ros2 component standalone --container-type multithreaded
|
||||
|
||||
# 3. 在已有 container 里加载组件
|
||||
ros2 component load <container_name> <package_name> <component_name>
|
||||
|
||||
# 例:
|
||||
ros2 component load /ComponentManager my_pkg my_component
|
||||
```
|
||||
|
||||
### Launch 文件
|
||||
|
||||
```python
|
||||
from launch_ros.actions import ComposableNodeContainer
|
||||
from launch_ros.descriptions import ComposableNode
|
||||
|
||||
container = ComposableNodeContainer(
|
||||
name='my_container',
|
||||
namespace='',
|
||||
package='rclcpp_components',
|
||||
executable='component_container_mt',
|
||||
composable_node_descriptions=[
|
||||
ComposableNode(
|
||||
package='my_pkg',
|
||||
plugin='my_pkg::MyComponent',
|
||||
name='node_a',
|
||||
),
|
||||
ComposableNode(
|
||||
package='my_pkg',
|
||||
plugin='my_pkg::MyComponent',
|
||||
name='node_b',
|
||||
),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
## 6. Python 等价做法
|
||||
|
||||
Python **不支持**真正的 Composable Node(必须用 C++ .so)。但有等价做法:
|
||||
|
||||
```python
|
||||
# 同进程多节点(共享内存,但仍走 DDS 内部)
|
||||
import rclpy
|
||||
from rclpy.executors import MultiThreadedExecutor
|
||||
|
||||
rclpy.init()
|
||||
node_a = NodeA()
|
||||
node_b = NodeB()
|
||||
|
||||
executor = MultiThreadedExecutor(num_threads=4)
|
||||
executor.add_node(node_a)
|
||||
executor.add_node(node_b)
|
||||
executor.spin()
|
||||
```
|
||||
|
||||
Python 多节点同进程 + MultiThreadedExecutor 是 ROS2 Python 等价的 Composable 做法。
|
||||
|
||||
## 7. 性能对比
|
||||
|
||||
| 维度 | 传统多进程 | C++ Composable | Python 多线程 |
|
||||
|---|---|---|---|
|
||||
| 启动时间 | 慢(每个进程 fork) | 快(动态加载) | 中 |
|
||||
| 进程间延迟 | ~100μs (DDS) | ~5μs (shared mem) | ~10μs |
|
||||
| 内存 | 每进程独立 | 共享 | 共享 |
|
||||
| 调试 | gdb attach 多个 | gdb 单进程 | gdb 单进程 |
|
||||
| 灵活性 | 高(可单独 kill) | 低(同进程) | 低 |
|
||||
|
||||
## 8. 何时用
|
||||
|
||||
**用 Composable**:
|
||||
- 同一 pipeline 多个节点(image → process → control)
|
||||
- 高频消息流(>100Hz)
|
||||
- 延迟敏感(机器人控制回路)
|
||||
|
||||
**不用 Composable**:
|
||||
- 节点可独立部署(某些在 PC,某些在 RK3506)
|
||||
- 需要单独 kill 重启某些节点
|
||||
- 节点崩溃隔离(传统模式崩溃只影响一个进程)
|
||||
|
||||
## 9. 推荐阅读
|
||||
|
||||
- [ROS2 Composition 设计稿](https://design.ros2.org/articles/composition.html)
|
||||
- [ROS2 Humble Composition 教程](https://docs.ros.org/en/humble/Tutorials/Intermediate/Launch/Using-Event-Handlers.html)
|
||||
- [ros2 component CLI](https://docs.ros.org/en/humble/Tutorials/Intermediate/Composition.html)
|
||||
- [`py_lifecycle_composable` 包](../src/py_lifecycle_composable/README.md)
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
# 19 · QoS (服务质量) 完全指南
|
||||
|
||||
> **目标**: 理解 ROS2 QoS 的 5 个维度、兼容规则、9 种常用组合、能独立为节点选 QoS。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 是什么](#1-是什么)
|
||||
- [2. 5 大维度](#2-5-大维度)
|
||||
- [3. 兼容规则](#3-兼容规则)
|
||||
- [4. 9 种常用组合](#4-9-种常用组合)
|
||||
- [5. Python API](#5-python-api)
|
||||
- [6. C++ API](#6-c-api)
|
||||
- [7. rclpy QoSProfile 详解](#7-rclpy-qosprofile-详解)
|
||||
- [8. 调试 QoS 不兼容](#8-调试-qos-不兼容)
|
||||
- [9. 推荐阅读](#9-推荐阅读)
|
||||
|
||||
---
|
||||
|
||||
## 1. 是什么
|
||||
|
||||
**QoS (Quality of Service)** 控制 DDS 通信的可靠性、实时性、持久性等。
|
||||
|
||||
ROS2 默认 QoS = `RELIABLE + VOLATILE + KEEP_LAST(10)`,适合大多数场景。
|
||||
但**高频传感器流** / **控制指令** / **参数发布**等场景需要**自定义 QoS**。
|
||||
|
||||
## 2. 5 大维度
|
||||
|
||||
| 维度 | 取值 | 默认 | 含义 |
|
||||
|---|---|---|---|
|
||||
| **Reliability** | RELIABLE / BEST_EFFORT | RELIABLE | 必须投递 / 丢一帧无所谓 |
|
||||
| **History** | KEEP_LAST(N) / KEEP_ALL | KEEP_LAST(10) | 队列策略 |
|
||||
| **Durability** | VOLATILE / TRANSIENT_LOCAL | VOLATILE | 晚订阅者是否收到旧数据 |
|
||||
| **Deadline** | Duration | ∞ | 最长多久发一次 |
|
||||
| **Lifespan** | Duration | ∞ | 多旧的消息失效 |
|
||||
|
||||
## 3. 兼容规则
|
||||
|
||||
**关键规则** — RELIABLE ↔ BEST_EFFORT 单向兼容:
|
||||
|
||||
| Publisher ↓ \ Subscriber → | RELIABLE | BEST_EFFORT |
|
||||
|---|---|---|
|
||||
| RELIABLE | ✅ | ❌ |
|
||||
| BEST_EFFORT | ✅ | ✅ |
|
||||
|
||||
**为什么 RELIABLE → BEST_EFFORT 不兼容**:
|
||||
- RELIABLE pub 等 ACK(确保投递)
|
||||
- BEST_EFFORT sub 不发 ACK
|
||||
- pub 等不到 ACK → 报 "QoS incompatible"
|
||||
|
||||
**报错样例**:
|
||||
```
|
||||
[WARN] ... New subscription discovered on this topic with incompatible QoS ...
|
||||
```
|
||||
|
||||
## 4. 9 种常用组合
|
||||
|
||||
| Reliability | Durability | History | 适用场景 |
|
||||
|---|---|---|---|
|
||||
| RELIABLE | VOLATILE | KEEP_LAST(10) | **默认 / 跨语言互通基线** |
|
||||
| RELIABLE | VOLATILE | KEEP_LAST(1) | 控制指令(只关心最新) |
|
||||
| RELIABLE | TRANSIENT_LOCAL | KEEP_LAST(1) | 参数 / 配置(晚订阅者也能拿到) |
|
||||
| RELIABLE | VOLATILE | KEEP_ALL | 日志(必须投递,不丢) |
|
||||
| RELIABLE | TRANSIENT_LOCAL | KEEP_ALL | 启动期参数 |
|
||||
| BEST_EFFORT | VOLATILE | KEEP_LAST(1) | 视频流(丢一帧无所谓) |
|
||||
| BEST_EFFORT | VOLATILE | KEEP_LAST(10) | Lidar / 雷达 |
|
||||
| BEST_EFFORT | TRANSIENT_LOCAL | KEEP_LAST(1) | 较老的状态快照 |
|
||||
| SYSTEM_DEFAULT | VOLATILE | KEEP_LAST(10) | 由 RMW 决定 |
|
||||
|
||||
## 5. Python API
|
||||
|
||||
```python
|
||||
from rclpy.qos import (
|
||||
QoSProfile,
|
||||
ReliabilityPolicy,
|
||||
HistoryPolicy,
|
||||
DurabilityPolicy,
|
||||
)
|
||||
|
||||
# 视频流:BEST_EFFORT + 深度 1
|
||||
sensor_qos = QoSProfile(
|
||||
reliability=ReliabilityPolicy.BEST_EFFORT,
|
||||
history=HistoryPolicy.KEEP_LAST,
|
||||
depth=1,
|
||||
)
|
||||
|
||||
# 控制指令:RELIABLE + 深度 1
|
||||
control_qos = QoSProfile(
|
||||
reliability=ReliabilityPolicy.RELIABLE,
|
||||
history=HistoryPolicy.KEEP_LAST,
|
||||
depth=1,
|
||||
)
|
||||
|
||||
# 参数:TRANSIENT_LOCAL(晚订阅者能拿历史)
|
||||
param_qos = QoSProfile(
|
||||
reliability=ReliabilityPolicy.RELIABLE,
|
||||
durability=DurabilityPolicy.TRANSIENT_LOCAL,
|
||||
depth=1,
|
||||
)
|
||||
|
||||
# 用
|
||||
publisher_ = self.create_publisher(String, 'topic', sensor_qos)
|
||||
```
|
||||
|
||||
## 6. C++ API
|
||||
|
||||
```cpp
|
||||
#include "rclcpp/qos.hpp"
|
||||
|
||||
using namespace rclcpp;
|
||||
|
||||
// 视频流
|
||||
auto sensor_qos = QoS(1)
|
||||
.reliability(RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT)
|
||||
.history(RMW_QOS_POLICY_HISTORY_KEEP_LAST);
|
||||
|
||||
// 参数
|
||||
auto param_qos = QoS(1)
|
||||
.reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE)
|
||||
.durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL);
|
||||
|
||||
// 用
|
||||
publisher_ = this->create_publisher<String>("topic", sensor_qos);
|
||||
```
|
||||
|
||||
## 7. rclpy QoSProfile 详解
|
||||
|
||||
完整参数:
|
||||
|
||||
```python
|
||||
QoSProfile(
|
||||
# Reliability
|
||||
reliability=ReliabilityPolicy.RELIABLE, # or BEST_EFFORT, SYSTEM_DEFAULT
|
||||
|
||||
# History
|
||||
history=HistoryPolicy.KEEP_LAST, # or KEEP_ALL
|
||||
depth=10, # 仅 KEEP_LAST 时有效
|
||||
|
||||
# Durability
|
||||
durability=DurabilityPolicy.VOLATILE, # or TRANSIENT_LOCAL
|
||||
|
||||
# Deadline(可省)
|
||||
deadline=Duration(seconds=0), # 0 = 无
|
||||
|
||||
# Lifespan(可省)
|
||||
lifespan=Duration(seconds=0), # 0 = 无
|
||||
|
||||
# Liveliness(可省)
|
||||
liveliness=LivelinessPolicy.SYSTEM_DEFAULT,
|
||||
liveliness_lease_duration=Duration(seconds=0),
|
||||
)
|
||||
```
|
||||
|
||||
## 8. 调试 QoS 不兼容
|
||||
|
||||
**症状**: `ros2 topic echo` 没输出,`ros2 topic info -v` 显示 QoS incompatible 警告。
|
||||
|
||||
**调试步骤**:
|
||||
|
||||
```bash
|
||||
# 1) 看 pub/sub 各自 QoS
|
||||
ros2 topic info /topic -v
|
||||
|
||||
# 2) 检查 pub/sub 是否在同 domain
|
||||
echo $ROS_DOMAIN_ID
|
||||
|
||||
# 3) 看具体 incompatibility
|
||||
# 错误信息: PolicyKind=RMW_QOS_POLICY_RELIABILITY
|
||||
```
|
||||
|
||||
**修法**: 改一边 QoS 匹配,或用 SYSTEM_DEFAULT。
|
||||
|
||||
## 9. 推荐阅读
|
||||
|
||||
- [ROS2 QoS 设计稿](https://design.ros2.org/articles/qos.html)
|
||||
- [ROS2 Humble QoS 文档](https://docs.ros.org/en/humble/Concepts/About-Quality-of-Service.html)
|
||||
- [OMG DDS 规范 v1.4](https://www.omg.org/spec/DDS/1.4/) — QoS 源头
|
||||
- [FastDDS QoS 配置](https://fast-dds.docs.eprosima.com/)
|
||||
- [`cpp_qos_demo` 包](../src/cpp_qos_demo/README.md)
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
# 20 · ros2 bag 录制与回放 完全指南
|
||||
|
||||
> **目标**: 掌握 ros2 bag 命令行,能录制 topic、回放、调试、训练数据采集。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 是什么](#1-是什么)
|
||||
- [2. 录制 topic](#2-录制-topic)
|
||||
- [3. 回放 topic](#3-回放-topic)
|
||||
- [4. 查看包信息](#4-查看包信息)
|
||||
- [5. 转换格式](#5-转换格式)
|
||||
- [6. QoS 与 bag](#6-qos-与-bag)
|
||||
- [7. VLA 训练数据采集](#7-vla-训练数据采集)
|
||||
- [8. 推荐阅读](#8-推荐阅读)
|
||||
|
||||
---
|
||||
|
||||
## 1. 是什么
|
||||
|
||||
**ros2 bag** = ROS2 自带的 topic 录制 / 回放工具,生成 `.db3` (SQLite) 文件 + `metadata.yaml`。
|
||||
|
||||
用途:
|
||||
- 调试:录一段数据,反复回放调试算法
|
||||
- 测试:用录的数据做单元测试
|
||||
- 训练:VLA / 机器学习数据采集
|
||||
- 回归:确保算法在新数据上仍 work
|
||||
|
||||
## 2. 录制 topic
|
||||
|
||||
```bash
|
||||
# 录制所有 topic
|
||||
ros2 bag record -a -o my_bag
|
||||
|
||||
# 录制指定 topic
|
||||
ros2 bag record /chatter /tf /joint_states -o my_bag
|
||||
|
||||
# 指定压缩格式
|
||||
ros2 bag record /chatter -o my_bag --compression-mode file --compression-format zstd
|
||||
|
||||
# 限制时长
|
||||
ros2 bag record /chatter -o my_bag --duration 30 # 30 秒
|
||||
|
||||
# 限制大小
|
||||
ros2 bag record /chatter -o my_bag --max-bag-size 100000000 # 100 MB
|
||||
|
||||
# 排除 topic
|
||||
ros2 bag record -a -o my_bag --exclude "/_.*"
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
my_bag/
|
||||
├── metadata.yaml
|
||||
└── my_bag_0.db3
|
||||
```
|
||||
|
||||
## 3. 回放 topic
|
||||
|
||||
```bash
|
||||
# 回放(需要原始节点还在,否则消息无 subscriber)
|
||||
ros2 bag play my_bag
|
||||
|
||||
# 循环回放
|
||||
ros2 bag play my_bag --loop
|
||||
|
||||
# 调整速率(2 倍速)
|
||||
ros2 bag play my_bag --rate 2.0
|
||||
|
||||
# 从中间开始
|
||||
ros2 bag play my_bag --start-offset 10 # 10 秒处开始
|
||||
|
||||
# 延迟发布
|
||||
ros2 bag play my_bag --delay 1.0 # 每条消息延迟 1 秒
|
||||
```
|
||||
|
||||
## 4. 查看包信息
|
||||
|
||||
```bash
|
||||
# 概览
|
||||
ros2 bag info my_bag
|
||||
|
||||
# YAML 格式详情
|
||||
ros2 bag info my_bag --yaml
|
||||
|
||||
# 单个 topic 详情
|
||||
ros2 bag info my_bag -t /chatter
|
||||
```
|
||||
|
||||
输出:
|
||||
|
||||
```
|
||||
Files: my_bag_0.db3
|
||||
Bag size: 1.2 MiB
|
||||
Storage id: sqlite3
|
||||
Duration: 10.05s
|
||||
Start: Aug 4 2026 14:00:00.123
|
||||
End: Aug 4 2026 14:00:10.123
|
||||
Messages: 100
|
||||
Topic information: MessageType Count
|
||||
std_msgs/String 100
|
||||
```
|
||||
|
||||
## 5. 转换格式
|
||||
|
||||
### 导出 CSV
|
||||
|
||||
```bash
|
||||
ros2 bag info my_bag --yaml > my_bag_info.yaml
|
||||
```
|
||||
|
||||
### 用 rosbags 库(Python)
|
||||
|
||||
```python
|
||||
from rosbags.rosbag2 import Reader
|
||||
|
||||
with Reader('my_bag') as reader:
|
||||
for msg in reader.messages():
|
||||
topic = msg.topic
|
||||
timestamp = msg.timestamp
|
||||
data = msg.message
|
||||
...
|
||||
```
|
||||
|
||||
## 6. QoS 与 bag
|
||||
|
||||
**关键**: 回放时 subscriber 的 QoS 必须 **兼容** publisher 的 QoS(录制时的 QoS)。
|
||||
|
||||
```bash
|
||||
# 默认 bag 会用 RELIABLE 录制,如果你用 BEST_EFFORT subscriber,会报错
|
||||
# 解决:用 --qos-profile-overrides 覆盖回放 QoS
|
||||
ros2 bag play my_bag --qos-profile-overrides-path qos_override.yaml
|
||||
```
|
||||
|
||||
`qos_override.yaml`:
|
||||
|
||||
```yaml
|
||||
/chatter:
|
||||
reliability: best_effort
|
||||
history: keep_last
|
||||
depth: 1
|
||||
```
|
||||
|
||||
## 7. VLA 训练数据采集
|
||||
|
||||
OpenVLA / π0 等模型需要大量 (image, instruction, action) 三元组。ROS2 bag 录制:
|
||||
|
||||
```bash
|
||||
# 录制图像 + 关节 + 指令
|
||||
ros2 bag record \
|
||||
/camera/color/image_raw \
|
||||
/camera/depth/color/points \
|
||||
/joint_states \
|
||||
/ee_pose \
|
||||
/task_instruction \
|
||||
-o vla_episode_001
|
||||
```
|
||||
|
||||
回放 + 转 VLA 训练格式:
|
||||
|
||||
```python
|
||||
from rosbags.rosbag2 import Reader
|
||||
from cv_bridge import CvBridge
|
||||
import numpy as np
|
||||
|
||||
bridge = CvBridge()
|
||||
samples = []
|
||||
|
||||
with Reader('vla_episode_001') as reader:
|
||||
for msg in reader.messages():
|
||||
if msg.topic == '/camera/color/image_raw':
|
||||
image = bridge.imgmsg_to_cv2(msg.message, 'bgr8')
|
||||
samples.append({'image': image, 'timestamp': msg.timestamp})
|
||||
```
|
||||
|
||||
## 8. 推荐阅读
|
||||
|
||||
- [ROS2 bag 命令行](https://docs.ros.org/en/humble/Tutorials/Beginner-Client-Libraries/Recording-And-Playing-Back-Data.html)
|
||||
- [rosbags Python 库](https://github.com/idx-lab/rosbags)
|
||||
- [Foxglove Studio(可视化 bag)](https://studio.foxglove.dev/)
|
||||
@@ -0,0 +1,214 @@
|
||||
# 21 · DDS 配置 + colcon overlay 完全指南
|
||||
|
||||
> **目标**: 理解 ROS2 DDS 中间件配置、colcon overlay 混合工作空间,能为多机 / 跨网段部署做正确配置。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. DDS 是什么](#1-dds-是什么)
|
||||
- [2. RMW 实现](#2-rmw-实现)
|
||||
- [3. 关键环境变量](#3-关键环境变量)
|
||||
- [4. domain ID 隔离](#4-domain-id-隔离)
|
||||
- [5. 跨网段发现](#5-跨网段发现)
|
||||
- [6. Cyclone DDS 配置](#6-cyclone-dds-配置)
|
||||
- [7. colcon overlay](#7-colcon-overlay)
|
||||
- [8. 三机部署实操](#8-三机部署实操)
|
||||
- [9. 推荐阅读](#9-推荐阅读)
|
||||
|
||||
---
|
||||
|
||||
## 1. DDS 是什么
|
||||
|
||||
**DDS (Data Distribution Service)** = OMG 制定的实时通信中间件标准。
|
||||
ROS2 默认用 DDS 做底层通信,不同 RMW 实现:
|
||||
- `rmw_fastrtps_cpp`(默认,Fast DDS)
|
||||
- `rmw_cyclonedds_cpp`(Cyclone DDS)
|
||||
|
||||
DDS 提供:
|
||||
- 自动节点发现(基于 UDP multicast)
|
||||
- 多种 QoS
|
||||
- 实时性(零拷贝、共享内存)
|
||||
|
||||
## 2. RMW 实现
|
||||
|
||||
| RMW | 包 | 适用 |
|
||||
|---|---|---|
|
||||
| `rmw_fastrtps_cpp` | `ros-humble-rmw-fastrtts-cpp` | 通用,默认 |
|
||||
| `rmw_cyclonedds_cpp` | `ros-humble-rmw-cyclonedds-cpp` | 跨网段 / Xenomai 实时 |
|
||||
|
||||
切换:
|
||||
|
||||
```bash
|
||||
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp
|
||||
# 然后 colcon build + 启动节点
|
||||
```
|
||||
|
||||
**注意**: 切换 RMW 后必须 `rm -rf build/ install/ log/` 再 build,否则 CMake 配置缓存导致 link 错误。
|
||||
|
||||
## 3. 关键环境变量
|
||||
|
||||
| 变量 | 用途 | 默认 |
|
||||
|---|---|---|
|
||||
| `ROS_DOMAIN_ID` | DDS 域 ID(0-232) | 0 |
|
||||
| `RMW_IMPLEMENTATION` | RMW 实现 | `rmw_fastrtts_cpp` |
|
||||
| `ROS_STATIC_PEERS` | 跨网段单播发现 | `<unset>` |
|
||||
| `ROS_DISCOVERY_SERVER` | 集中式发现服务 | `<unset>` |
|
||||
| `ROS_LOCALHOST_ONLY` | 仅本机 | 0 |
|
||||
| `CYCLONE_DDS_URI` | Cyclone DDS XML 配置 URI | `<unset>` |
|
||||
| `FASTRTPS_DEFAULT_PROFILES_FILE` | FastDDS XML 配置路径 | `<unset>` |
|
||||
|
||||
## 4. domain ID 隔离
|
||||
|
||||
同 `ROS_DOMAIN_ID` 才能互通,改 ID 就隔离:
|
||||
|
||||
```bash
|
||||
# PC 端
|
||||
ROS_DOMAIN_ID=42 ros2 launch my_pkg demo.py
|
||||
|
||||
# RK3506 端
|
||||
ROS_DOMAIN_ID=42 ros2 launch my_pkg demo.py
|
||||
# 两端可见
|
||||
|
||||
# 改 ID 后不可见
|
||||
ROS_DOMAIN_ID=43 ros2 launch my_pkg demo.py
|
||||
```
|
||||
|
||||
**注意**: 域 ID 范围 0-232(ROS2 DDS 协议)。
|
||||
|
||||
## 5. 跨网段发现
|
||||
|
||||
### 问题
|
||||
|
||||
UDP multicast **不跨路由器**,跨网段(如 192.168.1.x ↔ 192.168.2.x)默认看不见。
|
||||
|
||||
### 方案 1: 单播发现 (`ROS_STATIC_PEERS`)
|
||||
|
||||
```bash
|
||||
# PC 端(知道 RK3506 IP)
|
||||
ROS_STATIC_PEERS="192.168.2.10;192.168.2.11" \
|
||||
ros2 launch my_pkg demo.py
|
||||
```
|
||||
|
||||
### 方案 2: Discovery Server
|
||||
|
||||
```bash
|
||||
# PC 端启 discovery server
|
||||
ros2 run discovery_server discovery_server --address 0.0.0.0 --port 11811
|
||||
|
||||
# 客户端
|
||||
export ROS_DISCOVERY_SERVER=192.168.2.10:11811
|
||||
ros2 launch my_pkg demo.py
|
||||
```
|
||||
|
||||
### 方案 3: Cyclone DDS LAN 配置
|
||||
|
||||
`cyclonedds.xml`:
|
||||
|
||||
```xml
|
||||
<CycloneDDS>
|
||||
<NetworkInterface name="eth0" priority="default" multicast="default"/>
|
||||
</CycloneDDS>
|
||||
```
|
||||
|
||||
## 6. Cyclone DDS 配置
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
sudo apt install ros-humble-rmw-cyclonedds-cpp
|
||||
```
|
||||
|
||||
### 配置 XML
|
||||
|
||||
```bash
|
||||
export CYCLONE_DDS_URI=file:///etc/cyclonedds.xml
|
||||
```
|
||||
|
||||
`/etc/cyclonedds.xml`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" version="1.0"?>
|
||||
<CycloneDDS xmlns="https://cdds.io/config">
|
||||
<Domain id="any">
|
||||
<General>
|
||||
<Interfaces>
|
||||
<NetworkInterface autodetermine="true" priority="default" multicast="default"/>
|
||||
</Interfaces>
|
||||
</General>
|
||||
</Domain>
|
||||
</CycloneDDS>
|
||||
```
|
||||
|
||||
## 7. colcon overlay
|
||||
|
||||
**colcon overlay** = 多个 colcon 工作空间叠加,后者覆盖前者(同名包优先用 overlay)。
|
||||
|
||||
### 工作流
|
||||
|
||||
```bash
|
||||
# 主工作空间 base(完整)
|
||||
mkdir -p ~/ros2_main_ws/src
|
||||
cd ~/ros2_main_ws/src
|
||||
git clone <完整仓库> # 或 git pull
|
||||
cd ..
|
||||
colcon build --symlink-install # build 全部
|
||||
|
||||
# overlay 工作空间(只 build 修改的包)
|
||||
mkdir -p ~/ros2_overlay_ws/src
|
||||
cd ~/ros2_overlay_ws/src
|
||||
# 符号链接主工作空间的 src(只覆盖你想改的包)
|
||||
ln -s ~/ros2_main_ws/src/my_pkg .
|
||||
# 修改 my_pkg
|
||||
cd ..
|
||||
colcon build --symlink-install --packages-select my_pkg
|
||||
|
||||
# 激活:先 base 后 overlay
|
||||
source ~/ros2_main_ws/install/setup.bash
|
||||
source ~/ros2_overlay_ws/install/setup.bash # 覆盖
|
||||
```
|
||||
|
||||
### 典型场景
|
||||
|
||||
- **稳定版 + 开发版**:base 用稳定版,overlay 跑新代码
|
||||
- **共享代码 + 私有修改**:base 装共享库,overlay 装你的定制
|
||||
- **CI**:base 装大型依赖,overlay 只 build 改的
|
||||
|
||||
## 8. 三机部署实操
|
||||
|
||||
**典型拓扑**:
|
||||
```
|
||||
PC (192.168.1.10, ROS_DOMAIN_ID=0)
|
||||
│
|
||||
├─ RDK X5 (192.168.1.20, ROS_DOMAIN_ID=0)
|
||||
│
|
||||
└─ RK3506 × 2 (192.168.1.30, 192.168.1.31, ROS_DOMAIN_ID=0)
|
||||
```
|
||||
|
||||
**配置**: 三机同 ROS_DOMAIN_ID + 同 LAN,FastDDS multicast 自动发现。
|
||||
|
||||
**RK3506 精简**: 用 `ros-humble-ros-base`,关 daemon:
|
||||
|
||||
```bash
|
||||
# 装精简包
|
||||
sudo apt install ros-humble-ros-base
|
||||
|
||||
# 关 daemon(省内存)
|
||||
sudo systemctl disable --now ros2-daemon
|
||||
|
||||
# 设置环境变量(~/.bashrc)
|
||||
export ROS_DOMAIN_ID=0
|
||||
source /opt/ros/humble/setup.bash
|
||||
```
|
||||
|
||||
详见 [`doc/100-embedded-deployment.md`](100-embedded-deployment.md)。
|
||||
|
||||
## 9. 推荐阅读
|
||||
|
||||
- [ROS2 DDS 概念](https://docs.ros.org/en/humble/Concepts/About-DDS-Implementations.html)
|
||||
- [FastDDS 文档](https://fast-dds.docs.eprosima.com/)
|
||||
- [Cyclone DDS 文档](https://cyclonedds.io/docs/)
|
||||
- [colcon 文档](https://colcon.readthedocs.io/)
|
||||
- [REP-2002 ROS 2 Design](https://www.ros.org/reps/rep-2002.html)
|
||||
- [`py_overlay_dds` 包](../src/py_overlay_dds/README.md)
|
||||
- [三机部署实操](100-embedded-deployment.md)
|
||||
@@ -0,0 +1,775 @@
|
||||
# 编程规范 (CODING_STYLE) — ROS2 + Python + C++
|
||||
|
||||
> **目标**: 让本仓库所有代码符合 **ROS2 REP-2000** + **PEP 8** + **工业级实践**,
|
||||
> 从第一行代码就**专业、严谨、可维护**,为后续具身智能 / VLA 落地铺平基础。
|
||||
>
|
||||
> **适用范围**: 本仓库所有 Python (rclpy) / C++ (rclcpp) 代码 + 测试 + launch + 文档。
|
||||
>
|
||||
> **权威参考**:
|
||||
> - [ROS2 REP-2000: ROS 2 Design](https://www.ros.org/reps/rep-2002.html)
|
||||
> - [ROS2 Humble Code Style](https://docs.ros.org/en/humble/Contributing/Code-Style-Language-Versions.html)
|
||||
> - [PEP 8](https://peps.python.org/pep-0008/) / [PEP 257](https://peps.python.org/pep-0257/) / [PEP 484](https://peps.python.org/pep-0484/)
|
||||
> - [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html)
|
||||
> - [ROS2 Design: Parameter](https://design.ros2.org/articles/ros_parameters.html)
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 设计原则](#1-设计原则)
|
||||
- [2. Python 规范 (rclpy)](#2-python-规范-rclpy)
|
||||
- [3. C++ 规范 (rclcpp)](#3-c-规范-rclcpp)
|
||||
- [4. 测试规范](#4-测试规范)
|
||||
- [5. ROS2 特定规范](#5-ros2-特定规范)
|
||||
- [6. 文档规范](#6-文档规范)
|
||||
- [7. Git 规范](#7-git-规范)
|
||||
- [8. 错误处理 + 日志规范](#8-错误处理--日志规范)
|
||||
- [9. 反模式 (Anti-Patterns)](#9-反模式-anti-patterns)
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
### 1.1 五大铁律
|
||||
|
||||
| # | 原则 | 含义 |
|
||||
|---|---|---|
|
||||
| 1 | **配置与代码解耦** | 参数 / YAML / launch 传值,代码不硬编码 |
|
||||
| 2 | **错误显式处理** | callback 异常用 try/except + `get_logger().error`,不静默吞 |
|
||||
| 3 | **可测试优先** | 每个包至少 1 个 pytest/gtest,关键路径 100% 覆盖 |
|
||||
| 4 | **接口契约清晰** | type hints / docstring / 错误码 三件套 |
|
||||
| 5 | **命名即文档** | `publisher_` 不是 `pub`,`timer_callback` 不是 `cb` |
|
||||
|
||||
### 1.2 SOLID 简化版
|
||||
|
||||
- **S** (Single Responsibility): 一个节点一个职责,不要混合"传感器读取 + 控制 + 日志上传"
|
||||
- **O** (Open-Closed): 通过参数和 launch 扩展,不改代码
|
||||
- **L** (Liskov): 子类可替换父类(虚函数 override)
|
||||
- **I** (Interface Segregation): 接口小而专,避免上帝节点
|
||||
- **D** (Dependency Inversion): 依赖抽象 (msg / service / action 类型),不依赖实现
|
||||
|
||||
---
|
||||
|
||||
## 2. Python 规范 (rclpy)
|
||||
|
||||
### 2.1 命名 (Naming)
|
||||
|
||||
| 类型 | 规则 | 例子 |
|
||||
|---|---|---|
|
||||
| 模块 | `snake_case` | `publisher_node.py` |
|
||||
| 类 | `PascalCase` | `ChatterPublisher` (不是 `MyPublisher`) |
|
||||
| 节点属性 | `snake_case_`(后缀下划线) | `self.publisher_`, `self.timer_` |
|
||||
| 私有方法 | `_snake_case` | `def _on_timer(self)` |
|
||||
| 常量 | `UPPER_SNAKE_CASE` | `DEFAULT_RATE_HZ = 1.0` |
|
||||
| ROS2 节点名 | `snake_case`,表示功能 | `chatter_publisher` (不是 `node1`) |
|
||||
| ROS2 话题名 | `snake_case`,可加前缀 | `/chatter`, `/robot1/joint_states` |
|
||||
| ROS2 参数名 | `snake_case` | `publish_rate_hz` (不是 `period_ms` 混用) |
|
||||
|
||||
**关键:节点属性后缀下划线**避免与 rclpy 内部方法同名(`timer`, `publisher`, `subscription` 都是 rclpy 内部属性)。
|
||||
|
||||
### 2.2 Type Hints (必填)
|
||||
|
||||
```python
|
||||
# Python 3.10+ 用内置类型,不用 typing.List/Dict
|
||||
from typing import List # 除非必要,否则不导入
|
||||
|
||||
class ChatterPublisher(Node):
|
||||
def __init__(self) -> None:
|
||||
super().__init__('chatter_publisher')
|
||||
self.declare_parameter('publish_rate_hz', 1.0)
|
||||
rate: float = self.get_parameter('publish_rate_hz').value
|
||||
self.publisher_: Publisher[String] = self.create_publisher(String, 'chatter', 10)
|
||||
```
|
||||
|
||||
### 2.3 Docstring (Google Style)
|
||||
|
||||
```python
|
||||
"""ChatterPublisher - 周期性发布 String 到 /chatter 话题。
|
||||
|
||||
设计思想:
|
||||
ROS2 Topic 是异步多对多单向通信,本节点演示:
|
||||
1. 参数声明 + 类型推断
|
||||
2. 周期性发布 + QoS
|
||||
3. 优雅退出(KeyboardInterrupt + rclpy.shutdown)
|
||||
|
||||
参考:
|
||||
- ROS2 设计稿 https://design.ros2.org/articles/topic_and_service.html
|
||||
- QoS 文档 https://docs.ros.org/en/humble/Concepts/About-Quality-of-Service.html
|
||||
"""
|
||||
```
|
||||
|
||||
类/方法的 docstring 模板:
|
||||
|
||||
```python
|
||||
class Foo:
|
||||
"""类的一句话描述。"""
|
||||
|
||||
def method(self, arg: int) -> bool:
|
||||
"""方法的一句话描述。
|
||||
|
||||
Args:
|
||||
arg: 参数描述。
|
||||
|
||||
Returns:
|
||||
返回值描述。
|
||||
|
||||
Raises:
|
||||
ValueError: 何时抛。
|
||||
"""
|
||||
```
|
||||
|
||||
### 2.4 节点模板 (必背)
|
||||
|
||||
```python
|
||||
"""<NodeName> — <一句话描述>"""
|
||||
from typing import List, Optional
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from rclpy.publisher import Publisher
|
||||
from std_msgs.msg import String
|
||||
|
||||
|
||||
class MyNode(Node):
|
||||
"""节点描述。"""
|
||||
|
||||
DEFAULT_RATE_HZ: float = 1.0
|
||||
DEFAULT_TOPIC: str = 'chatter'
|
||||
QUEUE_SIZE: int = 10
|
||||
|
||||
def __init__(self, *, node_name: str = 'my_node') -> None:
|
||||
super().__init__(node_name)
|
||||
|
||||
# 1) 声明参数(类型由默认值推断)+ 描述符
|
||||
self.declare_parameter(
|
||||
'publish_rate_hz', self.DEFAULT_RATE_HZ,
|
||||
descriptor='发布频率 (Hz), 大于 0 的浮点数',
|
||||
)
|
||||
|
||||
# 2) 读取参数 + 构造组件
|
||||
rate: float = self.get_parameter('publish_rate_hz').value
|
||||
self.publisher_: Publisher[String] = self.create_publisher(
|
||||
String, self.DEFAULT_TOPIC, self.QUEUE_SIZE,
|
||||
)
|
||||
period: float = 1.0 / rate if rate > 0 else 1.0
|
||||
self.timer_ = self.create_timer(period, self._on_timer)
|
||||
|
||||
# 3) 内部状态(下划线)
|
||||
self._publish_count: int = 0
|
||||
|
||||
self.get_logger().info(f'MyNode started: rate={rate}Hz')
|
||||
|
||||
def _on_timer(self) -> None:
|
||||
"""定时器回调(下划线=内部方法)。"""
|
||||
msg = String()
|
||||
msg.data = f'Hello #{self._publish_count}'
|
||||
self.publisher_.publish(msg)
|
||||
self._publish_count += 1
|
||||
|
||||
|
||||
def main(args: Optional[List[str]] = None) -> None:
|
||||
"""ROS2 节点入口(标准模板)。"""
|
||||
rclpy.init(args=args)
|
||||
try:
|
||||
node = MyNode()
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
if rclpy.ok():
|
||||
rclpy.shutdown()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
### 2.5 包结构 (ament_python)
|
||||
|
||||
```
|
||||
src/<package_name>/
|
||||
├── package.xml # ROS2 包元数据
|
||||
├── setup.py # Python 包配置 + entry_points
|
||||
├── setup.cfg # ament_python install 路径
|
||||
├── resource/<package_name> # 空文件,只用于 ament 索引
|
||||
├── <package_name>/ # Python 模块
|
||||
│ ├── __init__.py
|
||||
│ └── <node_module>.py
|
||||
├── launch/ # launch 文件 (被 colcon 安装)
|
||||
│ └── <name>_launch.py
|
||||
├── config/ # YAML 配置文件
|
||||
│ └── default.yaml
|
||||
├── urdf/ # (可选) URDF
|
||||
├── srv/ msg/ action/ # (可选) 自定义接口
|
||||
├── test/ # pytest 用例
|
||||
│ ├── conftest.py # 共享 fixture
|
||||
│ └── test_<unit>.py
|
||||
└── README.md # 包自描述文档(每个包必须有)
|
||||
```
|
||||
|
||||
### 2.6 setup.py 模板
|
||||
|
||||
```python
|
||||
from setuptools import setup
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
PACKAGE_NAME = '<package_name>'
|
||||
|
||||
setup(
|
||||
name=PACKAGE_NAME,
|
||||
version='0.1.0',
|
||||
packages=[PACKAGE_NAME],
|
||||
data_files=[
|
||||
('share/ament_index/resource_index/packages', ['resource/' + PACKAGE_NAME]),
|
||||
('share/' + PACKAGE_NAME, ['package.xml']),
|
||||
(os.path.join('share', PACKAGE_NAME, 'launch'), glob('launch/*.py')),
|
||||
(os.path.join('share', PACKAGE_NAME, 'config'), glob('config/*.yaml')),
|
||||
(os.path.join('share', PACKAGE_NAME, 'urdf'), glob('urdf/*')),
|
||||
],
|
||||
install_requires=['setuptools'],
|
||||
zip_safe=True,
|
||||
maintainer='<name>',
|
||||
maintainer_email='<email>',
|
||||
description='<一句话描述>',
|
||||
license='MIT',
|
||||
tests_require=['pytest'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'<exec_name> = <package_name>.<module>:main',
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### 2.7 package.xml 模板
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
<?xml-model
|
||||
href="http://download.ros.org/schema/package_format3.xsd"
|
||||
schematypens="http://www.w3.org/2001/XMLSchema"?>
|
||||
<package format="3">
|
||||
<name><package_name></name>
|
||||
<version>0.1.0</version>
|
||||
<description><一句话描述,详细功能></description>
|
||||
<maintainer email="<email>"><name></maintainer>
|
||||
<license>MIT</license>
|
||||
|
||||
<!-- 运行依赖 -->
|
||||
<depend>rclpy</depend>
|
||||
<depend>std_msgs</depend>
|
||||
|
||||
<!-- 测试依赖 -->
|
||||
<test_depend>ament_copyright</test_depend>
|
||||
<test_depend>ament_flake8</test_depend>
|
||||
<test_depend>ament_pep257</test_depend>
|
||||
<test_depend>python3-pytest</test_depend>
|
||||
|
||||
<export>
|
||||
<build_type>ament_python</build_type>
|
||||
</export>
|
||||
</package>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. C++ 规范 (rclcpp)
|
||||
|
||||
### 3.1 命名
|
||||
|
||||
| 类型 | 规则 | 例子 |
|
||||
|---|---|---|
|
||||
| 类 | `PascalCase` | `ChatterPublisher` |
|
||||
| 函数/方法 | `snake_case` (ROS2 风格) | `timer_callback()` |
|
||||
| 成员变量 | `snake_case_`(后缀下划线) | `publisher_`, `count_` |
|
||||
| 常量 | `kPascalCase` 或 `UPPER_SNAKE` | `kDefaultRate` 或 `DEFAULT_RATE` |
|
||||
| 命名空间 | `snake_case` | `my_robot::control` |
|
||||
|
||||
### 3.2 必须项
|
||||
|
||||
- **智能指针**: `std::shared_ptr<T>` + `std::make_shared<T>()`
|
||||
- **`override`**: 虚函数必须标
|
||||
- **`const`**: 不修改成员的方法加 `const`
|
||||
- **`explicit`**: 单参数构造加 `explicit`
|
||||
- **`#pragma once`**: 头文件用
|
||||
- **`nullptr`**: 不用 `NULL`
|
||||
|
||||
### 3.3 节点模板
|
||||
|
||||
```cpp
|
||||
// chatter_publisher.hpp
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "std_msgs/msg/string.hpp"
|
||||
|
||||
namespace my_robot
|
||||
{
|
||||
|
||||
class ChatterPublisher : public rclcpp::Node
|
||||
{
|
||||
public:
|
||||
explicit ChatterPublisher(const rclcpp::NodeOptions & options = rclcpp::NodeOptions());
|
||||
|
||||
private:
|
||||
void timer_callback();
|
||||
|
||||
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
|
||||
rclcpp::TimerBase::SharedPtr timer_;
|
||||
size_t count_;
|
||||
};
|
||||
|
||||
} // namespace my_robot
|
||||
```
|
||||
|
||||
```cpp
|
||||
// chatter_publisher.cpp
|
||||
#include "my_robot/chatter_publisher.hpp"
|
||||
|
||||
namespace my_robot
|
||||
{
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
ChatterPublisher::ChatterPublisher(const rclcpp::NodeOptions & options)
|
||||
: rclcpp::Node("chatter_publisher", options), count_(0)
|
||||
{
|
||||
this->declare_parameter<int>("period_ms", 500);
|
||||
this->declare_parameter<std::string>("topic", "chatter");
|
||||
|
||||
const int period_ms = this->get_parameter("period_ms").as_int();
|
||||
const std::string topic = this->get_parameter("topic").as_string();
|
||||
|
||||
publisher_ = this->create_publisher<std_msgs::msg::String>(topic, 10);
|
||||
timer_ = this->create_wall_timer(
|
||||
std::chrono::milliseconds(period_ms),
|
||||
std::bind(&ChatterPublisher::timer_callback, this));
|
||||
|
||||
RCLCPP_INFO(this->get_logger(),
|
||||
"ChatterPublisher started: topic=%s, period=%dms",
|
||||
topic.c_str(), period_ms);
|
||||
}
|
||||
|
||||
void ChatterPublisher::timer_callback()
|
||||
{
|
||||
auto msg = std_msgs::msg::String();
|
||||
msg.data = "Hello from C++, seq=" + std::to_string(count_++);
|
||||
publisher_->publish(msg);
|
||||
}
|
||||
|
||||
} // namespace my_robot
|
||||
|
||||
// main
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
rclcpp::init(argc, argv);
|
||||
rclcpp::spin(std::make_shared<my_robot::ChatterPublisher>());
|
||||
rclcpp::shutdown();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 CMakeLists.txt 模板
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(my_robot LANGUAGES CXX)
|
||||
|
||||
if(NOT CMAKE_CXX_STANDARD)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
endif()
|
||||
|
||||
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
set(THIS_PACKAGE_INCLUDE_DEPENDS
|
||||
rclcpp
|
||||
std_msgs
|
||||
)
|
||||
|
||||
# 头文件库
|
||||
add_library(${PROJECT_NAME}_core SHARED
|
||||
src/chatter_publisher.cpp
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME}_core PUBLIC src)
|
||||
ament_target_dependencies(${PROJECT_NAME}_core ${THIS_PACKAGE_INCLUDE_DEPENDS})
|
||||
|
||||
# 可执行文件
|
||||
add_executable(chatter_publisher src/main.cpp)
|
||||
target_link_libraries(chatter_publisher ${PROJECT_NAME}_core)
|
||||
|
||||
# 安装
|
||||
install(TARGETS chatter_publisher
|
||||
DESTINATION lib/${PROJECT_NAME}
|
||||
)
|
||||
install(DIRECTORY launch config
|
||||
DESTINATION share/${PROJECT_NAME}
|
||||
)
|
||||
ament_package()
|
||||
```
|
||||
|
||||
### 3.5 测试 (gtest)
|
||||
|
||||
```cpp
|
||||
// test/test_chatter_publisher.cpp
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "my_robot/chatter_publisher.hpp"
|
||||
|
||||
class ChatterPublisherTest : public ::testing::Test
|
||||
{
|
||||
protected:
|
||||
static void SetUpTestSuite() { rclcpp::init(0, nullptr); }
|
||||
static void TearDownTestSuite() { rclcpp::shutdown(); }
|
||||
};
|
||||
|
||||
TEST_F(ChatterPublisherTest, ConstructsWithDefaults)
|
||||
{
|
||||
auto node = std::make_shared<my_robot::ChatterPublisher>();
|
||||
EXPECT_EQ(node->get_name(), std::string("chatter_publisher"));
|
||||
EXPECT_EQ(node->get_parameter("period_ms").as_int(), 500);
|
||||
EXPECT_EQ(node->get_parameter("topic").as_string(), std::string("chatter"));
|
||||
}
|
||||
|
||||
TEST_F(ChatterPublisherTest, PublishesMessages)
|
||||
{
|
||||
auto node = std::make_shared<my_robot::ChatterPublisher>();
|
||||
auto exec = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
|
||||
exec->add_node(node);
|
||||
|
||||
const auto end = std::chrono::steady_clock::now() + std::chrono::seconds(1);
|
||||
while (std::chrono::steady_clock::now() < end) {
|
||||
exec->spin_some(std::chrono::milliseconds(50));
|
||||
}
|
||||
SUCCEED();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 测试规范
|
||||
|
||||
### 4.1 测试金字塔
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ E2E (1-3) │ ← launch_testing + 真实场景
|
||||
├─────────────┤
|
||||
│ Integ (4-8) │ ← 同进程 spin + DDS
|
||||
├─────────────┤
|
||||
│ Unit (10+) │ ← 纯函数 / 参数声明 / 消息构造
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 4.2 pytest 模板 (conftest.py)
|
||||
|
||||
```python
|
||||
"""共享 fixture - 整个仓库所有 Python 包共用一套模式。"""
|
||||
from typing import Iterator
|
||||
import pytest
|
||||
import rclpy
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def ros_context() -> Iterator[None]:
|
||||
"""整个 session 共享 rclpy 上下文(避免反复 init/shutdown 引发 bug)。"""
|
||||
rclpy.init()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if rclpy.ok():
|
||||
rclpy.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def node(ros_context: None) -> Iterator:
|
||||
"""每个测试一个独立节点实例。"""
|
||||
from <package>.<module> import MyNode
|
||||
instance = MyNode()
|
||||
try:
|
||||
yield instance
|
||||
finally:
|
||||
instance.destroy_node()
|
||||
```
|
||||
|
||||
### 4.3 测试命名
|
||||
|
||||
```python
|
||||
def test_<unit>_<scenario>_<expected>():
|
||||
"""例: test_publisher_init_with_default_rate_uses_1hz"""
|
||||
```
|
||||
|
||||
### 4.4 测试覆盖要求
|
||||
|
||||
| 层级 | 数量 | 内容 |
|
||||
|---|---|---|
|
||||
| Unit | ≥3 | 参数声明 / 默认值 / 关键方法调用 |
|
||||
| Integ | ≥2 | 同进程 spin / DDS roundtrip |
|
||||
| E2E | 1 (可选) | 完整 launch + 多节点 |
|
||||
|
||||
---
|
||||
|
||||
## 5. ROS2 特定规范
|
||||
|
||||
### 5.1 节点命名
|
||||
|
||||
- **节点名**: `snake_case`,表示功能(`chatter_publisher` 不是 `node1`)
|
||||
- **节点必须有 docstring**: 一句话说清做什么
|
||||
- **节点类名 = 节点名 CamelCase**: `ChatterPublisher` ↔ `chatter_publisher`
|
||||
- **不混用**: `MyNode` 这种名字只用于基类,不要直接用
|
||||
|
||||
### 5.2 参数
|
||||
|
||||
- **参数名 `snake_case`**: `publish_rate_hz`, `topic_name`, `frame_id`
|
||||
- **带单位后缀**: `_hz`, `_ms`, `_sec`, `_bytes` (避免歧义)
|
||||
- **声明时给 `descriptor`**: 便于 `ros2 param describe`
|
||||
- **运行时不变参数**: `readonly=True`
|
||||
- **运行时可变**: 注册 `add_on_set_parameters_callback` 校验
|
||||
|
||||
### 5.3 消息 / Service / Action
|
||||
|
||||
- **优先标准接口**: `std_msgs` / `sensor_msgs` / `geometry_msgs` / `example_interfaces`
|
||||
- **必须自定义时**: 在自己包内 `msg/`, `srv/`, `action/`
|
||||
- **字段命名**: `snake_case`,带单位 (`velocity_mps`)
|
||||
- **不要嵌指针 / 引用类型**: 用 ID (`string object_id`) 而非 `string&`
|
||||
|
||||
### 5.4 QoS
|
||||
|
||||
- **默认 RELIABLE + KEEP_LAST(10)**: 跨语言互通零障碍
|
||||
- **传感器流**: BEST_EFFORT + KEEP_LAST(1)
|
||||
- **控制指令**: RELIABLE + KEEP_LAST(1) + DEADLINE
|
||||
- **状态发布**: TRANSIENT_LOCAL + KEEP_LAST(1)
|
||||
|
||||
### 5.5 Launch 文件
|
||||
|
||||
- **函数签名**: `def generate_launch_description() -> LaunchDescription`
|
||||
- **可配置参数**: 用 `LaunchConfiguration` + `DeclareLaunchArgument`
|
||||
- **路径**: `PathJoinSubstitution` + `FindPackageShare`
|
||||
- **嵌套**: `IncludeLaunchDescription` + `PythonLaunchDescriptionSource`
|
||||
- **节点命名空间**: 必要时 `PushRosNamespace`
|
||||
|
||||
### 5.6 TF
|
||||
|
||||
- **frame_id `snake_case`**: `base_link`, `gripper`, `camera_optical_frame`
|
||||
- **REP-103 约定**: x 前, y 左, z 上 (右手系)
|
||||
- **REP-105 语义**: `map` → `odom` → `base_link`
|
||||
|
||||
---
|
||||
|
||||
## 6. 文档规范
|
||||
|
||||
### 6.1 每个文件 docstring (必填)
|
||||
|
||||
```python
|
||||
"""<文件名> - <一句话功能描述>。
|
||||
|
||||
功能:
|
||||
- 列出要点 1
|
||||
- 列出要点 2
|
||||
|
||||
关键概念:
|
||||
- ROS2 概念 1
|
||||
- ROS2 概念 2
|
||||
|
||||
运行方式:
|
||||
ros2 run <pkg> <exec>
|
||||
|
||||
参考:
|
||||
- 官方文档链接
|
||||
"""
|
||||
```
|
||||
|
||||
### 6.2 包内 README.md (必填)
|
||||
|
||||
每个包必须有 `README.md`,包含:
|
||||
|
||||
1. **功能**(一句话)
|
||||
2. **关键概念**(表格)
|
||||
3. **运行**(代码块,3-5 种)
|
||||
4. **测试**(代码块)
|
||||
5. **深度学习链接**(指向 doc/ 下的文档)
|
||||
|
||||
### 6.3 commit message
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
|
||||
类型: feat / fix / docs / style / refactor / test / chore
|
||||
例子:
|
||||
feat(py_pubsub): 重写 publisher 为工业级风格 + type hints
|
||||
fix(cpp_robot_tf2): 加 const-correct 与 override
|
||||
docs(doc/15-params): 新增参数系统深度文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Git 规范
|
||||
|
||||
- **分支命名**: `feat/<name>` / `fix/<name>` / `docs/<name>`
|
||||
- **commit**: 中文/英文都行,但 subject ≤ 72 字符
|
||||
- **不修改全局 git config**: 用 `git -c user.name=x -c user.email=y commit` 临时设
|
||||
- **.gitignore**: 必须包含 `build/`, `install/`, `log/`, `.venv/`, `__pycache__/`
|
||||
|
||||
---
|
||||
|
||||
## 8. 错误处理 + 日志规范
|
||||
|
||||
### 8.1 日志级别
|
||||
|
||||
| 级别 | 何时用 |
|
||||
|---|---|
|
||||
| DEBUG | 周期事件(默认不打印,生产可用 `--log-level DEBUG` 看) |
|
||||
| INFO | 启动 / 关闭 / 配置变更 |
|
||||
| WARN | 异常但可恢复 |
|
||||
| ERROR | 操作失败但节点继续 |
|
||||
| FATAL | 节点即将退出 |
|
||||
|
||||
### 8.2 错误处理
|
||||
|
||||
```python
|
||||
# ✅ 正确: callback 里 try/except + 日志
|
||||
def _on_timer(self) -> None:
|
||||
try:
|
||||
msg = self._build_message()
|
||||
self.publisher_.publish(msg)
|
||||
except Exception as exc:
|
||||
self.get_logger().error(f'publish failed: {exc}', exc_info=True)
|
||||
|
||||
# ❌ 错: 让异常冒泡,节点崩
|
||||
def _on_timer(self) -> None:
|
||||
msg = self._build_message()
|
||||
self.publisher_.publish(msg)
|
||||
```
|
||||
|
||||
### 8.3 优雅退出
|
||||
|
||||
```python
|
||||
def main(args=None):
|
||||
rclpy.init(args=args)
|
||||
try:
|
||||
node = MyNode()
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
node.get_logger().info('KeyboardInterrupt → 退出')
|
||||
finally:
|
||||
if rclpy.ok():
|
||||
rclpy.shutdown()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 反模式 (Anti-Patterns)
|
||||
|
||||
### 9.1 ❌ 不要
|
||||
|
||||
```python
|
||||
# ❌ 1) 节点名不规范
|
||||
super().__init__('node1')
|
||||
|
||||
# ❌ 2) 硬编码
|
||||
self.publisher_ = self.create_publisher(String, 'chatter', 10) # topic 不可配
|
||||
|
||||
# ❌ 3) 无 type hints
|
||||
def callback(self, msg):
|
||||
pass
|
||||
|
||||
# ❌ 4) 无 docstring
|
||||
class MyNode(Node):
|
||||
def __init__(self):
|
||||
...
|
||||
|
||||
# ❌ 5) print 而不是日志
|
||||
print('starting...')
|
||||
|
||||
# ❌ 6) callback 里阻塞 / 抛异常
|
||||
def _on_timer(self):
|
||||
result = blocking_io_call()
|
||||
self.publisher_.publish(result) # 如果 blocking_io_call 抛 → 节点崩
|
||||
|
||||
# ❌ 7) 重复 init/shutdown
|
||||
def test_a():
|
||||
rclpy.init()
|
||||
...
|
||||
rclpy.shutdown()
|
||||
|
||||
def test_b():
|
||||
rclpy.init() # 第二次 init → 异常
|
||||
|
||||
# ❌ 8) 命名混淆
|
||||
self.pub = self.create_publisher(...) # pub 是 keyword 别用
|
||||
self.timer = self.create_timer(...) # timer 是 rclpy 内部属性,会冲突
|
||||
|
||||
# ❌ 9) 包名叫 launch (与 ROS2 系统包冲突)
|
||||
# 包名必须不能与 ROS2 自带包同名
|
||||
```
|
||||
|
||||
### 9.2 ✅ 要
|
||||
|
||||
```python
|
||||
# ✅ 1) 节点名表示功能
|
||||
super().__init__('chatter_publisher')
|
||||
|
||||
# ✅ 2) 参数化
|
||||
self.declare_parameter('topic', 'chatter')
|
||||
topic = self.get_parameter('topic').value
|
||||
|
||||
# ✅ 3) 完整 type hints
|
||||
def _on_timer(self) -> None:
|
||||
msg: String = self._build_message()
|
||||
|
||||
# ✅ 4) Google style docstring
|
||||
class ChatterPublisher(Node):
|
||||
"""ChatterPublisher - 周期性发布 String 到 /chatter 话题。"""
|
||||
|
||||
# ✅ 5) 用 get_logger
|
||||
self.get_logger().info('starting...')
|
||||
|
||||
# ✅ 6) callback 里 try/except
|
||||
def _on_timer(self) -> None:
|
||||
try:
|
||||
...
|
||||
except Exception as exc:
|
||||
self.get_logger().error(f'failed: {exc}')
|
||||
|
||||
# ✅ 7) conftest.py 共享 fixture
|
||||
# 在 conftest.py 里做 session-scope rclpy.init
|
||||
|
||||
# ✅ 8) 节点属性后缀下划线
|
||||
self.publisher_: Publisher[String] = self.create_publisher(...)
|
||||
self.timer_: Timer = self.create_timer(...)
|
||||
|
||||
# ✅ 9) 包名避让 ROS2 系统包
|
||||
# 用 bringup, my_robot, my_arm 等
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 自检清单 (提交前)
|
||||
|
||||
- [ ] 每个文件顶部有 docstring(功能 / 关键概念 / 运行方式 / 参考)
|
||||
- [ ] 类 / 方法有 Google-style docstring
|
||||
- [ ] 所有函数有 type hints
|
||||
- [ ] 节点名 `snake_case`,类名 `PascalCase`,属性后缀 `_`
|
||||
- [ ] 参数带 `descriptor` 描述
|
||||
- [ ] callback 有 try/except
|
||||
- [ ] 测试用 conftest.py 共享 fixture
|
||||
- [ ] 包内有 README.md
|
||||
- [ ] `colcon build` 通过
|
||||
- [ ] `colcon test` 100% 通过
|
||||
- [ ] commit message 含 type(scope): subject
|
||||
|
||||
---
|
||||
|
||||
**违反任何一条,代码不得合并。**
|
||||
**一切为了:专业 / 严谨 / 可维护 / 为后续 VLA 落地铺路。**
|
||||
Reference in New Issue
Block a user