嵌入式操作系统 | 模块七验收:端到端一键验收 + 项目答辩
本课程开源地址(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 |
| ⑩ | 清理现场(进程/端口/服务) | 全部 |
脚本的骨架非常朴素------一个计数器 + 两种打印:
bash
PASS=0; FAIL=0
ok() { echo " [通过] $1"; PASS=$((PASS + 1)); }
bad() { echo " [失败] $1"; FAIL=$((FAIL + 1)); }
command -v socat > /dev/null && ok "socat 已安装" || bad "socat 未安装"
最后:if [ "$FAIL" -eq 0 ]; then echo "验收通过"; else echo "有 $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 步:先把"总机"和网关程序准备好
shell
pgrep -x ubusd > /dev/null || sudo ubusd & # ubus 总机(课时 31 用过)
ls ~/lab/课时31-网关产品化uci与ubus/gateway_pro # 网关程序(课时 32 守护的就是它)
uci show device | head -3 # 配置在位
第 1 步:★跑一键验收(约 1~2 分钟)
shell
cd ~/lab/课时33-项目验收
sudo ./acceptance.sh # 想快点:./acceptance.sh --quick(跳过完整故障注入)
现象(实测,27 项全通过、0 失败):
text
================ 模块七 综合项目 验收 ================
时间: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 步:按《验收清单》给自己打分
shell
less ~/lab/课时33-项目验收/验收清单.md
逐条对照,把"A 功能 60 / B 健壮性 20 / C 工程化 12 / D 交付答辩 8"填完。
acceptance.sh 覆盖了其中绝大部分可自动化的项,剩下的(代码整洁、README、答辩)由老师和同学互评。
第 3 步:★换一个终端/新环境再跑一遍(真正的验收)
shell
# 换一个干净终端,或者在新克隆的目录里,按 README 从头跑一遍
cd ~/lab/课时33-项目验收
sudo ./acceptance.sh
这一步的意义:证明你的项目不是"只能在你机器上跑" 。
常见翻车点:路径写死、忘了装某个包、忘了
uci commit、忘了起ubusd。
第 4 步:答辩演练(5 分钟掐表)
shell
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 |
⑤ 报 5 秒内没收到上报 |
网关的上报目标不是 127.0.0.1:9100 |
看 /tmp/acc_gw.log 的启动行;或 `uci show device |
⑥ 报 /api/state 不对 |
5000 被占 / 网页没起来 | 看 /tmp/acc_app.log;`ss -ltnp |
| ⑦ 掉线判不出来 | 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、网络)
- 这个项目里哪一处最脆弱?如果要再加一道保险,你会加什么?
四、模块七小结(把整条链路再走一遍)
text
①设备面板(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|完整跑一遍(含前置依赖)
shell
# ===== ⓿ 前置:课时 24~32 的环境与程序都就位 =====
sudo apt install -y socat python3-pyqt5 python3-serial python3-flask xvfb
ls /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 总机(验收第 ④ 项要用)=====
sudo ubusd &
# ===== ② 一键验收 =====
cd ~/lab/课时33-项目验收
sudo ./acceptance.sh # 约 1~2 分钟;--quick 可跳过完整故障注入
# ===== ③ 看结果 =====
# 通过:27 项 失败:0 项
# 结论:模块七 综合项目 验收通过 ✓
# ===== ④ 自评 + 答辩演练 =====
less 验收清单.md
less 答辩提纲.md
acceptance.sh开头的C26/C29/C30/C31/C32按脚本自身位置算 (脚本在代码/课时33-项目验收/,往上两层就是代码/),所以整仓库 clone / 解压到任何目录都能直接跑,不用改路径。另外,仓库里不带 C 编译产物:首次运行脚本会自动用 gcc 编译课时 31 的gateway_pro(编过了就跳过),所以第 ⓿ 步不手工编译也能跑。
附录 B|acceptance.sh 的关键写法
B-1 计数与打印(整个脚本的骨架)
bash
PASS=0
FAIL=0
ok() { echo " [通过] $1"; PASS=$((PASS + 1)); }
bad() { echo " [失败] $1"; FAIL=$((FAIL + 1)); }
# 单项检查:条件成立就"通过",否则"失败"
[ -f /usr/local/include/libubus.h ] && ok "libubus 头文件在位" || bad "缺 libubus(回课时 15)"
B-2 把"现象"变成"可判断的条件"
bash
# 环境是否齐全
command -v socat > /dev/null && ok "socat 已安装" || bad "socat 未安装"
# 设备帧是否真的在发(读面板日志)
grep -aq '发送 0x80' /tmp/acc_panel.log && ok "设备面板在发 0x80 状态帧" || bad "设备面板没有发帧"
# 状态表是否正确(从 ubus 的 JSON 里抠字段)
STATUS="$(ubus call device status 2>/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}" -gt 0 ] && ok "ser_rx = $RX(串口确实收到过帧)" || bad "ser_rx = 0"
B-3 主动"做动作"再检查(掉线、自愈)
bash
# 掉线:杀掉设备 → 等 6 秒 → 看 online 是否变 0
pkill -f 'device_panel_seria[l]'
sleep 6
OFF="$(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 ≥ 1
PID1="$(systemctl show -p MainPID --value device-gateway)"
kill -9 "$PID1"
sleep 5
[ "$(systemctl is-active device-gateway)" = "active" ] && ok "kill -9 后服务被自动拉起" || bad "自愈失败"
NR="$(systemctl show -p NRestarts --value device-gateway)"
[ "${NR:-0}" -ge 1 ] && ok "重启次数 NRestarts = $NR" || bad "NRestarts = $NR"
B-4 收尾一定要清理(trap 保证异常退出也清理)
bash
cleanup_all() {
[ -n "${APP:-}" ] && kill $APP 2>/dev/null
[ -n "${LISTEN:-}" ] && kill $LISTEN 2>/dev/null
[ -n "${GW:-}" ] && kill $GW 2>/dev/null
pkill -f 'device_panel_seria[l]' 2>/dev/null
"$C32/install_service.sh" --uninstall > /dev/null 2>&1
pkill -x socat 2>/dev/null
sleep 1
}
trap cleanup_all EXIT
导出结论 :
exit 0(全通过)/exit 1(有失败)------ 这样以后放进 CI(比如 gitee 的流水线)也能用。