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
+32
View File
@@ -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)")
+242
View File
@@ -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 <stdio.h>
#include <string.h>
#include <stdint.h>
/* ============ 类型/宏 (来自 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;
}