diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md new file mode 100644 index 00000000..0d5662e2 --- /dev/null +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -0,0 +1,495 @@ +# UI 数据供给设计说明(高内聚低耦合) + +> **定位**:解决"内容页拿不到实时数据"的架构问题——给出数据从 App 层到 UI 层的**唯一通道**、数据契约、并发模型、写路径与测试策略。 +> **问题来源**:`docs/UI设计/页面补齐规划.md` §1 与 §3.1(现状:交流量第 2/3 页、直流量全部为硬编码占位值)。 +> **门禁口径**:`scripts/check_layer_deps.py` 的文件头已写明三种合法做法(注入 / 上层取好传进来 / 叶子工具进 ALLOW)。本文选择并细化前两种。 +> **前置阅读**:`docs/UI设计/内容页架构.md`(页面描述符与 4 个钩子)、`docs/架构设计/项目架构总览.md`(六层单向依赖)。 + +--- + +## 1. 问题与约束 + +### 1.1 现象 + +| 页 | 应该显示 | 实际显示 | +|---|---|---| +| 交流量 | 8 路电气量 + 温度的有效值 | 第 1 页写死实测值,第 2/3 页写死占位值 | +| 直流量 | 直流通道 | 3 行写死(`3.01/3.15/3.288`) | + +页面代码本身没问题——**问题在于页面没有任何合法途径拿到 App 层的数据**。 + +### 1.2 根因 + +六层单向依赖规定 `Drivers/BSP/**` 不得 include `App/**` 的头,且这条规则已经机械化(`scripts/check_layer_deps.py`,2026-10-22 起拦新增)。而 UI 需要的全部数据都在 `App/`: + +| 数据 | 现居位置 | 层 | +|---|---|---| +| 相量(有效值/相角) | `App/algo/phasor.h` | 算法层 | +| 频率 | `App/algo/freq.h` | 算法层 | +| 定值(40 项 + 封存态) | `App/algo/prot_set.h` | 算法层 | +| 遥信位图 / 品质 | `App/protocol/iec104/iec104_pts.h` | 协议层 | +| 时钟 | `App/util/sys_clock.h` | 工具层(**已在 ALLOW 豁免内**) | +| 运行/自检记录 | `App/log/sys_log.h` | 服务层 | + +**已有的 3 条基线违规**(`ui_page_runlog.c` / `ui_page_selftest.c` / `ui_w_records.h` → `sys_log.h`)就是这条通道缺失的化石证据——当时为了做记录页,选择了"直接 include 上层头",门禁注释里写着"记录读取应由上层递进来",但**递进来的机制一直没建**。 + +### 1.3 反模式:`extern` 全局变量 + +门禁只扫 `#include`。因此在 BSP 里写 `extern const float g_phasor_ua;` 就能**完全绕过检查**。 + +这是本设计要明确禁止的做法,理由不是"门禁会拦",而是它比 include 更糟: + +| 维度 | include 头 | extern 全局 | +|---|---|---| +| 编译期契约 | 有(类型/签名) | 无(类型写错是未定义行为) | +| 所有权 | 明确 | 无(谁写谁读说不清) | +| 并发 | 至少能看到调用 | 完全隐式 | +| 可测性 | 可桩替 | 必须链接真实现 | +| 门禁可见性 | 会报警 | **静默通过** | + +**结论**:数据通道必须是**显式的、有契约的、可测的**,不能靠"能编过"。 + +--- + +## 2. 方案选型 + +### 2.1 四个候选 + +| 方案 | 做法 | 依赖方向 | 绘制路径开销 | 可测性 | 并发 | +|---|---|---|---|---|---| +| **A 直连** | BSP include `App/algo/phasor.h` | BSP → App(**反向**) | 每行一次跨层调用 | 差(要链真实现) | 隐式 | +| **B extern** | BSP 声明 `extern` 全局 | 无(不可见) | 读全局 | 差 | 无保护 | +| **C 注入(pull)** | BSP 定义 `ui_data_src_t` 函数指针表,App 启动时注入;UI 主动调 `src->get_yc(i)` | App → BSP(**正确**) | 每行一次函数指针调用 | 好 | 调用时序耦合 | +| **D 快照推送(push)** | BSP 拥有快照存储 + 发布 API;App 周期组装并发布;页面只读本地快照 | App → BSP(**正确**) | 纯本地读 | 好(直接 publish 假帧) | 单写者 + 同上下文 | + +### 2.2 评分 + +| 维度 | A | B | C | D | +|---|---|---|---|---| +| 依赖方向正确 | ✗ | ✗ | ✓ | ✓ | +| 页面与数据源解耦(换数据源不改页面) | ✗ | ✗ | 部分(接口在 BSP,实现可换) | ✓(页面只认快照结构) | +| 绘制路径无跨层调用 | ✗ | ✓ | ✗ | ✓ | +| 主机单测可驱动 | ✗ | ✗ | ✓ | ✓ | +| 并发模型简单 | ✗ | ✗ | ✓ | ✓(单写者同上下文) | +| 模拟器 golden 可稳定 | ✗ | ✗ | 需假实现 | ✓(publish 固定帧) | +| 新增字段的扩散面 | 大 | 大 | 中(改接口 + 实现) | 小(改结构体 + 填充处) | + +**选 D 为主通道**,C(注入)用于两类不适合快照的场景:**写路径**(定值整定)与**记录分页读取**(数据量随记录增长,不能全量快照)。 + +### 2.3 为什么 D 能做到"高内聚" + +- **内聚**:`ui_data` 只干一件事——存最新一帧并给出只读访问;`ui_data_feed` 只干一件事——把 App 的散装数据组装成一帧。两者都不含对方的知识。 +- **耦合**:页面只依赖**一个结构体**,不依赖任何 App 模块;App 只依赖**一个发布函数**,不依赖任何 UI 概念(页面、行、几何一概不知)。 +- **变化隔离**:将来遥测链(谐波方和根 + 平均 + 死区)落地,只改 `ui_data_feed` 里的取数来源,**页面零改动**;反过来页面改版式,**App 零改动**。 + +--- + +## 3. 总体架构 + +### 3.1 依赖方向 + +```mermaid +flowchart TB + subgraph APP["App 层(业务)"] + F["App/app/ui_data_feed.c
组装一帧(唯一填充者)"] + SRC["phasor / freq / prot_set / iec104_pts / sys_clock"] + F --> SRC + end + subgraph BSP["Drivers/BSP/UI(表现层)"] + D["ui_data.c/h
快照存储 + 只读访问器"] + P["ui_page_ac / ui_page_yx / ui_page_setlist ...
只读快照 + 格式化 + 布局"] + P --> D + end + T["App/task/ui_task.c
调度:每轮调 feed_tick()"] --> F + T --> P + F -- "ui_data_publish_rt/par(&帧)" --> D +``` + +**include 方向**(全部合法): + +| 文件 | include | 方向 | +|---|---|---| +| `App/app/ui_data_feed.c` | `ui_data.h`(BSP)+ 各 App 头 | App → BSP ✓ | +| `App/task/ui_task.c` | `ui_data_feed.h` + `ui_menu.h` | App → BSP ✓ | +| `Drivers/BSP/UI/ui_page_*.c` | `ui_data.h`(同层 BSP) | 同层 ✓ | +| `Drivers/BSP/UI/ui_data.c` | 仅 `stdint.h` / `string.h` | 零依赖 ✓ | + +**`ui_data.c` 不 include 任何 App 头,也不 include 任何页面头**——它是叶子。 + +### 3.2 三个模块的职责边界 + +| 模块 | 位置 | 只做 | 绝不做 | +|---|---|---|---| +| `ui_data` | `Drivers/BSP/UI/` | 存最新一帧、给只读访问、记发布序号 | 取数、格式化、判断业务含义 | +| `ui_data_feed` | `App/app/` | 按节拍从各 App 模块取数、组装、发布;变化检测 | 布局、字符串格式化、LVGL 调用 | +| 页面 `ui_page_*` | `Drivers/BSP/UI/` | 读快照、按行表格式化、交给框架绘制 | 直接调 App、自己算业务量 | + +--- + +## 4. 数据契约 + +分**两条独立通道**,因为更新频率与语义完全不同: + +| 通道 | 内容 | 节拍 | 写者 | +|---|---|---|---| +| `rt`(实时量) | 遥测 9 路 + 品质、遥信位图、频率、时钟 | 200 ms | `ui_data_feed`(uiTask 上下文) | +| `par`(参数) | 定值 40 项(Q10)+ 校验和 + 封存态 + 越界位图 | 变化触发 | `ui_data_feed`(同上下文) | + +### 4.1 实时快照 `ui_data_rt_t` + +```c +#define UI_DATA_YC_N 9u /* 语义通道数:UA/UB/UC/UN/IA/IB/IC/IN/温度 */ +#define UI_DATA_YX_N 33u /* 遥信点数(与 104 点表同序) */ + +/* 品质位(与 104 QDS 语义对齐,页面只消费前 3 位) */ +#define UI_DATA_Q_IV 0x01u /* 无效:该通道无源/不可用 */ +#define UI_DATA_Q_NT 0x02u /* 未就绪:尚未攒满一周波 */ +#define UI_DATA_Q_OV 0x04u /* 越限:超过量程/削顶 */ +#define UI_DATA_Q_SB 0x08u /* 取代值 */ +#define UI_DATA_Q_BL 0x10u /* 闭锁 */ + +typedef struct { + float val; /* 数值;口径由 ui_data_rt_t.eng 决定 */ + uint8_t qds; /* 品质位组合 */ +} ui_data_yc_t; + +typedef struct { + uint32_t seq; /* 发布序号(单调递增;页面据此判"是否需重建行缓存") */ + uint32_t tick_ms; /* 发布时刻(tick 域,差值法可比) */ + uint8_t valid; /* 0 = 尚无首帧(页面显示 "--") */ + uint8_t eng; /* 0 = 原始码值(未标定)/ 1 = 二次值 / 2 = 一次值 */ + ui_data_yc_t yc[UI_DATA_YC_N]; + uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */ + uint64_t yx_valid; /* 遥信有效位图:bit i = 1 该点有硬件来源 */ + uint8_t freq_valid; + float freq_hz; + uint8_t clock_ok; /* 0 = 时钟无效(RTC 异常) */ + char clock[20]; /* "YYYY-MM-DD HH:MM:SS" 固定长度,无指针 */ +} ui_data_rt_t; +``` + +**要点**: + +- **`eng` 字段是诚实性的关键**:V0.1 未标定,`val` 是 ADC 码值(千级)。页面据此显示"未标定"角标或直接显示码值,**不会把码值当伏特**。标定落地后 `eng` 改 1,页面自动按工程量显示,**无代码改动**。 +- **字符串只用定长数组**:`clock[20]` 在结构体内,无指针、无生命周期问题(跨层传指针是经典事故源)。 +- **`yx_valid` 与 `yx_bits` 分开**:无效点不参与显示、不参与变位判断(沿用 104 点表的既有做法,避免"IV 通道抖动成变位")。 + +### 4.2 参数快照 `ui_data_par_t` + +```c +#define UI_DATA_SET_N 40u /* 定值项数(与 prot_set 枚举同序,见 §9.2 一致性测试) */ + +typedef struct { + uint32_t seq; + uint32_t checksum; /* 与 prot_set_checksum() 同值:页面据此判"参数是否变过" */ + uint8_t valid; + uint8_t locked; /* 定值闭锁(校验和自检失败等) */ + uint8_t bad_valid; /* 越界位图有效 */ + uint8_t rsv; + uint64_t bad_mask; /* 40 位:置 1 = 该定值项越界 */ + int32_t q10[UI_DATA_SET_N]; /* Q10 原值;页面按行表的小数位换算显示 */ +} ui_data_par_t; +``` + +**为什么存 Q10 而不是 float**:定值是**整表校验和覆盖的定点值**,快照存浮点会引入"显示值与校验和口径不一致"的二义性。Q10 → 显示值的小数位换算属表现层(§8)。 + +### 4.3 扩展规则 + +- **只允许尾部追加字段**,结构体开头保留 `seq/valid`。 +- 追加字段必须同时升 `UI_DATA_VER`(编译期常量),并在 `ui_data_feed` 里填默认值。 +- **禁止删除或重排字段**(页面按名字取,重排是静默错位)。 + +--- + +## 5. 并发与时序 + +### 5.1 单写者 + 同上下文(核心简化) + +**约定**:`rt` 与 `par` 的**唯一发布者**是 `ui_data_feed_tick()`,它在 **uiTask 上下文**运行;页面的读取也在 uiTask 上下文(绘制钩子与按键钩子都跑在 `ui_task_func` 主循环里,见 `docs/UI设计/内容页架构.md` §六)。 + +→ **读写同线程,不需要锁、不需要双缓冲、不需要 seqlock。** + +这一点值得展开,因为它与"直觉上要加保护"相反: + +| 顾虑 | 实际情况 | +|---|---| +| 发布时页面正在绘制? | 不可能:绘制与发布都在 uiTask,串行执行 | +| 104 主站写定值会改快照? | 不会:主站只改 `prot_set`,快照由 feed 下一拍重新拉取 | +| 其他任务要推数据? | 不允许——要加新数据,就在 feed 里加取数,仍由 uiTask 发布 | + +**如果将来真出现第二个发布者**(例如 netTask 收到主站写定值后要"立刻"刷新 UI),扩展路径是:给 `ui_data` 内部加"双槽 + 每槽 seq"(写非活动槽 → 盖 seq → 翻转单字节活动下标,同 `phasor` 双区先例)。**页面代码不受影响**——这正是把并发策略藏在 `ui_data` 内部的价值。 + +### 5.2 发布节拍 + +| 通道 | 节拍 | 理由 | +|---|---|---| +| `rt` | **200 ms**(内部计数分频,UI tick 10 ms × 20) | 与遥测链目标刷新率一致;人眼在 160×160 单色屏上感知不到更快 | +| `par` | **变化触发**:每 200 ms 比一次 `prot_set_checksum()`,变则重发 | 避免每拍拷 160 B;定值变化是低频事件 | + +`ui_data_feed_tick()` 每轮调用是**廉价**的:一次 tick 差值判断 + 一次 uint32 比较。真正的组装只在到期时发生。 + +### 5.3 启动时序 + +``` +内核启动前:ui_data_init()(清零 + valid=0)—— 纯内存操作,无 OS 依赖 + ↓ +uiTask 起来(NET_READY 屏障后):ui_menu_init() → 进入主循环 + ↓ +首轮循环:ui_data_feed_tick() 立即发布首帧(不等 200 ms) + ↓ +首帧之前任何页面被打开:valid=0 → 全部显示 "--"(不显示 0.000,避免误读为真实零值) +``` + +**为什么首帧要立即发**:待机屏与菜单屏在启动早期就可能被远端推流打开,若等 200 ms 会出现"一闪的 `--`"。首帧立即发布后,页面从第一帧就是真值(或明确的"未就绪")。 + +### 5.4 页面的行缓存(性能) + +框架的 `refresh(row, name, value)` 是**逐行调用**的(每帧每行一次)。若每行都 `ui_data_rt(&snap)` 拷 130 B,一页 8 行 = 1 KB/帧的冗余拷贝。 + +**做法**:页面用 `ui_data_rt_seq()` 判"快照是否变过",变过才整帧拷进**页面私有静态缓存**,其余行只读缓存。 + +```c +static ui_data_rt_t s_rt; /* 页面私有缓存 */ +static uint32_t s_seq; /* 上次拷贝时的序号 */ + +static void pg_refresh(uint16_t row, char *name, char *value) { + uint32_t seq = ui_data_rt_seq(); /* 轻量查询:只读一个 uint32 */ + if (seq != s_seq) { /* 快照变过才整帧拷贝 */ + (void)ui_data_rt(&s_rt); + s_seq = seq; + } + /* 以下只读 s_rt,按行表格式化 */ +} +``` + +> 用 `ui_data_rt_seq()` 门控而不是"row==0 才刷新",是为了避免依赖"框架总从第 0 行开始绘制"这一隐式时序假设——框架换绘制顺序时页面不会静默出错。 + +--- + +## 6. 写路径(定值整定) + +### 6.1 为什么 UI 不能直写 + +- `prot_set_set()` 之后**必须** `prot_set_seal()` 重算校验和;逐项写会把校验和打成中间态,被自检判为损坏。 +- 落盘由 `defaultTask`(配置引擎属主)执行,UI 直接写盘会撞单写者纪律。 +- 越界校验是 all-or-nothing:任一项越界整表不装载。 + +### 6.2 请求 / 状态交接(复用 `net_cfg` 范式) + +``` +ui_page_setlist(编辑态) + │ 用户按"保存" + ▼ +ui_data_param_request(&req) ← BSP 侧只投递,不执行 + │ (小环形队列,深度 4;满则返回 -1,页面提示"忙") + ▼ +ui_data_feed_tick()(App 侧,uiTask 上下文) + │ 取出请求 → 校验(调 prot_set 的只读接口预检) + ▼ +prot_set_set_sealed() / prot_set_set() + prot_set_seal() + │ 成功 → prot_set_dirty_mark() → defaultTask 落盘 + ▼ +ui_data_param_state(OK / ERR_RANGE / ERR_LOCKED / ERR_BUSY) + │ + ▼ +ui_page_setlist 显示"保存成功 / 越界:Ⅲ段定值" +``` + +**这套"UI 只投递、属主任务执行、状态回读"的结构,与 `net_cfg` 的 `net_cfg_save_request` / `net_cfg_save_state` 完全同构**(`Drivers/BSP/NETCFG/net_cfg.h`),不引入新范式——这是"低耦合"的另一个含义:**项目里同类问题只有一种解法**。 + +### 6.3 失败语义 + +| 返回 | 含义 | 页面表现 | +|---|---|---| +| `OK` | 已封存并置脏 | 行值刷新,页脚短暂"已保存" | +| `ERR_RANGE` | 某项越界 | 高亮越界行(用 `bad_mask`),页脚"越界" | +| `ERR_LOCKED` | 定值闭锁中(自检失败) | 页脚"定值闭锁",禁止编辑 | +| `ERR_BUSY` | 请求队列满 | 页脚"忙,请重试" | +| `ERR_TIMEOUT` | 3 s 未取走 | 页脚"无响应" | + +--- + +## 7. 记录页:另一种通道(注入式 reader) + +### 7.1 为什么记录不走快照 + +记录是**分页读文件**:条数随运行增长,不可能全量快照;且读盘有 I/O 延迟,不能放在绘制路径里同步做。 + +### 7.2 注入式 reader + +把 `ui_w_records` 从"直接 include `sys_log.h`"改为"接受一个只读函数表": + +```c +/* Drivers/BSP/UI/ui_w_records.h —— 不含任何 App 头 */ +typedef struct { + uint16_t (*count)(uint8_t log_id); + /* 返回 0 成功;-1 越界;-2 读失败 */ + int (*get)(uint8_t log_id, uint32_t idx, + char *ts, uint32_t ts_len, char *desc, uint32_t desc_len); +} ui_rec_reader_t; + +void ui_w_records_bind(const ui_rec_reader_t *reader); /* App 启动时注入 */ +``` + +- `log_id` 用 `uint8_t`(**不透明编号**),BSP 不知道 `SYSLOG_RUN` 是什么,只知道"0 号日志"。 +- 注入点在 `App/task/ui_task.c`(`ui_menu_init` 之前),实现体是 `sys_log` 的两个薄包装。 + +### 7.3 收益:可削减 3 条既有基线 + +改造完成后,`scripts/check_layer_deps.py` 的 `BASELINE` 可删掉: + +``` +("Drivers/BSP/UI/ui_page_runlog.c", "App/log/sys_log.h"), +("Drivers/BSP/UI/ui_page_selftest.c", "App/log/sys_log.h"), +("Drivers/BSP/UI/ui_w_records.h", "App/log/sys_log.h"), +``` + +基线"只减不增"——**本设计顺带把存量债还掉三条**,而不是新增。 + +> 另外两条(`net_cfg` / `ftp_cfg` → `cfg_file.h`)属配置引擎,与 UI 无关,不在本文范围。 + +--- + +## 8. 格式化归属:谁决定小数位 + +**结论:格式化属表现层,全部在页面行表里;快照只给数值与品质。** + +| 信息 | 归属 | 理由 | +|---|---|---| +| 数值(float / Q10) | 快照 | 业务事实 | +| 品质(IV/NT/OV) | 快照 | 业务事实 | +| 通道名("Ua"/"Ia") | 页面行表 | 显示文案 | +| 小数位(2 位/3 位) | 页面行表 | 显示规格 | +| 单位(V/A/Hz) | 页面行表 | 显示规格 | +| 无效符号(`--`) | 页面行表 | 显示规格 | +| 排序/分组/分页 | 页面行表 | 版式 | + +行表结构相应扩展(在现有 `cp_row_t` 基础上加两个字段,或新增页面侧结构): + +```c +typedef struct { + const char *name; /* "Ua" */ + uint8_t src; /* 快照内下标(0..UI_DATA_YC_N-1) */ + uint8_t dot; /* 小数位:电压 2 / 电流 3 / 频率 3 */ + const char *unit; /* "V" / "A" / "Hz" */ +} ui_ac_row_t; +``` + +**为什么不在快照里放格式化后的字符串**:那会让 App 知道"电压要 2 位小数",把显示规格漏进业务层——这正是要避免的耦合。 + +--- + +## 9. 可测试性 + +### 9.1 主机单测(L0.5) + +`ui_data` 是纯内存模块,天然可测: + +| 用例 | 判据 | +|---|---| +| 首帧前读取 | `ui_data_rt()` 返回 -1,`valid=0` | +| 发布后读取 | 字段逐项一致;`seq` 递增 | +| 参数变化检测 | checksum 不变 → 不重发;变 → 重发 | +| 请求队列 | 深度 4;满则 -1;取走后可再投 | +| 状态回读 | OK/ERR_RANGE/ERR_LOCKED 正确映射 | + +页面渲染可在模拟器侧断言(见 9.2),主机侧只测数据模块。 + +### 9.2 模拟器:固定帧保证 golden 稳定 + +`simulator/main.c` 不链接 `App/task/ui_task.c`,因此需要自己发布快照: + +- **默认(golden 模式)**:发布一帧**固定数据**(写死的 9 路值 + 位图),保证像素参考可复现。 +- **`--live` 模式**:用合成正弦逐帧更新,供人工观察动态刷新(不参与 golden)。 + +这是本设计对"可测性"的直接贡献:**页面数据来源可注入 → 像素回归不再依赖真实硬件**。 + +### 9.3 与"虚拟注入"的关系 + +`docs/UI设计/页面补齐规划.md` §7 提的厂家调试"虚拟注入",**天然落在 `ui_data_feed` 上**:注入值只要在 feed 组装时覆盖对应字段即可,**页面完全不知情**。 + +约束(必须写进代码注释):虚拟注入**只能在 feed 层覆盖快照字段**,绝不能改 `prot_set` / `phasor` 的输入——否则会污染保护判据路径。 + +### 9.4 一致性测试(防"魔法下标"漂移) + +页面行表用**不透明下标**(`src` / `set_idx`),BSP 侧不知道它对应哪个 App 枚举——这是解耦的代价,必须用测试兜住: + +| 断言 | 位置 | 内容 | +|---|---|---| +| 通道数一致 | `App/app/ui_data_feed.c` | `typedef char chk[(UI_DATA_YC_N == ADC_SEM_COUNT) ? 1 : -1];` | +| 定值项数一致 | 同上 | `typedef char chk2[(UI_DATA_SET_N == PS_ID_COUNT) ? 1 : -1];` | +| 下标语义一致 | `test/host/ui_data/` | 主机测试同时 include 两侧头,逐项断言"页面表的 src/set_idx 指向预期枚举" | + +第 3 条是关键:**它把"跨层魔法数字"从运行期风险降级为编译/测试期错误**。 + +--- + +## 10. 内存与性能 + +| 项 | 量 | 说明 | +|---|---|---| +| `ui_data` rt 快照 | ≈ 132 B | 9×8 + 位图 + 时钟串 | +| `ui_data` par 快照 | ≈ 180 B | 40×4 + 头部 | +| 状态/队列 | ≈ 60 B | 请求环 4 条 + 状态字 | +| **合计(RW_IRAM1)** | **≈ 380 B** | 当前 RW_IRAM1 余 57,504 B → 无压力 | +| 页面行缓存 | 每页 ≈ 132 B(仅数据页) | 页面私有静态,随页数线性;若 6 个数据页 ≈ 800 B | +| LVGL 池 | **0** | 快照不进池(不是 LVGL 对象) | +| 每 200 ms 拷贝 | ≈ 132 B | 一次 memcpy,微秒级 | +| 每帧(10 ms) | 0 次拷贝(比 seq 后只在变化时拷) | 见 §5.4 | + +**结论**:代价是"几百字节 + 每 200 ms 一次 memcpy",换来的是整个 UI 层的数据可及性。 + +--- + +## 11. 落地步骤(每步可独立验证) + +| 步 | 内容 | 验证 | 依赖 | +|---|---|---|---| +| **步骤1** | `ui_data.h/.c` + 主机单测(无任何消费者) | `test/host/ui_data` 全绿;`layer-deps` 无新增 | 无 | +| **步骤2** | `App/app/ui_data_feed.c` + `ui_task.c` 接线(每轮调 tick) | 串口打印首帧(临时锚点);`@build` 0/0 | 步骤1 | +| **步骤3** | 交流量 / 直流量回炉(接快照 + 行表加 `src/dot/unit`) | 模拟器固定帧 golden 更新;台架看真实码值变化 | 步骤2 | +| **步骤4** | 遥信量 + 定值/控制字/软压板查看(4 页) | 主站改定值 → 页面 200 ms 内变 | 步骤3 | +| **步骤5** | 写路径(请求/状态)+ 整定 4 页 | 就地改定值 → 保存 → 掉电重启 → 值保持 | 步骤4 | +| **步骤6** | 记录 reader 注入 + 删 3 条 layer-deps 基线 | `check_layer_deps.py` 基线由 6 → 3 | 可与步骤3 并行 | + +**步骤1~步骤2 是关键路径**:它们不产出任何用户可见效果,但决定后面所有页面是否可信。**不要跳过步骤1 直接改页面**——那会退化成"页面里塞假数据 2.0"。 + +--- + +## 12. 验收判据 + +| # | 判据 | 方法 | +|---|---|---| +| 1 | `Drivers/BSP/**` 无新增 App include | `scripts/check_layer_deps.py` 退出码 0 | +| 2 | 记录页改造后基线由 6 条减到 3 条 | 同上 `--prune` | +| 3 | 交流量页显示真实相量(非写死值) | 台架:注入已知信号 → 页面值随信号变 | +| 4 | 未标定时页面明确标识(不把码值当伏特) | 目检 + 文本断言(`eng=0` 时带标记) | +| 5 | 首帧前显示 `--` 而非 `0.000` | 模拟器:不 publish 直接开页,断言文本 | +| 6 | 定值整定全链闭环 | 就地改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致 | +| 7 | 像素回归稳定 | `ui-golden` 连续两次运行零差异 | +| 8 | 主机单测覆盖数据模块 | `test/host/ui_data` 全绿并挂 pre-commit 钩子 | + +--- + +## 13. 反模式清单(禁止事项) + +| # | 禁止 | 原因 | +|---|---|---| +| 1 | BSP 里 `extern` App 全局变量 | 绕过门禁且比 include 更危险(无契约/无所有权) | +| 2 | 页面直接 include `App/**` 头 | 门禁拦截;且页面被绑死在具体 App API | +| 3 | 快照里放指针或动态字符串 | 跨层生命周期事故;只用定长数组 | +| 4 | 每行 `refresh` 都整帧拷贝快照 | 8 行/帧冗余;用 seq 门控 | +| 5 | 在快照里放格式化后的字符串 | 显示规格漏进业务层 | +| 6 | 多个任务同时发布同一通道 | 破坏单写者约定;需要时在 `ui_data` 内部加保护,不改页面 | +| 7 | 虚拟注入改 `prot_set`/`phasor` 输入 | 会污染保护判据路径 | +| 8 | 定值逐项 `prot_set_set()` 不封存 | 校验和中间态 → 自检判损坏 | +| 9 | 为省事把 `sys_log.h` 加进 `ALLOW` | ALLOW 是"叶子工具"通道,`sys_log` 是有状态服务,加进去等于永久开后门 | + +--- + +## 14. 与既有文档和门禁的关系 + +- **门禁**:`scripts/check_layer_deps.py` 的三种合法做法本文全部用上(快照推送=②,写路径与记录 reader=①),**不新增任何 `ALLOW` 或 `BASELINE` 条目**,并计划削减 3 条。 +- **页面框架**:`docs/UI设计/内容页架构.md` 的 `refresh` / `count_cb` 钩子**签名不变**,页面只是改为从 `ui_data` 取数。 +- **页面清单**:`docs/UI设计/页面补齐规划.md` 的"批 1"第一项即本文 步骤1~步骤3。 +- **数据真实性**:`docs/问题记录/采集与数据处理短板评测.md` 的标定(§4.1)与遥测链(§4.2)落地后,只需改 `ui_data_feed` 的取数来源与 `eng` 取值,页面零改动。 diff --git a/docs/UI设计/页面扩充详细方案.md b/docs/UI设计/页面扩充详细方案.md new file mode 100644 index 00000000..0a0959b9 --- /dev/null +++ b/docs/UI设计/页面扩充详细方案.md @@ -0,0 +1,557 @@ +# 页面扩充详细方案(27 个空叶子 + 3 页回炉) + +> **定位**:`docs/UI设计/页面补齐规划.md` 回答"怎么归并、分几批";本文回答**每一页显示什么、数据从哪来、要改哪些代码**——可直接照着开工的粒度。 +> **数据通道**:`docs/UI设计/UI_数据供给设计说明.md`(快照推送 + 写路径 + 记录 reader 注入)。 +> **数据真实性**:`docs/问题记录/采集与数据处理短板评测.md`(标定、遥测链、运行参数段的缺口)。 +> **统计口径**:本文所有节点/叶子数字均由 `ui_menu_tree.txt` 脚本统计 + `Drivers/BSP/UI/ui_page_registry.c` 交叉核对,不引用旧文档结论。 + +--- + +## 1. 核对结论 + +### 1.1 统计口径与结果 + +| 项 | 数 | 来源 | +|---|---|---| +| 菜单节点总数 | **42** | `ui_menu_tab.h` 的 `UI_MENU_TAB_COUNT` | +| 叶子节点 | **33** | 无子项的节点 | +| 已绑定内容页 | **6** | `ui_page_registry.c` 的 `g_ui_page_map[]` | +| 密码页 | 1 | 非节点绑定(`@password=password`) | +| **空叶子** | **27** | 33 − 6 | + +已绑定 6 页:`装置信息`(node 1)、`交流量`(3)、`直流量`(4)、`网口设置`(30)、`自检记录`(39)、`运行记录`(40)。 + +### 1.2 27 个空叶子清单(按菜单顺序) + +| # | 叶子 | 节点 | 密码 | 父节点 | +|---|---|---|---|---| +| 1 | 遥信量 | 5 | — | 实时数据 | +| 2~5 | 参数 / 定值 / 控制字 / 软压板 | 8~11 | ✓ | 参数定值 → 整定 | +| 6~9 | 参数 / 定值 / 控制字 / 软压板 | 13~16 | — | 参数定值 → 查看 | +| 10~14 | 遥测死区 / 遥测系数 / 遥信类型 / 遥信防抖 / 双点遥信 | 18~22 | ✓ | 三遥设置 | +| 15~18 | 时钟设置 / 强制复归 / 手动录波 / 清除记录 | 24~27 | ✓ | 装置维护 | +| 19~20 | 通讯设置 / SNTP设置 | 29、31 | ✓ | 通讯参数(网口设置 30 已有页) | +| 21~27 | SOE记录 / 事故记录 / 操作记录 / 保护告警 / 保护启动 / 遥控记录 / 运行报告 | 33~38、41 | — | 记录查询 | + +> 记录查询下的 9 个叶子是**平级**的(`SOE记录` 不是分组节点),因此 SOE 页与其余 8 页同级,不需要为它单独加一层菜单。 + +### 1.3 已建 6 页的健康度 + +| 页 | 状态 | 证据 | +|---|---|---| +| 网口设置 | ✅ 真数据 | `net_cfg_get()` + 保存交接 `net_cfg_save_request/state` | +| 自检记录 | ✅ 真数据 | `ui_w_records` → `syslog_count/get_record` | +| 运行记录 | ✅ 真数据 | 同上 | +| 装置信息 | ⚠ 静态字面量 | `s_rows[]` 6 行写死(`SV0.010` / `HW0.010` / `4454` / `2024.08.27`) | +| 交流量 | ⚠ 静态字面量 | `s_rows[]` 24 行全是 Flash 字面量,**不会变**(第 1 页数值取自参考图实测,看着像真值) | +| 直流量 | ⚠ 静态字面量 | `s_rows[]` 3 行写死(`ZL01/ZL02/Pa1`) | + +**结论**:真正接通的只有 3 页(网口 + 2 个记录页)。所以"要动"的页数是 **27 个空叶子 + 3 页回炉 = 30 页次**。 + +### 1.4 与既有规划的关系 + +`docs/UI设计/页面补齐规划.md` 已把 27 个空叶子归并为 **7 个页面模块 + 1 个共享组件 + 2 项框架能力 + 1 个数据层**。本文沿用该归并,但**新增三项**(核对后才发现必需): + +| 新增项 | 为什么规划里没有 | +|---|---| +| `ui_w_soe`(二进制 SOE 解码组件) | 规划只写了"需新组件",没定接口 | +| `ui_page_action`(确认操作页) | 强制复归 / 清除记录 / 手动录波三处共用,规划里散落在"操作页" | +| `run_par`(运行参数模型) | 三遥设置 5 页的地基;规划里当成"数据现成",实际整段不存在 | + +--- + +## 2. 数据可用性:V0.1 到底能显示什么 + +这一节决定每页的"天花板"。**页面写得再漂亮,没有数据源就是假值 2.0。** + +### 2.1 现有真值清单(可被页面消费) + +| 数据 | API | 状态 | 说明 | +|---|---|---|---| +| 相量 Ua | `phasor_get(1, &p)` → `p.fAmSQ` | ✅ 真(**码值口径**) | 系数表只含 √2/N,未折算工程量 | +| 相量 U0(UN 通道) | `phasor_get(2, &p)` | ✅ 真(码值) | 零序电压基波 | +| 相量 Ia | `phasor_get(0, &p)` | ✅ 真(码值) | 过流/过负荷判据源 | +| 相量 Ia-2 次 | `phasor_get(3, &p)` | ✅ 真(码值) | 涌流制动用,页面可显示 | +| 相量 Ia-5 次 | `phasor_get(4, &p)` | ✅ 真(码值) | 选线用 | +| 频率 | `freq_get(&f)` + `freq_valid()` | 🔶 有条件 | 等周波未闭环(`docs/算法/继电保护功能复现规划.md` L1-7),104 点 `0x401D` 仍标 IV | +| 温度 | `adc_input_raw(ADC_SEM_TEMP, &raw)` | 🔶 仅码值 | 无换算 API | +| 遥信 6 点 | `iec104_pts_yx_read(idx, &v)` | ✅ 真 | YX15/16/17/18/23/31(`YX_VALID_MASK`) | +| 遥信 27 点 | 同上 | ❌ 恒无效 | 无硬件来源(开入未配置) | +| 定值 40 项 | `prot_set_get_f/get_ctl/checksum/locked/bad_mask` | ✅ 真 | Q10 定点 | +| 时钟 | `sys_clock_get_str(buf, len)` | ✅ 真 | 墙钟(可能慢 8 h,见台架记录) | +| 网口 | `net_cfg_get()` | ✅ 真 | IP/掩码/网关/MAC | +| 日志 | `syslog_count/get_record` | ✅ 真 | 仅运行、自检 2 类 | +| SOE | **无读取 API**(只有 `soe_store_post/drain`) | ❌ 需新增 | `/log/soe.log` 二进制 64 B | +| 元件状态 | `prot_elm_state/active_mask/name` | ✅ 真 | 当前态,无历史 | +| 功率/电度 | — | ❌ 禁止 | N-18:跨转换器不同步,V0.1 不得算功率 | +| 直流通道 | — | ❌ 无硬件 | `ADC_SEM` 只有 8 电气 + 温度 | +| 开入/开出 | — | ❌ 无代码 | 固件完全没有开入开出实现 | + +### 2.2 三档可用性分类(27 个空叶子的分档) + +| 档 | 含义 | 页数 | 叶子 | +|---|---|---|---| +| **A 类** | 数据现成,只差页面 | **17** | 参数/定值/控制字/软压板 ×2(8)、遥信量、时钟设置、强制复归、清除记录、事故/操作/告警/启动/遥控记录(5) | +| **B 类** | 需新数据层或新 API | **8** | 遥测死区/遥测系数/遥信类型/遥信防抖/双点遥信(5)、SOE记录、运行报告、通讯设置 | +| **C 类** | 功能本身不存在 | **2** | 手动录波(无录波)、SNTP设置(无 SNTP 客户端) | + +> A 类 17 页里有 5 页(事故/操作/告警/启动/遥控)**页面是 S 但写入方是 M**——`sys_log` 只有 2 个 ID,5 类新记录要先有写入点。这是本方案最容易低估的地方。 +> +> C 类的 2 页与"直流量无通道"是同一个性质的问题:不是页面没做,是**被测对象不存在**。页面做得再好也只能显示"未配置"。 + +### 2.3 对页面设计的硬约束 + +1. **未标定必须可见**:`eng=0` 时页面要显式标"未标定",否则码值被当成伏特(现场误读)。 +2. **无源点显示 `--`**,不显示 `0.000`。 +3. **不在 UI 层算业务量**:功率、电度、方和根一律等 App 侧给出。 +4. **中文名 ≤ 7 字**:左列 x=7、值列 x=101 → 名称可用宽度约 94 px,12 px 字宽 ≈ 7 个汉字。 +5. **日志描述 ≤ 9 汉字**:`SYSLOG_DESC_MAX=28` 字节,UTF-8 汉字 3 B/字。 + +--- + +## 3. 逐页方案 + +### 3.1 实时数据(3 页) + +#### 3.1.1 交流量(回炉,24 行 → 7 行) + +**为什么不是 24 行**:参考布局的 24 行是"双回线 + 三相 + 功率 + 极值"的产物。V0.1 只有 8 个电气通道,其中 5 个(UB/UC/IB/IC/IN)在 104 点表里明确标 `IV`(削顶),相量实例只有 5 个(IA-F1 / UA-F1 / UN-F1 / IA-F2 / IA-F5)。**照抄 24 行等于 17 行 `--`**。 + +| 行 | 显示名 | 数据源 | 小数位 | 单位 | +|---|---|---|---|---| +| 1 | `Ua` | `phasor_get(1)` → `fAmSQ` | 3 | — | +| 2 | `U0` | `phasor_get(2)` | 3 | — | +| 3 | `Ia` | `phasor_get(0)` | 3 | — | +| 4 | `Ia2` | `phasor_get(3)`(二次谐波) | 3 | — | +| 5 | `Ia5` | `phasor_get(4)`(五次谐波) | 3 | — | +| 6 | `Hz` | `freq_get` + `freq_valid` | 2 | Hz | +| 7 | `标定` | 快照 `eng` → `未标定`/`二次值`/`一次值` | — | — | + +- **第 7 行是诚实性行**:它不是测量值,是"上面 6 行的口径说明"。标定落地后自动变"二次值",无需改页面。 +- 单页 7 行(8 行/页),不分页。 +- 数据来源:`ui_data_rt_t` 的 `yc[0..5]`(下标语义见 `docs/UI设计/UI_数据供给设计说明.md` §9.4 一致性测试)。 +- 规模:**S**(改行表 + `refresh` 钩子,约 80 行)。 + +**待拍板**:是否接受"7 行"而非参考的"24 行"。若产品要求保留 24 行骨架(便于将来扩通道),则改成 24 行固定表、当前 17 行显 `--`。 + +#### 3.1.2 直流量(回炉,3 行 → 2 行) + +V0.1 **无直流采样通道**(`ADC_SEM` 只有 8 路电气 + 温度),点表 `0x401E/0x401F` 也注明"FTU 无"。 + +| 行 | 显示名 | 数据源 | 说明 | +|---|---|---|---| +| 1 | `温度` | `adc_input_raw(ADC_SEM_TEMP)` | 码值,未标定 | +| 2 | `直流通道` | 常量 | 值列显示 `未配置` | + +- 规模:**S**。 +- **待拍板**(三选一):① 按上表做(推荐,保留菜单语义与点表一致);② 菜单改名"温度量"(改产品语义);③ 删节点(与参考/点表不一致)。 + +#### 3.1.3 遥信量(新建) + +33 点表,但只有 6 点有硬件来源(`YX_VALID_MASK`:YX15/16/17/18/23/31)。 + +| 方案 | 行表 | 优点 | 缺点 | +|---|---|---|---| +| **A 固定 33 行(推荐)** | 33 行固定名表,无效点值显 `--` | 与点表一一对应,现场可对点;开入落地后**页面零改动**自动变真 | 5 页里 4 页多 `--` | +| B 只显有效点 | `count_cb` 返回有效点数 | 紧凑 | 看不出缺哪些点;开入落地后行数变化,`ui-golden` 需重做 | + +推荐 A。行表(节选,全 33 项按点表): + +| 行 | 名 | 有效 | 值 | +|---|---|---|---| +| 1~4 | `YK1~4 位置` | ❌ | `--`(双点,无硬件) | +| 5~14 | `YX5~14` | ❌ | `--` | +| 15 | `故障总` | ✅ | `动作`/`复归` | +| 16 | `过流Ⅰ段` | ✅ | 同上 | +| 17 | `过流Ⅱ段` | ✅ | 同上 | +| 18 | `过流Ⅲ段` | ✅ | 同上 | +| 19 | `零序Ⅰ段` | ❌ | `--` | +| 20 | `零序Ⅱ段` | ❌ | `--` | +| 21 | `接地故障` | ❌ | `--` | +| 22 | `重载` | ❌ | `--`(定值源缺失,见 `docs/问题记录/采集与数据处理短板评测.md` §4.3 运行参数段) | +| 23 | `过载` | ✅ | `动作`/`复归` | +| 24~30 | `外部遥信1~7` | ❌ | `--` | +| 31~33 | `闭锁1~3` | 31 ✅ | 其余 `--` | + +- 名表放页面侧(Flash const),**不进快照**——显示文案属表现层(`docs/UI设计/UI_数据供给设计说明.md` §8)。 +- 双点遥信 4 点用 `iec104_pts_yx_is_dp()` 决定是否显示"分/合",当前恒无效。 +- 规模:**M**(33 行名表 + `refresh` + 值映射,约 150 行)。 + +### 3.2 参数定值(8 叶子 → 2 个模块) + +8 个叶子(整定 4 + 查看 4)共用**一份** `ui_w_setlist` 组件 + **4 张视图表**。查看与整定的差别只是 `edit_capable` 与权限。 + +#### 3.2.1 40 项四视图划分 + +| 视图 | 项数 | 判据 | 页数(8 行/页) | +|---|---|---|---| +| 参数 | 8 | PT/CT 额定 + 4 个返回系数 | 1 | +| 定值 | 18 | 数值型(电流/电压/频率/延时) | 3 | +| 控制字 | 4 | `*_TRIP`(0 告警 / 1 跳闸) | 1 | +| 软压板 | 10 | `*_EN` + `PS_GLOBAL_EN` | 2 | +| **合计** | **40** | 与 `PS_ID_COUNT` 相等 | 7 | + +> 划分只改**显示分组**,不改枚举序、不改校验和覆盖序(`prot_set` 的枚举序 = 数组序 = 校验和序,只增不删不改序)。 + +#### 3.2.2 参数视图(8 项) + +| 行 | 显示名 | 枚举 | 单位 | 小数位 | +|---|---|---|---|---| +| 1 | `PT一次` | `PS_PT_PRI` | V | 0 | +| 2 | `PT二次` | `PS_PT_SEC` | V | 0 | +| 3 | `CT一次` | `PS_CT_PRI` | A | 0 | +| 4 | `CT二次` | `PS_CT_SEC` | A | 0 | +| 5 | `过量返回` | `PS_RET_OVER` | — | 3 | +| 6 | `欠量返回` | `PS_RET_UNDER` | — | 3 | +| 7 | `过频返回` | `PS_RET_OF` | — | 3 | +| 8 | `低频返回` | `PS_RET_UF` | — | 3 | + +#### 3.2.3 定值视图(18 项) + +| 行 | 显示名 | 枚举 | 单位 | 小数位 | +|---|---|---|---|---| +| 1 | `Ⅰ段电流` | `PS_OC1_CUR` | A | 2 | +| 2 | `Ⅰ段延时` | `PS_OC1_DLY` | ms | 0 | +| 3 | `Ⅱ段电流` | `PS_OC2_CUR` | A | 2 | +| 4 | `Ⅱ段延时` | `PS_OC2_DLY` | ms | 0 | +| 5 | `Ⅲ段电流` | `PS_OC3_CUR` | A | 2 | +| 6 | `Ⅲ段延时` | `PS_OC3_DLY` | ms | 0 | +| 7 | `过负荷电流` | `PS_OL_CUR` | A | 2 | +| 8 | `过负荷延时` | `PS_OL_DLY` | ms | 0 | +| 9 | `过压定值` | `PS_OV_VOL` | V | 1 | +| 10 | `过压延时` | `PS_OV_DLY` | ms | 0 | +| 11 | `低压定值` | `PS_UV_VOL` | V | 1 | +| 12 | `低压延时` | `PS_UV_DLY` | ms | 0 | +| 13 | `零序定值` | `PS_3U0_VOL` | V | 1 | +| 14 | `零序延时` | `PS_3U0_DLY` | ms | 0 | +| 15 | `过频定值` | `PS_OF_HZ` | Hz | 2 | +| 16 | `过频延时` | `PS_OF_DLY` | ms | 0 | +| 17 | `低频定值` | `PS_UF_HZ` | Hz | 2 | +| 18 | `低频延时` | `PS_UF_DLY` | ms | 0 | + +#### 3.2.4 控制字视图(4 项) + +| 行 | 显示名 | 枚举 | 值 | +|---|---|---|---| +| 1 | `Ⅰ段跳闸` | `PS_OC1_TRIP` | `告警`/`跳闸` | +| 2 | `Ⅱ段跳闸` | `PS_OC2_TRIP` | 同上 | +| 3 | `Ⅲ段跳闸` | `PS_OC3_TRIP` | 同上 | +| 4 | `过负荷跳闸` | `PS_OL_TRIP` | 同上 | + +#### 3.2.5 软压板视图(10 项) + +| 行 | 显示名 | 枚举 | 值 | +|---|---|---|---| +| 1 | `Ⅰ段投入` | `PS_OC1_EN` | `投入`/`退出` | +| 2 | `Ⅱ段投入` | `PS_OC2_EN` | 同上 | +| 3 | `Ⅲ段投入` | `PS_OC3_EN` | 同上 | +| 4 | `过负荷投入` | `PS_OL_EN` | 同上 | +| 5 | `过压投入` | `PS_OV_EN` | 同上 | +| 6 | `低压投入` | `PS_UV_EN` | 同上 | +| 7 | `零序投入` | `PS_3U0_EN` | 同上 | +| 8 | `过频投入` | `PS_OF_EN` | 同上 | +| 9 | `低频投入` | `PS_UF_EN` | 同上 | +| 10 | `保护总投入` | `PS_GLOBAL_EN` | 同上 | + +#### 3.2.6 查看与整定的差别 + +| 项 | 查看(4 页) | 整定(4 页) | +|---|---|---| +| 权限 | 无密码 → `UI_PERM_VIEW` | 密码 → 按所输密码授 `VIEW`/`MODIFY` | +| `edit_capable` | 0 | 1 | +| 页脚 | 只显示页码 | 额外一行"保存"(第 8 行固定为保存行) | +| 提交 | — | 整表 `prot_set_set*` + `prot_set_seal()` + 置脏,走请求/状态交接 | + +> 保存行占掉一个可见行 → 整定页行数 = 视图项数 + 1,分页随之增加:参数 9 行 2 页、定值 19 行 3 页、控制字 5 行 1 页、软压板 11 行 2 页(合计 8 页)。这是"保存必须整表封签"带来的必然代价,不是设计缺陷。 + +#### 3.2.7 编辑交互(复用网口设置页的模型) + +| 键 | 正常模式 | 编辑模式 | +|---|---|---| +| ↑↓ | 移选(框架内置) | 当前位 +1 / −1(按项步进) | +| ←→ | ← 返回上级(框架) | 移动数字位光标 | +| ENTER | 进编辑 / 触发保存行 | 确认本项(写 RAM 副本) | +| ESC | 关页(框架恒可拦截除外) | 关页(未保存的调整留在 RAM,不落盘) | + +- 权限不足时 ENTER 一律忽略(`ui_content_page_is_editable()` 判)。 +- 越界项在保存失败后由 `bad_mask` 高亮(`draw_cb` 画反显)。 +- 规模:`ui_w_setlist` **M**(约 250 行),`ui_w_editor` **M**(约 180 行)。 + +### 3.3 三遥设置(5 页,阻塞于 `run_par`) + +**现状**:死区是**编译期常量**——`App/protocol/iec104/iec104_pts.c` 的 `k_yc[].deadband`(第 244 行 `if (d > k_yc[i].deadband)`)。其余四项(系数/类型/防抖/双点)在固件里**完全没有对应结构**。 + +**所以这 5 页不是页面工作,是一个参数模型工作。** + +| 页 | 项数 | 数据模型 | 消费方 | +|---|---|---|---| +| 遥测死区 | 9 | `run_par.deadband[9]`(float,工程量) | `iec104_pts_cov_check`(替换 `k_yc[].deadband`) | +| 遥测系数 | 9 | `run_par.gain[9]`(Q10 微调,1.000 基准) | `iec104_pts_yc_read`(乘在 fAmSQ 上) | +| 遥信类型 | 33 | `run_par.yx_type[33]`(单点/双点/常闭) | `iec104_pts` 的 `M_SP`/`M_DP` 选择 | +| 遥信防抖 | 33 | `run_par.yx_debounce[33]`(ms) | 开入采集(尚未实现,**当前无消费方**) | +| 双点遥信 | 4 | `run_par.dp_map[4]`(虚端子映射) | 开入采集(同上) | + +**关键判断**: + +- 死区/系数两项**有消费方**(104 层),做完立刻生效 → 值得先做。 +- 遥信类型/防抖/双点三项的消费方是**开入采集**,而开入采集在固件里不存在 → 现在做页面 = 配置无处生效,属于"存了不用的参数"。**建议随开入量采集一起做**,不在本批。 +- 因此本批只做 **2 页(死区、系数)**,其余 3 页等开入。 + +**`run_par` 模块要点**(与 `prot_set` 并列,不合并——运行参数是"可随时改的运行旋钮",定值有 sum32 封存/闭锁语义,混在一起会搅乱闭锁判断): + +```c +/* App/algo/run_par.h —— 运行参数(无封存语义,可随时改) */ +#define RUN_PAR_YC_N 9u +#define RUN_PAR_YX_N 33u + +int run_par_init(void); /* 出厂默认 */ +float run_par_deadband(uint8_t yc_idx); +int run_par_set_deadband(uint8_t yc_idx, float v); /* 范围校验 */ +/* 落盘走 cfg_file 新段 "runpar";键名 ≤15 字符(CFG_FILE_MAX_NAME_LEN) */ +``` + +- 规模:`run_par` **M**(表驱动 + 单测),2 个页面 **M**。 +- 104 侧改动:`k_yc[].deadband` 改为查 `run_par`(1 行 + 去常量表字段),`yc_read` 乘增益(3 行)。 + +### 3.4 装置维护(4 页) + +| 页 | 形态 | 数据/接口 | 规模 | 阻塞 | +|---|---|---|---|---| +| 时钟设置 | 表单(6 字段:年月日时分秒) | `sys_clock_get_time()` 预填 → `sys_clock_set_unix()` 提交 | **M** | 无 | +| 强制复归 | 操作页(确认框) | `prot_elm_request_reset()` | **S** | 无 | +| 手动录波 | 操作页(确认框) | **无录波模块** | — | ⛔ 等录波 | +| 清除记录 | 操作页(选记录类型 + 确认) | 需新增 `syslog_clear(id)` + `soe_clear()` | **M**(API S + 页 M) | 无(API 自建) | + +- **时钟设置**:字段级编辑(年/月/日/时/分/秒),提交后立即 `sys_clock_set_unix()`;时钟无效时(`clock_ok=0`)允许设置。注意 RTC 可能慢 8 h(台架记录),页面不修正,只如实写。 +- **强制复归**:调 `prot_elm_request_reset()`(任意上下文安全,单字标志),页脚回"已请求复归"。 +- **手动录波**:**本批不建页**。菜单节点保留空叶子(现状行为 = 进空白终点框)。录波落地后再补。 +- **清除记录**:需要 `sys_log` 加删除 API(按 ID 删文件 + 重建 + 计数归零),以及 `soe_store` 加清空 API。页面列出 9 类记录供选择,二次确认。 + +### 3.5 通讯参数(3 页) + +| 页 | 形态 | 现状 | 结论 | +|---|---|---|---| +| 网口设置 | 已有真页 | `net_cfg` 全链闭环 | ✅ 不动 | +| 通讯设置 | 只读信息页 | **无配置模型**(104 端口 2404、CA=1 都是 `#define`) | 做只读页,值经快照下发 | +| SNTP设置 | — | **无 SNTP 客户端** | ⛔ 本批不建 | + +**通讯设置 4 行**(值必须在 App 侧取、经快照下发,页面不得自己 include 协议头): + +| 行 | 名 | 值来源 | +|---|---|---| +| 1 | `104端口` | `IEC104_PORT`(App 侧常量) | +| 2 | `公共地址` | `iec104_serv` 的 CA(当前固定 1) | +| 3 | `显示端口` | RemoDispBus 端口常量 | +| 4 | `FTP用户` | FTP 配置(`admin`,N-19 鉴权后) | + +- 需要在 `ui_data_rt_t` **尾部追加**一个 `comm` 子结构(`port`/`ca`/`disp_port` + 8 B 用户名),按 `docs/UI设计/UI_数据供给设计说明.md` §4.3 的扩展规则升 `UI_DATA_VER`。 +- 规模:**S**(快照 +4 字段,页面 60 行)。 +- **SNTP设置**:等 SNTP 客户端(`App/` 下无任何 SNTP 代码)。现在建页只能存"没人读的参数"。 + +### 3.6 记录查询(9 页) + +9 页 = **2 个组件 + 5 个新日志 ID + 1 个二进制解码器 + 1 个摘要页**。 + +| 页 | 组件 | 数据源 | 页面规模 | 写入方规模 | +|---|---|---|---|---| +| 自检记录 | `ui_w_records` | `SYSLOG_SELFTEST` | ✅ 已有 | — | +| 运行记录 | `ui_w_records` | `SYSLOG_RUN` | ✅ 已有 | — | +| 事故记录 | `ui_w_records` | **新** `SYSLOG_ACCIDENT` | S | **M** | +| 操作记录 | `ui_w_records` | **新** `SYSLOG_OPER` | S | **M** | +| 保护告警 | `ui_w_records` | **新** `SYSLOG_ALARM` | S | **M** | +| 保护启动 | `ui_w_records` | **新** `SYSLOG_START` | S | **M** | +| 遥控记录 | `ui_w_records` | **新** `SYSLOG_YK` | S | **M** | +| SOE记录 | **新** `ui_w_soe` | `/log/soe.log`(二进制 64 B) | **M** | 已有(`soe_store_post`) | +| 运行报告 | 专用摘要页 | 最近一条 SOE 动作事件 | S | 无 | + +**5 个新日志 ID 的写入点**(这是本批真正的成本): + +| 记录 | 写入点 | 判据 | +|---|---|---| +| 事故记录 | `App/task/protect_task.c` 的 `s_log_active_edge()` | `asserted` 上升沿 **且** 该元件 `*_TRIP` 控制字 = 1 | +| 保护告警 | 同上 | `asserted` 上升沿 **且** `*_TRIP` = 0 | +| 保护启动 | 同上(新增 `picked` 边沿检测) | `picked` 上升沿 | +| 操作记录 | 定值保存成功 / 强制复归 / 时钟设置 / 清除记录(4 处) | 动作成功后写一条 | +| 遥控记录 | `iec104_pts_yk_exec()` 返回处 | 每次执行结果(成功/失败/选择态) | + +**`ui_w_soe` 要点**: + +```c +/* Drivers/BSP/UI/ui_w_soe.h —— 与 ui_w_records 同构,但数据是二进制 */ +typedef struct { + uint8_t cache_valid; + uint32_t last_rec; + char ts[20]; + char line[24]; /* "过流Ⅰ段 动作" */ +} ui_w_soe_t; + +uint16_t ui_w_soe_count(ui_w_soe_t *inst); +void ui_w_soe_refresh(ui_w_soe_t *inst, uint16_t row, char *name, char *value); +``` + +- 数据经注入的 reader 取(不 include `soe_store.h`),与 `ui_rec_reader_t` 同一模式但返回 `soe_rec_t`。 +- 需要 `soe_store` 新增只读 API:`soe_count()` / `soe_read(idx, &rec)`(FatFs `f_open` + `f_lseek`,与 `syslog_get_record` 同构)。 +- 每条记录 2 行(时间戳 / "元件+类型"),与文本记录页一致 → 用户学习成本为零。 + +**运行报告(最小可用版)**:3 行——`最近动作`(元件名)、`动作时间`、`当前状态`。数据取最后一条 `SOE_T_ELM_ASSERT`。完整版(故障前后有效值 + 波形摘要)依赖录波,不在本批。 + +**记录容量**:新增 5 类日志需各自定上限(参考现有:运行 1024 条/64 KB、自检 512 条/32 KB)。按"事故/告警/启动各 512 条 + 操作/遥控各 256 条"计 ≈ **128 KB**(64 B/条),NAND 余量需按 `docs/子系统设计/运行与自检记录设计说明.md` 的容量口径核对。 + +### 3.7 装置信息(回炉) + +现在 6 行全是字面量,其中"软件校验 4454"是参考项目的值,与本机无关。 + +| 行 | 名 | 值来源 | +|---|---|---| +| 1 | `终端型号` | 编译期常量 `APP_MODEL`(`F30`) | +| 2 | `软件版本` | 编译期常量 `APP_SW_VER` | +| 3 | `硬件版本` | 编译期常量 `APP_HW_VER` | +| 4 | `程序日期` | `__DATE__`(编译期) | +| 5 | `软件校验` | 编译期常量(构建脚本生成,或先显示 `--`) | +| 6 | `MAC地址` | `net_cfg_get_mac()` | +| 7 | `IP地址` | `net_cfg_get()` + `net_cfg_fmt_ipv4()` | +| 8 | `运行时长` | 由 `ui_data_feed` 算开机 tick 差值(页面只拿秒数,不碰 RTOS) | + +- 新增 `App/app/app_ver.h`(4 个宏 + `__DATE__`),页面经快照取值。 +- 规模:**S**。 +- 说明:第 5 行"软件校验"若没有构建期 CRC 生成步骤,**先显示 `--` 而不是抄参考值**。 + +--- + +## 4. 组件与模块 + +### 4.1 已有 vs 待建 + +| 组件 | 状态 | 用途 | +|---|---|---| +| `ui_content_page` | ✅ 已有 | 页面框架(4 钩子 + 权限) | +| `ui_menu` / `ui_menu_view` / `ui_menu_tab` | ✅ 已有 | 导航 + 显示 + 生成表 | +| `ui_w_records` | ✅ 已有(需改为 reader 注入) | 文本记录页 | +| `ui_data` | ⛔ 待建(步骤1) | 快照存储 | +| `ui_data_feed` | ⛔ 待建(步骤2) | 快照组装 | +| `ui_w_setlist` | ⛔ 待建 | 定值四视图共用 | +| `ui_w_editor` | ⛔ 待建 | 数值编辑器 | +| `ui_w_soe` | ⛔ 待建 | SOE 二进制解码 | +| `ui_page_action` | ⛔ 待建 | 确认操作页(复归/清除/录波) | +| `run_par` | ⛔ 待建 | 运行参数模型 | + +### 4.2 六个待建件的接口要点 + +| 组件 | 关键接口 | 关键约束 | +|---|---|---| +| `ui_data` | `publish_rt/par`、`rt/par`、`rt_seq`、`param_request/state` | 单写者同上下文;不 include 任何 App 头 | +| `ui_data_feed` | `ui_data_feed_tick()` | 唯一填充者;不在绘制路径调用 | +| `ui_w_setlist` | `bind(view_tbl, count, commit_cb)` | 视图表含 `{name, enum_id, dot, unit}`;不直接调 `prot_set` | +| `ui_w_editor` | `edit_begin(row)` / `edit_key(key)` / `edit_commit()` | 只改 RAM 副本;提交由页面统一发起 | +| `ui_w_soe` | 同 `ui_w_records` | reader 注入;不 include `soe_store.h` | +| `ui_page_action` | `bind(title, prompt, ok_cb)` | 二次确认;`ok_cb` 只投递请求,不阻塞 | + +--- + +## 5. 菜单树改动 + +**本批不改菜单树**——27 个空叶子全在现有 42 节点里,`#page=` 注记逐个补上即可(改 `ui_menu_tree.txt` 后重跑 `python tools/menu_gen/gen_menu.py`)。 + +**唯一建议改动**(可选,需产品拍板):补参考项目的"厂家设置"分支。参考侧该分支有 18 个叶子(元件配置、恢复默认 ×5、交流显示、装置调试 ×8、版本信息)。若采纳,精简为 3 个叶子 + 2 个分组: + +| 建议叶子 | 理由 | +|---|---| +| 恢复默认 → 全部恢复 / 仅定值 / 仅软压板 | 现场救砖刚需 | +| 装置调试 → 交流虚遥测 / 虚拟遥信 | 无标准源时的唯一验证手段 | +| 装置调试 → 动作出口 | 出口测试 | + +(版本信息不单列,并入装置信息。) + +→ 42 + 9 = **51 节点**(`厂家设置` + `恢复默认` + `装置调试` 三个分组节点 + 6 个叶子)。未超 255 上限,但 `ui-golden` 的全部菜单屏参考图需重拍。 + +--- + +## 6. 分批与依赖 + +### 6.1 批 0:前置能力(不产出可见页面,但决定后面是否可信) + +| 步 | 内容 | 验证 | +|---|---|---| +| 0-1 | `ui_data` + 主机单测 | `test/host/ui_data` 全绿 | +| 0-2 | `ui_data_feed` + `ui_task` 接线 | 首帧串口锚点;`@build` 0/0 | +| 0-3 | `ui_w_editor`(数值编辑器) | 模拟器可改值 | +| 0-4 | `ui_page_action`(确认操作页) | 模拟器确认/取消 | + +### 6.2 批 1~4 + +| 批 | 页面 | 页数 | 依赖 | +|---|---|---|---| +| **1** | 交流量(回炉)、直流量(回炉)、装置信息(回炉)、参数/定值/控制字/软压板**查看**(4)、遥信量 | **8** | 批 0 | +| **2** | 整定 4 页、时钟设置、强制复归、清除记录、通讯设置 | **8** | 批 1 + 写路径 | +| **3** | 事故/操作/告警/启动/遥控记录(5)、SOE记录、运行报告、遥测死区、遥测系数 | **9** | 批 2 + 5 个写入方 + `run_par` + `soe` 读 API | +| **4**(可选) | 遥信类型/遥信防抖/双点遥信、手动录波、SNTP设置 | **5** | 开入采集 / 录波 / SNTP 客户端 | + +### 6.3 依赖关系 + +``` +批0 ui_data ──┬─→ 批1 实时数据 3 页 + 查看 4 页 + ├─→ 批2 整定 4 页(写路径) + └─→ 批3 记录 5 页(reader 注入)+ run_par 2 页 +开入采集 ─────────→ 批4 遥信类型/防抖/双点 +录波 ─────────────→ 批4 手动录波 + 运行报告完整版 +SNTP 客户端 ──────→ 批4 SNTP设置 +``` + +**关键路径**:批 0 → 批 1 → 批 2。批 3 的 5 个记录页可以并行(写入方在 App 侧,页面在 BSP 侧,互不阻塞)。 + +--- + +## 7. 预算 + +| 项 | 量 | 说明 | +|---|---|---| +| Flash(页面代码) | ≈ 12 KB | 8 个新页面文件(`yx`/`set_ro`/`set_rw`/`runpar`/`clock`/`comm`/`soe`/`report`)× ~1.2 KB + 3 个回炉页 | +| Flash(组件) | ≈ 8 KB | 6 个组件(`ui_data`/`ui_data_feed`/`ui_w_setlist`/`ui_w_editor`/`ui_w_soe`/`ui_page_action`)× ~1.4 KB | +| Flash(字库) | ≈ **+10 KB** | 新增约 25 字(段/投/延/零/频/荷/闸/跳/低/过/Ⅰ/Ⅱ/Ⅲ 等),12 px 1 bpp ≈ 370 B/字 | +| ROM 总量 | 268 KB → ≈ 298 KB | 1 MB Flash,余量充足 | +| RAM(`ui_data`) | ≈ 380 B | 见 `docs/UI设计/UI_数据供给设计说明.md` §10 | +| RAM(页面缓存) | ≈ 130 B/数据页 | 6 个数据页 ≈ 800 B | +| LVGL 池 | 0 增量 | 页面全自绘,无子对象 | +| 记录存储 | +160 KB NAND | 5 类新日志,需核 NAND 余量 | + +**字库是唯一需要提前处理的硬依赖**:新增中文名不在 `ui_cn_chars.txt` 里,必须重跑 `tools/font_gen/make_font.py`,否则屏上显示方块。 + +--- + +## 8. 验收判据 + +| # | 判据 | 方法 | +|---|---|---| +| 1 | 27 个空叶子全部有页(除批 4 延后的 5 个) | 遍历菜单树,进入每个叶子不出现空白框 | +| 2 | 3 个静态页全部接真数据 | 改变输入信号 → 页面值变 | +| 3 | 未标定态可见 | `eng=0` 时交流量第 7 行显示"未标定" | +| 4 | 无源点显示 `--` | 遥信量无效点、直流量直流通道 | +| 5 | 定值整定闭环 | 改 → 保存 → 重启 → 值保持 + 校验和自检通过 + 主站 C_RD 一致 | +| 6 | 分层门禁无新增 | `check_layer_deps.py` 退出码 0,基线 6 → 3 | +| 7 | 像素回归稳定 | `ui-golden` 连续两次零差异 | +| 8 | 每个新页有 golden 参考 | `simulator/tests/ref/` 新增对应 `.bin` | + +--- + +## 9. 待拍板 + +| # | 问题 | 选项 | 建议 | +|---|---|---|---| +| 1 | 交流量 7 行 vs 参考 24 行骨架 | 7 行紧凑 / 24 行留位 | **7 行**(不显示假 `--`) | +| 2 | 直流量页处置 | 温度+未配置 / 改名温度量 / 删节点 | **温度+未配置** | +| 3 | 遥信量 33 行固定 vs 只显有效点 | 固定 33 / 动态 6 | **固定 33**(可对点,零维护) | +| 4 | 三遥设置 5 页是否全做 | 全做 / 只做死区+系数 | **只做 2 页**(另 3 页配置无处生效) | +| 5 | 是否补厂家设置分支 | 补 6 节点 / 不补 | 看送检要求 | +| 6 | 5 类记录是否都在本批 | 全做 / 先事故+操作 | 全做(写入点集中在一处) | + +--- + +## 附:本文与既有文档的分工 + +| 文档 | 回答什么 | +|---|---| +| `docs/UI设计/页面补齐规划.md` | 27 页怎么归并、分几批、内存预算 | +| **本文** | 每一页显示哪些行、数据从哪来、要改哪些代码 | +| `docs/UI设计/UI_数据供给设计说明.md` | 数据怎么从 App 到 UI(通道/契约/并发/写路径) | +| `docs/UI设计/内容页架构.md` | 页面框架本身(描述符/钩子/权限) | +| `docs/问题记录/采集与数据处理短板评测.md` | 为什么有些数据现在没有(标定/遥测链/运行参数/录波) | diff --git a/docs/UI设计/页面补齐规划.md b/docs/UI设计/页面补齐规划.md new file mode 100644 index 00000000..e0e962d2 --- /dev/null +++ b/docs/UI设计/页面补齐规划.md @@ -0,0 +1,271 @@ +# UI 页面补齐规划(2026-10-23) + +> **定位**:盘清 42 节点菜单里**还没有内容页的 27 个叶子**,按"数据是否就绪 + 页面形态"归并成可施工的包,给出分批顺序与每页规格。 +> **前置阅读**:`docs/UI设计/内容页架构.md`(框架机制与新增页面六步)、`docs/UI设计/菜单页面设计.md`(菜单树与几何)、`docs/UI设计/UI_自动化测试流程.md`(像素回归门禁)。 +> **不重复登记**:页面级待办落地时按语义标识进 `docs/任务规划/登记簿.md`;本文只出规划与规格。 +> **逐页细化**:每页显示哪些行、数据从哪来、要改哪些代码,见 `docs/UI设计/页面扩充详细方案.md`(本文的落地细化版)。 + +--- + +## 1. 一页结论 + +**"27 个空页面"不是 27 份工作,是 7 个页面模块 + 1 个共享组件 + 2 项框架能力 + 1 个数据供应层。** + +真正卡住进度的不是"画页面",是两件事: + +| 卡点 | 实质 | 后果 | +|---|---|---| +| **UI 拿不到实时数据** | 分层门禁禁止 `Drivers/BSP/**` include `App/**` 头(`scripts/check_layer_deps.py`),而相量/定值/遥信全在 `App/` | 已建的 3 个数据页(装置信息/交流量/直流量)**整页都是 Flash 字面量**(`"3.01"` 这类),新建页也只能照抄假数据 | +| **没有数值编辑器** | 现有编辑能力只有"逐字节改 IP"(`ui_page_net_cfg.c`),定值要改浮点/整数并校验范围 | 4 个"整定"页无法开工 | + +**建议先做数据供应层与数值编辑这两项前置能力,再按"数据就绪度"分四批铺页**——顺序按数据走,不按菜单走。 + +--- + +## 2. 现状 + +### 2.1 规模 + +| 项 | 数 | +|---|---| +| 菜单节点总数 | **42**(9 菜单 + 33 叶子) | +| 有内容页的叶子 | **6**(装置信息 / 交流量 / 直流量 / 网口设置 / 自检记录 / 运行记录) | +| 无内容页的叶子 | **27** | +| 独立内容页(非节点绑定) | 1(用户密钥/密码页) | +| 已入库像素黄金参考 | 11 份(`simulator/tests/ref/shot_*.bin`) | + +### 2.2 已有 7 页的真实完成度 + +| 页 | 节点 | 数据来源 | 真实度 | +|---|---|---|---| +| 装置信息 | 1 | Flash 静态表 | ⚠ **6 行全是字面量**(含参考项目遗留的校验码 `4454`、日期 `2024.08.27`),不随构建变化 | +| 交流量 | 3 | Flash 静态表 | ❌ **24 行全是 Flash 字面量,不会变**(第 1 页数值取自参考图实测,看着像真值);且 24 行是参考机全套(含 P/Q/S/PF 与两段母线),**本机 V0.1 只有 8 路电气量 + 温度**,行表需按实际通道裁剪 | +| 直流量 | 4 | Flash 静态表 | ❌ **3 行全是占位**(`3.01/3.15/3.288`);**且 V0.1 硬件没有直流采样通道**(`ADC_SEM` 只有 8 路电气量 + 温度)→ 这页当前**无数据源** | +| 网口设置 | 30 | `net_cfg` RAM 副本 | ✅ 真(可编辑 + 保存交接) | +| 自检记录 | 39 | `sys_log`(`/log/selftest.log`) | ✅ 真(经 `ui_w_records`) | +| 运行记录 | 40 | `sys_log`(`/log/run.log`) | ✅ 真(同上) | +| 用户密钥 | — | 编译期密码 | ✅ 真 | + +**结论**:7 页里有 **3 页是静态字面量**(装置信息、交流量、直流量),其余 4 页为真。这 3 页**必须回炉**,它们是"有页但不可信",比空叶子更容易误判为已完成。 + +> 逐页的行表、数据源与代码改动量见 `docs/UI设计/页面扩充详细方案.md`。 + +### 2.3 27 个空叶子逐项清单 + +> 形态:**信息页**(静态行)/ **列表页**(静态或动态行)/ **表单页**(自绘 + 自有键)/ **操作页**(确认 → 执行 → 回报)。 +> 就绪度:✅ 数据现成 / 🔶 需新数据层 / ❌ 数据源不存在。 + +| 节点 | 叶子 | 建议形态 | 数据源 | 就绪度 | +|---|---|---|---|---| +| 5 | 遥信量 | 动态列表(33 行 / 5 页) | `iec104_pts` 遥信位图 + 有效位图 | ✅(需快照层) | +| 8 | 参数(整定) | 可编辑列表(8 项) | `prot_set` 参数类 | ✅(需编辑器) | +| 9 | 定值(整定) | 可编辑列表(18 项 / 3 页) | `prot_set` 数值类 | ✅(需编辑器) | +| 10 | 控制字(整定) | 可编辑列表(4 项) | `prot_set` 跳闸投退类 | ✅(需编辑器) | +| 11 | 软压板(整定) | 可编辑列表(10 项) | `prot_set` 投退类 + 全局 | ✅(需编辑器) | +| 13 | 参数(查看) | 列表(8 项 1 页) | 同 8 | ✅(需快照层) | +| 14 | 定值(查看) | 列表(18 项 / 3 页) | 同 9 | ✅(需快照层) | +| 15 | 控制字(查看) | 列表(4 项) | 同 10 | ✅(需快照层) | +| 16 | 软压板(查看) | 列表(10 项 / 2 页) | 同 11 | ✅(需快照层) | +| 18 | 遥测死区 | 可编辑列表(4 项) | 运行参数段 0x8020~0x8023 | ❌ 运行参数段未实现 | +| 19 | 遥测系数 | 可编辑列表(9 项) | 微调系数(参考 TLV10) | ❌ 随标定 | +| 20 | 遥信类型 | 可编辑列表 | 开入量 | ❌ 开入未实现 | +| 21 | 遥信防抖 | 可编辑列表(1 项) | 运行参数 0x8030 | ❌ 同上 | +| 22 | 双点遥信 | 可编辑列表 | 开入量 | ❌ 同上 | +| 24 | 时钟设置 | 表单(6 字段) | `sys_clock_get_time/set` | ✅ | +| 25 | 强制复归 | 操作页(确认) | `prot_elm_request_reset()` | ✅ | +| 26 | 手动录波 | 操作页(确认) | 录波模块 | ❌ 录波未实现 | +| 27 | 清除记录 | 操作页(选类型 + 确认) | `sys_log`(**缺删除 API**) | 🔶 需补 API | +| 29 | 通讯设置 | 可编辑列表(4~6 项) | 104 公共地址/端口/主站(当前固定) | 🔶 需 `comm_cfg` | +| 31 | SNTP设置 | 可编辑列表(3 项) | SNTP 客户端 | ❌ 未实现(产品口径亦未定) | +| 33 | SOE记录 | 动态列表(每记录 2 行) | `/log/soe.log` 二进制 64B | 🔶 需专用 reader | +| 34 | 事故记录 | 动态列表 | 跳闸记录 | ❌ 记录类型未实现 | +| 35 | 操作记录 | 动态列表 | 操作记录 | ❌ 同上 | +| 36 | 保护告警 | 动态列表 | 告警记录 | ❌ 同上 | +| 37 | 保护启动 | 动态列表 | 启动记录 | ❌ 同上 | +| 38 | 遥控记录 | 动态列表 | 遥控记录 | ❌ 同上 | +| 41 | 运行报告 | 信息页(统计) | 上电次数/运行时长/复位原因/通信统计 | ❌ 计数器未实现 | + +**统计**:数据现成 **11** 项 · 需新数据层 **3** 项 · 数据源不存在 **13** 项。 + +--- + +## 3. 三个前置能力(不先做,页面只能显示假数据) + +### 3.1 UI 数据快照层(最关键) + +**问题**:分层门禁规定 `Drivers/BSP/**` 不得 include `App/**` 头(`scripts/check_layer_deps.py`),现有 6 条基线只减不增。相量、定值、遥信全在 `App/`,所以页面**无法合法地直接取数**——这正是交流量/直流量只能写死占位值的根因。 + +**方案**:沿用门禁注释里已经写明的意图——"记录读取应由上层递进来"。即 **App 推、BSP 存、页面只读**: + +``` +App/app/ui_data_feed.c(App 层,可同时 include 两侧) + 每 N ms 组装一帧 → ui_data_publish_rt(&frame) / ui_data_publish_par(&frame) + │ + ▼ +Drivers/BSP/UI/ui_data.c(BSP 层,持有最新一帧) + │ ui_data_rt(&snap) / ui_data_par(&snap) —— 页面钩子只读 + ▼ +ui_page_ac / ui_page_yx / ui_page_setlist ... +``` + +> 契约、并发模型、写路径、记录 reader 注入与测试策略见 `docs/UI设计/UI_数据供给设计说明.md`,本节只保留规划粒度。 + +- 快照内容:9 路遥测值 + 品质、33 位遥信 + 有效位、40 项定值(Q10)+ 封存态、频率、时钟。 +- 发布节奏:遥测/遥信 **200 ms**(与遥测链同源);定值**仅变化时**发布(`prot_set_checksum()` 变化触发,避免每拍 160 B 拷贝)。 +- 方向正确:App → BSP 是"上层依赖下层",门禁允许;BSP 侧不认识 `App/` 任何符号。 +- **并发:单写者同上下文,不需要锁**——`ui_data_feed_tick()` 与页面钩子都跑在 uiTask;序号只用于页面侧缓存失效判断(`ui_data_rt_seq()`),不是同步原语。 +- 规模:**M**(`ui_data` + `ui_data_feed` 约 150 行 + 单测 + `ui_task.c` 接线)。 + +> ⚠ 没有这一层,后面 11 个"数据现成"的页面全部是假的。**它是本规划的第一优先项**,优先于任何单页。 + +### 3.2 数值编辑能力 + +**问题**:现有编辑只有 `ui_page_net_cfg.c` 的"逐字节改 IP"(↑↓ 调字节、←→ 移光标)。定值要改的是**浮点工程量**(10.0 A、120.0 V、500 ms),需要:位数移动、步进(含 0.01/0.1/1/10 分级)、范围校验、越界提示、保存失败提示。 + +**方案**:把编辑逻辑从 `ui_page_net_cfg.c` 抽成共享组件 `ui_w_editor`(或扩展 `ui_w_*` 家族),提供: + +| 能力 | 说明 | +|---|---| +| 进入/退出编辑 | ENTER 进入(需 `edit_capable` 且 `UI_PERM_MODIFY`)、ESC 取消并回滚 | +| 数值步进 | ↑↓ 按当前光标位加减;光标位决定步长(个位/十分位/百分位) | +| 范围校验 | 取 `prot_set_meta_t` 的 `min/max`,越界拒绝并提示 | +| 提交 | 行内 ENTER 暂存 → 末行"保存"统一提交(一次封存、一次校验和) | + +**关键设计**:定值保存必须**整表提交**而不是逐项写盘——`prot_set_set()` 批量写后必须 `prot_set_seal()`(见 `docs/算法/采集与算法层设计说明.md` §8)。逐项写会把校验和打成中间态,被自检判为损坏。 + +- 规模:**M**(组件 + 与 `prot_set` 的写回交接)。 + +### 3.3 记录类型表(记录页共用) + +已有 `ui_w_records` 组件把"读日志 → 行渲染"收成一份代码,`ui_page_selftest` / `ui_page_runlog` 只是薄跳板。**新增文本类记录页 = 一行实例 + 一个 `syslog_id_t`**。 + +但 `sys_log` 目前只有 2 个 ID(运行、自检),且 **`/log/soe.log` 是二进制 64 B 结构**,不是文本 64 B——不能直接复用 `ui_w_records`。两条路: + +| 记录 | 方案 | 成本 | +|---|---|---| +| 文本类(事故/操作/告警/启动/遥控) | 扩 `syslog_id_t` + 各记录写入方 | 页面 S,**写入方 M~L** | +| SOE(二进制) | 新 `ui_w_soe`:按 `soe_rec_parse()` 解码成"时间 + 类型 + 元件 + 值"两行 | **M** | + +数据怎么进页面已定:**注入 reader**(`ui_rec_reader_t`),页面不再 include `sys_log.h`——顺带退掉 3 条分层基线(6 → 3)。契约见 `docs/UI设计/UI_数据供给设计说明.md` §7。 + +--- + +## 4. 页面归并:27 页 → 7 模块 + +| 包 | 模块 | 覆盖叶子 | 复用 | +|---|---|---|---| +| **A 实时数据** | `ui_page_yx` | 5 | 动态列表 + 快照层 | +| **B 定值查看** | `ui_w_setlist` + `ui_page_set_ro` ×4 描述符 | 13/14/15/16 | 一份组件,4 张视图表 | +| **C 定值整定** | 同 B + `ui_w_editor` | 8/9/10/11 | 同上 + 编辑 | +| **D 三遥设置** | `ui_page_three_remote`(5 描述符) | 18/19/20/21/22 | 同 B 组件 | +| **E 装置维护** | `ui_page_clock`(表单)+ `ui_page_action`(操作页) | 24/25/26/27 | 操作页可复用(复归/录波/清除三处) | +| **F 通讯参数** | `ui_page_comm`(2 描述符) | 29/31 | 同 B 组件 | +| **G 记录查询** | `ui_page_soe` + `ui_w_records` 扩 ID | 33/34/35/36/37/38 | 记录组件 | +| (H 厂家设置) | 见 §7 | 0(菜单树需扩) | — | + +**定值 40 项的视图划分**(`prot_set` 现有 40 项,按语义分四组,不改校验和覆盖序): + +| 视图 | 项数 | 内容 | +|---|---|---| +| 参数 | 8 | PT/CT 一次二次额定、四个返回系数 | +| 定值 | 18 | 三段过流、过负荷、过压/欠压、零序过压、过频/低频的定值与延时 | +| 控制字 | 4 | OC1~OC3、OL 的"跳闸/告警"选择(`*_TRIP`) | +| 软压板 | 10 | 全局软压板 + 9 个元件投退(`*_EN`) | + +> 现状 `prot_set_meta_t` **没有名称字段、没有分类字段**。需要补:① 40 条中文名(受字体子集约束,见 §6);② 一个 `cat` 字段(ROM 表,不参与校验和,加字段安全)。 + +--- + +## 5. 分批实施 + +> 排序原则:**前置能力 > 数据现成 > 需补数据层 > 数据源不存在**。不按菜单顺序铺页。 + +### 批 1:打通数据 + 清掉"假页面"(9 个叶子 + 2 个回炉) + +1. **`ui_data` 快照层**(§3.1)——所有后续页面的地基。 +2. **交流量 / 直流量回炉**:接快照层,删掉占位值;直流量改成真实 3 路(直流通道 2 路 + 温度 1 路,或按 V0.1 实际可用量重定行)。 +3. **遥信量**(node 5):33 行,无效位显示 `--` 并置灰语义(单色屏用前缀 `*` 或后缀 `?` 表示无效)。 +4. **定值查看 4 页**(13/14/15/16):一份组件 + 4 张视图表 + 名称表。 +5. **时钟设置**(24):6 字段表单,写 `sys_clock_set` 后回读校验。 +6. **强制复归**(25):确认 → `prot_elm_request_reset()` → 回报(操作页首个实例)。 +7. **清除记录**(27):需先给 `sys_log` 补删除 API(按 ID 删文件 + 重建 + 计数归零)。 +8. **SOE 记录**(33):`ui_w_soe` 二进制解码列表。 + +**退出判据**:主站改一条定值 → 就地页面 200 ms 内显示新值;SOE 记录页能翻到最近 20 条。 + +### 批 2:整定与通讯(6 个叶子) + +9. **`ui_w_editor`**(§3.2)→ **定值整定 4 页**(8/9/10/11)。 +10. **通讯设置**(29):需先建 `comm_cfg`(`[comm]` 段:104 公共地址/端口/主站地址),照 `net_cfg` 的"RAM 副本 + 保存请求交接"范式。 +11. **SNTP设置**(31):**建议只做页面不接线**,或直接推迟到 SNTP 立项——当前对时走 8888 端口私有协议,SNTP 是另一条链。 + +**退出判据**:就地改一条定值 → 保存 → 掉电重启 → 值保持且校验和自检通过;主站 C_RD 读到同一值。 + +### 批 3:等数据层(12 个叶子) + +12. **三遥设置 5 页**(18~22)——等运行参数段与开入量。 +13. **事故/操作/告警/启动/遥控 5 页**(34~38)——等记录写入方(每类记录 = 一个写入点 + 一个 `syslog_id_t`)。 +14. **手动录波**(26)——等录波模块。 +15. **运行报告**(41)——等计数器(上电次数/运行时长/复位原因/104 统计)。 + +### 批 4(可选):厂家设置 + +见 §7。 + +--- + +## 6. 每页成本与门禁 + +**标准动作**(已由生成器收敛,实测 3 处人工 + 1 条命令): + +| # | 动作 | 落点 | +|---|---|---| +| 1 | 写 `ui_page_xxx.c/.h`(描述符 + 行表/回调) | `Drivers/BSP/UI/` | +| 2 | 菜单树叶子加 ` #page=xxx` | `ui_menu_tree.txt` | +| 3 | 跑 `python tools/menu_gen/gen_menu.py` | 自动改注册表 + 双端工程 | +| 4 | 登记黄金用例(键序 + 文本断言)并生成参考 | `simulator/tests/check_content_pages.py` + `ref/*.bin` | + +**三条硬约束**: + +1. **汉字子集**:任何新文案的汉字必须先过 `tools/font_gen/extract_cn.py` → `make_font.py`,否则显示空白方块(`font-charset` 门禁会拦)。40 条定值名 + 33 条遥信名会显著扩字库——**建议定值名沿用 2~4 字缩写**(如"过流Ⅰ段定值"→"Ⅰ段定值"),控制新增字数。 +2. **像素黄金**:新页入 `ui-golden`,此后任何几何/字体改动都会触发全量比对;参考图一旦入库就只能"改代码对齐",禁止刷参考掩盖。 +3. **RAM**:描述符 + 行表是 Flash const,**不占 LVGL 池**;开页峰值 +776 B(池 16 KB,实测峰值占 40%)→ 页数增加不影响池水位,但 `ui-golden` 的池基线常量要按新页复核。 + +--- + +## 7. 厂家设置:菜单树缺的一整支 + +参考项目的菜单有 **厂家设置**(`CN_COP_PWD` 厂家密码)一支,本项目菜单树**完全没有**: + +| 参考项 | 本项目 | 价值判断 | +|---|---|---| +| 版本信息(板件版本) | 无 | **建议合并进"装置信息"**——单板设备,装置信息已有型号/软硬件版本/校验和/日期,再开一页是重复 | +| 恢复默认(全部/参数/定值/软压板) | 无 | **建议做**:生产返修与现场救急必备;依赖定值/参数模型(批 2 之后) | +| 元件配置 | 无 | 不做:本项目元件表是编译期数据驱动,运行期改元件等于改点表,超出 V0.1 范围 | +| 交流显示(一次/二次值切换) | 无 | 建议做(1 项配置):依赖标定,随批 3 | +| 装置调试:虚拟遥信/交流虚遥测/动作虚事件/动作出口 | 无 | **建议做"虚拟注入"**:无标准源也能验证 UI/104/记录全链,正好补 B10 的前置条件(详见下) | + +> **虚拟注入的价值**:当前送检等标准源(B10),而虚拟注入能让"改一个假值 → 看 UI 是否变 → 看 104 是否上送 → 看 SOE 是否记录"这条链在没有硬件激励的情况下闭环。它是**测试基础设施**,不是产品功能,建议放在厂家菜单并加明显标识(标题后缀"调试")。 +> +> **代价**:菜单树从 42 节点扩到约 48~50 节点,`ui_menu_tree.txt` 改动会触发菜单 8 状态像素回归与 `task-table` 式索引文档同步。**这是需要拍板的项**——见 §8。 + +--- + +## 8. 需要拍板的 5 个问题 + +| # | 问题 | 影响 | +|---|---|---| +| 1 | 菜单是否扩到 48~50 节点(加厂家设置支)? | 42 是产品需求里的硬数字;扩了要改 `产品需求设计文档.md` §8.2 与菜单像素回归 | +| 2 | **直流量页怎么办?**(V0.1 没有直流采样通道)三选一:改成"温度 + 备用"页 / 从菜单删掉 / 保留空页显示 `--` | 决定行表与菜单节点数 | +| 3 | 定值名用全称还是缩写?(字体子集大小 vs 可读性) | 40 条名 × 平均字数,直接影响字库与 `font-charset` | +| 4 | 事故/操作/告警/启动/遥控 5 类记录,是否本轮就要? | 每类 = 一个写入方 + 一个 `syslog_id_t` + 一个页面,是本规划里最大的一块后端工作 | +| 5 | 虚拟注入是否要做?做在哪一层(旁路真实采样 vs 覆盖快照)? | 决定它是否会影响保护判据路径(**必须旁路,绝不能进保护链**) | +| 6 | 交流量行表裁到几行?(24 行参考全套 vs 本机 9 路实有) | 决定页数与黄金参考数量 | + +--- + +## 9. 与既有规划的关系 + +- 本文替代"剩余页面尚未规划"的状态(原 `docs/任务规划/README.md` §2.0 中 W6 的 U5 条目按"暂缓、未规划"记载)。 +- 页面工作**不应独立于数据层推进**:批 1 的第 1 项(快照层)与采集/遥测链共享同一批数据,建议与"测量子系统"同波次排期。 +- 与 `docs/问题记录/采集与数据处理短板评测.md` 的对应:该文的标定(`docs/问题记录/采集与数据处理短板评测.md` §4.1)与遥测链(同文 §4.2)落地后,交流量/直流量/遥测系数三页才有真实数据可显示。