五节点系统架构:把功能拆成"各管一段"的协作网
语音、调度、抓取、流水线、安全——五个功能塞进一个脚本会怎样?本实验从职责单一出发拆出五节点(N1/N2/N-grasp/N-conveyor/N3),讲透话题异步 + 服务同步的通信拓扑,剖析 N2 调度器的三条任务路由、palletize"先送料后抓取"的异步编排与 2s 自动重试兜底,再读 launch 的 7 个开关与双启动方式。
01问题引入:一个人干不完全场活
设想把"听指令、记任务、抓货物、转流水线、盯安全"五件事全写进一个 Python 文件会怎样?
02原理通俗讲解:餐厅后厨的分工与传菜
还是那家餐厅——五个后厨岗位就是五个节点,传菜条与对讲机就是两类通信方式:
KWS 唤醒 → ASR 识别 → LLM 意图分类 → 往"点单屏"(intent 话题)写一笔,播报由 TTS 完成。它从不直接动机械臂。
盯着点单屏(intent),按任务分流:装货/分拣直接喊抓取(grasp/start 服务),码垛先喊传送带备料。轻状态机,不干重活。
双模式:装货走视觉(AprilTag 定位),分拣/码垛走 yaml 固定点位八步法。完成发 status,不自作主张接新任务。
复用底盘电机 M1 + GPIO17 光电,run_until_sensor 转到遮挡就定点停,收到 danger 立即急停。
/scan 回调内算 60° 扇区最小距离,超限当场双发:放 danger 广播 + 释放 6 舵机力矩——不请示、不排队。
话题像对讲机广播(intent/danger/status 随时插话,人人可听);服务像提单办手续(发任务必须等回执 success)。秒级阻塞的抓取用服务拿确认,状态信号用话题保畅通。
03关联知识点:任务状态机与异步编排
| 节点 | 职责一句话 | 关键接口(方向) |
|---|---|---|
| N1 eai_factory_voice | KWS→ASR→LLM 意图→TTS 播报 / 语音打断检测 | 发 A1 intent、A3 interrupt;订 A2 speak、C1 danger |
| N2 eai_factory_scheduler | 任务状态机(装货/分拣/码垛),grasp 未就绪自动重试 | 订 A1/A3/C1;发 A2;调 E1-E3、D 系列、F3 |
| N-grasp eai_factory_grasp | 双模式抓取 + R8 打断 + 雷达危险中断 + 完成状态发布 | 订 set_task/interrupt/danger/vision target;发 grasp status;调 pick/place/go_work/set_gripper |
| N-conveyor eai_factory_conveyor | 流水线电机 + 光电传感器定点停 + 急停 | 发/订 set_motor、photo、state、emergency、danger |
| N3 eai_factory_safety | 雷达自身前方扇区检测→释放舵机力矩→危险解除恢复+回 work | 订 /scan;发 C1 danger、C2 status;发 C3 舵机力矩;调 C4 go_work |
任务状态机三条路由(scheduler_node.py L80-L94)——N2 的 _run_task 极简三分支:
def _run_task(self, task): if task == 'palletize': self._start_palletize_sequence() # 码垛:先送料后抓取(异步两步) elif task in ('load', 'sort'): self._trigger_grasp(task) # 装货/分拣:直接触发抓取 else: self.get_logger().warn(f'[SCHEDULER] unknown intent: {task}') def _conveyor_state_cb(self, msg): # palletize:货物到达光电(sensor_blocked) -> 在取料位触发抓取 if self._awaiting_sensor and msg.data == 'sensor_blocked': self._awaiting_sensor = False self._trigger_grasp('palletize')
N2 异步调用流水线服务后置 _awaiting_sensor=True 回主循环——不阻塞,等待期间仍能响应 interrupt / danger,完成信号靠 /eai_factory/conveyor/state 话题回调。
grasp/conveyor 服务未就绪(N-grasp 启动慢)时,存 _pending_task 并建 2s 定时器;_pending_task is None 早退防堆积,成功后置 None 并 cancel。
start_base/viz/voice/vision/grasp/conveyor/radar(radar 默认 false);voice/vision/grasp/conveyor 用 ExecuteProcess + venv python 直跑 .py(sherpa_onnx/cv2 4.11 依赖),scheduler/safety 用 Node + console_scripts 入口。
04实验目标
- 掌握五节点职责划分与协作关系,能解释"为什么拆成五个、每个管什么、谁消费谁的数据"
- 理解N2 任务状态机的 load/sort/palletize 三条路由与 palletize"先送料后抓取"编排
- 理解run_until_sensor 异步送料 → 订阅 conveyor/state 等 sensor_blocked → 再触发 grasp 的机制,能说明为何不用同步阻塞
- 掌握launch 多节点编排方法:7 个开关、venv python 直跑与 console_scripts 入口两种启动方式的差异
- 独立完成一张 eai_factory 系统架构图与一份架构理解笔记(附源码行号)
- 迁移:对任意多节点 ROS2 项目快速建立"节点-接口-编排"三维认知
05实验环境与准备
| 类别 | 内容 |
|---|---|
| 硬件 | B2 麦克纳姆底盘、C1 五自由度机械臂 + 夹爪(本实验仅阅读源码,无需上电);传感器:激光雷达、RGB-D 深度相机、光电传感器 |
| OS / ROS | Ubuntu 22.04 + ROS 2 Humble |
| Python | 3.10;voice/vision/grasp/conveyor 运行时用 /home/iflytek/ros2_venv/bin/python3 直跑,scheduler/safety 走 console_scripts 入口 |
| 核心库 | rclpy(Node、ReentrantCallbackGroup、Timer)、std_msgs、std_srvs |
| 阅读对象 | docs/02_架构设计.md、eai_factory/scheduler_node.py、grasp_node.py、conveyor_node.py、safety_node.py、launch/eai_factory.launch.py;复用包 car1_bringup / car1_arm_controller / car1_peripherals / apriltag_detector |
start_base:=false;venv 路径为实机路径,跨环境阅读勿照搬。06动画讲解:五节点通信拓扑与异步编排
看两条线:虚线 = 话题(异步广播,intent/status/danger),实线 = 服务(同步确认,grasp/start、run_until_sensor)。注意 N1→N2→N-grasp/N-conveyor 的星形驱动,以及 N3 并行广播给三方的独立安全通道;右下角演示 palletize 的异步两步时序。
07互动讲解:节点职责分工台
出现一个系统事件,应该由哪个节点响应?点击按钮做"排班":
08实操步骤:源码联查五节点
# ① 列出调度器全部回调/方法及行号 grep -n "def _" eai_factory/scheduler_node.py # _run_task / _start_palletize_sequence / _conveyor_state_cb # _trigger_grasp / _retry_grasp / _grasp_status_cb ... # ② 三类执行节点的服务/话题实现位置 grep -n "create_service\|create_subscription\|create_publisher" \ eai_factory/grasp_node.py eai_factory/conveyor_node.py eai_factory/safety_node.py # ③ launch 的 7 开关与两种启动方式 grep -n "DeclareLaunchArgument\|ExecuteProcess\|Node(" launch/eai_factory.launch.py # start_base/viz/voice/vision/grasp/conveyor/radar(radar 默认 false)
09重点与易错点
| ⚠️ 常见坑 | 现象 → 解法 |
|---|---|
| 以为 palletize 是"先抓后送" | 未理解码垛取料位在流水线上 → 对照 scheduler_node.py L8-L9 docstring 与 L98-L127:先 run_until_sensor 送料到取料位,等 sensor_blocked 再抓取 |
| start 返回 = 完成 | 混淆两种语义 → grasp/start 与 run_until_sensor 都立即返回(触发),完成走 grasp/status 与 conveyor/state 话题 |
| 分不清 ExecuteProcess 与 Node | 不了解 venv 依赖隔离 → voice 依赖 sherpa_onnx(仅 venv)、vision 依赖 cv2 4.11 必须直跑;scheduler/safety 仅标准接口用入口点 |
| 以为重试会堆积定时器 | 未看早退逻辑 → _retry_grasp 先判 _pending_task is None 直接 return,成功重放后置 None 并 cancel 定时器(L154-L169) |
| 以为 N3 只跟 N2 通信 | 漏看 C1/F10 → danger 广播发给 N2/N-grasp/N-conveyor 三方:流水线收到急停并调 /arm_controller/emergency_stop,抓取收到立即停止 |
| 架构图只画节点不画接口 | 接口才是连接件 → 必须标注 intent/speak/danger/grasp/start/run_until_sensor 等,与 EXP-03 接口清单呼应 |
- 五节点拓扑图:职责、话题/服务连线、N3 广播三方 无遗漏
- 能不看代码讲清 palletize 异步机制 与 2s 重试的触发/解除条件
- launch 7 开关默认值与典型组合(如 start_base:=false start_voice:=false 省内存测视觉)
- 双启动方式差异能说出原因:sherpa_onnx/cv2 仅 venv
- N3 停臂是话题直发(scan 回调内,无服务往返 150~220ms),回 work 才用服务
10实验总结与思考
五节点通信拓扑图;调度器状态机分析(三路由+异步编排+2s 重试);三类执行节点职责对比;launch 开关与双启动对照表。
N2 只编排不执行、N3 独立最高优先;话题异步广播 + 服务同步确认的组合拳是多节点系统的标准骨架。
节点-接口-编排三维度拆解任意 ROS2 项目;异步服务+状态话题编排;定时器重试兜底模式。
拓扑连线 30、状态机与编排分析 30、执行节点对比 20、launch 编排与架构图 20,合计 100。