Files
STM32F4-Base/scripts/check_font_charset.py
T
wandering1 0f7b5a87fb chore(编号): 存量编号全量转语义短语,门禁识别面与语料同批重打
登记簿、文档与代码注释里的存量字母加数字编号(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%。
2026-10-26 12:28:14 +08:00

262 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check_font_charset.py - UI 字符集差集门禁(防"缺字显示白框",原报告 §6.5 的 UI 字符集与像素回归门禁残余项)。
背景:
本工程唯一中文字体 `Drivers/BSP/UI/ui_font_cn_12.c` 是**子集**字体:只包含
`ui_cn_chars.txt` 列出的码位。生成链(font-regen skill 四步):
UI 源码 --extract_cn.py--> ui_cn_chars.txt --make_font.py--> ui_font_cn_12.c
`make_font.py` 的 `verify_coverage()` 只在**生成期**校验"txt ⊆ 字体 cmap";
若开发者改了 UI 字符串却忘了重跑 extract_cn / make_font,提交期**无任何门禁**
能拦住——缺字上机渲染为空白(白框),无日志无报错,只能靠肉眼截图发现。
本钩子把这条约定机械化(UI 字符集与像素回归门禁三钩子之二,golden 已建、registration-check 见 §2.18)。
判定口径(两段链条,缺一环即白框):
A. 必需字符集(源码在用 ∪ KEEP_EXTRA 保留字) vs ui_cn_chars.txt
必需、txt 无 -> [GAP] FAIL(字体必然缺此字形 → 白框;保留字被剔同样 FAIL——
防未来 extract_cn 按"源码已不用"静默删掉已落盘记录的用字)
txt 有、非必需 -> [WARN] 不拦(未知冗余字形只费 Flash 不致白框;--strict 升级为
FAIL;KEEP_EXTRA 内的字不计入冗余——刻意保留 ≠ 未知冗余)
B. ui_cn_chars.txt vs ui_font_cn_12.c 的 cmap
txt 有、字体无 -> [GAP] FAIL(lv_font_conv 对缺字形/低码位会**静默丢字**)
A 通过(txt ⊇ 必需)+ B 通过(字体 ⊇ txt)⟹ 字体 ⊇ 源码在用与刻意保留的每个字。
刻意保留清单 KEEP_EXTRA(同构 check_registration.py 的 KNOWN_GAPS:存量 + 逐条理由):
extract_cn 是**静态扫描**,照不出运行期**已落盘数据**里的汉字——典型如
/log/selftest.log 的定长 64 B 记录(跨固件持久,自检页直接渲染 desc)。这类字
源码已不用、但删字形会让**历史审计记录**渲染空白且不可逆;收益(每字 ~200 B RO,
占 1 MB Flash 万分之几)与代价完全不对称(同 2026-10-15 内存优化簇"收益不抵风险"
整簇暂缓的裁定逻辑)。新增条目必须写理由与出处;确认相关落盘数据已不存在后方可
删条目(删条目后下次 font-regen 自然瘦身)。
⚠ font-regen 流程:extract_cn 重生成 txt 时**不会**带上保留字(源码已不用),须把
KEEP_EXTRA 的字并回 ui_cn_chars.txt 再跑 make_font.py,否则本钩子 A 段 FAIL 并
逐字给出理由——刻意设计(静默丢失 = 事故),不是误报。
单一规则源(关键设计):
本脚本**不自创**第二套解析规则——字符集提取复用 `tools/font_gen/extract_cn.py`
的 `SCAN` 清单与 `collect()` 状态机(跳过注释/字符常量、只取双引号串内非 ASCII),
cmap 覆盖判定复用 `tools/font_gen/make_font.py` 的 `verify_coverage()`(含稀疏
unicode_list 与补回字形的单码位 cmap 条目)。两套规则必然漂移,这正是本项目
反复在防的(见 pre-commit使用说明.md §5.5 教训)。
用法(仓库根目录):
& '.venv\\Scripts\\python.exe' scripts/check_font_charset.py
& '.venv\\Scripts\\python.exe' scripts/check_font_charset.py --strict
# 注入测试用覆盖(不改动仓库文件即可复现"缺字"场景):
# --chars <副本> 模拟字符集文件缺/多字
# --font <副本> 模拟字体 cmap 丢字形
退出码:
0 = 通过(必需字符——源码在用 ∪ KEEP_EXTRA——全部在字体里;未知冗余仅 WARN)
1 = 失败(存在白框方向的缺口:必需字不在 txt(含 KEEP_EXTRA 保留字被剔),或 txt 字不在字体;--strict 时未知冗余也算)
2 = 环境错误(字符集文件 / 字体文件 / 生成器脚本缺失)
创建日期:2026-10-15(UI 字符集与像素回归门禁残余项,报告 §6.5 / §7 交付物清单"字符集差集检查"行)
"""
import argparse
import importlib.util
import os
import re
import sys
# --- 仓库根(scripts/ 的上一级)---
REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
TOOLS_DIR = os.path.join(REPO_ROOT, "tools", "font_gen")
# --- 刻意保留的冗余字(同构 check_registration.py 的 KNOWN_GAPS:逐条理由 + 出处)---
# 这些字**源码已不用**,但 2026-09-14~09-20 窗口的固件把它们写进了 /log/selftest.log
# 定长 64 B 记录(跨固件持久,自检页渲染 desc)。删字形 = 历史故障行永久渲染空白,
# 而收益只有每字 ~200 B RO——不对称,故保留并登记(Lead 裁定 2026-10-15,task-6 D 案)。
# 钩子按"必需"对待:txt/字体里缺任一保留字 → FAIL(防下次 font-regen 静默再删)。
KEEP_EXTRA = {
"异": "9eb1bd63(2026-09-14) 中文自检描述 \"NAND异常/ADC异常/时钟芯片异常/通讯异常/"
"文件系统异常\" 曾落盘 /log/selftest.log;6f51c62a(2026-09-20 串口输出禁中文) "
"改 ASCII,但已落盘记录跨固件持久,删字形使历史故障行渲染空白",
"常": "同 \"异常\" 系记录(时钟芯片异常/文件系统异常 等,9eb1bd63 写入,落盘持久)",
"芯": "9eb1bd63 写入的 \"时钟芯片异常\" 落盘 /log/selftest.log(SD2506 时钟芯片故障"
"路径当时可触发,记录持久)",
"片": "同 \"时钟芯片异常\"(9eb1bd63 写入,落盘持久)",
}
# 2026-10-25 道E task-15 补充:**成组保留**的历史落盘记录用字(35 字)。
# 发现方式:本钩子的「未知冗余」WARN 清单(txt 有、源码无、且不在 KEEP_EXTRA)实测正好
# 35 字——全部是 2026-09 窗口固件写进 /log/selftest.log(与 /log/run.log)定长 64 B 记录
# 里的中文描述用字。那批源码文案后来被改写(串口侧改 ASCII),**逐字出处已不可靠复原**:
# 能确认来源是落盘记录用字(不是美术字、也不是手工修 cmap),但指不到「这个字出自哪条
# commit」。两难取其下——字形不可再生(删了就永久白框)而保留成本极低(35 字 ≈ 1 KB RO)
# → 整组保留并在此**点名成组**,而不是给每个字编一条看起来可信的理由。
# 删除前置条件:确认设备上已无该窗口的落盘记录(或已接受历史记录部分渲染白框)。
# ⚠ 本组真正的价值是把一条路径钉死:**「按 extract_cn 输出重写 ui_cn_chars.txt」= 洗字形**
# (extract 只认当前源码字面量,实测 144 字,而 txt 现在 183 字)——谁按它的输出覆盖
# txt,就会把这 35 字连同上面 4 字一起洗掉,且只在渲染历史日志时才暴露、无日志无报错。
# 成因与判据详述 → docs/门禁说明/font-charset.md §6.1
_KEEP_HIST_SELFTEST = ("历史落盘记录用字(2026-09 窗口固件写入 /log/selftest.log 与"
" /log/run.log 的定长 64 B 中文描述;源码文案已改写、逐字出处不可靠"
" 复原,故整组保留——删任一即让对应历史行永久渲染空白且不可逆;"
" 成因与删除前置条件见 docs/门禁说明/font-charset.md §6.1)")
KEEP_EXTRA.update({c: _KEEP_HIST_SELFTEST for c in
"一位公分合外已年度延态总接显最月未求温状电示秒请跳载近道部配重长间闸障"})
def load_tool(name):
"""按文件路径加载 tools/font_gen/<name>.py(无包结构,importlib 直载)。
两个生成器模块级只有常量与函数定义(main 均有 __name__ 守卫),导入零副作用。
"""
path = os.path.join(TOOLS_DIR, name + ".py")
if not os.path.isfile(path):
print("[ENV-ERROR] 生成器缺失: %s" % path)
sys.exit(2)
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def fmt_char(c):
"""字符 -> `你(U+4F60)` 形态;GBK 控制台打不出的符号由 stdout replace 兜底。"""
return "%s(U+%04X)" % (c, ord(c))
def rel(path):
"""仓库相对路径;跨盘符(--font/--chars 指向临时目录)时退回原路径。"""
try:
return os.path.relpath(path, REPO_ROOT)
except ValueError:
return path
def fmt_chars(chars):
return " ".join(fmt_char(c) for c in sorted(chars))
def extract_source_chars(extract_cn):
"""复用 extract_cn 的 SCAN 清单 + collect() 状态机提取源码实际用到的字符集。"""
chars = set()
n = 0
for f in extract_cn.SCAN:
if not os.path.exists(f):
print(" skip (not found): %s" % os.path.relpath(f, REPO_ROOT))
continue
chars |= extract_cn.collect(open(f, encoding="utf-8-sig", errors="ignore").read())
n += 1
return chars, n
def check_font_coverage(make_font, font_path):
"""复用 make_font.verify_coverage 判定"字符集文件 ⊆ 字体 cmap"。
返回 None = 通过;返回 (缺字列表 | None, 原始报错) —— 缺字列表由报错里的
U+XXXX 码位反解而来(verify_coverage 只打印码位,这里补出字形便于定位)。
"""
try:
make_font.verify_coverage(font_path)
return None
except SystemExit as e:
msg = str(e)
codes = re.findall(r"U\+([0-9A-Fa-f]{4,6})", msg)
missing = [chr(int(x, 16)) for x in codes] if codes else None
return missing, msg
except OSError as e:
print("[ENV-ERROR] 读取字体失败: %s" % e)
sys.exit(2)
def main():
# Windows GBK 控制台打不出 ▶ 等符号:降级替换,避免 UnicodeEncodeError 崩溃
# (与 make_font.py 同款处理;pre-commit 经管道捕获时同理)
try:
sys.stdout.reconfigure(errors="replace")
except Exception:
pass
ap = argparse.ArgumentParser(description="UI 字符集差集门禁(源码 ↔ ui_cn_chars.txt ↔ ui_font_cn_12.c)")
ap.add_argument("--chars", default=None,
help="覆盖字符集文件路径(默认 ui_cn_chars.txt;注入测试用)")
ap.add_argument("--font", default=None,
help="覆盖字体 .c 路径(默认 Drivers/BSP/UI/ui_font_cn_12.c;注入测试用)")
ap.add_argument("--strict", action="store_true",
help="未知冗余字(txt 有、源码无、且不在 KEEP_EXTRA)也按 FAIL 处理(供审计/瘦身排期)")
args = ap.parse_args()
extract_cn = load_tool("extract_cn")
make_font = load_tool("make_font")
chars_path = args.chars or make_font.CHARS_FILE
font_path = args.font or make_font.OUT
for path, label in ((chars_path, "字符集文件"), (font_path, "字体文件")):
if not os.path.isfile(path):
print("[ENV-ERROR] %s缺失: %s" % (label, path))
return 2
# --- A 段:必需字符(源码在用 ∪ KEEP_EXTRA)vs 字符集文件 ---
src, nfiles = extract_source_chars(extract_cn)
txt = set(open(chars_path, encoding="utf-8").read().strip())
keep = set(KEEP_EXTRA)
missing_src = sorted(src - txt) # 白框方向 -> FAIL
missing_keep = sorted(keep - txt - src) # 保留字被静默剔除 -> FAIL(防历史落盘记录变白框)
extra_in_txt = sorted(txt - src - keep) # 未知冗余 -> WARN(--strict 升级)
kept_present = sorted((keep & txt) - src) # 保留字在位且源码不用 -> [KEEP] 信息行
# --- B 段:字符集文件 vs 字体 cmap(复用 verify_coverage,其事实源 = CHARS_FILE)---
make_font.CHARS_FILE = chars_path # 允许 --chars 注入;默认即仓库根 ui_cn_chars.txt
gaps = []
warns = []
keeps = []
keep_reasons = []
if missing_src:
gaps.append("[GAP] 字符集缺字 %d 个(UI 源码在用、不在 %s -> 字体必然无此字形,上机白框): %s"
% (len(missing_src), rel(chars_path), fmt_chars(missing_src)))
if missing_keep:
gaps.append("[GAP] 刻意保留字被剔除 %d 个(在 KEEP_EXTRA、不在 %s -> 历史落盘记录"
"(如 /log/selftest.log)将渲染空白,不可逆;把这些字并回字符集后重跑 make_font): %s"
% (len(missing_keep), rel(chars_path), fmt_chars(missing_keep)))
keep_reasons = [" KEEP_EXTRA 理由 %s: %s" % (fmt_char(c), KEEP_EXTRA[c])
for c in missing_keep]
if kept_present:
keeps.append("[KEEP] 刻意保留 %d 个(源码已不用但按 KEEP_EXTRA 保留,不计冗余;"
"理由见脚本头部与字典): %s" % (len(kept_present), fmt_chars(kept_present)))
if extra_in_txt:
line = ("[WARN] 字符集未知冗余 %d 个(在 txt、源码已不用且不在保留清单 -> 只费 Flash "
"不致白框;重跑 font-regen 可瘦身): %s" % (len(extra_in_txt), fmt_chars(extra_in_txt)))
if args.strict:
gaps.append(line.replace("[WARN]", "[GAP] (--strict)", 1))
else:
warns.append(line)
result = check_font_coverage(make_font, font_path)
if result is not None:
missing, msg = result
if missing:
gaps.append("[GAP] 字体缺字 %d 个(在字符集、不在 %s 的 cmap -> lv_font_conv 静默丢字,上机白框): %s"
% (len(missing), rel(font_path), fmt_chars(missing)))
else:
gaps.append("[GAP] 字体覆盖校验失败: %s" % msg)
# --- 输出 ---
print("== UI 字符集差集检查 ==")
print("扫描 UI 源码: %d 个文件 | 源码在用: %d 字 | 保留清单: %d 字 | 字符集: %d 字"
% (nfiles, len(src), len(keep), len(txt)))
for k in keeps:
print(k)
for w in warns:
print(w)
for g in gaps:
print(g)
for kr in keep_reasons:
print(kr)
if gaps:
print("修复:按 font-regen skill 四步重跑(extract_cn.py -> 并回 KEEP_EXTRA -> make_font.py -> diff 检查),"
"ui_cn_chars.txt 与 ui_font_cn_12.c 随代码一起提交")
print("结果: FAIL(%d 项缺口,%d 个 WARN)" % (len(gaps), len(warns)))
return 1
print("结果: PASS(必需字符(源码在用 ∪ 保留清单)全部在字体子集内%s%s)"
% (";%d 个保留字在位" % len(kept_present) if kept_present else "",
";%d 个未知冗余仅 WARN" % len(extra_in_txt) if warns else ""))
return 0
if __name__ == "__main__":
sys.exit(main())