Files
STM32F4-Base/Drivers/BSP/UI/ui_standby.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

344 lines
11 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 Standby (待机显示页)
* 模块功能:长时间无按键操作时自动切入的默认界面(6 路电流值 + 分隔线 + 底部时钟);
* 屏幕覆盖层模式——进入保存前屏与焦点、ENTER(确认键)还原,其余键忽略;
* 空闲检测 = lv_timer(1s 周期),活动通知由菜单/内容页键事件挂钩
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-14
* 修改记录:
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §5.2)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_standby.h"
#include "lvgl.h"
#include "ui_menu_view.h"
#include "ui_style.h"
#include "ui_tokens.h"
#define DBG_TAG "[UI_STANDBY]"
#include "dbg_log.h"
/* 私有宏定义区 */
/*
* 布局常量(设备像素;参考 mockups/待机显示页面.jpg):
* 屏幕宽高取令牌 UI_SCREEN_W/H;6 路电流值行自 y=28 起、行距 18
* (末行 y=118..130);分隔线 y=132;底部时钟 y=140 居中
*/
#define SB_CH_COUNT 6u /* 电流通道数 */
#define SB_ROW_Y0 28 /* 首数据行顶 y */
#define SB_PITCH 18 /* 行距(相邻行槽顶间距) */
#define SB_LABEL_X 24 /* 通道名 x(Ib/Ic/...) */
#define SB_EQ_X 72 /* "=" 号 x */
#define SB_VALUE_X 96 /* 数值 x(0.00A;v1 占位,待交流量标定管线接入实时值) */
#define SB_LINE_Y 132 /* 分隔线 y(高 1px) */
#define SB_TIME_Y 140 /* 底部时钟放置 y(居中) */
/* 静态变量定义区 */
/* 通道名与占位数值(Flash;全 ASCII,无需中文字形) */
static const char *const s_ch_name[SB_CH_COUNT] = { "Ib", "Ic", "Io", "Ia2", "Ib2", "Ic2" };
static const char *const s_ch_value[SB_CH_COUNT] = { "0.00A", "0.00A", "0.00A", "0.00A", "0.00A", "0.00A" };
static lv_obj_t *s_scr; /* 待机屏(init 时建、常驻不销毁) */
static lv_obj_t *s_cont; /* 全屏容器(焦点/键入口 + 自绘载体,无子对象) */
static uint32_t s_timeout_ms; /* 空闲超时阈值 */
static uint32_t s_last_ms; /* 最近一次用户活动时刻(lv_tick ms) */
static bool s_active; /* 是否处于待机屏 */
static lv_obj_t *s_prev_scr; /* 进入待机前的活动屏(ENTER 还原用) */
static lv_obj_t *s_prev_focus; /* 进入待机前组内焦点对象 */
static ui_standby_time_cb s_time_cb;
/* flat 样式(透明、无边框/圆角/内边距,剥离 mono 主题 card)取自共享样式表
* ui_style.c:与 ui_content_page / ui_menu_view 共用同一 const 对象,不再本地重复定义 */
/* 函数定义区 */
/*
* 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头)
* 入口参数:a - 区域 1 const lv_area_t *
* b - 区域 2 const lv_area_t *
* 返回值:true - 相交;false - 不相交
*/
static bool sb_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 *
* text - 文本(栈缓冲,同帧有效) const char *
* y - 放置 y int32_t
*/
static void sb_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y) {
lv_area_t area;
lv_draw_label_dsc_t ld;
area.x1 = 0;
area.x2 = UI_SCREEN_W - 1;
area.y1 = y;
area.y2 = y + UI_TEXT_BAND_H; /* 覆盖字体行高(13)+ 余量 */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = text;
ld.color = UI_COL_FG;
ld.ofs_x = (UI_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &area);
}
/*
* 函数功能:待机屏自绘(6 路电流值 + 分隔线 + 底部时钟;逐行 culling)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,待机模块已初始化
* 函数说明:1bpp 极性 white=暗墨——文本/分隔线均白色;数值 v1 占位 0.00A
*/
static void sb_draw_cb(lv_event_t *e) {
lv_layer_t *layer = lv_event_get_layer(e);
for (uint32_t i = 0u; i < SB_CH_COUNT; i++) {
int32_t row_y = SB_ROW_Y0 + (int32_t)i * SB_PITCH;
lv_area_t strip;
lv_draw_label_dsc_t ld;
strip.x1 = 0;
strip.x2 = UI_SCREEN_W - 1;
strip.y1 = row_y;
strip.y2 = (int32_t)(row_y + SB_PITCH - 1u);
if (!sb_areas_intersect(&strip, &layer->_clip_area)) {
continue; /* 行不在重绘区:跳过(culling) */
}
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = s_ch_name[i];
ld.text_static = 1;
ld.color = UI_COL_FG;
ld.ofs_x = SB_LABEL_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &strip);
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = "=";
ld.text_static = 1;
ld.color = UI_COL_FG;
ld.ofs_x = SB_EQ_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &strip);
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = s_ch_value[i];
ld.text_static = 1;
ld.color = UI_COL_FG;
ld.ofs_x = SB_VALUE_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &strip);
}
/* 分隔线(高 1px) */
{
lv_area_t line;
lv_draw_rect_dsc_t fd;
line.x1 = 8;
line.x2 = UI_SCREEN_W - 9;
line.y1 = SB_LINE_Y;
line.y2 = SB_LINE_Y;
if (sb_areas_intersect(&line, &layer->_clip_area)) {
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = UI_COL_FG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &line);
}
}
/* 底部时钟(每秒重绘;time_cb 平台提供) */
{
lv_area_t t;
t.x1 = 0;
t.x2 = UI_SCREEN_W - 1;
t.y1 = SB_TIME_Y;
t.y2 = (int32_t)(SB_TIME_Y + UI_TEXT_BAND_H);
if (sb_areas_intersect(&t, &layer->_clip_area)) {
char ts[24];
s_time_cb(ts, sizeof(ts));
sb_draw_centered_text(layer, ts, SB_TIME_Y);
}
}
}
/*
* 函数功能:进入待机屏(保存前屏与焦点 → 聚焦容器并切屏)
* 入口参数:无
* 返回值:无
* 限定条件:UI 任务上下文;未处于待机
*/
static void sb_enter(void) {
lv_group_t *g;
if (s_active || s_scr == NULL) {
return;
}
s_prev_scr = lv_screen_active();
g = lv_group_get_default();
s_prev_focus = (g != NULL) ? lv_group_get_focused(g) : NULL;
if (g != NULL) {
lv_group_add_obj(g, s_cont); /* 幂等:已在组内则忽略 */
lv_group_focus_obj(s_cont); /* 先获焦再切屏,按键立即由本模块接管 */
}
lv_screen_load(s_scr);
s_active = true;
DBG_INFO("standby: enter");
}
/*
* 函数功能:退出待机屏(还原前屏与焦点;重置空闲计时)
* 入口参数:无
* 返回值:无
* 限定条件:UI 任务上下文;处于待机
*/
static void sb_exit(void) {
if (!s_active) {
return;
}
s_active = false;
s_last_ms = lv_tick_get();
if (s_prev_focus != NULL && lv_obj_is_valid(s_prev_focus)) {
lv_group_focus_obj(s_prev_focus);
}
else {
/* 前焦点已失效(如内容页被销毁):回落菜单容器 */
lv_obj_t *mc = ui_menu_view_container();
if (mc != NULL) {
lv_group_focus_obj(mc);
}
}
if (s_prev_scr != NULL && lv_obj_is_valid(s_prev_scr)) {
lv_screen_load(s_prev_scr);
}
DBG_INFO("standby: exit");
}
/*
* 函数功能:待机屏按键事件(仅 ENTER 退出;其余键忽略但重置空闲计时)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:容器已获焦
*/
static void sb_key_cb(lv_event_t *e) {
lv_key_t key = (lv_key_t)lv_event_get_key(e);
ui_standby_notify_activity();
if (key == LV_KEY_ENTER) {
sb_exit(); /* 确认键:返回进入前的界面(菜单/内容页原样还原) */
}
}
/*
* 函数功能:空闲检测定时器回调(1s 周期;待机中同时驱动时钟每秒重绘)
* 入口参数:t - 定时器句柄 lv_timer_t *
* 返回值:无
*/
static void sb_idle_timer_cb(lv_timer_t *t) {
(void)t;
if (!s_active && s_timeout_ms > 0u && lv_tick_elaps(s_last_ms) >= s_timeout_ms) {
sb_enter();
}
else if (s_active) {
lv_obj_invalidate(s_cont); /* 底部时钟每秒刷新 */
}
}
/*
* 函数功能:初始化待机模块(建常驻屏/容器 + 空闲检测定时器)
* 入口参数:idle_timeout_ms - 无操作超时阈值(ms;0 = 拒绝初始化) uint32_t
* time_cb - 底部时钟字符串提供回调(不可为 NULL) ui_standby_time_cb
* 返回值:0 - 成功;-1 - 失败(已初始化/参数无效/LVGL 池耗尽) int
*/
int ui_standby_init(uint32_t idle_timeout_ms, ui_standby_time_cb time_cb) {
if (s_scr != NULL || idle_timeout_ms == 0u || time_cb == NULL) {
return -1;
}
s_timeout_ms = idle_timeout_ms;
s_time_cb = time_cb;
s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕(常驻,不随切屏销毁) */
if (s_scr == NULL) {
return -1; /* LVGL 池耗尽 */
}
s_cont = lv_obj_create(s_scr);
if (s_cont == NULL) {
lv_obj_delete(s_scr);
s_scr = NULL;
return -1;
}
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_size(s_cont, UI_SCREEN_W, UI_SCREEN_H);
lv_obj_add_style(s_cont, &ui_style_flat, 0);
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE);
lv_obj_add_event_cb(s_cont, sb_key_cb, LV_EVENT_KEY, NULL);
lv_obj_add_event_cb(s_cont, sb_draw_cb, LV_EVENT_DRAW_MAIN, NULL);
s_last_ms = lv_tick_get();
if (lv_timer_create(sb_idle_timer_cb, 1000u, NULL) == NULL) {
lv_obj_delete(s_cont);
lv_obj_delete(s_scr);
s_scr = NULL;
s_cont = NULL;
return -1;
}
DBG_INFO("standby: ready (idle=%ums)", (unsigned)idle_timeout_ms);
return 0;
}
/*
* 函数功能:通知用户活动(重置空闲计时)
* 入口参数:无
* 返回值:无
*/
void ui_standby_notify_activity(void) {
if (s_scr != NULL) {
s_last_ms = lv_tick_get();
}
}
/*
* 函数功能:查询是否处于待机屏
* 入口参数:无
* 返回值:true - 待机中;false - 否
*/
bool ui_standby_is_active(void) {
return s_active;
}
/*
* 函数功能:强制进入待机屏(测试钩子,绕过空闲计时)
* 入口参数:无
* 返回值:无
*/
void ui_standby_force_enter(void) {
sb_enter();
}