245 lines
7.9 KiB
C
245 lines
7.9 KiB
C
/**
|
||
* @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 帧后取快照
|
||
*/
|
||
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#ifdef _MSC_VER
|
||
#include <Windows.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 "net_cfg.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 无窗口离屏渲染(可注入键序列)并导出末帧像素
|
||
* @param keys 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键)
|
||
* @param path 输出文件(写入 160x160 I1 原始像素)
|
||
* @return 0 成功,非 0 失败
|
||
*/
|
||
static int run_headless(const char * keys, const char * path)
|
||
{
|
||
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;
|
||
}
|
||
|
||
/* 键注入:与 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);
|
||
}
|
||
|
||
/* 注键后 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;
|
||
}
|
||
|
||
/**
|
||
* @brief 程序入口
|
||
*/
|
||
int main(int argc, char **argv)
|
||
{
|
||
/* 截图模式:lvgl_sim.exe --screenshot <path> */
|
||
for (int i = 1; i < argc; i++) {
|
||
if (strcmp(argv[i], "--screenshot") == 0 && (i + 1) < argc) {
|
||
return run_headless("", argv[i + 1]);
|
||
}
|
||
if (strcmp(argv[i], "--keys") == 0 && (i + 2) < argc) {
|
||
return run_headless(argv[i + 1], argv[i + 2]);
|
||
}
|
||
}
|
||
|
||
/* 初始化 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;
|
||
}
|
||
|
||
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");
|
||
|
||
/* 主循环:驱动 LVGL 定时器与刷新
|
||
* 注意:SDL 事件(键盘/窗口/退出)由 LVGL SDL 驱动内部 5ms 定时器
|
||
* (lv_sdl_window.c 的 sdl_event_handler)统一轮询分发,
|
||
* 本循环不得再调 SDL_PollEvent,否则会先消费事件队列,
|
||
* 导致键盘输入永远到不了 LVGL */
|
||
while (1) {
|
||
/* 驱动 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;
|
||
}
|