diff --git a/luatos/air780epm/.gitignore b/luatos/air780epm/.gitignore index d009f81..c3e396c 100644 --- a/luatos/air780epm/.gitignore +++ b/luatos/air780epm/.gitignore @@ -1,171 +1,176 @@ -# Prerequisites -*.d -*.luac -*.lnks -*.pdf -.vscode -vscode.code-workspace -.idea - -# Object files -*.o -*.ko -*.obj -*.elf - -# Linker output -*.ilk -*.map -*.exp - -# Precompiled Headers -*.gch -*.pch - -# Libraries -*.lib -*.a -*.la -*.lo - -# Shared objects (inc. Windows DLLs) -*.dll -*.so -*.so.* -*.dylib - -# Executables -*.exe -*.out -*.app -*.i*86 -*.x86_64 -*.hex - -# Debug files -*.dSYM/ -*.su -*.idb -*.pdb - -# Kernel Module Compile Results -#*.mod* -*.cmd -.tmp_versions/ -modules.order -Module.symvers -Mkfile.old -dkms.conf - -bsp/air302/FlashToolCLI/ -bsp/air302/tmp/ -bsp/air302/local* -bsp/air302/disk/ -*.zip -tools/ -testcase/ -oldmodule/ -olddemo/ -script/ - - -bsp/air640w/disk/ -bsp/air640w/tmp/ -bsp/air640w/local* - -__pycache__ -*.pyc - -*.tlv -*.fs -bsp/air302/air302_sdk/** -/bsp/air724/sdk/out -/bsp/air724/sdk/hex -/bsp/air724/sdk/prebuilts/win32 - -*.cmake -*.xmake - -debug.log - -量产文件 - -CMakefiles -bsp/pc/build/ -bsp/win32/build/ -bsp/win32/release/ -bsp/pico/build/ -bsp/linux/build/ -bsp/zephyr/ - - -build/ -*.bat -cmd.lnk - -luat/demo/ -tools/snippet.json - -self_docs/ -# cursor编程相关文件 -.cursor/* -openspec/* -.codex - -app_engine/ -bsp/ -luat/ - -# MCP server -.chroma_code/ -components/ -components/h264/test/test_h264 -testcase/ctx.json -bsp/pc/pclogs/ -bsp/pc/spidrv/ -bsp/pc/fskv.bin -bsp/pc/otp.bin - -.worktree -.worktrees/ -*.log - -# Local AI / IDE tooling state (Mavis, Antigravity, OpenCode, etc.) -.mavis/ -.antigravitycli/ -.opencode/ -/docs/superpowers -# Media artifacts accidentally saved at repo root by tooling -/matrix-media-*.mp4 -/pgfs_flow.mp4 -/pcconf/ -/pgfs_video/ -/bsp/air1601/artifacts/ - -# Local worktrees (not to be tracked from the main checkout) -# Note: .claude/skills/ is intentionally tracked so /luatos-hw-test ships with the repo. -/.claude/settings.local.json -/.claude/worktrees/ -/.fleet-worktrees/ - -# PC BSP runtime artifacts (not to be tracked) -/bsp/pc/CrashDump_*.dmp -/bsp/pc/LastCoverageResults.log -/bsp/pc/test_output.txt -/bsp/pc/test_sqlite3_suite.db* -/bsp/pc/test_dir/ -/bsp/pc/test_dir1/ -/bsp/pc/test_fs_dir/ -/bsp/pc/file2.txt -/bsp/pc/mount_test.txt -/bsp/pc/os_test_*.txt -/bsp/pc/test_io.txt - -*.dmp -testresult/ -app_store/ -*.bin -single_app.json -.qoder/ - - +# Prerequisites +*.d +*.luac +*.lnks +*.pdf +.vscode +vscode.code-workspace +.idea + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +#*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +bsp/air302/FlashToolCLI/ +bsp/air302/tmp/ +bsp/air302/local* +bsp/air302/disk/ +*.zip +tools/ +testcase/ +oldmodule/ +olddemo/ +script/ + + +bsp/air640w/disk/ +bsp/air640w/tmp/ +bsp/air640w/local* + +__pycache__ +*.pyc + +*.tlv +*.fs +bsp/air302/air302_sdk/** +/bsp/air724/sdk/out +/bsp/air724/sdk/hex +/bsp/air724/sdk/prebuilts/win32 + +*.cmake +*.xmake + +debug.log + +量产文件 + +CMakefiles +bsp/pc/build/ +bsp/win32/build/ +bsp/win32/release/ +bsp/pico/build/ +bsp/linux/build/ +bsp/zephyr/ + + +build/ +*.bat +cmd.lnk + +luat/demo/ +tools/snippet.json + +self_docs/ +# cursor编程相关文件 +.cursor/* +openspec/* +.codex + +app_engine/ +bsp/ +luat/ + +# MCP server +.chroma_code/ +components/ +components/h264/test/test_h264 +testcase/ctx.json +bsp/pc/pclogs/ +bsp/pc/spidrv/ +bsp/pc/fskv.bin +bsp/pc/otp.bin + +.worktree +.worktrees/ +*.log + +# Local AI / IDE tooling state (Mavis, Antigravity, OpenCode, etc.) +.mavis/ +.antigravitycli/ +.opencode/ +/docs/superpowers +# Media artifacts accidentally saved at repo root by tooling +/matrix-media-*.mp4 +/pgfs_flow.mp4 +/pcconf/ +/pgfs_video/ +/bsp/air1601/artifacts/ + +# Local worktrees (not to be tracked from the main checkout) +# Note: .claude/skills/ is intentionally tracked so /luatos-hw-test ships with the repo. +/.claude/settings.local.json +/.claude/worktrees/ +/.fleet-worktrees/ + +# PC BSP runtime artifacts (not to be tracked) +/bsp/pc/CrashDump_*.dmp +/bsp/pc/LastCoverageResults.log +/bsp/pc/test_output.txt +/bsp/pc/test_sqlite3_suite.db* +/bsp/pc/test_dir/ +/bsp/pc/test_dir1/ +/bsp/pc/test_fs_dir/ +/bsp/pc/file2.txt +/bsp/pc/mount_test.txt +/bsp/pc/os_test_*.txt +/bsp/pc/test_io.txt + +*.dmp +testresult/ +app_store/ +*.bin +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/** diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/config.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/config.lua index e8e6e2d..1905846 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/config.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/config.lua @@ -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 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/main.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/main.lua index 8a73e38..465d80b 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/main.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/main.lua @@ -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" diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_main.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_main.lua index 22e17a4..f41f931 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_main.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_main.lua @@ -152,7 +152,8 @@ local function mqtt_client_main_task_func() end -- 配置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 log.error("mqtt_client_main_task_func", "mqtt_client:auth error") diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/mstick.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/mstick.lua new file mode 100644 index 0000000..b8dc06f --- /dev/null +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/mstick.lua @@ -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 } diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md b/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md index 43f2a6d..8a57ed9 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md @@ -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 许可。 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/tools/sim_frame_test.py b/luatos/air780epm/module/Air780EPM/demo/vd960Air/tools/sim_frame_test.py new file mode 100644 index 0000000..03a0b69 --- /dev/null +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/tools/sim_frame_test.py @@ -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('= 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全部通过 ✓") diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/tools/sim_load_test.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/tools/sim_load_test.lua new file mode 100644 index 0000000..10555ad --- /dev/null +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/tools/sim_load_test.lua @@ -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