0f7b5a87fb
登记簿、文档与代码注释里的存量字母加数字编号(N-x / S-x / T-x / E-x / A-x / 卡号) 一律改写为语义短语,净删 2,028 处引用;删除基线与待确认、并行分道方案、排期详述、 采集保护收尾 4 篇(内容已并入 docs/任务规划/README 与登记簿)。 门禁机器与三张账同批重打:check_ids.py 新增变更记录行豁免与第二根护栏 undecidable_total_ceiling;check_test_traceability.py 只加 stdout/stderr UTF-8 reconfigure(TC_RE 未动);check_cubemx_survival.py 的 --write 固定 newline=LF; id_registry / id_aliases(+19 条 retired-to-phrase)/ id_known_backlog 重打。 test/ 56 个文件同步 TC 标签语义化,其中 18 个 TU 改了 CHECK / printf 里的标签文字, 是本批唯一改变构建产物的文件集合。 为什么合成一笔、不拆四笔(实测撞闸,非偏好):pre-commit 跑钩子前会 stash 未 staged 部分,拆开后中间提交必然是「新规则 + 旧语料」——实跑可判定 85.15% < 基线 86.00%、 不可判定 1,453 > 上限 1,166,编号门禁直接拦下。编号识别面与它描述的语料只能同码移动。 实跑基线(工作区全量):754 文件 / 8,230 处编号字面量 / 可判定 7,079 = 86.01% (基线 86.0%,容差 0.1pp)/ 不可判定绝对数 1,151(上限 1,166)/ 校验 1-6 错误级 0 / 告警 = 悬空 2(App/algo/freq.c 的 S2,属 §4.4 同号不同义)+ 孤儿 50。 基线从 84.0 抬到 86.0 的理由见 docs/门禁说明/id-check.md §7.6:本批真删了 2,028 处 引用,可判定面随真实内容上升,不是把分母做小。同批新增排除 docs/硬件平台/ 语料,使 识别面少 264 处——芯片手册位号与网表料号不属编号空间(registry 有 excluded_paths_note)。 ⚠ 余量只剩 0.11pp(约 10 处)与绝对数 15 处:把「可判定的引用」改成散文会立刻撞第二 根护栏。另遗留一项:id_aliases 里 retired 的 N / S / T / A 别名没有 scope(U1~U5 有), 今后有人新写一个 A-4,校验 3 不会响,只能靠人,建议下批补 scope。 固件不变证据:115 个 .c/.h 剥注释并归一字符串后与 HEAD 逐字节全等,残留代码差异 0; App / Src / Drivers 的字符串字面量零改动。@build 0 错误 0 警告;40/40 主机套件、2,889 断言全通过;覆盖率 line 89.2% / func 92.6%;pre-commit 48 钩子 --all-files 全绿; check_doc_links --all 通过(176 文档,章节引用无法判定 0);逐字节卫生核过:14 个带 BOM 文件与 HEAD 一致、0 个 CR、0 个 U+FFFD,字符串字面量纯 ASCII。 .cubemx_survival_baseline.json 的 watchdog-module 哈希 56bf27 变 2bb051 属合法: Src/freertos.c 的 USER CODE 区内只有注释文字随编号改写,代码未变。 同批带上体积同码凭证与 RAM 台账行(@build 之后盖戳):RW_IRAM1 79,008 B / 60.3%, RW_CCM 57,376 B / 87.9%,阈值 92%。
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 控件类(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())
|