feat(ui-data): ui_data 三通道快照 + ui_data_feed 装配 + SOE 显示名映射

按 docs/UI设计/UI_数据供给设计说明.md 的 D 方案把 App→UI 数据通道落地:
App 组帧、BSP 存帧、页面只读;UI 侧零 App include,薄包装只在两端适配层。
本笔是纯增层、不接任何页面 → 零像素变化(菜单 8 状态 + 内容页 11 页黄金
参考逐字节不变,simulator/tests/ref 无 diff,未刷基线)。

新增
- Drivers/BSP/UI/ui_data.{c,h}:实时量 rt / 参数 par / 装置信息 info 三通道存储
  + 发布序号盖章 + 只读访问。零 App include、零 LVGL include(分层叶子);
  读写同在 uiTask 上下文串行 → 无锁、无双缓冲、无 seqlock(契约 §5.1)。
- App/app/ui_data_feed.{c,h}:快照唯一填充者。rt/info 每 200 ms 一帧,par 只在
  prot_set_checksum() 变化时重发;首轮立即发布,免启动初期闪一下 "--"。
- Drivers/BSP/UI/ui_w_soe.{c,h}:SOE 二进制记录的显示名映射与注入式 reader
  契约。未标定闭锁=7 / 标定复归=8 已进表——编号按 App/log/soe_rec.h 与
  App/algo/prot_elm.h 现场数过(类型 1..8 + 哨兵 9、元件 0..8、ELM_NONE=0xFF、
  FLG_IV=0x80),不采信文档与消息转述。reader 备好但两端未绑定:落盘侧还
  没有只读 API(Lead 已裁该 API 归下一工作项),未注入按"无记录"空态渲染。

量纲诚实性(契约 §4.1 同步改成真值口径,不再抄旧码值表述)
- eng 不写"0 = 原始码值":标定层 ROM 系数是引脚侧 LSB,采样环折算出来的就是
  ADC 引脚电压(承诺边界 docs/任务规划/数据可信.md §2.6);电气 8 路
  adc_cal_flags() 的 VALID 全置位才升 eng=1 二次值——整帧共用一个 eng,只要
  还有一路是引脚电压就不能贴"二次值"标签,宁可整体保守。
- TEMP 不参与线性标定 → 恒带新增品质位 UI_DATA_Q_RAW=0x20,页面不挂单位。
- UB/UC/IB/IC/IN 无基波相量实例(V0.1 削顶)→ Q_IV|Q_OV;未满一周波 → Q_NT。
- 谐波另开 ph[] 相量实例槽带:yc 的语义通道下标容不下同通道的二次/五次谐波。

接线与双端注册
- App/task/ui_task.c:ui_menu_init 之前 ui_data_init + 首帧发布;主循环每轮
  ui_data_feed_tick() 排在 ui_port_step 之前(本轮渲染用本轮刚发布的帧)。
- simulator/main.c:发布固定合成帧(无头与窗口两个初始化点,先于建页);
  定值不手抄 40 个数,改为链入 App/algo/prot_set.c 取出厂默认(抄表等于造
  第二真相);build_date 用固定串而非 __DATE__,否则跨天重建会把装置信息页
  黄金参考刷掉,而黄金参考是判据本身。
- 注册:uvprojx 加 3 条(App 组 ui_data_feed.c + UI 组 ui_data.c/ui_w_soe.c)与
  simulator/CMakeLists.txt;ui_data_feed.c 不进模拟器 → check_registration.py 的
  SIM_EXEMPT 加一行(App/app/app_main.c 先例,已报备)。

主机单测与钩子
- test/host/ui_data:82 断言全绿。钉三类事:快照语义(首帧前读 -1、序号由本层
  盖章、三通道互不串台、NULL 防御、init 幂等复位);跨层不透明下标现场一致
  (yc 序 = adc_semantic_t、q10 序 = prot_set_id_t,同一 TU 里 include 两侧头);
  SOE 编号与名字表现场一致(八类事件都有名、采样/标定四个词互不相同、
  名长不超列宽、越界回占位)。
- .pre-commit-config.yaml 加 ui-data-unit-test 钩子(第 43 个,Lead 已批自加)。
- scripts/id_registry.json 建族 TC-HOST-UIDATA(属主登记处 = 单测目录,同
  TC-HOST-ADCCAL 先例;不建族则新号被编号门禁判"未登记前缀")。

体积(相对 HEAD 台账 Code 215744 / RO 21396 / RW 1224 / ZI 131984)
- Code +1272 B、RO +108 B(字库 4 字,见上一笔)、RW +24 B、ZI +696 B
  → ER_IROM1 238520 B(22.7%)、RW_IRAM1 76504 B(58.4%,台账 57.8%)。
- .map 逐项:ui_data.o 488 B(Code 184/RW 12/ZI 476)、ui_data_feed.o 221 B
  (1034/12/209);LVGL 池 0 增量;ui_w_soe 未接线不进镜像(.map 无其符号)。
- 台架未跑(本笔零像素、零接线):首帧时刻、200 ms 刷新、eng 升档与遥信
  位图真值全部留"待台架实测",接线完成后请 Lead 排窗。

遗留与风险(随回报进台账)
- 写路径(契约 §6 的 param_request/state)不落:无消费者就是永不执行的死码,
  随整定 4 页一起落;届时须先定"UI 整定 vs iec104Task 写 prot_set"的并发口径
  (prot_set_set_sealed 的影子区非重入)。
- 公共地址 CA 未下发:固件里 CA 逐帧从 ASDU 头解析,无集中常量可取。
- 遥信位图与 App/protocol/iec104/iec104_pts.c 板端源是两处镜像(该文件位号
  写在函数体内未导出,UI 不得 include 协议头),开入采集落地时应并成单源。
- 版本类字段是本文件内的宏(仓库无产品级版本登记表,值沿用页面既有显示
  值);软件校验位在拿到构建期 CRC 步骤前一律显示 "--",不抄参考遗留值。

gate: 全量钩子(--all-files)40 项通过;cppcheck/test-cppcheck/coding-standard/
layer-deps(基线仍 3 条)/registration/bsp-err(存量 38 条不变)/font-charset/
id-check(增量无新账)/doc-links/ui-golden/主机单测与覆盖率全绿;唯一红项是
mem-numbers 与 mem-budget——两者只因 Flash/RAM 台账未回填(禁区,归 Lead)。

gate: only mem-budget/mem-numbers pending lead ledger backfill

自查:BOM(Drivers/BSP/UI/* 与 App/task/ui_task.c 保持 EF BB BF,新建 UI 两件
同规范;App/app 与 test/host 无 BOM 与本目录一致);CR = 0 全 LF;U+FFFD = 0;
字符串字面量纯 ASCII(中文只进注释)。
This commit is contained in:
2026-10-25 09:56:22 +08:00
parent 8cbc7bc6b6
commit 201d713226
16 changed files with 2118 additions and 3 deletions
+196
View File
@@ -0,0 +1,196 @@
/*
* 模块名称: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)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
*/
/* 头文件包含区 */
#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;
/* 函数定义区 */
/*
* 函数功能:快照层初始化——三通道清零(valid=0、seq=0),页面即按"无首帧"渲染
* 入口参数:无
* 返回值:无
* 限定条件:内核启动前可调(纯内存操作,零 OS 依赖);可重复调用(幂等)
* 函数说明:静态存储上电本已清零,本函数的意义是运行期复位(例如"恢复出厂后
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态
*/
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;
}
/*
* 函数功能:发布一帧实时量快照(覆盖存量并给 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;
}
+273
View File
@@ -0,0 +1,273 @@
/*
* 模块名称: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.0 2026-10-25 王建锋 创建(道E task-14:契约 §4 快照层落地;相对契约
* 两通道新增第三条 info,划分理由见本头"通道划分"注
* 与契约 §4.4)
*/
#ifndef UI_DATA_H
#define UI_DATA_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/*
* 契约版本:结构体字段只允许尾部追加,追加即升此值并在写侧填默认值
* (禁止删除或重排字段——页面按名字取,重排是静默错位;
* 口径 docs/UI设计/UI_数据供给设计说明.md §4.3)
*/
#define UI_DATA_VER 1u
/* ── 通道数量 ─────────────────────────────────────────────────────────────── */
/* 遥测语义通道数:下标序 = 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 默认系数是「数据可信」卡 1 定死
* 的引脚侧 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 该点有硬件来源 */
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 枚举序 */
} 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 登录用户名(只显名,不显口令) */
} 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);
#ifdef __cplusplus
}
#endif
#endif /* UI_DATA_H */
+196
View File
@@ -0,0 +1,196 @@
/*
* 模块名称:UI SOE 记录页共享组件实现(ui_w_soe)
* 模块功能:见 Drivers/BSP/UI/ui_w_soe.h——事件类型/元件显示名映射 + 每记录 2 行
* 渲染 + 单条读盘缓存;数据经注入 reader 取,未注入按空态渲染
* 适用范围:Drivers/BSP/UI
* 说明:与 ui_w_records.c 同构(同一份"分页读记录 → 两行/条 + 实例缓存 + 空态
* 伪行"骨架),差别只在记录形态是二进制解码后的视图结构
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_w_soe.h"
/* 私有宏定义区 */
/*
* 框架文本缓冲宽度(真源 Drivers/BSP/UI/ui_tokens.h 的 UI_BUF_STRLEN = 32)。
* 这里**有意不 include ui_tokens.h**:该头经 ui_port.h 牵入整条 LVGL 头链,而本组件
* 是纯数据 + 纯文本搬运、一个 LVGL 调用都没有——容量宏本地化之后,
* test/host/ui_data 可以直接把本 .c 编进主机单测核对显示名映射(同
* Drivers/BSP/UI/ui_w_records.h 把 ts/desc 容量宏本地化的同一先例与理由:
* 容量不足只会退化成占位显示,不会越界读)。框架令牌若加宽,本宏同步加宽。
*/
#define SOE_BUF_MAX 32u
#define SOE_INVALID_STR "--" /* 未知编号/越界/读失败的统一占位(ASCII) */
#define SOE_EMPTY_STR "无记录" /* 空态伪行(与文本记录页同字) */
#define SOE_NO_CLOCK_STR "时钟无效" /* 时标行值列:IV 位不显示猜测时间 */
/* 静态变量定义区 */
/* 注入的 SOE reader(模块级单例:SOE 落盘本就是全局一份,两端各只有一份实现;
* NULL = 未注入 → 空态。当前两端均未绑定,理由见头注"说明") */
static const ui_soe_reader_t *s_reader;
/*
* 事件类型显示名(下标 = UI_W_SOE_T_*;表长 = UI_W_SOE_T_COUNT)
* 词长分两档是**版式约束**不是措辞偏好:值列可用宽约 59 px(4 汉字),
* 名列约 94 px(7 汉字)——"动作/返回"是元件行的值列词,其余四类 4 字词
* 落在非元件事件的名列(口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条)
*/
static const char *const s_type_name[UI_W_SOE_T_COUNT] = {
SOE_INVALID_STR, /* 0 未知/未映射(含读失败) */
"动作", /* 1 元件动作 */
"返回", /* 2 元件返回 */
"压板退出", /* 3 软压板退出 */
"压板投入", /* 4 软压板投入 */
"采样闭锁", /* 5 采样故障闭锁 */
"采样恢复", /* 6 采样故障恢复 */
"标定闭锁", /* 7 未标定闭锁 */
"标定复归", /* 8 标定复归 */
};
/* 元件显示名(下标 = UI 元件编号 = 保护元件枚举序;越界不入表) */
static const char *const s_elm_name[UI_W_SOE_ELM_N] = {
"过流Ⅰ段", /* 0 */
"过流Ⅱ段", /* 1 */
"过流Ⅲ段", /* 2 */
"过负荷", /* 3 */
"过压", /* 4 */
"欠压", /* 5 */
"零序过压", /* 6 */
"过频", /* 7 */
"低频", /* 8 */
};
/* 函数定义区 */
/*
* 函数功能:注入 SOE reader
*/
void ui_w_soe_bind(const ui_soe_reader_t *a_reader) {
s_reader = a_reader;
}
/*
* 函数功能:把定长源串写入框架文本缓冲
* 入口参数:a_dst - 框架给的名/值缓冲(宽 = SOE_BUF_MAX,与框架 UI_BUF_STRLEN 同值) char *
* a_src - 源串(Flash 字面量或实例缓存) const char *
* src_bytes - 源缓冲总宽(含 NUL,整块拷入以带上终止符) size_t
* 返回值:无
* 限定条件:两者非 NULL;src_bytes 超 SOE_BUF_MAX 即按上限截断拷贝
* 函数说明:不用 strcpy/sprintf(门禁 CODE-RULE-06 禁用不安全函数);本组件
* 只做"整块搬运",不做格式化——格式化属页面行表(表现层)的事
*/
static void soe_put(char *a_dst, const char *a_src, size_t src_bytes) {
if ((a_dst == NULL) || (a_src == NULL)) {
return;
}
if (src_bytes > SOE_BUF_MAX) {
src_bytes = SOE_BUF_MAX;
}
(void)memcpy(a_dst, a_src, src_bytes);
}
/*
* 函数功能:事件类型编号 → 显示名
*/
const char *ui_w_soe_type_name(uint8_t a_type) {
if (a_type >= UI_W_SOE_T_COUNT) {
return SOE_INVALID_STR;
}
return s_type_name[a_type];
}
/*
* 函数功能:元件编号 → 显示名
*/
const char *ui_w_soe_elm_name(uint8_t a_elm) {
if (a_elm >= UI_W_SOE_ELM_N) {
return SOE_INVALID_STR;
}
return s_elm_name[a_elm];
}
/*
* 函数功能:总行数(每条记录 2 行;空态 = 单条伪行"无记录")
*/
uint16_t ui_w_soe_count(ui_w_soe_t *a_inst) {
uint32_t n;
(void)a_inst;
n = (s_reader != NULL) ? (uint32_t)s_reader->count() : 0u;
if (n == 0u) {
return 1u; /* 空态伪行(页脚"共 1 页",避免"共 0 页") */
}
return (uint16_t)(n * 2u);
}
/*
* 函数功能:逐行取数(偶数行 = 时标,奇数行 = 元件 + 事件类型)
*/
void ui_w_soe_refresh(ui_w_soe_t *a_inst, uint16_t a_row, char *a_name, char *a_value) {
uint32_t n;
uint32_t rec;
uint8_t ok;
a_value[0] = '\0';
n = (s_reader != NULL) ? (uint32_t)s_reader->count() : 0u;
if (n == 0u) {
(void)memcpy(a_name, SOE_EMPTY_STR, sizeof(SOE_EMPTY_STR));
return;
}
rec = (uint32_t)a_row / 2u;
ok = 0u;
if (a_inst->cache_valid != 0u && rec == a_inst->last_rec) {
ok = 1u; /* 同一条记录的两行共用一次读盘 */
} else if (s_reader != NULL) {
if ((s_reader->get)(rec, &a_inst->rec) == 0) {
a_inst->last_rec = rec;
a_inst->cache_valid = 1u;
ok = 1u;
} else {
a_inst->cache_valid = 0u; /* 越界/读失败:本行退化为占位,不留脏缓存 */
}
}
if (ok == 0u) {
(void)memcpy(a_name, SOE_INVALID_STR, sizeof(SOE_INVALID_STR));
return;
}
if (((uint16_t)a_row % 2u) == 0u) {
/* 时标行:IV 位或未对时 → 名列 "--"、值列"时钟无效",绝不显示猜测时间 */
if ((a_inst->rec.flags & UI_W_SOE_FLG_IV) != 0u || a_inst->rec.ts[0] == '\0') {
(void)memcpy(a_name, SOE_INVALID_STR, sizeof(SOE_INVALID_STR));
(void)memcpy(a_value, SOE_NO_CLOCK_STR, sizeof(SOE_NO_CLOCK_STR));
} else {
soe_put(a_name, a_inst->rec.ts, sizeof(a_inst->rec.ts));
}
return;
}
/* 内容行:元件事件 = 元件名 + 动作/返回;非元件事件 = 类型名(名列) */
if (a_inst->rec.elm != UI_W_SOE_ELM_NONE) {
const char *elm = ui_w_soe_elm_name(a_inst->rec.elm);
soe_put(a_name, elm, strlen(elm) + 1u);
/* 只有动作/返回两类是元件级事件;其余编号(理论不该出现)仍给类型名 */
if (a_inst->rec.type == UI_W_SOE_T_ELM_ASSERT
|| a_inst->rec.type == UI_W_SOE_T_ELM_RETURN) {
soe_put(a_value, ui_w_soe_type_name(a_inst->rec.type),
strlen(ui_w_soe_type_name(a_inst->rec.type)) + 1u);
}
} else {
const char *tn = ui_w_soe_type_name(a_inst->rec.type);
soe_put(a_name, tn, strlen(tn) + 1u);
}
}
+165
View File
@@ -0,0 +1,165 @@
/*
* 模块名称:UI SOE 记录页共享组件(ui_w_soe)
* 模块功能:把"/log/soe.log 二进制定长 64 B 事件记录 → 每记录 2 行(偶数行时标、
* 奇数行"元件 + 事件类型")+ 单条读盘缓存 + 空态伪行"无记录"这套逻辑
* 收成一份,与文本记录组件 ui_w_records 同构(用户学习成本为零);
* 数据经注入式 reader 函数表取,本头**不含任何 App 头**
* 与文本记录的区别:SOE 是结构化二进制记录(时标/类型/元件号/序号/值),
* 不是"时间戳 + 描述文本",不能让文本读者混读——故另立组件与另立
* 记录视图结构(照 ui_w_records.h 的注入模式,不复用其 desc 字段)
* 显示名映射(本组件私有,属表现层):事件类型与元件都按**不透明编号**入参,
* 编号值与 App 侧枚举逐项相等(一致性由 test/host/ui_data 主机单测
* 现场核对,不是照抄文档转述);文案是显示规格,不进快照也不进 App 层
* (口径 docs/UI设计/UI_数据供给设计说明.md §8)
* 适用范围:Drivers/BSP/UI(分层方向由 scripts/check_layer_deps.py 守护)
* 说明:reader 注入点已备好但**两端均未绑定**——SOE 文件的只读 API
* (条数 / 按序号读一条)尚未在落盘侧提供,绑定与页面接线属道E 后续
* 任务;未绑定期间本组件按空态渲染("无记录"伪行),不崩不读野
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14:事件类型/元件显示名映射 +
* reader 契约;映射含未标定闭锁/复归两类新事件)
*/
#ifndef UI_W_SOE_H
#define UI_W_SOE_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/* 时间戳缓冲容量(与文本记录组件同宽:19 字符 + NUL) */
#define UI_W_SOE_TS_LEN 20u
/*
* 事件类型不透明编号:UI 侧只认编号,不认识 App 的 soe_type_t。
* ⚠ 编号值与 App/log/soe_rec.h 的 soe_rec.h 枚举逐项相等(现场核对 2026-10-25:
* 1 元件动作 / 2 元件返回 / 3 软压板退出 / 4 软压板投入 / 5 采样闭锁 /
* 6 采样恢复 / 7 未标定闭锁 / 8 标定复归 / 9 = 哨兵),漂移由主机单测拦
* (test/host/ui_data 的 TC-UIDATA-012);新增类型 = 枚举尾追加 + 这里加编号
* + 名字表加一行 + 单测加一条
*/
#define UI_W_SOE_T_NONE 0u /* UI 私有:未知/未映射编号(读失败行) */
#define UI_W_SOE_T_ELM_ASSERT 1u /* 元件动作(值 = 状态打包) */
#define UI_W_SOE_T_ELM_RETURN 2u /* 元件返回(值 = 返回前状态快照) */
#define UI_W_SOE_T_PLATE_OUT 3u /* 软压板退出 */
#define UI_W_SOE_T_PLATE_IN 4u /* 软压板投入 */
#define UI_W_SOE_T_SAMPLE_BLOCK 5u /* 采样闭锁(值 = 采样故障闭锁位图) */
#define UI_W_SOE_T_SAMPLE_RESUME 6u /* 采样恢复 */
#define UI_W_SOE_T_CAL_BLOCK 7u /* 未标定闭锁(值 = VALID 未齐的通道位图) */
#define UI_W_SOE_T_CAL_RESUME 8u /* 标定复归 */
#define UI_W_SOE_T_COUNT 9u /* 哨兵 = 名字表长度(不是有效类型) */
/* 元件编号:与保护元件枚举同序(0..8),0xFF = 非元件事件
* (一致性由同名单测 TC-UIDATA-013 现场核对) */
#define UI_W_SOE_ELM_N 9u
#define UI_W_SOE_ELM_NONE 0xFFu
/* 记录标志位:bit7 = 记录时时钟无效(时标不可信,页面须显式标记而非显示假时间) */
#define UI_W_SOE_FLG_IV 0x80u
/*
* SOE 记录视图(reader 的输出形态;字段原样搬运,不做时标换算与业务判断)
* 单条 32 B:读盘缓存按实例存,一页 8 行 = 4 条记录也只缓存当前正在渲染的那条
*/
typedef struct {
char ts[UI_W_SOE_TS_LEN]; /* "YYYY-MM-DD HH:MM:SS";IV 或未对时时为空串 */
uint8_t type; /* UI_W_SOE_T_*(未知编号原样透传,名字表回 "--") */
uint8_t elm; /* 0..UI_W_SOE_ELM_N-1 或 UI_W_SOE_ELM_NONE */
uint8_t flags; /* UI_W_SOE_FLG_IV */
uint8_t rsv; /* 预留(对齐填充) */
uint32_t seq; /* 记录序号(上电内单调) */
uint32_t value; /* 类型相关值(页面当前不展开,留作详情行) */
} ui_w_soe_rec_t;
/*
* 注入式 SOE reader(只读函数表;契约与 ui_w_records.h 的 ui_rec_reader_t 同构,
* 只是数据是二进制记录,所以取回的是整条视图而不是两段文本)
* 实现体在 App / 模拟器侧,uiTask 上下文同步调用——绘制路径上的读盘时序与
* 文本记录页一致,无新增并发面
*/
typedef struct {
/*
* 函数功能:取 SOE 记录总条数
* 入口参数:无
* 返回值:条数(0 = 空态,页面显示"无记录"伪行) uint16_t
*/
uint16_t (*count)(void);
/*
* 函数功能:读第 idx 条 SOE 记录(解码成视图)
* 入口参数:a_idx - 记录序号(0 起) uint32_t
* a_out - 输出视图(非 NULL) ui_w_soe_rec_t *
* 返回值:0 = 成功;-1 = 序号越界;-2 = 读失败或校验和不符
* (码值与文本记录 reader 同族) int
*/
int (*get)(uint32_t a_idx, ui_w_soe_rec_t *a_out);
} ui_soe_reader_t;
/* SOE 记录页实例(读盘缓存必须按实例存,理由同 ui_w_records.h 的实例说明) */
typedef struct {
uint8_t cache_valid; /* 1 = rec 是 last_rec 那条 */
uint32_t last_rec; /* 已缓存的记录序号 */
ui_w_soe_rec_t rec; /* 当前记录缓存 */
} ui_w_soe_t;
/*
* 函数功能:注入 SOE reader(启动时一次;NULL = 撤销注入,页面回空态)
* 入口参数:a_reader - 只读函数表(生命周期由注入方保证,组件只存指针)
* const ui_soe_reader_t *
* 返回值:无
* 限定条件:uiTask 启动期注入(ui_menu_init 之前,与记录 reader 同址同纪律)
*/
void ui_w_soe_bind(const ui_soe_reader_t *a_reader);
/*
* 函数功能:事件类型编号 → 显示名(表现层映射表)
* 入口参数:a_type - 类型编号(UI_W_SOE_T_*) uint8_t
* 返回值:Flash 字符串指针(恒非 NULL;未知编号回 "--",不空指针、不空白) const char *
* 限定条件:无
* 函数说明:元件类事件(动作/返回)的名字短(2 字),放**值列**;
* 非元件事件(压板/采样/标定四类,4 字)放**名列**——两列宽度不同,
* 由 ui_w_soe_refresh 按 elm 是否有效决定落在哪一列
*/
const char *ui_w_soe_type_name(uint8_t a_type);
/*
* 函数功能:元件编号 → 显示名(表现层映射表)
* 入口参数:a_elm - 元件编号(0..UI_W_SOE_ELM_N-1) uint8_t
* 返回值:Flash 字符串指针(恒非 NULL;越界或非元件事件回 "--") const char *
* 限定条件:无
* 函数说明:名称最长 4 字(零序过压/过流Ⅰ段),名列可用宽约 94 px = 7 汉字
* (口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条)
*/
const char *ui_w_soe_elm_name(uint8_t a_elm);
/*
* 函数功能:总行数(每条记录 2 行;空态 = 单条伪行"无记录")
* 入口参数:a_inst - SOE 页实例 ui_w_soe_t *
* 返回值:总行数 uint16_t
* 限定条件:无(未注入 reader 视同 0 条)
*/
uint16_t ui_w_soe_count(ui_w_soe_t *a_inst);
/*
* 函数功能:逐行取数(偶数行 = 时标,奇数行 = 元件 + 事件类型)
* 入口参数:a_inst - SOE 页实例 ui_w_soe_t *
* a_row - 全局行下标 uint16_t
* a_name - 名输出缓冲(框架 UI_BUF_STRLEN) char *
* a_value - 值输出缓冲(同上) char *
* 返回值:无
* 限定条件:页面打开中(框架绘制/按键上下文,uiTask)
* 函数说明:时标行——时钟无效(IV 位)或时标为空时名列显示 "--",
* **不显示猜测时间**(诚实性判据,同待机屏空串先例);
* 内容行——元件事件名 = 元件、值 = 动作/返回;非元件事件名 = 类型、
* 值 = 空串(值列宽约 59 px = 4 汉字,非元件事件词放这里放不下)
*/
void ui_w_soe_refresh(ui_w_soe_t *a_inst, uint16_t a_row, char *a_name, char *a_value);
#ifdef __cplusplus
}
#endif
#endif /* UI_W_SOE_H */