零依赖也能做工业级:MT502 工牌定位测试工具的技术栈与实践效果

2440 字
12 分钟
零依赖也能做工业级:MT502 工牌定位测试工具的技术栈与实践效果

这个项目要解决什么#

工厂里的定位工牌(MT502)通过 LoRa 基站把报文透传出来,但要验证「工牌到底发了什么、发的对不对」,以前要么靠抓包肉眼看十六进制,要么等厂商给工具。这个项目就是给自己造一个趁手的测试台:基站 TCP 连上来,工具负责拆包、解析、展示、下发指令,一条链路全打通。

它给自己定了三条硬约束,后面所有技术选型都由此展开:

  1. 零 npm 依赖 —— 只用 Node 内置模块
  2. 无外部数据库服务 —— 数据落本地文件,不装 MySQL / Redis
  3. 全中文 —— 注释、控制台输出、界面、文档一律中文
为什么「零依赖」是硬约束

这个工具要拷到车间里那些不能联网、没人会配环境的 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 + JS5 个页面,无框架、无构建步骤,改完刷新即生效
图表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 拼字符串就够了。真正花工作量的地方在协议解析,而不是这些基础设施。

架构与数据流#

graph TD A[LoRa 基站 TCP 字节流] --> B[server.js 累积缓冲区] B --> C[dispatch.js 按帧头路由] C -->|0xAA / 0xBB| D[parser.js MT502] C -->|0xEE 0xFF| E[maote_parser.js Uwb+lora V3.2] D --> F[store.js 内存 Map] E --> G[maote_store.js 内存 Map] F --> H[monitor.js 电压润色] H --> I[(power.db SQLite)] F --> J[web.js /api/*] G --> J I --> J J --> K[前端页面 每 2 秒轮询] L[stdin / Web 按钮] --> M[commands.js 构建下行帧] M --> N[downlink.js 广播给基站]

上行是「字节流 → 帧 → 内存 → 接口 → 页面」,下行是「命令 → 帧 → 广播」。两套协议在 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 上按帧头分流:

项MT502Uwb+lora V3.2
帧头0xAA / 0xBB0xEE 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 后做到零丢帧、零串扰。

部署体验#

这是零依赖收益最直接的地方。新机器上的完整流程:

  1. 装 Node.js LTS(唯一的依赖,不需要 npm install)
  2. 拷贝整个文件夹
  3. 管理员跑一次 放行防火墙(管理员运行).bat
  4. 双击 简单启动.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 分包恰好切断双字节帧头的地方。这些只能靠压测、真机报文和长度自校验去兜底。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
零依赖也能做工业级:MT502 工牌定位测试工具的技术栈与实践效果
https://wwkcim.xyz/posts/mt502-lora-uwb-tool/
作者
wwk
发布于
2026-09-25
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
wwk
Hello, I'm wwk.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
分类
标签
站点统计
文章
16
分类
6
标签
39
总字数
18,697
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.13.5
文章许可
CC BY-NC-SA 4.0

文章目录