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 改同一批文件、文件内
交叠,硬切会产生不可编译的中间态,故整波一笔。
This commit is contained in:
2026-10-20 21:14:14 +08:00
parent 9d06798f2d
commit dafc18f5ed
47 changed files with 1447 additions and 450 deletions
+79 -107
View File
@@ -4,7 +4,9 @@
* 自绘全部内容(浅 V 装饰 + 居中标题 + 行数据 + 页脚页码),无子对象;
* 只持机制:屏/容器按需建毁、焦点、按键分发、描述符驱动通用绘制;
* 不持任何页面数据——各页数据/几何/钩子由 ui_page_xxx 模块经
* content_page_def_t 接入描述符表 s_defs[](架构见 内容页架构.md);
* content_page_def_t 提供,框架按**描述符指针**开页;"哪个叶子进哪一页"
* 的绑定在生成物 ui_page_registry.c(唯一真源 = ui_menu_tree.txt 的
* #page= 注记;架构见 内容页架构.md);
* 进入时切屏并获焦、ESC 返回菜单屏并还原焦点;
* 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
@@ -26,6 +28,12 @@
* 描述符 init_cb(开页后调一次,页面模块重置运行态)
* 2026-09-13 王建锋 cp_draw_cb 按区块拆为四个绘制子函数(cp_draw_deco/header/rows/footer),
* 主函数仅编排(行数取一次供行窗口/页脚共用),行为零变化
* 2026-10-15 王建锋 U1 设计令牌层:屏幕/骨架/行距/缓冲/1bpp 语义色字面量收进
* ui_tokens.h(纯替换,像素逐位一致)
* 2026-10-15 王建锋 U3 页面原型化:删 content_page_id_t 枚举与 s_defs[],open() 改收
* 描述符指针、s_open 变指针(NULL = 未打开);7 个页面头包含移交
* ui_page_registry.c;日志页身份改打描述符 id(ASCII 页名,比原
* id=%u 更可定位);绘制/按键/几何一律未动
*/
/* 头文件包含区 */
@@ -33,20 +41,6 @@
#include "ui_content_page.h"
#include "ui_page_device_info.h"
#include "ui_page_ac.h"
#include "ui_page_dc.h"
#include "ui_page_password.h"
#include "ui_page_net_cfg.h"
#include "ui_page_runlog.h"
#include "ui_page_selftest.h"
#include "ui_standby.h"
#include "ui_pwd_def.h"
@@ -55,6 +49,10 @@
#include "ui_deco.h"
#include "ui_style.h"
#include "ui_tokens.h"
#include "lvgl.h"
#define DBG_TAG "[UI_CONTENT_PAGE]"
@@ -63,57 +61,32 @@
/* 私有宏定义区 */
/*
* 布局常量(设备像素;参考图 10/20/21 实测,3x 放大图缩回 160x160 测量):
* 标题 y=2(CJK 字形顶距 +1 → 墨迹自 y=3 起,与参考图一致)、页脚 y=143;
* 字形顶距(CJK +1 / ASCII +2)已计入放置 y,墨迹与参考图对齐
* 布局取值全部来自 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,墨迹与参考图对齐
*/
#define CP_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */
#define CP_SCREEN_H 160 /* 屏幕高(px) */
#define CP_TITLE_Y 2 /* 标题放置 y(居中;墨迹自 y=3 起) */
#define CP_PAGE_LINE_Y 143 /* 页脚"第 X 页,共 Y 页"放置 y(x=6 左对齐) */
#define CP_ROW_BUF_LEN 32 /* 动态行栈缓冲长度(同帧同步光栅化;超出行带即被裁切) */
/* 静态变量定义区 */
/*
* 描述符表:页 ID → 页面模块描述符(每页一行;新增页 = 枚举值 + 此处一行,
* 框架机制不变,见 内容页架构.md §七)
* 当前打开的页:直接持页面描述符指针(NULL = 未打开)。
* 页身份由调用方给出(逻辑层经 ui_page_registry_for / _password 取描述符),
* 框架不再维护"页 ID 枚举 ↔ s_defs 下标 ↔ 菜单节点"三处必须手工对齐的映射
* ——那正是旧 menu_content_page_for() 手写 case 的错源(表序一变即错位)。
*/
static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = {
[CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc,
[CONTENT_PAGE_AC] = &ui_page_ac_desc,
[CONTENT_PAGE_DC] = &ui_page_dc_desc,
[CONTENT_PAGE_PASSWORD] = &ui_page_password_desc,
[CONTENT_PAGE_NET_CFG] = &ui_page_net_cfg_desc,
[CONTENT_PAGE_RUN_LOG] = &ui_page_runlog_desc,
[CONTENT_PAGE_SELFTEST_LOG] = &ui_page_selftest_desc,
};
static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */
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; /* 页面全屏容器(焦点/键入口 + 自绘载体) */
/* flat:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的 card 样式(同 ui_menu_view.c) */
/* 用常量样式(LV_STYLE_CONST_INIT):属性存静态数组,零 LV_MEM 池分配;
* 动态样式首次 set_prop 会从池分配 values_and_props 且永不释放(x64 实测 88B/个) */
static const lv_style_const_prop_t cp_flat_props[] = {
{ .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } },
{ .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } },
{ .prop = LV_STYLE_RADIUS, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } },
LV_STYLE_CONST_PROPS_END
};
LV_STYLE_CONST_INIT(cp_style_flat, cp_flat_props);
/* 容器样式:flat(透明 + 无边框/圆角/内边距)取自共享样式表 ui_style.c——
* 与 ui_menu_view / ui_standby 共用同一 const 对象(原先三处各定义一份逐字相同
* 的 9 项属性数组);常量样式零 LV_MEM 池分配的纪律见 ui_style.h */
/* 函数定义区 */
@@ -175,18 +148,18 @@ static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y
lv_draw_label_dsc_t ld;
area.x1 = 0;
area.x2 = CP_SCREEN_W - 1;
area.x2 = UI_SCREEN_W - 1;
area.y1 = y;
area.y2 = y + 15; /* 覆盖字体行高(13)+ 余量 */
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 = lv_color_white();
ld.color = UI_COL_FG;
/* 本 LVGL 版 lv_text_get_width:length=0 表示"不测量"(返回 0);
* length≠0 时循环无 NUL 检查,传 -1 会越串尾死循环——必须传 strlen */
ld.ofs_x = (CP_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2;
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);
}
@@ -211,7 +184,7 @@ static void cp_draw_deco(lv_layer_t *layer, const content_page_def_t *d) {
* d - 页面描述符 const content_page_def_t *
* 返回值:无
* 限定条件:页面已打开
* 函数说明:标题放置 y 每页可配(title_y,0 = 框架默认 CP_TITLE_Y);
* 函数说明:标题放置 y 每页可配(title_y,0 = 框架默认 UI_TITLE_Y);
* 副标题仅信息页(subtitle 非空);重绘区不相交则跳过区块
*/
static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) {
@@ -219,12 +192,12 @@ static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) {
/* 标题(居中;放置 y 每页可配,0 = 框架默认) */
{
int32_t ty = (d->title_y != 0) ? d->title_y : CP_TITLE_Y;
int32_t ty = (d->title_y != 0) ? d->title_y : UI_TITLE_Y;
t.x1 = 0;
t.x2 = CP_SCREEN_W - 1;
t.x2 = UI_SCREEN_W - 1;
t.y1 = ty;
t.y2 = ty + 15;
t.y2 = ty + UI_TEXT_BAND_H;
if (cp_areas_intersect(&t, &layer->_clip_area)) {
cp_draw_centered_text(layer, d->title, ty);
}
@@ -233,9 +206,9 @@ static void cp_draw_header(lv_layer_t *layer, const content_page_def_t *d) {
/* 副标题(仅信息页) */
if (d->subtitle != NULL) {
t.x1 = 0;
t.x2 = CP_SCREEN_W - 1;
t.x2 = UI_SCREEN_W - 1;
t.y1 = d->subtitle_y;
t.y2 = d->subtitle_y + 15;
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);
}
@@ -268,19 +241,19 @@ static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t
}
bool dynamic = (d->refresh != NULL);
char dyn_name[CP_ROW_BUF_LEN]; /* 动态行栈缓冲(同帧同步光栅化) */
char dyn_value[CP_ROW_BUF_LEN];
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 ? lv_color_black() : lv_color_white();
lv_color_t color = is_sel ? UI_COL_SEL_FG : UI_COL_FG;
lv_area_t strip;
/* 行条带(整宽)不在重绘区:跳过(culling) */
strip.x1 = 0;
strip.x2 = CP_SCREEN_W - 1;
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)) {
@@ -292,13 +265,13 @@ static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t
lv_draw_rect_dsc_t fd;
lv_area_t band;
band.x1 = 2;
band.x2 = CP_SCREEN_W - 3; /* x=156(与参考图实测一致) */
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 = lv_color_white();
fd.bg_color = UI_COL_SEL_BG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &band);
}
@@ -353,10 +326,10 @@ static void cp_draw_rows(lv_layer_t *layer, const content_page_def_t *d, int32_t
* (hide_footer;密码页跟随实机)
*/
static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32_t nrows) {
char buf[32]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */
char buf[UI_BUF_STRLEN]; /* 页行文本缓冲(栈):"第 %u 页,共 %u 页" 最长远不足 32B */
uint8_t cur; /* 当前页码(1 基) */
uint8_t pages; /* 总页数 */
lv_area_t t; /* 页脚条带区域(整宽,放置于 CP_PAGE_LINE_Y) */
lv_area_t t; /* 页脚条带区域(整宽,放置于 UI_PAGE_LINE_Y) */
/* 页码计算,两种来源:
* - 列表页(selectable):随选中行现算——当前页 = 选中行/每页行数 + 1
@@ -373,12 +346,12 @@ static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32
pages = d->fixed_total_pages;
}
/* 页脚条带:整宽,放置于 CP_PAGE_LINE_Y(143,字形顶距已计入放置 y,
/* 页脚条带:整宽,放置于 UI_PAGE_LINE_Y(143,字形顶距已计入放置 y,
* 墨迹与参考图对齐);高 15 = 字体行高 13 + 余量,整行文字一次覆盖 */
t.x1 = 0;
t.x2 = CP_SCREEN_W - 1;
t.y1 = CP_PAGE_LINE_Y;
t.y2 = CP_PAGE_LINE_Y + 15;
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)——两者皆成立才格式化并绘制 */
@@ -388,13 +361,13 @@ static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32
lv_draw_label_dsc_t ld;
/* ofs 相对 coords 原点(t.x1=0,t.y1=CP_PAGE_LINE_Y):
/* 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 = lv_color_white(); /* 页码恒白字(不像选中行那样反色) */
ld.ofs_x = 6;
ld.color = UI_COL_FG; /* 页码恒白字(不像选中行那样反色) */
ld.ofs_x = UI_FOOTER_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &t);
}
@@ -410,7 +383,7 @@ static void cp_draw_footer(lv_layer_t *layer, const content_page_def_t *d, int32
* 跳过不在重绘区的部分;末尾调 draw_cb(页自定义内容,在内置区块之后)
*/
static void cp_draw_cb(lv_event_t *e) {
const content_page_def_t *d = s_defs[s_open];
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); /* 行数取一次:供行窗口与页脚页码共用 */
@@ -438,7 +411,7 @@ static void cp_draw_cb(lv_event_t *e) {
* 信息页不响应;动态行数可能缩小,选中行越界时重置回首行
*/
static void cp_key_cb(lv_event_t *e) {
const content_page_def_t *d = s_defs[s_open];
const content_page_def_t *d = s_open;
lv_key_t key = (lv_key_t)lv_event_get_key(e);
ui_standby_notify_activity(); /* 用户活动:重置待机空闲计时 */
@@ -496,24 +469,23 @@ static void cp_key_cb(lv_event_t *e) {
/*
* 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器)
* 入口参数:id - 内容页 ID content_page_id_t
* 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password)
* const content_page_def_t *
* perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY)
* 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败)
* 返回值: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(content_page_id_t id, uint8_t perm) {
if (id >= CONTENT_PAGE_COUNT || s_open != CONTENT_PAGE_COUNT) {
return false; /* ID 无效或已有页面打开(防御) */
bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm) {
if (s_open != NULL) {
return false; /* 已有页面打开(防御:页面按需建毁,同一时刻至多一页) */
}
const content_page_def_t *d = s_defs[id];
if (d == NULL) {
DBG_ERROR("page open: id=%u no descriptor (enum/table drift)", (unsigned)id);
return false; /* 枚举/描述符表漂移:拒开并留痕,不崩溃 */
if (desc == NULL) {
DBG_ERROR("page open: NULL descriptor (node has no bound page)");
return false; /* 调用方把未绑定内容页的叶子当页面开了:拒开并留痕,不崩溃 */
}
s_perm = perm; /* 本次授予权限(页面可编辑性 = edit_capable 且修改权限) */
@@ -530,17 +502,17 @@ bool ui_content_page_open(content_page_id_t id, uint8_t perm) {
return false;
}
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级布局 */
lv_obj_set_size(s_cont, CP_SCREEN_W, CP_SCREEN_H); /* 全屏覆盖 */
lv_obj_add_style(s_cont, &cp_style_flat, 0); /* 透明、无边框:剥离主题 card */
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 = id;
s_open = desc;
s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */
if (d->init_cb != NULL) {
d->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */
if (desc->init_cb != NULL) {
desc->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */
}
/* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */
@@ -551,7 +523,7 @@ bool ui_content_page_open(content_page_id_t id, uint8_t perm) {
}
lv_screen_load(s_scr); /* 切到内容页(菜单屏对象保留、不销毁) */
DBG_INFO("page open: id=%u rows=%u", (unsigned)id, (unsigned)cp_row_total(d));
DBG_INFO("page open: id=%s rows=%u", desc->id, (unsigned)cp_row_total(desc));
return true;
}
@@ -564,7 +536,7 @@ bool ui_content_page_open(content_page_id_t id, uint8_t perm) {
* 2. 切回菜单屏后销毁页面屏幕,LVGL 池内存随即回落
*/
void ui_content_page_close(void) {
if (s_open == CONTENT_PAGE_COUNT || s_cont == NULL) {
if (s_open == NULL || s_cont == NULL) {
return; /* 未打开(防御) */
}
@@ -574,14 +546,14 @@ void ui_content_page_close(void) {
lv_screen_load(lv_obj_get_screen(menu_cont)); /* 切回菜单屏(对象未动,原样还原) */
}
DBG_INFO("page close: id=%u", (unsigned)s_open);
DBG_INFO("page close: id=%s", s_open->id);
lv_obj_delete(s_cont); /* 先删容器 */
lv_obj_delete(s_scr); /* 再删屏 */
s_scr = NULL;
s_cont = NULL;
s_open = CONTENT_PAGE_COUNT;
s_open = NULL;
s_sel = 0u;
s_perm = (uint8_t)UI_PERM_VIEW; /* 权限重置(卫生:不留上次进入的授权) */
}
@@ -594,7 +566,7 @@ void ui_content_page_close(void) {
* 函数说明:供逻辑层/调试判断当前处于菜单屏还是内容页
*/
bool ui_content_page_is_open(void) {
return (s_open != CONTENT_PAGE_COUNT);
return (s_open != NULL);
}
/*
@@ -605,10 +577,10 @@ bool ui_content_page_is_open(void) {
* 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 open 的 perm 实参授予
*/
bool ui_content_page_is_editable(void) {
if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) {
if (s_open == NULL) {
return false; /* 无页打开或无描述符(防御) */
}
return (s_defs[s_open]->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY);
return (s_open->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY);
}
/*
@@ -619,10 +591,10 @@ bool ui_content_page_is_editable(void) {
* 函数说明:供页面钩子(key_cb/draw_cb)感知当前选中位置
*/
uint16_t ui_content_page_selected_row(void) {
if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) {
if (s_open == NULL) {
return 0u;
}
if (s_defs[s_open]->selectable == 0u) {
if (s_open->selectable == 0u) {
return 0u; /* 信息页/表单页无行选中概念 */
}
return s_sel;
+19 -22
View File
@@ -4,7 +4,9 @@
* 屏/容器按需建毁生命周期 + 焦点 + 切屏、按键分发(内置 ↑↓/ESC + key_cb 钩子)、
* 描述符驱动通用绘制(浅 V + 标题/副标题 + 行 + 选中带 + 页脚 + draw_cb);
* 不持任何页面数据:各页数据/几何/钩子由 ui_page_xxx 模块经 content_page_def_t
* 接入框架描述符表(架构见 内容页架构.md);
* 提供,框架按**描述符指针**开页;"哪个叶子进哪一页"的绑定在生成物
* ui_page_registry.c(真源 = 菜单树 ui_menu_tree.txt 的 #page= 注记),
* 框架既无页 ID 枚举也无节点→页手写 case(架构见 内容页架构.md);
* 进入时切屏并获焦、ESC 返回菜单屏并还原焦点;
* 不持有导航状态——由逻辑层 ui_menu.c 在 ENTER 叶子节点时驱动
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
@@ -23,6 +25,11 @@
* 是否过密码页/所输密码决定);描述符新增 edit_capable(页面
* 是否支持编辑模式)+ 新 API ui_content_page_is_editable
* (页面钩子据以决定编辑行为:edit_capable 且修改权限)
* 2026-10-15 王建锋 U3 页面原型化:删 content_page_id_t 枚举与框架侧 s_defs[],
* open() 改收描述符指针;节点→页面绑定移交给生成物
* ui_page_registry.c(菜单树 #page= 注记为唯一真源),
* 逻辑层手写 menu_content_page_for() 随之删除;
* 描述符新增 id 字段(ASCII 页名,供日志/诊断定位是哪一页)
*/
#ifndef UI_CONTENT_PAGE_H
#define UI_CONTENT_PAGE_H
@@ -32,21 +39,6 @@
#include "lvgl.h"
/*
* 内容页 ID:与菜单叶子节点一一对应(映射见 ui_menu.c menu_content_page_for);
* CONTENT_PAGE_COUNT 兼作"无内容页/未打开"哨兵值
*/
typedef enum {
CONTENT_PAGE_DEVICE_INFO = 0u, /* 装置信息(node 1):静态键值行 + 副标题,单页 */
CONTENT_PAGE_AC, /* 交流量(node 3):数据列表,24 项 / 8 项每页 */
CONTENT_PAGE_DC, /* 直流量(node 4):数据列表,3 项单页 */
CONTENT_PAGE_PASSWORD, /* 用户密钥(密码页):3 位密码,表单/操作页首个实例 */
CONTENT_PAGE_NET_CFG, /* 网口设置(node 30):地址列表 + 保存行,可编辑(修改权限) */
CONTENT_PAGE_RUN_LOG, /* 运行记录(node 40):动态列表,每记录 2 行(时间戳+描述) */
CONTENT_PAGE_SELFTEST_LOG, /* 自检记录(node 39):动态列表,每记录 2 行(时间戳+故障) */
CONTENT_PAGE_COUNT /* 哨兵:无效 ID / 未打开 */
} content_page_id_t;
/* 行数据:左列名 + 右列值(Flash 字面量,不 strdup) */
typedef struct {
const char *name;
@@ -54,14 +46,18 @@ typedef struct {
} cp_row_t;
/*
* 页面描述符:一页 = 一个 Flash const 结构(几何 + 数据源 + 可选钩子)
* 由各 ui_page_xxx 页面模块提供,框架描述符表 s_defs[] 按页 ID 引用
* 页面描述符:一页 = 一个 Flash const 结构(身份 + 几何 + 数据源 + 可选钩子)
* 由各 ui_page_xxx 页面模块提供;"哪个叶子进哪一页"的绑定不在本头,也不在框架 .c,
* 而在生成物 ui_page_registry.c(唯一真源 = 菜单树 ui_menu_tree.txt 的 #page= 注记)。
* 框架按描述符**指针**开页,不再有页 ID 枚举与 s_defs[] 下标表——
* 少一处"枚举/表/节点下标三处必须对齐"的手工同步点。
*/
typedef struct {
const char *id; /* 页面模块名(ASCII,日志/诊断用;= ui_page_<id> 的 <id>) */
const char *title; /* 标题(浅 V 区居中) */
const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */
int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */
int32_t title_y; /* 标题放置 y;0 = 框架默认(CP_TITLE_Y) */
int32_t title_y; /* 标题放置 y;0 = 框架默认(UI_TITLE_Y) */
uint8_t hide_deco; /* 1 = 不绘"浅 V"装饰(密码页跟随实机) */
uint8_t hide_footer; /* 1 = 不绘页脚页码(密码页跟随实机:无页脚) */
uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */
@@ -88,17 +84,18 @@ typedef struct {
/*
* 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器)
* 入口参数:id - 内容页 ID content_page_id_t
* 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password)
* const content_page_def_t *
* perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY;
* 由逻辑层按是否过密码页/所输密码决定,页面经
* ui_content_page_is_editable 查询)
* 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败)
* 返回值:true - 成功;false - 失败(desc 为 NULL / 已有页面打开 / 对象创建失败)
* 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在)
* 函数说明:1. 新建独立屏幕,全屏容器自绘全部内容(无子对象,池开销极小)
* 2. 容器入默认输入组并获焦——按键由本模块处理(↑↓移选/ESC返回/页面钩子)
* 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原
*/
bool ui_content_page_open(content_page_id_t id, uint8_t perm);
bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm);
/*
* 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面)
+4 -3
View File
@@ -15,9 +15,10 @@
#include "ui_deco.h"
#include "ui_tokens.h"
/* 私有宏定义区 */
#define DECO_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */
#define DECO_LEG_W 3 /* "浅 V"两腿线宽(1bpp 下斜线边缘易被阈值吃掉,需加粗) */
#define DECO_HORIZ_W 2 /* "浅 V"水平线宽 */
@@ -31,7 +32,7 @@ static const lv_point_precise_t s_leg_r[2] = { {158, 7}, {145, 17} }; /* 右腿
static const lv_point_precise_t *const s_segs[3] = {s_leg_l, s_leg_r, s_hoz};
static const int32_t s_seg_w[3] = {DECO_LEG_W, DECO_LEG_W, DECO_HORIZ_W};
static const lv_area_t s_band = {0, 0, DECO_SCREEN_W - 1, 19}; /* "浅 V" culling 区(与原线段对象范围一致) */
static const lv_area_t s_band = {0, 0, UI_SCREEN_W - 1, 19}; /* "浅 V" culling 区(与原线段对象范围一致) */
/* 函数定义区 */
@@ -72,7 +73,7 @@ void ui_deco_draw(lv_layer_t *layer, const lv_area_t *clip) {
lv_draw_line_dsc_init(&ld);
ld.base.layer = layer;
ld.color = lv_color_white();
ld.color = UI_COL_FG;
ld.width = s_seg_w[i];
ld.p1 = s_segs[i][0];
ld.p2 = s_segs[i][1];
+52 -71
View File
@@ -311,11 +311,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x7f, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f,
0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xff, 0x80,
/* U+4E0A "上" */
0x8, 0x1, 0x0, 0x20, 0x4, 0x0, 0xfc, 0x10,
0x2, 0x0, 0x40, 0x8, 0x1, 0x0, 0x20, 0x7f,
0xf0,
/* U+4E3B "主" */
0x8, 0x0, 0x80, 0x0, 0x3f, 0xe0, 0x40, 0x8,
0x1, 0x3, 0xfe, 0x4, 0x0, 0x80, 0x10, 0x7f,
@@ -671,11 +666,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x7f, 0xd1, 0x14, 0x45, 0xff, 0x44, 0x51, 0x17,
0xfd, 0x11, 0x44, 0x51, 0x18, 0x1c,
/* U+7535 "电" */
0x8, 0x1, 0x3, 0xfe, 0x44, 0x48, 0x89, 0xff,
0x22, 0x24, 0x44, 0xff, 0x91, 0x4, 0x20, 0x83,
0xf0,
/* U+754C "界" */
0x7f, 0xc8, 0x89, 0xff, 0x22, 0x27, 0xfc, 0x8,
0x6, 0xc7, 0x7, 0x11, 0x2, 0x20, 0x84, 0x20,
@@ -828,11 +818,6 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x17, 0xfa, 0x8, 0x49, 0x29, 0xfd, 0x0, 0x5f,
0xf0,
/* U+91CD "重" */
0x3, 0xcf, 0x80, 0x10, 0x7f, 0xf4, 0x44, 0xff,
0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f,
0xf0,
/* U+91CF "量" */
0x7f, 0xc8, 0x89, 0x9, 0x7f, 0xf4, 0x44, 0xff,
0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f,
@@ -1018,20 +1003,20 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 723, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 740, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 757, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 774, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 788, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 804, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 821, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 837, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1},
{.bitmap_index = 757, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 771, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 787, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 804, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 820, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1},
{.bitmap_index = 832, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 849, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 866, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 883, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 900, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 917, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 934, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 951, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 968, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 951, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 967, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 984, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1001, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1018, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1042,8 +1027,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 1103, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1120, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1137, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1154, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1171, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1154, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1168, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1185, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1202, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1219, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1057,9 +1042,9 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 1355, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1372, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1389, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1406, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1423, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1439, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1406, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1422, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1435, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1452, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1469, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1486, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1067,22 +1052,22 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 1520, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1537, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1554, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1571, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1588, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1571, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1587, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1604, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1621, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1638, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1655, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1672, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1689, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1723, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1737, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1754, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1771, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1786, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1803, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1820, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1706, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1720, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1737, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1752, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1769, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1786, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1802, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1819, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1836, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1853, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1870, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1091,9 +1076,9 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 1921, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1938, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1955, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1972, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1989, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2006, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1972, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1986, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2003, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2020, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2037, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2054, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1113,18 +1098,15 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 2292, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2309, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2326, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2343, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2360, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2377, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2394, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2343, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2359, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2376, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2393, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2410, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2427, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2444, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2461, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2478, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 2479, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 2481, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */,
{.bitmap_index = 2491, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0} /* U+FF0C */
{.bitmap_index = 2427, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 2428, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 2430, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */,
{.bitmap_index = 2440, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0} /* U+FF0C */
};
/*---------------------
@@ -1132,22 +1114,21 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
*--------------------*/
static const uint16_t unicode_list_1[] = {
0x0, 0x1, 0x32, 0x82, 0x9b, 0xed, 0x153, 0x1d4,
0x1d8, 0x233, 0x35c, 0x368, 0x36a, 0x42d, 0x444, 0x49f,
0x50d, 0x531, 0x582, 0x5b9, 0x5c3, 0x5da, 0x5ee, 0x626,
0x641, 0x927, 0x937, 0x982, 0xb04, 0xd47, 0xd4e, 0xd4f,
0xd91, 0xd95, 0xdbd, 0x102f, 0x1086, 0x10f9, 0x1106, 0x1131,
0x1149, 0x114a, 0x114c, 0x1266, 0x142e, 0x1442, 0x148d, 0x149b,
0x149c, 0x1565, 0x159e, 0x15a0, 0x16c4, 0x173c, 0x1767, 0x176b,
0x17d7, 0x17dc, 0x17ed, 0x1916, 0x1923, 0x1976, 0x19dc, 0x1a18,
0x1ab7, 0x1c18, 0x1d72, 0x1ed9, 0x1f38, 0x1f42, 0x1ffc, 0x22b0,
0x243e, 0x243f, 0x271f, 0x272c, 0x2743, 0x28eb, 0x2902, 0x29f8,
0x2a63, 0x2c02, 0x2ce6, 0x2d23, 0x2e72, 0x2ef2, 0x30b6, 0x30bf,
0x30eb, 0x3148, 0x3165, 0x33e1, 0x34a6, 0x3a43, 0x3abc, 0x3d5d,
0x3d9b, 0x3da6, 0x3da7, 0x3db5, 0x3dd9, 0x3de6, 0x4166, 0x418a,
0x41c7, 0x4211, 0x425c, 0x43c4, 0x43c6, 0x4696, 0x469c, 0x4710,
0x4829, 0x485b, 0x4959, 0x4a6c, 0x4b7f, 0x4c83, 0x50cf, 0xb103,
0xb111
0x0, 0x32, 0x82, 0x9b, 0xed, 0x153, 0x1d4, 0x1d8,
0x233, 0x35c, 0x368, 0x36a, 0x42d, 0x444, 0x49f, 0x50d,
0x531, 0x582, 0x5b9, 0x5c3, 0x5da, 0x5ee, 0x626, 0x641,
0x927, 0x937, 0x982, 0xb04, 0xd47, 0xd4e, 0xd4f, 0xd91,
0xd95, 0xdbd, 0x102f, 0x1086, 0x10f9, 0x1106, 0x1131, 0x1149,
0x114a, 0x114c, 0x1266, 0x142e, 0x1442, 0x148d, 0x149b, 0x149c,
0x1565, 0x159e, 0x15a0, 0x16c4, 0x173c, 0x1767, 0x176b, 0x17d7,
0x17dc, 0x17ed, 0x1916, 0x1923, 0x1976, 0x19dc, 0x1a18, 0x1ab7,
0x1c18, 0x1d72, 0x1ed9, 0x1f38, 0x1f42, 0x1ffc, 0x22b0, 0x243e,
0x243f, 0x271f, 0x2743, 0x28eb, 0x2902, 0x29f8, 0x2a63, 0x2c02,
0x2ce6, 0x2d23, 0x2e72, 0x2ef2, 0x30b6, 0x30bf, 0x30eb, 0x3148,
0x3165, 0x33e1, 0x34a6, 0x3a43, 0x3abc, 0x3d5d, 0x3d9b, 0x3da6,
0x3da7, 0x3db5, 0x3dd9, 0x3de6, 0x4166, 0x418a, 0x41c7, 0x4211,
0x425c, 0x43c6, 0x4696, 0x469c, 0x4710, 0x4829, 0x485b, 0x4959,
0x4a6c, 0x4b7f, 0x4c83, 0x50cf, 0xb103, 0xb111
};
/*Collect the unicode lists and glyph_id offsets*/
@@ -1159,14 +1140,14 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
},
{
.range_start = 19977, .range_length = 45330, .glyph_id_start = 96,
.unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 121, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
.unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 118, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
},
{
.range_start = 9654, .range_length = 1, .glyph_id_start = 217,
.range_start = 9654, .range_length = 1, .glyph_id_start = 214,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
},
{
.range_start = 65292, .range_length = 1, .glyph_id_start = 218,
.range_start = 65292, .range_length = 1, .glyph_id_start = 215,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
}
};
+11 -30
View File
@@ -32,6 +32,8 @@
#include "ui_content_page.h"
#include "ui_page_registry.h"
#include "ui_page_password.h"
#include "ui_standby.h"
@@ -105,28 +107,6 @@ static void menu_back(void) {
ui_menu_view_focus();
}
/*
* 函数功能:查叶子节点对应的内容页(无内容页返回哨兵值)
* 入口参数:node - 菜单节点下标 uint8_t
* 返回值:内容页 ID content_page_id_t;无内容页 = CONTENT_PAGE_COUNT
* 限定条件:无
* 函数说明:仅部分叶子有内容页(参考图 10/20/21 + 网口设置 + 记录查询);下标对应
* ui_menu_tab.c
* node 1=装置信息、3=交流量、4=直流量、30=网口设置、39=自检记录、40=运行记录
* (表序变更时须同步本映射)
*/
static content_page_id_t menu_content_page_for(uint8_t node) {
switch (node) {
case 1u: return CONTENT_PAGE_DEVICE_INFO; /* 装置信息(L1 叶子) */
case 3u: return CONTENT_PAGE_AC; /* 交流量(实时数据子项) */
case 4u: return CONTENT_PAGE_DC; /* 直流量(实时数据子项) */
case 30u: return CONTENT_PAGE_NET_CFG; /* 网口设置(通讯参数子项,需密码) */
case 39u: return CONTENT_PAGE_SELFTEST_LOG; /* 自检记录(记录查询子项) */
case 40u: return CONTENT_PAGE_RUN_LOG; /* 运行记录(记录查询子项) */
default: return (content_page_id_t)CONTENT_PAGE_COUNT; /* 无内容页 */
}
}
/*
* 函数功能:密码页确认一致回调(预装进密码页模块;uiTask 上下文调用)
* 入口参数:perm - 密码页按所输密码授予的权限 ui_perm_t(UI_PERM_MODIFY/UI_PERM_VIEW)
@@ -152,10 +132,10 @@ static void menu_password_ok(uint8_t perm) {
{
uint8_t child = kids[s_sel[depth]];
content_page_id_t pid = menu_content_page_for(child);
const content_page_def_t *page = ui_page_registry_for(child);
if (pid < CONTENT_PAGE_COUNT) {
ui_content_page_open(pid, perm); /* 页已关,守卫可过:开目标内容页(携权限) */
if (page != NULL) {
ui_content_page_open(page, perm); /* 页已关,守卫可过:开目标内容页(携权限) */
}
else if (depth < MENU_PAGE_MAX - 1u) {
menu_open(child); /* 压层:建新框 + 焦点移到新框(叶子 → 空白终点框) */
@@ -202,17 +182,18 @@ static void menu_key_handle(uint8_t depth, lv_key_t key) {
/* 进子框:空框无条目可进;最深层(depth=MAX-1)无子层可再开 */
if (cnt > 0u) {
uint8_t child = kids[s_sel[depth]];
content_page_id_t pid = menu_content_page_for(child);
const content_page_def_t *page = ui_page_registry_for(child);
if (ui_menu_tab_has_pwd(child)) {
/* 需密码叶子(含带内容页者):先经密码页;授权按所输密码
(查看→只读 / 修改→可修改),回调携权续做"进入"动作 */
(查看→只读 / 修改→可修改),回调携权续做"进入"动作;
密码页非节点绑定,经注册表专用出口取 */
ui_page_password_arm(menu_password_ok);
ui_content_page_open(CONTENT_PAGE_PASSWORD, (uint8_t)UI_PERM_VIEW);
ui_content_page_open(ui_page_registry_password(), (uint8_t)UI_PERM_VIEW);
}
else if (pid < CONTENT_PAGE_COUNT) {
else if (page != NULL) {
/* 叶子有内容页:无密码直接进(默认只读权限) */
ui_content_page_open(pid, (uint8_t)UI_PERM_VIEW);
ui_content_page_open(page, (uint8_t)UI_PERM_VIEW);
}
else if (depth < MENU_PAGE_MAX - 1u) {
menu_open(child); /* 压层:建新框 + 焦点移到新框(叶子 → 空白终点框) */
+16 -26
View File
@@ -31,6 +31,10 @@
#include "ui_deco.h"
#include "ui_style.h"
#include "ui_tokens.h"
#include "lvgl.h"
#define DBG_TAG "[UI_MENU_VIEW]"
@@ -79,22 +83,8 @@ static uint8_t s_row_node[MENU_PAGE_MAX][MENU_ITEM_MAX]; /* 行数据:节
static menu_frame_t s_frame[MENU_PAGE_MAX]; /* 每层框几何缓存:位置/尺寸/选中行(现算不落状态,供子框对齐;见上方类型注释) */
/* 样式(静态,一次性初始化,常驻) */
/* flat:通用——透明、无边框/圆角/内边距(剥离主题 card)。用常量样式(LV_STYLE_CONST_INIT):
* 属性存静态数组、零 LV_MEM 池分配;动态样式首次 set_prop 会从池分配 values_and_props 且永不释放 */
static const lv_style_const_prop_t s_flat_props[] = {
{ .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } },
{ .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } },
{ .prop = LV_STYLE_RADIUS, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } },
LV_STYLE_CONST_PROPS_END
};
LV_STYLE_CONST_INIT(s_style_flat, s_flat_props);
/* flat(透明、无边框/圆角/内边距,剥离主题 card)取自共享样式表 ui_style.c:
* 与 ui_content_page / ui_standby 共用同一 const 对象,不再本地重复定义 */
static lv_style_t s_style_frame; /* 框体:透明底 + 1px 白边框 */
static lv_style_t s_style_scrollbar; /* 滚动条:白色、4px 宽 */
@@ -122,7 +112,7 @@ static void view_styles_init(void) {
lv_style_init(&s_style_frame);
lv_style_set_bg_opa(&s_style_frame, LV_OPA_TRANSP);
lv_style_set_border_width(&s_style_frame, MENU_FRAME_BORDER);
lv_style_set_border_color(&s_style_frame, lv_color_white());
lv_style_set_border_color(&s_style_frame, UI_COL_FG);
lv_style_set_radius(&s_style_frame, 0);
lv_style_set_pad_all(&s_style_frame, 0);
lv_style_set_pad_gap(&s_style_frame, 0);
@@ -130,8 +120,8 @@ static void view_styles_init(void) {
/* scrollbar:白色 4px 宽(行超框高时自动出现) */
lv_style_init(&s_style_scrollbar);
lv_style_set_bg_opa(&s_style_scrollbar, LV_OPA_COVER);
lv_style_set_bg_color(&s_style_scrollbar, lv_color_white());
lv_style_set_width(&s_style_scrollbar, 4);
lv_style_set_bg_color(&s_style_scrollbar, UI_COL_FG);
lv_style_set_width(&s_style_scrollbar, UI_SCROLLBAR_W);
}
/*
@@ -237,7 +227,7 @@ static void view_frame_draw_cb(lv_event_t *e) {
lv_area_t row_area;
const char *name = ui_menu_tab_name(s_row_node[depth][i]);
bool is_sel = (i == (int32_t)f->sel);
lv_color_t color = is_sel ? lv_color_black() : lv_color_white();
lv_color_t color = is_sel ? UI_COL_SEL_FG : UI_COL_FG;
row_area.x1 = area.x1;
row_area.x2 = area.x2;
@@ -254,7 +244,7 @@ static void view_frame_draw_cb(lv_event_t *e) {
lv_draw_rect_dsc_t fd;
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = lv_color_white();
fd.bg_color = UI_COL_SEL_BG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &row_area);
}
@@ -407,7 +397,7 @@ void ui_menu_view_frame_build(uint8_t depth, uint8_t node, uint8_t sel, uint16_t
if (box == NULL) {
return; /* LVGL 池耗尽:框未建,状态回滚 */
}
lv_obj_add_style(box, &s_style_flat, 0);
lv_obj_add_style(box, &ui_style_flat, 0);
lv_obj_add_style(box, &s_style_frame, 0);
lv_obj_add_flag(box, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(box, f->x, f->y);
@@ -571,7 +561,7 @@ static void view_deco_draw_cb(lv_event_t *e) {
sd.base.layer = layer;
sd.text = "当前模式 : 无模式";
sd.text_static = 1; /* Flash 字符串:不拷内存 */
sd.color = lv_color_white();
sd.color = UI_COL_FG;
sd.ofs_x = 0; /* 区域原点即状态行左上 (17,20) */
sd.ofs_y = 0;
lv_draw_label(layer, &sd, &s_status_area);
@@ -608,7 +598,7 @@ int ui_menu_view_init(void) {
if (menu == NULL) {
return -1;
}
lv_obj_add_style(menu, &s_style_flat, 0); /* 透明、无边框:纯定位层,不可见 */
lv_obj_add_style(menu, &ui_style_flat, 0); /* 透明、无边框:纯定位层,不可见 */
lv_obj_set_size(menu, sw, sh);
/* 持久容器:全屏覆盖、永不删(焦点/键唯一入口;各层框均建其内,防悬空) */
@@ -616,7 +606,7 @@ int ui_menu_view_init(void) {
if (s_cont == NULL) {
return -1;
}
lv_obj_add_style(s_cont, &s_style_flat, 0); /* 透明、无边框 */
lv_obj_add_style(s_cont, &ui_style_flat, 0); /* 透明、无边框 */
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); /* 绝对定位,不参与父级 flex 布局 */
lv_obj_set_pos(s_cont, 0, 0);
lv_obj_set_size(s_cont, sw, sh); /* 全屏覆盖 */
@@ -634,7 +624,7 @@ int ui_menu_view_init(void) {
if (s_frame_box[0] == NULL) {
return -1;
}
lv_obj_add_style(s_frame_box[0], &s_style_flat, 0); /* 透明底 */
lv_obj_add_style(s_frame_box[0], &ui_style_flat, 0); /* 透明底 */
lv_obj_add_style(s_frame_box[0], &s_style_frame, 0); /* 1px 白边框 */
lv_obj_add_flag(s_frame_box[0], LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_pos(s_frame_box[0], MENU_ROOT_BOX_X, MENU_ROOT_BOX_Y); /* 左上 (0,44):装饰/状态行之下 */
+6 -2
View File
@@ -21,11 +21,15 @@
#include "lvgl.h"
#include "ui_tokens.h"
#define MENU_PAGE_MAX 4 /* 页面深度上限(根框 + 3 级子框) */
#define MENU_ITEM_MAX 16 /* 单框最大条目数 */
#define MENU_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */
#define MENU_SCREEN_H 160 /* 屏幕高(px) */
/* 屏幕尺寸单一来源 = ui_tokens.h(权威定义在 ui_port.h,两端一致);
* 保留 MENU_SCREEN_* 旧名作为别名,兼容既有引用与本模块文档 */
#define MENU_SCREEN_W UI_SCREEN_W /* 屏幕宽(px) */
#define MENU_SCREEN_H UI_SCREEN_H /* 屏幕高(px) */
/*
* 函数功能:初始化显示层(样式 + 4 层持久容器 + 根框装饰 + 根框框体)
+10 -11
View File
@@ -38,6 +38,7 @@
#include "net_socket.h"
#include "net_types.h"
#include "ui_net_sink_backend.h"
#include "ui_tokens.h" /* 屏幕几何令牌(UI_SCREEN_W/H、UI_FB_SIZE 权威在 ui_port.h) */
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
#include "net_latency_test.h" /* M3 探针:prod_cycle_mcu(LCDMEM 处理净成本) */
@@ -53,16 +54,14 @@
#define CMD_KEY UI_REMO_CMD_KEY
#define CMD_LCDMEM UI_REMO_CMD_LCDMEM
/* 屏幕尺寸与帧大小(160x160 1bpp = 3200 字节) */
#define UI_W 160
#define UI_H 160
#define UI_MEMSIZE (UI_W * UI_H / 8)
/* 屏幕尺寸与帧大小取 ui_tokens.h / ui_port.h 的屏幕令牌(160x160 1bpp = 3200 字节),
* 本模块不再自带同值字面量——显示几何与协议几何必须是同一个事实 */
#define RM_MAX_RX_PAYLOAD 256 /* 客户端→服务端 payload 上限,防长度字段损坏引发失步 */
#define RM_RX_BUF 64 /* 接收暂存区:整块 recv 后逐字节喂入解析状态机 */
static uint8_t g_tx[4 + 4 + UI_MEMSIZE + 1]; /* 发送组帧缓冲:[TAG][CMD][LEN][DATA][CRC] */
static uint8_t g_lcdmem_data[4 + UI_MEMSIZE]; /* LCDMEM 应答缓冲:4B 偏移 + 1bpp 位图 */
static uint8_t g_tx[4 + 4 + UI_FB_SIZE + 1]; /* 发送组帧缓冲:[TAG][CMD][LEN][DATA][CRC] */
static uint8_t g_lcdmem_data[4 + UI_FB_SIZE]; /* LCDMEM 应答缓冲:4B 偏移 + 1bpp 位图 */
static uint8_t g_rx[RM_RX_BUF]; /* 接收暂存区:整块 recv 后逐字节喂入状态机 */
static int g_listen = -1; /* 监听 Socket fd(未启动或已停止时为 -1) */
@@ -194,9 +193,9 @@ static int remo_send(uint8_t cmd, const uint8_t *data, uint16_t len) {
static void remo_send_init(void) {
uint8_t init[8];
put_u16(init, UI_W);
put_u16(init + 2, UI_H);
put_u32(init + 4, UI_MEMSIZE);
put_u16(init, UI_SCREEN_W);
put_u16(init + 2, UI_SCREEN_H);
put_u32(init + 4, UI_FB_SIZE);
(void)remo_send(CMD_INIT, init, 8);
}
@@ -295,10 +294,10 @@ static void remo_handle(const uint8_t *f, int n) {
#endif
/* 整帧请求:经平台后端取 1bpp 位图(MCU: ui_port_get_framebuffer /
* 模拟器: sim_port_get_screen_mono)写入应答缓冲尾部,失败则静默丢弃 */
if (ui_net_sink_get_lcdmem(g_lcdmem_data + 4, UI_MEMSIZE) == 0) {
if (ui_net_sink_get_lcdmem(g_lcdmem_data + 4, UI_FB_SIZE) == 0) {
/* 应答数据段 = [偏移u32(回填请求原值)] + [1bpp 位图 3200B] */
memcpy(g_lcdmem_data, data, 4);
(void)remo_send(CMD_LCDMEM, g_lcdmem_data, (uint16_t)(4 + UI_MEMSIZE));
(void)remo_send(CMD_LCDMEM, g_lcdmem_data, (uint16_t)(4 + UI_FB_SIZE));
}
#if defined(TEST_LATENCY_PROBE)
lat_probe_add_seg("prod_cycle_mcu", lat_mark_us(&tm));
+5 -2
View File
@@ -13,12 +13,15 @@
#include <stdint.h>
#include "ui_port.h" /* UI_FB_SIZE:显示几何与协议几何的单一来源 */
#ifdef __cplusplus
extern "C" {
#endif
/* 1bpp 帧缓冲尺寸(160x160 / 8 = 3200 字节,须与 ui_net_sink.c 的 UI_MEMSIZE 一致) */
#define UI_NET_SINK_MONO_SIZE 3200
/* 1bpp 帧缓冲尺寸 = 屏幕令牌的帧字节数(UI_SCREEN_W*UI_SCREEN_H/8 = 3200),
* 与 ui_net_sink.c 的收发缓冲同源,不再各自写死字面量 */
#define UI_NET_SINK_MONO_SIZE UI_FB_SIZE
/*
* 函数功能:读取当前 1bpp 屏幕位图(LCDMEM 应答的数据源)
+10 -7
View File
@@ -13,6 +13,8 @@
/* 头文件包含区 */
#include "ui_page_ac.h"
#include "ui_tokens.h"
/* 静态变量定义区 */
/* 交流量行(参考图 20 = 第 1 页原样;第 2/3 页为占位数据,可替换) */
@@ -43,17 +45,18 @@ static const cp_row_t s_rows[] = {
{"Smax","2.45"},
};
/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;列表页,钩子全 NULL) */
/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=ac 绑定引用;列表页,钩子全 NULL) */
const content_page_def_t ui_page_ac_desc = {
.id = "ac",
.title = "交流量",
.selectable = 1u,
.top_y = 24,
.pitch = 14,
.band_h = 13,
.top_y = UI_LIST_TOP,
.pitch = UI_ROW_PITCH,
.band_h = UI_ROW_BAND_H,
.text_dy = 0,
.left_x = 7,
.right_x = 101,
.rows_per_page = 8u,
.left_x = UI_LIST_LEFT_X,
.right_x = UI_LIST_RIGHT_X,
.rows_per_page = UI_LIST_ROWS,
.row_count = 24,
.rows = s_rows,
};
+2 -2
View File
@@ -15,8 +15,8 @@
#include "ui_content_page.h"
/*
* 页面描述符(Flash const):交流量页 = CONTENT_PAGE_AC;
* 接入框架描述符表 s_defs[](定义于 ui_page_ac.c)
* 页面描述符(Flash const):交流量页(描述符 .id = "ac");
* 由生成物 ui_page_registry.c 按菜单树 #page=ac 注记绑定引用(定义于 ui_page_ac.c)
*/
extern const content_page_def_t ui_page_ac_desc;
+10 -7
View File
@@ -12,6 +12,8 @@
/* 头文件包含区 */
#include "ui_page_dc.h"
#include "ui_tokens.h"
/* 静态变量定义区 */
/* 直流量行(参考图 21) */
@@ -21,17 +23,18 @@ static const cp_row_t s_rows[] = {
{"Pa1", "3.288"},
};
/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;列表页单页,钩子全 NULL) */
/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=dc 绑定引用;列表页单页,钩子全 NULL) */
const content_page_def_t ui_page_dc_desc = {
.id = "dc",
.title = "直流量",
.selectable = 1u,
.top_y = 24,
.pitch = 14,
.band_h = 13,
.top_y = UI_LIST_TOP,
.pitch = UI_ROW_PITCH,
.band_h = UI_ROW_BAND_H,
.text_dy = 0,
.left_x = 7,
.right_x = 101,
.rows_per_page = 8u,
.left_x = UI_LIST_LEFT_X,
.right_x = UI_LIST_RIGHT_X,
.rows_per_page = UI_LIST_ROWS,
.row_count = 3,
.rows = s_rows,
};
+2 -2
View File
@@ -15,8 +15,8 @@
#include "ui_content_page.h"
/*
* 页面描述符(Flash const):直流量页 = CONTENT_PAGE_DC;
* 接入框架描述符表 s_defs[](定义于 ui_page_dc.c)
* 页面描述符(Flash const):直流量页(描述符 .id = "dc");
* 由生成物 ui_page_registry.c 按菜单树 #page=dc 注记绑定引用(定义于 ui_page_dc.c)
*/
extern const content_page_def_t ui_page_dc_desc;
+2 -1
View File
@@ -25,8 +25,9 @@ static const cp_row_t s_rows[] = {
{"程序日期:", "2024.08.27"},
};
/* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用;静态信息页,钩子全 NULL) */
/* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=device_info 绑定引用;静态信息页,钩子全 NULL) */
const content_page_def_t ui_page_device_info_desc = {
.id = "device_info",
.title = "装置信息",
.subtitle = "馈线自动化终端",
.subtitle_y = 25,
+2 -2
View File
@@ -14,8 +14,8 @@
#include "ui_content_page.h"
/*
* 页面描述符(Flash const):装置信息页 = CONTENT_PAGE_DEVICE_INFO;
* 接入框架描述符表 s_defs[](定义于 ui_page_device_info.c)
* 页面描述符(Flash const):装置信息页(描述符 .id = "device_info");
* 由生成物 ui_page_registry.c 按菜单树 #page=device_info 注记绑定引用(定义于 ui_page_device_info.c)
*/
extern const content_page_def_t ui_page_device_info_desc;
+22 -22
View File
@@ -30,10 +30,13 @@
#include "net_cfg.h"
#include "ui_tokens.h"
/* 私有宏定义区 */
/*
* 布局常量(设备像素;1bpp 屏 160x160):
* 布局取值:行骨架用 ui_tokens.h 的列表页令牌(UI_LIST_TOP / UI_ROW_PITCH /
* UI_ROW_BAND_H / UI_BUF_STRLEN),本文件只留本页私有的两列 x 与行数。
* 5 行共 1 页(行槽顶 y = 24+14i,末行止于 y=93,与页脚 y=143 无冲突);
* 名 x=4 / 值 x=54(值列须 ≥54:4 汉字名占 48px + 间隙;值列宽——
* IP/掩码/网关 11 ASCII 字符 = 66px 止于 119,MAC 17 字符(冒号分隔)= 102px 止于 155,
@@ -42,12 +45,8 @@
#define NC_ROW_COUNT 5u /* 4 地址行 + 1 保存行 */
#define NC_ADDR_ROW_MAX 4u /* 地址行范围 0..3(IP/掩码/网关/MAC) */
#define NC_SAVE_ROW 4u /* 保存行下标 */
#define NC_TOP_Y 24 /* 首行槽顶 y */
#define NC_PITCH 14 /* 行距 */
#define NC_BAND_H 13 /* 选中白带高 */
#define NC_LEFT_X 4 /* 左列 x(名) */
#define NC_RIGHT_X 54 /* 右列 x(值) */
#define NC_ROW_BUF_LEN 32u /* 行缓冲长度(与框架 CP_ROW_BUF_LEN 一致) */
/* 静态变量定义区 */
@@ -86,7 +85,7 @@ static uint8_t nc_octet_count(uint16_t row) {
/*
* 函数功能:生成指定地址行的值串(与 refresh 值列同源,编辑光标定位与行文本一致)
* 入口参数:row - 地址行下标(0..3) uint16_t
* p_buf - 输出缓冲(≥ NC_ROW_BUF_LEN) char *
* p_buf - 输出缓冲(≥ UI_BUF_STRLEN) char *
* 返回值:无
* 限定条件:p_buf 非空
* 函数说明:IP/掩码/网关 = "192.168.001.100"(每段 3 位补零定长,字节 i 恒在
@@ -99,7 +98,7 @@ static void nc_fmt_value(uint16_t row, char *p_buf) {
uint8_t mac[6];
net_cfg_get_mac(mac);
(void)lv_snprintf(p_buf, NC_ROW_BUF_LEN, "%02X:%02X:%02X:%02X:%02X:%02X",
(void)lv_snprintf(p_buf, UI_BUF_STRLEN, "%02X:%02X:%02X:%02X:%02X:%02X",
(unsigned)mac[0], (unsigned)mac[1], (unsigned)mac[2],
(unsigned)mac[3], (unsigned)mac[4], (unsigned)mac[5]);
return;
@@ -109,15 +108,15 @@ static void nc_fmt_value(uint16_t row, char *p_buf) {
const uint8_t *field = (row == 0u) ? cfg->ip
: ((row == 1u) ? cfg->mask : cfg->gw);
net_cfg_fmt_ipv4(field, p_buf, NC_ROW_BUF_LEN);
net_cfg_fmt_ipv4(field, p_buf, UI_BUF_STRLEN);
}
}
/*
* 函数功能:动态行刷新(逐行取名/值)
* 入口参数:row - 行下标(0..4) uint16_t
* name - 名缓冲(≥ NC_ROW_BUF_LEN,框架栈缓冲) char *
* value - 值缓冲(≥ NC_ROW_BUF_LEN,框架栈缓冲) char *
* name - 名缓冲(≥ UI_BUF_STRLEN,框架栈缓冲) char *
* value - 值缓冲(≥ UI_BUF_STRLEN,框架栈缓冲) char *
* 返回值:无
* 限定条件:页面打开(框架绘制链内逐行调用)
* 函数说明:地址行值取 net_cfg RAM 副本(MAC 全零时显示芯片实际 MAC 影子);
@@ -133,15 +132,15 @@ static void nc_refresh(uint16_t row, char *name, char *value) {
switch (row) {
case 0u:
(void)memcpy(name, "IP地址", sizeof("IP地址"));
net_cfg_fmt_ipv4(cfg->ip, value, NC_ROW_BUF_LEN);
net_cfg_fmt_ipv4(cfg->ip, value, UI_BUF_STRLEN);
break;
case 1u:
(void)memcpy(name, "子网掩码", sizeof("子网掩码"));
net_cfg_fmt_ipv4(cfg->mask, value, NC_ROW_BUF_LEN);
net_cfg_fmt_ipv4(cfg->mask, value, UI_BUF_STRLEN);
break;
case 2u:
(void)memcpy(name, "默认网关", sizeof("默认网关"));
net_cfg_fmt_ipv4(cfg->gw, value, NC_ROW_BUF_LEN);
net_cfg_fmt_ipv4(cfg->gw, value, UI_BUF_STRLEN);
break;
case 3u:
(void)memcpy(name, "MAC地址", sizeof("MAC地址"));
@@ -197,7 +196,7 @@ static void nc_adjust_octet(uint16_t row, int8_t delta) {
* 步进宽实测定位(ASCII 步进 6px/字符,与行文本布局一致)
*/
static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
char value[NC_ROW_BUF_LEN];
char value[UI_BUF_STRLEN];
lv_area_t box;
uint32_t start;
uint32_t seg_len;
@@ -219,14 +218,14 @@ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
start = (uint32_t)s_cursor * ((sel == 3u) ? 3u : 4u); /* 字节 i 字符偏移:IP/掩码/网关 4i,MAC 3i(冒号分隔) */
seg_len = (sel == 3u) ? 2u : 3u;
slot_y = NC_TOP_Y + (int32_t)sel * NC_PITCH;
slot_y = UI_LIST_TOP + (int32_t)sel * UI_ROW_PITCH;
x0 = NC_RIGHT_X + (int32_t)lv_text_get_width(value, start, LV_FONT_DEFAULT, 0);
w = (int32_t)lv_text_get_width(value + start, seg_len, LV_FONT_DEFAULT, 0);
box.x1 = x0 - 1;
box.x2 = x0 + w;
box.y1 = slot_y;
box.y2 = slot_y + NC_BAND_H - 1;
box.y2 = slot_y + UI_ROW_BAND_H - 1;
if (!nc_areas_intersect(&box, clip)) {
return; /* 编辑光标不在重绘区:跳过(culling) */
}
@@ -237,7 +236,7 @@ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = lv_color_black();
fd.bg_color = UI_COL_BG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &box);
@@ -255,7 +254,7 @@ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
ld.base.layer = layer;
ld.text = seg;
ld.text_static = 0u; /* 栈缓冲(同帧有效) */
ld.color = lv_color_white();
ld.color = UI_COL_FG;
ld.ofs_x = 1; /* box.x1 = x0-1,+1 = x0(与框架行文本原点一致) */
ld.ofs_y = 0; /* 与行文本垂直对齐(text_dy = 0) */
lv_draw_label(layer, &ld, &box);
@@ -346,18 +345,19 @@ static void nc_init_cb(void) {
}
/*
* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用):
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=net_cfg 绑定引用):
* 列表页(5 行 1 页,动态值)+ 可编辑(edit_capable,还须修改权限)
* + ← 拦截(编辑模式移字节光标;ESC 恒不可拦截)
*/
const content_page_def_t ui_page_net_cfg_desc = {
.id = "net_cfg",
.title = "网口设置",
.selectable = 1u,
.key_intercept = 1u,
.edit_capable = 1u,
.top_y = NC_TOP_Y,
.pitch = NC_PITCH,
.band_h = NC_BAND_H,
.top_y = UI_LIST_TOP,
.pitch = UI_ROW_PITCH,
.band_h = UI_ROW_BAND_H,
.text_dy = 0,
.left_x = NC_LEFT_X,
.right_x = NC_RIGHT_X,
+1 -1
View File
@@ -19,7 +19,7 @@
extern "C" {
#endif
/* 页面描述符(框架描述符表 s_defs[] 引用) */
/* 页面描述符(生成物 ui_page_registry.c 按菜单树 #page=net_cfg 注记绑定引用;.id = "net_cfg") */
extern const content_page_def_t ui_page_net_cfg_desc;
#ifdef __cplusplus
+11 -9
View File
@@ -27,6 +27,8 @@
#include "ui_pwd_def.h"
#include "ui_tokens.h"
/* 私有宏定义区 */
/*
@@ -45,7 +47,6 @@
#define PW_TITLE_Y 44 /* 标题放置 y(中上;0 = 框架默认,实机明显低于其余各页的 y=2) */
#define PW_ERR_Y 104 /* "密码输入错误"提示行顶 y(数字框行之下) */
#define PW_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致;提示文本居中用) */
/* 静态变量定义区 */
@@ -130,16 +131,16 @@ static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
fd.bg_opa = LV_OPA_COVER;
if (i == s_cur) {
/* 光标框:反显(1bpp:white=暗底,black=亮字) */
fd.bg_color = lv_color_white();
fd.bg_color = UI_COL_SEL_BG;
}
else {
/* 非光标框:亮底暗字 + 1px 暗边(1bpp:black=亮底,white=暗) */
fd.bg_color = lv_color_black();
fd.border_color = lv_color_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) ? lv_color_black() : lv_color_white());
pw_draw_digit(layer, &box, s_digits[i], (i == s_cur) ? UI_COL_SEL_FG : UI_COL_FG);
}
if (s_err != 0u) {
@@ -152,16 +153,16 @@ static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
if (w > 0) {
lv_area_t err_box;
err_box.x1 = (PW_SCREEN_W - w) / 2;
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 + 15u); /* 覆盖字体行高(13)+ 余量(同框架约定) */
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 = lv_color_white();
ld.color = UI_COL_FG;
ld.ofs_x = 0;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &err_box);
@@ -256,12 +257,13 @@ void ui_page_password_arm(void (*on_ok)(uint8_t perm)) {
}
/*
* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用):
* 页面描述符(外部链接:生成物 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,
+4 -3
View File
@@ -18,8 +18,9 @@
#include "ui_content_page.h"
/*
* 页面描述符(Flash const):用户密钥页 = CONTENT_PAGE_PASSWORD;
* 接入框架描述符表 s_defs[](定义于 ui_page_password.c)
* 页面描述符(Flash const):用户密钥页(描述符 .id = "password");
* 由生成物 ui_page_registry.c 按菜单树 `# @password=password` 指令**单独出口**
* (密码页非节点绑定:任何 #P 叶子都进同一页,故不在 node→desc 映射里)
*/
extern const content_page_def_t ui_page_password_desc;
@@ -30,7 +31,7 @@ extern const content_page_def_t ui_page_password_desc;
* 由逻辑层实现"关页 + 按权限续进入";
* NULL = 一致时仅关页)
* 返回值:无
* 限定条件:由逻辑层在 uiTask 上下文、ui_content_page_open(CONTENT_PAGE_PASSWORD)
* 限定条件:由逻辑层在 uiTask 上下文、ui_content_page_open(ui_page_registry_password())
* 之前调用
* 函数说明:页面比对 ui_pwd_def.h 的两个编译期密码(先查修改:两值同设时授高权);
* 每次进入重置为初始态(全 0、光标首框,与实机一致);页面模块不感知菜单
+54
View File
@@ -0,0 +1,54 @@
/*
* 模块名称:UI 页面注册表(自动生成)
* 模块功能:叶子节点 → 内容页描述符映射表 + 2 个查询 API(导航逻辑层消费)
* 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)
* 作者:王建锋
* 创建日期:2026-10-15
* 自动生成:本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 的 #page= 注记生成,请勿手改。
* 新增一页:菜单树给叶子加 " #page=<模块名>" + 写 ui_page_<模块名>.c,重跑生成器
*/
#include "ui_page_registry.h"
#include "ui_page_ac.h"
#include "ui_page_dc.h"
#include "ui_page_device_info.h"
#include "ui_page_net_cfg.h"
#include "ui_page_password.h"
#include "ui_page_runlog.h"
#include "ui_page_selftest.h"
const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = {
{ 1u, &ui_page_device_info_desc }, /* node 1 装置信息 */
{ 3u, &ui_page_ac_desc }, /* node 3 交流量 */
{ 4u, &ui_page_dc_desc }, /* node 4 直流量 */
{ 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */
{ 39u, &ui_page_selftest_desc }, /* node 39 自检记录 */
{ 40u, &ui_page_runlog_desc }, /* node 40 运行记录 */
};
/*
* 函数功能:查叶子节点绑定的内容页描述符
* 入口参数:node - 菜单叶子节点下标 uint8_t
* 返回值:页面描述符;未绑定返回 NULL const content_page_def_t *
* 限定条件:无(越界 node 不会命中任何条目,直接 NULL)
* 函数说明:线性扫描绑定表(条目 = 有内容页的叶子,个位数量级)
*/
const content_page_def_t *ui_page_registry_for(uint8_t node) {
for (uint8_t i = 0u; i < UI_PAGE_MAP_COUNT; i++) {
if (g_ui_page_map[i].node == node) {
return g_ui_page_map[i].desc;
}
}
return NULL;
}
/*
* 函数功能:取密码页描述符(需密码叶子统一进这一页,非节点绑定)
* 入口参数:无
* 返回值:密码页描述符 const content_page_def_t *
* 限定条件:无
* 函数说明:取值来自 ui_menu_tree.txt 的 @password 指令(生成期解析,运行期零开销)
*/
const content_page_def_t *ui_page_registry_password(void) {
return &ui_page_password_desc;
}
+56
View File
@@ -0,0 +1,56 @@
/*
* 模块名称:UI 页面注册表头文件(自动生成)
* 模块功能:叶子节点 → 内容页描述符的映射表与查询 API;密码页非节点绑定,单独出口
* 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)
* 作者:王建锋
* 创建日期:2026-10-15
* 自动生成:本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 的 #page= 注记生成,请勿手改。
* 新增一页:菜单树给叶子加 " #page=<模块名>" + 写 ui_page_<模块名>.c,重跑生成器
* (双端注册 uvprojx/CMakeLists 亦由生成器自动写入)
*/
#ifndef UI_PAGE_REGISTRY_H
#define UI_PAGE_REGISTRY_H
#include <stdint.h>
#include "ui_content_page.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 注册表条目:一个绑定了内容页的菜单叶子(表序 = 节点下标升序) */
typedef struct {
uint8_t node; /* 菜单叶子节点下标(g_menu_tab 下标) */
const content_page_def_t *desc; /* 页面描述符(Flash const) */
} ui_page_entry_t;
#define UI_PAGE_MAP_COUNT 6u /* 绑定了内容页的叶子数 */
extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT];
/*
* 函数功能:查叶子节点绑定的内容页描述符
* 入口参数:node - 菜单叶子节点下标 uint8_t
* 返回值:页面描述符;该叶子未绑定内容页返回 NULL const content_page_def_t *
* 限定条件:node < UI_MENU_TAB_COUNT
* 函数说明:取代旧 ui_menu.c 手写 case 分发——映射的唯一真源是 ui_menu_tree.txt,
* 表序变化不会造成"下标硬编码错位"(线性扫描:绑定页是个位数量级)
*/
const content_page_def_t *ui_page_registry_for(uint8_t node);
/*
* 函数功能:取密码页描述符(需密码叶子统一进这一页)
* 入口参数:无
* 返回值:密码页描述符;菜单树未声明 @password 指令时返回 NULL const content_page_def_t *
* 限定条件:无
* 函数说明:密码页**不是**节点绑定的(任何 #P 叶子都进同一页),故不进 node→desc 映射,
* 由本接口单独出口;取值来自 ui_menu_tree.txt 的 `# @password=<模块名>` 指令
*/
const content_page_def_t *ui_page_registry_password(void);
#ifdef __cplusplus
}
#endif
#endif /* UI_PAGE_REGISTRY_H */
+13 -10
View File
@@ -16,6 +16,8 @@
#include "ui_page_runlog.h"
#include "ui_tokens.h"
#include "sys_log.h"
/* 静态变量定义区 */
@@ -45,7 +47,7 @@ static uint16_t rl_count_cb(void) {
/*
* 函数功能:逐行取数(偶数行 = 时间戳、奇数行 = 描述;值列恒空)
* 入口参数:row - 全局行下标 uint16_t
* name - 名输出缓冲(框架栈缓冲 CP_ROW_BUF_LEN=32,含 NUL) char *
* name - 名输出缓冲(框架栈缓冲 UI_BUF_STRLEN=32,含 NUL) char *
* value - 值输出缓冲(同上;本页恒写空串) char *
* 限定条件:页面打开中(框架绘制/按键上下文,uiTask)
*/
@@ -79,21 +81,22 @@ static void rl_refresh(uint16_t row, char *name, char *value) {
}
/*
* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用):
* 动态列表页——每记录 2 行、8 行/页 = 4 条/页;band_h=13(同 AC/DC 约定:
* selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见);
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=runlog 绑定引用):
* 动态列表页——每记录 2 行、UI_LIST_ROWS 行/页 = 4 条/页;band_h=UI_ROW_BAND_H(同 AC/DC
* 约定:selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见);
* 时间戳 19 ASCII ≈114px < 屏宽可容
*/
const content_page_def_t ui_page_runlog_desc = {
.id = "runlog",
.title = "运行记录",
.selectable = 1u,
.top_y = 24,
.pitch = 14,
.band_h = 13,
.top_y = UI_LIST_TOP,
.pitch = UI_ROW_PITCH,
.band_h = UI_ROW_BAND_H,
.text_dy = 0,
.left_x = 7,
.right_x = 101,
.rows_per_page = 8u,
.left_x = UI_LIST_LEFT_X,
.right_x = UI_LIST_RIGHT_X,
.rows_per_page = UI_LIST_ROWS,
.count_cb = rl_count_cb,
.refresh = rl_refresh,
};
+2 -2
View File
@@ -16,8 +16,8 @@
#include "ui_content_page.h"
/*
* 页面描述符(Flash const):运行记录页 = CONTENT_PAGE_RUN_LOG;
* 接入框架描述符表 s_defs[](定义于 ui_page_runlog.c)
* 页面描述符(Flash const):运行记录页(描述符 .id = "runlog");
* 由生成物 ui_page_registry.c 按菜单树 #page=runlog 注记绑定引用(定义于 ui_page_runlog.c)
*/
extern const content_page_def_t ui_page_runlog_desc;
+13 -10
View File
@@ -16,6 +16,8 @@
#include "ui_page_selftest.h"
#include "ui_tokens.h"
#include "sys_log.h"
/* 静态变量定义区 */
@@ -45,7 +47,7 @@ static uint16_t st_count_cb(void) {
/*
* 函数功能:逐行取数(偶数行 = 时间戳、奇数行 = 故障描述;值列恒空)
* 入口参数:row - 全局行下标 uint16_t
* name - 名输出缓冲(框架栈缓冲 CP_ROW_BUF_LEN=32,含 NUL) char *
* name - 名输出缓冲(框架栈缓冲 UI_BUF_STRLEN=32,含 NUL) char *
* value - 值输出缓冲(同上;本页恒写空串) char *
* 限定条件:页面打开中(框架绘制/按键上下文,uiTask)
*/
@@ -79,21 +81,22 @@ static void st_refresh(uint16_t row, char *name, char *value) {
}
/*
* 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用):
* 动态列表页——每记录 2 行、8 行/页 = 4 条/页;band_h=13(同 AC/DC 约定:
* selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见);
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=selftest 绑定引用):
* 动态列表页——每记录 2 行、UI_LIST_ROWS 行/页 = 4 条/页;band_h=UI_ROW_BAND_H(同 AC/DC
* 约定:selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见);
* 时间戳 19 ASCII ≈114px < 屏宽可容
*/
const content_page_def_t ui_page_selftest_desc = {
.id = "selftest",
.title = "自检记录",
.selectable = 1u,
.top_y = 24,
.pitch = 14,
.band_h = 13,
.top_y = UI_LIST_TOP,
.pitch = UI_ROW_PITCH,
.band_h = UI_ROW_BAND_H,
.text_dy = 0,
.left_x = 7,
.right_x = 101,
.rows_per_page = 8u,
.left_x = UI_LIST_LEFT_X,
.right_x = UI_LIST_RIGHT_X,
.rows_per_page = UI_LIST_ROWS,
.count_cb = st_count_cb,
.refresh = st_refresh,
};
+2 -2
View File
@@ -16,8 +16,8 @@
#include "ui_content_page.h"
/*
* 页面描述符(Flash const):自检记录页 = CONTENT_PAGE_SELFTEST_LOG;
* 接入框架描述符表 s_defs[](定义于 ui_page_selftest.c)
* 页面描述符(Flash const):自检记录页(描述符 .id = "selftest");
* 由生成物 ui_page_registry.c 按菜单树 #page=selftest 注记绑定引用(定义于 ui_page_selftest.c)
*/
extern const content_page_def_t ui_page_selftest_desc;
+20 -33
View File
@@ -17,6 +17,8 @@
#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"
@@ -25,11 +27,9 @@
/*
* 布局常量(设备像素;参考 mockups/待机显示页面.jpg):
* 6 路电流值行自 y=28 起、行距 18(末行 y=118..130);分隔线 y=132;底部时钟 y=140 居中
* 屏幕宽高取令牌 UI_SCREEN_W/H;6 路电流值行自 y=28 起、行距 18
* (末行 y=118..130);分隔线 y=132;底部时钟 y=140 居中
*/
#define SB_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */
#define SB_SCREEN_H 160 /* 屏幕高(px) */
#define SB_CH_COUNT 6u /* 电流通道数 */
#define SB_ROW_Y0 28 /* 首数据行顶 y */
#define SB_PITCH 18 /* 行距(相邻行槽顶间距) */
@@ -55,21 +55,8 @@ 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 主题给普通 lv_obj 附加的 card 样式(同 ui_content_page.c) */
static const lv_style_const_prop_t sb_flat_props[] = {
{ .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } },
{ .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } },
{ .prop = LV_STYLE_RADIUS, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } },
LV_STYLE_CONST_PROPS_END
};
LV_STYLE_CONST_INIT(sb_style_flat, sb_flat_props);
/* flat 样式(透明、无边框/圆角/内边距,剥离 mono 主题 card)取自共享样式表
* ui_style.c:与 ui_content_page / ui_menu_view 共用同一 const 对象,不再本地重复定义 */
/* 函数定义区 */
@@ -99,15 +86,15 @@ static void sb_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y
lv_draw_label_dsc_t ld;
area.x1 = 0;
area.x2 = SB_SCREEN_W - 1;
area.x2 = UI_SCREEN_W - 1;
area.y1 = y;
area.y2 = y + 15; /* 覆盖字体行高(13)+ 余量 */
area.y2 = y + UI_TEXT_BAND_H; /* 覆盖字体行高(13)+ 余量 */
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = text;
ld.color = lv_color_white();
ld.ofs_x = (SB_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2;
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);
}
@@ -128,7 +115,7 @@ static void sb_draw_cb(lv_event_t *e) {
lv_draw_label_dsc_t ld;
strip.x1 = 0;
strip.x2 = SB_SCREEN_W - 1;
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)) {
@@ -139,7 +126,7 @@ static void sb_draw_cb(lv_event_t *e) {
ld.base.layer = layer;
ld.text = s_ch_name[i];
ld.text_static = 1;
ld.color = lv_color_white();
ld.color = UI_COL_FG;
ld.ofs_x = SB_LABEL_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &strip);
@@ -148,7 +135,7 @@ static void sb_draw_cb(lv_event_t *e) {
ld.base.layer = layer;
ld.text = "=";
ld.text_static = 1;
ld.color = lv_color_white();
ld.color = UI_COL_FG;
ld.ofs_x = SB_EQ_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &strip);
@@ -157,7 +144,7 @@ static void sb_draw_cb(lv_event_t *e) {
ld.base.layer = layer;
ld.text = s_ch_value[i];
ld.text_static = 1;
ld.color = lv_color_white();
ld.color = UI_COL_FG;
ld.ofs_x = SB_VALUE_X;
ld.ofs_y = 0;
lv_draw_label(layer, &ld, &strip);
@@ -169,13 +156,13 @@ static void sb_draw_cb(lv_event_t *e) {
lv_draw_rect_dsc_t fd;
line.x1 = 8;
line.x2 = SB_SCREEN_W - 9;
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 = lv_color_white();
fd.bg_color = UI_COL_FG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &line);
}
@@ -186,9 +173,9 @@ static void sb_draw_cb(lv_event_t *e) {
lv_area_t t;
t.x1 = 0;
t.x2 = SB_SCREEN_W - 1;
t.x2 = UI_SCREEN_W - 1;
t.y1 = SB_TIME_Y;
t.y2 = (int32_t)(SB_TIME_Y + 15);
t.y2 = (int32_t)(SB_TIME_Y + UI_TEXT_BAND_H);
if (sb_areas_intersect(&t, &layer->_clip_area)) {
char ts[24];
@@ -307,8 +294,8 @@ int ui_standby_init(uint32_t idle_timeout_ms, ui_standby_time_cb time_cb) {
return -1;
}
lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT);
lv_obj_set_size(s_cont, SB_SCREEN_W, SB_SCREEN_H);
lv_obj_add_style(s_cont, &sb_style_flat, 0);
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);
+36
View File
@@ -0,0 +1,36 @@
/*
* 模块名称:UI Shared Style (UI 共享样式表)
* 模块功能:flat 常量样式的属性数组与样式对象定义(声明见 ui_style.h);
* 消费者 = ui_menu_view.c / ui_content_page.c / ui_standby.c
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-15
* 修改记录:
* v1.0 2026-10-15 王建锋 创建初始版本(U3:原三处逐字相同的定义合并到此单一来源)
*/
/* 头文件包含区 */
#include "ui_style.h"
/* 静态变量定义区 */
/*
* flat 属性数组:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的
* card 样式。用常量样式(LV_STYLE_CONST_INIT):属性存静态数组、零 LV_MEM 池分配;
* 动态样式首次 set_prop 会从池分配 values_and_props 且永不释放(x64 实测 88 B/个)。
* 属性项与顺序 = 合并前三处定义的公共内容(三者原本逐字相同)。
*/
static const lv_style_const_prop_t s_flat_props[] = {
{ .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } },
{ .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } },
{ .prop = LV_STYLE_RADIUS, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } },
{ .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } },
LV_STYLE_CONST_PROPS_END
};
LV_STYLE_CONST_INIT(ui_style_flat, s_flat_props);
+38
View File
@@ -0,0 +1,38 @@
/*
* 模块名称:UI Shared Style (UI 共享样式表)
* 模块功能:跨模块复用的 LVGL 常量样式单一定义处——目前只有 flat(背景透明 +
* 边框/圆角/内边距全 0,用于剥离 mono 主题给普通 lv_obj 附加的 card 样式);
* 菜单显示层 ui_menu_view、内容页框架 ui_content_page、待机页 ui_standby
* 三处共用同一 const 对象(此前各自定义一份逐字相同的 9 项属性数组,
* = .rodata 里三份重复副本);
* 常量样式属性存静态数组、零 LV_MEM 池分配——动态样式首次 set_prop 会从
* 池分配 values_and_props 且永不释放(x64 实测 88 B/个),故全仓禁用
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-15
* 修改记录:
* v1.0 2026-10-15 王建锋 创建初始版本(U3:合并三处逐字重复的 flat 常量样式定义)
*/
#ifndef UI_STYLE_H
#define UI_STYLE_H
#include "lvgl.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
* flat 样式:背景透明 + 边框/圆角/内边距全 0。
* 用途:mono 主题会给普通 lv_obj 附加 card 样式(可见边框与内边距),自绘容器
* 必须先剥掉它,才能拿到干净的整屏绘制面。
* 定义在 ui_style.c;样式在 LVGL 内按指针被对象引用,三处共享同一 const 对象
* 不改变任何绘制结果(属性值与原三份逐字节相同)。
*/
extern const lv_style_t ui_style_flat;
#ifdef __cplusplus
}
#endif
#endif /* UI_STYLE_H */
+68
View File
@@ -0,0 +1,68 @@
/*
* 模块名称:UI Design Tokens (UI 设计令牌层 L0)
* 模块功能:UI 视觉骨架的唯一取值来源——屏幕几何、内容页骨架(标题/页脚/文本
* 条带/选中带内缩)、列表页原型(首行顶/行距/带高/两列 x/每页行数)、
* 滚动条宽、文本缓冲长度、1bpp 语义色、UI 任务节拍与待机阈值;
* 各页面/框架文件只引用本头令牌,不再自带同值字面量
* (治理背景见 docs/问题记录/项目审查与优化建议报告.md §6.3 L0);
* 纯取值定义,不含任何逻辑;改一处即全局生效,改前必须跑 ui-verify 像素回归
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-15
* 修改记录:
* v1.0 2026-10-15 王建锋 创建初始版本(U1:收编散落各 UI 文件的布局/颜色/缓冲魔数,
* 纯替换、零行为变化;像素与替换前逐位一致)
*/
#ifndef UI_TOKENS_H
#define UI_TOKENS_H
/*
* 依赖说明:
* 1. 屏幕几何(UI_SCREEN_W / UI_SCREEN_H / UI_FB_SIZE)的**权威定义在 ui_port.h**
* (显示端口,MCU 与模拟器两端共用),本头复用而不重复定义——避免同名宏二次定义。
* 2. 语义色宏展开为 LVGL 取色函数调用,故引用方须已包含 "lvgl.h"
* (UI 各 .c 均经 ui_content_page.h / ui_menu_view.h 等间接包含)。
*/
#include "ui_port.h"
/* ── 内容页骨架(框架 ui_content_page.c 与各自绘页共用)────────────────────── */
#define UI_TITLE_Y 2 /* 标题放置 y(居中;CJK 字形顶距 +1 → 墨迹自 y=3 起) */
#define UI_PAGE_LINE_Y 143 /* 页脚"第 X 页,共 Y 页"放置 y */
#define UI_TEXT_BAND_H 15 /* 单行文本条带下延量(字体行高 13 + 余量):y2 = y + 本值 */
#define UI_BUF_STRLEN 32 /* 动态行 / 页脚文本缓冲长度(含 NUL;超出行带即被裁切) */
#define UI_SEL_BAND_INSET_L 2 /* 选中白带左内缩(x1 = 本值) */
#define UI_SEL_BAND_INSET_R 3 /* 选中白带右内缩(x2 = 屏宽 - 本值,即止于 x=156) */
#define UI_FOOTER_X 6 /* 页脚文本左起 x(左对齐留白,与实机一致) */
/* ── 列表页原型(交流量/直流量/运行记录/自检记录/网口设置共用一套骨架)──────── */
#define UI_LIST_TOP 24 /* 首行槽顶 y */
#define UI_ROW_PITCH 14 /* 行距(相邻行槽顶间距) */
#define UI_ROW_BAND_H 13 /* 选中白带高(= 字体行高;selectable 页须 >0 否则选中行不可见) */
#define UI_LIST_LEFT_X 7 /* 左列 x(名) */
#define UI_LIST_RIGHT_X 101 /* 右列 x(值) */
#define UI_LIST_ROWS 8u /* 每页可见行数 */
/* ── 菜单显示层 ───────────────────────────────────────────────────────────── */
#define UI_SCROLLBAR_W 4 /* 自动滚动条宽(px;行超框高时出现) */
/* ── 1bpp 语义色(LV_COLOR_FORMAT_I1)─────────────────────────────────────── */
/*
* 本屏只有黑白两值,但保留语义名:换色深时只改本段即可平移,不必回改各绘制点。
* 极性(实测,勿凭直觉改):文本/分隔线/边框一律取 white(1bpp 下即"暗墨"),
* black 是底;选中行反显 = 白底黑字,故 SEL 前景取 black、SEL 背景取 white。
*/
#define UI_COL_FG lv_color_white() /* 墨:文本 / 分隔线 / 边框 / 滚动条 */
#define UI_COL_BG lv_color_black() /* 底:黑 */
#define UI_COL_SEL_FG lv_color_black() /* 选中行字色(反显后为黑字) */
#define UI_COL_SEL_BG lv_color_white() /* 选中行带底色(反显后为白带) */
/* ── UI 任务与待机策略 ────────────────────────────────────────────────────── */
#define UI_TASK_TICK_MS 10 /* uiTask 主循环周期:LVGL 推进步长 = osDelay 时长 */
#define UI_STANDBY_IDLE_MS 60000u /* 无操作进入待机屏的阈值(ms) */
#endif /* UI_TOKENS_H */