d64bbdc57f
数据面(App/algo,纯逻辑,双端编译) - wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask 唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃 兜底——下一次真故障无波可录比丢一段更贵 - wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带 锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度 - 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式 - 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样 纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉 - test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条 自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合 - 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定) 接线(任务层) - adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图), 段头按最低有效位精确记账,不经浮点回算 - adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实; 台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行 - defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到 关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms 补一条告警并记历史峰值(喂狗余量可核) - Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留 超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的, 半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进 - Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到 上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数; 段文件字节布局零改动,三份合成段哈希逐字节不变 - protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体 零改动 人机(160×160 单色屏) - 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器 得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30 - 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与 垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、 单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据 - ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、 手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口 - 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有 60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形 - 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变 - 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏 22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、 仪器用已在跑的单轮工作耗时探针,不新加埋点 台账与文档 - 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936 字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名; 散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建 重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置) - 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据); §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径 风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」 - 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项 - 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划 索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动) - 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件 形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记 「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场 - 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行 227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退 为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与 体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账 同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道 编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串 生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
1800 lines
75 KiB
C
1800 lines
75 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"
|
||
#include "wave_ring.h" /* 录波假源:真环(不进 SIM_EXEMPT,见下方块注释) */
|
||
#include "wave_store.h" /* 录波假源:真段格式与定点换算(同一条链) */
|
||
#include "adc_cal.h" /* ADC_CAL_K_Q16_* 封存系数原值(不手抄对照表) */ /* 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
|
||
*/
|
||
/* 镜像 App/task/ui_task.c 的 s_log_map:两端 UI 编号 → syslog_id_t 表同源同长;
|
||
* 顺序契约(== 枚举序、只准尾追加)在真机侧有编译期守卫,宿主侧由
|
||
* test/host/ui_data TC-UIDATA-024 与下面的数组长度同时把住 */
|
||
static const syslog_id_t s_sim_log_map[UI_W_REC_LOG_N] = {
|
||
SYSLOG_RUN, SYSLOG_SELFTEST, SYSLOG_ACCIDENT, SYSLOG_ALARM,
|
||
SYSLOG_START, SYSLOG_OPER, SYSLOG_YK
|
||
};
|
||
typedef char sim_log_map_len_check_t[(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT) ? 1 : -1];
|
||
|
||
static uint16_t sim_rec_count(uint8_t log_id) {
|
||
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||
return 0u;
|
||
}
|
||
return (uint16_t)syslog_count(s_sim_log_map[log_id]);
|
||
}
|
||
|
||
/*
|
||
* 函数功能:记录 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 >= (uint8_t)UI_W_REC_LOG_N) {
|
||
return -1;
|
||
}
|
||
return syslog_get_record(s_sim_log_map[log_id], idx, ts, (size_t)ts_len, desc,
|
||
(size_t)desc_len);
|
||
}
|
||
|
||
static const ui_rec_reader_t s_rec_reader = { sim_rec_count, sim_rec_get };
|
||
|
||
/*
|
||
* ======================= 道F task-3:装置维护写通道假件(镜像 ui_task.c)
|
||
*
|
||
* 形状与真机完全同一张函数表(六件),语义差一处如实记在下面:清除请求在宿主端
|
||
* 是同步落地的(simulator/sim/sim_syslog.c:本调用内就删完,状态直接 OK),
|
||
* 真机则先 WAIT 再等 defaultTask 落 —— 页面对两种时序都要显示正确,
|
||
* 所以清除记录页的值词表里 WAIT 与 OK 都留了位置(黄金参考只覆盖得到 OK)。
|
||
*/
|
||
|
||
static void sim_publish_data_frames(void); /* 前置声明:本文件下方定义 */
|
||
|
||
static int s_noclock; /* --noclock:把本机装成"墙钟无效" */
|
||
static uint32_t s_sim_epoch; /* 面板设过的墙钟(0 = 没设过) */
|
||
static uint32_t s_sim_active = 1u; /* 假"动作态元件数":1 → 复归请求后 0 */
|
||
static char s_sim_note[32]; /* 最近一条面板留痕(页面上看不到,
|
||
但自检/排障时是唯一的取证处) */
|
||
|
||
/*
|
||
* 函数功能:假"设置墙钟"——校验后落内存并立即重发实时帧
|
||
* 入口参数:a_epoch - Unix 秒 uint32_t
|
||
* 返回值:0 - 已生效;-1 - 越界(与真机同一判据) int
|
||
* 限定条件:uiTask 等价上下文(注键链内)
|
||
* 函数说明:真机的"回读证据"来自 SD2506 回读 + 快照层 200ms 重发;模拟器没有
|
||
* RTC 也没有周期发布(无头模式连 200ms 节拍都不走),这里用"当场重发
|
||
* 一帧"补上那半条时间轴——与 sim_param_pump() 同一个理由。不这么做的话,
|
||
* 保存成功但屏幕上的时间纹丝不动,黄金参考就会把"页面显示旧值"误当成
|
||
* "回读失败",而真机上这个组合根本不可能出现(那就是假故障)
|
||
*/
|
||
static int sim_maint_clock_set(uint32_t a_epoch) {
|
||
if ((a_epoch == 0u) || (a_epoch > (uint32_t)UI_DATA_CLOCK_EPOCH_MAX)) {
|
||
return -1;
|
||
}
|
||
s_sim_epoch = a_epoch;
|
||
sim_publish_data_frames();
|
||
return 0;
|
||
}
|
||
|
||
/* 函数功能:假"强制复归"——把动作态元件数清零(真机由 protectTask 下一拍清) */
|
||
static int sim_maint_reset_req(void) {
|
||
s_sim_active = 0u;
|
||
return 0;
|
||
}
|
||
|
||
/* 函数功能:假"动作态元件数"(复归页观测行 "保护动作 N") */
|
||
static uint32_t sim_maint_active_count(void) {
|
||
return s_sim_active;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:假"面板留痕"——真投进宿主端的 /log/oper.log(不是只记账)
|
||
* 入口参数:a_desc - 描述串 const char *
|
||
* 返回值:无
|
||
* 限定条件:uiTask 等价上下文
|
||
* 函数说明:留痕必须**真的落盘**,否则清除记录页上"清完 oper.log 之后操作记录
|
||
* 又从 0 变回 1"这条事实在模拟器里永远看不见,黄金参考也就白拍
|
||
*/
|
||
static void sim_maint_oper_note(const char *a_desc) {
|
||
if (a_desc == NULL) {
|
||
return;
|
||
}
|
||
(void)snprintf(s_sim_note, sizeof(s_sim_note), "%s", a_desc);
|
||
syslog_post(SYSLOG_OPER, a_desc); /* 该 API 无格式串形态(本头就是 const char *desc) */
|
||
}
|
||
|
||
/* 函数功能:假"请求清除记录"(宿主端同步落地,返回即已是终态) */
|
||
static int sim_maint_clear_req(uint8_t a_log_id) {
|
||
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||
return -1;
|
||
}
|
||
return (syslog_request_clear(s_sim_log_map[a_log_id]) == 0) ? 0 : -1;
|
||
}
|
||
|
||
/* 函数功能:假"清除状态"(值直通,与真机同一不改写口径) */
|
||
static int sim_maint_clear_state(uint8_t a_log_id) {
|
||
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||
return UI_DATA_CLEAR_ST_NONE;
|
||
}
|
||
return syslog_clear_state(s_sim_log_map[a_log_id]);
|
||
}
|
||
|
||
static const ui_data_maint_ops_t s_maint_ops = {
|
||
sim_maint_clock_set,
|
||
sim_maint_reset_req,
|
||
sim_maint_active_count,
|
||
sim_maint_oper_note,
|
||
sim_maint_clear_req,
|
||
sim_maint_clear_state
|
||
};
|
||
|
||
/*
|
||
* ======================= 道F task-7:遥测整定注入表假件(镜像 ui_data_feed.c 形状)
|
||
*
|
||
* 初值/合法域 = App/algo/run_par.c 工厂 ROM 与 RUN_PAR_DB_MAX_* 档位的镜像
|
||
* (模拟器不链 run_par,真源抄不来只能镜像;变更纪律同 s_sim_log_map 那条:
|
||
* 执行侧改档不改这里,屏上会呈现"档位与量纲自相矛盾"的肉眼可见错,黄金像素兜底)。
|
||
* ⚠ 脏位口径是这一家的存在理由:**写成功只置脏、绝不同步清**,清脏的唯一去处是
|
||
* sim_yt_pump()(镜像 defaultTask 每轮无条件 run_par_store_save())。同步清 =
|
||
* WAIT 态在模拟器里永不出现,"落盘确认"三态就只剩最好看那一帧被黄金钉住
|
||
* (Lead 裁定④原文的理由,别顺手"优化"成同步)。
|
||
*/
|
||
static float s_yt_db[UI_DATA_YT_DB_N] = {
|
||
0.5f, 0.5f, 0.5f, 0.5f, /* 电压死区 0.5 V(工厂额定档) */
|
||
0.005f, 0.005f, 0.005f, 0.005f, /* 电流死区 0.005 A */
|
||
0.5f /* 频率死区 0.5 Hz(绝对档) */
|
||
};
|
||
static float s_yt_k[UI_DATA_YT_CH_N] = {
|
||
152.587890625f, 152.587890625f, 152.587890625f, 152.587890625f,
|
||
152.587890625f, 152.587890625f, 152.587890625f, 152.587890625f
|
||
}; /* = adc_cal ROM 的 TPAFE 系数(µV/码);显示小数位 0 的量化后果见真机侧装配注 */
|
||
static uint8_t s_yt_dirty; /* 镜像 run_par 的整档脏位(页面上 WAIT 的唯一来源) */
|
||
static int s_yt_nopump; /* --yt-nopump:拍 WAIT 态黄金专用(截图前不清脏) */
|
||
|
||
/* 道G task-9 通讯参数假件:镜像 App/protocol/iec104/iec104_comm.c 的 RAM 副本
|
||
* 与脏位。模拟器不链 iec104_comm.c(与 run_par 假件同一条理由,见上方镜像
|
||
* 纪律注):值与合法带是镜像,初值 = IEC104_COMM_DEF_CA/_PORT,变更纪律同
|
||
* s_yt_* 三件套 */
|
||
static uint32_t s_comm_ca = 1u; /* 镜像 [comm] ca(工厂默认 1) */
|
||
static uint32_t s_comm_port = 2404u; /* 镜像 [comm] port(工厂默认 2404) */
|
||
static uint8_t s_comm_dirty; /* 镜像 iec104_comm 脏位 */
|
||
static int s_comm_nopump; /* --comm-nopump:拍通讯页 WAIT 态黄金专用 */
|
||
|
||
/* 死区粗守上限(工程量):镜像 RUN_PAR_DB_MAX_U/I/F 三档;分档与 feed 的 unit
|
||
* 分档必须同序(同一条对表纪律) */
|
||
/* 函数功能:假死区粗守上限(工程量档,镜像 RUN_PAR_DB_MAX_U/I/F) 返回值:上限 float */
|
||
static float sim_yt_db_hi(uint8_t a_idx) {
|
||
if (a_idx < 4u) {
|
||
return 50.0f;
|
||
}
|
||
if (a_idx < (UI_DATA_YT_DB_N - 1u)) {
|
||
return 1.0f;
|
||
}
|
||
if (a_idx == (UI_DATA_YT_DB_N - 1u)) {
|
||
return 5.0f;
|
||
}
|
||
return 0.0f;
|
||
}
|
||
|
||
/* 函数功能:假死区点数(镜像 RUN_PAR_DB_N) 返回值:点数 uint8_t */
|
||
static uint8_t sim_yt_db_n(void) {
|
||
return (uint8_t)UI_DATA_YT_DB_N;
|
||
}
|
||
|
||
/* 函数功能:假死区行现状(值现取自家数组、档位分档与 feed 同序) 返回值:0 / -1 int */
|
||
static int sim_yt_db_row(uint8_t a_idx, ui_data_yt_row_t *a_out) {
|
||
if ((a_out == NULL) || (a_idx >= UI_DATA_YT_DB_N)) {
|
||
return -1;
|
||
}
|
||
a_out->val = s_yt_db[a_idx];
|
||
a_out->lo = 0.0f;
|
||
a_out->hi = sim_yt_db_hi(a_idx);
|
||
a_out->decimals = 3u;
|
||
a_out->unit = (a_idx < 4u) ? UI_DATA_YT_UNIT_V
|
||
: ((a_idx < (UI_DATA_YT_DB_N - 1u)) ? UI_DATA_YT_UNIT_A : UI_DATA_YT_UNIT_HZ);
|
||
return 0;
|
||
}
|
||
|
||
/* 函数功能:假写死区——写守卫与执行侧同款语义:越界拒 = 值不动、**不置脏**
|
||
* (置了脏就是"屏上没改、却要落盘"的空承诺);接受 = 落值 + 置脏,
|
||
* 同一函数内发生。 返回值:0 / -1 int */
|
||
static int sim_yt_db_set(uint8_t a_idx, float a_eng) {
|
||
if ((a_idx >= UI_DATA_YT_DB_N) || (a_eng < 0.0f) || (a_eng > sim_yt_db_hi(a_idx))) {
|
||
return -1;
|
||
}
|
||
s_yt_db[a_idx] = a_eng;
|
||
s_yt_dirty = 1u;
|
||
return 0;
|
||
}
|
||
|
||
/* 函数功能:假系数点数(镜像 RUN_PAR_CH_N) 返回值:点数 uint8_t */
|
||
static uint8_t sim_yt_cal_n(void) {
|
||
return (uint8_t)UI_DATA_YT_CH_N;
|
||
}
|
||
|
||
/* 函数功能:假系数行现状(hi = RUN_PAR_K_MAX_UV 的镜像;见上注变更纪律) 返回值:0 / -1 int */
|
||
static int sim_yt_cal_row(uint8_t a_idx, ui_data_yt_row_t *a_out) {
|
||
if ((a_out == NULL) || (a_idx >= UI_DATA_YT_CH_N)) {
|
||
return -1;
|
||
}
|
||
a_out->val = s_yt_k[a_idx];
|
||
a_out->lo = 0.0f;
|
||
a_out->hi = 1000000000.0f; /* = RUN_PAR_K_MAX_UV 的镜像(粗守上限) */
|
||
a_out->decimals = 0u;
|
||
a_out->unit = UI_DATA_YT_UNIT_UV;
|
||
return 0;
|
||
}
|
||
|
||
/* 函数功能:假写系数 k(同 db_set 的"写值与置脏成对"语义) 返回值:0 / -1 int */
|
||
static int sim_yt_cal_k_set(uint8_t a_idx, float a_uv) {
|
||
if ((a_idx >= UI_DATA_YT_CH_N) || !(a_uv > 0.0f) || (a_uv > 1000000000.0f)) {
|
||
return -1; /* k<=0 与超粗守都拒;真带这一层模拟器不复刻(执行段的事) */
|
||
}
|
||
s_yt_k[a_idx] = a_uv;
|
||
s_yt_dirty = 1u;
|
||
return 0;
|
||
}
|
||
|
||
/* 函数功能:假未标定徽标源(恒已标定,理由见函数体注) 返回值:0 uint8_t */
|
||
static uint8_t sim_yt_cal_blocked(void) {
|
||
/* 模拟器恒"已标定":徽标腿的像素证据由整定四页黄金覆盖(同一注入式画法);
|
||
* 要拍"未标定 + 系数页"的组合另立带旗标的黄金态,本批不扩表 */
|
||
return 0u;
|
||
}
|
||
|
||
/* 函数功能:假整档脏位(页面上 WAIT 的唯一来源) 返回值:脏位 uint8_t */
|
||
static uint8_t sim_yt_pending(void) {
|
||
return s_yt_dirty;
|
||
}
|
||
|
||
static const ui_data_yt_ops_t s_yt_ops = {
|
||
sim_yt_db_n,
|
||
sim_yt_db_row,
|
||
sim_yt_db_set,
|
||
sim_yt_cal_n,
|
||
sim_yt_cal_row,
|
||
sim_yt_cal_k_set,
|
||
sim_yt_cal_blocked,
|
||
sim_yt_pending
|
||
};
|
||
|
||
/*
|
||
* 函数功能:落盘脏位的唯一清去处(镜像 defaultTask 每轮无条件 run_par_store_save)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:主循环 / 无头注键尾各一处;--yt-nopump(拍 WAIT 黄金)时整函数空转
|
||
* 函数说明:真机"保存成功"的语义 = cfg_file_save 返 0 才清脏;模拟器无失败面,
|
||
* pump 即落盘——ERR 腿不复刻(拍不出来也别假拍,屏上 ERR 由写被拒触发,
|
||
* 那条模拟器能真拍:见黄金 cal_reject)
|
||
*/
|
||
static void sim_yt_pump(void) {
|
||
if (s_yt_nopump != 0) {
|
||
return;
|
||
}
|
||
s_yt_dirty = 0u;
|
||
}
|
||
|
||
/* ── 道G task-9:通讯参数假件(通道六的注入侧镜像) ────────────────────── */
|
||
|
||
/*
|
||
* 函数功能:假 [comm] 读行(值 + 合法带;带 = iec104_comm.h 带宏的镜像)
|
||
* 入口参数:a_idx - UI_DATA_COMM_ROW_CA/_PORT uint8_t
|
||
* a_out - 行现状 ui_data_comm_row_t *
|
||
* 返回值:0 - 已整份写出;-1 - idx 越界 int
|
||
*/
|
||
static int sim_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out) {
|
||
if (a_idx == UI_DATA_COMM_ROW_CA) {
|
||
a_out->val = s_comm_ca;
|
||
a_out->lo = 1u;
|
||
a_out->hi = 65535u;
|
||
return 0;
|
||
}
|
||
if (a_idx == UI_DATA_COMM_ROW_PORT) {
|
||
a_out->val = s_comm_port;
|
||
a_out->lo = 1024u;
|
||
a_out->hi = 65535u;
|
||
return 0;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:假 [comm] 写行(带外拒、带内落副本并置脏——与真 setter 同形)
|
||
* 返回值:0 - 已写并置脏;-1 - 越界或带外拒 int
|
||
*/
|
||
static int sim_comm_set(uint8_t a_idx, uint32_t a_val) {
|
||
ui_data_comm_row_t r;
|
||
|
||
if (sim_comm_row(a_idx, &r) != 0) {
|
||
return -1;
|
||
}
|
||
if ((a_val < r.lo) || (a_val > r.hi)) {
|
||
return -1;
|
||
}
|
||
if (a_idx == UI_DATA_COMM_ROW_CA) {
|
||
s_comm_ca = a_val;
|
||
} else {
|
||
s_comm_port = a_val;
|
||
}
|
||
s_comm_dirty = 1u;
|
||
return 0;
|
||
}
|
||
|
||
/* 函数功能:假 [comm] 落盘脏位 返回值:1 有未落盘;0 干净 uint8_t */
|
||
static uint8_t sim_comm_pending(void) {
|
||
return s_comm_dirty;
|
||
}
|
||
|
||
static const ui_data_comm_ops_t s_comm_ops = {
|
||
sim_comm_row,
|
||
sim_comm_set,
|
||
sim_comm_pending
|
||
};
|
||
|
||
/*
|
||
* 函数功能:假 [comm] 落盘 pump(镜像 defaultTask 每轮无条件
|
||
* iec104_comm_store_save;模拟器无失败面,pump 即落盘)
|
||
* 限定条件:与 sim_yt_pump 同两个调用点;--comm-nopump 时停转拍 WAIT
|
||
*/
|
||
static void sim_comm_pump(void) {
|
||
if (s_comm_nopump != 0) {
|
||
return;
|
||
}
|
||
s_comm_dirty = 0u;
|
||
}
|
||
|
||
/* ═════════ 故障录波假源(UI 道,2026-10-29):真 ring + 真 store + 合成节拍 ═════════
|
||
* 口径(lead 裁定):wave_ring.c / wave_store.c **不进 SIM_EXEMPT**——模拟器链
|
||
* 的就是板端那份实现,回放页在模拟器里出的是**真波形**:合成 1kHz 节拍喂进
|
||
* ring,封存后用 wave_ring_read_cb 作 wave_store 打包游标的数据源产整段,
|
||
* 读侧再走 head_parse / data_off / le_decode / code_to_milli 同一条链——
|
||
* 与 prot_set.c 同一条「双端共享核心、不造第二真相」。
|
||
* 数值全取头文件宏与 wave_store 现算(WAVE_ST_* / ADC_CAL_K_Q16_*),
|
||
* 不手抄对照表。旗标:--wave-empty 拍「无录波段」空态黄金;--wave-bad 拍
|
||
* 「该段不可读」黄金(故意翻转数据区一字节,让**真 CRC 判废**去拒绝它——
|
||
* 不是把「不可读」写死在假件返回值里)。
|
||
*/
|
||
#define SW_IMG_MAX 2u /* 两段:左右键切段的像素证据(段号 + 相位都不同) */
|
||
|
||
static uint8_t s_wave_img[SW_IMG_MAX][WAVE_ST_SEG_SZ_CUR];
|
||
static uint8_t s_wave_idxval[SW_IMG_MAX]; /* 槽位对应的段号(模拟盘上编号) */
|
||
static uint8_t s_wave_cnt;
|
||
static int s_wave_empty; /* --wave-empty */
|
||
static int s_wave_bad; /* --wave-bad */
|
||
static uint32_t s_wave_now = 1000u; /* 合成毫秒(跨段单调) */
|
||
static uint32_t s_wave_seq = 10u; /* 合成帧序号(跨段单调) */
|
||
|
||
/*
|
||
* 函数功能:合成第 t 拍的 8 路原始码值
|
||
* 入口参数:t - 合成拍号
|
||
* 返回:无(写 codes[8],下标 = 语义号:UA UB UC UN IA IB IC IN)
|
||
* 函数说明:TPAFE 三路(sem 0/3/4)双极 50Hz 正弦(1kHz 采样每周波 20 点,
|
||
* 幅度 ±8000 码 = 引脚域约 ±1.2V);STM32 五路(sem 1/2/5/6/7)
|
||
* 半波 0..2048 且**钉死下轨**(V0.1 单极性无偏置的事实还原——
|
||
* 回放页必须让「贴 0 线」与「削顶」在像素上可分辨)
|
||
*/
|
||
static void sim_wave_codes(uint32_t a_t, int16_t *a_codes) {
|
||
/* 50Hz 正弦 20 点表(×8000 满幅一半再取整,固定表 = 逐像素可复现) */
|
||
static const int16_t k_sine[20] = {
|
||
0, 2351, 4530, 6428, 7893, 8000, 7893, 6428, 4530, 2351,
|
||
0, -2351, -4530, -6428, -7893, -8000, -7893, -6428, -4530, -2351
|
||
};
|
||
int16_t half = (int16_t)(4095 - (a_t % 20u) * 200u);
|
||
uint8_t i;
|
||
|
||
for (i = 0u; i < 8u; i++) {
|
||
if ((i == 0u) || (i == 3u) || (i == 4u)) {
|
||
a_codes[i] = k_sine[a_t % 20u];
|
||
} else {
|
||
a_codes[i] = (half > 0) ? half : 0; /* 半波衰减再钉下轨 0 */
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:同步跑完一整段「触发 → 冻结 → 封存 → 打包 → ACK → 冷却」
|
||
* 入口参数:无
|
||
* 返回值:无(成功则 s_wave_cnt++,坏图旗标开启时翻转数据区一字节)
|
||
* 限定条件:仅 uiTask/主线程绘制期外的同步调用(假件特权,真机无此路径)
|
||
* 函数说明:无头模式没有 lv_timer_handler 的 1ms 泵(同 sim_yt_pump 的
|
||
* 「当场排空」理由)——截图前必须把这段状态机原地跑完,
|
||
* 否则回放页永远停在空态,阶梯线/0 线/峰值/徽标就全都没有
|
||
* 像素证据。冷却 500ms 也要泵过去:下一段从 IDLE 起
|
||
*/
|
||
static void sim_wave_capture_one(void) {
|
||
wave_seal_cal_t cal;
|
||
wave_seg_meta_t meta;
|
||
wave_pack_ctx_t ctx;
|
||
int16_t codes[8];
|
||
uint8_t buf[256];
|
||
static uint8_t slot_pfx;
|
||
static uint8_t inited; /* ring 一次性初始化(假件镜像 adc 侧上电) */
|
||
uint32_t out_len = 0u;
|
||
uint32_t total;
|
||
int32_t r;
|
||
uint32_t t;
|
||
uint8_t i;
|
||
|
||
if (s_wave_cnt >= SW_IMG_MAX) {
|
||
return;
|
||
}
|
||
if (inited == 0u) {
|
||
wave_ring_init();
|
||
inited = 1u;
|
||
}
|
||
/* 预滚历史 ≥ 预窗:触发前的 200 拍先喂进环(真机上环一直在滚,
|
||
* 模拟器的环不预滚的话预窗就是 init 全零——回放页两周波里只有一半
|
||
* 有波形,像素证据废一半)。push 在 IDLE/FREEZE 都写环(SEALED 才丢),
|
||
* 契约见 App/algo/wave_ring.c 的 wave_ring_push 头注 */
|
||
for (t = 0u; t < 200u; t++) {
|
||
sim_wave_codes(s_wave_seq + t, codes);
|
||
(void)wave_ring_push(codes, 8u);
|
||
(void)wave_ring_tick(s_wave_now + t, s_wave_seq + t);
|
||
}
|
||
s_wave_now += 200u;
|
||
s_wave_seq += 200u;
|
||
if (wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u) != 0) {
|
||
return;
|
||
}
|
||
r = 0;
|
||
for (t = 0u; t < 4000u; t++) {
|
||
sim_wave_codes(s_wave_seq + t, codes);
|
||
(void)wave_ring_push(codes, 8u);
|
||
r = wave_ring_tick(s_wave_now + t, s_wave_seq + t);
|
||
if (r == 1) {
|
||
break;
|
||
}
|
||
}
|
||
if (r != 1) {
|
||
return;
|
||
}
|
||
s_wave_now += t + 1u;
|
||
s_wave_seq += t + 1u;
|
||
/* 封存交棒:V0.1 真值口径(未标定 → VALID 全 0;CLIPPED 五路 = 0xE6;
|
||
* 系数取宏原值,float 一面都不碰——§6.3) */
|
||
(void)memset(&cal, 0, sizeof(cal));
|
||
cal.eng = (uint8_t)WAVE_ENG_PIN;
|
||
cal.flags_seq = 0u;
|
||
cal.clip_mask = 0xE6u;
|
||
cal.wall = 0u;
|
||
for (i = 0u; i < 8u; i++) {
|
||
cal.ch[i].sem = i;
|
||
if ((i == 0u) || (i == 3u) || (i == 4u)) {
|
||
cal.ch[i].conv = 0u;
|
||
cal.ch[i].bits = 16u;
|
||
cal.ch[i].k_q16 = (int32_t)ADC_CAL_K_Q16_TPAFE;
|
||
cal.ch[i].avail = 0u;
|
||
} else {
|
||
cal.ch[i].conv = 1u;
|
||
cal.ch[i].bits = 12u;
|
||
cal.ch[i].k_q16 = (int32_t)ADC_CAL_K_Q16_STM32;
|
||
cal.ch[i].avail = (uint8_t)0x02u; /* 只 CLIPPED,无 VALID */
|
||
}
|
||
cal.ch[i].b_codes = 0;
|
||
}
|
||
if (wave_ring_latch_meta(&cal) != 0) {
|
||
return;
|
||
}
|
||
if (wave_ring_sealed_meta(&meta) != 0) {
|
||
return;
|
||
}
|
||
total = wave_store_seg_size(&meta);
|
||
if ((total == 0u) || (total > (uint32_t)WAVE_ST_SEG_SZ_CUR)) {
|
||
return;
|
||
}
|
||
if (wave_store_pack_start(&ctx, &meta, wave_ring_read_cb, NULL) != 0) {
|
||
return;
|
||
}
|
||
for (;;) {
|
||
uint32_t len = 0u;
|
||
|
||
r = wave_store_pack_next(&ctx, buf, (uint32_t)sizeof(buf), &len);
|
||
if ((r != 0) && (r != WAVE_ST_DONE)) {
|
||
return;
|
||
}
|
||
if ((out_len + len) > (uint32_t)WAVE_ST_SEG_SZ_CUR) {
|
||
return;
|
||
}
|
||
(void)memcpy(&s_wave_img[s_wave_cnt][out_len], buf, len);
|
||
out_len += len;
|
||
if (r == WAVE_ST_DONE) {
|
||
break;
|
||
}
|
||
}
|
||
if (out_len != total) {
|
||
return;
|
||
}
|
||
if (s_wave_bad != 0) {
|
||
/* 翻转数据区一点:真 seg_check 的双 CRC 判废会拒它——UI 侧必须
|
||
* 走到「该段不可读」而不是画半截线(伪造证据的反面教材做成像素) */
|
||
s_wave_img[s_wave_cnt][WAVE_ST_HEAD_BYTES(WAVE_ST_CH_NUM) + 16u] ^= 0xA5u;
|
||
}
|
||
s_wave_idxval[s_wave_cnt] = slot_pfx;
|
||
slot_pfx = (uint8_t)((slot_pfx + 1u) % WAVE_ST_SEG_MAX);
|
||
s_wave_cnt++;
|
||
(void)wave_ring_write_done(0);
|
||
/* 泵过冷却窗(COOL = 500ms 只加 ovf 不采新触发;不泵则第二段起不来) */
|
||
for (t = 0u; t < (WAVE_RING_COOL_MS + 1u); t++) {
|
||
sim_wave_codes(s_wave_seq + t, codes);
|
||
(void)wave_ring_push(codes, 8u);
|
||
(void)wave_ring_tick(s_wave_now + t, s_wave_seq + t);
|
||
}
|
||
s_wave_now += WAVE_RING_COOL_MS + 1u;
|
||
s_wave_seq += WAVE_RING_COOL_MS + 1u;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:懒触发建段(回放页第一次取数时把两段建齐)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:录波通道的 first/next 入口调用
|
||
*/
|
||
static void sim_wave_service(void) {
|
||
if (s_wave_empty != 0) {
|
||
return;
|
||
}
|
||
while (s_wave_cnt < SW_IMG_MAX) {
|
||
sim_wave_capture_one();
|
||
}
|
||
}
|
||
|
||
/* 函数功能:按段号找槽位 int */
|
||
static int sim_wave_slot(uint8_t a_idx) {
|
||
uint8_t i;
|
||
|
||
for (i = 0u; i < s_wave_cnt; i++) {
|
||
if (s_wave_idxval[i] == a_idx) {
|
||
return (int)i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:验段并把头解析进 p_meta(坏段拒得干净) int
|
||
* 说明:这里用全图 seg_check 与真机的流式判废**判据相同**(尺寸等式 +
|
||
* 双 CRC + sem==index),只差在真机为省 RAM 分窗——判定结果一致
|
||
* 就是「模拟器像素 = 真机行为」的前提
|
||
*/
|
||
static int sim_wave_verified(int a_slot, wave_seg_meta_t *p_meta) {
|
||
if (a_slot < 0) {
|
||
return UI_WAVE_NONE;
|
||
}
|
||
if (wave_store_seg_check(s_wave_img[a_slot], WAVE_ST_SEG_SZ_CUR,
|
||
p_meta) != 0) {
|
||
return UI_WAVE_BAD;
|
||
}
|
||
return UI_WAVE_OK;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:模拟器侧「手动录波」按键的受理判定(第五注入通道的 manual_req 实现)
|
||
* 入口参数:无
|
||
* 返 回 值:0 = 受理(界面据此显示已请求);-1 = 拒绝 int
|
||
* 限定条件:仅 UI 线程上下文(与固件 ui_task.c 的同名 ops 一一对应)
|
||
* 函数说明:--wave-empty 旗标下恒拒,目的是让「录波进行中/无段可录」那条文案在
|
||
* 模拟器里有像素证据;真受理路径只置计数,段的生成走 sim_wave_service()
|
||
* 的懒路径(等 UI 真的来要数据时才造出一段,避免开机就烧 CPU)。
|
||
*/
|
||
static int sim_wave_manual_req(void) { if (s_wave_empty != 0) {
|
||
return -1; /* 空态旗标:触发也拒(ERR 分支要有像素证据) */
|
||
}
|
||
return 0; /* 受理 = ring 已 IDLE 可来一段;段的生成走懒路径 */
|
||
}
|
||
|
||
/*
|
||
* 函数功能:取开屏默认段号(回放页第一次进页面时的落点)
|
||
* 入口参数:a_idx 输出段号
|
||
* 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int
|
||
* 限定条件:仅 UI 线程上下文
|
||
* 函数说明:与固件侧同口径——按**数值**取最大号(不是「时间最新」,回绕后两者
|
||
* 不等价),页顶显示的段号就是这一句的自证。
|
||
*/
|
||
static int sim_wave_seg_first(uint8_t *a_idx) { sim_wave_service();
|
||
if (s_wave_cnt == 0u) {
|
||
return UI_WAVE_NONE;
|
||
}
|
||
*a_idx = s_wave_idxval[s_wave_cnt - 1u];
|
||
return UI_WAVE_OK;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:切到数值上前一段(镜像 LR 左键;段号表按数值升序)
|
||
* 入口参数:a_cur 当前段号;a_out 输出切换后的段号
|
||
* 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int
|
||
* 限定条件:仅 UI 线程上下文;当前段不在表里(被覆盖)时退回最大号
|
||
*/
|
||
static int sim_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) { sim_wave_service();
|
||
if (s_wave_cnt == 0u) {
|
||
return UI_WAVE_NONE;
|
||
}
|
||
{
|
||
int pos = sim_wave_slot(a_cur);
|
||
if (pos < 0) {
|
||
pos = (int)s_wave_cnt - 1;
|
||
}
|
||
pos = (pos + (int)s_wave_cnt - 1) % (int)s_wave_cnt;
|
||
*a_out = s_wave_idxval[pos];
|
||
}
|
||
return UI_WAVE_OK;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:切到数值上后一段(镜像 LR 右键)
|
||
* 入口参数:a_cur 当前段号;a_out 输出切换后的段号
|
||
* 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 一段都没有 int
|
||
* 限定条件:仅 UI 线程上下文;表内循环(到尾回头),与固件侧同语义
|
||
*/
|
||
static int sim_wave_seg_next(uint8_t a_cur, uint8_t *a_out) { sim_wave_service();
|
||
if (s_wave_cnt == 0u) {
|
||
return UI_WAVE_NONE;
|
||
}
|
||
{
|
||
int pos = sim_wave_slot(a_cur);
|
||
if (pos < 0) {
|
||
pos = 0;
|
||
}
|
||
pos = (pos + 1) % (int)s_wave_cnt;
|
||
*a_out = s_wave_idxval[pos];
|
||
}
|
||
return UI_WAVE_OK;
|
||
}
|
||
|
||
static int sim_wave_seg_head(uint8_t a_idx, uint8_t *a_eng, uint8_t *a_trig,
|
||
uint8_t *a_clip) {
|
||
wave_seg_meta_t meta;
|
||
int rc = sim_wave_verified(sim_wave_slot(a_idx), &meta);
|
||
|
||
if (rc != UI_WAVE_OK) {
|
||
return rc;
|
||
}
|
||
*a_eng = meta.eng;
|
||
*a_trig = meta.trig_src;
|
||
*a_clip = (uint8_t)(meta.clip_mask & 0xFFu);
|
||
return UI_WAVE_OK;
|
||
}
|
||
|
||
static int sim_wave_seg_win(uint8_t a_idx, uint8_t a_ch, int16_t *a_codes,
|
||
int32_t *a_peak_milli) {
|
||
wave_seg_meta_t meta;
|
||
int slot = sim_wave_slot(a_idx);
|
||
int rc = sim_wave_verified(slot, &meta);
|
||
uint32_t start;
|
||
uint32_t off;
|
||
uint32_t got = 0u;
|
||
int16_t raw[UI_WAVE_WIN_PTS];
|
||
int32_t peak = 0;
|
||
int32_t milli;
|
||
uint8_t i;
|
||
|
||
if (rc != UI_WAVE_OK) {
|
||
return rc;
|
||
}
|
||
if ((a_ch >= meta.ch_count) || (meta.pre_pts < (UI_WAVE_WIN_PTS / 2u))) {
|
||
return UI_WAVE_BAD;
|
||
}
|
||
start = (uint32_t)meta.pre_pts - (UI_WAVE_WIN_PTS / 2u);
|
||
off = wave_store_data_off(meta.ch_count, meta.pts_per_ch, a_ch, start);
|
||
if (off == 0xFFFFFFFFu) {
|
||
return UI_WAVE_BAD;
|
||
}
|
||
if (wave_store_le_decode(&s_wave_img[slot][off], UI_WAVE_WIN_PTS * 2u,
|
||
raw, UI_WAVE_WIN_PTS, &got) != 0 ||
|
||
got != UI_WAVE_WIN_PTS) {
|
||
return UI_WAVE_BAD;
|
||
}
|
||
for (i = 0u; i < UI_WAVE_WIN_PTS; i++) {
|
||
int32_t v;
|
||
|
||
v = (int32_t)raw[i] + meta.ch[a_ch].b_codes;
|
||
if (v > 32767L) {
|
||
v = 32767L;
|
||
} else if (v < (-32768L)) {
|
||
v = -32768L;
|
||
}
|
||
a_codes[i] = (int16_t)v;
|
||
/* 同尺说明与真机胶水同款(App/task/ui_task.c seg_win 注释):
|
||
* code_to_milli 内部 (raw + b_codes) × k(App/algo/wave_store.c:567),
|
||
* b 只在唯一口径里加一次,PK 与屏上波形同尺 */
|
||
if (wave_store_code_to_milli(&meta.ch[a_ch], raw[i], &milli) == 0) {
|
||
/* 「正峰优先」并列判据与真机胶水同款(App/task/ui_task.c
|
||
* seg_win 注释):等幅异号时取正,峰值读数不随触发相位漂 */
|
||
if (((milli < 0 ? -milli : milli) > (peak < 0 ? -peak : peak)) ||
|
||
(((milli < 0 ? -milli : milli) == (peak < 0 ? -peak : peak)) &&
|
||
(milli > peak))) {
|
||
peak = milli;
|
||
}
|
||
}
|
||
}
|
||
*a_peak_milli = peak;
|
||
return UI_WAVE_OK;
|
||
}
|
||
|
||
/* 录波通道假表(与真机 s_wave_ops 同形状六件) */
|
||
static const ui_data_wave_ops_t s_wave_ops = {
|
||
sim_wave_manual_req,
|
||
sim_wave_seg_first,
|
||
sim_wave_seg_prev,
|
||
sim_wave_seg_next,
|
||
sim_wave_seg_head,
|
||
sim_wave_seg_win
|
||
};
|
||
/*
|
||
* 函数功能: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 个数字——抄一次就是一份会漂移的第二真相
|
||
*/
|
||
/* 最近一次发布的参数帧校验和(sim_param_pump 的"值变了才重发"门,与真机同纪律) */
|
||
static uint32_t s_par_pub_sum;
|
||
|
||
/*
|
||
* 函数功能:按 prot_set 现状填一帧参数快照(模拟器版 feed_fill_par)
|
||
* 入口参数:p_par - 目标帧(调用方已清零;本函数只填有真源的几个字段)
|
||
* 返回值:无
|
||
* 限定条件:prot_set_init() 已跑过(值区有效)
|
||
* 函数说明:40 项值全部现取 prot_set,不手抄数字(抄一次就是一份会漂移的第二真相)。
|
||
* cal_block 固定给 1:**真机现在就是**"UA/UN/IA 未标定 → 判据停走"
|
||
* (产品构建下 [adc_cal] 的 valid 位没写,用户裁定保持闭锁),模拟器要
|
||
* 与本机一致才配当像素基准——rt.eng 也按同一条口径钉在引脚档。
|
||
* 标定链在模拟器里没有数据源,所以这里给常量而不是"猜一个会变的值";
|
||
* 要拍"已标定"态的图,改这一处即可(黄金参考随之要一起刷)
|
||
*/
|
||
static void sim_fill_par_frame(ui_data_par_t *p_par) {
|
||
uint8_t i;
|
||
|
||
p_par->valid = 1u;
|
||
p_par->checksum = prot_set_checksum();
|
||
p_par->locked = prot_set_locked();
|
||
p_par->bad_mask = 0u; /* 装载成功:无越界项 */
|
||
p_par->cal_block = 1u; /* 与真机同口径:未标定(判据停走) */
|
||
for (i = 0u; i < UI_DATA_SET_N; i++) {
|
||
p_par->q10[i] = prot_set_get((prot_set_id_t)i);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:发布一帧参数快照并记下其校验和(写路径落定后重发用)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:prot_set_init() 之后
|
||
* 函数说明:与 App/app/ui_data_feed.c 同一条纪律——值变了才重发(这里比校验和,
|
||
* 模拟器没有自检闭锁与标定链,那两个源恒不变);页面按发布序号失效缓存
|
||
*/
|
||
static void sim_publish_par_frame(void) {
|
||
ui_data_par_t par;
|
||
|
||
(void)memset(&par, 0, sizeof(par));
|
||
sim_fill_par_frame(&par);
|
||
(void)ui_data_publish_par(&par);
|
||
s_par_pub_sum = par.checksum;
|
||
}
|
||
|
||
static void sim_publish_data_frames(void) {
|
||
ui_data_rt_t rt;
|
||
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 有效位图(与点表同集合) */
|
||
/* 双点位图照写侧口径给(App/protocol/iec104/iec104_pts.c 的 YX_DP_N = 4 → bit0~3)。
|
||
* 这 4 点**不在**上面的有效位图里(V0.1 无开入硬件),所以遥信页对它们打 "--"——
|
||
* 刻意不把 bit0 塞进 yx_valid 让画面多两个汉字好看:那会让黄金参考显示成"这台设备
|
||
* 有开关位置遥信",而真机上没有(屏上的话必须与本机一致,同页脚页码那条裁定) */
|
||
rt.yx_dp = 0x0FULL;
|
||
rt.freq_valid = 0u; /* eng=0 期间幅值门恒失效 → 无新鲜读数 */
|
||
rt.freq_hz = 0.0f;
|
||
/* 墙钟两态(道F task-3):--noclock 把这台机器装成"RTC 异常/未对时"——只有
|
||
* 这样才照得到时钟设置页的 "--" + "无时钟"徽标形态;面板设过时以面板值为准,
|
||
* 这就是页面上"回读证据"的唯一来源(真机那边是 SD2506 回读 + 快照层重发) */
|
||
rt.clock_ok = (s_noclock == 0) ? 1u : 0u;
|
||
if (s_noclock != 0) {
|
||
rt.clock[0] = 0; /* 无效时标 = 空串(页面据此显 "--",绝不显 1970) */
|
||
} else if (s_sim_epoch != 0u) {
|
||
ui_data_clock_t ck;
|
||
|
||
if (ui_data_clock_from_epoch(s_sim_epoch, &ck) == 0) {
|
||
(void)ui_data_clock_format(&ck, rt.clock, sizeof(rt.clock));
|
||
} else {
|
||
rt.clock[0] = 0;
|
||
rt.clock_ok = 0u;
|
||
}
|
||
} else {
|
||
(void)snprintf(rt.clock, sizeof(rt.clock), "%s", "2026-10-25 08:00:00");
|
||
}
|
||
(void)ui_data_publish_rt(&rt);
|
||
|
||
/* 定值帧:出厂默认 + 封存(幂等),再按 prot_set 现状发一帧(见上面两个函数) */
|
||
(void)prot_set_init();
|
||
sim_publish_par_frame();
|
||
|
||
(void)memset(&info, 0, sizeof(info));
|
||
info.valid = 1u;
|
||
info.uptime_s = 0u;
|
||
info.port_104 = (uint16_t)s_comm_port; /* 镜像 [comm] getter(真机 feed 同款:宏出口已删,道G task-9) */
|
||
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");
|
||
/* 运行报告四数:模拟器**没有** boot_stats(依赖 SD2506 I2C 与 RCC_CSR,模拟器不编译
|
||
* 该模块)也没有 104 协议栈 → 填确定值假数据,只为像素黄金可复现;真机走
|
||
* App/app/ui_data_feed.c 的 boot_stats_get()/iec104_serv_get_stats() 真源。
|
||
* 差异已登记 docs/测试说明/模拟器差距清单.md(2026-10-28 UI 补齐「运行报告」) */
|
||
info.boot_ready = 1u;
|
||
info.boot_count = 7u;
|
||
info.boot_seq_state = 2u; /* 延续(自 RTC 电池周期起) */
|
||
info.boot_write_failed = 0u;
|
||
info.rst_cause = 4u; /* 上电复位(BOOT_STATS_CAUSE_POR) */
|
||
info.iec104_conn_up = 3u;
|
||
info.iec104_apdu_tx = 128u;
|
||
info.iec104_tx_fail = 0u;
|
||
info.iec104_rx_bytes = 4096u;
|
||
(void)ui_data_publish_info(&info);
|
||
}
|
||
|
||
/* ── 定值写路径镜像(道F task-2:整定 4 页的"保存"落定)──────────────────── */
|
||
|
||
/*
|
||
* 函数功能:写路径判定"定值闭锁中吗"(模拟器版,镜像 App/app/ui_data_feed.c)
|
||
* 入口参数:无
|
||
* 返回值:prot_set 的闭锁态(模拟器里恒 0:不跑 定值校验和 自检) uint8_t
|
||
*(保护功能清单见 docs/算法/继电保护功能复现规划.md)
|
||
* 限定条件:主循环 / 无头注键链(单线程)
|
||
*/
|
||
static uint8_t sim_param_locked(void) {
|
||
return prot_set_locked();
|
||
}
|
||
|
||
/*
|
||
* 函数功能:写路径整批预检——单项是否落在 meta 上下限内
|
||
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
|
||
* 返回值:0 - 合法;-1 - 编号越界或值越界 int
|
||
* 限定条件:只读 g_prot_set_meta
|
||
* 函数说明:与真机走的是**同一份**上下限真源(App/algo/prot_set.c),
|
||
* 所以"模拟器里越界被拒"与"真机上越界被拒"是同一个判定,不是两份规矩
|
||
*/
|
||
static int sim_param_check(uint8_t a_id, int32_t a_q10) {
|
||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||
return -1;
|
||
}
|
||
if ((a_q10 < g_prot_set_meta[a_id].min) || (a_q10 > g_prot_set_meta[a_id].max)) {
|
||
return -1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:写路径落值——单项原子写入 + 重封 + 置脏(模拟器无文件系统,落盘到此为止)
|
||
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
|
||
* 返回值:0 - 已写入;-1 - 拒写 int
|
||
* 限定条件:BSP 侧已整批预检通过
|
||
* 函数说明:真机在这里之后还有 defaultTask 扫脏写 /prot_set.bin 一段——模拟器没有
|
||
* NAND,跨复位保持这条只能在台架验(差距已登记在案,别把模拟器跑绿
|
||
* 当成"保存能落盘"的证据 → docs/测试说明/模拟器差距清单.md)
|
||
*/
|
||
static int sim_param_apply(uint8_t a_id, int32_t a_q10) {
|
||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||
return -1;
|
||
}
|
||
return prot_set_set_sealed((prot_set_id_t)a_id, a_q10);
|
||
}
|
||
|
||
/* 注入给 ui_data_param_drain() 的执行侧实现(与真机 s_param_ops 同构) */
|
||
static const ui_data_param_ops_t s_param_ops = {
|
||
sim_param_locked,
|
||
sim_param_check,
|
||
sim_param_apply
|
||
};
|
||
|
||
/*
|
||
* 函数功能:落定页面投递的定值整定请求(模拟器版装配节拍)
|
||
* 入口参数:无
|
||
* 返回值:无
|
||
* 限定条件:主循环每轮一次(窗口模式)/ 注键结束后同步一次(无头模式)——
|
||
* 两处都必须排在按键之后、渲染/截图之前,否则保存行停在 WAIT
|
||
* 函数说明:真机这段在 App/app/ui_data_feed.c 的 tick 里(模拟器不链接装配器),
|
||
* 这里只做同样的三件事:出队落定 → 值变了就重发参数帧 → 让页面看见。
|
||
* drain 每轮最多处理一条,故用 while 把在队的清空(无头模式后面没有
|
||
* 下一个 tick,必须当场排空才能拍到终态)。
|
||
* 16 次上界是纯防御:队列深度只有 4,正常永不触及
|
||
*/
|
||
static void sim_param_pump(void) {
|
||
uint32_t guard = 0u;
|
||
|
||
while (ui_data_param_drain(&s_param_ops) == 1) {
|
||
if (prot_set_checksum() != s_par_pub_sum) {
|
||
sim_publish_par_frame();
|
||
ui_content_page_invalidate(); /* 让页面重绘:保存行 OK + 新值同帧 */
|
||
}
|
||
guard++;
|
||
if (guard >= 16u) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:无窗口离屏渲染(可注入键序列)并导出末帧像素
|
||
* 入口参数: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);
|
||
/* 装置维护写通道(道F task-3):与真机同一张表形状,假件见上方实现 */
|
||
if (ui_data_maint_bind(&s_maint_ops) != 0) {
|
||
printf("[sim] UI maint bind: FAIL\n");
|
||
}
|
||
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */
|
||
if (ui_data_yt_bind(&s_yt_ops) != 0) {
|
||
printf("[sim] UI yt bind: FAIL\n");
|
||
}
|
||
if (ui_data_comm_bind(&s_comm_ops) != 0) { /* 道G task-9 通讯参数假表(两处同文 replace_all) */
|
||
printf("[sim] UI comm bind: FAIL\n");
|
||
}
|
||
if (ui_data_wave_bind(&s_wave_ops) != 0) { /* 录波通道假表(两处同文,镜像真机绑定) */
|
||
printf("[sim] UI wave bind: FAIL\n");
|
||
}
|
||
/* 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();
|
||
}
|
||
|
||
/* 同上理由落定"整定保存"请求:真机由 ui_data_feed_tick() 每 10 ms 排一条,
|
||
* 无头模式没有那个节拍,不在此当场排空,截图就会停在 WAIT(终态拍不到,
|
||
* OK/ERR/LOCK 三条判据的像素证据也就全都不成立) */
|
||
sim_param_pump();
|
||
sim_yt_pump(); /* 道F task-7:截图前清脏 = 拍 OK 黄金;--yt-nopump 停转 = 拍 WAIT 黄金(Lead 裁定④:两态都要有像素证据) */
|
||
sim_comm_pump(); /* 道G task-9:通讯页同款两态;--comm-nopump 停转拍 WAIT */
|
||
|
||
/* --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");
|
||
|
||
/* 场景 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");
|
||
|
||
/* 场景 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
|
||
* 说明:场景 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 填充)
|
||
* → 「清除记录」(syslog_request_clear / syslog_clear_state)。宿主后端与 MCU 端
|
||
* 判据相同、时序不同:模拟器单线程,请求在调用内就落地,所以本端断言的是
|
||
* 「立刻 OK」——读到 WAIT 就说明两端语义漂了。完整分支矩阵(卷占用、unlink
|
||
* 错误码、幂等、读窗口失效)在 test/host/sys_log_mcu/ 那侧用假卷注入去测。
|
||
*/
|
||
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");
|
||
/* ── 「清除记录」:宿主后端必须与 MCU 端共用同一组判据 ──
|
||
* 分支矩阵(卷占用 / unlink 错误码 / 幂等 / 读窗口失效)由假卷那边测,
|
||
* 这里只测「同一份头契约在宿主上真对得上」——宿主没有卷锁,也就没有顺延。 */
|
||
|
||
/* 从未请求过的 id 必须是 NONE:UI 据此把「没操作过」和「已清除」分开显示 */
|
||
if (syslog_clear_state(SYSLOG_SELFTEST) != SYSLOG_CLEAR_NONE) {
|
||
printf("[logtest] FAIL selftest clear state = %d (expect NONE before any ask)\n",
|
||
syslog_clear_state(SYSLOG_SELFTEST));
|
||
return 1;
|
||
}
|
||
/* 越界 id:请求被拒、状态仍 NONE,且不得牵连合法 id */
|
||
if ((syslog_request_clear((syslog_id_t)SYSLOG_COUNT) == 0)
|
||
|| (syslog_clear_state((syslog_id_t)SYSLOG_COUNT) != SYSLOG_CLEAR_NONE)
|
||
|| (syslog_clear_state(SYSLOG_SELFTEST) != SYSLOG_CLEAR_NONE)) {
|
||
printf("[logtest] FAIL out-of-range clear was accepted\n");
|
||
return 1;
|
||
}
|
||
if (syslog_request_clear(SYSLOG_RUN) != 0) {
|
||
printf("[logtest] FAIL request_clear(run)\n");
|
||
return 1;
|
||
}
|
||
if (syslog_count(SYSLOG_RUN) != 0u) {
|
||
printf("[logtest] FAIL run count after clear = %u (expect 0)\n",
|
||
(unsigned)syslog_count(SYSLOG_RUN));
|
||
return 1;
|
||
}
|
||
/* 同步落地的终态;本端读到 WAIT 就是两端语义漂移 */
|
||
if (syslog_clear_state(SYSLOG_RUN) != SYSLOG_CLEAR_OK) {
|
||
printf("[logtest] FAIL run clear state = %d (expect OK on host)\n",
|
||
syslog_clear_state(SYSLOG_RUN));
|
||
return 1;
|
||
}
|
||
/* 文件必须真没了(不是「截成 0 字节」),而且不许牵连邻居 */
|
||
{
|
||
FILE *gone = fopen("sim_run.log", "rb");
|
||
FILE *neigh = fopen("sim_selftest.log", "rb");
|
||
|
||
if (gone != NULL) {
|
||
(void)fclose(gone);
|
||
printf("[logtest] FAIL sim_run.log still there after clear\n");
|
||
return 1;
|
||
}
|
||
if (neigh == NULL) {
|
||
printf("[logtest] FAIL sim_selftest.log lost: clear touched the wrong file\n");
|
||
return 1;
|
||
}
|
||
(void)fclose(neigh);
|
||
}
|
||
/* 幂等:文件已不在,再清一次仍算成功(UI 上重复点击不该报故障) */
|
||
if ((syslog_request_clear(SYSLOG_RUN) != 0)
|
||
|| (syslog_clear_state(SYSLOG_RUN) != SYSLOG_CLEAR_OK)) {
|
||
printf("[logtest] FAIL second clear is not idempotent\n");
|
||
return 1;
|
||
}
|
||
/* 删后重建可继续写:清除不能把日志通道本身弄坏(MCU 端那条 FA_WRITE 陷阱由
|
||
* test/host/sys_log_mcu/ 的 TC-SYSLOG-112 钉,本端同判据同断言形状) */
|
||
syslog_post(SYSLOG_RUN, "after clear");
|
||
if (syslog_count(SYSLOG_RUN) != 1u) {
|
||
printf("[logtest] FAIL run count after re-post = %u (expect 1)\n",
|
||
(unsigned)syslog_count(SYSLOG_RUN));
|
||
return 1;
|
||
}
|
||
if ((syslog_get_record(SYSLOG_RUN, 0u, ts, sizeof(ts), desc, sizeof(desc)) != 0)
|
||
|| (strcmp(desc, "after clear") != 0)) {
|
||
printf("[logtest] FAIL get_record after clear: desc mismatch\n");
|
||
return 1;
|
||
}
|
||
/* 两份记录各清各的:清 selftest 不得带走 run 里刚写的那一条 */
|
||
if ((syslog_request_clear(SYSLOG_SELFTEST) != 0)
|
||
|| (syslog_count(SYSLOG_SELFTEST) != 0u)
|
||
|| (syslog_count(SYSLOG_RUN) != 1u)) {
|
||
printf("[logtest] FAIL selftest clear (run must keep its 1 record)\n");
|
||
return 1;
|
||
}
|
||
printf("[logtest] OK clear: zeroed + file gone + isolated + idempotent + rebuild\n");
|
||
printf("[logtest] PASS\n");
|
||
return 0;
|
||
}
|
||
|
||
/**
|
||
* 函数功能:模拟器入口(解析模式开关 → 窗口模式或无头自检)
|
||
* 入口参数:argc - 参数个数 int
|
||
* argv - 参数数组(--screenshot / --keys / --standby / --cfgtest /
|
||
* --logtest / --idle / --noclock) char **
|
||
* 返回值:0 - 正常退出或自检 PASS;非 0 - 失败 int
|
||
*/
|
||
int main(int argc, char **argv) {
|
||
/* 形态旗标必须先扫(道F task-3):--keys / --standby 分支命中即 return,
|
||
* 放在它们后面的循环根本走不到 —— 这是旗标看似加了、实际只在无参窗口模式
|
||
* 生效的那类"看起来做了"的坑,写在这里省一个人重新踩 */
|
||
for (int i = 1; i < argc; i++) {
|
||
if (strcmp(argv[i], "--noclock") == 0) {
|
||
s_noclock = 1; /* 把这台机器装成"墙钟无效"(RTC 异常/未对时) */
|
||
}
|
||
if (strcmp(argv[i], "--yt-nopump") == 0) {
|
||
s_yt_nopump = 1; /* 落盘 pump 停转:拍"已受理未落盘"(WAIT)黄金专用 */
|
||
}
|
||
if (strcmp(argv[i], "--comm-nopump") == 0) {
|
||
s_comm_nopump = 1; /* 道G task-9:通讯页 WAIT 态黄金专用(同款语义独立开关) */
|
||
}
|
||
if (strcmp(argv[i], "--wave-empty") == 0) {
|
||
s_wave_empty = 1; /* 录波回放空态黄金专用:不建段且触发拒收 */
|
||
}
|
||
if (strcmp(argv[i], "--wave-bad") == 0) {
|
||
s_wave_bad = 1; /* 「该段不可读」黄金专用:数据区翻一点让真 CRC 判废 */
|
||
}
|
||
}
|
||
|
||
/* 截图/自检模式:--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);
|
||
/* 装置维护写通道(道F task-3):与真机同一张表形状,假件见上方实现 */
|
||
if (ui_data_maint_bind(&s_maint_ops) != 0) {
|
||
printf("[sim] UI maint bind: FAIL\n");
|
||
}
|
||
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页假源(形态镜像真机绑定) */
|
||
if (ui_data_yt_bind(&s_yt_ops) != 0) {
|
||
printf("[sim] UI yt bind: FAIL\n");
|
||
}
|
||
if (ui_data_comm_bind(&s_comm_ops) != 0) { /* 道G task-9 通讯参数假表(两处同文 replace_all) */
|
||
printf("[sim] UI comm bind: FAIL\n");
|
||
}
|
||
if (ui_data_wave_bind(&s_wave_ops) != 0) { /* 录波通道假表(两处同文,镜像真机绑定) */
|
||
printf("[sim] UI wave bind: FAIL\n");
|
||
}
|
||
/* 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();
|
||
|
||
/* 落定定值整定请求(镜像真机 ui_data_feed_tick 里的 ui_data_param_drain:
|
||
* 出队 → prot_set 落值 → 重发参数帧);队列空时只是一次计数判断 */
|
||
sim_param_pump();
|
||
sim_yt_pump(); /* 道F task-7:窗口模式同样每轮无条件清脏(真机 50 ms 落盘的镜像) */
|
||
sim_comm_pump(); /* 道G task-9:通讯页脏位同款镜像 */
|
||
|
||
/* 配置文件轮询(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;
|
||
}
|