综合项目 / 老人跌倒监护 / EXP-02
EXP-02 · 第 1 章 · 设计性 · 4 学时

分层系统架构:六层依赖与双流水线

摄像头、六麦克风、串口、大模型……十几种能力挤在一个程序里,靠什么不乱?本实验用六层依赖拆解 elderly_companion 工程,精读 fall/monitor.py 三大接口(process_frame / has_ask_session / on_kws)与 voice_loop.py 语音状态机(run / feed_text),看懂视觉流水线与语音流水线如何双线协同,并理解文本通道为什么只是同一台状态机的另一个入口。

难度 ⭐⭐ · 4 学时 六层依赖monitor.py 三接口 voice_loop 状态机双流水线

01问题引入:多模块系统靠什么"接口"对接

设想没有架构约束的写法:检测代码直接起线程录音、告警代码直接读摄像头帧、LLM 回复直接塞进 TTS —— 每处改动都要全局搜索,牵一发动全身。本工程用分层 + 接口解决:

1相机线程只管把帧送进 monitor.process_frame,它不需要知道后面是要询问还是告警
2六麦/ASR 链路想判断"现在能不能接收关键词",只问 monitor.has_ask_session,不伸手去摸状态机内部
3硬件唤醒事件经 monitor.on_kws 进入,文本输入经 voice_loop.feed_text 进入 —— 多个入口、同一状态机
4输出侧统一由 _dispatch 分发:播报、串口告警、证据落盘各安其位
💡 接口的含义接口就是"我承诺给你什么、你别管我怎么做到"。process_frame 承诺"每帧进来、判定推进";has_ask_session 承诺"告诉你现在是否在询问中"。承诺稳定,层与层才能并行开发、单独测试。

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 等
🔔 双流水线视觉流水线(帧 → 检测 → 姿态 → 几何 → 状态机)与语音流水线(唤醒 → 录音 → 识别 → 应答)各自独立运转,交汇点只有状态机与三个接口 —— 这就是"双流水线协同":相机卡顿不会堵死录音,LLM 加载慢也不阻塞判定。

03关联知识点:monitor.py 三大接口与 voice_loop 双入口

接口一 · L247–L252
process_frame(frame)

视觉流水线总入口:检测 → IoU 关联 → 姿态估计 → 几何特征 → 推进每人一台的状态机 → _dispatch 分发结果(L340–L383)。相机线程每帧调用,别的什么都不用知道。

接口二 · L152–L157
has_ask_session()

只读查询:当前是否有人处于 ASK 询问会话。语音链路用它决定此时收不收 confirm/cancel 关键词,不暴露状态机内部。

接口三 · L168–L173
on_kws()

硬件唤醒回调:六麦 0xA5 协议唤醒 / 中文 KWS 命中后由语音链路调用,把"进入聆听"事件注入系统。

状态机宿主 · L224–L271
voice_loop.run()

交互主循环:管理 IDLE/LISTENING/SPEAKING 节拍、播报完成事件、聆听窗口与超时兜底,把 TTS/ASR 两头接起来。

文本入口 · L154–L181
feed_text(text)

文本通道:把一行字当作 ASR 结果喂进 同一台状态机 —— "小飞小飞"=唤醒、"救命"=呼救、"没事"=取消,这就是需求 R12 的落点。

汇总出口
_dispatch(L340–L383)

状态机迁移后的统一分发:该播报的交给 TTS,该告警的发 0xAA55 帧,该留证的交给 FrameRing —— 决策与执行彻底分离。

04实验目标

  • 能画出六层依赖图,说出每层的职责与工程对应模块
  • 能复述 monitor.py 三大接口各自的签名职责、所在行号与调用方
  • 能说明视觉流水线与语音流水线的交汇点与隔离点(为什么相机卡顿不堵录音)
  • 能解释 feed_text 与音频通道为何必须共用同一台状态机(R12 的架构意义)
  • 完成一次文本通道验证:用 text 驱动状态机走通"唤醒 → 询问 → 应答"最小闭环

05实验环境与准备

1硬件到位:工控主机 + 摄像头 + XFM 六麦(本实验以代码精读为主,音频链路验证可用 --text 替代)
2进入工程:~/ros2_ws/src/xflz_eai_car1/app/elderly_companion
3环境自检(EXP-03 细讲,这里先跑通):python3 common/env_check.py --config config/fall_config.yaml,预期 [env] 自检通过
4备好两个查看窗口:一个读码(monitor.py / voice_loop.py),一个跑 demo 观察日志

06动画讲解:分层数据流与双流水线协同

左半幅:一帧数据自上而下穿过六层(包沿线流动);右半幅:视觉流水线与语音流水线平行推进,在状态机处交汇:

六层依赖 · 帧数据下行 ⑥ 应用入口 · fall_app --mode ⑤ 输出与证据 · 串口帧 + 三件套 ④ 状态机 · state_machine + voice_loop ③ 视觉监控 · monitor.process_frame ② 模型算法 · YOLO/Pose/KWS/ASR/TTS ① 配置与自检 · fall_config + env_check 双流水线 · 状态机交汇 视觉流水线 帧 → 检测姿态·几何IoU 关联→状态机 语音流水线 唤醒录音·ASR关键词判定文本 六状态机 每 人 一 台 TTS 播报询问告警帧+证据 交汇点只有状态机与三接口 · 层间不越界 feed_text 与 ASR 结果走同一入口(R12)
动画 2-1|左:光点沿六层下行,每层被点亮即承担一段职责;右:双流水线并行推进,虚线在六状态机交汇后流向 TTS 与告警

07互动讲解:六层职责与接口连线台

点击按钮,查看每个"接口合约"的承诺、调用方与被隔离的复杂度:

接口连线台 · Wire Bench
// 点击接口按钮,查看"承诺 → 调用方 → 隔离了什么"三段合约 架构原则:上层调接口,不摸下层内部;同一入口进状态机,行为才一致。
🙋 设计权衡三层接口都是刻意做小的:process_frame 只收一帧,has_ask_session 只答是不是,on_kws 只报"醒了"。接口越小,出错面越小 —— 这正是设计性实验要练的取舍。

08实操步骤:精读关键行与统一通道验证

1进入工程目录并跑环境自检,确认七库与模型就绪
2定位三大接口与状态机主循环,核对行号
3读 process_frame(L247–L252)→ 往下追 _dispatch(L340–L383),画出"一帧的旅程"
4读 voice_loop.run(L224–L271)与 feed_text(L154–L181),确认文本/音频汇入同一状态机
5启动文本通道 demo,依次输入"小飞小飞 → (被问)救命/没事",对照日志验证闭环
# ① 进入工程并自检
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
# 依次输入:小飞小飞 → (系统询问后)没事 / 救命
🔍 读码路线不要从头通读。按"一帧的旅程"读:相机线程 → process_frame → _dispatch → TTS/告警/证据;再按"一句话的旅程"读:feed_text → 状态机 → 应答。两条旅程在状态机处打结,其余都是直路。

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 定位接口 → 顺调用链读,而非逐行通读大文件。

❓ 排障思路
先切层,再定位

行为异常时先问"哪一层的承诺没兑现",把问题从全局缩小到单层。

📝 思考题 1如果把文本通道和音频通道做成两台独立状态机,现场会出现什么竞态?(提示:想象文本输入"没事"的同时,音频侧状态机正准备播报"您还好吗"—— R12 为什么强制"同一状态机"?)
📝 思考题 2monitor.py 的 has_ask_session 只返回"是否在询问中"。若语音层想拿到"询问还剩几秒",你建议扩接口(返回更多信息)还是保持现状(语音层自行计时)?从接口最小化原则与心跳一致性两方面论证。