fix(vd960Air): 修复上电反复复位, 打通测试服务器连接

现象: 上电后 Lua VM 报 uart_app.lua:28 attempt to index a boolean value
      → VM exit → 15s 后自动复位, 死循环, 永远走不到 MQTT 连接

根因(4 处叠加, 第 1 处致命):
1. config.lua 末尾漏 return —— require 对无返回值模块返回 true,
   local CFG = require "config" 后访问 CFG.CFG_xxx 直接报错
2. 全工程 10 处调用 mstick(), 但 LuatOS 并无此 API(正确为 mcu.ticks())
3. MQTT 服务器仍是合宙官方测试服 lbsmqtt.airm2m.com:1884
4. MQTT 鉴权用户名/密码硬编码为空串

修复:
1. config.lua 末尾加 return _G(配置项本就是全局变量);
   另: ClientID 的 mobile.imei() 加空值防护(or "unknown")
2. 新增 mstick.lua: 全局 mstick() = mcu.ticks() 封装(回退 os.time()*1000),
   原 10 处调用点零改动; main.lua 在 require "config" 之后 require "mstick"
3. 服务器改为 110.41.131.144:1883(明文非 TLS), 新增 CFG_MQTT_USER/PASS,
   mqtt_main 改为 auth(CLIENT_ID, CFG_MQTT_USER, CFG_MQTT_PASS, true)
4. .gitignore 放行 vd960Air/tools/ —— 官方 tools/ 忽略规则导致自检脚本
   无法入库(sim_frame_test.py 自 8/31 起一直未入库)

新增工具:
- tools/sim_load_test.lua: 离线加载+冒烟自检(lua5.3 + LuatOS API 桩 + 严格全局
  检查), 按 main.lua 真实顺序加载全部模块并冒烟调用关键函数, 用于抓取
  "require 返回值缺失""调用了不存在的 API"这类语法检查抓不到的运行时问题

验证:
- luac5.3 -p 全部 14 个 lua 文件通过
- 离线自检: 16 项断言 + 8 项冒烟全部通过
- Python 帧协议参考验证全部通过
- readme Markdown 表格列数 0 异常

文档: readme 补 §8 板级问题修复记录, 更新文件结构/待确认清单/计划表
This commit is contained in:
wangfq
2026-09-10 15:35:10 +08:00
parent 3ad7ae8f09
commit e8917e7041
8 changed files with 660 additions and 183 deletions
+6 -1
View File
@@ -168,4 +168,9 @@ app_store/
single_app.json
.qoder/
# --- vd960Air 自检工具需入库(2026-09-10) ---
# 官方 tools/ 为忽略规则, 但本工程 PC 侧自检脚本属重要资产, 需随工程入库:
# tools/sim_frame_test.py 帧协议参考验证(Python 1:1 复刻解析公式)
# tools/sim_load_test.lua 离线加载+冒烟自检(lua5.3 + LuatOS API 桩)
!module/Air780EPM/demo/vd960Air/tools/
!module/Air780EPM/demo/vd960Air/tools/**
@@ -10,10 +10,15 @@ vd960Air 是 Air8781P 整板(Air780EPM 核心模组)上的 LuatOS 应用,
]]
-- ==================== MQTT 服务器 ====================
-- 商用服务器地址和端口(替换为实际值)
-- 测试服务器(2026-09-10 用户提供): 110.41.131.144:1883, admin/password
-- 注: 后续 vd960DBN 更新版本后,经 BLE 设置的服务器参数会同步下发覆盖此处(配置同步, P1)
CFG_MQTT_HOST = "lbsmqtt.airm2m.com"
CFG_MQTT_PORT = 1884
-- ⚠ 商用环境须替换为平台实际地址; 密码不应长期写死在代码里(应走 §2.2 配置同步链路)
CFG_MQTT_HOST = "110.41.131.144"
CFG_MQTT_PORT = 1883
-- MQTT 鉴权(mqtt_main 调用 mqtt_client:auth; 空串表示匿名)
CFG_MQTT_USER = "admin"
CFG_MQTT_PASS = "password"
-- ==================== 设备标识 ====================
-- 设备序列码: 6 字节十六进制字符串(与 vd960DBN dev_serial 一致,有线/4G 同一设备)
@@ -31,7 +36,7 @@ CFG_TOPIC_UP = "dld960/" .. CFG_DEV_SERIAL .. "/dev" -- 设备上报
CFG_TOPIC_DOWN = "dld960/" .. CFG_DEV_SERIAL .. "/srv" -- 服务器下发
-- MQTT ClientID: 用 IMEI 保证唯一(与序列码解耦;鉴权待确认)
CFG_MQTT_CLIENT_ID = "vd960Air-" .. mobile.imei()
CFG_MQTT_CLIENT_ID = "vd960Air-" .. (mobile.imei() or "unknown")
-- ==================== UART(对接 vd960DBN) ====================
-- vd960DBN 侧: CH32V208 串口1(PB6=TX, PB7=RX)
@@ -50,3 +55,10 @@ CFG_REPORT_ALWAYS = true -- MVP: 每收到 0xC0 帧都上报 loop_data;P1
-- 用户决策 2026-08-31: 上行附加 link 对象 + initialize extra_info 含 imei/iccid
-- true = 上行 JSON 顶层注入 link 对象(见 link_info.lua)
CFG_LINK_ENABLE = true
-- ==================== 模块导出(必须, 不可删) ====================
-- ⚠ require "config" 时若文件无 return, Lua 返回 true;
-- 调用方 local CFG = require "config" 后访问 CFG.CFG_xxx 会报
-- "attempt to index a boolean value" ——2026-09-10 上电复位事故根因
-- 配置项以全局变量定义, 这里返回全局表供模块化访问
return _G
@@ -1,8 +1,8 @@
--[[
@module main
@summary vd960Air — Air8781P(Air780EPM) 4G 模块 LuatOS 应用入口(方案 C MVP)
@version 0.3
@date 2026.08.31
@version 0.3.1
@date 2026.09.10
@usage
vd960DBN 车检器在有线网络失效时的 4G 兜底通道(方案 C: Air780 协议转换):
@@ -22,6 +22,10 @@ log.info("main", PROJECT, VERSION)
-- ==================== 加载配置 ====================
require "config"
-- ============ 统一毫秒时基(全局 mstick(), 必须早于业务模块) ============
-- 修复 2026-09-10: 工程原调用 mstick() 但 LuatOS 无此 API, 上板必报 nil
require "mstick"
-- ==================== 网络环境检测看门狗 ====================
require "network_watchdog"
@@ -152,7 +152,8 @@ local function mqtt_client_main_task_func()
end
-- 配置mqtt client对象的client idusernamepassword和clean session标志
result = mqtt_client:auth(CFG.CFG_MQTT_CLIENT_ID, "", "", true)
-- 用户名/密码来自 config.lua(测试服 admin/password; 空串 = 匿名)
result = mqtt_client:auth(CFG.CFG_MQTT_CLIENT_ID, CFG.CFG_MQTT_USER or "", CFG.CFG_MQTT_PASS or "", true)
-- 如果配置失败
if not result then
log.error("mqtt_client_main_task_func", "mqtt_client:auth error")
@@ -0,0 +1,31 @@
--[[
@module mstick
@summary 统一毫秒时基(全局 mstick()): 封装 LuatOS mcu.ticks()
@version 0.1
@date 2026.09.10
@usage
背景: 工程内原有 10 处调用 mstick(), 但 LuatOS 并未提供该 API
(正确写法是 mcu.ticks(), 单位 1/mcu.hz() 秒; hz 通常 1000),
上板运行必然报 "attempt to call a nil value (global 'mstick')"。
本模块提供全局函数 mstick() 作为工程唯一时基入口:
- 优先 mcu.ticks(); 无 mcu 库时回退 os.time()*1000(便于 PC 侧离线自检)
- 约定单位毫秒。差值判据(now - marked)保持不变即可, 无需关心绝对基准
- ⚠ mcu.ticks() 为 32 位计数, 约 49.7 天回绕一次; 超时判定均为差值比较,
回绕瞬间最坏推迟一次触发(秒级), 对 5s/60s 级超时无实质影响
用法: 在 main.lua 中 require "mstick"(只 require, 不要用 local 接返回值,
否则会遮蔽同名全局函数, 导致后续 mstick() 调用报错)
]]
local function now_ms()
if mcu and type(mcu.ticks) == "function" then
return mcu.ticks()
end
return os.time() * 1000
end
-- 全局函数: 工程内既有 10 处 mstick() 调用点依赖此全局名
_G.mstick = now_ms
return { now = now_ms }
@@ -6,7 +6,7 @@
```
┌─────────────────┐ UART1 (115200) ┌──────────────────────┐ MQTT 3.1.1 ┌──────────┐
│ vd960DBN │ 帧协议(0x7D) │ Air8781P 整板 │ JSON │ 云平台 │
│ vd960DBN │ 0x7F 帧字节流 │ Air8781P 整板 │ JSON │ 云平台 │
│ CH32V208 │ ◄───────────────► │ Air780EPM (LuatOS) │ ─────────────► │ dld960 │
│ (车检器通信MCU) │ PB6=TX / PB7=RX │ vd960Air 应用 │ dld960/{sn}/ │ 主题族 │
└─────────────────┘ └──────────────────────┘ {dev,srv} └──────────┘
@@ -193,8 +193,9 @@ vd960DBN 与地感 MCU 的串口逻辑(loop_uart_proto.c,UART2,0x7F 帧):
```
vd960Air/
├── main.lua # 入口: 加载 config/看门狗/4G网卡/uart_app/app_iot/mqtt_main
├── config.lua # 配置: MQTT 服务器/序列号/主题/UART/上报节奏/link 开关
├── main.lua # 入口: 加载 config/mstick/看门狗/4G网卡/uart_app/app_iot/mqtt_main
├── config.lua # 配置: MQTT 服务器(地址/端口/账号密码)/序列号/主题/UART/上报节奏/link 开关
├── mstick.lua # 统一毫秒时基: 全局 mstick() 封装 mcu.ticks()(2026-09-10 新增)
├── frame_parser.lua # ★ 0x7F 帧解析状态机(Lua 版 lup_feed_byte,IDLE→HEADER→VALUE→CHECK)
├── proto_conv.lua # ★ 0xC0 传感帧 → loop_data + car_state/loop_state 沿检测
├── evt_queue.lua # ★ event_report 队列 + ACK 状态机(16深/5s×3/跨重连同 msg_id)
@@ -205,7 +206,8 @@ vd960Air/
├── netdrv_device.lua # 网卡: 仅 4G(官方 netdrv_4g)
├── network_watchdog.lua # 网络看门狗(官方原样)
├── tools/
── sim_frame_test.py # 帧协议参考验证(Python 1:1 复刻解析公式,字节级断言)
── sim_frame_test.py # 帧协议参考验证(Python 1:1 复刻解析公式,字节级断言)
│ └── sim_load_test.lua # 离线加载+冒烟自检(lua5.3 + LuatOS API 桩,2026-09-10 新增)
└── mqtt/
├── mqtt_main.lua # MQTT 客户端(连接成功 → initialize + 未决事件重发)
├── mqtt_receiver.lua # 下行分发: event_report ACK 路由 + report_config 时钟校准
@@ -243,7 +245,9 @@ vd960Air/
- [ ] vd960DBN UART1 帧格式是否按 §3 设计稿(0x7D 帧),或用户文档另有规定
- [ ] dev_serial 获取方式: config 写死 vs UART 握手动态下发(当前 config 写死)
- [ ] MQTT 服务器地址/端口/TLS、鉴权方式(平台为准)
- [x] **MQTT 服务器(2026-09-10 定)**: 测试服 `110.41.131.144:1883`,明文(非 TLS),鉴权 `admin/****`
—— 实现: `config.lua` `CFG_MQTT_HOST/PORT/USER/PASS`,`mqtt_main` 取配置传入 `mqtt:auth()`
- [ ] 商用 MQTT 地址/端口、是否启用 TLS(1883 明文 / 8883 TLS)、账号权限
- [ ] 4G 特有命令清单(如 `4g_status_query` / `link_status_report`)是否纳入主协议 4G 章节
- [ ] 通道切换策略: 有线失效判定、4G 启用条件、回切策略
- [ ] 心跳: 4G 通道 heartbeat 间隔/内容是否与有线一致
@@ -255,6 +259,7 @@ vd960Air/
|------|------|------|
| P0 | 工程骨架 + 帧协议 + MQTT 桥接 + link 注入 | ✅ 完成 |
| **P0.5** | **方案 C MVP: 0x7F 帧解析 + 0xC0→loop_data + 沿检测→event_report + ACK 状态机 + initialize/heartbeat + 时钟校准**(2026-08-31) | ✅ 代码完成,待板级 |
| **P0.5-fix** | **上电复位修复: config.lua 漏 return + mstick 时基 + MQTT 服务器/鉴权配置**(2026-09-10) | ✅ 已修,待板级复测 |
| P1 | 下行命令转换(loop_param_*/log_*/ota_* 转 0x7F)+ 0x7D 配置同步下发 + vd960DBN UART1 通道联调 | ⏳ 待板/待 vd960DBN 版本 |
| P2 | 板级联调: 帧校验、背压、掉线重连、看门狗 | ⏳ 待板 |
| P3 | 通道切换(策略待讨论)+ 脱机日志/OTA 走 4G | ⏳ 规划 |
@@ -265,4 +270,33 @@ vd960Air/
- [ ] BLE 设置服务器/topic 参数时(0x13/0x15 等),同步经 UART1 下发 Air780 配置
- [ ] 通道切换策略(待讨论: 动态自动 vs 人工设置;暂定默认 4G)
## 8 板级问题修复记录
### 8.1 2026-09-10 上电反复复位(无法连接测试服务器)
**现象**: 模块上电后无法连接 MQTT 测试服务器;模块日志结尾:
```
[000000000.212] I/user.main vd960Air 001.999.000
[000000000.281] E/main Luat:
[000000000.281] E/main uart_app.lua:28: attempt to index a boolean value (local 'CFG')
[000000000.281] E/main Lua VM exit!! reboot in 15000ms
```
即 Lua VM 报错退出 → 15s 后自动重启 → 死循环,永远走不到 MQTT 连接。
**根因(4 处叠加,第 1 处为致命)**:
| # | 问题 | 影响 | 修复 |
|---|------|------|------|
| 1 | `config.lua` **末尾漏写 `return`** | Lua 的 `require` 对无返回值模块返回 `true`,`local CFG = require "config"` 后访问 `CFG.CFG_xxx``attempt to index a boolean value`**VM 退出/复位** | 末尾 `return _G`(配置项本就是全局变量) |
| 2 | 全工程 **10 处调用 `mstick()`**,但 LuatOS 无此 API(正确为 `mcu.ticks()`) | 走到即报 `attempt to call a nil value (global 'mstick')`;波及 initialize/heartbeat/事件重发/时钟校准 | 新增 `mstick.lua`: 全局 `mstick()` = `mcu.ticks()` 封装(回退 `os.time()*1000`),**10 处调用点零改动** |
| 3 | MQTT 服务器仍是合宙官方测试服 `lbsmqtt.airm2m.com:1884` | 连不到用户测试服 | 改为 `110.41.131.144:1883` |
| 4 | MQTT 鉴权**用户名/密码硬编码为空串** | 测试服要求账号密码,匿名连接被拒 | `config.lua``CFG_MQTT_USER/PASS`;`mqtt_main` 改为 `auth(CLIENT_ID, CFG_MQTT_USER, CFG_MQTT_PASS, true)` |
**验证方式**: 新增离线自检 `tools/sim_load_test.lua`(lua5.3 + LuatOS API 桩 + **严格全局检查**),
`main.lua` 真实顺序加载全部模块并冒烟调用关键函数 → **全部通过**(16 项断言 + 8 项冒烟)。
**教训**:
- **"语法正确" ≠ "能跑"**: `luac -p` 全绿,但"模块漏 return""调用了不存在的 API"这类问题只在运行时暴露 → **每次上板前先跑 `sim_load_test.lua`**
- 新增 API 调用前先交叉核实 LuatOS 是否提供(如 `mstick` vs `mcu.ticks()`),不要"想当然"
> 基底来源: 合宙官方 Air780EPM demo/mqtt(main.lua / uart_app.lua / mqtt/* / network_watchdog / netdrv_device),MIT 许可。
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
"""vd960Air 帧协议参考验证 — 构造 0xC0 测试帧,验证解析公式/字节序/位域
用途: 本地无 Lua 解释器,用 Python 1:1 复刻 frame_parser/proto_conv 的解析逻辑,
验证《DLD960Loop_串口通信协议》0xC0 帧解析公式正确(Lua 代码与本文公式逐行对应)。
运行: python3 tools/sim_frame_test.py
"""
import struct, sys
MAGIC = 0x7F
# ==================== 参考实现(与 frame_parser.lua/proto_conv.lua 公式一致) ====================
def calc_xor_sum(data):
"""data: bytes(从 Addr 起到 Value 末,不含 magic)"""
x, s = 0, 0
for b in data:
x ^= b
s = (s + b) & 0xFF
return x, s
def build_frame(addr, cmd, payload: bytes):
"""组 0x7F 帧: [7F][Addr][LEN][CMD][Value: LEN-1][XOR][SUM]"""
value = bytes([cmd]) + payload # LEN 含 CMD
body = bytes([addr, len(value)]) + value
x, s = calc_xor_sum(body)
return bytes([MAGIC]) + body + bytes([x, s])
def parse_sensor(payload: bytes):
"""payload = cmd 之后的字节: SensType + SubPkgFlag + SensData"""
assert payload[0] == 0x0C, "SensType 必须 0x0C"
data = payload[2:] # 跳过 SensType + SubPkgFlag
channels = []
for i in range(4):
off = i * 12
cfg = data[off]
evl = data[off + 1]
freq_level = (cfg >> 6) & 0x03
sens = cfg & 0x0F
condition = (evl >> 4) & 0x0F
loop_state = (evl >> 3) & 0x01
car_state = (evl >> 2) & 0x01
misc_type = evl & 0x03
freq = int.from_bytes(data[off+2:off+5], 'little') # u24le
diff = int.from_bytes(data[off+5:off+8], 'little', signed=True) # i24le
misc = int.from_bytes(data[off+8:off+12], 'little') # u32le
LEVELS = ["high", "mid_high", "mid_low", "low"]
MISC = ["time", "cut_count", "flow_count", "relay_count"]
channels.append({
"ch": i + 1,
"level": LEVELS[freq_level],
"iscar": car_state == 1,
"loop_ok": loop_state == 0,
"freq": freq,
"diff": diff,
"sens": sens,
"cndtn": condition,
"misc": {"type": MISC[misc_type], "value": misc},
})
return channels
def loop_data_json(channels, msg_id, ts):
parts = []
for c in channels:
parts.append('{"ch":%d,"level":"%s","iscar":%s,"loop_ok":%s,"freq":%d,"diff":%d,'
'"sens":%d,"cndtn":%d,"misc":{"type":"%s","value":%d}}'
% (c["ch"], c["level"], str(c["iscar"]).lower(), str(c["loop_ok"]).lower(),
c["freq"], c["diff"], c["sens"], c["cndtn"], c["misc"]["type"], c["misc"]["value"]))
return '{"msg_id":%d,"cmd":"loop_data","ts":%d,"data":{"channels":[%s]}}' % (msg_id, ts, ",".join(parts))
# ==================== 测试用例 ====================
def test_frame_parser():
"""帧解析状态机: 组包 → 拆包 → 校验"""
# 构造 0xC0 传感帧(4 路)
ch1 = bytes([0x12, 0x00]) + (69418).to_bytes(3, 'little') + struct.pack('<i', 7)[:3] + (0).to_bytes(4, 'little')
ch2 = bytes([0x12, 0x04]) + (256).to_bytes(3, 'little') + struct.pack('<i', 255)[:3] + (350).to_bytes(4, 'little')
ch3 = bytes([0x12, 0x09]) + (0).to_bytes(3, 'little') + struct.pack('<i', 0)[:3] + (3).to_bytes(4, 'little')
ch4 = bytes([0x12, 0x02]) + (62100).to_bytes(3, 'little') + struct.pack('<i', 5)[:3] + (128).to_bytes(4, 'little')
sens_data = ch1 + ch2 + ch3 + ch4
payload = bytes([0x0C, 0x01]) + sens_data # SensType=0x0C + SubPkgFlag + SensData
frame = build_frame(0x00, 0xC0, payload)
# 模拟 frame_parser.feed 状态机(与 Lua 一致)
ST = {"IDLE":0,"ADDR":1,"LEN":2,"CMD":3,"VALUE":4,"CHECK_X":5,"CHECK_S":6}
state, cmd, value_len, value_buf, chk_x, chk_s = ST["IDLE"], 0, 0, b"", 0, 0
got_cmd, got_payload = None, None
for b in frame:
if state == ST["IDLE"]:
if b == MAGIC: state = ST["ADDR"]
elif state == ST["ADDR"]:
chk_x, chk_s = b, b; state = ST["LEN"]
elif state == ST["LEN"]:
chk_x ^= b; chk_s = (chk_s + b) & 0xFF
assert b >= 1, "LEN<1"
value_len = b - 1
value_buf = b""; state = ST["CMD"]
elif state == ST["CMD"]:
chk_x ^= b; chk_s = (chk_s + b) & 0xFF
cmd = b
state = ST["VALUE"] if value_len > 0 else ST["CHECK_X"]
elif state == ST["VALUE"]:
chk_x ^= b; chk_s = (chk_s + b) & 0xFF
value_buf += bytes([b])
if len(value_buf) >= value_len: state = ST["CHECK_X"]
elif state == ST["CHECK_X"]:
assert b == chk_x, f"XOR fail: got {b:02X} exp {chk_x:02X}"
state = ST["CHECK_S"]
elif state == ST["CHECK_S"]:
assert b == chk_s, f"SUM fail: got {b:02X} exp {chk_s:02X}"
got_cmd, got_payload = cmd, value_buf
state = ST["IDLE"]
assert got_cmd == 0xC0, f"cmd={got_cmd:02X}"
assert got_payload == payload, "payload 不一致"
print(f"[OK] frame_parser: 帧 {len(frame)}B, cmd=0xC0, payload {len(payload)}B, 校验通过")
return got_payload
def test_parse_sensor(payload):
channels = parse_sensor(payload)
assert len(channels) == 4
# 断言关键字段(协议示例 §3.07 核对)
c1 = channels[0]
assert c1["level"] == "high" and c1["freq"] == 69418 and c1["diff"] == 7 and c1["sens"] == 2, c1
c2 = channels[1]
assert c2["iscar"] is True and c2["freq"] == 256 and c2["diff"] == 255 and c2["misc"]["value"] == 350, c2
c3 = channels[2]
assert c3["loop_ok"] is False and c3["misc"]["type"] == "cut_count" and c3["misc"]["value"] == 3, c3
c4 = channels[3]
assert c4["misc"]["type"] == "flow_count" and c4["misc"]["value"] == 128, c4
print(f"[OK] parse_sensor: 4 路解析正确(level/freq/diff/sens/iscar/loop_ok/misc 全部核对)")
# loop_data JSON 输出示例
j = loop_data_json(channels, 1, 1719000100)
print(f"[OK] loop_data JSON({len(j)}B): {j[:150]}...")
return channels
def test_event_detect(channels):
"""沿检测: 路2 car 0→1 应出 car_enter"""
# 模拟 proto_conv.detect_events(prev 全 0)
prev_car, prev_loop = [0,0,0,0], [0,0,0,0]
events = []
for i, c in enumerate(channels):
cur_car = 1 if c["iscar"] else 0
cur_loop = 0 if c["loop_ok"] else 1
if prev_loop[i] == 0 and cur_loop == 0:
if prev_car[i] == 0 and cur_car == 1:
events.append({"type": "car_enter", "ch": i+1, "value": c["misc"]["value"]})
elif prev_car[i] == 1 and cur_car == 0:
events.append({"type": "car_leave", "ch": i+1, "value": c["misc"]["value"]})
if prev_loop[i] == 0 and cur_loop == 1:
events.append({"type": "loop_cut", "ch": i+1, "value": 0})
elif prev_loop[i] == 1 and cur_loop == 0:
events.append({"type": "loop_restore", "ch": i+1, "value": 0})
prev_car[i], prev_loop[i] = cur_car, cur_loop
types = [e["type"] for e in events]
assert "car_enter" in types and "loop_cut" in types, types
print(f"[OK] detect_events: 首帧事件 {types}(路2 有车→car_enter, 路3 断开→loop_cut)")
def test_evt_queue_json():
"""event_report 组包格式(协议 §5.3)"""
events = [{"type":"car_enter","ch":1,"value":350}, {"type":"car_leave","ch":1,"value":1200}]
parts = ['{"type":"%s","ch":%d,"value":%d}' % (e["type"], e["ch"], e["value"]) for e in events]
payload = '{"msg_id":1,"cmd":"event_report","ts":1719000100,"data":{"events":[%s]}}' % ",".join(parts)
assert '"cmd":"event_report"' in payload and '"events":[' in payload
print(f"[OK] event_report JSON({len(payload)}B): {payload[:130]}...")
if __name__ == "__main__":
print("=== vd960Air 帧协议参考验证 ===")
payload = test_frame_parser()
channels = test_parse_sensor(payload)
test_event_detect(channels)
test_evt_queue_json()
print("\n全部通过 ✓")
@@ -0,0 +1,217 @@
--[[
@module sim_load_test
@summary vd960Air 线 + (, lua5.3)
@version 0.2
@date 2026.09.10
@usage
cd vd960Air && lua5.3 tools/sim_load_test.lua
, lua5.3 + LuatOS API , main.lua ,
luac -p():
1) require config.lua return, CFG.CFG_xxx
"attempt to index a boolean value"(2026-09-10 )
2) LuatOS API mstick()( mcu.ticks())
3) 访( __index )
4) /
5)
PC 线, ( tools/ , main.lua require )
]]
-- ==================== 模块搜索路径 ====================
package.path = "./?.lua;./mqtt/?.lua;./tools/?.lua;" .. package.path
package.preload["netdrv_4g"] = function() return {} end -- 外部库(官方组件)打桩
-- ==================== 记录 + 通用桩工具 ====================
local TOP_CALLS = {} -- 加载期(顶层)发生的 API 调用
local function rec(s) TOP_CALLS[#TOP_CALLS + 1] = s end
local function stub(name)
return setmetatable({}, {
__index = function(_, k)
return function(...) rec(name .. "." .. tostring(k)); return 0 end
end,
})
end
-- ==================== 严格全局检查 ====================
-- 任何对未定义全局变量的访问立即报错(能直接抓出 mstick 这类"想当然"的 API)
local REAL_G = _G
setmetatable(_G, {
__index = function(_, k)
error("访问未定义的全局变量: " .. tostring(k)
.. " (LuatOS 无此 API 或模块未加载/未 return)", 2)
end,
})
-- ==================== LuatOS API 桩 ====================
_G.log = {
info = function(...) end,
warn = function(...) end,
error = function(...) end,
debug = function(...) end,
}
_G.mcu = {
ticks = function() return 123456789 end,
hz = function() return 1000 end,
}
_G.sys = {
taskInit = function(f, ...) rec("sys.taskInit") end,
taskInitEx = function(f, name) rec("sys.taskInitEx(" .. tostring(name) .. ")") end,
run = function() rec("sys.run") end,
wait = function(ms) return true end,
waitUntil = function(n, ms) return false end,
waitMsg = function(t, n) return nil end,
sendMsg = function(...) end,
publish = function(...) end,
subscribe = function(...) end,
cleanMsg = function(...) end,
timerStart = function(...) end,
timerStop = function(...) end,
timerLoopStart = function(...) end,
}
-- json: LuatOS 内置库(此处为极简桩, 仅保证自检流程走通; 不校验 JSON 内容)
_G.json = {
encode = function(t) return "{}" end,
decode = function(s)
if type(s) ~= "string" or #s == 0 then return nil end
return {}
end,
}
_G.mobile = {
imei = function() return "864317081893755" end,
iccid = function() return "89860620123456789012" end,
imsi = function() return "460001234567890" end,
msisdn = function() return "" end,
csq = function() return 24 end,
}
_G.uart = {
setup = function(id, baud, data, stop, parity)
rec(string.format("uart.setup(id=%s, baud=%s, data=%s, stop=%s, parity=%s)",
tostring(id), tostring(baud), tostring(data), tostring(stop), tostring(parity)))
end,
on = function(id, evt) rec("uart.on(id=" .. tostring(id) .. ", evt=" .. tostring(evt) .. ")") end,
write = function(id, data) return #data end,
read = function(id) return "" end,
close = function(id) end,
}
_G.socket = {
adapter = function(t) return true end,
dft = function() return 0 end,
setDNS = function(...) end,
connect = function(...) return 0 end,
}
local AUTH_ARG = nil
_G.mqtt = {
create = function(ssl, host, port)
rec(string.format("mqtt.create(ssl=%s, host=%s, port=%s)",
tostring(ssl), tostring(host), tostring(port)))
local cli = stub("mqtt_client")
cli.auth = function(self, cid, user, pass, clean)
AUTH_ARG = { cid = cid, user = user, pass = pass, clean = clean }
rec(string.format("mqtt_client:auth(client_id=%s, user=%s, pass=%s, clean=%s)",
tostring(cid), tostring(user), tostring(pass), tostring(clean)))
return 0
end
return cli
end,
}
-- ==================== 真实加载 main.lua ====================
print("============ vd960Air 离线加载自检 ============\n")
local ok, err = pcall(dofile, "main.lua")
if not ok then
print("\n✗ 加载 main.lua 失败:\n " .. tostring(err))
os.exit(1)
end
print("✓ main.lua 按真实顺序加载成功(含全部 require)\n")
-- ==================== 断言: 模块返回值与配置 ====================
local failures = 0
local function check(desc, cond, extra)
if cond then
print("" .. desc .. (extra and (" [" .. tostring(extra) .. "]") or ""))
else
failures = failures + 1
print("" .. desc .. (extra and (" [" .. tostring(extra) .. "]") or ""))
end
end
local function is_table(v) return type(v) == "table" end
print("============ 断言 ============")
local cfg = require "config"
check('require "config" 返回 table(非 boolean)', is_table(cfg), type(cfg))
check("CFG_MQTT_HOST", cfg.CFG_MQTT_HOST, cfg.CFG_MQTT_HOST)
check("CFG_MQTT_PORT", cfg.CFG_MQTT_PORT, cfg.CFG_MQTT_PORT)
check("CFG_MQTT_USER", cfg.CFG_MQTT_USER, cfg.CFG_MQTT_USER)
check("CFG_MQTT_PASS 已设置", type(cfg.CFG_MQTT_PASS) == "string" and #cfg.CFG_MQTT_PASS > 0)
check("CFG_UART_BAUD", cfg.CFG_UART_BAUD, cfg.CFG_UART_BAUD)
check("CFG_MQTT_CLIENT_ID", type(cfg.CFG_MQTT_CLIENT_ID) == "string", cfg.CFG_MQTT_CLIENT_ID)
check("CFG_TOPIC_UP 已生成", type(cfg.CFG_TOPIC_UP) == "string", cfg.CFG_TOPIC_UP)
check("全局 mstick 已定义(函数)", type(_G.mstick) == "function")
for _, m in ipairs({ "uart_app", "app_iot", "frame_parser", "proto_conv", "evt_queue", "clock", "link_info" }) do
local ok2, v = pcall(require, m)
check('require "' .. m .. '" 返回 table', ok2 and is_table(v), ok2 and type(v) or tostring(v))
end
-- ==================== 冒烟: 调用关键函数(抓未定义 API) ====================
print("\n============ 冒烟调用(不带硬件) ============")
local function smoke(desc, f, ...)
local ok3, e = pcall(f, ...)
check(desc, ok3, ok3 and "" or tostring(e))
end
local link_info = require "link_info"
local clock = require "clock"
local uart_app = require "uart_app"
local app_iot = require "app_iot"
smoke("mstick() 可调用", _G.mstick)
smoke("clock.now()", clock.now)
smoke("clock.synced()", clock.synced)
smoke("link_info.get()", link_info.get)
smoke("uart_app.get_loop_status()", uart_app.get_loop_status)
smoke("uart_app.poll()", uart_app.poll)
smoke("app_iot.on_connected()", app_iot.on_connected)
smoke("app_iot.init()", app_iot.init)
-- ==================== MQTT 连接参数核对 ====================
print("\n============ MQTT 参数 ============")
if AUTH_ARG then
check("auth 用户名取自 config", AUTH_ARG.user == cfg.CFG_MQTT_USER, tostring(AUTH_ARG.user))
check("auth 密码非空", type(AUTH_ARG.pass) == "string" and #AUTH_ARG.pass > 0)
check("auth client_id 非空", type(AUTH_ARG.cid) == "string" and #AUTH_ARG.cid > 0, tostring(AUTH_ARG.cid))
else
print(" ! 尚未走到 mqtt:auth()(由 mqtt_main 连接任务在连接成功前调用)")
end
-- ==================== 加载期 API 调用清单 ====================
print("\n============ 加载期(顶层)API 调用 ============")
local seen = {}
for _, c in ipairs(TOP_CALLS) do
if not seen[c] then
seen[c] = true
print(" - " .. c)
end
end
-- ==================== 结论 ====================
print("\n============ 结论 ============")
if failures == 0 then
print(" ✓ 离线自检全部通过(" .. #TOP_CALLS .. " 项加载期调用)")
os.exit(0)
else
print(" ✗ 失败 " .. failures .. "")
os.exit(1)
end