/* * 模块名称: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); }