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),
已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
This commit is contained in:
2026-10-26 15:51:12 +08:00
parent 0f7b5a87fb
commit 77779d49d6
55 changed files with 4236 additions and 289 deletions
+402 -31
View File
@@ -1,12 +1,20 @@
/*
* 模块名称:UI 定值查看视图模型(ui_w_setview)
* 模块功能:参数 / 定值 / 控制字 / 软压板四张视图表与它们的取数、换算、越界反显——
* 供四个查看页薄跳板共用;声明与分层理由见 ui_w_setview.h
* 四个查看页与四个整定页**共用同一批表**(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>
@@ -113,6 +121,21 @@ static const uint8_t s_dec[UI_DATA_SET_N] = {
/* 无参数帧时值列的占位串(与全仓"读不到就 --"同一口径) */
#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; /* 与任何合法发布序号都不同 */
@@ -122,6 +145,22 @@ static uint8_t s_par_ok;
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])))
@@ -162,6 +201,44 @@ static int32_t sv_q10_to_scaled(int32_t a_q10, uint8_t a_dec) {
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
@@ -191,6 +268,144 @@ static void sv_set_unused(uint8_t a_id, int32_t a_val) {
(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
@@ -205,7 +420,7 @@ 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)) {
if (s_cur_rows == NULL) {
return false;
}
if (a_row >= s_cur_n) {
@@ -216,9 +431,74 @@ static bool sv_bad(uint16_t a_row) {
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 前调用)
* 入口参数:无
@@ -348,34 +628,125 @@ void ui_w_setview_attach(ui_w_setlist_t *a_inst, ui_w_setview_t a_view) {
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_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);
}