Files
wandering1 48a8ef0564 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 上下文」已被装配层裁定覆盖)归算法层下一笔。
2026-10-27 05:07:18 +08:00

375 lines
16 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI 遥测整定视图模型实现(ui_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);
}