a7f526569a
取值依据是实测水位,不是预估:菜单根框 5,368 B、开页-关页往返
5,448 B、最重页面峰值 6,568 B(占 16 KB 的 40%)、真机开机
used=3,192 B。注意 16,384 B 建池后 lv_mem_monitor 报的 total 是
15,020 B(TLSF 控制块与对齐元数据 ≈1,364 B),核算余量用后者。
两端 lv_conf.h 同步改值(lv-conf-sync 生效值比对 163 项 PASS),
Lib/lv_conf.h 的 UTF-8 BOM 已按原样补回(Edit 会静默剥掉)。
验证:
- @build 0 错 0 警;ZI 154,456 → 129,880,精确 −24,576 B
- SRAM1 98,144/74.9% → 73,568/56.1%(余 57,504 B);CCM 不变 87.9%
- 模拟器菜单回归 PASS(8 状态像素 + 池基线 5,368 B 未变);11 页
黄金参考逐字节 10/11 一致,往返池水位 5,448 B 分毫未变
- 唯一差异 netcfg_save 保存态:用 40 KB 旧池做 A/B 复测同样失败,
确认与本改动无关(属 506eccc 的 cfg_file 行为变化),未刷黄金
参考掩盖,留给该改动属主处置
- @test PASS:fw build Oct 15 2026 07:31:34,无 [ERR]/Fault,
开机 622 ms 到 net init: OK,menu build done: used=3192
- 本地 CI 全绿(L0 + L0.5,15 runner / 955 断言)
- 台账受管区与内存快照区已 --write 回填,mem-budget / mem-numbers
由红转绿
连带结论:R-2 后半待迁体量 54,146 → 29,570 B,瓶颈由 SRAM1 换到
CCM(只剩 7,904 B);解锁链第 ① 步完成,下一步只差缩堆 56→32 KB
(B5 是反向扩容,缩回前须重跑 stack_report.py + 上板 min_ever)。
文档同步 20 处:本报告按修复即销项删除 R-9 行,并改写 §0.2/§0.3/
§0.4/§5.1/§5.2/§5.3/§6.4 与变更记录、去处表;RAM使用分析 §2.6.2
新增实测水位表、阶段 1.1 标已执行;台账 §3 解锁链、登记簿 N-27、
任务规划 README W3 行与链路图;UI 四篇、产品需求、内存内容分配
策略、运行与自检记录、项目部署环境要求、测试开发标准流程、
simulator/README、ui-verify skill 常量。顺带订正三处与脚本不符的
旧值:池水位常量 4784/4864 → 5368/5448、FreeRTOS 堆 48 → 56 KB。
910 lines
69 KiB
Markdown
910 lines
69 KiB
Markdown
# 菜单页面设计
|
||
|
||
> **用途**:定义 DTU 主菜单的**内容(42 节点层级树)、视觉规范、三层代码架构**(数据层 / 显示层 / 逻辑层),以及叶子节点进入后的**内容页模块**(§九)。
|
||
> **范围**:菜单层级(节点/父子/层级/进出导航)+ 三个已实现的内容页(装置信息/交流量/直流量,§九);其余叶子的内容页后续按同模式扩展。
|
||
> **来源**:用户提供的主菜单截图 7 屏(根页 + 实时数据/参数定值/三遥设置/装置维护/通讯参数/记录查询各一级子页 + 参数定值·整定二级)。
|
||
> **导航模型**:树形**逐级进入**——`ENTER`/`→` 进入子页,`ESC`/`←` 返回上一级;最多 **4 级(L0~L3)**。
|
||
|
||
> **阅读指南**:
|
||
>
|
||
> | 你想… | 读 |
|
||
> |---|---|
|
||
> | 搞懂**哪段代码干什么、三层怎么配合** | **§一 架构总览**(职责速览 / 调用矩阵 / 一次按键的旅程 / 初始化时序 / 层间不变量),再按需深入 §三 / §五 / §六 |
|
||
> | 深入某一层 | §三 数据层 / §五 显示层 / §六 逻辑层(各章自包含:职责边界 + 字段 + API) |
|
||
> | **改菜单**(增删/改名/换序条目) | §二(内容)+ §3.7(只改文本源、重跑生成器,代码零改动) |
|
||
> | **改视觉**(框/行/几何) | §四(显示规范)+ §五(显示层实现) |
|
||
> | **内容页**(装置信息/交流量/直流量) | §九(对象树/生命周期/页面定义表/API/内存预算) |
|
||
> | 搞懂**内存**(什么存、什么现算) | §7.3 + §7.5 |
|
||
> | **验证 / 调试** | §八 |
|
||
|
||
---
|
||
|
||
## 一、架构总览:三层拆分与协作
|
||
|
||
### 1.1 为什么拆三层
|
||
|
||
"菜单表 + 导航状态 + LVGL 渲染" 三块职责若同处一个文件则互相耦合(改菜单要动导航代码、改渲染易破坏导航)。因此拆为三层,**一层一文件、各管一件事**:
|
||
|
||
- **数据层**——只回答"**有什么条目**":只读菜单表 + 4 个查询 API;
|
||
- **显示层**——只回答"**怎么画**":持有**全部** LVGL 对象,**零**导航状态;
|
||
- **逻辑层**——只回答"**用户在哪、选中哪个**":唯一 17 B 导航状态 + 键语义(移/进/退)。
|
||
|
||
3 个文件 **MCU / PC 模拟器双端共用**(双端注册:Keil `.uvprojx` + `simulator/CMakeLists.txt`,见 `UI_设计架构.md` §1.4 约束 5)。
|
||
|
||
### 1.2 三层职责速览
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
K(["按键(远端键 / 模拟器键)"]) --> L["逻辑层 ui_menu.c<br/>唯一 17 B 导航状态(path/选择/深度/滚动)<br/>键语义:移动 / 进入 / 退出"]
|
||
L -- "单向驱动: frame_build / frame_destroy / frame_refresh / focus" --> V["显示层 ui_menu_view.c/h<br/>持全部 LVGL 对象,纯渲染,零导航状态"]
|
||
L -- "只读查询: children / valid" --> T["数据层 ui_menu_tab.c/h(生成物,只读)<br/>g_menu_tab[42] 父子索引表 + 4 个查询 API"]
|
||
V -- "只读查询: children / has_children / name" --> T
|
||
```
|
||
|
||
| 层 | 文件 | 一句话职责 | 持 LVGL 对象? | 持导航状态? |
|
||
|---|---|---|---|---|
|
||
| **数据层** | `Drivers/BSP/UI/ui_menu_tab.c/h`(生成物) | 回答"有什么条目":菜单表 + 查询 API | 否 | 否 |
|
||
| **逻辑层** | `Drivers/BSP/UI/ui_menu.c/h` | 回答"用户在哪、选中哪个":导航状态 + 键语义 | 否 | **是(唯一,17 B)** |
|
||
| **显示层** | `Drivers/BSP/UI/ui_menu_view.c/h` | 回答"怎么画":级联框渲染 | **是(全部)** | 否 |
|
||
|
||
> 记忆口诀:**数据层管"料",逻辑层管"账",显示层管"画"**。
|
||
|
||
### 1.3 依赖方向与调用矩阵
|
||
|
||
依赖**严格单向**:`逻辑 → 显示 → 数据`,数据层不依赖任何人;**禁止反向调用**(显示层不调逻辑层;数据/显示层不引用导航状态)。
|
||
|
||
| 调用方 ↓ / 被调 → | 数据层 | 显示层 | 逻辑层 |
|
||
|---|---|---|---|
|
||
| **数据层** `ui_menu_tab` | — | ✗ | ✗ |
|
||
| **显示层** `ui_menu_view` | ✓ 查询 API | — | ✗ |
|
||
| **逻辑层** `ui_menu` | ✓ 查询 API | ✓ 8 个显示接口 | — |
|
||
|
||
层间"词汇"只有两组接口(各层章节有完整签名):
|
||
|
||
| 方向 | 接口 |
|
||
|---|---|
|
||
| 逻辑 → 显示 | `ui_menu_view_init / container / frame_build / frame_destroy / frame_refresh / set_scroll / get_scroll / focus`(§5.5) |
|
||
| 逻辑/显示 → 数据 | `ui_menu_tab_children / has_children / name / valid`(§3.4) |
|
||
|
||
### 1.4 按键协作:一次按键的旅程
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant K as 按键(远端键 / 模拟器键)
|
||
participant G as LVGL 默认输入组(lv_group)
|
||
participant L as 逻辑层 ui_menu.c
|
||
participant V as 显示层 ui_menu_view
|
||
participant T as 数据层 ui_menu_tab
|
||
K->>G: 键码
|
||
Note over G: 焦点 = 持久容器 s_cont(唯一键入口;帧深度由逻辑层 s_depth 决定)
|
||
G->>L: LV_EVENT_KEY → menu_key_cb(深度=s_depth)→ menu_key_handle
|
||
L->>L: 状态变更:移动 s_sel[d] / 进入 s_depth+1+s_path / 退出 s_depth−1
|
||
L->>V: frame_refresh(高亮+跟滚)/ frame_build(建新框)/ frame_destroy(拆最右框)+ focus(移焦点)
|
||
V->>T: children / has_children / name(条目现算不落盘)
|
||
V-->>L: get_scroll(读回滚动位置,逻辑层写入 s_scroll[d])
|
||
Note over V: LVGL 下一帧渲染 → 屏幕变化
|
||
```
|
||
|
||
两个要点:
|
||
- **逻辑层决定"改成什么状态",显示层决定"画面怎么变"**——显示层只执行、不决策;数据层全程不参与(只被显示层查询条目/名字/菜单叶子)。
|
||
- 全程**单向同步**:一次按键 = 一次完整的"状态变更 → 画面更新",无异步、无回调上行。
|
||
|
||
### 1.5 初始化时序(`ui_menu_init`)
|
||
|
||
`uiTask` 等 `SYS_EVT_NET_READY` 后调 `ui_menu_init()`,三层按此顺序配合:
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as uiTask
|
||
participant L as 逻辑层 ui_menu.c
|
||
participant V as 显示层 ui_menu_view
|
||
participant T as 数据层 ui_menu_tab
|
||
U->>L: ui_menu_init()
|
||
L->>T: ui_menu_tab_valid() 表自检
|
||
alt 父索引越界(表损坏)
|
||
T-->>L: 不过 → 打 ERROR、放弃建菜单(返回 -1)
|
||
else 自检通过
|
||
T-->>L: 通过
|
||
end
|
||
L->>L: 清零 17 B 导航状态(s_path/s_sel/s_scroll=0,s_depth=0)
|
||
L->>V: ui_menu_view_init()(样式 → 屏幕根容器 → 持久容器(装饰/状态行自绘回调) → 根框本体(含自绘回调))
|
||
L->>V: 持久容器 lv_group_add_obj(默认组)+ 挂 menu_key_cb(深度取 s_depth)
|
||
L->>V: ui_menu_view_frame_build(0) 注册根框行数据 + focus()
|
||
Note over L: lv_mem_monitor 打印建前/建后 LVGL 池
|
||
L-->>U: 0(成功)
|
||
```
|
||
|
||
- 日志标记:`UI menu init: OK`(uiTask 打印)——`@test` 回归可作 Required 标记。
|
||
- LVGL 池实测(模拟器 16 KB 池):根框建完 `used=5368 B`(旧行对象实现为 13312 B,自绘 + 容器收敛省 8.3 KB);最深四级级联 `used=6040 B`(增量 1256 B = 3 个子框体),远小于池余量。
|
||
|
||
### 1.6 层间不变量(每层"绝不做")
|
||
|
||
| 层 | 绝不做 | 为什么 |
|
||
|---|---|---|
|
||
| 数据层 | 业务逻辑(4 个 API 是纯表过滤)、依赖 LVGL、修改表(`const`) | 表是唯一事实源;改表 = 改菜单(走生成器 §3.6) |
|
||
| 显示层 | 持导航状态(17 B 全在逻辑层)、解释按键(输入回调挂在逻辑层) | 显示层 = "状态的纯函数":同样状态必画同样画面 → 易验证、双端可复用 |
|
||
| 逻辑层 | 创建/修改 LVGL 对象(建对象全在显示层)、持 `lv_obj_t*` 句柄 | 唯一例外:输入接线 2 调 `lv_group_add_obj` / `lv_obj_add_event_cb`(容器须入输入组)+ 读事件参数 + `lv_mem_monitor` 内存监控 |
|
||
|
||
---
|
||
|
||
## 二、菜单内容:42 节点层级树
|
||
|
||
### 2.1 层级总览
|
||
|
||
主菜单为 4 级树:
|
||
|
||
| 层级 | 含义 | 例子 |
|
||
|---|---|---|
|
||
| **L0 根** | 主界面,7 个一级条目 | 主界面 |
|
||
| **L1 一级** | 根页的 7 个条目 | 装置信息、实时数据、参数定值、三遥设置、装置维护、通讯参数、记录查询 |
|
||
| **L2 二级** | 一级子菜单的子项 | 交流量、整定、遥测死区、时钟设置、通讯设置、SOE记录 … |
|
||
| **L3 三级** | 仅"参数定值 → 整定/查看"下的子项 | 参数、定值、控制字、软压板 |
|
||
|
||
节点分两类(决定导航行为):
|
||
|
||
| 类型 | 标识 | 行为 |
|
||
|---|---|---|
|
||
| **菜单节点** | 行尾 **▶** 箭头 | `ENTER` 进入 → 展开下一级子页 |
|
||
| **叶子节点** | 无箭头 | `ENTER` 进入 → 有内容页(node 1/3/4,§九)则切内容屏;否则进空白终点框 |
|
||
|
||
> 根页默认选中**第 1 项"装置信息"**(整行反色:白带、黑字)。
|
||
|
||
### 2.2 完整层级树
|
||
|
||
```text
|
||
主界面 (L0)
|
||
├─ 装置信息 [叶子]
|
||
├─ 实时数据 ▶ [菜单]
|
||
│ ├─ 交流量 [叶子]
|
||
│ ├─ 直流量 [叶子]
|
||
│ └─ 遥信量 [叶子]
|
||
├─ 参数定值 ▶ [菜单]
|
||
│ ├─ 整定 ▶ [菜单]
|
||
│ │ ├─ 参数 [叶子]
|
||
│ │ ├─ 定值 [叶子]
|
||
│ │ ├─ 控制字 [叶子]
|
||
│ │ └─ 软压板 [叶子]
|
||
│ └─ 查看 ▶ [菜单]
|
||
│ ├─ 参数 [叶子]
|
||
│ ├─ 定值 [叶子]
|
||
│ ├─ 控制字 [叶子]
|
||
│ └─ 软压板 [叶子]
|
||
├─ 三遥设置 ▶ [菜单]
|
||
│ ├─ 遥测死区 [叶子]
|
||
│ ├─ 遥测系数 [叶子]
|
||
│ ├─ 遥信类型 [叶子]
|
||
│ ├─ 遥信防抖 [叶子]
|
||
│ └─ 双点遥信 [叶子]
|
||
├─ 装置维护 ▶ [菜单]
|
||
│ ├─ 时钟设置 [叶子]
|
||
│ ├─ 强制复归 [叶子]
|
||
│ ├─ 手动录波 [叶子]
|
||
│ └─ 清除记录 [叶子]
|
||
├─ 通讯参数 ▶ [菜单]
|
||
│ ├─ 通讯设置 [叶子]
|
||
│ ├─ 网口设置 [叶子]
|
||
│ └─ SNTP设置 [叶子]
|
||
└─ 记录查询 ▶ [菜单]
|
||
├─ SOE记录 [叶子]
|
||
├─ 事故记录 [叶子]
|
||
├─ 操作记录 [叶子]
|
||
├─ 保护告警 [叶子]
|
||
├─ 保护启动 [叶子]
|
||
├─ 遥控记录 [叶子]
|
||
├─ 自检记录 [叶子]
|
||
├─ 运行记录 [叶子]
|
||
└─ 运行报告 [叶子]
|
||
```
|
||
|
||
**统计**:菜单节点 **9** 个(含根),叶子节点 **33** 个,共 **42** 个节点;最大深度 **4 级**。
|
||
|
||
### 2.3 节点定义表(逻辑层级定义)
|
||
|
||
> 扁平表,与 §3.3 生成物 `g_menu_tab[]` 一一对应:`#` = 数组下标,`父节点` = 父菜单的 `#`,`子项数` = 直接子项个数(叶子为 0)。
|
||
|
||
| # | 名称 | 层级 | 类型 | 父节点 | 子项数 |
|
||
|---|---|---|---|---|---|
|
||
| 0 | 主界面 | L0 | 菜单(根) | — | 7 |
|
||
| 1 | 装置信息 | L1 | 叶子 | 0 | 0 |
|
||
| 2 | 实时数据 | L1 | 菜单 | 0 | 3 |
|
||
| 3 | 交流量 | L2 | 叶子 | 2 | 0 |
|
||
| 4 | 直流量 | L2 | 叶子 | 2 | 0 |
|
||
| 5 | 遥信量 | L2 | 叶子 | 2 | 0 |
|
||
| 6 | 参数定值 | L1 | 菜单 | 0 | 2 |
|
||
| 7 | 整定 | L2 | 菜单 | 6 | 4 |
|
||
| 8 | 参数 | L3 | 叶子 | 7 | 0 |
|
||
| 9 | 定值 | L3 | 叶子 | 7 | 0 |
|
||
| 10 | 控制字 | L3 | 叶子 | 7 | 0 |
|
||
| 11 | 软压板 | L3 | 叶子 | 7 | 0 |
|
||
| 12 | 查看 | L2 | 菜单 | 6 | 4 |
|
||
| 13 | 参数 | L3 | 叶子 | 12 | 0 |
|
||
| 14 | 定值 | L3 | 叶子 | 12 | 0 |
|
||
| 15 | 控制字 | L3 | 叶子 | 12 | 0 |
|
||
| 16 | 软压板 | L3 | 叶子 | 12 | 0 |
|
||
| 17 | 三遥设置 | L1 | 菜单 | 0 | 5 |
|
||
| 18 | 遥测死区 | L2 | 叶子 | 17 | 0 |
|
||
| 19 | 遥测系数 | L2 | 叶子 | 17 | 0 |
|
||
| 20 | 遥信类型 | L2 | 叶子 | 17 | 0 |
|
||
| 21 | 遥信防抖 | L2 | 叶子 | 17 | 0 |
|
||
| 22 | 双点遥信 | L2 | 叶子 | 17 | 0 |
|
||
| 23 | 装置维护 | L1 | 菜单 | 0 | 4 |
|
||
| 24 | 时钟设置 | L2 | 叶子 | 23 | 0 |
|
||
| 25 | 强制复归 | L2 | 叶子 | 23 | 0 |
|
||
| 26 | 手动录波 | L2 | 叶子 | 23 | 0 |
|
||
| 27 | 清除记录 | L2 | 叶子 | 23 | 0 |
|
||
| 28 | 通讯参数 | L1 | 菜单 | 0 | 3 |
|
||
| 29 | 通讯设置 | L2 | 叶子 | 28 | 0 |
|
||
| 30 | 网口设置 | L2 | 叶子 | 28 | 0 |
|
||
| 31 | SNTP设置 | L2 | 叶子 | 28 | 0 |
|
||
| 32 | 记录查询 | L1 | 菜单 | 0 | 9 |
|
||
| 33 | SOE记录 | L2 | 叶子 | 32 | 0 |
|
||
| 34 | 事故记录 | L2 | 叶子 | 32 | 0 |
|
||
| 35 | 操作记录 | L2 | 叶子 | 32 | 0 |
|
||
| 36 | 保护告警 | L2 | 叶子 | 32 | 0 |
|
||
| 37 | 保护启动 | L2 | 叶子 | 32 | 0 |
|
||
| 38 | 遥控记录 | L2 | 叶子 | 32 | 0 |
|
||
| 39 | 自检记录 | L2 | 叶子 | 32 | 0 |
|
||
| 40 | 运行记录 | L2 | 叶子 | 32 | 0 |
|
||
| 41 | 运行报告 | L2 | 叶子 | 32 | 0 |
|
||
|
||
---
|
||
|
||
## 三、数据层:ui_menu_tab(生成物,只读)
|
||
|
||
> **一句话职责**:提供"有什么条目"——一张扁平父子索引表(42 节点,`const` 存 Flash、只读)+ 4 个查询 API,供逻辑层 / 显示层共用。
|
||
> **绝不做**:持有任何状态、依赖 LVGL、被手改(改菜单走 §3.6 生成器)。
|
||
|
||
### 3.1 选型:扁平"父子索引表"(邻接表 / 关系表模型)
|
||
|
||
成熟树存储方案对比(参考 Wikipedia "Tree (abstract data type)"、文件系统/关系库的层级表示):
|
||
|
||
| 方案 | 原理 | 本方案取舍 |
|
||
|---|---|---|
|
||
| 指针树(动态节点,父/子指针) | 运行时动态增删 | ✗ 需堆 + 指针,嵌入端 GC/碎片/内存风险,`const` 入 Flash 困难 |
|
||
| 首子-兄弟(left-child right-sibling) | 每节点只存 first_child / next_sibling | 紧凑但"取某节点全部子项"需链式遍历,不如邻接直观,调试不友好 |
|
||
| 数组位置表示(堆式) | 靠下标位置推父子(2i/2i+1) | ✗ 要求满二叉/固定出度;菜单出度不定且有命名,不适用 |
|
||
| **邻接表 / 父子索引表(关系表)** | 每行自带 `parent` 索引,子项=parent 等于本下标的行 | ✓ **选用**:无指针、每行自描述、增删改=改行、可入 Flash、启动可校验 |
|
||
|
||
> LVGL 8 已移除 `lv_menu` 控件(`lv_conf.h` 中 `LV_USE_MENU 0`),本项目的扁平表方案与"无 lv_menu"约束一致,且比控件更贴合嵌入端。
|
||
|
||
**核心不变式**:
|
||
- 子项 = 表中所有 `parent == 本下标` 的行,**按表序**排列(表序即显示序)。
|
||
- **菜单 vs 叶子由"是否有子项"自动推导**:加子项→自动出现 ▶;删光子项→自动变叶子。无需单独的类型字段,替换时零维护。
|
||
|
||
### 3.2 数据结构定义
|
||
|
||
```c
|
||
#include <stdint.h>
|
||
#include <stdbool.h>
|
||
|
||
#define MENU_ROOT 0xFFu /* 根哨兵:仅作根节点(主界面 #0)的 parent,表示"它是顶层";8 位,上限 255 项 */
|
||
|
||
/* 菜单节点 = 扁平表一行;子项 = parent 等于本下标的所有行(按表序) */
|
||
typedef struct {
|
||
uint8_t parent; /* 父节点下标(0..254);仅根节点(主界面#0)为 MENU_ROOT(0xFF) */
|
||
const char *name; /* 显示文本(字面量,存 Flash) */
|
||
} ui_menu_node_t;
|
||
```
|
||
|
||
> 实现落在生成器产物 `Drivers/BSP/UI/ui_menu_tab.h`(见 §3.6);字段/语义与此一致。
|
||
|
||
### 3.3 完整 42 节点表(按 §2.3 节点定义表落地)
|
||
|
||
> 产物 `ui_menu_tab.c`(§3.6 生成):`const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT]`,内容与下表一致。
|
||
|
||
```c
|
||
const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {
|
||
/* parent name 下标 */
|
||
{ MENU_ROOT, "主界面" }, /* 0 L0 菜单(根) */
|
||
{ 0, "装置信息" }, /* 1 L1 叶子 */
|
||
{ 0, "实时数据" }, /* 2 L1 菜单 */
|
||
{ 2, "交流量" }, /* 3 L2 叶子 */
|
||
{ 2, "直流量" }, /* 4 L2 叶子 */
|
||
{ 2, "遥信量" }, /* 5 L2 叶子 */
|
||
{ 0, "参数定值" }, /* 6 L1 菜单 */
|
||
{ 6, "整定" }, /* 7 L2 菜单 */
|
||
{ 7, "参数" }, /* 8 L3 叶子 */
|
||
{ 7, "定值" }, /* 9 L3 叶子 */
|
||
{ 7, "控制字" }, /* 10 L3 叶子 */
|
||
{ 7, "软压板" }, /* 11 L3 叶子 */
|
||
{ 6, "查看" }, /* 12 L2 菜单 */
|
||
{ 12, "参数" }, /* 13 L3 叶子 */
|
||
{ 12, "定值" }, /* 14 L3 叶子 */
|
||
{ 12, "控制字" }, /* 15 L3 叶子 */
|
||
{ 12, "软压板" }, /* 16 L3 叶子 */
|
||
{ 0, "三遥设置" }, /* 17 L1 菜单 */
|
||
{ 17, "遥测死区" }, /* 18 L2 叶子 */
|
||
{ 17, "遥测系数" }, /* 19 L2 叶子 */
|
||
{ 17, "遥信类型" }, /* 20 L2 叶子 */
|
||
{ 17, "遥信防抖" }, /* 21 L2 叶子 */
|
||
{ 17, "双点遥信" }, /* 22 L2 叶子 */
|
||
{ 0, "装置维护" }, /* 23 L1 菜单 */
|
||
{ 23, "时钟设置" }, /* 24 L2 叶子 */
|
||
{ 23, "强制复归" }, /* 25 L2 叶子 */
|
||
{ 23, "手动录波" }, /* 26 L2 叶子 */
|
||
{ 23, "清除记录" }, /* 27 L2 叶子 */
|
||
{ 0, "通讯参数" }, /* 28 L1 菜单 */
|
||
{ 28, "通讯设置" }, /* 29 L2 叶子 */
|
||
{ 28, "网口设置" }, /* 30 L2 叶子 */
|
||
{ 28, "SNTP设置" }, /* 31 L2 叶子 */
|
||
{ 0, "记录查询" }, /* 32 L1 菜单 */
|
||
{ 32, "SOE记录" }, /* 33 L2 叶子 */
|
||
{ 32, "事故记录" }, /* 34 L2 叶子 */
|
||
{ 32, "操作记录" }, /* 35 L2 叶子 */
|
||
{ 32, "保护告警" }, /* 36 L2 叶子 */
|
||
{ 32, "保护启动" }, /* 37 L2 叶子 */
|
||
{ 32, "遥控记录" }, /* 38 L2 叶子 */
|
||
{ 32, "自检记录" }, /* 39 L2 叶子 */
|
||
{ 32, "运行记录" }, /* 40 L2 叶子 */
|
||
{ 32, "运行报告" }, /* 41 L2 叶子 */
|
||
};
|
||
```
|
||
|
||
> `UI_MENU_TAB_COUNT`(=42)与 `MENU_ROOT` 在 `ui_menu_tab.h` 定义。
|
||
|
||
### 3.4 查询 API(逻辑层 / 显示层共用)
|
||
|
||
4 个查询 API 统一模块前缀 `ui_menu_tab_*`(与逻辑层 `menu_*` 函数名区分),参数 `uint8_t`(菜单上限 255 项),随表生成在 `ui_menu_tab.c`:
|
||
|
||
```c
|
||
/* ui_menu_tab.h(生成物;逻辑层与显示层共用) */
|
||
uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max); /* 取 node 直接子项(按表序),最多 max 个;返回实际个数 */
|
||
bool ui_menu_tab_has_children(uint8_t node); /* 有子项=菜单(带▶);无=叶子 */
|
||
const char *ui_menu_tab_name(uint8_t node); /* 显示名 */
|
||
bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效 */
|
||
```
|
||
|
||
**各层用哪几个**(谁问数据层什么问题):
|
||
|
||
| API | 逻辑层(键分发) | 显示层(建框渲染) |
|
||
|---|---|---|
|
||
| `children` | ✓ 取子项/子项数(判空框、取选中子节点) | ✓ 取本框条目列表 |
|
||
| `has_children` | — | ✓ 判行尾有无 ▶ |
|
||
| `name` | — | ✓ 行文本 |
|
||
| `valid` | ✓ 初始化自检(§1.5 第 1 步) | — |
|
||
|
||
**建框 = 对"当前层节点"做一次过滤**(条目**现算不落地**,见 §7.3):
|
||
|
||
```c
|
||
/* 显示层 ui_menu_view_frame_build(depth, node, sel, scroll) 内: */
|
||
uint8_t kids[MENU_ITEM_MAX];
|
||
uint8_t cnt = ui_menu_tab_children(node, kids, MENU_ITEM_MAX); /* node = s_path[depth] */
|
||
for (uint8_t i = 0; i < cnt; i++) {
|
||
/* 注册一行:s_row_node[depth][i] = kids[i](框体自绘时现查名称/箭头) */
|
||
}
|
||
```
|
||
|
||
> 导航/显示一律按 `parent == 节点` 过滤,**改名/换序/增删子项均不影响导航逻辑**。
|
||
|
||
### 3.5 启动自检:`ui_menu_tab_valid()`
|
||
|
||
> 实现随表生成在 `ui_menu_tab.c`:校验**非根节点 parent 下标均 < `UI_MENU_TAB_COUNT`**(捕获表内笔误/生成器缺陷)。
|
||
> 由 `ui_menu_init()` 首行调用(§1.5 第 1 步);失败 → 打 `DBG_ERROR("menu table: INVALID…")` 并放弃建菜单(返回 -1)。
|
||
> 校验取 parent 界校验即够——生成器保证 parent 恒为"上级节点下标"(结构上无环),界校验足以兜底。
|
||
|
||
### 3.6 声明式生成器:`ui_menu_tree.txt` → `ui_menu_tab.c/.h`(与字体生成 `ui_cn_chars.txt → make_font.py` 同模式)
|
||
|
||
菜单层级声明在**缩进即层级**的文本源 `ui_menu_tree.txt`(仓库根,每级 2 空格、`#` 注释),
|
||
脚本读源、推导每个节点 `parent` 下标、产出 C 表。**改菜单只编辑文本源,重跑脚本即可**,导航/显示代码零改动。
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `ui_menu_tree.txt` | **人工编辑源**(缩进即层级);改菜单改这里 |
|
||
| `tools/menu_gen/gen_menu.py` | 生成器(同 `make_font.py` 模式):解析缩进→推导 parent→产出 C 表;含根唯一/缩进对齐/层级上限自检 |
|
||
| `Drivers/BSP/UI/ui_menu_tab.h` | 产物:`ui_menu_node_t` 定义 + `MENU_ROOT` + `UI_MENU_TAB_COUNT` + `extern g_menu_tab[]` |
|
||
| `Drivers/BSP/UI/ui_menu_tab.c` | 产物:`g_menu_tab[UI_MENU_TAB_COUNT]` 完整 42 节点表 |
|
||
|
||
重新生成:
|
||
```
|
||
python tools/menu_gen/gen_menu.py
|
||
```
|
||
|
||
> 产物 `ui_menu_tab.c/.h` 含 42 节点(9 菜单 + 33 叶子,最深 L3);`ui_menu.c` 不持有本地表,导航/显示一律走 `ui_menu_tab_*` 查询 API(§3.4)。
|
||
|
||
> **编码陷阱(已踩)**:产物含中文**字符串字面量**,必须 **UTF-8 带 BOM**(同 `ui_menu.c`)。
|
||
> 无 BOM 时 cl.exe(代码页 936/GBK)会按 GBK 误读 UTF-8 字节 → 字符串解析错乱(C2001/C4819)。`gen_menu.py` 已用 `utf-8-sig` 输出。
|
||
|
||
### 3.7 改菜单流程
|
||
|
||
```text
|
||
1. 编辑 ui_menu_tree.txt —— 缩进即层级(每级 2 空格),# 为注释;增/删/改名/换序条目都只改这里;
|
||
行尾可选 ` #P` = 该叶子需密码(密码页按所输密码授权查看/修改权限,
|
||
密码值在 Drivers/BSP/UI/ui_pwd_def.h 编译期配置)
|
||
2. python tools/menu_gen/gen_menu.py —— 重跑生成器:表 / UI_MENU_TAB_COUNT / 8 位护栏 全部同步更新
|
||
3. 完成:导航(逻辑层)与渲染(显示层)代码零改动
|
||
```
|
||
|
||
> 注意:新增**中文汉字**时,须同步加入字体字符集 `ui_cn_chars.txt` 并重生成字体(见 `UI_中文字体生成与配置.md`),否则显示成方框。
|
||
|
||
### 3.8 Flash 占用(const,只读)
|
||
|
||
| 符号 | 定义处 | 内容 | 占用 |
|
||
|---|---|---|---|
|
||
| `ui_menu_node_t` | `ui_menu_tab.h` | `{ uint8_t parent; uint8_t pwd; const char *name; }`(32 位 8 B/项) | 类型定义 |
|
||
| `g_menu_tab[UI_MENU_TAB_COUNT]` | `ui_menu_tab.c`(生成) | 42 节点扁平父子表 | ≈ 42×8 B ≈ 336 B |
|
||
| 名字字面量 | 同上 | 42 个 `const char[]` | ≈ 400 B |
|
||
| `UI_MENU_TAB_COUNT` / `MENU_ROOT` | `ui_menu_tab.h` | 节点数 42 / 根哨兵 0xFF(8 位,上限 255 项) | 宏 |
|
||
|
||
> 逻辑层/显示层**只读**它(§3.4 的 API 都是对它的过滤/查询),从不修改。合计 ≈ **0.7 KB Flash**。
|
||
|
||
---
|
||
|
||
## 四、显示规范:横向级联子框模型
|
||
|
||
> **本章是显示层(§五)要实现的"图纸"**:讲"页"(每级的子项列表)在**屏上怎么摆**——每级一个**框**,贴靠在其**父框右侧**——一棵**横向的树**(往右生长)。
|
||
> 以下规则由参考图 02~07(`docs/UI设计/mockups/`)逐像素实测得出(设备 px,误差 ≤3px);参考图为**手工 mockup**,与实现的偏差见 §8.2 注 5。
|
||
|
||
### 4.1 总览
|
||
|
||

|
||
|
||
- 每级一个**框**,框 = 该级条目列表(紧凑列表,**无逐框页头/返回按钮**,返回靠 `←`);
|
||
- **无滑动动画**(1bpp 推流);
|
||
- 屏上几框 = 深度 × 各框宽 决定(不固定)。
|
||
|
||
### 4.2 水平布局(X):右贴靠 + 自适应宽
|
||
|
||
- 子框**左缘 = 父框右缘**:共享边框列、零间隙;逐级向右生长(实测:各截图子框均恰好贴在父框右缘 x≈67/106 处)。
|
||
- 框宽 = 该框最宽条目(文字 + ▶ + 边距 + 边框),按内容自适应(公式见 §5.3)。参考图实测:4字+箭头 ≈68、4字无箭头 ≈60、3字 ≈46、2字+箭头 ≈40;**实现实测**(模拟器像素统计):66 / 54 / 46 / 42,另有空终点框 6(偏差 2/8/0/2 px)。
|
||
- 最深框**不钉屏幕右缘**(实测:实时数据子框止于 x≈113/160;三遥/装置维护/记录查询子框止于 x≈127/160)。
|
||
- 本菜单最深路径宽 = 实现实测 66+42+42+6−3(共享边框)= **153 < 160** → 级联不会超屏宽;>160 参考图未覆盖,实际行为 = 框被全屏容器在屏缘裁切(右缘截断),无特殊处理。
|
||
|
||
### 4.3 垂直布局(Y):每框独立(顶对齐 + 底对齐上移)
|
||
|
||
- 默认:顶边 = 父框"选中行"**行顶**(实时数据/整定两例实测 Δ≤2px);框高 = 条目数 × 行高(**按内容**,不强顶屏底)。
|
||
- (顶边 + 框高) 超出屏底 → 整框上移使**子框底边对齐父"选中行"行底**(不是贴屏底):参考图实测 三遥设置 top=26 / 装置维护 top=54 / 通讯参数 top=82 / 记录查询 top=12;**实现实测 28 / 58 / 88 / 13**(Δ=2/4/6/1 px——四个样本不满足同一公式,实现取"底边对齐选中行行底"这一自洽规则,两端极例(三遥/记录查询)精确吻合,见 §8.2 注 5)。
|
||
- 上移后框可叠到屏顶状态行/装饰区(记录查询子框 top≈12)——**顶部不钳制**。
|
||
- 比整屏还高 → `↑`/`↓` 滚(初始对齐"选中行")。
|
||
|
||
### 4.4 选中态与焦点
|
||
|
||
- **选中态**:整行反色(白带、黑字);菜单行有 **▶**,叶子行无;**选中行保留 ▶**(箭头随反色变黑,参考图 02/04 实测);非选中行黑底白字(白箭头)。
|
||
- **焦点** = 最右(最深)框;框无页头/返回按钮(框即紧凑列表,返回靠 `←`);仅根框带主界面装饰(顶部"浅 V" + 状态行"当前模式 : 无模式")。
|
||
|
||
---
|
||
|
||
## 五、显示层:ui_menu_view(持有全部 LVGL 对象)
|
||
|
||
> **一句话职责**:按"状态 + 数据表"渲染级联框——持有**全部** LVGL 对象(样式 / 持久容器 / 框体;根框装饰与状态行由容器自绘、无对象),由逻辑层经 8 个接口(§5.5)驱动建/毁/刷新。行不是对象,由框体自绘。
|
||
> **绝不做**:持有导航状态(17 B 在逻辑层);解释按键(输入回调挂在逻辑层)。
|
||
|
||
### 5.1 职责边界
|
||
|
||
- 持有全部 LVGL 对象(样式/持久容器/框体);根框装饰与状态行由容器自绘(无对象)
|
||
- 按 `frame_build/destroy/refresh` 调用建/毁/刷新(依据"状态 + 数据表")
|
||
- 现算框几何(§5.3 公式)并按层缓存(`s_frame[]`)
|
||
- 提供滚动位读写(`set_scroll` / `get_scroll`)供逻辑层持久化
|
||
|
||
### 5.2 布局常量(设备 px,定义于 `ui_menu_view.c`)
|
||
|
||
| 常量 | 值 | 含义 |
|
||
|---|---|---|
|
||
| `MENU_ROW_H` | 15 | 行高(字体行高 13 + 上下各 1) |
|
||
| `MENU_ROW_TXT_TOP` | 1 | 行内文字垂直偏移(居中) |
|
||
| `MENU_ROW_PAD_L` / `MENU_ROW_PAD_R` | 2 / 2 | 行内左/右留白(紧凑,保证级联总宽 <160) |
|
||
| `MENU_ARROW_GAP` | 2 | 行内 文本→箭头 间距 |
|
||
| `MENU_FRAME_BORDER` | 1 | 框边框宽(子框与父框共享该边框列) |
|
||
| `MENU_ROOT_BOX_X` / `MENU_ROOT_BOX_Y` | 0 / 44 | 根框左上(左对齐、贴左缘) |
|
||
| `MENU_ROOT_STATUS_X` / `MENU_ROOT_STATUS_Y` | 17 / 20 | 状态行"当前模式 : 无模式" |
|
||
| `MENU_DECO_LEG_W` / `MENU_DECO_HORIZ_W` | 3 / 2 | 顶部"浅 V"腿线宽 / 水平线宽(1bpp 斜线边缘易被阈值吃掉,腿须比水平线粗) |
|
||
|
||
字体度量(`ui_font_cn_12`,宋体 12px 1bpp):汉字步进 12 px/字、▶ 步进 10 px(adv_w=165/16)、行高 13 px(见 `UI_中文字体生成与配置.md`)。
|
||
|
||
### 5.3 几何计算(`ui_menu_view_frame_build()`)
|
||
|
||
`frame_build` 拿到 (depth, node, sel, scroll) 后,先**算三个数**——框宽 w、位置 (x, y)、框高 h——再照数建框体、注册行数据、应用选中/滚动。四步各配一格图(下图 ①~④):
|
||
|
||

|
||
|
||
**① 框宽 w = 最宽行 + 2 条边框**(图①)
|
||
|
||
一行横向构成:`边框1 | 留白2 | 文本 |(菜单行: 间距2 + ▶10)| 留白2 | 边框1`。文本宽按字体实际步进量(`lv_text_get_width`,⚠️ 见 §8.2 陷阱 1);同框各行宽度可以不同,**框宽只由最宽的一行决定**,窄行左对齐、右侧留空:
|
||
|
||
w = max_行( PAD_L + 文本_w + [GAP + ▶_w] + PAD_R ) + 2×BORDER
|
||
|
||
实测五类框宽(1× 原尺寸,px):
|
||
|
||
| 框 | 最宽行构成 | 框宽 |
|
||
|---|---|---|
|
||
| 根框(7 行,4 字 + ▶) | 2 + 48 + 2 + 10 + 2 | **66** |
|
||
| 4 字无箭头框 | 2 + 48 + 2 | 54 |
|
||
| 3 字无箭头框 | 2 + 36 + 2 | 46 |
|
||
| 2 字 + ▶ 框 | 2 + 24 + 2 + 10 + 2 | 42 |
|
||
| 空终点框(无行) | 仅留白 + 边框 | 6 |
|
||
|
||
**② 子框 X:贴在父框右缘,共享 1px 边框列**(图②)
|
||
|
||
x = 父.x + 父.w − 1
|
||
|
||
减掉的 1 = 父框右边框那列像素:子框的左边框与父框的右边框**是同一列**(边框重叠 1px),所以零间隙、不重画,逐级向右生长。
|
||
|
||
**③ 子框 Y(默认):顶边对齐父框"选中行"行顶**(图③)
|
||
|
||
y = 父.y + BORDER + 父sel × ROW_H
|
||
|
||
父框选中哪一行,子框顶边就落在那一行的**行顶**——父子垂直方向"肩并肩",选中行不动子框就不动。
|
||
|
||
**④ 子框 Y(溢出时):整框上移,底边对齐选中行行底**(图④)
|
||
|
||
y + h > 160 → y = 选中行行顶 + ROW_H − h
|
||
|
||
子框比"选中行以下到屏底"的空间高 → 整框上移,底边卡在**选中行行底**(不是贴屏底——贴屏底会让父子脱钩,选中行一移子框位置就乱跳)。上移后仍顶出屏顶(y<0)→ 钳到 0;再不够 → 框高截到可见区、行在框内滚动(§5.4 `LV_SCROLLBAR_MODE_AUTO`)。
|
||
|
||
**框高 h = 按内容,不强顶屏底**
|
||
|
||
h = 行数 × ROW_H + 2×BORDER
|
||
|
||
行数少框就矮(2 行 = 32 px)、行数多框就高;上限受屏高 160 约束(触发④的上移/截断)。
|
||
|
||
**公式汇总**(`ui_menu_view_frame_build()` 的实现即此五式):
|
||
|
||
```text
|
||
w = max_行(PAD_L + 文本_w + [菜单: GAP + ▶_w] + PAD_R) + 2×BORDER (文本宽经 lv_text_get_width)
|
||
h = 行数 × ROW_H + 2×BORDER
|
||
子框 x = 父.x + 父.w − 1 (d>0,共享边框列)
|
||
子框 y = 父.y + BORDER + 父sel×ROW_H (d>0,顶对齐选中行行顶)
|
||
y + h > 160 → y = 选中行行顶 + ROW_H − h;y < 0 → y = 0(截断/框内滚动兜底)
|
||
```
|
||
|
||
### 5.4 对象模型
|
||
|
||
- 屏幕根容器(全屏 flat)→ **单一持久全屏容器** `s_cont`(各层框均建其内;`LV_OBJ_FLAG_IGNORE_LAYOUT` + 绝对定位,透明,永不删;级联框须同屏叠加)
|
||
- 根框体 `s_frame_box[0]` 持久(位置 (0,44)、宽自适应);子框体进入时建 / 返回时毁(`lv_obj_create(s_cont)` + flat + 边框样式 + 自绘回调,`IGNORE_LAYOUT` 绝对定位)
|
||
- 框体:自动滚动条(`LV_SCROLLBAR_MODE_AUTO`);行自绘(无子对象),行内容超框高才框内滚动(内容高经 `LV_EVENT_GET_SELF_SIZE` 虚报)
|
||
- 行 = **数据**(节点下标注册 `s_row_node[d][i]`,无对象);框体 `LV_EVENT_DRAW_MAIN` 自绘:选中行 = 整行白带(`lv_draw_rect`),每行 = `lv_draw_label` 文本(左留白 2)+ 箭头(紧跟文本、间距 2;叶子无箭头),静态字符串(Flash 表、不拷贝);按滚动偏移枚举可见行、重绘区外的行跳过(culling)
|
||
- 选中行 = 整行反色(白带、黑字),**箭头随反色变黑**(§4.4);非选中行黑底白字(白箭头)
|
||
- 根框装饰(浅 V + 状态行):容器 `DRAW_MAIN` 自绘(3 段线 `lv_draw_line` + 1 行文本 `lv_draw_label`,无对象;两腿 3px、水平线 2px)
|
||
- 空终点框:叶子 ENTER 后建 w=6、h=2 的占位框(无行),仅 ESC 可返回
|
||
|
||
### 5.5 公开 API
|
||
|
||
| 接口 | 作用 | 调用方 |
|
||
|---|---|---|
|
||
| `int ui_menu_view_init(void)` | 样式 + 持久容器(装饰/状态行自绘回调)+ 根框框体(含自绘回调)(一次性) | 逻辑层 `ui_menu_init` |
|
||
| `lv_obj_t *ui_menu_view_container(void)` | 取持久容器(焦点/键唯一入口;逻辑层在其上绑输入组/键回调) | 逻辑层 |
|
||
| `void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t scroll)` | 建该深度框:现算几何 + 注册行数据 + 应用选中/滚动(d=0 仅(重)注册行) | 逻辑层(进 / 初始化) |
|
||
| `void ui_menu_view_frame_destroy(uint8_t depth)` | 毁该深度框(框体连自绘回调整体删除、行数据清空;容器保留;根框 d=0 不可毁) | 逻辑层(退) |
|
||
| `void ui_menu_view_frame_refresh(uint8_t depth, uint8_t sel)` | 刷选中(选中数据 + 跟随滚动 + 重绘框体,不重建框) | 逻辑层(移选后) |
|
||
| `void ui_menu_view_set_scroll(uint8_t depth, uint16_t scroll)` | 应用滚动偏移(行数据重注册后按此还原) | 逻辑层 |
|
||
| `uint16_t ui_menu_view_get_scroll(uint8_t depth)` | 读当前滚动偏移(供逻辑层回读持久化) | 逻辑层 |
|
||
| `void ui_menu_view_focus(void)` | 焦点置持久容器(唯一键入口;框深度由 `s_depth` 决定) | 逻辑层(进/退) |
|
||
|
||
> 公共常量:`MENU_PAGE_MAX 4`(深度上限 = 根框 + 3 级子框)、`MENU_ITEM_MAX 16`(单框最大条目数)、`MENU_SCREEN_W/H 160`(与 `ui_port.h` / 模拟器一致)。
|
||
|
||
### 5.6 RAM 字段(显示层)
|
||
|
||
| 字段 | 类型 | 大小 | 用途 |
|
||
|---|---|---|---|
|
||
| `s_cont` | `lv_obj_t*` | 4 B | 单一全屏持久容器(焦点/键唯一入口;各层框建其内);透明常驻、**永不删**(防焦点对象悬空) |
|
||
| `s_row_node[4][16]` | `uint8_t` | 64 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` 重算,**不属于导航状态** |
|
||
|
||
> **只给"屏上的框"注册行数据**(每框 ≤16 行);返回时清掉被关闭最右框的行数据 → 行自绘无对象,LVGL 池只占**屏上框体**(2~3 框),关闭即释放。
|
||
|
||
---
|
||
|
||
## 六、逻辑层:ui_menu(纯状态机)
|
||
|
||
> **一句话职责**:持有**唯一** 17 B 导航状态(路径/选中/深度/滚动)+ 键语义(移/进/退),单向驱动显示层建/毁/刷新级联框。
|
||
> **绝不做**:持 LVGL 对象句柄(唯一例外:输入接线 2 调 `lv_group_add_obj`/`lv_obj_add_event_cb`);渲染。
|
||
|
||
### 6.1 职责边界
|
||
|
||
- 持有 17 B 导航状态(唯一持有者,§6.2)
|
||
- 解释按键:移(↑↓)/ 进(→/ENTER)/ 退(←/ESC)(§6.4)
|
||
- 查询数据层(`children`/`valid`)判键行为(§3.4)
|
||
- 单向驱动显示层:`frame_build/destroy/refresh/focus` + 滚动位回读持久化
|
||
|
||
### 6.2 17 B 导航状态(唯一"必须持久"的状态)
|
||
|
||
| 字段 | 类型 | 大小 | 用途 |
|
||
|---|---|---|---|
|
||
| `s_path[4]` | `uint8_t`×4 | 4 B | **路径**:`s_path[d]`=第 d 层"展示的节点下标"(`s_path[0]=0` 主界面);即"从根到当前(最右)框"的节点链 |
|
||
| `s_sel[4]` | `uint8_t`×4 | 4 B | **各层选中下标**:返回时还原该层高亮(选中不跳) |
|
||
| `s_depth` | `uint8_t` | 1 B | **当前深度**(0=根框)= 焦点所在(最右)框 |
|
||
| `s_scroll[4]` | `uint16_t`×4 | 8 B | **每框滚动偏移**(px):该框销毁再进入、行重建时按此还原位置 |
|
||
| **合计** | | **17 B** | 其余(条目列表/名字/菜单叶子/框几何)全部**现算**,不落 RAM(见 §7.3) |
|
||
|
||
### 6.3 导航规则:数据如何驱动"移 / 进 / 退"
|
||
|
||
> **一句话**:导航**从不遍历树**,它只反复向 `g_menu_tab` 问一个问题——
|
||
> **"谁的 parent 等于当前节点?"** 答案(按表序)就是当前页要显示的条目。
|
||
> **进** = 把选中的子项记为新的"当前节点"再问一遍;**退** = 忘掉它、还原上一个"当前节点"。
|
||
> 表是**扁平**的(无嵌套指针),"树"只是"反复按 parent 过滤"营造出来的错觉。
|
||
|
||
**当前页 = 对表的一次过滤**(页面**按需构建**——进入时建、返回时毁,内存 = 当前导航路径,见 §7.2):
|
||
|
||
```text
|
||
显示"节点 N 的页面" ⇔ 取 g_menu_tab 中所有 parent == N 的行(按表序 = 显示序)
|
||
```
|
||
|
||
| 你所在的页 | 当前节点 N | 执行的查询 | 页面上出现的条目 |
|
||
|---|---|---|---|
|
||
| 主界面(根页) | 0(主界面) | `parent == 0` | 装置信息、实时数据▶、参数定值▶、三遥设置▶、装置维护▶、通讯参数▶、记录查询▶(**7 项**)|
|
||
| 参数定值页 | 6(参数定值) | `parent == 6` | 整定▶、查看▶(**2 项**)|
|
||
| 整定页 | 7(整定) | `parent == 7` | 参数、定值、控制字、软压板(**4 项,全叶子**)|
|
||
|
||
> 行尾有没有 **▶** 也由同一张表判:`parent == 该节点` **非空** → 菜单(有 ▶);**为空** → 叶子(无 ▶)。
|
||
|
||
**移(↑ / ↓)**:只改当前页的选中下标 `s_sel[d]`(**首尾环绕**),**不碰表**。
|
||
|
||
**进(ENTER / →)**:选中条目 = 子节点 **M**(`kids[s_sel[d]]`)。向表问一句"**M 还有孩子吗?**"(`parent == M` 非空?)
|
||
- **有** → M 是菜单:**压一层**,`s_path[d+1]=M`,再按"过滤"显示 M 的孩子。
|
||
- **没有** → M 是叶子:进**终点**(空终点框占位,本阶段不展开内容页),不再下钻。
|
||
|
||
**退(ESC / ←)**:**弹一层**——销毁最右框、回到 depth-1 的父框;父框选中项**保持**为"刚才那个 M 在父框里的下标"(= 按下 ENTER 前的位置,不跳)。
|
||
|
||
**完整走查**(`#` = `g_menu_tab` 下标):主界面 → 参数定值 → 整定 → 参数,再一级级退回
|
||
|
||
```text
|
||
【根页 node=0】 parent==0 → 7 项;选中 [2]=参数定值(#6)
|
||
ENTER ─▶ 问"6 有孩子吗"→ parent==6 非空(整定/查看) → 压层
|
||
【参数定值页 node=6】 parent==6 → [整定#7, 查看#12];选中 [0]=整定
|
||
ENTER ─▶ 问"7 有孩子吗"→ parent==7 非空(参数/定值/控制字/软压板) → 压层
|
||
【整定页 node=7】 parent==7 → [参数#8, 定值#9, 控制字#10, 软压板#11];选中 [0]=参数
|
||
ENTER ─▶ 问"8 有孩子吗"→ parent==8 为空 → 叶子 → 进终点(空框)
|
||
【参数终点 node=8】(占位,无条目,到此为止)
|
||
ESC ─▶ 弹层 → 回【整定页】,选中仍 [0]=参数
|
||
ESC ─▶ 弹层 → 回【参数定值页】,选中仍 [0]=整定
|
||
ESC ─▶ 弹层 → 回【根页】,选中仍 [2]=参数定值
|
||
```
|
||
|
||
**导航状态 = 一条"当前节点"链**(= 从根到当前框的路径)
|
||
|
||
```text
|
||
s_path[0]=0 ─▶ s_path[1]=6 ─▶ s_path[2]=7 ─▶ s_path[3]=8
|
||
根页 参数定值页 整定页 参数终点
|
||
depth=0 depth=1 depth=2 depth=3
|
||
```
|
||
|
||
- 链上**每个 node 都是上一个的孩子**(parent 关系)→ 这条链天然就是路径,**无需额外记录"我从哪来"**。
|
||
- **进 = 链尾追加**(选中的子节点接上去);**退 = 链尾弹掉**。
|
||
- 内存只占"当前这条链"(`s_path[0..s_depth]`)+ 屏上各框各一套行数据(屏外框行清空),退回即释放(§7.2/§7.5)。
|
||
|
||
**对应到代码**(逻辑层决定、显示层执行;三层见 §一):
|
||
|
||
| 逻辑 | 函数 | 读 / 写的数据 |
|
||
|---|---|---|
|
||
| 建框(显示某节点子项) | 显示层 `ui_menu_view_frame_build(d, node, sel, scroll)`(由逻辑层调用) | 读 `s_path[d]`+`g_menu_tab` 现算条目;算 x/顶边/框宽、注册 `s_row_node[d][*]`、恢复 `s_scroll[d]` |
|
||
| 进(压层 / 终点) | 逻辑层 `menu_open(child)` | 写 `s_depth+1`、`s_path[d+1]=M`、`s_sel[d+1]`(同节点还原 / 换节点清零);再 `frame_build` + `focus` |
|
||
| 退(弹层) | 逻辑层 `menu_back()` | `ui_menu_view_frame_destroy(s_depth)`;`s_depth-1`、焦点回父框(父框不动、无需重建,`s_sel`/`s_scroll` 原样保留) |
|
||
| 移选 | 逻辑层 `menu_key_handle(d, key)` → `menu_refresh_layer(d)` | 写 `s_sel[d]`(首尾环绕);`frame_refresh`(高亮 + 跟随滚动);滚动位回读入 `s_scroll[d]` |
|
||
|
||
### 6.4 键语义与分发
|
||
|
||
**键位语义**:
|
||
|
||
| 键 | 行为 |
|
||
|---|---|
|
||
| `↑` / `↓` | 当前(最右)框内上/下移动选中(**首尾环绕**);到边缘且该框可滚则**滚动该框** |
|
||
| `→` 或 `ENTER` | 选中**菜单节点** → 其右侧**弹出子框**;选中**叶子节点** → 进终点(空框) |
|
||
| `←` 或 `ESC` | 返回:**关最右框**(毁框体+行数据)、焦点回父框、选中态保持(不跳) |
|
||
| 焦点 / 框头 | 焦点 = 最右(最深)框;**无逐框页头/返回按钮**(框即紧凑列表,返回靠 `←`);仅根框带主界面装饰 |
|
||
|
||
**进入/返回示例**(路径,`→`/`←` = 进/退动作方向):
|
||
```
|
||
主界面 → 参数定值 → 整定 → 参数
|
||
L0 L1 L2 L3
|
||
返回:主界面 ← 参数定值 ← 整定 ← 参数
|
||
```
|
||
|
||
**分发**(`menu_key_handle(d, key)`,`d` = 焦点所在层):
|
||
|
||
```text
|
||
cnt = ui_menu_tab_children(s_path[d], …) ← 先问数据层:本框几个条目
|
||
UP/DOWN : cnt>0 → s_sel[d] 首尾环绕 → menu_refresh_layer(高亮+跟随滚动+滚动位回读)
|
||
ENTER/RIGHT: cnt>0 且 d+1<4 → menu_open(kids[s_sel[d]]) (叶子也压层 → 空终点框)
|
||
ESC/LEFT : d>0 → menu_back() (根框 d=0 不响应;空框只响应返回)
|
||
```
|
||
|
||
### 6.5 公开 API
|
||
|
||
- `int ui_menu_init(void)` —— **唯一公开入口**(uiTask 调用);五步时序见 §1.5,表自检见 §3.5。
|
||
- 内部(`static`):`menu_open` / `menu_back` / `menu_key_handle` / `menu_key_cb` / `menu_refresh_layer`(见 §6.3/§6.4)。
|
||
|
||
---
|
||
|
||
## 七、层间配合细节
|
||
|
||
### 7.1 按键全序列(逐键分层动作)
|
||
|
||
> 通用管线见 §1.4;本表按键列出**逻辑层改什么状态、显示层做什么**。
|
||
|
||
| 键 | 逻辑层(17 B 状态) | 显示层(画面) |
|
||
|---|---|---|
|
||
| **移** ↑/↓ | `s_sel[d] = (s_sel[d] ∓ 1 + cnt) % cnt`(首尾环绕);`cnt=0`(空框)不变 | `frame_refresh(d, s_sel[d])`:改选中数据 + 跟随滚动 + 重绘框体(不重建);随后逻辑层 `get_scroll(d)` 回读入 `s_scroll[d]` |
|
||
| **进** →/ENTER | `s_depth+1`;`s_path[d+1]=kids[s_sel[d]]`;`s_sel[d+1]`(同节点还原原选中/滚动,换节点清零) | `frame_build(d+1, …)`:现算几何(贴父框右缘、顶对齐/底对齐上移)→ 注册行数据 → 应用选中/滚动;`focus()`(焦点恒在持久容器);叶子 → 空终点框(w=6, h=2) |
|
||
| **退** ←/ESC | `s_depth−1`(根框 d=0 不响应) | `frame_destroy(d)`:毁框体(含自绘回调,容器保留,池内存即回落);`focus()`(焦点恒在持久容器);父框不动、无需重建,`s_sel`/`s_scroll` 原样保留 |
|
||
|
||
### 7.2 框生命周期(进建退毁)
|
||
|
||
- **进时建**(`frame_build`):现算几何(§5.3;父框几何取父层 `s_frame` 缓存)→ 注册行数据(行 = 子项,现算不落地)→ 应用选中高亮 + 滚动位。
|
||
- **退时毁**(`frame_destroy`):框体(含自绘回调)整体销毁、容器保留(永不删,防焦点对象悬空);LVGL 池内存随即回落。
|
||
- **根框持久**(`ui_menu_view_init` 建一次,永不毁);子框随进/退建/毁。
|
||
- **焦点**:持久容器为唯一焦点对象(初始化时入输入组并获焦,永不删、焦点不迁移);框深度由逻辑层 `s_depth` 决定,无焦点争抢。
|
||
- **空终点框**:叶子 ENTER 后建 w=6、h=2 占位框(无行),仅 ESC 可返回。
|
||
|
||
### 7.3 存 vs 算(关键区分)
|
||
|
||
> **核心原则:能推导的就不存**——某页的条目列表可由 `g_menu_tab` 的 `parent==节点` 随时重算;
|
||
> 故**只持久化"不可推导"的导航历史(路径 + 各层选中 + 各框滚动)**,页面内容进出时按需重建。
|
||
|
||
| 数据 | 存 / 算 | 依据 |
|
||
|---|---|---|
|
||
| 路径(在哪些层) | **存** `s_path`(逻辑层) | 用户导航历史,不可推导 |
|
||
| 各层选中 | **存** `s_sel`(逻辑层) | 用户选择,不可推导 |
|
||
| 每框滚动位 | **存** `s_scroll`(逻辑层) | 用户 `↑`/`↓` 滚动,不可推导 |
|
||
| 某框条目列表 | **算** | `ui_menu_tab_children(s_path[d])` = 过滤 `parent==s_path[d]` |
|
||
| 条目是菜单/叶子(有无 ▶) | **算** | `ui_menu_tab_has_children(node)` = 存不存在 `parent==node` |
|
||
| 条目显示名 | **算** | `g_menu_tab[node].name` |
|
||
| 每框 x / 顶边 / 框宽 | **算** | 右贴靠布局 + 顶对齐/底对齐上移(§四),现算(按层缓存在 `s_frame[]`) |
|
||
|
||
> **结论:持久导航状态 = `s_path` + `s_sel` + `s_depth` + `s_scroll`(共 17 B,全在逻辑层)**;
|
||
> 框的条目/名字/菜单叶子/位置(框宽·x·顶边) 100% 现算,进/退时重建。
|
||
|
||
### 7.4 导航操作 × 字段读写
|
||
|
||
| 操作 | 读 | 写 |
|
||
|---|---|---|
|
||
| **建框**(进 / 初始化) | `s_path[d]`、`g_menu_tab`(现算条目)、`s_sel[d]`、`s_scroll[d]` | 算该框 x/顶边/框宽(§四);注册 `s_row_node[d][*]`;恢复滚动位 |
|
||
| **移**(↑/↓) | `s_sel[d]`、`s_scroll[d]`、`s_row_node[d][*]` | `s_sel[d]`(首尾环绕);到边缘则改 `s_scroll[d]`(滚);刷新高亮 |
|
||
| **进**(ENTER/→,菜单) | `s_sel[d]`→子节点 M、`ui_menu_tab_has_children(M)`=真 | `s_depth+1`、`s_path[d+1]=M`、`s_sel[d+1]=0`;新框贴父框右缘生成(既有框不动、不重排) |
|
||
| **进**(ENTER/→,叶子) | `ui_menu_tab_has_children(M)`=假 | `s_depth+1`、`s_path[d+1]=M`(终点框) |
|
||
| **退**(ESC/←) | `s_depth` | `s_depth-1`、销毁最右框行、焦点回父框(父框不动、无需重建,`s_sel[d-1]`/`s_scroll[d-1]` 原样保留) |
|
||
|
||
### 7.5 内存预算
|
||
|
||
**Flash(const,数据层)**:`g_menu_tab` 336 B + 名字 ~400 B ≈ **~0.7 KB**
|
||
|
||
**RAM(静态/常驻)**:
|
||
|
||
| 组 | 层 | 大小 |
|
||
|---|---|---|
|
||
| 导航历史 `s_path`+`s_sel`+`s_depth` | 逻辑层 | 9 B |
|
||
| 每框滚动 `s_scroll[4]` | 逻辑层 | 8 B |
|
||
| 容器 `s_cont` | 显示层 | 4 B |
|
||
| 行数据 `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_frame/scrollbar`(动态)+ flat 常量样式(Flash,零池占用 §9.5) | 显示层 | ~200 B |
|
||
| **小计** | | **~0.35 KB**(逻辑层 17 B + 显示层 ~0.33 KB) |
|
||
|
||
**LVGL 池(动态,`LV_MEM` 16 KB 预算内)**:行自绘无对象,池只占**屏上框体**(2~3 个 `lv_obj`)+ 根框装饰/状态行,
|
||
典型 ≈ **1~2 KB**,仍远小于 16 KB(池已于 2026-10-15 由 40 KB 缩到 16 KB,见 `docs/内存管理/RAM使用分析与优化方案.md` §1.1;真机开机实测 `menu build done: used=3192`)。
|
||
|
||
---
|
||
|
||
## 八、验证与实施陷阱
|
||
|
||
### 8.1 分级验证
|
||
|
||
| 环节 | 方法 |
|
||
|---|---|
|
||
| 模拟器无头 | `lvgl_sim.exe --screenshot shot.bin` / `--keys <seq> shot.bin`;I1 原始像素(160×160 = 3200 B)转 PNG(PIL)目检 |
|
||
| 对照参考图 | 7 状态(根页 + 6 个一级子框)逐一对照参考图 01~07;反色行/框几何用像素统计核验(偏差见 §8.2 注 5) |
|
||
| 编译 | `@build`(ARMCC,0 错 0 警,强制) |
|
||
| 真机 | `@test`(Required 含 `UI port init: OK;UI menu init: OK`;屏幕内容仍需人工目检) |
|
||
|
||
### 8.2 陷阱与保真度说明
|
||
|
||
1. 本 LVGL v9.3 的 `lv_text_get_width(txt, length, font, letter_space)` **length=0 时返回 0**(不是"整串");必须传 `strlen`——否则框宽计算全 0(根框退化 8 px)。
|
||
2. 无头模式**不得**调 `lv_timer_handler()` / `lv_refr_now()`:I1 + `LV_DISPLAY_RENDER_MODE_FULL` 全屏刷新会进渲染死循环(截图因此走 `lv_snapshot`);`--keys` 注键是同步处理,无需驱动帧。
|
||
3. 选中行**保留 ▶**(随反色变黑):参考图 02/04 原图即"选中行箭头仍在、仅随反色变黑",勿实现成"选中隐箭头"(§4.4)。
|
||
4. 字体缺字形 = 方框:菜单树新增/改名的汉字必须同步加入字符集 `ui_cn_chars.txt`(当前 122 唯一字)并重生成字体(重跑 `extract_cn.py` + `make_font.py`),否则新字显示为方框(流程见 `UI_中文字体生成与配置.md`)。
|
||
5. **参考图保真度**:参考图为手工 mockup,4 个样本(子框顶位置)不满足单一公式;实现采用自洽规则(顶对齐/底对齐上移),实测与参考图偏差:顶位置 2/4/6/1 px(三遥/装置维护/通讯参数/记录查询),框宽 2/8/0/2 px(4字+箭头/4字/3字/2字+箭头)。两端极例(三遥、记录查询)精确吻合;统一规则优于逐屏特调,偏差范围内不影响识读与操作。
|
||
6. 装饰"浅 V"两腿线宽必须 3(水平线 2):1bpp 下细斜线边缘被阈值吃掉,腿不粗于水平线即消失;且依赖 `LV_DRAW_SW_COMPLEX=1`(见 `UI_设计架构.md` §1.4 约束 7)。
|
||
|
||
---
|
||
|
||
## 九、内容页(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)
|
||
- **用户密钥页**(表单/操作页,无行槽):title_y=44、"浅 V"/页脚关(hide_deco/hide_footer)、key_intercept=1;自绘 3 个 14×14 数字框(间距 6px、行顶 y=78、首框左 x=53)+ 反显光标,详见 `内容页架构.md` §4.1
|
||
- 列表页窗口 = 选中行所在页(首可见行 = `(sel/每页数)×每页数`),行槽位置固定、数据随页切换
|
||
- 数据表为 Flash `const cp_row_t[]`(名+值字面量,不 strdup);交流量第 2/3 页当前为占位数据
|
||
|
||
### 9.3 键语义
|
||
|
||
| 键 | 行为 |
|
||
|---|---|
|
||
| ↑ / ↓ | 列表页移选(**首尾环绕、跨页连续**),窗口随页切换后重绘;信息页不响应 |
|
||
| ESC | 关页返回菜单(导航状态原样还原;恒不可拦截) |
|
||
| ← | `key_intercept=1` 时先交页面 key_cb(true = 已处理不关页,如密码页移光标);未置标志或未处理 → 照旧关页返回 |
|
||
|
||
### 9.4 公开 API
|
||
|
||
```c
|
||
bool ui_content_page_open(content_page_id_t id); /* 开页:建屏+切屏+焦点置页面容器;ID 无效/已有页打开 → false */
|
||
void ui_content_page_close(void); /* 关页:焦点回菜单容器 + 切回菜单屏 + 销毁页面 */
|
||
void ui_content_page_invalidate(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`;其余叶子无内容页(进空白终点框)。需密码叶子(行尾 ` #P`,如 node 8 参数;密码检查先于内容页判断,带内容页的需密码叶子同样先经密码页)先 arm + 开 `CONTENT_PAGE_PASSWORD`,确认一致后回调(携授予权限) **先关页再续做进入动作**(有内容页→开内容页携权限,页面经 `ui_content_page_is_editable()` 判只读/可编辑);无密码叶子直接开(默认只读)
|
||
|
||
### 9.5 内存预算(模拟器 x64 实测,16 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)。
|
||
|
||
---
|
||
|
||
## 十、变更记录
|
||
|
||
| 日期 | 变更 |
|
||
|---|---|
|
||
| 2026-09-03 | 初始版本:依据用户 7 屏主菜单截图定义完整 4 级逻辑层级(42 节点)+ 导航逻辑 + 与现有 `g_menu_tab` 差异对照 |
|
||
| 2026-09-03 | 按范围调整:移除"内容页类型"相关内容,聚焦菜单层级(节点/父子/层级/导航);新增 `子项数` 列;标注"查看"子项为推测待确认 |
|
||
| 2026-09-03 | **查看(#12)子项经用户确认**与"整定"同构(参数/定值/控制字/软压板),由推测转为正式定义,移除全部 ⚠ 标记与待确认节 |
|
||
| 2026-09-03 | 新增 §六"菜单树数据结构":选型扁平"父子索引表"(邻接表/关系表模型,对比指针树/首子-兄弟/堆式),给出 `ui_menu_node_t` + 完整 42 节点表 + 导航 API(`menu_get_children`/`menu_has_children`/`menu_get_name`)+ 启动自检 + 声明式生成器 + 与现有 `g_menu_tab` 迁移映射 |
|
||
| 2026-09-03 | **声明式生成器落地**(§6.6 由"可选"转"已实现"):新增 `ui_menu_tree.txt`(缩进即层级源)+ `tools/menu_gen/gen_menu.py`(同 `make_font.py` 模式),生成 `Drivers/BSP/UI/ui_menu_tab.c/.h`(42 节点:9 菜单 + 33 叶子,最深 L3);已跑通并校验 parent 下标/连通性/子项数全部正确 |
|
||
| 2026-09-04 | §四 新增"**导航走查**":用真实 `g_menu_tab` 下标逐步演示"移/进/退"如何由 `parent==当前节点` 一次过滤驱动(当前页=过滤、进=压页/终点、退=弹页、导航态=当前节点链),并修正根页模型为"主界面 node 0 的子项"(§6.2/6.7 同步,去掉"虚拟根页"表述) |
|
||
| 2026-09-04 | 新增 §七"导航运行时数据结构(完整清单)":静态层(`g_menu_tab` 只读)+ 运行时层(导航历史 `s_path`+`s_sel`+`s_depth`=13 B、持久容器 `s_cont[4]`、单套活跃行 `s_row*[]`、页头/装饰、样式);"存 vs 算"区分、操作×字段读写表、内存预算、与现有 `s_pages[4]` 收敛对照 |
|
||
| 2026-09-04 | 改名 `menu_is_menu` → `menu_has_children`(原名"menu 是不是 menu"含糊;新名=该节点有无子项);§四 路径/代码映射由旧 `s_pages[]` 统一为 §七 的 `s_path[]`/`s_depth`/`s_sel[]`/`s_row*[]` 模型,消除 §四/§七 不一致 |
|
||
| 2026-09-04 | 改名 `menu_children` → `menu_get_children`、`menu_name` → `menu_get_name`(裸名词改"动词+对象",API 统一 `has_`/`get_` 风格);§6.4 建页片段改用 `s_path`/`s_depth`/`s_cont` 现算模型(条目不落地) |
|
||
| 2026-09-04 | §6.4 补"签名约定"说明:`node`/`out` 取 `uint16_t` 因与 `parent`(须装 `0xFFFF` 根哨兵)同域、**非容量所需**(42 下标 8 位够);`max`=出参缓冲容量(防越界 + 对齐单页行数,今日 9≤16 不触发截断) |
|
||
| 2026-09-04 | **索引位宽 16→8 位**:`MENU_ROOT` 由 `0xFFFF` 降为 `0xFF`,`parent`/`node`/`out`/`s_path[]` 均改 `uint8_t`(菜单上限 255 项,远够);路径历史 13→9 B、省 ~20 B RAM,Flash 不变(结构体因指针仍 8 B/项);双护栏=生成器 `节点数>255 报错` + 头文件 C99 断言 `menu_tab_count_check`。生成器已改并重生成 `ui_menu_tab.c/.h`,MSVC /W4 编译 + 导航测试(根 7 / 整定 4 / 查看 4 / 记录查询 9)通过;同步 §四/§6.2/§6.4/§6.5/§七。取代上一条"签名约定(uint16_t)"决定 |
|
||
| 2026-09-04 | 修正 §四"进入/返回示例":返回行节点顺序原与进入行相反(参数在左)致 `←` 被读成"前进"方向;改为与进入行**同序**(主界面 L0 在左…参数 L3 在右)+ `←`,并补"怎么读"注(`→`/`←` = 进/退动作方向;返回 = 从最右 参数(L3) 沿 `←` 一级级退回最左 主界面(L0)) |
|
||
| 2026-09-04 | **子页面显示模型定稿(横向级联子框)**:§四 新增 ⑧"显示模型"——横向树,每级一框在上一级**右侧**、顶边对齐上级"选中行"行顶;水平**右锚定**(最深框钉右缘、父框超宽滚出左、行按屏上/外建毁)+ 框宽文字自适应 + **无动画**;垂直每框独立(顶边=上级选中行行顶、框高按内容、**仅底边>160 才上移**贴屏底、比整屏高则 `↑`/`↓` 滚);焦点=最右框(`↑`/`↓` 移选/滚、`→`/`ENTER` 进、`←`/`ESC` 关最右框)。§七 同步:行对象按层 `s_row[d][i]` 仅屏上框建、去掉逐框页头(`s_header`/`s_back_btn`/`s_title`)、新增每框滚动 `s_scroll[]`(8 B)、"存 vs 算"增 滚动=存/位置=算、持久态 9→17 B、内存预算(静态 ~0.75→~1.3 KB、LVGL 池=屏上 2~3 框)、对照表更新 |
|
||
| 2026-09-04 | **§四⑧ 按参考图 02~07 像素实测校准**(设备 px,误差 ≤3px):层级树/命名/箭头/选中态全部与图一致 ✓;显示模型两处修正——水平由"右锚定(钉屏幕右缘)"改为"**子框左缘=父框右缘**(共享边框列、零间隙)+ 宽自适应"(实测各子框均贴父框右缘 x≈67/106,最深框止于 x≈113~127 而非屏右缘);垂直溢出由"上移使底边贴屏底"改为"**上移使子框底边对齐父选中行行底**"(实测三遥 top=26/装置维护 top=54/通讯参数 top=82/记录查询 top=12 均吻合,且可叠到屏顶状态行区、顶部不钳制)。本菜单最深路径 ≈155<160 不会溢出;>160 行为与"根页框宽 99 vs 68"列为待确认。§7.2/§7.3/§7.4 "右锚定"表述同步 |
|
||
| 2026-09-04 | **实现落地,本文档与代码同步**:三层拆分实现(`ui_menu_tab` 生成物 + 新增 `ui_menu_view` 显示层、`ui_menu` 瘦身为纯状态机,§8.1);§四⑦/§6.4/§6.5/§6.6/§6.7 函数与 API 名同步为实际 `ui_menu_tab_*`/`ui_menu_view_*`/`menu_open\|menu_back\|menu_key_handle`;§五"待实现"→"已实现";§七 修正为实际字段(容器从不切 HIDDEN、`s_style_frame`、新增框几何缓存 `s_frame[]`,§7.2 G);§四⑧ 实测值区分"参考图/实现"两套(实现:根框 66、子框 42/46/54/6,顶 28/58/88/13,偏差入 §8.8 注 5),>160 溢出"待确认"落定为实现行为"屏缘裁切";新增 **§八 实现架构与关键实施细节**(布局常量/几何公式/对象模型/键路径/初始化时序/分级验证/6 条陷阱与保真度说明);验证:模拟器 7 状态对照参考图 01~07 + `@build` 0 错 0 警 |
|
||
| 2026-09-07 | **全文结构重排(理清三层架构与协作)**:旧版三层描述分散在 §六(数据结构)/§七(运行时数据)/§八(实现架构) 三处、§四 又混装导航逻辑与显示模型,读者难以看清各层职责与层间配合;重排为十章——**一 架构总览**(1.1 为什么拆三层 / 1.2 职责速览+依赖图 / 1.3 调用矩阵 / 1.4 一次按键的旅程 / 1.5 初始化时序 / 1.6 层间不变量)→ **二 菜单内容** → **三 数据层**(含 3.7 改菜单流程)→ **四 显示规范**(自 §四⑧ 抽出,含 4.5 参考图偏差)→ **五 显示层**(5.1 职责边界 / 5.5 公开 API 8 接口表)→ **六 逻辑层**(6.2 17 B 状态 / 6.3 导航规则含完整走查 / 6.4 键语义与分发)→ **七 层间配合细节**(7.1 逐键分层动作 / 7.2 框生命周期 / 7.3 存vs算 / 7.4 读写矩阵 / 7.5 内存预算带层归属)→ **八 验证与陷阱** → **九 迁移历史**(结构/运行时/内容三表收拢)→ **十 变更记录**;**修正归属错误**:`s_scroll[]` 实为**逻辑层** `ui_menu.c` 字段(17 B 导航状态之一,见 ui_menu.c),旧 §7.2 误划给显示层,新文 §6.2/§7.5 已归正;全部交叉引用同步到新章号;技术内容零删减 |
|
||
| 2026-09-07 | §1.2/§1.4/§1.5 三幅架构图由 ASCII 转 Mermaid(1 张 flowchart + 2 张 sequenceDiagram,初始化时序含表自检 alt/else 分支);键源措辞统一为“远端键”(与 `UI_设计架构.md`“远端客户端”一致:MCU 端按键经 TCP 7003 远端客户端 → ui_net_sink → keypad);职责表/调用矩阵/“两个要点”文字保留 |
|
||
| 2026-09-07 | 正文现状化:删除正文过渡性历史叙述(“设计阶段拟名…/实现采用…/见变更记录”等,旧文见 git 历史),各节改写为现状描述;移除 §九“迁移历史”整章(旧→新对照保留于本变更记录),变更记录重编号为 §九;§8 去日期注记,陷阱 3/4 改写为现状约束 |
|
||
| 2026-09-07 | §4.1 总览图由 ASCII 框线图升级为 SVG 精确几何图(新增 `docs/images/级联框总览.svg`,按 §5.2 常量 / §5.3 公式逐 px 绘制,含根框装饰 / 状态行 / 选中行反色 / 共享边框列标注 + ①~⑦ 位置标注);ASCII 版删除(旧图见 git 历史) |
|
||
| 2026-09-07 | 删除 §4.5“参考图与实现偏差”表(偏差数据已在 §8.2 注 5“参考图保真度”完整记载);正文两处引用(§四 引言、§8.1 验证表)改指 §8.2 注 5 |
|
||
| 2026-09-07 | §5.1 职责边界表去掉“不做”列、改为列表(负向约束已由章首“绝不做”行与 §1.6 层间不变量表承载,无信息损失) |
|
||
| 2026-09-07 | §6.1 职责边界表同样去掉“不做”列、改为列表(负向约束由章首“绝不做”行、§1.3 调用矩阵与 §1.6 层间不变量表承载,无信息损失) |
|
||
| 2026-09-08 | §5.3 几何计算重写为图文并茂:新增 `docs/images/级联框几何计算.svg` 四格图(① 行剖面定框宽 ② X 共享边框列 ③ Y 顶对齐 ④ Y 溢出上移,含虚线幽灵框 + 超屏红区示意),正文改为四步叙述 + 实测框宽表 + 公式汇总(五式内容不变) |
|
||
| 2026-09-08 | `menu_frame_t` 瘦身:`built` bool→`uint8_t`(与系统存储类型一致);`x/y/w/h` `int32_t`→`int16_t`(值域 0..160 / ≤242,16 位够;y 需有符号——钳制前可为负);`s_frame[4]` 96 B→48 B,省 48 B RAM(§5.4 对象模型表 / §7.5 内存预算表同步) |
|
||
| 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 | 用户密钥密码页落地(首个表单/操作页,§九 / `内容页架构.md` §4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API `ui_content_page_invalidate`;受保护叶子路由(arm→开密码页,确认一致后回调**先关页再续做进入动作**);§9.2/§9.3/§9.4 同步。验证:回归扩至 7 页 + 往返水位全 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 + 三页几何不变 |
|
||
| 2026-09-09 | 权限模型(§3.7/§3.8/§9.4 同步):菜单表行尾注记 ` #P`(需密码);`g_menu_tab` pwd 列改 0/1 布尔(API `ui_menu_tab_has_pwd`,表项仍 8 B/项);两个编译期密码(查看 000 / 修改 123)+ `ui_perm_t` 权限枚举在 `ui_pwd_def.h`(用户定案:密码仅编译期写入,不落 Flash、无运行期设置页)——密码页输查看密码授只读权限、输修改密码授修改权限(只有修改权限进入后才能改参数);逻辑层密码检查提前到内容页判断之前(带内容页的需密码叶子同样先经密码页,修复路由缺口);无密码叶子直接进、默认只读。验证:五门禁全绿(菜单 8 态 + 内容页 7 页像素一致、池 4784/4864 B 不变、Keil 0 错 0 警、代码规范 PASS) |
|