Files
STM32F4-Base/test/host/ui_data/ui_data_test.c
T
wandering1 4332142680 feat(ui-pages): 遥信量 33 行固定名表页接实时快照(新增 yx_dp)
批 4 主体(票 task-15;规格 docs/UI设计/页面扩充详细方案.md §3.1.3 方案 A)。V0.1 只有 6 点有
硬件来源,其余 27 点在屏上就是 "--"——这是真实状态,不是页面没做完。

页面与数据
- 33 行固定名表(Flash const)+ 动态分页(8 行/页 = 5 页)。两处命名不照抄文档简写:idx0~3
  屏上写「位置1~4」而非「YK1~4 位置」(遥信页出现 YK 前缀会被读成遥控对象,对应关系留在注释);
  idx30 写「保护闭锁」(写侧位号注释的语义即此,用编号反而丢信息)。idx4~13、idx31~32 点表未
  定义功能名,如实写点号占位(YX5…、YX32),不编造功能名。
- 值映射次序:先判无效 → 再判单/双点 → 最后看位值。无效位上的位值不代表任何事实,次序颠倒就会
  把垃圾位画成「复归」。
- 快照新增 yx_dp 双点位图(+8 B)。不教页面"前 4 点是双点":单双点是 104 点表事实(YX_DP_N /
  iec104_pts_yx_is_dp),抄进表现层就是第二真相——点表把双点段挪位或改起始下标,屏上不报错,
  只会用错词形容同一个点。写侧逐点问 is_dp() 拼位图。
- UI_DATA_YX_N(33) 与 iec104_pts_yx_count() 编译期比不了(那个 #define 在协议层 .c 内),故写侧
  留一条一次性 DBG_ERROR(约 90 B Flash,只在真漂移时打一次)。名表行数 = UI_DATA_YX_N 由数组
  宽度法编译期守卫钉住(ARMCC C99 无 _Static_assert,本仓惯用写法)。
- 补 include 一条实测坑:DBG_ERROR 是宏,漏 include "dbg_log.h" 时 ARMCC 把它当隐式声明函数放过
  编译期,错落在链接期(L6218E Undefined symbol DBG_ERROR)——报错点离根因隔了一整个链接步骤,
  已在 include 处写明「DBG_TAG 必须在 include 之前」。

一个只有像素文本断言能抓到的截断 bug
- 照抄批 1 的 *_PUT(dst, 源) 宏(长度取 sizeof(源)),但本表源是 const char *const 数组的
  **元素(指针)**,sizeof 得 4 → 「故障总」被截成「故」,而 "YX9" 恰好 4 字节看起来完全正常。
  编译器 / cppcheck / 编码钩子全不响。定位来自断言:第 2 页「过流Ⅰ段」匹配不到,同一行值列
  「复归」却在原位 = 名字被截、值没错位。修法:长度取目标缓冲容量 UI_BUF_STRLEN。同名宏、不同
  参数类别(数组 vs 指针),批 1 那处本身是对的,照抄才中招——教训写在宏定义上方。

模拟器与覆盖诚实性
- 固定帧发布 yx_dp = 0x0F,但刻意不把这几位置进 yx_valid:V0.1 无开入硬件,屏上就是 "--"。不用
  「让它显示合位/分位」的假数据填满画面,那等于让黄金参考声称本机有开关位置遥信。
- 代价如实登记:合位/分位这条词映射当前**没有任何自动化证据**(像素恒 IV;页面依赖 LVGL 进不了
  主机单测)。已写进方案 §3.1.3 落地注与契约落地进度·四,作为开入落地后的台架判据,不假装测过。

测试
- 主机单测 +2 条(TC-UIDATA-009c 双点位图原样透传、不被 valid 掩;009d 点数 33 的独立见证,注明
  绊红时要同步改哪四处)→ 85/85。
- 文本断言 +33 条(yx / yxp2 两态,含页码钉住 33/8 = 5 页的分页数学);两页像素基线单独一笔。
- font-charset 本批零新增字:名表用字全部命中既有 183 字子集——「置/故/障/总/外/部」正属批 1
  成组并入 KEEP_EXTRA 的那批字,第二次兑现(当时若按 extract 输出重写字符集,这些字早被洗掉,
  上屏是空白,而门禁只会说「字不在清单里」)。

体积(同一棵树、构建前后的实测增量)
- Code 224272→224688(+416);RO 24040→24640(+600);RW 1324→1328(+4);
  ZI 134844→135088(+244)→ Flash 约 +1020 B、RAM +248 B。
- RO 增量主要是 33 行名表与名称串,是"把 33 个真点名上屏"的代价,不是可省开销。

跳过的门禁(三条同族,同一根因):mem-numbers / mem-budget / size-gate(L0c)——台账与体积凭证
回填是 Lead 单写者,本分支不做静默处理,数值见上。
2026-10-25 15:33:04 +08:00

559 lines
25 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 数据快照层主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/UI/ui_data.c(快照存储 / 发布盖章 /
* 只读访问)与跨层契约下标空间的一致性。
*
* 被测对象:真实源文件 ../../../../Drivers/BSP/UI/ui_data.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本;本模块零平台依赖,无需任何桩)
*
* 为什么要主机侧单测(而不是只靠模拟器像素回归与台架):
* 1) 快照层是"页面看到的整个世界",它错一处全部页面一起错,而像素回归只看得到
* "当前接了线的页面"——未接线的通道(遥信 27 点、定值 40 项)错了不会让任何
* 一张参考图变红,只有把契约下标逐项钉死才照得出来;
* 2) 跨层"魔法下标"(yc 序 = adc_semantic_t、q10 序 = prot_set_id_t)是解耦的
* 代价,契约把它从运行期风险降级为编译/测试期错误的第 3 条断言就落在这里
* (同一 TU 里同时 include 两侧头,见
* docs/UI设计/UI_数据供给设计说明.md §9.4);
* 3) "首帧之前显示 -- 而不是 0.000"是诚实性判据,板子上电只有一次、台架碰不到,
* 主机侧可以随时复现。
*
* 字符串字面量一律纯 ASCII(编码铁律:断言名是打印用的,不参与渲染);
* 用例名里的通道/点号语义见各 TC 上方注释。
*
* 覆盖点(TC-UIDATA-xxx;属主登记处 = 本目录,族 TC-HOST-UIDATA):
* TC-UIDATA-001 跨层数量一致:UI_DATA_YC_N == ADC_SEM_COUNT、SET_N == PS_ID_COUNT
* TC-UIDATA-002 跨层语义序逐项一致(UA/UB/UC/UN/IA/IB/IC/IN/TEMP = 0..8,
* TEMP 恒为末槽——页面据此判"哪一路是码值域")
* TC-UIDATA-003 首帧之前:三通道读 -1、seq = 0(页面渲染 "--" 的依据)
* TC-UIDATA-004 发布后逐字段一致 + 序号盖章(入帧 seq 被忽略、由本层 +1)
* TC-UIDATA-005 连续发布 seq 单调递增;三通道各自独立计数、内容互不串台
* TC-UIDATA-006 NULL 防御:publish 返 -1 且存量不动;read 返 -2 且输出不动
* TC-UIDATA-007 init 幂等复位(发布后再 init -> 回到无首帧态)
* TC-UIDATA-008 量纲/品质常量:eng 三档互不重合且 PIN=0;六个品质位两两不相交;
* 三帧宽度在内存预算内(快照不是 LVGL 对象,只占 .bss)
* TC-UIDATA-009 遥信位集合 = YX15~18/23/31(六点);bits 与 valid 相互独立
* TC-UIDATA-010 相量槽 ph_n 原样透传(写侧已钳位,本层不再加工)
* TC-UIDATA-011 时钟字段容量足装 "YYYY-MM-DD HH:MM:SS"(19 + NUL)且不越界
* TC-UIDATA-012 SOE 事件类型不透明编号与 App/log/soe_rec.h 枚举逐项相等(含
* 未标定闭锁=7 / 标定复归=8);八类事件全部有显示名、名长不超
* 值列宽、采样与标定两组四名互不相同、越界编号回占位
* TC-UIDATA-013 SOE 元件编号与 App/algo/prot_elm.h 枚举逐项相等(0..8 +
* PE_COUNT == 表长);元件名不超名列宽、非元件/越界回占位
* TC-UIDATA-014 注入 reader 后的两行渲染:时标行/内容行分工、单条读盘缓存
* (同记录两行只读一次)、IV 时标抑制、读失败退化与不绑空态
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-25
*/
#include <stdio.h>
#include <string.h>
/* 生产源文件原样纳入(纯内存叶子,零平台依赖,直接编) */
#include "ui_data.c"
/* 契约另一侧的头:跨层下标空间一致性断言(本目录同时 include BSP 与 App 两侧头,
* 这就是契约 §9.4 第 3 条断言的落点) */
#include "adc_input.h" /* adc_semantic_t:遥测下标真源 */
#include "prot_set.h" /* prot_set_id_t:定值下标真源 */
/* 第二个被测件:SOE 显示名映射组件(道E task-14 落的表现层映射表)。
* 本组件已解除 LVGL 依赖(容量宏本地化,见其 .c 内注释),可直接编进主机单测 */
#include "ui_w_soe.c"
#include "soe_rec.h" /* soe_type_t:事件类型编号真源 */
#include "prot_elm.h" /* prot_elm_id_t:元件编号真源 */
/* ===================== 断言框架 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
} while (0)
/* 造一帧可辨识内容的实时帧(每个字段都填不同值,串台/漏拷立刻暴露) */
static void fill_rt(ui_data_rt_t *p, uint8_t seed) {
uint8_t i;
(void)memset(p, 0, sizeof(*p));
p->seq = 777u; /* 故意填非 0:本层应忽略并自己盖章 */
p->tick_ms = 12345u + seed;
p->valid = 1u;
p->eng = UI_DATA_ENG_PIN;
p->ph_n = (uint8_t)(seed % (UI_DATA_PH_MAX + 1u));
for (i = 0u; i < UI_DATA_YC_N; i++) {
p->yc[i].val = (float)i + 0.5f;
p->yc[i].qds = (uint8_t)(i + 1u);
}
for (i = 0u; i < UI_DATA_PH_MAX; i++) {
p->ph[i].val = (float)i * 2.0f;
p->ph[i].qds = (uint8_t)(0xA0u + i);
}
p->yx_bits = 0x000000010000ULL;
p->yx_valid = 0x4043C000ULL;
p->freq_valid = 1u;
p->freq_hz = 50.02f;
p->clock_ok = 1u;
(void)snprintf(p->clock, sizeof(p->clock), "%s", "2026-10-25 08:00:00");
}
/* 逐字段比对(seq 除外:本层盖章后必然与入帧不同,比对前由调用方对齐) */
static int rt_equals(const ui_data_rt_t *a, const ui_data_rt_t *b) {
if (a->tick_ms != b->tick_ms) return 0;
if (a->valid != b->valid) return 0;
if (a->eng != b->eng) return 0;
if (a->ph_n != b->ph_n) return 0;
if (a->yx_bits != b->yx_bits) return 0;
if (a->yx_valid != b->yx_valid) return 0;
if (a->freq_valid != b->freq_valid) return 0;
if (a->freq_hz != b->freq_hz) return 0;
if (a->clock_ok != b->clock_ok) return 0;
if (strcmp(a->clock, b->clock) != 0) return 0;
if (memcmp(a->yc, b->yc, sizeof(a->yc)) != 0) return 0;
if (memcmp(a->ph, b->ph, sizeof(a->ph)) != 0) return 0;
return 1;
}
/* ===================== TC-UIDATA-001 / 002:跨层下标空间 ===================== */
static void tc_contract_indices(void)
{
CHECK(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT, "TC-UIDATA-001a yc slots == ADC_SEM_COUNT");
CHECK(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT, "TC-UIDATA-001b q10 slots == PS_ID_COUNT");
CHECK((uint32_t)ADC_SEM_UA == 0u, "TC-UIDATA-002a UA == 0");
CHECK((uint32_t)ADC_SEM_UB == 1u, "TC-UIDATA-002b UB == 1");
CHECK((uint32_t)ADC_SEM_UC == 2u, "TC-UIDATA-002c UC == 2");
CHECK((uint32_t)ADC_SEM_UN == 3u, "TC-UIDATA-002d UN == 3");
CHECK((uint32_t)ADC_SEM_IA == 4u, "TC-UIDATA-002e IA == 4");
CHECK((uint32_t)ADC_SEM_IB == 5u, "TC-UIDATA-002f IB == 5");
CHECK((uint32_t)ADC_SEM_IC == 6u, "TC-UIDATA-002g IC == 6");
CHECK((uint32_t)ADC_SEM_IN == 7u, "TC-UIDATA-002h IN == 7");
/* TEMP 必须是末槽:写侧"sem < ADC_SEM_TEMP 才是电气量"的 eng 算法与页面的
* RAW 位判定都靠这个位置,插中间等于两条判据一起错位 */
CHECK((uint32_t)ADC_SEM_TEMP == (UI_DATA_YC_N - 1u), "TC-UIDATA-002i TEMP is last slot");
}
/* ===================== TC-UIDATA-003:首帧之前 ===================== */
static void tc_before_first_frame(void)
{
ui_data_rt_t rt;
ui_data_par_t par;
ui_data_info_t info;
ui_data_init();
(void)memset(&rt, 0x5A, sizeof(rt));
(void)memset(&par, 0x5A, sizeof(par));
(void)memset(&info, 0x5A, sizeof(info));
CHECK(ui_data_rt(&rt) == -1, "TC-UIDATA-003a rt read before first frame == -1");
CHECK(ui_data_par(&par) == -1, "TC-UIDATA-003b par read before first frame == -1");
CHECK(ui_data_info(&info) == -1, "TC-UIDATA-003c info read before first frame == -1");
CHECK((ui_data_rt_seq() == 0u) && (ui_data_par_seq() == 0u) && (ui_data_info_seq() == 0u),
"TC-UIDATA-003d all three seq == 0");
/* 输出缓冲必须一字不动——页面缓存被写脏会把"无数据"渲染成"有数据" */
{
const uint8_t *p = (const uint8_t *)&rt;
int dirty = 0;
size_t i;
for (i = 0u; i < sizeof(rt); i++) {
if (p[i] != 0x5Au) {
dirty = 1;
break;
}
}
CHECK(dirty == 0, "TC-UIDATA-003e output buffer untouched on failure");
}
}
/* ===================== TC-UIDATA-004 / 005:发布盖章与通道独立 ===================== */
static void tc_publish_and_seq(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_par_t par;
ui_data_par_t pg;
uint32_t seq1;
ui_data_init();
fill_rt(&src, 3u);
CHECK(ui_data_publish_rt(&src) == 0, "TC-UIDATA-004a publish returns 0");
CHECK(ui_data_rt_seq() == 1u, "TC-UIDATA-004b seq stamped 1 (incoming 777 ignored)");
(void)memset(&got, 0, sizeof(got));
CHECK(ui_data_rt(&got) == 0, "TC-UIDATA-004c readable after first publish");
got.seq = src.seq; /* 序号由本层盖章,其余字段须逐字节一致 */
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-004d all fields preserved verbatim");
/* 连续发布:seq 单调 +1,内容取最新一帧 */
fill_rt(&src, 9u);
(void)ui_data_publish_rt(&src);
seq1 = ui_data_rt_seq();
CHECK(seq1 == 2u, "TC-UIDATA-005a second publish bumps seq to 2");
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-005b latest frame is the one served");
/* 三通道独立:发 par 不动 rt 的 seq,也不动 rt 内容 */
(void)memset(&par, 0, sizeof(par));
par.valid = 1u;
par.checksum = 0xABCDu;
par.q10[0] = PROT_SET_Q10(10.0);
par.q10[UI_DATA_SET_N - 1u] = PROT_SET_Q10(55.5);
(void)ui_data_publish_par(&par);
CHECK((ui_data_par_seq() == 1u) && (ui_data_rt_seq() == seq1),
"TC-UIDATA-005c par publish does not touch rt seq");
(void)memset(&pg, 0, sizeof(pg));
CHECK((ui_data_par(&pg) == 0) && (pg.checksum == 0xABCDu)
&& (pg.q10[0] == PROT_SET_Q10(10.0))
&& (pg.q10[UI_DATA_SET_N - 1u] == PROT_SET_Q10(55.5)),
"TC-UIDATA-005d par stored verbatim (Q10 not rescaled here)");
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-005e par publish does not pollute rt");
}
/* ===================== TC-UIDATA-006:NULL 防御 ===================== */
static void tc_null_guard(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_init();
fill_rt(&src, 1u);
(void)ui_data_publish_rt(&src);
CHECK(ui_data_publish_rt(NULL) == -1, "TC-UIDATA-006a publish(NULL) == -1");
CHECK(ui_data_rt_seq() == 1u, "TC-UIDATA-006b failed publish keeps seq");
CHECK(ui_data_rt(NULL) == -2, "TC-UIDATA-006c read(NULL) == -2");
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-006d stored frame still intact");
}
/* ===================== TC-UIDATA-007:init 幂等复位 ===================== */
static void tc_init_idempotent(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_init();
fill_rt(&src, 2u);
(void)ui_data_publish_rt(&src);
(void)ui_data_publish_rt(NULL);
ui_data_init();
ui_data_init(); /* 重复调用仍幂等 */
CHECK(ui_data_rt_seq() == 0u, "TC-UIDATA-007a seq reset to 0 by init");
CHECK(ui_data_rt(&got) == -1, "TC-UIDATA-007b back to no-frame state after init");
}
/* ===================== TC-UIDATA-008:量纲与品质常量 ===================== */
static void tc_constants(void)
{
CHECK((UI_DATA_ENG_PIN == 0u) && (UI_DATA_ENG_SEC == 1u) && (UI_DATA_ENG_PRI == 2u),
"TC-UIDATA-008a eng levels are 0/1/2 (pin-V / secondary / primary)");
CHECK((UI_DATA_Q_IV | UI_DATA_Q_NT | UI_DATA_Q_OV | UI_DATA_Q_SB | UI_DATA_Q_BL
| UI_DATA_Q_RAW) == 0x3Fu, "TC-UIDATA-008b six quality bits fill low 6 bits");
CHECK((UI_DATA_Q_IV & UI_DATA_Q_NT) == 0u, "TC-UIDATA-008c IV/NT disjoint");
CHECK((UI_DATA_Q_OV & UI_DATA_Q_SB) == 0u, "TC-UIDATA-008d OV/SB disjoint");
CHECK((UI_DATA_Q_BL & UI_DATA_Q_RAW) == 0u, "TC-UIDATA-008e BL/RAW disjoint");
/* 内存预算护栏(docs/UI设计/UI_数据供给设计说明.md §10):快照是 .bss 里的
* 定长结构,不是 LVGL 对象;一旦有人往里塞大数组或指针,这里先响 */
CHECK(sizeof(ui_data_rt_t) <= 256u, "TC-UIDATA-008f rt frame within budget (<=256B)");
CHECK(sizeof(ui_data_par_t) <= 200u, "TC-UIDATA-008g par frame within budget (<=200B)");
CHECK(sizeof(ui_data_info_t) <= 128u, "TC-UIDATA-008h info frame within budget (<=128B)");
}
/* ===================== TC-UIDATA-009:遥信位集合与独立性 ===================== */
static void tc_yx_points(void)
{
/* V0.1 有硬件来源的遥信点集合(YX15~18/23/31,1 基序号):与 104 点表同源,
* 写侧 App/app/ui_data_feed.c 与 simulator/main.c 固定帧都用同一组位号。
* 任何一处改动都会让本条红,逼着改动方同时更新页面名表与位图镜像注释 */
const uint64_t valid_expect = ((uint64_t)1u << 14u) | ((uint64_t)1u << 15u) |
((uint64_t)1u << 16u) | ((uint64_t)1u << 17u) |
((uint64_t)1u << 22u) | ((uint64_t)1u << 30u);
ui_data_rt_t src;
ui_data_rt_t got;
const uint64_t dp_expect = 0x0FULL; /* V0.1 双点段 = bit0~3(YX1~4 开关位置类) */
ui_data_init();
fill_rt(&src, 5u);
src.yx_bits = 0u; /* 全部点位"分/未动作" */
src.yx_valid = valid_expect;
src.yx_dp = dp_expect;
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK(got.yx_valid == valid_expect, "TC-UIDATA-009a valid mask == YX15/16/17/18/23/31");
CHECK(got.yx_bits == 0u, "TC-UIDATA-009b bits and valid are independent fields");
/* 双点位图必须原样透传:页面按它选词(合位/分位 vs 动作/复归),快照层若做"归一化"
* 或按 valid 掩一次,页面就会把双点当单点形容——同一条事实两处解释必然漂 */
CHECK(got.yx_dp == dp_expect, "TC-UIDATA-009c dp mask passed through unmasked");
/* 点表与名表共用点数(1 基 IOA 1~33)。这里是**有意**再抄一遍字面量:测试的职责是
* 独立见证,协议层那个 #define 在 .c 内不可见,改点数时必须有人被绊一下——绊到后
* 要同时改:App/protocol/iec104/iec104_pts.c 的 YX_N、ui_data.h 的 UI_DATA_YX_N、
* 页面名表 ui_page_yx.c(有编译期数组守卫兜着)、本条 */
CHECK((uint32_t)UI_DATA_YX_N == 33u, "TC-UIDATA-009d point count is 33 (YX_N witness)");
}
/* ===================== TC-UIDATA-010:相量槽透传 ===================== */
static void tc_phasor_slots(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
uint8_t i;
ui_data_init();
fill_rt(&src, UI_DATA_PH_MAX);
src.ph_n = UI_DATA_PH_MAX;
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK(got.ph_n == UI_DATA_PH_MAX, "TC-UIDATA-010a ph_n passed through unchanged");
i = 0u;
while ((i < UI_DATA_PH_MAX) && (got.ph[i].val == src.ph[i].val)) {
i++;
}
CHECK(i == UI_DATA_PH_MAX, "TC-UIDATA-010b every phasor slot kept its value");
}
/* ===================== TC-UIDATA-011:时钟字段容量 ===================== */
static void tc_clock_field(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
char probe[UI_DATA_CLOCK_LEN];
(void)snprintf(probe, sizeof(probe), "%s", "2026-10-25 08:00:00");
CHECK(strlen(probe) == 19u, "TC-UIDATA-011a clock string is 19 chars");
CHECK(UI_DATA_CLOCK_LEN == 20u, "TC-UIDATA-011b capacity is 19 + NUL");
ui_data_init();
fill_rt(&src, 7u);
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK((got.clock_ok == 1u) && (strcmp(got.clock, "2026-10-25 08:00:00") == 0)
&& (got.clock[UI_DATA_CLOCK_LEN - 1u] == '\0'),
"TC-UIDATA-011c full string stored, last byte still NUL");
}
/* ====== TC-UIDATA-012 / 013:SOE 不透明编号与显示名映射(现场核对枚举头) ====== */
/* 名字表版式护栏:名列 7 汉字 = 21 字节、值列 4 汉字 = 12 字节(UTF-8 3 字节/字);
* 超宽只会被屏幕裁掉,不会被任何像素门禁报出来,所以在这里钉死
* (宽度口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条) */
static int all_names_fit(const char *const *tbl, unsigned n, size_t max_bytes) {
unsigned i;
for (i = 0u; i < n; i++) {
if (tbl[i] == NULL) return 0;
if (strlen(tbl[i]) == 0u) return 0;
if (strlen(tbl[i]) > max_bytes) return 0;
}
return 1;
}
static void tc_soe_codes(void)
{
/* 编号与 App/log/soe_rec.h 枚举逐项相等(不是照抄文档转述,是 include 真头对表) */
CHECK((uint8_t)SOE_T_ELM_ASSERT == UI_W_SOE_T_ELM_ASSERT, "TC-UIDATA-012a type ELM_ASSERT");
CHECK((uint8_t)SOE_T_ELM_RETURN == UI_W_SOE_T_ELM_RETURN, "TC-UIDATA-012b type ELM_RETURN");
CHECK((uint8_t)SOE_T_PLATE_OUT == UI_W_SOE_T_PLATE_OUT, "TC-UIDATA-012c type PLATE_OUT");
CHECK((uint8_t)SOE_T_PLATE_IN == UI_W_SOE_T_PLATE_IN, "TC-UIDATA-012d type PLATE_IN");
CHECK((uint8_t)SOE_T_SAMPLE_BLOCK == UI_W_SOE_T_SAMPLE_BLOCK, "TC-UIDATA-012e type SAMPLE_BLOCK");
CHECK((uint8_t)SOE_T_SAMPLE_RESUME == UI_W_SOE_T_SAMPLE_RESUME, "TC-UIDATA-012f type SAMPLE_RESUME");
CHECK((uint8_t)SOE_T_CAL_BLOCK == UI_W_SOE_T_CAL_BLOCK, "TC-UIDATA-012g type CAL_BLOCK == 7");
CHECK((uint8_t)SOE_T_CAL_RESUME == UI_W_SOE_T_CAL_RESUME, "TC-UIDATA-012h type CAL_RESUME == 8");
CHECK((uint8_t)SOE_T_COUNT == UI_W_SOE_T_COUNT, "TC-UIDATA-012i sentinel COUNT == 9");
CHECK(SOE_ELM_NONE == UI_W_SOE_ELM_NONE, "TC-UIDATA-012j ELM_NONE == 0xFF");
CHECK(SOE_FLG_IV == UI_W_SOE_FLG_IV, "TC-UIDATA-012k FLG_IV == 0x80");
/* 八类事件全部有名(新加类型忘配名 = 屏上"--",像素门禁照不出来) */
{
uint8_t t;
int ok = 1;
size_t longest = 0u;
for (t = 1u; t < UI_W_SOE_T_COUNT; t++) {
const char *nm = ui_w_soe_type_name(t);
if (nm == NULL) {
ok = 0; /* 契约上不可达(映射表恒返回串),留作防御分支 */
} else {
if ((strcmp(nm, "--") == 0) || (strlen(nm) == 0u)) {
ok = 0;
}
if (strlen(nm) > longest) {
longest = strlen(nm);
}
}
}
CHECK(ok == 1, "TC-UIDATA-012l every type has a display name");
/* 宽度按**实际画到哪一列**判:1/2 两类(动作/返回)画在值列(4 汉字 = 12 字节),
* 3..8 这些非元件事件的类型名画在名列(7 汉字 = 21 字节)。一刀切按 12 判会把
* "未标定闭锁"这类正常名字判成违规,一刀切按 21 判又放过了值列超宽——分列才咬手 */
CHECK((strlen(ui_w_soe_type_name(UI_W_SOE_T_ELM_ASSERT)) <= 12u)
&& (strlen(ui_w_soe_type_name(UI_W_SOE_T_ELM_RETURN)) <= 12u),
"TC-UIDATA-012m element event names fit value column (<=4 CJK)");
CHECK(longest <= 21u, "TC-UIDATA-012m2 type names fit name column (<=7 CJK)");
CHECK(strcmp(ui_w_soe_type_name(0u), "--") == 0, "TC-UIDATA-012n code 0 -> placeholder");
CHECK(strcmp(ui_w_soe_type_name(UI_W_SOE_T_COUNT), "--") == 0,
"TC-UIDATA-012o out-of-range -> placeholder");
/* 类型 5/6(采样)与 7/8(标定)四者必须互不相同:同名 = 抄表时串行,
* 现场排障会把"未标定闭锁"读成"采样闭锁" */
CHECK((strcmp(ui_w_soe_type_name(UI_W_SOE_T_SAMPLE_BLOCK),
ui_w_soe_type_name(UI_W_SOE_T_CAL_BLOCK)) != 0)
&& (strcmp(ui_w_soe_type_name(UI_W_SOE_T_SAMPLE_RESUME),
ui_w_soe_type_name(UI_W_SOE_T_CAL_RESUME)) != 0)
&& (strcmp(ui_w_soe_type_name(UI_W_SOE_T_CAL_BLOCK),
ui_w_soe_type_name(UI_W_SOE_T_CAL_RESUME)) != 0),
"TC-UIDATA-012p sample/cal lock-resume names are distinct");
}
}
static void tc_soe_elms(void)
{
CHECK(PE_OC1 == 0, "TC-UIDATA-013a PE_OC1 == 0");
CHECK(PE_OC2 == 1, "TC-UIDATA-013b PE_OC2 == 1");
CHECK(PE_OC3 == 2, "TC-UIDATA-013c PE_OC3 == 2");
CHECK(PE_OL == 3, "TC-UIDATA-013d PE_OL == 3");
CHECK(PE_OV == 4, "TC-UIDATA-013e PE_OV == 4");
CHECK(PE_UV == 5, "TC-UIDATA-013f PE_UV == 5");
CHECK(PE_3U0 == 6, "TC-UIDATA-013g PE_3U0 == 6");
CHECK(PE_OF == 7, "TC-UIDATA-013h PE_OF == 7");
CHECK(PE_UF == 8, "TC-UIDATA-013i PE_UF == 8");
CHECK((uint32_t)PE_COUNT == UI_W_SOE_ELM_N, "TC-UIDATA-013j PE_COUNT == UI_W_SOE_ELM_N");
CHECK(all_names_fit(s_elm_name, UI_W_SOE_ELM_N, 21u) == 1,
"TC-UIDATA-013k element names fit name column (<=7 CJK)");
CHECK(strcmp(ui_w_soe_elm_name(UI_W_SOE_ELM_NONE), "--") == 0,
"TC-UIDATA-013l non-element id -> placeholder");
CHECK(strcmp(ui_w_soe_elm_name(UI_W_SOE_ELM_N), "--") == 0,
"TC-UIDATA-013m out-of-range element -> placeholder");
}
/* ====== TC-UIDATA-014:注入 reader 后的两行渲染与单条缓存 ====== */
static uint32_t s_get_calls;
static ui_w_soe_rec_t s_fake_rec;
static uint16_t fake_count(void)
{
return 3u;
}
static int fake_get(uint32_t a_idx, ui_w_soe_rec_t *a_out)
{
s_get_calls++;
if (a_idx >= 3u) {
return -1;
}
(void)memcpy(a_out, &s_fake_rec, sizeof(*a_out));
a_out->seq = a_idx;
return 0;
}
static const ui_soe_reader_t s_fake_reader = { fake_count, fake_get };
static void tc_soe_render(void)
{
ui_w_soe_t inst;
char name[UI_W_SOE_TS_LEN + 16u];
char value[UI_W_SOE_TS_LEN + 16u];
(void)memset(&inst, 0, sizeof(inst));
(void)memset(&s_fake_rec, 0, sizeof(s_fake_rec));
s_reader = NULL;
CHECK(ui_w_soe_count(&inst) == 1u, "TC-UIDATA-014a unbound reader -> empty pseudo row");
ui_w_soe_bind(&s_fake_reader);
CHECK(ui_w_soe_count(&inst) == 6u, "TC-UIDATA-014b 3 records -> 6 rows");
/* 元件事件:名列 = 元件,值列 = 动作/返回 */
(void)strcpy(s_fake_rec.ts, "2026-10-25 08:00:00");
s_fake_rec.type = UI_W_SOE_T_ELM_ASSERT;
s_fake_rec.elm = (uint8_t)PE_OC1;
s_fake_rec.flags = 0u;
s_get_calls = 0u;
(void)memset(name, 0, sizeof(name));
(void)memset(value, 0, sizeof(value));
ui_w_soe_refresh(&inst, 0u, name, value);
CHECK((name[0] != 0) && (strlen(name) == 19u) && (value[0] == 0),
"TC-UIDATA-014c ts row shows full timestamp");
ui_w_soe_refresh(&inst, 1u, name, value);
CHECK((strlen(name) == 12u) && (strlen(value) == 6u) && (s_get_calls == 1u),
"TC-UIDATA-014d content row uses cached record (single disk read)");
/* IV 时标:不显示猜测时间,值列给"时钟无效" */
s_fake_rec.flags = UI_W_SOE_FLG_IV;
(void)memset(&inst, 0, sizeof(inst));
ui_w_soe_refresh(&inst, 0u, name, value);
CHECK((strcmp(name, "--") == 0) && (strlen(value) == 12u),
"TC-UIDATA-014e IV timestamp suppressed, marked in value column");
/* 非元件事件:类型名进名列,值列留空。15 字节 = "未标定闭锁" 5 汉字 × 3 B;
* 主机测试的字面量必须纯 ASCII(无 BOM 文件里中文串会吞引号),所以这里只按
* 字节数 + "不是占位串"判,串内容由 simulator 侧的 TEXTS 断言在像素上核 */
s_fake_rec.flags = 0u;
s_fake_rec.type = UI_W_SOE_T_CAL_BLOCK;
s_fake_rec.elm = UI_W_SOE_ELM_NONE;
(void)memset(&inst, 0, sizeof(inst));
ui_w_soe_refresh(&inst, 3u, name, value);
CHECK((strlen(name) == 15u) && (name[0] != '-') && (value[0] == 0),
"TC-UIDATA-014f non-element row: 5-CJK type name in name column");
/* 越界记录号:读失败退化为占位,不留脏缓存 */
ui_w_soe_refresh(&inst, 998u, name, value);
CHECK(strcmp(name, "--") == 0, "TC-UIDATA-014g failed read -> placeholder, no stale cache");
ui_w_soe_bind(NULL);
}
/* ===================== main ===================== */
int main(void)
{
printf("=== UI Data Snapshot Tests ===\n");
printf("-- contract index space --\n");
tc_contract_indices();
printf("-- before first frame --\n");
tc_before_first_frame();
printf("-- publish and seq --\n");
tc_publish_and_seq();
printf("-- null guard --\n");
tc_null_guard();
printf("-- init idempotent --\n");
tc_init_idempotent();
printf("-- constants --\n");
tc_constants();
printf("-- yx points --\n");
tc_yx_points();
printf("-- phasor slots --\n");
tc_phasor_slots();
printf("-- clock field --\n");
tc_clock_field();
printf("-- soe codes and names --\n");
tc_soe_codes();
printf("-- soe elements --\n");
tc_soe_elms();
printf("-- soe render --\n");
tc_soe_render();
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}