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:
+176
-171
@@ -1,171 +1,176 @@
|
|||||||
# Prerequisites
|
# Prerequisites
|
||||||
*.d
|
*.d
|
||||||
*.luac
|
*.luac
|
||||||
*.lnks
|
*.lnks
|
||||||
*.pdf
|
*.pdf
|
||||||
.vscode
|
.vscode
|
||||||
vscode.code-workspace
|
vscode.code-workspace
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
# Object files
|
# Object files
|
||||||
*.o
|
*.o
|
||||||
*.ko
|
*.ko
|
||||||
*.obj
|
*.obj
|
||||||
*.elf
|
*.elf
|
||||||
|
|
||||||
# Linker output
|
# Linker output
|
||||||
*.ilk
|
*.ilk
|
||||||
*.map
|
*.map
|
||||||
*.exp
|
*.exp
|
||||||
|
|
||||||
# Precompiled Headers
|
# Precompiled Headers
|
||||||
*.gch
|
*.gch
|
||||||
*.pch
|
*.pch
|
||||||
|
|
||||||
# Libraries
|
# Libraries
|
||||||
*.lib
|
*.lib
|
||||||
*.a
|
*.a
|
||||||
*.la
|
*.la
|
||||||
*.lo
|
*.lo
|
||||||
|
|
||||||
# Shared objects (inc. Windows DLLs)
|
# Shared objects (inc. Windows DLLs)
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.so.*
|
*.so.*
|
||||||
*.dylib
|
*.dylib
|
||||||
|
|
||||||
# Executables
|
# Executables
|
||||||
*.exe
|
*.exe
|
||||||
*.out
|
*.out
|
||||||
*.app
|
*.app
|
||||||
*.i*86
|
*.i*86
|
||||||
*.x86_64
|
*.x86_64
|
||||||
*.hex
|
*.hex
|
||||||
|
|
||||||
# Debug files
|
# Debug files
|
||||||
*.dSYM/
|
*.dSYM/
|
||||||
*.su
|
*.su
|
||||||
*.idb
|
*.idb
|
||||||
*.pdb
|
*.pdb
|
||||||
|
|
||||||
# Kernel Module Compile Results
|
# Kernel Module Compile Results
|
||||||
#*.mod*
|
#*.mod*
|
||||||
*.cmd
|
*.cmd
|
||||||
.tmp_versions/
|
.tmp_versions/
|
||||||
modules.order
|
modules.order
|
||||||
Module.symvers
|
Module.symvers
|
||||||
Mkfile.old
|
Mkfile.old
|
||||||
dkms.conf
|
dkms.conf
|
||||||
|
|
||||||
bsp/air302/FlashToolCLI/
|
bsp/air302/FlashToolCLI/
|
||||||
bsp/air302/tmp/
|
bsp/air302/tmp/
|
||||||
bsp/air302/local*
|
bsp/air302/local*
|
||||||
bsp/air302/disk/
|
bsp/air302/disk/
|
||||||
*.zip
|
*.zip
|
||||||
tools/
|
tools/
|
||||||
testcase/
|
testcase/
|
||||||
oldmodule/
|
oldmodule/
|
||||||
olddemo/
|
olddemo/
|
||||||
script/
|
script/
|
||||||
|
|
||||||
|
|
||||||
bsp/air640w/disk/
|
bsp/air640w/disk/
|
||||||
bsp/air640w/tmp/
|
bsp/air640w/tmp/
|
||||||
bsp/air640w/local*
|
bsp/air640w/local*
|
||||||
|
|
||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
*.tlv
|
*.tlv
|
||||||
*.fs
|
*.fs
|
||||||
bsp/air302/air302_sdk/**
|
bsp/air302/air302_sdk/**
|
||||||
/bsp/air724/sdk/out
|
/bsp/air724/sdk/out
|
||||||
/bsp/air724/sdk/hex
|
/bsp/air724/sdk/hex
|
||||||
/bsp/air724/sdk/prebuilts/win32
|
/bsp/air724/sdk/prebuilts/win32
|
||||||
|
|
||||||
*.cmake
|
*.cmake
|
||||||
*.xmake
|
*.xmake
|
||||||
|
|
||||||
debug.log
|
debug.log
|
||||||
|
|
||||||
量产文件
|
量产文件
|
||||||
|
|
||||||
CMakefiles
|
CMakefiles
|
||||||
bsp/pc/build/
|
bsp/pc/build/
|
||||||
bsp/win32/build/
|
bsp/win32/build/
|
||||||
bsp/win32/release/
|
bsp/win32/release/
|
||||||
bsp/pico/build/
|
bsp/pico/build/
|
||||||
bsp/linux/build/
|
bsp/linux/build/
|
||||||
bsp/zephyr/
|
bsp/zephyr/
|
||||||
|
|
||||||
|
|
||||||
build/
|
build/
|
||||||
*.bat
|
*.bat
|
||||||
cmd.lnk
|
cmd.lnk
|
||||||
|
|
||||||
luat/demo/
|
luat/demo/
|
||||||
tools/snippet.json
|
tools/snippet.json
|
||||||
|
|
||||||
self_docs/
|
self_docs/
|
||||||
# cursor编程相关文件
|
# cursor编程相关文件
|
||||||
.cursor/*
|
.cursor/*
|
||||||
openspec/*
|
openspec/*
|
||||||
.codex
|
.codex
|
||||||
|
|
||||||
app_engine/
|
app_engine/
|
||||||
bsp/
|
bsp/
|
||||||
luat/
|
luat/
|
||||||
|
|
||||||
# MCP server
|
# MCP server
|
||||||
.chroma_code/
|
.chroma_code/
|
||||||
components/
|
components/
|
||||||
components/h264/test/test_h264
|
components/h264/test/test_h264
|
||||||
testcase/ctx.json
|
testcase/ctx.json
|
||||||
bsp/pc/pclogs/
|
bsp/pc/pclogs/
|
||||||
bsp/pc/spidrv/
|
bsp/pc/spidrv/
|
||||||
bsp/pc/fskv.bin
|
bsp/pc/fskv.bin
|
||||||
bsp/pc/otp.bin
|
bsp/pc/otp.bin
|
||||||
|
|
||||||
.worktree
|
.worktree
|
||||||
.worktrees/
|
.worktrees/
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Local AI / IDE tooling state (Mavis, Antigravity, OpenCode, etc.)
|
# Local AI / IDE tooling state (Mavis, Antigravity, OpenCode, etc.)
|
||||||
.mavis/
|
.mavis/
|
||||||
.antigravitycli/
|
.antigravitycli/
|
||||||
.opencode/
|
.opencode/
|
||||||
/docs/superpowers
|
/docs/superpowers
|
||||||
# Media artifacts accidentally saved at repo root by tooling
|
# Media artifacts accidentally saved at repo root by tooling
|
||||||
/matrix-media-*.mp4
|
/matrix-media-*.mp4
|
||||||
/pgfs_flow.mp4
|
/pgfs_flow.mp4
|
||||||
/pcconf/
|
/pcconf/
|
||||||
/pgfs_video/
|
/pgfs_video/
|
||||||
/bsp/air1601/artifacts/
|
/bsp/air1601/artifacts/
|
||||||
|
|
||||||
# Local worktrees (not to be tracked from the main checkout)
|
# Local worktrees (not to be tracked from the main checkout)
|
||||||
# Note: .claude/skills/ is intentionally tracked so /luatos-hw-test ships with the repo.
|
# Note: .claude/skills/ is intentionally tracked so /luatos-hw-test ships with the repo.
|
||||||
/.claude/settings.local.json
|
/.claude/settings.local.json
|
||||||
/.claude/worktrees/
|
/.claude/worktrees/
|
||||||
/.fleet-worktrees/
|
/.fleet-worktrees/
|
||||||
|
|
||||||
# PC BSP runtime artifacts (not to be tracked)
|
# PC BSP runtime artifacts (not to be tracked)
|
||||||
/bsp/pc/CrashDump_*.dmp
|
/bsp/pc/CrashDump_*.dmp
|
||||||
/bsp/pc/LastCoverageResults.log
|
/bsp/pc/LastCoverageResults.log
|
||||||
/bsp/pc/test_output.txt
|
/bsp/pc/test_output.txt
|
||||||
/bsp/pc/test_sqlite3_suite.db*
|
/bsp/pc/test_sqlite3_suite.db*
|
||||||
/bsp/pc/test_dir/
|
/bsp/pc/test_dir/
|
||||||
/bsp/pc/test_dir1/
|
/bsp/pc/test_dir1/
|
||||||
/bsp/pc/test_fs_dir/
|
/bsp/pc/test_fs_dir/
|
||||||
/bsp/pc/file2.txt
|
/bsp/pc/file2.txt
|
||||||
/bsp/pc/mount_test.txt
|
/bsp/pc/mount_test.txt
|
||||||
/bsp/pc/os_test_*.txt
|
/bsp/pc/os_test_*.txt
|
||||||
/bsp/pc/test_io.txt
|
/bsp/pc/test_io.txt
|
||||||
|
|
||||||
*.dmp
|
*.dmp
|
||||||
testresult/
|
testresult/
|
||||||
app_store/
|
app_store/
|
||||||
*.bin
|
*.bin
|
||||||
single_app.json
|
single_app.json
|
||||||
.qoder/
|
.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 服务器 ====================
|
-- ==================== MQTT 服务器 ====================
|
||||||
-- 商用服务器地址和端口(替换为实际值)
|
-- 测试服务器(2026-09-10 用户提供): 110.41.131.144:1883, admin/password
|
||||||
-- 注: 后续 vd960DBN 更新版本后,经 BLE 设置的服务器参数会同步下发覆盖此处(配置同步, P1)
|
-- 注: 后续 vd960DBN 更新版本后,经 BLE 设置的服务器参数会同步下发覆盖此处(配置同步, P1)
|
||||||
CFG_MQTT_HOST = "lbsmqtt.airm2m.com"
|
-- ⚠ 商用环境须替换为平台实际地址; 密码不应长期写死在代码里(应走 §2.2 配置同步链路)
|
||||||
CFG_MQTT_PORT = 1884
|
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 同一设备)
|
-- 设备序列码: 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" -- 服务器下发
|
CFG_TOPIC_DOWN = "dld960/" .. CFG_DEV_SERIAL .. "/srv" -- 服务器下发
|
||||||
|
|
||||||
-- MQTT ClientID: 用 IMEI 保证唯一(与序列码解耦;鉴权待确认)
|
-- MQTT ClientID: 用 IMEI 保证唯一(与序列码解耦;鉴权待确认)
|
||||||
CFG_MQTT_CLIENT_ID = "vd960Air-" .. mobile.imei()
|
CFG_MQTT_CLIENT_ID = "vd960Air-" .. (mobile.imei() or "unknown")
|
||||||
|
|
||||||
-- ==================== UART(对接 vd960DBN) ====================
|
-- ==================== UART(对接 vd960DBN) ====================
|
||||||
-- vd960DBN 侧: CH32V208 串口1(PB6=TX, PB7=RX)
|
-- 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
|
-- 用户决策 2026-08-31: 上行附加 link 对象 + initialize extra_info 含 imei/iccid
|
||||||
-- true = 上行 JSON 顶层注入 link 对象(见 link_info.lua)
|
-- true = 上行 JSON 顶层注入 link 对象(见 link_info.lua)
|
||||||
CFG_LINK_ENABLE = true
|
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
|
@module main
|
||||||
@summary vd960Air — Air8781P(Air780EPM) 4G 模块 LuatOS 应用入口(方案 C MVP)
|
@summary vd960Air — Air8781P(Air780EPM) 4G 模块 LuatOS 应用入口(方案 C MVP)
|
||||||
@version 0.3
|
@version 0.3.1
|
||||||
@date 2026.08.31
|
@date 2026.09.10
|
||||||
@usage
|
@usage
|
||||||
vd960DBN 车检器在有线网络失效时的 4G 兜底通道(方案 C: Air780 协议转换):
|
vd960DBN 车检器在有线网络失效时的 4G 兜底通道(方案 C: Air780 协议转换):
|
||||||
|
|
||||||
@@ -22,6 +22,10 @@ log.info("main", PROJECT, VERSION)
|
|||||||
-- ==================== 加载配置 ====================
|
-- ==================== 加载配置 ====================
|
||||||
require "config"
|
require "config"
|
||||||
|
|
||||||
|
-- ============ 统一毫秒时基(全局 mstick(), 必须早于业务模块) ============
|
||||||
|
-- 修复 2026-09-10: 工程原调用 mstick() 但 LuatOS 无此 API, 上板必报 nil
|
||||||
|
require "mstick"
|
||||||
|
|
||||||
-- ==================== 网络环境检测看门狗 ====================
|
-- ==================== 网络环境检测看门狗 ====================
|
||||||
require "network_watchdog"
|
require "network_watchdog"
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,8 @@ local function mqtt_client_main_task_func()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- 配置mqtt client对象的client id,username,password和clean session标志
|
-- 配置mqtt client对象的client id,username,password和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
|
if not result then
|
||||||
log.error("mqtt_client_main_task_func", "mqtt_client:auth error")
|
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 ┌──────────┐
|
┌─────────────────┐ UART1 (115200) ┌──────────────────────┐ MQTT 3.1.1 ┌──────────┐
|
||||||
│ vd960DBN │ 帧协议(0x7D) │ Air8781P 整板 │ JSON │ 云平台 │
|
│ vd960DBN │ 0x7F 帧字节流 │ Air8781P 整板 │ JSON │ 云平台 │
|
||||||
│ CH32V208 │ ◄───────────────► │ Air780EPM (LuatOS) │ ─────────────► │ dld960 │
|
│ CH32V208 │ ◄───────────────► │ Air780EPM (LuatOS) │ ─────────────► │ dld960 │
|
||||||
│ (车检器通信MCU) │ PB6=TX / PB7=RX │ vd960Air 应用 │ dld960/{sn}/ │ 主题族 │
|
│ (车检器通信MCU) │ PB6=TX / PB7=RX │ vd960Air 应用 │ dld960/{sn}/ │ 主题族 │
|
||||||
└─────────────────┘ └──────────────────────┘ {dev,srv} └──────────┘
|
└─────────────────┘ └──────────────────────┘ {dev,srv} └──────────┘
|
||||||
@@ -193,8 +193,9 @@ vd960DBN 与地感 MCU 的串口逻辑(loop_uart_proto.c,UART2,0x7F 帧):
|
|||||||
|
|
||||||
```
|
```
|
||||||
vd960Air/
|
vd960Air/
|
||||||
├── main.lua # 入口: 加载 config/看门狗/4G网卡/uart_app/app_iot/mqtt_main
|
├── main.lua # 入口: 加载 config/mstick/看门狗/4G网卡/uart_app/app_iot/mqtt_main
|
||||||
├── config.lua # 配置: MQTT 服务器/序列号/主题/UART/上报节奏/link 开关
|
├── 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)
|
├── frame_parser.lua # ★ 0x7F 帧解析状态机(Lua 版 lup_feed_byte,IDLE→HEADER→VALUE→CHECK)
|
||||||
├── proto_conv.lua # ★ 0xC0 传感帧 → loop_data + car_state/loop_state 沿检测
|
├── proto_conv.lua # ★ 0xC0 传感帧 → loop_data + car_state/loop_state 沿检测
|
||||||
├── evt_queue.lua # ★ event_report 队列 + ACK 状态机(16深/5s×3/跨重连同 msg_id)
|
├── evt_queue.lua # ★ event_report 队列 + ACK 状态机(16深/5s×3/跨重连同 msg_id)
|
||||||
@@ -205,7 +206,8 @@ vd960Air/
|
|||||||
├── netdrv_device.lua # 网卡: 仅 4G(官方 netdrv_4g)
|
├── netdrv_device.lua # 网卡: 仅 4G(官方 netdrv_4g)
|
||||||
├── network_watchdog.lua # 网络看门狗(官方原样)
|
├── network_watchdog.lua # 网络看门狗(官方原样)
|
||||||
├── tools/
|
├── 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/
|
||||||
├── mqtt_main.lua # MQTT 客户端(连接成功 → initialize + 未决事件重发)
|
├── mqtt_main.lua # MQTT 客户端(连接成功 → initialize + 未决事件重发)
|
||||||
├── mqtt_receiver.lua # 下行分发: event_report ACK 路由 + report_config 时钟校准
|
├── mqtt_receiver.lua # 下行分发: event_report ACK 路由 + report_config 时钟校准
|
||||||
@@ -243,7 +245,9 @@ vd960Air/
|
|||||||
|
|
||||||
- [ ] vd960DBN UART1 帧格式是否按 §3 设计稿(0x7D 帧),或用户文档另有规定
|
- [ ] vd960DBN UART1 帧格式是否按 §3 设计稿(0x7D 帧),或用户文档另有规定
|
||||||
- [ ] dev_serial 获取方式: config 写死 vs UART 握手动态下发(当前 config 写死)
|
- [ ] 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_status_query` / `link_status_report`)是否纳入主协议 4G 章节
|
||||||
- [ ] 通道切换策略: 有线失效判定、4G 启用条件、回切策略
|
- [ ] 通道切换策略: 有线失效判定、4G 启用条件、回切策略
|
||||||
- [ ] 心跳: 4G 通道 heartbeat 间隔/内容是否与有线一致
|
- [ ] 心跳: 4G 通道 heartbeat 间隔/内容是否与有线一致
|
||||||
@@ -255,6 +259,7 @@ vd960Air/
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| P0 | 工程骨架 + 帧协议 + MQTT 桥接 + link 注入 | ✅ 完成 |
|
| P0 | 工程骨架 + 帧协议 + MQTT 桥接 + link 注入 | ✅ 完成 |
|
||||||
| **P0.5** | **方案 C MVP: 0x7F 帧解析 + 0xC0→loop_data + 沿检测→event_report + ACK 状态机 + initialize/heartbeat + 时钟校准**(2026-08-31) | ✅ 代码完成,待板级 |
|
| **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 版本 |
|
| P1 | 下行命令转换(loop_param_*/log_*/ota_* 转 0x7F)+ 0x7D 配置同步下发 + vd960DBN UART1 通道联调 | ⏳ 待板/待 vd960DBN 版本 |
|
||||||
| P2 | 板级联调: 帧校验、背压、掉线重连、看门狗 | ⏳ 待板 |
|
| P2 | 板级联调: 帧校验、背压、掉线重连、看门狗 | ⏳ 待板 |
|
||||||
| P3 | 通道切换(策略待讨论)+ 脱机日志/OTA 走 4G | ⏳ 规划 |
|
| P3 | 通道切换(策略待讨论)+ 脱机日志/OTA 走 4G | ⏳ 规划 |
|
||||||
@@ -265,4 +270,33 @@ vd960Air/
|
|||||||
- [ ] BLE 设置服务器/topic 参数时(0x13/0x15 等),同步经 UART1 下发 Air780 配置
|
- [ ] BLE 设置服务器/topic 参数时(0x13/0x15 等),同步经 UART1 下发 Air780 配置
|
||||||
- [ ] 通道切换策略(待讨论: 动态自动 vs 人工设置;暂定默认 4G)
|
- [ ] 通道切换策略(待讨论: 动态自动 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 许可。
|
> 基底来源: 合宙官方 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
|
||||||
Reference in New Issue
Block a user