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() 待删(只有测试在读 的自检口),排在本笔之后单独一笔。
This commit is contained in:
+475
-4
@@ -14,10 +14,16 @@
|
||||
* App 侧节拍里的 ui_data_param_drain(),两者同在 uiTask → 环形队列照旧
|
||||
* 无锁;若将来出现第二个投递上下文,唯一要做的是给 request/drain 两侧
|
||||
* 各加一把互斥(队列本身是单字 head/cnt 推进,页面侧不会看到半成品)
|
||||
* 模块功能补充:除三通道快照外,本模块还持有定值写路径的请求队列与状态字
|
||||
* (ui_data_param_request/state/drain)——本层只做"排队 + 状态机",
|
||||
* 不判断值域、不碰 prot_set、不碰文件系统(值域与写入事实经
|
||||
* ui_data_param_ops_t 由 App 侧注入,理由见 ui_data.h 写路径节)
|
||||
* 模块功能补充:除三通道快照外,本模块还持有三条与页面相关的东西:
|
||||
* ① 定值写路径的请求队列与状态字(ui_data_param_request/state/drain)
|
||||
* ——只做"排队 + 状态机",不判断值域、不碰 prot_set、不碰文件系统
|
||||
* (值域与写入事实经 ui_data_param_ops_t 由 App 侧注入);
|
||||
* ② 装置维护写通道(ui_data_maint_*:墙钟设置 / 强制复归 / 清除记录)
|
||||
* ——常驻注入的 ui_data_maint_ops_t 转发,本层不解释业务含义;
|
||||
* 为什么这里常驻注入而 ① 每次传表,见 ui_data.h 通道四的函数说明;
|
||||
* ③ 时钟字段换算(ui_data_clock_*:本地域、无时区偏移、纯整数零依赖)
|
||||
* ——「无效值拒收」的唯一判据处,也是唯一能在主机单测里跑全表的
|
||||
* 日历判据(页面 .c 依赖 lvgl.h,编不进主机测试单元)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
@@ -27,6 +33,15 @@
|
||||
* 与状态机(IDLE/WAIT/OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY),
|
||||
* 执行经注入函数表回调 App 侧;参数帧新增 cal_block
|
||||
* 字段随 publish 原样入存(本层不解释含义)
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-3(装置维护 3 页的前置通道):新增维护
|
||||
* 写通道 ui_data_maint_*(常驻注入 ui_data_maint_ops_t:
|
||||
* clock_set / reset_req / active_count / oper_note /
|
||||
* clear_req / clear_state 六口,未注入一律安全返回)与
|
||||
* 时钟字段换算 ui_data_clock_*(本地域无时区偏移;
|
||||
* to_epoch 同时是"无效值拒收"的唯一判据处)。
|
||||
* 三通道快照结构一个字节都没动 ⇒ UI_DATA_VER 保持 2
|
||||
* (版本号是页面与写侧唯一的对齐凭据,无字段变更却升版
|
||||
* = 让老页面以为新帧换了布局)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -420,3 +435,459 @@ int ui_data_param_drain(const ui_data_param_ops_t *a_ops) {
|
||||
s_pq_state = (s_pq_cnt != 0u) ? UI_DATA_PARAM_ST_WAIT : settled;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ── 通道四:装置维护写请求(时钟 / 强制复归 / 清除记录)──────────────────── */
|
||||
|
||||
/*
|
||||
* 注入的维护执行侧函数表(NULL = 未注入)。
|
||||
* 只存指针不拷内容:注入方(App/task/ui_task.c 与 simulator/main.c)的两端实现
|
||||
* 都是文件级 static const 表,生命周期 = 整个运行期,与 ui_w_records_bind() 存的
|
||||
* reader 指针同一条约定。撤销只可能出现在宿主测试里。
|
||||
*/
|
||||
static const ui_data_maint_ops_t *s_maint;
|
||||
|
||||
/*
|
||||
* 函数功能:注入装置维护执行侧函数表(启动期一次)
|
||||
* 入口参数:ops - 函数表;六个成员指针都不得为 NULL;NULL = 撤销注入
|
||||
* const ui_data_maint_ops_t *
|
||||
* 返回值:0 - 已注入(或已撤销);-1 - ops 非空但某成员为 NULL(存量不动) int
|
||||
* 限定条件:uiTask 启动期,先于 ui_menu_init()(与 ui_w_records_bind 同一处);
|
||||
* uiTask 之外的上下文不得调用(本层无锁,与快照层同一条并发前提)
|
||||
* 函数说明:成员逐条校验而不只看表指针——半途接错一条(例如忘了注 clear_state)
|
||||
* 会让清除记录页在"按下才崩"的位置崩,生成期/启动期就拒掉整表才是省事
|
||||
*/
|
||||
int ui_data_maint_bind(const ui_data_maint_ops_t *a_ops) {
|
||||
if (a_ops == NULL) {
|
||||
s_maint = NULL; /* 撤销注入:页面随即按"能力不可用"渲染 */
|
||||
return 0;
|
||||
}
|
||||
if ((a_ops->clock_set == NULL) || (a_ops->reset_req == NULL) ||
|
||||
(a_ops->active_count == NULL) || (a_ops->oper_note == NULL) ||
|
||||
(a_ops->clear_req == NULL) || (a_ops->clear_state == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
s_maint = a_ops;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询维护通道是否已注入
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 已注入;0 - 未注入 uint8_t
|
||||
* 限定条件:无(只读单字,任意上下文安全)
|
||||
* 函数说明:页面用它把"没接线"和"操作被拒"分开显示——前者是装配缺陷,
|
||||
* 现场再按一百次也不会好,必须看得见
|
||||
*/
|
||||
uint8_t ui_data_maint_ready(void) {
|
||||
return (s_maint != NULL) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求设置墙钟(时钟设置页"保存"行的唯一出口)
|
||||
* 入口参数:unix_epoch - 已整组校验过的秒(见 ui_data_clock_to_epoch) uint32_t
|
||||
* 返回值:0 - 执行侧受理;-1 - 未注入或执行侧失败;-2 - 参数无效(0 = 本层保留的
|
||||
* "未初始化"哨兵,与 sys_clock 的 UINT32_MAX 哨兵不同族,
|
||||
* 这里只挡 0 这一个明显不该写进 RTC 的值) int
|
||||
* 限定条件:uiTask 上下文(页面按键钩子)
|
||||
* 函数说明:本层不判日期合法性——判据只有一份,在 ui_data_clock_to_epoch();
|
||||
* 也不做回读,回读属执行侧(它才知道 sys_clock_get() 的真值)
|
||||
*/
|
||||
int ui_data_maint_clock_set(uint32_t a_unix_epoch) {
|
||||
if (s_maint == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (a_unix_epoch == 0u) {
|
||||
return -2;
|
||||
}
|
||||
return s_maint->clock_set(a_unix_epoch);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求复归全部保护元件(强制复归页的执行出口)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒收 int
|
||||
* 限定条件:uiTask 上下文
|
||||
* 函数说明:本函数**不投**操作记录,页面在返 0 之后自己投("本地面板发起"这个
|
||||
* 事实只有页面知道;远端 104 那条另有投递点,两边各一条、不得互替);
|
||||
* 返 -1 时页面必须一条都不投——给一次没发生的操作留审计记录,比少一条
|
||||
* 记录坏得多
|
||||
*/
|
||||
int ui_data_maint_reset_req(void) {
|
||||
if (s_maint == NULL) {
|
||||
return -1;
|
||||
}
|
||||
return s_maint->reset_req();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前动作元件数(复归页"保护动作 N"行的数据源)
|
||||
* 入口参数:无
|
||||
* 返回值:元件数;未注入返回 0 uint32_t
|
||||
* 限定条件:页面绘制路径可直调(一次函数指针转发,零 I/O、零锁)
|
||||
* 函数说明:返回类型取 uint32_t 与执行侧位图同宽,不在本层压成 uint8_t——
|
||||
* 元件数将来超 8 时,窄化会在页面里静默截成 0("明明动作了屏上说没有")
|
||||
*/
|
||||
uint32_t ui_data_maint_active_count(void) {
|
||||
if (s_maint == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
return s_maint->active_count();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:投一条操作记录留痕
|
||||
* 入口参数:desc - 描述串(ASCII 常量;NULL = 什么都不做) const char *
|
||||
* 返回值:0 - 已投递;-1 - 未注入或 desc 为 NULL int
|
||||
* 限定条件:uiTask 上下文
|
||||
* 函数说明:投递可能被队列丢弃(执行侧口径)——本层不因此把屏上的"操作成功"
|
||||
* 翻成失败:记录通道故障不等于操作没发生,混起来只会让现场误判装置
|
||||
*/
|
||||
int ui_data_maint_oper_note(const char *a_desc) {
|
||||
if ((s_maint == NULL) || (a_desc == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
s_maint->oper_note(a_desc);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求清除某类记录(清除记录页的执行出口)
|
||||
* 入口参数:log_id - UI 侧记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒绝 int
|
||||
* 限定条件:uiTask 上下文;"受理"≠"已清除",终态读 ui_data_maint_clear_state()
|
||||
* 函数说明:不在本层判 log_id 上界——上界的真源是记录侧有几个文件(映射表在
|
||||
* 适配层),这里再抄一份长度就成了第二真源,记录加一类时必然漏改
|
||||
*/
|
||||
int ui_data_maint_clear_req(uint8_t a_log_id) {
|
||||
if (s_maint == NULL) {
|
||||
return -1;
|
||||
}
|
||||
return s_maint->clear_req(a_log_id);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查某类记录的清除状态
|
||||
* 入口参数:log_id - 记录源编号 uint8_t
|
||||
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL;未注入返回 NONE int
|
||||
* 限定条件:页面绘制路径可直调
|
||||
* 函数说明:返回值直通执行侧、不做二次加工(与 ui_data_param_state() 同理:
|
||||
* 两处解释同一件事迟早漂移);WAIT 是正常态,页面不得画成失败
|
||||
*/
|
||||
int ui_data_maint_clear_state(uint8_t a_log_id) {
|
||||
if (s_maint == NULL) {
|
||||
return UI_DATA_CLEAR_ST_NONE;
|
||||
}
|
||||
return s_maint->clear_state(a_log_id);
|
||||
}
|
||||
|
||||
/* ── 时钟字段换算(本地域,无时区偏移)──────────────────────────────────── */
|
||||
|
||||
/*
|
||||
* 私有宏定义区(本小节自带,不与其它小节共用):
|
||||
* 算法 = Howard Hinnant 的 days_from_civil / civil_from_days(以 0000-03-01
|
||||
* 为纪元、400 年一周期),与 App/protocol/iec104/iec104_asdu.c 的正反两套实现
|
||||
* 同族;本工程年域 2000..2038,全程非负,用 uint32 就够,不引 int64 也不引 time.h
|
||||
*/
|
||||
#define CL_SECONDS_PER_DAY 86400u
|
||||
#define CL_SECONDS_OF_DAY_MAX 11647u /* 0x7FFFFFFF % 86400 = 2038-01-19 的 03:14:07 */
|
||||
#define CL_DAYS_EPOCH_MAX 24855u /* 0x7FFFFFFF / 86400 = 2038-01-19(距 1970-01-01 天数) */
|
||||
#define CL_DAYS_TO_0000 719468u /* 1970-01-01 距 0000-03-01 纪元的天数(Hinnant 偏移量) */
|
||||
#define CL_DAYS_PER_ERA 146097u /* 400 年一周期的天数 */
|
||||
|
||||
/*
|
||||
* 函数功能:公历日期 → 距 1970-01-01 的天数(调用方保证字段已判过合法性)
|
||||
* 入口参数:year - 年(>= 1) uint16_t month - 月 1..12 uint8_t day - 日 1..31 uint8_t
|
||||
* 返回值:天数(本工程年域内恒非负) uint32_t
|
||||
* 限定条件:月/日未做合法性判定——**合法性判定在 to_epoch(),不在这里**,
|
||||
* 换算函数保持"给什么算什么"才能与反向函数严格互逆
|
||||
* 函数说明:先做 1/2 月归到上一年的 11/12 月(y 相应减一),使 3 月成为岁首、
|
||||
* 闰日落在周期末端,之后每月天数变成 31 天/30.6 天的规整式子
|
||||
*/
|
||||
static uint32_t ui_data_clock_days_from_civil(uint16_t a_year, uint8_t a_month, uint8_t a_day) {
|
||||
uint32_t y = (a_month <= 2u) ? ((uint32_t)a_year - 1u) : (uint32_t)a_year;
|
||||
uint32_t era = y / 400u;
|
||||
uint32_t yoe = y - (era * 400u); /* [0, 399] */
|
||||
uint32_t mp = (a_month > 2u) ? ((uint32_t)a_month - 3u) : ((uint32_t)a_month + 9u);
|
||||
uint32_t doy = ((153u * mp + 2u) / 5u) + (uint32_t)a_day - 1u; /* [0, 365] */
|
||||
uint32_t doe = (yoe * 365u) + (yoe / 4u) - (yoe / 100u) + doy; /* [0, 146096] */
|
||||
|
||||
return (era * CL_DAYS_PER_ERA) + doe - CL_DAYS_TO_0000;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判断是否闰年(完整格里历规则)
|
||||
* 入口参数:year - 年 uint16_t
|
||||
* 返回值:1 - 闰年;0 - 平年 uint8_t
|
||||
* 限定条件:与 App/util/sys_clock.c 的 unix_to_sd2506() 内联判据逐字同一条规则
|
||||
* (4 年 && 非 100 年 || 400 年);2100 不在允许年域内,无需讨论该例外
|
||||
* 函数说明:取 uint8_t 而不是 bool——本工程布尔判定允许 bool,但本函数参与
|
||||
* 天数计算,返回宽度明确的整型省掉一次隐式转换(同 ui_w_editor_key()
|
||||
* 把"分发标志"写成 uint8_t 的口径)
|
||||
*/
|
||||
static uint8_t ui_data_clock_is_leap(uint16_t a_year) {
|
||||
if ((((uint32_t)a_year % 4u) == 0u) && (((uint32_t)a_year % 100u) != 0u)) {
|
||||
return 1u;
|
||||
}
|
||||
if (((uint32_t)a_year % 400u) == 0u) {
|
||||
return 1u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取某年某月的天数(含闰年 2 月)
|
||||
* 入口参数:year - 年 uint16_t month - 月 1..12 uint8_t
|
||||
* 返回值:28 / 29 / 30 / 31;月份为 0 或 > 12 时返回 0(调用方以 0 判非法,
|
||||
* 不开第二条错误码) uint8_t
|
||||
* 限定条件:无(纯查表 + 闰年判定,零依赖)
|
||||
* 函数说明:表序 = 1 月起(下标 0 是 1 月),与 sys_clock.c:145 那份 mdays[]
|
||||
* 的"0 月起"排法**故意不同**——本表按屏上月的直觉排(下标 = 月 - 1),
|
||||
* 少一次心算 -1 就少一次错位;两侧同一张表的等价性由主机单测逐月核对
|
||||
*/
|
||||
uint8_t ui_data_clock_days_in_month(uint16_t a_year, uint8_t a_month) {
|
||||
static const uint8_t k_mdays[12] = {
|
||||
31u, 28u, 31u, 30u, 31u, 30u, 31u, 31u, 30u, 31u, 30u, 31u
|
||||
};
|
||||
|
||||
if ((a_month < 1u) || (a_month > 12u)) {
|
||||
return 0u;
|
||||
}
|
||||
if ((a_month == 2u) && (ui_data_clock_is_leap(a_year) != 0u)) {
|
||||
return 29u;
|
||||
}
|
||||
return k_mdays[a_month - 1u];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:日历字段 → 本地域 epoch 秒,同时充当这组字段的合法性判据
|
||||
* 入口参数:p_in - 六个字段 const ui_data_clock_t *
|
||||
* p_out - 秒 uint32_t *
|
||||
* 返回值:0 - 合法并已输出;-1 - 参数为 NULL;-2 - 字段组合非法(任一段越界,
|
||||
* 含 2 月 30 日、非闰年 2 月 29 日、月 13、时 24、分/秒 60);
|
||||
* -3 - 字段各自合法但换算结果越界(> UI_DATA_CLOCK_EPOCH_MAX) int
|
||||
* 限定条件:年段先判(2000..2038),因此天数计算全程在非负 uint32 域内
|
||||
* 函数说明:三段边界分开返码,是为了让页面能给出现场听得懂的话:"2 月 30 日"
|
||||
* 是输入错(-2,光标回到出问题那一行),而"2038 年之后"是能力边界
|
||||
* (-3,说这一版装置的墙钟就到这儿)——两种情况混成一个 ERR,
|
||||
* 用户会反复试一个永远试不出的日期
|
||||
* ⚠ 全程不含时区偏移(口径见 ui_data.h 本节注释与
|
||||
* App/task/ui_task.c 的 SOE 时标注记):加 28800 的后果是"屏上输
|
||||
* 08:00、存进去 16:00",且模拟器里往返照样自洽、照不出来
|
||||
*/
|
||||
int ui_data_clock_to_epoch(const ui_data_clock_t *p_in, uint32_t *p_out) {
|
||||
uint32_t days;
|
||||
uint32_t sod;
|
||||
uint32_t dmax;
|
||||
|
||||
if ((p_in == NULL) || (p_out == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
if ((p_in->year < (uint16_t)UI_DATA_CLOCK_YEAR_MIN) ||
|
||||
(p_in->year > (uint16_t)UI_DATA_CLOCK_YEAR_MAX)) {
|
||||
return -2;
|
||||
}
|
||||
if ((p_in->month < 1u) || (p_in->month > 12u)) {
|
||||
return -2;
|
||||
}
|
||||
dmax = (uint32_t)ui_data_clock_days_in_month(p_in->year, p_in->month);
|
||||
if ((p_in->day < 1u) || ((uint32_t)p_in->day > dmax)) {
|
||||
return -2;
|
||||
}
|
||||
if (p_in->hour > 23u) {
|
||||
return -2;
|
||||
}
|
||||
if ((p_in->minute > 59u) || (p_in->second > 59u)) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
days = ui_data_clock_days_from_civil(p_in->year, p_in->month, p_in->day);
|
||||
sod = ((uint32_t)p_in->hour * 3600u) + ((uint32_t)p_in->minute * 60u) +
|
||||
(uint32_t)p_in->second;
|
||||
if ((days > CL_DAYS_EPOCH_MAX) || ((days == CL_DAYS_EPOCH_MAX) && (sod > CL_SECONDS_OF_DAY_MAX))) {
|
||||
return -3; /* 2038-01-19 03:14:07 之后:uint32 秒域到头,不是用户输错 */
|
||||
}
|
||||
|
||||
*p_out = (days * CL_SECONDS_PER_DAY) + sod;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:本地域 epoch 秒 → 日历字段(ui_data_clock_to_epoch 的反函数)
|
||||
* 入口参数:unix_epoch - 秒 uint32_t p_out - 输出 ui_data_clock_t *
|
||||
* 返回值:0 - 已输出;-1 - p_out 为 NULL;-2 - 超出允许域(0 哨兵、年 < 2000、
|
||||
* 或 > UI_DATA_CLOCK_EPOCH_MAX) int
|
||||
* 限定条件:与 to_epoch 严格互逆(主机单测按往返相等钉,见 test/host/ui_data);
|
||||
* 刻意把 0 也判越界:0 = 1970-01-01,落在 RTC 年域(2000..2099)之外,
|
||||
* 而它又恰好是"墙钟无效"在记录侧的占位起点,绝不允许被当真实时间显示
|
||||
*/
|
||||
int ui_data_clock_from_epoch(uint32_t a_unix_epoch, ui_data_clock_t *p_out) {
|
||||
uint32_t days;
|
||||
uint32_t sod;
|
||||
uint32_t z;
|
||||
uint32_t era;
|
||||
uint32_t doe;
|
||||
uint32_t yoe;
|
||||
uint32_t doy;
|
||||
uint32_t mp;
|
||||
uint32_t y;
|
||||
uint32_t m;
|
||||
|
||||
if (p_out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if ((a_unix_epoch == 0u) || (a_unix_epoch > UI_DATA_CLOCK_EPOCH_MAX)) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
days = a_unix_epoch / CL_SECONDS_PER_DAY;
|
||||
sod = a_unix_epoch % CL_SECONDS_PER_DAY;
|
||||
z = days + CL_DAYS_TO_0000; /* 移到 0000-03-01 纪元 */
|
||||
era = z / CL_DAYS_PER_ERA;
|
||||
doe = z - (era * CL_DAYS_PER_ERA); /* [0, 146096] */
|
||||
yoe = (doe - (doe / 1460u) + (doe / 36524u) - (doe / 146096u)) / 365u; /* [0, 399] */
|
||||
doy = doe - ((365u * yoe) + (yoe / 4u) - (yoe / 100u));
|
||||
mp = (5u * doy + 2u) / 153u;
|
||||
|
||||
/* 3 月纪元月序转公历月序:mp < 10 落在 3..12 月,否则落在 1/2 月并归上一年 */
|
||||
y = yoe + (era * 400u);
|
||||
if (mp < 10u) {
|
||||
m = mp + 3u;
|
||||
} else {
|
||||
m = mp - 9u;
|
||||
y += 1u; /* 1/2 月属于上一年 */
|
||||
}
|
||||
|
||||
p_out->year = (uint16_t)y;
|
||||
p_out->month = (uint8_t)m;
|
||||
p_out->day = (uint8_t)(doy - ((153u * mp + 2u) / 5u) + 1u);
|
||||
p_out->hour = (uint8_t)(sod / 3600u);
|
||||
p_out->minute = (uint8_t)((sod / 60u) % 60u);
|
||||
p_out->second = (uint8_t)(sod % 60u);
|
||||
|
||||
if ((p_out->year < (uint16_t)UI_DATA_CLOCK_YEAR_MIN) ||
|
||||
(p_out->year > (uint16_t)UI_DATA_CLOCK_YEAR_MAX)) {
|
||||
return -2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:解析定长时标串为日历字段
|
||||
* 入口参数:ts - "YYYY-MM-DD HH:MM:SS"(快照 rt.clock 的形状) const char *
|
||||
* p_out - 输出 ui_data_clock_t *
|
||||
* 返回值:0 - 成功;-1 - 参数为 NULL;-2 - 串型不对(不是 19 字符、分隔位不是
|
||||
* '-'/' '/':'、出现非数字)**或**型对但值非法
|
||||
* (组合再过 ui_data_clock_to_epoch() 把关) int
|
||||
* 限定条件:空串(墙钟无效时快照里的取值,ui_data.h:149)走 -2;
|
||||
* 本函数**不猜**:"1970-01-01 08:00:00"这类记录侧占位串在这里同样返 -2
|
||||
* (年 1970 出域),页面据此显示"无时钟"而不是把占位串画成真实时间
|
||||
* 函数说明:手写十进制累加而不用 sscanf——本模块是零 LVGL、零 App 的纯内存叶子,
|
||||
* 拉进 stdio 就等于给主机单测加依赖、给 MCU 端加一份 printf 家族
|
||||
*/
|
||||
int ui_data_clock_parse(const char *a_ts, ui_data_clock_t *p_out) {
|
||||
ui_data_clock_t t;
|
||||
uint32_t i;
|
||||
uint32_t acc;
|
||||
const char *p;
|
||||
|
||||
if ((a_ts == NULL) || (p_out == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
(void)memset(&t, 0, sizeof(t));
|
||||
|
||||
/* 五段数字:年 4 位、月/日/时/分/秒各 2 位;段间分隔位逐位核对 */
|
||||
p = a_ts;
|
||||
acc = 0u;
|
||||
for (i = 0u; i < 4u; i++) {
|
||||
if ((p[i] < '0') || (p[i] > '9')) {
|
||||
return -2;
|
||||
}
|
||||
acc = (acc * 10u) + (uint32_t)(p[i] - '0');
|
||||
}
|
||||
t.year = (uint16_t)acc;
|
||||
if (p[4] != '-') {
|
||||
return -2;
|
||||
}
|
||||
p += 5u;
|
||||
|
||||
{
|
||||
uint8_t *const fields[5] = { &t.month, &t.day, &t.hour, &t.minute, &t.second };
|
||||
const char seps[4] = { '-', ' ', ':', ':' };
|
||||
|
||||
for (i = 0u; i < 5u; i++) {
|
||||
if (((p[0] < '0') || (p[0] > '9')) || ((p[1] < '0') || (p[1] > '9'))) {
|
||||
return -2;
|
||||
}
|
||||
*fields[i] = (uint8_t)(((uint32_t)(p[0] - '0') * 10u) + (uint32_t)(p[1] - '0'));
|
||||
p += 2u;
|
||||
if (i < 4u) {
|
||||
if (*p != seps[i]) {
|
||||
return -2;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (*p != '\0') {
|
||||
return -2; /* 型对但尾部还有字符:不是本工程认的时标串 */
|
||||
}
|
||||
{
|
||||
uint32_t probe;
|
||||
if (ui_data_clock_to_epoch(&t, &probe) != 0) {
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
|
||||
*p_out = t;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:日历字段 → 定长时标串(与 ui_data_clock_parse 同串型)
|
||||
* 入口参数:p_in - 字段 const ui_data_clock_t *
|
||||
* buf - 输出缓冲 char *
|
||||
* len - 缓冲容量(含 NUL,至少 UI_DATA_CLOCK_TS_LEN + 1 = 20) uint32_t
|
||||
* 返回值:0 - 已写出;-1 - 参数无效或容量不足(**不写半个串**,避免调用方
|
||||
* 把截断串当完整时标显示) int
|
||||
* 限定条件:不判字段合法性(调用方刚从 parse/to_epoch 那条链出来,再判一遍
|
||||
* 就是第二条判据)
|
||||
*/
|
||||
int ui_data_clock_format(const ui_data_clock_t *p_in, char *a_buf, uint32_t a_len) {
|
||||
uint8_t i;
|
||||
char *p;
|
||||
uint32_t v;
|
||||
|
||||
if ((p_in == NULL) || (a_buf == NULL) ||
|
||||
(a_len < (UI_DATA_CLOCK_TS_LEN + 1u))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
p = a_buf;
|
||||
v = (uint32_t)p_in->year;
|
||||
for (i = 0u; i < 4u; i++) { /* 年份 4 位补零 */
|
||||
p[3u - i] = (char)('0' + (uint32_t)(v % 10u));
|
||||
v /= 10u;
|
||||
}
|
||||
p[4] = '-';
|
||||
p += 5;
|
||||
|
||||
{
|
||||
const uint8_t fields[5] = { p_in->month, p_in->day, p_in->hour,
|
||||
p_in->minute, p_in->second };
|
||||
const char seps[4] = { '-', ' ', ':', ':' };
|
||||
|
||||
for (i = 0u; i < 5u; i++) {
|
||||
p[0] = (char)('0' + ((uint32_t)fields[i] / 10u));
|
||||
p[1] = (char)('0' + ((uint32_t)fields[i] % 10u));
|
||||
p += 2;
|
||||
if (i < 4u) {
|
||||
*p = seps[i];
|
||||
p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
*p = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
* 的数据源);写路径仍不引入任何 App 依赖(执行事实
|
||||
* 经 ui_data_param_ops_t 由 App 侧注入,同契约 §7.2
|
||||
* 记录页注入式 reader 的路子)
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-3(docs/任务规划/UI补齐.md §2 第 3 行
|
||||
* 「装置维护 3 页」的前置):新增通道四——维护写请求
|
||||
* (ui_data_maint_*:墙钟设置 / 强制复归 / 清除记录,
|
||||
* 常驻注入 ui_data_maint_ops_t)与时钟字段换算
|
||||
* (ui_data_clock_*,本地域无时区偏移,兼作"无效值
|
||||
* 拒收"的唯一判据处)。三通道快照结构未变 ⇒
|
||||
* UI_DATA_VER 仍为 2。仍然零 App include:维护动作的
|
||||
* 真源全在 App 侧,本层只转达请求与结果(同 §7.2);
|
||||
* ⚠ 页面也不得直调 sys_clock_*——模拟器没链 sys_clock,
|
||||
* 那种写法只在模拟器链接期才炸,理由见通道四表注
|
||||
*/
|
||||
|
||||
#ifndef UI_DATA_H
|
||||
@@ -426,6 +436,259 @@ uint64_t ui_data_param_err_mask(void);
|
||||
*/
|
||||
int ui_data_param_drain(const ui_data_param_ops_t *a_ops);
|
||||
|
||||
/* ── 通道四:装置维护写请求(时钟 / 强制复归 / 清除记录)───────────────────── */
|
||||
|
||||
/*
|
||||
* 执行侧函数表(道F task-3 新增;注入式,与 ui_data_param_ops_t、
|
||||
* ui_w_records.h 的 ui_rec_reader_t 同一条路子)
|
||||
*
|
||||
* 为什么是注入而不是直接调:分层方向硬约束——BSP 层目录 Drivers/BSP 不得反向
|
||||
* include App 层的头(scripts/check_layer_deps.py 拦提交),而 sys_clock_set_unix /
|
||||
* prot_elm_request_reset / syslog_request_clear 三件事实的属主全在 App 侧。
|
||||
* ⚠ **另有一条同样硬的理由**:模拟器根本没有 sys_clock
|
||||
* (simulator/dbg_log.h:4「模拟器无 sys_clock/RTC」,CMakeLists 也没这份源),
|
||||
* 所以页面里哪怕只用一次 sys_clock_*,也是"MCU 编得过、模拟器链接期爆",
|
||||
* 而这种错只在另一端构建时才暴露。写动作一律走本表,两端各给实现。
|
||||
* ⚠ 本表刻意不含任何"范围数值/枚举值":什么是有效时间、哪条记录对应哪个文件,
|
||||
* 真源仍分别在各属主模块(本层不做第二真源,只转达请求与结果)。
|
||||
*/
|
||||
typedef struct {
|
||||
/*
|
||||
* 函数功能:设置系统墙钟(本地域 epoch 秒)
|
||||
* 入口参数:unix_epoch - 要设置的秒 uint32_t
|
||||
* 返回值:0 - 已生效;-1 - 设置失败 int
|
||||
* 限定条件:真机实现**必须**在 sys_clock_set_unix() 之后回读 sys_clock_get()
|
||||
* 复核(差值容差见 App 侧实现),不得"调了就算成"
|
||||
*/
|
||||
int (*clock_set)(uint32_t a_unix_epoch);
|
||||
/*
|
||||
* 函数功能:请求复归全部保护元件状态
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 已受理;-1 - 拒收 int
|
||||
* 限定条件:真机实现 = prot_elm_request_reset()(只置单字节标志,由
|
||||
* protectTask 下一拍消费),本层不等执行结果
|
||||
*/
|
||||
int (*reset_req)(void);
|
||||
/*
|
||||
* 函数功能:取当前处于动作态的保护元件数("动作位复归"的观测量)
|
||||
* 入口参数:无
|
||||
* 返回值:元件数(0 = 无元件动作) uint32_t
|
||||
* 限定条件:真机实现 = popcount(prot_elm_active_mask())
|
||||
*/
|
||||
uint32_t (*active_count)(void);
|
||||
/*
|
||||
* 函数功能:往"操作记录"投一条留痕(人工在面板上做过一件事)
|
||||
* 入口参数:desc - 描述串(NUL 结尾,调用方持有的常量串;投递侧即拷贝)
|
||||
* const char *
|
||||
* 返回值:无
|
||||
* 限定条件:真机实现 = syslog_post_quiet(SYSLOG_OPER, desc)。**谁触发谁投**:
|
||||
* 远端 104 遥控那条已由 App/protocol/iec104/iec104_pts.c 投,
|
||||
* 本地面板这条由页面在受理成功后投,两边各一条、不得互替
|
||||
*/
|
||||
void (*oper_note)(const char *a_desc);
|
||||
/*
|
||||
* 函数功能:请求清除某类记录(只登记请求,不在本上下文删任何东西)
|
||||
* 入口参数:log_id - UI 侧记录源编号(UI_W_REC_LOG_*,与 ui_rec_reader_t 同一编号空间) uint8_t
|
||||
* 返回值:0 - 请求已登记;-1 - 编号越界或登记失败 int
|
||||
* 限定条件:真机实现 = syslog_request_clear(映射后 id)。⚠ 共享代码**严禁**
|
||||
* 调 MCU 后端的 syslog_clear_apply()(模拟器无实现 = 链接期报错),
|
||||
* 也严禁 UI 直接 f_unlink / 碰卷锁(defaultTask 是唯一写者)
|
||||
*/
|
||||
int (*clear_req)(uint8_t a_log_id);
|
||||
/*
|
||||
* 函数功能:查某类记录"清除请求"的状态
|
||||
* 入口参数:log_id - 记录源编号 uint8_t
|
||||
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL 之一 int
|
||||
* 限定条件:真机实现 = syslog_clear_state(映射后 id),返回值直通不改码。
|
||||
* ⚠ WAIT 是正常态(删除在 defaultTask 的 syslog_drain() 尾部落地);
|
||||
* 模拟器端同步删除,永不出现 WAIT —— 页面两种都要能显示
|
||||
*/
|
||||
int (*clear_state)(uint8_t a_log_id);
|
||||
} ui_data_maint_ops_t;
|
||||
|
||||
/*
|
||||
* 清除状态(与 App/log/sys_log.h 的 SYSLOG_CLEAR_NONE/OK/WAIT/FAIL **逐值相等**;
|
||||
* 适配层直通、不做映射表,等值由主机单测在同一 TU 里逐项核对钉住,
|
||||
* 同 UI_W_SOE_T_* 的先例——等值只由测试钉,不由文档转述)
|
||||
*/
|
||||
#define UI_DATA_CLEAR_ST_NONE 0 /* 从未请求过清除 */
|
||||
#define UI_DATA_CLEAR_ST_OK 1 /* 已清空:同拍该记录条数必为 0 */
|
||||
#define UI_DATA_CLEAR_ST_WAIT (-1) /* 请求排队中(正常态,不是失败) */
|
||||
#define UI_DATA_CLEAR_ST_FAIL (-2) /* 落地失败:原记录一条没少,可再发一次请求 */
|
||||
|
||||
/*
|
||||
* 函数功能:注入装置维护执行侧函数表(启动期一次;先于 ui_menu_init)
|
||||
* 入口参数:ops - 函数表(六个成员指针都不得为 NULL;生命周期由注入方保证,
|
||||
* 本层只存指针不拷贝内容) const ui_data_maint_ops_t *
|
||||
* 返回值:0 - 已注入;-1 - ops 为 NULL 或某成员为 NULL(存量不动,页面按
|
||||
* "维护能力不可用"渲染,不崩不猜) int
|
||||
* 限定条件:uiTask 启动期(与 ui_w_records_bind 同一处);传 NULL = 撤销注入
|
||||
* 函数说明:与 ui_data_param_drain() 的"每次调用时传入"不同,本表是**常驻注入**:
|
||||
* 维护动作由用户按键触发、无固定节拍,若也走"每次传表"就得在 uiTask
|
||||
* 主循环里每轮挂一次,而那一轮并没有任何东西要落定(空转一次函数调用
|
||||
* 换不来任何判据)。撤销窗口只在测试里存在
|
||||
*/
|
||||
int ui_data_maint_bind(const ui_data_maint_ops_t *a_ops);
|
||||
|
||||
/*
|
||||
* 函数功能:查询维护通道是否已注入
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 已注入;0 - 未注入 uint8_t
|
||||
* 限定条件:无(只读单字)
|
||||
* 函数说明:页面用它决定"这页能不能操作"——未注入时按钮一律拒并按 LOCK 显示,
|
||||
* 比默默吞掉按键好(现场能立刻看出是装配缺了一步,而不是设备坏了)
|
||||
*/
|
||||
uint8_t ui_data_maint_ready(void);
|
||||
|
||||
/*
|
||||
* 函数功能:请求设置墙钟(时钟设置页"保存"行的唯一出口)
|
||||
* 入口参数:unix_epoch - 由 ui_data_clock_to_epoch() 合成、且已整组校验过的秒 uint32_t
|
||||
* 返回值:0 - 执行侧受理(终态由执行侧的回读校验给出);-1 - 未注入或执行侧失败;
|
||||
* -2 - 参数无效(本层不判日期合法性,那是
|
||||
* ui_data_clock_to_epoch() 的活,到这儿只判 0 哨兵) int
|
||||
* 限定条件:uiTask 上下文(页面按键钩子)
|
||||
*/
|
||||
int ui_data_maint_clock_set(uint32_t a_unix_epoch);
|
||||
|
||||
/*
|
||||
* 函数功能:请求复归全部保护元件(强制复归页的执行出口)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒收 int
|
||||
* 限定条件:uiTask 上下文;⚠ 操作记录**不在本函数里投**——"这是本地面板发起的"
|
||||
* 这一事实只有页面知道,由页面在返 0 之后显式调
|
||||
* ui_data_maint_oper_note() 投(拒收时不得投,屏上留痕与记录留痕
|
||||
* 必须同一条判据)
|
||||
*/
|
||||
int ui_data_maint_reset_req(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取当前动作元件数(复归页"保护动作 N"行的数据源;屏上用字须已入子集)
|
||||
* 入口参数:无
|
||||
* 返回值:元件数;未注入返回 0(= 显示"无元件动作",与真值 0 同形,
|
||||
* 但页脚/态标另有 LOCK 提示装配未接好) uint32_t
|
||||
* 限定条件:页面绘制路径可直调(一次函数指针调用,无 I/O、无锁)
|
||||
* 函数说明:本函数是"动作位复归可证"的**唯一观测量**:复归请求受理后,
|
||||
* 页面按帧重读,N 从 >0 变 0 就是屏上可见的证据;复归不掉只可能是
|
||||
* 测量仍在越限(元件重新 pickup),不是请求丢了
|
||||
*/
|
||||
uint32_t ui_data_maint_active_count(void);
|
||||
|
||||
/*
|
||||
* 函数功能:投一条操作记录留痕(页面在本地动作受理后调)
|
||||
* 入口参数:desc - 描述串(ASCII,长度受执行侧 SYSLOG_DESC_MAX 约束;NULL = 什么都不做)
|
||||
* const char *
|
||||
* 返回值:0 - 已投递;-1 - 未注入或 desc 为 NULL int
|
||||
* 限定条件:uiTask 上下文;投递本身不阻塞、可能因队列满被丢弃(执行侧口径,
|
||||
* 与 syslog_post_quiet 一致——本层不加"投递失败"的第二套显示语义:
|
||||
* 留痕丢了不等于操作没发生,把记录通道故障显示成操作失败更坏)
|
||||
*/
|
||||
int ui_data_maint_oper_note(const char *a_desc);
|
||||
|
||||
/*
|
||||
* 函数功能:请求清除某类记录(清除记录页的执行出口)
|
||||
* 入口参数:log_id - 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:0 - 已受理;-1 - 未注入 / 编号越界 / 执行侧拒绝 int
|
||||
* 限定条件:uiTask 上下文;"受理"≠"已清除",终态读 ui_data_maint_clear_state(),
|
||||
* 事实复核读记录条数(ui_w_records 的同一枚 reader)
|
||||
*/
|
||||
int ui_data_maint_clear_req(uint8_t a_log_id);
|
||||
|
||||
/*
|
||||
* 函数功能:查某类记录的清除状态(清除记录页值列的数据源)
|
||||
* 入口参数:log_id - 记录源编号 uint8_t
|
||||
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL;未注入或编号越界返回 NONE
|
||||
* ("从没请求过",页面据此显示条数而不是结果词) int
|
||||
* 限定条件:页面绘制路径可直调;返回值直通执行侧、不做二次加工(同
|
||||
* ui_data_param_state() 的理由:两处解释同一件事必然漂移)
|
||||
*/
|
||||
int ui_data_maint_clear_state(uint8_t a_log_id);
|
||||
|
||||
/* ── 时钟字段换算(本地域,无时区偏移;"无效值拒收"的唯一判据处)─────────── */
|
||||
|
||||
/*
|
||||
* 年份上下限(两条硬约束的交集,不是拍脑袋的整数区间):
|
||||
* 下限 2000:SD2506 RTC 年字段是相对值 0~99(+2000,见
|
||||
* Drivers/BSP/SD2506/sd2506.h 的 sd2506_time_t.year 注释),
|
||||
* 2000 以下写不进 RTC
|
||||
* 上限 2038:墙钟 epoch 是 uint32_t 秒,上界 0x7FFFFFFF = 2038-01-19 03:14:07
|
||||
* (App/util/sys_clock.h 的 sys_clock_set_unix 取值域一致)
|
||||
*/
|
||||
#define UI_DATA_CLOCK_YEAR_MIN 2000u
|
||||
#define UI_DATA_CLOCK_YEAR_MAX 2038u
|
||||
#define UI_DATA_CLOCK_EPOCH_MAX 0x7FFFFFFFu
|
||||
#define UI_DATA_CLOCK_TS_LEN 19u /* "YYYY-MM-DD HH:MM:SS"(与 rt.clock 同串型) */
|
||||
|
||||
/* 时钟日历字段(本地域;数值就是屏上显示的那六个数,不含任何时区语义) */
|
||||
typedef struct {
|
||||
uint16_t year; /* UI_DATA_CLOCK_YEAR_MIN..UI_DATA_CLOCK_YEAR_MAX */
|
||||
uint8_t month; /* 1..12 */
|
||||
uint8_t day; /* 1..该月真实天数(闰年由本层判,不交给调用方) */
|
||||
uint8_t hour; /* 0..23 */
|
||||
uint8_t minute; /* 0..59 */
|
||||
uint8_t second; /* 0..59 */
|
||||
} ui_data_clock_t;
|
||||
|
||||
/*
|
||||
* 函数功能:取某年某月的天数(含闰年 2 月)
|
||||
* 入口参数:year - 年 uint16_t month - 月 1..12 uint8_t
|
||||
* 返回值:28 / 29 / 30 / 31;月份越界返回 0(调用方据此判非法,不用第二条错误码) uint8_t
|
||||
* 限定条件:闰年规则 = (4 年 && !100 年) || 400 年(与 App/util/sys_clock.c
|
||||
* 的 unix_to_sd2506 / sd2506_to_unix 同一判据,两侧都是完整格里历规则;
|
||||
* 2100 年不在允许年域内,不需要讨论那个例外)
|
||||
*/
|
||||
uint8_t ui_data_clock_days_in_month(uint16_t a_year, uint8_t a_month);
|
||||
|
||||
/*
|
||||
* 函数功能:日历字段 → 本地域 epoch 秒(**同时是这组字段的合法性判据**)
|
||||
* 入口参数:p_in - 六个字段 const ui_data_clock_t *
|
||||
* p_out - 秒 uint32_t *
|
||||
* 返回值:0 - 合法并已输出;-1 - 参数为 NULL;-2 - 字段组合非法(年/月/日/时/分/秒
|
||||
* 任一越界,含 2 月 30 日、非闰年 2 月 29 日、月 13、时 24 等);
|
||||
* -3 - 组合合法但超出取值域(epoch > UI_DATA_CLOCK_EPOCH_MAX) int
|
||||
* 限定条件:无(纯整数,零依赖,可在主机单测跑全表)
|
||||
* 函数说明:日的当月真实上限**在这里**判,不在字段编辑器里判——编辑器只有静态
|
||||
* min/max(日一律 1..31),把闰年规则抄一份进编辑器就是第二真源。
|
||||
* ⚠ 不含时区偏移:本工程墙钟的 epoch 域本身就是本地时间
|
||||
* (App/util/sys_clock.h 的 sys_clock_set_unix 注明"秒,本地时间",
|
||||
* 其正/反换算两头都没有 ±8 运算;App/task/ui_task.c 的 SOE 时标注记
|
||||
* 已把"别顺手补 8 小时"钉过一次)。这里加 28800 的结果是"屏上输 08:00、
|
||||
* 存进去 16:00",且只在真机上错,模拟器照样自洽
|
||||
*/
|
||||
int ui_data_clock_to_epoch(const ui_data_clock_t *p_in, uint32_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:本地域 epoch 秒 → 日历字段
|
||||
* 入口参数:unix_epoch - 秒 uint32_t p_out - 输出 ui_data_clock_t *
|
||||
* 返回值:0 - 已输出;-1 - p_out 为 NULL;-2 - 超出允许年域(< 2000-01-01 或
|
||||
* > UI_DATA_CLOCK_EPOCH_MAX) int
|
||||
* 限定条件:与 ui_data_clock_to_epoch() 互为反函数(同一套格里历规则,
|
||||
* 往返必等;这条往返性是主机单测的断言之一,不是注释里的口头保证)
|
||||
*/
|
||||
int ui_data_clock_from_epoch(uint32_t a_unix_epoch, ui_data_clock_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:解析 "YYYY-MM-DD HH:MM:SS" 串为日历字段
|
||||
* 入口参数:ts - 待解析串(快照 rt.clock 的形状) const char *
|
||||
* p_out - 输出 ui_data_clock_t *
|
||||
* 返回值:0 - 成功;-1 - 参数 NULL;-2 - 串型不对(长度非 19、分隔位不是
|
||||
* '-'/' '/':'、出现非数字);-2 同时覆盖"型对但值非法"
|
||||
* (解析出的组合再过 to_epoch()) int
|
||||
* 限定条件:时钟无效时快照里是**空串**(ui_data.h:149 的口径),空串走 -2;
|
||||
* 调用方据此显示"无时钟"而不是 0000-00-00
|
||||
*/
|
||||
int ui_data_clock_parse(const char *a_ts, ui_data_clock_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:日历字段 → "YYYY-MM-DD HH:MM:SS"(与 ui_data_clock_parse 同串型)
|
||||
* 入口参数:p_in - 字段 const ui_data_clock_t *
|
||||
* buf - 输出缓冲 char * len - 缓冲容量(含 NUL;不足则整串不写) uint32_t
|
||||
* 返回值:0 - 已写出;-1 - 参数无效或容量不足 int
|
||||
* 限定条件:不判字段合法性(调用方刚从 parse/to_epoch 那条链出来);
|
||||
* 手写十进制、不引 stdio——本层是零 LVGL 零 App 的叶子,拉进
|
||||
* lv_snprintf/stdio 就等于给主机单测加依赖
|
||||
*/
|
||||
int ui_data_clock_format(const ui_data_clock_t *p_in, char *a_buf, uint32_t a_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+142
-104
@@ -1,11 +1,19 @@
|
||||
/*
|
||||
* 模块名称:UI 动作确认页(操作页共享实现)实现
|
||||
* 模块功能:见 ui_page_action.h 头注释(确认链状态机 + 投递回报 + 表单页自绘)
|
||||
* 模块功能:见 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 分发标志
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -20,24 +28,19 @@
|
||||
/* 私有宏定义区 */
|
||||
|
||||
#define AC_PROMPT_Y 44 /* 问句/结果行 放置 y(内容区上三分之一) */
|
||||
#define AC_STAGE_Y 66 /* 态标行("二次确认" / "ERR")放置 y */
|
||||
#define AC_STAGE_Y 66 /* 态标行("二次确认" / "ERR" / "LOCK")放置 y */
|
||||
#define AC_WATCH_Y 88 /* 可选观测行放置 y(实例没配观测行时这行留空) */
|
||||
#define AC_HINT_Y 110 /* 键提示行 放置 y */
|
||||
|
||||
/* 固定文案(用字均已入字体子集:二/次/确/认/取/消/返/回/越/界 及 ASCII) */
|
||||
/* 固定文案(用字均已入字体子集:二/次/确/认/取/消/返/回/越/界 及 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"
|
||||
|
||||
/* 私有变量定义区 */
|
||||
|
||||
/*
|
||||
* 模块单例:内容页框架全局单页(s_open 互斥),同一时刻至多一张操作页,
|
||||
* 故钩子经"当前实例指针"取上下文即可成立(头注已载此约束;批 2 各跳板
|
||||
* 各自 static 实例、先后开页互不冲突)
|
||||
*/
|
||||
static ui_page_action_t *s_act; /* 当前实例(NULL = 未在用) */
|
||||
static content_page_def_t s_desc; /* 模板描述符(开页时按实例填充) */
|
||||
#define AC_RO_TXT "LOCK" /* 只读进入的态标(词表与整定保存行一致,
|
||||
都是 ASCII,见 UI_数据供给设计说明.md §6.3) */
|
||||
|
||||
/* 私有函数声明区 */
|
||||
|
||||
@@ -77,123 +80,158 @@ static void ac_draw_centered(lv_layer_t *a_layer, const char *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;
|
||||
(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);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(框架内置区块之后:问句/态标/键提示 三行居中)
|
||||
* 入口参数:layer - 绘制层 lv_layer_t *
|
||||
* clip - 重绘区 const lv_area_t *
|
||||
* 函数功能:开页复位确认链(框架 init_cb 的实现体)
|
||||
* 入口参数:a_inst - 操作页实例 ui_page_action_t *
|
||||
* 返回值:无
|
||||
* 限定条件:经描述符 draw_cb 挂接,页面打开中(s_act 有效)
|
||||
* 限定条件:内容页框架开页链内调用(页面描述符的 init_cb 转发到此)
|
||||
* 函数说明:stage 每次进页归零是**判据**不是收尾清洁:上一轮按到"二次确认"就
|
||||
* ESC 退出、下一轮进来看都不看再按 ENTER,如果 stage 跨次保留就直接
|
||||
* 执行了——破坏性动作不接受这种路径。err 一并清,否则上一次的投递
|
||||
* 失败会挂在新一页的态标行上(用户以为新页一进来就出错)
|
||||
*/
|
||||
static void ac_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
|
||||
if ((s_act == NULL) || (s_act->prompt == NULL)) {
|
||||
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 换成功结果行) */
|
||||
if (s_act->stage == 2u) {
|
||||
if (s_act->done_text != NULL) {
|
||||
ac_draw_centered(layer, s_act->done_text, AC_PROMPT_Y, clip);
|
||||
/* 行 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(layer, s_act->prompt, AC_PROMPT_Y, clip);
|
||||
} else {
|
||||
ac_draw_centered(a_layer, a_inst->prompt, AC_PROMPT_Y, a_clip);
|
||||
}
|
||||
|
||||
/* 行 2:态标(二次确认 / 投递失败 ERR) */
|
||||
if (s_act->stage == 1u) {
|
||||
ac_draw_centered(layer, (s_act->err != 0u) ? AC_ERR_TXT : AC_STAGE2_TXT,
|
||||
AC_STAGE_Y, 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:键提示 */
|
||||
ac_draw_centered(layer, (s_act->stage == 2u) ? AC_HINT_BACK : AC_HINT_DO,
|
||||
AC_HINT_Y, clip);
|
||||
/* 行 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 - LVGL 键值 lv_key_t
|
||||
* 返回值:true = 已消费;false = 交回框架(其余键忽略)
|
||||
* 限定条件:经描述符 key_cb 挂接(key_intercept=0:LEFT 直接关页 = 取消,
|
||||
* 不进本函数;ESC 恒关页框架保证,同样不进本函数)
|
||||
* 函数说明:ENTER:stage0 -> stage1(二次确认);stage1 -> 投递 ok_cb:
|
||||
* 接受且有 done_text -> stage2 回报,接受且无 done_text -> 直接关页,
|
||||
* 拒收 -> err=1 留在 stage1;stage2 -> 关页返回
|
||||
* 函数功能:按键处理(确认链状态机;框架 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 一律消费但不推进、不投递
|
||||
*/
|
||||
static bool ac_key_cb(lv_key_t key) {
|
||||
if (key != LV_KEY_ENTER) {
|
||||
return false;
|
||||
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 (s_act == NULL) {
|
||||
return false; /* 防御 */
|
||||
if (a_key != LV_KEY_ENTER) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
if (s_act->stage == 0u) {
|
||||
s_act->stage = 1u;
|
||||
s_act->err = 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 true;
|
||||
return 1u;
|
||||
}
|
||||
|
||||
if (s_act->stage == 1u) {
|
||||
bool ok = false;
|
||||
|
||||
if (s_act->ok_cb != NULL) {
|
||||
ok = s_act->ok_cb(); /* 只投递请求,不等待执行结果 */
|
||||
}
|
||||
if (!ok) {
|
||||
s_act->err = 1u;
|
||||
ui_content_page_invalidate();
|
||||
return true; /* ERR 显示,再 ENTER 重试 */
|
||||
}
|
||||
if (s_act->done_text == NULL) {
|
||||
ui_content_page_close(); /* 成功即关页回菜单(无回报行形态) */
|
||||
return true;
|
||||
}
|
||||
s_act->stage = 2u;
|
||||
s_act->err = 0u;
|
||||
/* stage 1:第二次 ENTER = 真执行(只投递,不等执行结果) */
|
||||
ok = a_inst->ok_cb();
|
||||
if (!ok) {
|
||||
a_inst->err = 1u;
|
||||
ui_content_page_invalidate();
|
||||
return true;
|
||||
return 1u; /* ERR 显示,下一次 ENTER 重新走投递 */
|
||||
}
|
||||
|
||||
/* stage 2:已回报,ENTER 返回 */
|
||||
ui_content_page_close();
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:开操作页(装载实例 + 复位 + 框架开页)
|
||||
*/
|
||||
bsp_err_t ui_page_action_open(ui_page_action_t *a_inst, uint8_t a_perm) {
|
||||
if ((a_inst == NULL) || (a_inst->ok_cb == NULL) || (a_inst->prompt == NULL)) {
|
||||
return BSP_ERR_PARAM;
|
||||
if (a_inst->done_text == NULL) {
|
||||
ui_content_page_close(); /* 成功即关页(无回报行形态) */
|
||||
return 1u;
|
||||
}
|
||||
if (ui_content_page_is_open() != false) {
|
||||
return BSP_ERR_STATE; /* 框架单页互斥的显式化(失败原因对跳板可判) */
|
||||
}
|
||||
|
||||
s_act = a_inst;
|
||||
s_act->stage = 0u; /* 每次进入固定从一次确认开始 */
|
||||
s_act->err = 0u;
|
||||
|
||||
(void)memset(&s_desc, 0, sizeof(s_desc));
|
||||
s_desc.id = "action";
|
||||
s_desc.title = (a_inst->title != NULL) ? a_inst->title : a_inst->prompt;
|
||||
s_desc.hide_deco = 1u; /* 表单/操作页形态:无浅 V(用户密钥页先例) */
|
||||
s_desc.hide_footer = 1u; /* 无页脚页码 */
|
||||
s_desc.selectable = 0u; /* 无行列表 */
|
||||
s_desc.key_intercept = 0u; /* LEFT 不拦截:关页 = 取消 */
|
||||
s_desc.draw_cb = ac_draw_cb;
|
||||
s_desc.key_cb = ac_key_cb;
|
||||
|
||||
if (ui_content_page_open(&s_desc, a_perm) == false) {
|
||||
s_act = NULL; /* 开页失败不残留实例(钩子不会再被调) */
|
||||
return BSP_ERR_NO_SPACE;
|
||||
}
|
||||
return BSP_OK;
|
||||
a_inst->stage = 2u;
|
||||
a_inst->err = 0u;
|
||||
ui_content_page_invalidate();
|
||||
return 1u;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
/*
|
||||
* 模块名称:UI 动作确认页(操作页共享实现,ui_page_action)
|
||||
* 模块功能:可复用的"确认 -> 二次确认 -> 执行回报"操作页构建器——
|
||||
* 强制复归 / 清除记录 / 手动录波等叶子共用同一页实现,
|
||||
* 各自以一份实例(标题/问句/结果行/执行回调)开页;
|
||||
* 执行回调只投递请求不阻塞(跨任务交接由页面跳板实现),
|
||||
* 模块功能:可复用的"确认 -> 二次确认 -> 执行回报"操作页**钩子三件套**——
|
||||
* 强制复归 / 手动录波等破坏性叶子共用同一份确认链实现,各自持一份
|
||||
* 实例(标题 / 问句 / 结果行 / 执行回调 / 可选观测行);
|
||||
* 执行回调只投递请求不阻塞(跨任务交接由执行侧落定),
|
||||
* 成功经 done_text 回报后再 ENTER 返回,done_text=NULL 成功即关页;
|
||||
* 投递失败显示 ERR,下一次 ENTER 重新走二次确认
|
||||
* 投递失败显示 ERR,下一次 ENTER 重新走二次确认;
|
||||
* 只读权限(查看密码进入)可整页拒执行并显式说"没权限"(need_modify)
|
||||
* 适用范围:Drivers/BSP/UI(表单页形态:无浅 V 装饰、无页脚——用户密钥页
|
||||
* 跟随实机先例;ESC 恒关页 = 取消,LEFT 亦关页 = 取消)
|
||||
* 说明:同一时刻仅一张操作页在用(内容页框架本就全局单页),模块以
|
||||
* static 单例描述符 + 当前实例指针承载钩子上下文;页面跳板
|
||||
* (批 2 各 ui_page_xxx)持 ui_page_action_t 实例并在自己的入口
|
||||
* 调 ui_page_action_open()
|
||||
* 说明:本组件**不建描述符也不开页**(v1.1 起的形态):内容页框架按描述符
|
||||
* **指针**开页,而描述符只能由"一叶一模块"的页面文件导出(真源 =
|
||||
* ui_menu_tree.txt 的 #page 注记 + 生成物 ui_page_registry.c)——
|
||||
* 旧形态"组件自己 open()"决定了它永远不可能被注册表绑定(构建器不是
|
||||
* 描述符,全仓零调用者即其化石证据)。改成 init/draw/key 三个收实例的
|
||||
* 钩子后:确认链仍只有一份实现,而页面跳板薄到只剩"一份实例 + 一枚
|
||||
* 描述符"(见 ui_page_reset.c),多个操作页各自持实例、互不串态
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
@@ -19,6 +23,14 @@
|
||||
* 页面扩充详细方案.md §3.4 装置维护操作页 +
|
||||
* §4.2 ui_page_action bind(title, prompt, ok_cb)
|
||||
* 二次确认、ok_cb 只投递不阻塞)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-3(装置维护 3 页):改成钩子三件套
|
||||
* 形态(init/draw/key 收实例指针),删掉模块单例
|
||||
* 与 ui_page_action_open()——构建器开不了注册表
|
||||
* 绑定的页(框架按描述符指针开页),旧形态全仓
|
||||
* 零调用者;新增 need_modify(只读拒绝执行并显
|
||||
* LOCK)与可选观测行(watch_label + watch_cb,
|
||||
* 复归页用它把"动作位复归"画在同一屏上);
|
||||
* 错误通道由 bsp_err_t 改为无返回 + uint8_t 分发标志
|
||||
*/
|
||||
#ifndef UI_PAGE_ACTION_H
|
||||
#define UI_PAGE_ACTION_H
|
||||
@@ -30,39 +42,76 @@ extern "C" {
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "bsp_err.h" /* bsp_err_t(开页结果错误通道——BSP 公开 API 约定) */
|
||||
#include "lvgl.h" /* lv_key_t / lv_layer_t:钩子三件套的形参类型(本组件不建对象) */
|
||||
|
||||
/* 执行回调:只投递请求(事件位/消息队列),立即返回;
|
||||
* 返回值:true = 投递被接受(页面转成功回报),false = 拒收(页面显示 ERR) */
|
||||
typedef bool (*ui_page_action_ok_fn)(void);
|
||||
|
||||
/* 操作页实例(配置字段 Flash 指针 + 运行态);
|
||||
* 用法:页面跳板 static 一份、花括号初始化配置,开页经 ui_page_action_open */
|
||||
/* 操作页实例(配置字段 = Flash 指针与只读回调,运行态 = 本模块写的两个字节);
|
||||
* 用法:页面跳板 static 一份、花括号初始化配置,把 init/draw/key 三个钩子直接
|
||||
* 转进本组件(v1.1 起本组件不再自己开页——内容页框架按描述符指针开页,
|
||||
* 跳板就是那枚描述符的属主,见 ui_page_reset.c) */
|
||||
typedef struct {
|
||||
const char *title; /* Flash:页标题(框架头部居中) */
|
||||
const char *prompt; /* Flash:确认问句(如"确认复归所有保护?"——
|
||||
问句用字须已入字体子集) */
|
||||
/* 页标题不在本结构里:描述符(连 title)由页面跳板 own —— 见 ui_page_reset.c;
|
||||
* 两处各留一份标题就是两个真源,改一处屏上另一处不变那种 */
|
||||
const char *prompt; /* Flash:确认问句(如"确认复归保护?"——
|
||||
问句用字须已入字体子集,否则显示空白方块) */
|
||||
const char *done_text; /* Flash:成功后结果行(如"已请求复归");
|
||||
NULL = 成功后直接关页回菜单 */
|
||||
ui_page_action_ok_fn ok_cb; /* 必填:执行投递 */
|
||||
ui_page_action_ok_fn ok_cb; /* 必填:执行投递(只投递,不等待执行结果) */
|
||||
uint8_t need_modify; /* 1 = 只读权限(查看密码进入)时拒绝执行:
|
||||
两次 ENTER 都不投递、态标位改显 LOCK。
|
||||
破坏性动作必须置 1——只靠"屏上看着像能按"
|
||||
拦不住人,必须落在键路上 */
|
||||
const char *watch_label; /* Flash:可选观测行的前缀(NULL = 不画该行),
|
||||
如"保护动作"。配合 watch_cb 让"复归前 N>0、
|
||||
复归后 N=0"在同一屏上看得见 */
|
||||
uint32_t (*watch_cb)(void); /* 可选:观测行取值(NULL = 不画该行;与
|
||||
watch_label 要么都给要么都不给) */
|
||||
uint8_t stage; /* 运行态:0 一次确认 / 1 二次确认 / 2 完成回报 */
|
||||
uint8_t err; /* 运行态:1 = 上轮投递被拒(显示 ERR) */
|
||||
} ui_page_action_t;
|
||||
|
||||
/*
|
||||
* 函数功能:开操作页(装载实例 + 复位确认态 + 经内容页框架开页)
|
||||
* 入口参数:inst - 操作页实例(配置字段须已填好;ok_cb 不得为 NULL)
|
||||
* 函数功能:开页复位确认链(接框架的 init_cb;v1.1 起本组件不再自己开页)
|
||||
* 入口参数:inst - 操作页实例(ok_cb 不得为 NULL;配置字段由跳板在编译期填好)
|
||||
* ui_page_action_t *
|
||||
* perm - 授予权限(ui_perm_t;由跳板决定传什么权限,
|
||||
* 组件不拦权限只拦 ok_cb 为空)
|
||||
* 返回值:BSP_OK = 已开页;BSP_ERR_PARAM = 参数无效;
|
||||
* BSP_ERR_STATE = 已有页面打开(框架单页互斥);
|
||||
* BSP_ERR_NO_SPACE = 框架开页失败(池不足等) bsp_err_t
|
||||
* 限定条件:uiTask 上下文;每次进入从一次确认开始(不跨次保留 stage)
|
||||
* 函数说明:键语义——ENTER 推进确认链(0->1->执行->2),ESC/LEFT 关页取消;
|
||||
* stage=2 时 ENTER 关页返回
|
||||
* 返回值:无
|
||||
* 限定条件:内容页框架开页链内调用(uiTask);实例为 NULL 时空操作(防御)
|
||||
* 函数说明:每次进入固定从一次确认开始——"上一次按到一半"跨次保留下来就是
|
||||
* 第二次进页一按 ENTER 直接执行,那是操作票事故的形状,不是效率优化
|
||||
*/
|
||||
bsp_err_t ui_page_action_open(ui_page_action_t *a_inst, uint8_t a_perm);
|
||||
void ui_page_action_init(ui_page_action_t *a_inst);
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(接框架的 draw_cb:问句 / 态标 / 可选观测行 / 键提示,四行居中)
|
||||
* 入口参数:inst - 操作页实例 ui_page_action_t *
|
||||
* layer - 绘制层 lv_layer_t *
|
||||
* clip - 重绘区 const lv_area_t *
|
||||
* 返回值:无
|
||||
* 限定条件:框架绘制链末尾调用(本组件只画自己那几行,标题/装饰/页脚归框架);
|
||||
* 实例或 prompt 为 NULL 时空操作
|
||||
* 函数说明:态标行优先级——只读且需修改权限 > 二次确认 > 投递失败 ERR;
|
||||
* 只读那条排最前是刻意的:现场最常见的误读是"我按了没反应",
|
||||
* 把"没权限"显出来才不会让人反复按
|
||||
*/
|
||||
void ui_page_action_draw(ui_page_action_t *a_inst, lv_layer_t *a_layer,
|
||||
const lv_area_t *a_clip);
|
||||
|
||||
/*
|
||||
* 函数功能:按键处理(接框架的 key_cb:确认链状态机)
|
||||
* 入口参数:inst - 操作页实例 ui_page_action_t *
|
||||
* key - LVGL 键值 lv_key_t
|
||||
* 返回值:1 = 已消费(框架跳过内置语义);0 = 未消费(交回框架)
|
||||
* (键分发标志非错误通道,取 uint8_t 不用 bool——
|
||||
* bsp-err/err-type 口径,同 ui_w_editor_key)
|
||||
* 限定条件:经描述符 key_cb 挂接;LEFT/ESC 不进本函数(框架恒关页 = 取消)
|
||||
* 函数说明:ENTER——stage0 转二次确认;stage1 调 ok_cb 投递:受理且有 done_text
|
||||
* 转 stage2 回报、受理且无 done_text 直接关页、拒收置 err 留在 stage1;
|
||||
* stage2 关页返回。只读且 need_modify 时任何 ENTER 都不推进也不投递
|
||||
*/
|
||||
uint8_t ui_page_action_key(ui_page_action_t *a_inst, lv_key_t a_key);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
/*
|
||||
* 模块名称: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,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 模块名称:UI Page clear (清除记录内容页)
|
||||
* 模块功能:清除记录叶子的内容页声明——向框架 ui_content_page 提供页面描述符
|
||||
* (条数走 ui_w_records 的同一枚注入 reader、删除只发请求(syslog_request_clear 语义在 App 侧适配层))
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3「装置维护 3 页」,判据 =
|
||||
* docs/任务规划/UI补齐.md §2 第 3 行;形态 =
|
||||
* docs/UI设计/页面扩充详细方案.md §3.4)
|
||||
*/
|
||||
#ifndef UI_PAGE_CLEAR_H
|
||||
#define UI_PAGE_CLEAR_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "ui_content_page.h"
|
||||
|
||||
/* 页面描述符(定义在 ui_page_clear.c;生成物按菜单树 #page=clear 引用本符号) */
|
||||
extern const content_page_def_t ui_page_clear_desc;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* UI_PAGE_CLEAR_H */
|
||||
@@ -0,0 +1,388 @@
|
||||
/*
|
||||
* 模块名称: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,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 模块名称:UI Page clock (时钟设置内容页)
|
||||
* 模块功能:时钟设置叶子的内容页声明——向框架 ui_content_page 提供页面描述符
|
||||
* (字段编辑走 ui_w_editor、预填走 ui_data 实时帧、写方向只经 ui_data_maint_* 注入转发)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3「装置维护 3 页」,判据 =
|
||||
* docs/任务规划/UI补齐.md §2 第 3 行;形态 =
|
||||
* docs/UI设计/页面扩充详细方案.md §3.4)
|
||||
*/
|
||||
#ifndef UI_PAGE_CLOCK_H
|
||||
#define UI_PAGE_CLOCK_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "ui_content_page.h"
|
||||
|
||||
/* 页面描述符(定义在 ui_page_clock.c;生成物按菜单树 #page=clock 引用本符号) */
|
||||
extern const content_page_def_t ui_page_clock_desc;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* UI_PAGE_CLOCK_H */
|
||||
@@ -10,10 +10,13 @@
|
||||
#include "ui_page_registry.h"
|
||||
|
||||
#include "ui_page_ac.h"
|
||||
#include "ui_page_clear.h"
|
||||
#include "ui_page_clock.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_reset.h"
|
||||
#include "ui_page_runlog.h"
|
||||
#include "ui_page_selftest.h"
|
||||
#include "ui_page_soe.h"
|
||||
@@ -36,6 +39,9 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = {
|
||||
{ 14u, &ui_page_view_set_desc }, /* node 14 定值 */
|
||||
{ 15u, &ui_page_view_word_desc }, /* node 15 控制字 */
|
||||
{ 16u, &ui_page_view_plate_desc }, /* node 16 软压板 */
|
||||
{ 24u, &ui_page_clock_desc }, /* node 24 时钟设置 */
|
||||
{ 25u, &ui_page_reset_desc }, /* node 25 强制复归 */
|
||||
{ 27u, &ui_page_clear_desc }, /* node 27 清除记录 */
|
||||
{ 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */
|
||||
{ 33u, &ui_page_soe_desc }, /* node 33 SOE记录 */
|
||||
{ 39u, &ui_page_selftest_desc }, /* node 39 自检记录 */
|
||||
|
||||
@@ -25,7 +25,7 @@ typedef struct {
|
||||
const content_page_def_t *desc; /* 页面描述符(Flash const) */
|
||||
} ui_page_entry_t;
|
||||
|
||||
#define UI_PAGE_MAP_COUNT 16u /* 绑定了内容页的叶子数 */
|
||||
#define UI_PAGE_MAP_COUNT 19u /* 绑定了内容页的叶子数 */
|
||||
|
||||
extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT];
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* 模块名称:UI Page reset (强制复归内容页)
|
||||
* 模块功能:装置维护 → 强制复归 叶子(node 25,#P 受密码保护)的内容页——
|
||||
* 复用 ui_page_action 共享确认链(一次确认 → 二次确认 → 执行回报),
|
||||
* 并在同一屏画一行"保护动作 N"(N = 当前处于动作态的保护元件个数):
|
||||
* 复归前 N > 0、复归请求被 protectTask 消费后 N 归 0,这个变化就是
|
||||
* "动作位真的复归了"的屏上证据,不用翻页、不用外接表;
|
||||
* 执行只做两件事:投复归请求 + 补一条本地操作记录留痕;
|
||||
* 只读进入(查看密码)整页拒执行并显式说"没权限"(LOCK)——
|
||||
* 破坏性动作不给"看着像能按"的第二次机会;
|
||||
* 本文件零 App include、零存储触点(分层方向由注入表守住)
|
||||
* 为什么这么薄:确认链的形态(含二次确认与回报)是共享件,四个维护/操作类叶子
|
||||
* 共用一份实现;本文件只把"这一页问什么、执行什么、观测什么"三项
|
||||
* 配置接进框架的描述符骨架(同 ui_page_runlog 之于 ui_w_records)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3;判据 docs/任务规划/UI补齐.md
|
||||
* §2 第 3 行「强制复归确认后动作位复归且有 SOE」)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include "ui_page_reset.h"
|
||||
|
||||
#include "ui_content_page.h"
|
||||
#include "ui_data.h"
|
||||
#include "ui_page_action.h"
|
||||
|
||||
/* 私有宏定义区 */
|
||||
|
||||
/*
|
||||
* 本地这条操作记录的描述串:ASCII 20 字节 < SYSLOG_DESC_MAX(28)。
|
||||
* 与远端那条(App/protocol/iec104/iec104_pts.c 的 "force reset")**刻意不同串**——
|
||||
* 操作记录是审计面,两条一样就再也分不出"是主站下发的还是现场有人按面板",
|
||||
* 而那正是查遥控事故时第一个要问的问题。
|
||||
* 为什么由页面投而不是由 prot_elm_request_reset() 投:那条函数在 algo 层,
|
||||
* 在那里投会把 sys_log.h 拽进算法层(并把记录通道的依赖摊到 test/host/prot_dim
|
||||
* 的 include 路径上);而"这是本地发起的"这个事实只有页面知道(口径真源
|
||||
* App/log/README.md 五类记录表 + task-10 结论)。
|
||||
*/
|
||||
#define RS_OPER_NOTE "force reset by panel"
|
||||
|
||||
/* 私有函数声明区(静态初始化器要引用下面两个函数的地址,故先声明) */
|
||||
|
||||
static bool rs_ok(void);
|
||||
static uint32_t rs_watch(void);
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/*
|
||||
* 本页操作页实例(配置全为 Flash 字面量与函数地址;运行态两字节由组件写)。
|
||||
* 字段顺序 = ui_page_action_t 声明序(ARMCC v5 = C99 要求指定顺序一致)。
|
||||
*/
|
||||
static ui_page_action_t s_act = {
|
||||
"确认复归保护?", /* prompt:问句用字已在字库(确/认/复/归/保/护 + '?') */
|
||||
"已请求复归", /* done_text:回报"请求已受理",不谎称"已复归完成" */
|
||||
rs_ok, /* ok_cb */
|
||||
1u, /* need_modify:只读进入一律拒执行 */
|
||||
"保护动作", /* watch_label(用字全在既有子集内;曾拟"动作元件",
|
||||
其中"元"不在子集 —— 见本行下方注释与终报) */
|
||||
rs_watch, /* watch_cb */
|
||||
0u, /* stage(运行态初值) */
|
||||
0u /* err (运行态初值) */
|
||||
};
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:执行投递——请求复归,受理成功后补投本地操作记录
|
||||
* 入口参数:无
|
||||
* 返回值:true = 已受理(组件转成功回报);false = 拒收(组件显示 ERR) bool
|
||||
* 限定条件:由 ui_page_action_key 在第二次 ENTER 时调用(uiTask 上下文)
|
||||
* 函数说明:两条腿的次序即判据——**先请求后留痕**:请求被拒时一条记录都不投,
|
||||
* 给一次没发生过的操作写审计记录,比少一条记录坏得多(同
|
||||
* App/task/iec104_serv.c 对时留痕"只在真改成功之后才投"那条纪律)。
|
||||
* 维护通道未注入(装配缺一半)也走拒收这条腿:屏上显 ERR,
|
||||
* 比"按了像成功其实什么都没发生"诚实
|
||||
*/
|
||||
static bool rs_ok(void) {
|
||||
if (ui_data_maint_reset_req() != 0) {
|
||||
return false;
|
||||
}
|
||||
(void)ui_data_maint_oper_note(RS_OPER_NOTE);
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前处于动作态的保护元件数(观测行 "保护动作 N" 的 N)
|
||||
* 入口参数:无
|
||||
* 返回值:元件数(未注入时 0) uint32_t
|
||||
* 限定条件:绘制路径可直调(一次转发,零 I/O、零锁)
|
||||
* 函数说明:⚠ 台架判据的前提要写清:产品构建现处"未标定闭锁"
|
||||
* (App/task/protect_task.c 停走判据),闭锁态下元件本就是复位态,
|
||||
* "N 从 >0 变 0"这一眼要等两点标定解卡后才成立;解卡前该判据
|
||||
* 退化为"页面受理 + 操作记录落一条",不得把闭锁态报成复归无效
|
||||
*/
|
||||
static uint32_t rs_watch(void) {
|
||||
return ui_data_maint_active_count();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:开页复位确认链(框架 init_cb)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:框架开页链内调用
|
||||
* 函数说明:复位由组件做(stage/err 是它的运行态),本页不重复清一遍
|
||||
*/
|
||||
static void pg_init(void) {
|
||||
ui_page_action_init(&s_act);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:自绘(框架 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) {
|
||||
ui_page_action_draw(&s_act, a_layer, a_clip);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按键转发(框架 key_cb)
|
||||
* 入口参数:a_key - LVGL 键值 lv_key_t
|
||||
* 返回值:true = 本页已消费;false = 交回框架 bool
|
||||
* 限定条件:框架按键链内
|
||||
* 函数说明:确认链的判定全在组件里(含只读拒执行),本页只如实透传返回值,
|
||||
* 不在这层"顺手"加判据——两处各判一次早晚分家
|
||||
*/
|
||||
static bool pg_key(lv_key_t a_key) {
|
||||
return (ui_page_action_key(&s_act, a_key) != 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=reset 绑定引用)
|
||||
* 表单/操作页形态:无浅 V、无页脚、无行列表(用户密钥页与网口设置页先例);
|
||||
* edit_capable=1 是**能不能改**的那半,另半(本次进入授予的权限)由
|
||||
* ui_content_page_is_editable() 给——两半都在,组件才能判出"只读";
|
||||
* key_intercept=0:LEFT 不拦截,关页 = 取消(本链没有"编辑到一半"的状态要保)
|
||||
*/
|
||||
const content_page_def_t ui_page_reset_desc = {
|
||||
.id = "reset",
|
||||
.title = "强制复归",
|
||||
.hide_deco = 1u,
|
||||
.hide_footer = 1u,
|
||||
.selectable = 0u,
|
||||
.key_intercept = 0u,
|
||||
.edit_capable = 1u,
|
||||
.init_cb = pg_init,
|
||||
.draw_cb = pg_draw,
|
||||
.key_cb = pg_key,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 模块名称:UI Page reset (强制复归内容页)
|
||||
* 模块功能:强制复归叶子的内容页声明——向框架 ui_content_page 提供页面描述符
|
||||
* (确认链与观测行全部走 ui_page_action 共享实现,本模块零 App include、零存储触点)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-26
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-26 DTU 项目组 创建(道F task-3「装置维护 3 页」,判据 =
|
||||
* docs/任务规划/UI补齐.md §2 第 3 行;形态 =
|
||||
* docs/UI设计/页面扩充详细方案.md §3.4)
|
||||
*/
|
||||
#ifndef UI_PAGE_RESET_H
|
||||
#define UI_PAGE_RESET_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include "ui_content_page.h"
|
||||
|
||||
/* 页面描述符(定义在 ui_page_reset.c;生成物按菜单树 #page=reset 引用本符号) */
|
||||
extern const content_page_def_t ui_page_reset_desc;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* UI_PAGE_RESET_H */
|
||||
@@ -31,6 +31,26 @@ void ui_w_records_bind(const ui_rec_reader_t *a_reader) {
|
||||
s_reader = a_reader;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按记录源编号查条数(不经行缓存的裸计数)
|
||||
* 入口参数:log_id - 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:条数;未注入 reader 或编号未知一律 0,即空态,调用方按"无记录"显示
|
||||
* uint16_t
|
||||
* 限定条件:uiTask 上下文(记录侧绘制/按键链)
|
||||
* 函数说明:为什么开这个口子——清除记录页要在 7 行上分行显示各类条数(点完
|
||||
* "清除"之后条数归零就是它唯一的真反馈),而 reader 是本组件私有
|
||||
* 的注入表;不开放一个只读入口,页面就只能自己去注第二份 reader,
|
||||
* 那正好把 task-6 刚削掉的分层反向边又接回来(口径见
|
||||
* docs/UI设计/UI_数据供给设计说明.md §7.2/§7.3)。
|
||||
* 本函数不带行缓存:缓存属于"某一条记录的字段",与条数无关
|
||||
*/
|
||||
uint16_t ui_w_records_count_of(uint8_t a_log_id) {
|
||||
if (s_reader == NULL) {
|
||||
return 0u;
|
||||
}
|
||||
return s_reader->count(a_log_id);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:总行数(动态;每条记录 2 行,空日志 = 单条伪行"无记录")
|
||||
* 入口参数:a_inst - 记录页实例 ui_w_records_t *
|
||||
|
||||
@@ -27,11 +27,25 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* 记录源不透明编号:UI 侧只认"0 号 / 1 号日志",App 侧适配层映射到 syslog_id_t
|
||||
/* 记录源不透明编号:UI 侧只认"N 号日志",App 侧适配层映射到 syslog_id_t
|
||||
* (映射表见 App/task/ui_task.c 与 simulator/main.c 的绑定处;新增记录页 =
|
||||
* 这里加编号 + 两端适配层各加一条映射) */
|
||||
* 这里加编号 + 两端适配层各加一条映射)
|
||||
* ⚠ **顺序契约(Lead 裁定 2026-10-26,task-6 起 7 条)**:本表顺序必须逐字等于
|
||||
* App/log/sys_log.h 的 syslog_id_t 枚举序(RUN/SELFTEST/ACCIDENT/ALARM/START/
|
||||
* OPER/YK),且**只准尾部追加**——枚举序就是两端 s_paths[]/s_caps[] 的下标,
|
||||
* 中间插值会安静地把事故写进告警文件(记录侧已有 TC-SYSLOG-130/131/131b 那条
|
||||
* 机械防线,UI 侧对应的那条 = test/host/ui_data 的 TC-UIDATA-024 逐项核对 +
|
||||
* 两端适配层里的编译期数组守卫;编号"不透明"指的是**页面不得依赖**这个相等,
|
||||
* 不是允许两者各数各的:适配层始终显式映射,从不把本编号强转成 syslog_id_t)。
|
||||
* 追加之外的任何改动(换序/删号/复用号)都会让已落盘的记录被读成别的类别。 */
|
||||
#define UI_W_REC_LOG_RUN 0u /* 0 号:运行记录(MCU /log/run.log) */
|
||||
#define UI_W_REC_LOG_SELFTEST 1u /* 1 号:自检记录(MCU /log/selftest.log) */
|
||||
#define UI_W_REC_LOG_ACCIDENT 2u /* 2 号:事故记录(/log/accid.log,跳闸型动作) */
|
||||
#define UI_W_REC_LOG_ALARM 3u /* 3 号:保护告警(/log/alarm.log,告警型动作) */
|
||||
#define UI_W_REC_LOG_START 4u /* 4 号:保护启动(/log/start.log,picked 上升沿) */
|
||||
#define UI_W_REC_LOG_OPER 5u /* 5 号:操作记录(/log/oper.log,人工/远端操作留痕) */
|
||||
#define UI_W_REC_LOG_YK 6u /* 6 号:遥控记录(/log/yk.log,104 选择/执行/拒绝) */
|
||||
#define UI_W_REC_LOG_N 7u /* 编号个数(上界;两端映射表的数组长度,不是合法编号) */
|
||||
|
||||
/* 读取缓冲容量(UI 侧定义;必须 >= sys_log 契约下限:时间戳 19+NUL、描述 28+NUL。
|
||||
* sys_log 侧 SYSLOG_TS_LEN/SYSLOG_DESC_MAX 变化时这里同步加宽——容量不足时
|
||||
@@ -91,6 +105,19 @@ typedef struct {
|
||||
*/
|
||||
void ui_w_records_bind(const ui_rec_reader_t *a_reader);
|
||||
|
||||
/*
|
||||
* 函数功能:按记录源编号查条数(不经行缓存的裸计数;清除记录页逐行显示用)
|
||||
* 入口参数:log_id - 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:条数;未注入 reader 或编号未知一律 0,即空态,调用方按"无记录"显示
|
||||
* uint16_t
|
||||
* 限定条件:uiTask 上下文(绘制/按键链)
|
||||
* 函数说明:reader 是本组件私有的注入表,不开放只读入口的话,想按行显示条数的
|
||||
* 页面只能自己去注第二份 reader——那正好把 task-6 刚削掉的分层反向边
|
||||
* 接回来(口径 docs/UI设计/UI_数据供给设计说明.md §7.2/§7.3);
|
||||
* 本函数不带行缓存(缓存属于"某条记录的字段",与条数无关)
|
||||
*/
|
||||
uint16_t ui_w_records_count_of(uint8_t a_log_id);
|
||||
|
||||
/*
|
||||
* 函数功能:总行数(动态;每条记录 2 行,空日志 = 单条伪行"无记录")
|
||||
* 入口参数:a_inst - 记录页实例 ui_w_records_t *
|
||||
|
||||
Reference in New Issue
Block a user