Files
STM32F4-Base/Drivers/BSP/UI/ui_page_wave_play.c
T
super_agent_bot 046097918d fix(ui/wave): 回放页切通道/切段后不重绘——key_cb 接管键后补失效
回放页 v1.0 建的键处理返回 true(接管全部键),但从未调
ui_content_page_invalidate():框架只在 key_cb 返回 false 时才替页面
失效,于是「键送达、状态已切、屏上纹丝不动」。该病被枚举缺陷完全
遮住(整页恒空、没有可切的画面),随本波枚举修复首次上板才暴露:
真机按 ←/→/↑/↓ 帧字节完全不变(同帧哈希),布局与状态机无问题。

- 上/下切通道、左/右切段成功路径、空态再探路径,各补一次
  ui_content_page_invalidate()(与 ui_page_clock / ui_page_password
  等已接管键的页面同纪律);
- 切段失败仍保持「按了没动」的诚实语义(不重绘、不把忙态清成空);
- 修改记录 v1.1。

/keil-build 0 错误 0 警告(Code 247,288 B,+4 B),RAM 不变;
台账 / 同码凭证同批刷新。

Refs #38
2026-11-02 18:21:34 +08:00

446 lines
18 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 Page wave_play (录波回放内容页)
* 模块功能:记录查询 → 录波回放 叶子的内容页——单通道阶梯波形回放(设计
* docs/子系统设计/故障录波设计说明.md §8.1 元素表逐条落地):
* 顶行 = 通道名 + 段号 + 触发源字母(M 手动 / P 保护 / T 定时);
* 波形区 = 两周波 40 点、每点占 3 px 的**阶梯线**(1 bpp 下平滑
* 折线的过零平缓段会被阈值吃掉,ui_deco.h 实测口径;阶梯线每段
* 非水平即垂直,天然绕开,也更贴近「离散采样」的事实);
* 参考线 = 0 线(虚线)+ 窗口峰值标注(定点 mV,ASCII);
* 底行 = 单位徽标(只读段头 eng,eng 绝不参与算式——§6.3 钉死,
* PC 道同一口径);形状受限通道右上打「削」徽标;
* 空态显式文案「无录波段」/「该段不可读」——画成一条直线等于
* 伪造证据(§8.1 空态行,此判据优先于「页面上总得画点东西」)。
* 键位:上下切通道、左右切段(key_intercept=1 才能把 LEFT 从
* 「关页」手里要来);ESC 关页。
* 取数纪律:本文件零 App include——段号/段头/窗口码值/峰值 mV 全部经
* ui_data.h 录波注入表进来(波形算式与定点换算在执行侧,页面
* 连 b_codes 都不碰,收到的码值已并偏移);首帧绘制才懒取数
* (开页链零 I/O,绘制定律不破)。
* 显示窗口与光标(与落盘道/PC 道的同一裁定,写在这里免得两头漂):
* 窗口 = 触发点前一周波 + 后一周波共 40 点,窗口起点 = 段内点号
* 「预窗点数 减 二十」(即执行侧 seg_win 的固定窗);虚线竖光标
* 落在窗内第 二十 与第 廿一 点之间(0 基第 19/20 点边界,即
* 波形区正中 x)——依据 = 设计 §4.2 触发点位置 + App/algo/
* wave_ring.h 的点序注释(触发 tick 采样 = 预窗最后一点),
* 页面不复述任何换算式。
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:DTU 项目组
* 创建日期:2026-10-29
* 修改记录:
* v1.0 2026-10-29 DTU 项目组 创建(故障录波道 UI 侧)
* v1.1 2026-11-02 DTU 项目组 按键切通道/切段补 ui_content_page_invalidate:
* 本页 key_cb 返回 true(接管键),框架不再替本页失效重绘,状态变了
* 屏上却纹丝不动——该病在枚举修复前整页恒空、从未有键可切,故随
* 2026-11-02 回放页修复首次上板才暴露(真机实证见 PR #67)。
*/
/* 头文件包含区 */
#include "ui_page_wave_play.h"
#include <string.h>
#include "ui_content_page.h"
#include "ui_data.h"
#include "ui_tokens.h"
/* 私有宏定义区 */
/* 绘制区几何(屏宽 160:波形区左右各留 20 px 边;高 90 px) */
#define WP_WX0 20 /* 波形区左边界 x */
#define WP_PX_PER_PT 3 /* 每点占宽(§8.1 阶梯线口径) */
#define WP_WTOP 34 /* 波形区顶 y(信息行下方) */
#define WP_WBOT 124 /* 波形区底 y(0 线最多贴到这里) */
#define WP_WSPANS 90 /* 纵向可用像素 = WP_WBOT - WP_WTOP */
#define WP_CUR_X (WP_WX0 + (UI_WAVE_WIN_PTS / 2u) * WP_PX_PER_PT)
/* 触发虚线光标 x(窗口正中,理由见文件头) */
#define WP_INFO_Y 20 /* 顶行(通道/段号/触发 + 削徽标)条带 y */
#define WP_HINT_Y 128 /* 倒数第二行(峰值/键位提示)条带 y */
#define WP_ENG_Y 143 /* 底行(单位徽标)条带 y(页脚已隐藏) */
#define WP_EMPTY_Y 60 /* 空态文案条带 y(近屏中部) */
/* 页面数据态:未取数前不画任何波形元素 */
#define WP_ST_IDLE 0u /* 未取数/无段(空态文案) */
#define WP_ST_OK 1u /* 段完整,可画波形 */
#define WP_ST_BAD 2u /* 段不可读(显式文案,绝不画线) */
/* 私有函数声明区 */
static void wp_load_seg(void);
static void wp_ensure(void);
static void wp_draw_text(lv_layer_t *a_layer, const char *a_text,
int32_t a_x, int32_t a_y, uint8_t a_align,
const lv_area_t *a_clip);
static int32_t wp_map_y(int32_t a_v, int32_t a_dmin, int32_t a_span);
/* 私有变量定义区 */
/*
* 通道名(ASCII 段名 = 语义取数层的通道语义名,页面自带表避免为了
* 六个字母再开一条注入边;与段头通道表的语义顺序一致:三相电压/零序/
* 三相电流在前,形状受限路按段头 conv 位区分——名字本身两端一致)
*/
static const char *const s_ch_names[UI_WAVE_CH_CNT] = {
"UA", "UB", "UC", "UN", "IA", "IB", "IC", "IN"
};
/* 触发源字母(值域 = 段头 trig_src;未识别给 '?' 不给猜) */
static const char s_trig_letters[] = { '?', 'M', 'P', 'T' };
#define WP_TRIG_MAX ((uint8_t)(sizeof(s_trig_letters) - 1u))
static uint8_t s_state; /* WP_ST_* 三态 */
static uint8_t s_idx; /* 当前段号(盘上编号,0..99) */
static uint8_t s_ch; /* 当前通道(上下键切) */
static uint8_t s_eng; /* 段头单位口径(只读徽标) */
static uint8_t s_trig; /* 段头触发源 */
static uint8_t s_clip; /* 段头形状受限位图(位号=通道) */
static int16_t s_codes[UI_WAVE_WIN_PTS]; /* 当前窗码值(执行侧已并偏移) */
static int32_t s_peak_milli; /* 窗口峰值 mV(执行侧算好) */
/* 函数定义区 */
/*
* 函数功能:取当前段的头与窗口(三态落到 s_state,坏态一律显式文案)
* 入口参数:无(读 s_idx / s_ch)
* 返回值:无
* 限定条件:仅在 uiTask 绘制/按键链内调用(经注入表触到执行侧)
* 函数说明:头都读不成时连顶行都不画——通道名以外的字段全来自段头,
* 缺一半字段还硬凑顶行等于把「读不到」伪装成「读到了」
*/
static void wp_load_seg(void) {
int rc = ui_data_wave_seg_head(s_idx, &s_eng, &s_trig, &s_clip);
if (rc != UI_WAVE_OK) {
s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE;
return;
}
rc = ui_data_wave_seg_win(s_idx, s_ch, s_codes, &s_peak_milli);
if (rc != UI_WAVE_OK) {
s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE;
return;
}
s_state = WP_ST_OK;
}
/*
* 函数功能:首帧懒取数(开页链零 I/O 的落地方式:真正的卷读取推迟到
* 第一次绘制——模拟器截图与真机重绘走同一条链,行为一致)
* 入口参数:无
* 返回值:无
* 限定条件:draw_cb 开头调用
* 函数说明:取不到段就停在空态;此后再按左右键仍会重试(手动录波从
* 触发到成段有一秒级延迟,页开着等段出现是回放页的正常用法,
* 不做「试过就没有了」的缓存)
*/
static void wp_ensure(void) {
uint8_t idx = 0u;
int rc;
if (s_state != WP_ST_IDLE) {
return;
}
rc = ui_data_wave_seg_first(&idx);
if (rc == UI_WAVE_OK) {
s_idx = idx;
wp_load_seg();
}
}
/*
* 函数功能:单行文本绘制(对齐可选;ui_page_action 私有拷贝同法,此处
* 多一个左/右对齐是因为顶行要把「削」徽标钉到右上角)
* 入口参数:a_layer - 绘制层;a_text - Flash 字面量;a_x - 锚点 x(对齐
* 含义:左=起点 中=屏宽轴 右=终点);a_y - 条带顶;
* a_align - 0 左 / 1 中 / 2 右;a_clip - 重绘区
* 返回值:无
* 限定条件:a_text 非空;条带与 clip 不相交即返回(culling)
* 函数说明:lv_text_get_width 必须传 strlen(传 0 不测量、传 -1 死循环,
* 框架同款警示);本 LVGL 版条带矩形给 lv_draw_label 做底色无关
* 文本定位用,字模颜色固定 UI_COL_FG
*/
static void wp_draw_text(lv_layer_t *a_layer, const char *a_text,
int32_t a_x, int32_t a_y, uint8_t a_align,
const lv_area_t *a_clip) {
lv_area_t band;
lv_draw_label_dsc_t ld;
int32_t w = (int32_t)lv_text_get_width(a_text, (uint32_t)strlen(a_text),
LV_FONT_DEFAULT, 0);
band.x1 = 0;
band.x2 = UI_SCREEN_W - 1;
band.y1 = a_y;
band.y2 = a_y + UI_TEXT_BAND_H - 1;
if ((band.x1 > a_clip->x2) || (band.x2 < a_clip->x1) ||
(band.y1 > a_clip->y2) || (band.y2 < a_clip->y1)) {
return;
}
lv_draw_label_dsc_init(&ld);
ld.base.layer = a_layer;
ld.text = a_text;
ld.text_static = 1u;
ld.color = UI_COL_FG;
ld.ofs_x = (a_align == 1u) ? (a_x - (w / 2))
: ((a_align == 2u) ? (a_x - w) : a_x);
ld.ofs_y = 0;
lv_draw_label(a_layer, &ld, &band);
}
/*
* 函数功能:顶行触发字母(越界值给 '?' 不猜)
* 入口参数:a_trig - 段头 trig_src 原值
* 返回值:字母 ASCII char
* 限定条件:仅绘制路径
*/
static char wp_trig_letter(uint8_t a_trig) {
return (a_trig <= WP_TRIG_MAX) ? s_trig_letters[a_trig] : '?';
}
/*
* 函数功能:码值域到屏幕 y 的线性映射(0 基线必在域内:域取
* 「min(0, 窗最小) .. max(0, 窗最大)」,削顶钉下轨的通道也能
* 一眼看出贴的是 0 线还是别的什么)
* 入口参数:a_v - 码值;a_dmin - 域底;a_span - 域高(域宽,恒 >0)
* 返回值:波形区内 y 像素(WP_WBOT = 域底,WP_WTOP = 域顶) int32_t
* 限定条件:绘制路径纯算术,零除由调用侧钳 span 保证
* 函数说明:整数乘除即可——域高不超过 65534,先乘后除无溢出风险
*/
static int32_t wp_map_y(int32_t a_v, int32_t a_dmin, int32_t a_span) {
return WP_WBOT - (((a_v - a_dmin) * WP_WSPANS) / a_span);
}
/*
* 函数功能:波形区自绘——阶梯线 + 0 线(虚线)+ 触发点虚线竖光标
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
* 返回值:无
* 限定条件:仅 s_state == WP_ST_OK 时被调用;s_codes 已备
* 函数说明:阶梯 = 每点先画 3 px 水平段、再在点边界补一段垂直连线,
* 全程只有水平与垂直两种线段(1 bpp 抗阈值裁定的直接落地);
* 0 线与光标用 dash(ui_deco 先例只画过实线,dash 字段是 LVGL
* 线段 dsc 自带的,单色层同样按位落墨);整区一次 cull,区内
* 不再逐线判交——波形区就 120x90,逐线判交省不了多少,判错
* 反而留下半截线
*/
static void wp_draw_wave(lv_layer_t *a_layer, const lv_area_t *a_clip) {
lv_area_t band;
lv_draw_line_dsc_t ld;
int32_t vmin = s_codes[0];
int32_t vmax = s_codes[0];
int32_t dmin;
int32_t dmax;
int32_t span;
int32_t i;
int32_t y;
int32_t y_prev = 0;
band.x1 = 0;
band.x2 = UI_SCREEN_W - 1;
band.y1 = WP_WTOP;
band.y2 = WP_WBOT;
if ((band.x1 > a_clip->x2) || (band.x2 < a_clip->x1) ||
(band.y1 > a_clip->y2) || (band.y2 < a_clip->y1)) {
return;
}
for (i = 1; i < (int32_t)UI_WAVE_WIN_PTS; i++) {
int32_t v = s_codes[i];
if (v < vmin) {
vmin = v;
}
if (v > vmax) {
vmax = v;
}
}
dmin = (vmin < 0) ? vmin : 0;
dmax = (vmax > 0) ? vmax : 0;
span = dmax - dmin;
if (span < 1) {
span = 1; /* 全零窗:0 线与波形线重合贴在区底,诚实且无除零 */
}
lv_draw_line_dsc_init(&ld);
ld.base.layer = a_layer;
ld.color = UI_COL_FG;
ld.width = 1;
/* 0 线(虚线):恒在域内,永远画得出来 */
ld.dash_width = 2;
ld.dash_gap = 2;
y = wp_map_y(0, dmin, span);
ld.p1.x = WP_WX0 - 2;
ld.p1.y = y;
ld.p2.x = WP_WX0 + ((int32_t)UI_WAVE_WIN_PTS * WP_PX_PER_PT) + 1;
ld.p2.y = y;
lv_draw_line(a_layer, &ld);
/* 触发点竖光标(虚线):第 二十/廿一 点之间的点边界(见文件头裁定) */
ld.p1.x = WP_CUR_X;
ld.p1.y = WP_WTOP;
ld.p2.x = WP_CUR_X;
ld.p2.y = WP_WBOT;
lv_draw_line(a_layer, &ld);
/* 阶梯线(实线) */
ld.dash_width = 0;
ld.dash_gap = 0;
for (i = 0; i < (int32_t)UI_WAVE_WIN_PTS; i++) {
int32_t x;
x = WP_WX0 + (i * WP_PX_PER_PT);
y = wp_map_y((int32_t)s_codes[i], dmin, span);
ld.p1.x = x;
ld.p1.y = y;
ld.p2.x = x + WP_PX_PER_PT - 1;
ld.p2.y = y;
lv_draw_line(a_layer, &ld);
if ((i > 0) && (y != y_prev)) {
ld.p1.x = x;
ld.p1.y = y_prev;
ld.p2.x = x;
ld.p2.y = y;
lv_draw_line(a_layer, &ld);
}
y_prev = y;
}
}
/*
* 函数功能:单位徽标取词(只读 eng 选词,eng 不进任何算式——§6.3 口径;
* 未识别值按最保守的「引脚域」渲染,不猜高口径)
* 入口参数:a_eng - 段头 eng 原值
* 返回值:徽标文案(Flash 字面量) const char *
* 限定条件:仅绘制路径
*/
static const char *wp_eng_word(uint8_t a_eng) {
if (a_eng == (uint8_t)UI_DATA_ENG_SEC) {
return "二次值";
}
if (a_eng == (uint8_t)UI_DATA_ENG_PRI) {
return "一次值";
}
return "引脚域";
}
/*
* 函数功能:开页复位(框架 init_cb)——通道回到 UA、数据回到未取态
* 入口参数:无
* 返回值:无
* 限定条件:框架开页链内;只清状态零 I/O(懒取数在首帧绘制)
*/
static void pg_init(void) {
s_state = WP_ST_IDLE;
s_ch = 0u;
}
/*
* 函数功能:自绘(框架 draw_cb)
* 入口参数:a_layer / a_clip - 契约见 ui_content_page.h
* 返回值:无
* 限定条件:框架绘制链末尾调用
*/
static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) {
char buf[UI_BUF_STRLEN];
wp_ensure();
if (s_state == WP_ST_BAD) {
wp_draw_text(a_layer, "该段不可读", UI_SCREEN_W / 2, WP_EMPTY_Y, 1u,
a_clip);
return;
}
if (s_state != WP_ST_OK) {
wp_draw_text(a_layer, "无录波段", UI_SCREEN_W / 2, WP_EMPTY_Y, 1u,
a_clip);
return;
}
/* 顶行:左 = 通道 + 段号 + 触发字母;右 = 形状受限「削」徽标 */
(void)lv_snprintf(buf, sizeof(buf), "%s SEG%u %c", s_ch_names[s_ch],
(unsigned int)s_idx, wp_trig_letter(s_trig));
wp_draw_text(a_layer, buf, UI_LIST_LEFT_X, WP_INFO_Y, 0u, a_clip);
if ((s_clip & (uint8_t)(1u << s_ch)) != 0u) {
wp_draw_text(a_layer, "削", UI_SCREEN_W - 1, WP_INFO_Y, 2u, a_clip);
}
wp_draw_wave(a_layer, a_clip);
/* 倒数第二行:左 = 键位提示(ASCII);右 = 窗口峰值 mV(定点,ASCII) */
wp_draw_text(a_layer, "UD=CH LR=SEG", UI_LIST_LEFT_X, WP_HINT_Y, 0u,
a_clip);
(void)lv_snprintf(buf, sizeof(buf), "PK%dmV", (int)s_peak_milli);
wp_draw_text(a_layer, buf, UI_SCREEN_W - 1, WP_HINT_Y, 2u, a_clip);
/* 底行:单位徽标(eng) */
wp_draw_text(a_layer, wp_eng_word(s_eng), UI_SCREEN_W / 2, WP_ENG_Y, 1u,
a_clip);
}
/*
* 函数功能:按键处理(框架 key_cb)——上下切通道、左右切段
* 入口参数:a_key - LVGL 键值 lv_key_t
* 返回值:true = 本页已消费;false = 交回框架 bool
* 限定条件:框架按键链内(ESC 不到这里;LEFT 因 key_intercept=1 先到这里)
* 函数说明:环游语义 = 上下在八路间绕回、左右在现存段间绕回;切段失败
* (卷忙/无邻段)保持当前画面不动——「按了没动」比「画面被
* 忙态清成空」诚实,忙不忙在回放页本来就不是用户要的答案
*/
static bool pg_key(lv_key_t a_key) {
uint8_t next;
int rc;
switch (a_key) {
case LV_KEY_UP:
case LV_KEY_DOWN:
next = (a_key == LV_KEY_UP)
? (uint8_t)((s_ch + UI_WAVE_CH_CNT - 1u) % UI_WAVE_CH_CNT)
: (uint8_t)((s_ch + 1u) % UI_WAVE_CH_CNT);
s_ch = next;
if (s_state == WP_ST_OK) {
rc = ui_data_wave_seg_win(s_idx, s_ch, s_codes, &s_peak_milli);
if (rc != UI_WAVE_OK) {
s_state = (rc == UI_WAVE_BAD) ? WP_ST_BAD : WP_ST_IDLE;
}
}
/* 状态已变必须显式失效:框架只在 key_cb 返回 false 时自己 invalidate,
* 本页接管了键(true),不失效就永远画旧画面(2026-11-02 真机实证:
* 键送达、状态已切、屏上纹丝不动——该病随枚举修复首次上板才暴露) */
ui_content_page_invalidate();
return true;
case LV_KEY_LEFT:
case LV_KEY_RIGHT:
if (s_state == WP_ST_IDLE) {
wp_ensure(); /* 空态下按左右 = 再探一次盘(新段可能已落) */
ui_content_page_invalidate();
return true;
}
rc = (a_key == LV_KEY_RIGHT) ? ui_data_wave_seg_next(s_idx, &next)
: ui_data_wave_seg_prev(s_idx, &next);
if (rc == UI_WAVE_OK) {
s_idx = next;
s_state = WP_ST_IDLE; /* 先回空态再载,坏段不会留着旧画面 */
wp_load_seg();
ui_content_page_invalidate(); /* 理由同上:切段成功要重绘 */
}
return true;
default:
return false;
}
}
/*
* 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=wave_play 绑定引用)
* 查看页形态:无浅 V、无页脚(段号在顶行,页脚「共 N 页」与段号两套计数
* 反而打架)、不可选(单元素自绘页)、可编辑位 0(本页零写动作);
* key_intercept=1 是本页存在的理由级配置:LEFT 要拿来切上一段,不能让
* 框架默认的「LEFT 关页」吃掉
*/
const content_page_def_t ui_page_wave_play_desc = {
.id = "wave_play",
.title = "录波回放",
.hide_deco = 1u,
.hide_footer = 1u,
.selectable = 0u,
.key_intercept = 1u,
.edit_capable = 0u,
.init_cb = pg_init,
.draw_cb = pg_draw,
.key_cb = pg_key,
};