/* * 模块名称:UI 定值查看视图模型(ui_w_setview) * 模块功能:参数 / 定值 / 控制字 / 软压板四张视图表与它们的取数、换算、越界反显—— * 供四个查看页薄跳板共用;声明与分层理由见 ui_w_setview.h * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) * 作者:王建锋 * 创建日期:2026-10-25 * 修改记录: * v1.0 2026-10-25 王建锋 创建(道E task-15 批 3) */ /* 头文件包含区 */ #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 存不下 0.95:0.95×1024 截成 972,真存的是 0.94921875, * 按 3 位显示就打出一位根本不存在的"9",整定单上写 0.95 的人看到 0.949 会怀疑 * 装置、甚至反复重输(重输也还是 0.949)。2 位显示 0.95 才是这份存储在它自己可 * 分辨精度上的正确读数;文档的 3 位来自"把 Q10 当成十进制存储"这个误会 * 单位挂在值串尾(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 "--" /* 参数帧缓存(四个查看页共用一份:同屏只开一页,无并发读者) */ 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 回调的边界) */ /* 行表长度(编译期算,别手写数字——手写数字与表长不一致是最难查的错) */ #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; } /* * 函数功能:读某定值项的显示值(组件 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; } /* * 函数功能:某数据行是否越界(组件 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) || (s_par_ok == 0u) || (s_par.bad_valid == 0u)) { 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; return (s_par.bad_mask & bit) != 0u; } /* * 函数功能:按发布序号取参数帧快照(接各查看页 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)); switch (a_view) { case UI_W_SETVIEW_PARAM: a_inst->rows = s_rows_param; a_inst->count = SV_ROW_N(s_rows_param); break; case UI_W_SETVIEW_SET: a_inst->rows = s_rows_set; a_inst->count = SV_ROW_N(s_rows_set); break; case UI_W_SETVIEW_WORD: a_inst->rows = s_rows_word; a_inst->count = SV_ROW_N(s_rows_word); break; case UI_W_SETVIEW_PLATE: a_inst->rows = s_rows_plate; a_inst->count = SV_ROW_N(s_rows_plate); break; default: a_inst->rows = NULL; a_inst->count = 0u; break; } s_cur_rows = a_inst->rows; s_cur_n = a_inst->count; a_inst->get = sv_get; a_inst->set = sv_set_unused; a_inst->commit = NULL; /* NULL = 查看视图:无保存行、禁编辑(§3.2.6) */ a_inst->state = NULL; a_inst->bad = sv_bad; ui_w_setlist_reset(a_inst); }