feat(vd960Air): Air8781P 4G UART<->MQTT 桥接工程骨架

基于官方 Air780EPM demo/mqtt 骨架创建 vd960Air 工程:
- 目标: vd960DBN 有线网络失效时,经 Air780EPM 4G 兜底上报
- UART1(115200 8N1)对接 vd960DBN CH32V208 串口1 PB6/PB7
- MQTT 主题按《DLD960_IoT_MQTT协议》V1.10: 订阅 dld960/{sn}/srv, 发布 dld960/{sn}/dev
- 帧协议(设计稿 v0.1): [0x7D][LEN_H][LEN_L][JSON][SUM], 双向对称, ≤2048B
- 上行: DBN JSON 帧 → 4G 校验拆帧 → MQTT publish(JSON 命令面与有线完全一致)
- 下行: MQTT 订阅 → 组帧 → UART1 下发 DBN
- 改造: config.lua 集中配置 / 去掉官方 demo 的 "send from" 演示前缀 / 单路 MQTT
- readme: 架构、协议设计决策(4G 适配协议=命令面复用+网络类裁剪+4G特有扩展)、待确认清单
This commit is contained in:
wangfq
2026-08-31 11:35:22 +08:00
parent bb5e5dda68
commit 8911b04df2
9 changed files with 891 additions and 0 deletions
@@ -0,0 +1,39 @@
--[[
@module config
@summary vd960Air 配置: MQTT 服务器 / 设备序列号 / UART 参数
@date 2026.08.31
@usage
vd960Air 是 Air8781P 整板(Air780EPM 核心模组)上的 LuatOS 应用,
作为 vd960DBN 车检器在有线网络失效时的 4G 兜底通道(UART <-> MQTT 桥)。
协议依据:《DLD960_IoT_MQTT协议》V1.10(命令面/JSON 结构与有线通道完全一致)
]]
-- ==================== MQTT 服务器 ====================
-- 商用服务器地址和端口(替换为实际值;官方 demo 地址仅测试用)
CFG_MQTT_HOST = "lbsmqtt.airm2m.com"
CFG_MQTT_PORT = 1884
-- ==================== 设备标识 ====================
-- 设备序列码: 6 字节十六进制字符串(与 vd960DBN dev_serial 一致)
-- 说明: 有线通道的 Topic 用 dev_serial 生成;4G 通道必须同一主题族,平台才能统一管理。
-- 【待确认】是否由 vd960DBN 经 UART 握手动态下发,而非本地写死(见 readme 待办)
CFG_DEV_SERIAL = "A1B2C3D4E5F6"
-- 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)
-- MQTT ClientID: 用 IMEI 保证唯一(与序列码解耦;鉴权待确认)
CFG_MQTT_CLIENT_ID = "vd960Air-" .. mobile.imei()
-- ==================== UART(对接 vd960DBN) ====================
-- vd960DBN 侧: CH32V208 串口1(PB6=TX, PB7=RX)
-- 波特率: 官方 demo 默认 115200;vd960DBN UART1 预留口速率【待确认】
CFG_UART_ID = 1
CFG_UART_BAUD = 115200
-- 帧协议参数(4G <-> vd960DBN UART 链路帧,设计稿 v0.1)
-- 帧 = [0x7D] [LEN_H] [LEN_L] [PAYLOAD...] [SUM], 详见 uart_app.lua 头注释
CFG_FRAME_HEAD = 0x7D
CFG_FRAME_MAXLEN = 2048 -- PAYLOAD 最大字节数(防御: 超长判非法帧)
@@ -0,0 +1,40 @@
--[[
@module main
@summary vd960Air — Air8781P(Air780EPM) 4G 模块 LuatOS 应用入口
@version 0.1
@date 2026.08.31
@usage
vd960DBN 车检器在有线网络无效时的 4G 兜底通道(UART <-> MQTT 桥):
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)。
]]
PROJECT = "vd960Air"
VERSION = "001.999.000"
-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)
-- ==================== 加载配置 ====================
require "config"
-- ==================== 网络环境检测看门狗 ====================
require "network_watchdog"
-- ==================== 网卡驱动(仅 4G) ====================
require "netdrv_device"
-- ==================== 串口应用(UART1 <-> vd960DBN) ====================
require "uart_app"
-- ==================== MQTT 客户端(单路, 主题 dld960/{sn}/srv + dev) ====================
require "mqtt_main"
-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行
@@ -0,0 +1,266 @@
--[[
@module mqtt_main
@summary mqtt client 主应用功能模块
@version 1.0
@date 2025.07.28
@author 马梦阳
@usage
本文件为mqtt client 主应用功能模块,核心业务逻辑为:
1、创建一个mqtt client,连接server
2、处理连接/订阅/取消订阅/异常逻辑,出现异常后执行重连动作;
3、调用mqtt_receiver的外部接口mqtt_receiver.proc,对接收到的publish数据进行处理;
4、调用sys.sendMsg接口,发送"CONNECT OK"、"PUBLISH OK"和"DISCONNECTED"三种类型的"MQTT_EVENT"消息到mqtt_sender的task,控制publish数据发送逻辑;
5、收到MQTT心跳应答后,执行sys.publish("FEED_NETWORK_WATCHDOG") 对网络环境检测看门狗功能模块进行喂狗;
本文件没有对外接口,直接在main.lua中require "mqtt_main"就可以加载运行;
]]
-- 加载mqtt client数据接收功能模块
local mqtt_receiver = require "mqtt_receiver"
-- 加载mqtt client数据发送功能模块
local mqtt_sender = require "mqtt_sender"
-- 加载vd960Air配置
local CFG = require "config"
-- mqtt服务器地址和端口(来自config.lua,商用服务器)
local SERVER_ADDR = CFG.CFG_MQTT_HOST
local SERVER_PORT = CFG.CFG_MQTT_PORT
-- mqtt_main的任务名
local TASK_NAME = mqtt_sender.TASK_NAME_PREFIX.."main"
-- mqtt主题前缀/模板(与《DLD960_IoT_MQTT协议》§1.2 一致)
-- 下行订阅: dld960/{sn}/srv; 上行发布: dld960/{sn}/dev
local TOPIC_PREFIX = CFG.CFG_DEV_SERIAL
-- mqtt client的事件回调函数
local function mqtt_client_event_cbfunc(mqtt_client, event, data, payload, metas)
log.info("mqtt_client_event_cbfunc", mqtt_client, event, data, payload, json.encode(metas))
-- mqtt连接成功
if event == "conack" then
sys.sendMsg(TASK_NAME, "MQTT_EVENT", "CONNECT", true)
-- 订阅单主题
-- 第二个参数表示qos,取值范围为0,1,2,如果不设置,默认为0
if not mqtt_client:subscribe(CFG.CFG_TOPIC_DOWN) then
sys.sendMsg(TASK_NAME, "MQTT_EVENT", "SUBSCRIBE", false, -1)
end
-- 订阅多主题,如果有需要,打开注释
-- 表中的每一个订阅主题的格式为[topic]=qos
-- if not mqtt_client:subscribe(
-- {
-- [(TOPIC_PREFIX .. "/data"]=0,
-- [(TOPIC_PREFIX .. "/cmd"]=1
-- }
-- ) then
-- sys.sendMsg(TASK_NAME, "MQTT_EVENT", "SUBSCRIBE", false, -1)
-- end
-- 订阅结果
-- data:订阅应答结果,true为成功,false为失败
-- payloadnumber类型;成功时表示qos,取值范围为0,1,2;失败时表示失败码,一般是0x80
elseif event == "suback" then
-- 发送消息通知 mqtt main task
sys.sendMsg(TASK_NAME, "MQTT_EVENT", "SUBSCRIBE", data, payload)
-- 取消订阅成功
elseif event == "unsuback" then
-- 发送消息通知 mqtt main task
sys.sendMsg(TASK_NAME, "MQTT_EVENT", "UNSUBSCRIBE", true)
-- 接收到服务器下发的publish数据
-- datastring类型,表示topic
-- payloadstring类型,表示payload
-- metastable类型,数据内容如下
-- {
-- qos: number类型,取值范围0,1,2
-- retainnumber类型,取值范围0,1
-- dupnumber类型,取值范围0,1
-- message_id: number类型
-- }
elseif event == "recv" then
-- 对接收到的publish数据处理
mqtt_receiver.proc(data, payload, metas)
-- 发送成功publish数据
-- datanumber类型,表示message id
elseif event == "sent" then
-- 发送消息通知 mqtt sender task
sys.sendMsg(mqtt_sender.TASK_NAME, "MQTT_EVENT", "PUBLISH_OK", data)
-- 服务器断开mqtt连接
elseif event == "disconnect" then
-- 发送消息通知 mqtt main task
sys.sendMsg(TASK_NAME, "MQTT_EVENT", "DISCONNECTED", false)
-- 收到服务器的心跳应答
elseif event == "pong" then
-- 接收到数据,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
-- 严重异常,本地会主动断开连接
-- data:string类型,表示具体的异常,有以下几种:
-- "connect"tcp连接失败
-- "tx":数据发送失败
-- "conack"mqtt connect后,服务器应答CONNACK鉴权失败,失败码为payloadnumber类型)
-- "other":其他异常
elseif event == "error" then
if data == "connect" or data == "conack" then
-- 发送消息通知 mqtt main task,连接失败
sys.sendMsg(TASK_NAME, "MQTT_EVENT", "CONNECT", false)
elseif data == "other" or data == "tx" then
-- 发送消息通知 mqtt main task,出现异常
sys.sendMsg(TASK_NAME, "MQTT_EVENT", "ERROR")
end
end
end
-- mqtt main task 的任务处理函数
local function mqtt_client_main_task_func()
local mqtt_client
local result, msg
while true do
-- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
while not socket.adapter(socket.dft()) do
log.warn("mqtt_client_main_task_func", "wait IP_READY", socket.dft())
-- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
-- 或者等待1秒超时退出阻塞等待状态;
-- 注意:此处的1000毫秒超时不要修改的更长;
-- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
-- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
-- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
sys.waitUntil("IP_READY", 1000)
end
-- 检测到了IP_READY消息
log.info("mqtt_client_main_task_func", "recv IP_READY", socket.dft())
-- 清空此task绑定的消息队列中的未处理的消息
sys.cleanMsg(TASK_NAME)
-- 创建mqtt client对象
mqtt_client = mqtt.create(nil, SERVER_ADDR, SERVER_PORT)
-- 如果创建mqtt client对象失败
if not mqtt_client then
log.error("mqtt_client_main_task_func", "mqtt.create error")
goto EXCEPTION_PROC
end
-- 配置mqtt client对象的client idusernamepassword和clean session标志
result = mqtt_client:auth(CFG.CFG_MQTT_CLIENT_ID, "", "", true)
-- 如果配置失败
if not result then
log.error("mqtt_client_main_task_func", "mqtt_client:auth error")
goto EXCEPTION_PROC
end
-- 注册mqtt client对象的事件回调函数
mqtt_client:on(mqtt_client_event_cbfunc)
-- 设置mqtt keepalive时间为120秒
-- 如果没有设置,内核固件中默认为180秒
-- 有需要的话,可以打开注释
-- mqtt_client:keepalive(120)
-- 设置遗嘱消息,有需要的话,可以打开注释
-- mqtt_client:will(TOPIC_PREFIX .. "/status", "offline")
-- 连接server
result = mqtt_client:connect()
-- 如果连接server失败
if not result then
log.error("mqtt_client_main_task_func", "mqtt_client:connect error")
goto EXCEPTION_PROC
end
-- 连接、断开连接、订阅、取消订阅、异常等各种事件的处理调度逻辑
while true do
-- 等待"MQTT_EVENT"消息
msg = sys.waitMsg(TASK_NAME, "MQTT_EVENT")
log.info("mqtt_client_main_task_func waitMsg", msg[2], msg[3], msg[4])
-- connect连接结果
-- msg[3]表示连接结果,true为连接成功,false为连接失败
if msg[2] == "CONNECT" then
-- mqtt连接成功
if msg[3] then
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)
-- mqtt连接失败
else
log.info("mqtt_client_main_task_func", "connect error")
-- 退出循环,发起重连
break
end
-- subscribe订阅结果
-- msg[3]表示订阅结果,true为订阅成功,false为订阅失败
elseif msg[2] == "SUBSCRIBE" then
-- 订阅成功
if msg[3] then
log.info("mqtt_client_main_task_func", "subscribe success", "qos: "..(msg[4] or "nil"))
-- 订阅失败
else
log.error("mqtt_client_main_task_func", "subscribe error", "code", msg[4])
-- 主动断开mqtt client连接
mqtt_client:disconnect()
-- 发送disconnect之后,此处延时1秒,给数据发送预留一点儿时间,发送到服务器;
-- 即使1秒的时间不足以发送给服务器也没关系;对服务器来说,mqtt客户端只是没有优雅的断开,不影响什么实质功能;
sys.wait(1000)
break
end
-- unsubscribe取消订阅成功
elseif msg[2] == "UNSUBSCRIBE" then
log.info("mqtt_client_main_task_func", "unsubscribe success")
-- 需要主动关闭mqtt连接
-- 用户需要主动关闭mqtt连接时,可以调用sys.sendMsg(TASK_NAME, "MQTT_EVENT", "CLOSE")
elseif msg[2] == "CLOSE" then
-- 主动断开mqtt client连接
mqtt_client:disconnect()
-- 发送disconnect之后,此处延时1秒,给数据发送预留一点儿时间,发送到服务器;
-- 即使1秒的时间不足以发送给服务器也没关系;对服务器来说,mqtt客户端只是没有优雅的断开,不影响什么实质功能;
sys.wait(1000)
break
-- 被动关闭了mqtt连接
-- 被网络或者服务器断开了连接
elseif msg[2] == "DISCONNECTED" then
break
-- 出现了其他异常
elseif msg[2] == "ERROR" then
break
end
end
-- 出现异常
::EXCEPTION_PROC::
-- 清空此task绑定的消息队列中的未处理的消息
sys.cleanMsg(TASK_NAME)
-- 通知mqtt sender数据发送应用模块的task,MQTT连接已经断开
sys.sendMsg(mqtt_sender.TASK_NAME, "MQTT_EVENT", "DISCONNECTED")
-- 如果存在mqtt client对象
if mqtt_client then
-- 关闭mqtt client,并且释放mqtt client对象
mqtt_client:close()
mqtt_client = nil
end
-- 5秒后跳转到循环体开始位置,自动发起重连
sys.wait(5000)
end
end
--创建并且启动一个task
--运行这个task的处理函数mqtt_client_main_task_func
sys.taskInitEx(mqtt_client_main_task_func, TASK_NAME)
@@ -0,0 +1,64 @@
--[[
@module mqtt_receiver
@summary mqtt client数据接收处理应用功能模块
@version 1.0
@date 2025.07.29
@author 马梦阳
@usage
本文件为mqtt client 数据接收应用功能模块,核心业务逻辑为:
处理接收到的publish数据,同时将数据发送给其他应用功能模块做进一步处理;
本文件的对外接口有2个:
1、mqtt_receiver.proc(topic, payload, metas)publish数据处理入口,在mqtt_main.lua中调用;
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 = {}
--[[
处理接收到的publish数据
@api mqtt_receiver.proc(topic, payload, metas)
@param1 topic string
表示publish主题
@param2 payload string
表示publish数据负载
@param2 payload string
表示publish数据负载
@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
end
return mqtt_receiver
@@ -0,0 +1,156 @@
--[[
@module mqtt_sender
@summary mqtt client数据发送应用功能模块
@version 1.0
@date 2025.07.29
@author 马梦阳
@usage
本文件为mqtt client 数据发送应用功能模块,核心业务逻辑为:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)订阅"SEND_DATA_REQ"消息,将其他应用模块需要发送的数据存储到队列send_queue中;
2、mqtt sender task接收"CONNECT OK"、"PUBLISH_REQ"、"PUBLISH OK"三种类型的"MQTT_EVENT"消息,遍历队列send_queue,逐条发送数据到server
3、mqtt sender task接收"DISCONNECTED"类型的"MQTT_EVENT"消息,丢弃掉队列send_queue中未发送的数据;
4、任何一条数据无论发送成功还是失败,只要这条数据有回调函数,都会通过回调函数通知数据发送方;
本文件的对外接口有1个:
1、sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func):订阅"SEND_DATA_REQ"消息;
其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的topic,payload和qos以及回调函数和回调参数一起publish出去;
本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
]]
local mqtt_sender = {}
--[[
数据发送队列,数据结构为:
{
[1] = {topic="topic1", payload="payload1", qos=0, cb={func=callback_function1, para=callback_para1}},
[2] = {topic="topic2", payload="payload2", qos=1, cb={func=callback_function2, para=callback_para2}},
[3] = {topic="topic3", payload="payload3", qos=2, cb={func=callback_function3, para=callback_para3}},
}
topic的内容为publish的主题,string类型,必须存在;
payload的内容为publish的负载数据,string类型,必须存在;
qos的内容为publish的质量等级,number类型,取值范围0,1,2,可选,如果用户没有指定,默认为0;
cb.func的内容为数据发送结果的用户回调函数,可以不存在;
cb.para的内容为数据发送结果的用户回调函数的回调参数,可以不存在;
]]
local send_queue = {}
-- mqtt client的任务名前缀
mqtt_sender.TASK_NAME_PREFIX = "mqtt_"
-- mqtt_client_sender的任务名
mqtt_sender.TASK_NAME = mqtt_sender.TASK_NAME_PREFIX.."sender"
-- "SEND_DATA_REQ"消息的处理函数
local function send_data_req_proc_func(tag, topic, payload, qos, cb)
-- 将原始数据插入到发送队列send_queue中
-- vd960Air: 直接发原始 payload(vd960DBN 的 JSON 报文),不加演示前缀
table.insert(send_queue, {topic=topic, payload=payload, qos=qos or 0, cb=cb})
-- 发送消息通知 mqtt sender task,有新数据等待发送
sys.sendMsg(mqtt_sender.TASK_NAME, "MQTT_EVENT", "PUBLISH_REQ")
end
-- 按照顺序发送send_queue中的数据
-- 如果调用publish接口成功,则返回当前正在发送的数据项
-- 如果调用publish接口失败,通知回调函数发送失败后,继续发送下一条数据
local function publish_item(mqtt_client)
local item
-- 如果发送队列中有数据等待发送
while #send_queue>0 do
-- 取出来第一条数据赋值给item
-- 同时从队列send_queue中删除这一条数据
item = table.remove(send_queue, 1)
-- publish数据
-- result表示调用publish接口的同步结果,返回值有以下几种:
-- 如果失败,返回nil
-- 如果成功,number类型,qos为0时直接返回0qos为1或者2时返回publish报文的message id
result = mqtt_client:publish(item.topic, item.payload, item.qos)
-- publish接口调用成功
if result then
return item
-- publish接口调用失败
else
-- 如果当前发送的数据有用户回调函数,则执行用户回调函数
if item.cb and item.cb.func then
item.cb.func(false, item.cb.para)
end
end
end
end
local function publish_item_cbfunc(item, result)
if item then
-- 如果当前发送的数据有用户回调函数,则执行用户回调函数
if item.cb and item.cb.func then
item.cb.func(result, item.cb.para)
end
end
end
-- mqtt client sender的任务处理函数
local function mqtt_client_sender_task_func()
local mqtt_client
local send_item
local result, msg
while true do
-- 等待"MQTT_EVENT"消息
msg = sys.waitMsg(mqtt_sender.TASK_NAME, "MQTT_EVENT")
-- mqtt连接成功
-- msg[3]表示mqtt client对象
if msg[2] == "CONNECT_OK" then
mqtt_client = msg[3]
-- 发送send_queue中的数据
send_item = publish_item(mqtt_client)
-- mqtt publish数据请求
elseif msg[2] == "PUBLISH_REQ" then
-- 如果mqtt client对象存在,并且没有正在等待发送结果的发送数据项
if mqtt_client and not send_item then
-- 发送send_queue中的数据
send_item = publish_item(mqtt_client)
end
-- mqtt publish数据成功
elseif msg[2] == "PUBLISH_OK" then
-- publish成功,执行回调函数通知发送方
publish_item_cbfunc(send_item, true)
-- publish成功,通知网络环境检测看门狗功能模块进行喂狗
sys.publish("FEED_NETWORK_WATCHDOG")
-- 发送send_queue中的数据
send_item = publish_item(mqtt_client)
-- mqtt断开连接
elseif msg[2] == "DISCONNECTED" then
-- 清空mqtt client对象
mqtt_client = nil
-- 如果存在正在等待发送结果的发送项,执行回调函数通知发送方失败
publish_item_cbfunc(send_item, false)
-- 如果发送队列中有数据等待发送
while #send_queue>0 do
-- 取出来第一条数据赋值给send_item
-- 同时从队列send_queue中删除这一条数据
send_item = table.remove(send_queue,1)
-- 执行回调函数通知发送方失败
publish_item_cbfunc(send_item, false)
end
-- 当前没有正在等待发送结果的发送项
send_item = nil
end
end
end
-- 订阅"SEND_DATA_REQ"消息;
-- 其他应用模块如果需要发送数据,直接sys.publish这个消息即可,将需要发送的数据以及回调函数和回调参数一起publish出去;
-- 本demo项目中uart_app.lua和timer_app.lua中publish了这个消息;
sys.subscribe("SEND_DATA_REQ", send_data_req_proc_func)
--创建并且启动一个task
--运行这个task的处理函数mqtt_client_sender_task_func
sys.taskInitEx(mqtt_client_sender_task_func, mqtt_sender.TASK_NAME)
return mqtt_sender
@@ -0,0 +1,29 @@
--[[
@module netdrv_device
@summary 网络驱动设备功能模块
@version 1.0
@date 2025.07.24
@author 马梦阳
@usage
本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
1、netdrv_4gsocket.LWIP_GP4G网卡;
2、netdrv_eth_spisocket.LWIP_ETH,通过SPI外挂CH390H芯片的以太网卡;
3、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
根据自己的项目需求,只需要require以上三种中的一种即可;
本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
]]
-- 根据自己的项目需求,只需要require以下三种中的一种即可;
-- 加载“4G网卡”驱动模块
require "netdrv_4g"
-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
-- require "netdrv_eth_spi"
-- 加载“可以配置优先级的多种网卡”驱动模块
-- require "netdrv_multiple"
@@ -0,0 +1,63 @@
--[[
@module network_watchdog
@summary 网络环境检测看门狗功能模块
@version 1.0
@date 2025.07.23
@author 马梦阳
@usage
本文件为网络环境检测看门狗功能模块,监控网络环境是否工作正常(设备和服务器双向通信正常,或者至少单向通信正常),核心业务逻辑为:
1、启动一个网络环境检测看门狗task,等待其他mqtt网络应用功能模块来喂狗,如果喂狗超时,则控制软件重启;
2、如何确定“喂狗超时时间”,一般来说,有以下几个原则;
(1) 先确定一个最小基准值T1,2分钟或者5分钟或者10分钟,这个取值取决于具体项目需求,但是不能太短,因为开机后,在网络环境不太好的地方,网络初始化可能需要比较长的时间,一般推荐这个值不能小于2分钟;
(2) 再确定一个和产品业务逻辑有关的一个值T2,这个值和产品的应用业务逻辑息息相关,假设你的产品业务中:
<1> 服务器会定时下发数据给设备,例如设备连接上业务服务器之后,每隔3分钟,设备都会给服务器发送一次心跳,然后服务器都会立即回复一个心跳应答包;
这种情况下,可以取3分钟的大于等于1的倍数(例如1倍,1.5倍,2倍等等)+一段时间(例如10秒钟,如果前面是1倍,则此处必须加一段时间,给网络数据传输过程留够充足的时间);
<2> mqtt本身有keep alive的心跳机制,例如设备连接上业务服务器之后,默认每隔2分钟,设备都会给服务器发送一次心跳,服务器也会回复一个心跳应答数据;
这种情况下,可以取2分钟的大于等于1的倍数(例如1倍,1.5倍,2倍等等)+一段时间(例如10秒钟,如果前面是1倍,则此处必须加一段时间,给网络数据传输过程留够充足的时间);
(3) 取T1和T2的最大值,就是“喂狗超时时间”
3、其他mqtt网络业务功能模块的喂狗时机,和上面2.2的描述相对应,一般来说,可以在以下几种时间点执行喂狗动作:
(1) 设备收到服务器下发的数据时
(2) 设备收到服务器回复的mqtt心跳应答数据时
4、最重要的一点是:以上所说的原则,仅仅是建议,要根据自己的实际项目业务逻辑以及自己的需求最终确定看门狗方案
5、具体到本demo
(1) 产品业务逻辑为:
<1> 创建了一个mqtt连接,设备每隔5秒钟发送一次数据到服务器,服务器何时下发应用数据给设备不确定;
<2> 创建了一个mqtt ssl、不需要证书校验的连接,设备每隔5秒钟发送一次数据到服务器,服务器何时下发应用数据给设备不确定;
<3> 创建了一个mqtt ssl、client单向校验server证书的连接,设备每隔5秒钟发送一次数据到服务器,服务器何时下发应用数据给设备不确定;
<4> 每隔3分钟,这三路mqtt连接都会发送一次mqtt心跳给server,server收到心跳后回复心跳应答给client;
(2) 确定喂狗超时时间:
<1> 本demo支持单WIFI、单以太网、单4G网络连接外网,网络环境准备就绪预留2分钟的时间已经足够,所以最小基准值T1取值2分钟;
<2> 本demo中存在3路mqtt连接,但是这3路mqtt连接都没有定时或者至少一段时间,服务器下发应用数据给设备,所以无法基于服务器下发应用数据的业务逻辑来确定T2的值;
<3> 本demo中存在3路mqtt连接,每1路mqtt连接,设备都是3分钟发送一次mqtt心跳数据给服务器,服务器收到后会立即回复一个mqtt心跳应答数据给设备;
所以可以通过3分钟的大于等于1的倍数(例如1倍,1.5倍,2倍等等)+一段时间(例如10秒钟,如果前面是1倍,则此处必须加一段时间,给网络数据传输过程留够充足的时间)来确定T2的值;
在这个demo中,我能接受的网络连续异常时长是5分钟,所以,T2取值5分钟;
<4> 取T1 2分钟和T2 5分钟的最大值,最终的喂狗超时时间就是5分钟;
(3) 确定喂狗时机:
<1> 3路mqtt连接中,任何1路收到服务器的下发的应用数据时;
<2> 3路mqtt连接中,任何1路收到服务器的回复的心跳应答数据时;
6、本demo设计的网络环境检测看门狗功能模块,可以检测以下两种种的任意一种网络环境异常:
(1) 网络环境连续超过5分钟没有准备就绪
(2) mqtt、mqtt ssl、mqtt ssl单向校验证书3路连接中,连续5分钟没有收到服务器下发的应用数据或者服务器回复的心跳应答数据;
本文件没有对外接口,直接在main.lua中require "network_watchdog"就可以加载运行;
外部功能模块喂狗时,直接调用sys.publish("FEED_NETWORK_WATCHDOG")
]]
-- 网络环境检测看门狗task处理函数
local function network_watchdog_task_func()
while true do
--如果等待180秒没有等到"FEED_NETWORK_WATCHDOG"消息,则看门狗超时
if not sys.waitUntil("FEED_NETWORK_WATCHDOG", 300000) then
log.error("network_watchdog_task_func timeout")
-- 等待3秒钟,然后软件重启
sys.wait(3000)
rtos.reboot()
end
end
end
--创建并且启动一个task
--运行这个task的处理函数network_watchdog_task_func
sys.taskInit(network_watchdog_task_func)
@@ -0,0 +1,93 @@
# vd960Air — Air8781P(Air780EPM) 4G 通信模块 LuatOS 工程
> vd960DBN 车检器在**有线网络失效**时的 4G 兜底通道: UART <-> MQTT 桥。
## 1 系统架构
```
┌─────────────────┐ UART1 (115200) ┌──────────────────────┐ MQTT 3.1.1 ┌──────────┐
│ vd960DBN │ 帧协议(0x7D) │ 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协议》的 **"4G 通道适配"章节(或独立附录)**,标注:
- 不支持的命令清单(ssc_net_* / iot_net_* / iot_topic_* 等,4G 通道返回 `code=4`)
- 新增 4G 特有命令(如 `4g_status_query` / `link_status_report`)
- UART 链路帧格式(本工程 §3)
- 与主协议共享的 JSON 结构,平台侧零改动
## 3 UART 链路帧协议(设计稿 v0.1)
```
帧 = [HEAD 0x7D] [LEN_H] [LEN_L] [PAYLOAD...] [SUM]
- HEAD: 0x7D(与 DBN↔Loop 的 0x7F 帧区分,4G 链路独立魔数)
- LEN: uint16 大端, PAYLOAD(JSON) 字节数, ≤2048
- SUM: uint8 累加和(HEAD+LEN_H+LEN_L+PAYLOAD 全字节取低 8 位)
- 双向对称
```
- 为什么不用 0x7F 帧: 0x7F 帧 LEN 上限 56B,装不下 MQTT JSON(loop_data 数百字节)
- 为什么 2B 长度: 覆盖 OTA 大分片、log_query 快照流(hex 128B)等长报文
- 状态机按 LEN 精确吃字节,payload 内 0x7D 不影响同步;SUM 失败/超长 → 丢帧
## 4 文件结构
```
vd960Air/
├── main.lua # 入口: 加载 config/看门狗/4G网卡/uart_app/mqtt_main
├── config.lua # 配置: MQTT 服务器/序列号/主题/UART/帧参数
├── uart_app.lua # UART1 <-> vd960DBN 帧协议(接收状态机 + 下行组帧)
├── netdrv_device.lua # 网卡: 仅 4G(官方 netdrv_4g)
├── network_watchdog.lua # 网络看门狗(官方原样)
└── mqtt/
├── mqtt_main.lua # MQTT 客户端(单路, 主题改 dld960/{sn}/srv + dev)
├── mqtt_receiver.lua # 下行 → RECV_DATA_FROM_SERVER(去掉演示前缀)
└── 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 待确认清单(等开发文档/板级验证)
- [ ] vd960DBN UART1 波特率、帧格式是否按 §3 设计稿(或用户文档另有规定)
- [ ] 设备序列号 dev_serial 来源: config.lua 写死 vs UART 握手动态下发
- [ ] MQTT 服务器地址/端口/TLS、鉴权方式(平台为准)
- [ ] 4G 特有命令清单与"不支持命令返回 code=4"是否纳入主协议文档
- [ ] 通道切换策略: 有线失效判定、4G 启用条件、回切策略
- [ ] 心跳: 4G 通道 heartbeat 间隔/内容是否与有线一致
- [ ] OTA: 4G 通道是否需要支持 Loop/DBN OTA(4G 下行分片经 UART 转发可行性)
## 7 开发计划(基于官方 demo/mqtt 骨架)
| 阶段 | 内容 | 状态 |
|------|------|------|
| P0 | 工程骨架 + 帧协议 + MQTT 单路桥接(本工程) | ✅ 骨架完成 |
| P1 | 协议适配文档(4G 章节) + vd960DBN 侧 UART1 通道 | ⏳ 待文档 |
| P2 | 板级联调: 帧校验、背压、掉线重连、看门狗 | ⏳ 待板 |
| P3 | 4G 状态上报 + 通道切换 + 脱机日志/OTA 走 4G | ⏳ 规划 |
> 基底来源: 合宙官方 Air780EPM demo/mqtt(main.lua / uart_app.lua / mqtt/* / network_watchdog / netdrv_device),MIT 许可。
@@ -0,0 +1,141 @@
--[[
@module uart_app
@summary vd960Air 串口应用功能模块: UART1 <-> vd960DBN 帧协议对接
@version 0.1
@date 2026.08.31
@usage
核心职责:
1、打开 UART1(对接 vd960DBN CH32V208 串口1 PB6/PB7);
2、上行: 接收 vd960DBN 的 JSON 帧(复用《DLD960_IoT_MQTT协议》命令面),
校验后发布 SEND_DATA_REQ 消息 → mqtt_sender publish 到 dld960/{sn}/dev;
3、下行: 订阅 RECV_DATA_FROM_SERVER 消息,将 MQTT 下行 JSON 组帧后经 UART1 下发 vd960DBN。
UART 链路帧格式(设计稿 v0.1,待与 vd960DBN 侧联调确认):
┌──────┬───────┬───────┬────────────────────┬──────┐
│ HEAD │ LEN_H │ LEN_L │ PAYLOAD │ SUM │
│ 0x7D │ 1B │ 1B │ JSON(≤2048B) │ 1B │
└──────┴───────┴───────┴────────────────────┴──────┘
- HEAD: 0x7D(与 DBN↔Loop 的 0x7F 帧区分,4G 链路独立魔数)
- LEN: uint16 大端,PAYLOAD 字节数
- SUM: uint8 累加和,覆盖 HEAD+LEN_H+LEN_L+PAYLOAD 全部字节取低 8 位
- 方向: 双向对称(vd960DBN 发同样帧格式)
状态机: IDLE → HEAD → LEN_H → LEN_L → DATA → SUM → COMPLETE
注意: 状态机按 LEN 精确吃字节,payload 内出现的 0x7D 不影响帧同步;
仅 IDLE 状态寻找 HEAD;SUM 校验失败 / 超长帧 → 回 IDLE 丢弃。
]]
-- 加载配置
local CFG = require "config"
local UART_ID = CFG.CFG_UART_ID
local MAX_LEN = CFG.CFG_FRAME_MAXLEN
-- ==================== 接收状态机 ====================
local RX_IDLE, RX_HEAD, RX_LENH, RX_LENL, RX_DATA, RX_SUM = 0, 1, 2, 3, 4, 5
local rx_state = RX_IDLE
local rx_len = 0 -- 当前帧 PAYLOAD 长度
local rx_buf = "" -- PAYLOAD 缓冲
local rx_sum = 0 -- 累加和(覆盖 HEAD 起所有字节)
local function rx_reset()
rx_state = RX_IDLE
rx_len = 0
rx_buf = ""
rx_sum = 0
end
-- 一帧完整且校验通过 → 发布到 MQTT 发送队列
local function on_frame_ok(payload)
log.info("uart_app.frame", "len", payload:len())
-- SEND_DATA_REQ(tag, topic, payload, qos)
-- 上行统一发布到 dev 主题(平台侧与有线通道无差别)
sys.publish("SEND_DATA_REQ", "vd960DBN", CFG.CFG_TOPIC_UP, payload, 1)
end
-- 逐字节喂入接收状态机
local function feed_byte(b)
if rx_state == RX_IDLE then
if b == CFG.CFG_FRAME_HEAD then
rx_state = RX_HEAD
rx_sum = b
end
elseif rx_state == RX_HEAD then
rx_len = b * 256
rx_sum = (rx_sum + b) % 256
rx_state = RX_LENH
elseif rx_state == RX_LENH then
rx_len = rx_len + b
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
elseif rx_state == RX_LENL then
-- 0 长度帧非法
if rx_len == 0 then
rx_reset()
else
rx_state = RX_DATA
end
elseif rx_state == RX_DATA then
rx_buf = rx_buf .. string.char(b)
rx_sum = (rx_sum + b) % 256
if rx_buf:len() >= rx_len then
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", "checksum fail")
end
rx_reset()
end
end
-- UART1 接收中断处理
local function read()
local s
while true do
s = uart.read(UART_ID, 1024)
if not s or s:len() == 0 then
break
end
for i = 1, s:len() do
feed_byte(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
end
-- 组帧: HEAD + LEN(2B BE) + PAYLOAD + SUM
local len = payload:len()
local frame = string.char(CFG.CFG_FRAME_HEAD)
.. string.char(math.floor(len / 256)) .. string.char(len % 256)
.. payload
local sum = CFG.CFG_FRAME_HEAD
sum = (sum + math.floor(len / 256)) % 256
sum = (sum + len % 256) % 256
for i = 1, len do
sum = (sum + string.byte(payload, i)) % 256
end
frame = frame .. string.char(sum)
uart.write(UART_ID, frame)
log.info("uart_app.down", "len", len)
end
-- ==================== 初始化 ====================
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)