Files
STM32F4-Base/Drivers/BSP/UI/ui_content_page.c
T
wandering1 a5142c6d9d UI 内容页框架加实时页自动刷新机制(交流量页启用,300ms)
- ui_content_page:描述符尾部追加 auto_ms/auto_seq 可选字段(缺省=0/NULL=
  旧行为逐位不变);页打开期间框架持一只 lv_timer,仅当发布序号变过才整页
  失效(零空转失效),关页即销;新 API ui_content_page_auto_poll
- ui_page_ac.c:.auto_ms=300(≥200ms 发布节拍、避 400=2×200 锁相、300=
  15×20ms LVGL 整刷周期;最坏屏上滞后 500ms;完整取值账目在代码注释)
- simulator:rt 帧装配收口 sim_fill_rt_frame(n=0 冻结帧保 74 张黄金逐字节
  一致,n≥1 确定性斜坡);新增 --rtlive 两态自检 + 窗口模式 200ms 节拍发布;
  check_content_pages.py 增 [rtlive] 零按键 A≠B 落屏断言
- docs/UI设计:供给说明新增 §5.5 自动刷新口径;内容页架构 §3.1/§四/§九 同步
  (并修正 s_open 判定旧描述)
- 台账:Keil 构建后按固定顺序回填(Code=256,588 / RW_IRAM1 +8B / UI +8B)

验证:模拟器 0错0警;菜单 8 状态 + 内容页 74 张 + rtlive(A≠B 30 字节)PASS
(lead 独立复跑 RC=0);Keil 0错0警;mem-budget --gate / mem-numbers --scan 绿。
2026-11-13 14:23:06 +08:00

679 lines
30 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 Content Page (内容页框架)
* 模块功能:菜单叶子节点进入的内容页(独立全屏页面)框架——全屏容器
* 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码),无子对象;
* 只持机制:屏/容器按需建毁、焦点、按键分发、描述符驱动通用绘制;
* 不持任何页面数据——各页数据/几何/钩子由 ui_page_xxx 模块经
* content_page_def_t 提供,框架按**描述符指针**开页;"哪个叶子进哪一页"
* 的绑定在生成物 ui_page_registry.c(唯一真源 = ui_menu_tree.txt 的
* #page= 注记;架构见 内容页架构.md);
* 进入时切屏并获焦、ESC 返回菜单屏并还原焦点;
* 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-09
* 修改记录:
* 2026-09-09 王建锋 创建:装置信息/交流量/直流量三页(参考图 10/20/21 实测几何)
* 2026-09-09 王建锋 "浅 V"装饰几何提取至共享 ui_deco 模块(不再本地重复定义)
* 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入
* ui_page_xxx 模块,本文件只留机制 + 描述符表;绘制/按键改
* 按描述符驱动(静态表/refresh + 4 钩子),行为零变化
* 2026-09-09 王建锋 密码页(用户密钥):绘制链支持每页 title_y/"浅 V"/页脚开关;
* 按键分发支持每页 ← 拦截(ESC 恒不可拦截);新 API
* ui_content_page_invalidate(表单页触发整页重绘)
* 2026-09-09 王建锋 权限模型:open(id, perm) 记录授予权限(页面经
* ui_content_page_is_editable 查询:edit_capable 且修改权限)
* 2026-09-10 王建锋 网口设置页(node 30):新增 CONTENT_PAGE_NET_CFG 入描述符表;
* 新 API ui_content_page_selected_row(页面钩子感知选中行)+
* 描述符 init_cb(开页后调一次,页面模块重置运行态)
* 2026-09-13 王建锋 cp_draw_cb 按区块拆为四个绘制子函数(cp_draw_deco/header/rows/footer),
* 主函数仅编排(行数取一次供行窗口/页脚共用),行为零变化
* 2026-10-15 王建锋 UI 设计令牌层:屏幕/骨架/行距/缓冲/1bpp 语义色字面量收进
* ui_tokens.h(纯替换,像素逐位一致)
* 2026-10-15 王建锋 UI 页面原型化:删 content_page_id_t 枚举与 s_defs[],open() 改收
* 描述符指针、s_open 变指针(NULL = 未打开);7 个页面头包含移交
* ui_page_registry.c;日志页身份改打描述符 id(ASCII 页名,比原
* id=%u 更可定位);绘制/按键/几何一律未动
* 2026-11-02 DTU 项目组 新增 ui_content_page_current_desc(#69 板端导航身份查询:
* 页 id 从"只有串口开/关沿可见"升级为可按帧查询)
* 2026-11-13 DTU 项目组 实时页自动刷新:描述符 auto_ms>0 的页在打开期间由框架
* 持一只 lv_timer 轮询发布序号(默认通道一 ui_data_rt_seq),
* 序号变过才整页失效;新 API ui_content_page_auto_poll
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_content_page.h"
#include "ui_standby.h"
#include "ui_pwd_def.h"
#include "ui_menu_view.h"
#include "ui_deco.h"
#include "ui_style.h"
#include "ui_tokens.h"
#include "ui_data.h"
#include "lvgl.h"
#define DBG_TAG "[UI_CONTENT_PAGE]"
#include "dbg_log.h"
/* 私有宏定义区 */
/*
* 布局取值全部来自 ui_tokens.h(L0 设计令牌层,本文件不再自带同值字面量):
* 屏幕 UI_SCREEN_W/H、标题 UI_TITLE_Y、页脚 UI_PAGE_LINE_Y、文本条带
* UI_TEXT_BAND_H、行缓冲 UI_BUF_STRLEN、选中带内缩 UI_SEL_BAND_INSET_L/R、
* 页脚左起 UI_FOOTER_X、语义色 UI_COL_FG/BG/SEL_FG/SEL_BG。
* 几何实测来源仍是参考图 10/20/21(3x 放大图缩回 160x160 测量):标题 y=2
* (CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143;字形顶距
* (CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐
*/
/* 静态变量定义区 */
/*
* 当前打开的页:直接持页面描述符指针(NULL = 未打开)。
* 页身份由调用方给出(逻辑层经 ui_page_registry_for / _password 取描述符),
* 框架不再维护"页 ID 枚举 ↔ s_defs 下标 ↔ 菜单节点"三处必须手工对齐的映射
* ——那正是旧 menu_content_page_for() 手写 case 的错源(表序一变即错位)。
*/
static const content_page_def_t *s_open = NULL;
static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续) */
static uint8_t s_perm; /* 本次授予权限(ui_perm_t;开页时装载,关页重置) */
static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */
static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */
static lv_timer_t *s_auto_timer; /* 自动失效轮询定时器(仅 auto_ms>0 的页打开期间存在;关页即销) */
static uint32_t s_auto_seq; /* 上一拍见到的发布序号(开页时装基准,开页本身不多刷一帧) */
/* 容器样式:flat(透明 + 无边框/圆角/内边距)取自共享样式表 ui_style.c——
* 与 ui_menu_view / ui_standby 共用同一 const 对象(原先三处各定义一份逐字相同
* 的 9 项属性数组);常量样式零 LV_MEM 池分配的纪律见 ui_style.h */
/* 函数定义区 */
/*
* 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头)
* 入口参数:a - 区域 1 const lv_area_t *
* b - 区域 2 const lv_area_t *
* 返回值:true - 相交;false - 不相交
* 限定条件:无
* 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_menu_view.c 同实现)
*/
static bool cp_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);
}
/*
* 函数功能:取页面总行数(静态页 = 描述符 row_count,动态页 = count_cb)
* 入口参数:d - 页面描述符 const content_page_def_t *
* 返回值:总行数 uint16_t
* 限定条件:d 非空
* 函数说明:行数唯一入口;绘制(窗口/页脚)与按键(移选取模)均经此取
*/
static uint16_t cp_row_total(const content_page_def_t *d) {
if (d->count_cb != NULL) {
return d->count_cb();
}
return d->row_count;
}
/*
* 函数功能:失效当前打开的页面容器(触发整页重绘)
* 入口参数:无
* 返回值:无
* 限定条件:页面打开时(s_cont 非空)
* 函数说明:供页面模块(表单/操作页)自身状态变化后调用;无页打开时空操作
*/
void ui_content_page_invalidate(void) {
if (s_cont != NULL) {
lv_obj_invalidate(s_cont);
}
}
/*
* 函数功能:绘制居中单行文本(标题/副标题)
* 入口参数:layer - 绘制层 lv_layer_t *
* text - 文本(Flash) const char *
* y - 放置 y int32_t
* 返回值:无
* 限定条件:text 非空;重绘区已含该行区域(调用方 culling)
* 函数说明:x = (屏宽 - 文本实际步进)/2 居中;白色、静态字符串不拷内存
*/
static void cp_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.text_static = 1; /* Flash 字符串:不拷内存 */
ld.color = UI_COL_FG;
/* 本 LVGL 版 lv_text_get_width:length=0 表示"不测量"(返回 0);
* length≠0 时循环无 NUL 检查,传 -1 会越串尾死循环——必须传 strlen */
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);
}
/*
* 函数功能:绘制"浅 V"装饰(共享 ui_deco 模块,与菜单根框同几何)
* 入口参数:layer - 绘制层 lv_layer_t *
* d - 页面描述符 const content_page_def_t *
* 返回值:无
* 限定条件:页面已打开
* 函数说明:每页可关(hide_deco;密码页跟随实机:无装饰)
*/
static void cp_draw_deco(lv_layer_t *layer, const content_page_def_t *d) {
if (d->hide_deco == 0u) {
ui_deco_draw(layer, &layer->_clip_area);
}
}
/*
* 函数功能:绘制标题/副标题(居中;整宽带 + culling)
* 入口参数:layer - 绘制层 lv_layer_t *
* d - 页面描述符 const content_page_def_t *
* 返回值:无
* 限定条件:页面已打开
* 函数说明:标题放置 y 每页可配(title_y,0 = 框架默认 UI_TITLE_Y);
* 副标题仅信息页(subtitle 非空);重绘区不相交则跳过区块
*/
static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) {
lv_area_t t;
/* 标题(居中;放置 y 每页可配,0 = 框架默认) */
{
int32_t ty = (d->title_y != 0) ? d->title_y : UI_TITLE_Y;
t.x1 = 0;
t.x2 = UI_SCREEN_W - 1;
t.y1 = ty;
t.y2 = ty + UI_TEXT_BAND_H;
if (cp_areas_intersect(&t, &layer->_clip_area)) {
cp_draw_centered_text(layer, d->title, ty);
}
}
/* 副标题(仅信息页) */
if (d->subtitle != NULL) {
t.x1 = 0;
t.x2 = UI_SCREEN_W - 1;
t.y1 = d->subtitle_y;
t.y2 = d->subtitle_y + UI_TEXT_BAND_H;
if (cp_areas_intersect(&t, &layer->_clip_area)) {
cp_draw_centered_text(layer, d->subtitle, d->subtitle_y);
}
}
}
/*
* 函数功能:绘制行数据(列表页窗口跟随选中行所在页,信息页全量;逐行 culling)
* 入口参数:layer - 绘制层 lv_layer_t *
* d - 页面描述符 const content_page_def_t *
* nrows - 总行数 int32_t(调用方取一次,与页脚同帧共用)
* 返回值:无
* 限定条件:页面已打开
* 函数说明: 1. 列表页窗口顶行 = (sel/每页行数)*每页行数,末页不足整页截断
* 2. 行条带(整宽)不在重绘区则跳过(culling)
* 3. 选中行:整行白带(x=2..156,高 band_h)+ 黑字,非选中白字
* 4. 行文本:静态 = Flash 表(不拷内存);动态 = refresh() 逐行取
* 入栈缓冲(同帧同步光栅化,安全)
*/
static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) {
int32_t first = 0;
int32_t cnt = nrows;
if (d->selectable != 0u && d->rows_per_page > 0u) {
first = (int32_t)(s_sel / d->rows_per_page) * d->rows_per_page; /* 窗口顶行 */
cnt = nrows - first;
if (cnt > (int32_t)d->rows_per_page) {
cnt = (int32_t)d->rows_per_page; /* 末页不足整页时截断 */
}
}
bool dynamic = (d->refresh != NULL);
char dyn_name[UI_BUF_STRLEN]; /* 动态行栈缓冲(同帧同步光栅化) */
char dyn_value[UI_BUF_STRLEN];
for (int32_t i = 0; i < cnt; i++) {
int32_t row_idx = first + i; /* 数据下标(跨页连续) */
int32_t slot_y = d->top_y + i * d->pitch; /* 行槽顶(窗口内位置固定) */
bool is_sel = (d->selectable != 0u) && (row_idx == (int32_t)s_sel);
lv_color_t color = is_sel ? UI_COL_SEL_FG : UI_COL_FG;
lv_area_t strip;
/* 行条带(整宽)不在重绘区:跳过(culling) */
strip.x1 = 0;
strip.x2 = UI_SCREEN_W - 1;
strip.y1 = slot_y;
strip.y2 = slot_y + d->pitch - 1;
if (!cp_areas_intersect(&strip, &layer->_clip_area)) {
continue;
}
/* 选中行:整行白带(x=2..156,高 band_h) */
if (is_sel && d->band_h > 0) {
lv_draw_rect_dsc_t fd;
lv_area_t band;
band.x1 = UI_SEL_BAND_INSET_L;
band.x2 = UI_SCREEN_W - UI_SEL_BAND_INSET_R; /* x=156(与参考图实测一致) */
band.y1 = slot_y;
band.y2 = slot_y + d->band_h - 1;
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = UI_COL_SEL_BG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &band);
}
/* 行文本:静态 = Flash 表(不拷内存);动态 = refresh 逐行取(栈缓冲) */
const char *name;
const char *value;
if (dynamic) {
d->refresh((uint16_t)row_idx, dyn_name, dyn_value);
name = dyn_name;
value = dyn_value;
}
else {
name = d->rows[row_idx].name;
value = d->rows[row_idx].value;
}
{
lv_draw_label_dsc_t ld;
/* ofs_x/ofs_y 相对 coords 原点(strip.y1=slot_y),勿再叠加 slot_y */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = name;
ld.text_static = (uint32_t)(!dynamic); /* 静态不拷;动态缓冲同帧内有效 */
ld.color = color; /* 选中黑 / 非选中白 */
ld.ofs_x = d->left_x;
ld.ofs_y = d->text_dy;
lv_draw_label(layer, &ld, &strip);
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = value;
ld.text_static = (uint32_t)(!dynamic);
ld.color = color;
ld.ofs_x = d->right_x;
ld.ofs_y = d->text_dy;
lv_draw_label(layer, &ld, &strip);
}
}
}
/*
* 函数功能:绘制页脚"第 X 页,共 Y 页"
* 入口参数:layer - 绘制层 lv_layer_t *
* d - 页面描述符 const content_page_def_t *
* nrows - 总行数 int32_t(调用方取一次,与行窗口同帧同值)
* 返回值:无
* 限定条件:页面已打开
* 函数说明:列表页随选中行现算,信息页按参考图固定值;每页可关
* (hide_footer;密码页跟随实机)
*/
static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) {
char buf[UI_BUF_STRLEN]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */
uint8_t cur; /* 当前页码(1 基) */
uint8_t pages; /* 总页数 */
lv_area_t t; /* 页脚条带区域(整宽,放置于 UI_PAGE_LINE_Y) */
/* 页码计算,两种来源:
* - 列表页(selectable):随选中行现算——当前页 = 选中行/每页行数 + 1
* (选中行所在页);总页数 = ceil(nrows/每页行数),
* 写成经典向上取整 (n + p - 1) / p(nrows 恰为整页倍数时不多算一页);
* nrows 由调用方取一次、与行窗口共用,保证同帧页码与窗口一致
* - 信息/表单页(非 selectable):取参考图固定值(fixed_cur_page / fixed_total_pages) */
if (d->selectable != 0u) {
cur = (uint8_t)(s_sel / d->rows_per_page + 1u);
pages = (uint8_t)((nrows + (int32_t)d->rows_per_page - 1) / d->rows_per_page);
}
else {
cur = d->fixed_cur_page;
pages = d->fixed_total_pages;
}
/* 页脚条带:整宽,放置于 UI_PAGE_LINE_Y(143,字形顶距已计入放置 y,
* 墨迹与参考图对齐);高 15 = 字体行高 13 + 余量,整行文字一次覆盖 */
t.x1 = 0;
t.x2 = UI_SCREEN_W - 1;
t.y1 = UI_PAGE_LINE_Y;
t.y2 = UI_PAGE_LINE_Y + UI_TEXT_BAND_H;
/* 双重门控:① 本页开启页脚(hide_footer;密码页跟随实机:无页码)
* ② 页脚条带与重绘区相交(culling)——两者皆成立才格式化并绘制 */
if (d->hide_footer == 0u && cp_areas_intersect(&t, &layer->_clip_area)) {
/* lv_snprintf 而非 snprintf:MSVC 对 printf 族做格式串扫描,中文字面量会误报 C4819 */
lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)pages);
lv_draw_label_dsc_t ld;
/* ofs 相对 coords 原点(t.x1=0,t.y1=UI_PAGE_LINE_Y):
* 水平 6 = 左对齐、左留白 6px(与实机一致);垂直 0(顶距已计入放置 y) */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = buf; /* 栈缓冲:本页码每次现算,同帧内有效(同步光栅化) */
ld.color = UI_COL_FG; /* 页码恒白字(不像选中行那样反色) */
ld.ofs_x = UI_FOOTER_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &t);
}
}
/*
* 函数功能:页面自绘(编排:浅 V 装饰 + 标题/副标题 + 行数据 + 页脚页码 + 页面钩子)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:事件为 LV_EVENT_DRAW_MAIN,页面已打开
* 函数说明:行数取一次(cp_row_total)供行窗口与页脚页码共用(同帧同值);
* 各区块由子函数绘制(deco/header/rows/footer),均按 culling 区
* 跳过不在重绘区的部分;末尾调 draw_cb(页自定义内容,在内置区块之后)
*/
static void cp_draw_cb(lv_event_t *e) {
const content_page_def_t *d = s_open;
lv_layer_t *layer = lv_event_get_layer(e);
int32_t nrows = (int32_t)cp_row_total(d); /* 行数取一次:供行窗口与页脚页码共用 */
cp_draw_deco(layer, d);
cp_draw_header(layer, d);
cp_draw_rows(layer, d, nrows);
cp_draw_footer(layer, d, nrows);
/* 页面钩子:自定义内容(表单/操作页;在内置区块之后绘制) */
if (d->draw_cb != NULL) {
d->draw_cb(layer, &layer->_clip_area);
}
}
/*
* 函数功能:页面按键事件入口(↑↓移选 / ESC·←返回 / 页面钩子)
* 入口参数:e - 事件指针 lv_event_t *
* 返回值:无
* 限定条件:容器已加入默认输入组并获焦
* 函数说明: 1. ESC:恒关页返回菜单(框架保证,页面钩子不可拦截)
* 2. ←:页面置拦截标志(key_intercept)时先交 key_cb(true = 页已处理不关页),
* 否则关页返回(密码页光标 ←/→ 即由此处理)
* 3. 其余键:key_cb 钩子先调(返回 true = 页已处理,框架跳过内置)
* 4. 内置:↑/↓ 列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘;
* 信息页不响应;动态行数可能缩小,选中行越界时重置回首行
*/
static void cp_key_cb(lv_event_t *e) {
const content_page_def_t *d = s_open;
lv_key_t key = (lv_key_t)lv_event_get_key(e);
ui_standby_notify_activity(); /* 用户活动:重置待机空闲计时 */
/* ESC:关闭页面返回菜单屏(焦点/导航状态还原;恒不可拦截) */
if (key == LV_KEY_ESC) {
ui_content_page_close();
return;
}
/* ←:页面置拦截标志且钩子已处理则不关页(密码页光标 ←/→);否则照旧返回 */
if (key == LV_KEY_LEFT) {
if (d->key_intercept != 0u && d->key_cb != NULL && d->key_cb(key)) {
return;
}
ui_content_page_close();
return;
}
/* 其余键:页面钩子先调:true = 页已处理,框架跳过内置语义 */
if (d->key_cb != NULL && d->key_cb(key)) {
return;
}
switch (key) {
case LV_KEY_UP: {
uint16_t total = cp_row_total(d);
if (d->selectable != 0u && total > 0u) {
if (s_sel >= total) {
s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */
}
/* 上移、首尾环绕:先加 total 再减 1 保证无符号下不为负 */
s_sel = (uint16_t)((s_sel + total - 1u) % total);
lv_obj_invalidate(s_cont); /* 重绘(选中带/窗口/页码可能变化) */
}
break;
}
case LV_KEY_DOWN: {
uint16_t total = cp_row_total(d);
if (d->selectable != 0u && total > 0u) {
if (s_sel >= total) {
s_sel = 0u; /* 动态行数可能缩小:选中行越界重置回首行 */
}
s_sel = (uint16_t)((s_sel + 1u) % total); /* 下移、越尾绕回首行 */
lv_obj_invalidate(s_cont);
}
break;
}
default:
break; /* 其余键值忽略 */
}
}
/*
* 函数功能:自动刷新轮询一拍(契约见 ui_content_page.h;判定与序号基准只在这里一份)
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 上下文;无页打开 / 本页未订阅(auto_ms=0)时空操作
* 函数说明:不在此拷帧——失效后由框架重绘走页面 refresh 的 seq 门控取新帧(§5.4
* 页面私有缓存纪律不变),框架对数据内容零感知,只认序号
*/
void ui_content_page_auto_poll(void) {
const content_page_def_t *d = s_open;
uint32_t now;
if (d == NULL || d->auto_ms == 0u) {
return;
}
now = (d->auto_seq != NULL) ? d->auto_seq() : ui_data_rt_seq();
if (now == s_auto_seq) {
return; /* 数据没变:零失效(省电屏带宽纪律) */
}
s_auto_seq = now;
ui_content_page_invalidate(); /* 序号变过才整页失效 */
}
/*
* 函数功能:自动失效轮询定时器回调(薄封装;一切判定在 ui_content_page_auto_poll)
* 入口参数:t - 定时器对象(未用) lv_timer_t *
* 返回值:无
* 限定条件:lv_timer_handler 内(uiTask 上下文)——与其他 LVGL 调用同线程同串行点
* 函数说明:standby 期间内容页对象仍在(待机盖屏不关页),此时失效后台容器只是
* 多一次脏区合并,待机屏自身重绘节律不受影响(实测待机像素回归绿)
*/
static void cp_auto_timer_cb(lv_timer_t *t) {
(void)t;
ui_content_page_auto_poll();
}
/*
* 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器)
* 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password)
* const content_page_def_t *
* perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY)
* 返回值:true - 成功;false - 失败(desc 为 NULL / 已有页面打开 / 对象创建失败)
* 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在)
* 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象)
* 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管
* 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原
* 4. 记录授予权限(页面经 ui_content_page_is_editable 查询)
*/
bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm) {
if (s_open != NULL) {
return false; /* 已有页面打开(防御:页面按需建毁,同一时刻至多一页) */
}
if (desc == NULL) {
DBG_ERROR("page open: NULL descriptor (node has no bound page)");
return false; /* 调用方把未绑定内容页的叶子当页面开了:拒开并留痕,不崩溃 */
}
s_perm = perm; /* 本次授予权限(页面可编辑性 = edit_capable 且修改权限) */
s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕 */
if (s_scr == NULL) {
return false; /* LVGL 池耗尽 */
}
s_cont = lv_obj_create(s_scr);
if (s_cont == NULL) {
lv_obj_delete(s_scr); /* 回滚:屏已建、容器失败 */
s_scr = NULL;
return false;
}
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); /* 透明、无边框:剥离主题 card */
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); /* 可获焦:键事件入口 */
lv_obj_add_event_cb(s_cont, cp_key_cb, LV_EVENT_KEY, NULL);
lv_obj_add_event_cb(s_cont, cp_draw_cb, LV_EVENT_DRAW_MAIN, NULL);
s_open = desc;
s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */
if (desc->init_cb != NULL) {
desc->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */
}
/* 自动失效轮询(实时页自动刷新):定时器随页建销,页面模块零定时器。基准取
* "开页瞬间"的序号——开页首帧绘制本就已取过数,不该再为基线多补一次重绘;
* 之后每拍只在序号变过时失效。创建失败(LVGL 池耗尽)只降级回"按键才重绘"
* 旧行为并留痕,不挡开页(同 ui_w_setview 轮询定时器的降级口径)。 */
s_auto_timer = NULL;
if (desc->auto_ms != 0u) {
s_auto_seq = (desc->auto_seq != NULL) ? desc->auto_seq() : ui_data_rt_seq();
s_auto_timer = lv_timer_create(cp_auto_timer_cb, (uint32_t)desc->auto_ms, NULL);
if (s_auto_timer == NULL) {
DBG_ERROR("auto timer create failed (id=%s): key-only refresh fallback", desc->id);
}
}
/* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */
lv_group_t *g = lv_group_get_default();
if (g != NULL) {
lv_group_add_obj(g, s_cont);
lv_group_focus_obj(s_cont);
}
lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */
DBG_INFO("page open: id=%s rows=%u", desc->id, (unsigned)cp_row_total(desc));
return true;
}
/*
* 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面)
* 入口参数:无
* 返回值:无
* 限定条件:ui_content_page_is_open() 为 true
* 函数说明:1. 先移焦到菜单容器再删屏——防输入组焦点悬空(删除自动移出组)
* 2. 切回菜单屏后销毁页面屏幕,LVGL 池内存随即回落
*/
void ui_content_page_close(void) {
if (s_open == NULL || s_cont == NULL) {
return; /* 未打开(防御) */
}
lv_obj_t *menu_cont = ui_menu_view_container();
if (menu_cont != NULL) {
lv_group_focus_obj(menu_cont); /* 焦点先回菜单容器 */
lv_screen_load(lv_obj_get_screen(menu_cont)); /* 切回菜单屏(对象未动,原样还原) */
}
DBG_INFO("page close: id=%s", s_open->id);
/* 自动失效定时器随页销毁:留着它,下一拍就会去失效即将释放的 s_cont
* (回调与关页同在 uiTask 串行点,此处删表即绝后患) */
if (s_auto_timer != NULL) {
lv_timer_delete(s_auto_timer);
s_auto_timer = NULL;
}
lv_obj_delete(s_cont); /* 先删容器 */
lv_obj_delete(s_scr); /* 再删屏 */
s_scr = NULL;
s_cont = NULL;
s_open = NULL;
s_sel = 0u;
s_perm = (uint8_t)UI_PERM_VIEW; /* 权限重置(卫生:不留上次进入的授权) */
}
/*
* 函数功能:查询是否有内容页打开
* 入口参数:无
* 返回值:true - 有页面打开;false - 无
* 限定条件:无
* 函数说明:供逻辑层/调试判断当前处于菜单屏还是内容页
*/
bool ui_content_page_is_open(void) {
return (s_open != NULL);
}
/*
* 函数功能:查询当前打开的页面是否可编辑
* 入口参数:无
* 返回值:true - 页面支持编辑模式(edit_capable)且被授予修改权限;false - 其余
* 限定条件:无
* 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 open 的 perm 实参授予
*/
bool ui_content_page_is_editable(void) {
if (s_open == NULL) {
return false; /* 无页打开或无描述符(防御) */
}
return (s_open->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY);
}
/*
* 函数功能:查询当前列表页的选中行(全局下标,跨页连续)
* 入口参数:无
* 返回值:选中行下标;无页打开/非列表页 = 0 uint16_t
* 限定条件:无
* 函数说明:供页面钩子(key_cb/draw_cb)感知当前选中位置
*/
uint16_t ui_content_page_selected_row(void) {
if (s_open == NULL) {
return 0u;
}
if (s_open->selectable == 0u) {
return 0u; /* 信息页/表单页无行选中概念 */
}
return s_sel;
}
/*
* 函数功能:查询当前打开页面的描述符(契约见 ui_content_page.h,Issue #69)
* 入口参数:无
* 返回值:当前页面描述符指针(Flash 常量,只读);无页打开 = NULL
* 限定条件:同上下文(uiTask)读取才有一致性语义;本函数零副作用、不打日志
* 函数说明:与 ui_content_page_is_open 的区别只在于顺带交出身份(desc->id),
* 推流 STATUS 用它把"开着哪一页"随帧带回,不再只有串口开/关沿可见
*/
const content_page_def_t *ui_content_page_current_desc(void) {
return s_open;
}