/* * 模块名称: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 侧) */ /* 头文件包含区 */ #include "ui_page_wave_play.h" #include #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; } } return true; case LV_KEY_LEFT: case LV_KEY_RIGHT: if (s_state == WP_ST_IDLE) { wp_ensure(); /* 空态下按左右 = 再探一次盘(新段可能已落) */ 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(); } 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, };