Files
STM32F4-Base/Drivers/BSP/UI/ui_w_setlist.c
T
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

364 lines
14 KiB
C
Raw 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_setlist)实现
* 模块功能:见 ui_w_setlist.h 头注释(四视图共用列表渲染 + 整定输入抽象,
* 数据经回调注入,数值行走 ui_w_editor,二态行 ENTER 循环切换)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道B task-5 三组件之一)
* v1.1 2026-10-26 DTU 项目组 道F task-2:① 保存行新增 LOCK 词 + 闭锁态
* ENTER 不消费(禁编辑);② 二态行 ENTER **取模回绕**
* (原实现落在最后一项时写出 opts 之外的下标:屏上
* 看着没变、叠层已是越界值,保存必被预检挡回;
* task-2 是本页第一个消费者,此前无人按过这条键);
* 查看视图(commit/state 未绑)路径逐字未变
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_w_setlist.h"
#include "ui_tokens.h"
/* 私有宏定义区 */
#define SL_SAVE_NAME "保存" /* 保存行名(字体子集已含 保/存) */
#define SL_WIN_ROWS UI_LIST_ROWS /* 窗口行数(与框架 rows_per_page 同令牌) */
/* 函数定义区 */
/*
* 函数功能:判断两区域相交(与本目录各绘制文件同实现:static 局部)
* 入口参数:a - 区域 1 const lv_area_t *
* b - 区域 2 const lv_area_t *
* 返回值:true = 相交 bool
* 限定条件:无
*/
static bool sl_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);
}
/*
* 函数功能:整定视图判定(挂了提交回调 = 有保存行、可编辑)
* 入口参数:inst - 定值表实例 const ui_w_setlist_t *
* 返回值:true = 整定视图 bool
* 限定条件:inst 非空由调用链保证
*/
static bool sl_is_rw(const ui_w_setlist_t *a_inst) {
return (a_inst->commit != NULL);
}
/*
* 函数功能:二态行串表长度(NULL 结尾)
* 入口参数:opts - 串表 const char *const *
* 返回值:条目数(>=1;空表防御返回 1) uint8_t
* 限定条件:无
*/
static uint8_t sl_opts_count(const char *const *a_opts) {
uint8_t n = 0u;
if (a_opts == NULL) {
return 1u;
}
while (a_opts[n] != NULL) {
n++;
}
return (n > 0u) ? n : 1u;
}
/*
* 函数功能:当前页窗口顶行下标(与框架 cp_draw_rows / cp_draw_footer 同式)
* 入口参数:inst - 定值表实例 const ui_w_setlist_t *
* 返回值:窗口顶行(全局下标) uint16_t
* 限定条件:编辑中行恒在选中行所在页(编辑态 ↑↓ 被编辑器消费,选中不移)
*/
static uint16_t sl_window_first(const ui_w_setlist_t *a_inst) {
uint16_t sel = ui_content_page_selected_row();
(void)a_inst;
return (uint16_t)((sel / SL_WIN_ROWS) * SL_WIN_ROWS);
}
/*
* 函数功能:行槽顶 y(窗口内)
* 入口参数:inst - 定值表实例 const ui_w_setlist_t *
* row - 全局行下标 uint16_t
* 返回值:行槽顶 y int32_t
* 限定条件:row 在当前窗口内(绘制侧由 bad/ed 行可见性决定,越窗行不绘)
*/
static int32_t sl_slot_y(const ui_w_setlist_t *a_inst, uint16_t a_row) {
return UI_LIST_TOP + ((int32_t)a_row - (int32_t)sl_window_first(a_inst)) * UI_ROW_PITCH;
}
/*
* 函数功能:开页复位(清编辑态)
*/
void ui_w_setlist_reset(ui_w_setlist_t *a_inst) {
ui_w_editor_reset(&a_inst->ed);
a_inst->ed_row = 0u;
}
/*
* 函数功能:总行数(数据行 + 保存行[整定视图])
*/
uint16_t ui_w_setlist_count(const ui_w_setlist_t *a_inst) {
if (a_inst->rows == NULL) {
return 0u;
}
return (uint16_t)(a_inst->count + (sl_is_rw(a_inst) ? 1u : 0u));
}
/*
* 函数功能:逐行取名/值(框架 refresh 转发)
*/
void ui_w_setlist_refresh(ui_w_setlist_t *a_inst, uint16_t a_row,
char *a_name, char *a_value) {
uint16_t total = ui_w_setlist_count(a_inst);
a_name[0] = '\0';
a_value[0] = '\0';
if ((a_inst->rows == NULL) || (a_row >= total)) {
return; /* 防御:空表/行越界(动态收缩瞬间) */
}
if (a_row < a_inst->count) {
const ui_w_setlist_row_t *r = &a_inst->rows[a_row];
(void)lv_snprintf(a_name, UI_BUF_STRLEN, "%s", r->name);
if (r->kind == UI_W_SETLIST_KIND_ENUM) {
int32_t idx = a_inst->get(r->id);
uint8_t n = sl_opts_count(r->opts);
if (idx < 0) {
idx = 0;
}
if (idx >= (int32_t)n) {
idx = (int32_t)n - 1;
}
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", r->opts[idx]);
return;
}
if ((a_inst->ed.editing != 0u) && (a_inst->ed_row == a_row)) {
ui_w_editor_format(&a_inst->ed, a_value, UI_BUF_STRLEN); /* 编辑实时串 */
}
else {
/* 单位后缀(r->unit NULL = 不挂):先格式化到临时串再合成,一次界内写回,
* 不用 strcat 类函数(代码规范 7.5;且长度不足时会静默截半,屏幕上更难查) */
char fmt_buf[UI_BUF_STRLEN];
ui_w_editor_fmt_scaled(a_inst->get(r->id), r->decimals, fmt_buf, UI_BUF_STRLEN);
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s%s", fmt_buf,
(r->unit != NULL) ? r->unit : "");
}
return;
}
/* 保存行(整定视图最后一行;名"保存",值随交接态——net_cfg 保存行先例) */
(void)lv_snprintf(a_name, UI_BUF_STRLEN, "%s", SL_SAVE_NAME);
{
ui_w_setlist_st_t st = (a_inst->state != NULL) ? a_inst->state()
: UI_W_SETLIST_ST_IDLE;
switch (st) {
case UI_W_SETLIST_ST_WAIT:
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "WAIT");
break;
case UI_W_SETLIST_ST_OK:
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "OK");
break;
case UI_W_SETLIST_ST_FAIL:
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "ERR");
break;
case UI_W_SETLIST_ST_LOCK:
/* 定值闭锁:写不进去这件事必须写在保存行上(页脚/徽标都不算,
* 用户按保存之前看到的就是这一格);词用 ASCII 与 ENTER/WAIT 同排 */
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "LOCK");
break;
default:
(void)lv_snprintf(a_value, UI_BUF_STRLEN, "%s", "ENTER");
break;
}
}
}
/*
* 函数功能:按键处理(框架 key_cb 转发)
*/
uint8_t ui_w_setlist_key(ui_w_setlist_t *a_inst, lv_key_t a_key) {
uint16_t sel = ui_content_page_selected_row();
if (a_inst->rows == NULL) {
return 0u;
}
/* 编辑态:键先交编辑器 */
if (a_inst->ed.editing != 0u) {
if ((sel != a_inst->ed_row) || (sel >= a_inst->count)) {
/* 防御:编辑中选中被外部改动(正常不会——编辑态 ↑↓ 被消费)→ 弃编辑 */
ui_w_editor_reset(&a_inst->ed);
return 0u;
}
if (ui_w_editor_key(&a_inst->ed, a_key) == 0u) {
return 0u; /* 未消费键(本组件编辑态只接方向/ENTER) */
}
if (a_inst->ed.editing == 0u) {
/* ENTER 提交成功:值写 RAM 工作副本,落盘统一走保存行整表提交 */
a_inst->set(a_inst->rows[sel].id, ui_w_editor_value(&a_inst->ed));
}
ui_content_page_invalidate();
return 1u;
}
if (a_key == LV_KEY_ENTER) {
if (ui_content_page_is_editable() == false) {
return 0u; /* 只读权限 ENTER 一律忽略(框架内置也无 ENTER 语义) */
}
/* 定值闭锁(state 回调回 LOCK)→ ENTER 一律不消费:不进数值编辑、不切二态、
* 不投保存。权限门(上一条)说的是"这人不能写",这一条说的是"这台装置现在
* 写不进"——两件事都必须落在键路上,只靠屏显词会让用户白改一轮。
* ⚠ state() 回调可能带收口副作用(视图模型用它锁存落定结果),这里调它
* 是幂等的:同一次落定只会被消费一次(见 ui_w_setview sv_state 的 s_pending)
* ⚠ 已在编辑中的那一格不强行走这条路(编辑分支在上面已返回),其值只落在
* RAM 工作副本,投递口已被本门堵住,落不了盘 */
if ((a_inst->state != NULL) && (a_inst->state() == UI_W_SETLIST_ST_LOCK)) {
return 0u;
}
if (sel < a_inst->count) {
const ui_w_setlist_row_t *r = &a_inst->rows[sel];
if (r->kind == UI_W_SETLIST_KIND_NUM) {
ui_w_editor_begin(&a_inst->ed, a_inst->get(r->id),
r->min, r->max, r->decimals);
a_inst->ed_row = sel;
ui_content_page_invalidate();
return 1u;
}
/* 二态行:循环切换(控制字 告警<->跳闸、软压板 投入<->退出)
* ⚠ **必须取模回绕**(道F task-2 实测坑):原写法 nxt = cur + 1 不回绕,
* 落在最后一项时写出 opts 之外的下标 n——显示侧按 [0, n-1] 钳位,
* 于是"按 ENTER 屏幕上什么也没变",而叠层里存的已是越界下标,
* 随保存投出去就被 App 侧整批预检判成 ERR_RANGE:用户按了两下切换,
* 结果是整页保存被"越界"挡回。本组件在 task-2 之前没有整定页消费者,
* 这条只有接上写路径才会暴露(二态行 = 控制字 4 行 + 软压板 10 行,
* 初始值全为"跳闸/投入"= 最后一项,所以是**必现**而不是边角)
*/
{
int32_t cur = a_inst->get(r->id);
uint8_t n = sl_opts_count(r->opts);
int32_t nxt;
if ((cur < 0) || (cur >= (int32_t)n)) {
nxt = 0; /* 值区里是 opts 之外的脏值:回指第一项,不把脏值往下传 */
} else {
nxt = (int32_t)(((uint32_t)cur + 1u) % (uint32_t)n);
}
a_inst->set(r->id, nxt);
ui_content_page_invalidate();
return 1u;
}
}
if ((sel == a_inst->count) && sl_is_rw(a_inst)) {
/* 保存行(数据行之后的最后一行):整表提交投递(一次封存一次校验和,
* 显示由 state 回读) */
a_inst->commit();
ui_content_page_invalidate();
return 1u;
}
}
return 0u; /* 其余键交回框架:↑↓ 移选、LEFT 关页(编辑态才在上面消费) */
}
/*
* 函数功能:反显一个名区(保存失败越界行:白底黑字覆盖名文本区)
* 入口参数:inst - 定值表实例 const ui_w_setlist_t *
* layer - 绘制层 lv_layer_t *
* clip - 重绘区 const lv_area_t *
* row - 数据行下标 uint16_t
* 返回值:无
* 限定条件:行在当前窗口内(调用方已筛);编辑行不重复反显(选中带已反)
*/
static void sl_invert_name(const ui_w_setlist_t *a_inst, lv_layer_t *a_layer,
const lv_area_t *a_clip, uint16_t a_row) {
lv_area_t box;
int32_t top = sl_slot_y(a_inst, a_row);
box.x1 = UI_SEL_BAND_INSET_L;
box.x2 = UI_LIST_RIGHT_X - 3; /* 名区止于值列前留 3px 缝 */
box.y1 = top;
box.y2 = top + UI_ROW_BAND_H - 1;
if (!sl_areas_intersect(&box, a_clip)) {
return;
}
{
lv_draw_rect_dsc_t fd;
lv_draw_label_dsc_t ld;
char name[UI_BUF_STRLEN];
lv_draw_rect_dsc_init(&fd);
fd.base.layer = a_layer;
fd.bg_color = UI_COL_SEL_BG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(a_layer, &fd, &box);
(void)lv_snprintf(name, sizeof(name), "%s", a_inst->rows[a_row].name);
lv_draw_label_dsc_init(&ld);
ld.base.layer = a_layer;
ld.text = name;
ld.text_static = 0u; /* 栈缓冲(同帧有效) */
ld.color = UI_COL_SEL_FG;
ld.ofs_x = UI_LIST_LEFT_X - UI_SEL_BAND_INSET_L; /* ofs 相对 box.x1 */
ld.ofs_y = 0;
lv_draw_label(a_layer, &ld, &box);
}
}
/*
* 函数功能:自绘(编辑光标反显 + 越界行反显;框架内置区块之后)
*/
void ui_w_setlist_draw(ui_w_setlist_t *a_inst, lv_layer_t *a_layer,
const lv_area_t *a_clip) {
uint16_t first;
if (a_inst->rows == NULL) {
return;
}
first = sl_window_first(a_inst);
/* 1. 保存失败越界行反显(编辑行跳过:选中带已反,避免双重叠绘歧义) */
if (a_inst->bad != NULL) {
uint16_t total = ui_w_setlist_count(a_inst);
for (uint16_t r = first; (r < a_inst->count) && (r < total); r++) {
if ((r == a_inst->ed_row) && (a_inst->ed.editing != 0u)) {
continue;
}
if (r == ui_content_page_selected_row()) {
continue;
}
if (a_inst->bad(r)) {
sl_invert_name(a_inst, a_layer, a_clip, r);
}
}
}
/* 2. 数值编辑光标(含"越界"提示;几何交编辑器按传入行槽绘制) */
if (a_inst->ed.editing != 0u) {
if (a_inst->ed_row >= first) {
ui_w_editor_draw(&a_inst->ed, a_layer, a_clip,
sl_slot_y(a_inst, a_inst->ed_row),
0, UI_ROW_BAND_H, UI_LIST_RIGHT_X);
}
}
}