/* * 模块名称: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 #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, };