feat(vd960DBN): BLE 读取脱机日志 — OFFLOG_STAT/QUERY/CLEAR (0x25/0x26/0x27)

蓝牙通道补齐脱机日志读取 (此前仅 MQTT V1.06 / TCP JSON V1.02 有 log_* 命令):
- 3 条 BLE 命令, 语义对齐 MQTT log_stat/log_query/log_clear
- QUERY 直接传 32B OfflogEvt 原始结构 (二进制协议, 无需 JSON), 复用
  idx = start_seq - seq_first 定位, 不新增 offlog API
- 缓冲扩容: MAX_BLE_TMP_BUF_LEN 100→132, 新增 MAX_BLE_DAT_BUF_LEN=132
  (QUERY 响应 1+4x32B=129B), clear_buf_dbn_ble_all memset 同步
- 新协议文档 docs/DLD960_BLE协议.md V1.00 (帧格式+分包+命令+记录结构)
- 隔离测试 test_ble_offlog.c 嵌入源文件 3 case 真实文本, 7 例全过;
  offlog 回归 8 例 ALL PASS
- devlog V4.0; README 协议矩阵补 BLE 行
This commit is contained in:
wangfq
2026-08-10 14:24:20 +08:00
parent 5ebd8247f0
commit d6174b9d3f
8 changed files with 576 additions and 4 deletions
+153
View File
@@ -0,0 +1,153 @@
# DLD960 BLE 通信协议(脱机事件日志)
> 版本: V1.002026-08-10,脱机日志部分)
> 适用: vd960DBNCH32V208WCH BLE 协议栈)
> 用途: 小程序/APP 经蓝牙读取设备本地 W25Q32 环形事件日志(离线取证:区分设备真复位 vs MQTT 断连重连)
---
## 1 帧格式
与既有 BLE 配置命令一致:
```
Magic | Header | Data | CheckByte
| Addr/Sub Len CMD | | Xor Sum
1 Byte | 1B 1B 1B | xx | 1B 1B
```
- `Len = len(CMD) + len(Data)`(即 `pkg[2] = 1 + data_len`
- 校验:`Xor = XOR(pkg[1..Len+2])``Sum = SUM(pkg[1..Len+2])`,覆盖 header + cmd + data(不含 magic
- 本命令族 Magic = `0x8F`MAGIC_BYTE_DBN_DEFAULT
### 分包(长响应)
响应数据单包上限 **96B**BLE_BUFF_MAX_LEN=100 4)。超过则分包,header 字节 = `(pkg_amount << 4) | pkg_seq``pkg_seq` 从 1 递增;收端按 `pkg_amount`/`pkg_seq` 重组,最后一片 `pkg_amount == pkg_seq`
脱机日志 QUERY 响应最大 130B → 2 包(96 + 34)。
---
## 2 命令码
| 命令码 | 名称 | 方向 | 说明 |
|--------|------|------|------|
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计(分页定位) |
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
> 与 MQTT V1.06 / TCP JSON V1.02 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同。
---
## 3 查询日志统计 `OFFLOG_STAT` (0x25)
**请求 data** 无(`Len=1`,仅 cmd 字节)。
**响应 data19B,全小端):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用(Flash 初始化失败) |
| 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) |
| 3 | 4 | `count` | 有效记录条数(0~8064,环形覆盖后 < capacity |
| 7 | 4 | `capacity` | 容量上限(8064 |
| 11 | 4 | `seq_first` | 逻辑首条记录全局序号(`seq_last - count + 1`count=0 时为 0 |
| 15 | 4 | `seq_last` | 最新一条记录全局序号(跨 boot 单调递增) |
示例(boot_seq=2, count=1234, capacity=8064, seq_first=100, seq_last=1333):
```
8F 00 14 25 00 02 00 D2 04 00 00 00 80 1F 00 00 00 64 00 00 00 35 05 00 00 00 XX XX
-- -- -- -- -------------------- -------------------- -------------------- -----
| | | | status count=1234 capacity=8064 seq_first=100
| | | cmd=0x25 boot_seq=2 seq_last=1333
```
---
## 4 分页拉取日志 `OFFLOG_QUERY` (0x26)
**请求 data5B):**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界(< seq_first 或 > seq_last)返回 0 条 |
| 4 | 1 | `count` | 拉取条数,**上限 4**,超限按 4;0 按 4 处理 |
**响应 data**
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `status` | `0x00`=OK`0x01`=日志未启用;`0x02`=请求帧过短 |
| 1 | 1 | `count` | 本次实际返回记录条数(0~4) |
| 2 | N×32 | 记录 | `count` 条 OfflogEvt 原始结构(小端,见 §6) |
记录顺序 = 逻辑序号升序(与 `start_seq` 一致)。越界/空日志:`count=0`
请求示例(start_seq=1330, count=4):
```
8F 00 06 26 32 05 00 00 04 XX XX
-- -- -- -- -- -- -- -- ---
| | | | | | | | count=4
| | | | +--+--+--+ start_seq=1330 (LE)
| | | cmd=0x26
| | Len=5+1=6
```
---
## 5 清除日志 `OFFLOG_CLEAR` (0x27)
**请求 data** 无。
**响应 data1B):** `status``0x00`=OK。
> ⚠ **高风险操作**:清除动作本身写入事件流(`log_clear` 审计——留痕不可清除)。设备侧应做权限控制(与 MQTT `log_clear` 同语义)。
>
> ⚠ **阻塞 ~2.8s**63 个数据扇区 SPI 擦除(~45ms/扇区),期间主循环阻塞。若设备 IoT MQTT 在线,此期间 WCHNET 无法轮询,可能导致断连重连(60s keepalive 内可恢复)。请勿高频调用。
成功后 `OFFLOG_STAT``count` 归 1(仅剩审计记录),`seq_last` 继续递增(序号不复位)。
---
## 6 记录格式(OfflogEvt32B 定长,小端)
| 偏移 | 长度 | 字段 | 说明 |
|------|------|------|------|
| 0 | 1 | `magic` | `0xA5` |
| 1 | 1 | `type` | 事件类型(下表) |
| 2 | 1 | `len` | payload 有效字节数(0~12 |
| 3 | 1 | `flags` | bit0=1 → `unix_ts` 有效 |
| 4 | 4 | `seq` | 全局序号(跨 boot 递增) |
| 8 | 4 | `ts_ms` | boot 内相对时间(ms,断电归零) |
| 12 | 4 | `unix_ts` | 已同步 Unix 秒;`flags.bit0=0` → 0/无效(未同步段仅相对时间) |
| 16 | 2 | `boot_seq` | 所属启动段 |
| 18 | 2 | `rsvd` | 保留(0 |
| 20 | 12 | `payload` | 事件参数(下表) |
### 事件类型与 payload
| type | 事件 | payload | 说明 |
|------|------|---------|------|
| `0x01` | boot | `[4]` 复位原因寄存器原始值(大端),位解析:bit31=IWDG(看门狗) bit30=WWDG bit29=LPWR bit26=NRST引脚 bit25=POR(真断电) bit24=软件复位 | 上电/复位 |
| `0x10` | iot_connect | — | MQTT TCP 连接成功 |
| `0x11` | iot_ready | — | MQTT 订阅完成 → 发 initialize(重复上线直接证据) |
| `0x12` | iot_disconnect | `[1]` reason1=断开 2=超时 3=CONNACK拒绝 4=连接超时 | MQTT 断连 |
| `0x13` | iot_reconn | `[4]` 重连退避 ms(大端) | 重连退避 |
| `0x30` | evt_retry | `[5]` msg_id(大端4) + retry(1) | event_report ACK 超时重发 |
| `0x31` | evt_giveup | `[4]` msg_id(大端) | event_report 重试耗尽挂起 |
| `0x40` | coil | `[6]` sub(1) + ch(1) + value(大端4, 50ms 单位) | 线圈事件;sub1=car_enter 2=car_leave 3=loop_cut 4=loop_restore |
| `0x50` | time_anchor | —(`unix_ts` 即平台下发值,严格一致) | 时钟同步锚点 |
| `0x70` | log_clear | — | 日志清除(审计,不可清除) |
> payload 内多字节为**大端**(与 offlog 写入侧一致),OfflogEvt 其余字段为**小端**CPU 原生序,直接 memcpy)。
---
## 7 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR |