feat(UI): 遥测死区与系数 2 页——注入表第四张、三态用脏位、WAIT 与 OK 各出一张黄金

这两页是「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 上下文」已被装配层裁定覆盖)归算法层下一笔。
This commit is contained in:
2026-10-27 05:07:18 +08:00
parent 1d39571114
commit 48a8ef0564
27 changed files with 1682 additions and 11 deletions
+374
View File
@@ -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 <string.h>
#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);
}