Files
vd_960/vd960DBN/BLE/OnlyUpdateApp_Peripheral/APP/snapshot.c
T
wangfq f1c9358aad feat(vd960DBN): TCP/MQTT log_* 命令支持快照流 stream=snapshot (2026-08-18)
- snapshot.c/h: 新增 snap_rec_to_json() — SnapRec 64B → JSON (channels 对齐 0xC0, variation 3B 符号扩展, misc_type 全枚举)
- tcp_json_srv.c: handle_log_stat/query/clear 加 stream 解析 + snapshot 分支
- iot_mqtt_srv.c: log_stat/query/clear 加 stream 解析 + snapshot 分支
- 事件流 count=0 按上限处理 (与 BLE 对齐); 快照 QUERY count≤2, CLEAR ~45ms
- 新增 gcc 隔离单测 tests/test_snap_to_json.c 34 断言全过
- devlog V4.1
2026-08-18 09:01:22 +08:00

483 lines
19 KiB
C

/**
******************************************************************************
* @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 <string.h>
#include <stdio.h>
/* 编译期断言: 记录必须是 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)) {
/* 全新区: 懒擦 — 只擦当前写扇区 (数据扇区1) ~45ms, 其余扇区由环形写
切扇区时自动擦。原擦全部 751 扇区 ~34s → 主循环阻塞超 IWDG 4s → 复位 */
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096);
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;
}
/*===========================================================================
* 延后初始化 (2026-08-17): 开机 SNAP_START_DELAY_MS 后首次进入时执行一次。
* 目的: 避开启动早期 SPI 重负载窗口 (擦扇区 + 整扇区写回 ~80ms), 该窗口
* 疑似触发 VDD 跌落/跑飞 (08-17 诊断: BOOT_CNT 恒=1, RSTSCKR 无标志)。
* 附带: 3s 内的传感帧由 snap_enqueue/snap_flush 的 !_ready 门控自然丢弃,
* 不落盘 (开机初期串口数据视为无效快照)。
* 主循环每轮调用, 非阻塞 (未到延时/已初始化直接 return)。
*===========================================================================*/
#define SNAP_START_DELAY_MS 3000
void snap_delayed_init(void)
{
if (_ready) return; /* 已初始化 */
if (mstick() < SNAP_START_DELAY_MS) return; /* 未到延时窗口 */
snap_init();
}
/*===========================================================================
* 落盘 (主循环上下文)
*===========================================================================*/
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);
}
}
/*===========================================================================
* 入队 (主循环上下文: uart_srv → lup_process_frame → iot_sensor_ingest)
* USART2 ISR 只逐字节喂 lup_feed_byte, 完整帧由主循环 uart_srv 处理 →
* 本函数不在中断里, 但保留 RAM 暂存 + flush 两段式: 批量落盘省 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)
{
if (!_ready) return;
/* 逻辑清除 + 只擦写指针起点扇区 (~45ms): count=0 使旧数据不可读,
其余扇区由环形写覆盖时自动擦。原擦全部 751 扇区 ~34s > IWDG 4s → 复位 */
SPI_Flash_Erase_Sector(SNAP_DATA_BASE / 4096);
_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();
}
/* SnapRec -> JSON object (channels aligned with 0xC0 wire format).
Used by TCP JSON / MQTT log_query stream=snapshot (protocol V1.03/V1.07).
Returns snprintf written length; caller must ensure buf_len enough. */
int snap_rec_to_json(const SnapRec *r, char *buf, int buf_len)
{
int coil_count = (r->len > (SNAP_COILS_MAX * SNAP_COIL_BYTES))
? SNAP_COILS_MAX : (r->len / SNAP_COIL_BYTES);
if (coil_count < 0) coil_count = 0;
int pos = snprintf(buf, buf_len,
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"coil_count\":%u,\"channels\":[",
(unsigned long)r->seq, (unsigned)r->boot_seq,
(unsigned long)r->ts_ms, (unsigned)coil_count);
int c;
for (c = 0; c < coil_count; c++) {
const uint8_t *p = &r->coils[c * SNAP_COIL_BYTES];
uint8_t cfg = p[0];
uint8_t cond = p[1];
uint32_t freq = (uint32_t)p[2] | ((uint32_t)p[3] << 8) | ((uint32_t)p[4] << 16);
int32_t variation = (int32_t)((uint32_t)p[5] | ((uint32_t)p[6] << 8) | ((uint32_t)p[7] << 16));
uint32_t misc = (uint32_t)p[8] | ((uint32_t)p[9] << 8)
| ((uint32_t)p[10] << 16) | ((uint32_t)p[11] << 24);
uint8_t freq_level = (cfg >> 6) & 0x03;
uint8_t direction = (cfg >> 5) & 0x01;
uint8_t freq_type = (cfg >> 4) & 0x01;
uint8_t sensitivity = cfg & 0x0F;
uint8_t condition = (cond >> 4) & 0x0F;
uint8_t loop_state = (cond >> 3) & 0x01; /* 0=normal 1=cut */
uint8_t car_state = (cond >> 2) & 0x01; /* 0=nocar 1=car */
uint8_t misc_type = cond & 0x03;
const char *fl = "high";
const char *mt = "time";
if (variation & 0x800000) variation |= (int32_t)0xFF000000; /* 3B sign extend */
if (freq_level == 1) fl = "mid_high";
else if (freq_level == 2) fl = "mid_low";
else if (freq_level == 3) fl = "low";
if (misc_type == 1) mt = "cut_count";
else if (misc_type == 2) mt = "flow_count";
else if (misc_type == 3) mt = "relay_count";
if (c > 0) pos += snprintf(buf + pos, buf_len - pos, ",");
pos += snprintf(buf + pos, buf_len - pos,
"{\"ch\":%u,\"freq_level\":\"%s\",\"direction\":%u,\"freq_type\":%u,"
"\"sensitivity\":%u,\"condition\":%u,\"loop_ok\":%s,\"has_car\":%s,"
"\"misc_type\":\"%s\",\"freq\":%lu,\"variation\":%ld,\"misc\":%lu}",
(unsigned)(c + 1), fl, (unsigned)direction, (unsigned)freq_type,
(unsigned)sensitivity, (unsigned)condition,
loop_state ? "false" : "true", car_state ? "true" : "false",
mt, (unsigned long)freq, (long)variation, (unsigned long)misc);
}
pos += snprintf(buf + pos, buf_len - pos, "]}");
return pos;
}