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 序列号相同;
SM与ME都只有 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;
- 真机到货后做破坏性测试;
- 浏览器实际烟测;
- 公开前完整隐私审计。
会更早做
- 把热拔插作为 M0 验收项:模拟 EOF、设备消失和端口重现;
- 把阻塞调用清单写在异步架构旁边:串口、SMTP、备份都提前标注执行上下文;
- 准备历史 schema 样本:当前增量迁移够用,但长期升级应有显式
schema_version; - 从第一版就统一列表、计数与导出过滤器;
- 部署第一天就做恢复演练,而不是只确认能生成备份。
仍不会提前做
- 多用户;
- 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 有很多坑”,而是:硬件、网络和第三方服务都会用各自的方式打破软件的默认假设。
应对方法也不是增加越来越多特例,而是建立几个稳定结构:清楚的状态所有权、可重放的事件、幂等的接收端、可失败的模拟器、可执行的验收清单,以及从用户和攻击者视角进行的最终验证。
功能会继续变化,但这些结构能让系统在变化中保持可解释、可恢复、可维护。