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 json
import time import time
import struct
from typing import Optional, Any from typing import Optional, Any
from dataclasses import dataclass, field, asdict 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: def data_log_query(start_seq: int = 1, count: int = 4, stream: str = STREAM_EVENT) -> dict:
"""V1.07 log_query 分页拉取脱机日志 """V1.07 log_query 分页拉取脱机日志
stream=event: 事件流, count 上限 4 (设备侧超限按 4 处理) stream=event: 事件流, count 上限 4 (设备侧超限按 4 处理)
stream=snapshot: 快照流, count 上限 2 (64B×2 记录); 显式传 stream 字段 stream=snapshot: 快照流, count 上限 2 (hex 原始字节上报); 显式传 stream 字段
""" """
d: dict[str, Any] = { d: dict[str, Any] = {
"start_seq": max(1, int(start_seq)), "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": if len(parts) >= 2 and parts[0] == "dld960":
return parts[1] return parts[1]
return None 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 — 脱机日志翻页:上一页/下一页 + 自动填充起始序号 ## 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, CMD_LOG_STAT, CMD_LOG_QUERY, CMD_LOG_CLEAR,
STREAM_EVENT, STREAM_SNAPSHOT, STREAM_EVENT, STREAM_SNAPSHOT,
ERROR_MSGS, FREQ_LEVELS, OUTPUT_MODES, EVENT_TYPES, LOG_EVENT_TYPE_DESC, SNAP_MISC_TYPE_DESC, 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_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, data_pwd_verify, data_pwd_set, data_report_config, data_log_query, data_log_clear,
build_request, next_msg_id, build_request, next_msg_id,
@@ -1219,43 +1220,48 @@ class MainWindow(QMainWindow):
def _apply_log_query_data(self, data: dict): def _apply_log_query_data(self, data: dict):
records = data.get("records", []) records = data.get("records", [])
start_seq = data.get("start_seq", 0) start_seq = data.get("start_seq", 0)
# 快照记录带 coil_count/channels 字段, 事件记录带 type 字段 # V1.07: 记录为 {"seq":N,"hex":"..."} 原始字节; hex 长度 128=快照(64B) / 64=事件(32B)
is_snap = bool(records) and ("coil_count" in records[0] or "channels" in records[0]) is_snap = bool(records) and len(records[0].get("hex", "")) == 128
stream_name = "传感快照" if is_snap else "事件日志" stream_name = "传感快照" if is_snap else "事件日志"
lines = [f"log_query[{stream_name}] start_seq={start_seq} → 返回 {len(records)} 条:"] lines = [f"log_query[{stream_name}] start_seq={start_seq} → 返回 {len(records)} 条:"]
for rec in records: for rec in records:
ts_ms = rec.get("ts_ms", 0) hex_str = rec.get("hex", "")
if is_snap: 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( lines.append(
f" seq={rec.get('seq', 0)} boot={rec.get('boot_seq', 0)} " f" seq={s['seq']} boot={s['boot_seq']} "
f"boot+{ts_ms}ms ({coil_count}ch)" f"boot+{s['ts_ms']}ms ({s['coil_count']}ch)"
) )
for ch in rec.get("channels", []): for ch in s["channels"]:
mt = ch.get("misc_type", "?") mt = ch["misc_type"]
misc_desc = SNAP_MISC_TYPE_DESC.get(mt, mt) misc_desc = SNAP_MISC_TYPE_DESC.get(mt, mt)
lines.append( lines.append(
f" ch{ch.get('ch', 0)}: {ch.get('freq_level', '?')} " f" ch{ch['ch']}: {ch['freq_level']} "
f"dir={ch.get('direction', 0)} ftype={ch.get('freq_type', 0)} " f"dir={ch['direction']} ftype={ch['freq_type']} "
f"sens={ch.get('sensitivity', 0)} cond={ch.get('condition', 0)} " f"sens={ch['sensitivity']} cond={ch['condition']} "
f"loop={'OK' if ch.get('loop_ok') else ''} " f"loop={'OK' if ch['loop_ok'] else ''} "
f"car={'' if ch.get('has_car') else ''} " f"car={'' if ch['has_car'] else ''} "
f"freq={ch.get('freq', 0)}Hz Δ={ch.get('variation', 0)} " f"freq={ch['freq']}Hz Δ={ch['variation']} "
f"[{misc_desc}] misc={ch.get('misc', 0)}" f"[{misc_desc}] misc={ch['misc']}"
) )
else: else:
rtype = rec.get("type", "?") e = parse_offlog_hex(hex_str)
desc = LOG_EVENT_TYPE_DESC.get(rtype, rtype) if "error" in e:
unix_ts = rec.get("unix_ts", 0) lines.append(f" seq={rec.get('seq', 0)} 解析失败: {e['error']}")
if unix_ts: continue
ts_str = datetime.fromtimestamp(unix_ts).strftime("%Y-%m-%d %H:%M:%S") 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: else:
ts_str = f"boot+{ts_ms}ms (未同步)" ts_str = f"boot+{e['ts_ms']}ms (未同步)"
rdata = rec.get("data") pdesc = offlog_payload_desc(e["type"], e["payload"])
data_str = json.dumps(rdata, ensure_ascii=False) if rdata else ""
lines.append( lines.append(
f" seq={rec.get('seq', 0)} boot={rec.get('boot_seq', 0)} " f" seq={e['seq']} boot={e['boot_seq']} "
f"{ts_str} [{desc}] {data_str}" f"{ts_str} [{desc}] {pdesc}"
) )
if not records: if not records:
lines.append(" (无记录 / 起始序号越界, 可先用 log_stat 确认范围)") lines.append(" (无记录 / 起始序号越界, 可先用 log_stat 确认范围)")
+13 -87
View File
@@ -714,8 +714,9 @@ dld960/{dev_serial}/{direction}
## 4.17 分页拉取脱机日志 `log_query` ## 4.17 分页拉取脱机日志 `log_query`
> Topic: `dld960/{sn}/srv` > Topic: `dld960/{sn}/srv`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4**(受 MQTT 发布 ≤500B 限制,MSS=576 教训)/ 快照流 **2**64B×2 记录)。 > **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**(hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`。 > 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`。
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),平台按《DLD960 BLE 协议》字段表解析。
**请求:** **请求:**
@@ -738,7 +739,7 @@ dld960/{dev_serial}/{direction}
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` | | `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 | | `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 datastream=event** **响应 data**
```json ```json
{ {
@@ -746,11 +747,7 @@ dld960/{dev_serial}/{direction}
"records": [ "records": [
{ {
"seq": 1330, "seq": 1330,
"boot_seq": 2, "hex": "a53200000200000001000000..."
"ts_ms": 456789,
"unix_ts": 1784768575,
"type": "iot_ready",
"data": null
} }
] ]
} }
@@ -758,88 +755,17 @@ dld960/{dev_serial}/{direction}
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
| `seq` | uint32 | 全局序号 | | `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `boot_seq` | uint16 | 所属启动段 | | `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**flash 存储字节原样,小端) |
| `ts_ms` | uint32 | boot 内相对时间(ms,断电归零) |
| `unix_ts` | uint32 | 已同步 Unix 秒;**0 = 未同步**(回算见 §2.3 |
| `type` | string | 事件类型名(下表) |
| `data` | object/null | 类型相关参数 |
**事件类型表** **解析字段表(与 BLE 通道完全一致)**
| type | 含义 | data 字段 | | | 结构 | 字段 |
|------|------|-----------| |----|------|--------|
| `boot` | 上电/复位 | `{"rst": 复位原因寄存器原始值}`,位解析:bit31=IWDG(看门狗) bit30=WWDG bit29=LPWR bit26=NRST引脚 bit25=POR(真断电) bit24=软件复位 | | `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `iot_connect` | MQTT TCP 连接成功 | null | | `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
| `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 |
**响应 datastream=snapshot):** > 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算(见 §2.3)。
```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`;未同步段仅相对时间。
--- ---
@@ -1127,5 +1053,5 @@ dld960/{dev_serial}/{direction}
| V1.04 | 2026-07-15 | `event_report` 增加**平台必答**机制:应答格式(回显 `msg_id`)、设备 5s 超时重发(同 `msg_id`/`ts`,最多 3 次)、待发队列合并上报、平台去重与先落库后应答要求 | wangfq | | 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.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.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` ## 4.17 分页拉取脱机日志 `log_query`
> **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4**(受响应帧 ≤800B 限制)/ 快照流 **2**64B×2 记录)。 > **分页按全局序号,不按时间**(未同步段时间不可靠)。`count` 上限按流区分:事件流 **4** / 快照流 **2**hex 原始字节上报,体积可控)。
> 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot` > 通过 `data.stream` 区分日志流:`event`(缺省)/ `snapshot`
> **记录格式为存储原始字节的小写 hex 字符串**(与 BLE 通道直传的二进制同源同语义),上位机按《DLD960 BLE 协议》字段表解析。
**请求:** **请求:**
@@ -401,70 +402,25 @@ Client Device (DLD960)
| `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` | | `start_seq` | uint32 | 起始全局序号(含);越界(< `seq_first` 或 > `seq_last`)返回空 `records` |
| `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 | | `count` | uint8 | 拉取条数;事件流上限 **4**、快照流上限 **2**,超限按各自上限处理;0 按上限处理 |
**响应 datastream=event** **响应 data**
```json ```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 | 全局序号 | | `seq` | uint32 | 全局序号(与 hex 内 offset 4 字段一致,便于快速定位/排序) |
| `boot_seq` | uint16 | 所属启动段 | | `hex` | string | 记录原始字节的小写 hex:事件流 **OfflogEvt 32B → 64 字符**;快照流 **SnapRec 64B → 128 字符**flash 存储字节原样,小端) |
| `ts_ms` | uint32 | boot 内相对时间(ms,断电归零) |
| `unix_ts` | uint32 | 已同步 Unix 秒;**0 = 未同步**(回算见 §2.3 |
| `type` | string | 事件类型名(下表) |
| `data` | object/null | 类型相关参数 |
**事件类型表** **解析字段表(与 BLE 通道完全一致)**
| type | 含义 | data 字段 | | | 结构 | 字段 |
|------|------|-----------| |----|------|--------|
| `boot` | 上电/复位 | `{"rst": 复位原因寄存器原始值}`,位解析:bit31=IWDG(看门狗) bit30=WWDG bit29=LPWR bit26=NRST引脚 bit25=POR(真断电) bit24=软件复位 | | `event` | OfflogEvt 32B | 《DLD960 BLE 协议》§7magic(0xA5)/type/len/flags/seq/ts_ms/unix_ts/boot_seq/payload(12B),事件类型与 payload 定义同表 |
| `iot_connect` | MQTT TCP 连接成功 | null | | `snapshot` | SnapRec 64B | 《DLD960 BLE 协议》§6.4magic(0xA6)/len/flags/seq/ts_ms/boot_seq/coils(4×12B,与 0xC0 线上格式一致) |
| `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 |
**响应 datastream=snapshot):** > 时间戳语义同事件流:`unix_ts` 为已同步 Unix 秒(0=未同步),绝对时间用事件流 `time_anchor` 锚点回算。
```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`;未同步段仅相对时间。
--- ---
@@ -605,4 +561,4 @@ Client Device (DLD960)
| V1.00 | 2026-06-22 | 初始版本,基于串口协议 V1.01 | wangfq | | 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.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.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_RECONNECT_MAX_MS 60000 // 最大重连间隔 (指数退避上限)
#define IOT_MQTT_HEARTBEAT_MS 60000 // 心跳上报周期 (60s) #define IOT_MQTT_HEARTBEAT_MS 60000 // 心跳上报周期 (60s)
#define IOT_MQTT_RECV_BUF_LEN 1024 // 接收缓冲区 (单次交互 ≤1024) #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 最大长度 #define IOT_MQTT_TOPIC_MAX_LEN 128 // Topic 最大长度
/*=========================================================================== /*===========================================================================
@@ -162,7 +162,8 @@ void offlog_clear(void); /* 清空事件区 (清空
uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */ uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */
uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */ uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */
const char *offlog_type_str(uint8_t type); /* 事件类型数字 → 协议字符串名 */ 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 发布限制) */ #define OFFLOG_MAX_QUERY_RECORDS 4 /* log_query 分页上限 (MQTT ≤500B 发布限制) */
#endif /* _OFFLOG_H__ */ #endif /* _OFFLOG_H__ */
@@ -115,7 +115,7 @@ typedef struct {
Loop 150ms, publish_sensor flush, 8 1.2s ; Loop 150ms, publish_sensor flush, 8 1.2s ;
CH32V208 SRAM : 64B × 8 = 512B, SPI_FLASH_BUF 4KB */ CH32V208 SRAM : 64B × 8 = 512B, SPI_FLASH_BUF 4KB */
#define SNAP_RAM_DEPTH 8 #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 序号 */ uint32_t snap_boot_seq(void); /* 当前 boot 序号 */
int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */ int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */
void snap_clear(void); /* 清空快照区 (审计写入事件流) */ 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__ */ #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); iot_mqtt_publish(resp_topic, resp, strlen(resp), 1);
} else if (strcmp(cmd_str, "log_query") == 0) { } 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: 避免大栈开销 */ static char resp[IOT_MQTT_SEND_BUF_LEN]; /* static: 避免大栈开销 */
char stream_buf[16]; char stream_buf[16];
uint32_t start_seq = 0, req_count = 0; 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 */ uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) { while (fetched < req_count && (idx + fetched) < total) {
SnapRec rec; SnapRec rec;
char hex[132]; /* 64B -> 128 hex + NUL */
if (snap_read_idx(idx + fetched, &rec) != 0) break; if (snap_read_idx(idx + fetched, &rec) != 0) break;
if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ","); 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++; 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 */ uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) { while (fetched < req_count && (idx + fetched) < total) {
OfflogEvt evt; OfflogEvt evt;
char hex[68]; /* 32B -> 64 hex + NUL */
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break; if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
if (fetched > 0) pos += snprintf(resp + pos, sizeof(resp) - pos, ","); 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++; fetched++;
} }
} }
@@ -489,3 +489,17 @@ void offlog_clear(void)
offlog_evt(OFFLOG_EVT_LOG_CLEAR, NULL, 0); offlog_evt(OFFLOG_EVT_LOG_CLEAR, NULL, 0);
offlog_flush_head(); 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, "]}"); pos += snprintf(buf + pos, buf_len - pos, "]}");
return 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); 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) { static void handle_log_query(uint8_t socket, uint32_t msg_id, const char *json) {
char data_json[TCP_JSON_DATA_BUF_LEN]; char data_json[TCP_JSON_DATA_BUF_LEN];
char stream_buf[16]; 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 */ uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) { while (fetched < req_count && (idx + fetched) < total) {
SnapRec rec; SnapRec rec;
char hex[132]; /* 64B -> 128 hex + NUL */
if (snap_read_idx(idx + fetched, &rec) != 0) break; if (snap_read_idx(idx + fetched, &rec) != 0) break;
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ","); 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++; 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 */ uint32_t idx = start_seq - seq_first; /* 逻辑索引 = 全局序号 - seq_first */
while (fetched < req_count && (idx + fetched) < total) { while (fetched < req_count && (idx + fetched) < total) {
OfflogEvt evt; OfflogEvt evt;
char hex[68]; /* 32B -> 64 hex + NUL */
if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break; if (offlog_read_idx((uint16_t)(idx + fetched), &evt) != 0) break;
if (fetched > 0) pos += snprintf(data_json + pos, sizeof(data_json) - pos, ","); 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++; fetched++;
} }
} }
+36
View File
@@ -4,6 +4,41 @@
> >
> 项目定位: DLD960 通信板 — BLE 配网、TCP JSON 协议服务、Loop MCU 串口桥接 > 项目定位: 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 ## 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.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.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 | | 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;
}