fix(vd960Air): 修复 initialize 组装崩溃(mobile.msisdn 不存在) + link 采集兜底

现象(第一批修复后): 模块成功注网(IP_READY 10.38.28.79)并连上 MQTT(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 '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):
  桩面必须与真实固件一致 —— "想当然"多写的桩会掩盖真实 bug
- 反向验证: 把 mobile.msisdn() 注射回代码 → 自检精确报
  "✗ link_info.get() [link_info.lua:53: attempt to call a nil value (field 'msisdn')]"(exit 1);
  还原后 exit 0 → 该自检可拦住整类"API 面假设错误"

验证: luac5.3 -p 全绿; Lua 离线自检通过; Python 帧协议验证通过; readme 表格列数 0 异常
文档: readme §8 补第二批修复记录, 修正 link.msisdn 字段来源说明
This commit is contained in:
wangfq
2026-09-10 15:44:47 +08:00
parent e8917e7041
commit 2f75f5abff
4 changed files with 79 additions and 12 deletions
@@ -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",
}