# ros2_pro_systems **Repository Path**: ethanjoker/ros2_pro_systems ## Basic Information - **Project Name**: ros2_pro_systems - **Description**: No description available - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-14 - **Last Updated**: 2026-05-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ROS2 Pro > ROS2 上的工程治理层。**不替代 ROS2**,而是把团队里散落各处的约定(任务接口 / 状态语义 / 心跳监控 / 故障恢复 / 指标采集)统一成一套可复用、可观测、可恢复的规则。 当前阶段:**P4 - 开源收尾**(CONTRIBUTING / CODE_OF_CONDUCT / Apache-2.0 全文 / GitHub Actions CI / `docs/architecture.md` / `docs/benchmark.md` / `docs/fault-model.md` / 简历 STAR)。 P3 之前的能力(状态机 + 离线检测 + 节点恢复 + 自动 retry + 障碍语义 + 一键 demo + 故障注入 + 100 任务基准)已经全部落地。 --- ## Quick Start ```bash # 1) 编译(首次) cd ros2-pro colcon build --symlink-install # 2) 一键演示:每 5 秒一个任务,PENDING -> RUNNING -> SUCCEEDED ./scripts/run_demo.sh --quiet # 想看完整故障演示:另开一个终端,跑下面任意一条 # ./scripts/inject_fault.sh node-crash # gripper 进程崩溃 → 自动恢复 → retry # ./scripts/inject_fault.sh obstacle 0.4 6 # 障碍 0.4m 持续 6s → 任务 OBSTACLE_NEAR # 3) 100 任务基准(含 1 次故障注入),自动落盘 benchmark JSON ./scripts/run_benchmark.sh ``` **`run_benchmark.sh` 的实际输出样例**(`docs/benchmark_sample.json`): ```json { "total_tasks": 100, "succeeded": 100, "failed": 0, "success_rate": 1.0, "avg_latency_ms": 20.71, "p95_latency_ms": 21.0, "avg_recovery_time_s": 0.471, "recovery_samples": 7, "error_distribution": {}, "computed_ts_ms": 1778230306080, "dump_reason": "expected_total reached" } ``` 读法: | 字段 | 含义 | 本次结果 | 验收硬指标 | | --- | --- | --- | --- | | `success_rate` | 用户视角终态成功率(含 retry 后) | **100%** | ≥ 98% ✅ | | `avg_latency_ms` | 单任务 PENDING→SUCCEEDED 时长均值 | **20.7ms** | ≤ 50ms ✅ | | `p95_latency_ms` | P95 延迟 | 21ms | 应紧贴均值,无长尾 ✅ | | `avg_recovery_time_s` | retry 任务从 OFFLINE 到再次 SUCCEEDED | **0.47s** | ≤ 10s ✅ | | `recovery_samples` | 走过 retry 路径的任务数(统计意义) | 7 | ≥ 1 ✅ | | `error_distribution` | 失败原因分布 | `{}` | 无 retry 不可恢复失败 ✅ | > 注:`recovery_samples=7` 意味着:100 个任务中有 7 个原任务在故障窗内被 supervisor 标 OFFLINE, > 进入 RETRYING;故障恢复后由 router 重新派发,最终成功。这 7 个 retry 样本贡献 `avg_recovery_time_s`。 --- ## 1. 设计定位 - ROS2 提供"通信原子能力":topic / service / action / QoS - ROS2 Pro 提供"工程治理能力": - 标准任务入口:`TaskCommand` - 标准状态出口:`TaskState`(PENDING / RUNNING / SUCCEEDED / FAILED / TIMEOUT / RETRYING) - 标准健康信号:`NodeHeartbeat` - 标准故障语义:`error_code`(`OK / NODE_OFFLINE / OBSTACLE_NEAR / TIMEOUT`) - 标准恢复流程:`offline → recover → retry` - 标准指标口径:成功率 / 平均延迟 / 平均恢复时长 / 失败原因分布(P3 落地) --- ## 2. 节点角色 | 节点 | 语言 | 职责 | | --- | --- | --- | | `task_client` | Python | 任务发起方,每 `publish_period_s` 发一个 `TaskCommand` | | `task_router` | Python | **状态机唯一裁决者**:受理、派发、监听结果、超时、retry | | `gripper_adapter` | C++ | 业务执行:模拟 N 秒夹取后回 `SUCCEEDED`;`respawn=True` 进程级恢复 | | `safety_node` | C++ | 订阅 `/scan`,最近距离 < 0.6m 时发 `/obstacle_near=True` | | `fake_lidar` | Python | 仿真激光雷达,10Hz `LaserScan`;`min_range` 参数注入危险 | | `supervisor` | Python | 1Hz 巡检心跳;连续 3s 无心跳判 OFFLINE,再次到达判 RECOVERED | | `metrics_collector` | Python | 监听 `/task_state` 总线(P3 聚合统计) | --- ## 3. 通信总线 | Topic | 类型 | 方向 | | --- | --- | --- | | `/task_command` | `ros2_pro_msgs/TaskCommand` | client → router;router 重试时也走此 topic | | `/task_result` | `ros2_pro_msgs/TaskState` | gripper → router(业务回报) | | `/task_state` | `ros2_pro_msgs/TaskState` | router → metrics(终态总线) | | `/heartbeat` | `ros2_pro_msgs/NodeHeartbeat` | gripper / safety → supervisor | | `/node_status` | `ros2_pro_msgs/NodeHeartbeat` | supervisor → router(裁决:OFFLINE / RECOVERED) | | `/scan` | `sensor_msgs/LaserScan` | fake_lidar → safety | | `/obstacle_near` | `std_msgs/Bool` | safety → router | | `/inject_fault/gripper_adapter` | `std_msgs/Empty` | 测试钩子(topic 等价于 `pkill`) | --- ## 4. 状态机 ```text /task_command │ ▼ ┌────────────┐ │ PENDING │ └────────────┘ │ 派发 ▼ ┌────────────┐ ◄────── obstacle_near=True / NODE_OFFLINE / TIMEOUT │ RUNNING │ └────────────┘ │ │ SUCCEEDED ────┘ └──── FAILED │ │ error_code=NODE_OFFLINE && retries#retry" 进入 PENDING ``` --- ## 5. 环境要求 - Ubuntu 22.04 + ROS 2 **Humble** - Python 3.10+,`colcon`,`gcc/g++`(C++17) ```bash source /opt/ros/humble/setup.bash ``` --- ## 6. 构建与启动 ```bash cd ros2-pro colcon build --symlink-install source install/setup.bash ros2 launch launch/demo_sim.launch.py ``` 启动后正常情况每 5s 发起一个 `pick_box_A` 任务,经过约 3s 执行回报 `SUCCEEDED`: ```text [task_router] [task_id=task-0001] PENDING (router accepted, action=pick_box_A) [task_router] [task_id=task-0001] PENDING -> RUNNING (dispatched to gripper) [gripper_adapter] [task_id=task-0001] gripper start | action=pick_box_A [gripper_adapter] [task_id=task-0001] gripper finish | -> SUCCEEDED [task_router] [task_id=task-0001] SUCCEEDED (error_code=OK, latency_ms=3000, end_to_end_ms=3000, retries=0) ``` --- ## 7. 故障注入演示(手动 / 脚本) ### 7.1 障碍触发(OBSTACLE_NEAR) 推荐用脚本(已封装好两次 `param set` + 自动解除): ```bash ./scripts/inject_fault.sh obstacle # 默认 0.4m × 6s ./scripts/inject_fault.sh obstacle 0.3 4 # 自定义距离 / 持续时间 ``` 或等价的原生 ROS2 命令(打开第二个终端): ```bash source /opt/ros/humble/setup.bash source install/setup.bash ros2 param set /fake_lidar min_range 0.4 # 注入危险 sleep 6 ros2 param set /fake_lidar min_range 1.0 # 解除危险 ``` 预期日志: ```text [safety_node] obstacle_near edge -> TRUE | min_distance=0.40m threshold=0.60m [task_router] [task_id=task-0xxx] FAILED (error_code=OBSTACLE_NEAR, ...) ... [safety_node] obstacle_near edge -> FALSE | min_distance=1.00m threshold=0.60m [task_router] [task_id=task-0yyy] SUCCEEDED (error_code=OK, ...) ``` ### 7.2 节点崩溃 → 自动恢复 → 自动 retry 推荐用脚本(默认走 topic 注入,受限环境/容器都能跑): ```bash ./scripts/inject_fault.sh node-crash # 默认 topic 方式 ./scripts/inject_fault.sh node-crash --use-pkill # 强制走 pkill(需要权限可达) ``` 或等价的原生命令: ```bash # 方式 A:生产环境推荐 pkill -KILL -f gripper_adapter_node # 方式 B:受限/容器环境 ros2 topic pub /inject_fault/gripper_adapter std_msgs/msg/Empty '{}' --once ``` 预期日志(一次完整故障 → 恢复 → 重试 → 成功): ```text [gripper_adapter] fault injected via topic — terminating process to simulate crash [supervisor] node=gripper_adapter status=OFFLINE silent_for_ms=3107 threshold_ms=3000 [task_router] [task_id=task-XXXX] RUNNING -> RETRYING (reason=supervisor declared gripper_adapter OFFLINE) [supervisor] node=gripper_adapter status=RECOVERED recovery_time_ms=507 [task_router] [task_id=task-XXXX] RETRYING -> re-dispatch (node gripper_adapter recovered) [gripper_adapter] [task_id=task-XXXX#retry] gripper start [gripper_adapter] [task_id=task-XXXX#retry] gripper finish | -> SUCCEEDED [task_router] [task_id=task-XXXX#retry] SUCCEEDED (error_code=OK, retries=1) ``` `recovery_time_ms` 是 supervisor 看到的"心跳静默到再次出现"时长;端到端用户视角恢复时长(fault → 任务最终 SUCCEEDED)通常 5–7 秒,由 `respawn_delay` + `offline_threshold_s` + `execution_time_s` 决定。 --- ## 8. 三个演示脚本 所有脚本都位于 `scripts/`,共享下列默认行为: - 自动 `source /opt/ros/humble/setup.bash` 与本地 `install/setup.bash` - 默认 `ROS_DOMAIN_ID=88`,`ROS_LOCALHOST_ONLY=1`,与宿主机其它 ROS2 进程隔离 - 日志写到 `log_runtime/`(不污染 `~/.ros`) ### 8.1 `scripts/run_demo.sh` — 一键演示 ```bash ./scripts/run_demo.sh # 完整 launch 输出 ./scripts/run_demo.sh --quiet # 只打 PENDING/RUNNING/SUCCEEDED/OFFLINE/RECOVERED 等关键事件 ``` 正常输出(`--quiet` 模式): ```text [task_router] [task_id=task-0001] PENDING (router accepted, action=pick_box_A) [task_router] [task_id=task-0001] PENDING -> RUNNING (dispatched to gripper) [gripper_adapter] [task_id=task-0001] gripper start | action=pick_box_A [gripper_adapter] [task_id=task-0001] gripper finish | -> SUCCEEDED [task_router] [task_id=task-0001] SUCCEEDED (error_code=OK, latency_ms=3000, retries=0) ``` ### 8.2 `scripts/inject_fault.sh` — 故障注入 ```bash ./scripts/inject_fault.sh node-crash # gripper 进程崩溃(默认 topic 注入) ./scripts/inject_fault.sh node-crash --use-pkill # 强制 pkill(需有权限) ./scripts/inject_fault.sh obstacle # 障碍 0.4m × 6s ./scripts/inject_fault.sh obstacle 0.3 4 # 自定义距离 / 持续时间 ``` `node-crash` 完整事件链(demo 中可观察到): ```text [gripper_adapter] fault injected via topic — terminating process to simulate crash [supervisor] node=gripper_adapter status=OFFLINE silent_for_ms=3107 threshold_ms=3000 [task_router] [task_id=task-XXXX] RUNNING -> RETRYING (...) [supervisor] node=gripper_adapter status=RECOVERED recovery_time_ms=507 [task_router] [task_id=task-XXXX] RETRYING -> re-dispatch (...) [task_router] [task_id=task-XXXX#retry] SUCCEEDED (error_code=OK, retries=1) ``` > 与 `run_benchmark.sh` 一起使用时,要把 `ROS_DOMAIN_ID` 显式传给注入脚本,因为 > benchmark 默认每次会随机 domain: > `ROS_DOMAIN_ID= ./scripts/inject_fault.sh ...` ### 8.3 `scripts/run_benchmark.sh` — 100 任务基准 ```bash ./scripts/run_benchmark.sh # 100 任务,自动注入 1 次 node-crash ./scripts/run_benchmark.sh 50 # 50 任务 ./scripts/run_benchmark.sh 100 --no-fault # 不注入故障(看纯净延迟) ``` 行为: 1. 启动 launch,节奏改为 `task_period=0.5s` `exec_time=0.05s`(benchmark 节奏) 2. 给每个任务加随机前缀 `b-`,并在 `metrics_collector` 里按前缀严格过滤入账 3. 用**随机 ROS_DOMAIN_ID(50–199)**避开同机其它残留进程串扰 4. 第 5s 通过 `topic` 注入 1 次 gripper 崩溃 → 触发若干 task 走 retry 路径 5. `metrics_collector` 在用户视角终态数达到 `expected_total` 后自动 dump JSON, 写入 `log_runtime/benchmark_.json` 6. 脚本回显 JSON 内容并退出 也可在任意时刻手动 dump(demo 模式): ```bash ros2 service call /dump_metrics std_srvs/srv/Trigger {} ``` --- ## 9. 关键参数(默认值) | 参数 | 默认 | 说明 | | --- | --- | --- | | `task_client.publish_period_s` | 5.0 | 任务下发间隔 | | `task_client.deadline_ms` | 3000 | 任务硬截止 | | `gripper_adapter.execution_time_s` | 3.0 | 模拟夹取耗时(demo 用,便于注入故障) | | `safety_node.safety_threshold_m` | 0.6 | 障碍距离阈值 | | `fake_lidar.min_range` | 1.0 | 默认安全;`ros2 param set` 调小可注入危险 | | `supervisor.offline_threshold_s` | 3.0 | 心跳静默阈值 | | `supervisor.watched_nodes` | `[gripper_adapter, safety_node]` | 巡检列表 | | `task_router.max_retries` | 1 | 单任务最多自动 retry 次数 | --- ## 10. 单独验证消息流 ```bash ros2 topic echo /task_state # 终态总线 ros2 topic echo /node_status # supervisor 裁决 ros2 topic echo /obstacle_near # 障碍信号 ros2 topic echo /heartbeat # 心跳源 ros2 topic echo /benchmark_result # metrics_collector 推送的最终统计 ``` --- ## 11. 目录结构 ```text ros2-pro/ ├── src/ │ ├── ros2_pro_msgs/ # 标准消息(CMake/.msg) │ ├── task_router/ # Python — 状态机 │ ├── task_client/ # Python — 任务发起 │ ├── supervisor/ # Python — 心跳监督 │ ├── metrics_collector/ # Python — 实时指标聚合 + Trigger 服务 │ ├── fake_lidar/ # Python — 仿真雷达 │ ├── adapters/ │ │ └── gripper_adapter/ # C++ — 业务执行 + 故障注入钩子 │ └── safety/ │ └── safety_node/ # C++ — 障碍判定 ├── launch/demo_sim.launch.py ├── scripts/ │ ├── run_demo.sh # 一键演示 │ ├── inject_fault.sh # 故障注入(node-crash / obstacle) │ └── run_benchmark.sh # 100 任务基准 + 1 次 fault → JSON ├── docs/ │ ├── architecture.md # 架构、状态机、消息契约 │ ├── benchmark.md # 指标口径、复现方式、真实结果 │ ├── fault-model.md # 故障类型、错误码、闭环路径 │ ├── star-resume.md # 3 条简历 STAR 描述 │ └── benchmark_sample.json # 100 任务基准的真实输出样例 ├── .github/workflows/ci.yml # build / lint / test + benchmark smoke ├── log_runtime/ # 运行期日志 + benchmark JSON(gitignored) ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── LICENSE # Apache-2.0(完整文本) └── README.md ``` --- ## 12. 阶段路线图 - [x] **P1 - 脚手架**:消息包 + 节点骨架 + 一键 launch - [x] **P2 - 可靠性闭环**:状态机 + 3s 离线 + 恢复 + 1 次 retry + 雷达阈值 - [x] **P3 - HR 演示 + 100 次基准**:3 条脚本 + benchmark JSON - [x] **P4 - 开源收尾**:CONTRIBUTING / CoC / Apache-2.0 / GH Actions CI / `docs/*` / 简历 STAR(当前) --- ## 13. 文档索引 - 架构与设计 → [`docs/architecture.md`](docs/architecture.md) - 基准指标口径与复现 → [`docs/benchmark.md`](docs/benchmark.md) - 故障模型与错误码 → [`docs/fault-model.md`](docs/fault-model.md) - 简历用 STAR 描述(含量化数据)→ [`docs/star-resume.md`](docs/star-resume.md) - 真实 benchmark 输出样例 → [`docs/benchmark_sample.json`](docs/benchmark_sample.json) - 贡献指南 → [`CONTRIBUTING.md`](CONTRIBUTING.md) - 行为准则 → [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) --- ## License Apache License 2.0 — 完整文本见 [`LICENSE`](LICENSE)。