Files
wandering1 77779d49d6 feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐
UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机
OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批
预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block
(口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng
反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定
内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按
is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。

记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在
defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE);
事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身
(一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。
为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核,
丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。

为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费
两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过;
RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同
码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。

实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B;
RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS,
protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→
164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34,
既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、
不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页
连跑两次逐字节一致;字库 diff 空。

顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽
16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前
靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值
全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py
模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。

未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行;
LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
2026-10-26 15:51:12 +08:00

753 lines
40 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_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 <string.h>
#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);
}