Files
STM32F4-Base/App/task/ui_task.c
T
wandering1 d64bbdc57f feat(wave): 故障录波数据面、接线与单通道回放页(软件闭环交付)
数据面(App/algo,纯逻辑,双端编译)
- wave_ring:8 路 × 320 点列优先环(1 kHz,预触发 160 + 后触发 160),adcTask
  唯一写者;触发/冻结/封存/冷却四态,冻结持续到落盘回执才解冻,滞留 2 s 自弃
  兜底——下一次真故障无波可录比丢一段更贵
- wave_store:段头编解码 + 拉模式打包游标 + 自带无表位运算 CRC-16/MODBUS(带
  锚点自检)+ 段名与整段长度等式;格式不依赖平台类型宽度
- 段头只存原始码值与封存瞬间生效系数,工程量只当单位标签、不进任何算式
- 三套主机单测(环 113 / 编解码 170 / 触发状态机 110),全部无桩、生产源原样
  纳入;新文件补 stddef 是 ARMCC 与主机差异,主机侧照不出这条分叉
- test/host/wave_read.py 是按 §6.2 的独立第二实现(dump/fft/shape/plot),92 条
  自洽断言;三份合成段互读双校验全过、哈希钉死,格式面双向闭合
- 三套新单测不加专属钩子,由覆盖率总闸全量真跑(沿用既有裁定)

接线(任务层)
- adc_cal:新增生效系数的 32 位定点原值入口(与既有浮点入口共用生效视图),
  段头按最低有效位精确记账,不经浮点回算
- adcTask:每拍压 8 路语义原始码值并推进状态机,封存那一拍同拍交棒封存瞬间事实;
  台架定周期自抓走双层编译门,产品镜像里零存在;串口新增一条 wave: 周期行
- defaultTask:卷空闲后一次心跳整段搬完(try 锁 + 256 字节分片 + 一次完整开档到
  关档),成败都回执;失败先删半成品再计错,段号只在成功时推进;持锁超 50 ms
  补一条告警并记历史峰值(喂狗余量可核)
- Lead 复核补第四道并发陷阱:采集任务优先级高于落盘道,搬段中途它可能正好把滞留
  超时的本段自弃、甚至封出下一段——数据区校验和是对「实际读到的字节」算的,
  半新半旧的段照样自洽,故关档后复检环侧身份判废:删文件、不补账、段号不推进
- Lead 复核抓到触发挂号不是原子操作(旧实现分三个字节写,消费者可在窗口里读到
  上一段的触发源与元件号):已把载荷压进一个字节,越界当场拒且不记任何计数;
  段文件字节布局零改动,三份合成段哈希逐字节不变
- protectTask:保护启动上升沿同点请求录波,一拍一次、元件号取最低位;判据本体
  零改动

人机(160×160 单色屏)
- 菜单树节点 26「手动录波」绑页面 + 记录查询组末新增「录波回放」叶,重跑生成器
  得 43 节点 / 34 叶 / 30 已绑,页面映射 28 → 30
- 回放页一屏一路、触发点前后各一周波共 40 点、每点 3 像素阶梯线(只用水平与
  垂直两种线段——单色屏斜线会被阈值裁掉);零点虚线、触发光标、窗口峰值、
  单位徽标与形状受限徽标;空态与坏段各一句显式文案,绝不用半截波形冒充数据
- ui_data 新增录波注入通道(六件操作表、三态返回);执行侧胶水带段头缓存、
  手动请求后连缓存一起作废、逐点走全仓唯一的码值换算入口
- 模拟器接真实环与编解码合成两段(含坏段与空态旗标),6 张新黄金参考,既有
  60 张逐字节零漂移未用更新模式;字库 218 → 224 字 / 319 字形
- 两份图形配置里图表组件仍为关;零堆分配;页面池水位不变
- 回放读路径的 uiTask 阻塞上界按实码写清:一次渲染 = 段表刷新 + 未缓存段头最坏
  22 次小读;已验段只剩一次开窗读 + 40 点解码;无实测数不编,实测归台架、
  仪器用已在跑的单轮工作耗时探针,不新加埋点

台账与文档
- 内存台账:录波环与落盘/回放静态逐条入册,三档对照 81,536 → 88,296 → 89,936
  字节(62.2% → 68.6%),累计 +8,400;预估 5.9 KB 与实测 +6,760 的差额点名;
  散列文件头注释同步;任务栈表核对后无需抬栈;体积趋势行与同码凭证由权威构建
  重打(0 错误 0 警告,四项体积与上一轮相同——本轮改动只有注释与声明位置)
- 设计说明:§5 补第四条并发陷阱;§6.3 定字段口径(两份独立实现的共同依据);
  §7 回填实测;§9 周期行写实;§10 台架判据改两段式且不比绝对相位;§12 补读路径
  风险行;抬头状态改成「代码面已交付、未做完的只有板级那一轮」
- 卡片:软件闭环一栏列证据;台架三条按用户裁定保持未完成,不做虚假销项
- 口径同步:数据可信、UI 补齐、页面补齐规划、产品需求、采集短板评测、任务规划
  索引与两份 README;陈旧现状句清扫 16 处(带日期戳的历史取数行一律不动)
- 门禁说明:nested-comment 记仓库根探测必须兼容 linked worktree 的 gitdir 文件
  形态(修前该钩子在任务树里恒报环境错、静默不可用);coding-standard 记
  「函数头注释超过 24 行就把函数功能挤出回看窗口」这条机制与本次现场
- 覆盖率基线登记两个新覆盖模块(套件 41 → 44 套);环单测重打后该文件有效行
  227 → 226、覆盖行 226 → 225,比率仍 99.6%,是分母变小不是覆盖率回退

为什么是单一提交:文档链接、双端注册、生成器幂等(菜单与字库)、内存数字与
体积事实源、编号门禁这五道整仓门禁要求代码、页面、菜单树、字库、文档与台账
同处一个提交才自洽,按数据面/接线/人机/台账拆成四笔必然红其中至少四道

编号门禁新增行 14 处命中已按语义改写:魔数常量按字符命名、段名样例改由格式串
生成、频谱表头改 f / 2x / 5x 并加一行图例;改写后自洽断言 92 条仍全过
2026-10-29 19:23:15 +08:00

992 lines
49 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:UI Task
* 模块功能:UI 主任务,等待网络就绪后初始化显示/菜单/推流并周期刷新
* 适用平台:STM32F407 系列(LVGL + CH395F 以太网)
* 作者:王建锋
* 创建日期:2026-08-30
* 修改记录:
* v1.1 2026-09-14 王建锋 新增待机显示页初始化(ui_standby_init,空闲 60s 切入)
* v1.2 2026-10-10 王建锋 任务耗时测量埋点:主循环加常开探针
* (循环周期 / 单轮工作耗时 / ui_net_sink_poll 往返),
* 用于把"心跳缺席"拆成 饿死 / 阻塞在往返 / 自身拖长
* 三种互斥机理(判读见 docs/子系统设计/看门狗设计说明.md §11.4)
* v1.3 2026-10-10 王建锋 解耦心跳(docs/子系统设计/看门狗设计说明.md §11.3):把 task_wd_beat
* (TASK_WD_UI) 提到 ui_net_sink_poll() **之前**。原写法心跳
* 在推流往返之后 → 心跳周期 = 渲染 + 一次 netTask 往返 + 10ms,
* 实测 uiw≈uis≈50ms(心跳被 netTask 往返主导),心跳实际测的是
* "netTask 响应性"而非"UI 活着"。提到往返之前后,心跳测的是
* "UI 自己被调度到了"。注:若往返**永久**阻塞,本改仍不能救
* (心跳只发一次)→ 真要彻底去掉依赖须走事件驱动 accept
* (NET_EVENT_CONNECTED 回调),待复现快照确认阻塞机理后再定。
* v1.4 2026-10-25 DTU 项目组 F3(大窗判读):向 ui_net_sink 注册心跳回调
* (ui_task_sink_beat)——推流段内**每个可等待 net 往返**
* (accept/recv/send/close/重建)返回点即喂本任务心跳,
* 风暴期「单轮多个慢往返之和」不再串成 800ms 缺席链
* (2026-10-25 实测 bit4 8 连击、uisn=154ms 仍在段内;
* 按位连击判据 = docs/子系统设计/看门狗设计说明.md §5.3)。
* 单个往返自身仍无上界——该上界与彻底解耦(事件驱动
* accept)归登记簿「net 层跨上下文收口」(批 3),本票只做心跳续命。
* v1.5 2026-10-26 DTU 项目组 道F task-3 装置维护三页的执行侧:新增
* ui_data_maint_ops_t 六件适配(时钟=写+回读 RTC、
* 复归=prot_elm_request_reset、清除=syslog_request_clear
* /clear_state 直通)并在 ui_menu_init 之前注入;记录
* reader 的 2 条 if 链改成 7 条映射表 s_log_map,并加
* 编译期守卫把 UI_W_REC_LOG_* == syslog_id_t 枚举序钉死
*/
#include "ui_task.h"
#include <string.h>
#include "cmsis_os.h"
#include "sys_events.h"
#include "ui_port.h"
#include "ui_menu.h"
#include "ui_net_sink.h"
#include "ui_standby.h"
#include "ui_w_records.h" /* task-6 记录 reader 注入点(UI 页面不再 include sys_log.h) */
#include "ui_w_soe.h" /* task-15 SOE 记录 reader 注入点(同上:UI 不 include soe_store.h) */
#include <stdio.h> /* task-15 snprintf:CP56 → UI 视图里的 char[20] 时标 */
#include "ui_data.h" /* task-14 快照存侧(BSP):本任务只负责先复位再喂帧 */
#include "ui_data_feed.h" /* task-14 数据装配节拍入口(App/app,UI 侧唯一发布者) */
#include "sys_log.h"
#include "soe_store.h" /* task-15 SOE 只读遍历(count/read);写侧仍归 protectTask/defaultTask */
#include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */
#include "sys_clock.h"
#include "prot_elm.h" /* 道F task-3 装置维护:强制复归请求 + 动作态元件位图 */
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
#include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */
#include "task_watchdog.h"
/* 故障录波回放道(UI 道):录波注入表执行侧需要的四份头。
* wave_ring.h 给触发投递(request 任意上下文可调、非阻塞);
* wave_store.h 给段格式解析与定点 mV 换算(唯一口径,页面不复述算式);
* vol_lock.h + ff.h:/wave 只读遍历按 soe_store.c 读者同款纪律——
* try 锁非阻塞(卷忙本轮按「无段」处理,uiTask 绘制节拍绝不等待)、
* 锁跨完整文件操作序列、小窗口分次读、整段 5250B 绝不驻留 RAM */
#include "ff.h"
#include "vol_lock.h"
#include "wave_ring.h"
#include "wave_store.h"
#include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */
/*
* 函数功能:待机屏底部时钟提供回调(平台差异:MCU = sys_clock_get_str)
* 入口参数:buf - 时间串输出缓冲 char *
* len - buf 容量 size_t
* 返回值:无
*/
static void ui_task_time_cb(char *buf, size_t len) {
if (sys_clock_get_str(buf, len) == NULL && len > 0u) {
buf[0] = '\0'; /* 墙钟无效:空串(待机屏时钟行留白) */
}
}
/*
* UI 记录源编号 → syslog_id_t 映射表(道F task-6 起 7 类,task-3 装置维护「清除记录」
* 要在 7 行上分行显示;表长 = UI_W_REC_LOG_N,两端映射表同源同长)
*
* ⚠ 顺序契约(Lead 裁定 2026-10-26):UI_W_REC_LOG_* 逐字等于 syslog_id_t 枚举序,
* 只准尾部追加——枚举序就是 App/log 两端 s_paths[]/s_caps[] 的下标,中间插值会
* 安静地把事故读成告警。下面的编译期守卫就是这条契约在本端的机械防线(记录侧
* 已有 TC-SYSLOG-130/131/131b,UI 侧对应的一条在 test/host/ui_data TC-UIDATA-024,
* 两端各钉一次,谁中间插号谁编译不过 / 单测红)。
* 即便如此,适配层**始终显式逐项映射**、从不把 UI 编号强转成 syslog_id_t:
* 等值是今天的事实,不是页面可以依赖的契约(UI_W_REC_LOG_* 对页面仍是不透明号)。
*/
static const syslog_id_t s_log_map[UI_W_REC_LOG_N] = {
SYSLOG_RUN, SYSLOG_SELFTEST, SYSLOG_ACCIDENT, SYSLOG_ALARM,
SYSLOG_START, SYSLOG_OPER, SYSLOG_YK
};
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)
typedef char ui_task_log_order_check_t[
((UI_W_REC_LOG_RUN == (uint8_t)SYSLOG_RUN) &&
(UI_W_REC_LOG_SELFTEST == (uint8_t)SYSLOG_SELFTEST) &&
(UI_W_REC_LOG_ACCIDENT == (uint8_t)SYSLOG_ACCIDENT) &&
(UI_W_REC_LOG_ALARM == (uint8_t)SYSLOG_ALARM) &&
(UI_W_REC_LOG_START == (uint8_t)SYSLOG_START) &&
(UI_W_REC_LOG_OPER == (uint8_t)SYSLOG_OPER) &&
(UI_W_REC_LOG_YK == (uint8_t)SYSLOG_YK) &&
(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT)) ? 1 : -1];
#endif
/*
* 函数功能:记录 reader 适配——UI 不透明编号 → syslog_id_t 后取条数
* 入口参数:log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
* 返回值:记录条数;未知编号返回 0(页面退化为空态,与合并前直调同形) uint16_t
* 限定条件:uiTask 上下文同步调用(记录页与清除记录页的绘制路径,无 I/O 之外开销:
* syslog_count 读的是文件长度/内存计数,不逐条读盘)
*/
static uint16_t ui_task_rec_count(uint8_t log_id) {
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
return 0u;
}
return (uint16_t)syslog_count(s_log_map[log_id]);
}
/*
* 函数功能:记录 reader 适配——读记录字段直通 sys_log(错误码不改写)
* 入口参数:log_id/idx/ts/ts_len/desc/desc_len - 契约见 ui_w_records.h ui_rec_reader_t
* 返回值:0 成功;-1 编号/序号越界(未知编号同 -1);-2 读失败 int
* 限定条件:同上
*/
static int ui_task_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len,
char *desc, uint32_t desc_len) {
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
return -1;
}
return syslog_get_record(s_log_map[log_id], idx, ts, (size_t)ts_len, desc,
(size_t)desc_len);
}
/*
* ============================ 道F task-3:装置维护写通道(UI 注入执行侧)
*
* 为什么在这一层实现而不是在页面里:页面属 Drivers/BSP/UI,反调 App 侧的
* sys_clock / prot_elm / sys_log 是分层方向的反例(layer-deps 拦),更关键的是
* **模拟器根本不链 sys_clock**(simulator/dbg_log.h:4「模拟器无 sys_clock/RTC」)——
* 页面里用一次就是"MCU 编得过、模拟器链接期爆"。写动作一律走 ui_data_maint_ops_t,
* 两端各给实现(口径 docs/UI设计/UI_数据供给设计说明.md §6.2/§7.2)。
*
* uiTask 阻塞上界(IWDG 800ms 窗、本任务在 required_mask 内):本通道里只有
* 时钟设置会碰硬件——sys_clock_set_unix 内含 1 次 SD2506 写,再叠加本适配层刻意
* 加的回读复核 1 次读,各 SD2506_I2C_TIMEOUT_MS=100ms ⇒ 最坏 ≈200ms 落在一次
* "保存"按键上(心跳在循环尾,仍远小于 800ms;对时任务同样在本机上做过这件事)。
* 复归/清除两条只写内存标志或投递给别的任务,无 I/O。
*/
/*
* 函数功能:设置墙钟并**回读复核**(真机 = 写 RAM 基准 + 写 SD2506 RTC)
* 入口参数:a_epoch - Unix 秒(本地时间域,与 sys_clock 同一口径,无时区偏移) uint32_t
* 返回值:0 - 已生效且 RTC 回读一致;-1 - 越界 / 未生效 / RTC 回读不符 int
* 限定条件:uiTask 上下文(页面"保存"按键直达,同步返回终态)
* 函数说明:⚠ **sys_clock_set() 恒返 0**(App/util/sys_clock.c:462 起:RTC 写失败
* 只打 DBG_ERROR,不改返回值),所以本函数不能拿它的返回值当结论——
* 回读是唯一真判据;而只回读 sys_clock_get() 证明的仅是"RAM 基准换了",
* 面板对时的用户意图是**掉电后还要是对的**,故必须回读 RTC 本身。
* RTC 坏了但墙钟已改时本函数返 -1(屏上"保存行=ERR + 值列跟着新时间",
* 两个事实各说各的,比笼统报 OK 诚实);秒位允许 ±3 与"分/时/日回绕"两种
* 形态,因为两次读之间墙钟与 RTC 都在走。
* 上限 UI_DATA_CLOCK_EPOCH_MAX(2038) 在 UI 层已挡,这里再挡一次:
* 执行侧不信任调用侧(0 还兼作"未建钟"哨兵,面板永远不该发 0)
*/
static int ui_task_maint_clock_set(uint32_t a_epoch) {
sd2506_time_t want;
sd2506_time_t got;
uint32_t now;
int ds;
if ((a_epoch == 0u) || (a_epoch > (uint32_t)UI_DATA_CLOCK_EPOCH_MAX)) {
return -1;
}
if (sys_clock_set_unix(a_epoch) != 0) {
return -1; /* 今天到不了这条腿(sys_clock_set 恒 0),留着是因为它是契约 */
}
now = sys_clock_get();
if ((now == UINT32_MAX) || ((now + 2u) < a_epoch) || (now > (a_epoch + 2u))) {
return -1; /* RAM 基准没跟上:连"现在就对"都没成立 */
}
if (sys_clock_get_time(&want) != 0) {
return -1;
}
if (sd2506_get_time(&got) != BSP_OK) {
return -1; /* RTC 读不动:掉电保持这件事无法证明 */
}
if ((got.year != want.year) || (got.month != want.month) || (got.day != want.day) ||
(got.hour != want.hour) || (got.minute != want.minute)) {
return -1;
}
ds = (int)got.second - (int)want.second;
if (ds < 0) {
ds = -ds;
}
if ((ds > 3) &&
!(((want.second >= 58u) && (got.second <= 3u)) ||
((got.second >= 58u) && (want.second <= 3u)))) {
return -1;
}
return 0;
}
/*
* 函数功能:请求强制复归(跨上下文:protectTask 下一拍消费)
* 入口参数:无
* 返回值:0 - 已投递(prot_elm_request_reset 无拒绝分支) int
* 限定条件:uiTask 上下文;不等待执行结果
* 函数说明:只投递不阻塞(契约:UI 页面二次确认后调,页面立刻给"已请求复归")。
* **不在这里投操作记录**:远端 104 YK11 那条已经在
* App/protocol/iec104/iec104_pts.c 投过,在这里再投一次就会给每一次
* 主站复归多记一条"面板复归",审计面直接失真。本地这条由页面投
* (只有页面知道"这是人按的"),措辞也不同串:见 Drivers/BSP/UI/ui_page_reset.c
*/
static int ui_task_maint_reset_req(void) {
prot_elm_request_reset();
return 0;
}
/*
* 函数功能:取当前处于动作态的保护元件个数(复归页观测行 "保护动作 N" 的 N)
* 入口参数:无
* 返回值:位图中置 1 的位数(纯 RAM 读,无锁) uint32_t
* 限定条件:uiTask 绘制路径每帧可直调
* 函数说明:给个数而不是位图,是为了让 160x160 上一行放得下、也让"复归前 N>0 →
* 复归后 N=0"这个台架判据肉眼可读;判据前提(产品构建处于未标定闭锁,
* App/task/protect_task.c:98 停走判据 ⇒ 闭锁期 N 恒 0)已写进页面注释
*/
static uint32_t ui_task_maint_active_count(void) {
uint32_t mask = prot_elm_active_mask();
uint32_t n = 0u;
while (mask != 0u) {
n += (mask & 0x1u);
mask >>= 1;
}
return n;
}
/*
* 函数功能:投一条本地操作记录(面板动作留痕)
* 入口参数:a_desc - 描述串(ASCII,长度须 <= SYSLOG_DESC_MAX=28) const char *
* 返回值:无
* 限定条件:uiTask 上下文;只在动作真正被受理之后由页面调用
* 函数说明:用 quiet 口(与 iec104_serv.c 对时留痕同一选择):uiTask 不属于深链
* 任务,但 quiet 口的收/丢判据与 syslog_post 一致且不在调用链上摊开
* 行缓冲,留痕这件事没理由选更贵的那个口。
* 丢条不回报页面:syslog 侧有累计丢弃计数(syslog_post_quiet_dropped())
* 可查;页面上"动作做了没"与"这条审计写没写"是两件事,不该混在一个
* 态标里——把后者做成 ERR 会让人以为复归没做而再按一次
*/
static void ui_task_maint_oper_note(const char *a_desc) {
if (a_desc == NULL) {
return;
}
syslog_post_quiet(SYSLOG_OPER, a_desc);
}
/*
* 函数功能:请求清除某一类记录(只投递,落地在 defaultTask 的 syslog_drain 尾部)
* 入口参数:a_log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
* 返回值:0 - 已受理(MCU 端随后通常是 SYSLOG_CLEAR_WAIT);-1 - 越界编号 int
* 限定条件:uiTask 上下文
* 函数说明:本层是 UI 编号 → syslog_id_t 的**唯一**映射点之一(与 s_log_map 同表),
* 绝不在 UI 侧做 f_unlink / 也不调 MCU 专属的 syslog_clear_apply():
* 记录卷的属主是 defaultTask,uiTask 去碰 FAT 既违反唯一写者,又给
* IWDG required_mask 内的任务添一条无上界的存储路径(口径 App/log/README.md)
*/
static int ui_task_maint_clear_req(uint8_t a_log_id) {
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
return -1;
}
return (syslog_request_clear(s_log_map[a_log_id]) == 0) ? 0 : -1;
}
/*
* 函数功能:取上条清除请求的状态(值直通,不改写、不翻译)
* 入口参数:a_log_id - UI 记录源编号 uint8_t
* 返回值:UI_DATA_CLEAR_ST_NONE/_OK/_WAIT/_FAIL;越界编号按 NONE int
* 限定条件:uiTask 绘制路径每帧可直调(纯内存状态读)
* 函数说明:SYSLOG_CLEAR_* 与 UI_DATA_CLEAR_ST_* 逐值相等(0/1/-1/-2),由
* test/host/ui_data TC-UIDATA-024 钉住;这里保持直通,是为了让页面
* 显示的就是日志侧的事实,而不是"适配层以为的事实"
*/
static int ui_task_maint_clear_state(uint8_t a_log_id) {
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
return UI_DATA_CLEAR_ST_NONE;
}
return syslog_clear_state(s_log_map[a_log_id]);
}
/* 维护通道函数表(六件齐全;ui_data_maint_bind 拒绝残表,缺件就是装配错) */
static const ui_data_maint_ops_t s_maint_ops = {
ui_task_maint_clock_set,
ui_task_maint_reset_req,
ui_task_maint_active_count,
ui_task_maint_oper_note,
ui_task_maint_clear_req,
ui_task_maint_clear_state
};
static const ui_rec_reader_t s_rec_reader = { ui_task_rec_count, ui_task_rec_get };
/*
* 函数功能:SOE 记录条数适配(道E task-15 reader 注入)——读落盘文件的完整槽数
* 入口参数:无
* 返回值:条数;超过 uint16 上限时钳到 65535(组件契约是 uint16_t) uint16_t
* 限定条件:uiTask 上下文同步调用(SOE 记录页绘制路径)
*/
static uint16_t ui_task_soe_count(void) {
uint32_t n = soe_store_count();
return (n > 65535u) ? 65535u : (uint16_t)n;
}
/*
* 函数功能:读第 idx 条 SOE 记录并翻成 UI 视图(二进制 → 不透明显示字段)
* 入口参数:idx - 记录序号 uint32_t
* p_out - UI 侧记录视图(契约见 Drivers/BSP/UI/ui_w_soe.h) ui_w_soe_rec_t *
* 返回值:0 - 成功;-1 - 读失败 / 卷忙 / 校验和不过(页面这行退化显示,不猜) int
* 限定条件:uiTask 绘制路径;soe_store_read() 是 try 锁非阻塞,绝不拖住 UI 节拍
* 函数说明:时标**取记录内的 CP56**(事件自己的时间),不用"现在"换算。两条口径
* 叠起来正好与文本记录页一致,别"顺手补 8 小时":
* ① sys_clock 的 epoch 域本身就是本地时间(App/util/sys_clock.h 的
* sys_clock_set_unix 注明"Unix epoch time (秒,本地时间)");
* ② App/log/soe_rec.c 的 epoch→CP56 分解不加任何时区偏移。
* 所以这里打出来的墙上时钟 == 运行/自检记录页里 sys_clock_get_str() 的口径;
* 哪天真要引入显式时区,两侧必须一起改(跨模块陷阱,注释即防线)。
* 时钟无效(IV)不写在这里:它是显示语义,由 UI 侧按 SOE_FLG_IV 决定
* (组件把那行名字打"--"、值打"时钟无效"),App 只交代原始记录。
*/
static int ui_task_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) {
soe_rec_t rec;
uint32_t sec;
if ((p_out == NULL) || (soe_store_read(idx, &rec) != 0)) {
return -1;
}
/* CP56 字节表见 App/log/soe_rec.c 的 s_soe_epoch_to_cp56:
* [0..1]=分内毫秒(秒域时基 → 恒为秒×1000,亚秒不造假)[2]=分 [3]=时
* [4]=日 [5]=月 [6]=年-2000 */
sec = (((uint32_t)rec.cp56[1] << 8) | (uint32_t)rec.cp56[0]) / 1000u;
(void)snprintf(p_out->ts, sizeof(p_out->ts), "%04u-%02u-%02u %02u:%02u:%02u",
(unsigned)(2000u + ((uint32_t)rec.cp56[6] & 0x7Fu)),
(unsigned)((uint32_t)rec.cp56[5] & 0x0Fu),
(unsigned)((uint32_t)rec.cp56[4] & 0x1Fu),
(unsigned)((uint32_t)rec.cp56[3] & 0x1Fu),
(unsigned)((uint32_t)rec.cp56[2] & 0x3Fu),
(unsigned)sec);
p_out->type = rec.type;
p_out->elm = rec.elm_id;
p_out->flags = rec.flags;
p_out->seq = rec.seq;
p_out->value = rec.value;
return 0;
}
static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get };
/* ═══════════════ 故障录波注入表执行侧(UI 道,2026-10-29)═══════════════
* 纪律逐条对齐 soe_store.c 读者(vol_lock try / 锁跨完整文件序列 / 小窗口):
* - 卷忙(vol_lock_wait(0) 返 0)一律按「本轮无段」处理——uiTask 是
* LVGL + 推流节拍的所有者,等锁 = 停摆,宁可空一帧也不阻塞(lead 裁定);
* - 段完整性判定只信 wave_store 自己的判据(尺寸等式 / magic+ver / 双 CRC /
* sem==index),本胶水不读 avail/flags 做二次裁断(§6.3 与 2026-10-29
* 「VALID+CLIPPED 非矛盾」裁定同源:可用性不是完整性);
* - 整段 5250B 不驻留:256B 静态窗口分次读,头区一次、数据区流式过 CRC。
* uiTask 阻塞上界(IWDG 800ms 窗、uiTask 在 required_mask 内;写法对齐上方
* 维护通道那段上界,同文件先例):
* - 第零项是「本块不等待」:vol_lock_wait(0) 拿不到锁即按「本轮无段」
* 返回——不等锁 = 零阻塞,它本身就是上界的一项,不是只保心跳判读;
* - 一次渲染实际发生的 I/O(进回放页首绘、或按键后重绘那帧)三件叠加:
* ① 表重扫 = 1 次 f_opendir + 至多 WAVE_ST_SEG_MAX(100)次 f_readdir,
* 收尾是 100 元插入排序(纯内存);
* ② 段头装载 = 仅未缓存段才发生:1 次 f_open + 头区 1 次 128B 读 +
* head_parse 内的头 CRC + 数据区 20 次 256B 流式读 + 尾 2B 一次
* = 最坏 22 次小读;5120B 逐字节 CRC 与 sem 表校验是纯计算,
* 168MHz 下微秒到毫秒量级;缓存命中零卷操作;
* ③ 窗口装载 = 1 次 f_open + 1 次 f_lseek + 1 次 80B 读(40 点一窗),
* 解码 40 点 + 逐点 code_to_milli 峰值扫描 40 次(int64 乘除,
* 微秒级)——不是全段 8×320 扫描,回放页一屏只碰一个通道一窗;
* - 纯内存不碰卷的路径(评审据此看最坏的落点):manual_req 只投一个
* 请求字节;已缓存段的上下切道、左右切段的段号表环游;空态与坏态
* 直接返回。所以最坏只落在「第一次进回放页 / 第一次切到未缓存段」
* 这类一次性动作上,同段内连续按键不重复付 ②。
* 没有实测数,上面是与落盘道同一把尺的**推算**:落盘道的告警线是
* 50 ms / 21 次写,读路径无簇分配、不写 FAT 元数据,只会更便宜,故按
* 同量级不高于它估——一次渲染最坏一窗 ≈ 段表 + 一段全验 + 一窗,仍远
* 小于 800ms。实测归台架,仪器就用本文件已经在跑的 task_prof 单轮
* 工作耗时探针,不必新加埋点。
* 段序口径(V0.1 已知限制,终报点名):目录重建后按段号数值排序环游,
* 盘上写满 100 段翻转后「数值序 = 时间序」不再严格成立——每屏顶行都带
* 段号自证身份,查事故先看号再看序;翻转窗口排序升级留给台架结论。
*/
static uint8_t s_wave_idx[WAVE_ST_SEG_MAX]; /* 盘上现存段号升序表 */
static uint8_t s_wave_cnt; /* 表内条数 */
static uint8_t s_wave_scanned; /* 表是否有效(manual_req 后作废) */
static uint8_t s_wave_cached_idx; /* 段头缓存属于哪一段 */
static uint8_t s_wave_cache_ok; /* 该段已验完整(头+数据 CRC) */
static wave_seg_meta_t s_wave_meta; /* 已验段的头(172B,唯一驻留) */
static uint8_t s_wave_buf[256u]; /* 读窗:数据区流式 256B/次(lead 裁定
* 2026-10-29 第 1 条·第一条:128B 抬 256B,
* f_read 次数对半砍,RAM 只多 128B;
* 不选 100 位「已验」位图——位图的作废纪律
* 一旦漏做,「CRC 不符必须拒收」这条台架
* 判据对旧段号会静默失效,多 128B 没这个面)*/
/*
* 函数功能:重扫 /wave 目录,把现存段号收集成升序表
* 入口参数:无
* 返回值:0 - 扫描完成(条数见 s_wave_cnt);-1 - 卷忙本轮跳过 int
* 限定条件:uiTask;一次 f_opendir + 有界 f_readdir,全程锁内
* 函数说明:目录不存在(从未录过波)与目录空都归「0 段」,不是错误;
* 名字不合形态的文件(E_NAME)静默跳过——那是别的道写进来的
* 东西,回放页不替它背书也不报警
*/
static int ui_task_wave_scan(void) {
static DIR dir; /* static:同 FIL 理由,压栈深 */
FILINFO finfo;
uint8_t idx;
uint8_t i;
if (vol_lock_wait(0u) == 0u) {
return -1;
}
s_wave_cnt = 0u;
if (f_opendir(&dir, WAVE_ST_DIR) == FR_OK) {
for (;;) {
if (f_readdir(&dir, &finfo) != FR_OK || finfo.fname[0] == '\0') {
break;
}
if (finfo.fattrib & AM_DIR) {
continue;
}
if (wave_store_idx_from_name(finfo.fname, &idx) == 0) {
if (s_wave_cnt < WAVE_ST_SEG_MAX) {
s_wave_idx[s_wave_cnt++] = idx;
}
}
}
f_closedir(&dir);
}
vol_lock_release();
/* 插入排序:条数上限 100,不引 qsort 的 stdlib 依赖 */
for (i = 1u; i < s_wave_cnt; i++) {
uint8_t j;
uint8_t tmp;
tmp = s_wave_idx[i];
j = i;
while ((j > 0u) && (s_wave_idx[j - 1u] > tmp)) {
s_wave_idx[j] = s_wave_idx[j - 1u];
j--;
}
s_wave_idx[j] = tmp;
}
s_wave_scanned = 1u;
return 0;
}
/*
* 函数功能:确保段表可用(必要时重扫)
* 入口参数:无
* 返回值:0 - 表可用;-1 - 卷忙 int
* 限定条件:uiTask 绘制/按键链
*/
static int ui_task_wave_table(void) {
if (s_wave_scanned == 0u) {
return ui_task_wave_scan();
}
return 0;
}
/*
* 函数功能:表内查段号下标
* 入口参数:a_idx - 段号;a_pos - 出参下标
* 返回值:1 - 命中;0 - 不在表内 uint8_t
* 限定条件:表已可用(先过 ui_task_wave_table)
*/
static uint8_t ui_task_wave_find(uint8_t a_idx, uint8_t *a_pos) {
uint8_t i;
for (i = 0u; i < s_wave_cnt; i++) {
if (s_wave_idx[i] == a_idx) {
*a_pos = i;
return 1u;
}
}
return 0u;
}
/*
* 函数功能:读段并做完整性判定(头 CRC 由 head_parse 判,数据 CRC 流式判),
* 成功则把段头留在 s_wave_meta 供 seg_win 免重读
* 入口参数:a_idx - 段号
* 返回值:UI_WAVE_OK / UI_WAVE_NONE(卷忙/文件没了)/ UI_WAVE_BAD(判废) int
* 限定条件:uiTask;锁跨 open→close 完整序列;头 128B 一次、数据区 256B
* 分窗流式(共约 1 + 20 次 f_read),整段不驻留
* 耗时账(lead 裁定 2026-10-29 第 1 条·第三条并存):未缓存段的完整性
* 判定最坏约 21 次小读 + 5120B 逐位 CRC,全部在卷锁内、uiTask 上;
* IWDG 800ms 窗内余量几何**实测毫秒归台架**(终报点名待测项)。
* 缓存只留一段:左右切段 A→B→A 会把 B 的校验再来一遍——接受该
* 成本的代价换「CRC 不符必须拒收」永远现算,不做跨键存的位图
* 函数说明:缓存命中(同段且已验)直接返 OK——左右切段回到上一段、上下切
* 通道都靠它把重复的 5250B 全扫压成一次;文件读中途消失(被别的
* 写者轮转掉)按 NONE 处理,页面下轮自然重扫
*/
static int ui_task_wave_load_head(uint8_t a_idx) {
static FIL f; /* static:FIL = 528B,理由同 soe_store_read */
char path[WAVE_ST_PATH_MAX]; /* 容量 = WAVE_ST_PATH_MAX(17 字节口径,
* 自己声明路径缓冲不得写死 16) */
FRESULT fr;
UINT br = 0u;
int rc;
if ((s_wave_cache_ok != 0u) && (s_wave_cached_idx == a_idx)) {
return UI_WAVE_OK;
}
if (wave_store_seg_path(path, (uint32_t)sizeof(path), a_idx) != 0) {
return UI_WAVE_NONE;
}
if (vol_lock_wait(0u) == 0u) {
return UI_WAVE_NONE; /* 卷忙 = 本轮按无段处理(不阻塞 uiTask) */
}
rc = UI_WAVE_NONE;
if (f_open(&f, path, FA_READ) == FR_OK) {
uint32_t size;
uint32_t total;
uint32_t n;
size = (uint32_t)f_size(&f);
total = 0u;
rc = UI_WAVE_BAD;
/* 第一步:头区(首窗 128B = 当前形态整头);head_parse 自带判废 */
n = (size < (uint32_t)WAVE_ST_HEAD_SZ_CUR) ? size
: (uint32_t)WAVE_ST_HEAD_SZ_CUR;
fr = f_read(&f, s_wave_buf, n, &br);
if ((fr == FR_OK) && (br == n)) {
if (wave_store_head_parse(s_wave_buf, n, &s_wave_meta) == 0) {
total = wave_store_seg_size(&s_wave_meta);
}
}
/* 第二步:尺寸等式(零长/长度与头部字段矛盾 = 整段判废,§6.3) */
if ((total != 0u) && (size == total)) {
uint32_t off;
uint16_t crc;
uint8_t tail[2];
/* 第三步:数据区流式 CRC(128B 窗;CRC 覆盖不含尾两字节自身) */
crc = WAVE_ST_CRC_INIT;
off = wave_store_head_bytes(s_wave_meta.ch_count);
rc = UI_WAVE_BAD;
while ((off + 2u) < size) {
n = (size - 2u - off < (uint32_t)sizeof(s_wave_buf))
? (size - 2u - off) : (uint32_t)sizeof(s_wave_buf);
fr = f_read(&f, s_wave_buf, n, &br);
if ((fr != FR_OK) || (br != n)) {
rc = UI_WAVE_BAD;
break;
}
crc = wave_store_crc_update(crc, s_wave_buf, n);
off += n;
}
/* 第四步:读回存储值比对(低字节在前) */
if ((rc == UI_WAVE_BAD) && (f_lseek(&f, size - 2u) == FR_OK) &&
(f_read(&f, tail, 2u, &br) == FR_OK) && (br == 2u)) {
if (crc == (uint16_t)(tail[0] | ((uint16_t)tail[1] << 8))) {
rc = UI_WAVE_OK;
}
}
} else if (total == 0u) {
rc = UI_WAVE_BAD; /* seg_size 返 0 = 头字段自相矛盾,判废 */
} else {
rc = UI_WAVE_BAD; /* 长度与头部字段不符,判废 */
}
(void)f_close(&f);
}
vol_lock_release();
if (rc == UI_WAVE_OK) {
s_wave_cached_idx = a_idx;
s_wave_cache_ok = 1u;
} else {
s_wave_cache_ok = 0u;
}
return rc;
}
/*
* 函数功能:录波触发页唯一写出口——投一次手动触发请求
* 入口参数:无
* 返回值:0 - 已受理;非零 - 拒收(未 IDLE/冷却中/参数),页面显示 ERR int
* 限定条件:uiTask(request 本身任意上下文可调,这里只是转发不等待)
* 函数说明:受理 = ring 进冻结窗,成段最快也在约一秒后由默认任务落盘;
* 顺手作废段表缓存,页面下次取数会看到新段
*/
static int ui_task_wave_manual_req(void) {
int r = wave_ring_request((uint8_t)WAVE_TRIG_MANUAL, 0u);
if (r == 0) {
s_wave_scanned = 0u;
s_wave_cache_ok = 0u; /* lead 裁定 2026-10-29 第 3 条:段表与段头缓存
* 一起作废——同名文件会被轮转重写,旧头字段
* (pts/pre/k/b/逐路 avail)不作废就会顶着新段
* 的号用旧尺子。单槽缓存本身 = 已知限制:
* 只挡「本段验过不再重验」,多段来回切会重验 */
}
return r;
}
/*
* 函数功能:取盘上「数值最大段号」(回放页开屏落点)
* 入口参数:a_idx - 出参段号
* 返回值:UI_WAVE_OK / NONE(无段或卷忙本轮)/ BAD 本件不产(表只含现存文件)
* 函数说明:口径 = 数值最大,**不是时间最新**(lead 裁定 2026-10-29):
* 回绕后数值序不等于时间序,页顶显示的段号即自证;真做时间序要
* 逐段开文件读 frame_seq/t0,一百次开档不在回放页的账上,本波不做
*/
static int ui_task_wave_seg_first(uint8_t *a_idx) {
if (ui_task_wave_table() != 0) {
return UI_WAVE_NONE; /* 卷忙 = 本轮无段(同一条不阻塞纪律) */
}
if (s_wave_cnt == 0u) {
return UI_WAVE_NONE;
}
*a_idx = s_wave_idx[s_wave_cnt - 1u];
return UI_WAVE_OK;
}
/*
* 函数功能:环游取邻段(a_dir:-1 更早 / +1 更新;段号数值序环回)
* 入口参数:a_cur - 当前段号;a_out - 出参邻段段号
* 返回值:UI_WAVE_OK / NONE(表不可用或已无邻段可去) int
* 限定条件:uiTask;当前段已被轮转覆盖时先重扫一次再定位
*/
static int ui_task_wave_seg_around(uint8_t a_cur, int8_t a_dir, uint8_t *a_out) {
uint8_t pos = 0u;
if (ui_task_wave_table() != 0) {
return UI_WAVE_NONE;
}
if (ui_task_wave_find(a_cur, &pos) == 0u) {
if ((ui_task_wave_scan() != 0) || (s_wave_cnt == 0u) ||
(ui_task_wave_find(a_cur, &pos) == 0u)) {
/* 当前段没了(翻转覆盖):退回首段,画面由调用侧重载 */
if (s_wave_cnt == 0u) {
return UI_WAVE_NONE;
}
*a_out = s_wave_idx[0u];
return UI_WAVE_OK;
}
}
if (s_wave_cnt == 1u) {
*a_out = s_wave_idx[0u]; /* 单段:原地(用户看到的仍是这段) */
return UI_WAVE_OK;
}
if (a_dir > 0) {
*a_out = s_wave_idx[(uint8_t)((pos + 1u) % s_wave_cnt)];
} else {
*a_out = s_wave_idx[(uint8_t)((pos + s_wave_cnt - 1u) % s_wave_cnt)];
}
return UI_WAVE_OK;
}
/*
* 函数功能:切到段号表里的前一段(LR 左键的落点,数值升序表 → 「前」= 号更小)
* 入口参数:a_cur 当前段号;a_out 输出切换后的段号(越界/空表不写)
* 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 盘上一段都没有 int
* 限定条件:仅 uiTask 上下文(段号表是本任务私有缓存)
* 函数说明:只是 ui_task_wave_seg_around 的方向包装,存在的理由是 ops 表要两个
* 独立函数指针,不写第二份查找逻辑。
*/
static int ui_task_wave_seg_prev(uint8_t a_cur, uint8_t *a_out) {
return ui_task_wave_seg_around(a_cur, -1, a_out);
}
/*
* 函数功能:切到段号表里的后一段(LR 右键;数值升序表 → 「后」= 号更大)
* 入口参数:a_cur 当前段号;a_out 输出切换后的段号(越界/空表不写)
* 返 回 值:UI_WAVE_OK 已给出;UI_WAVE_NONE 盘上一段都没有 int
* 限定条件:仅 uiTask 上下文(同上)
* 函数说明:与 seg_prev 共用 ui_task_wave_seg_around,切段即触发一次懒加载;
* 未缓存段的读盘 + 流式 CRC 也发生在这条调用链上(单轮上界见本
* 文件「故障录波注入表执行侧」块首注释的 uiTask 阻塞上界段)。
*/
static int ui_task_wave_seg_next(uint8_t a_cur, uint8_t *a_out) {
return ui_task_wave_seg_around(a_cur, 1, a_out);
}
/*
* 函数功能:读段头(含双 CRC 完整性判定——判定发生在 ui_task_wave_load_head)
* 入口参数:a_idx - 段号;a_eng/a_trig/a_clip - 段头三字段原值
* 返回值:UI_WAVE_OK / NONE / BAD int
* 限定条件:uiTask;命中缓存零卷操作
*/
static int ui_task_wave_seg_head(uint8_t a_idx, uint8_t *a_eng,
uint8_t *a_trig, uint8_t *a_clip) {
int rc = ui_task_wave_load_head(a_idx);
if (rc != UI_WAVE_OK) {
return rc;
}
*a_eng = s_wave_meta.eng;
*a_trig = s_wave_meta.trig_src;
*a_clip = (uint8_t)(s_wave_meta.clip_mask & 0xFFu);
return UI_WAVE_OK;
}
/*
* 函数功能:读固定显示窗(触发点前后各一周波)的码值与峰值
* 入口参数:a_idx - 段号;a_ch - 通道号;a_codes - UI_WAVE_WIN_PTS 个码值
* (已并入段头 b_codes);a_peak_milli - 窗内绝对值最大点的 mV
* 返回值:UI_WAVE_OK / NONE(段头未验过/卷忙)/ BAD(数据区读不齐) int
* 限定条件:uiTask;先段头后窗口(未过完整性判定的段不供数据,页面显示
* 「该段不可读」而不是半截波形——判废优先于展示)
* 函数说明:窗口起点用段头 pre_pts 现算(§8.2「数据不是编译常量」:
* 起点 = pre_pts - 窗长一半,与光标居中是同一个 20 的一半,
* 改窗宽只改 ui_data.h 的 UI_WAVE_WIN_PTS 一处);峰值的 mV
* 换算逐点走 wave_store_code_to_milli —— 全仓唯一口径,
* 这里不重抄公式(§6.3;PC 道同一条款)
*/
static int ui_task_wave_seg_win(uint8_t a_idx, uint8_t a_ch,
int16_t *a_codes, int32_t *a_peak_milli) {
static FIL f;
char path[WAVE_ST_PATH_MAX];
UINT br = 0u;
uint32_t off;
uint32_t want;
uint32_t start;
uint32_t got_pts = 0u;
int16_t raw[UI_WAVE_WIN_PTS];
int32_t milli;
int rc;
if (ui_task_wave_load_head(a_idx) != UI_WAVE_OK) {
return UI_WAVE_BAD; /* 段头坏了/读不到:窗口无从谈起 */
}
if (a_ch >= s_wave_meta.ch_count) {
return UI_WAVE_BAD;
}
if ((s_wave_meta.pts_per_ch < UI_WAVE_WIN_PTS) ||
(s_wave_meta.pre_pts < (UI_WAVE_WIN_PTS / 2u))) {
return UI_WAVE_BAD;
}
start = (uint32_t)s_wave_meta.pre_pts - (UI_WAVE_WIN_PTS / 2u);
off = wave_store_data_off(s_wave_meta.ch_count, s_wave_meta.pts_per_ch,
a_ch, start);
if (off == 0xFFFFFFFFu) {
return UI_WAVE_BAD;
}
want = wave_store_data_bytes_avail(s_wave_meta.pts_per_ch, (uint16_t)start,
UI_WAVE_WIN_PTS);
if (want != UI_WAVE_WIN_PTS * 2u) {
return UI_WAVE_BAD; /* 窗口贴不出完整 40 点:显式不可读 */
}
if (wave_store_seg_path(path, (uint32_t)sizeof(path), a_idx) != 0) {
return UI_WAVE_NONE;
}
if (vol_lock_wait(0u) == 0u) {
return UI_WAVE_NONE;
}
rc = UI_WAVE_BAD;
if (f_open(&f, path, FA_READ) == FR_OK) {
if ((f_lseek(&f, off) == FR_OK) &&
(f_read(&f, s_wave_buf, want, &br) == FR_OK) && (br == want)) {
if ((wave_store_le_decode(s_wave_buf, want, raw, UI_WAVE_WIN_PTS,
&got_pts) == 0) &&
(got_pts == UI_WAVE_WIN_PTS)) {
rc = UI_WAVE_OK;
}
}
(void)f_close(&f);
}
vol_lock_release();
if (rc == UI_WAVE_OK) {
uint8_t i;
int32_t peak = 0;
for (i = 0u; i < UI_WAVE_WIN_PTS; i++) {
int32_t v;
v = (int32_t)raw[i] + s_wave_meta.ch[a_ch].b_codes;
if (v > 32767L) {
v = 32767L;
} else if (v < (-32768L)) {
v = -32768L;
}
a_codes[i] = (int16_t)v;
/* 显示与度量同一把尺(「谁被显示谁被度量」,§6.3):
* a_codes 画的是 raw+b,而 wave_store_code_to_milli 的公式是
* (a_code + b_codes) × k_q16 / 65536000 取整(App/algo/
* wave_store.c:567 实测口径)——传 raw 进去,b 已由唯一口径
* 自己加过,PK 数与屏上波形天然同尺,这里不再加第二遍
* (加两遍才是错);clamp 只护 int16 画布,b 的 V0.1 恒 0 */
if (wave_store_code_to_milli(&s_wave_meta.ch[a_ch], raw[i],
&milli) == 0) {
if ((milli < 0 ? -milli : milli) >
(peak < 0 ? -peak : peak)) {
peak = milli;
}
}
}
*a_peak_milli = peak;
}
return rc;
}
/* 录波通道函数表(六件齐全;ui_data_wave_bind 拒绝残表,缺件就是装配错) */
static const ui_data_wave_ops_t s_wave_ops = {
ui_task_wave_manual_req,
ui_task_wave_seg_first,
ui_task_wave_seg_prev,
ui_task_wave_seg_next,
ui_task_wave_seg_head,
ui_task_wave_seg_win,
};
/*
* 函数功能:推流往返边界的心跳续命钩(F3,注册给 ui_net_sink)
* 入口参数:无
* 返回值:无
* 限定条件:uiTask 轮询上下文(sink 在往返返回处调用),只做置位、不阻塞
* 函数说明:为何经回调注入而非 sink 直调 task_wd_beat()——分层边界(BSP 不得
* include App 头,layer-deps 门禁);心跳属主仍是本任务。
*/
static void ui_task_sink_beat(void) {
task_wd_beat(TASK_WD_UI);
}
#define DBG_TAG "[UI_TASK]"
#include "dbg_log.h"
/*
* 函数功能:等待网络就绪事件
* 入口参数:无
* 返回值:无
* 限定条件:sys_evtHandle 为 NULL 时直接返回
* 函数说明:无限期等待 SYS_EVT_NET_READY 标志
*/
static void ui_task_wait_net_ready(void) {
if (sys_evtHandle != NULL) {
/* osFlagsNoClear:就绪位是状态,不能被本任务消费掉(否则其它等待者永不唤醒)。
* 2026-10-02 台架修复:等待改 100ms 分片 + 每片喂心跳(与 timeSync/netTask/
* ftpTask 入口同模式)——IWDG 必需掩码含 UI 位,原 osWaitForever 无心跳:
* IWDG 启动(存储就绪)时网络未必就绪 → 掩码 800ms 不可达 → 停喂 → 复位循环。
* 100ms 分片 = 抽样窗,纯任务睡眠零队列开销。 */
for (;;) {
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny | osFlagsNoClear, 100u);
if ((r & osFlagsError) == 0u) {
break; /* NET_READY 已置位(状态位,未被消费) */
}
task_wd_beat(TASK_WD_UI); /* 看门狗:分片喂心跳(屏障等待态) */
}
}
}
/*
* 函数功能:UI 主任务入口,初始化显示/菜单/推流并进入刷新主循环
* 入口参数:argument - FreeRTOS 任务参数(未使用) void *
* 返回值:无
* 限定条件:FreeRTOS 已启动
* 函数说明:1. 等待 SYS_EVT_NET_READY 后依次初始化 ui_port/ui_menu/ui_net_sink
* 2. 主循环周期调用 ui_port_step 与 ui_net_sink_poll 刷新界面
*/
void ui_task_func(void *argument) {
(void)argument;
task_wd_bind(TASK_WD_UI); /* FTL 心跳路由:登记本任务句柄(入口一次) */
/* 等待网络就绪期间不可能喂狗:本任务此时尚未进入主循环,而就绪位由 netTask 置位。
* 该段若长期不成立,看门狗会复位——这是期望行为(网络 bring-up 卡死须暴露)。
* 注意本任务在 required_mask 内,主循环跑起来后须保证每 100ms 窗口内至少喂一次。 */
DBG_INFO("UI task: started, wait net ready");
ui_task_wait_net_ready();
DBG_INFO("UI task: net ready, init port");
DBG_INFO("UI task: ui_port_init enter");
if (ui_port_init() != 0) {
DBG_ERROR("UI port init: FAIL");
for (;;) { osDelay(1000); }
}
DBG_INFO("UI port init: OK");
/* 记录 reader 注入:必须在 ui_menu_init 之前(记录页在此前不可能被打开,
* 但契约把"启动期先绑定"写死;未绑定窗口组件按空态防御——见 ui_w_records.h) */
ui_w_records_bind(&s_rec_reader);
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页数据入口(未绑定按"无记录"空态) */
/* 装置维护写通道注入(道F task-3:时钟设置 / 强制复归 / 清除记录):同样必须先于
* ui_menu_init。未注入的窗口里六个入口一律安全返回(页面显 ERR 或空态),
* 既不崩也不假成功;但残表是真故障,必须能在启动日志里看见,不能静默 */
if (ui_data_maint_bind(&s_maint_ops) != 0) {
DBG_ERROR("UI maint bind: FAIL");
}
/* 录波通道注入(故障录波道 UI 侧,2026-10-29):手动录波触发页与回放页
* 的唯一数据入口,同样先于 ui_menu_init。未注入窗口里六件按「无录波段」
* 空态渲染(不画线、不崩);残表 = 装配错,启动日志必须可见 */
if (ui_data_wave_bind(&s_wave_ops) != 0) {
DBG_ERROR("UI wave bind: FAIL");
}
/* 快照层复位:必须先于 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");
for (;;) { osDelay(1000); }
}
DBG_INFO("UI menu init: OK");
if (ui_standby_init(UI_STANDBY_IDLE_MS, ui_task_time_cb) != 0) {
DBG_ERROR("UI standby init: FAIL");
} else {
DBG_INFO("UI standby: ready (idle=60s)");
}
if (ui_net_sink_start(UI_NET_SINK_PORT) != 0) {
DBG_ERROR("UI net sink: start FAIL");
} else {
DBG_INFO("UI net sink: started (port %u)", (unsigned)UI_NET_SINK_PORT);
}
/* F3:往返边界心跳钩(契约见 ui_net_sink.h;须在首轮 poll 之前注册)。
* start 失败也要注:失败路径的 close/重建重试同样经往返 */
ui_net_sink_set_beat_cb(ui_task_sink_beat);
#if defined(TEST_LATENCY_PROBE)
/* M3 探针 ui_loop:主循环实测周期(= LVGL 渲染 + 推流轮询 + osDelay(10))。
* 每轮开头先结算"上一轮到此轮"的间隔,再重置标记 → 得到真实循环周期。
* 单写者(本任务)/单读者(testTask),样本经 lat_probe_add 落静态缓冲。 */
lat_tmark_t tm_loop;
uint8_t tm_loop_valid = 0U;
#endif
/* 常开探针:TEST_LATENCY_PROBE 只在 CH395F 套件构建下打开,
* 且样本消费者是 testTask(产品构建里不跑)→ 产品构建拿不到任何 UI 周期数据。
* 故此处独立埋点,恒定生效、只写本模块的静态计数,由 defaultTask 周期上报。 */
while (1) {
/* 循环周期用 enter/exit 测:exit 结算"上轮起点 → 本轮起点",enter 记本轮起点。
* 首轮 exit 时尚未 enter 过 → 内部直接返回,不会拿 0 当起点。
* 选 enter/exit 而非"自己存一个 prev"的额外收益:两次循环之间段内标志保持为 1
* → 报告可读出"本轮已进行多久"。**这正是'uiTask 是否被饿住'的直接证据**:
* 若该值在两次报告间持续增长而 uic 不动,就是被调度饿死/阻塞,而非渲染拖长。 */
task_prof_exit(TASK_PROF_UI_PERIOD);
task_prof_enter(TASK_PROF_UI_PERIOD);
const uint32_t prof_t0 = task_prof_cyc(); /* 单轮工作耗时基准 */
#if defined(TEST_LATENCY_PROBE)
if (tm_loop_valid != 0U) {
lat_probe_add_seg("ui_loop", lat_mark_us(&tm_loop));
}
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 之后,
* 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后,
* 即使往返偶发变慢,心跳也不被其拖垮。
* ⚠ 若往返**永久**阻塞(xTaskNotifyWait portMAX_DELAY 不返回),本改只能发
* 一次心跳,救不了 → 彻底解耦须走事件驱动 accept,见文件头修改记录 v1.3。
* v1.4 补充:sink 内每个往返**返回点**已各自补喂(set_beat_cb 注入),
* 「单轮多个慢往返之和」不再构成缺席链;剩单往返 >800ms 才报——那是
* netTask 自身停摆,按位判据抓对了人(上界归「net 层跨上下文收口」)。 */
task_wd_beat(TASK_WD_UI);
/* 推流轮询段单独 enter/exit:它内部经 net_accept → net_wait_result 走到
* xTaskNotifyWait(portMAX_DELAY) 的**无限期阻塞**。若 uiTask 永久卡在这里,
* 样本永不结算,只有 task_prof 的"仍在段内已 X us"能证明它卡在何处 */
task_prof_enter(TASK_PROF_UI_SINK);
ui_net_sink_poll();
task_prof_exit(TASK_PROF_UI_SINK);
/* 单轮工作耗时(不含 osDelay):与循环周期对比即得"调度延迟" */
task_prof_sample(TASK_PROF_UI_WORK, task_prof_cyc() - prof_t0);
osDelay(UI_TASK_TICK_MS);
}
}