完成一个主界面开发流程
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
STM32F4-Base 的 UI 是 **160×160 1bpp 单色**菜单(`lv_menu` 树形菜单),使用自定义中文字体 `ui_font_cn_12`。在 MCU 上开发 UI 需要反复烧录调试,效率低。本模拟器在 PC 上直接编译运行相同的 UI 代码(`ui_menu.c`),实现所见即所得。
|
||||
STM32F4-Base 的 UI 是 **160×160 1bpp 单色**菜单(基元控件页面导航:`lv_obj` 页面 + 返回按钮切换),使用自定义中文字体 `ui_font_cn_12`。在 MCU 上开发 UI 需要反复烧录调试,效率低。本模拟器在 PC 上直接编译运行相同的 UI 代码(`ui_menu.c`),实现所见即所得。
|
||||
|
||||
### 1.2 架构
|
||||
|
||||
|
||||
@@ -76,9 +76,12 @@ python tools/font_gen/make_font.py --font C:/path/to/font.ttf # 换其他字
|
||||
```
|
||||
extracted SimSun regular (face0) -> C:\Users\...\Temp\simsun_regular.ttf
|
||||
RUN: lv_font_conv.cmd --font ... --size 12 --bpp 1 -r 0x20-0x7E --symbols ... --no-compress --format lvgl --lv-font-name ui_font_cn_12 -o ...\ui_font_cn_12.c
|
||||
已合并箭头 U+25B6:位图+10 字节,glyph_id=181
|
||||
OK 36393 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c
|
||||
```
|
||||
|
||||
> **▶ 箭头自动合并**:宋体(`simsun.ttc`)没有 U+25B6(▶)字形,`lv_font_conv` 会静默跳过 → 真机显示成方框。`make_font.py` 在生成后自动调用 `merge_arrow_glyph()`,把从 **Segoe UI Symbol**(`seguisym.ttf`,Windows 自带)预提取的箭头字形(位图 10 字节 + `glyph_dsc` + `cmap`)补回 `ui_font_cn_12.c`。该合并**幂等**(已存在则跳过),所以重生成字体后箭头不会丢。换用自带 ▶ 的字体(如黑体/Segoe)时可忽略此步。
|
||||
|
||||
参数说明:
|
||||
- `--bpp 1`:单色无抗锯齿;体积最小。**注意** lv_font_conv 在 1bpp 下斜线/细笔画偶发"丢像素",宋体尤甚。
|
||||
- `--bpp 2/3`:字形带灰度,渲染到 1bpp 屏时按 `LV_DRAW_SW_I1_LUM_THRESHOLD` 阈值化,笔画更连贯;子集仅几十~上百字,体积仍很小。
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
# UI 界面制作流程(图片 → HTML → LVGL)
|
||||
|
||||
> **用途**:规范"参考图 → HTML 样机 → LVGL 代码 → 双端验证"的完整管线,规定每一步的**动作、产物、放行条件(gate)**,保证过程可重复、可交接。
|
||||
> **适用**:所有 UI 新界面 / 界面大改;小改动走 §11 快速通道。
|
||||
> **配套文档**:`UI_设计架构.md`(系统如何工作)、`LVGL模拟器部署与使用说明.md`(模拟器)、`UI_中文字体生成与配置.md`(字体生成)。
|
||||
> **首个实例**:界面 01 主界面(根页),样机 `mockups/01_主界面.html`(本文所有示例数字均取自该试点)。
|
||||
|
||||
---
|
||||
|
||||
## 一、流程总览
|
||||
|
||||
```
|
||||
参考图 ──▶ [P0 需求录入] ──▶ 界面台账 + 问题清单
|
||||
│ gate: 界面定位/极性/阻塞问题已确认
|
||||
▼
|
||||
[P1 图片→HTML样机] ──▶ mockups/NN_界面名.html
|
||||
│ ▲ │
|
||||
│ └── 用户浏览器确认(循环至定稿)
|
||||
│ gate: 样机定稿 + 字符集核对完成
|
||||
▼
|
||||
[P2 HTML→LVGL代码] ──▶ Drivers/BSP/UI/ 代码 + 字体重生成
|
||||
│ gate: 代码与样机参数逐项一致 + 字体已重生成 + 内存预算通过
|
||||
▼
|
||||
[P3 模拟器验证] ──▶ 出图 → 与样机渲染图比对 → 不合依 HTML 改码
|
||||
│ gate: 模拟器出图与样机渲染图同构(忽略抗锯齿/字形微差)
|
||||
▼
|
||||
[P4 MCU 验证] ──▶ @build + @test + 真机目检
|
||||
│ gate: 0错0警 + test PASS + 目检通过
|
||||
▼
|
||||
[P5 归档] ──▶ 变更记录/台账更新
|
||||
```
|
||||
|
||||
### 1.1 各阶段职责分工
|
||||
|
||||
| 阶段 | 用户做 | AI 做 | 产物 |
|
||||
|---|---|---|---|
|
||||
| P0 需求录入 | 提供图片(每批 1~3 屏);说明各屏作用与菜单位置;回答问题 | 图片分析、界面登记、问题清单 | 台账 + Q 清单 |
|
||||
| P1 图片→HTML | 浏览器打开确认;指出问题**或直接改 HTML** | 度量核对、字符集核对、布局计算、样机制作与修改 | 定稿样机 |
|
||||
| P2 HTML→代码 | (可选)抽查代码 | 逐参数翻译、字体重生成、内存预算 | 代码 + 字体 |
|
||||
| P3 模拟器 | (可旁观) | 构建、离线出图、与样机渲染图比对、依 HTML 改码 | 比对结论 |
|
||||
| P4 MCU | **真机目检**(DTU-RemoteLCD) | @build / @test 执行、日志解析 | 日志 + 结论 |
|
||||
| P5 归档 | — | 更新变更记录与台账 | 文档 |
|
||||
|
||||
---
|
||||
|
||||
## 二、设计约束(硬规则)
|
||||
|
||||
管线中一切设计必须满足以下约束;违反即需重做,不存在"真机上再凑合"的选项。
|
||||
|
||||
| # | 约束 | 值 | 来源 |
|
||||
|---|---|---|---|
|
||||
| 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/` |
|
||||
| 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 铁律 |
|
||||
| C7 | 代码共用 | `ui_menu.c` / 字体为 MCU 与模拟器共用源,**禁止**在 `simulator/` 复制副本 | 架构文档 |
|
||||
| C8 | 视觉禁区(1bpp) | 圆角 / 阴影 / 透明度 / 渐变 / 多色 / 抗锯齿 全部禁用;样机与代码都不得引入 | 物理限制 |
|
||||
| C9 | 双端注册 | 新增 `Drivers/BSP/UI/*.c` 必须**两处**手动注册:Keil `.uvprojx` + `simulator/CMakeLists.txt` 的 `UI_SOURCES` 列表(均为显式列表,无通配扫描)。只注册一端 = 另一端静默缺文件 | 实测(2026-09-03) |
|
||||
|
||||
### 2.1 字体度量速查(2026-09-03 实测,字体变更时重测)
|
||||
|
||||
| 项 | 原始值 | = 设备像素 | 说明 |
|
||||
|---|---|---|---|
|
||||
| 汉字步进宽 | `adv_w = 192`(1/16 px) | **12 px/字** | 4 字标签 = 48px |
|
||||
| ASCII 步进宽 | `adv_w = 96` | **6 px/字** | "OK" = 12px,"V1.0" = 24px |
|
||||
| 汉字墨迹盒 | `box_w` 9~11,`box_h` 11~12 | 墨迹在盒内再内缩 1~2px | **字体自带侧边距**,贴边文字仍有视觉留白 |
|
||||
| 行高 | `line_height = 13` | 13px | 布局按 13 算,不是 12 |
|
||||
| 基线 | `base_line = 2`(自底部) | — | 垂直定位参考 |
|
||||
|
||||
> 样机字体必须用 **SimSun**(设备字体的源字体),浏览器渲染宽度与设备度量一致,预览才"诚实"。
|
||||
|
||||
---
|
||||
|
||||
## 三、P0:需求录入与界面台账
|
||||
|
||||
### 3.1 用户输入
|
||||
1. 参考图(建议每批 1~3 屏,不必一次给全)
|
||||
2. 每图一句话:是什么界面、干什么用、挂在菜单哪个位置
|
||||
3. 对 AI 提出的问题的回答(能答则答,答不了的标"待定")
|
||||
|
||||
### 3.2 AI 动作:图片分析(检查单)
|
||||
| 分析项 | 内容 |
|
||||
|---|---|
|
||||
| 总体结构 | 页头 / 主体(网格/列表/数值/曲线/表单)/ 页脚 / 状态栏 |
|
||||
| 元素类型 | 网格、列表、数值、曲线、开关、进度条、图标(1bpp 小屏图标基本不可行 → 文本替代) |
|
||||
| 文本内容 | **逐字抄录**(字符集核对的输入;漏一个字 = 真机一处空白) |
|
||||
| 比例换算 | 参考图比例 → 160×160 设备像素(参考屏分辨率通常大于 160,做**比例适配**而非像素照搬) |
|
||||
| 风格 | 极性(白底黑字/黑底白字)、边框样式、选中样式 |
|
||||
| 信息缺失 | 截图裁切、遮挡、模糊处 → 转成待确认问题 |
|
||||
|
||||
### 3.3 界面台账
|
||||
- 编号 `NN`(01 = 试点界面),名称、作用、菜单位置、状态(待做/进行中/已完成)
|
||||
- 台账随 P5 归档更新
|
||||
|
||||
### 3.4 Gate(进入 P1)
|
||||
- [ ] 界面作用与菜单位置已确认
|
||||
- [ ] 阻塞性问题已回答;非阻塞项已标注默认值(如 "TBD")
|
||||
|
||||
> **试点实例**:01 主界面。用户给一张根页截图:顶部梯形装饰 + 状态行"当前模式:无模式" + 7 行列表(装置信息/实时数据/参数定值/三遥设置/装置维护/通讯参数/记录查询),黑底白字(与现系统同极性)。AI 分析出:7 项与现有 `g_menu_tab` 的映射,其中 3 项疑似重命名(参数定值/三遥设置/通讯参数)→ Q1;"三遥设置"对应现有"遥测量"还是新增"三遥"节点 → Q2;旧菜单"厂家设置/版本信息"未列入 7 项,入口在哪 → Q3;"当前模式"指什么、数据从哪来 → Q4。
|
||||
|
||||
---
|
||||
|
||||
## 四、P1:图片 → HTML 样机
|
||||
|
||||
### 4.1 字体度量核对(字体每次变更时重做)
|
||||
1. 读 `Drivers/BSP/UI/ui_font_cn_12.c` 头部 Opts 注释 → 字体源(宋体)、尺寸(12)、bpp(1)、**当前字符集**
|
||||
2. 读 `glyph_dsc[]` 表 → `adv_w ÷ 16` = px(核对 CJK=12 / ASCII=6)
|
||||
3. 读 `font_dsc` → `line_height=13`、`base_line=2`
|
||||
4. 与 §2.1 速查不一致 → **全部样机参数重算**
|
||||
|
||||
### 4.2 字符集核对
|
||||
1. 收集本屏全部文本中的**去重汉字**
|
||||
2. 对照现有字符集(字体 `.c` 头部 `--symbols` 行为准;`ui_cn_chars.txt` 为生成物,重生成后一致)
|
||||
3. 输出:命中列表(现有)/ 新增列表(标红)→ 记入配套 `_说明.md`
|
||||
4. 新增列表决定 P2 字体重生成的范围
|
||||
|
||||
> **试点实例**:01 主界面 7 个菜单项共 22 个唯一汉字 → 命中 21(装置信息实时数据参数定值遥设维护通讯记录查询),**新增 1**(三);状态行"当前模式:无模式"新增 5(当前模式无);箭头 **▶**(U+25B6)为新增符号。合计**确定新增 7 个字形**(若状态行用全角冒号":"则再多 1,共 8)。
|
||||
|
||||
### 4.3 布局计算
|
||||
- 所有值为**设备像素整数**
|
||||
- 列宽:`cell_w = floor((W − 2×边距 − 间隙×(n−1)) / n)`,**1px 余量给最后一列**
|
||||
- 行高:`cell_h = floor((H − 2×边距 − 间隙×(m−1)) / m)`
|
||||
- 必须给出**验证等式**(每项相加 = 屏幕尺寸)
|
||||
- 文字宽度检查:`字数 × 12px`(步进宽)vs 格内宽(格宽 − 2px 边框)。步进宽超出但**墨迹宽**(约 −1px/端)放得下即可;放不下 → 减字数 / 减列数 / 回 P0 问用户
|
||||
|
||||
> **试点实例(7 行列表)**:
|
||||
> `160 = 顶边距2 + 顶装饰5 + 隙4 + 状态行13 + 隙5 + 列表框128 + 底边距3`
|
||||
> 列表框 `144×128`(1px 边框,x=8):内容高 `7×18 + 2(上下边框) = 128`,7 行 × 18px
|
||||
> 行文字 4字 = 步进 48px,行内宽 144(左留白 4px)→ 富余,**左对齐**成立
|
||||
>
|
||||
> 推论:行通栏 144px,文字**左对齐**(左留白 4px)+ 字体自带侧边距 = 视觉留白;行尾 ▶ 右对齐(右留白 4px)。选中行整行反色(白底黑字)。
|
||||
|
||||
### 4.4 样机制作
|
||||
**文件**:`docs/UI 设计/mockups/NN_界面名.html`(**仅页面**)+ 配套 `NN_界面名_说明.md`(规格)
|
||||
|
||||
**HTML 样机(只渲染页面本身,无其他可见内容)**:
|
||||
|
||||
| 要求 | 规定 |
|
||||
|---|---|
|
||||
| 自包含 | 内联 CSS,无外部依赖,**无 JS**,双击即开(Chrome/Edge 最佳,需 SimSun) |
|
||||
| **仅页面** | HTML 只渲染 160×160 页面;**不出现**标题/说明区/图例等任何非页面可见内容。规格、核对、问题等一律写入配套 `_说明.md` |
|
||||
| 视图 | ① 1:1 实际大小(160×160,所见即真机)② 4x 放大(细节检查);选中/告警等状态若属设计一部分则直接画在页面内,不单列 |
|
||||
| 4x 实现 | `--u` CSS 变量(1:1 取 `1px`,4x 取 `4px`),全部尺寸 `calc(N*var(--u))`,与 1:1 **同源**标记 |
|
||||
| 字体 | `font-family: SimSun, NSimSun, serif`(与设备字体同源同宽) |
|
||||
| 颜色 | 仅 `#000` / `#fff` |
|
||||
| 定位 | 绝对定位 + 精确 px(对 LVGL `lv_obj_set_pos` 翻译最忠实);确属流式布局才用 flex |
|
||||
| 文字对齐 | 默认左对齐(见 §4.3 推论);垂直居中 |
|
||||
| 1bpp 禁区 CSS | `border-radius` / `box-shadow` / `opacity` / 渐变 / `transform` / z-index 层叠 —— 出现即违规 |
|
||||
|
||||
**配套 `NN_界面名_说明.md`(规格,顺序固定)**:
|
||||
1. **布局参数表** —— 每个 px 值都列出(P2 翻译的依据,必须与样机 CSS 完全一致)+ 验证等式
|
||||
2. **行/元素内容与状态**
|
||||
3. **字符集核对结果** —— 命中 / 新增
|
||||
4. **待确认问题** —— 沿用 P0 的 Q 编号
|
||||
5. **映射建议**(菜单类界面:条目 → 子树)
|
||||
6. **LVGL 翻译要点** —— 本屏具体的翻译提示(控件选择、状态、内存注意点)
|
||||
|
||||
**文件头注释**(HTML 顶部,简短):样机规则 + `状态:制作中 / 已定稿 (YYYY-MM-DD)` + 指向配套 `_说明.md`
|
||||
|
||||
### 4.5 原图校验循环(渲染 → 目视比对 → 修正)
|
||||
> 目的:消除"AI 读图不准"的风险。样机完成后渲染成图,与原图并排目视比对,有差异就改,循环到基本一致。**不做像素级对齐**(字体字形本就不同,像素 diff 无意义)。
|
||||
|
||||
1. **渲染样机成图**:`python tools/ui_mock/render_mockup.py <mockup.html> <out.png>`(无头 Chrome/Edge 截 1:1 视图,得 160×160 PNG)
|
||||
2. **AI 读两图比对**:直接读 `原图.png` 与渲染出的 PNG,目视逐项核对——装饰形状/位置、状态行位置与文案、列表框位置/大小、行高、选中行、箭头位置
|
||||
3. **有差异 → 改 HTML → 重新渲染 → 再比对**,循环
|
||||
4. **基本一致**:结构与原图无明显出入(允许字体字形微差)→ 交用户浏览器最终确认
|
||||
5. 用户指出问题,或**直接修改 HTML** 后告知(HTML 即设计文件,用户改 = 设计变更)
|
||||
6. **定稿**:几何/文案/状态/参数与用户意图一致 → 文件头注释标 `已定稿 (日期)`,此后改动须用户明确提出
|
||||
|
||||
### 4.6 Gate(进入 P2)
|
||||
- [ ] 样机已定稿
|
||||
- [ ] **原图校验通过**:渲染图与原图目视比对基本一致(§4.5 循环收敛)
|
||||
- [ ] 待确认问题全部有答案或显式默认值
|
||||
- [ ] 字符集核对完成(新增字清单确定)
|
||||
|
||||
---
|
||||
|
||||
## 五、P2:HTML → LVGL 代码
|
||||
|
||||
### 5.1 翻译对照表(CSS → LVGL v9)
|
||||
|
||||
| 样机(HTML/CSS) | LVGL(设备端) | 备注 |
|
||||
|---|---|---|
|
||||
| 160×160 白底画布 | display(I1)+ 屏幕背景色 | 极性随全局决策(Q2 类问题) |
|
||||
| `left/top` 绝对定位 | `lv_obj_set_pos(o, x, y)` | 整数 px |
|
||||
| `width/height` | `lv_obj_set_size(o, w, h)` | |
|
||||
| flex row / row-wrap | `lv_obj_set_flex_flow(o, LV_FLEX_FLOW_ROW[_WRAP])` | |
|
||||
| `gap` | `lv_obj_set_style_pad_gap(o, g, 0)` | |
|
||||
| 1px 边框 | `lv_style_set_border_width(st,1)` + `border_color` | |
|
||||
| 仅底边框 | `lv_style_set_border_side(st, LV_BORDER_SIDE_BOTTOM, 1)` | v9 分边设置 |
|
||||
| 背景色 | `lv_style_set_bg_color` + `lv_style_set_bg_opa` | 1bpp 仅纯黑/纯白 |
|
||||
| 文本 | `lv_label_create` + `lv_label_set_text_static` | 静态字符串(C5) |
|
||||
| 文字对齐 | 标签定位 / `lv_label_set_long_mode` | 默认左对齐 |
|
||||
| 选中反色 | `LV_STATE_CHECKED` 样式(bg/文字色互换) | 条目对象**自身**带 CHECKED(防白底白字,架构 §5.5) |
|
||||
| `display:none` | `LV_OBJ_FLAG_HIDDEN` | 页面/状态切换 |
|
||||
| 圆角/阴影/透明/渐变 | **禁止翻译**(C8) | 样机里就不该有 |
|
||||
|
||||
### 5.2 文件策略与双端注册
|
||||
- **首选原地扩展** `ui_menu.c`(共用源,改一处两端生效)
|
||||
- 确需新文件(模块独立,如曲线页渲染器)时,**两处注册**(C9):
|
||||
1. Keil:`MDK-ARM/*.uvprojx` 手动添加
|
||||
2. 模拟器:`simulator/CMakeLists.txt` 的 `UI_SOURCES` 列表加一行
|
||||
- 新**页面类型**(内容页/编辑页)扩展现有"容器持久、条目动态"架构,**禁止**另起第二套架构
|
||||
- 字体文件 `ui_font_cn_12.c` 两端均已注册 → 重生成是纯内容替换,无需注册
|
||||
|
||||
### 5.3 翻译纪律
|
||||
1. **逐参数翻译**:代码中每个 px 值必须能在样机"布局参数表"中找到出处;禁止"看着办"
|
||||
2. 样式沿用现有 `s_style_flat` / `s_style_selected` 模式(静态样式、一次性初始化、后加优先覆盖主题)
|
||||
3. 焦点对象恒为**页面容器**;条目不入输入组(架构 §5.4)
|
||||
4. 容器**永不删除**(indev 释放事件 use-after-free,架构 §5.2);只建/毁条目
|
||||
5. 编码纪律(C6):Edit 工具精确替换;文件保持 UTF-8 BOM 不变
|
||||
6. 注释按代码规范 V1.0:文件头模块块 + 函数头块(功能/参数/返回/限定/说明)
|
||||
|
||||
### 5.4 字体重生成(固定顺序,不得跳过或调换)
|
||||
```
|
||||
1. 代码落库(新界面字符串已写入 ui_menu.c 等 UI 源码)
|
||||
2. python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 → 仓库根 ui_cn_chars.txt(自动含新字)
|
||||
3. python tools/font_gen/make_font.py # 宋体 12px 1bpp + ASCII + 字符集 → ui_font_cn_12.c(全量重生成 + 自动补回宋体缺失的 ▶ 箭头,幂等)
|
||||
4. 生成物 diff 检查:新增字在位、旧字不变、Opts 行字符集与预期一致
|
||||
```
|
||||
- 一次性环境准备:`npm i -g lv_font_conv`、`pip install fonttools`
|
||||
- `ui_cn_chars.txt`(仓库根,受 git 跟踪)与 `ui_font_cn_12.c` **随代码一起提交**
|
||||
- 双端 `LV_FONT_DEFAULT` 均指向 `&ui_font_cn_12` → 重生成后两端自动生效
|
||||
- P1 已知的新增字若含生僻字(宋体可能无此字形)→ 在写码前先手动验证字形,避免"代码写完才发现是空白"
|
||||
- **已处理实例:▶(U+25B6)箭头**——宋体无此字形,`lv_font_conv` 静默跳过 → 真机方框。`make_font.py` 已内置 `merge_arrow_glyph()` 自动从 Segoe UI Symbol 补回(幂等),重生成字体后箭头不丢。换用自带 ▶ 的字体时可忽略
|
||||
|
||||
### 5.5 内存预算
|
||||
- 估算:`对象数 × 单价`(`lv_obj` ≈ 150B、`lv_label` ≈ 200B 量级,以实测为准)
|
||||
- **实测点**:建页前后打 `lv_mem_monitor` 池水位(沿用现有 `menu build start/done` 日志模式)
|
||||
- 红线:**最坏路径**(根页 + 最深下钻 + 当前内容页)< 40KB 且留 ≥25% 余量
|
||||
- 超预算 → 回到 P1 减元素 / 改按需建毁策略;不得靠加大 `LV_MEM_SIZE` 硬扛(RAM 总预算紧张)
|
||||
|
||||
### 5.6 Gate(进入 P3)
|
||||
- [ ] 代码与样机参数逐项一致(自查对照表)
|
||||
- [ ] 字体已重生成且 diff 检查通过
|
||||
- [ ] 内存预算通过(估算 + 代码中埋好水位日志)
|
||||
- [ ] 新文件(如有)双端注册完成
|
||||
|
||||
---
|
||||
|
||||
## 六、P3:模拟器验证
|
||||
|
||||
### 6.1 动作
|
||||
1. `cd simulator; cmake --build build --config Debug`(改过 `CMakeLists.txt` 才需重新 `cmake -B`)
|
||||
2. **离线出图**(模拟器页面渲染完成后):
|
||||
- `.\build\Debug\lvgl_sim.exe --screenshot shot.bin` → 离屏渲染首帧,输出 I1(1bpp) 位图
|
||||
- `python i1_to_png.py shot.bin -o shot.png --scale 4` → 160×160 PNG(放大便于比对)
|
||||
3. **读两图比对**:模拟器 `shot.png` vs 样机渲染图 `mockups/render_NN.png`,按 §6.2 检查单逐项核对
|
||||
(样机渲染图由 P1 的 `tools/ui_mock/render_mockup.py` 产出,即本屏"输入图片")
|
||||
4. **不合适 → 依 HTML 样机改 LVGL 代码**(HTML 为设计基准)→ 重构建 → 重新出图 → 再比对,循环至一致
|
||||
5. 交互行为(导航/滚动/选中)在窗口模式 `.\build\Debug\lvgl_sim.exe` 内按键验证(离线出图只覆盖首帧静态画面)
|
||||
|
||||
### 6.2 比对检查单
|
||||
| 项 | 通过标准 |
|
||||
|---|---|
|
||||
| 几何 | 位置/尺寸/边框与 1:1 视图逐格一致(容差 1px) |
|
||||
| 文本 | 内容完整、**无空白块**(空白 = 缺字 → 回 §5.4)、宽度/位置正确 |
|
||||
| 状态 | 选中反色正确(极性正确:黑底白字↔白底黑字不反) |
|
||||
| 导航 | ↑↓ 移动(环绕)、→/Enter 进入、←/Esc 返回;滚动条行为正常 |
|
||||
| 日志 | `menu build done: used=…` 在预算内;无 ERROR/FAIL |
|
||||
|
||||
### 6.3 已知不阻塞差异(不修)
|
||||
- 浏览器灰边抗锯齿 vs 1bpp 硬边
|
||||
- 浏览器 TTF 渲染 vs 位图字体的字形微差(同源宋体,形状近似即可)
|
||||
- 系统 DPI 缩放导致的窗口显示大小差异
|
||||
|
||||
### 6.4 Gate(进入 P4)
|
||||
- [ ] 模拟器出图 `shot.png` 与样机渲染图 `render_NN.png` 逐项同构(§6.2,忽略 6.3)
|
||||
- [ ] 导航行为符合键位语义表(架构 §3.4)
|
||||
|
||||
---
|
||||
|
||||
## 七、P4:MCU 验证
|
||||
|
||||
1. `@build` → **0 错误 0 警告**(AGENTS.md 强制;有警告看 `MDK-ARM/build_log.txt`)
|
||||
2. `@test`(一键 `test/run_boot_test.ps1`):
|
||||
- 采集窗口**先于烧录武装**(脚本 `-FlashCmd` 分支已保证顺序)
|
||||
- Required:`UI port init: OK;UI menu init: OK` + 本界面定制标记(按屏增补,如页头标题字样)
|
||||
- Forbidden:`FAIL;ERROR;HardFault`
|
||||
3. **真机目检**(用户执行,DTU-RemoteLCD @ TCP 7003):极性 / 边框 / 文字 / 选中态 / 滚动
|
||||
4. 串口日志:池水位、任务栈高水位(`uxTaskGetStackHighWaterMark`,uiTask 预算 16384 words)
|
||||
|
||||
### Gate(进入 P5)
|
||||
- [ ] @build 0错0警
|
||||
- [ ] @test PASS
|
||||
- [ ] 真机目检通过(用户确认)
|
||||
|
||||
---
|
||||
|
||||
## 八、P5:归档
|
||||
|
||||
1. 更新 `docs/UI 设计/UI_设计架构.md`:
|
||||
- 变更记录(日期 + 一行)
|
||||
- 受影响章节:文件地图 / 关键参数 / 菜单架构 / 键位语义
|
||||
2. HTML 样机**保留**于 `mockups/`(定稿版 = 设计档案,后续改界面须先改样机再改代码)
|
||||
3. 台账:该界面状态 → 已完成(日期)
|
||||
4. 提交检查:代码 + 字体(`ui_font_cn_12.c`)+ `ui_cn_chars.txt` + 样机 HTML + 文档 一起进 git
|
||||
|
||||
---
|
||||
|
||||
## 九、批量生产策略
|
||||
|
||||
1. **试点先行**:01 主界面走完全流程,校准各阶段耗时、翻译损耗点、内存基线(试点已完成 P1,后续阶段数据回填本文档)
|
||||
2. **按界面族批量**:
|
||||
- 列表页(复用现有条目行架构)→ 数据显示页(数值行框架)→ 参数编辑页(编辑/确认交互)→ 曲线/状态页
|
||||
- 每族先做**族内第一屏**产出可复用的页面框架,再批量铺同族界面
|
||||
3. **每批收尾**:字体统一重生成(各屏字符集**并集**,一次生成)→ 全量回归(已完成界面重跑 @test)
|
||||
4. 字符集只增不减;字体文件每次**全量重生成**,禁止手工拼合
|
||||
|
||||
---
|
||||
|
||||
## 十、常见坑速查
|
||||
|
||||
| 坑 | 现象 | 对策 |
|
||||
|---|---|---|
|
||||
| PowerShell 重写含中文文件 | 整文件 U+FFFD 乱码(不可逆) | 只许 Edit 工具(C6) |
|
||||
| 用了字体子集外的字 | 该字显示空白 | P1 字符集核对 + P2 字体重生成 |
|
||||
| 字体缺字形(如宋体无 ▶) | 该字符显示成方框 □ | `make_font.py` 已内置 `merge_arrow_glyph()` 自动补回;其余缺字形同法(从有该字的字体提取位图合并,§5.4) |
|
||||
| 新 .c 只注册了 Keil(或只改了 CMake) | 单端编译缺函数/静默缺失 | 双端注册(C9) |
|
||||
| 模拟器主循环调 `SDL_PollEvent` | 键盘全部无响应 | 事件只由 LVGL SDL 驱动内部轮询(架构 §5.7) |
|
||||
| 选中行"白底白字" | 选中文字不可见 | 条目行用 label 自身携带 CHECKED 态(架构 §5.5) |
|
||||
| 按键处理里删除焦点容器 | use-after-free 崩溃 | 容器持久不删(架构 §5.2) |
|
||||
| LVGL 池爆 40KB | 对象创建失败/画面残缺 | §5.5 预算 + 按需建毁 |
|
||||
| 极性配反 | 黑屏/白屏/不可读 | 模拟器 mono 主题 `dark_mode` 参数 + MCU 端背景色一致 |
|
||||
| 步进宽接近/超出格宽的标签居中 | 亚像素错位/不对称 | 左对齐 + 字体侧边距(§4.3) |
|
||||
| 关闭了 `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 阶段就拦下 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、小改动快速通道(简化流程)
|
||||
|
||||
| 改动类型 | 流程 |
|
||||
|---|---|
|
||||
| 文案修改 | 改样机 → 改代码 → **字符集核对**(有新字则重生成字体)→ P3 比对 |
|
||||
| 间距/尺寸调整 | 改样机参数表 → 改代码 → P3/P4 合并为一轮验证 |
|
||||
| 纯交互调整(键位/滚动) | 改样机/配套说明 → 改代码 → P3 验证行为 |
|
||||
| 字体重生成 | 任何引入新字的改动后强制执行 §5.4(**最常见的漏项**) |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:样机 HTML 模板骨架(仅页面)
|
||||
|
||||
```html
|
||||
<!--
|
||||
界面 NN:<名称>
|
||||
状态:制作中 / 已定稿 (YYYY-MM-DD)
|
||||
规则:160×160 设备像素 | 1bpp 纯黑白 | SimSun 12px(汉字 12px/字,ASCII 6px/字,行高 13)
|
||||
本文件仅渲染页面本身;规格见 NN_<名称>_说明.md
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>界面 NN | <名称></title>
|
||||
<style>
|
||||
html, body { margin:0; }
|
||||
body { padding:24px; background:#e8e8e8;
|
||||
display:flex; gap:24px; align-items:flex-start; flex-wrap:wrap; }
|
||||
/* 1 设备像素 = var(--u);1:1 取 1px,4x 取 4px,同源放大 */
|
||||
.ui { --u:1px; position:relative;
|
||||
width:calc(160*var(--u)); height:calc(160*var(--u));
|
||||
background:#fff; color:#000; overflow:hidden;
|
||||
font-family:SimSun,NSimSun,serif; }
|
||||
.ui.z4 { --u:4px; }
|
||||
/* 元素:绝对定位 + 精确 px,全部 calc(N*var(--u));
|
||||
禁用 border-radius / box-shadow / opacity / 渐变 / transform */
|
||||
.el { position:absolute; box-sizing:border-box; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 仅页面,无标题/说明/图例。选中/告警等状态若属设计一部分则直接画在页面内。 -->
|
||||
<div class="ui">…标记…</div> <!-- ① 1:1 -->
|
||||
<div class="ui z4">…同标记…</div> <!-- ② 4x -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## 附录 B:单界面生产检查单(复制使用)
|
||||
|
||||
```
|
||||
界面 NN:<名称> 负责人:____ 开始:____
|
||||
|
||||
P0 [ ] 台账登记(编号/作用/菜单位置)
|
||||
P1 [ ] 图片分析(结构/元素/文本逐字/比例/缺失)
|
||||
[ ] 字体度量核对(与 §2.1 一致)
|
||||
[ ] 字符集核对:__ 命中 / __ 新增
|
||||
[ ] 布局计算(验证等式:____________)
|
||||
[ ] 样机 HTML 产出(仅页面:1:1 / 4x)
|
||||
[ ] 配套 NN_名称_说明.md 产出(布局/内容/字符集/问题/映射/LVGL 要点)
|
||||
[ ] 原图校验:渲染成图 + 读两图目视比对,循环至基本一致(§4.5)
|
||||
[ ] 用户浏览器确认定稿(日期:____)
|
||||
P2 [ ] 代码翻译(与样机参数逐项一致)
|
||||
[ ] 字体重生成(extract → make_font → diff 检查)
|
||||
[ ] 新文件双端注册(如适用,C9)
|
||||
[ ] 内存预算(实测水位:____KB / 40KB)
|
||||
P3 [ ] 模拟器构建 0 错误
|
||||
[ ] 离线出图:--screenshot shot.bin → i1_to_png.py → shot.png
|
||||
[ ] shot.png 与样机渲染图(render_NN.png)逐项比对一致(几何/文本/状态);不一致依 HTML 改码再出图(§6.1)
|
||||
[ ] 导航行为验证(窗口模式按键)
|
||||
P4 [ ] @build 0 错 0 警
|
||||
[ ] @test PASS(Required:____ / Forbidden:FAIL;ERROR;HardFault)
|
||||
[ ] 真机目检通过
|
||||
P5 [ ] UI_设计架构.md 变更记录更新
|
||||
[ ] 台账状态 → 已完成(代码+字体+字符集+样机+文档 一并提交)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-09-03 | 初始版本:基于 01 主界面试点(P0/P1 已执行)沉淀全流程规范 |
|
||||
| 2026-09-03 | P3 增加"离线出图比对":模拟器 `--screenshot` 出图 + `simulator/i1_to_png.py` 转 PNG,与样机渲染图(render_NN.png)逐项比对,不合适则依 HTML 改 LVGL 代码再出图,循环至一致 |
|
||||
| 2026-09-03 | 字体 ▶(U+25B6)箭头:宋体无此字形 → 从 Segoe UI Symbol 提取位图合并入 `ui_font_cn_12.c`;`make_font.py` 内置 `merge_arrow_glyph()` 幂等自动补回,重生成字体后箭头不丢 |
|
||||
| 2026-09-03 | 顶部"开口浅 V"装饰两腿不渲染:根因 `simulator/lv_conf.h` 设 `LV_DRAW_SW_COMPLEX 0`(省代码)→ `lv_line` 斜段被跳过。改为两端显式 `LV_DRAW_SW_COMPLEX 1` + `LV_DRAW_SW_CIRCLE_CACHE_SIZE 4`;三图(原图/HTML/截图)顶部装饰现已一致 |
|
||||
| 2026-09-03 | 顶部"开口浅 V"两腿偏细(1bpp 斜线边缘被阈值吃掉):`ui_menu.c` 装饰拆 3 段 `lv_line`,两腿加粗 `MENU_DECO_LEG_W`=3、水平线 `MENU_DECO_HORIZ_W`=2(`deco_line_apply` 统一样式),三图腿粗现已一致(约 2~3px) |
|
||||
@@ -0,0 +1,228 @@
|
||||
# UI 设计架构
|
||||
|
||||
> **用途**:说明 UI 系统**如何实际工作**(渲染/输入/菜单架构)与**设计考量**(为什么这样做)。
|
||||
> **配套文档**:`LVGL模拟器部署与使用说明.md`(模拟器部署/使用)、`UI_中文字体生成与配置.md`(字体生成)。
|
||||
|
||||
---
|
||||
|
||||
## 一、UI 系统速览
|
||||
|
||||
### 1.1 总体方案
|
||||
|
||||
MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓冲(3200 B)经 **RemoDispBus 协议**(TCP 端口 7003)按需推给远端客户端(DTU-RemoteLCD)显示;远端按键走同一通道回传。"屏幕"实际是 PC 端客户端。
|
||||
|
||||
### 1.2 文件地图
|
||||
|
||||
| 文件 | 端 | 职责 |
|
||||
|---|---|---|
|
||||
| `Drivers/BSP/UI/ui_port.c/h` | MCU | LVGL 离屏显示端口(I1 + flush 到内存帧缓冲)+ 远端 keypad 输入 |
|
||||
| `Drivers/BSP/UI/ui_menu.c/h` | **共用** | 基元控件页面导航菜单(`g_menu_tab[]` 节点表驱动) |
|
||||
| `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 刷新循环 |
|
||||
| `Lib/lv_conf.h` | MCU | LVGL 配置(1bpp / 40KB 内存池 / 默认字体) |
|
||||
| `simulator/`(main.c / sim_port.c / lv_conf.h / CMakeLists.txt) | PC | SDL2 模拟器(替代 ui_port,共享 ui_menu.c 与字体) |
|
||||
| `tools/font_gen/`(extract_cn.py / make_font.py) | 工具 | 字符集提取 / 字体生成 |
|
||||
|
||||
### 1.3 关键参数
|
||||
|
||||
| 项 | 值 | 位置 |
|
||||
|---|---|---|
|
||||
| 屏幕 | 160×160,1bpp(`LV_COLOR_FORMAT_I1`) | `ui_port.h` |
|
||||
| 帧缓冲 | 3200 B(`g_ui_fbuf`,flush 回调整帧 memcpy) | `ui_port.c` |
|
||||
| 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` |
|
||||
| uiTask 栈 | 16384 words(BelowNormal) | `Src/freertos.c` |
|
||||
|
||||
### 1.4 硬约束(改 UI 必须遵守)
|
||||
|
||||
1. **RAM 紧张**:新增页面/控件/缓冲前必须核算 LVGL 池(40KB)与任务栈余量(见 `待处理内容.md`)
|
||||
2. **新增汉字必须重新生成字体**(`tools/font_gen/` 两步),否则显示空白
|
||||
3. 文本一律用静态字符串(`lv_label_set_text_static`),不 strdup
|
||||
4. UI 相关文件为 **UTF-8 带 BOM**(`Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h`),其余源码无 BOM;禁止 PowerShell 整文件重写(编码铁律,见 AGENTS.md)
|
||||
5. 模拟器与 MCU **共用** `ui_menu.c` / `ui_font_cn_12.c`,禁止在 `simulator/` 下复制副本
|
||||
6. 验证顺序:模拟器预览 → `@build`(0 错 0 警)→ `@test`(Required 加 `UI port init: OK;UI menu init: OK`)
|
||||
7. **`LV_DRAW_SW_COMPLEX` 必须为 1**(两端均显式声明):主界面顶部"开口浅 V"装饰含斜线,`lv_line` 的斜段仅在复杂渲染器下绘制(`draw_line_skew`),关闭则只画水平线、两腿消失。且 `LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4),否则零长数组编译报错(C2466)
|
||||
|
||||
---
|
||||
|
||||
## 二、渲染链路(画面怎么到"屏幕")
|
||||
|
||||
### 2.1 MCU 端(产品实际路径)
|
||||
|
||||
```
|
||||
LVGL 场景(160×160,LV_COLOR_FORMAT_I1 单色)
|
||||
→ SW 软件渲染器(仅启用 I1,裁掉其余色深)
|
||||
→ flush 回调:整帧 memcpy 到内存帧缓冲 g_ui_fbuf(3200 B)
|
||||
→ RemoDispBus 协议(TCP 7003):远端客户端发 LCDMEM 请求才回送整帧
|
||||
→ DTU-RemoteLCD 客户端(PC)解码显示
|
||||
```
|
||||
|
||||
- MCU **无物理屏幕**:"屏幕"是 PC 端客户端,帧缓冲只是网络传输的载体
|
||||
- **按需拉取**:不主动推帧,客户端要才给(菜单是低频交互,够用且省带宽)
|
||||
- LVGL 视角:`ui_port.c` 就是一个普通 display(I1 格式 + flush 回调),不知道帧最终走网络
|
||||
|
||||
### 2.2 模拟器端(PC 开发路径)
|
||||
|
||||
```
|
||||
同一份 LVGL 场景(共用 ui_menu.c / ui_font_cn_12.c)
|
||||
→ SW 渲染器(I1)
|
||||
→ SDL 窗口 blit(LV_SDL_RENDER_MODE_PARTIAL,1bpp 强制要求)
|
||||
→ 屏幕(160×160,高 DPI 下可能放大显示,属正常)
|
||||
```
|
||||
|
||||
- 两端**场景层完全相同**(同一份 UI 源码),只有"渲染结果去向"不同:MCU 进帧缓冲走网络,模拟器进 SDL 窗口
|
||||
- 模拟器必须初始化 **mono 主题**(`lv_theme_mono_init`):1bpp 下无主题 = 黑底黑字,全黑屏
|
||||
|
||||
## 三、输入链路(按键怎么到菜单)
|
||||
|
||||
### 3.1 统一模型(两端相同)
|
||||
|
||||
```
|
||||
按键 → keypad 输入设备(LV_INDEV_TYPE_KEYPAD)
|
||||
→ 默认输入组(lv_group)
|
||||
→ 组内焦点对象 = 当前可见页面容器
|
||||
→ 容器的 LV_EVENT_KEY 回调(menu_page_key_cb)
|
||||
```
|
||||
|
||||
- keypad 输入设备经 `lv_group_send_data` 把键码发给**焦点对象**
|
||||
- LVGL v9 的输入组**不自动注册默认键回调**(方向键移动焦点需焦点对象自己实现)
|
||||
→ 页面容器是组内唯一焦点对象,按键全部由容器统一处理,无焦点争抢
|
||||
- 按下时输入设备捕获焦点对象(indev_obj_act),释放时向其回发 RELEASED/CLICKED
|
||||
→ 焦点对象(容器)必须存活,这是"容器持久不删"的原因之一(见 §5.2)
|
||||
|
||||
### 3.2 MCU 端
|
||||
|
||||
```
|
||||
远端客户端按键 → TCP 7003 → ui_net_sink 解析
|
||||
→ ui_port_queue_key(UI_KEY_* → LV_KEY_* 映射)
|
||||
→ keypad 输入设备 read_cb 上报 → 默认输入组
|
||||
```
|
||||
|
||||
### 3.3 模拟器端
|
||||
|
||||
```
|
||||
SDL 键盘 → LVGL SDL 驱动内部 5ms 定时器(sdl_event_handler)
|
||||
→ 键码映射(↑↓←→/Enter/Esc → LV_KEY_*)
|
||||
→ keypad 输入设备 → 默认输入组
|
||||
```
|
||||
|
||||
- ⚠️ 主循环**不得**再调 `SDL_PollEvent`:会先消费事件队列,按键全部失效(2026-09-02 实际踩坑,见 §5.7)
|
||||
|
||||
### 3.4 键位语义
|
||||
|
||||
| 键 | 行为 |
|
||||
|---|---|
|
||||
| ↑ / ↓ | 移动选中(首尾环绕);选中行反色高亮 + 自动滚动到可见区 |
|
||||
| → / Enter | 打开选中条目的子页(叶子节点打开空白占位页) |
|
||||
| ← / Esc | 返回上级页面(本页条目销毁,父页选中态不变) |
|
||||
|
||||
## 四、菜单架构(核心)
|
||||
|
||||
### 4.1 数据驱动:节点表
|
||||
|
||||
- `g_menu_tab[]`(`ui_menu.c`):62 个节点,每项 = 层级(0=根)+ 名称
|
||||
- 树序即表序:某节点的子节点 = 其后**连续的同(level+1)层级段**
|
||||
- 根页 = 全部一级节点(⚠️ 双根 `主界面`/`厂家设置` 混排在同一根页,已知问题,待拆分)
|
||||
|
||||
### 4.2 对象树
|
||||
|
||||
```
|
||||
screen
|
||||
└─ menu(铺满屏幕,flex 纵向)
|
||||
├─ header(高 18px:返回按钮 28×16 + 标题 label)
|
||||
└─ content(flex grow,占满剩余高度)
|
||||
├─ page[0] 根页容器(depth 0)
|
||||
├─ page[1] 一级子页容器(depth 1)
|
||||
├─ page[2] 二级子页容器(depth 2)
|
||||
└─ page[3] 三级子页容器(depth 3)
|
||||
```
|
||||
|
||||
- 4 个页面容器**持久存在**(每深度一个),永不删除;同一时刻仅一个可见(HIDDEN 切换)
|
||||
- 每个容器:flex 纵向排列条目 + 自动滚动条 + 加入默认输入组 + 容器级 `LV_EVENT_KEY` 回调
|
||||
- 页头返回按钮:真 `lv_button`,**不入输入组**(不参与键导航),点击回调预留供将来触摸输入;根页隐藏
|
||||
|
||||
### 4.3 条目行
|
||||
|
||||
- 行 = `lv_label`(整行一个 label:宽 100%、高随内容、文本左对齐)
|
||||
- 行**不入输入组**:纯展示,按键一律由容器处理
|
||||
- 选中行 = CHECKED 状态 → 反色(白底黑字,1bpp 下即"高亮")
|
||||
- 行高 ≈ 字体 12px + 上下内边距 2×2 ≈ 16px;根页 12 行 ≈ 192px > 页面区 142px → 滚动条出现
|
||||
|
||||
### 4.4 页面生命周期(按需构建)
|
||||
|
||||
```
|
||||
ENTER/→ 打开子页:
|
||||
下一深度容器上构建条目(label 行)→ 隐藏旧容器、显示新容器 → 焦点移至新容器
|
||||
←/Esc 返回:
|
||||
销毁本页条目(lv_obj_clean,容器保留)→ 显示父容器 → 焦点移回父容器
|
||||
```
|
||||
|
||||
- **容器持久、条目动态**:LVGL 池内存占用 = 当前路径(根页 12 行 + 各级子页,最坏约 25KB < 40KB 池)
|
||||
- 叶子节点(无子节点):打开空白占位页(0 条目),Enter 无动作,Esc 返回
|
||||
- 深度保护:depth+1 ≥ 4 不再下钻
|
||||
|
||||
### 4.5 焦点与按键处理
|
||||
|
||||
- 焦点对象 = 当前可见页面容器;切页时 `lv_group_focus_obj` 显式转移
|
||||
- 容器回调 `menu_page_key_cb` 统一分发(见 §3.4 键位语义)
|
||||
- 选中变化只改行的 CHECKED 状态 + `lv_obj_scroll_to_view` 跟随,不重建对象
|
||||
|
||||
## 五、设计决策与考量(为什么这样做)
|
||||
|
||||
### 5.1 弃用 lv_menu 控件
|
||||
|
||||
- `lv_menu` 已被 LVGL 官方废弃;官方建议用基元控件构建菜单:
|
||||
`lv_obj` 构建页面 + 返回按钮切换可见页面(参考官方示例 `lv_example_menu_navigation`)
|
||||
- 基元控件方案的好处:无废弃风险、样式完全自主可控、与 mono 主题/1bpp 单色兼容性好
|
||||
- 配套动作:`LV_USE_MENU` 两端配置置 0(MCU `Lib/lv_conf.h` + 模拟器 `simulator/lv_conf.h`),省代码空间
|
||||
|
||||
### 5.2 页面容器持久不删
|
||||
|
||||
- keypad 输入设备在**按下时**捕获焦点对象,**释放时**向其回发 RELEASED/CLICKED
|
||||
- 若在按键处理器内删除焦点对象(如"返回时销毁当前页"),释放事件将落在已释放内存上 → use-after-free
|
||||
- 容器永不删除,回发事件始终落在存活对象上,天然安全;要释放的只是容器内的条目
|
||||
|
||||
### 5.3 条目按需建/销
|
||||
|
||||
- LVGL 池仅 40KB(`LV_MEM_SIZE`),全量预建 62 个节点的所有页面必然爆内存
|
||||
- 只建"当前路径"上的页面:内存 = 根页 + 各级子页条目(最坏约 25KB)
|
||||
- 返回即 `lv_obj_clean` 释放,内存随时回落到根页水位
|
||||
|
||||
### 5.4 容器是唯一焦点对象
|
||||
|
||||
- v9 输入组不自动处理方向键(无默认键回调),焦点移动完全由焦点对象自己实现
|
||||
- 容器集中处理按键:逻辑单点、行为可预测、无多对象焦点争抢
|
||||
- 条目行不入组:行只负责"显示选中态",不参与焦点/点击,避免行与容器抢键
|
||||
|
||||
### 5.5 条目行用 label 而非 button+子 label
|
||||
|
||||
- 样式解析规则(`lv_obj_style.c`):**先查对象自身样式**,查不到才向父对象继承(继承时用**父对象**的状态)
|
||||
- 子 label 自带主题 card 样式(文字色固定为白),自身样式总能命中 → 永远继承不到
|
||||
父按钮的选中态文字色 → 选中行"白底白字",文字不可见(2026-09-02 实际踩坑)
|
||||
- label 行**自身**携带 CHECKED 态:选中样式(白底黑字)对自身精确匹配,直接生效
|
||||
- 行不需要按钮语义(按压反馈/点击):输入由容器统一处理,行是纯展示
|
||||
|
||||
### 5.6 flat 样式剥离主题 card
|
||||
|
||||
- mono 主题给所有基元对象附加 card 样式(背景/边框/圆角/内边距/文字色)
|
||||
- 160×160 小屏 1bpp 菜单不需要卡片观感(边框圆角浪费像素)
|
||||
- flat 样式(透明背景/无边框/无圆角/零内边距)**后加优先**,逐项覆盖 card
|
||||
|
||||
### 5.7 SDL 事件只由 LVGL 驱动轮询
|
||||
|
||||
- LVGL SDL 驱动内部有 5ms 定时器(`lv_sdl_window.c` 的 `sdl_event_handler`)统一 `SDL_PollEvent` 并分发
|
||||
- 主循环若再 `SDL_PollEvent`,会**先消费**事件队列 → 驱动定时器轮询时队列已空 → 键盘全部失效
|
||||
- 关窗退出由驱动 `LV_SDL_DIRECT_EXIT=1` 自动 `exit(0)`,主循环无需处理 SDL_QUIT
|
||||
|
||||
## 六、变更记录(简表)
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-08-30 | LVGL v9.3.0 入库;初始 UI(ui_port / ui_menu / ui_net_sink / ui_task);开启 192K 全部 RAM |
|
||||
| 2026-08-31 | 推流/按键链路打通;移除 LVGL 未用 demos/tests;搭建 PC 模拟器(CMake + SDL2) |
|
||||
| 2026-09-01 | 代码规范钩子落地,UI 代码随全库规范修复 |
|
||||
| 2026-09-02 | 菜单重构:弃用 lv_menu → 基元控件页面导航(§4/§5);字体 83→78 字(补「返/回」);修复模拟器主循环吞事件致键盘失效(§5.7);修复选中行白底白字(§5.5) |
|
||||
| 2026-09-03 | 关键约束 7:`LV_DRAW_SW_COMPLEX` 须=1(顶部"浅 V"斜线依赖);两端 lv_conf 显式固化,`LV_DRAW_SW_CIRCLE_CACHE_SIZE`=4 防零长数组 |
|
||||
@@ -0,0 +1,91 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
界面 01:主界面(根页)样机 — 160×160
|
||||
状态:制作中
|
||||
规则:160×160 设备像素 | 1bpp 纯黑白 | SimSun 12px(汉字 12px/字,ASCII 6px/字,行高 13)
|
||||
本文件仅渲染页面本身;规格见 01_主界面_说明.md
|
||||
布局依据:原图 01_主界面原始图片.png 实测(628×631 @ 3.925x 缩放)
|
||||
-->
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>界面 01 | 主界面</title>
|
||||
<style>
|
||||
html, body { margin:0; }
|
||||
body { padding:24px; background:#e8e8e8;
|
||||
display:flex; gap:24px; align-items:flex-start; flex-wrap:wrap; }
|
||||
|
||||
/* ===== 屏体:1 设备像素 = var(--u) ===== */
|
||||
.ui { --u:1px; position:relative;
|
||||
width:calc(160*var(--u)); height:calc(160*var(--u));
|
||||
background:#000; color:#fff; overflow:hidden;
|
||||
font-family:SimSun,NSimSun,serif; }
|
||||
.ui.z4 { --u:4px; }
|
||||
|
||||
/* 顶部装饰:开口浅 V(底边 y=17 x16~145;两腿外上翘至 y=7 x7/x158),线宽 2px
|
||||
(注:MCU 实现中两腿加粗为 3px 以补偿 1bpp 斜线阈值损耗,见 ui_menu.c MENU_DECO_LEG_W) */
|
||||
.deco { position:absolute; left:0; top:0;
|
||||
width:calc(160*var(--u)); height:calc(160*var(--u)); display:block; }
|
||||
|
||||
/* 状态行:左对齐 x=17,y=20 */
|
||||
.status { position:absolute; left:calc(17*var(--u)); top:calc(20*var(--u));
|
||||
font-size:calc(12*var(--u)); line-height:calc(13*var(--u)); white-space:nowrap; }
|
||||
|
||||
/* 列表框:左对齐 x=0,宽 99,7 行 × 15px + 1px 边框 = 高 107 */
|
||||
.list { position:absolute; left:0; top:calc(44*var(--u));
|
||||
width:calc(99*var(--u)); height:calc(107*var(--u));
|
||||
box-sizing:border-box; border:calc(1*var(--u)) solid #fff; }
|
||||
|
||||
/* 行:高 15;文字左对齐(左留白 8);箭头紧跟文字(x=58) */
|
||||
.row { position:absolute; left:0; width:100%; height:calc(15*var(--u));
|
||||
font-size:calc(12*var(--u)); line-height:calc(15*var(--u)); }
|
||||
.row .lb { position:absolute; left:calc(8*var(--u)); top:0; }
|
||||
.row .ar { position:absolute; left:calc(58*var(--u)); top:0; }
|
||||
.row.sel { background:#fff; color:#000; }
|
||||
.r1 { top:calc(0*var(--u)); }
|
||||
.r2 { top:calc(15*var(--u)); }
|
||||
.r3 { top:calc(30*var(--u)); }
|
||||
.r4 { top:calc(45*var(--u)); }
|
||||
.r5 { top:calc(60*var(--u)); }
|
||||
.r6 { top:calc(75*var(--u)); }
|
||||
.r7 { top:calc(90*var(--u)); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ① 1:1 实际大小(160×160,所见即真机) -->
|
||||
<div class="ui">
|
||||
<svg class="deco" viewBox="0 0 160 160">
|
||||
<path d="M 7 7 L 16 17 L 145 17 L 158 7" fill="none" stroke="#fff" stroke-width="2"/>
|
||||
</svg>
|
||||
<div class="status">当前模式 : 无模式</div>
|
||||
<div class="list">
|
||||
<div class="row r1 sel"><span class="lb">装置信息</span></div>
|
||||
<div class="row r2"><span class="lb">实时数据</span><span class="ar">▶</span></div>
|
||||
<div class="row r3"><span class="lb">参数定值</span><span class="ar">▶</span></div>
|
||||
<div class="row r4"><span class="lb">三遥设置</span><span class="ar">▶</span></div>
|
||||
<div class="row r5"><span class="lb">装置维护</span><span class="ar">▶</span></div>
|
||||
<div class="row r6"><span class="lb">通讯参数</span><span class="ar">▶</span></div>
|
||||
<div class="row r7"><span class="lb">记录查询</span><span class="ar">▶</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ② 4x 放大(1 设备像素 = 4 屏幕像素,布局与 1:1 完全等价) -->
|
||||
<div class="ui z4">
|
||||
<svg class="deco" viewBox="0 0 160 160">
|
||||
<path d="M 7 7 L 16 17 L 145 17 L 158 7" fill="none" stroke="#fff" stroke-width="2"/>
|
||||
</svg>
|
||||
<div class="status">当前模式 : 无模式</div>
|
||||
<div class="list">
|
||||
<div class="row r1 sel"><span class="lb">装置信息</span></div>
|
||||
<div class="row r2"><span class="lb">实时数据</span><span class="ar">▶</span></div>
|
||||
<div class="row r3"><span class="lb">参数定值</span><span class="ar">▶</span></div>
|
||||
<div class="row r4"><span class="lb">三遥设置</span><span class="ar">▶</span></div>
|
||||
<div class="row r5"><span class="lb">装置维护</span><span class="ar">▶</span></div>
|
||||
<div class="row r6"><span class="lb">通讯参数</span><span class="ar">▶</span></div>
|
||||
<div class="row r7"><span class="lb">记录查询</span><span class="ar">▶</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,77 @@
|
||||
# 界面 01:主界面(根页)— 规格说明
|
||||
|
||||
> 配套样机:`01_主界面.html`(仅渲染页面本身)。本文件为设计规格(source of truth):布局参数、字符集核对、待确认问题、映射、LVGL 翻译要点。
|
||||
> 规则:160×160 设备像素 | 1bpp 纯黑白 | SimSun 12px(汉字 12px/字,ASCII 6px/字,行高 13)
|
||||
> 布局依据:原图 `01_主界面原始图片.png`(628×631 @ 3.925x 缩放)实测 + 渲染目视比对。
|
||||
|
||||
---
|
||||
|
||||
## 一、布局参数(单位:设备像素)
|
||||
|
||||
| 项 | 值 | 说明 |
|
||||
|---|---|---|
|
||||
| 顶部装饰 | 开口浅 V,两腿 3px / 水平线 2px | 左尖 (7,7)、底左 (16,17)、底右 (145,17)、右尖 (158,7);底边 y=17,两腿外上翘至 y=7。实现拆 3 段 `lv_line`:两腿加粗(`MENU_DECO_LEG_W`=3)补偿 1bpp 斜线边缘阈值损耗,水平线保持 2px(`MENU_DECO_HORIZ_W`) |
|
||||
| 状态行 | 左对齐 x=17,顶 y=20 | 行高 13,文案"当前模式 : 无模式"(全角冒号,两侧半角空格) |
|
||||
| 列表框 | x=0,顶 y=44,宽 99,高 107 | **左对齐**(非通栏),1px 白边框,内 7 行 × 15px(7×15+2 边框=107) |
|
||||
| 行高 | 15 | 7 行 |
|
||||
| 行内文字 | 左对齐,左留白 8 | 4 字 = 48px(x 8~56) |
|
||||
| 箭头 ▶ | x=58(紧跟文字) | 非右对齐;仅第 2~7 行有 |
|
||||
| 选中行 | 第 1 行整行反色 | 白底黑字,**无箭头**,白色高亮铺满框宽 |
|
||||
|
||||
## 二、行内容与状态
|
||||
|
||||
| 行 | 文本 | 箭头 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 1 | 装置信息 | 无 | **选中**(整行反色:白底黑字) |
|
||||
| 2 | 实时数据 | ▶(紧跟文字) | 普通(黑底白字) |
|
||||
| 3 | 参数定值 | ▶(紧跟文字) | 普通 |
|
||||
| 4 | 三遥设置 | ▶(紧跟文字) | 普通 |
|
||||
| 5 | 装置维护 | ▶(紧跟文字) | 普通 |
|
||||
| 6 | 通讯参数 | ▶(紧跟文字) | 普通 |
|
||||
| 7 | 记录查询 | ▶(紧跟文字) | 普通 |
|
||||
|
||||
说明:选中行(第 1 行)无 ▶ 箭头;其余行 ▶ 紧跟在 4 字文本后(非右对齐)。行内文字左对齐、左留白 8px。
|
||||
|
||||
## 三、字符集核对(对照现有 ui_font_cn_12:78 汉字 + ASCII 0x20~0x7E)
|
||||
|
||||
- **菜单 7 项**共 22 个唯一汉字:命中 21(装 置 信 息 实 时 数 据 参 定 值 遥 设 维 护 通 讯 记 录 查 询),**新增 1(三)**。
|
||||
- **状态行**"当前模式 : 无模式":**新增 5(当 前 模 式 无)** + **全角冒号 :(U+FF1A,新增)**;空格为 ASCII(已在字库)。
|
||||
- **箭头**:**▶(U+25B6,新增符号)**。
|
||||
|
||||
→ **确定新增 8 个字形**:三 当 前 模 式 无 : ▶。
|
||||
|
||||
写 LVGL 代码前必须重新生成字体,否则新增字在真机显示为空白。流程:代码落库 → `python tools/font_gen/extract_cn.py`(自动从 UI 源码字符串提取)→ `python tools/font_gen/make_font.py` → 两端(MCU/模拟器)同时生效。
|
||||
|
||||
## 四、待确认问题
|
||||
|
||||
> 设计形态(框宽/选中态/装饰形状/冒号)已按原图定形(见第一、二节),不再待确认。以下为**内容/数据**问题,需用户答复。
|
||||
|
||||
| # | 问题 | 说明 |
|
||||
|---|---|---|
|
||||
| Q1 | 7 个条目 ↔ 子树映射(见第五节表)是否成立? | 其中 3 项疑似重命名(参数定值/三遥设置/通讯参数),需逐项确认 |
|
||||
| Q2 | "三遥设置"对应现有哪个节点? | 现有 g_menu_tab 有"遥测量"(测量值/遥调系数/遥测系数/双字节/装置遥调);"三遥"是否 = 遥测+遥信+遥控 的新节点? |
|
||||
| Q3 | "厂家设置 / 版本信息"入口在哪? | 旧菜单中"厂家设置"为独立顶级项(含元件设置/恢复默认/版本显示/版本信息),新 7 项列表未包含——从哪进入? |
|
||||
| Q4 | "当前模式"指什么、数据从哪来? | 状态行动态值来源(运行/保护/通信模式?);"无模式"为默认?需 App 层供给字符串 |
|
||||
|
||||
## 五、条目 → 子树 建议映射(Q1,均待确认;对照现有 g_menu_tab)
|
||||
|
||||
| # | 条目 | 现有节点 | 建议映射 |
|
||||
|---|---|---|---|
|
||||
| 1 | 装置信息 | 装置信息(L1) | 直接沿用(无变化) |
|
||||
| 2 | 实时数据 | 实时数据(L1)→ 交流量/直流量/开入量 | 直接沿用(无变化) |
|
||||
| 3 | 参数定值 | 定值参数(L1)→ 定值/查看 | **重命名** 定值参数 → 参数定值?(待确认) |
|
||||
| 4 | 三遥设置 | 遥测量(L1)→ 测量值/遥调系数/… | 对应"遥测量"?或新增"三遥"(遥测/遥信/遥控)节点?(Q2) |
|
||||
| 5 | 装置维护 | 装置维护(L1)→ 时间设置/强制复归/手动录波/自动录波 | 直接沿用(无变化) |
|
||||
| 6 | 通讯参数 | 通讯设置(L1)→ 通讯参数/网络配置/SNTP设置 | **重命名** 通讯设置 → 通讯参数?或提升 L2"通讯参数"?(待确认) |
|
||||
| 7 | 记录查询 | 记录查询(L1)→ SOE/事故/操作/告警/… | 直接沿用(无变化) |
|
||||
|
||||
注:旧菜单中的"厂家设置"(L0,含 元件设置/恢复默认/版本显示/版本信息/装置遥调/装置记录)未出现在新 7 项列表中,入口待 Q3 确认。
|
||||
|
||||
## 六、LVGL 翻译要点(阶段 2 用)
|
||||
|
||||
1. 列表 = 持久容器 `lv_obj`(x=0, y=44, 99×107,1px 边框)+ 7 个行子 `lv_obj`(高 15,绝对定位 `lv_obj_set_pos`,固定布局更可控)
|
||||
2. 行 = `lv_obj` + `lv_label`(静态文本,左对齐 x=8)+ `lv_label`("▶",x=58,紧跟文字);选中行 = `LV_STATE_CHECKED` 整行反色(白底黑字,高亮铺满框宽)
|
||||
3. 状态行 = `lv_label`(x=17, y=20,文本由 App 层供给,Q4)
|
||||
4. 顶部装饰 = 开口浅 V(4 段折线:(7,7)→(16,17)→(145,17)→(158,7)):LVGL 可用 3 条 `lv_obj` 线段拼合(斜线用 `lv_line`),或预渲染 1bpp 位图 `lv_image`(更省对象、更精确)
|
||||
5. 1bpp 禁区(本样机均未使用):圆角 / 阴影 / 透明度 / 渐变 / 多色
|
||||
6. 内存:根页 = 1 列表容器 + 7 行 ×(行 obj + 2 label)+ 状态 label + 装饰 ≈ 20+ 个对象,为最高频页面,阶段 2 需核算 40KB LVGL 池占用
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Reference in New Issue
Block a user