feat(UI): 装置维护 3 页落地(时钟 / 强制复归 / 清除记录),内容页黄金 27→40

三个薄页面模块 ui_page_clock / ui_page_reset / ui_page_clear,页面注册由生成器
自动写双端(裁定 A-1:「三叶各绑一页」与「零新增 .c」不可兼得,选薄页面加生成器,
不写手写 node switch——那种分发在增删叶子时会静默换功能,且没有任何门禁照得到)。

数据侧是注入式函数表:UI 不 include App 层的头。两条硬理由都实测过——分层方向
(layer-deps)之外,模拟器根本不链 sys_clock,ALLOW 豁免挡得住 layer-deps,
挡不住模拟器链接期。面板对时的唯一真判据是回读 RTC:sys_clock_set() 恒返 0
(RTC 写失败只打日志),所以真机侧写 1 后回读 1,最坏约 200 ms 落在一次「保存」
按键上,IWDG 800 ms 窗有余;上界与依据都写进注释。

「无时钟」徽标位置有两个像素级实测坑:放标题带右侧会被 4 字标题连成「时钟设置
无时钟」一句话;放页脚带左侧 x=7 会压住页脚——页脚从 x=6 起排,不是居中。
最终落在清单末行与页脚之间的空带 y=124。clear_done 那张顺带给出像素证据:
清 run.log 之后「操作记录」那一行从 2 变 3,页面上没缓存、没撒谎。

ui_w_records 的 UI_W_REC_LOG_* 顺序必须等于 syslog_id_t 顺序(7 类,只准尾追加),
已加编译期顺序守卫;执行侧 7 条映射表同样带编译期守卫。

测试与真值:内容页黄金 27 → 40 页(+13),文本断言 +68、反向断言 ABSENT +19;
27 张旧参考 git diff 为空 = 逐字节未变。往返池 5448 B 与菜单常驻堆 5368 B 均未变;
字库未扩(源码在用 171 并 KEEP_EXTRA 39 = 清单 183),font-charset 与 cn-chars、
cn-font 字节一致。@build 0 错误 0 警告;uiTask 静态最坏链 1,336 B / 16,384
(余 91%)与基线相同,页面钩子没进最坏链。

一条测试学结论单独留痕:反向断言不能当「位置不重叠」的证据——本脚本的字形匹配
把 y 带判得比字面宽(徽标在 125..136、页脚在 144..155,仍被 (140,160) 判命中)。
该防线改由正向断言「页脚未被徽标压住」承担(压字那一版实测过不了它)。

两条自我更正一并记档(作者自报,按真值而非原口径):
① 先前报「文案零新增汉字」是错的——观测行拟用「动作元件」,「元」不在子集,
cn-chars 与 font-charset 如实红;最终改「保护动作 N」才让「零新增」成为事实。
顺序是:先按机器红,再按真值写结论。
② 先前报「6 个文件 BOM 逐个核过」被 bom-guard 证伪:丢 BOM 发生在上一次编译之后,
正是 AGENTS 那句「历史 0 警告不代表安全」的活例;此后一律以退出码为准。

未覆盖项不假装:台架「N>0 → N=0」在产品构建的未标定闭锁期拿不到(闭锁期 N 恒 0),
判据显式退化为「页面受理 + 落一条操作记录」,写在 rs_watch 注释里。

生成物入库例外照用:uvprojx 只加 3 个 ui_page_ 的 FilePath(真增删源文件),
CMakeLists 同批;两处都由 gen_menu.py 自己写,没手写。

本笔先走了一次真钩子:20 条钩子实跑通过(ui-golden 菜单 8 状态加内容页 40 页、
generated-fresh、registration-check、font-charset、ui-data 单测、cppcheck、
coding-standard、bom、mojibake、id-check、doc-links 等),只有 mem-numbers 与
mem-budget 两条红——磁盘上的 .map 出自 20:39 那次构建、含三条在飞道的代码,
与 HEAD 台账必然不同码。按仓库既有约定(这两条等台账回填时允许 --no-verify 并
正文留证)落这一笔;回填由我统一在收尾一笔做。

三条在飞道(task-26 崩溃证据路径、task-16 与 task-17 谓词收敛和卷锁观测、
task-36 台架工具)的未暂存改动被 autostash 暂时收走,钩子看到的是 HEAD 加本笔;
过程中曾把 comm 道未跟踪的 iec104_comm.c 临时移出(它不在 stash 覆盖面内),
那次移出反而让 ui-golden 撞见 CMake 缓存里的悬空引用而红——已改用「先落 comm
那笔、再落本笔」的顺序消掉这个中间态。ui_data_maint_ready() 待删(只有测试在读
的自检口),排在本笔之后单独一笔。
This commit is contained in:
2026-10-26 22:43:54 +08:00
parent 45086cf5f9
commit 52e28885cb
34 changed files with 2854 additions and 175 deletions
+159 -3
View File
@@ -85,12 +85,113 @@ PAGES = [
("netcfg_ro", "ddddde de e"), # 查看密码 000 → 只读态
("netcfg_edit", "ddddde de uruuruuue e"), # 编辑模式:IP 首字节光标反显
("netcfg_save", "ddddde de uruuruuue dddd e"), # 保存行 ENTER → 值列 OK
# ---- 道F task-3 装置维护三页(根菜单第 5 项 = 4 下;子项 0/1/3 = 时钟/复归/清除)
# 键序读法与上面同源:e 进子菜单后再 e 打开叶子(叶子带 #P → 先落密码页),
# "uruuruuue" = 修改口令 123(可编辑态),单一个 e = 查看口令 000(只读态)。
# 空格是分组占位:模拟器键映射把空格当可读分隔跳过(simulator/main.c 键映射)
("clock", "dddd ee uruuruuue"),
("clock_ro", "dddd ee e"),
("clock_edit", "dddd ee uruuruuue e"),
("clock_save", "dddd ee uruuruuue ddddd e u e d e"), # 秒行 +1 → 保存
("clock_noclock", "dddd ee uruuruuue"), # 同 clock 键序 + --noclock
("reset", "dddd ede uruuruuue"),
("reset_2nd", "dddd ede uruuruuue e"),
("reset_done", "dddd ede uruuruuue ee"),
("reset_ro", "dddd ede e"),
("clear", "dddd eddde uruuruuue"),
("clear_2nd", "dddd eddde uruuruuue e"),
("clear_done", "dddd eddde uruuruuue ee"),
("clear_ro", "dddd eddde e"),
]
# 文本级断言表:页名 -> [(名称, 期望串, y 带, 是否反显)]
# 只断言确定的内容;标签与值分列断言(两列 x 不同,不能拼成一串)。
# 覆盖本工程新增/易缺字的关键文案(网口设置/地址/掩码/逗号/保存/OK 等)。
TEXTS = {
# ---- 道F task-3 装置维护三页。行槽沿用列表页口径(首行 24+14i),
# 操作页四行 y = 44/66/88/110(Drivers/BSP/UI/ui_page_action.c 的 AC_*_Y)。
# 只断言"这一屏此刻该有什么",不拿参考图当断言(参考图会跟着实现一起错)
"clock": [
("标题", "时钟设置", (0, 24), False),
("r0名", "年", (24, 38), True), ("r0值", "2026", (24, 38), True),
("r1名", "月", (38, 52), False), ("r1值", "10", (38, 52), False),
("r2名", "日", (52, 66), False), ("r2值", "25", (52, 66), False),
("r3名", "时", (66, 80), False), ("r3值", "08", (66, 80), False),
("r4名", "分", (80, 94), False), ("r4值", "00", (80, 94), False),
("r5名", "秒", (94, 108), False), ("r5值", "00", (94, 108), False),
("r6名", "保存", (108, 122), False), ("r6值", "ENTER", (108, 122), False),
("页脚", "第 1 页,共 1 页", (140, 160), False),
],
"clock_ro": [
("标题", "时钟设置", (0, 24), False),
("r0名", "年", (24, 38), True), ("r0值", "2026", (24, 38), True),
("r5名", "秒", (94, 108), False), ("r5值", "00", (94, 108), False),
("页脚", "第 1 页,共 1 页", (140, 160), False),
],
"clock_edit": [
("标题", "时钟设置", (0, 24), False),
("r0名", "年", (24, 38), True),
],
"clock_save": [
("标题", "时钟设置", (0, 24), False),
("r0名", "年", (24, 38), False), ("r0值", "2026", (24, 38), False),
("r5名", "秒", (94, 108), False), ("r5值改后", "01", (94, 108), False),
("r6名", "保存", (108, 122), True), ("r6值", "OK", (108, 122), True),
],
"clock_noclock": [
("标题", "时钟设置", (0, 24), False),
("无时钟徽标", "无时钟", (122, 140), False),
("页脚未被徽标压住", "第 1 页,共 1 页", (140, 160), False),
("r0名", "年", (24, 38), True), ("r0值空", "--", (24, 38), True),
("r6名", "保存", (108, 122), False),
],
"reset": [
("标题", "强制复归", (0, 24), False),
("问句", "确认复归保护?", (38, 60), False),
("观测行", "保护动作", (82, 102), False),
("键提示", "ENTER=确认 ESC=取消", (104, 126), False),
],
"reset_2nd": [
("标题", "强制复归", (0, 24), False),
("问句", "确认复归保护?", (38, 60), False),
("二次确认", "二次确认", (60, 82), False),
("观测行", "保护动作", (82, 102), False),
],
"reset_done": [
("标题", "强制复归", (0, 24), False),
("结果行", "已请求复归", (38, 60), False),
("观测行归零", "保护动作", (82, 102), False),
("返回提示", "ENTER=返回", (104, 126), False),
],
"reset_ro": [
("标题", "强制复归", (0, 24), False),
("问句", "确认复归保护?", (38, 60), False),
("只读态标", "LOCK", (60, 82), False),
("只读提示", "ESC=返回", (104, 126), False),
],
"clear": [
("标题", "清除记录", (0, 24), False),
("r0名", "运行记录", (24, 38), True),
("r1名", "自检记录", (38, 52), False),
("r3名", "保护告警", (66, 80), False),
("r5名", "操作记录", (94, 108), False),
("r6名", "遥控记录", (108, 122), False),
("页脚", "第 1 页,共 1 页", (140, 160), False),
],
"clear_2nd": [
("标题", "清除记录", (0, 24), False),
("确认问", "确认?", (24, 38), True),
],
"clear_done": [
("标题", "清除记录", (0, 24), False),
("结果", "OK", (24, 38), True),
("页脚", "第 1 页,共 1 页", (140, 160), False),
],
"clear_ro": [
("标题", "清除记录", (0, 24), False),
("r0名", "运行记录", (24, 38), True),
("r6名", "遥控记录", (108, 122), False),
],
# v2.0(道E task-15 回炉):6 行 Flash 字面量 -> 8 行真值;标签去全角冒号(要给值列
# 让出 MAC 的 102 px),行槽 = 38 + 13i。值全部来自模拟器固定帧(simulator/main.c 的
# sim_publish_data_frames)与 net_cfg 出厂副本 —— 逐行断言等于把这两处口径钉在像素侧。
@@ -383,6 +484,53 @@ TEXTS = {
# · 徽标只属于整定态(可改值但判据停走),不属于只读态,也不属于四个查看页;
# · 只读态没有"保存"这件事(无 commit → 组件不画保存行)。
ABSENT = {
# ---- 道F task-3:反向断言钉的是"规矩本身",不依赖参考图 ----
# 1970 那条是本页最值钱的反向断言:记录侧的占位串一旦漏进时钟页,
# 用户一次"照抄保存"就把 1970 写进 RTC(口径见 ui_page_clock.c 的 CK_INVAL 注)
"clock_noclock": [
("无效时标不显 1970", "1970", (0, 160), False),
("无效时标不显旧帧值", "2026", (0, 160), False),
("无效时标不显 08:00", "08:00", (0, 160), False),
# 徽标压页脚这件事**不用反向断言钉**:实测本脚本的字形匹配把 y 带判得
# 比字面宽(徽标在 125..136、页脚在 144..155,写 ("无时钟", (140,160)) 反
# 而报"出现了")——拿它当"位置不重叠"的证据会假红。真正的防线在 TEXTS 那
# 一侧:"页脚未被徽标压住"这条正向断言在压字的版本上实测过不了(就是
# 上一轮那次 FAIL),它才是这条位置规矩的机器防线。
],
"clock": [
("有效时标不该挂无时钟徽标", "无时钟", (122, 140), False),
],
"clock_ro": [
("只读页没有保存这件事", "保存", (0, 140), False),
],
"clock_save": [
("保存成功后保存行不再提示 ENTER", "ENTER", (108, 122), True),
],
# 操作页 hide_footer=1:页脚/页码都不该出现(出现 = 描述符没关掉内置区块)
"reset": [
("操作页无页脚", "第 1 页", (138, 160), False),
("未确认前不投递", "已请求复归", (0, 160), False),
],
"reset_2nd": [
("停在二次确认就没投递", "已请求复归", (0, 160), False),
("操作页无页脚", "第 1 页", (138, 160), False),
],
"reset_done": [
("回报后问句让位", "确认复归保护?", (38, 60), False),
],
"reset_ro": [
("只读不进二次确认", "二次确认", (0, 160), False),
("只读一条都不投", "已请求复归", (0, 160), False),
("只读不显示动作结果", "OK", (60, 82), False),
],
"clear": [
("没按过就不该有确认问", "确认?", (0, 160), False),
("没投递就不该有结果词", "OK", (0, 140), False),
],
"clear_ro": [
("只读不进确认态", "确认?", (0, 160), False),
("只读不投递", "WAIT", (0, 160), False),
],
"sparam_ro": [
("只读页无未标定徽标", "未标定", (0, 24), False),
("只读页无保存行", "保存", (0, 140), False),
@@ -416,7 +564,15 @@ STRIDE = W // 8 # I1:20 字节/行
POOL_AFTER_ROUNDTRIP = 5448
def run_sim(seq, out, cwd):
# 每页额外的模拟器旗标(道F task-3):clock_noclock 需要一台"墙钟无效"的机器,
# 而 rt 帧只有 --noclock 这一条路能装出来(真机侧对应"RTC 异常/未对时")。
# 不为它另造一套假数据:假的是"时钟有没有"这件事,页面与显示链全是真的。
EXTRA = {
"clock_noclock": ["--noclock"],
}
def run_sim(seq, out, cwd, extra=()):
"""在指定工作目录里跑一次无头截图。
模拟器按**工作目录**读写 `sim_config.ini`:目录里残留一份配置(尤其
@@ -424,7 +580,7 @@ def run_sim(seq, out, cwd):
拒绝保存 → 网口设置页保存行显示 ERR → 与"工厂默认态"的黄金参考对不上。
因此一律在空临时目录里跑,回到"无配置文件 = 工厂默认"这一确定态。
"""
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True,
r = subprocess.run([str(EXE), "--keys", seq, out] + list(extra), capture_output=True,
cwd=str(cwd))
if r.returncode != 0:
raise RuntimeError("lvgl_sim --keys %r rc=%d: %s"
@@ -589,7 +745,7 @@ def main():
shots = {}
for name, seq in PAGES:
shot = os.path.join(tmp, "shot_%s.bin" % name)
run_sim(seq, shot, tmp)
run_sim(seq, shot, tmp, EXTRA.get(name, ()))
shots[name] = open(shot, "rb").read()
n_pos = 0