feat(vd960DBN)+fix(DBNMQTTool): log_query 改 hex 原始字节上报 (2026-08-18)

背景: MQTT 快照流实测 MQTTSerialize_publish failed — JSON 化快照记录 ~810B/条 超 800B 发送缓冲
方案(用户拍板): 对齐 BLE 通道, 原始字节 hex 上报

固件 (V4.3):
- offlog.c/h: 新增 offlog_evt_to_hex() (32B→64 hex)
- snapshot.c/h: 新增 snap_rec_to_hex() (64B→128 hex); 删 SNAP_MAX_QUERY_JSON, 恢复 count=2
- tcp_json_srv.c / iot_mqtt_srv.c: log_query 改 {"seq":N,"hex":"..."}; SEND_BUF 保持 800
- 2 条快照 hex 响应 406B < 800B

文档: TCP JSON V1.03 / MQTT V1.07 §4.17 records 改 hex + 解析表引用 BLE §6.4/§7

工具: parse_offlog_hex/parse_snap_hex/offlog_payload_desc + hex 展示; 验证: gcc 9 断言 + 工具解析全过 + offscreen UI
This commit is contained in:
wangfq
2026-08-18 14:04:26 +08:00
parent 5e9b188c6a
commit 5a1893cd1c
15 changed files with 331 additions and 180 deletions
+90 -1
View File
@@ -8,6 +8,7 @@ DLD960 IoT MQTT 协议定义
import json
import time
import struct
from typing import Optional, Any
from dataclasses import dataclass, field, asdict
@@ -250,7 +251,7 @@ def data_report_config(sensor_type: int = 12, enable: bool = True,
def data_log_query(start_seq: int = 1, count: int = 4, stream: str = STREAM_EVENT) -> dict:
"""V1.07 log_query 分页拉取脱机日志
stream=event: 事件流, count 上限 4 (设备侧超限按 4 处理)
stream=snapshot: 快照流, count 上限 2 (64B×2 记录); 显式传 stream 字段
stream=snapshot: 快照流, count 上限 2 (hex 原始字节上报); 显式传 stream 字段
"""
d: dict[str, Any] = {
"start_seq": max(1, int(start_seq)),
@@ -282,3 +283,91 @@ def parse_topic_dev_serial(topic: str) -> Optional[str]:
if len(parts) >= 2 and parts[0] == "dld960":
return parts[1]
return None
# ============================================================
# 脱机日志 hex 解析 (V1.07: log_query 响应 records[].hex 原始字节)
# 字段表与《DLD960 BLE 协议》§6.4 (SnapRec) / §7 (OfflogEvt) 一致
# ============================================================
# OfflogEvt 事件类型 (BLE 协议 §7 事件类型表)
OFFLOG_TYPE_NAMES = {
0x01: "boot", 0x10: "iot_connect", 0x11: "iot_ready", 0x12: "iot_disconnect",
0x13: "iot_reconn", 0x30: "evt_retry", 0x31: "evt_giveup", 0x40: "coil",
0x50: "time_anchor", 0x70: "log_clear",
}
_OFFLOG_SUB_NAMES = {1: "car_enter", 2: "car_leave", 3: "loop_cut", 4: "loop_restore"}
_OFFLOG_DISC_REASONS = {1: "断开", 2: "超时", 3: "CONNACK拒绝", 4: "连接超时"}
def parse_offlog_hex(h: str) -> dict:
"""OfflogEvt 32B hex → dict (magic/type/seq/ts_ms/unix_ts/boot_seq/payload)"""
b = bytes.fromhex(h)
if len(b) != 32:
return {"error": f"bad len {len(b)}"}
magic, etype, elen, flags, seq, ts_ms, unix_ts, boot_seq, rsvd, payload = \
struct.unpack('<BBBBIIIHH12s', b)
return {"magic": magic, "type": etype, "len": elen, "flags": flags, "seq": seq,
"ts_ms": ts_ms, "unix_ts": unix_ts, "boot_seq": boot_seq, "payload": payload}
def offlog_payload_desc(etype: int, payload: bytes) -> str:
"""事件 payload 人类可读描述 (BLE 协议 §7 payload 定义, 大端)"""
if etype == 0x01: # boot: 复位原因寄存器
rst = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
bits = [n for n, b in [("LPWR", 31), ("WWDG", 30), ("IWDG", 29),
("SFT", 28), ("POR", 27), ("NRST", 26)] if rst & (1 << b)]
return f"rst=0x{rst:08X} ({'+'.join(bits) if bits else 'none'})"
if etype == 0x12: # iot_disconnect
r = payload[0] if payload else 0
return f"reason={r} ({_OFFLOG_DISC_REASONS.get(r, '?')})"
if etype == 0x13: # iot_reconn
return f"backoff_ms={int.from_bytes(payload[0:4], 'big')}" if len(payload) >= 4 else ""
if etype == 0x30: # evt_retry
mid = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
retry = payload[4] if len(payload) >= 5 else 0
return f"msg_id={mid} retry={retry}"
if etype == 0x31: # evt_giveup
mid = int.from_bytes(payload[0:4], 'big') if len(payload) >= 4 else 0
return f"msg_id={mid}"
if etype == 0x40: # coil
sub = _OFFLOG_SUB_NAMES.get(payload[0], payload[0]) if payload else "?"
ch = payload[1] if len(payload) >= 2 else 0
val = int.from_bytes(payload[2:6], 'big') if len(payload) >= 6 else 0
return f"sub={sub} ch={ch} value={val}"
if etype in (0x10, 0x11, 0x50, 0x70): # 无 payload
return ""
return payload.hex() if payload else ""
def parse_snap_hex(h: str) -> dict:
"""SnapRec 64B hex → dict (seq/boot_seq/ts_ms/coil_count/channels[])"""
b = bytes.fromhex(h)
if len(b) != 64:
return {"error": f"bad len {len(b)}"}
magic, slen, flags, rsvd, seq, ts_ms, boot_seq, rsvd2, coils = \
struct.unpack('<BBBBIIHH48s', b)
coil_count = min(slen // 12, 4)
channels = []
for c in range(coil_count):
p = coils[c * 12:(c + 1) * 12]
cfg, cond = p[0], p[1]
freq = p[2] | (p[3] << 8) | (p[4] << 16)
var = p[5] | (p[6] << 8) | (p[7] << 16)
if var & 0x800000:
var -= 0x1000000
misc = int.from_bytes(p[8:12], 'little')
channels.append({
"ch": c + 1,
"freq_level": ("high", "mid_high", "mid_low", "low")[(cfg >> 6) & 3],
"direction": (cfg >> 5) & 1,
"freq_type": (cfg >> 4) & 1,
"sensitivity": cfg & 0x0F,
"condition": (cond >> 4) & 0x0F,
"loop_ok": not ((cond >> 3) & 1),
"has_car": bool((cond >> 2) & 1),
"misc_type": ("time", "cut_count", "flow_count", "relay_count")[cond & 3],
"freq": freq, "variation": var, "misc": misc,
})
return {"seq": seq, "boot_seq": boot_seq, "ts_ms": ts_ms,
"coil_count": coil_count, "channels": channels}
+20
View File
@@ -6,6 +6,26 @@
---
## 2026-08-18 — log_query 响应改 hex 原始字节解析展示(协议 V1.07 修订)
### 背景
MQTT 快照流实测 `MQTTSerialize_publish failed`——JSON 化快照记录超 800B 发送缓冲。协议修订为**原始字节 hex 上报**(与 BLE 通道同语义),工具同步改为 hex 解析。
### 变更
- **protocol.py**:新增 `parse_offlog_hex`OfflogEvt 32B`<BBBBIIIHH12s`)、`parse_snap_hex`SnapRec 64B`<BBBBIIHH48s`)、`offlog_payload_desc`boot 复位位/coil/evt_retry/evt_giveup/iot_disconnect/reconn)、`OFFLOG_TYPE_NAMES`——字段表与 BLE 协议 §6.4/§7 一致
- `data_log_query` snapshot count 恢复 **2**hex 体积可控)
- **main.py**`_apply_log_query_data` 解析 `records[].hex`hex 长度 128=快照 / 64=事件);事件流展示 type 描述 + payload 可读化,快照流逐通道字段
### 验证
- 构造事件/快照二进制→hex→解析回断言全过(POR/SFT 复位位、coil payload、evt_retry、快照 4ch、负 variation 符号扩展)
- 2 条快照 hex 响应 payload = 406B < 800B
- offscreen UI:事件流(上电/复位、线圈事件、event 重发)+ 快照流(4ch/2ch)展示 + 翻页正常
---
## 2026-08-18 — 脱机日志翻页:上一页/下一页 + 自动填充起始序号
### 变更
+31 -25
View File
@@ -34,6 +34,7 @@ from dbn_mqtt_tool.protocol import (
CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR,
STREAM_EVENT, STREAM_SNAPSHOT,
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, LOG_EVENT_TYPE_DESC, SNAP_MISC_TYPE_DESC,
OFFLOG_TYPE_NAMES, parse_offlog_hex, offlog_payload_desc, parse_snap_hex,
data_ssc_net_set, data_iot_net_set, data_iot_topic_set,
data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear,
build_request, next_msg_id,
@@ -1219,43 +1220,48 @@ class MainWindow(QMainWindow):
def _apply_log_query_data(self, data: dict):
records = data.get("records", [])
start_seq = data.get("start_seq", 0)
# 快照记录带 coil_count/channels 字段, 事件记录带 type 字段
is_snap = bool(records) and ("coil_count" in records[0] or "channels" in records[0])
# V1.07: 记录为 {"seq":N,"hex":"..."} 原始字节; hex 长度 128=快照(64B) / 64=事件(32B)
is_snap = bool(records) and len(records[0].get("hex", "")) == 128
stream_name = "传感快照" if is_snap else "事件日志"
lines = [f"log_query[{stream_name}] start_seq={start_seq} → 返回 {len(records)} 条:"]
for rec in records:
ts_ms = rec.get("ts_ms", 0)
hex_str = rec.get("hex", "")
if is_snap:
coil_count = rec.get("coil_count", 0)
s = parse_snap_hex(hex_str)
if "error" in s:
lines.append(f" seq={rec.get('seq', 0)} 解析失败: {s['error']}")
continue
lines.append(
f" seq={rec.get('seq', 0)} boot={rec.get('boot_seq', 0)} "
f"boot+{ts_ms}ms ({coil_count}ch)"
f" seq={s['seq']} boot={s['boot_seq']} "
f"boot+{s['ts_ms']}ms ({s['coil_count']}ch)"
)
for ch in rec.get("channels", []):
mt = ch.get("misc_type", "?")
for ch in s["channels"]:
mt = ch["misc_type"]
misc_desc = SNAP_MISC_TYPE_DESC.get(mt, mt)
lines.append(
f" ch{ch.get('ch', 0)}: {ch.get('freq_level', '?')} "
f"dir={ch.get('direction', 0)} ftype={ch.get('freq_type', 0)} "
f"sens={ch.get('sensitivity', 0)} cond={ch.get('condition', 0)} "
f"loop={'OK' if ch.get('loop_ok') else ''} "
f"car={'' if ch.get('has_car') else ''} "
f"freq={ch.get('freq', 0)}Hz Δ={ch.get('variation', 0)} "
f"[{misc_desc}] misc={ch.get('misc', 0)}"
f" ch{ch['ch']}: {ch['freq_level']} "
f"dir={ch['direction']} ftype={ch['freq_type']} "
f"sens={ch['sensitivity']} cond={ch['condition']} "
f"loop={'OK' if ch['loop_ok'] else ''} "
f"car={'' if ch['has_car'] else ''} "
f"freq={ch['freq']}Hz Δ={ch['variation']} "
f"[{misc_desc}] misc={ch['misc']}"
)
else:
rtype = rec.get("type", "?")
desc = LOG_EVENT_TYPE_DESC.get(rtype, rtype)
unix_ts = rec.get("unix_ts", 0)
if unix_ts:
ts_str = datetime.fromtimestamp(unix_ts).strftime("%Y-%m-%d %H:%M:%S")
e = parse_offlog_hex(hex_str)
if "error" in e:
lines.append(f" seq={rec.get('seq', 0)} 解析失败: {e['error']}")
continue
tname = OFFLOG_TYPE_NAMES.get(e["type"], f"0x{e['type']:02x}")
desc = LOG_EVENT_TYPE_DESC.get(tname, tname)
if e["unix_ts"]:
ts_str = datetime.fromtimestamp(e["unix_ts"]).strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = f"boot+{ts_ms}ms (未同步)"
rdata = rec.get("data")
data_str = json.dumps(rdata, ensure_ascii=False) if rdata else ""
ts_str = f"boot+{e['ts_ms']}ms (未同步)"
pdesc = offlog_payload_desc(e["type"], e["payload"])
lines.append(
f" seq={rec.get('seq', 0)} boot={rec.get('boot_seq', 0)} "
f"{ts_str} [{desc}] {data_str}"
f" seq={e['seq']} boot={e['boot_seq']} "
f"{ts_str} [{desc}] {pdesc}"
)
if not records:
lines.append(" (无记录 / 起始序号越界, 可先用 log_stat 确认范围)")
+13 -87
View File
@@ -714,8 +714,9 @@ dld960/{dev_serial}/{direction}
## 4.17 分页拉取脱机日志 `log_query`
> Topic: `dld960/{sn}/srv`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4**(受 MQTT 发布 ≤500B 限制,MSS=576 教训)/ 快照流 **2**64B×2 记录)。
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),平台按《DLD960 BLE 协议》字段表解析。
**请求:**
@@ -738,7 +739,7 @@ dld960/{dev_serial}/{direction}
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 datastream=event**
**响应 data**
```json
{
@@ -746,11 +747,7 @@ dld960/{dev_serial}/{direction}
"records": [
{
"seq": 1330,
"boot_seq": 2,
"ts_ms": 456789,
"unix_ts": 1784768575,
"type": "iot_ready",
"data": null
"hex": "a53200000200000001000000..."
}
]
}
@@ -758,88 +755,17 @@ dld960/{dev_serial}/{direction}
| 字段 | 类型 | 说明 |
|------|------|------|
| `seq` | uint32 | 全局序号 |
| `boot_seq` | uint16 | 所属启动段 |
| `ts_ms` | uint32 | boot 内相对时间(ms,断电归零) |
| `unix_ts` | uint32 | 已同步 Unix 秒;**0 = 未同步**(回算见 §2.3 |
| `type` | string | 事件类型名(下表) |
| `data` | object/null | 类型相关参数 |
| `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**flash 存储字节原样,小端) |
**事件类型表**
**解析字段表(与 BLE 通道完全一致)**
| type | 含义 | data 字段 |
|------|------|-----------|
| `boot` | 上电/复位 | `{"rst": 复位原因寄存器原始值}`,位解析:bit31=IWDG(看门狗) bit30=WWDG bit29=LPWR bit26=NRST引脚 bit25=POR(真断电) bit24=软件复位 |
| `iot_connect` | MQTT TCP 连接成功 | null |
| `iot_ready` | MQTT 订阅完成 → 发 initialize | null |
| `iot_disconnect` | MQTT 断连 | `{"reason": N}`1=断开 2=超时 3=CONNACK拒绝 4=连接超时 |
| `iot_reconn` | 重连退避 | `{"backoff_ms": 5000}` |
| `evt_retry` | event_report ACK 超时重发 | `{"msg_id":5,"retry":2}` |
| `evt_giveup` | event_report 重试耗尽挂起 | `{"msg_id":5}` |
| `coil` | 线圈事件 | `{"sub":"car_enter","ch":1,"value":97}`sub: car_enter/car_leave/loop_cut/loop_restorevalue 为 50ms 单位时间量 |
| `time_anchor` | 时钟同步锚点 | null`unix_ts` 即平台下发值,严格一致) |
| `log_clear` | 日志清除(审计) | null |
| | 结构 | 字段 |
|----|------|--------|
| `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
**响应 datastream=snapshot):**
```json
{
"start_seq": 12345,
"records": [
{
"seq": 12345,
"boot_seq": 2,
"ts_ms": 456789,
"coil_count": 4,
"channels": [
{
"ch": 1,
"freq_level": "high",
"direction": 0,
"freq_type": 1,
"sensitivity": 2,
"condition": 0,
"loop_ok": true,
"has_car": false,
"misc_type": "time",
"freq": 69418,
"variation": 7,
"misc": 0
}
]
}
]
}
```
快照记录为 64B 定长原始结构 `SnapRec`(与 0xC0 线上线圈单元逐字节一致,见《DLD960Loop 串口通信协议》§3.07),JSON 化后字段:
| 记录字段 | 类型 | 说明 |
|----------|------|------|
| `seq` | uint32 | 全局序号(跨 boot 递增) |
| `boot_seq` | uint16 | 所属启动段 |
| `ts_ms` | uint32 | boot 内相对时间(ms,采集时刻) |
| `coil_count` | uint8 | 本记录线圈数(SnapRec.len/121~4 |
| `channels` | array | 线圈传感单元(每路 12B,与 0xC0 线上格式一致) |
`channels[]` 单元字段(与 0xC0 传感单元一致):
| 字段 | 类型 | 说明 |
|------|------|------|
| `ch` | uint8 | 通道号 1~4 |
| `freq_level` | string | 频率档位:`"high"`(33nF) / `"mid_high"`(43nF) / `"mid_low"`(66nF) / `"low"`(76nF) |
| `direction` | uint8 | 0=触发模式 1=方向判别 |
| `freq_type` | uint8 | 0=初始频率 1=当前实时频率 |
| `sensitivity` | uint8 | 灵敏度等级(cfg 低四位) |
| `condition` | uint8 | 环境评估值(cond 高四位,值越大干扰越大) |
| `loop_ok` | bool | 线圈正常(SnapRec loop_state bit0=正常→true1=断开→false |
| `has_car` | bool | 有车(SnapRec car_state bit1=有车) |
| `misc_type` | string | `"time"` / `"cut_count"` / `"flow_count"` / `"relay_count"`(00 时间量 / 01 线圈断开次数 / 10 车流量 / 11 继电器输出次数) |
| `freq` | uint32 | 线圈频率(Hz3B LE 无符号) |
| `variation` | int32 | 变化量(3B LE 有符号补码,`Origin CAPVD`;正=车/裕量,负=反向漂移) |
| `misc` | uint32 | 杂项值(misc_type=time 时 = 通过时间/车间距,**50ms 单位**) |
> 绝对时间回算同事件流:用事件流 `time_anchor`boot_seq ↔ unix_ts 映射)+ 本记录 `boot_seq`/`ts_ms`;未同步段仅相对时间。
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算(见 §2.3)。
---
@@ -1127,5 +1053,5 @@ dld960/{dev_serial}/{direction}
| V1.04 | 2026-07-15 | `event_report` 增加**平台必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、平台去重与先落库后应答要求 | wangfq |
| V1.05 | 2026-07-15 | 增加**设备时钟同步**(§2.3,方案B):设备无 RTC,`initialize` 上线后平台经 `report_config` 命令下发 Unix `ts`,设备据此校准,之后上行 `ts` 为真实 Unix 时间;校准前为上电秒数 | wangfq |
| V1.06 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);§2.3 补充日志双时间戳语义(`ts_ms` 相对 + `unix_ts` 已同步,0=未同步,锚点回算规则) | wangfq |
| V1.07 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤2SnapRec 64B 原始结构 JSON 化)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
| V1.07 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤14 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
+13 -57
View File
@@ -386,8 +386,9 @@ Client Device (DLD960)
## 4.17 分页拉取脱机日志 `log_query`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4**(受响应帧 ≤800B 限制)/ 快照流 **2**64B×2 记录)。
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),上位机按《DLD960 BLE 协议》字段表解析。
**请求:**
@@ -401,70 +402,25 @@ Client Device (DLD960)
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 datastream=event**
**响应 data**
```json
{"start_seq":1330,"records":[{"seq":1330,"boot_seq":2,"ts_ms":456789,"unix_ts":1784768575,"type":"iot_ready","data":null}]}
{"start_seq":1330,"records":[{"seq":1330,"hex":"a53200000200000001000000..."}]}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| `seq` | uint32 | 全局序号 |
| `boot_seq` | uint16 | 所属启动段 |
| `ts_ms` | uint32 | boot 内相对时间(ms,断电归零) |
| `unix_ts` | uint32 | 已同步 Unix 秒;**0 = 未同步**(回算见 §2.3 |
| `type` | string | 事件类型名(下表) |
| `data` | object/null | 类型相关参数 |
| `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**flash 存储字节原样,小端) |
**事件类型表**
**解析字段表(与 BLE 通道完全一致)**
| type | 含义 | data 字段 |
|------|------|-----------|
| `boot` | 上电/复位 | `{"rst": 复位原因寄存器原始值}`,位解析:bit31=IWDG(看门狗) bit30=WWDG bit29=LPWR bit26=NRST引脚 bit25=POR(真断电) bit24=软件复位 |
| `iot_connect` | MQTT TCP 连接成功 | null |
| `iot_ready` | MQTT 订阅完成 → 发 initialize | null |
| `iot_disconnect` | MQTT 断连 | `{"reason": N}`1=断开 2=超时 3=CONNACK拒绝 4=连接超时 |
| `iot_reconn` | 重连退避 | `{"backoff_ms": 5000}` |
| `evt_retry` | event_report ACK 超时重发 | `{"msg_id":5,"retry":2}` |
| `evt_giveup` | event_report 重试耗尽挂起 | `{"msg_id":5}` |
| `coil` | 线圈事件 | `{"sub":"car_enter","ch":1,"value":97}`sub: car_enter/car_leave/loop_cut/loop_restorevalue 为 50ms 单位时间量 |
| `time_anchor` | 时钟同步锚点 | null`unix_ts` 即平台下发值,严格一致) |
| `log_clear` | 日志清除(审计) | null |
| | 结构 | 字段 |
|----|------|--------|
| `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
**响应 datastream=snapshot):**
```json
{"start_seq":12345,"records":[{"seq":12345,"boot_seq":2,"ts_ms":456789,"coil_count":4,"channels":[{"ch":1,"freq_level":"high","direction":0,"freq_type":1,"sensitivity":2,"condition":0,"loop_ok":true,"has_car":false,"misc_type":"time","freq":69418,"variation":7,"misc":0}]}]}
```
快照记录为 64B 定长原始结构 `SnapRec`(与 0xC0 线上线圈单元逐字节一致,见《DLD960Loop 串口通信协议》§3.07),JSON 化后字段:
| 记录字段 | 类型 | 说明 |
|----------|------|------|
| `seq` | uint32 | 全局序号(跨 boot 递增) |
| `boot_seq` | uint16 | 所属启动段 |
| `ts_ms` | uint32 | boot 内相对时间(ms,采集时刻) |
| `coil_count` | uint8 | 本记录线圈数(SnapRec.len/121~4 |
| `channels` | array | 线圈传感单元(每路 12B,与 0xC0 线上格式一致) |
`channels[]` 单元字段(与 0xC0 传感单元一致):
| 字段 | 类型 | 说明 |
|------|------|------|
| `ch` | uint8 | 通道号 1~4 |
| `freq_level` | string | 频率档位:`"high"`(33nF) / `"mid_high"`(43nF) / `"mid_low"`(66nF) / `"low"`(76nF) |
| `direction` | uint8 | 0=触发模式 1=方向判别 |
| `freq_type` | uint8 | 0=初始频率 1=当前实时频率 |
| `sensitivity` | uint8 | 灵敏度等级(cfg 低四位) |
| `condition` | uint8 | 环境评估值(cond 高四位,值越大干扰越大) |
| `loop_ok` | bool | 线圈正常(SnapRec loop_state bit0=正常→true1=断开→false |
| `has_car` | bool | 有车(SnapRec car_state bit1=有车) |
| `misc_type` | string | `"time"` / `"cut_count"` / `"flow_count"` / `"relay_count"`(00 时间量 / 01 线圈断开次数 / 10 车流量 / 11 继电器输出次数) |
| `freq` | uint32 | 线圈频率(Hz3B LE 无符号) |
| `variation` | int32 | 变化量(3B LE 有符号补码,`Origin CAPVD`;正=车/裕量,负=反向漂移) |
| `misc` | uint32 | 杂项值(misc_type=time 时 = 通过时间/车间距,**50ms 单位**) |
> 绝对时间回算同事件流:用事件流 `time_anchor`boot_seq ↔ unix_ts 映射)+ 本记录 `boot_seq`/`ts_ms`;未同步段仅相对时间。
> 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算。
---
@@ -605,4 +561,4 @@ Client Device (DLD960)
| V1.00 | 2026-06-22 | 初始版本,基于串口协议 V1.01 | wangfq |
| V1.01 | 2026-07-15 | `event_report` 增加**客户端必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、客户端去重与先落库后应答要求(与 MQTT 协议 V1.04 对称) | wangfq |
| V1.02 | 2026-08-04 | 增加**脱机事件日志**命令:`log_stat`(统计/分页定位)、`log_query`(按全局序号分页,count≤4)、`log_clear`(清除+审计留痕);事件类型表与 MQTT 协议 V1.06 对齐 | wangfq |
| V1.03 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤2SnapRec 64B 原始结构 JSON 化)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
| V1.03 | 2026-08-18 | `log_stat` / `log_query` / `log_clear` 增加**快照流**支持(`stream=snapshot`,与 BLE 0x28/0x29/0x2A 同语义):快照统计 capacity 随芯片动态(48064~449472)、快照分页 count≤14 通道记录 JSON ~810B 超发送缓冲,实测修正;BLE 原始通道仍 ≤2)、快照清除审计留痕;`capacity`/`count` 类型修正为 uint32W25Q256 事件流 130944 超 16bit | wangfq |
@@ -22,7 +22,7 @@
#define IOT_MQTT_RECONNECT_MAX_MS 60000 // 最大重连间隔 (指数退避上限)
#define IOT_MQTT_HEARTBEAT_MS 60000 // 心跳上报周期 (60s)
#define IOT_MQTT_RECV_BUF_LEN 1024 // 接收缓冲区 (单次交互 ≤1024)
#define IOT_MQTT_SEND_BUF_LEN 800 // 发送缓冲区 (2026-08-17: 1024→800, loop_data~604B+头~30B<800, 省 224B .bss)
#define IOT_MQTT_SEND_BUF_LEN 800 // 发送缓冲区 (2026-08-18: hex 上报方案, 快照 2 条 128hex×2+头 ~420B; loop_data ~604B; 不扩容)
#define IOT_MQTT_TOPIC_MAX_LEN 128 // Topic 最大长度
/*===========================================================================
@@ -162,7 +162,8 @@ void offlog_clear(void); /* 清空事件区 (清空
uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */
uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */
const char *offlog_type_str(uint8_t type); /* 事件类型数字 → 协议字符串名 */
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len); /* 记录 → JSON 对象 */
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len); /* 记录 → JSON 对象 (旧方案, 保留) */
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len); /* 32B → 64 hex (TCP/MQTT log_query, V1.03/V1.07) */
#define OFFLOG_MAX_QUERY_RECORDS 4 /* log_query 分页上限 (MQTT ≤500B 发布限制) */
#endif /* _OFFLOG_H__ */
@@ -115,7 +115,7 @@ typedef struct {
Loop 150ms, publish_sensor flush, 8 1.2s ;
CH32V208 SRAM : 64B × 8 = 512B, SPI_FLASH_BUF 4KB */
#define SNAP_RAM_DEPTH 8
#define SNAP_MAX_QUERY_RECORDS 2 /* 快照 QUERY 分页上限: (132-2)/64 */
#define SNAP_MAX_QUERY_RECORDS 2 /* QUERY 分页上限: BLE 原始 64B×2 (132B 缓冲); TCP/MQTT hex 上报 64B->128 字符×2 亦安全 (2026-08-18) */
/*===========================================================================
@@ -133,6 +133,7 @@ uint32_t snap_seq_last(void); /* 最新一条记录全局序
uint32_t snap_boot_seq(void); /* 当前 boot 序号 */
int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */
void snap_clear(void); /* 清空快照区 (审计写入事件流) */
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len); /* 记录 -> JSON (TCP/MQTT log_query) */
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len); /* 记录 -> JSON (旧方案, 保留) */
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len); /* 64B -> 128 hex (TCP/MQTT log_query, V1.03/V1.07) */
#endif /* _SNAPSHOT_H__ */
@@ -590,7 +590,7 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
} else if (strcmp(cmd_str, "log_query") == 0) {
/* 分页拉取脱机日志: 按全局序号, event/snapshot 流 (协议 V1.07) */
/* 分页拉取脱机日志: 按全局序号, event/snapshot 流, hex 原始字节上报 (协议 V1.07) */
static char resp[IOT_MQTT_SEND_BUF_LEN]; /* static: 避免大栈开销 */
char stream_buf[16];
uint32_t start_seq = 0, req_count = 0;
@@ -622,9 +622,12 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
SnapRec rec;
char hex[132]; /* 64B -> 128 hex + NUL */
if (snap_read_idx(idx + fetched, &rec) != 0) break;
if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ",");
pos += snap_rec_to_json(&rec, resp + pos, sizeof(resp) - pos);
snap_rec_to_hex(&rec, hex, sizeof(hex));
pos += snprintf(resp + pos, sizeof(resp) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)rec.seq, hex);
fetched++;
}
}
@@ -640,9 +643,12 @@ static void iot_handle_publish(const char *topic, uint8_t *payload, int payload_
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
OfflogEvt evt;
char hex[68]; /* 32B -> 64 hex + NUL */
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ",");
pos += offlog_evt_to_json(&evt, resp + pos, sizeof(resp) - pos);
offlog_evt_to_hex(&evt, hex, sizeof(hex));
pos += snprintf(resp + pos, sizeof(resp) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)evt.seq, hex);
fetched++;
}
}
@@ -489,3 +489,17 @@ void offlog_clear(void)
offlog_evt(OFFLOG_EVT_LOG_CLEAR, NULL, 0);
offlog_flush_head();
}
/* OfflogEvt -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §7 字段表解析)
Used by TCP JSON / MQTT log_query stream=event (protocol V1.03/V1.07).
Returns snprintf written length; 32B -> 64 hex chars. */
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)e;
for (i = 0; i < (int)sizeof(OfflogEvt); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
@@ -480,3 +480,17 @@ int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len)
pos += snprintf(buf + pos, buf_len - pos, "]}");
return pos;
}
/* SnapRec -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §6.4 字段表解析)
Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07).
Returns snprintf written length; 64B -> 128 hex chars. */
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)r;
for (i = 0; i < (int)sizeof(SnapRec); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
@@ -682,7 +682,7 @@ static void handle_log_stat(uint8_t socket, uint32_t msg_id, const char *json) {
json_send_ok(socket, msg_id, "log_stat", data_json);
}
/* 4.17 log_query — 分页拉取脱机日志 (event/snapshot 流; 事件≤4, 快照≤2, 协议 V1.03) */
/* 4.17 log_query — 分页拉取脱机日志 (event/snapshot 流, hex 原始字节上报, 协议 V1.03) */
static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json) {
char data_json[TCP_JSON_DATA_BUF_LEN];
char stream_buf[16];
@@ -709,9 +709,12 @@ static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json)
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
SnapRec rec;
char hex[132]; /* 64B -> 128 hex + NUL */
if (snap_read_idx(idx + fetched, &rec) != 0) break;
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ",");
pos += snap_rec_to_json(&rec, data_json + pos, sizeof(data_json) - pos);
snap_rec_to_hex(&rec, hex, sizeof(hex));
pos += snprintf(data_json + pos, sizeof(data_json) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)rec.seq, hex);
fetched++;
}
}
@@ -727,9 +730,12 @@ static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json)
uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) {
OfflogEvt evt;
char hex[68]; /* 32B -> 64 hex + NUL */
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ",");
pos += offlog_evt_to_json(&evt, data_json + pos, sizeof(data_json) - pos);
offlog_evt_to_hex(&evt, hex, sizeof(hex));
pos += snprintf(data_json + pos, sizeof(data_json) - pos,
"{\"seq\":%lu,\"hex\":\"%s\"}", (unsigned long)evt.seq, hex);
fetched++;
}
}
+36
View File
@@ -4,6 +4,41 @@
>
> 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接
## 2026-08-18 — log_query 改为 hex 原始字节上报(协议 V1.03/V1.07 修订)
### 背景
板级实测:MQTT 快照流 `log_query(stream=snapshot)` `fetched=2`**`MQTTSerialize_publish failed`**——JSON 化快照记录(4ch×12 字段 ≈ 810B/条)超 `IOT_MQTT_SEND_BUF_LEN=800B`,事件流 4 条 ~600B 可发,快照 1 条都发不出。
### 方案(用户拍板):对齐 BLE 通道——原始字节 hex 上报
- 事件记录 **OfflogEvt 32B → 64 hex 字符**;快照记录 **SnapRec 64B → 128 hex 字符**flash 存储字节原样小端)
- 上位机按《DLD960 BLE 协议》§6.4/§7 字段表解析,与 BLE 通道同语义、解析逻辑复用
- 体积实测:**2 条快照 hex 响应 406B < 800B**——无需扩容缓冲、无需砍 count
### 变更
| 文件 | 变更 |
|------|------|
| `offlog.c/h` | 新增 `offlog_evt_to_hex()`32B→64 hex |
| `snapshot.c/h` | 新增 `snap_rec_to_hex()`64B→128 hex);删 `SNAP_MAX_QUERY_JSON` 宏,恢复 `SNAP_MAX_QUERY_RECORDS=2` 全通道统一 |
| `tcp_json_srv.c` | `log_query` event/snapshot 分支改 `{"seq":N,"hex":"..."}` 上报 |
| `iot_mqtt_srv.c` | 同上;`IOT_MQTT_SEND_BUF_LEN` 保持 800(hex 方案体积可控,此前 1024 变更撤销) |
| 协议文档 | TCP JSON V1.03 / MQTT V1.07 §4.17records 改 hex + 解析表引用 BLE 字段表;快照 count 恢复 2 |
### 验证
- gcc 单测 `tests/test_snap_to_hex.c`**9 断言全过**(128 hex、全小写、字段字节序、缓冲截断安全)
- 工具 hex 解析器:boot(POR/SFT)/coil/evt_retry payload + 快照 4ch 解析全过;2 条快照 hex 响应 406B
- offscreen UI:事件流/快照流 hex 展示 + 翻页正常
- 保留 `offlog_evt_to_json`/`snap_rec_to_json`(旧方案,未来可能用于调试导出)
### 遗留
- 待板级:MRS 编译 + 真机 MQTT/TCP `log_query(stream=snapshot)` 2 条拉取
---
## 2026-08-18 — MQTT 命令集补齐:ssc_net_query / iot_net_query / iot_topic_query
### 背景
@@ -1335,6 +1370,7 @@ TCP 超时要等 ~2 分钟才触发 `SINT_STAT_TIM_OUT`,
| 版本 | 时间 | 说明 |
|------|------|------|
| V4.3 | 2026-08-18 | log_query 改 hex 原始字节上报 (协议 V1.03/V1.07 修订): offlog_evt_to_hex/snap_rec_to_hex, 2 条快照 406B<800B, 快照 count 恢复 2 |
| V4.2 | 2026-08-18 | MQTT 命令集补齐: ssc_net_query/iot_net_query/iot_topic_query 只读组包 (协议 V1.02 对齐), 工具禁用未实现按钮 |
| V4.1 | 2026-08-18 | TCP/MQTT 脱机日志快照流: log_stat/log_query/log_clear 支持 stream=snapshot (协议 V1.03/V1.07), snap_rec_to_json 序列化, 事件流 count=0 按上限 |
| V4.0 | 2026-08-10 | BLE 脱机日志接口: OFFLOG_STAT/QUERY/CLEAR (0x25/0x26/0x27), 32B OfflogEvt 二进制直传, 缓冲扩容 132B, 隔离测试 7 例 + 协议文档 V1.00 |
BIN
View File
Binary file not shown.
+76
View File
@@ -0,0 +1,76 @@
#include <stdio.h>
#include <string.h>
#include <stdint.h>
/* mock SnapRec (与 snapshot.h 一致, 64B) */
typedef struct {
uint8_t magic;
uint8_t len;
uint8_t flags;
uint8_t rsvd;
uint32_t seq;
uint32_t ts_ms;
uint16_t boot_seq;
uint16_t rsvd2;
uint8_t coils[48];
} SnapRec;
#define SNAP_COILS_MAX 4
#define SNAP_COIL_BYTES 12
int snap_rec_to_hex(const SnapRec *r, char *buf, int buf_len)
{
int i, pos = 0;
const uint8_t *p = (const uint8_t *)r;
for (i = 0; i < (int)sizeof(SnapRec); i++) {
if (pos >= buf_len - 3) break;
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
}
return pos;
}
#define CHECK(cond) do { if (!(cond)) { printf("FAIL %d: %s\n", __LINE__, #cond); fails++; } else { passes++; } } while (0)
static int passes = 0, fails = 0;
int main(void)
{
char buf[256];
SnapRec r;
/* 用例1: 与 test_snap_to_json case2 相同数据, 验证 hex 与 JSON 语义一致
ch1: cfg=0xE8(11 1 0 0000: ///0) cond=0xFF(cond15///relay)
freq=0x00C383=50051, variation=-10, misc=128 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6; r.len = 48; r.seq = 999; r.ts_ms = 1234; r.boot_seq = 3;
r.coils[0] = 0xE8; r.coils[1] = 0xFF;
r.coils[2] = 0x83; r.coils[3] = 0xC3; r.coils[4] = 0x00; /* 50051 */
r.coils[5] = 0xF6; r.coils[6] = 0xFF; r.coils[7] = 0xFF; /* -10 */
r.coils[8] = 0x80; r.coils[9] = 0x00; r.coils[10] = 0x00; r.coils[11] = 0x00; /* 128 */
int n = snap_rec_to_hex(&r, buf, sizeof(buf));
printf("hex(%d): %s\n", n, buf);
CHECK(n == 128);
CHECK(strlen(buf) == 128);
/* 头部: magic=0xA6 len=0x30(48) flags=0 rsvd=0 seq=0xE7030000(999 LE) */
CHECK(strncmp(buf, "a6300000e7030000d204", 20) == 0);
/* ch1 12B: e8 ff 83 c3 00 f6 ff ff 80 00 00 00 */
CHECK(strncmp(buf + 32, "e8ff83c300f6ffff80000000", 24) == 0);
/* 全部小写 */
CHECK(strspn(buf, "0123456789abcdef") == 128);
/* 用例2: 空记录 len=0 */
memset(&r, 0, sizeof(r));
r.magic = 0xA6;
n = snap_rec_to_hex(&r, buf, sizeof(buf));
CHECK(n == 128);
CHECK(strncmp(buf, "a60000000000000000000000", 24) == 0);
/* 用例3: 缓冲不足截断安全 (buf_len=10 不越界) */
memset(&r, 0, sizeof(r));
n = snap_rec_to_hex(&r, buf, 10);
CHECK(n == 8);
CHECK(strlen(buf) == 8);
printf("PASS=%d FAIL=%d\n", passes, fails);
return fails ? 1 : 0;
}