52e28885cb
三个薄页面模块 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() 待删(只有测试在读 的自检口),排在本笔之后单独一笔。
255 lines
11 KiB
C
255 lines
11 KiB
C
/*
|
||
* 模块名称: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,
|
||
};
|