diff --git a/Drivers/BSP/UI/ui_content_page.c b/Drivers/BSP/UI/ui_content_page.c index 7bfc062a..ad0dac6b 100644 --- a/Drivers/BSP/UI/ui_content_page.c +++ b/Drivers/BSP/UI/ui_content_page.c @@ -36,6 +36,9 @@ * id=%u 更可定位);绘制/按键/几何一律未动 * 2026-11-02 DTU 项目组 新增 ui_content_page_current_desc(#69 板端导航身份查询: * 页 id 从"只有串口开/关沿可见"升级为可按帧查询) + * 2026-11-13 DTU 项目组 实时页自动刷新:描述符 auto_ms>0 的页在打开期间由框架 + * 持一只 lv_timer 轮询发布序号(默认通道一 ui_data_rt_seq), + * 序号变过才整页失效;新 API ui_content_page_auto_poll */ /* 头文件包含区 */ @@ -55,6 +58,8 @@ #include "ui_tokens.h" +#include "ui_data.h" + #include "lvgl.h" #define DBG_TAG "[UI_CONTENT_PAGE]" @@ -85,6 +90,8 @@ static uint16_t s_sel; /* 列表页选中行( static uint8_t s_perm; /* 本次授予权限(ui_perm_t;开页时装载,关页重置) */ static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */ static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */ +static lv_timer_t *s_auto_timer; /* 自动失效轮询定时器(仅 auto_ms>0 的页打开期间存在;关页即销) */ +static uint32_t s_auto_seq; /* 上一拍见到的发布序号(开页时装基准,开页本身不多刷一帧) */ /* 容器样式:flat(透明 + 无边框/圆角/内边距)取自共享样式表 ui_style.c—— * 与 ui_menu_view / ui_standby 共用同一 const 对象(原先三处各定义一份逐字相同 @@ -469,6 +476,42 @@ static void cp_key_cb(lv_event_t *e) { } } +/* + * 函数功能:自动刷新轮询一拍(契约见 ui_content_page.h;判定与序号基准只在这里一份) + * 入口参数:无 + * 返回值:无 + * 限定条件:uiTask 上下文;无页打开 / 本页未订阅(auto_ms=0)时空操作 + * 函数说明:不在此拷帧——失效后由框架重绘走页面 refresh 的 seq 门控取新帧(§5.4 + * 页面私有缓存纪律不变),框架对数据内容零感知,只认序号 + */ +void ui_content_page_auto_poll(void) { + const content_page_def_t *d = s_open; + uint32_t now; + + if (d == NULL || d->auto_ms == 0u) { + return; + } + now = (d->auto_seq != NULL) ? d->auto_seq() : ui_data_rt_seq(); + if (now == s_auto_seq) { + return; /* 数据没变:零失效(省电屏带宽纪律) */ + } + s_auto_seq = now; + ui_content_page_invalidate(); /* 序号变过才整页失效 */ +} + +/* + * 函数功能:自动失效轮询定时器回调(薄封装;一切判定在 ui_content_page_auto_poll) + * 入口参数:t - 定时器对象(未用) lv_timer_t * + * 返回值:无 + * 限定条件:lv_timer_handler 内(uiTask 上下文)——与其他 LVGL 调用同线程同串行点 + * 函数说明:standby 期间内容页对象仍在(待机盖屏不关页),此时失效后台容器只是 + * 多一次脏区合并,待机屏自身重绘节律不受影响(实测待机像素回归绿) + */ +static void cp_auto_timer_cb(lv_timer_t *t) { + (void)t; + ui_content_page_auto_poll(); +} + /* * 函数功能:打开指定内容页(建屏 + 切屏 + 焦点置页面容器) * 入口参数:desc - 页面描述符(取自 ui_page_registry_for / ui_page_registry_password) @@ -517,6 +560,19 @@ bool ui_content_page_open(const content_page_def_t *desc, uint8_t perm) { desc->init_cb(); /* 页面模块重置运行态(编辑模式/光标/保存标志等) */ } + /* 自动失效轮询(实时页自动刷新):定时器随页建销,页面模块零定时器。基准取 + * "开页瞬间"的序号——开页首帧绘制本就已取过数,不该再为基线多补一次重绘; + * 之后每拍只在序号变过时失效。创建失败(LVGL 池耗尽)只降级回"按键才重绘" + * 旧行为并留痕,不挡开页(同 ui_w_setview 轮询定时器的降级口径)。 */ + s_auto_timer = NULL; + if (desc->auto_ms != 0u) { + s_auto_seq = (desc->auto_seq != NULL) ? desc->auto_seq() : ui_data_rt_seq(); + s_auto_timer = lv_timer_create(cp_auto_timer_cb, (uint32_t)desc->auto_ms, NULL); + if (s_auto_timer == NULL) { + DBG_ERROR("auto timer create failed (id=%s): key-only refresh fallback", desc->id); + } + } + /* 焦点/键入口:入默认输入组并获焦(先于切屏,保证按键立即由本模块接管) */ lv_group_t *g = lv_group_get_default(); if (g != NULL) { @@ -550,6 +606,13 @@ void ui_content_page_close(void) { DBG_INFO("page close: id=%s", s_open->id); + /* 自动失效定时器随页销毁:留着它,下一拍就会去失效即将释放的 s_cont + * (回调与关页同在 uiTask 串行点,此处删表即绝后患) */ + if (s_auto_timer != NULL) { + lv_timer_delete(s_auto_timer); + s_auto_timer = NULL; + } + lv_obj_delete(s_cont); /* 先删容器 */ lv_obj_delete(s_scr); /* 再删屏 */ diff --git a/Drivers/BSP/UI/ui_content_page.h b/Drivers/BSP/UI/ui_content_page.h index 6b566633..0e44e56a 100644 --- a/Drivers/BSP/UI/ui_content_page.h +++ b/Drivers/BSP/UI/ui_content_page.h @@ -30,6 +30,11 @@ * ui_page_registry.c(菜单树 #page= 注记为唯一真源), * 逻辑层手写 menu_content_page_for() 随之删除; * 描述符新增 id 字段(ASCII 页名,供日志/诊断定位是哪一页) + * 2026-11-13 DTU 项目组 实时页自动刷新:描述符新增 auto_ms/auto_seq 两字段, + * 新 API ui_content_page_auto_poll——框架在页面打开期间持 + * 一只轮询定时器,发布序号变过才整页失效,数据没变零重绘; + * 页面模块不再各自造定时器(周期账见 ui_page_ac.c 的 + * AC_AUTO_MS 注与 UI_数据供给设计说明.md §5.4) */ #ifndef UI_CONTENT_PAGE_H #define UI_CONTENT_PAGE_H @@ -80,6 +85,15 @@ typedef struct { uint16_t (*count_cb)(void); /* 动态行数(记录页) */ void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */ bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */ + /* —— 自动失效轮询(全可选;auto_ms=0 = 只在按键重绘,未订阅页行为逐位不变)—— + * auto_ms>0:本页打开期间框架持一只 lv_timer(周期 auto_ms ms,uiTask 上下文随 + * lv_timer_handler 走),每拍取发布序号(auto_seq(),NULL = ui_data_rt_seq() + * 即通道一),序号与上一拍不同才整页失效——取数门控仍是页面 refresh 里的 + * seq 判断(UI_数据供给设计说明.md §5.4 的页面私有缓存约定原样有效)。 + * 关页即销定时器;页面模块禁止自造定时器。周期取值账见 ui_page_ac.c 的 + * AC_AUTO_MS 注释(发布者 rt 每 200ms 一帧,见同文 §5.2)。 */ + uint16_t auto_ms; /* 轮询周期(ms);0 = 不启用自动刷新 */ + uint32_t (*auto_seq)(void); /* 新鲜度序号源;NULL = ui_data_rt_seq() */ } content_page_def_t; /* @@ -154,4 +168,16 @@ bool ui_content_page_is_editable(void); */ uint16_t ui_content_page_selected_row(void); +/* + * 函数功能:自动刷新轮询一拍(生产由框架定时器回调调用;无头模拟器 --rtlive 自检 + * 没有定时器节拍、直调本函数顶替——两个入口共用同一份判定,规则不会两套) + * 入口参数:无 + * 返回值:无 + * 限定条件:uiTask 上下文(lv_timer_handler 内或其等价串行点) + * 函数说明:1. 无页打开 / 本页 auto_ms=0:空操作; + * 2. 序号与上一拍相同:不失效——数据没变的一次整屏重绘是纯支出(屏带宽/功耗); + * 3. 序号变了:记下新基准并整页失效,真正取数仍走页面 refresh 的 seq 门控 + */ +void ui_content_page_auto_poll(void); + #endif /* UI_CONTENT_PAGE_H */ diff --git a/Drivers/BSP/UI/ui_page_ac.c b/Drivers/BSP/UI/ui_page_ac.c index 515f5bdf..c7211b93 100644 --- a/Drivers/BSP/UI/ui_page_ac.c +++ b/Drivers/BSP/UI/ui_page_ac.c @@ -50,6 +50,19 @@ #define AC_Q_BAD (UI_DATA_Q_IV | UI_DATA_Q_NT | UI_DATA_Q_RAW | UI_DATA_Q_SB | \ UI_DATA_Q_BL | UI_DATA_Q_OV) +/* 自动刷新轮询周期(ms;框架级机制,字段语义见 ui_content_page.h 的 auto_ms 注)。 + * 取 300 的账(允许区间 250~500;发布者 rt 通道每 200ms 一帧,见 + * UI_数据供给设计说明.md §5.2): + * ① 必须 ≥ 发布节拍 200ms——轮得比发得快只是空转,白耗 uiTask; + * ② 避开 400ms(恰为 2×200):采样拍与发布拍锁相,感知滞后最坏 800ms; + * ③ 300 = 15 × LVGL 刷新周期 20ms(LV_DEF_REFR_PERIOD 两端同值):失效恰好落进 + * 下一次整刷合并,不多触发一帧;最坏屏上滞后 = 200 + 300 = 500ms,对一周波 + * 相量级的电气量读数足够,读表不觉得卡; + * ④ 250 只比 300 早至多 50ms(新帧首先受发布节拍约束),代价是多 20% 空转拍; + * 500 最坏滞后升到 700ms,数值跳变看着像"翻页"。 + * 稳态开销为零:序号没变不失效不重绘(ui_content_page_auto_poll 纪律)。 */ +#define AC_AUTO_MS 300u + /* 静态变量定义区 */ /* 行名表(Flash) */ @@ -228,4 +241,7 @@ const content_page_def_t ui_page_ac_desc = { .rows = NULL, .init_cb = ac_init, .refresh = ac_refresh, + /* 自动刷新(task-2):框架轮询通道一发布序号,来新帧才整页失效——本页不自造 + * 定时器;auto_seq 缺省 = ui_data_rt_seq(),与 ac_frame_ready 的取数门控同源。 */ + .auto_ms = AC_AUTO_MS, }; diff --git a/docs/UI设计/UI_数据供给设计说明.md b/docs/UI设计/UI_数据供给设计说明.md index 70bdebb5..247b44f4 100644 --- a/docs/UI设计/UI_数据供给设计说明.md +++ b/docs/UI设计/UI_数据供给设计说明.md @@ -340,6 +340,26 @@ static void pg_refresh(uint16_t row, char *name, char *value) { > 用 `ui_data_rt_seq()` 门控而不是"row==0 才刷新",是为了避免依赖"框架总从第 0 行开始绘制"这一隐式时序假设——框架换绘制顺序时页面不会静默出错。 +### 5.5 框架级自动失效(实时页不靠按键也有新数) + +§5.4 的 seq 门解决"取数不冗余",不解决"什么时候该重绘":框架旧行为是**按键触发绘制才走 `refresh()`**——`rt` 帧每 200 ms 在发,人停在「交流量」页不动按键,屏上数值就是冻结的。 + +新机制(task-2):`content_page_def_t` 尾部新增两个可选字段,定时器由**框架 `ui_content_page` 统一持有**(开页创建、关页销毁,同时最多一只),页面模块禁止自造轮询定时器: + +| 字段 | 语义 | +|---|---| +| `uint16_t auto_ms` | 0 = 关闭(保持旧行为;指定初始化未写即 0);>0 = 该页打开期间的轮询周期 | +| `uint32_t (*auto_seq)(void)` | 新鲜度来源;NULL(缺省)= `ui_data_rt_seq()`;读 `info` 通道的页面可填 `ui_data_info_seq()` | + +每拍框架调 `ui_content_page_auto_poll()`(回调经 uiTask 主循环 `lv_timer_handler` 触发,与绘制钩子同上下文):**序号变了才整页失效,没变零动作**——不拷帧、不失效、不重绘,稳态开销与旧行为一致;取数门控仍走 §5.4 的页面私有 seq 缓存,失效只是把"下次按键才有机会读新帧"变成"到点就有机会读新帧"。 + +当前口径: + +- **已启用**:`ac` 交流量页 `.auto_ms = AC_AUTO_MS`(300 ms;250~500 ms 允许区间的取值账目详述在 `ui_page_ac.c` 的 `AC_AUTO_MS` 注释——要点:轮询不得快于 200 ms 发布节拍、避开 400 ms 与其锁相、300 = 15 × LVGL 整刷周期 20 ms(两端 `LV_DEF_REFR_PERIOD` 同值),最坏屏上滞后 200 + 300 = 500 ms)。 +- **不迁移**:待机屏 `ui_standby.c`(自持 1 s 整屏定时器 + seq 门控)、整定视图 `ui_w_setview.c`(`sv_poll` 定时器盯 `par` 通道)——两处各自已有节拍,纳入框架只会多一只定时器。 +- **未接入(待 lead/用户裁定)**:`dc`/`yx`/`clock`(读 `rt`)与 `device_info`/`runreport`(读 `info`)仍是"按键才取新数";接入只差描述符一行 `.auto_ms`(info 页另加 `.auto_seq`)。 + +**回归确定性**:无头模式(`--keys`/74 张黄金参考)从不跑 `lv_timer_handler`,定时器不可能在截屏前触发,且仿真仍是"一次发布冻结帧"——黄金参考逐字节不变(`check_content_pages.py` 实证)。自动失效路径的验证走新开关 `--rtlive <键序> `:开页截 A → 发布第 1 号确定性斜坡帧 → **直接调用** `ui_content_page_auto_poll()`(等价定时器到点)→ 截 B;A≠B 即证明"零按键、新帧落屏"。窗口模式(真机行为仿真)每 200 ms 发一帧斜坡值,屏上数值自己会走。 --- ## 6. 写路径(定值整定) diff --git a/docs/UI设计/内容页架构.md b/docs/UI设计/内容页架构.md index e76ec2fa..9a1fe74b 100644 --- a/docs/UI设计/内容页架构.md +++ b/docs/UI设计/内容页架构.md @@ -96,15 +96,19 @@ flowchart TD | `bool ui_content_page_open(id)` | 开页 | ① 查 `s_defs[id]`:NULL → 打 `ERROR` 日志并返回 false(枚举/表漂移防护)② `lv_obj_create(NULL)` 建屏 + 全屏容器(flat 常量样式 + `IGNORE_LAYOUT` + `CLICK_FOCUSABLE`)③ 容器**先**入默认输入组并获焦 ⓸ `lv_screen_load` 切屏 | | `void ui_content_page_close()` | 关页 | ① 焦点**先**回菜单持久容器(防输入组焦点悬空)② 切回菜单屏(菜单屏对象未动,原样还原)③ 再删容器+屏,池内存回落 | | `void ui_content_page_invalidate()` | 整页重绘 | 失效页面容器;供表单/操作页自身状态变化后调用(无页打开时空操作) | -| `bool ui_content_page_is_open()` | 查询 | `s_open != CONTENT_PAGE_COUNT` | +| `void ui_content_page_auto_poll()` | 自动失效轮询 | 供框架定时器(`auto_ms>0` 的页打开期间才存在)每拍调用:发布序号(`auto_seq()`,NULL = `ui_data_rt_seq()`)与上一拍不同才整页失效,没变零动作;无页打开 / `auto_ms=0` 时空操作。模拟器 `--rtlive` 直接调它做确定性验证(机制见 `UI_数据供给设计说明.md` §5.5) | +| `bool ui_content_page_is_open()` | 查询 | `s_open != NULL` | -框架持有的运行状态(全部 4 个变量,约 20 B): +框架持有的运行状态(基础 5 个 + 自动失效 2 个,与 ui_content_page.c 静态块一致): ```c -static content_page_id_t s_open; /* 当前打开的页(CONTENT_PAGE_COUNT = 未打开) */ +static const content_page_def_t *s_open; /* 当前打开页的描述符(NULL = 未打开) */ static uint16_t s_sel; /* 列表页选中行(全局下标,跨页连续;每次进入清零) */ +static uint8_t s_perm; /* 本次开页授予的权限(ui_perm_t;开页时装载,关页重置) */ static lv_obj_t *s_scr; /* 页面屏幕(进入时建、返回时毁) */ static lv_obj_t *s_cont; /* 页面全屏容器(焦点/键入口 + 自绘载体) */ +static lv_timer_t *s_auto_timer; /* 自动失效轮询定时器(仅 auto_ms>0 的页打开期间存在;关页即销) */ +static uint32_t s_auto_seq; /* 上一拍见到的发布序号(开页时装基准,开页本身不多刷一帧) */ ``` ### 3.2 页面生命周期:一次 ENTER → 一次 ESC @@ -249,6 +253,9 @@ typedef struct { uint16_t (*count_cb)(void); /* 动态行数(记录页) */ void (*draw_cb)(lv_layer_t *layer, const lv_area_t *clip); /* 表单/操作页:自定义内容 */ bool (*key_cb)(lv_key_t key); /* 页面自有键;true = 页已处理 */ + /* —— 自动失效轮询(auto_ms=0 = 只在按键重绘,未订阅页行为逐位不变;见 UI_数据供给设计说明.md §5.5)—— */ + uint16_t auto_ms; /* 轮询周期(ms);0 = 不启用自动刷新 */ + uint32_t (*auto_seq)(void); /* 新鲜度序号源;NULL = ui_data_rt_seq() */ } content_page_def_t; ``` @@ -273,6 +280,7 @@ typedef struct { | `rows` | `const cp_row_t *` | 静态数据表(Flash) | 6 行 | 24 行 | 3 行 | NULL | | `fixed_cur_page` / `fixed_total_pages` | `uint8_t` | 信息页页脚固定值 | 10 / 1 | — | — | — | | `refresh` / `count_cb` / `draw_cb` / `key_cb` | 函数指针 | 扩展钩子 | 全 NULL | 全 NULL | 全 NULL | draw_cb + key_cb(余 NULL) | +| `auto_ms` / `auto_seq` | `uint16_t` / 函数指针 | 自动失效轮询周期 / 新鲜度源(`UI_数据供给设计说明.md` §5.5) | 0 / NULL | **300 / NULL** | 0 / NULL | 0 / NULL | 扩展钩子(机制已实现;密码页用 `draw_cb` + `key_cb`,其余 3 页未用): @@ -283,6 +291,8 @@ typedef struct { | `draw_cb` | `void (lv_layer_t *layer, const lv_area_t *clip)` | 绘制链末尾(内置区块之后) | 表单/操作页自定义内容 | | `key_cb` | `bool (lv_key_t key)` | 键分发时(ESC 之后、内置 ↑↓ 之前) | 页面自有键(ENTER 确认等);返回 true = 页已处理 | +> 自动失效(`auto_ms`/`auto_seq`)**不是页面钩子**:定时器、轮询节拍与序号比较全在框架(`ui_content_page_auto_poll`),页面只声明周期与可选序号源;口径见 `UI_数据供给设计说明.md` §5.5。 + ### 4.1 表单/操作页首个应用:密码页(已落地) 实机(淮源电力 F30)已有"用户密钥"页:进入某些子叶面前须先输入密码(实机照片见 @@ -493,3 +503,4 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash | 2026-10-26 | 验证与回归段的页数口径修正为 **27 页**黄金参考(`simulator/tests/ref/` 入库 27 个 `shot_*.bin`,含整定 9 态);上方变更记录里的「11 页」是当时事实不动 | | 2026-10-26 | 门禁常量修正为 5368 B / 5448 B(= 两个回归脚本头部现值);本文件正文表早已是 5368/5448,只这一处残留 2026-10-11 裁剪 LVGL 控件类之前的 4784/4864 | | 2026-10-26 | 验证与回归段的黄金参考页数 **27 → 40 页**(UI 三页落地后 `simulator/tests/ref/` 实数 40 张);上方变更记录里的 11 / 27 是各当时下限,保留 | +| 2026-11-13 | 框架级自动失效(task-2):描述符尾部新增 `auto_ms`/`auto_seq`(当前仅 ac 页启用 300 ms),新增框架 API `ui_content_page_auto_poll()`(§3.1);§3.1 静态块与代码同步(补 `s_perm` 与自动失效 2 变量);回归脚本新增 `--rtlive` 零按键落屏检查(74 张黄金参考逐字节不变,实测 PASS);机制详述在 `UI_数据供给设计说明.md` §5.5 | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index bb94a8a0..2d585ce8 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 288,148 B (281.39 KB) | 1,048,576 B (1024 KB) | 27.5% | -| RW_IRAM1 | 97,240 B (94.96 KB) | 131,072 B (128 KB) | 74.2% | +| ER_IROM1 | 288,624 B (281.86 KB) | 1,048,576 B (1024 KB) | 27.5% | +| RW_IRAM1 | 97,248 B (94.97 KB) | 131,072 B (128 KB) | 74.2% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **290,228 B (283.4 KB)** | 1,048,576 B (1024 KB) | **27.7%** | +| **Flash (ROM 合计)** | **290,712 B (283.9 KB)** | 1,048,576 B (1024 KB) | **27.7%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 042cf628..4f7872f5 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,16 +15,16 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 288,148 B | 1,048,576 B | 27.5% | 参考 | +| ER_IROM1 (Flash) | 288,624 B | 1,048,576 B | 27.5% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 97,240 B | 131,072 B | 74.2% | PASS | +| RW_IRAM1 | 97,248 B | 131,072 B | 74.2% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,958 B | 12 KB | ⚠ 超 1.6 KB | | 存储 | 9,144 B | 12 KB | OK | -| UI | 36,723 B | 16 KB | ⚠ 超 19.9 KB | +| UI | 36,731 B | 16 KB | ⚠ 超 19.9 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | | 应用 | 26,609 B | 8 KB | ⚠ 超 18.0 KB | | 其它 | 10,792 B | — | 参考 | diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 139cad88..a67acea0 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -21,4 +21,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-11-09 255160 31204 2080 152152 286364 27.3 96808 73.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;nand_ftl.o=8288;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-10 255160 31204 2080 152152 286364 27.3 96808 73.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;nand_ftl.o=8288;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-11 256024 31760 2080 152368 287784 27.4 97024 74.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;nand_ftl.o=8288;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;wave_ring.o=5332;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 -2026-11-13 256428 31720 2080 152584 288148 27.5 97240 74.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;nand_ftl.o=8288;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;wave_ring.o=5332;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 +2026-11-13 256588 32036 2088 152584 288624 27.5 97248 74.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;nand_ftl.o=8288;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;wave_ring.o=5332;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 563832d9..51d7b7c7 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,17 +1,17 @@ { - "built_at": "2026-11-13T13:11:29+08:00", - "fingerprint": "eb152c8e20a33b63da91fd3b5e32877b83b581c43d0c4956375ed13b6531db3f", + "built_at": "2026-11-13T14:23:05+08:00", + "fingerprint": "f44730aca08c9f35b9eec8a83322b342ee21a2280a94ce2c277b5c62a26fb442", "fingerprint_files": 1479, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "40a2816a9971928d1ca6fa218921b86dae79bd63", + "git_head": "5bab248deeac14b0d6235e0fe312bcd9388a5e0a", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1794542035, + "map_mtime": 1794549805, "pct": { - "ER_IROM1": 27.48, + "ER_IROM1": 27.53, "RW_CCM": 87.89, "RW_IRAM1": 74.19, "UNINIT_CCM": 18.75 @@ -19,7 +19,7 @@ "regions": { "ER_IROM1": { "max": 1048576, - "size": 288148 + "size": 288624 }, "RW_CCM": { "max": 65280, @@ -27,7 +27,7 @@ }, "RW_IRAM1": { "max": 131072, - "size": 97240 + "size": 97248 }, "UNINIT_CCM": { "max": 256, @@ -37,9 +37,9 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 256428, - "ro_data": 31720, - "rw_data": 2080, + "code": 256588, + "ro_data": 32036, + "rw_data": 2088, "zi_data": 152584 }, "threshold_pct": 92.0, diff --git a/simulator/main.c b/simulator/main.c index a22032ae..457d8e8f 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -6,6 +6,8 @@ * 创建日期:2026-08-31 * 修改记录: * v1.0 2026-08-31 王建锋 创建:基于 lv_port_pc_vscode 适配 + * v1.1 2026-11-13 DTU 项目组 task-2:rt 帧装配收口成 sim_fill_rt_frame 唯一装配口 + + * --rtlive 两态自检 + 窗口模式 200ms 节拍重发 live 帧 * * @file main.c * @brief LVGL 模拟器入口 — 160x160 1bpp 单色菜单 UI @@ -26,6 +28,13 @@ * seq 字符:u=↑ d=↓ l=← r=→ e=Enter s=Esc(其余字符忽略) * 每键经 lv_group_send_data 注入(与 keypad 输入设备同路径),驱动 5 帧后取快照 * + * 实时页自动刷新自检模式(无窗口,两态比对,退出码 0=两枪都出图): + * lvgl_sim.exe --rtlive shotA.bin shotB.bin + * 先注键开到目标页拍 A(冻结帧 n=0);随后发布一帧确定性斜坡帧(仿真设备还在 + * 采集,发布序号前进)并直调生产判定体 ui_content_page_auto_poll(无头无定时器 + * 节拍,直调顶替),再拍 B。A 不等于 B 逐字节成立 = 不按任何键,新数据照样刷屏; + * 像素差异断言在 simulator/tests/check_content_pages.py 里做 + * * 配置轮询自检模式(无窗口、进程内闭环、退出码 0=PASS / 非0=FAIL): * lvgl_sim.exe --cfgtest * 验证链:net_cfg_load(只读 INI;旧 NCFG 档迁移已于 2026-10-10 删除) @@ -64,6 +73,7 @@ #include "sim_port.h" #include "ui_menu.h" #include "ui_standby.h" +#include "ui_content_page.h" /* task-2:--rtlive 直调框架自动刷新判定体 */ #include "ui_net_sink.h" #include "net_cfg.h" #include "cfg_file.h" @@ -972,6 +982,123 @@ static void sim_publish_par_frame(void) { s_par_pub_sum = par.checksum; } +/* + * 函数功能:组装一帧 rt 快照——黄金冻结帧(n=0)与 live 斜坡帧(n>=1)共用这唯一 + * 装配口,仿真假数据不存在第二份 + * 入口参数:p_rt - 目标帧(调用方持有,本函数先整帧清零) ui_data_rt_t * + * n - 帧号;0 = 黄金冻结帧(逐字节等于重构前,74 张黄金参考建立在 + * 它上面),>0 = 确定性斜坡帧(三角波 0..5..0 周期 11 帧,无随机, + * 仅 --rtlive 自检与窗口模式节拍使用) uint32_t + * 返回值:无 + * 限定条件:--noclock / --epoch 形态旗标已就位(时钟段读它们) + * 函数说明:随 n 变的只有显示相关字段(相量/遥测/tick/墙钟秒),品质位与遥信位图 + * 原样冻结——模拟的是设备还在采集,不是设备自己变坏了 + */ +static void sim_fill_rt_frame(ui_data_rt_t *p_rt, uint32_t n) { + uint8_t i; + + (void)memset(p_rt, 0, sizeof(*p_rt)); + p_rt->valid = 1u; + p_rt->eng = UI_DATA_ENG_PIN; + p_rt->tick_ms = 0u; + /* 遥测下标 = adc_semantic_t 语义序(UA/UB/UC/UN/IA/IB/IC/IN/TEMP)。 + * 模拟器不 include adc_input.h,故这里用语义序注释对照,不写符号名 */ + p_rt->yc[0].val = 0.8123f; /* UA 基波(引脚电压域) */ + p_rt->yc[0].qds = 0u; + p_rt->yc[1].val = 0.0f; /* UB:V0.1 硬件削顶无采样源 */ + p_rt->yc[1].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; + p_rt->yc[2].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* UC 同上 */ + p_rt->yc[3].val = 0.0054f; /* UN 零序电压基波 */ + p_rt->yc[3].qds = 0u; + p_rt->yc[4].val = 0.0456f; /* IA 基波 */ + p_rt->yc[4].qds = 0u; + p_rt->yc[5].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IB */ + p_rt->yc[6].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IC */ + p_rt->yc[7].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IN */ + p_rt->yc[8].val = 2048.0f; /* TEMP:码值(不参与线性标定) */ + p_rt->yc[8].qds = UI_DATA_Q_RAW; + /* 相量实例槽(序 = phasor k_inst[]:IA-F1/UA-F1/UN-F1/IA-F2/IA-F5) */ + p_rt->ph_n = 5u; + p_rt->ph[0].val = 0.0456f; + p_rt->ph[1].val = 0.8123f; + p_rt->ph[2].val = 0.0054f; + p_rt->ph[3].val = 0.0017f; /* IA 二次谐波(涌流制动监视量) */ + p_rt->ph[4].val = 0.0008f; /* IA 五次谐波(选线用) */ + for (i = 0u; (i < UI_DATA_PH_MAX) && (i < p_rt->ph_n); i++) { + p_rt->ph[i].qds = 0u; + } + for (; i < UI_DATA_PH_MAX; i++) { + p_rt->ph[i].qds = UI_DATA_Q_IV; /* 未发布的槽:无效,页面显示 "--" */ + } + p_rt->yx_bits = 0u; /* 固定帧无动作态 */ + p_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 让画面多两个汉字好看:那会让黄金参考显示成"这台设备 + * 有开关位置遥信",而真机上没有(屏上的话必须与本机一致,同页脚页码那条裁定) */ + p_rt->yx_dp = 0x0FULL; + p_rt->freq_valid = 0u; /* eng=0 期间幅值门恒失效 → 无新鲜读数 */ + p_rt->freq_hz = 0.0f; + /* 墙钟两态(道F task-3):--noclock 把这台机器装成"RTC 异常/未对时"——只有 + * 这样才照得到时钟设置页的 "--" + "无时钟"徽标形态;面板设过时以面板值为准, + * 这就是页面上"回读证据"的唯一来源(真机那边是 SD2506 回读 + 快照层重发) */ + p_rt->clock_ok = (s_noclock == 0) ? 1u : 0u; + if (s_noclock != 0) { + p_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, p_rt->clock, sizeof(p_rt->clock)); + } else { + p_rt->clock[0] = 0; + p_rt->clock_ok = 0u; + } + } else { + (void)snprintf(p_rt->clock, sizeof(p_rt->clock), "%s", "2026-10-25 08:00:00"); + } + + /* live 斜坡(n=0 完全不进本段:黄金像素逐位不变)。三角波避免单调漂移——窗口 + * 模式跑多久数值域都有界;相量实例与对应遥测写同值(镜像 App/app/ui_data_feed.c + * 的同源纪律)。freq 保持 freq_valid=0:eng=0 期间幅值门恒失效,不为了让 Hz 行 + * 看着在动就伪造新鲜读数 */ + if (n != 0u) { + uint32_t k = n % 11u; + uint32_t s = (k <= 5u) ? k : (10u - k); + float f = (float)s; + + p_rt->tick_ms = n * 200u; + p_rt->yc[0].val = 0.8123f + (0.0020f * f); + p_rt->yc[3].val = 0.0054f + (0.0002f * f); + p_rt->yc[4].val = 0.0456f + (0.0004f * f); + p_rt->ph[0].val = p_rt->yc[4].val; + p_rt->ph[1].val = p_rt->yc[0].val; + p_rt->ph[2].val = p_rt->yc[3].val; + p_rt->ph[3].val = 0.0017f + (0.0001f * f); + p_rt->ph[4].val = 0.0008f + (0.0001f * f); + if ((p_rt->clock_ok != 0u) && (p_rt->clock[0] != (char)0) && (s_sim_epoch == 0u)) { + (void)snprintf(p_rt->clock, sizeof(p_rt->clock), "2026-10-25 08:00:%02u", + (unsigned int)(n % 60u)); + } + } +} + +/* + * 函数功能:发布一帧 live 斜坡快照(--rtlive 自检与窗口模式节拍共用;真机对应 + * App/app/ui_data_feed.c 的 200ms 发布拍) + * 入口参数:n - 帧号(>=1;0 是冻结帧,只由 sim_publish_data_frames 开局发一次) uint32_t + * 返回值:无 + * 限定条件:ui_data_init() 之后 + * 函数说明:ui_data_publish_rt 自带发布序号前进——这正是内容页框架自动失效的触发 + * 源(序号变过才整页失效,见 ui_content_page_auto_poll) + */ +static void sim_publish_rt_live(uint32_t n) { + ui_data_rt_t rt; + + sim_fill_rt_frame(&rt, n); + (void)ui_data_publish_rt(&rt); +} + static void sim_publish_data_frames(void) { ui_data_rt_t rt; ui_data_info_t info; @@ -979,68 +1106,10 @@ static void sim_publish_data_frames(void) { 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"); - } + sim_fill_rt_frame(&rt, 0u); /* 冻结帧:黄金基准,装配与斜坡逻辑见 sim_fill_rt_frame */ (void)ui_data_publish_rt(&rt); + /* 定值帧:出厂默认 + 封存(幂等),再按 prot_set 现状发一帧(见上面两个函数) */ (void)prot_set_init(); sim_publish_par_frame(); @@ -1153,14 +1222,49 @@ static void sim_param_pump(void) { } } +/* + * 函数功能:把当前屏快照(I1)写成像素文件(run_headless 首枪与 --rtlive 第二枪共用) + * 入口参数:path - 输出文件路径(写入 160x160 I1 原始像素) const char * + * 返回值:0 - 成功;1 - 快照失败或文件打不开 int + * 限定条件:离屏显示与目标屏已就绪(无头上下文内调用) + * 函数说明:I1 缓冲区布局 [palette 8B][pixel data],跳调色板只写像素;快照即整屏 + * 同步重渲,不依赖 lv_timer_handler(无头禁驱动帧循环的纪律不变) + */ +static int snap_to_file(const char *path) { + 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; + 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 wn = fwrite(px, 1, px_bytes, fp); + fclose(fp); + lv_draw_buf_destroy(snap); + printf("screenshot: %s (%zu bytes, %ux%u I1)\n", path, wn, snap_w, snap_h); + return 0; +} + /* * 函数功能:无窗口离屏渲染(可注入键序列)并导出末帧像素 * 入口参数:keys - 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键) const char * * path - 输出文件(写入 160x160 I1 原始像素) const char * * standby - 非 0 = 注键后强制进入待机屏再截图(--standby 模式) int + * path_b - 非 NULL = --rtlive 模式:A 枪出图后发布一帧 live 斜坡帧并直调 + * 框架自动刷新判定体,再拍 B 写到本路径 const char * * 返回值:0 - 成功;非 0 - 失败 int */ -static int run_headless(const char * keys, const char * path, int standby) { +static int run_headless(const char * keys, const char * path, int standby, const char * path_b) { setvbuf(stdout, NULL, _IONBF, 0); lv_init(); lv_delay_set_cb(shot_delay_cb); @@ -1256,28 +1360,24 @@ static int run_headless(const char * keys, const char * path, int standby) { (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; + int rc = snap_to_file(path); + + if (rc != 0) { + return rc; } - 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; + /* --rtlive 第二枪:发布前进过序号的 live 斜坡帧后,无头没有定时器节拍, + * 直调生产判定体 ui_content_page_auto_poll 顶替——它就是板端定时器回调里 + * 跑的同一份代码;"数据变过才失效"的判定与真机逐句相同 */ + if (path_b != NULL) { + sim_publish_rt_live(1u); + ui_content_page_auto_poll(); + rc = snap_to_file(path_b); + if (rc != 0) { + return rc; + } + printf("rtlive: frames written (A=%s B=%s)\n", path, path_b); } - 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; } @@ -1624,7 +1724,7 @@ static int run_logtest(void) { * 函数功能:模拟器入口(解析模式开关 → 窗口模式或无头自检) * 入口参数:argc - 参数个数 int * argv - 参数数组(--screenshot / --keys / --standby / --cfgtest / - * --logtest / --idle / --noclock) char ** + * --logtest / --idle / --noclock / --rtlive) char ** * 返回值:0 - 正常退出或自检 PASS;非 0 - 失败 int */ int main(int argc, char **argv) { @@ -1652,13 +1752,16 @@ int main(int argc, char **argv) { /* 截图/自检模式:--screenshot / --keys / --standby / --cfgtest / --logtest */ for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--screenshot") == 0 && (i + 1) < argc) { - return run_headless("", argv[i + 1], 0); + return run_headless("", argv[i + 1], 0, NULL); } if (strcmp(argv[i], "--keys") == 0 && (i + 2) < argc) { - return run_headless(argv[i + 1], argv[i + 2], 0); + return run_headless(argv[i + 1], argv[i + 2], 0, NULL); } if (strcmp(argv[i], "--standby") == 0 && (i + 1) < argc) { - return run_headless("", argv[i + 1], 1); + return run_headless("", argv[i + 1], 1, NULL); + } + if (strcmp(argv[i], "--rtlive") == 0 && (i + 3) < argc) { + return run_headless(argv[i + 1], argv[i + 2], 0, argv[i + 3]); } if (strcmp(argv[i], "--cfgtest") == 0) { return run_cfgtest(); @@ -1759,6 +1862,11 @@ int main(int argc, char **argv) { /* 配置文件轮询起点(2s 周期,与 MCU netTask 一致) */ uint32_t cfg_last_poll = (uint32_t)SDL_GetTicks(); + /* rt live 节拍(窗口模式实时演示):镜像板端 ui_data_feed 的 200ms 发布拍; + * 无头回归模式根本不走本循环——黄金参考永远建立在冻结帧 n=0 上 */ + uint32_t rt_last_pub = cfg_last_poll; + uint32_t rt_live_n = 0u; + /* 主循环:驱动 LVGL 定时器与刷新 * 注意:SDL 事件(键盘/窗口/退出)由 LVGL SDL 驱动内部 5ms 定时器 * (lv_sdl_window.c 的 sdl_event_handler)统一轮询分发, @@ -1791,6 +1899,14 @@ int main(int argc, char **argv) { cfg_file_poll(); } + /* rt 帧按拍重发(窗口实时演示,板端 feed 同款 200ms 拍):确定性斜坡帧、 + * 无随机;发布序号前进后,实时页由内容页框架自动失效定时器刷屏—— + * 打开交流量页不按键即可看到数值自己走 */ + if ((uint32_t)(now_ms - rt_last_pub) >= 200u) { + rt_last_pub = now_ms; + sim_publish_rt_live(++rt_live_n); + } + /* 驱动 LVGL 定时器与刷新 */ uint32_t sleep_ms = lv_timer_handler(); if (sleep_ms == LV_NO_TIMER_READY) { diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index 0c9dcfa8..d95cd031 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -17,6 +17,10 @@ check_content_pages.py — 叶子内容页像素黄金参考回归 + 文本级 另做开页-关页往返池水位门禁:`es`(info)与 `ddeeesll`(密码页,ESC 后 ←← 弹出路上压的 2 个菜单框回根框,完全还原初始态)后 used 须等于 POOL_AFTER_ROUNDTRIP。 +另跑 --rtlive 两态比对,实证「不按任何键,新数据照样刷屏」(task-2 框架自动失效链: +开页拍 A → 仿真发布一帧斜坡帧并直调 ui_content_page_auto_poll → 拍 B,A≠B 才 PASS; +黄金参考本身仍建立在冻结帧上,与本检查互不影响)。 + 黄金参考更新流程(有意改视觉时):先肉眼看图确认新效果正确,再 --update 刷新, 刷新即改基准 → `git diff --stat simulator/tests/ref` 必须逐页过目并在提交正文说明; 最后 regen_ref_png.py 重生成目检 PNG;误报排查见 docs/UI设计/UI_自动化测试流程.md。 @@ -1220,13 +1224,36 @@ def main(): else: print("[rt:%s] PASS 开页-关页往返池水位 %d B" % (rt_name, used)) + # --rtlive 两态比对(task-2):开交流量页拍 A,发布 live 斜坡帧 + 框架判定体再拍 B。 + # A≠B = 「数据在变、页面没被按键过,屏上内容照样更新」成立;黄金参考不受影响。 + rtA = os.path.join(tmp, "rtlive_a.bin") + rtB = os.path.join(tmp, "rtlive_b.bin") + proc = subprocess.run([str(EXE), "--rtlive", "dee", rtA, rtB], + cwd=tmp, capture_output=True, text=True) + if proc.returncode != 0: + ok = False + print("[rtlive] FAIL 退出码 %d stderr=%s" % (proc.returncode, proc.stderr.strip())) + elif "rtlive: frames written" not in proc.stdout: + ok = False + print("[rtlive] FAIL 输出缺标记行:\n%s" % proc.stdout) + else: + a = Path(rtA).read_bytes() + b = Path(rtB).read_bytes() + if len(a) != len(b) or a == b: + ok = False + print("[rtlive] FAIL A/B 异常(lenA=%d lenB=%d equal=%s)——自动失效链断了或斜坡帧没动显示值" + % (len(a), len(b), a == b)) + else: + diff = sum(1 for x, y in zip(a, b) if x != y) + print("[rtlive] PASS 未按键实时数据落屏(A≠B,字节差异 %d)" % diff) + if update: print("PASS: 黄金参考已刷新(%d 页)" % len(PAGES)) shutil.rmtree(tmp, ignore_errors=True) return 0 if ok: - print("PASS: %d 内容页 + 往返池水位全部通过" % len(PAGES)) + print("PASS: %d 内容页 + 往返池水位 + rtlive 全部通过" % len(PAGES)) shutil.rmtree(tmp, ignore_errors=True) return 0 # 失败时保留临时截图便于排查