告警与证据链:让每一次报警"有声、有据、可回放"
状态机喊出 ALERT 之后,系统要做三件事:让下位机响起来(STM32 串口帧驱动蜂鸣/LED/OLED)、让证据留下来(FrameRing 三前二后三件套 + events.jsonl)、让画面播出去(:8088 MJPEG 推流)。本实验拆解 [AA][55]+Func+Len+Data+CRC8 帧格式与 CRC-8/MAXIM 校验、buzzer.refresh 5s + alert_tts_interval 10s 双节流、环形缓冲容量公式,按 T9-1~T9-8 用例完成含降级路径的全流程验证。
01问题引入:报警响了,证据呢
深夜老人跌倒,蜂鸣器响了、语音也播了 —— 可第二天子女问"当时到底怎么倒的",系统什么也拿不出;或者反过来:证据存了 20 分钟全程录像、占满磁盘,隐私还全暴露。告警系统的完整闭环是声、据、流三件事同时成立:不响是失职,不留证据是无据,不分流展示是盲区。
02原理通俗讲解:寄包裹式的告警帧与"行车记录仪"
串口是无边界的字节流,必须自己切帧。0xAA 0x55 帧头=快递面单(收端滑到这两个字节就知道帧开始了),Len=包裹重量(按它收 Data),CRC8=防伪校验码 —— 从 Func 到 Data 末尾算出 1 字节,不含帧头。校验值对不上就丢弃,防传输误码。
多项式 x⁸+x⁵+x⁴+1(反射 0x8C),查表法每字节一次异或+一次查表,微秒级。公开对拍基准:checksum_crc8(b"123456789")==0xA1 —— 算不出 0xA1 说明实现错了。
不等事故发生了才开录 —— FrameRing 常驻环形缓冲只留最近 3s(capacity=3.0×10=30 帧),ALERT 瞬间取回前段,再收 2s 后段,落盘"前截图+后截图+约5s视频+事件日志",超 100 条自动轮转。
蜂鸣/LED/OLED 走 buzzer.refresh 5.0s,TTS 走 alert_tts_interval 10.0s,两套独立时间戳。硬件提醒密一点、语音播报疏一点 —— 都急会吵到老人,都疏又提醒不足。首次触发 force=True 无条件响。
03关联知识点:协议帧 · 证据链 · 推流
| 知识点 | 内容 | 源码锚点 |
|---|---|---|
| 5.2.1 STM32 串口协议 | [AA][55][Func 1B][Len 1B][Data N B][CRC8 1B];蜂鸣器 Func=2 <HHHH>(freq/on_ms/off_ms/repeat)、LED Func=1 <BHHH>、OLED Func=10(行号+UTF-8);/dev/ttyUSB0 1Mbps;CRC 范围 Func→Data;蜂鸣帧实例 AA 55 02 08 E8 03 96 00 96 00 00 00 2D(1000Hz/0.15s/0.15s) | stm32_protocol.py L31-L33(常量)、L36-L40(CRC8 查表)、L44-L47(build_frame)、L50-L71(三种 build) |
| 告警调用链与门控 | _dispatch(ALERT) → _start_alarm(force=True) → _refresh_alarm → hw_alert.buzzer_on → build_buzzer → uart.send;_gated 同时检查 buzzer.enabled 与串口可用,任一不满足静默跳过;串口打开失败自动降级(「串口打开失败,硬件告警降级为音频+日志」) | monitor.py L502-L525(_start_alarm/_refresh_alarm);alert_hw.py L25-L34(降级)、L70-L78(_gated) |
| 5.2.2 证据存档 | FrameRing capacity=max(1,int(pre×fps))=30 帧,deque 挤出最旧;window() 按时间戳取帧(掉帧也健壮);_pending_save 双阶段:ALERT 立取 pre、每帧收 post 至 due=+2.0s;save_evidence 写 jpg/_post.jpg/avi(mp4v,≥2帧)/events.jsonl,max_files=100 轮转 | evidence.py L19-L33(FrameRing)、L42-L86(save_evidence/轮转);monitor.py L251(每帧 push)、L508-L512(pending)、L354-L363(落盘) |
| 5.2.3 MJPEG 推流 | mjpeg_streamer;multipart/x-mixed-replace 逐帧独立 JPEG、低延迟无帧间压缩;start() 先 socket 预检端口防静默 404;流名自动补 .mjpg;stop() 吞 aiohttp GracefulExit 防 core dumped | mjpeg_server.py L42(补 .mjpg)、L47-L57(预检)、L61-L63(set_frame)、L68-L71(stop) |
| 测试与清理 | docs/09 用例 T9-1~T9-8 四大类(正常/降级/轮转/冲突);pkill 清理 aplay/llm_server/mjpeg_streamer,TIME_WAIT 不算占用 | docs/09_告警与证据测试用例.md;monitor.py L579-L588(shutdown 强制落盘) |
04实验目标
- 能手工推演一帧蜂鸣器帧的 CRC8(对照 check("123456789")=0xA1 与实测 0x2D),说清 CRC 范围为何不含帧头
- 能默写告警调用链 _dispatch → _start_alarm → _refresh_alarm → buzzer_on → build_buzzer → uart.send,解释 force 语义
- 能独立触发 ALERT 并验证证据四件套(jpg / _post.jpg / avi / events.jsonl)完整性
- 能解释 FrameRing 容量公式 3.0×10=30 与按时间取帧的健壮性
- 能实测双节流节奏(蜂鸣≈5s、TTS≈10s),并完成 T9 用例 + pkill 清理
05实验环境与准备
06动画讲解:告警帧结构与证据时间轴
07互动讲解:告警链路与节流沙盘
08实操步骤:T9 用例与证据验证
09重点与易错点
- CRC 范围不含帧头:payload=[func,len]+data(build_frame L45),帧头只管同步
- Len 是 Data 长度,不含 Func/Len 自身;OLED 的 Len=2+utf8 字节数
- 双定时器各管各的:5s 管蜂鸣/LED/OLED、10s 管 TTS,force=True 保首次无延迟
- 降级靠门控:三件硬件操作全走 _gated,串口不可用即静默跳过,绝不裸调 uart.send
| 现象 | 原因 | 解决 |
|---|---|---|
| .avi 没生成 | 合并帧数 <2(evidence L66 条件) | 用更长视频或调小 evidence.pre_seconds |
| events.jsonl 为空 | 状态机根本没进 ALERT | 先确认日志有 ALERT:{…}、输入词在 confirm_words 内 |
| 首次蜂鸣也被"节流"了 | 误解 force 语义 | _start_alarm L513 传 force=True 本就无条件首响,查日志核实 |
| 启动报"端口 8088 已被占用" | 上次 demo 未退净 / 与 llm 8080 冲突 | lsof -i :8088 找真残留进程 kill;两端口分开配置(双保险预检) |
| 浏览器打不开推流 | mjpeg_streamer 未装 / 不同网段 / 防火墙 | pip show 查装;同局域网;mjpeg_server.py L13-L15 注释给了清华源安装命令 |
| 拔串口后程序崩溃 | 有硬件操作绕过了 _gated | 核对 alert_hw L70-L78:buzzer/led/oled 必须都过门控 |
| 证据目录无限膨胀 | max_files 被调大/轮转失效 | 默认 100 自动删最旧(同名 jpg/_post/avi 连带),需要全留再调大 |
| pkill 后仍报端口占用 | socket 处于 TIME_WAIT,或 daemon 线程残留 | lsof 分辨 TIME_WAIT(无需处理)与真残留(pkill -9 -f mjpeg_streamer) |
10总结与思考
帧结构分析笔记(含 CRC 手工对拍 0x2D/0xA1)、告警调用链图、四件套完整性记录、双节流实测节奏、T9-1~T9-8 Pass/Fail 表、pkill 清理清单。
帧头/Len/CRC 的切帧与校验分工;环形缓冲"常驻时间窗、触发即取证"的事件驱动思路;时间戳差值节流天然免疫帧率波动。
串口协议逐字段解析法;门控+降级的容错写法;lsof/pgrep 三层残留排查 —— 让每轮实验起点干净。
没证据?查是否进 ALERT → 目录可写 → 帧数≥2。没声?查 force 首响日志 → 门控 available → 串口设备号。推流 404?先 lsof 端口。