现场: RST_REASON 打印 0x08000000, 对照 WCH ch32v20x.h 发现 offlog.h 位定义整体错位 (IWDG=bit31❌实为bit29, SFT=bit24❌实为bit28, POR=bit25❌实为bit27, LPWR=bit29❌实为bit31) 结论: 0x08000000=PORRSTF=上电/掉电复位 → 电源跌落方向 (SPI擦除电流脉冲?) 修正: - offlog.h 位定义对齐 WCH 库, 新增 OFFLOG_RST_RMVF=bit24 - peripheral_main.c RMVF 清除改 bit24 (原写 bit27=PORRSTF 只读位, 复位标志从未清除, 历史累积污染诊断) - 协议文档 boot 事件位解析同步 测试回归全过
244 lines
11 KiB
Markdown
244 lines
11 KiB
Markdown
# DLD960 BLE 通信协议(脱机事件日志 + 传感快照)
|
||
|
||
> 版本: V1.02(2026-08-12,新增传感快照 3 命令;分包上限随协商 MTU 动态)
|
||
> 适用: vd960DBN(CH32V208,WCH BLE 协议栈)
|
||
> 用途: 小程序/APP 经蓝牙读取设备本地 W25Qxx 环形日志(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形)
|
||
|
||
---
|
||
|
||
## 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)
|
||
|
||
### 分包(长响应)
|
||
|
||
响应单包数据上限**随协商 MTU 动态变化**(V1.01,修复"Too large noti 丢包"):`chunk = min(peripheralMTU - 9, 94)`(整包 = 帧头4 + dat + ckb2,须 ≤ MTU-3 且 ≤ 本地缓冲 100B)。分包时 header 字节 = `(pkg_amount << 4) | pkg_seq`,`pkg_seq` 从 1 递增;收端按 `pkg_amount`/`pkg_seq` 重组,最后一片 `pkg_amount == pkg_seq`。
|
||
|
||
| 协商 MTU | 单包最大 dat | 整包最大长度 |
|
||
|---------|-------------|-------------|
|
||
| 23(未协商) | 14 | 20 |
|
||
| 96(常见 Android) | 87 | 93 |
|
||
| 185(iOS 常见) | 94 | 100 |
|
||
|
||
脱机日志 QUERY 响应最大 130B:MTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
|
||
|
||
> 分包发送由 `performPeriodicTask`(50ms TMOS 周期)主动续传,两包间隔 ≤50ms,不依赖收包事件(2026-08-12 修复)。
|
||
|
||
---
|
||
|
||
## 2 命令码
|
||
|
||
| 命令码 | 名称 | 方向 | 说明 |
|
||
|--------|------|------|------|
|
||
| `0x25` | `OFFLOG_STAT` | APP→设备 | 查询脱机事件日志统计(分页定位) |
|
||
| `0x26` | `OFFLOG_QUERY` | APP→设备 | 按全局序号分页拉取日志记录 |
|
||
| `0x27` | `OFFLOG_CLEAR` | APP→设备 | 清除日志(审计留痕) |
|
||
| `0x28` | `SNAP_STAT` | APP→设备 | 查询传感快照统计(分页定位) |
|
||
| `0x29` | `SNAP_QUERY` | APP→设备 | 按全局序号分页拉取快照记录 |
|
||
| `0x2A` | `SNAP_CLEAR` | APP→设备 | 清除快照(审计写入事件流) |
|
||
|
||
> 与 MQTT V1.06 / TCP JSON V1.02 的 `log_stat` / `log_query` / `log_clear` 语义一致,通道不同。
|
||
|
||
---
|
||
|
||
## 3 查询日志统计 `OFFLOG_STAT` (0x25)
|
||
|
||
**请求 data:** 无(`Len=1`,仅 cmd 字节)。
|
||
|
||
**响应 data(19B,全小端):**
|
||
|
||
| 偏移 | 长度 | 字段 | 说明 |
|
||
|------|------|------|------|
|
||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=日志未启用(Flash 初始化失败) |
|
||
| 1 | 2 | `boot_seq` | 当前启动序号(每次上电 +1,区分复位段) |
|
||
| 3 | 4 | `count` | 有效记录条数(0~8064,环形覆盖后 < capacity) |
|
||
| 7 | 4 | `capacity` | 容量上限(8064) |
|
||
| 3 | 4 | `count` | 有效记录条数(0~capacity,环形覆盖后 < capacity) |
|
||
| 7 | 4 | `capacity` | 容量上限(**随存储芯片动态**:W25Q32=16256 / W25Q64=32640 / W25Q128=65408 / W25Q256=130944) |
|
||
|
||
示例(boot_seq=2, count=1234, capacity=16256, seq_first=100, seq_last=1333):
|
||
|
||
```
|
||
8F 00 14 25 00 02 00 D2 04 00 00 00 80 3F 00 00 00 64 00 00 00 35 05 00 00 00 XX XX
|
||
-- -- -- -- -------------------- -------------------- -------------------- -----
|
||
| | | | status count=1234 capacity=16256 seq_first=100
|
||
| | | | cmd=0x25 boot_seq=2 seq_last=1333
|
||
```
|
||
|
||
---
|
||
|
||
## 4 分页拉取日志 `OFFLOG_QUERY` (0x26)
|
||
|
||
**请求 data(5B):**
|
||
|
||
| 偏移 | 长度 | 字段 | 说明 |
|
||
|------|------|------|------|
|
||
| 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:** 无。
|
||
|
||
**响应 data(1B):** `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 传感快照日志(0x28/0x29/0x2A,V1.02 新增)
|
||
|
||
**用途**:0xC0 传感帧(4 线圈波形)按上报节奏落盘,与事件日志**分区独立**(快照区 = 总容量 − 固定区 576KB − 事件区)。断网期间波形照常记录,可离线回放。
|
||
|
||
**线程语义(设备侧)**:采集路径(USART2 中断)只做打包 + RAM 暂存(8 深,满丢新),主循环每轮 `snap_flush()` 落盘——QUERY 读到的是已落盘记录。
|
||
|
||
### 6.1 查询快照统计 `SNAP_STAT` (0x28)
|
||
|
||
**请求 data:** 无(`Len=1`)。
|
||
|
||
**响应 data(19B,全小端):** 与 §3 `OFFLOG_STAT` 布局相同:
|
||
|
||
| 偏移 | 长度 | 字段 | 说明 |
|
||
|------|------|------|------|
|
||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=快照未启用 |
|
||
| 1 | 2 | `boot_seq` | 当前启动序号 |
|
||
| 3 | 4 | `count` | 有效记录条数(0~capacity) |
|
||
| 7 | 4 | `capacity` | 容量上限(**随芯片动态**:W25Q32=48064 / W25Q64=105408 / W25Q128=220096 / W25Q256=449472) |
|
||
| 11 | 4 | `seq_first` | 最早一条全局序号(count=0 时=0) |
|
||
| 15 | 4 | `seq_last` | 最新一条全局序号 |
|
||
|
||
### 6.2 分页拉取快照 `SNAP_QUERY` (0x29)
|
||
|
||
**请求 data(5B):**
|
||
|
||
| 偏移 | 长度 | 字段 | 说明 |
|
||
|------|------|------|------|
|
||
| 0 | 4 | `start_seq` | 起始全局序号(小端,含);越界返回 0 条 |
|
||
| 4 | 1 | `count` | 拉取条数,**上限 2**(64B×2+2=130B ≤ 单包缓冲),超限按 2;0 按 2 |
|
||
|
||
**响应 data:**
|
||
|
||
| 偏移 | 长度 | 字段 | 说明 |
|
||
|------|------|------|------|
|
||
| 0 | 1 | `status` | `0x00`=OK;`0x01`=快照未启用;`0x02`=请求帧过短 |
|
||
| 1 | 1 | `count` | 本次实际返回记录条数(0~2) |
|
||
| 2 | N×64 | 记录 | `count` 条 SnapRec 原始结构(小端,见 §6.4) |
|
||
|
||
记录顺序 = 逻辑序号升序。QUERY 响应最大 130B:MTU=96 时 2 包(87+43 dat);MTU≥103 时 2 包(94+36 dat)。
|
||
|
||
### 6.3 清除快照 `SNAP_CLEAR` (0x2A)
|
||
|
||
**请求 data:** 无。**响应 data(1B):** `status`:`0x00`=OK。
|
||
|
||
> ⚠ **高风险操作**:清除动作本身写入**事件流** `log_clear` 审计(payload[0]=2=快照流,不可清除)。
|
||
> 阻塞 ~45ms:逻辑清除(count=0 旧数据立即不可读)+ 只擦写指针起点扇区,其余扇区由环形写覆盖时自动擦(原擦全部 751 扇区 ~34s,会触发 IWDG 4s 复位——2026-08-12 修复)。
|
||
|
||
成功后 `SNAP_STAT` 的 `count` 归 0,`seq_last` 继续递增(序号不复位)。
|
||
|
||
### 6.4 快照记录格式(SnapRec,64B 定长,小端)
|
||
|
||
| 偏移 | 长度 | 字段 | 说明 |
|
||
|------|------|------|------|
|
||
| 0 | 1 | `magic` | `0xA6`(区别于事件 `0xA5`) |
|
||
| 1 | 1 | `len` | 线圈数据有效长度 = coil_count×12(0~48) |
|
||
| 2 | 1 | `flags` | bit0=分包帧(预留) |
|
||
| 3 | 1 | `rsvd` | 保留(0) |
|
||
| 4 | 4 | `seq` | 全局序号(跨 boot 递增) |
|
||
| 8 | 4 | `ts_ms` | boot 内相对时间(ms,采集时刻) |
|
||
| 12 | 2 | `boot_seq` | 所属启动段 |
|
||
| 14 | 2 | `rsvd2` | 保留(0) |
|
||
| 16 | 48 | `coils` | 4×12B 线圈数据,与 0xC0 线上格式一致 |
|
||
|
||
**线圈 12B 单元**(与 0xC0 帧内每路线圈完全一致):
|
||
|
||
| 偏移(单元内) | 长度 | 字段 | 说明 |
|
||
|------|------|------|------|
|
||
| 0 | 1 | `cfg` | freq_level(2) + direction(1) + freq_type(1) + sensitivity(4) |
|
||
| 1 | 1 | `cond` | condition(4) + loop_state(1) + car_state(1) + misc_type(2) |
|
||
| 2 | 3 | `freq` | 频率,3B 小端无符号 |
|
||
| 5 | 3 | `variation` | 变化量,3B 小端有符号补码(= Origin − CAPVD) |
|
||
| 8 | 4 | `misc` | 杂项 4B 小端(misc_type=0 时=通过时间,50ms 单位) |
|
||
|
||
> 绝对时间回算:用事件流 `time_anchor`(boot_seq ↔ unix_ts 映射)+ 本记录 `boot_seq`/`ts_ms`。未同步段仅相对时间。
|
||
|
||
---
|
||
|
||
## 7 记录格式(OfflogEvt,32B 定长,小端)
|
||
|
||
| 偏移 | 长度 | 字段 | 说明 |
|
||
|------|------|------|------|
|
||
| 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=LPWR bit30=WWDG bit29=IWDG(看门狗) bit28=软件复位 bit27=POR(真断电) bit26=NRST引脚 | 上电/复位 |
|
||
| `0x10` | iot_connect | — | MQTT TCP 连接成功 |
|
||
| `0x11` | iot_ready | — | MQTT 订阅完成 → 发 initialize(重复上线直接证据) |
|
||
| `0x12` | iot_disconnect | `[1]` reason:1=断开 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 单位) | 线圈事件;sub:1=car_enter 2=car_leave 3=loop_cut 4=loop_restore |
|
||
| `0x50` | time_anchor | —(`unix_ts` 即平台下发值,严格一致) | 时钟同步锚点 |
|
||
| `0x70` | log_clear | — | 日志清除(审计,不可清除) |
|
||
|
||
> payload 内多字节为**大端**(与 offlog 写入侧一致),OfflogEvt 其余字段为**小端**(CPU 原生序,直接 memcpy)。
|
||
|
||
---
|
||
|
||
## 8 版本历史
|
||
|
||
| 版本 | 日期 | 说明 |
|
||
|------|------|------|
|
||
| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR |
|
||
| V1.01 | 2026-08-12 | 分包上限改为随协商 MTU 动态:`chunk = min(MTU-9, 94)`;修复 Too large noti 丢包 |
|
||
| V1.02 | 2026-08-12 | 新增传感快照 3 命令:SNAP_STAT / SNAP_QUERY / SNAP_CLEAR(0x28/0x29/0x2A) |
|