Files
STM32F4-Base/tools/menu_gen/gen_menu.py
T
wandering1 508577bbf0 UI 生成文件按工程约定产出:生成器补文件头、K&R 大括号与版本守卫压平
- gen_menu.py:ui_menu_tab.h/.c 输出工程标准文件头,5 个函数定义改 K&R 大括号
- make_font.py:新增 normalize_project_conventions 后处理——工程文件头
  (替换内嵌绝对路径的 lv_font_conv 头注释)、包含收敛为 #include "lvgl.h"、
  LVGL 9.3.0 版本守卫静态压平(含守卫宏定义清理)
- 重生成 ui_font_cn_12.c / ui_menu_tab.c / ui_menu_tab.h:仅头部/大括号/守卫
  行变化,字形数据与菜单表逐字节不变
- 两文件随之满足规范检查,pre-commit 中无需再排除(配合上一提交生效)

验证:pre-commit 全钩子通过、模拟器 0 警、菜单/内容页像素回归 PASS、Keil 0 错 0 警
2026-09-13 19:38:30 +08:00

265 lines
12 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 -*-
"""生成菜单节点表 ui_menu_tab.c / ui_menu_tab.h(扁平"父子索引表",输出到 Drivers/BSP/UI/)。
把"菜单长什么样"声明在缩进文本源 ui_menu_tree.txt(缩进即层级,每级 2 空格,
`#` 开头为注释;行尾可选 ` #P` = 该叶子需密码(密码页按所输密码授权:查看/修改);
密码值在 Drivers/BSP/UI/ui_pwd_def.h 编译期配置)。
本脚本读源、推导每个节点的 parent 下标、产出 C 表,供 ui_menu.c 消费。
改菜单只需编辑 ui_menu_tree.txt 后重跑本脚本(与字体 ui_cn_chars.txt -> make_font.py 同模式)。
用法(仓库根目录或任意目录均可,脚本自动定位仓库根):
python tools/menu_gen/gen_menu.py
"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SRC = os.path.join(ROOT, 'ui_menu_tree.txt')
OUT_H = os.path.join(ROOT, 'Drivers', 'BSP', 'UI', 'ui_menu_tab.h')
OUT_C = os.path.join(ROOT, 'Drivers', 'BSP', 'UI', 'ui_menu_tab.c')
ROOT_ID = 0xFF # 内部根标记(= MENU_ROOT=0xFF);与 8 位索引一致,映射到 C 的 MENU_ROOT 宏
MAX_DEPTH = 8
MAX_NODES = 255 # uint8_t 索引 + 哨兵 0xFF → 最多 255 个节点(下标 0..254)
def parse_tree(text):
"""把缩进文本解析为 [(parent, name, depth), ...]。
parent 为父节点下标,根节点为 ROOT_ID;子项 = parent 等于本下标的所有行(按表序)。
缩进单元取"最小非零缩进"(每级一个单元),据此把缩进换算成层级。
"""
lines = []
for raw in text.splitlines():
line = raw.rstrip()
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
if '\t' in line:
sys.exit('ERROR: 请用空格缩进(勿用 Tab): ' + repr(raw))
indent = len(line) - len(line.lstrip(' '))
name, pwd = stripped, None
m = re.match(r'^(.*?)[ \t]+#([A-Za-z0-9]+)$', stripped)
if m:
if m.group(2) != 'P':
sys.exit('ERROR: 行尾注记仅支持 #P(需密码): %r(出现 #%s;旧 #V/#M 类别注记已废弃,'
'密码值改在 ui_pwd_def.h 配置)' % (raw, m.group(2)))
name, pwd = m.group(1).strip(), True
lines.append((indent, name, pwd))
if not lines:
sys.exit('ERROR: ui_menu_tree.txt 无有效节点')
indents = sorted(set(i for i, _, _ in lines if i > 0))
unit = indents[0] if indents else 1
for i, name, _ in lines:
if i % unit != 0:
sys.exit('ERROR: 缩进不是 %d 的整数倍: %r (indent=%d)' % (unit, name, i))
nodes = []
stack = [] # [(depth, index), ...] 当前各层的父节点下标
for indent, name, pwd in lines:
depth = indent // unit
if depth > MAX_DEPTH:
sys.exit('ERROR: 层级过深(>%d): %r' % (MAX_DEPTH, name))
while stack and stack[-1][0] >= depth:
stack.pop()
parent = stack[-1][1] if stack else ROOT_ID
idx = len(nodes)
nodes.append((parent, name, depth, pwd))
stack.append((depth, idx))
roots = [i for i, (_, _, d, _) in enumerate(nodes) if d == 0]
if len(roots) != 1:
sys.exit('ERROR: 必须有且仅有一个根节点(depth 0),当前 %d 个: %s'
% (len(roots), [nodes[i][1] for i in roots]))
return nodes
def emit(nodes):
n = len(nodes)
menus = sum(1 for i in range(n) if any(nodes[j][0] == i for j in range(n)))
max_depth = max(d for _, _, d, _ in nodes)
h = [
'/*',
' * 模块名称:UI 菜单节点表头文件(自动生成)',
' * 模块功能:菜单节点结构、表规模护栏与表查询 API 声明',
' * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)',
' * 作者:王建锋',
' * 创建日期:2026-09-07',
' * 修改记录:',
' * 2026-09-13 王建锋 工程约定规范化(文件头),内容由生成器自动更新',
' * 自动生成:本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 自动生成,请勿手改。',
' * 修改菜单:编辑 ui_menu_tree.txt 后重跑 `python tools/menu_gen/gen_menu.py`。',
' */',
'#ifndef UI_MENU_TAB_H',
'#define UI_MENU_TAB_H',
'',
'#include <stdint.h>',
'#include <stdbool.h>',
'',
'#define MENU_ROOT 0xFFu /* 根哨兵:仅根节点(主界面)的 parent 取此值;8 位,上限 255 项 */',
'',
'/* 菜单节点:子项 = parent 等于本下标的所有行(按表序);有子项=菜单,无=叶子 */',
'/* 字段序 parent/pwd 在前:32 位下两项 8 位合并 + 4 位指针 = 8 B/项 */',
'typedef struct {',
' uint8_t parent; /* 父节点下标(0..254);根为 MENU_ROOT(0xFF) */',
' uint8_t pwd; /* 1 = 需密码(密码页按所输密码授权);0 = 直接进(只读) */',
' const char *name; /* 显示文本(字面量,存 Flash) */',
'} ui_menu_node_t;',
'',
'#define UI_MENU_TAB_COUNT %d' % n,
'',
'/* 护栏:8 位索引上限 255 项(哨兵 0xFF 占用 255);超界则编译失败(C99 写法,兼容 ARMCC v5) */',
'typedef char menu_tab_count_check[(UI_MENU_TAB_COUNT <= 255) ? 1 : -1];',
'',
'extern const ui_menu_node_t g_menu_tab[];',
'',
'/*',
' * 表查询 API(导航逻辑层 ui_menu.c 与显示层 ui_menu_view.c 共用):',
' * 实现随表生成,改菜单(ui_menu_tree.txt)调用方零代码改动。',
' */',
'uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max); /* 取 node 直接子项(按表序),最多 max 个;返回实际个数 */',
'bool ui_menu_tab_has_children(uint8_t node); /* 有子项=菜单(带▶);无=叶子 */',
'const char *ui_menu_tab_name(uint8_t node); /* 显示名 */',
'bool ui_menu_tab_has_pwd(uint8_t node); /* 需密码(密码页按所输密码授权);false = 直接进(只读) */',
'bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效、pwd 为 0/1 */',
'',
'#endif',
'',
]
c = [
'/*',
' * 模块名称:UI 菜单节点表(自动生成)',
' * 模块功能:菜单节点扁平父子索引表 + 5 个表查询 API(导航逻辑层与显示层共用)',
' * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)',
' * 作者:王建锋',
' * 创建日期:2026-09-07',
' * 修改记录:',
' * 2026-09-13 王建锋 工程约定规范化(文件头 / K&R 大括号),内容由生成器自动更新',
' * 自动生成:本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 自动生成,请勿手改。',
' * 修改菜单:编辑 ui_menu_tree.txt 后重跑 `python tools/menu_gen/gen_menu.py`。',
' */',
'#include "ui_menu_tab.h"',
'',
'const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {',
]
for i, (parent, name, depth, pwd) in enumerate(nodes):
pstr = 'MENU_ROOT' if parent == ROOT_ID else '%d' % parent
cstr = '1u' if pwd else '0u'
c.append(' { %8s, %2s, "%s" }, /* %2d L%d */' % (pstr, cstr, name, i, depth))
c.append('};')
c.append('')
c += [
'/*',
' * 函数功能:取 node 的直接子项(按表序),最多 max 个',
' * 入口参数:node - 节点下标 uint8_t',
' * out - 子节点下标输出缓冲 uint8_t *',
' * max - 缓冲容量 uint8_t',
' * 返回值:实际子项个数 uint8_t',
' * 限定条件:node < UI_MENU_TAB_COUNT',
' * 函数说明:子项 = 表中所有 parent == 本下标的行(表序即显示序);建页核心查询',
' */',
'uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max) {',
' uint8_t n = 0u;',
' for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT && n < max; i++) {',
' if (g_menu_tab[i].parent == node) {',
' out[n++] = i;',
' }',
' }',
' return n;',
'}',
'',
'/*',
' * 函数功能:判断 node 有无子项',
' * 入口参数:node - 节点下标 uint8_t',
' * 返回值:有子项返回 true(=菜单、行带▶);无子项返回 false(=叶子)',
' * 限定条件:node < UI_MENU_TAB_COUNT',
' * 函数说明:菜单/叶子由"是否有子项"自动推导,无单独类型字段',
' */',
'bool ui_menu_tab_has_children(uint8_t node) {',
' for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT; i++) {',
' if (g_menu_tab[i].parent == node) {',
' return true;',
' }',
' }',
' return false;',
'}',
'',
'/*',
' * 函数功能:取节点显示名',
' * 入口参数:node - 节点下标 uint8_t',
' * 返回值:显示文本(Flash 字面量) const char *',
' * 限定条件:node < UI_MENU_TAB_COUNT',
' * 函数说明:行文本一律经此取静态字符串,不 strdup',
' */',
'const char *ui_menu_tab_name(uint8_t node) {',
' return g_menu_tab[node].name;',
'}',
'',
'/*',
' * 函数功能:判断节点是否需密码(叶子密码保护)',
' * 入口参数:node - 节点下标 uint8_t',
' * 返回值:true - 需密码页(按所输密码授权查看/修改权限);false - 直接进(只读)',
' * 限定条件:无',
' * 说明:表 pwd 列取自源 ui_menu_tree.txt 行尾 #P 注记;导航逻辑层据此决定是否先经密码页',
' */',
'bool ui_menu_tab_has_pwd(uint8_t node) {',
' return (g_menu_tab[node].pwd != 0u);',
'}',
'',
'/*',
' * 函数功能:菜单表启动自检',
' * 入口参数:无',
' * 返回值:true - 表有效;false - 存在越界 parent(表内笔误)',
' * 限定条件:无',
' * 函数说明:每个非根节点的 parent 必须为 MENU_ROOT 或有效下标;在 ui_menu_init 调用',
' */',
'bool ui_menu_tab_valid(void) {',
' for (uint8_t i = 0u; i < UI_MENU_TAB_COUNT; i++) {',
' if (i == 0u) {',
' continue; /* 根 */',
' }',
' uint8_t p = g_menu_tab[i].parent;',
' if (p != MENU_ROOT && p >= UI_MENU_TAB_COUNT) {',
' return false;',
' }',
' if (g_menu_tab[i].pwd > 1u) {',
' return false; /* pwd 只取 0/1(行尾注记笔误捕获) */',
' }',
' }',
' return true;',
'}',
'',
]
return '\n'.join(h), '\n'.join(c), menus, max_depth
def main():
if not os.path.exists(SRC):
sys.exit('ERROR: 未找到 ui_menu_tree.txt(应在仓库根目录)')
text = open(SRC, encoding='utf-8-sig').read()
nodes = parse_tree(text)
if len(nodes) > MAX_NODES:
sys.exit('ERROR: 节点数 %d 超过 uint8_t 索引上限 %d(哨兵 MENU_ROOT=0xFF 占用 255);请精简菜单或改回 16 位索引' % (len(nodes), MAX_NODES))
header, body, menus, max_depth = emit(nodes)
# Drivers/BSP/UI/* 约定为 UTF-8 带 BOM(同 ui_menu.c);字符串字面量含中文,
# 无 BOM 时 cl.exe(代码页 936) 会按 GBK 误读 UTF-8 字节导致字符串解析错乱。
with open(OUT_H, 'w', encoding='utf-8-sig', newline='') as fp:
fp.write(header)
with open(OUT_C, 'w', encoding='utf-8-sig', newline='') as fp:
fp.write(body)
protected = sum(1 for _, _, _, p in nodes if p)
print('nodes=%d menu=%d leaf=%d protected=%d max_depth=%d'
% (len(nodes), menus, len(nodes) - menus, protected, max_depth))
print('->', os.path.relpath(OUT_H, ROOT))
print('->', os.path.relpath(OUT_C, ROOT))
if __name__ == '__main__':
main()