From 433214268075784d903aac29307cfb2f3a70baca Mon Sep 17 00:00:00 2001
From: Wanderingss <1624155937@qq.com>
Date: Sun, 25 Oct 2026 15:33:04 +0800
Subject: [PATCH] =?UTF-8?q?feat(ui-pages):=20=E9=81=A5=E4=BF=A1=E9=87=8F?=
=?UTF-8?q?=2033=20=E8=A1=8C=E5=9B=BA=E5=AE=9A=E5=90=8D=E8=A1=A8=E9=A1=B5?=
=?UTF-8?q?=E6=8E=A5=E5=AE=9E=E6=97=B6=E5=BF=AB=E7=85=A7=EF=BC=88=E6=96=B0?=
=?UTF-8?q?=E5=A2=9E=20yx=5Fdp=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
批 4 主体(票 task-15;规格 docs/UI设计/页面扩充详细方案.md §3.1.3 方案 A)。V0.1 只有 6 点有
硬件来源,其余 27 点在屏上就是 "--"——这是真实状态,不是页面没做完。
页面与数据
- 33 行固定名表(Flash const)+ 动态分页(8 行/页 = 5 页)。两处命名不照抄文档简写:idx0~3
屏上写「位置1~4」而非「YK1~4 位置」(遥信页出现 YK 前缀会被读成遥控对象,对应关系留在注释);
idx30 写「保护闭锁」(写侧位号注释的语义即此,用编号反而丢信息)。idx4~13、idx31~32 点表未
定义功能名,如实写点号占位(YX5…、YX32),不编造功能名。
- 值映射次序:先判无效 → 再判单/双点 → 最后看位值。无效位上的位值不代表任何事实,次序颠倒就会
把垃圾位画成「复归」。
- 快照新增 yx_dp 双点位图(+8 B)。不教页面"前 4 点是双点":单双点是 104 点表事实(YX_DP_N /
iec104_pts_yx_is_dp),抄进表现层就是第二真相——点表把双点段挪位或改起始下标,屏上不报错,
只会用错词形容同一个点。写侧逐点问 is_dp() 拼位图。
- UI_DATA_YX_N(33) 与 iec104_pts_yx_count() 编译期比不了(那个 #define 在协议层 .c 内),故写侧
留一条一次性 DBG_ERROR(约 90 B Flash,只在真漂移时打一次)。名表行数 = UI_DATA_YX_N 由数组
宽度法编译期守卫钉住(ARMCC C99 无 _Static_assert,本仓惯用写法)。
- 补 include 一条实测坑:DBG_ERROR 是宏,漏 include "dbg_log.h" 时 ARMCC 把它当隐式声明函数放过
编译期,错落在链接期(L6218E Undefined symbol DBG_ERROR)——报错点离根因隔了一整个链接步骤,
已在 include 处写明「DBG_TAG 必须在 include 之前」。
一个只有像素文本断言能抓到的截断 bug
- 照抄批 1 的 *_PUT(dst, 源) 宏(长度取 sizeof(源)),但本表源是 const char *const 数组的
**元素(指针)**,sizeof 得 4 → 「故障总」被截成「故」,而 "YX9" 恰好 4 字节看起来完全正常。
编译器 / cppcheck / 编码钩子全不响。定位来自断言:第 2 页「过流Ⅰ段」匹配不到,同一行值列
「复归」却在原位 = 名字被截、值没错位。修法:长度取目标缓冲容量 UI_BUF_STRLEN。同名宏、不同
参数类别(数组 vs 指针),批 1 那处本身是对的,照抄才中招——教训写在宏定义上方。
模拟器与覆盖诚实性
- 固定帧发布 yx_dp = 0x0F,但刻意不把这几位置进 yx_valid:V0.1 无开入硬件,屏上就是 "--"。不用
「让它显示合位/分位」的假数据填满画面,那等于让黄金参考声称本机有开关位置遥信。
- 代价如实登记:合位/分位这条词映射当前**没有任何自动化证据**(像素恒 IV;页面依赖 LVGL 进不了
主机单测)。已写进方案 §3.1.3 落地注与契约落地进度·四,作为开入落地后的台架判据,不假装测过。
测试
- 主机单测 +2 条(TC-UIDATA-009c 双点位图原样透传、不被 valid 掩;009d 点数 33 的独立见证,注明
绊红时要同步改哪四处)→ 85/85。
- 文本断言 +33 条(yx / yxp2 两态,含页码钉住 33/8 = 5 页的分页数学);两页像素基线单独一笔。
- font-charset 本批零新增字:名表用字全部命中既有 183 字子集——「置/故/障/总/外/部」正属批 1
成组并入 KEEP_EXTRA 的那批字,第二次兑现(当时若按 extract 输出重写字符集,这些字早被洗掉,
上屏是空白,而门禁只会说「字不在清单里」)。
体积(同一棵树、构建前后的实测增量)
- Code 224272→224688(+416);RO 24040→24640(+600);RW 1324→1328(+4);
ZI 134844→135088(+244)→ Flash 约 +1020 B、RAM +248 B。
- RO 增量主要是 33 行名表与名称串,是"把 33 个真点名上屏"的代价,不是可省开销。
跳过的门禁(三条同族,同一根因):mem-numbers / mem-budget / size-gate(L0c)——台账与体积凭证
回填是 Lead 单写者,本分支不做静默处理,数值见上。
---
App/app/ui_data_feed.c | 44 ++++++
Drivers/BSP/UI/ui_data.h | 12 +-
Drivers/BSP/UI/ui_page_registry.c | 2 +
Drivers/BSP/UI/ui_page_registry.h | 2 +-
Drivers/BSP/UI/ui_page_yx.c | 203 +++++++++++++++++++++++++
Drivers/BSP/UI/ui_page_yx.h | 27 ++++
MDK-ARM/STM32F407-Demo.uvprojx | 5 +
docs/UI设计/UI_数据供给设计说明.md | 19 +++
simulator/CMakeLists.txt | 1 +
simulator/main.c | 7 +-
simulator/tests/check_content_pages.py | 50 ++++++
test/host/ui_data/ui_data_test.c | 11 ++
ui_menu_tree.txt | 2 +-
13 files changed, 381 insertions(+), 4 deletions(-)
create mode 100644 Drivers/BSP/UI/ui_page_yx.c
create mode 100644 Drivers/BSP/UI/ui_page_yx.h
diff --git a/App/app/ui_data_feed.c b/App/app/ui_data_feed.c
index 49c83913..e8b2e7de 100644
--- a/App/app/ui_data_feed.c
+++ b/App/app/ui_data_feed.c
@@ -24,6 +24,14 @@
#include "ui_data_feed.h"
+/* 日志锚点:本文件只在"点表点数与快照点数不等"这一条静默漂移上打一条一次性日志
+ * (每条约 90 B Flash,见 AGENTS「日志不是免费的」——常规发布路径刻意一条都不加)。
+ * ⚠ DBG_TAG 必须在 include 之前定义:dbg_log.h 的 DBG_ERROR 是宏,漏 include 时
+ * ARMCC 只把 DBG_ERROR 当隐式声明函数放过编译期,错误落在链接期(L6218E
+ * Undefined symbol),比漏写头文件本身更难归因 */
+#define DBG_TAG "[UIDATA]"
+#include "dbg_log.h"
+
/* 下层(Drivers/BSP):快照存侧 + 两个端口/用户名常量来源(App → BSP 是合法方向) */
#include "ui_data.h"
#include "ui_net_sink.h" /* UI_NET_SINK_PORT:远程显示(RemoDispBus)监听端口 */
@@ -38,6 +46,7 @@
#include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */
#include "sys_clock.h" /* 墙钟字符串 */
#include "iec104_serv.h" /* IEC104_SERV_PORT:实际监听端口(非协议层同名常量) */
+#include "iec104_pts.h" /* task-15 批4:遥信点数与单/双点性质(点表事实的唯一来源) */
/* 私有宏定义区 */
@@ -184,6 +193,27 @@ static uint8_t feed_eng(void) {
return UI_DATA_ENG_SEC;
}
+/*
+ * 函数功能:组装遥信双点位图(bit i = 第 i 点是双点型 M_DP)
+ * 入口参数:无
+ * 返回值:位图 uint64_t
+ * 限定条件:uiTask 上下文;点数上限 UI_DATA_YX_N(<64,故位图放得下)
+ * 函数说明:逐点问点表 `iec104_pts_yx_is_dp()`,**不在此复制"前 4 点是双点"这个事实**
+ * ——那句话写在这里就是第二真相,点表一改就悄悄错(下发位图的理由见
+ * ui_data.h 的 yx_dp 字段注释)
+ */
+static uint64_t feed_yx_dp(void) {
+ uint64_t m = 0u;
+ uint8_t i;
+
+ for (i = 0u; (i < (uint8_t)UI_DATA_YX_N) && (i < 64u); i++) {
+ if (iec104_pts_yx_is_dp(i) != 0u) {
+ m |= ((uint64_t)1u << i);
+ }
+ }
+ return m;
+}
+
/*
* 函数功能:组装遥信位图(bit i = 第 i 点"合/动作")
* 入口参数:无
@@ -316,6 +346,20 @@ static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) {
p_snap->yx_bits = feed_yx_bits();
p_snap->yx_valid = FEED_YX_VALID_MASK;
+ p_snap->yx_dp = feed_yx_dp();
+ /* 点数漂移检查(只在真漂走时打一条日志):点表 YX_N 与本层 UI_DATA_YX_N 一旦不等,
+ * 页面按 33 行画、协议按另一个数上送,两边都"看起来正常"——这是静默错,值得花一条
+ * DBG(约 90 B Flash)换一次现场可见的自证;只打一次不刷屏(同丢弃计数口径) */
+ {
+ static uint8_t s_yx_n_bad;
+ uint8_t n = iec104_pts_yx_count();
+
+ if ((n != (uint8_t)UI_DATA_YX_N) && (s_yx_n_bad == 0u)) {
+ s_yx_n_bad = 1u;
+ DBG_ERROR("yx point count drift: pts=%u ui_name_table=%u",
+ (unsigned)n, (unsigned)UI_DATA_YX_N);
+ }
+ }
/* 测频:读数新鲜度单独走 freq_valid()(幅值门 + 收敛),两者任一不成立
* 页面即显示 "--"——eng=0 期间幅值门恒失效,读数本就不可消费,如实反映 */
diff --git a/Drivers/BSP/UI/ui_data.h b/Drivers/BSP/UI/ui_data.h
index 4ecd6be5..205ac511 100644
--- a/Drivers/BSP/UI/ui_data.h
+++ b/Drivers/BSP/UI/ui_data.h
@@ -121,7 +121,17 @@ typedef struct {
uint8_t rsv4[3]; /* 预留(对齐填充) */
ui_data_yc_t ph[UI_DATA_PH_MAX]; /* 下标 = 相量实例序(含谐波),见 UI_DATA_PH_MAX 注 */
uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */
- uint64_t yx_valid; /* 遥信有效位图:bit i = 1 该点有硬件来源 */
+ uint64_t yx_valid; /* 遥信有效位图: bit i = 1 该点有硬件来源 */
+ uint64_t yx_dp; /* 遥信双点位图: bit i = 1 该点是双点型(M_DP)
+ * 值列打"合位/分位",单点打"动作/复归"。
+ * 为什么下发位图而不让页面自己认定"前 4 点是
+ * 双点":单/双点是**104 点表事实**(见
+ * App/protocol/iec104/iec104_pts.c 的 YX_DP_N 与
+ * iec104_pts_yx_is_dp),抄一份进表现层就是第二
+ * 真相——点表把某点改成双点、或把双点段挪到下标
+ * 中段,屏上不会报错,只会用错词形容同一个点。
+ * V0.1 双点 4 点无开入硬件 → 不在 yx_valid 内,
+ * 值列仍是 "--"(本字段不参与无效判定) */
uint8_t freq_valid; /* 1 = 测频读数新鲜(幅值门 OK 且已收敛) */
uint8_t rsv2[3]; /* 预留(对齐填充) */
float freq_hz; /* 测得频率(Hz);freq_valid=0 时不可消费 */
diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c
index 6bf225e9..84c0e44a 100644
--- a/Drivers/BSP/UI/ui_page_registry.c
+++ b/Drivers/BSP/UI/ui_page_registry.c
@@ -21,11 +21,13 @@
#include "ui_page_view_plate.h"
#include "ui_page_view_set.h"
#include "ui_page_view_word.h"
+#include "ui_page_yx.h"
const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = {
{ 1u, &ui_page_device_info_desc }, /* node 1 装置信息 */
{ 3u, &ui_page_ac_desc }, /* node 3 交流量 */
{ 4u, &ui_page_dc_desc }, /* node 4 直流量 */
+ { 5u, &ui_page_yx_desc }, /* node 5 遥信量 */
{ 13u, &ui_page_view_param_desc }, /* node 13 参数 */
{ 14u, &ui_page_view_set_desc }, /* node 14 定值 */
{ 15u, &ui_page_view_word_desc }, /* node 15 控制字 */
diff --git a/Drivers/BSP/UI/ui_page_registry.h b/Drivers/BSP/UI/ui_page_registry.h
index ac46b20d..eeabbbcf 100644
--- a/Drivers/BSP/UI/ui_page_registry.h
+++ b/Drivers/BSP/UI/ui_page_registry.h
@@ -25,7 +25,7 @@ typedef struct {
const content_page_def_t *desc; /* 页面描述符(Flash const) */
} ui_page_entry_t;
-#define UI_PAGE_MAP_COUNT 11u /* 绑定了内容页的叶子数 */
+#define UI_PAGE_MAP_COUNT 12u /* 绑定了内容页的叶子数 */
extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT];
diff --git a/Drivers/BSP/UI/ui_page_yx.c b/Drivers/BSP/UI/ui_page_yx.c
new file mode 100644
index 00000000..40085799
--- /dev/null
+++ b/Drivers/BSP/UI/ui_page_yx.c
@@ -0,0 +1,203 @@
+/*
+ * 模块名称:UI Page YX (遥信量内容页)
+ * 模块功能:33 点遥信的固定名表页(方案 A)——每行一个遥信点,值列按快照的三个位图
+ * (有效 / 动作 / 单双点)映射成"合位-分位"、"动作-复归"或"--";动态列表页,
+ * 数据只读 Drivers/BSP/UI/ui_data.h 的实时快照,零 App include
+ * 为什么是 33 行固定表(V0.1 只有 6 点有硬件来源,另外 27 行都是 "--"):
+ * ① 与 104 点表一一对应,现场拿这张屏就能与上位机逐点对;② 开入落地后本页
+ * 零改动自动变真;③ 只显有效点的做法看不出"缺哪些点",而且点数一变黄金参考
+ * 就得重做(把判据建立在会漂的东西上等于没有判据)——取舍记录在
+ * docs/UI设计/页面扩充详细方案.md §3.1.3
+ * 名表为什么在页面侧而不在快照里:显示文案属表现层(同文档 §8);点数与单/双点性质
+ * 反过来**必须**由写侧下发(那是点表事实),两者边界见 ui_data.h 的 yx_dp 注释
+ * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码)
+ * 作者:王建锋
+ * 创建日期:2026-10-25
+ * 修改记录:
+ * v1.0 2026-10-25 王建锋 创建(道E task-15 批 4)
+ */
+/* 头文件包含区 */
+
+#include "ui_page_yx.h"
+
+#include "ui_data.h"
+#include "ui_tokens.h"
+
+/* 静态变量定义区 */
+
+/*
+ * 33 点固定名表(下标 = 遥信位图位号 = 104 点表 IOA - 1,IOA 1~33)
+ *
+ * 两处偏离文档的命名(文档 §3.1.3 用的是区间简写,落到逐行名字时必须选一个,理由写在
+ * 这里而不是留给人猜):
+ * · 文档把 idx0~3 写成「YK1~4 位置」:遥信页上出现"YK"前缀会被读成遥控对象(那是
+ * 另一页的事),屏上只写「位置1~4」,与遥控的对应关系留在本注释;
+ * · 文档把 idx30~32 写成「闭锁1~3」:idx30 在写侧有明确语义来源(保护闭锁,定值校验
+ * 和自检失败),用编号反而把信息丢了;idx31/32 点表未定义功能名,如实写点号占位,
+ * **不编造功能名**。同理 idx4~13(YX5~14)也用点号占位。
+ *
+ * 长度与 UI_DATA_YX_N 的一致性由下面的编译期数组守卫钉住(本仓惯用宽度法,ARMCC C99
+ * 没有 _Static_assert):名表少写/多写一行直接编译失败,不会到了屏上才少一行
+ */
+static const char *const s_yx_name[UI_DATA_YX_N] = {
+ "位置1", "位置2", "位置3", "位置4", /* 0~3 双点;V0.1 无开入硬件 */
+ "YX5", "YX6", "YX7", "YX8", "YX9", /* 4~8 点表未定义功能名 */
+ "YX10", "YX11", "YX12", "YX13", "YX14", /* 9~13 (同上,不编名) */
+ "故障总", "过流Ⅰ段", "过流Ⅱ段", "过流Ⅲ段", /* 14~17 YX15~18,有硬件来源 */
+ "零序Ⅰ段", "零序Ⅱ段", /* 18,19 V0.1 无该判据上送 */
+ "接地故障", "重载", "过载", /* 20~22 过载(YX23)有来源 */
+ "外部遥信1", "外部遥信2", "外部遥信3", "外部遥信4", /* 23~26 开入未实现 */
+ "外部遥信5", "外部遥信6", "外部遥信7", /* 27~29 */
+ "保护闭锁", "YX32", "YX33", /* 30~32 闭锁(YX31)有来源 */
+};
+
+/* 名表行数与快照点数一致(不一致 = 编译期错误,见上注) */
+typedef char yx_name_count_check[(sizeof(s_yx_name) / sizeof(s_yx_name[0]) == UI_DATA_YX_N) ? 1 : -1];
+
+/* 值列词表(全在既有 183 字子集内,font-charset 实测零新增) */
+#define YX_INVALID "--"
+#define YX_DP_ON "合位"
+#define YX_DP_OFF "分位"
+#define YX_SP_ON "动作"
+#define YX_SP_OFF "复归"
+
+/* 帧缓存:只在发布序号变化时整帧拷一次(一帧约 200 B,逐行只读标量) */
+static ui_data_rt_t s_rt;
+static uint32_t s_seq = 0xFFFFFFFFu;
+
+/* 函数定义区 */
+
+/*
+ * 函数功能:定长缓冲内拷字面量(代码规范 7.5 禁 strcpy;长度取调用点 sizeof)
+ * 入口参数:a_dst - 目标缓冲(框架行缓冲 UI_BUF_STRLEN) char *
+ * a_src - 源字面量(含 NUL) const char *
+ * len - 目标容量 uint32_t
+ * 返回值:无
+ * 限定条件:源串长度必须小于 len(编译期由字面量 sizeof 保证,不做运行期截断——
+ * 截断只会把"动作"打成"动",比读错更难查)
+ */
+static void yx_put(char *a_dst, const char *a_src, uint32_t len) {
+ uint32_t i = 0u;
+
+ if (len == 0u) {
+ return;
+ }
+ while (((i + 1u) < len) && (a_src[i] != '\0')) {
+ a_dst[i] = a_src[i];
+ i++;
+ }
+ a_dst[i] = '\0';
+}
+
+/*
+ * 长度参数**必须取目标缓冲容量**,不能取 sizeof(源):本表的源是 `const char *const`
+ * 表里的**指针**,`sizeof(lit)` 得到的是指针宽度(4/8),于是 "故障总" 会被拷成第一个
+ * 汉字 "故"——ASCII 名("YX9" 恰好 4 字节)看着完全正常,CJK 名集体缺尾巴。这个错
+ * 编译器、cppcheck、编码钩子都不会响,是像素文本断言把它揪出来的(第 2 页第 7 行
+ * "过流Ⅰ段" 匹配不到,值列 "复归" 却在原位 = 名字被截、值没错位)。
+ * 批 1 那几处 *_PUT 宏传的是**字面量数组**(sizeof 就是数组长度),照抄到这里就中招——
+ * 同名宏、不同参数类别,正是"看着像同构其实不同"的那类坑。
+ */
+#define YX_PUT(dst, lit) yx_put((dst), (lit), (uint32_t)UI_BUF_STRLEN)
+
+/*
+ * 函数功能:开页复位帧缓存(接框架 init_cb)
+ * 入口参数:无
+ * 返回值:无
+ * 限定条件:框架开页链内(uiTask)
+ * 函数说明:序号必须复位——否则重开本页会沿用上次开页的帧号,首帧之前的 "--" 状态
+ * 可能被永久留在屏上(批 1 已踩过一次,同一条纪律再用一遍)
+ */
+static void pg_init(void) {
+ s_seq = 0xFFFFFFFFu;
+ (void)memset(&s_rt, 0, sizeof(s_rt));
+}
+
+/*
+ * 函数功能:按发布序号刷新帧缓存
+ * 入口参数:无
+ * 返回值:无
+ * 限定条件:uiTask 绘制链内(refresh 前)
+ * 函数说明:ui_data_rt() 的通道是 0=成功 / -1=尚无首帧(不是 bool 成败);失败时把
+ * 序号打回不可能的值,下一轮一定重试,不会停在半新不旧的帧上
+ */
+static void pg_sync(void) {
+ uint32_t seq = ui_data_rt_seq();
+
+ if ((seq == s_seq) && (s_rt.valid != 0u)) {
+ return;
+ }
+ if (ui_data_rt(&s_rt) != 0) {
+ s_seq = 0xFFFFFFFFu;
+ (void)memset(&s_rt, 0, sizeof(s_rt));
+ return;
+ }
+ s_seq = seq;
+}
+
+/*
+ * 函数功能:总行数(固定 33 点,不随有效性增减)
+ * 入口参数:无
+ * 返回值:行数 uint16_t
+ * 限定条件:框架绘制/翻页链内
+ * 函数说明:故意返回常量——"只显有效点"会让现场无法判断缺了哪些点(方案 B 的缺点),
+ * 且行数变化会让黄金参考随运行态漂移
+ */
+static uint16_t pg_count(void) {
+ return (uint16_t)UI_DATA_YX_N;
+}
+
+/*
+ * 函数功能:逐行取数(名 = 固定名表,值 = 三个位图映射出的词)
+ * 入口参数:a_row - 行下标(0..32) uint16_t
+ * a_name - 名输出缓冲(UI_BUF_STRLEN) char *
+ * a_value - 值输出缓冲 char *
+ * 返回值:无
+ * 限定条件:框架绘制链内(双缓冲各调一次,本函数无副作用)
+ * 函数说明:判定顺序是"先无效、再单双点、最后位值"——无效位上的位值是真随机垃圾
+ * (位图没更新过就是 0,更新了也不代表该点有效),所以 IV 必须排在最前;
+ * 双点段 V0.1 恒无效,"合位/分位"这条支路当前不可达但必须写:开入一接上
+ * 就走通,而按协议写死的 4 个点本来就是双点(可达性由点表决定,不由本页决定)
+ */
+static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) {
+ uint64_t bit;
+
+ a_value[0] = '\0';
+ if (a_row >= (uint16_t)UI_DATA_YX_N) {
+ a_name[0] = '\0';
+ return; /* 防御:行号越界不越读名表(count 恒 33,走不到) */
+ }
+ pg_sync();
+ YX_PUT(a_name, s_yx_name[a_row]);
+ bit = (uint64_t)1u << (uint8_t)a_row;
+ if ((s_rt.valid == 0u) || ((s_rt.yx_valid & bit) == 0u)) {
+ YX_PUT(a_value, YX_INVALID);
+ return;
+ }
+ if ((s_rt.yx_dp & bit) != 0u) {
+ YX_PUT(a_value, ((s_rt.yx_bits & bit) != 0u) ? YX_DP_ON : YX_DP_OFF);
+ return;
+ }
+ YX_PUT(a_value, ((s_rt.yx_bits & bit) != 0u) ? YX_SP_ON : YX_SP_OFF);
+}
+
+/*
+ * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=yx 绑定引用):
+ * 固定 33 行的动态页(rows=NULL + count_cb/refresh),8 行/页 = 5 页;band_h 必须 >0
+ * (selectable 页选中行是黑字,无白带即不可见,同记录页约定)
+ */
+const content_page_def_t ui_page_yx_desc = {
+ .id = "yx",
+ .title = "遥信量",
+ .selectable = 1u,
+ .top_y = UI_LIST_TOP,
+ .pitch = UI_ROW_PITCH,
+ .band_h = UI_ROW_BAND_H,
+ .text_dy = 0,
+ .left_x = UI_LIST_LEFT_X,
+ .right_x = UI_LIST_RIGHT_X,
+ .rows_per_page = UI_LIST_ROWS,
+ .init_cb = pg_init,
+ .count_cb = pg_count,
+ .refresh = pg_refresh,
+};
diff --git a/Drivers/BSP/UI/ui_page_yx.h b/Drivers/BSP/UI/ui_page_yx.h
new file mode 100644
index 00000000..cbf464eb
--- /dev/null
+++ b/Drivers/BSP/UI/ui_page_yx.h
@@ -0,0 +1,27 @@
+/*
+ * 模块名称:UI Page YX (遥信量内容页)
+ * 模块功能:实时数据 → 遥信量 叶子的内容页声明——向框架 ui_content_page 提供页面描述符
+ * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
+ * 作者:王建锋
+ * 创建日期:2026-10-25
+ * 修改记录:
+ * v1.0 2026-10-25 王建锋 创建(道E task-15 批 4;规格 =
+ * docs/UI设计/页面扩充详细方案.md §3.1.3 方案 A)
+ */
+#ifndef UI_PAGE_YX_H
+#define UI_PAGE_YX_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include "ui_content_page.h"
+
+/* 页面描述符(定义在 ui_page_yx.c;生成物按菜单树 #page=yx 引用本符号) */
+extern const content_page_def_t ui_page_yx_desc;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UI_PAGE_YX_H */
diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx
index f4b8f960..eef86605 100644
--- a/MDK-ARM/STM32F407-Demo.uvprojx
+++ b/MDK-ARM/STM32F407-Demo.uvprojx
@@ -3596,6 +3596,11 @@
1
..\Drivers\BSP\UI\ui_page_view_word.c
+
+ ui_page_yx.c
+ 1
+ ..\Drivers\BSP\UI\ui_page_yx.c
+
diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md
index 1ccf06e8..2488cad6 100644
--- a/docs/UI设计/UI_数据供给设计说明.md
+++ b/docs/UI设计/UI_数据供给设计说明.md
@@ -163,6 +163,7 @@ typedef struct {
ui_data_yc_t yc[UI_DATA_YC_N];
uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */
uint64_t yx_valid; /* 遥信有效位图:bit i = 1 该点有硬件来源 */
+ uint64_t yx_dp; /* 遥信双点位图:bit i = 1 该点是双点型(M_DP) */
uint8_t freq_valid;
float freq_hz;
uint8_t clock_ok; /* 0 = 时钟无效(RTC 异常) */
@@ -175,6 +176,13 @@ typedef struct {
- **`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 通道抖动成变位")。
+- **`yx_dp`(2026-10-25 批 4 追加)**:单点还是双点是 **104 点表事实**(`iec104_pts.c` 的 `YX_DP_N`
+ / `iec104_pts_yx_is_dp()`),由写侧 `ui_data_feed.c` 逐点问点表后下发位图;页面只按位图选词
+ (双点"合位/分位"、单点"动作/复归"、无效 "--")。把"前 4 点是双点"抄进表现层就是第二真相:
+ 点表改成双点段挪位或加高起始下标,屏上不会报错,只会用错词形容同一个点。V0.1 这 4 点无开入
+ 硬件 → 不在 `yx_valid` 内,值列仍打 "--"(本字段不参与无效判定)。点数一致性由写侧一条
+ 一次性 `DBG_ERROR` 兜(`UI_DATA_YX_N` 与 `iec104_pts_yx_count()` 不等时留痕),因为该常量
+ 在协议层是 .c 内的 `#define`,编译期拿不到。
### 4.2 参数快照 `ui_data_par_t`
@@ -507,6 +515,17 @@ typedef struct {
> `docs/UI设计/页面扩充详细方案.md` §3.2.2 落地注;行表与 `prot_set` 枚举序/上下限的一致性由
> `tools/setview_check/check_setview_limits.py` 核验(BSP 侧看不到 App 头,物理上无法自证,必须有人从外面钉)。
+> 【2026-10-25 落地进度·四】遥信通道已被消费:**遥信量**页(`ui_page_yx.c`,33 行固定名表,
+> 方案 A)按 `yx_valid` → `yx_dp` → `yx_bits` 的次序取值列(先判无效:无效位上的位值不代表任何
+> 事实;再判单/双点选词;最后才看动作态)。起草时为此补了 `yx_dp` 一个字段(§4.1),由
+> `ui_data_feed.c` 逐点问 `iec104_pts_yx_is_dp()` 得到——**点数与单双点性质都留在协议层**,页面
+> 不复制这些事实;`UI_DATA_YX_N` 与 `iec104_pts_yx_count()` 一旦不等,写侧打一条一次性
+> `DBG_ERROR`(点数在协议层是 .c 内的宏,编译期拿不到,只能运行期留痕)。模拟器侧同步发布
+> `yx_dp = 0x0F` 但**不**把这 4 位置进 `yx_valid`:V0.1 没有开入硬件,屏上就是 `--`——刻意不用
+> 「让它显示合位/分位」的假数据把画面填满,那等于让黄金参考声称本机有开关位置遥信。因此
+> 「合位/分位」这条词映射当前**没有任何自动化证据覆盖**(像素恒 IV、页面依赖 LVGL 也进不了
+> 主机单测),它已登记为开入落地后的台架判据,而不是假装测过。
+
---
## 12. 验收判据
diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt
index 2ebf2323..beab355a 100644
--- a/simulator/CMakeLists.txt
+++ b/simulator/CMakeLists.txt
@@ -109,6 +109,7 @@ set(UI_SOURCES
${UI_DIR}/ui_page_view_plate.c
${UI_DIR}/ui_page_view_set.c
${UI_DIR}/ui_page_view_word.c
+ ${UI_DIR}/ui_page_yx.c
)
# 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程)
diff --git a/simulator/main.c b/simulator/main.c
index b86f454c..fc894a9b 100644
--- a/simulator/main.c
+++ b/simulator/main.c
@@ -274,7 +274,12 @@ static void sim_publish_data_frames(void) {
rt.ph[i].qds = UI_DATA_Q_IV; /* 未发布的槽:无效,页面显示 "--" */
}
rt.yx_bits = 0u; /* 固定帧无动作态 */
- rt.yx_valid = 0x4043C000ULL; /* YX15~18/23/31 有效位图 */
+ rt.yx_valid = 0x4043C000ULL; /* YX15~18/23/31 有效位图(与点表同集合) */
+ /* 双点位图照写侧口径给(App/protocol/iec104/iec104_pts.c 的 YX_DP_N = 4 → bit0~3)。
+ * 这 4 点**不在**上面的有效位图里(V0.1 无开入硬件),所以遥信页对它们打 "--"——
+ * 刻意不把 bit0 塞进 yx_valid 让画面多两个汉字好看:那会让黄金参考显示成"这台设备
+ * 有开关位置遥信",而真机上没有(屏上的话必须与本机一致,同页脚页码那条裁定) */
+ rt.yx_dp = 0x0FULL;
rt.freq_valid = 0u; /* eng=0 期间幅值门恒失效 → 无新鲜读数 */
rt.freq_hz = 0.0f;
rt.clock_ok = 1u;
diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py
index 02177b8f..57f5c9ca 100644
--- a/simulator/tests/check_content_pages.py
+++ b/simulator/tests/check_content_pages.py
@@ -41,6 +41,10 @@ PAGES = [
("info", "e"), # 装置信息(node 1)
("ac", "dee"), # 交流量(node 3,实时数据下第 1 项)
("dc", "dede"), # 直流量(node 4,实时数据下第 2 项)
+ # 遥信量(实时数据下第 3 项):d 到实时数据 → e 进(选中交流量)→ dd 到遥信量 → e 开页;
+ # yxp2 再多按 8 次下移,让选中行跨过页边界落到第 2 页首行
+ ("yx", "dedde"),
+ ("yxp2", "dedde dddddddd"),
# SOE 记录(node 51,记录查询下第 1 项):6 下进记录查询 → e → 首项 SOE记录 → e;
# 数据 = simulator/main.c 的固定 3 条假记录(含 type 7 未标定闭锁这条非元件支路)
("soe", "dddddde e"),
@@ -112,6 +116,52 @@ TEXTS = {
("r5名", "未标定闭锁", (94, 108), False),
("页脚", "第 1 页,共 1 页", (140, 160), False),
],
+ # ── 批 4:遥信量(33 点固定名表,5 页)──
+ # 第 1 页 8 行**全是 --**(位置 1~4 是无硬件的双点,YX5~8 点表未定义)——这不是画面坏了,
+ # 是 V0.1 的真实状态:27 个点没有开入来源。方案 B(只显有效点)会把这件事藏起来,方案 A
+ # 让现场能逐点对、也能一眼看出缺哪些点(取舍见 页面扩充详细方案.md §3.1.3)。
+ # 第 2 页才出现有来源的点(故障总 / 过流Ⅰ段 = 复归);两页合起来同时钉住分页数学:
+ # 33 行 / 8 行每页 = 5 页,页码算错这里直接响。
+ "yx": [
+ ("标题", "遥信量", (0, 24), False),
+ ("r0名", "位置1", (24, 38), True),
+ ("r0值", "--", (24, 38), True),
+ ("r1名", "位置2", (38, 52), False),
+ ("r1值", "--", (38, 52), False),
+ ("r2名", "位置3", (52, 66), False),
+ ("r2值", "--", (52, 66), False),
+ ("r3名", "位置4", (66, 80), False),
+ ("r3值", "--", (66, 80), False),
+ ("r4名", "YX5", (80, 94), False),
+ ("r4值", "--", (80, 94), False),
+ ("r5名", "YX6", (94, 108), False),
+ ("r5值", "--", (94, 108), False),
+ ("r6名", "YX7", (108, 122), False),
+ ("r6值", "--", (108, 122), False),
+ ("r7名", "YX8", (122, 136), False),
+ ("r7值", "--", (122, 136), False),
+ ("页脚", "第 1 页,共 5 页", (140, 160), False),
+ ],
+ "yxp2": [
+ ("标题", "遥信量", (0, 24), False),
+ ("r0名", "YX9", (24, 38), True),
+ ("r0值", "--", (24, 38), True),
+ ("r1名", "YX10", (38, 52), False),
+ ("r1值", "--", (38, 52), False),
+ ("r2名", "YX11", (52, 66), False),
+ ("r2值", "--", (52, 66), False),
+ ("r3名", "YX12", (66, 80), False),
+ ("r3值", "--", (66, 80), False),
+ ("r4名", "YX13", (80, 94), False),
+ ("r4值", "--", (80, 94), False),
+ ("r5名", "YX14", (94, 108), False),
+ ("r5值", "--", (94, 108), False),
+ ("r6名", "故障总", (108, 122), False),
+ ("r6值", "复归", (108, 122), False),
+ ("r7名", "过流Ⅰ段", (122, 136), False),
+ ("r7值", "复归", (122, 136), False),
+ ("页脚", "第 2 页,共 5 页", (140, 160), False), # 选中行落到第 2 页首行(YX9);页码同时钉住 33/8 = 5 页
+ ],
# ── 批 3:参数定值 → 查看 四页(数据 = prot_set 出厂默认,值串带单位)──
# 名与值两列都断:值列断的是换算后的小数位与单位——光看像素,没人会去核 0.95 是不是被
# Q10 存成了 0.94921875。控制字页还多钉一层:Ⅰ/Ⅱ 段默认是「跳闸」、Ⅲ 与过负荷段默认是
diff --git a/test/host/ui_data/ui_data_test.c b/test/host/ui_data/ui_data_test.c
index b9fe964b..a1d74d70 100644
--- a/test/host/ui_data/ui_data_test.c
+++ b/test/host/ui_data/ui_data_test.c
@@ -287,15 +287,26 @@ static void tc_yx_points(void)
ui_data_rt_t src;
ui_data_rt_t got;
+ const uint64_t dp_expect = 0x0FULL; /* V0.1 双点段 = bit0~3(YX1~4 开关位置类) */
+
ui_data_init();
fill_rt(&src, 5u);
src.yx_bits = 0u; /* 全部点位"分/未动作" */
src.yx_valid = valid_expect;
+ src.yx_dp = dp_expect;
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK(got.yx_valid == valid_expect, "TC-UIDATA-009a valid mask == YX15/16/17/18/23/31");
CHECK(got.yx_bits == 0u, "TC-UIDATA-009b bits and valid are independent fields");
+ /* 双点位图必须原样透传:页面按它选词(合位/分位 vs 动作/复归),快照层若做"归一化"
+ * 或按 valid 掩一次,页面就会把双点当单点形容——同一条事实两处解释必然漂 */
+ CHECK(got.yx_dp == dp_expect, "TC-UIDATA-009c dp mask passed through unmasked");
+ /* 点表与名表共用点数(1 基 IOA 1~33)。这里是**有意**再抄一遍字面量:测试的职责是
+ * 独立见证,协议层那个 #define 在 .c 内不可见,改点数时必须有人被绊一下——绊到后
+ * 要同时改:App/protocol/iec104/iec104_pts.c 的 YX_N、ui_data.h 的 UI_DATA_YX_N、
+ * 页面名表 ui_page_yx.c(有编译期数组守卫兜着)、本条 */
+ CHECK((uint32_t)UI_DATA_YX_N == 33u, "TC-UIDATA-009d point count is 33 (YX_N witness)");
}
/* ===================== TC-UIDATA-010:相量槽透传 ===================== */
diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt
index 12d9b322..3a7436ea 100644
--- a/ui_menu_tree.txt
+++ b/ui_menu_tree.txt
@@ -21,7 +21,7 @@
实时数据
交流量 #page=ac
直流量 #page=dc
- 遥信量
+ 遥信量 #page=yx
参数定值
整定
参数 #P