d64bbdc57f
数据面(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 条仍全过
1174 lines
50 KiB
C
1174 lines
50 KiB
C
/*
|
||
* 模块名称: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);
|
||
}
|