/* * 模块名称:UI 定值查看视图模型(ui_w_setview) * 模块功能:参数 / 定值 / 控制字 / 软压板四张视图表与它们的取数、换算、越界反显—— * 四个查看页与四个整定页**共用同一批表**(attach = 只读,attach_edit = * 可写:另绑 set/commit/state + 编辑叠层);声明与分层理由见 ui_w_setview.h * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) * 作者:王建锋 * 创建日期:2026-10-25 * 修改记录: * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3) * v1.1 2026-10-26 DTU 项目组 道F task-2:整定侧落地——编辑叠层(改动位图 + * 工作值,只提交改动 id)、sv_commit 经 * ui_data_param_request() 投递、sv_state 锁存落定 * 并映射 LOCK、bad 回调加"本次提交挡下项"第二来源、 * 统一按键门 ui_w_setview_key()、标题"未标定"徽标; * 新增 .bss ≈190 B(叠层 160 B + 位图/态), * 查看路径与四张行表逐字未变 */ /* 头文件包含区 */ #include #include "ui_w_setview.h" #include "ui_data.h" #include "ui_tokens.h" /* UI_BUF_STRLEN:框架行缓冲长度(无帧占位串要写进同一缓冲) */ /* 静态变量定义区 */ /* 二态行的串表(下标 = 记录值;0/1 的语义来自 App/algo/prot_set.h 的 meta 注释: * *_TRIP 0 = 告警 / 1 = 跳闸,*_EN 0 = 退出 / 1 = 投入;串表以 NULL 结尾, * 组件按串表长度决定 ENTER 循环切换的范围) */ const char *const s_opts_trip[3] = { "告警", "跳闸", NULL }; const char *const s_opts_plate[3] = { "退出", "投入", NULL }; /* * 四张视图表(Flash const)。行表的三项口径全部有真源,不在本文件里自由发挥: * · id = App/algo/prot_set.h 的 prot_set_id_t 枚举序(BSP 不 include App 头, * 故写裸数字并把枚举名注在行尾;对不上由主机单测与像素值两处暴露) * · min/max = App/algo/prot_set.c 的 g_prot_set_meta 上下限(Q10)换算到本行 * decimals 后的整数——**照抄换算结果,不自己拟定**;整定页复用同一批表, * 界内外一处一份,不会出现"查看看着合法、整定说越界"的两套边界 * · 显示名/小数位/单位 = docs/UI设计/页面扩充详细方案.md §3.2.2~§3.2.5 四张表。 * 其中四个返回系数按 **2 位小数** 显示:这不是我偏离文档,而是文档原口径(3 位) * 本身错了——它把 Q10 当成了十进制存储。Q10 存不下 0.95:0.95×1024 截成 972,真存 * 的是 0.94921875;按 3 位显示等于打出一位根本不存在的"9",整定单上写 0.95 的人看 * 到 0.949 会怀疑装置、甚至反复重输(重输也还是 0.949)。2 位才是这份存储在自己 * 可分辨精度上的正确读数。§3.2.2 已同步修正(表里 3 位改 2 位并加 ⚠ 指向本注释) * 单位挂在值串尾(A/V/ms/Hz 全 ASCII,不占中文字库):值列只有数字会让"60000"既是 * ms 又是 A,读者只能靠猜;行表带 unit 字段是本次给组件加的(NULL = 不挂) */ /* 参数视图(8 行 = 1 页) */ static const ui_w_setlist_row_t s_rows_param[8] = { { "PT一次", 27u, UI_W_SETLIST_KIND_NUM, 0u, 100, 500000, NULL, "V" }, /* PS_PT_PRI */ { "PT二次", 28u, UI_W_SETLIST_KIND_NUM, 0u, 1, 200, NULL, "V" }, /* PS_PT_SEC */ { "CT一次", 29u, UI_W_SETLIST_KIND_NUM, 0u, 1, 10000, NULL, "A" }, /* PS_CT_PRI */ { "CT二次", 30u, UI_W_SETLIST_KIND_NUM, 0u, 1, 10, NULL, "A" }, /* PS_CT_SEC */ { "过量返回", 25u, UI_W_SETLIST_KIND_NUM, 2u, 50, 100, NULL, NULL }, /* PS_RET_OVER */ { "欠量返回", 26u, UI_W_SETLIST_KIND_NUM, 2u, 100, 150, NULL, NULL }, /* PS_RET_UNDER */ { "过频返回", 38u, UI_W_SETLIST_KIND_NUM, 2u, 90, 100, NULL, NULL }, /* PS_RET_OF */ { "低频返回", 39u, UI_W_SETLIST_KIND_NUM, 2u, 100, 110, NULL, NULL }, /* PS_RET_UF */ }; /* 定值视图(18 行 = 3 页) */ static const ui_w_setlist_row_t s_rows_set[18] = { { "Ⅰ段电流", 2u, UI_W_SETLIST_KIND_NUM, 2u, 0, 10000, NULL, "A" }, /* PS_OC1_CUR */ { "Ⅰ段延时", 3u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OC1_DLY */ { "Ⅱ段电流", 6u, UI_W_SETLIST_KIND_NUM, 2u, 0, 10000, NULL, "A" }, /* PS_OC2_CUR */ { "Ⅱ段延时", 7u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OC2_DLY */ { "Ⅲ段电流", 10u, UI_W_SETLIST_KIND_NUM, 2u, 0, 10000, NULL, "A" }, /* PS_OC3_CUR */ { "Ⅲ段延时", 11u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OC3_DLY */ { "过负荷电流", 14u, UI_W_SETLIST_KIND_NUM, 2u, 0, 10000, NULL, "A" }, /* PS_OL_CUR */ { "过负荷延时", 15u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OL_DLY */ { "过压定值", 17u, UI_W_SETLIST_KIND_NUM, 1u, 0, 2000, NULL, "V" }, /* PS_OV_VOL */ { "过压延时", 18u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OV_DLY */ { "低压定值", 20u, UI_W_SETLIST_KIND_NUM, 1u, 0, 2000, NULL, "V" }, /* PS_UV_VOL */ { "低压延时", 21u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_UV_DLY */ { "零序定值", 23u, UI_W_SETLIST_KIND_NUM, 1u, 0, 2000, NULL, "V" }, /* PS_3U0_VOL */ { "零序延时", 24u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_3U0_DLY */ { "过频定值", 33u, UI_W_SETLIST_KIND_NUM, 2u, 5000, 5500, NULL, "Hz" }, /* PS_OF_HZ */ { "过频延时", 34u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_OF_DLY */ { "低频定值", 36u, UI_W_SETLIST_KIND_NUM, 2u, 4500, 5000, NULL, "Hz" }, /* PS_UF_HZ */ { "低频延时", 37u, UI_W_SETLIST_KIND_NUM, 0u, 0, 60000, NULL, "ms" }, /* PS_UF_DLY */ }; /* 控制字视图(4 行 = 1 页) */ static const ui_w_setlist_row_t s_rows_word[4] = { { "Ⅰ段跳闸", 1u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_trip, NULL }, /* PS_OC1_TRIP */ { "Ⅱ段跳闸", 5u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_trip, NULL }, /* PS_OC2_TRIP */ { "Ⅲ段跳闸", 9u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_trip, NULL }, /* PS_OC3_TRIP */ { "过负荷跳闸", 13u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_trip, NULL }, /* PS_OL_TRIP */ }; /* 软压板视图(10 行 = 2 页) */ static const ui_w_setlist_row_t s_rows_plate[10] = { { "Ⅰ段投入", 0u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OC1_EN */ { "Ⅱ段投入", 4u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OC2_EN */ { "Ⅲ段投入", 8u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OC3_EN */ { "过负荷投入", 12u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OL_EN */ { "过压投入", 16u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OV_EN */ { "低压投入", 19u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_UV_EN */ { "零序投入", 22u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_3U0_EN */ { "过频投入", 32u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_OF_EN */ { "低频投入", 35u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_UF_EN */ { "保护总投入", 31u, UI_W_SETLIST_KIND_ENUM, 0u, 0, 0, s_opts_plate, NULL }, /* PS_GLOBAL_EN */ }; /* * 每个定值项的显示小数位(下标 = 枚举序;二态项为 0,不参与数值换算) * 为什么要单独一张表而不是从行表里找:组件的 get(id) 回调只给 id 不给行号, * 同一 id 只会出现在一张视图里(四张表恰好覆盖 0..39 各一次,由主机单测钉住), * 所以这张表是无歧义的,且比遍历四张表便宜 */ static const uint8_t s_dec[UI_DATA_SET_N] = { 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 2, 0, 0, 2, 0, 2, 2, }; /* 无参数帧时值列的占位串(与全仓"读不到就 --"同一口径) */ #define SV_NO_DATA "--" /* 参数帧/落定结果轮询节拍(ms):见 sv_poll_cb 的"为什么必须有"。 * 100 ms 是"用户看不出延迟"与"每拍一次比较"的折中,比 rt 帧的 200 ms 更细一档, * 这样保存落定与主站写值最迟 100 ms 上屏 */ #define SV_POLL_MS 100u /* 轮询定时器(LVGL 池对象,首张定值页 attach 时建、只建不删;NULL 初始化是 * coding-standard CODE-RULE-15 对本层唯一文件作用域指针的要求) */ static lv_timer_t *s_poll_timer = NULL; /* 未标定徽标(整定页标题右侧):三字已在字库里(交流量页标定行、SOE type7 在用), * 故本次不新增字面字符;词与 par.cal_block 的屏上口径一致,别换成"标定失败" * 之类没登记过的说法(font-charset 与像素断言都按这个词钉) */ #define SV_CAL_BADGE "未标定" #define SV_BADGE_RIGHT_X 141 /* 文字右边界:让开"浅 V"右腿(y7~17 占 x≈147~158) */ /* 参数帧缓存(四个查看页共用一份:同屏只开一页,无并发读者) */ static ui_data_par_t s_par; static uint32_t s_par_seq = 0xFFFFFFFFu; /* 与任何合法发布序号都不同 */ static uint8_t s_par_ok; /* 当前视图的行表(bad 回调只收到行号,需要它把行号换回 id 再查位图) */ static const ui_w_setlist_row_t *s_cur_rows; static uint16_t s_cur_n; /* 当前视图行数(bad 回调的边界) */ /* * 整定视图状态(道F task-2) * ⚠ 与 s_cur_rows 同一假设:**同屏只开一页**,故这些状态是模块级单份而非按实例存 * (查看页四份缓存同一条理由)。将来要并排开两页,得连 s_cur_rows 一起收进实例 * * RAM 台账:s_wq 160 B + 三个位图 24 B + 态与标志 ≈ **190 B .bss** *(落 RW_IRAM1;口径 docs/内存管理/RAM使用分析与优化方案.md,回报里已单独列给 Lead) */ static int32_t s_wq[UI_DATA_SET_N]; /* 工作值(工程量 × 10^dec,下标 = id) */ static uint64_t s_wch; /* 改动位图:bit=1 → 该行本次要提交 */ static uint64_t s_commit_mask; /* 在飞请求覆盖的位(落定后按它清叠层) */ static uint64_t s_bad_mask; /* 本次提交被挡下的越界项(bad 回调第二个来源) */ static ui_w_setlist_st_t s_disp; /* 保存行显示态(锁存,见 sv_state 的注) */ static uint8_t s_pending; /* 1 = 已投递未落定 */ static uint8_t s_edit; /* 1 = 当前 attach 是整定视图(徽标/按键门都看它) */ /* 行表长度(编译期算,别手写数字——手写数字与表长不一致是最难查的错) */ #define SV_ROW_N(tbl) ((uint16_t)(sizeof(tbl) / sizeof((tbl)[0]))) /* 函数定义区 */ /* * 函数功能:Q10 原值 → 指定小数位的定点显示值(四舍五入) * 入口参数:a_q10 - Q10 原值(工程量 × 1024) int32_t * a_dec - 小数位 0..3 uint8_t * 返回值:工程量 × 10^a_dec int32_t * 限定条件:|a_q10| 在 int32 范围内且换算后仍在 int32 内(行表生成时已按 * g_prot_set_meta 的最大量程核过:最大项 PT 一次额定 500000 V,d=0) * 函数说明:不用浮点也不用 64 位除法——MicroLIB 的 soft-float 与 __aeabi_ldiv * 一进镜像就是几百字节,而模拟器(宿主 CRT)永远照不出这笔代价(本仓 * 反复记录的同一条口径)。做法是拆整数段与小数段: * v = ip×1024 + fp(fp∈[0,1023],负数用算术右移取 floor,fp 仍非负), * 小数段按 10^d 缩放后四舍五入,满 10^d 就进位回整数段。 * 误差上界半个最低显示位——与 prot_set 自己的 Q10 精度同源, * 屏上显示 10.00A 而实际 10.0004A 不是本页造成的,是存储精度 */ static int32_t sv_q10_to_scaled(int32_t a_q10, uint8_t a_dec) { static const int32_t p10[4] = { 1, 10, 100, 1000 }; int32_t ip; uint32_t fp; int32_t r; ip = a_q10 >> 10; fp = (uint32_t)a_q10 & 0x3FFu; r = ip * p10[a_dec]; if (a_dec != 0u) { uint32_t fr = (fp * (uint32_t)p10[a_dec] + 512u) >> 10; /* 作用域收在这里 */ if (fr >= (uint32_t)p10[a_dec]) { /* 小数段进位(如 0.9996 → 1.000) */ r += p10[a_dec]; fr -= (uint32_t)p10[a_dec]; } r += (int32_t)fr; } return r; } /* * 函数功能:显示值(工程量 × 10^dec)→ Q10 定点(整定提交方向,sv_q10_to_scaled 的逆) * 入口参数:a_scaled - 显示域整数值 int32_t * a_dec - 该行小数位 0..3 uint8_t * 返回值:Q10 定点值(工程量 × 1024) int32_t * 限定条件:a_dec <= 3;a_scaled 非负(组件编辑器契约 min>=0,本工程定值全非负) * 函数说明:q10 = (scaled×1024 + den/2) / den,四舍五入,**纯 32 位整数**—— * 不用 float、更不用 __aeabi_ldiv(64 位除),理由与 sv_q10_to_scaled 同一条 * (MicroLIB 代价,模拟器照不出来)。溢出界已核:最大显示值 PT一次额定 * 500000(dec=0)→ 500000×1024 = 512,000,000 < int32 满 * ⚠ 往返精度:这条换算与正向量程不是逐位可逆的(Q10 存 972 → 显示 95 → * 再写回 973,差 1 个 LSB)。所以**只提交改动位图内的 id**:用户没动的 * 行根本不进这个函数,也就不会被"顺手改掉"(docs/UI设计/UI_数据供给设计 * 说明.md §6.2 的 mask 语义;越界值不落盘这条判据也建在它上面) */ static int32_t sv_scaled_to_q10(int32_t a_scaled, uint8_t a_dec) { static const int32_t p10[4] = { 1, 10, 100, 1000 }; int32_t den = p10[a_dec]; return (a_scaled * 1024 + den / 2) / den; } /* * 函数功能:判断两区域相交(与本目录各绘制文件同实现:static 局部) * 入口参数:a - 区域 1 const lv_area_t * * b - 区域 2 const lv_area_t * * 返回值:true = 相交 bool * 限定条件:无 */ static bool sv_areas_intersect(const lv_area_t *a, const lv_area_t *b) { int32_t x1 = (a->x1 > b->x1) ? a->x1 : b->x1; int32_t y1 = (a->y1 > b->y1) ? a->y1 : b->y1; int32_t x2 = (a->x2 < b->x2) ? a->x2 : b->x2; int32_t y2 = (a->y2 < b->y2) ? a->y2 : b->y2; return (x1 <= x2 && y1 <= y2); } /* * 函数功能:读某定值项的显示值(组件 get 回调) * 入口参数:a_id - 行表登记的 id(= 枚举序) uint8_t * 返回值:工程量 × 10^decimals int32_t * 限定条件:ui_w_setview_sync() 已先跑过且 ui_w_setview_ready() 为真 * (页面在值列打 "--" 之前不会走到这里) * 函数说明:id 越界(>39)返回 0 属防御性死支——行表是本文件内 const,越界 id * 只可能来自代码改动错误,由主机单测在构建期前拦,不靠运行期兜 */ static int32_t sv_get(uint8_t a_id) { if (a_id >= (uint8_t)UI_DATA_SET_N) { return 0; } return sv_q10_to_scaled(s_par.q10[a_id], s_dec[a_id]); } /* * 函数功能:只读视图的写回调占位(组件契约要求非 NULL) * 入口参数:a_id/a_val - 未使用 * 返回值:无 * 限定条件:查看视图不可编辑,组件不会调到这里 * 函数说明:刻意空实现——查看页**没有任何**该写进 RAM 工作副本的语义; * 真要在查看页改值,得先把整定批次的 commit/state 一起接上 */ static void sv_set_unused(uint8_t a_id, int32_t a_val) { (void)a_id; (void)a_val; } /* * 函数功能:读某定值项的**编辑态**显示值(整定视图的 get 回调) * 入口参数:a_id - 行表登记的 id(= prot_set 枚举序) uint8_t * 返回值:改过 → 叠层工作值;未改 → 参数帧换算值 int32_t * 限定条件:整定视图(attach_edit)专用;帧未就位时由页面值列打 "--",走不到这里 * 函数说明:叠层优先——用户改到一半的数必须以叠层为准,否则每帧重绘都会把 * 未提交的编辑冲回帧值("改完光标一动就跳回原值"就是这种实现写反了); * 未改的行反过来一律跟随帧,主站 C_SE 写进来的新值 ≤200 ms 自己上屏 */ static int32_t sv_get_edit(uint8_t a_id) { if ((a_id < (uint8_t)UI_DATA_SET_N) && ((s_wch & ((uint64_t)1u << a_id)) != 0u)) { return s_wq[a_id]; } return sv_get(a_id); } /* * 函数功能:写某定值项的编辑叠层(整定视图的 set 回调;不落任何数据源) * 入口参数:a_id - 行 id uint8_t * a_val - 新显示值(工程量 × 10^dec) int32_t * 返回值:无 * 限定条件:整定视图专用;组件只在 ENTER 提交/二态切换时调 * 函数说明:只置位不改帧——真正的写入发生在保存行 ENTER → sv_commit → 快照层队列 * → App 侧 prot_set_set_sealed()。顺手清掉该行的"上次越界"反显: * 用户已经改过的行还挂着上一次失败的 redflag,是在说一件已经不成立的事 */ static void sv_set_edit(uint8_t a_id, int32_t a_val) { if (a_id >= (uint8_t)UI_DATA_SET_N) { return; } s_wq[a_id] = a_val; s_wch |= ((uint64_t)1u << a_id); s_bad_mask &= ~((uint64_t)1u << a_id); } /* * 函数功能:提交本次改动(整定视图的 commit 回调——只投递,不执行、不等待) * 入口参数:无 * 返回值:无(结果经 sv_state 回读,组件保存行显示) * 限定条件:uiTask 按键链内(保存行 ENTER);整定视图专用 * 函数说明:请求形状 = 改动位图 + 按位换算的 Q10(ui_data_param_req_t,168 B 栈帧)。 * 本函数**不**判断越界、不判闭锁、更不碰 FatFs:那三件事分别属于 * prot_set 的 meta、prot_set 的自检态与 defaultTask(契约 §6.1)。 * 队列满(-1)时叠层原样保留:用户直接再按一次保存即可,不必重输 */ static void sv_commit(void) { ui_data_param_req_t req; uint64_t bit = 1u; uint8_t i; int ret; (void)memset(&req, 0, sizeof(req)); req.mask = s_wch; for (i = 0u; i < (uint8_t)UI_DATA_SET_N; i++) { if ((s_wch & bit) != 0u) { req.q10[i] = sv_scaled_to_q10(s_wq[i], s_dec[i]); } bit <<= 1u; } s_bad_mask = 0u; /* 新一轮提交:上一次的越界认定作废 */ ret = ui_data_param_request(&req); if (ret == 0) { s_commit_mask = s_wch; s_pending = 1u; s_disp = UI_W_SETLIST_ST_WAIT; } else { s_pending = 0u; s_commit_mask = 0u; s_disp = UI_W_SETLIST_ST_FAIL; /* 忙/参数无效 → 页脚口径"忙,请重试" */ } } /* * 函数功能:回读保存行显示态(整定视图的 state 回调;顺带收口一次落定) * 入口参数:无 * 返回值:ui_w_setlist_st_t(IDLE/WAIT/OK/FAIL/LOCK) * 限定条件:uiTask(组件绘制保存行时调用;ui_w_setlist_key 的闭锁门也调它) * 函数说明:为什么要把落定"锁存"进 s_disp 而不是每次直读快照层状态—— * 快照层的 ui_data_param_state() 是**全局单值**,落定后一直停在那儿; * 直读会让用户移动一下光标(触发整页重绘)就看到别的页留下的旧结果, * 甚至把本次 * OK 显示成 ENTER。这里改成事件锁存:本页投过一次才显示那次的答案, * 开页 attach_edit 复位成 ENTER。 * 优先级(从高到低,逐条都有各自的"为什么"): * ① 帧里 locked=1 → LOCK:装置现在写不进去,比任何历史结果都该先说; * ② 在飞请求落定 → WAIT/OK/FAIL/LOCK(ERR_LOCKED 也映射成 LOCK: * 帧可能还没刷到 locked 位,但执行侧刚说过的"闭锁"是当下一手事实); * ③ 否则回显锁存值(ENTER/OK/ERR 等) */ static ui_w_setlist_st_t sv_state(void) { ui_w_setview_sync(); if ((s_par_ok != 0u) && (s_par.locked != 0u)) { return UI_W_SETLIST_ST_LOCK; } if (s_pending != 0u) { int p = ui_data_param_state(); if (p == UI_DATA_PARAM_ST_WAIT) { s_disp = UI_W_SETLIST_ST_WAIT; } else { s_pending = 0u; /* 一次落定只消费一次 */ if (p == UI_DATA_PARAM_ST_OK) { s_wch &= ~s_commit_mask; /* 这批已进值区:叠层让位给新帧 */ s_bad_mask = 0u; s_disp = UI_W_SETLIST_ST_OK; } else { s_bad_mask = (p == UI_DATA_PARAM_ST_ERR_RANGE) ? ui_data_param_err_mask() : 0u; s_disp = (p == UI_DATA_PARAM_ST_ERR_LOCKED) ? UI_W_SETLIST_ST_LOCK : UI_W_SETLIST_ST_FAIL; } s_commit_mask = 0u; } } return s_disp; } /* * 函数功能:复位整定侧状态(attach / attach_edit 开页时都调) * 入口参数:无 * 返回值:无 * 限定条件:uiTask(开页 attach 链内,先于组件 reset) * 函数说明:**每次开页都清稿**是刻意的:改动位图与工作值只在本页这一次打开里有意义, * 带进下一次打开会出现"没改任何东西却显示着上次的编辑值"(更糟的是下次 * 保存会把那份陈旧草稿投出去)。ESC 关页天然弃稿(不投递),与本函数 * 合起来正好是 net_cfg 那套"改到一半退出 = 什么都没发生"的语义 */ static void sv_view_reset(void) { (void)memset(s_wq, 0, sizeof(s_wq)); s_wch = 0u; s_commit_mask = 0u; s_bad_mask = 0u; s_disp = UI_W_SETLIST_ST_IDLE; s_pending = 0u; s_edit = 0u; } /* * 函数功能:某数据行是否越界(组件 bad 回调 → 反显高亮) * 入口参数:a_row - 数据行下标(不含保存行) uint16_t * 返回值:true = 反显;false = 正常 bool * 限定条件:uiTask 绘制链内;s_cur_rows 已由 attach 设定 * 函数说明:位图来自 prot_set_bad_mask()(上一次整表装载/提交后仍在量程外的项), * 经 par 帧 bad_mask + bad_valid 下发。bad_valid=0 表示位图无意义 * (App 侧只在位图非 0 时置 1),此时一律不反显——把"没数据"画成 * "这行有问题"是假告警,比不画更糟 */ static bool sv_bad(uint16_t a_row) { const ui_w_setlist_row_t *r; uint64_t bit; if (s_cur_rows == NULL) { return false; } if (a_row >= s_cur_n) { return false; /* 行号越界(组件不会给,防御:越界不反显也不越读) */ } r = &s_cur_rows[a_row]; if (r->id >= (uint8_t)UI_DATA_SET_N) { return false; } bit = (uint64_t)1u << r->id; /* * 两个来源,任一命中即反显(整定批次加了①,查看页②逐字不变): * ① s_bad_mask —— **本次提交**被 App 侧整批预检挡下的项(ui_data_param_err_mask() * 经 sv_state 收进来,只在 ERR_RANGE 那一轮非 0;用户再改该行、或重新提交 * 即清)。这是"越界行高亮"判据的屏上落点:整批一字未写,但必须说清是哪几行 * 把人挡住的,否则用户只能逐行猜。 * ② s_par.bad_mask —— prot_set 整表装载/解析留下的越界位图,且必须 bad_valid=1 * 才参与(bad_valid=0 时位图无意义,把"没数据"画成"这行有问题"是假告警) */ if ((s_bad_mask & bit) != 0u) { return true; } if ((s_par_ok == 0u) || (s_par.bad_valid == 0u)) { return false; } return (s_par.bad_mask & bit) != 0u; } /* * 函数功能:参数帧/落定结果轮询(LVGL 定时器,100 ms 一拍) * 入口参数:a_timer - 定时器句柄(未使用) lv_timer_t * * 返回值:无 * 限定条件:uiTask 上下文(LVGL 定时器在 lv_timer_handler 内跑,与页面同一线程) * 函数说明:**为什么必须有这条定时器**——LVGL 只重绘被 invalidate 的区域,而本页 * 的数据是**异步**变的: * ① 参数帧由 App 侧节拍发布(保存落定后校验和变、主站 C_SE 写进来、 * 标定完成让 cal_block 翻转),页面自己无从察觉; * ② 整定的落定结果更隐蔽:ERR_RANGE / ERR_LOCKED **不改校验和**, * 也就不会发布新帧——只有 ui_data_param_state() 变了。 * 没有这条轮询,用户按完"保存"屏上会一直停在 WAIT,越界行的反显也要 * 等下一次按键才出现(判据"越界行高亮"在真机上就落不了地)。 * 代价:一拍一次 uint32 比较 + 一次 bool 判断,无变化时零重绘、零拷贝; * 页面没打开时直接返回(菜单屏/待机屏一像素都不动)。 * 与 net_cfg 的差别:net_cfg 的保存行状态至今只在按键触发的重绘里更新 * (先例的既有局限),本页不能沿用——整定的 OK/ERR/LOCK 是用户判断 * "这台装置到底吃了我改的值没有"的唯一凭据 */ static void sv_poll_cb(lv_timer_t *a_timer) { (void)a_timer; if (ui_content_page_is_open() == false) { return; } if (ui_data_par_seq() != s_par_seq) { ui_content_page_invalidate(); /* 新参数帧:整页重绘(refresh 里同步序号) */ return; } if ((s_pending != 0u) && (ui_data_param_state() != UI_DATA_PARAM_ST_WAIT)) { ui_content_page_invalidate(); /* 落定但不改校验和(ERR_RANGE/ERR_LOCKED) */ } } /* * 函数功能:确保轮询定时器已建(首张定值相关页面 attach 时建一次,之后不再建) * 入口参数:无 * 返回值:无 * 限定条件:uiTask(页面 open 链内) * 函数说明:只建不删(同 ui_standby_init 的 sb_idle_timer_cb 先例:模块生命周期 = * UI 生命周期);回调自带"没开页就返回"的门,停在菜单屏时它只是一次比较。 * 建失败 = LVGL 池耗尽,此时页面退回"按键才重绘"的旧行为——不另外报错, * 因为那条错误在屏上没有任何可显示的通道(池耗尽本身早已是更大的问题) */ static void sv_poll_ensure(void) { if (s_poll_timer == NULL) { s_poll_timer = lv_timer_create(sv_poll_cb, SV_POLL_MS, NULL); } } /* * 函数功能:按发布序号取参数帧快照(接各查看页 refresh 前调用) * 入口参数:无 * 返回值:无 * 限定条件:uiTask 上下文(页面绘制链内);只读快照层,不写任何东西 * 函数说明:序号没变就一帧都不重拷(一帧约 184 B;四个查看页共用这份缓存——同屏 * 只可能开着一页,彼此之间不存在并发读者)。读取失败把序号打回不可能的值, * 下一轮一定重试,不会停在半新不旧的帧上 */ void ui_w_setview_sync(void) { uint32_t seq = ui_data_par_seq(); if ((seq == s_par_seq) && (s_par_ok != 0u)) { return; /* 序号未变:一帧都不重拷 */ } /* ui_data_par() 的通道是 0=成功 / -1=尚无首帧 / -2=参数无效(快照层的既有约定, * 不是布尔成败)——把它当 bool 用会反义:模拟器与真机上都会永远"读不到", * 于是整页值列恒 "--",而页面看起来完全正常(本函数第一版就踩了这一条, * 是看像素图才发现满屏横杠,静态检查看不出来) */ if (ui_data_par(&s_par) != 0) { s_par_ok = 0u; s_par_seq = 0xFFFFFFFFu; /* 失败态不认序号,下一轮一定重试 */ (void)memset(&s_par, 0, sizeof(s_par)); return; } s_par_seq = seq; s_par_ok = (s_par.valid != 0u) ? 1u : 0u; } /* * 函数功能:逐行取数(页面 refresh 的直直通:先同步参数帧,再走组件) * 入口参数:a_inst - 实例 ui_w_setlist_t *;a_row - 行下标 uint16_t * a_name/a_value - 框架行缓冲(UI_BUF_STRLEN) char * * 返回值:无 * 限定条件:框架绘制链内(uiTask) * 函数说明:**参数帧未就位时只把值列打成 "--",名字照打**——空值会被读成"这项是 0", * 整页空白会让人以为菜单坏了;打出"名字 + --"才说得出真正的事实 * ("这张表在等参数帧")。帧就位后走组件:数值行按小数位 + 单位, * 二态行打串表词 */ void ui_w_setview_refresh(ui_w_setlist_t *a_inst, uint16_t a_row, char *a_name, char *a_value) { ui_w_setview_sync(); if (ui_w_setview_ready() != 0u) { ui_w_setlist_refresh(a_inst, a_row, a_name, a_value); return; } a_name[0] = '\0'; a_value[0] = '\0'; if ((a_inst != NULL) && (a_inst->rows != NULL) && (a_row < a_inst->count)) { (void)lv_snprintf(a_name, UI_BUF_STRLEN, "%s", a_inst->rows[a_row].name); } (void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", SV_NO_DATA); } /* * 函数功能:参数帧是否已就位 * 入口参数:无 * 返回值:1 = 有有效帧可显示;0 = 尚无首帧或读取失败 uint8_t * 限定条件:uiTask 上下文;纯查询,无副作用 * 函数说明:返回 uint8_t 而不是 bool 是**本层约定**(bsp-err 的 err-type:BSP 公开 API * 不得返回 bool——bool 把"为什么失败"整条丢掉,而这里虽然确实不需要原因, * 也不能给整层开一个按名字豁免的例外;先例见 ui_w_setlist_key 的键分发标志)。 * 调用方拿它决定值列打换算值还是打 "--",没有错误要上报 */ uint8_t ui_w_setview_ready(void) { return s_par_ok; } /* * 函数功能:取某视图的行表与行数(结构自测用;页面不需要,attach 时才自己取) * 入口参数:a_view - 视图编号 ui_w_setview_t;a_n - 行数输出(不可为 NULL) uint16_t * * 返回值:行表首地址;视图号非法返回 NULL const ui_w_setlist_row_t * * 限定条件:只读;行表是 const Flash 表 * 函数说明:存在的理由是让主机单测能把四张表当成数据来核(id 是否恰好覆盖 0..39 * 各一次、二态行有没有串表、小数位与 dec 查表是否一致)。这些约束在 * 编译期看不出来,页面上也只有当前可见的 8 行会被像素断言看到—— * 第 2、3 页的行错了对齐,屏幕上要翻两页才看得见,而人不会去翻 */ const ui_w_setlist_row_t *ui_w_setview_table(ui_w_setview_t a_view, uint16_t *a_n) { if (a_n == NULL) { return NULL; } switch (a_view) { case UI_W_SETVIEW_PARAM: *a_n = SV_ROW_N(s_rows_param); return s_rows_param; case UI_W_SETVIEW_SET: *a_n = SV_ROW_N(s_rows_set); return s_rows_set; case UI_W_SETVIEW_WORD: *a_n = SV_ROW_N(s_rows_word); return s_rows_word; case UI_W_SETVIEW_PLATE: *a_n = SV_ROW_N(s_rows_plate); return s_rows_plate; default: *a_n = 0u; return NULL; } } /* * 函数功能:取 id → 显示小数位的查表(结构自测用) * 入口参数:a_n - 表长输出(= UI_DATA_SET_N) uint16_t * * 返回值:表首地址 const uint8_t * * 限定条件:只读 */ const uint8_t *ui_w_setview_dec_table(uint16_t *a_n) { if (a_n != NULL) { *a_n = (uint16_t)UI_DATA_SET_N; } return s_dec; } /* * 函数功能:把某个查看视图绑到定值表实例(行表 + 取数 + 反显一次配齐) * 入口参数:a_inst - 页面持有的实例(可为 NULL,NULL 直接返回) ui_w_setlist_t * * a_view - 视图编号 ui_w_setview_t * 返回值:无 * 限定条件:页面 init_cb 内调用(uiTask);每次开页都重新绑,不信任上一次的残留 * 函数说明:先 memset 再填 —— 组件实例里的编辑态跨页留着就是脏状态;视图号非法 * 给空表(页面退化成 0 行空页,比按错表导航安全) */ void ui_w_setview_attach(ui_w_setlist_t *a_inst, ui_w_setview_t a_view) { if (a_inst == NULL) { return; } (void)memset(a_inst, 0, sizeof(*a_inst)); /* 行表取自 ui_w_setview_table()(本模块唯一的行表出口):attach 与 attach_edit * 共用同一张表,才会出现"查看合法、整定也合法"——两处各自 switch 就是两份口径 */ a_inst->rows = ui_w_setview_table(a_view, &a_inst->count); s_cur_rows = a_inst->rows; s_cur_n = a_inst->count; a_inst->get = sv_get; a_inst->set = sv_set_unused; /* NULL = 查看视图:无保存行、禁编辑(§3.2.6);state 同 NULL → 保存行也没得显示 */ a_inst->commit = NULL; a_inst->state = NULL; a_inst->bad = sv_bad; /* 整定侧状态一律复位:查看页既不该看见上一次的改动位图,也不该在下次开整定页 * 时把它带过去(s_edit=0 同时关掉"未标定"徽标——查看页像素零变化的硬条件) */ sv_view_reset(); sv_poll_ensure(); /* 帧是异步变的:页面必须能自己发现新帧/落定(见 sv_poll_cb) */ ui_w_setlist_reset(a_inst); } /* * 函数功能:把某个**整定**视图绑到定值表实例(行表与 attach 同一批,另绑写回调) * 入口参数:a_inst - 页面持有的实例(NULL 直接返回) ui_w_setlist_t * * a_view - 视图编号 ui_w_setview_t * 返回值:无 * 限定条件:页面 init_cb 内、且 ui_content_page_is_editable() 为真时才调 * (只读权限进来调这个等于给只读用户开了保存行) * 函数说明:与 attach 的差别只有 get/set/commit/state 四个钩子 + s_edit 标志, * 行表、行数、bad 回调、越界反显全部复用——**同一张行表**是这一版 * 设计的关键:上下限的镜像只有一份,漂移由 setview-check 钩子拦 */ void ui_w_setview_attach_edit(ui_w_setlist_t *a_inst, ui_w_setview_t a_view) { if (a_inst == NULL) { return; } (void)memset(a_inst, 0, sizeof(*a_inst)); a_inst->rows = ui_w_setview_table(a_view, &a_inst->count); s_cur_rows = a_inst->rows; s_cur_n = a_inst->count; a_inst->get = sv_get_edit; a_inst->set = sv_set_edit; a_inst->commit = sv_commit; /* 非 NULL = 整定视图:组件画保存行并放开编辑 */ a_inst->state = sv_state; a_inst->bad = sv_bad; sv_view_reset(); s_edit = 1u; sv_poll_ensure(); /* 同 attach:整定页更要看到"落定"这一刻 */ ui_w_setlist_reset(a_inst); } /* * 函数功能:两态统一按键转发(页面 key_cb 一行转过来) * 入口参数:a_inst - 已 attach 的实例 ui_w_setlist_t * * a_key - LVGL 键值 lv_key_t * 返回值:1 - 本页已消费(框架跳过内置);0 - 交回框架(↑↓ 移选、← 关页) * (键分发标志非错误通道,取 uint8_t——BSP err-type 约定) * 限定条件:uiTask 按键链内(框架 key_cb;描述符须 edit_capable=1 + key_intercept=1) * 函数说明:两道门都在本函数里,四页共用一条规矩(写在页面里就会长出不一致的四个版本): * 1. 只读视图(commit==NULL)直接交回框架:查看页绑了 key_cb 也**没有 * 任何行为变化**——这是"绑 key_cb 不改像素"的依据,vparam~vplate * 四张黄金参考仍逐位一致就是它的机器证明 * 2. 参数帧未就位时不接受写交互:值列此刻是 "--",拿 "--" 进编辑器等于 * 把"还不知道"当成 0 让人改,改完还能投出去("无数据不许打 0"的同一条 * 诚实性纪律,见 ui_w_setview_refresh 的注释) * ⚠ 判"就位"之前必须先 ui_w_setview_sync():s_par_ok 原本**只在绘制路径** * 里被置位,而"画过没有"是 LVGL 的事、与按键不同步。模拟器无头回归 * 两次注键之间根本不重绘 → 开页后的第一个 ENTER 永远被这道门挡掉; * 真机上开页 10 ms 内会重绘,于是这条坑只在测试侧咬人("代码没错、 * 时序有错"最难查)。sync 在帧没变时只是一次 seq 比较,多调零代价 */ uint8_t ui_w_setview_key(ui_w_setlist_t *a_inst, lv_key_t a_key) { if ((a_inst == NULL) || (a_inst->commit == NULL)) { return 0u; } ui_w_setview_sync(); if (ui_w_setview_ready() == 0u) { return 0u; } return ui_w_setlist_key(a_inst, a_key); } /* * 函数功能:整定页标题右侧画"未标定"徽标(未标定闭锁的屏上表达) * 入口参数:a_layer - 绘制层 lv_layer_t * * a_clip - 重绘区 const lv_area_t * * 返回值:无 * 限定条件:页面 draw_cb 内(框架内置区块之后);三条件全满足才落墨:整定视图 + * 帧已就位 + par.cal_block 非 0 * 函数说明:几何为什么这样取:标题带只有 y=2..17 这一条,而"浅 V"右腿在 y7~17 * 占掉 x≈147..158(ui_deco.c 的点集),所以徽标右边界取 141、左边界按 * 实宽左推(3 个汉字 = 36 px → x≈105..141),既不压装饰也不压居中的标题 * (最长标题"软压板/控制字" 3 字到 x≈98 为止)。字色取 UI_COL_FG, * 与标题同一极性;不做反显——反显在 1bpp 下表示"选中",徽标不是可选中项 */ void ui_w_setview_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 = SV_CAL_BADGE; size_t len; if ((s_edit == 0u) || (s_par_ok == 0u) || (s_par.cal_block == 0u)) { return; } len = strlen(txt); area.x2 = UI_SCREEN_W - 1; area.y1 = UI_TITLE_Y; area.y2 = UI_TITLE_Y + UI_TEXT_BAND_H; /* 右对齐:先用与框架标题同一套测宽(lv_text_get_width + strlen, * 这里 length 必须传 strlen——该版本传 -1/0 要么死循环要么返回 0) */ area.x1 = SV_BADGE_RIGHT_X - (int32_t)lv_text_get_width(txt, (uint32_t)len, LV_FONT_DEFAULT, 0); if (!sv_areas_intersect(&area, a_clip)) { return; } lv_draw_label_dsc_init(&ld); ld.base.layer = a_layer; ld.text = txt; ld.text_static = 1u; /* Flash 字面量:不拷内存(与框架标题同法) */ ld.color = UI_COL_FG; ld.ofs_x = 0; ld.ofs_y = 0; lv_draw_label(a_layer, &ld, &area); }