零依赖也能做工业级:MT502 工牌定位测试工具的技术栈与实践效果
这个项目要解决什么
工厂里的定位工牌(MT502)通过 LoRa 基站把报文透传出来,但要验证「工牌到底发了什么、发的对不对」,以前要么靠抓包肉眼看十六进制,要么等厂商给工具。这个项目就是给自己造一个趁手的测试台:基站 TCP 连上来,工具负责拆包、解析、展示、下发指令,一条链路全打通。
它给自己定了三条硬约束,后面所有技术选型都由此展开:
- 零 npm 依赖 —— 只用 Node 内置模块
- 无外部数据库服务 —— 数据落本地文件,不装 MySQL / Redis
- 全中文 —— 注释、控制台输出、界面、文档一律中文
这个工具要拷到车间里那些不能联网、没人会配环境的 Windows 电脑上跑。只要出现一次 npm install,交付就变成了「先装环境、再装依赖、再祈祷网络通」。把依赖砍到零之后,交付动作只剩两步:装 Node,双击 bat。
技术栈
| 层 | 选型 | 说明 |
|---|---|---|
| 运行时 | Node.js ≥ 22.5 | 主功能 ≥ 14 即可跑;电压监控依赖内置 node:sqlite,需 ≥ 22.5 |
| TCP 服务 | 内置 net | 监听 3001,接收基站透传字节流,手工处理粘包/半包 |
| Web 服务 | 内置 http | 监听 3000,静态文件 + 手写 REST 路由,无 Express |
| 持久化 | 内置 node:sqlite | 仅用于长期电压监控,落 data/power.db 单文件 |
| 前端 | 原生 HTML + JS | 5 个页面,无框架、无构建步骤,改完刷新即生效 |
| 图表 | Canvas 手绘 | 电压/电量阶梯线图,自己算坐标轴与降采样 |
| 导出 | Excel 2003 XML | 纯前端拼 XML 生成 .xls,不引任何表格库 |
| 测试 | 内置 assert | 无测试框架,13 个测试文件各自独立可跑 |
| 文档 | Markdown + 自研转换器 | tools/md_to_html.js 零依赖转自包含 HTML,方便现场阅读打印 |
代码规模:src/ 12 个模块约 2400 行,public/ 5 个页面约 1800 行,测试与工具合计约 3300 行。
有意思的是「零依赖」并没有牺牲多少东西:HTTP 手写路由不到 300 行,Canvas 画阶梯线不到 200 行,Excel 导出用 XML 拼字符串就够了。真正花工作量的地方在协议解析,而不是这些基础设施。
架构与数据流
上行是「字节流 → 帧 → 内存 → 接口 → 页面」,下行是「命令 → 帧 → 广播」。两套协议在 dispatch.js 处按帧头分流,之后存储、展示、报警各自独立,互不影响。
三个真正花功夫的技术点
1. 粘包与半包:长度全靠标志位推导
MT502 的 0xAA 帧没有长度字段,帧长完全由第 2 个字节的位域算出来:bit10 是信标数量、bit32 是类型、bit4 有没有计步、bit5 有没有 SOS 和电量、bit7 有没有扩展字节——而扩展字节自己还有一层位域。解析器必须边读边推,遇到半包就缓存等下一批数据,遇到无效字节就丢一个字节重新同步。
// 起始标识是 2 字节时,只到了第一个字节必须判半包,不能当垃圾丢弃if (head === MAOTE_H0) { if (pos + 1 >= buf.length) { r = null; } // 半包等待 else if (buf[pos + 1] === MAOTE_H1) { r = maote.tryFrame(buf, pos, flush); } else { r = { invalid: true }; }}Uwb+lora V3.2 的帧头是 EE FF 两个字节。最初的实现在只收到 EE、第二个字节还没到时,直接把 EE 当无效字节丢了 —— 整帧错位。压测数据很直观:逐字节分包场景 100 帧里 0 帧能用,随机分包 62/100,300 轮压测 1371/1500。改成「部分到达的标识判半包等待」后,压测零丢帧。
教训适用于所有协议解析:多字节起始标识必须把「部分到达」当作半包,不能当垃圾丢弃。而且单测只喂完整帧永远暴露不了这个问题,必须做逐字节、随机长度的边界测试。
2. 单端口双协议:靠帧头自动识别
第二套协议进来时,没有新增端口、没有加连接级配置,就在原来的 3001 上按帧头分流:
| 项 | MT502 | Uwb+lora V3.2 |
|---|---|---|
| 帧头 | 0xAA / 0xBB | 0xEE 0xFF |
| 校验 | 无 | (~sum) & 0xFF |
| 设备标识 | 8 位十六进制卡号 | 6 字节 / 12 位十六进制 |
| 字节序 | 小端 double | 大端 DWORD(度 × 10⁶) |
前端用顶部 Tab 切换,SOS 置顶、报警导出、频率统计全部跟随当前协议;存储层也是两套独立的 Map,reset 只清当前协议,互不污染。
3. 电压润色:过滤掉物理上不可能的抖动
电压监控要长期记录工牌电池电压(原始值 0~1023),但无线上报的原始数据抖得厉害。算法基于一个业务假设——电压只会缓慢下降:
- 与基准偏差 ≤ 容差(默认 5)→ 保留,基准跟随移动
- 向上大幅跳变 → 丢弃(电压不会无缘无故升高)
- 向下大幅偏离 → 进入候选,连续 3 次稳定在同一低位才判定为真实下降,重置基准
两个参数都能用环境变量调(POLISH_TOLERANCE / POLISH_RESYNC),服务重启后基准从 power_smooth 表恢复,不会从头开始学。
项目效果
功能落地情况
| 能力 | 实现情况 |
|---|---|
| 实时数据表格 | 每 2 秒刷新,卡号/电量/信标 MAC/SOS/上报频率,搜索 + 分页 25 条 |
| SOS 报警 | 红色高亮 + 置顶 1 分钟倒计时,收到即自动回确认帧(MT502 回 0xAA 06,V3.2 回 0x03) |
| 报警导出 | 按卡汇总,一键导出 Excel 兼容 .xls,每晚 24:00 自动清空 |
| 电压 / 电量曲线 | Canvas 手绘阶梯线,Y 轴自动分档,支持 ?card= 直达 |
| 长期监控 | SQLite 落库,每次上报记一条,可全量导出(上限 100 万条) |
| 下行指令 | CLI + Web 双入口,时间同步、改周期、改扫描窗口、休眠、重启、闪灯震动 |
| 无硬件自检 | sim_station.js 模拟基站发混合帧,e2e_check.js 21 项一键体检 |
实测数据
我在本地用 Node 22.22.2 复跑了一遍,全部通过:
parser.test.js 21 项 ✓ (手册示例 + 粘包/半包/脏数据)maote_parser.test.js 16 项 ✓ (含混合流任意交错)maote_store.test.js 19 项 ✓maote_commands.test.js 19 项 ✓ui_maote_table.test.js 8 项 ✓ (无浏览器,vm 里跑内联脚本)e2e.test.js ✓ (spawn 真实服务 + 模拟基站)项目当前状态是 13 个测试文件全绿,并且做过一次真实服务端到端冒烟:混合协议上行、双协议 SOS 自动确认、快照隔离、报警导出、7 个下行命令字确实广播到基站侧,21/21 通过。
压测方面,integration_p4.test.js 覆盖逐字节 / 3 字节碎块 / 随机长度 / 整包粘包 / 300 轮 2100 帧等场景,修复丢帧 bug 后做到零丢帧、零串扰。
部署体验
这是零依赖收益最直接的地方。新机器上的完整流程:
- 装 Node.js LTS(唯一的依赖,不需要
npm install) - 拷贝整个文件夹
- 管理员跑一次
放行防火墙(管理员运行).bat - 双击
简单启动.bat
看到 TCP 监听: 0.0.0.0:3001 和 Web 界面: http://localhost:3000 就成了。没有基站也能自检——跑一下模拟基站就能看到完整数据流。Linux 侧还有 install-systemd.sh 一键注册开机自启。
几个值得记下来的教训
单测造的帧永远满足自己的假设。 UWB 信标解析曾经全空,根因是协议文档把卫星段写成绝对偏移,却没说清状态位为 0 时这段到底发不发——该型号固件照样发 12 字节全 0,导致数量字节被读成「搜星数 0」。12 个用例全绿也没能拦住。最后的解法是改成候选布局 + 帧总长度自校验:两个候选起点都试算,谁刚好把消息体吃完就用谁。
Number(null) === 0 是个陷阱。 报警导出页曾经永远空白,因为可选时间参数缺失时 Number(q.get('to')) 得到 0,而 Number.isFinite(0) 为真,于是把所有时间戳都当成「超出上界」过滤掉了。可选数值参数必须先判空再转数字。
「计数 + 定长数组」的边界要按整条全长判定。 曾经因为边界写成 off + 9 <= len(实际每条 10 字节),遇到「校验位正确但消息体残缺」的畸形帧会越界抛 RangeError,而异常发生在 TCP data 回调里 —— 一台异常终端就能让整个服务进程退出。
小结
这个项目证明了一件事:工业场景的工具不一定要重。零依赖、单文件数据库、原生前端,加起来不到 8000 行代码,却把双协议解析、实时监控、报警导出、长期数据沉淀全做齐了,还能一键拷到车间电脑上跑。
真正的复杂度从来不在框架选型上,而在协议本身的坑里——那些文档没写清楚、固件行为和手册不一致、TCP 分包恰好切断双字节帧头的地方。这些只能靠压测、真机报文和长度自校验去兜底。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!













