模拟器迁移 P5:2s 配置轮询 + --cfgtest 无头自检;修复 MSVC rename 覆盖与外科保存截断缺陷
This commit is contained in:
@@ -150,6 +150,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)
|
||||
|
||||
## 图形绘制(文档配图)
|
||||
|
||||
|
||||
+27
-29
@@ -105,8 +105,8 @@ static int s_str_ci_eq(const char *p_a, const char *p_b) {
|
||||
* 入口参数:p_p - 当前位置指针(入/出) char **
|
||||
* 返回值:行首指针 char *
|
||||
* 限定条件:*p_p 指向可写缓冲内
|
||||
* 函数说明:行界定 = '\n'(缺失时到 NUL);会修改缓冲(行末字节),
|
||||
* 保存路径 Pass 1 后须经 s_restore_newlines 复原
|
||||
* 函数说明:行界定 = '\n'(缺失时到 NUL);会修改缓冲(行末字节,
|
||||
* 含 CRLF 的 '\r'),保存路径 Pass 1 须用一次性副本
|
||||
*/
|
||||
static char *s_split_line(char **p_p) {
|
||||
char *p_line = *p_p;
|
||||
@@ -136,22 +136,6 @@ static char *s_split_line(char **p_p) {
|
||||
return p_line;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:复原行末换行符(s_split_line 的逆操作,供保存 Pass 2 复用缓冲)
|
||||
* 入口参数:p_buf - 行末已 NUL 化的缓冲 char *
|
||||
* 返回值:无
|
||||
* 限定条件:p_buf 非空
|
||||
*/
|
||||
static void s_restore_newlines(char *p_buf) {
|
||||
char *p = p_buf;
|
||||
|
||||
while (*p != '\0') {
|
||||
size_t l = strlen(p);
|
||||
p[l] = '\n';
|
||||
p = p + l + 1u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:识别段名行(非修改式)
|
||||
* 入口参数:p_line - 行文本 const char *
|
||||
@@ -802,7 +786,7 @@ static int s_surgical(char *p_in, size_t in_len, char *p_out, size_t out_cap, si
|
||||
* 函数功能:保存 Pass 1——定位段头行号与各受管键行号
|
||||
* 入口参数:p_in - 原文本(可修改) char *
|
||||
* 返回值:无
|
||||
* 限定条件:完成后须调 s_restore_newlines 复原行末
|
||||
* 限定条件:行末被 NUL 化且不复原——调用方须传一次性副本
|
||||
*/
|
||||
static void s_surgical_pass1(char *p_in) {
|
||||
char *p = p_in;
|
||||
@@ -861,8 +845,6 @@ static void s_surgical_pass1(char *p_in) {
|
||||
}
|
||||
line_no++;
|
||||
}
|
||||
|
||||
s_restore_newlines(p_in);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -1039,23 +1021,39 @@ int cfg_file_save(void) {
|
||||
|
||||
/* 外科手术式保存:保留注释/未知段/未知键 */
|
||||
{
|
||||
uint8_t *p_in = NULL;
|
||||
char *p_out = (char *)malloc(CFG_OUT_MAX);
|
||||
uint8_t *p_in = NULL; /* 加载原文(pristine,供 Pass 2 重切分) */
|
||||
uint8_t *p_in1 = NULL; /* Pass 1 专用副本(s_split_line 破坏性 NUL 化后丢弃) */
|
||||
char *p_out = NULL;
|
||||
size_t in_len = 0u;
|
||||
size_t out_len = 0u;
|
||||
int r;
|
||||
|
||||
if (p_out == NULL) {
|
||||
DBG_ERROR("save: malloc %uB FAIL", (unsigned)CFG_OUT_MAX);
|
||||
return -1;
|
||||
}
|
||||
if (s_load_file(&p_in, &in_len) != 0) {
|
||||
free(p_out);
|
||||
DBG_ERROR("save: file read FAIL");
|
||||
return -1;
|
||||
}
|
||||
|
||||
s_surgical_pass1((char *)p_in);
|
||||
/* Pass 1 在独立副本上切分(避免破坏原文的行结束符);
|
||||
* 副本在 p_out 分配前释放,堆峰值维持 p_in + p_out(约 9KB) */
|
||||
p_in1 = (uint8_t *)malloc(in_len + 1u);
|
||||
if (p_in1 == NULL) {
|
||||
free(p_in);
|
||||
DBG_ERROR("save: malloc %uB FAIL", (unsigned)(in_len + 1u));
|
||||
return -1;
|
||||
}
|
||||
(void)memcpy(p_in1, p_in, in_len + 1u);
|
||||
|
||||
s_surgical_pass1((char *)p_in1);
|
||||
|
||||
free(p_in1);
|
||||
|
||||
p_out = (char *)malloc(CFG_OUT_MAX);
|
||||
if (p_out == NULL) {
|
||||
free(p_in);
|
||||
DBG_ERROR("save: malloc %uB FAIL", (unsigned)CFG_OUT_MAX);
|
||||
return -1;
|
||||
}
|
||||
|
||||
r = s_surgical((char *)p_in, in_len, p_out, CFG_OUT_MAX, &out_len);
|
||||
|
||||
free(p_in);
|
||||
|
||||
+2
-1
@@ -486,7 +486,7 @@ void adc_cfg_init(void)
|
||||
| **P2** | FS 线程安全 + 时间戳 | `ffconf.h`、`Lib/FatFs/ffsystem.c`(新)、`App/util/sys_clock.c`(sys_clock_get_time;移入中间件层)、`uvprojx`、架构总览 §1.1(sys_clock 改归 ④) | `@build` 0/0;`@test` 启动 PASS(无死锁) |
|
||||
| **P3** ✅ 2026-09-14 | cfg_file 引擎(双端)+ net_cfg 集成 + 旧档迁移 | `App/cfg_file.{h,c}`、`App/cfg_file_store.h`、`App/cfg_file_store_mcu.c`(新)、`simulator/sim/cfg_file_store_sim.c`(新)、`net_cfg.{h,c}`(段注册 + 引擎化 load/save)、`net_cfg_legacy.c`(新,NCFG 迁移)、删 `net_cfg_store*`、`uvprojx`、`CMakeLists.txt` | Keil `@build` 0/0;模拟器生成 `sim_config.ini` 且编辑回读一致;旧档 NCFG 自动迁移三态正确 —— **已实测通过**(工厂模板 / 编辑回读 / 非法键回退 / 迁移三态 / 双端 0 警告;`cfg_file.c` 含中文注释块 → UTF-8 **带 BOM**) |
|
||||
| **P4** | 轮询 + D15 协议 + 测试标记 | `net_task.c`(沉降去抖轮询 + 落盘收敛 + 串口标记)、`lftpd.c`(STOR tmp+rename) | `@build`;`@test` 启动新标记(`cfg file: loaded OK` 等);FTP 上传无半截可见(rename 原子) |
|
||||
| **P5** | 模拟器迁移 | `simulator/sim/cfg_file_store_sim.c`(新)、删 `net_cfg_store_sim.c`、`main.c`(轮询)、`CMakeLists.txt` | sim 构建 + 运行;`sim_netcfg.bin` 自动迁移 |
|
||||
| **P5** ✅ 2026-09-14 | 模拟器迁移 | `simulator/sim/cfg_file_store_sim.c`(新)、删 `net_cfg_store_sim.c`、`simulator/main.c`(2s 轮询 + `--cfgtest` 自检)、`CMakeLists.txt` | sim 构建 0 警告 + 4 场景无头 PASS(工厂 / 有效旧档迁移 / 无效旧档保留 / UI 无头);`sim_netcfg.bin` 自动迁移 + 轮询沉降重载 + 外科保存无损回环 —— **已实测通过** |
|
||||
| **P6** | 文档同步 | `网络配置说明.md`(§0/3/6/7 重写)、`AGENTS.md`、`README.md`、`项目架构总览.md` + `gen_arch_svg.py`、`内容页架构.md`、BSD 指南、`simulator/README.md`、`ui_page_net_cfg.c` 头注释、`docs/测试说明/` | pre-commit 全绿;全库无"Flash"旧表述残留 |
|
||||
| **P7** | FTP 闭环脚本 | `test/ftp_config_test.py`(新)+ 测试说明 | 真机闭环 PASS(§11.2) |
|
||||
| **P8** | 总回归 + 提交 | `@build` + `@test` + 模拟器 ui-verify + `pre-commit --all-files` | 0 错误 0 警告;全 PASS |
|
||||
@@ -569,3 +569,4 @@ void adc_cfg_init(void)
|
||||
| 2026-09-13 | v1.6:`sys_clock` 由 ③基础服务层改归 **④中间件层**(通用平台时间服务),文件移入 `App/util/`(与 crc/ringbuf 同目录):`ffsystem` → `sys_clock` 成为**层内引用**,工程反向依赖消除;"反向依赖例外" 措辞同步撤销 |
|
||||
| 2026-09-13 | v1.7:FS 并发定案 **A+**(REENTRANT 互斥 + 三条一致性协议:写 tmp+rename 扩至 FTP STOR / 沉降去抖重载 / 配置文件单写者收敛 netTask);**actor 方案(单所有者队列 + 全代理层)经完整评估否决**(代理 ~400–600 行、RAM +4KB、符号改名/影子头、自死锁纪律——结构优势可由三协议 ~40 行等价中性化);新增 D15,D9 改为"一切写入 tmp+rename",P4 增列 lftpd STOR 改造 |
|
||||
| 2026-09-14 | v1.8:**P3 完成**(双端 0/0):`cfg_file` 段键表引擎 + `cfg_file_store` 双端后端 + `net_cfg` [network] 段集成(对外 API 不变,D14 兑现)+ `net_cfg_legacy` 旧档迁移;无头实测通过(工厂模板生成 / 编辑回读 / 非法键工厂回退 / 未知段容留 / 迁移三态 / 双端 0 警告);§6 接口修订为实现版(store 带路径参数 + 三路径 getter;变更回调随段注册,撤销独立 `cfg_file_register_cb`);§8 静态增量按实测 706 B 修订 |
|
||||
| 2026-09-14 | v1.9:**P5 完成**(sim 0 警告):模拟器主循环接入 2s `cfg_file_poll`(与 MCU netTask 同周期);新增无头自检 `lvgl_sim.exe --cfgtest`(加载/迁移 → 建基线 → 伪造外部编辑 → 两次轮询沉降 → 断言 RAM 更新 + 外科保存回环校验注释/值无损);修复其暴露的两处缺陷:① MSVC `rename()` 不覆盖已存在目标(C1117)——模拟器后端先删目标再 rename(目标丢失时引擎自动重建模板兜底);② **外科保存截断 bug**——`s_split_line` 将 CRLF 行的 `\r`/`\n` 双双 NUL 化,旧 `s_restore_newlines` 只复原一个 `\n` 即遇 NUL 终止,保存产物只剩首行(P3 未触发"对已存在文件保存"故未暴露)——改为 Pass 1 用一次性副本(堆峰值维持约 9KB)、删除复原函数 |
|
||||
|
||||
@@ -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]` 段(模拟器无网络栈,变更回调为空操作,仅日志可见)。
|
||||
|
||||
---
|
||||
|
||||
## 四、已知坑(构建/运行)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user