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:
@@ -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
|
@module config
|
||||||
@summary vd960Air 配置: MQTT 服务器 / 设备序列号 / UART 参数
|
@summary vd960Air 配置: MQTT 服务器 / 设备标识 / 主题 / UART / 上报参数
|
||||||
@date 2026.08.31
|
@date 2026.08.31
|
||||||
@usage
|
@usage
|
||||||
vd960Air 是 Air8781P 整板(Air780EPM 核心模组)上的 LuatOS 应用,
|
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 服务器 ====================
|
-- ==================== MQTT 服务器 ====================
|
||||||
-- 商用服务器地址和端口(替换为实际值;官方 demo 地址仅测试用)
|
-- 商用服务器地址和端口(替换为实际值)
|
||||||
|
-- 注: 后续 vd960DBN 更新版本后,经 BLE 设置的服务器参数会同步下发覆盖此处(配置同步, P1)
|
||||||
CFG_MQTT_HOST = "lbsmqtt.airm2m.com"
|
CFG_MQTT_HOST = "lbsmqtt.airm2m.com"
|
||||||
CFG_MQTT_PORT = 1884
|
CFG_MQTT_PORT = 1884
|
||||||
|
|
||||||
-- ==================== 设备标识 ====================
|
-- ==================== 设备标识 ====================
|
||||||
-- 设备序列码: 6 字节十六进制字符串(与 vd960DBN dev_serial 一致)
|
-- 设备序列码: 6 字节十六进制字符串(与 vd960DBN dev_serial 一致,有线/4G 同一设备)
|
||||||
-- 说明: 有线通道的 Topic 用 dev_serial 生成;4G 通道必须同一主题族,平台才能统一管理。
|
|
||||||
-- 【待确认】是否由 vd960DBN 经 UART 握手动态下发,而非本地写死(见 readme 待办)
|
|
||||||
CFG_DEV_SERIAL = "A1B2C3D4E5F6"
|
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 一致)
|
-- MQTT Topic(与《DLD960_IoT_MQTT协议》§1.2 一致)
|
||||||
CFG_TOPIC_UP = "dld960/" .. CFG_DEV_SERIAL .. "/dev" -- 设备上报(4G publish)
|
CFG_TOPIC_UP = "dld960/" .. CFG_DEV_SERIAL .. "/dev" -- 设备上报
|
||||||
CFG_TOPIC_DOWN = "dld960/" .. CFG_DEV_SERIAL .. "/srv" -- 服务器下发(4G subscribe)
|
CFG_TOPIC_DOWN = "dld960/" .. CFG_DEV_SERIAL .. "/srv" -- 服务器下发
|
||||||
|
|
||||||
-- MQTT ClientID: 用 IMEI 保证唯一(与序列码解耦;鉴权待确认)
|
-- MQTT ClientID: 用 IMEI 保证唯一(与序列码解耦;鉴权待确认)
|
||||||
CFG_MQTT_CLIENT_ID = "vd960Air-" .. mobile.imei()
|
CFG_MQTT_CLIENT_ID = "vd960Air-" .. mobile.imei()
|
||||||
|
|
||||||
-- ==================== UART(对接 vd960DBN) ====================
|
-- ==================== UART(对接 vd960DBN) ====================
|
||||||
-- vd960DBN 侧: CH32V208 串口1(PB6=TX, PB7=RX)
|
-- vd960DBN 侧: CH32V208 串口1(PB6=TX, PB7=RX)
|
||||||
-- 波特率: 官方 demo 默认 115200;vd960DBN UART1 预留口速率【待确认】
|
-- 波特率: 115200(用户决策 2026-08-31,后续如需再调)
|
||||||
CFG_UART_ID = 1
|
CFG_UART_ID = 1
|
||||||
CFG_UART_BAUD = 115200
|
CFG_UART_BAUD = 115200
|
||||||
|
|
||||||
-- 帧协议参数(4G <-> vd960DBN UART 链路帧,设计稿 v0.2 — 用户决策 2026-08-31 校验统一 XOR+SUM)
|
-- 链路帧: 0x7F 帧字节流(方案 C,数据面透传 Loop 地感协议)
|
||||||
-- 帧 = [0x7D] [LEN_H] [LEN_L] [PAYLOAD...] [XOR] [SUM]
|
-- 0x7D 帧仅用于 DBN↔Air780 配置同步/握手(P1)
|
||||||
-- 校验范围: 从 LEN_H 起到 PAYLOAD 末字节(不含 HEAD,与 DBN↔Loop 0x7F 同风格)
|
|
||||||
CFG_FRAME_HEAD = 0x7D
|
-- ==================== 上报节奏 ====================
|
||||||
CFG_FRAME_MAXLEN = 2048 -- PAYLOAD 最大字节数(防御: 超长判非法帧)
|
CFG_HEARTBEAT_INTERVAL = 60 -- 心跳周期(秒,协议 §5.4 默认 60)
|
||||||
|
CFG_REPORT_ALWAYS = true -- MVP: 每收到 0xC0 帧都上报 loop_data;P1 按协议三档节流
|
||||||
|
|
||||||
-- ==================== 4G 特有字段注入 ====================
|
-- ==================== 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)
|
-- true = 上行 JSON 顶层注入 link 对象(见 link_info.lua)
|
||||||
CFG_LINK_ENABLE = true
|
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
|
@module main
|
||||||
@summary vd960Air — Air8781P(Air780EPM) 4G 模块 LuatOS 应用入口
|
@summary vd960Air — Air8781P(Air780EPM) 4G 模块 LuatOS 应用入口(方案 C MVP)
|
||||||
@version 0.1
|
@version 0.3
|
||||||
@date 2026.08.31
|
@date 2026.08.31
|
||||||
@usage
|
@usage
|
||||||
vd960DBN 车检器在有线网络无效时的 4G 兜底通道(UART <-> MQTT 桥):
|
vd960DBN 车检器在有线网络失效时的 4G 兜底通道(方案 C: Air780 协议转换):
|
||||||
|
|
||||||
1、UART1 对接 vd960DBN(CH32V208 串口1 PB6/PB7),帧协议见 uart_app.lua;
|
1、UART1 对接 vd960DBN(CH32V208 串口1 PB6/PB7),0x7F 帧字节流透传;
|
||||||
2、上行: vd960DBN JSON 帧 → 4G → MQTT publish 到 dld960/{sn}/dev
|
2、0x7F 帧解析 → 0xC0 传感 → loop_data JSON(标准,注入 link)→ MQTT dld960/{sn}/dev;
|
||||||
(JSON 命令面与《DLD960_IoT_MQTT协议》V1.10 完全一致,平台侧无感);
|
3、car_state/loop_state 沿检测 → event_report(ACK + 5s×3 重发,对齐 DBN 语义);
|
||||||
3、下行: MQTT 订阅 dld960/{sn}/srv → 4G → 组帧 → UART1 下发 vd960DBN;
|
4、下行: report_config 时钟校准 + event_report ACK;业务命令转换 P1;
|
||||||
4、网络看门狗: 长时间无网络业务则重启系统(官方 network_watchdog);
|
5、initialize(extra_info imei/iccid + link)/ heartbeat 周期上报;
|
||||||
5、网卡: 仅 4G(netdrv_4g)。
|
6、网络看门狗 + 4G 网卡(官方组件)。
|
||||||
]]
|
]]
|
||||||
PROJECT = "vd960Air"
|
PROJECT = "vd960Air"
|
||||||
VERSION = "001.999.000"
|
VERSION = "001.999.000"
|
||||||
@@ -28,9 +28,13 @@ require "network_watchdog"
|
|||||||
-- ==================== 网卡驱动(仅 4G) ====================
|
-- ==================== 网卡驱动(仅 4G) ====================
|
||||||
require "netdrv_device"
|
require "netdrv_device"
|
||||||
|
|
||||||
-- ==================== 串口应用(UART1 <-> vd960DBN) ====================
|
-- ==================== 串口应用(UART1 <-> vd960DBN,0x7F 帧) ====================
|
||||||
require "uart_app"
|
require "uart_app"
|
||||||
|
|
||||||
|
-- ==================== 设备业务(initialize/heartbeat/poll) ====================
|
||||||
|
local app_iot = require "app_iot"
|
||||||
|
app_iot.init()
|
||||||
|
|
||||||
-- ==================== MQTT 客户端(单路, 主题 dld960/{sn}/srv + dev) ====================
|
-- ==================== MQTT 客户端(单路, 主题 dld960/{sn}/srv + dev) ====================
|
||||||
require "mqtt_main"
|
require "mqtt_main"
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ local mqtt_receiver = require "mqtt_receiver"
|
|||||||
local mqtt_sender = require "mqtt_sender"
|
local mqtt_sender = require "mqtt_sender"
|
||||||
-- 加载vd960Air配置
|
-- 加载vd960Air配置
|
||||||
local CFG = require "config"
|
local CFG = require "config"
|
||||||
|
-- 设备业务(initialize/heartbeat)
|
||||||
|
local app_iot = require "app_iot"
|
||||||
|
|
||||||
-- mqtt服务器地址和端口(来自config.lua,商用服务器)
|
-- mqtt服务器地址和端口(来自config.lua,商用服务器)
|
||||||
local SERVER_ADDR = CFG.CFG_MQTT_HOST
|
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")
|
log.info("mqtt_client_main_task_func", "connect success")
|
||||||
-- 通知mqtt sender数据发送应用模块的task,MQTT连接成功
|
-- 通知mqtt sender数据发送应用模块的task,MQTT连接成功
|
||||||
sys.sendMsg(mqtt_sender.TASK_NAME, "MQTT_EVENT", "CONNECT_OK", mqtt_client)
|
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连接失败
|
-- mqtt连接失败
|
||||||
else
|
else
|
||||||
log.info("mqtt_client_main_task_func", "connect error")
|
log.info("mqtt_client_main_task_func", "connect error")
|
||||||
|
|||||||
@@ -1,64 +1,55 @@
|
|||||||
--[[
|
--[[
|
||||||
@module mqtt_receiver
|
@module mqtt_receiver
|
||||||
@summary mqtt client数据接收处理应用功能模块
|
@summary MQTT 下行命令分发(方案 C)
|
||||||
@version 1.0
|
@version 0.2
|
||||||
@date 2025.07.29
|
@date 2026.08.31
|
||||||
@author 马梦阳
|
|
||||||
@usage
|
@usage
|
||||||
本文件为mqtt client 数据接收应用功能模块,核心业务逻辑为:
|
平台下行(dld960/{sn}/srv)处理:
|
||||||
处理接收到的publish数据,同时将数据发送给其他应用功能模块做进一步处理;
|
- event_report ACK(回显 msg_id + code=0)→ evt_queue 出队
|
||||||
|
- report_config → 时钟校准(信封 ts,协议 §2.3)
|
||||||
本文件的对外接口有2个:
|
- 其他标准 JSON 命令(loop_param_*/log_*/ota_* 等)→ P1: 转 0x7F 帧下发 vd960Loop(待实现)
|
||||||
1、mqtt_receiver.proc(topic, payload, metas):publish数据处理入口,在mqtt_main.lua中调用;
|
- frame_cmd(可选兜底)→ P1: hex 解码 → UART1 透传(待实现)
|
||||||
2、sys.publish("RECV_DATA_FROM_SERVER", "recv from mqtt server: ", topic, payload):
|
|
||||||
将接收到的publish中的topic和payload数据通过消息"RECV_DATA_FROM_SERVER"发布出去;
|
|
||||||
需要处理数据的应用功能模块订阅处理此消息即可,本demo项目中uart_app.lua中订阅处理了本消息;
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
local mqtt_receiver = {}
|
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
|
||||||
|
|
||||||
--[[
|
local cmd = obj.cmd
|
||||||
处理接收到的publish数据
|
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
|
elseif cmd == "report_config" then
|
||||||
表示publish主题
|
-- 时钟校准(信封 ts 填当前 Unix 秒)
|
||||||
|
local clock = require "clock"
|
||||||
|
clock.sync(obj.ts or 0, mstick())
|
||||||
|
log.info("mqtt_receiver", "clock sync", obj.ts)
|
||||||
|
|
||||||
@param2 payload string
|
else
|
||||||
表示publish数据负载
|
-- P1: 下行命令转换(loop_param_*/log_*/ota_* 等)暂未实现
|
||||||
|
log.info("mqtt_receiver", "down cmd", cmd, "msg_id", msg_id)
|
||||||
|
end
|
||||||
|
|
||||||
@param2 payload string
|
-- 喂网络看门狗
|
||||||
表示publish数据负载
|
|
||||||
|
|
||||||
@param3 metas table
|
|
||||||
表示publish报文的一些参数;格式如下:
|
|
||||||
{
|
|
||||||
qos: number类型,取值范围0,1,2
|
|
||||||
retain:number类型,取值范围0,1
|
|
||||||
dup:number类型,取值范围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")
|
sys.publish("FEED_NETWORK_WATCHDOG")
|
||||||
|
end
|
||||||
|
|
||||||
-- 将topic和payload通过"RECV_DATA_FROM_SERVER"消息publish出去,给其他应用模块处理
|
function mqtt_receiver.proc(topic, payload, metas)
|
||||||
-- vd960Air: 直接传原始 payload,由 uart_app 组帧后下发 vd960DBN
|
proc(topic, payload, metas)
|
||||||
sys.publish("RECV_DATA_FROM_SERVER", topic, payload)
|
|
||||||
|
|
||||||
-- 也可以直接在此处编写代码,处理topic和payload
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return mqtt_receiver
|
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/
|
vd960Air/
|
||||||
├── main.lua # 入口: 加载 config/看门狗/4G网卡/uart_app/mqtt_main
|
├── main.lua # 入口: 加载 config/看门狗/4G网卡/uart_app/app_iot/mqtt_main
|
||||||
├── config.lua # 配置: MQTT 服务器/序列号/主题/UART/帧参数/link 注入开关
|
├── config.lua # 配置: MQTT 服务器/序列号/主题/UART/上报节奏/link 开关
|
||||||
├── uart_app.lua # UART1 <-> vd960DBN 帧协议(接收状态机 + 下行组帧 + 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)
|
├── link_info.lua # 4G 链路信息采集(IMEI/ICCID/IMSI/MSISDN/CSQ)
|
||||||
├── netdrv_device.lua # 网卡: 仅 4G(官方 netdrv_4g)
|
├── netdrv_device.lua # 网卡: 仅 4G(官方 netdrv_4g)
|
||||||
├── network_watchdog.lua # 网络看门狗(官方原样)
|
├── network_watchdog.lua # 网络看门狗(官方原样)
|
||||||
|
├── tools/
|
||||||
|
│ └── sim_frame_test.py # 帧协议参考验证(Python 1:1 复刻解析公式,字节级断言)
|
||||||
└── mqtt/
|
└── mqtt/
|
||||||
├── mqtt_main.lua # MQTT 客户端(单路, 主题改 dld960/{sn}/srv + dev)
|
├── mqtt_main.lua # MQTT 客户端(连接成功 → initialize + 未决事件重发)
|
||||||
├── mqtt_receiver.lua # 下行 → RECV_DATA_FROM_SERVER(去掉演示前缀)
|
├── mqtt_receiver.lua # 下行分发: event_report ACK 路由 + report_config 时钟校准
|
||||||
└── mqtt_sender.lua # 上行队列 + publish(去掉演示前缀)
|
└── mqtt_sender.lua # 上行队列 + publish(官方改造)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5 vd960DBN 对接要点
|
## 5 vd960DBN 对接要点
|
||||||
@@ -246,10 +253,11 @@ vd960Air/
|
|||||||
|
|
||||||
| 阶段 | 内容 | 状态 |
|
| 阶段 | 内容 | 状态 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| P0 | 工程骨架 + 帧协议 + MQTT 单路桥接 + link 字段注入 | ✅ 完成 |
|
| P0 | 工程骨架 + 帧协议 + MQTT 桥接 + link 注入 | ✅ 完成 |
|
||||||
| P1 | 协议适配文档(4G 章节)+ vd960DBN 侧 UART1 通道 + **配置同步下发(Air780 存储更新)** | ⏳ 待文档/待 vd960DBN 版本 |
|
| **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 | 板级联调: 帧校验、背压、掉线重连、看门狗 | ⏳ 待板 |
|
| P2 | 板级联调: 帧校验、背压、掉线重连、看门狗 | ⏳ 待板 |
|
||||||
| P3 | 4G 状态上报 + 通道切换(策略待讨论)+ 脱机日志/OTA 走 4G | ⏳ 规划 |
|
| P3 | 通道切换(策略待讨论)+ 脱机日志/OTA 走 4G | ⏳ 规划 |
|
||||||
|
|
||||||
**vd960DBN 侧开发计划(2026-08-31 用户确认列入):**
|
**vd960DBN 侧开发计划(2026-08-31 用户确认列入):**
|
||||||
- [ ] UART1(PB6/PB7)通道: 帧收发 + 0x7F 帧魔数分流(0x7F → UART2 透传 / 0x8F → 本地)
|
- [ ] UART1(PB6/PB7)通道: 帧收发 + 0x7F 帧魔数分流(0x7F → UART2 透传 / 0x8F → 本地)
|
||||||
|
|||||||
@@ -1,132 +1,123 @@
|
|||||||
--[[
|
--[[
|
||||||
@module uart_app
|
@module uart_app
|
||||||
@summary vd960Air 串口应用功能模块: UART1 <-> vd960DBN 帧协议对接
|
@summary vd960Air 串口应用: UART1 对接 vd960DBN(0x7F 帧) → 标准 JSON 上报(方案 C)
|
||||||
@version 0.2
|
@version 0.3
|
||||||
@date 2026.08.31
|
@date 2026.08.31
|
||||||
@usage
|
@usage
|
||||||
核心职责:
|
上行链路(方案 C):
|
||||||
1、打开 UART1(对接 vd960DBN CH32V208 串口1 PB6/PB7);
|
vd960Loop --0x7F帧--> vd960DBN --原样转发(UART1)--> Air780
|
||||||
2、上行: 接收 vd960DBN 的 JSON 帧(复用《DLD960_IoT_MQTT协议》命令面),
|
→ frame_parser 解析 0x7F 帧
|
||||||
校验后注入 link 对象 → 发布 SEND_DATA_REQ → mqtt_sender publish 到 dld960/{sn}/dev;
|
→ 0xC0 传感帧: proto_conv 解析 → loop_data JSON(注入 link)发布
|
||||||
3、下行: 订阅 RECV_DATA_FROM_SERVER 消息,将 MQTT 下行 JSON 组帧后经 UART1 下发 vd960DBN。
|
→ car_state/loop_state 沿检测: evt_queue 入队 → event_report 发布(ACK 机制)
|
||||||
|
下行链路:
|
||||||
UART 链路帧格式(v0.2,校验统一 XOR+SUM — 用户决策 2026-08-31,与 DBN↔Loop 0x7F 一致):
|
MQTT 标准 JSON 命令 → (P1: 转 0x7F 帧) → UART1 → vd960DBN → vd960Loop
|
||||||
┌──────┬───────┬───────┬────────────────────┬──────┬──────┐
|
MVP 范围: 上行(initialize/loop_data/event_report/heartbeat)+ report_config 时钟校准;
|
||||||
│ HEAD │ LEN_H │ LEN_L │ PAYLOAD │ XOR │ SUM │
|
下行命令转换与 0x7D 配置同步列入 P1。
|
||||||
│ 0x7D │ 1B │ 1B │ JSON(≤2048B) │ 1B │ 1B │
|
|
||||||
└──────┴───────┴───────┴────────────────────┴──────┴──────┘
|
|
||||||
- HEAD: 0x7D(避开 0x7F DBN↔Loop / 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 丢弃。
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
-- 加载配置
|
-- 依赖
|
||||||
local CFG = require "config"
|
local CFG = require "config"
|
||||||
-- 4G 链路信息采集(IMEI/ICCID/CSQ)
|
|
||||||
local link_info = require "link_info"
|
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 uart_app = {}
|
||||||
local MAX_LEN = CFG.CFG_FRAME_MAXLEN
|
|
||||||
|
|
||||||
-- ==================== 接收状态机 ====================
|
local UART_ID = CFG.CFG_UART_ID
|
||||||
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 function rx_reset()
|
-- ==================== 公共工具 ====================
|
||||||
rx_state = RX_IDLE
|
-- 上行 JSON 注入 link 对象(4G 特有字段)
|
||||||
rx_len = 0
|
local function inject_link(payload)
|
||||||
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 通道设备(流量卡识别/信号监控)。不破坏主协议结构,老平台忽略即可。
|
|
||||||
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 = link_info.get()
|
||||||
payload = json.encode(obj)
|
return json.encode(obj)
|
||||||
else
|
|
||||||
-- JSON 解析失败: 原样转发,不阻塞上行
|
|
||||||
log.warn("uart_app.frame", "json decode fail, forward raw")
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
return payload
|
||||||
|
end
|
||||||
|
|
||||||
-- SEND_DATA_REQ(tag, topic, payload, qos)
|
-- 组 loop_data JSON(协议 §5.2)
|
||||||
-- 上行统一发布到 dev 主题(平台侧与有线通道无差别)
|
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)
|
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
|
end
|
||||||
|
|
||||||
-- 逐字节喂入接收状态机
|
-- ==================== 0x7F 帧回调 ====================
|
||||||
local function feed_byte(b)
|
-- 独立 loop_data msg_id 计数
|
||||||
if rx_state == RX_IDLE then
|
local g_ld_msg_id = 0
|
||||||
if b == CFG.CFG_FRAME_HEAD then
|
local function next_ld_msg_id()
|
||||||
rx_state = RX_HEAD
|
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
|
end
|
||||||
elseif rx_state == RX_HEAD then
|
-- 刷新 loop_status 缓存
|
||||||
rx_len = b * 256
|
for i = 1, #channels do
|
||||||
rx_xor = b
|
last_loop_ok[i] = channels[i].loop_ok
|
||||||
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
|
|
||||||
end
|
end
|
||||||
elseif rx_state == RX_LENL then
|
-- 沿检测 → 事件入队(返回是否需要立即发布)
|
||||||
-- 0 长度帧非法
|
local need_evt = false
|
||||||
if rx_len == 0 then
|
local events = proto_conv.detect_events(channels)
|
||||||
rx_reset()
|
for i = 1, #events do
|
||||||
else
|
need_evt = evt_queue.enqueue(events[i]) or need_evt
|
||||||
rx_state = RX_DATA
|
|
||||||
end
|
end
|
||||||
elseif rx_state == RX_DATA then
|
-- loop_data 上报(带 link)
|
||||||
rx_buf = rx_buf .. string.char(b)
|
local ts = clock.now()
|
||||||
rx_xor = rx_xor ~ b
|
local ld = inject_link(build_loop_data(channels, ts, next_ld_msg_id()))
|
||||||
rx_sum = (rx_sum + b) % 256
|
sys.publish("SEND_DATA_REQ", "vd960DBN", CFG.CFG_TOPIC_UP, ld, 0)
|
||||||
if rx_buf:len() >= rx_len then
|
-- 事件发布
|
||||||
rx_state = RX_XOR
|
if need_evt then
|
||||||
|
publish_event()
|
||||||
end
|
end
|
||||||
elseif rx_state == RX_XOR then
|
else
|
||||||
if b ~= rx_xor then
|
-- 其他 0x7F 帧(命令响应等): P1 下行命令转换实现后处理
|
||||||
log.warn("uart_app.feed", "xor fail")
|
log.info("uart_app.frame", "cmd", string.format("0x%02X", cmd), "len", payload:len())
|
||||||
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()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- UART1 接收中断处理
|
-- ==================== UART 接收 ====================
|
||||||
local function read()
|
local function read()
|
||||||
local s
|
local s
|
||||||
while true do
|
while true do
|
||||||
@@ -135,40 +126,45 @@ local function read()
|
|||||||
break
|
break
|
||||||
end
|
end
|
||||||
for i = 1, s:len() do
|
for i = 1, s:len() do
|
||||||
feed_byte(string.byte(s, i))
|
frame_parser.feed(string.byte(s, i))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ==================== 下行: MQTT → UART1 ====================
|
-- ==================== 对外接口 ====================
|
||||||
-- mqtt_receiver 发布 RECV_DATA_FROM_SERVER(topic, payload)
|
-- MQTT 重连后: 未决事件包立即重发
|
||||||
local function recv_data_from_server_proc(topic, payload)
|
function uart_app.on_mqtt_reconnect()
|
||||||
if not payload or payload:len() == 0 then
|
local action = evt_queue.on_reconnect(mstick())
|
||||||
return
|
if action == "resend" then
|
||||||
|
publish_event()
|
||||||
end
|
end
|
||||||
-- 组帧: HEAD + LEN(2B BE) + PAYLOAD + XOR + SUM
|
end
|
||||||
-- 校验范围: 从 LEN_H 起(不含 HEAD,与 0x7F 同风格)
|
|
||||||
local len = payload:len()
|
-- 平台 ACK(事件确认)路由
|
||||||
local hi = math.floor(len / 256)
|
function uart_app.handle_event_ack(msg_id, code)
|
||||||
local lo = len % 256
|
evt_queue.handle_ack(msg_id, code)
|
||||||
local x = hi ~ lo
|
end
|
||||||
local s = (hi + lo) % 256
|
|
||||||
for i = 1, len do
|
-- 主循环驱动: 事件重发超时检查
|
||||||
local b = string.byte(payload, i)
|
function uart_app.poll()
|
||||||
x = x ~ b
|
local action = evt_queue.poll(mstick())
|
||||||
s = (s + b) % 256
|
if action == "resend" then
|
||||||
|
publish_event()
|
||||||
end
|
end
|
||||||
local frame = string.char(CFG.CFG_FRAME_HEAD)
|
end
|
||||||
.. string.char(hi) .. string.char(lo)
|
|
||||||
.. payload
|
-- 上报当前传感缓存? MVP: 心跳带 loop_status 由 mqtt_main 维护,此处暂不提供
|
||||||
.. string.char(x) .. string.char(s)
|
|
||||||
uart.write(UART_ID, frame)
|
-- heartbeat 用: 最近 4 路线圈状态(true=正常)
|
||||||
log.info("uart_app.down", "len", len)
|
function uart_app.get_loop_status()
|
||||||
|
return last_loop_ok
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ==================== 初始化 ====================
|
-- ==================== 初始化 ====================
|
||||||
|
frame_parser.init(on_frame)
|
||||||
uart.setup(UART_ID, CFG.CFG_UART_BAUD, 8, 1)
|
uart.setup(UART_ID, CFG.CFG_UART_BAUD, 8, 1)
|
||||||
uart.on(UART_ID, "receive", read)
|
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)
|
log.info("uart_app", "init ok", "baud", CFG.CFG_UART_BAUD, "topic_up", CFG.CFG_TOPIC_UP)
|
||||||
|
|
||||||
|
return uart_app
|
||||||
|
|||||||
Reference in New Issue
Block a user