增加两页UI

This commit is contained in:
2026-09-09 13:18:06 +08:00
parent 9ead5679a1
commit 24c2f57555
24 changed files with 1436 additions and 122 deletions
@@ -27,13 +27,14 @@
安装完成后验证可用:`lv_font_conv -h` 能打印用法即成功。
> **本机 Python 解释器**:PATH 默认 `python` 是 msys64 的(无 pip/fonttools),直接跑 `make_font.py` 会失败。用 conda base 环境(已预装 fontTools):
> **Python 解释器(统一入口)**:用仓库内 `.venv`(已装 fontTools,见 AGENTS.md「Python 环境」):
>
> ```powershell
> & 'C:\ProgramData\anaconda3\python.exe' tools/font_gen/make_font.py
> & '.venv\Scripts\python.exe' tools/font_gen/make_font.py
> ```
>
> `extract_cn.py` 纯标准库,任意 python3 均可。换机器时若该解释器缺 fonttools,用其自带包管理器安装(如 `& '...\python.exe' -m pip install fonttools`)。
> PATH 默认 `python` 是 msys64 的(无 fonttools),直接跑会失败。`.venv` 缺失/换机时重建:`& 'C:\ProgramData\anaconda3\python.exe' -m venv .venv; & '.venv\Scripts\python.exe' -m pip install -r requirements.txt`。
> `extract_cn.py` 纯标准库,任意 python3 均可。
---
@@ -54,7 +55,7 @@ scanned 13 files; unique non-ASCII chars: 78
-> D:\Code\DTU Code\STM32F4-Base\ui_cn_chars.txt
```
产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 78 字),可留作字符集记录。
产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 97 字),可留作字符集记录。
---
@@ -62,21 +63,21 @@ scanned 13 files; unique non-ASCII chars: 78
已落库脚本 `tools/font_gen/make_font.py`:自动定位 `lv_font_conv`(全局安装或 PATH)、自动处理宋体 `.ttc` 的 face0 抽取,直接生成 `Drivers/BSP/UI/ui_font_cn_12.c`。
**前置依赖(仅需一次):**
**前置依赖(仅需一次,统一装进仓库 `.venv`):**
```bash
npm i -g lv_font_conv # TTF/OTF -> LVGL 字体 离线转换器
pip install fonttools # 仅当使用 .ttc 集合字体(如宋体)时需要
.venv\Scripts\python.exe -m pip install fonttools # 仅当使用 .ttc 集合字体(如宋体)时需要;已含于 requirements.txt
```
> 若 `npm i -g lv_font_conv` 报代理错误,先清代理:`npm config delete proxy` / `npm config delete https-proxy`。
**执行命令(在仓库根目录,用 §2.1 的 conda base 解释器):**
**执行命令(在仓库根目录,用 §2.1 的 `.venv` 解释器):**
```powershell
& 'C:\ProgramData\anaconda3\python.exe' tools/font_gen/make_font.py # 默认:宋体, 12px, 1bpp
& 'C:\ProgramData\anaconda3\python.exe' tools/font_gen/make_font.py --bpp 2 # 宋体细笔画发虚时改用 2bpp
& 'C:\ProgramData\anaconda3\python.exe' tools/font_gen/make_font.py --font C:/path/to/font.ttf # 换其他字体
& '.venv\Scripts\python.exe' tools/font_gen/make_font.py # 默认:宋体, 12px, 1bpp
& '.venv\Scripts\python.exe' tools/font_gen/make_font.py --bpp 2 # 宋体细笔画发虚时改用 2bpp
& '.venv\Scripts\python.exe' tools/font_gen/make_font.py --font C:/path/to/font.ttf # 换其他字体
```
输出示例:
+11 -4
View File
@@ -2,7 +2,7 @@
> **用途**:规范"参考图 → HTML 样机 → LVGL 代码 → 双端验证"的完整管线,规定每一步的**动作、产物、放行条件(gate)**,保证过程可重复、可交接。
> **适用**:所有 UI 新界面 / 界面大改;小改动走 §11 快速通道。
> **配套文档**:`UI_设计架构.md`(系统如何工作)、`菜单页面设计.md`(菜单/导航行为与数据流设计)、`LVGL模拟器部署与使用说明.md`(模拟器)、`UI_中文字体生成与配置.md`(字体生成)。
> **配套文档**:`UI_设计架构.md`(系统如何工作)、`菜单页面设计.md`(菜单/导航行为与数据流设计)、`LVGL模拟器部署与使用说明.md`(模拟器)、`UI_中文字体生成与配置.md`(字体生成)、`UI_自动化测试流程.md`(P3/P4 回归门禁的可执行版本:脚本 + 池水位常量 + 黄金参考更新)。
> **首个实例**:界面 01 主界面(根页),样机 `mockups/01_主界面.html`(本文所有示例数字均取自该试点)。
---
@@ -48,7 +48,7 @@ flowchart TD
|---|---|---|---|
| C1 | 屏幕 | 160×160,1bpp 单色(`LV_COLOR_FORMAT_I1`),无物理屏(远端客户端显示) | `ui_port.h` |
| C2 | 字体 | **唯一**:`ui_font_cn_12`(宋体 12px 1bpp)。汉字 12px/字,ASCII 6px/字,行高 13px。需要其他字号 = 大工程(新字体 + 双端 `lv_conf.h`),默认禁止 | `ui_font_cn_12.c` |
| C3 | 字符集 | 子集字体(当前 78 汉字 + ASCII 0x20-0x7E);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` |
| C3 | 字符集 | 子集字体(当前 97 汉字 + ASCII 0x20-0x7E,共 193 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` |
| C4 | LVGL 内存池 | 40KB(`LV_MEM_SIZE`);任一时刻只放"当前导航路径 + 当前内容页" | `Lib/lv_conf.h` |
| C5 | 文本 | 一律静态字符串(`lv_label_set_text_static`),禁止动态字符串/strdup | 架构文档 |
| C6 | 编码 | `Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h` 为 **UTF-8 带 BOM**;改动只用 Edit 工具,**禁止** PowerShell/cmd 整文件重写(U+FFFD 不可逆) | AGENTS.md 铁律 |
@@ -214,10 +214,10 @@ flowchart TD
```
1. 代码落库(新界面字符串已写入 ui_menu.c 等 UI 源码)
2. python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 → 仓库根 ui_cn_chars.txt(自动含新字;纯标准库,任意 python3)
3. & 'C:\ProgramData\anaconda3\python.exe' tools/font_gen/make_font.py # 宋体 12px 1bpp + ASCII + 字符集 → ui_font_cn_12.c(全量重生成 + 自动补回宋体缺失的 ▶ 箭头,幂等;需 fontTools)
3. & '.venv\Scripts\python.exe' tools/font_gen/make_font.py # 宋体 12px 1bpp + ASCII + 字符集 → ui_font_cn_12.c(全量重生成 + 自动补回宋体缺失的 ▶ 箭头,幂等;需 fontTools)
4. 生成物 diff 检查:新增字在位、旧字不变、Opts 行字符集与预期一致
```
- 一次性环境准备:`npm i -g lv_font_conv`;fonttools 用 conda base 解释器装(本机 `C:\ProgramData\anaconda3\python.exe` 已预装 fontTools,PATH 默认 python 是 msys64 的、无 pip/fonttools,勿用它跑 make_font.py)
- 一次性环境准备:`npm i -g lv_font_conv`;fonttools/pillow 统一装进仓库 `.venv`(`.venv\Scripts\python.exe -m pip install -r requirements.txt`,见 AGENTS.md「Python 环境」)。PATH 默认 python 是 msys64 的、无第三方库,勿用它跑 make_font.py
- `ui_cn_chars.txt`(仓库根,受 git 跟踪)与 `ui_font_cn_12.c` **随代码一起提交**
- 双端 `LV_FONT_DEFAULT` 均指向 `&ui_font_cn_12` → 重生成后两端自动生效
- P1 已知的新增字若含生僻字(宋体可能无此字形)→ 在写码前先手动验证字形,避免"代码写完才发现是空白"
@@ -248,6 +248,9 @@ flowchart TD
(样机渲染图由 P1 的 `tools/ui_mock/render_mockup.py` 产出,即本屏"输入图片")
4. **不合适 → 依 HTML 样机改 LVGL 代码**(HTML 为设计基准)→ 重构建 → 重新出图 → 再比对,循环至一致
5. 交互行为(导航/滚动/选中)在窗口模式 `.\build\Debug\lvgl_sim.exe` 内按键验证(离线出图只覆盖首帧静态画面)
6. **自动化回归**(任何 UI 改动必跑,脚本已入库无需重写):仓库根目录执行
`python simulator/tests/check_menu_rows.py` + `python simulator/tests/check_content_pages.py`,
全部门禁与常量说明见 `UI_自动化测试流程.md`
### 6.2 比对检查单
| 项 | 通过标准 |
@@ -266,6 +269,7 @@ flowchart TD
### 6.4 Gate(进入 P4)
- [ ] 模拟器出图 `shot.png` 与样机渲染图 `render_NN.png` 逐项同构(§6.2,忽略 6.3)
- [ ] 导航行为符合键位语义表(架构 §3.4)
- [ ] 自动化回归 PASS:`check_menu_rows.py` + `check_content_pages.py`(`UI_自动化测试流程.md`)
---
@@ -339,6 +343,8 @@ flowchart TD
| 关闭了 `LV_DRAW_SW_COMPLEX`(省代码) | `lv_line` 斜线不绘制(如主界面顶部"浅 V"两腿消失,只剩水平线) | `LV_DRAW_SW_COMPLEX` 两端显式 =1;`LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4,否则零长数组 C2466)(架构 §关键约束 7) |
| 斜线用细 `line_width`(如 2px) | 1bpp 阈值吃掉斜线边缘,腿偏细、比设计稿细 | 斜线加粗(腿 3px),或按段拆 `lv_line` 分粗细(如"浅 V"两腿 3 / 水平线 2) |
| 样机用了花哨 CSS | P2 翻译不出来 | 1bpp 禁区(C8),P1 阶段就拦下 |
| UI 源文件(UTF-8 BOM)里 `snprintf` 格式串含中文 | 模拟器 cl.exe 报 C4819(代码页 936 误读 UTF-8 字节) | 格式化输出用 `lv_snprintf`(LVGL 自带,双端一致),勿用标准库 `snprintf` |
| 常驻样式用动态 `lv_style_t` + set_prop | LVGL 池永久占用:首次 set_prop 分配 `values_and_props` 且永不释放(x64 实测 88 B/个) | 常驻样式一律常量样式:`LV_STYLE_CONST_INIT(var, props[])` + `lv_style_const_prop_t[]`(属性存 Flash、零池分配,见架构 §5.6) |
---
@@ -433,3 +439,4 @@ P5 [ ] UI_设计架构.md 变更记录更新
| 2026-09-03 | §一 流程总览重画为清晰的纵向管线图:每阶段标注 产物 + Gate,P1/P3 的"比对→修正"迭代环显式标出(⟲);补全原先遗漏的 P1 原图校验环/`NN_说明.md` 产物、P2 双端注册、P3 导航 Gate,与各阶段细则(§3~§8)对齐 |
| 2026-09-04 | §九 新增 9.1“样机投入分流(按屏型)”:明确 HTML 样机=静态外观轴(设计先行、非代码生成器),设计文档 + 模拟器=行为/数据轴;静态内容页走完整样机管线、导航/交互重屏 HTML 只做静态首帧、行为靠 `菜单页面设计.md`;配套文档补列 `菜单页面设计.md` |
| 2026-09-07 | §一 五阶段管线图由 ASCII 转 Mermaid flowchart:P1/P3“比对→修正”迭代环由行内 ⟲ 注记改为显式判定节点 + 回边(未收敛不得前进更直观);各 Gate 条件与产物标注保留、语义不变 |
| 2026-09-09 | 内容页三屏(装置信息/交流量/直流量,`ui_content_page`)落地后回填:C3 字符集 78→97 汉字(193 字形);§十 新增两坑——UI BOM 源文件 `snprintf` 中文格式串触发 C4819(改 `lv_snprintf`)、常驻动态样式池永久占用(改 `LV_STYLE_CONST_INIT` 常量样式,x64 实测省 88 B/个) |
@@ -0,0 +1,78 @@
# UI 自动化测试流程(模拟器回归)
> **用途**:所有涉及 UI 的改动(菜单结构 / 内容页 / 字体 / 样式 / 几何)改完代码后,按本文跑一遍全部门禁再提交。脚本已入库,直接调用即可,无需临时重写。
> **定位**:`UI_界面制作流程.md` P3/P4 门禁的可执行版本;设计期样机比对(P1/P2)仍走该文档与 `tools/ui_mock/`。
---
## 一、前置条件
| 项 | 说明 |
|---|---|
| 模拟器已构建 | 仓库根目录:`cmake --build simulator/build --config Debug`(首次需先 `cd simulator; cmake -B build -G "Visual Studio 16 2019" -A x64`,见 `simulator/README.md` §3.2) |
| Python | 统一入口 `.venv\Scripts\python.exe`(AGENTS.md「Python 环境」);两个回归脚本纯标准库,任意 python3 均可,为保持一致一律用 .venv |
## 二、流程总览
```powershell
# 仓库根目录执行(python 统一用 .venv,见 AGENTS.md「Python 环境」)
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)
```
| 步 | 脚本/命令 | 门禁内容 | 预期输出 |
|---|---|---|---|
| 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(S3==S1、S6==S1、S7==S2)+ S0 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 4784 B)` |
| 2 | `simulator/tests/check_content_pages.py` | 装置信息/交流量/直流量三页截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 3 内容页 + 往返池水位全部通过` |
| 3 | `MDK-ARM\build.bat` | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK |
| 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS |
**退出码约定**:两个脚本均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失、参考缺失)。
失败时脚本打印差异明细(ink runs 对照 / 像素差数),内容页脚本保留现场截图于临时目录并提示路径。
## 三、内存水位门禁常量
| 常量 | 所在脚本 | 当前值 | 含义 |
|---|---|---|---|
| `MENU_POOL_BASELINE` | check_menu_rows.py | **4784 B** | 菜单根框建完的 LVGL 池 used(x64 模拟器) |
| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | **4864 B** | 开页-关页往返后水位 = 基线 + 一次性残留 80 B |
**漂移处理**:菜单/页面结构或样式变更导致门禁失败时,先确认新值合理(残留分解见
`菜单页面设计.md` §9.5),再更新脚本常量并在本文变更记录登记。**禁止为过门禁盲目改常量**——
无法解释的漂移 = 内存回归 bug。
## 四、黄金参考更新流程(有意改视觉时)
1. **先肉眼确认新效果正确**:窗口模式运行 `simulator\build\Debug\lvgl_sim.exe`,或截图转 PNG
(`python simulator/i1_to_png.py <shot.bin> --scale 6`)对照 mockup;
2. 刷新参考:`.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update`(重写 `ref/shot_*.bin`);
3. `git status` / `git diff` 确认 `simulator/tests/ref/` 变化符合预期,随代码一起提交。
**何时不该更新**:未有意改视觉而某页 FAIL → 是 bug(缺字、几何漂移、样式回归),修代码而不是刷参考。
菜单状态无黄金参考文件——其断言是结构性的(白带占比/边框/往返一致),不依赖入库位图。
## 五、误报排查
| 现象 | 常见原因与处置 |
|---|---|
| 多页大面积像素差异 | 字体重生成(`ui_font_cn_12.c` 变化)→ 确认字形正确后 `--update` 刷参考 |
| 单行/单字差异 | 几何或文案变更 → 先判断是否有意修改;无意 = bug |
| 池水位门禁失败但像素一致 | 样式/结构变更(如动态样式转常量、增删框体)→ 确认新值合理后更新常量并登记 |
| `找不到 lvgl_sim.exe` | 模拟器未构建或 build/ 被删 → 先按 §一 重建 |
| 终端输出中文乱码 | PowerShell 代码页渲染问题,不影响判定(看 PASS/FAIL 与退出码) |
## 六、与其他文档的关系
- `UI_界面制作流程.md`:P1 样机生成(`tools/ui_mock/render_mockup.py`)→ P2 设计评审 → **P3 本文回归** → P4 MCU 上机目检
- `菜单页面设计.md` §9:内容页对象树 / 几何表 / 内存预算(本文数字的来源)
- `simulator/README.md` §3.5/§3.6:无头模式(`--keys`/`--screenshot`)与回归脚本说明
## 变更记录
| 日期 | 变更 |
|---|---|
| 2026-09-09 | Python 解释器统一为仓库 `.venv`(新增 `requirements.txt`,`.gitignore` 忽略 `.venv/`);§1/§2/§4 命令同步改用 `.venv\Scripts\python.exe` |
| 2026-09-09 | 初版:菜单 8 状态像素回归 + 内容页黄金参考回归(ref/ 3 图,I1 原始位图入库)+ 池水位门禁(4784/4864 B)。脚本自会话临时目录整理入 `simulator/tests/`(相对路径定位 exe、输出改临时目录),替代每次重写 |
+17 -3
View File
@@ -19,6 +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_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 刷新循环 |
@@ -36,7 +37,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
| LVGL 内存池 | 40 KB(`LV_MEM_SIZE`) | `Lib/lv_conf.h` |
| 刷新周期 | 10 ms(`ui_port_step(10)` + `osDelay(10)`) | `ui_task.c` |
| 推流端口 | 7003(TCP,与 DTU-RemoteLCD 客户端对齐) | `ui_net_sink.h` |
| 默认字体 | `ui_font_cn_12`(12px 1bpp,78 汉字 + ASCII,约 35KB) | `Lib/lv_conf.h` |
| 默认字体 | `ui_font_cn_12`(12px 1bpp,97 汉字 + ASCII 0x20–0x7E + ▶,共 193 字形) | `Lib/lv_conf.h` |
| uiTask 栈 | 16384 words(BelowNormal) | `Src/freertos.c` |
### 1.4 硬约束(改 UI 必须遵守)
@@ -172,7 +173,7 @@ ENTER/→ 进子框:
销毁最右框(框体连自绘回调整体删除,容器保留)→ 焦点恒在持久容器;父层选中/滚动位原样保留
```
- **容器持久、框动态**:行自绘无对象,池内存 = 屏上框体(实测根框 4872 B、最深四级级联 6128 B,远低于 40KB 池)
- **容器持久、框动态**:行自绘无对象,池内存 = 屏上框体(实测根框 4784 B、最深四级级联 6040 B,远低于 40KB 池)
- 同节点再次进入:还原该框上次选中与滚动位(`s_sel/s_scroll`);换节点则清零
- 叶子节点(无子项):ENTER 建空白终点框(w=6、h=2 占位),仅 ESC 可返回
- 深度保护:depth+1 ≥ 4 不再下钻
@@ -183,6 +184,16 @@ ENTER/→ 进子框:
- 每层容器挂 `menu_key_cb`(user_data = depth),统一分发到逻辑层 `menu_key_handle`:↑↓ 移选(首尾环绕)+ 跟随滚动、→/ENTER 进、←/ESC 退
- 选中变化只改选中数据 + 跟随滚动 + 重绘框体,不重建;滚动位回读持久化到 `s_scroll[d]`
### 4.7 内容页(ui_content_page)
> 菜单叶子节点进入后的**独立全屏页面**(装置信息/交流量/直流量),与级联框同屏互斥。设计细节见 `菜单页面设计.md` §九。
- **对象树**:`screen(新) → cont 全屏容器(flat + CLICK_FOCUSABLE,键入口)`;行全部自绘(无子对象),与级联框同一"行=数据"策略
- **生命周期 = 按需建毁**:ENTER 叶子(node 1/3/4)→ `lv_obj_create(NULL)` 新建独立屏 + 容器 → 入默认输入组获焦 → `lv_screen_load` 切屏;ESC → 焦点先回菜单持久容器、切回菜单屏,再删容器+屏。菜单屏对象**保留不销毁**(返回即原样还原)
- **导航状态不压层**:菜单 `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 同量级),常驻样式一律用常量样式
## 五、设计决策与考量(为什么这样做)
### 5.1 弃用 lv_menu 控件
@@ -201,7 +212,7 @@ ENTER/→ 进子框:
### 5.3 框按需建/销
- LVGL 池仅 40KB(`LV_MEM_SIZE`),全量预建 42 个节点的所有页面必然爆内存
- 只建"当前路径"上的框:行自绘无对象,池内存 = 屏上框体(实测根框 used=4872 B、最深四级级联 used=6128 B,远低于 40KB 池)
- 只建"当前路径"上的框:行自绘无对象,池内存 = 屏上框体(实测根框 used=4784 B、最深四级级联 used=6040 B,远低于 40KB 池)
- 返回即 `lv_obj_delete` 释放,内存随时回落到根框水位
### 5.4 持久容器是唯一焦点对象
@@ -223,6 +234,7 @@ ENTER/→ 进子框:
- mono 主题给所有基元对象附加 card 样式(背景/边框/圆角/内边距/文字色)
- 160×160 小屏 1bpp 菜单不需要卡片观感(边框圆角浪费像素)
- flat 样式(透明背景/无边框/无圆角/零内边距)**后加优先**,逐项覆盖 card
- **常驻 flat 样式必须用常量样式**(`LV_STYLE_CONST_INIT` + `lv_style_const_prop_t[]`):动态 `lv_style_t` 的 `values_and_props` 是池分配且永不释放(x64 实测 88 B/个),RAM 紧张下每个常驻动态样式都是永久泄漏;常量样式属性存 Flash、零池占用
### 5.7 SDL 事件只由 LVGL 驱动轮询
@@ -241,3 +253,5 @@ ENTER/→ 进子框:
| 2026-09-03 | 关键约束 7:`LV_DRAW_SW_COMPLEX` 须=1(顶部"浅 V"斜线依赖);两端 lv_conf 显式固化,`LV_DRAW_SW_CIRCLE_CACHE_SIZE`=4 防零长数组 |
| 2026-09-04 | 菜单重构为"横向级联框"模型(见 `菜单页面设计.md` §四⑧):数据/显示/逻辑三层拆分(新增 `ui_menu_view` 显示层、`ui_menu_tab` 生成物,`ui_menu` 瘦身为纯状态机);字体重生成补缺字形(78 字,含菜单树新增的整/控/软/板等);模拟器加 `--keys <seq>` 无头注键截图验证 |
| 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` 避免两文件冲突 |
@@ -40,7 +40,7 @@
→ **确定新增 8 个字形**:三 当 前 模 式 无 : ▶。
写 LVGL 代码前必须重新生成字体,否则新增字在真机显示为空白。流程:代码落库 → `python tools/font_gen/extract_cn.py`(自动从 UI 源码字符串提取)→ `& 'C:\ProgramData\anaconda3\python.exe' tools/font_gen/make_font.py`(需 fontTools,见 `UI_中文字体生成与配置.md` §2.1)→ 两端(MCU/模拟器)同时生效。
写 LVGL 代码前必须重新生成字体,否则新增字在真机显示为空白。流程:代码落库 → `python tools/font_gen/extract_cn.py`(自动从 UI 源码字符串提取)→ `& '.venv\Scripts\python.exe' tools/font_gen/make_font.py`(需 fontTools,见 `UI_中文字体生成与配置.md` §2.1)→ 两端(MCU/模拟器)同时生效。
## 四、待确认问题
+82 -8
View File
@@ -1,7 +1,7 @@
# 菜单页面设计
> **用途**:定义 DTU 主菜单的**内容(42 节点层级树)、视觉规范、三层代码架构**(数据层 / 显示层 / 逻辑层)。
> **范围**:本阶段**只做菜单层级**(节点、父子、层级、进出导航);各叶子进入后的**内容页**(字段/交互/数据)另行设计,本文不含。
> **用途**:定义 DTU 主菜单的**内容(42 节点层级树)、视觉规范、三层代码架构**(数据层 / 显示层 / 逻辑层),以及叶子节点进入后的**内容页模块**(§九)。
> **范围**:菜单层级(节点/父子/层级/进出导航)+ 三个已实现的内容页(装置信息/交流量/直流量,§九);其余叶子的内容页后续按同模式扩展。
> **来源**:用户提供的主菜单截图 7 屏(根页 + 实时数据/参数定值/三遥设置/装置维护/通讯参数/记录查询各一级子页 + 参数定值·整定二级)。
> **导航模型**:树形**逐级进入**——`ENTER`/`→` 进入子页,`ESC`/`←` 返回上一级;最多 **4 级(L0~L3)**。
@@ -13,6 +13,7 @@
> | 深入某一层 | §三 数据层 / §五 显示层 / §六 逻辑层(各章自包含:职责边界 + 字段 + API) |
> | **改菜单**(增删/改名/换序条目) | §二(内容)+ §3.7(只改文本源、重跑生成器,代码零改动) |
> | **改视觉**(框/行/几何) | §四(显示规范)+ §五(显示层实现) |
> | **内容页**(装置信息/交流量/直流量) | §九(对象树/生命周期/页面定义表/API/内存预算) |
> | 搞懂**内存**(什么存、什么现算) | §7.3 + §7.5 |
> | **验证 / 调试** | §八 |
@@ -114,7 +115,7 @@ sequenceDiagram
```
- 日志标记:`UI menu init: OK`(uiTask 打印)——`@test` 回归可作 Required 标记。
- LVGL 池实测(模拟器 40 KB 池):根框建完 `used=4872 B`(旧行对象实现为 13312 B,自绘 + 容器收敛省 8.2 KB);最深四级级联 `used=6128 B`(增量 1256 B = 3 个子框体),远小于池余量。
- LVGL 池实测(模拟器 40 KB 池):根框建完 `used=4784 B`(旧行对象实现为 13312 B,自绘 + 容器收敛省 8.3 KB);最深四级级联 `used=6040 B`(增量 1256 B = 3 个子框体),远小于池余量。
### 1.6 层间不变量(每层"绝不做")
@@ -144,7 +145,7 @@ sequenceDiagram
| 类型 | 标识 | 行为 |
|---|---|---|
| **菜单节点** | 行尾 **▶** 箭头 | `ENTER` 进入 → 展开下一级子页 |
| **叶子节点** | 无箭头 | `ENTER` 进入 → **终点**(本阶段仅占位,内容页后续设计) |
| **叶子节点** | 无箭头 | `ENTER` 进入 → 有内容页(node 1/3/4,§九)则切内容屏;否则进空白终点框 |
> 根页默认选中**第 1 项"装置信息"**(整行反色:白带、黑字)。
@@ -574,7 +575,7 @@ y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框
|---|---|---|---|
| `s_cont` | `lv_obj_t*` | 4 B | 单一全屏持久容器(焦点/键唯一入口;各层框建其内);透明常驻、**永不删**(防焦点对象悬空) |
| `s_row_node[4][16]` | `uint8_t` | 64 B | 行数据:节点下标(名称/箭头绘制时现查,无对象);**仅屏上框注册** |
| `s_style_flat/frame/scrollbar` | `lv_style_t`×3 | ~300 B | 样式(一次性初始化,常驻) |
| `s_style_frame/scrollbar` | `lv_style_t`×2(动态)+ flat 常量样式(Flash) | ~200 B | 样式(一次性初始化,常驻;flat 为常量样式零池占用,见 §9.5) |
| `s_frame_box[4]` | `lv_obj_t*`×4 | 16 B | 框体(d=0 持久;d>0 进入时建、返回时毁) |
| `s_frame[4]` | `menu_frame_t`(built/sel/item_cnt/x/y/w/h) | 48 B | 每框几何缓存:子框对齐要用**父框刚算出**的 x/w/选中行,故按层留一份;随时可由 `s_path`/`s_sel` 重算,**不属于导航状态** |
@@ -765,8 +766,8 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响
| 行数据 `s_row_node[d][i]` `[4][16]` | 显示层 | 64 B |
| 框体句柄 `s_frame_box[4]` | 显示层 | 16 B |
| 框几何缓存 `s_frame[4]`(`menu_frame_t`) | 显示层 | 48 B |
| 样式 `s_style_*`(3 个,一次性) | 显示层 | ~300 B |
| **小计** | | **~0.45 KB**(逻辑层 17 B + 显示层 ~0.4 KB) |
| 样式 `s_style_frame/scrollbar`(动态)+ flat 常量样式(Flash,零池占用 §9.5) | 显示层 | ~200 B |
| **小计** | | **~0.35 KB**(逻辑层 17 B + 显示层 ~0.33 KB) |
**LVGL 池(动态,`LV_MEM` 40 KB 预算内)**:行自绘无对象,池只占**屏上框体**(2~3 个 `lv_obj`)+ 根框装饰/状态行,
典型 ≈ **1~2 KB**,仍远小于 40 KB(池可再缩减,见 `docs/待处理内容/RAM使用分析与优化方案.md`)。
@@ -795,7 +796,78 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响
---
## 九、变更记录
## 九、内容页(ui_content_page)
> **一句话职责**:菜单叶子节点进入后的**独立全屏页面**——装置信息(node 1)、交流量(node 3)、直流量(node 4)。与级联框同屏互斥,按需建毁。
> **文件**:`Drivers/BSP/UI/ui_content_page.c/h`(MCU / PC 模拟器双端共用,注册见 `UI_设计架构.md` §1.2/§1.4 约束 5)。
### 9.1 对象树与生命周期
```text
菜单屏(持久) 内容页屏幕(按需建毁)
screen_menu screen_page = lv_obj_create(NULL)
└─ cont 持久容器(焦点/键入口) └─ s_cont 全屏容器(flat + CLICK_FOCUSABLE,唯一键入口)
├─ 根框体 / 子框体… (行全部自绘,无子对象——与级联框同一"行=数据"策略)
```
| 动作 | 序列 |
|---|---|
| **开页**(菜单 ENTER 叶子 node 1/3/4) | `lv_obj_create(NULL)` 新建屏 → 建全屏容器(flat 常量样式 + 键回调 + 自绘回调)→ 入默认输入组并获焦(先于切屏,按键立即由本模块接管)→ `lv_screen_load` 切屏。菜单屏对象**保留不销毁** |
| **关页**(ESC/←) | 焦点先回菜单持久容器 → 切回菜单屏(原样还原)→ 删容器、再删屏。LVGL 池内存随即回落 |
- **导航状态不压层**:菜单 `s_depth/s_sel/s_scroll` 原样保留,返回即还原;内容页只记自身选中行 `s_sel`(每次进入从首行开始,不跨次保留)
- **单页互斥**:同一时刻至多一个内容页打开(`s_open` 哨兵防御)
### 9.2 页面定义表(Flash 常量,几何实测自参考图 10/20/21)
| 参数 | 装置信息 | 交流量 | 直流量 |
|---|---|---|---|
| `selectable` | 0(静态键值行) | 1(数据列表) | 1(数据列表) |
| 行数 / 每页 | 6 / 不分页 | 24 / 8(3 页) | 3 / 单页 |
| 首行槽顶 y / 行距 | 40 / 15 | 24 / 14 | 24 / 14 |
| 选中白带高 | 无 | 13(x=2..156) | 13(x=2..156) |
| 左列 x / 值列 x | 标签 6 / 86 | 名 7 / 101 | 名 7 / 101 |
| 副标题 | "馈线自动化终端" y=25 | — | — |
| 页脚(y=143,x=6) | 固定"第 10 页,共 1 页"(按参考图原样) | 动态"第 X 页,共 3 页" | 动态"第 1 页,共 1 页" |
- 标题 y=2(居中,浅 V 装饰区内;装饰几何与菜单根框一致:两腿 3px / 水平线 2px)
- 列表页窗口 = 选中行所在页(首可见行 = `(sel/每页数)×每页数`),行槽位置固定、数据随页切换
- 数据表为 Flash `const cp_row_t[]`(名+值字面量,不 strdup);交流量第 2/3 页当前为占位数据
### 9.3 键语义
| 键 | 行为 |
|---|---|
| ↑ / ↓ | 列表页移选(**首尾环绕、跨页连续**),窗口随页切换后重绘;信息页不响应 |
| ESC / ← | 关页返回菜单(导航状态原样还原) |
### 9.4 公开 API
```c
bool ui_content_page_open(content_page_id_t id); /* 开页:建屏+切屏+焦点置页面容器;ID 无效/已有页打开 → false */
void ui_content_page_close(void); /* 关页:焦点回菜单容器 + 切回菜单屏 + 销毁页面 */
bool ui_content_page_is_open(void); /* 查询是否有内容页打开 */
```
- 节点→页映射在逻辑层 `ui_menu.c`(`menu_content_page_for(node)`):node 1 → `CONTENT_PAGE_DEVICE_INFO`、3 → `AC`、4 → `DC`;其余叶子无内容页(进空白终点框)
### 9.5 内存预算(模拟器 x64 实测,40 KB 池)
| 时刻 | LVGL 池 used | 说明 |
|---|---|---|
| 菜单基线(根框建完) | 4784 B | 同 §1.5 |
| 内容页打开峰值 | 5560~5976 B | +776~1192 B:屏对象+容器+事件列表+局部样式+screens 数组扩容(x64 指针 8B,MCU 32 位下更小) |
| 关页后回落 | **4864 B** | 一次性残留 **+80 B**(见下),非泄漏 |
**+80 B 一次性残留分解**(均为 LVGL 核心行为,与内容页模块无关;MCU 32 位下更小):
- +8 B:`disp->screens` 数组高水位——开页时第 5 个屏使 TLSF 块 32→40 B,关页缩回后块不归还(分配器行为)
- +72 B:`lv_screen_load()` 内部对菜单屏执行 `lv_obj_set_pos(act_scr,0,0)` → 局部 X/Y 样式各分配一个零初始化 `lv_style_t`+属性数组;每屏一次性,随菜单屏销毁才释放
**常驻样式零池占用**:flat 样式用常量样式(`LV_STYLE_CONST_INIT` + `lv_style_const_prop_t[]`,属性存 Flash)。动态 `lv_style_t` 首次 set_prop 会从池分配 `values_and_props` 且永不释放(x64 实测 88 B/个)——常驻样式禁用动态初始化(见 `UI_设计架构.md` §5.6)。
---
## 十、变更记录
| 日期 | 变更 |
|---|---|
@@ -826,3 +898,5 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响
| 2026-09-08 | 行 label 指针合并:删 `s_row_text` / `s_row_arrow` 两数组(省 512 B);文本/箭头颜色改在行容器 `s_row` 上设置、子 label 继承(LVGL `text_color` 为可继承属性,`lv_style.c` 属性标志表证实);§5.4 对象模型表 / §7.5 内存预算表同步(768 B→256 B) |
| 2026-09-08 | **行对象 → 自绘重写(方案1,省 LVGL 池"每行对象机制税")**:行不再是 LVGL 对象——行数据改 `s_row_node[4][16]`(`uint8_t` 节点下标,256 B→64 B),框体 `LV_EVENT_DRAW_MAIN` 自绘(选中行白带 `lv_draw_rect` + 每行 `lv_draw_label` 文本/箭头,按滚动偏移枚举可见行、重绘区外行 culling);框内滚动改 `LV_EVENT_GET_SELF_SIZE` 虚报内容高 + 手动跟随滚动(`view_scroll_to_sel`,替代 `lv_obj_scroll_to_view`);删 `s_style_selected`、`view_add_row` 与 flex 堆行;`frame_build/destroy/refresh` 改行数据注册/清空/重绘。§5.4/§5.6/§6.2/§7.1~7.5 同步。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归(白带/边框/根框不变性 + 3 组往返态逐像素一致)全 PASS |
| 2026-09-08 | **4 层容器→单一持久容器 + 根框装饰/状态行自绘(RAM 收敛)**:焦点/键唯一入口 = 持久容器(框深度由逻辑层 `s_depth` 决定,键回调去掉 `user_data=depth`,`container`/`focus` 接口去参);根框装饰(浅 V)与状态行改容器 `DRAW_MAIN` 自绘(3 段 `lv_draw_line` + 1 行 `lv_draw_label`,删 3 `lv_line` + 1 label + 3 容器对象);静态显示层 ~0.5→~0.4 KB。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归全 PASS(装饰/状态行像素级一致);LVGL 池实测 根框 7168→4872 B、最深四级级联 8488→6128 B(省 ~2.3 KB) |
| 2026-09-09 | **新增 §九"内容页(ui_content_page)"**:装置信息/交流量/直流量三叶子页落地(独立屏按需建毁 + 行自绘,几何实测自参考图 10/20/21);节点→页映射入逻辑层 `menu_content_page_for`(node 1/3/4),§2.1 叶子行为同步;flat 样式改常量样式(`LV_STYLE_CONST_INIT` 零池分配,动态常驻样式 x64 实测 88 B/个永久占用);关页后一次性残留 +80 B 分解入 §9.5(screens 数组高水位 +8、`lv_screen_load` 菜单屏局部 X/Y 样式 +72,均 LVGL 核心行为非泄漏)。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器三页对照参考图几何一致 + 菜单 8 状态像素回归全 PASS |
| 2026-09-09 | 菜单显示层 flat 样式同法转常量样式(§5.6/§7.5/§9.5):LVGL 池基线 4872→4784 B、最深四级级联 6128→6040 B、内容页峰值 5648~6064→5560~5976 B(x64 省 88 B);`LV_STYLE_CONST_INIT` 生成外部链接符号,内容页同名者改名 `cp_style_flat`。验证:`@build` 0 错 0 警 + 代码规范 PASS + 菜单 8 状态像素回归全 PASS + 三页几何不变 |