diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/app_iot.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/app_iot.lua new file mode 100644 index 0000000..8f13820 --- /dev/null +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/app_iot.lua @@ -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 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/clock.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/clock.lua new file mode 100644 index 0000000..ddc70d7 --- /dev/null +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/clock.lua @@ -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 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/config.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/config.lua index 7a5fc5c..e8e6e2d 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/config.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/config.lua @@ -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 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/evt_queue.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/evt_queue.lua new file mode 100644 index 0000000..19c3be1 --- /dev/null +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/evt_queue.lua @@ -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 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/frame_parser.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/frame_parser.lua new file mode 100644 index 0000000..50504a4 --- /dev/null +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/frame_parser.lua @@ -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 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/main.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/main.lua index 00a3a29..8a73e38 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/main.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/main.lua @@ -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" diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_main.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_main.lua index 91feb1c..22e17a4 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_main.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_main.lua @@ -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") diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_receiver.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_receiver.lua index 521914b..f6371f8 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_receiver.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/mqtt/mqtt_receiver.lua @@ -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个: -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中订阅处理了本消息; +平台下行(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 - 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") - - -- 将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 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/proto_conv.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/proto_conv.lua new file mode 100644 index 0000000..6926790 --- /dev/null +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/proto_conv.lua @@ -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 diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md b/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md index 1611491..43f2a6d 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/readme.md @@ -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 → 本地) diff --git a/luatos/air780epm/module/Air780EPM/demo/vd960Air/uart_app.lua b/luatos/air780epm/module/Air780EPM/demo/vd960Air/uart_app.lua index 5ba44d3..fd845b5 100644 --- a/luatos/air780epm/module/Air780EPM/demo/vd960Air/uart_app.lua +++ b/luatos/air780epm/module/Air780EPM/demo/vd960Air/uart_app.lua @@ -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