先存后刷: MQTT 分片下载 → W25Qxx OTA 暂存区 (0x010000 512KB) → 全镜像 CRC32 复核 → 本地 0x9F ISP 透传刷写 Loop MCU (AT32F421) - 新增 ota_srv.c/h: CRC32(ISO-HDLC 逐位零表)/OtaMeta 双备份(节流 4KB flush)/ 会话状态机(begin/data/end/abort/flash)/本地刷写状态机(非阻塞 tick: A5→A6→A7 停等 ACK 1s×3)/安全窗口(有车拒绝 force 跳过)/失败 event_report ota_error - iot_mqtt_srv.c: 6 个 ota_* 命令分发 + 会话静默(event_report 积压/offlog·快照 落盘暂停, 结束补发) + IOT_EVT_OTA_ERROR + iot_any_car - usart_biz.c: uart_srv OTA 分支 ACK 分发 (本地刷写→ota_flash_feed_ack, BLE 透传不变) - peripheral_main.c: ota_init + ota_poll 挂主循环 - offlog: OTA_START 0x60/OTA_RESULT 0x61 审计事件 - tests/test_ota_srv.c: gcc 隔离单测 9 组全过 (mock NOR/UART2/时钟, 嵌入真实实现) 抓到 2 个真 bug: ①A7 序号未递减(bootloader 等不到末包) ②重发时 retry 清零(超时永不失败) - 语法检查: ota_srv/iot_mqtt_srv/usart_biz/offlog 0 新增错误 (基线 interrupt 假阳性除外) - devlog 置顶条目 + 待板上验证清单
530 lines
19 KiB
C
530 lines
19 KiB
C
/**
|
|
******************************************************************************
|
|
* @file offlog.c
|
|
* @author wfq
|
|
* @version V1.0
|
|
* @date 2026-08-04
|
|
* @brief DBN 脱机事件日志 — W25Q32 环形实现
|
|
*
|
|
* 依赖: storage.c (SPI_Flash_*), cmcng.h (mstick), net_srv.c (dev_time_now)
|
|
* 所有写操作在主循环上下文调用 (不在中断), SPI 擦除 ~45ms 阻塞可接受。
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "offlog.h"
|
|
#include "storage.h"
|
|
#include "cmcng.h"
|
|
#include "net_srv.h"
|
|
#include <string.h>
|
|
|
|
/* 编译期断言: 记录必须是 32B (定长环形索引依赖, 踩过 36B padding 的坑) */
|
|
typedef char offlog_evt_size_must_be_32[(sizeof(OfflogEvt) == 32) ? 1 : -1];
|
|
typedef char offlog_head_size_must_be_32[(sizeof(OfflogHead) == 32) ? 1 : -1];
|
|
|
|
/*===========================================================================
|
|
* 内部状态
|
|
*===========================================================================*/
|
|
static uint32_t _wr_off; /* 下一条记录写入的绝对地址 */
|
|
static uint16_t _wr_sector; /* 当前写扇区号 (区内 1..63) */
|
|
static uint32_t _wr_seq; /* 下一条记录的全局 seq */
|
|
static uint16_t _boot_seq; /* 当前 boot 序号 */
|
|
static uint32_t _count; /* 有效记录数 (0..max_records) */
|
|
static uint8_t _ready; /* init 完成标志 */
|
|
|
|
/*===========================================================================
|
|
* 运行时分区表 (ROADMAP 2026-08-12: 上电读 JEDEC ID 决定事件区容量)
|
|
*===========================================================================*/
|
|
OfflogPart g_offlog_part = {
|
|
OFFLOG_CHIP_W25Q32, /* 默认 */
|
|
0x80000UL, /* 512KB */
|
|
127, /* (512KB/4096) - 1 头扇区 */
|
|
16256, /* 127 * 128 */
|
|
};
|
|
|
|
/* 读 JEDEC ID → 填分区表。未知芯片按 W25Q32 兜底 (与 storage_init 打印对照) */
|
|
static void offlog_part_detect(void)
|
|
{
|
|
uint8_t dev = SPI_Flash_ReadJEDEC_ID();
|
|
uint32_t size = 0x80000UL;
|
|
uint8_t chip = OFFLOG_CHIP_W25Q32;
|
|
|
|
switch (dev) {
|
|
case OFFLOG_CHIP_W25Q32: size = 0x80000UL; break; /* 512KB */
|
|
case OFFLOG_CHIP_W25Q64: size = 0x100000UL; break; /* 1MB */
|
|
case OFFLOG_CHIP_W25Q128: size = 0x200000UL; break; /* 2MB */
|
|
case OFFLOG_CHIP_W25Q256: size = 0x400000UL; break; /* 4MB */
|
|
default: chip = OFFLOG_CHIP_W25Q32; break; /* 未知→默认 */
|
|
}
|
|
g_offlog_part.chip = chip;
|
|
g_offlog_part.area_size = size;
|
|
g_offlog_part.data_sectors = (size / 4096UL) - 1;
|
|
g_offlog_part.max_records = g_offlog_part.data_sectors * OFFLOG_EVT_PER_SECTOR;
|
|
}
|
|
|
|
/*===========================================================================
|
|
* 基础读写
|
|
*===========================================================================*/
|
|
static void offlog_head_write(const OfflogHead *h)
|
|
{
|
|
uint8_t buf[32];
|
|
memset(buf, 0xFF, sizeof(buf));
|
|
memcpy(buf, h, sizeof(OfflogHead));
|
|
SPI_Flash_Write(buf, OFFLOG_AREA_BASE, sizeof(buf));
|
|
}
|
|
|
|
static void offlog_head_read(OfflogHead *h)
|
|
{
|
|
uint8_t buf[32];
|
|
SPI_Flash_Read(buf, OFFLOG_AREA_BASE, sizeof(buf));
|
|
memcpy(h, buf, sizeof(OfflogHead));
|
|
}
|
|
|
|
static int offlog_evt_read_at(uint32_t abs_off, OfflogEvt *out)
|
|
{
|
|
SPI_Flash_Read((uint8_t *)out, abs_off, OFFLOG_EVT_SIZE);
|
|
return (out->magic == OFFLOG_EVT_MAGIC) ? 0 : -1;
|
|
}
|
|
|
|
/*===========================================================================
|
|
* 上电扫描恢复
|
|
* 从头扇区记录的写位置向后扫, 校验 magic + seq 连续, 找真实写位置。
|
|
* 正常情况 (无掉电) 第一条就无效, 立即恢复; 掉电则向前推进若干条。
|
|
*===========================================================================*/
|
|
static void offlog_scan_recover(const OfflogHead *h)
|
|
{
|
|
uint32_t pos = h->wr_off;
|
|
uint32_t scanned = 0;
|
|
uint32_t expect_seq = h->wr_seq;
|
|
OfflogEvt e;
|
|
|
|
/* 防御: wr_off 必须在数据区范围内且 32B 对齐 */
|
|
if (pos < OFFLOG_DATA_BASE || pos >= OFFLOG_AREA_BASE + OFFLOG_AREA_SIZE) {
|
|
pos = OFFLOG_DATA_BASE;
|
|
}
|
|
if ((pos - OFFLOG_DATA_BASE) % OFFLOG_EVT_SIZE != 0) {
|
|
pos = OFFLOG_DATA_BASE; /* 未对齐 → 从头开始 */
|
|
}
|
|
|
|
while (scanned < OFFLOG_MAX_RECORDS) {
|
|
if (offlog_evt_read_at(pos, &e) != 0) {
|
|
break; /* 找到真实写位置 */
|
|
}
|
|
if (expect_seq != 0 && e.seq != expect_seq) {
|
|
break; /* seq 不连续 (损坏/残留) → 视为写位置 */
|
|
}
|
|
pos += OFFLOG_EVT_SIZE;
|
|
if (pos >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) {
|
|
pos = OFFLOG_DATA_BASE; /* 回绕数据区 */
|
|
}
|
|
scanned++;
|
|
expect_seq++;
|
|
}
|
|
|
|
_wr_off = pos;
|
|
_wr_sector = (uint16_t)((pos - OFFLOG_DATA_BASE) / 4096) + 1; /* 区内 1..63 */
|
|
_wr_seq = expect_seq; /* 下一条 = 最后有效 seq + 1 */
|
|
_count = h->count + scanned;
|
|
if (_count > OFFLOG_MAX_RECORDS) _count = OFFLOG_MAX_RECORDS;
|
|
_boot_seq = (uint16_t)(h->boot_seq + 1);
|
|
if (_boot_seq == 0) _boot_seq = 1;
|
|
}
|
|
|
|
/*===========================================================================
|
|
* 初始化
|
|
*===========================================================================*/
|
|
void offlog_init(void)
|
|
{
|
|
OfflogHead h;
|
|
|
|
offlog_part_detect(); /* JEDEC ID → 事件区容量 (W25Q32/Q64/Q128/Q256) */
|
|
memset(&h, 0xFF, sizeof(h));
|
|
offlog_head_read(&h);
|
|
|
|
if (!(h.magic[0] == OFFLOG_HEAD_MAGIC0 && h.magic[1] == OFFLOG_HEAD_MAGIC1 &&
|
|
h.magic[2] == OFFLOG_HEAD_MAGIC2 && h.magic[3] == OFFLOG_HEAD_MAGIC3)) {
|
|
/* 全新区: 懒擦 — 只擦当前写扇区 ~45ms (原擦全部 127 扇区 ~5.7s,
|
|
主循环阻塞超 IWDG 4s 风险), 其余由环形写切扇区时自动擦 */
|
|
SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096);
|
|
memset(&h, 0xFF, sizeof(h));
|
|
h.magic[0] = OFFLOG_HEAD_MAGIC0;
|
|
h.magic[1] = OFFLOG_HEAD_MAGIC1;
|
|
h.magic[2] = OFFLOG_HEAD_MAGIC2;
|
|
h.magic[3] = OFFLOG_HEAD_MAGIC3;
|
|
h.boot_seq = 0;
|
|
h.wr_sector = 1;
|
|
h.wr_off = OFFLOG_DATA_BASE;
|
|
h.wr_seq = 0;
|
|
h.count = 0;
|
|
offlog_head_write(&h);
|
|
|
|
_wr_off = OFFLOG_DATA_BASE;
|
|
_wr_sector = 1;
|
|
_wr_seq = 1;
|
|
_count = 0;
|
|
_boot_seq = 1;
|
|
} else {
|
|
offlog_scan_recover(&h);
|
|
}
|
|
|
|
/* 回写头: boot_seq 递增 */
|
|
h.magic[0] = OFFLOG_HEAD_MAGIC0;
|
|
h.magic[1] = OFFLOG_HEAD_MAGIC1;
|
|
h.magic[2] = OFFLOG_HEAD_MAGIC2;
|
|
h.magic[3] = OFFLOG_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;
|
|
offlog_head_write(&h);
|
|
|
|
_ready = 1;
|
|
}
|
|
|
|
/*===========================================================================
|
|
* 写一条记录 (主循环上下文)
|
|
*===========================================================================*/
|
|
static void offlog_flush_head(void)
|
|
{
|
|
OfflogHead h;
|
|
h.magic[0] = OFFLOG_HEAD_MAGIC0;
|
|
h.magic[1] = OFFLOG_HEAD_MAGIC1;
|
|
h.magic[2] = OFFLOG_HEAD_MAGIC2;
|
|
h.magic[3] = OFFLOG_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;
|
|
offlog_head_write(&h);
|
|
}
|
|
|
|
/* 目标扇区是否已有数据 (环形覆盖判定): 读首条记录 magic。
|
|
扇区整擦后顺序写, 首条必先写, 故首条 0xA5 即可判定有数据 */
|
|
static int offlog_sector_has_data(uint32_t abs_sec_off)
|
|
{
|
|
OfflogEvt e;
|
|
SPI_Flash_Read((uint8_t *)&e, abs_sec_off, OFFLOG_EVT_SIZE);
|
|
return (e.magic == OFFLOG_EVT_MAGIC) ? 1 : 0;
|
|
}
|
|
|
|
static void offlog_write_raw(const OfflogEvt *e)
|
|
{
|
|
/* 环形回绕: 写指针越过数据区末尾 → 回到区首 */
|
|
if (_wr_off >= OFFLOG_DATA_BASE + OFFLOG_DATA_SIZE) {
|
|
_wr_off = OFFLOG_DATA_BASE;
|
|
}
|
|
|
|
/* 切扇区判定: 写指针所在扇区与 _wr_sector 不一致 → 擦除目标扇区。
|
|
覆盖两种情形: ① 正好落在扇区边界 (sec_pos==0, 上一轮写满)
|
|
② 当前扇区放不下下一条 (sec_pos + 32 > 4096) */
|
|
{
|
|
uint32_t sec_index = (_wr_off - OFFLOG_DATA_BASE) / 4096; /* 0..62 */
|
|
if ((uint32_t)(_wr_sector - 1) != sec_index) {
|
|
uint32_t target_abs = (OFFLOG_DATA_BASE / 4096 + sec_index) * 4096;
|
|
/* 仅环形覆盖 (目标扇区有旧数据) 才扣减 count;
|
|
顺序推进到空扇区擦除无记录损失 */
|
|
if (offlog_sector_has_data(target_abs)) {
|
|
uint32_t erased = (_count > OFFLOG_EVT_PER_SECTOR)
|
|
? OFFLOG_EVT_PER_SECTOR : _count;
|
|
_count -= erased;
|
|
}
|
|
SPI_Flash_Erase_Sector(target_abs / 4096);
|
|
_wr_sector = (uint16_t)(sec_index + 1); /* 区内 1..63 */
|
|
offlog_flush_head(); /* 扇区切换 → 更新头 (掉电恢复锚点) */
|
|
}
|
|
}
|
|
|
|
SPI_Flash_Write_NoCheck((uint8_t *)e, _wr_off, OFFLOG_EVT_SIZE);
|
|
_wr_off += OFFLOG_EVT_SIZE;
|
|
_wr_seq++;
|
|
if (_count < OFFLOG_MAX_RECORDS) _count++;
|
|
}
|
|
|
|
/* 内部: 支持外部指定 unix_ts (TIME_ANCHOR 用平台下发值, 严格一致) */
|
|
static void offlog_evt_ts(uint8_t type, const uint8_t *payload, uint8_t len, uint32_t unix_ts)
|
|
{
|
|
OfflogEvt e;
|
|
uint32_t now = mstick();
|
|
|
|
if (!_ready) return;
|
|
|
|
memset(&e, 0, sizeof(e));
|
|
e.magic = OFFLOG_EVT_MAGIC;
|
|
e.type = type;
|
|
e.len = (len > 12) ? 12 : len;
|
|
e.seq = _wr_seq;
|
|
e.boot_seq= _boot_seq;
|
|
e.ts_ms = now;
|
|
e.unix_ts = unix_ts;
|
|
if (unix_ts >= 1600000000UL) { /* 与 dev_time_sync 合法性门槛一致 */
|
|
e.flags |= OFFLOG_UNIX_VALID_FLAG;
|
|
}
|
|
if (payload != NULL && e.len > 0) {
|
|
memcpy(e.payload, payload, e.len);
|
|
}
|
|
offlog_write_raw(&e);
|
|
}
|
|
|
|
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len)
|
|
{
|
|
offlog_evt_ts(type, payload, len, dev_time_now());
|
|
}
|
|
|
|
/*===========================================================================
|
|
* 便捷事件包装
|
|
*===========================================================================*/
|
|
void offlog_boot(uint32_t reset_reason)
|
|
{
|
|
uint8_t p[4];
|
|
p[0] = (uint8_t)(reset_reason >> 24);
|
|
p[1] = (uint8_t)(reset_reason >> 16);
|
|
p[2] = (uint8_t)(reset_reason >> 8);
|
|
p[3] = (uint8_t)reset_reason;
|
|
offlog_evt(OFFLOG_EVT_BOOT, p, 4);
|
|
}
|
|
|
|
void offlog_iot_connect(void)
|
|
{
|
|
offlog_evt(OFFLOG_EVT_IOT_CONNECT, NULL, 0);
|
|
}
|
|
|
|
void offlog_iot_ready(void)
|
|
{
|
|
offlog_evt(OFFLOG_EVT_IOT_READY, NULL, 0);
|
|
}
|
|
|
|
void offlog_iot_disconnect(uint8_t reason)
|
|
{
|
|
offlog_evt(OFFLOG_EVT_IOT_DISCONN, &reason, 1);
|
|
}
|
|
|
|
void offlog_iot_reconn(uint32_t backoff_ms)
|
|
{
|
|
uint8_t p[4];
|
|
p[0] = (uint8_t)(backoff_ms >> 24);
|
|
p[1] = (uint8_t)(backoff_ms >> 16);
|
|
p[2] = (uint8_t)(backoff_ms >> 8);
|
|
p[3] = (uint8_t)backoff_ms;
|
|
offlog_evt(OFFLOG_EVT_IOT_RECONN, p, 4);
|
|
}
|
|
|
|
void offlog_evt_retry(uint32_t msg_id, uint8_t retry)
|
|
{
|
|
uint8_t p[5];
|
|
p[0] = (uint8_t)(msg_id >> 24);
|
|
p[1] = (uint8_t)(msg_id >> 16);
|
|
p[2] = (uint8_t)(msg_id >> 8);
|
|
p[3] = (uint8_t)msg_id;
|
|
p[4] = retry;
|
|
offlog_evt(OFFLOG_EVT_EVT_RETRY, p, 5);
|
|
}
|
|
|
|
void offlog_evt_giveup(uint32_t msg_id)
|
|
{
|
|
uint8_t p[4];
|
|
p[0] = (uint8_t)(msg_id >> 24);
|
|
p[1] = (uint8_t)(msg_id >> 16);
|
|
p[2] = (uint8_t)(msg_id >> 8);
|
|
p[3] = (uint8_t)msg_id;
|
|
offlog_evt(OFFLOG_EVT_EVT_GIVEUP, p, 4);
|
|
}
|
|
|
|
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value)
|
|
{
|
|
uint8_t p[6];
|
|
p[0] = sub;
|
|
p[1] = ch;
|
|
p[2] = (uint8_t)(value >> 24);
|
|
p[3] = (uint8_t)(value >> 16);
|
|
p[4] = (uint8_t)(value >> 8);
|
|
p[5] = (uint8_t)value;
|
|
offlog_evt(OFFLOG_EVT_COIL, p, 6);
|
|
}
|
|
|
|
void offlog_ota_start(uint8_t slot, uint32_t size)
|
|
{
|
|
uint8_t p[6];
|
|
p[0] = slot;
|
|
p[1] = 0; /* target: 0=loop */
|
|
p[2] = (uint8_t)(size >> 24);
|
|
p[3] = (uint8_t)(size >> 16);
|
|
p[4] = (uint8_t)(size >> 8);
|
|
p[5] = (uint8_t)size;
|
|
offlog_evt(OFFLOG_EVT_OTA_START, p, 6);
|
|
}
|
|
|
|
void offlog_ota_result(uint32_t code)
|
|
{
|
|
uint8_t p[4];
|
|
p[0] = (uint8_t)(code >> 24);
|
|
p[1] = (uint8_t)(code >> 16);
|
|
p[2] = (uint8_t)(code >> 8);
|
|
p[3] = (uint8_t)code;
|
|
offlog_evt(OFFLOG_EVT_OTA_RESULT, p, 4);
|
|
}
|
|
|
|
void offlog_time_anchor(uint32_t unix_ts)
|
|
{
|
|
offlog_evt_ts(OFFLOG_EVT_TIME_ANCHOR, NULL, 0, unix_ts); /* 严格用平台下发值 */
|
|
}
|
|
|
|
/*===========================================================================
|
|
* 查询
|
|
*===========================================================================*/
|
|
uint32_t offlog_boot_seq(void)
|
|
{
|
|
return _boot_seq;
|
|
}
|
|
|
|
uint16_t offlog_count(void)
|
|
{
|
|
return (uint16_t)_count;
|
|
}
|
|
|
|
int offlog_read_idx(uint16_t idx, OfflogEvt *out)
|
|
{
|
|
uint32_t cnt = _count;
|
|
if (!_ready || idx >= cnt) return -1;
|
|
|
|
/* 逻辑首 = 写位置向前 cnt 条 (环形) */
|
|
uint32_t phys = (_wr_off - OFFLOG_DATA_BASE); /* 0..DATA_SIZE */
|
|
uint32_t start = (phys + OFFLOG_DATA_SIZE - (uint32_t)cnt * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE;
|
|
uint32_t read_off = OFFLOG_DATA_BASE + (start + (uint32_t)idx * OFFLOG_EVT_SIZE) % OFFLOG_DATA_SIZE;
|
|
|
|
return offlog_evt_read_at(read_off, out);
|
|
}
|
|
|
|
/*===========================================================================
|
|
* 协议导出 (MQTT V1.06 / TCP JSON V1.02 共用)
|
|
*===========================================================================*/
|
|
uint8_t offlog_enabled(void)
|
|
{
|
|
return _ready;
|
|
}
|
|
|
|
uint32_t offlog_seq_last(void)
|
|
{
|
|
return (_wr_seq > 0) ? (_wr_seq - 1) : 0;
|
|
}
|
|
|
|
const char *offlog_type_str(uint8_t type)
|
|
{
|
|
switch (type) {
|
|
case OFFLOG_EVT_BOOT: return "boot";
|
|
case OFFLOG_EVT_IOT_CONNECT: return "iot_connect";
|
|
case OFFLOG_EVT_IOT_READY: return "iot_ready";
|
|
case OFFLOG_EVT_IOT_DISCONN: return "iot_disconnect";
|
|
case OFFLOG_EVT_IOT_RECONN: return "iot_reconn";
|
|
case OFFLOG_EVT_EVT_RETRY: return "evt_retry";
|
|
case OFFLOG_EVT_EVT_GIVEUP: return "evt_giveup";
|
|
case OFFLOG_EVT_COIL: return "coil";
|
|
case OFFLOG_EVT_TIME_ANCHOR: return "time_anchor";
|
|
case OFFLOG_EVT_OTA_START: return "ota_start";
|
|
case OFFLOG_EVT_OTA_RESULT: return "ota_result";
|
|
case OFFLOG_EVT_LOG_CLEAR: return "log_clear";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
/* 线圈事件子类型: 1=进 2=出 3=断开 4=恢复
|
|
(与 iot_mqtt_srv.c offlog_coil 调用一致) */
|
|
static const char *offlog_coil_sub_str(uint8_t sub)
|
|
{
|
|
switch (sub) {
|
|
case 1: return "car_enter";
|
|
case 2: return "car_leave";
|
|
case 3: return "loop_cut";
|
|
case 4: return "loop_restore";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
/* payload 大端 uint32 (与 offlog_boot/offlog_coil 写入一致) */
|
|
static uint32_t offlog_payload_u32(const uint8_t *p)
|
|
{
|
|
return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | p[3];
|
|
}
|
|
|
|
/* 记录 → JSON 对象 (data 按事件类型组装; 无参数事件 data=null)
|
|
回返 snprintf 写入长度; 调用方须保证 buf_len 充足 (一条最大约130B) */
|
|
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len)
|
|
{
|
|
const char *type = offlog_type_str(e->type);
|
|
char data[96];
|
|
|
|
switch (e->type) {
|
|
case OFFLOG_EVT_BOOT:
|
|
snprintf(data, sizeof(data), "{\"rst\":%lu}",
|
|
(unsigned long)offlog_payload_u32(e->payload));
|
|
break;
|
|
case OFFLOG_EVT_IOT_DISCONN:
|
|
snprintf(data, sizeof(data), "{\"reason\":%u}", e->payload[0]);
|
|
break;
|
|
case OFFLOG_EVT_IOT_RECONN:
|
|
snprintf(data, sizeof(data), "{\"backoff_ms\":%lu}",
|
|
(unsigned long)offlog_payload_u32(e->payload));
|
|
break;
|
|
case OFFLOG_EVT_EVT_RETRY:
|
|
snprintf(data, sizeof(data), "{\"msg_id\":%lu,\"retry\":%u}",
|
|
(unsigned long)offlog_payload_u32(e->payload), e->payload[4]);
|
|
break;
|
|
case OFFLOG_EVT_EVT_GIVEUP:
|
|
snprintf(data, sizeof(data), "{\"msg_id\":%lu}",
|
|
(unsigned long)offlog_payload_u32(e->payload));
|
|
break;
|
|
case OFFLOG_EVT_COIL:
|
|
snprintf(data, sizeof(data), "{\"sub\":\"%s\",\"ch\":%u,\"value\":%lu}",
|
|
offlog_coil_sub_str(e->payload[0]), e->payload[1],
|
|
(unsigned long)offlog_payload_u32(e->payload + 2));
|
|
break;
|
|
default:
|
|
data[0] = '\0'; /* iot_connect/iot_ready/time_anchor/log_clear → null */
|
|
break;
|
|
}
|
|
|
|
if (data[0] == '\0') {
|
|
return snprintf(buf, buf_len,
|
|
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":null}",
|
|
(unsigned long)e->seq, (unsigned)e->boot_seq,
|
|
(unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type);
|
|
}
|
|
return snprintf(buf, buf_len,
|
|
"{\"seq\":%lu,\"boot_seq\":%u,\"ts_ms\":%lu,\"unix_ts\":%lu,\"type\":\"%s\",\"data\":%s}",
|
|
(unsigned long)e->seq, (unsigned)e->boot_seq,
|
|
(unsigned long)e->ts_ms, (unsigned long)e->unix_ts, type, data);
|
|
}
|
|
|
|
/*===========================================================================
|
|
* 清空 (审计: 清空后写一条 LOG_CLEAR)
|
|
*===========================================================================*/
|
|
void offlog_clear(void)
|
|
{
|
|
if (!_ready) return;
|
|
|
|
/* 逻辑清除 + 只擦写指针起点扇区 (~45ms, 原 2.8s 接近 IWDG 4s 风险):
|
|
count=0 使旧数据不可读, 其余扇区由环形写覆盖时自动擦 */
|
|
SPI_Flash_Erase_Sector(OFFLOG_DATA_BASE / 4096);
|
|
_wr_off = OFFLOG_DATA_BASE;
|
|
_wr_sector = 1;
|
|
_count = 0;
|
|
/* _wr_seq / _boot_seq 不重置: 序号单调递增, 保证日志全局唯一 */
|
|
|
|
offlog_evt(OFFLOG_EVT_LOG_CLEAR, NULL, 0);
|
|
offlog_flush_head();
|
|
}
|
|
|
|
/* OfflogEvt -> hex string (flash 原始字节, 小端原样; 上位机按 BLE 协议 §7 字段表解析)
|
|
Used by TCP JSON / MQTT log_query stream=event (protocol V1.03/V1.07).
|
|
Returns snprintf written length; 32B -> 64 hex chars. */
|
|
int offlog_evt_to_hex(const OfflogEvt *e, char *buf, int buf_len)
|
|
{
|
|
int i, pos = 0;
|
|
const uint8_t *p = (const uint8_t *)e;
|
|
for (i = 0; i < (int)sizeof(OfflogEvt); i++) {
|
|
if (pos >= buf_len - 3) break;
|
|
pos += snprintf(buf + pos, buf_len - pos, "%02x", p[i]);
|
|
}
|
|
return pos;
|
|
}
|