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。
199 lines
7.6 KiB
C
199 lines
7.6 KiB
C
/*
|
||
* 模块名称: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);
|
||
}
|
||
}
|