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

389 lines
17 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 clock (时钟设置内容页)
* 模块功能:装置维护 → 时钟设置 叶子(node 24,#P 受密码保护)的内容页——
* 六字段表单(年/月/日/时/分/秒)+ 保存行(仅可编辑态才有),
* 预填取自 ui_data 实时帧的 clock[20] + clock_ok(本文件不 include
* 任何 App 头,尤其**不碰 sys_clock**:模拟器没链 sys_clock,页面里
* 用一次就是"MCU 编得过、模拟器链接期爆"),字段编辑复用 ui_w_editor
* (逐位 ±1、左右移光标、ENTER 提交、越界拒退并提示),
* 保存走 ui_data_maint_clock_set() 注入转发——执行侧真机实现
* = sys_clock_set_unix() + 回读复核,返 0 才是终态;
* 无效值拒收:六字段先过 ui_data_clock_to_epoch() 整组校验
* (含 2 月 30 日、非闰年 2 月 29 日这类"每段都合法、组合非法"),
* 不过则**一个字节都不写**、保存行显 ERR;
* 墙钟无效(clock_ok=0 或串解析失败):六行值列一律 "--" + 标题右侧
* "无时钟"徽标,**绝不显示也绝不预填 1970**(口径见下方 CK_INVAL 注)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:DTU 项目组
* 创建日期:2026-10-26
* 修改记录:
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3;判据 docs/任务规划/UI补齐.md
* §2 第 3 行「时钟设置写入 sys_clock_set_unix()
* 并回读」;形态 = 页面扩充详细方案.md §3.4 表单 6 字段)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_page_clock.h"
#include "ui_data.h"
#include "ui_tokens.h"
#include "ui_w_editor.h"
/* 私有宏定义区 */
#define CK_FIELD_N 6u /* 字段行数:年月日时分秒 */
#define CK_ROW_COUNT_MAX 7u /* 最多行数:6 字段 + 1 保存行 */
#define CK_SAVE_ROW CK_FIELD_N /* 保存行下标(仅可编辑态存在) */
#define CK_BADGE_X 7 /* 徽标左边界:与行名同一列 */
#define CK_BADGE_Y 124 /* 徽标上边界:清单最后一行(108..121)与页脚带(140+)
之间的空带,实测三条链都没占过它 */
#define CK_NO_CLOCK_TXT "无时钟" /* 徽标(用字已在字库) */
#define CK_INVAL_VAL "--" /* 无效墙钟的字段值列——**不是 1970**:
记录侧那条 "1970-01-01 08:00:00" 是
sys_log 写死占位串(App/log/sys_log_mcu.c
落盘时 sys_clock_get_str 返 NULL 才用),
把它抄来预填等于让用户"照抄保存",
一次点击就把 1970 写进 RTC —— 比不显示更坏 */
/* 保存行值列词表:与整定保存行、网口设置保存行同一排 ASCII 词。
* 没有 WAIT 这一档:维护通道是**同步转发**,返回值就是执行侧带回读结论的终态,
* 屏上挂一个永远等不到的 WAIT 比不挂更让人困惑(契约 §6.3 第 2 条同一口径) */
#define CK_ST_ENTER "ENTER"
#define CK_ST_OK "OK"
#define CK_ST_ERR "ERR"
/*
* 本地这条操作记录的描述串:ASCII 18 字节 < SYSLOG_DESC_MAX(28)。
* 与远端 "clock set by 104"(App/task/iec104_serv.c)对称——面板与主站各一条、
* 串不同,查"这装置什么时候被谁对过时"才分得开。只在保存成功那条腿投。
*/
#define CK_OPER_NOTE "clock set by panel"
/* 静态变量定义区 */
/* 字段区间:日的"当月真实上限"不在这里判(编辑器只有静态 min/max),
* 组合合法性统一由 ui_data_clock_to_epoch() 在保存时把关——少一处第二判据 */
static const uint16_t k_ck_min[CK_FIELD_N] = {
(uint16_t)UI_DATA_CLOCK_YEAR_MIN, 1u, 1u, 0u, 0u, 0u
};
static const uint16_t k_ck_max[CK_FIELD_N] = {
(uint16_t)UI_DATA_CLOCK_YEAR_MAX, 12u, 31u, 23u, 59u, 59u
};
static const char *const k_ck_name[CK_FIELD_N] = {
"年", "月", "日", "时", "分", "秒"
};
/* 实时帧缓存:只在发布序号变过时整帧读(§5.4 行缓存纪律;本层不做逐行整帧拷贝) */
static ui_data_rt_t s_rt;
static uint32_t s_rt_seq;
static uint16_t s_val[CK_FIELD_N]; /* 工作值(编辑提交后落在这里;保存前不写任何数据源) */
static uint8_t s_have; /* 1 = 最近一次预填来自一帧有效时标;0 = 无有效时标 */
static uint8_t s_dirty; /* 1 = 用户改过工作值还没落钟:帧不得覆盖用户输入,
值列也不写 "--"(否则编辑到一半被 200ms 节拍冲掉) */
static uint8_t s_editing; /* 1 = 正在编辑(被编辑行 = 当前选中行) */
static uint8_t s_st; /* 保存态锁存:0 未投过 / 1 OK / 2 ERR */
static uint8_t s_ed_row; /* 被编辑行号(仅 s_editing=1 时有意义) */
static ui_w_editor_t s_ed; /* 数值编辑器实例(本页独占,勿跨页共用) */
/* 函数定义区 */
/*
* 函数功能:跟随发布序号取实时帧,并把时标拆成六个工作值
* 入口参数:无
* 返回值:无
* 限定条件:uiTask(框架绘制/按键链内)
* 函数说明:解析失败(空串 = 墙钟无效;或快照里出现非本工程口径的串)一律
* s_have=0 → 六行显 "--" + 徽标;**不猜**、不回填 0、不回填 1970
*/
static void ck_sync(void) {
uint32_t seq = ui_data_rt_seq();
ui_data_clock_t f;
if ((seq != s_rt_seq) && (ui_data_rt(&s_rt) == 0)) {
s_rt_seq = seq;
}
if ((s_editing != 0u) || (s_dirty != 0u)) {
return; /* 用户正在改:200 ms 一帧的节拍不得冲掉他还没落钟的输入 */
}
if ((s_rt.clock_ok == 0u) || (ui_data_clock_parse(s_rt.clock, &f) != 0)) {
s_have = 0u;
return;
}
/* 无未落钟改动 ⇒ 值列跟着墙钟走:保存成功后新时标进帧,这就是"回读校验
* 真读回来了"的屏上证据,不必页面自己造一份"我以为写进去了"的显示 */
s_val[0] = f.year;
s_val[1] = f.month;
s_val[2] = f.day;
s_val[3] = f.hour;
s_val[4] = f.minute;
s_val[5] = f.second;
s_have = 1u;
}
/*
* 函数功能:取当前可编辑性(本次进入是否有修改权限)
* 入口参数:无
* 返回值:1 = 可编辑;0 = 只读 uint8_t
* 限定条件:框架开页之后(init_cb 起,is_editable 读到的就是本次进入的权限)
* 函数说明:只读进入连保存行都不给(同整定四页的只读分支)——一个按不动的
* 保存行只是徒增"是不是设备坏了"的误判面
*/
static uint8_t ck_editable(void) {
return (ui_content_page_is_editable() != false) ? 1u : 0u;
}
/*
* 函数功能:总行数(框架 count_cb)
* 入口参数:无
* 返回值:6(只读)或 7(可编辑:6 字段 + 保存行) uint16_t
* 限定条件:框架绘制/翻页链内
*/
static uint16_t pg_count(void) {
return (uint16_t)(CK_FIELD_N + ck_editable());
}
/*
* 函数功能:逐行取数(框架 refresh)
* 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h
* 返回值:无
* 限定条件:框架绘制链内
*/
static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) {
uint16_t sel = ui_content_page_selected_row();
(void)memset(a_value, 0, UI_BUF_STRLEN);
if (a_row >= CK_FIELD_N) {
(void)memcpy(a_name, "保存", sizeof("保存"));
(void)memcpy(a_value, (s_st == 0u) ? CK_ST_ENTER
: ((s_st == 1u) ? CK_ST_OK : CK_ST_ERR),
((s_st == 0u) ? sizeof(CK_ST_ENTER)
: ((s_st == 1u) ? sizeof(CK_ST_OK) : sizeof(CK_ST_ERR))));
return;
}
(void)memcpy(a_name, k_ck_name[a_row], strlen(k_ck_name[a_row]) + 1u);
if ((s_editing != 0u) && (sel == a_row)) {
ui_w_editor_format(&s_ed, a_value, UI_BUF_STRLEN); /* 编辑串与光标同规则 */
return;
}
if ((s_have == 0u) && (s_dirty == 0u) && (s_editing == 0u)) {
(void)memcpy(a_value, CK_INVAL_VAL, sizeof(CK_INVAL_VAL));
return; /* 墙钟无效且用户还没动手:显 "--",不显 1970、也不显 0 */
}
if (a_row == 0u) {
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%04u", (unsigned int)s_val[0]);
} else {
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%02u", (unsigned int)s_val[a_row]);
}
}
/*
* 函数功能:页脚带靠左画"无时钟"徽标(墙钟无效的屏上表达)
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
* 返回值:无
* 限定条件:框架绘制链内;只在 s_have=0 时落墨
* 函数说明:UI_COL_FG、不做反显——反显在 1bpp 下表示"选中",徽标不是可选中项;
* 左边界/所在带见下方注释(实测过标题带会让"无时钟"和标题连成一句)
*/
static void ck_draw_badge(lv_layer_t *a_layer, const lv_area_t *a_clip) {
lv_area_t area;
lv_draw_label_dsc_t ld;
const char *txt = CK_NO_CLOCK_TXT;
if ((s_have != 0u) || (s_dirty != 0u)) {
return; /* 已有有效时标;或用户正在填一个新时间(此时钟确实还没建立,但
屏上已经在看他输入的数,再压一个徽标只是抢注意力) */
}
/* 位置实测两次才定下来,两次踩的坑都记在这里(都是"看起来放得下"):
* ① 标题带右侧(整定页"未标定"徽标那条线,右界 141):本标题"时钟设置"4 个字
* 居中占到 x≈104,徽标 36 px 正好贴在它后面,一屏读成"时钟设置无时钟"一句
* 话——整定页能那么放是因为它最长标题只有 3 个字(到 x≈98)。
* ② 页脚带左侧 x=7:页脚"第 1 页,共 1 页"**不是居中的**,实测从 x=6 起排,
* 徽标直接压在"第 1 页"上(像素比对里表现为两边都缺一块)。
* 现落点 = 清单最后一行(108..121)与页脚带(140 起)之间的空带:谁都不压,
* 且仍在"这一屏说的是时钟"的视线内 */
area.x1 = CK_BADGE_X;
/* 该版本 lv_text_get_width:长度必须传 strlen(传 -1/0 会死循环或返回 0) */
area.x2 = CK_BADGE_X
+ (int32_t)lv_text_get_width(txt, (uint32_t)strlen(txt), LV_FONT_DEFAULT, 0);
area.y1 = CK_BADGE_Y;
area.y2 = CK_BADGE_Y + UI_TEXT_BAND_H;
if ((area.x1 > a_clip->x2) || (a_clip->x1 > area.x2) ||
(area.y1 > a_clip->y2) || (a_clip->y1 > area.y2)) {
return;
}
lv_draw_label_dsc_init(&ld);
ld.base.layer = a_layer;
ld.text = txt;
ld.text_static = 1u;
ld.color = UI_COL_FG;
ld.ofs_x = 0;
ld.ofs_y = 0;
lv_draw_label(a_layer, &ld, &area);
}
/*
* 函数功能:自绘(框架 draw_cb 末尾:编辑器光标反显 + 无时钟徽标)
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
* 返回值:无
* 限定条件:框架绘制链内
*/
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
if (s_editing != 0u) {
uint16_t sel = ui_content_page_selected_row();
int32_t row_top;
/* 本页 ≤7 行、rows_per_page = UI_LIST_ROWS(8) ⇒ 恒第 1 页、窗口首行 = 0;
* 两个变量都只在编辑态有意义,就声明在编辑态这块里(cppcheck variableScope)*/
row_top = UI_LIST_TOP + ((int32_t)sel * UI_ROW_PITCH);
ui_w_editor_draw(&s_ed, a_layer, a_clip, row_top, 0, UI_ROW_BAND_H, UI_LIST_RIGHT_X);
}
ck_draw_badge(a_layer, a_clip);
}
/*
* 函数功能:保存——整组校验通过后投递墙钟设置,成功后补投本地操作记录
* 入口参数:无
* 返回值:无
* 限定条件:可编辑态、保存行 ENTER;uiTask 上下文
* 函数说明:三步顺序即判据:① to_epoch 整组校验(组合非法 → 一个字节都不写,
* 屏上 ERR,光标留在页面上,用户改的就是那一行不必重新找);
* ② 注入转发(真机 = sys_clock_set_unix + 回读复核;**sys_clock_set()
* 本身恒返 0**,RTC 写失败只打日志,所以回读是唯一真判据);
* ③ 只有返 0 才投操作记录——给一次没落地的对时留审计记录,
* 比少一条记录坏得多
* 结果落在 s_st 上并**锁存**:状态是全局单值,直读会让用户动一下光标
* 就看到别处留下的旧结果(契约 §6.3 第 3 条同一条理由)
*/
static void ck_save(void) {
ui_data_clock_t f;
uint32_t epoch = 0u;
int rc;
f.year = (uint16_t)s_val[0];
f.month = (uint8_t)s_val[1];
f.day = (uint8_t)s_val[2];
f.hour = (uint8_t)s_val[3];
f.minute = (uint8_t)s_val[4];
f.second = (uint8_t)s_val[5];
if (ui_data_clock_to_epoch(&f, &epoch) != 0) {
s_st = 2u; /* 组合非法:一条都不投 */
return;
}
rc = ui_data_maint_clock_set(epoch);
if (rc == 0) {
s_st = 1u;
s_dirty = 0u; /* 已落钟:值列交还给实时帧,回读的新时标自己会上屏 */
(void)ui_data_maint_oper_note(CK_OPER_NOTE);
} else {
s_st = 2u;
}
}
/*
* 函数功能:开页重置运行态(框架 init_cb)
* 入口参数:无
* 返回值:无
* 限定条件:框架开页链内(此时本次授予权限已装载,ck_editable 读得到真值)
*/
static void pg_init(void) {
s_rt_seq = 0u;
s_have = 0u;
s_dirty = 0u;
s_st = 0u;
s_editing = 0u;
s_ed_row = 0u;
ui_w_editor_reset(&s_ed);
(void)memset(s_val, 0, sizeof(s_val));
ck_sync();
if (s_have == 0u) {
/* 无有效时标:起编值取各字段下限(不是 0、也不是 1970)——用户从
* 2000-01-01 00:00:00 起步改,比从一片 0 里猜要少按十几次键 */
s_val[0] = (uint16_t)UI_DATA_CLOCK_YEAR_MIN;
s_val[1] = 1u;
s_val[2] = 1u;
}
}
/*
* 函数功能:按键转发(框架 key_cb)
* 入口参数:a_key - LVGL 键值 lv_key_t
* 返回值:true = 本页已消费;false = 交回框架(↑↓ 移选、← 关页) bool
* 限定条件:框架按键链内;描述符 edit_capable=1 + key_intercept=1
* 函数说明:编辑中的 ←→ 是"移数字光标",必须先给页面(key_intercept=1 的用途),
* 否则框架会把 ← 当成关页,编辑到一半就跳出去;ESC 恒不到这里
* (框架保证任何一页退得出去);只读进入 ENTER 一律忽略
*/
static bool pg_key(lv_key_t a_key) {
uint16_t sel = ui_content_page_selected_row();
if (s_editing != 0u) {
if (ui_w_editor_key(&s_ed, a_key) == 0u) {
return false; /* 编辑器不认的键交回框架 */
}
if (ui_w_editor_is_editing(&s_ed) == false) {
/* 编辑器已提交(ENTER 且范围通过):落工作值,作废上一次的保存结果 */
if (s_ed_row < CK_FIELD_N) {
s_val[s_ed_row] = (uint16_t)ui_w_editor_value(&s_ed);
s_dirty = 1u; /* 工作值与墙钟从此两回事,别再让帧覆盖它 */
}
s_editing = 0u;
s_st = 0u; /* 改了任何一位,上一次的保存结果就作废显示 */
}
return true;
}
if (a_key != LV_KEY_ENTER) {
return false; /* ↑↓ 等交回框架内置移选 */
}
if (ck_editable() == 0u) {
return false; /* 只读:不编辑、不保存(保存行本来也不在屏上) */
}
if (sel >= CK_FIELD_N) {
ck_save();
ui_content_page_invalidate();
return true;
}
/* 进字段编辑:初值取当前工作值;无有效时标时从下限起编 */
s_ed_row = (uint8_t)sel;
s_editing = 1u;
ui_w_editor_begin(&s_ed, (int32_t)s_val[sel], (int32_t)k_ck_min[sel],
(int32_t)k_ck_max[sel], 0u);
ui_content_page_invalidate();
return true;
}
/*
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=clock 绑定引用)
* 列表页骨架(6~7 行 1 页,几何令牌同整定四页)+ 可编辑(edit_capable,
* 还要本次授予修改权限)+ ← 拦截(编辑中移数字光标)。
* band_h 必须 >0:selectable 页选中行是黑字,没有白带就是黑底黑字看不见选中行
*/
const content_page_def_t ui_page_clock_desc = {
.id = "clock",
.title = "时钟设置",
.selectable = 1u,
.edit_capable = 1u,
.key_intercept = 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 = CK_ROW_COUNT_MAX,
.init_cb = pg_init,
.count_cb = pg_count,
.refresh = pg_refresh,
.draw_cb = pg_draw,
.key_cb = pg_key,
};