Merge branch 'v2-on-ui' into task/meas-snap

# Conflicts:
#	scripts/id_registry.json
This commit is contained in:
2026-10-25 10:29:18 +08:00
42 changed files with 3024 additions and 115 deletions
+32
View File
@@ -312,6 +312,21 @@ repos:
files: ^(App/fs/vol_lock\.(c|h)|test/host/vol_lock/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7f2) 卷空闲同步(写回缓存空闲 flush)主机侧单元测试(2026-10-25 加,道F 批 2)
# 背景:该模块是写回缓存掉电窗口的兜底网——掉电与"脏页驻留"本身台架
# 不可按需构造;卷忙跳过 / ioctl 失败边沿 latch / 32 位 tick 回绕 /
# 失败路径成对放锁(不得空放)全是纯状态逻辑。本钩子用 MSVC 离线编译
# 真实源文件(#include 纳入;桩:可控时钟 + 事件位图 + 卷忙注入 +
# 可注入 disk_ioctl),把四级门与 latch 分支一次钉死。
# files 含被测源与其头、就绪位属主 sys_events 的消费面 default_task.c、
# 以及 harness 本身。
- id: vol-idle-sync-unit-test
name: 卷空闲同步单测(主机侧)
entry: python test/host/vol_idle_sync/run_unit_test.py
language: system
files: ^(App/fs/vol_idle_sync\.(c|h)|App/task/default_task\.c|test/host/vol_idle_sync/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加)
# 背景:time_sync.c 是 TCP 时间同步协议状态机,也是 N-17 号看门狗风险的**全部
# 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/
@@ -482,6 +497,23 @@ repos:
files: ^(Drivers/BSP/GD5F2GQ5UE/nand_ftl\.(c|h)|Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue\.h|Lib/dhara/(map|journal|nand|error|bytes)\.(c|h)|test/host/nand_port/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7z) UI 数据快照层(道E task-14)主机侧单元测试(2026-10-25 加)
# 背景:ui_data 是"页面看到的整个世界",但 ui-golden 只覆盖已接线的页面——
# 遥信 27 点、定值 40 项、遥测未接线的通道错了不会让任何参考图变红。本钩子
# 用 MSVC 离线编译真实 ui_data.c 与 ui_w_soe.c(#include 纳入,零桩),把两件事
# 钉死:① 快照存储语义(首帧前读 -1 / 序号由本层盖章 / 三通道互不串台 /
# NULL 防御);② 跨层不透明下标与显示名映射的**现场一致性**——测试单元在同一
# TU 里同时 include BSP 侧与 App/algo、App/log 真头,逐项比对 adc_semantic_t /
# prot_set_id_t / soe_type_t / prot_elm_id_t(解耦换来的"魔法下标"风险,口径
# docs/UI设计/UI_数据供给设计说明.md §9.4;SOE 类型含未标定闭锁=7/复归=8)。
# files 覆盖被测两件 + 编号真源四头 + 页面消费面(ui_content_page/ui_menu*)+ harness。
- id: ui-data-unit-test
name: UI 数据快照层单测(主机侧)
entry: python test/host/ui_data/run_unit_test.py
language: system
files: ^(Drivers/BSP/UI/(ui_data|ui_w_soe|ui_w_records|ui_content_page|ui_menu)\.(c|h)|App/app/ui_data_feed\.(c|h)|App/algo/(adc_input|prot_set|prot_elm)\.(c|h)|App/log/soe_rec\.(c|h)|test/host/ui_data/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 8) 测试套件开关门禁(2026-10-01 加)
# 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件,
# 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里
+2 -2
View File
@@ -90,7 +90,7 @@
## 提交门禁(pre-commit)
- 任一失败即拦截,共 **42 个钩子**(**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 18 个(`-unit-test` 后缀,2026-10-25 增补 adc-cal / prot-dim 两个)+ `test-coverage`(**覆盖率总闸**:跑全部 35 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 11 页黄金参考,U4)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3),报告 S-1)+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**),报告 S-5)
- 任一失败即拦截,共 **44 个钩子**(**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败)+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 20 个(`-unit-test` 后缀,2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data 四个)+ `test-coverage`(**覆盖率总闸**:跑全部 37 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,N-23/U4)+ `mem-budget`(RAM 92% 预算门禁,N-27)+ `font-charset`(UI 字符集差集检查,防缺字白框,U4)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 11 页黄金参考,U4)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3),报告 S-1)+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**),报告 S-5)
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
@@ -173,7 +173,7 @@
| ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` |
| 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
| RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` |
| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(42 个钩子 → 23 篇索引) |
| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(44 个钩子 → 23 篇索引) |
| 实体编号怎么写 / 前缀总表 / `id-check` 八项校验 | `docs/编号规范.md`(§4 总表、§8 门禁与实测基线) |
| 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 |
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
+443
View File
@@ -0,0 +1,443 @@
/*
* 模块名称:UI 数据组装器(ui_data_feed)
* 模块功能:App → UI 数据通道的写侧——按节拍从采集/标定/相量/测频/保护/协议/墙钟
* 各模块取数,组装成 docs/UI设计/UI_数据供给设计说明.md §4 契约的三帧
* (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h
* 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**,不含任何布局、
* 格式化与 LVGL 调用(显示规格全在页面行表,同文档 §8)
* 量纲诚实性:eng 字段按**真值**填——docs/任务规划/数据可信.md §2.6 把承诺边界定在
* "引脚电压",故未做两点标定前 eng = UI_DATA_ENG_PIN(值 = ADC 引脚电压 V,
* 不是端子二次值);电气 8 路 adc_cal_flags() 的 VALID 全置位才升
* UI_DATA_ENG_SEC。TEMP 按决策不参与线性标定 → 恒带 UI_DATA_Q_RAW,
* 页面据此不挂单位(三态口径同文档 §4.2)
* 适用平台:STM32F407ZGTx(PC 模拟器不链接本文件,见 ui_data_feed.h 端接法)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
*/
/* 头文件包含区 */
#include <string.h>
#include "cmsis_os.h" /* osKernelGetTickCount:与 adc_task 同一 ms 时基 */
#include "ui_data_feed.h"
/* 下层(Drivers/BSP):快照存侧 + 两个端口/用户名常量来源(App → BSP 是合法方向) */
#include "ui_data.h"
#include "ui_net_sink.h" /* UI_NET_SINK_PORT:远程显示(RemoDispBus)监听端口 */
#include "ftp_cfg.h" /* FTP_CFG_USER_MAX / ftp_cfg_get() */
/* 同层(App)数据源 */
#include "adc_input.h" /* ADC_SEM_COUNT / adc_input_raw(TEMP 码值) */
#include "adc_cal.h" /* adc_cal_flags:VALID/CLIPPED 三态可用性 */
#include "phasor.h" /* 基波/谐波有效值(fAmSQ) */
#include "freq.h" /* 测频读数与新鲜度 */
#include "prot_elm.h" /* 元件动作态(遥信位图来源) */
#include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */
#include "sys_clock.h" /* 墙钟字符串 */
#include "iec104_serv.h" /* IEC104_SERV_PORT:实际监听端口(非协议层同名常量) */
/* 私有宏定义区 */
/* 实时帧与信息帧发布节拍(ms);参数帧只在定值校验和变化时发布
* (节拍依据 docs/UI设计/UI_数据供给设计说明.md §5.2) */
#define FEED_RT_PERIOD_MS 200u
/* 遥信位号:**与 App/protocol/iec104/iec104_pts.c 板端源的位图同源**(该文件把
* 位号写死在函数体内、未导出,UI 侧不得 include 协议头,故在此镜像一份)。
* 变更纪律:104 点表位号改动时必须同步这里 + 页面名表 + 主机一致性测试
* (test/host/ui_data 的位图断言)。V0.1 有硬件来源的只有以下 6 位,
* 其余 27 位恒无效(开入未实现);过压/欠压/过频/低频四个元件在
* V0.1 点表里没有对应遥信点,故本函数不产出。 */
#define FEED_YX_BIT_FAULT_ALL 14u /* YX15 故障总(任一电压/电流段或零序动作) */
#define FEED_YX_BIT_OC1 15u /* YX16 过流 I 段 */
#define FEED_YX_BIT_OC2 16u /* YX17 过流 II 段 */
#define FEED_YX_BIT_OC3 17u /* YX18 过流 III 段 */
#define FEED_YX_BIT_OL 22u /* YX23 过载(过负荷) */
#define FEED_YX_BIT_LOCK 30u /* YX31 保护闭锁(定值校验和自检失败) */
/* YX1~4(bit0..3)是开关位置类双点点,V0.1 无开入硬件 → 恒 0 且不在有效位图内,
* 因此本文件不为它们定义常量(定义了也不参与任何判定 = 看着像接了线其实没有) */
/* 有效位图:YX15~18/23/31(与 104 点表 YX_VALID_MASK 同集合) */
#define FEED_YX_VALID_MASK ((((uint64_t)1u << FEED_YX_BIT_FAULT_ALL) | \
((uint64_t)1u << FEED_YX_BIT_OC1) | \
((uint64_t)1u << FEED_YX_BIT_OC2) | \
((uint64_t)1u << FEED_YX_BIT_OC3) | \
((uint64_t)1u << FEED_YX_BIT_OL) | \
((uint64_t)1u << FEED_YX_BIT_LOCK)))
/*
* 装置身份(版本类):仓库内**没有**产品级版本登记表,值沿用页面既有显示值
* (docs/UI设计/内容页架构.md 的装置信息行表现值),有正式口径后只改本段一处。
* ⚠ 软件校验不抄参考项目遗留的 "4454":构建期没有 CRC 生成步骤,如实显示 "--"
* (处置口径 docs/UI设计/页面扩充详细方案.md §3.7 第 5 行)
*/
#define FEED_MODEL "F30"
#define FEED_SW_VER "SV0.010"
#define FEED_HW_VER "HW0.010"
#define FEED_SW_SUM "--"
/* 编译期一致性守卫(ARMCC C99 无 _Static_assert,用项目惯用的数组宽度法,
* 先例 App/cfg/cfg_file.c 与 Drivers/BSP/UI/ui_menu_tab.h):
* 快照下标空间与各 App 枚举一旦漂移,编译即失败,不留到运行期
* (契约 docs/UI设计/UI_数据供给设计说明.md §9.4 第 1/2 条) */
typedef char feed_chk_yc_n_t[(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT) ? 1 : -1];
typedef char feed_chk_set_n_t[(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT) ? 1 : -1];
typedef char feed_chk_fuser_t[(UI_DATA_FUSER_LEN >= FTP_CFG_USER_MAX) ? 1 : -1];
/* 静态变量定义区 */
/* 组装缓冲:三帧串行填充,共用一块存储(取最大者 = 参数帧),省两份 RAM */
static union {
ui_data_rt_t rt;
ui_data_par_t par;
ui_data_info_t info;
} s_scr;
/* 语义通道 → 基波相量实例下标缓存(-1 = 该通道无基波实例 → 该路发 IV);
* 首帧惰性建表(相量实例表是 ROM 表,运行期不变,建一次即可) */
static int8_t s_yc_phasor[ADC_SEM_COUNT];
static uint8_t s_map_done;
/* 发布节拍与参数变化检测状态 */
static uint32_t s_last_rt_ms;
static uint32_t s_par_sum;
static uint8_t s_par_sum_valid;
static uint8_t s_first_done;
/* 函数定义区 */
/*
* 函数功能:定长缓冲内拷贝字符串(跨层快照只允许定长数组,禁指针)
* 入口参数:p_dst - 目标缓冲(结构体内数组首地址) char *
* dst_len - 目标容量(含 NUL) size_t
* a_src - 源串(Flash 字面量或上层缓冲) const char *
* 返回值:无
* 限定条件:三者非 NULL 且 dst_len > 0;截断时保证 NUL 结尾
* 函数说明:不用 strcpy/sprintf(门禁 CODE-RULE-06 禁用不安全函数),
* 超长**静默截断**——快照字段本就是定长显示串,截断只会少显示尾部,
* 不会越界;本函数每 200 ms 调用个位数次,不为此加日志
*/
static void feed_copy_str(char *p_dst, size_t dst_len, const char *a_src) {
size_t n;
if ((p_dst == NULL) || (a_src == NULL) || (dst_len == 0u)) {
return;
}
n = strlen(a_src);
if (n > (dst_len - 1u)) {
n = dst_len - 1u;
}
(void)memcpy(p_dst, a_src, n);
p_dst[n] = '\0';
}
/*
* 函数功能:建立"语义通道 → 基波相量实例"映射(首帧一次)
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 上下文,ui_data_feed_tick 首帧前调用
* 函数说明:只认 harm == 1 的实例;一个语义通道有多个基波实例时取**首个**
* (现状 k_inst[] 每通道最多一个基波,取首个即唯一解)。
* UB/UC/IB/IC/IN 无实例(V0.1 硬件削顶,N-18)→ 留 -1,该路发 IV
*/
static void feed_map_phasor(void) {
uint8_t n;
uint8_t i;
for (i = 0u; i < (uint8_t)ADC_SEM_COUNT; i++) {
s_yc_phasor[i] = -1;
}
n = phasor_inst_count();
for (i = 0u; i < n; i++) {
const phasor_inst_t *p = phasor_inst(i);
if ((p == NULL) || (p->harm != 1u) || ((uint8_t)p->sem >= (uint8_t)ADC_SEM_COUNT)) {
continue;
}
if (s_yc_phasor[(uint8_t)p->sem] < 0) {
s_yc_phasor[(uint8_t)p->sem] = (int8_t)i;
}
}
s_map_done = 1u;
}
/*
* 函数功能:算整帧量纲口径 eng
* 入口参数:无
* 返回值:UI_DATA_ENG_PIN 或 UI_DATA_ENG_SEC uint8_t
* 限定条件:无(纯读)
* 函数说明:**全或无**——电气 8 路(TEMP 除外,它不参与线性标定)的
* ADC_CAL_F_VALID 全置位才升二次值。取全或无是因为整帧共用一个 eng:
* 只要还有一路是引脚电压,页面挂"二次值"就会把那一路的读数误标
* (宁可整体显示"未标定",也不给部分真值贴真值标签)
*/
static uint8_t feed_eng(void) {
uint8_t sem;
for (sem = 0u; sem < (uint8_t)ADC_SEM_TEMP; sem++) {
if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_VALID) == 0u) {
return UI_DATA_ENG_PIN;
}
}
return UI_DATA_ENG_SEC;
}
/*
* 函数功能:组装遥信位图(bit i = 第 i 点"合/动作")
* 入口参数:无
* 返回值:位图 uint64_t
* 限定条件:uiTask 上下文(读 protectTask 写的元件状态快照,读侧拷贝语义,
* 与 App/protocol/iec104/iec104_pts.c 板端源同形)
* 函数说明:位号镜像本文件 FEED_YX_BIT_* 一组宏的注释(含同步纪律);
* 双点遥信 YX1~4 无硬件 → 恒 0 且不在有效位图内
*/
static uint64_t feed_yx_bits(void) {
prot_elm_state_t st;
uint64_t bits = 0u;
uint8_t fault_all;
fault_all = 0u;
prot_elm_state(PE_OC1, &st);
if (st.asserted != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_OC1);
fault_all = 1u;
}
prot_elm_state(PE_OC2, &st);
if (st.asserted != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_OC2);
fault_all = 1u;
}
prot_elm_state(PE_OC3, &st);
if (st.asserted != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_OC3);
fault_all = 1u;
}
prot_elm_state(PE_3U0, &st);
if (st.asserted != 0u) {
fault_all = 1u;
}
if (fault_all != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_FAULT_ALL);
}
prot_elm_state(PE_OL, &st);
if (st.asserted != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_OL);
}
if (prot_set_locked() != 0u) {
bits |= ((uint64_t)1u << FEED_YX_BIT_LOCK);
}
return bits;
}
/*
* 函数功能:组装实时量快照(遥测 9 路 + 相量实例槽 + 遥信 + 频率 + 墙钟)
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_rt_t *
* tick_ms - 发布时刻(tick 域) uint32_t
* 返回值:无
* 限定条件:uiTask 上下文;映射表已建(feed_map_phasor)
* 函数说明:取数口径与 104 上送同源不同路——本函数读**相量使用区**(1 ms 刷新、
* 双区原子交换),不做死区与平均;遥测链快照(docs/任务规划/数据可信.md
* §4.5 的 meas 快照)落地后,本函数改成读该快照即可,页面零改动
* (这正是本层存在的意义:换数据源只改这里)
*/
static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) {
uint32_t now_ep;
uint8_t sem;
char ts[UI_DATA_CLOCK_LEN];
p_snap->tick_ms = tick_ms;
p_snap->valid = 1u;
p_snap->eng = feed_eng();
/* 遥测:语义通道序的基波有效值(采样环已按 adc_cal 折算 → 当前为引脚电压域) */
for (sem = 0u; sem < (uint8_t)ADC_SEM_COUNT; sem++) {
int8_t pi = s_yc_phasor[sem];
p_snap->yc[sem].val = 0.0f;
p_snap->yc[sem].qds = 0u;
if (sem == (uint8_t)ADC_SEM_TEMP) {
/* 温度:不参与线性标定,透传转换器码值并打 RAW 位(页面不得挂单位) */
int32_t code = 0;
p_snap->yc[sem].qds = UI_DATA_Q_RAW;
if (adc_input_raw((adc_semantic_t)sem, &code) == 0) {
p_snap->yc[sem].val = (float)code;
} else {
p_snap->yc[sem].qds |= UI_DATA_Q_IV;
}
continue;
}
if (pi < 0) {
/* 无基波实例 = V0.1 该通道无可用采样源(削顶) → 无效 */
p_snap->yc[sem].qds = UI_DATA_Q_IV;
} else {
phasor_t ph;
int rc = phasor_get((uint8_t)pi, &ph);
if (rc == 0) {
p_snap->yc[sem].val = ph.fAmSQ; /* 有效值口径(系数已折 sqrt2/N) */
} else if (rc == -2) {
p_snap->yc[sem].qds = UI_DATA_Q_NT; /* 未攒满一周波 */
} else {
p_snap->yc[sem].qds = UI_DATA_Q_IV;
}
}
if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_CLIPPED) != 0u) {
p_snap->yc[sem].qds |= UI_DATA_Q_OV; /* 硬件削顶:交流量不可信 */
}
}
/* 相量实例槽:含谐波(Ia 二次/五次),交流量页的 Ia2/Ia5 行走这条带 */
{
uint8_t n = phasor_inst_count();
if (n > UI_DATA_PH_MAX) {
n = UI_DATA_PH_MAX; /* 超出槽上限的实例本帧不发布(现 5 实例,余 3 槽) */
}
p_snap->ph_n = n;
for (sem = 0u; sem < UI_DATA_PH_MAX; sem++) {
p_snap->ph[sem].val = 0.0f;
p_snap->ph[sem].qds = UI_DATA_Q_IV;
}
for (sem = 0u; sem < n; sem++) {
phasor_t ph;
if (phasor_get(sem, &ph) == 0) {
p_snap->ph[sem].val = ph.fAmSQ;
p_snap->ph[sem].qds = 0u;
} else {
p_snap->ph[sem].qds = UI_DATA_Q_NT;
}
}
}
p_snap->yx_bits = feed_yx_bits();
p_snap->yx_valid = FEED_YX_VALID_MASK;
/* 测频:读数新鲜度单独走 freq_valid()(幅值门 + 收敛),两者任一不成立
* 页面即显示 "--"——eng=0 期间幅值门恒失效,读数本就不可消费,如实反映 */
{
float hz = 0.0f;
p_snap->freq_valid = 0u;
p_snap->freq_hz = 0.0f;
if (freq_get(&hz) == 0) {
p_snap->freq_hz = hz;
if (freq_valid() != 0u) {
p_snap->freq_valid = 1u;
}
}
}
/* 墙钟:UINT32_MAX 是 sys_clock_get() 的失败哨兵 → clock_ok=0、串清空 */
p_snap->clock_ok = 0u;
p_snap->clock[0] = '\0';
now_ep = sys_clock_get();
if ((now_ep != UINT32_MAX) && (sys_clock_get_str(ts, sizeof(ts)) != NULL)) {
feed_copy_str(p_snap->clock, sizeof(p_snap->clock), ts);
p_snap->clock_ok = 1u;
}
}
/*
* 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图)
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_par_t *
* sum - 本次发布的 prot_set_checksum() 值 uint32_t
* 返回值:无
* 限定条件:uiTask 上下文(读侧;写者 = iec104Task 的 set_sealed 与装载路径,
* 读到的永远是"值区与封存一致"的完整快照)
* 函数说明:只搬 Q10 原值不换算小数位——换算属表现层
* (docs/UI设计/UI_数据供给设计说明.md §8)
*/
static void feed_fill_par(ui_data_par_t *p_snap, uint32_t sum) {
uint8_t i;
p_snap->checksum = sum;
p_snap->valid = 1u;
p_snap->locked = prot_set_locked();
p_snap->bad_mask = prot_set_bad_mask();
/* 越界位图非 0 = 上一次整表装载/提交有越界项(prot_set_bad_mask 的既有口径),
* 页面据此反显;为 0 时无意义位图不参与显示 */
p_snap->bad_valid = (p_snap->bad_mask != 0u) ? 1u : 0u;
for (i = 0u; i < (uint8_t)PS_ID_COUNT; i++) {
p_snap->q10[i] = prot_set_get((prot_set_id_t)i);
}
}
/*
* 函数功能:组装装置信息快照(身份 + 运行时长 + 通讯参数)
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_info_t *
* tick_ms - 当前 tick(换算 uptime_s 用) uint32_t
* 返回值:无
* 限定条件:uiTask 上下文
* 函数说明:页面不得自己 include 协议/配置头取这些值,一律经本帧下发
* (docs/UI设计/页面扩充详细方案.md §3.5"值必须在 App 侧取");
* 公共地址 CA 不在此下发——固件里 CA 是逐帧从 ASDU 头解析的,
* 没有集中常量可取,待 104 侧给出查询点后按扩展规则追加字段
*/
static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) {
const ftp_cfg_t *p_ftp = ftp_cfg_get();
p_snap->valid = 1u;
p_snap->uptime_s = tick_ms / 1000u;
p_snap->port_104 = (uint16_t)IEC104_SERV_PORT;
p_snap->port_disp = (uint16_t)UI_NET_SINK_PORT;
feed_copy_str(p_snap->model, sizeof(p_snap->model), FEED_MODEL);
feed_copy_str(p_snap->sw_ver, sizeof(p_snap->sw_ver), FEED_SW_VER);
feed_copy_str(p_snap->hw_ver, sizeof(p_snap->hw_ver), FEED_HW_VER);
feed_copy_str(p_snap->build_date, sizeof(p_snap->build_date), __DATE__);
feed_copy_str(p_snap->sw_sum, sizeof(p_snap->sw_sum), FEED_SW_SUM);
if (p_ftp != NULL) {
feed_copy_str(p_snap->ftp_user, sizeof(p_snap->ftp_user), p_ftp->user);
} else {
p_snap->ftp_user[0] = '\0';
}
}
/*
* 函数功能:装配节拍入口——实时帧与信息帧满 200 ms 发布一次,参数帧仅在校验和
* 变化时发布;首轮立即发布(免启动初期出现一闪的 "--")
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
* 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较;
* 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径)
*/
void ui_data_feed_tick(void) {
uint32_t now = osKernelGetTickCount();
uint32_t sum = prot_set_checksum();
uint8_t due_rt;
if (s_map_done == 0u) {
feed_map_phasor();
}
due_rt = 0u;
if ((s_first_done == 0u) || ((uint32_t)(now - s_last_rt_ms) >= FEED_RT_PERIOD_MS)) {
due_rt = 1u;
s_first_done = 1u;
s_last_rt_ms = now;
}
if (due_rt != 0u) {
(void)memset(&s_scr, 0, sizeof(s_scr));
feed_fill_rt(&s_scr.rt, now);
(void)ui_data_publish_rt(&s_scr.rt);
(void)memset(&s_scr, 0, sizeof(s_scr));
feed_fill_info(&s_scr.info, now);
(void)ui_data_publish_info(&s_scr.info);
}
/* 参数帧:校验和门控——定值变化是低频事件,不随 200 ms 白拷 184 B
* (docs/UI设计/UI_数据供给设计说明.md §5.2) */
if ((s_par_sum_valid == 0u) || (sum != s_par_sum)) {
(void)memset(&s_scr, 0, sizeof(s_scr));
feed_fill_par(&s_scr.par, sum);
(void)ui_data_publish_par(&s_scr.par);
s_par_sum = sum;
s_par_sum_valid = 1u;
}
}
+46
View File
@@ -0,0 +1,46 @@
/*
* 模块名称:UI 数据组装器(ui_data_feed)
* 模块功能:App → UI 数据通道的"写侧"——按节拍从采集/算法/保护/协议/时钟各模块
* 取数,组装成 docs/UI设计/UI_数据供给设计说明.md §4 契约的三帧
* (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h
* 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**
* 适用范围:App/app(装配层:可同时 include 两侧头,方向仍是上层依赖下层,
* scripts/check_layer_deps.py 只拦"Drivers/BSP 目录反向 include App 目录")
* 线程约定:uiTask 上下文,主循环每轮调 ui_data_feed_tick() 一次;与页面读写同
* 上下文串行 → 无锁(docs/UI设计/UI_数据供给设计说明.md §5.1)。
* ⚠ 不得在 adcTask/protectTask/netTask 里调用本模块
* 端接法:MCU = App/task/ui_task.c 主循环;PC 模拟器**不链接本文件**(模拟器不跑
* 固件装配链,快照由 simulator/main.c 直接发布固定帧保证像素回归可复现,
* 见 docs/UI设计/UI_数据供给设计说明.md §9.2;模拟器豁免登记在
* scripts/check_registration.py 的 SIM_EXEMPT)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
*/
#ifndef UI_DATA_FEED_H
#define UI_DATA_FEED_H
#ifdef __cplusplus
extern "C" {
#endif
/*
* 函数功能:装配节拍入口——实时帧与信息帧满 200 ms 发布一次,参数帧仅在校验和
* 变化时发布;首轮立即发布(免启动后出现一闪的 "--")
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
* 函数说明:本函数每轮代价 = 一次 tick 差值比较 + 一次校验和比较;真正的取数
* 与组装只在到期时发生(docs/UI设计/UI_数据供给设计说明.md §5.2)。
* 内部零阻塞、零堆分配、零日志(日志每条约 90 B Flash + 216 B 栈,
* 200 ms 周期打不起)
*/
void ui_data_feed_tick(void);
#ifdef __cplusplus
}
#endif
#endif /* UI_DATA_FEED_H */
+125
View File
@@ -0,0 +1,125 @@
/*
* 模块名称:Volume Idle Sync(写回缓存空闲 flush)— MCU 实现
* 模块功能:就绪门控 + 周期节流 + 卷锁 try + disk_ioctl(CTRL_SYNC) 的兜底
* 状态机(设计/使用约定/边界见 vol_idle_sync.h)
* 适用平台:STM32F407ZGTx + FreeRTOS
* 作者:DTU 项目组
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 DTU 项目组 创建(道F 批 2:写回缓存空闲 flush 收口)
*/
/* 头文件包含区 */
#include "vol_idle_sync.h"
#include "test_config.h"
#if !defined(TEST_SUITE_STORAGE)
#include "cmsis_os.h"
#include "FreeRTOS.h"
#include "task.h"
#include "ff.h" /* FatFs 基础类型(diskio.h 依赖 BYTE/LBA_t 等,须先纳) */
#include "diskio.h" /* disk_ioctl / CTRL_SYNC / DRESULT */
#include "vol_lock.h"
#include "sys_events.h"
/* 本模块日志仅一条失败留痕(≤60 字符),默认行缓冲即可,不调 DBG_LINE_BUF_MAX */
#include "dbg_log.h"
/* Private defines -----------------------------------------------------------*/
/* 空闲同步检查周期(= 漏网脏页的最大掉电驻留窗,另加一个主循环拍 ≈50ms)。
* 取值依据:clean 态为"脏标志 + journal"双短路(零 NAND 触点),高频不伤介质
* 只是白跑;脏态最多一次页编程 + journal 收账,与 f_close 同型,但落在卷
* 已空闲的窗口、远离活跃写会话。5 s 与主循环其余慢观测(栈/堆水位 2 s)同
* 量级而更稀疏——本模块是兜底网,不是持久性主路径(主路径 = close 内同步)。
* 锁忙(卷不空闲)同样按本周期重试:不做欠账补偿,节奏恒定好推理。 */
#define VOL_IDLE_SYNC_PERIOD_MS 5000u
/* 静态变量定义区 */
/* 状态量放文件级 static(非函数内):主机侧白盒单测(test/host/vol_idle_sync/)
* 以 #include 纳入本文件后需要复位它们;命名带 vol 前缀避免与测试桩撞名 */
static uint32_t s_vol_sync_last_ms; /* 上次尝试时刻(tick ms,uint32 回绕) */
static uint8_t s_vol_sync_armed; /* 0 = 从未尝试(首拍直接放行) */
static uint8_t s_vol_sync_fail_latch; /* 失败留痕边沿 latch:一个 episode 一条 */
/* 函数定义区 */
/*
* 函数功能:存储空闲周期同步轮询(就绪门控 → 节流 → try 卷锁 → CTRL_SYNC)
* 入口参数:无 void
* 返回值:无 void
* 限定条件:仅 defaultTask 主循环调用;内部绝不阻塞(try 锁 + 常数级耗时,
* 最坏成本 = 一次 f_close 的同步段)
* 函数说明:四级门,逐级变贵:
* ① 就绪门:sys_evtHandle 未建或 STORAGE_READY 未置 → 静默
* (挂载前 disk_ioctl 只会 RES_NOTRDY,且 bring-up 失败重试期
* 卷状态未定,一律不碰);
* ② 节流门:距上次尝试不足 VOL_IDLE_SYNC_PERIOD_MS → 返回。
* 无符号差值判据((uint32)(now - last) < 周期)天然跨 32 位回绕
* 正确,与 stack_probe_poll 同一 idiom;勿"优化"成大小比较;
* ③ 空闲门:vol_lock_wait(0) try——锁被持有 = 有文件操作序列在做
* (syslog 落盘 / 配置保存 / lftpd open...close),本轮跳过,
* 绝不改变"文件持有期内不同步"的既有语义;
* ④ 动作:disk_ioctl(0, CTRL_SYNC)——FatFs 官方幂等落盘原语
* (f_close 尾部同一条路径):FTL 脏页刷入 + dhara_map_sync。
* clean 时两级短路立即返回,零 NAND 操作。
* 失败留痕:RES_ERROR 只按"边沿"打一条 DBG_ERROR(latch),持续失败
* 不刷屏;下一次成功后 latch 复位,新 episode 再报。底层失败形态
* (Cache flush FAIL / Map sync failed)已由 nand_ftl 就地打点,
* 本条只补"空闲路径也救不回"的上下文,不重复堆日志锚点。
*/
void vol_idle_sync_poll(void) {
const uint32_t now = (uint32_t)xTaskGetTickCount();
DRESULT dr;
/* ① 就绪门 */
if (sys_evtHandle == NULL) {
return;
}
if ((osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) == 0u) {
return;
}
/* ② 节流门(回绕安全判据,见函数说明②) */
if ((s_vol_sync_armed != 0u)
&& ((uint32_t)(now - s_vol_sync_last_ms)
< (uint32_t)pdMS_TO_TICKS(VOL_IDLE_SYNC_PERIOD_MS))) {
return;
}
s_vol_sync_last_ms = now;
s_vol_sync_armed = 1u;
/* ③ 空闲门:try 锁,拿不到 = 卷不空闲,本轮跳过(下周期再试) */
if (vol_lock_wait(0u) == 0u) {
return;
}
/* ④ 幂等落盘:脏页刷入 + journal 收账;clean 时零 NAND 触点 */
dr = disk_ioctl(0, CTRL_SYNC, NULL);
vol_lock_release(); /* 成败都放锁:同步失败不产生锁泄漏 */
if (dr != RES_OK) {
if (s_vol_sync_fail_latch == 0u) {
s_vol_sync_fail_latch = 1u;
DBG_ERROR("[FS] idle sync FAIL (dr=%d)", (int)dr);
}
} else {
s_vol_sync_fail_latch = 0u; /* 成功:重新武装,下个 episode 可再报 */
}
}
#else /* TEST_SUITE_STORAGE */
/*
* 函数功能:STORAGE 套件构建下的空实现(编译期裁剪落点)
* 入口参数:无 void
* 返回值:无 void
* 限定条件:仅当 test_config.h 选中 TEST_SUITE_STORAGE 时编入本分支
* 函数说明:该构建里挂载/盘写由 testTask 的 storage_test_run 独占(测试含
* nand_ftl_deinit 掉电模拟),后台同步与它并发会互相踩坏 FTL/FatFs
* 状态——必须彻底静默,而非仅靠运行时门控。
*/
void vol_idle_sync_poll(void) {
}
#endif /* TEST_SUITE_STORAGE */
+50
View File
@@ -0,0 +1,50 @@
/*
* 模块名称:Volume Idle Sync(写回缓存空闲 flush)
* 模块功能:随 defaultTask 主循环周期轮询,在存储**空闲**时对 NAND FatFs 卷
* 补发一次 CTRL_SYNC(FatFs 幂等落盘语义 = FTL 单页写回缓存脏页刷入
* + dhara journal 待办收账,见 docs/架构设计/STM32F4-Base存储架构说明.md
* §5.5),作为写回缓存持久性的兜底网:
* - 正常写事务语义不变:f_close 内部 sync_fs 已同步,本模块零干预;
* - 价值在漏网兜底:任何"缓存留脏、之后再无调用触发 sync"的状态
* (异常路径中途放锁等)其掉电驻留窗从"无限期,直到下一笔写"
* 收敛为"不超过本模块周期";
* - 卷不空闲(卷锁被持有:syslog 落盘 / 配置保存 / lftpd
* open...close 序列进行中)本轮跳过——不改变文件持有期(如 FTP
* STOR 进行中)的既有语义。
* clean 态零 NAND 触点(脏标志与 journal 双短路);脏态成本与一次
* f_close 同型。周期 5 s(.c 内 VOL_IDLE_SYNC_PERIOD_MS)。
* ⚠ 边界(勿当成掉电保护):本板无掉电检测硬件,"掉电前强制 flush"
* 不可行;"单次写事务进行中掉电"的丢失边界也不因本模块改变。
* 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2);仅 MCU 端,
* 主机侧 L0.5 单测 test/host/vol_idle_sync/
* 作者:DTU 项目组
* 创建日期:2026-10-25(道F 批 2:写回缓存空闲 flush 收口)
* 修改记录:
* v1.0 2026-10-25 DTU 项目组 创建:STORAGE_READY 门控 + 周期节流 +
* 卷锁 try + disk_ioctl(CTRL_SYNC) + 失败边沿 latch
*/
#ifndef VOL_IDLE_SYNC_H
#define VOL_IDLE_SYNC_H
#ifdef __cplusplus
extern "C" {
#endif
/*
* 函数功能:存储空闲周期同步的轮询入口(内部自带就绪门控 / 节流 / 非阻塞
* try 锁 / clean 短路,调用点每轮无条件调用即可)
* 入口参数:无 void
* 返回值:无 void
* 限定条件:仅 defaultTask 主循环调用(存储属主任务;本函数内部绝不阻塞——
* defaultTask 是看门狗喂狗点,见 vol_lock.h 使用约定);
* TEST_SUITE_STORAGE 构建下为空实现(卷由 testTask 独占,后台同步
* 必须彻底静默)
*/
void vol_idle_sync_poll(void);
#ifdef __cplusplus
}
#endif
#endif /* VOL_IDLE_SYNC_H */
+7 -4
View File
@@ -334,8 +334,10 @@ static int s_append(syslog_id_t id, const char *desc) {
* **不改掉电语义**:批次仍在同一次 syslog_drain() 调用内写完并
* f_close;close 内部 sync_fs → disk_ioctl(CTRL_SYNC) 会调
* ftl_flush_cache() 把 FTL 的单页写回缓存刷进 NAND(nand_ftl.c)。
* 落盘时机与旧实现每条 close 等价,批次不跨调用驻留 RAM;残余只到
* "单次写事务进行中掉电/复位"为止(登记簿 N-22,2026-10-22 裁定暂不做)。
* 落盘时机与旧实现每条 close 等价,批次不跨调用驻留 RAM;漏网脏页
* (留脏后再无调用触发刷出)已有写回缓存空闲 flush 兜底
* (App/fs/vol_idle_sync.h:卷空闲 + 周期 CTRL_SYNC),掉电残余只到
* "单次写事务进行中掉电/复位"为止。
* 自愈重试与错误去重的判据与 s_append_rec 一致;容量不足时返回 -3
* 而不是就地 unlink——"满了就重建"必须由按条慢路径统一执行,
* 否则一批 8 条会跨越多轮重建、语义说不清。
@@ -846,8 +848,9 @@ int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) {
* ⚠ 2026-10-02 round 3~8 那次"复位后写入整段丢失"的根因是 dhara resume 锚错
* checkpoint 使 journal 整段回退(docs/架构设计/STM32F4-Base存储架构说明.md
* §5.4.1),不是这层缓存:单页缓存最多丢最后未跨页那笔(≤512 B),丢不出
* "整段"。真正的残余(无空闲/掉电前周期 flush)= 登记簿 N-22,2026-10-22
* 用户裁定暂不做。
* "整段"。空闲周期 flush 兜底已接线(App/fs/vol_idle_sync.h:STORAGE_READY
* 门控 + 周期节流 + 卷锁 try + CTRL_SYNC,脏页驻留窗由周期封顶);本板无
* 掉电检测硬件,"掉电前强制 flush"仍是物理边界,不在该兜底承诺之内。
* 顺序硬约束不变:s_append 自行取卷锁(非递归锁),必须在 vol_lock_release()
* **之后**调用,绝不可在持锁状态下调用。
* 容量护栏:> 8KB 先 unlink 重建,防长跑无界增长(8.3 合法名 wdev.log)。
+9
View File
@@ -23,6 +23,9 @@
* 耗时(task_prof_report),并在**心跳位图出现 1→0 边沿**
* 的瞬间留一份现场快照(task_prof_report_raw,裸 UART);
* 另给控制台排空与 syslog 落盘各加一个耗时探针
* v1.3 2026-10-25 DTU 项目组 写回缓存空闲 flush 接线:主循环 syslog 排空
* 之后调 vol_idle_sync_poll()(卷空闲周期 CTRL_SYNC 兜底,
* 模块 = App/fs/vol_idle_sync.[ch],道F 批 2)
*/
#include "default_task.h"
@@ -46,6 +49,7 @@
#include "task_watchdog.h"
#include "task_prof.h" /* 各任务最坏耗时上报 */
#include "vol_lock.h"
#include "vol_idle_sync.h" /* 写回缓存空闲 flush 兜底(App/fs/vol_idle_sync.h) */
#include "task_handles.h" /* 任务句柄与属性块的引用契约(定义处仍是 Src/freertos.c) */
/* Private defines -----------------------------------------------------------*/
@@ -275,6 +279,11 @@ void default_task_func(void *argument) {
task_prof_enter(TASK_PROF_DEF_SYSLOG);
syslog_drain();
task_prof_exit(TASK_PROF_DEF_SYSLOG);
/* 写回缓存空闲 flush 兜底:卷空闲(try 锁)+ 周期节流 + 就绪门控全在
* 模块内(App/fs/vol_idle_sync.h);clean 态零 NAND 触点,脏态最坏 =
* 一次 f_close 同型的同步段。放在 drain 之后:本拍若写过日志,close
* 已同步过,这里随即 clean 短路——漏网脏页的驻留窗由周期封顶 */
vol_idle_sync_poll();
#endif
/* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */
task_wd_seg(TASK_WD_SEG_STACK);
+13
View File
@@ -31,6 +31,8 @@
#include "ui_net_sink.h"
#include "ui_standby.h"
#include "ui_w_records.h" /* task-6 记录 reader 注入点(UI 页面不再 include sys_log.h) */
#include "ui_data.h" /* task-14 快照存侧(BSP):本任务只负责先复位再喂帧 */
#include "ui_data_feed.h" /* task-14 数据装配节拍入口(App/app,UI 侧唯一发布者) */
#include "sys_log.h"
#include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */
#include "sys_clock.h"
@@ -143,6 +145,13 @@ void ui_task_func(void *argument) {
/* 记录 reader 注入:必须在 ui_menu_init 之前(记录页在此前不可能被打开,
* 但契约把"启动期先绑定"写死;未绑定窗口组件按空态防御——见 ui_w_records.h) */
ui_w_records_bind(&s_rec_reader);
/* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据"
* 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */
ui_data_init();
/* 首帧立即发布:待机屏/菜单屏在启动早期就可能被远端推流打开,等 200 ms
* 会闪一下 "--"(同文档 §5.3);本调用在 ui_menu_init 之前,页面尚未建立,
* 发布路径不触 LVGL,安全 */
ui_data_feed_tick();
DBG_INFO("UI task: ui_menu_init enter");
if (ui_menu_init() != 0) {
DBG_ERROR("UI menu init: FAIL");
@@ -190,6 +199,10 @@ void ui_task_func(void *argument) {
lat_mark(&tm_loop);
tm_loop_valid = 1U;
#endif
/* 数据装配节拍(200 ms 内部自分频,未到期时只比一次 tick):
* 排在渲染之前,本轮绘制的就是本轮刚发布的那一帧
* (UI_数据供给设计说明.md §5.1 单写者同上下文的前提就在这儿) */
ui_data_feed_tick();
ui_port_step(UI_TASK_TICK_MS);
/* 心跳**先于**推流往返发出(判读口径 docs/子系统设计/看门狗设计说明.md §11.3):UI 心跳测的是
* "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后,
+196
View File
@@ -0,0 +1,196 @@
/*
* 模块名称:UI 数据快照存储(ui_data)
* 模块功能:三通道快照(实时量 rt / 参数 par / 装置信息 info)的存储、发布盖章与
* 只读访问——本模块是纯内存叶子:不取数、不格式化、不判断业务含义、
* 不碰 LVGL、不 include 任何 App 头(职责边界与依赖方向 =
* Drivers/BSP/UI/ui_data.h 文件头与
* docs/UI设计/UI_数据供给设计说明.md §3.2)
* 并发模型:单写者同上下文——唯一发布者 App/app/ui_data_feed.c 与全部页面钩子都在
* uiTask 内串行执行,故无锁、无双缓冲、无 seqlock;将来真出现第二个
* 发布者,保护收在本模块内部(双槽 + 每槽 seq + 单字节活动下标,照
* App/algo/phasor.c 双区先例),页面代码不受影响
* (判据 docs/UI设计/UI_数据供给设计说明.md §5.1)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_data.h"
/* 静态变量定义区 */
/* 三通道存储(上电全零 = "尚无首帧",与 ui_data_init() 后状态一致) */
static ui_data_rt_t s_rt;
static ui_data_par_t s_par;
static ui_data_info_t s_info;
/*
* 发布计数:seq 由本层盖章,与入帧携带的 seq 无关——入帧的该字段被覆盖。
* 读侧"有无首帧"的判据是计数为 0,不是帧内 valid:valid 是"这一帧有没有有效
* 数据"(写侧可能发布一帧全无效的真值),两者语义不同,混用会让"首帧之前"
* 与"首帧但无数据"在页面上显示成同一种态。
*/
static uint32_t s_rt_cnt;
static uint32_t s_par_cnt;
static uint32_t s_info_cnt;
/* 函数定义区 */
/*
* 函数功能:快照层初始化——三通道清零(valid=0、seq=0),页面即按"无首帧"渲染
* 入口参数:无
* 返回值:无
* 限定条件:内核启动前可调(纯内存操作,零 OS 依赖);可重复调用(幂等)
* 函数说明:静态存储上电本已清零,本函数的意义是运行期复位(例如"恢复出厂后
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态
*/
void ui_data_init(void) {
(void)memset(&s_rt, 0, sizeof(s_rt));
(void)memset(&s_par, 0, sizeof(s_par));
(void)memset(&s_info, 0, sizeof(s_info));
s_rt_cnt = 0u;
s_par_cnt = 0u;
s_info_cnt = 0u;
}
/*
* 函数功能:发布一帧实时量快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧(本层整帧拷入自有存储,调用方可复用其栈/静态空间)
* const ui_data_rt_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:唯一发布者 = App/app/ui_data_feed.c,uiTask 上下文
* 函数说明:seq 由本层盖章(入帧 seq 被忽略);其余字段原样入存,本层不做任何
* 业务判断与格式化
*/
int ui_data_publish_rt(const ui_data_rt_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_rt, a_snap, sizeof(s_rt));
s_rt_cnt += 1u;
s_rt.seq = s_rt_cnt;
return 0;
}
/*
* 函数功能:发布一帧参数快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_par_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上;发布节拍 = 定值校验和变化才发
* (docs/UI设计/UI_数据供给设计说明.md §5.2)
*/
int ui_data_publish_par(const ui_data_par_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_par, a_snap, sizeof(s_par));
s_par_cnt += 1u;
s_par.seq = s_par_cnt;
return 0;
}
/*
* 函数功能:发布一帧装置信息快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_info_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上
*/
int ui_data_publish_info(const ui_data_info_t *a_snap) {
if (a_snap == NULL) {
return -1;
}
(void)memcpy(&s_info, a_snap, sizeof(s_info));
s_info_cnt += 1u;
s_info.seq = s_info_cnt;
return 0;
}
/*
* 函数功能:读最新一帧实时量快照(整帧拷给页面私有缓存)
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_rt_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动,页面显示 "--");
* -2 - 参数无效(NULL) int
* 限定条件:页面钩子上下文(uiTask);勿在逐行 refresh 里无条件整帧拷贝,
* 先比 ui_data_rt_seq()(用法见 Drivers/BSP/UI/ui_data.h 文件头)
*/
int ui_data_rt(ui_data_rt_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_rt_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_rt, sizeof(s_rt));
return 0;
}
/*
* 函数功能:读最新一帧参数快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_par_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上;定值页同样用 ui_data_par_seq() 门控页面私有缓存
*/
int ui_data_par(ui_data_par_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_par_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_par, sizeof(s_par));
return 0;
}
/*
* 函数功能:读最新一帧装置信息快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_info_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上
*/
int ui_data_info(ui_data_info_t *a_out) {
if (a_out == NULL) {
return -2;
}
if (s_info_cnt == 0u) {
return -1;
}
(void)memcpy(a_out, &s_info, sizeof(s_info));
return 0;
}
/*
* 函数功能:查实时帧发布序号(页面缓存失效判据)
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:轻量查询——只读一个 uint32,可逐行调用;序号只在发布时 +1,页面
* 据此把"整帧拷贝"从每帧每行降到只在换帧时一次
*/
uint32_t ui_data_rt_seq(void) {
return s_rt.seq;
}
/*
* 函数功能:查参数帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_par_seq(void) {
return s_par.seq;
}
/*
* 函数功能:查装置信息帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_info_seq(void) {
return s_info.seq;
}
+273
View File
@@ -0,0 +1,273 @@
/*
* 模块名称:UI 数据快照层(ui_data)
* 模块功能:App 层 → UI 层唯一数据通道的"存侧"——只持有最新一帧实时量(rt)/
* 参数(par)/装置信息(info) 快照 + 发布序号,给页面只读访问;
* 不取数、不格式化、不判断业务含义(职责边界 =
* docs/UI设计/UI_数据供给设计说明.md §3.2)
* 适用范围:Drivers/BSP/UI(表现层叶子:不 include 任何 App 头、不 include 任何
* 页面头、不碰 LVGL——分层方向由 scripts/check_layer_deps.py 守护,
* BSP → App 反向 include 一律拦提交)
* 并发模型:写侧 = App/app/ui_data_feed.c(唯一发布者);读侧 = ui_page_* 的绘制/
* 按键钩子。两侧同在 uiTask 上下文串行执行 → 无锁、无双缓冲、无 seqlock
* (判据与"将来出现第二发布者"时的扩展路径见
* docs/UI设计/UI_数据供给设计说明.md §5.1)
* 页面用法:行缓存按"发布序号变过才整帧拷"门控,勿逐行整帧拷贝
* (docs/UI设计/UI_数据供给设计说明.md §5.4):
* static ui_data_rt_t s_rt;
* static uint32_t s_seq;
* if (ui_data_rt_seq() != s_seq) {
* (void)ui_data_rt(&s_rt);
* s_seq = ui_data_rt_seq();
* }
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14:契约 §4 快照层落地;相对契约
* 两通道新增第三条 info,划分理由见本头"通道划分"注
* 与契约 §4.4)
*/
#ifndef UI_DATA_H
#define UI_DATA_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/*
* 契约版本:结构体字段只允许尾部追加,追加即升此值并在写侧填默认值
* (禁止删除或重排字段——页面按名字取,重排是静默错位;
* 口径 docs/UI设计/UI_数据供给设计说明.md §4.3)
*/
#define UI_DATA_VER 1u
/* ── 通道数量 ─────────────────────────────────────────────────────────────── */
/* 遥测语义通道数:下标序 = adc_semantic_t(UA/UB/UC/UN/IA/IB/IC/IN/TEMP)。
* BSP 侧不得 include App 头,故"两数相等"的编译期守卫落在写侧
* App/app/ui_data_feed.c,语义逐项对齐的断言落在 test/host/ui_data/
* (docs/UI设计/UI_数据供给设计说明.md §9.4 一致性测试) */
#define UI_DATA_YC_N 9u
/* 遥信点数:与 104 点表同序(bit i = 第 i 点),页面名表按点表逐行登记 */
#define UI_DATA_YX_N 33u
/* 定值项数:与 prot_set 枚举同序(校验和覆盖序),四视图划分不改序 */
#define UI_DATA_SET_N 40u
/* 相量实例槽上限:谐波(Ia 二次/五次)不能塞进"语义通道"下标空间——同一通道
* 的二次/五次谐波会与基波撞位,故实时帧另开一条按**相量实例序**索引的槽位,
* 供交流量页显示 Ia2/Ia5;实际发布槽数由 ph_n 给出,超出上限的实例本帧不发布
* (写侧口径见 App/app/ui_data_feed.c 的 fill_rt 函数)。上限取 8 = 现用 5 实例
* (IA/UA/UN 基波 + IA 二次/五次)+ 遥测谐波轨留位,每槽 8 B */
#define UI_DATA_PH_MAX 8u
/* ── 量纲口径 eng(诚实性字段:数值 val 的单位由它决定)───────────────────── */
/*
* 真值语义(现场口径 = docs/任务规划/数据可信.md §2.6 的承诺边界 + §4.2 三态
* 可用性;契约文档原先写的"0 = 原始码值(未标定)"是标定落地前的旧口径,
* 已按本表更正,勿再照抄旧表述):
* ENG_PIN 引脚电压 V —— 现状:adc_cal 的 ROM 默认系数是「数据可信」卡 1 定死
* 的引脚侧 LSB(TPAFE 152.59 uV/码、STM32 805.66 uV/码),采样环
* 折算出来的就是 ADC 引脚电压,不是端子二次值
* ENG_SEC 二次值 —— 电气 8 路 adc_cal_flags() 的 VALID 全置位(拿到端子变比
* 并完成两点标定)后由写侧升档,页面自动按二次值显示,零改动
* ENG_PRI 一次值 —— 预留(再乘 PT/CT 变比),当前写侧不会产出
*/
#define UI_DATA_ENG_PIN 0u
#define UI_DATA_ENG_SEC 1u
#define UI_DATA_ENG_PRI 2u
/* ── 品质位(与 104 QDS 语义对齐;页面只消费自己关心的位)─────────────────── */
#define UI_DATA_Q_IV 0x01u /* 无效:该通道无源/不可用 → 页面显示 "--" */
#define UI_DATA_Q_NT 0x02u /* 未就绪:尚未攒满一周波/未收敛 */
#define UI_DATA_Q_OV 0x04u /* 越限:超过量程/硬件削顶(V0.1 STM32 侧单极性) */
#define UI_DATA_Q_SB 0x08u /* 取代值(虚拟注入/替身值,页面应带角标) */
#define UI_DATA_Q_BL 0x10u /* 闭锁:该量被业务闭锁(未标定闭锁/采样闭锁) */
/* 原始码值域:val 不是工程量而是转换器码值——TEMP 不参与线性标定
* (docs/任务规划/数据可信.md §4.2 的"TEMP 透传码值"决策),有此位即页面
* 不得给 val 挂 V/A 单位,也不得与工程量互比 */
#define UI_DATA_Q_RAW 0x20u
/* ── 字符串容量(结构体内定长数组,跨层零指针;容量宏本地化)──────────────── */
#define UI_DATA_CLOCK_LEN 20u /* "YYYY-MM-DD HH:MM:SS" 19 + NUL */
#define UI_DATA_MODEL_LEN 8u /* 终端型号("F30") */
#define UI_DATA_VERSTR_LEN 16u /* 软件/硬件版本串 */
#define UI_DATA_DATE_LEN 12u /* 程序日期(编译期 __DATE__,留 NUL 余量) */
#define UI_DATA_SUM_LEN 8u /* 软件校验(无构建期 CRC 步骤时如实为 "--") */
#define UI_DATA_FUSER_LEN 16u /* FTP 用户名;与 Drivers/BSP/NETCFG/ftp_cfg.h 的
* FTP_CFG_USER_MAX 同宽,写侧受该上限约束 */
/* ── 通道一:实时量(写侧节拍 200 ms)────────────────────────────────────── */
/* 单路遥测:数值 + 品质 */
typedef struct {
float val; /* 数值;单位口径由 ui_data_rt_t.eng 决定,RAW 位另说 */
uint8_t qds; /* 品质位组合(UI_DATA_Q_*) */
uint8_t rsv[3]; /* 对齐填充(显式命名,勿留匿名空洞) */
} ui_data_yc_t;
/* 实时快照:遥测 9 路 + 遥信位图 + 频率 + 墙钟 */
typedef struct {
uint32_t seq; /* 发布序号(本层盖章,单调递增) */
uint32_t tick_ms; /* 发布时刻(tick 域,差值法可比) */
uint8_t valid; /* 0 = 尚无首帧(页面全显 "--") */
uint8_t eng; /* 量纲口径(UI_DATA_ENG_*) */
uint8_t rsv[2]; /* 预留(对齐填充) */
ui_data_yc_t yc[UI_DATA_YC_N]; /* 下标 = adc_semantic_t 语义序(基波有效值) */
uint8_t ph_n; /* 本帧发布的相量实例槽数(0..UI_DATA_PH_MAX) */
uint8_t rsv4[3]; /* 预留(对齐填充) */
ui_data_yc_t ph[UI_DATA_PH_MAX]; /* 下标 = 相量实例序(含谐波),见 UI_DATA_PH_MAX 注 */
uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */
uint64_t yx_valid; /* 遥信有效位图:bit i = 1 该点有硬件来源 */
uint8_t freq_valid; /* 1 = 测频读数新鲜(幅值门 OK 且已收敛) */
uint8_t rsv2[3]; /* 预留(对齐填充) */
float freq_hz; /* 测得频率(Hz);freq_valid=0 时不可消费 */
uint8_t clock_ok; /* 0 = 墙钟无效(RTC 异常/未对时) */
uint8_t rsv3[3]; /* 预留(对齐填充) */
char clock[UI_DATA_CLOCK_LEN]; /* "YYYY-MM-DD HH:MM:SS";无效时空串 */
} ui_data_rt_t;
/* ── 通道二:参数(写侧节拍 = 定值校验和变化才发)────────────────────────── */
/*
* 参数快照:40 项定值 Q10 原值 + 封存态
* 存 Q10 而非 float 的理由:定值是整表校验和覆盖的定点值,快照存浮点会引入
* "显示值与校验和口径不一致"的二义性(Q10 → 显示小数位换算属表现层,见
* docs/UI设计/UI_数据供给设计说明.md §8)
*/
typedef struct {
uint32_t seq; /* 发布序号(本层盖章) */
uint32_t checksum; /* 与 prot_set_checksum() 同值:页面据此判"参数变过" */
uint8_t valid; /* 0 = 尚无首帧 */
uint8_t locked; /* 1 = 定值闭锁(校验和自检失败等)→ 页面禁编辑 */
uint8_t bad_valid; /* 1 = bad_mask 有意义(整表装载/提交失败之后) */
uint8_t rsv; /* 预留 */
uint64_t bad_mask; /* 40 位:置 1 = 该定值项越界(页面反显该行) */
int32_t q10[UI_DATA_SET_N]; /* Q10 原值,下标 = prot_set_id_t 枚举序 */
} ui_data_par_t;
/* ── 通道三:装置信息(静态身份 + 通讯参数 + 运行时长)───────────────────── */
/*
* 装置信息快照(通道划分理由见本头文件头,契约侧同一条登记在
* docs/UI设计/UI_数据供给设计说明.md §4.4——同一事实只在一处详述)
* 写侧随 rt 同节拍发布(内容基本不变,代价是一次约 90 B 拷贝)
*/
typedef struct {
uint32_t seq; /* 发布序号(本层盖章) */
uint8_t valid; /* 0 = 尚无首帧 */
uint8_t rsv[3]; /* 预留(对齐填充) */
uint32_t uptime_s; /* 开机运行时长(秒;写侧按 tick 差算) */
uint16_t port_104; /* IEC 60870-5-104 上行监听端口 */
uint16_t port_disp; /* 远程显示(RemoDispBus)端口 */
char model[UI_DATA_MODEL_LEN]; /* 终端型号 */
char sw_ver[UI_DATA_VERSTR_LEN]; /* 软件版本 */
char hw_ver[UI_DATA_VERSTR_LEN]; /* 硬件版本 */
char build_date[UI_DATA_DATE_LEN]; /* 程序日期(编译期) */
char sw_sum[UI_DATA_SUM_LEN]; /* 软件校验;无构建期 CRC 步骤 = "--" */
char ftp_user[UI_DATA_FUSER_LEN]; /* FTP 登录用户名(只显名,不显口令) */
} ui_data_info_t;
/* ── 初始化 ───────────────────────────────────────────────────────────────── */
/*
* 函数功能:快照层初始化——三通道清零(valid=0、seq=0),页面即按"无首帧"渲染
* 入口参数:无
* 返回值:无
* 限定条件:内核启动前可调(纯内存操作,零 OS 依赖);可重复调用(幂等)
* 函数说明:静态存储上电本已清零,本函数的意义是运行期复位(例如"恢复出厂后
* 重开 UI"这类后续路径),以及让宿主侧测试能随时造出"首帧之前"的状态
*/
void ui_data_init(void);
/* ── 发布(写侧唯一入口;单写者同上下文,无锁)───────────────────────────── */
/*
* 函数功能:发布一帧实时量快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧(本层整帧拷入自有存储,调用方可复用其栈/静态空间)
* const ui_data_rt_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:唯一发布者 = App/app/ui_data_feed.c,uiTask 上下文
* 函数说明:seq 由本层盖章(写侧不必填);其余字段原样入存,本层不做任何业务
* 判断与格式化
*/
int ui_data_publish_rt(const ui_data_rt_t *a_snap);
/*
* 函数功能:发布一帧参数快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_par_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上;发布节拍 = 定值校验和变化才发
* (docs/UI设计/UI_数据供给设计说明.md §5.2)
*/
int ui_data_publish_par(const ui_data_par_t *a_snap);
/*
* 函数功能:发布一帧装置信息快照(覆盖存量并给 seq 盖章 +1)
* 入口参数:a_snap - 源帧 const ui_data_info_t *
* 返回值:0 - 已发布;-1 - 参数无效(NULL,存量不动) int
* 限定条件:同上
*/
int ui_data_publish_info(const ui_data_info_t *a_snap);
/* ── 只读访问(页面侧)───────────────────────────────────────────────────── */
/*
* 函数功能:读最新一帧实时量快照(整帧拷给页面私有缓存)
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_rt_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动,页面显示 "--");
* -2 - 参数无效(NULL) int
* 限定条件:页面钩子上下文(uiTask);勿在逐行 refresh 里无条件整帧拷贝,
* 先比 ui_data_rt_seq()(用法见本头文件头)
*/
int ui_data_rt(ui_data_rt_t *a_out);
/*
* 函数功能:读最新一帧参数快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_par_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上;定值页同样用 ui_data_par_seq() 门控页面私有缓存
*/
int ui_data_par(ui_data_par_t *a_out);
/*
* 函数功能:读最新一帧装置信息快照
* 入口参数:a_out - 输出缓冲(非 NULL) ui_data_info_t *
* 返回值:0 - 已拷出;-1 - 尚无首帧(输出不动);-2 - 参数无效 int
* 限定条件:同上
*/
int ui_data_info(ui_data_info_t *a_out);
/*
* 函数功能:查实时帧发布序号(页面缓存失效判据)
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无(只读单字,任意上下文安全)
* 函数说明:轻量查询——只读一个 uint32,可逐行调用(契约 §5.4 的门控就靠它)
*/
uint32_t ui_data_rt_seq(void);
/*
* 函数功能:查参数帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_par_seq(void);
/*
* 函数功能:查装置信息帧发布序号
* 入口参数:无
* 返回值:当前 seq;从未发布 = 0 uint32_t
* 限定条件:无
*/
uint32_t ui_data_info_seq(void);
#ifdef __cplusplus
}
#endif
#endif /* UI_DATA_H */
+88 -64
View File
@@ -599,6 +599,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x9f, 0xf0, 0x0, 0x4, 0x4a, 0x45, 0x42, 0xc7,
0xc0,
/* U+6062 "恢" */
0x24, 0x4, 0x80, 0xbf, 0xda, 0xa, 0x49, 0x49,
0x6a, 0xb1, 0x54, 0x28, 0x85, 0x28, 0xc9, 0x12,
0x10,
/* U+606F "息" */
0x8, 0xf, 0xf9, 0x1, 0x3f, 0xe4, 0x4, 0xff,
0x90, 0x13, 0xfe, 0x4, 0xa, 0x49, 0x42, 0xc7,
@@ -664,6 +669,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xbe, 0x94, 0x52, 0x8a, 0x91, 0x23, 0xea, 0x46,
0x30,
/* U+6548 "效" */
0x21, 0x2, 0x23, 0xf4, 0x0, 0xf4, 0xa4, 0x8a,
0xa0, 0x51, 0x4a, 0x10, 0x85, 0x11, 0x15, 0x41,
0x10,
/* U+6570 "数" */
0x11, 0xa, 0xa0, 0x44, 0x7e, 0xf3, 0x24, 0xb2,
0xa5, 0x51, 0xa, 0xf9, 0x49, 0x10, 0xc5, 0x65,
@@ -735,6 +745,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0x9a, 0xad, 0x14, 0x22, 0x84, 0x20, 0x8a, 0x16,
0x30,
/* U+6837 "样" */
0x24, 0x44, 0x50, 0xbf, 0xf8, 0x82, 0x10, 0xcf,
0x9c, 0x45, 0x8, 0x2f, 0xe4, 0x20, 0x84, 0x10,
0x80,
/* U+68C0 "检" */
0x21, 0x4, 0x20, 0x8a, 0x7a, 0x22, 0x82, 0xcf,
0x9c, 0x5, 0x12, 0x29, 0x44, 0xa8, 0x92, 0x17,
@@ -1058,6 +1073,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = {
0xfe, 0xe5, 0x7, 0xf8, 0xd5, 0x1a, 0xaf, 0x8d,
0x20, 0xa7, 0xf4, 0x82, 0xbf, 0xd6, 0x9, 0x80,
/* U+91C7 "采" */
0x3, 0xcf, 0x80, 0x20, 0x22, 0x22, 0x8, 0x8,
0x3f, 0xf8, 0x70, 0x15, 0x4, 0x93, 0x11, 0x82,
0x0,
/* U+91CD "重" */
0x3, 0xcf, 0x80, 0x10, 0x7f, 0xf4, 0x44, 0xff,
0x91, 0x13, 0xfe, 0x4, 0xf, 0xf8, 0x10, 0x7f,
@@ -1344,14 +1364,14 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 1677, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1694, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1711, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1728, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1744, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1757, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1774, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1790, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1807, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1821, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1838, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1728, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1745, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1762, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1778, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 1791, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1808, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1824, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1841, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1855, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1872, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1889, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1361,10 +1381,10 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 1957, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1974, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 1991, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2008, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2024, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2041, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2058, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2008, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2025, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2042, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2059, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2075, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2092, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2109, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1374,18 +1394,18 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 2177, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2194, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2211, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2228, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2242, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2259, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2276, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 2291, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2308, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2325, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2341, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2358, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2228, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2245, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2262, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2279, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2293, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2310, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2327, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 2342, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2359, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2376, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2392, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2409, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2459, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1394,10 +1414,10 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 2510, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2527, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2544, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2561, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 2575, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2592, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2609, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2561, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2578, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2595, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2612, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 2626, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2643, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 2660, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
@@ -1422,30 +1442,34 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = {
{.bitmap_index = 2983, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3000, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3017, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3034, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3050, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3067, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3084, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3034, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3051, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3068, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3085, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3101, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3118, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3135, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3152, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3169, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 3184, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 3199, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3216, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3233, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3250, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3169, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3186, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3203, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3220, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3237, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 3252, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2},
{.bitmap_index = 3267, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3284, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3300, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3317, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3334, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3351, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3284, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3301, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3318, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3335, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3352, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3368, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3385, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 3386, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 3388, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */
{.bitmap_index = 3385, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3402, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3419, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3436, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2},
{.bitmap_index = 3453, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 3454, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0},
{.bitmap_index = 3456, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */
};
/*---------------------
@@ -1460,22 +1484,22 @@ static const uint16_t unicode_list_1[] = {
0x3297, 0x32a8, 0x32cf, 0x32ea, 0x357e, 0x35d0, 0x35e0, 0x362b,
0x37ad, 0x37b6, 0x39f0, 0x39f7, 0x39f8, 0x3a3a, 0x3a3e, 0x3a66,
0x3c92, 0x3cd8, 0x3d14, 0x3d2f, 0x3d46, 0x3d96, 0x3da2, 0x3daf,
0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f0f, 0x40d7,
0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, 0x4245, 0x4247,
0x4249, 0x436d, 0x43e5, 0x4410, 0x4414, 0x4480, 0x4485, 0x4496,
0x44de, 0x45a0, 0x45a8, 0x45bf, 0x45ca, 0x45cc, 0x461f, 0x4685,
0x46a7, 0x46c1, 0x4760, 0x48c1, 0x49c0, 0x49c1, 0x4a1b, 0x4a55,
0x4ae2, 0x4b82, 0x4be1, 0x4beb, 0x4c28, 0x4ca5, 0x4cc9, 0x4f59,
0x50e7, 0x50e8, 0x5156, 0x53c8, 0x53d5, 0x53ec, 0x5594, 0x55ab,
0x56a1, 0x570c, 0x570e, 0x57da, 0x5872, 0x58ab, 0x598f, 0x59cc,
0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, 0x5d94, 0x5df1, 0x5e0e, 0x608a,
0x614f, 0x6217, 0x66ec, 0x6765, 0x6a06, 0x6a44, 0x6a4f, 0x6a50,
0x6a5e, 0x6a82, 0x6a8f, 0x6a97, 0x6bbf, 0x6c2a, 0x6c93, 0x6e0f,
0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, 0x6e74, 0x6ea0, 0x6eba,
0x6ef3, 0x6f05, 0x6f88, 0x6fed, 0x706d, 0x706f, 0x733f, 0x7345,
0x73a1, 0x73b9, 0x741f, 0x748d, 0x7494, 0x7498, 0x74d2, 0x7504,
0x753c, 0x7596, 0x7602, 0x7715, 0x7731, 0x7828, 0x792c, 0x7d78,
0xddac, 0xddba
0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f02, 0x3f0f,
0x40d7, 0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, 0x4245,
0x4247, 0x4249, 0x436d, 0x43e5, 0x43e8, 0x4410, 0x4414, 0x4480,
0x4485, 0x4496, 0x44de, 0x45a0, 0x45a8, 0x45bf, 0x45ca, 0x45cc,
0x461f, 0x4685, 0x46a7, 0x46c1, 0x46d7, 0x4760, 0x48c1, 0x49c0,
0x49c1, 0x4a1b, 0x4a55, 0x4ae2, 0x4b82, 0x4be1, 0x4beb, 0x4c28,
0x4ca5, 0x4cc9, 0x4f59, 0x50e7, 0x50e8, 0x5156, 0x53c8, 0x53d5,
0x53ec, 0x5594, 0x55ab, 0x56a1, 0x570c, 0x570e, 0x57da, 0x5872,
0x58ab, 0x598f, 0x59cc, 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, 0x5d94,
0x5df1, 0x5e0e, 0x608a, 0x614f, 0x6217, 0x66ec, 0x6765, 0x6a06,
0x6a44, 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, 0x6a8f, 0x6a97, 0x6bbf,
0x6c2a, 0x6c93, 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71,
0x6e74, 0x6ea0, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, 0x6fed, 0x7067,
0x706d, 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, 0x741f, 0x748d,
0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, 0x7602, 0x7715,
0x7731, 0x7828, 0x792c, 0x7d78, 0xddac, 0xddba
};
/*Collect the unicode lists and glyph_id offsets*/
@@ -1486,12 +1510,12 @@ static const lv_font_fmt_txt_cmap_t cmaps[] =
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
},
{
.range_start = 9654, .range_length = 1, .glyph_id_start = 274,
.range_start = 9654, .range_length = 1, .glyph_id_start = 278,
.unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY
},
{
.range_start = 8544, .range_length = 56763, .glyph_id_start = 96,
.unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 178, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
.unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 182, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY
}
};
+196
View File
@@ -0,0 +1,196 @@
/*
* 模块名称:UI SOE 记录页共享组件实现(ui_w_soe)
* 模块功能:见 Drivers/BSP/UI/ui_w_soe.h——事件类型/元件显示名映射 + 每记录 2 行
* 渲染 + 单条读盘缓存;数据经注入 reader 取,未注入按空态渲染
* 适用范围:Drivers/BSP/UI
* 说明:与 ui_w_records.c 同构(同一份"分页读记录 → 两行/条 + 实例缓存 + 空态
* 伪行"骨架),差别只在记录形态是二进制解码后的视图结构
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14)
*/
/* 头文件包含区 */
#include <string.h>
#include "ui_w_soe.h"
/* 私有宏定义区 */
/*
* 框架文本缓冲宽度(真源 Drivers/BSP/UI/ui_tokens.h 的 UI_BUF_STRLEN = 32)。
* 这里**有意不 include ui_tokens.h**:该头经 ui_port.h 牵入整条 LVGL 头链,而本组件
* 是纯数据 + 纯文本搬运、一个 LVGL 调用都没有——容量宏本地化之后,
* test/host/ui_data 可以直接把本 .c 编进主机单测核对显示名映射(同
* Drivers/BSP/UI/ui_w_records.h 把 ts/desc 容量宏本地化的同一先例与理由:
* 容量不足只会退化成占位显示,不会越界读)。框架令牌若加宽,本宏同步加宽。
*/
#define SOE_BUF_MAX 32u
#define SOE_INVALID_STR "--" /* 未知编号/越界/读失败的统一占位(ASCII) */
#define SOE_EMPTY_STR "无记录" /* 空态伪行(与文本记录页同字) */
#define SOE_NO_CLOCK_STR "时钟无效" /* 时标行值列:IV 位不显示猜测时间 */
/* 静态变量定义区 */
/* 注入的 SOE reader(模块级单例:SOE 落盘本就是全局一份,两端各只有一份实现;
* NULL = 未注入 → 空态。当前两端均未绑定,理由见头注"说明") */
static const ui_soe_reader_t *s_reader;
/*
* 事件类型显示名(下标 = UI_W_SOE_T_*;表长 = UI_W_SOE_T_COUNT)
* 词长分两档是**版式约束**不是措辞偏好:值列可用宽约 59 px(4 汉字),
* 名列约 94 px(7 汉字)——"动作/返回"是元件行的值列词,其余四类 4 字词
* 落在非元件事件的名列(口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条)
*/
static const char *const s_type_name[UI_W_SOE_T_COUNT] = {
SOE_INVALID_STR, /* 0 未知/未映射(含读失败) */
"动作", /* 1 元件动作 */
"返回", /* 2 元件返回 */
"压板退出", /* 3 软压板退出 */
"压板投入", /* 4 软压板投入 */
"采样闭锁", /* 5 采样故障闭锁 */
"采样恢复", /* 6 采样故障恢复 */
"标定闭锁", /* 7 未标定闭锁 */
"标定复归", /* 8 标定复归 */
};
/* 元件显示名(下标 = UI 元件编号 = 保护元件枚举序;越界不入表) */
static const char *const s_elm_name[UI_W_SOE_ELM_N] = {
"过流Ⅰ段", /* 0 */
"过流Ⅱ段", /* 1 */
"过流Ⅲ段", /* 2 */
"过负荷", /* 3 */
"过压", /* 4 */
"欠压", /* 5 */
"零序过压", /* 6 */
"过频", /* 7 */
"低频", /* 8 */
};
/* 函数定义区 */
/*
* 函数功能:注入 SOE reader
*/
void ui_w_soe_bind(const ui_soe_reader_t *a_reader) {
s_reader = a_reader;
}
/*
* 函数功能:把定长源串写入框架文本缓冲
* 入口参数:a_dst - 框架给的名/值缓冲(宽 = SOE_BUF_MAX,与框架 UI_BUF_STRLEN 同值) char *
* a_src - 源串(Flash 字面量或实例缓存) const char *
* src_bytes - 源缓冲总宽(含 NUL,整块拷入以带上终止符) size_t
* 返回值:无
* 限定条件:两者非 NULL;src_bytes 超 SOE_BUF_MAX 即按上限截断拷贝
* 函数说明:不用 strcpy/sprintf(门禁 CODE-RULE-06 禁用不安全函数);本组件
* 只做"整块搬运",不做格式化——格式化属页面行表(表现层)的事
*/
static void soe_put(char *a_dst, const char *a_src, size_t src_bytes) {
if ((a_dst == NULL) || (a_src == NULL)) {
return;
}
if (src_bytes > SOE_BUF_MAX) {
src_bytes = SOE_BUF_MAX;
}
(void)memcpy(a_dst, a_src, src_bytes);
}
/*
* 函数功能:事件类型编号 → 显示名
*/
const char *ui_w_soe_type_name(uint8_t a_type) {
if (a_type >= UI_W_SOE_T_COUNT) {
return SOE_INVALID_STR;
}
return s_type_name[a_type];
}
/*
* 函数功能:元件编号 → 显示名
*/
const char *ui_w_soe_elm_name(uint8_t a_elm) {
if (a_elm >= UI_W_SOE_ELM_N) {
return SOE_INVALID_STR;
}
return s_elm_name[a_elm];
}
/*
* 函数功能:总行数(每条记录 2 行;空态 = 单条伪行"无记录")
*/
uint16_t ui_w_soe_count(ui_w_soe_t *a_inst) {
uint32_t n;
(void)a_inst;
n = (s_reader != NULL) ? (uint32_t)s_reader->count() : 0u;
if (n == 0u) {
return 1u; /* 空态伪行(页脚"共 1 页",避免"共 0 页") */
}
return (uint16_t)(n * 2u);
}
/*
* 函数功能:逐行取数(偶数行 = 时标,奇数行 = 元件 + 事件类型)
*/
void ui_w_soe_refresh(ui_w_soe_t *a_inst, uint16_t a_row, char *a_name, char *a_value) {
uint32_t n;
uint32_t rec;
uint8_t ok;
a_value[0] = '\0';
n = (s_reader != NULL) ? (uint32_t)s_reader->count() : 0u;
if (n == 0u) {
(void)memcpy(a_name, SOE_EMPTY_STR, sizeof(SOE_EMPTY_STR));
return;
}
rec = (uint32_t)a_row / 2u;
ok = 0u;
if (a_inst->cache_valid != 0u && rec == a_inst->last_rec) {
ok = 1u; /* 同一条记录的两行共用一次读盘 */
} else if (s_reader != NULL) {
if ((s_reader->get)(rec, &a_inst->rec) == 0) {
a_inst->last_rec = rec;
a_inst->cache_valid = 1u;
ok = 1u;
} else {
a_inst->cache_valid = 0u; /* 越界/读失败:本行退化为占位,不留脏缓存 */
}
}
if (ok == 0u) {
(void)memcpy(a_name, SOE_INVALID_STR, sizeof(SOE_INVALID_STR));
return;
}
if (((uint16_t)a_row % 2u) == 0u) {
/* 时标行:IV 位或未对时 → 名列 "--"、值列"时钟无效",绝不显示猜测时间 */
if ((a_inst->rec.flags & UI_W_SOE_FLG_IV) != 0u || a_inst->rec.ts[0] == '\0') {
(void)memcpy(a_name, SOE_INVALID_STR, sizeof(SOE_INVALID_STR));
(void)memcpy(a_value, SOE_NO_CLOCK_STR, sizeof(SOE_NO_CLOCK_STR));
} else {
soe_put(a_name, a_inst->rec.ts, sizeof(a_inst->rec.ts));
}
return;
}
/* 内容行:元件事件 = 元件名 + 动作/返回;非元件事件 = 类型名(名列) */
if (a_inst->rec.elm != UI_W_SOE_ELM_NONE) {
const char *elm = ui_w_soe_elm_name(a_inst->rec.elm);
soe_put(a_name, elm, strlen(elm) + 1u);
/* 只有动作/返回两类是元件级事件;其余编号(理论不该出现)仍给类型名 */
if (a_inst->rec.type == UI_W_SOE_T_ELM_ASSERT
|| a_inst->rec.type == UI_W_SOE_T_ELM_RETURN) {
soe_put(a_value, ui_w_soe_type_name(a_inst->rec.type),
strlen(ui_w_soe_type_name(a_inst->rec.type)) + 1u);
}
} else {
const char *tn = ui_w_soe_type_name(a_inst->rec.type);
soe_put(a_name, tn, strlen(tn) + 1u);
}
}
+165
View File
@@ -0,0 +1,165 @@
/*
* 模块名称:UI SOE 记录页共享组件(ui_w_soe)
* 模块功能:把"/log/soe.log 二进制定长 64 B 事件记录 → 每记录 2 行(偶数行时标、
* 奇数行"元件 + 事件类型")+ 单条读盘缓存 + 空态伪行"无记录"这套逻辑
* 收成一份,与文本记录组件 ui_w_records 同构(用户学习成本为零);
* 数据经注入式 reader 函数表取,本头**不含任何 App 头**
* 与文本记录的区别:SOE 是结构化二进制记录(时标/类型/元件号/序号/值),
* 不是"时间戳 + 描述文本",不能让文本读者混读——故另立组件与另立
* 记录视图结构(照 ui_w_records.h 的注入模式,不复用其 desc 字段)
* 显示名映射(本组件私有,属表现层):事件类型与元件都按**不透明编号**入参,
* 编号值与 App 侧枚举逐项相等(一致性由 test/host/ui_data 主机单测
* 现场核对,不是照抄文档转述);文案是显示规格,不进快照也不进 App 层
* (口径 docs/UI设计/UI_数据供给设计说明.md §8)
* 适用范围:Drivers/BSP/UI(分层方向由 scripts/check_layer_deps.py 守护)
* 说明:reader 注入点已备好但**两端均未绑定**——SOE 文件的只读 API
* (条数 / 按序号读一条)尚未在落盘侧提供,绑定与页面接线属道E 后续
* 任务;未绑定期间本组件按空态渲染("无记录"伪行),不崩不读野
* 作者:王建锋
* 创建日期:2026-10-25
* 修改记录:
* v1.0 2026-10-25 王建锋 创建(道E task-14:事件类型/元件显示名映射 +
* reader 契约;映射含未标定闭锁/复归两类新事件)
*/
#ifndef UI_W_SOE_H
#define UI_W_SOE_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/* 时间戳缓冲容量(与文本记录组件同宽:19 字符 + NUL) */
#define UI_W_SOE_TS_LEN 20u
/*
* 事件类型不透明编号:UI 侧只认编号,不认识 App 的 soe_type_t。
* ⚠ 编号值与 App/log/soe_rec.h 的 soe_rec.h 枚举逐项相等(现场核对 2026-10-25:
* 1 元件动作 / 2 元件返回 / 3 软压板退出 / 4 软压板投入 / 5 采样闭锁 /
* 6 采样恢复 / 7 未标定闭锁 / 8 标定复归 / 9 = 哨兵),漂移由主机单测拦
* (test/host/ui_data 的 TC-UIDATA-012);新增类型 = 枚举尾追加 + 这里加编号
* + 名字表加一行 + 单测加一条
*/
#define UI_W_SOE_T_NONE 0u /* UI 私有:未知/未映射编号(读失败行) */
#define UI_W_SOE_T_ELM_ASSERT 1u /* 元件动作(值 = 状态打包) */
#define UI_W_SOE_T_ELM_RETURN 2u /* 元件返回(值 = 返回前状态快照) */
#define UI_W_SOE_T_PLATE_OUT 3u /* 软压板退出 */
#define UI_W_SOE_T_PLATE_IN 4u /* 软压板投入 */
#define UI_W_SOE_T_SAMPLE_BLOCK 5u /* 采样闭锁(值 = 采样故障闭锁位图) */
#define UI_W_SOE_T_SAMPLE_RESUME 6u /* 采样恢复 */
#define UI_W_SOE_T_CAL_BLOCK 7u /* 未标定闭锁(值 = VALID 未齐的通道位图) */
#define UI_W_SOE_T_CAL_RESUME 8u /* 标定复归 */
#define UI_W_SOE_T_COUNT 9u /* 哨兵 = 名字表长度(不是有效类型) */
/* 元件编号:与保护元件枚举同序(0..8),0xFF = 非元件事件
* (一致性由同名单测 TC-UIDATA-013 现场核对) */
#define UI_W_SOE_ELM_N 9u
#define UI_W_SOE_ELM_NONE 0xFFu
/* 记录标志位:bit7 = 记录时时钟无效(时标不可信,页面须显式标记而非显示假时间) */
#define UI_W_SOE_FLG_IV 0x80u
/*
* SOE 记录视图(reader 的输出形态;字段原样搬运,不做时标换算与业务判断)
* 单条 32 B:读盘缓存按实例存,一页 8 行 = 4 条记录也只缓存当前正在渲染的那条
*/
typedef struct {
char ts[UI_W_SOE_TS_LEN]; /* "YYYY-MM-DD HH:MM:SS";IV 或未对时时为空串 */
uint8_t type; /* UI_W_SOE_T_*(未知编号原样透传,名字表回 "--") */
uint8_t elm; /* 0..UI_W_SOE_ELM_N-1 或 UI_W_SOE_ELM_NONE */
uint8_t flags; /* UI_W_SOE_FLG_IV */
uint8_t rsv; /* 预留(对齐填充) */
uint32_t seq; /* 记录序号(上电内单调) */
uint32_t value; /* 类型相关值(页面当前不展开,留作详情行) */
} ui_w_soe_rec_t;
/*
* 注入式 SOE reader(只读函数表;契约与 ui_w_records.h 的 ui_rec_reader_t 同构,
* 只是数据是二进制记录,所以取回的是整条视图而不是两段文本)
* 实现体在 App / 模拟器侧,uiTask 上下文同步调用——绘制路径上的读盘时序与
* 文本记录页一致,无新增并发面
*/
typedef struct {
/*
* 函数功能:取 SOE 记录总条数
* 入口参数:无
* 返回值:条数(0 = 空态,页面显示"无记录"伪行) uint16_t
*/
uint16_t (*count)(void);
/*
* 函数功能:读第 idx 条 SOE 记录(解码成视图)
* 入口参数:a_idx - 记录序号(0 起) uint32_t
* a_out - 输出视图(非 NULL) ui_w_soe_rec_t *
* 返回值:0 = 成功;-1 = 序号越界;-2 = 读失败或校验和不符
* (码值与文本记录 reader 同族) int
*/
int (*get)(uint32_t a_idx, ui_w_soe_rec_t *a_out);
} ui_soe_reader_t;
/* SOE 记录页实例(读盘缓存必须按实例存,理由同 ui_w_records.h 的实例说明) */
typedef struct {
uint8_t cache_valid; /* 1 = rec 是 last_rec 那条 */
uint32_t last_rec; /* 已缓存的记录序号 */
ui_w_soe_rec_t rec; /* 当前记录缓存 */
} ui_w_soe_t;
/*
* 函数功能:注入 SOE reader(启动时一次;NULL = 撤销注入,页面回空态)
* 入口参数:a_reader - 只读函数表(生命周期由注入方保证,组件只存指针)
* const ui_soe_reader_t *
* 返回值:无
* 限定条件:uiTask 启动期注入(ui_menu_init 之前,与记录 reader 同址同纪律)
*/
void ui_w_soe_bind(const ui_soe_reader_t *a_reader);
/*
* 函数功能:事件类型编号 → 显示名(表现层映射表)
* 入口参数:a_type - 类型编号(UI_W_SOE_T_*) uint8_t
* 返回值:Flash 字符串指针(恒非 NULL;未知编号回 "--",不空指针、不空白) const char *
* 限定条件:无
* 函数说明:元件类事件(动作/返回)的名字短(2 字),放**值列**;
* 非元件事件(压板/采样/标定四类,4 字)放**名列**——两列宽度不同,
* 由 ui_w_soe_refresh 按 elm 是否有效决定落在哪一列
*/
const char *ui_w_soe_type_name(uint8_t a_type);
/*
* 函数功能:元件编号 → 显示名(表现层映射表)
* 入口参数:a_elm - 元件编号(0..UI_W_SOE_ELM_N-1) uint8_t
* 返回值:Flash 字符串指针(恒非 NULL;越界或非元件事件回 "--") const char *
* 限定条件:无
* 函数说明:名称最长 4 字(零序过压/过流Ⅰ段),名列可用宽约 94 px = 7 汉字
* (口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条)
*/
const char *ui_w_soe_elm_name(uint8_t a_elm);
/*
* 函数功能:总行数(每条记录 2 行;空态 = 单条伪行"无记录")
* 入口参数:a_inst - SOE 页实例 ui_w_soe_t *
* 返回值:总行数 uint16_t
* 限定条件:无(未注入 reader 视同 0 条)
*/
uint16_t ui_w_soe_count(ui_w_soe_t *a_inst);
/*
* 函数功能:逐行取数(偶数行 = 时标,奇数行 = 元件 + 事件类型)
* 入口参数:a_inst - SOE 页实例 ui_w_soe_t *
* a_row - 全局行下标 uint16_t
* a_name - 名输出缓冲(框架 UI_BUF_STRLEN) char *
* a_value - 值输出缓冲(同上) char *
* 返回值:无
* 限定条件:页面打开中(框架绘制/按键上下文,uiTask)
* 函数说明:时标行——时钟无效(IV 位)或时标为空时名列显示 "--",
* **不显示猜测时间**(诚实性判据,同待机屏空串先例);
* 内容行——元件事件名 = 元件、值 = 动作/返回;非元件事件名 = 类型、
* 值 = 空串(值列宽约 59 px = 4 汉字,非元件事件词放这里放不下)
*/
void ui_w_soe_refresh(ui_w_soe_t *a_inst, uint16_t a_row, char *a_name, char *a_value);
#ifdef __cplusplus
}
#endif
#endif /* UI_W_SOE_H */
+20
View File
@@ -937,6 +937,11 @@
<FileType>1</FileType>
<FilePath>..\App\fs\vol_lock.c</FilePath>
</File>
<File>
<FileName>vol_idle_sync.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\fs\vol_idle_sync.c</FilePath>
</File>
<File>
<FileName>time_sync.c</FileName>
<FileType>1</FileType>
@@ -1032,6 +1037,11 @@
<FileType>1</FileType>
<FilePath>..\App\algo\freq.c</FilePath>
</File>
<File>
<FileName>ui_data_feed.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\app\ui_data_feed.c</FilePath>
</File>
</Files>
</Group>
<Group>
@@ -3541,6 +3551,16 @@
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_style.c</FilePath>
</File>
<File>
<FileName>ui_data.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_data.c</FilePath>
</File>
<File>
<FileName>ui_w_soe.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\UI\ui_w_soe.c</FilePath>
</File>
</Files>
</Group>
<Group>
@@ -55,7 +55,7 @@ scanned 13 files; unique non-ASCII chars: 78
-> D:\Code\DTU Code\STM32F4-Base\ui_cn_chars.txt
```
产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 179 字,含 ▶/,/: 等符号;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,源码未用前由 font-charset 门禁记 WARN 放行),可留作字符集记录。
产物 `ui_cn_chars.txt` 为本工程实际显示的汉字清单(当前 183 字,含 ▶/,/: 等符号;其中 60 字为 2026-10-25 按页面扩充规划显示串预置,同日道E 落 SOE 记录显示名映射时新用 采/样/恢/效 4 字;清单里源码暂未在用的字由 font-charset 门禁记 WARN 放行),可留作字符集记录。
---
@@ -152,7 +152,7 @@ OK 58192 bytes -> ...\Drivers\BSP\UI\ui_font_cn_12.c
1. **1bpp 细笔画发虚**:宋体在 1bpp 下可能丢像素。若上板/客户端中文偏淡,把第 4 节 `--bpp` 改为 `2` 重生成即可(子集小,体积可接受)。
2. **字符集变更**:以后新增中文菜单项,须重跑第 3、4 节(重新提取 `ui_cn_chars.txt` 并生成),否则新字显示为方框。
3. **编码**:UI 源文件须为 **UTF-8(带 BOM)**,LVGL 按 UTF-8 解码中文串;字体 `.c` 为纯 ASCII,无编码风险。当前 Keil 已加 Misc Controls 抑制项,无需额外 `--locale=english`。
4. **字体体积**:12px + 1bpp + 179 字(含 ASCII/符号,共 274 字形)源文件 ≈ 56.8KB;ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。
4. **字体体积**:12px + 1bpp + 183 字(含 ASCII/符号,共 278 字形)源文件 ≈ 57.8KB;ROM 增量以 `.map` 为准(2026-10-25 扩 60 字实测 `RO-data` +1,528 B、RAM 0;同日 SOE 显示名再用 4 字实测 `RO-data` +108 B、RAM 0——1bpp 12px 一个字约 27 B,含字形描述符);改 bpp 2 约翻倍,仍在 F407 Flash 余量内。
5. **换字体**:黑体用 `simhei.ttf`(单文件免抽取);其余字体同理替换 `--font` 路径后重生成。
---
+35 -2
View File
@@ -147,6 +147,7 @@ flowchart TB
#define UI_DATA_Q_OV 0x04u /* 越限:超过量程/削顶 */
#define UI_DATA_Q_SB 0x08u /* 取代值 */
#define UI_DATA_Q_BL 0x10u /* 闭锁 */
#define UI_DATA_Q_RAW 0x20u /* 原始码值域:val 不是工程量而是转换器码值(TEMP 用) */
typedef struct {
float val; /* 数值;口径由 ui_data_rt_t.eng 决定 */
@@ -157,7 +158,8 @@ typedef struct {
uint32_t seq; /* 发布序号(单调递增;页面据此判"是否需重建行缓存") */
uint32_t tick_ms; /* 发布时刻(tick 域,差值法可比) */
uint8_t valid; /* 0 = 尚无首帧(页面显示 "--") */
uint8_t eng; /* 0 = 原始码值(未标定)/ 1 = 二次值 / 2 = 一次值 */
uint8_t eng; /* 量纲口径:0 = ADC 引脚电压(现状)/ 1 = 二次值 /
* 2 = 一次值(预留)——真值语义见下方"要点"第 1 条 */
ui_data_yc_t yc[UI_DATA_YC_N];
uint64_t yx_bits; /* 遥信位图:bit i = 第 i 点"合/动作" */
uint64_t yx_valid; /* 遥信有效位图:bit i = 1 该点有硬件来源 */
@@ -170,7 +172,7 @@ typedef struct {
**要点**:
- **`eng` 字段是诚实性的关键**:V0.1 未标定,`val` 是 ADC 码值(千级)。页面据此显示"未标定"角标或直接显示码值,**不会把码值当伏特**。标定落地后 `eng` 改 1,页面自动按工程量显示,**无代码改动**。
- **`eng` 字段是诚实性的关键**(【2026-10-25 落地注】本条原写"0 = 原始码值(未标定)、`val` 是 ADC 码值(千级)",那是标定落地前的旧口径,已按现场改真值语义):标定层 `adc_cal` 的 ROM 默认系数按**引脚侧 LSB**定死(TPAFE 152.59 µV/码、STM32 805.66 µV/码),采样环折算出来的 `val` 现在是 **ADC 引脚电压 V**——既不是千级码值,也不是端子二次值。承诺边界的依据是 `docs/任务规划/数据可信.md` §2.6:这一波只承诺到"引脚电压","端子二次值"要等变比确认与两点标定。所以页面文案是 `eng=0` → "未标定"、`eng=1` → "二次值",而不是"码值";升档条件写在 `App/app/ui_data_feed.c` 的 `feed_eng()`——**电气 8 路 `adc_cal_flags()` 的 VALID 全置位才升**(整帧共用一个 `eng`,只要还有一路是引脚电压就不能给整帧贴"二次值"标签,宁可整体保守)。TEMP 不参与线性标定 → 该路恒带 `UI_DATA_Q_RAW`,页面据此不挂单位、不与其他路互比。
- **字符串只用定长数组**:`clock[20]` 在结构体内,无指针、无生命周期问题(跨层传指针是经典事故源)。
- **`yx_valid` 与 `yx_bits` 分开**:无效点不参与显示、不参与变位判断(沿用 104 点表的既有做法,避免"IV 通道抖动成变位")。
@@ -198,6 +200,21 @@ typedef struct {
- **只允许尾部追加字段**,结构体开头保留 `seq/valid`。
- 追加字段必须同时升 `UI_DATA_VER`(编译期常量),并在 `ui_data_feed` 里填默认值。
- **禁止删除或重排字段**(页面按名字取,重排是静默错位)。
### 4.4 通道三:装置信息 `ui_data_info_t`(2026-10-25 落地新增)
落地时比本文原稿多出一条同构通道,承载**既不属于 200 ms 实时量、也不属于校验和门控参数帧**的那批量:
| 字段 | 内容 | 为什么不塞进已有的两条通道 |
|---|---|---|
| `model` / `sw_ver` / `hw_ver` / `build_date` / `sw_sum` | 装置身份(型号、软硬件版本、程序日期、软件校验) | 编译期常量,一辈子不变;塞进 `rt` 等于每 200 ms 白拷约 60 B 字符串 |
| `uptime_s` | 开机运行时长 | 每秒都在变;塞进 `par` 会被"校验和没变就不重发"的门控冻住(页面显示一个不再增长的时长 = 假数据) |
| `port_104` / `port_disp` / `ftp_user` | 通讯参数(104 端口、远程显示端口、FTP 用户名) | 值必须在 App 侧取(页面不得自己 include 协议头,`docs/UI设计/页面扩充详细方案.md` §3.5),但它们是静态量,与遥测节拍无关 |
- 结构与两条主通道同构:`seq` + `valid` 打头、定长字符数组、零指针、由 `ui_data` 盖章序号、由 `App/app/ui_data_feed.c` 独家填充,随实时帧同节拍发布。
- **公共地址 CA 故意不在本帧**:固件里 CA 是逐帧从 ASDU 头解析的,没有集中常量可取;等 104 侧给出查询点再按 `docs/UI设计/UI_数据供给设计说明.md` §4.3 的扩展规则追加字段并升 `UI_DATA_VER`。
- 版本类字段目前是 `App/app/ui_data_feed.c` 里的宏(仓库内没有产品级版本登记表,值沿用页面既有显示值,有正式口径后只改那一处);**软件校验位在拿到构建期 CRC 生成步骤之前一律显示 `--`**,不抄参考项目遗留值。
> 另一处落地细化:`ui_data_rt_t` 除 `yc[]`(语义通道序基波有效值)外还带一条 `ph[]` 相量实例槽带(`ph_n` 给实际槽数)。原因是谐波(`Ia` 的二次/五次)无法在"语义通道"下标空间里表达——同一通道的谐波会与基波撞位,而交流量页要显示它们(`docs/UI设计/页面扩充详细方案.md` §3.1.1 第 4/5 行)。两条带的元素同形(数值 + 品质),页面行表按需选一条。
---
@@ -351,6 +368,14 @@ void ui_w_records_bind(const ui_rec_reader_t *reader); /* App 启动时注入
基线"只减不增"——**本设计顺带把存量债还掉三条**,而不是新增。
> 另外两条(`net_cfg` / `ftp_cfg` → `cfg_file.h`)属配置引擎,与 UI 无关,不在本文范围。
### 7.4 SOE 记录:同构 reader + UI 侧显示名映射(2026-10-25 落地新增)
`/log/soe.log` 是**二进制**定长 64 B 记录(时标/类型/元件号/序号/值),不是"时间戳 + 文本描述",不能与 `ui_w_records` 混读,故另立同构组件 `Drivers/BSP/UI/ui_w_soe.c`:
- **注入式 reader** `ui_soe_reader_t`(`count()` + `get(idx, &view)`,错误码 0/-1/-2 与 `ui_rec_reader_t` 同族);`get` 返回的是 **UI 侧视图结构** `ui_w_soe_rec_t`(时标串 + 不透明类型号 + 不透明元件号 + 标志 + 序号/值),适配层负责解码与 CP56 → 文本换算,UI 不 include `App/log/soe_rec.h`。
- **显示名映射在 UI 侧**(`ui_w_soe_type_name()` / `ui_w_soe_elm_name()`):类型与元件都是**表现层文案**,不进快照也不回落到 App 层(同 `docs/UI设计/UI_数据供给设计说明.md` §8 的格式化归属结论)。UI 侧另立一套不透明编号常量(`UI_W_SOE_T_*` / `UI_W_SOE_ELM_*`),其**数值与 App 枚举逐项相等**——这条等价关系不靠文档转述,由主机单测在同一 TU 里 include 两侧头逐项核对(`test/host/ui_data/` 的用例 `TC-UIDATA-012`/`013`,2026-10-25 现场核对结果:类型 1..8 + 哨兵 9、`ELM_NONE=0xFF`、`FLG_IV=0x80`、元件 0..8 与 `PE_COUNT=9`)。
- **未标定闭锁/标定复归两类新事件已进映射表**(`UI_W_SOE_T_CAL_BLOCK=7` / `UI_W_SOE_T_CAL_RESUME=8`,缺字 采/样/恢/效 已走字体重生成 + 像素回归)。单测额外钉了两条容易悄悄发生的错:八类事件必须有名(忘配名只会在屏上显示 `--`,像素门禁照不出来)、"采样闭锁/恢复"与"标定闭锁/复归"四名必须互不相同(同名 = 现场把未标定闭锁读成采样故障)。
- **本批不绑定 reader**:落盘侧还没有只读 API(`App/log/soe_store.c` 只有 `post/drain/dropped`),需要加法式的 `soe_count()` / `soe_read(idx, &rec)`(FatFs `f_open` + `f_lseek`,与 `syslog_get_record` 同构);该文件属道F 的潜在写域,先后次序由 Lead 排。未注入期间组件按空态渲染("无记录"伪行),因此本批**不产生任何像素变化**。
---
@@ -408,6 +433,12 @@ typedef struct {
这是本设计对"可测性"的直接贡献:**页面数据来源可注入 → 像素回归不再依赖真实硬件**。
> 【2026-10-25 落地注】`simulator/main.c` 的实现形态与两条原稿未写死的细节:
> 1. 固定帧由 `sim_publish_data_frames()` 一次性发布,**无头与窗口两个初始化点都调**(与记录 reader 注入同址、同"先于 `ui_menu_init`"纪律);
> 2. 固定帧里的定值**不手抄 40 个数字**,而是把 `App/algo/prot_set.c`(零平台依赖的纯算法,双端共享核心)链进模拟器、`prot_set_init()` 后取出厂默认——抄表等于造第二真相(同 `scripts/check_font_charset.py` 的"单一规则源"口径);
> 3. `build_date` 用**固定串**而不是 `__DATE__`:否则跨天重建会把装置信息页的黄金参考刷掉,而黄金参考是判据本身。
> 4. 原稿设的 `--live` 合成正弦模式**尚未实现**(当前无消费者,等动态刷新真的需要人眼观察时再加)。
### 9.3 与"虚拟注入"的关系
`docs/UI设计/页面补齐规划.md` §7 提的厂家调试"虚拟注入",**天然落在 `ui_data_feed` 上**:注入值只要在 feed 组装时覆盖对应字段即可,**页面完全不知情**。
@@ -458,6 +489,8 @@ typedef struct {
**步骤1~步骤2 是关键路径**:它们不产出任何用户可见效果,但决定后面所有页面是否可信。**不要跳过步骤1 直接改页面**——那会退化成"页面里塞假数据 2.0"。
> 【2026-10-25 落地进度】步骤1、步骤2 已落(道E task-14,分支 `task/ui-data-feed`):`ui_data` 三通道 + 主机单测 82 项全绿(`test/host/ui_data/`,已挂 `ui-data-unit-test` 钩子)、`ui_data_feed` 已在 `App/task/ui_task.c` 主循环接线并在建页前发首帧。**改动两处与原稿不同**:① 实时帧的 `eng` 与品质位按标定层真值语义落地(见 `docs/UI设计/UI_数据供给设计说明.md` §4.1 的落地注);② 新增第三条通道 `info`(同文档 §4.4)。**写路径(步骤5 的 `ui_data_param_request/state`)随整定 4 页一起落**,本批无消费者,先落等于落一段永不执行的代码。步骤3 起(页面接线)在道E 第二个工作项。
---
## 12. 验收判据
+1 -1
View File
@@ -48,7 +48,7 @@ flowchart TD
|---|---|---|---|
| C1 | 屏幕 | 160×160,1bpp 单色(`LV_COLOR_FORMAT_I1`),无物理屏(远端客户端显示) | `ui_port.h` |
| C2 | 字体 | **唯一**:`ui_font_cn_12`(宋体 12px 1bpp)。汉字 12px/字,ASCII 6px/字,行高 13px。需要其他字号 = 大工程(新字体 + 双端 `lv_conf.h`),默认禁止 | `ui_font_cn_12.c` |
| C3 | 字符集 | 子集字体(当前 179 汉字/符号 + ASCII 0x20-0x7E,共 274 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` |
| C3 | 字符集 | 子集字体(当前 183 汉字/符号 + ASCII 0x20-0x7E,共 278 字形);**缺字显示空白**;新字必须重生成字体 | `tools/font_gen/` |
| C4 | LVGL 内存池 | 16KB(`LV_MEM_SIZE`,2026-10-15 由 40KB 收缩;实测峰值 6,568 B = 池的 40%);任一时刻只放"当前导航路径 + 当前内容页" | `Lib/lv_conf.h` |
| C5 | 文本 | 一律静态字符串(`lv_label_set_text_static`),禁止动态字符串/strdup | 架构文档 |
| C6 | 编码 | `Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h` 为 **UTF-8 带 BOM**;改动只用 Edit 工具,**禁止** PowerShell/cmd 整文件重写(U+FFFD 不可逆) | AGENTS.md 铁律 |
+1 -1
View File
@@ -38,7 +38,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
| LVGL 内存池 | 16 KB(`LV_MEM_SIZE`,2026-10-15 由 40 KB 收缩;实测峰值 6,568 B = 池的 40%) | `Lib/lv_conf.h` |
| 刷新周期 | 10 ms(`ui_port_step(10)` + `osDelay(10)`) | `ui_task.c` |
| 推流端口 | 7003(TCP,与 DTU-RemoteLCD 客户端对齐) | `ui_net_sink.h` |
| 默认字体 | `ui_font_cn_12`(12px 1bpp,179 汉字/符号 + ASCII 0x20–0x7E,共 274 字形) | `Lib/lv_conf.h` |
| 默认字体 | `ui_font_cn_12`(12px 1bpp,183 汉字/符号 + ASCII 0x20–0x7E,共 278 字形) | `Lib/lv_conf.h` |
| uiTask 栈 | 16384 words(BelowNormal) | `Src/freertos.c` |
### 1.4 硬约束(改 UI 必须遵守)
@@ -47,10 +47,10 @@ C2 dhara 失败类 +84;RO 另 +208,全是 C2 的失败类名表)→ `ER_IR
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 237,140 B (231.58 KB) | 1,048,576 B (1024 KB) | 22.6% |
| RW_IRAM1 | 75,784 B (74.01 KB) | 131,072 B (128 KB) | 57.8% |
| ER_IROM1 | 238,708 B (233.11 KB) | 1,048,576 B (1024 KB) | 22.8% |
| RW_IRAM1 | 76,512 B (74.72 KB) | 131,072 B (128 KB) | 58.4% |
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
| **Flash (ROM 合计)** | **238,364 B (232.8 KB)** | 1,048,576 B (1024 KB) | **22.7%** |
| **Flash (ROM 合计)** | **239,964 B (234.3 KB)** | 1,048,576 B (1024 KB) | **22.9%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
+4 -4
View File
@@ -15,19 +15,19 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
| ER_IROM1 (Flash) | 237,140 B | 1,048,576 B | 22.6% | 参考 |
| ER_IROM1 (Flash) | 238,708 B | 1,048,576 B | 22.8% | 参考 |
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
| RW_IRAM1 | 75,784 B | 131,072 B | 57.8% | PASS |
| RW_IRAM1 | 76,512 B | 131,072 B | 58.4% | PASS |
| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |
|---|---|---|---|
| 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB |
| 存储 | 7,056 B | 12 KB | OK |
| UI | 30,426 B | 16 KB | ⚠ 超 13.7 KB |
| UI | 31,135 B | 16 KB | ⚠ 超 14.4 KB |
| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 |
| 应用 | 15,406 B | 8 KB | ⚠ 超 7.0 KB |
| 其它 | 9,208 B | — | 参考 |
| 其它 | 9,216 B | — | 参考 |
<!-- MEM_BUDGET:END -->
- **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)——
@@ -391,6 +391,16 @@ FTL 之上还有一个 **单页写回缓存 (write-back cache)**:
- **写**: 写入缓存 → 标记脏
- **全页写入**: 直接刷出 (跳过缓存)
- **同步 (CTRL_SYNC)**: 刷出脏页 + dhara_map_sync
- **空闲 flush 兜底(2026-10-25,道F 批 2)**: defaultTask 主循环按"存储就绪 +
卷空闲(`vol_lock_wait(0)` try)+ 周期节流(5 s)"周期性下发一次 `CTRL_SYNC`,
把"缓存留脏、之后再无调用触发 sync"的漏网脏页其掉电驻留窗封顶为一个周期;
卷锁被持有(syslog 落盘 / 配置保存 / lftpd open...close 进行中)时跳过,
**不改变文件持有期的既有语义**。clean 态 = 脏标志 + journal 双短路、零 NAND
触点;脏态成本与一次 `f_close` 的同步段同型。实现:`App/fs/vol_idle_sync.[ch]`
(单测 `test/host/vol_idle_sync/`)。
- ⚠ **边界(勿当成掉电保护)**: 本板**无掉电检测硬件**,"掉电前强制 flush"
不可行;单次写事务进行中掉电、以及 FTP STOR 文件持有期的落盘边界,均不因
空闲兜底改变。
---
+1 -1
View File
@@ -1,6 +1,6 @@
# 门禁逐项说明(每种门禁一篇)
> 这一目录回答一个问题:**提交时那 40 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
> 这一目录回答一个问题:**提交时那 44 个钩子到底在干什么、拦谁、放谁、什么时候会骗人。**
> 每个门禁一篇,八段固定结构:查什么 / 为什么要它 / 触发面 / 报错怎么读 / 好处 /
> **限制与盲区** / 豁免与基线 / 改门禁本身要注意什么。**最后一节是这套文档存在的理由**——
> 一份只看"拦了什么"的门禁文档,会让人以为绿就是对的。
+2
View File
@@ -97,7 +97,9 @@ SIM_EXEMPT = [
("App/algo/adc_cal.c", "L1.5 标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"),
("App/algo/meas.c", "L2 遥测快照发布纯算法(唯一发布者 = MCU 侧 adcTask 200 ms 分频;104/UI 侧消费属卡 9/UI 波次另行接线,模拟器无消费路径;L0.5 单测 test/host/meas 覆盖),模拟器无消费路径"),
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
("App/app/ui_data_feed.c", "UI 快照装配器(依赖 phasor/prot_set/iec104 端口常量等固件侧数据源);模拟器不跑装配链,快照由 simulator/main.c 直接发布固定帧,模拟器无消费路径"),
("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"),
("App/fs/vol_idle_sync.c", "MCU 存储属主后台空闲同步(FatFs disk_ioctl + 卷锁 + sys_events),L0.5 覆盖于 test/host/vol_idle_sync/,模拟器无消费路径"),
("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"),
("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"),
("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
+44
View File
@@ -1708,6 +1708,28 @@
"owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH 先例;2026-10-25 道A 卡2 建族):属主登记处 = 单测代码目录,不逐号补文档",
"cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-UIDATA",
"class": "TC",
"title": "主机侧单测用例号:UI 数据快照层与跨层契约一致性(test/host/ui_data,道E task-14)",
"pattern": "\\bTC-UIDATA-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/ui_data/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-UIDATA-014",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCCAL 先例;2026-10-25 道E task-14 建族):属主登记处 = 单测代码目录,不逐号补文档;本族号同时是 UI 不透明编号与 App 枚举的现场核对锚(SOE 类型/元件号)",
"cursor_note": "游标按建族时实测最大母号收录(2026-10-25):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-PROTDIM",
"class": "TC",
@@ -1730,6 +1752,28 @@
"owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH/TC-HOST-ADCCAL 先例;2026-10-25 道A 卡7 建族):属主登记处 = 单测代码目录,不逐号补文档",
"cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-VOLSYNC",
"class": "TC",
"title": "主机侧单测用例号:写回缓存空闲 flush(test/host/vol_idle_sync,道F 批 2)",
"pattern": "\\bTC-VOLSYNC-[0-9]{1,4}[a-z]?\\b",
"digit_width": [
2,
4
],
"owner_docs_glob": [
"test/host/vol_idle_sync/*"
],
"scope": "global",
"frozen": false,
"cursor": "TC-VOLSYNC-012",
"cursor_owner_only": true,
"check_dangling": true,
"allow_orphans": true,
"subitem_ok": true,
"owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH/TC-HOST-ADCCAL 先例;2026-10-25 道F 批 2 建族):属主登记处 = 单测代码目录,不逐号补文档",
"cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌"
},
{
"name": "TC-HOST-MEAS",
"class": "TC",
+1 -1
View File
@@ -7,4 +7,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
2026-10-22 213436 18652 1188 131500 232088 22.1 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
2026-10-23 213676 18860 1188 131500 232536 22.2 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
2026-10-25 215744 21396 1224 131984 237140 22.6 75784 57.8 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
2026-10-25 217204 21504 1256 132680 238708 22.8 76512 58.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
1 date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
7 2026-10-22 213436 18652 1188 131500 232088 22.1 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
8 2026-10-23 213676 18860 1188 131500 232536 22.2 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736
9 2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
10 2026-10-25 215744 217204 21396 21504 1224 1256 131984 132680 237140 238708 22.6 22.8 75784 76512 57.8 58.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736
+13 -13
View File
@@ -1,25 +1,25 @@
{
"built_at": "2026-10-25T06:07:53+08:00",
"fingerprint": "9b2b3b209d19275aea4744529bdb7d3b14087c27765e40fb7e25ccdafe0e721d",
"fingerprint_files": 1396,
"built_at": "2026-10-25T10:03:18+08:00",
"fingerprint": "a63f8cb23657ecbb642a47b1e9edee90328086c8d7335e479b428fdd8d6516f5",
"fingerprint_files": 1404,
"fingerprint_source": "git-index",
"gated_regions": [
"RW_IRAM1",
"RW_CCM"
],
"git_head": "73f9e104928472db3084e7d03acdd3e6ac68cd89",
"git_head": "98664a11ed6f7ef80d09ba134998cb9e5f7c47c7",
"map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map",
"map_mtime": 1792879590,
"map_mtime": 1792893770,
"pct": {
"ER_IROM1": 22.62,
"ER_IROM1": 22.76,
"RW_CCM": 87.89,
"RW_IRAM1": 57.82,
"RW_IRAM1": 58.37,
"UNINIT_CCM": 18.75
},
"regions": {
"ER_IROM1": {
"max": 1048576,
"size": 237140
"size": 238708
},
"RW_CCM": {
"max": 65280,
@@ -27,7 +27,7 @@
},
"RW_IRAM1": {
"max": 131072,
"size": 75784
"size": 76512
},
"UNINIT_CCM": {
"max": 256,
@@ -37,10 +37,10 @@
"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": 215744,
"ro_data": 21396,
"rw_data": 1224,
"zi_data": 131984
"code": 217204,
"ro_data": 21504,
"rw_data": 1256,
"zi_data": 132680
},
"threshold_pct": 92.0,
"worktree_dirt_at_stamp": 0,
+76 -14
View File
@@ -88,6 +88,14 @@
"lines_covered": 27,
"lines_valid": 29
},
"App/fs/vol_idle_sync.c": {
"func_rate": 1.0,
"funcs_covered": 1,
"funcs_valid": 1,
"line_rate": 1.0,
"lines_covered": 21,
"lines_valid": 21
},
"App/fs/vol_lock.c": {
"func_rate": 1.0,
"funcs_covered": 3,
@@ -303,6 +311,22 @@
"line_rate": 0.9643,
"lines_covered": 108,
"lines_valid": 112
},
"Drivers/BSP/UI/ui_data.c": {
"func_rate": 0.9,
"funcs_covered": 9,
"funcs_valid": 10,
"line_rate": 0.8,
"lines_covered": 52,
"lines_valid": 65
},
"Drivers/BSP/UI/ui_w_soe.c": {
"func_rate": 1.0,
"funcs_covered": 6,
"funcs_valid": 6,
"line_rate": 0.9355,
"lines_covered": 58,
"lines_valid": 62
}
},
"generated_by": "scripts/check_test_coverage.py --update-baseline",
@@ -564,6 +588,21 @@
"lines_total": 99,
"ok": true
},
"ui_data": {
"files": {
"Drivers/BSP/UI/ui_data.c": 65,
"Drivers/BSP/UI/ui_w_soe.c": 62
},
"lines_total": 127,
"ok": true
},
"vol_idle_sync": {
"files": {
"App/fs/vol_idle_sync.c": 21
},
"lines_total": 21,
"ok": true
},
"vol_lock": {
"files": {
"App/fs/vol_lock.c": 17
@@ -593,6 +632,7 @@
"App/cfg/cfg_file.c",
"App/cfg/cfg_file_store_mcu.c",
"App/cfg/prot_set_store.c",
"App/fs/vol_idle_sync.c",
"App/fs/vol_lock.c",
"App/log/console_log.c",
"App/log/soe_rec.c",
@@ -619,20 +659,22 @@
"Drivers/BSP/NET/net_select.c",
"Drivers/BSP/NET/net_socket.c",
"Drivers/BSP/NETCFG/ftp_cfg.c",
"Drivers/BSP/NETCFG/net_cfg.c"
"Drivers/BSP/NETCFG/net_cfg.c",
"Drivers/BSP/UI/ui_data.c",
"Drivers/BSP/UI/ui_w_soe.c"
],
"totals": {
"files_generated_excluded": 5,
"files_tested": 38,
"files_untested": 115,
"func_rate": 0.9258,
"funcs_covered": 412,
"funcs_valid": 445,
"line_rate": 0.8965,
"lines_covered": 5802,
"lines_valid": 6472,
"modules_ok": 35,
"modules_total": 35
"files_tested": 41,
"files_untested": 120,
"func_rate": 0.9264,
"funcs_covered": 428,
"funcs_valid": 462,
"line_rate": 0.8962,
"lines_covered": 5933,
"lines_valid": 6620,
"modules_ok": 37,
"modules_total": 37
},
"untested_sources": [
{
@@ -656,7 +698,7 @@
"path": "Drivers/BSP/UI/ui_content_page.c"
},
{
"code_lines": 281,
"code_lines": 282,
"path": "App/task/default_task.c"
},
{
@@ -667,6 +709,10 @@
"code_lines": 264,
"path": "Drivers/BSP/SD2506/sd2506.c"
},
{
"code_lines": 242,
"path": "App/app/ui_data_feed.c"
},
{
"code_lines": 224,
"path": "App/task/adc_task.c"
@@ -723,13 +769,17 @@
"code_lines": 98,
"path": "Drivers/BSP/SD2506/sd2506.h"
},
{
"code_lines": 95,
"path": "App/task/ui_task.c"
},
{
"code_lines": 93,
"path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h"
},
{
"code_lines": 92,
"path": "App/task/ui_task.c"
"code_lines": 88,
"path": "Drivers/BSP/UI/ui_data.h"
},
{
"code_lines": 83,
@@ -775,6 +825,10 @@
"code_lines": 50,
"path": "Drivers/BSP/UI/ui_w_setlist.h"
},
{
"code_lines": 47,
"path": "Drivers/BSP/UI/ui_w_soe.h"
},
{
"code_lines": 46,
"path": "App/log/soe_rec.h"
@@ -1039,6 +1093,14 @@
"code_lines": 11,
"path": "Drivers/BSP/dbg_cfg.h"
},
{
"code_lines": 10,
"path": "App/app/ui_data_feed.h"
},
{
"code_lines": 10,
"path": "App/fs/vol_idle_sync.h"
},
{
"code_lines": 10,
"path": "App/task/default_task.h"
+12 -1
View File
@@ -58,6 +58,7 @@ include_directories(
# 2026-10-02 测试目录重构后头文件移入 test/board/)
${PROJECT_ROOT}/App/cfg # cfg_file.h / cfg_file_store.h(通用配置文件引擎,双端共享核心)
${PROJECT_ROOT}/App/log # sys_log.h(运行/自检记录引擎,双端共享核心)
${PROJECT_ROOT}/App/algo # prot_set.h(定值模型纯算法,双端共享核心;见 ALGO_SOURCES)
${PROJECT_ROOT}/Drivers/BSP # dbg_log.h(使用 simulator 的 dbg_cfg.h 配置)
${SDL2_DIR}/include # SDL2 头文件
)
@@ -100,6 +101,8 @@ set(UI_SOURCES
${UI_DIR}/ui_w_editor.c
${UI_DIR}/ui_w_setlist.c
${UI_DIR}/ui_page_action.c
${UI_DIR}/ui_data.c # 数据快照存侧(道E task-14;写侧 App/app/ui_data_feed.c 不进模拟器,见 scripts/check_registration.py)
${UI_DIR}/ui_w_soe.c # SOE 记录组件(道E task-14;reader 注入点待绑定,未绑定按空态渲染)
)
# 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程)
@@ -115,6 +118,14 @@ set(NETCFG_SOURCES
sim/cfg_file_store_sim.c
)
# 保护定值模型(App/algo/prot_set.c 零平台依赖,双端共享核心)。
# 模拟器只需要"出厂默认定值"来发布 UI 快照帧(main.c 的 sim_publish_data_frames),
# 不在这里复制一份 40 项数字——抄表就等于造第二真相(同 scripts/check_font_charset.py
# "单一规则源"的同一治理口径)。
set(ALGO_SOURCES
${PROJECT_ROOT}/App/algo/prot_set.c
)
# 网络后端(net_* 核心 API 的 Winsock 实现 + UI 推流平台后端;MCU 端另编 net_socket.c 与 _mcu 后端)
set(NET_SOURCES
sim/net_socket_sim.c
@@ -124,7 +135,7 @@ set(NET_SOURCES
#=============================================================================
# 构建可执行文件
#=============================================================================
add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES} ${SYSLOG_SOURCES} ${NET_SOURCES})
add_executable(lvgl_sim WIN32 ${SIM_SOURCES} ${UI_SOURCES} ${NETCFG_SOURCES} ${SYSLOG_SOURCES} ${NET_SOURCES} ${ALGO_SOURCES})
target_compile_definitions(lvgl_sim PRIVATE
LV_CONF_INCLUDE_SIMPLE # 让 lvgl.h 找到 lv_conf.h
+1 -1
View File
@@ -262,7 +262,7 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化
- `LV_USE_THEME_MONO = 1` 且 `sim_port_init` 中调用了 `lv_theme_mono_init`
- 主循环调用了 `lv_refr_now(NULL)`(PARTIAL 模式保证首帧)
- 没有重复设置 tick(SDL 驱动内部已 `lv_tick_set_cb`,**不要**再 `SDL_AddTimer` + `lv_tick_inc`)
4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 179 汉字/符号 + ASCII,共 274 字形),缺字渲染为空白。
4. **中文显示空白**:字体 `ui_font_cn_12` 是子集字体(当前 183 汉字/符号 + ASCII,共 278 字形),缺字渲染为空白。
补字流程见 `docs/UI设计/UI_中文字体生成与配置.md`(`tools/font_gen/` 两步脚本)。
5. **改了 UI 代码但模拟器没变化**:确认改的是 `Drivers/BSP/UI/ui_menu.c`(共享原件)而非本目录副本(本目录无副本),然后重新 `cmake --build`。
6. **编码**:UI 源码为 UTF-8(带 BOM),MSVC 已加 `/utf-8`。**不要**用 PowerShell `Set-Content` 等重写含中文的文件(编码铁律,见根目录 `AGENTS.md`)。
+97
View File
@@ -69,6 +69,8 @@
#include "cfg_file_store.h"
#include "sys_log.h"
#include "ui_w_records.h" /* task-6 记录 reader 注入点(镜像 App/task/ui_task.c 的绑定) */
#include "ui_data.h" /* task-14 快照存侧:模拟器在此直接发布固定帧(装配器不进模拟器) */
#include "prot_set.h" /* task-14 定值出厂默认(App/algo 纯算法,双端可编,见 simulator/CMakeLists) */
/* 屏幕尺寸(与 MCU 端一致) */
#define SIM_SCREEN_W 160
@@ -171,6 +173,95 @@ static int sim_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len,
static const ui_rec_reader_t s_rec_reader = { sim_rec_count, sim_rec_get };
/*
* 函数功能:发布固定数据帧(镜像 App/app/ui_data_feed.c 的装配结果)
* 入口参数:无
* 返回值:无
* 限定条件:ui_menu_init 之前调用(无头与窗口两个初始化点共用,先于建页)
* 函数说明:模拟器不链接装配器(无硬件数据源;真值还会让像素参考天天变),
* 这里发布一帧**固定合成值**,页面数据来源因此可注入——像素回归不再
* 依赖真实硬件(UI_数据供给设计说明.md §9.2)。要点三条:
* 1. eng 固定 = UI_DATA_ENG_PIN,与真机未标定态同口径(数据可信.md
* §2.6 承诺边界 = 引脚电压),页面据此显示"未标定";
* 2. build_date 用固定串而**不是** __DATE__——否则跨天重建会把装置
* 信息页的黄金参考刷掉(黄金参考是判据本身,不得随手刷);
* 3. 定值取 prot_set 出厂默认(App/algo/prot_set.c 是纯算法,双端可编),
* 不手抄 40 个数字——抄一次就是一份会漂移的第二真相
*/
static void sim_publish_data_frames(void) {
ui_data_rt_t rt;
ui_data_par_t par;
ui_data_info_t info;
uint8_t i;
ui_data_init();
(void)memset(&rt, 0, sizeof(rt));
rt.valid = 1u;
rt.eng = UI_DATA_ENG_PIN;
rt.tick_ms = 0u;
/* 遥测下标 = adc_semantic_t 语义序(UA/UB/UC/UN/IA/IB/IC/IN/TEMP)。
* 模拟器不 include adc_input.h,故这里用语义序注释对照,不写符号名 */
rt.yc[0].val = 0.8123f; /* UA 基波(引脚电压域) */
rt.yc[0].qds = 0u;
rt.yc[1].val = 0.0f; /* UB:V0.1 硬件削顶无采样源 */
rt.yc[1].qds = UI_DATA_Q_IV | UI_DATA_Q_OV;
rt.yc[2].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* UC 同上 */
rt.yc[3].val = 0.0054f; /* UN 零序电压基波 */
rt.yc[3].qds = 0u;
rt.yc[4].val = 0.0456f; /* IA 基波 */
rt.yc[4].qds = 0u;
rt.yc[5].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IB */
rt.yc[6].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IC */
rt.yc[7].qds = UI_DATA_Q_IV | UI_DATA_Q_OV; /* IN */
rt.yc[8].val = 2048.0f; /* TEMP:码值(不参与线性标定) */
rt.yc[8].qds = UI_DATA_Q_RAW;
/* 相量实例槽(序 = phasor k_inst[]:IA-F1/UA-F1/UN-F1/IA-F2/IA-F5) */
rt.ph_n = 5u;
rt.ph[0].val = 0.0456f;
rt.ph[1].val = 0.8123f;
rt.ph[2].val = 0.0054f;
rt.ph[3].val = 0.0017f; /* IA 二次谐波(涌流制动监视量) */
rt.ph[4].val = 0.0008f; /* IA 五次谐波(选线用) */
for (i = 0u; (i < UI_DATA_PH_MAX) && (i < rt.ph_n); i++) {
rt.ph[i].qds = 0u;
}
for (; i < UI_DATA_PH_MAX; i++) {
rt.ph[i].qds = UI_DATA_Q_IV; /* 未发布的槽:无效,页面显示 "--" */
}
rt.yx_bits = 0u; /* 固定帧无动作态 */
rt.yx_valid = 0x4043C000ULL; /* YX15~18/23/31 有效位图 */
rt.freq_valid = 0u; /* eng=0 期间幅值门恒失效 → 无新鲜读数 */
rt.freq_hz = 0.0f;
rt.clock_ok = 1u;
(void)snprintf(rt.clock, sizeof(rt.clock), "%s", "2026-10-25 08:00:00");
(void)ui_data_publish_rt(&rt);
(void)memset(&par, 0, sizeof(par));
(void)prot_set_init(); /* 出厂默认 + 封存(幂等) */
par.valid = 1u;
par.checksum = prot_set_checksum();
par.locked = prot_set_locked();
par.bad_mask = 0u; /* 装载成功:无越界项 */
for (i = 0u; i < UI_DATA_SET_N; i++) {
par.q10[i] = prot_set_get((prot_set_id_t)i);
}
(void)ui_data_publish_par(&par);
(void)memset(&info, 0, sizeof(info));
info.valid = 1u;
info.uptime_s = 0u;
info.port_104 = 2404u; /* App/task/iec104_serv.h 的 IEC104_SERV_PORT */
info.port_disp = (uint16_t)UI_NET_SINK_PORT; /* 同源常量(BSP 头,模拟器可见) */
(void)snprintf(info.model, sizeof(info.model), "%s", "F30");
(void)snprintf(info.sw_ver, sizeof(info.sw_ver), "%s", "SV0.010");
(void)snprintf(info.hw_ver, sizeof(info.hw_ver), "%s", "HW0.010");
(void)snprintf(info.build_date, sizeof(info.build_date), "%s", "Oct 25 2026");
(void)snprintf(info.sw_sum, sizeof(info.sw_sum), "%s", "--");
(void)snprintf(info.ftp_user, sizeof(info.ftp_user), "%s", "admin");
(void)ui_data_publish_info(&info);
}
/*
* 函数功能:无窗口离屏渲染(可注入键序列)并导出末帧像素
* 入口参数:keys - 键序列(u=上 d=下 l=左 r=右 e=确认 s=返回;空 = 不注键) const char *
@@ -212,6 +303,9 @@ static int run_headless(const char * keys, const char * path, int standby) {
/* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */
ui_w_records_bind(&s_rec_reader);
/* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页,
* 页面任何时刻打开都拿得到帧,不必等装配节拍) */
sim_publish_data_frames();
if (ui_menu_init() != 0) {
return 1;
@@ -607,6 +701,9 @@ int main(int argc, char **argv) {
/* 构建菜单 UI(与 MCU 端共用 ui_menu.c) */
/* task-6 记录 reader 注入(镜像 ui_task.c:先于 ui_menu_init,两端同形) */
ui_w_records_bind(&s_rec_reader);
/* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页,
* 页面任何时刻打开都拿得到帧,不必等装配节拍) */
sim_publish_data_frames();
if (ui_menu_init() != 0) {
fprintf(stderr, "ui_menu_init failed\n");
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""UI 数据快照层(ui_data)主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/ui_data/ui_data_test.c(它以 #include 纳入
**生产源文件** Drivers/BSP/UI/ui_data.c,并同时 include 契约另一侧的
App/algo 头),在 PC 上离线跑"快照存储 + 发布盖章 + 跨层下标一致性",
失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / 编译运行全部委托 test/host/common/unit_test_harness.py。
为什么要主机侧单测(而不是只用模拟器像素回归):
快照层是页面看到的整个世界,但 ui-golden 只覆盖"已接线的页面"——遥信 27 点、
定值 40 项、遥测 9 路这类"还没接线的通道"错了不会让任何一张参考图变红。
跨层不透明下标(yc 序 = adc_semantic_t、q10 序 = prot_set_id_t)正是解耦换来
的风险面,契约把它钉在编译/测试期(docs/UI设计/UI_数据供给设计说明.md §9.4)。
用法(仓库根目录):
python test/host/ui_data/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误。
设计依据:docs/UI设计/UI_数据供给设计说明.md §9.1 / §12 判据 8(道E task-14);
创建日期:2026-10-25
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 被测侧(BSP/UI)与契约另一侧(App/algo)都要在包含路径里:测试单元在同一 TU
# 里同时看到两侧头,才有资格断言"页面表的下标指向预期枚举"。
BUILD = TestBuild(
name="UI Data Snapshot Tests",
src=os.path.join(HERE, "ui_data_test.c"),
include_dirs=[
HERE,
os.path.join(ROOT, "Drivers", "BSP", "UI"),
os.path.join(ROOT, "App", "algo"),
os.path.join(ROOT, "App", "log"), # soe_rec.h:SOE 事件类型编号真源
],
summary_marker="=== UI Data Snapshot Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+538
View File
@@ -0,0 +1,538 @@
/*
* 模块名称:UI 数据快照层主机侧单元测试
* 模块功能:在 PC 上离线验证 Drivers/BSP/UI/ui_data.c(快照存储 / 发布盖章 /
* 只读访问)与跨层契约下标空间的一致性。
*
* 被测对象:真实源文件 ../../../../Drivers/BSP/UI/ui_data.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本;本模块零平台依赖,无需任何桩)
*
* 为什么要主机侧单测(而不是只靠模拟器像素回归与台架):
* 1) 快照层是"页面看到的整个世界",它错一处全部页面一起错,而像素回归只看得到
* "当前接了线的页面"——未接线的通道(遥信 27 点、定值 40 项)错了不会让任何
* 一张参考图变红,只有把契约下标逐项钉死才照得出来;
* 2) 跨层"魔法下标"(yc 序 = adc_semantic_t、q10 序 = prot_set_id_t)是解耦的
* 代价,契约把它从运行期风险降级为编译/测试期错误的第 3 条断言就落在这里
* (同一 TU 里同时 include 两侧头,见
* docs/UI设计/UI_数据供给设计说明.md §9.4);
* 3) "首帧之前显示 -- 而不是 0.000"是诚实性判据,板子上电只有一次、台架碰不到,
* 主机侧可以随时复现。
*
* 字符串字面量一律纯 ASCII(编码铁律:断言名是打印用的,不参与渲染);
* 用例名里的通道/点号语义见各 TC 上方注释。
*
* 覆盖点(TC-UIDATA-xxx;属主登记处 = 本目录,族 TC-HOST-UIDATA):
* TC-UIDATA-001 跨层数量一致:UI_DATA_YC_N == ADC_SEM_COUNT、SET_N == PS_ID_COUNT
* TC-UIDATA-002 跨层语义序逐项一致(UA/UB/UC/UN/IA/IB/IC/IN/TEMP = 0..8,
* TEMP 恒为末槽——页面据此判"哪一路是码值域")
* TC-UIDATA-003 首帧之前:三通道读 -1、seq = 0(页面渲染 "--" 的依据)
* TC-UIDATA-004 发布后逐字段一致 + 序号盖章(入帧 seq 被忽略、由本层 +1)
* TC-UIDATA-005 连续发布 seq 单调递增;三通道各自独立计数、内容互不串台
* TC-UIDATA-006 NULL 防御:publish 返 -1 且存量不动;read 返 -2 且输出不动
* TC-UIDATA-007 init 幂等复位(发布后再 init -> 回到无首帧态)
* TC-UIDATA-008 量纲/品质常量:eng 三档互不重合且 PIN=0;六个品质位两两不相交;
* 三帧宽度在内存预算内(快照不是 LVGL 对象,只占 .bss)
* TC-UIDATA-009 遥信位集合 = YX15~18/23/31(六点);bits 与 valid 相互独立
* TC-UIDATA-010 相量槽 ph_n 原样透传(写侧已钳位,本层不再加工)
* TC-UIDATA-011 时钟字段容量足装 "YYYY-MM-DD HH:MM:SS"(19 + NUL)且不越界
* TC-UIDATA-012 SOE 事件类型不透明编号与 App/log/soe_rec.h 枚举逐项相等(含
* 未标定闭锁=7 / 标定复归=8);八类事件全部有显示名、名长不超
* 值列宽、采样与标定两组四名互不相同、越界编号回占位
* TC-UIDATA-013 SOE 元件编号与 App/algo/prot_elm.h 枚举逐项相等(0..8 +
* PE_COUNT == 表长);元件名不超名列宽、非元件/越界回占位
* TC-UIDATA-014 注入 reader 后的两行渲染:时标行/内容行分工、单条读盘缓存
* (同记录两行只读一次)、IV 时标抑制、读失败退化与不绑空态
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-25
*/
#include <stdio.h>
#include <string.h>
/* 生产源文件原样纳入(纯内存叶子,零平台依赖,直接编) */
#include "ui_data.c"
/* 契约另一侧的头:跨层下标空间一致性断言(本目录同时 include BSP 与 App 两侧头,
* 这就是契约 §9.4 第 3 条断言的落点) */
#include "adc_input.h" /* adc_semantic_t:遥测下标真源 */
#include "prot_set.h" /* prot_set_id_t:定值下标真源 */
/* 第二个被测件:SOE 显示名映射组件(道E task-14 落的表现层映射表)。
* 本组件已解除 LVGL 依赖(容量宏本地化,见其 .c 内注释),可直接编进主机单测 */
#include "ui_w_soe.c"
#include "soe_rec.h" /* soe_type_t:事件类型编号真源 */
#include "prot_elm.h" /* prot_elm_id_t:元件编号真源 */
/* ===================== 断言框架 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
} while (0)
/* 造一帧可辨识内容的实时帧(每个字段都填不同值,串台/漏拷立刻暴露) */
static void fill_rt(ui_data_rt_t *p, uint8_t seed) {
uint8_t i;
(void)memset(p, 0, sizeof(*p));
p->seq = 777u; /* 故意填非 0:本层应忽略并自己盖章 */
p->tick_ms = 12345u + seed;
p->valid = 1u;
p->eng = UI_DATA_ENG_PIN;
p->ph_n = (uint8_t)(seed % (UI_DATA_PH_MAX + 1u));
for (i = 0u; i < UI_DATA_YC_N; i++) {
p->yc[i].val = (float)i + 0.5f;
p->yc[i].qds = (uint8_t)(i + 1u);
}
for (i = 0u; i < UI_DATA_PH_MAX; i++) {
p->ph[i].val = (float)i * 2.0f;
p->ph[i].qds = (uint8_t)(0xA0u + i);
}
p->yx_bits = 0x000000010000ULL;
p->yx_valid = 0x4043C000ULL;
p->freq_valid = 1u;
p->freq_hz = 50.02f;
p->clock_ok = 1u;
(void)snprintf(p->clock, sizeof(p->clock), "%s", "2026-10-25 08:00:00");
}
/* 逐字段比对(seq 除外:本层盖章后必然与入帧不同,比对前由调用方对齐) */
static int rt_equals(const ui_data_rt_t *a, const ui_data_rt_t *b) {
if (a->tick_ms != b->tick_ms) return 0;
if (a->valid != b->valid) return 0;
if (a->eng != b->eng) return 0;
if (a->ph_n != b->ph_n) return 0;
if (a->yx_bits != b->yx_bits) return 0;
if (a->yx_valid != b->yx_valid) return 0;
if (a->freq_valid != b->freq_valid) return 0;
if (a->freq_hz != b->freq_hz) return 0;
if (a->clock_ok != b->clock_ok) return 0;
if (strcmp(a->clock, b->clock) != 0) return 0;
if (memcmp(a->yc, b->yc, sizeof(a->yc)) != 0) return 0;
if (memcmp(a->ph, b->ph, sizeof(a->ph)) != 0) return 0;
return 1;
}
/* ===================== TC-UIDATA-001 / 002:跨层下标空间 ===================== */
static void tc_contract_indices(void)
{
CHECK(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT, "TC-UIDATA-001a yc slots == ADC_SEM_COUNT");
CHECK(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT, "TC-UIDATA-001b q10 slots == PS_ID_COUNT");
CHECK((uint32_t)ADC_SEM_UA == 0u, "TC-UIDATA-002a UA == 0");
CHECK((uint32_t)ADC_SEM_UB == 1u, "TC-UIDATA-002b UB == 1");
CHECK((uint32_t)ADC_SEM_UC == 2u, "TC-UIDATA-002c UC == 2");
CHECK((uint32_t)ADC_SEM_UN == 3u, "TC-UIDATA-002d UN == 3");
CHECK((uint32_t)ADC_SEM_IA == 4u, "TC-UIDATA-002e IA == 4");
CHECK((uint32_t)ADC_SEM_IB == 5u, "TC-UIDATA-002f IB == 5");
CHECK((uint32_t)ADC_SEM_IC == 6u, "TC-UIDATA-002g IC == 6");
CHECK((uint32_t)ADC_SEM_IN == 7u, "TC-UIDATA-002h IN == 7");
/* TEMP 必须是末槽:写侧"sem < ADC_SEM_TEMP 才是电气量"的 eng 算法与页面的
* RAW 位判定都靠这个位置,插中间等于两条判据一起错位 */
CHECK((uint32_t)ADC_SEM_TEMP == (UI_DATA_YC_N - 1u), "TC-UIDATA-002i TEMP is last slot");
}
/* ===================== TC-UIDATA-003:首帧之前 ===================== */
static void tc_before_first_frame(void)
{
ui_data_rt_t rt;
ui_data_par_t par;
ui_data_info_t info;
ui_data_init();
(void)memset(&rt, 0x5A, sizeof(rt));
(void)memset(&par, 0x5A, sizeof(par));
(void)memset(&info, 0x5A, sizeof(info));
CHECK(ui_data_rt(&rt) == -1, "TC-UIDATA-003a rt read before first frame == -1");
CHECK(ui_data_par(&par) == -1, "TC-UIDATA-003b par read before first frame == -1");
CHECK(ui_data_info(&info) == -1, "TC-UIDATA-003c info read before first frame == -1");
CHECK((ui_data_rt_seq() == 0u) && (ui_data_par_seq() == 0u) && (ui_data_info_seq() == 0u),
"TC-UIDATA-003d all three seq == 0");
/* 输出缓冲必须一字不动——页面缓存被写脏会把"无数据"渲染成"有数据" */
{
const uint8_t *p = (const uint8_t *)&rt;
int dirty = 0;
size_t i;
for (i = 0u; i < sizeof(rt); i++) {
if (p[i] != 0x5Au) {
dirty = 1;
break;
}
}
CHECK(dirty == 0, "TC-UIDATA-003e output buffer untouched on failure");
}
}
/* ===================== TC-UIDATA-004 / 005:发布盖章与通道独立 ===================== */
static void tc_publish_and_seq(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_par_t par;
ui_data_par_t pg;
uint32_t seq1;
ui_data_init();
fill_rt(&src, 3u);
CHECK(ui_data_publish_rt(&src) == 0, "TC-UIDATA-004a publish returns 0");
CHECK(ui_data_rt_seq() == 1u, "TC-UIDATA-004b seq stamped 1 (incoming 777 ignored)");
(void)memset(&got, 0, sizeof(got));
CHECK(ui_data_rt(&got) == 0, "TC-UIDATA-004c readable after first publish");
got.seq = src.seq; /* 序号由本层盖章,其余字段须逐字节一致 */
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-004d all fields preserved verbatim");
/* 连续发布:seq 单调 +1,内容取最新一帧 */
fill_rt(&src, 9u);
(void)ui_data_publish_rt(&src);
seq1 = ui_data_rt_seq();
CHECK(seq1 == 2u, "TC-UIDATA-005a second publish bumps seq to 2");
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-005b latest frame is the one served");
/* 三通道独立:发 par 不动 rt 的 seq,也不动 rt 内容 */
(void)memset(&par, 0, sizeof(par));
par.valid = 1u;
par.checksum = 0xABCDu;
par.q10[0] = PROT_SET_Q10(10.0);
par.q10[UI_DATA_SET_N - 1u] = PROT_SET_Q10(55.5);
(void)ui_data_publish_par(&par);
CHECK((ui_data_par_seq() == 1u) && (ui_data_rt_seq() == seq1),
"TC-UIDATA-005c par publish does not touch rt seq");
(void)memset(&pg, 0, sizeof(pg));
CHECK((ui_data_par(&pg) == 0) && (pg.checksum == 0xABCDu)
&& (pg.q10[0] == PROT_SET_Q10(10.0))
&& (pg.q10[UI_DATA_SET_N - 1u] == PROT_SET_Q10(55.5)),
"TC-UIDATA-005d par stored verbatim (Q10 not rescaled here)");
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-005e par publish does not pollute rt");
}
/* ===================== TC-UIDATA-006:NULL 防御 ===================== */
static void tc_null_guard(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_init();
fill_rt(&src, 1u);
(void)ui_data_publish_rt(&src);
CHECK(ui_data_publish_rt(NULL) == -1, "TC-UIDATA-006a publish(NULL) == -1");
CHECK(ui_data_rt_seq() == 1u, "TC-UIDATA-006b failed publish keeps seq");
CHECK(ui_data_rt(NULL) == -2, "TC-UIDATA-006c read(NULL) == -2");
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
got.seq = src.seq;
CHECK(rt_equals(&got, &src) != 0, "TC-UIDATA-006d stored frame still intact");
}
/* ===================== TC-UIDATA-007:init 幂等复位 ===================== */
static void tc_init_idempotent(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_init();
fill_rt(&src, 2u);
(void)ui_data_publish_rt(&src);
(void)ui_data_publish_rt(NULL);
ui_data_init();
ui_data_init(); /* 重复调用仍幂等 */
CHECK(ui_data_rt_seq() == 0u, "TC-UIDATA-007a seq reset to 0 by init");
CHECK(ui_data_rt(&got) == -1, "TC-UIDATA-007b back to no-frame state after init");
}
/* ===================== TC-UIDATA-008:量纲与品质常量 ===================== */
static void tc_constants(void)
{
CHECK((UI_DATA_ENG_PIN == 0u) && (UI_DATA_ENG_SEC == 1u) && (UI_DATA_ENG_PRI == 2u),
"TC-UIDATA-008a eng levels are 0/1/2 (pin-V / secondary / primary)");
CHECK((UI_DATA_Q_IV | UI_DATA_Q_NT | UI_DATA_Q_OV | UI_DATA_Q_SB | UI_DATA_Q_BL
| UI_DATA_Q_RAW) == 0x3Fu, "TC-UIDATA-008b six quality bits fill low 6 bits");
CHECK((UI_DATA_Q_IV & UI_DATA_Q_NT) == 0u, "TC-UIDATA-008c IV/NT disjoint");
CHECK((UI_DATA_Q_OV & UI_DATA_Q_SB) == 0u, "TC-UIDATA-008d OV/SB disjoint");
CHECK((UI_DATA_Q_BL & UI_DATA_Q_RAW) == 0u, "TC-UIDATA-008e BL/RAW disjoint");
/* 内存预算护栏(docs/UI设计/UI_数据供给设计说明.md §10):快照是 .bss 里的
* 定长结构,不是 LVGL 对象;一旦有人往里塞大数组或指针,这里先响 */
CHECK(sizeof(ui_data_rt_t) <= 256u, "TC-UIDATA-008f rt frame within budget (<=256B)");
CHECK(sizeof(ui_data_par_t) <= 200u, "TC-UIDATA-008g par frame within budget (<=200B)");
CHECK(sizeof(ui_data_info_t) <= 128u, "TC-UIDATA-008h info frame within budget (<=128B)");
}
/* ===================== TC-UIDATA-009:遥信位集合与独立性 ===================== */
static void tc_yx_points(void)
{
/* V0.1 有硬件来源的遥信点集合(YX15~18/23/31,1 基序号):与 104 点表同源,
* 写侧 App/app/ui_data_feed.c 与 simulator/main.c 固定帧都用同一组位号。
* 任何一处改动都会让本条红,逼着改动方同时更新页面名表与位图镜像注释 */
const uint64_t valid_expect = ((uint64_t)1u << 14u) | ((uint64_t)1u << 15u) |
((uint64_t)1u << 16u) | ((uint64_t)1u << 17u) |
((uint64_t)1u << 22u) | ((uint64_t)1u << 30u);
ui_data_rt_t src;
ui_data_rt_t got;
ui_data_init();
fill_rt(&src, 5u);
src.yx_bits = 0u; /* 全部点位"分/未动作" */
src.yx_valid = valid_expect;
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK(got.yx_valid == valid_expect, "TC-UIDATA-009a valid mask == YX15/16/17/18/23/31");
CHECK(got.yx_bits == 0u, "TC-UIDATA-009b bits and valid are independent fields");
}
/* ===================== TC-UIDATA-010:相量槽透传 ===================== */
static void tc_phasor_slots(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
uint8_t i;
ui_data_init();
fill_rt(&src, UI_DATA_PH_MAX);
src.ph_n = UI_DATA_PH_MAX;
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK(got.ph_n == UI_DATA_PH_MAX, "TC-UIDATA-010a ph_n passed through unchanged");
i = 0u;
while ((i < UI_DATA_PH_MAX) && (got.ph[i].val == src.ph[i].val)) {
i++;
}
CHECK(i == UI_DATA_PH_MAX, "TC-UIDATA-010b every phasor slot kept its value");
}
/* ===================== TC-UIDATA-011:时钟字段容量 ===================== */
static void tc_clock_field(void)
{
ui_data_rt_t src;
ui_data_rt_t got;
char probe[UI_DATA_CLOCK_LEN];
(void)snprintf(probe, sizeof(probe), "%s", "2026-10-25 08:00:00");
CHECK(strlen(probe) == 19u, "TC-UIDATA-011a clock string is 19 chars");
CHECK(UI_DATA_CLOCK_LEN == 20u, "TC-UIDATA-011b capacity is 19 + NUL");
ui_data_init();
fill_rt(&src, 7u);
(void)ui_data_publish_rt(&src);
(void)memset(&got, 0, sizeof(got));
(void)ui_data_rt(&got);
CHECK((got.clock_ok == 1u) && (strcmp(got.clock, "2026-10-25 08:00:00") == 0)
&& (got.clock[UI_DATA_CLOCK_LEN - 1u] == '\0'),
"TC-UIDATA-011c full string stored, last byte still NUL");
}
/* ====== TC-UIDATA-012 / 013:SOE 不透明编号与显示名映射(现场核对枚举头) ====== */
/* 名字表版式护栏:名列 7 汉字 = 21 字节、值列 4 汉字 = 12 字节(UTF-8 3 字节/字);
* 超宽只会被屏幕裁掉,不会被任何像素门禁报出来,所以在这里钉死
* (宽度口径 docs/UI设计/页面扩充详细方案.md §2.3 第 4 条) */
static int all_names_fit(const char *const *tbl, unsigned n, size_t max_bytes) {
unsigned i;
for (i = 0u; i < n; i++) {
if (tbl[i] == NULL) return 0;
if (strlen(tbl[i]) == 0u) return 0;
if (strlen(tbl[i]) > max_bytes) return 0;
}
return 1;
}
static void tc_soe_codes(void)
{
/* 编号与 App/log/soe_rec.h 枚举逐项相等(不是照抄文档转述,是 include 真头对表) */
CHECK((uint8_t)SOE_T_ELM_ASSERT == UI_W_SOE_T_ELM_ASSERT, "TC-UIDATA-012a type ELM_ASSERT");
CHECK((uint8_t)SOE_T_ELM_RETURN == UI_W_SOE_T_ELM_RETURN, "TC-UIDATA-012b type ELM_RETURN");
CHECK((uint8_t)SOE_T_PLATE_OUT == UI_W_SOE_T_PLATE_OUT, "TC-UIDATA-012c type PLATE_OUT");
CHECK((uint8_t)SOE_T_PLATE_IN == UI_W_SOE_T_PLATE_IN, "TC-UIDATA-012d type PLATE_IN");
CHECK((uint8_t)SOE_T_SAMPLE_BLOCK == UI_W_SOE_T_SAMPLE_BLOCK, "TC-UIDATA-012e type SAMPLE_BLOCK");
CHECK((uint8_t)SOE_T_SAMPLE_RESUME == UI_W_SOE_T_SAMPLE_RESUME, "TC-UIDATA-012f type SAMPLE_RESUME");
CHECK((uint8_t)SOE_T_CAL_BLOCK == UI_W_SOE_T_CAL_BLOCK, "TC-UIDATA-012g type CAL_BLOCK == 7");
CHECK((uint8_t)SOE_T_CAL_RESUME == UI_W_SOE_T_CAL_RESUME, "TC-UIDATA-012h type CAL_RESUME == 8");
CHECK((uint8_t)SOE_T_COUNT == UI_W_SOE_T_COUNT, "TC-UIDATA-012i sentinel COUNT == 9");
CHECK(SOE_ELM_NONE == UI_W_SOE_ELM_NONE, "TC-UIDATA-012j ELM_NONE == 0xFF");
CHECK(SOE_FLG_IV == UI_W_SOE_FLG_IV, "TC-UIDATA-012k FLG_IV == 0x80");
/* 八类事件全部有名(新加类型忘配名 = 屏上"--",像素门禁照不出来) */
{
uint8_t t;
int ok = 1;
size_t longest = 0u;
for (t = 1u; t < UI_W_SOE_T_COUNT; t++) {
const char *nm = ui_w_soe_type_name(t);
if (nm == NULL) {
ok = 0; /* 契约上不可达(映射表恒返回串),留作防御分支 */
} else {
if ((strcmp(nm, "--") == 0) || (strlen(nm) == 0u)) {
ok = 0;
}
if (strlen(nm) > longest) {
longest = strlen(nm);
}
}
}
CHECK(ok == 1, "TC-UIDATA-012l every type has a display name");
CHECK(longest <= 12u, "TC-UIDATA-012m type names fit value column (<=4 CJK)");
CHECK(strcmp(ui_w_soe_type_name(0u), "--") == 0, "TC-UIDATA-012n code 0 -> placeholder");
CHECK(strcmp(ui_w_soe_type_name(UI_W_SOE_T_COUNT), "--") == 0,
"TC-UIDATA-012o out-of-range -> placeholder");
/* 类型 5/6(采样)与 7/8(标定)四者必须互不相同:同名 = 抄表时串行,
* 现场排障会把"未标定闭锁"读成"采样闭锁" */
CHECK((strcmp(ui_w_soe_type_name(UI_W_SOE_T_SAMPLE_BLOCK),
ui_w_soe_type_name(UI_W_SOE_T_CAL_BLOCK)) != 0)
&& (strcmp(ui_w_soe_type_name(UI_W_SOE_T_SAMPLE_RESUME),
ui_w_soe_type_name(UI_W_SOE_T_CAL_RESUME)) != 0)
&& (strcmp(ui_w_soe_type_name(UI_W_SOE_T_CAL_BLOCK),
ui_w_soe_type_name(UI_W_SOE_T_CAL_RESUME)) != 0),
"TC-UIDATA-012p sample/cal lock-resume names are distinct");
}
}
static void tc_soe_elms(void)
{
CHECK(PE_OC1 == 0, "TC-UIDATA-013a PE_OC1 == 0");
CHECK(PE_OC2 == 1, "TC-UIDATA-013b PE_OC2 == 1");
CHECK(PE_OC3 == 2, "TC-UIDATA-013c PE_OC3 == 2");
CHECK(PE_OL == 3, "TC-UIDATA-013d PE_OL == 3");
CHECK(PE_OV == 4, "TC-UIDATA-013e PE_OV == 4");
CHECK(PE_UV == 5, "TC-UIDATA-013f PE_UV == 5");
CHECK(PE_3U0 == 6, "TC-UIDATA-013g PE_3U0 == 6");
CHECK(PE_OF == 7, "TC-UIDATA-013h PE_OF == 7");
CHECK(PE_UF == 8, "TC-UIDATA-013i PE_UF == 8");
CHECK((uint32_t)PE_COUNT == UI_W_SOE_ELM_N, "TC-UIDATA-013j PE_COUNT == UI_W_SOE_ELM_N");
CHECK(all_names_fit(s_elm_name, UI_W_SOE_ELM_N, 21u) == 1,
"TC-UIDATA-013k element names fit name column (<=7 CJK)");
CHECK(strcmp(ui_w_soe_elm_name(UI_W_SOE_ELM_NONE), "--") == 0,
"TC-UIDATA-013l non-element id -> placeholder");
CHECK(strcmp(ui_w_soe_elm_name(UI_W_SOE_ELM_N), "--") == 0,
"TC-UIDATA-013m out-of-range element -> placeholder");
}
/* ====== TC-UIDATA-014:注入 reader 后的两行渲染与单条缓存 ====== */
static uint32_t s_get_calls;
static ui_w_soe_rec_t s_fake_rec;
static uint16_t fake_count(void)
{
return 3u;
}
static int fake_get(uint32_t a_idx, ui_w_soe_rec_t *a_out)
{
s_get_calls++;
if (a_idx >= 3u) {
return -1;
}
(void)memcpy(a_out, &s_fake_rec, sizeof(*a_out));
a_out->seq = a_idx;
return 0;
}
static const ui_soe_reader_t s_fake_reader = { fake_count, fake_get };
static void tc_soe_render(void)
{
ui_w_soe_t inst;
char name[UI_W_SOE_TS_LEN + 16u];
char value[UI_W_SOE_TS_LEN + 16u];
(void)memset(&inst, 0, sizeof(inst));
(void)memset(&s_fake_rec, 0, sizeof(s_fake_rec));
s_reader = NULL;
CHECK(ui_w_soe_count(&inst) == 1u, "TC-UIDATA-014a unbound reader -> empty pseudo row");
ui_w_soe_bind(&s_fake_reader);
CHECK(ui_w_soe_count(&inst) == 6u, "TC-UIDATA-014b 3 records -> 6 rows");
/* 元件事件:名列 = 元件,值列 = 动作/返回 */
(void)strcpy(s_fake_rec.ts, "2026-10-25 08:00:00");
s_fake_rec.type = UI_W_SOE_T_ELM_ASSERT;
s_fake_rec.elm = (uint8_t)PE_OC1;
s_fake_rec.flags = 0u;
s_get_calls = 0u;
(void)memset(name, 0, sizeof(name));
(void)memset(value, 0, sizeof(value));
ui_w_soe_refresh(&inst, 0u, name, value);
CHECK((name[0] != 0) && (strlen(name) == 19u) && (value[0] == 0),
"TC-UIDATA-014c ts row shows full timestamp");
ui_w_soe_refresh(&inst, 1u, name, value);
CHECK((strlen(name) == 12u) && (strlen(value) == 6u) && (s_get_calls == 1u),
"TC-UIDATA-014d content row uses cached record (single disk read)");
/* IV 时标:不显示猜测时间,值列给"时钟无效" */
s_fake_rec.flags = UI_W_SOE_FLG_IV;
(void)memset(&inst, 0, sizeof(inst));
ui_w_soe_refresh(&inst, 0u, name, value);
CHECK((strcmp(name, "--") == 0) && (strlen(value) == 12u),
"TC-UIDATA-014e IV timestamp suppressed, marked in value column");
/* 非元件事件:类型名进名列,值列留空 */
s_fake_rec.flags = 0u;
s_fake_rec.type = UI_W_SOE_T_CAL_BLOCK;
s_fake_rec.elm = UI_W_SOE_ELM_NONE;
(void)memset(&inst, 0, sizeof(inst));
ui_w_soe_refresh(&inst, 3u, name, value);
CHECK((strlen(name) == 12u) && (value[0] == 0), "TC-UIDATA-014f non-element row: type in name column");
/* 越界记录号:读失败退化为占位,不留脏缓存 */
ui_w_soe_refresh(&inst, 998u, name, value);
CHECK(strcmp(name, "--") == 0, "TC-UIDATA-014g failed read -> placeholder, no stale cache");
ui_w_soe_bind(NULL);
}
/* ===================== main ===================== */
int main(void)
{
printf("=== UI Data Snapshot Tests ===\n");
printf("-- contract index space --\n");
tc_contract_indices();
printf("-- before first frame --\n");
tc_before_first_frame();
printf("-- publish and seq --\n");
tc_publish_and_seq();
printf("-- null guard --\n");
tc_null_guard();
printf("-- init idempotent --\n");
tc_init_idempotent();
printf("-- constants --\n");
tc_constants();
printf("-- yx points --\n");
tc_yx_points();
printf("-- phasor slots --\n");
tc_phasor_slots();
printf("-- clock field --\n");
tc_clock_field();
printf("-- soe codes and names --\n");
tc_soe_codes();
printf("-- soe elements --\n");
tc_soe_elms();
printf("-- soe render --\n");
tc_soe_render();
printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""写回缓存空闲 flush(vol_idle_sync)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/vol_idle_sync/vol_idle_sync_test.c(它以 #include
纳入**生产源文件** App/fs/vol_idle_sync.c),在 PC 上离线跑就绪门控 / 节流边界 /
卷忙跳过 / 失败边沿 latch / 成对放锁 / tick 回绕,失败即非 0 退出。
零硬件、零烧录、毫秒级。
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
`test/host/common/unit_test_harness.py`(本文件只做声明)。
为什么要主机侧单测(而不是只用台架):
本模块是「掉电窗口兜底网」——掉电与脏页驻留窗在台架上都不可按需复现;卷忙 /
ioctl 失败 / 32 位 tick 回绕这些分支在真机上构造成本极高。门控、节流、latch
全是纯状态逻辑,主机侧一次钉死。
用法(仓库根目录):
python test/host/vol_idle_sync/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:App/fs/vol_idle_sync.h 文件头(含「无掉电检测硬件」边界声明);
创建日期:2026-10-25
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
# harness 位于 test/host/common/
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 test_config.h / cmsis_os(_2).h /
# FreeRTOS.h / task.h / dbg_log.h(真实头会拖入整套 FreeRTOS/CMSIS/HAL)。
# ff.h / diskio.h / vol_lock.h / sys_events.h 用**真实头**(接口契约与就绪位数值
# 原汁,同 lftpd_io / nand_port 先例;本模块经真实 sys_events.h 拿就绪位定义)。
BUILD = TestBuild(
name="Volume Idle Sync Tests",
src=os.path.join(HERE, "vol_idle_sync_test.c"),
include_dirs=[
os.path.join(HERE, "stub"),
os.path.join(ROOT, "App", "fs"),
os.path.join(ROOT, "App", "app"),
os.path.join(ROOT, "Lib", "FatFs"),
HERE,
],
summary_marker="=== Volume Idle Sync Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+29
View File
@@ -0,0 +1,29 @@
/*
* 主机侧单测桩:FreeRTOS.h(vol_idle_sync 模块专用)
* 模块功能:最小 FreeRTOS 时基类型与宏(真实头会拉整棵 portmacro/projdefs)。
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
* 保真点:TickType_t = uint32_t;configTICK_RATE_HZ = 1000
* (Inc/FreeRTOSConfig.h)→ pdMS_TO_TICKS(ms) == ms。
* 节流用例的 4999/5000 拍边界直接依赖该等式。
* 创建日期:2026-10-25
*/
#ifndef FREERTOS_H
#define FREERTOS_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef uint32_t TickType_t;
#define configTICK_RATE_HZ 1000U
#define pdMS_TO_TICKS(ms) ((TickType_t)((uint32_t)(ms) * (1000U / configTICK_RATE_HZ)))
#ifdef __cplusplus
}
#endif
#endif /* FREERTOS_H */
+14
View File
@@ -0,0 +1,14 @@
/*
* 主机侧单测桩:cmsis_os.h(vol_idle_sync 模块专用)
* 模块功能:真实 cmsis_os.h(CMSIS-RTOS V2 包装)会拖入整套 FreeRTOS;本模块
* 只需要事件标志接口,全部收敛在 cmsis_os2.h 桩里。
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
* 创建日期:2026-10-25
*/
#ifndef CMSIS_OS_H
#define CMSIS_OS_H
#include "cmsis_os2.h"
#endif /* CMSIS_OS_H */
+32
View File
@@ -0,0 +1,32 @@
/*
* 主机侧单测桩:cmsis_os2.h(vol_idle_sync 模块专用)
* 模块功能:为 App/fs/vol_idle_sync.c(含经 sys_events.h)提供主机可编译的
* CMSIS-RTOS v2 事件标志最小子集。
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
* 保真点(对齐真实 cmsis_os2.h):
* - osEventFlagsId_t = void*;
* - uint32_t osEventFlagsGet(osEventFlagsId_t)(返回当前位图,不消费——
* 就绪位 osFlagsNoClear 语义在生产调用点之外,本桩只回静态值);
* - 就绪位数值一律来自**真实** App/app/sys_events.h(不在本桩复刻)。
* osEventFlagsGet 由测试单元给强符号桩(可注入位图)。
* 创建日期:2026-10-25
*/
#ifndef CMSIS_OS2_H
#define CMSIS_OS2_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef void *osEventFlagsId_t;
uint32_t osEventFlagsGet(osEventFlagsId_t ef_id);
#ifdef __cplusplus
}
#endif
#endif /* CMSIS_OS2_H */
+17
View File
@@ -0,0 +1,17 @@
/*
* 主机侧单测桩:dbg_log.h(vol_idle_sync 模块专用)
* 模块功能:DBG_* 换成计数,让"失败留痕 = 边沿 latch、成功不刷屏"可断言。
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
* 创建日期:2026-10-25
*/
#ifndef DBG_LOG_H
#define DBG_LOG_H
extern int g_vis_error_count;
extern int g_vis_info_count;
#define DBG_INFO(fmt, ...) do { (void)(fmt); g_vis_info_count++; } while (0)
#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_vis_error_count++; } while (0)
#endif /* DBG_LOG_H */
+24
View File
@@ -0,0 +1,24 @@
/*
* 主机侧单测桩:task.h(vol_idle_sync 模块专用)
* 模块功能:只声明 xTaskGetTickCount,由测试单元给可控时钟强符号桩
* (驱动节流边界与 32 位回绕用例)。
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
* 创建日期:2026-10-25
*/
#ifndef TASK_H
#define TASK_H
#include "FreeRTOS.h"
#ifdef __cplusplus
extern "C" {
#endif
TickType_t xTaskGetTickCount(void);
#ifdef __cplusplus
}
#endif
#endif /* TASK_H */
@@ -0,0 +1,15 @@
/*
* 主机侧单测桩:test_config.h(vol_idle_sync 模块专用)
* 模块功能:给主机编译一个"产品构建"形态——四个 TEST_SUITE_* 全部未定义,
* vol_idle_sync.c 走产品分支(STORAGE 套件的 no-op 分支由板端
* test-suite-switch 构建侧覆盖,主机侧不重复裁剪测试)。
* ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。
* 创建日期:2026-10-25
*/
#ifndef TEST_CONFIG_H
#define TEST_CONFIG_H
/* 刻意不定义任何 TEST_SUITE_* / ENABLE_DESTRUCTIVE_TESTS */
#endif /* TEST_CONFIG_H */
@@ -0,0 +1,282 @@
/*
* 模块名称:Volume Idle Sync(写回缓存空闲 flush)主机侧单元测试
* 模块功能:在 PC 上对 App/fs/vol_idle_sync.c 做离线验证(产品分支全函数覆盖)。
*
* 被测对象:真实源文件 ../../../App/fs/vol_idle_sync.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本)。平台依赖由本文件给出的**可注入强符号桩**:
* - xTaskGetTickCount -> 可控时钟(节流边界 + 32 位回绕)
* - osEventFlagsGet -> 可注入就绪位图 / 句柄判空
* - vol_lock_wait -> 可注入 空闲/忙(try 语义)+ 透传记录
* - vol_lock_release -> 计数(断言成对、失败不泄漏)
* - disk_ioctl -> 可注入 RES_OK/RES_ERROR + pdrv/cmd 记录
* 事件位数值取自**真实** App/app/sys_events.h(不在桩里复刻)。
*
* 为什么要主机侧单测(而不是只用台架):
* 本模块是"掉电窗口兜底网"——掉电本身不可台架复现,脏页驻留窗更不能;
* 失败路径(卷忙 / ioctl 失败 / tick 回绕)在真机上极难按需构造。
* 门控/节流/latch 全是纯状态逻辑,主机侧一次钉死,板端只验证接入存在性。
*
* 覆盖点:
* TC-VOLSYNC-001 句柄未建 -> 完全静默(不读事件/不取锁/不 ioctl)
* TC-VOLSYNC-002 STORAGE_READY 未置 -> 静默(读了事件但不取锁)
* TC-VOLSYNC-003 就绪首拍:try 锁(timeout=0) + CTRL_SYNC(pdrv=0) + 放锁各一次
* TC-VOLSYNC-004 周期内重复拍 -> 完全抑制(不取锁不 ioctl)
* TC-VOLSYNC-005 节流边界:周期-1 拍抑制 / 满周期拍放行(1ms tick 等式)
* TC-VOLSYNC-006 卷忙(try 失败)-> 不 ioctl、不 release(不得空放锁)
* TC-VOLSYNC-007 卷忙跳过不欠账:节奏恒定(下次尝试 = 满周期之后)
* TC-VOLSYNC-008 ioctl 失败一个 episode 只打一条(边沿 latch,持续失败不刷屏)
* TC-VOLSYNC-009 成功后 latch 重新武装:下一个失败 episode 再报一条
* TC-VOLSYNC-010 失败路径同样成对放锁(release 次数 == 有效 ioctl 次数)
* TC-VOLSYNC-011 tick 32 位回绕:跨 0 后节流判据仍正确(该抑制抑制/该放行放行)
* TC-VOLSYNC-012 成功路径零日志(不打 DBG_ERROR/DBG_INFO)
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-25
*/
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
/* ===================== 真实中间件头(与 lftpd_io/nand_port 同一份) ======== */
#include "ff.h" /* FatFs 基础类型(diskio.h 依赖,先纳) */
#include "diskio.h"
/* ===================== 平台桩(stub 目录优先于真实头) ==================== */
#include "test_config.h"
#include "cmsis_os.h"
#include "FreeRTOS.h"
#include "task.h"
#include "sys_events.h" /* 真实头:SYS_EVT_STORAGE_READY / sys_evtHandle extern */
#include "dbg_log.h"
/* ===================== 强符号桩 =====================
* 时钟:非零起步(1000),避免"初值 0"与"回绕"在断言里混同 */
static uint32_t s_tick_now;
TickType_t xTaskGetTickCount(void) {
return (TickType_t)s_tick_now;
}
/* 事件标志:句柄实体在本测试单元定义(真实 sys_events.h 只 extern) */
static uint32_t s_evt_flags;
static int s_efget_calls;
osEventFlagsId_t sys_evtHandle;
uint32_t osEventFlagsGet(osEventFlagsId_t ef_id) {
(void)ef_id;
s_efget_calls++;
return s_evt_flags;
}
/* 卷锁:1 = 获取成功,0 = 忙(try 语义下即"本轮跳过") */
static int s_lock_ok;
static int s_lock_calls;
static int s_release_calls;
static uint32_t s_last_wait_to;
int vol_lock_wait(uint32_t timeout_ms) {
s_lock_calls++;
s_last_wait_to = timeout_ms;
return s_lock_ok;
}
void vol_lock_release(void) {
s_release_calls++;
}
/* 磁盘控制:只接住被测代码真正会发的那一发(pdrv=0 / CTRL_SYNC) */
static DRESULT s_diskio_res;
static int s_diskio_calls;
static BYTE s_diskio_pdrv;
static BYTE s_diskio_cmd;
DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
(void)buff;
s_diskio_calls++;
s_diskio_pdrv = pdrv;
s_diskio_cmd = cmd;
return s_diskio_res;
}
/* dbg_log 桩计数(stub/dbg_log.h 声明 extern) */
int g_vis_error_count;
int g_vis_info_count;
/* ===================== 被测代码(原样纳入生产源) ===================== */
#include "vol_idle_sync.c"
/* ===================== 断言 ===================== */
static int s_total = 0, s_passed = 0, s_failed = 0;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
} while (0)
#define FAKE_EVT_HANDLE ((osEventFlagsId_t)0xE7)
#define PERIOD_TICKS 5000u /* = pdMS_TO_TICKS(VOL_IDLE_SYNC_PERIOD_MS),
* 1 kHz tick 下与毫秒数值等值(桩保真点) */
/* 复位所有桩与被测模块状态量(白盒:生产源已纳入本编译单元,文件级 static
* 可直接复位,等价"刚上电") */
static void fakes_reset(void) {
s_tick_now = 1000u;
s_evt_flags = 0u;
sys_evtHandle = NULL;
s_efget_calls = 0;
s_lock_ok = 1;
s_lock_calls = 0;
s_release_calls = 0;
s_last_wait_to = 0xFFFFFFFFu;
s_diskio_res = RES_OK;
s_diskio_calls = 0;
s_diskio_pdrv = 0xFFu;
s_diskio_cmd = 0xFFu;
g_vis_error_count = 0;
g_vis_info_count = 0;
s_vol_sync_last_ms = 0u;
s_vol_sync_armed = 0u;
s_vol_sync_fail_latch = 0u;
}
/* 便捷:推进时钟并空转主循环拍(模拟 defaultTask 循环里的连续 poll) */
static void advance_and_poll(uint32_t ticks) {
s_tick_now += ticks;
vol_idle_sync_poll();
}
/* ===================== TC-VOLSYNC-001/002:就绪门控 ===================== */
static void tc_gate(void) {
fakes_reset();
vol_idle_sync_poll();
CHECK((s_efget_calls == 0) && (s_lock_calls == 0) && (s_diskio_calls == 0),
"TC-VOLSYNC-001 handle NULL: fully silent (no flags read, no lock, no ioctl)");
fakes_reset();
sys_evtHandle = FAKE_EVT_HANDLE; /* 句柄已建但未就绪 */
vol_idle_sync_poll();
CHECK((s_efget_calls == 1) && (s_lock_calls == 0) && (s_diskio_calls == 0),
"TC-VOLSYNC-002 STORAGE_READY clear: read flags then bail (no lock, no ioctl)");
}
/* ===================== TC-VOLSYNC-003..007:放行/节流/卷忙 ===================== */
static void tc_run_throttle(void) {
fakes_reset();
sys_evtHandle = FAKE_EVT_HANDLE;
s_evt_flags = SYS_EVT_STORAGE_READY;
vol_idle_sync_poll();
CHECK((s_lock_calls == 1) && (s_last_wait_to == 0u),
"TC-VOLSYNC-003 first ready tick: try-lock taken with timeout=0 (non-blocking)");
CHECK((s_diskio_calls == 1) && (s_diskio_pdrv == 0u) && (s_diskio_cmd == (BYTE)CTRL_SYNC),
"TC-VOLSYNC-003b ioctl exactly once with pdrv=0 / CTRL_SYNC");
CHECK(s_release_calls == 1,
"TC-VOLSYNC-003c lock released exactly once after ioctl");
vol_idle_sync_poll();
vol_idle_sync_poll();
CHECK((s_lock_calls == 1) && (s_diskio_calls == 1),
"TC-VOLSYNC-004 repeats within period fully suppressed (no lock, no ioctl)");
/* 边界:+4999 抑制 / +1(累计满 5000)放行 */
advance_and_poll(PERIOD_TICKS - 1u);
CHECK(s_diskio_calls == 1, "TC-VOLSYNC-005 period-1 tick: still suppressed");
advance_and_poll(1u);
CHECK(s_diskio_calls == 2, "TC-VOLSYNC-005b exact period tick: fires again");
/* 卷忙:ioctl 不加、release 不得空放(相对断言:忙尝试整段零增量) */
{
const int lk = s_lock_calls;
const int rel = s_release_calls;
s_lock_ok = 0;
advance_and_poll(PERIOD_TICKS);
CHECK((s_lock_calls == lk + 1) && (s_diskio_calls == 2) && (s_release_calls == rel),
"TC-VOLSYNC-006 lock busy: attempt logged, no ioctl and NO release (never unlock what we do not hold)");
/* 跳过不欠账:忙的那拍消费了槽位 -> 下次尝试在一个满周期之后 */
s_lock_ok = 1;
advance_and_poll(PERIOD_TICKS - 1u);
CHECK(s_lock_calls == lk + 1,
"TC-VOLSYNC-007 busy skip consumed the slot: constant cadence");
advance_and_poll(1u);
CHECK((s_lock_calls == lk + 2) && (s_diskio_calls == 3) && (s_release_calls == rel + 1),
"TC-VOLSYNC-007b next attempt exactly one full period after the skip");
}
}
/* ===================== TC-VOLSYNC-008..010/012:失败 latch / 成对放锁 ======= */
static void tc_fail_latch(void) {
fakes_reset();
sys_evtHandle = FAKE_EVT_HANDLE;
s_evt_flags = SYS_EVT_STORAGE_READY;
vol_idle_sync_poll(); /* 首拍成功 */
CHECK((g_vis_error_count == 0) && (g_vis_info_count == 0),
"TC-VOLSYNC-012 success path logs nothing");
s_diskio_res = RES_ERROR;
advance_and_poll(PERIOD_TICKS);
CHECK(g_vis_error_count == 1, "TC-VOLSYNC-008 first failure logs exactly one line");
advance_and_poll(PERIOD_TICKS);
advance_and_poll(PERIOD_TICKS);
CHECK((g_vis_error_count == 1) && (s_diskio_calls == 4),
"TC-VOLSYNC-008b continuing failures do not spam (edge latch holds)");
CHECK(s_release_calls == s_diskio_calls,
"TC-VOLSYNC-010 release paired with every attempt incl. failures (no lock leak)");
s_diskio_res = RES_OK;
advance_and_poll(PERIOD_TICKS);
CHECK((g_vis_error_count == 1) && (s_vol_sync_fail_latch == 0u),
"TC-VOLSYNC-009 success clears latch (re-armed for next episode)");
s_diskio_res = RES_ERROR;
advance_and_poll(PERIOD_TICKS);
CHECK(g_vis_error_count == 2, "TC-VOLSYNC-009b new failing episode logs again (one line)");
}
/* ===================== TC-VOLSYNC-011:tick 回绕 ===================== */
static void tc_wrap(void) {
fakes_reset();
sys_evtHandle = FAKE_EVT_HANDLE;
s_evt_flags = SYS_EVT_STORAGE_READY;
/* 白盒置态:上次尝试贴着 32 位顶端,当前拍刚跨过 0 */
s_vol_sync_armed = 1u;
s_vol_sync_last_ms = 0xFFFFF000u;
s_tick_now = 0x0300u; /* 无符号差 = 0x1300 = 4864 < 5000 */
vol_idle_sync_poll();
CHECK(s_diskio_calls == 0, "TC-VOLSYNC-011 wrap: unsigned-diff still suppresses early tick");
s_vol_sync_last_ms = 0xFFFFF000u;
s_tick_now = 0x0800u; /* 无符号差 = 0x1800 = 6144 >= 5000 */
vol_idle_sync_poll();
CHECK(s_diskio_calls == 1, "TC-VOLSYNC-011b wrap past zero fires when diff says so");
}
/* ===================== main ===================== */
int main(void) {
printf("=== Volume Idle Sync Tests ===\n");
printf("-- gate (readiness) --\n");
tc_gate();
printf("-- run / throttle / idle-skip --\n");
tc_run_throttle();
printf("-- failure latch / pairing --\n");
tc_fail_latch();
printf("-- tick wraparound --\n");
tc_wrap();
printf("\n=== Volume Idle Sync Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+1 -1
View File
@@ -1 +1 @@
ⅠⅡⅢ▶一三主事二交件位低作保信值入公共关出分制前动化区压参双取口号合启告回地址型复外子字存定实密已常年序度延异式强归当录态总息户手投抖护报据接控掩操故数整无日时显最月期未本板查标校检模欠次死段求波流测消清温点片版状用电界直看码硬确示秒程端第类系线终维网置自芯荷行装警认讯记设询误请负越跳软载输过运近返退通道遥部配重量钟钥锁错长闭间闸防除障零面页频馈验默,:
ⅠⅡⅢ▶一三主事二交件位低作保信值入公共关出分制前动化区压参双取口号合启告回地址型复外子字存定实密已常年序度延异式强归当录态总恢息户手投抖护报据接控掩操故效数整无日时显最月期未本板查标校样检模欠次死段求波流测消清温点片版状用电界直看码硬确示秒程端第类系线终维网置自芯荷行装警认讯记设询误请负越跳软载输过运近返退通道遥部配采重量钟钥锁错长闭间闸防除障零面页频馈验默,: