Files
STM32F4-Base/Drivers/BSP/UI/ui_page_password.c
T
wandering1 dafc18f5ed feat(UI): W6 第一波——设计令牌层、页面注册表生成器与 UI 门禁补强
启动 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 改同一批文件、文件内
交叠,硬切会产生不可编译的中间态,故整波一笔。
2026-10-20 21:14:14 +08:00

276 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称: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,
};