协议先行 (08893d5, MQTT V1.06 + TCP JSON V1.02) 的固件侧实现:
- offlog 新增导出 API: enabled/seq_last/type_str/evt_to_json
(seq_first=seq_last-count+1, idx=start_seq-seq_first, count≤4)
- MQTT iot_handle_publish 三分支: log_stat/log_query/log_clear
(log_query 响应 static buf 防大栈; log_clear 阻塞~2.8s 主循环可接受)
- TCP JSON 三 handler + cmd 表 (需鉴权)
- 单测新增 test_export_json (8 例全过): boot rst 大端/coil sub/evt_retry/data:null/seq 公式
- devlog V3.9
146 lines
8.4 KiB
C
146 lines
8.4 KiB
C
/**
|
||
******************************************************************************
|
||
* @file offlog.h
|
||
* @author wfq
|
||
* @version V1.0
|
||
* @date 2026-08-04
|
||
* @brief DBN 脱机事件日志 (W25Q32 外部 SPI NOR, 环形)
|
||
*
|
||
* 目的: 解决"平台重复上线/异常重启但现场无日志"的取证问题。
|
||
* 区分「设备真复位」vs「MQTT 断连重连」(两者都会导致平台重收 initialize):
|
||
* - 真复位: 会有 OFFLOG_EVT_BOOT 事件, boot_seq 递增
|
||
* - 断连重连: boot_seq 不变, 只有 IOT_* 网络事件
|
||
*
|
||
* 分区 (W25Q32, 4MB):
|
||
* | 参数区 0x000000 64KB (现有配置, 不动)
|
||
* | 事件区 0x010000 256KB = 扇区 16..79 (头扇区 + 63 数据扇区)
|
||
* | OTA 区 0x050000 512KB (预留)
|
||
* | 快照区 0x0D0000 ~3.25MB (预留, 后续)
|
||
*
|
||
* 环形实现:
|
||
* - 扇区 0 (区内) = 头扇区, 存元数据 (boot_seq/写位置/条数), 扇区切换时更新
|
||
* - 扇区 1..63 = 数据扇区, 每扇区 128 条 x 32B 定长记录
|
||
* - 顺序写, 写满擦下一扇区 (环形覆盖), 天然磨损均衡
|
||
* - 上电从头部记录的写位置向后扫描, 找第一条无效记录 = 真实写位置 (掉电恢复)
|
||
*
|
||
* 记录格式 (32B 定长):
|
||
* magic(1) type(1) len(1) flags(1) seq(4) boot_seq(2) rsvd(2)
|
||
* ts_ms(4) unix_ts(4) payload(14)
|
||
******************************************************************************
|
||
*/
|
||
|
||
#ifndef _OFFLOG_H__
|
||
#define _OFFLOG_H__
|
||
|
||
#include <stdint.h>
|
||
|
||
/*===========================================================================
|
||
* 分区与容量
|
||
*===========================================================================*/
|
||
#define OFFLOG_AREA_BASE 0x010000UL /* 事件日志区起始 (绝对地址) */
|
||
#define OFFLOG_AREA_SIZE 0x040000UL /* 256KB */
|
||
#define OFFLOG_HEAD_SECTOR 0 /* 区内扇区号: 头扇区 */
|
||
#define OFFLOG_DATA_SECTOR_CNT 63 /* 数据扇区数 (1..63) */
|
||
#define OFFLOG_EVT_PER_SECTOR 128 /* 4096 / 32 */
|
||
|
||
#define OFFLOG_DATA_BASE (OFFLOG_AREA_BASE + 4096)
|
||
#define OFFLOG_DATA_SIZE (OFFLOG_DATA_SECTOR_CNT * 4096)
|
||
#define OFFLOG_MAX_RECORDS (OFFLOG_DATA_SECTOR_CNT * OFFLOG_EVT_PER_SECTOR) /* 8064 */
|
||
|
||
/* 头扇区记录 (32B, 扇区切换时更新) */
|
||
typedef struct {
|
||
uint8_t magic[4]; /* "OFFL" */
|
||
uint32_t boot_seq; /* 上次上电序号 (本 boot 由 init 时 +1) */
|
||
uint32_t wr_sector; /* 上次写扇区号 (区内 1..63) */
|
||
uint32_t wr_off; /* 上次写偏移 (相对 OFFLOG_AREA_BASE) */
|
||
uint32_t wr_seq; /* 下一条记录全局 seq (掉电恢复锚点) */
|
||
uint32_t count; /* 有效记录数 (0..8064) */
|
||
uint32_t rsvd[2];
|
||
} OfflogHead; /* 28B, 按 32B 对齐使用 */
|
||
|
||
#define OFFLOG_HEAD_MAGIC0 'O'
|
||
#define OFFLOG_HEAD_MAGIC1 'F'
|
||
#define OFFLOG_HEAD_MAGIC2 'L'
|
||
#define OFFLOG_HEAD_MAGIC3 'L'
|
||
|
||
/* 事件记录 (32B 定长, 字段重排保证自然对齐无 padding:
|
||
4×uint8 → uint32×3 → uint16×2 → payload[12] = 4+12+4+12 = 32) */
|
||
typedef struct {
|
||
uint8_t magic; /* 0xA5 */
|
||
uint8_t type; /* OFFLOG_EVT_xxx */
|
||
uint8_t len; /* payload 有效字节数 (0..12) */
|
||
uint8_t flags; /* bit0: unix_ts 有效 */
|
||
uint32_t seq; /* 全局序号 (跨 boot 递增) */
|
||
uint32_t ts_ms; /* boot 内相对时间 mstick() */
|
||
uint32_t unix_ts; /* 已同步 Unix 秒 (flags.bit0=1 时有效) */
|
||
uint16_t boot_seq; /* 启动序号 */
|
||
uint16_t rsvd;
|
||
uint8_t payload[12]; /* 事件参数 */
|
||
} OfflogEvt; /* 32B, 无 padding */
|
||
|
||
#define OFFLOG_EVT_MAGIC 0xA5
|
||
#define OFFLOG_EVT_SIZE sizeof(OfflogEvt)
|
||
#define OFFLOG_UNIX_VALID_FLAG 0x01
|
||
|
||
/*===========================================================================
|
||
* 事件类型
|
||
*===========================================================================*/
|
||
enum {
|
||
OFFLOG_EVT_BOOT = 0x01, /* 上电/复位; payload[0..3]=复位原因寄存器 RCC_RSTSCKR */
|
||
OFFLOG_EVT_IOT_CONNECT = 0x10, /* MQTT TCP 连接成功 (socket 中断) */
|
||
OFFLOG_EVT_IOT_READY = 0x11, /* MQTT SUBACK → 发 initialize (重复上线直接原因!) */
|
||
OFFLOG_EVT_IOT_DISCONN = 0x12, /* MQTT 断连; payload[0]=原因(1=断开 2=超时 3=CONNACK拒绝 4=连接超时) */
|
||
OFFLOG_EVT_IOT_RECONN = 0x13, /* 重连退避; payload[0..3]=退避 ms */
|
||
OFFLOG_EVT_EVT_RETRY = 0x30, /* event_report ACK 超时重发; payload[0..3]=msg_id, payload[4]=retry */
|
||
OFFLOG_EVT_EVT_GIVEUP = 0x31, /* event_report 重试耗尽挂起; payload[0..3]=msg_id */
|
||
OFFLOG_EVT_COIL = 0x40, /* 线圈事件; payload[0]=sub(1=进 2=出 3=断开 4=恢复), payload[1]=ch, payload[2..5]=value(50ms) */
|
||
OFFLOG_EVT_TIME_ANCHOR = 0x50, /* 时钟同步锚点; unix_ts=同步值 */
|
||
OFFLOG_EVT_LOG_CLEAR = 0x70, /* 日志清除 (审计) */
|
||
};
|
||
|
||
/* 复位原因寄存器 (CH32V20x RCC 基址 0x40021000, RSTSCKR 偏移 0x24,
|
||
STM32F1 兼容布局; 若实际硬件布局不同, 读出的原因位会异常, 待板上验证) */
|
||
#define OFFLOG_RCC_BASE 0x40021000UL
|
||
#define OFFLOG_RCC_RSTSCKR (*(volatile uint32_t *)(OFFLOG_RCC_BASE + 0x24UL))
|
||
|
||
/* 复位原因位 (RCC_RSTSCKR, STM32F1/CH32V20x 兼容布局, 待板上验证) */
|
||
#define OFFLOG_RST_IWDG (1UL << 31) /* 独立看门狗复位 (IWDG 超时 → 主循环卡死!) */
|
||
#define OFFLOG_RST_WWDG (1UL << 30) /* 窗口看门狗复位 */
|
||
#define OFFLOG_RST_LPWR (1UL << 29) /* 低功耗复位 */
|
||
#define OFFLOG_RST_PIN (1UL << 26) /* NRST 引脚复位 */
|
||
#define OFFLOG_RST_POR (1UL << 25) /* 上电/掉电复位 (真断电!) */
|
||
#define OFFLOG_RST_SFT (1UL << 24) /* 软件复位 NVIC_SystemReset */
|
||
|
||
/*===========================================================================
|
||
* API
|
||
*===========================================================================*/
|
||
void offlog_init(void); /* 上电初始化 (扫描恢复) + 内部准备 BOOT 事件 */
|
||
void offlog_boot(uint32_t reset_reason); /* 记录 BOOT 事件 (带复位原因) */
|
||
|
||
void offlog_evt(uint8_t type, const uint8_t *payload, uint8_t len);
|
||
|
||
void offlog_iot_connect(void);
|
||
void offlog_iot_ready(void);
|
||
void offlog_iot_disconnect(uint8_t reason);
|
||
void offlog_iot_reconn(uint32_t backoff_ms);
|
||
void offlog_evt_retry(uint32_t msg_id, uint8_t retry);
|
||
void offlog_evt_giveup(uint32_t msg_id);
|
||
void offlog_coil(uint8_t sub, uint8_t ch, uint32_t value);
|
||
void offlog_time_anchor(uint32_t unix_ts);
|
||
|
||
uint32_t offlog_boot_seq(void); /* 当前 boot 序号 */
|
||
uint16_t offlog_count(void); /* 有效记录数 */
|
||
int offlog_read_idx(uint16_t idx, OfflogEvt *out); /* 按逻辑序号读, 0=成功 -1=越界 */
|
||
void offlog_clear(void); /* 清空事件区 (清空后写 LOG_CLEAR 审计) */
|
||
|
||
/*===========================================================================
|
||
* 协议导出 (MQTT V1.06 / TCP JSON V1.02)
|
||
*===========================================================================*/
|
||
uint8_t offlog_enabled(void); /* 日志功能是否启用 (Flash 初始化成功) */
|
||
uint32_t offlog_seq_last(void); /* 最新一条记录全局序号 (0=空) */
|
||
const char *offlog_type_str(uint8_t type); /* 事件类型数字 → 协议字符串名 */
|
||
int offlog_evt_to_json(const OfflogEvt *e, char *buf, int buf_len); /* 记录 → JSON 对象 */
|
||
#define OFFLOG_MAX_QUERY_RECORDS 4 /* log_query 分页上限 (MQTT ≤500B 发布限制) */
|
||
|
||
#endif /* _OFFLOG_H__ */
|