Files
STM32F4-Base/simulator/main.c
T
wandering1 201d713226 feat(ui-data): ui_data 三通道快照 + ui_data_feed 装配 + SOE 显示名映射
按 docs/UI设计/UI_数据供给设计说明.md 的 D 方案把 App→UI 数据通道落地:
App 组帧、BSP 存帧、页面只读;UI 侧零 App include,薄包装只在两端适配层。
本笔是纯增层、不接任何页面 → 零像素变化(菜单 8 状态 + 内容页 11 页黄金
参考逐字节不变,simulator/tests/ref 无 diff,未刷基线)。

新增
- Drivers/BSP/UI/ui_data.{c,h}:实时量 rt / 参数 par / 装置信息 info 三通道存储
  + 发布序号盖章 + 只读访问。零 App include、零 LVGL include(分层叶子);
  读写同在 uiTask 上下文串行 → 无锁、无双缓冲、无 seqlock(契约 §5.1)。
- App/app/ui_data_feed.{c,h}:快照唯一填充者。rt/info 每 200 ms 一帧,par 只在
  prot_set_checksum() 变化时重发;首轮立即发布,免启动初期闪一下 "--"。
- Drivers/BSP/UI/ui_w_soe.{c,h}:SOE 二进制记录的显示名映射与注入式 reader
  契约。未标定闭锁=7 / 标定复归=8 已进表——编号按 App/log/soe_rec.h 与
  App/algo/prot_elm.h 现场数过(类型 1..8 + 哨兵 9、元件 0..8、ELM_NONE=0xFF、
  FLG_IV=0x80),不采信文档与消息转述。reader 备好但两端未绑定:落盘侧还
  没有只读 API(Lead 已裁该 API 归下一工作项),未注入按"无记录"空态渲染。

量纲诚实性(契约 §4.1 同步改成真值口径,不再抄旧码值表述)
- eng 不写"0 = 原始码值":标定层 ROM 系数是引脚侧 LSB,采样环折算出来的就是
  ADC 引脚电压(承诺边界 docs/任务规划/数据可信.md §2.6);电气 8 路
  adc_cal_flags() 的 VALID 全置位才升 eng=1 二次值——整帧共用一个 eng,只要
  还有一路是引脚电压就不能贴"二次值"标签,宁可整体保守。
- TEMP 不参与线性标定 → 恒带新增品质位 UI_DATA_Q_RAW=0x20,页面不挂单位。
- UB/UC/IB/IC/IN 无基波相量实例(V0.1 削顶)→ Q_IV|Q_OV;未满一周波 → Q_NT。
- 谐波另开 ph[] 相量实例槽带:yc 的语义通道下标容不下同通道的二次/五次谐波。

接线与双端注册
- App/task/ui_task.c:ui_menu_init 之前 ui_data_init + 首帧发布;主循环每轮
  ui_data_feed_tick() 排在 ui_port_step 之前(本轮渲染用本轮刚发布的帧)。
- simulator/main.c:发布固定合成帧(无头与窗口两个初始化点,先于建页);
  定值不手抄 40 个数,改为链入 App/algo/prot_set.c 取出厂默认(抄表等于造
  第二真相);build_date 用固定串而非 __DATE__,否则跨天重建会把装置信息页
  黄金参考刷掉,而黄金参考是判据本身。
- 注册:uvprojx 加 3 条(App 组 ui_data_feed.c + UI 组 ui_data.c/ui_w_soe.c)与
  simulator/CMakeLists.txt;ui_data_feed.c 不进模拟器 → check_registration.py 的
  SIM_EXEMPT 加一行(App/app/app_main.c 先例,已报备)。

主机单测与钩子
- test/host/ui_data:82 断言全绿。钉三类事:快照语义(首帧前读 -1、序号由本层
  盖章、三通道互不串台、NULL 防御、init 幂等复位);跨层不透明下标现场一致
  (yc 序 = adc_semantic_t、q10 序 = prot_set_id_t,同一 TU 里 include 两侧头);
  SOE 编号与名字表现场一致(八类事件都有名、采样/标定四个词互不相同、
  名长不超列宽、越界回占位)。
- .pre-commit-config.yaml 加 ui-data-unit-test 钩子(第 43 个,Lead 已批自加)。
- scripts/id_registry.json 建族 TC-HOST-UIDATA(属主登记处 = 单测目录,同
  TC-HOST-ADCCAL 先例;不建族则新号被编号门禁判"未登记前缀")。

体积(相对 HEAD 台账 Code 215744 / RO 21396 / RW 1224 / ZI 131984)
- Code +1272 B、RO +108 B(字库 4 字,见上一笔)、RW +24 B、ZI +696 B
  → ER_IROM1 238520 B(22.7%)、RW_IRAM1 76504 B(58.4%,台账 57.8%)。
- .map 逐项:ui_data.o 488 B(Code 184/RW 12/ZI 476)、ui_data_feed.o 221 B
  (1034/12/209);LVGL 池 0 增量;ui_w_soe 未接线不进镜像(.map 无其符号)。
- 台架未跑(本笔零像素、零接线):首帧时刻、200 ms 刷新、eng 升档与遥信
  位图真值全部留"待台架实测",接线完成后请 Lead 排窗。

遗留与风险(随回报进台账)
- 写路径(契约 §6 的 param_request/state)不落:无消费者就是永不执行的死码,
  随整定 4 页一起落;届时须先定"UI 整定 vs iec104Task 写 prot_set"的并发口径
  (prot_set_set_sealed 的影子区非重入)。
- 公共地址 CA 未下发:固件里 CA 逐帧从 ASDU 头解析,无集中常量可取。
- 遥信位图与 App/protocol/iec104/iec104_pts.c 板端源是两处镜像(该文件位号
  写在函数体内未导出,UI 不得 include 协议头),开入采集落地时应并成单源。
- 版本类字段是本文件内的宏(仓库无产品级版本登记表,值沿用页面既有显示
  值);软件校验位在拿到构建期 CRC 步骤前一律显示 "--",不抄参考遗留值。

gate: 全量钩子(--all-files)40 项通过;cppcheck/test-cppcheck/coding-standard/
layer-deps(基线仍 3 条)/registration/bsp-err(存量 38 条不变)/font-charset/
id-check(增量无新账)/doc-links/ui-golden/主机单测与覆盖率全绿;唯一红项是
mem-numbers 与 mem-budget——两者只因 Flash/RAM 台账未回填(禁区,归 Lead)。

gate: only mem-budget/mem-numbers pending lead ledger backfill

自查:BOM(Drivers/BSP/UI/* 与 App/task/ui_task.c 保持 EF BB BF,新建 UI 两件
同规范;App/app 与 test/host 无 BOM 与本目录一致);CR = 0 全 LF;U+FFFD = 0;
字符串字面量纯 ASCII(中文只进注释)。
2026-10-25 09:56:22 +08:00

774 lines
31 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 模块名称:PC 模拟器入口(simulator/main.c)
* 模块功能:在 PC 上以 SDL2 跑**与固件同一套** UI 源码,提供窗口调试与多种无头自检模式
* 适用平台:Windows(CMake + SDL2)
* 作者:王建锋
* 创建日期:2026-08-31
* 修改记录:
* v1.0 2026-08-31 王建锋 创建:基于 lv_port_pc_vscode 适配
*
* @file main.c
* @brief LVGL 模拟器入口 — 160x160 1bpp 单色菜单 UI
* @note 基于 lv_port_pc_vscode 适配,用于 PC 上 UI 开发与调试
*
* 键盘映射:
* ↑/↓/←/→ → 方向键导航
* Enter → 确认/进入
* Escape → 返回
*
* 截图模式(无窗口离屏渲染,导出像素供比对):
* lvgl_sim.exe --screenshot shot.bin
* 走与 MCU 端一致的离屏软件渲染(ARGB8888 缓冲 + mono 主题),
* 渲染首帧后将 160x160 像素按 BGRA 顺序写入 shot.bin(102400 字节),随即退出。
*
* 键序截图模式(无窗口 + 注入按键序列后截图,用于级联菜单无头回归):
* 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(只读 INI;旧 NCFG 档迁移已于 2026-10-10 删除)
* → 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 <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#ifdef _MSC_VER
#include <Windows.h>
#include <winsock2.h>
#else
#include <unistd.h>
#endif
#define SDL_MAIN_HANDLED
#include <SDL.h>
#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"
#include "ui_w_records.h" /* task-6 记录 reader 注入点(镜像 App/task/ui_task.c 的绑定) */
#include "ui_data.h" /* task-14 快照存侧:模拟器在此直接发布固定帧(装配器不进模拟器) */
#include "prot_set.h" /* task-14 定值出厂默认(App/algo 纯算法,双端可编,见 simulator/CMakeLists) */
/* 屏幕尺寸(与 MCU 端一致) */
#define SIM_SCREEN_W 160
#define SIM_SCREEN_H 160
/* 离屏截图缓冲:I1(1bpp,与 MCU/模拟器窗口一致;软件渲染器仅支持 I1),160x160/8 */
static uint8_t g_shot_buf[SIM_SCREEN_W * SIM_SCREEN_H / 8];
/*
* 函数功能:离屏截图用的空 delay 回调
* 入口参数:ms - 请求延时的毫秒数(未使用) uint32_t
* 返回值:无
* 说明:默认 lv_delay_ms 会忙等 tick 前进,但离屏渲染时 tick 不会在等待期间推进,
* 导致死循环挂死。window 模式经 SDL 设了真实 delay,离屏这里用空实现代替。
*/
static void shot_delay_cb(uint32_t ms) {
(void)ms;
}
/*
* 函数功能:离屏截图 flush 回调(不写真实设备,直接标记就绪)
* 入口参数:disp - LVGL 显示对象 lv_display_t *
* area - 刷新区域(未使用) const lv_area_t *
* color_p - 像素缓冲(未使用) uint8_t *
* 返回值:无
*/
static void shot_flush_cb(lv_display_t * disp, const lv_area_t * area, uint8_t * color_p) {
(void)area;
(void)color_p;
lv_display_flush_ready(disp);
}
/*
* 函数功能:键序列字符 → LVGL 键值映射(--keys 无头回归用)
* 入口参数:c - 键序列字符(u=↑ d=↓ l=← r=→ e=确认 s=返回) char
* 返回值:LVGL 键值;非键字符返回 0 = 忽略 uint32_t
*/
static uint32_t sim_map_key_char(char c) {
switch (c) {
case 'u': return LV_KEY_UP;
case 'd': return LV_KEY_DOWN;
case 'l': return LV_KEY_LEFT;
case 'r': return LV_KEY_RIGHT;
case 'e': return LV_KEY_ENTER;
case 's': return LV_KEY_ESC;
default: return 0;
}
}
/*
* 函数功能:待机屏底部时钟提供回调(平台差异:模拟器 = 宿主 time())
* 入口参数:buf - 输出缓冲(格式化时间串) char *
* len - 缓冲长度 size_t
* 返回值:无
*/
static void sim_time_cb(char *buf, size_t len) {
time_t t = time(NULL);
struct tm tmv = {0}; /* 显式清零:localtime_s/r 是"输出型"参数,静态分析按未初始化报 */
#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);
}
/*
* 函数功能:记录 reader 适配(task-6 注入,镜像 ui_task.c)——UI 编号 → syslog_id_t
* 入口参数:log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
* 返回值:记录条数;未知编号返回 0(页面空态) uint16_t
*/
static uint16_t sim_rec_count(uint8_t log_id) {
if (log_id == UI_W_REC_LOG_RUN) {
return (uint16_t)syslog_count(SYSLOG_RUN);
}
if (log_id == UI_W_REC_LOG_SELFTEST) {
return (uint16_t)syslog_count(SYSLOG_SELFTEST);
}
return 0u;
}
/*
* 函数功能:记录 reader 适配(task-6 注入)——读记录字段直通 sys_log(错误码不改写)
* 入口参数:log_id/idx/ts/ts_len/desc/desc_len - 契约见 ui_w_records.h ui_rec_reader_t
* 返回值:0 成功;-1 编号/序号越界;-2 读失败 int
*/
static int sim_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len,
char *desc, uint32_t desc_len) {
if (log_id == UI_W_REC_LOG_RUN) {
return syslog_get_record(SYSLOG_RUN, idx, ts, (size_t)ts_len, desc, (size_t)desc_len);
}
if (log_id == UI_W_REC_LOG_SELFTEST) {
return syslog_get_record(SYSLOG_SELFTEST, idx, ts, (size_t)ts_len, desc, (size_t)desc_len);
}
return -1;
}
static const ui_rec_reader_t s_rec_reader = { sim_rec_count, sim_rec_get };
/*
* 函数功能:发布固定数据帧(镜像 App/app/ui_data_feed.c 的装配结果)
* 入口参数:无
* 返回值:无
* 限定条件:ui_menu_init 之前调用(无头与窗口两个初始化点共用,先于建页)
* 函数说明:模拟器不链接装配器(无硬件数据源;真值还会让像素参考天天变),
* 这里发布一帧**固定合成值**,页面数据来源因此可注入——像素回归不再
* 依赖真实硬件(UI_数据供给设计说明.md §9.2)。要点三条:
* 1. eng 固定 = UI_DATA_ENG_PIN,与真机未标定态同口径(数据可信.md
* §2.6 承诺边界 = 引脚电压),页面据此显示"未标定";
* 2. build_date 用固定串而**不是** __DATE__——否则跨天重建会把装置
* 信息页的黄金参考刷掉(黄金参考是判据本身,不得随手刷);
* 3. 定值取 prot_set 出厂默认(App/algo/prot_set.c 是纯算法,双端可编),
* 不手抄 40 个数字——抄一次就是一份会漂移的第二真相
*/
static void sim_publish_data_frames(void) {
ui_data_rt_t rt;
ui_data_par_t par;
ui_data_info_t info;
uint8_t i;
ui_data_init();
(void)memset(&rt, 0, sizeof(rt));
rt.valid = 1u;
rt.eng = UI_DATA_ENG_PIN;
rt.tick_ms = 0u;
/* 遥测下标 = adc_semantic_t 语义序(UA/UB/UC/UN/IA/IB/IC/IN/TEMP)。
* 模拟器不 include adc_input.h,故这里用语义序注释对照,不写符号名 */
rt.yc[0].val = 0.8123f; /* UA 基波(引脚电压域) */
rt.yc[0].qds = 0u;
rt.yc[1].val = 0.0f; /* UB:V0.1 硬件削顶无采样源 */
rt.yc[1].qds = UI_DATA_Q_IV | UI_DATA_Q_OV;
rt.yc[2].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* UC 同上 */
rt.yc[3].val = 0.0054f; /* UN 零序电压基波 */
rt.yc[3].qds = 0u;
rt.yc[4].val = 0.0456f; /* IA 基波 */
rt.yc[4].qds = 0u;
rt.yc[5].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IB */
rt.yc[6].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IC */
rt.yc[7].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IN */
rt.yc[8].val = 2048.0f; /* TEMP:码值(不参与线性标定) */
rt.yc[8].qds = UI_DATA_Q_RAW;
/* 相量实例槽(序 = phasor k_inst[]:IA-F1/UA-F1/UN-F1/IA-F2/IA-F5) */
rt.ph_n = 5u;
rt.ph[0].val = 0.0456f;
rt.ph[1].val = 0.8123f;
rt.ph[2].val = 0.0054f;
rt.ph[3].val = 0.0017f; /* IA 二次谐波(涌流制动监视量) */
rt.ph[4].val = 0.0008f; /* IA 五次谐波(选线用) */
for (i = 0u; (i < UI_DATA_PH_MAX) && (i < rt.ph_n); i++) {
rt.ph[i].qds = 0u;
}
for (; i < UI_DATA_PH_MAX; i++) {
rt.ph[i].qds = UI_DATA_Q_IV; /* 未发布的槽:无效,页面显示 "--" */
}
rt.yx_bits = 0u; /* 固定帧无动作态 */
rt.yx_valid = 0x4043C000ULL; /* YX15~18/23/31 有效位图 */
rt.freq_valid = 0u; /* eng=0 期间幅值门恒失效 → 无新鲜读数 */
rt.freq_hz = 0.0f;
rt.clock_ok = 1u;
(void)snprintf(rt.clock, sizeof(rt.clock), "%s", "2026-10-25 08:00:00");
(void)ui_data_publish_rt(&rt);
(void)memset(&par, 0, sizeof(par));
(void)prot_set_init(); /* 出厂默认 + 封存(幂等) */
par.valid = 1u;
par.checksum = prot_set_checksum();
par.locked = prot_set_locked();
par.bad_mask = 0u; /* 装载成功:无越界项 */
for (i = 0u; i < UI_DATA_SET_N; i++) {
par.q10[i] = prot_set_get((prot_set_id_t)i);
}
(void)ui_data_publish_par(&par);
(void)memset(&info, 0, sizeof(info));
info.valid = 1u;
info.uptime_s = 0u;
info.port_104 = 2404u; /* App/task/iec104_serv.h 的 IEC104_SERV_PORT */
info.port_disp = (uint16_t)UI_NET_SINK_PORT; /* 同源常量(BSP 头,模拟器可见) */
(void)snprintf(info.model, sizeof(info.model), "%s", "F30");
(void)snprintf(info.sw_ver, sizeof(info.sw_ver), "%s", "SV0.010");
(void)snprintf(info.hw_ver, sizeof(info.hw_ver), "%s", "HW0.010");
(void)snprintf(info.build_date, sizeof(info.build_date), "%s", "Oct 25 2026");
(void)snprintf(info.sw_sum, sizeof(info.sw_sum), "%s", "--");
(void)snprintf(info.ftp_user, sizeof(info.ftp_user), "%s", "admin");
(void)ui_data_publish_info(&info);
}
/*
* 函数功能:无窗口离屏渲染(可注入键序列)并导出末帧像素
* 入口参数:keys - 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键) const char *
* path - 输出文件(写入 160x160 I1 原始像素) const char *
* standby - 非 0 = 注键后强制进入待机屏再截图(--standby 模式) int
* 返回值:0 - 成功;非 0 - 失败 int
*/
static int run_headless(const char * keys, const char * path, int standby) {
setvbuf(stdout, NULL, _IONBF, 0);
lv_init();
lv_delay_set_cb(shot_delay_cb);
lv_display_t *disp = lv_display_create(SIM_SCREEN_W, SIM_SCREEN_H);
if (disp == NULL) {
return 1;
}
lv_display_set_default(disp);
lv_display_set_color_format(disp, LV_COLOR_FORMAT_I1);
lv_display_set_buffers(disp, g_shot_buf, NULL, sizeof(g_shot_buf),
LV_DISPLAY_RENDER_MODE_FULL);
lv_display_set_flush_cb(disp, shot_flush_cb);
lv_group_t *g = lv_group_create();
if (g == NULL) {
return 1;
}
lv_group_set_default(g);
#if LV_USE_THEME_MONO
lv_theme_t *th = lv_theme_mono_init(disp, true, LV_FONT_DEFAULT);
if (th != NULL) {
lv_display_set_theme(disp, th);
}
#endif
/* 配置 RAM 副本:镜像 netTask 上电流程(net_cfg_load → 工厂默认),
* 使网口设置内容页显示与真机一致的工厂默认值 */
(void)net_cfg_load();
/* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */
ui_w_records_bind(&s_rec_reader);
/* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页,
* 页面任何时刻打开都拿得到帧,不必等装配节拍) */
sim_publish_data_frames();
if (ui_menu_init() != 0) {
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 全屏刷新会进渲染死循环 */
for (const char *p = keys; *p != '\0'; p++) {
uint32_t key = sim_map_key_char(*p);
if (key == 0u) {
continue;
}
lv_group_send_data(g, key);
}
/* 执行待处理"保存网口配置"请求:无头模式不跑主循环(帧驱动被禁用),
* 此处同步完成保存交接,保持与窗口模式/真机一致的终态(WAIT → OK/ERR) */
while (net_cfg_save_state() == (int)NET_CFG_SAVE_ST_WAIT) {
net_cfg_save_run();
}
/* --standby:注键后强制进入待机屏(截图前) */
if (standby != 0) {
ui_standby_force_enter();
}
/* 注键后 LVGL 池占用(无头模式;内存回归用,对照 ui_menu_init 基线) */
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
printf("pool after keys: used=%u free=%u\n",
(unsigned)(mon.total_size - mon.free_size), (unsigned)mon.free_size);
/* 用 snapshot 把整屏渲进独立 I1 draw_buf(避开 I1+FULL 的渲染死循环) */
lv_draw_buf_t *snap = lv_snapshot_take(lv_screen_active(), LV_COLOR_FORMAT_I1);
if (snap == NULL) {
fprintf(stderr, "[shot] snapshot failed\n");
return 1;
}
uint32_t snap_w = snap->header.w;
uint32_t snap_h = snap->header.h;
/* I1 缓冲区布局:[palette 8B][pixel data],跳过 palette 只写像素 */
uint32_t pal_bytes = LV_COLOR_INDEXED_PALETTE_SIZE(LV_COLOR_FORMAT_I1) * (uint32_t)sizeof(lv_color32_t);
const uint8_t * px = snap->data + pal_bytes; /* 只读:写文件时不再改动快照 */
uint32_t px_bytes = (uint32_t)snap->header.stride * (uint32_t)snap_h;
FILE *fp = fopen(path, "wb");
if (fp == NULL) {
lv_draw_buf_destroy(snap);
fprintf(stderr, "fopen failed: %s\n", path);
return 1;
}
size_t n = fwrite(px, 1, px_bytes, fp);
fclose(fp);
lv_draw_buf_destroy(snap);
printf("screenshot: %s (%zu bytes, %ux%u I1)\n", path, n, snap_w, snap_h);
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 };
/*
* 函数功能:配置轮询自检(无窗口、无需 LVGL/SDL,进程内闭环)
* 入口参数:无
* 返回值:0 - PASS;非 0 - FAIL int
* 说明:验证链 net_cfg_load(只读 INI)→ poll 建基线 → 伪造外部编辑
* → poll 两次(沉降去抖)→ 重载并更新 RAM 副本
*/
static int run_cfgtest_scenarios(void) {
int i;
const net_cfg_t *p_cfg;
setvbuf(stdout, NULL, _IONBF, 0);
(void)net_cfg_load(); /* 加载 INI(含 [network] 段注册;旧 NCFG 档不再读取) */
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");
/* E-01 场景 1:伪造超长文件(> CFG_FILE_MAX_LEN)→ poll 必须判损坏
* (与 load/MCU 同一契约):RAM 回工厂默认 + 文件重建为出厂模板,
* 而非截断解析残留 10.0.0.1(修复前死码走的是截断分支) */
{
uint8_t big[CFG_FILE_MAX_LEN + 64u];
cfg_file_stat_t st_chk;
(void)memset(big, ';', sizeof(big)); /* 纯注释:内容无关,尺寸超即触发 */
if (cfg_store_write_atomic(cfg_store_config_path(), big, sizeof(big)) != 0) {
printf("[cfgtest] FAIL forge oversized write\n");
return 1;
}
cfg_file_poll(); /* 检出变更(沉降计数 1) */
cfg_file_poll(); /* 沉降完成 → 损坏分支:工厂默认 + 重建模板 */
if (cfg_store_stat(cfg_store_config_path(), &st_chk) != 0 ||
st_chk.size == 0u || st_chk.size > CFG_FILE_MAX_LEN) {
printf("[cfgtest] FAIL oversized file not rebuilt (size=%lu)\n",
(unsigned long)st_chk.size);
return 1;
}
p_cfg = net_cfg_get();
if (p_cfg->ip[0] != 192u || p_cfg->ip[1] != 168u ||
p_cfg->ip[2] != 1u || p_cfg->ip[3] != 100u) {
printf("[cfgtest] FAIL ip not factory after oversized: %u.%u.%u.%u\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 oversized file rebuilt to factory template\n");
/* E-01 场景 2:save 拒写"雷"文件——伪造合法界内纯注释文件(4000B,无
* [network] 段),外科手术保存文末补写受管段 → 输出必然超
* CFG_FILE_MAX_LEN:必须拒绝(net_cfg_save 返 -1)且原文件不动;
* 修复前此处会成功写出 ~4200B 超长文件 = 下次开机被销毁的雷 */
{
uint8_t pad[4000u];
cfg_file_stat_t st_chk;
(void)memset(pad, ';', sizeof(pad));
if (cfg_store_write_atomic(cfg_store_config_path(), pad, sizeof(pad)) != 0) {
printf("[cfgtest] FAIL forge padding write\n");
return 1;
}
if (net_cfg_save() == 0) {
printf("[cfgtest] FAIL oversized document was written (landmine)\n");
return 1;
}
(void)cfg_store_stat(cfg_store_config_path(), &st_chk);
if (st_chk.size != 4000u) {
printf("[cfgtest] FAIL file modified by rejected save (size=%lu)\n",
(unsigned long)st_chk.size);
return 1;
}
}
printf("[cfgtest] OK save rejected oversized document (file untouched)\n");
printf("[cfgtest] PASS\n");
return 0;
}
/*
* 函数功能:配置自检外壳——保证不在工作目录留下伪造的配置档
* 入口参数:无
* 返回值:run_cfgtest_scenarios 的判定 int
* 说明:E-01 场景 2 会写一个 4000 B 纯注释"雷"文件,并断言"拒写后原文件不动"
* (size == 4000)——**因此连正常通过路径也会把它留在原地**。该文件是
* gitignore 的本地产物,但之后每一次 UI 回归都会加载它:外科手术保存要在
* 4000 B 注释后补写受管段,输出必然超 CFG_FILE_MAX_LEN → 引擎按设计拒写
* → 网口设置页保存行显示 ERR → 黄金参考(工厂默认态)比对失败
* (2026-10-15 实测踩中)。故无论成败一律删除,让配置路径回到
* "无文件 = 工厂默认"这一确定态;回归脚本另以空工作目录运行做双保险
* → docs/UI设计/UI_自动化测试流程.md §三
*/
static int run_cfgtest(void) {
int rc = run_cfgtest_scenarios();
(void)cfg_store_remove(cfg_store_config_path());
(void)cfg_store_remove(cfg_store_tmp_path());
return rc;
}
/*
* 函数功能:日志引擎自检(无窗口、进程内闭环)
* 入口参数:无
* 返回值:0 - PASS;非 0 - FAIL int
* 说明:验证链 清理旧档 → post N 条 → count/get 回读断言 + 宿主文件字节级校验
* (64B 定长、ISO 时间戳格式、epoch 字段 ≈ now、NUL 填充)
*/
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, "power on");
syslog_post(SYSLOG_SELFTEST, "clock chip fault");
syslog_post(SYSLOG_SELFTEST, "NAND fault");
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, "power on") != 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 fault") != 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("power on"); 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;
}
/**
* 函数功能:模拟器入口(解析模式开关 → 窗口模式或无头自检)
* 入口参数:argc - 参数个数 int
* argv - 参数数组(--screenshot / --keys / --standby / --cfgtest /
* --logtest / --idle) char **
* 返回值:0 - 正常退出或自检 PASS;非 0 - 失败 int
*/
int main(int argc, char **argv) {
/* 截图/自检模式:--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], 0);
}
if (strcmp(argv[i], "--keys") == 0 && (i + 2) < argc) {
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 使用) */
#ifdef _MSC_VER
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#endif
/* 初始化 SDL2(仅 timer + events,视频由 lv_sdl_window 内部初始化) */
if (SDL_Init(SDL_INIT_TIMER | SDL_INIT_EVENTS) != 0) {
fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError());
return 1;
}
/* 初始化 LVGL */
lv_init();
/* 初始化模拟器端口:创建 SDL 窗口 + 键盘输入 */
if (sim_port_init(SIM_SCREEN_W, SIM_SCREEN_H) != 0) {
fprintf(stderr, "sim_port_init failed\n");
SDL_Quit();
return 1;
}
/* 设置 SDL 窗口标题 */
lv_display_t *disp = lv_display_get_default();
if (disp != NULL) {
lv_sdl_window_set_title(disp, "STM32F4-Base UI Simulator (160x160 1bpp)");
}
/* 配置 RAM 副本:镜像 netTask 上电流程(net_cfg_load → 工厂默认) */
(void)net_cfg_load();
/* 构建菜单 UI(与 MCU 端共用 ui_menu.c) */
/* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */
ui_w_records_bind(&s_rec_reader);
/* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页,
* 页面任何时刻打开都拿得到帧,不必等装配节拍) */
sim_publish_data_frames();
if (ui_menu_init() != 0) {
fprintf(stderr, "ui_menu_init failed\n");
SDL_Quit();
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");
} else {
fprintf(stderr, "ui_net_sink_start failed (port 7003)\n");
}
printf("=== LVGL Simulator Started ===\n");
printf("Screen: %dx%d 1bpp\n", SIM_SCREEN_W, SIM_SCREEN_H);
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)统一轮询分发,
* 本循环不得再调 SDL_PollEvent,否则会先消费事件队列,
* 导致键盘输入永远到不了 LVGL */
while (1) {
/* 轮询 RemoDispBus 协议(非阻塞收发) */
ui_net_sink_poll();
/* 执行待处理"保存网口配置"请求(单线程主循环 = 配置引擎属主;
* 无请求 = 空操作);"立即应用"回调未注册 → 空操作 */
net_cfg_save_run();
/* 配置文件轮询(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) {
sleep_ms = LV_DEF_REFR_PERIOD;
}
/* 强制整屏刷新(1bpp SDL PARTIAL 模式下保证首帧输出) */
lv_refr_now(NULL);
#ifdef _MSC_VER
Sleep(sleep_ms);
#else
usleep(sleep_ms * 1000);
#endif
}
return 0;
}