diff --git a/.opencode/skills/font-regen/SKILL.md b/.opencode/skills/font-regen/SKILL.md new file mode 100644 index 00000000..e1078abb --- /dev/null +++ b/.opencode/skills/font-regen/SKILL.md @@ -0,0 +1,32 @@ +--- +name: font-regen +description: 重生成中文子集字体 ui_font_cn_12(extract_cn 提取 → make_font 生成 → diff 检查,固定 4 步,.venv 解释器)。Use when UI 代码新增中文文案、用户报告屏幕缺字/空白块、或提交任何含新汉字的 UI 字符串之前。 +--- + +# 中文字体重生成 + +本工程**唯一字体** `ui_font_cn_12`(宋体 12px 1bpp 子集,MCU/模拟器双端共用)。**缺字渲染为空白**——新增汉字必须走以下固定顺序,不得跳过或调换。详细原理见 `docs/UI 设计/UI_中文字体生成与配置.md`。 + +## 固定顺序(仓库根目录执行) + +```powershell +# 1. 代码落库(新界面字符串已写入 ui_menu.c / ui_content_page.c 等 UI 源码) +# 2. 提取字符集(纯标准库,任意 python3 均可) +python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 → 仓库根 ui_cn_chars.txt +# 3. 生成字体(需 fontTools,一律用 .venv;宋体 .ttc 的 face0 抽取与 ▶ 箭头补回均自动完成,幂等) +.venv\Scripts\python.exe tools/font_gen/make_font.py +# 变体:--bpp 2(宋体细笔画发虚时)/ --font C:/path/to/font.ttf(换字体) +# 4. diff 检查:新增字在位、旧字不变、生成物 Opts 行字符集与预期一致 +``` + +## 提交清单 + +`ui_cn_chars.txt`(仓库根)+ `Drivers/BSP/UI/ui_font_cn_12.c` **随代码一起提交**,缺一个都会让另一端/下次构建字体与字集不一致。 + +## 坑 + +- 不要用 PATH 默认 `python`(msys64,无 fonttools)跑 make_font.py;`.venv` 缺失时按 AGENTS.md「Python 环境」重建 +- `requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 解码,中文注释 → UnicodeDecodeError) +- 生僻字(宋体可能无字形)→ **写码前先手动验证字形**,避免"代码写完才发现是空白" +- 生成成功后跑 `ui-verify` skill 全回归;黄金参考因字形变化而 FAIL 属预期 → `check_content_pages.py --update` 刷新 +- 字数变化时同步更新文档中的字符集计数(当前 97 汉字 + ASCII 0x20-0x7E,共 193 字形):`UI_中文字体生成与配置.md`、`UI_界面制作流程.md` C3、`UI_设计架构.md`、`simulator/README.md` 已知坑 #4 diff --git a/.opencode/skills/new-source-file/SKILL.md b/.opencode/skills/new-source-file/SKILL.md new file mode 100644 index 00000000..c0159e9a --- /dev/null +++ b/.opencode/skills/new-source-file/SKILL.md @@ -0,0 +1,39 @@ +--- +name: new-source-file +description: 新增 .c 源文件的双端注册:MDK-ARM/STM32F407-Demo.uvprojx 与 simulator/CMakeLists.txt 均为显式文件列表,新文件必须手动加进两处再双端构建验证。Use when 新建 App/、Drivers/BSP/ 下的 .c 文件,或构建/运行报 undefined reference、"改了代码没生效" 而怀疑文件漏注册。 +--- + +# 新文件双端注册 + +本工程 Keil 工程与 PC 模拟器都使用**显式文件列表**(无通配扫描)——新 `.c` 文件不手动注册就不会被编译;只注册一端 = 另一端**静默**缺文件(不一定报错,功能悄悄不生效)。 + +## 注册步骤 + +1. **Keil MDK**:编辑 `MDK-ARM/STM32F407-Demo.uvprojx`,在对应 `` 的 `` 中按相邻条目格式插入(`..\` 开头反斜杠路径): + +```xml + + ui_new_module.c + 1 + ..\Drivers\BSP\UI\ui_new_module.c + +``` + + 现有组:`App`、`Drivers/BSP`、`Drivers/BSP/UI`、`Test` 等;新文件放语义对应的组。 + +2. **模拟器**(仅当文件需 PC 端编译,如 `Drivers/BSP/UI/*.c`):在 `simulator/CMakeLists.txt` 的 `set(UI_SOURCES ...)` 列表加一行 `${UI_DIR}/ui_new_module.c`。非 UI 模块文件通常不需模拟器注册。 + +3. **双端构建验证**(两端都必须过): + +```powershell +cmake --build simulator/build --config Debug # 0 警告 +& "MDK-ARM\build.bat" # BUILD_OK,0 错 0 警 +``` + +## 注意 + +- **禁止**把源文件复制进 `simulator/`——两端共用 `Drivers/BSP/` 原件(UI_界面制作流程.md C7) +- 只注册 `.c`(含 `.s`);`.h` 靠 include 路径找到,不加工程 +- 漏注册的典型症状:一端编译失败(undefined reference)或一端行为悄悄停留在旧逻辑 +- 注册完成后跑回归:UI 文件 → `ui-verify` skill;其他模块 → `@test`(`test/run_boot_test.ps1`) +- 改过 `CMakeLists.txt` 后需重新 `cmake -B build` 配置 diff --git a/.opencode/skills/ui-verify/SKILL.md b/.opencode/skills/ui-verify/SKILL.md new file mode 100644 index 00000000..81eb4e01 --- /dev/null +++ b/.opencode/skills/ui-verify/SKILL.md @@ -0,0 +1,45 @@ +--- +name: ui-verify +description: UI 改动后的回归验证:5 步门禁(模拟器构建、菜单像素回归、内容页黄金参考、Keil 构建、代码规范)。Use whenever code under Drivers/BSP/UI, 字体 ui_font_cn_12.c / ui_cn_chars.txt、菜单/内容页被修改,或用户要求"验证 UI / 跑 UI 回归 / UI 改动检查"。 +--- + +# UI 回归验证 + +UI 相关改动完成后必须跑完 5 个门禁再提交。完整说明见 `docs/UI 设计/UI_自动化测试流程.md`。 + +## 步骤(仓库根目录执行,python 一律用 .venv) + +```powershell +cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警告) +.venv\Scripts\python.exe simulator/tests/check_menu_rows.py # 1) 菜单 8 状态像素回归 + 池基线门禁 +.venv\Scripts\python.exe simulator/tests/check_content_pages.py # 2) 内容页黄金参考回归 + 往返池门禁 +& "MDK-ARM\build.bat" # 3) Keil 构建(BUILD_OK,0 错 0 警) +.venv\Scripts\python.exe scripts/check_coding_standard.py # 4) 代码规范检查(PASS) +``` + +退出码:0=PASS / 1=断言失败 / 2=环境错误(exe 缺失等,先按 AGENTS.md 重建对应环境再重跑)。 + +## 失败处置决策 + +| 现象 | 判定 | 处置 | +|---|---|---| +| 内容页 FAIL,且**有意**改了该页视觉 | 预期变化 | 先肉眼确认新效果正确(窗口模式或 `i1_to_png.py` 出图对照 mockup),再 `.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update` 刷参考,`git diff` 检查 ref/ 变化后随代码提交 | +| 内容页 FAIL,但**未**有意改视觉 | bug(缺字/几何漂移/样式回归) | 修代码;**禁止**刷参考掩盖 | +| 池水位门禁失败但像素一致 | 结构/样式变更(常量样式转换、增删框体等) | 确认新值合理(残留分解见 `菜单页面设计.md` §9.5)→ 更新脚本常量 + 在流程文档变更记录登记 | +| 菜单回归失败 | 结构断言(白带占比/边框/往返一致) | 按脚本打印的未过断言清单逐项修代码,无参考文件可刷 | + +## 池水位门禁常量(在脚本头部) + +| 常量 | 脚本 | 当前值 | 含义 | +|---|---|---|---| +| `MENU_POOL_BASELINE` | check_menu_rows.py | 4784 B | 菜单根框建完的 LVGL 池 used(x64 模拟器) | +| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | 4864 B | 开页-关页往返后 = 基线 + 80 B 一次性残留 | + +**禁止为过门禁盲目改常量**——无法解释的漂移 = 内存回归 bug。 + +## 关联注意 + +- `.venv` 缺失/换机:`& 'C:\ProgramData\anaconda3\python.exe' -m venv .venv; & '.venv\Scripts\python.exe' -m pip install -r requirements.txt` +- 新增 UI `.c` 文件先走 `new-source-file` skill 双端注册,否则一端静默缺文件 +- 字体重生成后全页大面积像素差异属预期:确认字形正确后 `--update` 刷参考 +- 涉及新界面/大改时本 skill 是 P3 门禁,完整管线(P0~P5)见 `docs/UI 设计/UI_界面制作流程.md` diff --git a/Drivers/BSP/UI/ui_content_page.c b/Drivers/BSP/UI/ui_content_page.c index 9838f999..6fa95393 100644 --- a/Drivers/BSP/UI/ui_content_page.c +++ b/Drivers/BSP/UI/ui_content_page.c @@ -1,13 +1,21 @@ /* - * 模块名称:UI Content Page (内容页面显示层) - * 模块功能:菜单叶子节点的内容页(装置信息/交流量/直流量)——独立 LVGL 屏幕, - * 全屏容器 DRAW_MAIN 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + - * 页脚页码),无子对象;进入时切屏并获焦、ESC 返回菜单屏并还原焦点 + * 模块名称:UI Content Page (内容页框架) + * 模块功能:菜单叶子节点进入的内容页(独立全屏页面)框架——全屏容器 + * 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码),无子对象; + * 只持机制:屏/容器按需建毁、焦点、按键分发、描述符驱动通用绘制; + * 不持任何页面数据——各页数据/几何/钩子由 ui_page_xxx 模块经 + * content_page_def_t 接入描述符表 s_defs[](架构见 内容页架构.md); + * 进入时切屏并获焦、ESC 返回菜单屏并还原焦点; + * 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21 实测几何) + * 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义) + * 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入 + * ui_page_xxx 模块,本文件只留机制 + 描述符表;绘制/按键改 + * 按描述符驱动(静态表/refresh + 4 钩子),行为零变化 */ /* 头文件包含区 */ @@ -15,8 +23,16 @@ #include "ui_content_page.h" +#include "ui_page_device_info.h" + +#include "ui_page_ac.h" + +#include "ui_page_dc.h" + #include "ui_menu_view.h" +#include "ui_deco.h" + #include "lvgl.h" #define DBG_TAG "[UI_CONTENT_PAGE]" @@ -27,8 +43,6 @@ /* * 布局常量(设备像素;参考图 10/20/21 实测,3x 放大图缩回 160x160 测量): * 标题 y=2(CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143; - * 信息页行:首行 y=40、行距 15、标签列 x=6 / 值列 x=86(无选中带); - * 列表页行:槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=7 / 值 x=101; * 字形顶距(CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐 */ #define CP_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ @@ -37,106 +51,18 @@ #define CP_TITLE_Y 2 /* 标题放置 y(居中;墨迹自 y=3 起) */ #define CP_PAGE_LINE_Y 143 /* 页脚"第 X 页,共 Y 页"放置 y(x=6 左对齐) */ -#define CP_DECO_LEG_W 3 /* "浅 V"两腿线宽(与菜单根框一致:1bpp 斜线须加粗) */ -#define CP_DECO_HORIZ_W 2 /* "浅 V"水平线宽 */ - -/* 私有类型定义区 */ - -/* 行数据:左列名 + 右列值(Flash 字面量,不 strdup) */ -typedef struct { - const char *name; - const char *value; -} cp_row_t; - -/* - * 页面定义(Flash 常量表):标题/行几何/数据表/页脚参数; - * selectable=1 为可移选分页的数据列表页,0 为静态信息页 - */ -typedef struct { - const char *title; /* 标题(浅 V 区居中) */ - uint8_t selectable; /* 1 = 数据列表页(↑↓移选/分页);0 = 信息页(静态行) */ - int32_t top_y; /* 首行槽顶 y */ - int32_t pitch; /* 行距(相邻行槽顶间距) */ - int32_t band_h; /* 选中白带高(0 = 无选中带) */ - int32_t text_dy; /* 文本放置相对行槽顶的垂直偏移 */ - int32_t left_x; /* 左列 x(信息页=标签、列表页=名) */ - int32_t right_x; /* 右列 x(值) */ - uint8_t rows_per_page; /* 每页可见行数(0 = 不分页) */ - uint16_t total_rows; /* 数据总行数 */ - const cp_row_t *rows; /* 数据表(Flash) */ - const char *subtitle; /* 副标题(居中;NULL = 无,仅信息页有) */ - int32_t subtitle_y; /* 副标题放置 y(-1 = 无) */ - uint8_t fixed_cur_page; /* 页脚"第 X 页"固定显示值(信息页按参考图原样) */ - uint8_t fixed_total_pages; /* 页脚"共 Y 页"固定显示值 */ -} cp_def_t; +#define CP_ROW_BUF_LEN 32 /* 动态行栈缓冲长度(同帧同步光栅化;超出行带即被裁切) */ /* 静态变量定义区 */ -/* "浅 V"装饰点集与线宽:几何与菜单根框一致(ui_menu_view.c s_deco_*),culling 区同范围 */ -static const lv_point_precise_t s_deco_leg_l[2] = { {7, 7}, {16, 17} }; /* 左腿 */ -static const lv_point_precise_t s_deco_hoz[2] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */ -static const lv_point_precise_t s_deco_leg_r[2] = { {158, 7}, {145, 17} }; /* 右腿 */ -static const lv_point_precise_t *const s_deco_segs[3] = {s_deco_leg_l, s_deco_leg_r, s_deco_hoz}; -static const int32_t s_deco_seg_w[3] = {CP_DECO_LEG_W, CP_DECO_LEG_W, CP_DECO_HORIZ_W}; - -static const lv_area_t s_deco_band = {0, 0, CP_SCREEN_W - 1, 19}; /* 浅 V culling 区 */ - -/* 装置信息行(参考图 10;标签含全角冒号,值列固定 x=86) */ -static const cp_row_t s_info_rows[] = { - {"终端类型:", "F30"}, - {"终端型号:", "F30"}, - {"软件版本:", "SV0.010"}, - {"硬件版本:", "HW0.010"}, - {"软件校验:", "4454"}, - {"程序日期:", "2024.08.27"}, -}; - -/* 交流量行(参考图 20 = 第 1 页原样;第 2/3 页为占位数据,可替换) */ -static const cp_row_t s_ac_rows[] = { - {"Ia1", "0.000"}, /* 第 1 页(参考图 20) */ - {"Ib1", "0.137"}, - {"Ic1", "0.274"}, - {"Ia2", "0.411"}, - {"Ib2", "0.548"}, - {"Ic2", "0.685"}, - {"Ua1", "0.82"}, - {"Ub1", "0.96"}, - {"Uc1", "1.03"}, /* 第 2 页(占位数据) */ - {"Uab", "1.17"}, - {"Ubc", "1.24"}, - {"Uca", "1.31"}, - {"Hz", "50.00"}, - {"P", "1.42"}, - {"Q", "0.87"}, - {"S", "1.63"}, - {"PF", "0.95"}, /* 第 3 页(占位数据) */ - {"Iavg","0.35"}, - {"Uavg","0.87"}, - {"Fmax","50.02"}, - {"Imax","1.24"}, - {"Pmax","2.10"}, - {"Qmax","1.05"}, - {"Smax","2.45"}, -}; - -/* 直流量行(参考图 21) */ -static const cp_row_t s_dc_rows[] = { - {"ZL01", "3.01"}, - {"ZL02", "3.15"}, - {"Pa1", "3.288"}, -}; - -/* 页面定义表(顺序与 content_page_id_t 一致) */ -static const cp_def_t s_defs[CONTENT_PAGE_COUNT] = { - /* 装置信息:静态键值行 + 副标题;页脚按参考图原样"第 10 页,共 1 页" */ - {"装置信息", 0u, 40, 15, 0, 0, 6, 86, 0u, (uint16_t)6, s_info_rows, - "馈线自动化终端", 25, 10u, 1u}, - /* 交流量:24 项 / 8 项每页(3 页);text_dy=0:ASCII 字形顶距 +2,墨迹落槽顶+2(与参考图一致) */ - {"交流量", 1u, 24, 14, 13, 0, 7, 101, 8u, (uint16_t)24, s_ac_rows, - NULL, -1, 0u, 0u}, - /* 直流量:3 项单页 */ - {"直流量", 1u, 24, 14, 13, 0, 7, 101, 8u, (uint16_t)3, s_dc_rows, - NULL, -1, 0u, 0u}, +/* + * 描述符表:页 ID → 页面模块描述符(每页一行;新增页 = 枚举值 + 此处一行, + * 框架机制不变,见 内容页架构.md §七) + */ +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, }; static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */ @@ -181,6 +107,20 @@ static bool cp_areas_intersect(const lv_area_t *a, const lv_area_t *b) { return (x1 <= x2 && y1 <= y2); } +/* + * 函数功能:取页面总行数(静态页 = 描述符 row_count,动态页 = count_cb) + * 入口参数:d - 页面描述符 const content_page_def_t * + * 返回值:总行数 uint16_t + * 限定条件:d 非空 + * 函数说明:行数唯一入口;绘制(窗口/页脚)与按键(移选取模)均经此取 + */ +static uint16_t cp_row_total(const content_page_def_t *d) { + if (d->count_cb != NULL) { + return d->count_cb(); + } + return d->row_count; +} + /* * 函数功能:绘制居中单行文本(标题/副标题) * 入口参数:layer - 绘制层 lv_layer_t * @@ -211,34 +151,25 @@ static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y } /* - * 函数功能:页面自绘(浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码) + * 函数功能:页面自绘(浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码 + 页面钩子) * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:事件为 LV_EVENT_DRAW_MAIN,页面已打开 - * 函数说明: 1. 浅 V 装饰几何与菜单根框一致(3 段线逐段画) - * 2. 列表页窗口 = 选中行所在页(首可见行 = (sel/每页数)*每页数), + * 函数说明: 1. 浅 V 装饰:共享 ui_deco 模块(与菜单根框同几何) + * 2. 行数:静态页取 row_count,动态页取 count_cb(); + * 列表页窗口 = 选中行所在页(首可见行 = (sel/每页数)*每页数), * 行槽位置固定、数据随页切换;信息页全量静态绘制 - * 3. 选中行整行白带(x=2..156)+ 黑字,非选中白字 - * 4. 各区块按 culling 区跳过不在重绘区的部分 + * 3. 行文本:静态页直绘 Flash 表(不拷内存);动态页经 refresh() + * 逐行取入栈缓冲(同帧同步光栅化,安全) + * 4. 选中行整行白带(x=2..156)+ 黑字,非选中白字 + * 5. 各区块按 culling 区跳过不在重绘区的部分;末尾调 draw_cb(页自定义内容) */ static void cp_draw_cb(lv_event_t *e) { - const cp_def_t *d = &s_defs[s_open]; + const content_page_def_t *d = s_defs[s_open]; lv_layer_t *layer = lv_event_get_layer(e); - /* "浅 V"装饰:3 段线(与菜单根框同几何) */ - if (cp_areas_intersect(&s_deco_band, &layer->_clip_area)) { - for (uint8_t i = 0; i < 3; i++) { - lv_draw_line_dsc_t ld; - - lv_draw_line_dsc_init(&ld); - ld.base.layer = layer; - ld.color = lv_color_white(); - ld.width = s_deco_seg_w[i]; - ld.p1 = s_deco_segs[i][0]; - ld.p2 = s_deco_segs[i][1]; - lv_draw_line(layer, &ld); - } - } + /* "浅 V"装饰:共享 ui_deco 模块(与菜单根框同几何) */ + ui_deco_draw(layer, &layer->_clip_area); /* 标题(居中) */ { @@ -267,17 +198,22 @@ static void cp_draw_cb(lv_event_t *e) { } /* 行数据:列表页窗口跟随选中行所在页,信息页全量 */ + int32_t nrows = (int32_t)cp_row_total(d); int32_t first = 0; - int32_t cnt = (int32_t)d->total_rows; + int32_t cnt = nrows; - if (d->selectable && d->rows_per_page > 0u) { + if (d->selectable != 0u && d->rows_per_page > 0u) { first = (int32_t)(s_sel / d->rows_per_page) * d->rows_per_page; /* 窗口顶行 */ - cnt = (int32_t)d->total_rows - first; + cnt = nrows - first; if (cnt > (int32_t)d->rows_per_page) { cnt = (int32_t)d->rows_per_page; /* 末页不足整页时截断 */ } } + bool dynamic = (d->refresh != NULL); + char dyn_name[CP_ROW_BUF_LEN]; /* 动态行栈缓冲(同帧同步光栅化) */ + char dyn_value[CP_ROW_BUF_LEN]; + for (int32_t i = 0; i < cnt; i++) { int32_t row_idx = first + i; /* 数据下标(跨页连续) */ int32_t slot_y = d->top_y + i * d->pitch; /* 行槽顶(窗口内位置固定) */ @@ -310,15 +246,28 @@ static void cp_draw_cb(lv_event_t *e) { lv_draw_rect(layer, &fd, &band); } - /* 左列(标签/名)与右列(值):固定列 x,垂直偏移 text_dy */ + /* 行文本:静态 = Flash 表(不拷内存);动态 = refresh 逐行取(栈缓冲) */ + const char *name; + const char *value; + + if (dynamic) { + d->refresh((uint16_t)row_idx, dyn_name, dyn_value); + name = dyn_name; + value = dyn_value; + } + else { + name = d->rows[row_idx].name; + value = d->rows[row_idx].value; + } + { lv_draw_label_dsc_t ld; /* ofs_x/ofs_y 相对 coords 原点(strip.y1=slot_y),勿再叠加 slot_y */ lv_draw_label_dsc_init(&ld); ld.base.layer = layer; - ld.text = d->rows[row_idx].name; - ld.text_static = 1; /* Flash 字符串:不拷内存 */ + ld.text = name; + ld.text_static = (uint32_t)(!dynamic); /* 静态不拷;动态缓冲同帧内有效 */ ld.color = color; /* 选中黑 / 非选中白 */ ld.ofs_x = d->left_x; ld.ofs_y = d->text_dy; @@ -326,8 +275,8 @@ static void cp_draw_cb(lv_event_t *e) { lv_draw_label_dsc_init(&ld); ld.base.layer = layer; - ld.text = d->rows[row_idx].value; - ld.text_static = 1; + ld.text = value; + ld.text_static = (uint32_t)(!dynamic); ld.color = color; ld.ofs_x = d->right_x; ld.ofs_y = d->text_dy; @@ -339,16 +288,16 @@ static void cp_draw_cb(lv_event_t *e) { { char buf[32]; uint8_t cur; - uint8_t total; + uint8_t pages; lv_area_t t; - if (d->selectable) { + if (d->selectable != 0u) { cur = (uint8_t)(s_sel / d->rows_per_page + 1u); - total = (uint8_t)((d->total_rows + d->rows_per_page - 1u) / d->rows_per_page); + pages = (uint8_t)((nrows + (int32_t)d->rows_per_page - 1) / d->rows_per_page); } else { cur = d->fixed_cur_page; - total = d->fixed_total_pages; + pages = d->fixed_total_pages; } t.x1 = 0; @@ -357,7 +306,7 @@ static void cp_draw_cb(lv_event_t *e) { t.y2 = CP_PAGE_LINE_Y + 15; if (cp_areas_intersect(&t, &layer->_clip_area)) { /* lv_snprintf 而非 snprintf:MSVC 对 printf 族做格式串扫描,中文字面量会误报 C4819 */ - lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)total); + lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)pages); lv_draw_label_dsc_t ld; @@ -371,38 +320,64 @@ static void cp_draw_cb(lv_event_t *e) { lv_draw_label(layer, &ld, &t); } } + + /* 页面钩子:自定义内容(表单/操作页;在内置区块之后绘制) */ + if (d->draw_cb != NULL) { + d->draw_cb(layer, &layer->_clip_area); + } } /* - * 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回) + * 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回 / 页面钩子) * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:容器已加入默认输入组并获焦 - * 函数说明:1. ↑/↓:列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘;信息页不响应 - * 2. ESC/←:关闭页面返回菜单(导航状态未压层,原样还原) + * 函数说明: 1. ESC/←:恒关页返回菜单(框架保证,页面钩子不可拦截) + * 2. 其余键:key_cb 钩子先调(返回 true = 页已处理,框架跳过内置) + * 3. 内置:↑/↓ 列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘; + * 信息页不响应;动态行数可能缩小,选中行越界时重置回首行 */ static void cp_key_cb(lv_event_t *e) { - const cp_def_t *d = &s_defs[s_open]; + const content_page_def_t *d = s_defs[s_open]; lv_key_t key = (lv_key_t)lv_event_get_key(e); + /* ESC/←:关闭页面返回菜单屏(焦点/导航状态还原) */ + if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { + ui_content_page_close(); + return; + } + + /* 页面钩子先调:true = 页已处理,框架跳过内置语义 */ + if (d->key_cb != NULL && d->key_cb(key)) { + return; + } + switch (key) { - case LV_KEY_UP: - if (d->selectable && d->total_rows > 0u) { + case LV_KEY_UP: { + uint16_t total = cp_row_total(d); + + if (d->selectable != 0u && total > 0u) { + if (s_sel >= total) { + s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */ + } /* 上移、首尾环绕:先加 total 再减 1 保证无符号下不为负 */ - s_sel = (uint16_t)((s_sel + d->total_rows - 1u) % d->total_rows); + s_sel = (uint16_t)((s_sel + total - 1u) % total); lv_obj_invalidate(s_cont); /* 重绘(选中带/窗口/页码可能变化) */ } break; - case LV_KEY_DOWN: - if (d->selectable && d->total_rows > 0u) { - s_sel = (uint16_t)((s_sel + 1u) % d->total_rows); /* 下移、越尾绕回首行 */ + } + case LV_KEY_DOWN: { + uint16_t total = cp_row_total(d); + + if (d->selectable != 0u && total > 0u) { + if (s_sel >= total) { + s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */ + } + s_sel = (uint16_t)((s_sel + 1u) % total); /* 下移、越尾绕回首行 */ lv_obj_invalidate(s_cont); } break; - case LV_KEY_ESC: - case LV_KEY_LEFT: - ui_content_page_close(); /* 返回菜单屏(焦点/导航状态还原) */ - break; + } default: break; /* 其余键值忽略 */ } @@ -411,7 +386,7 @@ static void cp_key_cb(lv_event_t *e) { /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) * 入口参数:id - 内容页 ID content_page_id_t - * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/对象创建失败) + * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象) * 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管 @@ -422,6 +397,12 @@ bool ui_content_page_open(content_page_id_t id) { return false; /* ID 无效或已有页面打开(防御) */ } + 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; /* 枚举/描述符表漂移:拒开并留痕,不崩溃 */ + } + s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕 */ if (s_scr == NULL) { return false; /* LVGL 池耗尽 */ @@ -451,7 +432,7 @@ bool ui_content_page_open(content_page_id_t id) { } lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */ - DBG_INFO("page open: id=%u rows=%u", (unsigned)id, (unsigned)s_defs[id].total_rows); + DBG_INFO("page open: id=%u rows=%u", (unsigned)id, (unsigned)cp_row_total(d)); return true; } diff --git a/Drivers/BSP/UI/ui_content_page.h b/Drivers/BSP/UI/ui_content_page.h index 3e1ad85c..7c55fb5d 100644 --- a/Drivers/BSP/UI/ui_content_page.h +++ b/Drivers/BSP/UI/ui_content_page.h @@ -1,14 +1,21 @@ /* - * 模块名称:UI Content Page (内容页面显示层) - * 模块功能:菜单叶子节点的内容页(装置信息/交流量/直流量)——独立 LVGL 屏幕, - * 全屏容器自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码), - * 无子对象;进入时切屏并获焦、ESC 返回菜单屏并还原焦点; + * 模块名称:UI Content Page (内容页框架) + * 模块功能:菜单叶子节点进入的内容页(独立全屏页面)框架—— + * 屏/容器按需建毁生命周期 + 焦点 + 切屏、按键分发(内置 ↑↓/ESC + key_cb 钩子)、 + * 描述符驱动通用绘制(浅 V + 标题/副标题 + 行 + 选中带 + 页脚 + draw_cb); + * 不持任何页面数据:各页数据/几何/钩子由 ui_page_xxx 模块经 content_page_def_t + * 接入框架描述符表(架构见 内容页架构.md); + * 进入时切屏并获焦、ESC 返回菜单屏并还原焦点; * 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-09 * 修改记录: * 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21) + * 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义) + * 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入 + * ui_page_xxx 模块;本文件只留描述类型与 API;描述符新增 + * 4 个扩展钩子(refresh/count_cb/draw_cb/key_cb) */ #ifndef UI_CONTENT_PAGE_H #define UI_CONTENT_PAGE_H @@ -16,6 +23,8 @@ #include #include +#include "lvgl.h" + /* * 内容页 ID:与菜单叶子节点一一对应(映射见 ui_menu.c menu_content_page_for); * CONTENT_PAGE_COUNT 兼作"无内容页/未打开"哨兵值 @@ -27,13 +36,46 @@ typedef enum { CONTENT_PAGE_COUNT /* 哨兵:无效 ID / 未打开 */ } content_page_id_t; +/* 行数据:左列名 + 右列值(Flash 字面量,不 strdup) */ +typedef struct { + const char *name; + const char *value; +} cp_row_t; + +/* + * 页面描述符:一页 = 一个 Flash const 结构(几何 + 数据源 + 可选钩子) + * 由各 ui_page_xxx 页面模块提供,框架描述符表 s_defs[] 按页 ID 引用 + */ +typedef struct { + const char *title; /* 标题(浅 V 区居中) */ + const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */ + int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */ + uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */ + int32_t top_y; /* 首行槽顶 y */ + int32_t pitch; /* 行距(相邻行槽顶间距) */ + int32_t band_h; /* 选中白带高(0 = 无选中带) */ + int32_t text_dy; /* 文本放置相对行槽顶的垂直偏移 */ + int32_t left_x; /* 左列 x(信息页=标签、列表页=名) */ + int32_t right_x; /* 右列 x(值) */ + uint8_t rows_per_page; /* 每页可见行数(0 = 不分页) */ + uint16_t row_count; /* 总行数(静态页;动态页用 count_cb) */ + const cp_row_t *rows; /* 静态数据表(Flash);NULL = 动态页 */ + uint8_t fixed_cur_page; /* 信息页页脚"第 X 页"固定显示值 */ + uint8_t fixed_total_pages; /* 信息页页脚"共 Y 页"固定显示值 */ + /* —— 扩展钩子(全可选;NULL = 纯内置行为;均运行在 uiTask 上下文)—— */ + void (*refresh)(uint16_t row, char *name, char *value); /* 动态行:逐行取数(名/值) */ + uint16_t (*count_cb)(void); /* 动态行数(记录页) */ + void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */ + bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */ +} content_page_def_t; + /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) * 入口参数:id - 内容页 ID content_page_id_t - * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/对象创建失败) + * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. 新建独立屏幕,全屏容器自绘全部内容(无子对象,池开销极小) - * 2. 容器入默认输入组并获焦——按键由本模块处理(↑↓移选/ESC返回) + * 2. 容器入默认输入组并获焦——按键由本模块处理(↑↓移选/ESC返回/页面钩子) * 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原 */ bool ui_content_page_open(content_page_id_t id); diff --git a/Drivers/BSP/UI/ui_deco.c b/Drivers/BSP/UI/ui_deco.c new file mode 100644 index 00000000..d8d78039 --- /dev/null +++ b/Drivers/BSP/UI/ui_deco.c @@ -0,0 +1,81 @@ +/* + * 模块名称:UI Deco (顶部装饰层) + * 模块功能:顶部"浅 V"装饰(左腿/水平线/右腿 3 段线)共享自绘—— + * 菜单根框与内容页共用的几何点集、线宽与 culling 区在本模块唯一定义, + * 各页面经 ui_deco_draw 调用绘制(无 LVGL 对象,DRAW_MAIN 自绘) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:提取 ui_menu_view.c / ui_content_page.c 重复的"浅 V"几何 + */ + +/* 头文件包含区 */ +#include + +#include "ui_deco.h" + +/* 私有宏定义区 */ + +#define DECO_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ +#define DECO_LEG_W 3 /* "浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */ +#define DECO_HORIZ_W 2 /* "浅 V"水平线宽 */ + +/* 静态变量定义区 */ + +static const lv_point_precise_t s_leg_l[2] = { {7, 7}, {16, 17} }; /* 左腿:自顶端斜下 */ +static const lv_point_precise_t s_hoz[2] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */ +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 区(与原线段对象范围一致) */ + +/* 函数定义区 */ + +/* + * 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头) + * 入口参数:a - 区域 1 const lv_area_t * + * b - 区域 2 const lv_area_t * + * 返回值:true - 相交;false - 不相交 + * 限定条件:无 + * 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_menu_view.c 同实现) + */ +static bool deco_areas_intersect(const lv_area_t *a, const lv_area_t *b) { + int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1; + int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1; + int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2; + int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2; + + return (x1 <= x2 && y1 <= y2); +} + +/* + * 函数功能:绘制顶部"浅 V"装饰(3 段线,线宽各异,白色) + * 入口参数:layer - 绘制层 lv_layer_t * + * clip - 重绘裁剪区 const lv_area_t * + * 返回值:无 + * 限定条件:layer/clip 有效(调用方自 LV_EVENT_DRAW_MAIN 事件取得) + * 函数说明:1. 几何共享:两腿 3px 粗、水平线 2px 细(1bpp 斜线边缘易被阈值吃掉,腿须加粗) + * 2. 绘制次序固定(左腿、右腿、水平线),重叠像素与原渲染一致 + * 3. 重绘区未覆盖 culling 区(屏顶 20px 带)则整体跳过 + */ +void ui_deco_draw(lv_layer_t *layer, const lv_area_t *clip) { + if (!deco_areas_intersect(&s_band, clip)) { + return; + } + + for (uint8_t i = 0; i < 3; i++) { + lv_draw_line_dsc_t ld; + + lv_draw_line_dsc_init(&ld); + ld.base.layer = layer; + ld.color = lv_color_white(); + ld.width = s_seg_w[i]; + ld.p1 = s_segs[i][0]; + ld.p2 = s_segs[i][1]; + lv_draw_line(layer, &ld); + } +} diff --git a/Drivers/BSP/UI/ui_deco.h b/Drivers/BSP/UI/ui_deco.h new file mode 100644 index 00000000..328e1a6a --- /dev/null +++ b/Drivers/BSP/UI/ui_deco.h @@ -0,0 +1,29 @@ +/* + * 模块名称:UI Deco (顶部装饰层) + * 模块功能:顶部"浅 V"装饰(左腿/水平线/右腿 3 段线)共享自绘接口—— + * 菜单根框(ui_menu_view)与内容页(ui_content_page)共用同一几何 + * 与绘制次序;重绘区未覆盖 culling 区则整体跳过 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:提取 ui_menu_view.c / ui_content_page.c 重复的"浅 V"几何 + */ +#ifndef UI_DECO_H +#define UI_DECO_H + +#include "lvgl.h" + +/* + * 函数功能:绘制顶部"浅 V"装饰(3 段线,线宽各异,白色) + * 入口参数:layer - 绘制层 lv_layer_t * + * clip - 重绘裁剪区 const lv_area_t * + * 返回值:无 + * 限定条件:layer/clip 有效(调用方自 LV_EVENT_DRAW_MAIN 事件取得) + * 函数说明:1. 几何共享:两腿 3px 粗、水平线 2px 细(1bpp 斜线边缘易被阈值吃掉,腿须加粗) + * 2. 绘制次序固定(左腿、右腿、水平线),重叠像素与原渲染一致 + * 3. 重绘区未覆盖 culling 区(屏顶 20px 带)则整体跳过 + */ +void ui_deco_draw(lv_layer_t *layer, const lv_area_t *clip); + +#endif /* UI_DECO_H */ diff --git a/Drivers/BSP/UI/ui_menu_view.c b/Drivers/BSP/UI/ui_menu_view.c index 1d294467..d03e644e 100644 --- a/Drivers/BSP/UI/ui_menu_view.c +++ b/Drivers/BSP/UI/ui_menu_view.c @@ -19,6 +19,7 @@ * 2026-09-08 王建锋 4 层容器→1 个持久容器(焦点/键唯一入口;帧深度由逻辑层 * s_depth 决定,键回调 user_data=depth 冗余);根框装饰 * (浅 V)+状态行改容器自绘(删 3 lv_line + 1 label 对象) + * 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义) */ /* 头文件包含区 */ @@ -28,6 +29,8 @@ #include "ui_menu_tab.h" +#include "ui_deco.h" + #include "lvgl.h" #define DBG_TAG "[UI_MENU_VIEW]" @@ -52,8 +55,6 @@ #define MENU_ROOT_BOX_Y 44 /* 根框顶 */ #define MENU_ROOT_STATUS_X 17 /* 状态行左 */ #define MENU_ROOT_STATUS_Y 20 /* 状态行顶 */ -#define MENU_DECO_LEG_W 3 /* 顶部"浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */ -#define MENU_DECO_HORIZ_W 2 /* 顶部"浅 V"水平线宽 */ /* 私有类型定义区 */ @@ -542,45 +543,25 @@ lv_obj_t *ui_menu_view_container(void) { } /* - * 根框装饰(浅 V)点集与自绘区域:容器 DRAW_MAIN 自绘(无对象); - * culling 区域覆盖原 lv_line 对象范围((0,0) 处 160×20)与状态行文本区 + * 根框装饰(浅 V):几何/绘制/culling 由共享 ui_deco 模块承载(无对象,容器自绘); + * 状态行 culling 区为本模块私有(文本约 104px 宽 + 余量) */ -static const lv_point_precise_t s_deco_leg_l[2] = { {7, 7}, {16, 17} }; /* 左腿:自顶端斜下 */ -static const lv_point_precise_t s_deco_hoz[2] = { {16, 17}, {145, 17} }; /* 水平线:接两腿底端 */ -static const lv_point_precise_t s_deco_leg_r[2] = { {158, 7}, {145, 17} }; /* 右腿 */ -static const lv_point_precise_t *const s_deco_segs[3] = {s_deco_leg_l, s_deco_leg_r, s_deco_hoz}; -static const int32_t s_deco_seg_w[3] = {MENU_DECO_LEG_W, MENU_DECO_LEG_W, MENU_DECO_HORIZ_W}; - -static const lv_area_t s_deco_band = {0, 0, MENU_SCREEN_W - 1, 19}; /* 浅 V culling 区(与原线段对象范围一致) */ -static const lv_area_t s_status_area = {MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y, 130, 33}; /* 状态行 culling 区(文本约 104px 宽 + 余量) */ +static const lv_area_t s_status_area = {MENU_ROOT_STATUS_X, MENU_ROOT_STATUS_Y, 130, 33}; /* 状态行 culling 区 */ /* * 函数功能:自绘根框装饰 + 状态行(容器 DRAW_MAIN;替代旧 4 个 LVGL 对象) * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:事件为 LV_EVENT_DRAW_MAIN,容器已建 - * 函数说明: 1. 浅 V:两腿 3px 粗、水平线 2px 细(1bpp 斜线边缘易被阈值吃掉,腿须比水平线粗) - * 2. 绘制次序 = 旧对象创建序(左腿、右腿、水平线),重叠像素与旧渲染一致 - * 3. 状态行:静态字符串、白色,与旧 label 同字体同位置 - * 4. 重绘区不覆盖对应 culling 区则跳过(culling) + * 函数说明: 1. 浅 V:共享 ui_deco 模块(两腿 3px / 水平线 2px,绘制次序左腿、右腿、水平线) + * 2. 状态行:静态字符串、白色,与旧 label 同字体同位置 + * 3. 重绘区不覆盖对应 culling 区则跳过(culling) */ static void view_deco_draw_cb(lv_event_t *e) { lv_layer_t *layer = lv_event_get_layer(e); - /* "浅 V"装饰:3 段线(线宽各异,逐段画) */ - if (areas_intersect(&s_deco_band, &layer->_clip_area)) { - for (uint8_t i = 0; i < 3; i++) { - lv_draw_line_dsc_t ld; - - lv_draw_line_dsc_init(&ld); - ld.base.layer = layer; - ld.color = lv_color_white(); - ld.width = s_deco_seg_w[i]; - ld.p1 = s_deco_segs[i][0]; - ld.p2 = s_deco_segs[i][1]; - lv_draw_line(layer, &ld); - } - } + /* "浅 V"装饰:共享 ui_deco 模块(几何/绘制次序/culling 均在其内) */ + ui_deco_draw(layer, &layer->_clip_area); /* 状态行:当前模式(静态文本) */ if (areas_intersect(&s_status_area, &layer->_clip_area)) { diff --git a/Drivers/BSP/UI/ui_page_ac.c b/Drivers/BSP/UI/ui_page_ac.c new file mode 100644 index 00000000..f9c37d5a --- /dev/null +++ b/Drivers/BSP/UI/ui_page_ac.c @@ -0,0 +1,59 @@ +/* + * 模块名称:UI Page AC (交流量内容页) + * 模块功能:交流量叶子节点的内容页数据——数据列表(24 项 / 8 项每页,共 3 页); + * 行槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=7 / 值 x=101(参考图 20 实测); + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ + +/* 头文件包含区 */ +#include "ui_page_ac.h" + +/* 静态变量定义区 */ + +/* 交流量行(参考图 20 = 第 1 页原样;第 2/3 页为占位数据,可替换) */ +static const cp_row_t s_rows[] = { + {"Ia1", "0.000"}, /* 第 1 页(参考图 20) */ + {"Ib1", "0.137"}, + {"Ic1", "0.274"}, + {"Ia2", "0.411"}, + {"Ib2", "0.548"}, + {"Ic2", "0.685"}, + {"Ua1", "0.82"}, + {"Ub1", "0.96"}, + {"Uc1", "1.03"}, /* 第 2 页(占位数据) */ + {"Uab", "1.17"}, + {"Ubc", "1.24"}, + {"Uca", "1.31"}, + {"Hz", "50.00"}, + {"P", "1.42"}, + {"Q", "0.87"}, + {"S", "1.63"}, + {"PF", "0.95"}, /* 第 3 页(占位数据) */ + {"Iavg","0.35"}, + {"Uavg","0.87"}, + {"Fmax","50.02"}, + {"Imax","1.24"}, + {"Pmax","2.10"}, + {"Qmax","1.05"}, + {"Smax","2.45"}, +}; + +/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;列表页,钩子全 NULL) */ +const content_page_def_t ui_page_ac_desc = { + .title = "交流量", + .selectable = 1u, + .top_y = 24, + .pitch = 14, + .band_h = 13, + .text_dy = 0, + .left_x = 7, + .right_x = 101, + .rows_per_page = 8u, + .row_count = 24, + .rows = s_rows, +}; diff --git a/Drivers/BSP/UI/ui_page_ac.h b/Drivers/BSP/UI/ui_page_ac.h new file mode 100644 index 00000000..f91a7ea8 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_ac.h @@ -0,0 +1,23 @@ +/* + * 模块名称:UI Page AC (交流量内容页) + * 模块功能:交流量叶子节点的内容页数据——数据列表(24 项 / 8 项每页,共 3 页; + * 第 1 页参考图 20 原样,第 2/3 页占位数据),可移选分页 + 选中白带; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ +#ifndef UI_PAGE_AC_H +#define UI_PAGE_AC_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):交流量页 = CONTENT_PAGE_AC; + * 接入框架描述符表 s_defs[](定义于 ui_page_ac.c) + */ +extern const content_page_def_t ui_page_ac_desc; + +#endif /* UI_PAGE_AC_H */ diff --git a/Drivers/BSP/UI/ui_page_dc.c b/Drivers/BSP/UI/ui_page_dc.c new file mode 100644 index 00000000..b157f06d --- /dev/null +++ b/Drivers/BSP/UI/ui_page_dc.c @@ -0,0 +1,37 @@ +/* + * 模块名称:UI Page DC (直流量内容页) + * 模块功能:直流量叶子节点的内容页数据——数据列表(3 项单页); + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ + +/* 头文件包含区 */ +#include "ui_page_dc.h" + +/* 静态变量定义区 */ + +/* 直流量行(参考图 21) */ +static const cp_row_t s_rows[] = { + {"ZL01", "3.01"}, + {"ZL02", "3.15"}, + {"Pa1", "3.288"}, +}; + +/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;列表页单页,钩子全 NULL) */ +const content_page_def_t ui_page_dc_desc = { + .title = "直流量", + .selectable = 1u, + .top_y = 24, + .pitch = 14, + .band_h = 13, + .text_dy = 0, + .left_x = 7, + .right_x = 101, + .rows_per_page = 8u, + .row_count = 3, + .rows = s_rows, +}; diff --git a/Drivers/BSP/UI/ui_page_dc.h b/Drivers/BSP/UI/ui_page_dc.h new file mode 100644 index 00000000..3eb29ae5 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_dc.h @@ -0,0 +1,23 @@ +/* + * 模块名称:UI Page DC (直流量内容页) + * 模块功能:直流量叶子节点的内容页数据——数据列表(3 项单页); + * 行几何与交流量页一致(参考图 21 实测); + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ +#ifndef UI_PAGE_DC_H +#define UI_PAGE_DC_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):直流量页 = CONTENT_PAGE_DC; + * 接入框架描述符表 s_defs[](定义于 ui_page_dc.c) + */ +extern const content_page_def_t ui_page_dc_desc; + +#endif /* UI_PAGE_DC_H */ diff --git a/Drivers/BSP/UI/ui_page_device_info.c b/Drivers/BSP/UI/ui_page_device_info.c new file mode 100644 index 00000000..61804629 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_device_info.c @@ -0,0 +1,45 @@ +/* + * 模块名称:UI Page Device Info (装置信息内容页) + * 模块功能:装置信息叶子节点的内容页数据——静态键值行(参考图 10,值列固定 x=86) + * + 副标题,单页,页脚按参考图原样"第 10 页,共 1 页"; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ + +/* 头文件包含区 */ +#include "ui_page_device_info.h" + +/* 静态变量定义区 */ + +/* 装置信息行(参考图 10;标签含全角冒号,值列固定 x=86) */ +static const cp_row_t s_rows[] = { + {"终端类型:", "F30"}, + {"终端型号:", "F30"}, + {"软件版本:", "SV0.010"}, + {"硬件版本:", "HW0.010"}, + {"软件校验:", "4454"}, + {"程序日期:", "2024.08.27"}, +}; + +/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;静态信息页,钩子全 NULL) */ +const content_page_def_t ui_page_device_info_desc = { + .title = "装置信息", + .subtitle = "馈线自动化终端", + .subtitle_y = 25, + .selectable = 0u, + .top_y = 40, + .pitch = 15, + .band_h = 0, + .text_dy = 0, + .left_x = 6, + .right_x = 86, + .rows_per_page = 0u, + .row_count = 6, + .rows = s_rows, + .fixed_cur_page = 10u, + .fixed_total_pages = 1u, +}; diff --git a/Drivers/BSP/UI/ui_page_device_info.h b/Drivers/BSP/UI/ui_page_device_info.h new file mode 100644 index 00000000..4850a366 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_device_info.h @@ -0,0 +1,22 @@ +/* + * 模块名称:UI Page Device Info (装置信息内容页) + * 模块功能:装置信息叶子节点的内容页数据(静态键值行 + 副标题,单页)—— + * 向框架 ui_content_page 提供页面描述符(数据/几何/钩子,零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:自 ui_content_page 单体拆出(数据原样迁移,零改动) + */ +#ifndef UI_PAGE_DEVICE_INFO_H +#define UI_PAGE_DEVICE_INFO_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):装置信息页 = CONTENT_PAGE_DEVICE_INFO; + * 接入框架描述符表 s_defs[](定义于 ui_page_device_info.c) + */ +extern const content_page_def_t ui_page_device_info_desc; + +#endif /* UI_PAGE_DEVICE_INFO_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index c448dcc6..a5100423 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -3128,6 +3128,26 @@ 1 ..\Drivers\BSP\UI\ui_content_page.c + + ui_deco.c + 1 + ..\Drivers\BSP\UI\ui_deco.c + + + ui_page_device_info.c + 1 + ..\Drivers\BSP\UI\ui_page_device_info.c + + + ui_page_ac.c + 1 + ..\Drivers\BSP\UI\ui_page_ac.c + + + ui_page_dc.c + 1 + ..\Drivers\BSP\UI\ui_page_dc.c + ui_menu_tab.c 1 diff --git a/docs/UI 设计/UI_设计架构.md b/docs/UI 设计/UI_设计架构.md index 7eb72716..421d8dd6 100644 --- a/docs/UI 设计/UI_设计架构.md +++ b/docs/UI 设计/UI_设计架构.md @@ -19,7 +19,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 | `Drivers/BSP/UI/ui_menu.c/h` | **共用** | 菜单导航逻辑层:持有导航状态(路径/选中/深度/滚动,共 17 B)+ 键语义,单向驱动显示层;不持 LVGL 对象 | | `Drivers/BSP/UI/ui_menu_view.c/h` | **共用** | 菜单显示层:横向级联框渲染(右贴靠/自适应宽/顶底对齐),持有全部 LVGL 对象与样式、无导航状态 | | `Drivers/BSP/UI/ui_menu_tab.c/h` | **共用** | 菜单数据层:`g_menu_tab[]` 父子索引表 + 查询 API(生成物,勿手改) | -| `Drivers/BSP/UI/ui_content_page.c/h` | **共用** | 内容页模块:装置信息/交流量/直流量三页(独立屏按需建毁,自绘行,零常驻池占用) | +| `Drivers/BSP/UI/ui_content_page.c/h` | **共用** | 内容页框架(机制 + 描述符表);装置信息/交流量/直流量三页由 `ui_page_xxx` 模块接入(独立屏按需建毁,自绘行,零常驻池占用);多页面"框架+页面插件"架构见 `内容页架构.md` | | `Drivers/BSP/UI/ui_net_sink.c/h` | MCU | RemoDispBus 服务端(收键 + 回送帧缓冲) | | `Drivers/BSP/UI/ui_font_cn_12.c` | **共用** | 中文 1bpp 子集字体(`lv_font_conv` 生成物) | | `App/task/ui_task.c/h` | MCU | UI 任务:等 NET_READY → 初始化 → 10ms 刷新循环 | @@ -193,6 +193,7 @@ ENTER/→ 进子框: - **导航状态不压层**:菜单 `s_depth/s_sel/s_scroll` 原样保留,内容页只记自身选中行 `s_sel`(每次进入从首行开始) - **池占用实测**(模拟器 x64,40KB 池):开页峰值 +776 B(屏+容器+事件列表+局部样式),关页后回落;一次性残留 **+80 B**(screens 数组高水位 +8、`lv_screen_load` 给菜单屏设 X/Y 局部样式 +72,均为 LVGL 核心行为、非本模块泄漏) - **flat 样式用常量样式**(`LV_STYLE_CONST_INIT`):属性存静态数组、零池分配;动态 `lv_style_t` 首次 set_prop 会从池分配 `values_and_props` 且永不释放(x64 实测 88 B/个,MCU 同量级),常驻样式一律用常量样式 +- **多页面架构(已定稿并拆分,2026-09-09)**:待实现 30 个叶子页;框架(本模块)+ 每页一文件(`ui_page_xxx`,描述符为 extern const 对象)+ 4 个扩展钩子(refresh/count_cb/draw_cb/key_cb),见 `内容页架构.md`;架构第一步(共享"浅 V"装饰 `ui_deco`)与拆分本身均于 2026-09-09 落地,纯重构回归全绿(像素一致、池 4784/4864 B 不变) ## 五、设计决策与考量(为什么这样做) @@ -255,3 +256,5 @@ ENTER/→ 进子框: | 2026-09-08 | 菜单 RAM 收敛两连:行对象→框体自绘(行降为数据,§4.4/§5.5);4 层容器→单一持久容器(焦点恒在其上,框深度由逻辑层 `s_depth` 决定,§4.3/§5.4)+ 根框装饰/状态行容器自绘(无对象);LVGL 池实测 根框 4872 B / 最深四级级联 6128 B | | 2026-09-09 | 内容页模块 `ui_content_page`(§4.7):装置信息/交流量/直流量三叶子页,独立屏按需建毁 + 行自绘;flat 样式改常量样式(`LV_STYLE_CONST_INIT`,零池分配,§5.6);字体 78→97 汉字(193 字形) | | 2026-09-09 | 菜单显示层 flat 样式同法转常量样式(§5.6):LVGL 池基线 4872→4784 B、最深四级级联 6128→6040 B(x64 省 88 B,MCU 同量级);`LV_STYLE_CONST_INIT` 生成外部链接符号,内容页同名者改名 `cp_style_flat` 避免两文件冲突 | +| 2026-09-09 | 内容页多页面架构定稿(`内容页架构.md`):框架+页面插件模型(每页一文件 `ui_page_xxx` + 4 个扩展钩子)、30 个待实现叶子页清单、新增页面六步流程;同日"浅 V"装饰提取为共享 `ui_deco` 模块(架构第一步,菜单根框/内容页共用,像素回归验证一致) | +| 2026-09-09 | 内容页拆分执行(`内容页架构.md` §九 变更记录):`ui_content_page` 提纯为纯框架(描述符表 + NULL 防护 + 描述符驱动绘制/按键、4 个扩展钩子);三页数据拆出 `ui_page_device_info` / `ui_page_ac` / `ui_page_dc`(extern const 描述符对象);双端注册;Keil 0 错 0 警 + 模拟器 0 警告 + `ui-verify` 五门禁全绿(像素一致、池 4784/4864 B 不变) | diff --git a/docs/UI 设计/mockups/密码页面实际机器的示例图片.jpg b/docs/UI 设计/mockups/密码页面实际机器的示例图片.jpg new file mode 100644 index 00000000..316bf025 --- /dev/null +++ b/docs/UI 设计/mockups/密码页面实际机器的示例图片.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1401177b4d28ee9a3dcd4fe64e6b4710d03de82ff908b0e5b274a14466be0d35 +size 66895 diff --git a/docs/UI 设计/内容页架构.md b/docs/UI 设计/内容页架构.md new file mode 100644 index 00000000..3981031b --- /dev/null +++ b/docs/UI 设计/内容页架构.md @@ -0,0 +1,433 @@ +# 内容页架构 + +> **用途**:说明内容页体系在代码中**实际如何工作**(框架机制 / 页面插件 / 数据流 / 键与绘制行为 / 内存), +> 并给出新增页面的标准步骤。全文与当前代码一一对应;页面像素几何(行槽/选中带/页脚实测值) +> 见 `菜单页面设计.md` §九。 +> **配套文档**:`UI_设计架构.md`(§1.2 文件地图 / §4.7 内容页 / §5.6 常量样式)、`菜单页面设计.md`(§九 内容页)、 +> `UI_界面制作流程.md`(参考图→代码管线)、`UI_自动化测试流程.md`(回归门禁与黄金参考)。 + +--- + +## 一、什么是内容页 + +菜单**叶子节点** ENTER 后进入的**独立全屏页面**:新建一块 LVGL 屏幕,全部内容(装饰 + 标题 + 行 + 页脚) +由框架一次性自绘(**无子对象**),ESC 返回菜单屏。当前已接 3 页: + +| 页面 | 菜单节点 | 形态 | 数据 | 描述符对象 | +|---|---|---|---|---| +| 装置信息 | node 1 | 静态信息页(6 行 + 副标题,不分页) | Flash 表 | `ui_page_device_info_desc` | +| 交流量 | node 3 | 列表页(24 项,8 项/页,共 3 页) | Flash 表(第 1 页实测,2/3 页占位) | `ui_page_ac_desc` | +| 直流量 | node 4 | 列表页(3 项,单页) | Flash 表 | `ui_page_dc_desc` | + +设计核心是 **"框架 + 页面插件"**:框架 `ui_content_page` 只持**机制**(屏/容器生命周期、焦点、 +按键分发、通用绘制),每页的**数据/几何/钩子**由独立 `ui_page_xxx` 模块经描述符 `content_page_def_t` +提供;两者只以描述符相遇——框架不持任何页面数据,页面模块不碰任何 LVGL 对象。 + +### 1.1 系统总览 + +```mermaid +flowchart TD + K(["按键 / LVGL 刷新
(均在 uiTask 上下文,10ms 周期)"]) --> M["逻辑层 ui_menu.c
导航状态机(17 B)+ 键语义
menu_content_page_for:叶子节点 → 页 ID"] + + subgraph FW["框架 ui_content_page.c/h —— 只有机制,零页面数据"] + S[("描述符表 s_defs[]
页 ID → const content_page_def_t *
(几何 + 数据源 + 4 钩子)")] + L1["屏/容器生命周期
按需建毁 · 焦点 · 切屏"] + L2["按键分发
内置 ↑↓/ESC + key_cb 钩子"] + L3["通用绘制(LV_EVENT_DRAW_MAIN)
浅 V → 标题/副标题 → 行 → 选中带 → 页脚 → draw_cb"] + end + + M -- "ENTER:ui_content_page_open(id)" --> S + S -- "描述符驱动" --> L1 + S -- "描述符驱动" --> L2 + S -- "描述符驱动" --> L3 + L2 -- "ESC/←:close()" --> L1 + + subgraph PG["页面插件 —— 数据(Flash)+ 几何 + 钩子实现,零 LVGL 对象"] + P1["ui_page_device_info
装置信息(静态信息页)"] + P2["ui_page_ac
交流量(列表页)"] + P3["ui_page_dc
直流量(列表页)"] + PN["ui_page_xxx
扩展点:每新增页一文件"] + end + + P1 -- "const 描述符(Flash)" --> S + P2 -- "const 描述符(Flash)" --> S + P3 -- "const 描述符(Flash)" --> S + PN -- "const 描述符(Flash)" --> S + + L3 -- "ui_deco_draw" --> D["ui_deco(共享)
顶部“浅 V”装饰"] + L3 -- "动态行:refresh / count_cb" --> PG + L2 -- "页面自有键:key_cb" --> PG +``` + +读图要点: + +- **数据只向下一层流**:按键 → 逻辑层 → 框架;页面插件不主动调任何人,只被动提供描述符与钩子 +- **框架 ↔ 页面只以描述符相遇**:框架不持页面数据,页面不碰 LVGL 对象 +- 反向调用(`refresh/count_cb/draw_cb/key_cb`)是**框架回调页面**,方向与依赖方向相反 +- 菜单根框与内容页共用同一"浅 V"装饰(`ui_deco` 模块,几何唯一定义处) + +## 二、系统组成 + +| 文件 | 角色 | 一句话职责 | +|---|---|---| +| `ui_content_page.c/h` | **框架** | 屏/容器生命周期 + 按键分发 + 描述符驱动通用绘制 + 描述符表;公开 `content_page_id_t` / `content_page_def_t` / `cp_row_t` 与 3 个 API | +| `ui_page_device_info.c/h` | 页面插件 | 装置信息页:6 行 Flash 表 + 描述符对象 `ui_page_device_info_desc` | +| `ui_page_ac.c/h` | 页面插件 | 交流量页:24 行 Flash 表 + 描述符对象 `ui_page_ac_desc` | +| `ui_page_dc.c/h` | 页面插件 | 直流量页:3 行 Flash 表 + 描述符对象 `ui_page_dc_desc` | +| `ui_deco.c/h` | 共享 | 顶部"浅 V"装饰(3 段线 + culling),菜单根框/内容页共用 | +| `ui_menu.c` | 逻辑层 | `menu_content_page_for` 叶子节点 → 页 ID 映射;ENTER 叶子时调 `ui_content_page_open` | +| `ui_menu_view.c` | 逻辑层依赖 | `ui_content_page_close` 经 `ui_menu_view_container()` 把焦点切回菜单持久容器 | + +依赖方向:`ui_menu(逻辑)→ ui_content_page(框架)→ ui_page_xxx(页面数据)`; +页面模块只依赖框架头文件(取 `content_page_def_t` / `cp_row_t` 类型),**不反向调用任何人**。 + +## 三、框架机制(ui_content_page.c) + +### 3.1 公开 API + +| API | 作用 | 关键行为(代码实际) | +|---|---|---| +| `bool ui_content_page_open(id)` | 开页 | ① 查 `s_defs[id]`:NULL → 打 `ERROR` 日志并返回 false(枚举/表漂移防护)② `lv_obj_create(NULL)` 建屏 + 全屏容器(flat 常量样式 + `IGNORE_LAYOUT` + `CLICK_FOCUSABLE`)③ 容器**先**入默认输入组并获焦 ⓸ `lv_screen_load` 切屏 | +| `void ui_content_page_close()` | 关页 | ① 焦点**先**回菜单持久容器(防输入组焦点悬空)② 切回菜单屏(菜单屏对象未动,原样还原)③ 再删容器+屏,池内存回落 | +| `bool ui_content_page_is_open()` | 查询 | `s_open != CONTENT_PAGE_COUNT` | + +框架持有的运行状态(全部 4 个变量,约 20 B): + +```c +static content_page_id_t s_open; /* 当前打开的页(CONTENT_PAGE_COUNT = 未打开) */ +static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续;每次进入清零) */ +static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */ +static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */ +``` + +### 3.2 页面生命周期:一次 ENTER → 一次 ESC + +```mermaid +sequenceDiagram + participant U as uiTask(按键 / 10ms 刷新) + participant M as 逻辑层 ui_menu.c + participant F as 框架 ui_content_page + participant P as 页面模块 ui_page_xxx + participant D as ui_deco(共享装饰) + + Note over M,F: 菜单屏:焦点 = 菜单持久容器,按键归逻辑层 + U->>M: ENTER(LV_EVENT_KEY → menu_key_cb) + M->>M: menu_content_page_for(node):叶子节点 → 页 ID + M->>F: ui_content_page_open(id) + F->>F: s_defs[id] 查描述符(NULL → 返回 false + 错误日志) + F->>F: 建屏 + 全屏容器(flat 常量样式)→ 入输入组获焦 → 切屏 + Note over F: 内容页:焦点 = 页面容器,按键归框架键分发 + F->>D: DRAW_MAIN:ui_deco_draw(浅 V) + F->>F: 标题/副标题(居中) + F->>P: refresh(row, name, value)(动态页逐行取数;同帧同步光栅化,栈缓冲安全) + F->>P: count_cb(动态行数) + F->>F: 行 + 选中白带 + 页脚页码 + F->>P: draw_cb(表单/操作页:自定义内容) + U->>F: ESC/←(LV_EVENT_KEY → 框架键分发) + F->>F: close():焦点先回菜单持久容器 → 切回菜单屏 → 再销毁页面屏 + Note over M: 菜单导航状态(s_depth/s_sel/s_scroll)原样保留,返回即还原 +``` + +要点: + +- **导航状态不压层**:进内容页不动菜单 `s_depth/s_sel/s_scroll`,ESC 返回即原样还原 +- 菜单屏对象**保留不销毁**(`lv_screen_load` 来回切,屏上对象原样还原) +- 页面按需建毁:任一时刻至多一个"屏 + 容器"存在 +- 焦点/键入口在切屏**之前**接线(先入组获焦,后 `lv_screen_load`),按键立即由框架接管 + +### 3.3 描述符表与页面注册 + +```c +/* ui_content_page.c(实际代码) */ +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, +}; +``` + +- 每页一行:`页 ID → &描述符对象`;绘制/按键逻辑一律经 `s_defs[s_open]` 取当前页描述符 +- **描述符是 extern const 对象**(头文件声明、页面模块 `.c` 定义),框架表直接存其**地址** + > ⚠️ 为什么不用"访问器函数"建表:ARMCC v5 禁止用函数名初始化对象指针数组(error #144, + > 拆分实施时实测命中)。extern const 对象模式与菜单数据层 `g_menu_tab[]` 一致,且天然落 Flash。 +- **NULL 防护**:`open()` 时查表为 NULL(枚举加了值但表漏加行)→ 拒开 + `ERROR` 日志,不崩溃 + +### 3.4 按键分发(cp_key_cb) + +```mermaid +flowchart TD + A(["按键(焦点容器 → LV_EVENT_KEY → cp_key_cb)"]) --> B{"ESC / ← ?"} + B -- 是 --> C["ui_content_page_close()
关页返回菜单(框架保证,页面钩子不可拦截)"] + B -- 否 --> D{"key_cb 钩子非空
且返回 true ?"} + D -- 是 --> E["页面已处理(表单/操作页自有键)
框架跳过内置语义"] + D -- 否 --> F{"↑ / ↓ ?"} + F -- 是 --> G{"selectable=1 且
总行数 > 0 ?"} + G -- 是 --> H["移选(首尾环绕,跨页连续)
选中行越界 → 重置首行(防动态行数缩小)
lv_obj_invalidate 触发重绘"] + G -- 否 --> I["不响应(信息页 / 空表)"] + F -- 否 --> J["其余键值:忽略"] +``` + +行为细节(与代码一致): + +- **ESC/← 恒返回**:最先判断,`key_cb` 无法拦截(框架保证页面可退出) +- **key_cb 先于内置 ↑↓**:返回 `true` = 页已处理,框架不再执行内置移选 +- **↑↓ 移选**:`(sel + total ∓ 1) % total` 首尾环绕;`s_sel` 是全局下标,跨页连续 +- **越界防护**:`s_sel >= total` 时先重置首行(为动态行数可能缩小而设;静态页恒不触发) +- 移选只改 `s_sel` + `lv_obj_invalidate(s_cont)` 触发重绘,**不重建任何对象** + +### 3.5 通用绘制链(cp_draw_cb,LV_EVENT_DRAW_MAIN) + +```mermaid +flowchart TD + A(["LV_EVENT_DRAW_MAIN → cp_draw_cb"]) --> B["① 浅 V 装饰:ui_deco_draw(共享模块,内置 culling)
② 标题(居中,y=2) ⓢ 副标题(居中,y=subtitle_y;仅非空页)"] + B --> C["④ 行区块
列表页:窗口 = 选中行所在页(首可见行 = (sel/每页数)×每页数)
信息页:全量;行槽位置固定,数据随页切换"] + C --> C1{"行数据来源"} + C1 -- "静态:rows 表非空" --> C2["直绘 Flash 表(text_static=1,不拷内存)"] + C1 -- "动态:refresh 非空" --> C3["refresh() 逐行取入栈缓冲(32 B×2,同帧同步光栅化)"] + C2 --> D + C3 --> D["逐行:culling 判交 → 选中行白带(x=2..156,高 band_h)
→ 左/右两列 lv_draw_label(选中黑字,其余白字)"] + D --> E["⑤ 页脚“第 X 页,共 Y 页”(y=143,x=6)
列表页:随选中行现算;信息页:fixed_cur/total 固定值"] + E --> F{"draw_cb 非空 ?"} + F -- 是 --> G["⑥ 页面自定义内容(表单/操作页,在内置区块之后)"] + F -- 否 --> H(["绘制完成"]) + G --> H +``` + +行为细节(与代码一致): + +- **行窗口**:`first = (s_sel / rows_per_page) * rows_per_page`,`cnt` 截断到 `rows_per_page` + (末页不足整页);信息页(`selectable=0`)全量绘制 +- **culling**:每个区块(标题/副标题/行条带/页脚)先判"重绘区 ∩ 该区块 ≠ ∅"(`cp_areas_intersect`, + 与 LVGL `lv_area_intersect` 等价的本地实现,避免依赖私有头),不交则整块跳过 +- **选中带**:`band_h > 0` 且该行选中 → 整行白带 `x=2..156`(参考图实测),文字随之黑字 +- **文本**:静态行 `text_static=1`(Flash 字符串不拷贝);动态行与页脚页码用栈缓冲 + (`lv_draw_label` 在同一刷新周期内**同步**光栅化,指针仅需调用期间有效) +- **页脚**:`lv_snprintf` 现算(MSVC 对 `snprintf` 中文格式串误报 C4819,故用 LVGL 自带 `lv_snprintf`) +- 容器样式为**常量样式**(`LV_STYLE_CONST_INIT`,零 LVGL 池分配),见 `UI_设计架构.md` §5.6 + +## 四、页面描述符(content_page_def_t) + +```c +/* ui_content_page.h(实际代码) */ +typedef struct { + const char *title; /* 标题(浅 V 区居中) */ + const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */ + int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */ + uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */ + int32_t top_y; /* 首行槽顶 y */ + int32_t pitch; /* 行距(相邻行槽顶间距) */ + int32_t band_h; /* 选中白带高(0 = 无选中带) */ + int32_t text_dy; /* 文本放置相对行槽顶的垂直偏移 */ + int32_t left_x; /* 左列 x(信息页=标签、列表页=名) */ + int32_t right_x; /* 右列 x(值) */ + uint8_t rows_per_page; /* 每页可见行数(0 = 不分页) */ + uint16_t row_count; /* 总行数(静态页;动态页用 count_cb) */ + const cp_row_t *rows; /* 静态数据表(Flash);NULL = 动态页 */ + uint8_t fixed_cur_page; /* 信息页页脚"第 X 页"固定显示值 */ + uint8_t fixed_total_pages; /* 信息页页脚"共 Y 页"固定显示值 */ + /* —— 扩展钩子(全可选;NULL = 纯内置行为;均运行在 uiTask 上下文)—— */ + void (*refresh)(uint16_t row, char *name, char *value); /* 动态行:逐行取数(名/值) */ + uint16_t (*count_cb)(void); /* 动态行数(记录页) */ + void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */ + bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */ +} content_page_def_t; +``` + +字段一览(含当前 3 页实际值): + +| 字段 | 类型 | 作用 | 装置信息 | 交流量 | 直流量 | +|---|---|---|---|---|---| +| `title` | `const char *` | 标题(浅 V 区居中) | "装置信息" | "交流量" | "直流量" | +| `subtitle` | `const char *` | 副标题;NULL = 无 | "馈线自动化终端" | NULL | NULL | +| `subtitle_y` | `int32_t` | 副标题放置 y | 25 | — | — | +| `selectable` | `uint8_t` | 1 = 列表页(↑↓+分页);0 = 信息页 | 0 | 1 | 1 | +| `top_y` | `int32_t` | 首行槽顶 y | 40 | 24 | 24 | +| `pitch` | `int32_t` | 行距 | 15 | 14 | 14 | +| `band_h` | `int32_t` | 选中白带高(0 = 无) | 0 | 13 | 13 | +| `text_dy` | `int32_t` | 文本垂直偏移 | 0 | 0 | 0 | +| `left_x` / `right_x` | `int32_t` | 左列(标签/名)/ 右列(值)x | 6 / 86 | 7 / 101 | 7 / 101 | +| `rows_per_page` | `uint8_t` | 每页行数(0 = 不分页) | 0 | 8 | 8 | +| `row_count` | `uint16_t` | 总行数(静态页) | 6 | 24 | 3 | +| `rows` | `const cp_row_t *` | 静态数据表(Flash) | 6 行 | 24 行 | 3 行 | +| `fixed_cur_page` / `fixed_total_pages` | `uint8_t` | 信息页页脚固定值 | 10 / 1 | — | — | +| `refresh` / `count_cb` / `draw_cb` / `key_cb` | 函数指针 | 扩展钩子 | 全 NULL | 全 NULL | 全 NULL | + +扩展钩子(机制已实现,当前 3 页均未使用): + +| 钩子 | 签名 | 框架调用时机(uiTask 上下文) | 用途 | +|---|---|---|---| +| `refresh` | `void (uint16_t row, char *name, char *value)` | 绘制行区块时,每行一次、`lv_draw_label` 之前 | 动态行:逐行取名/值(遥信量、记录页等) | +| `count_cb` | `uint16_t (void)` | 绘制(行窗口/页脚)与按键(移选取模)经 `cp_row_total()` 取行数 | 动态行数(记录查询类) | +| `draw_cb` | `void (lv_layer_t *layer, const lv_area_t *clip)` | 绘制链末尾(内置区块之后) | 表单/操作页自定义内容 | +| `key_cb` | `bool (lv_key_t key)` | 键分发时(ESC 之后、内置 ↑↓ 之前) | 页面自有键(ENTER 确认等);返回 true = 页已处理 | + +### 4.1 表单/操作页首个应用:密码页(待实现) + +实机(淮源电力 F30)已有"用户密钥"页:进入某些子叶面前须先输入密码(实机照片见 +`mockups/密码页面实际机器的示例图片.jpg`)。它是本项目**第一个表单/操作页**,落地形态即 +`draw_cb` + `key_cb` 组合。 + +**功能规则(已确认)** + +- 密码共 **3 位**;子页(叶子)可配置"是否需要密码页",每个受保护页可设**专属密码** +- 受保护叶子 ENTER:先开密码页;输入与专属密码一致后按确认键进入目标页;ESC 随时返回菜单 +- 键语义(全部由页面 `key_cb` 处理;`selectable=0`,框架内置 ↑↓ 移选不适用): + +| 键 | 行为 | +|---|---| +| ↑ / ↓ | 调整光标位数字值(0–9,环绕:9↔0) | +| ← / → | 移动光标(反显框)左/右(环绕:首框↔末框) | +| ENTER | 确认:一致 → 关密码页并打开目标页;不一致 → 3 框清空回 `0`、光标回首框,重输(已确认) | +| ESC | 返回菜单(框架保证,不可拦截) | + +**实机视觉(照片;✅ 已确认按此实现)** + +- 标题"用户密钥"居中、位于屏中上部(约屏高 1/4 处);**无**"浅 V"装饰、**无**页脚 +- 3 个方框单行居中(约屏高 1/2 处),均**预填 `0`**:首框**反显(黑底白字)= 光标位**, + 其余 2 框白底黑字、细边框 + +**框架映射(描述符配置)** + +| 字段 | 值 | +|---|---| +| `selectable` | 0(无内置移选) | +| `rows` / `row_count` | NULL / 0 | +| `draw_cb` | 非空:绘制标题 + 3 个数字框(光标框反显) | +| `key_cb` | 非空:↑↓ / ←→ / ENTER 按上表 | +| `title` | "用户密钥"(y≈45 中上、"浅 V"关 —— 跟随实机;需下方 ⚠️ 扩展 2) | + +**⚠️ 框架扩展需求(落地前必须完成)** + +1. **← 键拦截(✅ 已确认:描述符拦截开关)**:当前 `cp_key_cb` 将 `LV_KEY_LEFT` 与 ESC 同等对待:在调用 `key_cb` + **之前**即无条件关页返回(ui_content_page.c:345,注释明示"页面钩子不可拦截")——密码页的光标 + ←/→ 移动会被框架吞掉。方案:描述符增加拦截标志(如 `uint8_t key_intercept`),置位时 `LV_KEY_LEFT` + **先**交 `key_cb`,返回 false 才执行内置返回;`LV_KEY_ESC` **不受**拦截("页面恒可退出"的框架 + 保证不放松)。现有 3 页不置标志,行为完全不变 +2. **每页标题 y / "浅 V" 开关(✅ 需要,视觉已确认跟随实机)**:绘制链当前固定标题 `CP_TITLE_Y=2` + 且恒绘"浅 V";实机密码页无装饰、标题在中上部 → 描述符增加标题 y 与装饰开关字段 + +**设计确认进度** + +| # | 事项 | 状态 | +|---|---|---| +| 1 | 密码不一致行为 | ✅ 已确认:清空重输(3 框回 `0`、光标回首框) | +| 2 | 每页专属密码的存放与设置 | ✅ 已确认:配置页可设、存 Flash 配置区(配置页本身亦是后续表单/操作页) | +| 3 | 视觉风格 | ✅ 已确认:跟随实机照片(无"浅 V"、标题中上)→ 需 ⚠️ 扩展 2 | +| 4 | ← 键冲突解法 | ✅ 已确认:方案 A 描述符拦截开关(`key_intercept`);`LV_KEY_LEFT` 先交页面钩子,ESC 恒不可拦截,现有 3 页不置开关、零影响 | +| 5 | 边界环绕行为 | ✅ 已确认:都环绕(数字 9↔0,光标首框↔末框) | + +## 五、页面模块(ui_page_xxx) + +每页一个 `.c/h` 文件对,只含**数据(Flash)+ 描述符对象**,零 LVGL 对象、零机制代码。 +以装置信息页为例(`ui_page_device_info.c` 实际代码节选): + +```c +/* 头文件包含区 */ +#include "ui_page_device_info.h" /* 内含 ui_content_page.h,并声明描述符对象 */ + +/* 静态变量定义区 */ + +static const cp_row_t s_rows[] = { + {"终端类型:", "F30"}, + {"终端型号:", "F30"}, + {"软件版本:", "SV0.010"}, + /* … 共 6 行(参考图 10) */ + {"程序日期:", "2024.08.27"}, +}; + +/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用) */ +const content_page_def_t ui_page_device_info_desc = { + .title = "装置信息", + .subtitle = "馈线自动化终端", + .subtitle_y = 25, + .selectable = 0u, + .top_y = 40, + .pitch = 15, + .band_h = 0, + .text_dy = 0, + .left_x = 6, + .right_x = 86, + .rows_per_page = 0u, + .row_count = 6, + .rows = s_rows, + .fixed_cur_page = 10u, + .fixed_total_pages = 1u, +}; +``` + +```c +/* ui_page_device_info.h(对外只有两个声明) */ +#include "ui_content_page.h" + +extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash const) */ +``` + +- 初始化器按**声明顺序**写指定域(ARMCC v5 = C99 要求顺序一致);省略的钩子字段零初始化为 NULL +- 行表 `s_rows[]` 是模块私有(`static`),只有描述符对象是外部链接 +- 动态页的差异:`rows = NULL`,改在描述符里给 `refresh`(逐行取数),行数变化再给 `count_cb` +- 文件编码:`Drivers/BSP/UI/` 下为 **UTF-8 带 BOM**(中文字面量依赖 BOM 让 MSVC 认 UTF-8, + 见 AGENTS.md 编码铁律) + +## 六、执行上下文与内存 + +**执行上下文** + +- 框架全部回调(绘制 `cp_draw_cb`、键 `cp_key_cb`、4 个页面钩子)运行在 **uiTask 上下文** + (LVGL 刷新与按键都走 `ui_task_func` 主循环,10 ms 周期) +- `refresh` 取到的动态文本在同一刷新周期内经 `lv_draw_label` **同步**光栅化 → 32 B 栈缓冲即安全 +- 跨任务数据(ADC/网络/RTC/存储)沿用既有约定:生产者任务写快照(原子/队列), + UI 任务在钩子里读快照;框架与页面模块**不加锁** + +**内存(实测,40 KB LVGL 池)** + +| 项 | 值 | 说明 | +|---|---|---| +| 框架常驻 RAM | ~20 B | `s_open/s_sel/s_scr/s_cont` 4 变量 | +| 开页峰值 | +776 B | 屏 + 容器 + 事件列表 + 常量样式引用 | +| 关页回落 | 基线 +80 B 一次性残留 | screens 数组高水位 +8、`lv_screen_load` 菜单屏局部 X/Y 样式 +72(LVGL 核心行为,非泄漏;分解见 `菜单页面设计.md` §9.5) | +| 池水位门禁 | 4784 B / 4864 B | 菜单基线 / 开页-关页往返(回归脚本常量) | +| 描述符/行表/标题 | Flash const | 不进 LVGL 池;每页增量 = 描述符 ~40 B + 行表 + 代码 | +| 动态行栈缓冲 | 2×32 B | 仅绘制期存在 | + +## 七、新增页面(标准步骤) + +| # | 动作 | 改哪里 | +|---|---|---| +| 1 | `content_page_id_t` 加一个枚举值 | `ui_content_page.h` | +| 2 | 新建 `ui_page_xxx.c/h`:行表(或回调)+ `extern const content_page_def_t ui_page_xxx_desc`(头文件声明 + `.c` 定义) | `Drivers/BSP/UI/` | +| 3 | 描述符表加一行 `[新 ID] = &ui_page_xxx_desc` | `ui_content_page.c` `s_defs[]` | +| 4 | 叶子节点 → 页 ID 映射加一个 case | `ui_menu.c` `menu_content_page_for` | +| 5 | 双端注册(显式文件列表,无通配) | `MDK-ARM/*.uvprojx`(Drivers/BSP/UI 组)+ `simulator/CMakeLists.txt`(`UI_SOURCES`) | +| 6 | 回归验证 | `ui-verify` 五门禁;新页在 `check_content_pages.py` 登记黄金参考;含新汉字先重生成字体 | + +按页面形态选描述符配置: + +| 页面形态 | 配置要点 | +|---|---| +| 静态信息页 | `selectable=0`、`rows`=Flash 表、`fixed_cur_page/fixed_total_pages` 定页脚 | +| 静态列表页 | `selectable=1`、`rows_per_page` 分页、`rows`=Flash 表(钩子全 NULL) | +| 动态数据页 | `rows=NULL` + `refresh`(逐行取数);行数随数据变再加 `count_cb` | +| 表单/操作页 | `selectable=0`、`rows=NULL`、`draw_cb` 自绘内容 + `key_cb` 自有键(ESC 返回仍由框架保证;首个实例=密码页,见 §4.1) | + +## 八、验证与回归 + +- **门禁**(`ui-verify` skill / `UI_自动化测试流程.md`): + 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 3 内容页黄金参考逐字节比对 + + 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查 +- **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 4784 B`、`POOL_AFTER_ROUNDTRIP = 4864 B` +- **失败处置**:像素不一致 = 代码 bug(修代码,**禁止**刷参考掩盖);**有意**改视觉 → + 先肉眼确认新效果正确,再 `check_content_pages.py --update` 刷参考并随代码提交 +- 新增页面后同步在 `check_content_pages.py` 增加该页的按键序列与参考图 + +## 九、变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-09-09 | 架构定稿:"框架 + 页面插件"模型,新增页面六步流程 | +| 2026-09-09 | 总览图由 ASCII 框线升级为 Mermaid(结构 flowchart + 生命周期 sequenceDiagram) | +| 2026-09-09 | 拆分执行:`ui_content_page` 提纯为框架(描述符表 + NULL 防护 + 描述符驱动绘制/按键、4 扩展钩子);三页拆出 `ui_page_device_info` / `ui_page_ac` / `ui_page_dc`(extern const 描述符对象);双端注册;五门禁全绿(像素一致、池 4784/4864 B 不变);描述符定稿 extern const 对象模式(ARMCC #144:函数名不能入对象指针表) | +| 2026-09-09 | 按实际代码重写(本版):章节重组为"总览 → 组成 → 框架机制 → 描述符 → 页面模块 → 上下文与内存 → 新增页面 → 验证";去掉规划性页面清单与拆分前执行清单;图扩充为 4 张 Mermaid(系统总览 / 页面生命周期 / 按键分发 / 绘制链);全部内容逐项核对当前代码 | +| 2026-09-09 | 密码页(首个表单/操作页,待实现)设计落档(§4.1):3 位密码、每页专属密码、每页"是否需要密码页"配置、↑↓ 调值 / ←→ 移光标 / ENTER 确认进入;实机视觉参考 `mockups/密码页面实际机器的示例图片.jpg`;同时发现框架冲突:`cp_key_cb` 对 `LV_KEY_LEFT` 先于钩子拦截(ui_content_page.c:345),落地前需描述符拦截标志扩展(ESC 恒不可拦截);4 项待确认(错误行为 / 密码存放 / 环绕行为 / 视觉风格) | +| 2026-09-09 | 密码页设计确认 3 项(§4.1):不一致 = 清空重输;每页专属密码 = 配置页可设 + 存 Flash 配置区;视觉 = 跟随实机照片(由此确定绘制链扩展需求:每页标题 y / "浅 V" 开关);← 键拦截方案与环绕行为仍待确认 | +| 2026-09-09 | 密码页 ← 键冲突解法确认(§4.1):方案 A 描述符拦截开关(`key_intercept`:`LV_KEY_LEFT` 先交页面钩子、ESC 恒不可拦截、现有 3 页零影响);框架扩展清单定稿(拦截标志 + 每页标题 y / "浅 V" 开关),仅剩环绕行为待确认 | +| 2026-09-09 | 密码页设计定稿(§4.1):边界行为 = 都环绕(数字 9↔0、光标首框↔末框);5 项设计全部确认、无遗留待确认——具备实施条件(框架扩展 + 新页面模块 + 每页保护标志/密码) | diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index fd3d1005..3a09ae20 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -70,6 +70,10 @@ set(UI_SOURCES ${UI_DIR}/ui_menu.c ${UI_DIR}/ui_menu_view.c ${UI_DIR}/ui_content_page.c + ${UI_DIR}/ui_deco.c + ${UI_DIR}/ui_page_device_info.c + ${UI_DIR}/ui_page_ac.c + ${UI_DIR}/ui_page_dc.c ${UI_DIR}/ui_menu_tab.c ${UI_DIR}/ui_font_cn_12.c )