Files
STM32F4-Base/simulator/main.c
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(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 条仍全过
2026-10-29 19:23:15 +08:00

1800 lines
75 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 模块名称:PC 模拟器入口(simulator/main.c)
* 模块功能:在 PC 上以 SDL2 跑**与固件同一套** UI 源码,提供窗口调试与多种无头自检模式
* 适用平台:Windows(CMake + SDL2)
* 作者:王建锋
* 创建日期:2026-08-31
* 修改记录:
* v1.0 2026-08-31 王建锋 创建:基于 lv_port_pc_vscode 适配
*
* @file main.c
* @brief LVGL 模拟器入口 — 160x160 1bpp 单色菜单 UI
* @note 基于 lv_port_pc_vscode 适配,用于 PC 上 UI 开发与调试
*
* 键盘映射:
* ↑/↓/←/→ → 方向键导航
* Enter → 确认/进入
* Escape → 返回
*
* 截图模式(无窗口离屏渲染,导出像素供比对):
* lvgl_sim.exe --screenshot shot.bin
* 走与 MCU 端一致的离屏软件渲染(ARGB8888 缓冲 + mono 主题),
* 渲染首帧后将 160x160 像素按 BGRA 顺序写入 shot.bin(102400 字节),随即退出。
*
* 键序截图模式(无窗口 + 注入按键序列后截图,用于级联菜单无头回归):
* lvgl_sim.exe --keys <seq> shot.bin
* seq 字符:u=↑ d=↓ l=← r=→ e=Enter s=Esc(其余字符忽略)
* 每键经 lv_group_send_data 注入(与 keypad 输入设备同路径),驱动 5 帧后取快照
*
* 配置轮询自检模式(无窗口、进程内闭环、退出码 0=PASS / 非0=FAIL):
* lvgl_sim.exe --cfgtest
* 验证链:net_cfg_load(只读 INI;旧 NCFG 档迁移已于 2026-10-10 删除)
* → poll 建基线 → 伪造外部编辑
* (原子写新文档,等价 FTP 上传/文本编辑器)→ poll 两次(4s 沉降)
* → 断言 RAM 副本已按新文件重载
*
* 日志引擎自检模式(无窗口、进程内闭环、退出码 0=PASS / 非0=FAIL):
* lvgl_sim.exe --logtest
* 验证链:post N 条 → count/get 回读断言 + 宿主文件字节级校验
* (64B 定长、ISO 时间戳、epoch 字段、NUL 填充)
* → 「清除记录」:计数归零 + 文件真没了 + 只动自己那份 + 幂等 + 删后还能继续写
*
* 待机屏截图模式(无窗口,强制进入待机后截图):
* lvgl_sim.exe --standby shot.bin
*
* 窗口模式空闲超时覆盖:lvgl_sim.exe --idle <秒>(默认 60s)
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#ifdef _MSC_VER
#include <Windows.h>
#include <winsock2.h>
#else
#include <unistd.h>
#endif
#define SDL_MAIN_HANDLED
#include <SDL.h>
#include "lvgl.h"
#include "sim_port.h"
#include "ui_menu.h"
#include "ui_standby.h"
#include "ui_net_sink.h"
#include "net_cfg.h"
#include "cfg_file.h"
#include "cfg_file_store.h"
#include "sys_log.h"
#include "ui_w_records.h" /* task-6 记录 reader 注入点(镜像 App/task/ui_task.c 的绑定) */
#include "ui_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;
}