From 48a8ef05649242fe46fcc4417cea7c58d36391c1 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 27 Oct 2026 05:07:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(UI):=20=E9=81=A5=E6=B5=8B=E6=AD=BB?= =?UTF-8?q?=E5=8C=BA=E4=B8=8E=E7=B3=BB=E6=95=B0=202=20=E9=A1=B5=E2=80=94?= =?UTF-8?q?=E2=80=94=E6=B3=A8=E5=85=A5=E8=A1=A8=E7=AC=AC=E5=9B=9B=E5=BC=A0?= =?UTF-8?q?=E3=80=81=E4=B8=89=E6=80=81=E7=94=A8=E8=84=8F=E4=BD=8D=E3=80=81?= =?UTF-8?q?WAIT=20=E4=B8=8E=20OK=20=E5=90=84=E5=87=BA=E4=B8=80=E5=BC=A0?= =?UTF-8?q?=E9=BB=84=E9=87=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这两页是「UI补齐」里唯一带**持久化语义**的一档,所以整笔的设计重心不在画面上,而在**页面如何诚实表达 「已受理」与「已落盘」**——这一点由 setpage-scout 在开工前的只读查实钉下来:cfg_file_save() 全仓唯一调用 者是 run_par_store_save(),而 default_task.c:246 **每轮无条件**调它、函数首行 s_dirty==0 就直接返回 ⇒ 落盘动力本来就在 defaultTask,不在 104 会话、也不在事件位。因此本笔走 A2(UI 不当授权写者),不需要 新事件位、不需要新同步口。 页面三态判据 = run_par_dirty_peek():ENTER / WAIT(脏位仍在 = 已受理未落盘)/ OK(脏位归零 = 整档已外科 保存成功)/ 写被拒 = ERR。两条限制写在页面注释里没藏:脏位是**整档全局**的(一趟落 [run_par] 9 键 + [adc_cal] 两段,归零只证明「这一档落盘了」,不证明「我这一项单独落盘」);他人随后再改会重新置脏 ⇒ 沿用 clear/clock 页的结果锁存,把「我这次动作的结果」与「档位此刻的状态」分开。 **为什么黄金必须出两张**(这条是本轮最有价值的防假证据规则):模拟器假件**保留脏位**、全仓唯一的 s_yt_dirty=0 写点是 sim_yt_pump()(setter 只置不清),于是 WAIT 与 OK 各出一帧——两组同键序、唯一差别是 EXTRA 里的 --yt-nopump。若假件同步清脏位,真机的 WAIT 态永远不会被像素回归覆盖,黄金就成了「只钉住最 好看那一帧」。TC-UIDATA-028o 另钉住 ui_data 层零缓存。 形状:新增 ui_data 第四张注入表(八字段整表校验、拒残表),实现落在装配层 App/app/ui_data_feed.c, 页面零 App include(分层方向 + 模拟器不链 run_par 两条老理由都成立);共用视图模型 ui_w_ytview 承载两页。 合法域一律由执行侧出口给(run_par_db_range / RUN_PAR_K_MAX_UV),页面不抄第二真源——系数页 v1 只用粗守 并在注释写明两层守卫互不掩盖,µV/码 ×[0.5,2.0] 那条真带的出口器归 task-43,**没有**在页面里抄常量。 取舍与遗留(如实):系数行 d=0 整 µV(1e9 装不下两位小数,注释写了「要小数改哪、不收紧假上限」);b 覆盖 无口未做;徽标只画系数页;行名因「率」缺字改为 Hz死区(font-charset 机器先抓到的,不是我目测判的)。 实测:@build 0 错误 0 警告(Code=233,900 / RW-data=1,536);ui_data 单测 277/277、run_par 67/67 零回归; 内容页黄金 47 页全过、**存量 40 张逐字节零漂移**(git 里只有 7 张新增);menu_rows / 池水位 / font-charset / bom / mojibake / nested-comment / cppcheck / coding-standard / layer-deps / bsp-err 全 0 新增;编号五项式 refs 8,778 / dec 7,622 / und **1,156**(上限 1,166)/ 比值 86.83%(基线 86.0% 之上)/ 错误差值 0,注释零裸形 字母+数字。generated-fresh 与 registration-check 本地 PASS;新增 6 只源文件按规矩**双端注册同笔**(叶子 + registry 19→21 + uvprojx + CMakeLists),uvprojx 这次属「真的增删源文件」才入库。 移交三条不混进本笔:① 全量 --selftest 现 exit 1 是 task-41 硬闸在模拟器上也拒 reset/clear(既有红,快检档 不受影响),归台架道定夺;② 内容页 40→47 的全仓文档计数改写由 Lead 派;③ run_par_deadband_set 的限定 条件语措辞(「iec104Task 上下文」已被装配层裁定覆盖)归算法层下一笔。 --- App/app/ui_data_feed.c | 151 ++++++++- Drivers/BSP/UI/ui_data.c | 147 ++++++++ Drivers/BSP/UI/ui_data.h | 155 +++++++++ Drivers/BSP/UI/ui_page_calib.c | 118 +++++++ Drivers/BSP/UI/ui_page_calib.h | 25 ++ Drivers/BSP/UI/ui_page_deadband.c | 117 +++++++ Drivers/BSP/UI/ui_page_deadband.h | 25 ++ Drivers/BSP/UI/ui_page_registry.c | 4 + Drivers/BSP/UI/ui_page_registry.h | 2 +- Drivers/BSP/UI/ui_w_ytview.c | 374 +++++++++++++++++++++ Drivers/BSP/UI/ui_w_ytview.h | 83 +++++ MDK-ARM/STM32F407-Demo.uvprojx | 15 + docs/内存管理/RAM使用分析与优化方案.md | 57 +++- docs/内存管理/RAM预算台账.md | 8 +- scripts/ram_baseline.tsv | 1 + simulator/CMakeLists.txt | 3 + simulator/main.c | 148 ++++++++ simulator/tests/check_content_pages.py | 69 ++++ simulator/tests/ref/shot_calib.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_calib_ok.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_calib_reject.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_calib_wait.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_deadband.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_deadband_ok.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_deadband_wait.bin | Bin 0 -> 3200 bytes test/host/ui_data/ui_data_test.c | 187 +++++++++++ ui_menu_tree.txt | 4 +- 27 files changed, 1682 insertions(+), 11 deletions(-) create mode 100644 Drivers/BSP/UI/ui_page_calib.c create mode 100644 Drivers/BSP/UI/ui_page_calib.h create mode 100644 Drivers/BSP/UI/ui_page_deadband.c create mode 100644 Drivers/BSP/UI/ui_page_deadband.h create mode 100644 Drivers/BSP/UI/ui_w_ytview.c create mode 100644 Drivers/BSP/UI/ui_w_ytview.h create mode 100644 simulator/tests/ref/shot_calib.bin create mode 100644 simulator/tests/ref/shot_calib_ok.bin create mode 100644 simulator/tests/ref/shot_calib_reject.bin create mode 100644 simulator/tests/ref/shot_calib_wait.bin create mode 100644 simulator/tests/ref/shot_deadband.bin create mode 100644 simulator/tests/ref/shot_deadband_ok.bin create mode 100644 simulator/tests/ref/shot_deadband_wait.bin diff --git a/App/app/ui_data_feed.c b/App/app/ui_data_feed.c index 0fbf6063..997795b4 100644 --- a/App/app/ui_data_feed.c +++ b/App/app/ui_data_feed.c @@ -24,6 +24,10 @@ * (定值写路径出队 → 闭锁/越界预检 → 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) + * 装配层实现 + 首轮懒绑(时序与通道四绑定等价, + * 见 feed_yt_bind_once 函数头);行数等值由编译期 + * 守卫钉,量纲分档与合法域分档同源同序 * v1.2 2026-10-26 DTU 项目组 道F task-16:删除本地镜像 feed_cal_block(), * 两个调用点(填帧 / 门控比较)改调 * adc_cal_prot_blocked()——同一份三路判定此前在 @@ -55,7 +59,8 @@ /* 同层(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:未标定闭锁单点 */ +#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" /* 元件动作态(遥信位图来源) */ @@ -476,6 +481,143 @@ static const ui_data_param_ops_t s_param_ops = { 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 +}; + +/* 首轮懒绑一次: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; + /* * 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图 + 未标定闭锁) @@ -558,6 +700,13 @@ void ui_data_feed_tick(void) { 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"); + } + } + now = osKernelGetTickCount(); if (s_map_done == 0u) { diff --git a/Drivers/BSP/UI/ui_data.c b/Drivers/BSP/UI/ui_data.c index 60bd2644..33d2d239 100644 --- a/Drivers/BSP/UI/ui_data.c +++ b/Drivers/BSP/UI/ui_data.c @@ -879,3 +879,150 @@ int ui_data_clock_format(const ui_data_clock_t *p_in, char *a_buf, uint32_t a_le *p = '\0'; return 0; } + +/* ── 通道五:遥测整定注入(死区 / 系数)──────────────────────────────────── */ + +/* + * 注入的遥测整定执行侧函数表(NULL = 未注入)。 + * 与 s_maint 同一条生命周期约定:注入方(App/app/ui_data_feed.c 与 + * simulator/main.c)给的都文件级 static const 表,本层只存指针不拷内容; + * 撤销窗口只在宿主测试里存在。 + * + * ⚠ 本层**不持有任何遥测整定的值**:九条死区与八路系数的真源在 + * App/algo/run_par.c 与 App/algo/adc_cal.c,落盘动力在 defaultTask 的每轮 + * 无条件 run_par_store_save()(首行 if (s_dirty == 0u) return 0;,道F task-7 + * 查实结论:落盘链不靠 104 会话、也不靠事件位)。本层若自己存一份,就正是 + * 卡片判据"不新增第二份真源"要拦的那件事。页面能问的只有两样:现状(*_row) + * 与"我这次动作落没落"(pending)。 + */ +static const ui_data_yt_ops_t *s_yt; + +/* + * 函数功能:注入遥测整定执行侧函数表(启动期一次) + * 入口参数:ops - 函数表;八个成员指针都不得为 NULL;NULL = 撤销注入 + * 返回值:0 - 已注入(或已撤销);-1 - ops 非空但某成员为 NULL(存量不动) int + * 限定条件:uiTask 启动期,先于 ui_menu_init();本层无锁,与快照层同一并发前提 + * 函数说明:整表校验的理由与 ui_data_maint_bind() 逐字相同——半途接错一条会让 + * 页面在"按下才崩"的位置崩,装配期就拒掉残表才是省事。残表被拒后**存量 + * 不动**(TC-UIDATA-027 钉住这条:好表在用时被残表覆盖会静默把已通的口 + * 打断) + */ +int ui_data_yt_bind(const ui_data_yt_ops_t *a_ops) { + if (a_ops == NULL) { + s_yt = NULL; /* 撤销注入:页面随即按"整定能力不可用"渲染 */ + return 0; + } + if ((a_ops->db_n == NULL) || (a_ops->db_row == NULL) || + (a_ops->db_set == NULL) || (a_ops->cal_n == NULL) || + (a_ops->cal_row == NULL) || (a_ops->cal_k_set == NULL) || + (a_ops->cal_blocked == NULL) || (a_ops->pending == NULL)) { + return -1; + } + s_yt = a_ops; + return 0; +} + +/* + * 函数功能:死区点数 + * 返回值:点数;未注入返回 0(页面据此渲染空表,而不是拿编译期常量当"真源说 + * 有几行"——行数归执行侧,见 ui_data.h 通道五表注) uint8_t + * 限定条件:页面绘制路径可直调(一次函数指针转发,零 I/O、零锁) + */ +uint8_t ui_data_yt_db_n(void) { + if (s_yt == NULL) { + return 0u; + } + return s_yt->db_n(); +} + +/* + * 函数功能:读第 idx 行死区现状(工程量 + 粗守域 + 量纲 + 显示小数位) + * 入口参数:idx - 0..8(0..7 = 电气语义通道序,8 = 频率) uint8_t + * a_out - 行现状 ui_data_yt_row_t * + * 返回值:0 - 已写出;-1 - 未注入 / 参数空 / 执行侧无此行(页面这行退化显示, + * 不猜、不拿上一行数值顶替) int + * 限定条件:页面绘制与按键链内(uiTask) + * 函数说明:出参在本层不预清——执行侧契约是"返 0 才整份写",页面返非 0 时 + * 不得读该结构,这条口径由 TC-UIDATA-028 钉 + */ +int ui_data_yt_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out) { + if ((s_yt == NULL) || (a_out == NULL)) { + return -1; + } + return s_yt->db_row(a_idx, a_out); +} + +/* + * 函数功能:写第 idx 行死区(工程量)——两页唯一的死区写出口 + * 返回值:0 - 副本已写且已置脏;-1 - 未注入或执行侧拒(副本未动、不置脏) int + * 限定条件:uiTask 按键链内(编辑提交) + * 函数说明:未注入与执行侧拒绝**同码返回**是刻意的:两条腿屏上都是"值没变 + + * 保存行 ERR",页面不需要、也不该分辨"是没接上还是被拒"——那是装配期 + * 故障与工况故障的区分,归日志不归屏幕 + */ +int ui_data_yt_db_set(uint8_t a_idx, float a_eng) { + if (s_yt == NULL) { + return -1; + } + return s_yt->db_set(a_idx, a_eng); +} + +/* 函数功能:系数覆盖点数;未注入返回 0(理由同 ui_data_yt_db_n) uint8_t */ +uint8_t ui_data_yt_cal_n(void) { + if (s_yt == NULL) { + return 0u; + } + return s_yt->cal_n(); +} + +/* 函数功能:读第 idx 路系数现状;返回值与不猜口径同 ui_data_yt_db_row int */ +int ui_data_yt_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out) { + if ((s_yt == NULL) || (a_out == NULL)) { + return -1; + } + return s_yt->cal_row(a_idx, a_out); +} + +/* + * 函数功能:写第 idx 路系数 k(µV/码)——两页唯一的系数写出口 + * 返回值:0 - 已写并置脏;-1 - 未注入或拒(副本未动、不置脏) int + * 限定条件:uiTask 按键链内 + * 函数说明:粗守(上限 RUN_PAR_K_MAX_UV)与真带(执行段 ×[0.5,2.0])两层守卫 + * **互不掩盖**:屏上输入框只能挡误写,真带裁在 adc_cal;本层不加第三层 + */ +int ui_data_yt_cal_k_set(uint8_t a_idx, float a_uv) { + if (s_yt == NULL) { + return -1; + } + return s_yt->cal_k_set(a_idx, a_uv); +} + +/* + * 函数功能:未标定闭锁徽标源 + * 返回值:1 - 闭锁中;0 - 正常**或未注入** uint8_t + * 限定条件:页面绘制路径可直调 + * 函数说明:未注入按 0 是"宁可不画"而不是"报个徽标保平安"——徽标是断言, + * 没有真源时的断言就是假断言;此时页面上写值本来也全被拒(写口返 -1), + * 信息不缺这条 + */ +uint8_t ui_data_yt_cal_blocked(void) { + if (s_yt == NULL) { + return 0u; + } + return s_yt->cal_blocked(); +} + +/* + * 函数功能:整档落盘脏位探查(保存行三态的唯一判据源) + * 返回值:1 - 有已整定未落盘改动;0 - 干净或未注入 uint8_t + * 限定条件:页面绘制路径可直调(每帧一次转发,零 I/O) + * 函数说明:本层**不加任何边沿记忆**——"我这次动作的结果"要锁存是页面的事 + * (脏位整档全局、他人再改会重新置脏,两条口径见 ui_data.h pending + * 成员注);在这里做锁存会把两页的状态耦合成一份 + */ +uint8_t ui_data_yt_pending(void) { + if (s_yt == NULL) { + return 0u; + } + return s_yt->pending(); +} diff --git a/Drivers/BSP/UI/ui_data.h b/Drivers/BSP/UI/ui_data.h index 45afd248..b5dddc42 100644 --- a/Drivers/BSP/UI/ui_data.h +++ b/Drivers/BSP/UI/ui_data.h @@ -679,6 +679,161 @@ int ui_data_clock_parse(const char *a_ts, ui_data_clock_t *p_out); */ int ui_data_clock_format(const ui_data_clock_t *p_in, char *a_buf, uint32_t a_len); +/* ── 通道五:遥测整定注入(死区 / 系数,道F task-7)──────────────────────── + * + * 为什么又是一张注入表而不是页面直调 run_par / adc_cal:与通道四完全同一组理由 + * ——① 分层方向(Drivers/BSP 不得反向 include App 头,layer-deps 拦提交); + * ② 模拟器既不链 App/algo/run_par.c 也不链 App/algo/adc_cal.c,页面里用一次 + * 真源函数就是"MCU 编得过、模拟器链接期爆"(通道四表注已记过一次的同族坑); + * ③ 合法域与"写值 + 置脏"必须原子发生在执行侧(run_par_cal_k_set 的文件头契约: + * 拆开等于"屏上已改、盘上永不落、且没有任何错误返回"),本表只转达。 + * ⚠ 本表刻意不含任何范围数值的拷贝:lo/hi 由执行侧逐行现给(合法域唯一出口 + * run_par_db_range,系数粗守上限唯一出口 RUN_PAR_K_MAX_UV),页面抄一份数字 + * 就是第二真源——µV 真带(执行段裁)没有公开访问器,也**不许**页面自己补一份。 + */ + +/* 死区点数:下标 0..7 = 电气语义通道序(UA..IN),8 = 频率(与 104 地址映射表 + * 0x8030~0x8038 同序);系数覆盖点数:电气 8 路(语义枚举前缀段)。 + * 与 App/algo/run_par.h 的 RUN_PAR_DB_N / RUN_PAR_CH_N 等值——BSP 看不到 App 头, + * 等值由装配层编译期守卫(App/app/ui_data_feed.c)与 TC-UIDATA-027 同时钉住 */ +#define UI_DATA_YT_DB_N 9u +#define UI_DATA_YT_CH_N 8u + +/* 量纲代码:unit 是"这行数值本身的物理量纲"(诚实性字段,同 rt 帧 eng 的思路), + * 归数据源管;把它显示成什么字符串("V"/"Hz"/"uV")才是表现层的事 */ +#define UI_DATA_YT_UNIT_V 0u /* 电压死区,V */ +#define UI_DATA_YT_UNIT_A 1u /* 电流死区,A */ +#define UI_DATA_YT_UNIT_HZ 2u /* 频率死区,Hz */ +#define UI_DATA_YT_UNIT_UV 3u /* 系数 k,µV/码(屏上串用 ASCII "uV") */ + +/* 一行遥测整定项的完整现状(执行侧现给,页面只读) */ +typedef struct { + float val; /* 当前工程量:死区 V/A/Hz;系数 = adc_cal 当前生效值(µV/码, + 与 104 回显同视图:文件坏值被合法带哨兵回退 ROM 时显 ROM) */ + float lo; /* 粗守下限(含)——合法域唯一出口给,页面不得自造 */ + float hi; /* 粗守上限(含)——同上;真带在执行段另裁,两层互不掩盖 */ + uint8_t unit; /* UI_DATA_YT_UNIT_* */ + uint8_t decimals; /* 显示小数位(**显示域选择**,不是数据源精度,归装配层定) */ +} ui_data_yt_row_t; + +/* + * 执行侧函数表(常驻注入;形状与 ui_data_maint_ops_t 同款:整表校验、拒残表) + * + * 与定值整定(通道 param 的位图批量 + 工作叠层)**故意不同形**:死区/系数写的是 + * 运行参数档([run_par]/[adc_cal]),语义 setter 本身就是"写 RAM 副本 + 置脏" + * 的原子出口、单行即提交;落盘确认是独立一问(pending),所以本表没有 commit + * 一口——保存行按键读的就是 pending()。 + */ +typedef struct { + /* + * 函数功能:死区点数(执行侧真值;未注入时本层返回 0) + * 返回值:点数 uint8_t + */ + uint8_t (*db_n)(void); + /* + * 函数功能:读第 idx 行死区现状(工程量 + 粗守域 + 量纲 + 显示小数位) + * 返回值:0 - 已写出;-1 - idx 越界 / 指针空 / 执行侧无此行 int + */ + int (*db_row)(uint8_t a_idx, ui_data_yt_row_t *a_out); + /* + * 函数功能:写第 idx 行死区(工程量)——真机 = run_par_deadband_set() + * 返回值:0 - 副本已写且已置脏;-1 - 粗守或执行侧拒(副本未动、不置脏) int + */ + int (*db_set)(uint8_t a_idx, float a_eng); + /* 函数功能:系数覆盖点数(执行侧真值) 返回值:点数 uint8_t */ + uint8_t (*cal_n)(void); + /* 函数功能:读第 idx 路系数现状(同 db_row,工程量域为 µV/码) int */ + int (*cal_row)(uint8_t a_idx, ui_data_yt_row_t *a_out); + /* + * 函数功能:写第 idx 路系数 k(µV/码)——真机 = run_par_cal_k_set() + * 返回值:0 - 已写并置脏;-1 - 粗守或执行段真带拒(副本未动、不置脏) int + */ + int (*cal_k_set)(uint8_t a_idx, float a_uv); + /* + * 函数功能:未标定闭锁徽标源——真机 = adc_cal_prot_blocked()(task-16 单点 + * 访问器,与判据停走同一个判定;这里**不**用 rt 帧 eng 反推) + * 返回值:1 - 未标定闭锁中;0 - 正常 uint8_t + */ + uint8_t (*cal_blocked)(void); + /* + * 函数功能:整档落盘脏位探查——真机 = run_par_dirty_peek() + * 返回值:1 - 有已整定未落盘改动;0 - 干净 uint8_t + * ⚠ 两条口径必须与页面注释同步(道F task-7 Lead 裁定): + * ① 脏位是**整档全局**的(一趟落 [run_par] 九键 + [adc_cal] 两段),归零 + * 证明"这一档落盘了",不证明"我这一项单独落盘"; + * ② 他人(104 C_SE)随后再改会重新置脏 ⇒ 页面必须结果锁存,把"我这次动作 + * 的结果"与"档位此刻的状态"分开(clear/clock 页同一条先例)。 + */ + uint8_t (*pending)(void); +} ui_data_yt_ops_t; + +/* + * 函数功能:注入遥测整定执行侧函数表(启动期一次;形状/语义逐字对齐 ui_data_maint_bind) + * 入口参数:ops - 函数表(八个成员指针都不得为 NULL);NULL = 撤销注入(只在宿主测试存在) + * 返回值:0 - 已注入(或已撤销);-1 - ops 非空但成员残缺(存量不动,页面按 + * "整定能力不可用"渲染,不崩不猜) int + * 限定条件:uiTask 启动期、先于 ui_menu_init()(真机在装配层 ui_data_feed_tick() + * 首轮懒绑一次——该调用点在 ui_menu_init 之前,时序与通道四绑定等价) + */ +int ui_data_yt_bind(const ui_data_yt_ops_t *a_ops); + +/* + * 函数功能:死区点数 + * 入口参数:无 + * 返回值:点数;未注入返回 0 uint8_t + */ +uint8_t ui_data_yt_db_n(void); + +/* + * 函数功能:读第 idx 行死区现状 + * 入口参数:idx - 行下标 uint8_t;a_out - 行现状(返 0 才被整份写出) ui_data_yt_row_t * + * 返回值:0 - 已写出;-1 - 未注入 / 参数空 / 执行侧无此行 int + */ +int ui_data_yt_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out); + +/* + * 函数功能:写第 idx 行死区(工程量) + * 入口参数:idx - 行下标 uint8_t;a_eng - 工程量(V/A/Hz) float + * 返回值:0 - 已写并置脏;-1 - 未注入或执行侧拒(未注入与拒绝同码:页面都按 + * ERR 显示,装配故障与工况故障的区分归日志不归屏幕) int + */ +int ui_data_yt_db_set(uint8_t a_idx, float a_eng); + +/* + * 函数功能:系数覆盖点数 + * 入口参数:无 + * 返回值:点数;未注入返回 0 uint8_t + */ +uint8_t ui_data_yt_cal_n(void); + +/* + * 函数功能:读第 idx 路系数现状(µV/码域) + * 入口参数:idx - 电气语义通道序 uint8_t;a_out - 行现状 ui_data_yt_row_t * + * 返回值:0 - 已写出;-1 - 未注入 / 参数空 / 执行侧无此行 int + */ +int ui_data_yt_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out); + +/* + * 函数功能:写第 idx 路系数 k(µV/码) + * 入口参数:idx - 电气语义通道序 uint8_t;a_uv - 新系数(µV/码) float + * 返回值:0 - 已写并置脏;-1 - 未注入或拒(粗守/真带,副本未动) int + */ +int ui_data_yt_cal_k_set(uint8_t a_idx, float a_uv); + +/* + * 函数功能:未标定闭锁徽标源 + * 入口参数:无 + * 返回值:1 - 闭锁中;0 - 正常或未注入(宁可不画,不假报) uint8_t + */ +uint8_t ui_data_yt_cal_blocked(void); + +/* + * 函数功能:整档落盘脏位探查(保存行三态判据) + * 入口参数:无 + * 返回值:1 - 有已整定未落盘改动;0 - 干净或未注入 uint8_t + */ +uint8_t ui_data_yt_pending(void); + #ifdef __cplusplus } #endif diff --git a/Drivers/BSP/UI/ui_page_calib.c b/Drivers/BSP/UI/ui_page_calib.c new file mode 100644 index 00000000..7c4d0056 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_calib.c @@ -0,0 +1,118 @@ +/* + * 模块名称:UI Page calib (遥测系数内容页) + * 模块功能:三遥设置 → 遥测系数 叶子(#P 受密码保护)的内容页——8 行数值表, + * 每页 8 行自动分页;本文件零 App include:行表现状(值/粗守域/量纲/ + * 小数位)逐行来自 ui_data.h 通道五注入表,写与落盘确认全经 + * ui_w_ytview 视图模型(状态机/反显/徽标在那一侧,两页共用一份)—— + * 本文件只是标准骨架几何令牌接进框架的薄跳板,改一次口径两页一致 + * 两态一页:123 修改权限 → 可编辑(编辑器 + 保存行 = 落盘确认);000 查看权限 + * → 只读(attach 内不绑 commit → 无保存行、key_cb 整页交回框架, + * 行为与"没有 key_cb"逐字等价,整定四页同一条分派纪律) + * 徽标说明:只读/可编辑两态都画"未标定"(cal_blocked 经注入表现读)——徽标说的 + * 是数据可信度,与本次权限无关;它**不**禁任何编辑(与定值 LOCK 分工: + * LOCK 才写不进去,未标定只是"改了也不会动作",ui_w_setlist.h 三词三源) + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:DTU 项目组 + * 创建日期:2026-10-27 + * 修改记录: + * v1.0 2026-10-27 DTU 项目组 创建(道F task-7;判据 docs/任务规划/UI补齐.md + * §2 第 4 行「改死区能改变 104 上送门限 / 改 k 后 + * 页面值按比例变化 / 只走既有落盘链不加第二真源」) + */ +/* 头文件包含区 */ +#include "ui_page_calib.h" + +#include "ui_tokens.h" +#include "ui_w_setlist.h" +#include "ui_w_ytview.h" + +/* 静态变量定义区 */ + +/* 本页定值表实例(组件状态独占一份,勿跨页共用——ui_w_setlist 实例化约定) */ +static ui_w_setlist_t s_sl; + +/* 函数定义区 */ + +/* + * 函数功能:开页绑视图(行表现状逐行现取 + 两态分派都在视图模型内;接 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内(本次进入权限已装载,时序依据见 ui_page_view_param.c + * 同位置注释:框架先装权限再调 init_cb) + */ +static void pg_init(void) { + ui_w_ytview_attach_cal(&s_sl); +} + +/* + * 函数功能:总行数(数据行现给 + 可编辑态才有保存行;接 count_cb) + * 入口参数:无 + * 返回值:总行数 uint16_t + * 限定条件:框架绘制/翻页链内 + */ +static uint16_t pg_count(void) { + return ui_w_setlist_count(&s_sl); +} + +/* + * 函数功能:逐行取数(接 refresh;行现状在视图模型里现取,本层无缓存) + * 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + ui_w_setlist_refresh(&s_sl, a_row, a_name, a_value); +} + +/* + * 函数功能:自绘层(编辑光标反显 + 写被拒行反显 + 未标定徽标) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内,内置区块画完之后 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_w_setlist_draw(&s_sl, a_layer, a_clip); + ui_w_ytview_draw_cal_badge(a_layer, a_clip); +} + +/* + * 函数功能:按键转发(接 key_cb) + * 入口参数:a_key - LVGL 键值 lv_key_t + * 返回值:true - 本页已消费;false - 交回框架(↑↓ 移选、← 关页) + * 限定条件:描述符 edit_capable=1 + key_intercept=1(编辑中 ←→ 是移数字光标, + * 必须先给页面,否则框架把 ← 当关页——整定四页同款实证) + * 函数说明:第一条门 = 只读进入(未绑 commit)整页交回框架,与"本页没有 + * key_cb"逐字等价(黄金参考只读态为证);其余分发规则全在组件 + */ +static bool pg_key(lv_key_t a_key) { + if (s_sl.commit == NULL) { + return false; + } + return (ui_w_setlist_key(&s_sl, a_key) != 0u); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=calib 绑定引用) + * 列表页骨架 + 可编辑 + ← 拦截;band_h 必须 >0(选中行黑字白底反显,同四整定页); + * row_count 给静态上限(8 数据行 + 保存行),行数运行时以注入表现报为准 + */ +const content_page_def_t ui_page_calib_desc = { + .id = "calib", + .title = "遥测系数", + .selectable = 1u, + .edit_capable = 1u, + .key_intercept = 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, + .row_count = 9u, + .init_cb = pg_init, + .count_cb = pg_count, + .refresh = pg_refresh, + .draw_cb = pg_draw, + .key_cb = pg_key, +}; diff --git a/Drivers/BSP/UI/ui_page_calib.h b/Drivers/BSP/UI/ui_page_calib.h new file mode 100644 index 00000000..b843920e --- /dev/null +++ b/Drivers/BSP/UI/ui_page_calib.h @@ -0,0 +1,25 @@ +/* + * 模块名称:UI Page calib (遥测系数内容页对外声明) + * 模块功能:供生成物 ui_page_registry.c 引用的页面描述符声明 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:DTU 项目组 + * 创建日期:2026-10-27 + * 修改记录: + * v1.0 2026-10-27 DTU 项目组 创建(道F task-7) + */ +#ifndef UI_PAGE_CALIB_H +#define UI_PAGE_CALIB_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +extern const content_page_def_t ui_page_calib_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_CALIB_H */ diff --git a/Drivers/BSP/UI/ui_page_deadband.c b/Drivers/BSP/UI/ui_page_deadband.c new file mode 100644 index 00000000..e49bc5ac --- /dev/null +++ b/Drivers/BSP/UI/ui_page_deadband.c @@ -0,0 +1,117 @@ +/* + * 模块名称:UI Page deadband (遥测死区内容页) + * 模块功能:三遥设置 → 遥测死区 叶子(#P 受密码保护)的内容页——9 行数值表, + * 每页 8 行自动分页;本文件零 App include:行表现状(值/粗守域/量纲/ + * 小数位)逐行来自 ui_data.h 通道五注入表,写与落盘确认全经 + * ui_w_ytview 视图模型(状态机/反显/徽标在那一侧,两页共用一份)—— + * 本文件只是标准骨架几何令牌接进框架的薄跳板,改一次口径两页一致 + * 两态一页:123 修改权限 → 可编辑(编辑器 + 保存行 = 落盘确认);000 查看权限 + * → 只读(attach 内不绑 commit → 无保存行、key_cb 整页交回框架, + * 行为与"没有 key_cb"逐字等价,整定四页同一条分派纪律) + * 徽标说明:本页**不画**"未标定"——死区数值来自 [run_par] 档、与两点标定无关, + * 挂不相干的告警等于稀释真告警(ui_w_ytview.h 同注) + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:DTU 项目组 + * 创建日期:2026-10-27 + * 修改记录: + * v1.0 2026-10-27 DTU 项目组 创建(道F task-7;判据 docs/任务规划/UI补齐.md + * §2 第 4 行「改死区能改变 104 上送门限 / 改 k 后 + * 页面值按比例变化 / 只走既有落盘链不加第二真源」) + */ +/* 头文件包含区 */ +#include "ui_page_deadband.h" + +#include "ui_tokens.h" +#include "ui_w_setlist.h" +#include "ui_w_ytview.h" + +/* 静态变量定义区 */ + +/* 本页定值表实例(组件状态独占一份,勿跨页共用——ui_w_setlist 实例化约定) */ +static ui_w_setlist_t s_sl; + +/* 函数定义区 */ + +/* + * 函数功能:开页绑视图(行表现状逐行现取 + 两态分派都在视图模型内;接 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内(本次进入权限已装载,时序依据见 ui_page_view_param.c + * 同位置注释:框架先装权限再调 init_cb) + */ +static void pg_init(void) { + ui_w_ytview_attach_db(&s_sl); +} + +/* + * 函数功能:总行数(数据行现给 + 可编辑态才有保存行;接 count_cb) + * 入口参数:无 + * 返回值:总行数 uint16_t + * 限定条件:框架绘制/翻页链内 + */ +static uint16_t pg_count(void) { + return ui_w_setlist_count(&s_sl); +} + +/* + * 函数功能:逐行取数(接 refresh;行现状在视图模型里现取,本层无缓存) + * 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + ui_w_setlist_refresh(&s_sl, a_row, a_name, a_value); +} + +/* + * 函数功能:自绘层(编辑光标反显 + 写被拒行反显) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内,内置区块画完之后 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_w_setlist_draw(&s_sl, a_layer, a_clip); + +} + +/* + * 函数功能:按键转发(接 key_cb) + * 入口参数:a_key - LVGL 键值 lv_key_t + * 返回值:true - 本页已消费;false - 交回框架(↑↓ 移选、← 关页) + * 限定条件:描述符 edit_capable=1 + key_intercept=1(编辑中 ←→ 是移数字光标, + * 必须先给页面,否则框架把 ← 当关页——整定四页同款实证) + * 函数说明:第一条门 = 只读进入(未绑 commit)整页交回框架,与"本页没有 + * key_cb"逐字等价(黄金参考只读态为证);其余分发规则全在组件 + */ +static bool pg_key(lv_key_t a_key) { + if (s_sl.commit == NULL) { + return false; + } + return (ui_w_setlist_key(&s_sl, a_key) != 0u); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=deadband 绑定引用) + * 列表页骨架 + 可编辑 + ← 拦截;band_h 必须 >0(选中行黑字白底反显,同四整定页); + * row_count 给静态上限(9 数据行 + 保存行),行数运行时以注入表现报为准 + */ +const content_page_def_t ui_page_deadband_desc = { + .id = "deadband", + .title = "遥测死区", + .selectable = 1u, + .edit_capable = 1u, + .key_intercept = 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, + .row_count = 10u, + .init_cb = pg_init, + .count_cb = pg_count, + .refresh = pg_refresh, + .draw_cb = pg_draw, + .key_cb = pg_key, +}; diff --git a/Drivers/BSP/UI/ui_page_deadband.h b/Drivers/BSP/UI/ui_page_deadband.h new file mode 100644 index 00000000..ea91efcb --- /dev/null +++ b/Drivers/BSP/UI/ui_page_deadband.h @@ -0,0 +1,25 @@ +/* + * 模块名称:UI Page deadband (遥测死区内容页对外声明) + * 模块功能:供生成物 ui_page_registry.c 引用的页面描述符声明 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:DTU 项目组 + * 创建日期:2026-10-27 + * 修改记录: + * v1.0 2026-10-27 DTU 项目组 创建(道F task-7) + */ +#ifndef UI_PAGE_DEADBAND_H +#define UI_PAGE_DEADBAND_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +extern const content_page_def_t ui_page_deadband_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_DEADBAND_H */ diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c index eb7861a5..7de2fe40 100644 --- a/Drivers/BSP/UI/ui_page_registry.c +++ b/Drivers/BSP/UI/ui_page_registry.c @@ -10,9 +10,11 @@ #include "ui_page_registry.h" #include "ui_page_ac.h" +#include "ui_page_calib.h" #include "ui_page_clear.h" #include "ui_page_clock.h" #include "ui_page_dc.h" +#include "ui_page_deadband.h" #include "ui_page_device_info.h" #include "ui_page_net_cfg.h" #include "ui_page_password.h" @@ -39,6 +41,8 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 14u, &ui_page_view_set_desc }, /* node 14 定值 */ { 15u, &ui_page_view_word_desc }, /* node 15 控制字 */ { 16u, &ui_page_view_plate_desc }, /* node 16 软压板 */ + { 18u, &ui_page_deadband_desc }, /* node 18 遥测死区 */ + { 19u, &ui_page_calib_desc }, /* node 19 遥测系数 */ { 24u, &ui_page_clock_desc }, /* node 24 时钟设置 */ { 25u, &ui_page_reset_desc }, /* node 25 强制复归 */ { 27u, &ui_page_clear_desc }, /* node 27 清除记录 */ diff --git a/Drivers/BSP/UI/ui_page_registry.h b/Drivers/BSP/UI/ui_page_registry.h index 3cab046e..1d2addfd 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 19u /* 绑定了内容页的叶子数 */ +#define UI_PAGE_MAP_COUNT 21u /* 绑定了内容页的叶子数 */ extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT]; diff --git a/Drivers/BSP/UI/ui_w_ytview.c b/Drivers/BSP/UI/ui_w_ytview.c new file mode 100644 index 00000000..1339faae --- /dev/null +++ b/Drivers/BSP/UI/ui_w_ytview.c @@ -0,0 +1,374 @@ +/* + * 模块名称:UI 遥测整定视图模型实现(ui_w_ytview) + * 模块功能:死区/系数两张页的行表填充(注入表现状 → 显示域)、逐行写转发、 + * 落盘确认状态机、越界行反显与未标定徽标——对外形状与纪律见配对头文件 + * (两态分派 / 无叠层 / 保存行=确认 / 两层守卫互不掩盖 四条都在头注) + * 编辑即生效的取舍(如实记在这里,评审时不靠口头解释):定值四页有"叠层 + 整表 + * 封存 + ESC 弃稿",本两页没有——[run_par]/[adc_cal] 的语义 setter 就是 + * 执行侧写入口(写值与置脏不可拆),页面再造一份叠层只会造出"屏显值与 + * RAM 副本不一致"的第二个视图。后果是**按下 ENTER 提交那一行即刻生效**、 + * 退出页面不回滚;"哪些已生效"由每行的显示值本身说明(读的就是真源), + * "落没落盘"由保存行说明。v1 认可这条语义(Lead 裁定 task-7 派发单), + * 要"草稿/弃稿"得改注入表形状(叠层 + 批量提交),不在本批 + * 状态机(每视图独立一份,两页互不干扰): + * ENTER --保存行按键--> 脏位在 ? WAIT : OK(无改动/已落 = OK,与定值写 + * 路径"空请求不消费队列位、直接 OK"同口径);WAIT 期间脏位归零 → 锁存 + * OK(此后他人再改重新置脏**不回退**显示——"我这次动作的结果"与"档位 + * 此刻的状态"分开,clear/clock 页先例);任一写被拒(执行侧返 -1)→ + * 粘滞 ERR + 该行反显,直到下次写成功或再按保存行重新评估 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:DTU 项目组 + * 创建日期:2026-10-27 + * 修改记录: + * v1.0 2026-10-27 DTU 项目组 创建(道F task-7) + */ + +/* 头文件包含区 */ +#include + +#include "ui_w_ytview.h" + +#include "ui_data.h" +#include "ui_tokens.h" + +/* 私有宏定义区 */ + +/* 保存行四态的内部编码(对外映射成 ui_w_setlist_st_t,不直接外露——枚举值序 + * 被各页钉着,这里换底层编码不影响组件) */ +#define YT_ST_ENTER 0u /* 初始 / 已作废:值列 ENTER */ +#define YT_ST_WAIT 1u /* 按过保存、脏位仍在:WAIT */ +#define YT_ST_OK 2u /* 脏位归零(或按时无脏):OK,锁存 */ +#define YT_ST_ERR 3u /* 最近一次写被执行侧拒:粘滞 ERR */ + +/* 徽标右界与文案:右界 141 的理由、测宽传 strlen 的版本坑,均沿用整定四页 + * 徽标的既有实测结论(ui_w_setview.c 的 draw_cal_badge 头注),不重新发明 */ +#define YT_BADGE_RIGHT_X 141 +#define YT_CAL_BADGE "未标定" + +/* 私有变量定义区 */ + +/* 行名表(Flash):下标即执行侧 idx,注释是唯一的对表处—— + * 死区 0..7 = 电气语义通道 UA..IN、8 = 频率(104 地址映射 0x8030~ 同序); + * 系数 0..7 = 电气语义通道 UA..IN(adc_semantic_t 前缀段) */ +static const char *const k_db_name[UI_DATA_YT_DB_N] = { + "UA死区", "UB死区", "UC死区", "UN死区", + "IA死区", "IB死区", "IC死区", "IN死区", + "Hz死区" /* 频率死区行的上屏名用 Hz(本仓既有写法,交流量页 Hz 行同词): + * "率"不在 183 字子集里,font-charset 机器先抓出来——改文案不改字库; + * 这一行的小数位数与量纲由执行侧现给(0.5 Hz / decimals 3 / UNIT_HZ), + * 名字里的 Hz 只是让现场知道该行量纲,不参与取数 */ +}; +static const char *const k_cal_name[UI_DATA_YT_CH_N] = { + "UA系数", "UB系数", "UC系数", "UN系数", + "IA系数", "IB系数", "IC系数", "IN系数" +}; +/* 量纲代码 → 屏上串:µV 用 ASCII "uV"(字库无希腊小写 mu,缺字即白框—— + * font-charset 只保汉字子集,ASCII 字形里本来也没有 µ) */ +static const char *const k_unit_str[UI_DATA_YT_UNIT_UV + 1u] = { + "V", "A", "Hz", "uV" +}; +/* 显示域倍率(decimals 契约 0..3,与 ui_w_setlist 行表同口径) */ +static const int32_t k_p10[4] = { 1, 10, 100, 1000 }; + +typedef struct { + ui_w_setlist_row_t rows[UI_DATA_YT_DB_N]; /* 按较大的死区表定容;系数表只占前 8 槽 */ + uint16_t badmap; /* 写被拒行反显位图(≤16 行) */ + uint8_t st; /* YT_ST_*,锁存的保存结果 */ + uint8_t n; /* 本次开页实际建成的行数 */ +} yt_view_t; + +static yt_view_t s_db; +static yt_view_t s_cal; + +/* 函数定义区 */ + +/* + * 函数功能:工程量 → 显示域整数(四舍五入,半程进位朝远离 0 侧) + * 入口参数:v - 工程量 float;d - 小数位 0..3 uint8_t + * 返回值:显示域整数 int32_t + * 限定条件:v 与行 min/max 同来自执行侧,粗守上限量级(系数 1e9 µV)在 d=0 时 + * 仍落在 int32 内——这是系数行显示小数位取 0 的硬理由(见装配层建表处), + * 不是排版偏好 + */ +static int32_t yt_scale(float a_v, uint8_t a_d) { + float p = (float)k_p10[a_d]; + float s = a_v * p; + + return (int32_t)((s >= 0.0f) ? (s + 0.5f) : (s - 0.5f)); +} + +/* + * 函数功能:逐行从注入表建行表(名/量纲/小数位/min/max 全部现给,本模块零范围字面量) + * 入口参数:v - 目标视图 yt_view_t *;want - 执行侧报出的行数 uint8_t; + * names - 行名表 const char *const *;rowfn - 对应注入口的读函数 + * 返回值:无 + * 限定条件:attach 链内(开页) + * 函数说明:读到第 i 行失败就停在 i 行(**不补空行、不跳过**):半张表比空表更 + * 危险——用户会以为下面的行"没有这回事"。行数超过行槽数组上限的部分 + * 同样不显示(正常不可能发生:want 已由调用方钳到契约值,这里双保险) + */ +static void yt_fill(yt_view_t *a_v, uint8_t a_want, const char *const *a_names, + int (*a_rowfn)(uint8_t, ui_data_yt_row_t *)) { + uint8_t i; + + a_v->n = 0u; + for (i = 0u; (i < a_want) && (i < (uint8_t)UI_DATA_YT_DB_N); i++) { + ui_data_yt_row_t r; + uint8_t d; + + if (a_rowfn(i, &r) != 0) { + break; + } + d = (r.decimals <= 3u) ? r.decimals : 3u; /* 组件契约 0..3,越界钳住而不是读脏倍率 */ + a_v->rows[a_v->n].name = a_names[i]; + a_v->rows[a_v->n].id = i; + a_v->rows[a_v->n].kind = UI_W_SETLIST_KIND_NUM; + a_v->rows[a_v->n].decimals = d; + a_v->rows[a_v->n].min = yt_scale(r.lo, d); + a_v->rows[a_v->n].max = yt_scale(r.hi, d); + a_v->rows[a_v->n].opts = NULL; + a_v->rows[a_v->n].unit = (r.unit <= UI_DATA_YT_UNIT_UV) + ? k_unit_str[r.unit] : NULL; + a_v->n++; + } +} + +/* + * 函数功能:写结果记账(成功/被拒两条腿共一个入口,防两页写两遍规则写漏) + * 入口参数:v - 视图 yt_view_t *;id - 行下标 uint8_t;rc - 注入口返回值 int + * 返回值:无 + * 限定条件:set 回调链内(uiTask 按键) + * 函数说明:拒绝 → 该行反显置位 + 粘滞 ERR;成功 → 只清**这一行**的反显(别的 + * 行还欠着被执行侧拒的账,不陪它一起销)+ 作废上次保存结果(改了任何 + * 一位,上一次的 OK/ERR 都不该再挂屏,clock 页同一条纪律) + */ +static void yt_note_write(yt_view_t *a_v, uint8_t a_id, int a_rc) { + if (a_rc != 0) { + a_v->badmap = (uint16_t)(a_v->badmap | (uint16_t)(1u << a_id)); + a_v->st = YT_ST_ERR; + } else { + a_v->badmap = (uint16_t)(a_v->badmap & (uint16_t)~(1u << a_id)); + a_v->st = YT_ST_ENTER; + } +} + +/* + * 函数功能:保存行按键(不是"投递整表"——值早在逐行 ENTER 时写进副本并置脏, + * 这里只做一次落盘确认的起点) + * 入口参数:v - 视图 yt_view_t * + * 返回值:无 + * 限定条件:可编辑态、保存行 ENTER;uiTask 按键链 + * 函数说明:先清越界反显再评估(按保存 = "我知道刚才被拒了,看新一轮结果"); + * 脏位仍在 → WAIT 开始锁存观察;无脏 → 直接 OK——两种都如实:真机 + * defaultTask 每 50 ms 无条件落一遍,WAIT 窗口毫秒级,**模拟器假件刻意 + * 不自动清脏**(由无头 pump 清),否则 WAIT 态永远不会被像素回归覆盖, + * 黄金就成了只钉住最好看那一帧(Lead 裁定④的原文理由) + */ +static void yt_commit(yt_view_t *a_v) { + a_v->badmap = 0u; + a_v->st = (ui_data_yt_pending() != 0u) ? YT_ST_WAIT : YT_ST_OK; +} + +/* + * 函数功能:保存行值列回读(组件每次绘制调一次;WAIT → OK 的边沿在这里收尾) + * 入口参数:v - 视图 yt_view_t * + * 返回值:组件态枚举 ui_w_setlist_st_t + * 限定条件:绘制链内;只读视图组件不会调到这里(未绑 state) + * 函数说明:锁存语义 = 只前进不回退:WAIT 期间看到脏位归零就落 OK;此后他人 + * (104 选择段)再改重新置脏也**不**把 OK 拽回 WAIT——用户按保存那次 + * 动作确实落盘了,档位的新脏是别人的动作,不归本次显示(ui_data.h + * pending 成员注的两条口径,第②条落在这三行代码上) + */ +static ui_w_setlist_st_t yt_state(yt_view_t *a_v) { + if ((a_v->st == YT_ST_WAIT) && (ui_data_yt_pending() == 0u)) { + a_v->st = YT_ST_OK; + } + switch (a_v->st) { + case YT_ST_WAIT: + return UI_W_SETLIST_ST_WAIT; + case YT_ST_OK: + return UI_W_SETLIST_ST_OK; + case YT_ST_ERR: + return UI_W_SETLIST_ST_FAIL; + default: + return UI_W_SETLIST_ST_IDLE; + } +} + +/* 函数功能:某行是否需要越界反显(写被拒的账挂在这行值上,同整定页越界反显 + * 的视觉语言) 返回值:true = 反显 bool */ +static bool yt_bad(const yt_view_t *a_v, uint16_t a_row) { + if (a_row >= 16u) { + return false; + } + return ((a_v->badmap >> a_row) & 0x1u) != 0u; +} + +/* ── 死区视图的回调组(id = 执行侧 idx 直通) ────────────────────────────── */ + +static int32_t yt_db_get(uint8_t a_id) { + ui_data_yt_row_t r; + + if ((a_id >= s_db.n) || (ui_data_yt_db_row(a_id, &r) != 0)) { + return 0; /* 取不到现状的行显示 0 而不是旧缓存:本组件没有缓存可回退, + 0 配 "--" 之外的解释都不诚实;attach 时读失败该行根本不存在 */ + } + return yt_scale(r.val, r.decimals); +} + +static void yt_db_write(uint8_t a_id, int32_t a_val) { + float eng; + if (a_id >= s_db.n) { + return; + } + eng = (float)a_val / (float)k_p10[s_db.rows[a_id].decimals]; + yt_note_write(&s_db, a_id, ui_data_yt_db_set(a_id, eng)); +} + +static void yt_db_commit(void) { yt_commit(&s_db); } +static ui_w_setlist_st_t yt_db_state(void) { return yt_state(&s_db); } +static bool yt_db_bad(uint16_t a_row) { return yt_bad(&s_db, a_row); } + +/* ── 系数视图的回调组 ────────────────────────────────────────────────────── */ + +/* + * 函数功能:读系数行当前值(显示域;口径同 yt_db_get,现状现取零缓存) + * 入口参数:a_id - 行下标(= 执行侧语义通道序) uint8_t + * 返回值:工程量 × 10^decimals int32_t + * 限定条件:组件取数回调链内(绘制) + */ +static int32_t yt_cal_get(uint8_t a_id) { + ui_data_yt_row_t r; + + if ((a_id >= s_cal.n) || (ui_data_yt_cal_row(a_id, &r) != 0)) { + return 0; + } + return yt_scale(r.val, r.decimals); +} + +/* + * 函数功能:写一行系数(显示域 → µV/码工程量后转注入表;结果记账同 yt_db_write) + * 入口参数:a_id - 行下标 uint8_t;a_val - 显示域新值 int32_t + * 返回值:无(拒绝经 yt_note_write 上屏:行反显 + 保存行 ERR) + * 限定条件:组件编辑提交回调链内(可编辑态) + */ +static void yt_cal_write(uint8_t a_id, int32_t a_val) { + float uv; + if (a_id >= s_cal.n) { + return; + } + uv = (float)a_val / (float)k_p10[s_cal.rows[a_id].decimals]; + yt_note_write(&s_cal, a_id, ui_data_yt_cal_k_set(a_id, uv)); +} + +static void yt_cal_commit(void) { yt_commit(&s_cal); } +static ui_w_setlist_st_t yt_cal_state(void) { return yt_state(&s_cal); } +static bool yt_cal_bad(uint16_t a_row) { return yt_bad(&s_cal, a_row); } + +/* ── 对外 attach 与徽标 ──────────────────────────────────────────────────── */ + +/* + * 函数功能:死区视图绑表(两态在同一函数里分派一次,页面只调这一个口) + * 入口参数:a_inst - 页面持有的定值表实例 ui_w_setlist_t * + * 返回值:无 + * 限定条件:页面 init_cb 内(本次进入权限已装载) + * 函数说明:行数**执行侧现给**(ui_data_yt_db_n):未注入 = 0 行 = 空表页, + * 一个像素都不画假数据;只读进入不绑 commit/state——组件因此既不画 + * 保存行也放开不了编辑(set 契约"必填",绑的就是真写口,只读态组件 + * 保证不会调它,与整定四页同一绑法同一理由) + */ +void ui_w_ytview_attach_db(ui_w_setlist_t *a_inst) { + uint8_t want; + + if (a_inst == NULL) { + return; + } + want = ui_data_yt_db_n(); + if (want > (uint8_t)UI_DATA_YT_DB_N) { + want = (uint8_t)UI_DATA_YT_DB_N; + } + s_db.badmap = 0u; + s_db.st = YT_ST_ENTER; + yt_fill(&s_db, want, k_db_name, ui_data_yt_db_row); + + (void)memset(a_inst, 0, sizeof(*a_inst)); + a_inst->rows = s_db.rows; + a_inst->count = s_db.n; + a_inst->get = yt_db_get; + a_inst->set = yt_db_write; + a_inst->bad = yt_db_bad; + if (ui_content_page_is_editable() != false) { + a_inst->commit = yt_db_commit; + a_inst->state = yt_db_state; + } + ui_w_setlist_reset(a_inst); +} + +/* + * 函数功能:系数视图绑表(形状与 attach_db 逐字同构;两视图各持一份状态机, + * 死区页的 WAIT 不会串门到系数页) + * 入口参数:a_inst - 页面持有的定值表实例 ui_w_setlist_t * + * 返回值:无 + * 限定条件:页面 init_cb 内 + */ +void ui_w_ytview_attach_cal(ui_w_setlist_t *a_inst) { + uint8_t want; + + if (a_inst == NULL) { + return; + } + want = ui_data_yt_cal_n(); + if (want > (uint8_t)UI_DATA_YT_CH_N) { + want = (uint8_t)UI_DATA_YT_CH_N; + } + s_cal.badmap = 0u; + s_cal.st = YT_ST_ENTER; + yt_fill(&s_cal, want, k_cal_name, ui_data_yt_cal_row); + + (void)memset(a_inst, 0, sizeof(*a_inst)); + a_inst->rows = s_cal.rows; + a_inst->count = s_cal.n; + a_inst->get = yt_cal_get; + a_inst->set = yt_cal_write; + a_inst->bad = yt_cal_bad; + if (ui_content_page_is_editable() != false) { + a_inst->commit = yt_cal_commit; + a_inst->state = yt_cal_state; + } + ui_w_setlist_reset(a_inst); +} + +/* + * 函数功能:系数页"未标定"徽标(头文件里写清了死区页为何不调、右界为何取 141) + * 入口参数:a_layer - 绘制层 lv_layer_t * + * a_clip - 重绘区 const lv_area_t * + * 返回值:无 + * 限定条件:系数页 draw_cb 内;cal_blocked() 为假时一个像素都不落 + */ +void ui_w_ytview_draw_cal_badge(lv_layer_t *a_layer, const lv_area_t *a_clip) { + lv_area_t area; + lv_draw_label_dsc_t ld; + const char *txt = YT_CAL_BADGE; + + if (ui_data_yt_cal_blocked() == 0u) { + return; + } + area.x2 = YT_BADGE_RIGHT_X; + area.y1 = UI_TITLE_Y; + area.y2 = UI_TITLE_Y + UI_TEXT_BAND_H; + /* 该版本 lv_text_get_width:长度必须传 strlen(传 -1/0 死循环或返 0) */ + area.x1 = YT_BADGE_RIGHT_X + - (int32_t)lv_text_get_width(txt, (uint32_t)strlen(txt), LV_FONT_DEFAULT, 0); + if ((area.x1 > a_clip->x2) || (a_clip->x1 > area.x2) || + (area.y1 > a_clip->y2) || (a_clip->y1 > area.y2)) { + return; + } + lv_draw_label_dsc_init(&ld); + ld.base.layer = a_layer; + ld.text = txt; + ld.text_static = 1u; + ld.color = UI_COL_FG; + ld.ofs_x = 0; + ld.ofs_y = 0; + lv_draw_label(a_layer, &ld, &area); +} diff --git a/Drivers/BSP/UI/ui_w_ytview.h b/Drivers/BSP/UI/ui_w_ytview.h new file mode 100644 index 00000000..81e7923a --- /dev/null +++ b/Drivers/BSP/UI/ui_w_ytview.h @@ -0,0 +1,83 @@ +/* + * 模块名称:UI 遥测整定视图模型(ui_w_ytview) + * 模块功能:两张遥测整定页(遥测死区 9 行 / 遥测系数 8 行)的**共用视图模型**—— + * 行表现状从 ui_data.h 通道五注入表逐行现取(工程量 + 粗守域 + 量纲 + + * 显示小数位),换算成 ui_w_setlist 的显示域(工程量 × 10^decimals), + * 并把"写一行 / 落盘确认 / 越界反显 / 未标定徽标"四件事收在一处,供两张 + * 页面薄跳板(ui_page_deadband / ui_page_calib)共用——两页共用一份状态机 + * 与换算,与 ui_w_setview"行表只有一份,界内界外就不会两套口径"同一条理由 + * 与整定四页(ui_w_setview 的写方向)**故意不同形**,三处差别都写在 .c 的函数头: + * ① 无工作叠层:死区/系数的真源本身就是 RAM 副本,语义 setter 是执行侧 + * 写入口(写值与置脏同一函数内发生),"编辑即生效、ESC 不回滚"是本档 + * 语义(与定值"攒一批整表封存"不同,取舍见 .c 文件头); + * ② 保存行不是"投递整表",而是**落盘确认**:值在逐行 ENTER 时已经写进 + * 副本并置脏,保存行按键只锁存"我这次动作的结果"(脏位归零 = 整档已 + * 外科保存成功,判据 = ui_data_yt_pending(),与 104 留痕同一条 + * "先 peek 再算成功",不另发明); + * ③ 没有 LOCK 态:本档不受定值校验和闭锁管辖;"未标定"(cal_blocked) + * 只画徽标、不禁编辑——改系数恰恰是走向标定的动作,禁了就是把路堵死 + * 值域诚实性:行 min/max **逐行来自执行侧现给**(合法域唯一出口 run_par_db_range / + * 粗守上限 RUN_PAR_K_MAX_UV)——本模块不出现任何一个范围数字字面量, + * 抄一份就是卡片判据要拦的第二真源;µV 真带(执行段裁)没有公开访问器, + * 本模块**不**补一份假的(task-43 收口前屏上只有粗守这一层,真带拒的值 + * 经 set 返回 -1 → 行反显 + 保存行 ERR,两层守卫互不掩盖) + * 分层:只依赖 Drivers/BSP/UI 同层(ui_data.h 注入表 + ui_w_setlist/ui_w_editor), + * 零 App include(layer-deps + 模拟器链接期两条老理由,见 ui_data.h 通道五表注) + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:DTU 项目组 + * 创建日期:2026-10-27 + * 修改记录: + * v1.0 2026-10-27 DTU 项目组 创建(道F task-7;判据 docs/任务规划/UI补齐.md + * §2 第 4 行;Lead 裁定:注入表八口形状、结果锁存、 + * 粗守 + 注释两层守卫、真带访问器归 task-43) + */ +#ifndef UI_W_YTVIEW_H +#define UI_W_YTVIEW_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#include "ui_w_setlist.h" /* ui_w_setlist_t + lvgl(经 ui_content_page.h) */ + +/* + * 函数功能:把**死区**视图绑到定值表实例(行表现状逐行现取;两态在内部按 + * ui_content_page_is_editable() 分派——只读不绑 commit/state,无保存行) + * 入口参数:a_inst - 页面持有的实例(static,一页一份) ui_w_setlist_t * + * 返回值:无 + * 限定条件:页面 init_cb 内调用(uiTask 上下文,此时本次进入的权限已装载) + * 函数说明:每次开页都重填行表并清历史结果(st/badmap)——上一次开页的 WAIT/OK + * 与越界反显都不该带到这一次来(同整定四页"开页清稿"纪律) + */ +void ui_w_ytview_attach_db(ui_w_setlist_t *a_inst); + +/* + * 函数功能:把**系数**视图绑到定值表实例(形状/分派/清稿与 attach_db 同一条路) + * 入口参数:a_inst - 页面持有的实例 ui_w_setlist_t * + * 返回值:无 + * 限定条件:页面 init_cb 内调用 + */ +void ui_w_ytview_attach_cal(ui_w_setlist_t *a_inst); + +/* + * 函数功能:系数页标题右侧画"未标定"徽标(cal_blocked 的屏上表达) + * 入口参数:a_layer - 绘制层 lv_layer_t * + * a_clip - 重绘区 const lv_area_t * + * 返回值:无 + * 限定条件:系数页 draw_cb 内(框架内置区块之后);只在 cal_blocked() 为真时落墨 + * 函数说明:**死区页刻意不调它**——死区数值来自 [run_par] 档、与两点标定无关, + * 在那页画"未标定"是把不相关的告警挂到不相干的屏上(徽标是断言)。 + * 几何右界取 141:与整定四页徽标同一实测理由(标题带装饰右腿占 x 约 + * 147..158);本标题"遥测系数"4 字到 x 约 104,与徽标左缘仅 1 px 间隙, + * 会被读成一句"遥测系数未标定"——该读法语义成立(本页说的就是系数、 + * 而系数此刻确实没标定过),像素黄金目检时确认过不再挪 + */ +void ui_w_ytview_draw_cal_badge(lv_layer_t *a_layer, const lv_area_t *a_clip); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_W_YTVIEW_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 2045d455..2be9583e 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -3571,6 +3571,11 @@ 1 ..\Drivers\BSP\UI\ui_w_soe.c + + ui_w_ytview.c + 1 + ..\Drivers\BSP\UI\ui_w_ytview.c + ui_w_setview.c 1 @@ -3621,6 +3626,16 @@ 1 ..\Drivers\BSP\UI\ui_page_reset.c + + ui_page_calib.c + 1 + ..\Drivers\BSP\UI\ui_page_calib.c + + + ui_page_deadband.c + 1 + ..\Drivers\BSP\UI\ui_page_deadband.c + diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 87ee4733..93fc2a97 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 257,272 B (251.24 KB) | 1,048,576 B (1024 KB) | 24.5% | -| RW_IRAM1 | 80,312 B (78.43 KB) | 131,072 B (128 KB) | 61.3% | +| ER_IROM1 | 260,004 B (253.91 KB) | 1,048,576 B (1024 KB) | 24.8% | +| RW_IRAM1 | 80,920 B (79.02 KB) | 131,072 B (128 KB) | 61.7% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **258,792 B (252.7 KB)** | 1,048,576 B (1024 KB) | **24.7%** | +| **Flash (ROM 合计)** | **261,540 B (255.4 KB)** | 1,048,576 B (1024 KB) | **24.9%** | 各区的容量与常量来源: @@ -470,6 +470,57 @@ FreeRTOS 堆不变(`free=17248 / min_ever=9312`)。验证链:`@build` 0/0 **留作候选、本轮刻意未做**:① **共享静态行缓冲池**(`dbg_console_line_claim()/release()` 发还栈上数组,改为从 2 个静态槽领取)——可再削每条日志帧的全部 152 B(全链再省 ~1.3 KB),但把"日志缓冲在调用者栈上"改成"跨任务共享 + 抢占失败即丢行",**改变日志并发语义**,需配套主机侧单测才动;② `timeSyncTask` 静态余量已正好等于 1024 B ISR 底线,**该链上再加任何调用都必须先抬栈**(已在 `Src/freertos.c` 栈注释里写明)。 +#### 4.0.7 configASSERT↔控制台幻影环:三树实测与记账口径(2026-10-26 道G task-38 出证) + +**机制**:`configASSERT` 在 FreeRTOS port/tasks/queue 内部可达(map 中 `refers to +vApplicationAssertFailed` 共 103 行),而 `vApplicationAssertFailed` 钩子(`Src/freertos.c`, +2026-09-29 起)经 `dbg_console_write_raw` 打印 `[ERR] ASSERT FAILED`;同时控制台裸发/入环 +路径一度也走 `taskENTER_CRITICAL()`(→ `vPortEnter/ExitCritical` → `configASSERT`)—— +两个方向互可达 ⇒ 调用图成**幻影环**。armlink 对环的展开策略(截断,或 `In Cycle` 单趟 +记账)**随环的形状变动**:三棵树里 port.o、FreeRTOSConfig.h 与 configASSERT 宏逐字节相同 +(本节按树核对),静态记账却差出最高 80 B——这就是「幻影环只是记账」的完整证明。 + +**三树实测**(每行钉双指纹:产物路径 + htm 内 Linker run 时刻;量数一律用**该树自己的** +`scripts/stack_report.py` 从该树根跑起,重跑脚本不算新测量): + +| 树(提交态) | 产物指纹 | `vPortExitCritical` 子树 D | 最坏链含断言族 | defaultTask / netTask / iec104Task | +|---|---|---|---|---| +| bench-21(77779d49) | `_wt\bench-21-77779d49\MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm`,Linker run 16:27:18 | **32**(展开 `⇒vApplicationAssertFailed⇒dbg_console_write_raw⇒con_tx_bounded`,钩子 D24) | 138 个函数条目的最坏子树经过钩子(浅链 +16~24 B,例 `net_get_generation` D72) | 3032 / 1912 / 1456 | +| bench-23(1d4b6a17,task-23 整行入环) | `_wt\bench-23-1d4b6a17\…\STM32F407-Demo.htm`,Linker run 19:43:40 | 8(截断) | 0 | 3040 / 1920 / 1464(+8 = `dbg_console_drain` 局部变量,task-23 真账) | +| 主干在飞(task-26 钩子 begin/end 接线,未提交) | 主干 .htm,Linker run 20:39:43 ——**该产物已被 22:52 覆盖,本行数字是会话内逐帧存档**(差分表见 task-38 卡片与 45086cf5 提交正文) | 8,但 `con_tx_release` 子树 D152(`⇒vPortExitCritical⇒vApplicationAssertFailed⇒dbg_console_tx_end (Cycle)`) | 3 条任务链各 **+80 B**;iec104Task 1544 → margin 1016 < 1024 = **FAIL** | 3120 / 2000 / 1544 | +| 主干现状(b24b2b5f,PRIMASK 落地后) | `MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm`,Linker run 22:52:12 | 8(钩子未在其下展开) | **9/9 条最坏链干净** | 3040 / 1920 / 1464(margin 2080/1920/1096,全 PASS) | + +**① 现状点名(22:52 树)**:断言→控制台这条边**仍然活着**(map refers 103 行未减,钩子 +`[Called By]` 仍挂着 port/tasks/queue 全家),只是 armlink 当前不在临界区函数下展开它—— +状态是「**休眠**」不是「解耦」。模块级记账底数还在:**37 个函数、14 个模块**的最坏子树 +经过 `vPortExitCritical`(每个吃 8 B 底账)——`net_socket`×12、`net_pending`×9、`tasks`×3、 +`task_watchdog`×2、`port`×2、`adc_input_poll` / `tpafe5160_snapshot` / `sys_clock_get` / +`osEventFlagsGet` / `xEventGroupClearBits` / `prvIsQueueEmpty` / `vListInsertEnd` / +`fire_event` / `s_epoch_ms` 各 1。**不止 console_log 一家**:凡走内核临界区 API 的模块都在 +这张账本上,环形状再变时先咬这些链。 + +**② 量化表("把环断掉"能回收多少)**:现状每链 **0 B**——断言已不在任何最坏链,没有可 +回收的存量;历史兑现值即上表:浅链 +16~24 B(b21 时代)→ 全图截断(b23 起)→ 深链 ++80 B×3(20:39 实证)。环形状再波动一次的潜在摆动带 ≈ **−80 ~ +144 B/链**(弹药深度 = +`dbg_console_write_raw` 现测 D128 + 钩子自身帧 0)。 +⚠ **反向风险(钉死,原话引用为准):若将来 `configASSERT` 被定义成空宏或被裁剪,本表 +每链会"回收"约 80~90 B——那是记账变化,不是固件变好;谁把它写成优化收益就是在骗人** +(真实代价是内核误用全部静默)。要"正式断环"而不牺牲取证,正确方向是让断言钩子的 +发送口不再回流临界区(task-26 的 PRIMASK 窗口已是这个方向),**不是**把宏裁空。 + +**③ 口径:硬线 vs 实际余量**:静态最坏链是**保守建模**——`Error_Handler`(own 152 + 环 +记账 208 = D360)与 `[ERR]→NVIC_SystemReset` 这类终点路径,真实运行不会把环走满;它 +也不算 ISR 上下文保存(约 100 B)与间接调用(§4.0.6 盲区条同源)。所以静态链适合当 +**硬线**(可复现、无噪声、机器可判:margin ≥ max(1024 B, cfg×30%)),不适合当实际余量; +实际余量看 §4.0.2 的串口 `[STACK]` 水位。台架判据「栈水位不劣于上一轮」必须加容差或 +改拿本节的静态硬线(「栈水位判据加容差或改静态最坏链当硬线」卡 = task-32,与本节和 +§4.0.2 互指)。 + +**取证纪律(本节方法的可复用模板)**:凡跨树/跨构建引用数字,必须同时钉**产物路径 + +Linker run 时刻**——commit hash 不是稳定身份(重写正文即换 hash),tree hash + 父提交 +才是;测量脚本必须从目标树根用该树自己的 `scripts/`(`REPO_ROOT` 跟随 `__file__`, +拿主树脚本进别的树量 = 量的还是主树产物,2026-10-26 实测踩实)。 + #### 阶段 0 验收 - 串口可见 `[STACK]` 报告:每任务 `peak_used/free_min` + 堆 `free/min_ever` diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index ed60dc60..3abe1469 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,18 +15,18 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 257,272 B | 1,048,576 B | 24.5% | 参考 | +| ER_IROM1 (Flash) | 260,004 B | 1,048,576 B | 24.8% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 80,312 B | 131,072 B | 61.3% | PASS | +| RW_IRAM1 | 80,920 B | 131,072 B | 61.7% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB | | 存储 | 7,072 B | 12 KB | OK | -| UI | 33,533 B | 16 KB | ⚠ 超 16.7 KB | +| UI | 34,085 B | 16 KB | ⚠ 超 17.3 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 15,485 B | 8 KB | ⚠ 超 7.1 KB | +| 应用 | 15,537 B | 8 KB | ⚠ 超 7.2 KB | | 其它 | 10,523 B | — | 参考 | diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 121b5c3e..0d73867f 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -9,3 +9,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736 2026-10-25 224860 24640 1332 135100 249500 23.8 79008 60.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736 2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736 +2026-10-27 233900 26104 1536 136808 260004 24.8 80920 61.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 97f7449f..74ee6d99 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -105,6 +105,7 @@ set(UI_SOURCES ${UI_DIR}/ui_data.c # 数据快照存侧(道E task-14;写侧 App/app/ui_data_feed.c 不进模拟器,见 scripts/check_registration.py) ${UI_DIR}/ui_w_soe.c # SOE 记录组件(道E task-14;reader 已在批 2 两端绑定) ${UI_DIR}/ui_w_setview.c # 定值查看视图模型(道E task-15 批 3:四张视图表 + Q10 换算 + 越界反显) + ${UI_DIR}/ui_w_ytview.c # 遥测整定视图模型(道F task-7:死区/系数两页共用注入表视图 + 落盘确认状态机) ${UI_DIR}/ui_page_soe.c ${UI_DIR}/ui_page_view_param.c ${UI_DIR}/ui_page_view_plate.c @@ -114,6 +115,8 @@ set(UI_SOURCES ${UI_DIR}/ui_page_clear.c ${UI_DIR}/ui_page_clock.c ${UI_DIR}/ui_page_reset.c + ${UI_DIR}/ui_page_calib.c + ${UI_DIR}/ui_page_deadband.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) diff --git a/simulator/main.c b/simulator/main.c index 93cfd9bf..365bce96 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -267,6 +267,143 @@ static const ui_data_maint_ops_t s_maint_ops = { sim_maint_clear_req, sim_maint_clear_state }; + +/* + * ======================= 道F task-7:遥测整定注入表假件(镜像 ui_data_feed.c 形状) + * + * 初值/合法域 = App/algo/run_par.c 工厂 ROM 与 RUN_PAR_DB_MAX_* 档位的镜像 + * (模拟器不链 run_par,真源抄不来只能镜像;变更纪律同 s_sim_log_map 那条: + * 执行侧改档不改这里,屏上会呈现"档位与量纲自相矛盾"的肉眼可见错,黄金像素兜底)。 + * ⚠ 脏位口径是这一家的存在理由:**写成功只置脏、绝不同步清**,清脏的唯一去处是 + * sim_yt_pump()(镜像 defaultTask 每轮无条件 run_par_store_save())。同步清 = + * WAIT 态在模拟器里永不出现,"落盘确认"三态就只剩最好看那一帧被黄金钉住 + * (Lead 裁定④原文的理由,别顺手"优化"成同步)。 + */ +static float s_yt_db[UI_DATA_YT_DB_N] = { + 0.5f, 0.5f, 0.5f, 0.5f, /* 电压死区 0.5 V(工厂额定档) */ + 0.005f, 0.005f, 0.005f, 0.005f, /* 电流死区 0.005 A */ + 0.5f /* 频率死区 0.5 Hz(绝对档) */ +}; +static float s_yt_k[UI_DATA_YT_CH_N] = { + 152.587890625f, 152.587890625f, 152.587890625f, 152.587890625f, + 152.587890625f, 152.587890625f, 152.587890625f, 152.587890625f +}; /* = adc_cal ROM 的 TPAFE 系数(µV/码);显示小数位 0 的量化后果见真机侧装配注 */ +static uint8_t s_yt_dirty; /* 镜像 run_par 的整档脏位(页面上 WAIT 的唯一来源) */ +static int s_yt_nopump; /* --yt-nopump:拍 WAIT 态黄金专用(截图前不清脏) */ + +/* 死区粗守上限(工程量):镜像 RUN_PAR_DB_MAX_U/I/F 三档;分档与 feed 的 unit + * 分档必须同序(同一条对表纪律) */ +/* 函数功能:假死区粗守上限(工程量档,镜像 RUN_PAR_DB_MAX_U/I/F) 返回值:上限 float */ +static float sim_yt_db_hi(uint8_t a_idx) { + if (a_idx < 4u) { + return 50.0f; + } + if (a_idx < (UI_DATA_YT_DB_N - 1u)) { + return 1.0f; + } + if (a_idx == (UI_DATA_YT_DB_N - 1u)) { + return 5.0f; + } + return 0.0f; +} + +/* 函数功能:假死区点数(镜像 RUN_PAR_DB_N) 返回值:点数 uint8_t */ +static uint8_t sim_yt_db_n(void) { + return (uint8_t)UI_DATA_YT_DB_N; +} + +/* 函数功能:假死区行现状(值现取自家数组、档位分档与 feed 同序) 返回值:0 / -1 int */ +static int sim_yt_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out) { + if ((a_out == NULL) || (a_idx >= UI_DATA_YT_DB_N)) { + return -1; + } + a_out->val = s_yt_db[a_idx]; + a_out->lo = 0.0f; + a_out->hi = sim_yt_db_hi(a_idx); + a_out->decimals = 3u; + a_out->unit = (a_idx < 4u) ? UI_DATA_YT_UNIT_V + : ((a_idx < (UI_DATA_YT_DB_N - 1u)) ? UI_DATA_YT_UNIT_A : UI_DATA_YT_UNIT_HZ); + return 0; +} + +/* 函数功能:假写死区——写守卫与执行侧同款语义:越界拒 = 值不动、**不置脏** + * (置了脏就是"屏上没改、却要落盘"的空承诺);接受 = 落值 + 置脏, + * 同一函数内发生。 返回值:0 / -1 int */ +static int sim_yt_db_set(uint8_t a_idx, float a_eng) { + if ((a_idx >= UI_DATA_YT_DB_N) || (a_eng < 0.0f) || (a_eng > sim_yt_db_hi(a_idx))) { + return -1; + } + s_yt_db[a_idx] = a_eng; + s_yt_dirty = 1u; + return 0; +} + +/* 函数功能:假系数点数(镜像 RUN_PAR_CH_N) 返回值:点数 uint8_t */ +static uint8_t sim_yt_cal_n(void) { + return (uint8_t)UI_DATA_YT_CH_N; +} + +/* 函数功能:假系数行现状(hi = RUN_PAR_K_MAX_UV 的镜像;见上注变更纪律) 返回值:0 / -1 int */ +static int sim_yt_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out) { + if ((a_out == NULL) || (a_idx >= UI_DATA_YT_CH_N)) { + return -1; + } + a_out->val = s_yt_k[a_idx]; + a_out->lo = 0.0f; + a_out->hi = 1000000000.0f; /* = RUN_PAR_K_MAX_UV 的镜像(粗守上限) */ + a_out->decimals = 0u; + a_out->unit = UI_DATA_YT_UNIT_UV; + return 0; +} + +/* 函数功能:假写系数 k(同 db_set 的"写值与置脏成对"语义) 返回值:0 / -1 int */ +static int sim_yt_cal_k_set(uint8_t a_idx, float a_uv) { + if ((a_idx >= UI_DATA_YT_CH_N) || !(a_uv > 0.0f) || (a_uv > 1000000000.0f)) { + return -1; /* k<=0 与超粗守都拒;真带这一层模拟器不复刻(执行段的事) */ + } + s_yt_k[a_idx] = a_uv; + s_yt_dirty = 1u; + return 0; +} + +/* 函数功能:假未标定徽标源(恒已标定,理由见函数体注) 返回值:0 uint8_t */ +static uint8_t sim_yt_cal_blocked(void) { + /* 模拟器恒"已标定":徽标腿的像素证据由整定四页黄金覆盖(同一注入式画法); + * 要拍"未标定 + 系数页"的组合另立带旗标的黄金态,本批不扩表 */ + return 0u; +} + +/* 函数功能:假整档脏位(页面上 WAIT 的唯一来源) 返回值:脏位 uint8_t */ +static uint8_t sim_yt_pending(void) { + return s_yt_dirty; +} + +static const ui_data_yt_ops_t s_yt_ops = { + sim_yt_db_n, + sim_yt_db_row, + sim_yt_db_set, + sim_yt_cal_n, + sim_yt_cal_row, + sim_yt_cal_k_set, + sim_yt_cal_blocked, + sim_yt_pending +}; + +/* + * 函数功能:落盘脏位的唯一清去处(镜像 defaultTask 每轮无条件 run_par_store_save) + * 入口参数:无 + * 返回值:无 + * 限定条件:主循环 / 无头注键尾各一处;--yt-nopump(拍 WAIT 黄金)时整函数空转 + * 函数说明:真机"保存成功"的语义 = cfg_file_save 返 0 才清脏;模拟器无失败面, + * pump 即落盘——ERR 腿不复刻(拍不出来也别假拍,屏上 ERR 由写被拒触发, + * 那条模拟器能真拍:见黄金 cal_reject) + */ +static void sim_yt_pump(void) { + if (s_yt_nopump != 0) { + return; + } + s_yt_dirty = 0u; +} /* * 函数功能:SOE 记录假源——固定 3 条(镜像 App/task/ui_task.c 真机绑定的形态) * 入口参数:无 @@ -584,6 +721,9 @@ static int run_headless(const char * keys, const char * path, int standby) { printf("[sim] UI maint bind: FAIL\n"); } ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */ + if (ui_data_yt_bind(&s_yt_ops) != 0) { + printf("[sim] UI yt bind: FAIL\n"); + } /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); @@ -617,6 +757,7 @@ static int run_headless(const char * keys, const char * path, int standby) { * 无头模式没有那个节拍,不在此当场排空,截图就会停在 WAIT(终态拍不到, * OK/ERR/LOCK 三条判据的像素证据也就全都不成立) */ sim_param_pump(); + sim_yt_pump(); /* 道F task-7:截图前清脏 = 拍 OK 黄金;--yt-nopump 停转 = 拍 WAIT 黄金(Lead 裁定④:两态都要有像素证据) */ /* --standby:注键后强制进入待机屏(截图前) */ if (standby != 0) { @@ -1009,6 +1150,9 @@ int main(int argc, char **argv) { if (strcmp(argv[i], "--noclock") == 0) { s_noclock = 1; /* 把这台机器装成"墙钟无效"(RTC 异常/未对时) */ } + if (strcmp(argv[i], "--yt-nopump") == 0) { + s_yt_nopump = 1; /* 落盘 pump 停转:拍"已受理未落盘"(WAIT)黄金专用 */ + } } /* 截图/自检模式:--screenshot / --keys / --standby / --cfgtest / --logtest */ @@ -1080,6 +1224,9 @@ int main(int argc, char **argv) { printf("[sim] UI maint bind: FAIL\n"); } ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */ + if (ui_data_yt_bind(&s_yt_ops) != 0) { + printf("[sim] UI yt bind: FAIL\n"); + } /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); @@ -1128,6 +1275,7 @@ int main(int argc, char **argv) { /* 落定定值整定请求(镜像真机 ui_data_feed_tick 里的 ui_data_param_drain: * 出队 → prot_set 落值 → 重发参数帧);队列空时只是一次计数判断 */ sim_param_pump(); + sim_yt_pump(); /* 道F task-7:窗口模式同样每轮无条件清脏(真机 50 ms 落盘的镜像) */ /* 配置文件轮询(2s 周期:外部文件变更 4s 沉降后自动重载) */ uint32_t now_ms = (uint32_t)SDL_GetTicks(); diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index 5c89a5c6..cb885c51 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -102,6 +102,25 @@ PAGES = [ ("clear_2nd", "dddd eddde uruuruuue e"), ("clear_done", "dddd eddde uruuruuue ee"), ("clear_ro", "dddd eddde e"), + # ---- 道F task-7 遥测整定两页(根菜单第 4 项 = 3 下;子项 0/1 = 死区/系数) + # 键序读法与 task-3 三页同源:e 进子菜单后再 e 开叶子(#P → 先落密码页), + # "uruuruuue" = 修改口令 123(可编辑)。 + # 死区 9 行 + 保存 = 10 行(2 页);系数 8 行 + 保存 = 9 行(2 页,保存行独在第 2 页)。 + # ⚠ wait/ok 两张**必须成对存在**:脏位在模拟器由 sim_yt_pump 清(镜像真机 + # defaultTask 每轮无条件落盘),wait 态靠 --yt-nopump 停 pump 拍下来—— + # 只钉 OK 就是"只钉最好看那一帧"(Lead 裁定 task-7 派发④原文), + # 真机 WAIT 窗口毫秒级、人工永远补拍不到,只有这里替它留了像素证据。 + ("deadband", "ddd ee uruuruuue"), # 首屏(选中首行 UA死区 0.500V) + ("deadband_wait", "ddd ee uruuruuue e uu e ddddddddd e"), # UA→0.502 提交 → 保存行 ENTER:WAIT(+--yt-nopump) + ("deadband_ok", "ddd ee uruuruuue e uu e ddddddddd e"), # 同键序、pump 正常 → OK(脏位真被清) + ("calib", "ddd e de uruuruuue"), # 首屏 8 行系数(153uV = ROM TPAFE 整 µV) + ("calib_wait", "ddd e de uruuruuue e u e dddddddd e"), # UA→154 提交 → 保存行:WAIT(+--yt-nopump) + ("calib_ok", "ddd e de uruuruuue e u e dddddddd e"), # 同键序、pump 正常 → OK + # 拒后**再**移到保存行(不按保存):一张图同时钉"粘滞 ERR 词"与"越界行反显" + # (r0 反显要等光标移开才单独可见——与选中白条重叠时像素分不出来,这是一开始 + # 拍在首屏的图目检后改出来的键序);不按保存是因为按键会重新评估、ERR 会被 + # 新一轮 WAIT/OK 覆盖——ERR 的像素证据只有这条窗口是真的 + ("calib_reject", "ddd e de uruuruuue e ddd l ddddd l d e dddddddd"), ] # 文本级断言表:页名 -> [(名称, 期望串, y 带, 是否反显)] @@ -192,6 +211,52 @@ TEXTS = { ("r0名", "运行记录", (24, 38), True), ("r6名", "遥控记录", (108, 122), False), ], + # ---- 道F task-7 遥测整定两页。行槽沿用列表页口径(24 + 14i),单位串由 + # ui_w_setlist 直接贴数值尾("0.500V"无空格),µV 在屏上是 ASCII "uV" + "deadband": [ + ("标题", "遥测死区", (0, 24), False), + ("r0名", "UA死区", (24, 38), True), ("r0值", "0.500V", (24, 38), True), + ("r4名", "IA死区", (80, 94), False), ("r4值", "0.005A", (80, 94), False), + ("r7名", "IN死区", (122, 136), False), ("r7值", "0.005A", (122, 136), False), + ("页脚", "第 1 页,共 2 页", (140, 160), False), + ], + "deadband_wait": [ + ("标题", "遥测死区", (0, 24), False), + # 第 2 页窗口:槽 0 = Hz死区行(跟随真源、本轮未动),槽 1 = 保存行(选中反显) + ("r0名", "Hz死区", (24, 38), False), ("r0值", "0.500Hz", (24, 38), False), + ("保存名", "保存", (38, 52), True), ("保存值", "WAIT", (38, 52), True), + ("页脚", "第 2 页,共 2 页", (140, 160), False), + ], + "deadband_ok": [ + ("标题", "遥测死区", (0, 24), False), + ("r0名", "Hz死区", (24, 38), False), + ("保存名", "保存", (38, 52), True), ("保存值", "OK", (38, 52), True), + ("页脚", "第 2 页,共 2 页", (140, 160), False), + ], + "calib": [ + ("标题", "遥测系数", (0, 24), False), + ("r0名", "UA系数", (24, 38), True), ("r0值", "153uV", (24, 38), True), + ("r7名", "IN系数", (122, 136), False), ("r7值", "153uV", (122, 136), False), + ("页脚", "第 1 页,共 2 页", (140, 160), False), + ], + "calib_wait": [ + ("标题", "遥测系数", (0, 24), False), + ("保存名", "保存", (24, 38), True), ("保存值", "WAIT", (24, 38), True), + ("页脚", "第 2 页,共 2 页", (140, 160), False), + ], + "calib_ok": [ + ("标题", "遥测系数", (0, 24), False), + ("保存名", "保存", (24, 38), True), ("保存值", "OK", (24, 38), True), + ("页脚", "第 2 页,共 2 页", (140, 160), False), + ], + "calib_reject": [ + ("标题", "遥测系数", (0, 24), False), + # 第 2 页(光标移到保存行、未按键):k=0 被执行侧拒 → **值纹丝不动** + # (副本未动、不置脏——"写值与置脏成对"的另一半判据)+ 保存行粘滞 ERR; + # r0 的越界反显在第 1 页像素里(文本断言不猜反显形状,逐字节比对钉住) + ("保存名", "保存", (24, 38), True), ("保存值", "ERR", (24, 38), True), + ("页脚", "第 2 页,共 2 页", (140, 160), False), + ], # v2.0(道E task-15 回炉):6 行 Flash 字面量 -> 8 行真值;标签去全角冒号(要给值列 # 让出 MAC 的 102 px),行槽 = 38 + 13i。值全部来自模拟器固定帧(simulator/main.c 的 # sim_publish_data_frames)与 net_cfg 出厂副本 —— 逐行断言等于把这两处口径钉在像素侧。 @@ -569,6 +634,10 @@ POOL_AFTER_ROUNDTRIP = 5448 # 不为它另造一套假数据:假的是"时钟有没有"这件事,页面与显示链全是真的。 EXTRA = { "clock_noclock": ["--noclock"], + # 道F task-7:WAIT 态黄金 = 落盘 pump 停转(假件保留脏位,Lead 裁定④); + # OK 态同键序不加旗标,两图唯一差别就是"脏有没有被清"——对照即凭证 + "deadband_wait": ["--yt-nopump"], + "calib_wait": ["--yt-nopump"], } diff --git a/simulator/tests/ref/shot_calib.bin b/simulator/tests/ref/shot_calib.bin new file mode 100644 index 0000000000000000000000000000000000000000..b9180514c4e01377e85d9ddcb269fb49cb6ad189 GIT binary patch literal 3200 zcmd6p&1(}u7{=fIXp`1xHt3-Tq3tB}P=w~#gRp4~NDtB?dTvo~V%wO0gi@PnwKOKu z59v(=(Sxw3;#DM3g#HDdiV})=Y=Z}@G+zdW;foDT9THr`o;Arl#8tomZ`-A3n^tT2HUOHmJPu!gJ2`y ztd7AOf=$3E7Xzx5+qocE1>#%`5=-wBYuVj8I2-!97<-iGG5`?bBGHyiT@h=~0km>f z+Ua?6=_QvJ0hqOZvH+UhFcoHLKsK#k_7AVj#;w?Pjj`p=^}J?IS93Q#R^9qta(vVC zw&y#X*%oZ-!LDxFVtkhEe3o-r`rsauHFGLwUh~-ezF?DcKR)^FU`+TnwRepyQ<^>N zv9awj+qqxy?YJZOq_;!RVNu#;AN-d9t`_+V_3j^T! zlzhx($;d(3d6A9Ju2$|)2e4VupHk;sq~$kUFuf2ItZ=)n*Jt`VmxaDsQes_8-#xY- zTL)({AJ>FndZqstXAsQaqCy=XBTE~J=Le6C})tp>1NWT!IUkK7K1nHM3k_$olg&_Sx{-b`GK>DQ{=@)|Z z3qksYApJs+ej!M|V7WA_UouF)M3H`pBK<;;ej!M|5TsuS(l4ZG{c^&;uhX+DL4fcf zIM21_Pxoz1HSp?OX$cBxPzzJu|z*+voh-Ei9+kyfUtCCS}PI1-% zjfw26p)BxTx?7B~_4mER{sEIgfCv-m(Tu*vd&dDr7`tmG59iOBoCcuQ!P*9pEl)M5 z#e*y|*fzu!_Ksp%Ca(9q)WlD_g7s2Nn)tdPZr822Y)AOIFAmd+K^JzPIa{fAT7tR0 zv&H%kR{iXs@ydJgO{ed>a#*D(ST9w!p@}bU1+xJy6IVN@O!YtJ5aafb@)PhA@DuP8 z@DuP8_y-A~5Wqwa>~}5xa_);-WC_wXYallQX#;d1lp$4e8U5=)6M`mEXWDMB1&X5B zy{anS%25b$78eXzpenjXed(y{ql-9YbR5!W7D+2Qbs!K7a-;;8aXL#|s=31)6Obby V(d$Gshsl$4A<=mNG|K;^z5xUxLV5rI literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_calib_reject.bin b/simulator/tests/ref/shot_calib_reject.bin new file mode 100644 index 0000000000000000000000000000000000000000..e03aa3ddd8c5167209570da517e5ce7b5740facc GIT binary patch literal 3200 zcmeH}u}i~16vp2rsjXTxMG*(VBE>-jBQ6eNYR%F~5qJF)90jW$;@}|Y;I23Z1edBH z#p=@4%|Xyo#KA=uMd;P|a+kZ(tfGr~L-O5^@4dYHrU3l7XjRj%&DJqXowY)2zLj+h zk0eOQ7t6Wk-b3Icl#9xZYwH=olBCQQ z2_cWRw*BGB4zJ>Q%zpd|Q)b_8D8^D1Uo-oBS>3MjaUZaCm;p!={4 literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_calib_wait.bin b/simulator/tests/ref/shot_calib_wait.bin new file mode 100644 index 0000000000000000000000000000000000000000..47a1d7283c598949c9ba3c552df6f8728af0ad14 GIT binary patch literal 3200 zcmeH}Jxjw-6o$`DQd_lXM28N7MWllW21IZWQ#DH`i|8N@`V$<5`f(8l2SEpS#WA4h z;0H*ty14oaY-z>8MK^`sHlEyjliIAJi#bE`o`?6`ocm4z_;JyaqTcDv$1Js%*CN?w z)|MKKk$^8UW$SxS(nly45BIjI4hR+}1uimec1z?yuyGJ<>BUZD<^)@TWiBQad$Zgl zSP2X+GX1tTEN1bv1ZOkPTLarWTt)%nT%=p4tZ6aZ0nozPZ8^0&bj)Qh09u=C6+m?Y zRYFSuIkb7~4?8osisLY|SPxQWK5qD|wpfW`!_2E?|8}*7kD_4f4d1+EM$Yx~s!2AH zufBPVjY{9qNYP{UbIJ0r{x|BEl3iCDzEjCAa^G0R)yzD(@fr4RF*9F1sfiQtEfiQtEfq#$yi2-!h!Tql_KjnSN8y$nR%SyzHK$!&#C^Die9>cd4We{aV z?|b)V9ndt*oz->!U%3lnf+c-M9MCn(KwrAc^I?-jhU?GHq)$ literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_deadband.bin b/simulator/tests/ref/shot_deadband.bin new file mode 100644 index 0000000000000000000000000000000000000000..d7834924682d554d7951cf13b142699fb719dd09 GIT binary patch literal 3200 zcmc(hL2DC17>2*yY)xB(ZY_d`Lf2_5*i%kgw2(C{*!G~+KhV;nP-#q43O#g{fawN` z7CqOSxEH->51}O3LiGnIcuFXu$M&XRnssJ&Ci%9zh*(VLva?UVnc00Z^RfWPZgyP_ z%`ts?&->mC;@;sAq6HCjWn9uFp#;(3iowpfpW_@SBJUc18T7MCk4V*FvHHusLP z{BmVlz{6TC|CzI;@_kuX+s%LCY;iN+V!wR0aPRdpVqbXO)&{QQEVor@&9_;eE3>*% zxh&UhUfo^0&Y1V3ydvfk7s~dx<$FkWEZwDzhFA4xP^x3ukF4uhrtKq8O=Eq6^(lK{ zvkri27|vzZac){q*_Afc7X13-Vx7ICI zm)o)Ay2^*q><(klZQ1M+X!@9Zj{yGHo1z!?vZ|T3n6K?ci|`T;dvSUqyqu5lqW+zi z>p7SL7q9cj?!tO_`$OuVp>n zfOtdE2M^>}4|GjqV|AU*>9;<~j22T^ot2_>W}mEMgD9hqW4D=SQ${NkXfc?wb$DaY hXE{)-a*Zc8#1f#=*GZ%Vi}lkR73kwYit_)`egkJa2}b|` literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_deadband_ok.bin b/simulator/tests/ref/shot_deadband_ok.bin new file mode 100644 index 0000000000000000000000000000000000000000..0c4208fce1733a04f6272bd90d1f39aecf5c24b9 GIT binary patch literal 3200 zcmeH}&ubGw6vy98rY6>)TYK>!bP2T(FD?irNO0G%hsKM+f1vpj>`kaYM(IIwN>e=B z^Mc@|Do8MhP`t=Viy(rBeTVG#ee#|6X5L%|;ID%Z>4_Ta zXO6^=D2?Y=E>~C@!CWJ#%q5U;_3!@s;M(#NZ7h?463zMGrr*-64w|ih4}vXQvpdjM z;sy;CS?n8vqgb;NoM*ZoR=c3st#6MjmvrAEKv^;GbfdAX6r2S(rPRBll}GV8E&`x6 z9d`i=A`?TjWRT$LW&dN=T?)lOZ;TA;8wXN$na4E(>e;=f)W~+Z6adt=J5?!m_Wf=7 z#h`YrQ>~*IJzeW4A{pykDPhum*4mn3Rmrw#d)8OAko6j#ski3$n(|h5KKMyr^0BsY zAbUW`=kk*ZtDc-Ag)@npy&@q(_b|%R(EaqluxM4u(0$!Dw?5fR*nQ)he2JN}Sxn&M zvu4-(<72}T-|p4J6RiJp+T*KU>%JLJeXnA+w`o|k`cT5qeeu>X2{1ypKYp&%^e4=W zBv11RYv4&XBuN>18+_o%h&B6K_KP7D zLt-tBlC=UDV{)x!$)}Kt1t@7|8%6|J%y-b|EhIinY-G3{K4pS?%tr?S!=T0Q!Al$W gGNOaH1#~$S%0TfrWhZ8ZMNBB}2f&d}wSN)60fqQv#{d8T literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_deadband_wait.bin b/simulator/tests/ref/shot_deadband_wait.bin new file mode 100644 index 0000000000000000000000000000000000000000..22fcb819e8187ec1f9439a3b406520e151987a47 GIT binary patch literal 3200 zcmeH}&ubGw6vy98rY6>)TYK>!bP2T(F9t*i65KWHq48o6Jcx(pPp~(kDx>sJb4pV@ z+w+2=2Y-MBg9x=Z|AT~96udMyg-mvR^J6#J^&*JiVc#M9eV=^i&CHw21NiIUL;72d zwG#*7w;+k`eaP{xr2mkW&mNphiUx~)Nf6Z%ZRs+q}zWV;At=S!D zDRGthi_G|jP*JQ=@=r5e538L~?E05SrE_}UJU~$~_jtX&tQ4FCIHuJ5gO!Ko9%lj2 znvA;uxiA$&w4_kTlgs|ctg|G-zCIY~*Vgx?>@b&W1k|;=4XKgsaw!0)ZMVx(?Cg2l z@{2+3YP(!RF?zh(Rzxz^zEHrV^Q5^s!^)Cv)7Gr7Y$5B`-MHSW*KNpG+4<;2`;t%9 z^?kVq6g)0JiLh$XJrX#TIO!(}L*Wd9G!@S02Qi!6KFnhxoHwob)hD-p3^cnx_1!eA zvLXE{CWc+yJa zc3SrG0`h`bOQUFQ2#hhg*0SU%q+$UIn%OZUAy~|-pwC^1e3;nC@Hl+RLhdpT9Ry+q mO@0qv*m#x^?VBy2!y#7$iq|Pt49hKILh(ER6*;Q?i}(e-N?|tu literal 0 HcmV?d00001 diff --git a/test/host/ui_data/ui_data_test.c b/test/host/ui_data/ui_data_test.c index aec2699c..b81dee19 100644 --- a/test/host/ui_data/ui_data_test.c +++ b/test/host/ui_data/ui_data_test.c @@ -74,6 +74,11 @@ * test-cppcheck 抓过,真判据落在 TC-UIDATA-026s) * TC-UIDATA-025 维护通道未注入防御:六口安全返回 + 残表整表拒绝且不覆盖好表 * TC-UIDATA-026 注入后转发言行:epoch 0 哨兵不下发、reset_req 自行投操作记录=0 次 + * TC-UIDATA-027 遥测整定通道未注入防御:八个口一律安全返回、不碰出参; + * 残表整表拒绝且不覆盖好表(与 025 同一条纪律的第五张表复刻) + * TC-UIDATA-028 遥测整定注入后转发言行:行现状逐字段原样上抛、写口的 idx 与 + * 工程量原样下传、返回码直通、pending 无状态直读(本层零缓存—— + * 页面 OK/WAIT 的唯一依据必须是执行侧此刻的脏位) * (谁触发谁投,防"远端 + 本地"双投)、clear 状态码直通不改码 * * 通过:全部 TC PASS(main 返回 0)。 @@ -1297,6 +1302,184 @@ static void tc_maint_forwarding(void) CHECK(s_stub_clear_state_calls == 2, "TC-UIDATA-026r state queried once per call"); } +/* ── 道F task-7:通道五(遥测整定注入表)桩 ─────────────────────────────── */ + +static uint8_t s_yt_db_calls; +static uint8_t s_yt_db_last_idx; +static float s_yt_db_last_val; +static int s_yt_db_set_rc; +static uint8_t s_yt_cal_calls; +static uint8_t s_yt_cal_last_idx; +static float s_yt_cal_last_uv; +static int s_yt_cal_set_rc; +static uint8_t s_yt_blocked; +static uint8_t s_yt_pending_ret; +static int s_yt_pending_calls; + +static uint8_t yt_stub_db_n(void) +{ + return UI_DATA_YT_DB_N; +} + +static int yt_stub_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out) +{ + if (a_idx >= UI_DATA_YT_DB_N) { + return -1; + } + a_out->val = 0.125f + (float)a_idx; + a_out->lo = 0.0f; + a_out->hi = 50.0f; + a_out->unit = UI_DATA_YT_UNIT_V; + a_out->decimals = 3u; + return 0; +} + +static int yt_stub_db_set(uint8_t a_idx, float a_eng) +{ + s_yt_db_calls++; + s_yt_db_last_idx = a_idx; + s_yt_db_last_val = a_eng; + return s_yt_db_set_rc; +} + +static uint8_t yt_stub_cal_n(void) +{ + return UI_DATA_YT_CH_N; +} + +static int yt_stub_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out) +{ + if (a_idx >= UI_DATA_YT_CH_N) { + return -1; + } + a_out->val = 152.5f; + a_out->lo = 0.0f; + a_out->hi = 1000000000.0f; + a_out->unit = UI_DATA_YT_UNIT_UV; + a_out->decimals = 0u; + return 0; +} + +static int yt_stub_cal_k_set(uint8_t a_idx, float a_uv) +{ + s_yt_cal_calls++; + s_yt_cal_last_idx = a_idx; + s_yt_cal_last_uv = a_uv; + return s_yt_cal_set_rc; +} + +static uint8_t yt_stub_blocked(void) +{ + return s_yt_blocked; +} + +static uint8_t yt_stub_pending(void) +{ + s_yt_pending_calls++; + return s_yt_pending_ret; +} + +static const ui_data_yt_ops_t s_yt_stub = { + yt_stub_db_n, yt_stub_db_row, yt_stub_db_set, yt_stub_cal_n, + yt_stub_cal_row, yt_stub_cal_k_set, yt_stub_blocked, yt_stub_pending +}; + +/* 残表:只缺最后一口(pending)——八口校验里最容易被"顺手少注一件"漏掉的那个 */ +static const ui_data_yt_ops_t s_yt_broken = { + yt_stub_db_n, yt_stub_db_row, yt_stub_db_set, yt_stub_cal_n, + yt_stub_cal_row, yt_stub_cal_k_set, yt_stub_blocked, NULL +}; + +/* + * TC-UIDATA-027 未注入防御(025 的第五张表复刻):八个口一律安全返回; + * db_row/cal_row 未注入时**不得碰出参**(页面拿栈上未初始化结构当现状读, + * 比返回 -1 更坏——这里用预填哨兵钉住"一个字节都没写")。 + */ +static void tc_yt_not_bound(void) +{ + ui_data_yt_row_t r; + float sentinel = -777.0f; + + (void)ui_data_yt_bind(NULL); + + r.val = sentinel; + CHECK(ui_data_yt_db_n() == 0u, "TC-UIDATA-027a db_n not bound is 0"); + CHECK(ui_data_yt_db_row(0u, &r) == -1, "TC-UIDATA-027b db_row not bound is -1"); + CHECK(r.val == sentinel, "TC-UIDATA-027c db_row not bound wrote nothing"); + r.val = sentinel; + CHECK(ui_data_yt_cal_row(0u, &r) == -1, "TC-UIDATA-027d cal_row not bound is -1"); + CHECK(r.val == sentinel, "TC-UIDATA-027e cal_row not bound wrote nothing"); + CHECK(ui_data_yt_cal_n() == 0u, "TC-UIDATA-027f cal_n not bound is 0"); + CHECK(ui_data_yt_db_set(0u, 1.0f) == -1, "TC-UIDATA-027g db_set not bound is -1"); + CHECK(ui_data_yt_cal_k_set(0u, 1.0f) == -1, "TC-UIDATA-027h cal_k_set not bound is -1"); + CHECK(ui_data_yt_cal_blocked() == 0u, "TC-UIDATA-027i blocked not bound is 0 (never fabricate)"); + CHECK(ui_data_yt_pending() == 0u, "TC-UIDATA-027j pending not bound is 0"); + CHECK(ui_data_yt_db_row(0u, NULL) == -1, "TC-UIDATA-027k NULL out refused even bound-or-not"); + CHECK(ui_data_yt_bind(&s_yt_broken) == -1, "TC-UIDATA-027l incomplete table refused"); + CHECK(ui_data_yt_db_n() == 0u, "TC-UIDATA-027m refused table left channel unbound"); + CHECK(ui_data_yt_bind(&s_yt_stub) == 0, "TC-UIDATA-027n complete table accepted"); + CHECK(ui_data_yt_db_n() == UI_DATA_YT_DB_N, "TC-UIDATA-027o bound table forwards count"); + CHECK(ui_data_yt_bind(&s_yt_broken) == -1, "TC-UIDATA-027p broken refused again"); + CHECK(ui_data_yt_db_n() == UI_DATA_YT_DB_N, "TC-UIDATA-027q refusal keeps the good table active"); +} + +/* + * TC-UIDATA-028 转发言行(两张页面全部判据的地基): + * - 行现状逐字段原样上抛(val/lo/hi/unit/decimals 五样一个不许"顺手加工"—— + * 合法域是执行侧事实,转发层改一个数就是第二真源) + * - 写口 idx 与工程量原样下传,执行侧返回码直通(0/-1 两态,页面按 ERR 显示) + * - pending 无状态直读:本层每问一次转发一次(calls 增长)、桩改什么口径立刻 + * 变什么口径——页面锁存的合法性建立在"本层不缓存"之上 + * - 越界 idx 由执行侧判(桩返 -1 直通),本层不抄一份行数守卫 + */ +static void tc_yt_forwarding(void) +{ + ui_data_yt_row_t r; + + (void)ui_data_yt_bind(&s_yt_stub); + s_yt_db_calls = 0u; s_yt_cal_calls = 0u; s_yt_pending_calls = 0; + s_yt_db_set_rc = 0; s_yt_cal_set_rc = 0; + + CHECK(ui_data_yt_db_row(2u, &r) == 0, "TC-UIDATA-028a db_row forwarded"); + CHECK((r.val == (0.125f + 2.0f)) && (r.lo == 0.0f) && (r.hi == 50.0f) && + (r.unit == UI_DATA_YT_UNIT_V) && (r.decimals == 3u), + "TC-UIDATA-028b row fields pass through verbatim (no second source of truth)"); + CHECK(ui_data_yt_db_row(UI_DATA_YT_DB_N, &r) == -1, "TC-UIDATA-028c executor-side oob idx propagates"); + + CHECK((ui_data_yt_db_set(3u, 0.25f) == 0), "TC-UIDATA-028d db_set accepted"); + CHECK((s_yt_db_calls == 1u) && (s_yt_db_last_idx == 3u) && (s_yt_db_last_val == 0.25f), + "TC-UIDATA-028e executor got exact idx and engineering value"); + s_yt_db_set_rc = -1; + CHECK(ui_data_yt_db_set(3u, 0.25f) == -1, "TC-UIDATA-028f executor rejection propagates"); + + CHECK((ui_data_yt_cal_n() == UI_DATA_YT_CH_N) && (ui_data_yt_cal_row(7u, &r) == 0) && + (r.unit == UI_DATA_YT_UNIT_UV) && (r.hi == 1000000000.0f) && (r.decimals == 0u), + "TC-UIDATA-028g cal row forwards (coarse-guard hi verbatim)"); + s_yt_cal_set_rc = 0; + CHECK(ui_data_yt_cal_k_set(7u, 153.0f) == 0, "TC-UIDATA-028h cal_k_set accepted"); + CHECK((s_yt_cal_calls == 1u) && (s_yt_cal_last_idx == 7u) && (s_yt_cal_last_uv == 153.0f), + "TC-UIDATA-028i cal executor got exact idx and uv value"); + s_yt_cal_set_rc = -1; + CHECK(ui_data_yt_cal_k_set(7u, 0.0f) == -1, "TC-UIDATA-028j cal rejection propagates (k<=0 leg)"); + + s_yt_blocked = 1u; + CHECK(ui_data_yt_cal_blocked() == 1u, "TC-UIDATA-028k blocked badge forwarded"); + s_yt_blocked = 0u; + CHECK(ui_data_yt_cal_blocked() == 0u, "TC-UIDATA-028l badge clear follows executor immediately"); + + s_yt_pending_ret = 1u; + CHECK(ui_data_yt_pending() == 1u, "TC-UIDATA-028m pending=dirty forwarded (WAIT leg)"); + s_yt_pending_ret = 0u; + CHECK(ui_data_yt_pending() == 0u, "TC-UIDATA-028n pending follows to clean (OK leg, zero cache)"); + CHECK(s_yt_pending_calls == 2, "TC-UIDATA-028o every question reaches executor (stateless layer)"); + + (void)ui_data_yt_bind(NULL); + CHECK(ui_data_yt_pending() == 0u, "TC-UIDATA-028p unbind restores safe defaults"); + /* 未注入不得把问题漏给桩:计数不涨 = 撤销后桩彻底脱离(028o 只数了绑定期的 + * 两次,这条把"撤销后一次都不到"补成闭环) */ + CHECK(s_yt_pending_calls == 2, "TC-UIDATA-028q unbind stops reaching executor entirely"); +} + /* ===================== main ===================== */ int main(void) @@ -1351,6 +1534,10 @@ int main(void) tc_maint_not_bound(); printf("-- maint channel: forwarding --\n"); tc_maint_forwarding(); + printf("-- yt channel: not bound --\n"); + tc_yt_not_bound(); + printf("-- yt channel: forwarding --\n"); + tc_yt_forwarding(); printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index e30ba7b3..155f1a28 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -34,8 +34,8 @@ 控制字 #page=view_word 软压板 #page=view_plate 三遥设置 - 遥测死区 #P - 遥测系数 #P + 遥测死区 #P #page=deadband + 遥测系数 #P #page=calib 遥信类型 #P 遥信防抖 #P 双点遥信 #P