From 92887a3d925163d56e18186d628a9149ad331dd1 Mon Sep 17 00:00:00 2001 From: wangfq Date: Wed, 12 Aug 2026 17:39:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(vd960DBN):=20=E4=BC=A0=E6=84=9F=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E5=8C=BA=E8=90=BD=E5=9C=B0=20snapshot.c=20+=20BLE=20S?= =?UTF-8?q?NAP=5FSTAT/QUERY/CLEAR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - snapshot.h/c: 64B 定长 SnapRec (头部16B + 4x12B 0xC0线圈数据原样) 环形分区独立于事件日志, 快照区 = 总容量 - 固定区576KB - 事件区 - 线程模型: USART2 ISR 只打包+RAM暂存(8深满丢新), 主循环 snap_flush 落盘 (iot_sensor_ingest 在中断上下文, SPI 45ms 擦除严禁进中断) - dbn_ble_srv: SNAP_STAT(0x28)/SNAP_QUERY(0x29)/SNAP_CLEAR(0x2A) QUERY 上限 2 条 (64x2+2=130B, ODR 教训防御) - iot_mqtt_srv: ingest 挂 snap_enqueue, publish_sensor 挂 snap_flush (放在 READY 检查前, 断网照常落盘) - peripheral_main: offlog_init 后加 snap_init - 清空审计: 写事件流 log_clear payload[0]=2 - 测试: test_snapshot.c 8 例全过, offlog/ble_offlog 回归全过 - 文档: DLD960_BLE协议 V1.02 + ROADMAP P1.2 状态 + devlog --- .gitignore | 5 + docs/DLD960_BLE协议.md | 92 +++- docs/ROADMAP.md | 4 +- .../APP/dbn_ble_srv.c | 92 ++++ .../APP/include/dbn_ble_srv.h | 3 + .../APP/include/offlog.h | 2 +- .../APP/include/snapshot.h | 136 ++++++ .../APP/iot_mqtt_srv.c | 6 + .../APP/peripheral_main.c | 2 + .../OnlyUpdateApp_Peripheral/APP/snapshot.c | 408 ++++++++++++++++++ vd960DBN/docs/devlog.md | 28 ++ vd960DBN/tests/test_snapshot.c | 353 +++++++++++++++ 12 files changed, 1123 insertions(+), 8 deletions(-) create mode 100644 vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/snapshot.h create mode 100644 vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c create mode 100644 vd960DBN/tests/test_snapshot.c diff --git a/.gitignore b/.gitignore index 58cde25..b135414 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,8 @@ wchreference/ __pycache__/ *.pyc + +# gcc 单测编译产物 (tests/*.c 保留) +test_offlog +test_ble_offlog +test_snapshot diff --git a/docs/DLD960_BLE协议.md b/docs/DLD960_BLE协议.md index b8d99ee..99c5bc4 100644 --- a/docs/DLD960_BLE协议.md +++ b/docs/DLD960_BLE协议.md @@ -1,8 +1,8 @@ -# DLD960 BLE 通信协议(脱机事件日志) +# DLD960 BLE 通信协议(脱机事件日志 + 传感快照) -> 版本: V1.01(2026-08-12,分包上限改为随协商 MTU 动态) +> 版本: V1.02(2026-08-12,新增传感快照 3 命令;分包上限随协商 MTU 动态) > 适用: vd960DBN(CH32V208,WCH BLE 协议栈) -> 用途: 小程序/APP 经蓝牙读取设备本地 W25Q32 环形事件日志(离线取证:区分设备真复位 vs MQTT 断连重连) +> 用途: 小程序/APP 经蓝牙读取设备本地 W25Qxx 环形日志(离线取证:事件流区分真复位 vs MQTT 断连重连;快照流回放 0xC0 传感波形) --- @@ -43,6 +43,9 @@ Magic | Header | Data | CheckByte | `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` 语义一致,通道不同。 @@ -120,7 +123,84 @@ Magic | Header | Data | CheckByte --- -## 6 记录格式(OfflogEvt,32B 定长,小端) +## 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=快照流,不可清除)。 +> ⚠ **阻塞时长随芯片**:W25Q32 快照 751 扇区 SPI 擦除 ≈ 34s,期间主循环阻塞。请勿高频调用。 + +成功后 `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 定长,小端) | 偏移 | 长度 | 字段 | 说明 | |------|------|------|------| @@ -154,8 +234,10 @@ Magic | Header | Data | CheckByte --- -## 7 版本历史 +## 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) | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 17aee0f..06e32fe 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -91,8 +91,8 @@ **两条日志流分开存**: -1. **事件流(必录,低速率)**:car_enter/car_leave(含时间量)、线圈断线/恢复、继电器动作、上电/复位(含复位原因)、MQTT/TCP 连接与断开、event_report ACK 超时/重发/放弃、配置变更(含来源:BLE/MQTT/TCP)、灵敏度换档、**时钟同步锚点**、固件升级开始/结果、日志清除操作(审计自记录) -2. **快照流(环形可覆盖)**:0xC0 帧二进制原样 + 头部(序号/boot_seq/mstick),~64B/条,**记录节奏与网络上报同频**(空闲按 interval、活动 300ms、car 沿立即)——直接挂在 `iot_sensor_ingest()` 同源出口,天然与上报一致 +1. **事件流(必录,低速率)**:car_enter/car_leave(含时间量)、线圈断线/恢复、继电器动作、上电/复位(含复位原因)、MQTT/TCP 连接与断开、event_report ACK 超时/重发/放弃、配置变更(含来源:BLE/MQTT/TCP)、灵敏度换档、**时钟同步锚点**、固件升级开始/结果、日志清除操作(审计自记录)——**已实现(offlog)** +2. **快照流(环形可覆盖)**:0xC0 帧 4 线圈数据原样(12B/线圈)+ 头部(序号/boot_seq/mstick),64B/条,**记录节奏与上报同频**——直接挂在 `iot_sensor_ingest()` 同源出口。**已实现(snapshot,2026-08-12)**:中断只入 RAM 暂存(8 深满丢新),主循环 flush 落盘;BLE 新增 SNAP_STAT/QUERY/CLEAR(0x28/0x29/0x2A) **容量预算**(**以 W25Q32 为基准**:事件日志 512KB ≈ 16384 条 / 快照区 3008KB ≈ **4.8 万条** @64B): diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c index e28961d..ac41aeb 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/dbn_ble_srv.c @@ -11,6 +11,7 @@ #include #include "net_srv.h" #include "offlog.h" +#include "snapshot.h" extern uint16_t peripheral_get_mtu(void); uint8_t g_flag_notify_temp = 0; //ʱ֪ͨnotify flag, 0 disable, 1 enable @@ -1012,6 +1013,97 @@ void manage_dbn_ble_default(uint8_t *pkg, uint8_t len) set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i); PRINT("BLE: offlog_clear done\n"); } break; + case CMD_DBN_SNAP_STAT: { + /* snap 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 = snap_count(); + uint32_t _seq_last = snap_seq_last(); + uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0; + uint32_t _boot = snap_boot_seq(); + uint32_t _cap = SNAP_MAX_RECORDS; + + if (!snap_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: snap_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_SNAP_QUERY: { + /* paged fetch: req pkg[4..7]=start_seq(LE32) pkg[8]=count(1..2) + resp: status(1) count(1) + N x 64B SnapRec raw (LE) */ + uint32_t _start_seq = 0; + uint32_t _req_count = 0; + uint32_t _total = snap_count(); + uint32_t _seq_last = snap_seq_last(); + uint32_t _seq_first = (_total > 0) ? (_seq_last - _total + 1) : 0; + uint8_t _i = 0, _j = 0; + + if (!snap_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 > SNAP_MAX_QUERY_RECORDS) _req_count = SNAP_MAX_QUERY_RECORDS; + if (_req_count == 0) _req_count = SNAP_MAX_QUERY_RECORDS; + /* overflow guard: resp dat = 2 + N*64 must fit tmp buf (ODR lesson) */ + { uint16_t _max_rec = (MAX_BLE_TMP_BUF_LEN >= 2) ? + ((MAX_BLE_TMP_BUF_LEN - 2) / (uint16_t)sizeof(SnapRec)) : 0; + if (_req_count > _max_rec) _req_count = _max_rec; } + + 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) { + SnapRec _rec; + if (snap_read_idx(_idx + _j, &_rec) != 0) break; + memcpy(&tmp_ble_buf[_i], &_rec, sizeof(SnapRec)); + _i += sizeof(SnapRec); + _j++; + } + } + tmp_ble_buf[1] = _j; + set_response_buf(&g_buf_ble_response, MAGIC_BYTE_DBN_DEFAULT, _cmd, tmp_ble_buf, _i); + PRINT("BLE: snap_query start_seq=%lu req=%lu fetched=%u\n", + (unsigned long)_start_seq, (unsigned long)_req_count, (unsigned)_j); + } break; + case CMD_DBN_SNAP_CLEAR: { + /* clear snapshot (audit trail). blocking (sectors erase), main-loop ctx ok */ + uint8_t _i = 0; + snap_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: snap_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 0992167..10b7c04 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/dbn_ble_srv.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/dbn_ble_srv.h @@ -52,6 +52,9 @@ #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_SNAP_STAT 0x28 // query snapshot stat +#define CMD_DBN_SNAP_QUERY 0x29 // query snapshot records by seq +#define CMD_DBN_SNAP_CLEAR 0x2A // clear snapshot (audit trail) #define CMD_DBN_RW_UART_BAUD 0x31 diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h index 49ab0bf..6aad6d3 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h @@ -15,7 +15,7 @@ * | 参数区 0x000000 64KB (固定) * | OTA 区 0x010000 512KB (固定) * | 事件区 0x090000 动态: W25Q32=512KB / Q64=1MB / Q128=2MB / Q256=4MB - * | 快照区 事件区后 剩余容量 (预留, 后续) + * | 快照区 事件区后 剩余容量 (snapshot.c, 2026-08-12 已实现) * 上电读 JEDEC ID (0x9F) 决定事件区容量 (g_offlog_part 运行时分区表) * * 环形实现: diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/snapshot.h b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/snapshot.h new file mode 100644 index 0000000..ded143f --- /dev/null +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/include/snapshot.h @@ -0,0 +1,136 @@ +/** + ****************************************************************************** + * @file snapshot.h + * @author wfq + * @version V1.0 + * @date 2026-08-12 + * @brief DBN 传感快照日志 (W25Qxx 外部 SPI NOR, 环形, 与事件日志区分区独立) + * + * 用途: 0xC0 传感帧 (4 线圈) 按上报节奏落盘, 断网期间也有现场波形可查; + * 事件流管"发生了什么", 快照流管"波形长什么样"。 + * + * 分区 (ROADMAP 2026-08-12 新规划): + * | 参数区 0x000000 64KB (固定) + * | OTA 区 0x010000 512KB (固定) + * | 事件区 0x090000 动态 (offlog 持有, g_offlog_part.area_size) + * | 快照区 事件区后 剩余容量 (总容量 - 固定区 - 事件区) + * 上电读 JEDEC ID (0x9F) 决定事件区/快照区容量 (g_snap_part 运行时分区表) + * + * 环形实现 (与 offlog 同模式): + * - 扇区 0 (区内) = 头扇区, 存元数据 (boot_seq/写位置/条数), 扇区切换时更新 + * - 数据扇区每扇区 64 条 x 64B 定长记录 (4096/64) + * - 顺序写, 写满擦下一扇区 (环形覆盖), 天然磨损均衡 + * - 上电从头部记录的写位置向后扫描, 找第一条无效记录 = 真实写位置 (掉电恢复) + * + * 线程模型 (关键): + * - snap_enqueue(): 中断上下文 (USART2 ISR → lup_process_frame → iot_sensor_ingest), + * 只做打包 + RAM 环形暂存 (memcpy, 无阻塞, 无 SPI 操作); 满时丢新不覆盖 + * - snap_flush(): 主循环上下文 (iot_mqtt_publish_sensor), RAM 暂存 → SPI 落盘 + * (扇区切换擦除 ~45ms 阻塞仅发生在主循环, 与 offlog 同底线) + * - 单生产者 (中断) 单消费者 (主循环) 无锁环形: 生产者只写 tail, + * 消费者只读 head, 留一格判满 + * + * 记录格式 SnapRec (64B 定长, 无 padding): + * magic(1) len(1) flags(1) rsvd(1) | seq(4) ts_ms(4) boot_seq(2) rsvd2(2) | coils(48) + * coils = 4 x 12B, 与 0xC0 线上线圈单元完全一致: + * [0] 配置1 freq_level(2)|direction(1)|freq_type(1)|sensitivity(4) + * [1] 条件 condition(4)|loop_state(1)|car_state(1)|misc_type(2) + * [2..4] 频率 3B LE + * [5..7] 变化量 3B LE (有符号补码) + * [8..11] 杂项 4B LE + ****************************************************************************** + */ + +#ifndef _SNAPSHOT_H__ +#define _SNAPSHOT_H__ + +#include +#include "offlog.h" /* OFFLOG_CHIP_xxx + g_offlog_part (事件区大小) */ +#include "loop_uart_proto.h" /* LUP_SensorReport / LUP_CoilSensor */ + +/*=========================================================================== + * 分区与容量 + *===========================================================================*/ +/* 固定分区 (与 offlog 一致): 参数区 64KB + OTA 512KB */ +#define SNAP_PARAM_SIZE 0x10000UL /* 参数区 64KB (固定) */ +#define SNAP_OTA_SIZE 0x80000UL /* OTA 镜像暂存 512KB (固定) */ +#define SNAP_FIXED_SIZE (SNAP_PARAM_SIZE + SNAP_OTA_SIZE) /* 0x090000 */ + +#define SNAP_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */ +#define SNAP_REC_PER_SECTOR 64 /* 4096 / 64 */ + +/* 运行时分区表 (snap_init 由 JEDEC ID + g_offlog_part 填充; 默认 W25Q32) */ +typedef struct { + uint8_t chip; /* OFFLOG_CHIP_xxx */ + uint32_t area_base; /* 快照区起始 (事件区后) */ + uint32_t area_size; /* 快照区大小 (剩余容量) */ + uint32_t data_sectors; /* 数据扇区数 (不含头扇区) */ + uint32_t max_records; /* 最大记录数 = data_sectors * 64 */ +} SnapPart; +extern SnapPart g_snap_part; + +/* 兼容宏: 展开为运行时值 */ +#define SNAP_DATA_BASE (g_snap_part.area_base + 4096UL) +#define SNAP_AREA_SIZE (g_snap_part.area_size) +#define SNAP_DATA_SECTOR_CNT (g_snap_part.data_sectors) +#define SNAP_DATA_SIZE ((g_snap_part.data_sectors) * 4096UL) +#define SNAP_MAX_RECORDS (g_snap_part.max_records) + +/* 头扇区记录 (32B, 扇区切换时更新) */ +typedef struct { + uint8_t magic[4]; /* "SNAP" */ + uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */ + uint32_t wr_sector; /* 上次写扇区号 (区内 1..n) */ + uint32_t wr_off; /* 上次写偏移 (绝对地址) */ + uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */ + uint32_t count; /* 有效记录数 (0..max_records) */ + uint32_t rsvd[2]; +} SnapHead; /* 28B, 按 32B 对齐使用 */ + +#define SNAP_HEAD_MAGIC0 'S' +#define SNAP_HEAD_MAGIC1 'N' +#define SNAP_HEAD_MAGIC2 'A' +#define SNAP_HEAD_MAGIC3 'P' + +/* 快照记录 (64B 定长, 字段重排保证自然对齐无 padding: + 4×uint8 → uint32×2 → uint16×2 → coils[48] = 16+48 = 64) */ +typedef struct { + uint8_t magic; /* 0xA6 (区别于事件 0xA5) */ + uint8_t len; /* 线圈数据有效长度 = coil_count*12 (0..48) */ + uint8_t flags; /* bit0: 分包帧 (预留) */ + uint8_t rsvd; + uint32_t seq; /* 全局序号 (跨 boot 递增, flush 时分配) */ + uint32_t ts_ms; /* boot 内相对时间 mstick() (采集时刻) */ + uint16_t boot_seq; /* 启动序号 */ + uint16_t rsvd2; + uint8_t coils[48]; /* 4 × 12B 线圈数据 (0xC0 线上格式原样) */ +} SnapRec; /* 64B, 无 padding */ + +#define SNAP_MAGIC 0xA6 +#define SNAP_REC_SIZE sizeof(SnapRec) +#define SNAP_COIL_BYTES 12 +#define SNAP_COILS_MAX 4 + +/* RAM 暂存深度 (中断 enqueue → 主循环 flush 之间的缓冲) + Loop 帧最快 150ms, publish_sensor 每轮 flush, 8 深 ≈ 1.2s 余量; + CH32V208 SRAM 紧张: 64B × 8 = 512B, 远小于 SPI_FLASH_BUF 的 4KB */ +#define SNAP_RAM_DEPTH 8 +#define SNAP_MAX_QUERY_RECORDS 2 /* 快照 QUERY 分页上限: (132-2)/64 */ + + +/*=========================================================================== + * API + *===========================================================================*/ +void snap_init(void); /* 上电初始化 (扫描恢复) */ +uint8_t snap_enabled(void); /* Flash 初始化成功 */ + +void snap_enqueue(const LUP_SensorReport *sr); /* 中断安全: 打包→RAM 暂存 (满丢新) */ +void snap_flush(void); /* 主循环: RAM 暂存→SPI 落盘 */ + +uint32_t snap_count(void); /* 有效记录数 */ +uint32_t snap_seq_last(void); /* 最新一条记录全局序号 (0=空) */ +uint32_t snap_boot_seq(void); /* 当前 boot 序号 */ +int snap_read_idx(uint32_t idx, SnapRec *out); /* 按逻辑序号读, 0=成功 -1=越界 */ +void snap_clear(void); /* 清空快照区 (审计写入事件流) */ + +#endif /* _SNAPSHOT_H__ */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c index 93e9fc7..dabf066 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/iot_mqtt_srv.c @@ -24,6 +24,7 @@ #include "tcp_json_srv.h" #include "storage.h" #include "offlog.h" +#include "snapshot.h" #include "ch32v20x_iwdg.h" #include #include @@ -164,6 +165,7 @@ static void iot_sensor_ingest(const LUP_SensorReport *sr) { iot_evt_feed(sr); // 事件沿检测 memcpy(&_cached_sr, sr, sizeof(LUP_SensorReport)); // 刷新 loop_data 快照 _cached_sr_valid = 1; + snap_enqueue(sr); /* 快照入队: 中断安全, 主循环 snap_flush 落盘 */ } /* lup 传感回调: uart_srv 消费路径的帧从这里喂入 (lup_process_frame 已过校验) */ @@ -774,6 +776,10 @@ void iot_mqtt_publish_sensor(void) { } } + /* 快照落盘: 主循环上下文 (Step1 直读帧 + 回调帧都已在 ingest 入队); + 放在 READY/enable 检查之前 → 断网/未使能期间照常落 (脱机取证语义) */ + snap_flush(); + /*--- 事件上报状态机: 发送/重发/重连补报 ---*/ iot_evt_process(); diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c index 323eae1..3be6451 100644 --- a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/peripheral_main.c @@ -23,6 +23,7 @@ #include "net_srv.h" #include "storage.h" #include "offlog.h" +#include "snapshot.h" #include "tcp_json_srv.h" #include "loop_uart_proto.h" #include "iot_mqtt_srv.h" @@ -304,6 +305,7 @@ int main(void) GetMacAddr(gMacAddr); storage_init(); offlog_init(); + snap_init(); /* 复位原因: 必须在初始化后立即读 (离复位时刻最近), 并清除标志, 保证下次上电只反映本次复位原因 (RCC_RSTSCKR, STM32F1/CH32V20x 兼容) */ diff --git a/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c new file mode 100644 index 0000000..9c0da31 --- /dev/null +++ b/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c @@ -0,0 +1,408 @@ +/** + ****************************************************************************** + * @file snapshot.c + * @author wfq + * @version V1.0 + * @date 2026-08-12 + * @brief DBN 传感快照日志 — 环形实现 (与 offlog 同模式, 分区独立) + * + * 依赖: storage.c (SPI_Flash_*), cmcng.h (mstick), offlog.c (g_offlog_part + 审计) + * 线程模型: enqueue 在中断 (USART2 ISR), flush/write_raw/clear 在主循环。 + * 所有 SPI 操作只在主循环 (扇区擦除 ~45ms 阻塞可接受, 与 offlog 同底线)。 + ****************************************************************************** + */ + +#include "snapshot.h" +#include "storage.h" +#include "cmcng.h" +#include + +/* 编译期断言: 记录必须是 64B (定长环形索引依赖) */ +typedef char snap_rec_size_must_be_64[(sizeof(SnapRec) == 64) ? 1 : -1]; +typedef char snap_head_size_must_be_32[(sizeof(SnapHead) == 32) ? 1 : -1]; + +/*=========================================================================== + * 内部状态 (仅主循环访问: init/flush/clear/查询) + *===========================================================================*/ +static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */ +static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..n) */ +static uint32_t _wr_seq; /* 下一条记录的全局 seq */ +static uint16_t _boot_seq; /* 当前 boot 序号 */ +static uint32_t _count; /* 有效记录数 (0..max_records) */ +static uint8_t _ready; /* init 完成标志 */ + +/* 中断安全 RAM 暂存 (单生产者=中断 enqueue / 单消费者=主循环 flush, 无锁) + 生产者只写 g_snap_wr / g_snap_count, 消费者只读 g_snap_rd; count 满则丢新 */ +static SnapRec g_snap_pending[SNAP_RAM_DEPTH]; +static volatile uint8_t g_snap_rd; /* 消费者 (主循环) 读指针 */ +static volatile uint8_t g_snap_wr; /* 生产者 (中断) 写指针 */ +static volatile uint8_t g_snap_count; /* 暂存条数 (容量 = SNAP_RAM_DEPTH) */ +static volatile uint32_t g_snap_drop; /* 满时丢弃计数 (调试用) */ + +/*=========================================================================== + * 运行时分区表 (ROADMAP 2026-08-12: JEDEC ID + 事件区大小 → 快照区 = 剩余) + *===========================================================================*/ +SnapPart g_snap_part = { + OFFLOG_CHIP_W25Q32, /* 默认 */ + 0x110000UL, /* W25Q32: 0x090000 + 512KB 事件区 */ + 0x2F0000UL, /* 3008KB = 4MB - 576KB - 512KB */ + 751, /* 3008KB/4096 - 1 头扇区 */ + 48064, /* 751 * 64 */ +}; + +/* 芯片总容量 (W25Q32 4MB → W25Q256 32MB) */ +static uint32_t snap_chip_total(uint8_t chip) +{ + switch (chip) { + case OFFLOG_CHIP_W25Q32: return 0x00400000UL; + case OFFLOG_CHIP_W25Q64: return 0x00800000UL; + case OFFLOG_CHIP_W25Q128: return 0x01000000UL; + case OFFLOG_CHIP_W25Q256: return 0x02000000UL; + default: return 0x00400000UL; /* 未知→默认 4MB */ + } +} + +/* 读 JEDEC ID → 填分区表。事件区大小与 offlog 同一事实来源 (g_offlog_part), + 快照区 = 总容量 - 固定区 - 事件区。未知芯片按 W25Q32 兜底 */ +static void snap_part_detect(void) +{ + uint8_t dev = SPI_Flash_ReadJEDEC_ID(); + uint8_t chip = OFFLOG_CHIP_W25Q32; + uint32_t evt_size; + uint32_t total; + + switch (dev) { + case OFFLOG_CHIP_W25Q32: chip = dev; break; + case OFFLOG_CHIP_W25Q64: chip = dev; break; + case OFFLOG_CHIP_W25Q128: chip = dev; break; + case OFFLOG_CHIP_W25Q256: chip = dev; break; + default: chip = OFFLOG_CHIP_W25Q32; break; + } + evt_size = g_offlog_part.area_size; /* 与事件区严格一致 */ + total = snap_chip_total(chip); + + g_snap_part.chip = chip; + g_snap_part.area_base = SNAP_FIXED_SIZE + evt_size; + g_snap_part.area_size = total - SNAP_FIXED_SIZE - evt_size; + g_snap_part.data_sectors = (g_snap_part.area_size / 4096UL) - 1; + g_snap_part.max_records = g_snap_part.data_sectors * SNAP_REC_PER_SECTOR; +} + +/*=========================================================================== + * 基础读写 + *===========================================================================*/ +static void snap_head_write(const SnapHead *h) +{ + uint8_t buf[32]; + memset(buf, 0xFF, sizeof(buf)); + memcpy(buf, h, sizeof(SnapHead)); + SPI_Flash_Write(buf, g_snap_part.area_base, sizeof(buf)); +} + +static void snap_head_read(SnapHead *h) +{ + uint8_t buf[32]; + SPI_Flash_Read(buf, g_snap_part.area_base, sizeof(buf)); + memcpy(h, buf, sizeof(SnapHead)); +} + +static int snap_rec_read_at(uint32_t abs_off, SnapRec *out) +{ + SPI_Flash_Read((uint8_t *)out, abs_off, SNAP_REC_SIZE); + return (out->magic == SNAP_MAGIC) ? 0 : -1; +} + +/*=========================================================================== + * 上电扫描恢复 (与 offlog 同模式: 从头记录写位置向后扫, magic + seq 连续) + *===========================================================================*/ +static void snap_scan_recover(const SnapHead *h) +{ + uint32_t pos = h->wr_off; + uint32_t scanned = 0; + uint32_t expect_seq = h->wr_seq; + SnapRec r; + + /* 防御: wr_off 必须在数据区范围内且 64B 对齐 */ + if (pos < SNAP_DATA_BASE || pos >= g_snap_part.area_base + g_snap_part.area_size) { + pos = SNAP_DATA_BASE; + } + if ((pos - SNAP_DATA_BASE) % SNAP_REC_SIZE != 0) { + pos = SNAP_DATA_BASE; + } + + while (scanned < SNAP_MAX_RECORDS) { + if (snap_rec_read_at(pos, &r) != 0) { + break; /* 找到真实写位置 */ + } + if (expect_seq != 0 && r.seq != expect_seq) { + break; /* seq 不连续 (损坏/残留) → 视为写位置 */ + } + pos += SNAP_REC_SIZE; + if (pos >= SNAP_DATA_BASE + SNAP_DATA_SIZE) { + pos = SNAP_DATA_BASE; /* 回绕数据区 */ + } + scanned++; + expect_seq++; + } + + _wr_off = pos; + _wr_sector = (uint16_t)((pos - SNAP_DATA_BASE) / 4096) + 1; /* 区内 1..n */ + _wr_seq = expect_seq; + _count = h->count + scanned; + if (_count > SNAP_MAX_RECORDS) _count = SNAP_MAX_RECORDS; + _boot_seq = (uint16_t)(h->boot_seq + 1); + if (_boot_seq == 0) _boot_seq = 1; +} + +/*=========================================================================== + * 初始化 + *===========================================================================*/ +void snap_init(void) +{ + SnapHead h; + + snap_part_detect(); /* JEDEC ID + 事件区大小 → 快照区容量 */ + memset(&h, 0xFF, sizeof(h)); + snap_head_read(&h); + + if (!(h.magic[0] == SNAP_HEAD_MAGIC0 && h.magic[1] == SNAP_HEAD_MAGIC1 && + h.magic[2] == SNAP_HEAD_MAGIC2 && h.magic[3] == SNAP_HEAD_MAGIC3)) { + /* 全新区: 擦全部数据扇区, 写头 */ + uint32_t i; + for (i = 0; i < SNAP_DATA_SECTOR_CNT; i++) { + SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096 + i); + } + memset(&h, 0xFF, sizeof(h)); + h.magic[0] = SNAP_HEAD_MAGIC0; + h.magic[1] = SNAP_HEAD_MAGIC1; + h.magic[2] = SNAP_HEAD_MAGIC2; + h.magic[3] = SNAP_HEAD_MAGIC3; + h.boot_seq = 0; + h.wr_sector = 1; + h.wr_off = SNAP_DATA_BASE; + h.wr_seq = 0; + h.count = 0; + snap_head_write(&h); + + _wr_off = SNAP_DATA_BASE; + _wr_sector = 1; + _wr_seq = 1; + _count = 0; + _boot_seq = 1; + } else { + snap_scan_recover(&h); + } + + /* 回写头: boot_seq 递增 */ + h.magic[0] = SNAP_HEAD_MAGIC0; + h.magic[1] = SNAP_HEAD_MAGIC1; + h.magic[2] = SNAP_HEAD_MAGIC2; + h.magic[3] = SNAP_HEAD_MAGIC3; + h.boot_seq = _boot_seq; + h.wr_sector = _wr_sector; + h.wr_off = _wr_off; + h.wr_seq = _wr_seq; + h.count = _count; + snap_head_write(&h); + + g_snap_rd = 0; + g_snap_wr = 0; + g_snap_count = 0; + g_snap_drop = 0; + _ready = 1; +} + +/*=========================================================================== + * 落盘 (主循环上下文) + *===========================================================================*/ +static void snap_flush_head(void) +{ + SnapHead h; + h.magic[0] = SNAP_HEAD_MAGIC0; + h.magic[1] = SNAP_HEAD_MAGIC1; + h.magic[2] = SNAP_HEAD_MAGIC2; + h.magic[3] = SNAP_HEAD_MAGIC3; + h.boot_seq = _boot_seq; + h.wr_sector = _wr_sector; + h.wr_off = _wr_off; + h.wr_seq = _wr_seq; + h.count = _count; + snap_head_write(&h); +} + +/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic */ +static int snap_sector_has_data(uint32_t abs_sec_off) +{ + SnapRec r; + SPI_Flash_Read((uint8_t *)&r, abs_sec_off, SNAP_REC_SIZE); + return (r.magic == SNAP_MAGIC) ? 1 : 0; +} + +/* 写一条记录 (仅主循环; rec->seq 已由 snap_flush 分配) */ +static void snap_write_raw(const SnapRec *rec) +{ + /* 环形回绕: 写指针越过数据区末尾 → 回到区首 */ + if (_wr_off >= SNAP_DATA_BASE + SNAP_DATA_SIZE) { + _wr_off = SNAP_DATA_BASE; + } + + /* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区 */ + { + uint32_t sec_index = (_wr_off - SNAP_DATA_BASE) / 4096; /* 0..n-2 */ + if ((uint32_t)(_wr_sector - 1) != sec_index) { + uint32_t target_abs = (SNAP_DATA_BASE / 4096 + sec_index) * 4096; + if (snap_sector_has_data(target_abs)) { + uint32_t erased = (_count > SNAP_REC_PER_SECTOR) + ? SNAP_REC_PER_SECTOR : _count; + _count -= erased; + } + SPI_Flash_Erase_Sector(target_abs / 4096); + _wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..n */ + snap_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */ + } + } + + SPI_Flash_Write_NoCheck((uint8_t *)rec, _wr_off, SNAP_REC_SIZE); + _wr_off += SNAP_REC_SIZE; + _wr_seq++; + if (_count < SNAP_MAX_RECORDS) _count++; +} + +/*=========================================================================== + * 打包 (与 0xC0 线上线圈单元 12B 格式一致) + *===========================================================================*/ +static void snap_pack_coil(const LUP_CoilSensor *cs, uint8_t *o) +{ + o[0] = (uint8_t)((cs->freq_level << 6) | (cs->direction << 5) + | (cs->freq_type << 4) | (cs->sensitivity & 0x0F)); + o[1] = (uint8_t)((cs->condition << 4) | (cs->loop_state << 3) + | (cs->car_state << 2) | (cs->misc_type & 0x03)); + o[2] = (uint8_t)(cs->freq & 0xFF); + o[3] = (uint8_t)((cs->freq >> 8) & 0xFF); + o[4] = (uint8_t)((cs->freq >> 16) & 0xFF); + o[5] = (uint8_t)(cs->variation & 0xFF); + o[6] = (uint8_t)((cs->variation >> 8) & 0xFF); + o[7] = (uint8_t)((cs->variation >> 16) & 0xFF); + o[8] = (uint8_t)(cs->misc.passtime_ms & 0xFF); + o[9] = (uint8_t)((cs->misc.passtime_ms >> 8) & 0xFF); + o[10] = (uint8_t)((cs->misc.passtime_ms >> 16) & 0xFF); + o[11] = (uint8_t)((cs->misc.passtime_ms >> 24) & 0xFF); +} + +/* 打包传感帧 → SnapRec (seq 留待 flush 时分配; ts_ms/boot_seq 用采集时刻) */ +static void snap_pack(const LUP_SensorReport *sr, SnapRec *rec) +{ + uint8_t i, n; + + memset(rec, 0, sizeof(*rec)); + rec->magic = SNAP_MAGIC; + n = (sr->coil_count > SNAP_COILS_MAX) ? SNAP_COILS_MAX : sr->coil_count; + rec->len = n * SNAP_COIL_BYTES; + rec->ts_ms = mstick(); + rec->boot_seq = _boot_seq; + for (i = 0; i < n; i++) { + snap_pack_coil(&sr->coils[i], rec->coils + i * SNAP_COIL_BYTES); + } +} + +/*=========================================================================== + * 中断安全入队 (USART2 ISR 上下文; 无 SPI, 无阻塞; 满丢新不覆盖) + * - 生产者只写 g_snap_wr / g_snap_pending / g_snap_count + * - 消费者只读 g_snap_rd; count 为 volatile 字节, 单核单指令读写天然原子 + * - 满判定: count >= DEPTH (容量 = SNAP_RAM_DEPTH) + *===========================================================================*/ +void snap_enqueue(const LUP_SensorReport *sr) +{ + SnapRec rec; + + if (!_ready || sr == NULL) return; + + snap_pack(sr, &rec); + + if (g_snap_count >= SNAP_RAM_DEPTH) { + g_snap_drop++; /* 暂存满 → 丢新帧 (快照环形, 可接受) */ + return; + } + g_snap_pending[g_snap_wr] = rec; + g_snap_wr = (uint8_t)((g_snap_wr + 1) & (SNAP_RAM_DEPTH - 1)); + g_snap_count++; +} + +/*=========================================================================== + * 主循环落盘 (iot_mqtt_publish_sensor 每轮调用; 断网/未使能照常落) + * - seq 在主循环分配 (中断不碰 _wr_seq, 避免共享状态) + *===========================================================================*/ +void snap_flush(void) +{ + if (!_ready) return; + + while (g_snap_count > 0) { + SnapRec *rec = &g_snap_pending[g_snap_rd]; + rec->seq = _wr_seq; /* 主循环独占分配 */ + snap_write_raw(rec); + g_snap_rd = (uint8_t)((g_snap_rd + 1) & (SNAP_RAM_DEPTH - 1)); + g_snap_count--; + } + if (g_snap_drop != 0) { + g_snap_drop = 0; /* 丢弃计数清零 (调试观察点) */ + } +} + +/*=========================================================================== + * 查询 + *===========================================================================*/ +uint8_t snap_enabled(void) +{ + return _ready; +} + +uint32_t snap_count(void) +{ + return _count; +} + +uint32_t snap_seq_last(void) +{ + return (_wr_seq > 0) ? (_wr_seq - 1) : 0; +} + +uint32_t snap_boot_seq(void) +{ + return _boot_seq; +} + +int snap_read_idx(uint32_t idx, SnapRec *out) +{ + uint32_t cnt = _count; + if (!_ready || idx >= cnt) return -1; + + /* 逻辑首 = 写位置向前 cnt 条 (环形) */ + uint32_t phys = (_wr_off - SNAP_DATA_BASE); /* 0..DATA_SIZE */ + uint32_t start = (phys + SNAP_DATA_SIZE - cnt * SNAP_REC_SIZE) % SNAP_DATA_SIZE; + uint32_t read_off = SNAP_DATA_BASE + (start + idx * SNAP_REC_SIZE) % SNAP_DATA_SIZE; + + return snap_rec_read_at(read_off, out); +} + +/*=========================================================================== + * 清空 (审计: 事件流记一条 LOG_CLEAR, payload[0]=2=快照流) + *===========================================================================*/ +void snap_clear(void) +{ + uint32_t i; + if (!_ready) return; + + for (i = 0; i < SNAP_DATA_SECTOR_CNT; i++) { + SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096 + i); + } + _wr_off = SNAP_DATA_BASE; + _wr_sector = 1; + _count = 0; + /* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证快照全局唯一 */ + + { + uint8_t p[1] = {2}; /* 2 = 快照流 (审计区分) */ + offlog_evt(OFFLOG_EVT_LOG_CLEAR, p, 1); + } + snap_flush_head(); +} diff --git a/vd960DBN/docs/devlog.md b/vd960DBN/docs/devlog.md index 67bbce5..177f65d 100644 --- a/vd960DBN/docs/devlog.md +++ b/vd960DBN/docs/devlog.md @@ -6,6 +6,34 @@ --- +## 2026-08-12 — 传感快照区落地(snapshot.c,BLE 新增 0x28/0x29/0x2A) + +### 背景 + +ROADMAP P1.2 分区规划(参数区→OTA→事件日志区→传感快照区)中,事件区已落地(offlog),快照区此前为预留。本次实现快照流:0xC0 传感帧按上报节奏落盘,断网期间波形照常记录,可离线回放。 + +### 关键设计决策 + +1. **64B 定长记录 SnapRec**:头部 16B(magic=0xA6/len/flags/seq/ts_ms/boot_seq)+ 4×12B 线圈数据(与 0xC0 线上格式逐字节一致,可直接对照抓包)→ W25Q32 下 48064 条。 +2. **线程安全(踩坑预警)**:`iot_sensor_ingest()` 在 **USART2 ISR 上下文**(`loop_uart_proto.h:209` 明确标注)被调用!SPI 擦除 ~45ms 绝不能在中断里做 → 中断只打包+入 RAM 暂存(8 深,满丢新),主循环 `iot_mqtt_publish_sensor()` 每轮 `snap_flush()` 落盘。单生产者(中断)单消费者(主循环)count 计数环形,volatile 字节天然原子。 +3. **分区表同一事实来源**:快照区 = 总容量 − 固定区 576KB − 事件区(`g_offlog_part.area_size`),杜绝两模块对事件区大小理解不一致。 +4. **审计**:快照清除写事件流 `log_clear`(payload[0]=2=快照流)。 +5. **BLE 指令对齐 OFFLOG 模式**:SNAP_STAT(0x28)/SNAP_QUERY(0x29)/SNAP_CLEAR(0x2A);QUERY 上限 2 条(64×2+2=130B ≤ 单包缓冲,ODR 教训防御)。 + +### 测试 + +`test_snapshot.c` 8 例全过(中断安全/打包格式/环形回绕/掉电恢复/扇区切换/暂存满丢新/清空审计/分区表);`test_offlog` 8 例、`test_ble_offlog` 7 例回归全过。 + +> ⚠ 测试踩坑:不要在同文件 include offlog.c + snapshot.c —— 两个 .c 的 static 变量(如 `_wr_off`)在同一翻译单元**符号冲突**,快照 clear 审计会污染事件写指针。测试改为 mock `g_offlog_part` + `offlog_evt`。 + +### 待板上验证 + +- 快照区 0x110000 起、W25Q32 48064 条 capacity 打印 +- 中断风暴下暂存满丢新行为(观察点:`g_snap_drop`) +- 掉电恢复跨扇区扫描 + +--- + ## 2026-08-12 — BLE 分包粒度动态化修复(Too large noti 丢包) ### 背景 diff --git a/vd960DBN/tests/test_snapshot.c b/vd960DBN/tests/test_snapshot.c new file mode 100644 index 0000000..dfe71c7 --- /dev/null +++ b/vd960DBN/tests/test_snapshot.c @@ -0,0 +1,353 @@ +/** + * test_snapshot.c — 传感快照日志 gcc 隔离单测 + * + * 编译: gcc -I../BLE/OnlyUpdateApp_Peripheral/APP/include \ + * -o test_snapshot test_snapshot.c + * 运行: ./test_snapshot + * + * Mock: W25Q32 NOR 行为 (写=AND, 擦=0xFF), mstick 固定值, + * g_offlog_part (事件区 512KB), offlog_evt (审计记录 mock) + * 注意: 不 include offlog.c — 两个 .c 的 static 变量 (如 _wr_off) 在 + * 同一翻译单元会符号冲突 (踩过: snap_clear 审计写乱了快照写指针) + * 覆盖: 全新初始化 / 中断安全(enqueue 不落盘) / flush 落盘 / 打包格式 + * 环形回绕 / 掉电恢复 / 扇区切换 / 暂存满丢新 / 清空审计 / 分区表 + */ + +#include +#include +#include +#include "../BLE/OnlyUpdateApp_Peripheral/APP/include/offlog.h" + +/* ============ mock W25Q32 (4MB, NOR 语义) ============ */ +static uint8_t flash[4 * 1024 * 1024]; + +void SPI_Flash_Erase_Sector(uint32_t sec) +{ + memset(flash + sec * 4096, 0xFF, 4096); +} + +void SPI_Flash_Read(uint8_t *buf, uint32_t addr, uint16_t size) +{ + memcpy(buf, flash + addr, size); +} + +void SPI_Flash_Write_NoCheck(uint8_t *buf, uint32_t addr, uint16_t size) +{ + uint16_t i; + for (i = 0; i < size; i++) flash[addr + i] &= buf[i]; /* NOR: 只能 1→0 */ +} + +uint8_t SPI_Flash_ReadJEDEC_ID(void) { return 0x16; } /* mock W25Q32 */ + +void SPI_Flash_Write(uint8_t *buf, uint32_t addr, uint16_t size) +{ + /* 模拟 storage.c 行为: 目标区域非全 FF 则擦整扇区再写 */ + uint16_t i; + int need_erase = 0; + for (i = 0; i < size; i++) { + if (flash[addr + i] != 0xFF) { need_erase = 1; break; } + } + if (need_erase) SPI_Flash_Erase_Sector(addr / 4096); + SPI_Flash_Write_NoCheck(buf, addr, size); +} + +/* ============ mock 时间 ============ */ +static uint32_t mock_ms = 0; +uint32_t mstick(void) { return mock_ms; } + +/* ============ mock offlog 依赖 (不 include offlog.c, 避免 static 冲突) ============ */ +OfflogPart g_offlog_part = { + OFFLOG_CHIP_W25Q32, /* W25Q32 */ + 0x80000UL, /* 事件区 512KB */ + 127, + 16256, +}; +static uint8_t g_audit_type; +static uint8_t g_audit_payload[4]; +static uint8_t g_audit_len; +void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len) +{ + g_audit_type = type; + g_audit_len = len; + if (payload && len > 0 && len <= 4) memcpy(g_audit_payload, payload, len); +} + +/* ch32v20x 库类型 (单测环境无 SDK) */ +typedef unsigned char u8; + +/* ============ 被测模块 (直接包含实现) ============ */ +#include "../BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c" + +/* ============ 断言 ============ */ +static int failures = 0; +#define CHECK(cond) do { \ + if (!(cond)) { printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); failures++; } \ +} while (0) + +static void reset_flash(void) +{ + memset(flash, 0xFF, sizeof(flash)); + mock_ms = 0; +} + +/* 构造一帧传感报告: 4 线圈, 各线圈填可辨识值 */ +static void make_report(LUP_SensorReport *sr, uint32_t base_freq, int32_t base_var) +{ + uint8_t i; + memset(sr, 0, sizeof(*sr)); + sr->sens_type = 0x0C; + sr->coil_count = 4; + for (i = 0; i < 4; i++) { + sr->coils[i].freq_level = 2; /* 中低 66nF */ + sr->coils[i].direction = 1; + sr->coils[i].freq_type = 0; + sr->coils[i].sensitivity = 9; + sr->coils[i].condition = 5; + sr->coils[i].loop_state = 1; + sr->coils[i].car_state = (i & 1); /* 0/1 交替 */ + sr->coils[i].misc_type = 0; + sr->coils[i].freq = base_freq + i * 1000; + sr->coils[i].variation = base_var + i * 100; + sr->coils[i].misc.passtime_ms = 0x10000000 + i; + } +} + +/* 测试1: 全新初始化 → enqueue 不落盘(中断安全), flush 落盘可读回 */ +static void test_fresh_init_and_flush(void) +{ + LUP_SensorReport sr; + SnapRec r; + uint32_t i; + + reset_flash(); + snap_init(); + CHECK(snap_enabled() == 1); + CHECK(snap_count() == 0); + + make_report(&sr, 0x123456, 1000); + mock_ms = 12345; + + /* 中断路径: 只 enqueue, 不 flush → flash 不得有任何快照数据 */ + snap_enqueue(&sr); + CHECK(snap_count() == 0); /* 未落盘 */ + for (i = 0; i < 64; i++) { + CHECK(flash[SNAP_DATA_BASE + i] == 0xFF); /* 数据区仍全 FF */ + } + + /* 主循环: flush → 落盘 1 条 */ + snap_flush(); + CHECK(snap_count() == 1); + CHECK(snap_read_idx(0, &r) == 0); + CHECK(r.magic == SNAP_MAGIC); + CHECK(r.len == 48); /* 4 线圈 × 12B */ + CHECK(r.seq == 1); + CHECK(r.boot_seq == 1); + CHECK(r.ts_ms == 12345); + CHECK(snap_seq_last() == 1); + + printf("PASS test_fresh_init_and_flush\n"); +} + +/* 测试2: 打包格式与 0xC0 线上线圈单元 12B 一致 */ +static void test_pack_format(void) +{ + LUP_SensorReport sr; + SnapRec r; + + reset_flash(); + snap_init(); + make_report(&sr, 0x123456, -12345); /* 负变化量 */ + /* 覆盖值: freq_level=2 direction=1 freq_type=0 sens=9 → o[0]=0xA9 + condition=5 loop=1 car=0 misc=3 → o[1]=0x5B + freq=0x123456 → 56 34 12 + variation=-12345 → 0xFFFFCFC7 截 24bit → C7 CF FF + misc=0x10000000 → 00 00 00 10 */ + sr.coils[0].freq = 0x123456; + sr.coils[0].variation = -12345; + sr.coils[0].misc_type = 3; + sr.coils[0].misc.relay_count = 0x10000000; + + snap_enqueue(&sr); + snap_flush(); + snap_read_idx(0, &r); + + CHECK(r.coils[0] == 0xA9); + CHECK(r.coils[1] == 0x5B); + CHECK(r.coils[2] == 0x56 && r.coils[3] == 0x34 && r.coils[4] == 0x12); + CHECK(r.coils[5] == 0xC7 && r.coils[6] == 0xCF && r.coils[7] == 0xFF); + CHECK(r.coils[8] == 0x00 && r.coils[9] == 0x00 && r.coils[10] == 0x00 && r.coils[11] == 0x10); + + /* 线圈 2: car_state=1 → o[1] bit2=1 → 0x5F (其他字段相同) */ + CHECK((r.coils[12 + 1] & 0x04) == 0x04); + CHECK((r.coils[0 + 1] & 0x04) == 0x00); + + printf("PASS test_pack_format\n"); +} + +/* 测试3: 环形回绕 — 写满 max+5 条, count 封顶, 序号单调 */ +static void test_ring_wrap(void) +{ + LUP_SensorReport sr; + SnapRec r; + uint32_t i, n = SNAP_MAX_RECORDS + 5; + + reset_flash(); + snap_init(); + make_report(&sr, 0x100, 100); + for (i = 0; i < n; i++) { + snap_enqueue(&sr); + snap_flush(); + } + CHECK(snap_count() == SNAP_MAX_RECORDS - SNAP_REC_PER_SECTOR + 5); + CHECK(snap_read_idx(snap_count() - 1, &r) == 0); + CHECK(r.seq == n); + CHECK(snap_seq_last() == n); + CHECK(snap_read_idx(snap_count(), &r) == -1); /* 越界 */ + printf("PASS test_ring_wrap (count %lu, max %lu)\n", + (unsigned long)snap_count(), (unsigned long)SNAP_MAX_RECORDS); +} + +/* 测试4: 掉电恢复 — 写 10 条后重新 init, boot_seq 递增, seq 连续 */ +static void test_power_loss_recovery(void) +{ + LUP_SensorReport sr; + SnapRec r; + uint32_t i; + + reset_flash(); + snap_init(); + make_report(&sr, 0x200, 200); + for (i = 0; i < 10; i++) { + snap_enqueue(&sr); + snap_flush(); + } + CHECK(snap_count() == 10); + + snap_init(); /* 模拟掉电重启 */ + CHECK(snap_boot_seq() == 2); + CHECK(snap_count() == 10); + + snap_enqueue(&sr); + snap_flush(); + CHECK(snap_count() == 11); + CHECK(snap_read_idx(10, &r) == 0); + CHECK(r.seq == 11); + CHECK(r.boot_seq == 2); + printf("PASS test_power_loss_recovery\n"); +} + +/* 测试5: 扇区切换 — 写满 64 条后头扇区更新, 数据完整 */ +static void test_sector_switch(void) +{ + LUP_SensorReport sr; + SnapRec r; + SnapHead h; + uint32_t i; + + reset_flash(); + snap_init(); + make_report(&sr, 0x300, 300); + for (i = 0; i < 64; i++) { + snap_enqueue(&sr); + snap_flush(); + } + snap_enqueue(&sr); + snap_flush(); /* 第 65 条触发扇区切换 */ + + CHECK(snap_count() == 65); + SPI_Flash_Read((uint8_t *)&h, g_snap_part.area_base, sizeof(h)); + CHECK(h.magic[0] == SNAP_HEAD_MAGIC0); + CHECK(h.wr_sector == 2); + CHECK(h.wr_off == SNAP_DATA_BASE + 4096); + + CHECK(snap_read_idx(63, &r) == 0); + CHECK(r.seq == 64); + CHECK(snap_read_idx(64, &r) == 0); + CHECK(r.seq == 65); + printf("PASS test_sector_switch\n"); +} + +/* 测试6: RAM 暂存满丢新 — 8 深, 第 9 条丢, flush 只落 8 条 */ +static void test_ram_overflow_drop(void) +{ + LUP_SensorReport sr; + SnapRec r; + uint32_t i; + + reset_flash(); + snap_init(); + make_report(&sr, 0x400, 400); + + for (i = 0; i < SNAP_RAM_DEPTH + 3; i++) { /* 11 条, 深 8 */ + snap_enqueue(&sr); /* 不 flush, 模拟中断风暴 */ + } + CHECK(snap_count() == 0); /* 全部还在 RAM */ + snap_flush(); + CHECK(snap_count() == 8); /* 只落 8 条, 丢 3 条 */ + CHECK(snap_read_idx(0, &r) == 0); + CHECK(r.seq == 1); /* 丢的是最新(尾部) */ + CHECK(snap_read_idx(7, &r) == 0); + CHECK(r.seq == 8); + printf("PASS test_ram_overflow_drop\n"); +} + +/* 测试7: 清空 — 擦数据区 + 事件流审计 (payload[0]=2=快照流) */ +static void test_clear_audit(void) +{ + LUP_SensorReport sr; + SnapRec r; + uint32_t i; + + reset_flash(); + g_audit_type = 0; g_audit_len = 0; memset(g_audit_payload, 0, sizeof(g_audit_payload)); + snap_init(); + make_report(&sr, 0x500, 500); + for (i = 0; i < 5; i++) { + snap_enqueue(&sr); + snap_flush(); + } + CHECK(snap_count() == 5); + + snap_clear(); + CHECK(snap_count() == 0); + CHECK(snap_seq_last() == 5); /* seq 不重置 */ + CHECK(snap_read_idx(0, &r) == -1); /* 数据区已空 */ + + /* 审计: offlog_evt 被调用, type=LOG_CLEAR, payload[0]=2 */ + CHECK(g_audit_type == OFFLOG_EVT_LOG_CLEAR); + CHECK(g_audit_len == 1); + CHECK(g_audit_payload[0] == 2); + printf("PASS test_clear_audit\n"); +} + +/* 测试8: 运行时分区 — W25Q32 快照区 0x110000, 3008KB, 48064 条 */ +static void test_part_table(void) +{ + reset_flash(); + snap_init(); + CHECK(g_snap_part.area_base == 0x110000UL); + CHECK(g_snap_part.area_size == 0x2F0000UL); /* 3008KB */ + CHECK(g_snap_part.data_sectors == 751); + CHECK(g_snap_part.max_records == 751 * 64); + CHECK(SNAP_DATA_BASE == 0x111000UL); /* base + 4KB 头扇区 */ + printf("PASS test_part_table\n"); +} + +int main(void) +{ + test_fresh_init_and_flush(); + test_pack_format(); + test_ring_wrap(); + test_power_loss_recovery(); + test_sector_switch(); + test_ram_overflow_drop(); + test_clear_audit(); + test_part_table(); + + if (failures) { + printf("\n%d FAILURE(S)\n", failures); + return 1; + } + printf("\nALL PASS\n"); + return 0; +}