Files
STM32F4-Base/Drivers/BSP/UI/ui_page_action.c
T
wandering1 52e28885cb feat(UI): 装置维护 3 页落地(时钟 / 强制复归 / 清除记录),内容页黄金 27→40
三个薄页面模块 ui_page_clock / ui_page_reset / ui_page_clear,页面注册由生成器
自动写双端(裁定 A-1:「三叶各绑一页」与「零新增 .c」不可兼得,选薄页面加生成器,
不写手写 node switch——那种分发在增删叶子时会静默换功能,且没有任何门禁照得到)。

数据侧是注入式函数表:UI 不 include App 层的头。两条硬理由都实测过——分层方向
(layer-deps)之外,模拟器根本不链 sys_clock,ALLOW 豁免挡得住 layer-deps,
挡不住模拟器链接期。面板对时的唯一真判据是回读 RTC:sys_clock_set() 恒返 0
(RTC 写失败只打日志),所以真机侧写 1 后回读 1,最坏约 200 ms 落在一次「保存」
按键上,IWDG 800 ms 窗有余;上界与依据都写进注释。

「无时钟」徽标位置有两个像素级实测坑:放标题带右侧会被 4 字标题连成「时钟设置
无时钟」一句话;放页脚带左侧 x=7 会压住页脚——页脚从 x=6 起排,不是居中。
最终落在清单末行与页脚之间的空带 y=124。clear_done 那张顺带给出像素证据:
清 run.log 之后「操作记录」那一行从 2 变 3,页面上没缓存、没撒谎。

ui_w_records 的 UI_W_REC_LOG_* 顺序必须等于 syslog_id_t 顺序(7 类,只准尾追加),
已加编译期顺序守卫;执行侧 7 条映射表同样带编译期守卫。

测试与真值:内容页黄金 27 → 40 页(+13),文本断言 +68、反向断言 ABSENT +19;
27 张旧参考 git diff 为空 = 逐字节未变。往返池 5448 B 与菜单常驻堆 5368 B 均未变;
字库未扩(源码在用 171 并 KEEP_EXTRA 39 = 清单 183),font-charset 与 cn-chars、
cn-font 字节一致。@build 0 错误 0 警告;uiTask 静态最坏链 1,336 B / 16,384
(余 91%)与基线相同,页面钩子没进最坏链。

一条测试学结论单独留痕:反向断言不能当「位置不重叠」的证据——本脚本的字形匹配
把 y 带判得比字面宽(徽标在 125..136、页脚在 144..155,仍被 (140,160) 判命中)。
该防线改由正向断言「页脚未被徽标压住」承担(压字那一版实测过不了它)。

两条自我更正一并记档(作者自报,按真值而非原口径):
① 先前报「文案零新增汉字」是错的——观测行拟用「动作元件」,「元」不在子集,
cn-chars 与 font-charset 如实红;最终改「保护动作 N」才让「零新增」成为事实。
顺序是:先按机器红,再按真值写结论。
② 先前报「6 个文件 BOM 逐个核过」被 bom-guard 证伪:丢 BOM 发生在上一次编译之后,
正是 AGENTS 那句「历史 0 警告不代表安全」的活例;此后一律以退出码为准。

未覆盖项不假装:台架「N>0 → N=0」在产品构建的未标定闭锁期拿不到(闭锁期 N 恒 0),
判据显式退化为「页面受理 + 落一条操作记录」,写在 rs_watch 注释里。

生成物入库例外照用:uvprojx 只加 3 个 ui_page_ 的 FilePath(真增删源文件),
CMakeLists 同批;两处都由 gen_menu.py 自己写,没手写。

本笔先走了一次真钩子:20 条钩子实跑通过(ui-golden 菜单 8 状态加内容页 40 页、
generated-fresh、registration-check、font-charset、ui-data 单测、cppcheck、
coding-standard、bom、mojibake、id-check、doc-links 等),只有 mem-numbers 与
mem-budget 两条红——磁盘上的 .map 出自 20:39 那次构建、含三条在飞道的代码,
与 HEAD 台账必然不同码。按仓库既有约定(这两条等台账回填时允许 --no-verify 并
正文留证)落这一笔;回填由我统一在收尾一笔做。

三条在飞道(task-26 崩溃证据路径、task-16 与 task-17 谓词收敛和卷锁观测、
task-36 台架工具)的未暂存改动被 autostash 暂时收走,钩子看到的是 HEAD 加本笔;
过程中曾把 comm 道未跟踪的 iec104_comm.c 临时移出(它不在 stash 覆盖面内),
那次移出反而让 ui-golden 撞见 CMake 缓存里的悬空引用而红——已改用「先落 comm
那笔、再落本笔」的顺序消掉这个中间态。ui_data_maint_ready() 待删(只有测试在读
的自检口),排在本笔之后单独一笔。
2026-10-26 22:43:54 +08:00

238 lines
11 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI 动作确认页(操作页共享实现)实现
* 模块功能:见 ui_page_action.h 头注释——本文件是那三个钩子的实现体:
* init 复位确认链、draw 画问句/态标/观测行/键提示四行、key 走状态机并
* 只投递不阻塞。实例由页面跳板持有,本文件不再有模块单例,也不建描述符
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道B task-5 三组件之一)
* v1.1 2026-10-26 DTU 项目组 道F task-3:删模块单例 s_act/s_desc 与
* ui_page_action_open()(全仓零调用者),改出
* ui_page_action_init/draw/key 三个收实例指针的
* 钩子;新增只读拒绝(need_modify → 态标 LOCK、
* 键提示 ESC=返回)与可选观测行(watch_label +
* watch_cb);返回值只留 uint8_t 分发标志
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_page_action.h"
#include "ui_content_page.h"
#include "ui_tokens.h"
/* 私有宏定义区 */
#define AC_PROMPT_Y 44 /* 问句/结果行 放置 y(内容区上三分之一) */
#define AC_STAGE_Y 66 /* 态标行("二次确认" / "ERR" / "LOCK")放置 y */
#define AC_WATCH_Y 88 /* 可选观测行放置 y(实例没配观测行时这行留空) */
#define AC_HINT_Y 110 /* 键提示行 放置 y */
/* 固定文案(用字均已入字体子集:二/次/确/认/取/消/返/回/越/界 及 ASCII;
* 道F task-3 起新增的"只读"两词沿用同一批用字,未扩字库) */
#define AC_STAGE2_TXT "二次确认"
#define AC_HINT_DO "ENTER=确认 ESC=取消"
#define AC_HINT_BACK "ENTER=返回"
#define AC_HINT_RO "ESC=返回" /* 只读:ENTER 无效,提示必须跟着改口 */
#define AC_ERR_TXT "ERR"
#define AC_RO_TXT "LOCK" /* 只读进入的态标(词表与整定保存行一致,
都是 ASCII,见 UI_数据供给设计说明.md §6.3) */
/* 私有函数声明区 */
static void ac_draw_centered(lv_layer_t *a_layer, const char *a_text,
int32_t a_y, const lv_area_t *a_clip);
/* 函数定义区 */
/*
* 函数功能:居中绘制单行文本(框架 cp_draw_centered_text 同法,本文件私有一致拷贝)
* 入口参数:layer - 绘制层 lv_layer_t *
* text - 文本(Flash 字面量) const char *
* y - 放置 y int32_t
* clip - 重绘区 const lv_area_t *
* 返回值:无
* 限定条件:text 非空;条带(整宽 x0..159,高 UI_TEXT_BAND_H)与 clip 相交才绘制
*/
static void ac_draw_centered(lv_layer_t *a_layer, const char *a_text,
int32_t a_y, const lv_area_t *a_clip) {
lv_area_t t;
lv_draw_label_dsc_t ld;
/* 本 LVGL 版 lv_text_get_width:必须传 strlen(0 = 不测量返回 0,
* 传 -1 越串尾死循环——框架同款注释警示) */
t.x1 = 0;
t.x2 = UI_SCREEN_W - 1;
t.y1 = a_y;
t.y2 = a_y + UI_TEXT_BAND_H;
if (!((t.x1 <= a_clip->x2) && (a_clip->x1 <= t.x2) &&
(t.y1 <= a_clip->y2) && (a_clip->y1 <= t.y2))) {
return; /* 不相交(culling) */
}
lv_draw_label_dsc_init(&ld);
ld.base.layer = a_layer;
ld.text = a_text;
ld.text_static = 1u; /* Flash 字面量:不拷内存 */
ld.color = UI_COL_FG;
ld.ofs_x = (UI_SCREEN_W -
(int32_t)lv_text_get_width(a_text, (uint32_t)strlen(a_text), LV_FONT_DEFAULT, 0)) / 2;
ld.ofs_y = 0;
lv_draw_label(a_layer, &ld, &t);
}
/*
* 函数功能:开页复位确认链(框架 init_cb 的实现体)
* 入口参数:a_inst - 操作页实例 ui_page_action_t *
* 返回值:无
* 限定条件:内容页框架开页链内调用(页面描述符的 init_cb 转发到此)
* 函数说明:stage 每次进页归零是**判据**不是收尾清洁:上一轮按到"二次确认"就
* ESC 退出、下一轮进来看都不看再按 ENTER,如果 stage 跨次保留就直接
* 执行了——破坏性动作不接受这种路径。err 一并清,否则上一次的投递
* 失败会挂在新一页的态标行上(用户以为新页一进来就出错)
*/
void ui_page_action_init(ui_page_action_t *a_inst) {
if (a_inst == NULL) {
return; /* 防御:跳板没填实例(编译期就该发现,运行期只保不崩) */
}
a_inst->stage = 0u;
a_inst->err = 0u;
}
/*
* 函数功能:只读进入判定(本次开页是否"没有修改权限")
* 入口参数:a_inst - 操作页实例 const ui_page_action_t *
* 返回值:1 = 本页需要修改权限而本次只有查看权限;0 = 其余 uint8_t
* 限定条件:框架绘制/按键链内(开页时权限已装载,is_editable 读到的就是本次的)
* 函数说明:判据只在这一处——绘制与按键两侧各自判一次,早晚在"屏上能按但按不动"
* 这种半套状态上分家
*/
static uint8_t ui_page_action_is_readonly(const ui_page_action_t *a_inst) {
if (a_inst->need_modify == 0u) {
return 0u;
}
return (ui_content_page_is_editable() == false) ? 1u : 0u;
}
/*
* 函数功能:自绘(框架内置区块之后:问句 / 态标 / 可选观测行 / 键提示,四行居中)
* 入口参数:a_inst - 操作页实例 ui_page_action_t *
* a_layer - 绘制层 lv_layer_t *
* a_clip - 重绘区 const lv_area_t *
* 返回值:无
* 限定条件:经页面描述符 draw_cb 挂接;实例或 prompt 为 NULL 时空操作
* 函数说明:行 2 的优先级是刻意的——只读(LOCK) > 二次确认 > 投递失败(ERR):
* 没权限时画"二次确认"会让人继续按,比不画更坏
*/
void ui_page_action_draw(ui_page_action_t *a_inst, lv_layer_t *a_layer,
const lv_area_t *a_clip) {
if ((a_inst == NULL) || (a_inst->prompt == NULL)) {
return;
}
/* 行 1:问句(stage 2 换成功结果行;done_text=NULL 时根本走不到 stage 2) */
if (a_inst->stage == 2u) {
if (a_inst->done_text != NULL) {
ac_draw_centered(a_layer, a_inst->done_text, AC_PROMPT_Y, a_clip);
}
} else {
ac_draw_centered(a_layer, a_inst->prompt, AC_PROMPT_Y, a_clip);
}
/* 行 2:态标(只读 / 二次确认 / 投递失败) */
if (ui_page_action_is_readonly(a_inst) != 0u) {
ac_draw_centered(a_layer, AC_RO_TXT, AC_STAGE_Y, a_clip);
} else if (a_inst->stage == 1u) {
ac_draw_centered(a_layer,
(a_inst->err != 0u) ? AC_ERR_TXT : AC_STAGE2_TXT,
AC_STAGE_Y, a_clip);
} else {
/* stage 0/2:这里没有要说的话——留空行比硬凑一个词诚实 */
}
/* 行 3:可选观测行(复归页用它把"保护动作 N"钉在确认同一屏上:
* N 从 >0 变 0 就是"动作位真的复归了"的屏上证据,不用翻页也不用接表) */
if ((a_inst->watch_label != NULL) && (a_inst->watch_cb != NULL)) {
char watch[UI_BUF_STRLEN];
(void)lv_snprintf(watch, sizeof(watch), "%s %u", a_inst->watch_label,
(unsigned int)a_inst->watch_cb());
/* 观测行是拼出来的栈串,不是 Flash 字面量:本函数同帧内绘制完即弃 */
ac_draw_centered(a_layer, watch, AC_WATCH_Y, a_clip);
}
/* 行 4:键提示(只读时 ENTER 无效,提示必须跟着改口——屏上说"ENTER=确认"
* 而按下去没反应,是现场投诉最多的"设备坏了"误判来源) */
if (ui_page_action_is_readonly(a_inst) != 0u) {
ac_draw_centered(a_layer, AC_HINT_RO, AC_HINT_Y, a_clip);
} else {
ac_draw_centered(a_layer,
(a_inst->stage == 2u) ? AC_HINT_BACK : AC_HINT_DO,
AC_HINT_Y, a_clip);
}
}
/*
* 函数功能:按键处理(确认链状态机;框架 key_cb 的实现体)
* 入口参数:a_inst - 操作页实例 ui_page_action_t *
* a_key - LVGL 键值 lv_key_t
* 返回值:1 = 已消费(框架跳过内置语义);0 = 未消费(其余键交回框架)
* (键分发标志非错误通道,取 uint8_t——bsp-err/err-type)
* 限定条件:经页面描述符 key_cb 挂接;LEFT/ESC 不进本函数(框架恒关页 = 取消,
* 这是"任何一页都退得出去"的框架保证,本组件不得拦截)
* 函数说明:ENTER——stage0 转二次确认;stage1 调 ok_cb 投递:受理且有 done_text
* 转 stage2 回报、受理且无 done_text 直接关页、拒收置 err 留在 stage1
* (再 ENTER 重走一次投递,不自动重试:连点两次就投两次,
* 清除记录这类动作要的是"我确实在两下之内只发了一次");
* stage2 关页返回——回报行必须让用户自己确认看到了才走,
* 成功即关页的形态只在 done_text=NULL(没什么可回报的)时才成立。
* 只读进入:先放行 stage2 的返回支(已完成的页总得关得掉),
* 其余 ENTER 一律消费但不推进、不投递
*/
uint8_t ui_page_action_key(ui_page_action_t *a_inst, lv_key_t a_key) {
bool ok = false;
if ((a_inst == NULL) || (a_inst->ok_cb == NULL)) {
return 0u; /* 防御:配置不全的实例不该出现在这里 */
}
if (a_key != LV_KEY_ENTER) {
return 0u;
}
if (a_inst->stage == 2u) {
ui_content_page_close();
return 1u; /* 已回报:ENTER = 收页回菜单 */
}
if (ui_page_action_is_readonly(a_inst) != 0u) {
return 1u; /* 只读:吃掉按键,态标行已在说 LOCK,不做任何动作 */
}
if (a_inst->stage == 0u) {
a_inst->stage = 1u;
a_inst->err = 0u;
ui_content_page_invalidate();
return 1u;
}
/* stage 1:第二次 ENTER = 真执行(只投递,不等执行结果) */
ok = a_inst->ok_cb();
if (!ok) {
a_inst->err = 1u;
ui_content_page_invalidate();
return 1u; /* ERR 显示,下一次 ENTER 重新走投递 */
}
if (a_inst->done_text == NULL) {
ui_content_page_close(); /* 成功即关页(无回报行形态) */
return 1u;
}
a_inst->stage = 2u;
a_inst->err = 0u;
ui_content_page_invalidate();
return 1u;
}