Files
STM32F4-Base/Drivers/BSP/UI/ui_data.c
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(App/algo,纯逻辑,双端编译)
- wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask
  唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃
  兜底——下一次真故障无波可录比丢一段更贵
- wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带
  锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度
- 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式
- 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样
  纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉
- test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条
  自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合
- 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定)

接线(任务层)
- adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图),
  段头按最低有效位精确记账,不经浮点回算
- adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实;
  台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行
- defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到
  关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms
  补一条告警并记历史峰值(喂狗余量可核)
- Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留
  超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的,
  半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进
- Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到
  上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数;
  段文件字节布局零改动,三份合成段哈希逐字节不变
- protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体
  零改动

人机(160×160 单色屏)
- 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器
  得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30
- 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与
  垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、
  单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据
- ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、
  手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口
- 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有
  60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形
- 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变
- 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏
  22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、
  仪器用已在跑的单轮工作耗时探针,不新加埋点

台账与文档
- 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936
  字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名;
  散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建
  重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置)
- 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据);
  §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径
  风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」
- 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项
- 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划
  索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动)
- 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件
  形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记
  「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场
- 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行
  227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退

为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与
体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账
同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道

编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串
生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
2026-10-29 19:23:15 +08:00

1174 lines
50 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.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 <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;
}
/*
* 函数功能:请求设置墙钟(时钟设置页"保存"行的唯一出口)
* 入口参数: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();
}
/* ── 通道六:故障录波(wave)注入与包装 ─────────────────────────────────────
* 包装层零判据:未注入统一按「无录波段」态返(页面显式空态文案,绝不
* 画平线——设计 §8.1 空态行);六件的参数校验只到指针级,语义判定
* (卷忙/段损坏)全在执行侧,与通道四/五同一条分层纪律。
*/
static const ui_data_wave_ops_t *s_wave = NULL;
/*
* 函数功能:注入录波执行侧函数表(启动期一次)
* 入口参数:ops - 六件齐;NULL = 撤销注入
* 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动,理由逐字同 comm bind) int
* 限定条件:uiTask 启动期,先于 ui_menu_init()
*/
int ui_data_wave_bind(const ui_data_wave_ops_t *a_ops) {
if (a_ops == NULL) {
s_wave = NULL;
return 0;
}
if ((a_ops->manual_req == NULL) || (a_ops->seg_first == NULL) ||
(a_ops->seg_prev == NULL) || (a_ops->seg_next == NULL) ||
(a_ops->seg_head == NULL) || (a_ops->seg_win == NULL)) {
return -1;
}
s_wave = a_ops;
return 0;
}
/* 函数功能:投手动录波请求(未注入 = 拒收,页面显示 ERR) bool */
int ui_data_wave_manual_req(void) {
if (s_wave == NULL) {
return -1;
}
return s_wave->manual_req();
}
/* 函数功能:录波通道三态查询的统一未注入兜底(返 NONE 显空态,不崩) int */
static int wp_wave_none(void) {
return UI_WAVE_NONE;
}
/* 函数功能:取数值最大段号(回绕后不保证时间最新——段号自证,见 .h 契约) int */
int ui_data_wave_seg_first(uint8_t *a_idx) {
if ((s_wave == NULL) || (a_idx == NULL)) {
return wp_wave_none();
}
return s_wave->seg_first(a_idx);
}
/* 函数功能:取更早一段段号 int */
int ui_data_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) {
if ((s_wave == NULL) || (a_out == NULL)) {
return wp_wave_none();
}
return s_wave->seg_prev(a_cur, a_out);
}
/* 函数功能:取更新一段段号 int */
int ui_data_wave_seg_next(uint8_t a_cur, uint8_t *a_out) {
if ((s_wave == NULL) || (a_out == NULL)) {
return wp_wave_none();
}
return s_wave->seg_next(a_cur, a_out);
}
/* 函数功能:读段头(执行侧含 CRC 完整性判定) int */
int ui_data_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig,
uint8_t *a_clip) {
if ((s_wave == NULL) || (a_eng == NULL) || (a_trig == NULL) ||
(a_clip == NULL)) {
return wp_wave_none();
}
return s_wave->seg_head(a_idx, a_eng, a_trig, a_clip);
}
/* 函数功能:读显示窗口码值与峰值 mV int */
int ui_data_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes,
int32_t *a_peak_milli) {
if ((s_wave == NULL) || (a_codes == NULL) || (a_peak_milli == NULL) ||
(a_ch >= UI_WAVE_CH_CNT)) {
return wp_wave_none();
}
return s_wave->seg_win(a_idx, a_ch, a_codes, a_peak_milli);
}