Files
STM32F4-Base/Drivers/BSP/UI/ui_data.c
T
wandering1 52e28885cb feat(UI): 装置维护 3 页落地(时钟 / 强制复归 / 清除记录),内容页黄金 27→40
三个薄页面模块 ui_page_clock / ui_page_reset / ui_page_clear,页面注册由生成器
自动写双端(裁定 A-1:「三叶各绑一页」与「零新增 .c」不可兼得,选薄页面加生成器,
不写手写 node switch——那种分发在增删叶子时会静默换功能,且没有任何门禁照得到)。

数据侧是注入式函数表:UI 不 include App 层的头。两条硬理由都实测过——分层方向
(layer-deps)之外,模拟器根本不链 sys_clock,ALLOW 豁免挡得住 layer-deps,
挡不住模拟器链接期。面板对时的唯一真判据是回读 RTC:sys_clock_set() 恒返 0
(RTC 写失败只打日志),所以真机侧写 1 后回读 1,最坏约 200 ms 落在一次「保存」
按键上,IWDG 800 ms 窗有余;上界与依据都写进注释。

「无时钟」徽标位置有两个像素级实测坑:放标题带右侧会被 4 字标题连成「时钟设置
无时钟」一句话;放页脚带左侧 x=7 会压住页脚——页脚从 x=6 起排,不是居中。
最终落在清单末行与页脚之间的空带 y=124。clear_done 那张顺带给出像素证据:
清 run.log 之后「操作记录」那一行从 2 变 3,页面上没缓存、没撒谎。

ui_w_records 的 UI_W_REC_LOG_* 顺序必须等于 syslog_id_t 顺序(7 类,只准尾追加),
已加编译期顺序守卫;执行侧 7 条映射表同样带编译期守卫。

测试与真值:内容页黄金 27 → 40 页(+13),文本断言 +68、反向断言 ABSENT +19;
27 张旧参考 git diff 为空 = 逐字节未变。往返池 5448 B 与菜单常驻堆 5368 B 均未变;
字库未扩(源码在用 171 并 KEEP_EXTRA 39 = 清单 183),font-charset 与 cn-chars、
cn-font 字节一致。@build 0 错误 0 警告;uiTask 静态最坏链 1,336 B / 16,384
(余 91%)与基线相同,页面钩子没进最坏链。

一条测试学结论单独留痕:反向断言不能当「位置不重叠」的证据——本脚本的字形匹配
把 y 带判得比字面宽(徽标在 125..136、页脚在 144..155,仍被 (140,160) 判命中)。
该防线改由正向断言「页脚未被徽标压住」承担(压字那一版实测过不了它)。

两条自我更正一并记档(作者自报,按真值而非原口径):
① 先前报「文案零新增汉字」是错的——观测行拟用「动作元件」,「元」不在子集,
cn-chars 与 font-charset 如实红;最终改「保护动作 N」才让「零新增」成为事实。
顺序是:先按机器红,再按真值写结论。
② 先前报「6 个文件 BOM 逐个核过」被 bom-guard 证伪:丢 BOM 发生在上一次编译之后,
正是 AGENTS 那句「历史 0 警告不代表安全」的活例;此后一律以退出码为准。

未覆盖项不假装:台架「N>0 → N=0」在产品构建的未标定闭锁期拿不到(闭锁期 N 恒 0),
判据显式退化为「页面受理 + 落一条操作记录」,写在 rs_watch 注释里。

生成物入库例外照用:uvprojx 只加 3 个 ui_page_ 的 FilePath(真增删源文件),
CMakeLists 同批;两处都由 gen_menu.py 自己写,没手写。

本笔先走了一次真钩子:20 条钩子实跑通过(ui-golden 菜单 8 状态加内容页 40 页、
generated-fresh、registration-check、font-charset、ui-data 单测、cppcheck、
coding-standard、bom、mojibake、id-check、doc-links 等),只有 mem-numbers 与
mem-budget 两条红——磁盘上的 .map 出自 20:39 那次构建、含三条在飞道的代码,
与 HEAD 台账必然不同码。按仓库既有约定(这两条等台账回填时允许 --no-verify 并
正文留证)落这一笔;回填由我统一在收尾一笔做。

三条在飞道(task-26 崩溃证据路径、task-16 与 task-17 谓词收敛和卷锁观测、
task-36 台架工具)的未暂存改动被 autostash 暂时收走,钩子看到的是 HEAD 加本笔;
过程中曾把 comm 道未跟踪的 iec104_comm.c 临时移出(它不在 stash 覆盖面内),
那次移出反而让 ui-golden 撞见 CMake 缓存里的悬空引用而红——已改用「先落 comm
那笔、再落本笔」的顺序消掉这个中间态。ui_data_maint_ready() 待删(只有测试在读
的自检口),排在本笔之后单独一笔。
2026-10-26 22:43:54 +08:00

894 lines
38 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI 数据快照存储(ui_data)
* 模块功能:三通道快照(实时量 rt / 参数 par / 装置信息 info)的存储、发布盖章与
* 只读访问——本模块是纯内存叶子:不取数、不格式化、不判断业务含义、
* 不碰 LVGL、不 include 任何 App 头(职责边界与依赖方向 =
* Drivers/BSP/UI/ui_data.h 文件头与
* docs/UI设计/UI_数据供给设计说明.md §3.2)
* 并发模型:单写者同上下文——唯一发布者 App/app/ui_data_feed.c 与全部页面钩子都在
* uiTask 内串行执行,故无锁、无双缓冲、无 seqlock;将来真出现第二个
* 发布者,保护收在本模块内部(双槽 + 每槽 seq + 单字节活动下标,照
* App/algo/phasor.c 双区先例),页面代码不受影响
* (判据 docs/UI设计/UI_数据供给设计说明.md §5.1)
* 写路径(定值整定请求队列)同理:生产者是页面"保存"按键钩子、消费者是
* 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_maint_*:墙钟设置 / 强制复归 / 清除记录)
* ——常驻注入的 ui_data_maint_ops_t 转发,本层不解释业务含义;
* 为什么这里常驻注入而 ① 每次传表,见 ui_data.h 通道四的函数说明;
* ③ 时钟字段换算(ui_data_clock_*:本地域、无时区偏移、纯整数零依赖)
* ——「无效值拒收」的唯一判据处,也是唯一能在主机单测里跑全表的
* 日历判据(页面 .c 依赖 lvgl.h,编不进主机测试单元)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
* v1.1 2026-10-26 DTU 项目组 道F task-1:新增定值写路径环形队列(深度 4、满返 -1)
* 与状态机(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
* (版本号是页面与写侧唯一的对齐凭据,无字段变更却升版
* = 让老页面以为新帧换了布局)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_data.h"
/* 静态变量定义区 */
/* 三通道存储(上电全零 = "尚无首帧",与 ui_data_init() 后状态一致) */
static ui_data_rt_t s_rt;
static ui_data_par_t s_par;
static ui_data_info_t s_info;
/*
* 发布计数:seq 由本层盖章,与入帧携带的 seq 无关——入帧的该字段被覆盖。
* 读侧"有无首帧"的判据是计数为 0,不是帧内 valid:valid 是"这一帧有没有有效
* 数据"(写侧可能发布一帧全无效的真值),两者语义不同,混用会让"首帧之前"
* 与"首帧但无数据"在页面上显示成同一种态。
*/
static uint32_t s_rt_cnt;
static uint32_t s_par_cnt;
static uint32_t s_info_cnt;
/* ── 写路径:定值整定请求队列(深度 UI_DATA_PARAM_QD 的静态环形数组)──────── */
/*
* mask 合法位集合 = 低 UI_DATA_SET_N 位(40 项 → 0x000000FFFFFFFFFF)。
* 用"全 1 右移"而不是"1 左移 40 位再减 1":前者在 UI_DATA_SET_N == 64 时仍是
* 良定义(移 0 位),后者会退化成立即未定义行为(移位数 == 位宽)。
* 项数一旦超过 64,下面的编译期守卫当场拦下——位图放不下就是表示法该换,
* 绝不能静默把高 24 项变成"投不进去也不报错"
*/
#define UI_DATA_PARAM_MASK_VALID ((uint64_t)(~(uint64_t)0u) >> (64u - UI_DATA_SET_N))
typedef char ui_data_chk_mask_t[((uint32_t)UI_DATA_SET_N <= 64u) ? 1 : -1];
/* 环形队列:head = 出队下标,cnt = 在队条数(0..UI_DATA_PARAM_QD)
* 为什么用 head+cnt 而不是 head+tail:cnt 直接就是"还有几条没落定",
* state() 判 WAIT 不必再做 (tail - head) 取模减法,少一处可算错的地方 */
static ui_data_param_req_t s_pq[UI_DATA_PARAM_QD];
static uint8_t s_pq_head;
static uint8_t s_pq_cnt;
/* 写路径状态字与越界位图(页面只读;int/uint64 单字读写在 M4 上不撕裂) */
static int s_pq_state = UI_DATA_PARAM_ST_IDLE;
static uint64_t s_pq_err_mask;
/* 函数定义区 */
/*
* 函数功能:快照层初始化——三通道清零(valid=0、seq=0)+ 写路径队列复位,
* 页面即按"无首帧/未投递"渲染
* 入口参数:无
* 返回值:无
* 限定条件:内核启动前可调(纯内存操作,零 OS 依赖);可重复调用(幂等)
* 函数说明:静态存储上电本已清零,本函数的意义是运行期复位(例如"恢复出厂后
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态。
* 写路径一并丢弃在队请求是**有意**的:旧请求带的是旧配置下的编辑叠层,
* 复位后还把它落进 prot_set,等于拿陈旧草稿覆盖新装载的定值
*/
void ui_data_init(void) {
(void)memset(&s_rt, 0, sizeof(s_rt));
(void)memset(&s_par, 0, sizeof(s_par));
(void)memset(&s_info, 0, sizeof(s_info));
s_rt_cnt = 0u;
s_par_cnt = 0u;
s_info_cnt = 0u;
(void)memset(s_pq, 0, sizeof(s_pq));
s_pq_head = 0u;
s_pq_cnt = 0u;
s_pq_state = UI_DATA_PARAM_ST_IDLE;
s_pq_err_mask = 0u;
}
/*
* 函数功能:发布一帧实时量快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧(本层整帧拷入自有存储,调用方可复用其栈/静态空间)
* const ui_data_rt_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:唯一发布者 = App/app/ui_data_feed.c,uiTask 上下文
* 函数说明:seq 由本层盖章(入帧 seq 被忽略);其余字段原样入存,本层不做任何
* 业务判断与格式化
*/
int ui_data_publish_rt(const ui_data_rt_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_rt, a_snap, sizeof(s_rt));
s_rt_cnt += 1u;
s_rt.seq = s_rt_cnt;
return 0;
}
/*
* 函数功能:发布一帧参数快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_par_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上;发布节拍 = 定值校验和变化才发
* (docs/UI设计/UI_数据供给设计说明.md §5.2)
*/
int ui_data_publish_par(const ui_data_par_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_par, a_snap, sizeof(s_par));
s_par_cnt += 1u;
s_par.seq = s_par_cnt;
return 0;
}
/*
* 函数功能:发布一帧装置信息快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_info_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上
*/
int ui_data_publish_info(const ui_data_info_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_info, a_snap, sizeof(s_info));
s_info_cnt += 1u;
s_info.seq = s_info_cnt;
return 0;
}
/*
* 函数功能:读最新一帧实时量快照(整帧拷给页面私有缓存)
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_rt_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动,页面显示 "--");
* -2 - 参数无效(NULL) int
* 限定条件:页面钩子上下文(uiTask);勿在逐行 refresh 里无条件整帧拷贝,
* 先比 ui_data_rt_seq()(用法见 Drivers/BSP/UI/ui_data.h 文件头)
*/
int ui_data_rt(ui_data_rt_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_rt_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_rt, sizeof(s_rt));
return 0;
}
/*
* 函数功能:读最新一帧参数快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_par_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上;定值页同样用 ui_data_par_seq() 门控页面私有缓存
*/
int ui_data_par(ui_data_par_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_par_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_par, sizeof(s_par));
return 0;
}
/*
* 函数功能:读最新一帧装置信息快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_info_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上
*/
int ui_data_info(ui_data_info_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_info_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_info, sizeof(s_info));
return 0;
}
/*
* 函数功能:查实时帧发布序号(页面缓存失效判据)
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:轻量查询——只读一个 uint32,可逐行调用;序号只在发布时 +1,页面
* 据此把"整帧拷贝"从每帧每行降到只在换帧时一次
*/
uint32_t ui_data_rt_seq(void) {
return s_rt.seq;
}
/*
* 函数功能:查参数帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_par_seq(void) {
return s_par.seq;
}
/*
* 函数功能:查装置信息帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_info_seq(void) {
return s_info.seq;
}
/*
* 函数功能:投递一条定值写入请求(页面"保存"行的唯一出口——只投递,不执行)
* 入口参数:a_req - 请求(本层整条拷入环形队列,调用方可复用其栈/叠层空间)
* const ui_data_param_req_t *
* 返回值:0 - 已入队(状态转 WAIT);或 mask 为 0 的空请求(无改动可保存,
* 状态直接转 OK,不占槽位)
* -1 - 队列满(状态转 ERR_BUSY,在队请求一条不动);
* -2 - 参数无效:NULL 或 mask 含 UI_DATA_SET_N 以上的位
* (状态与队列都不动——把调用方的编程错误落成屏上"错误"会误导现场) int
* 限定条件:uiTask 上下文(页面按键钩子);与 drain 同上下文串行 → 无锁
* 函数说明:本层不校验值域(越界与否属执行侧事实,落在 drain 的整批预检),
* 不 include App 头、不碰 prot_set、不碰 FatFs(UI 直写定值会把校验和
* 打成中间态、直写盘会撞 defaultTask 单写者纪律——契约 §6.1)
* 空请求按成功处理是刻意的:保存行 ENTER 时"一个都没改"是正常操作,
* 报 ERR 会让用户以为设备有问题
*/
int ui_data_param_request(const ui_data_param_req_t *a_req) {
uint8_t slot;
if (a_req == NULL) {
return -2;
}
if ((a_req->mask & ~UI_DATA_PARAM_MASK_VALID) != 0u) {
return -2;
}
if (a_req->mask == 0u) {
s_pq_state = UI_DATA_PARAM_ST_OK;
s_pq_err_mask = 0u;
return 0;
}
if (s_pq_cnt >= (uint8_t)UI_DATA_PARAM_QD) {
s_pq_state = UI_DATA_PARAM_ST_ERR_BUSY;
return -1;
}
slot = (uint8_t)(((uint32_t)s_pq_head + (uint32_t)s_pq_cnt) % UI_DATA_PARAM_QD);
(void)memcpy(&s_pq[slot], a_req, sizeof(s_pq[slot]));
s_pq_cnt += 1u;
s_pq_state = UI_DATA_PARAM_ST_WAIT;
return 0;
}
/*
* 函数功能:取写路径状态(页面保存行显示)
* 入口参数:无
* 返回值:UI_DATA_PARAM_ST_IDLE / _WAIT / _OK / _ERR_RANGE / _ERR_LOCKED /
* _ERR_BUSY 之一 int
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:本函数就是 s_pq_state 的直读,不做"pending 优先"之类的二次加工——
* 三条写路径(投递成功→WAIT、投递被拒→ERR_BUSY、drain 落定→终态或
* 残留则 WAIT)已把顺序语义收在写侧,读侧再判一次等于两处解释同一件事
*/
int ui_data_param_state(void) {
return s_pq_state;
}
/*
* 函数功能:取最近一条请求的越界项位图(页面高亮越界行)
* 入口参数:无
* 返回值:bit i = 1 → 第 i 项越界;最近终态不是 ERR_RANGE 时恒 0 uint64_t
* 限定条件:无(只读单字)
* 函数说明:与 ui_data_par_t.bad_mask 两条来源不同(本函数 = 本次提交被预检挡下
* 的项;bad_mask = prot_set 整表装载/解析留下的越界位图),页面在
* ERR_RANGE 那一帧用本函数,混用会把上一次装载的越界显示成本次的
*/
uint64_t ui_data_param_err_mask(void) {
return s_pq_err_mask;
}
/*
* 函数功能:整批预检一条请求(位图内置位的项逐个问执行侧 check())
* 入口参数:a_ops - 执行侧函数表(调用方已核非 NULL)
* a_req - 待检请求
* 返回值:越界项位图(bit i = 第 i 项越界;0 = 本批全部在界内) uint64_t
* 限定条件:只调 check(),不写任何东西("预检"与"落值"分成两个函数,
* 正是为了让 all-or-nothing 有地方落脚:整批扫完才决定写不写)
* 函数说明:一次扫全 40 位而不是"遇到第一个越界就返回"——err_mask 要给页面
* **全部**越界行反显,只报第一行等于让用户改完一行再撞下一行
*/
static uint64_t ui_data_param_scan(const ui_data_param_ops_t *a_ops,
const ui_data_param_req_t *a_req) {
uint64_t bad = 0u;
uint64_t bit = 1u;
uint8_t i;
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
if ((a_req->mask & bit) != 0u) {
if (a_ops->check(i, a_req->q10[i]) != 0) {
bad |= bit;
}
}
bit <<= 1u;
}
return bad;
}
/*
* 函数功能:逐项落值一条请求(调用前 ui_data_param_scan() 已确认整批在界内)
* 入口参数:a_ops - 执行侧函数表
* a_req - 待写请求
* 返回值:0 - 位图内全部写入;-1 - 某项被执行侧拒绝(*p_bad 输出该项) int
* 限定条件:apply() 的副作用由执行侧定义(App 实现 = prot_set_set_sealed,
* 每项自带置脏);本函数不回滚已写项——prot_set 没有回滚接口
* 函数说明:走到这里还失败 = 执行侧与预检不一致(两个真源漂移),理论不可达;
* 如实报失败项比谎称"整批未写"更诚实,故**可能已有前几项生效**
*/
static int ui_data_param_write(const ui_data_param_ops_t *a_ops,
const ui_data_param_req_t *a_req,
uint64_t *p_bad) {
uint64_t bit = 1u;
uint8_t i;
for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) {
if ((a_req->mask & bit) != 0u) {
if (a_ops->apply(i, a_req->q10[i]) != 0) {
*p_bad = bit;
return -1;
}
}
bit <<= 1u;
}
*p_bad = 0u;
return 0;
}
/*
* 函数功能:处理一条定值写入请求(App 装配层节拍入口:出队 → 闭锁 → 预检 → 落定)
* 入口参数:a_ops - 执行侧函数表(locked/check/apply 三者都不得为 NULL)
* const ui_data_param_ops_t *
* 返回值:1 - 处理并提交了一条(状态已转终态,或队列仍有残留则转 WAIT);
* 0 - 队列空(状态与队列都不动);
* -1 - 参数无效(a_ops 或其成员为 NULL,状态与队列都不动) int
* 限定条件:uiTask 上下文(App/app/ui_data_feed.c 的 tick 每轮一次);每轮最多
* 处理一条——整定是低频人工事件,一轮一条已够,且把最坏耗时钉在
* 2 × 40 次回调(预检一遍 + 写入一遍),不随队列堆积抖动
* 函数说明:四步顺序即判据(契约 §6.2),判定本体拆成 scan/write 两个静态函数
* (见其函数说明)——
* ① 取队首;② locked() → ERR_LOCKED 并**丢弃本条**(闭锁是业务态,不做
* "排队等解锁",否则屏上 WAIT 无限期挂着,用户以为设备卡死);
* ③ scan() 任一项越界 → ERR_RANGE 且**一字不写**(all-or-nothing,与
* prot_set_load_raw 同纪律:半张表落进值区就是"看着保存成功、其实混着
* 新旧两套定值");④ write() 全成 → OK。
* ⚠ 顺序不可调:先判闭锁再判越界——闭锁态下 check() 的语义仍成立,但
* 页面要的是"定值闭锁"这个更高优先级的原因(契约 §6.3 表序)
*/
int ui_data_param_drain(const ui_data_param_ops_t *a_ops) {
const ui_data_param_req_t *p_req;
uint64_t bad;
int settled;
if ((a_ops == NULL) || (a_ops->locked == NULL) ||
(a_ops->check == NULL) || (a_ops->apply == NULL)) {
return -1;
}
if (s_pq_cnt == 0u) {
return 0;
}
p_req = &s_pq[s_pq_head];
bad = 0u;
if (a_ops->locked() != 0u) {
settled = UI_DATA_PARAM_ST_ERR_LOCKED;
} else {
settled = UI_DATA_PARAM_ST_OK;
bad = ui_data_param_scan(a_ops, p_req);
if (bad != 0u) {
settled = UI_DATA_PARAM_ST_ERR_RANGE; /* 预检挡下:一字未写 */
} else {
if (ui_data_param_write(a_ops, p_req, &bad) != 0) {
settled = UI_DATA_PARAM_ST_ERR_RANGE; /* 执行侧拒写:见函数说明 */
}
/* 两关都过 → settled 保持 OK */
}
}
s_pq_err_mask = bad;
s_pq_head = (uint8_t)(((uint32_t)s_pq_head + 1u) % UI_DATA_PARAM_QD);
s_pq_cnt -= 1u;
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;
}