Files
STM32F4-Base/App/log/sys_log.h
T
wandering1 77779d49d6 feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐
UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机
OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批
预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block
(口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng
反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定
内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按
is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。

记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在
defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE);
事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身
(一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。
为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核,
丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。

为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费
两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过;
RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同
码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。

实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B;
RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS,
protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→
164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34,
既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、
不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页
连跑两次逐字节一致;字库 diff 空。

顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽
16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前
靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值
全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py
模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。

未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行;
LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
2026-10-26 15:51:12 +08:00

311 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:System Log — 运行/自检记录引擎(双端共享核心)
* 模块功能:定长 64B 文本日志记录的公共 API、格式常量与行拆分;
* MCU 后端 = FatFS /log 目录日志文件(sys_log_mcu.c),模拟器后端 = 宿主文件(sim_syslog.c)
* 适用平台:STM32F407ZGTx + PC 模拟器(共享头,无 OS/LVGL 依赖)
* 作者:王建锋
* 创建日期:2026-09-14
* 修改记录:
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4)
* v1.2 2026-10-26 王建锋 新增「记录删除」API(UI 装置维护「清除记录」的前置,
* 道F task-4;判据 docs/任务规划/UI补齐.md §2 第 3 行
* 「不允许只做一个没有实际效果的按钮」):
* 共享层 syslog_request_clear() / syslog_clear_state()
* (双端都实现,UI 侧只准用这两个)+ MCU 后端专属
* syslog_clear_apply()(落地半边,仅 defaultTask)。
* 请求与落地分离的理由与执行点写在下面声明处
* v1.1 2026-10-24 王建锋 仅契约注释更正,与 App/log/sys_log_mcu.c 的 C/F 两项
* 整改同步(无签名、无行为改动):
* syslog_get_record 写死容量要求(ts_len > SYSLOG_TS_LEN、
* desc_len > SYSLOG_DESC_MAX)并明确"容量不足一律 -1、
* 不截断、不写调用方缓冲"——旧注释像是"给少了会被截断",
* 实际是整条读不到;syslog_post 写明必须假设丢弃存在、
* 且丢弃留痕已按通道节流(节流后无丢弃计数)
*/
#ifndef SYS_LOG_H
#define SYS_LOG_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
/*
* 日志 ID:决定落盘文件与容量上限
*
* ⚠ **记录类型 = 日志 ID 本身**(一类记录 = /log 下一个严格 8.3 短名的文件),
* 记录体内**没有**类型字段位(Lead 裁定 2026-10-26,task-10):64B 定长格式被
* UI 记录页(文本读者)与主机解码工具同时按固定偏移读,塞类型字节等于当场砸掉
* 这两批既有读者;而运行/自检记录已经在这份格式里跑了很久,不能为五类新记录
* 改版本位。于是"这条记录是什么类型"由它在哪个文件里回答。
* 同理二进制 SOE 通道的 soe_type_t 本轮**不动**:改它必须同步 UI 侧的类型名表
* (类型名表在 UI 侧 <Drivers/BSP/UI/ui_w_soe.c>,不在本道写域),而且 SOE
* 页与这五页是两套读者。
* 口径与五个写入点已登记进 App/log/README.md(task-11「记录 5 页」按此做)。
*/
typedef enum {
SYSLOG_RUN = 0u, /* 运行记录 /log/run.log:上电/重启事件 */
SYSLOG_SELFTEST, /* 自检记录 /log/selftest.log:开机自检故障(仅故障才写) */
/* ↓ 五类记录(docs/UI设计/页面扩充详细方案.md §3.6 的五行,task-10 落写入点)
* ⚠ 只准**尾部追加**:id 值即枚举序 = 两端 s_paths[]/s_caps[] 的下标,
* 中间插值会让老文件被新 id 读成别的类型(静默错类别,比崩更难查)。 */
SYSLOG_ACCIDENT, /* 事故记录 /log/accid.log:跳闸投退=1 的元件动作 */
SYSLOG_ALARM, /* 保护告警 /log/alarm.log:跳闸投退=0 或该项不存在的元件动作 */
SYSLOG_START, /* 保护启动 /log/start.log:元件 picked 上升沿 */
SYSLOG_OPER, /* 操作记录 /log/oper.log:强制复归 / 清除记录 / 对时 / 定值保存 */
SYSLOG_YK, /* 遥控记录 /log/yk.log:104 遥控的选择 / 执行 / 拒绝 */
SYSLOG_COUNT /* 哨兵值,非有效 ID */
} syslog_id_t;
/*
* 记录格式(定长 64B,一行一条;设计文档 §4.2):
* [0..18] "YYYY-MM-DD HH:MM:SS"(ISO 8601 空格变体,19 字符;墙钟无效 = "1970-01-01 08:00:00")
* [19] ' '
* [20..29] epoch 秒 ASCII "%010u"(机器解析/上电重启分类用;墙钟无效 = "0000000000")
* [30] ' '
* [31..58] 描述 UTF-8(≤28B = 9 汉字),不足补 '\0'
* [59..63] '\0' 填充
*/
#define SYSLOG_REC_SIZE 64u /* 单条记录字节数(定长) */
#define SYSLOG_TS_LEN 19u /* 时间戳字段长度 "YYYY-MM-DD HH:MM:SS" */
#define SYSLOG_EPOCH_OFFS 20u /* epoch ASCII 字段偏移 */
#define SYSLOG_EPOCH_LEN 10u /* epoch ASCII 字段长度(2026 年 Unix 秒 = 10 位) */
#define SYSLOG_DESC_OFFS 31u /* 描述字段偏移 */
#define SYSLOG_DESC_MAX 28u /* 描述最大字节数(受框架行缓冲 CP_ROW_BUF_LEN=32 约束) */
/*
* 容量上限(条数;满时删除重建,设计文档 §4.3)
*
* ⚠ **上限是逻辑上限,不是预分配**:文件按追加写自然增长,一条不写就不占簇;
* 到顶才 unlink 重建(照 run.log 先例)。七类全满的名义峰值 =
* (1024+512+512+1024+1024+512+256) × 64B = **304 KB**,NAND 2 GB、且这些
* 记录只在事件边沿才写(判据动作/遥控/人工操作),不构成容量或磨损瓶颈。
* 告警/启动给到 1024 是因为一次系统故障会连片触发多个元件,别让最想了解
* 故障过程的两类记录最先被截掉。
*/
#define SYSLOG_RUN_MAX 1024u /* /log/run.log:64KB */
#define SYSLOG_SELFTEST_MAX 512u /* /log/selftest.log:32KB */
#define SYSLOG_ACCIDENT_MAX 512u /* /log/accid.log:32KB(跳闸稀有,换长历史) */
#define SYSLOG_ALARM_MAX 1024u /* /log/alarm.log:64KB */
#define SYSLOG_START_MAX 1024u /* /log/start.log:64KB */
#define SYSLOG_OPER_MAX 512u /* /log/oper.log:32KB */
#define SYSLOG_YK_MAX 256u /* /log/yk.log:16KB(低频,与事故同稀有) */
/*
* 上电/重启分类窗口(秒):run.log 末条 epoch 距 now < 该值 → "reboot",否则 "power on"
*/
#define SYSLOG_REBOOT_WINDOW_S 600u
/*
* 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃)
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(UTF-8;超过 SYSLOG_DESC_MAX 的部分丢弃,落盘只留前 28 字节)
* 本参数**投递时即拷贝进待发条目**:调用方缓冲区可立即复用,
* 不要求生命周期覆盖到落盘(2026-10-24 台架实证后由「只存指针」
* 改为条目内嵌定长缓冲,理由见 sys_log_mcu.c 的 syslog_entry_t 注释)
* 指针本身不可为 NULL const char *
* 返回值:无
* 限定条件:MCU 端 FreeRTOS 启动前调用走静态待发数组(由 defaultTask 挂载后落盘)
* 函数说明:调用方**必须假设投递可能被丢弃**——MCU 后端(sys_log_mcu.c)队列 8 槽、
* 启动前待发数组 8 槽,卷被 ftpTask 长期占用时 defaultTask 会把整批回投
* 队列(队列因此可长时间处于满态),此时本函数直接丢弃该条,并只在
* "转入故障"的那一次留痕(丢弃留痕按正常→故障节流,F 项整改 2026-10-24;
* 节流后**丢弃总数没有计数**,要计量得另加计数器)。必须留住的证据走
* sys_log_evidence(),不要靠 syslog_post 重试。
* 模拟器后端(simulator/sim/sim_syslog.c)是单线程直写宿主文件、**没有
* 队列与丢弃**,因此上面这段满态语义只约束 MCU 端。
*/
void syslog_post(syslog_id_t id, const char *desc);
/*
* 函数功能:投递一条日志——**调用链上不带日志行缓冲**的那一个版本
* 入口参数:id - 日志 ID syslog_id_t
* desc - 描述串(NUL 结尾;超过 SYSLOG_DESC_MAX 截断),同 syslog_post
* 返回值:无
* 限定条件:任意上下文;MCU 端不碰 FatFs、不碰卷锁、不阻塞
* 函数说明:为什么要有第二个投递口(不是洁癖):每个 DBG_* 调用点在调用者栈上
* 内联约 216 B 行缓冲,而 syslog_post 的两个"满态丢弃留痕"分支就躺在
* 它自己的帧里——于是"调一次 post"在静态最坏链上等于"带上这两处缓冲"。
* protectTask 的静态最坏链是 B9 写进 DoD 的硬线(872 B / 57%,当年在
* post 里加一条 DBG_* 实测垫到 1192 B = stack_report FAIL),iec104Task
* 同样是深链任务,所以这五类记录的写入点一律走本函数。
* 代价是丢弃时不当场留痕:只累加 syslog_post_dropped(),由唯一写者
* defaultTask 在 syslog_drain() 里按差值补一条(照 soe_store 的先例)。
* 与 syslog_post 的差别**只有留痕路径**,收/丢的判据逐条相同。
* 模拟器后端没有队列也没有栈预算问题,本函数等价于 syslog_post。
*/
void syslog_post_quiet(syslog_id_t id, const char *desc);
/*
* 函数功能:取 syslog_post_quiet() 通道的累计丢弃条数(可观测性)
* 入口参数:无
* 返回值:MCU 端 = 队列满 / 启动前数组满累计次数;模拟器端恒 0(直写不丢) uint32_t
* 函数说明:与 soe_store_dropped() 同款定位。留痕由 drain 按差值上报,本函数是
* 给单测与台架读数用的,不参与任何判定路径。
*/
uint32_t syslog_post_dropped(void);
/*
* 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1))
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:条数;文件缺失/未挂载返回 0 uint32_t
*/
uint32_t syslog_count(syslog_id_t id);
/*
* 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾)
* 入口参数:id - 日志 ID syslog_id_t
* idx - 记录序号(0 起) uint32_t
* p_ts - 时间戳输出缓冲(不可为 NULL) char *
* ts_len- p_ts 容量,必须 > SYSLOG_TS_LEN(即 ≥ SYSLOG_TS_LEN+1 = 20 字节) size_t
* p_desc- 描述输出缓冲(不可为 NULL) char *
* desc_len - p_desc 容量,必须 > SYSLOG_DESC_MAX(即 ≥ 29 字节) size_t
* 返回值:0 - 成功;-1 - 越界 / 读失败 / **缓冲容量不足** int
* 限定条件:容量不足一律返回 -1,**不做静默截断、也不写调用方一个字节**
* (C 项整改 2026-10-24;旧注释只写"≥…+1 字节"像是"给少了会被截断",
* 照它给 28 字节缓冲的结果是整条读不到而不是读到 27 字符)。
* 描述字段在写入侧已截到 SYSLOG_DESC_MAX,所以按上面的最小容量给缓冲
* 就一定装得下,读侧不需要第二道截断。
* MCU 后端与模拟器后端(sim_syslog.c)在这条守卫上取值相同,行为一致;
* 模拟器侧那份同样不可达的截断支还留在文件里(未列入本轮改动范围)。
* 调用方:uiTask 的日志页(行缓冲 32B)与 SYSLOG 取证打印。
*/
int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len);
/*
* 函数功能:读取末条记录的 epoch 秒(上电/重启分类用)
* 入口参数:id - 日志 ID syslog_id_t
* p_epoch- 输出缓冲(不可为 NULL;无记录时置 0) uint32_t *
* 返回值:0 - 成功读到末条;-1 - 文件缺失/空 int
*/
int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch);
/*
* 记录删除(「清除记录」):请求 → 落地分离
*
* 为什么必须分两步:defaultTask 是本模块**唯一写者**(存储架构说明 §10.1 第 7 条),
* uiTask 直接 f_unlink 会与写者的追加写/「容量满 unlink 重建」交错,而且删除要走
* 卷锁 + FAT 回写——uiTask 在 IWDG required_mask 内、每 100 ms 喂一次心跳,
* 给它一条最坏数百 ms 的存储路径就是拿误复位当代价。故 UI 侧只登记请求,
* 真正的删除在 defaultTask 的 syslog_drain() 尾部执行
* (范式同 App/algo/prot_elm.c 的 prot_elm_request_reset():单字节标志 + 消费者落地)。
*
* 状态码(syslog_clear_state 返回值;写成宏而不是枚举,与本头既有 SYSLOG_* 常量同族)
*/
#define SYSLOG_CLEAR_NONE 0 /* 从未请求过清除(上电初值) */
#define SYSLOG_CLEAR_OK 1 /* 已清空:文件不在盘上,同拍 syslog_count() 必为 0 */
#define SYSLOG_CLEAR_WAIT (-1) /* 请求排队中:等 defaultTask 下一轮 drain(含卷占用顺延、卷未就绪挂起) */
#define SYSLOG_CLEAR_FAIL (-2) /* 最近一次落地失败:原文件未被删除,可再发一次请求 */
/*
* 函数功能:登记「清除某日志全部记录」的请求(任意上下文;不碰存储、不阻塞)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 请求已登记(同一 id 重复登记合并为一次,幂等)
* -1 - id 越界(请求未登记,状态一个字节都没动) int
* 限定条件:非唯一写者的上下文(uiTask / netTask / iec104Task)**只能**用本函数发起
* 删除,不得直调 MCU 后端的 syslog_clear_apply()
* 函数说明:本函数**不等于"已清除"**,只代表"已受理"——MCU 端的删除动作发生在
* defaultTask 的下一次 syslog_drain()(50 ms 节拍),结果读
* syslog_clear_state(),事实复核读 syslog_count()。
* ⚠ 两端时序有别(同 syslog_post 的队列/直写差异,那边写在 post 的头注释里):
* MCU 端返回后状态通常是 SYSLOG_CLEAR_WAIT;模拟器端单线程、本调用内
* 同步落地,返回后立即是 OK/FAIL,**永远不会读到 WAIT**。UI 侧要按
* "WAIT 也正常"写判断,别把 WAIT 当失败。
* 刻意**不**在删除成功后补一条 "records cleared" 留痕:判据要求
* syslog_count() 归零,写了立刻变 1;要留痕归操作记录/SOE
* (docs/任务规划/UI补齐.md 第 6 项的写入方),不是本 API 的职责。
*/
int syslog_request_clear(syslog_id_t id);
/*
* 函数功能:查「清除记录」的状态(给 UI 出真实反馈,不是"按了就算成功")
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:SYSLOG_CLEAR_NONE(0) / SYSLOG_CLEAR_OK(1) / SYSLOG_CLEAR_WAIT(-1) /
* SYSLOG_CLEAR_FAIL(-2);id 越界按 NONE 返回(等价"从没请求过") int
* 限定条件:任意上下文(MCU 端只读两个 volatile 单字节,Cortex-M 上原子,无锁)
* 函数说明:排队位优先于结果字节——否则连点两次会先把上一次的结果当成这次的,
* 还在排队的请求被显示成"已清空"。
* 无锁交接的最坏竞态只有两种,都不产生错误数据:① 写者已销账、尚未写
* 结果的那几个指令周期里读 → 读到上一次的结果(同向,多为 OK);
* ② 上一次失败被新请求清零 → 那次失败不再显示(用户已在重新发起,
* 语义正确)。为它上临界区要引 FreeRTOS 头 + 宿主桩,不值。
*/
int syslog_clear_state(syslog_id_t id);
/*
* MCU 后端专属 API(sys_log_mcu.c;模拟器端无对应实现,勿在共享代码调用)
*/
/*
* 函数功能:日志引擎初始化(创建 FreeRTOS 消息队列)
* 入口参数:无
* 返回值:0 - 成功;-1 - 队列创建失败 int
* 限定条件:app_main_init() 起始处调用,先于任何 syslog_post()
*/
int syslog_mcu_init(void);
/*
* 函数功能:卷挂载成功后的一次性处理(建 /log 目录 + 写"上电/重启"分类记录)
* 入口参数:无
* 返回值:无
* 限定条件:defaultTask f_mount/f_mkfs 成功且置位 SYS_EVT_STORAGE_READY 前调用一次
*/
void syslog_mcu_mounted(void);
/*
* 函数功能:排空待发日志并落盘(启动前数组 + 运行期队列,FIFO)
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试)
* 本函数尾部同时消费「清除记录」请求(syslog_request_clear 的落地半边,
* 排在条目排空之后——删除必须是本轮对这两个文件的最后一次盘上动作,
* 先删后写会把刚排空的旧记录又落回去,"清除"就变成没效果的按钮)
*/
void syslog_drain(void);
/*
* 函数功能:立即删除某日志的全部记录(「清除记录」的落地半边,MCU 后端专属)
* 入口参数:id - 日志 ID syslog_id_t
* 返回值:0 - 记录已清空(含"文件本就不存在"的幂等成功,见下)
* -1 - 未清空:卷被占用或删除失败,**原文件一个字节都没少** int
* 限定条件:仅 defaultTask(syslog 唯一写者)上下文,且调用处**不得持卷锁**
* (卷锁非递归锁);卷锁一律 try(vol_lock_wait(0),本任务 = 看门狗
* 喂狗点,阻塞等锁 = 停喂 = IWDG 误复位)
* 函数说明:其它上下文请调 syslog_request_clear()(本头文件上方);本函数在
* MCU 后端之外**没有任何实现**,共享代码误调会在链接期报错,
* 这比运行时断言硬。产品路径上唯一的调用者是 syslog_drain 尾部的
* 请求消费循环,直接调用只用于板级套件与主机单测。
* 幂等口径:只有 FR_NO_FILE 算"已经没有什么可删了"→ 返 0;
* FR_NO_PATH(/log 目录不在)**不算**幂等——记录确实读不到,但成因是
* 目录没了而不是"被清空",补目录归 syslog_mcu_mounted() 每 2s 的重试,
* 这里如实返 -1,不把卷异常洗成"清除成功"。
* 名字/参数类错误码(FR_INVALID_NAME 等)一律只返 -1 不做任何补救动作:
* 本函数压根没有卷级动作(无 f_mkdir / 无 f_mkfs / 不重试 unlink),
* 这正是存储架构说明 §10.1 第 10 条在删除路径上的落地形态。
*/
int syslog_clear_apply(syslog_id_t id);
/*
* 函数功能:把一条取证行追加到 /log/wdev.log(掉电不丢;变长行,不走 64B 定长格式)
* 入口参数:p_desc - 取证描述行(调用方已格式化;不可为 NULL) const char *
* 返回值:0 - 成功;-1 - 卷占用或写失败(证据可能回滚,**不阻断判定**) int
* 限定条件:poller(defaultTask)上下文调用;卷锁一律 try(绝不阻塞,
* 本上下文 = 看门狗喂狗点)
* 函数说明:第 7 轮取证通道(设计见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录:
* 误复位后 RAM 态随复位消失、stall 行经僵尸串口丢失,故证据必须落盘)。
* 内部在释放卷锁后立刻追加一条 s_append(SYSLOG_RUN, ...) 写另一个
* 文件——FTL 单页缓存语义为"写到不同 LPN 才把前一脏页刷进 NAND",
* 没有该 flush 诱导,本行证据会在约 1.7s 后的 IWDG 复位中回滚丢失。
* 文件 > 8KB 时先 unlink 重建,防长跑无界增长。
*/
int sys_log_evidence(const char *p_desc);
#ifdef __cplusplus
}
#endif
#endif /* SYS_LOG_H */