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:
@@ -1,8 +1,9 @@
|
|||||||
--[[
|
--[[
|
||||||
@module app_iot
|
@module app_iot
|
||||||
@summary vd960Air 设备业务: initialize 上线 / heartbeat 心跳 / 主循环驱动
|
@summary vd960Air 设备业务: initialize 上线 / heartbeat 心跳 / 主循环驱动
|
||||||
@version 0.1
|
@version 0.1.1
|
||||||
@date 2026.08.31
|
@date 2026.08.31
|
||||||
|
@modify 2026.09.10 link 采集加 get_link() 兜底, mobile API 缺失不再崩 VM
|
||||||
@usage
|
@usage
|
||||||
- on_connected(): MQTT 连接成功后调用 → 发 initialize(extra_info imei/iccid + link)
|
- on_connected(): MQTT 连接成功后调用 → 发 initialize(extra_info imei/iccid + link)
|
||||||
- heartbeat 定时任务: 60s 发 heartbeat(协议 §5.4,带 link)
|
- heartbeat 定时任务: 60s 发 heartbeat(协议 §5.4,带 link)
|
||||||
@@ -17,12 +18,24 @@ local app_iot = {}
|
|||||||
|
|
||||||
local HEARTBEAT_INTERVAL = CFG.CFG_HEARTBEAT_INTERVAL or 60
|
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
|
-- 上行 JSON 注入 link
|
||||||
local function inject_link(payload)
|
local function inject_link(payload)
|
||||||
if CFG.CFG_LINK_ENABLE then
|
if CFG.CFG_LINK_ENABLE then
|
||||||
local ok, obj = pcall(json.decode, payload)
|
local ok, obj = pcall(json.decode, payload)
|
||||||
if ok and type(obj) == "table" then
|
if ok and type(obj) == "table" then
|
||||||
obj.link = link_info.get()
|
obj.link = get_link()
|
||||||
return json.encode(obj)
|
return json.encode(obj)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -31,7 +44,7 @@ end
|
|||||||
|
|
||||||
-- 组 initialize(协议 §5.1,extra_info 含 imei/iccid V1.13)
|
-- 组 initialize(协议 §5.1,extra_info 含 imei/iccid V1.13)
|
||||||
local function build_initialize()
|
local function build_initialize()
|
||||||
local info = link_info.get()
|
local info = get_link()
|
||||||
local payload = string.format(
|
local payload = string.format(
|
||||||
'{"msg_id":1,"cmd":"initialize","ts":%d,'
|
'{"msg_id":1,"cmd":"initialize","ts":%d,'
|
||||||
.. '"data":{"dev_serial":"%s","model":"%s","hard_ver":"%s","soft_ver":"%s",'
|
.. '"data":{"dev_serial":"%s","model":"%s","hard_ver":"%s","soft_ver":"%s",'
|
||||||
|
|||||||
@@ -2,31 +2,55 @@
|
|||||||
@module link_info
|
@module link_info
|
||||||
@summary 4G 链路信息采集: IMEI / 流量卡ICCID / IMSI / 手机号 / CSQ 信号
|
@summary 4G 链路信息采集: IMEI / 流量卡ICCID / IMSI / 手机号 / CSQ 信号
|
||||||
@date 2026.08.31
|
@date 2026.08.31
|
||||||
|
@modify 2026.09.10 全部 mobile API 走 safe() 包装 —— 修 link_info.lua:29
|
||||||
|
"attempt to call a nil value (field 'msisdn')" 导致的上电复位
|
||||||
@usage
|
@usage
|
||||||
4G 通道特有字段,随上行 JSON 注入(顶层 link 对象,平台可选解析,不破坏主协议结构)。
|
4G 通道特有字段,随上行 JSON 注入(顶层 link 对象,平台可选解析,不破坏主协议结构)。
|
||||||
字段说明(接入《DLD960_IoT_MQTT协议》4G 通道适配章节时一并标注):
|
字段说明(接入《DLD960_IoT_MQTT协议》4G 通道适配章节时一并标注):
|
||||||
- imei: 4G 模块 IMEI(mobile.imei())
|
- imei: 4G 模块 IMEI(mobile.imei())
|
||||||
- iccid: 流量卡 ICCID(mobile.iccid();卡商未写入返回 nil → 置空串)
|
- iccid: 流量卡 ICCID(mobile.iccid();卡商未写入返回 nil → 置空串)
|
||||||
- imsi: IMSI(mobile.imsi();部分运营商/测试卡返回 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=无信号)
|
- csq: 信号强度 0-31(mobile.csq();越大越好,31=最强,99/255=无信号)
|
||||||
- net: 网络制式(当前 4G 通道固定 "4G";后续若支持 5G 或以太网备份可扩展)
|
- net: 网络制式(当前 4G 通道固定 "4G";后续若支持 5G 或以太网备份可扩展)
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local link_info = {}
|
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 每次调用现读;若需降低开销,可改为定时缓存刷新)
|
-- 实时采集(CSQ 每次调用现读;若需降低开销,可改为定时缓存刷新)
|
||||||
|
-- ⚠ 本函数保证永不抛异常(对外 API 全部走 safe), 调用方即使不 pcall 也无 VM 复位风险
|
||||||
function link_info.get()
|
function link_info.get()
|
||||||
local csq = mobile.csq() or 0
|
local csq = safe(mobile.csq)
|
||||||
-- mobile.csq() 在某些固件返回 table(如 {csq=xx, ber=xx});此处兼容
|
-- mobile.csq() 在某些固件返回 table(如 {csq=xx, ber=xx});此处兼容
|
||||||
if type(csq) == "table" then
|
if type(csq) == "table" then
|
||||||
csq = csq.csq or 0
|
csq = csq.csq
|
||||||
|
end
|
||||||
|
if type(csq) ~= "number" then
|
||||||
|
csq = 0
|
||||||
end
|
end
|
||||||
return {
|
return {
|
||||||
imei = mobile.imei() or "",
|
imei = safe(mobile.imei) or "",
|
||||||
iccid = mobile.iccid() or "",
|
iccid = safe(mobile.iccid) or "",
|
||||||
imsi = mobile.imsi() or "",
|
imsi = safe(mobile.imsi) or "",
|
||||||
msisdn = mobile.msisdn() or "",
|
msisdn = safe(mobile.number, 0) or "",
|
||||||
csq = csq,
|
csq = csq,
|
||||||
net = "4G",
|
net = "4G",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
| `imei` | mobile.imei() | 4G 模块 IMEI,设备唯一标识 |
|
| `imei` | mobile.imei() | 4G 模块 IMEI,设备唯一标识 |
|
||||||
| `iccid` | mobile.iccid() | **流量卡卡号**,物联网卡管理识别用(卡商未写入 → 空串) |
|
| `iccid` | mobile.iccid() | **流量卡卡号**,物联网卡管理识别用(卡商未写入 → 空串) |
|
||||||
| `imsi` | mobile.imsi() | IMSI(部分卡返回空) |
|
| `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 无信号) |
|
| `csq` | mobile.csq() | 信号强度 0-31(31 最强,99/255 无信号) |
|
||||||
| `net` | 固定 "4G" | 网络制式(预留扩展) |
|
| `net` | 固定 "4G" | 网络制式(预留扩展) |
|
||||||
|
|
||||||
@@ -300,3 +300,31 @@ vd960Air/
|
|||||||
- 新增 API 调用前先交叉核实 LuatOS 是否提供(如 `mstick` vs `mcu.ticks()`),不要"想当然"
|
- 新增 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 许可。
|
||||||
|
|
||||||
|
### 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 面假设错误"。
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ _G.mobile = {
|
|||||||
imei = function() return "864317081893755" end,
|
imei = function() return "864317081893755" end,
|
||||||
iccid = function() return "89860620123456789012" end,
|
iccid = function() return "89860620123456789012" end,
|
||||||
imsi = function() return "460001234567890" 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,
|
csq = function() return 24 end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user