嵌入式操作系统 | 模块七验收:端到端一键验收 + 项目答辩
本课程开源地址(Gitee):https://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git
课件、示例代码与验收脚本都在该仓库,可直接git clone或下载 ZIP 使用。
模块七 · 2 学时 · 环境:Ubuntu(完整项目环境)+ socat 虚拟串口
一句话概述:这节课不写新功能,只做两件事——① 用acceptance.sh一键把整条链路重跑一遍,逐项打勾(本节实测27 项全通过);② 按《验收清单》自评、按《答辩提纲》演练 5 分钟讲解。学完这节,你手上有一个能演示、能交付、能自愈的小系统。
本节目标
- 会用"一条命令 + 一个现象"的方式证明每个功能点(这就是回归测试的思路)
- 能独立跑通
acceptance.sh,看懂每一项检查在验什么、失败时怎么查 - 会按《验收清单》给自己打分(A 功能 60 / B 健壮性 20 / C 工程化 12 / D 交付答辩 8)
- 能按《答辩提纲》讲清架构、演示主流程、回答原理题
一、讲解内容
1.1 知识讲解
① 验收和"我觉得能跑"的区别.验收的核心是可复现的证据:
| 不行的说法 | 验收要的写法 |
|---|---|
| “网页能控制设备” | 执行wget -qO- 'http://127.0.0.1:5000/cmd?name=led_on',4 秒后ubus call device status里"led": 1 |
| “掉线能发现” | 杀掉面板,6 秒后/api/state里"online":0;重启面板 6 秒后变回1 |
| “挺健壮的” | 灌一帧坏 CRC,网关日志出现"串口帧 CRC 失败…丢弃",之后ser_rx继续增长 |
②acceptance.sh的设计思路(十项,27 个检查点)
| 段 | 检查什么 | 用到的组件 |
|---|---|---|
| ① | 环境:socat / libubox / libubus / Flask / pyserial / uci+ubus / systemd | 课时 15、24 |
| ② | 虚拟串口建立 | socat |
| ③ | 设备面板在发帧 + 网关起来了 + 认到设备 | 课时 26、31 |
| ④ | 状态表正确(online=1、ser_rx>0) | 课时 29、31(ubus) |
| ⑤ | 每 2 秒0x81上报 | 课时 29 |
| ⑥ | 网页显示在线 + 点按钮能改状态 | 课时 30 |
| ⑦ | 掉线判 0 / 恢复判 1 | 课时 29、30 |
| ⑧ | 灌坏帧后照常工作 | 课时 32 |
| ⑨ | systemd 服务化 +kill -9自愈 | 课时 32 |
| ⑩ | 清理现场(进程/端口/服务) | 全部 |
脚本的骨架非常朴素——一个计数器 + 两种打印:
PASS=0;FAIL=0ok(){echo" [通过]$1";PASS=$((PASS+1));}bad(){echo" [失败]$1";FAIL=$((FAIL+1));}command-vsocat>/dev/null&&ok"socat 已安装"||bad"socat 未安装"最后:if["$FAIL"-eq0];thenecho"验收通过";elseecho"有$FAIL项没过";fi③ 为什么必须"一键"?十个检查点手工做一遍要十几分钟,而且容易漏;写成脚本以后:每次改代码都重跑一遍(回归测试),改坏了立刻能发现是哪一项坏了。这也解释了课时 32 为什么要搞故障注入——验收里必须包含"坏情况"。
④ 验收清单(100 分)与答辩(5 分钟)见同目录两份文件:
代码/课时33-项目验收/验收清单.md:A 功能 60 分、B 健壮性 20 分、C 工程化 12 分、D 交付答辩 8 分,每项都写了"怎么证明"代码/课时33-项目验收/答辩提纲.md:5 分钟时间分配 + 12 个常问问题 + 加分/扣分项
⑤ 整个模块七做出来的东西(复习地图)
| 课时 | 做了什么 | 关键文件 |
|---|---|---|
| 24 | 环境 + 协议契约 | check_env.py、hello_qt.py、hello_flask.py、协议契约.md |
| 25 | 设备面板界面(PyQt5) | device_panel.py/device_panel_full.py |
| 26 | 面板接串口(半帧/CRC/断线重连) | device_panel_serial.py、serial_monitor.py |
| 27 | 网关雏形:串口 ⇄ UDP | gateway.c、udp_client.py |
| 28 | 网关 TCP 命令通道(粘包/半包/多客户端) | gateway_tcp.c、tcp_client.py |
| 29 | 状态表 + 每 2 秒上报 + 在线判定 | gateway_full.c、state_listener.py |
| 30 | 网页上位机(显示 + 控制闭环) | app.py |
| 31 | 产品化:配置读 uci + 状态走 ubus | gateway_pro.c、device.conf、ubus_query.sh |
| 32 | 可靠性:故障注入 + systemd 自愈 | fault_device.py、fault_tcp.py、gateway.service、install_service.sh |
| 33 | 验收:一键端到端 + 答辩 | acceptance.sh、验收清单.md、答辩提纲.md |
1.2 操作步骤(一条命令一步,看现象)
实测环境是在
代码/目录下(脚本里的路径指向各课时目录);同学们按自己的目录改路径即可。
第 0 步:先把"总机"和网关程序准备好
pgrep-xubusd>/dev/null||sudoubusd&# ubus 总机(课时 31 用过)ls~/lab/课时31-网关产品化uci与ubus/gateway_pro# 网关程序(课时 32 守护的就是它)uci show device|head-3# 配置在位第 1 步:★跑一键验收(约 1~2 分钟)
cd~/lab/课时33-项目验收sudo./acceptance.sh# 想快点:./acceptance.sh --quick(跳过完整故障注入)现象(实测,27 项全通过、0 失败):
================ 模块七 综合项目 验收 ================ 时间:2026-09-26 06:04:04 【① 环境检查】 [通过] socat 已安装 [通过] libubox 头文件在位 [通过] libubus 头文件在位 [通过] Flask 可用 [通过] pyserial 可用 [通过] uci / ubus 可用 [通过] systemd 可用 【② 建立虚拟串口】 [通过] 虚拟串口 /tmp/ttyV0 <-> /tmp/ttyV1 已建立 【③ 设备面板 + 网关】 [通过] 设备面板在发 0x80 状态帧 [通过] 网关已启动(配置读 /etc/config/device) [通过] 网关认到了设备(状态表已建立) 【④ 网关状态查询(ubus)】 [通过] ubus call device status 有返回 [通过] online = 1(设备在线) [通过] ser_rx = 3(串口确实收到过帧) 【⑤ 状态上报(每 2 秒 0x81)】 [通过] 监听端 5 秒内收到 3 帧 0x81 上报 收到 55 AA 81 05 00 00 00 F7 01 0D AD → 灯 灭 | 风扇 0 档 | 24.7 ℃ | 在线 【⑥ 网页上位机(显示 + 控制)】 [通过] /api/state 显示设备在线 [通过] 网页按钮控制成功(led=1 fan=2) 【⑦ 设备掉线判定与恢复】 [通过] 设备掉线后 online 变成 0 [通过] 设备回来后 online 自动恢复成 1 【⑧ 故障注入(坏帧不影响正常工作)】 [通过] 网关识别并丢弃了坏 CRC 帧 [通过] 故障注入后网关照常收帧(ser_rx 25 → 50) [通过] 故障注入后设备仍在线 【⑨ systemd 守护与自愈】 [通过] 服务已启动(systemctl is-active = active) [通过] kill -9 后服务被自动拉起(active) [通过] 重启次数 NRestarts = 2 [通过] 服务已卸载(现场恢复干净) 【⑩ 清理现场】 [通过] 所有进程已停止,端口已释放 ================ 验收结果 ================ 通过:27 项 失败:0 项 结论:模块七 综合项目 验收通过 ✓第 2 步:按《验收清单》给自己打分
less~/lab/课时33-项目验收/验收清单.md逐条对照,把"A 功能 60 / B 健壮性 20 / C 工程化 12 / D 交付答辩 8"填完。acceptance.sh覆盖了其中绝大部分可自动化的项,剩下的(代码整洁、README、答辩)由老师和同学互评。
第 3 步:★换一个终端/新环境再跑一遍(真正的验收)
# 换一个干净终端,或者在新克隆的目录里,按 README 从头跑一遍cd~/lab/课时33-项目验收sudo./acceptance.sh这一步的意义:证明你的项目不是"只能在你机器上跑"。
常见翻车点:路径写死、忘了装某个包、忘了uci commit、忘了起ubusd。
第 4 步:答辩演练(5 分钟掐表)
less~/lab/课时33-项目验收/答辩提纲.md按提纲的四段做一遍:① 架构图 1 分钟 → ② 主流程演示 2 分钟 → ③ 掉线 + 自愈 1 分钟 → ④ 坑与收获 1 分钟。
再让同桌随机问"12 个常问问题"里的 3 个,答不上来的回去补。
第 5 步:收工。脚本已自动清理(进程/端口/服务都还原);ubusd可留可停(sudo pkill -x ubusd)。
1.3 常见错误与排查
| 现象 | 原因 | 解决 |
|---|---|---|
④ 报ubus 查询没返回 | ubusd 没启动 | sudo ubusd &;或ls -l /var/run/ubus/ubus.sock确认 |
③ 报设备面板没有发帧 | 面板没起来(PyQt/Xvfb 问题) | 看/tmp/acc_panel.log;无桌面时用xvfb-run python3 device_panel_serial.py |
③ 报网关没起来 | 9000 端口被占 / 串口打不开 | ss -ltnp | grep 9000杀掉旧进程;确认 socat 起了 |
⑤ 报5 秒内没收到上报 | 网关的上报目标不是127.0.0.1:9100 | 看/tmp/acc_gw.log的启动行;或uci show device | grep report |
⑥ 报/api/state 不对 | 5000 被占 / 网页没起来 | 看/tmp/acc_app.log;ss -ltnp | grep 5000 |
| ⑦ 掉线判不出来 | offline_ms配得太大 | uci set device.main.offline_ms=3000 && uci commit device后重启网关 |
| ⑨ 服务 self-heal 失败 | systemd 不可用(容器里常见) | 用虚拟机跑;或在真机上确认 PID 1 是 systemd |
| 脚本跑完端口还被占 | 上一次跑挂了没清理 | pkill -f gateway_pro; pkill -x socat,或重启虚拟机 |
| 全部失败但手工跑却正常 | 目录不对(脚本找各课时目录找错了) | 脚本用$0自动算出C26/C29/C30/C31/C32(从"脚本所在目录"往上两层取代码/),仓库 clone 或解压到哪都能跑;只有你把某个课时目录搬走时才需要改开头的这几行 |
二、学生练习
本节命令速查表:
| 命令 | 作用 | 示例 |
|---|---|---|
sudo ./acceptance.sh | 一键端到端验收(十项 27 检查点) | --quick快跑 |
sudo ubusd & | 起 ubus 总机(④ 需要) | 早已在课时 31 做过 |
less 验收清单.md/less 答辩提纲.md | 自评 / 演练 | 边看边填 |
exit $FAIL式的脚本骨架 | 自己写检查脚本 | 见 1.1 的ok/bad |
| 编号 | 任务 | 提示 | 预期结果 |
|---|---|---|---|
| L33.1 | 跑通acceptance.sh | 第 1 步 | 27 项全通过 |
| L33.2 | 故意制造一处失败 | 改offline_ms为 60000 再跑 | ⑦ 报失败,学会"看哪项红了" |
| L33.3 | 给脚本加一个新检查点 | 例如检查/etc/config/device里的report_ms与网关启动行一致 | 脚本变 28 项 |
| L33.4 | 写自己的 README | 别人照着能跑通(含依赖、步骤、现象) | 同桌按它跑一遍不出错 |
| L33.5 | 按清单自评 | 逐项填"自测"列 | 分数 ≥ 85 |
| L33.6 | 答辩演练 | 掐表 5 分钟 + 随机抽 3 问 | 讲解流利、问题能答 |
三、作业(模块七项目交付物)
必交(按此清单打包,命名学号-姓名-模块七项目/):
| # | 交付物 | 要求 |
|---|---|---|
| 1 | 代码 | 课时 24~32 你写/改过的文件;gcc -Wall -Wextra零警告 |
| 2 | README.md | 依赖清单、编译命令、运行步骤(几个终端分别跑什么)、预期现象、常见问题 |
| 3 | 协议契约.md | 帧格式、CRC、类型表(可沿用课时 24 的版本,改动要更新) |
| 4 | 验收截图/记录 | acceptance.sh的完整输出(27 项)+ 网页界面截图 + 掉线/自愈各一张 |
| 5 | 《验收清单》自评表 | 逐项打分并写一句依据 |
| 6 | 答辩提纲 | 按模板填自己的架构图与两个"坑" |
思考题(写在 README 末尾):
- 验收脚本里哪几项是"功能"、哪几项是"健壮性"、哪几项是"工程化"?为什么验收要三者都覆盖?
- 如果要把这个项目搬到真实硬件(设备换成单片机 + 真串口,网关跑在 ARM 开发板上),你需要改哪些地方?(提示:串口路径、交叉编译、systemd、网络)
- 这个项目里哪一处最脆弱?如果要再加一道保险,你会加什么?
四、模块七小结(把整条链路再走一遍)
①设备面板(PyQt5) ②网关(C, uloop) ③上位机(Flask 网页) ┌───────────────┐ 串口 ┌──────────────────────┐ TCP命令 ┌──────────────┐ │ 灯/风扇/温度 │ 55AA帧 │ 四道防线:帧头→LEN→CRC │◀────────│ 按钮(表单) │ │ 0x80 每秒上报 │ ─────▶ │ →类型 │ │ │ │ 收命令改状态 │ ◀───── │ 状态表(灯/风扇/温度/在线)│ UDP上报 │ 页面显示状态 │ └───────────────┘ │ 0x81 每 2 秒上报 │ ──────▶ │ /api/state │ │ 配置读 uci、状态走 ubus │ 0x81 └──────────────┘ └──────────────────────┘ ↑ systemd 守护(挂了自动起)- 协议:
55 AA | TYPE | LEN | PAYLOAD | CRC16(低字节在前);TCP 外加 2 字节大端长度前缀 - 职责划分:设备只管"状态 + 执行命令";网关管"转帧 + 记状态 + 判在线 + 对外提供接口";上位机管"显示 + 控制"
- 工程化三件套:配置外置(uci)、状态可查(ubus)、崩溃自愈(systemd)
- 可靠性三件套:帧同步 + CRC 校验(坏数据不进门)、缓冲区上限(不被灌爆)、在线判定(失联能发现)
全课程回头看:模块一~六(Ubuntu 基础、C 语言与结构体、进程与服务、网络编程、uloop 事件驱动、uci/ubus/JSON),到这里都变成了一个能演示的项目里的一颗螺丝钉——这就是"嵌入式 Linux 应用开发"的完整套路。
继续往哪走(选修/毕设方向):① 换成真实传感器(DHT11/DS18B20)与真串口;② 用 SQLite 存历史数据 + 画曲线;③ 用 MQTT 把状态发给云平台;④ 交叉编译到 ARM 开发板(课时 16 起的库要在板子上重新编);⑤ 给网页加登录与权限。
附录:怎么运行 + 脚本关键代码
三个文件在
代码/课时33-项目验收/:acceptance.sh(一键验收,十项 27 检查点)、验收清单.md(100 分自评表)、答辩提纲.md(5 分钟讲解 + 12 问)。
附录 A|完整跑一遍(含前置依赖)
# ===== ⓿ 前置:课时 24~32 的环境与程序都就位 =====sudoaptinstall-ysocat python3-pyqt5 python3-serial python3-flask xvfbls/usr/local/lib/libuci.so /usr/local/lib/libubus.so# 课时 15 编的ls~/lab/课时31-网关产品化uci与ubus/gateway_pro# 课时 31 编的uci show device|head-3# 课时 31 装的配置# ===== ① 起 ubus 总机(验收第 ④ 项要用)=====sudoubusd&# ===== ② 一键验收 =====cd~/lab/课时33-项目验收sudo./acceptance.sh# 约 1~2 分钟;--quick 可跳过完整故障注入# ===== ③ 看结果 =====# 通过:27 项 失败:0 项# 结论:模块七 综合项目 验收通过 ✓# ===== ④ 自评 + 答辩演练 =====less验收清单.mdless答辩提纲.md
acceptance.sh开头的C26/C29/C30/C31/C32按脚本自身位置算(脚本在代码/课时33-项目验收/,往上两层就是代码/),所以整仓库 clone / 解压到任何目录都能直接跑,不用改路径。另外,仓库里不带 C 编译产物:首次运行脚本会自动用 gcc 编译课时 31 的gateway_pro(编过了就跳过),所以第 ⓿ 步不手工编译也能跑。
附录 B|acceptance.sh的关键写法
B-1 计数与打印(整个脚本的骨架)
PASS=0FAIL=0ok(){echo" [通过]$1";PASS=$((PASS+1));}bad(){echo" [失败]$1";FAIL=$((FAIL+1));}# 单项检查:条件成立就"通过",否则"失败"[-f/usr/local/include/libubus.h]&&ok"libubus 头文件在位"||bad"缺 libubus(回课时 15)"B-2 把"现象"变成"可判断的条件"
# 环境是否齐全command-vsocat>/dev/null&&ok"socat 已安装"||bad"socat 未安装"# 设备帧是否真的在发(读面板日志)grep-aq'发送 0x80'/tmp/acc_panel.log&&ok"设备面板在发 0x80 状态帧"||bad"设备面板没有发帧"# 状态表是否正确(从 ubus 的 JSON 里抠字段)STATUS="$(ubus call device status2>/dev/null)"echo"$STATUS"|grep-q'"online": 1'&&ok"online = 1(设备在线)"||bad"online 不是 1"RX="$(echo"$STATUS"|grep-o'"ser_rx": [0-9]*'|grep-o'[0-9]*')"["${RX:-0}"-gt0]&&ok"ser_rx =$RX(串口确实收到过帧)"||bad"ser_rx = 0"B-3 主动"做动作"再检查(掉线、自愈)
# 掉线:杀掉设备 → 等 6 秒 → 看 online 是否变 0pkill-f'device_panel_seria[l]'sleep6OFF="$(wget-qO- http://127.0.0.1:5000/api/state|grep-o'"online":[0-9]*'|grep-o'[0-9]*')"["${OFF:-1}"="0"]&&ok"设备掉线后 online 变成 0"||bad"掉线没判出来(online=$OFF)"# 自愈:kill -9 → 等 5 秒 → 服务还在 active 且 NRestarts ≥ 1PID1="$(systemctl show-pMainPID--valuedevice-gateway)"kill-9"$PID1"sleep5["$(systemctl is-active device-gateway)"="active"]&&ok"kill -9 后服务被自动拉起"||bad"自愈失败"NR="$(systemctl show-pNRestarts--valuedevice-gateway)"["${NR:-0}"-ge1]&&ok"重启次数 NRestarts =$NR"||bad"NRestarts =$NR"B-4 收尾一定要清理(trap保证异常退出也清理)
cleanup_all(){[-n"${APP:-}"]&&kill$APP2>/dev/null[-n"${LISTEN:-}"]&&kill$LISTEN2>/dev/null[-n"${GW:-}"]&&kill$GW2>/dev/nullpkill-f'device_panel_seria[l]'2>/dev/null"$C32/install_service.sh"--uninstall>/dev/null2>&1pkill-xsocat2>/dev/nullsleep1}trapcleanup_all EXIT导出结论:
exit 0(全通过)/exit 1(有失败)—— 这样以后放进 CI(比如 gitee 的流水线)也能用。