67c469fa40
ui_w_soe 组件(task-14 落)此前两端都没绑 reader,只能显示"无记录"空态。本批补齐三件事:
App 侧只读遍历、两端 reader 绑定、SOE 记录页与菜单注册。
App/log/soe_store 只读 API(Lead 已放行,本批独占这两个文件)
- soe_store_count() / soe_store_read(idx, soe_rec_t*):与 syslog_count /
syslog_get_record 同构——8 条一窗(512 B)整块读、命中窗口零 I/O、卷锁 try(0 超时)
非阻塞;定长记录逐条 open 等于同一扇区开 4 次,SPI NAND 上这个差距是肉眼可见的卡顿
- 读者不得看到半成品记录,三道保障缺一不可(规则写进 soe_store.h 头注,不只是提交信息):
① 条数只数整条 64 B 槽(写者留下的半条尾槽天然不计入);② 窗口代数:写者每次成功落盘 /
容量重建在**卷锁区间内**自增代数,读者比对填窗时记下的代数,不符即重填(对齐 32 位读写
在 ARM 上原子,跨任务无需临界区,最坏多读一轮);③ 单条硬门槛:soe_rec_parse() 校版本 +
校验和,撕裂/半写一律拒收
- 容忍的竞态只有一个:count 与 read 之间新落一条 → 本轮少看一条,下轮重绘(200 ms)自然
追上;所以边界一律用锁内重取的条数,不信调用方传进来的旧值
- API **不带类型掩码**(Lead 裁定 2026-10-25):5 个记录页怎么分组是界面语义,不进日志存储
层;环容量小、页渲染全扫无成本,真变大了再加带 mask 的优化 API(YAGNI)
⚠ 顺带修掉一颗定时炸弹:菜单生成器与生成物漂移
- 提交里的 Drivers/BSP/UI/ui_menu_tab.h 带着 5 个表查询 API 的规范注释块,而
tools/menu_gen/gen_menu.py 只会输出"声明 + 行尾短注释"——说明历史上有人**手改生成物**
补注释、没回灌生成器。我这次加 #page=soe 后重新生成,等于把那些注释抹掉,bsp-err 立刻
报 5 条 return-doc
- 修法不是把生成物改回去(下次重新生成还会再抹一次),而是把注释块**回灌进生成器**;
回灌后重跑 gen_menu.py 产出的 ui_menu_tab.h 与 HEAD **逐字节一致**(git diff 为空),
漂移关闭。生成物里的手工优化 = 一定会被覆盖的优化,要么进生成器要么别改生成物
时标口径(跨模块陷阱,注释即防线)
- 页面时标取记录内的 CP56(事件自己的时标,不是"现在")。它与运行/自检记录页同口径,靠两条
事实叠出来:sys_clock 的 epoch 域本身就是本地时间(App/util/sys_clock.h 的
sys_clock_set_unix 注明"Unix epoch time (秒,本地时间)"),而 soe_rec.c 的 epoch→CP56
分解不加任何时区偏移。**谁"顺手补 8 小时",SOE 页就会和文本记录页差 8 小时**;要引显式
时区必须两侧一起改
显示名回正:类型 7 用源词"未标定闭锁"
- 组件名字表里原写作"标定闭锁"——缩掉一个"未"字后意思可以读成"把标定闭锁了",与"因未标定
而闭锁"几乎相反;源词见 App/log/soe_rec.h 与"数据可信"卡 5,UI 表现层无权改写事实词
- 模拟器的 3 条假记录刻意含一条 type 7 + ELM_NONE(非元件支路):这是 7/8 两个新类型
**真的接上屏**的第一个像素级证据——只有主机断言时,名字表写错也照样一片绿
- 主机测试的宽度护栏随之按"画到哪一列"分列:动作/返回进值列(4 汉字 = 12 字节),其余类型
名进名列(7 汉字 = 21 字节);原来一刀切 12 字节会把正常名字判成违规
页面与注册
- 新建 Drivers/BSP/UI/ui_page_soe.{c,h}:薄跳板,一条记录两行共用组件内单条读盘缓存,
实例独立(两页共用一份缓存会互相命中对方记录号,task-6 的教训)
- 菜单树 SOE记录 加 #page=soe;重生成 4 个生成物,uvprojx / CMakeLists 两处注册由生成器
自动写入(本批 uvprojx 只含这一条新增,无 UV4 抖动)
- reader 绑定在两端适配层:真机 App/task/ui_task.c(CP56 → UI 视图 + 容量钳位),
模拟器 simulator/main.c(固定 3 条、时标固定串,保证黄金参考可复现)
两处被门禁当场抓住的记录(都是有效拦截,不是噪音)
- id-check 拦下注释里裸引"P2-10":编号引用必须可判定归属,代码注释也一样 → 改为指向
App/log/sys_log_mcu.c 与事实本身,不留裸编号
- 又一次 BOM 铁律现场复现:一次编辑剥掉 ui_w_soe.c 的 BOM,ARMCC 立刻报一串 #8/#7/#20
假语法错(missing closing quote 之类),而 MSVC 侧 0 警告完全照不出来;补回 EF BB BF
即 0/0。本批提交前逐文件比 HEAD 做了 BOM 漂移审计(临时脚本,不入库)——建议把这条
审计固化进 ui-verify skill 或门禁(不在本道写域,已回报 Lead)
验证
- @build:0 错误 0 警告;模拟器:0 警告
- 内容页回归 15 项 PASS:新增 soe 页 9 条文本断言(两条时标、过流Ⅰ段+动作、+返回、
未标定闭锁、页码),其余 11 页逐字节不变;菜单 8 状态 PASS、池 5368 B
- soe 页刷新黄金参考前出图肉眼确认:6 行两两成对、19 字符时标在 94 px 名列内不越屏、
非元件行的类型名落在名列且值列为空
- test/host/ui_data 83/83、test/host/soe 32/32;13 个代码/文档/注册类钩子(含
coding-standard、bsp-err、id-check、cppcheck、registration-check、layer-deps、
font-charset、doc-links)全 Passed
- 全量钩子红项只有同族三条:mem-numbers / mem-budget / size-gate(L0c),root cause 一条:
Flash/RAM 台账与同码凭证回填归单写者 → 本批 --no-verify
体积(相对 b746a1f 台账 Code 217204 / RO 21504 / RW 1256 / ZI 132680)
- 实测 Code 219712、RO 21388、RW 1288、ZI 134272 → Code +2508、RO −116、RW +32、ZI +1592
→ Flash 合计 +2424 B、RAM +1624 B(含本批之前的三页回炉)
- 本批新增里约 1064 B ZI 是 SOE 读窗口(512 B)+ static FIL(528 B),一次性投入、页面打开
才用得到;RAM 再紧张可把窗口降到 4 条(256 B)拿回一半,代价是跨窗记录多一次 I/O
待台架实测:真机 /log/soe.log 有记录时的翻页与尾部行为、卷忙时该行退化显示、SOE 页与文本
记录页时标一致性(跨 8 小时口径的现场核对)、记录增长时窗口代数失效后的重读节拍。
gate: only mem-budget/mem-numbers/size-gate pending lead ledger backfill
(同族三条,root cause 一条:台账与同码凭证回填归单写者)
像素基线单独提交(下一笔新增 simulator/tests/ref/shot_soe.bin):本笔单独 checkout 时
ui-golden 会报"缺参考",属预期拆分;上述钩子结论取自"本笔 + 下一笔"的合并树。
自查:BOM 漂移审计 17 个改动文件全对齐 HEAD;CR = 0;U+FFFD = 0;字面量纯 ASCII。
820 lines
33 KiB
C
820 lines
33 KiB
C
/**
|
||
* 模块名称: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_w_soe.h" /* task-15 SOE 记录 reader 注入点(同上:镜像真机绑定) */
|
||
#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 };
|
||
/*
|
||
* 函数功能:SOE 记录假源——固定 3 条(镜像 App/task/ui_task.c 真机绑定的形态)
|
||
* 入口参数:无
|
||
* 返回值:条数 3 uint16_t
|
||
* 限定条件:模拟器侧(PC 上没有 /log/soe.log,读盘侧不参与模拟器构建)
|
||
* 函数说明:三条刻意覆盖组件的三条显示支路——① 元件事件(元件名 + "动作");
|
||
* ② 同元件复归("返回");③ **非元件**的未标定闭锁(type 7 + ELM_NONE,
|
||
* 类型名进名列)。第 ③ 条是"新增的两个事件类型 7/8 到底有没有接上屏"的
|
||
* 唯一像素级证据:少了它,名字表写错也照样一片绿
|
||
*/
|
||
static uint16_t sim_soe_count(void) {
|
||
return 3u;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:读第 idx 条假 SOE 记录
|
||
* 入口参数:idx - 记录序号 uint32_t
|
||
* p_out - UI 视图输出 ui_w_soe_rec_t *
|
||
* 返回值:0 - 成功;-1 - 空指针或越界 int
|
||
* 限定条件:时标用**固定串**——黄金参考要求逐像素可复现,跑一次一个样就等于没有基线
|
||
*/
|
||
static int sim_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) {
|
||
static const uint8_t k_type[3] = { UI_W_SOE_T_ELM_ASSERT, UI_W_SOE_T_ELM_RETURN,
|
||
UI_W_SOE_T_CAL_BLOCK };
|
||
/* 元件号:0 = 过流 Ⅰ 段;第 3 条是非元件事件 → 不透明"无元件"值(与 App 同值) */
|
||
static const uint8_t k_elm[3] = { 0u, 0u, UI_W_SOE_ELM_NONE };
|
||
static const char k_ts[3][UI_W_SOE_TS_LEN] = { "2026-10-25 08:00:01",
|
||
"2026-10-25 08:00:03",
|
||
"2026-10-25 08:00:05" };
|
||
|
||
if ((p_out == NULL) || (idx >= 3u)) {
|
||
return -1;
|
||
}
|
||
(void)snprintf(p_out->ts, sizeof(p_out->ts), "%s", k_ts[idx]);
|
||
p_out->type = k_type[idx];
|
||
p_out->elm = k_elm[idx];
|
||
p_out->flags = 0u;
|
||
p_out->seq = idx + 1u;
|
||
p_out->value = idx + 1u;
|
||
return 0;
|
||
}
|
||
|
||
static const ui_soe_reader_t s_soe_reader = { sim_soe_count, sim_soe_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);
|
||
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */
|
||
/* 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);
|
||
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */
|
||
/* 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;
|
||
}
|