/* * 模块名称:UI Standby (待机显示页) * 模块功能:长时间无按键操作时自动切入的默认界面(6 路电流值 + 分隔线 + 底部时钟); * 屏幕覆盖层模式——进入保存前屏与焦点、ENTER(确认键)还原,其余键忽略; * 空闲检测 = lv_timer(1s 周期),活动通知由菜单/内容页键事件挂钩 * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 * 创建日期:2026-09-14 * 修改记录: * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §5.2) */ /* 头文件包含区 */ #include #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(); }