Air780E 多卡短信中枢(六):完整复盘、错误假设与工程经验清单

最后更新:2026-08-04
所属系列:Air780E 多卡短信中枢实战 · 第 6 / 6 篇
文章目录 38 个章节

air780e-hub 从第一个提交到双模块实测和公开发布,主要集中在 2026 年 8 月 3 日到 4 日。时间很短,但过程不是“一次写完”:先用模拟器拆掉协议风险,再逐层建立 Agent、Server、通知和 Web 闭环,硬件到手后用真机逐个推翻假设,最后补齐运维、安全和公开发布。

这篇不再按模块讲功能,而是把整个过程压缩成一张路线图,并整理哪些决策真正降低了复杂度、哪些假设最危险、哪些经验可以直接复用到下一次软硬件项目。

一、从 M0 到 M7 的完整过程

M0:先造一个会犯错的假模块

第一阶段没有真实硬件,目标是建立 AT 与短信闭环:

  • GSM7 / UCS2 PDU 编解码;
  • SMS-DELIVER / SMS-SUBMIT;
  • 长短信分段与重组;
  • AT 命令串行化和 URC 分离;
  • PTY transport 与假 Air780E;
  • 存储容量、信号变化和短信注入。

关键不是让 mock 永远返回 OK,而是让它模拟存储写满、超时、长短信和异步 URC。一个只会走成功路径的模拟器,会让测试产生错误安全感。

这一阶段已经抓到两个真实解析问题:+CEREG 响应被当成 URC,以及 +CMT 前缀吞掉 +CMTI

M1:把 Agent 做成离线也能工作的系统

Agent 增加:

  • TOML 配置;
  • 每模块独立 Worker;
  • 本地 SQLite 事件队列;
  • 出站 WebSocket;
  • seq / ack 与重放;
  • 状态采样去噪;
  • systemd 与 udev 部署文件。

阶段目标不是“能连接 Server”,而是 Server 不存在时,Agent 仍能启动、收短信、保存事件和维护任务。

M2:中心服务只接受幂等事件

Server 完成:

  • WebSocket Token 认证;
  • (agent_id, seq) 去重;
  • SIM、设备、短信、渠道、规则、任务数据模型;
  • 单管理员认证与 Session;
  • REST API;
  • Docker Compose 与反向代理部署。

这一阶段把“Agent 是事件事实源,Server 是镜像”落实为数据库约束,而不是停留在架构图上。

M3:先做可操作后台

第一版 Web 只追求闭环:

  • 登录;
  • 设备与 SIM 状态;
  • 短信列表;
  • 主动发送;
  • 基础检索。

它不够漂亮,但能让真实用户路径尽早暴露:选哪张卡、发送失败怎么显示、两张卡的同一号码如何区分。

M4:通知不是一个 Webhook

通知引擎支持 Bark、Telegram、飞书、企业微信、钉钉、自定义 POST / GET 与 SMTP,并实现:

  • 按 SIM、关键词、正则匹配;
  • 渠道级去重与规则优先级;
  • 模板;
  • 超时和重试;
  • 业务错误码判断;
  • 不含正文的审计日志。

这里最重要的发现是“HTTP 200 不等于投递成功”。

M5:保号任务回到设备侧

保号支持 interval 与五段 cron,动作包括短信、Ping 和原始 AT,还加入抖动、随机后缀、重试与结果通知。

真正难点是状态同步:Server 是任务配置的真相,Agent 是执行状态的真相。每次连接后全量对齐,才不会让离线期间已删除的任务继续运行。

M6:真机负责推翻假设

两块真实 Air780E 接入后确认:

  • 一个模块有多个 ACM 接口;
  • 不止一个接口响应 AT,但短信 URC 有固定出口;
  • 两块模块 USB 序列号相同;
  • SMME 都只有 10 条存储;
  • 按 IMEI 运行时发现比 udev 路径绑定可靠;
  • 真机响应可能出现 PDU 长度不足;
  • 热拔插错误传播存在断层;
  • 同步串口打开会阻塞整个事件循环。

模拟器没有失去价值。恰恰因为软件闭环已经稳定,这些差异才能快速归因到硬件边界,而不是在几十个未完成模块之间盲猜。

M7:从功能完整走向可运营

最后补齐:

  • 短信会话、未读、回复、验证码复制;
  • 全文搜索和 CSV;
  • 7 / 30 / 90 天趋势与信号曲线;
  • 规则调试器;
  • 模块掉线与恢复告警;
  • Web AT 调试台;
  • 短信 TTL;
  • SQLite 快照备份恢复;
  • 桌面与移动端响应式布局;
  • 公开部署和安全文档。

最终 Agent 179 项测试、Server 99 项测试,共 278 项。

二、最危险的错误假设

初始假设 真机 / 联调结果 最终做法
一个模块对应一个串口 一个模块有三个 ACM 口 枚举并做完整 AT + URC 探测
USB 序列号可唯一标识模块 两块模块都是 000000000001 Agent 按 IMEI / ICCID 认领
SIM 通常能存几十条短信 SM / ME 都只有 10 条 收到即读、落库、删除
端口读失败会自然让 Worker 离线 EOF 和异常被多层吞掉 设计明确的失败传播链
异步函数里的库调用不会阻塞 serial.Serial() 卡住事件循环 阻塞操作移入线程
HTTP 200 表示推送成功 服务商在 JSON 中返回业务失败 每家适配业务成功条件
任务修改时下发一次就够 离线 Agent 保留已删除任务 每次连接全量同步
等待同步回执更可靠 同一收帧循环发生逻辑死锁 全量同步单向、幂等、可重发
Secure Cookie 越强越好 HTTP 局域网访问无法登录 按实际 scheme,并信任正确代理头
slim 镜像也有系统时区 ZoneInfo 找不到数据库 显式依赖 tzdata
构建通过说明页面没问题 输入和按钮实际错位、移动端溢出 桌面与移动视口真实烟测
删除秘密文件就安全 Git 历史仍保留内容和作者邮箱 扫描完整历史,必要时重写

这张表背后有一个共同点:每个错误假设在正常路径里都很合理,只有边界实验才能推翻。

三、最有效的工程决策

1. 让状态所有权只有一个答案

  • 硬件事件:Agent 是真相;
  • 管理配置:Server 是真相;
  • SIM 历史:按 ICCID / sim_id 归属;
  • 端口:只是当前观测值,不是身份。

所有权模糊时,系统会用双向同步补洞,最后出现冲突。所有权清楚后,另一侧只需要镜像、确认或全量收敛。

2. 接受网络会断

断线不是异常分支,而是常态:

  • 本地先落库;
  • 至少一次传输;
  • Server 幂等;
  • 带抖动退避;
  • 队列按数据价值淘汰;
  • 本地任务不依赖中心连接。

如果一开始按“连接通常稳定”设计,后期补断网支持往往要重写数据流。

3. 把协议约束写进数据库和测试

“重复消息不能重复入库”不能只靠 if 判断,要有 (agent_id, seq) 唯一约束。

“短信正文不能进日志”不能只靠注释,要有包含独特验证码的回归测试。

“规则同渠道只推一次”不能只靠页面提示,要测试多个规则同时命中。

越重要的不变量,越应该由更底层、更难绕过的机制保护。

4. 用最小复杂度解决真实规模

SQLite、单 FastAPI 进程、JSON WebSocket 和 systemd 都不新潮,但它们让系统容易部署、备份和排查。

项目没有为了“将来可能”提前引入:

  • Kafka / RabbitMQ;
  • PostgreSQL 高可用;
  • 多管理员 RBAC;
  • 插件系统;
  • 微服务;
  • Kubernetes。

预留清晰边界比预装复杂基础设施更有价值。

5. 修根因,不给症状打补丁

几个典型例子:

  • 模块拔掉仍显示在线:修错误传播,不只是缩短离线超时;
  • 端口编号变化:改身份发现,不是增加更多 udev 特例;
  • 通知预览不一致:复用真实渲染,不是继续同步两套代码;
  • CSV 数量不对:统一查询过滤器,不是在页面改显示数字。

短期补丁通常把一个确定问题变成多个隐藏分支。

四、测试策略:什么值得自动化

测试数量本身不是目标。真正值得锁住的是边界和不变量。

Agent

  • GSM7 扩展表与 UDH 对齐;
  • UCS2 和长短信分段重组;
  • URC 插入命令响应;
  • +CMT / +CMTI 前缀边界;
  • PDU 声明长度与实际长度;
  • 两模块并行但串口内串行;
  • 事件重启不丢、ack 丢失可重放;
  • 队列压力下短信不淘汰;
  • 保号断网执行和回执补传;
  • 日志中无短信正文。

Server

  • 匿名访问受保护接口返回 401;
  • 错误 Token、重复 Agent ID 与非法协议帧;
  • 同一 seq 重放不重复入库;
  • 两张 SIM 的消息、会话和未读隔离;
  • 通知规则优先级与渠道去重;
  • 各服务商 HTTP 200 业务错误;
  • 时区模板渲染;
  • TTL 清理不误删新数据;
  • 备份恢复前校验。

不能只靠自动化的部分

  • 哪个 ACM 口产生短信 URC;
  • 供电是否稳定;
  • 运营商是否把某种动作算作保号;
  • 反向代理是否正确传 WebSocket 与 scheme;
  • 手机浏览器上的布局和复制体验;
  • 通知是否真的到达最终设备。

这些需要清晰的人工验收清单,而不是假装一个 mock 已经覆盖现实。

五、排错时最有用的思维方式

先画出状态流,再看日志

例如“拔掉模块仍在线”,先画:

内核设备消失
→ fd 返回什么
→ transport 如何表达
→ AT client 如何结束等待
→ Worker 如何切离线
→ Server 如何收到状态
→ 页面何时刷新

日志只是每个节点的观测。没有状态流,很容易在某一层反复加打印,却不知道缺的是哪条边。

区分事实、推断和假设

  • 事实:dmesg 显示三个 ACM;
  • 推断:其中一个可能是 AT 口;
  • 假设:能响应 ATI 的口会发送 +CMTI

然后设计最小实验验证最后一项。很多弯路来自把推断当成事实。

优先做能缩小问题空间的实验

  • 极简 PTY 模块区分解析器和真机;
  • 直接 socat 区分 Agent 和串口;
  • MockTransport 区分通知规则和服务商;
  • 桌面 / 移动截图区分数据与布局;
  • 匿名 Raw URL 区分本地 Git 和公开仓库状态。

好的实验不一定修问题,但会让下一步只剩一两个方向。

六、如果重新做一次,我会保持什么、改变什么

会保持

  • 先定范围和非目标;
  • 先写假模块与协议测试;
  • Agent 本地持久化;
  • sim_id 作为业务归属;
  • 出站 WSS;
  • 单进程 Server 与 SQLite;
  • 真机到货后做破坏性测试;
  • 浏览器实际烟测;
  • 公开前完整隐私审计。

会更早做

  1. 把热拔插作为 M0 验收项:模拟 EOF、设备消失和端口重现;
  2. 把阻塞调用清单写在异步架构旁边:串口、SMTP、备份都提前标注执行上下文;
  3. 准备历史 schema 样本:当前增量迁移够用,但长期升级应有显式 schema_version
  4. 从第一版就统一列表、计数与导出过滤器
  5. 部署第一天就做恢复演练,而不是只确认能生成备份。

仍不会提前做

  • 多用户;
  • PostgreSQL;
  • 高可用 Server;
  • 原生 App;
  • 通知插件 SDK;
  • 支持所有 modem。

没有真实需求和维护资源时,这些只会稀释最关键的短信可靠性。

七、后续路线

下一阶段不应继续堆页面,而应加强发布与可靠性:

发布基线

  • CI 自动执行 Agent / Server 测试、前端构建和 Compose 校验;
  • Secret、依赖与镜像扫描;
  • 明确许可证、版本策略和支持矩阵;
  • 可复现 Release、SBOM 与可选多架构镜像。

数据可靠性

  • 显式 schema_version 与顺序迁移;
  • 迁移前自动快照和失败恢复;
  • 定期恢复演练;
  • 十万级短信查询与流式 CSV 基准;
  • 磁盘、数据库、任务延迟和通知失败可观测性。

硬件验证

  • 固件与 Linux 发行版支持矩阵;
  • 冷启动、热插拔、USB Hub 和 ModemManager;
  • 弱信号、漫游与网络切换;
  • 至少一个真实保号周期的持续观察。

八、给类似项目的一份落地清单

设计前

  • 写清楚核心目标和明确不做的功能;
  • 判断业务身份与物理设备身份是否一致;
  • 明确每类状态的唯一所有者;
  • 把断网、断电、重启和重复事件当正常场景。

写驱动时

  • 命令串行化,URC 独立分流;
  • 使用协议长度和序号做验证;
  • 模拟超时、截断、插队和存储满;
  • 每一层明确关闭与错误传播;
  • 审计所有同步阻塞调用。

做中心服务时

  • 所有上行写入幂等;
  • 敏感日志采用允许列表,而不是事后打码;
  • 第三方 API 检查业务成功条件;
  • 预览和真实执行共享核心逻辑;
  • 备份、恢复和保留期一起设计。

上线前

  • 真机执行冷启动、热拔插、换端口和断网;
  • 真实发送中文、长短信和多卡同号码消息;
  • 桌面、手机逐页操作;
  • 验证 HTTPS、WSS、Cookie 和代理头;
  • 扫描当前文件、Git 历史和构建产物;
  • 从匿名外部视角检查最终发布结果。

结语

这个项目最深的体会不是“Air780E 有很多坑”,而是:硬件、网络和第三方服务都会用各自的方式打破软件的默认假设。

应对方法也不是增加越来越多特例,而是建立几个稳定结构:清楚的状态所有权、可重放的事件、幂等的接收端、可失败的模拟器、可执行的验收清单,以及从用户和攻击者视角进行的最终验证。

功能会继续变化,但这些结构能让系统在变化中保持可解释、可恢复、可维护。