/* * 模块名称: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.x 2026-10-27 DTU 项目组 道G task-9:通道六通讯参数转发实现。 * 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 #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; } /* * 函数功能:请求设置墙钟(时钟设置页"保存"行的唯一出口) * 入口参数: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; } /* ── 通道五:遥测整定注入(死区 / 系数)──────────────────────────────────── */ /* * 注入的遥测整定执行侧函数表(NULL = 未注入)。 * 与 s_maint 同一条生命周期约定:注入方(App/app/ui_data_feed.c 与 * simulator/main.c)给的都文件级 static const 表,本层只存指针不拷内容; * 撤销窗口只在宿主测试里存在。 * * ⚠ 本层**不持有任何遥测整定的值**:九条死区与八路系数的真源在 * App/algo/run_par.c 与 App/algo/adc_cal.c,落盘动力在 defaultTask 的每轮 * 无条件 run_par_store_save()(首行 if (s_dirty == 0u) return 0;,道F task-7 * 查实结论:落盘链不靠 104 会话、也不靠事件位)。本层若自己存一份,就正是 * 卡片判据"不新增第二份真源"要拦的那件事。页面能问的只有两样:现状(*_row) * 与"我这次动作落没落"(pending)。 */ static const ui_data_yt_ops_t *s_yt; /* * 函数功能:注入遥测整定执行侧函数表(启动期一次) * 入口参数:ops - 函数表;八个成员指针都不得为 NULL;NULL = 撤销注入 * 返回值:0 - 已注入(或已撤销);-1 - ops 非空但某成员为 NULL(存量不动) int * 限定条件:uiTask 启动期,先于 ui_menu_init();本层无锁,与快照层同一并发前提 * 函数说明:整表校验的理由与 ui_data_maint_bind() 逐字相同——半途接错一条会让 * 页面在"按下才崩"的位置崩,装配期就拒掉残表才是省事。残表被拒后**存量 * 不动**(TC-UIDATA-027 钉住这条:好表在用时被残表覆盖会静默把已通的口 * 打断) */ int ui_data_yt_bind(const ui_data_yt_ops_t *a_ops) { if (a_ops == NULL) { s_yt = NULL; /* 撤销注入:页面随即按"整定能力不可用"渲染 */ return 0; } if ((a_ops->db_n == NULL) || (a_ops->db_row == NULL) || (a_ops->db_set == NULL) || (a_ops->cal_n == NULL) || (a_ops->cal_row == NULL) || (a_ops->cal_k_set == NULL) || (a_ops->cal_blocked == NULL) || (a_ops->pending == NULL)) { return -1; } s_yt = a_ops; return 0; } /* * 函数功能:死区点数 * 返回值:点数;未注入返回 0(页面据此渲染空表,而不是拿编译期常量当"真源说 * 有几行"——行数归执行侧,见 ui_data.h 通道五表注) uint8_t * 限定条件:页面绘制路径可直调(一次函数指针转发,零 I/O、零锁) */ uint8_t ui_data_yt_db_n(void) { if (s_yt == NULL) { return 0u; } return s_yt->db_n(); } /* * 函数功能:读第 idx 行死区现状(工程量 + 粗守域 + 量纲 + 显示小数位) * 入口参数:idx - 0..8(0..7 = 电气语义通道序,8 = 频率) uint8_t * a_out - 行现状 ui_data_yt_row_t * * 返回值:0 - 已写出;-1 - 未注入 / 参数空 / 执行侧无此行(页面这行退化显示, * 不猜、不拿上一行数值顶替) int * 限定条件:页面绘制与按键链内(uiTask) * 函数说明:出参在本层不预清——执行侧契约是"返 0 才整份写",页面返非 0 时 * 不得读该结构,这条口径由 TC-UIDATA-028 钉 */ int ui_data_yt_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out) { if ((s_yt == NULL) || (a_out == NULL)) { return -1; } return s_yt->db_row(a_idx, a_out); } /* * 函数功能:写第 idx 行死区(工程量)——两页唯一的死区写出口 * 返回值:0 - 副本已写且已置脏;-1 - 未注入或执行侧拒(副本未动、不置脏) int * 限定条件:uiTask 按键链内(编辑提交) * 函数说明:未注入与执行侧拒绝**同码返回**是刻意的:两条腿屏上都是"值没变 + * 保存行 ERR",页面不需要、也不该分辨"是没接上还是被拒"——那是装配期 * 故障与工况故障的区分,归日志不归屏幕 */ int ui_data_yt_db_set(uint8_t a_idx, float a_eng) { if (s_yt == NULL) { return -1; } return s_yt->db_set(a_idx, a_eng); } /* 函数功能:系数覆盖点数;未注入返回 0(理由同 ui_data_yt_db_n) uint8_t */ uint8_t ui_data_yt_cal_n(void) { if (s_yt == NULL) { return 0u; } return s_yt->cal_n(); } /* 函数功能:读第 idx 路系数现状;返回值与不猜口径同 ui_data_yt_db_row int */ int ui_data_yt_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out) { if ((s_yt == NULL) || (a_out == NULL)) { return -1; } return s_yt->cal_row(a_idx, a_out); } /* * 函数功能:写第 idx 路系数 k(µV/码)——两页唯一的系数写出口 * 返回值:0 - 已写并置脏;-1 - 未注入或拒(副本未动、不置脏) int * 限定条件:uiTask 按键链内 * 函数说明:粗守(上限 RUN_PAR_K_MAX_UV)与真带(执行段 ×[0.5,2.0])两层守卫 * **互不掩盖**:屏上输入框只能挡误写,真带裁在 adc_cal;本层不加第三层 */ int ui_data_yt_cal_k_set(uint8_t a_idx, float a_uv) { if (s_yt == NULL) { return -1; } return s_yt->cal_k_set(a_idx, a_uv); } /* * 函数功能:未标定闭锁徽标源 * 返回值:1 - 闭锁中;0 - 正常**或未注入** uint8_t * 限定条件:页面绘制路径可直调 * 函数说明:未注入按 0 是"宁可不画"而不是"报个徽标保平安"——徽标是断言, * 没有真源时的断言就是假断言;此时页面上写值本来也全被拒(写口返 -1), * 信息不缺这条 */ uint8_t ui_data_yt_cal_blocked(void) { if (s_yt == NULL) { return 0u; } return s_yt->cal_blocked(); } /* * 函数功能:整档落盘脏位探查(保存行三态的唯一判据源) * 返回值:1 - 有已整定未落盘改动;0 - 干净或未注入 uint8_t * 限定条件:页面绘制路径可直调(每帧一次转发,零 I/O) * 函数说明:本层**不加任何边沿记忆**——"我这次动作的结果"要锁存是页面的事 * (脏位整档全局、他人再改会重新置脏,两条口径见 ui_data.h pending * 成员注);在这里做锁存会把两页的状态耦合成一份 */ uint8_t ui_data_yt_pending(void) { if (s_yt == NULL) { return 0u; } return s_yt->pending(); } /* ============================================================ * 通道六:通讯参数([comm])整定注入(道G task-9,通讯设置页) * * 与通道五同一组存在理由(ui_data.h 通道六表注:分层 / 模拟器 / 原子性); * 本层同样**不持有任何 [comm] 值**——真源在 App/protocol/iec104/iec104_comm.c * 的 RAM 副本,落盘动力在 defaultTask 每轮无条件 iec104_comm_store_save()。 * 未注入语义按"能力不可用"退化(row/set → -1、pending → 0),页面不崩不猜。 * ============================================================ */ static const ui_data_comm_ops_t *s_comm; /* * 函数功能:注入通讯参数执行侧函数表(启动期一次) * 入口参数:ops - 三件齐;NULL = 撤销注入 * 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动,理由逐字同 yt/maint bind) int * 限定条件:uiTask 启动期,先于 ui_menu_init() */ int ui_data_comm_bind(const ui_data_comm_ops_t *a_ops) { if (a_ops == NULL) { s_comm = NULL; return 0; } if ((a_ops->row == NULL) || (a_ops->set == NULL) || (a_ops->pending == NULL)) { return -1; } s_comm = a_ops; return 0; } /* * 函数功能:读第 idx 行通讯参数现状 * 返回值:0 - 已整份写出;-1 - 未注入 / 指针空(出参不预清,返 0 才可读—— * 通道五同一条契约) int */ int ui_data_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out) { if ((s_comm == NULL) || (a_out == NULL)) { return -1; } return s_comm->row(a_idx, a_out); } /* * 函数功能:写第 idx 行通讯参数——通讯设置页唯一写出口 * 返回值:0 - 副本已写且已置脏;-1 - 未注入或带外拒(同码口径同通道五) int */ int ui_data_comm_set(uint8_t a_idx, uint32_t a_val) { if (s_comm == NULL) { return -1; } return s_comm->set(a_idx, a_val); } /* 函数功能:[comm] 段落盘脏位(保存行三态判据;不加边沿记忆,锁存归页面) uint8_t */ uint8_t ui_data_comm_pending(void) { if (s_comm == NULL) { return 0u; } return s_comm->pending(); }