Files
wandering1 dafc18f5ed feat(UI): W6 第一波——设计令牌层、页面注册表生成器与 UI 门禁补强
启动 docs/任务规划/README.md 的 W6 UI 扩展簇(用户裁定,覆盖 2026-10-11
的"本轮不排期")。四个工作包,全部验证通过。

## U1 设计令牌层(零行为变化)
新建 Drivers/BSP/UI/ui_tokens.h(20 个宏),框架 + 7 个页面 + App/task/
ui_task.c 全部接入。屏幕几何**复用 ui_port.h 既有 UI_SCREEN_W/H**,不重复
定义;令牌头自身只 include ui_port.h,不把 LVGL 拖进全部消费者。
补收了报告没列但代码里真重复的量:UI_TEXT_BAND_H 15(7 次)、列表左右边界
与每页行数(各 4 个页面逐字复制)、推流协议几何与显示几何同源(此前硬编码
3200 且注释写着"须与 UI_FB_SIZE 一致"——典型漂移隐患)、UI_TASK_TICK_MS
(ui_port_step 与 osDelay 必须同值,原先靠注释维系)。
故意不收页面私有几何与"同形不同义"的值(device_info 的 pitch=15 与列表
UI_ROW_PITCH=14 合并即改像素)。

## U3 页面注册表与生成器(删手工分发)
ui_page_registry.{c,h} 由生成器产出,**手工 menu_content_page_for() 已删
除**(仓库内仅存 2 处历史说明注释)。密码页按代码实况**单独出口**
(ui_page_registry_password())——它非节点绑定,任何 #P 叶子进同一页。
顺带落地此前判定"U1 阶段做不了"的共享 flat 样式 ui_style.{c,h}。
生成器除产注册表外**自动把新 .c 写进 uvprojx 与 CMakeLists.txt**,且
**双向对账**:加页打印 registered、删页打印 pruned,不留死条目;并双向
校验(#page 找不到实例报错、实例未被任何 #page 引用也报错)。

## U4 门禁补强(35 个钩子)
新增 font-charset 钩子(scripts/check_font_charset.py):复用
tools/font_gen 的同一套提取与 cmap 解析规则(importlib 直载,零第二套
规则),A 段查源码在用 vs 字符集、B 段查字符集 vs 字体 cmap。
registration-check 补第 ④ 类:实测原规则对"进了两端构建但没接描述符表"
的页面**全绿**——desc 被链接器剥离、编译链接全过、菜单永远打不开的死代码。
仍照不出的两项已如实写进文档(枚举漏加归 @build、菜单树漏挂归 golden)。

## 字体子集瘦身(D 案,4 留 3 删)
ui_cn_chars.txt 122→119 字(恰删 上/电/重,零新增)。异/常/片/芯 进
KEEP_EXTRA 带理由白名单:它们曾在 9eb1bd63 被写进中文自检描述落盘
/log/selftest.log(定长 64B 记录跨固件持久),删字形会使历史故障记录
渲染空白且不可逆。保留字按**必需**处理,反向有牙——谁只跑 extract_cn
把它们静默洗掉,钩子直接 FAIL 并逐字打印理由。
--strict 口径为"未知冗余 = 0"而非"冗余 = 0"。

## 验证
@build 0 错 0 警;ER_IROM1 232,052 B(较 HEAD −228 B)、RW_IRAM1
75,232 B **零增量**、RW_CCM 87.9% 未变。golden:菜单 8 状态全像素一致 +
内容页 11 页逐字节一致,**参考图 sha256 差异 0 条**(未用 --update 刷
基线);池水位菜单基线 5368 B、开页-关页往返 5448 B 与 U1 时相同。
38 个在途修改文件逐个核对 BOM/CRLF/U+FFFD 全部与 HEAD 一致。
L0 33 钩子 + L0.5 16 runner / 1104 断言全绿;check_doc_links --all
142 篇、无法判定 0;mem-budget / mem-numbers 已回填。

## 单位成本实测(U5 排期依据)
临时页 zzprobe 绑到"遥信量"叶子 → 只跑生成器 → 带页 @build 0/0 →
注册表出现 {5u, &ui_page_zzprobe_desc} → 回滚后 6 个产出文件 zzprobe
残留计数均 0、重编回基线。
**结论:新增一页 = 3 处人工(新 .c + 新 .h + 菜单树那一行加
 #page=<名>)+ 1 条生成器命令,手工编辑注册文件 0 处。**
据此修正报告原口径"8 处降到 1 处"——那个 1 只在"1 处对既有文件的修改"
意义下成立,页面自身 2 个文件本就要写,真实处数是 3。

## 文档同步与两处自我修正
§0.1 UI 行 B → B+;§6.1「内容页」「设计令牌」两行按实况回填;§6.5 U1/U3
状态更新;§6 顶部加"建议栏不可照抄"总警示(本轮四次证伪:ui_port.h 已有
同名宏、LV_COLOR_BLACK/WHITE 不存在且极性反了、27 页实为目标页数现存 7
个、生成器草图漏密码页非节点绑定)。
两处改口:① §6.5 U4 从我上一版**误写的 3/3 改回 2/3**——"golden 脚本早已
存在"不等于"已进自动门禁",两个 golden 脚本至今未挂 pre-commit,该缺口
单列为待办;② 原计划"分三笔提交"**作废**,U1 与 U3 改同一批文件、文件内
交叠,硬切会产生不可编译的中间态,故整波一笔。
2026-10-20 21:14:14 +08:00

371 lines
14 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 Page Net Cfg (网口设置内容页)
* 模块功能:网口设置叶子节点的内容页数据 + 行为——5 行地址列表(IP 地址/子网
* 掩码/默认网关/MAC 地址 + 保存行,5 行每页共 1 页);
* 行槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=4 / 值 x=54;
* 值取自 net_cfg RAM 副本(defaultTask 上电自 Flash 加载;
* netTask 回写实际 MAC 影子);
* 编辑模式:ENTER 进选中地址行(字节光标 = 活动字节,反显标记),
* ↑↓调字节 0..255(环绕、值列实时刷新)、←→移字节光标(首↔末环绕)、
* ENTER 确认退出编辑;
* 权限:edit_capable=1——查看密码 → 只读(编辑/保存键无效),
* 修改密码 → 可编辑;保存行 ENTER → 投递保存请求
* (net_cfg_save_request;属主任务 defaultTask 写盘并立即应用
* 重建网络,无需重启);保存行值随交接状态显示
* ENTER/WAIT/OK/ERR;
* 向框架 ui_content_page 提供页面描述符(零 LVGL 对象)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-09-10
* 修改记录:
* v1.0 2026-09-10 王建锋 创建初始版本
* v1.1 2026-09-15 王建锋 保存改异步交接:投递请求 → 属主任务写盘/应用;
* 保存行随交接状态显示 ENTER/WAIT/OK/ERR
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_page_net_cfg.h"
#include "net_cfg.h"
#include "ui_tokens.h"
/* 私有宏定义区 */
/*
* 布局取值:行骨架用 ui_tokens.h 的列表页令牌(UI_LIST_TOP / UI_ROW_PITCH /
* UI_ROW_BAND_H / UI_BUF_STRLEN),本文件只留本页私有的两列 x 与行数。
* 5 行共 1 页(行槽顶 y = 24+14i,末行止于 y=93,与页脚 y=143 无冲突);
* 名 x=4 / 值 x=54(值列须 ≥54:4 汉字名占 48px + 间隙;值列宽——
* IP/掩码/网关 11 ASCII 字符 = 66px 止于 119,MAC 17 字符(冒号分隔)= 102px 止于 155,
* 均在选中白带 x=2..156 内)
*/
#define NC_ROW_COUNT 5u /* 4 地址行 + 1 保存行 */
#define NC_ADDR_ROW_MAX 4u /* 地址行范围 0..3(IP/掩码/网关/MAC) */
#define NC_SAVE_ROW 4u /* 保存行下标 */
#define NC_LEFT_X 4 /* 左列 x(名) */
#define NC_RIGHT_X 54 /* 右列 x(值) */
/* 静态变量定义区 */
/* 页面运行态(每次开页经 init_cb 重置;约 2 B) */
static uint8_t s_editing; /* 1 = 编辑中(被编辑行 = 当前选中行) */
static uint8_t s_cursor; /* 编辑模式活动字节(IP/掩码/网关 0..3,MAC 0..5) */
/* 函数定义区 */
/*
* 函数功能:判断两区域是否相交(与 ui_content_page.c / ui_page_password.c 同实现)
* 入口参数:a - 区域 1 const lv_area_t *
* b - 区域 2 const lv_area_t *
* 返回值:true - 相交;false - 不相交
* 限定条件:无
*/
static bool nc_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);
}
/*
* 函数功能:取地址行字节数
* 入口参数:row - 地址行下标(0..3) uint16_t
* 返回值:字节数(IP/掩码/网关 = 4,MAC = 6) uint8_t
* 限定条件:row < NC_ADDR_ROW_MAX
*/
static uint8_t nc_octet_count(uint16_t row) {
return (row == 3u) ? 6u : 4u;
}
/*
* 函数功能:生成指定地址行的值串(与 refresh 值列同源,编辑光标定位与行文本一致)
* 入口参数:row - 地址行下标(0..3) uint16_t
* p_buf - 输出缓冲(≥ UI_BUF_STRLEN) char *
* 返回值:无
* 限定条件:p_buf 非空
* 函数说明:IP/掩码/网关 = "192.168.001.100"(每段 3 位补零定长,字节 i 恒在
* 字符 4i 起);MAC = "A4:94:B6:12:34:56"(冒号分隔,字节 i 恒在字符 3i 起)
*/
static void nc_fmt_value(uint16_t row, char *p_buf) {
const net_cfg_t *cfg = net_cfg_get();
if (row == 3u) {
uint8_t mac[6];
net_cfg_get_mac(mac);
(void)lv_snprintf(p_buf, UI_BUF_STRLEN, "%02X:%02X:%02X:%02X:%02X:%02X",
(unsigned)mac[0], (unsigned)mac[1], (unsigned)mac[2],
(unsigned)mac[3], (unsigned)mac[4], (unsigned)mac[5]);
return;
}
{
const uint8_t *field = (row == 0u) ? cfg->ip
: ((row == 1u) ? cfg->mask : cfg->gw);
net_cfg_fmt_ipv4(field, p_buf, UI_BUF_STRLEN);
}
}
/*
* 函数功能:动态行刷新(逐行取名/值)
* 入口参数:row - 行下标(0..4) uint16_t
* name - 名缓冲(≥ UI_BUF_STRLEN,框架栈缓冲) char *
* value - 值缓冲(≥ UI_BUF_STRLEN,框架栈缓冲) char *
* 返回值:无
* 限定条件:页面打开(框架绘制链内逐行调用)
* 函数说明:地址行值取 net_cfg RAM 副本(MAC 全零时显示芯片实际 MAC 影子);
* 保存行值随保存交接状态(net_cfg_save_state)显示
* ENTER/WAIT/OK/ERR
*/
static void nc_refresh(uint16_t row, char *name, char *value) {
const net_cfg_t *cfg = net_cfg_get();
name[0] = '\0';
value[0] = '\0';
switch (row) {
case 0u:
(void)memcpy(name, "IP地址", sizeof("IP地址"));
net_cfg_fmt_ipv4(cfg->ip, value, UI_BUF_STRLEN);
break;
case 1u:
(void)memcpy(name, "子网掩码", sizeof("子网掩码"));
net_cfg_fmt_ipv4(cfg->mask, value, UI_BUF_STRLEN);
break;
case 2u:
(void)memcpy(name, "默认网关", sizeof("默认网关"));
net_cfg_fmt_ipv4(cfg->gw, value, UI_BUF_STRLEN);
break;
case 3u:
(void)memcpy(name, "MAC地址", sizeof("MAC地址"));
nc_fmt_value(row, value);
break;
case 4u:
(void)memcpy(name, "保存", sizeof("保存"));
switch (net_cfg_save_state()) {
case NET_CFG_SAVE_ST_WAIT:
(void)memcpy(value, "WAIT", sizeof("WAIT"));
break;
case NET_CFG_SAVE_ST_OK:
(void)memcpy(value, "OK", sizeof("OK"));
break;
case NET_CFG_SAVE_ST_FAIL:
(void)memcpy(value, "ERR", sizeof("ERR"));
break;
default:
(void)memcpy(value, "ENTER", sizeof("ENTER"));
break;
}
break;
default:
break;
}
}
/*
* 函数功能:调整被编辑地址行的活动字节(0..255 环绕,实时落 RAM 副本)
* 入口参数:row - 地址行下标(0..3) uint16_t
* delta - 调整方向(>0 = +1,<0 = -1) int8_t
* 返回值:无
* 限定条件:编辑模式(s_cursor 有效)
* 函数说明:只改 RAM 副本;写盘须显式经保存行投递 net_cfg_save_request
*/
static void nc_adjust_octet(uint16_t row, int8_t delta) {
net_cfg_t cfg = *net_cfg_get();
uint8_t *field = (row == 0u) ? cfg.ip
: ((row == 1u) ? cfg.mask : ((row == 2u) ? cfg.gw : cfg.mac));
field[s_cursor] = (uint8_t)((field[s_cursor] + ((delta > 0) ? 1u : 255u)) % 256u);
net_cfg_set(&cfg);
}
/*
* 函数功能:页面自绘(编辑模式:选中行值内活动字节反显)
* 入口参数:layer - 绘制层 lv_layer_t *
* clip - 重绘区 const lv_area_t *
* 返回值:无
* 限定条件:页面打开(框架绘制链末尾调用)
* 函数说明:非编辑模式无附加绘制(行/带/页脚全由框架);编辑模式在选中行白带
* 内对活动字节区域反显(1bpp:white=暗底 black=亮字),位置按文本
* 步进宽实测定位(ASCII 步进 6px/字符,与行文本布局一致)
*/
static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) {
char value[UI_BUF_STRLEN];
lv_area_t box;
uint32_t start;
uint32_t seg_len;
int32_t slot_y;
int32_t x0;
int32_t w;
uint16_t sel;
if (s_editing == 0u || ui_content_page_is_editable() == false) {
return;
}
sel = ui_content_page_selected_row();
if (sel >= NC_ADDR_ROW_MAX) {
return; /* 防御:保存行无编辑模式 */
}
nc_fmt_value(sel, value);
start = (uint32_t)s_cursor * ((sel == 3u) ? 3u : 4u); /* 字节 i 字符偏移:IP/掩码/网关 4i,MAC 3i(冒号分隔) */
seg_len = (sel == 3u) ? 2u : 3u;
slot_y = UI_LIST_TOP + (int32_t)sel * UI_ROW_PITCH;
x0 = NC_RIGHT_X + (int32_t)lv_text_get_width(value, start, LV_FONT_DEFAULT, 0);
w = (int32_t)lv_text_get_width(value + start, seg_len, LV_FONT_DEFAULT, 0);
box.x1 = x0 - 1;
box.x2 = x0 + w;
box.y1 = slot_y;
box.y2 = slot_y + UI_ROW_BAND_H - 1;
if (!nc_areas_intersect(&box, clip)) {
return; /* 编辑光标不在重绘区:跳过(culling) */
}
/* 反显:黑底白字(与选中行白带黑字相反,方能在带内凸显活动字节;
* 1bpp:black=暗底 white=亮字) */
lv_draw_rect_dsc_t fd;
lv_draw_rect_dsc_init(&fd);
fd.base.layer = layer;
fd.bg_color = UI_COL_BG;
fd.bg_opa = LV_OPA_COVER;
lv_draw_rect(layer, &fd, &box);
{
lv_draw_label_dsc_t ld;
char seg[4]; /* 段文本:IP/掩码/网关 3 字符、MAC 2 字符,+ '\0' */
/* 只绘制"段文本"本身:若传整串(value + start),LVGL 在未置 EXPAND 标志时
* 按 coords 宽度(此处为段宽)折行,会逐行向盒下方绘制、溢出到其它行/空白区。
* 传段文本(宽度 ≤ 盒宽)则单行绘制,恰好落在光标盒内。 */
memcpy(seg, value + start, seg_len);
seg[seg_len] = '\0';
lv_draw_label_dsc_init(&ld);
ld.base.layer = layer;
ld.text = seg;
ld.text_static = 0u; /* 栈缓冲(同帧有效) */
ld.color = UI_COL_FG;
ld.ofs_x = 1; /* box.x1 = x0-1,+1 = x0(与框架行文本原点一致) */
ld.ofs_y = 0; /* 与行文本垂直对齐(text_dy = 0) */
lv_draw_label(layer, &ld, &box);
}
}
/*
* 函数功能:页面按键处理(列表页 + 编辑模式)
* 入口参数:key - LVGL 键值 lv_key_t
* 返回值:true - 页已处理(框架跳过内置语义);false - 未处理
* 限定条件:页面打开;经框架键分发调用(uiTask 上下文)
* 函数说明: 1. 编辑模式:↑↓调活动字节(0..255 环绕),←→移字节光标(首↔末
* 环绕;← 键经框架拦截标志转入本函数),ENTER 确认退出编辑
* (值已实时落 RAM 副本);ESC 恒关页(框架保证,未保存调整留在
* RAM,下次开页重显、不写盘、重启失效)
* 2. 正常模式:ENTER 地址行 → 进编辑模式(需可编辑权限);ENTER
* 保存行 → 投递保存请求 net_cfg_save_request(需可编辑权限;
* 写盘与"立即应用"由属主任务 defaultTask 执行,保存行随交接
* 状态显示 WAIT/OK/ERR);↑↓ 落框架移选、← 落框架关页;
* 只读态 ENTER 一律忽略
*/
static bool nc_key_cb(lv_key_t key) {
uint16_t sel = ui_content_page_selected_row();
if (s_editing != 0u) {
uint8_t n;
if (sel >= NC_ADDR_ROW_MAX) {
s_editing = 0u; /* 防御:编辑中选中行越界(编辑态移选被拦截,不应发生) */
return false;
}
n = nc_octet_count(sel);
switch (key) {
case LV_KEY_UP:
nc_adjust_octet(sel, 1);
ui_content_page_invalidate();
return true;
case LV_KEY_DOWN:
nc_adjust_octet(sel, -1);
ui_content_page_invalidate();
return true;
case LV_KEY_LEFT:
s_cursor = (uint8_t)((s_cursor + n - 1u) % n); /* 光标左移,首绕末 */
ui_content_page_invalidate();
return true;
case LV_KEY_RIGHT:
s_cursor = (uint8_t)((s_cursor + 1u) % n); /* 光标右移,末绕首 */
ui_content_page_invalidate();
return true;
case LV_KEY_ENTER:
s_editing = 0u; /* 确认(值已在 RAM 副本);退出编辑 */
ui_content_page_invalidate();
return true;
default:
return false; /* ESC 框架恒关页;其余键落内置忽略 */
}
}
if (key == LV_KEY_ENTER && ui_content_page_is_editable() != false) {
if (sel < NC_ADDR_ROW_MAX) {
s_editing = 1u;
s_cursor = 0u;
ui_content_page_invalidate();
return true;
}
if (sel == NC_SAVE_ROW) {
(void)net_cfg_save_request(); /* 投递保存请求:属主任务写盘并立即应用 */
ui_content_page_invalidate();
return true;
}
}
return false;
}
/*
* 函数功能:重置页面运行态
* 入口参数:无
* 返回值:无
* 限定条件:框架 ui_content_page_open 内调用(开页后一次)
* 函数说明:页面模块静态态不随屏/容器建毁自动复位,须自重置
* (编辑态/光标/保存提示)
*/
static void nc_init_cb(void) {
s_editing = 0u;
s_cursor = 0u;
}
/*
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=net_cfg 绑定引用):
* 列表页(5 行 1 页,动态值)+ 可编辑(edit_capable,还须修改权限)
* + ← 拦截(编辑模式移字节光标;ESC 恒不可拦截)
*/
const content_page_def_t ui_page_net_cfg_desc = {
.id = "net_cfg",
.title = "网口设置",
.selectable = 1u,
.key_intercept = 1u,
.edit_capable = 1u,
.top_y = UI_LIST_TOP,
.pitch = UI_ROW_PITCH,
.band_h = UI_ROW_BAND_H,
.text_dy = 0,
.left_x = NC_LEFT_X,
.right_x = NC_RIGHT_X,
.rows_per_page = NC_ROW_COUNT,
.row_count = NC_ROW_COUNT,
.refresh = nc_refresh,
.init_cb = nc_init_cb,
.draw_cb = nc_draw_cb,
.key_cb = nc_key_cb,
};