Files
wandering1 94b0d79026 feat(ui)+chore(docs): UI 补齐清单清空;数据可信销项与「卡」号清退
本笔含两组收尾工作(同一工作区内交错在 6 个共享文档/门禁配置里,
拆开会产生"文档声称的页数与树里黄金数不一致"的中间提交,故合为一笔):

一、UI 补齐(清单清空)
- 新建 6 个内容页模块:ui_page_accident / oper / alarm / start / yk
  复用 ui_w_records 共享组件(分页、空态"无记录"、按实例读盘缓存),
  记录层与五个写入点已由道F task-10 / 道G task-12 落地,本批只补页面;
  ui_page_runreport 为 13 行 2 页只读统计,四个数逐个挂范围标签
  (自电池周期 / 本次开机),boot_ready=0 显 "--" 不显 0。
- 数据面:ui_data_info_t 尾部追加 9 字段,ui_data_feed 用
  boot_stats_get() + iec104_serv_get_stats() 纯搬运;模拟器填确定值假数据
  (差异登记 docs/测试说明/模拟器差距清单.md §2.3 两行)。
- 接线:菜单树 6 叶绑 #page=,绑定叶 22→28、UI_PAGE_MAP_COUNT=28;
  字库 185→218 字 / 280→313 字形;双端注册 uvprojx +30、CMakeLists +6。
- 回归:内容页黄金 53→60(含 runreport_p2 真拍第 2 页),文本断言
  425 正向 / 36 反向;存量 53 张逐字节零漂移;记录 5 页各用独立空目录
  (模拟器把操作留痕写进 cwd 的 sim_oper.log,共用 tmp 会让空态拍不到)。

二、数据可信销项 + 「数据可信」任务卡号整族清退
- 删除已闭环的"已知电压注入验证"行与闭环结论块(证据在
  docs/硬件平台/ADC通道映射说明.md §8.1 与
  docs/测试说明/ADC前端注入验证流程.md §5~§7),§5 标识列改语义短语。
- 卡 0/0b/1~12 共 14 个号全仓 288 站点 / 56 文件清退成语义短语,
  代码注释、测试、门禁配置与文档一并改;换算表入 docs/编号规范.md §9.3。
  这一族是中文前缀,不进 check_ids 语料,刻意不入 id_aliases.json。

验证(实测)
- @build 0 错 0 警:Code=236456 RO=28124 RW=1876 ZI=137084;
  RAM +464 B(RW +304 / ZI +160),RW_IRAM1 62.2% / RW_CCM 87.9%,
  体积同码凭证与 RAM 台账已回填。
- check_menu_rows 8 状态 rc=0(池 5368/5448 常量未动);
  check_content_pages 60 页 rc=0;check_font_charset / registration /
  coding-standard / bom-drift / mojibake / doc-links --all / layer-deps /
  nested-comment 全 rc=0;check_ids --strict 无错误;主机单测
  ui_data 295 / boot_stats 21 / sys_log_mcu 141 / soe 32 / soe_store 27。

未做(不是遗漏)
- 六页真机判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、
  拔 RTC 电池落 RESTARTED、IWDG 复位落 rst_cause、主站连一次 conn_up+1)
  已登记 docs/任务规划/登记簿.md §7;模拟器结构上照不到。
- docs/产品需求/产品需求设计文档.md 属另一条在飞改动,本笔不含;
  其中我已同步 6 处现状数字(28 叶 / 60 页 / 218 字),随该笔入库。
2026-10-28 07:23:16 +08:00

858 lines
41 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI 数据组装器(ui_data_feed)
* 模块功能:App → UI 数据通道的写侧——按节拍从采集/标定/相量/测频/保护/协议/墙钟
* 各模块取数,组装成 docs/UI设计/UI_数据供给设计说明.md §4 契约的三帧
* (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h
* 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**,不含任何布局、
* 格式化与 LVGL 调用(显示规格全在页面行表,同文档 §8)
* 写方向(道F task-1):同一条 tick 顺带把页面投递的定值整定请求落定——
* ui_data_param_drain() 每轮取一条,执行动作经本文件的 s_param_ops
* (locked = prot_set_locked / check = g_prot_set_meta 上下限 /
* apply = prot_set_set_sealed)注入给 BSP;值域与落盘事实都留在 App
* 侧,BSP 只有队列与状态机(契约 §6.1"UI 不能直写"的落地形态)
* 量纲诚实性: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)
* v1.1 2026-10-26 DTU 项目组 道F task-1:① tick 前置 ui_data_param_drain()
* (定值写路径出队 → 闭锁/越界预检 → set_sealed,
* 见 s_param_ops);② 参数帧尾填 cal_block(未标定
* 闭锁徽标,三路口径同 protect_task.c,不用 eng)
* v1.3 2026-10-27 DTU 项目组 道F task-7:遥测整定注入表(ui_data_yt_ops_t)
* v1.4 2026-10-27 DTU 项目组 道G task-9:通讯参数注入表(ui_data_comm_ops_t,
* 通讯设置页);port_104 快照从 IEC104_SERV_PORT 宏
* 切 iec104_comm_port() 真源(宏同笔删除,交接口径见
* IEC104上行设计说明.md §8)
* 装配层实现 + 首轮懒绑(时序与通道四绑定等价,
* 见 feed_yt_bind_once 函数头);行数等值由编译期
* 守卫钉,量纲分档与合法域分档同源同序
* v1.2 2026-10-26 DTU 项目组 道F task-16:删除本地镜像 feed_cal_block(),
* 两个调用点(填帧 / 门控比较)改调
* adc_cal_prot_blocked()——同一份三路判定此前在
* 本文件与 protect_task.c 各写一遍,且各自带一份
* #ifndef DTU_BENCH(只给单个 TU 加宏就能让屏与判据
* 分叉);真源现落在 App/algo/adc_cal.c,旁路全仓一处。
* 行为不变(判定式逐字同构、调用时机不变)
*/
/* 头文件包含区 */
#include <string.h>
#include "cmsis_os.h" /* osKernelGetTickCount:与 adc_task 同一 ms 时基 */
#include "boot_stats.h" /* 运行报告:上电次数 + 本次复位原因快照(道G task-12 数据面) */
#include "iec104_serv.h" /* 运行报告:四个 104 只读观测计数(本次开机以来,跨重连不清零) */
#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)监听端口 */
#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 三态可用性;adc_cal_prot_blocked:未标定闭锁单点;adc_cal_k_uv:系数当前生效值(与 104 回显同视图) */
#include "run_par.h" /* 道F task-7 遥测整定注入表:死区/合法域/系数 setter/脏位(真源全在本模块与 adc_cal,feed 只转达) */
#include "phasor.h" /* 基波/谐波有效值(fAmSQ) */
#include "freq.h" /* 测频读数与新鲜度 */
#include "prot_elm.h" /* 元件动作态(遥信位图来源) */
#include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */
#include "sys_clock.h" /* 墙钟字符串 */
#include "iec104_comm.h" /* 道G task-9:[comm] 真源 getter/setter/脏位;快照端口切 getter,IEC104_SERV_PORT 字面宏同笔删除 */
#include "iec104_pts.h" /* task-15 批4:遥信点数与单/双点性质(点表事实的唯一来源) */
/* 私有宏定义区 */
/* 实时帧与信息帧发布节拍(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];
/* 写路径注入表的 id 形参是 uint8_t(BSP 侧不透明编号宽度,位图只有 64 位):
* 定值项数一旦破 64/255,request 的 mask 与 check/apply 的编号都会静默截断,
* 故在写侧也钉一道(BSP 侧同一条守卫在 Drivers/BSP/UI/ui_data.c) */
typedef char feed_chk_set_u8_t[((uint32_t)PS_ID_COUNT <= 64u) ? 1 : -1];
typedef char feed_chk_fuser_t[(UI_DATA_FUSER_LEN >= FTP_CFG_USER_MAX) ? 1 : -1];
/* 静态变量定义区 */
/* 组装缓冲:三帧串行填充,共用一块存储,省两份 RAM。
* 实际最大者是**实时帧**(208 B > par 192 B > info),故给 par 尾追加字段
* 只要不超过 rt 就一字节 RAM 都不多花(这条由主机单测 TC-UIDATA-021g/h 钉住,
* 尺寸一变这里就是唯一会撒谎的地方) */
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;
/* 发布节拍与参数变化检测状态
* par 帧有**三个各自独立**的变化源,任一变了就要重发一帧(详见 tick 里的门控注释):
* s_par_sum 定值改动(校验和)
* s_par_locked 定值闭锁(定值校验和自检失配,校验和不动)
* s_par_cal_block 未标定闭锁(两点标定完成,校验和也不动)
* 后两个各占一字节——省下来就是"屏上状态与真值脱拍"这笔债 */
static uint32_t s_last_rt_ms;
static uint32_t s_par_sum;
static uint8_t s_par_sum_valid;
static uint8_t s_par_locked;
static uint8_t s_par_cal_block;
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 硬件削顶:双转换器无共同采样触发)→ 留 -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 点是双点型 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 点"合/动作")
* 入口参数:无
* 返回值:位图 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;
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 期间幅值门恒失效,读数本就不可消费,如实反映 */
{
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;
}
}
/* ── 写路径(契约 §6.2):把 prot_set 的三个动作包成注入给 BSP 的函数表 ────── */
/*
* 函数功能:写路径判定"定值闭锁中吗"(BSP 经 ui_data_param_ops_t 来问)
* 入口参数:无
* 返 回 值:1 - 闭锁(定值校验和自检失败态,禁一切定值写入);0 - 可写 uint8_t
* 限定条件:uiTask 上下文(只读 prot_set 状态位)
* 函数说明:这里的"闭锁"是**定值闭锁**(= par.locked = YX31 遥信),**不是**未标定
* 闭锁(= par.cal_block):后者仍可改定值、只是判据停走,两者在屏上不得
* 互认(三词三源口径 docs/任务规划/数据可信.md §4.3 + 落地简报 §7-6)
*/
static uint8_t feed_param_locked(void) {
return prot_set_locked();
}
/*
* 函数功能:写路径整批预检——单项是否落在 meta 上下限内
* 入口参数:a_id - 定值项编号(位图位序;uint8_t 是 BSP 侧不透明编号的宽度)
* a_q10 - 待写 Q10 定点值
* 返 回 值:0 - 合法;-1 - 编号越界或值越界 int
* 限定条件:只读 g_prot_set_meta(ROM 表),任意上下文安全
* 函数说明:上下限**只此一处取**——BSP 行表(Drivers/BSP/UI/ui_w_setview.c)是它的
* 换算镜像,由 setview-check 钩子核一致;本函数再抄一份数字就是第二真相
* ("查看合法、整定越界"两套边界就是这么来的)
*/
static int feed_param_check(uint8_t a_id, int32_t a_q10) {
if (a_id >= (uint8_t)PS_ID_COUNT) {
return -1;
}
if ((a_q10 < g_prot_set_meta[a_id].min) || (a_q10 > g_prot_set_meta[a_id].max)) {
return -1;
}
return 0;
}
/*
* 函数功能:写路径落值——单项**原子**写入 + 重封校验和 + 置脏
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
* 返 回 值:0 - 已写入;-1 - 编号越界或 prot_set 拒写(原值不动) int
* 限定条件:uiTask 上下文;调用前 BSP 已整批预检通过(两条件与本函数一致)
* 函数说明:用 prot_set_set_sealed()(影子拷贝→改→重封→单字交换,**每项自带置脏**,
* 落盘由 defaultTask 50 ms 圈扫脏写 /prot_set.bin,本模块不碰 FatFs)。
* ⚠ 不得改成 prot_set_set() + 批末 prot_set_seal():两条路径之间有
* "已改未封存"窗口,prot_set.c 明令不得交错,撞上就被定值校验和自检判成
* "定值被非法改写"→ 整台保护闭锁
*/
static int feed_param_apply(uint8_t a_id, int32_t a_q10) {
if (a_id >= (uint8_t)PS_ID_COUNT) {
return -1;
}
return prot_set_set_sealed((prot_set_id_t)a_id, a_q10);
}
/* 注入给 ui_data_param_drain() 的执行侧实现(BSP 不保存副本,每次调用现传) */
static const ui_data_param_ops_t s_param_ops = {
feed_param_locked,
feed_param_check,
feed_param_apply
};
/*
* ======================= 道F task-7:遥测整定注入表(ui_data_yt_ops_t)装配
*
* 两页唯一的执行侧转达处。四条装配纪律:
* ① 数值与合法域**都**现给执行侧(run_par_deadband / run_par_db_range /
* adc_cal_k_uv / RUN_PAR_K_MAX_UV)——本文件不出现任何范围数字字面量;
* 系数真带(µV/码执行段裁)没有公开访问器,也**不在此补一份**(task-43 收口),
* 真带拒的值经 set 原样返 -1,页面反显 + 保存行 ERR,两层守卫互不掩盖。
* ② 写走语义 setter(写值与置脏同一函数内发生,run_par_cal_k_set 文件头契约);
* 本层不存在"只置脏不写值"或"只写值不置脏"的组合,UI 永远够不到裸脏位。
* ③ 跨任务写口径:真机默认档 = iec104Task 的 C_SE;本文件的调用在 uiTask,
* 这**正是** Lead 裁定(task-7 派发②)批准的"装配层转达"整定路径——与 104
* 走同一批 setter、同一 int32 单字原子契约,不新开工;run_par.h 里死区旧
* 接口"iec104Task 上下文"的限定条件语写于该裁定之前,语义已被裁定覆盖
* (系数两支的限定条件已明写装配层入口,措辞同步归算法层下一笔)。
* ④ 落盘不靠本表:写成功置脏 → defaultTask 每轮无条件 run_par_store_save()
* (查实:唯一 cfg_file_save 调用者,首行"无脏立即返回";104 的 peek→事件位
* 只是加速器不是必经链)——所以"OK = 盘上真落了"在真机与模拟器(pump 清脏)
* 两端口径一致,黄金参考拍的 WAIT/OK 不是表演。
*/
/* 跨层行数等值守卫:BSP 侧契约值 ↔ 执行侧真值。本文件是全仓第一处两份头
* 同时可见的编译单元——BSP 物理上无法自证(看不到 App 头),守卫落这里 */
typedef char feed_yt_db_n_check_t[(UI_DATA_YT_DB_N == (uint8_t)RUN_PAR_DB_N) ? 1 : -1];
typedef char feed_yt_ch_n_check_t[(UI_DATA_YT_CH_N == (uint8_t)RUN_PAR_CH_N) ? 1 : -1];
/*
* 函数功能:死区点数(执行侧真值直通)
* 入口参数:无
* 返回值:RUN_PAR_DB_N uint8_t
* 限定条件:uiTask(注入表回调)
* 函数说明:行数从这一头出、BSP 契约常量在文件头编译期守卫对表——两边不等
* 编不过,等值不靠注释维持
*/
static uint8_t feed_yt_db_n(void) {
return (uint8_t)RUN_PAR_DB_N;
}
/*
* 函数功能:读第 idx 行死区现状(值 + 粗守域现给;量纲按点型分档)
* 入口参数:idx - 0..8(0..7 电气语义通道序,8 频率) uint8_t
* a_out - 行现状 ui_data_yt_row_t *
* 返回值:0 - 已写出;-1 - 越界 / 指针空(run_par_db_range 自判,本层不重复判) int
* 函数说明:unit 的分档(前 4 电压、中 4 电流、末 1 频率)**必须**与
* run_par_db_range 的档位分档同序——两处真源都没导出量纲,唯一防线是
* run_par.h 地址映射表(0x8030~0x8038 = UA..IN/FREQ)与本表同序这条
* 写死的对表纪律 + 黄金像素(档位漂了屏上会呈现"0~50"配 "A" 的肉眼矛盾);
* decimals 取 3 = 死区在引擎里的存储分辨率(毫单位 int32,换算单点
* s_milli_to_eng 在 run_par.c,本层不重除)
*/
static int feed_yt_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out) {
float lo;
float hi;
if (run_par_db_range(a_idx, &lo, &hi) != 0) {
return -1;
}
a_out->val = run_par_deadband(a_idx);
a_out->lo = lo;
a_out->hi = hi;
a_out->decimals = 3u;
a_out->unit = (a_idx < 4u) ? UI_DATA_YT_UNIT_V
: ((a_idx < 8u) ? UI_DATA_YT_UNIT_A : UI_DATA_YT_UNIT_HZ);
return 0;
}
/* 函数功能:写第 idx 行死区(直通语义 setter;返回值原样上抛,页面据此记账) int */
static int feed_yt_db_set(uint8_t a_idx, float a_eng) {
return run_par_deadband_set(a_idx, a_eng);
}
static uint8_t feed_yt_cal_n(void) {
return (uint8_t)RUN_PAR_CH_N;
}
/*
* 函数功能:读第 idx 路系数现状(k = adc_cal **当前生效值**,µV/码)
* 返回值:0 - 已写出;-1 - idx 越界或指针空 int
* 函数说明:显示值与 104 C_RD 回显同视图(run_par_104_read 用的就是 adc_cal_k_uv:
* 文件坏值被真带回退 ROM 时,屏与主站看到同一份 ROM——"页面值按比例
* 变化"判据因此量的就是执行段真正吃的数);
* lo/hi 只有粗守一层(0..RUN_PAR_K_MAX_UV,上限来自 run_par 唯一出口),
* 真带在执行段裁——两层守卫的注释纪律见 ui_w_ytview 头注;
* decimals 取 0 是**显示域容量约束**不是精度偏好:粗守上限 1e9 在
* ui_w_setlist 的 int32 显示域里只够零位小数(带 2 位则上限装不下),
* 屏上输入按整 µV 量化、写口仍收 float;要小数位得收紧粗守或改组件
* 值通道,都不在本批,也不许在此抄一份更小的假上限
*/
static int feed_yt_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out) {
if ((a_out == NULL) || (a_idx >= (uint8_t)RUN_PAR_CH_N)) {
return -1;
}
a_out->val = adc_cal_k_uv((adc_semantic_t)a_idx);
a_out->lo = 0.0f; /* 下限 0 由写口拒 k<=0 兜住,这里只给输入框边界 */
a_out->hi = RUN_PAR_K_MAX_UV;
a_out->decimals = 0u;
a_out->unit = UI_DATA_YT_UNIT_UV;
return 0;
}
/* 函数功能:写第 idx 路系数 k(越界防御后再进语义 setter——setter 形参是枚举,
* 编译器帮不上越界忙;返回码直通) int */
static int feed_yt_cal_k_set(uint8_t a_idx, float a_uv) {
if (a_idx >= (uint8_t)RUN_PAR_CH_N) {
return -1;
}
return run_par_cal_k_set((adc_semantic_t)a_idx, a_uv);
}
/* 函数功能:未标定闭锁徽标源(task-16 单点访问器,与判据停走同一个判定) uint8_t */
static uint8_t feed_yt_cal_blocked(void) {
return (adc_cal_prot_blocked() != 0) ? 1u : 0u;
}
/* 函数功能:整档落盘脏位(保存行三态判据;与 104 留痕同一个 peek,不另发明) uint8_t */
static uint8_t feed_yt_pending(void) {
return run_par_dirty_peek();
}
/* 遥测整定注入表(八件齐全;ui_data_yt_bind 拒残表,缺件就是装配错) */
static const ui_data_yt_ops_t s_yt_ops = {
feed_yt_db_n,
feed_yt_db_row,
feed_yt_db_set,
feed_yt_cal_n,
feed_yt_cal_row,
feed_yt_cal_k_set,
feed_yt_cal_blocked,
feed_yt_pending
};
/* ======================= 道G task-9:通讯参数注入表(ui_data_comm_ops_t)===
*
* 页面形状(两行的名/序)归 ui_page_comm.c;本层只转达值与合法带——
* lo/hi 现取 iec104_comm.h 带宏(唯一出口),值现取 getter(带内自愈),
* 装配层**不写第二份数值**(通道五同一条纪律)。
*/
/*
* 函数功能:读第 idx 行通讯参数现状(值 + 合法带)
* 入口参数:idx - UI_DATA_COMM_ROW_CA/_PORT uint8_t
* a_out - 行现状 ui_data_comm_row_t *
* 返回值:0 - 已整份写出;-1 - idx 越界(不猜、不顶替) int
* 限定条件:uiTask 页面绘制链(经 ui_data 通道六转发)
*/
static int feed_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out) {
if (a_idx == UI_DATA_COMM_ROW_CA) {
a_out->val = iec104_comm_ca();
a_out->lo = IEC104_COMM_CA_MIN;
a_out->hi = IEC104_COMM_CA_MAX;
return 0;
}
if (a_idx == UI_DATA_COMM_ROW_PORT) {
a_out->val = iec104_comm_port();
a_out->lo = IEC104_COMM_PORT_MIN;
a_out->hi = IEC104_COMM_PORT_MAX;
return 0;
}
return -1;
}
/*
* 函数功能:写第 idx 行通讯参数(守卫在 setter:带外拒、副本不动、不置脏)
* 返回值:0 - 已写并置脏;-1 - idx 越界或带外拒 int
* 限定条件:uiTask 按键链(编辑提交)
* 函数说明:a_val > 65535 不在此先裁——截断成 u16 会把 65536 洗成 0 再被
* setter 合法拒掉,等于替屏幕编了个假值;越界一律原样送 setter 拒
*/
static int feed_comm_set(uint8_t a_idx, uint32_t a_val) {
if (a_val > 65535uL) {
return -1;
}
if (a_idx == UI_DATA_COMM_ROW_CA) {
return iec104_comm_ca_set((uint16_t)a_val);
}
if (a_idx == UI_DATA_COMM_ROW_PORT) {
return iec104_comm_port_set((uint16_t)a_val);
}
return -1;
}
/* 函数功能:[comm] 段落盘脏位(与页面 WAIT/OK 同一枚 peek,不另发明) uint8_t */
static uint8_t feed_comm_pending(void) {
return iec104_comm_dirty_peek();
}
/* 通讯参数注入表(三件齐全;ui_data_comm_bind 拒残表) */
static const ui_data_comm_ops_t s_comm_ops = {
feed_comm_row,
feed_comm_set,
feed_comm_pending
};
/* 首轮懒绑一次:ui_data_feed_tick() 的首个调用点在 App/task/ui_task.c 里先于
* ui_menu_init()(首帧发布那次),与通道四 ui_data_maint_bind 的绑定时序等价;
* 不在 ui_task.c 直接绑是为了本批写域收在派发单列出的文件里。残表是真故障,
* 必须能在启动日志里看见,不能静默(通道四同口径) */
static uint8_t s_yt_bound;
static uint8_t s_comm_bound; /* 道G task-9 通讯参数表懒绑门(同款先置位防刷日志) */
/*
* 函数功能:组装参数快照(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;
/* v2 尾追加:未标定闭锁徽标(与判据停走**同一个判定**:App/algo/adc_cal.c 的
* adc_cal_prot_blocked(),道F task-16 起本文件不再自带一份三路判定——两份实现
* 可以各自漂移,屏上"未标定"就会与判据实际停不走同一件事)。
* ⚠ 不要改回用 rt.eng 反推:eng 要求电气 **8** 路全 VALID,V0.1 双转换器下
* UB/UC/IB/IC/IN 永不置位 → eng 恒为引脚档,照它画徽标就永不解锁;
* 两个谓词各有用途,边界写在 adc_cal.h 的函数头第③条。
* ⚠ 与上面的 locked 是两件事——locked 禁编辑,cal_block 只说明"改了也不会动作" */
p_snap->cal_block = adc_cal_prot_blocked();
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();
boot_stats_t boot;
iec104_serv_stats_t st104;
p_snap->valid = 1u;
p_snap->uptime_s = tick_ms / 1000u;
p_snap->port_104 = iec104_comm_port(); /* 真源 = [comm] RAM 副本(getter 带内恒成立,u16 读原子) */
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';
}
/* 运行报告四数(2026-10-28 UI 补齐):纯搬运,页面不得自己去 include 这两个头。
* uptime_s 仍由本函数用 tick 差算——boot_stats 不提供时长,别在那里再算一份。 */
boot_stats_get(&boot);
iec104_serv_get_stats(&st104);
p_snap->boot_count = boot.boot_count;
p_snap->boot_seq_state = boot.seq_state;
p_snap->boot_write_failed = boot.write_failed;
p_snap->rst_cause = boot.rst_cause;
p_snap->boot_ready = boot.ready;
p_snap->iec104_conn_up = st104.conn_up;
p_snap->iec104_apdu_tx = st104.apdu_tx;
p_snap->iec104_tx_fail = st104.tx_fail;
p_snap->iec104_rx_bytes = st104.rx_bytes;
}
/*
* 函数功能:装配节拍入口——先落定一条定值整定请求(写方向),再发布实时帧与
* 信息帧(满 200 ms 一次)、参数帧(仅校验和变化时);首轮立即发布
* (免启动初期出现一闪的 "--")
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
* 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较 +
* 一次空队列判断(drain 无请求时只是一次计数比较);
* 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径)。
* 写路径失败态(ERR_RANGE/ERR_LOCKED/ERR_BUSY)走屏上通道,不打日志
* (每条 DBG 约 90 B Flash + 216 B 栈,AGENTS「日志不是免费的」)
*/
void ui_data_feed_tick(void) {
uint32_t now;
uint32_t sum;
uint8_t due_rt;
if (s_yt_bound == 0u) {
s_yt_bound = 1u; /* 先置位再绑:失败也不每轮重试刷日志(90 B/条) */
if (ui_data_yt_bind(&s_yt_ops) != 0) {
DBG_ERROR("UI yt bind: FAIL");
}
}
if (s_comm_bound == 0u) {
s_comm_bound = 1u; /* 道G task-9:同款一轮一绑;两表时序等价(先于 ui_menu_init) */
if (ui_data_comm_bind(&s_comm_ops) != 0) {
DBG_ERROR("UI comm bind: FAIL");
}
}
now = osKernelGetTickCount();
if (s_map_done == 0u) {
feed_map_phasor();
}
/* 写路径:先落定整定请求,**再**取校验和——本轮刚写进 prot_set 的定值就进
* 本轮"校验和变化才发"那道门,页面保存行的 OK 与新值同一帧回来,不必多等
* 一轮(10 ms)。顺序反了不出错,只是慢一轮。
* 每轮最多处理一条(ui_data_param_drain 的限定条件):整定是人工低频事件,
* 一轮一条已经把最坏耗时钉住,不必排空整个队列
* ⚠ 返回值必须显式丢弃:本函数不消费"处理了几条",但 bsp-err 的
* consume-result 只认接住或 (void) 两种写法(实测编译器与 cppcheck 都不报) */
(void)ui_data_param_drain(&s_param_ops);
sum = prot_set_checksum();
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 白拷一帧(契约 §5.2)。
* 但"校验和没变"不等于"页面该看的没变"——本帧里还有两个与校验和无关的事实:
* · locked(定值闭锁):自检失配时置位,校验和一字不变,
* 而整定页保存行要显示 LOCK 并禁编辑;
* · cal_block(未标定闭锁):两点标定写完 [adc_cal] 的 valid 就翻转,
* 校验和同样不动,而标题上的"未标定"徽标要当场消失。
* 只盯校验和,这两件事都到不了屏上(真机现象:标定做完了屏上徽标永远挂着,
* 直到某次定值改动才被顺带刷掉——那是把两件不相干的事当同步点)。
* 代价:每轮多两次字节比较(无变化时零拷贝),换来屏上状态与真值同拍 */
{
uint8_t locked_now = prot_set_locked();
uint8_t cal_now = adc_cal_prot_blocked(); /* 标定层单点,与判据同源 */
if ((s_par_sum_valid == 0u) || (sum != s_par_sum)
|| (locked_now != s_par_locked) || (cal_now != s_par_cal_block)) {
(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_locked = locked_now;
s_par_cal_block = cal_now;
s_par_sum_valid = 1u;
}
}
}