dafc18f5ed
启动 docs/任务规划/README.md 的 W6 UI 扩展簇(用户裁定,覆盖 2026-10-11
的"本轮不排期")。四个工作包,全部验证通过。
## U1 设计令牌层(零行为变化)
新建 Drivers/BSP/UI/ui_tokens.h(20 个宏),框架 + 7 个页面 + App/task/
ui_task.c 全部接入。屏幕几何**复用 ui_port.h 既有 UI_SCREEN_W/H**,不重复
定义;令牌头自身只 include ui_port.h,不把 LVGL 拖进全部消费者。
补收了报告没列但代码里真重复的量:UI_TEXT_BAND_H 15(7 次)、列表左右边界
与每页行数(各 4 个页面逐字复制)、推流协议几何与显示几何同源(此前硬编码
3200 且注释写着"须与 UI_FB_SIZE 一致"——典型漂移隐患)、UI_TASK_TICK_MS
(ui_port_step 与 osDelay 必须同值,原先靠注释维系)。
故意不收页面私有几何与"同形不同义"的值(device_info 的 pitch=15 与列表
UI_ROW_PITCH=14 合并即改像素)。
## U3 页面注册表与生成器(删手工分发)
ui_page_registry.{c,h} 由生成器产出,**手工 menu_content_page_for() 已删
除**(仓库内仅存 2 处历史说明注释)。密码页按代码实况**单独出口**
(ui_page_registry_password())——它非节点绑定,任何 #P 叶子进同一页。
顺带落地此前判定"U1 阶段做不了"的共享 flat 样式 ui_style.{c,h}。
生成器除产注册表外**自动把新 .c 写进 uvprojx 与 CMakeLists.txt**,且
**双向对账**:加页打印 registered、删页打印 pruned,不留死条目;并双向
校验(#page 找不到实例报错、实例未被任何 #page 引用也报错)。
## U4 门禁补强(35 个钩子)
新增 font-charset 钩子(scripts/check_font_charset.py):复用
tools/font_gen 的同一套提取与 cmap 解析规则(importlib 直载,零第二套
规则),A 段查源码在用 vs 字符集、B 段查字符集 vs 字体 cmap。
registration-check 补第 ④ 类:实测原规则对"进了两端构建但没接描述符表"
的页面**全绿**——desc 被链接器剥离、编译链接全过、菜单永远打不开的死代码。
仍照不出的两项已如实写进文档(枚举漏加归 @build、菜单树漏挂归 golden)。
## 字体子集瘦身(D 案,4 留 3 删)
ui_cn_chars.txt 122→119 字(恰删 上/电/重,零新增)。异/常/片/芯 进
KEEP_EXTRA 带理由白名单:它们曾在 9eb1bd63 被写进中文自检描述落盘
/log/selftest.log(定长 64B 记录跨固件持久),删字形会使历史故障记录
渲染空白且不可逆。保留字按**必需**处理,反向有牙——谁只跑 extract_cn
把它们静默洗掉,钩子直接 FAIL 并逐字打印理由。
--strict 口径为"未知冗余 = 0"而非"冗余 = 0"。
## 验证
@build 0 错 0 警;ER_IROM1 232,052 B(较 HEAD −228 B)、RW_IRAM1
75,232 B **零增量**、RW_CCM 87.9% 未变。golden:菜单 8 状态全像素一致 +
内容页 11 页逐字节一致,**参考图 sha256 差异 0 条**(未用 --update 刷
基线);池水位菜单基线 5368 B、开页-关页往返 5448 B 与 U1 时相同。
38 个在途修改文件逐个核对 BOM/CRLF/U+FFFD 全部与 HEAD 一致。
L0 33 钩子 + L0.5 16 runner / 1104 断言全绿;check_doc_links --all
142 篇、无法判定 0;mem-budget / mem-numbers 已回填。
## 单位成本实测(U5 排期依据)
临时页 zzprobe 绑到"遥信量"叶子 → 只跑生成器 → 带页 @build 0/0 →
注册表出现 {5u, &ui_page_zzprobe_desc} → 回滚后 6 个产出文件 zzprobe
残留计数均 0、重编回基线。
**结论:新增一页 = 3 处人工(新 .c + 新 .h + 菜单树那一行加
#page=<名>)+ 1 条生成器命令,手工编辑注册文件 0 处。**
据此修正报告原口径"8 处降到 1 处"——那个 1 只在"1 处对既有文件的修改"
意义下成立,页面自身 2 个文件本就要写,真实处数是 3。
## 文档同步与两处自我修正
§0.1 UI 行 B → B+;§6.1「内容页」「设计令牌」两行按实况回填;§6.5 U1/U3
状态更新;§6 顶部加"建议栏不可照抄"总警示(本轮四次证伪:ui_port.h 已有
同名宏、LV_COLOR_BLACK/WHITE 不存在且极性反了、27 页实为目标页数现存 7
个、生成器草图漏密码页非节点绑定)。
两处改口:① §6.5 U4 从我上一版**误写的 3/3 改回 2/3**——"golden 脚本早已
存在"不等于"已进自动门禁",两个 golden 脚本至今未挂 pre-commit,该缺口
单列为待办;② 原计划"分三笔提交"**作废**,U1 与 U3 改同一批文件、文件内
交叠,硬切会产生不可编译的中间态,故整波一笔。
276 lines
12 KiB
C
276 lines
12 KiB
C
/*
|
||
* 模块名称:UI Page Password (用户密钥密码页)
|
||
* 模块功能:3 位密码输入页(表单/操作页首个实例)——标题由框架自绘(无"浅 V"、
|
||
* 无页脚,跟随实机),本模块自绘 3 个数字框:预填 0、首框反显 = 光标位;
|
||
* ↑↓调值(9↔0 环绕)、←→移光标(首↔末环绕)、ENTER 确认(修改密码→完成
|
||
* 回调授修改权限 / 查看密码→完成回调授只读权限 / 不一致→清空重输)、
|
||
* ESC 返回菜单(框架保证);
|
||
* 向框架 ui_content_page 提供页面描述符(零 LVGL 对象)
|
||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||
* 作者:王建锋
|
||
* 创建日期:2026-09-09
|
||
* 修改记录:
|
||
* 2026-09-09 王建锋 创建:表单/操作页首个实例(内容页架构.md §4.1;
|
||
* 实机参考 mockups/密码页面实际机器的示例图片.jpg)
|
||
* 2026-09-09 王建锋 改权限模型:arm 只载回调(不再载期望密码);页面比对
|
||
* ui_pwd_def.h 两个编译期密码(先查修改),一致时回调携
|
||
* 授予的权限(UI_PERM_MODIFY / UI_PERM_VIEW)
|
||
* 2026-09-14 王建锋 输错提示:ENTER 不一致显示"密码输入错误",方向键清除后重输
|
||
* 2026-09-14 王建锋 修 lv_text_get_width 传 -1 死循环(本 LVGL 版 length≠0 时
|
||
* while(i<length) 无 NUL 检查,越串尾读野内存;改传 strlen)
|
||
*/
|
||
|
||
/* 头文件包含区 */
|
||
#include <string.h>
|
||
|
||
#include "ui_page_password.h"
|
||
|
||
#include "ui_pwd_def.h"
|
||
|
||
#include "ui_tokens.h"
|
||
|
||
/* 私有宏定义区 */
|
||
|
||
/*
|
||
* 布局常量(设备像素;实机参考 mockups/密码页面实际机器的示例图片.jpg 实测):
|
||
* 3 个数字框单行居中于屏中下部(框 14x14、间距 6、行顶 y=78、首框 x=53 →
|
||
* 3 框 + 2 距 = 54px,屏中心 80);数字墨迹框内水平居中、垂直贴框顶
|
||
* (ASCII 字形顶距 +2 已计入字体度量)
|
||
*/
|
||
#define PW_DIGITS 3 /* 密码位数 */
|
||
#define PW_BOX_W 14 /* 数字框宽(px) */
|
||
#define PW_BOX_H 14 /* 数字框高(px) */
|
||
#define PW_BOX_GAP 6 /* 数字框间距(px) */
|
||
#define PW_BOX_Y 78 /* 数字框行顶 y */
|
||
#define PW_BOX_X0 53 /* 首个数字框左 x(居中) */
|
||
|
||
#define PW_TITLE_Y 44 /* 标题放置 y(中上;0 = 框架默认,实机明显低于其余各页的 y=2) */
|
||
|
||
#define PW_ERR_Y 104 /* "密码输入错误"提示行顶 y(数字框行之下) */
|
||
|
||
/* 静态变量定义区 */
|
||
|
||
/* 单位数字文本(Flash):数字 0..9 各 1 字符(text_static,不拷内存) */
|
||
static const char *const s_digit_str[10] = {
|
||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
|
||
};
|
||
|
||
/* 页面运行状态(RAM,约 10 B):3 位值 + 光标位 + 错误标志 + 完成回调 */
|
||
static uint8_t s_digits[PW_DIGITS]; /* 3 位密码值(0..9) */
|
||
static uint8_t s_cur; /* 光标位 0..2(反显框) */
|
||
static uint8_t s_err; /* 错误提示标志(1 = 显示"密码输入错误",方向键清除) */
|
||
static void (*s_on_ok)(uint8_t); /* 完成回调(逻辑层预装,实参=授予权限;NULL = 一致时仅关页) */
|
||
|
||
/* 函数定义区 */
|
||
|
||
/*
|
||
* 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头)
|
||
* 入口参数:a - 区域 1 const lv_area_t *
|
||
* b - 区域 2 const lv_area_t *
|
||
* 返回值:true - 相交;false - 不相交
|
||
* 限定条件:无
|
||
* 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_content_page.c 同实现)
|
||
*/
|
||
static bool pw_areas_intersect(const lv_area_t *a, const lv_area_t *b) {
|
||
int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1;
|
||
int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1;
|
||
int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2;
|
||
int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2;
|
||
|
||
return (x1 <= x2 && y1 <= y2);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:绘制数字框内数字(单字符,水平居中)
|
||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||
* box - 数字框区域 const lv_area_t *
|
||
* digit - 数字值(0..9) uint32_t
|
||
* color - 文本颜色 lv_color_t
|
||
* 返回值:无
|
||
* 限定条件:数字框在重绘区内(调用方 culling)
|
||
* 函数说明:文本为 Flash 字面量(不拷内存);ofs 相对区域原点
|
||
*/
|
||
static void pw_draw_digit(lv_layer_t *layer, const lv_area_t *box, uint32_t digit, lv_color_t color) {
|
||
lv_draw_label_dsc_t ld;
|
||
|
||
lv_draw_label_dsc_init(&ld);
|
||
ld.base.layer = layer;
|
||
ld.text = s_digit_str[digit];
|
||
ld.text_static = 1;
|
||
ld.color = color;
|
||
ld.ofs_x = (PW_BOX_W - lv_text_get_width(s_digit_str[digit], 1, LV_FONT_DEFAULT, 0)) / 2;
|
||
ld.ofs_y = 0;
|
||
lv_draw_label(layer, &ld, box);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:页面自绘(3 个数字框:光标框反显,其余白底黑框)
|
||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||
* clip - 重绘区 const lv_area_t *
|
||
* 返回值:无
|
||
* 限定条件:页面打开(框架绘制链末尾调用)
|
||
* 函数说明:标题/页脚由框架处理(每页 title_y、"浅 V"/页脚已关);本模块只绘数字框;
|
||
* 1bpp 极性:white=暗、black=亮——光标框反显(white 底 black 字),
|
||
* 其余框亮底暗字 + 1px 暗边
|
||
*/
|
||
static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
|
||
for (uint32_t i = 0u; i < PW_DIGITS; i++) {
|
||
lv_area_t box;
|
||
lv_draw_rect_dsc_t fd;
|
||
|
||
box.x1 = (int32_t)(PW_BOX_X0 + i * (PW_BOX_W + PW_BOX_GAP));
|
||
box.x2 = (int32_t)(box.x1 + PW_BOX_W - 1u);
|
||
box.y1 = PW_BOX_Y;
|
||
box.y2 = (int32_t)(PW_BOX_Y + PW_BOX_H - 1u);
|
||
if (!pw_areas_intersect(&box, clip)) {
|
||
continue; /* 数字框不在重绘区:跳过(culling) */
|
||
}
|
||
|
||
lv_draw_rect_dsc_init(&fd);
|
||
fd.base.layer = layer;
|
||
fd.bg_opa = LV_OPA_COVER;
|
||
if (i == s_cur) {
|
||
/* 光标框:反显(1bpp:white=暗底,black=亮字) */
|
||
fd.bg_color = UI_COL_SEL_BG;
|
||
}
|
||
else {
|
||
/* 非光标框:亮底暗字 + 1px 暗边(1bpp:black=亮底,white=暗) */
|
||
fd.bg_color = UI_COL_BG;
|
||
fd.border_color = UI_COL_FG;
|
||
fd.border_width = 1;
|
||
}
|
||
lv_draw_rect(layer, &fd, &box);
|
||
pw_draw_digit(layer, &box, s_digits[i], (i == s_cur) ? UI_COL_SEL_FG : UI_COL_FG);
|
||
}
|
||
|
||
if (s_err != 0u) {
|
||
/* "密码输入错误"提示(水平居中;1bpp:white=暗墨) */
|
||
lv_draw_label_dsc_t ld;
|
||
/* 本 LVGL 版 lv_text_get_width:length=0 = 不测量(返回 0)、length≠0 时
|
||
* while(i<length) 无 NUL 检查——必须传 strlen,传 -1 会越串尾死循环 */
|
||
int32_t w = lv_text_get_width("密码输入错误", (uint32_t)strlen("密码输入错误"), LV_FONT_DEFAULT, 0);
|
||
|
||
if (w > 0) {
|
||
lv_area_t err_box;
|
||
|
||
err_box.x1 = (UI_SCREEN_W - w) / 2;
|
||
err_box.x2 = (int32_t)(err_box.x1 + w - 1u);
|
||
err_box.y1 = PW_ERR_Y;
|
||
err_box.y2 = (int32_t)(err_box.y1 + UI_TEXT_BAND_H); /* 覆盖字体行高(13)+ 余量(同框架约定) */
|
||
if (pw_areas_intersect(&err_box, clip)) {
|
||
lv_draw_label_dsc_init(&ld);
|
||
ld.base.layer = layer;
|
||
ld.text = "密码输入错误";
|
||
ld.text_static = 1;
|
||
ld.color = UI_COL_FG;
|
||
ld.ofs_x = 0;
|
||
ld.ofs_y = 0;
|
||
lv_draw_label(layer, &ld, &err_box);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:页面按键处理(表单页:键全由页面处理)
|
||
* 入口参数:key - LVGL 键值 lv_key_t
|
||
* 返回值:true - 页已处理(框架跳过内置语义);false - 未处理
|
||
* 限定条件:页面打开;经框架键分发调用(uiTask 上下文)
|
||
* 函数说明: 1. ↑/↓:调整光标位数字(+1/-1,9↔0 环绕);错误提示态下先清除提示
|
||
* 2. ←/→:移动光标(首↔末环绕;← 键经框架拦截标志转入本函数);同上清提示
|
||
* 3. ENTER:修改密码 → 完成回调(授修改权限);查看密码 → 完成回调
|
||
* (授只读权限);不一致 → 清空重输 + 显示"密码输入错误"
|
||
* 4. ESC 不经本函数(框架最优先恒关页);其余键值未处理
|
||
*/
|
||
static bool pw_key_cb(lv_key_t key) {
|
||
if (key != LV_KEY_ENTER && s_err != 0u) {
|
||
s_err = 0u; /* 错误提示态:方向键先清除提示,再正常执行该键(重新输入) */
|
||
}
|
||
|
||
switch (key) {
|
||
case LV_KEY_UP:
|
||
s_digits[s_cur] = (uint8_t)((s_digits[s_cur] + 1u) % 10u); /* 值 +1,9 环绕 0 */
|
||
break;
|
||
case LV_KEY_DOWN:
|
||
s_digits[s_cur] = (uint8_t)((s_digits[s_cur] + 9u) % 10u); /* 值 -1,0 环绕 9 */
|
||
break;
|
||
case LV_KEY_LEFT:
|
||
s_cur = (uint8_t)((s_cur + PW_DIGITS - 1u) % PW_DIGITS); /* 光标左移,首绕末 */
|
||
break;
|
||
case LV_KEY_RIGHT:
|
||
s_cur = (uint8_t)((s_cur + 1u) % PW_DIGITS); /* 光标右移,末绕首 */
|
||
break;
|
||
case LV_KEY_ENTER: {
|
||
uint16_t input = (uint16_t)((uint32_t)s_digits[0] * 100u
|
||
+ (uint32_t)s_digits[1] * 10u
|
||
+ (uint32_t)s_digits[2]);
|
||
uint8_t perm;
|
||
|
||
if (input == (uint16_t)UI_PWD_VALUE_MODIFY) {
|
||
perm = (uint8_t)UI_PERM_MODIFY; /* 修改密码:授修改权限(先查,两值同设时授高权) */
|
||
}
|
||
else if (input == (uint16_t)UI_PWD_VALUE_VIEW) {
|
||
perm = (uint8_t)UI_PERM_VIEW; /* 查看密码:授只读权限 */
|
||
}
|
||
else {
|
||
/* 不一致:清空重输(3 框回 0、光标回首框)+ 显示错误提示 */
|
||
s_digits[0] = 0u;
|
||
s_digits[1] = 0u;
|
||
s_digits[2] = 0u;
|
||
s_cur = 0u;
|
||
s_err = 1u;
|
||
break;
|
||
}
|
||
|
||
/* 一致:逻辑层接管"关页 + 按权限续进入"(回调内自行关页) */
|
||
if (s_on_ok != NULL) {
|
||
s_on_ok(perm);
|
||
}
|
||
else {
|
||
ui_content_page_close(); /* 防御:无回调 → 仅关页 */
|
||
}
|
||
return true; /* 页面已关(或已切走),无需重绘 */
|
||
}
|
||
default:
|
||
return false; /* 未处理:落回框架内置语义 */
|
||
}
|
||
|
||
ui_content_page_invalidate(); /* 状态已变:触发整页重绘 */
|
||
return true;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:装载完成回调(须在开页前调用)
|
||
* 入口参数:on_ok - 完成回调(NULL = 一致时仅关页);一致后以授予的权限为实参
|
||
* 返回值:无
|
||
* 限定条件:由逻辑层在 uiTask 上下文、开页前调用
|
||
* 函数说明:密码页比对 ui_pwd_def.h 的两个编译期密码值(无需载值);
|
||
* 每次进入重置为初始态(全 0、光标首框,与实机初始显示一致)
|
||
*/
|
||
void ui_page_password_arm(void (*on_ok)(uint8_t perm)) {
|
||
s_on_ok = on_ok;
|
||
s_digits[0] = 0u;
|
||
s_digits[1] = 0u;
|
||
s_digits[2] = 0u;
|
||
s_cur = 0u;
|
||
s_err = 0u;
|
||
}
|
||
|
||
/*
|
||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 `# @password=password` 指令出口引用):
|
||
* 表单/操作页——selectable=0、无行数据,自绘内容 + 自有键;
|
||
* 标题/装饰/页脚跟随实机(无"浅 V"、标题中上、无页脚);
|
||
* key_intercept=1:← 键先由 key_cb 处理(移光标),ESC 恒不可拦截
|
||
*/
|
||
const content_page_def_t ui_page_password_desc = {
|
||
.id = "password",
|
||
.title = "用户密钥",
|
||
.title_y = PW_TITLE_Y,
|
||
.hide_deco = 1u,
|
||
.hide_footer = 1u,
|
||
.selectable = 0u,
|
||
.key_intercept = 1u,
|
||
.draw_cb = pw_draw_cb,
|
||
.key_cb = pw_key_cb,
|
||
};
|