Files
STM32F4-Base/simulator
wandering1 52e28885cb feat(UI): 装置维护 3 页落地(时钟 / 强制复归 / 清除记录),内容页黄金 27→40
三个薄页面模块 ui_page_clock / ui_page_reset / ui_page_clear,页面注册由生成器
自动写双端(裁定 A-1:「三叶各绑一页」与「零新增 .c」不可兼得,选薄页面加生成器,
不写手写 node switch——那种分发在增删叶子时会静默换功能,且没有任何门禁照得到)。

数据侧是注入式函数表:UI 不 include App 层的头。两条硬理由都实测过——分层方向
(layer-deps)之外,模拟器根本不链 sys_clock,ALLOW 豁免挡得住 layer-deps,
挡不住模拟器链接期。面板对时的唯一真判据是回读 RTC:sys_clock_set() 恒返 0
(RTC 写失败只打日志),所以真机侧写 1 后回读 1,最坏约 200 ms 落在一次「保存」
按键上,IWDG 800 ms 窗有余;上界与依据都写进注释。

「无时钟」徽标位置有两个像素级实测坑:放标题带右侧会被 4 字标题连成「时钟设置
无时钟」一句话;放页脚带左侧 x=7 会压住页脚——页脚从 x=6 起排,不是居中。
最终落在清单末行与页脚之间的空带 y=124。clear_done 那张顺带给出像素证据:
清 run.log 之后「操作记录」那一行从 2 变 3,页面上没缓存、没撒谎。

ui_w_records 的 UI_W_REC_LOG_* 顺序必须等于 syslog_id_t 顺序(7 类,只准尾追加),
已加编译期顺序守卫;执行侧 7 条映射表同样带编译期守卫。

测试与真值:内容页黄金 27 → 40 页(+13),文本断言 +68、反向断言 ABSENT +19;
27 张旧参考 git diff 为空 = 逐字节未变。往返池 5448 B 与菜单常驻堆 5368 B 均未变;
字库未扩(源码在用 171 并 KEEP_EXTRA 39 = 清单 183),font-charset 与 cn-chars、
cn-font 字节一致。@build 0 错误 0 警告;uiTask 静态最坏链 1,336 B / 16,384
(余 91%)与基线相同,页面钩子没进最坏链。

一条测试学结论单独留痕:反向断言不能当「位置不重叠」的证据——本脚本的字形匹配
把 y 带判得比字面宽(徽标在 125..136、页脚在 144..155,仍被 (140,160) 判命中)。
该防线改由正向断言「页脚未被徽标压住」承担(压字那一版实测过不了它)。

两条自我更正一并记档(作者自报,按真值而非原口径):
① 先前报「文案零新增汉字」是错的——观测行拟用「动作元件」,「元」不在子集,
cn-chars 与 font-charset 如实红;最终改「保护动作 N」才让「零新增」成为事实。
顺序是:先按机器红,再按真值写结论。
② 先前报「6 个文件 BOM 逐个核过」被 bom-guard 证伪:丢 BOM 发生在上一次编译之后,
正是 AGENTS 那句「历史 0 警告不代表安全」的活例;此后一律以退出码为准。

未覆盖项不假装:台架「N>0 → N=0」在产品构建的未标定闭锁期拿不到(闭锁期 N 恒 0),
判据显式退化为「页面受理 + 落一条操作记录」,写在 rs_watch 注释里。

生成物入库例外照用:uvprojx 只加 3 个 ui_page_ 的 FilePath(真增删源文件),
CMakeLists 同批;两处都由 gen_menu.py 自己写,没手写。

本笔先走了一次真钩子:20 条钩子实跑通过(ui-golden 菜单 8 状态加内容页 40 页、
generated-fresh、registration-check、font-charset、ui-data 单测、cppcheck、
coding-standard、bom、mojibake、id-check、doc-links 等),只有 mem-numbers 与
mem-budget 两条红——磁盘上的 .map 出自 20:39 那次构建、含三条在飞道的代码,
与 HEAD 台账必然不同码。按仓库既有约定(这两条等台账回填时允许 --no-verify 并
正文留证)落这一笔;回填由我统一在收尾一笔做。

三条在飞道(task-26 崩溃证据路径、task-16 与 task-17 谓词收敛和卷锁观测、
task-36 台架工具)的未暂存改动被 autostash 暂时收走,钩子看到的是 HEAD 加本笔;
过程中曾把 comm 道未跟踪的 iec104_comm.c 临时移出(它不在 stash 覆盖面内),
那次移出反而让 ui-golden 撞见 CMake 缓存里的悬空引用而红——已改用「先落 comm
那笔、再落本笔」的顺序消掉这个中间态。ui_data_maint_ready() 待删(只有测试在读
的自检口),排在本笔之后单独一笔。
2026-10-26 22:43:54 +08:00
..
2026-09-03 23:08:16 +08:00

LVGL PC 模拟器(simulator/)

在 PC 上编译运行与 MCU 同一份 UI 源码(Drivers/BSP/UI/ui_menu.c + ui_font_cn_12.c), 实时预览 160×160 1bpp 单色菜单界面,无需烧录 MCU。

部署细节与完整 FAQ 见 docs/UI设计/LVGL模拟器部署与使用说明.md;本文聚焦本目录的内容与架构。


一、目录内容

simulator/
├── CMakeLists.txt          # 构建脚本(CMake + MSVC,x64)
├── build_sim.bat / .ps1    # 一键构建入口(自动探测 VS2019/2022/2026 生成器,x64;.bat 绕过执行策略)
├── main.c                  # 入口:SDL 初始化 → LVGL → 菜单 → 主循环
├── sim/                    # 平台层(PC 专用实现;MCU 端对应实现在各 BSP 目录)
│   ├── sim_port.c / .h     # 显示/输入端口(SDL 窗口 + 键盘 + mono 主题),替代 MCU 端 ui_port.c
│   ├── net_socket_sim.c    # 网络后端:Winsock 实现 net_* BSD API,替代 MCU 端 net_socket.c(CH395F)
│   ├── ui_net_sink_backend_sim.c  # RemoDispBus 平台 backend:取帧/注键(配合共享 ui_net_sink.c)
│   └── cfg_file_store_sim.c         # 配置文件存储后端:本地 sim_config.ini(旧档 sim_netcfg.bin 已废弃,不读不删)
├── lv_conf.h               # 模拟器专用 LVGL 配置(与 MCU 端 Lib/lv_conf.h 相互独立)
├── i1_to_png.py            # 截图转 PNG:把 --screenshot 的 I1(1bpp) 原始位图转成 PNG(见 §3.5)
├── tests/                  # 无头回归脚本 + 本地黄金参考(见 §3.6,流程文档 docs/UI设计/UI_自动化测试流程.md)
│   ├── check_menu_rows.py      # 菜单 8 状态像素回归 + 池基线门禁
│   ├── check_content_pages.py  # 文本断言 + 内容页黄金参考回归 + 往返池门禁(--update 刷参考)
│   ├── check_remo_sink.py      # RemoDispBus 远端推流协议回归(起窗口模拟器 + TCP 取帧 + 参考比对)
│   ├── regen_ref_png.py        # 由 ref/*.bin 批量重生成目检 PNG(--scale 6 --invert)
│   └── ref/                    # 黄金参考 shot_*.bin(**入库**,判据本体)+ 目检 *.png(本地派生物,不入库)
├── dbg_log.h               # 调试日志 stub:DBG_* 映射到 printf(覆盖 MCU 版头文件)
├── SDL2/
│   └── SDL2-2.32.10/       # SDL2 预编译 SDK(随仓库提供,无需安装)
│       ├── include/        # SDL2 头文件
│       │   └── SDL2/       # ★ 头文件副本(LVGL SDL 驱动写死 #include "SDL2/SDL.h")
│       └── lib/
│           ├── x64/        # SDL2.lib / SDL2main.lib / SDL2.dll(构建使用)
│           └── x86/        # 32 位库(当前未使用)
└── build/                  # CMake 生成物(可整体删除重建,勿提交)
    └── Debug/lvgl_sim.exe  # 可执行文件(SDL2.dll 由 POST_BUILD 自动复制到同目录)

文件职责

文件 说明
main.c 程序入口。SDL_Init(TIMER|EVENTS) → lv_init() → sim_port_init(160,160) → ui_menu_init()(共享代码)→ 主循环(SDL 事件 + lv_timer_handler + lv_refr_now)
sim/sim_port.c 创建 SDL 窗口显示设备(lv_sdl_window_create)、初始化 mono 主题(lv_theme_mono_init,1bpp 必需)、创建键盘输入设备并挂默认输入组。不创建鼠标(纯键盘导航,与 MCU 端 keypad 一致)。sim_port_get_screen_mono() 经 SDL_RenderReadPixels 读回屏幕像素转 1bpp,供推流取帧
sim/net_socket_sim.c 网络后端:Winsock 实现 net_* BSD Socket API(socket/bind/listen/accept/recv/send/close),替代 MCU 端 Drivers/BSP/NET/net_socket.c(CH395F)
sim/ui_net_sink_backend_sim.c RemoDispBus 平台 backend:ui_net_sink_get_lcdmem→sim_port_get_screen_mono、ui_net_sink_inject_key→lv_group_send_data;配合共享 Drivers/BSP/UI/ui_net_sink.c(协议服务端,双端同一份,TCP 7003)
sim/cfg_file_store_sim.c 通用配置文件存储后端:本地文件 sim_config.ini(fopen/rename 原子写),替代 MCU 端 App/cfg/cfg_file_store_mcu.c(FatFS);旧档 sim_netcfg.bin 已废弃——不读、不删、不迁移(2026-10-10)
lv_conf.h 模拟器专用 LVGL 配置。与 MCU 端 Lib/lv_conf.h 的关键差异见 §3.3
dbg_log.h MCU 版 dbg_log.h 依赖 sys_clock.h(RTC 时间戳),PC 无法编译。此 stub 把 DBG_INFO/ERROR/DEBUG 映射到 printf;因 simulator/ 在 include 路径最前,优先生效覆盖 MCU 版
build_sim.bat / build_sim.ps1 一键构建入口:vswhere 探测本机 VS 大版本(16=2019 / 17=2022 / 18=2026,取已装最高)→ 映射 CMake 生成器;build/ 缓存生成器与本机不符(换机/升级 VS)时自动删除重建(带重试,抗文件锁)。构建入口一律走它,文档/命令中勿写死生成器版本;.bat 仅负责绕过 PowerShell 执行策略并转交 .ps1
CMakeLists.txt 收集 LVGL 源码(../Lib/lvgl)+ 共享 UI 源码(../Drivers/BSP/UI)+ 通用配置文件引擎(../App/cfg_file.c、../Drivers/BSP/NETCFG 的 net_cfg;net_cfg_legacy 已于 2026-10-10 删除)+ sim/ 平台层,链接预编译 SDL2,产出 lvgl_sim.exe
i1_to_png.py 把 --screenshot 产出的 I1(1bpp) 原始位图转成 PNG,便于离线比对设计稿。详见 §3.5
tests/ 无头回归脚本(菜单 8 状态像素回归 + 内容页 27 页:文本断言 / 反向断言 / 黄金参考逐字节比对);ref/shot_*.bin 随仓库入库(判据本体,改动等于改判据,评审须逐页过目),ref/*.png 是本地派生的目检图(.gitignore 挡掉)。详见 §3.6

共享代码不复制:ui_menu.c / ui_font_cn_12.c / ui_net_sink.c 直接引用 Drivers/BSP/UI/ 下的原件, MCU 与 PC 两端永远同步。不要在本目录内复制 UI 源码。


二、架构框架

2.1 运行架构

main.c
  │
  ├─ SDL_Init(TIMER | EVENTS)              # 仅初始化计时/事件,视频由 LVGL SDL 驱动内部接管
  ├─ lv_init()                             # LVGL v9.3.0 内核
  ├─ sim_port_init(160, 160)
  │    ├─ lv_group_create + set_default   # 默认输入组
  │    ├─ lv_sdl_window_create(160,160)   # SDL 窗口 = 显示设备(I1 单色)
  │    ├─ lv_theme_mono_init(disp, true,  # mono 主题(白字黑底),1bpp 下不初始化 = 黑屏
  │    │              LV_FONT_DEFAULT)
  │    └─ lv_sdl_keyboard_create           # 键盘 → 默认输入组(方向键/Enter/Esc)
  ├─ ui_menu_init()                        # ★ 与 MCU 共用(Drivers/BSP/UI/ui_menu.c)
  │                                        #   基元控件页面导航,节点表驱动
  └─ 主循环
       ├─ SDL_PollEvent                    # SDL_QUIT → 退出
       ├─ lv_timer_handler()               # 驱动 LVGL 定时器/重绘
       ├─ lv_refr_now(NULL)                # 强制整屏刷新(PARTIAL 模式下保证首帧输出)
       └─ Sleep(lv_timer_handler 返回值)   # 自适应休眠

2.2 渲染链路(1bpp 单色)

LVGL 场景(基元控件页面/label,LV_COLOR_FORMAT_I1)
  → SW 渲染器(仅启用 I1,LV_DRAW_SW_SUPPORT_I1=1)
  → SDL 窗口 blit(LV_SDL_RENDER_MODE_PARTIAL,1bpp 强制要求)
  → 屏幕(160×160 物理像素,Windows 高 DPI 可能放大显示,属正常)

对照 MCU 端链路(ui_port.c):

LVGL 场景(同一份 ui_menu.c)
  → SW 渲染器(I1)
  → flush 回调 memcpy 到内存帧缓冲 g_ui_fbuf(3200 B)
  → RemoDispBus 协议(TCP 7003)按需推给远端客户端显示

两端场景层完全相同,只有"渲染结果去向"不同:模拟器进 SDL 窗口,MCU 进内存帧缓冲再走网络。

2.3 MCU 端 vs 模拟器 对照

项 MCU(Keil MDK) 模拟器(CMake + MSVC)
LVGL 编译 .uvprojx 工程添加 Lib/lvgl 源文件 Lib/lvgl/CMakeLists.txt(最小 CMake,仅模拟器用,不影响 Keil 编译)
lv_conf.h Lib/lv_conf.h simulator/lv_conf.h(独立,互不影响)
显示端口 ui_port.c(离屏 framebuffer + flush 回调) sim_port.c(SDL 窗口)
输入 远端 keypad(ui_port_queue_key,网络回传键码) SDL 键盘(lv_sdl_keyboard_create)
主题 无主题(LV_USE_THEME_* = 0),控件用内置基础样式 mono 主题必须开启(否则 1bpp 黑底黑字)
日志 Drivers/BSP/dbg_log.h(USART1 + RTC 时间戳) simulator/dbg_log.h(printf stub)
业务任务 netTask / ftpTask / timeSyncTask 等 无(仅编译纯软件部分的配置文件引擎 net_cfg/cfg_file,可离线验证配置加载/保存)
时钟 lv_tick_inc 由 ui_task 手动喂(10ms 周期) SDL 驱动内部 lv_tick_set_cb(SDL_GetTicks),不可再手动喂
UI 数据来源 App/app/ui_data_feed.c 每轮从 phasor / prot_set / 104 点表…装配并发布三帧 main.c 一次性发布固定帧(定值不手抄数字,取链进来的 App/algo/prot_set.c 出厂默认);口径 docs/UI设计/UI_数据供给设计说明.md §9.2
定值写路径(保存) ui_data_feed_tick() 里 ui_data_param_drain() 落定 → 置脏 → defaultTask 写 /prot_set.bin sim_param_pump() 用同一份 g_prot_set_meta 上下限预检后落值到 RAM,到此为止:模拟器无 NAND,"重启值保持 / 主站 C_RD 读到同值"只能上板验(差距登记 docs/测试说明/模拟器差距清单.md)

2.4 关键设计决策

  1. SDL2:LVGL 官方 PC 端口同款显示方案;使用仓库内预编译 SDK(SDL2/SDL2-2.32.10/),无需系统安装
  2. CMake + MSVC:build_sim.ps1 按本机 VS 版本自动选生成器(16=2019 / 17=2022 / 18=2026;不同机器版本不同,勿写死),-A x64;MSVC 下加 /utf-8 消除中文注释的 C4819 警告
  3. 共享 UI 源码:ui_menu.c / ui_font_cn_12.c 直接编译 Drivers/BSP/UI/ 原件,改一处两端同时生效
  4. 纯键盘导航:不创建鼠标/触摸设备,与 MCU 端 keypad 操作语义一致(↑↓←→ / Enter / Esc)
  5. UI 为主:不含 CH395F 网络 / NAND 存储 / ADC 等硬件业务;通用配置文件引擎(cfg_file)与 net_cfg 核心为纯软件,配置加载/保存可在模拟器离线验证(sim_config.ini;旧档 sim_netcfg.bin 已废弃、不参与)

三、构建与运行

3.1 环境要求

依赖 版本 说明
CMake ≥ 3.15 本机验证 4.3.0-rc1
MSVC VS2019 / 2022 / 2026 任一 build_sim.ps1 自动探测生成器(16=2019 / 17=2022 / 18=2026,取已装最高),Hostx64/x64
SDL2 2.32.10 已内置于 SDL2/,无需安装
操作系统 Windows 已验证 win32

3.2 构建命令

# 一键构建(仓库根目录执行;自动探测本机 VS2019/2022/2026 生成器,
# build/ 缓存生成器与本机不符时自动删除重建)
& "simulator\build_sim.bat"

# 运行
.\simulator\build\Debug\lvgl_sim.exe
# 或:cmake --build simulator\build --config Debug --target run

# 全量重建
& "simulator\build_sim.bat" -Clean

改 CMakeLists.txt 后再跑一次 build_sim.bat 即可(缓存不匹配会自动重建);遇到奇怪问题加 -Clean 全量重建。 手动 CMake 等效:cd simulator; cmake -B build -G "Visual Studio <NN> <YYYY>" -A x64; cmake --build build --config Debug,生成器号须与本机 VS 大版本一致(16=2019 / 17=2022 / 18=2026)。

3.3 lv_conf.h 与 MCU 端的关键差异

配置 MCU 端(Lib/lv_conf.h) 模拟器(本目录) 原因
LV_USE_SDL 0 1 SDL 显示/键盘驱动
LV_SDL_RENDER_MODE — PARTIAL 1bpp + SDL 非 PARTIAL 会 #error
LV_SDL_INCLUDE_PATH — "SDL2/SDL.h" 对应 include/SDL2/ 头文件副本
LV_USE_THEME_MONO 0 1 1bpp 无主题 = 黑底黑字不可见
LV_DRAW_SW_SUPPORT_I1 1 1 仅启用 I1 渲染,裁掉其余色深
LV_MEM_SIZE 16 KB 16 KB 必须一致(lv-conf-sync 比对生效值;2026-10-15 由 40 KB 同步收缩,PC 侧不再"放宽")
LV_FONT_DEFAULT &ui_font_cn_12 同 共用同一字体
其余控件/驱动 裁剪 基本一致(另开 LV_USE_TEXTAREA/KEYBOARD/BUTTONMATRIX) 模拟器略宽松,便于调试

3.4 键盘操作

按键 功能
↑ / ↓ 菜单项上下移动
→ 进入子页
Enter 确认 / 进入子页
← / Escape 返回上级

3.5 截图比对设计稿(离线出图)

窗口模式只适合人工目检。要把渲染结果存成 PNG 与设计参考(docs/UI设计/mockups/ 下的原始参考图)做目视比对,用离屏截图 + 转 PNG:

cd simulator
# 1) 离屏渲染首帧,输出 I1(1bpp) 原始位图(已剥离 8 字节调色板,纯像素数据)
.\build\Debug\lvgl_sim.exe --screenshot shot.bin
# 2) 转 PNG(默认 160x160;--scale 放大便于查看;python 统一用仓库 .venv,见 AGENTS.md「Python 环境」)
..\.venv\Scripts\python.exe i1_to_png.py shot.bin                # -> shot.png
..\.venv\Scripts\python.exe i1_to_png.py shot.bin -o 预览.png --scale 6

I1 位图格式(i1_to_png.py 据此解析):1 位/像素,每字节 MSB 在左(bit7 = 该行最左像素),bit=1 前景 / bit=0 背景,每行 宽/8 字节、行内无填充。

i1_to_png.py 常用参数:

参数 说明
-o, --output 输出 PNG 路径(默认与输入同名 .png)
-W, -H 宽高(默认 160/160,非默认分辨率时必填)
--scale N 最近邻放大 N 倍,便于查看小图
--invert 前/背景互换
--fg / --bg 自定义前/背景色 R,G,B(默认黑白)
--no-auto-palette 关闭调色板自动识别(按纯像素解析)

依赖 Pillow(已含于仓库 .venv,见 requirements.txt)。shot.bin/shot*.png 均已列入 .gitignore,可随时删除重生成。

3.6 无头回归测试(tests/)

UI 改动的自动化门禁脚本(完整流程见 docs/UI设计/UI_自动化测试流程.md)。除 check_remo_sink.py(需 Pillow,.venv 已含)外均为纯标准库、任意 python3 均可;为环境统一,一律用仓库 .venv 运行:

# 仓库根目录执行(前置:本目录已构建出 build/Debug/lvgl_sim.exe)
.venv\Scripts\python.exe simulator/tests/check_menu_rows.py      # 菜单 8 状态像素回归 + 池基线门禁
.venv\Scripts\python.exe simulator/tests/check_content_pages.py  # 文本断言 + 内容页黄金参考回归 + 往返池门禁
.venv\Scripts\python.exe simulator/tests/check_remo_sink.py      # RemoDispBus 远端推流协议回归(起窗口模拟器 + TCP 取帧 + 参考比对)
脚本 原理 失败输出
check_menu_rows.py 键序注入(u/d/e/s)跑 8 个菜单状态,断言选中行白带、非选行黑底白字、框体边框、往返状态逐像素一致;MENU-STATE-00 池水位须等于脚本内 MENU_POOL_BASELINE 未过断言清单 + 保留临时截图
check_content_pages.py ① 文本断言(解析字体字形,在截图 y 带内精确匹配期望串,不依赖 ref/)② 反向断言(ABSENT 表:某串不该出现,如查看页/只读态不得出现「未标定」徽标与「保存」行——像素参考会被 --update 刷成"自证正确",反向断言不依赖参考图)③ 27 页无头截图与 tests/ref/shot_*.bin 逐字节比对(含 2026-10-26 新增的整定 9 态:进页 / 保存行 / 只读态 / 编辑态 / 越界 / 保存 OK / 落定后可读 / 二态回绕);另校验开页-关页往返池水位 = POOL_AFTER_ROUNDTRIP 文本未命中项 / 反向断言被命中 / 差异像素数 + 双方 ink runs 对照
check_remo_sink.py 启动窗口模拟器(非无头,CREATE_NEW_PROCESS_GROUP 防管道阻塞),经 TCP 7003 发 LCDMEM 取 1bpp 帧;断言首帧=LCDMEM(无 INIT 污染)/位图 3200 字节/非平凡;复用 i1_to_png 出目检 PNG;与无头参考(lvgl_sim --screenshot)逐位比对 <2% 判 MATCH。守护"INIT 污染 / 快照卡死"两个历史缺陷 各判据失败原因 + sim_err.txt 尾部 + 保留 PNG(默认不删,--clean 才删)
  • 黄金参考与目检 PNG(ref/*.bin 入库、ref/*.png 本地派生不入库):换机或想目检时—— .venv\Scripts\python.exe simulator/tests/check_content_pages.py --update 出 ref/*.bin, .venv\Scripts\python.exe simulator/tests/regen_ref_png.py 出目检 ref/*.png(默认 --scale 6 --invert)。 有意改视觉时先肉眼确认新效果正确再 --update(会先跑文本 + 反向断言防误刷), 并且改完必须 git diff --stat simulator/tests/ref 逐页过目:出现在这份清单里的每一张 都是"判据被改了",正常应当只有本次有意改动的那几张;--update 一把全刷等于把别的意外 一起签进仓库(ref/*.bin 是入库的,刷错就是提交了错判据)。
  • 回归为什么是确定的(无头不跑 LVGL 定时器):run_headless() 只在注入键值时调 lv_group_send_data(),从不调 lv_timer_handler()/lv_refr_now()(I1 + FULL 刷新模式在 离屏渲染上会进死循环,见 §四 已知坑与 main.c 里 shot_delay_cb 的注释)。推论有两条: ① 界面侧任何周期性 LVGL 定时器(如定值视图为"落定/主站写值/标定完成"上屏而建的 100 ms 轮询)在回归里一次都不会触发 → 像素与它无关,只有窗口模式才用它;② 两次注键之间不发生 重绘,所以"数据就位"不能依赖"这一页画过没有"——判据连跑两次逐字节一致即为证据,设计口径见 docs/UI设计/UI_数据供给设计说明.md §9.2 注 6。
  • 池水位常量漂移:菜单/页面结构变更导致门禁失败时,确认新值合理后再更新脚本内常量并登记流程文档变更记录;无法解释的漂移 = 内存回归 bug。
  • 退出码均为 0=PASS / 1=断言失败 / 2=环境错误(exe 缺失等)。

3.7 配置文件自检(--cfgtest)

配置引擎"加载 → 轮询 → 重载 → 保存"的进程内闭环验证(无需窗口 / LVGL,退出码 0=PASS / 非 0=FAIL);自检期间在当前工作目录临时使用 sim_config.ini,结束时无论成败一律删除(2026-10-15 起,原因见下文 ⚠):

.\build\Debug\lvgl_sim.exe --cfgtest

验证链:net_cfg_load(旧档迁移已于 2026-10-10 删除,只走 INI)→ 第 1 次 poll 建基线 → 伪造"外部编辑"(原子写,等价 FTP 上传/文本编辑器)→ 2 次 poll 沉降(对应真机 4s 沉降)→ 断言 RAM 副本已更新为新值 → 对已存在文件做外科式保存、回读校验注释/值无损。

空目录运行 = 验证出厂模板生成;超限文件(>4096 B)= 判损坏并重建出厂模板;超限文档保存 = 拒绝且不动原文件。窗口模式主循环以与 MCU netTask 相同的 2s 周期调用 cfg_file_poll():外部文件变更沉降后自动重载 [network] 段(模拟器无网络栈,变更回调为空操作,仅日志可见)。

⚠ 为什么必须删掉伪造档:超限文档保存那条断言要求"拒写后原文件不动"(size == 4000),所以连通过路径也会把这份 4000 B 纯注释档留在工作目录。之后每一次运行都会加载它 → 外科式保存要在 4000 B 注释后补写受管段 → 输出必超 CFG_FILE_MAX_LEN → 引擎按设计拒写 → 网口设置页保存行显示 ERR,把 UI 像素回归带偏(2026-10-15 实测踩中)。故 run_cfgtest() 只是外壳,跑完一律删除;simulator/tests/ 两套回归另以空临时目录当工作目录运行做双保险 → docs/UI设计/UI_自动化测试流程.md §五。


四、已知坑(构建/运行)

  1. cannot open include file "SDL2/SDL.h" LVGL SDL 驱动写死 #include "SDL2/SDL.h",而原版 SDK 头文件平铺在 include/ 下。 必须把头文件复制一份到 include/SDL2/ 子目录(本仓库已做好):
    $inc = "simulator\SDL2\SDL2-2.32.10\include"
    New-Item -ItemType Directory -Path "$inc\SDL2" -Force
    Copy-Item "$inc\*.h" "$inc\SDL2\"
    
  2. cannot open include file "lv_conf.h" LV_CONF_DIR / SDL2_INCLUDE_DIR 两个 CMake 变量必须在 add_subdirectory(lvgl) 之前定义(见 CMakeLists.txt 中的顺序注释),勿随意调整。
  3. 运行后窗口全黑,按序排查:
    • 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 是子集字体(当前 183 汉字/符号 + ASCII,共 278 字形),缺字渲染为空白。 补字流程见 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)。
  7. 键盘输入无响应(方向键/Enter 无效):main.c 主循环不得调用 SDL_PollEvent。 LVGL SDL 驱动内部有 5ms 定时器(lv_sdl_window.c 的 sdl_event_handler)统一轮询分发 SDL 事件; 主循环若先轮询会消费掉事件队列,键盘输入永远到不了 LVGL(现象:画面正常但按键全部无效)。 关窗退出由驱动 LV_SDL_DIRECT_EXIT=1 自动 exit(0),主循环无需处理 SDL_QUIT。

五、边界说明

  • 模拟器跑 UI + 配置文件引擎:不含 CH395F 网络、NAND 存储、ADC、RS-485 等硬件业务;cfg_file 引擎与 net_cfg(纯软件部分)在模拟器编译运行,可离线验证配置加载/保存
  • 模拟器含 ui_net_sink(Winsock 版 RemoDispBus 推流服务端,TCP 7003)——远端看图链路 可在模拟器上验证(simulator/tests/check_remo_sink.py);CH395F 真实网络栈仍只在 MCU 上验证
  • build/ 为生成物,可随时删除重建(已在 .gitignore 中忽略)
  • Lib/lvgl/CMakeLists.txt 仅被本模拟器引用,不影响 Keil 的 MCU 编译

模拟器跑过 ≠ 真机跑过:本目录与真机的全部已知差异(网络 pending 机制缺失、代次号恒 0、 日志/配置的 mtime 粒度与堆后端差异、IWDG/NAND/RTC 等硬件不可建模)统一登记在 ../docs/测试说明/模拟器差距清单.md—— 改动触及 sim/ 任何后端时,必须同步在那里登记一行;改动前先看 docs/测试说明/模拟器差距清单.md §3 判断是否必须回真机。