From 8cbc7bc6b66fcf82a6e06b87195e8e97b0a4b44d Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 25 Oct 2026 09:56:10 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(ui-font):=20=E5=AD=97=E5=BA=93?= =?UTF-8?q?=E8=A1=A5=20SOE=20=E6=98=BE=E7=A4=BA=E5=90=8D=E7=94=A8=E5=AD=97?= =?UTF-8?q?=20=E9=87=87/=E6=A0=B7/=E6=81=A2/=E6=95=88=EF=BC=88179=E2=86=92?= =?UTF-8?q?183=20=E5=AD=97=20/=20274=E2=86=92278=20=E5=AD=97=E5=BD=A2?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 单独提交:按字库独占规矩,缺字改动自成一笔,不与功能改动混笔。 用字来源(下一笔提交的 ui_w_soe 显示名映射):事件类型"采样闭锁/ 采样恢复/标定复归"与时标行的"时钟无效"。缺字上屏是空白方块、无日志 无报错,只能靠像素回归与门禁照出来。 流程(font-regen 四步,未跳步): 1. extract_cn 扫出 144 字 < 现清单 179 字:35 字源码已不用但不属 KEEP_EXTRA 那 4 字(异/常/片/芯)——它们是 2026-09 窗口固件写进 /log/selftest.log 定长记录的历史用字,跨固件持久落盘,静态扫描照不出来。故按并集回填, 本笔只加 4 字、不做瘦身:瘦身等于让历史审计记录永久渲染空白且不可逆, 收益(约 35 字 × 27 B RO)与代价完全不对称(同门禁头注"刻意保留 ≠ 未知冗余"的同一判定逻辑)。这 35 字要不要入 KEEP_EXTRA 已回报 Lead 裁决,不在本道写域。 2. make_font.py 重生成 ui_font_cn_12.c:coverage OK,183 字全覆盖;▶/Ⅰ/Ⅱ/Ⅲ 补回字形的单码位 cmap 条目未被宽稀疏 range 吞掉(首命中守卫通过)。 3. 像素回归:菜单 8 状态 + 内容页 11 页黄金参考逐字节不变, simulator/tests/ref 无 diff → 本笔不产生基线变化,未用 --update 刷参考。 4. 构建:@build 0 错误 0 警告(RO-data 21396→21504,+108 B,1bpp 12px 每字 约 27 B 含字形描述符);模拟器 0 警告。 计数字段同步(同一事实只改数字,不重复抄流程): - docs/UI设计/UI_中文字体生成与配置.md:产物说明 + 体积档(含 4 字实测增量) - docs/UI设计/UI_界面制作流程.md C3 行 - docs/UI设计/UI_设计架构.md 默认字体行 - simulator/README.md 已知坑第 4 条 自查:BOM/CR/U+FFFD/字面量 ASCII 全树核对通过;全量钩子 40 项通过, 仅台账两条待回填(数字随下一笔的 ui-data 提交一起由 Lead 回填)。 gate: only mem-budget/mem-numbers pending lead ledger backfill --- Drivers/BSP/UI/ui_font_cn_12.c | 152 ++++++++++++++++----------- docs/UI设计/UI_中文字体生成与配置.md | 4 +- docs/UI设计/UI_界面制作流程.md | 2 +- docs/UI设计/UI_设计架构.md | 2 +- simulator/README.md | 2 +- ui_cn_chars.txt | 2 +- 6 files changed, 94 insertions(+), 70 deletions(-) diff --git a/Drivers/BSP/UI/ui_font_cn_12.c b/Drivers/BSP/UI/ui_font_cn_12.c index d31d858f..0fcdf358 100644 --- a/Drivers/BSP/UI/ui_font_cn_12.c +++ b/Drivers/BSP/UI/ui_font_cn_12.c @@ -599,6 +599,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x9f, 0xf0, 0x0, 0x4, 0x4a, 0x45, 0x42, 0xc7, 0xc0, + /* U+6062 "恢" */ + 0x24, 0x4, 0x80, 0xbf, 0xda, 0xa, 0x49, 0x49, + 0x6a, 0xb1, 0x54, 0x28, 0x85, 0x28, 0xc9, 0x12, + 0x10, + /* U+606F "息" */ 0x8, 0xf, 0xf9, 0x1, 0x3f, 0xe4, 0x4, 0xff, 0x90, 0x13, 0xfe, 0x4, 0xa, 0x49, 0x42, 0xc7, @@ -664,6 +669,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xbe, 0x94, 0x52, 0x8a, 0x91, 0x23, 0xea, 0x46, 0x30, + /* U+6548 "效" */ + 0x21, 0x2, 0x23, 0xf4, 0x0, 0xf4, 0xa4, 0x8a, + 0xa0, 0x51, 0x4a, 0x10, 0x85, 0x11, 0x15, 0x41, + 0x10, + /* U+6570 "数" */ 0x11, 0xa, 0xa0, 0x44, 0x7e, 0xf3, 0x24, 0xb2, 0xa5, 0x51, 0xa, 0xf9, 0x49, 0x10, 0xc5, 0x65, @@ -735,6 +745,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x9a, 0xad, 0x14, 0x22, 0x84, 0x20, 0x8a, 0x16, 0x30, + /* U+6837 "样" */ + 0x24, 0x44, 0x50, 0xbf, 0xf8, 0x82, 0x10, 0xcf, + 0x9c, 0x45, 0x8, 0x2f, 0xe4, 0x20, 0x84, 0x10, + 0x80, + /* U+68C0 "检" */ 0x21, 0x4, 0x20, 0x8a, 0x7a, 0x22, 0x82, 0xcf, 0x9c, 0x5, 0x12, 0x29, 0x44, 0xa8, 0x92, 0x17, @@ -1058,6 +1073,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xfe, 0xe5, 0x7, 0xf8, 0xd5, 0x1a, 0xaf, 0x8d, 0x20, 0xa7, 0xf4, 0x82, 0xbf, 0xd6, 0x9, 0x80, + /* U+91C7 "采" */ + 0x3, 0xcf, 0x80, 0x20, 0x22, 0x22, 0x8, 0x8, + 0x3f, 0xf8, 0x70, 0x15, 0x4, 0x93, 0x11, 0x82, + 0x0, + /* U+91CD "重" */ 0x3, 0xcf, 0x80, 0x10, 0x7f, 0xf4, 0x44, 0xff, 0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f, @@ -1344,14 +1364,14 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1677, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1694, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1711, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1728, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1744, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1757, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1774, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1790, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1807, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1821, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1838, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1728, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1745, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1762, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1778, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1791, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1808, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1824, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1841, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1855, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1872, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1889, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1361,10 +1381,10 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1957, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1974, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1991, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2008, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2024, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2041, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2058, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2008, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2025, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2042, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2059, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2075, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2092, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2109, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1374,18 +1394,18 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2177, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2194, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2211, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2228, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2242, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2259, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2276, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2291, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2308, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2325, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2341, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2358, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2228, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2245, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2262, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2279, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2293, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2310, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2327, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 2342, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2359, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2376, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2392, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2409, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2459, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1394,10 +1414,10 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2510, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2527, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2544, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2561, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2575, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2592, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2609, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2561, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2578, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2595, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2612, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 2626, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2643, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2660, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1422,30 +1442,34 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2983, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3000, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3017, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3034, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3050, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3067, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3084, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3034, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3051, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3068, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3085, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3101, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3118, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3135, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3152, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3169, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3184, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3199, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3216, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3233, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3250, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3169, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3186, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3203, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3220, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3237, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3252, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 3267, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3284, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3300, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3317, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3334, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3351, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3284, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3301, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3318, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3335, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3352, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3368, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3385, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 3386, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 3388, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ + {.bitmap_index = 3385, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3402, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3419, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3436, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3453, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 3454, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 3456, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ }; /*--------------------- @@ -1460,22 +1484,22 @@ static const uint16_t unicode_list_1[] = { 0x3297, 0x32a8, 0x32cf, 0x32ea, 0x357e, 0x35d0, 0x35e0, 0x362b, 0x37ad, 0x37b6, 0x39f0, 0x39f7, 0x39f8, 0x3a3a, 0x3a3e, 0x3a66, 0x3c92, 0x3cd8, 0x3d14, 0x3d2f, 0x3d46, 0x3d96, 0x3da2, 0x3daf, - 0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f0f, 0x40d7, - 0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, 0x4245, 0x4247, - 0x4249, 0x436d, 0x43e5, 0x4410, 0x4414, 0x4480, 0x4485, 0x4496, - 0x44de, 0x45a0, 0x45a8, 0x45bf, 0x45ca, 0x45cc, 0x461f, 0x4685, - 0x46a7, 0x46c1, 0x4760, 0x48c1, 0x49c0, 0x49c1, 0x4a1b, 0x4a55, - 0x4ae2, 0x4b82, 0x4be1, 0x4beb, 0x4c28, 0x4ca5, 0x4cc9, 0x4f59, - 0x50e7, 0x50e8, 0x5156, 0x53c8, 0x53d5, 0x53ec, 0x5594, 0x55ab, - 0x56a1, 0x570c, 0x570e, 0x57da, 0x5872, 0x58ab, 0x598f, 0x59cc, - 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, 0x5d94, 0x5df1, 0x5e0e, 0x608a, - 0x614f, 0x6217, 0x66ec, 0x6765, 0x6a06, 0x6a44, 0x6a4f, 0x6a50, - 0x6a5e, 0x6a82, 0x6a8f, 0x6a97, 0x6bbf, 0x6c2a, 0x6c93, 0x6e0f, - 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, 0x6e74, 0x6ea0, 0x6eba, - 0x6ef3, 0x6f05, 0x6f88, 0x6fed, 0x706d, 0x706f, 0x733f, 0x7345, - 0x73a1, 0x73b9, 0x741f, 0x748d, 0x7494, 0x7498, 0x74d2, 0x7504, - 0x753c, 0x7596, 0x7602, 0x7715, 0x7731, 0x7828, 0x792c, 0x7d78, - 0xddac, 0xddba + 0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f02, 0x3f0f, + 0x40d7, 0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, 0x4245, + 0x4247, 0x4249, 0x436d, 0x43e5, 0x43e8, 0x4410, 0x4414, 0x4480, + 0x4485, 0x4496, 0x44de, 0x45a0, 0x45a8, 0x45bf, 0x45ca, 0x45cc, + 0x461f, 0x4685, 0x46a7, 0x46c1, 0x46d7, 0x4760, 0x48c1, 0x49c0, + 0x49c1, 0x4a1b, 0x4a55, 0x4ae2, 0x4b82, 0x4be1, 0x4beb, 0x4c28, + 0x4ca5, 0x4cc9, 0x4f59, 0x50e7, 0x50e8, 0x5156, 0x53c8, 0x53d5, + 0x53ec, 0x5594, 0x55ab, 0x56a1, 0x570c, 0x570e, 0x57da, 0x5872, + 0x58ab, 0x598f, 0x59cc, 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, 0x5d94, + 0x5df1, 0x5e0e, 0x608a, 0x614f, 0x6217, 0x66ec, 0x6765, 0x6a06, + 0x6a44, 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, 0x6a8f, 0x6a97, 0x6bbf, + 0x6c2a, 0x6c93, 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, + 0x6e74, 0x6ea0, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, 0x6fed, 0x7067, + 0x706d, 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, 0x741f, 0x748d, + 0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, 0x7602, 0x7715, + 0x7731, 0x7828, 0x792c, 0x7d78, 0xddac, 0xddba }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1486,12 +1510,12 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { - .range_start = 9654, .range_length = 1, .glyph_id_start = 274, + .range_start = 9654, .range_length = 1, .glyph_id_start = 278, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { .range_start = 8544, .range_length = 56763, .glyph_id_start = 96, - .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 178, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 182, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/docs/UI设计/UI_中文字体生成与配置.md b/docs/UI设计/UI_中文字体生成与配置.md index 3d6572e6..3e4bd9ca 100644 --- a/docs/UI设计/UI_中文字体生成与配置.md +++ b/docs/UI设计/UI_中文字体生成与配置.md @@ -55,7 +55,7 @@ scanned 13 files; unique non-ASCII chars: 78 -> D:\Code\DTU Code\STM32F4-Base\ui_cn_chars.txt ``` -产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 179 字,含 ▶/,/: 等符号;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,源码未用前由 font-charset 门禁记 WARN 放行),可留作字符集记录。 +产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 183 字,含 ▶/,/: 等符号;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,同日道E 落 SOE 记录显示名映射时新用 采/样/恢/效 4 字;清单里源码暂未在用的字由 font-charset 门禁记 WARN 放行),可留作字符集记录。 --- @@ -152,7 +152,7 @@ OK 58192 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c 1. **1bpp 细笔画发虚**:宋体在 1bpp 下可能丢像素。若上板/客户端中文偏淡,把第 4 节 `--bpp` 改为 `2` 重生成即可(子集小,体积可接受)。 2. **字符集变更**:以后新增中文菜单项,须重跑第 3、4 节(重新提取 `ui_cn_chars.txt` 并生成),否则新字显示为方框。 3. **编码**:UI 源文件须为 **UTF-8(带 BOM)**,LVGL 按 UTF-8 解码中文串;字体 `.c` 为纯 ASCII,无编码风险。当前 Keil 已加 Misc Controls 抑制项,无需额外 `--locale=english`。 -4. **字体体积**:12px + 1bpp + 179 字(含 ASCII/符号,共 274 字形)源文件 ≈ 56.8KB;ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 +4. **字体体积**:12px + 1bpp + 183 字(含 ASCII/符号,共 278 字形)源文件 ≈ 57.8KB;ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0;同日 SOE 显示名再用 4 字实测 `RO-data` +108 B、RAM 0——1bpp 12px 一个字约 27 B,含字形描述符);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 5. **换字体**:黑体用 `simhei.ttf`(单文件免抽取);其余字体同理替换 `--font` 路径后重生成。 --- diff --git a/docs/UI设计/UI_界面制作流程.md b/docs/UI设计/UI_界面制作流程.md index 6b608a25..21de40a1 100644 --- a/docs/UI设计/UI_界面制作流程.md +++ b/docs/UI设计/UI_界面制作流程.md @@ -48,7 +48,7 @@ flowchart TD |---|---|---|---| | C1 | 屏幕 | 160×160,1bpp 单色(`LV_COLOR_FORMAT_I1`),无物理屏(远端客户端显示) | `ui_port.h` | | C2 | 字体 | **唯一**:`ui_font_cn_12`(宋体 12px 1bpp)。汉字 12px/字,ASCII 6px/字,行高 13px。需要其他字号 = 大工程(新字体 + 双端 `lv_conf.h`),默认禁止 | `ui_font_cn_12.c` | -| C3 | 字符集 | 子集字体(当前 179 汉字/符号 + ASCII 0x20-0x7E,共 274 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | +| C3 | 字符集 | 子集字体(当前 183 汉字/符号 + ASCII 0x20-0x7E,共 278 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | | C4 | LVGL 内存池 | 16KB(`LV_MEM_SIZE`,2026-10-15 由 40KB 收缩;实测峰值 6,568 B = 池的 40%);任一时刻只放"当前导航路径 + 当前内容页" | `Lib/lv_conf.h` | | C5 | 文本 | 一律静态字符串(`lv_label_set_text_static`),禁止动态字符串/strdup | 架构文档 | | C6 | 编码 | `Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h` 为 **UTF-8 带 BOM**;改动只用 Edit 工具,**禁止** PowerShell/cmd 整文件重写(U+FFFD 不可逆) | AGENTS.md 铁律 | diff --git a/docs/UI设计/UI_设计架构.md b/docs/UI设计/UI_设计架构.md index ab8cdac7..126e9ae7 100644 --- a/docs/UI设计/UI_设计架构.md +++ b/docs/UI设计/UI_设计架构.md @@ -38,7 +38,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 | LVGL 内存池 | 16 KB(`LV_MEM_SIZE`,2026-10-15 由 40 KB 收缩;实测峰值 6,568 B = 池的 40%) | `Lib/lv_conf.h` | | 刷新周期 | 10 ms(`ui_port_step(10)` + `osDelay(10)`) | `ui_task.c` | | 推流端口 | 7003(TCP,与 DTU-RemoteLCD 客户端对齐) | `ui_net_sink.h` | -| 默认字体 | `ui_font_cn_12`(12px 1bpp,179 汉字/符号 + ASCII 0x20–0x7E,共 274 字形) | `Lib/lv_conf.h` | +| 默认字体 | `ui_font_cn_12`(12px 1bpp,183 汉字/符号 + ASCII 0x20–0x7E,共 278 字形) | `Lib/lv_conf.h` | | uiTask 栈 | 16384 words(BelowNormal) | `Src/freertos.c` | ### 1.4 硬约束(改 UI 必须遵守) diff --git a/simulator/README.md b/simulator/README.md index 58174cc8..9d8719da 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -262,7 +262,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化 - `LV_USE_THEME_MONO = 1` 且 `sim_port_init` 中调用了 `lv_theme_mono_init` - 主循环调用了 `lv_refr_now(NULL)`(PARTIAL 模式保证首帧) - 没有重复设置 tick(SDL 驱动内部已 `lv_tick_set_cb`,**不要**再 `SDL_AddTimer` + `lv_tick_inc`) -4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 179 汉字/符号 + ASCII,共 274 字形),缺字渲染为空白。 +4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 183 汉字/符号 + ASCII,共 278 字形),缺字渲染为空白。 补字流程见 `docs/UI设计/UI_中文字体生成与配置.md`(`tools/font_gen/` 两步脚本)。 5. **改了 UI 代码但模拟器没变化**:确认改的是 `Drivers/BSP/UI/ui_menu.c`(共享原件)而非本目录副本(本目录无副本),然后重新 `cmake --build`。 6. **编码**:UI 源码为 UTF-8(带 BOM),MSVC 已加 `/utf-8`。**不要**用 PowerShell `Set-Content` 等重写含中文的文件(编码铁律,见根目录 `AGENTS.md`)。 diff --git a/ui_cn_chars.txt b/ui_cn_chars.txt index 86655786..e9c8f79e 100644 --- a/ui_cn_chars.txt +++ b/ui_cn_chars.txt @@ -1 +1 @@ -ⅠⅡⅢ▶一三主事二交件位低作保信值入公共关出分制前动化区压参双取口号合启告回地址型复外子字存定实密已常年序度延异式强归当录态总息户手投抖护报据接控掩操故数整无日时显最月期未本板查标校检模欠次死段求波流测消清温点片版状用电界直看码硬确示秒程端第类系线终维网置自芯荷行装警认讯记设询误请负越跳软载输过运近返退通道遥部配重量钟钥锁错长闭间闸防除障零面页频馈验默,: +ⅠⅡⅢ▶一三主事二交件位低作保信值入公共关出分制前动化区压参双取口号合启告回地址型复外子字存定实密已常年序度延异式强归当录态总恢息户手投抖护报据接控掩操故效数整无日时显最月期未本板查标校样检模欠次死段求波流测消清温点片版状用电界直看码硬确示秒程端第类系线终维网置自芯荷行装警认讯记设询误请负越跳软载输过运近返退通道遥部配采重量钟钥锁错长闭间闸防除障零面页频馈验默,: From 201d713226322792bd26a63c7c491d686d0b0911 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 25 Oct 2026 09:56:22 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(ui-data):=20ui=5Fdata=20=E4=B8=89?= =?UTF-8?q?=E9=80=9A=E9=81=93=E5=BF=AB=E7=85=A7=20+=20ui=5Fdata=5Ffeed=20?= =?UTF-8?q?=E8=A3=85=E9=85=8D=20+=20SOE=20=E6=98=BE=E7=A4=BA=E5=90=8D?= =?UTF-8?q?=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按 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(中文只进注释)。 --- .pre-commit-config.yaml | 17 + App/app/ui_data_feed.c | 443 ++++++++++++++++++++++++ App/app/ui_data_feed.h | 46 +++ App/task/ui_task.c | 13 + Drivers/BSP/UI/ui_data.c | 196 +++++++++++ Drivers/BSP/UI/ui_data.h | 273 +++++++++++++++ Drivers/BSP/UI/ui_w_soe.c | 196 +++++++++++ Drivers/BSP/UI/ui_w_soe.h | 165 +++++++++ MDK-ARM/STM32F407-Demo.uvprojx | 15 + docs/UI设计/UI_数据供给设计说明.md | 37 +- scripts/check_registration.py | 1 + scripts/id_registry.json | 22 ++ simulator/CMakeLists.txt | 13 +- simulator/main.c | 97 ++++++ test/host/ui_data/run_unit_test.py | 49 +++ test/host/ui_data/ui_data_test.c | 538 +++++++++++++++++++++++++++++ 16 files changed, 2118 insertions(+), 3 deletions(-) create mode 100644 App/app/ui_data_feed.c create mode 100644 App/app/ui_data_feed.h create mode 100644 Drivers/BSP/UI/ui_data.c create mode 100644 Drivers/BSP/UI/ui_data.h create mode 100644 Drivers/BSP/UI/ui_w_soe.c create mode 100644 Drivers/BSP/UI/ui_w_soe.h create mode 100644 test/host/ui_data/run_unit_test.py create mode 100644 test/host/ui_data/ui_data_test.c diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e8162f1..b1f00562 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -467,6 +467,23 @@ repos: files: ^(Drivers/BSP/GD5F2GQ5UE/nand_ftl\.(c|h)|Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue\.h|Lib/dhara/(map|journal|nand|error|bytes)\.(c|h)|test/host/nand_port/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false + # 7z) UI 数据快照层(道E task-14)主机侧单元测试(2026-10-25 加) + # 背景:ui_data 是"页面看到的整个世界",但 ui-golden 只覆盖已接线的页面—— + # 遥信 27 点、定值 40 项、遥测未接线的通道错了不会让任何参考图变红。本钩子 + # 用 MSVC 离线编译真实 ui_data.c 与 ui_w_soe.c(#include 纳入,零桩),把两件事 + # 钉死:① 快照存储语义(首帧前读 -1 / 序号由本层盖章 / 三通道互不串台 / + # NULL 防御);② 跨层不透明下标与显示名映射的**现场一致性**——测试单元在同一 + # TU 里同时 include BSP 侧与 App/algo、App/log 真头,逐项比对 adc_semantic_t / + # prot_set_id_t / soe_type_t / prot_elm_id_t(解耦换来的"魔法下标"风险,口径 + # docs/UI设计/UI_数据供给设计说明.md §9.4;SOE 类型含未标定闭锁=7/复归=8)。 + # files 覆盖被测两件 + 编号真源四头 + 页面消费面(ui_content_page/ui_menu*)+ harness。 + - id: ui-data-unit-test + name: UI 数据快照层单测(主机侧) + entry: python test/host/ui_data/run_unit_test.py + language: system + files: ^(Drivers/BSP/UI/(ui_data|ui_w_soe|ui_w_records|ui_content_page|ui_menu)\.(c|h)|App/app/ui_data_feed\.(c|h)|App/algo/(adc_input|prot_set|prot_elm)\.(c|h)|App/log/soe_rec\.(c|h)|test/host/ui_data/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + # 8) 测试套件开关门禁(2026-10-01 加) # 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件, # 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里 diff --git a/App/app/ui_data_feed.c b/App/app/ui_data_feed.c new file mode 100644 index 00000000..49c83913 --- /dev/null +++ b/App/app/ui_data_feed.c @@ -0,0 +1,443 @@ +/* + * 模块名称:UI 数据组装器(ui_data_feed) + * 模块功能:App → UI 数据通道的写侧——按节拍从采集/标定/相量/测频/保护/协议/墙钟 + * 各模块取数,组装成 docs/UI设计/UI_数据供给设计说明.md §4 契约的三帧 + * (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h + * 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**,不含任何布局、 + * 格式化与 LVGL 调用(显示规格全在页面行表,同文档 §8) + * 量纲诚实性:eng 字段按**真值**填——docs/任务规划/数据可信.md §2.6 把承诺边界定在 + * "引脚电压",故未做两点标定前 eng = UI_DATA_ENG_PIN(值 = ADC 引脚电压 V, + * 不是端子二次值);电气 8 路 adc_cal_flags() 的 VALID 全置位才升 + * UI_DATA_ENG_SEC。TEMP 按决策不参与线性标定 → 恒带 UI_DATA_Q_RAW, + * 页面据此不挂单位(三态口径同文档 §4.2) + * 适用平台:STM32F407ZGTx(PC 模拟器不链接本文件,见 ui_data_feed.h 端接法) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-14) + */ + +/* 头文件包含区 */ +#include + +#include "cmsis_os.h" /* osKernelGetTickCount:与 adc_task 同一 ms 时基 */ + +#include "ui_data_feed.h" + +/* 下层(Drivers/BSP):快照存侧 + 两个端口/用户名常量来源(App → BSP 是合法方向) */ +#include "ui_data.h" +#include "ui_net_sink.h" /* UI_NET_SINK_PORT:远程显示(RemoDispBus)监听端口 */ +#include "ftp_cfg.h" /* FTP_CFG_USER_MAX / ftp_cfg_get() */ + +/* 同层(App)数据源 */ +#include "adc_input.h" /* ADC_SEM_COUNT / adc_input_raw(TEMP 码值) */ +#include "adc_cal.h" /* adc_cal_flags:VALID/CLIPPED 三态可用性 */ +#include "phasor.h" /* 基波/谐波有效值(fAmSQ) */ +#include "freq.h" /* 测频读数与新鲜度 */ +#include "prot_elm.h" /* 元件动作态(遥信位图来源) */ +#include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */ +#include "sys_clock.h" /* 墙钟字符串 */ +#include "iec104_serv.h" /* IEC104_SERV_PORT:实际监听端口(非协议层同名常量) */ + +/* 私有宏定义区 */ + +/* 实时帧与信息帧发布节拍(ms);参数帧只在定值校验和变化时发布 + * (节拍依据 docs/UI设计/UI_数据供给设计说明.md §5.2) */ +#define FEED_RT_PERIOD_MS 200u + +/* 遥信位号:**与 App/protocol/iec104/iec104_pts.c 板端源的位图同源**(该文件把 + * 位号写死在函数体内、未导出,UI 侧不得 include 协议头,故在此镜像一份)。 + * 变更纪律:104 点表位号改动时必须同步这里 + 页面名表 + 主机一致性测试 + * (test/host/ui_data 的位图断言)。V0.1 有硬件来源的只有以下 6 位, + * 其余 27 位恒无效(开入未实现);过压/欠压/过频/低频四个元件在 + * V0.1 点表里没有对应遥信点,故本函数不产出。 */ +#define FEED_YX_BIT_FAULT_ALL 14u /* YX15 故障总(任一电压/电流段或零序动作) */ +#define FEED_YX_BIT_OC1 15u /* YX16 过流 I 段 */ +#define FEED_YX_BIT_OC2 16u /* YX17 过流 II 段 */ +#define FEED_YX_BIT_OC3 17u /* YX18 过流 III 段 */ +#define FEED_YX_BIT_OL 22u /* YX23 过载(过负荷) */ +#define FEED_YX_BIT_LOCK 30u /* YX31 保护闭锁(定值校验和自检失败) */ +/* YX1~4(bit0..3)是开关位置类双点点,V0.1 无开入硬件 → 恒 0 且不在有效位图内, + * 因此本文件不为它们定义常量(定义了也不参与任何判定 = 看着像接了线其实没有) */ +/* 有效位图:YX15~18/23/31(与 104 点表 YX_VALID_MASK 同集合) */ +#define FEED_YX_VALID_MASK ((((uint64_t)1u << FEED_YX_BIT_FAULT_ALL) | \ + ((uint64_t)1u << FEED_YX_BIT_OC1) | \ + ((uint64_t)1u << FEED_YX_BIT_OC2) | \ + ((uint64_t)1u << FEED_YX_BIT_OC3) | \ + ((uint64_t)1u << FEED_YX_BIT_OL) | \ + ((uint64_t)1u << FEED_YX_BIT_LOCK))) + +/* + * 装置身份(版本类):仓库内**没有**产品级版本登记表,值沿用页面既有显示值 + * (docs/UI设计/内容页架构.md 的装置信息行表现值),有正式口径后只改本段一处。 + * ⚠ 软件校验不抄参考项目遗留的 "4454":构建期没有 CRC 生成步骤,如实显示 "--" + * (处置口径 docs/UI设计/页面扩充详细方案.md §3.7 第 5 行) + */ +#define FEED_MODEL "F30" +#define FEED_SW_VER "SV0.010" +#define FEED_HW_VER "HW0.010" +#define FEED_SW_SUM "--" + +/* 编译期一致性守卫(ARMCC C99 无 _Static_assert,用项目惯用的数组宽度法, + * 先例 App/cfg/cfg_file.c 与 Drivers/BSP/UI/ui_menu_tab.h): + * 快照下标空间与各 App 枚举一旦漂移,编译即失败,不留到运行期 + * (契约 docs/UI设计/UI_数据供给设计说明.md §9.4 第 1/2 条) */ +typedef char feed_chk_yc_n_t[(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT) ? 1 : -1]; +typedef char feed_chk_set_n_t[(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT) ? 1 : -1]; +typedef char feed_chk_fuser_t[(UI_DATA_FUSER_LEN >= FTP_CFG_USER_MAX) ? 1 : -1]; + +/* 静态变量定义区 */ + +/* 组装缓冲:三帧串行填充,共用一块存储(取最大者 = 参数帧),省两份 RAM */ +static union { + ui_data_rt_t rt; + ui_data_par_t par; + ui_data_info_t info; +} s_scr; + +/* 语义通道 → 基波相量实例下标缓存(-1 = 该通道无基波实例 → 该路发 IV); + * 首帧惰性建表(相量实例表是 ROM 表,运行期不变,建一次即可) */ +static int8_t s_yc_phasor[ADC_SEM_COUNT]; +static uint8_t s_map_done; + +/* 发布节拍与参数变化检测状态 */ +static uint32_t s_last_rt_ms; +static uint32_t s_par_sum; +static uint8_t s_par_sum_valid; +static uint8_t s_first_done; + +/* 函数定义区 */ + +/* + * 函数功能:定长缓冲内拷贝字符串(跨层快照只允许定长数组,禁指针) + * 入口参数:p_dst - 目标缓冲(结构体内数组首地址) char * + * dst_len - 目标容量(含 NUL) size_t + * a_src - 源串(Flash 字面量或上层缓冲) const char * + * 返回值:无 + * 限定条件:三者非 NULL 且 dst_len > 0;截断时保证 NUL 结尾 + * 函数说明:不用 strcpy/sprintf(门禁 CODE-RULE-06 禁用不安全函数), + * 超长**静默截断**——快照字段本就是定长显示串,截断只会少显示尾部, + * 不会越界;本函数每 200 ms 调用个位数次,不为此加日志 + */ +static void feed_copy_str(char *p_dst, size_t dst_len, const char *a_src) { + size_t n; + + if ((p_dst == NULL) || (a_src == NULL) || (dst_len == 0u)) { + return; + } + n = strlen(a_src); + if (n > (dst_len - 1u)) { + n = dst_len - 1u; + } + (void)memcpy(p_dst, a_src, n); + p_dst[n] = '\0'; +} + +/* + * 函数功能:建立"语义通道 → 基波相量实例"映射(首帧一次) + * 入口参数:无 + * 返回值:无 + * 限定条件:uiTask 上下文,ui_data_feed_tick 首帧前调用 + * 函数说明:只认 harm == 1 的实例;一个语义通道有多个基波实例时取**首个** + * (现状 k_inst[] 每通道最多一个基波,取首个即唯一解)。 + * UB/UC/IB/IC/IN 无实例(V0.1 硬件削顶,N-18)→ 留 -1,该路发 IV + */ +static void feed_map_phasor(void) { + uint8_t n; + uint8_t i; + + for (i = 0u; i < (uint8_t)ADC_SEM_COUNT; i++) { + s_yc_phasor[i] = -1; + } + n = phasor_inst_count(); + for (i = 0u; i < n; i++) { + const phasor_inst_t *p = phasor_inst(i); + + if ((p == NULL) || (p->harm != 1u) || ((uint8_t)p->sem >= (uint8_t)ADC_SEM_COUNT)) { + continue; + } + if (s_yc_phasor[(uint8_t)p->sem] < 0) { + s_yc_phasor[(uint8_t)p->sem] = (int8_t)i; + } + } + s_map_done = 1u; +} + +/* + * 函数功能:算整帧量纲口径 eng + * 入口参数:无 + * 返回值:UI_DATA_ENG_PIN 或 UI_DATA_ENG_SEC uint8_t + * 限定条件:无(纯读) + * 函数说明:**全或无**——电气 8 路(TEMP 除外,它不参与线性标定)的 + * ADC_CAL_F_VALID 全置位才升二次值。取全或无是因为整帧共用一个 eng: + * 只要还有一路是引脚电压,页面挂"二次值"就会把那一路的读数误标 + * (宁可整体显示"未标定",也不给部分真值贴真值标签) + */ +static uint8_t feed_eng(void) { + uint8_t sem; + + for (sem = 0u; sem < (uint8_t)ADC_SEM_TEMP; sem++) { + if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_VALID) == 0u) { + return UI_DATA_ENG_PIN; + } + } + return UI_DATA_ENG_SEC; +} + +/* + * 函数功能:组装遥信位图(bit i = 第 i 点"合/动作") + * 入口参数:无 + * 返回值:位图 uint64_t + * 限定条件:uiTask 上下文(读 protectTask 写的元件状态快照,读侧拷贝语义, + * 与 App/protocol/iec104/iec104_pts.c 板端源同形) + * 函数说明:位号镜像本文件 FEED_YX_BIT_* 一组宏的注释(含同步纪律); + * 双点遥信 YX1~4 无硬件 → 恒 0 且不在有效位图内 + */ +static uint64_t feed_yx_bits(void) { + prot_elm_state_t st; + uint64_t bits = 0u; + uint8_t fault_all; + + fault_all = 0u; + prot_elm_state(PE_OC1, &st); + if (st.asserted != 0u) { + bits |= ((uint64_t)1u << FEED_YX_BIT_OC1); + fault_all = 1u; + } + prot_elm_state(PE_OC2, &st); + if (st.asserted != 0u) { + bits |= ((uint64_t)1u << FEED_YX_BIT_OC2); + fault_all = 1u; + } + prot_elm_state(PE_OC3, &st); + if (st.asserted != 0u) { + bits |= ((uint64_t)1u << FEED_YX_BIT_OC3); + fault_all = 1u; + } + prot_elm_state(PE_3U0, &st); + if (st.asserted != 0u) { + fault_all = 1u; + } + if (fault_all != 0u) { + bits |= ((uint64_t)1u << FEED_YX_BIT_FAULT_ALL); + } + prot_elm_state(PE_OL, &st); + if (st.asserted != 0u) { + bits |= ((uint64_t)1u << FEED_YX_BIT_OL); + } + if (prot_set_locked() != 0u) { + bits |= ((uint64_t)1u << FEED_YX_BIT_LOCK); + } + return bits; +} + +/* + * 函数功能:组装实时量快照(遥测 9 路 + 相量实例槽 + 遥信 + 频率 + 墙钟) + * 入口参数:p_snap - 目标帧(调用方已清零) ui_data_rt_t * + * tick_ms - 发布时刻(tick 域) uint32_t + * 返回值:无 + * 限定条件:uiTask 上下文;映射表已建(feed_map_phasor) + * 函数说明:取数口径与 104 上送同源不同路——本函数读**相量使用区**(1 ms 刷新、 + * 双区原子交换),不做死区与平均;遥测链快照(docs/任务规划/数据可信.md + * §4.5 的 meas 快照)落地后,本函数改成读该快照即可,页面零改动 + * (这正是本层存在的意义:换数据源只改这里) + */ +static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) { + uint32_t now_ep; + uint8_t sem; + char ts[UI_DATA_CLOCK_LEN]; + + p_snap->tick_ms = tick_ms; + p_snap->valid = 1u; + p_snap->eng = feed_eng(); + + /* 遥测:语义通道序的基波有效值(采样环已按 adc_cal 折算 → 当前为引脚电压域) */ + for (sem = 0u; sem < (uint8_t)ADC_SEM_COUNT; sem++) { + int8_t pi = s_yc_phasor[sem]; + + p_snap->yc[sem].val = 0.0f; + p_snap->yc[sem].qds = 0u; + + if (sem == (uint8_t)ADC_SEM_TEMP) { + /* 温度:不参与线性标定,透传转换器码值并打 RAW 位(页面不得挂单位) */ + int32_t code = 0; + + p_snap->yc[sem].qds = UI_DATA_Q_RAW; + if (adc_input_raw((adc_semantic_t)sem, &code) == 0) { + p_snap->yc[sem].val = (float)code; + } else { + p_snap->yc[sem].qds |= UI_DATA_Q_IV; + } + continue; + } + if (pi < 0) { + /* 无基波实例 = V0.1 该通道无可用采样源(削顶) → 无效 */ + p_snap->yc[sem].qds = UI_DATA_Q_IV; + } else { + phasor_t ph; + int rc = phasor_get((uint8_t)pi, &ph); + + if (rc == 0) { + p_snap->yc[sem].val = ph.fAmSQ; /* 有效值口径(系数已折 sqrt2/N) */ + } else if (rc == -2) { + p_snap->yc[sem].qds = UI_DATA_Q_NT; /* 未攒满一周波 */ + } else { + p_snap->yc[sem].qds = UI_DATA_Q_IV; + } + } + if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_CLIPPED) != 0u) { + p_snap->yc[sem].qds |= UI_DATA_Q_OV; /* 硬件削顶:交流量不可信 */ + } + } + + /* 相量实例槽:含谐波(Ia 二次/五次),交流量页的 Ia2/Ia5 行走这条带 */ + { + uint8_t n = phasor_inst_count(); + + if (n > UI_DATA_PH_MAX) { + n = UI_DATA_PH_MAX; /* 超出槽上限的实例本帧不发布(现 5 实例,余 3 槽) */ + } + p_snap->ph_n = n; + for (sem = 0u; sem < UI_DATA_PH_MAX; sem++) { + p_snap->ph[sem].val = 0.0f; + p_snap->ph[sem].qds = UI_DATA_Q_IV; + } + for (sem = 0u; sem < n; sem++) { + phasor_t ph; + + if (phasor_get(sem, &ph) == 0) { + p_snap->ph[sem].val = ph.fAmSQ; + p_snap->ph[sem].qds = 0u; + } else { + p_snap->ph[sem].qds = UI_DATA_Q_NT; + } + } + } + + p_snap->yx_bits = feed_yx_bits(); + p_snap->yx_valid = FEED_YX_VALID_MASK; + + /* 测频:读数新鲜度单独走 freq_valid()(幅值门 + 收敛),两者任一不成立 + * 页面即显示 "--"——eng=0 期间幅值门恒失效,读数本就不可消费,如实反映 */ + { + float hz = 0.0f; + + p_snap->freq_valid = 0u; + p_snap->freq_hz = 0.0f; + if (freq_get(&hz) == 0) { + p_snap->freq_hz = hz; + if (freq_valid() != 0u) { + p_snap->freq_valid = 1u; + } + } + } + + /* 墙钟:UINT32_MAX 是 sys_clock_get() 的失败哨兵 → clock_ok=0、串清空 */ + p_snap->clock_ok = 0u; + p_snap->clock[0] = '\0'; + now_ep = sys_clock_get(); + if ((now_ep != UINT32_MAX) && (sys_clock_get_str(ts, sizeof(ts)) != NULL)) { + feed_copy_str(p_snap->clock, sizeof(p_snap->clock), ts); + p_snap->clock_ok = 1u; + } +} + +/* + * 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图) + * 入口参数:p_snap - 目标帧(调用方已清零) ui_data_par_t * + * sum - 本次发布的 prot_set_checksum() 值 uint32_t + * 返回值:无 + * 限定条件:uiTask 上下文(读侧;写者 = iec104Task 的 set_sealed 与装载路径, + * 读到的永远是"值区与封存一致"的完整快照) + * 函数说明:只搬 Q10 原值不换算小数位——换算属表现层 + * (docs/UI设计/UI_数据供给设计说明.md §8) + */ +static void feed_fill_par(ui_data_par_t *p_snap, uint32_t sum) { + uint8_t i; + + p_snap->checksum = sum; + p_snap->valid = 1u; + p_snap->locked = prot_set_locked(); + p_snap->bad_mask = prot_set_bad_mask(); + /* 越界位图非 0 = 上一次整表装载/提交有越界项(prot_set_bad_mask 的既有口径), + * 页面据此反显;为 0 时无意义位图不参与显示 */ + p_snap->bad_valid = (p_snap->bad_mask != 0u) ? 1u : 0u; + for (i = 0u; i < (uint8_t)PS_ID_COUNT; i++) { + p_snap->q10[i] = prot_set_get((prot_set_id_t)i); + } +} + +/* + * 函数功能:组装装置信息快照(身份 + 运行时长 + 通讯参数) + * 入口参数:p_snap - 目标帧(调用方已清零) ui_data_info_t * + * tick_ms - 当前 tick(换算 uptime_s 用) uint32_t + * 返回值:无 + * 限定条件:uiTask 上下文 + * 函数说明:页面不得自己 include 协议/配置头取这些值,一律经本帧下发 + * (docs/UI设计/页面扩充详细方案.md §3.5"值必须在 App 侧取"); + * 公共地址 CA 不在此下发——固件里 CA 是逐帧从 ASDU 头解析的, + * 没有集中常量可取,待 104 侧给出查询点后按扩展规则追加字段 + */ +static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) { + const ftp_cfg_t *p_ftp = ftp_cfg_get(); + + p_snap->valid = 1u; + p_snap->uptime_s = tick_ms / 1000u; + p_snap->port_104 = (uint16_t)IEC104_SERV_PORT; + p_snap->port_disp = (uint16_t)UI_NET_SINK_PORT; + feed_copy_str(p_snap->model, sizeof(p_snap->model), FEED_MODEL); + feed_copy_str(p_snap->sw_ver, sizeof(p_snap->sw_ver), FEED_SW_VER); + feed_copy_str(p_snap->hw_ver, sizeof(p_snap->hw_ver), FEED_HW_VER); + feed_copy_str(p_snap->build_date, sizeof(p_snap->build_date), __DATE__); + feed_copy_str(p_snap->sw_sum, sizeof(p_snap->sw_sum), FEED_SW_SUM); + if (p_ftp != NULL) { + feed_copy_str(p_snap->ftp_user, sizeof(p_snap->ftp_user), p_ftp->user); + } else { + p_snap->ftp_user[0] = '\0'; + } +} + +/* + * 函数功能:装配节拍入口——实时帧与信息帧满 200 ms 发布一次,参数帧仅在校验和 + * 变化时发布;首轮立即发布(免启动初期出现一闪的 "--") + * 入口参数:无 + * 返回值:无 + * 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms) + * 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较; + * 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径) + */ +void ui_data_feed_tick(void) { + uint32_t now = osKernelGetTickCount(); + uint32_t sum = prot_set_checksum(); + uint8_t due_rt; + + if (s_map_done == 0u) { + feed_map_phasor(); + } + + due_rt = 0u; + if ((s_first_done == 0u) || ((uint32_t)(now - s_last_rt_ms) >= FEED_RT_PERIOD_MS)) { + due_rt = 1u; + s_first_done = 1u; + s_last_rt_ms = now; + } + if (due_rt != 0u) { + (void)memset(&s_scr, 0, sizeof(s_scr)); + feed_fill_rt(&s_scr.rt, now); + (void)ui_data_publish_rt(&s_scr.rt); + + (void)memset(&s_scr, 0, sizeof(s_scr)); + feed_fill_info(&s_scr.info, now); + (void)ui_data_publish_info(&s_scr.info); + } + + /* 参数帧:校验和门控——定值变化是低频事件,不随 200 ms 白拷 184 B + * (docs/UI设计/UI_数据供给设计说明.md §5.2) */ + if ((s_par_sum_valid == 0u) || (sum != s_par_sum)) { + (void)memset(&s_scr, 0, sizeof(s_scr)); + feed_fill_par(&s_scr.par, sum); + (void)ui_data_publish_par(&s_scr.par); + s_par_sum = sum; + s_par_sum_valid = 1u; + } +} diff --git a/App/app/ui_data_feed.h b/App/app/ui_data_feed.h new file mode 100644 index 00000000..9ff8fcc0 --- /dev/null +++ b/App/app/ui_data_feed.h @@ -0,0 +1,46 @@ +/* + * 模块名称:UI 数据组装器(ui_data_feed) + * 模块功能:App → UI 数据通道的"写侧"——按节拍从采集/算法/保护/协议/时钟各模块 + * 取数,组装成 docs/UI设计/UI_数据供给设计说明.md §4 契约的三帧 + * (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h + * 的发布 API 推给 UI 层;本模块是快照的**唯一填充者** + * 适用范围:App/app(装配层:可同时 include 两侧头,方向仍是上层依赖下层, + * scripts/check_layer_deps.py 只拦"Drivers/BSP 目录反向 include App 目录") + * 线程约定:uiTask 上下文,主循环每轮调 ui_data_feed_tick() 一次;与页面读写同 + * 上下文串行 → 无锁(docs/UI设计/UI_数据供给设计说明.md §5.1)。 + * ⚠ 不得在 adcTask/protectTask/netTask 里调用本模块 + * 端接法:MCU = App/task/ui_task.c 主循环;PC 模拟器**不链接本文件**(模拟器不跑 + * 固件装配链,快照由 simulator/main.c 直接发布固定帧保证像素回归可复现, + * 见 docs/UI设计/UI_数据供给设计说明.md §9.2;模拟器豁免登记在 + * scripts/check_registration.py 的 SIM_EXEMPT) + * 作者:王建锋 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 王建锋 创建(道E task-14) + */ + +#ifndef UI_DATA_FEED_H +#define UI_DATA_FEED_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * 函数功能:装配节拍入口——实时帧与信息帧满 200 ms 发布一次,参数帧仅在校验和 + * 变化时发布;首轮立即发布(免启动后出现一闪的 "--") + * 入口参数:无 + * 返回值:无 + * 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms) + * 函数说明:本函数每轮代价 = 一次 tick 差值比较 + 一次校验和比较;真正的取数 + * 与组装只在到期时发生(docs/UI设计/UI_数据供给设计说明.md §5.2)。 + * 内部零阻塞、零堆分配、零日志(日志每条约 90 B Flash + 216 B 栈, + * 200 ms 周期打不起) + */ +void ui_data_feed_tick(void); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_DATA_FEED_H */ diff --git a/App/task/ui_task.c b/App/task/ui_task.c index d67cb6c0..53429c13 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -31,6 +31,8 @@ #include "ui_net_sink.h" #include "ui_standby.h" #include "ui_w_records.h" /* task-6 记录 reader 注入点(UI 页面不再 include sys_log.h) */ +#include "ui_data.h" /* task-14 快照存侧(BSP):本任务只负责先复位再喂帧 */ +#include "ui_data_feed.h" /* task-14 数据装配节拍入口(App/app,UI 侧唯一发布者) */ #include "sys_log.h" #include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */ #include "sys_clock.h" @@ -143,6 +145,13 @@ void ui_task_func(void *argument) { /* 记录 reader 注入:必须在 ui_menu_init 之前(记录页在此前不可能被打开, * 但契约把"启动期先绑定"写死;未绑定窗口组件按空态防御——见 ui_w_records.h) */ ui_w_records_bind(&s_rec_reader); + /* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据" + * 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */ + ui_data_init(); + /* 首帧立即发布:待机屏/菜单屏在启动早期就可能被远端推流打开,等 200 ms + * 会闪一下 "--"(同文档 §5.3);本调用在 ui_menu_init 之前,页面尚未建立, + * 发布路径不触 LVGL,安全 */ + ui_data_feed_tick(); DBG_INFO("UI task: ui_menu_init enter"); if (ui_menu_init() != 0) { DBG_ERROR("UI menu init: FAIL"); @@ -190,6 +199,10 @@ void ui_task_func(void *argument) { lat_mark(&tm_loop); tm_loop_valid = 1U; #endif + /* 数据装配节拍(200 ms 内部自分频,未到期时只比一次 tick): + * 排在渲染之前,本轮绘制的就是本轮刚发布的那一帧 + * (UI_数据供给设计说明.md §5.1 单写者同上下文的前提就在这儿) */ + ui_data_feed_tick(); ui_port_step(UI_TASK_TICK_MS); /* 心跳**先于**推流往返发出(判读口径 docs/子系统设计/看门狗设计说明.md §11.3):UI 心跳测的是 * "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后, diff --git a/Drivers/BSP/UI/ui_data.c b/Drivers/BSP/UI/ui_data.c new file mode 100644 index 00000000..a723b008 --- /dev/null +++ b/Drivers/BSP/UI/ui_data.c @@ -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 + +#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; +} diff --git a/Drivers/BSP/UI/ui_data.h b/Drivers/BSP/UI/ui_data.h new file mode 100644 index 00000000..4ecd6be5 --- /dev/null +++ b/Drivers/BSP/UI/ui_data.h @@ -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 + +/* + * 契约版本:结构体字段只允许尾部追加,追加即升此值并在写侧填默认值 + * (禁止删除或重排字段——页面按名字取,重排是静默错位; + * 口径 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 */ diff --git a/Drivers/BSP/UI/ui_w_soe.c b/Drivers/BSP/UI/ui_w_soe.c new file mode 100644 index 00000000..e8c564c8 --- /dev/null +++ b/Drivers/BSP/UI/ui_w_soe.c @@ -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 + +#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); + } +} diff --git a/Drivers/BSP/UI/ui_w_soe.h b/Drivers/BSP/UI/ui_w_soe.h new file mode 100644 index 00000000..427f31d7 --- /dev/null +++ b/Drivers/BSP/UI/ui_w_soe.h @@ -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 + +/* 时间戳缓冲容量(与文本记录组件同宽: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 */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index b324cb42..b0db1c23 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1027,6 +1027,11 @@ 1 ..\App\algo\freq.c + + ui_data_feed.c + 1 + ..\App\app\ui_data_feed.c + @@ -3536,6 +3541,16 @@ 1 ..\Drivers\BSP\UI\ui_style.c + + ui_data.c + 1 + ..\Drivers\BSP\UI\ui_data.c + + + ui_w_soe.c + 1 + ..\Drivers\BSP\UI\ui_w_soe.c + diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md index d2cc9012..bb2405a1 100644 --- a/docs/UI设计/UI_数据供给设计说明.md +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -147,6 +147,7 @@ flowchart TB #define UI_DATA_Q_OV 0x04u /* 越限:超过量程/削顶 */ #define UI_DATA_Q_SB 0x08u /* 取代值 */ #define UI_DATA_Q_BL 0x10u /* 闭锁 */ +#define UI_DATA_Q_RAW 0x20u /* 原始码值域:val 不是工程量而是转换器码值(TEMP 用) */ typedef struct { float val; /* 数值;口径由 ui_data_rt_t.eng 决定 */ @@ -157,7 +158,8 @@ typedef struct { uint32_t seq; /* 发布序号(单调递增;页面据此判"是否需重建行缓存") */ uint32_t tick_ms; /* 发布时刻(tick 域,差值法可比) */ uint8_t valid; /* 0 = 尚无首帧(页面显示 "--") */ - uint8_t eng; /* 0 = 原始码值(未标定)/ 1 = 二次值 / 2 = 一次值 */ + uint8_t eng; /* 量纲口径:0 = ADC 引脚电压(现状)/ 1 = 二次值 / + * 2 = 一次值(预留)——真值语义见下方"要点"第 1 条 */ ui_data_yc_t yc[UI_DATA_YC_N]; uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */ uint64_t yx_valid; /* 遥信有效位图:bit i = 1 该点有硬件来源 */ @@ -170,7 +172,7 @@ typedef struct { **要点**: -- **`eng` 字段是诚实性的关键**:V0.1 未标定,`val` 是 ADC 码值(千级)。页面据此显示"未标定"角标或直接显示码值,**不会把码值当伏特**。标定落地后 `eng` 改 1,页面自动按工程量显示,**无代码改动**。 +- **`eng` 字段是诚实性的关键**(【2026-10-25 落地注】本条原写"0 = 原始码值(未标定)、`val` 是 ADC 码值(千级)",那是标定落地前的旧口径,已按现场改真值语义):标定层 `adc_cal` 的 ROM 默认系数按**引脚侧 LSB**定死(TPAFE 152.59 µV/码、STM32 805.66 µV/码),采样环折算出来的 `val` 现在是 **ADC 引脚电压 V**——既不是千级码值,也不是端子二次值。承诺边界的依据是 `docs/任务规划/数据可信.md` §2.6:这一波只承诺到"引脚电压","端子二次值"要等变比确认与两点标定。所以页面文案是 `eng=0` → "未标定"、`eng=1` → "二次值",而不是"码值";升档条件写在 `App/app/ui_data_feed.c` 的 `feed_eng()`——**电气 8 路 `adc_cal_flags()` 的 VALID 全置位才升**(整帧共用一个 `eng`,只要还有一路是引脚电压就不能给整帧贴"二次值"标签,宁可整体保守)。TEMP 不参与线性标定 → 该路恒带 `UI_DATA_Q_RAW`,页面据此不挂单位、不与其他路互比。 - **字符串只用定长数组**:`clock[20]` 在结构体内,无指针、无生命周期问题(跨层传指针是经典事故源)。 - **`yx_valid` 与 `yx_bits` 分开**:无效点不参与显示、不参与变位判断(沿用 104 点表的既有做法,避免"IV 通道抖动成变位")。 @@ -198,6 +200,21 @@ typedef struct { - **只允许尾部追加字段**,结构体开头保留 `seq/valid`。 - 追加字段必须同时升 `UI_DATA_VER`(编译期常量),并在 `ui_data_feed` 里填默认值。 - **禁止删除或重排字段**(页面按名字取,重排是静默错位)。 +### 4.4 通道三:装置信息 `ui_data_info_t`(2026-10-25 落地新增) + +落地时比本文原稿多出一条同构通道,承载**既不属于 200 ms 实时量、也不属于校验和门控参数帧**的那批量: + +| 字段 | 内容 | 为什么不塞进已有的两条通道 | +|---|---|---| +| `model` / `sw_ver` / `hw_ver` / `build_date` / `sw_sum` | 装置身份(型号、软硬件版本、程序日期、软件校验) | 编译期常量,一辈子不变;塞进 `rt` 等于每 200 ms 白拷约 60 B 字符串 | +| `uptime_s` | 开机运行时长 | 每秒都在变;塞进 `par` 会被"校验和没变就不重发"的门控冻住(页面显示一个不再增长的时长 = 假数据) | +| `port_104` / `port_disp` / `ftp_user` | 通讯参数(104 端口、远程显示端口、FTP 用户名) | 值必须在 App 侧取(页面不得自己 include 协议头,`docs/UI设计/页面扩充详细方案.md` §3.5),但它们是静态量,与遥测节拍无关 | + +- 结构与两条主通道同构:`seq` + `valid` 打头、定长字符数组、零指针、由 `ui_data` 盖章序号、由 `App/app/ui_data_feed.c` 独家填充,随实时帧同节拍发布。 +- **公共地址 CA 故意不在本帧**:固件里 CA 是逐帧从 ASDU 头解析的,没有集中常量可取;等 104 侧给出查询点再按 `docs/UI设计/UI_数据供给设计说明.md` §4.3 的扩展规则追加字段并升 `UI_DATA_VER`。 +- 版本类字段目前是 `App/app/ui_data_feed.c` 里的宏(仓库内没有产品级版本登记表,值沿用页面既有显示值,有正式口径后只改那一处);**软件校验位在拿到构建期 CRC 生成步骤之前一律显示 `--`**,不抄参考项目遗留值。 + +> 另一处落地细化:`ui_data_rt_t` 除 `yc[]`(语义通道序基波有效值)外还带一条 `ph[]` 相量实例槽带(`ph_n` 给实际槽数)。原因是谐波(`Ia` 的二次/五次)无法在"语义通道"下标空间里表达——同一通道的谐波会与基波撞位,而交流量页要显示它们(`docs/UI设计/页面扩充详细方案.md` §3.1.1 第 4/5 行)。两条带的元素同形(数值 + 品质),页面行表按需选一条。 --- @@ -351,6 +368,14 @@ void ui_w_records_bind(const ui_rec_reader_t *reader); /* App 启动时注入 基线"只减不增"——**本设计顺带把存量债还掉三条**,而不是新增。 > 另外两条(`net_cfg` / `ftp_cfg` → `cfg_file.h`)属配置引擎,与 UI 无关,不在本文范围。 +### 7.4 SOE 记录:同构 reader + UI 侧显示名映射(2026-10-25 落地新增) + +`/log/soe.log` 是**二进制**定长 64 B 记录(时标/类型/元件号/序号/值),不是"时间戳 + 文本描述",不能与 `ui_w_records` 混读,故另立同构组件 `Drivers/BSP/UI/ui_w_soe.c`: + +- **注入式 reader** `ui_soe_reader_t`(`count()` + `get(idx, &view)`,错误码 0/-1/-2 与 `ui_rec_reader_t` 同族);`get` 返回的是 **UI 侧视图结构** `ui_w_soe_rec_t`(时标串 + 不透明类型号 + 不透明元件号 + 标志 + 序号/值),适配层负责解码与 CP56 → 文本换算,UI 不 include `App/log/soe_rec.h`。 +- **显示名映射在 UI 侧**(`ui_w_soe_type_name()` / `ui_w_soe_elm_name()`):类型与元件都是**表现层文案**,不进快照也不回落到 App 层(同 `docs/UI设计/UI_数据供给设计说明.md` §8 的格式化归属结论)。UI 侧另立一套不透明编号常量(`UI_W_SOE_T_*` / `UI_W_SOE_ELM_*`),其**数值与 App 枚举逐项相等**——这条等价关系不靠文档转述,由主机单测在同一 TU 里 include 两侧头逐项核对(`test/host/ui_data/` 的用例 `TC-UIDATA-012`/`013`,2026-10-25 现场核对结果:类型 1..8 + 哨兵 9、`ELM_NONE=0xFF`、`FLG_IV=0x80`、元件 0..8 与 `PE_COUNT=9`)。 +- **未标定闭锁/标定复归两类新事件已进映射表**(`UI_W_SOE_T_CAL_BLOCK=7` / `UI_W_SOE_T_CAL_RESUME=8`,缺字 采/样/恢/效 已走字体重生成 + 像素回归)。单测额外钉了两条容易悄悄发生的错:八类事件必须有名(忘配名只会在屏上显示 `--`,像素门禁照不出来)、"采样闭锁/恢复"与"标定闭锁/复归"四名必须互不相同(同名 = 现场把未标定闭锁读成采样故障)。 +- **本批不绑定 reader**:落盘侧还没有只读 API(`App/log/soe_store.c` 只有 `post/drain/dropped`),需要加法式的 `soe_count()` / `soe_read(idx, &rec)`(FatFs `f_open` + `f_lseek`,与 `syslog_get_record` 同构);该文件属道F 的潜在写域,先后次序由 Lead 排。未注入期间组件按空态渲染("无记录"伪行),因此本批**不产生任何像素变化**。 --- @@ -408,6 +433,12 @@ typedef struct { 这是本设计对"可测性"的直接贡献:**页面数据来源可注入 → 像素回归不再依赖真实硬件**。 +> 【2026-10-25 落地注】`simulator/main.c` 的实现形态与两条原稿未写死的细节: +> 1. 固定帧由 `sim_publish_data_frames()` 一次性发布,**无头与窗口两个初始化点都调**(与记录 reader 注入同址、同"先于 `ui_menu_init`"纪律); +> 2. 固定帧里的定值**不手抄 40 个数字**,而是把 `App/algo/prot_set.c`(零平台依赖的纯算法,双端共享核心)链进模拟器、`prot_set_init()` 后取出厂默认——抄表等于造第二真相(同 `scripts/check_font_charset.py` 的"单一规则源"口径); +> 3. `build_date` 用**固定串**而不是 `__DATE__`:否则跨天重建会把装置信息页的黄金参考刷掉,而黄金参考是判据本身。 +> 4. 原稿设的 `--live` 合成正弦模式**尚未实现**(当前无消费者,等动态刷新真的需要人眼观察时再加)。 + ### 9.3 与"虚拟注入"的关系 `docs/UI设计/页面补齐规划.md` §7 提的厂家调试"虚拟注入",**天然落在 `ui_data_feed` 上**:注入值只要在 feed 组装时覆盖对应字段即可,**页面完全不知情**。 @@ -458,6 +489,8 @@ typedef struct { **步骤1~步骤2 是关键路径**:它们不产出任何用户可见效果,但决定后面所有页面是否可信。**不要跳过步骤1 直接改页面**——那会退化成"页面里塞假数据 2.0"。 +> 【2026-10-25 落地进度】步骤1、步骤2 已落(道E task-14,分支 `task/ui-data-feed`):`ui_data` 三通道 + 主机单测 82 项全绿(`test/host/ui_data/`,已挂 `ui-data-unit-test` 钩子)、`ui_data_feed` 已在 `App/task/ui_task.c` 主循环接线并在建页前发首帧。**改动两处与原稿不同**:① 实时帧的 `eng` 与品质位按标定层真值语义落地(见 `docs/UI设计/UI_数据供给设计说明.md` §4.1 的落地注);② 新增第三条通道 `info`(同文档 §4.4)。**写路径(步骤5 的 `ui_data_param_request/state`)随整定 4 页一起落**,本批无消费者,先落等于落一段永不执行的代码。步骤3 起(页面接线)在道E 第二个工作项。 + --- ## 12. 验收判据 diff --git a/scripts/check_registration.py b/scripts/check_registration.py index baaf42b9..9125e6b5 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -96,6 +96,7 @@ SIM_EXEMPT = [ ("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"), ("App/algo/adc_cal.c", "L1.5 标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"), ("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"), + ("App/app/ui_data_feed.c", "UI 快照装配器(依赖 phasor/prot_set/iec104 端口常量等固件侧数据源);模拟器不跑装配链,快照由 simulator/main.c 直接发布固定帧,模拟器无消费路径"), ("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"), ("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"), ("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"), diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 67e663de..69fbb0ac 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1708,6 +1708,28 @@ "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH 先例;2026-10-25 道A 卡2 建族):属主登记处 = 单测代码目录,不逐号补文档", "cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌" }, + { + "name": "TC-HOST-UIDATA", + "class": "TC", + "title": "主机侧单测用例号:UI 数据快照层与跨层契约一致性(test/host/ui_data,道E task-14)", + "pattern": "\\bTC-UIDATA-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/ui_data/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-UIDATA-014", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCCAL 先例;2026-10-25 道E task-14 建族):属主登记处 = 单测代码目录,不逐号补文档;本族号同时是 UI 不透明编号与 App 枚举的现场核对锚(SOE 类型/元件号)", + "cursor_note": "游标按建族时实测最大母号收录(2026-10-25):越界即视为未登记的冒号牌" + }, { "name": "TC-HOST-PROTDIM", "class": "TC", diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 158ab12f..93e7d0da 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -58,6 +58,7 @@ include_directories( # 2026-10-02 测试目录重构后头文件移入 test/board/) ${PROJECT_ROOT}/App/cfg # cfg_file.h / cfg_file_store.h(通用配置文件引擎,双端共享核心) ${PROJECT_ROOT}/App/log # sys_log.h(运行/自检记录引擎,双端共享核心) + ${PROJECT_ROOT}/App/algo # prot_set.h(定值模型纯算法,双端共享核心;见 ALGO_SOURCES) ${PROJECT_ROOT}/Drivers/BSP # dbg_log.h(使用 simulator 的 dbg_cfg.h 配置) ${SDL2_DIR}/include # SDL2 头文件 ) @@ -100,6 +101,8 @@ set(UI_SOURCES ${UI_DIR}/ui_w_editor.c ${UI_DIR}/ui_w_setlist.c ${UI_DIR}/ui_page_action.c + ${UI_DIR}/ui_data.c # 数据快照存侧(道E task-14;写侧 App/app/ui_data_feed.c 不进模拟器,见 scripts/check_registration.py) + ${UI_DIR}/ui_w_soe.c # SOE 记录组件(道E task-14;reader 注入点待绑定,未绑定按空态渲染) ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) @@ -115,6 +118,14 @@ set(NETCFG_SOURCES sim/cfg_file_store_sim.c ) +# 保护定值模型(App/algo/prot_set.c 零平台依赖,双端共享核心)。 +# 模拟器只需要"出厂默认定值"来发布 UI 快照帧(main.c 的 sim_publish_data_frames), +# 不在这里复制一份 40 项数字——抄表就等于造第二真相(同 scripts/check_font_charset.py +# "单一规则源"的同一治理口径)。 +set(ALGO_SOURCES + ${PROJECT_ROOT}/App/algo/prot_set.c +) + # 网络后端(net_* 核心 API 的 Winsock 实现 + UI 推流平台后端;MCU 端另编 net_socket.c 与 _mcu 后端) set(NET_SOURCES sim/net_socket_sim.c @@ -124,7 +135,7 @@ set(NET_SOURCES #============================================================================= # 构建可执行文件 #============================================================================= -add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES} ${SYSLOG_SOURCES} ${NET_SOURCES}) +add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES} ${SYSLOG_SOURCES} ${NET_SOURCES} ${ALGO_SOURCES}) target_compile_definitions(lvgl_sim PRIVATE LV_CONF_INCLUDE_SIMPLE # 让 lvgl.h 找到 lv_conf.h diff --git a/simulator/main.c b/simulator/main.c index b0fdf790..ada85b66 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -69,6 +69,8 @@ #include "cfg_file_store.h" #include "sys_log.h" #include "ui_w_records.h" /* task-6 记录 reader 注入点(镜像 App/task/ui_task.c 的绑定) */ +#include "ui_data.h" /* task-14 快照存侧:模拟器在此直接发布固定帧(装配器不进模拟器) */ +#include "prot_set.h" /* task-14 定值出厂默认(App/algo 纯算法,双端可编,见 simulator/CMakeLists) */ /* 屏幕尺寸(与 MCU 端一致) */ #define SIM_SCREEN_W 160 @@ -171,6 +173,95 @@ static int sim_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len, static const ui_rec_reader_t s_rec_reader = { sim_rec_count, sim_rec_get }; +/* + * 函数功能:发布固定数据帧(镜像 App/app/ui_data_feed.c 的装配结果) + * 入口参数:无 + * 返回值:无 + * 限定条件:ui_menu_init 之前调用(无头与窗口两个初始化点共用,先于建页) + * 函数说明:模拟器不链接装配器(无硬件数据源;真值还会让像素参考天天变), + * 这里发布一帧**固定合成值**,页面数据来源因此可注入——像素回归不再 + * 依赖真实硬件(UI_数据供给设计说明.md §9.2)。要点三条: + * 1. eng 固定 = UI_DATA_ENG_PIN,与真机未标定态同口径(数据可信.md + * §2.6 承诺边界 = 引脚电压),页面据此显示"未标定"; + * 2. build_date 用固定串而**不是** __DATE__——否则跨天重建会把装置 + * 信息页的黄金参考刷掉(黄金参考是判据本身,不得随手刷); + * 3. 定值取 prot_set 出厂默认(App/algo/prot_set.c 是纯算法,双端可编), + * 不手抄 40 个数字——抄一次就是一份会漂移的第二真相 + */ +static void sim_publish_data_frames(void) { + ui_data_rt_t rt; + ui_data_par_t par; + ui_data_info_t info; + uint8_t i; + + ui_data_init(); + + (void)memset(&rt, 0, sizeof(rt)); + rt.valid = 1u; + rt.eng = UI_DATA_ENG_PIN; + rt.tick_ms = 0u; + /* 遥测下标 = adc_semantic_t 语义序(UA/UB/UC/UN/IA/IB/IC/IN/TEMP)。 + * 模拟器不 include adc_input.h,故这里用语义序注释对照,不写符号名 */ + rt.yc[0].val = 0.8123f; /* UA 基波(引脚电压域) */ + rt.yc[0].qds = 0u; + rt.yc[1].val = 0.0f; /* UB:V0.1 硬件削顶无采样源 */ + rt.yc[1].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; + rt.yc[2].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* UC 同上 */ + rt.yc[3].val = 0.0054f; /* UN 零序电压基波 */ + rt.yc[3].qds = 0u; + rt.yc[4].val = 0.0456f; /* IA 基波 */ + rt.yc[4].qds = 0u; + rt.yc[5].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IB */ + rt.yc[6].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IC */ + rt.yc[7].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IN */ + rt.yc[8].val = 2048.0f; /* TEMP:码值(不参与线性标定) */ + rt.yc[8].qds = UI_DATA_Q_RAW; + /* 相量实例槽(序 = phasor k_inst[]:IA-F1/UA-F1/UN-F1/IA-F2/IA-F5) */ + rt.ph_n = 5u; + rt.ph[0].val = 0.0456f; + rt.ph[1].val = 0.8123f; + rt.ph[2].val = 0.0054f; + rt.ph[3].val = 0.0017f; /* IA 二次谐波(涌流制动监视量) */ + rt.ph[4].val = 0.0008f; /* IA 五次谐波(选线用) */ + for (i = 0u; (i < UI_DATA_PH_MAX) && (i < rt.ph_n); i++) { + rt.ph[i].qds = 0u; + } + for (; i < UI_DATA_PH_MAX; i++) { + rt.ph[i].qds = UI_DATA_Q_IV; /* 未发布的槽:无效,页面显示 "--" */ + } + rt.yx_bits = 0u; /* 固定帧无动作态 */ + rt.yx_valid = 0x4043C000ULL; /* YX15~18/23/31 有效位图 */ + rt.freq_valid = 0u; /* eng=0 期间幅值门恒失效 → 无新鲜读数 */ + rt.freq_hz = 0.0f; + rt.clock_ok = 1u; + (void)snprintf(rt.clock, sizeof(rt.clock), "%s", "2026-10-25 08:00:00"); + (void)ui_data_publish_rt(&rt); + + (void)memset(&par, 0, sizeof(par)); + (void)prot_set_init(); /* 出厂默认 + 封存(幂等) */ + par.valid = 1u; + par.checksum = prot_set_checksum(); + par.locked = prot_set_locked(); + par.bad_mask = 0u; /* 装载成功:无越界项 */ + for (i = 0u; i < UI_DATA_SET_N; i++) { + par.q10[i] = prot_set_get((prot_set_id_t)i); + } + (void)ui_data_publish_par(&par); + + (void)memset(&info, 0, sizeof(info)); + info.valid = 1u; + info.uptime_s = 0u; + info.port_104 = 2404u; /* App/task/iec104_serv.h 的 IEC104_SERV_PORT */ + info.port_disp = (uint16_t)UI_NET_SINK_PORT; /* 同源常量(BSP 头,模拟器可见) */ + (void)snprintf(info.model, sizeof(info.model), "%s", "F30"); + (void)snprintf(info.sw_ver, sizeof(info.sw_ver), "%s", "SV0.010"); + (void)snprintf(info.hw_ver, sizeof(info.hw_ver), "%s", "HW0.010"); + (void)snprintf(info.build_date, sizeof(info.build_date), "%s", "Oct 25 2026"); + (void)snprintf(info.sw_sum, sizeof(info.sw_sum), "%s", "--"); + (void)snprintf(info.ftp_user, sizeof(info.ftp_user), "%s", "admin"); + (void)ui_data_publish_info(&info); +} + /* * 函数功能:无窗口离屏渲染(可注入键序列)并导出末帧像素 * 入口参数:keys - 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键) const char * @@ -212,6 +303,9 @@ static int run_headless(const char * keys, const char * path, int standby) { /* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */ ui_w_records_bind(&s_rec_reader); + /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, + * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ + sim_publish_data_frames(); if (ui_menu_init() != 0) { return 1; @@ -607,6 +701,9 @@ int main(int argc, char **argv) { /* 构建菜单 UI(与 MCU 端共用 ui_menu.c) */ /* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */ ui_w_records_bind(&s_rec_reader); + /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, + * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ + sim_publish_data_frames(); if (ui_menu_init() != 0) { fprintf(stderr, "ui_menu_init failed\n"); diff --git a/test/host/ui_data/run_unit_test.py b/test/host/ui_data/run_unit_test.py new file mode 100644 index 00000000..977a9d56 --- /dev/null +++ b/test/host/ui_data/run_unit_test.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""UI 数据快照层(ui_data)主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/ui_data/ui_data_test.c(它以 #include 纳入 + **生产源文件** Drivers/BSP/UI/ui_data.c,并同时 include 契约另一侧的 + App/algo 头),在 PC 上离线跑"快照存储 + 发布盖章 + 跨层下标一致性", + 失败即非 0 退出。零硬件、零烧录、毫秒级。 + 工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只用模拟器像素回归): + 快照层是页面看到的整个世界,但 ui-golden 只覆盖"已接线的页面"——遥信 27 点、 + 定值 40 项、遥测 9 路这类"还没接线的通道"错了不会让任何一张参考图变红。 + 跨层不透明下标(yc 序 = adc_semantic_t、q10 序 = prot_set_id_t)正是解耦换来 + 的风险面,契约把它钉在编译/测试期(docs/UI设计/UI_数据供给设计说明.md §9.4)。 + +用法(仓库根目录): + python test/host/ui_data/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 +设计依据:docs/UI设计/UI_数据供给设计说明.md §9.1 / §12 判据 8(道E task-14); +创建日期:2026-10-25 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 被测侧(BSP/UI)与契约另一侧(App/algo)都要在包含路径里:测试单元在同一 TU +# 里同时看到两侧头,才有资格断言"页面表的下标指向预期枚举"。 +BUILD = TestBuild( + name="UI Data Snapshot Tests", + src=os.path.join(HERE, "ui_data_test.c"), + include_dirs=[ + HERE, + os.path.join(ROOT, "Drivers", "BSP", "UI"), + os.path.join(ROOT, "App", "algo"), + os.path.join(ROOT, "App", "log"), # soe_rec.h:SOE 事件类型编号真源 + ], + summary_marker="=== UI Data Snapshot Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/ui_data/ui_data_test.c b/test/host/ui_data/ui_data_test.c new file mode 100644 index 00000000..c4490f94 --- /dev/null +++ b/test/host/ui_data/ui_data_test.c @@ -0,0 +1,538 @@ +/* + * 模块名称: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 +#include + +/* 生产源文件原样纳入(纯内存叶子,零平台依赖,直接编) */ +#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; + + ui_data_init(); + fill_rt(&src, 5u); + src.yx_bits = 0u; /* 全部点位"分/未动作" */ + src.yx_valid = valid_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"); +} + +/* ===================== 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"); + CHECK(longest <= 12u, "TC-UIDATA-012m type names fit value column (<=4 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"); + + /* 非元件事件:类型名进名列,值列留空 */ + 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) == 12u) && (value[0] == 0), "TC-UIDATA-014f non-element row: type 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; +}