diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/app_iot.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/app_iot.lua index 8f13820..292423b 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/app_iot.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/app_iot.lua @@ -1,8 +1,9 @@ --[[ @module app_iot @summary vd960Air 设备业务: initialize 上线 / heartbeat 心跳 / 主循环驱动 -@version 0.1 +@version 0.1.1 @date 2026.08.31 +@modify 2026.09.10 link 采集加 get_link() 兜底, mobile API 缺失不再崩 VM @usage - on_connected(): MQTT 连接成功后调用 → 发 initialize(extra_info imei/iccid + link) - heartbeat 定时任务: 60s 发 heartbeat(协议 §5.4,带 link) @@ -17,12 +18,24 @@ local app_iot = {} local HEARTBEAT_INTERVAL = CFG.CFG_HEARTBEAT_INTERVAL or 60 +-- 采集 link 信息(兜底: link 采集失败绝不影响上行, 更不允许崩 VM) +-- ⚠ 2026-09-10: link_info.get() 曾因调用不存在的 mobile.msisdn 抛异常导致 +-- on_connected -> build_initialize 链路整机复位; 此处双层防护 +local function get_link() + local ok, info = pcall(link_info.get) + if ok and type(info) == "table" then + return info + end + log.warn("app_iot", "link_info.get failed", info) + return { imei = "", iccid = "", imsi = "", msisdn = "", csq = 0, net = "4G" } +end + -- 上行 JSON 注入 link local function inject_link(payload) if CFG.CFG_LINK_ENABLE then local ok, obj = pcall(json.decode, payload) if ok and type(obj) == "table" then - obj.link = link_info.get() + obj.link = get_link() return json.encode(obj) end end @@ -31,7 +44,7 @@ end -- 组 initialize(协议 §5.1,extra_info 含 imei/iccid V1.13) local function build_initialize() - local info = link_info.get() + local info = get_link() local payload = string.format( '{"msg_id":1,"cmd":"initialize","ts":%d,' .. '"data":{"dev_serial":"%s","model":"%s","hard_ver":"%s","soft_ver":"%s",' diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/link_info.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/link_info.lua index 96063b8..d842d7c 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/link_info.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/link_info.lua @@ -2,31 +2,55 @@ @module link_info @summary 4G 链路信息采集: IMEI / 流量卡ICCID / IMSI / 手机号 / CSQ 信号 @date 2026.08.31 +@modify 2026.09.10 全部 mobile API 走 safe() 包装 —— 修 link_info.lua:29 + "attempt to call a nil value (field 'msisdn')" 导致的上电复位 @usage 4G 通道特有字段,随上行 JSON 注入(顶层 link 对象,平台可选解析,不破坏主协议结构)。 字段说明(接入《DLD960_IoT_MQTT协议》4G 通道适配章节时一并标注): - imei: 4G 模块 IMEI(mobile.imei()) - iccid: 流量卡 ICCID(mobile.iccid();卡商未写入返回 nil → 置空串) - imsi: IMSI(mobile.imsi();部分运营商/测试卡返回 nil) -- msisdn: 手机号(mobile.msisdn();物联网卡通常拿不到 → 置空串) +- msisdn: 手机号(Air780EPM 无 mobile.msisdn;官方 API 为 mobile.number(simid), + 物联卡普遍无机号 → 通常空串) - csq: 信号强度 0-31(mobile.csq();越大越好,31=最强,99/255=无信号) - net: 网络制式(当前 4G 通道固定 "4G";后续若支持 5G 或以太网备份可扩展) ]] local link_info = {} +--[[ +安全的 mobile API 调用 +⚠ LuatOS 各固件/型号的 API 面并不一致: 字段**不存在**时直接调用会抛 + "attempt to call a nil value" 并让 Lua VM 直接退出(设备 15s 后复位), + 而不是像返回 nil 那样温和。Air780EPM 实测无 mobile.msisdn + (官方 API 是 mobile.number(simid),见 demo/mobile/mobile_test.lua)。 +故本工程所有 mobile.xxx 调用一律经此函数: 字段不存在或调用异常 → nil, 绝不抛出。 +]] +local function safe(fn, ...) + if type(fn) ~= "function" then return nil end + local ok, v = pcall(fn, ...) + if ok then return v end + return nil +end + +link_info.safe = safe -- 供其他模块复用 + -- 实时采集(CSQ 每次调用现读;若需降低开销,可改为定时缓存刷新) +-- ⚠ 本函数保证永不抛异常(对外 API 全部走 safe), 调用方即使不 pcall 也无 VM 复位风险 function link_info.get() - local csq = mobile.csq() or 0 + local csq = safe(mobile.csq) -- mobile.csq() 在某些固件返回 table(如 {csq=xx, ber=xx});此处兼容 if type(csq) == "table" then - csq = csq.csq or 0 + csq = csq.csq + end + if type(csq) ~= "number" then + csq = 0 end return { - imei = mobile.imei() or "", - iccid = mobile.iccid() or "", - imsi = mobile.imsi() or "", - msisdn = mobile.msisdn() or "", + imei = safe(mobile.imei) or "", + iccid = safe(mobile.iccid) or "", + imsi = safe(mobile.imsi) or "", + msisdn = safe(mobile.number, 0) or "", csq = csq, net = "4G", } diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md b/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md index 8a57ed9..b87506d 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md @@ -147,7 +147,7 @@ | `imei` | mobile.imei() | 4G 模块 IMEI,设备唯一标识 | | `iccid` | mobile.iccid() | **流量卡卡号**,物联网卡管理识别用(卡商未写入 → 空串) | | `imsi` | mobile.imsi() | IMSI(部分卡返回空) | -| `msisdn` | mobile.msisdn() | 手机号(物联网卡通常拿不到 → 空串) | +| `msisdn` | `safe(mobile.number, 0)` | 手机号。⚠ Air780EPM **无 `mobile.msisdn()`**(实测直接调用崩 VM), 官方 API 为 `mobile.number(simid)`; 物联卡普遍无机号 → 通常空串 | | `csq` | mobile.csq() | 信号强度 0-31(31 最强,99/255 无信号) | | `net` | 固定 "4G" | 网络制式(预留扩展) | @@ -300,3 +300,31 @@ vd960Air/ - 新增 API 调用前先交叉核实 LuatOS 是否提供(如 `mstick` vs `mcu.ticks()`),不要"想当然" > 基底来源: 合宙官方 Air780EPM demo/mqtt(main.lua / uart_app.lua / mqtt/* / network_watchdog / netdrv_device),MIT 许可。 + +### 2026-09-10 第二批: 修复后的下一处崩溃(link_info / mobile.msisdn) + +**背景**: 第一批修复后, 模块成功注网并连上 MQTT(`IP_READY 10.38.28.79` → `conack nil nil` → `suback true 0`), +随即在**组装 initialize 报文时**崩溃: + +``` +E/user.coroutine.resume link_info.lua:29: attempt to call a nil value (field 'msisdn') + link_info.lua:29: in function 'link_info.get' + app_iot.lua:34: in upvalue 'build_initialize' + app_iot.lua:65: in function 'app_iot.on_connected' +E/main Lua VM exit!! reboot in 15000ms +``` + +**根因**: `mobile.msisdn()` **在 Air780EPM 上不存在**。LuatOS 各固件的 API 面不一致, 字段不存在时 +**直接调用是"抛异常崩 VM"而非"返回 nil"** —— 与 `mstick()` 同性质。官方手机号 API 是 +`mobile.number(simid)`(见 `demo/mobile/mobile_test.lua`), 且物联网卡普遍无机号。 + +**修复(防御性, 两层)**: +1. `link_info.lua`: 新增 `safe(fn, ...)` 助手(`type(fn)=="function"` 判断 + `pcall`), **所有 `mobile.*` 调用统一走它** + → 字段缺失/调用异常一律返回 nil、不再抛出; `msisdn` 改用 `safe(mobile.number, 0)`; `csq` 兼容 table 返回 +2. `app_iot.lua`: 新增 `get_link()` 兜底(内部 `pcall` + 失败时返回全空 link 表), `inject_link` 与 `build_initialize` 均改用它 + → **link 采集失败绝不影响上行, 更不允许崩 VM** + +**验证(含反向验证)**: `tools/sim_load_test.lua` 的 mobile 桩改为"只含官方 demo 证实存在的 API"(移除 `msisdn`)。 +反向验证: 把 `mobile.msisdn()` 注射回代码 → 自检精确报 +`✗ link_info.get() [link_info.lua:53: attempt to call a nil value (field 'msisdn')]`(exit 1); 还原后 exit 0。 +→ 该自检可拦住整类"API 面假设错误"。 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 index 10555ad..3dc19ac 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/tools/sim_load_test.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/tools/sim_load_test.lua @@ -89,7 +89,9 @@ _G.mobile = { imei = function() return "864317081893755" end, iccid = function() return "89860620123456789012" end, imsi = function() return "460001234567890" end, - msisdn = function() return "" end, + -- ⚠ 桩面必须与真实固件一致: 官方 demo 无 mobile.msisdn, 此处**故意不桩**, + -- 这样"代码调用了不存在的 API"才能在自检阶段暴露(2026-09-10 上电复位教训) + number = function() return "" end, csq = function() return 24 end, }