67c469fa40
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。
104 lines
5.6 KiB
C
104 lines
5.6 KiB
C
/*
|
||
* 模块名称: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 */
|