# vd960Air — Air8781P(Air780EPM) 4G 通信模块 LuatOS 工程 > vd960DBN 车检器在**有线网络失效**时的 4G 兜底通道: UART <-> MQTT 桥。 ## 1 系统架构 ``` ┌─────────────────┐ UART1 (115200) ┌──────────────────────┐ MQTT 3.1.1 ┌──────────┐ │ vd960DBN │ 0x7F 帧字节流 │ Air8781P 整板 │ JSON │ 云平台 │ │ CH32V208 │ ◄───────────────► │ Air780EPM (LuatOS) │ ─────────────► │ dld960 │ │ (车检器通信MCU) │ PB6=TX / PB7=RX │ vd960Air 应用 │ dld960/{sn}/ │ 主题族 │ └─────────────────┘ └──────────────────────┘ {dev,srv} └──────────┘ ▲ 原有有线通道(UART2→Loop + ETH/MQTT) 仍保留, 4G 为兜底 ``` - **上行**: vd960DBN 组 JSON 帧 → UART1 → 4G 校验/拆帧 → MQTT publish `dld960/{sn}/dev` - **下行**: 4G 订阅 `dld960/{sn}/srv` → 收到 JSON → 组帧 → UART1 → vd960DBN - JSON 命令面与《DLD960_IoT_MQTT协议》V1.10 **完全一致**,平台侧无感 ## 2 协议设计决策(4G 是否需要单独协议?) **结论: 要,但以"适配协议"形式——命令面复用、网络类裁剪、4G 特有扩展,不出第二套业务协议。** | 层面 | 决策 | 理由 | |------|------|------| | 上行 JSON(initialize/loop_data/event_report/heartbeat/响应) | **100% 复用主协议** | 平台按《DLD960_IoT_MQTT协议》统一收,有线/4G 双通道无差别 | | 核心下行(report_config/pwd_verify/loop_param_*/log_*/ota_*/device_reset) | **复用,4G 做 UART 透传** | 4G 只做桥,命令由 vd960DBN 处理,平台命令面不变 | | 网络配置类(ssc_net_set/query、iot_net_set/query、iot_topic_set/query) | **4G 通道不适用,返回 code=4** | 4G 接入网是运营商 SIM 网络,APN/主题由 4G 侧管,与 DBN 有线网络参数无关 | | 4G 特有状态(SIM/信号/注册/IMEI/固件版本/链路状态) | **新增命令(4G 侧自答,不进 UART)** | 平台需要区分"有线通/4G 通/都不通",4G 状态是兜底通道健康度关键 | | 通道切换策略 | **新增** | 有线优先 / 4G 兜底 / 心跳超时切换,由 vd960DBN 侧决策(4G 上报自身可用性) | **落地方式**: 并入《DLD960_IoT_MQTT协议》主协议(用户决策 2026-08-31),新增 **"4G 通道适配"章节**,标注: - 不支持的命令清单(ssc_net_* / iot_net_* / iot_topic_* 等,4G 通道返回 `code=4`) - 新增 4G 特有命令(如 `4g_status_query` / `link_status_report`) - **4G 特有字段(link 对象)**: 上行 JSON 顶层附加 `link` 字段,老平台忽略、新平台可管理 4G 通道 - UART 链路帧格式(本工程 §3) - 与主协议共享的 JSON 结构,平台侧零改动 ### 2.1 数据透传方案(方案 B,用户 2026-08-31 提出,列入 vd960DBN 计划) **核心思路: vd960DBN 做纯转发,不参与 4G 通道的 MQTT 协议解析。** ``` 上行: vd960Loop --0x7F帧(UART2)--> vd960DBN --原样转发(UART1)--> Air780 --MQTT--> 平台 下行: 平台 --MQTT--> Air780 --原样(UART1)--> vd960DBN --0x7F帧(UART2)--> vd960Loop ``` - **魔数分流(现成规则)**: UART1 收到 `0x7F` 帧 → 转发 UART2(Loop);`0x8F` 帧 → DBN 本地处理 - **Air780 职责**: 0x7F 帧解析状态机(Lua 版 lup_feed_byte,只切包不懂内容)+ MQTT 收发 + 帧封装 - **vd960DBN 职责**: UART2↔UART1 双向转发(复用 manage_dbn_ble_transparent 透传模式) - 地感指令(0x7F 配置命令)下行零改造,协议原样 **已定稿(用户决策 2026-08-31):** - ✅ **方案 B(原始帧透传)正式采用** - ✅ **4G 上报格式 = hex 封装**(平台友好,参照 log_query records hex 风格) - ✅ **0x7D 帧作废(2026-09-10 用户决策)**: 数据面走 0x7F 帧字节流透传;配置同步改用 **0x8F 本机侧私有帧**(与 BLE 同魔数,两端各自本地消费、不转发,见 §2.2) **待确认(影响定稿):** - [ ] 上行双通道策略: 默认 4G / 有线失效才走 4G / 双通道并存(呼应 §6 通道切换讨论) ### 2.1.1 hex 封装报文格式(协议更新说明 — 并入《DLD960_IoT_MQTT协议》4G 适配章节) **上行 `frame_report`**(Air780 → 平台,每个 0x7F 帧一条): ```json { "msg_id": 123, "cmd": "frame_report", "ts": 1719000000, "data": { "frame": "7f00c0060102030405aabb" }, "link": { "imei": "860012345678901", "iccid": "89860012345678901234", "csq": 23, "net": "4G" } } ``` - `data.frame`: **0x7F 帧完整字节 hex**(含魔数 0x7F/校验字节),小写 - 平台按《DLD960Loop_串口通信协议》解析 frame 内容(0xC0 传感 / 0x0C 响应 / 事件等) - `link`: 4G 特有字段(§2.3),每条上行携带 **下行 `frame_cmd`**(平台 → Air780): ```json { "msg_id": 456, "cmd": "frame_cmd", "ts": 1719000100, "data": { "frame": "7f000809010203040506" } } ``` - `data.frame`: 0x7F 帧(→ vd960Loop 地感指令)或 0x8F 帧(→ vd960DBN 本地配置)hex - Air780 收到 → hex 解码 → 原始字节 → UART1 → vd960DBN 魔数分流(0x7F→UART2 / 0x8F→本地) - 地感/设备响应仍经 `frame_report` 上行 **协议更新说明清单(并入主协议时逐条落实):** 1. 新增上行 `frame_report` + 下行 `frame_cmd` 两个命令(4G 通道专用) 2. **平台双通道格式差异说明**: 有线通道 = 标准 JSON 业务命令(loop_data/event_report);4G 通道 = frame_report/frame_cmd(hex 帧透传)——平台须按通道区分解析 3. 4G 通道网络配置类命令不适用(ssc_net_* / iot_net_* / iot_topic_*,返回 code=4 或文档标注) 4. 链路层: 数据面 0x7F 帧字节流;**0x8F 本机侧私有帧**用于 DBN↔Air780 配置同步(§2.2) 5. 平台侧新增《DLD960Loop_串口通信协议》解析依赖(hex → 帧) **与方案 A(JSON 桥)对比:** | 对比项 | 方案 A: JSON 桥(此前设计) | 方案 B: 原始帧透传(本次) | |---|---|---| | DBN 侧开发量 | 需 UART1 通道 + JSON 组包/解析 + MQTT 语义 | **纯转发,最小** | | Air780 侧职责 | MQTT 壳 + JSON 透传 | 0x7F 帧切包 + MQTT + 封装 | | 平台侧 | 收标准 JSON(与有线一致) | 收 0x7F 帧/hex 封装(需解析《DLD960Loop_串口通信协议》) | | 地感指令下行 | 经 JSON 命令中转 | **原样透传,零改造** | | 链路帧 | 0x7D 帧(2B LEN 装 JSON) — **已作废** | 0x7F 帧字节流 + 0x8F 配置同步帧(§3) | ### 2.2 服务器参数配置链路(用户决策 2026-08-31,列入 vd960DBN 开发计划) **服务器参数(地址/端口/ClientID/账号密码/Topic)由小程序 BLE 蓝牙设置,经 UART1 同步到 Air780。**(2026-09-10 定稿) ``` 小程序 → BLE → vd960DBN(CH32V208) → UART1(0x8F 本机侧私有帧) → Air780 fskv 缓存 ``` **权威源规则**: 配置权威源 = **DBN 侧 flash**(`IOT_NET_INFO`/`IOT_Topic`)。Air780 的 fskv **仅作启动缓存**, 唯一合法写入者 = **收到的 DBN 配置帧**;平台经 4G 下发配置一律拒绝(《DLD960_IoT_MQTT协议》§6.6 回 `code=4`)。 - **vd960DBN 侧**: BLE 写 `0x13`(SET_IOT_NET)/`0x15`(SET_IOT_TOPIC)/`0x09`(UPDATE_DEV_SERIAL) 成功后, ① 应答 Air780 的配置请求(拉取);② 链路可用时主动推送。命令复用 `0x14`(GET_IOT_NET)/`0x16`(GET_IOT_TOPIC), 载荷复用 `set_response_iot_net/topic()` 构造器 → **与 BLE 读响应逐字节同源**(零新增序列化) - **Air780 侧**: `config.lua` 值降级为**出厂默认**;上电先读 fskv 缓存立即连接,再拉权威值校准; 配置变化 → 断开重连重订阅。`frame_parser.lua` 增 `0x8F` 分支(本地消费,不转发); 配置读取从 require 期 `local` 改为**重连时重读**;fskv **仅在值变化时写入**(littlefs 擦写磨损) - **DBN 不维护"待同步"标志位**: 推送时链路不可用则等 Air780 下次拉取(无状态,天然自愈) - **帧长上限**: `0x8F` 独立上限 **260B**(LEN 1B ≤255);`0x7F` 业务帧维持 70B —— **两侧帧缓冲需同步扩容** - 协议面: 《DLD960_IoT_MQTT协议》**§6.9 4G 配置同步** - 好处: 现场换 4G 模块/重新配对时,参数随 BLE 一次配好,不依赖 Air780 单独刷脚本 ### 2.3 4G 特有字段: link 对象(用户决策 2026-08-31) 上行 JSON(initialize / loop_data / event_report / heartbeat / 命令响应)注入: ```json { "msg_id": 1, "cmd": "loop_data", "ts": 1719000000, "data": { "...": "主协议原样" }, "link": { "imei": "860012345678901", "iccid": "89860012345678901234", "imsi": "460001234567890", "msisdn": "", "csq": 23, "net": "4G" } } ``` | 字段 | 来源 | 说明 | |------|------|------| | `imei` | mobile.imei() | 4G 模块 IMEI,设备唯一标识 | | `iccid` | mobile.iccid() | **流量卡卡号**,物联网卡管理识别用(卡商未写入 → 空串) | | `imsi` | mobile.imsi() | IMSI(部分卡返回空) | | `msisdn` | `safe(mobile.number, 0)` | 手机号。⚠ Air780EPM **无 `mobile.msisdn()`**(实测直接调用崩 VM), 官方 API 为 `mobile.number(simid)`; 物联卡普遍无机号 → 通常空串 | | `csq` | mobile.csq() | 信号强度 0-31(31 最强,99/255 无信号) | | `net` | 固定 "4G" | 网络制式(预留扩展) | - 实现: `link_info.lua` 采集 + `uart_app.lua` 上行 json.decode → 注入 `link` → encode - JSON 解析失败时原样转发,不阻塞上行 - 开关: `config.lua` `CFG_LINK_ENABLE` ## 3 UART 链路帧协议(定稿: 0x7F 业务帧 + 0x8F 本机侧私有帧) > **原"设计稿 v0.2"(0x7D 帧 / 2B 大端 LEN / PAYLOAD 装 JSON ≤2048)已作废**(2026-09-10 用户决策)。 > 实际采用 vd960DBN 侧既有帧布局的**双魔数分流**,不再自造第二套帧协议。 ``` [0]=MAGIC [1]=Addr [2]=LEN(1B, =1+data) [3]=CMD [4..]=DATA [-2]=XOR [-1]=SUM 帧总长 = LEN + 5; 校验范围 = 从 Addr 起 2+LEN 字节(不含 MAGIC); 双向对称 ``` | 魔数 | 用途 | 帧长上限 | |---|---|---| | `0x7F` | 业务帧: 转发 UART2(Loop),字节流透传 | 70B(Loop 协议) | | `0x8F` | **本机侧私有帧**: 链路两端各自本地消费,**不转发**(DBN 私有指令 / 4G 配置同步) | **260B** | | `0x9F` | OTA(既有) | 既有 | 其他魔数: 丢弃 + 回找最近魔数 resync。 ### 3.1 与 DBN↔Loop 0x7F 帧的对比(2026-08-31 代码核实) vd960DBN 与地感 MCU 的串口逻辑(loop_uart_proto.c,UART2,0x7F 帧): ``` [7F][Addr][LEN(1B)][CMD][Value: LEN-1B][XOR][SUM] 状态机: IDLE(找0x7F) → HEADER → VALUE → CHECK → COMPLETE 接收: UART2 RX DMA(uart2_dma_poll) 批量喂 lup_feed_byte() 校验: XOR+SUM,从 Addr 起(不含 magic) ``` | 对比项 | DBN↔Loop (0x7F) | DBN↔Air780 (0x8F 配置同步) | |---|---|---| | LEN 字段 | **1B**,Value 0~64B(协议明确上限) | **1B**,LEN=1+data,**帧 ≤260B**(数据 ≤254B) | | 魔数 | 0x7F | 0x8F | | 校验 | XOR + SUM(从 Addr 起) | **XOR + SUM(从 Addr 起,不含魔数)** —— 与 0x7F 完全一致 | | 状态机 | IDLE→HEADER→VALUE→CHECK | 同构(IDLE→HEAD→LEN→DATA→XOR→SUM) | | 用途 | 命令-响应短帧 | **本机侧配置同步**(net 229B / topic 129B) | - 为什么配置同步不用 0x7F 帧: 其 LEN 1B 且 Loop 协议上限 70B,装不下 ClientID/密码/Topic 等长字段 → `0x8F` 独立放宽到 260B;`0x7F` 纪律不变 - 状态机同构 + 校验统一 → 解析骨架可复用(注: UART1 用**独立装配器实例**,不可与全局 `g_lup_parser` 共用) - ✅ **校验定稿为 XOR+SUM**(2026-08-31 用户确认),与 0x7F 完全一致 ## 4 文件结构 ``` vd960Air/ ├── 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) ├── clock.lua # 设备时钟: report_config 校准 → 真实 Unix 秒(协议 §2.3) ├── app_iot.lua # initialize(extra_info imei/iccid + link)/ heartbeat / poll 驱动 ├── uart_app.lua # UART1 接收 → frame_parser → 上行 JSON 发布;ACK 路由 ├── link_info.lua # 4G 链路信息采集(IMEI/ICCID/IMSI/MSISDN/CSQ) ├── netdrv_device.lua # 网卡: 仅 4G(官方 netdrv_4g) ├── network_watchdog.lua # 网络看门狗(官方原样) ├── tools/ │ ├── 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 时钟校准 └── mqtt_sender.lua # 上行队列 + publish(官方改造) ``` ## 5 vd960DBN 对接要点 - **vd960DBN 串口1**: CH32V208 PB6(PB6=TX?)、PB7(RX),TTL 电平 - **波特率**: 115200(官方 demo 默认;vd960DBN UART1 预留口速率待确认) - vd960DBN 侧需要新增: UART1 帧收发 + JSON 组包/解析的"4G 通道"适配(与现有 TCP/MQTT 双栈并列的第三通道,见《DLD960_IoT_MQTT协议》4G 适配章节) - 8N1,无流控(帧协议自带长度+校验,无需硬件流控) ## 6 决策记录 & 待确认清单 ### 已决策(用户 2026-08-31) - [x] **波特率**: UART1 默认 **115200**,后续如需再调(config.lua) - [x] **dev_serial**: 采用与有线通道**同一序列号**(Topic 族一致,平台认同一台设备);config.lua 配置,部署时与 vd960DBN 保持一致 - [x] **4G 特有字段**: 上行注入 `link` 对象(IMEI/ICCID/IMSI/MSISDN/CSQ),平台按 §2.3 解析 - [x] **协议并入主协议**: 4G 适配作为《DLD960_IoT_MQTT协议》章节,4G 特有部分(link 字段/不支持命令/4G 状态命令)单独标注 - [x] **服务器参数配置链路**: 当前由小程序 BLE 设置;vd960DBN 后续版本(列入计划)将 BLE 设置的服务器/topic 参数**同步经 UART1 下发 Air780**,Air780 存储更新配置(§2.2) - [x] **通道切换策略(暂定)**: **默认 4G 无线**作为上报通道 - [x] **方案 B(原始帧透传)采用** + **4G 上报格式 = hex 封装**(frame_report/frame_cmd,§2.1.1;协议更新说明已列) ### 待讨论(通道切换策略,2026-08-31 用户提出) - **动态自动切换 vs 人工设置**: 有线通道失效时自动切 4G?还是由人工(拨码/小程序)指定通道? - 动态自动: 需 vd960DBN 检测有线通断(MQTT/TCP 心跳超时)→ 切 4G;回切策略(有线恢复后自动回切?) - 人工设置: 拨码/小程序固定通道,简单可控但现场需有人干预 - 影响面: vd960DBN 侧决策逻辑 + Air780 侧角色(常驻 4G 上报 vs 备用唤醒)+ 平台侧通道标识 - **待后续讨论定稿后更新本节** ### 待确认(等开发文档/板级验证) - [x] **vd960DBN UART1 帧格式(2026-09-10 定稿)**: 按 §3 —— 0x7F 业务帧字节流 + 0x8F 本机侧私有帧(原 0x7D 设计稿作废) - [ ] dev_serial 获取方式: config 写死 vs UART 握手动态下发(当前 config 写死) - [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 间隔/内容是否与有线一致 - [ ] OTA: 4G 通道是否需要支持 Loop/DBN OTA(4G 下行分片经 UART 转发可行性) ## 7 开发计划(基于官方 demo/mqtt 骨架) | 阶段 | 内容 | 状态 | |------|------|------| | 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)+ **0x8F 配置同步(拉取+推送)** + vd960DBN UART1 通道联调 | ⏳ 待板/待 vd960DBN 版本 | | P2 | 板级联调: 帧校验、背压、掉线重连、看门狗 | ⏳ 待板 | | P3 | 通道切换(策略待讨论)+ 脱机日志/OTA 走 4G | ⏳ 规划 | **vd960DBN 侧开发计划(2026-08-31 用户确认列入):** - [x] UART1(PB6/PB7)通道: 帧收发 + 魔数分流(0x7F → UART2 透传 / 0x8F → 本地) —— **已实现**(`d4d57f6` + `c879ed0`, 2026-09-10) - [x] **UART2↔UART1 双向透传(方案 B)**: Loop 传感数据(0xC0)转发 Air780;4G 下行地感指令转发 Loop(复用 manage_dbn_ble_transparent 模式) —— **已实现**(`d4d57f6`, 2026-09-10) - [ ] BLE 设置服务器/topic 参数时(0x13/0x15/0x09 等),同步经 UART1 下发 Air780 配置 —— **协议已定稿**(《DLD960_IoT_MQTT协议》§6.9),待两侧实现 - [ ] 通道切换策略(待讨论: 动态自动 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 许可。 ### 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 面假设错误"。