fix(台架脚本): board_ui_nav 的 UNKNOWN 回退键序改为 e-s-l,冷启动落在待机态不再空转

旧逻辑 UNKNOWN 分支里的 l 条件**永不可达**:classify 只在 MENU 态填 cols,UNKNOWN 恒
cols=None ⇒ 回退实际等于静默连按 s。而本机待机态按 s/l 无效、要按 e 才进菜单 ⇒
冷启动恰好落在待机态时,闭环导航直接开不了工(症状是界面点不动,脚本却报正常)。

现在 BACKOFF_KEYS 默认 esl、--backoff-keys 可覆盖(要旧行为填 sl),回退时**循环消费
键序、绝不连按同一键**,并新增 _dump_unknown 把认不出的帧存证下来,免得下次只能靠
复述。同笔带上 task-24 在 19:41:56 重跑产生的 trace 归档(1 行,不是本次手改)。
This commit is contained in:
2026-10-26 23:42:27 +08:00
parent 8ff2743374
commit 496b4dc5e2
2 changed files with 91 additions and 11 deletions
@@ -138,4 +138,4 @@
"page": "view_set",
"cols": null
}
]
]
+90 -10
View File
@@ -22,6 +22,8 @@
python scripts/board_ui_nav.py --walk # 台架窗口:走全部绑定页并存帧
python scripts/board_ui_nav.py --board --to yx --grab /tmp/yx
可选:--trace 路径(步迹落盘) --dump-fail 目录(把认错的那一屏存下来)
可选:--backoff-keys esl(UNKNOWN 屏的回退键序;默认 esl 含 e——本机待机态
按 s/l 无效、要按 e 才进菜单,2026-10-26 task-24 台架实测)
--board / --walk 属真机动作:必须另加 --i-hold-bench-lock 显式确认已持 bench 锁,
否则直接拒绝(退出码 3,不碰网络)——台架是独占资源,脚本不替人判断有没有在用。
--walk 的存证目录默认在 ci-artifacts/uinav/walk(版本控制之外),--outdir 可改;
@@ -57,6 +59,11 @@ WALK_DIR = os.environ.get("DSH_UINAV_WALK") or os.path.join(REPO, "ci-artifacts"
# (判据脚本自己在提交门禁里跑,顺手往 scripts/ 写 json 就是把工作树弄脏)
TRACE = None
FAIL_DIR = None
# UNKNOWN 屏回退键序(task-24,2026-10-26 台架实证):to_root 认不出屏时按此序**循环**消费,
# 绝不连按同一键。默认含 e——本机待机态按 s/l 无效、要按 e 才进菜单;旧逻辑 UNKNOWN 分支
# 的 l 条件永不可达(classify 只在 MENU 态填 cols,UNKNOWN 恒 cols=None),等于静默连按 s:
# 冷启动恰好落在待机态 = 闭环导航直接开不了工。--backoff-keys 可覆盖(如旧行为 "sl")。
BACKOFF_KEYS = "esl"
W = H = 160
STRIDE = W // 8
@@ -576,6 +583,42 @@ class Navigator(object):
"page": st["page"], "cols": st["cols"]})
return fr, st
def _dump_unknown(self, fr, st):
"""认不出屏时的现场取证(task-24:不许静默反复按键)。
判定依据 = 现场复算 classify 的三个判据源、与它的判定顺序同口径——打印的是
"为什么没认出来",不是另立一套标准:密码页投票(4 字中 3 判 PWD)、标题命中
(page_of 返回的态键)、菜单选带条数(menu_cols)。另打 40x20 墨量预览(每字符
= 一个 4x4 块的 band_frac),"当前这一屏"在控制台直接可读;给了 --dump-fail 时
把原始帧一并落盘——台架窗口短,事后翻帧比现场重走便宜(同 go_page 的 fail_ 帧先例)。
"""
votes = 0
for g in self.clf.pwd_chars:
if fr.match(g, (24, 80)):
votes += 1
_pid, keys, txt = self.clf.page_of(fr)
cols = self.clf.menu_cols(fr)
print(" 依据:ink=%d 密码投票 %d/4 标题=%s%s 选带=%d 页码=%s"
% (fr.ink, votes, txt or "未命中",
"(态键 %s)" % ",".join(keys) if keys else "",
len(cols), str(st.get("no"))))
print(" 当前帧 40x20 预览(# 有墨 / . 空白):")
for y in range(0, H, 4):
row = "".join("#" if fr.band_frac(y, min(y + 4, H), x, min(x + 4, W)) > 0.0
else "." for x in range(0, W, 4))
print(" |%s|" % row)
if FAIL_DIR:
try:
if not os.path.isdir(FAIL_DIR):
os.makedirs(FAIL_DIR)
tag = "".join(c if c.isalnum() else "_" for c in (fr.name or "frame"))[:24]
fn = os.path.join(FAIL_DIR, "unknown_%s_%d.bin" % (tag, fr.ink))
with open(fn, "wb") as fp:
fp.write(fr.data)
print(" 原始帧已存:", fn)
except OSError as exc:
print(" 存原始帧失败(不影响判定):", exc)
def to_root(self, limit=14):
"""回到"只有一列打开"的根菜单。
@@ -584,6 +627,7 @@ class Navigator(object):
· **级联列只能用 l 收**,菜单态按 s 不收列(Lead 大窗实测)——沿用盲走脚本
的 ssssssss 回根法,正是深页错位的来源之一。
"""
unk = 0
for _ in range(limit):
fr, st = self.snap()
if st["kind"] == "MENU" and self.n_cols(st) == 1:
@@ -603,14 +647,24 @@ class Navigator(object):
self._step("s", "退出页面 / 密码页 / 空白态回菜单")
continue
if st["kind"] == "UNKNOWN":
# 认不出的屏:先按 l(收列不会造成破坏),再按 s 兜一次
if self.n_cols(st) > 1:
self._step("l", "UNKNOWN 且看起来有多列 → 收列")
else:
self._step("s", "UNKNOWN 且无列可收 → 退出")
# 认不出的屏:按可配置键序**循环**退避(默认 esl,--backoff-keys 可改),
# 每轮先打印当前帧与判定依据再按键(task-24 两条台架实证教训都在这):
# · 旧版"看起来多列按 l 否则按 s"——UNKNOWN 态 cols 恒 None(classify 只在
# MENU 态填 cols),l 分支永不可达,实际是静默连按 s;本机待机态 s 无效、
# 要按 e 才进菜单,冷启动落在待机态 = 板端闭环回归不可复现;
# · 静默反复按同一键等于什么都没做,现场只剩耗时、不留证据。
keys = BACKOFF_KEYS or "esl"
ch = keys[unk % len(keys)]
unk += 1
why = ("UNKNOWN 屏第 %d 次:按 '%s'(退避键序 %r,可 --backoff-keys 覆盖)"
% (unk, ch, keys))
print("[未知屏] %s" % why)
self._dump_unknown(fr, st)
self._step(ch, why)
continue
self._step("l", "菜单多列 → 收回一级")
raise RuntimeError("回根失败(14 步内没见到单列根菜单)")
raise RuntimeError("回根失败(%d 步内没见到单列根菜单;UNKNOWN 退避键序 %r,"
"可用 --backoff-keys 覆盖)" % (limit, BACKOFF_KEYS))
def seek_in_column(self, col, want, limit=12):
"""在当前列把选带移到标签 want:先下后上,走到边界就换向(不猜步数)。
@@ -777,7 +831,7 @@ def selftest_board_frames(clf):
def selftest_refs(clf):
"""仓库黄金参考(18 页)逐帧分类自检:每张图的期望值 = 它的文件名。
"""仓库黄金参考(27 页)逐帧分类自检:每张图的期望值 = 它的文件名。
这一组是**语义正确的基线**(模拟器与板端同一份 UI 源码),用来把"分类器错了"与
"留档帧本来就是走歪的"分开——两类失败含义完全不同,混在一起就只能靠猜。
@@ -887,18 +941,44 @@ def _dump_trace(log):
def main(argv):
global TRACE, FAIL_DIR
global TRACE, FAIL_DIR, BACKOFF_KEYS
if "--trace" in argv:
TRACE = argv[argv.index("--trace") + 1]
if "--dump-fail" in argv:
FAIL_DIR = argv[argv.index("--dump-fail") + 1]
if "--backoff-keys" in argv:
bk = argv[argv.index("--backoff-keys") + 1]
# 只收导航器真会注的键字符;空串或含别的字符一律当场拒——退避键序打错宁可现在报错,
# 也不要它变成"跑了半天都在按奇怪的东西"(同注释行注记写错必须当场失败的口径)
if not bk or any(c not in "ulrdes" for c in bk):
print("--backoff-keys 不能为空、只允许 u/l/r/d/e/s 的组合(收到 %r)" % bk)
return 3
BACKOFF_KEYS = bk
clf = Classifier()
if "--help" in argv:
print(__doc__)
return 0
if "--list" in argv:
for k in sorted(clf.pages):
print(k, "标题=", [t for t, g in clf.titles.items() if g == k])
# 只用 Classifier 真实暴露的接口拼清单:tree(菜单树 #page= 叶 = --to 合法页 id
# 与路径的唯一真源)、title2harness(标题→黄金态键)、pwd_pages(#P 叶)、
# page_path(id→标签路径)。旧版打的 clf.titles 根本不存在(AttributeError,
# 整条子命令不可用),且 clf.pages 的键是**黄金态键**不是页 id——即便 titles 存在
# 列出来的也不能喂给 --to。**不另造标题表**:手写第二真相下次菜单树改动必然漂移。
titles_by_page = {}
for (_lvl, label, page, _np) in clf.tree:
if page:
lst = titles_by_page.setdefault(page, [])
if label not in lst:
lst.append(label)
for pid in sorted(titles_by_page):
path = page_path(clf, pid)
states = sorted({k for t in titles_by_page[pid]
for k in clf.title2harness.get(t, [])})
print("%-12s 标题=%-14s 路径=%s%s 黄金态=%s"
% (pid, "、".join(titles_by_page[pid]),
">".join(path) if path else "-",
" [#P 需密码]" if pid in clf.pwd_pages else "",
",".join(states) or "(无文本断言,认屏只认标题)"))
return 0
if "--classify" in argv:
tgt = argv[argv.index("--classify") + 1]