274 lines
12 KiB
C
274 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"
|
||
|
||
/* 私有宏定义区 */
|
||
|
||
/*
|
||
* 布局常量(设备像素;实机参考 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(数字框行之下) */
|
||
#define PW_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致;提示文本居中用) */
|
||
|
||
/* 静态变量定义区 */
|
||
|
||
/* 单位数字文本(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 = lv_color_white();
|
||
}
|
||
else {
|
||
/* 非光标框:亮底暗字 + 1px 暗边(1bpp:black=亮底,white=暗) */
|
||
fd.bg_color = lv_color_black();
|
||
fd.border_color = lv_color_white();
|
||
fd.border_width = 1;
|
||
}
|
||
lv_draw_rect(layer, &fd, &box);
|
||
pw_draw_digit(layer, &box, s_digits[i], (i == s_cur) ? lv_color_black() : lv_color_white());
|
||
}
|
||
|
||
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 = (PW_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 + 15u); /* 覆盖字体行高(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 = lv_color_white();
|
||
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;
|
||
}
|
||
|
||
/*
|
||
* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用):
|
||
* 表单/操作页——selectable=0、无行数据,自绘内容 + 自有键;
|
||
* 标题/装饰/页脚跟随实机(无"浅 V"、标题中上、无页脚);
|
||
* key_intercept=1:← 键先由 key_cb 处理(移光标),ESC 恒不可拦截
|
||
*/
|
||
const content_page_def_t ui_page_password_desc = {
|
||
.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,
|
||
};
|