门禁扩容:新增 lv-conf-sync、simulator/ 纳入全部钩子;含 L3 定值/元件框架落地

本轮把"自研代码落在门禁之外"的口子收掉,同时落地 LVGL 特性裁剪与
L3 保护定值/元件框架。

门禁(.pre-commit-config.yaml,26 个钩子)
- 新增 scripts/check_lv_conf_sync.py + 钩子 lv-conf-sync:比对两份
  lv_conf.h 的**生效值**(未定义则取 lv_conf_internal.h 兜底默认),
  抓 MISMATCH / LVGL 不认的宏名 / 同文件重复定义
- 顶层 exclude 删掉 simulator/:其下 21 个 tracked 文件全是本项目自研
  代码,生成物(build/、SDL2/、tests/ref/、shot*)已在 .gitignore;
  doc-links 的 exclude 同步删除
- lv-conf-sync 的 files: 覆盖两端配置 → 只改 simulator/lv_conf.h 也触发
  (pre-commit 4.x 已移除 always: true,实测只告警并跳过,无兜底开关)
- 新增 prot-set-unit-test / prot-elm-unit-test 两个主机侧单测钩子
- S11(2.3.5) 判定细化:禁的是"用路径找文件",尖括号系统头带子目录
  (如 <sys/stat.h>)不再误报;全仓库复核仅 2 处项目代码判定变化

模拟器代码整改(纳入钩子后暴露,全部修代码而非豁免)
- cppcheck 4 处:sim_syslog.c 判空分支内解引用 p_epoch(真实 bug)、
  2 处 struct tm 未初始化、1 处指针可加 const
- coding-standard 61 处:44 处函数左大括号并到右括号同行、8 处补函数
  注释、3 处补文件头注释块、2 处 __ 前缀保护宏改 XXX_H、2 处 sprintf
  改 snprintf(写入字节数与原来完全一致)

LVGL 特性裁剪(F-1/F-2/F-3)
- 两端 lv_conf.h 对齐同一特性子集:删 26 个 LVGL 9.3 不存在的选项名、
  3 处重复定义,10 项靠默认值隐式生效的改为显式固化
- 实测对镜像体积中性;价值在防漂移,且模拟器 golden 回归跑的正是固件
  实际编译的配置

L3 软件侧
- App/algo/prot_set.* Q10 定值模型(范围校验 / 整表 all-or-nothing /
  校验和封存-自检-闭锁)+ prot_elm.* 通用"滞回+定时限"元件框架 7 实例
- test/host/prot_set、test/host/prot_elm 主机侧单测;uvprojx 挂载两处

文档同步
- pre-commit使用说明:§2.8 S11 口径、§2.15 lv-conf-sync 详解、新增 §5.5
  复盘(教训:exclude 只写真正不该进钩子的路径)
- AGENTS.md、UI_设计架构 §1.4 第 10 条、RAM 分析、审查报告刷新与销项

验证
- @build BUILD_OK 0/0(Code=194276 RO=15872 RW=876 ZI=139892)
- 模拟器重建:菜单 8 状态像素逐字节一致(池 5368 B)、11 内容页黄金参考
  全过(往返 5448 B)、remo sink 0/25600 位差异、--logtest/--cfgtest PASS
- 全仓库 cppcheck / coding-standard / no-mojibake --all-files 全过
- check_doc_links --all 129/0;check_mem_numbers --check 内存数字一致
- 端到端:往 simulator/lv_conf.h 注入一行漂移 → 钩子 FAIL(exit 1)
This commit is contained in:
2026-10-11 08:48:49 +08:00
parent 13a4e61c27
commit c936441c83
36 changed files with 2220 additions and 565 deletions
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""两份 lv_conf.h 特性子集一致性门禁(2026-10-11 加,随 F-1/F-2/F-3 落地)。
背景:`Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC)是**两份独立文件**,
此前长期漂移——模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、
5 个未用控件类、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 全靠
`lv_conf_internal.h` 的默认值隐式生效,把用不到的代码整份链进固件
= 实测 **64,576 B 死代码**(旧镜像的 23.5%)。更糟的是 golden 回归跑的是
模拟器那份配置,**验证不到固件实际编译的特性子集**。
判定口径(关键):比对的是**生效值**,不是"文件里写了什么"——
文件里没定义的宏,取 `Lib/lvgl/src/lv_conf_internal.h` 的默认值。
所以"MCU 没写、模拟器写了 0"会被算成 MCU=1 → 直接判 FAIL,这正是原事故形态。
四类问题:
MISMATCH 生效值不同 → FAIL(特性子集不一致,golden 回归无效)
INVALID 写了 LVGL 不认的宏名 → FAIL(如 9.3 没有 LV_USE_THEME_BASIC,写了不生效)
DUPLICATE 同一文件里定义两次 → FAIL(后者生效,极易误读)
IMPLICIT 一边靠默认值生效 → 警告(不 FAIL;若默认值与另一边不同则已升级为 MISMATCH)
用法:
python scripts/check_lv_conf_sync.py # 检查(默认)
python scripts/check_lv_conf_sync.py --list # 打印全量对照表
python scripts/check_lv_conf_sync.py --mcu A --sim B --internal C
退出码:0 = 一致;1 = 有不一致/无效/重复;2 = 环境错误(文件缺失)
"""
import argparse
import io
import os
import re
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEF_MCU = os.path.join(REPO, "Lib", "lv_conf.h")
DEF_SIM = os.path.join(REPO, "simulator", "lv_conf.h")
DEF_INTERNAL = os.path.join(REPO, "Lib", "lvgl", "src", "lv_conf_internal.h")
DEF_LVGL_SRC = os.path.join(REPO, "Lib", "lvgl", "src")
RE_DEFINE = re.compile(r"^[ \t]*#[ \t]*define[ \t]+(LV_[A-Za-z0-9_]+)[ \t]*(.*)$")
RE_IFNDEF = re.compile(r"^[ \t]*#[ \t]*ifndef[ \t]+(LV_[A-Za-z0-9_]+)[ \t]*$")
RE_ANYIF = re.compile(r"^[ \t]*#[ \t]*if(def|ndef)?\b")
RE_ENDIF = re.compile(r"^[ \t]*#[ \t]*endif\b")
RE_NUM = re.compile(r"^(?:0[xX][0-9a-fA-F]+|[-+]?\d+)[uUlL]*$")
# 两份文件自己的 include 保护宏,不参与比对
IGNORE_EXACT = {"LV_CONF_H"}
# 只属于某一端的宏(前缀匹配):模拟器驱动 / 演示 / 头文件替换 / OS 抽象细节。
# 这些不参与"特性子集"一致性判定。
IGNORE_PREFIX = (
"LV_USE_SDL", "LV_SDL_", # SDL2 驱动(仅模拟器)
"LV_USE_LINUX_", "LV_USE_WINDOWS", "LV_USE_QNX", "LV_USE_UEFI",
"LV_USE_WAYLAND", "LV_USE_GLFW", "LV_USE_NUTTX", "LV_USE_OPENGLES",
"LV_USE_EVDEV", "LV_USE_LIBINPUT", "LV_USE_GENERIC_MIPI",
"LV_USE_FT81X", "LV_USE_LOVYAN_GFX",
"LV_USE_DEMO_", # 演示工程
"LV_STDINT_INCLUDE", "LV_STDDEF_INCLUDE", "LV_STDBOOL_INCLUDE",
"LV_INTTYPES_INCLUDE", "LV_LIMITS_INCLUDE", "LV_STDARG_INCLUDE",
"LV_OS_PRIORITY", "LV_OS_TASK_NAME", # LV_USE_OS=0 时惰性
"LV_FONT_CUSTOM_INCLUDE", "LV_FONT_USE_CUSTOM_INCLUDE",
)
# 允许两端不同(已核对理由):仍会打印,但不判 FAIL。
ALLOW_DIFF = {
"LV_USE_SNAPSHOT": "模拟器 --screenshot 需要 lv_snapshot;固件不用(省 Flash)",
"LV_OBJ_STYLE_CACHE": "模拟器开样式缓存便于调试;固件靠默认",
"LV_USE_OBJ_NAME": "模拟器调试辅助(对象命名);固件不需要",
"LV_USE_OBJ_ID": "模拟器调试辅助;固件不需要",
"LV_OBJ_ID_AUTO_ASSIGN": "随 LV_USE_OBJ_ID",
"LV_USE_OBJ_ID_BUILTIN": "随 LV_USE_OBJ_ID",
"LV_USE_ASSERT_NULL": "固件关断言省体积;模拟器保留有助定位",
"LV_USE_ASSERT_MALLOC": "同上",
"LV_USE_ASSERT_USER_CODE": "同上",
"LV_USE_ASSERT_OBJ": "同上",
"LV_USE_ASSERT_STYLE": "同上",
"LV_USE_ASSERT_LAYOUT": "同上",
}
def strip_comment(text):
"""去掉行内 /* */ 与 // 注释(不跨行)。"""
text = re.sub(r"/\*.*?\*/", "", text)
text = re.sub(r"//[^\"]*$", "", text)
return text.strip()
def read_lines(path):
"""按 UTF-8 读(MCU 那份带 BOM,用 utf-8-sig 兼容)。"""
with io.open(path, "r", encoding="utf-8-sig", errors="replace") as f:
return f.read().split("\n")
def parse_defines(path):
"""返回 (name -> 首个定义值, name -> 定义次数, 出现顺序)。"""
vals, counts, order = {}, {}, []
for line in read_lines(path):
m = RE_DEFINE.match(line)
if not m:
continue
name, raw = m.group(1), strip_comment(m.group(2))
counts[name] = counts.get(name, 0) + 1
if name not in vals:
vals[name] = raw
order.append(name)
return vals, counts, order
def parse_defaults(path):
"""从 lv_conf_internal.h 抽**兜底**默认值。
该文件每个选项的形状是:
#ifndef LV_X
#ifdef CONFIG_LV_X
#define LV_X CONFIG_LV_X <- ESP-IDF 兼容分支,不是默认值
#else
#define LV_X 0 <- 真正的兜底默认值
#endif
#endif
所以必须扫完整块、取**非 CONFIG_ 的字面量**(多个取最后一个)。
"""
lines = read_lines(path)
defaults = {}
i, n = 0, len(lines)
while i < n:
m = RE_IFNDEF.match(lines[i])
if not m:
i += 1
continue
name = m.group(1)
block, depth, j = [], 0, i + 1
while j < n:
line = lines[j]
if RE_ANYIF.match(line):
depth += 1
elif RE_ENDIF.match(line):
if depth == 0:
break
depth -= 1
block.append(line)
j += 1
cands = []
for line in block:
d = RE_DEFINE.match(line)
if d and d.group(1) == name:
cands.append(strip_comment(d.group(2)) or "1")
if cands:
literal = [c for c in cands if not c.startswith("CONFIG_")]
defaults[name] = literal[-1] if literal else cands[-1]
# 注意:不能跳到 j+1——lv_conf_internal.h 自身有 `#ifndef LV_CONF_INTERNAL_H`
# 大保护块,跳过去会把整份文件吞掉(实测 defaults 变空)。逐行前进,
# 让嵌套的 `#ifndef` 也能被扫到。
i += 1
return defaults
def build_symbols(src_dir):
"""扫 LVGL 头文件里的数值宏(LV_OS_NONE / LV_STDLIB_BUILTIN / LV_TXT_ENC_UTF8 等)。"""
symbols = {}
for root, _dirs, files in os.walk(src_dir):
for fn in files:
if not fn.endswith(".h"):
continue
try:
lines = read_lines(os.path.join(root, fn))
except OSError:
continue
for line in lines:
m = RE_DEFINE.match(line)
if not m:
continue
raw = strip_comment(m.group(2))
if RE_NUM.match(raw):
symbols.setdefault(m.group(1), raw)
return symbols
def normalize(value, symbols, depth=0):
"""归一化取值:去括号/整数后缀/空白,符号名解析成数值,便于跨文件比对。"""
v = (value or "").strip()
while v.startswith("(") and v.endswith(")"):
inner = v[1:-1].strip()
if not inner:
break
v = inner
v = re.sub(r"\s+", " ", v)
if RE_NUM.match(v):
v = re.sub(r"[uUlL]+$", "", v)
return str(int(v, 0))
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", v):
if v in symbols and depth < 4:
return normalize(symbols[v], symbols, depth + 1)
return v
return re.sub(r"(\d)[uUlL]+\b", r"\1", v)
def main(argv=None):
ap = argparse.ArgumentParser(description="两份 lv_conf.h 特性子集一致性门禁")
ap.add_argument("--mcu", default=DEF_MCU)
ap.add_argument("--sim", default=DEF_SIM)
ap.add_argument("--internal", default=DEF_INTERNAL)
ap.add_argument("--lvgl-src", default=DEF_LVGL_SRC)
ap.add_argument("--list", action="store_true", help="打印全量对照表")
a = ap.parse_args(argv)
for p in (a.mcu, a.sim, a.internal):
if not os.path.isfile(p):
print("[ERROR] 文件不存在: %s" % p)
return 2
mcu, mcu_cnt, _ = parse_defines(a.mcu)
sim, sim_cnt, _ = parse_defines(a.sim)
defaults = parse_defaults(a.internal)
symbols = build_symbols(a.lvgl_src)
names = sorted(set(mcu) | set(sim))
checked = [n for n in names if n not in IGNORE_EXACT and not n.startswith(IGNORE_PREFIX)]
bad = []
warn = []
rows = []
for n in checked:
in_mcu, in_sim = n in mcu, n in sim
vm = mcu.get(n, defaults.get(n))
vs = sim.get(n, defaults.get(n))
if vm is None or vs is None:
bad.append(("UNKNOWN", n, "取不到生效值(MCU=%s / SIM=%s)" % (vm, vs)))
continue
nm, ns = normalize(vm, symbols), normalize(vs, symbols)
rows.append((n, "显式" if in_mcu else "默认", nm, "显式" if in_sim else "默认", ns))
if nm != ns:
if n in ALLOW_DIFF:
print("[allow] %-40s MCU=%s SIM=%s (%s)" % (n, nm, ns, ALLOW_DIFF[n]))
else:
bad.append(("MISMATCH", n, "MCU=%s(%s) vs SIM=%s(%s)"
% (nm, "显式" if in_mcu else "默认", ns, "显式" if in_sim else "默认")))
if in_mcu != in_sim:
side = "MCU" if not in_mcu else "SIM"
warn.append(("IMPLICIT", n, "%s 端未显式定义,靠 lv_conf_internal 默认 = %s"
% (side, ns if not in_mcu else nm)))
known = set(defaults) | set(symbols)
for label, vals in (("MCU", mcu), ("SIM", sim)):
for n in sorted(vals):
if n in known or n in IGNORE_EXACT or n.startswith(IGNORE_PREFIX):
continue
bad.append(("INVALID", n, "%s 端定义了 LVGL 不认的宏(写了不生效)" % label))
for label, cnt in (("MCU", mcu_cnt), ("SIM", sim_cnt)):
for n in sorted(cnt):
if cnt[n] > 1:
bad.append(("DUPLICATE", n, "%s 端定义 %d 次(生效值 = 最后一次)" % (label, cnt[n])))
if a.list:
print("%-42s %-8s %-24s %-8s %s" % ("宏", "MCU 来源", "MCU 生效值", "SIM 来源", "SIM 生效值"))
for r in rows:
print("%-42s %-8s %-24s %-8s %s" % r)
print("对照 %d 项(忽略端专属宏 %d 项)" % (len(rows), len(names) - len(checked)))
for kind, n, why in warn:
print("[warn] %-40s %s" % (n, why))
if warn:
print("-> IMPLICIT 不判失败,但默认值随 LVGL 版本变;建议显式固化")
if not bad:
print("PASS: 两份 lv_conf.h 特性子集一致(生效值比对 %d 项,警告 %d 项)"
% (len(rows), len(warn)))
return 0
print("FAIL: %d 项不一致" % len(bad))
for kind, n, why in bad:
print(" [%s] %-38s %s" % (kind, n, why))
print("-> 两端必须跑同一套特性子集,否则模拟器 golden 回归验证不到固件实际编译的配置")
print(" 规则与理由见 docs/UI设计/UI_设计架构.md §1.4 第 10 条")
return 1
if __name__ == "__main__":
sys.exit(main())