模拟器迁移 P5:2s 配置轮询 + --cfgtest 无头自检;修复 MSVC rename 覆盖与外科保存截断缺陷

This commit is contained in:
2026-09-14 09:33:32 +08:00
parent 97661415ca
commit bc4a7da8a0
6 changed files with 151 additions and 32 deletions
+12
View File
@@ -228,6 +228,18 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI 设计/UI_自动化
- **池水位常量漂移**:菜单/页面结构变更导致门禁失败时,确认新值合理后再更新脚本内常量并登记流程文档变更记录;无法解释的漂移 = 内存回归 bug。
- 退出码均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失等)。
### 3.7 配置文件自检(--cfgtest)
配置引擎"加载 → 轮询 → 重载 → 保存"的进程内闭环验证(无需窗口 / LVGL,退出码 0=PASS / 非 0=FAIL),在当前工作目录产生 `sim_config.ini`(`sim_netcfg.bin` 参与迁移时):
```powershell
.\build\Debug\lvgl_sim.exe --cfgtest
```
验证链:`net_cfg_load`(含旧档自动迁移)→ 第 1 次 poll 建基线 → 伪造"外部编辑"(原子写,等价 FTP 上传/文本编辑器)→ 2 次 poll 沉降(对应真机 4s 沉降)→ 断言 RAM 副本已更新为新值 → 对已存在文件做外科式保存、回读校验注释/值无损。
空目录运行 = 验证出厂模板生成;预置有效 `sim_netcfg.bin` = 验证旧档迁移(迁移后删除);无效旧档(CRC 不符)= 保留旧文件 + 生成出厂模板。窗口模式主循环以与 MCU netTask 相同的 2s 周期调用 `cfg_file_poll()`:外部文件变更沉降后自动重载 `[network]` 段(模拟器无网络栈,变更回调为空操作,仅日志可见)。
---
## 四、已知坑(构建/运行)
+104
View File
@@ -17,6 +17,12 @@
* lvgl_sim.exe --keys <seq> shot.bin
* seq 字符:u=↑ d=↓ l=← r=→ e=Enter s=Esc(其余字符忽略)
* 每键经 lv_group_send_data 注入(与 keypad 输入设备同路径),驱动 5 帧后取快照
*
* 配置轮询自检模式(无窗口、进程内闭环、退出码 0=PASS / 非0=FAIL):
* lvgl_sim.exe --cfgtest
* 验证链:net_cfg_load(含旧档迁移)→ poll 建基线 → 伪造外部编辑
* (原子写新文档,等价 FTP 上传/文本编辑器)→ poll 两次(4s 沉降)
* → 断言 RAM 副本已按新文件重载
*/
#include <stdlib.h>
@@ -38,6 +44,8 @@
#include "ui_menu.h"
#include "ui_net_sink.h"
#include "net_cfg.h"
#include "cfg_file.h"
#include "cfg_file_store.h"
/* 屏幕尺寸(与 MCU 端一致) */
#define SIM_SCREEN_W 160
@@ -168,6 +176,89 @@ static int run_headless(const char * keys, const char * path)
return 0;
}
/* 伪造"外部编辑"文档(绕过引擎原子写 = 等价 FTP 上传/文本编辑器);
* 刻意短于出厂模板,保证 (size, mtime) 变更令牌必变 */
static const char s_cfgtest_doc[] =
"; cfgtest forged external edit\r\n"
"\r\n"
"[network]\r\n"
"ip=10.0.0.1\r\n"
"mask=255.255.255.0\r\n"
"gw=10.0.0.254\r\n"
"mac=00:00:00:00:00:00\r\n";
/* 伪造编辑后 RAM 副本应呈现的 IP */
static const uint8_t s_cfgtest_ip[4] = { 10u, 0u, 0u, 1u };
/*
* @brief 配置轮询自检(无窗口、无需 LVGL/SDL,进程内闭环)
* @note 验证链:net_cfg_load(含旧档迁移)→ poll 建基线 → 伪造外部编辑
* → poll 两次(沉降去抖)→ 重载并更新 RAM 副本
* @return 0 PASS,非 0 FAIL
*/
static int run_cfgtest(void)
{
int i;
const net_cfg_t *p_cfg;
setvbuf(stdout, NULL, _IONBF, 0);
(void)net_cfg_load(); /* 加载 / 旧档迁移(含 [network] 段注册) */
cfg_file_poll(); /* 第 1 次 poll:建立变更基线 */
/* 伪造外部编辑(原子写,与 P4 后 FTP STOR 同路径) */
if (cfg_store_write_atomic(cfg_store_config_path(),
(const uint8_t *)s_cfgtest_doc,
strlen(s_cfgtest_doc)) != 0) {
fprintf(stderr, "[cfgtest] forge write failed\n");
return 1;
}
cfg_file_poll(); /* 第 2 次 poll:检出变更,沉降计数 1 */
cfg_file_poll(); /* 第 3 次 poll:沉降完成 → 重载 + 变更回调 */
p_cfg = net_cfg_get();
for (i = 0; i < 4; i++) {
if (p_cfg->ip[i] != s_cfgtest_ip[i]) {
printf("[cfgtest] FAIL ip after reload = %u.%u.%u.%u (expect 10.0.0.1)\n",
(unsigned)p_cfg->ip[0], (unsigned)p_cfg->ip[1],
(unsigned)p_cfg->ip[2], (unsigned)p_cfg->ip[3]);
return 1;
}
}
printf("[cfgtest] OK ip after reload = 10.0.0.1\n");
/* 保存回环:外科手术式保存(目标文件已存在)→ 回读校验注释保留与值一致 */
if (net_cfg_save() != 0) {
printf("[cfgtest] FAIL net_cfg_save (existing file)\n");
return 1;
}
{
uint8_t buf[CFG_FILE_MAX_LEN + 1u];
char *p_doc;
size_t n = 0u;
if (cfg_store_read(cfg_store_config_path(), buf, sizeof(buf), &n) != 0) {
printf("[cfgtest] FAIL readback open\n");
return 1;
}
buf[n] = '\0';
p_doc = (char *)buf;
if (strstr(p_doc, "; cfgtest forged external edit") == NULL) {
printf("[cfgtest] FAIL comment lost after surgical save\n");
return 1;
}
if (strstr(p_doc, "ip=10.0.0.1") == NULL) {
printf("[cfgtest] FAIL value lost after surgical save\n");
return 1;
}
}
printf("[cfgtest] OK surgical save (comment kept, value intact)\n");
printf("[cfgtest] PASS\n");
return 0;
}
/**
* @brief 程序入口
*/
@@ -181,6 +272,9 @@ int main(int argc, char **argv)
if (strcmp(argv[i], "--keys") == 0 && (i + 2) < argc) {
return run_headless(argv[i + 1], argv[i + 2]);
}
if (strcmp(argv[i], "--cfgtest") == 0) {
return run_cfgtest();
}
}
/* 初始化 Winsock(供 ui_net_sink 使用) */
@@ -233,6 +327,9 @@ int main(int argc, char **argv)
printf("Keyboard: ↑↓←→ navigate, Enter confirm, Escape back\n");
printf("Press Ctrl+C or close window to exit\n\n");
/* 配置文件轮询起点(2s 周期,与 MCU netTask 一致) */
uint32_t cfg_last_poll = (uint32_t)SDL_GetTicks();
/* 主循环:驱动 LVGL 定时器与刷新
* 注意:SDL 事件(键盘/窗口/退出)由 LVGL SDL 驱动内部 5ms 定时器
* (lv_sdl_window.c 的 sdl_event_handler)统一轮询分发,
@@ -242,6 +339,13 @@ int main(int argc, char **argv)
/* 轮询 RemoDispBus 协议(非阻塞收发) */
ui_net_sink_poll();
/* 配置文件轮询(2s 周期:外部文件变更 4s 沉降后自动重载) */
uint32_t now_ms = (uint32_t)SDL_GetTicks();
if ((uint32_t)(now_ms - cfg_last_poll) >= 2000u) {
cfg_last_poll = now_ms;
cfg_file_poll();
}
/* 驱动 LVGL 定时器与刷新 */
uint32_t sleep_ms = lv_timer_handler();
if (sleep_ms == LV_NO_TIMER_READY) {
+5 -2
View File
@@ -56,8 +56,10 @@ int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nre
* 入口参数:p_path - 目标文件路径 const char *
* p_buf - 待写数据(不可为 NULL) const uint8_t *
* len - 数据长度 size_t
* 返回值:0 - 成功;-1 - 写/改名失败(目标文件保持不动,临时文件清理) int
* 限定条件:无
* 返回值:0 - 成功;-1 - 写/改名失败(临时文件清理) int
* 限定条件:MSVC rename() 不覆盖已存在目标(C1117),须先 remove 再 rename
* (tmp 已完整写盘关闭后操作,中断窗口仅毫秒级;目标丢失时引擎
* 下次加载自动重建出厂模板,与 MCU 端 f_rename 直接替换等效)
*/
int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) {
FILE *fp;
@@ -76,6 +78,7 @@ int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len)
(void)remove(s_path_tmp);
return -1;
}
(void)remove(p_path); /* 目标可能不存在(首写):remove 失败忽略 */
if (rename(s_path_tmp, p_path) != 0) {
(void)remove(s_path_tmp);
return -1;