Files
wandering1 94b0d79026 feat(ui)+chore(docs): UI 补齐清单清空;数据可信销项与「卡」号清退
本笔含两组收尾工作(同一工作区内交错在 6 个共享文档/门禁配置里,
拆开会产生"文档声称的页数与树里黄金数不一致"的中间提交,故合为一笔):

一、UI 补齐(清单清空)
- 新建 6 个内容页模块:ui_page_accident / oper / alarm / start / yk
  复用 ui_w_records 共享组件(分页、空态"无记录"、按实例读盘缓存),
  记录层与五个写入点已由道F task-10 / 道G task-12 落地,本批只补页面;
  ui_page_runreport 为 13 行 2 页只读统计,四个数逐个挂范围标签
  (自电池周期 / 本次开机),boot_ready=0 显 "--" 不显 0。
- 数据面:ui_data_info_t 尾部追加 9 字段,ui_data_feed 用
  boot_stats_get() + iec104_serv_get_stats() 纯搬运;模拟器填确定值假数据
  (差异登记 docs/测试说明/模拟器差距清单.md §2.3 两行)。
- 接线:菜单树 6 叶绑 #page=,绑定叶 22→28、UI_PAGE_MAP_COUNT=28;
  字库 185→218 字 / 280→313 字形;双端注册 uvprojx +30、CMakeLists +6。
- 回归:内容页黄金 53→60(含 runreport_p2 真拍第 2 页),文本断言
  425 正向 / 36 反向;存量 53 张逐字节零漂移;记录 5 页各用独立空目录
  (模拟器把操作留痕写进 cwd 的 sim_oper.log,共用 tmp 会让空态拍不到)。

二、数据可信销项 + 「数据可信」任务卡号整族清退
- 删除已闭环的"已知电压注入验证"行与闭环结论块(证据在
  docs/硬件平台/ADC通道映射说明.md §8.1 与
  docs/测试说明/ADC前端注入验证流程.md §5~§7),§5 标识列改语义短语。
- 卡 0/0b/1~12 共 14 个号全仓 288 站点 / 56 文件清退成语义短语,
  代码注释、测试、门禁配置与文档一并改;换算表入 docs/编号规范.md §9.3。
  这一族是中文前缀,不进 check_ids 语料,刻意不入 id_aliases.json。

验证(实测)
- @build 0 错 0 警:Code=236456 RO=28124 RW=1876 ZI=137084;
  RAM +464 B(RW +304 / ZI +160),RW_IRAM1 62.2% / RW_CCM 87.9%,
  体积同码凭证与 RAM 台账已回填。
- check_menu_rows 8 状态 rc=0(池 5368/5448 常量未动);
  check_content_pages 60 页 rc=0;check_font_charset / registration /
  coding-standard / bom-drift / mojibake / doc-links --all / layer-deps /
  nested-comment 全 rc=0;check_ids --strict 无错误;主机单测
  ui_data 295 / boot_stats 21 / sys_log_mcu 141 / soe 32 / soe_store 27。

未做(不是遗漏)
- 六页真机判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、
  拔 RTC 电池落 RESTARTED、IWDG 复位落 rst_cause、主站连一次 conn_up+1)
  已登记 docs/任务规划/登记簿.md §7;模拟器结构上照不到。
- docs/产品需求/产品需求设计文档.md 属另一条在飞改动,本笔不含;
  其中我已同步 6 处现状数字(28 叶 / 60 页 / 218 字),随该笔入库。
2026-10-28 07:23:16 +08:00

312 lines
20 KiB
C
Raw Permalink 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 页」按此做——五个页面已于
* 2026-10-28 落地:ui_page_accident / oper / alarm / start / yk,各页只读一个文件)。
*/
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 */