分层系统架构:六层依赖与双流水线
摄像头、六麦克风、串口、大模型……十几种能力挤在一个程序里,靠什么不乱?本实验用六层依赖拆解 elderly_companion 工程,精读 fall/monitor.py 三大接口(process_frame / has_ask_session / on_kws)与 voice_loop.py 语音状态机(run / feed_text),看懂视觉流水线与语音流水线如何双线协同,并理解文本通道为什么只是同一台状态机的另一个入口。
01问题引入:多模块系统靠什么"接口"对接
设想没有架构约束的写法:检测代码直接起线程录音、告警代码直接读摄像头帧、LLM 回复直接塞进 TTS —— 每处改动都要全局搜索,牵一发动全身。本工程用分层 + 接口解决:
02原理通俗讲解:六层像"医院的科室分工"
把系统想象成一家医院:挂号信息(配置)统一管理,检验科出报告(模型推理),主治医生下诊断(状态机),护士执行医嘱(TTS/告警),病历室留档(证据),门诊大厅是入口(apps)。六层依赖自下而上:
| 层 | 职责(科室) | 工程对应 |
|---|---|---|
| ① 配置与自检层 | "医院章程":所有参数一处存放,开机先体检 | config/fall_config.yaml、common/config_schema、common/env_check |
| ② 模型算法层 | "检验科":帧进数字出,不做业务决策 | YOLOv8 RKNN 检测、BlazePose/RTMPose 姿态、sherpa-onnx KWS/ASR、Matcha-TTS、RKLLM |
| ③ 视觉监控层 | "影像主治":把检测+姿态+几何串成每帧流水线 | fall/monitor.py(process_frame 入口) |
| ④ 决策与交互状态机层 | "会诊决策":跌倒六状态机 + 询问/播报窗口 | fall/state_machine.py、voice_loop.py(run / feed_text) |
| ⑤ 输出与证据层 | "护士执行 + 病历留档":串口告警帧、三件套与事件日志 | alert 硬件驱动、evidence FrameRing 落盘 |
| ⑥ 应用入口层 | "门诊大厅":面向不同验证场景的启动器 | apps/fall_app.py(--mode camera/image/video)、speech_*.py、fall_video_demo.py 等 |
03关联知识点:monitor.py 三大接口与 voice_loop 双入口
视觉流水线总入口:检测 → IoU 关联 → 姿态估计 → 几何特征 → 推进每人一台的状态机 → _dispatch 分发结果(L340–L383)。相机线程每帧调用,别的什么都不用知道。
只读查询:当前是否有人处于 ASK 询问会话。语音链路用它决定此时收不收 confirm/cancel 关键词,不暴露状态机内部。
硬件唤醒回调:六麦 0xA5 协议唤醒 / 中文 KWS 命中后由语音链路调用,把"进入聆听"事件注入系统。
交互主循环:管理 IDLE/LISTENING/SPEAKING 节拍、播报完成事件、聆听窗口与超时兜底,把 TTS/ASR 两头接起来。
文本通道:把一行字当作 ASR 结果喂进 同一台状态机 —— "小飞小飞"=唤醒、"救命"=呼救、"没事"=取消,这就是需求 R12 的落点。
状态机迁移后的统一分发:该播报的交给 TTS,该告警的发 0xAA55 帧,该留证的交给 FrameRing —— 决策与执行彻底分离。
04实验目标
- 能画出六层依赖图,说出每层的职责与工程对应模块
- 能复述 monitor.py 三大接口各自的签名职责、所在行号与调用方
- 能说明视觉流水线与语音流水线的交汇点与隔离点(为什么相机卡顿不堵录音)
- 能解释 feed_text 与音频通道为何必须共用同一台状态机(R12 的架构意义)
- 完成一次文本通道验证:用 text 驱动状态机走通"唤醒 → 询问 → 应答"最小闭环
05实验环境与准备
--text 替代)~/ros2_ws/src/xflz_eai_car1/app/elderly_companionpython3 common/env_check.py --config config/fall_config.yaml,预期 [env] 自检通过06动画讲解:分层数据流与双流水线协同
左半幅:一帧数据自上而下穿过六层(包沿线流动);右半幅:视觉流水线与语音流水线平行推进,在状态机处交汇:
07互动讲解:六层职责与接口连线台
点击按钮,查看每个"接口合约"的承诺、调用方与被隔离的复杂度:
08实操步骤:精读关键行与统一通道验证
# ① 进入工程并自检 cd ~/ros2_ws/src/xflz_eai_car1/app/elderly_companion python3 common/env_check.py --config config/fall_config.yaml # 预期:[env] 自检通过(缺项时逐条 MISSING,EXIT 2) # ② 定位三大接口(行号应与本课第 03 节一致) grep -n "def process_frame\|def has_ask_session\|def on_kws" fall/monitor.py # 247: def process_frame(...) 每帧入口 # 152: def has_ask_session(...) 询问会话只读查询 # 168: def on_kws(...) 硬件/KWS 唤醒回调 # ③ 定位语音状态机宿主与文本入口 grep -n "def run\|def feed_text" voice_loop.py # 224: def run(...) 交互主循环 # 154: def feed_text(...) 文本通道 → 同一状态机(R12) # ④ 文本通道跑最小闭环(无需麦克风/摄像头) python3 apps/speech_text_demo.py # 依次输入:小飞小飞 → (系统询问后)没事 / 救命
09重点与易错点
- 三大接口的职责边界:process_frame 管帧、has_ask_session 管查询、on_kws 管唤醒,不可互相挪用
- 文本与音频是同一台状态机的两个入口(feed_text L154–L181),不是两套逻辑
- 双流水线的隔离点在状态机:相机/LLM 的延迟不会阻塞另一条线
- _dispatch 是决策与执行的分界:状态机只给结论,不直接碰硬件
| ⚠️ 常见坑 | 现象 → 解法 |
|---|---|
| 行号对不上 | grep 输出与本课不符 → 以现场 grep 行号为准,行号漂移不影响接口职责理解 |
| 文本通道"没反应" | 跳过唤醒直接输入"救命" → 先"小飞小飞"进入聆听,语序与 R12 一致 |
| 把 has_ask_session 当状态机用 | 想在语音层直接改状态 → 它是只读查询,迁移必须走状态机自身事件 |
| 双流水线串成单线 | 在 process_frame 里同步等 TTS —— 视觉线被播报堵死 → 交由 voice_loop 异步承担 |
| 越层调用 | apps 入口直接 import 内部模块改变量 → 经接口与事件,改动只落在服务方 |
10总结与思考
手绘六层架构图,以及"一帧的旅程""一句话的旅程"两条调用链。
三大接口的承诺边界;交汇点唯一(状态机),其余层间完全隔离。
grep 定位接口 → 顺调用链读,而非逐行通读大文件。
行为异常时先问"哪一层的承诺没兑现",把问题从全局缩小到单层。