增加网络配置界面
This commit is contained in:
@@ -127,6 +127,132 @@ def fix_trailing_newline(path):
|
||||
fp.write(fixed)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 低码位符号补回(与箭头字形合并同一模式)
|
||||
# lv_font_conv 的稀疏 cmap 从符号集中最小码位起算,低于该起点的符号被静默丢弃
|
||||
# (实测:U+3001 全角逗号 < 0x4E2D 范围起点,未进字体 → 页脚","长期渲染为空白)。
|
||||
# 处理:对每个此类符号单独生成迷你字体(真实宋体位图),再并入主字体
|
||||
# (位图 + glyph_dsc + 单码位 cmap,幂等)。
|
||||
# ---------------------------------------------------------------------------
|
||||
EXTRA_SYMBOLS = [','] # U+3001 全角逗号(页脚"第 X 页,共 Y 页")
|
||||
|
||||
|
||||
def merge_extra_symbols(font_path, size, bpp):
|
||||
"""把 lv_font_conv 丢失的低码位符号从迷你字体并入主字体(逐符号、幂等)。"""
|
||||
import re
|
||||
for ch in EXTRA_SYMBOLS:
|
||||
cp = ord(ch)
|
||||
raw = open(font_path, 'rb').read()
|
||||
has_bom = raw.startswith(b'\xef\xbb\xbf')
|
||||
src = raw.decode('utf-8-sig')
|
||||
if re.search(r'\.range_start\s*=\s*%d\s*,' % cp, src):
|
||||
print('U+%04X %r 已存在,跳过合并' % (cp, ch))
|
||||
continue
|
||||
|
||||
# 1) 单独生成仅含该符号的迷你字体(真实宋体位图/度量)
|
||||
tmp = os.path.join(tempfile.gettempdir(), 'font_extra_%04x.c' % cp)
|
||||
cmd = resolve_lv_font_conv() + ['--font', resolve_font(None),
|
||||
'--size', str(size), '--bpp', str(bpp),
|
||||
'--symbols', ch, '--no-compress',
|
||||
'--format', 'lvgl', '--lv-font-name', 'tmp_extra',
|
||||
'-o', tmp]
|
||||
r = subprocess.run(cmd)
|
||||
if r.returncode != 0:
|
||||
sys.exit('ERROR: 生成 U+%04X 迷你字体失败' % cp)
|
||||
mini = open(tmp, encoding='utf-8-sig').read()
|
||||
|
||||
# 2) 从迷你字体提取位图字节 / glyph_dsc 字段 / 码位
|
||||
bm = re.search(r'glyph_bitmap\[\]\s*=\s*\{', mini)
|
||||
end_bm = mini.find('\n};', bm.end())
|
||||
body_bm = re.sub(r'/\*.*?\*/', '', mini[bm.end():end_bm], flags=re.S)
|
||||
bytes_ = re.findall(r'0x[0-9a-fA-F]+', body_bm)
|
||||
gd = re.search(r'glyph_dsc\[\]\s*=\s*\{', mini)
|
||||
end_gd = mini.find('\n};', gd.end())
|
||||
entries = re.findall(r'\{\.bitmap_index = (\d+), \.adv_w = (\d+), \.box_w = (\d+), '
|
||||
r'\.box_h = (\d+), \.ofs_x = (-?\d+), \.ofs_y = (-?\d+)\}',
|
||||
mini[gd.end():end_gd])
|
||||
if len(entries) < 2:
|
||||
sys.exit('ERROR: 无法解析 U+%04X 迷你字体 glyph_dsc' % cp)
|
||||
# 取第 1 项(下标 0 是保留空字形)
|
||||
bmp_ofs, adv, bw, bh, ox, oy = (int(v) for v in entries[1])
|
||||
cm = re.search(r'cmaps\[\]\s*=\s*\{', mini)
|
||||
m = re.search(r'\.range_start\s*=\s*(\d+)\s*,\s*\.range_length\s*=\s*(\d+)',
|
||||
mini[cm.end():])
|
||||
if not m or int(m.group(2)) != 1:
|
||||
sys.exit('ERROR: U+%04X 迷你字体 cmap 非单码位,无法并入' % cp)
|
||||
sym_cp, sym_len = int(m.group(1)), 1
|
||||
|
||||
# 3) 并入主字体:位图追加(偏移平移)+ glyph_dsc 追加 + 单码位 cmap 追加
|
||||
body = re.search(r'glyph_bitmap\[\]\s*=\s*\{', src)
|
||||
end_main_bm = src.find('\n};', body.end())
|
||||
main_bytes = len(re.findall(r'0x[0-9a-fA-F]+',
|
||||
re.sub(r'/\*.*?\*/', '', src[body.end():end_main_bm], flags=re.S)))
|
||||
glyph_count = len(re.findall(r'\.bitmap_index\s*=\s*\d+', src))
|
||||
|
||||
bmp = ',\n\n /* U+%04X %r(来源:宋体迷你字体并入;lv_font_conv 丢低码位符号) */\n ' % (cp, ch) \
|
||||
+ ', '.join(bytes_)
|
||||
src = src[:end_main_bm] + bmp + src[end_main_bm:]
|
||||
|
||||
gd = re.search(r'glyph_dsc\[\]\s*=\s*\{', src)
|
||||
end_main_gd = src.find('\n};', gd.end())
|
||||
entry = ',\n {.bitmap_index = %d, .adv_w = %d, .box_w = %d, .box_h = %d, ' \
|
||||
'.ofs_x = %d, .ofs_y = %d} /* U+%04X */' % (
|
||||
main_bytes + bmp_ofs, adv, bw, bh, ox, oy, cp)
|
||||
src = src[:end_main_gd] + entry + src[end_main_gd:]
|
||||
|
||||
cm = re.search(r'cmaps\[\]\s*=\s*\{', src)
|
||||
end_main_cm = src.find('\n};', cm.end())
|
||||
cmap = ',\n {\n .range_start = %d, .range_length = %d, ' \
|
||||
'.glyph_id_start = %d,\n' \
|
||||
' .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, ' \
|
||||
'.type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY\n }' % (sym_cp, sym_len, glyph_count)
|
||||
src = src[:end_main_cm] + cmap + src[end_main_cm:]
|
||||
|
||||
src = re.sub(r'\.cmap_num = (\d+),',
|
||||
lambda m: '.cmap_num = %d,' % (int(m.group(1)) + 1), src, count=1)
|
||||
|
||||
out = b'\xef\xbb\xbf' if has_bom else b''
|
||||
open(font_path, 'wb').write(out + src.encode('utf-8'))
|
||||
print('已合并 U+%04X %r:%d 字节位图,glyph_id=%d' % (cp, ch, len(bytes_), glyph_count))
|
||||
|
||||
|
||||
def verify_coverage(font_path):
|
||||
"""生成后校验:ui_cn_chars.txt 的全部码位必须进字体 cmap(防静默丢字)。
|
||||
|
||||
lv_font_conv 对某些码位会静默丢弃(低于稀疏范围起点、或源字体缺字形),
|
||||
静默缺字在 UI 上表现为空白,极难发现。此校验把"缺字"变成生成期硬错误。
|
||||
须在 merge_arrow_glyph / merge_extra_symbols 之后调用(合并的符号也要计入)。
|
||||
"""
|
||||
import re
|
||||
src = open(font_path, encoding='utf-8-sig').read()
|
||||
# 稀疏 cmap 的 unicode_list_N:十六进制偏移(相对 range_start)
|
||||
ulists = {}
|
||||
for m in re.finditer(r'static const uint16_t (unicode_list_\d+)\[\] = \{(.*?)\};', src, re.S):
|
||||
ulists[m.group(1)] = [int(x, 16) for x in re.findall(r'0x[0-9a-fA-F]+', m.group(2))]
|
||||
cmaps = [] # (range_start, range_length, sparse_offsets_or_None)
|
||||
for m in re.finditer(r'\.range_start = (\d+), \.range_length = (\d+), \.glyph_id_start = (\d+),\s*'
|
||||
r'\.unicode_list = ([^,]+), \.glyph_id_ofs_list = ([^,]+), \.list_length = (\d+), '
|
||||
r'\.type = (\w+)', src):
|
||||
ul = m.group(4).strip()
|
||||
cmaps.append((int(m.group(1)), int(m.group(2)), ulists.get(ul)))
|
||||
|
||||
def covered(cp):
|
||||
for rs, rl, offs in cmaps:
|
||||
if offs is None:
|
||||
if rs <= cp < rs + rl:
|
||||
return True
|
||||
elif (cp - rs) in offs:
|
||||
return True
|
||||
return False
|
||||
|
||||
chars = sorted(set(open(CHARS_FILE, encoding='utf-8').read().strip()))
|
||||
missing = [c for c in chars if not covered(ord(c))]
|
||||
if missing:
|
||||
sys.exit('ERROR: 字体缺字 %d 个(UI 将显示空白): %s'
|
||||
% (len(missing), ' '.join('U+%04X' % ord(c) for c in missing)))
|
||||
print('coverage OK: %d 个字符全部进字体' % len(chars))
|
||||
|
||||
|
||||
def main():
|
||||
# Windows GBK 控制台无法打印 ▶ 等符号:降级为替换字符,避免 UnicodeEncodeError 崩溃
|
||||
try:
|
||||
@@ -161,6 +287,8 @@ def main():
|
||||
if r.returncode != 0:
|
||||
sys.exit('FAILED rc=%d' % r.returncode)
|
||||
merge_arrow_glyph(OUT) # 补回宋体缺失的 ▶ 字形(幂等)
|
||||
merge_extra_symbols(OUT, args.size, args.bpp) # 补回 lv_font_conv 丢弃的低码位符号(幂等)
|
||||
verify_coverage(OUT) # 生成后校验:字符集全部码位必须进字体(防静默丢字)
|
||||
fix_trailing_newline(OUT) # 规范化末尾换行(pre-commit end-of-file-fixer)
|
||||
print('OK %d bytes -> %s' % (os.path.getsize(OUT), OUT))
|
||||
|
||||
|
||||
+43
-12
@@ -3,13 +3,16 @@
|
||||
"""生成菜单节点表 ui_menu_tab.c / ui_menu_tab.h(扁平"父子索引表",输出到 Drivers/BSP/UI/)。
|
||||
|
||||
把"菜单长什么样"声明在缩进文本源 ui_menu_tree.txt(缩进即层级,每级 2 空格,
|
||||
`#` 开头为注释)。本脚本读源、推导每个节点的 parent 下标、产出 C 表,供 ui_menu.c 消费。
|
||||
`#` 开头为注释;行尾可选 ` #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__))))
|
||||
@@ -36,20 +39,27 @@ def parse_tree(text):
|
||||
if '\t' in line:
|
||||
sys.exit('ERROR: 请用空格缩进(勿用 Tab): ' + repr(raw))
|
||||
indent = len(line) - len(line.lstrip(' '))
|
||||
lines.append((indent, stripped))
|
||||
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))
|
||||
indents = sorted(set(i for i, _, _ in lines if i > 0))
|
||||
unit = indents[0] if indents else 1
|
||||
for i, name in lines:
|
||||
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 in lines:
|
||||
for indent, name, pwd in lines:
|
||||
depth = indent // unit
|
||||
if depth > MAX_DEPTH:
|
||||
sys.exit('ERROR: 层级过深(>%d): %r' % (MAX_DEPTH, name))
|
||||
@@ -57,10 +67,10 @@ def parse_tree(text):
|
||||
stack.pop()
|
||||
parent = stack[-1][1] if stack else ROOT_ID
|
||||
idx = len(nodes)
|
||||
nodes.append((parent, name, depth))
|
||||
nodes.append((parent, name, depth, pwd))
|
||||
stack.append((depth, idx))
|
||||
|
||||
roots = [i for i, (_, _, d) in enumerate(nodes) if d == 0]
|
||||
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]))
|
||||
@@ -70,7 +80,7 @@ def parse_tree(text):
|
||||
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)
|
||||
max_depth = max(d for _, _, d, _ in nodes)
|
||||
|
||||
h = [
|
||||
'/* 本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 自动生成,请勿手改。',
|
||||
@@ -85,8 +95,10 @@ def emit(nodes):
|
||||
'#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;',
|
||||
'',
|
||||
@@ -104,7 +116,8 @@ def emit(nodes):
|
||||
'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_valid(void); /* 启动自检:非根 parent 下标均有效 */',
|
||||
'bool ui_menu_tab_has_pwd(uint8_t node); /* 需密码(密码页按所输密码授权);false = 直接进(只读) */',
|
||||
'bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效、pwd 为 0/1 */',
|
||||
'',
|
||||
'#endif',
|
||||
'',
|
||||
@@ -118,9 +131,10 @@ def emit(nodes):
|
||||
'',
|
||||
'const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {',
|
||||
]
|
||||
for i, (parent, name, depth) in enumerate(nodes):
|
||||
for i, (parent, name, depth, pwd) in enumerate(nodes):
|
||||
pstr = 'MENU_ROOT' if parent == ROOT_ID else '%d' % parent
|
||||
c.append(' { %8s, "%s" }, /* %2d L%d */' % (pstr, name, i, depth))
|
||||
cstr = '1u' if pwd else '0u'
|
||||
c.append(' { %8s, %2s, "%s" }, /* %2d L%d */' % (pstr, cstr, name, i, depth))
|
||||
c.append('};')
|
||||
c.append('')
|
||||
c += [
|
||||
@@ -174,6 +188,18 @@ def emit(nodes):
|
||||
'}',
|
||||
'',
|
||||
'/*',
|
||||
' * 函数功能:判断节点是否需密码(叶子密码保护)',
|
||||
' * 入口参数: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(表内笔误)',
|
||||
@@ -190,6 +216,9 @@ def emit(nodes):
|
||||
' 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;',
|
||||
'}',
|
||||
@@ -213,7 +242,9 @@ def main():
|
||||
fp.write(header)
|
||||
with open(OUT_C, 'w', encoding='utf-8-sig', newline='') as fp:
|
||||
fp.write(body)
|
||||
print('nodes=%d menu=%d leaf=%d max_depth=%d' % (len(nodes), menus, len(nodes) - menus, max_depth))
|
||||
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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user