Files
STM32F4-Base/Drivers/BSP/UI/ui_data.h
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

1037 lines
61 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)
* 模块功能:App 层 → UI 层唯一数据通道的"存侧"——只持有最新一帧实时量(rt)/
* 参数(par)/装置信息(info) 快照 + 发布序号,给页面只读访问;
* 不取数、不格式化、不判断业务含义(职责边界 =
* docs/UI设计/UI_数据供给设计说明.md §3.2)
* 适用范围:Drivers/BSP/UI(表现层叶子:不 include 任何 App 头、不 include 任何
* 页面头、不碰 LVGL——分层方向由 scripts/check_layer_deps.py 守护,
* BSP → App 反向 include 一律拦提交)
* 并发模型:写侧 = App/app/ui_data_feed.c(唯一发布者);读侧 = ui_page_* 的绘制/
* 按键钩子。两侧同在 uiTask 上下文串行执行 → 无锁、无双缓冲、无 seqlock
* (判据与"将来出现第二发布者"时的扩展路径见
* docs/UI设计/UI_数据供给设计说明.md §5.1)
* 页面用法:行缓存按"发布序号变过才整帧拷"门控,勿逐行整帧拷贝
* (docs/UI设计/UI_数据供给设计说明.md §5.4):
* static ui_data_rt_t s_rt;
* static uint32_t s_seq;
* if (ui_data_rt_seq() != s_seq) {
* (void)ui_data_rt(&s_rt);
* s_seq = ui_data_rt_seq();
* }
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.x 2026-10-27 DTU 项目组 道G task-9:通道六通讯参数注入表
* (ui_data_comm_ops_t,通讯设置页消费)。
* v1.0 2026-10-25 王建锋 创建(道E task-14:契约 §4 快照层落地;相对契约
* 两通道新增第三条 info,划分理由见本头"通道划分"注
* 与契约 §4.4)
* v1.1 2026-10-26 DTU 项目组 道F task-1(docs/任务规划/UI补齐.md §2 第 1 行):
* 新增定值写路径(param_request/param_state/
* param_drain,契约 §6.2 深度 4 环形队列)+ 参数帧尾
* 追加 cal_block(UI_DATA_VER 1→2,整定页"未标定"徽标
* 的数据源);写路径仍不引入任何 App 依赖(执行事实
* 经 ui_data_param_ops_t 由 App 侧注入,同契约 §7.2
* 记录页注入式 reader 的路子)
* v1.2 2026-10-26 DTU 项目组 道F task-3(docs/任务规划/UI补齐.md §2 第 3 行
* 「装置维护 3 页」的前置):新增通道四——维护写请求
* (ui_data_maint_*:墙钟设置 / 强制复归 / 清除记录,
* 常驻注入 ui_data_maint_ops_t)与时钟字段换算
* (ui_data_clock_*,本地域无时区偏移,兼作"无效值
* 拒收"的唯一判据处)。三通道快照结构未变 ⇒
* UI_DATA_VER 仍为 2。仍然零 App include:维护动作的
* 真源全在 App 侧,本层只转达请求与结果(同 §7.2);
* ⚠ 页面也不得直调 sys_clock_*——模拟器没链 sys_clock,
* 那种写法只在模拟器链接期才炸,理由见通道四表注
*/
#ifndef UI_DATA_H
#define UI_DATA_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/*
* 契约版本:结构体字段只允许尾部追加,追加即升此值并在写侧填默认值
* (禁止删除或重排字段——页面按名字取,重排是静默错位;
* 口径 docs/UI设计/UI_数据供给设计说明.md §4.3)
*/
/* v2(2026-10-26,道F task-1):ui_data_par_t 尾追加 cal_block——版本号是页面与
* 写侧唯一的对齐凭据,追加字段不升版 = 老页面按新帧长 memcpy 时静默错位 */
#define UI_DATA_VER 2u
/* ── 通道数量 ─────────────────────────────────────────────────────────────── */
/* 遥测语义通道数:下标序 = adc_semantic_t(UA/UB/UC/UN/IA/IB/IC/IN/TEMP)。
* BSP 侧不得 include App 头,故"两数相等"的编译期守卫落在写侧
* App/app/ui_data_feed.c,语义逐项对齐的断言落在 test/host/ui_data/
* (docs/UI设计/UI_数据供给设计说明.md §9.4 一致性测试) */
#define UI_DATA_YC_N 9u
/* 遥信点数:与 104 点表同序(bit i = 第 i 点),页面名表按点表逐行登记 */
#define UI_DATA_YX_N 33u
/* 定值项数:与 prot_set 枚举同序(校验和覆盖序),四视图划分不改序 */
#define UI_DATA_SET_N 40u
/* 相量实例槽上限:谐波(Ia 二次/五次)不能塞进"语义通道"下标空间——同一通道
* 的二次/五次谐波会与基波撞位,故实时帧另开一条按**相量实例序**索引的槽位,
* 供交流量页显示 Ia2/Ia5;实际发布槽数由 ph_n 给出,超出上限的实例本帧不发布
* (写侧口径见 App/app/ui_data_feed.c 的 fill_rt 函数)。上限取 8 = 现用 5 实例
* (IA/UA/UN 基波 + IA 二次/五次)+ 遥测谐波轨留位,每槽 8 B */
#define UI_DATA_PH_MAX 8u
/* ── 量纲口径 eng(诚实性字段:数值 val 的单位由它决定)───────────────────── */
/*
* 真值语义(现场口径 = docs/任务规划/数据可信.md §2.6 的承诺边界 + §4.2 三态
* 可用性;契约文档原先写的"0 = 原始码值(未标定)"是标定落地前的旧口径,
* 已按本表更正,勿再照抄旧表述):
* ENG_PIN 引脚电压 V —— 现状:adc_cal 的 ROM 默认系数是「数据可信」量程口径冻结定死
* 的引脚侧 LSB(TPAFE 152.59 uV/码、STM32 805.66 uV/码),采样环
* 折算出来的就是 ADC 引脚电压,不是端子二次值
* ENG_SEC 二次值 —— 电气 8 路 adc_cal_flags() 的 VALID 全置位(拿到端子变比
* 并完成两点标定)后由写侧升档,页面自动按二次值显示,零改动
* ENG_PRI 一次值 —— 预留(再乘 PT/CT 变比),当前写侧不会产出
*/
#define UI_DATA_ENG_PIN 0u
#define UI_DATA_ENG_SEC 1u
#define UI_DATA_ENG_PRI 2u
/* ── 品质位(与 104 QDS 语义对齐;页面只消费自己关心的位)─────────────────── */
#define UI_DATA_Q_IV 0x01u /* 无效:该通道无源/不可用 → 页面显示 "--" */
#define UI_DATA_Q_NT 0x02u /* 未就绪:尚未攒满一周波/未收敛 */
#define UI_DATA_Q_OV 0x04u /* 越限:超过量程/硬件削顶(V0.1 STM32 侧单极性) */
#define UI_DATA_Q_SB 0x08u /* 取代值(虚拟注入/替身值,页面应带角标) */
#define UI_DATA_Q_BL 0x10u /* 闭锁:该量被业务闭锁(未标定闭锁/采样闭锁) */
/* 原始码值域:val 不是工程量而是转换器码值——TEMP 不参与线性标定
* (docs/任务规划/数据可信.md §4.2 的"TEMP 透传码值"决策),有此位即页面
* 不得给 val 挂 V/A 单位,也不得与工程量互比 */
#define UI_DATA_Q_RAW 0x20u
/* ── 字符串容量(结构体内定长数组,跨层零指针;容量宏本地化)──────────────── */
#define UI_DATA_CLOCK_LEN 20u /* "YYYY-MM-DD HH:MM:SS" 19 + NUL */
#define UI_DATA_MODEL_LEN 8u /* 终端型号("F30") */
#define UI_DATA_VERSTR_LEN 16u /* 软件/硬件版本串 */
#define UI_DATA_DATE_LEN 12u /* 程序日期(编译期 __DATE__,留 NUL 余量) */
#define UI_DATA_SUM_LEN 8u /* 软件校验(无构建期 CRC 步骤时如实为 "--") */
#define UI_DATA_FUSER_LEN 16u /* FTP 用户名;与 Drivers/BSP/NETCFG/ftp_cfg.h 的
* FTP_CFG_USER_MAX 同宽,写侧受该上限约束 */
/* ── 通道一:实时量(写侧节拍 200 ms)────────────────────────────────────── */
/* 单路遥测:数值 + 品质 */
typedef struct {
float val; /* 数值;单位口径由 ui_data_rt_t.eng 决定,RAW 位另说 */
uint8_t qds; /* 品质位组合(UI_DATA_Q_*) */
uint8_t rsv[3]; /* 对齐填充(显式命名,勿留匿名空洞) */
} ui_data_yc_t;
/* 实时快照:遥测 9 路 + 遥信位图 + 频率 + 墙钟 */
typedef struct {
uint32_t seq; /* 发布序号(本层盖章,单调递增) */
uint32_t tick_ms; /* 发布时刻(tick 域,差值法可比) */
uint8_t valid; /* 0 = 尚无首帧(页面全显 "--") */
uint8_t eng; /* 量纲口径(UI_DATA_ENG_*) */
uint8_t rsv[2]; /* 预留(对齐填充) */
ui_data_yc_t yc[UI_DATA_YC_N]; /* 下标 = adc_semantic_t 语义序(基波有效值) */
uint8_t ph_n; /* 本帧发布的相量实例槽数(0..UI_DATA_PH_MAX) */
uint8_t rsv4[3]; /* 预留(对齐填充) */
ui_data_yc_t ph[UI_DATA_PH_MAX]; /* 下标 = 相量实例序(含谐波),见 UI_DATA_PH_MAX 注 */
uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */
uint64_t yx_valid; /* 遥信有效位图: bit i = 1 该点有硬件来源 */
uint64_t yx_dp; /* 遥信双点位图: bit i = 1 该点是双点型(M_DP)
* 值列打"合位/分位",单点打"动作/复归"。
* 为什么下发位图而不让页面自己认定"前 4 点是
* 双点":单/双点是**104 点表事实**(见
* App/protocol/iec104/iec104_pts.c 的 YX_DP_N 与
* iec104_pts_yx_is_dp),抄一份进表现层就是第二
* 真相——点表把某点改成双点、或把双点段挪到下标
* 中段,屏上不会报错,只会用错词形容同一个点。
* V0.1 双点 4 点无开入硬件 → 不在 yx_valid 内,
* 值列仍是 "--"(本字段不参与无效判定) */
uint8_t freq_valid; /* 1 = 测频读数新鲜(幅值门 OK 且已收敛) */
uint8_t rsv2[3]; /* 预留(对齐填充) */
float freq_hz; /* 测得频率(Hz);freq_valid=0 时不可消费 */
uint8_t clock_ok; /* 0 = 墙钟无效(RTC 异常/未对时) */
uint8_t rsv3[3]; /* 预留(对齐填充) */
char clock[UI_DATA_CLOCK_LEN]; /* "YYYY-MM-DD HH:MM:SS";无效时空串 */
} ui_data_rt_t;
/* ── 通道二:参数(写侧节拍 = 定值校验和变化才发)────────────────────────── */
/*
* 参数快照:40 项定值 Q10 原值 + 封存态
* 存 Q10 而非 float 的理由:定值是整表校验和覆盖的定点值,快照存浮点会引入
* "显示值与校验和口径不一致"的二义性(Q10 → 显示小数位换算属表现层,见
* docs/UI设计/UI_数据供给设计说明.md §8)
*/
typedef struct {
uint32_t seq; /* 发布序号(本层盖章) */
uint32_t checksum; /* 与 prot_set_checksum() 同值:页面据此判"参数变过" */
uint8_t valid; /* 0 = 尚无首帧 */
uint8_t locked; /* 1 = 定值闭锁(校验和自检失败等)→ 页面禁编辑 */
uint8_t bad_valid; /* 1 = bad_mask 有意义(整表装载/提交失败之后) */
uint8_t rsv; /* 预留 */
uint64_t bad_mask; /* 40 位:置 1 = 该定值项越界(页面反显该行) */
int32_t q10[UI_DATA_SET_N]; /* Q10 原值,下标 = prot_set_id_t 枚举序 */
/* v2 尾追加:未标定闭锁(整定页"未标定"徽标的数据源)。
* 1 = UA/UN/IA 三路里至少一路的标定 VALID 未置位 → 保护判据当前停走,页面必须
* 让用户看见"改了定值也不会动作";0 = 三路全 VALID(判据在跑)。
* ⚠ 口径**不是** rt.eng:eng 是整帧量纲档(要求电气 8 路全 VALID),V0.1 双
* 转换器下 UB/UC/IB/IC/IN 永不置位 → eng 恒为引脚档,照 eng 画徽标就成了
* "标定做完屏上仍永不解锁"。三路口径的权威实现 = App/task/protect_task.c
* 的 s_cal_locked()(写侧 App/app/ui_data_feed.c 按同一条判据现算,本层只存
* 真值、不判断)。与上面 locked(定值校验和自检闭锁 = 禁编辑)是两件事,
* 屏上不得互认(三词三源见 docs/任务规划/数据可信.md §4.3) */
uint8_t cal_block;
} ui_data_par_t;
/* ── 通道三:装置信息(静态身份 + 通讯参数 + 运行时长)───────────────────── */
/*
* 装置信息快照(通道划分理由见本头文件头,契约侧同一条登记在
* docs/UI设计/UI_数据供给设计说明.md §4.4——同一事实只在一处详述)
* 写侧随 rt 同节拍发布(内容基本不变,代价是一次约 90 B 拷贝)
*/
typedef struct {
uint32_t seq; /* 发布序号(本层盖章) */
uint8_t valid; /* 0 = 尚无首帧 */
uint8_t rsv[3]; /* 预留(对齐填充) */
uint32_t uptime_s; /* 开机运行时长(秒;写侧按 tick 差算) */
uint16_t port_104; /* IEC 60870-5-104 上行监听端口 */
uint16_t port_disp; /* 远程显示(RemoDispBus)端口 */
char model[UI_DATA_MODEL_LEN]; /* 终端型号 */
char sw_ver[UI_DATA_VERSTR_LEN]; /* 软件版本 */
char hw_ver[UI_DATA_VERSTR_LEN]; /* 硬件版本 */
char build_date[UI_DATA_DATE_LEN]; /* 程序日期(编译期) */
char sw_sum[UI_DATA_SUM_LEN]; /* 软件校验;无构建期 CRC 步骤 = "--" */
char ftp_user[UI_DATA_FUSER_LEN]; /* FTP 登录用户名(只显名,不显口令) */
/* ── 运行报告四数(2026-10-28 UI 补齐「运行报告」;BSP 侧只搬运、不算第二份,
* 口径唯一出处 = App/util/boot_stats.h 的四数台账与 App/task/iec104_serv.h
* 的观测计数注;屏上文案(含"禁写终身次数"这类口径)归 UI 页面侧
* <Drivers/BSP/UI/ui_page_runreport.c>,分层方向由 layer-deps 门禁守护 ── */
uint32_t boot_count; /* 上电次数(掉电保持,自 RTC 电池周期起);
boot_seq_state == 0(UNKNOWN)时不可信 */
uint8_t boot_seq_state; /* 0 未知 / 1 首次启用 / 2 延续 / 3 电池域掉过重开 */
uint8_t boot_write_failed; /* 1 = 本次自增没写回 RTC SRAM(账本只会少不会重) */
uint8_t rst_cause; /* 本次开机复位原因编码(BOOT_STATS_CAUSE_*) */
uint8_t boot_ready; /* 0 = 开机账本未初始化 → 整组按占位显示,不得读成 0 */
uint32_t iec104_conn_up; /* 主站接入次数(本次开机以来,跨重连不清零) */
uint32_t iec104_apdu_tx; /* 成功发出的 APDU 帧数(短写不算,落在 tx_fail) */
uint32_t iec104_tx_fail; /* 发送短写/失败次数 */
uint32_t iec104_rx_bytes; /* 收到的**字节**数(帧边界在 link 层,serv 看不见帧) */
} ui_data_info_t;
/* ── 初始化 ───────────────────────────────────────────────────────────────── */
/*
* 函数功能:快照层初始化——三通道清零(valid=0、seq=0),页面即按"无首帧"渲染
* 入口参数:无
* 返回值:无
* 限定条件:内核启动前可调(纯内存操作,零 OS 依赖);可重复调用(幂等)
* 函数说明:静态存储上电本已清零,本函数的意义是运行期复位(例如"恢复出厂后
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态
*/
void ui_data_init(void);
/* ── 发布(写侧唯一入口;单写者同上下文,无锁)───────────────────────────── */
/*
* 函数功能:发布一帧实时量快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧(本层整帧拷入自有存储,调用方可复用其栈/静态空间)
* const ui_data_rt_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:唯一发布者 = App/app/ui_data_feed.c,uiTask 上下文
* 函数说明:seq 由本层盖章(写侧不必填);其余字段原样入存,本层不做任何业务
* 判断与格式化
*/
int ui_data_publish_rt(const ui_data_rt_t *a_snap);
/*
* 函数功能:发布一帧参数快照(覆盖存量并给 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);
/*
* 函数功能:发布一帧装置信息快照(覆盖存量并给 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);
/* ── 只读访问(页面侧)───────────────────────────────────────────────────── */
/*
* 函数功能:读最新一帧实时量快照(整帧拷给页面私有缓存)
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_rt_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动,页面显示 "--");
* -2 - 参数无效(NULL) int
* 限定条件:页面钩子上下文(uiTask);勿在逐行 refresh 里无条件整帧拷贝,
* 先比 ui_data_rt_seq()(用法见本头文件头)
*/
int ui_data_rt(ui_data_rt_t *a_out);
/*
* 函数功能:读最新一帧参数快照
* 入口参数: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);
/*
* 函数功能:读最新一帧装置信息快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_info_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上
*/
int ui_data_info(ui_data_info_t *a_out);
/*
* 函数功能:查实时帧发布序号(页面缓存失效判据)
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:轻量查询——只读一个 uint32,可逐行调用(契约 §5.4 的门控就靠它)
*/
uint32_t ui_data_rt_seq(void);
/*
* 函数功能:查参数帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_par_seq(void);
/*
* 函数功能:查装置信息帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_info_seq(void);
/* ── 写路径:定值整定请求(页面只投递,执行在 App 侧)───────────────────── */
/*
* 请求队列深度(契约 docs/UI设计/UI_数据供给设计说明.md §6.2 定死 4)
* 为什么是 4 不是 1:整定页"保存"是用户按键,一条请求要等 App 侧 drain 才落定;
* 深度 1 会让连按两次保存把第二条判成"忙",而 4 格 = 本层 .bss 多
* 4 × sizeof(ui_data_param_req_t)(168 B/格,主机单测 TC-UIDATA-016 钉住尺寸,
* 内存台账 docs/内存管理/RAM使用分析与优化方案.md)。加深 = 纯 RAM 代价,
* 本层不做任何按内存自适应
*/
#define UI_DATA_PARAM_QD 4u
/*
* 一条定值写入请求 = "本次保存改了哪些项 + 它们的 Q10 新值"
*
* 为什么是「位图 + 定长 40 槽」而不是「项数 + 变长 id 数组」:表现层编辑叠层本就
* 是一张改动位图(uint64_t)+ 一张工作值表(int32_t[UI_DATA_SET_N]),本形状与它
* 逐位同构 → 提交侧一次 memcpy、执行侧逐项按位取,两侧都不必再维护第三种表示;
* 定长还让队列退化成 O(1) 静态环形数组(零堆、零变长拷贝、零 malloc 门禁风险)。
*
* ⚠ mask 只有 64 位 → 定值项数硬上限 64;越界位由 ui_data.c 的编译期守卫拦住,
* 将来项数破 64 必须换表示(不会静默丢项)。
* ⚠ **mask 未置位的槽本次不提交**:本层与执行侧都不读它——调用方可以只填 mask 内
* 的槽(整帧 memcpy 入队会把 mask 外的字节一并存下来,但那字节永不被读,不影响
* 任何判定;先 memset 再填也完全可以,两种写法都安全)。
* 绝不允许"从显示值回提全表"——Q10 存不下 0.95 这类返回系数(真存 972/1024),
* 按显示域往返会把用户没动的项顺手改掉
* (docs/任务规划/UI补齐.md §2 第 2 行判据"值不落盘"的前提)。
*/
typedef struct {
uint64_t mask; /* bit i = 1 → 第 i 项随本请求提交 */
int32_t q10[UI_DATA_SET_N]; /* Q10 新值;仅 mask 置位的槽有意义 */
} ui_data_param_req_t;
/*
* 写路径状态(页面保存行显示;语义表 = 契约 §6.3)
* IDLE - 从未投递 → 页面显 ENTER
* WAIT - 已投递未落定(队列里还有请求在路上)→ 页面显 WAIT
* OK - 最近一条已逐项写入并置脏 → 页面显 OK
* ERR_RANGE - 最近一条整批拒绝:任一项越界 → 一字未写 → 页面显 ERR 并高亮
* 越界行(越界项经 ui_data_param_err_mask() 取)
* ERR_LOCKED - 最近一条被定值闭锁拒(写侧自检失败态)→ 页面显 LOCK 并禁编辑
* ERR_BUSY - 最近一次投递被拒:队列满 → 页面显 ERR、页脚"忙,请重试"
* 状态变量的三条写入路径(本层唯二写者 = request 与 drain,同在 uiTask):
* 投递成功 → WAIT;投递被拒(满)→ ERR_BUSY;drain 落定 → 终态,但队列仍有
* 残留时改写回 WAIT。故"满被拒"看得见(ERR_BUSY),随后逐步 drain 又回到 WAIT
*/
#define UI_DATA_PARAM_ST_IDLE 0
#define UI_DATA_PARAM_ST_WAIT 1
#define UI_DATA_PARAM_ST_OK 2
#define UI_DATA_PARAM_ST_ERR_RANGE 3
#define UI_DATA_PARAM_ST_ERR_LOCKED 4
#define UI_DATA_PARAM_ST_ERR_BUSY 5
/*
* 执行侧函数表(由 App 在 ui_data_feed_tick() 每次调用时传入;本层不保存副本)
*
* 为什么用注入而不是直接调 prot_set:分层方向硬约束——Drivers/BSP 不得 include
* App 头(scripts/check_layer_deps.py 拦提交),而"什么算越界、写了要不要置脏"
* 全在 App/algo/prot_set 那侧。同契约 §7.2 记录页的注入式 reader:BSP 声明抽象
* 函数表,App 给实现,主机单测给桩 → 队列与状态机的**生产代码**能在 PC 上跑
* 全表(越界不改值 / 闭锁拒写 / 正常往返三条判据都覆盖到真代码,不是副本)。
* ⚠ 本表不含任何"范围数值"——上下限真源仍是 App/algo/prot_set.c 的
* g_prot_set_meta,本层只按 check() 的返回值决定整批写不写(不新增第二真源)
*/
typedef struct {
uint8_t (*locked)(void); /* 1 = 定值闭锁中 */
int (*check)(uint8_t a_id, int32_t a_q10); /* 0 合法 / -1 越界 */
int (*apply)(uint8_t a_id, int32_t a_q10); /* 0 成功 / -1 失败 */
} ui_data_param_ops_t;
/*
* 函数功能:投递一条定值写入请求(页面"保存"行的唯一出口——只投递,不执行)
* 入口参数:a_req - 请求(mask 与 q10 整条拷入队列,调用方可复用其栈空间)
* const ui_data_param_req_t *
* 返回值:0 - 已入队(状态转 WAIT);mask 为 0 的空请求也返回 0,但按"无改动可
* 保存"直接落终态 OK 且**不占槽位**(保存行 ENTER 时一个都没改
* 是正常操作,报 ERR 会让现场以为设备有问题);
* -1 - 队列满(状态转 ERR_BUSY,在队请求一条不动);
* -2 - 参数无效:NULL、或 mask 含 UI_DATA_SET_N 以上的位
* (状态与队列都不动:调用方的编程错误不该落成屏上的"越界") int
* 限定条件:uiTask 上下文(页面按键钩子);与 drain 同上下文 → 无锁
* 函数说明:本函数**不**校验值域(越界与否是执行侧事实,在 drain 里整批预检),
* 也不碰 prot_set、不碰文件系统(落盘归 defaultTask,AGENTS 模块约定)
*/
int ui_data_param_request(const ui_data_param_req_t *a_req);
/*
* 函数功能:取写路径状态(页面保存行显示)
* 入口参数:无
* 返回值:UI_DATA_PARAM_ST_* 之一 int
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:ERR_TIMEOUT(契约 §6.3 的"3 s 未取走")本层**不产出**——uiTask 每
* 10 ms 就 drain 一次,真出现"永不落定"说明 App 侧没接线,属装配缺陷;
* 页面若要兜底可自己按投递时刻计时(docs/UI设计/UI_数据供给设计说明.md
* §6.3 已把它列为页面本地可选项)
*/
int ui_data_param_state(void);
/*
* 函数功能:取最近一条请求的越界项位图(页面高亮越界行)
* 入口参数:无
* 返回值:bit i = 1 → 第 i 项越界;最近终态不是 ERR_RANGE 时恒 0 uint64_t
* 限定条件:无(只读单字)
* 函数说明:与 ui_data_par_t.bad_mask 是两条不同来源——bad_mask 是 prot_set 整表
* 装载/解析留下的越界位图(App 侧事实),本函数是**本次提交**里被预检
* 挡下的项;页面在 ERR_RANGE 那一帧用本函数,别把两者混成一份
*/
uint64_t ui_data_param_err_mask(void);
/*
* 函数功能:处理一条定值写入请求(App 装配层节拍入口;出队 → 判定 → 落定)
* 入口参数:a_ops - 执行侧函数表(三个函数指针都不得为 NULL)
* const ui_data_param_ops_t *
* 返回值:1 - 处理并提交了一条(状态已落终态或转 WAIT);0 - 队列空(状态不动);
* -1 - 参数无效(ops 或其函数指针为 NULL,队列与状态都不动) int
* 限定条件:uiTask 上下文,紧跟页面之后调(App/app/ui_data_feed.c 的 tick);
* 每轮最多处理一条——整定是低频人工事件,一轮一条已够,且把最坏耗时
* 钉在 40 次 check + 40 次 apply,不会随队列堆积而抖动
* 函数说明:固定四步(顺序即判据,契约 §6.2)——
* ① 取队首一条;② ops->locked() 非 0 → ERR_LOCKED,**本条丢弃**(闭锁
* 是业务态,用户解闭锁后重新提交;不做"排队等解锁",那会让屏上的 WAIT
* 无限期挂着);③ 按 mask 逐项 ops->check() 整批预检,任一项越界 →
* ERR_RANGE 且**一字不写**(all-or-nothing,与 prot_set_load_raw 同纪律),
* 越界项记入 err_mask;④ 预检全过再逐项 ops->apply(),全成 → OK。
* ④ 中途失败(预检已过仍失败 = 执行侧与预检不一致,理论不可达)不回滚:
* prot_set 无回滚接口,谎称"整批未写"比如实报 ERR_RANGE 更坏,故按实际
* 失败项置 err_mask 后报 ERR_RANGE,并在注释里写明可能部分已生效
*/
int ui_data_param_drain(const ui_data_param_ops_t *a_ops);
/* ── 通道四:装置维护写请求(时钟 / 强制复归 / 清除记录)───────────────────── */
/*
* 执行侧函数表(道F task-3 新增;注入式,与 ui_data_param_ops_t、
* ui_w_records.h 的 ui_rec_reader_t 同一条路子)
*
* 为什么是注入而不是直接调:分层方向硬约束——BSP 层目录 Drivers/BSP 不得反向
* include App 层的头(scripts/check_layer_deps.py 拦提交),而 sys_clock_set_unix /
* prot_elm_request_reset / syslog_request_clear 三件事实的属主全在 App 侧。
* ⚠ **另有一条同样硬的理由**:模拟器根本没有 sys_clock
* (simulator/dbg_log.h:4「模拟器无 sys_clock/RTC」,CMakeLists 也没这份源),
* 所以页面里哪怕只用一次 sys_clock_*,也是"MCU 编得过、模拟器链接期爆",
* 而这种错只在另一端构建时才暴露。写动作一律走本表,两端各给实现。
* ⚠ 本表刻意不含任何"范围数值/枚举值":什么是有效时间、哪条记录对应哪个文件,
* 真源仍分别在各属主模块(本层不做第二真源,只转达请求与结果)。
*/
typedef struct {
/*
* 函数功能:设置系统墙钟(本地域 epoch 秒)
* 入口参数:unix_epoch - 要设置的秒 uint32_t
* 返回值:0 - 已生效;-1 - 设置失败 int
* 限定条件:真机实现**必须**在 sys_clock_set_unix() 之后回读 sys_clock_get()
* 复核(差值容差见 App 侧实现),不得"调了就算成"
*/
int (*clock_set)(uint32_t a_unix_epoch);
/*
* 函数功能:请求复归全部保护元件状态
* 入口参数:无
* 返回值:0 - 已受理;-1 - 拒收 int
* 限定条件:真机实现 = prot_elm_request_reset()(只置单字节标志,由
* protectTask 下一拍消费),本层不等执行结果
*/
int (*reset_req)(void);
/*
* 函数功能:取当前处于动作态的保护元件数("动作位复归"的观测量)
* 入口参数:无
* 返回值:元件数(0 = 无元件动作) uint32_t
* 限定条件:真机实现 = popcount(prot_elm_active_mask())
*/
uint32_t (*active_count)(void);
/*
* 函数功能:往"操作记录"投一条留痕(人工在面板上做过一件事)
* 入口参数:desc - 描述串(NUL 结尾,调用方持有的常量串;投递侧即拷贝)
* const char *
* 返回值:无
* 限定条件:真机实现 = syslog_post_quiet(SYSLOG_OPER, desc)。**谁触发谁投**:
* 远端 104 遥控那条已由 App/protocol/iec104/iec104_pts.c 投,
* 本地面板这条由页面在受理成功后投,两边各一条、不得互替
*/
void (*oper_note)(const char *a_desc);
/*
* 函数功能:请求清除某类记录(只登记请求,不在本上下文删任何东西)
* 入口参数:log_id - UI 侧记录源编号(UI_W_REC_LOG_*,与 ui_rec_reader_t 同一编号空间) uint8_t
* 返回值:0 - 请求已登记;-1 - 编号越界或登记失败 int
* 限定条件:真机实现 = syslog_request_clear(映射后 id)。⚠ 共享代码**严禁**
* 调 MCU 后端的 syslog_clear_apply()(模拟器无实现 = 链接期报错),
* 也严禁 UI 直接 f_unlink / 碰卷锁(defaultTask 是唯一写者)
*/
int (*clear_req)(uint8_t a_log_id);
/*
* 函数功能:查某类记录"清除请求"的状态
* 入口参数:log_id - 记录源编号 uint8_t
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL 之一 int
* 限定条件:真机实现 = syslog_clear_state(映射后 id),返回值直通不改码。
* ⚠ WAIT 是正常态(删除在 defaultTask 的 syslog_drain() 尾部落地);
* 模拟器端同步删除,永不出现 WAIT —— 页面两种都要能显示
*/
int (*clear_state)(uint8_t a_log_id);
} ui_data_maint_ops_t;
/*
* 清除状态(与 App/log/sys_log.h 的 SYSLOG_CLEAR_NONE/OK/WAIT/FAIL **逐值相等**;
* 适配层直通、不做映射表,等值由主机单测在同一 TU 里逐项核对钉住,
* 同 UI_W_SOE_T_* 的先例——等值只由测试钉,不由文档转述)
*/
#define UI_DATA_CLEAR_ST_NONE 0 /* 从未请求过清除 */
#define UI_DATA_CLEAR_ST_OK 1 /* 已清空:同拍该记录条数必为 0 */
#define UI_DATA_CLEAR_ST_WAIT (-1) /* 请求排队中(正常态,不是失败) */
#define UI_DATA_CLEAR_ST_FAIL (-2) /* 落地失败:原记录一条没少,可再发一次请求 */
/*
* 函数功能:注入装置维护执行侧函数表(启动期一次;先于 ui_menu_init)
* 入口参数:ops - 函数表(六个成员指针都不得为 NULL;生命周期由注入方保证,
* 本层只存指针不拷贝内容) const ui_data_maint_ops_t *
* 返回值:0 - 已注入;-1 - ops 为 NULL 或某成员为 NULL(存量不动,页面按
* "维护能力不可用"渲染,不崩不猜) int
* 限定条件:uiTask 启动期(与 ui_w_records_bind 同一处);传 NULL = 撤销注入
* 函数说明:与 ui_data_param_drain() 的"每次调用时传入"不同,本表是**常驻注入**:
* 维护动作由用户按键触发、无固定节拍,若也走"每次传表"就得在 uiTask
* 主循环里每轮挂一次,而那一轮并没有任何东西要落定(空转一次函数调用
* 换不来任何判据)。撤销窗口只在测试里存在
*/
int ui_data_maint_bind(const ui_data_maint_ops_t *a_ops);
/*
* 函数功能:请求设置墙钟(时钟设置页"保存"行的唯一出口)
* 入口参数:unix_epoch - 由 ui_data_clock_to_epoch() 合成、且已整组校验过的秒 uint32_t
* 返回值:0 - 执行侧受理(终态由执行侧的回读校验给出);-1 - 未注入或执行侧失败;
* -2 - 参数无效(本层不判日期合法性,那是
* ui_data_clock_to_epoch() 的活,到这儿只判 0 哨兵) int
* 限定条件:uiTask 上下文(页面按键钩子)
*/
int ui_data_maint_clock_set(uint32_t a_unix_epoch);
/*
* 函数功能:请求复归全部保护元件(强制复归页的执行出口)
* 入口参数:无
* 返回值:0 - 已受理;-1 - 未注入或执行侧拒收 int
* 限定条件:uiTask 上下文;⚠ 操作记录**不在本函数里投**——"这是本地面板发起的"
* 这一事实只有页面知道,由页面在返 0 之后显式调
* ui_data_maint_oper_note() 投(拒收时不得投,屏上留痕与记录留痕
* 必须同一条判据)
*/
int ui_data_maint_reset_req(void);
/*
* 函数功能:取当前动作元件数(复归页"保护动作 N"行的数据源;屏上用字须已入子集)
* 入口参数:无
* 返回值:元件数;未注入返回 0(= 显示"无元件动作",与真值 0 同形,
* 但页脚/态标另有 LOCK 提示装配未接好) uint32_t
* 限定条件:页面绘制路径可直调(一次函数指针调用,无 I/O、无锁)
* 函数说明:本函数是"动作位复归可证"的**唯一观测量**:复归请求受理后,
* 页面按帧重读,N 从 >0 变 0 就是屏上可见的证据;复归不掉只可能是
* 测量仍在越限(元件重新 pickup),不是请求丢了
*/
uint32_t ui_data_maint_active_count(void);
/*
* 函数功能:投一条操作记录留痕(页面在本地动作受理后调)
* 入口参数:desc - 描述串(ASCII,长度受执行侧 SYSLOG_DESC_MAX 约束;NULL = 什么都不做)
* const char *
* 返回值:0 - 已投递;-1 - 未注入或 desc 为 NULL int
* 限定条件:uiTask 上下文;投递本身不阻塞、可能因队列满被丢弃(执行侧口径,
* 与 syslog_post_quiet 一致——本层不加"投递失败"的第二套显示语义:
* 留痕丢了不等于操作没发生,把记录通道故障显示成操作失败更坏)
*/
int ui_data_maint_oper_note(const char *a_desc);
/*
* 函数功能:请求清除某类记录(清除记录页的执行出口)
* 入口参数:log_id - 记录源编号(UI_W_REC_LOG_*) uint8_t
* 返回值:0 - 已受理;-1 - 未注入 / 编号越界 / 执行侧拒绝 int
* 限定条件:uiTask 上下文;"受理"≠"已清除",终态读 ui_data_maint_clear_state(),
* 事实复核读记录条数(ui_w_records 的同一枚 reader)
*/
int ui_data_maint_clear_req(uint8_t a_log_id);
/*
* 函数功能:查某类记录的清除状态(清除记录页值列的数据源)
* 入口参数:log_id - 记录源编号 uint8_t
* 返回值:UI_DATA_CLEAR_ST_NONE / _OK / _WAIT / _FAIL;未注入或编号越界返回 NONE
* ("从没请求过",页面据此显示条数而不是结果词) int
* 限定条件:页面绘制路径可直调;返回值直通执行侧、不做二次加工(同
* ui_data_param_state() 的理由:两处解释同一件事必然漂移)
*/
int ui_data_maint_clear_state(uint8_t a_log_id);
/* ── 时钟字段换算(本地域,无时区偏移;"无效值拒收"的唯一判据处)─────────── */
/*
* 年份上下限(两条硬约束的交集,不是拍脑袋的整数区间):
* 下限 2000:SD2506 RTC 年字段是相对值 0~99(+2000,见
* Drivers/BSP/SD2506/sd2506.h 的 sd2506_time_t.year 注释),
* 2000 以下写不进 RTC
* 上限 2038:墙钟 epoch 是 uint32_t 秒,上界 0x7FFFFFFF = 2038-01-19 03:14:07
* (App/util/sys_clock.h 的 sys_clock_set_unix 取值域一致)
*/
#define UI_DATA_CLOCK_YEAR_MIN 2000u
#define UI_DATA_CLOCK_YEAR_MAX 2038u
#define UI_DATA_CLOCK_EPOCH_MAX 0x7FFFFFFFu
#define UI_DATA_CLOCK_TS_LEN 19u /* "YYYY-MM-DD HH:MM:SS"(与 rt.clock 同串型) */
/* 时钟日历字段(本地域;数值就是屏上显示的那六个数,不含任何时区语义) */
typedef struct {
uint16_t year; /* UI_DATA_CLOCK_YEAR_MIN..UI_DATA_CLOCK_YEAR_MAX */
uint8_t month; /* 1..12 */
uint8_t day; /* 1..该月真实天数(闰年由本层判,不交给调用方) */
uint8_t hour; /* 0..23 */
uint8_t minute; /* 0..59 */
uint8_t second; /* 0..59 */
} ui_data_clock_t;
/*
* 函数功能:取某年某月的天数(含闰年 2 月)
* 入口参数:year - 年 uint16_t month - 月 1..12 uint8_t
* 返回值:28 / 29 / 30 / 31;月份越界返回 0(调用方据此判非法,不用第二条错误码) uint8_t
* 限定条件:闰年规则 = (4 年 && !100 年) || 400 年(与 App/util/sys_clock.c
* 的 unix_to_sd2506 / sd2506_to_unix 同一判据,两侧都是完整格里历规则;
* 2100 年不在允许年域内,不需要讨论那个例外)
*/
uint8_t ui_data_clock_days_in_month(uint16_t a_year, uint8_t a_month);
/*
* 函数功能:日历字段 → 本地域 epoch 秒(**同时是这组字段的合法性判据**)
* 入口参数:p_in - 六个字段 const ui_data_clock_t *
* p_out - 秒 uint32_t *
* 返回值:0 - 合法并已输出;-1 - 参数为 NULL;-2 - 字段组合非法(年/月/日/时/分/秒
* 任一越界,含 2 月 30 日、非闰年 2 月 29 日、月 13、时 24 等);
* -3 - 组合合法但超出取值域(epoch > UI_DATA_CLOCK_EPOCH_MAX) int
* 限定条件:无(纯整数,零依赖,可在主机单测跑全表)
* 函数说明:日的当月真实上限**在这里**判,不在字段编辑器里判——编辑器只有静态
* min/max(日一律 1..31),把闰年规则抄一份进编辑器就是第二真源。
* ⚠ 不含时区偏移:本工程墙钟的 epoch 域本身就是本地时间
* (App/util/sys_clock.h 的 sys_clock_set_unix 注明"秒,本地时间",
* 其正/反换算两头都没有 ±8 运算;App/task/ui_task.c 的 SOE 时标注记
* 已把"别顺手补 8 小时"钉过一次)。这里加 28800 的结果是"屏上输 08:00、
* 存进去 16:00",且只在真机上错,模拟器照样自洽
*/
int ui_data_clock_to_epoch(const ui_data_clock_t *p_in, uint32_t *p_out);
/*
* 函数功能:本地域 epoch 秒 → 日历字段
* 入口参数:unix_epoch - 秒 uint32_t p_out - 输出 ui_data_clock_t *
* 返回值:0 - 已输出;-1 - p_out 为 NULL;-2 - 超出允许年域(< 2000-01-01 或
* > UI_DATA_CLOCK_EPOCH_MAX) int
* 限定条件:与 ui_data_clock_to_epoch() 互为反函数(同一套格里历规则,
* 往返必等;这条往返性是主机单测的断言之一,不是注释里的口头保证)
*/
int ui_data_clock_from_epoch(uint32_t a_unix_epoch, ui_data_clock_t *p_out);
/*
* 函数功能:解析 "YYYY-MM-DD HH:MM:SS" 串为日历字段
* 入口参数:ts - 待解析串(快照 rt.clock 的形状) const char *
* p_out - 输出 ui_data_clock_t *
* 返回值:0 - 成功;-1 - 参数 NULL;-2 - 串型不对(长度非 19、分隔位不是
* '-'/' '/':'、出现非数字);-2 同时覆盖"型对但值非法"
* (解析出的组合再过 to_epoch()) int
* 限定条件:时钟无效时快照里是**空串**(ui_data.h:149 的口径),空串走 -2;
* 调用方据此显示"无时钟"而不是 0000-00-00
*/
int ui_data_clock_parse(const char *a_ts, ui_data_clock_t *p_out);
/*
* 函数功能:日历字段 → "YYYY-MM-DD HH:MM:SS"(与 ui_data_clock_parse 同串型)
* 入口参数:p_in - 字段 const ui_data_clock_t *
* buf - 输出缓冲 char * len - 缓冲容量(含 NUL;不足则整串不写) uint32_t
* 返回值:0 - 已写出;-1 - 参数无效或容量不足 int
* 限定条件:不判字段合法性(调用方刚从 parse/to_epoch 那条链出来);
* 手写十进制、不引 stdio——本层是零 LVGL 零 App 的叶子,拉进
* lv_snprintf/stdio 就等于给主机单测加依赖
*/
int ui_data_clock_format(const ui_data_clock_t *p_in, char *a_buf, uint32_t a_len);
/* ── 通道五:遥测整定注入(死区 / 系数,道F task-7)────────────────────────
*
* 为什么又是一张注入表而不是页面直调 run_par / adc_cal:与通道四完全同一组理由
* ——① 分层方向(Drivers/BSP 不得反向 include App 头,layer-deps 拦提交);
* ② 模拟器既不链 App/algo/run_par.c 也不链 App/algo/adc_cal.c,页面里用一次
* 真源函数就是"MCU 编得过、模拟器链接期爆"(通道四表注已记过一次的同族坑);
* ③ 合法域与"写值 + 置脏"必须原子发生在执行侧(run_par_cal_k_set 的文件头契约:
* 拆开等于"屏上已改、盘上永不落、且没有任何错误返回"),本表只转达。
* ⚠ 本表刻意不含任何范围数值的拷贝:lo/hi 由执行侧逐行现给(合法域唯一出口
* run_par_db_range,系数粗守上限唯一出口 RUN_PAR_K_MAX_UV),页面抄一份数字
* 就是第二真源——µV 真带(执行段裁)没有公开访问器,也**不许**页面自己补一份。
*/
/* 死区点数:下标 0..7 = 电气语义通道序(UA..IN),8 = 频率(与 104 地址映射表
* 0x8030~0x8038 同序);系数覆盖点数:电气 8 路(语义枚举前缀段)。
* 与 App/algo/run_par.h 的 RUN_PAR_DB_N / RUN_PAR_CH_N 等值——BSP 看不到 App 头,
* 等值由装配层编译期守卫(App/app/ui_data_feed.c)与 TC-UIDATA-027 同时钉住 */
#define UI_DATA_YT_DB_N 9u
#define UI_DATA_YT_CH_N 8u
/* 量纲代码:unit 是"这行数值本身的物理量纲"(诚实性字段,同 rt 帧 eng 的思路),
* 归数据源管;把它显示成什么字符串("V"/"Hz"/"uV")才是表现层的事 */
#define UI_DATA_YT_UNIT_V 0u /* 电压死区,V */
#define UI_DATA_YT_UNIT_A 1u /* 电流死区,A */
#define UI_DATA_YT_UNIT_HZ 2u /* 频率死区,Hz */
#define UI_DATA_YT_UNIT_UV 3u /* 系数 k,µV/码(屏上串用 ASCII "uV") */
/* 一行遥测整定项的完整现状(执行侧现给,页面只读) */
typedef struct {
float val; /* 当前工程量:死区 V/A/Hz;系数 = adc_cal 当前生效值(µV/码,
与 104 回显同视图:文件坏值被合法带哨兵回退 ROM 时显 ROM) */
float lo; /* 粗守下限(含)——合法域唯一出口给,页面不得自造 */
float hi; /* 粗守上限(含)——同上;真带在执行段另裁,两层互不掩盖 */
uint8_t unit; /* UI_DATA_YT_UNIT_* */
uint8_t decimals; /* 显示小数位(**显示域选择**,不是数据源精度,归装配层定) */
} ui_data_yt_row_t;
/*
* 执行侧函数表(常驻注入;形状与 ui_data_maint_ops_t 同款:整表校验、拒残表)
*
* 与定值整定(通道 param 的位图批量 + 工作叠层)**故意不同形**:死区/系数写的是
* 运行参数档([run_par]/[adc_cal]),语义 setter 本身就是"写 RAM 副本 + 置脏"
* 的原子出口、单行即提交;落盘确认是独立一问(pending),所以本表没有 commit
* 一口——保存行按键读的就是 pending()。
*/
typedef struct {
/*
* 函数功能:死区点数(执行侧真值;未注入时本层返回 0)
* 返回值:点数 uint8_t
*/
uint8_t (*db_n)(void);
/*
* 函数功能:读第 idx 行死区现状(工程量 + 粗守域 + 量纲 + 显示小数位)
* 返回值:0 - 已写出;-1 - idx 越界 / 指针空 / 执行侧无此行 int
*/
int (*db_row)(uint8_t a_idx, ui_data_yt_row_t *a_out);
/*
* 函数功能:写第 idx 行死区(工程量)——真机 = run_par_deadband_set()
* 返回值:0 - 副本已写且已置脏;-1 - 粗守或执行侧拒(副本未动、不置脏) int
*/
int (*db_set)(uint8_t a_idx, float a_eng);
/* 函数功能:系数覆盖点数(执行侧真值) 返回值:点数 uint8_t */
uint8_t (*cal_n)(void);
/* 函数功能:读第 idx 路系数现状(同 db_row,工程量域为 µV/码) int */
int (*cal_row)(uint8_t a_idx, ui_data_yt_row_t *a_out);
/*
* 函数功能:写第 idx 路系数 k(µV/码)——真机 = run_par_cal_k_set()
* 返回值:0 - 已写并置脏;-1 - 粗守或执行段真带拒(副本未动、不置脏) int
*/
int (*cal_k_set)(uint8_t a_idx, float a_uv);
/*
* 函数功能:未标定闭锁徽标源——真机 = adc_cal_prot_blocked()(task-16 单点
* 访问器,与判据停走同一个判定;这里**不**用 rt 帧 eng 反推)
* 返回值:1 - 未标定闭锁中;0 - 正常 uint8_t
*/
uint8_t (*cal_blocked)(void);
/*
* 函数功能:整档落盘脏位探查——真机 = run_par_dirty_peek()
* 返回值:1 - 有已整定未落盘改动;0 - 干净 uint8_t
* ⚠ 两条口径必须与页面注释同步(道F task-7 Lead 裁定):
* ① 脏位是**整档全局**的(一趟落 [run_par] 九键 + [adc_cal] 两段),归零
* 证明"这一档落盘了",不证明"我这一项单独落盘";
* ② 他人(104 C_SE)随后再改会重新置脏 ⇒ 页面必须结果锁存,把"我这次动作
* 的结果"与"档位此刻的状态"分开(clear/clock 页同一条先例)。
*/
uint8_t (*pending)(void);
} ui_data_yt_ops_t;
/*
* 函数功能:注入遥测整定执行侧函数表(启动期一次;形状/语义逐字对齐 ui_data_maint_bind)
* 入口参数:ops - 函数表(八个成员指针都不得为 NULL);NULL = 撤销注入(只在宿主测试存在)
* 返回值:0 - 已注入(或已撤销);-1 - ops 非空但成员残缺(存量不动,页面按
* "整定能力不可用"渲染,不崩不猜) int
* 限定条件:uiTask 启动期、先于 ui_menu_init()(真机在装配层 ui_data_feed_tick()
* 首轮懒绑一次——该调用点在 ui_menu_init 之前,时序与通道四绑定等价)
*/
int ui_data_yt_bind(const ui_data_yt_ops_t *a_ops);
/*
* 函数功能:死区点数
* 入口参数:无
* 返回值:点数;未注入返回 0 uint8_t
*/
uint8_t ui_data_yt_db_n(void);
/*
* 函数功能:读第 idx 行死区现状
* 入口参数:idx - 行下标 uint8_t;a_out - 行现状(返 0 才被整份写出) ui_data_yt_row_t *
* 返回值:0 - 已写出;-1 - 未注入 / 参数空 / 执行侧无此行 int
*/
int ui_data_yt_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out);
/*
* 函数功能:写第 idx 行死区(工程量)
* 入口参数:idx - 行下标 uint8_t;a_eng - 工程量(V/A/Hz) float
* 返回值:0 - 已写并置脏;-1 - 未注入或执行侧拒(未注入与拒绝同码:页面都按
* ERR 显示,装配故障与工况故障的区分归日志不归屏幕) int
*/
int ui_data_yt_db_set(uint8_t a_idx, float a_eng);
/*
* 函数功能:系数覆盖点数
* 入口参数:无
* 返回值:点数;未注入返回 0 uint8_t
*/
uint8_t ui_data_yt_cal_n(void);
/*
* 函数功能:读第 idx 路系数现状(µV/码域)
* 入口参数:idx - 电气语义通道序 uint8_t;a_out - 行现状 ui_data_yt_row_t *
* 返回值:0 - 已写出;-1 - 未注入 / 参数空 / 执行侧无此行 int
*/
int ui_data_yt_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out);
/*
* 函数功能:写第 idx 路系数 k(µV/码)
* 入口参数:idx - 电气语义通道序 uint8_t;a_uv - 新系数(µV/码) float
* 返回值:0 - 已写并置脏;-1 - 未注入或拒(粗守/真带,副本未动) int
*/
int ui_data_yt_cal_k_set(uint8_t a_idx, float a_uv);
/*
* 函数功能:未标定闭锁徽标源
* 入口参数:无
* 返回值:1 - 闭锁中;0 - 正常或未注入(宁可不画,不假报) uint8_t
*/
uint8_t ui_data_yt_cal_blocked(void);
/*
* 函数功能:整档落盘脏位探查(保存行三态判据)
* 入口参数:无
* 返回值:1 - 有已整定未落盘改动;0 - 干净或未注入 uint8_t
*/
uint8_t ui_data_yt_pending(void);
/* ── 通道六:通讯参数([comm])整定注入(道G task-9,通讯设置页)───────
*
* 理由与通道五逐字同构(分层方向 layer-deps / 模拟器不链 App/protocol /
* "写值+置脏"原子性归执行侧),此处只记差异:
* ① 行数不进表——[comm] 就两个键(ca/port),行名与行数是**页面形状**,
* 不是数据源;lo/hi 与值由 row() 现给(合法带唯一出口 = iec104_comm.h
* 的带宏与 getter 守卫),页面不得抄数(通道五"第二真源"条同口径)。
* ② 生效语义与通道五不同档:port 下次重 bind 监听生效、ca **改后重启**
* 生效(页脚与保存行不承诺"立即生效",措辞口径见 ui_page_comm.c 头)。
* ③ 值域 u16 级,但通道口径走 uint32——编辑器值通道是 int32 定点,
* set() 收到 >65535 由执行侧带外拒(副本不动),页面不做二次守卫。
*/
/* 行下标(行序 = 页面行序,装配与页面各自钉死,TC 双向锁) */
#define UI_DATA_COMM_ROW_CA 0u /* 公共地址 1..65535(改后重启生效) */
#define UI_DATA_COMM_ROW_PORT 1u /* 监听端口 1024..65535(下次重 bind 生效) */
#define UI_DATA_COMM_ROW_N 2u
/* 一行通讯参数的现状(执行侧现给;无浮点,uint32 直达) */
typedef struct {
uint32_t val; /* 当前值(RAM 副本 getter) */
uint32_t lo; /* 合法带下限(含)——iec104_comm.h 带宏现给 */
uint32_t hi; /* 合法带上限(含)——同上 */
} ui_data_comm_row_t;
/* 执行侧函数表(常驻注入;整表校验、拒残表——通道四/五同款纪律) */
typedef struct {
/*
* 函数功能:读第 idx 行现状(值 + 合法带)
* 返回值:0 - 已整份写出;-1 - idx 越界 / 指针空 / 未注入侧无此行 int
*/
int (*row)(uint8_t a_idx, ui_data_comm_row_t *a_out);
/*
* 函数功能:写第 idx 行(真机 = iec104_comm_ca_set / iec104_comm_port_set)
* 返回值:0 - 副本已写且已置脏;-1 - 带外拒(副本未动、不置脏) int
*/
int (*set)(uint8_t a_idx, uint32_t a_val);
/*
* 函数功能:段级落盘脏位探查——真机 = iec104_comm_dirty_peek()
* 返回值:1 - 有已改未落盘;0 - 干净。落盘动力 = defaultTask 每轮无条件
* iec104_comm_store_save()(通道五注释里查实的那条链同款)。
* 页面按「结果锁存」画 WAIT/OK(他人 104 侧无自动写者,比通道五
* 更干净,但锁存纪律不变——同一条口径别做两套)
*/
uint8_t (*pending)(void);
} ui_data_comm_ops_t;
/*
* 函数功能:注入通讯参数执行侧函数表(启动期一次;形状对齐 ui_data_yt_bind)
* 入口参数:ops - 函数表(三件都不得为 NULL);NULL = 撤销注入(宿主测试用)
* 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动;页面按"不可用"渲染) int
* 限定条件:uiTask 启动期、先于 ui_menu_init()(真机 = 装配层 tick 首轮懒绑,
* 与通道五同点位相邻)
*/
int ui_data_comm_bind(const ui_data_comm_ops_t *a_ops);
/*
* 函数功能:读第 idx 行通讯参数现状
* 返回值:0 - 已写出;-1 - 未注入 / 参数空 / idx 越界(退化显示,不猜) int
*/
int ui_data_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out);
/*
* 函数功能:写第 idx 行通讯参数
* 返回值:0 - 已写并置脏;-1 - 未注入或带外拒(未注入与拒绝同码,同通道五) int
*/
int ui_data_comm_set(uint8_t a_idx, uint32_t a_val);
/*
* 函数功能:[comm] 段落盘脏位探查(保存行三态判据)
* 返回值:1 - 有已改未落盘;0 - 干净或未注入 uint8_t
*/
uint8_t ui_data_comm_pending(void);
/* ── 通道六:故障录波(wave)────────────────────────────────────────────────
* 页面零 App include、零存储触点:录波执行侧(触发投递 / 段索引 / 段头字段 /
* 窗口码值与峰值 mV)全部经本注入表进来,真实现见 App/task/ui_task.c 的
* s_wave_ops;PC 模拟器绑同一形状的假件(simulator/main.c)。取数口径的
* 唯一真源是 docs/子系统设计/故障录波设计说明.md §6.3(定点 mV 换算走
* wave_store_code_to_milli,eng 只做徽标、绝不进算式)。
*/
/* 录波通道数(与段头 ch_count 恒等:电气八路,温度不入波) */
#define UI_WAVE_CH_CNT 8u
/* 回放显示窗口点数(两周波;口径推导与设计 §4.2 的触发点位置钉在一起,
* 窗起点与虚线光标由执行侧与页面各按同一定义实现,页面侧写死 */
#define UI_WAVE_WIN_PTS 40u
/* 通道查询返回态:段存在且可用 */
#define UI_WAVE_OK 0
/* 无段(目录扫空 / 卷忙跳过 / 执行侧未注入,页面显示「无录波段」) */
#define UI_WAVE_NONE 1
/* 有段但不可读(长度不符 / 版本不符 / 任一段 CRC 不符,页面显示
* 「该段不可读」——两种态都必须显式说,画直线等于伪造证据(设计 §8.1) */
#define UI_WAVE_BAD 2
/*
* 录波执行侧注入表(六件齐全;ui_data_wave_bind 拒绝残表)
* 返回值约定:除 manual_req 外,各件返回 UI_WAVE_OK / NONE / BAD 三态;
* seg_win 的出参仅在返 UI_WAVE_OK 时可读(通道五同一条出参契约)。
*/
typedef struct {
/* 投一次手动录波请求。返回值:0 - 已受理;非零 - 拒收(含未落段通道
* 不可用),页面据此走确认链的 ERR 支 */
int (*manual_req)(void);
/* 取盘上数值最大的段号(回放页开屏落点;左右键从此起环游)。
* 回绕后本序不保证时间最新,页顶段号即自证;时间序要靠段头
* frame_seq/t0,代价是 100 次开文件,本波不做(lead 裁定 2026-10-29) */
int (*seg_first)(uint8_t *a_idx);
/* 取比 a_cur 更早一段的段号(到最早端后环回数值最大端) */
int (*seg_prev)(uint8_t a_cur, uint8_t *a_out);
/* 取比 a_cur 更新一段的段号(到最大端后环回数值最小端) */
int (*seg_next)(uint8_t a_cur, uint8_t *a_out);
/* 读段头并做完整性判定(头 CRC 与数据 CRC 都在执行侧验完)。
* 出参:eng - 单位口径(UI_DATA_ENG_* 同值域);trig - 触发源
* (段头 trig_src 原值:手动/保护/定时);clip - 形状受限通道位图
* (段头 clip 字节,位号 = 通道号) */
int (*seg_head)(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig,
uint8_t *a_clip);
/* 读当前显示窗口的码值(已并入 b_codes 偏移)与窗口峰值。
* 出参:a_codes - UI_WAVE_WIN_PTS 个码值;a_peak_milli - 窗口内
* 绝对值最大的那一点折算的 mV(定点,执行侧经 code_to_milli 算好,
* 页面不重复换算式——§6.3 的单一口径条款;b_codes 由 code_to_milli
* 内部加,与 a_codes 里已并的偏移同一把尺:谁被显示谁被度量) */
int (*seg_win)(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes,
int32_t *a_peak_milli);
} ui_data_wave_ops_t;
/*
* 函数功能:注入录波执行侧函数表(启动期一次;形状对齐 ui_data_comm_bind)
* 入口参数:ops - 函数表(六件都不得为 NULL);NULL = 撤销注入(宿主测试用)
* 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动) int
* 限定条件:uiTask 启动期、先于 ui_menu_init()
*/
int ui_data_wave_bind(const ui_data_wave_ops_t *a_ops);
/*
* 函数功能:投一次手动录波请求(录波触发页唯一写出口)
* 返回值:0 - 已受理;-1 - 未注入或拒收(未注入与拒收同码,同通道五) int
*/
int ui_data_wave_manual_req(void);
/*
* 函数功能:取数值最大段号(排序口径见 ui_data.c 同名胶水注释:回绕后本序
* 不保证时间最新,页顶段号即自证;真时间序要逐段读 frame_seq/t0,
* 代价 100 次开文件,本波不做)
* 入口参数:a_idx - 出参,段号(仅返 OK 时可读)
* 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD(未注入按 NONE 渲染空态,不崩) int
*/
int ui_data_wave_seg_first(uint8_t *a_idx);
/*
* 函数功能:取 a_cur 的更早一段段号(环形回绕)
* 入口参数:a_cur - 当前段号;a_out - 出参(仅返 OK 时可读)
* 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD int
*/
int ui_data_wave_seg_prev(uint8_t a_cur, uint8_t *a_out);
/*
* 函数功能:取 a_cur 的更新一段段号(环形回绕,方向与 prev 相反)
* 入口参数:a_cur - 当前段号;a_out - 出参(仅返 OK 时可读)
* 返回值:UI_WAVE_OK / UI_WAVE_NONE / UI_WAVE_BAD int
*/
int ui_data_wave_seg_next(uint8_t a_cur, uint8_t *a_out);
/*
* 函数功能:读段头(含执行侧 CRC 完整性判定)——出参仅返 OK 时可读
* 返回值:UI_WAVE_OK / NONE / BAD(未注入按 NONE) int
*/
int ui_data_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig,
uint8_t *a_clip);
/*
* 函数功能:读显示窗口码值与峰值 mV——出参仅返 OK 时可读
* 返回值:UI_WAVE_OK / NONE / BAD(未注入按 NONE) int
*/
int ui_data_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes,
int32_t *a_peak_milli);
#ifdef __cplusplus
}
#endif
#endif /* UI_DATA_H */