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:
@@ -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,与 DBN↔Loop 0x7F 一致):
|
||||
┌──────┬───────┬───────┬────────────────────┬──────┬──────┐
|
||||
│ HEAD │ LEN_H │ LEN_L │ PAYLOAD │ XOR │ SUM │
|
||||
│ 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 丢弃。
|
||||
上行链路(方案 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
|
||||
|
||||
Reference in New Issue
Block a user