Files
STM32F4-Base/Drivers/BSP/UI/ui_w_soe.c
T
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

199 lines
7.6 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.
/*
* 模块名称:UI SOE 记录页共享组件实现(ui_w_soe)
* 模块功能:见 Drivers/BSP/UI/ui_w_soe.h——事件类型/元件显示名映射 + 每记录 2 行
* 渲染 + 单条读盘缓存;数据经注入 reader 取,未注入按空态渲染
* 适用范围:Drivers/BSP/UI
* 说明:与 ui_w_records.c 同构(同一份"分页读记录 → 两行/条 + 实例缓存 + 空态
* 伪行"骨架),差别只在记录形态是二进制解码后的视图结构
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_w_soe.h"
/* 私有宏定义区 */
/*
* 框架文本缓冲宽度(真源 Drivers/BSP/UI/ui_tokens.h 的 UI_BUF_STRLEN = 32)。
* 这里**有意不 include ui_tokens.h**:该头经 ui_port.h 牵入整条 LVGL 头链,而本组件
* 是纯数据 + 纯文本搬运、一个 LVGL 调用都没有——容量宏本地化之后,
* test/host/ui_data 可以直接把本 .c 编进主机单测核对显示名映射(同
* Drivers/BSP/UI/ui_w_records.h 把 ts/desc 容量宏本地化的同一先例与理由:
* 容量不足只会退化成占位显示,不会越界读)。框架令牌若加宽,本宏同步加宽。
*/
#define SOE_BUF_MAX 32u
#define SOE_INVALID_STR "--" /* 未知编号/越界/读失败的统一占位(ASCII) */
#define SOE_EMPTY_STR "无记录" /* 空态伪行(与文本记录页同字) */
#define SOE_NO_CLOCK_STR "时钟无效" /* 时标行值列:IV 位不显示猜测时间 */
/* 静态变量定义区 */
/* 注入的 SOE reader(模块级单例:SOE 落盘本就是全局一份,两端各只有一份实现;
* NULL = 未注入 → 空态。当前两端均未绑定,理由见头注"说明") */
static const ui_soe_reader_t *s_reader;
/*
* 事件类型显示名(下标 = UI_W_SOE_T_*;表长 = UI_W_SOE_T_COUNT)
* 词长分两档是**版式约束**不是措辞偏好:值列可用宽约 59 px(4 汉字),
* 名列约 94 px(7 汉字)——"动作/返回"是元件行的值列词,其余四类 4 字词
* 落在非元件事件的名列(口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条)
*/
static const char *const s_type_name[UI_W_SOE_T_COUNT] = {
SOE_INVALID_STR, /* 0 未知/未映射(含读失败) */
"动作", /* 1 元件动作 */
"返回", /* 2 元件返回 */
"压板退出", /* 3 软压板退出 */
"压板投入", /* 4 软压板投入 */
"采样闭锁", /* 5 采样故障闭锁 */
"采样恢复", /* 6 采样故障恢复 */
"未标定闭锁", /* 7 未标定闭锁(照 App/log/soe_rec.h 与 数据可信 卡5 的原词,
* 不缩成"标定闭锁"——那个写法可以读成"把标定闭锁了",
* 而这一行要说的是"因未标定而闭锁",一字之差含义相反) */
"标定复归", /* 8 标定复归 */
};
/* 元件显示名(下标 = UI 元件编号 = 保护元件枚举序;越界不入表) */
static const char *const s_elm_name[UI_W_SOE_ELM_N] = {
"过流Ⅰ段", /* 0 */
"过流Ⅱ段", /* 1 */
"过流Ⅲ段", /* 2 */
"过负荷", /* 3 */
"过压", /* 4 */
"欠压", /* 5 */
"零序过压", /* 6 */
"过频", /* 7 */
"低频", /* 8 */
};
/* 函数定义区 */
/*
* 函数功能:注入 SOE reader
*/
void ui_w_soe_bind(const ui_soe_reader_t *a_reader) {
s_reader = a_reader;
}
/*
* 函数功能:把定长源串写入框架文本缓冲
* 入口参数:a_dst - 框架给的名/值缓冲(宽 = SOE_BUF_MAX,与框架 UI_BUF_STRLEN 同值) char *
* a_src - 源串(Flash 字面量或实例缓存) const char *
* src_bytes - 源缓冲总宽(含 NUL,整块拷入以带上终止符) size_t
* 返回值:无
* 限定条件:两者非 NULL;src_bytes 超 SOE_BUF_MAX 即按上限截断拷贝
* 函数说明:不用 strcpy/sprintf(门禁 CODE-RULE-06 禁用不安全函数);本组件
* 只做"整块搬运",不做格式化——格式化属页面行表(表现层)的事
*/
static void soe_put(char *a_dst, const char *a_src, size_t src_bytes) {
if ((a_dst == NULL) || (a_src == NULL)) {
return;
}
if (src_bytes > SOE_BUF_MAX) {
src_bytes = SOE_BUF_MAX;
}
(void)memcpy(a_dst, a_src, src_bytes);
}
/*
* 函数功能:事件类型编号 → 显示名
*/
const char *ui_w_soe_type_name(uint8_t a_type) {
if (a_type >= UI_W_SOE_T_COUNT) {
return SOE_INVALID_STR;
}
return s_type_name[a_type];
}
/*
* 函数功能:元件编号 → 显示名
*/
const char *ui_w_soe_elm_name(uint8_t a_elm) {
if (a_elm >= UI_W_SOE_ELM_N) {
return SOE_INVALID_STR;
}
return s_elm_name[a_elm];
}
/*
* 函数功能:总行数(每条记录 2 行;空态 = 单条伪行"无记录")
*/
uint16_t ui_w_soe_count(ui_w_soe_t *a_inst) {
uint32_t n;
(void)a_inst;
n = (s_reader != NULL) ? (uint32_t)s_reader->count() : 0u;
if (n == 0u) {
return 1u; /* 空态伪行(页脚"共 1 页",避免"共 0 页") */
}
return (uint16_t)(n * 2u);
}
/*
* 函数功能:逐行取数(偶数行 = 时标,奇数行 = 元件 + 事件类型)
*/
void ui_w_soe_refresh(ui_w_soe_t *a_inst, uint16_t a_row, char *a_name, char *a_value) {
uint32_t n;
uint32_t rec;
uint8_t ok;
a_value[0] = '\0';
n = (s_reader != NULL) ? (uint32_t)s_reader->count() : 0u;
if (n == 0u) {
(void)memcpy(a_name, SOE_EMPTY_STR, sizeof(SOE_EMPTY_STR));
return;
}
rec = (uint32_t)a_row / 2u;
ok = 0u;
if (a_inst->cache_valid != 0u && rec == a_inst->last_rec) {
ok = 1u; /* 同一条记录的两行共用一次读盘 */
} else if (s_reader != NULL) {
if ((s_reader->get)(rec, &a_inst->rec) == 0) {
a_inst->last_rec = rec;
a_inst->cache_valid = 1u;
ok = 1u;
} else {
a_inst->cache_valid = 0u; /* 越界/读失败:本行退化为占位,不留脏缓存 */
}
}
if (ok == 0u) {
(void)memcpy(a_name, SOE_INVALID_STR, sizeof(SOE_INVALID_STR));
return;
}
if (((uint16_t)a_row % 2u) == 0u) {
/* 时标行:IV 位或未对时 → 名列 "--"、值列"时钟无效",绝不显示猜测时间 */
if ((a_inst->rec.flags & UI_W_SOE_FLG_IV) != 0u || a_inst->rec.ts[0] == '\0') {
(void)memcpy(a_name, SOE_INVALID_STR, sizeof(SOE_INVALID_STR));
(void)memcpy(a_value, SOE_NO_CLOCK_STR, sizeof(SOE_NO_CLOCK_STR));
} else {
soe_put(a_name, a_inst->rec.ts, sizeof(a_inst->rec.ts));
}
return;
}
/* 内容行:元件事件 = 元件名 + 动作/返回;非元件事件 = 类型名(名列) */
if (a_inst->rec.elm != UI_W_SOE_ELM_NONE) {
const char *elm = ui_w_soe_elm_name(a_inst->rec.elm);
soe_put(a_name, elm, strlen(elm) + 1u);
/* 只有动作/返回两类是元件级事件;其余编号(理论不该出现)仍给类型名 */
if (a_inst->rec.type == UI_W_SOE_T_ELM_ASSERT
|| a_inst->rec.type == UI_W_SOE_T_ELM_RETURN) {
soe_put(a_value, ui_w_soe_type_name(a_inst->rec.type),
strlen(ui_w_soe_type_name(a_inst->rec.type)) + 1u);
}
} else {
const char *tn = ui_w_soe_type_name(a_inst->rec.type);
soe_put(a_name, tn, strlen(tn) + 1u);
}
}