feat(vd960Air): 方案 C MVP — 0x7F 帧解析 + 标准 JSON 上报(2026-08-31)

协议依据:《DLD960_IoT_MQTT协议》V1.13(4G 通道适配方案 C)

新增模块:
- frame_parser.lua: 0x7F 帧解析状态机(Lua 版 lup_feed_byte, XOR+SUM 校验)
- proto_conv.lua: 0xC0 传感帧 → loop_data + car_state/loop_state 沿检测(4 类线圈事件)
- evt_queue.lua: event_report 队列 + ACK 状态机(16深/5s×3/跨重连同 msg_id,对齐 DBN)
- clock.lua: report_config 时钟校准(协议 §2.3)
- app_iot.lua: initialize(extra_info imei/iccid + link)/ heartbeat / poll 驱动
- tools/sim_frame_test.py: 帧协议参考验证(56B 帧/4 路解析/沿检测/JSON 格式,全过)

改造:
- uart_app.lua: 0x7D 帧 → 0x7F 帧驱动;loop_data/event_report 组包 + link 注入
- mqtt_receiver.lua: 下行分发(ACK 路由 + 时钟校准)
- mqtt_main.lua: 连接成功 → initialize + 未决事件重发
- config.lua/main.lua: 方案 C 配置与入口

MVP 范围: 上行(initialize/loop_data/event_report/heartbeat)+ report_config;
下行命令转换与 0x7D 配置同步列入 P1。
This commit is contained in:
wangfq
2026-08-31 15:47:43 +08:00
parent 6d763f02fe
commit 3ad7ae8f09
11 changed files with 796 additions and 207 deletions
@@ -0,0 +1,98 @@
--[[
@module app_iot
@summary vd960Air 设备业务: initialize 上线 / heartbeat 心跳 / 主循环驱动
@version 0.1
@date 2026.08.31
@usage
- on_connected(): MQTT 连接成功后调用 → 发 initialize(extra_info imei/iccid + link)
- heartbeat 定时任务: 60s 发 heartbeat(协议 §5.4,带 link)
- poll(): 主循环驱动 uart_app 事件重发检查(可挂 sys.timerLoopStart)
]]
local CFG = require "config"
local link_info = require "link_info"
local clock = require "clock"
local app_iot = {}
local HEARTBEAT_INTERVAL = CFG.CFG_HEARTBEAT_INTERVAL or 60
-- 上行 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()
return json.encode(obj)
end
end
return payload
end
-- 组 initialize(协议 §5.1,extra_info 含 imei/iccid V1.13)
local function build_initialize()
local info = link_info.get()
local payload = string.format(
'{"msg_id":1,"cmd":"initialize","ts":%d,'
.. '"data":{"dev_serial":"%s","model":"%s","hard_ver":"%s","soft_ver":"%s",'
.. '"loop_ver":"%s","loop_hw_ver":"%s",'
.. '"extra_info":{"code":"%s","imei":"%s","iccid":"%s","csq":"%s","location":""}}}',
clock.now(),
CFG.CFG_DEV_SERIAL, CFG.CFG_MODEL or "DLD960", CFG.CFG_HW_VER or "1.0", CFG.CFG_SW_VER or "1.0",
CFG.CFG_LOOP_VER or "", CFG.CFG_LOOP_HW_VER or "",
info.imsi, info.imei, info.iccid, tostring(info.csq))
return inject_link(payload)
end
-- 组 heartbeat(协议 §5.4)
local function build_heartbeat()
local uart_app = require "uart_app"
local loop_ok = uart_app.get_loop_status()
local ls = string.format('[%s,%s,%s,%s]',
tostring(loop_ok[1]), tostring(loop_ok[2]),
tostring(loop_ok[3]), tostring(loop_ok[4]))
local payload = string.format(
'{"msg_id":%d,"cmd":"heartbeat","ts":%d,'
.. '"data":{"uptime":%d,"loop_status":%s,"net_status":true,"iot_status":true}}',
math.floor(mstick() / 1000) % 100000 + 1000, clock.now(),
math.floor(mstick() / 1000), ls)
return inject_link(payload)
end
-- MQTT 连接成功后调用
function app_iot.on_connected()
-- 发 initialize(上线)
local payload = build_initialize()
sys.publish("SEND_DATA_REQ", "vd960DBN", CFG.CFG_TOPIC_UP, payload, 1)
log.info("app_iot", "initialize sent")
end
-- 心跳定时任务
local function heartbeat_task()
while true do
sys.wait(HEARTBEAT_INTERVAL * 1000)
local payload = build_heartbeat()
sys.publish("SEND_DATA_REQ", "vd960DBN", CFG.CFG_TOPIC_UP, payload, 0)
log.info("app_iot", "heartbeat sent")
end
end
-- 主循环驱动: 事件重发检查(500ms 粒度)
local function poll_task()
local uart_app = require "uart_app"
while true do
sys.wait(500)
uart_app.poll()
end
end
-- 初始化(evt_queue 复位 + 启动定时任务)
function app_iot.init()
local evt_queue = require "evt_queue"
evt_queue.init()
sys.taskInit(heartbeat_task)
sys.taskInit(poll_task)
log.info("app_iot", "init ok")
end
return app_iot
@@ -0,0 +1,44 @@
--[[
@module clock
@summary 设备时钟: 平台 report_config 校准后输出真实 Unix 秒
@version 0.1
@date 2026.08.31
@usage
对齐《DLD960_IoT_MQTT协议》§2.3 时钟同步语义:
- 未校准: 返回上电秒数(mstick()/1000,值很小,平台识别"未校准")
- 校准: base_unix + (now_ms - base_ms)/1000(真实 Unix 秒)
- 合法性门槛 ≥1600000000(2020-09 之后),非法值忽略
- 重启后需重新校准(无掉电保持)
]]
local clock = {}
local base_unix = 0
local base_ms = 0
-- 校准(平台 report_config 下发 ts 时调用)
-- @param unix_ts number: 平台下发的 Unix 秒
-- @param now_ms number: 当前 mstick()
function clock.sync(unix_ts, now_ms)
if not unix_ts or unix_ts < 1600000000 then
return -- 非法值忽略
end
base_unix = unix_ts
base_ms = now_ms or mstick()
end
-- 当前 Unix 秒(未校准返回上电秒数)
function clock.now(now_ms)
now_ms = now_ms or mstick()
if base_unix >= 1600000000 then
return base_unix + math.floor((now_ms - base_ms) / 1000)
end
return math.floor(now_ms / 1000)
end
-- 是否已校准
function clock.synced()
return base_unix >= 1600000000
end
return clock
@@ -1,45 +1,52 @@
--[[
@module config
@summary vd960Air 配置: MQTT 服务器 / 设备序列号 / UART 参数
@summary vd960Air 配置: MQTT 服务器 / 设备标识 / 主题 / UART / 上报参数
@date 2026.08.31
@usage
vd960Air 是 Air8781P 整板(Air780EPM 核心模组)上的 LuatOS 应用,
作为 vd960DBN 车检器在有线网络失效时的 4G 兜底通道(UART <-> MQTT 桥)。
作为 vd960DBN 车检器在有线网络失效时的 4G 兜底通道(方案 C: Air780 协议转换)。
协议依据:《DLD960_IoT_MQTT协议》V1.10(命令面/JSON 结构与有线通道完全一致)
协议依据:《DLD960_IoT_MQTT协议》V1.13(4G 通道适配方案 C,标准 JSON 命令面)
]]
-- ==================== MQTT 服务器 ====================
-- 商用服务器地址和端口(替换为实际值;官方 demo 地址仅测试用)
-- 商用服务器地址和端口(替换为实际值)
-- 注: 后续 vd960DBN 更新版本后,经 BLE 设置的服务器参数会同步下发覆盖此处(配置同步, P1)
CFG_MQTT_HOST = "lbsmqtt.airm2m.com"
CFG_MQTT_PORT = 1884
-- ==================== 设备标识 ====================
-- 设备序列码: 6 字节十六进制字符串(与 vd960DBN dev_serial 一致)
-- 说明: 有线通道的 Topic 用 dev_serial 生成;4G 通道必须同一主题族,平台才能统一管理。
-- 【待确认】是否由 vd960DBN 经 UART 握手动态下发,而非本地写死(见 readme 待办)
-- 设备序列码: 6 字节十六进制字符串(与 vd960DBN dev_serial 一致,有线/4G 同一设备)
CFG_DEV_SERIAL = "A1B2C3D4E5F6"
-- 产品信息(initialize 上报,协议 §5.1)
CFG_MODEL = "DLD960"
CFG_HW_VER = "1.0"
CFG_SW_VER = "1.0"
CFG_LOOP_VER = "" -- 地感 Loop 固件版本(0x4A 查询 P1 后填充)
CFG_LOOP_HW_VER = "" -- 地感 Loop 硬件版本
-- MQTT Topic(与《DLD960_IoT_MQTT协议》§1.2 一致)
CFG_TOPIC_UP = "dld960/" .. CFG_DEV_SERIAL .. "/dev" -- 设备上报(4G publish)
CFG_TOPIC_DOWN = "dld960/" .. CFG_DEV_SERIAL .. "/srv" -- 服务器下发(4G subscribe)
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()
-- ==================== UART(对接 vd960DBN) ====================
-- vd960DBN 侧: CH32V208 串口1(PB6=TX, PB7=RX)
-- 波特率: 官方 demo 默认 115200;vd960DBN UART1 预留口速率【待确认】
-- 波特率: 115200(用户决策 2026-08-31,后续如需再调)
CFG_UART_ID = 1
CFG_UART_BAUD = 115200
-- 帧协议参数(4G <-> vd960DBN UART 链路帧,设计稿 v0.2 — 用户决策 2026-08-31 校验统一 XOR+SUM)
-- 帧 = [0x7D] [LEN_H] [LEN_L] [PAYLOAD...] [XOR] [SUM]
-- 校验范围: 从 LEN_H 起到 PAYLOAD 末字节(不含 HEAD,与 DBN↔Loop 0x7F 同风格)
CFG_FRAME_HEAD = 0x7D
CFG_FRAME_MAXLEN = 2048 -- PAYLOAD 最大字节数(防御: 超长判非法帧)
-- 链路帧: 0x7F 帧字节流(方案 C,数据面透传 Loop 地感协议)
-- 0x7D 帧仅用于 DBN↔Air780 配置同步/握手(P1)
-- ==================== 上报节奏 ====================
CFG_HEARTBEAT_INTERVAL = 60 -- 心跳周期(秒,协议 §5.4 默认 60)
CFG_REPORT_ALWAYS = true -- MVP: 每收到 0xC0 帧都上报 loop_data;P1 按协议三档节流
-- ==================== 4G 特有字段注入 ====================
-- 用户决策 2026-08-31: 4G 上报附加 IMEI/流量卡ICCID/IMSI/MSISDN/CSQ 字段
-- 用户决策 2026-08-31: 上行附加 link 对象 + initialize extra_info 含 imei/iccid
-- true = 上行 JSON 顶层注入 link 对象(见 link_info.lua)
CFG_LINK_ENABLE = true
@@ -0,0 +1,169 @@
--[[
@module evt_queue
@summary event_report 待发队列 + ACK 状态机(Lua 版 iot_evt_*,对齐 DBN 语义)
@version 0.1
@date 2026.08.31
@usage
复刻 vd960DBN iot_mqtt_srv.c 的 event_report 机制(协议 V1.04 / §5.3):
- 16 深环形队列,溢出丢最旧
- 平台 ACK(code=0, 回显 msg_id) → 出队
- 5s 超时重发,同 msg_id/原始 ts,最多 3 次;耗尽丢弃
- 跨 MQTT 重连保持同 msg_id 立即重发(平台按 (sn,msg_id) 去重)
- 多事件合并一条 publish
- 独立 msg_id(不复用 MQTT packet id)
用法:
local eq = require "evt_queue"
eq.init() -- 上电初始化
-- 沿检测出事件:
local need_pub = eq.enqueue({type="car_enter", ch=1, value=350})
if need_pub then local ok = publish_now() end
-- 主循环/定时器驱动:
local action = eq.poll(now_ms, now_ts) -- "resend" | nil
-- MQTT 重连后:
local action = eq.on_reconnect(now_ms)
-- 收到平台 ACK:
eq.handle_ack(msg_id, code)
]]
local evt_queue = {}
-- 事件类型名(与协议 §5.3 一致)
local EVT_NAMES = { "car_enter", "car_leave", "loop_cut", "loop_restore" }
local DEPTH = 16
local ACK_TIMEOUT_MS = 5000
local MAX_RETRY = 3
-- 环形队列
local queue = {} -- table,逻辑索引 0..DEPTH-1
local head = 0 -- 队首
local count = 0 -- 有效条数
-- 待确认包状态
local msg_id = 0 -- event_report 独立 msg_id(递增)
local pend_id = 0 -- 待确认包 msg_id,0=无
local pend_n = 0 -- 待确认包含事件数
local pend_ts = 0 -- 原始 ts(重发不刷新)
local retry = 0 -- 已重发次数
local sent_ms = 0 -- 上次发送时刻
function evt_queue.init()
queue = {}
head = 0
count = 0
msg_id = 0
pend_id = 0
pend_n = 0
pend_ts = 0
retry = 0
sent_ms = 0
end
-- 入队;返回 true = 当前无未决包,可立即发布
function evt_queue.enqueue(evt)
if count >= DEPTH then
head = (head + 1) % DEPTH -- 溢出丢最旧
count = count - 1
end
queue[(head + count) % DEPTH] = evt
count = count + 1
return (pend_id == 0)
end
-- 组 event_report JSON(合并队列中全部事件)
-- @param ts number: 时间戳(首发用 now_ts;重发用 pend_ts 由调用方传入)
-- @return payload string 或 nil(队列空)
function evt_queue.build_report(ts)
if count == 0 then
return nil
end
msg_id = msg_id + 1
local parts = {}
for i = 0, count - 1 do
local e = queue[(head + i) % DEPTH]
local tname = EVT_NAMES[e.type] or "car_enter"
parts[#parts + 1] = string.format('{"type":"%s","ch":%d,"value":%d}',
tname, e.ch, e.value or 0)
end
local payload = string.format('{"msg_id":%d,"cmd":"event_report","ts":%d,'
.. '"data":{"events":[%s]}}',
msg_id, ts, table.concat(parts, ","))
return payload
end
-- 发送后登记待确认(首发/重发后调用)
function evt_queue.mark_sent(id, n, ts, now_ms)
pend_id = id
pend_n = n
pend_ts = ts
retry = 0
sent_ms = now_ms
end
-- 平台 ACK 处理(协议 §5.3: 平台回显 msg_id + code)
function evt_queue.handle_ack(ack_id, code)
if pend_id == 0 or ack_id ~= pend_id then
return
end
if code ~= 0 then
return -- 非 0 保持挂起
end
head = (head + pend_n) % DEPTH
count = count - pend_n
if count < 0 then count = 0 end
pend_id = 0
pend_n = 0
retry = 0
end
-- 主循环/定时器驱动: 超时重发判定
-- @return "resend"(需要重发) | nil
function evt_queue.poll(now_ms, now_ts)
if pend_id == 0 then
return nil
end
if now_ms - sent_ms < ACK_TIMEOUT_MS then
return nil
end
if retry >= MAX_RETRY then
-- 重试耗尽: 丢弃本包(可扩展 offlog 记录)
head = (head + pend_n) % DEPTH
count = count - pend_n
if count < 0 then count = 0 end
pend_id = 0
pend_n = 0
retry = 0
return nil
end
retry = retry + 1
sent_ms = now_ms
return "resend"
end
-- MQTT 重连后: 未决包立即重发(同 msg_id/原始 ts)
function evt_queue.on_reconnect(now_ms)
if pend_id ~= 0 then
retry = 0
sent_ms = now_ms
return "resend"
end
return nil
end
-- 取待确认信息(重发时用)
function evt_queue.pending_info()
return pend_id, pend_n, pend_ts
end
-- 最近一次 build_report 生成的 msg_id(发布后登记用)
function evt_queue.last_msg_id()
return msg_id
end
-- 队列事件数
function evt_queue.count()
return count
end
return evt_queue
@@ -0,0 +1,125 @@
--[[
@module frame_parser
@summary 0x7F 帧解析状态机(Lua 版 lup_feed_byte)
@version 0.1
@date 2026.08.31
@usage
解析 vd960DBN 经 UART1 转发的 0x7F 帧(Loop 地感协议),只切包不懂内容:
- 帧格式: [7F][Addr][LEN][CMD][Value: LEN-1B][XOR][SUM]
- LEN = 1 + data_len(含 CMD 字节)
- 校验: XOR + SUM,从 Addr 起(不含 magic 0x7F),与 DBN↔Loop 完全一致
- 状态机: IDLE → HEADER(Addr/LEN/CMD) → VALUE(LEN-1B) → CHECK(XOR/SUM) → COMPLETE
- 完整帧通过回调 on_frame(cmd, payload) 交付;校验失败/超长帧回 IDLE 丢弃
用法:
local fp = require "frame_parser"
fp.init(function(cmd, payload)
-- cmd: 命令码(number)
-- payload: 除 CMD 外的数据字节(string, 可为空)
end)
-- UART 收到字节流逐字节喂入:
fp.feed(string.byte(s, i))
]]
local frame_parser = {}
-- 状态
local ST_IDLE, ST_ADDR, ST_LEN, ST_CMD, ST_VALUE, ST_CHECK_X, ST_CHECK_S =
0, 1, 2, 3, 4, 5, 6
-- 常量(与 DLD960Loop 协议一致)
local MAGIC_0x7F = 0x7F
local MAX_FRAME_VALUE = 64 -- 协议约定 Value 上限(防御)
local state = ST_IDLE
local cmd = 0
local value_len = 0 -- LEN - 1(除 CMD 外的数据字节数)
local value_buf = "" -- 数据缓冲
local chk_xor = 0 -- 从 Addr 起累计
local chk_sum = 0
local on_frame = nil -- 回调
local function reset()
state = ST_IDLE
cmd = 0
value_len = 0
value_buf = ""
chk_xor = 0
chk_sum = 0
end
-- 完整帧校验通过 → 回调
local function complete()
if on_frame then
on_frame(cmd, value_buf)
end
reset()
end
-- 逐字节喂入
function frame_parser.feed(b)
if state == ST_IDLE then
if b == MAGIC_0x7F then
state = ST_ADDR
end
elseif state == ST_ADDR then
chk_xor = b
chk_sum = b
state = ST_LEN
elseif state == ST_LEN then
chk_xor = chk_xor ~ b
chk_sum = (chk_sum + b) % 256
if b < 1 then
reset() -- LEN=0 非法
else
value_len = b - 1 -- 减 CMD 字节
if value_len > MAX_FRAME_VALUE then
reset() -- 超长帧丢弃
else
value_buf = ""
state = ST_CMD
end
end
elseif state == ST_CMD then
chk_xor = chk_xor ~ b
chk_sum = (chk_sum + b) % 256
cmd = b
if value_len == 0 then
state = ST_CHECK_X
else
state = ST_VALUE
end
elseif state == ST_VALUE then
chk_xor = chk_xor ~ b
chk_sum = (chk_sum + b) % 256
value_buf = value_buf .. string.char(b)
if value_buf:len() >= value_len then
state = ST_CHECK_X
end
elseif state == ST_CHECK_X then
if b ~= chk_xor then
reset()
else
state = ST_CHECK_S
end
elseif state == ST_CHECK_S then
if b == chk_sum then
complete()
else
reset()
end
end
end
-- 初始化回调
function frame_parser.init(cb)
on_frame = cb
reset()
end
-- 复位(链路异常/切换时调用)
function frame_parser.reset()
reset()
end
return frame_parser
@@ -1,17 +1,17 @@
--[[
@module main
@summary vd960Air — Air8781P(Air780EPM) 4G 模块 LuatOS 应用入口
@version 0.1
@summary vd960Air — Air8781P(Air780EPM) 4G 模块 LuatOS 应用入口(方案 C MVP)
@version 0.3
@date 2026.08.31
@usage
vd960DBN 车检器在有线网络效时的 4G 兜底通道(UART <-> MQTT 桥):
vd960DBN 车检器在有线网络效时的 4G 兜底通道(方案 C: Air780 协议转换):
1、UART1 对接 vd960DBN(CH32V208 串口1 PB6/PB7),帧协议见 uart_app.lua;
2、上行: vd960DBN JSON 帧 → 4G → MQTT publish 到 dld960/{sn}/dev
(JSON 命令面与《DLD960_IoT_MQTT协议》V1.10 完全一致,平台侧无感);
3、下行: MQTT 订阅 dld960/{sn}/srv → 4G → 组帧 → UART1 下发 vd960DBN;
4、网络看门狗: 长时间无网络业务则重启系统(官方 network_watchdog);
5、网卡: 仅 4G(netdrv_4g)。
1、UART1 对接 vd960DBN(CH32V208 串口1 PB6/PB7),0x7F 帧字节流透传;
2、0x7F 帧解析 → 0xC0 传感 → loop_data JSON(标准,注入 link)→ MQTT dld960/{sn}/dev;
3、car_state/loop_state 沿检测 → event_report(ACK + 5s×3 重发,对齐 DBN 语义);
4、下行: report_config 时钟校准 + event_report ACK;业务命令转换 P1;
5、initialize(extra_info imei/iccid + link)/ heartbeat 周期上报;
6、网络看门狗 + 4G 网卡(官方组件)。
]]
PROJECT = "vd960Air"
VERSION = "001.999.000"
@@ -28,9 +28,13 @@ require "network_watchdog"
-- ==================== 网卡驱动(仅 4G) ====================
require "netdrv_device"
-- ==================== 串口应用(UART1 <-> vd960DBN) ====================
-- ==================== 串口应用(UART1 <-> vd960DBN,0x7F 帧) ====================
require "uart_app"
-- ==================== 设备业务(initialize/heartbeat/poll) ====================
local app_iot = require "app_iot"
app_iot.init()
-- ==================== MQTT 客户端(单路, 主题 dld960/{sn}/srv + dev) ====================
require "mqtt_main"
@@ -22,6 +22,8 @@ local mqtt_receiver = require "mqtt_receiver"
local mqtt_sender = require "mqtt_sender"
-- 加载vd960Air配置
local CFG = require "config"
-- 设备业务(initialize/heartbeat)
local app_iot = require "app_iot"
-- mqtt服务器地址和端口(来自config.lua,商用服务器)
local SERVER_ADDR = CFG.CFG_MQTT_HOST
@@ -191,6 +193,10 @@ local function mqtt_client_main_task_func()
log.info("mqtt_client_main_task_func", "connect success")
-- 通知mqtt sender数据发送应用模块的task,MQTT连接成功
sys.sendMsg(mqtt_sender.TASK_NAME, "MQTT_EVENT", "CONNECT_OK", mqtt_client)
-- vd960Air: 上线发 initialize + 未决事件包立即重发
app_iot.on_connected()
local uart_app = require "uart_app"
uart_app.on_mqtt_reconnect()
-- mqtt连接失败
else
log.info("mqtt_client_main_task_func", "connect error")
@@ -1,64 +1,55 @@
--[[
@module mqtt_receiver
@summary mqtt client数据接收处理应用功能模块
@version 1.0
@date 2025.07.29
@author
@summary MQTT ( C)
@version 0.2
@date 2026.08.31
@usage
mqtt client
publish数据
2
1mqtt_receiver.proc(topic, payload, metas)publish数据处理入口mqtt_main.lua中调用
2sys.publish("RECV_DATA_FROM_SERVER", "recv from mqtt server: ", topic, payload)
publish中的topic和payload数据通过消息"RECV_DATA_FROM_SERVER"
demo项目中uart_app.lua中订阅处理了本消息
(dld960/{sn}/srv):
- event_report ACK( msg_id + code=0) evt_queue
- report_config ( ts, §2.3)
- JSON (loop_param_*/log_*/ota_* ) P1: 0x7F vd960Loop()
- frame_cmd() P1: hex UART1 ()
]]
local mqtt_receiver = {}
local function proc(topic, payload, metas)
if not payload or payload:len() == 0 then
return
end
local ok, obj = pcall(json.decode, payload)
if not ok or type(obj) ~= "table" then
log.warn("mqtt_receiver", "bad json")
return
end
--[[
publish数据
local cmd = obj.cmd
local msg_id = obj.msg_id or 0
local code = obj.code
@api mqtt_receiver.proc(topic, payload, metas)
if cmd == "event_report" then
-- 平台 ACK: 回显 msg_id + code;code=0 → 出队
local uart_app = require "uart_app"
uart_app.handle_event_ack(msg_id, code or 0)
log.info("mqtt_receiver", "evt ack", msg_id, code)
@param1 topic string
publish主题
elseif cmd == "report_config" then
-- 时钟校准(信封 ts 填当前 Unix 秒)
local clock = require "clock"
clock.sync(obj.ts or 0, mstick())
log.info("mqtt_receiver", "clock sync", obj.ts)
@param2 payload string
publish数据负载
else
-- P1: 下行命令转换(loop_param_*/log_*/ota_* 等)暂未实现
log.info("mqtt_receiver", "down cmd", cmd, "msg_id", msg_id)
end
@param2 payload string
publish数据负载
-- 喂网络看门狗
sys.publish("FEED_NETWORK_WATCHDOG")
end
@param3 metas table
publish报文的一些参数
{
qos: number类型0,1,2
retainnumber类型0,1
dupnumber类型0,1
message_id: number类型
}
@return1 result nil
@usage
mqtt_receiver.proc(topic, payload, metas)
]]
function mqtt_receiver.proc(topic, payload, metas)
log.info("mqtt_receiver.proc", topic, payload:len(), json.encode(metas))
-- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
-- 将topic和payload通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
-- vd960Air: 直接传原始 payload,由 uart_app 组帧后下发 vd960DBN
sys.publish("RECV_DATA_FROM_SERVER", topic, payload)
-- 也可以直接在此处编写代码,处理topic和payload
proc(topic, payload, metas)
end
return mqtt_receiver
@@ -0,0 +1,141 @@
--[[
@module proto_conv
@summary 0x7F JSON ( C )
@version 0.1
@date 2026.08.31
@usage
Air780 vd960DBN 0x7F ,DLD960_IoT_MQTT协议 JSON :
- 0xC0 (线) loop_data JSON
- car_state / loop_state 沿 event_report (car_enter/car_leave/loop_cut/loop_restore)
- 4G = 线( DBN , §6.4)
0xC0 payload(cmd=0xC0 ):
[SensType(1)][SubPkgFlag(1)][SensData: 4×12B]
线 12B(DLD960Loop_串口通信协议§3.07 ):
config(1): freq_level(bit6-7) + direction(bit5) + freq_type(bit4) + sens(bit0-3)
eval(1): condition(bit4-7) + loop_state(bit3) + car_state(bit2) + misc_type(bit0-1)
freq(3B LE ) + variation(3B LE ) + misc(4B LE)
]]
local proto_conv = {}
-- 线圈高低频档位 → level 字符串
local LEVEL_NAMES = { "high", "mid_high", "mid_low", "low" }
-- 杂项类型 → type 字符串
local MISC_NAMES = { "time", "cut_count", "flow_count", "relay_count" }
-- 3B 小端无符号
local function u24le(s, off)
return string.byte(s, off)
+ string.byte(s, off + 1) * 256
+ string.byte(s, off + 2) * 65536
end
-- 3B 小端有符号(补码)
local function i24le(s, off)
local v = u24le(s, off)
if v >= 0x800000 then
v = v - 0x1000000
end
return v
end
-- 4B 小端无符号
local function u32le(s, off)
return string.byte(s, off)
+ string.byte(s, off + 1) * 256
+ string.byte(s, off + 2) * 65536
+ string.byte(s, off + 3) * 16777216
end
-- 解析 0xC0 传感帧 payload → 4 路通道表
-- @param payload string: cmd 之后的字节(SensType + SubPkgFlag + SensData)
-- @return channels table or nil: [{ch, level, iscar, loop_ok, freq, diff, sens, cndtn, misc}]
function proto_conv.parse_sensor(payload)
if not payload or payload:len() < 2 then
return nil
end
local sens_type = string.byte(payload, 1)
if sens_type ~= 0x0C then
return nil -- 非多线圈传感类型
end
-- payload: [SensType(1)][SubPkgFlag(1)][SensData...]
local data = payload:sub(3)
local channels = {}
local coil_count = 4 -- 960Loop 固定 4 路
for i = 1, coil_count do
local off = (i - 1) * 12 + 1
if data:len() < off + 11 then
break
end
local cfg = string.byte(data, off)
local evl = string.byte(data, off + 1)
local freq_level = (cfg >> 6) & 0x03
local sens = cfg & 0x0F
local condition = (evl >> 4) & 0x0F
local loop_state = (evl >> 3) & 0x01
local car_state = (evl >> 2) & 0x01
local misc_type = evl & 0x03
local freq = u24le(data, off + 2)
local diff = i24le(data, off + 5)
local misc = u32le(data, off + 8)
channels[i] = {
ch = i,
level = LEVEL_NAMES[freq_level + 1] or "high",
iscar = (car_state == 1),
loop_ok = (loop_state == 0),
freq = freq,
diff = diff,
sens = sens,
cndtn = condition,
misc = { type = MISC_NAMES[misc_type + 1] or "time", value = misc },
}
end
return channels
end
-- ==================== 事件沿检测 ====================
-- 保存上一帧每路 car_state / loop_state
local prev_car = { 0, 0, 0, 0 }
local prev_loop = { 0, 0, 0, 0 }
-- 沿检测: 输入当前通道表,输出事件列表
-- @return events: [{type, ch, value}]
function proto_conv.detect_events(channels)
local events = {}
if not channels then
return events
end
for i = 1, #channels do
local ch = channels[i]
local cur_car = ch.iscar and 1 or 0
local cur_loop = ch.loop_ok and 0 or 1 -- loop_ok=false → 断开=1
if i <= 4 then
-- car 沿(线圈断开期间屏蔽,与 DBN 同策略)
if prev_loop[i] == 0 and cur_loop == 0 then
if prev_car[i] == 0 and cur_car == 1 then
events[#events + 1] = { type = "car_enter", ch = i, value = ch.misc.value }
elseif prev_car[i] == 1 and cur_car == 0 then
events[#events + 1] = { type = "car_leave", ch = i, value = ch.misc.value }
end
end
-- loop 沿
if prev_loop[i] == 0 and cur_loop == 1 then
events[#events + 1] = { type = "loop_cut", ch = i, value = 0 }
elseif prev_loop[i] == 1 and cur_loop == 0 then
events[#events + 1] = { type = "loop_restore", ch = i, value = 0 }
end
prev_car[i] = cur_car
prev_loop[i] = cur_loop
end
end
return events
end
-- 复位沿检测状态(上电/MQTT 重连后避免误报)
function proto_conv.reset_events()
prev_car = { 0, 0, 0, 0 }
prev_loop = { 0, 0, 0, 0 }
end
return proto_conv
@@ -193,16 +193,23 @@ vd960DBN 与地感 MCU 的串口逻辑(loop_uart_proto.c,UART2,0x7F 帧):
```
vd960Air/
├── main.lua # 入口: 加载 config/看门狗/4G网卡/uart_app/mqtt_main
├── config.lua # 配置: MQTT 服务器/序列号/主题/UART/帧参数/link 注入开关
├── uart_app.lua # UART1 <-> vd960DBN 帧协议(接收状态机 + 下行组帧 + link 注入)
├── main.lua # 入口: 加载 config/看门狗/4G网卡/uart_app/app_iot/mqtt_main
├── config.lua # 配置: MQTT 服务器/序列号/主题/UART/上报节奏/link 开关
├── 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 复刻解析公式,字节级断言)
└── mqtt/
├── mqtt_main.lua # MQTT 客户端(单路, 主题改 dld960/{sn}/srv + dev)
├── mqtt_receiver.lua # 下行 → RECV_DATA_FROM_SERVER(去掉演示前缀)
└── mqtt_sender.lua # 上行队列 + publish(去掉演示前缀)
├── mqtt_main.lua # MQTT 客户端(连接成功 → initialize + 未决事件重发)
├── mqtt_receiver.lua # 下行分发: event_report ACK 路由 + report_config 时钟校准
└── mqtt_sender.lua # 上行队列 + publish(官方改造)
```
## 5 vd960DBN 对接要点
@@ -246,10 +253,11 @@ vd960Air/
| 阶段 | 内容 | 状态 |
|------|------|------|
| P0 | 工程骨架 + 帧协议 + MQTT 单路桥接 + link 字段注入 | ✅ 完成 |
| P1 | 协议适配文档(4G 章节)+ vd960DBN 侧 UART1 通道 + **配置同步下发(Air780 存储更新)** | ⏳ 待文档/待 vd960DBN 版本 |
| P0 | 工程骨架 + 帧协议 + MQTT 桥接 + link 注入 | ✅ 完成 |
| **P0.5** | **方案 C MVP: 0x7F 帧解析 + 0xC0→loop_data + 沿检测→event_report + ACK 状态机 + initialize/heartbeat + 时钟校准**(2026-08-31) | ✅ 代码完成,待板级 |
| P1 | 下行命令转换(loop_param_*/log_*/ota_* 转 0x7F)+ 0x7D 配置同步下发 + vd960DBN UART1 通道联调 | ⏳ 待板/待 vd960DBN 版本 |
| P2 | 板级联调: 帧校验、背压、掉线重连、看门狗 | ⏳ 待板 |
| P3 | 4G 状态上报 + 通道切换(策略待讨论)+ 脱机日志/OTA 走 4G | ⏳ 规划 |
| P3 | 通道切换(策略待讨论)+ 脱机日志/OTA 走 4G | ⏳ 规划 |
**vd960DBN 侧开发计划(2026-08-31 用户确认列入):**
- [ ] UART1(PB6/PB7)通道: 帧收发 + 0x7F 帧魔数分流(0x7F → UART2 透传 / 0x8F → 本地)
@@ -1,132 +1,123 @@
--[[
@module uart_app
@summary vd960Air : UART1 <-> vd960DBN
@version 0.2
@summary vd960Air : UART1 vd960DBN(0x7F ) JSON ( C)
@version 0.3
@date 2026.08.31
@usage
:
1 UART1( vd960DBN CH32V208 1 PB6/PB7);
2: vd960DBN JSON (DLD960_IoT_MQTT协议),
link SEND_DATA_REQ mqtt_sender publish dld960/{sn}/dev;
3: RECV_DATA_FROM_SERVER , MQTT JSON UART1 vd960DBN
UART (v0.2, XOR+SUM 2026-08-31, DBNLoop 0x7F ):
HEAD LEN_H LEN_L PAYLOAD XOR SUM
0x7D 1B 1B JSON(2048B) 1B 1B
- HEAD: 0x7D( 0x7F DBNLoop / 0x8F DBN / 0x9F OTA )
- LEN: uint16 ,PAYLOAD
- XOR: XOR , LEN_H PAYLOAD ( HEAD, 0x7F )
- SUM: ,
- : (vd960DBN )
: IDLE HEAD LEN_H LEN_L DATA XOR SUM COMPLETE
: LEN ,payload 0x7D ;
IDLE HEAD; / IDLE
( C):
vd960Loop --0x7F帧--> vd960DBN --原样转发(UART1)--> Air780
frame_parser 0x7F
0xC0 : proto_conv loop_data JSON( link)
car_state/loop_state 沿: evt_queue event_report (ACK )
:
MQTT JSON (P1: 0x7F ) UART1 vd960DBN vd960Loop
MVP : (initialize/loop_data/event_report/heartbeat)+ report_config ;
0x7D P1
]]
-- 加载配置
-- 依赖
local CFG = require "config"
-- 4G 链路信息采集(IMEI/ICCID/CSQ)
local link_info = require "link_info"
local frame_parser = require "frame_parser"
local proto_conv = require "proto_conv"
local evt_queue = require "evt_queue"
local clock = require "clock"
local UART_ID = CFG.CFG_UART_ID
local MAX_LEN = CFG.CFG_FRAME_MAXLEN
local uart_app = {}
-- ==================== 接收状态机 ====================
local RX_IDLE, RX_HEAD, RX_LENH, RX_LENL, RX_DATA, RX_XOR, RX_SUM = 0, 1, 2, 3, 4, 5, 6
local rx_state = RX_IDLE
local rx_len = 0 -- 当前帧 PAYLOAD 长度
local rx_buf = "" -- PAYLOAD 缓冲
local rx_xor = 0 -- XOR 校验(从 LEN_H 起累加)
local rx_sum = 0 -- SUM 校验(从 LEN_H 起累加)
local UART_ID = CFG.CFG_UART_ID
local function rx_reset()
rx_state = RX_IDLE
rx_len = 0
rx_buf = ""
rx_xor = 0
rx_sum = 0
end
-- 一帧完整且校验通过 → 发布到 MQTT 发送队列
local function on_frame_ok(payload)
log.info("uart_app.frame", "len", payload:len())
-- 4G 特有字段注入(用户决策 2026-08-31):
-- 上行 JSON 顶层附加 link 对象 {imei, iccid, imsi, msisdn, csq, net},
-- 方便平台管理 4G 通道设备(流量卡识别/信号监控)。不破坏主协议结构,老平台忽略即可。
-- ==================== 公共工具 ====================
-- 上行 JSON 注入 link 对象(4G 特有字段)
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()
payload = json.encode(obj)
else
-- JSON 解析失败: 原样转发,不阻塞上行
log.warn("uart_app.frame", "json decode fail, forward raw")
return json.encode(obj)
end
end
return payload
end
-- SEND_DATA_REQ(tag, topic, payload, qos)
-- 上行统一发布到 dev 主题(平台侧与有线通道无差别)
-- 组 loop_data JSON(协议 §5.2)
local function build_loop_data(channels, ts, msg_id)
local parts = {}
for i = 1, #channels do
local c = channels[i]
parts[i] = string.format(
'{"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, tostring(c.iscar), tostring(c.loop_ok),
c.freq, c.diff, c.sens, c.cndtn, c.misc.type, c.misc.value)
end
return string.format('{"msg_id":%d,"cmd":"loop_data","ts":%d,"data":{"channels":[%s]}}',
msg_id, ts, table.concat(parts, ","))
end
-- 发布 event_report(新发或重发)
local function publish_event()
local pend_id, pend_n, pend_ts = evt_queue.pending_info()
local ts
if pend_n and pend_n > 0 then
ts = pend_ts -- 重发: 原始 ts,msg_id 不变
else
ts = clock.now() -- 新发
end
local payload = evt_queue.build_report(ts)
if not payload then
return
end
payload = inject_link(payload)
sys.publish("SEND_DATA_REQ", "vd960DBN", CFG.CFG_TOPIC_UP, payload, 1)
local id = evt_queue.last_msg_id()
evt_queue.mark_sent(id, evt_queue.count(), ts, mstick())
log.info("uart_app.evt", "pub id", id, "n", evt_queue.count(), "ts", ts)
end
-- 逐字节喂入接收状态机
local function feed_byte(b)
if rx_state == RX_IDLE then
if b == CFG.CFG_FRAME_HEAD then
rx_state = RX_HEAD
-- ==================== 0x7F 帧回调 ====================
-- 独立 loop_data msg_id 计数
local g_ld_msg_id = 0
local function next_ld_msg_id()
g_ld_msg_id = g_ld_msg_id + 1
return g_ld_msg_id
end
-- 最近通道状态缓存(heartbeat 的 loop_status 用)
local last_loop_ok = { true, true, true, true }
local function on_frame(cmd, payload)
if cmd == 0xC0 then
-- 多线圈传感帧 → loop_data + 沿检测事件
local channels = proto_conv.parse_sensor(payload)
if not channels then
return
end
elseif rx_state == RX_HEAD then
rx_len = b * 256
rx_xor = b
rx_sum = b
rx_state = RX_LENH
elseif rx_state == RX_LENH then
rx_len = rx_len + b
rx_xor = rx_xor ~ b -- XOR 累加(bitwise)
rx_sum = (rx_sum + b) % 256
if rx_len > MAX_LEN then
log.warn("uart_app.feed", "frame too long", rx_len)
rx_reset()
else
rx_buf = ""
rx_state = RX_LENL
-- 刷新 loop_status 缓存
for i = 1, #channels do
last_loop_ok[i] = channels[i].loop_ok
end
elseif rx_state == RX_LENL then
-- 0 长度帧非法
if rx_len == 0 then
rx_reset()
else
rx_state = RX_DATA
-- 沿检测 → 事件入队(返回是否需要立即发布)
local need_evt = false
local events = proto_conv.detect_events(channels)
for i = 1, #events do
need_evt = evt_queue.enqueue(events[i]) or need_evt
end
elseif rx_state == RX_DATA then
rx_buf = rx_buf .. string.char(b)
rx_xor = rx_xor ~ b
rx_sum = (rx_sum + b) % 256
if rx_buf:len() >= rx_len then
rx_state = RX_XOR
-- loop_data 上报(带 link)
local ts = clock.now()
local ld = inject_link(build_loop_data(channels, ts, next_ld_msg_id()))
sys.publish("SEND_DATA_REQ", "vd960DBN", CFG.CFG_TOPIC_UP, ld, 0)
-- 事件发布
if need_evt then
publish_event()
end
elseif rx_state == RX_XOR then
if b ~= rx_xor then
log.warn("uart_app.feed", "xor fail")
rx_reset()
else
rx_state = RX_SUM
end
elseif rx_state == RX_SUM then
if b == rx_sum then
on_frame_ok(rx_buf)
else
log.warn("uart_app.feed", "sum fail")
end
rx_reset()
else
-- 其他 0x7F 帧(命令响应等): P1 下行命令转换实现后处理
log.info("uart_app.frame", "cmd", string.format("0x%02X", cmd), "len", payload:len())
end
end
-- UART1 接收中断处理
-- ==================== UART 接收 ====================
local function read()
local s
while true do
@@ -135,40 +126,45 @@ local function read()
break
end
for i = 1, s:len() do
feed_byte(string.byte(s, i))
frame_parser.feed(string.byte(s, i))
end
end
end
-- ==================== 下行: MQTT → UART1 ====================
-- mqtt_receiver 发布 RECV_DATA_FROM_SERVER(topic, payload)
local function recv_data_from_server_proc(topic, payload)
if not payload or payload:len() == 0 then
return
-- ==================== 对外接口 ====================
-- MQTT 重连后: 未决事件包立即重发
function uart_app.on_mqtt_reconnect()
local action = evt_queue.on_reconnect(mstick())
if action == "resend" then
publish_event()
end
-- 组帧: HEAD + LEN(2B BE) + PAYLOAD + XOR + SUM
-- 校验范围: 从 LEN_H 起(不含 HEAD,与 0x7F 同风格)
local len = payload:len()
local hi = math.floor(len / 256)
local lo = len % 256
local x = hi ~ lo
local s = (hi + lo) % 256
for i = 1, len do
local b = string.byte(payload, i)
x = x ~ b
s = (s + b) % 256
end
-- 平台 ACK(事件确认)路由
function uart_app.handle_event_ack(msg_id, code)
evt_queue.handle_ack(msg_id, code)
end
-- 主循环驱动: 事件重发超时检查
function uart_app.poll()
local action = evt_queue.poll(mstick())
if action == "resend" then
publish_event()
end
local frame = string.char(CFG.CFG_FRAME_HEAD)
.. string.char(hi) .. string.char(lo)
.. payload
.. string.char(x) .. string.char(s)
uart.write(UART_ID, frame)
log.info("uart_app.down", "len", len)
end
-- 上报当前传感缓存? MVP: 心跳带 loop_status 由 mqtt_main 维护,此处暂不提供
-- heartbeat 用: 最近 4 路线圈状态(true=正常)
function uart_app.get_loop_status()
return last_loop_ok
end
-- ==================== 初始化 ====================
frame_parser.init(on_frame)
uart.setup(UART_ID, CFG.CFG_UART_BAUD, 8, 1)
uart.on(UART_ID, "receive", read)
sys.subscribe("RECV_DATA_FROM_SERVER", recv_data_from_server_proc)
log.info("uart_app", "init ok", "baud", CFG.CFG_UART_BAUD, "topic_up", CFG.CFG_TOPIC_UP)
return uart_app