7efa58d9a1
分两段(用户裁定 2026-11-02):先做地址迁移,再把同类字面量收敛成
单一真源,以后再改地址只动一处。
一、IP 迁移 192.168.1.100 → 192.168.31.100(gw 同步 → .31.1)
- 工厂默认:Drivers/BSP/NETCFG/net_cfg.c 的 NET_CFG_DEF_IP/_GW
- 注释/文档示例、主机与板级测试、台架 py 脚本、两份 sim_config.ini
- 字节序教学点同步换算 hex:0x6401A8C0 → 0x641FA8C0(内存
C0 A8 1F 64),旧版示例引文与历史 Trap 记录按冻结纪律不动
- UI 黄金基准:网口设置 4 态 + shot_info 共 5 张 .bin 由
check_content_pages.py --update 重刷(渲染值 IP 变了,属预期变更)
- 保留判定:客户点表引用(IEC104/继电保护规划)、芯片手册摘录、
日期冻结的变更记录行、已退役台架档案 MDK-ARM/retracted_bench_*
二、单一真源收口(A+B+C+D,用户批准全做)
- 新建 test/host/bench_defaults.py:台架 py 默认值唯一定义点
(DEVICE_IP/DEVICE_GW/PC_IP/NET_BCAST),22 个脚本改为引用;
MDK-ARM 两探针与 scripts/board_ui_nav.py 经 sys.path shim 引入
- test/board/net_test_task.h 新增 TEST_PC_PEER_IP,板测三处
PHASE*_REMOTE_IP 改引该宏(保留 -D 覆盖);Phase1 断言改
memcmp 数组比较,数组各只出现一次
- simulator/tests/check_content_pages.py 从 net_cfg.c 正则解析
NET_CFG_DEF_* 推导期望值(解析失败即报错、不静默兜底),
脚本内台架 IP 字面量清零
- 装饰性注释去具体值(net_cfg.h/net_socket.h/net_socket.c/
net_socket_sim.c);字节序教学 hex 对刻意保留为唯一写值点
- 刻意不动:主机单测夹具 IP(逐文件钉住的任意值是正确测试
实践)、文档正文 IP(那是文档内容本身)
顺带:scripts/gen_task_table.py 规范串清掉「,B5」——编号清退提交
72b4b094 改了受管区没重跑生成器,task-table 门禁因此一直红
验证:产品 @build 0/0;CH395F 套件开关构建 0/0 后还原产品态;
6 套主机单测 rc=0;content_pages(含宏推导路径)/菜单 8 态/
推流位比对全 PASS;bom-drift 99 无漂移;doc-links/coding-standard/
cppcheck 绿;py_compile 23 文件 + --help 冒烟(含 shim 绝对路径)
1179 lines
68 KiB
Python
1179 lines
68 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
check_content_pages.py — 叶子内容页像素黄金参考回归 + 文本级断言 + 往返池水位门禁。
|
||
|
||
用法:
|
||
python simulator/tests/check_content_pages.py # 文本断言 + 与本地黄金参考逐字节比对
|
||
python simulator/tests/check_content_pages.py --update # 有意改视觉后刷新黄金参考(改动会进 diff)
|
||
前置:simulator 已构建(build/Debug/lvgl_sim.exe 存在);纯标准库,任意 python3。
|
||
ref/*.bin 是**入库的像素黄金参考**(2026-10-22 起):新克隆直接可跑,无需先生成。
|
||
缺失 = 工作树不完整(`git checkout -- simulator/tests/ref/`),不是"首次运行"。
|
||
|
||
原理:① 文本级断言——解析 ui_font_cn_12.c 字形,把期望字符串(标题/标签/值/页脚)在截图
|
||
对应 y 带内做精确匹配;不依赖 ref/,先于像素比对运行,失败即中止(防误刷参考)。
|
||
② 每页用键序注入打开(info=e / ac=dee / dc=dede / pwd=ddeee 等,见 PAGES),无头截图
|
||
I1(160x160) 与 ref/shot_<page>.bin 逐字节比对——任何像素变化即 FAIL(含字体、几何、颜色)。
|
||
另做开页-关页往返池水位门禁:`es`(info)与 `ddeeesll`(密码页,ESC 后 ←← 弹出路上压的
|
||
2 个菜单框回根框,完全还原初始态)后 used 须等于 POOL_AFTER_ROUNDTRIP。
|
||
|
||
黄金参考更新流程(有意改视觉时):先肉眼看图确认新效果正确,再 --update 刷新,
|
||
刷新即改基准 → `git diff --stat simulator/tests/ref` 必须逐页过目并在提交正文说明;
|
||
最后 regen_ref_png.py 重生成目检 PNG;误报排查见 docs/UI设计/UI_自动化测试流程.md。
|
||
|
||
退出码:0 全 PASS;1 文本断言失败/有页不一致/池水位漂移;2 运行/环境错误(exe 缺失、参考缺失)。
|
||
"""
|
||
import os
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
# lvgl_sim.exe 相对本脚本定位(simulator/tests/ -> simulator/build/Debug/)
|
||
EXE = Path(__file__).resolve().parents[1] / "build" / "Debug" / "lvgl_sim.exe"
|
||
REF_DIR = Path(__file__).resolve().parent / "ref"
|
||
|
||
# 工厂默认 = 黄金参考的真源:Drivers/BSP/NETCFG/net_cfg.c 的 NET_CFG_DEF_* 宏。
|
||
# run_sim 一律在空临时目录跑(无 ini → 模拟器按编译期宏显示),故断言值必须与该宏
|
||
# 一致;这里现读现算,改默认 IP 只动 C 宏,本脚本零同步(2026-11-02 教训:本文件
|
||
# 曾钉死 7 处补零字面量,IP 迁移时逐处爆红)。
|
||
_NETCFG_C = Path(__file__).resolve().parents[2] / "Drivers" / "BSP" / "NETCFG" / "net_cfg.c"
|
||
|
||
|
||
def _read_net_cfg_default(name):
|
||
"""抽取 `#define name { a, b, c, d }` 四字节(容忍 u 后缀/任意空白)。
|
||
|
||
解析失败立即报错——绝不回退到写死旧值(宁可红在明处)。
|
||
"""
|
||
pat = r"#define\s+%s\s*\{\s*(\d+)u?\s*,\s*(\d+)u?\s*,\s*(\d+)u?\s*,\s*(\d+)u?\s*\}" % name
|
||
m = re.search(pat, _NETCFG_C.read_text(encoding="utf-8-sig"))
|
||
if not m:
|
||
raise RuntimeError("net_cfg.c 的 %s 解析失败:宏形态变了就同步本函数的正则(勿改回写死值)" % name)
|
||
return tuple(int(g) for g in m.groups())
|
||
|
||
|
||
def _fmt_ip03(octets):
|
||
"""net_cfg_fmt_ipv4 的既有口径:后三段补零成 3 位、首段原样(同一格式化真源)。"""
|
||
return "%d.%03d.%03d.%03d" % octets
|
||
|
||
|
||
EXP_IP = _fmt_ip03(_read_net_cfg_default("NET_CFG_DEF_IP"))
|
||
EXP_MASK = _fmt_ip03(_read_net_cfg_default("NET_CFG_DEF_MASK"))
|
||
EXP_GW = _fmt_ip03(_read_net_cfg_default("NET_CFG_DEF_GW"))
|
||
EXP_IP_TAIL = EXP_IP.split(".", 1)[1] # 编辑态首字节被光标反显覆盖,只断言尾部
|
||
|
||
# (页名, 注入键序):与 ui_menu.c 节点映射/路由一致(u=上 d=下 l=左 r=右 e=回车 s=ESC);
|
||
# 密码页 = 受保护叶子(参数定值/整定/参数 node 8,默认密码 123)
|
||
PAGES = [
|
||
("info", "e"), # 装置信息(node 1)
|
||
("ac", "dee"), # 交流量(node 3,实时数据下第 1 项)
|
||
("dc", "dede"), # 直流量(node 4,实时数据下第 2 项)
|
||
# 遥信量(实时数据下第 3 项):d 到实时数据 → e 进(选中交流量)→ dd 到遥信量 → e 开页;
|
||
# yxp2 再多按 8 次下移,让选中行跨过页边界落到第 2 页首行
|
||
("yx", "dedde"),
|
||
("yxp2", "dedde dddddddd"),
|
||
# SOE 记录(node 51,记录查询下第 1 项):6 下进记录查询 → e → 首项 SOE记录 → e;
|
||
# 数据 = simulator/main.c 的固定 3 条假记录(含 type 7 未标定闭锁这条非元件支路)
|
||
("soe", "dddddde e"),
|
||
# 参数定值 → 查看 的四个只读页(批 3):dd 进"参数定值"→ e 进(选中"整定")→ d 到"查看"
|
||
# → e 进(选中"参数")→ e 开页;定值/控制字/软压板 依次多按 d 选到该行再 e
|
||
("vparam", "ddedee"),
|
||
("vset", "ddedede"),
|
||
("vword", "ddededde"),
|
||
("vplate", "ddededdde"),
|
||
# ── 道F task-2:整定 4 页(参数定值 → 整定 → *,#P 叶,123=修改密码可编辑)──
|
||
# 导航公共前缀:dd 进"参数定值"→ e 进(选中"整定")→ e 进整定子框(选中"参数")
|
||
# → e 进密码页;密码 123 = "uruuruuue"(三框各 +1、两次右移、确认,与 netcfg 同串法);
|
||
# 000(直接 e)= 查看密码 → 只读。定值/控制字/软压板 在整定子框里多按 d 选到该行再 e。
|
||
# 保存行在第 (行数) 行:参数 9 行 = 2 页 → 8×d 落到保存行;定值 19 行 = 3 页。
|
||
("sparam_p2", "ddeee uruuruuue dddddddd"), # 参数整定第 2 页:保存行 ENTER
|
||
("sparam_ro", "ddeee e"), # 000 只读:无保存行、无徽标
|
||
("sparam_edit", "ddeee uruuruuue d e"), # PT二次 进编辑(值列是编辑串,无单位)
|
||
("sparam_oob", "ddeee uruuruuue d e l l d e"), # 编到 0(< min=1)→ ENTER 拒退 + 越界
|
||
("sparam_save", "ddeee uruuruuue e u e dddddddd e"), # PT一次 +1 → 保存行 ENTER → OK
|
||
("sparam_landed", "ddeee uruuruuue e u e dddddddd e u"), # 保存后回首屏:新值 10001V 已在帧里
|
||
("sset", "ddeed e uruuruuue"), # 定值整定首屏(18 项 + 保存 = 3 页)
|
||
("sword_cycle", "ddeedde uruuruuue e"), # 控制字 二态行 ENTER:跳闸 → 告警
|
||
("splate", "ddeeddde uruuruuue"), # 软压板整定首屏(10 项 + 保存 = 2 页)
|
||
("pwd", "ddeee"), # 密码页初始态:3 框 000、光标首框
|
||
("pwe", "ddeee uru"), # 密码页调值+移光标:110、光标第 2 框
|
||
# 密码页输 123:u / r / u,u / r / u,u,u / e → 关页并按授权"续做进入"。
|
||
# ⚠ **道F task-2 起这张图的落点变了**(黄金参考已 --update 刷新,属有意改判据):
|
||
# 整定 4 叶绑定内容页之前,123 确认后走 menu_password_ok 的"无内容页 → 压空白
|
||
# 终点框"分支(屏上是 4 框菜单,整定叶本身是个死叶子);绑定之后走
|
||
# "有内容页 → ui_content_page_open(page, UI_PERM_MODIFY)"分支,落在**整定-参数页**。
|
||
# 也就是说这张图现在同时钉住两件事:密码页授权链正确 + 123 真的能开可编辑整定页。
|
||
# 旧图并非"错",是那条分支从此不再可达——留档见 2026-10-26 提交正文。
|
||
("pwk", "ddeee uruuruuue"),
|
||
("pwc", "ddeee ue"), # 密码页 100 不一致 → 清空重输(回到 000 初始态像素)
|
||
# 网口设置(node 30,通讯参数下第 2 项,需密码):
|
||
# dddddd = 根框下移 5 至通讯参数;e 进框;d 选中网口设置;e 进密码页
|
||
("netcfg", "ddddde de uruuruuue"), # 修改密码 123 → 可编辑态(工厂默认值)
|
||
("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"),
|
||
# ---- 道F task-7 遥测整定两页(根菜单第 4 项 = 3 下;子项 0/1 = 死区/系数)
|
||
# 键序读法与 task-3 三页同源:e 进子菜单后再 e 开叶子(#P → 先落密码页),
|
||
# "uruuruuue" = 修改口令 123(可编辑)。
|
||
# 死区 9 行 + 保存 = 10 行(2 页);系数 8 行 + 保存 = 9 行(2 页,保存行独在第 2 页)。
|
||
# ⚠ wait/ok 两张**必须成对存在**:脏位在模拟器由 sim_yt_pump 清(镜像真机
|
||
# defaultTask 每轮无条件落盘),wait 态靠 --yt-nopump 停 pump 拍下来——
|
||
# 只钉 OK 就是"只钉最好看那一帧"(Lead 裁定 task-7 派发④原文),
|
||
# 真机 WAIT 窗口毫秒级、人工永远补拍不到,只有这里替它留了像素证据。
|
||
("deadband", "ddd ee uruuruuue"), # 首屏(选中首行 UA死区 0.500V)
|
||
("deadband_wait", "ddd ee uruuruuue e uu e ddddddddd e"), # UA→0.502 提交 → 保存行 ENTER:WAIT(+--yt-nopump)
|
||
("deadband_ok", "ddd ee uruuruuue e uu e ddddddddd e"), # 同键序、pump 正常 → OK(脏位真被清)
|
||
("calib", "ddd e de uruuruuue"), # 首屏 8 行系数(153uV = ROM TPAFE 整 µV)
|
||
("calib_wait", "ddd e de uruuruuue e u e dddddddd e"), # UA→154 提交 → 保存行:WAIT(+--yt-nopump)
|
||
("calib_ok", "ddd e de uruuruuue e u e dddddddd e"), # 同键序、pump 正常 → OK
|
||
# 拒后**再**移到保存行(不按保存):一张图同时钉"粘滞 ERR 词"与"越界行反显"
|
||
# (r0 反显要等光标移开才单独可见——与选中白条重叠时像素分不出来,这是一开始
|
||
# 拍在首屏的图目检后改出来的键序);不按保存是因为按键会重新评估、ERR 会被
|
||
# 新一轮 WAIT/OK 覆盖——ERR 的像素证据只有这条窗口是真的
|
||
("calib_reject", "ddd e de uruuruuue e ddd l ddddd l d e dddddddd"),
|
||
# ── 道G task-9:通讯设置页(comm,#P 叶;[comm] 工厂默认 CA=1 / 端口 2404)──
|
||
# 导航:ddddd 落到"通讯参数"(主界面第 6 个节点)→ e 进(首叶通讯设置被选中)
|
||
# → e 进密码页;123 = "uruuruuue" 可编辑,000(直接 e)只读。共 3 行不分页,
|
||
# 保存行恒在第 3 槽(y 带 52..66)。
|
||
("comm", "ddddd ee uruuruuue"), # 首屏:1 / 2404 / 保存行 ENTER
|
||
("comm_ro", "ddddd ee e"), # 000 只读:无保存行
|
||
("comm_edit", "ddddd ee uruuruuue e"), # CA 行进编辑(个位光标反显)
|
||
("comm_reject", "ddddd ee uruuruuue e d e"), # CA 编到 0(< min=1)→ ENTER 拒退
|
||
("comm_wait", "ddddd ee uruuruuue e u e dd e"), # CA 1→2 提交 → 下移到保存行 ENTER:WAIT(+--comm-nopump)
|
||
("comm_ok", "ddddd ee uruuruuue e u e dd e"), # 同键序、pump 正常 → OK(脏位真被清)
|
||
# ── 接线道 task-6:记录 5 页 + 运行报告(六叶同一笔绑 #page= 注记)────────────────
|
||
# 键序 = 主界面第 7 项「记录查询」(6 下)→ e 进子框(进框后首项 SOE记录 即被选中)
|
||
# → 再按 n 下选到目标叶 → e 开页。实测下数与 node 号严格对应:
|
||
# 1=事故 34 / 2=操作 35 / 3=保护告警 36 / 4=保护启动 37 / 5=遥控 38 /
|
||
# 6=自检 39 / 7=运行 40 / 8=运行报告 41(与 Lead 交接口径同源,逐个键序实测过)。
|
||
# 这五页在**模拟器侧没有写入方**(sim_rec_count 对这些编号恒 0)→ 拍到的是空态判据:
|
||
# 伪行"无记录"落在第 0 行,而第 0 行正是选中行(白底黑字)→ 文本断言一律 inv=True
|
||
# (实测 inv=False 匹不上,这是 ui_content_page 选中带的实现事实,不是 bug)。
|
||
("accident", "dddddde d e"),
|
||
("oper", "dddddde dd e"),
|
||
("alarm", "dddddde ddd e"),
|
||
("start", "dddddde dddd e"),
|
||
("yk", "dddddde ddddd e"),
|
||
# 运行报告(node 41):12 行 / UI_LIST_ROWS=8 → 真 2 页。**四个数逐个挂范围标签**是判据
|
||
# (docs/任务规划/UI补齐.md §2「运行报告」行),所以两张都必须拍——首屏钉上电次数/
|
||
# 计数范围/复位原因/主站接入 + 范围标签"本次开机",第 2 页钉 104 另外三枚(发送帧数
|
||
# 128 / 发送失败 0 / 接收字节 4096)与页脚真实页码 2/2。四枚假数据值互不相同是刻意的:
|
||
# 行错位/串行只有"像素 + 逐行文本断言"两把尺子同时才照得出来(Lead 裁定 2026-10-28,
|
||
# 因此当时用例是 53+7=60 张,不是 53+6=59;现集合 66 张,见 PAGES 表)。
|
||
("runreport", "dddddde dddddddd e"),
|
||
("runreport_p2", "dddddde dddddddd e dddddddd"), # 页内再 8 下 → 第 2 页(选中落 r0=发送帧数)
|
||
# ── 故障录波 UI 道(2026-10-29):手动录波三态 + 回放三态(真实波形/空态/坏段)──
|
||
# 导航:装置维护 = 根第 5 项(4 下)→ e 进子框(时钟被选中)→ dd 到第 3 项「手动录波」;
|
||
# 记录查询 = 根第 7 项(6 下)→ e 进(SOE 被选中)→ 九下落到组末新叶「录波回放」(node 42)。
|
||
# 假数据纪律:模拟器泵**喂真 ring + 走真 store 打包链**(simulator/main.c 录波假源块注释),
|
||
# 回放页拍到的是真阶梯(每点 3 px)、真 0 线、真峰值 mV——不是手画折线的像素摆拍。
|
||
# wave_play_bad 的「不可读」由**翻转数据区一字节让真双 CRC 判废**产生(--wave-bad),
|
||
# 不是把"不可读"写死在假件返回值里;wave_play_empty 用 --wave-empty 不建段。
|
||
("wave_manual", "dddd edde uruuruuue"), # 确认链首屏(stage0 问句)
|
||
("wave_manual_done", "dddd edde uruuruuue ee"), # 两次 ENTER → 受理回报行
|
||
("wave_manual_ro", "dddd edde e"), # 查看密码 000 只读 → LOCK 拒执行
|
||
("wave_play", "dddddde ddddddddd e"), # 数值最大段 UA:正弦阶梯 + 0 线 + 光标
|
||
("wave_play_bad", "dddddde ddddddddd e"), # 坏段:显式「该段不可读」,绝不画线
|
||
("wave_play_empty", "dddddde ddddddddd 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),
|
||
],
|
||
# ---- 道F task-7 遥测整定两页。行槽沿用列表页口径(24 + 14i),单位串由
|
||
# ui_w_setlist 直接贴数值尾("0.500V"无空格),µV 在屏上是 ASCII "uV"
|
||
"deadband": [
|
||
("标题", "遥测死区", (0, 24), False),
|
||
("r0名", "UA死区", (24, 38), True), ("r0值", "0.500V", (24, 38), True),
|
||
("r4名", "IA死区", (80, 94), False), ("r4值", "0.005A", (80, 94), False),
|
||
("r7名", "IN死区", (122, 136), False), ("r7值", "0.005A", (122, 136), False),
|
||
("页脚", "第 1 页,共 2 页", (140, 160), False),
|
||
],
|
||
"deadband_wait": [
|
||
("标题", "遥测死区", (0, 24), False),
|
||
# 第 2 页窗口:槽 0 = Hz死区行(跟随真源、本轮未动),槽 1 = 保存行(选中反显)
|
||
("r0名", "Hz死区", (24, 38), False), ("r0值", "0.500Hz", (24, 38), False),
|
||
("保存名", "保存", (38, 52), True), ("保存值", "WAIT", (38, 52), True),
|
||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||
],
|
||
"deadband_ok": [
|
||
("标题", "遥测死区", (0, 24), False),
|
||
("r0名", "Hz死区", (24, 38), False),
|
||
("保存名", "保存", (38, 52), True), ("保存值", "OK", (38, 52), True),
|
||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||
],
|
||
"calib": [
|
||
("标题", "遥测系数", (0, 24), False),
|
||
("r0名", "UA系数", (24, 38), True), ("r0值", "153uV", (24, 38), True),
|
||
("r7名", "IN系数", (122, 136), False), ("r7值", "153uV", (122, 136), False),
|
||
("页脚", "第 1 页,共 2 页", (140, 160), False),
|
||
],
|
||
"calib_wait": [
|
||
("标题", "遥测系数", (0, 24), False),
|
||
("保存名", "保存", (24, 38), True), ("保存值", "WAIT", (24, 38), True),
|
||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||
],
|
||
"calib_ok": [
|
||
("标题", "遥测系数", (0, 24), False),
|
||
("保存名", "保存", (24, 38), True), ("保存值", "OK", (24, 38), True),
|
||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||
],
|
||
"calib_reject": [
|
||
("标题", "遥测系数", (0, 24), False),
|
||
# 第 2 页(光标移到保存行、未按键):k=0 被执行侧拒 → **值纹丝不动**
|
||
# (副本未动、不置脏——"写值与置脏成对"的另一半判据)+ 保存行粘滞 ERR;
|
||
# r0 的越界反显在第 1 页像素里(文本断言不猜反显形状,逐字节比对钉住)
|
||
("保存名", "保存", (24, 38), True), ("保存值", "ERR", (24, 38), True),
|
||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||
],
|
||
# v2.0(道E task-15 回炉):6 行 Flash 字面量 -> 8 行真值;标签去全角冒号(要给值列
|
||
# 让出 MAC 的 102 px),行槽 = 38 + 13i。值全部来自模拟器固定帧(simulator/main.c 的
|
||
# sim_publish_data_frames)与 net_cfg 出厂副本 —— 逐行断言等于把这两处口径钉在像素侧。
|
||
"info": [
|
||
("标题", "装置信息", (0, 24), False),
|
||
("副标题", "馈线自动化终端", (22, 40), False),
|
||
("r0名", "终端型号", (38, 51), False), ("r0值", "F30", (38, 51), False),
|
||
("r1名", "软件版本", (51, 64), False), ("r1值", "SV0.010", (51, 64), False),
|
||
("r2名", "硬件版本", (64, 77), False), ("r2值", "HW0.010", (64, 77), False),
|
||
# 软件校验:构建期 CRC 步骤未落地 -> "--"(曾抄参考项目的 "4454",那是别人家镜像)
|
||
("r3名", "软件校验", (77, 90), False), ("r3值", "--", (77, 90), False),
|
||
("r4名", "程序日期", (90, 103), False), ("r4值", "Oct 25 2026", (90, 103), False),
|
||
("r5名", "MAC地址", (103, 116), False),
|
||
("r5值", "00:00:00:00:00:00", (103, 116), False),
|
||
# net_cfg_fmt_ipv4 的既有口径是三段补零(与网口设置页同一格式化真源),照实断言
|
||
("r6名", "IP地址", (116, 129), False), ("r6值", EXP_IP, (116, 129), False),
|
||
("r7名", "运行时长", (129, 142), False), ("r7值", "00:00:00", (129, 142), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
# 交流量:7 行快照真值(24 行参考图假骨架已删)。eng=0(未标定)期间值列是 ADC 引脚
|
||
# 电压,第 7 行必须把口径写在屏上;UB/UC/IB/IC/IN 削顶无采样源,本页不列它们
|
||
# (列出来就是 5 行 "--" 的假骨架,同 §3.1.1 的取舍理由)。Hz 未测出 -> "--"。
|
||
"ac": [
|
||
("标题", "交流量", (0, 24), False),
|
||
("r0名", "Ua", (24, 38), True), ("r0值", "0.812", (24, 38), True),
|
||
("r1名", "U0", (38, 52), False), ("r1值", "0.005", (38, 52), False),
|
||
("r2名", "Ia", (52, 66), False), ("r2值", "0.046", (52, 66), False),
|
||
("r3名", "Ia2", (66, 80), False), ("r3值", "0.002", (66, 80), False),
|
||
("r4名", "Ia5", (80, 94), False), ("r4值", "0.001", (80, 94), False),
|
||
("r5名", "Hz", (94, 108), False), ("r5值", "--", (94, 108), False),
|
||
("r6名", "标定", (108, 122), False), ("r6值", "未标定", (108, 122), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
# SOE 记录:每条记录 2 行(时标 / 元件名+动作-返回)。三条假记录刻意覆盖三条显示
|
||
# 支路:元件事件、元件复归、**非元件**的未标定闭锁(type 7,类型名进名列)。
|
||
# 第 5 行是"新增事件类型有没有接上屏"的像素级证据——只测 1/2 的话,7/8 写错也绿。
|
||
"soe": [
|
||
("标题", "SOE记录", (0, 24), False),
|
||
("r0时标", "2026-10-25 08:00:01", (24, 38), True),
|
||
("r1名", "过流Ⅰ段", (38, 52), False), ("r1值", "动作", (38, 52), False),
|
||
("r2时标", "2026-10-25 08:00:03", (52, 66), False),
|
||
("r3名", "过流Ⅰ段", (66, 80), False), ("r3值", "返回", (66, 80), False),
|
||
("r4时标", "2026-10-25 08:00:05", (80, 94), False),
|
||
("r5名", "未标定闭锁", (94, 108), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
# ── 批 4:遥信量(33 点固定名表,5 页)──
|
||
# 第 1 页 8 行**全是 --**(位置 1~4 是无硬件的双点,YX5~8 点表未定义)——这不是画面坏了,
|
||
# 是 V0.1 的真实状态:27 个点没有开入来源。方案 B(只显有效点)会把这件事藏起来,方案 A
|
||
# 让现场能逐点对、也能一眼看出缺哪些点(取舍见 页面扩充详细方案.md §3.1.3)。
|
||
# 第 2 页才出现有来源的点(故障总 / 过流Ⅰ段 = 复归);两页合起来同时钉住分页数学:
|
||
# 33 行 / 8 行每页 = 5 页,页码算错这里直接响。
|
||
"yx": [
|
||
("标题", "遥信量", (0, 24), False),
|
||
("r0名", "位置1", (24, 38), True),
|
||
("r0值", "--", (24, 38), True),
|
||
("r1名", "位置2", (38, 52), False),
|
||
("r1值", "--", (38, 52), False),
|
||
("r2名", "位置3", (52, 66), False),
|
||
("r2值", "--", (52, 66), False),
|
||
("r3名", "位置4", (66, 80), False),
|
||
("r3值", "--", (66, 80), False),
|
||
("r4名", "YX5", (80, 94), False),
|
||
("r4值", "--", (80, 94), False),
|
||
("r5名", "YX6", (94, 108), False),
|
||
("r5值", "--", (94, 108), False),
|
||
("r6名", "YX7", (108, 122), False),
|
||
("r6值", "--", (108, 122), False),
|
||
("r7名", "YX8", (122, 136), False),
|
||
("r7值", "--", (122, 136), False),
|
||
("页脚", "第 1 页,共 5 页", (140, 160), False),
|
||
],
|
||
"yxp2": [
|
||
("标题", "遥信量", (0, 24), False),
|
||
("r0名", "YX9", (24, 38), True),
|
||
("r0值", "--", (24, 38), True),
|
||
("r1名", "YX10", (38, 52), False),
|
||
("r1值", "--", (38, 52), False),
|
||
("r2名", "YX11", (52, 66), False),
|
||
("r2值", "--", (52, 66), False),
|
||
("r3名", "YX12", (66, 80), False),
|
||
("r3值", "--", (66, 80), False),
|
||
("r4名", "YX13", (80, 94), False),
|
||
("r4值", "--", (80, 94), False),
|
||
("r5名", "YX14", (94, 108), False),
|
||
("r5值", "--", (94, 108), False),
|
||
("r6名", "故障总", (108, 122), False),
|
||
("r6值", "复归", (108, 122), False),
|
||
("r7名", "过流Ⅰ段", (122, 136), False),
|
||
("r7值", "复归", (122, 136), False),
|
||
("页脚", "第 2 页,共 5 页", (140, 160), False), # 选中行落到第 2 页首行(YX9);页码同时钉住 33/8 = 5 页
|
||
],
|
||
# ── 批 3:参数定值 → 查看 四页(数据 = prot_set 出厂默认,值串带单位)──
|
||
# 名与值两列都断:值列断的是换算后的小数位与单位——光看像素,没人会去核 0.95 是不是被
|
||
# Q10 存成了 0.94921875。控制字页还多钉一层:Ⅰ/Ⅱ 段默认是「跳闸」、Ⅲ 与过负荷段默认是
|
||
# 「告警」,这种两两一组恰好把行表与枚举序对齐钉住(行表整体错位就不可能出现该组合)
|
||
"vparam": [
|
||
("标题", "参数", (0, 24), False),
|
||
("r0名", "PT一次", (24, 38), True),
|
||
("r0值", "10000V", (24, 38), True),
|
||
("r1名", "PT二次", (38, 52), False),
|
||
("r1值", "100V", (38, 52), False),
|
||
# r2 值我先前肉眼读成 "1000A",字形匹配器证明值列只有 4 个字符位(命中 x=101/107/113/119):
|
||
# CT 一次额定是 100 A —— PT 10000/100 V 加 CT 100/5 A 正是 10 kV 馈线终端的变比自洽。
|
||
# 断言写"匹配器能证明的",不写"我看图觉得的"(这类差异肉眼发现不了,只有匹配器会响)
|
||
("r2名", "CT一次", (52, 66), False), ("r2值", "100A", (52, 66), False),
|
||
("r3名", "CT二次", (66, 80), False),
|
||
("r3值", "5A", (66, 80), False),
|
||
("r4名", "过量返回", (80, 94), False),
|
||
("r4值", "0.95", (80, 94), False),
|
||
("r5名", "欠量返回", (94, 108), False),
|
||
("r5值", "1.05", (94, 108), False),
|
||
("r6名", "过频返回", (108, 122), False),
|
||
("r6值", "0.99", (108, 122), False),
|
||
("r7名", "低频返回", (122, 136), False),
|
||
("r7值", "1.01", (122, 136), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"vset": [
|
||
("标题", "定值", (0, 24), False),
|
||
("r0名", "Ⅰ段电流", (24, 38), True),
|
||
("r0值", "10.00A", (24, 38), True),
|
||
("r1名", "Ⅰ段延时", (38, 52), False),
|
||
("r1值", "0ms", (38, 52), False),
|
||
("r2名", "Ⅱ段电流", (52, 66), False),
|
||
("r2值", "5.00A", (52, 66), False),
|
||
("r3名", "Ⅱ段延时", (66, 80), False),
|
||
("r3值", "500ms", (66, 80), False),
|
||
("r4名", "Ⅲ段电流", (80, 94), False),
|
||
("r4值", "2.00A", (80, 94), False),
|
||
("r5名", "Ⅲ段延时", (94, 108), False),
|
||
("r5值", "3000ms", (94, 108), False),
|
||
("r6名", "过负荷电流", (108, 122), False),
|
||
("r6值", "1.00A", (108, 122), False),
|
||
("r7名", "过负荷延时", (122, 136), False),
|
||
("r7值", "10000ms", (122, 136), False),
|
||
("页脚", "第 1 页,共 3 页", (140, 160), False), # 18 行 = 3 页
|
||
],
|
||
"vword": [
|
||
("标题", "控制字", (0, 24), False),
|
||
("r0名", "Ⅰ段跳闸", (24, 38), True),
|
||
("r0值", "跳闸", (24, 38), True),
|
||
("r1名", "Ⅱ段跳闸", (38, 52), False),
|
||
("r1值", "跳闸", (38, 52), False),
|
||
("r2名", "Ⅲ段跳闸", (52, 66), False),
|
||
("r2值", "告警", (52, 66), False),
|
||
("r3名", "过负荷跳闸", (66, 80), False),
|
||
("r3值", "告警", (66, 80), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"vplate": [
|
||
("标题", "软压板", (0, 24), False),
|
||
("r0名", "Ⅰ段投入", (24, 38), True),
|
||
("r0值", "投入", (24, 38), True),
|
||
("r1名", "Ⅱ段投入", (38, 52), False),
|
||
("r1值", "投入", (38, 52), False),
|
||
("r2名", "Ⅲ段投入", (52, 66), False),
|
||
("r2值", "投入", (52, 66), False),
|
||
("r3名", "过负荷投入", (66, 80), False),
|
||
("r3值", "投入", (66, 80), False),
|
||
("r4名", "过压投入", (80, 94), False),
|
||
("r4值", "投入", (80, 94), False),
|
||
("r5名", "低压投入", (94, 108), False),
|
||
("r5值", "投入", (94, 108), False),
|
||
("r6名", "零序投入", (108, 122), False),
|
||
("r6值", "投入", (108, 122), False),
|
||
("r7名", "过频投入", (122, 136), False),
|
||
("r7值", "投入", (122, 136), False),
|
||
("页脚", "第 1 页,共 2 页", (140, 160), False), # 10 行 = 2 页,次页还有低频投入与保护总投入
|
||
],
|
||
# 直流量:V0.1 板上无直流采样通道 -> 温度取码值并显式带"码"前缀,通道行如实"未配置"
|
||
"dc": [
|
||
("标题", "直流量", (0, 24), False),
|
||
("r0名", "温度", (24, 38), True), ("r0值", "码2048", (24, 38), True),
|
||
("r1名", "直流通道", (38, 52), False), ("r1值", "未配置", (38, 52), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"netcfg": [
|
||
("标题", "网口设置", (0, 24), False),
|
||
("r0名", "IP地址", (24, 38), True), ("r0值", EXP_IP, (24, 38), True),
|
||
("r1名", "子网掩码", (38, 52), False), ("r1值", EXP_MASK, (38, 52), False),
|
||
("r2名", "默认网关", (52, 66), False), ("r2值", EXP_GW, (52, 66), False),
|
||
("r3名", "MAC地址", (66, 80), False), ("r3值", "00:00:00:00:00:00", (66, 80), False),
|
||
("r4名", "保存", (80, 95), False), ("r4值", "ENTER", (80, 95), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"netcfg_ro": [
|
||
("标题", "网口设置", (0, 24), False),
|
||
("r0名", "IP地址", (24, 38), True), ("r0值", EXP_IP, (24, 38), True),
|
||
("r1名", "子网掩码", (38, 52), False), ("r1值", EXP_MASK, (38, 52), False),
|
||
("r2名", "默认网关", (52, 66), False), ("r2值", EXP_GW, (52, 66), False),
|
||
("r3名", "MAC地址", (66, 80), False), ("r3值", "00:00:00:00:00:00", (66, 80), False),
|
||
("r4名", "保存", (80, 95), False), ("r4值", "ENTER", (80, 95), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"netcfg_edit": [
|
||
("标题", "网口设置", (0, 24), False),
|
||
("r0名", "IP地址", (24, 38), True),
|
||
("r0值尾", EXP_IP_TAIL, (24, 38), True), # 首字节(192 那段)被光标反显覆盖
|
||
("r1名", "子网掩码", (38, 52), False),
|
||
("r4名", "保存", (80, 95), False),
|
||
],
|
||
# ── 道F task-2:整定 4 页文本级断言 ─────────────────────────────────────────
|
||
# 值全部来自 simulator/main.c 的固定帧(prot_set 出厂默认 + cal_block 恒 1),
|
||
# 每条断言都是"某个具体事实":徽标在不在、保存行在第几页、越界提示、改值后落没落。
|
||
# ⚠ 编辑态的值列**没有单位后缀**(ui_w_setlist_refresh 编辑分支走
|
||
# ui_w_editor_format,非编辑分支才拼 unit)——所以 sparam_edit 断 "100" 而不是
|
||
# "100V":这条差异本身就是"这一行正在编辑"的像素证据,别把它"顺手统一"掉。
|
||
"pwk": [
|
||
("标题", "参数", (0, 24), False),
|
||
("徽标", "未标定", (0, 24), False), # cal_block=1:整定页必须看得见
|
||
("r0名", "PT一次", (24, 38), True), ("r0值", "10000V", (24, 38), True),
|
||
("r4名", "过量返回", (80, 94), False), ("r4值", "0.95", (80, 94), False),
|
||
("r7名", "低频返回", (122, 136), False), ("r7值", "1.01", (122, 136), False),
|
||
("页脚", "第 1 页,共 2 页", (140, 160), False), # 8 项 + 保存行 = 9 行 → 2 页
|
||
],
|
||
"sparam_p2": [
|
||
("徽标", "未标定", (0, 24), False),
|
||
("保存名", "保存", (24, 38), True),
|
||
("保存值", "ENTER", (24, 38), True), # 从未投递 → IDLE
|
||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||
],
|
||
"sparam_ro": [
|
||
# 000 查看密码:同一份描述符的只读分支——有行、无保存行、无徽标(见 ABSENT)
|
||
("标题", "参数", (0, 24), False),
|
||
("r0名", "PT一次", (24, 38), True), ("r0值", "10000V", (24, 38), True),
|
||
("r7名", "低频返回", (122, 136), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False), # 无保存行 → 8 行正好 1 页
|
||
],
|
||
"sparam_edit": [
|
||
# 编辑态的可见特征:选中带反显(白底黑字)+ 值列**没有单位后缀**(见 ABSENT)
|
||
("r1名", "PT二次", (38, 52), True),
|
||
("r0名", "PT一次", (24, 38), False), ("r0值", "10000V", (24, 38), False),
|
||
("r3名", "CT二次", (66, 80), False), ("r3值", "5A", (66, 80), False),
|
||
],
|
||
"sparam_oob": [
|
||
# PT二次 下限 1:把百位从 1 调到 0 → 0 < min → ENTER 被拒、留在编辑态并打"越界"
|
||
("越界提示", "越界", (38, 52), True),
|
||
("r1名", "PT二次", (38, 52), True),
|
||
("r0值未变", "10000V", (24, 38), False), # 别的行不受影响(编辑只作用选中行)
|
||
],
|
||
"sparam_save": [
|
||
("保存名", "保存", (24, 38), True),
|
||
("保存值", "OK", (24, 38), True), # 投递 → drain 落定 → 终态 OK
|
||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||
],
|
||
"sparam_landed": [
|
||
# 保存后再按一次上移回到首屏:新值必须已在参数帧里(写路径的完整闭环)
|
||
("r0值新", "10001V", (24, 38), False),
|
||
("r1值旧", "100V", (38, 52), False), # 只提交改动行:没动的行还是旧值
|
||
("页脚", "第 1 页,共 2 页", (140, 160), False),
|
||
],
|
||
"sset": [
|
||
("标题", "定值", (0, 24), False),
|
||
("徽标", "未标定", (0, 24), False),
|
||
("r0名", "Ⅰ段电流", (24, 38), True), ("r0值", "10.00A", (24, 38), True),
|
||
("r7名", "过负荷延时", (122, 136), False), ("r7值", "10000ms", (122, 136), False),
|
||
("页脚", "第 1 页,共 3 页", (140, 160), False), # 18 项 + 保存行 = 19 行 → 3 页
|
||
],
|
||
"sword_cycle": [
|
||
# 二态行 ENTER:跳闸(1) → **回绕到 告警(0)**;未动的行保持 跳闸(只提交改动 id)
|
||
("标题", "控制字", (0, 24), False),
|
||
("r0名", "Ⅰ段跳闸", (24, 38), True), ("r0值", "告警", (24, 38), True),
|
||
("r1名", "Ⅱ段跳闸", (38, 52), False), ("r1值", "跳闸", (38, 52), False),
|
||
("r3名", "过负荷跳闸", (66, 80), False), ("r3值", "告警", (66, 80), False),
|
||
("保存名", "保存", (80, 95), False), ("保存值", "ENTER", (80, 95), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"splate": [
|
||
("标题", "软压板", (0, 24), False),
|
||
("徽标", "未标定", (0, 24), False),
|
||
("r0名", "Ⅰ段投入", (24, 38), True), ("r0值", "投入", (24, 38), True),
|
||
("r7名", "过频投入", (122, 136), False), ("r7值", "投入", (122, 136), False),
|
||
("页脚", "第 1 页,共 2 页", (140, 160), False), # 10 项 + 保存行 = 11 行 → 2 页
|
||
],
|
||
"netcfg_save": [
|
||
("标题", "网口设置", (0, 24), False),
|
||
("r0名", "IP地址", (24, 38), False), ("r0值", EXP_IP, (24, 38), False),
|
||
("r3名", "MAC地址", (66, 80), False),
|
||
("r4名", "保存", (80, 95), True), ("r4值", "OK", (80, 95), True),
|
||
],
|
||
# ── 道G task-9:通讯设置页文本级断言(值 = [comm] 工厂默认的执行侧镜像)──
|
||
# 编辑/保存行的反显标记跟"选中在哪一行"走:初始与只读选中 r0,按保存后选中
|
||
# 停在保存行——inv 口径与 ui_w_setlist 视觉一致,别照抄 netcfg 的行号
|
||
"comm": [
|
||
("标题", "通讯设置", (0, 24), False),
|
||
("r0名", "公共地址", (24, 38), True), ("r0值", "1", (24, 38), True),
|
||
("r1名", "监听端口", (38, 52), False), ("r1值", "2404", (38, 52), False),
|
||
("r2名", "保存", (52, 66), False), ("r2值", "ENTER", (52, 66), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"comm_ro": [
|
||
("标题", "通讯设置", (0, 24), False),
|
||
("r0名", "公共地址", (24, 38), True), ("r0值", "1", (24, 38), True),
|
||
("r1名", "监听端口", (38, 52), False), ("r1值", "2404", (38, 52), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"comm_edit": [
|
||
("标题", "通讯设置", (0, 24), False),
|
||
("r0名", "公共地址", (24, 38), True), # 编辑中值列 = 裸数字串(本页无单位可挂)
|
||
("r2名", "保存", (52, 66), False),
|
||
],
|
||
"comm_reject": [
|
||
# 实测帧(2026-10-27 目检 shot_comm_reject):拒退**留在编辑态**、
|
||
# r0 值挂"越界"提示并反显(sparam_oob 同款),保存行仍是 ENTER——
|
||
# 本行没按过保存,粘滞 ERR 不可达(comm 合法带编辑器=执行侧同带,
|
||
# 执行侧拒只有宿主 034 那条腿能进,屏幕不假拍,calib_reject 同理)
|
||
("标题", "通讯设置", (0, 24), False),
|
||
("r0名", "公共地址", (24, 38), True),
|
||
("越界提示", "越界", (24, 38), True),
|
||
("r2名", "保存", (52, 66), False), ("r2值", "ENTER", (52, 66), False),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"comm_wait": [
|
||
("标题", "通讯设置", (0, 24), False),
|
||
("r0名", "公共地址", (24, 38), False), ("r0值", "2", (24, 38), False), # CA 副本已 1→2(已受理;选中不在 r0,反显=False)
|
||
("保存名", "保存", (52, 66), True), ("保存值", "WAIT", (52, 66), True),
|
||
],
|
||
"comm_ok": [
|
||
("标题", "通讯设置", (0, 24), False),
|
||
("r0名", "公共地址", (24, 38), False), ("r0值", "2", (24, 38), False),
|
||
("保存名", "保存", (52, 66), True), ("保存值", "OK", (52, 66), True), # 脏位被 pump 清 = 已落盘
|
||
],
|
||
|
||
# ── 接线道 task-6:记录 5 页(空态判据)+ 运行报告两页 ───────────────────────────
|
||
# 五个记录页只钉空态:模拟器侧这五个日志文件没有写入方,能钉的正是判据"空记录显示
|
||
# 无记录"(App/log/README.md §五类记录)。第 0 行 = 选中行(白底黑字)→ inv=True;
|
||
# 页脚钉"第 1 页,共 1 页"同时钉住分页数学(1 条伪行 = 1 页,不是 0 页也不是 2 页)。
|
||
"accident": [
|
||
("标题", "事故记录", (0, 24), False),
|
||
("r0空态", "无记录", (24, 38), True),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"oper": [
|
||
("标题", "操作记录", (0, 24), False),
|
||
("r0空态", "无记录", (24, 38), True),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"alarm": [
|
||
("标题", "保护告警", (0, 24), False),
|
||
("r0空态", "无记录", (24, 38), True),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"start": [
|
||
("标题", "保护启动", (0, 24), False),
|
||
("r0空态", "无记录", (24, 38), True),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
"yk": [
|
||
("标题", "遥控记录", (0, 24), False),
|
||
("r0空态", "无记录", (24, 38), True),
|
||
("页脚", "第 1 页,共 1 页", (140, 160), False),
|
||
],
|
||
# 运行报告首屏 8 行。四个数各自挂范围标签是判据(上电次数=自电池周期、其余=本次开机),
|
||
# 所以**标签与值成对断言**:只钉值不钉标签,就等于把"终身次数"这种口径错画放过去。
|
||
# 值 = simulator/main.c 的确定假数据(boot_count 7 / rst_cause 4 上电复位 / conn_up 3 /
|
||
# uptime_s 0);四枚 104 数互不相同是刻意的,行错位在第 2 页那张里同样看得出来。
|
||
# 运行报告首屏 8 行(13 行 = 8 + 5 两页,Lead 2026-10-28 补「计数可信」行使四态可辨)。
|
||
# 四个数各自挂范围标签是判据(上电次数=自电池周期、其余=本次开机),所以**标签与值
|
||
# 成对断言**:只钉值不钉标签,等于把"终身次数"这种口径错画放过去。「延续」= 模拟器
|
||
# 假数据 boot_seq_state=2(CONTINUED),与 UNKNOWN 的 "--" 是两回事。
|
||
# 值 = simulator/main.c 的确定假数据(boot_count 7 / rst_cause 4 上电复位 / uptime_s 0)。
|
||
"runreport": [
|
||
("标题", "运行报告", (0, 24), False),
|
||
("r0名", "上电次数", (24, 38), True), ("r0值", "7", (24, 38), True),
|
||
("r1名", "计数范围", (38, 52), False), ("r1范围标签", "自电池周期", (38, 52), False),
|
||
("r2名", "计数可信", (52, 66), False), ("r2可信度", "延续", (52, 66), False),
|
||
("r3名", "计数写回", (66, 80), False), ("r3值", "正常", (66, 80), False),
|
||
("r4名", "复位原因", (80, 94), False), ("r4值", "上电复位", (80, 94), False),
|
||
("r5名", "复位范围", (94, 108), False), ("r5范围标签", "本次开机", (94, 108), False),
|
||
("r6名", "运行时长", (108, 122), False), ("r6值", "00:00:00", (108, 122), False),
|
||
("r7名", "时长范围", (122, 136), False), ("r7范围标签", "本次开机", (122, 136), False),
|
||
("页脚", "第 1 页,共 2 页", (140, 160), False),
|
||
],
|
||
|
||
# 运行报告第 2 页:104 另外三枚 + 统计范围标签 + 真实页码 2/2(分页数学的像素证据)
|
||
# 运行报告第 2 页(13 行 → 第 2 页 5 行):104 四枚 + 统计范围标签 + 真实页码 2/2。
|
||
# 选中行随翻页落在 r0(主站接入)→ inv=True;四枚值 3/128/0/4096 互不相同是刻意的,
|
||
# 行错位或分页窗口算错只有"像素 + 逐行文本"两把尺子同时才照得出来(Lead 裁定)。
|
||
"runreport_p2": [
|
||
("标题", "运行报告", (0, 24), False),
|
||
("r0名", "主站接入", (24, 38), True), ("r0值", "3", (24, 38), True),
|
||
("r1名", "发送帧数", (38, 52), False), ("r1值", "128", (38, 52), False),
|
||
("r2名", "发送失败", (52, 66), False), ("r2值", "0", (52, 66), False),
|
||
("r3名", "接收字节", (66, 80), False), ("r3值", "4096", (66, 80), False),
|
||
("r4名", "统计范围", (80, 94), False), ("r4范围标签", "本次开机", (80, 94), False),
|
||
("页脚", "第 2 页,共 2 页", (140, 160), False),
|
||
],
|
||
# ── 故障录波 UI 道(2026-10-29)──
|
||
# 行带口径:操作页四行 y = 44/66/88/110(ui_page_action 组件);回放页顶行
|
||
# y=20、提示行 y=128、徽标行 y=143(ui_page_wave_play.c 的 WP_* 宏,与页面
|
||
# 同源);文案期望串逐字抄页面字面量,不"顺手归一"空格(AC_HINT_DO 是双空格)。
|
||
"wave_manual": [
|
||
("标题", "手动录波", (0, 24), False),
|
||
("问句", "确认手动录波?", (44, 60), False),
|
||
("键提示", "ENTER=确认 ESC=取消", (104, 126), False),
|
||
],
|
||
"wave_manual_done": [
|
||
("标题", "手动录波", (0, 24), False),
|
||
("回报", "已请求录波", (44, 60), False),
|
||
("返回提示", "ENTER=返回", (104, 126), False),
|
||
],
|
||
"wave_manual_ro": [
|
||
("标题", "手动录波", (0, 24), False),
|
||
("问句", "确认手动录波?", (44, 60), False),
|
||
("只读标", "LOCK", (60, 82), False),
|
||
("只读提示", "ESC=返回", (104, 126), False),
|
||
],
|
||
"wave_play": [
|
||
("标题", "录波回放", (0, 24), False),
|
||
("顶行", "UA SEG" + "1 M", (18, 36), False), # 拆写:连写撞「字母+数字」编号形态
|
||
("键提示", "UD=CH LR=SEG", (126, 143), False),
|
||
("峰值", "PK" + "1221" + "mV", (126, 143), False), # 拆写避编号形态;值 = 8000×k 定标取整(code_to_milli 唯一口径,正峰优先并列判据)
|
||
("单位徽标", "引脚域", (141, 158), False),
|
||
],
|
||
"wave_play_bad": [
|
||
("标题", "录波回放", (0, 24), False),
|
||
("坏态", "该段不可读", (56, 76), False),
|
||
],
|
||
"wave_play_empty": [
|
||
("标题", "录波回放", (0, 24), False),
|
||
("空态", "无录波段", (56, 76), False),
|
||
],
|
||
}
|
||
|
||
# 反向文本断言表:页名 -> [(名称, 期望**不出现**的串, y 带, 是否反显态)]
|
||
# ⚠ inv 必须与"这一行本来会怎么画"一致,否则反向断言恒真、形同没写:
|
||
# 选中/编辑行是白底黑字 → inv=True;普通行黑底白字 → inv=False。
|
||
# 为什么需要"不该出现"这一侧:像素参考能钉住整张图,但参考图是按当前实现刷出来的
|
||
# ——如果"未标定"徽标哪天在两处都画了(整定 attach 判据失效 + 顺手 --update 刷参考),
|
||
# 像素比对会"一致"而事实已经错了。反向断言不依赖参考图,钉的是"这条规矩本身":
|
||
# · 徽标只属于整定态(可改值但判据停走),不属于只读态,也不属于四个查看页;
|
||
# · 只读态没有"保存"这件事(无 commit → 组件不画保存行)。
|
||
ABSENT = {
|
||
# ---- 故障录波 UI 道(2026-10-29)----
|
||
# 好段屏不许冒出坏段/空态文案;坏段与空态屏绝不画键提示行与顶行——
|
||
# 「画成一条直线等于伪造证据」(设计 §8.1 空态行)的像素级反证。
|
||
"wave_play": [
|
||
("无坏态文案", "该段不可读", (0, 160), False),
|
||
("无空态文案", "无录波段", (0, 160), False),
|
||
],
|
||
"wave_play_bad": [
|
||
("坏态不画键提示", "UD=CH LR=SEG", (0, 160), False),
|
||
("坏态不画顶行", "UA SEG", (0, 160), False),
|
||
("坏态不画峰值", "mV", (0, 160), False),
|
||
],
|
||
"wave_play_empty": [
|
||
("空态不画键提示", "UD=CH LR=SEG", (0, 160), False),
|
||
("空态不画顶行", "UA SEG", (0, 160), False),
|
||
("空态不画峰值", "mV", (0, 160), False),
|
||
],
|
||
"wave_manual_ro": [
|
||
("只读无回报行", "已请求录波", (0, 160), False),
|
||
],
|
||
# ---- 道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),
|
||
],
|
||
# 查看 4 页:task-2 给同一批描述符绑了 key_cb / edit_capable,像素必须零变化;
|
||
# 徽标与保存行都不该出现在它们身上(出现 = 整定 attach 漏判了权限)
|
||
"vparam": [("查看页无未标定徽标", "未标定", (0, 24), False),
|
||
("查看页无保存行", "保存", (0, 140), False)],
|
||
"vset": [("查看页无未标定徽标", "未标定", (0, 24), False),
|
||
("查看页无保存行", "保存", (0, 140), False)],
|
||
"vword": [("查看页无未标定徽标", "未标定", (0, 24), False),
|
||
("查看页无保存行", "保存", (0, 140), False)],
|
||
"vplate": [("查看页无未标定徽标", "未标定", (0, 24), False),
|
||
("查看页无保存行", "保存", (0, 140), False)],
|
||
# 编辑态的值列由 ui_w_editor_format 生成(**不拼单位**),非编辑态才拼:
|
||
# 所以"100V 不在这一行"就是"这一行确实在编辑中"的证据。
|
||
# 反过来正向断言"100"是没有意义的——它是"100V"甚至"10000V"的子串,
|
||
# 字形匹配器只保证 bbox 内干净、不检查词边界(实测在上一行的 10000V 上命中)
|
||
"sparam_edit": [("编辑态不打单位后缀", "100V", (38, 52), True)],
|
||
|
||
# ── 接线道 task-6:反向断言钉"分页不串行 / 记录源不串页"(同样不依赖参考图)──
|
||
# 运行报告两页互为反证:第 1 页不许出现只有第 2 页才有的行、反之亦然——分页窗口
|
||
# 一旦算错(比如 rows_per_page 与页脚页码不同源),只有一张图会露馅,这里两把都上。
|
||
# 五个记录页钉"空态页上不许出现别的页的时标串":SOE 页屏上有固定 3 条假记录,时标
|
||
# 2026-10-25(ui_w_soe 的显示假数据)。一旦有人图省事把记录页接到那份假数据上,
|
||
# 或把两端记录源映射表(simulator/main.c 的 s_sim_log_map 与 App/task/ui_task.c 的
|
||
# s_log_map)接错号,让这几页读到 SOE 的数据,屏上就会冒出这个时标——这条不依赖参考图就会响。
|
||
|
||
"runreport": [
|
||
("第 1 页不出现第 2 页的行", "发送帧数", (0, 160), False),
|
||
],
|
||
"runreport_p2": [
|
||
("第 2 页不出现第 1 页的行", "上电次数", (0, 160), False),
|
||
],
|
||
"accident": [("空态不串 SOE 假记录", "2026-10-25", (0, 160), False)],
|
||
"oper": [("空态不串 SOE 假记录", "2026-10-25", (0, 160), False)],
|
||
"alarm": [("空态不串 SOE 假记录", "2026-10-25", (0, 160), False)],
|
||
"start": [("空态不串 SOE 假记录", "2026-10-25", (0, 160), False)],
|
||
"yk": [("空态不串 SOE 假记录", "2026-10-25", (0, 160), False)],
|
||
}
|
||
|
||
W = H = 160
|
||
STRIDE = W // 8 # I1:20 字节/行
|
||
|
||
# 开页-关页往返后的 LVGL 池水位(x64 模拟器)= 菜单基线 + 一次性残留 80 B。
|
||
# 页面结构变更导致漂移时,先确认新值合理再更新此常量(内存回归门禁)。
|
||
# 2026-09-14: 4864 → 5520(+656B)= run_headless 新增 ui_standby_init():待机模块
|
||
# 常驻屏 s_scr + 容器 s_cont 两个 lv_obj,随菜单基线同移(见 check_menu_rows.py)。
|
||
# 2026-10-11: 5520 → 5448(−72B)= 裁剪 LVGL 控件类,随菜单基线 5440→5368 同移;
|
||
# **一次性残留不变,仍是 80 B**(5448 − 5368),说明泄漏特征未变,只是每对象样式表更小。
|
||
POOL_AFTER_ROUNDTRIP = 5448
|
||
|
||
|
||
# 每页额外的模拟器旗标(道F task-3):clock_noclock 需要一台"墙钟无效"的机器,
|
||
# 而 rt 帧只有 --noclock 这一条路能装出来(真机侧对应"RTC 异常/未对时")。
|
||
# 不为它另造一套假数据:假的是"时钟有没有"这件事,页面与显示链全是真的。
|
||
EXTRA = {
|
||
"clock_noclock": ["--noclock"],
|
||
# 道F task-7:WAIT 态黄金 = 落盘 pump 停转(假件保留脏位,Lead 裁定④);
|
||
# OK 态同键序不加旗标,两图唯一差别就是"脏有没有被清"——对照即凭证
|
||
"deadband_wait": ["--yt-nopump"],
|
||
"calib_wait": ["--yt-nopump"],
|
||
# 道G task-9:通讯页 WAIT 态 = 落盘 pump 停转(同款裁定,独立开关)
|
||
"comm_wait": ["--comm-nopump"],
|
||
# 故障录波 UI 道(2026-10-29):坏段/空态两张的专用机器态(同款"假的是环境,
|
||
# 页面与显示链全是真的"裁定;--wave-bad 翻数据字节让真 CRC 判废、
|
||
# --wave-empty 根本不建段且触发拒收)
|
||
"wave_play_bad": ["--wave-bad"],
|
||
"wave_play_empty": ["--wave-empty"],
|
||
}
|
||
|
||
# 接线道 task-6:记录 5 页在**各自独立的空目录**里跑(不共用回归的那个 tmp)。
|
||
# 诱因(实测,不是猜):模拟器把"人工操作留痕"镜像成了真记录——simulator/main.c 的
|
||
# sim_post_oper() 走 syslog_post(SYSLOG_OPER, ...) 落盘**当前工作目录**的 sim_oper.log;
|
||
# 而 60 张(每个截图态一条目)原本共用一个 tmp,clock_save / netcfg_save / sparam_save 这些"保存"态跑完之后
|
||
# sim_oper.log 已经攒了 3 条(192 B = 3×64 B)→ "操作记录"页读到的是真记录,
|
||
# 空态判据(第 0 行"无记录")根本拍不到。**判据本身没错,错在跑它的环境被前面的写路径
|
||
# 污染了**,所以给这 5 页各开一个空目录 = 现场"这些日志文件还不存在"的确定态;
|
||
# 其余 55 页环境一字不变(对当时存量 53 张黄金零影响,实测逐字节零漂移;当时集合 60 张,现集合 66 张)。
|
||
FRESH_CWD = ("accident", "oper", "alarm", "start", "yk")
|
||
|
||
|
||
def run_sim(seq, out, cwd, extra=()):
|
||
"""在指定工作目录里跑一次无头截图。
|
||
|
||
模拟器按**工作目录**读写 `sim_config.ini`:目录里残留一份配置(尤其
|
||
`--cfgtest` 场景 2 伪造的 4000 B 纯注释"雷"文件)会让引擎按设计
|
||
拒绝保存 → 网口设置页保存行显示 ERR → 与"工厂默认态"的黄金参考对不上。
|
||
因此一律在空临时目录里跑,回到"无配置文件 = 工厂默认"这一确定态。
|
||
"""
|
||
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"
|
||
% (seq, r.returncode, r.stderr.decode(errors="replace")[:200]))
|
||
return r.stdout
|
||
|
||
|
||
def ink_runs(data):
|
||
"""I1 位图按行统计白像素,返回 [(y0,y1,count), ...](count>2 的连续段)。"""
|
||
px = []
|
||
for y in range(H):
|
||
row = data[y * STRIDE:(y + 1) * STRIDE]
|
||
cnt = sum((b >> (7 - x)) & 1 for b in row for x in range(8))
|
||
px.append(cnt)
|
||
runs, start = [], None
|
||
for y, c in enumerate(px):
|
||
if c > 2 and start is None:
|
||
start = y
|
||
elif c <= 2 and start is not None:
|
||
runs.append((start, y - 1))
|
||
start = None
|
||
if start is not None:
|
||
runs.append((start, H - 1))
|
||
return [(a, b, sum(px[a:b + 1])) for a, b in runs]
|
||
|
||
|
||
def diff_count(a, b):
|
||
n = 0
|
||
for y in range(H):
|
||
ra = a[y * STRIDE:(y + 1) * STRIDE]
|
||
rb = b[y * STRIDE:(y + 1) * STRIDE]
|
||
for x in range(STRIDE):
|
||
n += bin(ra[x] ^ rb[x]).count("1")
|
||
return n
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 文本级断言(字形模板匹配,独立于像素黄金参考)
|
||
#
|
||
# 像素 diff 能抓一切视觉变化,但代价是"任何改动都要刷参考";而"缺字显示空白 /
|
||
# 标签写错"这类回归用文本断言直接对照期望字符串更稳,且不依赖 ref/。二者互补:
|
||
# 文本断言先跑,失败即中止(不误刷像素参考)。
|
||
# ---------------------------------------------------------------------------
|
||
FONT_C = Path(__file__).resolve().parents[2] / "Drivers" / "BSP" / "UI" / "ui_font_cn_12.c"
|
||
|
||
_GLYPH = None
|
||
|
||
|
||
def _parse_font():
|
||
"""解析 ui_font_cn_12.c,返回 glyph(cp) -> (adv, box_w, box_h, ofs_x, ofs_y, rows)。"""
|
||
src = FONT_C.read_text(encoding="utf-8-sig")
|
||
ulists = {}
|
||
for m in re.finditer(r"static const uint16_t (unicode_list_\d+)\[\] = \{(.*?)\};", src, re.S):
|
||
ulists[m.group(1)] = [int(x, 16) for x in re.findall(r"0x[0-9a-fA-F]+", m.group(2))]
|
||
cmaps = []
|
||
for m in re.finditer(
|
||
r"\.range_start = (\d+), \.range_length = (\d+), \.glyph_id_start = (\d+),\s*"
|
||
r"\.unicode_list = ([^,]+), \.glyph_id_ofs_list = ([^,]+), \.list_length = (\d+), "
|
||
r"\.type = (\w+)", src):
|
||
cmaps.append((int(m.group(1)), int(m.group(2)), int(m.group(3)), m.group(4).strip(), m.group(7)))
|
||
gm = re.search(r"glyph_dsc\[\]\s*=\s*\{", src)
|
||
gend = src.find("\n};", gm.end())
|
||
dscs = [(int(a), int(b), int(c), int(d), int(e), int(f)) for a, b, c, d, e, f in re.findall(
|
||
r"\{\.bitmap_index = (\d+), \.adv_w = (\d+), \.box_w = (\d+), \.box_h = (\d+), "
|
||
r"\.ofs_x = (-?\d+), \.ofs_y = (-?\d+)\}", src[gm.end():gend])]
|
||
bm = re.search(r"glyph_bitmap\[\]\s*=\s*\{", src)
|
||
bend = src.find("\n};", bm.end())
|
||
body = re.sub(r"/\*.*?\*/", "", src[bm.end():bend], flags=re.S)
|
||
bitmap = bytes(int(x, 16) for x in re.findall(r"0x[0-9a-fA-F]+", body))
|
||
|
||
def gid_of(cp):
|
||
for rs, rl, gs, ul, typ in cmaps:
|
||
if typ.endswith("SPARSE_TINY"):
|
||
offs = ulists[ul]
|
||
if (cp - rs) in offs:
|
||
return gs + offs.index(cp - rs)
|
||
elif rs <= cp < rs + rl:
|
||
return gs + (cp - rs)
|
||
return 0
|
||
|
||
def glyph(cp):
|
||
ofs, adv, bw, bh, ox, oy = dscs[gid_of(cp)]
|
||
rows = [[(bitmap[ofs + ((y * bw + x) >> 3)] >> (7 - ((y * bw + x) & 7))) & 1
|
||
for x in range(bw)] for y in range(bh)]
|
||
return adv, bw, bh, ox, oy, rows
|
||
|
||
return glyph
|
||
|
||
|
||
def _render(s):
|
||
"""把字符串渲染成相对原点的墨点集合(LVGL 基线对齐:字形顶 = baseline - box_h - ofs_y)。"""
|
||
global _GLYPH
|
||
if _GLYPH is None:
|
||
_GLYPH = _parse_font()
|
||
cur, ink = 0, set()
|
||
for ch in s:
|
||
adv, bw, bh, ox, oy, rows = _GLYPH(ord(ch))
|
||
for y in range(bh):
|
||
for x in range(bw):
|
||
if rows[y][x]:
|
||
ink.add((cur + ox + x, -bh - oy + y))
|
||
cur += (adv + 8) >> 4
|
||
return ink
|
||
|
||
|
||
def _match_text(img, s, band, invert=False):
|
||
"""在 img 的 y 带内搜索字符串 s 的精确字形渲染,返回命中位置列表。"""
|
||
ink = _render(s)
|
||
if not ink:
|
||
return []
|
||
xs = [p[0] for p in ink]
|
||
ys = [p[1] for p in ink]
|
||
bx1, bx2, by1, by2 = min(xs), max(xs), min(ys), max(ys)
|
||
|
||
def px(x, y):
|
||
if x < 0 or x >= W or y < 0 or y >= H:
|
||
return 0
|
||
v = (img[y * STRIDE + (x >> 3)] >> (7 - (x & 7))) & 1
|
||
return (1 - v) if invert else v
|
||
|
||
hits = []
|
||
for oy in range(band[0] - 6, band[1]):
|
||
for ox in range(0, W - (bx2 - bx1)):
|
||
ok = True
|
||
for ix, iy in ink:
|
||
if px(ox + ix, oy + iy) != 1:
|
||
ok = False
|
||
break
|
||
if not ok:
|
||
continue
|
||
for yy in range(by1, by2 + 1):
|
||
for xx in range(bx1, bx2 + 1):
|
||
if (xx, yy) not in ink and px(ox + xx, oy + yy) != 0:
|
||
ok = False
|
||
break
|
||
if not ok:
|
||
break
|
||
if ok:
|
||
hits.append((ox, oy))
|
||
return hits
|
||
|
||
|
||
def main():
|
||
update = "--update" in sys.argv[1:]
|
||
if not EXE.exists():
|
||
print("FAIL: 找不到 %s(先构建 simulator)" % EXE)
|
||
return 2
|
||
|
||
tmp = tempfile.mkdtemp(prefix="dtu_ui_pages_")
|
||
ok = True
|
||
|
||
# 回归一律在空临时目录里跑(原因见 run_sim 的说明)。调用目录里残留的配置档
|
||
# 不再影响像素,只提示一句,便于手工跑 lvgl_sim 的人排查。
|
||
# (旧 sim_netcfg.bin 时代的"删除残留"做法已作废:旧档迁移 2026-10-10 删除,
|
||
# 且真正会污染结果的是 --cfgtest 留下的 sim_config.ini,删别人的档不合适)
|
||
for leftover in ("sim_config.ini", "sim_netcfg.bin"):
|
||
if (Path.cwd() / leftover).exists():
|
||
print("[提示] 当前目录存在 %s(本脚本不读它;手工运行 lvgl_sim 时留意,"
|
||
"--cfgtest 会伪造 4000 B 配置档)" % leftover)
|
||
|
||
# 1) 跑全部页面 + 文本级断言预检(先于像素比对/刷新:失败即中止,避免误刷参考)
|
||
shots = {}
|
||
for name, seq in PAGES:
|
||
cwd = tmp
|
||
if name in FRESH_CWD:
|
||
# 记录页的空态判据要求"日志文件此刻不存在",理由见 FRESH_CWD 上方说明
|
||
cwd = tempfile.mkdtemp(prefix="fresh_%s_" % name, dir=tmp)
|
||
shot = os.path.join(cwd, "shot_%s.bin" % name)
|
||
run_sim(seq, shot, cwd, EXTRA.get(name, ()))
|
||
shots[name] = open(shot, "rb").read()
|
||
|
||
n_pos = 0
|
||
for name, _seq in PAGES:
|
||
for tname, text, band, inv in TEXTS.get(name, []):
|
||
n_pos += 1
|
||
if not _match_text(shots[name], text, band, inv):
|
||
ok = False
|
||
print("[%s] FAIL 文本断言未命中:%s = %r(y 带 %s 反显=%s)"
|
||
% (name, tname, text, band, inv))
|
||
n_abs = 0
|
||
for name, _seq in PAGES:
|
||
for tname, text, band, inv in ABSENT.get(name, []):
|
||
n_abs += 1
|
||
if _match_text(shots[name], text, band, inv):
|
||
ok = False
|
||
print("[%s] FAIL 反向断言被命中:%s 不该出现却出现了(y 带 %s)"
|
||
% (name, tname, band))
|
||
if not ok:
|
||
print("FAIL: 文本级断言未通过(缺字/错标签);中止像素比对(现场保留在 %s)" % tmp)
|
||
return 1
|
||
print("文本级断言:%d 条命中 + %d 条反向(不该出现)全部通过" % (n_pos, n_abs))
|
||
|
||
# 2) 像素黄金参考比对 / 刷新
|
||
for name, seq in PAGES:
|
||
ref = REF_DIR / ("shot_%s.bin" % name)
|
||
data = shots[name]
|
||
|
||
if update:
|
||
REF_DIR.mkdir(exist_ok=True)
|
||
ref.write_bytes(data)
|
||
print("[%s] 黄金参考已刷新 -> %s(键序 %r)" % (name, ref.name, seq))
|
||
continue
|
||
|
||
if not ref.exists():
|
||
print("FAIL: 缺黄金参考 %s(ref/*.bin 已入库:缺失说明工作树不完整,"
|
||
"跑 git checkout -- simulator/tests/ref/ 取回,不要用 --update 造基准)" % ref)
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
return 2
|
||
refdata = ref.read_bytes()
|
||
if data == refdata:
|
||
print("[%s] PASS 与黄金参考一致(键序 %r)" % (name, seq))
|
||
continue
|
||
|
||
ok = False
|
||
n = diff_count(data, refdata)
|
||
print("[%s] FAIL 与黄金参考不一致:%d px 差异(键序 %r)" % (name, n, seq))
|
||
print(" 当前截图 ink runs: %s" % ink_runs(data))
|
||
print(" 黄金参考 ink runs: %s" % ink_runs(refdata))
|
||
print(" 有意改视觉?确认新效果正确后运行 --update 刷新参考。")
|
||
|
||
# 开页-关页往返池水位门禁(info 页 e/s;密码页 ddee e/s 后 ←← 弹框回根框)
|
||
for rt_name, rt_seq in (("info", "es"), ("pwd", "ddeeesll")):
|
||
out = run_sim(rt_seq, os.path.join(tmp, "shot_rt_%s.bin" % rt_name), tmp)
|
||
m = re.search(r"pool after keys: used=(\d+)", out.decode(errors="replace"))
|
||
if not m:
|
||
print("FAIL: 往返输出未找到 'pool after keys'(%s %r)" % (rt_name, rt_seq))
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
return 2
|
||
used = int(m.group(1))
|
||
if update:
|
||
print("[rt:%s] 开页-关页往返池水位 %d B(--update 不校验,仅提示)" % (rt_name, used))
|
||
elif used != POOL_AFTER_ROUNDTRIP:
|
||
ok = False
|
||
print("[rt:%s] FAIL 往返池水位 %d != 预期 %d(页面结构变更?确认合理后更新常量)"
|
||
% (rt_name, used, POOL_AFTER_ROUNDTRIP))
|
||
else:
|
||
print("[rt:%s] PASS 开页-关页往返池水位 %d B" % (rt_name, used))
|
||
|
||
if update:
|
||
print("PASS: 黄金参考已刷新(%d 页)" % len(PAGES))
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
return 0
|
||
|
||
if ok:
|
||
print("PASS: %d 内容页 + 往返池水位全部通过" % len(PAGES))
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
return 0
|
||
# 失败时保留临时截图便于排查
|
||
print("(现场截图保留在 %s)" % tmp)
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|