From d6174b9d3f0dcbf45d5c5aa6d1e03d79687a4520 Mon Sep 17 00:00:00 2001 From: wangfq Date: Mon, 10 Aug 2026 14:24:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(vd960DBN):=20BLE=20=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E8=84=B1=E6=9C=BA=E6=97=A5=E5=BF=97=20=E2=80=94=20OFFLOG=5FSTA?= =?UTF-8?q?T/QUERY/CLEAR=20(0x25/0x26/0x27)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 蓝牙通道补齐脱机日志读取 (此前仅 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 行 --- .gitignore | 3 + README.md | 1 + docs/DLD960_BLE协议.md | 153 +++++++++++ .../APP/dbn_ble_srv.c | 91 ++++++- .../APP/include/dbn_ble_srv.h | 9 +- vd960DBN/docs/devlog.md | 49 ++++ vd960DBN/tests/extract_offlog_cases.py | 32 +++ vd960DBN/tests/test_ble_offlog.c | 242 ++++++++++++++++++ 8 files changed, 576 insertions(+), 4 deletions(-) create mode 100644 docs/DLD960_BLE协议.md create mode 100644 vd960DBN/tests/extract_offlog_cases.py create mode 100644 vd960DBN/tests/test_ble_offlog.c diff --git a/.gitignore b/.gitignore index 2b9f141..58cde25 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ *.i *.old +# 测试生成物 (extract_offlog_cases.py 输出, 嵌入源文件真实代码) +offlog_cases_embedded.c + # Object files *.o *.ko diff --git a/README.md b/README.md index 0847f1d..f40196b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ DLD960 是一款基于环形线圈检测原理的四通道车辆检测器,支 | [DLD960_串口通信协议.md](docs/DLD960_串口通信协议.md) | V1.01 | 整机 TTL 串口 | 设备管理、参数配置、数据上报 | | [DLD960_TCP_JSON协议.md](docs/DLD960_TCP_JSON协议.md) | V1.02 | 以太网 TCP :5960 | 鉴权 + 命令 + event_report 客户端必答 + 脱机日志 | | [DLD960_IoT_MQTT协议.md](docs/DLD960_IoT_MQTT协议.md) | V1.06 | 云平台 MQTT | 双主题 `{sn}/srv`+`{sn}/dev`、initialize、event_report 平台必答、设备时钟同步、脱机日志 | +| [DLD960_BLE协议.md](docs/DLD960_BLE协议.md) | V1.00 | 蓝牙 BLE | 帧格式 + 分包 + 脱机日志命令(OFFLOG_STAT/QUERY/CLEAR,与 MQTT/TCP 同语义) | | [DLD960硬件资源.md](docs/DLD960硬件资源.md) | — | 硬件 | 双 MCU IO 分配、继电器、指示灯、拨码 | ## 开发文档 diff --git a/docs/DLD960_BLE协议.md b/docs/DLD960_BLE协议.md new file mode 100644 index 0000000..42fcc0e --- /dev/null +++ b/docs/DLD960_BLE协议.md @@ -0,0 +1,153 @@ +# DLD960 BLE 通信协议(脱机事件日志) + +> 版本: V1.00(2026-08-10,脱机日志部分) +> 适用: vd960DBN(CH32V208,WCH 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 字节)。 + +**响应 data(19B,全小端):** + +| 偏移 | 长度 | 字段 | 说明 | +|------|------|------|------| +| 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) + +**请求 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 记录格式(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=IWDG(看门狗) bit30=WWDG bit29=LPWR bit26=NRST引脚 bit25=POR(真断电) bit24=软件复位 | 上电/复位 | +| `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)。 + +--- + +## 7 版本历史 + +| 版本 | 日期 | 说明 | +|------|------|------| +| V1.00 | 2026-08-10 | 脱机事件日志 3 命令:OFFLOG_STAT / OFFLOG_QUERY / OFFLOG_CLEAR | diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c index cc5dcdb..454f542 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c @@ -10,7 +10,7 @@ #include "storage.h" #include #include "net_srv.h" - +#include "offlog.h" uint8_t g_flag_notify_temp = 0; //ʱ֪ͨnotify flag, 0 disable, 1 enable @@ -65,7 +65,7 @@ void clear_buf_dbn_ble_all(Buf_DBN_BLE * buf) buf->flag = 0; buf->magic = 0; buf->cmd = 0; - memset(buf->dat, 0, MAX_BLE_BUF_LEN); + memset(buf->dat, 0, MAX_BLE_DAT_BUF_LEN); buf->dat_len = 0; buf->dat_offset = 0; buf->pkg_amount = 0; @@ -876,6 +876,93 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len) } PRINT("Rcv_acs_enable_flag: %d\n", g_dbn_ble_state_acs_enable.flag); } break; + case CMD_DBN_OFFLOG_STAT: { + /* offlog stat: status(1) boot_seq(2) count(4) capacity(4) seq_first(4) seq_last(4), all LE */ + uint8_t _i = 0; + uint32_t _total = offlog_count(); + uint32_t _seq_last = offlog_seq_last(); + uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0; + uint32_t _boot = offlog_boot_seq(); + uint32_t _cap = OFFLOG_MAX_RECORDS; + + if (!offlog_enabled()) { + tmp_ble_buf[_i++] = 0x01; /* disabled */ + } else { + tmp_ble_buf[_i++] = 0x00; /* ok */ + tmp_ble_buf[_i++] = (uint8_t)(_boot & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_boot >> 8) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)(_total & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_total >> 8) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_total >> 16) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_total >> 24) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)(_cap & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_cap >> 8) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_cap >> 16) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_cap >> 24) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)(_seq_first & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 8) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 16) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_seq_first >> 24) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)(_seq_last & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 8) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 16) & 0xFF); + tmp_ble_buf[_i++] = (uint8_t)((_seq_last >> 24) & 0xFF); + } + set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i); + PRINT("BLE: offlog_stat count=%lu seq_first=%lu seq_last=%lu\n", + (unsigned long)_total, (unsigned long)_seq_first, (unsigned long)_seq_last); + } break; + case CMD_DBN_OFFLOG_QUERY: { + /* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..4) + resp: status(1) count(1) + N x 32B OfflogEvt raw (LE) */ + uint32_t _start_seq = 0; + uint32_t _req_count = 0; + uint32_t _total = offlog_count(); + uint32_t _seq_last = offlog_seq_last(); + uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0; + uint8_t _i = 0, _j = 0; + + if (!offlog_enabled()) { + tmp_ble_buf[_i++] = 0x01; /* disabled */ + set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i); + break; + } + if (len < 11) { /* frame too short: magic+header+len+cmd+5data+2ckb */ + tmp_ble_buf[_i++] = 0x02; /* bad request */ + set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i); + break; + } + _start_seq = (uint32_t)pkg[4] | ((uint32_t)pkg[5] << 8) + | ((uint32_t)pkg[6] << 16) | ((uint32_t)pkg[7] << 24); + _req_count = pkg[8]; + if (_req_count > OFFLOG_MAX_QUERY_RECORDS) _req_count = OFFLOG_MAX_QUERY_RECORDS; + if (_req_count == 0) _req_count = OFFLOG_MAX_QUERY_RECORDS; + + tmp_ble_buf[_i++] = 0x00; /* status ok */ + tmp_ble_buf[_i++] = 0; /* count placeholder */ + if (_total > 0 && _start_seq >= _seq_first && _start_seq <= _seq_last) { + uint32_t _idx = _start_seq - _seq_first; + while (_j < _req_count && (_idx + _j) < _total) { + OfflogEvt _evt; + if (offlog_read_idx((uint16_t)(_idx + _j), &_evt) != 0) break; + memcpy(&tmp_ble_buf[_i], &_evt, sizeof(OfflogEvt)); + _i += sizeof(OfflogEvt); + _j++; + } + } + tmp_ble_buf[1] = _j; + set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i); + PRINT("BLE: offlog_query start_seq=%lu req=%lu fetched=%u\n", + (unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j); + } break; + case CMD_DBN_OFFLOG_CLEAR: { + /* clear offlog (audit trail). blocking ~2.8s (63 sectors erase), main-loop ctx ok */ + uint8_t _i = 0; + offlog_clear(); + tmp_ble_buf[_i++] = 0x00; /* ok */ + set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i); + PRINT("BLE: offlog_clear done\n"); + } break; default: { } break; diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/dbn_ble_srv.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/dbn_ble_srv.h index ec26f42..0992167 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/dbn_ble_srv.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/dbn_ble_srv.h @@ -49,6 +49,10 @@ #define CMD_DBN_SET_CJQ_PARAM 0x23 // #define CMD_DBN_GET_CJQ_PARAM 0x24 // ȡ +#define CMD_DBN_OFFLOG_STAT 0x25 // query offlog stat +#define CMD_DBN_OFFLOG_QUERY 0x26 // query offlog records by seq +#define CMD_DBN_OFFLOG_CLEAR 0x27 // clear offlog (audit trail) + #define CMD_DBN_RW_UART_BAUD 0x31 @@ -73,7 +77,8 @@ #define MAX_BLE_DAT_RESPONSE_LEN (BLE_BUFF_MAX_LEN - 4) // 14 // ֽܳ20ֽڣȥ magicheaderckbУֽڣʣݵֽΪ14 #define MAX_BLE_Notify_Buf_LEN BLE_BUFF_MAX_LEN //24 #define MAX_BLE_BUF_LEN BLE_BUFF_MAX_LEN //128 -#define MAX_BLE_TMP_BUF_LEN BLE_BUFF_MAX_LEN +#define MAX_BLE_TMP_BUF_LEN 132 // offlog QUERY: 1 status + 4x32B records +#define MAX_BLE_DAT_BUF_LEN 132 // response data buffer (align tmp buf) typedef struct _BLE_Notify_Buf_ { @@ -91,7 +96,7 @@ typedef struct _BUF_DBN_BLE_ uint8_t dat_offset; uint8_t pkg_amount; uint8_t pkg_seq; - uint8_t dat[MAX_BLE_BUF_LEN]; + uint8_t dat[MAX_BLE_DAT_BUF_LEN]; } Buf_DBN_BLE; extern Buf_DBN_BLE g_buf_ble_response; diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index f4cc4a5..8db6aac 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -6,6 +6,54 @@ --- +## 2026-08-10 — BLE 读取脱机日志接口 (OFFLOG_STAT/QUERY/CLEAR) + +### 背景 + +offlog 日志已落地 MQTT V1.06 / TCP JSON V1.02 分发(2026-08-05),但 BLE 侧(小程序/APP 现场离线取证)无读取通道。现场常见场景:设备无网/断网,需蓝牙直连拉日志判断"重复上线"是平台问题还是设备复位。 + +### 方案:3 条 BLE 命令,复用 offlog 底层 API + +| 命令码 | 名称 | 语义(对齐 MQTT log_*) | +|--------|------|--------------------------| +| `0x25` | `CMD_DBN_OFFLOG_STAT` | 日志统计:status + boot_seq(2) + count(4) + capacity(4) + seq_first(4) + seq_last(4),全 LE,19B | +| `0x26` | `CMD_DBN_OFFLOG_QUERY` | 分页拉取:请求 `start_seq(LE32) + count(1)`;响应 `status(1) + count(1) + N×32B OfflogEvt 原始结构`,N≤4(对齐 OFFLOG_MAX_QUERY_RECORDS) | +| `0x27` | `CMD_DBN_OFFLOG_CLEAR` | 清空(审计留痕),阻塞 ~2.8s,主循环上下文可接受 | + +- QUERY 定位复用 MQTT 同款公式 `idx = start_seq - seq_first` → `offlog_read_idx()`,不新增 offlog API +- 记录直接传 32B 二进制 OfflogEvt(BLE 是二进制协议,无需 JSON 化;文档给出结构偏移 + 事件类型表) +- 响应超 96B 自动分包(`set_response_buf` 既有机制):QUERY 最大 130B → 2 包 + +### 缓冲扩容(RAM +64B) + +| 宏 | 旧值 | 新值 | 原因 | +|----|------|------|------| +| `MAX_BLE_TMP_BUF_LEN` | `BLE_BUFF_MAX_LEN`(100) | **132** | QUERY 组包 1+4×32=129B | +| `Buf_DBN_BLE.dat` | `MAX_BLE_BUF_LEN`(100) | **`MAX_BLE_DAT_BUF_LEN`(132)** | set_response_buf 拷贝 129B | + +新增 `MAX_BLE_DAT_BUF_LEN=132`;`clear_buf_dbn_ble_all` 的 memset 同步改用新宏。`tmp_ble_buf`/`Buf_DBN_BLE` 仅 dbn_ble_srv.c/h 内使用,影响面局部。 + +### 关键决策与坑 + +1. **GBK+CRLF 文件二进制编辑**:dbn_ble_srv.c/h 是 GBK 编码(file 报 ISO-8859),`patch` 工具会静默转码成 UTF-8 致中文注释乱码。全部用 Python rb/wb 精确替换,每处 count==1 校验;改后 file 复核仍 ISO-8859。新增注释用 ASCII 规避编码问题。 +2. **case 内变量名冲突**:`manage_dbn_ble_default` 顶部已有 `uint8_t i = 0, k = 0`,case 内再声明 `i` 会 redefinition。全部改用 `_i`/`_j`。 +3. **QUERY 短帧防御**:`len < 11`(magic+header+len+cmd+5data+2ckb)返回 status=0x02,防 pkg[4..8] 越界读。 +4. **LOG_CLEAR 阻塞告警**:BLE 连接期间 2.8s 擦除阻塞,若 MQTT 在线会导致 WCHNET 短时失服务(60s keepalive 可吸收),文档已标注。 +5. **编码验证**:check_c_balance.py 自身 docstring 有转义 bug(`\\x` 在普通字符串触发 unicodeescape),已改 raw string 修复。 + +### 验证 + +- 新增 `tests/test_ble_offlog.c`:**隔离测试框架嵌入 dbn_ble_srv.c 提取的 3 case 真实文本**(`extract_offlog_cases.py` 提取,生成物 gitignore),mock offlog_* + set_response_buf,7 例全过: + - STAT 正常(19B 字段逐字节验证)/ disabled + - QUERY 正常(4×32B 记录 + seq/type/payload 偏移验证)/ 越界空 / 短帧 / disabled + - CLEAR 正常 +- offlog 回归 8 例 ALL PASS(既有 7 例 + export_json 未破坏) +- 新增 `docs/DLD960_BLE协议.md` V1.00(帧格式 + 分包 + 3 命令 + OfflogEvt 32B 结构 + 事件类型表) +- README 协议文档表新增 BLE 协议行 +- 本地无 RISC-V 工具链,MRS 真编译待板上联调(语法级 check_c_balance 通过) + +--- + ## 2026-08-05 — offlog 协议导出命令分发落地 (MQTT V1.06 + TCP JSON V1.02) ### 背景 @@ -756,6 +804,7 @@ TCP 超时要等 ~2 分钟才触发 `SINT_STAT_TIM_OUT`, | 版本 | 时间 | 说明 | |------|------|------| +| V4.0 | 2026-08-10 | BLE 脱机日志接口: OFFLOG_STAT/QUERY/CLEAR (0x25/0x26/0x27), 32B OfflogEvt 二进制直传, 缓冲扩容 132B, 隔离测试 7 例 + 协议文档 V1.00 | | V3.9 | 2026-08-05 | offlog 协议导出命令分发: log_stat/log_query/log_clear 落地 MQTT V1.06 + TCP JSON V1.02 (seq_first/seq_last 计算、idx 映射、OFFLOG_MAX_QUERY_RECORDS=4、log_clear 阻塞~2.8s) | | V3.8 | 2026-08-04 | offlog: TIME_ANCHOR 严格用平台下发 unix_ts (offlog_evt_ts), 单测 7 例 | | V3.7 | 2026-08-04 | tcp_json_srv 缓冲宏化: data_json[2048]→TCP_JSON_DATA_BUF_LEN(1024), 省栈1KB×3 | diff --git a/vd960DBN/tests/extract_offlog_cases.py b/vd960DBN/tests/extract_offlog_cases.py new file mode 100644 index 0000000..51e4de8 --- /dev/null +++ b/vd960DBN/tests/extract_offlog_cases.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""从 dbn_ble_srv.c 提取脱机日志 3 case 文本, 供 test_ble_offlog.c 隔离验证。 + +用法: python3 extract_offlog_cases.py +输出: ./offlog_cases_embedded.c (在 tests/ 目录下运行) + +原理: 测的是源文件真实代码而非拷贝 — 提取 `case CMD_DBN_OFFLOG_STAT` +到 `default:` 之间的文本 (含 STAT/QUERY/CLEAR 3 case), 嵌入测试框架编译。 +""" +import os + +SRC = os.path.join(os.path.dirname(__file__), + "../BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c") +OUT = os.path.join(os.path.dirname(__file__), "offlog_cases_embedded.c") + +with open(SRC, "rb") as f: + content = f.read() +text = content.decode("utf-8", errors="replace").replace("\r\n", "\n") + +start = text.find("case CMD_DBN_OFFLOG_STAT") +end = text.find("default:\n", start) +if start == -1 or end == -1: + raise SystemExit("ERROR: 未找到 offlog 3 case (确认 dbn_ble_srv.c 已修改)") + +cases = text[start:end] +for c in ("CMD_DBN_OFFLOG_STAT", "CMD_DBN_OFFLOG_QUERY", "CMD_DBN_OFFLOG_CLEAR"): + if c not in cases: + raise SystemExit(f"ERROR: 提取内容缺少 {c}") + +with open(OUT, "w") as f: + f.write(cases) +print(f"OK: {OUT} ({len(cases)} bytes, 3 cases)") diff --git a/vd960DBN/tests/test_ble_offlog.c b/vd960DBN/tests/test_ble_offlog.c new file mode 100644 index 0000000..4c437ac --- /dev/null +++ b/vd960DBN/tests/test_ble_offlog.c @@ -0,0 +1,242 @@ +/** + * test_ble_offlog.c — dbn_ble_srv.c 脱机日志 3 case 的隔离验证 + * + * 直接从 dbn_ble_srv.c 提取 case 文本嵌入本文件, 测的是源文件真实代码。 + * Mock: offlog_* / set_response_buf / PRINT / Buf_DBN_BLE 类型。 + * + * 编译: gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \ + * -o test_ble_offlog test_ble_offlog.c (需先跑 extract_offlog_cases.py) + * 运行: ./test_ble_offlog + */ +#include +#include +#include + +/* ============ 类型/宏 (来自 dbn_ble_srv.h 编辑后, 避免 include BLE 栈) ============ */ +#define MAGIC_BYTE_DBN_DEFAULT 0x8F +#define CMD_DBN_OFFLOG_STAT 0x25 +#define CMD_DBN_OFFLOG_QUERY 0x26 +#define CMD_DBN_OFFLOG_CLEAR 0x27 +#define MAX_BLE_TMP_BUF_LEN 132 +#define MAX_BLE_DAT_BUF_LEN 132 +#define MAX_BLE_DAT_RESPONSE_LEN (100 - 4) /* 单包数据上限 96B */ + +typedef struct _BLE_Notify_Buf_ { + uint8_t flag; + uint8_t len; + uint8_t buf[100]; +} BLE_Notify_Buf; + +typedef struct _BUF_DBN_BLE_ { + uint8_t flag; + uint8_t magic; + uint8_t cmd; + uint8_t dat_len ; + uint8_t dat_offset; + uint8_t pkg_amount; + uint8_t pkg_seq; + uint8_t dat[MAX_BLE_DAT_BUF_LEN]; +} Buf_DBN_BLE; + +#include "offlog.h" + +#define PRINT(...) ((void)0) + +/* ============ 全局 (来自 dbn_ble_srv.c) ============ */ +static uint8_t tmp_ble_buf[MAX_BLE_TMP_BUF_LEN]; +static Buf_DBN_BLE g_buf_ble_response; + +/* ============ offlog mock ============ */ +static uint32_t mock_count = 0; +static uint32_t mock_seq_last = 0; +static uint32_t mock_boot = 0; +static uint8_t mock_enabled = 1; + +uint16_t offlog_count(void) { return (uint16_t)mock_count; } +uint32_t offlog_seq_last(void) { return mock_seq_last; } +uint32_t offlog_boot_seq(void) { return mock_boot; } +uint8_t offlog_enabled(void) { return mock_enabled; } +void offlog_clear(void) { mock_count = 1; mock_seq_last++; } /* 审计留痕: count 归 1, seq 继续 */ + +int offlog_read_idx(uint16_t idx, OfflogEvt *out) +{ + uint32_t seq_first; + if (idx >= mock_count) return -1; + seq_first = mock_seq_last - mock_count + 1; + memset(out, 0, sizeof(OfflogEvt)); + out->magic = OFFLOG_EVT_MAGIC; + out->type = (idx % 3 == 0) ? OFFLOG_EVT_BOOT : + (idx % 3 == 1) ? OFFLOG_EVT_IOT_READY : OFFLOG_EVT_COIL; + out->seq = seq_first + idx; + out->boot_seq = 2; + out->ts_ms = 1000 + idx * 100; + out->unix_ts = 1784768575UL; + if (idx % 3 == 2) { /* COIL: sub=1 ch=1 value=97 */ + out->payload[0] = 1; out->payload[1] = 1; + out->payload[2] = 0; out->payload[3] = 0; out->payload[4] = 0; out->payload[5] = 97; + } + return 0; +} + +/* ============ set_response_buf mock: 只记录调用参数 ============ */ +static uint8_t last_cmd = 0; +static uint8_t last_len = 0; +static uint8_t last_dat[160]; + +uint8_t set_response_buf(Buf_DBN_BLE *response_dst, uint8_t magic, uint8_t cmd, + uint8_t *dat, uint8_t dat_len) +{ + uint8_t i; + memset(last_dat, 0, sizeof(last_dat)); + last_cmd = cmd; + last_len = dat_len; + for (i = 0; i < dat_len; i++) last_dat[i] = dat[i]; + return 0; +} + +/* ============ 被测: 从 dbn_ble_srv.c 提取的 3 case (嵌入 switch) ============ */ +static void run_case(uint8_t cmd, uint8_t *pkg, uint8_t len) +{ + uint8_t _cmd = cmd; + switch (_cmd) { +#include "offlog_cases_embedded.c" + default: break; + } +} + +/* ============ 断言 ============ */ +static int failures = 0; +#define CHECK(cond) do { \ + if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \ +} while (0) + +static uint8_t pkg[16]; + +static void test_stat_ok(void) +{ + mock_enabled = 1; mock_count = 1234; mock_seq_last = 1333; mock_boot = 2; + memset(tmp_ble_buf, 0, sizeof(tmp_ble_buf)); + run_case(0x25, NULL, 0); + CHECK(last_cmd == 0x25); + CHECK(last_len == 19); + /* status=0 */ + CHECK(last_dat[0] == 0x00); + /* boot_seq=2 LE */ + CHECK(last_dat[1] == 0x02 && last_dat[2] == 0x00); + /* count=1234 LE */ + CHECK(last_dat[3] == 0xD2 && last_dat[4] == 0x04 && last_dat[5] == 0x00 && last_dat[6] == 0x00); + /* capacity=8064 LE */ + CHECK(last_dat[7] == 0x80 && last_dat[8] == 0x1F && last_dat[9] == 0x00 && last_dat[10] == 0x00); + /* seq_first=100 LE */ + CHECK(last_dat[11] == 0x64 && last_dat[12] == 0x00 && last_dat[13] == 0x00 && last_dat[14] == 0x00); + /* seq_last=1333 LE */ + CHECK(last_dat[15] == 0x35 && last_dat[16] == 0x05 && last_dat[17] == 0x00 && last_dat[18] == 0x00); + printf("PASS test_stat_ok\n"); +} + +static void test_stat_disabled(void) +{ + mock_enabled = 0; + run_case(0x25, NULL, 0); + CHECK(last_cmd == 0x25); + CHECK(last_len == 1); + CHECK(last_dat[0] == 0x01); + printf("PASS test_stat_disabled\n"); +} + +static void test_query_ok(void) +{ + /* 10 条: seq 11..20, 请求 start_seq=13 count=4 → 返回 seq 13,14,15,16 */ + mock_enabled = 1; mock_count = 10; mock_seq_last = 20; mock_boot = 2; + memset(pkg, 0, sizeof(pkg)); + pkg[4] = 13; /* start_seq LE */ + pkg[5] = 0; pkg[6] = 0; pkg[7] = 0; + pkg[8] = 4; /* count */ + memset(tmp_ble_buf, 0, sizeof(tmp_ble_buf)); + run_case(0x26, pkg, 11); + + CHECK(last_cmd == 0x26); + CHECK(last_len == 2 + 4 * 32); /* status + count + 4×32B */ + CHECK(last_dat[0] == 0x00); /* status ok */ + CHECK(last_dat[1] == 4); /* fetched 4 */ + /* 记录0 (idx=2, seq=13): type=coil (mock: idx%3==2 → COIL), 仅查 magic+seq */ + CHECK(last_dat[2] == OFFLOG_EVT_MAGIC); + CHECK(last_dat[6] == 13); /* seq LE32 偏移4 */ + CHECK(last_dat[7] == 0); + /* 记录1 (idx=3, seq=14): type=boot */ + CHECK(last_dat[2+32+1] == OFFLOG_EVT_BOOT); + CHECK(last_dat[2+32+4] == 14); + /* 记录2 (idx=4, seq=15): type=iot_ready, payload 空 */ + CHECK(last_dat[2+64+1] == OFFLOG_EVT_IOT_READY); + CHECK(last_dat[2+64+4] == 15); + /* 记录3 (idx=5, seq=16): type=coil, payload sub=1 ch=1 value=97 */ + CHECK(last_dat[2+96+1] == OFFLOG_EVT_COIL); + CHECK(last_dat[2+96+4] == 16); + CHECK(last_dat[2+96+20] == 1); /* payload[0] sub */ + CHECK(last_dat[2+96+21] == 1); /* payload[1] ch */ + CHECK(last_dat[2+96+25] == 97); /* payload[5] value LSB */ + printf("PASS test_query_ok\n"); +} + +static void test_query_range_error(void) +{ + mock_enabled = 1; mock_count = 10; mock_seq_last = 20; mock_boot = 2; + memset(pkg, 0, sizeof(pkg)); + pkg[4] = 100; /* 越界 > seq_last */ + pkg[8] = 4; + run_case(0x26, pkg, 11); + CHECK(last_cmd == 0x26); + CHECK(last_len == 2); + CHECK(last_dat[0] == 0x00); + CHECK(last_dat[1] == 0); /* 空 records */ + printf("PASS test_query_range_error\n"); +} + +static void test_query_short_frame(void) +{ + mock_enabled = 1; + pkg[8] = 4; + run_case(0x26, pkg, 9); /* len < 11 */ + CHECK(last_cmd == 0x26); + CHECK(last_len == 1); + CHECK(last_dat[0] == 0x02); /* bad request */ + printf("PASS test_query_short_frame\n"); +} + +static void test_query_disabled(void) +{ + mock_enabled = 0; + pkg[8] = 4; + run_case(0x26, pkg, 11); + CHECK(last_cmd == 0x26); + CHECK(last_len == 1); + CHECK(last_dat[0] == 0x01); /* disabled */ + printf("PASS test_query_disabled\n"); +} + +static void test_clear(void) +{ + mock_enabled = 1; mock_count = 5; mock_seq_last = 30; + run_case(0x27, NULL, 0); + CHECK(last_cmd == 0x27); + CHECK(last_len == 1); + CHECK(last_dat[0] == 0x00); + /* offlog_clear mock: count 归 1, seq_last 递增 */ + CHECK(mock_count == 1); + CHECK(mock_seq_last == 31); + printf("PASS test_clear\n"); +} + +int main(void) +{ + test_stat_ok(); + test_stat_disabled(); + test_query_ok(); + test_query_range_error(); + test_query_short_frame(); + test_query_disabled(); + test_clear(); + if (failures == 0) printf("\nALL PASS\n"); + else printf("\n%d FAILURES\n", failures); + return failures ? 1 : 0; +}