#!/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 ', '#include ', '', '#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()