diff --git a/.gitignore b/.gitignore index 66e6a6e8..d0373114 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,12 @@ Lib/lvgl/sconscript simulator/build/ sim_dbg.log +# 模拟器宿主侧产物(lvgl_sim.exe / 测试脚本在 CWD 生成,不入库): +# sim_config.ini = net_cfg_load 首启工厂默认落盘;sim_*.log = syslog 后端宿主文件 +sim_config.ini +sim_run.log +sim_selftest.log + # LVGL PC Simulator - screenshot outputs (generated, see simulator/i1_to_png.py) simulator/shot*.png simulator/shot*.bin diff --git a/AGENTS.md b/AGENTS.md index 8412e918..ba5985bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -94,6 +94,7 @@ Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart | `Drivers/BSP/NET/` | BSD Socket API 网络抽象层 + lftpd | | `Drivers/BSP/NETCFG/` | 网口配置(通用配置文件 `/config.ini` [network] 段 + 旧档 NCFG 迁移;网口设置内容页数据源) | | `App/cfg_file.*` + `App/cfg_file_store*` | 通用配置文件引擎(双端共享核心 + 平台存储后端:INI 解析/校验/外科保存/模板/沉降轮询) | +| `App/log/sys_log.h` + `sys_log_mcu.c` | 运行/自检记录引擎(定长 64B 文本记录,落盘 `/log/run.log`·`selftest.log`,defaultTask 唯一写者;UI 页与 FTP 数据源) | | `Drivers/BSP/CH395F/` | CH395F SPI 驱动 | | `Drivers/BSP/GD5F2GQ5UE/` | NAND Flash 驱动 + dhara FTL + FatFS diskio | | `Drivers/BSP/TPAFE5160/` | ADC 并行接口驱动 | @@ -151,6 +152,7 @@ cmake --build build --config Debug - 详见 `docs/UI 设计/LVGL模拟器部署与使用说明.md` - 中文字体:`ui_cn_chars.txt`(字符集)+ `tools/font_gen/` 生成 `Drivers/BSP/UI/ui_font_cn_12.c`,见 `docs/UI 设计/UI_中文字体生成与配置.md` - 配置自检:`simulator\build\Debug\lvgl_sim.exe --cfgtest`(无头、进程内闭环:加载/旧档迁移 → 2s 轮询沉降 → 重载断言 + 外科保存无损回环;0=PASS,详见 `simulator/README.md` §3.7) +- 日志引擎自检:`lvgl_sim.exe --logtest`(post N 条 → count/get 回读断言 + 宿主文件字节级校验;0=PASS);待机屏截图:`--standby shot.bin`;窗口模式空闲超时覆盖 `--idle <秒>` ## 图形绘制(文档配图) diff --git a/App/app_main.c b/App/app_main.c index a1dc763b..47caaf15 100644 --- a/App/app_main.c +++ b/App/app_main.c @@ -8,6 +8,7 @@ * v1.0 2026-07-19 王建锋 创建初始版本 * v1.1 2026-08-29 王建锋 将原 main.c 中各外设初始化统一收归 app_main_init, * 并按"时间子系统优先"的顺序编排启动流程 + * v1.2 2026-09-14 王建锋 新增自检埋点(RTC/NAND/ADC 失败 → sys_log 自检记录)+ syslog_mcu_init() */ #include "app_main.h" @@ -20,6 +21,7 @@ #include "sys_clock.h" #include "gd5f2gq5ue.h" #include "tpafe5160.h" +#include "sys_log.h" #define DBG_TAG "[APP]" #include "dbg_log.h" @@ -41,13 +43,18 @@ * 6) 触发首次 ADC 转换 */ int app_main_init(void) { + /* 日志引擎初始化(创建待发队列;须先于任何 syslog_post) */ + (void)syslog_mcu_init(); + /* * 1) 时间子系统初始化(尽早完成,后续所有日志带时间戳) * sd2506_init() 从 RTC 读取初始时间 * sys_clock_init() 将 RTC 时间转为 Unix epoch,建立墙钟基准 */ sd2506_init(); - sys_clock_init(); + if (sys_clock_init() != 0) { + syslog_post(SYSLOG_SELFTEST, "时钟芯片异常"); + } /* 2) RS-485 初始化(UART5 + PD0 方向控制),初始化即自动开启接收 */ rs485_init(&g_rs485, &huart5, ST_DIR4_GPIO_Port, ST_DIR4_Pin); @@ -61,13 +68,19 @@ int app_main_init(void) { (ret == GD5F_BUSY_TIMEOUT) ? "BUSY TIMEOUT" : (ret == GD5F_ECC_ERROR) ? "ECC ERROR" : (ret == GD5F_PROGRAM_FAIL) ? "PROGRAM FAIL" : - (ret == GD5F_ERASE_FAIL) ? "ERASE FAIL" : - (ret == GD5F_ID_MISMATCH) ? "ID MISMATCH" : "UNKNOWN"); + (ret == GD5F_ERASE_FAIL) ? "ERASE FAIL" : + (ret == GD5F_ID_MISMATCH) ? "ID MISMATCH" : "UNKNOWN"); + if (ret != GD5F_OK) { + syslog_post(SYSLOG_SELFTEST, "NAND异常"); + } /* 4) TPAFE5160 ADC 测试 */ int tp_ret = tpafe5160_init(); DBG_INFO("ADC init: %s", (tp_ret == TPAFE5160_OK) ? "OK" : "TIMEOUT"); + if (tp_ret != TPAFE5160_OK) { + syslog_post(SYSLOG_SELFTEST, "ADC异常"); + } /* 5) SD2506 RTC 诊断信息 */ { diff --git a/App/log/sys_log.h b/App/log/sys_log.h new file mode 100644 index 00000000..828e719d --- /dev/null +++ b/App/log/sys_log.h @@ -0,0 +1,126 @@ +/* + * 模块名称:System Log — 运行/自检记录引擎(双端共享核心) + * 模块功能:定长 64B 文本日志记录的公共 API、格式常量与行拆分; + * MCU 后端 = FatFS /log 目录日志文件(sys_log_mcu.c),模拟器后端 = 宿主文件(sim_syslog.c) + * 适用平台:STM32F407ZGTx + PC 模拟器(共享头,无 OS/LVGL 依赖) + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §4) + */ + +#ifndef SYS_LOG_H +#define SYS_LOG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +/* + * 日志 ID:决定落盘文件与容量上限 + */ +typedef enum { + SYSLOG_RUN = 0u, /* 运行记录 /log/run.log:上电/重启事件 */ + SYSLOG_SELFTEST, /* 自检记录 /log/selftest.log:开机自检故障(仅故障才写) */ + SYSLOG_COUNT /* 哨兵值,非有效 ID */ +} syslog_id_t; + +/* + * 记录格式(定长 64B,一行一条;设计文档 §4.2): + * [0..18] "YYYY-MM-DD HH:MM:SS"(ISO 8601 空格变体,19 字符;墙钟无效 = "1970-01-01 08:00:00") + * [19] ' ' + * [20..29] epoch 秒 ASCII "%010u"(机器解析/上电重启分类用;墙钟无效 = "0000000000") + * [30] ' ' + * [31..58] 描述 UTF-8(≤28B = 9 汉字),不足补 '\0' + * [59..63] '\0' 填充 + */ +#define SYSLOG_REC_SIZE 64u /* 单条记录字节数(定长) */ +#define SYSLOG_TS_LEN 19u /* 时间戳字段长度 "YYYY-MM-DD HH:MM:SS" */ +#define SYSLOG_EPOCH_OFFS 20u /* epoch ASCII 字段偏移 */ +#define SYSLOG_EPOCH_LEN 10u /* epoch ASCII 字段长度(2026 年 Unix 秒 = 10 位) */ +#define SYSLOG_DESC_OFFS 31u /* 描述字段偏移 */ +#define SYSLOG_DESC_MAX 28u /* 描述最大字节数(受框架行缓冲 CP_ROW_BUF_LEN=32 约束) */ + +/* + * 容量上限(条数;满时删除重建,设计文档 §4.3) + */ +#define SYSLOG_RUN_MAX 1024u /* /log/run.log:64KB */ +#define SYSLOG_SELFTEST_MAX 512u /* /log/selftest.log:32KB */ + +/* + * 上电/重启分类窗口(秒):run.log 末条 epoch 距 now < 该值 → "装置重启记录",否则 "装置上电记录" + */ +#define SYSLOG_REBOOT_WINDOW_S 600u + +/* + * 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃) + * 入口参数:id - 日志 ID syslog_id_t + * desc - 描述串(UTF-8,≤SYSLOG_DESC_MAX 字节;超长截断) const char * + * 返回值:无 + * 限定条件:MCU 端 FreeRTOS 启动前调用走静态待发数组(由 defaultTask 挂载后落盘) + */ +void syslog_post(syslog_id_t id, const char *desc); + +/* + * 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1)) + * 入口参数:id - 日志 ID syslog_id_t + * 返回值:条数;文件缺失/未挂载返回 0 uint32_t + */ +uint32_t syslog_count(syslog_id_t id); + +/* + * 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾) + * 入口参数:id - 日志 ID syslog_id_t + * idx - 记录序号(0 起) uint32_t + * p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节;不可为 NULL) char * + * ts_len- p_ts 容量 size_t + * p_desc- 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节;不可为 NULL) char * + * desc_len - p_desc 容量 size_t + * 返回值:0 - 成功;-1 - 越界/读失败 int + */ +int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len); + +/* + * 函数功能:读取末条记录的 epoch 秒(上电/重启分类用) + * 入口参数:id - 日志 ID syslog_id_t + * p_epoch- 输出缓冲(不可为 NULL;无记录时置 0) uint32_t * + * 返回值:0 - 成功读到末条;-1 - 文件缺失/空 int + */ +int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch); + +/* + * MCU 后端专属 API(sys_log_mcu.c;模拟器端无对应实现,勿在共享代码调用) + */ + +/* + * 函数功能:日志引擎初始化(创建 FreeRTOS 消息队列) + * 入口参数:无 + * 返回值:0 - 成功;-1 - 队列创建失败 int + * 限定条件:app_main_init() 起始处调用,先于任何 syslog_post() + */ +int syslog_mcu_init(void); + +/* + * 函数功能:卷挂载成功后的一次性处理(建 /log 目录 + 写"上电/重启"分类记录) + * 入口参数:无 + * 返回值:无 + * 限定条件:defaultTask f_mount/f_mkfs 成功且置位 SYS_EVT_STORAGE_READY 前调用一次 + */ +void syslog_mcu_mounted(void); + +/* + * 函数功能:排空待发日志并落盘(启动前数组 + 运行期队列,FIFO) + * 入口参数:无 + * 返回值:无 + * 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试) + */ +void syslog_drain(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SYS_LOG_H */ diff --git a/App/log/sys_log_mcu.c b/App/log/sys_log_mcu.c new file mode 100644 index 00000000..419390d8 --- /dev/null +++ b/App/log/sys_log_mcu.c @@ -0,0 +1,321 @@ +/* + * 模块名称:System Log (MCU 后端) — 运行/自检记录落盘 + * 模块功能:FatFS /log 目录日志文件(run.log/selftest.log)单写者落盘;启动前静态待发数组 + FreeRTOS 消息队列, + * defaultTask(唯一写者)排空落盘;容量满删除重建;上电/重启分类 + * 适用平台:STM32F407ZGTx + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §4.4) + */ + +/* 头文件包含区 */ +#include +#include + +#define DBG_TAG "[SYSLOG]" +#include "dbg_log.h" + +#include "sys_log.h" +#include "ff.h" +#include "cmsis_os.h" +#include "sys_clock.h" + +/* 静态变量定义区 */ + +static const char *const s_paths[SYSLOG_COUNT] = { "/log/run.log", "/log/selftest.log" }; +static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX }; + +#define SYSLOG_QUEUE_MAX 8u + +/* 待发条目:desc 为调用方静态串(Flash 字面量),只存指针不拷贝 */ +typedef struct { + uint8_t id; + const char *desc; +} syslog_entry_t; + +static syslog_entry_t s_preboot[SYSLOG_QUEUE_MAX]; /* 启动前待发数组(RTOS 前单生产者/后单消费者,免锁) */ +static volatile uint8_t s_preboot_cnt; /* 数组条目数 */ +static osMessageQueueId_t s_queue; /* 运行期消息队列(syslog_mcu_init 创建) */ +static volatile uint8_t s_fs_ready; /* 卷挂载 + /log 目录就绪后置位 */ + +/* 函数定义区 */ + +/* + * 函数功能:追加一条记录(格式化 64B + FA_OPEN_APPEND;容量满先删除重建) + * 入口参数:id - 日志 ID syslog_id_t + * desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char * + * 返回值:0 - 成功;-1 - 写失败 int + * 限定条件:仅 defaultTask(唯一写者)调用;卷已挂载 + */ +static int s_append(syslog_id_t id, const char *desc) { + FIL f; + FRESULT fr; + UINT bw = 0u; + uint8_t rec[SYSLOG_REC_SIZE]; + char ts[SYSLOG_TS_LEN + 1u]; + uint32_t epoch; + size_t dl; + + if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) { + return -1; + } + if (syslog_count(id) >= s_caps[id]) { + fr = f_unlink(s_paths[id]); + DBG_INFO("cap full, recreate %s", s_paths[id]); + if (fr != FR_OK && fr != FR_NO_FILE) { + return -1; + } + } + memset(rec, 0u, sizeof(rec)); + if (sys_clock_get_str(ts, sizeof(ts)) == NULL) { + memcpy(ts, "1970-01-01 08:00:00", SYSLOG_TS_LEN); + } + ts[SYSLOG_TS_LEN] = '\0'; + epoch = sys_clock_get(); + if (epoch == UINT32_MAX) { + epoch = 0u; + } + memcpy(rec, ts, SYSLOG_TS_LEN); + rec[SYSLOG_TS_LEN] = ' '; + (void)snprintf((char *)(rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u, "%010u", (unsigned int)epoch); + rec[SYSLOG_DESC_OFFS - 1u] = ' '; + dl = strlen(desc); + if (dl > SYSLOG_DESC_MAX) { + dl = SYSLOG_DESC_MAX; + } + memcpy(rec + SYSLOG_DESC_OFFS, desc, dl); + + fr = f_open(&f, s_paths[id], FA_OPEN_APPEND); + if (fr != FR_OK) { + DBG_ERROR("open %s fail (%d)", s_paths[id], (int)fr); + return -1; + } + fr = f_write(&f, rec, SYSLOG_REC_SIZE, &bw); + (void)f_close(&f); + if (fr != FR_OK || bw != SYSLOG_REC_SIZE) { + DBG_ERROR("write %s fail (%d)", s_paths[id], (int)fr); + return -1; + } + return 0; +} + +/* + * 函数功能:读取第 idx 条原始记录(64B) + * 入口参数:id - 日志 ID syslog_id_t + * idx - 记录序号(0 起) uint32_t + * p_rec- 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t * + * 返回值:0 - 成功;-1 - 越界/读失败 int + */ +static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) { + FIL f; + FRESULT fr; + UINT br = 0u; + + if (p_rec == NULL || idx >= syslog_count(id)) { + return -1; + } + fr = f_open(&f, s_paths[id], FA_READ); + if (fr != FR_OK) { + return -1; + } + fr = f_lseek(&f, (FSIZE_t)idx * SYSLOG_REC_SIZE); + if (fr == FR_OK) { + fr = f_read(&f, p_rec, SYSLOG_REC_SIZE, &br); + } + (void)f_close(&f); + return (fr == FR_OK && br == SYSLOG_REC_SIZE) ? 0 : -1; +} + +/* + * 函数功能:日志引擎初始化(创建 FreeRTOS 消息队列) + * 入口参数:无 + * 返回值:0 - 成功;-1 - 队列创建失败 int + * 限定条件:app_main_init() 起始处调用,先于任何 syslog_post() + */ +int syslog_mcu_init(void) { + static const osMessageQueueAttr_t s_queue_attr = { .name = "syslog" }; + + s_queue = osMessageQueueNew(SYSLOG_QUEUE_MAX, sizeof(syslog_entry_t), &s_queue_attr); + if (s_queue == NULL) { + DBG_ERROR("queue create fail"); + return -1; + } + return 0; +} + +/* + * 函数功能:卷挂载成功后的一次性处理(建 /log 目录 + 写"上电/重启"分类记录) + * 入口参数:无 + * 返回值:无 + * 限定条件:defaultTask f_mount/f_mkfs 成功且置位 SYS_EVT_STORAGE_READY 前调用一次 + */ +void syslog_mcu_mounted(void) { + FILINFO finfo; + uint32_t last = 0u; + const char *desc = "装置上电记录"; + + if (f_stat("/log", &finfo) != FR_OK) { + FRESULT fr = f_mkdir("/log"); + if (fr != FR_OK && fr != FR_EXIST) { + DBG_ERROR("mkdir /log fail (%d)", (int)fr); + return; + } + } + s_fs_ready = 1u; + + /* 上电/重启分类:末条 epoch 距 now < SYSLOG_REBOOT_WINDOW_S → 重启,否则上电 */ + if (syslog_last_epoch(SYSLOG_RUN, &last) == 0 && sys_clock_get() != UINT32_MAX) { + if ((uint32_t)(sys_clock_get() - last) < SYSLOG_REBOOT_WINDOW_S) { + desc = "装置重启记录"; + } + } + (void)s_append(SYSLOG_RUN, desc); + DBG_INFO("runlog boot entry: %s", desc); +} + +/* + * 函数功能:排空待发日志并落盘(启动前数组 + 运行期队列,FIFO) + * 入口参数:无 + * 返回值:无 + * 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试) + */ +void syslog_drain(void) { + syslog_entry_t e; + + if (!s_fs_ready || s_queue == NULL) { + return; + } + while (s_preboot_cnt > 0u) { + e = s_preboot[0]; + if (s_append((syslog_id_t)e.id, e.desc) != 0) { + break; /* 落盘失败:保留剩余条目,下轮重试 */ + } + memmove(&s_preboot[0], &s_preboot[1], sizeof(syslog_entry_t) * (size_t)(s_preboot_cnt - 1u)); + s_preboot_cnt--; + } + for (;;) { + if (osMessageQueueGet(s_queue, &e, 0u, 0u) != osOK) { + break; /* 队列空 */ + } + if (s_append((syslog_id_t)e.id, e.desc) != 0) { + DBG_ERROR("drop entry id=%d", (int)e.id); + break; /* 落盘失败:停止排空,避免连续丢弃 */ + } + } +} + +/* + * 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃) + * 入口参数:id - 日志 ID syslog_id_t + * desc - 描述串(UTF-8,静态存储期,超长截断) const char * + * 返回值:无 + * 限定条件:FreeRTOS 启动前走静态待发数组;desc 必须为静态/常量串 + */ +void syslog_post(syslog_id_t id, const char *desc) { + syslog_entry_t e; + + if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) { + return; + } + e.id = (uint8_t)id; + e.desc = desc; + if (s_queue != NULL && osKernelGetState() == osKernelRunning) { + if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) { + DBG_ERROR("queue full, drop: %s", desc); + } + } else { + uint8_t n = s_preboot_cnt; + if (n < SYSLOG_QUEUE_MAX) { + s_preboot[n] = e; + s_preboot_cnt = n + 1u; + } else { + DBG_ERROR("preboot full, drop: %s", desc); + } + } +} + +/* + * 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1)) + * 入口参数:id - 日志 ID syslog_id_t + * 返回值:条数;文件缺失/未挂载返回 0 uint32_t + */ +uint32_t syslog_count(syslog_id_t id) { + FILINFO finfo; + + if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) { + return 0u; + } + if (f_stat(s_paths[id], &finfo) != FR_OK) { + return 0u; + } + return (uint32_t)finfo.fsize / SYSLOG_REC_SIZE; +} + +/* + * 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾) + * 入口参数:id - 日志 ID syslog_id_t + * idx - 记录序号(0 起) uint32_t + * p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char * + * ts_len - p_ts 容量 size_t + * p_desc - 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节,不可为 NULL) char * + * desc_len - p_desc 容量 size_t + * 返回值:0 - 成功;-1 - 越界/读失败 int + */ +int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len) { + uint8_t rec[SYSLOG_REC_SIZE]; + size_t dl = 0u; + + if (p_ts == NULL || p_desc == NULL || ts_len <= SYSLOG_TS_LEN || desc_len <= SYSLOG_DESC_MAX) { + return -1; + } + if (s_read_raw(id, idx, rec) != 0) { + return -1; + } + memcpy(p_ts, rec, SYSLOG_TS_LEN); + p_ts[SYSLOG_TS_LEN] = '\0'; + while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') { + dl++; + } + if (dl >= desc_len) { + dl = desc_len - 1u; + } + memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl); + p_desc[dl] = '\0'; + return 0; +} + +/* + * 函数功能:读取末条记录的 epoch 秒(上电/重启分类用) + * 入口参数:id - 日志 ID syslog_id_t + * p_epoch- 输出缓冲(不可为 NULL;无记录时置 0) uint32_t * + * 返回值:0 - 成功读到末条;-1 - 文件缺失/空 int + */ +int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) { + uint8_t rec[SYSLOG_REC_SIZE]; + uint32_t v = 0u; + uint32_t i; + + if (syslog_count(id) == 0u) { + if (p_epoch != NULL) { + *p_epoch = 0u; + } + return -1; + } + if (s_read_raw(id, syslog_count(id) - 1u, rec) != 0) { + if (p_epoch != NULL) { + *p_epoch = 0u; + } + return -1; + } + for (i = 0u; i < SYSLOG_EPOCH_LEN; i++) { + char c = (char)rec[SYSLOG_EPOCH_OFFS + i]; + if (c >= '0' && c <= '9') { + v = v * 10u + (uint32_t)(c - '0'); + } + } + if (p_epoch != NULL) { + *p_epoch = v; + } + return 0; +} diff --git a/App/task/net_task.c b/App/task/net_task.c index bc5e9355..714bcefa 100644 --- a/App/task/net_task.c +++ b/App/task/net_task.c @@ -10,6 +10,7 @@ * v1.1 2026-07-19 王建锋 migrate from freertos.c StartNetTask * v1.2 2026-07-21 王建锋 网络初始化移入本任务(net_init + TCP 监听) * v1.3 2026-07-22 王建锋 单连接模式重构,增加大文件/边界/KeepAlive 测试 + * v1.4 2026-09-14 王建锋 新增自检埋点:PHY 链路 3s 未建立 → sys_log "通讯异常" */ #include @@ -20,6 +21,7 @@ #include "net_cfg.h" #include "ch395f.h" #include "sys_events.h" +#include "sys_log.h" #define DBG_TAG "[NET_TASK]" #include "dbg_log.h" @@ -107,8 +109,13 @@ int net_stack_bring_up(void) { (phy == CH395F_PHY_100M_FULL) ? "100M FULL" : (phy == CH395F_PHY_100M_HALF) ? "100M HALF" : (phy == CH395F_PHY_10M_FULL) ? "10M FULL" : - (phy == CH395F_PHY_10M_HALF) ? "10M HALF" : - (phy == CH395F_PHY_DISCONN) ? "DISCONNECT" : "UNKNOWN"); + (phy == CH395F_PHY_10M_HALF) ? "10M HALF" : + (phy == CH395F_PHY_DISCONN) ? "DISCONNECT" : "UNKNOWN"); + + /* 自检埋点:链路超时未建立 → 记入自检记录(仅故障才写) */ + if (phy == CH395F_PHY_DISCONN) { + syslog_post(SYSLOG_SELFTEST, "通讯异常"); + } /* 网络已重建完成、对外可用:递增代次号,通知各 TCP 服务(FTP/对时/远程显示) * 旧 Socket 已随芯片复位失效、应重建监听 */ diff --git a/App/task/ui_task.c b/App/task/ui_task.c index c3606ae0..1ad33d34 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -5,15 +5,32 @@ * 作者:王建锋 * 创建日期:2026-08-30 * 修改记录: + * v1.1 2026-09-14 王建锋 新增待机显示页初始化(ui_standby_init,空闲 60s 切入) */ #include "ui_task.h" +#include + #include "cmsis_os.h" #include "sys_events.h" #include "ui_port.h" #include "ui_menu.h" #include "ui_net_sink.h" +#include "ui_standby.h" +#include "sys_clock.h" + +/* + * 函数功能:待机屏底部时钟提供回调(平台差异:MCU = sys_clock_get_str) + * 入口参数:buf - 时间串输出缓冲 char * + * len - buf 容量 size_t + * 返回值:无 + */ +static void ui_task_time_cb(char *buf, size_t len) { + if (sys_clock_get_str(buf, len) == NULL && len > 0u) { + buf[0] = '\0'; /* 墙钟无效:空串(待机屏时钟行留白) */ + } +} #define DBG_TAG "[UI_TASK]" #include "dbg_log.h" @@ -59,6 +76,11 @@ void ui_task_func(void *argument) { for (;;) { osDelay(1000); } } DBG_INFO("UI menu init: OK"); + if (ui_standby_init(60000u, ui_task_time_cb) != 0) { + DBG_ERROR("UI standby init: FAIL"); + } else { + DBG_INFO("UI standby: ready (idle=60s)"); + } if (ui_net_sink_start(UI_NET_SINK_PORT) != 0) { DBG_ERROR("UI net sink: start FAIL"); } diff --git a/Drivers/BSP/UI/ui_content_page.c b/Drivers/BSP/UI/ui_content_page.c index 6aa331a1..adf7e7f0 100644 --- a/Drivers/BSP/UI/ui_content_page.c +++ b/Drivers/BSP/UI/ui_content_page.c @@ -43,6 +43,12 @@ #include "ui_page_net_cfg.h" +#include "ui_page_runlog.h" + +#include "ui_page_selftest.h" + +#include "ui_standby.h" + #include "ui_pwd_def.h" #include "ui_menu_view.h" @@ -76,11 +82,13 @@ * 框架机制不变,见 内容页架构.md §七) */ 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, + [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, + [CONTENT_PAGE_RUN_LOG] = &ui_page_runlog_desc, + [CONTENT_PAGE_SELFTEST_LOG] = &ui_page_selftest_desc, }; static content_page_id_t s_open = CONTENT_PAGE_COUNT; /* 当前打开的页(COUNT = 未打开) */ @@ -176,8 +184,9 @@ static void cp_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y ld.text = text; ld.text_static = 1; /* Flash 字符串:不拷内存 */ ld.color = lv_color_white(); - /* 本 LVGL 版 lv_text_get_width 的 length=0 表示"不测量",必须传 strlen */ - ld.ofs_x = (CP_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2; + /* 本 LVGL 版 lv_text_get_width:length=0 表示"不测量"(返回 0); + * length≠0 时循环无 NUL 检查,传 -1 会越串尾死循环——必须传 strlen */ + ld.ofs_x = (CP_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2; ld.ofs_y = 0; lv_draw_label(layer, &ld, &area); } @@ -432,6 +441,8 @@ 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); + ui_standby_notify_activity(); /* 用户活动:重置待机空闲计时 */ + /* ESC:关闭页面返回菜单屏(焦点/导航状态还原;恒不可拦截) */ if (key == LV_KEY_ESC) { ui_content_page_close(); diff --git a/Drivers/BSP/UI/ui_content_page.h b/Drivers/BSP/UI/ui_content_page.h index 98383bfe..8fb1d90f 100644 --- a/Drivers/BSP/UI/ui_content_page.h +++ b/Drivers/BSP/UI/ui_content_page.h @@ -42,6 +42,8 @@ typedef enum { CONTENT_PAGE_DC, /* 直流量(node 4):数据列表,3 项单页 */ CONTENT_PAGE_PASSWORD, /* 用户密钥(密码页):3 位密码,表单/操作页首个实例 */ CONTENT_PAGE_NET_CFG, /* 网口设置(node 30):地址列表 + 保存行,可编辑(修改权限) */ + CONTENT_PAGE_RUN_LOG, /* 运行记录(node 40):动态列表,每记录 2 行(时间戳+描述) */ + CONTENT_PAGE_SELFTEST_LOG, /* 自检记录(node 39):动态列表,每记录 2 行(时间戳+故障) */ CONTENT_PAGE_COUNT /* 哨兵:无效 ID / 未打开 */ } content_page_id_t; diff --git a/Drivers/BSP/UI/ui_font_cn_12.c b/Drivers/BSP/UI/ui_font_cn_12.c index deff186e..33a53019 100644 --- a/Drivers/BSP/UI/ui_font_cn_12.c +++ b/Drivers/BSP/UI/ui_font_cn_12.c @@ -311,6 +311,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x7f, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0xff, 0x80, + /* U+4E0A "上" */ + 0x8, 0x1, 0x0, 0x20, 0x4, 0x0, 0xfc, 0x10, + 0x2, 0x0, 0x40, 0x8, 0x1, 0x0, 0x20, 0x7f, + 0xf0, + /* U+4E3B "主" */ 0x8, 0x0, 0x80, 0x0, 0x3f, 0xe0, 0x40, 0x8, 0x1, 0x3, 0xfe, 0x4, 0x0, 0x80, 0x10, 0x7f, @@ -351,6 +356,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x92, 0x12, 0x7e, 0x48, 0x49, 0xf9, 0x21, 0x2f, 0xf0, + /* U+5165 "入" */ + 0x30, 0x1, 0x0, 0x10, 0x2, 0x0, 0xa0, 0x14, + 0x2, 0x80, 0x88, 0x11, 0x4, 0x11, 0x1, 0x40, + 0x10, + /* U+5171 "共" */ 0x11, 0x2, 0x20, 0x44, 0x3f, 0xe1, 0x10, 0x22, 0x4, 0x47, 0xff, 0x0, 0x2, 0x20, 0x82, 0x20, @@ -466,11 +476,21 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x63, 0xe9, 0x80, 0x4, 0x8, 0x89, 0x11, 0x3f, 0xe0, + /* U+5E38 "常" */ + 0x24, 0x82, 0xa3, 0xff, 0xc0, 0x13, 0xf8, 0x41, + 0xf, 0xe0, 0x20, 0x7f, 0xc8, 0x89, 0x13, 0x2, + 0x0, + /* U+5E8F "序" */ 0x2, 0xf, 0xfd, 0x0, 0x2f, 0xe4, 0x8, 0x9a, 0x10, 0x83, 0xff, 0x42, 0x28, 0x49, 0x8, 0x43, 0x0, + /* U+5F02 "异" */ + 0x7f, 0xc8, 0x9, 0xff, 0x20, 0x4, 0x2, 0x7f, + 0xc4, 0x20, 0x84, 0xff, 0xe2, 0x10, 0x82, 0x20, + 0x40, + /* U+5F0F "式" */ 0x1, 0x40, 0x24, 0x4, 0x7f, 0xf0, 0x10, 0x2, 0x1f, 0x40, 0x88, 0x10, 0x82, 0x14, 0x79, 0xf0, @@ -637,6 +657,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x90, 0x12, 0x2, 0x7f, 0xc0, 0x1, 0x49, 0x44, 0x90, + /* U+7247 "片" */ + 0x1, 0x4, 0x20, 0x84, 0x10, 0x83, 0xfe, 0x40, + 0x8, 0x1, 0xfc, 0x20, 0x88, 0x11, 0x2, 0x40, + 0x40, + /* U+7248 "版" */ 0x10, 0x6a, 0xf1, 0x50, 0x2a, 0x7, 0xfe, 0x8a, 0x51, 0x4b, 0xaa, 0x55, 0x4a, 0x91, 0x65, 0x49, @@ -646,6 +671,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x7f, 0xd1, 0x14, 0x45, 0xff, 0x44, 0x51, 0x17, 0xfd, 0x11, 0x44, 0x51, 0x18, 0x1c, + /* U+7535 "电" */ + 0x8, 0x1, 0x3, 0xfe, 0x44, 0x48, 0x89, 0xff, + 0x22, 0x24, 0x44, 0xff, 0x91, 0x4, 0x20, 0x83, + 0xf0, + /* U+754C "界" */ 0x7f, 0xc8, 0x89, 0xff, 0x22, 0x27, 0xfc, 0x8, 0x6, 0xc7, 0x7, 0x11, 0x2, 0x20, 0x84, 0x20, @@ -723,6 +753,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x8, 0x8, 0x3f, 0xf0, 0x18, 0xf, 0xfe, 0x3, 0x1, 0xff, 0xc0, 0x60, 0x3f, 0xf0, + /* U+82AF "芯" */ + 0x11, 0x2, 0x23, 0xff, 0x88, 0x80, 0x0, 0x8, + 0x4, 0x82, 0x92, 0x50, 0x2a, 0x16, 0x42, 0x87, + 0xc0, + /* U+884C "行" */ 0x10, 0x4, 0xf9, 0x0, 0x48, 0x2, 0xfe, 0xc1, 0x28, 0x21, 0x4, 0x20, 0x84, 0x10, 0x82, 0x11, @@ -763,11 +798,21 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x53, 0xea, 0x45, 0x48, 0xad, 0xf5, 0x20, 0x80, 0x60, + /* U+8BEF "误" */ + 0x8f, 0x89, 0x10, 0x22, 0x7, 0xcc, 0x0, 0xbf, + 0x90, 0x83, 0xff, 0x42, 0xc, 0xa1, 0x22, 0x8, + 0x30, + /* U+8F6F "软" */ 0x22, 0x4, 0x43, 0xcf, 0xa2, 0x16, 0x45, 0x52, 0x3c, 0x41, 0x8, 0x32, 0x9c, 0x50, 0x91, 0x14, 0x10, + /* U+8F93 "输" */ + 0x21, 0x4, 0x53, 0xd1, 0x25, 0xda, 0x1, 0xfc, + 0x4a, 0xa9, 0xf5, 0xea, 0xa5, 0xd4, 0xa8, 0x95, + 0x30, + /* U+8FD0 "运" */ 0x40, 0x4, 0xf8, 0x0, 0x0, 0xe, 0xfe, 0x42, 0x8, 0x81, 0x22, 0x2f, 0xe4, 0x5, 0x40, 0x47, @@ -783,6 +828,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x17, 0xfa, 0x8, 0x49, 0x29, 0xfd, 0x0, 0x5f, 0xf0, + /* U+91CD "重" */ + 0x3, 0xcf, 0x80, 0x10, 0x7f, 0xf4, 0x44, 0xff, + 0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f, + 0xf0, + /* U+91CF "量" */ 0x7f, 0xc8, 0x89, 0x9, 0x7f, 0xf4, 0x44, 0xff, 0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f, @@ -798,6 +848,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x48, 0x8f, 0xdf, 0x22, 0x25, 0x44, 0xc8, 0x92, 0x70, + /* U+9519 "错" */ + 0x42, 0x48, 0x49, 0xdf, 0xc1, 0x2e, 0xfe, 0x80, + 0x11, 0xff, 0xa1, 0x47, 0xea, 0x85, 0x9f, 0xa2, + 0x10, + /* U+9632 "防" */ 0x2, 0x1e, 0x22, 0x7f, 0xc9, 0xa, 0x21, 0x47, 0xe4, 0x8c, 0x91, 0x92, 0x3c, 0x86, 0x10, 0xc4, @@ -962,21 +1017,21 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.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 = 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 = 740, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 757, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 774, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 788, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 804, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 821, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 837, .adv_w = 192, .box_w = 9, .box_h = 10, .ofs_x = 1, .ofs_y = -1}, {.bitmap_index = 849, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 866, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.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 = 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 = 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 = 11, .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}, @@ -984,11 +1039,11 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.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 = 1103, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1120, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1137, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1154, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1171, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .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}, @@ -999,46 +1054,46 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.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}, - {.bitmap_index = 1435, .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 = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1372, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1389, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1406, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1423, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1439, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, {.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 = 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 = 1520, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1537, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1554, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1571, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1588, .adv_w = 192, .box_w = 11, .box_h = 11, .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 = 1638, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1655, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1672, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1689, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1706, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1723, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1737, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1754, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1771, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .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 = 11, .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 = 1904, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1921, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1938, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1955, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1972, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1989, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2006, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .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}, @@ -1050,15 +1105,26 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.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 */ + {.bitmap_index = 2207, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2224, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2241, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2258, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2275, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2292, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2309, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2326, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2343, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2360, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2377, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2394, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2410, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2427, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2444, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2461, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2478, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 2479, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 2481, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */, + {.bitmap_index = 2491, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0} /* U+FF0C */ }; /*--------------------- @@ -1066,20 +1132,22 @@ 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, 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, - 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 + 0x0, 0x1, 0x32, 0x82, 0x9b, 0xed, 0x153, 0x1d4, + 0x1d8, 0x233, 0x35c, 0x368, 0x36a, 0x42d, 0x444, 0x49f, + 0x50d, 0x531, 0x582, 0x5b9, 0x5c3, 0x5da, 0x5ee, 0x626, + 0x641, 0x927, 0x937, 0x982, 0xb04, 0xd47, 0xd4e, 0xd4f, + 0xd91, 0xd95, 0xdbd, 0x102f, 0x1086, 0x10f9, 0x1106, 0x1131, + 0x1149, 0x114a, 0x114c, 0x1266, 0x142e, 0x1442, 0x148d, 0x149b, + 0x149c, 0x1565, 0x159e, 0x15a0, 0x16c4, 0x173c, 0x1767, 0x176b, + 0x17d7, 0x17dc, 0x17ed, 0x1916, 0x1923, 0x1976, 0x19dc, 0x1a18, + 0x1ab7, 0x1c18, 0x1d72, 0x1ed9, 0x1f38, 0x1f42, 0x1ffc, 0x22b0, + 0x243e, 0x243f, 0x271f, 0x272c, 0x2743, 0x28eb, 0x2902, 0x29f8, + 0x2a63, 0x2c02, 0x2ce6, 0x2d23, 0x2e72, 0x2ef2, 0x30b6, 0x30bf, + 0x30eb, 0x3148, 0x3165, 0x33e1, 0x34a6, 0x3a43, 0x3abc, 0x3d5d, + 0x3d9b, 0x3da6, 0x3da7, 0x3db5, 0x3dd9, 0x3de6, 0x4166, 0x418a, + 0x41c7, 0x4211, 0x425c, 0x43c4, 0x43c6, 0x4696, 0x469c, 0x4710, + 0x4829, 0x485b, 0x4959, 0x4a6c, 0x4b7f, 0x4c83, 0x50cf, 0xb103, + 0xb111 }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1091,14 +1159,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 = 110, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 121, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY }, { - .range_start = 9654, .range_length = 1, .glyph_id_start = 206, + .range_start = 9654, .range_length = 1, .glyph_id_start = 217, .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, + .range_start = 65292, .range_length = 1, .glyph_id_start = 218, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY } }; diff --git a/Drivers/BSP/UI/ui_menu.c b/Drivers/BSP/UI/ui_menu.c index b960b325..377e9418 100644 --- a/Drivers/BSP/UI/ui_menu.c +++ b/Drivers/BSP/UI/ui_menu.c @@ -20,6 +20,7 @@ * 列改布尔"需密码"(行尾 #P);密码页按所输密码授权(查看→只读 * / 修改→可修改),授权经回调随开页传给内容页(页面经 * is_editable 判编辑);无密码叶子直接进、默认只读 + * 2026-09-14 王建锋 运行记录/自检记录内容页映射(node 40/39)+ 键事件挂钩待机空闲通知 */ /* 头文件包含区 */ @@ -33,6 +34,8 @@ #include "ui_page_password.h" +#include "ui_standby.h" + #include "ui_menu_tab.h" #include "ui_pwd_def.h" @@ -107,9 +110,10 @@ static void menu_back(void) { * 入口参数:node - 菜单节点下标 uint8_t * 返回值:内容页 ID content_page_id_t;无内容页 = CONTENT_PAGE_COUNT * 限定条件:无 - * 函数说明:仅部分叶子有内容页(参考图 10/20/21 + 网口设置);下标对应 + * 函数说明:仅部分叶子有内容页(参考图 10/20/21 + 网口设置 + 记录查询);下标对应 * ui_menu_tab.c - * node 1=装置信息、3=交流量、4=直流量、30=网口设置(表序变更时须同步本映射) + * node 1=装置信息、3=交流量、4=直流量、30=网口设置、39=自检记录、40=运行记录 + * (表序变更时须同步本映射) */ static content_page_id_t menu_content_page_for(uint8_t node) { switch (node) { @@ -117,6 +121,8 @@ static content_page_id_t menu_content_page_for(uint8_t node) { case 3u: return CONTENT_PAGE_AC; /* 交流量(实时数据子项) */ case 4u: return CONTENT_PAGE_DC; /* 直流量(实时数据子项) */ case 30u: return CONTENT_PAGE_NET_CFG; /* 网口设置(通讯参数子项,需密码) */ + case 39u: return CONTENT_PAGE_SELFTEST_LOG; /* 自检记录(记录查询子项) */ + case 40u: return CONTENT_PAGE_RUN_LOG; /* 运行记录(记录查询子项) */ default: return (content_page_id_t)CONTENT_PAGE_COUNT; /* 无内容页 */ } } @@ -234,6 +240,7 @@ static void menu_key_handle(uint8_t depth, lv_key_t key) { * (焦点恒为持久容器,焦点框深度即 s_depth,无需 user_data 传层) */ static void menu_key_cb(lv_event_t *e) { + ui_standby_notify_activity(); /* 用户活动:重置待机空闲计时 */ menu_key_handle(s_depth, (lv_key_t)lv_event_get_key(e)); } diff --git a/Drivers/BSP/UI/ui_page_password.c b/Drivers/BSP/UI/ui_page_password.c index e8cfbdce..3054dfb0 100644 --- a/Drivers/BSP/UI/ui_page_password.c +++ b/Drivers/BSP/UI/ui_page_password.c @@ -15,9 +15,14 @@ * 2026-09-09 王建锋 改权限模型:arm 只载回调(不再载期望密码);页面比对 * ui_pwd_def.h 两个编译期密码(先查修改),一致时回调携 * 授予的权限(UI_PERM_MODIFY / UI_PERM_VIEW) + * 2026-09-14 王建锋 输错提示:ENTER 不一致显示"密码输入错误",方向键清除后重输 + * 2026-09-14 王建锋 修 lv_text_get_width 传 -1 死循环(本 LVGL 版 length≠0 时 + * while(i + #include "ui_page_password.h" #include "ui_pwd_def.h" @@ -39,6 +44,9 @@ #define PW_TITLE_Y 44 /* 标题放置 y(中上;0 = 框架默认,实机明显低于其余各页的 y=2) */ +#define PW_ERR_Y 104 /* "密码输入错误"提示行顶 y(数字框行之下) */ +#define PW_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致;提示文本居中用) */ + /* 静态变量定义区 */ /* 单位数字文本(Flash):数字 0..9 各 1 字符(text_static,不拷内存) */ @@ -46,9 +54,10 @@ static const char *const s_digit_str[10] = { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" }; -/* 页面运行状态(RAM,约 9 B):3 位值 + 光标位 + 完成回调 */ +/* 页面运行状态(RAM,约 10 B):3 位值 + 光标位 + 错误标志 + 完成回调 */ static uint8_t s_digits[PW_DIGITS]; /* 3 位密码值(0..9) */ static uint8_t s_cur; /* 光标位 0..2(反显框) */ +static uint8_t s_err; /* 错误提示标志(1 = 显示"密码输入错误",方向键清除) */ static void (*s_on_ok)(uint8_t); /* 完成回调(逻辑层预装,实参=授予权限;NULL = 一致时仅关页) */ /* 函数定义区 */ @@ -132,6 +141,33 @@ static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { lv_draw_rect(layer, &fd, &box); pw_draw_digit(layer, &box, s_digits[i], (i == s_cur) ? lv_color_black() : lv_color_white()); } + + if (s_err != 0u) { + /* "密码输入错误"提示(水平居中;1bpp:white=暗墨) */ + lv_draw_label_dsc_t ld; + /* 本 LVGL 版 lv_text_get_width:length=0 = 不测量(返回 0)、length≠0 时 + * while(i 0) { + lv_area_t err_box; + + err_box.x1 = (PW_SCREEN_W - w) / 2; + err_box.x2 = (int32_t)(err_box.x1 + w - 1u); + err_box.y1 = PW_ERR_Y; + err_box.y2 = (int32_t)(err_box.y1 + 15u); /* 覆盖字体行高(13)+ 余量(同框架约定) */ + if (pw_areas_intersect(&err_box, clip)) { + lv_draw_label_dsc_init(&ld); + ld.base.layer = layer; + ld.text = "密码输入错误"; + ld.text_static = 1; + ld.color = lv_color_white(); + ld.ofs_x = 0; + ld.ofs_y = 0; + lv_draw_label(layer, &ld, &err_box); + } + } + } } /* @@ -139,13 +175,17 @@ static void pw_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { * 入口参数:key - LVGL 键值 lv_key_t * 返回值:true - 页已处理(框架跳过内置语义);false - 未处理 * 限定条件:页面打开;经框架键分发调用(uiTask 上下文) - * 函数说明: 1. ↑/↓:调整光标位数字(+1/-1,9↔0 环绕) - * 2. ←/→:移动光标(首↔末环绕;← 键经框架拦截标志转入本函数) + * 函数说明: 1. ↑/↓:调整光标位数字(+1/-1,9↔0 环绕);错误提示态下先清除提示 + * 2. ←/→:移动光标(首↔末环绕;← 键经框架拦截标志转入本函数);同上清提示 * 3. ENTER:修改密码 → 完成回调(授修改权限);查看密码 → 完成回调 - * (授只读权限);不一致 → 清空重输 + * (授只读权限);不一致 → 清空重输 + 显示"密码输入错误" * 4. ESC 不经本函数(框架最优先恒关页);其余键值未处理 */ static bool pw_key_cb(lv_key_t key) { + if (key != LV_KEY_ENTER && s_err != 0u) { + s_err = 0u; /* 错误提示态:方向键先清除提示,再正常执行该键(重新输入) */ + } + switch (key) { case LV_KEY_UP: s_digits[s_cur] = (uint8_t)((s_digits[s_cur] + 1u) % 10u); /* 值 +1,9 环绕 0 */ @@ -172,11 +212,12 @@ static bool pw_key_cb(lv_key_t key) { perm = (uint8_t)UI_PERM_VIEW; /* 查看密码:授只读权限 */ } else { - /* 不一致:清空重输(3 框回 0、光标回首框) */ + /* 不一致:清空重输(3 框回 0、光标回首框)+ 显示错误提示 */ s_digits[0] = 0u; s_digits[1] = 0u; s_digits[2] = 0u; s_cur = 0u; + s_err = 1u; break; } @@ -211,6 +252,7 @@ void ui_page_password_arm(void (*on_ok)(uint8_t perm)) { s_digits[1] = 0u; s_digits[2] = 0u; s_cur = 0u; + s_err = 0u; } /* diff --git a/Drivers/BSP/UI/ui_page_runlog.c b/Drivers/BSP/UI/ui_page_runlog.c new file mode 100644 index 00000000..027635b4 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_runlog.c @@ -0,0 +1,99 @@ +/* + * 模块名称:UI Page RunLog (运行记录内容页) + * 模块功能:运行记录叶子节点(node 40)的内容页数据——动态列表,每条记录 2 行 + * (偶数行 = 时间戳、奇数行 = 描述),可移选分页;数据源 = sys_log API + * (/log/run.log,上电/重启事件);空日志显示单条伪行"无记录"; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §5.3) + */ + +/* 头文件包含区 */ +#include + +#include "ui_page_runlog.h" + +#include "sys_log.h" + +/* 静态变量定义区 */ + +/* 最近读取的记录缓存(偶数行读盘后,奇数行复用;避免同记录重复 I/O) */ +static uint8_t s_cache_valid; /* 缓存有效标志(0 = 无缓存) */ +static uint32_t s_last_rec; /* 已缓存的记录序号 */ +static char s_ts[SYSLOG_TS_LEN + 1u]; +static char s_desc[SYSLOG_DESC_MAX + 1u]; + +/* 函数定义区 */ + +/* + * 函数功能:总行数(动态;每条记录 2 行,空日志 = 单条伪行"无记录") + * 入口参数:无 + * 返回值:总行数 uint16_t + */ +static uint16_t rl_count_cb(void) { + uint32_t n = syslog_count(SYSLOG_RUN); + + if (n == 0u) { + return 1u; /* 空态伪行(页脚"共 1 页",避免"共 0 页") */ + } + return (uint16_t)(n * 2u); +} + +/* + * 函数功能:逐行取数(偶数行 = 时间戳、奇数行 = 描述;值列恒空) + * 入口参数:row - 全局行下标 uint16_t + * name - 名输出缓冲(框架栈缓冲 CP_ROW_BUF_LEN=32,含 NUL) char * + * value - 值输出缓冲(同上;本页恒写空串) char * + * 限定条件:页面打开中(框架绘制/按键上下文,uiTask) + */ +static void rl_refresh(uint16_t row, char *name, char *value) { + uint32_t n = syslog_count(SYSLOG_RUN); + + value[0] = '\0'; + if (n == 0u) { + (void)memcpy(name, "无记录", sizeof("无记录")); + return; + } + + { + uint32_t rec = row / 2u; + + if (!s_cache_valid || rec != s_last_rec) { + if (syslog_get_record(SYSLOG_RUN, rec, s_ts, sizeof(s_ts), s_desc, sizeof(s_desc)) != 0) { + name[0] = '\0'; /* 读失败(越界/存储异常):空行防御 */ + return; + } + s_last_rec = rec; + s_cache_valid = 1u; + } + if ((row % 2u) == 0u) { + (void)memcpy(name, s_ts, sizeof(s_ts)); + } + else { + (void)memcpy(name, s_desc, sizeof(s_desc)); + } + } +} + +/* + * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): + * 动态列表页——每记录 2 行、8 行/页 = 4 条/页;band_h=13(同 AC/DC 约定: + * selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见); + * 时间戳 19 ASCII ≈114px < 屏宽可容 + */ +const content_page_def_t ui_page_runlog_desc = { + .title = "运行记录", + .selectable = 1u, + .top_y = 24, + .pitch = 14, + .band_h = 13, + .text_dy = 0, + .left_x = 7, + .right_x = 101, + .rows_per_page = 8u, + .count_cb = rl_count_cb, + .refresh = rl_refresh, +}; diff --git a/Drivers/BSP/UI/ui_page_runlog.h b/Drivers/BSP/UI/ui_page_runlog.h new file mode 100644 index 00000000..03315bbd --- /dev/null +++ b/Drivers/BSP/UI/ui_page_runlog.h @@ -0,0 +1,24 @@ +/* + * 模块名称:UI Page RunLog (运行记录内容页) + * 模块功能:运行记录叶子节点(node 40)的内容页数据——动态列表,每条记录 2 行 + * (偶数行 = 时间戳、奇数行 = 描述),可移选分页;数据源 = sys_log API + * (/log/run.log,上电/重启事件);空日志显示单条伪行"无记录"; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §5.3) + */ +#ifndef UI_PAGE_RUNLOG_H +#define UI_PAGE_RUNLOG_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):运行记录页 = CONTENT_PAGE_RUN_LOG; + * 接入框架描述符表 s_defs[](定义于 ui_page_runlog.c) + */ +extern const content_page_def_t ui_page_runlog_desc; + +#endif /* UI_PAGE_RUNLOG_H */ diff --git a/Drivers/BSP/UI/ui_page_selftest.c b/Drivers/BSP/UI/ui_page_selftest.c new file mode 100644 index 00000000..5d5f1ed4 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_selftest.c @@ -0,0 +1,99 @@ +/* + * 模块名称:UI Page SelfTest (自检记录内容页) + * 模块功能:自检记录叶子节点(node 39)的内容页数据——动态列表,每条记录 2 行 + * (偶数行 = 时间戳、奇数行 = 故障描述),可移选分页;数据源 = sys_log API + * (/log/selftest.log,开机自检故障,仅故障才写);空日志显示单条伪行"无记录"; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §5.4) + */ + +/* 头文件包含区 */ +#include + +#include "ui_page_selftest.h" + +#include "sys_log.h" + +/* 静态变量定义区 */ + +/* 最近读取的记录缓存(偶数行读盘后,奇数行复用;避免同记录重复 I/O) */ +static uint8_t s_cache_valid; /* 缓存有效标志(0 = 无缓存) */ +static uint32_t s_last_rec; /* 已缓存的记录序号 */ +static char s_ts[SYSLOG_TS_LEN + 1u]; +static char s_desc[SYSLOG_DESC_MAX + 1u]; + +/* 函数定义区 */ + +/* + * 函数功能:总行数(动态;每条记录 2 行,空日志 = 单条伪行"无记录") + * 入口参数:无 + * 返回值:总行数 uint16_t + */ +static uint16_t st_count_cb(void) { + uint32_t n = syslog_count(SYSLOG_SELFTEST); + + if (n == 0u) { + return 1u; /* 空态伪行(页脚"共 1 页",避免"共 0 页") */ + } + return (uint16_t)(n * 2u); +} + +/* + * 函数功能:逐行取数(偶数行 = 时间戳、奇数行 = 故障描述;值列恒空) + * 入口参数:row - 全局行下标 uint16_t + * name - 名输出缓冲(框架栈缓冲 CP_ROW_BUF_LEN=32,含 NUL) char * + * value - 值输出缓冲(同上;本页恒写空串) char * + * 限定条件:页面打开中(框架绘制/按键上下文,uiTask) + */ +static void st_refresh(uint16_t row, char *name, char *value) { + uint32_t n = syslog_count(SYSLOG_SELFTEST); + + value[0] = '\0'; + if (n == 0u) { + (void)memcpy(name, "无记录", sizeof("无记录")); + return; + } + + { + uint32_t rec = row / 2u; + + if (!s_cache_valid || rec != s_last_rec) { + if (syslog_get_record(SYSLOG_SELFTEST, rec, s_ts, sizeof(s_ts), s_desc, sizeof(s_desc)) != 0) { + name[0] = '\0'; /* 读失败(越界/存储异常):空行防御 */ + return; + } + s_last_rec = rec; + s_cache_valid = 1u; + } + if ((row % 2u) == 0u) { + (void)memcpy(name, s_ts, sizeof(s_ts)); + } + else { + (void)memcpy(name, s_desc, sizeof(s_desc)); + } + } +} + +/* + * 页面描述符(外部链接:框架描述符表 s_defs[] 直接引用): + * 动态列表页——每记录 2 行、8 行/页 = 4 条/页;band_h=13(同 AC/DC 约定: + * selectable 页选中行黑字必须配白带,band_h=0 会致选中行不可见); + * 时间戳 19 ASCII ≈114px < 屏宽可容 + */ +const content_page_def_t ui_page_selftest_desc = { + .title = "自检记录", + .selectable = 1u, + .top_y = 24, + .pitch = 14, + .band_h = 13, + .text_dy = 0, + .left_x = 7, + .right_x = 101, + .rows_per_page = 8u, + .count_cb = st_count_cb, + .refresh = st_refresh, +}; diff --git a/Drivers/BSP/UI/ui_page_selftest.h b/Drivers/BSP/UI/ui_page_selftest.h new file mode 100644 index 00000000..6a303e1c --- /dev/null +++ b/Drivers/BSP/UI/ui_page_selftest.h @@ -0,0 +1,24 @@ +/* + * 模块名称:UI Page SelfTest (自检记录内容页) + * 模块功能:自检记录叶子节点(node 39)的内容页数据——动态列表,每条记录 2 行 + * (偶数行 = 时间戳、奇数行 = 故障描述),可移选分页;数据源 = sys_log API + * (/log/selftest.log,开机自检故障,仅故障才写);空日志显示单条伪行"无记录"; + * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §5.4) + */ +#ifndef UI_PAGE_SELFTEST_H +#define UI_PAGE_SELFTEST_H + +#include "ui_content_page.h" + +/* + * 页面描述符(Flash const):自检记录页 = CONTENT_PAGE_SELFTEST_LOG; + * 接入框架描述符表 s_defs[](定义于 ui_page_selftest.c) + */ +extern const content_page_def_t ui_page_selftest_desc; + +#endif /* UI_PAGE_SELFTEST_H */ diff --git a/Drivers/BSP/UI/ui_standby.c b/Drivers/BSP/UI/ui_standby.c new file mode 100644 index 00000000..f9097519 --- /dev/null +++ b/Drivers/BSP/UI/ui_standby.c @@ -0,0 +1,356 @@ +/* + * 模块名称:UI Standby (待机显示页) + * 模块功能:长时间无按键操作时自动切入的默认界面(6 路电流值 + 分隔线 + 底部时钟); + * 屏幕覆盖层模式——进入保存前屏与焦点、ENTER(确认键)还原,其余键忽略; + * 空闲检测 = lv_timer(1s 周期),活动通知由菜单/内容页键事件挂钩 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §5.2) + */ + +/* 头文件包含区 */ +#include + +#include "ui_standby.h" + +#include "lvgl.h" +#include "ui_menu_view.h" + +#define DBG_TAG "[UI_STANDBY]" +#include "dbg_log.h" + +/* 私有宏定义区 */ + +/* + * 布局常量(设备像素;参考 mockups/待机显示页面.jpg): + * 6 路电流值行自 y=28 起、行距 18(末行 y=118..130);分隔线 y=132;底部时钟 y=140 居中 + */ +#define SB_SCREEN_W 160 /* 屏幕宽(px,与 ui_port.h / 模拟器一致) */ +#define SB_SCREEN_H 160 /* 屏幕高(px) */ + +#define SB_CH_COUNT 6u /* 电流通道数 */ +#define SB_ROW_Y0 28 /* 首数据行顶 y */ +#define SB_PITCH 18 /* 行距(相邻行槽顶间距) */ +#define SB_LABEL_X 24 /* 通道名 x(Ib/Ic/...) */ +#define SB_EQ_X 72 /* "=" 号 x */ +#define SB_VALUE_X 96 /* 数值 x(0.00A;v1 占位,待交流量标定管线接入实时值) */ + +#define SB_LINE_Y 132 /* 分隔线 y(高 1px) */ +#define SB_TIME_Y 140 /* 底部时钟放置 y(居中) */ + +/* 静态变量定义区 */ + +/* 通道名与占位数值(Flash;全 ASCII,无需中文字形) */ +static const char *const s_ch_name[SB_CH_COUNT] = { "Ib", "Ic", "Io", "Ia2", "Ib2", "Ic2" }; +static const char *const s_ch_value[SB_CH_COUNT] = { "0.00A", "0.00A", "0.00A", "0.00A", "0.00A", "0.00A" }; + +static lv_obj_t *s_scr; /* 待机屏(init 时建、常驻不销毁) */ +static lv_obj_t *s_cont; /* 全屏容器(焦点/键入口 + 自绘载体,无子对象) */ +static uint32_t s_timeout_ms; /* 空闲超时阈值 */ +static uint32_t s_last_ms; /* 最近一次用户活动时刻(lv_tick ms) */ +static bool s_active; /* 是否处于待机屏 */ +static lv_obj_t *s_prev_scr; /* 进入待机前的活动屏(ENTER 还原用) */ +static lv_obj_t *s_prev_focus; /* 进入待机前组内焦点对象 */ +static ui_standby_time_cb s_time_cb; + +/* flat:透明、无边框/圆角/内边距——剥离 mono 主题给普通 lv_obj 附加的 card 样式(同 ui_content_page.c) */ +static const lv_style_const_prop_t sb_flat_props[] = { + { .prop = LV_STYLE_BG_OPA, .value = { .num = LV_OPA_TRANSP } }, + { .prop = LV_STYLE_BORDER_WIDTH, .value = { .num = 0 } }, + { .prop = LV_STYLE_RADIUS, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_LEFT, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_RIGHT, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_TOP, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_BOTTOM, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_ROW, .value = { .num = 0 } }, + { .prop = LV_STYLE_PAD_COLUMN, .value = { .num = 0 } }, + LV_STYLE_CONST_PROPS_END +}; + +LV_STYLE_CONST_INIT(sb_style_flat, sb_flat_props); + +/* 函数定义区 */ + +/* + * 函数功能:判断两区域是否相交(等价 lv_area_intersect 非空判断;避免依赖 LVGL 私有头) + * 入口参数:a - 区域 1 const lv_area_t * + * b - 区域 2 const lv_area_t * + * 返回值:true - 相交;false - 不相交 + */ +static bool sb_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 * + * text - 文本(栈缓冲,同帧有效) const char * + * y - 放置 y int32_t + */ +static void sb_draw_centered_text(lv_layer_t *layer, const char *text, int32_t y) { + lv_area_t area; + lv_draw_label_dsc_t ld; + + area.x1 = 0; + area.x2 = SB_SCREEN_W - 1; + area.y1 = y; + area.y2 = y + 15; /* 覆盖字体行高(13)+ 余量 */ + + lv_draw_label_dsc_init(&ld); + ld.base.layer = layer; + ld.text = text; + ld.color = lv_color_white(); + ld.ofs_x = (SB_SCREEN_W - lv_text_get_width(text, (uint32_t)strlen(text), LV_FONT_DEFAULT, 0)) / 2; + ld.ofs_y = 0; + lv_draw_label(layer, &ld, &area); +} + +/* + * 函数功能:待机屏自绘(6 路电流值 + 分隔线 + 底部时钟;逐行 culling) + * 入口参数:e - 事件指针 lv_event_t * + * 返回值:无 + * 限定条件:事件为 LV_EVENT_DRAW_MAIN,待机模块已初始化 + * 函数说明:1bpp 极性 white=暗墨——文本/分隔线均白色;数值 v1 占位 0.00A + */ +static void sb_draw_cb(lv_event_t *e) { + lv_layer_t *layer = lv_event_get_layer(e); + + for (uint32_t i = 0u; i < SB_CH_COUNT; i++) { + int32_t row_y = SB_ROW_Y0 + (int32_t)i * SB_PITCH; + lv_area_t strip; + lv_draw_label_dsc_t ld; + + strip.x1 = 0; + strip.x2 = SB_SCREEN_W - 1; + strip.y1 = row_y; + strip.y2 = (int32_t)(row_y + SB_PITCH - 1u); + if (!sb_areas_intersect(&strip, &layer->_clip_area)) { + continue; /* 行不在重绘区:跳过(culling) */ + } + + lv_draw_label_dsc_init(&ld); + ld.base.layer = layer; + ld.text = s_ch_name[i]; + ld.text_static = 1; + ld.color = lv_color_white(); + ld.ofs_x = SB_LABEL_X; + ld.ofs_y = 0; + lv_draw_label(layer, &ld, &strip); + + lv_draw_label_dsc_init(&ld); + ld.base.layer = layer; + ld.text = "="; + ld.text_static = 1; + ld.color = lv_color_white(); + ld.ofs_x = SB_EQ_X; + ld.ofs_y = 0; + lv_draw_label(layer, &ld, &strip); + + lv_draw_label_dsc_init(&ld); + ld.base.layer = layer; + ld.text = s_ch_value[i]; + ld.text_static = 1; + ld.color = lv_color_white(); + ld.ofs_x = SB_VALUE_X; + ld.ofs_y = 0; + lv_draw_label(layer, &ld, &strip); + } + + /* 分隔线(高 1px) */ + { + lv_area_t line; + lv_draw_rect_dsc_t fd; + + line.x1 = 8; + line.x2 = SB_SCREEN_W - 9; + line.y1 = SB_LINE_Y; + line.y2 = SB_LINE_Y; + if (sb_areas_intersect(&line, &layer->_clip_area)) { + lv_draw_rect_dsc_init(&fd); + fd.base.layer = layer; + fd.bg_color = lv_color_white(); + fd.bg_opa = LV_OPA_COVER; + lv_draw_rect(layer, &fd, &line); + } + } + + /* 底部时钟(每秒重绘;time_cb 平台提供) */ + { + lv_area_t t; + + t.x1 = 0; + t.x2 = SB_SCREEN_W - 1; + t.y1 = SB_TIME_Y; + t.y2 = (int32_t)(SB_TIME_Y + 15); + if (sb_areas_intersect(&t, &layer->_clip_area)) { + char ts[24]; + + s_time_cb(ts, sizeof(ts)); + sb_draw_centered_text(layer, ts, SB_TIME_Y); + } + } +} + +/* + * 函数功能:进入待机屏(保存前屏与焦点 → 聚焦容器并切屏) + * 入口参数:无 + * 返回值:无 + * 限定条件:UI 任务上下文;未处于待机 + */ +static void sb_enter(void) { + lv_group_t *g; + + if (s_active || s_scr == NULL) { + return; + } + s_prev_scr = lv_screen_active(); + g = lv_group_get_default(); + s_prev_focus = (g != NULL) ? lv_group_get_focused(g) : NULL; + if (g != NULL) { + lv_group_add_obj(g, s_cont); /* 幂等:已在组内则忽略 */ + lv_group_focus_obj(s_cont); /* 先获焦再切屏,按键立即由本模块接管 */ + } + lv_screen_load(s_scr); + s_active = true; + DBG_INFO("standby: enter"); +} + +/* + * 函数功能:退出待机屏(还原前屏与焦点;重置空闲计时) + * 入口参数:无 + * 返回值:无 + * 限定条件:UI 任务上下文;处于待机 + */ +static void sb_exit(void) { + if (!s_active) { + return; + } + s_active = false; + s_last_ms = lv_tick_get(); + + if (s_prev_focus != NULL && lv_obj_is_valid(s_prev_focus)) { + lv_group_focus_obj(s_prev_focus); + } + else { + /* 前焦点已失效(如内容页被销毁):回落菜单容器 */ + lv_obj_t *mc = ui_menu_view_container(); + if (mc != NULL) { + lv_group_focus_obj(mc); + } + } + if (s_prev_scr != NULL && lv_obj_is_valid(s_prev_scr)) { + lv_screen_load(s_prev_scr); + } + DBG_INFO("standby: exit"); +} + +/* + * 函数功能:待机屏按键事件(仅 ENTER 退出;其余键忽略但重置空闲计时) + * 入口参数:e - 事件指针 lv_event_t * + * 返回值:无 + * 限定条件:容器已获焦 + */ +static void sb_key_cb(lv_event_t *e) { + lv_key_t key = (lv_key_t)lv_event_get_key(e); + + ui_standby_notify_activity(); + if (key == LV_KEY_ENTER) { + sb_exit(); /* 确认键:返回进入前的界面(菜单/内容页原样还原) */ + } +} + +/* + * 函数功能:空闲检测定时器回调(1s 周期;待机中同时驱动时钟每秒重绘) + * 入口参数:t - 定时器句柄 lv_timer_t * + * 返回值:无 + */ +static void sb_idle_timer_cb(lv_timer_t *t) { + (void)t; + + if (!s_active && s_timeout_ms > 0u && lv_tick_elaps(s_last_ms) >= s_timeout_ms) { + sb_enter(); + } + else if (s_active) { + lv_obj_invalidate(s_cont); /* 底部时钟每秒刷新 */ + } +} + +/* + * 函数功能:初始化待机模块(建常驻屏/容器 + 空闲检测定时器) + * 入口参数:idle_timeout_ms - 无操作超时阈值(ms;0 = 拒绝初始化) uint32_t + * time_cb - 底部时钟字符串提供回调(不可为 NULL) ui_standby_time_cb + * 返回值:0 - 成功;-1 - 失败(已初始化/参数无效/LVGL 池耗尽) int + */ +int ui_standby_init(uint32_t idle_timeout_ms, ui_standby_time_cb time_cb) { + if (s_scr != NULL || idle_timeout_ms == 0u || time_cb == NULL) { + return -1; + } + + s_timeout_ms = idle_timeout_ms; + s_time_cb = time_cb; + + s_scr = lv_obj_create(NULL); /* parent=NULL → 新建屏幕(常驻,不随切屏销毁) */ + if (s_scr == NULL) { + return -1; /* LVGL 池耗尽 */ + } + s_cont = lv_obj_create(s_scr); + if (s_cont == NULL) { + lv_obj_delete(s_scr); + s_scr = NULL; + return -1; + } + lv_obj_add_flag(s_cont, LV_OBJ_FLAG_IGNORE_LAYOUT); + lv_obj_set_size(s_cont, SB_SCREEN_W, SB_SCREEN_H); + lv_obj_add_style(s_cont, &sb_style_flat, 0); + lv_obj_add_flag(s_cont, LV_OBJ_FLAG_CLICK_FOCUSABLE); + lv_obj_add_event_cb(s_cont, sb_key_cb, LV_EVENT_KEY, NULL); + lv_obj_add_event_cb(s_cont, sb_draw_cb, LV_EVENT_DRAW_MAIN, NULL); + + s_last_ms = lv_tick_get(); + if (lv_timer_create(sb_idle_timer_cb, 1000u, NULL) == NULL) { + lv_obj_delete(s_cont); + lv_obj_delete(s_scr); + s_scr = NULL; + s_cont = NULL; + return -1; + } + + DBG_INFO("standby: ready (idle=%ums)", (unsigned)idle_timeout_ms); + return 0; +} + +/* + * 函数功能:通知用户活动(重置空闲计时) + * 入口参数:无 + * 返回值:无 + */ +void ui_standby_notify_activity(void) { + if (s_scr != NULL) { + s_last_ms = lv_tick_get(); + } +} + +/* + * 函数功能:查询是否处于待机屏 + * 入口参数:无 + * 返回值:true - 待机中;false - 否 + */ +bool ui_standby_is_active(void) { + return s_active; +} + +/* + * 函数功能:强制进入待机屏(测试钩子,绕过空闲计时) + * 入口参数:无 + * 返回值:无 + */ +void ui_standby_force_enter(void) { + sb_enter(); +} diff --git a/Drivers/BSP/UI/ui_standby.h b/Drivers/BSP/UI/ui_standby.h new file mode 100644 index 00000000..6b7e32ac --- /dev/null +++ b/Drivers/BSP/UI/ui_standby.h @@ -0,0 +1,67 @@ +/* + * 模块名称:UI Standby (待机显示页) + * 模块功能:长时间无按键操作时自动切入的默认界面(6 路电流值 + 分隔线 + 底部时钟); + * 屏幕覆盖层模式——进入保存前屏与焦点、ENTER(确认键)还原,其余键忽略; + * 空闲检测 = lv_timer(1s 周期),活动通知由菜单/内容页键事件挂钩 + * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §5.2) + */ + +#ifndef UI_STANDBY_H +#define UI_STANDBY_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +/* + * 时间字符串提供回调(平台差异:MCU = sys_clock_get_str,模拟器 = 宿主 time()) + */ +typedef void (*ui_standby_time_cb)(char *buf, size_t len); + +/* + * 函数功能:初始化待机模块(建常驻屏/容器 + 空闲检测定时器) + * 入口参数:idle_timeout_ms - 无操作超时阈值(ms;0 = 拒绝初始化) uint32_t + * time_cb - 底部时钟字符串提供回调(不可为 NULL) ui_standby_time_cb + * 返回值:0 - 成功;-1 - 失败(已初始化/参数无效/LVGL 池耗尽) int + * 限定条件:ui_menu_init() 已完成(默认输入组存在);lv_tick_inc/lv_timer_handler 已被驱动 + */ +int ui_standby_init(uint32_t idle_timeout_ms, ui_standby_time_cb time_cb); + +/* + * 函数功能:通知用户活动(重置空闲计时) + * 入口参数:无 + * 返回值:无 + * 限定条件:ui_standby_init() 已成功;任意任务上下文(仅写一个时间戳) + * 函数说明:菜单/内容页每收到一个键调用一次;待机屏上的按键同样经此重置 + */ +void ui_standby_notify_activity(void); + +/* + * 函数功能:查询是否处于待机屏 + * 入口参数:无 + * 返回值:true - 待机中;false - 否 + * 限定条件:无 + */ +bool ui_standby_is_active(void); + +/* + * 函数功能:强制进入待机屏(测试钩子,绕过空闲计时) + * 入口参数:无 + * 返回值:无 + * 限定条件:ui_standby_init() 已成功;UI 任务上下文(模拟器无头回归用) + */ +void ui_standby_force_enter(void); + +#ifdef __cplusplus +} +#endif + +#endif /* UI_STANDBY_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index e46fced0..95f46934 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;../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 + ../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/log;../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 @@ -770,6 +770,11 @@ 1 ..\App\cfg_file_store_mcu.c + + sys_log_mcu.c + 1 + ..\App\log\sys_log_mcu.c + sys_clock.c 1 @@ -3183,6 +3188,21 @@ 1 ..\Drivers\BSP\UI\ui_page_net_cfg.c + + ui_page_runlog.c + 1 + ..\Drivers\BSP\UI\ui_page_runlog.c + + + ui_page_selftest.c + 1 + ..\Drivers\BSP\UI\ui_page_selftest.c + + + ui_standby.c + 1 + ..\Drivers\BSP\UI\ui_standby.c + ui_menu_tab.c 1 diff --git a/Src/freertos.c b/Src/freertos.c index 24959041..97578f84 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -38,6 +38,7 @@ #include "../test/ch395f_test.h" /* ENABLE_NET_LAYER_TESTS:netTask 条件创建 */ #include "time_sync.h" #include "sys_events.h" +#include "sys_log.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -222,18 +223,24 @@ void StartDefaultTask(void *argument) } if (fr == FR_OK) { DBG_INFO("[FS] FATFS mounted"); + /* 日志引擎挂载后处理:建 /log 目录 + 写"上电/重启"分类记录(唯一写者) */ + syslog_mcu_mounted(); /* 广播"存储就绪",唤醒依赖文件系统的任务 */ if (sys_evtHandle != NULL) { osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY); } } else { DBG_ERROR("[FS] f_mount failed (fr=%d)", (int)fr); + /* 存储不可写:自检故障仅能滞留待发队列(串口已告警) */ + syslog_post(SYSLOG_SELFTEST, "文件系统异常"); } for (;;) { HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin); + /* 排空运行期日志(上电/重启分类与自检故障落盘) */ + syslog_drain(); osDelay(500); } /* USER CODE END StartDefaultTask */ diff --git a/docs/UI 设计/UI_中文字体生成与配置.md b/docs/UI 设计/UI_中文字体生成与配置.md index ad96b3c1..ff5a68cb 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` 为本工程实际显示的汉字清单(当前 111 字,含 ▶/,等符号),可留作字符集记录。 +产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 122 字,含 ▶/,等符号),可留作字符集记录。 --- @@ -152,7 +152,7 @@ OK 43614 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 + 111 字(含 ASCII/符号)≈ 43.6KB(RO-data);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 +4. **字体体积**:12px + 1bpp + 122 字(含 ASCII/符号,共 217 字形)≈ 44.4KB(RO-data);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。 5. **换字体**:黑体用 `simhei.ttf`(单文件免抽取);其余字体同理替换 `--font` 路径后重生成。 --- diff --git a/docs/UI 设计/UI_界面制作流程.md b/docs/UI 设计/UI_界面制作流程.md index 2176b481..0c50ff66 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 | 字符集 | 子集字体(当前 111 汉字/符号 + ASCII 0x20-0x7E,共 208 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` | +| C3 | 字符集 | 子集字体(当前 122 汉字/符号 + ASCII 0x20-0x7E,共 217 字形);**缺字显示空白**;新字必须重生成字体 | `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 铁律 | @@ -411,3 +411,4 @@ P4 [ ] UI_设计架构.md 变更记录更新 | 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` 使参考图显示工厂默认值 | | 2026-09-13 | 移除 HTML 样机阶段:五阶段 → **四阶段管线**(P0 需求录入+设计规格 / P1 规格→代码 / P2 模拟器 / P3 MCU / P4 归档);设计基准改为"规格布局参数表 + 参考图",P2 比对基准由"样机渲染图"改为"参考图 + 规格参数表"(根因分流:修代码 / 回 P0 修规格);用户视觉确认点由 HTML 浏览器移到 P2 模拟器窗口/出图;删除 `mockups/01_主界面.html`、`render_01.png`、`tools/ui_mock/render_mockup.py`,保留 `NN_说明.md` 规格与原始参考图;`NN_说明.md` 升格为唯一设计基准(P1 照参数表翻译代码,P2 出图对照参数表 + 原图);`UI_自动化测试流程.md` 门禁编号同步改为 P2/P3 | +| 2026-09-14 | 运行记录/自检记录页 + 待机屏落地后回填:C3 字符集 111→122 字(217 字形,补输/入/错/误/上/电/重/芯/片/异/常——**故障描述串在非 UI 源码中 extract_cn 扫不到,须手动补字**);新文案走 P4 字体重生成 + `check_content_pages.py` pwc 态参考更新(密码错误提示);池基线 4784→5440 / 往返 4864→5520(+656B = 待机模块常驻屏+容器,见两测试脚本头部注记) | diff --git a/docs/UI 设计/UI_设计架构.md b/docs/UI 设计/UI_设计架构.md index ed97c0e3..4024d770 100644 --- a/docs/UI 设计/UI_设计架构.md +++ b/docs/UI 设计/UI_设计架构.md @@ -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,111 汉字/符号 + ASCII 0x20–0x7E,共 208 字形) | `Lib/lv_conf.h` | +| 默认字体 | `ui_font_cn_12`(12px 1bpp,122 汉字/符号 + ASCII 0x20–0x7E,共 217 字形) | `Lib/lv_conf.h` | | uiTask 栈 | 16384 words(BelowNormal) | `Src/freertos.c` | ### 1.4 硬约束(改 UI 必须遵守) @@ -260,3 +260,4 @@ ENTER/→ 进子框: | 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 页,五门禁全绿 | +| 2026-09-14 | 运行记录/自检记录内容页 + 待机显示模块:新增 `App/log/sys_log`(定长 64B 文本记录引擎,MCU 落 NAND `/log/run.log`、`/log/selftest.log`,FTP 可直下)+ 页面插件 `ui_page_runlog` / `ui_page_selftest`(动态列表:count_cb + refresh,每条 2 行时间戳/描述)+ `ui_standby`(空闲超时自动切待机屏,仅确认键回菜单);密码页错误提示"密码输入错误"+清空重输;字体 111→122 字(217 字形);详见 `docs/运行与自检记录设计说明.md` | diff --git a/docs/UI 设计/mockups/待机显示页面.jpg b/docs/UI 设计/mockups/待机显示页面.jpg new file mode 100644 index 00000000..04dd2589 --- /dev/null +++ b/docs/UI 设计/mockups/待机显示页面.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baa38197c987aa697d0a5b75ddadc8b3e8c8a83d344036c9a3de5ad8e48faa15 +size 129679 diff --git a/docs/UI 设计/mockups/自检记录页.jpg b/docs/UI 设计/mockups/自检记录页.jpg new file mode 100644 index 00000000..e491d770 --- /dev/null +++ b/docs/UI 设计/mockups/自检记录页.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63bee25f0a5a18d0e4a9c34c72f6640b87d496f3f4bb2387f293b40e995a33fc +size 414205 diff --git a/docs/UI 设计/mockups/运行记录页.jpg b/docs/UI 设计/mockups/运行记录页.jpg new file mode 100644 index 00000000..dda47441 --- /dev/null +++ b/docs/UI 设计/mockups/运行记录页.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8972c52e60100584d60440126af736337aae38b9be245e6ef76506ef471a5a5e +size 400033 diff --git a/docs/UI 设计/内容页架构.md b/docs/UI 设计/内容页架构.md index 1dbeb164..37f123a9 100644 --- a/docs/UI 设计/内容页架构.md +++ b/docs/UI 设计/内容页架构.md @@ -477,3 +477,4 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash | 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),五门禁全绿 | | 2026-09-10 | 修复网口设置页编辑态渲染 bug:光标段原传整串 `value + start`,而 `lv_draw_label` 按 `coords`(光标盒,仅段宽)折行 → 逐行向盒下方绘制、溢出到其它行与空白区("下方弹出一串字符")。改为只传**段文本**(IP/掩码/网关 3 字符、MAC 2 字符);§3.5 补"`lv_draw_label` 折行陷阱"说明 | | 2026-09-10 | 网口设置页 MAC 值改常识格式 **`A4:94:B6:12:34:56`**(冒号分隔,17 字符 = 102px);布局调整 名 x=6→4、值 x=60→54(MAC ink 止于 155,仍在白带 2..157 内);编辑光标段偏移 MAC 2i→3i | +| 2026-09-14 | 运行记录/自检记录动态列表页(`ui_page_runlog` / `ui_page_selftest`):描述符新增 `count_cb` + `refresh`(开页/翻页时重读条数与内容,每条 2 行 = 时间戳/描述),数据源为 `App/log/sys_log` 引擎的 `/log/run.log`、`/log/selftest.log`;**框架不变量 selectable ⇒ band_h>0**——band_h=0 时选中行恒黑字无白带 → 黑底不可见(首版两页均踩中,改 band_h=13 对齐 AC/DC 约定);新坑:本 LVGL 版本 `lv_text_get_width` length≠0 时无 NUL 检查,传 -1 越串尾死循环,必须传 strlen(密码页错误提示首版触发,已修并全库排查) | diff --git a/docs/UI 设计/菜单页面设计.md b/docs/UI 设计/菜单页面设计.md index 70d00da9..eab851f9 100644 --- a/docs/UI 设计/菜单页面设计.md +++ b/docs/UI 设计/菜单页面设计.md @@ -792,7 +792,7 @@ ESC/LEFT : d>0 → menu_back() (根框 d=0 不响 1. 本 LVGL v9.3 的 `lv_text_get_width(txt, length, font, letter_space)` **length=0 时返回 0**(不是"整串");必须传 `strlen`——否则框宽计算全 0(根框退化 8 px)。 2. 无头模式**不得**调 `lv_timer_handler()` / `lv_refr_now()`:I1 + `LV_DISPLAY_RENDER_MODE_FULL` 全屏刷新会进渲染死循环(截图因此走 `lv_snapshot`);`--keys` 注键是同步处理,无需驱动帧。 3. 选中行**保留 ▶**(随反色变黑):参考图 02/04 原图即"选中行箭头仍在、仅随反色变黑",勿实现成"选中隐箭头"(§4.4)。 -4. 字体缺字形 = 方框:菜单树新增/改名的汉字必须同步加入字符集 `ui_cn_chars.txt`(当前 78 唯一字)并重生成字体(重跑 `extract_cn.py` + `make_font.py`),否则新字显示为方框(流程见 `UI_中文字体生成与配置.md`)。 +4. 字体缺字形 = 方框:菜单树新增/改名的汉字必须同步加入字符集 `ui_cn_chars.txt`(当前 122 唯一字)并重生成字体(重跑 `extract_cn.py` + `make_font.py`),否则新字显示为方框(流程见 `UI_中文字体生成与配置.md`)。 5. **参考图保真度**:参考图为手工 mockup,4 个样本(子框顶位置)不满足单一公式;实现采用自洽规则(顶对齐/底对齐上移),实测与参考图偏差:顶位置 2/4/6/1 px(三遥/装置维护/通讯参数/记录查询),框宽 2/8/0/2 px(4字+箭头/4字/3字/2字+箭头)。两端极例(三遥、记录查询)精确吻合;统一规则优于逐屏特调,偏差范围内不影响识读与操作。 6. 装饰"浅 V"两腿线宽必须 3(水平线 2):1bpp 下细斜线边缘被阈值吃掉,腿不粗于水平线即消失;且依赖 `LV_DRAW_SW_COMPLEX=1`(见 `UI_设计架构.md` §1.4 约束 7)。 diff --git a/docs/运行与自检记录设计说明.md b/docs/运行与自检记录设计说明.md new file mode 100644 index 00000000..bdde0624 --- /dev/null +++ b/docs/运行与自检记录设计说明.md @@ -0,0 +1,277 @@ +# 运行/自检记录与待机显示设计说明 + +> **版本**:v1.1(已实施) +> **日期**:2026-09-14 +> **状态**:S1–S9 全部完成——双端构建 0/0、`--logtest` PASS、ui-verify 五门禁全绿;真机 @test 待上电回归(§8.2) +> **关联文档**:`项目架构总览.md` · `STM32F4-Base存储架构说明.md` · `系统墙钟时间维护说明.md` · `UI 设计/内容页架构.md`(如存在)· `docs/UI 设计/mockups/`(待机显示页面.jpg / 运行记录页.jpg / 自检记录页.jpg / 密码页面实际机器的示例图片.jpg) + +## 1. 背景与需求 + +| # | 需求 | 说明 | +|---|------|------| +| R1 | 密码错误提示 | 用户密钥页输错密码时提示"密码输入错误",并允许重新输入(现状:仅清空重输、无提示) | +| R2 | 待机显示页 | 长时间无操作时默认显示的界面(参考 `mockups/待机显示页面.jpg`:6 路电流值 + 底部时钟);**只有按确认键才进入菜单** | +| R3 | 运行记录页 | 记录装置上电时间、重启时间(参考 `mockups/运行记录页.jpg`:标题 + 逐条"时间戳 + 事件" + 页脚分页) | +| R4 | 自检记录页 | 记录开机自检结果,有问题才记录(参考 `mockups/自检记录页.jpg`) | +| R5 | FTP 可读存储 | 记录以**纯文本文件**落盘,FTP(根目录 = FatFS 卷根)可直接下载读取 | +| R6 | 先调研后设计 | 网络调研存储方式选型 → 写入本设计文档 → 按文档实施 | + +## 2. 现状分析 + +### 2.1 UI 框架(`Drivers/BSP/UI/`,MCU/模拟器共用) + +- **内容页 = 描述符驱动**:`content_page_def_t`(标题/几何/行数据源/4 钩子),框架 `ui_content_page.c` 统一建屏、切屏、焦点、按键分发与绘制;页面模块零 LVGL 对象。 +- **分页已内建**:`selectable=1` + `rows_per_page`(↑↓移选、窗口跟随)+ 页脚"第 X 页,共 Y 页"自动计算;动态行经 `refresh(row, name, value)` 逐行取数(栈缓冲 **32B/列**),行数经 `count_cb()`。 +- **菜单树已含目标叶子**:`ui_menu_tree.txt` "记录查询"下已有 **自检记录 = node 39、运行记录 = node 40**(当前为空白终点框,无内容页映射);无需改树。 +- **密码页** `ui_page_password.c`:3 位数字框 + ↑↓←→/ENTER;输错仅清空重输(R1 缺口)。 +- **按键流**:物理/远端键 → LVGL keypad → 默认输入组 → 焦点对象事件回调(菜单容器 `menu_key_cb` / 内容页容器 `cp_key_cb`)——空闲检测可在此统一挂钩。 +- **字体**:子集位图字体 `ui_font_cn_12`,字符集 `ui_cn_chars.txt`(当前 122 汉字/符号 + ASCII)。 + +### 2.2 存储与时间 + +| 项 | 现状 | +|---|------| +| 卷 | GD5F2GQ5UE SPI NAND **256MB**(2048 块 × 64 页 × 2KB),dhara FTL(磨损均衡/坏块管理),FatFS FAT32,`FF_USE_LFN=0` → **文件名必须 8.3** | +| FTP | lftpd 根目录 = 卷根 `/`;现有文件 `/config.ini`、`/cfg/netcfg.bin`(均 8.3) | +| 并发 | `FF_FS_REENTRANT=1` + 系统锁(`ffsystem.c` osMutex):单次 `f_*` 调用受保护,**多调用序列(open→seek→write→close)需单写者纪律** | +| 墙钟 | `sys_clock_get_str(buf,20)` → `"YYYY-MM-DD HH:MM:SS"`;`sys_clock_get()` → Unix 秒(失败 = `UINT32_MAX`);SD2506 RTC + timeSyncTask TCP 校时 | +| 挂载 | defaultTask:`f_mount(forced=1)`,空卷自动 `f_mkfs` FAT32,成功后置 `SYS_EVT_STORAGE_READY` | + +### 2.3 现有自检检查点(v1 埋点位置) + +| 检查点 | 位置 | 失败表现 | +|---|------|---------| +| RTC 读取/墙钟建立 | `app_main_init()`:`sd2506_init()` + `sys_clock_init()`(FreeRTOS 启动前) | 时间戳无效,后续日志无意义时间 | +| NAND 初始化 | `app_main_init()`:`gd5f2gq5ue_init()` 返回非 `GD5F_OK` | 存储不可用 | +| ADC 初始化 | `app_main_init()`:`tpafe5160_init()` 超时 | 采样不可用 | +| FatFS 挂载/建卷 | defaultTask:`f_mount`/`f_mkfs` 失败(置位 STORAGE_READY 前) | FTP/配置/记录全部不可写 | +| 网络链路 | net_task `net_stack_bring_up()`:PHY 等待 ≤3s 后仍 DISCONNECT | 组网可用但无链路 | + +### 2.4 RAM/LVGL 池预算(依据 `docs/内存管理/RAM使用分析与优化方案.md`) + +- FreeRTOS 堆真实余量 ≈ **11~12KB**;LVGL 池 40KB 静态,菜单常驻占用 ≈5KB。 +- 本设计新增 RAM:**全部 < 1KB**(§7),不新增任务、不动任务栈。 + +## 3. 网络调研摘要(R6) + +| 主题 | 结论 | 来源 | +|---|------|------| +| 日志行格式与时间戳 | syslog(RFC 5424):一行一条记录;TIMESTAMP 采用 **ISO 8601/RFC 3339**(`YYYY-MM-DDTHH:MM:SS[.frac][Z]`,机器可读、字典序可排序);MSG 用 UTF-8。本设计取空格变体 `YYYY-MM-DD HH:MM:SS`(与既有 `sys_clock_get_str` 输出一致),另附 epoch 秒字段供程序化解析 | RFC 5424 §6.2.3/§6.4(datatracker.ietf.org/doc/html/rfc5424) | +| 写入模型 | Linux printk:消息先进 **RAM 环形缓冲**,由**专用线程**异步刷持久存储;热路径禁止同步 I/O。本设计对应"RAM 待发队列 + defaultTask 单写者落盘" | kernel.org/doc/html/latest/core-api/printk-basics.html | +| FatFs 追加语义 | `FA_OPEN_APPEND`(= FA_WRITE\|FA_SEEKEND):打开即定位 EOF,文件不存在则创建;配合 `f_lseek(fp, FILE_SIZE)` 可取文件大小。本设计每条记录一次 `f_open→f_write(64B)→f_close` | 工程内 `Lib/FatFs/ff.h:399`、`ff.c:3871`(与官方文档一致) | +| NAND 写入代价 | dhara FTL 已做磨损均衡;FAT32 下每次小追加约消耗 1 个簇(4KB)。本场景**写频率极低**(每上电 1~N 条、故障才写),簇开销与磨损可接受;以文件容量上限约束增长 | `docs/STM32F4-Base存储架构说明.md` §6.3 + GD5F 驱动参数 | + +**选型结论**:纯文本、**定长 64B 记录、一行一条**(ISO 时间戳 + epoch + UTF-8 描述);追加写 + 容量上限截断;RAM 队列 + 单写者任务落盘。 +不选变长行(需扫描计数/解析,掉电半行难界定);不选二进制格式(违背 R5 FTP 直读)。 + +## 4. 总体设计 + +### 4.1 模块与文件清单 + +| 文件 | 端 | 说明 | +|---|---|------| +| `App/log/sys_log.h` | 共享 | 公共 API + 记录格式常量 + 行拆分内联函数(无 OS 依赖) | +| `App/log/sys_log_mcu.c` | MCU | FatFS 后端:待发队列(启动前静态数组 + FreeRTOS 消息队列)、defaultTask 落盘、容量截断、上电/重启分类 | +| `sim/sim_syslog.c` | 模拟器 | 宿主文件后端(CWD 下 `sim_run.log` / `sim_selftest.log`),直接追加,`time()` 时间戳 | +| `Drivers/BSP/UI/ui_standby.h/.c` | 共享 | 空闲检测 + 待机屏(屏幕覆盖层:保存/还原前屏与焦点) | +| `Drivers/BSP/UI/ui_page_runlog.h/.c` | 共享 | 运行记录内容页描述符(动态行,数据源 = sys_log API) | +| `Drivers/BSP/UI/ui_page_selftest.h/.c` | 共享 | 自检记录内容页描述符(同上) | +| `App/app_main.c` | MCU | 埋点:RTC/NAND/ADC 失败 → `syslog_post(SYSLOG_SELFTEST, …)` | +| `Src/freertos.c`(defaultTask USER CODE) | MCU | 挂载失败埋点 + **上电/重启分类记录** + 队列排空落盘循环 | +| `App/task/net_task.c` | MCU | 埋点:链路超时 → `syslog_post(SYSLOG_SELFTEST, "通讯异常")` | +| `Drivers/BSP/UI/ui_page_password.c` | 共享 | R1:错误提示 + 重输 | +| `Drivers/BSP/UI/ui_content_page.h/.c`、`ui_menu.c` | 共享 | 新增 2 个内容页 ID/描述符表项/node 39·40 映射;键事件挂钩空闲通知 | +| `ui_cn_chars.txt` + `tools/font_gen/` | — | 补 10 字重生成字体(§6) | +| `MDK-ARM/*.uvprojx`、`simulator/CMakeLists.txt` | — | 新文件双端注册(new-source-file skill) | +| `simulator/main.c` | 模拟器 | `--logtest` / `--standby` / `--idle ` 无头自检与测试钩子 | + +### 4.2 日志记录格式(定长 64B,一行一条) + +``` +偏移 长度 内容 +[0..18] 19 "YYYY-MM-DD HH:MM:SS"(ISO 8601 空格变体;墙钟无效时 = "1970-01-01 08:00:00") +[19] 1 ' ' +[20..29] 10 epoch 秒 ASCII("%010u",机器解析/上电重启分类用;墙钟无效 = "0000000000") +[30] 1 ' ' +[31..58] 28 描述 UTF-8(≤28B = 9 个汉字),不足补 '\0' +[59..63] 5 '\0' 填充 +``` + +示例行:`2026-09-14 16:48:34 1789465714 装置上电记录`(后随 NUL 填充至 64B)。 + +**定长收益**:条数 = `filesize / 64`(O(1),免扫描);第 k 条偏移 = `k*64`(随机读,UI 翻页零解析成本);掉电半行被 `floor(size/64)` 自然排除。 +**epoch 10 位约束**:2026 年 Unix 秒已达 10 位(2001-09-23 起),字段按 `%010u` 定宽;uint32 上限 4294967295(2106 年)内不溢出。 +**描述 ≤28B 约束**:受框架动态行栈缓冲 `CP_ROW_BUF_LEN=32`(含 NUL)限制,不改框架。 + +### 4.3 文件与容量 + +| 文件 | 内容 | 上限 | 满时策略 | +|---|------|------|---------| +| `/log/run.log` | 上电/重启记录 | **1024 条(64KB)** | 删除重建(整卷清空,串口告警);DTU 上电频率低,容量充裕 | +| `/log/selftest.log` | 自检故障记录 | **512 条(32KB)** | 同上 | + +- 目录 `/log/` 首次落盘前 `f_mkdir`(已存在则忽略);文件名均 8.3 合规。 +- FTP 视角:卷根下多一个 `LOG` 目录,两个 `.log` 文本文件可直接下载(R5)。 +- 记录**只增不改不删**(除满时重建与未来"清除记录"功能),保证 FTP 端所见即历史。 + +### 4.4 写路径(单写者,调研结论落地) + +``` +任意上下文 syslog_post(id, desc) + ├─ FreeRTOS 未运行(app_main_init 期)→ 静态待发数组(8 槽,满则丢弃+计数) + └─ 调度器已运行 → osMessageQueueSend(8 槽,满则丢弃+计数;日志不得阻塞业务) + +defaultTask(唯一写者,挂载成功后): + 1) f_mkdir("/log")(幂等) + 2) 排空启动前数组 + 队列 → 逐条落盘(f_open FA_OPEN_APPEND → f_write(64B) → f_close) + 3) 写"上电/重启"分类记录(§5.3,基于 run.log 末条 epoch) + 4) 主循环:osMessageQueueGet(阻塞, 10s 超时) → 落盘;超时空转继续 LED 闪烁 +``` + +- 读路径与写路径并发安全:UI 页在 uiTask 直接 `f_open(FA_READ)` 按偏移读记录(只读既有内容,写者只在 EOF 追加);FatFS 系统锁保护单次调用。 +- **存储未就绪/挂载失败**:待发条目滞留 RAM(下次上电丢失),串口打印告警——存储故障时本就无法持久化,属预期行为。 + +### 4.5 读路径(UI / 模拟器) + +```c +uint32_t syslog_count(syslog_id_t id); /* = filesize/64 */ +int syslog_get_record(syslog_id_t id, uint32_t idx, char *ts, size_t ts_len, + char *desc, size_t desc_len); /* 越界返回 -1 */ +``` + +- MCU:`f_open(FA_READ) → f_lseek(idx*64) → f_read(64B) → f_close`,内联拆分函数切 ts/desc。 +- 模拟器:宿主文件同语义(UI 页双端零差异)。 +- UI 页每帧仅读当前窗口 ≤8 条记录(每次重绘 8×64B SPI 读,用户翻页触发,频率极低)。 + +## 5. 功能设计 + +### 5.1 R1 密码错误提示(`ui_page_password.c`) + +- ENTER 比对失败 → `s_err=1`:3 框清零、光标回首框,**数字框下方居中显示"密码输入错误"**(y≈104)。 +- 错误态下按 ↑↓←→:**先清除提示**再正常执行该键(即"让用户重新输入");再按 ENTER = 重新比对。 +- 比对成功路径不变(回调授权/关页)。布局常量与绘制沿用现有 culling 模式,零 LVGL 对象。 + +### 5.2 R2 待机显示页(`ui_standby.c/h`) + +**进入条件**:UI 初始化完成后,**60s(默认,可配)内无任何按键** → 切待机屏。 +- 空闲检测 = `lv_timer`(1s 周期,MCU/模拟器均经 `lv_timer_handler` 驱动,无 OS 依赖); +- 活动通知挂钩:`menu_key_cb`、`cp_key_cb` 每收到一个键调 `ui_standby_notify_activity()`(重置计时)。 + +**待机屏内容**(参考 mockup,全 ASCII 无需新字体字): + +``` +Ib = 0.00A ← 6 行电流值(v1 占位 0.00A,与 AC/DC 页现状一致; +Ic = 0.00A 待交流量标定管线落地后接 TPAFE5160 实时值) +Io = 0.00A +Ia2 = 0.00A +Ib2 = 0.00A +Ic2 = 0.00A +────────────────────── ← 分隔线(y≈132) +YYYY-MM-DD HH:MM:SS ← 底部实时时钟(每秒重绘,lv_timer 驱动 invalidate) +``` + +**键语义(用户定案)**:待机屏上**只有 ENTER(确认键)进入菜单**;其余按键忽略(同时重置空闲计时)。 +- 实现为**屏幕覆盖层**:进入时保存当前活动屏 + 组内焦点对象,加载待机屏并聚焦其容器;ENTER 时还原前屏与焦点。 +- 内容页打开期间超时 → 同样切待机(页面对象保留在底层,ENTER 返回后原样恢复,编辑态不丢)。 + +### 5.3 R3 运行记录(上电/重启分类) + +defaultTask 挂载成功、排空待发队列**之后**写一条: + +``` +读 run.log 末条记录的 epoch 字段 E: + 墙钟有效 且 (now - E) < 600s → "装置重启记录"(近期刚运行过 = 软复位/看门狗) + 其余(无文件/首条/间隔≥10min/墙钟无效)→ "装置上电记录" +``` + +- 阈值 `SYSLOG_REBOOT_WINDOW_S = 600`:断电 <10min 会被误判为重启——无掉电检测硬件时该歧义不可消除,取工程折中并在此声明。 +- 串口标记(供 @test):`runlog boot entry: 装置上电记录 / 装置重启记录`(`sys_log_mcu.c` `DBG_INFO`,与落盘描述串同源)。 + +### 5.4 R4 自检记录(v1 项表) + +| 检查点 | 触发时机 | 描述串(落盘 + UI 显示同源) | +|---|------|------| +| RTC/墙钟建立失败 | app_main_init | `时钟芯片异常` | +| NAND 初始化失败 | app_main_init | `NAND异常` | +| ADC 初始化超时 | app_main_init | `ADC异常` | +| FatFS 挂载/建卷失败 | defaultTask(仅串口,存储不可写) | (无法落盘,串口 `[SELFTEST] fs: FAIL`) | +| 网络链路 3s 未建立 | net_task bring_up 后 | `通讯异常` | + +- **只记故障**;全部通过不写记录(与 mockup"有问题进行记录"一致)。 +- v1 不做"已复归/动作/标志字"(mockup 中为保护继电器专有语义)——列入开放问题 O2。 +- UI 页空态:`count_cb` 返回 0 时以单条伪行 `无记录` 呈现(页脚"共 1 页",避免"共 0 页")。 + +## 6. UI 布局与字体 + +### 6.1 新增字符集(11 字)→ 重生成 `ui_font_cn_12.c` + +| 用途 | 新字 | +|---|------| +| 密码错误提示"**密码输入错误**" | 输、入、错、误 | +| 运行记录"装置**上电**记录 / 装置**重**启记录" | 上、电、重 | +| 自检故障名"时**钟芯片异常**"等(异/常为公共后缀) | 芯、片、异、常 | + +流程按 `font-regen` skill:改 `ui_cn_chars.txt` → extract_cn → make_font → diff。注意:**extract_cn 只扫 UI 源码**,故障描述串在非 UI 源码(sys_log_mcu.c/app_main.c/net_task.c)中,须手动补入字符集;其余文案(运行记录/自检记录/无记录/第 X 页共 Y 页)均已在字符集内;待机页全 ASCII。 + +### 6.2 页面几何(160×160,沿用框架常量体系) + +| 页 | 参数 | +|---|------| +| 运行记录 / 自检记录 | `selectable=1, rows_per_page=8, top_y=24, pitch=14, band_h=13`(同 AC/DC 约定;**偏离 mockup**:实机照片无选中带,但框架 selectable 页选中行恒黑字、band_h=0 时黑字黑底不可见,故保留标准白带),`left_x=7`;**每条记录 = 2 行**:偶数行 = 时间戳(`YYYY-MM-DD HH:MM:SS`,19 ASCII ≈114px 可容)、奇数行 = 描述(≤9 汉字);↑↓按行移动(8 行/页 = 4 条/页),页脚自动分页 | +| 待机屏 | 6 数据行 y=28 起 pitch=18(label x≈24 / `=` x≈72 / 值 x≈96);分隔线 y≈132;时钟 y≈140 居中 | +| 密码页错误提示 | "密码输入错误" 居中,y≈104(数字框行 y=78..92 之下) | + +## 7. 内存预算 + +| 项 | 大小 | 位置 | +|---|------|------| +| sys_log MCU:启动前待发数组 8×12B + 队列 8×12B + 状态 | ≈300B(静态)+ ≈150B(FreeRTOS 堆,消息队列) | .bss / heap_4 | +| ui_standby:待机屏 + 容器 LVGL 对象(常驻) | ≈300B | LVGL 池(40KB,当前占用 ≈5KB) | +| 记录页模块 | 0(描述符驱动,零 LVGL 对象;仅栈缓冲) | — | +| 字体 +10 字形 | ≈250B Flash | .rodata | + +**结论:不新增任务、不动任务栈、FreeRTOS 堆增量 <200B,LVGL 池余量充足。** + +## 8. 测试计划 + +### 8.1 模拟器(无头自动化) + +| 用例 | 手段 | +|---|------| +| 日志引擎闭环 `--logtest` | post N 条 → count/get 回读断言 + 宿主文件字节级校验(64B 定长、ISO 时间戳、NUL 填充),0=PASS(仿 `--cfgtest`) | +| 待机屏渲染 `--standby` | 无头注键后直接调测试钩子强制进入待机 → 截图比对(布局/时钟行);窗口模式 `--idle ` 可人工验证真实超时路径 | +| 记录页像素回归 | `--keys` 导航至 node 39/40 → 截图 + 文本断言(时间戳行/描述行/页脚分页),纳入 ui-verify 门禁 | +| 密码错误提示 | `--keys` 输错密码序列 → 截图断言"密码输入错误"可见;再按方向键 → 提示消失 | + +### 8.2 真机(@test 串口标记) + +Required:`App init OK`、`Network Init`、`net init: OK`、**`runlog boot entry:`**(装置上电记录 / 装置重启记录)、**`UI standby: ready (idle=60s)`**; +Forbidden:`FAIL`/`ERROR`/`HardFault`。 +人工目检:待机屏(静置 60s)、记录页翻页、FTP 下载 `/log/*.log` 文本可读性。 + +## 9. 风险与开放问题 + +| # | 项 | 策略 | +|---|----|------| +| O1 | 断电 <10min 的"上电/重启"歧义 | §5.3 阈值折中,文档声明;如需精确可后续加掉电标志(PVD) | +| O2 | 故障"已复归"/持续监测(mockup 语义) | v2:周期自检 + 活动故障状态机 + 恢复记录 | +| O3 | "清除记录"菜单项(node 27 #P,现为空框) | v2:UI 动作 = 删除/重建 log 文件(与满时策略同路径) | +| O4 | 待机页电流值接实时数据 | 待交流量标定管线(AC 页占位数据替换)落地后接入 `tpafe5160_get_buf()` | +| R-1 | FAT32 小追加簇开销 | §3 调研结论:写频率极低可接受;容量上限约束增长 | +| R-2 | UI 读与落盘并发 | 只读既有内容 + EOF 追加 + FatFS 系统锁,无互斥需求(§4.4) | + +## 10. 实施步骤(按序执行,每步双端构建 0/0) + +> ✅ **S1–S9 已于 2026-09-14 全部完成**:Keil `@build` 0 错 0 警 + 模拟器 0 警告;S8 = `--logtest` PASS(期间修复 ts 19 字符 / epoch 10 位两处格式 bug)+ ui-verify 五门禁全绿(池基线 5440/5520B、pwc 参考更新)+ 记录页/待机屏/密码错误提示截图目检通过;S9 = 文档同步。 + +| # | 步骤 | 产物 | +|---|------|------| +| S1 | 字体补字 + 重生成 | `ui_cn_chars.txt`、`ui_font_cn_12.c` | +| S2 | 日志引擎:`sys_log.h`(共享 API/格式)+ `sys_log_mcu.c` + `sim/sim_syslog.c` | 双端可 post/count/get | +| S3 | 自检埋点 + defaultTask 落盘循环 + 上电/重启分类 | app_main/freertos/net_task 接线 | +| S4 | 密码错误提示 | ui_page_password.c | +| S5 | 待机屏 `ui_standby`(空闲检测 + 覆盖层)+ 键挂钩 | ui_menu/ui_content_page 各一行通知 | +| S6 | 记录页 ×2 + 内容页框架接线(枚举/描述符表/node 映射) | ui_page_runlog/selftest、ui_content_page.h/.c、ui_menu.c | +| S7 | 双端注册新文件(uvprojx + CMakeLists)+ 构建 0/0 | new-source-file skill | +| S8 | 模拟器无头回归(--logtest/--standby/截图断言)+ 真机 @test | 测试通过记录 | +| S9 | 文档同步(项目架构总览、AGENTS.md、本文档状态) | docs | diff --git a/docs/项目架构总览.md b/docs/项目架构总览.md index 523bb219..deb7c112 100644 --- a/docs/项目架构总览.md +++ b/docs/项目架构总览.md @@ -52,11 +52,11 @@ | 任务 | 栈 (字节) | 优先级 | 职责 | 入口 | |---|---|---|---|---| -| `defaultTask` | 2048 | Normal | FatFS 挂载(无卷 `f_mkfs`)→ 置 `STORAGE_READY` → LED 闪烁 | `StartDefaultTask` | +| `defaultTask` | 2048 | Normal | FatFS 挂载(无卷 `f_mkfs`)→ `syslog_mcu_mounted` + 日志落盘(唯一写者,LED 循环内 `syslog_drain`)→ 置 `STORAGE_READY` → LED 闪烁 | `StartDefaultTask` | | `netTask` | 4096 | Normal | 等存储 → `net_stack_bring_up()` → 置 `NET_READY` → `net_poll` 轮询 | `StartNetTask` | | `ftpTask` | 4096 | Normal | 等网络+存储 → `lftpd_start("/", 21)` | `StartFtpTask` | | `testTask` | 4096 | BelowNormal | 测试套件入口(`test/test_config.h` 选择;产品构建空转) | `start_test_task` | -| `uiTask` | 16384 | BelowNormal | 等网络 → `ui_port_init`/`ui_menu_init`/`ui_net_sink` → 10ms 刷新 | `ui_task_func` | +| `uiTask` | 16384 | BelowNormal | 等网络 → `ui_port_init`/`ui_menu_init`/`ui_standby_init`(空闲 60s 切待机屏)/`ui_net_sink` → 10ms 刷新 | `ui_task_func` | | `timeSyncTask` | 2048 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC | `time_sync_task` | > `idle`(128 words)/ `timer`(256 words)由 FreeRTOS 内核创建。所有任务栈从 48KB heap_4 堆分配。 @@ -133,11 +133,13 @@ flowchart LR subgraph USERS["使用方"] NC["net_cfg
/config.ini(INI [network] 段)"] + SLG["sys_log
/log/run.log · selftest.log"] LF["lftpd
FTP 文件读写"] ST["测试套件
STORAGE/GD5F"] end NC --> FS + SLG --> FS LF --> FS ST --> FS FS --> DIO --> FTL --> NAND --> CHIP @@ -148,6 +150,7 @@ flowchart LR ``` - 掉电持久化:`net_cfg` 配置(通用配置文件 `/config.ini`,INI `[network]` 段,cfg_file 引擎),保存后下次上电生效、或立即生效(网络重建)、外部变更 4s 沉降后自动重载;旧版 NCFG 二进制(`/cfg/netcfg.bin`)首启自动迁移。 +- 日志记录:`sys_log` 引擎写定长 64B 文本记录到 `/log/run.log`(运行记录)与 `/log/selftest.log`(自检记录),defaultTask 唯一写者,FTP 可直下(详见 `docs/运行与自检记录设计说明.md`)。 - NAND 坏块表(BBT)持久化在保留块池,不交给 dhara 管理。 --- @@ -195,6 +198,8 @@ flowchart TB P3["ui_page_dc"] P4["ui_page_password"] P5["ui_page_net_cfg"] + P6["ui_page_runlog
运行记录(动态列表)"] + P7["ui_page_selftest
自检记录(动态列表)"] end subgraph PORT["显示端口"] UP["ui_port.c
160x160 1bpp 缓冲"] @@ -204,17 +209,20 @@ flowchart TB UM --> MT UM --> MV UM --> CP + UM --> STBY["ui_standby.c
待机屏(空闲超时切入)"] MV --> DECO - CP --> P1 & P2 & P3 & P4 & P5 + CP --> P1 & P2 & P3 & P4 & P5 & P6 & P7 MV --> UP SINK --> UP P5 -.->|"net_cfg RAM 副本"| CFG["net_cfg"] P4 -.->|"ui_pwd_def.h 编译期密码"| PWD["查看 000 / 修改 123"] + P6 & P7 -.->|"/log/run.log · selftest.log"| SLG["App/log/sys_log
定长 64B 记录引擎(defaultTask 落盘)"] ``` - 内容页为**独立全屏屏**,由框架一次性自绘(**零 LVGL 子对象**),ESC 返回菜单屏。 - 菜单受保护叶子(`ui_menu_tree.txt` 行尾 ` #P`):先弹密码页,授权后 `menu_password_ok` **先关页再续进入动作**。 -- 字体 `ui_font_cn_12.c` 由 `tools/font_gen/` 生成(宋体 12px 1bpp 子集,111 字/208 字形)。 +- 待机屏:空闲超时(默认 60s)自动切入,仅确认键回菜单;数据源同实时量页。 +- 字体 `ui_font_cn_12.c` 由 `tools/font_gen/` 生成(宋体 12px 1bpp 子集,122 字/217 字形)。 --- @@ -231,6 +239,7 @@ STM32F4-Base/ │ ├── sys_events.h 就绪屏障 bit 定义 │ ├── app_cfg.h 功能编译开关(APP_NET/ADC/RS485_ENABLE) │ ├── task/ net_task / ui_task / adc_task* / rs485_task* +│ ├── log/ sys_log 记录引擎(定长 64B;MCU 后端落盘 /log/*.log) │ └── util/ crc(CRC-16/8)、ringbuf、sys_clock(墙钟) ├── Drivers/BSP/ 纯手工驱动/服务(不受 CubeMX 保护) │ ├── CH395F/ 以太网 MAC+PHY @@ -306,3 +315,4 @@ flowchart LR | 2026-09-10 | §2.2 就绪屏障图由 Mermaid 改为**手绘 SVG**(`tools/arch_diagram/gen_barrier_svg.py` → `就绪屏障.svg`):三栏(生产者 / 事件组 / 消费者)+ 水平对齐 + All 汇合点,消除原图 9 条边的交叉;PNG 预览本地渲染、不入库 | | 2026-09-13 | `sys_clock` 由 ③基础服务层改归 **④中间件层**(通用平台时间服务,日志/文件/对时共用;文件移入 `App/util/` 与 crc/ringbuf 同目录,include 路径不变):`FatFs` → `sys_clock` 成为**层内引用**,工程反向依赖消除;同日新增的 §1.2 "反向依赖例外" 条目撤销 | | 2026-09-14 | 通用配置文件(P3):网口配置存储由 `/cfg/netcfg.bin`(NCFG 二进制)迁移至 `/config.ini`(INI `[network] 段`,`App/cfg_file` 引擎 + `cfg_file_store` 双端后端);`net_cfg` 对外 API 不变;旧档首启自动迁移;`cfg_file` 列为 MCU/模拟器共享源码 | +| 2026-09-14 | 运行/自检记录:新增 `App/log/sys_log`(定长 64B 文本记录引擎,MCU 后端 `/log/run.log`·`selftest.log`,defaultTask 唯一写者)+ UI 页面插件 `ui_page_runlog`/`ui_page_selftest`(动态列表)+ `ui_standby` 待机屏(空闲超时切入);密码页错误提示;字体 111→122 字(217 字形);详见 `docs/运行与自检记录设计说明.md` | diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 83c9a859..e4bda8af 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -53,6 +53,7 @@ include_directories( ${NET_DIR} # net_socket.h / net_types.h(net_* API 双端共享核心) ${NETCFG_DIR} # net_cfg.h(网络配置,双端共享核心) ${PROJECT_ROOT}/App # cfg_file.h / cfg_file_store.h(通用配置文件引擎,双端共享核心) + ${PROJECT_ROOT}/App/log # sys_log.h(运行/自检记录引擎,双端共享核心) ${PROJECT_ROOT}/Drivers/BSP # dbg_log.h(使用 simulator 的 dbg_cfg.h 配置) ${SDL2_DIR}/include # SDL2 头文件 ) @@ -82,11 +83,19 @@ set(UI_SOURCES ${UI_DIR}/ui_page_dc.c ${UI_DIR}/ui_page_password.c ${UI_DIR}/ui_page_net_cfg.c + ${UI_DIR}/ui_page_runlog.c + ${UI_DIR}/ui_page_selftest.c + ${UI_DIR}/ui_standby.c ${UI_DIR}/ui_menu_tab.c ${UI_DIR}/ui_font_cn_12.c ${UI_DIR}/ui_net_sink.c ) +# 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) +set(SYSLOG_SOURCES + sim/sim_syslog.c +) + # 网络配置(双端共享核心 + cfg_file 引擎 + 平台存储后端;MCU 端编 cfg_file_store_mcu.c,见 Keil 工程) set(NETCFG_SOURCES ${NETCFG_DIR}/net_cfg.c @@ -104,7 +113,7 @@ set(NET_SOURCES #============================================================================= # 构建可执行文件 #============================================================================= -add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES} ${NET_SOURCES}) +add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES} ${SYSLOG_SOURCES} ${NET_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 aafdaa96..b531f99d 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -258,7 +258,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` 是子集字体(当前 111 汉字/符号 + ASCII,共 208 字形),缺字渲染为空白。 +4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 122 汉字/符号 + ASCII,共 217 字形),缺字渲染为空白。 补字流程见 `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 0d543336..f89132b8 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -23,11 +23,22 @@ * 验证链:net_cfg_load(含旧档迁移)→ poll 建基线 → 伪造外部编辑 * (原子写新文档,等价 FTP 上传/文本编辑器)→ poll 两次(4s 沉降) * → 断言 RAM 副本已按新文件重载 + * + * 日志引擎自检模式(无窗口、进程内闭环、退出码 0=PASS / 非0=FAIL): + * lvgl_sim.exe --logtest + * 验证链:post N 条 → count/get 回读断言 + 宿主文件字节级校验 + * (64B 定长、ISO 时间戳、epoch 字段、NUL 填充) + * + * 待机屏截图模式(无窗口,强制进入待机后截图): + * lvgl_sim.exe --standby shot.bin + * + * 窗口模式空闲超时覆盖:lvgl_sim.exe --idle <秒>(默认 60s) */ #include #include #include +#include #ifdef _MSC_VER #include @@ -42,10 +53,12 @@ #include "lvgl.h" #include "sim_port.h" #include "ui_menu.h" +#include "ui_standby.h" #include "ui_net_sink.h" #include "net_cfg.h" #include "cfg_file.h" #include "cfg_file_store.h" +#include "sys_log.h" /* 屏幕尺寸(与 MCU 端一致) */ #define SIM_SCREEN_W 160 @@ -90,13 +103,32 @@ static uint32_t sim_map_key_char(char c) } } +/* + * @brief 待机屏底部时钟提供回调(平台差异:模拟器 = 宿主 time()) + */ +static void sim_time_cb(char *buf, size_t len) +{ + time_t t = time(NULL); + struct tm tmv; + +#if defined(_MSC_VER) + localtime_s(&tmv, &t); +#else + localtime_r(&t, &tmv); +#endif + (void)snprintf(buf, len, "%04d-%02d-%02d %02d:%02d:%02d", + tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday, + tmv.tm_hour, tmv.tm_min, tmv.tm_sec); +} + /* * @brief 无窗口离屏渲染(可注入键序列)并导出末帧像素 * @param keys 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键) * @param path 输出文件(写入 160x160 I1 原始像素) + * @param standby 非 0 = 注键后强制进入待机屏再截图(--standby 模式) * @return 0 成功,非 0 失败 */ -static int run_headless(const char * keys, const char * path) +static int run_headless(const char * keys, const char * path, int standby) { setvbuf(stdout, NULL, _IONBF, 0); lv_init(); @@ -133,6 +165,10 @@ static int run_headless(const char * keys, const char * path) return 1; } + /* 待机模块(无头模式不驱动 lv_timer_handler,空闲定时器不会触发; + * --standby 经 force_enter 测试钩子直接切入) */ + (void)ui_standby_init(60000u, sim_time_cb); + /* 键注入:与 keypad 输入设备同路径(向焦点对象发 LV_EVENT_KEY)。 * 键处理是同步的(状态变更 + 场景更新当场完成),无需驱动帧; * 无头模式严禁调 lv_timer_handler()/lv_refr_now()——I1+FULL 全屏刷新会进渲染死循环 */ @@ -144,6 +180,11 @@ static int run_headless(const char * keys, const char * path) lv_group_send_data(g, key); } + /* --standby:注键后强制进入待机屏(截图前) */ + if (standby != 0) { + ui_standby_force_enter(); + } + /* 注键后 LVGL 池占用(无头模式;内存回归用,对照 ui_menu_init 基线) */ lv_mem_monitor_t mon; lv_mem_monitor(&mon); @@ -259,22 +300,136 @@ static int run_cfgtest(void) return 0; } +/* + * @brief 日志引擎自检(无窗口、进程内闭环) + * @note 验证链:清理旧档 → post N 条 → count/get 回读断言 + 宿主文件字节级校验 + * (64B 定长、ISO 时间戳格式、epoch 字段 ≈ now、NUL 填充) + * @return 0 PASS,非 0 FAIL + */ +static int run_logtest(void) +{ + uint32_t ep; + char ts[SYSLOG_TS_LEN + 1u]; + char desc[SYSLOG_DESC_MAX + 1u]; + + setvbuf(stdout, NULL, _IONBF, 0); + + /* 确定性起点:清理旧档 */ + (void)remove("sim_run.log"); + (void)remove("sim_selftest.log"); + + syslog_post(SYSLOG_RUN, "装置上电记录"); + syslog_post(SYSLOG_SELFTEST, "时钟芯片异常"); + syslog_post(SYSLOG_SELFTEST, "NAND异常"); + + if (syslog_count(SYSLOG_RUN) != 1u) { + printf("[logtest] FAIL run count = %u (expect 1)\n", (unsigned)syslog_count(SYSLOG_RUN)); + return 1; + } + if (syslog_count(SYSLOG_SELFTEST) != 2u) { + printf("[logtest] FAIL selftest count = %u (expect 2)\n", (unsigned)syslog_count(SYSLOG_SELFTEST)); + return 1; + } + + /* 回读断言:时间戳格式 + 描述内容 */ + if (syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) != 0) { + printf("[logtest] FAIL get_record(run, 0)\n"); + return 1; + } + if (strlen(ts) != SYSLOG_TS_LEN || ts[4] != '-' || ts[7] != '-' || ts[10] != ' ') { + printf("[logtest] FAIL ts format = \"%s\"\n", ts); + return 1; + } + if (strcmp(desc, "装置上电记录") != 0) { + printf("[logtest] FAIL desc = \"%s\"\n", desc); + return 1; + } + if (syslog_get_record(SYSLOG_SELFTEST, 1u, ts, sizeof(ts), desc, sizeof(desc)) != 0) { + printf("[logtest] FAIL get_record(selftest, 1)\n"); + return 1; + } + if (strcmp(desc, "NAND异常") != 0) { + printf("[logtest] FAIL desc[1] = \"%s\"\n", desc); + return 1; + } + + /* epoch 字段 ≈ now(±5s) */ + if (syslog_last_epoch(SYSLOG_RUN, &ep) != 0) { + printf("[logtest] FAIL last_epoch(run)\n"); + return 1; + } + if ((uint32_t)abs((int)(ep - (uint32_t)time(NULL))) > 5u) { + printf("[logtest] FAIL epoch = %u (now = %lu)\n", (unsigned)ep, (unsigned long)time(NULL)); + return 1; + } + + /* 宿主文件字节级校验:64B 定长 + 字段布局 + NUL 填充 */ + { + FILE *fp = fopen("sim_run.log", "rb"); + uint8_t rec[SYSLOG_REC_SIZE]; + + if (fp == NULL) { + printf("[logtest] FAIL open sim_run.log\n"); + return 1; + } + if (fread(rec, 1u, sizeof(rec), fp) != sizeof(rec)) { + fclose(fp); + printf("[logtest] FAIL record size != %u\n", (unsigned)SYSLOG_REC_SIZE); + return 1; + } + fclose(fp); + + if (rec[SYSLOG_TS_LEN] != ' ' || rec[SYSLOG_DESC_OFFS - 1u] != ' ') { + printf("[logtest] FAIL field separators at [%u]/[%u]\n", + (unsigned)SYSLOG_TS_LEN, (unsigned)(SYSLOG_DESC_OFFS - 1u)); + return 1; + } + for (uint32_t i = SYSLOG_DESC_OFFS + strlen("装置上电记录"); i < SYSLOG_REC_SIZE; i++) { + if (rec[i] != '\0') { + printf("[logtest] FAIL NUL padding at [%u]\n", (unsigned)i); + return 1; + } + } + } + + printf("[logtest] OK count/get/epoch/bytes\n"); + printf("[logtest] PASS\n"); + return 0; +} + /** * @brief 程序入口 */ int main(int argc, char **argv) { - /* 截图模式:lvgl_sim.exe --screenshot */ + /* 截图/自检模式:--screenshot / --keys / --standby / --cfgtest / --logtest */ for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--screenshot") == 0 && (i + 1) < argc) { - return run_headless("", argv[i + 1]); + return run_headless("", argv[i + 1], 0); } if (strcmp(argv[i], "--keys") == 0 && (i + 2) < argc) { - return run_headless(argv[i + 1], argv[i + 2]); + return run_headless(argv[i + 1], argv[i + 2], 0); + } + if (strcmp(argv[i], "--standby") == 0 && (i + 1) < argc) { + return run_headless("", argv[i + 1], 1); } if (strcmp(argv[i], "--cfgtest") == 0) { return run_cfgtest(); } + if (strcmp(argv[i], "--logtest") == 0) { + return run_logtest(); + } + } + + /* 窗口模式空闲超时覆盖(--idle <秒>,默认 60) */ + uint32_t idle_s = 60u; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--idle") == 0 && (i + 1) < argc) { + int v = atoi(argv[i + 1]); + if (v > 0) { + idle_s = (uint32_t)v; + } + } } /* 初始化 Winsock(供 ui_net_sink 使用) */ @@ -315,6 +470,13 @@ int main(int argc, char **argv) return 1; } + /* 待机显示页(空闲超时 --idle <秒>,默认 60s;ENTER 返回) */ + if (ui_standby_init(idle_s * 1000u, sim_time_cb) != 0) { + fprintf(stderr, "ui_standby_init failed\n"); + } else { + printf("Standby: ready (idle=%us)\n", (unsigned)idle_s); + } + /* 启动 RemoDispBus TCP 监听(端口 7003,与 MCU 端一致) */ if (ui_net_sink_start(7003) == 0) { printf("RemoDispBus server listening on port 7003\n"); diff --git a/simulator/sim/sim_syslog.c b/simulator/sim/sim_syslog.c new file mode 100644 index 00000000..333028a1 --- /dev/null +++ b/simulator/sim/sim_syslog.c @@ -0,0 +1,204 @@ +/* + * 模块名称:System Log (模拟器后端) — 运行/自检记录落盘 + * 模块功能:宿主文件 sim_run.log / sim_selftest.log(工作目录),与 MCU 端同格式 + * (定长 64B 记录);单线程直写,无队列 + * 适用平台:PC 模拟器 + * 作者:王建锋 + * 创建日期:2026-09-14 + * 修改记录: + * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/运行与自检记录设计说明.md §4) + */ + +/* 头文件包含区 */ +#include +#include +#include +#include + +#include "sys_log.h" + +/* 静态变量定义区 */ + +static const char s_paths[SYSLOG_COUNT][16] = { "sim_run.log", "sim_selftest.log" }; +static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX }; + +/* 函数定义区 */ + +/* + * 函数功能:取当前宿主本地时间(ISO 8601 空格变体 + epoch) + * 入口参数:p_ts - 输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char * + * p_epoch- 输出 epoch 秒(不可为 NULL) uint32_t * + * 返回值:无 + */ +static void s_now(char *p_ts, uint32_t *p_epoch) { + time_t t = time(NULL); + struct tm tmv; + +#if defined(_WIN32) + localtime_s(&tmv, &t); +#else + localtime_r(&t, &tmv); +#endif + (void)sprintf(p_ts, "%04d-%02d-%02d %02d:%02d:%02d", + tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday, + tmv.tm_hour, tmv.tm_min, tmv.tm_sec); + *p_epoch = (uint32_t)t; +} + +/* + * 函数功能:追加一条记录(格式化 64B;容量满先删除重建) + * 入口参数:id - 日志 ID syslog_id_t + * desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char * + * 返回值:0 - 成功;-1 - 写失败 int + */ +static int s_append(syslog_id_t id, const char *desc) { + FILE *fp; + uint8_t rec[SYSLOG_REC_SIZE]; + char ts[SYSLOG_TS_LEN + 1u]; + uint32_t epoch = 0u; + size_t dl; + + if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) { + return -1; + } + if (syslog_count(id) >= s_caps[id]) { + (void)remove(s_paths[id]); + } + memset(rec, 0u, sizeof(rec)); + s_now(ts, &epoch); + memcpy(rec, ts, SYSLOG_TS_LEN); + rec[SYSLOG_TS_LEN] = ' '; + (void)sprintf((char *)(rec + SYSLOG_EPOCH_OFFS), "%010u", (unsigned int)epoch); + rec[SYSLOG_DESC_OFFS - 1u] = ' '; + dl = strlen(desc); + if (dl > SYSLOG_DESC_MAX) { + dl = SYSLOG_DESC_MAX; + } + memcpy(rec + SYSLOG_DESC_OFFS, desc, dl); + + fp = fopen(s_paths[id], "ab"); + if (fp == NULL) { + return -1; + } + if (fwrite(rec, 1u, sizeof(rec), fp) != sizeof(rec)) { + (void)fclose(fp); + return -1; + } + (void)fclose(fp); + return 0; +} + +/* + * 函数功能:读取第 idx 条原始记录(64B) + * 入口参数:id - 日志 ID syslog_id_t + * idx - 记录序号(0 起) uint32_t + * p_rec- 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t * + * 返回值:0 - 成功;-1 - 越界/读失败 int + */ +static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) { + FILE *fp; + + if (p_rec == NULL || idx >= syslog_count(id)) { + return -1; + } + fp = fopen(s_paths[id], "rb"); + if (fp == NULL) { + return -1; + } + if (fseek(fp, (long)idx * SYSLOG_REC_SIZE, SEEK_SET) != 0 || + fread(p_rec, 1u, sizeof(uint8_t) * SYSLOG_REC_SIZE, fp) != SYSLOG_REC_SIZE) { + (void)fclose(fp); + return -1; + } + (void)fclose(fp); + return 0; +} + +/* + * 函数功能:投递一条日志(单线程直写宿主文件) + * 入口参数:id - 日志 ID syslog_id_t + * desc - 描述串(UTF-8,超长截断) const char * + * 返回值:无 + */ +void syslog_post(syslog_id_t id, const char *desc) { + (void)s_append(id, desc); +} + +/* + * 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1)) + * 入口参数:id - 日志 ID syslog_id_t + * 返回值:条数;文件缺失返回 0 uint32_t + */ +uint32_t syslog_count(syslog_id_t id) { + struct stat st; + + if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) { + return 0u; + } + if (stat(s_paths[id], &st) != 0) { + return 0u; + } + return (uint32_t)st.st_size / SYSLOG_REC_SIZE; +} + +/* + * 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾) + * 入口参数:id - 日志 ID syslog_id_t + * idx - 记录序号(0 起) uint32_t + * p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char * + * ts_len - p_ts 容量 size_t + * p_desc - 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节,不可为 NULL) char * + * desc_len - p_desc 容量 size_t + * 返回值:0 - 成功;-1 - 越界/读失败 int + */ +int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len) { + uint8_t rec[SYSLOG_REC_SIZE]; + size_t dl = 0u; + + if (p_ts == NULL || p_desc == NULL || ts_len <= SYSLOG_TS_LEN || desc_len <= SYSLOG_DESC_MAX) { + return -1; + } + if (s_read_raw(id, idx, rec) != 0) { + return -1; + } + memcpy(p_ts, rec, SYSLOG_TS_LEN); + p_ts[SYSLOG_TS_LEN] = '\0'; + while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') { + dl++; + } + if (dl >= desc_len) { + dl = desc_len - 1u; + } + memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl); + p_desc[dl] = '\0'; + return 0; +} + +/* + * 函数功能:读取末条记录的 epoch 秒(上电/重启分类用) + * 入口参数:id - 日志 ID syslog_id_t + * p_epoch- 输出缓冲(不可为 NULL;无记录时置 0) uint32_t * + * 返回值:0 - 成功读到末条;-1 - 文件缺失/空 int + */ +int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) { + uint8_t rec[SYSLOG_REC_SIZE]; + uint32_t v = 0u; + uint32_t i; + + if (p_epoch == NULL || syslog_count(id) == 0u) { + *p_epoch = 0u; + return -1; + } + if (s_read_raw(id, syslog_count(id) - 1u, rec) != 0) { + *p_epoch = 0u; + return -1; + } + for (i = 0u; i < SYSLOG_EPOCH_LEN; i++) { + char c = (char)rec[SYSLOG_EPOCH_OFFS + i]; + if (c >= '0' && c <= '9') { + v = v * 10u + (uint32_t)(c - '0'); + } + } + *p_epoch = v; + return 0; +} diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index 7b97fbcf..42b3d661 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -104,7 +104,9 @@ STRIDE = W // 8 # I1:20 字节/行 # 开页-关页往返后的 LVGL 池水位(x64 模拟器)= 菜单基线 + 一次性残留 80 B。 # 页面结构变更导致漂移时,先确认新值合理再更新此常量(内存回归门禁)。 -POOL_AFTER_ROUNDTRIP = 4864 +# 2026-09-14: 4864 → 5520(+656B)= run_headless 新增 ui_standby_init():待机模块 +# 常驻屏 s_scr + 容器 s_cont 两个 lv_obj,随菜单基线同移(见 check_menu_rows.py)。 +POOL_AFTER_ROUNDTRIP = 5520 def run_sim(seq, out): diff --git a/simulator/tests/check_menu_rows.py b/simulator/tests/check_menu_rows.py index 63b57160..48956b59 100644 --- a/simulator/tests/check_menu_rows.py +++ b/simulator/tests/check_menu_rows.py @@ -55,7 +55,9 @@ ROOT_X0, ROOT_Y0, ROOT_W, ROOT_H = 0, 44, 66, 107 # LVGL 池基线水位(menu build done,x64 模拟器):菜单结构/样式变更导致漂移时, # 先确认新值合理再更新此常量(内存回归门禁,见 docs/UI 设计/UI_自动化测试流程.md) -MENU_POOL_BASELINE = 4784 +# 2026-09-14: 4784 → 5440(+656B)= run_headless 新增 ui_standby_init():待机模块 +# 常驻屏 s_scr + 容器 s_cont 两个 lv_obj(先于池测量创建),属有意结构变更。 +MENU_POOL_BASELINE = 5440 def load(path): diff --git a/ui_cn_chars.txt b/ui_cn_chars.txt index 64459320..e7b09f53 100644 --- a/ui_cn_chars.txt +++ b/ui_cn_chars.txt @@ -1 +1 @@ -▶三主事交件作保信值共关制前动化区压参双口号启告地址型复子字存定实密序式强归当录息户手抖护报据控掩操故数整无日时期本板查校检模死波流测清点版用界直看码硬程端第类系线终维网置自行装警认讯记设询软运通遥量钟钥防除面页馈验默,: +▶三主事交件作保信值入共关制前动化区压参双口号启告地址型复子字存定实密序式强归当录息户手抖护报据控掩操故数整无日时期本板查校检模死波流测清点版用界直看码硬程端第类系线终维网置自行装警认讯记设询误软输运通遥量钟钥错防除面页馈验默上电重芯片异常,: