diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa60d851..182a2fb8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -478,3 +478,23 @@ repos: language: system files: ^(App/|Drivers/BSP/|Src/|Inc/|Lib/lv_conf\.h|test/board/|scripts/ram_baseline\.|docs/内存管理/|\.pre-commit-config\.yaml$) pass_filenames: false + + # 11) UI 字符集差集检查(2026-10-15 加,报告 §6.5 U4 残余 / §7 交付物清单"字符集差集检查"行) + # 背景:`ui_font_cn_12.c` 是**子集**字体,只含 `ui_cn_chars.txt` 列出的码位。 + # 生成链 UI 源码 --extract_cn--> ui_cn_chars.txt --make_font--> 字体 .c,其中 + # verify_coverage 只在**生成期**校验"txt ⊆ cmap";改完 UI 字符串忘了重跑 + # font-regen 就提交 → 缺字上机渲染为空白(白框),无日志无报错只能肉眼截图发现 + # (font-regen skill 的"缺字渲染为空白"约定,此前纯靠自律)。本钩子把它机械化: + # A. 源码在用 vs txt 缺 → FAIL(白框方向);多 → WARN(只费 Flash,--strict 升级) + # B. txt vs 字体 cmap 缺 → FAIL(lv_font_conv 对缺字形/低码位**静默丢字**) + # A 过 + B 过 ⟹ 字体 ⊇ 源码每个在用字。提取与 cmap 解析**复用** tools/font_gen + # 两个生成器的同一套规则(importlib 直载 extract_cn.SCAN/collect + make_font. + # verify_coverage),杜绝第二套解析规则漂移(§5.5 同类教训)。 + # ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖全部触发面: + # UI 源码(含字体 .c 自身)+ ui_task.{c,h} + 字符集 txt + 两个生成器 + 本脚本。 + - id: font-charset + name: UI 字符集差集检查(防缺字白框) + entry: python scripts/check_font_charset.py + language: system + files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|ui_cn_chars\.txt$|tools/font_gen/|scripts/check_font_charset\.py$) + pass_filenames: false diff --git a/AGENTS.md b/AGENTS.md index f1e8ca7f..752a2de5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,7 +90,7 @@ ## 提交门禁(pre-commit) -- 任一失败即拦截,共 **34 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验,N-23)+ `mem-budget`(RAM 92% 预算门禁,N-27) +- 任一失败即拦截,共 **35 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 16 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4) - **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5) - ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关 - **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/` @@ -173,7 +173,7 @@ | ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` | | 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` | | RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` | -| pre-commit 34 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` | +| pre-commit 35 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` | | 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` | | 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill | | 烧录与复位语义 | `docs/测试说明/ST-LINK_CLI_复位使用说明.md` | diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 6e6d1d60..b6e030af 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -30,6 +30,7 @@ #include "ui_menu.h" #include "ui_net_sink.h" #include "ui_standby.h" +#include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */ #include "sys_clock.h" #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */ @@ -108,7 +109,7 @@ void ui_task_func(void *argument) { for (;;) { osDelay(1000); } } DBG_INFO("UI menu init: OK"); - if (ui_standby_init(60000u, ui_task_time_cb) != 0) { + if (ui_standby_init(UI_STANDBY_IDLE_MS, ui_task_time_cb) != 0) { DBG_ERROR("UI standby init: FAIL"); } else { DBG_INFO("UI standby: ready (idle=60s)"); @@ -149,7 +150,7 @@ void ui_task_func(void *argument) { lat_mark(&tm_loop); tm_loop_valid = 1U; #endif - ui_port_step(10); + ui_port_step(UI_TASK_TICK_MS); /* 心跳**先于**推流往返发出(P1-4a,看门狗设计说明 §11.3):UI 心跳测的是 * "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后, * 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后, @@ -167,6 +168,6 @@ void ui_task_func(void *argument) { /* 单轮工作耗时(不含 osDelay):与循环周期对比即得"调度延迟" */ task_prof_sample(TASK_PROF_UI_WORK, task_prof_cyc() - prof_t0); - osDelay(10); + osDelay(UI_TASK_TICK_MS); } } diff --git a/Drivers/BSP/UI/ui_content_page.c b/Drivers/BSP/UI/ui_content_page.c index adf7e7f0..7625bd58 100644 --- a/Drivers/BSP/UI/ui_content_page.c +++ b/Drivers/BSP/UI/ui_content_page.c @@ -4,7 +4,9 @@ * 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码),无子对象; * 只持机制:屏/容器按需建毁、焦点、按键分发、描述符驱动通用绘制; * 不持任何页面数据——各页数据/几何/钩子由 ui_page_xxx 模块经 - * content_page_def_t 接入描述符表 s_defs[](架构见 内容页架构.md); + * content_page_def_t 提供,框架按**描述符指针**开页;"哪个叶子进哪一页" + * 的绑定在生成物 ui_page_registry.c(唯一真源 = ui_menu_tree.txt 的 + * #page= 注记;架构见 内容页架构.md); * 进入时切屏并获焦、ESC 返回菜单屏并还原焦点; * 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) @@ -26,6 +28,12 @@ * 描述符 init_cb(开页后调一次,页面模块重置运行态) * 2026-09-13 王建锋 cp_draw_cb 按区块拆为四个绘制子函数(cp_draw_deco/header/rows/footer), * 主函数仅编排(行数取一次供行窗口/页脚共用),行为零变化 + * 2026-10-15 王建锋 U1 设计令牌层:屏幕/骨架/行距/缓冲/1bpp 语义色字面量收进 + * ui_tokens.h(纯替换,像素逐位一致) + * 2026-10-15 王建锋 U3 页面原型化:删 content_page_id_t 枚举与 s_defs[],open() 改收 + * 描述符指针、s_open 变指针(NULL = 未打开);7 个页面头包含移交 + * ui_page_registry.c;日志页身份改打描述符 id(ASCII 页名,比原 + * id=%u 更可定位);绘制/按键/几何一律未动 */ /* 头文件包含区 */ @@ -33,20 +41,6 @@ #include "ui_content_page.h" -#include "ui_page_device_info.h" - -#include "ui_page_ac.h" - -#include "ui_page_dc.h" - -#include "ui_page_password.h" - -#include "ui_page_net_cfg.h" - -#include "ui_page_runlog.h" - -#include "ui_page_selftest.h" - #include "ui_standby.h" #include "ui_pwd_def.h" @@ -55,6 +49,10 @@ #include "ui_deco.h" +#include "ui_style.h" + +#include "ui_tokens.h" + #include "lvgl.h" #define DBG_TAG "[UI_CONTENT_PAGE]" @@ -63,57 +61,32 @@ /* 私有宏定义区 */ /* - * 布局常量(设备像素;参考图 10/20/21 实测,3x 放大图缩回 160x160 测量): - * 标题 y=2(CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143; - * 字形顶距(CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐 + * 布局取值全部来自 ui_tokens.h(L0 设计令牌层,本文件不再自带同值字面量): + * 屏幕 UI_SCREEN_W/H、标题 UI_TITLE_Y、页脚 UI_PAGE_LINE_Y、文本条带 + * UI_TEXT_BAND_H、行缓冲 UI_BUF_STRLEN、选中带内缩 UI_SEL_BAND_INSET_L/R、 + * 页脚左起 UI_FOOTER_X、语义色 UI_COL_FG/BG/SEL_FG/SEL_BG。 + * 几何实测来源仍是参考图 10/20/21(3x 放大图缩回 160x160 测量):标题 y=2 + * (CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143;字形顶距 + * (CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐 */ -#define CP_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ -#define CP_SCREEN_H 160 /* 屏幕高(px) */ - -#define CP_TITLE_Y 2 /* 标题放置 y(居中;墨迹自 y=3 起) */ -#define CP_PAGE_LINE_Y 143 /* 页脚"第 X 页,共 Y 页"放置 y(x=6 左对齐) */ - -#define CP_ROW_BUF_LEN 32 /* 动态行栈缓冲长度(同帧同步光栅化;超出行带即被裁切) */ /* 静态变量定义区 */ /* - * 描述符表:页 ID → 页面模块描述符(每页一行;新增页 = 枚举值 + 此处一行, - * 框架机制不变,见 内容页架构.md §七) + * 当前打开的页:直接持页面描述符指针(NULL = 未打开)。 + * 页身份由调用方给出(逻辑层经 ui_page_registry_for / _password 取描述符), + * 框架不再维护"页 ID 枚举 ↔ s_defs 下标 ↔ 菜单节点"三处必须手工对齐的映射 + * ——那正是旧 menu_content_page_for() 手写 case 的错源(表序一变即错位)。 */ -static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = { - [CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc, - [CONTENT_PAGE_AC] = &ui_page_ac_desc, - [CONTENT_PAGE_DC] = &ui_page_dc_desc, - [CONTENT_PAGE_PASSWORD] = &ui_page_password_desc, - [CONTENT_PAGE_NET_CFG] = &ui_page_net_cfg_desc, - [CONTENT_PAGE_RUN_LOG] = &ui_page_runlog_desc, - [CONTENT_PAGE_SELFTEST_LOG] = &ui_page_selftest_desc, -}; - -static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */ +static const content_page_def_t *s_open = NULL; static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续) */ static uint8_t s_perm; /* 本次授予权限(ui_perm_t;开页时装载,关页重置) */ static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */ static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */ -/* flat:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的 card 样式(同 ui_menu_view.c) */ -/* 用常量样式(LV_STYLE_CONST_INIT):属性存静态数组,零 LV_MEM 池分配; - * 动态样式首次 set_prop 会从池分配 values_and_props 且永不释放(x64 实测 88B/个) */ -static const lv_style_const_prop_t cp_flat_props[] = { - { .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } }, - { .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } }, - { .prop = LV_STYLE_RADIUS, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } }, - LV_STYLE_CONST_PROPS_END -}; - -LV_STYLE_CONST_INIT(cp_style_flat, cp_flat_props); +/* 容器样式:flat(透明 + 无边框/圆角/内边距)取自共享样式表 ui_style.c—— + * 与 ui_menu_view / ui_standby 共用同一 const 对象(原先三处各定义一份逐字相同 + * 的 9 项属性数组);常量样式零 LV_MEM 池分配的纪律见 ui_style.h */ /* 函数定义区 */ @@ -175,18 +148,18 @@ static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y lv_draw_label_dsc_t ld; area.x1 = 0; - area.x2 = CP_SCREEN_W - 1; + area.x2 = UI_SCREEN_W - 1; area.y1 = y; - area.y2 = y + 15; /* 覆盖字体行高(13)+ 余量 */ + area.y2 = y + UI_TEXT_BAND_H; /* 覆盖字体行高(13)+ 余量 */ lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = text; ld.text_static = 1; /* Flash 字符串:不拷内存 */ - ld.color = lv_color_white(); + ld.color = UI_COL_FG; /* 本 LVGL 版 lv_text_get_width:length=0 表示"不测量"(返回 0); * length≠0 时循环无 NUL 检查,传 -1 会越串尾死循环——必须传 strlen */ - ld.ofs_x = (CP_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2; + ld.ofs_x = (UI_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2; ld.ofs_y = 0; lv_draw_label(layer, &ld, &area); } @@ -211,7 +184,7 @@ static void cp_draw_deco(lv_layer_t *layer, const content_page_def_t *d) { * d - 页面描述符 const content_page_def_t * * 返回值:无 * 限定条件:页面已打开 - * 函数说明:标题放置 y 每页可配(title_y,0 = 框架默认 CP_TITLE_Y); + * 函数说明:标题放置 y 每页可配(title_y,0 = 框架默认 UI_TITLE_Y); * 副标题仅信息页(subtitle 非空);重绘区不相交则跳过区块 */ static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) { @@ -219,12 +192,12 @@ static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) { /* 标题(居中;放置 y 每页可配,0 = 框架默认) */ { - int32_t ty = (d->title_y != 0) ? d->title_y : CP_TITLE_Y; + int32_t ty = (d->title_y != 0) ? d->title_y : UI_TITLE_Y; t.x1 = 0; - t.x2 = CP_SCREEN_W - 1; + t.x2 = UI_SCREEN_W - 1; t.y1 = ty; - t.y2 = ty + 15; + t.y2 = ty + UI_TEXT_BAND_H; if (cp_areas_intersect(&t, &layer->_clip_area)) { cp_draw_centered_text(layer, d->title, ty); } @@ -233,9 +206,9 @@ static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) { /* 副标题(仅信息页) */ if (d->subtitle != NULL) { t.x1 = 0; - t.x2 = CP_SCREEN_W - 1; + t.x2 = UI_SCREEN_W - 1; t.y1 = d->subtitle_y; - t.y2 = d->subtitle_y + 15; + t.y2 = d->subtitle_y + UI_TEXT_BAND_H; if (cp_areas_intersect(&t, &layer->_clip_area)) { cp_draw_centered_text(layer, d->subtitle, d->subtitle_y); } @@ -268,19 +241,19 @@ static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t } bool dynamic = (d->refresh != NULL); - char dyn_name[CP_ROW_BUF_LEN]; /* 动态行栈缓冲(同帧同步光栅化) */ - char dyn_value[CP_ROW_BUF_LEN]; + char dyn_name[UI_BUF_STRLEN]; /* 动态行栈缓冲(同帧同步光栅化) */ + char dyn_value[UI_BUF_STRLEN]; for (int32_t i = 0; i < cnt; i++) { int32_t row_idx = first + i; /* 数据下标(跨页连续) */ int32_t slot_y = d->top_y + i * d->pitch; /* 行槽顶(窗口内位置固定) */ bool is_sel = (d->selectable != 0u) && (row_idx == (int32_t)s_sel); - lv_color_t color = is_sel ? lv_color_black() : lv_color_white(); + lv_color_t color = is_sel ? UI_COL_SEL_FG : UI_COL_FG; lv_area_t strip; /* 行条带(整宽)不在重绘区:跳过(culling) */ strip.x1 = 0; - strip.x2 = CP_SCREEN_W - 1; + strip.x2 = UI_SCREEN_W - 1; strip.y1 = slot_y; strip.y2 = slot_y + d->pitch - 1; if (!cp_areas_intersect(&strip, &layer->_clip_area)) { @@ -292,13 +265,13 @@ static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t lv_draw_rect_dsc_t fd; lv_area_t band; - band.x1 = 2; - band.x2 = CP_SCREEN_W - 3; /* x=156(与参考图实测一致) */ + band.x1 = UI_SEL_BAND_INSET_L; + band.x2 = UI_SCREEN_W - UI_SEL_BAND_INSET_R; /* x=156(与参考图实测一致) */ band.y1 = slot_y; band.y2 = slot_y + d->band_h - 1; lv_draw_rect_dsc_init(&fd); fd.base.layer = layer; - fd.bg_color = lv_color_white(); + fd.bg_color = UI_COL_SEL_BG; fd.bg_opa = LV_OPA_COVER; lv_draw_rect(layer, &fd, &band); } @@ -353,10 +326,10 @@ static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t * (hide_footer;密码页跟随实机) */ static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) { - char buf[32]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */ + char buf[UI_BUF_STRLEN]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */ uint8_t cur; /* 当前页码(1 基) */ uint8_t pages; /* 总页数 */ - lv_area_t t; /* 页脚条带区域(整宽,放置于 CP_PAGE_LINE_Y) */ + lv_area_t t; /* 页脚条带区域(整宽,放置于 UI_PAGE_LINE_Y) */ /* 页码计算,两种来源: * - 列表页(selectable):随选中行现算——当前页 = 选中行/每页行数 + 1 @@ -373,12 +346,12 @@ static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32 pages = d->fixed_total_pages; } - /* 页脚条带:整宽,放置于 CP_PAGE_LINE_Y(143,字形顶距已计入放置 y, + /* 页脚条带:整宽,放置于 UI_PAGE_LINE_Y(143,字形顶距已计入放置 y, * 墨迹与参考图对齐);高 15 = 字体行高 13 + 余量,整行文字一次覆盖 */ t.x1 = 0; - t.x2 = CP_SCREEN_W - 1; - t.y1 = CP_PAGE_LINE_Y; - t.y2 = CP_PAGE_LINE_Y + 15; + t.x2 = UI_SCREEN_W - 1; + t.y1 = UI_PAGE_LINE_Y; + t.y2 = UI_PAGE_LINE_Y + UI_TEXT_BAND_H; /* 双重门控:① 本页开启页脚(hide_footer;密码页跟随实机:无页码) * ② 页脚条带与重绘区相交(culling)——两者皆成立才格式化并绘制 */ @@ -388,13 +361,13 @@ static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32 lv_draw_label_dsc_t ld; - /* ofs 相对 coords 原点(t.x1=0,t.y1=CP_PAGE_LINE_Y): + /* ofs 相对 coords 原点(t.x1=0,t.y1=UI_PAGE_LINE_Y): * 水平 6 = 左对齐、左留白 6px(与实机一致);垂直 0(顶距已计入放置 y) */ lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = buf; /* 栈缓冲:本页码每次现算,同帧内有效(同步光栅化) */ - ld.color = lv_color_white(); /* 页码恒白字(不像选中行那样反色) */ - ld.ofs_x = 6; + ld.color = UI_COL_FG; /* 页码恒白字(不像选中行那样反色) */ + ld.ofs_x = UI_FOOTER_X; ld.ofs_y = 0; lv_draw_label(layer, &ld, &t); } @@ -410,7 +383,7 @@ static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32 * 跳过不在重绘区的部分;末尾调 draw_cb(页自定义内容,在内置区块之后) */ static void cp_draw_cb(lv_event_t *e) { - const content_page_def_t *d = s_defs[s_open]; + const content_page_def_t *d = s_open; lv_layer_t *layer = lv_event_get_layer(e); int32_t nrows = (int32_t)cp_row_total(d); /* 行数取一次:供行窗口与页脚页码共用 */ @@ -438,7 +411,7 @@ static void cp_draw_cb(lv_event_t *e) { * 信息页不响应;动态行数可能缩小,选中行越界时重置回首行 */ static void cp_key_cb(lv_event_t *e) { - const content_page_def_t *d = s_defs[s_open]; + const content_page_def_t *d = s_open; lv_key_t key = (lv_key_t)lv_event_get_key(e); ui_standby_notify_activity(); /* 用户活动:重置待机空闲计时 */ @@ -496,24 +469,23 @@ static void cp_key_cb(lv_event_t *e) { /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) - * 入口参数:id - 内容页 ID content_page_id_t + * 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password) + * const content_page_def_t * * perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY) - * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) + * 返回值:true - 成功;false - 失败(desc 为 NULL / 已有页面打开 / 对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象) * 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管 * 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原 * 4. 记录授予权限(页面经 ui_content_page_is_editable 查询) */ -bool ui_content_page_open(content_page_id_t id, uint8_t perm) { - if (id >= CONTENT_PAGE_COUNT || s_open != CONTENT_PAGE_COUNT) { - return false; /* ID 无效或已有页面打开(防御) */ +bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm) { + if (s_open != NULL) { + return false; /* 已有页面打开(防御:页面按需建毁,同一时刻至多一页) */ } - - const content_page_def_t *d = s_defs[id]; - if (d == NULL) { - DBG_ERROR("page open: id=%u no descriptor (enum/table drift)", (unsigned)id); - return false; /* 枚举/描述符表漂移:拒开并留痕,不崩溃 */ + if (desc == NULL) { + DBG_ERROR("page open: NULL descriptor (node has no bound page)"); + return false; /* 调用方把未绑定内容页的叶子当页面开了:拒开并留痕,不崩溃 */ } s_perm = perm; /* 本次授予权限(页面可编辑性 = edit_capable 且修改权限) */ @@ -530,17 +502,17 @@ bool ui_content_page_open(content_page_id_t id, uint8_t perm) { return false; } lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级布局 */ - lv_obj_set_size(s_cont, CP_SCREEN_W, CP_SCREEN_H); /* 全屏覆盖 */ - lv_obj_add_style(s_cont, &cp_style_flat, 0); /* 透明、无边框:剥离主题 card */ + lv_obj_set_size(s_cont, UI_SCREEN_W, UI_SCREEN_H); /* 全屏覆盖 */ + lv_obj_add_style(s_cont, &ui_style_flat, 0); /* 透明、无边框:剥离主题 card */ lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */ lv_obj_add_event_cb(s_cont, cp_key_cb, LV_EVENT_KEY, NULL); lv_obj_add_event_cb(s_cont, cp_draw_cb, LV_EVENT_DRAW_MAIN, NULL); - s_open = id; + s_open = desc; s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */ - if (d->init_cb != NULL) { - d->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */ + if (desc->init_cb != NULL) { + desc->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */ } /* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */ @@ -551,7 +523,7 @@ bool ui_content_page_open(content_page_id_t id, uint8_t perm) { } lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */ - DBG_INFO("page open: id=%u rows=%u", (unsigned)id, (unsigned)cp_row_total(d)); + DBG_INFO("page open: id=%s rows=%u", desc->id, (unsigned)cp_row_total(desc)); return true; } @@ -564,7 +536,7 @@ bool ui_content_page_open(content_page_id_t id, uint8_t perm) { * 2. 切回菜单屏后销毁页面屏幕,LVGL 池内存随即回落 */ void ui_content_page_close(void) { - if (s_open == CONTENT_PAGE_COUNT || s_cont == NULL) { + if (s_open == NULL || s_cont == NULL) { return; /* 未打开(防御) */ } @@ -574,14 +546,14 @@ void ui_content_page_close(void) { lv_screen_load(lv_obj_get_screen(menu_cont)); /* 切回菜单屏(对象未动,原样还原) */ } - DBG_INFO("page close: id=%u", (unsigned)s_open); + DBG_INFO("page close: id=%s", s_open->id); lv_obj_delete(s_cont); /* 先删容器 */ lv_obj_delete(s_scr); /* 再删屏 */ s_scr = NULL; s_cont = NULL; - s_open = CONTENT_PAGE_COUNT; + s_open = NULL; s_sel = 0u; s_perm = (uint8_t)UI_PERM_VIEW; /* 权限重置(卫生:不留上次进入的授权) */ } @@ -594,7 +566,7 @@ void ui_content_page_close(void) { * 函数说明:供逻辑层/调试判断当前处于菜单屏还是内容页 */ bool ui_content_page_is_open(void) { - return (s_open != CONTENT_PAGE_COUNT); + return (s_open != NULL); } /* @@ -605,10 +577,10 @@ bool ui_content_page_is_open(void) { * 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 open 的 perm 实参授予 */ bool ui_content_page_is_editable(void) { - if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) { + if (s_open == NULL) { return false; /* 无页打开或无描述符(防御) */ } - return (s_defs[s_open]->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY); + return (s_open->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY); } /* @@ -619,10 +591,10 @@ bool ui_content_page_is_editable(void) { * 函数说明:供页面钩子(key_cb/draw_cb)感知当前选中位置 */ uint16_t ui_content_page_selected_row(void) { - if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) { + if (s_open == NULL) { return 0u; } - if (s_defs[s_open]->selectable == 0u) { + if (s_open->selectable == 0u) { return 0u; /* 信息页/表单页无行选中概念 */ } return s_sel; diff --git a/Drivers/BSP/UI/ui_content_page.h b/Drivers/BSP/UI/ui_content_page.h index 8fb1d90f..63eaca69 100644 --- a/Drivers/BSP/UI/ui_content_page.h +++ b/Drivers/BSP/UI/ui_content_page.h @@ -4,7 +4,9 @@ * 屏/容器按需建毁生命周期 + 焦点 + 切屏、按键分发(内置 ↑↓/ESC + key_cb 钩子)、 * 描述符驱动通用绘制(浅 V + 标题/副标题 + 行 + 选中带 + 页脚 + draw_cb); * 不持任何页面数据:各页数据/几何/钩子由 ui_page_xxx 模块经 content_page_def_t - * 接入框架描述符表(架构见 内容页架构.md); + * 提供,框架按**描述符指针**开页;"哪个叶子进哪一页"的绑定在生成物 + * ui_page_registry.c(真源 = 菜单树 ui_menu_tree.txt 的 #page= 注记), + * 框架既无页 ID 枚举也无节点→页手写 case(架构见 内容页架构.md); * 进入时切屏并获焦、ESC 返回菜单屏并还原焦点; * 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) @@ -23,6 +25,11 @@ * 是否过密码页/所输密码决定);描述符新增 edit_capable(页面 * 是否支持编辑模式)+ 新 API ui_content_page_is_editable * (页面钩子据以决定编辑行为:edit_capable 且修改权限) + * 2026-10-15 王建锋 U3 页面原型化:删 content_page_id_t 枚举与框架侧 s_defs[], + * open() 改收描述符指针;节点→页面绑定移交给生成物 + * ui_page_registry.c(菜单树 #page= 注记为唯一真源), + * 逻辑层手写 menu_content_page_for() 随之删除; + * 描述符新增 id 字段(ASCII 页名,供日志/诊断定位是哪一页) */ #ifndef UI_CONTENT_PAGE_H #define UI_CONTENT_PAGE_H @@ -32,21 +39,6 @@ #include "lvgl.h" -/* - * 内容页 ID:与菜单叶子节点一一对应(映射见 ui_menu.c menu_content_page_for); - * CONTENT_PAGE_COUNT 兼作"无内容页/未打开"哨兵值 - */ -typedef enum { - CONTENT_PAGE_DEVICE_INFO = 0u, /* 装置信息(node 1):静态键值行 + 副标题,单页 */ - CONTENT_PAGE_AC, /* 交流量(node 3):数据列表,24 项 / 8 项每页 */ - CONTENT_PAGE_DC, /* 直流量(node 4):数据列表,3 项单页 */ - CONTENT_PAGE_PASSWORD, /* 用户密钥(密码页):3 位密码,表单/操作页首个实例 */ - CONTENT_PAGE_NET_CFG, /* 网口设置(node 30):地址列表 + 保存行,可编辑(修改权限) */ - CONTENT_PAGE_RUN_LOG, /* 运行记录(node 40):动态列表,每记录 2 行(时间戳+描述) */ - CONTENT_PAGE_SELFTEST_LOG, /* 自检记录(node 39):动态列表,每记录 2 行(时间戳+故障) */ - CONTENT_PAGE_COUNT /* 哨兵:无效 ID / 未打开 */ -} content_page_id_t; - /* 行数据:左列名 + 右列值(Flash 字面量,不 strdup) */ typedef struct { const char *name; @@ -54,14 +46,18 @@ typedef struct { } cp_row_t; /* - * 页面描述符:一页 = 一个 Flash const 结构(几何 + 数据源 + 可选钩子) - * 由各 ui_page_xxx 页面模块提供,框架描述符表 s_defs[] 按页 ID 引用 + * 页面描述符:一页 = 一个 Flash const 结构(身份 + 几何 + 数据源 + 可选钩子) + * 由各 ui_page_xxx 页面模块提供;"哪个叶子进哪一页"的绑定不在本头,也不在框架 .c, + * 而在生成物 ui_page_registry.c(唯一真源 = 菜单树 ui_menu_tree.txt 的 #page= 注记)。 + * 框架按描述符**指针**开页,不再有页 ID 枚举与 s_defs[] 下标表—— + * 少一处"枚举/表/节点下标三处必须对齐"的手工同步点。 */ typedef struct { + const char *id; /* 页面模块名(ASCII,日志/诊断用;= ui_page_ 的 ) */ const char *title; /* 标题(浅 V 区居中) */ const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */ int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */ - int32_t title_y; /* 标题放置 y;0 = 框架默认(CP_TITLE_Y) */ + int32_t title_y; /* 标题放置 y;0 = 框架默认(UI_TITLE_Y) */ uint8_t hide_deco; /* 1 = 不绘"浅 V"装饰(密码页跟随实机) */ uint8_t hide_footer; /* 1 = 不绘页脚页码(密码页跟随实机:无页脚) */ uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */ @@ -88,17 +84,18 @@ typedef struct { /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) - * 入口参数:id - 内容页 ID content_page_id_t + * 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password) + * const content_page_def_t * * perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY; * 由逻辑层按是否过密码页/所输密码决定,页面经 * ui_content_page_is_editable 查询) - * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) + * 返回值:true - 成功;false - 失败(desc 为 NULL / 已有页面打开 / 对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. 新建独立屏幕,全屏容器自绘全部内容(无子对象,池开销极小) * 2. 容器入默认输入组并获焦——按键由本模块处理(↑↓移选/ESC返回/页面钩子) * 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原 */ -bool ui_content_page_open(content_page_id_t id, uint8_t perm); +bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm); /* * 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面) diff --git a/Drivers/BSP/UI/ui_deco.c b/Drivers/BSP/UI/ui_deco.c index d8d78039..b3107c5a 100644 --- a/Drivers/BSP/UI/ui_deco.c +++ b/Drivers/BSP/UI/ui_deco.c @@ -15,9 +15,10 @@ #include "ui_deco.h" +#include "ui_tokens.h" + /* 私有宏定义区 */ -#define DECO_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ #define DECO_LEG_W 3 /* "浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */ #define DECO_HORIZ_W 2 /* "浅 V"水平线宽 */ @@ -31,7 +32,7 @@ static const lv_point_precise_t s_leg_r[2] = { {158, 7}, {145, 17} }; /* 右腿 static const lv_point_precise_t *const s_segs[3] = {s_leg_l, s_leg_r, s_hoz}; static const int32_t s_seg_w[3] = {DECO_LEG_W, DECO_LEG_W, DECO_HORIZ_W}; -static const lv_area_t s_band = {0, 0, DECO_SCREEN_W - 1, 19}; /* "浅 V" culling 区(与原线段对象范围一致) */ +static const lv_area_t s_band = {0, 0, UI_SCREEN_W - 1, 19}; /* "浅 V" culling 区(与原线段对象范围一致) */ /* 函数定义区 */ @@ -72,7 +73,7 @@ void ui_deco_draw(lv_layer_t *layer, const lv_area_t *clip) { lv_draw_line_dsc_init(&ld); ld.base.layer = layer; - ld.color = lv_color_white(); + ld.color = UI_COL_FG; ld.width = s_seg_w[i]; ld.p1 = s_segs[i][0]; ld.p2 = s_segs[i][1]; diff --git a/Drivers/BSP/UI/ui_font_cn_12.c b/Drivers/BSP/UI/ui_font_cn_12.c index 33a53019..1813a705 100644 --- a/Drivers/BSP/UI/ui_font_cn_12.c +++ b/Drivers/BSP/UI/ui_font_cn_12.c @@ -311,11 +311,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x7f, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xff, 0x80, - /* U+4E0A "上" */ - 0x8, 0x1, 0x0, 0x20, 0x4, 0x0, 0xfc, 0x10, - 0x2, 0x0, 0x40, 0x8, 0x1, 0x0, 0x20, 0x7f, - 0xf0, - /* U+4E3B "主" */ 0x8, 0x0, 0x80, 0x0, 0x3f, 0xe0, 0x40, 0x8, 0x1, 0x3, 0xfe, 0x4, 0x0, 0x80, 0x10, 0x7f, @@ -671,11 +666,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x7f, 0xd1, 0x14, 0x45, 0xff, 0x44, 0x51, 0x17, 0xfd, 0x11, 0x44, 0x51, 0x18, 0x1c, - /* U+7535 "电" */ - 0x8, 0x1, 0x3, 0xfe, 0x44, 0x48, 0x89, 0xff, - 0x22, 0x24, 0x44, 0xff, 0x91, 0x4, 0x20, 0x83, - 0xf0, - /* U+754C "界" */ 0x7f, 0xc8, 0x89, 0xff, 0x22, 0x27, 0xfc, 0x8, 0x6, 0xc7, 0x7, 0x11, 0x2, 0x20, 0x84, 0x20, @@ -828,11 +818,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x17, 0xfa, 0x8, 0x49, 0x29, 0xfd, 0x0, 0x5f, 0xf0, - /* U+91CD "重" */ - 0x3, 0xcf, 0x80, 0x10, 0x7f, 0xf4, 0x44, 0xff, - 0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f, - 0xf0, - /* U+91CF "量" */ 0x7f, 0xc8, 0x89, 0x9, 0x7f, 0xf4, 0x44, 0xff, 0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f, @@ -1018,20 +1003,20 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 723, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 740, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 757, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 774, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 788, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 804, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 821, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 837, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, + {.bitmap_index = 757, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 771, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 787, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 804, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 820, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, + {.bitmap_index = 832, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 849, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 866, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 883, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 900, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 917, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 934, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 951, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 968, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 951, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 967, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 984, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1001, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1018, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1042,8 +1027,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1103, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1120, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1137, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1154, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1171, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1154, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1168, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1185, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1202, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1219, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1057,9 +1042,9 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1355, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1372, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1389, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1406, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1423, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1439, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1406, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1422, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1435, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1452, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1469, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1486, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1067,22 +1052,22 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1520, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1537, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1554, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1571, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1588, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1571, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1587, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1604, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1621, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1638, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1655, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1672, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1689, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1723, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1737, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1754, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1771, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1786, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1803, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1820, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1706, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1720, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1737, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1752, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1769, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1786, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1802, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1819, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1836, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1853, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1870, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1091,9 +1076,9 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1921, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1938, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1955, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1972, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1989, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2006, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1972, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1986, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2003, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2020, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2037, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2054, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1113,18 +1098,15 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2292, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2309, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2326, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2343, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2360, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2377, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2394, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2343, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2359, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2376, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2393, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2410, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2427, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2444, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2461, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2478, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 2479, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 2481, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */, - {.bitmap_index = 2491, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0} /* U+FF0C */ + {.bitmap_index = 2427, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 2428, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 2430, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */, + {.bitmap_index = 2440, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0} /* U+FF0C */ }; /*--------------------- @@ -1132,22 +1114,21 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { *--------------------*/ static const uint16_t unicode_list_1[] = { - 0x0, 0x1, 0x32, 0x82, 0x9b, 0xed, 0x153, 0x1d4, - 0x1d8, 0x233, 0x35c, 0x368, 0x36a, 0x42d, 0x444, 0x49f, - 0x50d, 0x531, 0x582, 0x5b9, 0x5c3, 0x5da, 0x5ee, 0x626, - 0x641, 0x927, 0x937, 0x982, 0xb04, 0xd47, 0xd4e, 0xd4f, - 0xd91, 0xd95, 0xdbd, 0x102f, 0x1086, 0x10f9, 0x1106, 0x1131, - 0x1149, 0x114a, 0x114c, 0x1266, 0x142e, 0x1442, 0x148d, 0x149b, - 0x149c, 0x1565, 0x159e, 0x15a0, 0x16c4, 0x173c, 0x1767, 0x176b, - 0x17d7, 0x17dc, 0x17ed, 0x1916, 0x1923, 0x1976, 0x19dc, 0x1a18, - 0x1ab7, 0x1c18, 0x1d72, 0x1ed9, 0x1f38, 0x1f42, 0x1ffc, 0x22b0, - 0x243e, 0x243f, 0x271f, 0x272c, 0x2743, 0x28eb, 0x2902, 0x29f8, - 0x2a63, 0x2c02, 0x2ce6, 0x2d23, 0x2e72, 0x2ef2, 0x30b6, 0x30bf, - 0x30eb, 0x3148, 0x3165, 0x33e1, 0x34a6, 0x3a43, 0x3abc, 0x3d5d, - 0x3d9b, 0x3da6, 0x3da7, 0x3db5, 0x3dd9, 0x3de6, 0x4166, 0x418a, - 0x41c7, 0x4211, 0x425c, 0x43c4, 0x43c6, 0x4696, 0x469c, 0x4710, - 0x4829, 0x485b, 0x4959, 0x4a6c, 0x4b7f, 0x4c83, 0x50cf, 0xb103, - 0xb111 + 0x0, 0x32, 0x82, 0x9b, 0xed, 0x153, 0x1d4, 0x1d8, + 0x233, 0x35c, 0x368, 0x36a, 0x42d, 0x444, 0x49f, 0x50d, + 0x531, 0x582, 0x5b9, 0x5c3, 0x5da, 0x5ee, 0x626, 0x641, + 0x927, 0x937, 0x982, 0xb04, 0xd47, 0xd4e, 0xd4f, 0xd91, + 0xd95, 0xdbd, 0x102f, 0x1086, 0x10f9, 0x1106, 0x1131, 0x1149, + 0x114a, 0x114c, 0x1266, 0x142e, 0x1442, 0x148d, 0x149b, 0x149c, + 0x1565, 0x159e, 0x15a0, 0x16c4, 0x173c, 0x1767, 0x176b, 0x17d7, + 0x17dc, 0x17ed, 0x1916, 0x1923, 0x1976, 0x19dc, 0x1a18, 0x1ab7, + 0x1c18, 0x1d72, 0x1ed9, 0x1f38, 0x1f42, 0x1ffc, 0x22b0, 0x243e, + 0x243f, 0x271f, 0x2743, 0x28eb, 0x2902, 0x29f8, 0x2a63, 0x2c02, + 0x2ce6, 0x2d23, 0x2e72, 0x2ef2, 0x30b6, 0x30bf, 0x30eb, 0x3148, + 0x3165, 0x33e1, 0x34a6, 0x3a43, 0x3abc, 0x3d5d, 0x3d9b, 0x3da6, + 0x3da7, 0x3db5, 0x3dd9, 0x3de6, 0x4166, 0x418a, 0x41c7, 0x4211, + 0x425c, 0x43c6, 0x4696, 0x469c, 0x4710, 0x4829, 0x485b, 0x4959, + 0x4a6c, 0x4b7f, 0x4c83, 0x50cf, 0xb103, 0xb111 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1159,14 +1140,14 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = }, { .range_start = 19977, .range_length = 45330, .glyph_id_start = 96, - .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 121, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 118, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY }, { - .range_start = 9654, .range_length = 1, .glyph_id_start = 217, + .range_start = 9654, .range_length = 1, .glyph_id_start = 214, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { - .range_start = 65292, .range_length = 1, .glyph_id_start = 218, + .range_start = 65292, .range_length = 1, .glyph_id_start = 215, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY } }; diff --git a/Drivers/BSP/UI/ui_menu.c b/Drivers/BSP/UI/ui_menu.c index 377e9418..1d691431 100644 --- a/Drivers/BSP/UI/ui_menu.c +++ b/Drivers/BSP/UI/ui_menu.c @@ -32,6 +32,8 @@ #include "ui_content_page.h" +#include "ui_page_registry.h" + #include "ui_page_password.h" #include "ui_standby.h" @@ -105,28 +107,6 @@ static void menu_back(void) { ui_menu_view_focus(); } -/* - * 函数功能:查叶子节点对应的内容页(无内容页返回哨兵值) - * 入口参数:node - 菜单节点下标 uint8_t - * 返回值:内容页 ID content_page_id_t;无内容页 = CONTENT_PAGE_COUNT - * 限定条件:无 - * 函数说明:仅部分叶子有内容页(参考图 10/20/21 + 网口设置 + 记录查询);下标对应 - * ui_menu_tab.c - * node 1=装置信息、3=交流量、4=直流量、30=网口设置、39=自检记录、40=运行记录 - * (表序变更时须同步本映射) - */ -static content_page_id_t menu_content_page_for(uint8_t node) { - switch (node) { - case 1u: return CONTENT_PAGE_DEVICE_INFO; /* 装置信息(L1 叶子) */ - case 3u: return CONTENT_PAGE_AC; /* 交流量(实时数据子项) */ - case 4u: return CONTENT_PAGE_DC; /* 直流量(实时数据子项) */ - case 30u: return CONTENT_PAGE_NET_CFG; /* 网口设置(通讯参数子项,需密码) */ - case 39u: return CONTENT_PAGE_SELFTEST_LOG; /* 自检记录(记录查询子项) */ - case 40u: return CONTENT_PAGE_RUN_LOG; /* 运行记录(记录查询子项) */ - default: return (content_page_id_t)CONTENT_PAGE_COUNT; /* 无内容页 */ - } -} - /* * 函数功能:密码页确认一致回调(预装进密码页模块;uiTask 上下文调用) * 入口参数:perm - 密码页按所输密码授予的权限 ui_perm_t(UI_PERM_MODIFY/UI_PERM_VIEW) @@ -152,10 +132,10 @@ static void menu_password_ok(uint8_t perm) { { uint8_t child = kids[s_sel[depth]]; - content_page_id_t pid = menu_content_page_for(child); + const content_page_def_t *page = ui_page_registry_for(child); - if (pid < CONTENT_PAGE_COUNT) { - ui_content_page_open(pid, perm); /* 页已关,守卫可过:开目标内容页(携权限) */ + if (page != NULL) { + ui_content_page_open(page, perm); /* 页已关,守卫可过:开目标内容页(携权限) */ } else if (depth < MENU_PAGE_MAX - 1u) { menu_open(child); /* 压层:建新框 + 焦点移到新框(叶子 → 空白终点框) */ @@ -202,17 +182,18 @@ static void menu_key_handle(uint8_t depth, lv_key_t key) { /* 进子框:空框无条目可进;最深层(depth=MAX-1)无子层可再开 */ if (cnt > 0u) { uint8_t child = kids[s_sel[depth]]; - content_page_id_t pid = menu_content_page_for(child); + const content_page_def_t *page = ui_page_registry_for(child); if (ui_menu_tab_has_pwd(child)) { /* 需密码叶子(含带内容页者):先经密码页;授权按所输密码 - (查看→只读 / 修改→可修改),回调携权续做"进入"动作 */ + (查看→只读 / 修改→可修改),回调携权续做"进入"动作; + 密码页非节点绑定,经注册表专用出口取 */ ui_page_password_arm(menu_password_ok); - ui_content_page_open(CONTENT_PAGE_PASSWORD, (uint8_t)UI_PERM_VIEW); + ui_content_page_open(ui_page_registry_password(), (uint8_t)UI_PERM_VIEW); } - else if (pid < CONTENT_PAGE_COUNT) { + else if (page != NULL) { /* 叶子有内容页:无密码直接进(默认只读权限) */ - ui_content_page_open(pid, (uint8_t)UI_PERM_VIEW); + ui_content_page_open(page, (uint8_t)UI_PERM_VIEW); } else if (depth < MENU_PAGE_MAX - 1u) { menu_open(child); /* 压层:建新框 + 焦点移到新框(叶子 → 空白终点框) */ diff --git a/Drivers/BSP/UI/ui_menu_view.c b/Drivers/BSP/UI/ui_menu_view.c index 7777e23f..59b89a8e 100644 --- a/Drivers/BSP/UI/ui_menu_view.c +++ b/Drivers/BSP/UI/ui_menu_view.c @@ -31,6 +31,10 @@ #include "ui_deco.h" +#include "ui_style.h" + +#include "ui_tokens.h" + #include "lvgl.h" #define DBG_TAG "[UI_MENU_VIEW]" @@ -79,22 +83,8 @@ static uint8_t s_row_node[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行数据:节 static menu_frame_t s_frame[MENU_PAGE_MAX]; /* 每层框几何缓存:位置/尺寸/选中行(现算不落状态,供子框对齐;见上方类型注释) */ /* 样式(静态,一次性初始化,常驻) */ -/* flat:通用——透明、无边框/圆角/内边距(剥离主题 card)。用常量样式(LV_STYLE_CONST_INIT): - * 属性存静态数组、零 LV_MEM 池分配;动态样式首次 set_prop 会从池分配 values_and_props 且永不释放 */ -static const lv_style_const_prop_t s_flat_props[] = { - { .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } }, - { .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } }, - { .prop = LV_STYLE_RADIUS, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } }, - LV_STYLE_CONST_PROPS_END -}; - -LV_STYLE_CONST_INIT(s_style_flat, s_flat_props); +/* flat(透明、无边框/圆角/内边距,剥离主题 card)取自共享样式表 ui_style.c: + * 与 ui_content_page / ui_standby 共用同一 const 对象,不再本地重复定义 */ static lv_style_t s_style_frame; /* 框体:透明底 + 1px 白边框 */ static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */ @@ -122,7 +112,7 @@ static void view_styles_init(void) { lv_style_init(&s_style_frame); lv_style_set_bg_opa(&s_style_frame, LV_OPA_TRANSP); lv_style_set_border_width(&s_style_frame, MENU_FRAME_BORDER); - lv_style_set_border_color(&s_style_frame, lv_color_white()); + lv_style_set_border_color(&s_style_frame, UI_COL_FG); lv_style_set_radius(&s_style_frame, 0); lv_style_set_pad_all(&s_style_frame, 0); lv_style_set_pad_gap(&s_style_frame, 0); @@ -130,8 +120,8 @@ static void view_styles_init(void) { /* scrollbar:白色 4px 宽(行超框高时自动出现) */ lv_style_init(&s_style_scrollbar); lv_style_set_bg_opa(&s_style_scrollbar, LV_OPA_COVER); - lv_style_set_bg_color(&s_style_scrollbar, lv_color_white()); - lv_style_set_width(&s_style_scrollbar, 4); + lv_style_set_bg_color(&s_style_scrollbar, UI_COL_FG); + lv_style_set_width(&s_style_scrollbar, UI_SCROLLBAR_W); } /* @@ -237,7 +227,7 @@ static void view_frame_draw_cb(lv_event_t *e) { lv_area_t row_area; const char *name = ui_menu_tab_name(s_row_node[depth][i]); bool is_sel = (i == (int32_t)f->sel); - lv_color_t color = is_sel ? lv_color_black() : lv_color_white(); + lv_color_t color = is_sel ? UI_COL_SEL_FG : UI_COL_FG; row_area.x1 = area.x1; row_area.x2 = area.x2; @@ -254,7 +244,7 @@ static void view_frame_draw_cb(lv_event_t *e) { lv_draw_rect_dsc_t fd; lv_draw_rect_dsc_init(&fd); fd.base.layer = layer; - fd.bg_color = lv_color_white(); + fd.bg_color = UI_COL_SEL_BG; fd.bg_opa = LV_OPA_COVER; lv_draw_rect(layer, &fd, &row_area); } @@ -407,7 +397,7 @@ void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t if (box == NULL) { return; /* LVGL 池耗尽:框未建,状态回滚 */ } - lv_obj_add_style(box, &s_style_flat, 0); + lv_obj_add_style(box, &ui_style_flat, 0); lv_obj_add_style(box, &s_style_frame, 0); lv_obj_add_flag(box, LV_OBJ_FLAG_IGNORE_LAYOUT); lv_obj_set_pos(box, f->x, f->y); @@ -571,7 +561,7 @@ static void view_deco_draw_cb(lv_event_t *e) { sd.base.layer = layer; sd.text = "当前模式 : 无模式"; sd.text_static = 1; /* Flash 字符串:不拷内存 */ - sd.color = lv_color_white(); + sd.color = UI_COL_FG; sd.ofs_x = 0; /* 区域原点即状态行左上 (17,20) */ sd.ofs_y = 0; lv_draw_label(layer, &sd, &s_status_area); @@ -608,7 +598,7 @@ int ui_menu_view_init(void) { if (menu == NULL) { return -1; } - lv_obj_add_style(menu, &s_style_flat, 0); /* 透明、无边框:纯定位层,不可见 */ + lv_obj_add_style(menu, &ui_style_flat, 0); /* 透明、无边框:纯定位层,不可见 */ lv_obj_set_size(menu, sw, sh); /* 持久容器:全屏覆盖、永不删(焦点/键唯一入口;各层框均建其内,防悬空) */ @@ -616,7 +606,7 @@ int ui_menu_view_init(void) { if (s_cont == NULL) { return -1; } - lv_obj_add_style(s_cont, &s_style_flat, 0); /* 透明、无边框 */ + lv_obj_add_style(s_cont, &ui_style_flat, 0); /* 透明、无边框 */ lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级 flex 布局 */ lv_obj_set_pos(s_cont, 0, 0); lv_obj_set_size(s_cont, sw, sh); /* 全屏覆盖 */ @@ -634,7 +624,7 @@ int ui_menu_view_init(void) { if (s_frame_box[0] == NULL) { return -1; } - lv_obj_add_style(s_frame_box[0], &s_style_flat, 0); /* 透明底 */ + lv_obj_add_style(s_frame_box[0], &ui_style_flat, 0); /* 透明底 */ lv_obj_add_style(s_frame_box[0], &s_style_frame, 0); /* 1px 白边框 */ lv_obj_add_flag(s_frame_box[0], LV_OBJ_FLAG_IGNORE_LAYOUT); lv_obj_set_pos(s_frame_box[0], MENU_ROOT_BOX_X, MENU_ROOT_BOX_Y); /* 左上 (0,44):装饰/状态行之下 */ diff --git a/Drivers/BSP/UI/ui_menu_view.h b/Drivers/BSP/UI/ui_menu_view.h index 5e354b26..7fd7bcf5 100644 --- a/Drivers/BSP/UI/ui_menu_view.h +++ b/Drivers/BSP/UI/ui_menu_view.h @@ -21,11 +21,15 @@ #include "lvgl.h" +#include "ui_tokens.h" + #define MENU_PAGE_MAX 4 /* 页面深度上限(根框 + 3 级子框) */ #define MENU_ITEM_MAX 16 /* 单框最大条目数 */ -#define MENU_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ -#define MENU_SCREEN_H 160 /* 屏幕高(px) */ +/* 屏幕尺寸单一来源 = ui_tokens.h(权威定义在 ui_port.h,两端一致); + * 保留 MENU_SCREEN_* 旧名作为别名,兼容既有引用与本模块文档 */ +#define MENU_SCREEN_W UI_SCREEN_W /* 屏幕宽(px) */ +#define MENU_SCREEN_H UI_SCREEN_H /* 屏幕高(px) */ /* * 函数功能:初始化显示层(样式 + 4 层持久容器 + 根框装饰 + 根框框体) diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index f66a39c4..3eecaec3 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -38,6 +38,7 @@ #include "net_socket.h" #include "net_types.h" #include "ui_net_sink_backend.h" +#include "ui_tokens.h" /* 屏幕几何令牌(UI_SCREEN_W/H、UI_FB_SIZE 权威在 ui_port.h) */ #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* M3 探针:prod_cycle_mcu(LCDMEM 处理净成本) */ @@ -53,16 +54,14 @@ #define CMD_KEY UI_REMO_CMD_KEY #define CMD_LCDMEM UI_REMO_CMD_LCDMEM -/* 屏幕尺寸与帧大小(160x160 1bpp = 3200 字节) */ -#define UI_W 160 -#define UI_H 160 -#define UI_MEMSIZE (UI_W * UI_H / 8) +/* 屏幕尺寸与帧大小取 ui_tokens.h / ui_port.h 的屏幕令牌(160x160 1bpp = 3200 字节), + * 本模块不再自带同值字面量——显示几何与协议几何必须是同一个事实 */ #define RM_MAX_RX_PAYLOAD 256 /* 客户端→服务端 payload 上限,防长度字段损坏引发失步 */ #define RM_RX_BUF 64 /* 接收暂存区:整块 recv 后逐字节喂入解析状态机 */ -static uint8_t g_tx[4 + 4 + UI_MEMSIZE + 1]; /* 发送组帧缓冲:[TAG][CMD][LEN][DATA][CRC] */ -static uint8_t g_lcdmem_data[4 + UI_MEMSIZE]; /* LCDMEM 应答缓冲:4B 偏移 + 1bpp 位图 */ +static uint8_t g_tx[4 + 4 + UI_FB_SIZE + 1]; /* 发送组帧缓冲:[TAG][CMD][LEN][DATA][CRC] */ +static uint8_t g_lcdmem_data[4 + UI_FB_SIZE]; /* LCDMEM 应答缓冲:4B 偏移 + 1bpp 位图 */ static uint8_t g_rx[RM_RX_BUF]; /* 接收暂存区:整块 recv 后逐字节喂入状态机 */ static int g_listen = -1; /* 监听 Socket fd(未启动或已停止时为 -1) */ @@ -194,9 +193,9 @@ static int remo_send(uint8_t cmd, const uint8_t *data, uint16_t len) { static void remo_send_init(void) { uint8_t init[8]; - put_u16(init, UI_W); - put_u16(init + 2, UI_H); - put_u32(init + 4, UI_MEMSIZE); + put_u16(init, UI_SCREEN_W); + put_u16(init + 2, UI_SCREEN_H); + put_u32(init + 4, UI_FB_SIZE); (void)remo_send(CMD_INIT, init, 8); } @@ -295,10 +294,10 @@ static void remo_handle(const uint8_t *f, int n) { #endif /* 整帧请求:经平台后端取 1bpp 位图(MCU: ui_port_get_framebuffer / * 模拟器: sim_port_get_screen_mono)写入应答缓冲尾部,失败则静默丢弃 */ - if (ui_net_sink_get_lcdmem(g_lcdmem_data + 4, UI_MEMSIZE) == 0) { + if (ui_net_sink_get_lcdmem(g_lcdmem_data + 4, UI_FB_SIZE) == 0) { /* 应答数据段 = [偏移u32(回填请求原值)] + [1bpp 位图 3200B] */ memcpy(g_lcdmem_data, data, 4); - (void)remo_send(CMD_LCDMEM, g_lcdmem_data, (uint16_t)(4 + UI_MEMSIZE)); + (void)remo_send(CMD_LCDMEM, g_lcdmem_data, (uint16_t)(4 + UI_FB_SIZE)); } #if defined(TEST_LATENCY_PROBE) lat_probe_add_seg("prod_cycle_mcu", lat_mark_us(&tm)); diff --git a/Drivers/BSP/UI/ui_net_sink_backend.h b/Drivers/BSP/UI/ui_net_sink_backend.h index a2ca901f..a66ed1de 100644 --- a/Drivers/BSP/UI/ui_net_sink_backend.h +++ b/Drivers/BSP/UI/ui_net_sink_backend.h @@ -13,12 +13,15 @@ #include +#include "ui_port.h" /* UI_FB_SIZE:显示几何与协议几何的单一来源 */ + #ifdef __cplusplus extern "C" { #endif -/* 1bpp 帧缓冲尺寸(160x160 / 8 = 3200 字节,须与 ui_net_sink.c 的 UI_MEMSIZE 一致) */ -#define UI_NET_SINK_MONO_SIZE 3200 +/* 1bpp 帧缓冲尺寸 = 屏幕令牌的帧字节数(UI_SCREEN_W*UI_SCREEN_H/8 = 3200), + * 与 ui_net_sink.c 的收发缓冲同源,不再各自写死字面量 */ +#define UI_NET_SINK_MONO_SIZE UI_FB_SIZE /* * 函数功能:读取当前 1bpp 屏幕位图(LCDMEM 应答的数据源) diff --git a/Drivers/BSP/UI/ui_page_ac.c b/Drivers/BSP/UI/ui_page_ac.c index f9c37d5a..a9ec7569 100644 --- a/Drivers/BSP/UI/ui_page_ac.c +++ b/Drivers/BSP/UI/ui_page_ac.c @@ -13,6 +13,8 @@ /* 头文件包含区 */ #include "ui_page_ac.h" +#include "ui_tokens.h" + /* 静态变量定义区 */ /* 交流量行(参考图 20 = 第 1 页原样;第 2/3 页为占位数据,可替换) */ @@ -43,17 +45,18 @@ static const cp_row_t s_rows[] = { {"Smax","2.45"}, }; -/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;列表页,钩子全 NULL) */ +/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=ac 绑定引用;列表页,钩子全 NULL) */ const content_page_def_t ui_page_ac_desc = { + .id = "ac", .title = "交流量", .selectable = 1u, - .top_y = 24, - .pitch = 14, - .band_h = 13, + .top_y = UI_LIST_TOP, + .pitch = UI_ROW_PITCH, + .band_h = UI_ROW_BAND_H, .text_dy = 0, - .left_x = 7, - .right_x = 101, - .rows_per_page = 8u, + .left_x = UI_LIST_LEFT_X, + .right_x = UI_LIST_RIGHT_X, + .rows_per_page = UI_LIST_ROWS, .row_count = 24, .rows = s_rows, }; diff --git a/Drivers/BSP/UI/ui_page_ac.h b/Drivers/BSP/UI/ui_page_ac.h index f91a7ea8..d9cdaced 100644 --- a/Drivers/BSP/UI/ui_page_ac.h +++ b/Drivers/BSP/UI/ui_page_ac.h @@ -15,8 +15,8 @@ #include "ui_content_page.h" /* - * 页面描述符(Flash const):交流量页 = CONTENT_PAGE_AC; - * 接入框架描述符表 s_defs[](定义于 ui_page_ac.c) + * 页面描述符(Flash const):交流量页(描述符 .id = "ac"); + * 由生成物 ui_page_registry.c 按菜单树 #page=ac 注记绑定引用(定义于 ui_page_ac.c) */ extern const content_page_def_t ui_page_ac_desc; diff --git a/Drivers/BSP/UI/ui_page_dc.c b/Drivers/BSP/UI/ui_page_dc.c index b157f06d..1888f3df 100644 --- a/Drivers/BSP/UI/ui_page_dc.c +++ b/Drivers/BSP/UI/ui_page_dc.c @@ -12,6 +12,8 @@ /* 头文件包含区 */ #include "ui_page_dc.h" +#include "ui_tokens.h" + /* 静态变量定义区 */ /* 直流量行(参考图 21) */ @@ -21,17 +23,18 @@ static const cp_row_t s_rows[] = { {"Pa1", "3.288"}, }; -/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;列表页单页,钩子全 NULL) */ +/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=dc 绑定引用;列表页单页,钩子全 NULL) */ const content_page_def_t ui_page_dc_desc = { + .id = "dc", .title = "直流量", .selectable = 1u, - .top_y = 24, - .pitch = 14, - .band_h = 13, + .top_y = UI_LIST_TOP, + .pitch = UI_ROW_PITCH, + .band_h = UI_ROW_BAND_H, .text_dy = 0, - .left_x = 7, - .right_x = 101, - .rows_per_page = 8u, + .left_x = UI_LIST_LEFT_X, + .right_x = UI_LIST_RIGHT_X, + .rows_per_page = UI_LIST_ROWS, .row_count = 3, .rows = s_rows, }; diff --git a/Drivers/BSP/UI/ui_page_dc.h b/Drivers/BSP/UI/ui_page_dc.h index 3eb29ae5..357a8c95 100644 --- a/Drivers/BSP/UI/ui_page_dc.h +++ b/Drivers/BSP/UI/ui_page_dc.h @@ -15,8 +15,8 @@ #include "ui_content_page.h" /* - * 页面描述符(Flash const):直流量页 = CONTENT_PAGE_DC; - * 接入框架描述符表 s_defs[](定义于 ui_page_dc.c) + * 页面描述符(Flash const):直流量页(描述符 .id = "dc"); + * 由生成物 ui_page_registry.c 按菜单树 #page=dc 注记绑定引用(定义于 ui_page_dc.c) */ extern const content_page_def_t ui_page_dc_desc; diff --git a/Drivers/BSP/UI/ui_page_device_info.c b/Drivers/BSP/UI/ui_page_device_info.c index 61804629..64cdda44 100644 --- a/Drivers/BSP/UI/ui_page_device_info.c +++ b/Drivers/BSP/UI/ui_page_device_info.c @@ -25,8 +25,9 @@ static const cp_row_t s_rows[] = { {"程序日期:", "2024.08.27"}, }; -/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;静态信息页,钩子全 NULL) */ +/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=device_info 绑定引用;静态信息页,钩子全 NULL) */ const content_page_def_t ui_page_device_info_desc = { + .id = "device_info", .title = "装置信息", .subtitle = "馈线自动化终端", .subtitle_y = 25, diff --git a/Drivers/BSP/UI/ui_page_device_info.h b/Drivers/BSP/UI/ui_page_device_info.h index 4850a366..588f604d 100644 --- a/Drivers/BSP/UI/ui_page_device_info.h +++ b/Drivers/BSP/UI/ui_page_device_info.h @@ -14,8 +14,8 @@ #include "ui_content_page.h" /* - * 页面描述符(Flash const):装置信息页 = CONTENT_PAGE_DEVICE_INFO; - * 接入框架描述符表 s_defs[](定义于 ui_page_device_info.c) + * 页面描述符(Flash const):装置信息页(描述符 .id = "device_info"); + * 由生成物 ui_page_registry.c 按菜单树 #page=device_info 注记绑定引用(定义于 ui_page_device_info.c) */ extern const content_page_def_t ui_page_device_info_desc; diff --git a/Drivers/BSP/UI/ui_page_net_cfg.c b/Drivers/BSP/UI/ui_page_net_cfg.c index 7e2eaf05..6444da36 100644 --- a/Drivers/BSP/UI/ui_page_net_cfg.c +++ b/Drivers/BSP/UI/ui_page_net_cfg.c @@ -30,10 +30,13 @@ #include "net_cfg.h" +#include "ui_tokens.h" + /* 私有宏定义区 */ /* - * 布局常量(设备像素;1bpp 屏 160x160): + * 布局取值:行骨架用 ui_tokens.h 的列表页令牌(UI_LIST_TOP / UI_ROW_PITCH / + * UI_ROW_BAND_H / UI_BUF_STRLEN),本文件只留本页私有的两列 x 与行数。 * 5 行共 1 页(行槽顶 y = 24+14i,末行止于 y=93,与页脚 y=143 无冲突); * 名 x=4 / 值 x=54(值列须 ≥54:4 汉字名占 48px + 间隙;值列宽—— * IP/掩码/网关 11 ASCII 字符 = 66px 止于 119,MAC 17 字符(冒号分隔)= 102px 止于 155, @@ -42,12 +45,8 @@ #define NC_ROW_COUNT 5u /* 4 地址行 + 1 保存行 */ #define NC_ADDR_ROW_MAX 4u /* 地址行范围 0..3(IP/掩码/网关/MAC) */ #define NC_SAVE_ROW 4u /* 保存行下标 */ -#define NC_TOP_Y 24 /* 首行槽顶 y */ -#define NC_PITCH 14 /* 行距 */ -#define NC_BAND_H 13 /* 选中白带高 */ #define NC_LEFT_X 4 /* 左列 x(名) */ #define NC_RIGHT_X 54 /* 右列 x(值) */ -#define NC_ROW_BUF_LEN 32u /* 行缓冲长度(与框架 CP_ROW_BUF_LEN 一致) */ /* 静态变量定义区 */ @@ -86,7 +85,7 @@ static uint8_t nc_octet_count(uint16_t row) { /* * 函数功能:生成指定地址行的值串(与 refresh 值列同源,编辑光标定位与行文本一致) * 入口参数:row - 地址行下标(0..3) uint16_t - * p_buf - 输出缓冲(≥ NC_ROW_BUF_LEN) char * + * p_buf - 输出缓冲(≥ UI_BUF_STRLEN) char * * 返回值:无 * 限定条件:p_buf 非空 * 函数说明:IP/掩码/网关 = "192.168.001.100"(每段 3 位补零定长,字节 i 恒在 @@ -99,7 +98,7 @@ static void nc_fmt_value(uint16_t row, char *p_buf) { uint8_t mac[6]; net_cfg_get_mac(mac); - (void)lv_snprintf(p_buf, NC_ROW_BUF_LEN, "%02X:%02X:%02X:%02X:%02X:%02X", + (void)lv_snprintf(p_buf, UI_BUF_STRLEN, "%02X:%02X:%02X:%02X:%02X:%02X", (unsigned)mac[0], (unsigned)mac[1], (unsigned)mac[2], (unsigned)mac[3], (unsigned)mac[4], (unsigned)mac[5]); return; @@ -109,15 +108,15 @@ static void nc_fmt_value(uint16_t row, char *p_buf) { const uint8_t *field = (row == 0u) ? cfg->ip : ((row == 1u) ? cfg->mask : cfg->gw); - net_cfg_fmt_ipv4(field, p_buf, NC_ROW_BUF_LEN); + net_cfg_fmt_ipv4(field, p_buf, UI_BUF_STRLEN); } } /* * 函数功能:动态行刷新(逐行取名/值) * 入口参数:row - 行下标(0..4) uint16_t - * name - 名缓冲(≥ NC_ROW_BUF_LEN,框架栈缓冲) char * - * value - 值缓冲(≥ NC_ROW_BUF_LEN,框架栈缓冲) char * + * name - 名缓冲(≥ UI_BUF_STRLEN,框架栈缓冲) char * + * value - 值缓冲(≥ UI_BUF_STRLEN,框架栈缓冲) char * * 返回值:无 * 限定条件:页面打开(框架绘制链内逐行调用) * 函数说明:地址行值取 net_cfg RAM 副本(MAC 全零时显示芯片实际 MAC 影子); @@ -133,15 +132,15 @@ static void nc_refresh(uint16_t row, char *name, char *value) { switch (row) { case 0u: (void)memcpy(name, "IP地址", sizeof("IP地址")); - net_cfg_fmt_ipv4(cfg->ip, value, NC_ROW_BUF_LEN); + net_cfg_fmt_ipv4(cfg->ip, value, UI_BUF_STRLEN); break; case 1u: (void)memcpy(name, "子网掩码", sizeof("子网掩码")); - net_cfg_fmt_ipv4(cfg->mask, value, NC_ROW_BUF_LEN); + net_cfg_fmt_ipv4(cfg->mask, value, UI_BUF_STRLEN); break; case 2u: (void)memcpy(name, "默认网关", sizeof("默认网关")); - net_cfg_fmt_ipv4(cfg->gw, value, NC_ROW_BUF_LEN); + net_cfg_fmt_ipv4(cfg->gw, value, UI_BUF_STRLEN); break; case 3u: (void)memcpy(name, "MAC地址", sizeof("MAC地址")); @@ -197,7 +196,7 @@ static void nc_adjust_octet(uint16_t row, int8_t delta) { * 步进宽实测定位(ASCII 步进 6px/字符,与行文本布局一致) */ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { - char value[NC_ROW_BUF_LEN]; + char value[UI_BUF_STRLEN]; lv_area_t box; uint32_t start; uint32_t seg_len; @@ -219,14 +218,14 @@ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { start = (uint32_t)s_cursor * ((sel == 3u) ? 3u : 4u); /* 字节 i 字符偏移:IP/掩码/网关 4i,MAC 3i(冒号分隔) */ seg_len = (sel == 3u) ? 2u : 3u; - slot_y = NC_TOP_Y + (int32_t)sel * NC_PITCH; + slot_y = UI_LIST_TOP + (int32_t)sel * UI_ROW_PITCH; x0 = NC_RIGHT_X + (int32_t)lv_text_get_width(value, start, LV_FONT_DEFAULT, 0); w = (int32_t)lv_text_get_width(value + start, seg_len, LV_FONT_DEFAULT, 0); box.x1 = x0 - 1; box.x2 = x0 + w; box.y1 = slot_y; - box.y2 = slot_y + NC_BAND_H - 1; + box.y2 = slot_y + UI_ROW_BAND_H - 1; if (!nc_areas_intersect(&box, clip)) { return; /* 编辑光标不在重绘区:跳过(culling) */ } @@ -237,7 +236,7 @@ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { lv_draw_rect_dsc_init(&fd); fd.base.layer = layer; - fd.bg_color = lv_color_black(); + fd.bg_color = UI_COL_BG; fd.bg_opa = LV_OPA_COVER; lv_draw_rect(layer, &fd, &box); @@ -255,7 +254,7 @@ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { ld.base.layer = layer; ld.text = seg; ld.text_static = 0u; /* 栈缓冲(同帧有效) */ - ld.color = lv_color_white(); + ld.color = UI_COL_FG; ld.ofs_x = 1; /* box.x1 = x0-1,+1 = x0(与框架行文本原点一致) */ ld.ofs_y = 0; /* 与行文本垂直对齐(text_dy = 0) */ lv_draw_label(layer, &ld, &box); @@ -346,18 +345,19 @@ static void nc_init_cb(void) { } /* - * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=net_cfg 绑定引用): * 列表页(5 行 1 页,动态值)+ 可编辑(edit_capable,还须修改权限) * + ← 拦截(编辑模式移字节光标;ESC 恒不可拦截) */ const content_page_def_t ui_page_net_cfg_desc = { + .id = "net_cfg", .title = "网口设置", .selectable = 1u, .key_intercept = 1u, .edit_capable = 1u, - .top_y = NC_TOP_Y, - .pitch = NC_PITCH, - .band_h = NC_BAND_H, + .top_y = UI_LIST_TOP, + .pitch = UI_ROW_PITCH, + .band_h = UI_ROW_BAND_H, .text_dy = 0, .left_x = NC_LEFT_X, .right_x = NC_RIGHT_X, diff --git a/Drivers/BSP/UI/ui_page_net_cfg.h b/Drivers/BSP/UI/ui_page_net_cfg.h index b323b84e..d85e0788 100644 --- a/Drivers/BSP/UI/ui_page_net_cfg.h +++ b/Drivers/BSP/UI/ui_page_net_cfg.h @@ -19,7 +19,7 @@ extern "C" { #endif -/* 页面描述符(框架描述符表 s_defs[] 引用) */ +/* 页面描述符(生成物 ui_page_registry.c 按菜单树 #page=net_cfg 注记绑定引用;.id = "net_cfg") */ extern const content_page_def_t ui_page_net_cfg_desc; #ifdef __cplusplus diff --git a/Drivers/BSP/UI/ui_page_password.c b/Drivers/BSP/UI/ui_page_password.c index 3054dfb0..1d4c287d 100644 --- a/Drivers/BSP/UI/ui_page_password.c +++ b/Drivers/BSP/UI/ui_page_password.c @@ -27,6 +27,8 @@ #include "ui_pwd_def.h" +#include "ui_tokens.h" + /* 私有宏定义区 */ /* @@ -45,7 +47,6 @@ #define PW_TITLE_Y 44 /* 标题放置 y(中上;0 = 框架默认,实机明显低于其余各页的 y=2) */ #define PW_ERR_Y 104 /* "密码输入错误"提示行顶 y(数字框行之下) */ -#define PW_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致;提示文本居中用) */ /* 静态变量定义区 */ @@ -130,16 +131,16 @@ static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { fd.bg_opa = LV_OPA_COVER; if (i == s_cur) { /* 光标框:反显(1bpp:white=暗底,black=亮字) */ - fd.bg_color = lv_color_white(); + fd.bg_color = UI_COL_SEL_BG; } else { /* 非光标框:亮底暗字 + 1px 暗边(1bpp:black=亮底,white=暗) */ - fd.bg_color = lv_color_black(); - fd.border_color = lv_color_white(); + fd.bg_color = UI_COL_BG; + fd.border_color = UI_COL_FG; fd.border_width = 1; } lv_draw_rect(layer, &fd, &box); - pw_draw_digit(layer, &box, s_digits[i], (i == s_cur) ? lv_color_black() : lv_color_white()); + pw_draw_digit(layer, &box, s_digits[i], (i == s_cur) ? UI_COL_SEL_FG : UI_COL_FG); } if (s_err != 0u) { @@ -152,16 +153,16 @@ static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { if (w > 0) { lv_area_t err_box; - err_box.x1 = (PW_SCREEN_W - w) / 2; + err_box.x1 = (UI_SCREEN_W - w) / 2; err_box.x2 = (int32_t)(err_box.x1 + w - 1u); err_box.y1 = PW_ERR_Y; - err_box.y2 = (int32_t)(err_box.y1 + 15u); /* 覆盖字体行高(13)+ 余量(同框架约定) */ + err_box.y2 = (int32_t)(err_box.y1 + UI_TEXT_BAND_H); /* 覆盖字体行高(13)+ 余量(同框架约定) */ if (pw_areas_intersect(&err_box, clip)) { lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = "密码输入错误"; ld.text_static = 1; - ld.color = lv_color_white(); + ld.color = UI_COL_FG; ld.ofs_x = 0; ld.ofs_y = 0; lv_draw_label(layer, &ld, &err_box); @@ -256,12 +257,13 @@ void ui_page_password_arm(void (*on_ok)(uint8_t perm)) { } /* - * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 `# @password=password` 指令出口引用): * 表单/操作页——selectable=0、无行数据,自绘内容 + 自有键; * 标题/装饰/页脚跟随实机(无"浅 V"、标题中上、无页脚); * key_intercept=1:← 键先由 key_cb 处理(移光标),ESC 恒不可拦截 */ const content_page_def_t ui_page_password_desc = { + .id = "password", .title = "用户密钥", .title_y = PW_TITLE_Y, .hide_deco = 1u, diff --git a/Drivers/BSP/UI/ui_page_password.h b/Drivers/BSP/UI/ui_page_password.h index 764c954e..12b852ff 100644 --- a/Drivers/BSP/UI/ui_page_password.h +++ b/Drivers/BSP/UI/ui_page_password.h @@ -18,8 +18,9 @@ #include "ui_content_page.h" /* - * 页面描述符(Flash const):用户密钥页 = CONTENT_PAGE_PASSWORD; - * 接入框架描述符表 s_defs[](定义于 ui_page_password.c) + * 页面描述符(Flash const):用户密钥页(描述符 .id = "password"); + * 由生成物 ui_page_registry.c 按菜单树 `# @password=password` 指令**单独出口** + * (密码页非节点绑定:任何 #P 叶子都进同一页,故不在 node→desc 映射里) */ extern const content_page_def_t ui_page_password_desc; @@ -30,7 +31,7 @@ extern const content_page_def_t ui_page_password_desc; * 由逻辑层实现"关页 + 按权限续进入"; * NULL = 一致时仅关页) * 返回值:无 - * 限定条件:由逻辑层在 uiTask 上下文、ui_content_page_open(CONTENT_PAGE_PASSWORD) + * 限定条件:由逻辑层在 uiTask 上下文、ui_content_page_open(ui_page_registry_password()) * 之前调用 * 函数说明:页面比对 ui_pwd_def.h 的两个编译期密码(先查修改:两值同设时授高权); * 每次进入重置为初始态(全 0、光标首框,与实机一致);页面模块不感知菜单 diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c new file mode 100644 index 00000000..540c05c9 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_registry.c @@ -0,0 +1,54 @@ +/* + * 模块名称:UI 页面注册表(自动生成) + * 模块功能:叶子节点 → 内容页描述符映射表 + 2 个查询 API(导航逻辑层消费) + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-15 + * 自动生成:本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 的 #page= 注记生成,请勿手改。 + * 新增一页:菜单树给叶子加 " #page=<模块名>" + 写 ui_page_<模块名>.c,重跑生成器 + */ +#include "ui_page_registry.h" + +#include "ui_page_ac.h" +#include "ui_page_dc.h" +#include "ui_page_device_info.h" +#include "ui_page_net_cfg.h" +#include "ui_page_password.h" +#include "ui_page_runlog.h" +#include "ui_page_selftest.h" + +const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { + { 1u, &ui_page_device_info_desc }, /* node 1 装置信息 */ + { 3u, &ui_page_ac_desc }, /* node 3 交流量 */ + { 4u, &ui_page_dc_desc }, /* node 4 直流量 */ + { 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */ + { 39u, &ui_page_selftest_desc }, /* node 39 自检记录 */ + { 40u, &ui_page_runlog_desc }, /* node 40 运行记录 */ +}; + +/* + * 函数功能:查叶子节点绑定的内容页描述符 + * 入口参数:node - 菜单叶子节点下标 uint8_t + * 返回值:页面描述符;未绑定返回 NULL const content_page_def_t * + * 限定条件:无(越界 node 不会命中任何条目,直接 NULL) + * 函数说明:线性扫描绑定表(条目 = 有内容页的叶子,个位数量级) + */ +const content_page_def_t *ui_page_registry_for(uint8_t node) { + for (uint8_t i = 0u; i < UI_PAGE_MAP_COUNT; i++) { + if (g_ui_page_map[i].node == node) { + return g_ui_page_map[i].desc; + } + } + return NULL; +} + +/* + * 函数功能:取密码页描述符(需密码叶子统一进这一页,非节点绑定) + * 入口参数:无 + * 返回值:密码页描述符 const content_page_def_t * + * 限定条件:无 + * 函数说明:取值来自 ui_menu_tree.txt 的 @password 指令(生成期解析,运行期零开销) + */ +const content_page_def_t *ui_page_registry_password(void) { + return &ui_page_password_desc; +} diff --git a/Drivers/BSP/UI/ui_page_registry.h b/Drivers/BSP/UI/ui_page_registry.h new file mode 100644 index 00000000..02bb5422 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_registry.h @@ -0,0 +1,56 @@ +/* + * 模块名称:UI 页面注册表头文件(自动生成) + * 模块功能:叶子节点 → 内容页描述符的映射表与查询 API;密码页非节点绑定,单独出口 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:王建锋 + * 创建日期:2026-10-15 + * 自动生成:本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 的 #page= 注记生成,请勿手改。 + * 新增一页:菜单树给叶子加 " #page=<模块名>" + 写 ui_page_<模块名>.c,重跑生成器 + * (双端注册 uvprojx/CMakeLists 亦由生成器自动写入) + */ +#ifndef UI_PAGE_REGISTRY_H +#define UI_PAGE_REGISTRY_H + +#include + +#include "ui_content_page.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* 注册表条目:一个绑定了内容页的菜单叶子(表序 = 节点下标升序) */ +typedef struct { + uint8_t node; /* 菜单叶子节点下标(g_menu_tab 下标) */ + const content_page_def_t *desc; /* 页面描述符(Flash const) */ +} ui_page_entry_t; + +#define UI_PAGE_MAP_COUNT 6u /* 绑定了内容页的叶子数 */ + +extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT]; + +/* + * 函数功能:查叶子节点绑定的内容页描述符 + * 入口参数:node - 菜单叶子节点下标 uint8_t + * 返回值:页面描述符;该叶子未绑定内容页返回 NULL const content_page_def_t * + * 限定条件:node < UI_MENU_TAB_COUNT + * 函数说明:取代旧 ui_menu.c 手写 case 分发——映射的唯一真源是 ui_menu_tree.txt, + * 表序变化不会造成"下标硬编码错位"(线性扫描:绑定页是个位数量级) + */ +const content_page_def_t *ui_page_registry_for(uint8_t node); + +/* + * 函数功能:取密码页描述符(需密码叶子统一进这一页) + * 入口参数:无 + * 返回值:密码页描述符;菜单树未声明 @password 指令时返回 NULL const content_page_def_t * + * 限定条件:无 + * 函数说明:密码页**不是**节点绑定的(任何 #P 叶子都进同一页),故不进 node→desc 映射, + * 由本接口单独出口;取值来自 ui_menu_tree.txt 的 `# @password=<模块名>` 指令 + */ +const content_page_def_t *ui_page_registry_password(void); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_REGISTRY_H */ diff --git a/Drivers/BSP/UI/ui_page_runlog.c b/Drivers/BSP/UI/ui_page_runlog.c index df3e44e9..1b5214ba 100644 --- a/Drivers/BSP/UI/ui_page_runlog.c +++ b/Drivers/BSP/UI/ui_page_runlog.c @@ -16,6 +16,8 @@ #include "ui_page_runlog.h" +#include "ui_tokens.h" + #include "sys_log.h" /* 静态变量定义区 */ @@ -45,7 +47,7 @@ static uint16_t rl_count_cb(void) { /* * 函数功能:逐行取数(偶数行 = 时间戳、奇数行 = 描述;值列恒空) * 入口参数:row - 全局行下标 uint16_t - * name - 名输出缓冲(框架栈缓冲 CP_ROW_BUF_LEN=32,含 NUL) char * + * name - 名输出缓冲(框架栈缓冲 UI_BUF_STRLEN=32,含 NUL) char * * value - 值输出缓冲(同上;本页恒写空串) char * * 限定条件:页面打开中(框架绘制/按键上下文,uiTask) */ @@ -79,21 +81,22 @@ static void rl_refresh(uint16_t row, char *name, char *value) { } /* - * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): - * 动态列表页——每记录 2 行、8 行/页 = 4 条/页;band_h=13(同 AC/DC 约定: - * selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见); + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=runlog 绑定引用): + * 动态列表页——每记录 2 行、UI_LIST_ROWS 行/页 = 4 条/页;band_h=UI_ROW_BAND_H(同 AC/DC + * 约定:selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见); * 时间戳 19 ASCII ≈114px < 屏宽可容 */ const content_page_def_t ui_page_runlog_desc = { + .id = "runlog", .title = "运行记录", .selectable = 1u, - .top_y = 24, - .pitch = 14, - .band_h = 13, + .top_y = UI_LIST_TOP, + .pitch = UI_ROW_PITCH, + .band_h = UI_ROW_BAND_H, .text_dy = 0, - .left_x = 7, - .right_x = 101, - .rows_per_page = 8u, + .left_x = UI_LIST_LEFT_X, + .right_x = UI_LIST_RIGHT_X, + .rows_per_page = UI_LIST_ROWS, .count_cb = rl_count_cb, .refresh = rl_refresh, }; diff --git a/Drivers/BSP/UI/ui_page_runlog.h b/Drivers/BSP/UI/ui_page_runlog.h index 555626de..3febc822 100644 --- a/Drivers/BSP/UI/ui_page_runlog.h +++ b/Drivers/BSP/UI/ui_page_runlog.h @@ -16,8 +16,8 @@ #include "ui_content_page.h" /* - * 页面描述符(Flash const):运行记录页 = CONTENT_PAGE_RUN_LOG; - * 接入框架描述符表 s_defs[](定义于 ui_page_runlog.c) + * 页面描述符(Flash const):运行记录页(描述符 .id = "runlog"); + * 由生成物 ui_page_registry.c 按菜单树 #page=runlog 注记绑定引用(定义于 ui_page_runlog.c) */ extern const content_page_def_t ui_page_runlog_desc; diff --git a/Drivers/BSP/UI/ui_page_selftest.c b/Drivers/BSP/UI/ui_page_selftest.c index 0a12a767..2579171e 100644 --- a/Drivers/BSP/UI/ui_page_selftest.c +++ b/Drivers/BSP/UI/ui_page_selftest.c @@ -16,6 +16,8 @@ #include "ui_page_selftest.h" +#include "ui_tokens.h" + #include "sys_log.h" /* 静态变量定义区 */ @@ -45,7 +47,7 @@ static uint16_t st_count_cb(void) { /* * 函数功能:逐行取数(偶数行 = 时间戳、奇数行 = 故障描述;值列恒空) * 入口参数:row - 全局行下标 uint16_t - * name - 名输出缓冲(框架栈缓冲 CP_ROW_BUF_LEN=32,含 NUL) char * + * name - 名输出缓冲(框架栈缓冲 UI_BUF_STRLEN=32,含 NUL) char * * value - 值输出缓冲(同上;本页恒写空串) char * * 限定条件:页面打开中(框架绘制/按键上下文,uiTask) */ @@ -79,21 +81,22 @@ static void st_refresh(uint16_t row, char *name, char *value) { } /* - * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): - * 动态列表页——每记录 2 行、8 行/页 = 4 条/页;band_h=13(同 AC/DC 约定: - * selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见); + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=selftest 绑定引用): + * 动态列表页——每记录 2 行、UI_LIST_ROWS 行/页 = 4 条/页;band_h=UI_ROW_BAND_H(同 AC/DC + * 约定:selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见); * 时间戳 19 ASCII ≈114px < 屏宽可容 */ const content_page_def_t ui_page_selftest_desc = { + .id = "selftest", .title = "自检记录", .selectable = 1u, - .top_y = 24, - .pitch = 14, - .band_h = 13, + .top_y = UI_LIST_TOP, + .pitch = UI_ROW_PITCH, + .band_h = UI_ROW_BAND_H, .text_dy = 0, - .left_x = 7, - .right_x = 101, - .rows_per_page = 8u, + .left_x = UI_LIST_LEFT_X, + .right_x = UI_LIST_RIGHT_X, + .rows_per_page = UI_LIST_ROWS, .count_cb = st_count_cb, .refresh = st_refresh, }; diff --git a/Drivers/BSP/UI/ui_page_selftest.h b/Drivers/BSP/UI/ui_page_selftest.h index 354c0927..2f139002 100644 --- a/Drivers/BSP/UI/ui_page_selftest.h +++ b/Drivers/BSP/UI/ui_page_selftest.h @@ -16,8 +16,8 @@ #include "ui_content_page.h" /* - * 页面描述符(Flash const):自检记录页 = CONTENT_PAGE_SELFTEST_LOG; - * 接入框架描述符表 s_defs[](定义于 ui_page_selftest.c) + * 页面描述符(Flash const):自检记录页(描述符 .id = "selftest"); + * 由生成物 ui_page_registry.c 按菜单树 #page=selftest 注记绑定引用(定义于 ui_page_selftest.c) */ extern const content_page_def_t ui_page_selftest_desc; diff --git a/Drivers/BSP/UI/ui_standby.c b/Drivers/BSP/UI/ui_standby.c index 2d510b16..0cd42d20 100644 --- a/Drivers/BSP/UI/ui_standby.c +++ b/Drivers/BSP/UI/ui_standby.c @@ -17,6 +17,8 @@ #include "lvgl.h" #include "ui_menu_view.h" +#include "ui_style.h" +#include "ui_tokens.h" #define DBG_TAG "[UI_STANDBY]" #include "dbg_log.h" @@ -25,11 +27,9 @@ /* * 布局常量(设备像素;参考 mockups/待机显示页面.jpg): - * 6 路电流值行自 y=28 起、行距 18(末行 y=118..130);分隔线 y=132;底部时钟 y=140 居中 + * 屏幕宽高取令牌 UI_SCREEN_W/H;6 路电流值行自 y=28 起、行距 18 + * (末行 y=118..130);分隔线 y=132;底部时钟 y=140 居中 */ -#define SB_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ -#define SB_SCREEN_H 160 /* 屏幕高(px) */ - #define SB_CH_COUNT 6u /* 电流通道数 */ #define SB_ROW_Y0 28 /* 首数据行顶 y */ #define SB_PITCH 18 /* 行距(相邻行槽顶间距) */ @@ -55,21 +55,8 @@ static lv_obj_t *s_prev_scr; /* 进入待机前的活动屏(ENTER 还原 static lv_obj_t *s_prev_focus; /* 进入待机前组内焦点对象 */ static ui_standby_time_cb s_time_cb; -/* flat:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的 card 样式(同 ui_content_page.c) */ -static const lv_style_const_prop_t sb_flat_props[] = { - { .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } }, - { .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } }, - { .prop = LV_STYLE_RADIUS, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } }, - { .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } }, - LV_STYLE_CONST_PROPS_END -}; - -LV_STYLE_CONST_INIT(sb_style_flat, sb_flat_props); +/* flat 样式(透明、无边框/圆角/内边距,剥离 mono 主题 card)取自共享样式表 + * ui_style.c:与 ui_content_page / ui_menu_view 共用同一 const 对象,不再本地重复定义 */ /* 函数定义区 */ @@ -99,15 +86,15 @@ static void sb_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y lv_draw_label_dsc_t ld; area.x1 = 0; - area.x2 = SB_SCREEN_W - 1; + area.x2 = UI_SCREEN_W - 1; area.y1 = y; - area.y2 = y + 15; /* 覆盖字体行高(13)+ 余量 */ + area.y2 = y + UI_TEXT_BAND_H; /* 覆盖字体行高(13)+ 余量 */ lv_draw_label_dsc_init(&ld); ld.base.layer = layer; ld.text = text; - ld.color = lv_color_white(); - ld.ofs_x = (SB_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2; + ld.color = UI_COL_FG; + ld.ofs_x = (UI_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2; ld.ofs_y = 0; lv_draw_label(layer, &ld, &area); } @@ -128,7 +115,7 @@ static void sb_draw_cb(lv_event_t *e) { lv_draw_label_dsc_t ld; strip.x1 = 0; - strip.x2 = SB_SCREEN_W - 1; + strip.x2 = UI_SCREEN_W - 1; strip.y1 = row_y; strip.y2 = (int32_t)(row_y + SB_PITCH - 1u); if (!sb_areas_intersect(&strip, &layer->_clip_area)) { @@ -139,7 +126,7 @@ static void sb_draw_cb(lv_event_t *e) { ld.base.layer = layer; ld.text = s_ch_name[i]; ld.text_static = 1; - ld.color = lv_color_white(); + ld.color = UI_COL_FG; ld.ofs_x = SB_LABEL_X; ld.ofs_y = 0; lv_draw_label(layer, &ld, &strip); @@ -148,7 +135,7 @@ static void sb_draw_cb(lv_event_t *e) { ld.base.layer = layer; ld.text = "="; ld.text_static = 1; - ld.color = lv_color_white(); + ld.color = UI_COL_FG; ld.ofs_x = SB_EQ_X; ld.ofs_y = 0; lv_draw_label(layer, &ld, &strip); @@ -157,7 +144,7 @@ static void sb_draw_cb(lv_event_t *e) { ld.base.layer = layer; ld.text = s_ch_value[i]; ld.text_static = 1; - ld.color = lv_color_white(); + ld.color = UI_COL_FG; ld.ofs_x = SB_VALUE_X; ld.ofs_y = 0; lv_draw_label(layer, &ld, &strip); @@ -169,13 +156,13 @@ static void sb_draw_cb(lv_event_t *e) { lv_draw_rect_dsc_t fd; line.x1 = 8; - line.x2 = SB_SCREEN_W - 9; + line.x2 = UI_SCREEN_W - 9; line.y1 = SB_LINE_Y; line.y2 = SB_LINE_Y; if (sb_areas_intersect(&line, &layer->_clip_area)) { lv_draw_rect_dsc_init(&fd); fd.base.layer = layer; - fd.bg_color = lv_color_white(); + fd.bg_color = UI_COL_FG; fd.bg_opa = LV_OPA_COVER; lv_draw_rect(layer, &fd, &line); } @@ -186,9 +173,9 @@ static void sb_draw_cb(lv_event_t *e) { lv_area_t t; t.x1 = 0; - t.x2 = SB_SCREEN_W - 1; + t.x2 = UI_SCREEN_W - 1; t.y1 = SB_TIME_Y; - t.y2 = (int32_t)(SB_TIME_Y + 15); + t.y2 = (int32_t)(SB_TIME_Y + UI_TEXT_BAND_H); if (sb_areas_intersect(&t, &layer->_clip_area)) { char ts[24]; @@ -307,8 +294,8 @@ int ui_standby_init(uint32_t idle_timeout_ms, ui_standby_time_cb time_cb) { return -1; } lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); - lv_obj_set_size(s_cont, SB_SCREEN_W, SB_SCREEN_H); - lv_obj_add_style(s_cont, &sb_style_flat, 0); + lv_obj_set_size(s_cont, UI_SCREEN_W, UI_SCREEN_H); + lv_obj_add_style(s_cont, &ui_style_flat, 0); lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); lv_obj_add_event_cb(s_cont, sb_key_cb, LV_EVENT_KEY, NULL); lv_obj_add_event_cb(s_cont, sb_draw_cb, LV_EVENT_DRAW_MAIN, NULL); diff --git a/Drivers/BSP/UI/ui_style.c b/Drivers/BSP/UI/ui_style.c new file mode 100644 index 00000000..c609449b --- /dev/null +++ b/Drivers/BSP/UI/ui_style.c @@ -0,0 +1,36 @@ +/* + * 模块名称:UI Shared Style (UI 共享样式表) + * 模块功能:flat 常量样式的属性数组与样式对象定义(声明见 ui_style.h); + * 消费者 = ui_menu_view.c / ui_content_page.c / ui_standby.c + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-15 + * 修改记录: + * v1.0 2026-10-15 王建锋 创建初始版本(U3:原三处逐字相同的定义合并到此单一来源) + */ + +/* 头文件包含区 */ +#include "ui_style.h" + +/* 静态变量定义区 */ + +/* + * flat 属性数组:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的 + * card 样式。用常量样式(LV_STYLE_CONST_INIT):属性存静态数组、零 LV_MEM 池分配; + * 动态样式首次 set_prop 会从池分配 values_and_props 且永不释放(x64 实测 88 B/个)。 + * 属性项与顺序 = 合并前三处定义的公共内容(三者原本逐字相同)。 + */ +static const lv_style_const_prop_t s_flat_props[] = { + { .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } }, + { .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } }, + { .prop = LV_STYLE_RADIUS, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } }, + LV_STYLE_CONST_PROPS_END +}; + +LV_STYLE_CONST_INIT(ui_style_flat, s_flat_props); diff --git a/Drivers/BSP/UI/ui_style.h b/Drivers/BSP/UI/ui_style.h new file mode 100644 index 00000000..7146daa3 --- /dev/null +++ b/Drivers/BSP/UI/ui_style.h @@ -0,0 +1,38 @@ +/* + * 模块名称:UI Shared Style (UI 共享样式表) + * 模块功能:跨模块复用的 LVGL 常量样式单一定义处——目前只有 flat(背景透明 + + * 边框/圆角/内边距全 0,用于剥离 mono 主题给普通 lv_obj 附加的 card 样式); + * 菜单显示层 ui_menu_view、内容页框架 ui_content_page、待机页 ui_standby + * 三处共用同一 const 对象(此前各自定义一份逐字相同的 9 项属性数组, + * = .rodata 里三份重复副本); + * 常量样式属性存静态数组、零 LV_MEM 池分配——动态样式首次 set_prop 会从 + * 池分配 values_and_props 且永不释放(x64 实测 88 B/个),故全仓禁用 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-15 + * 修改记录: + * v1.0 2026-10-15 王建锋 创建初始版本(U3:合并三处逐字重复的 flat 常量样式定义) + */ +#ifndef UI_STYLE_H +#define UI_STYLE_H + +#include "lvgl.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * flat 样式:背景透明 + 边框/圆角/内边距全 0。 + * 用途:mono 主题会给普通 lv_obj 附加 card 样式(可见边框与内边距),自绘容器 + * 必须先剥掉它,才能拿到干净的整屏绘制面。 + * 定义在 ui_style.c;样式在 LVGL 内按指针被对象引用,三处共享同一 const 对象 + * 不改变任何绘制结果(属性值与原三份逐字节相同)。 + */ +extern const lv_style_t ui_style_flat; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_STYLE_H */ diff --git a/Drivers/BSP/UI/ui_tokens.h b/Drivers/BSP/UI/ui_tokens.h new file mode 100644 index 00000000..10977ea3 --- /dev/null +++ b/Drivers/BSP/UI/ui_tokens.h @@ -0,0 +1,68 @@ +/* + * 模块名称:UI Design Tokens (UI 设计令牌层 L0) + * 模块功能:UI 视觉骨架的唯一取值来源——屏幕几何、内容页骨架(标题/页脚/文本 + * 条带/选中带内缩)、列表页原型(首行顶/行距/带高/两列 x/每页行数)、 + * 滚动条宽、文本缓冲长度、1bpp 语义色、UI 任务节拍与待机阈值; + * 各页面/框架文件只引用本头令牌,不再自带同值字面量 + * (治理背景见 docs/问题记录/项目审查与优化建议报告.md §6.3 L0); + * 纯取值定义,不含任何逻辑;改一处即全局生效,改前必须跑 ui-verify 像素回归 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-10-15 + * 修改记录: + * v1.0 2026-10-15 王建锋 创建初始版本(U1:收编散落各 UI 文件的布局/颜色/缓冲魔数, + * 纯替换、零行为变化;像素与替换前逐位一致) + */ +#ifndef UI_TOKENS_H +#define UI_TOKENS_H + +/* + * 依赖说明: + * 1. 屏幕几何(UI_SCREEN_W / UI_SCREEN_H / UI_FB_SIZE)的**权威定义在 ui_port.h** + * (显示端口,MCU 与模拟器两端共用),本头复用而不重复定义——避免同名宏二次定义。 + * 2. 语义色宏展开为 LVGL 取色函数调用,故引用方须已包含 "lvgl.h" + * (UI 各 .c 均经 ui_content_page.h / ui_menu_view.h 等间接包含)。 + */ +#include "ui_port.h" + +/* ── 内容页骨架(框架 ui_content_page.c 与各自绘页共用)────────────────────── */ + +#define UI_TITLE_Y 2 /* 标题放置 y(居中;CJK 字形顶距 +1 → 墨迹自 y=3 起) */ +#define UI_PAGE_LINE_Y 143 /* 页脚"第 X 页,共 Y 页"放置 y */ +#define UI_TEXT_BAND_H 15 /* 单行文本条带下延量(字体行高 13 + 余量):y2 = y + 本值 */ +#define UI_BUF_STRLEN 32 /* 动态行 / 页脚文本缓冲长度(含 NUL;超出行带即被裁切) */ +#define UI_SEL_BAND_INSET_L 2 /* 选中白带左内缩(x1 = 本值) */ +#define UI_SEL_BAND_INSET_R 3 /* 选中白带右内缩(x2 = 屏宽 - 本值,即止于 x=156) */ +#define UI_FOOTER_X 6 /* 页脚文本左起 x(左对齐留白,与实机一致) */ + +/* ── 列表页原型(交流量/直流量/运行记录/自检记录/网口设置共用一套骨架)──────── */ + +#define UI_LIST_TOP 24 /* 首行槽顶 y */ +#define UI_ROW_PITCH 14 /* 行距(相邻行槽顶间距) */ +#define UI_ROW_BAND_H 13 /* 选中白带高(= 字体行高;selectable 页须 >0 否则选中行不可见) */ +#define UI_LIST_LEFT_X 7 /* 左列 x(名) */ +#define UI_LIST_RIGHT_X 101 /* 右列 x(值) */ +#define UI_LIST_ROWS 8u /* 每页可见行数 */ + +/* ── 菜单显示层 ───────────────────────────────────────────────────────────── */ + +#define UI_SCROLLBAR_W 4 /* 自动滚动条宽(px;行超框高时出现) */ + +/* ── 1bpp 语义色(LV_COLOR_FORMAT_I1)─────────────────────────────────────── */ + +/* + * 本屏只有黑白两值,但保留语义名:换色深时只改本段即可平移,不必回改各绘制点。 + * 极性(实测,勿凭直觉改):文本/分隔线/边框一律取 white(1bpp 下即"暗墨"), + * black 是底;选中行反显 = 白底黑字,故 SEL 前景取 black、SEL 背景取 white。 + */ +#define UI_COL_FG lv_color_white() /* 墨:文本 / 分隔线 / 边框 / 滚动条 */ +#define UI_COL_BG lv_color_black() /* 底:黑 */ +#define UI_COL_SEL_FG lv_color_black() /* 选中行字色(反显后为黑字) */ +#define UI_COL_SEL_BG lv_color_white() /* 选中行带底色(反显后为白带) */ + +/* ── UI 任务与待机策略 ────────────────────────────────────────────────────── */ + +#define UI_TASK_TICK_MS 10 /* uiTask 主循环周期:LVGL 推进步长 = osDelay 时长 */ +#define UI_STANDBY_IDLE_MS 60000u /* 无操作进入待机屏的阈值(ms) */ + +#endif /* UI_TOKENS_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index f54f8627..2983d55f 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -3501,6 +3501,16 @@ 1 ..\Drivers\BSP\UI\ui_font_cn_12.c + + ui_page_registry.c + 1 + ..\Drivers\BSP\UI\ui_page_registry.c + + + ui_style.c + 1 + ..\Drivers\BSP\UI\ui_style.c + diff --git a/docs/UI设计/UI_中文字体生成与配置.md b/docs/UI设计/UI_中文字体生成与配置.md index ee93180b..d16d3467 100644 --- a/docs/UI设计/UI_中文字体生成与配置.md +++ b/docs/UI设计/UI_中文字体生成与配置.md @@ -167,3 +167,20 @@ OK 43614 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c | `ui_cn_chars.txt`(工程根) | 实际显示汉字清单记录(可删) | | `tools/font_gen/extract_cn.py` | 提取字符集脚本(见第 3 节) | | `tools/font_gen/make_font.py` | 生成字体脚本,自动定位 lv_font_conv 并处理 `.ttc`(见第 4 节) | + + +--- + +## 重生成前的安全流程(2026-10-15 实测沉淀) + +字体是**生成物**,但它进的是**像素基线 + 台架可见效果**。工具链一旦漂移,损失的是"保留字形静默变化"——而这种变化**编译和链接都不报错**。下面四步是必做项,不是可选: + +1. **仓库外 dry-run**:先在临时目录搭一个最小生成器根(拷 `tools/font_gen` 两脚本 + 目标 `ui_cn_chars.txt`;`make_font` 的 ROOT 由自身位置推导,所以整目录拷走即可独立运行),在沙箱里跑完整生成链,确认 rc / coverage / 字节数 / BOM / LF / `EF BF BD` 全部正确,**才允许在仓库内跑正路**。 +2. **确定性实证**:沙箱产物与仓库产物做 sha256 对比。一致即证明生成器确定性,之后任何 A/B 对比都可以用"快照 + 字节级还原",不必重新生成。 +3. **逐码位结构对账(关键一步)**:解析新旧字体 `.c` 的 `glyph_bitmap`(按 `/* U+XXXX */` 注释切块)与 `glyph_dsc`,断言三件事——**删除集 == 预期字符集**、**其余码位位图字节零差异**、**度量多重集不变**。这一步把"golden 应该不变"**从期望升级为结构证明**,golden 跑出来只是复核。换工具版本(例如新装 `lv_font_conv`)时此项**必做**——它能照出"工具版本漂移导致保留字形悄悄变化",那才是真正会刷爆 golden 的事故源。 +4. **A/B 必须同树**:并行开发时**千万不要拿别人给的"绿灯基线数"当 before**——那个数可能已经含了本次改动(实测照出过这种情况,见下)。正确做法:`git show HEAD:<字体路径>` 字节级换回旧产物 → 在**同一棵当前树**重跑 before → sha256 校验换回新产物跑 after。 + +> **口径:字体源文件字节数 ≠ ROM 降幅。** 源文件 ~45 KB 编到 `.constdata` 只有 ~4.5 KB,剔 3 个字形的真实降幅是**百字节量级**(实测 `ER_IROM1` −80 B、字体对象 RO −81 B,与"3 字 × ~25 B 位图 + 3 × 12 B 描述符、对齐后"预测吻合)。**报尺寸变化一律用 `.map` 的 `ER_IROM1` / 对象 RO**,不要把源文件字节数当成 Flash 增量。 + +> **保留字形(`KEEP_EXTRA`)的存在理由**:`ui_cn_chars.txt` 里除了"源码在用",还可能有**刻意保留**的字——当前为 异 / 常 / 片 / 芯,因为它们曾在 2026-09-14 `9eb1bd63` 被写进中文自检描述落盘 `/log/selftest.log`(定长 64 B 记录跨固件升级持久),删字形会使历史故障记录渲染空白且不可逆。`scripts/check_font_charset.py` 把这类字**按必需处理**:既照"缺字",也照"保留字被静默剔除"(严格模式下 FAIL 并逐字打印保留理由)。 +> **所以"只跑 `extract_cn` 重新生成字符集"会把保留字洗掉**——这是本流程最容易犯的错,钩子就是为了挡住它。`--strict` 的判据因此是"**未知**冗余 = 0",而不是"冗余 = 0"。 diff --git a/docs/代码规范/pre-commit使用说明.md b/docs/代码规范/pre-commit使用说明.md index 699febc8..dc4b6bfb 100644 --- a/docs/代码规范/pre-commit使用说明.md +++ b/docs/代码规范/pre-commit使用说明.md @@ -25,8 +25,9 @@ git commit ├─ cubemx-survival CubeMX 重生成存活审计(易失定制清单 + 基线漂移) ├─ watchdog-unit-test 看门狗判定状态机主机侧单测(L0.5) ├─ test-suite-switch 测试套件开关=产品构建(防测试固件静默提交) - ├─ registration-check 双端注册反向校验(uvprojx + CMakeLists,N-23) - └─ mem-budget RAM 预算 92% 阈值门禁(N-27 三件套,2026-10-12) + ├─ registration-check 双端注册反向校验(uvprojx + CMakeLists,N-23;含 UI 页面描述符表接线,U4) + ├─ mem-budget RAM 预算 92% 阈值门禁(N-27 三件套,2026-10-12) + └─ font-charset UI 字符集差集检查(防缺字白框,U4,2026-10-15) └─ commit-msg(.git/hooks/commit-msg) └─ gitlint 校验 commit message 格式 ``` @@ -52,6 +53,7 @@ git commit | scripts/check_test_suite.py | 随仓库提交 | 测试套件开关门禁(纯 Python,无依赖) | | scripts/check_registration.py | 随仓库提交 | 双端注册反向校验(纯 Python,无依赖) | | scripts/ram_baseline.py | 随仓库提交 | RAM 预算 92% 门禁 + 趋势台账(纯 Python,无依赖) | +| scripts/check_font_charset.py | 随仓库提交 | UI 字符集差集检查(纯 Python,无依赖;复用 `tools/font_gen/` 两个生成器的提取与 cmap 解析规则) | > **PATH 要求**:cppcheck 安装后需在 PATH 中可找到(winget 自动写入 User PATH,但当前终端不会立即生效,需**重开终端**);装在非 PATH 位置时可用 `DTU_CPPCHECK` 环境变量指向 `cppcheck.exe` 完整路径(显式设置但无效 → 硬报错,不静默回退)。 @@ -360,17 +362,18 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法 | **为什么需要** | 锁定 60 s / 解锁 / 清零 / 回绕是**防口令爆破契约核心**——板端每验一条要真等 60 s、回绕要跑满 49.7 天;纯函数设计(tick 注入)就是为让这些分支主机侧毫秒级穷举 | | **运行器** | `test/host/ftp_auth/run_unit_test.py`(MSVC `cl.exe`,L0.5) | -### 2.18 registration-check — 双端注册反向校验(2026-10-12 加,N-23) +### 2.18 registration-check — 双端注册反向校验 + UI 页面接线(2026-10-12 加,N-23;U4 补强 2026-10-15) | 项 | 说明 | |---|---| | **触发条件** | 任意 `.c` 文件 + `*.uvprojx` + `CMakeLists.txt` + 脚本自身变动(pre-commit 4.x 无 `always: true`,`files:` 必须覆盖全部触发面) | | **处理方式** | **阻止提交**(存在**新**缺口或幽灵注册即拦截);存量缺口走白名单只 `[WARN]` 不拦 | | **检查对象** | 扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表:`MDK-ARM/STM32F407-Demo.uvprojx` 的 `` + `simulator/CMakeLists.txt` 的显式源列表(先展开 `${NETCFG_DIR}` 等变量) | -| **三类缺口** | ① 存在但未进 uvprojx(固件不编译)② 存在但未进 CMakeLists(模拟器不编译,**仅当本应进模拟器时**才算)③ 注册了但文件不存在(**幽灵注册**,改名/删除后残留) | +| **四类缺口** | ① 存在但未进 uvprojx(固件不编译)② 存在但未进 CMakeLists(模拟器不编译,**仅当本应进模拟器时**才算)③ 注册了但文件不存在(**幽灵注册**,改名/删除后残留)④ UI 页面模块未接入描述符表(见下行,U4) | +| **UI 页面门禁(U4,2026-10-15 补)** | UI 页面的"注册"有**两层**:**构建注册**(uvprojx + CMakeLists,①② 类)与**框架注册**(`ui_content_page.h` 枚举 + `ui_content_page.c` 的 `s_defs[]` 描述符表)。①②③ 原本只照得出前者——实测"页面已进两端构建但没接 `s_defs[]`"时钩子**全绿**:desc 无人引用被链接器整段剥离,两端编译链接全过、菜单永远打不开这一页,是又一形态"写了却从不生效"的死代码。故补 ④:凡 `ui_page_*.c` 定义 `ui_page__desc`,该符号必须在 `Drivers/BSP/UI/` 内(除该页自身 `.c`/`.h`)被引用,否则 `[GAP]` FAIL。**新增 UI 页面必须过本钩子**即此机械口径;U3 落地生成式 `ui_page_registry.c` 后规则依旧成立(不定义自身 desc 的 `ui_page_*.c` 不判为页面模块)。⚠ 本钩子照不出**枚举值漏加**(`ui_content_page.h` 的 `content_page_id_t`)与**菜单树漏挂节点**——前者是 `s_defs[]` 下标依据(漏加则表项编译不过,由 `@build` 兜底),后者归 U3 生成器 + golden 回归 | | **豁免机制** | MCU-only 文件不进模拟器是**正确**的(模拟器另有 `*_sim.c` 替身或无对应)。豁免用**显式清单 + 每行理由**(命名模式 `*_mcu.c` + 个别点名),**不整目录一刀切**(N-23 教训正是目录级盲区);唯一目录级豁免 `Src/`(CubeMX 生成,MCU 专属)→ 见脚本 `SIM_EXEMPT` | | **存量白名单** | `KNOWN_GAPS` 登记 N-23 存量缺口(`App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c`,Lead 处置中)→ 命中打 `[WARN]` 不拦,只对新缺口 FAIL,故钩子可立即上线不拦现有提交;`--strict` 关白名单供审计 | -| **为什么需要** | 新 `.c` 必须手动注册两处才会被编译;历史上 `rs485_task.c`/`lftpd_log.c` **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效(出处审查报告 N-06 残余) | +| **为什么需要** | 新 `.c` 必须手动注册两处才会被编译;历史上 `rs485_task.c`/`lftpd_log.c` **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效(出处审查报告 N-06 残余);④ 的出处是报告 §6.5 U4"把注册检查算作 UI 页面门禁一环"(2026-10-15 实测 ①②③ 对未接线页面全绿,故补规则) | | **运行器** | `scripts/check_registration.py`(纯标准库;退出码 0=一致 / 1=有缺口 / 2=环境错误) | ### 2.19 mem-budget — RAM 预算 92% 阈值门禁(2026-10-12 加,N-27 三件套) @@ -429,6 +432,20 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法 | **为什么需要** | N-12 / N-13 两类缺陷真机**极难复现**:is_free 误判要恰好撞上"页首 0xFF、后段有数据"的页形态,越界地址要哨兵从别处漏进来;两者发作后都只表现为"某次复位后丢数据",无从定位。移植层是纯逻辑 + 假芯片,能钉死真机极难复现的越界/误判路径,且"越界不产生任何芯片访问"这类判据在板端**根本拿不到观测面** | | **运行器** | `test/host/nand_port/run_unit_test.py`(MSVC `cl.exe`,L0.5);套件说明见 `test/host/README.md` | +### 2.24 font-charset — UI 字符集差集检查(2026-10-15 加,报告 §6.5 U4 残余) + +| 项 | 说明 | +|---|---| +| **触发条件** | `Drivers/BSP/UI/` 下任意文件(含字体 `ui_font_cn_12.c` 自身)、`App/task/ui_task.(c\|h)`、`ui_cn_chars.txt`、`tools/font_gen/`(两个生成器)或本脚本自身变动(pre-commit 4.x 无 `always: true`,`files:` 必须覆盖全部触发面) | +| **处理方式** | **阻止提交**(存在"白框方向"缺口即 FAIL);字符集**冗余**字 `[WARN]` 不拦(只费 Flash 不致白框,`--strict` 升级为 FAIL 供审计/瘦身排期) | +| **判定口径** | 两段链条:**A.** UI 源码在用字符 vs `ui_cn_chars.txt`——源码有、txt 无 → FAIL(字体必然无此字形);txt 有、源码无 → WARN。**B.** `ui_cn_chars.txt` vs `ui_font_cn_12.c` 的 cmap——txt 有、字体无 → FAIL(`lv_font_conv` 对缺字形/低码位**静默丢字**)。A 过 + B 过 ⟹ 字体 ⊇ 源码每个在用字 | +| **单一规则源** | 本脚本**不自创第二套解析规则**:字符集提取复用 `tools/font_gen/extract_cn.py` 的 `SCAN` 清单 + `collect()` 状态机(跳注释/字符常量、只取双引号串内非 ASCII),cmap 覆盖判定复用 `tools/font_gen/make_font.py` 的 `verify_coverage()`(含稀疏 `unicode_list` 与补回字形的单码位条目),importlib 直载零副作用——生成期校验与提交期校验**同源**,杜绝两套规则漂移(§5.5 同类教训) | +| **典型报错** | `[GAP] 字体缺字 1 个(在字符集、不在 Drivers/BSP/UI/ui_font_cn_12.c 的 cmap -> lv_font_conv 静默丢字,上机白框): 压(U+538B)`,附 font-regen 四步修复提示 | +| **修复方式** | 按 **font-regen** skill 重跑 `extract_cn.py` → `make_font.py` → diff 检查,`ui_cn_chars.txt` 与 `ui_font_cn_12.c` 随代码一起提交 | +| **为什么需要** | `ui_font_cn_12.c` 是**子集**字体,只含 txt 列出的码位;`make_font.py` 的 `verify_coverage` 只在**生成期**校验"txt ⊆ cmap"——改完 UI 字符串忘了重跑 font-regen 就提交,缺字上机渲染为空白(白框),无日志无报错、只能肉眼截图发现。font-regen skill 的约定此前**纯靠自律**,本钩子把它机械化(U4 三钩子至此齐:golden ✅、字符集差集 ✅、注册检查算作 UI 门禁 ✅ → §2.18 ④) | +| **首次落地实测(牙齿)** | 注入均打在**临时副本**(`--chars` / `--font` 覆盖,不碰仓库文件):① txt 删 ▶ → FAIL 点名 `▶(U+25B6)`;② 字体副本删 U+25B6 单码位 cmap 条目 → FAIL 同字;③ 字体副本删 U+538B 的稀疏 `unicode_list` 偏移 → FAIL 点名 `压(U+538B)`;基线 PASS。落地时 txt 内有 **7 个历史冗余字**(上/常/异/片/电/芯/重,源码已不用)→ 仅 WARN 不拦,下次 font-regen 顺带瘦身 | +| **运行器** | `scripts/check_font_charset.py`(纯标准库;`--chars` / `--font` 指临时副本做注入测试、`--strict` 升级冗余字;退出码 0=通过 / 1=白框缺口 / 2=环境错误) | + --- ## 3. 日常工作流 @@ -556,7 +573,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env | 文件 | 位置 | 说明 | |---|---|---| -| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**34 个钩子**,2026-10-15 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,B8 频率测量单测见 §2.21,B9 SOE 动作记录单测见 §2.22,N-21 dhara NAND 移植层单测见 §2.23,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 16 个) | +| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**35 个钩子**,2026-10-15 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验(含 U4 补强的 UI 页面描述符表接线 ④)见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,B8 频率测量单测见 §2.21,B9 SOE 动作记录单测见 §2.22,N-21 dhara NAND 移植层单测见 §2.23,U4 残余字符集差集检查见 §2.24,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 16 个) | | `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) | | `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) | | `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 | @@ -567,6 +584,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env | `scripts/check_cubemx_survival.py` | scripts/ | CubeMX 重生成存活审计 | | `scripts/check_lv_conf_sync.py` | scripts/ | 两份 `lv_conf.h` 特性子集一致性(比对生效值 + 无效宏名 + 重复定义) | | `scripts/check_test_suite.py` | scripts/ | 测试套件开关门禁(防测试固件静默提交) | +| `scripts/check_font_charset.py` | scripts/ | UI 字符集差集检查(复用 `tools/font_gen/` 提取与 cmap 解析规则,防缺字白框) | | `docs/代码规范/嵌入式C语言代码规范_V1.0.md` | docs/代码规范/ | 代码规范原文(S1~S19 规则来源) | | `docs/代码规范/cppcheck使用说明.md` | docs/代码规范/ | cppcheck 钩子详解(规则/配置/抑制) | | `docs/文档规范.md` | docs/ | 文档体系规范(链接形态/命名/门禁要求) | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 9c46fa3f..38ff57b4 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 232,280 B (226.84 KB) | 1,048,576 B (1024 KB) | 22.2% | +| ER_IROM1 | 232,052 B (226.61 KB) | 1,048,576 B (1024 KB) | 22.1% | | RW_IRAM1 | 75,232 B (73.47 KB) | 131,072 B (128 KB) | 57.4% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **233,416 B (227.9 KB)** | 1,048,576 B (1024 KB) | **22.3%** | +| **Flash (ROM 合计)** | **233,192 B (227.7 KB)** | 1,048,576 B (1024 KB) | **22.2%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index e0b5c06b..97e30ac1 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 232,280 B | 1,048,576 B | 22.2% | 参考 | +| ER_IROM1 (Flash) | 232,052 B | 1,048,576 B | 22.1% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | | RW_IRAM1 | 75,232 B | 131,072 B | 57.4% | PASS | @@ -24,7 +24,7 @@ |---|---|---|---| | 网络 | 13,690 B | 12 KB | ⚠ 超 1.4 KB | | 存储 | 7,056 B | 12 KB | OK | -| UI | 30,412 B | 16 KB | ⚠ 超 13.7 KB | +| UI | 30,416 B | 16 KB | ⚠ 超 13.7 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | | 应用 | 14,911 B | 8 KB | ⚠ 超 6.6 KB | | 其它 | 9,184 B | — | 参考 | diff --git a/docs/测试说明/测试开发标准流程.md b/docs/测试说明/测试开发标准流程.md index cd9ba4a1..31fc09b2 100644 --- a/docs/测试说明/测试开发标准流程.md +++ b/docs/测试说明/测试开发标准流程.md @@ -382,7 +382,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 - `--update` 有**误刷保护**(先跑文本层断言,失败即中止)——**未有意改视觉而 FAIL = bug,修代码,禁止刷参考掩盖**; - 刷新参考后必须重跑 `regen_ref_png.py` 重生成目检 PNG(否则旧 PNG 看似乱码); - 门禁常量漂移:先确认新值合理(残留分解见 `菜单页面设计.md` §9.5)→ 更新常量 + 在 `UI_自动化测试流程.md` 变更记录登记; -- **UI 新增中文文案提交前必须先跑 font-regen**(extract_cn → make_font → diff 检查):字符集 `ui_cn_chars.txt` 缺字则上机显示空白。 +- **UI 新增中文文案提交前必须先跑 font-regen**(extract_cn → make_font → diff 检查):字符集 `ui_cn_chars.txt` 缺字则上机显示空白——**2026-10-15 起该约定已入门禁**(`font-charset` 钩子:源码在用字符 ∉ txt 或 txt ∉ 字体 cmap → 提交 FAIL,详见 `../代码规范/pre-commit使用说明.md` §2.24)。 --- @@ -625,6 +625,7 @@ CI 就静默停摆(2026-10-11 实测 run 16~21 共 12 个 job 零日志)。 | 日期 | 变更 | |---|---| +| 2026-10-15 | **U4 残余入门禁(钩子总数 34→35)**:① 新增 `font-charset` 钩子(`scripts/check_font_charset.py`)——UI 源码在用字符 vs `ui_cn_chars.txt` vs `ui_font_cn_12.c` cmap 两段差集,白框方向 FAIL、冗余字 WARN;提取与 cmap 解析**复用** `tools/font_gen/` 两个生成器同一套规则(importlib 直载),牙齿实测=临时副本注入缺字三例全 FAIL 点名到字。② `registration-check` 补第 ④ 类缺口——`ui_page_*.c` 定义的 `ui_page__desc` 未接入 `ui_content_page.c` 的 `s_defs[]` 描述符表即 FAIL(实测原 ①②③ 对"进了两端构建但菜单打不开"的页面全绿)。§5.3 font-regen 铁律补"已入门禁"指针;详解见 `../代码规范/pre-commit使用说明.md` §2.18/§2.24 | | 2026-10-15 | **新增 `test/host/nand_port/`(dhara NAND 移植层 L0.5,N-21 / 报告 N-12+N-13)**:`#include` 真实 `nand_ftl.c` + 真实 `Lib/dhara/{journal,map}.c`,芯片侧换成 **RAM 假芯片**(32 块 × 64 页 × 2048 B,两段式读缓存 + NOR 叠加编程 + 双编程违规计数 + 访问次数记账),149 断言覆盖 is_free 整页扫描、六入口越界守卫(`-1` + `DHARA_E_CORRUPT_MAP` + 零芯片访问 / 零标坏块)、故障注入与 dhara map"写入 → 重新 resume"场景;**内联一份"只看页首 64 B"的旧算法做对照**,证明断言能咬人。挂 pre-commit 钩子 `nand-port-unit-test`(第 16 个主机侧单测,钩子总数 33→34)。§4.1 登记运行器、§4.2.1 台账新增该模块并给"其余驱动不适格"补例外说明(驱动不适格 ≠ 驱动之上的胶水层不适格) | | 2026-10-12 | **§9 瘦身:机器侧内容迁出**(远程 CI 文档独立成 `docs/CI/`)。§9.2 两个 job 的**步骤级细节**并入 `docs/CI/远程CI完整指南.md` §5.5、§9.4 部署前置表并入其 §0.2;本文 §9 只保留**测试分层视角**的内容(CI 覆盖哪些层、两个 job 各守哪层、触发与 paths-ignore 语义、本地等价 `ci_local.py`、"CI 不替代本地门禁")。§9.1 两个 🔴 块压缩为结论 + 指针(机制根因与排障口径归 `docs/CI/`)。相关文档表两行 CI 条目合并为一行指向 `docs/CI/README.md` | | 2026-10-10 | §4.2.1 台账:`Drivers/BSP/NETCFG/net_cfg_legacy.c` 由 `⬜ 候选` 改为 **`⛔ 已删除`**——旧档 NCFG 迁移整体移除(`net_cfg_legacy.{c,h}` 删除、`cfg_store_legacy_path()` 撤销;旧档 `/cfg/netcfg.bin` 不读、不删、不迁移,配置回落工厂默认),该测试候选随之撤销 | diff --git a/docs/环境与部署/并行任务工作树方案.md b/docs/环境与部署/并行任务工作树方案.md index 396ab9bb..e01e2a09 100644 --- a/docs/环境与部署/并行任务工作树方案.md +++ b/docs/环境与部署/并行任务工作树方案.md @@ -397,7 +397,7 @@ docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内 | 主题 | 去处 | |---|---| -| 提交门禁 34 个钩子 | `docs/代码规范/pre-commit使用说明.md` | +| 提交门禁 35 个钩子 | `docs/代码规范/pre-commit使用说明.md` | | 自动化测试与台架流程 | `docs/测试说明/自动化测试流程.md` | | 文档写作规范(编码/链接/登记) | `docs/文档规范.md` | | 工具路径环境变量(换机) | `docs/环境与部署/本地工具路径环境变量方案.md` | diff --git a/docs/问题记录/项目审查与优化建议报告.md b/docs/问题记录/项目审查与优化建议报告.md index 3ef3e5a2..65e29c8c 100644 --- a/docs/问题记录/项目审查与优化建议报告.md +++ b/docs/问题记录/项目审查与优化建议报告.md @@ -28,7 +28,7 @@ | 模块设计 | **A−** | 看门狗(IWDG + 心跳位图 + 4 个 Fault 现场 + `.noinit`)、就绪屏障、卷锁、FTP 鉴权、`Error_Handler` 自恢复、ADC 全链均已落地;**原列的 dhara 移植层两处缺陷(N-12/N-13)与日志逐条开关(N-16)已于 2026-10-15 修复** → `GD5F2GQ5UE_Trap_Records.md` Trap 07、`../架构设计/STM32F4-Base存储架构说明.md` §5.3、`../子系统设计/运行与自检记录设计说明.md` §4.4/§4.5 | 驱动层无统一错误码约定(S-5)、RS485 无协议栈(随 B2 暂缓) | | 内存使用 | **B+** | 堆迁 CCM + 栈/堆三面观测常开 + `mem-budget` 92% 门禁 + 预算台账与趋势基线;**R-9 已落地(2026-10-15)→ SRAM1 由 74.9% 降到 56.1%;同日移植层与日志修复 +1.6 KB → 现 **57.4%(余 55,840 B)**,不再是卡点** | **内存优化簇已整簇暂缓**(用户裁定 2026-10-15):R-2 后半(待迁 29,570 B → CCM,只差缩堆 56→32 KB)与 R-3 / R-4 / R-6 不再排期,理由与重开条件见 §5.3;**当前真正的约束是 CCM 只剩 7,904 B**——加任务 / 抬栈 / 加堆会先在这里撞墙,届时按 §5.3 重开;MicroLIB 512 B C 堆仍无机械检查(§5.5) | | Flash 使用 | **A−** | F-1/F-2/F-3 落地省 64,576 B → 占用 **22.2%(232,280 B)** | F-4/F-5/F-6 ≈10 KB + 编译时长(§5.4) | -| UI 标准化 | **B** | golden 回归已建(`simulator/tests/` 四脚本,含池水位门禁) | 无令牌层 / 无组件库 / 无页注册表(手写 `case` 仍在)、**27 页**未落地、golden 与字符集差集未挂门禁(§6) | +| UI 标准化 | **B+** | W6 第一波已落地(2026-10-15):**L0 设计令牌层** `ui_tokens.h` 建成、**L2 页面注册表** `ui_page_registry.{c,h}` 由生成器产出并**删除了手写 `case` 分发**、`font-charset` 钩子 + `registration-check` 第 ④ 类接线校验上线、字体子集瘦身 −80 B | 无组件库(L1 `ui_w_*` 未建)、**目标页未落地**(现存 7 个 `ui_page_*.c`)、**golden 两脚本仍未挂 pre-commit**(`font-charset` 已挂,golden 未挂)、`ui_w_form` 推迟 U5(§6) | ### 0.3 主线建议 @@ -429,12 +429,29 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap ## 6. UI 界面标准化实现方案 +> ⚠ **本节"建议 / 目标结构 / 草图"不可照抄——落地前必须逐条对代码核实**(2026-10-15 立此口径)。 +> +> 本节是**体系评审视角的建议**,不是实现规格。启动 W6 后按 §6 实际执行 U1 / U3 / U4 三包,**四次证伪**: +> +> | # | 报告写的 | 代码实况 | 照抄的后果 | +> |---|---|---|---| +> | 1 | §6.3 L0 清单要新建 `UI_SCREEN_W/H` | `ui_port.h:21-22` **早有同名宏** | 同名宏二次定义 | +> | 2 | `UI_COL_FG = LV_COLOR_BLACK` / `UI_COL_BG = LV_COLOR_WHITE` | LVGL 9.3 **无这两个宏**(只有 `lv_color_black()/white()` 函数);且**极性反了**——代码里所有墨色恒用 `lv_color_white()`,选中行才是黑字 | 先编译失败,改完若按报告极性则**整屏反色** | +> | 3 | §6.5 U1 行"27 页未落地" | 现存 `ui_page_*.c` 实测 **7 个**;27 是**目标页数**不是文件数 | 工作量与拆包按错数排 | +> | 4 | §6.3 生成器草图 `@page=deadzone` | **密码页不是节点绑定的**(任何 `#P` 叶子进同一页),注册表必须单独出口 | 做不出能进密码页的注册表 | +> +> 另有 §6.3 L0 清单 **9 条取值/存在性错误**(行距 13→实为 14、列表顶 15→实为 24、滚动条宽 2→实为 4、`UI_PAD_X 3` 不存在、`UI_ROW_H 12` 不存在、`UI_TITLE_H 11` 不存在、`UI_STYLE_FLAT` 在 U1 阶段做不了……)**逐条明细见 §6.3 更正块**,此处不重复。 +> +> **执行规则**:① 按本节动手前,先 grep 代码核一遍建议值与建议结构是否已存在、取值是否对;② 发现不符**一律以代码为准**,不要"先按报告做再改";③ 把差集**回报并记进本节**,不要静默修正(否则下一个人还会再踩);④ 本节"发现"栏(现状描述、问题定位、成本量化)经复核**仍然可信**,不可照抄的是**建议栏**。 +> +> **同一规律在本报告之外也复现过**:§4 的 N-13 建议用了本仓裁剪版 dhara 里**不存在**的 `DHARA_E_UNSUPPORTED`(照抄编译失败)。所以这不是 UI 章节的局部问题,而是**"建议"这一体裁的通病**——写建议时没有对代码核。后续任何按本报告排期的工作包,任务卡都应显式写一句"以代码实况为准,报告建议值不可直接采用"。 + ### 6.1 现状(框架已有,但未"闭环") | 层 | 现状 | 评价 | |----|------|------| -| 内容页 | `ui_content_page.c` 描述符表 `s_defs[]` **7 页**(DEVICE_INFO/AC/DC/PASSWORD/NET_CFG/RUN_LOG/SELFTEST_LOG) | ❌ `ui_menu_tab.h:28` 42 节点(**叶子 33 个**),有内容页的叶子 **6 个**(PASSWORD 非叶子映射);`menu_content_page_for()`(`ui_menu.c:118-128`)仍是手写 `case 1u/3u/4u/30u/39u/40u:` | -| 设计令牌 | **无**(每页自带 `CP_SCREEN_W/H 160`、`CP_TITLE_Y 2`、`CP_PAGE_LINE_Y 143`、`CP_ROW_BUF_LEN 32` 等布局常量) | ❌ 魔法数字分散在各页,`Drivers/BSP/UI/ui_tokens.h` 不存在(U1 未办) | +| 内容页 | 描述符 `content_page_def_t` 仍是**唯一**页面描述符(刻意不新建第二套);节点→页面映射由**生成器产出** `ui_page_registry.c`,`{node, desc}` 表 + `ui_page_registry_for()` + `ui_page_registry_password()` | ✅(2026-10-15 U3)**手写 `menu_content_page_for()` 已删除**,仓库内仅存 2 处历史说明注释;密码页按代码实况**单独出口**(非节点绑定),不进 `node→desc` 映射 | +| 设计令牌 | `Drivers/BSP/UI/ui_tokens.h` **已建**(2026-10-15 U1,20 个宏),框架 + 7 页 + `App/task/ui_task.c` 全部接入;**屏幕几何复用 `ui_port.h` 既有 `UI_SCREEN_W/H`**,不重复定义 | ✅ 纯替换验收:`@build` 0/0 且 Program Size 逐字节不变、golden 逐像素一致、池水位未变。⚠ 落地时修正本报告 L0 清单 9 处取值/存在性错误(见 §6.3 更正块) | | golden 回归 | `simulator/tests/` 四脚本:`check_menu_rows.py`(菜单 8 状态全像素)、`check_content_pages.py`(内容页黄金参考逐字节 + 文本断言 + **LVGL 池水位门禁**)、`regen_ref_png.py`、`check_remo_sink.py` | 🟡 **有基线但未入门禁**:两个脚本未挂 pre-commit / `ui-verify` | ### 6.2 新增一个页面要走多少步(当前) @@ -476,6 +493,17 @@ graph LR #define UI_COL_SEL_FG UI_COL_BG #define UI_COL_SEL_BG UI_COL_FG /* 样式:只用 const 描述符(动态 lv_style 会每次泄漏 88 B,见 ui_content_page.c) */ + +> ⚠ **上面这份 L0 清单与代码实况有 9 处不符,U1 落地时已按代码为准修正**(2026-10-15 实测,`ui-tokens` 报回)。**照抄会翻车的两条**: +> ① **`UI_SCREEN_W/H` 不是新建**——`ui_port.h:21-22` 早有同名宏,在 `ui_tokens.h` 再定义一次就是同名宏二次定义;实际做法是 `ui_tokens.h` 只 include `ui_port.h` 复用,并把散落的 7 处 `160`(`CP_/MENU_/SB_/DECO_/PW_SCREEN_`、推流协议 `UI_W/H`)收敛到这一个来源。 +> ② **`UI_COL_FG = LV_COLOR_BLACK` / `UI_COL_BG = LV_COLOR_WHITE` 两处都错**——本仓 LVGL 9.3 **没有** `LV_COLOR_BLACK/WHITE` 宏(全库 0 处定义),只有函数 `lv_color_black()/lv_color_white()`,照抄直接编不过;而且**极性反了**:代码里所有墨(文本 / 分隔线 / 边框 / 滚动条)恒用 `lv_color_white()`,选中行才是黑字。按本报告写会把整屏反色。落地定义:`FG=white`、`BG=black`、`SEL_FG=black`、`SEL_BG=white`。 +> +> 其余 7 条是取值/存在性错误(报告值 → 代码实况):`UI_TITLE_H 11` → 代码无此值,真实重复量是 `+15` 文本条带(建为 `UI_TEXT_BAND_H 15`);`UI_ROW_PITCH 13` → 列表行距实为 **14**(13 是字体行高/选中带高);`UI_ROW_H 12` → 代码无 12 作行高,选中白带高 **13**(`UI_ROW_BAND_H`);`UI_LIST_TOP 15` → 列表首行顶实为 **24**;`UI_SCROLLBAR_W 2` → 实为 **4**;`UI_PAD_X 3` → 不存在"统一内边距 3",那个 3 是选中白带**右内缩**(建为 `UI_SEL_BAND_INSET_R 3` 配 `_L 2`);**`extern const lv_style_const_prop_t UI_STYLE_FLAT[]` 在 U1 做不了**——共享 `extern` 数组需新增 `.c` 落定义 → 必须双端注册(U1 禁碰注册文件),退化成头里 `static const` 又会在"包含但未使用"的 TU 上招 `#177-D` 破 0 警,已并入 U3。 +> +> **教训**:本报告的 UI 章节是**体系评审视角的建议**,不是实现规格。落地前必须逐条对代码核一遍——本次 9 条里若照抄,两条编译失败、一条整屏反色。U3/U5 执行时同样以代码为准,发现与本节不符**直接按代码做并在回报里列出**。 +> +> ⚠ **U3 开工侦察又补两条**(2026-10-15,`ui-tokens`):① **密码页不是节点绑定的**——任何 `#P` 叶子都进同一个密码页,所以注册表除 `node→desc` 映射外**必须单独出口一个"密码页"入口**;本节生成器草图的 `@page=deadzone` 没覆盖这条,照抄会做不出能进密码页的注册表。② **`ui_w_form` 从 U3 推迟到 U5**(Lead 裁定):全仓只有 **N=1** 个表单实例(`ui_page_net_cfg.c`),且它的"表单"实质是"5 行地址列表 + 逐字节编辑光标"、**并未走配置引擎的段注册表**,与本节设想的"cfg 段/键绑定 + 十进制/IP 输入约束"**并不同形**。从单一实例反推接口等于猜,而这是要动像素的重构——**重复三次再抽象**。U3 范围因此收敛为「注册表 + 删手工分发 + 共享 flat 样式」三件(这才是 U5 能并行的真前提);`ui_w_form` 等 U5 有 2~3 个真实 form 实例可对照时再抽。 +> 同批裁定:**不新建 `ui_page_desc_t`**——代码里 `content_page_def_t`(title/count_cb/refresh/draw_cb/key_cb/edit_capable…,7 个页面全在用)已是唯一描述符,再造一套等于一个页面两套描述符;注册表只加 `ui_page_entry_t{node, desc}` 做节点绑定。**不加 archetype 枚举**(无消费者的元数据,真正需要它的是 U5 的"由原型自动实例化页面")。 extern const lv_style_const_prop_t UI_STYLE_FLAT[]; ``` @@ -542,10 +570,10 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea | 步骤 | 内容 | 交付判据 | 状态 | |------|------|---------|------| -| U1 | `ui_tokens.h` 令牌化(纯替换,不改行为) | `@build` PASS + 模拟器截图与替换前逐像素一致 | ❌ 未办。🟡 **验收条件如今可机械执行**——golden 基线已建,"逐像素一致"从口头判据变成可跑断言 | +| U1 | `ui_tokens.h` 令牌化(纯替换,不改行为) | `@build` PASS + 模拟器截图与替换前逐像素一致 | ✅ **已落地(2026-10-15)**:`ui_tokens.h` 20 个宏 + 15 个文件接令牌。**零行为变化证据**:`@build` 0 错 0 警且 `Program Size` 与改前**逐字节相同**(Code=213540 / RO=18740 / RW=1136 / ZI=131520);golden 菜单 8 状态全像素一致 + 内容页 **11 页逐字节一致**,且比对对象是改前备份的 sha256 基线(**不是 `--update` 刷出来的**);池水位 5368 / 5448 B 未变。⚠ 落地时修正本节 L0 清单 9 处与代码不符(见上) | | U2 | `ui_w_records`(合并 runlog/selftest) | 两页行为不变,代码 −1 个文件 | ❌ 未办(两份结构几乎相同的代码仍在) | -| U3 | `ui_w_form` + `ui_page_desc_t` 原型化 + 生成器产注册表 | 手工 `menu_content_page_for()` 删除;新增一页只需"改树 + 加实例文件" | ❌ 未办(`ui_page_registry.c` / `gen_page_registry.py` 均不存在) | -| U4 | golden + 字符集检查 + 注册检查入 pre-commit | 三个新 hook 全绿 | 🟡 **1/3 落地**:golden 已建;❌ 字符集差集、❌ 注册检查未入门禁(注册检查钩子已存在,缺的是把它算作 UI 门禁的一环) | +| U3 | `ui_w_form` + `ui_page_desc_t` 原型化 + 生成器产注册表 | 手工 `menu_content_page_for()` 删除;新增一页只需"改树 + 加实例文件" | ✅ **已落地(2026-10-15)**:`ui_page_registry.{c,h}` + `ui_style.{c,h}`(原"U1 做不了"的共享 flat 样式一并落)+ 扩 `gen_menu.py` 产注册表**并自动写 `uvprojx`/`CMakeLists.txt`**;手工 `menu_content_page_for()` 已删。**实测单位成本:新增一页 = 3 处人工**(新 `.c` + 新 `.h` + 菜单树那一行加 ` #page=<名>`)**+ 1 条生成器命令,手工编辑注册文件 0 处**;生成器**双向对账**(增 `registered` / 删 `pruned`)且双向校验死代码。验收:`@build` 0/0、golden 菜单 8 状态 + 内容页 11 页逐字节一致、池水位 5368/5448 B 未变、`ER_IROM1` 232,052 B(较已提交 HEAD −228 B)、SRAM1 零增量。⚠ **本报告原口径"8 处降到 1 处"须修正**:那个 1 只在"1 处对既有文件的修改"意义下成立,页面自身 2 个文件本就要写,真实改动处数是 **3** | +| U4 | golden + 字符集检查 + 注册检查入 pre-commit | 三个新 hook 全绿 | 🟡 **2/3 落地(2026-10-15)**:`font-charset` 新钩子(字符集差集,防缺字白框)+ `registration-check` 补第 ④ 类"UI 页面描述符表接线"检查。钩子数 34 → **35** 。钩子数 34 → **35**。⚠ **修正我自己上一版的错**:此处原写"3/3 落地",是把"golden 脚本早已存在"误当成"golden 已进自动门禁"。实况是**两个 golden 脚本至今未挂 pre-commit**(一直在人工跑,无自动门禁),故改回 **2/3**,缺口单列为待办。⚠ **`--strict` 的验收口径已改**:判据是"**未知**冗余 = 0"而非"冗余 = 0"——`ui_cn_chars.txt` 里 异/常/片/芯 4 字属**刻意保留**(2026-09-14 `9eb1bd63` 写入的中文自检描述曾落盘 `/log/selftest.log`,定长 64 B 记录跨固件持久,删字形会使历史故障行渲染空白),按 `check_registration.py` 的 `KNOWN_GAPS` 同构做法进带理由的保留清单;上/电/重 3 字已证从未出现在任何字符串字面量(仅注释),照删。为几百字节牺牲已落盘审计记录的可读性不划算——同一权衡口径见 §5.3 内存簇暂缓裁定 | | U5 | 用原型批量落剩余 **27 页**(每页 ≤30 行) | 菜单树全绿、无 `CONTENT_PAGE_COUNT` 哨兵命中 | ❌ 未动 | > ✅ **2026-10-15 用户裁定启动**(覆盖 2026-10-11 的「本轮不排期」,登记簿 U 簇 / W6):已开智能体团队推进。**并行度受注册文件约束**——新增一个 `.c` 必须同时改 `MDK-ARM/STM32F407-Demo.uvprojx` 与 `simulator/CMakeLists.txt` 这两个共享文件,所以任何时刻只允许一个成员碰它们。实际排法:**第一波 U1(令牌层,不新增 .c)∥ U4 残余(只动 scripts/ 与门禁文档)真并行** → U3(原型化 + 生成器,独占注册文件,串行段)→ U2 → U5(页面批量,须等 U3 的生成器把注册自动化,否则无法并行)。⚠ 本节原写「27 页」是**目标页数**,现存 `ui_page_*.c` 实测只有 **7 个** → U5 开工前先对齐目标页清单差集。 @@ -573,7 +601,7 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea | `Drivers/BSP/UI/ui_w_{list,kv,records,form,msgbox,title}.{c,h}` | 组件库(需双端注册) | U2/U3 | ❌ 未建 | | `tools/menu_gen/gen_menu.py` 扩展 + `ui_page_registry.c` 生成 | 生成器 | U3 | ❌ 未建 | | `check_coding_standard.py` 加 `malloc(` 正则 | 门禁补强 | §5.5 第 5 条 | ❌ 未建 | -| 字符集差集检查 + golden 挂 pre-commit | 门禁补强 | U4 残余 | ❌ 未建 | +| `scripts/check_font_charset.py`(字符集差集)+ `registration-check` 第 ④ 类 UI 接线检查 | 门禁补强 | U4 残余 | ✅ **已建(2026-10-15)**——提取与 cmap 解析复用 `tools/font_gen/` 同一套规则(`importlib` 直载,零第二套规则);牙齿实测三例(缺字 / 字体缺码位 / 稀疏 cmap 缺偏移)均 FAIL 并点名到具体字符 | --- @@ -631,6 +659,8 @@ const ui_page_desc_t ui_page_deadzone = UI_FORM_PAGE("遥测死区", s_items_dea | **2026-10-15** | **删除 §0.2「未闭环问题」小节** | Top 2 修复销项后该节只剩指针与暂缓说明,而三条内容都已在别处**单点详述**:未闭环清单 → §0.4;内存簇暂缓 → §0.3 主线一与 §5.3 横幅;Top 2 修复证据 → 上一条变更记录与文末「已销项条目的记录去处」。按 `../文档规范.md`「同一事实只在一处详述」删除,不留空壳。**§0.3 / §0.4 编号保持不变、不重排**——`../任务规划/README.md` §2 前言引用本报告 §0.3,本报告内部另有 §0.4 交叉引用,重排会全断(与本表 2026-10-14 行第 ⑤ 条"报告自有编号不回改、不重排"同一口径)。文首「销项状态」行的指针由「§0.2 / §0.4」改为「§0.4」 | | **2026-10-15** | **删除 §0.3 主线建议第 1 条("内存这条线到此为止")** | 该条是同日暂缓裁定的**复述层**,同一事实已有三处单点详述:§0.4 内存两行(⏸ 暂缓)、§5.3 暂缓横幅(三条理由 + 保留常开的治理面 + 重开条件)、登记簿 N-27 与 `../内存管理/` 两份文档的横幅。按 `../文档规范.md`「同一事实只在一处详述」删复述、留权威。⚠ 上一条变更记录里"内存簇暂缓 → §0.3 主线一"这个去处**自此失效,以本行为准**(变更记录本身不改,它是历史)。§0.3 现只剩 UI 一条主线;标题「两条主线建议」→「主线建议」,**§0.3 编号不变**(`../任务规划/README.md` §2 前言引用的是编号不是标题) | | **2026-10-15** | **启动 W6 UI 标准化簇**(用户裁定,覆盖 2026-10-11 的「本轮不排期」) | §0.3 删掉内存主线后只剩 UI 这一条,随即启动。已按 §6.5 的 U1~U5 拆成 5 个工作包上共享任务板,第一波 **U1 令牌层 ∥ U4 门禁补强** 两名成员并行(写范围严格不相交:U1 只写 `Drivers/BSP/UI/` + `App/task/ui_task.c`,U4 只写 `scripts/` + 门禁与规范文档)。**并行度的真实约束不是任务数量,而是注册文件**:新增 `.c` 必须改 `uvprojx` + `CMakeLists.txt` 两个共享文件,同工作树两人同改必互相踩,故 U3(生成器/注册表)是必须串行的一段,U2/U5 排在它后面——U5 的页面批量要等 U3 把注册自动化之后才谈得上并行。同步 §6.5 注与本表 W6 行;`../任务规划/README.md` §2.0 W6 行同步。另修正交付物清单:移植层单测拟名 `test/host/dhara/` 实际落地为 `test/host/nand_port/`,状态 ❌ → ✅ | +| **2026-10-15** | **§6 顶部加"建议栏不可照抄"总警示** | 启动 W6 后按 §6 执行 U1/U3/U4,**四次证伪**:① §6.3 要新建 `UI_SCREEN_W/H` 而 `ui_port.h:21-22` 早有同名宏;② `LV_COLOR_BLACK/WHITE` 在 LVGL 9.3 不存在**且极性反了**(墨色实为 white),照抄先编译失败、改完整屏反色;③ "27 页未落地"是**目标页数**、现存 `ui_page_*.c` 实测 7 个;④ 生成器草图漏掉**密码页非节点绑定**。另有 L0 清单 9 条取值错,明细在 §6.3 更正块。**不再逐条打补丁**,改为节顶立总口径:动手前逐条 grep 对代码核、不符一律以代码为准、差集必须回报不得静默修正;并明确**"发现"栏可信、"建议"栏不可照抄**。同一规律在 §4 的 N-13(`DHARA_E_UNSUPPORTED` 不存在)已复现,故这不是 UI 章节局部问题,而是"建议"这一体裁的通病 → 后续任务卡须显式写"报告建议值不可直接采用" | +| **2026-10-15** | **W6 第一波落地:U1 + U3 验证通过并回填现状表** | Lead 亲自验证(成员被模型路由变更连续打断,最终停用):`@build` **0 错 0 警**;golden **菜单 8 状态全像素 + 内容页 11 页逐字节一致**、池水位 **5368 / 5448 B 与 U1 基线相同**、参考图 sha256 **差异 0 条**(证明无人用 `--update` 刷基线);`registration-check`(含第 ④ 类"页面必须被表引用")与 `font-charset` **Passed**;`ER_IROM1` **232,052 B**(较已提交 HEAD −228 B)、`RW_IRAM1` **75,232 B 零增量**;对 38 个在途修改文件逐个核对 **BOM / CRLF / U+FFFD 全部与 HEAD 一致**。**单位成本实测**(临时页 `zzprobe` 绑到 `遥信量` 叶子 → 只跑生成器 → 带页 `@build` 0/0 → 注册表出现 `{5u, &ui_page_zzprobe_desc}` → 回滚后 6 个产出文件的 `zzprobe` 残留计数均为 0、重编回基线)得出 **新增一页 = 3 处人工(新 `.c` + 新 `.h` + 树那一行加 ` #page=<名>`)+ 1 条生成器命令,手工编辑注册文件 0 处**,并确认生成器 `registered` / `pruned` **双向对账**(不只增、也减)。据此修正 §0.1 UI 行(B → **B+**)、§6.1「内容页」与「设计令牌」两行、§6.5 U3 行;并把 §6.5 U4 从我上一版**误写的 3/3 改回 2/3**(golden 两脚本至今未挂 pre-commit,"脚本已存在"≠"已进自动门禁");§6.5 U3 原口径"8 处降到 1 处"改为**真实 3 处**。**"分三笔提交"计划作废**:U1 与 U3 改同一批文件、文件内交叠,硬切会产生不可编译的中间态,改为整波一笔 | | **2026-10-14** | **清理:删除已完成与过时内容** | ① **已修复项按销项删除**(去处见下表):N-02(FTP 鉴权)、N-03c(`Error_Handler` 降级)、**N-11(DMA 可达性三层防线 + 规则族 R1~R4)**、S-3(注册反向校验)、P2-5/P2-6(SD2506 负温度与 WRTC)、P2-8(`NET_RECV_TIMEOUT_MS` 已删)、P2-9(`mkdir` 幂等重试)、§5.5 前三条(内存治理三件套)、交付物清单已建项。② **删除过时层**:2026-09-16 / 2026-10-11 双列基线表与"复核期间未提交改动"口径说明、旧"组合结果预估"表、逐条"2026-10-11 复核"批注、历史变更记录行——内存数字改由 `../内存管理/RAM预算台账.md` 与 `scripts/ram_baseline.tsv` 持有。③ **按 2026-10-14 `.map` 与代码重测**全部保留结论的数字(含 `lftpd_log.c` 已删除并入 `lftpd_auth.c`,N-06 残余只剩 `rs485_task.c` 一条刻意缺口)。④ **修正三处错误**:R-5(`configMAX_PRIORITIES` 56→32)**判定不可行**(CMSIS-RTOS2 `#error`);原 §7 把报告 S-5(驱动统一错误码)与登记簿 S-5(卷锁覆盖面)混为一谈;原 §5.5 第 4 条建议的 `_Static_assert` 形态在 AC5 上取不到链接期地址,已被"窗口断言 + 启动自检 + 运行时守卫"三层取代。⑤ 报告自有编号**不回改、不重排**(`../编号规范.md`) | ### 已销项条目的记录去处 diff --git a/scripts/check_font_charset.py b/scripts/check_font_charset.py new file mode 100644 index 00000000..fa1790f4 --- /dev/null +++ b/scripts/check_font_charset.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""check_font_charset.py - UI 字符集差集门禁(防"缺字显示白框",报告 §6.5 U4 残余)。 + +背景: + 本工程唯一中文字体 `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,提交期**无任何门禁** + 能拦住——缺字上机渲染为空白(白框),无日志无报错,只能靠肉眼截图发现。 + 本钩子把这条约定机械化(U4 三钩子之二,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(U4 残余,报告 §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 写入,落盘持久)", +} + + +def load_tool(name): + """按文件路径加载 tools/font_gen/.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()) diff --git a/scripts/check_registration.py b/scripts/check_registration.py index ea39e886..eccc6b4d 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -10,12 +10,20 @@ 历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。 -本脚本做什么(三类缺口): +本脚本做什么(四类缺口): 扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表: ① 存在但未进 uvprojx -> 固件不编译(死代码 / 漏注册) ② 存在但未进 CMakeLists -> 模拟器不编译(**仅当该文件本应进模拟器时**才是缺口; MCU-only 文件不进模拟器是**正确**的,见下方豁免) ③ 注册了但文件不存在(反向)-> 幽灵注册(改名/删除后残留的注册条目) + 另对 UI 页面模块(U4 门禁补强,2026-10-15,报告 §6.5): + ④ `Drivers/BSP/UI/ui_page_*.c` 定义了 `ui_page__desc`,但该符号在 UI 目录内 + 除自身 .c/.h 外无任何引用 -> 页面未接入 + `ui_content_page.c` 的 `s_defs[]` 描述符表:两端**编译 + 链接全过**(desc 无人引用,链接器整段剥离),菜单永远 + 打不开这一页 = 又一形态的"写了却从不生效"死代码。 + ①②③ 对此完全照不出(2026-10-15 实测),故补本类; + 即"新增 UI 页面必须过 registration-check"的机械口径。 豁免机制(关键): 模拟器只建模 **UI + 网络配置共享核心**;其余模块(MCU 驱动 / 固件任务 / 纯算法 / @@ -237,6 +245,66 @@ def parse_cmake(): return sources +# --- UI 页面模块 → 描述符表接线(④,U4 门禁补强)--- +UI_DIR_REL = "Drivers/BSP/UI" + + +def check_ui_page_wiring(): + """每个定义 `ui_page__desc` 的 `ui_page_*.c`,其 desc 必须被 UI 目录内其他文件引用。 + + UI 页面的"注册"有**两层**:构建注册(uvprojx + CMakeLists,①② 类)与框架注册 + (`ui_content_page.h` 枚举 + `ui_content_page.c` 的 `s_defs[]` 表)。漏了后者的页面 + 两端照常编译链接(desc 无引用 → 链接器剥离),①②③ 全绿但菜单打不开这一页。 + 判定:页面模块 = `ui_page_*.c` 中定义 `_desc =` 的文件(不定义 desc 的 + `ui_page_*.c` 不是页面模块——未来 U3 生成的 `ui_page_registry.c` 承载表引用, + 本规则依旧成立且不会把它误判为页面);引用面 = `Drivers/BSP/UI/` 下全部 .c/.h, + 排除该页自身 .c/.h(自身头文件里的 extern 声明不算接线)。 + """ + ui_dir = os.path.join(REPO_ROOT, UI_DIR_REL.replace("/", os.sep)) + if not os.path.isdir(ui_dir): + return [], 0 + + # 1) 页面模块清单:ui_page_*.c 中定义 `_desc =` 的 + pages = {} # sym -> repo-rel .c + for fn in sorted(os.listdir(ui_dir)): + if not (fn.startswith("ui_page_") and fn.endswith(".c")): + continue + try: + text = open(os.path.join(ui_dir, fn), encoding="utf-8-sig", errors="replace").read() + except OSError: + continue + sym = fn[:-2] + "_desc" + if re.search(r"\b%s\b\s*=" % re.escape(sym), text): + pages[sym] = "%s/%s" % (UI_DIR_REL, fn) + if not pages: + return [], 0 + + # 2) 引用面扫描:UI 目录全部 .c/.h,排除各页自身 .c/.h + referenced = {sym: False for sym in pages} + for dirpath, _dirnames, filenames in os.walk(ui_dir): + for fname in filenames: + if not fname.endswith((".c", ".h")): + continue + rel_p = to_repo_rel(os.path.join(dirpath, fname)) + try: + text = open(os.path.join(dirpath, fname), encoding="utf-8-sig", errors="replace").read() + except OSError: + continue + for sym, own_c in pages.items(): + if rel_p in (own_c, own_c[:-2] + ".h"): + continue + if re.search(r"\b%s\b" % re.escape(sym), text): + referenced[sym] = True + + gaps = [] + for sym, rel_c in sorted(pages.items()): + if not referenced[sym]: + gaps.append("[GAP] %s | 页面未接入描述符表(%s 在 %s/ 内除自身外无引用 -> " + "编译得过但菜单打不开,死代码;新增 UI 页面必须过本项)" + % (rel_c, sym, UI_DIR_REL)) + return gaps, len(pages) + + def main(): ap = argparse.ArgumentParser(description="双端注册反向校验(uvprojx + CMakeLists)") ap.add_argument("--strict", action="store_true", @@ -290,17 +358,21 @@ def main(): if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))): gaps.append("[GAP] %s | 幽灵注册(CMakeLists)" % f) + # ④ UI 页面模块未接入描述符表(框架注册,U4;①②③ 照不出的盲区) + ui_gaps, ui_pages = check_ui_page_wiring() + gaps.extend(ui_gaps) + # 输出 print("== 双端注册反向校验 ==") - print("扫描 .c: %d | uvprojx(扫描范围): %d | CMakeLists(扫描范围): %d" - % (len(scanned), len(uv_c), len(cmake_c))) + print("扫描 .c: %d | uvprojx(扫描范围): %d | CMakeLists(扫描范围): %d | UI 页面模块: %d" + % (len(scanned), len(uv_c), len(cmake_c), ui_pages)) for w in warns: print(w) for g in gaps: print(g) if gaps: - print("结果: FAIL(%d 个新缺口/幽灵注册,%d 个存量 WARN)" % (len(gaps), len(warns))) + print("结果: FAIL(%d 个新缺口/幽灵注册/页面未接线,%d 个存量 WARN)" % (len(gaps), len(warns))) return 1 if warns: print("结果: PASS(无新缺口;%d 个存量缺口已白名单 WARN,N-23 Lead 处置中)" % len(warns)) diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 70c76aa3..ed7dc1b5 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -3,3 +3,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-13 211456 18760 1104 154232 230216 22.0 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 2026-10-14 212296 18740 1112 154456 231036 22.0 98144 74.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 2026-10-15 213540 18740 1136 131520 232280 22.2 75232 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736 +2026-10-20 213472 18580 1140 131516 232052 22.1 75232 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736 diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index a4c1b642..f77a1a9d 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -94,6 +94,8 @@ set(UI_SOURCES ${UI_DIR}/ui_menu_tab.c ${UI_DIR}/ui_font_cn_12.c ${UI_DIR}/ui_net_sink.c + ${UI_DIR}/ui_page_registry.c + ${UI_DIR}/ui_style.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) diff --git a/tools/menu_gen/gen_menu.py b/tools/menu_gen/gen_menu.py index 07590306..1f910adb 100644 --- a/tools/menu_gen/gen_menu.py +++ b/tools/menu_gen/gen_menu.py @@ -1,12 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""生成菜单节点表 ui_menu_tab.c / ui_menu_tab.h(扁平"父子索引表",输出到 Drivers/BSP/UI/)。 +"""生成菜单节点表与页面注册表(输出到 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 同模式)。 +`#` 开头为注释;行尾可选注记,可叠加: + ` #P` 该叶子需密码(密码页按所输密码授权:查看/修改) + ` #page=<名>` 该叶子绑定内容页实例 ui_page_<名>.c(描述符 ui_page_<名>_desc) +文件级指令(写在注释行里): + `# @password=<名>` 需密码叶子统一进的密码页模块名(省略 = 无密码页) +密码值在 Drivers/BSP/UI/ui_pwd_def.h 编译期配置。 +本脚本读源、推导每个节点的 parent 下标,产出: + ① ui_menu_tab.c / .h 菜单节点扁平父子索引表(供 ui_menu.c 消费) + ② ui_page_registry.c/.h 叶子节点 → 页面描述符注册表(取代手写 case 分发) + ③ 双端注册:把 ui_page_registry.c 与各绑定页 .c 自动写进 + MDK-ARM/STM32F407-Demo.uvprojx 与 simulator/CMakeLists.txt 的显式列表 + (只做定点插入、不重排文件;已存在则不动 = 幂等) +并校验:声明了 #page 但实例文件/符号缺失 → 报错;定义了 desc 却无叶子绑定 → 报错。 +改菜单/加页面只需编辑 ui_menu_tree.txt + 写页面实例文件,然后重跑本脚本。 用法(仓库根目录或任意目录均可,脚本自动定位仓库根): python tools/menu_gen/gen_menu.py @@ -19,34 +29,85 @@ 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') +OUT_REG_H = os.path.join(ROOT, 'Drivers', 'BSP', 'UI', 'ui_page_registry.h') +OUT_REG_C = os.path.join(ROOT, 'Drivers', 'BSP', 'UI', 'ui_page_registry.c') +UI_DIR = os.path.join(ROOT, 'Drivers', 'BSP', 'UI') +UVPROJX = os.path.join(ROOT, 'MDK-ARM', 'STM32F407-Demo.uvprojx') +CMAKELISTS = os.path.join(ROOT, 'simulator', 'CMakeLists.txt') +UI_GROUP = 'Drivers/BSP/UI' # uvprojx 里的组名 ROOT_ID = 0xFF # 内部根标记(= MENU_ROOT=0xFF);与 8 位索引一致,映射到 C 的 MENU_ROOT 宏 MAX_DEPTH = 8 MAX_NODES = 255 # uint8_t 索引 + 哨兵 0xFF → 最多 255 个节点(下标 0..254) +STEM_RE = re.compile(r'^[a-z][a-z0-9_]*$') +# 行尾注记串:一个或多个 ` #KEY` / ` #KEY=VAL` +ANN_TAIL_RE = re.compile(r'((?:[ \t]+#[A-Za-z0-9]+(?:=[^\s#]+)?)+)$') +ANN_ONE_RE = re.compile(r'#([A-Za-z0-9]+)(?:=([^\s#]+))?') +# 文件级指令:`# @key=value`(整行注释,不与节点冲突) +DIRECTIVE_RE = re.compile(r'^[ \t]*#[ \t]*@([A-Za-z0-9_]+)[ \t]*=[ \t]*(\S+)') + + +def parse_annotations(stripped, raw): + """剥出行尾注记,返回 (节点名, {'P': bool, 'page': str|None})。 + + 节点名原样保留(含内部空格),只去掉尾部 ` #KEY[=VAL]` 串。 + 未知注记 / #P 带值 / #page 缺值 一律报错——注记写错必须当场失败, + 不能静默降级成"没密码/没页面"(那类错误两端编译全过、只在菜单里打不开页)。 + """ + m = ANN_TAIL_RE.search(stripped) + if not m: + return stripped, {'P': False, 'page': None} + name = stripped[:m.start()] + pwd, page = False, None + for tok in ANN_ONE_RE.findall(m.group(1)): + key, val = tok[0].upper(), tok[1] or None + if key == 'P': + if val is not None: + sys.exit('ERROR: #P 不接受取值: %r' % raw) + pwd = True + elif key == 'PAGE': + if val is None: + sys.exit('ERROR: #page 需要取值(页面模块名): %r' % raw) + if page is not None: + sys.exit('ERROR: #page 重复: %r' % raw) + if not STEM_RE.match(val): + sys.exit('ERROR: #page 取值须为小写字母/数字/下划线且以字母开头: %r(出现 %r)' + % (raw, val)) + page = val + else: + sys.exit('ERROR: 行尾注记仅支持 #P(需密码) 与 #page=<模块名>(绑定内容页): ' + '%r(出现 #%s;旧 #V/#M 类别注记已废弃,密码值改在 ui_pwd_def.h 配置)' + % (raw, tok[0])) + return name, {'P': pwd, 'page': page} def parse_tree(text): - """把缩进文本解析为 [(parent, name, depth), ...]。 + """把缩进文本解析为 (nodes, directives)。 + nodes = [(parent, name, depth, pwd, page), ...]; parent 为父节点下标,根节点为 ROOT_ID;子项 = parent 等于本下标的所有行(按表序)。 缩进单元取"最小非零缩进"(每级一个单元),据此把缩进换算成层级。 + directives = {小写指令名: 取值},来自 `# @key=value` 注释行。 """ lines = [] + directives = {} for raw in text.splitlines(): line = raw.rstrip() stripped = line.strip() - if not stripped or stripped.startswith('#'): + if not stripped: + continue + if stripped.startswith('#'): + dm = DIRECTIVE_RE.match(line) + if dm: + key = dm.group(1).lower() + if key in directives: + sys.exit('ERROR: 指令重复定义: @%s' % key) + directives[key] = dm.group(2) 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)) + name, ann = parse_annotations(stripped, raw) + lines.append((indent, name, ann)) if not lines: sys.exit('ERROR: ui_menu_tree.txt 无有效节点') @@ -59,7 +120,7 @@ def parse_tree(text): nodes = [] stack = [] # [(depth, index), ...] 当前各层的父节点下标 - for indent, name, pwd in lines: + for indent, name, ann in lines: depth = indent // unit if depth > MAX_DEPTH: sys.exit('ERROR: 层级过深(>%d): %r' % (MAX_DEPTH, name)) @@ -67,20 +128,20 @@ def parse_tree(text): stack.pop() parent = stack[-1][1] if stack else ROOT_ID idx = len(nodes) - nodes.append((parent, name, depth, pwd)) + nodes.append((parent, name, depth, ann['P'], ann['page'])) 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])) - return nodes + return nodes, directives 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 = [ '/*', @@ -147,7 +208,7 @@ def emit(nodes): '', 'const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {', ] - for i, (parent, name, depth, pwd) in enumerate(nodes): + for i, (parent, name, depth, pwd, _page) 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)) @@ -239,25 +300,353 @@ def emit(nodes): return '\n'.join(h), '\n'.join(c), menus, max_depth +def has_children(nodes, idx): + """节点是否有子项(有子项 = 菜单,进子框而非内容页)。""" + return any(n[0] == idx for n in nodes) + + +def page_files_on_disk(): + """扫描 Drivers/BSP/UI/ui_page_*.c,返回 {模块名: 文件名}。 + + 判定与 scripts/check_registration.py 规则④一致:只有**定义了 ui_page_<名>_desc** + 的 ui_page_*.c 才算页面实例;生成物 ui_page_registry.c 不定义该符号,天然排除。 + """ + out = {} + for fn in sorted(os.listdir(UI_DIR)): + if not (fn.startswith('ui_page_') and fn.endswith('.c')): + continue + stem = fn[:-2][len('ui_page_'):] + if stem == 'registry': + continue + try: + text = open(os.path.join(UI_DIR, fn), encoding='utf-8-sig', + errors='replace').read() + except OSError: + continue + if re.search(r'\bui_page_%s_desc\b\s*=' % re.escape(stem), text): + out[stem] = fn + return out + + +def validate_pages(nodes, directives): + """校验 #page 绑定与磁盘实例双向对齐,返回 ([(node, 节点名, 模块名)], 密码页模块名|None)。 + + 五类错误一律当场失败(不产出半成品表): + ① #page 标在有子项的节点上(那种节点进子框,永不进内容页) + ② 声明了 #page / @password 但实例文件或描述符符号不存在 + ③ 磁盘上有页面实例却无任何叶子绑定(两端编译链接全过、菜单打不开 = 死代码) + ④ 有 #P 叶子却没有 @password 指令(运行时取到 NULL 描述符,开页静默失败) + ⑤ 有 @password 指令却没有任何 #P 叶子(密码页永不出现的死配置) + """ + disk = page_files_on_disk() + password_stem = directives.get('password') + bound = [] + for idx, (parent, name, depth, pwd, page) in enumerate(nodes): + if page is None: + continue + if has_children(nodes, idx): + sys.exit('ERROR: #page 只能标在叶子上: %r (node %d) 有子项,该节点进子框而非内容页' + % (name, idx)) + if page not in disk: + sys.exit('ERROR: 节点 %r (node %d) 声明 #page=%s,但 Drivers/BSP/UI/ui_page_%s.c ' + '不存在或未定义 ui_page_%s_desc' % (name, idx, page, page, page)) + bound.append((idx, name, page)) + if password_stem is not None and password_stem not in disk: + sys.exit('ERROR: 指令 @password=%s 指向的 Drivers/BSP/UI/ui_page_%s.c 不存在' + '或未定义 ui_page_%s_desc' % (password_stem, password_stem, password_stem)) + + # 密码一致性:#P 叶子与 @password 指令必须成对出现。 + # 只标 #P 不声明指令 → 运行时 ui_page_registry_password() 返回 NULL,按 ENTER 开页 + # 失败停在菜单屏(两端编译全过、只在真按钥匙才暴露);只声明指令没有 #P 叶子 → + # 密码页永不出现,属死配置。两种都在生成期就拦。 + protected_nodes = [idx for idx, (_p, _n, _d, pwd, _pg) in enumerate(nodes) if pwd] + if protected_nodes and password_stem is None: + sys.exit('ERROR: %d 个叶子标了 #P(需密码)但菜单树缺 `# @password=<模块名>` 指令:' + '这些叶子按 ENTER 会取到 NULL 描述符、开页失败停在菜单屏(node %s 等)' + % (len(protected_nodes), ', '.join(str(i) for i in protected_nodes[:5]))) + if password_stem is not None and not protected_nodes: + sys.exit('ERROR: 指令 @password=%s 已声明,但没有任何叶子标 #P —— 密码页永不出现,' + '属死配置(要么给相应叶子加 #P,要么删掉该指令)' % password_stem) + + used = {p for _, _, p in bound} + if password_stem: + used.add(password_stem) + orphans = sorted(s for s in disk if s not in used) + if orphans: + sys.exit('ERROR: 以下页面实例未被任何叶子 #page 引用(写了却从不生效的死代码): %s' + % ', '.join('ui_page_%s.c' % s for s in orphans)) + return bound, password_stem + + +def emit_registry(bound, password_stem): + """产出 ui_page_registry.h / .c 文本。""" + stems = sorted({p for _, _, p in bound} | ({password_stem} if password_stem else set())) + + h = [ + '/*', + ' * 模块名称:UI 页面注册表头文件(自动生成)', + ' * 模块功能:叶子节点 → 内容页描述符的映射表与查询 API;密码页非节点绑定,单独出口', + ' * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)', + ' * 作者:王建锋', + ' * 创建日期:2026-10-15', + ' * 自动生成:本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 的 #page= 注记生成,请勿手改。', + ' * 新增一页:菜单树给叶子加 " #page=<模块名>" + 写 ui_page_<模块名>.c,重跑生成器', + ' * (双端注册 uvprojx/CMakeLists 亦由生成器自动写入)', + ' */', + '#ifndef UI_PAGE_REGISTRY_H', + '#define UI_PAGE_REGISTRY_H', + '', + '#include ', + '', + '#include "ui_content_page.h"', + '', + '#ifdef __cplusplus', + 'extern "C" {', + '#endif', + '', + '/* 注册表条目:一个绑定了内容页的菜单叶子(表序 = 节点下标升序) */', + 'typedef struct {', + ' uint8_t node; /* 菜单叶子节点下标(g_menu_tab 下标) */', + ' const content_page_def_t *desc; /* 页面描述符(Flash const) */', + '} ui_page_entry_t;', + '', + '#define UI_PAGE_MAP_COUNT %du /* 绑定了内容页的叶子数 */' % len(bound), + '', + 'extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT];', + '', + '/*', + ' * 函数功能:查叶子节点绑定的内容页描述符', + ' * 入口参数:node - 菜单叶子节点下标 uint8_t', + ' * 返回值:页面描述符;该叶子未绑定内容页返回 NULL const content_page_def_t *', + ' * 限定条件:node < UI_MENU_TAB_COUNT', + ' * 函数说明:取代旧 ui_menu.c 手写 case 分发——映射的唯一真源是 ui_menu_tree.txt,', + ' * 表序变化不会造成"下标硬编码错位"(线性扫描:绑定页是个位数量级)', + ' */', + 'const content_page_def_t *ui_page_registry_for(uint8_t node);', + '', + '/*', + ' * 函数功能:取密码页描述符(需密码叶子统一进这一页)', + ' * 入口参数:无', + ' * 返回值:密码页描述符;菜单树未声明 @password 指令时返回 NULL const content_page_def_t *', + ' * 限定条件:无', + ' * 函数说明:密码页**不是**节点绑定的(任何 #P 叶子都进同一页),故不进 node→desc 映射,', + ' * 由本接口单独出口;取值来自 ui_menu_tree.txt 的 `# @password=<模块名>` 指令', + ' */', + 'const content_page_def_t *ui_page_registry_password(void);', + '', + '#ifdef __cplusplus', + '}', + '#endif', + '', + '#endif /* UI_PAGE_REGISTRY_H */', + '', + ] + + c = [ + '/*', + ' * 模块名称:UI 页面注册表(自动生成)', + ' * 模块功能:叶子节点 → 内容页描述符映射表 + 2 个查询 API(导航逻辑层消费)', + ' * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)', + ' * 作者:王建锋', + ' * 创建日期:2026-10-15', + ' * 自动生成:本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 的 #page= 注记生成,请勿手改。', + ' * 新增一页:菜单树给叶子加 " #page=<模块名>" + 写 ui_page_<模块名>.c,重跑生成器', + ' */', + '#include "ui_page_registry.h"', + '', + ] + for s in stems: + c.append('#include "ui_page_%s.h"' % s) + c.append('') + c.append('const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = {') + for idx, name, page in bound: + c.append(' { %3du, &ui_page_%s_desc }, /* node %d %s */' % (idx, page, idx, name)) + c.append('};') + c.append('') + c += [ + '/*', + ' * 函数功能:查叶子节点绑定的内容页描述符', + ' * 入口参数:node - 菜单叶子节点下标 uint8_t', + ' * 返回值:页面描述符;未绑定返回 NULL const content_page_def_t *', + ' * 限定条件:无(越界 node 不会命中任何条目,直接 NULL)', + ' * 函数说明:线性扫描绑定表(条目 = 有内容页的叶子,个位数量级)', + ' */', + 'const content_page_def_t *ui_page_registry_for(uint8_t node) {', + ' for (uint8_t i = 0u; i < UI_PAGE_MAP_COUNT; i++) {', + ' if (g_ui_page_map[i].node == node) {', + ' return g_ui_page_map[i].desc;', + ' }', + ' }', + ' return NULL;', + '}', + '', + '/*', + ' * 函数功能:取密码页描述符(需密码叶子统一进这一页,非节点绑定)', + ' * 入口参数:无', + ' * 返回值:密码页描述符%s const content_page_def_t *' + % (';未声明 @password 时返回 NULL' if not password_stem else ''), + ' * 限定条件:无', + ' * 函数说明:取值来自 ui_menu_tree.txt 的 @password 指令(生成期解析,运行期零开销)', + ' */', + 'const content_page_def_t *ui_page_registry_password(void) {', + ] + if password_stem: + c.append(' return &ui_page_%s_desc;' % password_stem) + else: + c.append(' return NULL;') + c.append('}') + c.append('') + return '\n'.join(h), '\n'.join(c) + + +def _read_text(path): + """按原字节读文本(newline='' 防 Windows 把 LF 写成 CRLF)。""" + with open(path, 'r', encoding='utf-8', newline='') as fp: + return fp.read() + + +def _write_text(path, text): + with open(path, 'w', encoding='utf-8', newline='') as fp: + fp.write(text) + + +def prune_registration(): + """删掉两份注册表里指向**已不存在的 ui_page_*.c** 的死条目,返回被删项。 + + 与 sync_registration 对称:删页/改名后残留的条目会让 Keil 直接报 + "cannot open source input file"(模拟器同理报源文件缺失),属必须清掉的死引用; + 只匹配 ui_page_*.c 这一类(生成器自己负责的范围),不碰其它组/其它模块的条目。 + """ + pruned = [] + + uv = _read_text(UVPROJX) + blk_re = re.compile(r'[ \t]*[ \t]*\r?\n' + r'[ \t]*(ui_page_[a-z0-9_]*\.c)' + r'.*?[ \t]*\r?\n', re.S) + + def _drop(m): + fn = m.group(1) + if os.path.isfile(os.path.join(UI_DIR, fn)): + return m.group(0) + pruned.append(('uvprojx', fn)) + return '' + + new_uv = blk_re.sub(_drop, uv) + if new_uv != uv: + _write_text(UVPROJX, new_uv) + + cm = _read_text(CMAKELISTS) + line_re = re.compile(r'^[ \t]*\$\{UI_DIR\}/(ui_page_[a-z0-9_]*\.c)[ \t]*\r?\n', re.M) + + def _drop_cm(m): + fn = m.group(1) + if os.path.isfile(os.path.join(UI_DIR, fn)): + return m.group(0) + pruned.append(('CMakeLists', fn)) + return '' + + new_cm = line_re.sub(_drop_cm, cm) + if new_cm != cm: + _write_text(CMAKELISTS, new_cm) + + return pruned + + +def sync_registration(filenames): + """把 UI 目录下的 .c 文件名补进 uvprojx 与 CMakeLists 的显式列表,返回实际插入项。 + + 只做定点插入、不重排不改写其余内容:uvprojx 由 UV4 维护,任何无关改动都会污染 + diff(AGENTS 铁律"UV4 抖动不入库");已存在则不动(幂等,重复跑不产生 diff)。 + """ + inserted = [] + + # ① uvprojx:Drivers/BSP/UI 组的 末尾补 块 + uv = _read_text(UVPROJX) + gi = uv.find('%s' % UI_GROUP) + if gi < 0: + sys.exit('ERROR: uvprojx 找不到组 %r,无法自动注册(请检查工程结构)' % UI_GROUP) + fe = uv.find('', gi) + if fe < 0: + sys.exit('ERROR: uvprojx 的 %s 组没有 ,无法自动注册' % UI_GROUP) + line_start = uv.rfind('\n', 0, fe) + 1 + add = '' + for fn in filenames: + if '..\\Drivers\\BSP\\UI\\%s' % fn in uv: + continue + add += (' \n' + ' %s\n' + ' 1\n' + ' ..\\Drivers\\BSP\\UI\\%s\n' + ' \n' % (fn, fn)) + inserted.append(('uvprojx', fn)) + if add: + uv = uv[:line_start] + add + uv[line_start:] + _write_text(UVPROJX, uv) + + # ② CMakeLists:set(UI_SOURCES ...) 列表末尾补 ${UI_DIR}/xxx.c + cm = _read_text(CMAKELISTS) + si = cm.find('set(UI_SOURCES') + if si < 0: + sys.exit('ERROR: CMakeLists.txt 找不到 set(UI_SOURCES ...),无法自动注册') + ce = cm.find('\n)', si) + if ce < 0: + sys.exit('ERROR: CMakeLists.txt 的 UI_SOURCES 没有行首 ) 收尾,无法自动注册') + line_start = ce + 1 + add = '' + for fn in filenames: + if '${UI_DIR}/%s' % fn in cm[si:ce]: + continue + add += ' ${UI_DIR}/%s\n' % fn + inserted.append(('CMakeLists', fn)) + if add: + cm = cm[:line_start] + add + cm[line_start:] + _write_text(CMAKELISTS, cm) + + return inserted + + 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) + nodes, directives = parse_tree(text) if len(nodes) > MAX_NODES: sys.exit('ERROR: 节点数 %d 超过 uint8_t 索引上限 %d(哨兵 MENU_ROOT=0xFF 占用 255);请精简菜单或改回 16 位索引' % (len(nodes), MAX_NODES)) + unknown = sorted(set(directives) - {'password'}) + if unknown: + sys.exit('ERROR: 未知指令 @%s(当前仅支持 @password=<页面模块名>)' % ', @'.join(unknown)) + + bound, password_stem = validate_pages(nodes, directives) + header, body, menus, max_depth = emit(nodes) + reg_h, reg_c = emit_registry(bound, password_stem) + # 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) + for path, content in ((OUT_H, header), (OUT_C, body), + (OUT_REG_H, reg_h), (OUT_REG_C, reg_c)): + with open(path, 'w', encoding='utf-8-sig', newline='') as fp: + fp.write(content) + + # 双端注册:注册表本身 + 每个绑定页实例(密码页也必须是已绑定或 @password 声明的) + needed = ['ui_page_registry.c'] + sorted({'ui_page_%s.c' % p for _, _, p in bound}) + pruned = prune_registration() # 先清死引用,再补缺目(改名场景两头都覆盖) + inserted = sync_registration(needed) + + 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('pages bound=%d password=%s' % (len(bound), password_stem or '(none)')) print('->', os.path.relpath(OUT_H, ROOT)) print('->', os.path.relpath(OUT_C, ROOT)) + print('->', os.path.relpath(OUT_REG_H, ROOT)) + print('->', os.path.relpath(OUT_REG_C, ROOT)) + for target, fn in pruned: + print(' pruned %-12s %s' % (target, fn)) + for target, fn in inserted: + print(' registered %-12s %s' % (target, fn)) + if not pruned and not inserted: + print(' registration up-to-date (uvprojx + CMakeLists unchanged)') if __name__ == '__main__': diff --git a/ui_cn_chars.txt b/ui_cn_chars.txt index e7b09f53..a47be134 100644 --- a/ui_cn_chars.txt +++ b/ui_cn_chars.txt @@ -1 +1 @@ -▶三主事交件作保信值入共关制前动化区压参双口号启告地址型复子字存定实密序式强归当录息户手抖护报据控掩操故数整无日时期本板查校检模死波流测清点版用界直看码硬程端第类系线终维网置自行装警认讯记设询误软输运通遥量钟钥错防除面页馈验默上电重芯片异常,: +▶三主事交件作保信值入共关制前动化区压参双口号启告地址型复子字存定实密常序异式强归当录息户手抖护报据控掩操故数整无日时期本板查校检模死波流测清点片版用界直看码硬程端第类系线终维网置自芯行装警认讯记设询误软输运通遥量钟钥错防除面页馈验默,: diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index 6994def5..4a97fe12 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -2,14 +2,25 @@ # 规则:缩进即层级,每级 2 空格;`#` 开头为注释;空行忽略。 # 行尾可选 ` #P` = 该叶子需密码(密码页按所输密码授权:查看密码→只读 / 修改密码→可修改);无尾注 = 不需密码页(直接进,只读)。 # 密码值:Drivers/BSP/UI/ui_pwd_def.h 编译期配置(查看/修改各一个,改值 = 重新编译)。 -# 改菜单只需编辑本文件,然后重跑: +# 行尾可选 ` #page=<模块名>` = 该叶子绑定一个内容页实例(U3 页面注册表): +# 模块名 = Drivers/BSP/UI/ui_page_<模块名>.c,其描述符符号须为 ui_page_<模块名>_desc。 +# 声明了 #page 但实例文件缺失/符号不符 → 生成器报错(不产出半成品表)。 +# 未被任何叶子 #page 引用的 ui_page_*.c 同样报错(写了却从不生效的死代码)。 +# 注:密码页**不是**节点绑定的(任何 #P 叶子都进同一页),故由下方 @password 指令单独声明。 +# 指令(整份文件级,写在注释行里): +# # @password=<模块名> = 需密码叶子统一进的内容页模块名(省略 = 无密码页)。 +# 改菜单/改页面绑定只需编辑本文件,然后重跑: # python tools/menu_gen/gen_menu.py # 产物:Drivers/BSP/UI/ui_menu_tab.c / ui_menu_tab.h +# Drivers/BSP/UI/ui_page_registry.c / ui_page_registry.h(节点→页面注册表,自动生成) +# 生成器并自动把新增的 ui_page_*.c 写进 MDK-ARM/STM32F407-Demo.uvprojx 与 +# simulator/CMakeLists.txt 两处显式列表(双端注册),无需手工编辑注册文件。 +# @password=password 主界面 - 装置信息 + 装置信息 #page=device_info 实时数据 - 交流量 - 直流量 + 交流量 #page=ac + 直流量 #page=dc 遥信量 参数定值 整定 @@ -35,7 +46,7 @@ 清除记录 #P 通讯参数 通讯设置 #P - 网口设置 #P + 网口设置 #P #page=net_cfg SNTP设置 #P 记录查询 SOE记录 @@ -44,6 +55,6 @@ 保护告警 保护启动 遥控记录 - 自检记录 - 运行记录 + 自检记录 #page=selftest + 运行记录 #page=runlog 运行报告