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:
2026-10-26 22:43:54 +08:00
parent 45086cf5f9
commit 52e28885cb
34 changed files with 2854 additions and 175 deletions
+475 -4
View File
@@ -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;
}