增加两页UI

This commit is contained in:
2026-09-09 13:18:06 +08:00
parent 9ead5679a1
commit 24c2f57555
24 changed files with 1436 additions and 122 deletions
+226
View File
@@ -0,0 +1,226 @@
#!/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):
S0 "" 根框 7 行 sel=0
S1 "d" sel=1
S2 "dd" sel=2
S3 "ddu" 回退 → 须与 S1 全像素一致
S4 "de" 开 L2(实时数据 3 子项),根框不变
S5 "ded" L2 sel=1
S6 "des" 返回 → 须与 S1 全像素一致(销毁/重建一致性)
S7 "ddes" 根框 sel=2、L2 已毁 → 须与 S2 全像素一致
断言:选中行白带(白占比>0.50)+ 带内黑字;非选行黑底白字(0.03<占比<0.45);
框体 1px 白边框;一致性状态逐字节相同;S0 池水位 = 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 = [
("S0", ""),
("S1", "d"),
("S2", "dd"),
("S3", "ddu"),
("S4", "de"),
("S5", "ded"),
("S6", "des"),
("S7", "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)
MENU_POOL_BASELINE = 4784
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)
r = subprocess.run([str(EXE), "--keys", seq, out], capture_output=True)
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 == "S0":
s0_out = r.stdout
# S0(空键序)= 菜单基线:池水位门禁
m = re.search(r"pool after keys: used=(\d+)", s0_out.decode(errors="replace"))
if not m:
print("FAIL: S0 输出未找到 'pool after keys'")
return 2
used = int(m.group(1))
check(used == MENU_POOL_BASELINE,
"S0 池水位 %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))
# --- S0:根框 7 行,sel=0 ---
p = S["S0"]
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)
# --- S1/S2:sel 移位 ---
for name, sel in (("S1", 1), ("S2", 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)
# --- S4:L2 开于 sel=1 行右侧(3 子项、无箭头),根框区与 S1 相同 ---
p = S["S4"]
lx0, ly0, lw, lh = l2_geom(1, 3)
border_check(p, lx0, ly0, lw, lh, "S4-L2框")
for i in range(3):
row_check(p, *l2_row_strip(lx0, ly0, i, lw), band=(i == 0), tag="S4-L2行%d" % i)
# 根框区(x 0..65)须与 S1 逐像素一致
for y in range(H):
for x in range(0, ROOT_X0 + ROOT_W):
check(S["S1"][y][x] == p[y][x], "S4 根框区(x=%d,y=%d) 与 S1 不一致" % (x, y))
# L2 框下方区域须全黑(无残留对象)
f = frac(p, lx0 + 1, lx0 + lw - 2, ly0 + lh + 1, min(ly0 + lh + 14, H - 1))
check(f == 0.0, "S4 L2 框下方有像素(占比 %.3f)" % f)
# --- S5:L2 sel=1,根框区仍与 S1 同 ---
p = S["S5"]
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="S5-L2行%d" % i)
for y in range(H):
for x in range(0, ROOT_X0 + ROOT_W):
check(S["S1"][y][x] == p[y][x], "S5 根框区(x=%d,y=%d) 与 S1 不一致" % (x, y))
# --- 一致性状态:逐字节全像素比对 ---
for a, b, tag in (("S3", "S1", "ddu==d"),
("S6", "S1", "des==d(往返)"),
("S7", "S2", "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())