301c3e5183
新增门禁本体 - scripts/check_ids.py:实现 docs/编号规范.md §8 八项校验;默认 warn-only, 唯一硬拦项是「可判定比例跌破基线」(防回退的本体) - scripts/id_registry.json:66 个前缀族,逐项来自 §4 总表 + 全库实测游标, 偏差一律写进 cursor_note,不凭记忆编造 - scripts/id_aliases.json:旧号 → 新号 / 作废标记(照搬 section_aliases.json 范式) - 顺带收编 check_test_traceability.py:正则补齐大写后缀与无模块码形态, 比对范围扩到 test/host 与 7 篇「单元测试说明.md」,另加 --board-only 回退旧口径 批 1 改名(用户授权解禁「不改写存量号」) - 代码规范条款 S1~S19 → CODE-RULE-nn:78 处 / 2 文件 - UI 像素回归状态 S0~S7 → MENU-STATE-nn:61 处 / 3 文件(冻结在 07) - 两族属主登记处 = 脚本自身的规则表 / STATES 表,文档只作引用方 - 新增 scripts/rename_ids.py(§7.3 规划脚本):映射表驱动、显式 scope、默认干跑、 自动写 scoped 别名,跳过 [示例]/[计划] 行与变更记录表,按原字节读写保 BOM 行尾 - 已应用映射表存档 scripts/rename_maps/batch1.json 作审计轨迹 有牙实测(证明门禁不是空转) - 注入 Z-99 / 裸形 / 悬空 / 越游标 / 未紧邻 / 跨文档重复定义 六例, 分别被校验 1/2/3/5/6/4 点名,--strict 退出码 1;临时档删除后工作树干净 验证与同步 - 可判定比例基线 82.3% → 82.7%(已 --update-baseline 入档);错误级 1312 → 1291 - ci_local.py 全绿(L0 + L0.5 共 1104 断言);ui-golden Passed; check_doc_links.py --all 无法判定 0 - 未改任何 C/H 源文件,故本次不需 @build - 文档:AGENTS.md 与 pre-commit使用说明.md 计数 36→37 并新增 §2.25 详解; 编号规范 §4.1 补两行、§8.1 落地记录、§8.2 规范与仓库偏差、§8.3 迁移批次记录
236 lines
9.7 KiB
Python
236 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
check_menu_rows.py — 自绘菜单 8 状态像素回归(160x160 I1,MSB-first,1=白 0=黑)。
|
||
|
||
用法:
|
||
python simulator/tests/check_menu_rows.py # 在仓库任意目录均可运行
|
||
前置:simulator 已构建(build/Debug/lvgl_sim.exe 存在);纯标准库,任意 python3。
|
||
|
||
状态(键序注入,u=上 d=下 e=Enter s=Esc):
|
||
MENU-STATE-00 "" 根框 7 行 sel=0
|
||
MENU-STATE-01 "d" sel=1
|
||
MENU-STATE-02 "dd" sel=2
|
||
MENU-STATE-03 "ddu" 回退 → 须与 MENU-STATE-01 全像素一致
|
||
MENU-STATE-04 "de" 开 L2(实时数据 3 子项),根框不变
|
||
MENU-STATE-05 "ded" L2 sel=1
|
||
MENU-STATE-06 "des" 返回 → 须与 MENU-STATE-01 全像素一致(销毁/重建一致性)
|
||
MENU-STATE-07 "ddes" 根框 sel=2、L2 已毁 → 须与 MENU-STATE-02 全像素一致
|
||
断言:选中行白带(白占比>0.50)+ 带内黑字;非选行黑底白字(0.03<占比<0.45);
|
||
框体 1px 白边框;一致性状态逐字节相同;MENU-STATE-00 池水位 = MENU_POOL_BASELINE。
|
||
|
||
退出码: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"
|
||
|
||
STATES = [
|
||
("MENU-STATE-00", ""),
|
||
("MENU-STATE-01", "d"),
|
||
("MENU-STATE-02", "dd"),
|
||
("MENU-STATE-03", "ddu"),
|
||
("MENU-STATE-04", "de"),
|
||
("MENU-STATE-05", "ded"),
|
||
("MENU-STATE-06", "des"),
|
||
("MENU-STATE-07", "ddes"),
|
||
]
|
||
|
||
W = H = 160
|
||
STRIDE = W // 8 # I1:20 字节/行
|
||
ROW_H = 15
|
||
BAND_MIN = 0.50 # 选中行白带:白像素占比下限
|
||
NBAND_LO = 0.03 # 非选行:白字占比下限(文本必须存在)
|
||
NBAND_HI = 0.45 # 非选行:白像素占比上限(背景须为黑)
|
||
|
||
# 根框几何(view_init):x=0,y=44,w=66,h=107
|
||
ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H = 0, 44, 66, 107
|
||
|
||
# LVGL 池基线水位(menu build done,x64 模拟器):菜单结构/样式变更导致漂移时,
|
||
# 先确认新值合理再更新此常量(内存回归门禁,见 docs/UI设计/UI_自动化测试流程.md)
|
||
# 2026-09-14: 4784 → 5440(+656B)= run_headless 新增 ui_standby_init():待机模块
|
||
# 常驻屏 s_scr + 容器 s_cont 两个 lv_obj(先于池测量创建),属有意结构变更。
|
||
# 2026-10-11: 5440 → 5368(−72B)= 裁剪 LVGL 控件类(F-2:label/button/buttonmatrix/
|
||
# bar/textarea 全关,simulator/lv_conf.h 与 Lib/lv_conf.h 对齐为同一特性子集)。
|
||
# 像素逐字节未变(11 页黄金参考在**裁剪前**生成、裁剪后全部一致);差值来自
|
||
# lv_theme_apply 不再为已不存在的控件类登记样式条目 → 每对象样式表更小。更优。
|
||
MENU_POOL_BASELINE = 5368
|
||
|
||
|
||
def load(path):
|
||
data = open(path, "rb").read()
|
||
if len(data) < STRIDE * H:
|
||
raise RuntimeError("shot 过短: %s (%d)" % (path, len(data)))
|
||
px = [[0] * W for _ in range(H)]
|
||
for y in range(H):
|
||
row = data[y * STRIDE:(y + 1) * STRIDE]
|
||
for x in range(W):
|
||
px[y][x] = (row[x // 8] >> (7 - x % 8)) & 1
|
||
return px
|
||
|
||
|
||
def frac(px, x1, x2, y1, y2):
|
||
tot = wh = 0
|
||
for y in range(y1, y2 + 1):
|
||
for x in range(x1, x2 + 1):
|
||
tot += 1
|
||
wh += px[y][x]
|
||
return wh / tot if tot else 0.0
|
||
|
||
|
||
def root_row_strip(i):
|
||
y1 = ROOT_Y0 + 1 + i * ROW_H
|
||
return (ROOT_X0 + 1, ROOT_X0 + ROOT_W - 2, y1, y1 + ROW_H - 1)
|
||
|
||
|
||
def l2_geom(root_sel, cnt):
|
||
x0 = ROOT_X0 + ROOT_W - 1 # 65(共享边框列)
|
||
y0 = ROOT_Y0 + 1 + root_sel * ROW_H # 顶对齐父框选中行行顶
|
||
w = 42 # 2+36+2 + 2 边框(3 汉字行)
|
||
h = cnt * ROW_H + 2
|
||
return x0, y0, w, h
|
||
|
||
|
||
def l2_row_strip(x0, y0, i, w):
|
||
y1 = y0 + 1 + i * ROW_H
|
||
return (x0 + 1, x0 + w - 2, y1, y1 + ROW_H - 1)
|
||
|
||
|
||
fails = []
|
||
|
||
|
||
def check(cond, msg):
|
||
if not cond:
|
||
fails.append(msg)
|
||
|
||
|
||
def raw(path):
|
||
return open(path, "rb").read()
|
||
|
||
|
||
def main():
|
||
if not EXE.exists():
|
||
print("FAIL: 找不到 %s(先构建 simulator)" % EXE)
|
||
return 2
|
||
|
||
tmp = tempfile.mkdtemp(prefix="dtu_ui_rows_")
|
||
paths = {}
|
||
s0_out = None
|
||
try:
|
||
for name, seq in STATES:
|
||
out = os.path.join(tmp, "rows_%s.bin" % name)
|
||
# 空临时目录当工作目录:模拟器按 cwd 读写 sim_config.ini,
|
||
# 残留档(--cfgtest 会伪造 4000 B)会改变页面内容 → 回归必须自洽
|
||
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True,
|
||
cwd=tmp)
|
||
if r.returncode != 0:
|
||
print("[%s] 运行失败 rc=%d: %s" % (name, r.returncode,
|
||
r.stderr.decode(errors="replace")[:200]))
|
||
return 2
|
||
paths[name] = out
|
||
if name == "MENU-STATE-00":
|
||
s0_out = r.stdout
|
||
|
||
# MENU-STATE-00(空键序)= 菜单基线:池水位门禁
|
||
m = re.search(r"pool after keys: used=(\d+)", s0_out.decode(errors="replace"))
|
||
if not m:
|
||
print("FAIL: MENU-STATE-00 输出未找到 'pool after keys'")
|
||
return 2
|
||
used = int(m.group(1))
|
||
check(used == MENU_POOL_BASELINE,
|
||
"MENU-STATE-00 池水位 %d != 基线 %d(菜单结构/样式变更?确认合理后更新常量)"
|
||
% (used, MENU_POOL_BASELINE))
|
||
|
||
shots = {n: load(p) for n, p in paths.items()}
|
||
S = shots
|
||
|
||
def row_check(px, x1, x2, y1, y2, band, tag):
|
||
f = frac(px, x1, x2, y1, y2)
|
||
if band:
|
||
check(f > BAND_MIN, "%s 选中行白占比 %.3f <= %.2f" % (tag, f, BAND_MIN))
|
||
# 带内须有黑字(文本落在白带上)
|
||
check(frac(px, x1, x2, y1, y2) < 0.999, "%s 带内无黑字" % tag)
|
||
else:
|
||
check(NBAND_LO < f < NBAND_HI, "%s 非选行白占比 %.3f 不在 (%.2f,%.2f)"
|
||
% (tag, f, NBAND_LO, NBAND_HI))
|
||
|
||
def border_check(px, x0, y0, w, h, tag):
|
||
for y in range(y0 + 1, y0 + h - 1):
|
||
check(px[y][x0] == 1 and px[y][x0 + w - 1] == 1,
|
||
"%s 左右边框 y=%d 非白" % (tag, y))
|
||
for x in range(x0 + 1, x0 + w - 1):
|
||
check(px[y0][x] == 1 and px[y0 + h - 1][x] == 1,
|
||
"%s 上下边框 x=%d 非白" % (tag, x))
|
||
|
||
# --- MENU-STATE-00:根框 7 行,sel=0 ---
|
||
p = S["MENU-STATE-00"]
|
||
border_check(p, ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H, "S0根框")
|
||
for i in range(7):
|
||
row_check(p, *root_row_strip(i), band=(i == 0), tag="S0行%d" % i)
|
||
|
||
# --- MENU-STATE-01/MENU-STATE-02:sel 移位 ---
|
||
for name, sel in (("MENU-STATE-01", 1), ("MENU-STATE-02", 2)):
|
||
p = S[name]
|
||
border_check(p, ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H, name + "根框")
|
||
for i in range(7):
|
||
row_check(p, *root_row_strip(i), band=(i == sel), tag=name + "行%d" % i)
|
||
|
||
# --- MENU-STATE-04:L2 开于 sel=1 行右侧(3 子项、无箭头),根框区与 MENU-STATE-01 相同 ---
|
||
p = S["MENU-STATE-04"]
|
||
lx0, ly0, lw, lh = l2_geom(1, 3)
|
||
border_check(p, lx0, ly0, lw, lh, "MENU-STATE-04-L2框")
|
||
for i in range(3):
|
||
row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 0), tag="MENU-STATE-04-L2行%d" % i)
|
||
# 根框区(x 0..65)须与 MENU-STATE-01 逐像素一致
|
||
for y in range(H):
|
||
for x in range(0, ROOT_X0 + ROOT_W):
|
||
check(S["MENU-STATE-01"][y][x] == p[y][x], "MENU-STATE-04 根框区(x=%d,y=%d) 与 MENU-STATE-01 不一致" % (x, y))
|
||
# L2 框下方区域须全黑(无残留对象)
|
||
f = frac(p, lx0 + 1, lx0 + lw - 2, ly0 + lh + 1, min(ly0 + lh + 14, H - 1))
|
||
check(f == 0.0, "MENU-STATE-04 L2 框下方有像素(占比 %.3f)" % f)
|
||
|
||
# --- MENU-STATE-05:L2 sel=1,根框区仍与 MENU-STATE-01 同 ---
|
||
p = S["MENU-STATE-05"]
|
||
lx0, ly0, lw, lh = l2_geom(1, 3)
|
||
for i in range(3):
|
||
row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 1), tag="MENU-STATE-05-L2行%d" % i)
|
||
for y in range(H):
|
||
for x in range(0, ROOT_X0 + ROOT_W):
|
||
check(S["MENU-STATE-01"][y][x] == p[y][x], "MENU-STATE-05 根框区(x=%d,y=%d) 与 MENU-STATE-01 不一致" % (x, y))
|
||
|
||
# --- 一致性状态:逐字节全像素比对 ---
|
||
for a, b, tag in (("MENU-STATE-03", "MENU-STATE-01", "ddu==d"),
|
||
("MENU-STATE-06", "MENU-STATE-01", "des==d(往返)"),
|
||
("MENU-STATE-07", "MENU-STATE-02", "ddes==dd(往返)")):
|
||
if raw(paths[a]) == raw(paths[b]):
|
||
print("[%s] PASS 全像素一致 (%s)" % (a, tag))
|
||
else:
|
||
pa, pb = S[a], S[b]
|
||
diff = sum(1 for y in range(H) for x in range(W) if pa[y][x] != pb[y][x])
|
||
fails.append("%s 与 %s 不一致(%d px,%s)" % (a, b, diff, tag))
|
||
|
||
if fails:
|
||
print("FAIL: %d 项断言未过" % len(fails))
|
||
for msg in fails[:40]:
|
||
print(" -", msg)
|
||
if len(fails) > 40:
|
||
print(" ... 另 %d 项" % (len(fails) - 40))
|
||
return 1
|
||
print("PASS: 8 状态全部断言通过(%d 图,池基线 %d B)" % (len(paths), MENU_POOL_BASELINE))
|
||
return 0
|
||
finally:
|
||
# 清理临时截图(失败时保留现场便于排查:仅成功时删)
|
||
if not fails:
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|