diff --git a/.gitignore b/.gitignore index 570ec49d..7ce84d4a 100644 --- a/.gitignore +++ b/.gitignore @@ -95,8 +95,9 @@ simulator/SDL2/ # Binary / large files *.bin -# 内容页黄金参考截图(回归测试基准,须入库) -!simulator/tests/ref/*.bin +# 内容页黄金参考(ref/*.bin)与目检 PNG 均为本地生成物,不入库: +# 首次/换机后先跑 check_content_pages.py --update 生成 ref/*.bin,再跑 regen_ref_png.py 出 PNG +simulator/tests/ref/ *.pyc # Python venv (unified env, see AGENTS.md "Python 环境"; rebuild via requirements.txt) diff --git a/AGENTS.md b/AGENTS.md index 728c5d4f..1951f0a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,7 @@ Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart - **app_main_init 顺序**(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init`(墙钟基准)→ `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → SD2506 诊断 → `tpafe5160_start_conv_irq` - **就绪屏障**(`App/sys_events.h` Event Flags,取代魔法延时/全局轮询):`SYS_EVT_NET_READY`(bit0, netTask 置位) / `SYS_EVT_STORAGE_READY`(bit1, defaultTask 置位) / `SYS_EVT_FTP_READY`(bit2) / `SYS_EVT_TIMESYNC_READY`(bit3);消费者用 `osEventFlagsWait` 阻塞等待 - **defaultTask**:FatFS 挂载(`f_mount` forced,无卷自动 `f_mkfs` FAT32)→ 置 `SYS_EVT_STORAGE_READY` → LED 闪烁循环 -- **netTask**:延时 2s → `net_stack_bring_up()`(net_init + MAC + PHY 强制 100M 全双工 + 等链路 ≤3s)→ 置 `SYS_EVT_NET_READY` → 循环 `net_poll()` + `net_process_messages()` + `osDelay(1)`(tick=1ms) +- **netTask**:等 `SYS_EVT_STORAGE_READY`(≤10s)→ `net_stack_bring_up()`(`net_cfg_load` 读 Flash 网口配置 → `net_init(ip,mask,gw,mac)` + PHY 强制 100M 全双工 + 等链路 ≤3s → 回写芯片实际 MAC 影子)→ 置 `SYS_EVT_NET_READY` → 循环 `net_poll()` + `net_process_messages()` + `osDelay(1)`(tick=1ms) - **ftpTask**:`osEventFlagsWait(NET_READY | STORAGE_READY, WaitAll)` 后 `lftpd_start("/", 21, &ftp)`(已无 7s 魔法延时) - **testTask**:测试套件入口(`test/test_config.h` 选择;产品构建下空转) - **uiTask**:等 `SYS_EVT_NET_READY` 后 `ui_port_init` → `ui_menu_init` → 启动 UI net sink @@ -69,8 +69,8 @@ Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart - `net_socket.h/c` — BSD Socket API;**单连接模式**(`ch395f_set_fun_para(0x09)`:多 Socket + TCP Server 单连接 + 手动关闭),连接停留在 Socket 0 - `net_select.h/c` — select/poll I/O 多路复用 - 线程安全通过消息队列(`netMsgQueue`)实现,所有 CH395F 操作在 netTask 中串行处理 -- `net_init` 顺序:check_exist → reset → `set_fun_para(0x09)` → MSS/重传/KeepAlive → IP → `ch395f_init()` → Socket 0~7 缓冲分配 → ping 使能 -- 静态 IP 192.168.1.100/24(gw 192.168.1.1),PHY 强制 100M 全双工 +- `net_init` 顺序:check_exist → reset → `set_fun_para(0x09)` → MSS/重传/KeepAlive → IP → `ch395f_init()` → `ch395f_set_mac_addr()`(mac 非空且非全零时)→ Socket 0~7 缓冲分配 → ping 使能 +- 网口参数(IP/掩码/网关/MAC)取自 Flash 配置(`Drivers/BSP/NETCFG/`:magic 'NCFG' + ver + CRC-16/MODBUS;工厂默认 192.168.1.100/24、gw .1,MAC 全零 = 芯片内置),保存后**下次上电生效**;PHY 强制 100M 全双工 - **新文件需手动添加到 Keil MDK 工程**才能编译 ## 测试构建模式 @@ -91,6 +91,7 @@ Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart | `App/task/net_task.c` | netTask 主循环(网络初始化 + 轮询) | | `test/test_config.h` | 测试套件选择(互斥开关) | | `Drivers/BSP/NET/` | BSD Socket API 网络抽象层 + lftpd | +| `Drivers/BSP/NETCFG/` | 网口配置(Flash 持久化 + CRC,网口设置内容页数据源) | | `Drivers/BSP/CH395F/` | CH395F SPI 驱动 | | `Drivers/BSP/GD5F2GQ5UE/` | NAND Flash 驱动 + dhara FTL + FatFS diskio | | `Drivers/BSP/TPAFE5160/` | ADC 并行接口驱动 | diff --git a/App/task/net_task.c b/App/task/net_task.c index 81fdd53d..59e7a691 100644 --- a/App/task/net_task.c +++ b/App/task/net_task.c @@ -17,6 +17,7 @@ #include "cmsis_os.h" #include "net_socket.h" #include "net_select.h" +#include "net_cfg.h" #include "ch395f.h" #include "sys_events.h" @@ -27,14 +28,29 @@ * 函数功能:CH395F 协议栈一次性初始化(两种构建模式共用) * 入口参数:无 * 返 回 值:0 - 成功,-1 - 失败 - * 函数说明:net_init → MAC 打印 → PHY 强制 100M 全双工 → 广播网络就绪事件。 + * 函数说明:net_cfg 加载(首启/无文件 = 工厂默认)→ net_init → MAC 回写 + * 配置模块 → MAC 打印 → PHY 强制 100M 全双工 → 广播网络就绪事件。 * NET 层模式由 netTask 调用;硬件层模式(阶段 1~5)netTask 不创建, * 由 testTask 调用后独占 CH395F。 */ int net_stack_bring_up(void) { + const net_cfg_t *cfg; + char ip[16]; + char mask[16]; + char gw[16]; + DBG_INFO("=== Network Init ==="); - if (net_init("192.168.1.100", "255.255.255.0", "192.168.1.1") != 0) { + /* 加载网络配置(存储后端:NAND 上 FatFS 文件 /cfg/netcfg.bin; + * 首启/存储未就绪 = 工厂默认,与既有硬编码组网值一致) */ + (void)net_cfg_load(); + cfg = net_cfg_get(); + + net_cfg_fmt_ipv4(cfg->ip, ip, sizeof(ip)); + net_cfg_fmt_ipv4(cfg->mask, mask, sizeof(mask)); + net_cfg_fmt_ipv4(cfg->gw, gw, sizeof(gw)); + + if (net_init(ip, mask, gw, cfg->mac) != 0) { DBG_ERROR("net init: FAIL"); return -1; } @@ -45,6 +61,10 @@ int net_stack_bring_up(void) { DBG_INFO("MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + /* 回写芯片实际 MAC 到配置模块(UI "网口设置"页只读影子, + * 不直接触 CH395F SPI——SPI 归 netTask 集中处理) */ + net_cfg_set_active_mac(mac); + uint8_t ipinf[20]; ch395f_get_ip_inf(ipinf); DBG_INFO("IP_INF: IP=%d.%d.%d.%d GW=%d.%d.%d.%d MASK=%d.%d.%d.%d", @@ -95,7 +115,8 @@ int net_task_init(void) { * 函数功能:网络任务主函数 * 入口参数:arg - FreeRTOS 传入参数(未使用) * 返 回 值:无 - * 函数说明:启动后先调用 net_stack_bring_up() 初始化 CH395F 协议栈, + * 函数说明:等待存储就绪(就绪屏障,10s 超时回退工厂默认,存储故障 + * 不阻塞组网)→ net_stack_bring_up() 初始化 CH395F 协议栈, * 再进入轮询循环处理网络事件。 * 仅 NET 层测试模式(阶段 6~10)会创建本任务; * 硬件层模式下 netTask 不创建,CH395F 由 testTask 独占。 @@ -103,7 +124,12 @@ int net_task_init(void) { void net_task_func(void const *arg) { (void)arg; - osDelay(2000); + /* 配置在 NAND 的 FatFS 卷上:等存储就绪再读(取代原 2s 魔法延时); + * 超时(存储异常)继续以工厂默认组网,不卡网络启动 */ + if (sys_evtHandle != NULL) { + (void)osEventFlagsWait(sys_evtHandle, SYS_EVT_STORAGE_READY, + osFlagsWaitAny, 10000u); + } if (net_stack_bring_up() != 0) { for (;;) { diff --git a/Drivers/BSP/NET/lftpd/lftpd.c b/Drivers/BSP/NET/lftpd/lftpd.c index f579152b..03e3ce81 100644 --- a/Drivers/BSP/NET/lftpd/lftpd.c +++ b/Drivers/BSP/NET/lftpd/lftpd.c @@ -6,6 +6,8 @@ #include #include +#include "FreeRTOS.h" + #include "lftpd.h" #include "private/lftpd_status.h" #include "private/lftpd_inet.h" @@ -222,14 +224,14 @@ static int cmd_cwd(lftpd_client_t *client, const char *arg) int is_dir; if (lftpd_io_stat(path, &size, &is_dir) != 0 || !is_dir) { send_simple_response(client->socket, 550, STATUS_550); - free(path); + vPortFree(path); return -1; } } strncpy(client->directory, path, sizeof(client->directory) - 1); client->directory[sizeof(client->directory) - 1] = '\0'; - free(path); + vPortFree(path); send_simple_response(client->socket, 250, STATUS_250); return 0; @@ -248,18 +250,18 @@ static int cmd_dele(lftpd_client_t *client, const char *arg) int is_dir; if (lftpd_io_stat(path, &size, &is_dir) != 0) { send_simple_response(client->socket, 550, STATUS_550); - free(path); + vPortFree(path); return -1; } if (is_dir) { send_simple_response(client->socket, 550, STATUS_550); - free(path); + vPortFree(path); return -1; } lftpd_io_unlink(path); - free(path); + vPortFree(path); send_simple_response(client->socket, 250, STATUS_250); return 0; } @@ -459,7 +461,7 @@ static int cmd_retr(lftpd_client_t *client, const char *arg) lftpd_log_error("DOWNLOAD '%s' FAILED", path); send_simple_response(client->socket, 450, STATUS_450); } - free(path); + vPortFree(path); return 0; } @@ -481,7 +483,7 @@ static int cmd_size(lftpd_client_t *client, const char *arg) send_simple_response(client->socket, 550, STATUS_550); } - free(path); + vPortFree(path); return 0; } @@ -518,7 +520,7 @@ static int cmd_stor(lftpd_client_t *client, const char *arg) lftpd_log_error("UPLOAD '%s' FAILED", path); send_simple_response(client->socket, 450, STATUS_450); } - free(path); + vPortFree(path); return 0; } diff --git a/Drivers/BSP/NET/lftpd/lftpd_io.c b/Drivers/BSP/NET/lftpd/lftpd_io.c index 5c0ecf64..7b7168de 100644 --- a/Drivers/BSP/NET/lftpd/lftpd_io.c +++ b/Drivers/BSP/NET/lftpd/lftpd_io.c @@ -3,6 +3,8 @@ #include #include +#include "FreeRTOS.h" + #include "ff.h" #include "private/lftpd_io.h" @@ -41,19 +43,19 @@ char *lftpd_io_canonicalize_path(const char *base, const char *name) char *path; if (name[0] == '/') { - path = (char *)malloc(strlen(name) + 1); + path = (char *)pvPortMalloc(strlen(name) + 1); if (path == NULL) return NULL; strcpy(path, name); } else { size_t len = strlen(base) + 1 + strlen(name) + 1; - path = (char *)malloc(len); + path = (char *)pvPortMalloc(len); if (path == NULL) return NULL; snprintf(path, len, "%s/%s", base, name); } - char *abs_path = (char *)malloc(strlen(path) + 2); + char *abs_path = (char *)pvPortMalloc(strlen(path) + 2); if (abs_path == NULL) { - free(path); + vPortFree(path); return NULL; } abs_path[0] = '\0'; @@ -83,7 +85,7 @@ char *lftpd_io_canonicalize_path(const char *base, const char *name) *p = saved; } - free(path); + vPortFree(path); if (strlen(abs_path) == 0) { strcpy(abs_path, "/"); @@ -186,13 +188,13 @@ int lftpd_io_unlink(const char *path) void *lftpd_io_opendir(const char *path) { - DIR *dp = (DIR *)malloc(sizeof(DIR)); + DIR *dp = (DIR *)pvPortMalloc(sizeof(DIR)); if (dp == NULL) { return NULL; } const char *fpath = to_fatfs_path(path); if (f_opendir(dp, fpath) != FR_OK) { - free(dp); + vPortFree(dp); return NULL; } return (void *)dp; @@ -230,6 +232,6 @@ void lftpd_io_closedir(void *dp) { if (dp != NULL) { f_closedir((DIR *)dp); - free(dp); + vPortFree(dp); } } diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 823cdc25..943f9fe4 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -11,6 +11,7 @@ #include #include +#include #include "FreeRTOS.h" #include "task.h" @@ -69,11 +70,27 @@ static int net_relisten_locked(net_sock_t *p_sock); static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen); static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int addrlen); +/* + * 函数功能:判断 6 字节 MAC 是否全零 + * 入口参数:p_mac - MAC 指针(不可为 NULL) const uint8_t * + * 返回值:true - 全零(= 使用芯片内置 MAC);false - 非全零 + * 限定条件:p_mac 非空 + */ +static bool s_mac_is_zero(const uint8_t *p_mac) { + for (int i = 0; i < 6; i++) { + if (p_mac[i] != 0u) { + return false; + } + } + return true; +} + /* * 函数功能:网络子系统初始化(配置 CH395F + 启用多连接模式) * 入口参数:ip - IP 地址字符串 "192.168.1.100",NULL 使用 DHCP 自动获取 const char * * mask - 子网掩码字符串 "255.255.255.0",NULL 使用默认值 const char * * gateway - 网关地址字符串 "192.168.1.1",NULL 使用默认值 const char * + * mac - MAC 地址(6 字节,低字节在前),NULL/全零使用芯片内置 const uint8_t * * 返回值:0 - 初始化成功 int * -1 - 初始化失败(芯片检测/复位/协议栈初始化失败) int * 限定条件:SPI2 已正确初始化;netTask 尚未启动 @@ -81,8 +98,10 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a * 2. 启用 TCP Server 多连接模式(bit1=1),设置 TCP MSS、重传参数、KeepAlive 参数 * 3. 配置 IP/网关/子网掩码,或启用 DHCP * 4. 初始化 CH395F 协议栈并使能 PING 响应 + * 5. mac 非空且非全零:初始化成功后设置 MAC(写芯片 EEPROM, + * 每上电重放同值幂等) */ -int net_init(const char *ip, const char *mask, const char *gateway) { + int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac) { ch395f_status_t ret; DBG_INFO("net_init: start"); @@ -155,6 +174,13 @@ int net_init(const char *ip, const char *mask, const char *gateway) { return -1; } + /* 设置 MAC(须在协议栈初始化之后,限定条件"芯片已初始化";写芯片 EEPROM, + * 每上电重放同值幂等);NULL 或全零 = 保持芯片内置 MAC */ + if (mac != NULL && s_mac_is_zero(mac) == false) { + ch395f_set_mac_addr((uint8_t *)mac); + DBG_INFO("net_init: set MAC"); + } + /* 多 Socket 模式:为 Socket 0~7 各分配独立收发缓冲(必须在 OPEN_SOCKET 之前) * 手册(§5.45):CH395F 内部缓冲共 48 块 × 512B = 24KB;芯片上电默认仅给 Socket 0~3 * 分配(每 Socket 接收 8 块/发送 4 块),Socket 4~7 为 NULL,故用到 8 个 Socket 必须重分配。 @@ -520,7 +546,7 @@ int net_listen_locked(int sockfd) { break; /* 打开成功,退出重试 */ } ch395f_close_socket(sockfd); /* 打开失败先关闭,再延时后重试 */ - HAL_Delay(500); + osDelay(500); } /* 3 次仍失败,返回错误 */ if (status != CH395F_ERR_SUCCESS) { @@ -795,7 +821,7 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a break; /* 打开成功,退出重试 */ } ch395f_close_socket(sockfd); /* 打开失败先关闭,再延时后重试 */ - HAL_Delay(500); + osDelay(500); } if (status != CH395F_ERR_SUCCESS) { @@ -1020,7 +1046,7 @@ static int net_relisten_locked(net_sock_t *p_sock) { break; } ch395f_close_socket((uint8_t)sockfd); /* 打开失败先关闭,再延时后重试 */ - HAL_Delay(500); + osDelay(500); } if (status != CH395F_ERR_SUCCESS) { s_net_errno = NET_ERR_BUSY; diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index febd883d..0c55707d 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -53,11 +53,14 @@ typedef struct { * 入口参数:ip - 本地 IP 地址字符串 "192.168.1.100",NULL 使用 DHCP * mask - 子网掩码字符串 "255.255.255.0",NULL 使用默认 * gateway - 网关地址字符串 "192.168.1.1",NULL 使用默认 + * mac - MAC 地址(6 字节,低字节在前),NULL 或全零使用芯片内置 * 返回值:0 成功,-1 失败 * 限定条件:SPI2 已正确初始化 - * 函数说明:必须在所有网络操作之前调用 + * 函数说明:必须在所有网络操作之前调用;mac 非空且非全零时于协议栈 + * 初始化成功后设置(ch395f_set_mac_addr 写芯片 EEPROM, + * 每上电重放同值幂等) */ -int net_init(const char *ip, const char *mask, const char *gateway); +int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac); /* * 函数功能:轮询所有 Socket 状态(必须在主循环中调用) diff --git a/Drivers/BSP/NETCFG/net_cfg.c b/Drivers/BSP/NETCFG/net_cfg.c new file mode 100644 index 00000000..c1a514ce --- /dev/null +++ b/Drivers/BSP/NETCFG/net_cfg.c @@ -0,0 +1,260 @@ +/* + * 模块名称:Network Configuration Core (配置核心) + * 模块功能:RAM 配置副本 + 工厂默认 + 加载/保存编排 + 实际 MAC 影子 + + * IPv4 字符串转换;磁盘格式 magic 'NCFG' + ver + CRC-16(MODBUS) + * + 18B 字段(两端小端一致,sizeof 含尾部对齐填充) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * v1.0 2026-09-10 王建锋 创建初始版本 + */ + +/* 头文件包含区 */ +#include + +#include + +#include + +#include "net_cfg.h" + +#include "net_cfg_store.h" + +#define DBG_TAG "[NET_CFG]" +#include "dbg_log.h" + +/* 私有宏定义区 */ + +#define NET_CFG_MAGIC 0x4647434Eu /* 'NCFG'(文件字节序 N,C,G,F;小端装载) */ +#define NET_CFG_VER 1u /* 磁盘格式版本 */ + +/* 工厂默认值(单一来源:s_cfg 初值与 s_cfg_defaults 共用,避免两处漂移) */ +#define NET_CFG_DEF_IP { 192u, 168u, 1u, 100u } +#define NET_CFG_DEF_MASK { 255u, 255u, 255u, 0u } +#define NET_CFG_DEF_GW { 192u, 168u, 1u, 1u } +#define NET_CFG_DEF_MAC { 0u, 0u, 0u, 0u, 0u, 0u } + +/* 磁盘格式(两端布局一致:u32 4 对齐;字段区 18B 连续,CRC 覆盖之) */ +typedef struct { + uint32_t magic; /* NET_CFG_MAGIC */ + uint16_t ver; /* 格式版本 */ + uint16_t crc; /* CRC-16/MODBUS(覆盖 ip..mac 共 18B) */ + uint8_t ip[4]; + uint8_t mask[4]; + uint8_t gw[4]; + uint8_t mac[6]; +} net_cfg_blob_t; + +/* 静态变量定义区 */ + +/* RAM 配置副本:以工厂默认值静态初始化(落 .data),自启动即有效—— + * 不依赖 net_cfg_load 是否被调用(load 仅在文件有效时覆盖) */ +static net_cfg_t s_cfg = { + .ip = NET_CFG_DEF_IP, + .mask = NET_CFG_DEF_MASK, + .gw = NET_CFG_DEF_GW, + .mac = NET_CFG_DEF_MAC, +}; +static uint8_t s_active_mac[6]; /* 芯片实际 MAC 影子(netTask 回写) */ +static uint8_t s_active_valid; /* 影子有效标志(0 = 尚未回写) */ + +/* 函数定义区 */ + +/* + * 函数功能:CRC-16/MODBUS(无查表逐位算法) + * 入口参数:p_data - 数据指针 const uint8_t * + * len - 数据长度 size_t + * 返回值:CRC-16 值 uint16_t + * 限定条件:p_data 非空(len 可为 0,返回初始值 0xFFFF) + * 函数说明:文件私有(双端共享、避免跨模块依赖:模拟器不编译 App/util; + * 数据量 ≤32B,性能无关) + */ +static uint16_t s_crc16_modbus(const uint8_t *p_data, size_t len) { + uint16_t crc = 0xFFFFu; + + for (size_t i = 0u; i < len; i++) { + crc = (uint16_t)(crc ^ (uint16_t)p_data[i]); + for (int b = 0; b < 8; b++) { + if (crc & 0x0001u) { + crc = (uint16_t)((crc >> 1) ^ 0xA001u); + } + else { + crc = (uint16_t)(crc >> 1); + } + } + } + return crc; +} + +/* + * 函数功能:填充工厂默认配置 + * 入口参数:p_cfg - 输出结构指针(不可为 NULL) net_cfg_t * + * 返回值:无 + * 限定条件:p_cfg 非空 + * 函数说明:与既有硬编码组网值一致(192.168.1.100/24,gw .1,MAC 全零 + * = 芯片内置),保证首启/存储异常时行为与旧版本相同 + */ +static void s_cfg_defaults(net_cfg_t *p_cfg) { + const net_cfg_t def = { + .ip = NET_CFG_DEF_IP, + .mask = NET_CFG_DEF_MASK, + .gw = NET_CFG_DEF_GW, + .mac = NET_CFG_DEF_MAC, + }; + + *p_cfg = def; +} + +/* + * 函数功能:判断 6 字节 MAC 是否全零 + * 入口参数:p_mac - MAC 指针(不可为 NULL) const uint8_t * + * 返回值:true - 全零;false - 非全零 + * 限定条件:p_mac 非空 + */ +static bool s_mac_is_zero(const uint8_t *p_mac) { + for (int i = 0; i < 6; i++) { + if (p_mac[i] != 0u) { + return false; + } + } + return true; +} + +/* + * 函数功能:从存储加载网络配置到 RAM 副本 + * 入口参数:无 + * 返回值:恒 0(RAM 恒有效:文件缺失/CRC 错回退工厂默认) int + * 限定条件:MCU 端应在存储就绪后调用;未就绪时文件读失败自动回退 + * 函数说明:见 net_cfg.h 同名函数说明 + */ +int net_cfg_load(void) { + net_cfg_blob_t blob; + + if (net_cfg_store_read((uint8_t *)&blob, sizeof(blob)) != 0) { + /* 文件缺失/不可读(首启、存储未就绪):工厂默认 */ + s_cfg_defaults(&s_cfg); + DBG_INFO("load: no valid file, factory defaults"); + return 0; + } + + if (blob.magic != NET_CFG_MAGIC || blob.ver != NET_CFG_VER + || s_crc16_modbus(blob.ip, 18u) != blob.crc) { + /* 格式损坏:工厂默认并留痕(不让坏配置带崩网络) */ + s_cfg_defaults(&s_cfg); + DBG_ERROR("load: blob invalid (magic/ver/crc), factory defaults"); + return 0; + } + + memcpy(s_cfg.ip, blob.ip, 4u); + memcpy(s_cfg.mask, blob.mask, 4u); + memcpy(s_cfg.gw, blob.gw, 4u); + memcpy(s_cfg.mac, blob.mac, 6u); + DBG_INFO("load: OK ip=%u.%u.%u.%u", + (unsigned)s_cfg.ip[0], (unsigned)s_cfg.ip[1], + (unsigned)s_cfg.ip[2], (unsigned)s_cfg.ip[3]); + return 0; +} + +/* + * 函数功能:保存当前 RAM 配置到存储 + * 入口参数:无 + * 返回值:0 - 成功;-1 - 写失败 int + * 限定条件:MCU 端应在存储就绪后调用 + * 函数说明:组装磁盘格式经后端写盘;下次上电生效 + */ +int net_cfg_save(void) { + net_cfg_blob_t blob; + + memcpy(blob.ip, s_cfg.ip, 4u); + memcpy(blob.mask, s_cfg.mask, 4u); + memcpy(blob.gw, s_cfg.gw, 4u); + memcpy(blob.mac, s_cfg.mac, 6u); + blob.magic = NET_CFG_MAGIC; + blob.ver = NET_CFG_VER; + blob.crc = s_crc16_modbus(blob.ip, 18u); + + if (net_cfg_store_write((const uint8_t *)&blob, sizeof(blob)) != 0) { + DBG_ERROR("save: store write FAILED"); + return -1; + } + DBG_INFO("save: OK (effective at next power-on)"); + return 0; +} + +/* + * 函数功能:取 RAM 配置副本(只读指针) + * 入口参数:无 + * 返回值:RAM 配置副本指针(恒非空) const net_cfg_t * + * 限定条件:无 + */ +const net_cfg_t *net_cfg_get(void) { + return &s_cfg; +} + +/* + * 函数功能:更新 RAM 配置副本(不写盘) + * 入口参数:p_cfg - 新配置指针(不可为 NULL) const net_cfg_t * + * 返回值:无 + * 限定条件:p_cfg 非空 + */ +void net_cfg_set(const net_cfg_t *p_cfg) { + if (p_cfg == NULL) { + return; + } + s_cfg = *p_cfg; +} + +/* + * 函数功能:回写芯片实际 MAC(影子区) + * 入口参数:p_mac - 芯片读回的实际 MAC(不可为 NULL) const uint8_t * + * 返回值:无 + * 限定条件:net_init 成功后由 netTask 上下文调用 + */ +void net_cfg_set_active_mac(const uint8_t *p_mac) { + if (p_mac == NULL) { + return; + } + memcpy(s_active_mac, p_mac, 6u); + s_active_valid = 1u; +} + +/* + * 函数功能:取显示用 MAC + * 入口参数:p_out - 6 字节输出缓冲(不可为 NULL) uint8_t * + * 返回值:无 + * 限定条件:p_out 非空 + * 函数说明:存储非全零 → 存储值;否则芯片实际(已回写);否则全零 + */ +void net_cfg_get_mac(uint8_t *p_out) { + if (p_out == NULL) { + return; + } + if (!s_mac_is_zero(s_cfg.mac)) { + memcpy(p_out, s_cfg.mac, 6u); + } + else if (s_active_valid != 0u) { + memcpy(p_out, s_active_mac, 6u); + } + else { + memset(p_out, 0, 6u); + } +} + +/* + * 函数功能:4 字节 IPv4 格式化为点分十进制字符串(每段 3 位补零) + * 入口参数:p_ip - 4 字节 IP(不可为 NULL) const uint8_t * + * p_buf - 输出缓冲(不可为 NULL) char * + * len - 缓冲长度 size_t + * 返回值:无 + * 限定条件:p_ip/p_buf 非空;len ≥ 16 + * 函数说明:"192.168.001.100"(定长 11 字符,UI 编辑光标按固定偏移定位字节) + */ +void net_cfg_fmt_ipv4(const uint8_t *p_ip, char *p_buf, size_t len) { + if (p_ip == NULL || p_buf == NULL || len == 0u) { + return; + } + (void)snprintf(p_buf, len, "%03u.%03u.%03u.%03u", + (unsigned)p_ip[0], (unsigned)p_ip[1], + (unsigned)p_ip[2], (unsigned)p_ip[3]); +} diff --git a/Drivers/BSP/NETCFG/net_cfg.h b/Drivers/BSP/NETCFG/net_cfg.h new file mode 100644 index 00000000..5ebcd210 --- /dev/null +++ b/Drivers/BSP/NETCFG/net_cfg.h @@ -0,0 +1,116 @@ +/* + * 模块名称:Network Configuration (NET CFG) + * 模块功能:网络参数配置(IP/掩码/网关/MAC)——双端共享核心:RAM 配置副本 + + * 工厂默认 + 加载/保存编排 + 实际 MAC 影子;存储后端经 net_cfg_store.h + * 抽象(MCU = NAND 上 FatFS 文件 /cfg/netcfg.bin,模拟器 = 本地文件 + * sim_netcfg.bin);磁盘格式 = magic + ver + CRC-16 + 18B 字段; + * MAC 全零哨兵 = 不覆盖芯片内置 MAC;保存后下次上电生效 + * (每次上电 netTask 在 net_init 前加载) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * v1.0 2026-09-10 王建锋 创建初始版本 + */ + +#ifndef NET_CFG_H +#define NET_CFG_H + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * 网络参数配置(RAM 副本) + * mac 全零哨兵:不设置芯片 MAC(用芯片内置值,存于芯片 EEPROM) + */ +typedef struct { + uint8_t ip[4]; /* IP 地址(工厂默认 192.168.1.100) */ + uint8_t mask[4]; /* 子网掩码(工厂默认 255.255.255.0) */ + uint8_t gw[4]; /* 默认网关(工厂默认 192.168.1.1) */ + uint8_t mac[6]; /* MAC 地址(全零 = 芯片内置 MAC) */ +} net_cfg_t; + +/* + * 函数功能:从存储加载网络配置到 RAM 副本 + * 入口参数:无 + * 返回值:恒 0(RAM 恒有效:文件缺失/损坏回退工厂默认) int + * 限定条件:MCU 端应在存储就绪(SYS_EVT_STORAGE_READY)后调用; + * 未就绪时文件读失败,自动回退工厂默认(不阻塞网络启动) + * 函数说明: 1. 文件缺失(首启/未保存过)→ 工厂默认 + * 2. magic/ver/CRC 错误 → 工厂默认并留痕(不让坏配置带崩网络) + * 3. 校验通过 → 装入 RAM 副本 + */ +int net_cfg_load(void); + +/* + * 函数功能:保存当前 RAM 配置到存储(Flash 持久化) + * 入口参数:无 + * 返回值:0 - 成功;-1 - 写失败 int + * 限定条件:MCU 端应在存储就绪后调用 + * 函数说明:组装磁盘格式(magic+ver+CRC-16+18B 字段)经后端写盘; + * 下次上电生效(netTask 在 net_init 前加载) + */ +int net_cfg_save(void); + +/* + * 函数功能:取 RAM 配置副本(只读指针) + * 入口参数:无 + * 返回值:RAM 配置副本指针(恒非空) const net_cfg_t * + * 限定条件:无 + * 函数说明:netTask(启动期读取组网)与 UI 页(显示/编辑)共用 + */ +const net_cfg_t *net_cfg_get(void); + +/* + * 函数功能:更新 RAM 配置副本(不写盘) + * 入口参数:p_cfg - 新配置指针(不可为 NULL) const net_cfg_t * + * 返回值:无 + * 限定条件:p_cfg 非空;写盘须另行调 net_cfg_save + * 函数说明:UI 编辑页调整字段后经此落 RAM 副本 + */ +void net_cfg_set(const net_cfg_t *p_cfg); + +/* + * 函数功能:回写芯片实际 MAC(影子区,供 UI 显示) + * 入口参数:p_mac - 芯片读回的实际 MAC(不可为 NULL) const uint8_t * + * 返回值:无 + * 限定条件:net_init 成功后由 netTask 上下文调用 + * 函数说明:CH395F SPI 归 netTask 独占(中断/轮询集中处理);UI 页不可直接 + * 调 SPI 读命令,故 netTask 初始化后读回实际 MAC 存于此供 + * "网口设置"页显示(存储 MAC 全零时显示芯片内置 MAC) + */ +void net_cfg_set_active_mac(const uint8_t *p_mac); + +/* + * 函数功能:取显示用 MAC + * 入口参数:p_out - 6 字节输出缓冲(不可为 NULL) uint8_t * + * 返回值:无 + * 限定条件:p_out 非空 + * 函数说明:存储 MAC 非全零 → 存储值;否则芯片实际(已回写时); + * 否则全零(网络未初始化/芯片内置未知) + */ +void net_cfg_get_mac(uint8_t *p_out); + +/* + * 函数功能:4 字节 IPv4 格式化为点分十进制字符串(每段 3 位补零) + * 入口参数:p_ip - 4 字节 IP(不可为 NULL) const uint8_t * + * p_buf - 输出缓冲(不可为 NULL) char * + * len - 缓冲长度 size_t + * 返回值:无 + * 限定条件:p_ip/p_buf 非空;len ≥ 16(输出 11 字符 + '\0') + * 函数说明:"192.168.001.100"——每段 3 位补零使字符串定长,UI 编辑光标 + * 可按固定偏移(4i/3i 起始)定位字节;net_init 的 parse_ip_string + * 对前导零兼容(十进制逐段解析) + */ +void net_cfg_fmt_ipv4(const uint8_t *p_ip, char *p_buf, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_CFG_H */ diff --git a/Drivers/BSP/NETCFG/net_cfg_store.h b/Drivers/BSP/NETCFG/net_cfg_store.h new file mode 100644 index 00000000..77331a4a --- /dev/null +++ b/Drivers/BSP/NETCFG/net_cfg_store.h @@ -0,0 +1,46 @@ +/* + * 模块名称:Network Configuration Store (存储后端接口) + * 模块功能:net_cfg 加载/保存的磁盘读写抽象——每平台一份实现(互斥编译): + * MCU = NAND 上 FatFS 文件 /cfg/netcfg.bin(net_cfg_store_mcu.c); + * 模拟器 = 工作目录本地文件 sim_netcfg.bin(net_cfg_store_sim.c) + * 适用平台:STM32F407ZGTx / PC 模拟器(接口共用;两端各编一份实现) + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * v1.0 2026-09-10 王建锋 创建初始版本 + */ + +#ifndef NET_CFG_STORE_H +#define NET_CFG_STORE_H + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * 函数功能:读取配置文件 + * 入口参数:p_buf - 接收缓冲(不可为 NULL) uint8_t * + * len - 文件长度(须与磁盘格式一致,net_cfg.c 的 net_cfg_blob_t) size_t + * 返回值:0 - 成功;-1 - 文件缺失/读失败 int + * 限定条件:后端存储可用(MCU:FatFS 已挂载;未挂载时实现直接返回 -1 即可) + */ +int net_cfg_store_read(uint8_t *p_buf, size_t len); + +/* + * 函数功能:写配置文件(不存在则创建,存在则覆盖) + * 入口参数:p_buf - 待写数据(不可为 NULL) const uint8_t * + * len - 数据长度(须与磁盘格式一致) size_t + * 返回值:0 - 成功;-1 - 写失败 int + * 限定条件:后端存储可用 + */ +int net_cfg_store_write(const uint8_t *p_buf, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* NET_CFG_STORE_H */ diff --git a/Drivers/BSP/NETCFG/net_cfg_store_mcu.c b/Drivers/BSP/NETCFG/net_cfg_store_mcu.c new file mode 100644 index 00000000..f1883bf1 --- /dev/null +++ b/Drivers/BSP/NETCFG/net_cfg_store_mcu.c @@ -0,0 +1,71 @@ +/* + * 模块名称:Network Configuration Store (MCU 后端) + * 模块功能:net_cfg 存储后端——NAND 上 FatFS 文件 /cfg/netcfg.bin + * (目录 /cfg 首次写入时自动创建;卷由 defaultTask 挂载并广播 + * SYS_EVT_STORAGE_READY 后方可读写) + * 适用平台:STM32F407ZGTx + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * v1.0 2026-09-10 王建锋 创建初始版本 + */ + +/* 头文件包含区 */ +#include "net_cfg_store.h" + +#include "ff.h" + +/* 静态变量定义区 */ + +static const char s_path[] = "/cfg/netcfg.bin"; + +/* 函数定义区 */ + +/* + * 函数功能:读取配置文件 + * 入口参数:p_buf - 接收缓冲(不可为 NULL) uint8_t * + * len - 文件长度 size_t + * 返回值:0 - 成功;-1 - 文件缺失/读失败 int + * 限定条件:FatFS 已挂载(未挂载时 f_open 返回 FR_NO_SYSTEM,转 -1) + */ +int net_cfg_store_read(uint8_t *p_buf, size_t len) { + FIL f; + FRESULT fr; + UINT br = 0u; + + if (p_buf == NULL) { + return -1; + } + fr = f_open(&f, s_path, FA_READ); + if (fr != FR_OK) { + return -1; + } + fr = f_read(&f, p_buf, (UINT)len, &br); + (void)f_close(&f); + return (fr == FR_OK && br == (UINT)len) ? 0 : -1; +} + +/* + * 函数功能:写配置文件(目录不存在则创建;文件覆盖写) + * 入口参数:p_buf - 待写数据(不可为 NULL) const uint8_t * + * len - 数据长度 size_t + * 返回值:0 - 成功;-1 - 写失败 int + * 限定条件:FatFS 已挂载 + */ +int net_cfg_store_write(const uint8_t *p_buf, size_t len) { + FIL f; + FRESULT fr; + UINT bw = 0u; + + if (p_buf == NULL) { + return -1; + } + (void)f_mkdir("/cfg"); /* 目录已存在(FR_EXIST)不影响后续建文件 */ + fr = f_open(&f, s_path, FA_WRITE | FA_CREATE_ALWAYS); + if (fr != FR_OK) { + return -1; + } + fr = f_write(&f, (const void *)p_buf, (UINT)len, &bw); + (void)f_close(&f); + return (fr == FR_OK && bw == (UINT)len) ? 0 : -1; +} diff --git a/Drivers/BSP/NETCFG/net_cfg_store_sim.c b/Drivers/BSP/NETCFG/net_cfg_store_sim.c new file mode 100644 index 00000000..31c1e815 --- /dev/null +++ b/Drivers/BSP/NETCFG/net_cfg_store_sim.c @@ -0,0 +1,67 @@ +/* + * 模块名称:Network Configuration Store (模拟器后端) + * 模块功能:net_cfg 存储后端——模拟器工作目录本地文件 sim_netcfg.bin + * (磁盘格式与 MCU 端一致,两端可互读) + * 适用平台:PC 模拟器 + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * v1.0 2026-09-10 王建锋 创建初始版本 + */ + +/* 头文件包含区 */ +#include + +#include "net_cfg_store.h" + +/* 静态变量定义区 */ + +static const char s_path[] = "sim_netcfg.bin"; + +/* 函数定义区 */ + +/* + * 函数功能:读取配置文件 + * 入口参数:p_buf - 接收缓冲(不可为 NULL) uint8_t * + * len - 文件长度 size_t + * 返回值:0 - 成功;-1 - 文件缺失/读失败 int + * 限定条件:无 + */ +int net_cfg_store_read(uint8_t *p_buf, size_t len) { + FILE *fp; + size_t n; + + if (p_buf == NULL) { + return -1; + } + fp = fopen(s_path, "rb"); + if (fp == NULL) { + return -1; + } + n = fread(p_buf, 1u, len, fp); + fclose(fp); + return (n == len) ? 0 : -1; +} + +/* + * 函数功能:写配置文件(不存在则创建,存在则覆盖) + * 入口参数:p_buf - 待写数据(不可为 NULL) const uint8_t * + * len - 数据长度 size_t + * 返回值:0 - 成功;-1 - 写失败 int + * 限定条件:无 + */ +int net_cfg_store_write(const uint8_t *p_buf, size_t len) { + FILE *fp; + size_t n; + + if (p_buf == NULL) { + return -1; + } + fp = fopen(s_path, "wb"); + if (fp == NULL) { + return -1; + } + n = fwrite(p_buf, 1u, len, fp); + fclose(fp); + return (n == len) ? 0 : -1; +} diff --git a/Drivers/BSP/UI/ui_content_page.c b/Drivers/BSP/UI/ui_content_page.c index 6fa95393..9d158466 100644 --- a/Drivers/BSP/UI/ui_content_page.c +++ b/Drivers/BSP/UI/ui_content_page.c @@ -16,6 +16,14 @@ * 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入 * ui_page_xxx 模块,本文件只留机制 + 描述符表;绘制/按键改 * 按描述符驱动(静态表/refresh + 4 钩子),行为零变化 + * 2026-09-09 王建锋 密码页(用户密钥):绘制链支持每页 title_y/"浅 V"/页脚开关; + * 按键分发支持每页 ← 拦截(ESC 恒不可拦截);新 API + * ui_content_page_invalidate(表单页触发整页重绘) + * 2026-09-09 王建锋 权限模型:open(id, perm) 记录授予权限(页面经 + * ui_content_page_is_editable 查询:edit_capable 且修改权限) + * 2026-09-10 王建锋 网口设置页(node 30):新增 CONTENT_PAGE_NET_CFG 入描述符表; + * 新 API ui_content_page_selected_row(页面钩子感知选中行)+ + * 描述符 init_cb(开页后调一次,页面模块重置运行态) */ /* 头文件包含区 */ @@ -29,6 +37,12 @@ #include "ui_page_dc.h" +#include "ui_page_password.h" + +#include "ui_page_net_cfg.h" + +#include "ui_pwd_def.h" + #include "ui_menu_view.h" #include "ui_deco.h" @@ -63,10 +77,13 @@ static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = { [CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc, [CONTENT_PAGE_AC] = &ui_page_ac_desc, [CONTENT_PAGE_DC] = &ui_page_dc_desc, + [CONTENT_PAGE_PASSWORD] = &ui_page_password_desc, + [CONTENT_PAGE_NET_CFG] = &ui_page_net_cfg_desc, }; static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */ static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续) */ +static uint8_t s_perm; /* 本次授予权限(ui_perm_t;开页时装载,关页重置) */ static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */ static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */ @@ -121,6 +138,19 @@ static uint16_t cp_row_total(const content_page_def_t *d) { return d->row_count; } +/* + * 函数功能:失效当前打开的页面容器(触发整页重绘) + * 入口参数:无 + * 返回值:无 + * 限定条件:页面打开时(s_cont 非空) + * 函数说明:供页面模块(表单/操作页)自身状态变化后调用;无页打开时空操作 + */ +void ui_content_page_invalidate(void) { + if (s_cont != NULL) { + lv_obj_invalidate(s_cont); + } +} + /* * 函数功能:绘制居中单行文本(标题/副标题) * 入口参数:layer - 绘制层 lv_layer_t * @@ -168,19 +198,22 @@ static void cp_draw_cb(lv_event_t *e) { const content_page_def_t *d = s_defs[s_open]; lv_layer_t *layer = lv_event_get_layer(e); - /* "浅 V"装饰:共享 ui_deco 模块(与菜单根框同几何) */ - ui_deco_draw(layer, &layer->_clip_area); + /* "浅 V"装饰:共享 ui_deco 模块(与菜单根框同几何);每页可关(密码页跟随实机:无装饰) */ + if (d->hide_deco == 0u) { + ui_deco_draw(layer, &layer->_clip_area); + } - /* 标题(居中) */ + /* 标题(居中;放置 y 每页可配,0 = 框架默认) */ { + int32_t ty = (d->title_y != 0) ? d->title_y : CP_TITLE_Y; lv_area_t t; t.x1 = 0; t.x2 = CP_SCREEN_W - 1; - t.y1 = CP_TITLE_Y; - t.y2 = CP_TITLE_Y + 15; + t.y1 = ty; + t.y2 = ty + 15; if (cp_areas_intersect(&t, &layer->_clip_area)) { - cp_draw_centered_text(layer, d->title, CP_TITLE_Y); + cp_draw_centered_text(layer, d->title, ty); } } @@ -284,7 +317,7 @@ static void cp_draw_cb(lv_event_t *e) { } } - /* 页脚"第 X 页,共 Y 页":列表页随选中行现算,信息页按参考图固定值 */ + /* 页脚"第 X 页,共 Y 页":列表页随选中行现算,信息页按参考图固定值;每页可关(密码页跟随实机) */ { char buf[32]; uint8_t cur; @@ -304,7 +337,7 @@ static void cp_draw_cb(lv_event_t *e) { t.x2 = CP_SCREEN_W - 1; t.y1 = CP_PAGE_LINE_Y; t.y2 = CP_PAGE_LINE_Y + 15; - if (cp_areas_intersect(&t, &layer->_clip_area)) { + if (d->hide_footer == 0u && cp_areas_intersect(&t, &layer->_clip_area)) { /* lv_snprintf 而非 snprintf:MSVC 对 printf 族做格式串扫描,中文字面量会误报 C4819 */ lv_snprintf(buf, sizeof(buf), "第 %u 页,共 %u 页", (unsigned)cur, (unsigned)pages); @@ -332,22 +365,33 @@ static void cp_draw_cb(lv_event_t *e) { * 入口参数:e - 事件指针 lv_event_t * * 返回值:无 * 限定条件:容器已加入默认输入组并获焦 - * 函数说明: 1. ESC/←:恒关页返回菜单(框架保证,页面钩子不可拦截) - * 2. 其余键:key_cb 钩子先调(返回 true = 页已处理,框架跳过内置) - * 3. 内置:↑/↓ 列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘; + * 函数说明: 1. ESC:恒关页返回菜单(框架保证,页面钩子不可拦截) + * 2. ←:页面置拦截标志(key_intercept)时先交 key_cb(true = 页已处理不关页), + * 否则关页返回(密码页光标 ←/→ 即由此处理) + * 3. 其余键:key_cb 钩子先调(返回 true = 页已处理,框架跳过内置) + * 4. 内置:↑/↓ 列表页移选(首尾环绕,跨页连续),窗口随页切换后重绘; * 信息页不响应;动态行数可能缩小,选中行越界时重置回首行 */ static void cp_key_cb(lv_event_t *e) { const content_page_def_t *d = s_defs[s_open]; lv_key_t key = (lv_key_t)lv_event_get_key(e); - /* ESC/←:关闭页面返回菜单屏(焦点/导航状态还原) */ - if (key == LV_KEY_ESC || key == LV_KEY_LEFT) { + /* ESC:关闭页面返回菜单屏(焦点/导航状态还原;恒不可拦截) */ + if (key == LV_KEY_ESC) { ui_content_page_close(); return; } - /* 页面钩子先调:true = 页已处理,框架跳过内置语义 */ + /* ←:页面置拦截标志且钩子已处理则不关页(密码页光标 ←/→);否则照旧返回 */ + if (key == LV_KEY_LEFT) { + if (d->key_intercept != 0u && d->key_cb != NULL && d->key_cb(key)) { + return; + } + ui_content_page_close(); + return; + } + + /* 其余键:页面钩子先调:true = 页已处理,框架跳过内置语义 */ if (d->key_cb != NULL && d->key_cb(key)) { return; } @@ -385,14 +429,16 @@ static void cp_key_cb(lv_event_t *e) { /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) - * 入口参数:id - 内容页 ID content_page_id_t + * 入口参数:id - 内容页 ID content_page_id_t + * perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY) * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. lv_obj_create(NULL) 新建独立屏幕 + 全屏容器(自绘载体,无子对象) * 2. 容器入默认输入组并获焦后再切屏——按键立即由本模块接管 * 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原 + * 4. 记录授予权限(页面经 ui_content_page_is_editable 查询) */ -bool ui_content_page_open(content_page_id_t id) { +bool ui_content_page_open(content_page_id_t id, uint8_t perm) { if (id >= CONTENT_PAGE_COUNT || s_open != CONTENT_PAGE_COUNT) { return false; /* ID 无效或已有页面打开(防御) */ } @@ -403,6 +449,8 @@ bool ui_content_page_open(content_page_id_t id) { return false; /* 枚举/描述符表漂移:拒开并留痕,不崩溃 */ } + s_perm = perm; /* 本次授予权限(页面可编辑性 = edit_capable 且修改权限) */ + s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕 */ if (s_scr == NULL) { return false; /* LVGL 池耗尽 */ @@ -424,6 +472,10 @@ bool ui_content_page_open(content_page_id_t id) { s_open = id; s_sel = 0u; /* 每次进入从首行开始(页面按需建毁,不跨次保留) */ + if (d->init_cb != NULL) { + d->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */ + } + /* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */ lv_group_t *g = lv_group_get_default(); if (g != NULL) { @@ -464,6 +516,7 @@ void ui_content_page_close(void) { s_cont = NULL; s_open = CONTENT_PAGE_COUNT; s_sel = 0u; + s_perm = (uint8_t)UI_PERM_VIEW; /* 权限重置(卫生:不留上次进入的授权) */ } /* @@ -476,3 +529,34 @@ void ui_content_page_close(void) { bool ui_content_page_is_open(void) { return (s_open != CONTENT_PAGE_COUNT); } + +/* + * 函数功能:查询当前打开的页面是否可编辑 + * 入口参数:无 + * 返回值:true - 页面支持编辑模式(edit_capable)且被授予修改权限;false - 其余 + * 限定条件:无 + * 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 open 的 perm 实参授予 + */ +bool ui_content_page_is_editable(void) { + if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) { + return false; /* 无页打开或无描述符(防御) */ + } + return (s_defs[s_open]->edit_capable != 0u && s_perm == (uint8_t)UI_PERM_MODIFY); +} + +/* + * 函数功能:查询当前列表页的选中行(全局下标,跨页连续) + * 入口参数:无 + * 返回值:选中行下标;无页打开/非列表页 = 0 uint16_t + * 限定条件:无 + * 函数说明:供页面钩子(key_cb/draw_cb)感知当前选中位置 + */ +uint16_t ui_content_page_selected_row(void) { + if (s_open == CONTENT_PAGE_COUNT || s_defs[s_open] == NULL) { + return 0u; + } + if (s_defs[s_open]->selectable == 0u) { + return 0u; /* 信息页/表单页无行选中概念 */ + } + return s_sel; +} diff --git a/Drivers/BSP/UI/ui_content_page.h b/Drivers/BSP/UI/ui_content_page.h index 7c55fb5d..98383bfe 100644 --- a/Drivers/BSP/UI/ui_content_page.h +++ b/Drivers/BSP/UI/ui_content_page.h @@ -16,6 +16,13 @@ * 2026-09-09 王建锋 拆分为"框架 + 页面插件"模型(内容页架构.md):三页数据移入 * ui_page_xxx 模块;本文件只留描述类型与 API;描述符新增 * 4 个扩展钩子(refresh/count_cb/draw_cb/key_cb) + * 2026-09-09 王建锋 密码页(用户密钥):描述符新增 title_y/hide_deco/hide_footer/key_intercept + * 4 字段 + 新 API ui_content_page_invalidate;← 键可每页拦截 + * (ESC 恒不可拦截) + * 2026-09-09 王建锋 权限模型:open() 增授予权限实参(ui_perm_t,逻辑层按 + * 是否过密码页/所输密码决定);描述符新增 edit_capable(页面 + * 是否支持编辑模式)+ 新 API ui_content_page_is_editable + * (页面钩子据以决定编辑行为:edit_capable 且修改权限) */ #ifndef UI_CONTENT_PAGE_H #define UI_CONTENT_PAGE_H @@ -33,6 +40,8 @@ typedef enum { CONTENT_PAGE_DEVICE_INFO = 0u, /* 装置信息(node 1):静态键值行 + 副标题,单页 */ CONTENT_PAGE_AC, /* 交流量(node 3):数据列表,24 项 / 8 项每页 */ CONTENT_PAGE_DC, /* 直流量(node 4):数据列表,3 项单页 */ + CONTENT_PAGE_PASSWORD, /* 用户密钥(密码页):3 位密码,表单/操作页首个实例 */ + CONTENT_PAGE_NET_CFG, /* 网口设置(node 30):地址列表 + 保存行,可编辑(修改权限) */ CONTENT_PAGE_COUNT /* 哨兵:无效 ID / 未打开 */ } content_page_id_t; @@ -50,7 +59,12 @@ typedef struct { const char *title; /* 标题(浅 V 区居中) */ const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */ int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */ + int32_t title_y; /* 标题放置 y;0 = 框架默认(CP_TITLE_Y) */ + uint8_t hide_deco; /* 1 = 不绘"浅 V"装饰(密码页跟随实机) */ + uint8_t hide_footer; /* 1 = 不绘页脚页码(密码页跟随实机:无页脚) */ uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */ + uint8_t key_intercept; /* 1 = LV_KEY_LEFT 先交 key_cb(true = 页已处理不关页);ESC 恒不可拦截 */ + uint8_t edit_capable; /* 1 = 本页支持编辑模式(可编辑还须 UI_PERM_MODIFY 权限) */ int32_t top_y; /* 首行槽顶 y */ int32_t pitch; /* 行距(相邻行槽顶间距) */ int32_t band_h; /* 选中白带高(0 = 无选中带) */ @@ -63,22 +77,26 @@ typedef struct { uint8_t fixed_cur_page; /* 信息页页脚"第 X 页"固定显示值 */ uint8_t fixed_total_pages; /* 信息页页脚"共 Y 页"固定显示值 */ /* —— 扩展钩子(全可选;NULL = 纯内置行为;均运行在 uiTask 上下文)—— */ - void (*refresh)(uint16_t row, char *name, char *value); /* 动态行:逐行取数(名/值) */ - uint16_t (*count_cb)(void); /* 动态行数(记录页) */ - void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */ - bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */ + void (*init_cb)(void); /* 开页后调一次(页面模块重置运行态) */ + void (*refresh)(uint16_t row, char *name, char *value); /* 动态行:逐行取数(名/值) */ + uint16_t (*count_cb)(void); /* 动态行数(记录页) */ + void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */ + bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */ } content_page_def_t; /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) - * 入口参数:id - 内容页 ID content_page_id_t + * 入口参数:id - 内容页 ID content_page_id_t + * perm - 本次进入授予的权限 ui_perm_t(UI_PERM_VIEW/MODIFY; + * 由逻辑层按是否过密码页/所输密码决定,页面经 + * ui_content_page_is_editable 查询) * 返回值:true - 成功;false - 失败(ID 无效/已有页面打开/无描述符/对象创建失败) * 限定条件:ui_menu_init() 已完成(默认输入组与菜单屏存在) * 函数说明:1. 新建独立屏幕,全屏容器自绘全部内容(无子对象,池开销极小) * 2. 容器入默认输入组并获焦——按键由本模块处理(↑↓移选/ESC返回/页面钩子) * 3. 导航状态不压层:菜单 s_depth/s_sel 原样保留,ESC 返回即还原 */ -bool ui_content_page_open(content_page_id_t id); +bool ui_content_page_open(content_page_id_t id, uint8_t perm); /* * 函数功能:关闭当前内容页(焦点回菜单容器 + 切回菜单屏 + 销毁页面) @@ -89,6 +107,15 @@ bool ui_content_page_open(content_page_id_t id); */ void ui_content_page_close(void); +/* + * 函数功能:失效当前打开的页面容器(触发整页重绘) + * 入口参数:无 + * 返回值:无 + * 限定条件:有页面打开时生效 + * 函数说明:供页面模块(表单/操作页)自身状态变化后调用;无页打开时空操作 + */ +void ui_content_page_invalidate(void); + /* * 函数功能:查询是否有内容页打开 * 入口参数:无 @@ -98,4 +125,25 @@ void ui_content_page_close(void); */ bool ui_content_page_is_open(void); +/* + * 函数功能:查询当前打开的页面是否可编辑 + * 入口参数:无 + * 返回值:true - 页面支持编辑模式且被授予修改权限;false - 其余(无页打开/ + * 页面不支持编辑/仅只读权限) + * 限定条件:无 + * 函数说明:供页面钩子(key_cb/draw_cb)决定编辑行为;权限由 ui_content_page_open + * 的 perm 实参授予(逻辑层按密码页结果/无密码默认只读传入) + */ +bool ui_content_page_is_editable(void); + +/* + * 函数功能:查询当前列表页的选中行(全局下标,跨页连续) + * 入口参数:无 + * 返回值:选中行下标;无页打开/非列表页 = 0 uint16_t + * 限定条件:无 + * 函数说明:供页面钩子(key_cb/draw_cb)感知当前选中位置(编辑模式以 + * 选中行作用、绘制钩子定位选中行带) + */ +uint16_t ui_content_page_selected_row(void); + #endif /* UI_CONTENT_PAGE_H */ diff --git a/Drivers/BSP/UI/ui_font_cn_12.c b/Drivers/BSP/UI/ui_font_cn_12.c index 2d6cd1c5..5432eb8a 100644 --- a/Drivers/BSP/UI/ui_font_cn_12.c +++ b/Drivers/BSP/UI/ui_font_cn_12.c @@ -1,7 +1,7 @@ /******************************************************************************* * Size: 12 px * Bpp: 1 - * Opts: --font C:\Users\ADMINI~1\AppData\Local\Temp\simsun_regular.ttf --size 12 --bpp 1 -r 0x20-0x7E --symbols ▶三主事交作保信值制前动区压参双口启告型复字定实式强归当录息手抖护报据控操故数整无时板查检模死波流测清点界直看类系维网置自行装警讯记设询软运通遥量钟防除面:馈线化终端号件版硬校验程序日期第页共, --no-compress --format lvgl --lv-font-name ui_font_cn_12 -o D:\Code\DTU Code\STM32F4-Base\Drivers\BSP\UI\ui_font_cn_12.c + * Opts: --font C:\Users\ADMINI~1\AppData\Local\Temp\simsun_regular.ttf --size 12 --bpp 1 -r 0x20-0x7E --symbols ▶三主事交件作保信值共关制前动化区压参双口号启告地址型复子字存定实密序式强归当录息户手抖护报据控掩操故数整无日时期本板查校检模死波流测清点版用界直看码硬程端第类系线终维网置自行装警认讯记设询软运通遥量钟钥防除面页馈验默,: --no-compress --format lvgl --lv-font-name ui_font_cn_12 -o D:\Code\DTU Code\STM32F4-Base\Drivers\BSP\UI\ui_font_cn_12.c ******************************************************************************/ #ifdef LV_LVGL_H_INCLUDE_SIMPLE @@ -357,6 +357,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x4, 0x47, 0xff, 0x0, 0x2, 0x20, 0x82, 0x20, 0x20, + /* U+5173 "关" */ + 0x20, 0x82, 0x20, 0x0, 0x3f, 0xe0, 0x40, 0x8, + 0x3f, 0xf8, 0x20, 0xa, 0x2, 0x20, 0x82, 0x60, + 0x30, + /* U+5236 "制" */ 0x10, 0x2a, 0x5, 0xf4, 0xc8, 0x9f, 0xf2, 0x22, 0x5f, 0x4a, 0xa9, 0x55, 0x2a, 0x85, 0x70, 0x88, @@ -413,6 +418,16 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xc0, 0x3, 0xfe, 0x40, 0x48, 0x9, 0xff, 0x20, 0x20, + /* U+5730 "地" */ + 0x21, 0x4, 0x20, 0x95, 0x92, 0xdf, 0x72, 0x5a, + 0x49, 0x49, 0x2b, 0x25, 0x6, 0x87, 0x10, 0x81, + 0xf0, + + /* U+5740 "址" */ + 0x20, 0x84, 0x10, 0x82, 0x12, 0x4f, 0x4e, 0x49, + 0x9, 0x21, 0x24, 0x24, 0x86, 0x93, 0x12, 0x7, + 0xf0, + /* U+578B "型" */ 0x0, 0x2f, 0xd4, 0x92, 0x92, 0x5f, 0xea, 0x49, 0x51, 0xc, 0x3, 0x4, 0xf, 0xf8, 0x10, 0x7f, @@ -423,11 +438,20 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x8f, 0xf0, 0x80, 0x3f, 0xca, 0x10, 0x3c, 0x38, 0x70, + /* U+5B50 "子" */ + 0x3f, 0x80, 0x20, 0x8, 0x2, 0x0, 0x41, 0xff, + 0xc1, 0x0, 0x20, 0x4, 0x0, 0x80, 0x70, 0x0, + /* U+5B57 "字" */ 0x8, 0x0, 0x83, 0xff, 0xc0, 0x10, 0x0, 0x7f, 0x0, 0x40, 0x10, 0xff, 0xe0, 0x80, 0x10, 0xe, 0x0, + /* U+5B58 "存" */ + 0x8, 0x1, 0x3, 0xff, 0x88, 0x2, 0x7c, 0x41, + 0x18, 0x45, 0x7f, 0x21, 0x4, 0x20, 0x84, 0x11, + 0x80, + /* U+5B9A "定" */ 0x8, 0x0, 0x83, 0xff, 0xc0, 0x10, 0x1, 0xff, 0xc1, 0x1, 0x20, 0x27, 0xc4, 0x81, 0x50, 0x47, @@ -438,6 +462,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x4, 0x87, 0xff, 0x4, 0x1, 0x30, 0xc1, 0x60, 0x10, + /* U+5BC6 "密" */ + 0x4, 0x1f, 0xfe, 0x20, 0x82, 0x85, 0x24, 0xa9, + 0x63, 0xe9, 0x80, 0x4, 0x8, 0x89, 0x11, 0x3f, + 0xe0, + /* U+5E8F "序" */ 0x2, 0xf, 0xfd, 0x0, 0x2f, 0xe4, 0x8, 0x9a, 0x10, 0x83, 0xff, 0x42, 0x28, 0x49, 0x8, 0x43, @@ -472,6 +501,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x90, 0x13, 0xfe, 0x4, 0xa, 0x49, 0x42, 0xc7, 0xd0, + /* U+6237 "户" */ + 0x4, 0x0, 0x40, 0xff, 0x90, 0x12, 0x2, 0x40, + 0x4f, 0xf9, 0x1, 0x20, 0x8, 0x1, 0x0, 0x40, + 0x0, + /* U+624B "手" */ 0x3, 0xcf, 0x80, 0x10, 0x2, 0x7, 0xfc, 0x8, 0x1, 0x7, 0xff, 0x4, 0x0, 0x80, 0x10, 0xe, @@ -502,6 +536,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x9a, 0xe, 0x3e, 0x41, 0x8, 0x21, 0x4, 0x67, 0xf0, + /* U+63A9 "掩" */ + 0x21, 0x5, 0xfc, 0x8a, 0x7a, 0x22, 0x92, 0x4f, + 0x8d, 0x57, 0x3e, 0x25, 0x44, 0xf8, 0x94, 0xb0, + 0x70, + /* U+64CD "操" */ 0x27, 0xc4, 0x88, 0x9f, 0x78, 0x2, 0xee, 0x55, 0x4f, 0xbf, 0x8, 0x2f, 0xe4, 0x70, 0x95, 0x34, @@ -540,6 +579,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x5e, 0x4a, 0x4f, 0xfd, 0x20, 0x25, 0x24, 0xc3, 0x30, + /* U+672C "本" */ + 0x4, 0x0, 0x83, 0xff, 0x82, 0x0, 0xe0, 0x2a, + 0x5, 0x41, 0x24, 0x44, 0x57, 0xf4, 0x10, 0x2, + 0x0, + /* U+677F "板" */ 0x20, 0x64, 0xf0, 0x90, 0x7a, 0x2, 0x7e, 0xea, 0x5b, 0x4d, 0x2a, 0x25, 0x44, 0x90, 0xa5, 0x19, @@ -599,6 +643,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x51, 0x4b, 0xaa, 0x55, 0x4a, 0x91, 0x65, 0x49, 0x10, + /* U+7528 "用" */ + 0x7f, 0xd1, 0x14, 0x45, 0xff, 0x44, 0x51, 0x17, + 0xfd, 0x11, 0x44, 0x51, 0x18, 0x1c, + /* U+754C "界" */ 0x7f, 0xc8, 0x89, 0xff, 0x22, 0x27, 0xfc, 0x8, 0x6, 0xc7, 0x7, 0x11, 0x2, 0x20, 0x84, 0x20, @@ -613,6 +661,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xc8, 0x13, 0xfe, 0xa0, 0x47, 0xf8, 0x81, 0x1f, 0xe0, + /* U+7801 "码" */ + 0xf, 0xde, 0x8, 0x91, 0x12, 0x24, 0x44, 0xef, + 0xf4, 0xa, 0x81, 0x5f, 0xae, 0x5, 0x40, 0x80, + 0x60, + /* U+786C "硬" */ 0xf7, 0xe4, 0x20, 0x9f, 0x22, 0xa7, 0x7d, 0xaa, 0x95, 0xf2, 0xc8, 0x55, 0xe, 0x61, 0x53, 0x80, @@ -686,6 +739,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x82, 0x8f, 0xff, 0x0, 0xf, 0xf8, 0x82, 0x1f, 0xc0, + /* U+8BA4 "认" */ + 0x41, 0x4, 0x20, 0x4, 0x0, 0x8e, 0x10, 0x42, + 0x8, 0xa1, 0x14, 0x2a, 0x86, 0x88, 0x91, 0x4, + 0x10, + /* U+8BAF "讯" */ 0x40, 0x5, 0xf0, 0x12, 0x2, 0x4e, 0x48, 0x5f, 0x9, 0x21, 0x24, 0x24, 0x86, 0x94, 0x91, 0x82, @@ -736,6 +794,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x48, 0xaf, 0xdf, 0x22, 0xa5, 0x10, 0xc2, 0x10, 0x40, + /* U+94A5 "钥" */ + 0x40, 0x8, 0x7d, 0xe8, 0xc1, 0x17, 0xbe, 0x44, + 0x48, 0x8f, 0xdf, 0x22, 0x25, 0x44, 0xc8, 0x92, + 0x70, + /* U+9632 "防" */ 0x2, 0x1e, 0x22, 0x7f, 0xc9, 0xa, 0x21, 0x47, 0xe4, 0x8c, 0x91, 0x92, 0x3c, 0x86, 0x10, 0xc4, @@ -765,6 +828,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xbc, 0x0, 0x92, 0x39, 0x5a, 0xa8, 0x52, 0x37, 0xf0, + /* U+9ED8 "默" */ + 0xfc, 0x92, 0x9b, 0x72, 0xca, 0x4f, 0xde, 0x21, + 0x3f, 0x20, 0x84, 0x1d, 0x5c, 0x29, 0x55, 0x55, + 0x10, + /* U+FF0C "," */ 0xf6, @@ -772,7 +840,10 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0xf0, 0xf0, /* U+25B6 "▶"(来源:Segoe UI Symbol;宋体无此字形) */ - 0x80, 0xe0, 0xf0, 0xfc, 0xff, 0xfe, 0xf8, 0xf0, 0xc0, 0x00 + 0x80, 0xe0, 0xf0, 0xfc, 0xff, 0xfe, 0xf8, 0xf0, 0xc0, 0x00, + + /* U+FF0C ','(来源:宋体迷你字体并入;lv_font_conv 丢低码位符号) */ + 0xf6 }; @@ -891,12 +962,12 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 672, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 689, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 723, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 737, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 753, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 770, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 786, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, - {.bitmap_index = 798, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 723, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 740, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 754, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 770, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 787, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 803, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, {.bitmap_index = 815, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 832, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 849, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -904,33 +975,33 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 883, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 900, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 917, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 934, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 951, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 968, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 985, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1002, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1016, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1033, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1050, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1067, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1084, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1101, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1118, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1135, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1152, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1169, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1186, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1203, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1220, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1236, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1249, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1266, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1283, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1300, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1317, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1334, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1351, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1368, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 934, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 950, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 967, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 984, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1001, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1018, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1035, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1052, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1069, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1086, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1103, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1117, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1134, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1151, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1168, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1185, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1202, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1219, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1236, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1253, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1270, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1287, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1304, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1321, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1338, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1355, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1371, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 1384, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1401, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1418, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -938,42 +1009,57 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1452, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1469, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1486, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1503, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1518, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1535, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1551, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1568, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1585, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1602, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1619, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1636, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1653, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1670, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1687, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1704, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1721, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1735, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1752, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1769, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1786, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1803, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1820, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1837, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1854, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1871, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1888, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1905, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1922, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1939, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1956, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1973, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1990, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2006, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2023, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2040, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2057, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 2058, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 2060, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ + {.bitmap_index = 1503, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1520, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1536, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1553, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1570, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1587, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1604, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1621, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1638, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1652, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1669, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1684, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1701, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1718, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1734, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1751, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1768, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1785, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1802, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1819, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1836, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1853, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1870, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1887, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1904, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1918, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1935, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1952, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1969, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1986, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2003, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2020, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2037, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2054, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2071, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2088, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2105, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2122, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2139, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2156, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2173, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2190, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2207, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2223, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2240, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2257, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2274, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2291, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 2292, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 2294, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */, + {.bitmap_index = 2304, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0} /* U+FF0C */ }; /*--------------------- @@ -982,17 +1068,19 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { static const uint16_t unicode_list_1[] = { 0x0, 0x32, 0x82, 0x9b, 0xed, 0x153, 0x1d4, 0x1d8, - 0x233, 0x368, 0x42d, 0x444, 0x49f, 0x50d, 0x531, 0x582, - 0x5b9, 0x5c3, 0x5da, 0x5ee, 0x626, 0x641, 0x982, 0xb04, - 0xd4e, 0xd91, 0xd95, 0x1086, 0x1106, 0x1131, 0x1149, 0x114a, - 0x114c, 0x1266, 0x1442, 0x148d, 0x149b, 0x149c, 0x1565, 0x159e, + 0x233, 0x368, 0x36a, 0x42d, 0x444, 0x49f, 0x50d, 0x531, + 0x582, 0x5b9, 0x5c3, 0x5da, 0x5ee, 0x626, 0x641, 0x927, + 0x937, 0x982, 0xb04, 0xd47, 0xd4e, 0xd4f, 0xd91, 0xd95, + 0xdbd, 0x1086, 0x1106, 0x1131, 0x1149, 0x114a, 0x114c, 0x1266, + 0x142e, 0x1442, 0x148d, 0x149b, 0x149c, 0x1565, 0x159e, 0x15a0, 0x16c4, 0x173c, 0x1767, 0x176b, 0x17d7, 0x17dc, 0x17ed, 0x1916, - 0x1976, 0x19dc, 0x1a18, 0x1ab7, 0x1c18, 0x1d72, 0x1ed9, 0x1f38, - 0x1f42, 0x1ffc, 0x22b0, 0x243f, 0x2743, 0x28eb, 0x2902, 0x2a63, - 0x2c02, 0x2ce6, 0x2d23, 0x2e72, 0x2ef2, 0x30b6, 0x30bf, 0x30eb, - 0x3148, 0x3165, 0x33e1, 0x3a43, 0x3abc, 0x3d5d, 0x3da6, 0x3da7, - 0x3db5, 0x3dd9, 0x4166, 0x41c7, 0x4211, 0x425c, 0x43c6, 0x4696, - 0x4829, 0x485b, 0x4959, 0x4a6c, 0x4b7f, 0x4c83, 0xb103, 0xb111 + 0x1923, 0x1976, 0x19dc, 0x1a18, 0x1ab7, 0x1c18, 0x1d72, 0x1ed9, + 0x1f38, 0x1f42, 0x1ffc, 0x22b0, 0x243f, 0x271f, 0x2743, 0x28eb, + 0x2902, 0x29f8, 0x2a63, 0x2c02, 0x2ce6, 0x2d23, 0x2e72, 0x2ef2, + 0x30b6, 0x30bf, 0x30eb, 0x3148, 0x3165, 0x33e1, 0x3a43, 0x3abc, + 0x3d5d, 0x3d9b, 0x3da6, 0x3da7, 0x3db5, 0x3dd9, 0x4166, 0x41c7, + 0x4211, 0x425c, 0x43c6, 0x4696, 0x469c, 0x4829, 0x485b, 0x4959, + 0x4a6c, 0x4b7f, 0x4c83, 0x50cf, 0xb103, 0xb111 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1004,10 +1092,14 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = }, { .range_start = 19977, .range_length = 45330, .glyph_id_start = 96, - .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 96, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 110, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY }, { - .range_start = 9654, .range_length = 1, .glyph_id_start = 192, + .range_start = 9654, .range_length = 1, .glyph_id_start = 206, + .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY + }, + { + .range_start = 65292, .range_length = 1, .glyph_id_start = 207, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY } }; @@ -1033,7 +1125,7 @@ static lv_font_fmt_txt_dsc_t font_dsc = { .cmaps = cmaps, .kern_dsc = NULL, .kern_scale = 0, - .cmap_num = 3, + .cmap_num = 4, .bpp = 1, .kern_classes = 0, .bitmap_format = 0, diff --git a/Drivers/BSP/UI/ui_menu.c b/Drivers/BSP/UI/ui_menu.c index c4290e2d..b960b325 100644 --- a/Drivers/BSP/UI/ui_menu.c +++ b/Drivers/BSP/UI/ui_menu.c @@ -12,6 +12,14 @@ * 2026-09-08 王建锋 按代码规范 V1.0 补全分区注释与关键行注释;函数定义括号改 K&R 风格(同行) * 2026-09-08 王建锋 4 层容器→单一持久容器(焦点/键唯一入口;键回调深度改取 * s_depth,去掉 user_data=depth;container/focus 调用去参) + * 2026-09-09 王建锋 密码页路由:需密码叶子(ui_menu_tab_has_pwd)ENTER + * → 先密码页;确认一致由 menu_password_ok 续做原"进入"动作 + * (内容屏/子框) + * 2026-09-09 王建锋 权限模型:密码检查提到内容页判断之前(带内容页的受保护 + * 叶子也先经密码页,修复"内容页叶子绕过密码"路由缺口);表 pwd + * 列改布尔"需密码"(行尾 #P);密码页按所输密码授权(查看→只读 + * / 修改→可修改),授权经回调随开页传给内容页(页面经 + * is_editable 判编辑);无密码叶子直接进、默认只读 */ /* 头文件包含区 */ @@ -23,8 +31,12 @@ #include "ui_content_page.h" +#include "ui_page_password.h" + #include "ui_menu_tab.h" +#include "ui_pwd_def.h" + #include "lvgl.h" #define DBG_TAG "[UI_MENU]" @@ -95,15 +107,54 @@ static void menu_back(void) { * 入口参数:node - 菜单节点下标 uint8_t * 返回值:内容页 ID content_page_id_t;无内容页 = CONTENT_PAGE_COUNT * 限定条件:无 - * 函数说明:仅部分叶子有内容页(参考图 10/20/21);下标对应 ui_menu_tab.c - * node 1=装置信息、3=交流量、4=直流量(表序变更时须同步本映射) + * 函数说明:仅部分叶子有内容页(参考图 10/20/21 + 网口设置);下标对应 + * ui_menu_tab.c + * node 1=装置信息、3=交流量、4=直流量、30=网口设置(表序变更时须同步本映射) */ static content_page_id_t menu_content_page_for(uint8_t node) { switch (node) { - case 1u: return CONTENT_PAGE_DEVICE_INFO; /* 装置信息(L1 叶子) */ - case 3u: return CONTENT_PAGE_AC; /* 交流量(实时数据子项) */ - case 4u: return CONTENT_PAGE_DC; /* 直流量(实时数据子项) */ - default: return (content_page_id_t)CONTENT_PAGE_COUNT; /* 无内容页 */ + case 1u: return CONTENT_PAGE_DEVICE_INFO; /* 装置信息(L1 叶子) */ + case 3u: return CONTENT_PAGE_AC; /* 交流量(实时数据子项) */ + case 4u: return CONTENT_PAGE_DC; /* 直流量(实时数据子项) */ + case 30u: return CONTENT_PAGE_NET_CFG; /* 网口设置(通讯参数子项,需密码) */ + default: return (content_page_id_t)CONTENT_PAGE_COUNT; /* 无内容页 */ + } +} + +/* + * 函数功能:密码页确认一致回调(预装进密码页模块;uiTask 上下文调用) + * 入口参数:perm - 密码页按所输密码授予的权限 ui_perm_t(UI_PERM_MODIFY/UI_PERM_VIEW) + * 返回值:无 + * 限定条件:密码页打开中;导航状态未变(选中条目仍为该受保护叶子) + * 函数说明: 1. 先关密码页(焦点回菜单容器、切回菜单屏;与 ESC 路径同模式, + * 事件内删对象由 LVGL e->deleted 机制保证安全) + * 2. 再沿未变的导航状态续做"进入"动作:有内容页 → 开内容页(携授予的 + * 权限;页面经 is_editable 判只读/可编辑);否则压层进子框 + * (叶子 → 空白终点框,权限不涉及) + * 3. 防御:状态异常 / 最深层无子层可压时仅停在菜单屏(页已关) + */ +static void menu_password_ok(uint8_t perm) { + ui_content_page_close(); /* 先关密码页回菜单屏,后续动作才不会落在被盖住的菜单屏上 */ + + uint8_t depth = s_depth; + uint8_t kids[MENU_ITEM_MAX]; + uint8_t cnt = ui_menu_tab_children(s_path[depth], kids, MENU_ITEM_MAX); + + if (cnt == 0u || s_sel[depth] >= cnt) { + return; /* 防御:状态异常,页已关,停在菜单屏 */ + } + + { + uint8_t child = kids[s_sel[depth]]; + content_page_id_t pid = menu_content_page_for(child); + + if (pid < CONTENT_PAGE_COUNT) { + ui_content_page_open(pid, perm); /* 页已关,守卫可过:开目标内容页(携权限) */ + } + else if (depth < MENU_PAGE_MAX - 1u) { + menu_open(child); /* 压层:建新框 + 焦点移到新框(叶子 → 空白终点框) */ + } + /* 最深层无子层可压:页已关,停在菜单屏 */ } } @@ -114,7 +165,10 @@ static content_page_id_t menu_content_page_for(uint8_t node) { * 返回值:无 * 限定条件:ui_menu_init() 已完成 * 函数说明: 1. UP/DOWN:移选(首尾环绕);选中越出可见区由显示层跟随滚动 - * 2. ENTER/RIGHT:叶子有内容页 → 切内容屏(不压层);否则进子框(叶子 → 空白终点框) + * 2. ENTER/RIGHT:需密码叶子(含带内容页者)→ 密码页(授权按所输 + * 密码:查看→只读 / 修改→可修改,menu_password_ok 携权续进入); + * 否则叶子有内容页 → 切内容屏(不压层,默认只读); + * 再否则进子框(叶子 → 空白终点框) * 3. ESC/LEFT:关最右框(根框不响应) * 4. 空框(终点页 0 条目)只响应返回 */ @@ -141,14 +195,21 @@ static void menu_key_handle(uint8_t depth, lv_key_t key) { case LV_KEY_RIGHT: { /* 进子框:空框无条目可进;最深层(depth=MAX-1)无子层可再开 */ if (cnt > 0u) { - content_page_id_t pid = menu_content_page_for(kids[s_sel[depth]]); + uint8_t child = kids[s_sel[depth]]; + content_page_id_t pid = menu_content_page_for(child); - if (pid < CONTENT_PAGE_COUNT) { - /* 叶子有内容页:切内容屏(不压层,导航状态原样保留;ESC 返回即还原) */ - ui_content_page_open(pid); + if (ui_menu_tab_has_pwd(child)) { + /* 需密码叶子(含带内容页者):先经密码页;授权按所输密码 + (查看→只读 / 修改→可修改),回调携权续做"进入"动作 */ + ui_page_password_arm(menu_password_ok); + ui_content_page_open(CONTENT_PAGE_PASSWORD, (uint8_t)UI_PERM_VIEW); + } + else if (pid < CONTENT_PAGE_COUNT) { + /* 叶子有内容页:无密码直接进(默认只读权限) */ + ui_content_page_open(pid, (uint8_t)UI_PERM_VIEW); } else if (depth < MENU_PAGE_MAX - 1u) { - menu_open(kids[s_sel[depth]]); /* 压层:建新框 + 焦点移到新框(叶子 → 空白终点框) */ + menu_open(child); /* 压层:建新框 + 焦点移到新框(叶子 → 空白终点框) */ } } break; diff --git a/Drivers/BSP/UI/ui_menu_tab.c b/Drivers/BSP/UI/ui_menu_tab.c index 9da96230..10cf4fad 100644 --- a/Drivers/BSP/UI/ui_menu_tab.c +++ b/Drivers/BSP/UI/ui_menu_tab.c @@ -4,48 +4,48 @@ #include "ui_menu_tab.h" const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = { - { MENU_ROOT, "主界面" }, /* 0 L0 */ - { 0, "装置信息" }, /* 1 L1 */ - { 0, "实时数据" }, /* 2 L1 */ - { 2, "交流量" }, /* 3 L2 */ - { 2, "直流量" }, /* 4 L2 */ - { 2, "遥信量" }, /* 5 L2 */ - { 0, "参数定值" }, /* 6 L1 */ - { 6, "整定" }, /* 7 L2 */ - { 7, "参数" }, /* 8 L3 */ - { 7, "定值" }, /* 9 L3 */ - { 7, "控制字" }, /* 10 L3 */ - { 7, "软压板" }, /* 11 L3 */ - { 6, "查看" }, /* 12 L2 */ - { 12, "参数" }, /* 13 L3 */ - { 12, "定值" }, /* 14 L3 */ - { 12, "控制字" }, /* 15 L3 */ - { 12, "软压板" }, /* 16 L3 */ - { 0, "三遥设置" }, /* 17 L1 */ - { 17, "遥测死区" }, /* 18 L2 */ - { 17, "遥测系数" }, /* 19 L2 */ - { 17, "遥信类型" }, /* 20 L2 */ - { 17, "遥信防抖" }, /* 21 L2 */ - { 17, "双点遥信" }, /* 22 L2 */ - { 0, "装置维护" }, /* 23 L1 */ - { 23, "时钟设置" }, /* 24 L2 */ - { 23, "强制复归" }, /* 25 L2 */ - { 23, "手动录波" }, /* 26 L2 */ - { 23, "清除记录" }, /* 27 L2 */ - { 0, "通讯参数" }, /* 28 L1 */ - { 28, "通讯设置" }, /* 29 L2 */ - { 28, "网口设置" }, /* 30 L2 */ - { 28, "SNTP设置" }, /* 31 L2 */ - { 0, "记录查询" }, /* 32 L1 */ - { 32, "SOE记录" }, /* 33 L2 */ - { 32, "事故记录" }, /* 34 L2 */ - { 32, "操作记录" }, /* 35 L2 */ - { 32, "保护告警" }, /* 36 L2 */ - { 32, "保护启动" }, /* 37 L2 */ - { 32, "遥控记录" }, /* 38 L2 */ - { 32, "自检记录" }, /* 39 L2 */ - { 32, "运行记录" }, /* 40 L2 */ - { 32, "运行报告" }, /* 41 L2 */ + { MENU_ROOT, 0u, "主界面" }, /* 0 L0 */ + { 0, 0u, "装置信息" }, /* 1 L1 */ + { 0, 0u, "实时数据" }, /* 2 L1 */ + { 2, 0u, "交流量" }, /* 3 L2 */ + { 2, 0u, "直流量" }, /* 4 L2 */ + { 2, 0u, "遥信量" }, /* 5 L2 */ + { 0, 0u, "参数定值" }, /* 6 L1 */ + { 6, 0u, "整定" }, /* 7 L2 */ + { 7, 1u, "参数" }, /* 8 L3 */ + { 7, 1u, "定值" }, /* 9 L3 */ + { 7, 1u, "控制字" }, /* 10 L3 */ + { 7, 1u, "软压板" }, /* 11 L3 */ + { 6, 0u, "查看" }, /* 12 L2 */ + { 12, 0u, "参数" }, /* 13 L3 */ + { 12, 0u, "定值" }, /* 14 L3 */ + { 12, 0u, "控制字" }, /* 15 L3 */ + { 12, 0u, "软压板" }, /* 16 L3 */ + { 0, 0u, "三遥设置" }, /* 17 L1 */ + { 17, 1u, "遥测死区" }, /* 18 L2 */ + { 17, 1u, "遥测系数" }, /* 19 L2 */ + { 17, 1u, "遥信类型" }, /* 20 L2 */ + { 17, 1u, "遥信防抖" }, /* 21 L2 */ + { 17, 1u, "双点遥信" }, /* 22 L2 */ + { 0, 0u, "装置维护" }, /* 23 L1 */ + { 23, 1u, "时钟设置" }, /* 24 L2 */ + { 23, 1u, "强制复归" }, /* 25 L2 */ + { 23, 1u, "手动录波" }, /* 26 L2 */ + { 23, 1u, "清除记录" }, /* 27 L2 */ + { 0, 0u, "通讯参数" }, /* 28 L1 */ + { 28, 1u, "通讯设置" }, /* 29 L2 */ + { 28, 1u, "网口设置" }, /* 30 L2 */ + { 28, 1u, "SNTP设置" }, /* 31 L2 */ + { 0, 0u, "记录查询" }, /* 32 L1 */ + { 32, 0u, "SOE记录" }, /* 33 L2 */ + { 32, 0u, "事故记录" }, /* 34 L2 */ + { 32, 0u, "操作记录" }, /* 35 L2 */ + { 32, 0u, "保护告警" }, /* 36 L2 */ + { 32, 0u, "保护启动" }, /* 37 L2 */ + { 32, 0u, "遥控记录" }, /* 38 L2 */ + { 32, 0u, "自检记录" }, /* 39 L2 */ + { 32, 0u, "运行记录" }, /* 40 L2 */ + { 32, 0u, "运行报告" }, /* 41 L2 */ }; /* @@ -97,6 +97,18 @@ const char *ui_menu_tab_name(uint8_t node) return g_menu_tab[node].name; } +/* + * 函数功能:判断节点是否需密码(叶子密码保护) + * 入口参数:node - 节点下标 uint8_t + * 返回值:true - 需密码页(按所输密码授权查看/修改权限);false - 直接进(只读) + * 限定条件:无 + * 说明:表 pwd 列取自源 ui_menu_tree.txt 行尾 #P 注记;导航逻辑层据此决定是否先经密码页 + */ +bool ui_menu_tab_has_pwd(uint8_t node) +{ + return (g_menu_tab[node].pwd != 0u); +} + /* * 函数功能:菜单表启动自检 * 入口参数:无 @@ -114,6 +126,9 @@ bool ui_menu_tab_valid(void) if (p != MENU_ROOT && p >= UI_MENU_TAB_COUNT) { return false; } + if (g_menu_tab[i].pwd > 1u) { + return false; /* pwd 只取 0/1(行尾注记笔误捕获) */ + } } return true; } diff --git a/Drivers/BSP/UI/ui_menu_tab.h b/Drivers/BSP/UI/ui_menu_tab.h index ebb63e47..3f11cb0f 100644 --- a/Drivers/BSP/UI/ui_menu_tab.h +++ b/Drivers/BSP/UI/ui_menu_tab.h @@ -10,8 +10,10 @@ #define MENU_ROOT 0xFFu /* 根哨兵:仅根节点(主界面)的 parent 取此值;8 位,上限 255 项 */ /* 菜单节点:子项 = parent 等于本下标的所有行(按表序);有子项=菜单,无=叶子 */ +/* 字段序 parent/pwd 在前:32 位下两项 8 位合并 + 4 位指针 = 8 B/项 */ typedef struct { uint8_t parent; /* 父节点下标(0..254);根为 MENU_ROOT(0xFF) */ + uint8_t pwd; /* 1 = 需密码(密码页按所输密码授权);0 = 直接进(只读) */ const char *name; /* 显示文本(字面量,存 Flash) */ } ui_menu_node_t; @@ -29,6 +31,7 @@ extern const ui_menu_node_t g_menu_tab[]; uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max); /* 取 node 直接子项(按表序),最多 max 个;返回实际个数 */ bool ui_menu_tab_has_children(uint8_t node); /* 有子项=菜单(带▶);无=叶子 */ const char *ui_menu_tab_name(uint8_t node); /* 显示名 */ -bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效 */ +bool ui_menu_tab_has_pwd(uint8_t node); /* 需密码(密码页按所输密码授权);false = 直接进(只读) */ +bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效、pwd 为 0/1 */ #endif diff --git a/Drivers/BSP/UI/ui_page_net_cfg.c b/Drivers/BSP/UI/ui_page_net_cfg.c new file mode 100644 index 00000000..0368a9ed --- /dev/null +++ b/Drivers/BSP/UI/ui_page_net_cfg.c @@ -0,0 +1,342 @@ +/* + * 模块名称:UI Page Net Cfg (网口设置内容页) + * 模块功能:网口设置叶子节点的内容页数据 + 行为——5 行地址列表(IP 地址/子网 + * 掩码/默认网关/MAC 地址 + 保存行,5 行每页共 1 页); + * 行槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=6 / 值 x=60; + * 值取自 net_cfg RAM 副本(netTask 上电自 Flash 加载并回写实际 MAC); + * 编辑模式:ENTER 进选中地址行(字节光标 = 活动字节,反显标记), + * ↑↓调字节 0..255(环绕、值列实时刷新)、←→移字节光标(首↔末环绕)、 + * ENTER 确认退出编辑; + * 权限:edit_capable=1——查看密码 → 只读(编辑/保存键无效), + * 修改密码 → 可编辑;保存行 ENTER → net_cfg_save()(下次上电生效); + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * v1.0 2026-09-10 王建锋 创建初始版本 + */ + +/* 头文件包含区 */ +#include + +#include "ui_page_net_cfg.h" + +#include "net_cfg.h" + +/* 私有宏定义区 */ + +/* + * 布局常量(设备像素;1bpp 屏 160x160): + * 5 行共 1 页(行槽顶 y = 24+14i,末行止于 y=93,与页脚 y=143 无冲突); + * 名 x=6 / 值 x=60(值列须 ≥60:4 汉字名占 48px + 间隙;值列宽—— + * IP/掩码/网关 11 ASCII 字符 = 66px 止于 126,MAC 12 hex 字符 = 72px 止于 132) + */ +#define NC_ROW_COUNT 5u /* 4 地址行 + 1 保存行 */ +#define NC_ADDR_ROW_MAX 4u /* 地址行范围 0..3(IP/掩码/网关/MAC) */ +#define NC_SAVE_ROW 4u /* 保存行下标 */ +#define NC_TOP_Y 24 /* 首行槽顶 y */ +#define NC_PITCH 14 /* 行距 */ +#define NC_BAND_H 13 /* 选中白带高 */ +#define NC_LEFT_X 6 /* 左列 x(名) */ +#define NC_RIGHT_X 60 /* 右列 x(值) */ +#define NC_ROW_BUF_LEN 32u /* 行缓冲长度(与框架 CP_ROW_BUF_LEN 一致) */ + +/* 静态变量定义区 */ + +/* 页面运行态(每次开页经 init_cb 重置;约 3 B) */ +static uint8_t s_editing; /* 1 = 编辑中(被编辑行 = 当前选中行) */ +static uint8_t s_cursor; /* 编辑模式活动字节(IP/掩码/网关 0..3,MAC 0..5) */ +static uint8_t s_saved; /* 保存行值:0 = "ENTER",1 = "OK",2 = "ERR" */ + +/* 函数定义区 */ + +/* + * 函数功能:判断两区域是否相交(与 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 - 输出缓冲(≥ NC_ROW_BUF_LEN) char * + * 返回值:无 + * 限定条件:p_buf 非空 + * 函数说明:IP/掩码/网关 = "192.168.001.100"(每段 3 位补零定长,字节 i 恒在 + * 字符 4i 起);MAC = "A494B6123456"(无分隔 hex,字节 i 恒在字符 2i 起) + */ +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, NC_ROW_BUF_LEN, "%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, NC_ROW_BUF_LEN); + } +} + +/* + * 函数功能:动态行刷新(逐行取名/值) + * 入口参数:row - 行下标(0..4) uint16_t + * name - 名缓冲(≥ NC_ROW_BUF_LEN,框架栈缓冲) char * + * value - 值缓冲(≥ NC_ROW_BUF_LEN,框架栈缓冲) char * + * 返回值:无 + * 限定条件:页面打开(框架绘制链内逐行调用) + * 函数说明:地址行值取 net_cfg RAM 副本(MAC 全零时显示芯片实际 MAC 影子); + * 保存行值随 s_saved 态显示 ENTER/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)strcpy(name, "IP地址"); + net_cfg_fmt_ipv4(cfg->ip, value, NC_ROW_BUF_LEN); + break; + case 1u: + (void)strcpy(name, "子网掩码"); + net_cfg_fmt_ipv4(cfg->mask, value, NC_ROW_BUF_LEN); + break; + case 2u: + (void)strcpy(name, "默认网关"); + net_cfg_fmt_ipv4(cfg->gw, value, NC_ROW_BUF_LEN); + break; + case 3u: + (void)strcpy(name, "MAC地址"); + nc_fmt_value(row, value); + break; + case 4u: + (void)strcpy(name, "保存"); + (void)strcpy(value, (s_saved == 0u) ? "ENTER" : ((s_saved == 1u) ? "OK" : "ERR")); + break; + default: + break; + } +} + +/* + * 函数功能:调整被编辑地址行的活动字节(0..255 环绕,实时落 RAM 副本) + * 入口参数:row - 地址行下标(0..3) uint16_t + * delta - 调整方向(>0 = +1,<0 = -1) int8_t + * 返回值:无 + * 限定条件:编辑模式(s_cursor 有效) + * 函数说明:只改 RAM 副本;写盘须显式经保存行 net_cfg_save + */ +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[NC_ROW_BUF_LEN]; + 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) ? 2u : 4u); + seg_len = (sel == 3u) ? 2u : 3u; + + slot_y = NC_TOP_Y + (int32_t)sel * NC_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 + NC_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 = lv_color_black(); + fd.bg_opa = LV_OPA_COVER; + lv_draw_rect(layer, &fd, &box); + + { + lv_draw_label_dsc_t ld; + + lv_draw_label_dsc_init(&ld); + ld.base.layer = layer; + ld.text = value + start; + ld.text_static = 0u; /* 栈缓冲(同帧有效) */ + ld.color = lv_color_white(); + 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(需可编辑权限);↑↓ 落框架移选、 + * ← 落框架关页;只读态 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) { + s_saved = (net_cfg_save() == 0) ? 1u : 2u; + ui_content_page_invalidate(); + return true; + } + } + + return false; +} + +/* + * 函数功能:重置页面运行态 + * 入口参数:无 + * 返回值:无 + * 限定条件:框架 ui_content_page_open 内调用(开页后一次) + * 函数说明:页面模块静态态不随屏/容器建毁自动复位,须自重置 + * (编辑态/光标/保存提示) + */ +static void nc_init_cb(void) { + s_editing = 0u; + s_cursor = 0u; + s_saved = 0u; +} + +/* + * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): + * 列表页(5 行 1 页,动态值)+ 可编辑(edit_capable,还须修改权限) + * + ← 拦截(编辑模式移字节光标;ESC 恒不可拦截) + */ +const content_page_def_t ui_page_net_cfg_desc = { + .title = "网口设置", + .selectable = 1u, + .key_intercept = 1u, + .edit_capable = 1u, + .top_y = NC_TOP_Y, + .pitch = NC_PITCH, + .band_h = NC_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, +}; diff --git a/Drivers/BSP/UI/ui_page_net_cfg.h b/Drivers/BSP/UI/ui_page_net_cfg.h new file mode 100644 index 00000000..b323b84e --- /dev/null +++ b/Drivers/BSP/UI/ui_page_net_cfg.h @@ -0,0 +1,29 @@ +/* + * 模块名称:UI Page Net Cfg (网口设置内容页) + * 模块功能:网口设置叶子节点的内容页描述符(5 行地址列表 + 保存行; + * 可编辑页:修改权限下 ENTER 进编辑、↑↓ 调字节、←→ 移字节光标、 + * 保存行 ENTER 写 Flash 下次上电生效) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-10 + * 修改记录: + * v1.0 2026-09-10 王建锋 创建初始版本 + */ + +#ifndef UI_PAGE_NET_CFG_H +#define UI_PAGE_NET_CFG_H + +#include "ui_content_page.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* 页面描述符(框架描述符表 s_defs[] 引用) */ +extern const content_page_def_t ui_page_net_cfg_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_NET_CFG_H */ diff --git a/Drivers/BSP/UI/ui_page_password.c b/Drivers/BSP/UI/ui_page_password.c new file mode 100644 index 00000000..e8cfbdce --- /dev/null +++ b/Drivers/BSP/UI/ui_page_password.c @@ -0,0 +1,231 @@ +/* + * 模块名称:UI Page Password (用户密钥密码页) + * 模块功能:3 位密码输入页(表单/操作页首个实例)——标题由框架自绘(无"浅 V"、 + * 无页脚,跟随实机),本模块自绘 3 个数字框:预填 0、首框反显 = 光标位; + * ↑↓调值(9↔0 环绕)、←→移光标(首↔末环绕)、ENTER 确认(修改密码→完成 + * 回调授修改权限 / 查看密码→完成回调授只读权限 / 不一致→清空重输)、 + * ESC 返回菜单(框架保证); + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:表单/操作页首个实例(内容页架构.md §4.1; + * 实机参考 mockups/密码页面实际机器的示例图片.jpg) + * 2026-09-09 王建锋 改权限模型:arm 只载回调(不再载期望密码);页面比对 + * ui_pwd_def.h 两个编译期密码(先查修改),一致时回调携 + * 授予的权限(UI_PERM_MODIFY / UI_PERM_VIEW) + */ + +/* 头文件包含区 */ +#include "ui_page_password.h" + +#include "ui_pwd_def.h" + +/* 私有宏定义区 */ + +/* + * 布局常量(设备像素;实机参考 mockups/密码页面实际机器的示例图片.jpg 实测): + * 3 个数字框单行居中于屏中下部(框 14x14、间距 6、行顶 y=78、首框 x=53 → + * 3 框 + 2 距 = 54px,屏中心 80);数字墨迹框内水平居中、垂直贴框顶 + * (ASCII 字形顶距 +2 已计入字体度量) + */ +#define PW_DIGITS 3 /* 密码位数 */ +#define PW_BOX_W 14 /* 数字框宽(px) */ +#define PW_BOX_H 14 /* 数字框高(px) */ +#define PW_BOX_GAP 6 /* 数字框间距(px) */ +#define PW_BOX_Y 78 /* 数字框行顶 y */ +#define PW_BOX_X0 53 /* 首个数字框左 x(居中) */ + +#define PW_TITLE_Y 44 /* 标题放置 y(中上;0 = 框架默认,实机明显低于其余各页的 y=2) */ + +/* 静态变量定义区 */ + +/* 单位数字文本(Flash):数字 0..9 各 1 字符(text_static,不拷内存) */ +static const char *const s_digit_str[10] = { + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" +}; + +/* 页面运行状态(RAM,约 9 B):3 位值 + 光标位 + 完成回调 */ +static uint8_t s_digits[PW_DIGITS]; /* 3 位密码值(0..9) */ +static uint8_t s_cur; /* 光标位 0..2(反显框) */ +static void (*s_on_ok)(uint8_t); /* 完成回调(逻辑层预装,实参=授予权限;NULL = 一致时仅关页) */ + +/* 函数定义区 */ + +/* + * 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头) + * 入口参数:a - 区域 1 const lv_area_t * + * b - 区域 2 const lv_area_t * + * 返回值:true - 相交;false - 不相交 + * 限定条件:无 + * 函数说明:四边取 min/max 成交集,任一边为空即不相交(与 ui_content_page.c 同实现) + */ +static bool pw_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); +} + +/* + * 函数功能:绘制数字框内数字(单字符,水平居中) + * 入口参数:layer - 绘制层 lv_layer_t * + * box - 数字框区域 const lv_area_t * + * digit - 数字值(0..9) uint32_t + * color - 文本颜色 lv_color_t + * 返回值:无 + * 限定条件:数字框在重绘区内(调用方 culling) + * 函数说明:文本为 Flash 字面量(不拷内存);ofs 相对区域原点 + */ +static void pw_draw_digit(lv_layer_t *layer, const lv_area_t *box, uint32_t digit, lv_color_t color) { + lv_draw_label_dsc_t ld; + + lv_draw_label_dsc_init(&ld); + ld.base.layer = layer; + ld.text = s_digit_str[digit]; + ld.text_static = 1; + ld.color = color; + ld.ofs_x = (PW_BOX_W - lv_text_get_width(s_digit_str[digit], 1, LV_FONT_DEFAULT, 0)) / 2; + ld.ofs_y = 0; + lv_draw_label(layer, &ld, box); +} + +/* + * 函数功能:页面自绘(3 个数字框:光标框反显,其余白底黑框) + * 入口参数:layer - 绘制层 lv_layer_t * + * clip - 重绘区 const lv_area_t * + * 返回值:无 + * 限定条件:页面打开(框架绘制链末尾调用) + * 函数说明:标题/页脚由框架处理(每页 title_y、"浅 V"/页脚已关);本模块只绘数字框; + * 1bpp 极性:white=暗、black=亮——光标框反显(white 底 black 字), + * 其余框亮底暗字 + 1px 暗边 + */ +static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { + for (uint32_t i = 0u; i < PW_DIGITS; i++) { + lv_area_t box; + lv_draw_rect_dsc_t fd; + + box.x1 = (int32_t)(PW_BOX_X0 + i * (PW_BOX_W + PW_BOX_GAP)); + box.x2 = (int32_t)(box.x1 + PW_BOX_W - 1u); + box.y1 = PW_BOX_Y; + box.y2 = (int32_t)(PW_BOX_Y + PW_BOX_H - 1u); + if (!pw_areas_intersect(&box, clip)) { + continue; /* 数字框不在重绘区:跳过(culling) */ + } + + lv_draw_rect_dsc_init(&fd); + fd.base.layer = layer; + fd.bg_opa = LV_OPA_COVER; + if (i == s_cur) { + /* 光标框:反显(1bpp:white=暗底,black=亮字) */ + fd.bg_color = lv_color_white(); + } + else { + /* 非光标框:亮底暗字 + 1px 暗边(1bpp:black=亮底,white=暗) */ + fd.bg_color = lv_color_black(); + fd.border_color = lv_color_white(); + fd.border_width = 1; + } + lv_draw_rect(layer, &fd, &box); + pw_draw_digit(layer, &box, s_digits[i], (i == s_cur) ? lv_color_black() : lv_color_white()); + } +} + +/* + * 函数功能:页面按键处理(表单页:键全由页面处理) + * 入口参数:key - LVGL 键值 lv_key_t + * 返回值:true - 页已处理(框架跳过内置语义);false - 未处理 + * 限定条件:页面打开;经框架键分发调用(uiTask 上下文) + * 函数说明: 1. ↑/↓:调整光标位数字(+1/-1,9↔0 环绕) + * 2. ←/→:移动光标(首↔末环绕;← 键经框架拦截标志转入本函数) + * 3. ENTER:修改密码 → 完成回调(授修改权限);查看密码 → 完成回调 + * (授只读权限);不一致 → 清空重输 + * 4. ESC 不经本函数(框架最优先恒关页);其余键值未处理 + */ +static bool pw_key_cb(lv_key_t key) { + switch (key) { + case LV_KEY_UP: + s_digits[s_cur] = (uint8_t)((s_digits[s_cur] + 1u) % 10u); /* 值 +1,9 环绕 0 */ + break; + case LV_KEY_DOWN: + s_digits[s_cur] = (uint8_t)((s_digits[s_cur] + 9u) % 10u); /* 值 -1,0 环绕 9 */ + break; + case LV_KEY_LEFT: + s_cur = (uint8_t)((s_cur + PW_DIGITS - 1u) % PW_DIGITS); /* 光标左移,首绕末 */ + break; + case LV_KEY_RIGHT: + s_cur = (uint8_t)((s_cur + 1u) % PW_DIGITS); /* 光标右移,末绕首 */ + break; + case LV_KEY_ENTER: { + uint16_t input = (uint16_t)((uint32_t)s_digits[0] * 100u + + (uint32_t)s_digits[1] * 10u + + (uint32_t)s_digits[2]); + uint8_t perm; + + if (input == (uint16_t)UI_PWD_VALUE_MODIFY) { + perm = (uint8_t)UI_PERM_MODIFY; /* 修改密码:授修改权限(先查,两值同设时授高权) */ + } + else if (input == (uint16_t)UI_PWD_VALUE_VIEW) { + perm = (uint8_t)UI_PERM_VIEW; /* 查看密码:授只读权限 */ + } + else { + /* 不一致:清空重输(3 框回 0、光标回首框) */ + s_digits[0] = 0u; + s_digits[1] = 0u; + s_digits[2] = 0u; + s_cur = 0u; + break; + } + + /* 一致:逻辑层接管"关页 + 按权限续进入"(回调内自行关页) */ + if (s_on_ok != NULL) { + s_on_ok(perm); + } + else { + ui_content_page_close(); /* 防御:无回调 → 仅关页 */ + } + return true; /* 页面已关(或已切走),无需重绘 */ + } + default: + return false; /* 未处理:落回框架内置语义 */ + } + + ui_content_page_invalidate(); /* 状态已变:触发整页重绘 */ + return true; +} + +/* + * 函数功能:装载完成回调(须在开页前调用) + * 入口参数:on_ok - 完成回调(NULL = 一致时仅关页);一致后以授予的权限为实参 + * 返回值:无 + * 限定条件:由逻辑层在 uiTask 上下文、开页前调用 + * 函数说明:密码页比对 ui_pwd_def.h 的两个编译期密码值(无需载值); + * 每次进入重置为初始态(全 0、光标首框,与实机初始显示一致) + */ +void ui_page_password_arm(void (*on_ok)(uint8_t perm)) { + s_on_ok = on_ok; + s_digits[0] = 0u; + s_digits[1] = 0u; + s_digits[2] = 0u; + s_cur = 0u; +} + +/* + * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): + * 表单/操作页——selectable=0、无行数据,自绘内容 + 自有键; + * 标题/装饰/页脚跟随实机(无"浅 V"、标题中上、无页脚); + * key_intercept=1:← 键先由 key_cb 处理(移光标),ESC 恒不可拦截 + */ +const content_page_def_t ui_page_password_desc = { + .title = "用户密钥", + .title_y = PW_TITLE_Y, + .hide_deco = 1u, + .hide_footer = 1u, + .selectable = 0u, + .key_intercept = 1u, + .draw_cb = pw_draw_cb, + .key_cb = pw_key_cb, +}; diff --git a/Drivers/BSP/UI/ui_page_password.h b/Drivers/BSP/UI/ui_page_password.h new file mode 100644 index 00000000..764c954e --- /dev/null +++ b/Drivers/BSP/UI/ui_page_password.h @@ -0,0 +1,40 @@ +/* + * 模块名称:UI Page Password (用户密钥密码页) + * 模块功能:3 位密码输入页(表单/操作页首个实例)——3 个数字框 + 反显光标, + * ↑↓调值(9↔0 环绕)、←→移光标(首↔末环绕)、ENTER 确认、ESC 返回菜单; + * 确认一致时按所输密码授予权限(查看密码→只读 / 修改密码→可修改), + * 由逻辑层预装的完成回调(携权限)接管"关页 + 转目标"; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:表单/操作页首个实例(内容页架构.md §4.1; + * 实机参考 mockups/密码页面实际机器的示例图片.jpg) + */ +#ifndef UI_PAGE_PASSWORD_H +#define UI_PAGE_PASSWORD_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):用户密钥页 = CONTENT_PAGE_PASSWORD; + * 接入框架描述符表 s_defs[](定义于 ui_page_password.c) + */ +extern const content_page_def_t ui_page_password_desc; + +/* + * 函数功能:装载完成回调(须在开页前调用) + * 入口参数:on_ok - 完成回调(ENTER 确认一致后调用,实参为授予的权限: + * UI_PERM_MODIFY = 输了修改密码 / UI_PERM_VIEW = 输了查看密码; + * 由逻辑层实现"关页 + 按权限续进入"; + * NULL = 一致时仅关页) + * 返回值:无 + * 限定条件:由逻辑层在 uiTask 上下文、ui_content_page_open(CONTENT_PAGE_PASSWORD) + * 之前调用 + * 函数说明:页面比对 ui_pwd_def.h 的两个编译期密码(先查修改:两值同设时授高权); + * 每次进入重置为初始态(全 0、光标首框,与实机一致);页面模块不感知菜单 + */ +void ui_page_password_arm(void (*on_ok)(uint8_t perm)); + +#endif /* UI_PAGE_PASSWORD_H */ diff --git a/Drivers/BSP/UI/ui_pwd_def.h b/Drivers/BSP/UI/ui_pwd_def.h new file mode 100644 index 00000000..d738d3bf --- /dev/null +++ b/Drivers/BSP/UI/ui_pwd_def.h @@ -0,0 +1,40 @@ +/* + * 模块名称:UI PWD Def (密码与权限配置) + * 模块功能:查看/修改两个密码的编译期取值(3 位 0..999)+ 权限枚举定义—— + * 密码值唯一配置点;改值后重新编译生效,不占 RAM、不落 Flash; + * 菜单表(ui_menu_tree.txt)叶子行尾 #P 注记标"需密码", + * 进叶子时按所输密码授权(查看密码→只读 / 修改密码→可修改) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-09 + * 修改记录: + * 2026-09-09 王建锋 创建:密码值编译期配置(用户定案:密码仅编译期 + * 写入,不做运行期设置页与 Flash 持久化) + * 2026-09-09 王建锋 改权限模型:密码不绑定特定叶子,同一叶子输不同密码 + * 获不同权限(查看密码→只读 / 修改密码→可修改); + * 新增 ui_perm_t 权限枚举;树注记改 #P(需密码) + */ +#ifndef UI_PWD_DEF_H +#define UI_PWD_DEF_H + +#include + +/* + * 权限:进入受保护叶子时获得的权限级别(由所输密码决定): + * 同一叶子输查看密码 → 只读;输修改密码 → 可修改(含查看); + * 无密码叶子直接进,默认只读 + */ +typedef enum { + UI_PERM_VIEW = 0u, /* 只读权限:查看密码授予(无密码叶子的默认权限) */ + UI_PERM_MODIFY, /* 修改权限:修改密码授予(含查看) */ + UI_PERM_COUNT /* 哨兵:非法权限(自检) */ +} ui_perm_t; + +#define UI_PWD_VALUE_VIEW 0u /* 查看密码:输入授予只读权限 */ +#define UI_PWD_VALUE_MODIFY 123u /* 修改密码:输入授予修改权限 */ + +/* 护栏:密码值域 0..999(3 位),越界编译失败(C99 写法,兼容 ARMCC v5) */ +typedef char ui_pwd_value_view_check[(UI_PWD_VALUE_VIEW <= 999u) ? 1 : -1]; +typedef char ui_pwd_value_modify_check[(UI_PWD_VALUE_MODIFY <= 999u) ? 1 : -1]; + +#endif /* UI_PWD_DEF_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index a5100423..ac493a20 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -340,7 +340,7 @@ --diag_suppress=1,68,111,188,191,546,1293 USE_HAL_DRIVER,STM32F407xx,NDEBUG - ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../App;../App/task;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test;../Lib/lvgl;../Drivers/BSP/UI;../Lib + ../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/task;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test;../Lib/lvgl;../Drivers/BSP/UI;../Lib @@ -1236,6 +1236,16 @@ 1 ..\Drivers\BSP\NET\net_socket.c + + net_cfg.c + 1 + ..\Drivers\BSP\NETCFG\net_cfg.c + + + net_cfg_store_mcu.c + 1 + ..\Drivers\BSP\NETCFG\net_cfg_store_mcu.c + nand_ftl.c 1 @@ -3148,6 +3158,16 @@ 1 ..\Drivers\BSP\UI\ui_page_dc.c + + ui_page_password.c + 1 + ..\Drivers\BSP\UI\ui_page_password.c + + + ui_page_net_cfg.c + 1 + ..\Drivers\BSP\UI\ui_page_net_cfg.c + ui_menu_tab.c 1 diff --git a/docs/UI 设计/UI_中文字体生成与配置.md b/docs/UI 设计/UI_中文字体生成与配置.md index 68ea886b..ad96b3c1 100644 --- a/docs/UI 设计/UI_中文字体生成与配置.md +++ b/docs/UI 设计/UI_中文字体生成与配置.md @@ -55,7 +55,7 @@ scanned 13 files; unique non-ASCII chars: 78 -> D:\Code\DTU Code\STM32F4-Base\ui_cn_chars.txt ``` -产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 97 字),可留作字符集记录。 +产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 111 字,含 ▶/,等符号),可留作字符集记录。 --- @@ -85,12 +85,17 @@ npm i -g lv_font_conv # TTF/OTF -> LVGL 字体 离线转换器 ``` extracted SimSun regular (face0) -> C:\Users\...\Temp\simsun_regular.ttf RUN: lv_font_conv.cmd --font ... --size 12 --bpp 1 -r 0x20-0x7E --symbols ... --no-compress --format lvgl --lv-font-name ui_font_cn_12 -o ...\ui_font_cn_12.c -已合并箭头 U+25B6:位图+10 字节,glyph_id=173 -OK 35485 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c +已合并箭头 U+25B6:位图+10 字节,glyph_id=206 +已合并 U+FF0C ',':1 字节位图,glyph_id=207 +OK 43614 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c ``` > **▶ 箭头自动合并**:宋体(`simsun.ttc`)没有 U+25B6(▶)字形,`lv_font_conv` 会静默跳过 → 真机显示成方框。`make_font.py` 在生成后自动调用 `merge_arrow_glyph()`,把从 **Segoe UI Symbol**(`seguisym.ttf`,Windows 自带)预提取的箭头字形(位图 10 字节 + `glyph_dsc` + `cmap`)补回 `ui_font_cn_12.c`。该合并**幂等**(已存在则跳过),所以重生成字体后箭头不会丢。换用自带 ▶ 的字体(如黑体/Segoe)时可忽略此步。 +> **低码位符号自动合并**:`lv_font_conv` 的稀疏 cmap 从符号集最小码位起算,**低于该起点的符号被静默丢弃**(实测 U+3001/U+FF0C 等:U+FF0C 全角逗号 < 0x4E2D 范围起点时丢失,页脚"第 X 页,共 Y 页"的逗号长期渲染为空白)。`make_font.py` 的 `merge_extra_symbols()` 对每个此类符号单独生成迷你字体(真实宋体位图/度量),再并入主字体(位图 + `glyph_dsc` + 单码位 `cmap`),同样**幂等**。新增此类符号时把它加入脚本的 `EXTRA_SYMBOLS` 列表即可。 + +> **生成后覆盖率断言**:`make_font.py` 在合并后调用 `verify_coverage()`——解析生成字体的 cmap,校验 `ui_cn_chars.txt` 的**全部码位**都进了字体,缺一个即以非零退出码报错(列出 `U+XXXX`)。把 lv_font_conv 的静默丢字(低码位、源字体缺字形)从"上线后才发现空白"变成"生成期硬错误"。 + 参数说明: - `--bpp 1`:单色无抗锯齿;体积最小。**注意** lv_font_conv 在 1bpp 下斜线/细笔画偶发"丢像素",宋体尤甚。 - `--bpp 2/3`:字形带灰度,渲染到 1bpp 屏时按 `LV_DRAW_SW_I1_LUM_THRESHOLD` 阈值化,笔画更连贯;子集仅几十~上百字,体积仍很小。 @@ -147,7 +152,7 @@ OK 35485 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c 1. **1bpp 细笔画发虚**:宋体在 1bpp 下可能丢像素。若上板/客户端中文偏淡,把第 4 节 `--bpp` 改为 `2` 重生成即可(子集小,体积可接受)。 2. **字符集变更**:以后新增中文菜单项,须重跑第 3、4 节(重新提取 `ui_cn_chars.txt` 并生成),否则新字显示为方框。 3. **编码**:UI 源文件须为 **UTF-8(带 BOM)**,LVGL 按 UTF-8 解码中文串;字体 `.c` 为纯 ASCII,无编码风险。当前 Keil 已加 Misc Controls 抑制项,无需额外 `--locale=english`。 -4. **字体体积**:12px + 1bpp + ~78 字 ≈ 35.5KB(RO-data);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 +4. **字体体积**:12px + 1bpp + 111 字(含 ASCII/符号)≈ 43.6KB(RO-data);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 5. **换字体**:黑体用 `simhei.ttf`(单文件免抽取);其余字体同理替换 `--font` 路径后重生成。 --- diff --git a/docs/UI 设计/UI_界面制作流程.md b/docs/UI 设计/UI_界面制作流程.md index 880c888c..0111cab4 100644 --- a/docs/UI 设计/UI_界面制作流程.md +++ b/docs/UI 设计/UI_界面制作流程.md @@ -48,7 +48,7 @@ flowchart TD |---|---|---|---| | C1 | 屏幕 | 160×160,1bpp 单色(`LV_COLOR_FORMAT_I1`),无物理屏(远端客户端显示) | `ui_port.h` | | C2 | 字体 | **唯一**:`ui_font_cn_12`(宋体 12px 1bpp)。汉字 12px/字,ASCII 6px/字,行高 13px。需要其他字号 = 大工程(新字体 + 双端 `lv_conf.h`),默认禁止 | `ui_font_cn_12.c` | -| C3 | 字符集 | 子集字体(当前 97 汉字 + ASCII 0x20-0x7E,共 193 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | +| C3 | 字符集 | 子集字体(当前 111 汉字/符号 + ASCII 0x20-0x7E,共 208 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | | C4 | LVGL 内存池 | 40KB(`LV_MEM_SIZE`);任一时刻只放"当前导航路径 + 当前内容页" | `Lib/lv_conf.h` | | C5 | 文本 | 一律静态字符串(`lv_label_set_text_static`),禁止动态字符串/strdup | 架构文档 | | C6 | 编码 | `Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h` 为 **UTF-8 带 BOM**;改动只用 Edit 工具,**禁止** PowerShell/cmd 整文件重写(U+FFFD 不可逆) | AGENTS.md 铁律 | @@ -440,3 +440,5 @@ P5 [ ] UI_设计架构.md 变更记录更新 | 2026-09-04 | §九 新增 9.1“样机投入分流(按屏型)”:明确 HTML 样机=静态外观轴(设计先行、非代码生成器),设计文档 + 模拟器=行为/数据轴;静态内容页走完整样机管线、导航/交互重屏 HTML 只做静态首帧、行为靠 `菜单页面设计.md`;配套文档补列 `菜单页面设计.md` | | 2026-09-07 | §一 五阶段管线图由 ASCII 转 Mermaid flowchart:P1/P3“比对→修正”迭代环由行内 ⟲ 注记改为显式判定节点 + 回边(未收敛不得前进更直观);各 Gate 条件与产物标注保留、语义不变 | | 2026-09-09 | 内容页三屏(装置信息/交流量/直流量,`ui_content_page`)落地后回填:C3 字符集 78→97 汉字(193 字形);§十 新增两坑——UI BOM 源文件 `snprintf` 中文格式串触发 C4819(改 `lv_snprintf`)、常驻动态样式池永久占用(改 `LV_STYLE_CONST_INIT` 常量样式,x64 实测省 88 B/个) | +| 2026-09-09 | 用户密钥密码页落地后回填:C3 字符集 97→102 汉字(198 字形,补"本/用/户/密/钥");新文案走 P4 字体重生成 + `check_content_pages.py` 新增密码页 4 状态黄金参考 | +| 2026-09-10 | 网口设置内容页落地后回填:C3 字符集 102→111 字符(208 字形,补网口设置/地址/掩码等,并修复 `lv_font_conv` 丢弃的低码位符号 U+FF0C 全角逗号 → 页脚逗号不再空白);新文案走 P4 字体重生成 + `check_content_pages.py` 新增网口设置 4 状态黄金参考(可编辑/只读/编辑光标反显/保存 OK);模拟器 `main` 调 `net_cfg_load` 使参考图显示工厂默认值 | diff --git a/docs/UI 设计/UI_自动化测试流程.md b/docs/UI 设计/UI_自动化测试流程.md index a57e848e..12f08424 100644 --- a/docs/UI 设计/UI_自动化测试流程.md +++ b/docs/UI 设计/UI_自动化测试流程.md @@ -26,12 +26,19 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 | 步 | 脚本/命令 | 门禁内容 | 预期输出 | |---|---|---|---| | 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(S3==S1、S6==S1、S7==S2)+ S0 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 4784 B)` | -| 2 | `simulator/tests/check_content_pages.py` | 装置信息/交流量/直流量三页截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 3 内容页 + 往返池水位全部通过` | +| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,50 条,独立于 `ref/`)→ ② 11 页截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 11 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) | | 3 | `MDK-ARM\build.bat` | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK | | 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS | +> **文本级断言层**(2026-09-10 新增):`check_content_pages.py` 内置字形模板匹配——直接解析 +> `ui_font_cn_12.c` 的 cmap/glyph_dsc/bitmap,把期望字符串(标题/标签/值/页脚,含新增易缺字文案) +> 渲染成墨点集合,在截图对应 y 带内做精确匹配(LVGL 基线对齐)。价值:**缺字显示空白 / 标签写错** +> 这类回归不再只靠像素位图(那需要刷参考,且无视觉后端时难以判读);文本断言不依赖 `ref/`,且 +> **先于**像素比对运行,失败即中止(不误刷参考)。文本断言覆盖确定文案,像素参考仍负责几何/样式/ +> 光标反显等其余视觉。 + **退出码约定**:两个脚本均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失、参考缺失)。 -失败时脚本打印差异明细(ink runs 对照 / 像素差数),内容页脚本保留现场截图于临时目录并提示路径。 +失败时脚本打印差异明细(文本断言未命中项 / ink runs 对照 / 像素差数),内容页脚本保留现场截图于临时目录并提示路径。 ## 三、内存水位门禁常量 @@ -44,12 +51,27 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 `菜单页面设计.md` §9.5),再更新脚本常量并在本文变更记录登记。**禁止为过门禁盲目改常量**—— 无法解释的漂移 = 内存回归 bug。 -## 四、黄金参考更新流程(有意改视觉时) +## 四、黄金参考与目检 PNG(均为本地生成物,不入库) -1. **先肉眼确认新效果正确**:窗口模式运行 `simulator\build\Debug\lvgl_sim.exe`,或截图转 PNG - (`python simulator/i1_to_png.py --scale 6`)对照 mockup; -2. 刷新参考:`.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update`(重写 `ref/shot_*.bin`); -3. `git status` / `git diff` 确认 `simulator/tests/ref/` 变化符合预期,随代码一起提交。 +`simulator/tests/ref/` 下的 `.bin`(像素黄金参考)与 `.png`(目检图)**都不入库**(`.gitignore`)—— +首次/换机后必须先生成一次: + +```powershell +.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update # 生成 ref/*.bin +.venv\Scripts\python.exe simulator/tests/regen_ref_png.py # 生成 ref/*.png(--scale 6 --invert) +``` + +有意改视觉时的流程: + +1. **先肉眼确认新效果正确**:窗口模式运行 `simulator\build\Debug\lvgl_sim.exe`,或刷新后跑 + `regen_ref_png.py` 看图对照 mockup; +2. 刷新参考:`.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update`(重写 `ref/*.bin`); +3. **重生成目检 PNG**:`.venv\Scripts\python.exe simulator/tests/regen_ref_png.py`。 + `.png` 是本地调试产物、**不会**随 `.bin` 自动更新——不重跑则 PNG 仍是旧渲染(缺字/错位,看似"乱码")。 + 默认 `--scale 6 --invert`(深字白底便于目检);`--no-invert` = 白字黑底(与设备一致); +4. 无需提交 `ref/`(已 gitignore)。 + +**误刷保护**:`--update` 会**先跑文本级断言**,断言失败即中止,避免把缺字/错标签的渲染"刷成"新基准。 **何时不该更新**:未有意改视觉而某页 FAIL → 是 bug(缺字、几何漂移、样式回归),修代码而不是刷参考。 菜单状态无黄金参考文件——其断言是结构性的(白带占比/边框/往返一致),不依赖入库位图。 @@ -76,3 +98,6 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警 |---|---| | 2026-09-09 | Python 解释器统一为仓库 `.venv`(新增 `requirements.txt`,`.gitignore` 忽略 `.venv/`);§1/§2/§4 命令同步改用 `.venv\Scripts\python.exe` | | 2026-09-09 | 初版:菜单 8 状态像素回归 + 内容页黄金参考回归(ref/ 3 图,I1 原始位图入库)+ 池水位门禁(4784/4864 B)。脚本自会话临时目录整理入 `simulator/tests/`(相对路径定位 exe、输出改临时目录),替代每次重写 | +| 2026-09-10 | `check_content_pages.py` 新增**文本级断言层**(字形模板匹配 50 条,独立于 `ref/`,先于像素比对运行、失败即中止防误刷参考);内容页扩至 11 页(+网口设置 4 态);§二/§四同步 | +| 2026-09-10 | 新增 `simulator/tests/regen_ref_png.py`(由 `ref/*.bin` 批量重生成目检 PNG,默认 `--scale 6 --invert`);`ref/*.png` 加入 `.gitignore`(本地调试产物不入库);§四 补"重生成目检 PNG"步骤(修复"刷了 `.bin` 参考但 `.png` 仍是旧渲染 → 看似乱码") | +| 2026-09-10 | `simulator/tests/ref/` **整目录不入库**(`.bin` 黄金参考 + `.png` 目检图均为本地生成物):`git rm --cached` 原有 3 个 `.bin`,`.gitignore` 改为忽略整目录;首次/换机后先 `--update` 生成参考;`--update` 先跑文本断言防误刷;§四 重写 | diff --git a/docs/UI 设计/UI_设计架构.md b/docs/UI 设计/UI_设计架构.md index 421d8dd6..8f2eefc1 100644 --- a/docs/UI 设计/UI_设计架构.md +++ b/docs/UI 设计/UI_设计架构.md @@ -19,7 +19,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 | `Drivers/BSP/UI/ui_menu.c/h` | **共用** | 菜单导航逻辑层:持有导航状态(路径/选中/深度/滚动,共 17 B)+ 键语义,单向驱动显示层;不持 LVGL 对象 | | `Drivers/BSP/UI/ui_menu_view.c/h` | **共用** | 菜单显示层:横向级联框渲染(右贴靠/自适应宽/顶底对齐),持有全部 LVGL 对象与样式、无导航状态 | | `Drivers/BSP/UI/ui_menu_tab.c/h` | **共用** | 菜单数据层:`g_menu_tab[]` 父子索引表 + 查询 API(生成物,勿手改) | -| `Drivers/BSP/UI/ui_content_page.c/h` | **共用** | 内容页框架(机制 + 描述符表);装置信息/交流量/直流量三页由 `ui_page_xxx` 模块接入(独立屏按需建毁,自绘行,零常驻池占用);多页面"框架+页面插件"架构见 `内容页架构.md` | +| `Drivers/BSP/UI/ui_content_page.c/h` | **共用** | 内容页框架(机制 + 描述符表);装置信息/交流量/直流量/用户密钥四页由 `ui_page_xxx` 模块接入(独立屏按需建毁,自绘行,零常驻池占用);多页面"框架+页面插件"架构见 `内容页架构.md` | | `Drivers/BSP/UI/ui_net_sink.c/h` | MCU | RemoDispBus 服务端(收键 + 回送帧缓冲) | | `Drivers/BSP/UI/ui_font_cn_12.c` | **共用** | 中文 1bpp 子集字体(`lv_font_conv` 生成物) | | `App/task/ui_task.c/h` | MCU | UI 任务:等 NET_READY → 初始化 → 10ms 刷新循环 | @@ -37,7 +37,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓 | LVGL 内存池 | 40 KB(`LV_MEM_SIZE`) | `Lib/lv_conf.h` | | 刷新周期 | 10 ms(`ui_port_step(10)` + `osDelay(10)`) | `ui_task.c` | | 推流端口 | 7003(TCP,与 DTU-RemoteLCD 客户端对齐) | `ui_net_sink.h` | -| 默认字体 | `ui_font_cn_12`(12px 1bpp,97 汉字 + ASCII 0x20–0x7E + ▶,共 193 字形) | `Lib/lv_conf.h` | +| 默认字体 | `ui_font_cn_12`(12px 1bpp,111 汉字/符号 + ASCII 0x20–0x7E,共 208 字形) | `Lib/lv_conf.h` | | uiTask 栈 | 16384 words(BelowNormal) | `Src/freertos.c` | ### 1.4 硬约束(改 UI 必须遵守) @@ -186,14 +186,14 @@ ENTER/→ 进子框: ### 4.7 内容页(ui_content_page) -> 菜单叶子节点进入后的**独立全屏页面**(装置信息/交流量/直流量),与级联框同屏互斥。设计细节见 `菜单页面设计.md` §九。 +> 菜单叶子节点进入后的**独立全屏页面**(装置信息/交流量/直流量/用户密钥密码页),与级联框同屏互斥。设计细节见 `菜单页面设计.md` §九、`内容页架构.md`。 - **对象树**:`screen(新) → cont 全屏容器(flat + CLICK_FOCUSABLE,键入口)`;行全部自绘(无子对象),与级联框同一"行=数据"策略 - **生命周期 = 按需建毁**:ENTER 叶子(node 1/3/4)→ `lv_obj_create(NULL)` 新建独立屏 + 容器 → 入默认输入组获焦 → `lv_screen_load` 切屏;ESC → 焦点先回菜单持久容器、切回菜单屏,再删容器+屏。菜单屏对象**保留不销毁**(返回即原样还原) - **导航状态不压层**:菜单 `s_depth/s_sel/s_scroll` 原样保留,内容页只记自身选中行 `s_sel`(每次进入从首行开始) - **池占用实测**(模拟器 x64,40KB 池):开页峰值 +776 B(屏+容器+事件列表+局部样式),关页后回落;一次性残留 **+80 B**(screens 数组高水位 +8、`lv_screen_load` 给菜单屏设 X/Y 局部样式 +72,均为 LVGL 核心行为、非本模块泄漏) - **flat 样式用常量样式**(`LV_STYLE_CONST_INIT`):属性存静态数组、零池分配;动态 `lv_style_t` 首次 set_prop 会从池分配 `values_and_props` 且永不释放(x64 实测 88 B/个,MCU 同量级),常驻样式一律用常量样式 -- **多页面架构(已定稿并拆分,2026-09-09)**:待实现 30 个叶子页;框架(本模块)+ 每页一文件(`ui_page_xxx`,描述符为 extern const 对象)+ 4 个扩展钩子(refresh/count_cb/draw_cb/key_cb),见 `内容页架构.md`;架构第一步(共享"浅 V"装饰 `ui_deco`)与拆分本身均于 2026-09-09 落地,纯重构回归全绿(像素一致、池 4784/4864 B 不变) +- **多页面架构(已定稿并拆分,2026-09-09)**:待实现 30 个叶子页;框架(本模块)+ 每页一文件(`ui_page_xxx`,描述符为 extern const 对象)+ 4 个扩展钩子(refresh/count_cb/draw_cb/key_cb),见 `内容页架构.md`;架构第一步(共享"浅 V"装饰 `ui_deco`)与拆分本身均于 2026-09-09 落地,纯重构回归全绿(像素一致、池 4784/4864 B 不变);首个表单/操作页(用户密钥密码页:draw_cb+key_cb + 描述符 title_y/hide_deco/hide_footer/key_intercept)同日落地 ## 五、设计决策与考量(为什么这样做) @@ -258,3 +258,5 @@ ENTER/→ 进子框: | 2026-09-09 | 菜单显示层 flat 样式同法转常量样式(§5.6):LVGL 池基线 4872→4784 B、最深四级级联 6128→6040 B(x64 省 88 B,MCU 同量级);`LV_STYLE_CONST_INIT` 生成外部链接符号,内容页同名者改名 `cp_style_flat` 避免两文件冲突 | | 2026-09-09 | 内容页多页面架构定稿(`内容页架构.md`):框架+页面插件模型(每页一文件 `ui_page_xxx` + 4 个扩展钩子)、30 个待实现叶子页清单、新增页面六步流程;同日"浅 V"装饰提取为共享 `ui_deco` 模块(架构第一步,菜单根框/内容页共用,像素回归验证一致) | | 2026-09-09 | 内容页拆分执行(`内容页架构.md` §九 变更记录):`ui_content_page` 提纯为纯框架(描述符表 + NULL 防护 + 描述符驱动绘制/按键、4 个扩展钩子);三页数据拆出 `ui_page_device_info` / `ui_page_ac` / `ui_page_dc`(extern const 描述符对象);双端注册;Keil 0 错 0 警 + 模拟器 0 警告 + `ui-verify` 五门禁全绿(像素一致、池 4784/4864 B 不变) | +| 2026-09-09 | 用户密钥密码页落地(首个表单/操作页,§4.7 / `内容页架构.md` §4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API `ui_content_page_invalidate`;新模块 `ui_page_password`(3 数字框反显光标 + arm API);菜单受保护叶子路由(arm→开密码页,确认一致后回调**先关页再续做进入动作**);字体 97→102 汉字(198 字形);双端注册;回归扩至 7 页 + 往返水位,五门禁全绿 | +| 2026-09-10 | 网口设置内容页(§4.7 / `内容页架构.md`):新增 `Drivers/BSP/NETCFG`(Flash 持久化 + CRC-16,工厂默认 192.168.1.100/24)+ 页面插件 `ui_page_net_cfg`(5 行动态值 + 可编辑:↑↓调字节/←→移光标/ENTER 确认,保存行 ENTER → `net_cfg_save` 下次上电生效);框架新增钩子 `init_cb` + API `ui_content_page_selected_row`;`net_init` 增 `mac` 参数、netTask 等 `SYS_EVT_STORAGE_READY` → `net_cfg_load` → `net_init(ip,mask,gw,mac)` → 回写实际 MAC 影子;字体 102→111 字符(208 字形,修复低码位符号丢失);双端注册;回归扩至 11 页,五门禁全绿 | diff --git a/docs/UI 设计/内容页架构.md b/docs/UI 设计/内容页架构.md index 3981031b..12d421eb 100644 --- a/docs/UI 设计/内容页架构.md +++ b/docs/UI 设计/内容页架构.md @@ -11,13 +11,15 @@ ## 一、什么是内容页 菜单**叶子节点** ENTER 后进入的**独立全屏页面**:新建一块 LVGL 屏幕,全部内容(装饰 + 标题 + 行 + 页脚) -由框架一次性自绘(**无子对象**),ESC 返回菜单屏。当前已接 3 页: +由框架一次性自绘(**无子对象**),ESC 返回菜单屏。当前已接 5 页: | 页面 | 菜单节点 | 形态 | 数据 | 描述符对象 | |---|---|---|---|---| | 装置信息 | node 1 | 静态信息页(6 行 + 副标题,不分页) | Flash 表 | `ui_page_device_info_desc` | | 交流量 | node 3 | 列表页(24 项,8 项/页,共 3 页) | Flash 表(第 1 页实测,2/3 页占位) | `ui_page_ac_desc` | | 直流量 | node 4 | 列表页(3 项,单页) | Flash 表 | `ui_page_dc_desc` | +| 网口设置 | node 30(通讯参数子项) | 列表页(5 行 1 页,动态值)+ 可编辑(`edit_capable`,↑↓调字节/←→移光标/ENTER 确认;保存行 ENTER → 写 Flash,下次上电生效) | `net_cfg` RAM 副本(netTask 上电自 Flash 加载) | `ui_page_net_cfg_desc` | +| 用户密钥 | 需密码叶子(如 node 8 参数) | 表单/操作页(draw_cb + key_cb;无行、无"浅 V"、无页脚) | arm 装载:完成回调(携授予权限);页面比对两个编译期密码 | `ui_page_password_desc` | 设计核心是 **"框架 + 页面插件"**:框架 `ui_content_page` 只持**机制**(屏/容器生命周期、焦点、 按键分发、通用绘制),每页的**数据/几何/钩子**由独立 `ui_page_xxx` 模块经描述符 `content_page_def_t` @@ -36,7 +38,7 @@ flowchart TD L3["通用绘制(LV_EVENT_DRAW_MAIN)
浅 V → 标题/副标题 → 行 → 选中带 → 页脚 → draw_cb"] end - M -- "ENTER:ui_content_page_open(id)" --> S + M -- "ENTER:ui_content_page_open(id, perm)" --> S S -- "描述符驱动" --> L1 S -- "描述符驱动" --> L2 S -- "描述符驱动" --> L3 @@ -46,12 +48,14 @@ flowchart TD P1["ui_page_device_info
装置信息(静态信息页)"] P2["ui_page_ac
交流量(列表页)"] P3["ui_page_dc
直流量(列表页)"] + P4["ui_page_password
用户密钥(表单/操作页)"] PN["ui_page_xxx
扩展点:每新增页一文件"] end P1 -- "const 描述符(Flash)" --> S P2 -- "const 描述符(Flash)" --> S P3 -- "const 描述符(Flash)" --> S + P4 -- "const 描述符(Flash)" --> S PN -- "const 描述符(Flash)" --> S L3 -- "ui_deco_draw" --> D["ui_deco(共享)
顶部“浅 V”装饰"] @@ -70,12 +74,14 @@ flowchart TD | 文件 | 角色 | 一句话职责 | |---|---|---| -| `ui_content_page.c/h` | **框架** | 屏/容器生命周期 + 按键分发 + 描述符驱动通用绘制 + 描述符表;公开 `content_page_id_t` / `content_page_def_t` / `cp_row_t` 与 3 个 API | +| `ui_content_page.c/h` | **框架** | 屏/容器生命周期 + 按键分发 + 描述符驱动通用绘制 + 描述符表;公开 `content_page_id_t` / `content_page_def_t` / `cp_row_t` 与 4 个 API | | `ui_page_device_info.c/h` | 页面插件 | 装置信息页:6 行 Flash 表 + 描述符对象 `ui_page_device_info_desc` | | `ui_page_ac.c/h` | 页面插件 | 交流量页:24 行 Flash 表 + 描述符对象 `ui_page_ac_desc` | | `ui_page_dc.c/h` | 页面插件 | 直流量页:3 行 Flash 表 + 描述符对象 `ui_page_dc_desc` | +| `ui_page_password.c/h` | 页面插件 | 用户密钥页:3 数字框自绘(draw_cb)+ ↑↓/←→/ENTER 键处理(key_cb)+ arm API + 描述符对象 `ui_page_password_desc` | +| `ui_page_net_cfg.c/h` | 页面插件 | 网口设置页:5 行(IP/掩码/网关/MAC + 保存)动态值 + 可编辑(draw_cb 反显活动字节 + key_cb 调字节/移光标/保存)+ `init_cb` 重置运行态 + 描述符对象 `ui_page_net_cfg_desc`(数据源 `net_cfg`) | | `ui_deco.c/h` | 共享 | 顶部"浅 V"装饰(3 段线 + culling),菜单根框/内容页共用 | -| `ui_menu.c` | 逻辑层 | `menu_content_page_for` 叶子节点 → 页 ID 映射;ENTER 叶子时调 `ui_content_page_open` | +| `ui_menu.c` | 逻辑层 | `menu_content_page_for` 叶子节点 → 页 ID 映射;ENTER 普通叶子调 `ui_content_page_open`;受保护叶子先 arm + 开密码页,确认一致后回调**先关页再续做进入动作**(`menu_password_ok`) | | `ui_menu_view.c` | 逻辑层依赖 | `ui_content_page_close` 经 `ui_menu_view_container()` 把焦点切回菜单持久容器 | 依赖方向:`ui_menu(逻辑)→ ui_content_page(框架)→ ui_page_xxx(页面数据)`; @@ -89,6 +95,7 @@ flowchart TD |---|---|---| | `bool ui_content_page_open(id)` | 开页 | ① 查 `s_defs[id]`:NULL → 打 `ERROR` 日志并返回 false(枚举/表漂移防护)② `lv_obj_create(NULL)` 建屏 + 全屏容器(flat 常量样式 + `IGNORE_LAYOUT` + `CLICK_FOCUSABLE`)③ 容器**先**入默认输入组并获焦 ⓸ `lv_screen_load` 切屏 | | `void ui_content_page_close()` | 关页 | ① 焦点**先**回菜单持久容器(防输入组焦点悬空)② 切回菜单屏(菜单屏对象未动,原样还原)③ 再删容器+屏,池内存回落 | +| `void ui_content_page_invalidate()` | 整页重绘 | 失效页面容器;供表单/操作页自身状态变化后调用(无页打开时空操作) | | `bool ui_content_page_is_open()` | 查询 | `s_open != CONTENT_PAGE_COUNT` | 框架持有的运行状态(全部 4 个变量,约 20 B): @@ -143,6 +150,8 @@ static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = { [CONTENT_PAGE_DEVICE_INFO] = &ui_page_device_info_desc, [CONTENT_PAGE_AC] = &ui_page_ac_desc, [CONTENT_PAGE_DC] = &ui_page_dc_desc, + [CONTENT_PAGE_PASSWORD] = &ui_page_password_desc, + [CONTENT_PAGE_NET_CFG] = &ui_page_net_cfg_desc, }; ``` @@ -156,10 +165,13 @@ static const content_page_def_t *const s_defs[CONTENT_PAGE_COUNT] = { ```mermaid flowchart TD - A(["按键(焦点容器 → LV_EVENT_KEY → cp_key_cb)"]) --> B{"ESC / ← ?"} - B -- 是 --> C["ui_content_page_close()
关页返回菜单(框架保证,页面钩子不可拦截)"] - B -- 否 --> D{"key_cb 钩子非空
且返回 true ?"} - D -- 是 --> E["页面已处理(表单/操作页自有键)
框架跳过内置语义"] + A(["按键(焦点容器 → LV_EVENT_KEY → cp_key_cb)"]) --> B{"ESC ?"} + B -- 是 --> C["ui_content_page_close()
关页返回菜单(框架保证,恒不可拦截)"] + B -- 否 --> L{"← ?"} + L -- "key_intercept=1 且
key_cb(←) 返回 true" --> E["页面已处理(表单/操作页自有键)
框架跳过内置语义"] + L -- "未置标志 / 钩子未处理" --> C + L -- 否 --> D{"key_cb 钩子非空
且返回 true ?"} + D -- 是 --> E D -- 否 --> F{"↑ / ↓ ?"} F -- 是 --> G{"selectable=1 且
总行数 > 0 ?"} G -- 是 --> H["移选(首尾环绕,跨页连续)
选中行越界 → 重置首行(防动态行数缩小)
lv_obj_invalidate 触发重绘"] @@ -169,7 +181,8 @@ flowchart TD 行为细节(与代码一致): -- **ESC/← 恒返回**:最先判断,`key_cb` 无法拦截(框架保证页面可退出) +- **ESC 恒返回**:最先判断,`key_cb` 无法拦截(框架保证页面可退出) +- **← 每页可拦截**:`key_intercept=1` 时 ← **先**交 `key_cb`(true = 页已处理不关页,如密码页光标移动);未置标志或钩子未处理则照旧关页 - **key_cb 先于内置 ↑↓**:返回 `true` = 页已处理,框架不再执行内置移选 - **↑↓ 移选**:`(sel + total ∓ 1) % total` 首尾环绕;`s_sel` 是全局下标,跨页连续 - **越界防护**:`s_sel >= total` 时先重置首行(为动态行数可能缩小而设;静态页恒不触发) @@ -179,14 +192,14 @@ flowchart TD ```mermaid flowchart TD - A(["LV_EVENT_DRAW_MAIN → cp_draw_cb"]) --> B["① 浅 V 装饰:ui_deco_draw(共享模块,内置 culling)
② 标题(居中,y=2) ⓢ 副标题(居中,y=subtitle_y;仅非空页)"] + A(["LV_EVENT_DRAW_MAIN → cp_draw_cb"]) --> B["① 浅 V 装饰:ui_deco_draw(共享模块,内置 culling;hide_deco=1 跳过)
② 标题(居中,每页 title_y / 默认 CP_TITLE_Y=y2) ⓢ 副标题(居中,y=subtitle_y;仅非空页)"] B --> C["④ 行区块
列表页:窗口 = 选中行所在页(首可见行 = (sel/每页数)×每页数)
信息页:全量;行槽位置固定,数据随页切换"] C --> C1{"行数据来源"} C1 -- "静态:rows 表非空" --> C2["直绘 Flash 表(text_static=1,不拷内存)"] C1 -- "动态:refresh 非空" --> C3["refresh() 逐行取入栈缓冲(32 B×2,同帧同步光栅化)"] C2 --> D C3 --> D["逐行:culling 判交 → 选中行白带(x=2..156,高 band_h)
→ 左/右两列 lv_draw_label(选中黑字,其余白字)"] - D --> E["⑤ 页脚“第 X 页,共 Y 页”(y=143,x=6)
列表页:随选中行现算;信息页:fixed_cur/total 固定值"] + D --> E["⑤ 页脚“第 X 页,共 Y 页”(y=143,x=6;hide_footer=1 跳过)
列表页:随选中行现算;信息页:fixed_cur/total 固定值"] E --> F{"draw_cb 非空 ?"} F -- 是 --> G["⑥ 页面自定义内容(表单/操作页,在内置区块之后)"] F -- 否 --> H(["绘制完成"]) @@ -213,7 +226,11 @@ typedef struct { const char *title; /* 标题(浅 V 区居中) */ const char *subtitle; /* 副标题;NULL = 无(仅信息页有) */ int32_t subtitle_y; /* 副标题放置 y(仅 subtitle 非空时有效) */ + int32_t title_y; /* 标题放置 y;0 = 框架默认(CP_TITLE_Y) */ + uint8_t hide_deco; /* 1 = 不绘"浅 V"装饰(密码页跟随实机) */ + uint8_t hide_footer; /* 1 = 不绘页脚页码(密码页跟随实机:无页脚) */ uint8_t selectable; /* 1 = 列表页(↑↓移选+分页);0 = 信息页(静态行) */ + uint8_t key_intercept; /* 1 = LV_KEY_LEFT 先交 key_cb(true = 页已处理不关页);ESC 恒不可拦截 */ int32_t top_y; /* 首行槽顶 y */ int32_t pitch; /* 行距(相邻行槽顶间距) */ int32_t band_h; /* 选中白带高(0 = 无选中带) */ @@ -233,26 +250,29 @@ typedef struct { } content_page_def_t; ``` -字段一览(含当前 3 页实际值): +字段一览(含当前 4 页实际值): -| 字段 | 类型 | 作用 | 装置信息 | 交流量 | 直流量 | -|---|---|---|---|---|---| -| `title` | `const char *` | 标题(浅 V 区居中) | "装置信息" | "交流量" | "直流量" | -| `subtitle` | `const char *` | 副标题;NULL = 无 | "馈线自动化终端" | NULL | NULL | -| `subtitle_y` | `int32_t` | 副标题放置 y | 25 | — | — | -| `selectable` | `uint8_t` | 1 = 列表页(↑↓+分页);0 = 信息页 | 0 | 1 | 1 | -| `top_y` | `int32_t` | 首行槽顶 y | 40 | 24 | 24 | -| `pitch` | `int32_t` | 行距 | 15 | 14 | 14 | -| `band_h` | `int32_t` | 选中白带高(0 = 无) | 0 | 13 | 13 | -| `text_dy` | `int32_t` | 文本垂直偏移 | 0 | 0 | 0 | -| `left_x` / `right_x` | `int32_t` | 左列(标签/名)/ 右列(值)x | 6 / 86 | 7 / 101 | 7 / 101 | -| `rows_per_page` | `uint8_t` | 每页行数(0 = 不分页) | 0 | 8 | 8 | -| `row_count` | `uint16_t` | 总行数(静态页) | 6 | 24 | 3 | -| `rows` | `const cp_row_t *` | 静态数据表(Flash) | 6 行 | 24 行 | 3 行 | -| `fixed_cur_page` / `fixed_total_pages` | `uint8_t` | 信息页页脚固定值 | 10 / 1 | — | — | -| `refresh` / `count_cb` / `draw_cb` / `key_cb` | 函数指针 | 扩展钩子 | 全 NULL | 全 NULL | 全 NULL | +| 字段 | 类型 | 作用 | 装置信息 | 交流量 | 直流量 | 用户密钥 | +|---|---|---|---|---|---|---| +| `title` | `const char *` | 标题(浅 V 区居中) | "装置信息" | "交流量" | "直流量" | "用户密钥" | +| `subtitle` | `const char *` | 副标题;NULL = 无 | "馈线自动化终端" | NULL | NULL | NULL | +| `subtitle_y` | `int32_t` | 副标题放置 y | 25 | — | — | — | +| `title_y` | `int32_t` | 标题放置 y(0 = 框架默认) | 0 | 0 | 0 | 44 | +| `hide_deco` / `hide_footer` | `uint8_t` | "浅 V" / 页脚开关 | 0 / 0 | 0 / 0 | 0 / 0 | 1 / 1 | +| `selectable` | `uint8_t` | 1 = 列表页(↑↓+分页);0 = 信息页 | 0 | 1 | 1 | 0 | +| `key_intercept` | `uint8_t` | ← 拦截标志(先交 key_cb) | 0 | 0 | 0 | 1 | +| `top_y` | `int32_t` | 首行槽顶 y | 40 | 24 | 24 | —(无行) | +| `pitch` | `int32_t` | 行距 | 15 | 14 | 14 | —(无行) | +| `band_h` | `int32_t` | 选中白带高(0 = 无) | 0 | 13 | 13 | —(无行) | +| `text_dy` | `int32_t` | 文本垂直偏移 | 0 | 0 | 0 | —(无行) | +| `left_x` / `right_x` | `int32_t` | 左列(标签/名)/ 右列(值)x | 6 / 86 | 7 / 101 | 7 / 101 | —(无行) | +| `rows_per_page` | `uint8_t` | 每页行数(0 = 不分页) | 0 | 8 | 8 | 0 | +| `row_count` | `uint16_t` | 总行数(静态页) | 6 | 24 | 3 | 0 | +| `rows` | `const cp_row_t *` | 静态数据表(Flash) | 6 行 | 24 行 | 3 行 | NULL | +| `fixed_cur_page` / `fixed_total_pages` | `uint8_t` | 信息页页脚固定值 | 10 / 1 | — | — | — | +| `refresh` / `count_cb` / `draw_cb` / `key_cb` | 函数指针 | 扩展钩子 | 全 NULL | 全 NULL | 全 NULL | draw_cb + key_cb(余 NULL) | -扩展钩子(机制已实现,当前 3 页均未使用): +扩展钩子(机制已实现;密码页用 `draw_cb` + `key_cb`,其余 3 页未用): | 钩子 | 签名 | 框架调用时机(uiTask 上下文) | 用途 | |---|---|---|---| @@ -261,7 +281,7 @@ typedef struct { | `draw_cb` | `void (lv_layer_t *layer, const lv_area_t *clip)` | 绘制链末尾(内置区块之后) | 表单/操作页自定义内容 | | `key_cb` | `bool (lv_key_t key)` | 键分发时(ESC 之后、内置 ↑↓ 之前) | 页面自有键(ENTER 确认等);返回 true = 页已处理 | -### 4.1 表单/操作页首个应用:密码页(待实现) +### 4.1 表单/操作页首个应用:密码页(已落地) 实机(淮源电力 F30)已有"用户密钥"页:进入某些子叶面前须先输入密码(实机照片见 `mockups/密码页面实际机器的示例图片.jpg`)。它是本项目**第一个表单/操作页**,落地形态即 @@ -269,15 +289,18 @@ typedef struct { **功能规则(已确认)** -- 密码共 **3 位**;子页(叶子)可配置"是否需要密码页",每个受保护页可设**专属密码** -- 受保护叶子 ENTER:先开密码页;输入与专属密码一致后按确认键进入目标页;ESC 随时返回菜单 +- 密码共 **3 位**;子页(叶子)可配置"是否需要密码页"(行尾 ` #P`);密码页按所输密码**授权**: + 查看密码 → 只读权限 / 修改密码 → 修改权限(只有修改权限进入后才能改参数); + 密码值为编译期配置(`ui_pwd_def.h`,查看/修改各一个,改值 = 重新编译) +- 需密码叶子 ENTER:先开密码页;一致后按授予权限进入目标页(页面据此只读/可编辑);ESC 随时返回菜单 +- 无密码叶子直接进,默认只读权限 - 键语义(全部由页面 `key_cb` 处理;`selectable=0`,框架内置 ↑↓ 移选不适用): | 键 | 行为 | |---|---| | ↑ / ↓ | 调整光标位数字值(0–9,环绕:9↔0) | | ← / → | 移动光标(反显框)左/右(环绕:首框↔末框) | -| ENTER | 确认:一致 → 关密码页并打开目标页;不一致 → 3 框清空回 `0`、光标回首框,重输(已确认) | +| ENTER | 确认:修改密码 → 关密码页并以修改权限打开目标页;查看密码 → 以只读权限打开;不一致 → 3 框清空回 `0`、光标回首框,重输(已确认) | | ESC | 返回菜单(框架保证,不可拦截) | **实机视觉(照片;✅ 已确认按此实现)** @@ -296,26 +319,42 @@ typedef struct { | `key_cb` | 非空:↑↓ / ←→ / ENTER 按上表 | | `title` | "用户密钥"(y≈45 中上、"浅 V"关 —— 跟随实机;需下方 ⚠️ 扩展 2) | -**⚠️ 框架扩展需求(落地前必须完成)** +**✅ 框架扩展(已落地,见 §3.4/§3.5)** -1. **← 键拦截(✅ 已确认:描述符拦截开关)**:当前 `cp_key_cb` 将 `LV_KEY_LEFT` 与 ESC 同等对待:在调用 `key_cb` - **之前**即无条件关页返回(ui_content_page.c:345,注释明示"页面钩子不可拦截")——密码页的光标 - ←/→ 移动会被框架吞掉。方案:描述符增加拦截标志(如 `uint8_t key_intercept`),置位时 `LV_KEY_LEFT` - **先**交 `key_cb`,返回 false 才执行内置返回;`LV_KEY_ESC` **不受**拦截("页面恒可退出"的框架 - 保证不放松)。现有 3 页不置标志,行为完全不变 -2. **每页标题 y / "浅 V" 开关(✅ 需要,视觉已确认跟随实机)**:绘制链当前固定标题 `CP_TITLE_Y=2` - 且恒绘"浅 V";实机密码页无装饰、标题在中上部 → 描述符增加标题 y 与装饰开关字段 +1. **← 键拦截**:描述符 `key_intercept`——置位时 `LV_KEY_LEFT` **先**交 `key_cb`(true = 页已处理不关页), + 未置标志或钩子未处理则照旧关页;`LV_KEY_ESC` **不受**拦截("页面恒可退出"的框架保证不放松)。 + 其余 3 页不置标志,行为完全不变 +2. **每页标题 y / "浅 V" / 页脚开关**:描述符 `title_y`(0 = 默认 CP_TITLE_Y)/ `hide_deco` / `hide_footer`; + 密码页取 title_y=44、双开关置位,跟随实机照片 **设计确认进度** | # | 事项 | 状态 | |---|---|---| | 1 | 密码不一致行为 | ✅ 已确认:清空重输(3 框回 `0`、光标回首框) | -| 2 | 每页专属密码的存放与设置 | ✅ 已确认:配置页可设、存 Flash 配置区(配置页本身亦是后续表单/操作页) | +| 2 | 密码值的存放与设置 | ✅ 已确认(2026-09-09 修订):仅编译期配置(`ui_pwd_def.h`,查看/修改各一个值,按所输密码授权),不做运行期设置页、不落 Flash(用户定案:密码只编译期写入) | | 3 | 视觉风格 | ✅ 已确认:跟随实机照片(无"浅 V"、标题中上)→ 需 ⚠️ 扩展 2 | | 4 | ← 键冲突解法 | ✅ 已确认:方案 A 描述符拦截开关(`key_intercept`);`LV_KEY_LEFT` 先交页面钩子,ESC 恒不可拦截,现有 3 页不置开关、零影响 | | 5 | 边界环绕行为 | ✅ 已确认:都环绕(数字 9↔0,光标首框↔末框) | +**落地要点(与代码一致)** + +- **arm API**:`ui_page_password_arm(on_ok)` 由逻辑层在开页前装载完成回调;每次进入重置初始态 + (全 `0`、光标首框,与实机一致)。页面模块不感知菜单,比对 `ui_pwd_def.h` 两个编译期密码 + (先查修改:两值同设时授高权),一致时回调以授予的权限(`ui_perm_t`)为实参 +- **权限注入**:开页 `ui_content_page_open(id, perm)` 携授予权限;描述符 `edit_capable` 声明页面 + 支持编辑模式;页面钩子经 `ui_content_page_is_editable()`(= edit_capable 且修改权限)决定 + 只读/可编辑行为 +- **菜单路由**:需密码叶子 ENTER → `arm(menu_password_ok)` + 开 `CONTENT_PAGE_PASSWORD`; + 密码检查先于内容页判断(带内容页的需密码叶子同样先经密码页); + 确认一致时回调(携权限)**先关密码页回菜单屏、再沿未变导航状态续做"进入"动作**(有内容页→ + 开内容页携权限,否则压层进子框);无密码叶子直接开(默认只读)。 + 先关页是必须的:不关则后续动作落在被盖住的菜单屏上(焦点已移走,按键全错位); + 事件内删对象由 LVGL `e->deleted` 机制保证安全(与 ESC 路径同模式) +- **几何**:标题 y=44;3 个 14×14 数字框、间距 6px、行顶 y=78、首框左 x=53(整组居中);光标框反显 +- **回归**:`check_content_pages.py` 含密码页 4 状态(初始 / 输 1 位 / 正确确认进菜单 / 不一致清空), + pwk 终态经像素级几何断言验证为"4 框菜单 + sel=[2,0,0] + 终点空框" + ## 五、页面模块(ui_page_xxx) 每页一个 `.c/h` 文件对,只含**数据(Flash)+ 描述符对象**,零 LVGL 对象、零机制代码。 @@ -412,12 +451,12 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash ## 八、验证与回归 - **门禁**(`ui-verify` skill / `UI_自动化测试流程.md`): - 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 3 内容页黄金参考逐字节比对 - + 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查 + 0) 模拟器构建 0 警告 → 1) 菜单 8 状态像素回归 + 池基线 → 2) 内容页文本断言 + 11 页黄金参考逐字节比对 + (网口设置 4 态 + 密码页 4 态等)+ 开页-关页往返水位 → 3) Keil 构建 0 错 0 警 → 4) 代码规范检查 - **门禁常量**(脚本头部):`MENU_POOL_BASELINE = 4784 B`、`POOL_AFTER_ROUNDTRIP = 4864 B` - **失败处置**:像素不一致 = 代码 bug(修代码,**禁止**刷参考掩盖);**有意**改视觉 → - 先肉眼确认新效果正确,再 `check_content_pages.py --update` 刷参考并随代码提交 -- 新增页面后同步在 `check_content_pages.py` 增加该页的按键序列与参考图 + 先肉眼确认新效果正确,再 `check_content_pages.py --update` 刷参考(`ref/` 为本地生成物、不入库) +- 新增页面后同步在 `check_content_pages.py` 增加该页的按键序列、文本断言与参考图 ## 九、变更记录 @@ -431,3 +470,6 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash | 2026-09-09 | 密码页设计确认 3 项(§4.1):不一致 = 清空重输;每页专属密码 = 配置页可设 + 存 Flash 配置区;视觉 = 跟随实机照片(由此确定绘制链扩展需求:每页标题 y / "浅 V" 开关);← 键拦截方案与环绕行为仍待确认 | | 2026-09-09 | 密码页 ← 键冲突解法确认(§4.1):方案 A 描述符拦截开关(`key_intercept`:`LV_KEY_LEFT` 先交页面钩子、ESC 恒不可拦截、现有 3 页零影响);框架扩展清单定稿(拦截标志 + 每页标题 y / "浅 V" 开关),仅剩环绕行为待确认 | | 2026-09-09 | 密码页设计定稿(§4.1):边界行为 = 都环绕(数字 9↔0、光标首框↔末框);5 项设计全部确认、无遗留待确认——具备实施条件(框架扩展 + 新页面模块 + 每页保护标志/密码) | +| 2026-09-09 | 密码页落地(§4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API `ui_content_page_invalidate`;新模块 `ui_page_password`(3 数字框反显光标 + arm API,draw_cb+key_cb);菜单受保护叶子路由(arm→开密码页,确认一致后 `menu_password_ok` **先关页再续做进入动作**);字体 97→102 汉字(198 字形);双端注册;回归扩至 7 页 + 往返水位,五门禁全绿 | +| 2026-09-09 | 权限模型(替代"每页专属密码",§4.1):表行尾注记 ` #P`(需密码,无类别);`ui_pwd_def.h` 两个编译期密码(查看 000 / 修改 123)+ 新 `ui_perm_t` 权限枚举——密码页输查看密码授只读权限、输修改密码授修改权限(只有修改权限进入后才能改参数);生成器同步改造并重新生成(pwd 列 = 0/1 布尔,`ui_menu_tab_has_pwd`);框架 `ui_content_page_open(id, perm)` 携授予权限 + 描述符 `edit_capable` + 新 API `ui_content_page_is_editable`(页面钩子据以判只读/可编辑);路由密码检查提到内容页判断之前(修复带内容页需密码叶子绕过密码的路由缺口);无密码叶子直接进、默认只读。§4.1 功能规则/待确认表/落地要点同步。验证:五门禁全绿(菜单 8 态 + 7 页像素一致、池 4784/4864 B 不变、Keil 0 错 0 警、代码规范 PASS) | +| 2026-09-10 | 网口设置内容页:新增 `Drivers/BSP/NETCFG/`(Flash 持久化 magic 'NCFG'+CRC-16/MODBUS,工厂默认 192.168.1.100/24、gw .1、MAC 全零=芯片内置)+ 页面插件 `ui_page_net_cfg`(5 行动态值 + 可编辑:↑↓调字节 0..255 环绕 / ←→移光标 / ENTER 确认,保存行 ENTER → `net_cfg_save` 下次上电生效);框架新增描述符钩子 `init_cb`(开页重置运行态)+ API `ui_content_page_selected_row`;`net_init` 增 `mac` 参数、netTask 改为等 `SYS_EVT_STORAGE_READY` → `net_cfg_load` → `net_init(ip,mask,gw,mac)` → 回写实际 MAC 影子;模拟器 main 调 `net_cfg_load` 使参考图显示工厂默认值;字体修复:lv_font_conv 丢弃低码位符号(U+FF0C 全角逗号 < 0x4E2D 范围起点 → 页脚逗号长期空白)改由 `merge_extra_symbols()` 补回 + 原本缺字(存/密/户/本/用/钥)→ 111 字 / 208 字形;回归扩至 11 页(网口设置 4 态:可编辑/只读/编辑光标反显/保存 OK),五门禁全绿 | diff --git a/docs/UI 设计/菜单页面设计.md b/docs/UI 设计/菜单页面设计.md index a6a276ec..ee393b86 100644 --- a/docs/UI 设计/菜单页面设计.md +++ b/docs/UI 设计/菜单页面设计.md @@ -407,7 +407,9 @@ python tools/menu_gen/gen_menu.py ### 3.7 改菜单流程 ```text -1. 编辑 ui_menu_tree.txt —— 缩进即层级(每级 2 空格),# 为注释;增/删/改名/换序条目都只改这里 +1. 编辑 ui_menu_tree.txt —— 缩进即层级(每级 2 空格),# 为注释;增/删/改名/换序条目都只改这里; + 行尾可选 ` #P` = 该叶子需密码(密码页按所输密码授权查看/修改权限, + 密码值在 Drivers/BSP/UI/ui_pwd_def.h 编译期配置) 2. python tools/menu_gen/gen_menu.py —— 重跑生成器:表 / UI_MENU_TAB_COUNT / 8 位护栏 全部同步更新 3. 完成:导航(逻辑层)与渲染(显示层)代码零改动 ``` @@ -418,7 +420,7 @@ python tools/menu_gen/gen_menu.py | 符号 | 定义处 | 内容 | 占用 | |---|---|---|---| -| `ui_menu_node_t` | `ui_menu_tab.h` | `{ uint8_t parent; const char *name; }` | 类型定义 | +| `ui_menu_node_t` | `ui_menu_tab.h` | `{ uint8_t parent; uint8_t pwd; const char *name; }`(32 位 8 B/项) | 类型定义 | | `g_menu_tab[UI_MENU_TAB_COUNT]` | `ui_menu_tab.c`(生成) | 42 节点扁平父子表 | ≈ 42×8 B ≈ 336 B | | 名字字面量 | 同上 | 42 个 `const char[]` | ≈ 400 B | | `UI_MENU_TAB_COUNT` / `MENU_ROOT` | `ui_menu_tab.h` | 节点数 42 / 根哨兵 0xFF(8 位,上限 255 项) | 宏 | @@ -798,7 +800,7 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响 ## 九、内容页(ui_content_page) -> **一句话职责**:菜单叶子节点进入后的**独立全屏页面**——装置信息(node 1)、交流量(node 3)、直流量(node 4)。与级联框同屏互斥,按需建毁。 +> **一句话职责**:菜单叶子节点进入后的**独立全屏页面**——装置信息(node 1)、交流量(node 3)、直流量(node 4)、用户密钥密码页(受保护叶子,表单/操作页)。与级联框同屏互斥,按需建毁。 > **文件**:`Drivers/BSP/UI/ui_content_page.c/h`(MCU / PC 模拟器双端共用,注册见 `UI_设计架构.md` §1.2/§1.4 约束 5)。 ### 9.1 对象树与生命周期 @@ -831,6 +833,7 @@ screen_menu screen_page = lv_obj_create(NULL) | 页脚(y=143,x=6) | 固定"第 10 页,共 1 页"(按参考图原样) | 动态"第 X 页,共 3 页" | 动态"第 1 页,共 1 页" | - 标题 y=2(居中,浅 V 装饰区内;装饰几何与菜单根框一致:两腿 3px / 水平线 2px) +- **用户密钥页**(表单/操作页,无行槽):title_y=44、"浅 V"/页脚关(hide_deco/hide_footer)、key_intercept=1;自绘 3 个 14×14 数字框(间距 6px、行顶 y=78、首框左 x=53)+ 反显光标,详见 `内容页架构.md` §4.1 - 列表页窗口 = 选中行所在页(首可见行 = `(sel/每页数)×每页数`),行槽位置固定、数据随页切换 - 数据表为 Flash `const cp_row_t[]`(名+值字面量,不 strdup);交流量第 2/3 页当前为占位数据 @@ -839,17 +842,19 @@ screen_menu screen_page = lv_obj_create(NULL) | 键 | 行为 | |---|---| | ↑ / ↓ | 列表页移选(**首尾环绕、跨页连续**),窗口随页切换后重绘;信息页不响应 | -| ESC / ← | 关页返回菜单(导航状态原样还原) | +| ESC | 关页返回菜单(导航状态原样还原;恒不可拦截) | +| ← | `key_intercept=1` 时先交页面 key_cb(true = 已处理不关页,如密码页移光标);未置标志或未处理 → 照旧关页返回 | ### 9.4 公开 API ```c bool ui_content_page_open(content_page_id_t id); /* 开页:建屏+切屏+焦点置页面容器;ID 无效/已有页打开 → false */ void ui_content_page_close(void); /* 关页:焦点回菜单容器 + 切回菜单屏 + 销毁页面 */ +void ui_content_page_invalidate(void); /* 整页重绘(表单/操作页自身状态变化后调用) */ bool ui_content_page_is_open(void); /* 查询是否有内容页打开 */ ``` -- 节点→页映射在逻辑层 `ui_menu.c`(`menu_content_page_for(node)`):node 1 → `CONTENT_PAGE_DEVICE_INFO`、3 → `AC`、4 → `DC`;其余叶子无内容页(进空白终点框) +- 节点→页映射在逻辑层 `ui_menu.c`(`menu_content_page_for(node)`):node 1 → `CONTENT_PAGE_DEVICE_INFO`、3 → `AC`、4 → `DC`;其余叶子无内容页(进空白终点框)。需密码叶子(行尾 ` #P`,如 node 8 参数;密码检查先于内容页判断,带内容页的需密码叶子同样先经密码页)先 arm + 开 `CONTENT_PAGE_PASSWORD`,确认一致后回调(携授予权限) **先关页再续做进入动作**(有内容页→开内容页携权限,页面经 `ui_content_page_is_editable()` 判只读/可编辑);无密码叶子直接开(默认只读) ### 9.5 内存预算(模拟器 x64 实测,40 KB 池) @@ -899,4 +904,6 @@ bool ui_content_page_is_open(void); /* 查询是否有内容页 | 2026-09-08 | **行对象 → 自绘重写(方案1,省 LVGL 池"每行对象机制税")**:行不再是 LVGL 对象——行数据改 `s_row_node[4][16]`(`uint8_t` 节点下标,256 B→64 B),框体 `LV_EVENT_DRAW_MAIN` 自绘(选中行白带 `lv_draw_rect` + 每行 `lv_draw_label` 文本/箭头,按滚动偏移枚举可见行、重绘区外行 culling);框内滚动改 `LV_EVENT_GET_SELF_SIZE` 虚报内容高 + 手动跟随滚动(`view_scroll_to_sel`,替代 `lv_obj_scroll_to_view`);删 `s_style_selected`、`view_add_row` 与 flex 堆行;`frame_build/destroy/refresh` 改行数据注册/清空/重绘。§5.4/§5.6/§6.2/§7.1~7.5 同步。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归(白带/边框/根框不变性 + 3 组往返态逐像素一致)全 PASS | | 2026-09-08 | **4 层容器→单一持久容器 + 根框装饰/状态行自绘(RAM 收敛)**:焦点/键唯一入口 = 持久容器(框深度由逻辑层 `s_depth` 决定,键回调去掉 `user_data=depth`,`container`/`focus` 接口去参);根框装饰(浅 V)与状态行改容器 `DRAW_MAIN` 自绘(3 段 `lv_draw_line` + 1 行 `lv_draw_label`,删 3 `lv_line` + 1 label + 3 容器对象);静态显示层 ~0.5→~0.4 KB。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器 8 状态像素回归全 PASS(装饰/状态行像素级一致);LVGL 池实测 根框 7168→4872 B、最深四级级联 8488→6128 B(省 ~2.3 KB) | | 2026-09-09 | **新增 §九"内容页(ui_content_page)"**:装置信息/交流量/直流量三叶子页落地(独立屏按需建毁 + 行自绘,几何实测自参考图 10/20/21);节点→页映射入逻辑层 `menu_content_page_for`(node 1/3/4),§2.1 叶子行为同步;flat 样式改常量样式(`LV_STYLE_CONST_INIT` 零池分配,动态常驻样式 x64 实测 88 B/个永久占用);关页后一次性残留 +80 B 分解入 §9.5(screens 数组高水位 +8、`lv_screen_load` 菜单屏局部 X/Y 样式 +72,均 LVGL 核心行为非泄漏)。验证:`@build` 0 错 0 警 + 代码规范 PASS + 模拟器三页对照参考图几何一致 + 菜单 8 状态像素回归全 PASS | +| 2026-09-09 | 用户密钥密码页落地(首个表单/操作页,§九 / `内容页架构.md` §4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API `ui_content_page_invalidate`;受保护叶子路由(arm→开密码页,确认一致后回调**先关页再续做进入动作**);§9.2/§9.3/§9.4 同步。验证:回归扩至 7 页 + 往返水位全 PASS、五门禁全绿 | | 2026-09-09 | 菜单显示层 flat 样式同法转常量样式(§5.6/§7.5/§9.5):LVGL 池基线 4872→4784 B、最深四级级联 6128→6040 B、内容页峰值 5648~6064→5560~5976 B(x64 省 88 B);`LV_STYLE_CONST_INIT` 生成外部链接符号,内容页同名者改名 `cp_style_flat`。验证:`@build` 0 错 0 警 + 代码规范 PASS + 菜单 8 状态像素回归全 PASS + 三页几何不变 | +| 2026-09-09 | 权限模型(§3.7/§3.8/§9.4 同步):菜单表行尾注记 ` #P`(需密码);`g_menu_tab` pwd 列改 0/1 布尔(API `ui_menu_tab_has_pwd`,表项仍 8 B/项);两个编译期密码(查看 000 / 修改 123)+ `ui_perm_t` 权限枚举在 `ui_pwd_def.h`(用户定案:密码仅编译期写入,不落 Flash、无运行期设置页)——密码页输查看密码授只读权限、输修改密码授修改权限(只有修改权限进入后才能改参数);逻辑层密码检查提前到内容页判断之前(带内容页的需密码叶子同样先经密码页,修复路由缺口);无密码叶子直接进、默认只读。验证:五门禁全绿(菜单 8 态 + 内容页 7 页像素一致、池 4784/4864 B 不变、Keil 0 错 0 警、代码规范 PASS) | diff --git a/docs/UI 设计/菜单页面设计.pdf b/docs/UI 设计/菜单页面设计.pdf new file mode 100644 index 00000000..88234b6b Binary files /dev/null and b/docs/UI 设计/菜单页面设计.pdf differ diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 7bebacec..4fe5d65b 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -264,6 +264,8 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/ `lftpd_io.c:44,49,54` 的 `malloc` / 对应 `free` 与 `:189` 的 `malloc(sizeof(DIR))` 改为 `pvPortMalloc` / `vPortFree`(加 `#include "FreeRTOS.h"`)。消除"512B C 堆"地雷,FTP 长路径不再随机失败。 +> ✅ **2026-09-10 已落地**:`lftpd_io.c`(3 处 `malloc` + 3 处 `free`)与 `lftpd.c`(7 处 `free`,释放 `lftpd_io_canonicalize_path` 返回的堆指针)全部改用 `pvPortMalloc`/`vPortFree`;两文件加 `#include "FreeRTOS.h"`。lftpd 不参与模拟器构建,仅 MCU 端。Keil 重建 0 错 0 警。 + #### 2.2 缩减 configTOTAL_HEAP_SIZE 阶段 1 完成后复测:堆内需容纳 ≈ 栈(15KB) + TCB(2.3KB) + 队列/事件组(0.5KB) + lftpd 临时分配(1KB) + 余量。`configTOTAL_HEAP_SIZE` 49152 → **32768**,要求全功能负载下 `heap free ≥ 8KB` 持续成立(浸泡 1 小时验证)。 diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 3a09ae20..82d7a801 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -7,7 +7,8 @@ project(lvgl_sim C) set(PROJECT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/..") set(LVGL_DIR "${PROJECT_ROOT}/Lib/lvgl") set(UI_DIR "${PROJECT_ROOT}/Drivers/BSP/UI") -set(SDL2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10") +set(NETCFG_DIR "${PROJECT_ROOT}/Drivers/BSP/NETCFG") +set(SDL2_DIR "${CMAKE_CURRENT_SOURCE_DIR}/SDL2/SDL2-2.32.10") #============================================================================= # 编译标准 @@ -47,6 +48,7 @@ include_directories( ${LVGL_DIR} # lvgl.h ${LVGL_DIR}/src # lvgl 内部头文件 ${UI_DIR} # ui_menu.h, ui_font_cn_12.h + ${NETCFG_DIR} # net_cfg.h(网络配置,双端共享核心) ${PROJECT_ROOT}/Drivers/BSP # dbg_log.h(使用 simulator 的 dbg_cfg.h 配置) ${SDL2_DIR}/include # SDL2 头文件 ) @@ -74,14 +76,22 @@ set(UI_SOURCES ${UI_DIR}/ui_page_device_info.c ${UI_DIR}/ui_page_ac.c ${UI_DIR}/ui_page_dc.c + ${UI_DIR}/ui_page_password.c + ${UI_DIR}/ui_page_net_cfg.c ${UI_DIR}/ui_menu_tab.c ${UI_DIR}/ui_font_cn_12.c ) +# 网络配置(双端共享核心 + 模拟器存储后端;MCU 端编 net_cfg_store_mcu.c,见 Keil 工程) +set(NETCFG_SOURCES + ${NETCFG_DIR}/net_cfg.c + ${NETCFG_DIR}/net_cfg_store_sim.c +) + #============================================================================= # 构建可执行文件 #============================================================================= -add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES}) +add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES}) target_compile_definitions(lvgl_sim PRIVATE LV_CONF_INCLUDE_SIMPLE # 让 lvgl.h 找到 lv_conf.h diff --git a/simulator/README.md b/simulator/README.md index 686018fc..aeb11f05 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -16,10 +16,11 @@ simulator/ ├── sim_port.c / sim_port.h # 模拟器显示/输入端口(SDL 窗口 + 键盘),替代 MCU 端 ui_port.c ├── lv_conf.h # 模拟器专用 LVGL 配置(与 MCU 端 Lib/lv_conf.h 相互独立) ├── i1_to_png.py # 截图转 PNG:把 --screenshot 的 I1(1bpp) 原始位图转成 PNG(见 §3.5) -├── tests/ # 无头回归脚本 + 黄金参考(见 §3.6,流程文档 docs/UI 设计/UI_自动化测试流程.md) +├── tests/ # 无头回归脚本 + 本地黄金参考(见 §3.6,流程文档 docs/UI 设计/UI_自动化测试流程.md) │ ├── check_menu_rows.py # 菜单 8 状态像素回归 + 池基线门禁 -│ ├── check_content_pages.py # 内容页黄金参考回归 + 开页-关页往返池门禁(--update 刷参考) -│ └── ref/ # 黄金参考截图 shot_{info,ac,dc}.bin(I1 原始位图,随仓库提交) +│ ├── check_content_pages.py # 文本断言 + 内容页黄金参考回归 + 往返池门禁(--update 刷参考) +│ ├── regen_ref_png.py # 由 ref/*.bin 批量重生成目检 PNG(--scale 6 --invert) +│ └── ref/ # 黄金参考 shot_*.bin + 目检 *.png(**本地生成物,不入库**) ├── dbg_log.h # 调试日志 stub:DBG_* 映射到 printf(覆盖 MCU 版头文件) ├── SDL2/ │ └── SDL2-2.32.10/ # SDL2 预编译 SDK(随仓库提供,无需安装) @@ -42,7 +43,7 @@ simulator/ | `dbg_log.h` | MCU 版 `dbg_log.h` 依赖 `sys_clock.h`(RTC 时间戳),PC 无法编译。此 stub 把 `DBG_INFO/ERROR/DEBUG` 映射到 `printf`;因 `simulator/` 在 include 路径最前,**优先生效**覆盖 MCU 版 | | `CMakeLists.txt` | 收集 LVGL 源码(`../Lib/lvgl`)+ 共享 UI 源码(`../Drivers/BSP/UI`)+ 本目录端口代码,链接预编译 SDL2,产出 `lvgl_sim.exe` | | `i1_to_png.py` | 把 `--screenshot` 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5 | -| `tests/` | 无头回归脚本(菜单 8 状态像素回归、内容页黄金参考回归)+ 入库黄金参考截图。详见 §3.6 | +| `tests/` | 无头回归脚本(菜单 8 状态像素回归、内容页文本断言 + 黄金参考回归);`ref/` 为**本地生成物、不入库**(首次 `--update` 生成)。详见 §3.6 | > **共享代码不复制**:`ui_menu.c` / `ui_font_cn_12.c` 直接引用 `Drivers/BSP/UI/` 下的原件, > MCU 与 PC 两端永远同步。**不要**在本目录内复制 UI 源码。 @@ -202,16 +203,18 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI 设计/UI_自动化 ```powershell # 仓库根目录执行(前置:本目录已构建出 build/Debug/lvgl_sim.exe) .venv\Scripts\python.exe simulator/tests/check_menu_rows.py # 菜单 8 状态像素回归 + 池基线门禁 -.venv\Scripts\python.exe simulator/tests/check_content_pages.py # 内容页黄金参考回归 + 开页-关页往返池门禁 +.venv\Scripts\python.exe simulator/tests/check_content_pages.py # 文本断言 + 内容页黄金参考回归 + 往返池门禁 ``` | 脚本 | 原理 | 失败输出 | |------|------|----------| | `check_menu_rows.py` | 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;S0 池水位须等于脚本内 `MENU_POOL_BASELINE` | 未过断言清单 + 保留临时截图 | -| `check_content_pages.py` | 三内容页(info=`e` / ac=`dee` / dc=`dede`)无头截图与 `tests/ref/shot_*.bin` **逐字节**比对;另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 差异像素数 + 双方 ink runs 对照 | +| `check_content_pages.py` | ① 文本断言(解析字体字形,在截图内精确匹配期望串,不依赖 `ref/`)② 11 内容页无头截图与 `tests/ref/shot_*.bin` **逐字节**比对;另校验开页-关页往返池水位 = `POOL_AFTER_ROUNDTRIP` | 文本未命中项 / 差异像素数 + 双方 ink runs 对照 | -- **黄金参考更新**(有意改视觉后):先肉眼确认新效果正确,再 - `.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update` 刷新 `ref/`,随代码提交。 +- **黄金参考与目检 PNG**(`ref/` 不入库):首次/换机后先生成一次—— + `.venv\Scripts\python.exe simulator/tests/check_content_pages.py --update` 出 `ref/*.bin`, + `.venv\Scripts\python.exe simulator/tests/regen_ref_png.py` 出目检 `ref/*.png`(默认 `--scale 6 --invert`)。 + 有意改视觉时先肉眼确认新效果正确再 `--update`(会先跑文本断言防误刷)。 - **池水位常量漂移**:菜单/页面结构变更导致门禁失败时,确认新值合理后再更新脚本内常量并登记流程文档变更记录;无法解释的漂移 = 内存回归 bug。 - 退出码均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失等)。 @@ -233,7 +236,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI 设计/UI_自动化 - `LV_USE_THEME_MONO = 1` 且 `sim_port_init` 中调用了 `lv_theme_mono_init` - 主循环调用了 `lv_refr_now(NULL)`(PARTIAL 模式保证首帧) - 没有重复设置 tick(SDL 驱动内部已 `lv_tick_set_cb`,**不要**再 `SDL_AddTimer` + `lv_tick_inc`) -4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 97 汉字 + ASCII,共 193 字形),缺字渲染为空白。 +4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 111 汉字/符号 + ASCII,共 208 字形),缺字渲染为空白。 补字流程见 `docs/UI 设计/UI_中文字体生成与配置.md`(`tools/font_gen/` 两步脚本)。 5. **改了 UI 代码但模拟器没变化**:确认改的是 `Drivers/BSP/UI/ui_menu.c`(共享原件)而非本目录副本(本目录无副本),然后重新 `cmake --build`。 6. **编码**:UI 源码为 UTF-8(带 BOM),MSVC 已加 `/utf-8`。**不要**用 PowerShell `Set-Content` 等重写含中文的文件(编码铁律,见根目录 `AGENTS.md`)。 diff --git a/simulator/main.c b/simulator/main.c index 74037a86..65b999fa 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -35,6 +35,7 @@ #include "lvgl.h" #include "sim_port.h" #include "ui_menu.h" +#include "net_cfg.h" /* 屏幕尺寸(与 MCU 端一致) */ #define SIM_SCREEN_W 160 @@ -114,6 +115,10 @@ static int run_headless(const char * keys, const char * path) } #endif + /* 配置 RAM 副本:镜像 netTask 上电流程(net_cfg_load → 工厂默认), + * 使网口设置内容页显示与真机一致的工厂默认值 */ + (void)net_cfg_load(); + if (ui_menu_init() != 0) { return 1; } @@ -198,6 +203,9 @@ int main(int argc, char **argv) lv_sdl_window_set_title(disp, "STM32F4-Base UI Simulator (160x160 1bpp)"); } + /* 配置 RAM 副本:镜像 netTask 上电流程(net_cfg_load → 工厂默认) */ + (void)net_cfg_load(); + /* 构建菜单 UI(与 MCU 端共用 ui_menu.c) */ if (ui_menu_init() != 0) { fprintf(stderr, "ui_menu_init failed\n"); diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index cd47d764..d5c84abc 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -1,21 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -check_content_pages.py — 三个叶子内容页(装置信息/交流量/直流量)像素黄金参考回归。 +check_content_pages.py — 叶子内容页像素黄金参考回归 + 文本级断言 + 往返池水位门禁。 用法: - python simulator/tests/check_content_pages.py # 与入库黄金参考逐字节比对 - python simulator/tests/check_content_pages.py --update # 有意改视觉后刷新黄金参考 + python simulator/tests/check_content_pages.py # 文本断言 + 与本地黄金参考逐字节比对 + python simulator/tests/check_content_pages.py --update # 有意改视觉后刷新本地黄金参考 前置:simulator 已构建(build/Debug/lvgl_sim.exe 存在);纯标准库,任意 python3。 + ref/*.bin 为**本地生成物、不入库**(.gitignore):首次/换机后须先 `--update` 生成一次。 -原理:每页用键序注入打开(info=e / ac=dee / dc=dede),无头截图 I1(160x160) -与 ref/shot_.bin 逐字节比对——任何像素变化即 FAIL(含字体、几何、颜色)。 -另做开页-关页往返池水位门禁:`es` 后 used 须等于 POOL_AFTER_ROUNDTRIP。 +原理:① 文本级断言——解析 ui_font_cn_12.c 字形,把期望字符串(标题/标签/值/页脚)在截图 + 对应 y 带内做精确匹配;不依赖 ref/,先于像素比对运行,失败即中止(防误刷参考)。 + ② 每页用键序注入打开(info=e / ac=dee / dc=dede / pwd=ddeee 等,见 PAGES),无头截图 + I1(160x160) 与 ref/shot_.bin 逐字节比对——任何像素变化即 FAIL(含字体、几何、颜色)。 +另做开页-关页往返池水位门禁:`es`(info)与 `ddeeesll`(密码页,ESC 后 ←← 弹出路上压的 +2 个菜单框回根框,完全还原初始态)后 used 须等于 POOL_AFTER_ROUNDTRIP。 黄金参考更新流程(有意改视觉时):先肉眼看图确认新效果正确,再 --update 刷新, -最后 git diff 检查 ref/ 变化符合预期。误报排查见 docs/UI 设计/UI_自动化测试流程.md。 +最后 regen_ref_png.py 重生成目检 PNG;误报排查见 docs/UI 设计/UI_自动化测试流程.md。 -退出码:0 全 PASS;1 有页不一致或池水位漂移;2 运行/环境错误(exe 缺失、参考缺失)。 +退出码:0 全 PASS;1 文本断言失败/有页不一致/池水位漂移;2 运行/环境错误(exe 缺失、参考缺失)。 """ import os import re @@ -29,13 +33,72 @@ from pathlib import Path EXE = Path(__file__).resolve().parents[1] / "build" / "Debug" / "lvgl_sim.exe" REF_DIR = Path(__file__).resolve().parent / "ref" -# (页名, 打开键序):与 ui_menu.c menu_content_page_for() 的节点映射一致 +# (页名, 注入键序):与 ui_menu.c 节点映射/路由一致(u=上 d=下 l=左 r=右 e=回车 s=ESC); +# 密码页 = 受保护叶子(参数定值/整定/参数 node 8,默认密码 123) PAGES = [ - ("info", "e"), # 装置信息(node 1) - ("ac", "dee"), # 交流量(node 3,实时数据下第 1 项) - ("dc", "dede"), # 直流量(node 4,实时数据下第 2 项) + ("info", "e"), # 装置信息(node 1) + ("ac", "dee"), # 交流量(node 3,实时数据下第 1 项) + ("dc", "dede"), # 直流量(node 4,实时数据下第 2 项) + ("pwd", "ddeee"), # 密码页初始态:3 框 000、光标首框 + ("pwe", "ddeee uru"), # 密码页调值+移光标:110、光标第 2 框 + ("pwk", "ddeee uruuruuue"), # 密码页输 123:u / r / u,u / r / u,u,u / e → 关页进空白终点框(菜单屏) + ("pwc", "ddeee ue"), # 密码页 100 不一致 → 清空重输(回到 000 初始态像素) + # 网口设置(node 30,通讯参数下第 2 项,需密码): + # dddddd = 根框下移 5 至通讯参数;e 进框;d 选中网口设置;e 进密码页 + ("netcfg", "ddddde de uruuruuue"), # 修改密码 123 → 可编辑态(工厂默认值) + ("netcfg_ro", "ddddde de e"), # 查看密码 000 → 只读态 + ("netcfg_edit", "ddddde de uruuruuue e"), # 编辑模式:IP 首字节光标反显 + ("netcfg_save", "ddddde de uruuruuue dddd e"), # 保存行 ENTER → 值列 OK ] +# 文本级断言表:页名 -> [(名称, 期望串, y 带, 是否反显)] +# 只断言确定的内容;标签与值分列断言(两列 x 不同,不能拼成一串)。 +# 覆盖本工程新增/易缺字的关键文案(网口设置/地址/掩码/逗号/保存/OK 等)。 +TEXTS = { + "info": [ + ("标题", "装置信息", (0, 24), False), + ("副标题", "馈线自动化终端", (22, 40), False), + ("r0名", "终端类型:", (40, 55), False), ("r0值", "F30", (40, 55), False), + ("r1名", "终端型号:", (55, 70), False), ("r1值", "F30", (55, 70), False), + ("r2名", "软件版本:", (70, 85), False), ("r2值", "SV0.010", (70, 85), False), + ("r3名", "硬件版本:", (85, 100), False), ("r3值", "HW0.010", (85, 100), False), + ("r4名", "软件校验:", (100, 115), False), ("r4值", "4454", (100, 115), False), + ("r5名", "程序日期:", (115, 130), False), ("r5值", "2024.08.27", (115, 130), False), + ("页脚", "第 10 页,共 1 页", (140, 160), False), + ], + "netcfg": [ + ("标题", "网口设置", (0, 24), False), + ("r0名", "IP地址", (24, 38), True), ("r0值", "192.168.001.100", (24, 38), True), + ("r1名", "子网掩码", (38, 52), False), ("r1值", "255.255.255.000", (38, 52), False), + ("r2名", "默认网关", (52, 66), False), ("r2值", "192.168.001.001", (52, 66), False), + ("r3名", "MAC地址", (66, 80), False), ("r3值", "000000000000", (66, 80), False), + ("r4名", "保存", (80, 95), False), ("r4值", "ENTER", (80, 95), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "netcfg_ro": [ + ("标题", "网口设置", (0, 24), False), + ("r0名", "IP地址", (24, 38), True), ("r0值", "192.168.001.100", (24, 38), True), + ("r1名", "子网掩码", (38, 52), False), ("r1值", "255.255.255.000", (38, 52), False), + ("r2名", "默认网关", (52, 66), False), ("r2值", "192.168.001.001", (52, 66), False), + ("r3名", "MAC地址", (66, 80), False), ("r3值", "000000000000", (66, 80), False), + ("r4名", "保存", (80, 95), False), ("r4值", "ENTER", (80, 95), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "netcfg_edit": [ + ("标题", "网口设置", (0, 24), False), + ("r0名", "IP地址", (24, 38), True), + ("r0值尾", "168.001.100", (24, 38), True), # 首字节 192 被光标反显覆盖 + ("r1名", "子网掩码", (38, 52), False), + ("r4名", "保存", (80, 95), False), + ], + "netcfg_save": [ + ("标题", "网口设置", (0, 24), False), + ("r0名", "IP地址", (24, 38), False), ("r0值", "192.168.001.100", (24, 38), False), + ("r3名", "MAC地址", (66, 80), False), + ("r4名", "保存", (80, 95), True), ("r4值", "OK", (80, 95), True), + ], +} + W = H = 160 STRIDE = W // 8 # I1:20 字节/行 @@ -81,6 +144,112 @@ def diff_count(a, b): return n +# --------------------------------------------------------------------------- +# 文本级断言(字形模板匹配,独立于像素黄金参考) +# +# 像素 diff 能抓一切视觉变化,但代价是"任何改动都要刷参考";而"缺字显示空白 / +# 标签写错"这类回归用文本断言直接对照期望字符串更稳,且不依赖 ref/。二者互补: +# 文本断言先跑,失败即中止(不误刷像素参考)。 +# --------------------------------------------------------------------------- +FONT_C = Path(__file__).resolve().parents[2] / "Drivers" / "BSP" / "UI" / "ui_font_cn_12.c" + +_GLYPH = None + + +def _parse_font(): + """解析 ui_font_cn_12.c,返回 glyph(cp) -> (adv, box_w, box_h, ofs_x, ofs_y, rows)。""" + src = FONT_C.read_text(encoding="utf-8-sig") + ulists = {} + for m in re.finditer(r"static const uint16_t (unicode_list_\d+)\[\] = \{(.*?)\};", src, re.S): + ulists[m.group(1)] = [int(x, 16) for x in re.findall(r"0x[0-9a-fA-F]+", m.group(2))] + cmaps = [] + for m in re.finditer( + r"\.range_start = (\d+), \.range_length = (\d+), \.glyph_id_start = (\d+),\s*" + r"\.unicode_list = ([^,]+), \.glyph_id_ofs_list = ([^,]+), \.list_length = (\d+), " + r"\.type = (\w+)", src): + cmaps.append((int(m.group(1)), int(m.group(2)), int(m.group(3)), m.group(4).strip(), m.group(7))) + gm = re.search(r"glyph_dsc\[\]\s*=\s*\{", src) + gend = src.find("\n};", gm.end()) + dscs = [(int(a), int(b), int(c), int(d), int(e), int(f)) for a, b, c, d, e, f in re.findall( + r"\{\.bitmap_index = (\d+), \.adv_w = (\d+), \.box_w = (\d+), \.box_h = (\d+), " + r"\.ofs_x = (-?\d+), \.ofs_y = (-?\d+)\}", src[gm.end():gend])] + bm = re.search(r"glyph_bitmap\[\]\s*=\s*\{", src) + bend = src.find("\n};", bm.end()) + body = re.sub(r"/\*.*?\*/", "", src[bm.end():bend], flags=re.S) + bitmap = bytes(int(x, 16) for x in re.findall(r"0x[0-9a-fA-F]+", body)) + + def gid_of(cp): + for rs, rl, gs, ul, typ in cmaps: + if typ.endswith("SPARSE_TINY"): + offs = ulists[ul] + if (cp - rs) in offs: + return gs + offs.index(cp - rs) + elif rs <= cp < rs + rl: + return gs + (cp - rs) + return 0 + + def glyph(cp): + ofs, adv, bw, bh, ox, oy = dscs[gid_of(cp)] + rows = [[(bitmap[ofs + ((y * bw + x) >> 3)] >> (7 - ((y * bw + x) & 7))) & 1 + for x in range(bw)] for y in range(bh)] + return adv, bw, bh, ox, oy, rows + + return glyph + + +def _render(s): + """把字符串渲染成相对原点的墨点集合(LVGL 基线对齐:字形顶 = baseline - box_h - ofs_y)。""" + global _GLYPH + if _GLYPH is None: + _GLYPH = _parse_font() + cur, ink = 0, set() + for ch in s: + adv, bw, bh, ox, oy, rows = _GLYPH(ord(ch)) + for y in range(bh): + for x in range(bw): + if rows[y][x]: + ink.add((cur + ox + x, -bh - oy + y)) + cur += (adv + 8) >> 4 + return ink + + +def _match_text(img, s, band, invert=False): + """在 img 的 y 带内搜索字符串 s 的精确字形渲染,返回命中位置列表。""" + ink = _render(s) + if not ink: + return [] + xs = [p[0] for p in ink] + ys = [p[1] for p in ink] + bx1, bx2, by1, by2 = min(xs), max(xs), min(ys), max(ys) + + def px(x, y): + if x < 0 or x >= W or y < 0 or y >= H: + return 0 + v = (img[y * STRIDE + (x >> 3)] >> (7 - (x & 7))) & 1 + return (1 - v) if invert else v + + hits = [] + for oy in range(band[0] - 6, band[1]): + for ox in range(0, W - (bx2 - bx1)): + ok = True + for ix, iy in ink: + if px(ox + ix, oy + iy) != 1: + ok = False + break + if not ok: + continue + for yy in range(by1, by2 + 1): + for xx in range(bx1, bx2 + 1): + if (xx, yy) not in ink and px(ox + xx, oy + yy) != 0: + ok = False + break + if not ok: + break + if ok: + hits.append((ox, oy)) + return hits + + def main(): update = "--update" in sys.argv[1:] if not EXE.exists(): @@ -89,11 +258,34 @@ def main(): tmp = tempfile.mkdtemp(prefix="dtu_ui_pages_") ok = True + + # 清理手工运行残留的配置存储文件(模拟器工作目录),防"工厂默认值"像素参考被污染 + stale = Path.cwd() / "sim_netcfg.bin" + if stale.exists(): + stale.unlink() + + # 1) 跑全部页面 + 文本级断言预检(先于像素比对/刷新:失败即中止,避免误刷参考) + shots = {} for name, seq in PAGES: - ref = REF_DIR / ("shot_%s.bin" % name) shot = os.path.join(tmp, "shot_%s.bin" % name) run_sim(seq, shot) - data = open(shot, "rb").read() + shots[name] = open(shot, "rb").read() + + for name, _seq in PAGES: + for tname, text, band, inv in TEXTS.get(name, []): + if not _match_text(shots[name], text, band, inv): + ok = False + print("[%s] FAIL 文本断言未命中:%s = %r(y 带 %s 反显=%s)" + % (name, tname, text, band, inv)) + if not ok: + print("FAIL: 文本级断言未通过(缺字/错标签);中止像素比对(现场保留在 %s)" % tmp) + return 1 + print("文本级断言:%d 条全部命中" % sum(len(TEXTS.get(n, [])) for n, _ in PAGES)) + + # 2) 像素黄金参考比对 / 刷新 + for name, seq in PAGES: + ref = REF_DIR / ("shot_%s.bin" % name) + data = shots[name] if update: REF_DIR.mkdir(exist_ok=True) @@ -102,7 +294,7 @@ def main(): continue if not ref.exists(): - print("FAIL: 缺黄金参考 %s(首次运行请加 --update 生成并入库)" % ref) + print("FAIL: 缺黄金参考 %s(首次/换机后请加 --update 生成;ref/ 不入库)" % ref) shutil.rmtree(tmp, ignore_errors=True) return 2 refdata = ref.read_bytes() @@ -117,22 +309,23 @@ def main(): print(" 黄金参考 ink runs: %s" % ink_runs(refdata)) print(" 有意改视觉?确认新效果正确后运行 --update 刷新参考。") - # 开页-关页往返池水位门禁(info 页:e 打开、s 关闭回菜单) - out = run_sim("es", os.path.join(tmp, "shot_rt.bin")) - m = re.search(r"pool after keys: used=(\d+)", out.decode(errors="replace")) - if not m: - print("FAIL: 往返输出未找到 'pool after keys'") - shutil.rmtree(tmp, ignore_errors=True) - return 2 - used = int(m.group(1)) - if update: - print("[rt] 开页-关页往返池水位 %d B(--update 不校验,仅提示)" % used) - elif used != POOL_AFTER_ROUNDTRIP: - ok = False - print("[rt] FAIL 往返池水位 %d != 预期 %d(页面结构变更?确认合理后更新常量)" - % (used, POOL_AFTER_ROUNDTRIP)) - else: - print("[rt] PASS 开页-关页往返池水位 %d B" % used) + # 开页-关页往返池水位门禁(info 页 e/s;密码页 ddee e/s 后 ←← 弹框回根框) + for rt_name, rt_seq in (("info", "es"), ("pwd", "ddeeesll")): + out = run_sim(rt_seq, os.path.join(tmp, "shot_rt_%s.bin" % rt_name)) + m = re.search(r"pool after keys: used=(\d+)", out.decode(errors="replace")) + if not m: + print("FAIL: 往返输出未找到 'pool after keys'(%s %r)" % (rt_name, rt_seq)) + shutil.rmtree(tmp, ignore_errors=True) + return 2 + used = int(m.group(1)) + if update: + print("[rt:%s] 开页-关页往返池水位 %d B(--update 不校验,仅提示)" % (rt_name, used)) + elif used != POOL_AFTER_ROUNDTRIP: + ok = False + print("[rt:%s] FAIL 往返池水位 %d != 预期 %d(页面结构变更?确认合理后更新常量)" + % (rt_name, used, POOL_AFTER_ROUNDTRIP)) + else: + print("[rt:%s] PASS 开页-关页往返池水位 %d B" % (rt_name, used)) if update: print("PASS: 黄金参考已刷新(%d 页)" % len(PAGES)) @@ -140,7 +333,7 @@ def main(): return 0 if ok: - print("PASS: 3 内容页 + 往返池水位全部通过") + print("PASS: %d 内容页 + 往返池水位全部通过" % len(PAGES)) shutil.rmtree(tmp, ignore_errors=True) return 0 # 失败时保留临时截图便于排查 diff --git a/simulator/tests/ref/shot_ac.bin b/simulator/tests/ref/shot_ac.bin deleted file mode 100644 index ba2953f9..00000000 Binary files a/simulator/tests/ref/shot_ac.bin and /dev/null differ diff --git a/simulator/tests/ref/shot_dc.bin b/simulator/tests/ref/shot_dc.bin deleted file mode 100644 index 33d8060c..00000000 Binary files a/simulator/tests/ref/shot_dc.bin and /dev/null differ diff --git a/simulator/tests/ref/shot_info.bin b/simulator/tests/ref/shot_info.bin deleted file mode 100644 index 07d0dc5a..00000000 Binary files a/simulator/tests/ref/shot_info.bin and /dev/null differ diff --git a/simulator/tests/regen_ref_png.py b/simulator/tests/regen_ref_png.py new file mode 100644 index 00000000..475775fe --- /dev/null +++ b/simulator/tests/regen_ref_png.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +regen_ref_png.py — 由黄金参考 .bin 批量重生成目检用 PNG(simulator/tests/ref/*.png)。 + +背景:ref/*.png 是本地目检调试产物(.gitignore 忽略),**不会**随 .bin 参考自动更新。 +刷过 `check_content_pages.py --update` 或改了字体/视觉后,必须重跑本脚本,否则 PNG +仍是旧渲染(缺字/错位,看似"乱码")。 + +用法(仓库根目录;PIL 依赖用 .venv,见 AGENTS.md「Python 环境」): + .venv\\Scripts\\python.exe simulator/tests/regen_ref_png.py + .venv\\Scripts\\python.exe simulator/tests/regen_ref_png.py --scale 4 + .venv\\Scripts\\python.exe simulator/tests/regen_ref_png.py --no-invert # 白字黑底(设备原样) + +默认 `--scale 6 --invert`(深字白底,便于目检)。 +退出码:0 全部成功;1 无 .bin 参考 / 有文件转换失败。 +""" +import argparse +import importlib.util +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent # simulator/tests/ +REF_DIR = HERE / "ref" +I1_TO_PNG = HERE.parent / "i1_to_png.py" # simulator/i1_to_png.py + + +def _load_i1_to_png(): + """从 simulator/i1_to_png.py 载入转换模块(目录不同,不能直接 import)。""" + spec = importlib.util.spec_from_file_location("i1_to_png", I1_TO_PNG) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + + +def main(): + ap = argparse.ArgumentParser(description="由 ref/*.bin 批量重生成目检 PNG") + ap.add_argument("--scale", type=int, default=6, help="放大倍数(默认 6)") + ap.add_argument("--invert", dest="invert", action="store_true", default=True, + help="前景/背景互换:深字白底(默认开,便于目检)") + ap.add_argument("--no-invert", dest="invert", action="store_false", + help="关闭互换:白字黑底(与设备显示一致)") + args = ap.parse_args() + + if not I1_TO_PNG.exists(): + print("FAIL: 找不到 %s" % I1_TO_PNG) + return 1 + + bins = sorted(REF_DIR.glob("*.bin")) + if not bins: + print("FAIL: %s 下没有 .bin 参考(先运行 check_content_pages.py --update)" % REF_DIR) + return 1 + + i1_to_png = _load_i1_to_png() + ok = 0 + for b in bins: + try: + out = i1_to_png.i1_to_png(str(b), scale=args.scale, invert=args.invert) + print(" %-24s -> %s" % (b.name, Path(out).name)) + ok += 1 + except Exception as e: # 单文件失败不中断其余 + print(" %-24s FAIL: %s" % (b.name, e)) + + print("%s: %d/%d 张 PNG 已生成(scale=%d invert=%s)" + % ("PASS" if ok == len(bins) else "FAIL", ok, len(bins), args.scale, args.invert)) + return 0 if ok == len(bins) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/font_gen/make_font.py b/tools/font_gen/make_font.py index 6ec148b3..3b169ac7 100644 --- a/tools/font_gen/make_font.py +++ b/tools/font_gen/make_font.py @@ -127,6 +127,132 @@ def fix_trailing_newline(path): fp.write(fixed) +# --------------------------------------------------------------------------- +# 低码位符号补回(与箭头字形合并同一模式) +# lv_font_conv 的稀疏 cmap 从符号集中最小码位起算,低于该起点的符号被静默丢弃 +# (实测:U+3001 全角逗号 < 0x4E2D 范围起点,未进字体 → 页脚","长期渲染为空白)。 +# 处理:对每个此类符号单独生成迷你字体(真实宋体位图),再并入主字体 +# (位图 + glyph_dsc + 单码位 cmap,幂等)。 +# --------------------------------------------------------------------------- +EXTRA_SYMBOLS = [','] # U+3001 全角逗号(页脚"第 X 页,共 Y 页") + + +def merge_extra_symbols(font_path, size, bpp): + """把 lv_font_conv 丢失的低码位符号从迷你字体并入主字体(逐符号、幂等)。""" + import re + for ch in EXTRA_SYMBOLS: + cp = ord(ch) + raw = open(font_path, 'rb').read() + has_bom = raw.startswith(b'\xef\xbb\xbf') + src = raw.decode('utf-8-sig') + if re.search(r'\.range_start\s*=\s*%d\s*,' % cp, src): + print('U+%04X %r 已存在,跳过合并' % (cp, ch)) + continue + + # 1) 单独生成仅含该符号的迷你字体(真实宋体位图/度量) + tmp = os.path.join(tempfile.gettempdir(), 'font_extra_%04x.c' % cp) + cmd = resolve_lv_font_conv() + ['--font', resolve_font(None), + '--size', str(size), '--bpp', str(bpp), + '--symbols', ch, '--no-compress', + '--format', 'lvgl', '--lv-font-name', 'tmp_extra', + '-o', tmp] + r = subprocess.run(cmd) + if r.returncode != 0: + sys.exit('ERROR: 生成 U+%04X 迷你字体失败' % cp) + mini = open(tmp, encoding='utf-8-sig').read() + + # 2) 从迷你字体提取位图字节 / glyph_dsc 字段 / 码位 + bm = re.search(r'glyph_bitmap\[\]\s*=\s*\{', mini) + end_bm = mini.find('\n};', bm.end()) + body_bm = re.sub(r'/\*.*?\*/', '', mini[bm.end():end_bm], flags=re.S) + bytes_ = re.findall(r'0x[0-9a-fA-F]+', body_bm) + gd = re.search(r'glyph_dsc\[\]\s*=\s*\{', mini) + end_gd = mini.find('\n};', gd.end()) + entries = re.findall(r'\{\.bitmap_index = (\d+), \.adv_w = (\d+), \.box_w = (\d+), ' + r'\.box_h = (\d+), \.ofs_x = (-?\d+), \.ofs_y = (-?\d+)\}', + mini[gd.end():end_gd]) + if len(entries) < 2: + sys.exit('ERROR: 无法解析 U+%04X 迷你字体 glyph_dsc' % cp) + # 取第 1 项(下标 0 是保留空字形) + bmp_ofs, adv, bw, bh, ox, oy = (int(v) for v in entries[1]) + cm = re.search(r'cmaps\[\]\s*=\s*\{', mini) + m = re.search(r'\.range_start\s*=\s*(\d+)\s*,\s*\.range_length\s*=\s*(\d+)', + mini[cm.end():]) + if not m or int(m.group(2)) != 1: + sys.exit('ERROR: U+%04X 迷你字体 cmap 非单码位,无法并入' % cp) + sym_cp, sym_len = int(m.group(1)), 1 + + # 3) 并入主字体:位图追加(偏移平移)+ glyph_dsc 追加 + 单码位 cmap 追加 + body = re.search(r'glyph_bitmap\[\]\s*=\s*\{', src) + end_main_bm = src.find('\n};', body.end()) + main_bytes = len(re.findall(r'0x[0-9a-fA-F]+', + re.sub(r'/\*.*?\*/', '', src[body.end():end_main_bm], flags=re.S))) + glyph_count = len(re.findall(r'\.bitmap_index\s*=\s*\d+', src)) + + bmp = ',\n\n /* U+%04X %r(来源:宋体迷你字体并入;lv_font_conv 丢低码位符号) */\n ' % (cp, ch) \ + + ', '.join(bytes_) + src = src[:end_main_bm] + bmp + src[end_main_bm:] + + gd = re.search(r'glyph_dsc\[\]\s*=\s*\{', src) + end_main_gd = src.find('\n};', gd.end()) + entry = ',\n {.bitmap_index = %d, .adv_w = %d, .box_w = %d, .box_h = %d, ' \ + '.ofs_x = %d, .ofs_y = %d} /* U+%04X */' % ( + main_bytes + bmp_ofs, adv, bw, bh, ox, oy, cp) + src = src[:end_main_gd] + entry + src[end_main_gd:] + + cm = re.search(r'cmaps\[\]\s*=\s*\{', src) + end_main_cm = src.find('\n};', cm.end()) + cmap = ',\n {\n .range_start = %d, .range_length = %d, ' \ + '.glyph_id_start = %d,\n' \ + ' .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, ' \ + '.type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY\n }' % (sym_cp, sym_len, glyph_count) + src = src[:end_main_cm] + cmap + src[end_main_cm:] + + src = re.sub(r'\.cmap_num = (\d+),', + lambda m: '.cmap_num = %d,' % (int(m.group(1)) + 1), src, count=1) + + out = b'\xef\xbb\xbf' if has_bom else b'' + open(font_path, 'wb').write(out + src.encode('utf-8')) + print('已合并 U+%04X %r:%d 字节位图,glyph_id=%d' % (cp, ch, len(bytes_), glyph_count)) + + +def verify_coverage(font_path): + """生成后校验:ui_cn_chars.txt 的全部码位必须进字体 cmap(防静默丢字)。 + + lv_font_conv 对某些码位会静默丢弃(低于稀疏范围起点、或源字体缺字形), + 静默缺字在 UI 上表现为空白,极难发现。此校验把"缺字"变成生成期硬错误。 + 须在 merge_arrow_glyph / merge_extra_symbols 之后调用(合并的符号也要计入)。 + """ + import re + src = open(font_path, encoding='utf-8-sig').read() + # 稀疏 cmap 的 unicode_list_N:十六进制偏移(相对 range_start) + ulists = {} + for m in re.finditer(r'static const uint16_t (unicode_list_\d+)\[\] = \{(.*?)\};', src, re.S): + ulists[m.group(1)] = [int(x, 16) for x in re.findall(r'0x[0-9a-fA-F]+', m.group(2))] + cmaps = [] # (range_start, range_length, sparse_offsets_or_None) + for m in re.finditer(r'\.range_start = (\d+), \.range_length = (\d+), \.glyph_id_start = (\d+),\s*' + r'\.unicode_list = ([^,]+), \.glyph_id_ofs_list = ([^,]+), \.list_length = (\d+), ' + r'\.type = (\w+)', src): + ul = m.group(4).strip() + cmaps.append((int(m.group(1)), int(m.group(2)), ulists.get(ul))) + + def covered(cp): + for rs, rl, offs in cmaps: + if offs is None: + if rs <= cp < rs + rl: + return True + elif (cp - rs) in offs: + return True + return False + + chars = sorted(set(open(CHARS_FILE, encoding='utf-8').read().strip())) + missing = [c for c in chars if not covered(ord(c))] + if missing: + sys.exit('ERROR: 字体缺字 %d 个(UI 将显示空白): %s' + % (len(missing), ' '.join('U+%04X' % ord(c) for c in missing))) + print('coverage OK: %d 个字符全部进字体' % len(chars)) + + def main(): # Windows GBK 控制台无法打印 ▶ 等符号:降级为替换字符,避免 UnicodeEncodeError 崩溃 try: @@ -161,6 +287,8 @@ def main(): if r.returncode != 0: sys.exit('FAILED rc=%d' % r.returncode) merge_arrow_glyph(OUT) # 补回宋体缺失的 ▶ 字形(幂等) + merge_extra_symbols(OUT, args.size, args.bpp) # 补回 lv_font_conv 丢弃的低码位符号(幂等) + verify_coverage(OUT) # 生成后校验:字符集全部码位必须进字体(防静默丢字) fix_trailing_newline(OUT) # 规范化末尾换行(pre-commit end-of-file-fixer) print('OK %d bytes -> %s' % (os.path.getsize(OUT), OUT)) diff --git a/tools/menu_gen/gen_menu.py b/tools/menu_gen/gen_menu.py index 3c9d9fa1..4156be20 100644 --- a/tools/menu_gen/gen_menu.py +++ b/tools/menu_gen/gen_menu.py @@ -3,13 +3,16 @@ """生成菜单节点表 ui_menu_tab.c / ui_menu_tab.h(扁平"父子索引表",输出到 Drivers/BSP/UI/)。 把"菜单长什么样"声明在缩进文本源 ui_menu_tree.txt(缩进即层级,每级 2 空格, -`#` 开头为注释)。本脚本读源、推导每个节点的 parent 下标、产出 C 表,供 ui_menu.c 消费。 +`#` 开头为注释;行尾可选 ` #P` = 该叶子需密码(密码页按所输密码授权:查看/修改); +密码值在 Drivers/BSP/UI/ui_pwd_def.h 编译期配置)。 +本脚本读源、推导每个节点的 parent 下标、产出 C 表,供 ui_menu.c 消费。 改菜单只需编辑 ui_menu_tree.txt 后重跑本脚本(与字体 ui_cn_chars.txt -> make_font.py 同模式)。 用法(仓库根目录或任意目录均可,脚本自动定位仓库根): python tools/menu_gen/gen_menu.py """ import os +import re import sys ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @@ -36,20 +39,27 @@ def parse_tree(text): if '\t' in line: sys.exit('ERROR: 请用空格缩进(勿用 Tab): ' + repr(raw)) indent = len(line) - len(line.lstrip(' ')) - lines.append((indent, stripped)) + name, pwd = stripped, None + m = re.match(r'^(.*?)[ \t]+#([A-Za-z0-9]+)$', stripped) + if m: + if m.group(2) != 'P': + sys.exit('ERROR: 行尾注记仅支持 #P(需密码): %r(出现 #%s;旧 #V/#M 类别注记已废弃,' + '密码值改在 ui_pwd_def.h 配置)' % (raw, m.group(2))) + name, pwd = m.group(1).strip(), True + lines.append((indent, name, pwd)) if not lines: sys.exit('ERROR: ui_menu_tree.txt 无有效节点') - indents = sorted(set(i for i, _ in lines if i > 0)) + indents = sorted(set(i for i, _, _ in lines if i > 0)) unit = indents[0] if indents else 1 - for i, name in lines: + for i, name, _ in lines: if i % unit != 0: sys.exit('ERROR: 缩进不是 %d 的整数倍: %r (indent=%d)' % (unit, name, i)) nodes = [] stack = [] # [(depth, index), ...] 当前各层的父节点下标 - for indent, name in lines: + for indent, name, pwd in lines: depth = indent // unit if depth > MAX_DEPTH: sys.exit('ERROR: 层级过深(>%d): %r' % (MAX_DEPTH, name)) @@ -57,10 +67,10 @@ def parse_tree(text): stack.pop() parent = stack[-1][1] if stack else ROOT_ID idx = len(nodes) - nodes.append((parent, name, depth)) + nodes.append((parent, name, depth, pwd)) stack.append((depth, idx)) - roots = [i for i, (_, _, d) in enumerate(nodes) if d == 0] + roots = [i for i, (_, _, d, _) in enumerate(nodes) if d == 0] if len(roots) != 1: sys.exit('ERROR: 必须有且仅有一个根节点(depth 0),当前 %d 个: %s' % (len(roots), [nodes[i][1] for i in roots])) @@ -70,7 +80,7 @@ def parse_tree(text): def emit(nodes): n = len(nodes) menus = sum(1 for i in range(n) if any(nodes[j][0] == i for j in range(n))) - max_depth = max(d for _, _, d in nodes) + max_depth = max(d for _, _, d, _ in nodes) h = [ '/* 本文件由 tools/menu_gen/gen_menu.py 从 ui_menu_tree.txt 自动生成,请勿手改。', @@ -85,8 +95,10 @@ def emit(nodes): '#define MENU_ROOT 0xFFu /* 根哨兵:仅根节点(主界面)的 parent 取此值;8 位,上限 255 项 */', '', '/* 菜单节点:子项 = parent 等于本下标的所有行(按表序);有子项=菜单,无=叶子 */', + '/* 字段序 parent/pwd 在前:32 位下两项 8 位合并 + 4 位指针 = 8 B/项 */', 'typedef struct {', ' uint8_t parent; /* 父节点下标(0..254);根为 MENU_ROOT(0xFF) */', + ' uint8_t pwd; /* 1 = 需密码(密码页按所输密码授权);0 = 直接进(只读) */', ' const char *name; /* 显示文本(字面量,存 Flash) */', '} ui_menu_node_t;', '', @@ -104,7 +116,8 @@ def emit(nodes): 'uint8_t ui_menu_tab_children(uint8_t node, uint8_t out[], uint8_t max); /* 取 node 直接子项(按表序),最多 max 个;返回实际个数 */', 'bool ui_menu_tab_has_children(uint8_t node); /* 有子项=菜单(带▶);无=叶子 */', 'const char *ui_menu_tab_name(uint8_t node); /* 显示名 */', - 'bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效 */', + 'bool ui_menu_tab_has_pwd(uint8_t node); /* 需密码(密码页按所输密码授权);false = 直接进(只读) */', + 'bool ui_menu_tab_valid(void); /* 启动自检:非根 parent 下标均有效、pwd 为 0/1 */', '', '#endif', '', @@ -118,9 +131,10 @@ def emit(nodes): '', 'const ui_menu_node_t g_menu_tab[UI_MENU_TAB_COUNT] = {', ] - for i, (parent, name, depth) in enumerate(nodes): + for i, (parent, name, depth, pwd) in enumerate(nodes): pstr = 'MENU_ROOT' if parent == ROOT_ID else '%d' % parent - c.append(' { %8s, "%s" }, /* %2d L%d */' % (pstr, name, i, depth)) + cstr = '1u' if pwd else '0u' + c.append(' { %8s, %2s, "%s" }, /* %2d L%d */' % (pstr, cstr, name, i, depth)) c.append('};') c.append('') c += [ @@ -174,6 +188,18 @@ def emit(nodes): '}', '', '/*', + ' * 函数功能:判断节点是否需密码(叶子密码保护)', + ' * 入口参数:node - 节点下标 uint8_t', + ' * 返回值:true - 需密码页(按所输密码授权查看/修改权限);false - 直接进(只读)', + ' * 限定条件:无', + ' * 说明:表 pwd 列取自源 ui_menu_tree.txt 行尾 #P 注记;导航逻辑层据此决定是否先经密码页', + ' */', + 'bool ui_menu_tab_has_pwd(uint8_t node)', + '{', + ' return (g_menu_tab[node].pwd != 0u);', + '}', + '', + '/*', ' * 函数功能:菜单表启动自检', ' * 入口参数:无', ' * 返回值:true - 表有效;false - 存在越界 parent(表内笔误)', @@ -190,6 +216,9 @@ def emit(nodes): ' if (p != MENU_ROOT && p >= UI_MENU_TAB_COUNT) {', ' return false;', ' }', + ' if (g_menu_tab[i].pwd > 1u) {', + ' return false; /* pwd 只取 0/1(行尾注记笔误捕获) */', + ' }', ' }', ' return true;', '}', @@ -213,7 +242,9 @@ def main(): fp.write(header) with open(OUT_C, 'w', encoding='utf-8-sig', newline='') as fp: fp.write(body) - print('nodes=%d menu=%d leaf=%d max_depth=%d' % (len(nodes), menus, len(nodes) - menus, max_depth)) + protected = sum(1 for _, _, _, p in nodes if p) + print('nodes=%d menu=%d leaf=%d protected=%d max_depth=%d' + % (len(nodes), menus, len(nodes) - menus, protected, max_depth)) print('->', os.path.relpath(OUT_H, ROOT)) print('->', os.path.relpath(OUT_C, ROOT)) diff --git a/ui_cn_chars.txt b/ui_cn_chars.txt index 2f9c21cb..64459320 100644 --- a/ui_cn_chars.txt +++ b/ui_cn_chars.txt @@ -1 +1 @@ -▶三主事交作保信值制前动区压参双口启告型复字定实式强归当录息手抖护报据控操故数整无时板查检模死波流测清点界直看类系维网置自行装警讯记设询软运通遥量钟防除面:馈线化终端号件版硬校验程序日期第页共, +▶三主事交件作保信值共关制前动化区压参双口号启告地址型复子字存定实密序式强归当录息户手抖护报据控掩操故数整无日时期本板查校检模死波流测清点版用界直看码硬程端第类系线终维网置自行装警认讯记设询软运通遥量钟钥防除面页馈验默,: diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index e617487d..6994def5 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -1,5 +1,7 @@ # 主菜单层级定义(声明式源) # 规则:缩进即层级,每级 2 空格;`#` 开头为注释;空行忽略。 +# 行尾可选 ` #P` = 该叶子需密码(密码页按所输密码授权:查看密码→只读 / 修改密码→可修改);无尾注 = 不需密码页(直接进,只读)。 +# 密码值:Drivers/BSP/UI/ui_pwd_def.h 编译期配置(查看/修改各一个,改值 = 重新编译)。 # 改菜单只需编辑本文件,然后重跑: # python tools/menu_gen/gen_menu.py # 产物:Drivers/BSP/UI/ui_menu_tab.c / ui_menu_tab.h @@ -11,30 +13,30 @@ 遥信量 参数定值 整定 - 参数 - 定值 - 控制字 - 软压板 + 参数 #P + 定值 #P + 控制字 #P + 软压板 #P 查看 参数 定值 控制字 软压板 三遥设置 - 遥测死区 - 遥测系数 - 遥信类型 - 遥信防抖 - 双点遥信 + 遥测死区 #P + 遥测系数 #P + 遥信类型 #P + 遥信防抖 #P + 双点遥信 #P 装置维护 - 时钟设置 - 强制复归 - 手动录波 - 清除记录 + 时钟设置 #P + 强制复归 #P + 手动录波 #P + 清除记录 #P 通讯参数 - 通讯设置 - 网口设置 - SNTP设置 + 通讯设置 #P + 网口设置 #P + SNTP设置 #P 记录查询 SOE记录 事故记录