Serial Studio 频谱频率标记(FFT Frequency Markers)设计实现指南:从数据模型到监控渲染的完整链路
【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio
导读:本文基于 Serial Studio 仓库中的设计规格 spec.md、plan.md 与 tasks.md 编写,完整还原了"FFT 频率标记与报警频带"从动机、需求、数据模型、编辑器、API、渲染到测试验证的全过程。读完本文,你将掌握:如何在 Serial Studio 中为任意启用 FFT 的数据集添加带标签的频率标记与频带、设置警告/报警阈值并让频谱控件自动监控实时峰值,以及这套能力如何通过项目 JSON 持久化并通过远程 API 读写。文中涉及的源码均可在仓库对应路径下查阅。
一、背景与动机:为频谱中的"已知频率"建立工程语义
振动与声学分析的核心对象往往是已知频率:800 Hz 的齿轮啮合线(gear-mesh line)、1× 转速的不平衡线、轴承缺陷频带、需要避开的共振点。在引入本功能之前,Serial Studio 的 FFT 控件只能绘制整条频谱,对每个赫兹一视同仁——用户必须肉眼定位自己关心的频率,记住"800 附近那个鼓包"的含义,再凭目测判断其电平是否超标。既没有方法为工程上有意义的频率打标签,也无法一眼看出被监控的谱线是否越过了警告或报警电平,更没有任何东西持久化到项目文件中来承载这些领域知识(见 spec.md 的 Problem/Motivation 一节)。
而数据集在幅度域早已解决了等价问题:报警频带(alarm bands,带严重级别、颜色和标签的彩色 min/max 区域)可在 Project Editor 专用对话框中编辑、持久化到项目文件,并驱动 Bar/Gauge/Meter/LED 等控件的可视化。频率域理应获得同样的待遇:在频谱上提供命名标记与频带(风格类似 DAW 均衡器与频谱分析仪——带柔和渐变的半透明彩色区域、带标签的频率线),并附带每个标记的电平阈值,越限即点亮。这正是本规格(Spec 0019)的出发点。
二、目标与非目标:明确边界
2.1 目标(Goals)
规格明确了八个可验收的目标(spec.md 的 Goals 一节):
- 标记编辑:用户可在 Project Editor 中为任意启用 FFT 的数据集挂载频率标记列表——每个标记包含频率(或频带)、标签、颜色,以及可选的警告/报警幅度阈值(dB)。
- FFT 渲染:FFT 控件在线性与对数频率轴、任意主题下都能美观清晰地渲染这些标记:点标记为带标签的竖线,频带为 DAW-EQ 风格的半透明渐变区域。
- 自动监控:每个显示 tick 自动测量标记频率邻域内的实时峰值电平,当电平越过配置阈值时标记视觉状态升级(正常 → 警告 → 报警),并附带实时 dB 读数。
- 持久化:标记列表持久化在项目文件中,未触碰的项目保存/加载保持字节级一致,且可完全通过远程 API 读写。
- Waterfall 一致渲染:Waterfall(瀑布图/声谱图)控件在同一频率轴上渲染相同标记,保证两个频谱视图一致。
2.2 非目标(Non-Goals)
- 不做全局报警系统集成:标记报警是控件局部的视觉,不接入应用级报警监视器、MQTT 或导出(FFT 仅在控件内部可见时才计算,无头谱报警引擎是另一个更大的功能)。
- 不做自动峰值检测/跟踪:标记停留在用户配置的频率上,不找峰、不跟踪转速、不移动标记。
- 不做谐波/边带光标(1×/2×/3× 族):用户可为每个谐波单独添加标记,自动生成族是未来工作。
- 不做逐标记历史或日志:读数仅实时。
- 不引入新依赖、不引入新文件格式:标记搭乘现有项目 JSON schema(增量式,同 0014/0017 纪律)。
三、需求与验收标准(Requirements & Acceptance Criteria)
规格提出 8 条需求(R1–R8),每条都有明确的验收标准(AC1–AC7):
| 需求 | 内容 | 验收标准(AC) |
|---|---|---|
| R1 Schema | FFT 数据集可携带零个或多个频率标记:起始频率 Hz(必填)、可选结束频率 Hz(缺省或相等=点标记,否则为频带)、标签、颜色(hex,空则从主题自动取)、可选警告 dB、可选报警 dB | AC1:pytest 往返——带标记(点+频带,含/不含阈值)的项目重新加载一致;无标记项目与旧构建保存的字节级一致 |
| R2 Editor | Project Editor 提供与 Alarm Bands 编辑器外观/布局/交互语法一致的"Frequency Markers"编辑器 | AC5:维护者人工验证——与 Alarm Bands 观感一致、校验阻止越界频率、Cancel 丢弃/Apply 持久化且控件实时更新 |
| R3 FFT 渲染 | 点标记为竖线+标签芯片;频带为半透明垂直区域+柔和水平渐变;线性/对数轴、缩放平移、明暗主题均正确,标记不遮挡频谱曲线(区域在曲线之下,标签在上) | AC3:维护者在实时 FFT(如音频输入)上验证 |
| R4 监控 | 每个显示 tick 计算标记带内的峰值显示电平 dB;越限则升级视觉状态并显示实时峰值 dB;无阈值只显示读数 | AC4:阈值低于当前峰值时升级、调回后降级 |
| R5 工具栏开关 | FFT 控件工具栏增加"Show Frequency Markers"开关(按控件持久化) | AC3/AC4 覆盖 |
| R6 API | 标记列表通过远程 API 往返:既走通用 dataset-update 路径,也走专用原子 get/set 命令;非法输入被拒绝/钳制并告警 | AC2:pytest API 往返 + 非法负载拒绝/钳制 |
| R7 Waterfall 渲染 | 在声谱图上绘制相同标记(竖线/半透明频带+标签芯片),缩放/平移/色图正确;升级视觉为可选打磨 | AC6:维护者人工验证 |
| R8 零热路径影响 | 所有计算都在控件显示层以 UI 刷新率进行,帧摄入/解析路径零改动 | AC7:--benchmark-hotpath门禁不变 |
3.1 关键设计决策(Decisions)
规格在 2026-07-17 以委派授权方式记录了几条决策(spec.md 的 Decisions 一节):
- 标记存放在数据集的项目 JSON 中(同 alarm bands),而非 widgetSettings:它们是属于项目的工程配置、必须可通过 API 寻址,而不是逐控件的外观状态。
- 阈值采用显示 dB 值(FFT 控件的原生 Y 单位),与用户从图上读到的数值一致;不做线性单位阈值。
- 点标记监控窗口是小的固定邻域(几个 bin),轻微失谐的谱线仍能触发;确切宽度是 plan 阶段的常量(±2 bin)。
- Waterfall 只保证渲染(+便宜的读数),不强制升级视觉——其 C++ 绘制的轴层使每 tick 重标签开销过大;超越线/带/标签的部分是打磨而非契约。
四、数据模型与持久化(Data Model & Persistence)
4.1 FrequencyMarker 结构体
计划(plan.md)与已落地的源码 Frame.h 完全对应。DataModel::FrequencyMarker定义如下:
struct alignas(8) FrequencyMarker { double frequency = 0; ///< Marker frequency in Hz (band start when endFrequency is set) double endFrequency = 0; ///< Band end in Hz; <= frequency means a point marker double warningDb = std::numeric_limits<double>::quiet_NaN(); ///< Warning level (NaN = unset) double alarmDb = std::numeric_limits<double>::quiet_NaN(); ///< Alarm level (NaN = unset) QString color; ///< Optional hex override; empty -> automatic (theme palette) QString label; ///< Optional human label shown on the marker chip }; static_assert(sizeof(FrequencyMarker) % alignof(FrequencyMarker) == 0, "Unaligned FrequencyMarker struct");要点:endFrequency <= frequency即视为点标记;阈值默认为 NaN(= 未设置);alignas(8)与 static_assert 维持对齐纪律(与AlarmBand一致,见 Frame.h)。
4.2 Dataset 扩展与 Keys
数据集新增std::vector<FrequencyMarker> fftMarkers;成员(Frame.h),紧邻既有的alarmBands。新的Keys::常量(单一事实来源):FFTMarkers("fftMarkers")、Frequency("freq")、EndFrequency("endFreq")、WarningDb("warningDb")、AlarmDb("alarmDb"),并复用Keys::Label/Keys::Color。
一个标记的 JSON 形态:
{"freq": 800, "endFreq": 850, "label": "Gear mesh", "color": "#ff5722", "warningDb": -40, "alarmDb": -25}除freq外所有字段可选;写回时省略可选字段(NaN 阈值、空字符串、endFreq <= freq永不序列化)。序列化契约与AlarmBand相同:数组仅在非空时写出,保证未触碰的项目字节级一致;读取时校验有限且freq > 0,endFreq仅在> freq时保留,垃圾输入返回 false 并丢弃条目。
4.3 无 schema 版本升级
本特性为纯增量键:旧构建读取时忽略未知键(与 0014/0017 相同策略);无需遗留别名;无 Sessions DB 影响。
五、编辑器:FrequencyMarkersEditor 对话框
5.1 架构复用 Alarm Bands 模式
编辑器完全克隆AlarmBandsEditor的语法(plan.md 的 Editor 一节)。完整链路为:
- DatasetView 功能区的"Freq. Markers" 按钮(Behavior 区,当所选数据集设置了
DatasetFFT || DatasetWaterfall时启用,图标复用fft.svg)。 - 触发
ProjectEditor::openFrequencyMarkersEditorForSelection()(ProjectEditorForms.cpp):从m_selectedDataset.fftMarkers构建 QVariantList,Nyquist =fftSamplingRate * 0.5(rate <= 0 时有回退保护)。 - 发出
openFrequencyMarkersEditor(groupId, datasetId, nyquist, markers)信号 → DatasetView 的Connections显示懒加载对话框。 - 点击 Apply 调用
Cpp_JSON_ProjectEditor.commitFrequencyMarkers(list)(ProjectEditorCommit.cpp,镜像commitAlarmBands)→ 重建m_selectedDataset.fftMarkers、pm.updateDataset(...)、buildDatasetModel(...)。 - 仪表盘实时拾取沿用既有的 modified→autosave→
syncRuntime()路径(与 alarm bands 相同;控件在仪表盘重新配置时重新实例化)。
5.2 对话框形态
FrequencyMarkersEditor.qml(新建,基于Widgets.SmartDialog)包含:
- Presets 卡片:提供几个诚实可用的静态预设——50 Hz / 60 Hz 市电哼声(含 2 次谐波的点标记)。("1×/2×/3× 转轴阶次"按基准频率动态生成脚手架被否决,预设保持静态。)
- 标记表格:Start Hz、End Hz(留空 = 点标记)、Label、Color 色板(ColorDialog + 右键重置)、Warn dB、Alarm dB、上移/下移、删除。
- 实时预览条:0…Nyquist 刻度上的标记总览(频带为半透明区域、点为刻度)。
- 底部 Cancel / Apply IconButtons。
- 内联校验:编辑时 Hz 钳制到 0…Nyquist;收集时归一化
warn <= alarm;空/非法数字视为未设置。
多选行为与 Alarm Bands 完全一致:commitFrequencyMarkers仅作用于m_selectedDataset(单选语义,按钮使能跟随 Alarm Bands 的同一规则)。
六、FFT 控件:模型层监控与 QML 渲染
6.1 C++ 模型:零分配监控
FFTPlot.h 中可见完整的 QML 接口面:
Q_PROPERTY(QVariantList markers READ markers ...) Q_INVOKABLE double markerPeakDb(int index) const; // 实时峰值 dB Q_INVOKABLE int markerState(int index) const; // 0 正常 / 1 警告 / 2 报警 // signal: markerValuesChanged()内部有struct MarkerRuntime(解析后的 bin 窗口、阈值、实时读数)与预分配的m_markerRt向量。流程为:
- 构造时:复制数据集的标记,钳制到 0…Nyquist,将每个标记解析为
[binLo, binHi]——点标记取 ±2 bin 邻域(轻微失谐的线也能触发),钳制到频谱范围。rebuildFftPlan()中重新解析,因为 bin 宽度随 FFT 尺寸变化。 - 每个 UI tick(
updateData()在computeBinSpectrum之后):对每个标记在m_binDb[binLo..binHi]上取峰值 → 存储峰值与状态(经 NaN 感知的阈值比较:0 正常 / 1 警告 / 2 报警)→ 发出markerValuesChanged()。 - 稳态零分配:标记/bin/状态向量在构造或
rebuildFftPlan时定容;per-tick 循环只做浮点比较;markersQVariantList 在构造时构建一次(配置,而非 per-tick 数据)。QML 通过markerPeakDb(i)/markerState(i)(double/int 返回)按需读取——不产生 per-tick 容器。
阈值语义:采用显示 dB(ballistics 处理后),即"所见即所判"(WYSIWYG)——原始 dB 会针对用户看不到的峰值报警。
6.2 QML 叠加层:DAW-EQ 风格渲染
FFTPlot.qml 中的渲染设计:
- 频带 delegate:
Rectangle+ 水平Gradient(边缘 0.04 → 核心 0.22 的标记色不透明度)+ 1px 边缘描边。 - 点 delegate:2px 竖线 + 背后 8px 柔和发光矩形。
- 芯片:胶囊形
Label(标记色背景 0.9 不透明度、widget_base文字,复用 PlotWidget 的光标标签惯例),文本格式label · −42.1 dB;警告状态用Cpp_ThemeManager.alarmColorForSeverity(2)重染色/线,报警用严重级 3 + 不透明度闪烁(SequentialAnimation)。 - 分层:频带放在裁剪过的
Item中、父级为plot.curveLayer、z: -1(在PlotCurve描边之下渲染);标签芯片在第二层叠加(曲线之上,复用 PlotWidget 中_triggerLine的模式)。 - 坐标映射:Hz→x 用控件自身的 world→pixel 数学(
logX ? log10(freq) : freq,基于plot.xVisibleMin/xVisibleRange,与PlotCurve完全相同的变换),因此缩放/平移自动跟随。 - 芯片防重叠:顶部对齐,水平重叠时后到的芯片自动换行(JS 对少量标记做简单贪心行分配)。
- 工具栏开关:
labels.svg图标,经saveWidgetSetting(widgetId, "showFrequencyMarkers", ...)持久化,默认开启。 - 主题反应式:QML 绑定
Cpp_ThemeManager.colors自动适配明暗主题。 - 翻译纪律:芯片文本只用编号占位符(
%1/%2),规避%n/.arg()翻译陷阱(common-mistakes 要求)。
七、Waterfall:C++ 绘制的标记渲染
Waterfall(Pro 功能,QQuickPaintedItem)用 C++ 的paint()绘制相同标记(plan.md 的 Waterfall 一节):
- 构造时复制标记;
updateData()从刚写入的m_smoothed行计算每个标记的峰值/状态(少量比较)。 paint()绘制频带填充/标记线/标签芯片,在轴层合成之后,用与drawXAxis完全一致的xMinHz/xMaxHz缩放平移数学做 Hz→x 映射(在 Waterfall.cpp 内提取一个共享小助手,保证两个绘制路径不会漂移)。- 关键决策:标记不放进缓存的轴层——升级色调会随行变化,而轴缓存只在缩放/主题变化时重建,会过期;每帧多画几根线相比图像 blit 的开销微不足道。
- 新增
markersVisibleQ_PROPERTY(默认 true),QML 工具栏开关以同一showFrequencyMarkers设置名持久化。 - 配色刷新通过既有
onThemeChanged实现。
八、API / SDK 面:原子 get/set + 增量更新键
远程 API 面完整镜像 alarm-bands 模式(plan.md 的 API 一节):
applyDatasetNumericFields新增fftMarkers分支:数组经DataModel::read(FrequencyMarker&, ...)解析,非法条目丢弃(镜像 alarmBands;takeParam保持未知字段告警诚实)。- 新命令
project.dataset.getFFTMarkers/project.dataset.setFFTMarkers:原子数组读/写,set 时返回droppedInvalid计数,注册在 ProjectHandler.cpp 中 alarm-bands 对旁边。 - 免费特性:无
BUILD_COMMERCIAL守卫——schema 与 FFT 控件为 GPL,仅 Waterfall渲染器属于 Pro 且已在控件层门控。 - SDK 再生成:由
scripts/sanitize-commit.py(generate-sdk 步骤)在结束时执行。
九、热路径与线程影响(Hotpath & Threading)
- 不触碰热路径:
FrameReader/CircularBuffer/FrameBuilder/ Dashboard 摄入或 span lane 零改动;所有新计算都运行在控件模型的 GUI 线程、UI tick 节奏,位于既有fftData()环读的下游。--benchmark-hotpath预期字节级不变(AC7)。 - 无新跨线程信号/槽:
markerValuesChanged同线程(控件 → QML);编辑器/API 路径与今天一样在主线程。 - 无新缓存热路径标志输入。
- 时间戳所有权不变——该特性永不接触帧。
- 稳态分配:如上所述,per-tick 循环仅比较浮点。
十、权衡与备选方案(Tradeoffs & Alternatives)
计划文档以决策表形式记录了关键取舍(plan.md 的 Tradeoffs 一节):
| 决策点 | 选项 | 选择与理由 |
|---|---|---|
| 标记存放位置 | 数据集 JSON vs widgetSettings vs 新顶层 map | 数据集 JSON(fftMarkers),同alarmBands——工程配置、API 可寻址、天然按数据集 |
| 监控主体 | 控件局部 vs AlarmMonitor 集中 | 控件局部——FFT 仅存在于可见控件内;集中式 = 无头 FFT 引擎(规格非目标) |
| FFT 渲染方式 | QML 叠加 vs C++ 绘制层 vs QtGraphs 系列 | QML 叠加进curveLayer——主题/缩放/平移免费,匹配 PlotCurve 分层,无场景图工作 |
| Waterfall 渲染 | 缓存轴层 vs 逐帧绘制 | 逐帧——升级色调逐行变化;轴缓存仅在缩放/主题时重建,会过期 |
| 阈值语义 | 显示 dB(ballistics 后)vs 原始 dB | 显示 dB——与图所见即所得;原始 dB 会对用户看不到的峰值报警 |
| 实时状态到 QML | per-tick QVariantList 属性 vs 信号+可调用轮询 | 可调用 +markerValuesChanged——无 per-tick 容器抖动 |
| 点标记窗口 | 精确 bin vs ±2 bin | ±2 bin——轻微失谐的线仍能触发;常量且在 @brief 中说明 |
| 编辑器入口 | 表单模型行 vs 专用对话框 | 专用对话框——结构体列表不适合行语法;Alarm Bands 已开创先例 |
十一、风险与缓解(Risks & Mitigations)
- 构造闭包/启动:无风险——不触碰任何 ProjectModel 构造可达代码;编辑器启动仅在用户点击时运行。
- 字节级序列化回归(0014/0017 纪律):
FFTMarkers仅非空时写出;可选子键省略;由新 pytest 往返覆盖(保存未触碰项目并 diff)。 %n/.arg()翻译陷阱:芯片文本只用编号占位符。- QML 叠加在缩放下:映射使用
xVisibleMin/xVisibleRange(与 PlotCurve/光标相同)——已对照既有光标数学验证;裁剪到图层,平移出界标记不会渗出。 - Waterfall Hz 映射漂移:提取一个本地助手供
drawXAxis与标记通道共用,两者不可能不一致。 - 多选提交:
commitFrequencyMarkers仅作用于m_selectedDataset,与commitAlarmBands相同(单选;功能区按钮沿用 Alarm Bands 的使能规则,多选行为一致)。 - Apply 后仪表盘陈旧:依赖与 alarm bands 相同的 updateDataset→autosave→
syncRuntime路径;AC5 验证实时拾取,若 FFT 控件需要轻推,修复是既有 reconfigure 信号而非新路径。 - NaN 处理:阈值比较前用
std::isnan守卫;JSON 写入器省略 NaN(绝不输出null)。
十二、测试与验证计划
12.1 自动化测试(AC1/AC2)
新增 test_fft_markers.py(维护者运行,需应用 + API 服务器启动):
- 经
project.dataset.update与setFFTMarkers设置标记;getFFTMarkers回显一致。 - 保存/加载保持完整。
- 非法负载(负频率、垃圾类型、反序频带、warn > alarm)→ 以
droppedInvalid丢弃/归一化。 - 无标记项目 → 保存的 JSON 不含
fftMarkers键。
12.2 维护者人工验证(AC3–AC6)
- AC3/AC4:音频输入实时 FFT 上,800 Hz 点标记与带标签频带在两种轴模式下按要求渲染、跟随缩放/平移、明暗主题下观感自然、芯片显示实时 dB;阈值低于当前峰值时升级(警告/报警视觉可区分),调回后降级。
- AC5:编辑器与 Alarm Bands 并排对照观感一致;校验阻止越界频率;Cancel 丢弃、Apply 持久化且控件无需重启实时更新。
- AC6:相同标记以正确频率出现在 Waterfall 上,缩放/平移下正确。
- AC7:
--benchmark-hotpath门禁不变(无摄入路径改动),CI 确认。
12.3 静态验证
- 每个触及文件过
python scripts/code-verify.py --fix/--check。 - C++ diff 过
qt-cpp-review(6 个 agent),已确认修复的问题包括:双域 bin 钳制、spectrumSize==0守卫、freq 上限、芯片行加固、hasThresholds接线。 - 结束时运行
python scripts/sanitize-commit.py(仅 sanitize,不提交)。
十三、任务分解与实施顺序(Tasks Overview)
实施按 9 个任务自上而下进行(tasks.md),依赖链清晰:
| 任务 | 内容 | 依赖 |
|---|---|---|
| T1 | Schema:FrequencyMarker结构 + Keys + serialize/read | 无 |
| T2 | 编辑器 C++:启动信号 + 提交槽 | T1 |
| T3 | API:dataset-update 键 + 原子 get/set 命令 | T1 |
| T4 | FFT 控件模型:标记配置、监控、QML 面 | T1 |
| T5 | FFT QML:叠加渲染 + 工具栏开关 | T4 |
| T6 | 编辑器 QML:FrequencyMarkersEditor 对话框 + DatasetView 接线 | T2 |
| T7 | Waterfall:标记渲染 + 逐行状态 + 开关 | T1 |
| T8 | pytest:持久化 + API 往返 | T3 |
| T9 | 文档:architecture 笔记 + 规格收尾 | T1–T8 |
规格状态已标记为done(2026-07-17 实现;AC1/AC2 测试已写,AC3–AC7 运行时检查等待维护者执行)。
十四、相关代码位置速查
- 数据模型:Frame.h(
FrequencyMarker)、Frame.h(Dataset::fftMarkers)、序列化/反序列化声明见 Frame.h - FFT 控件模型接口:FFTPlot.h(
markers属性、markerPeakDb/markerState可调用、MarkerRuntime、m_markerRt) - FFT QML 渲染:FFTPlot.qml
- 编辑器 QML(新):
app/qml/ProjectEditor/Dialogs/FrequencyMarkersEditor.qml - 功能区入口:DatasetView.qml
- 编辑器 C++:ProjectEditorForms.cpp、ProjectEditorCommit.cpp
- API 注册:ProjectHandler.cpp、ProjectHandlerEntities.cpp
- 集成测试:test_fft_markers.py
- 规格文档:spec.md、plan.md、tasks.md
结语
Spec 0019 为 Serial Studio 的频谱域补齐了与幅度域 alarm bands 对称的"工程语义"能力:命名频率标记与 DAW-EQ 风格频带、显示 dB 阈值的自动监控与视觉升级、项目文件持久化、远程 API 原子读写,以及 FFT 与 Waterfall 两个频谱视图的一致渲染——全程零热路径影响、零稳态分配、纯增量 schema。对于振动监测、声学分析、旋转机械故障诊断等典型应用,这使"盯住已知频率"从肉眼劳动变成可配置、可持久化、可编程监控的工程流程。读者可以顺着上文给出的源码路径,从 Frame.h 的数据结构一路读到 FFTPlot.h 的运行时监控与 FFTPlot.qml 的渲染层,完整复现这条从需求到实现的链路。
【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考