d64bbdc57f
数据面(App/algo,纯逻辑,双端编译) - wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask 唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃 兜底——下一次真故障无波可录比丢一段更贵 - wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带 锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度 - 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式 - 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样 纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉 - test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条 自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合 - 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定) 接线(任务层) - adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图), 段头按最低有效位精确记账,不经浮点回算 - adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实; 台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行 - defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到 关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms 补一条告警并记历史峰值(喂狗余量可核) - Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留 超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的, 半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进 - Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到 上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数; 段文件字节布局零改动,三份合成段哈希逐字节不变 - protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体 零改动 人机(160×160 单色屏) - 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器 得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30 - 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与 垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、 单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据 - ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、 手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口 - 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有 60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形 - 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变 - 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏 22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、 仪器用已在跑的单轮工作耗时探针,不新加埋点 台账与文档 - 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936 字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名; 散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建 重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置) - 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据); §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径 风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」 - 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项 - 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划 索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动) - 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件 形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记 「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场 - 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行 227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退 为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与 体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账 同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道 编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串 生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
488 lines
38 KiB
Markdown
488 lines
38 KiB
Markdown
# 内容页架构
|
||
|
||
> **用途**:说明内容页体系在代码中**实际如何工作**(框架机制 / 页面插件 / 数据流 / 键与绘制行为 / 内存),
|
||
> 并给出新增页面的标准步骤。全文与当前代码一一对应;页面像素几何(行槽/选中带/页脚实测值)
|
||
> 见 `菜单页面设计.md` §九。
|
||
> **配套文档**:`UI_设计架构.md`(§1.2 文件地图 / §4.7 内容页 / §5.6 常量样式)、`菜单页面设计.md`(§九 内容页)、
|
||
> `UI_界面制作流程.md`(参考图→代码管线)、`UI_自动化测试流程.md`(回归门禁与黄金参考)。
|
||
|
||
---
|
||
|
||
## 一、什么是内容页
|
||
|
||
菜单**叶子节点** ENTER 后进入的**独立全屏页面**:新建一块 LVGL 屏幕,全部内容(装饰 + 标题 + 行 + 页脚)
|
||
由框架一次性自绘(**无子对象**),ESC 返回菜单屏。当前已接 5 页:
|
||
|
||
| 页面 | 菜单节点 | 形态 | 数据 | 描述符对象 |
|
||
|---|---|---|---|---|
|
||
| 装置信息 | node 1 | 静态信息页(6 行 + 副标题,不分页) | Flash 表 | `ui_page_device_info_desc` |
|
||
| 交流量 | node 3 | 列表页(24 项,8 项/页,共 3 页) | Flash 表(第 1 页实测,2/3 页占位) | `ui_page_ac_desc` |
|
||
| 直流量 | node 4 | 列表页(3 项,单页) | Flash 表 | `ui_page_dc_desc` |
|
||
| 网口设置 | node 30(通讯参数子项) | 列表页(5 行 1 页,动态值)+ 可编辑(`edit_capable`,↑↓调字节/←→移光标/ENTER 确认;保存行 ENTER → 投递保存请求,属主任务 defaultTask 写盘 + 立即应用(免重启),行值随交接状态 ENTER/WAIT/OK/ERR) | `net_cfg` RAM 副本(defaultTask 上电自 Flash 加载;netTask 回写实际 MAC 影子) | `ui_page_net_cfg_desc` |
|
||
| 用户密钥 | 需密码叶子(如 node 8 参数) | 表单/操作页(draw_cb + key_cb;无行、无"浅 V"、无页脚) | arm 装载:完成回调(携授予权限);页面比对两个编译期密码 | `ui_page_password_desc` |
|
||
|
||
设计核心是 **"框架 + 页面插件"**:框架 `ui_content_page` 只持**机制**(屏/容器生命周期、焦点、
|
||
按键分发、通用绘制),每页的**数据/几何/钩子**由独立 `ui_page_xxx` 模块经描述符 `content_page_def_t`
|
||
提供;两者只以描述符相遇——框架不持任何页面数据,页面模块不碰任何 LVGL 对象。
|
||
|
||
### 1.1 系统总览
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
K(["按键 / LVGL 刷新<br/>(均在 uiTask 上下文,10ms 周期)"]) --> M["逻辑层 ui_menu.c<br/>导航状态机(17 B)+ 键语义<br/>menu_content_page_for:叶子节点 → 页 ID"]
|
||
|
||
subgraph FW["框架 ui_content_page.c/h —— 只有机制,零页面数据"]
|
||
S[("描述符表 s_defs[]<br/>页 ID → const content_page_def_t *<br/>(几何 + 数据源 + 4 钩子)")]
|
||
一级菜单["屏/容器生命周期<br/>按需建毁 · 焦点 · 切屏"]
|
||
二级菜单["按键分发<br/>内置 ↑↓/ESC + key_cb 钩子"]
|
||
三级菜单["通用绘制(LV_EVENT_DRAW_MAIN)<br/>浅 V → 标题/副标题 → 行 → 选中带 → 页脚 → draw_cb"]
|
||
end
|
||
|
||
M -- "ENTER:ui_content_page_open(id, perm)" --> S
|
||
S -- "描述符驱动" --> 一级菜单
|
||
S -- "描述符驱动" --> 二级菜单
|
||
S -- "描述符驱动" --> 三级菜单
|
||
二级菜单 -- "ESC/←:close()" --> 一级菜单
|
||
|
||
subgraph PG["页面插件 —— 数据(Flash)+ 几何 + 钩子实现,零 LVGL 对象"]
|
||
P1["ui_page_device_info<br/>装置信息(静态信息页)"]
|
||
P2["ui_page_ac<br/>交流量(列表页)"]
|
||
P3["ui_page_dc<br/>直流量(列表页)"]
|
||
P4["ui_page_password<br/>用户密钥(表单/操作页)"]
|
||
PN["ui_page_xxx<br/>扩展点:每新增页一文件"]
|
||
end
|
||
|
||
P1 -- "const 描述符(Flash)" --> S
|
||
P2 -- "const 描述符(Flash)" --> S
|
||
P3 -- "const 描述符(Flash)" --> S
|
||
P4 -- "const 描述符(Flash)" --> S
|
||
PN -- "const 描述符(Flash)" --> S
|
||
|
||
三级菜单 -- "ui_deco_draw" --> D["ui_deco(共享)<br/>顶部“浅 V”装饰"]
|
||
三级菜单 -- "动态行:refresh / count_cb" --> PG
|
||
二级菜单 -- "页面自有键:key_cb" --> PG
|
||
```
|
||
|
||
读图要点:
|
||
|
||
- **数据只向下一层流**:按键 → 逻辑层 → 框架;页面插件不主动调任何人,只被动提供描述符与钩子
|
||
- **框架 ↔ 页面只以描述符相遇**:框架不持页面数据,页面不碰 LVGL 对象
|
||
- 反向调用(`refresh/count_cb/draw_cb/key_cb`)是**框架回调页面**,方向与依赖方向相反
|
||
- 菜单根框与内容页共用同一"浅 V"装饰(`ui_deco` 模块,几何唯一定义处)
|
||
|
||
## 二、系统组成
|
||
|
||
| 文件 | 角色 | 一句话职责 |
|
||
|---|---|---|
|
||
| `ui_content_page.c/h` | **框架** | 屏/容器生命周期 + 按键分发 + 描述符驱动通用绘制 + 描述符表;公开 `content_page_id_t` / `content_page_def_t` / `cp_row_t` 与 4 个 API |
|
||
| `ui_page_device_info.c/h` | 页面插件 | 装置信息页:6 行 Flash 表 + 描述符对象 `ui_page_device_info_desc` |
|
||
| `ui_page_ac.c/h` | 页面插件 | 交流量页:24 行 Flash 表 + 描述符对象 `ui_page_ac_desc` |
|
||
| `ui_page_dc.c/h` | 页面插件 | 直流量页:3 行 Flash 表 + 描述符对象 `ui_page_dc_desc` |
|
||
| `ui_page_password.c/h` | 页面插件 | 用户密钥页:3 数字框自绘(draw_cb)+ ↑↓/←→/ENTER 键处理(key_cb)+ arm API + 描述符对象 `ui_page_password_desc` |
|
||
| `ui_page_net_cfg.c/h` | 页面插件 | 网口设置页:5 行(IP/掩码/网关/MAC + 保存)动态值 + 可编辑(draw_cb 反显活动字节 + key_cb 调字节/移光标/保存)+ `init_cb` 重置运行态 + 描述符对象 `ui_page_net_cfg_desc`(数据源 `net_cfg`) |
|
||
| `ui_deco.c/h` | 共享 | 顶部"浅 V"装饰(3 段线 + culling),菜单根框/内容页共用 |
|
||
| `ui_menu.c` | 逻辑层 | `menu_content_page_for` 叶子节点 → 页 ID 映射;ENTER 普通叶子调 `ui_content_page_open`;受保护叶子先 arm + 开密码页,确认一致后回调**先关页再续做进入动作**(`menu_password_ok`) |
|
||
| `ui_menu_view.c` | 逻辑层依赖 | `ui_content_page_close` 经 `ui_menu_view_container()` 把焦点切回菜单持久容器 |
|
||
|
||
依赖方向:`ui_menu(逻辑)→ ui_content_page(框架)→ ui_page_xxx(页面数据)`;
|
||
页面模块只依赖框架头文件(取 `content_page_def_t` / `cp_row_t` 类型),**不反向调用任何人**。
|
||
|
||
## 三、框架机制(ui_content_page.c)
|
||
|
||
### 3.1 公开 API
|
||
|
||
| API | 作用 | 关键行为(代码实际) |
|
||
|---|---|---|
|
||
| `bool ui_content_page_open(id)` | 开页 | ① 查 `s_defs[id]`:NULL → 打 `ERROR` 日志并返回 false(枚举/表漂移防护)② `lv_obj_create(NULL)` 建屏 + 全屏容器(flat 常量样式 + `IGNORE_LAYOUT` + `CLICK_FOCUSABLE`)③ 容器**先**入默认输入组并获焦 ⓸ `lv_screen_load` 切屏 |
|
||
| `void ui_content_page_close()` | 关页 | ① 焦点**先**回菜单持久容器(防输入组焦点悬空)② 切回菜单屏(菜单屏对象未动,原样还原)③ 再删容器+屏,池内存回落 |
|
||
| `void ui_content_page_invalidate()` | 整页重绘 | 失效页面容器;供表单/操作页自身状态变化后调用(无页打开时空操作) |
|
||
| `bool ui_content_page_is_open()` | 查询 | `s_open != CONTENT_PAGE_COUNT` |
|
||
|
||
框架持有的运行状态(全部 4 个变量,约 20 B):
|
||
|
||
```c
|
||
static content_page_id_t s_open; /* 当前打开的页(CONTENT_PAGE_COUNT = 未打开) */
|
||
static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续;每次进入清零) */
|
||
static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */
|
||
static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */
|
||
```
|
||
|
||
### 3.2 页面生命周期:一次 ENTER → 一次 ESC
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as uiTask(按键 / 10ms 刷新)
|
||
participant M as 逻辑层 ui_menu.c
|
||
participant F as 框架 ui_content_page
|
||
participant P as 页面模块 ui_page_xxx
|
||
participant D as ui_deco(共享装饰)
|
||
|
||
Note over M,F: 菜单屏:焦点 = 菜单持久容器,按键归逻辑层
|
||
U->>M: ENTER(LV_EVENT_KEY → menu_key_cb)
|
||
M->>M: menu_content_page_for(node):叶子节点 → 页 ID
|
||
M->>F: ui_content_page_open(id)
|
||
F->>F: s_defs[id] 查描述符(NULL → 返回 false + 错误日志)
|
||
F->>F: 建屏 + 全屏容器(flat 常量样式)→ 入输入组获焦 → 切屏
|
||
Note over F: 内容页:焦点 = 页面容器,按键归框架键分发
|
||
F->>D: DRAW_MAIN:ui_deco_draw(浅 V)
|
||
F->>F: 标题/副标题(居中)
|
||
F->>P: refresh(row, name, value)(动态页逐行取数;同帧同步光栅化,栈缓冲安全)
|
||
F->>P: count_cb(动态行数)
|
||
F->>F: 行 + 选中白带 + 页脚页码
|
||
F->>P: draw_cb(表单/操作页:自定义内容)
|
||
U->>F: ESC/←(LV_EVENT_KEY → 框架键分发)
|
||
F->>F: close():焦点先回菜单持久容器 → 切回菜单屏 → 再销毁页面屏
|
||
Note over M: 菜单导航状态(s_depth/s_sel/s_scroll)原样保留,返回即还原
|
||
```
|
||
|
||
要点:
|
||
|
||
- **导航状态不压层**:进内容页不动菜单 `s_depth/s_sel/s_scroll`,ESC 返回即原样还原
|
||
- 菜单屏对象**保留不销毁**(`lv_screen_load` 来回切,屏上对象原样还原)
|
||
- 页面按需建毁:任一时刻至多一个"屏 + 容器"存在
|
||
- 焦点/键入口在切屏**之前**接线(先入组获焦,后 `lv_screen_load`),按键立即由框架接管
|
||
|
||
### 3.3 描述符表与页面注册
|
||
|
||
```c
|
||
/* ui_content_page.c(实际代码) */
|
||
static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = {
|
||
[CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc,
|
||
[CONTENT_PAGE_AC] = &ui_page_ac_desc,
|
||
[CONTENT_PAGE_DC] = &ui_page_dc_desc,
|
||
[CONTENT_PAGE_PASSWORD] = &ui_page_password_desc,
|
||
[CONTENT_PAGE_NET_CFG] = &ui_page_net_cfg_desc,
|
||
};
|
||
```
|
||
|
||
- 每页一行:`页 ID → &描述符对象`;绘制/按键逻辑一律经 `s_defs[s_open]` 取当前页描述符
|
||
- **描述符是 extern const 对象**(头文件声明、页面模块 `.c` 定义),框架表直接存其**地址**
|
||
> ⚠️ 为什么不用"访问器函数"建表:ARMCC v5 禁止用函数名初始化对象指针数组(error #144,
|
||
> 拆分实施时实测命中)。extern const 对象模式与菜单数据层 `g_menu_tab[]` 一致,且天然落 Flash。
|
||
- **NULL 防护**:`open()` 时查表为 NULL(枚举加了值但表漏加行)→ 拒开 + `ERROR` 日志,不崩溃
|
||
|
||
### 3.4 按键分发(cp_key_cb)
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A(["按键(焦点容器 → LV_EVENT_KEY → cp_key_cb)"]) --> B{"ESC ?"}
|
||
B -- 是 --> C["ui_content_page_close()<br/>关页返回菜单(框架保证,恒不可拦截)"]
|
||
B -- 否 --> L{"← ?"}
|
||
L -- "key_intercept=1 且<br/>key_cb(←) 返回 true" --> E["页面已处理(表单/操作页自有键)<br/>框架跳过内置语义"]
|
||
L -- "未置标志 / 钩子未处理" --> C
|
||
L -- 否 --> D{"key_cb 钩子非空<br/>且返回 true ?"}
|
||
D -- 是 --> E
|
||
D -- 否 --> F{"↑ / ↓ ?"}
|
||
F -- 是 --> G{"selectable=1 且<br/>总行数 > 0 ?"}
|
||
G -- 是 --> H["移选(首尾环绕,跨页连续)<br/>选中行越界 → 重置首行(防动态行数缩小)<br/>lv_obj_invalidate 触发重绘"]
|
||
G -- 否 --> I["不响应(信息页 / 空表)"]
|
||
F -- 否 --> J["其余键值:忽略"]
|
||
```
|
||
|
||
行为细节(与代码一致):
|
||
|
||
- **ESC 恒返回**:最先判断,`key_cb` 无法拦截(框架保证页面可退出)
|
||
- **← 每页可拦截**:`key_intercept=1` 时 ← **先**交 `key_cb`(true = 页已处理不关页,如密码页光标移动);未置标志或钩子未处理则照旧关页
|
||
- **key_cb 先于内置 ↑↓**:返回 `true` = 页已处理,框架不再执行内置移选
|
||
- **↑↓ 移选**:`(sel + total ∓ 1) % total` 首尾环绕;`s_sel` 是全局下标,跨页连续
|
||
- **越界防护**:`s_sel >= total` 时先重置首行(为动态行数可能缩小而设;静态页恒不触发)
|
||
- 移选只改 `s_sel` + `lv_obj_invalidate(s_cont)` 触发重绘,**不重建任何对象**
|
||
|
||
### 3.5 通用绘制链(cp_draw_cb,LV_EVENT_DRAW_MAIN)
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A(["LV_EVENT_DRAW_MAIN → cp_draw_cb"]) --> B["① 浅 V 装饰:ui_deco_draw(共享模块,内置 culling;hide_deco=1 跳过)<br/>② 标题(居中,每页 title_y / 默认 CP_TITLE_Y=y2) ⓢ 副标题(居中,y=subtitle_y;仅非空页)"]
|
||
B --> C["④ 行区块<br/>列表页:窗口 = 选中行所在页(首可见行 = (sel/每页数)×每页数)<br/>信息页:全量;行槽位置固定,数据随页切换"]
|
||
C --> C1{"行数据来源"}
|
||
C1 -- "静态:rows 表非空" --> C2["直绘 Flash 表(text_static=1,不拷内存)"]
|
||
C1 -- "动态:refresh 非空" --> C3["refresh() 逐行取入栈缓冲(32 B×2,同帧同步光栅化)"]
|
||
C2 --> D
|
||
C3 --> D["逐行:culling 判交 → 选中行白带(x=2..156,高 band_h)<br/>→ 左/右两列 lv_draw_label(选中黑字,其余白字)"]
|
||
D --> E["⑤ 页脚“第 X 页,共 Y 页”(y=143,x=6;hide_footer=1 跳过)<br/>列表页:随选中行现算;信息页:fixed_cur/total 固定值"]
|
||
E --> F{"draw_cb 非空 ?"}
|
||
F -- 是 --> G["⑥ 页面自定义内容(表单/操作页,在内置区块之后)"]
|
||
F -- 否 --> H(["绘制完成"])
|
||
G --> H
|
||
```
|
||
|
||
行为细节(与代码一致):
|
||
|
||
- **行窗口**:`first = (s_sel / rows_per_page) * rows_per_page`,`cnt` 截断到 `rows_per_page`
|
||
(末页不足整页);信息页(`selectable=0`)全量绘制
|
||
- **culling**:每个区块(标题/副标题/行条带/页脚)先判"重绘区 ∩ 该区块 ≠ ∅"(`cp_areas_intersect`,
|
||
与 LVGL `lv_area_intersect` 等价的本地实现,避免依赖私有头),不交则整块跳过
|
||
- **选中带**:`band_h > 0` 且该行选中 → 整行白带 `x=2..156`(参考图实测),文字随之黑字
|
||
- **文本**:静态行 `text_static=1`(Flash 字符串不拷贝);动态行与页脚页码用栈缓冲
|
||
(`lv_draw_label` 在同一刷新周期内**同步**光栅化,指针仅需调用期间有效)
|
||
- **页脚**:`lv_snprintf` 现算(MSVC 对 `snprintf` 中文格式串误报 C4819,故用 LVGL 自带 `lv_snprintf`)
|
||
|
||
> ⚠️ **`lv_draw_label` 折行陷阱**:`lv_draw_label` 的换行宽度取 **`coords` 宽度**(未置 `LV_TEXT_FLAG_EXPAND` 时 `w = lv_area_get_width(coords)`)。在**窄区域**(光标盒 / 小方框)里传入**长于该区域的文本**时,LVGL 会按该宽度**折行并逐行向下绘制**,溢出到下方其它行与空白区(表现为"下方弹出一串字符")。规则:窄区域只传"恰好该区域宽度的文本"(如网口设置光标段只传 3 个字符),长文本请在整行宽条带内绘制。
|
||
- 容器样式为**常量样式**(`LV_STYLE_CONST_INIT`,零 LVGL 池分配),见 `UI_设计架构.md` §5.6
|
||
|
||
## 四、页面描述符(content_page_def_t)
|
||
|
||
```c
|
||
/* ui_content_page.h(实际代码) */
|
||
typedef struct {
|
||
const char *title; /* 标题(浅 V 区居中) */
|
||
const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */
|
||
int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */
|
||
int32_t title_y; /* 标题放置 y;0 = 框架默认(CP_TITLE_Y) */
|
||
uint8_t hide_deco; /* 1 = 不绘"浅 V"装饰(密码页跟随实机) */
|
||
uint8_t hide_footer; /* 1 = 不绘页脚页码(密码页跟随实机:无页脚) */
|
||
uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */
|
||
uint8_t key_intercept; /* 1 = LV_KEY_LEFT 先交 key_cb(true = 页已处理不关页);ESC 恒不可拦截 */
|
||
int32_t top_y; /* 首行槽顶 y */
|
||
int32_t pitch; /* 行距(相邻行槽顶间距) */
|
||
int32_t band_h; /* 选中白带高(0 = 无选中带) */
|
||
int32_t text_dy; /* 文本放置相对行槽顶的垂直偏移 */
|
||
int32_t left_x; /* 左列 x(信息页=标签、列表页=名) */
|
||
int32_t right_x; /* 右列 x(值) */
|
||
uint8_t rows_per_page; /* 每页可见行数(0 = 不分页) */
|
||
uint16_t row_count; /* 总行数(静态页;动态页用 count_cb) */
|
||
const cp_row_t *rows; /* 静态数据表(Flash);NULL = 动态页 */
|
||
uint8_t fixed_cur_page; /* 信息页页脚"第 X 页"固定显示值 */
|
||
uint8_t fixed_total_pages; /* 信息页页脚"共 Y 页"固定显示值 */
|
||
/* —— 扩展钩子(全可选;NULL = 纯内置行为;均运行在 uiTask 上下文)—— */
|
||
void (*refresh)(uint16_t row, char *name, char *value); /* 动态行:逐行取数(名/值) */
|
||
uint16_t (*count_cb)(void); /* 动态行数(记录页) */
|
||
void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */
|
||
bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */
|
||
} content_page_def_t;
|
||
```
|
||
|
||
字段一览(含当前 4 页实际值):
|
||
|
||
| 字段 | 类型 | 作用 | 装置信息 | 交流量 | 直流量 | 用户密钥 |
|
||
|---|---|---|---|---|---|---|
|
||
| `title` | `const char *` | 标题(浅 V 区居中) | "装置信息" | "交流量" | "直流量" | "用户密钥" |
|
||
| `subtitle` | `const char *` | 副标题;NULL = 无 | "馈线自动化终端" | NULL | NULL | NULL |
|
||
| `subtitle_y` | `int32_t` | 副标题放置 y | 25 | — | — | — |
|
||
| `title_y` | `int32_t` | 标题放置 y(0 = 框架默认) | 0 | 0 | 0 | 44 |
|
||
| `hide_deco` / `hide_footer` | `uint8_t` | "浅 V" / 页脚开关 | 0 / 0 | 0 / 0 | 0 / 0 | 1 / 1 |
|
||
| `selectable` | `uint8_t` | 1 = 列表页(↑↓+分页);0 = 信息页 | 0 | 1 | 1 | 0 |
|
||
| `key_intercept` | `uint8_t` | ← 拦截标志(先交 key_cb) | 0 | 0 | 0 | 1 |
|
||
| `top_y` | `int32_t` | 首行槽顶 y | 40 | 24 | 24 | —(无行) |
|
||
| `pitch` | `int32_t` | 行距 | 15 | 14 | 14 | —(无行) |
|
||
| `band_h` | `int32_t` | 选中白带高(0 = 无) | 0 | 13 | 13 | —(无行) |
|
||
| `text_dy` | `int32_t` | 文本垂直偏移 | 0 | 0 | 0 | —(无行) |
|
||
| `left_x` / `right_x` | `int32_t` | 左列(标签/名)/ 右列(值)x | 6 / 86 | 7 / 101 | 7 / 101 | —(无行) |
|
||
| `rows_per_page` | `uint8_t` | 每页行数(0 = 不分页) | 0 | 8 | 8 | 0 |
|
||
| `row_count` | `uint16_t` | 总行数(静态页) | 6 | 24 | 3 | 0 |
|
||
| `rows` | `const cp_row_t *` | 静态数据表(Flash) | 6 行 | 24 行 | 3 行 | NULL |
|
||
| `fixed_cur_page` / `fixed_total_pages` | `uint8_t` | 信息页页脚固定值 | 10 / 1 | — | — | — |
|
||
| `refresh` / `count_cb` / `draw_cb` / `key_cb` | 函数指针 | 扩展钩子 | 全 NULL | 全 NULL | 全 NULL | draw_cb + key_cb(余 NULL) |
|
||
|
||
扩展钩子(机制已实现;密码页用 `draw_cb` + `key_cb`,其余 3 页未用):
|
||
|
||
| 钩子 | 签名 | 框架调用时机(uiTask 上下文) | 用途 |
|
||
|---|---|---|---|
|
||
| `refresh` | `void (uint16_t row, char *name, char *value)` | 绘制行区块时,每行一次、`lv_draw_label` 之前 | 动态行:逐行取名/值(遥信量、记录页等) |
|
||
| `count_cb` | `uint16_t (void)` | 绘制(行窗口/页脚)与按键(移选取模)经 `cp_row_total()` 取行数 | 动态行数(记录查询类) |
|
||
| `draw_cb` | `void (lv_layer_t *layer, const lv_area_t *clip)` | 绘制链末尾(内置区块之后) | 表单/操作页自定义内容 |
|
||
| `key_cb` | `bool (lv_key_t key)` | 键分发时(ESC 之后、内置 ↑↓ 之前) | 页面自有键(ENTER 确认等);返回 true = 页已处理 |
|
||
|
||
### 4.1 表单/操作页首个应用:密码页(已落地)
|
||
|
||
实机(淮源电力 F30)已有"用户密钥"页:进入某些子叶面前须先输入密码(实机照片见
|
||
`mockups/密码页面实际机器的示例图片.jpg`)。它是本项目**第一个表单/操作页**,落地形态即
|
||
`draw_cb` + `key_cb` 组合。
|
||
|
||
**功能规则(已确认)**
|
||
|
||
- 密码共 **3 位**;子页(叶子)可配置"是否需要密码页"(行尾 ` #P`);密码页按所输密码**授权**:
|
||
查看密码 → 只读权限 / 修改密码 → 修改权限(只有修改权限进入后才能改参数);
|
||
密码值为编译期配置(`ui_pwd_def.h`,查看/修改各一个,改值 = 重新编译)
|
||
- 需密码叶子 ENTER:先开密码页;一致后按授予权限进入目标页(页面据此只读/可编辑);ESC 随时返回菜单
|
||
- 无密码叶子直接进,默认只读权限
|
||
- 键语义(全部由页面 `key_cb` 处理;`selectable=0`,框架内置 ↑↓ 移选不适用):
|
||
|
||
| 键 | 行为 |
|
||
|---|---|
|
||
| ↑ / ↓ | 调整光标位数字值(0–9,环绕:9↔0) |
|
||
| ← / → | 移动光标(反显框)左/右(环绕:首框↔末框) |
|
||
| ENTER | 确认:修改密码 → 关密码页并以修改权限打开目标页;查看密码 → 以只读权限打开;不一致 → 3 框清空回 `0`、光标回首框,重输(已确认) |
|
||
| ESC | 返回菜单(框架保证,不可拦截) |
|
||
|
||
**实机视觉(照片;✅ 已确认按此实现)**
|
||
|
||
- 标题"用户密钥"居中、位于屏中上部(约屏高 1/4 处);**无**"浅 V"装饰、**无**页脚
|
||
- 3 个方框单行居中(约屏高 1/2 处),均**预填 `0`**:首框**反显(黑底白字)= 光标位**,
|
||
其余 2 框白底黑字、细边框
|
||
|
||
**框架映射(描述符配置)**
|
||
|
||
| 字段 | 值 |
|
||
|---|---|
|
||
| `selectable` | 0(无内置移选) |
|
||
| `rows` / `row_count` | NULL / 0 |
|
||
| `draw_cb` | 非空:绘制标题 + 3 个数字框(光标框反显) |
|
||
| `key_cb` | 非空:↑↓ / ←→ / ENTER 按上表 |
|
||
| `title` | "用户密钥"(y≈45 中上、"浅 V"关 —— 跟随实机;需下方 ⚠️ 扩展 2) |
|
||
|
||
**✅ 框架扩展(已落地,见 §3.4/§3.5)**
|
||
|
||
1. **← 键拦截**:描述符 `key_intercept`——置位时 `LV_KEY_LEFT` **先**交 `key_cb`(true = 页已处理不关页),
|
||
未置标志或钩子未处理则照旧关页;`LV_KEY_ESC` **不受**拦截("页面恒可退出"的框架保证不放松)。
|
||
其余 3 页不置标志,行为完全不变
|
||
2. **每页标题 y / "浅 V" / 页脚开关**:描述符 `title_y`(0 = 默认 CP_TITLE_Y)/ `hide_deco` / `hide_footer`;
|
||
密码页取 title_y=44、双开关置位,跟随实机照片
|
||
|
||
**设计确认进度**
|
||
|
||
| # | 事项 | 状态 |
|
||
|---|---|---|
|
||
| 1 | 密码不一致行为 | ✅ 已确认:清空重输(3 框回 `0`、光标回首框) |
|
||
| 2 | 密码值的存放与设置 | ✅ 已确认(2026-09-09 修订):仅编译期配置(`ui_pwd_def.h`,查看/修改各一个值,按所输密码授权),不做运行期设置页、不落 Flash(用户定案:密码只编译期写入) |
|
||
| 3 | 视觉风格 | ✅ 已确认:跟随实机照片(无"浅 V"、标题中上)→ 需 ⚠️ 扩展 2 |
|
||
| 4 | ← 键冲突解法 | ✅ 已确认:方案 A 描述符拦截开关(`key_intercept`);`LV_KEY_LEFT` 先交页面钩子,ESC 恒不可拦截,现有 3 页不置开关、零影响 |
|
||
| 5 | 边界环绕行为 | ✅ 已确认:都环绕(数字 9↔0,光标首框↔末框) |
|
||
|
||
**落地要点(与代码一致)**
|
||
|
||
- **arm API**:`ui_page_password_arm(on_ok)` 由逻辑层在开页前装载完成回调;每次进入重置初始态
|
||
(全 `0`、光标首框,与实机一致)。页面模块不感知菜单,比对 `ui_pwd_def.h` 两个编译期密码
|
||
(先查修改:两值同设时授高权),一致时回调以授予的权限(`ui_perm_t`)为实参
|
||
- **权限注入**:开页 `ui_content_page_open(id, perm)` 携授予权限;描述符 `edit_capable` 声明页面
|
||
支持编辑模式;页面钩子经 `ui_content_page_is_editable()`(= edit_capable 且修改权限)决定
|
||
只读/可编辑行为
|
||
- **菜单路由**:需密码叶子 ENTER → `arm(menu_password_ok)` + 开 `CONTENT_PAGE_PASSWORD`;
|
||
密码检查先于内容页判断(带内容页的需密码叶子同样先经密码页);
|
||
确认一致时回调(携权限)**先关密码页回菜单屏、再沿未变导航状态续做"进入"动作**(有内容页→
|
||
开内容页携权限,否则压层进子框);无密码叶子直接开(默认只读)。
|
||
先关页是必须的:不关则后续动作落在被盖住的菜单屏上(焦点已移走,按键全错位);
|
||
事件内删对象由 LVGL `e->deleted` 机制保证安全(与 ESC 路径同模式)
|
||
- **几何**:标题 y=44;3 个 14×14 数字框、间距 6px、行顶 y=78、首框左 x=53(整组居中);光标框反显
|
||
- **回归**:`check_content_pages.py` 含密码页 4 状态(初始 / 输 1 位 / 正确确认进菜单 / 不一致清空),
|
||
pwk 终态经像素级几何断言验证为"4 框菜单 + sel=[2,0,0] + 终点空框"
|
||
|
||
## 五、页面模块(ui_page_xxx)
|
||
|
||
每页一个 `.c/h` 文件对,只含**数据(Flash)+ 描述符对象**,零 LVGL 对象、零机制代码。
|
||
以装置信息页为例(`ui_page_device_info.c` 实际代码节选):
|
||
|
||
```c
|
||
/* 头文件包含区 */
|
||
#include "ui_page_device_info.h" /* 内含 ui_content_page.h,并声明描述符对象 */
|
||
|
||
/* 静态变量定义区 */
|
||
|
||
static const cp_row_t s_rows[] = {
|
||
{"终端类型:", "F30"},
|
||
{"终端型号:", "F30"},
|
||
{"软件版本:", "SV0.010"},
|
||
/* … 共 6 行(参考图 10) */
|
||
{"程序日期:", "2024.08.27"},
|
||
};
|
||
|
||
/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用) */
|
||
const content_page_def_t ui_page_device_info_desc = {
|
||
.title = "装置信息",
|
||
.subtitle = "馈线自动化终端",
|
||
.subtitle_y = 25,
|
||
.selectable = 0u,
|
||
.top_y = 40,
|
||
.pitch = 15,
|
||
.band_h = 0,
|
||
.text_dy = 0,
|
||
.left_x = 6,
|
||
.right_x = 86,
|
||
.rows_per_page = 0u,
|
||
.row_count = 6,
|
||
.rows = s_rows,
|
||
.fixed_cur_page = 10u,
|
||
.fixed_total_pages = 1u,
|
||
};
|
||
```
|
||
|
||
```c
|
||
/* ui_page_device_info.h(对外只有两个声明) */
|
||
#include "ui_content_page.h"
|
||
|
||
extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash const) */
|
||
```
|
||
|
||
- 初始化器按**声明顺序**写指定域(ARMCC v5 = C99 要求顺序一致);省略的钩子字段零初始化为 NULL
|
||
- 行表 `s_rows[]` 是模块私有(`static`),只有描述符对象是外部链接
|
||
- 动态页的差异:`rows = NULL`,改在描述符里给 `refresh`(逐行取数),行数变化再给 `count_cb`
|
||
- 文件编码:`Drivers/BSP/UI/` 下为 **UTF-8 带 BOM**(中文字面量依赖 BOM 让 MSVC 认 UTF-8,
|
||
见 AGENTS.md 编码铁律)
|
||
|
||
## 六、执行上下文与内存
|
||
|
||
**执行上下文**
|
||
|
||
- 框架全部回调(绘制 `cp_draw_cb`、键 `cp_key_cb`、4 个页面钩子)运行在 **uiTask 上下文**
|
||
(LVGL 刷新与按键都走 `ui_task_func` 主循环,10 ms 周期)
|
||
- `refresh` 取到的动态文本在同一刷新周期内经 `lv_draw_label` **同步**光栅化 → 32 B 栈缓冲即安全
|
||
- 跨任务数据(ADC/网络/RTC/存储)沿用既有约定:生产者任务写快照(原子/队列),
|
||
UI 任务在钩子里读快照;框架与页面模块**不加锁**
|
||
|
||
**内存(实测,16 KB LVGL 池)**
|
||
|
||
| 项 | 值 | 说明 |
|
||
|---|---|---|
|
||
| 框架常驻 RAM | ~20 B | `s_open/s_sel/s_scr/s_cont` 4 变量 |
|
||
| 开页峰值 | +776 B | 屏 + 容器 + 事件列表 + 常量样式引用 |
|
||
| 关页回落 | 基线 +80 B 一次性残留 | screens 数组高水位 +8、`lv_screen_load` 菜单屏局部 X/Y 样式 +72(LVGL 核心行为,非泄漏;分解见 `菜单页面设计.md` §9.5) |
|
||
| 池水位门禁 | 5368 B / 5448 B | 菜单基线 / 开页-关页往返(回归脚本常量,2026-10-11 随 LVGL 控件裁剪由 4784/4864 上移;**与池总大小无关**,16 KB 池下实测值不变) |
|
||
| 描述符/行表/标题 | Flash const | 不进 LVGL 池;每页增量 = 描述符 ~40 B + 行表 + 代码 |
|
||
| 动态行栈缓冲 | 2×32 B | 仅绘制期存在 |
|
||
|
||
## 七、新增页面(标准步骤)
|
||
|
||
| # | 动作 | 改哪里 |
|
||
|---|---|---|
|
||
| 1 | `content_page_id_t` 加一个枚举值 | `ui_content_page.h` |
|
||
| 2 | 新建 `ui_page_xxx.c/h`:行表(或回调)+ `extern const content_page_def_t ui_page_xxx_desc`(头文件声明 + `.c` 定义) | `Drivers/BSP/UI/` |
|
||
| 3 | 描述符表加一行 `[新 ID] = &ui_page_xxx_desc` | `ui_content_page.c` `s_defs[]` |
|
||
| 4 | 叶子节点 → 页 ID 映射加一个 case | `ui_menu.c` `menu_content_page_for` |
|
||
| 5 | 双端注册(显式文件列表,无通配) | `MDK-ARM/*.uvprojx`(Drivers/BSP/UI 组)+ `simulator/CMakeLists.txt`(`UI_SOURCES`) |
|
||
| 6 | 回归验证 | `ui-verify` 五门禁;新页在 `check_content_pages.py` 登记黄金参考;含新汉字先重生成字体 |
|
||
|
||
按页面形态选描述符配置:
|
||
|
||
| 页面形态 | 配置要点 |
|
||
|---|---|
|
||
| 静态信息页 | `selectable=0`、`rows`=Flash 表、`fixed_cur_page/fixed_total_pages` 定页脚 |
|
||
| 静态列表页 | `selectable=1`、`rows_per_page` 分页、`rows`=Flash 表(钩子全 NULL) |
|
||
| 动态数据页 | `rows=NULL` + `refresh`(逐行取数);行数随数据变再加 `count_cb` |
|
||
| 表单/操作页 | `selectable=0`、`rows=NULL`、`draw_cb` 自绘内容 + `key_cb` 自有键(ESC 返回仍由框架保证;首个实例=密码页,见 §4.1) |
|
||
|
||
## 八、验证与回归
|
||
|
||
- **门禁**(`ui-verify` skill / `UI_自动化测试流程.md`):
|
||
0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 66 张黄金参考逐字节比对
|
||
(网口设置 4 态 + 密码页 4 态等)+ 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查
|
||
- **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 5368 B`、`POOL_AFTER_ROUNDTRIP = 5448 B`
|
||
- **失败处置**:像素不一致 = 代码 bug(修代码,**禁止**刷参考掩盖);**有意**改视觉 →
|
||
先肉眼确认新效果正确,再 `check_content_pages.py --update` 刷参考(`ref/*.bin` 已入库,刷了会进 diff,必须一并提交并说明)
|
||
- 新增页面后同步在 `check_content_pages.py` 增加该页的按键序列、文本断言与参考图
|
||
|
||
## 九、变更记录
|
||
|
||
| 日期 | 变更 |
|
||
|---|---|
|
||
| 2026-09-09 | 架构定稿:"框架 + 页面插件"模型,新增页面六步流程 |
|
||
| 2026-09-09 | 总览图由 ASCII 框线升级为 Mermaid(结构 flowchart + 生命周期 sequenceDiagram) |
|
||
| 2026-09-09 | 拆分执行:`ui_content_page` 提纯为框架(描述符表 + NULL 防护 + 描述符驱动绘制/按键、4 扩展钩子);三页拆出 `ui_page_device_info` / `ui_page_ac` / `ui_page_dc`(extern const 描述符对象);双端注册;五门禁全绿(像素一致、池 4784/4864 B 不变);描述符定稿 extern const 对象模式(ARMCC #144:函数名不能入对象指针表) |
|
||
| 2026-09-09 | 按实际代码重写(本版):章节重组为"总览 → 组成 → 框架机制 → 描述符 → 页面模块 → 上下文与内存 → 新增页面 → 验证";去掉规划性页面清单与拆分前执行清单;图扩充为 4 张 Mermaid(系统总览 / 页面生命周期 / 按键分发 / 绘制链);全部内容逐项核对当前代码 |
|
||
| 2026-09-09 | 密码页(首个表单/操作页,待实现)设计落档(§4.1):3 位密码、每页专属密码、每页"是否需要密码页"配置、↑↓ 调值 / ←→ 移光标 / ENTER 确认进入;实机视觉参考 `mockups/密码页面实际机器的示例图片.jpg`;同时发现框架冲突:`cp_key_cb` 对 `LV_KEY_LEFT` 先于钩子拦截(ui_content_page.c:345),落地前需描述符拦截标志扩展(ESC 恒不可拦截);4 项待确认(错误行为 / 密码存放 / 环绕行为 / 视觉风格) |
|
||
| 2026-09-09 | 密码页设计确认 3 项(§4.1):不一致 = 清空重输;每页专属密码 = 配置页可设 + 存 Flash 配置区;视觉 = 跟随实机照片(由此确定绘制链扩展需求:每页标题 y / "浅 V" 开关);← 键拦截方案与环绕行为仍待确认 |
|
||
| 2026-09-09 | 密码页 ← 键冲突解法确认(§4.1):方案 A 描述符拦截开关(`key_intercept`:`LV_KEY_LEFT` 先交页面钩子、ESC 恒不可拦截、现有 3 页零影响);框架扩展清单定稿(拦截标志 + 每页标题 y / "浅 V" 开关),仅剩环绕行为待确认 |
|
||
| 2026-09-09 | 密码页设计定稿(§4.1):边界行为 = 都环绕(数字 9↔0、光标首框↔末框);5 项设计全部确认、无遗留待确认——具备实施条件(框架扩展 + 新页面模块 + 每页保护标志/密码) |
|
||
| 2026-09-09 | 密码页落地(§4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API `ui_content_page_invalidate`;新模块 `ui_page_password`(3 数字框反显光标 + arm API,draw_cb+key_cb);菜单受保护叶子路由(arm→开密码页,确认一致后 `menu_password_ok` **先关页再续做进入动作**);字体 97→102 汉字(198 字形);双端注册;回归扩至 7 页 + 往返水位,五门禁全绿 |
|
||
| 2026-09-09 | 权限模型(替代"每页专属密码",§4.1):表行尾注记 ` #P`(需密码,无类别);`ui_pwd_def.h` 两个编译期密码(查看 000 / 修改 123)+ 新 `ui_perm_t` 权限枚举——密码页输查看密码授只读权限、输修改密码授修改权限(只有修改权限进入后才能改参数);生成器同步改造并重新生成(pwd 列 = 0/1 布尔,`ui_menu_tab_has_pwd`);框架 `ui_content_page_open(id, perm)` 携授予权限 + 描述符 `edit_capable` + 新 API `ui_content_page_is_editable`(页面钩子据以判只读/可编辑);路由密码检查提到内容页判断之前(修复带内容页需密码叶子绕过密码的路由缺口);无密码叶子直接进、默认只读。§4.1 功能规则/待确认表/落地要点同步。验证:五门禁全绿(菜单 8 态 + 7 页像素一致、池 4784/4864 B 不变、Keil 0 错 0 警、代码规范 PASS) |
|
||
| 2026-09-10 | 网口设置内容页:新增 `Drivers/BSP/NETCFG/`(Flash 持久化 magic 'NCFG'+CRC-16/MODBUS,工厂默认 192.168.1.100/24、gw .1、MAC 全零=芯片内置)+ 页面插件 `ui_page_net_cfg`(5 行动态值 + 可编辑:↑↓调字节 0..255 环绕 / ←→移光标 / ENTER 确认,保存行 ENTER → `net_cfg_save` 下次上电生效);框架新增描述符钩子 `init_cb`(开页重置运行态)+ API `ui_content_page_selected_row`;`net_init` 增 `mac` 参数、netTask 改为等 `SYS_EVT_STORAGE_READY` → `net_cfg_load` → `net_init(ip,mask,gw,mac)` → 回写实际 MAC 影子;模拟器 main 调 `net_cfg_load` 使参考图显示工厂默认值;字体修复:lv_font_conv 丢弃低码位符号(U+FF0C 全角逗号 < 0x4E2D 范围起点 → 页脚逗号长期空白)改由 `merge_extra_symbols()` 补回 + 原本缺字(存/密/户/本/用/钥)→ 111 字 / 208 字形;回归扩至 11 页(网口设置 4 态:可编辑/只读/编辑光标反显/保存 OK),五门禁全绿 |
|
||
| 2026-09-10 | 修复网口设置页编辑态渲染 bug:光标段原传整串 `value + start`,而 `lv_draw_label` 按 `coords`(光标盒,仅段宽)折行 → 逐行向盒下方绘制、溢出到其它行与空白区("下方弹出一串字符")。改为只传**段文本**(IP/掩码/网关 3 字符、MAC 2 字符);§3.5 补"`lv_draw_label` 折行陷阱"说明 |
|
||
| 2026-09-10 | 网口设置页 MAC 值改常识格式 **`A4:94:B6:12:34:56`**(冒号分隔,17 字符 = 102px);布局调整 名 x=6→4、值 x=60→54(MAC ink 止于 155,仍在白带 2..157 内);编辑光标段偏移 MAC 2i→3i |
|
||
| 2026-09-14 | 运行记录/自检记录动态列表页(`ui_page_runlog` / `ui_page_selftest`):描述符新增 `count_cb` + `refresh`(开页/翻页时重读条数与内容,每条 2 行 = 时间戳/描述),数据源为 `App/log/sys_log` 引擎的 `/log/run.log`、`/log/selftest.log`;**框架不变量 selectable ⇒ band_h>0**——band_h=0 时选中行恒黑字无白带 → 黑底不可见(首版两页均踩中,改 band_h=13 对齐 AC/DC 约定);新坑:本 LVGL 版本 `lv_text_get_width` length≠0 时无 NUL 检查,传 -1 越串尾死循环,必须传 strlen(密码页错误提示首版触发,已修并全库排查) |
|
||
| 2026-09-15 | 网口设置页保存行改**异步交接**(P1-3 跨任务竞态修复,配置引擎属主 = defaultTask):保存行 ENTER → `net_cfg_save_request()`(置"待保存"标志 + `SYS_EVT_CFG_SAVE` 唤醒属主任务,UI 不再直接触碰 cfg_file 引擎);写盘与"立即应用"由 defaultTask 的 `net_cfg_save_run()` 执行(成功 → OK 并触发 `SYS_EVT_NET_RECONFIG` 免重启重建;失败 → ERR 不触发应用);行值改随交接状态 `net_cfg_save_state()` 显示 ENTER/WAIT/OK/ERR(原为页面局部 `s_saved` 同步结果,删除);无头自检(`--keys`)在注键后同步完成交接保持终态一致;五门禁全绿(像素/文本与改前一致,池水位不变) |
|
||
| 2026-10-22 | §八 失败处置口径更新:`ref/*.bin` 黄金参考已入库,刷参考会进 `git diff`、必须随改动一并提交并说明理由(详见 `UI_自动化测试流程.md` §四) |
|
||
| 2026-10-27 | 内容页黄金参考 **40 → 47 页**(遥测死区与系数 2 页落地,`simulator/tests/ref/` 实测 47 张:7 张新增、存量 40 张逐字节零漂移);本文件的当前态计数同步改 47,历史行 27 → 40 原样保留 |
|
||
| 2026-10-27 | 内容页黄金参考 **47 → 53 页**(通讯设置页落地并绑 `#page=comm`;判据 = `simulator/tests/ref/` 入库 `shot_*.bin` 实数 53 张,`git diff --name-status 31ca4a1f HEAD -- simulator/tests/ref` 实测只有 6 个 `A`、零 `M` ⇒ 存量 47 张逐字节零漂移);本文件的当前态计数已同步 |
|
||
| 2026-10-26 | 验证与回归段的页数口径修正为 **27 页**黄金参考(`simulator/tests/ref/` 入库 27 个 `shot_*.bin`,含整定 9 态);上方变更记录里的「11 页」是当时事实不动 |
|
||
| 2026-10-26 | 门禁常量修正为 5368 B / 5448 B(= 两个回归脚本头部现值);本文件正文表早已是 5368/5448,只这一处残留 2026-10-11 裁剪 LVGL 控件类之前的 4784/4864 |
|
||
| 2026-10-26 | 验证与回归段的黄金参考页数 **27 → 40 页**(UI 三页落地后 `simulator/tests/ref/` 实数 40 张);上方变更记录里的 11 / 27 是各当时下限,保留 |
|