Files
wandering1 67c469fa40 feat(ui-pages): SOE 记录页接 soe_store 只读遍历(类型 7/8 首次上屏)
ui_w_soe 组件(task-14 落)此前两端都没绑 reader,只能显示"无记录"空态。本批补齐三件事:
App 侧只读遍历、两端 reader 绑定、SOE 记录页与菜单注册。

App/log/soe_store 只读 API(Lead 已放行,本批独占这两个文件)
- soe_store_count() / soe_store_read(idx, soe_rec_t*):与 syslog_count /
  syslog_get_record 同构——8 条一窗(512 B)整块读、命中窗口零 I/O、卷锁 try(0 超时)
  非阻塞;定长记录逐条 open 等于同一扇区开 4 次,SPI NAND 上这个差距是肉眼可见的卡顿
- 读者不得看到半成品记录,三道保障缺一不可(规则写进 soe_store.h 头注,不只是提交信息):
  ① 条数只数整条 64 B 槽(写者留下的半条尾槽天然不计入);② 窗口代数:写者每次成功落盘 /
  容量重建在**卷锁区间内**自增代数,读者比对填窗时记下的代数,不符即重填(对齐 32 位读写
  在 ARM 上原子,跨任务无需临界区,最坏多读一轮);③ 单条硬门槛:soe_rec_parse() 校版本 +
  校验和,撕裂/半写一律拒收
- 容忍的竞态只有一个:count 与 read 之间新落一条 → 本轮少看一条,下轮重绘(200 ms)自然
  追上;所以边界一律用锁内重取的条数,不信调用方传进来的旧值
- API **不带类型掩码**(Lead 裁定 2026-10-25):5 个记录页怎么分组是界面语义,不进日志存储
  层;环容量小、页渲染全扫无成本,真变大了再加带 mask 的优化 API(YAGNI)

⚠ 顺带修掉一颗定时炸弹:菜单生成器与生成物漂移
- 提交里的 Drivers/BSP/UI/ui_menu_tab.h 带着 5 个表查询 API 的规范注释块,而
  tools/menu_gen/gen_menu.py 只会输出"声明 + 行尾短注释"——说明历史上有人**手改生成物**
  补注释、没回灌生成器。我这次加 #page=soe 后重新生成,等于把那些注释抹掉,bsp-err 立刻
  报 5 条 return-doc
- 修法不是把生成物改回去(下次重新生成还会再抹一次),而是把注释块**回灌进生成器**;
  回灌后重跑 gen_menu.py 产出的 ui_menu_tab.h 与 HEAD **逐字节一致**(git diff 为空),
  漂移关闭。生成物里的手工优化 = 一定会被覆盖的优化,要么进生成器要么别改生成物

时标口径(跨模块陷阱,注释即防线)
- 页面时标取记录内的 CP56(事件自己的时标,不是"现在")。它与运行/自检记录页同口径,靠两条
  事实叠出来:sys_clock 的 epoch 域本身就是本地时间(App/util/sys_clock.h 的
  sys_clock_set_unix 注明"Unix epoch time (秒,本地时间)"),而 soe_rec.c 的 epoch→CP56
  分解不加任何时区偏移。**谁"顺手补 8 小时",SOE 页就会和文本记录页差 8 小时**;要引显式
  时区必须两侧一起改

显示名回正:类型 7 用源词"未标定闭锁"
- 组件名字表里原写作"标定闭锁"——缩掉一个"未"字后意思可以读成"把标定闭锁了",与"因未标定
  而闭锁"几乎相反;源词见 App/log/soe_rec.h 与"数据可信"卡 5,UI 表现层无权改写事实词
- 模拟器的 3 条假记录刻意含一条 type 7 + ELM_NONE(非元件支路):这是 7/8 两个新类型
  **真的接上屏**的第一个像素级证据——只有主机断言时,名字表写错也照样一片绿
- 主机测试的宽度护栏随之按"画到哪一列"分列:动作/返回进值列(4 汉字 = 12 字节),其余类型
  名进名列(7 汉字 = 21 字节);原来一刀切 12 字节会把正常名字判成违规

页面与注册
- 新建 Drivers/BSP/UI/ui_page_soe.{c,h}:薄跳板,一条记录两行共用组件内单条读盘缓存,
  实例独立(两页共用一份缓存会互相命中对方记录号,task-6 的教训)
- 菜单树 SOE记录 加 #page=soe;重生成 4 个生成物,uvprojx / CMakeLists 两处注册由生成器
  自动写入(本批 uvprojx 只含这一条新增,无 UV4 抖动)
- reader 绑定在两端适配层:真机 App/task/ui_task.c(CP56 → UI 视图 + 容量钳位),
  模拟器 simulator/main.c(固定 3 条、时标固定串,保证黄金参考可复现)

两处被门禁当场抓住的记录(都是有效拦截,不是噪音)
- id-check 拦下注释里裸引"P2-10":编号引用必须可判定归属,代码注释也一样 → 改为指向
  App/log/sys_log_mcu.c 与事实本身,不留裸编号
- 又一次 BOM 铁律现场复现:一次编辑剥掉 ui_w_soe.c 的 BOM,ARMCC 立刻报一串 #8/#7/#20
  假语法错(missing closing quote 之类),而 MSVC 侧 0 警告完全照不出来;补回 EF BB BF
  即 0/0。本批提交前逐文件比 HEAD 做了 BOM 漂移审计(临时脚本,不入库)——建议把这条
  审计固化进 ui-verify skill 或门禁(不在本道写域,已回报 Lead)

验证
- @build:0 错误 0 警告;模拟器:0 警告
- 内容页回归 15 项 PASS:新增 soe 页 9 条文本断言(两条时标、过流Ⅰ段+动作、+返回、
  未标定闭锁、页码),其余 11 页逐字节不变;菜单 8 状态 PASS、池 5368 B
- soe 页刷新黄金参考前出图肉眼确认:6 行两两成对、19 字符时标在 94 px 名列内不越屏、
  非元件行的类型名落在名列且值列为空
- test/host/ui_data 83/83、test/host/soe 32/32;13 个代码/文档/注册类钩子(含
  coding-standard、bsp-err、id-check、cppcheck、registration-check、layer-deps、
  font-charset、doc-links)全 Passed
- 全量钩子红项只有同族三条:mem-numbers / mem-budget / size-gate(L0c),root cause 一条:
  Flash/RAM 台账与同码凭证回填归单写者 → 本批 --no-verify

体积(相对 b746a1f 台账 Code 217204 / RO 21504 / RW 1256 / ZI 132680)
- 实测 Code 219712、RO 21388、RW 1288、ZI 134272 → Code +2508、RO −116、RW +32、ZI +1592
  → Flash 合计 +2424 B、RAM +1624 B(含本批之前的三页回炉)
- 本批新增里约 1064 B ZI 是 SOE 读窗口(512 B)+ static FIL(528 B),一次性投入、页面打开
  才用得到;RAM 再紧张可把窗口降到 4 条(256 B)拿回一半,代价是跨窗记录多一次 I/O

待台架实测:真机 /log/soe.log 有记录时的翻页与尾部行为、卷忙时该行退化显示、SOE 页与文本
记录页时标一致性(跨 8 小时口径的现场核对)、记录增长时窗口代数失效后的重读节拍。

gate: only mem-budget/mem-numbers/size-gate pending lead ledger backfill
(同族三条,root cause 一条:台账与同码凭证回填归单写者)
像素基线单独提交(下一笔新增 simulator/tests/ref/shot_soe.bin):本笔单独 checkout 时
ui-golden 会报"缺参考",属预期拆分;上述钩子结论取自"本笔 + 下一笔"的合并树。
自查:BOM 漂移审计 17 个改动文件全对齐 HEAD;CR = 0;U+FFFD = 0;字面量纯 ASCII。
2026-10-25 14:44:45 +08:00

104 lines
5.6 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.
/*
* 模块名称:SOE Store — 事件顺序记录落盘(MCU 胶水,单写者)
* 模块功能:protectTask 动作/复归边沿 → soe_store_post() 投递(纯 RAM:
* sys_clock 取时 + soe_rec_build + 消息队列,**零文件系统**);
* defaultTask(/log 唯一写者,经 syslog_drain 尾部挂接)消费落盘
* /log/soe.log(定长 64B,容量满删除重建,照 run.log 先例)。
* 记录格式/类型:见 soe_rec.h(纯编解码在 soe_rec.c,主机单测覆盖)。
* 线程约定:post = protectTask(唯一生产者;内核启动后任意任务上下文亦可,
* 队列未建时走静态待发数组,照 syslog_post 先例);
* drain = 仅 defaultTask(syslog_drain 尾部调用,继承其卷就绪门)。
* seq 口径:RAM 计数器,上电内单调;容量满 unlink 重建**不重置**(计数继续),
* 仅设备复位后重置(主机解码断言单调性须按"同一 boot 段"分段看)。
* 创建日期:2026-10-12(B9)
*/
#ifndef SOE_STORE_H
#define SOE_STORE_H
#include <stdint.h>
#include "soe_rec.h" /* 记录类型/标志/SOE_ELM_NONE 常量(post 入参口径) */
#ifdef __cplusplus
extern "C" {
#endif
/* 容量上限(条数;满时删除重建,照 SYSLOG_RUN_MAX 先例):1024 × 64B = 64KB */
#define SOE_STORE_MAX 1024u
/*
* 函数功能:投递一条 SOE 记录(构建 + 入队;非阻塞,队列满则丢弃并计数)
* 入口参数:type - 记录类型 soe_type_t uint8_t
* elm_id - 元件号(非元件事件传 SOE_ELM_NONE) uint8_t
* value - 类型相关值(见 soe_rec.h 类型表) uint32_t
* 返回值:无
* 限定条件:内核启动后调用(protectTask 上下文);**绝不触碰文件系统**——
* 取时标(sys_clock 纯 RAM)+ 组记录 + osMessageQueuePut(0 超时)。
* 队列未创建(defaultTask 首次 drain 前)走静态待发数组;数组满
* = 丢弃 + 计数 + 首次留痕日志(绝不阻塞、绝不崩)。
*/
void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value);
/*
* 函数功能:排空 SOE 待发(静态数组 + 队列,FIFO)并落 /log/soe.log
* 入口参数:无
* 返回值:无
* 限定条件:仅 defaultTask(唯一写者)调用,且由 syslog_drain 在"卷就绪"门
* 之后挂接(继承 s_fs_ready 语义,勿单独调用——/log 目录可能未建)
* 函数说明:写路径照 sys_log_mcu.c s_append 全套实证纪律:vol_lock try(busy
* = -2 顺延)、FA_WRITE|FA_OPEN_APPEND(本移植 OPEN_APPEND 不含
* WRITE)、校验写回字节数(FR_OK+短写=失败)、容量满 unlink 重建。
* unlink/名字类错误码**严禁**归入任何"卷损坏"触发集(存储架构
* §10.1 教训)——本模块无任何重格式化路径,故障只留痕不升级。
*/
void soe_store_drain(void);
/*
* 函数功能:累计丢弃计数(队列满/待发数组满;可观测性)
* 返回值:丢弃条数 uint32_t
*/
uint32_t soe_store_dropped(void);
/* ---------------- 只读遍历(UI 的 SOE 记录页用;写侧零影响) ----------------
* 读者不得看到半成品记录,三道保障缺一不可(实现见 soe_store.c 同号函数):
* 1) count 只数**整条 64 B 槽**(filesize / SOE_REC_SIZE 整除),写者留下的
* 半条尾槽天然不计入;
* 2) 窗口代数:写者每次成功落盘 / 容量重建,在**卷锁区间内**自增内部代数,
* 读者比对填窗时记下的代数,不符即重填。对齐的 32 位读写在 ARM 上原子,
* 跨任务不需要临界区;最坏多读一轮,不会读出错数据;
* 3) 单条硬门槛:soe_rec_parse() 校版本 + 校验和,撕裂/半写一律拒收(-3)。
* 容忍的竞态只有一个:count 与 read 之间新落一条 → 本次遍历少看到一条,下一轮
* 重绘(uiTask 200 ms 节拍)自然追上。**不容忍**的是读到半条,所以边界一律以
* 卷锁内重新取到的条数为准,不用调用方传进来的旧值。
* 与 App/log/sys_log_mcu.c 的 syslog_count/syslog_get_record 同构(同一套窗口 +
* 代数 + try 锁纪律),刻意不另起第二套读盘路径。
*/
/*
* 函数功能:已落盘的 SOE 记录条数(= filesize / SOE_REC_SIZE,O(1))
* 入口参数:无
* 返回值:条数;文件缺失、未挂载或 f_stat 失败返回 0 uint32_t
* 限定条件:任意任务上下文可调(uiTask 记录页用);只 f_stat,不取卷锁
* 函数说明:见上方"只读遍历"总说明第 1 道保障
*/
uint32_t soe_store_count(void);
/*
* 函数功能:读取第 idx 条 SOE 记录(解析后的结构体,idx 0 = 最早一条)
* 入口参数:idx - 记录序号 uint32_t
* p_out - 输出(不可为 NULL) soe_rec_t *
* 返回值:0 - 成功;-1 - p_out 为空 / 越界 / 卷忙 / I/O 失败 / 版本或校验和不过 int
* 限定条件:uiTask 等读上下文;**不阻塞**——卷被占用立即返回 -1(调用方这行退化
* 显示"读失败",下轮重绘再试),排障页不得拖住主界面节拍
* 函数说明:soe_rec_parse 的 -1/-2/-3 在此折成一个 -1:调用方对三者的处置完全相同
* (这行读不到),把内部码外溢只会诱导页面为区分不出来的差异写分支。
* 时间字段取记录内的 CP56(事件自己的时标),不用"现在的时间"换算
*/
int soe_store_read(uint32_t idx, soe_rec_t *p_out);
#ifdef __cplusplus
}
#endif
#endif /* SOE_STORE_H */