/** * @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 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 副本已按新文件重载 * * 日志引擎自检模式(无窗口、进程内闭环、退出码 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 #include #else #include #endif #define SDL_MAIN_HANDLED #include #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 #define SIM_SCREEN_H 160 /* 离屏截图缓冲:I1(1bpp,与 MCU/模拟器窗口一致;软件渲染器仅支持 I1),160x160/8 */ static uint8_t g_shot_buf[SIM_SCREEN_W * SIM_SCREEN_H / 8]; /* * @brief 离屏截图用空 delay 回调 * @note 默认 lv_delay_ms 会忙等 tick 前进,但离屏渲染时 tick 不会在等待期间推进, * 导致死循环挂死。window 模式经 SDL 设了真实 delay,离屏这里用空实现代替。 */ static void shot_delay_cb(uint32_t ms) { (void)ms; } /* * @brief 离屏截图 flush 回调(不写真实设备,直接标记就绪) */ 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 键值(非键字符返回 0 = 忽略) */ 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; } } /* * @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, 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(); 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); 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 }; /* * @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"); /* 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; } /* * @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, "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; } /** * @brief 程序入口 */ 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) */ 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; }