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

255 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 Page clear (清除记录内容页)
* 模块功能:装置维护 → 清除记录 叶子(node 27,#P 受密码保护)的内容页——
* 七行清单(运行 / 自检 / 事故 / 保护告警 / 保护启动 / 操作 / 遥控记录),
* 值列 = 该类记录当前条数(0 条显"无记录"),逐行独立清除;
* 行内两段式确认:第一次 ENTER 该行值列变"确认?",第二次 ENTER 才投递,
* 之后该行按清除状态显 WAIT / OK / ERR,直到光标移开才收回;
* 条数只走 ui_w_records 的**同一枚注入 reader**(裸计数入口
* ui_w_records_count_of),删除只发请求(执行侧 = syslog_request_clear),
* 真反馈 = 状态机 + 下一帧条数归零,不是"按了就算成功";
* 只读进入(查看密码)整页不投递;
* 本文件零 App include、零文件系统触点(既不调 MCU 专属的
* syslog_clear_apply(),也绝不自己 f_unlink / 碰卷锁——defaultTask 是
* 记录唯一写者,uiTask 在 IWDG required_mask 内,给它一条最坏数百 ms
* 的存储路径就是拿误复位当代价)
* 为什么不做"一键清全部":七类记录分属不同事件面(运行/自检 vs 事故/告警/启动 vs
* 操作/遥控审计),一次点击清光七类 = 一次误操作毁掉全部审计面;
* 逐行清已完全覆盖判据(删后重建、计数归零、不存在时幂等)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:DTU 项目组
* 创建日期:2026-10-26
* 修改记录:
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3;判据 docs/任务规划/UI补齐.md
* §2 第 3 行「清除记录…不允许只做没有实际效果的按钮」;
* 删除 API 与请求/落地分离 = task-4,口径真源
* App/log/README.md 与 App/log/sys_log.h)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_page_clear.h"
#include "ui_content_page.h"
#include "ui_data.h"
#include "ui_tokens.h"
#include "ui_w_records.h"
/* 私有宏定义区 */
#define CL_ROW_N UI_W_REC_LOG_N /* 行数 = 记录源个数(两端映射表长度同源) */
#define CL_NONE 0xFFu /* "无行"哨兵(行号上限 7,绝不撞) */
#define CL_TXT_NONE "无记录" /* 0 条(用字已在子集,与两条记录页同一词) */
#define CL_TXT_CONFIRM "确认?" /* 第一段确认('?' 是 ASCII) */
/* 结果词沿用全 ASCII 一排:WAIT 是正常态(MCU 端删除在 defaultTask 的
* syslog_drain() 尾部落地),模拟器端同步删除永不出现 WAIT —— 两端都要能显示 */
#define CL_TXT_WAIT "WAIT"
#define CL_TXT_OK "OK"
#define CL_TXT_ERR "ERR"
/* 私有函数声明区 */
static const char *cl_name_of(uint16_t a_row);
static uint8_t cl_id_of(uint16_t a_row);
/* 静态变量定义区 */
/* 行名表(Flash);顺序 = UI_W_REC_LOG_* 编号序,与两端映射表同一套下标 */
static const char *const k_cl_name[CL_ROW_N] = {
"运行记录", "自检记录", "事故记录", "保护告警", "保护启动", "操作记录", "遥控记录"
};
static const uint8_t k_cl_id[CL_ROW_N] = {
UI_W_REC_LOG_RUN, UI_W_REC_LOG_SELFTEST, UI_W_REC_LOG_ACCIDENT, UI_W_REC_LOG_ALARM,
UI_W_REC_LOG_START, UI_W_REC_LOG_OPER, UI_W_REC_LOG_YK
};
static uint8_t s_confirm; /* 待确认行(第一次 ENTER 已按下),CL_NONE = 无 */
static uint8_t s_note; /* 已投递、要显示清除结果的行,CL_NONE = 无 */
static uint8_t s_err; /* 投递被拒的行,CL_NONE = 无 */
static uint16_t s_sel_seen; /* 上一次绘制时的选中行(移开光标即收回三个锁存) */
/* 函数定义区 */
/*
* 函数功能:取行名
* 入口参数:row - 行号 uint16_t
* 返回值:名(Flash 字面量;越界回空串,不越表读) const char *
* 限定条件:无
*/
static const char *cl_name_of(uint16_t a_row) {
if (a_row >= CL_ROW_N) {
return "";
}
return k_cl_name[a_row];
}
/*
* 函数功能:取行对应的记录源编号
* 入口参数:row - 行号 uint16_t
* 返回值:UI_W_REC_LOG_*;越界回 0(首号,调用方已在越界时不调本函数) uint8_t
* 限定条件:无
* 函数说明:编号不透明——本页不知道也不该知道"3 号是 alarm.log",映射在两端
* 适配层(App/task/ui_task.c 与 simulator/main.c),加一类记录时
* 本表加一行、两端各加一条映射,屏上自动多一行
*/
static uint8_t cl_id_of(uint16_t a_row) {
if (a_row >= CL_ROW_N) {
return (uint8_t)UI_W_REC_LOG_RUN;
}
return k_cl_id[a_row];
}
/*
* 函数功能:开页复位(框架 init_cb)
* 入口参数:无
* 返回值:无
* 限定条件:框架开页链内(此时本次授予权限已装载)
* 函数说明:三个锁存全部归无——清除状态是**按 id 的全局单值**(syslog 侧
* 上电以来一直是 OK),开页直读会把"昨天清过操作记录"显示成"这次
* 清成功了"(契约 §6.3 第 3 条同一件事:状态是全局的,页面必须锁存)
*/
static void pg_init(void) {
s_confirm = CL_NONE;
s_note = CL_NONE;
s_err = CL_NONE;
s_sel_seen = ui_content_page_selected_row();
}
/*
* 函数功能:逐行取数(框架 refresh;值列按"结果 > 确认 > 条数"优先级)
* 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h
* 返回值:无
* 限定条件:框架绘制链内
* 函数说明:已投递那一行的词来自 ui_data_maint_clear_state()(执行侧直通,
* FAIL 也照实显 ERR:原文件一个字节没少,可以再发一次请求);
* 没投递的行每帧现算条数——清 run.log 之后 sys_log 会往**操作记录**
* 补一条留痕,所以"操作记录"那行会从 0 变成 1,这是真的,页面不缓存
* "我以为归零了"(清 oper.log 自己时刻意不补,App/log/README.md 已载)
*/
static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) {
uint8_t id;
uint16_t n;
(void)memset(a_value, 0, UI_BUF_STRLEN);
id = cl_id_of(a_row);
(void)memcpy(a_name, cl_name_of(a_row), strlen(cl_name_of(a_row)) + 1u);
if ((uint8_t)a_row == s_note) {
int st = ui_data_maint_clear_state(id);
if (st == UI_DATA_CLEAR_ST_OK) {
(void)memcpy(a_value, CL_TXT_OK, sizeof(CL_TXT_OK));
} else if (st == UI_DATA_CLEAR_ST_WAIT) {
(void)memcpy(a_value, CL_TXT_WAIT, sizeof(CL_TXT_WAIT));
} else {
(void)memcpy(a_value, CL_TXT_ERR, sizeof(CL_TXT_ERR));
}
return;
}
if ((uint8_t)a_row == s_err) {
(void)memcpy(a_value, CL_TXT_ERR, sizeof(CL_TXT_ERR));
return;
}
if ((uint8_t)a_row == s_confirm) {
(void)memcpy(a_value, CL_TXT_CONFIRM, sizeof(CL_TXT_CONFIRM));
return;
}
n = ui_w_records_count_of(id); /* 现算:不留"我以为归零了"的缓存 */
if (n == 0u) {
(void)memcpy(a_value, CL_TXT_NONE, sizeof(CL_TXT_NONE));
return;
}
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%u", (unsigned int)n);
}
/*
* 函数功能:自绘(框架 draw_cb)——顺带承担"光标移开就收回锁存"的观测
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
* 返回值:无
* 限定条件:框架绘制链内
* 函数说明:为什么在 draw 里看选中行:↑↓ 移选是框架内置的(本页 key_cb 对
* 非 ENTER 键一律交回框架,看不到它们),而每次移选框架都会
* invalidate 并重绘——draw 是唯一必然发生、且总在状态变化之后的观测点。
* 语义 = "用户已经看过程序结果 / 已经放弃这次确认",本页不再有
* "上一次的结果挂在另一行上"的显示
*/
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
uint16_t sel = ui_content_page_selected_row();
(void)a_layer;
(void)a_clip;
if (sel != s_sel_seen) {
s_sel_seen = sel;
s_confirm = CL_NONE;
s_note = CL_NONE;
s_err = CL_NONE;
}
}
/*
* 函数功能:按键处理(行内两段式确认)
* 入口参数:a_key - LVGL 键值 lv_key_t
* 返回值:true = 本页已消费;false = 交回框架(↑↓ 移选、←/ESC 关页 = 取消) bool
* 限定条件:框架按键链内;描述符 edit_capable=1
* 函数说明:只读进入一律交回框架(本页没有只读可做的动作,也没有保存行可显
* LOCK——清单页没有态标位,加了反而像"可以按");
* 第一次 ENTER 只是把该行标成待确认(不做任何投递),第二次才投;
* 投递被拒(含维护通道未注入)时置 s_err,光标一动就收回
*/
static bool pg_key(lv_key_t a_key) {
uint16_t sel;
if (a_key != LV_KEY_ENTER) {
return false;
}
if (ui_content_page_is_editable() == false) {
return false; /* 只读:清除是修改类动作,一条都不投 */
}
sel = ui_content_page_selected_row();
if (sel >= CL_ROW_N) {
return true; /* 越界防御:不投任何编号 */
}
if ((uint8_t)sel != s_confirm) {
s_confirm = (uint8_t)sel; /* 第一段:只标记,不投递 */
s_note = CL_NONE;
s_err = CL_NONE;
ui_content_page_invalidate();
return true;
}
s_confirm = CL_NONE;
s_err = CL_NONE;
if (ui_data_maint_clear_req(cl_id_of(sel)) == 0) {
s_note = (uint8_t)sel; /* 已受理:接下来按状态机显 WAIT/OK/ERR */
} else {
s_err = (uint8_t)sel;
}
ui_content_page_invalidate();
return true;
}
/*
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=clear 绑定引用)
* 七行 1 页(rows_per_page = UI_LIST_ROWS(8) > 7 ⇒ 不翻页);key_intercept=0:
* 本页没有"编辑到一半"的态要保,← 与 ESC 一律关页 = 取消
*/
const content_page_def_t ui_page_clear_desc = {
.id = "clear",
.title = "清除记录",
.selectable = 1u,
.edit_capable = 1u,
.top_y = UI_LIST_TOP,
.pitch = UI_ROW_PITCH,
.band_h = UI_ROW_BAND_H,
.text_dy = 0,
.left_x = UI_LIST_LEFT_X,
.right_x = UI_LIST_RIGHT_X,
.rows_per_page = UI_LIST_ROWS,
.row_count = CL_ROW_N,
.init_cb = pg_init,
.refresh = pg_refresh,
.draw_cb = pg_draw,
.key_cb = pg_key,
};