news 2026/9/17 2:17:40

Serial Studio 频谱频率标记(FFT Frequency Markers):基于 spec 0019 的谱线标注、报警频带与监视体系实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Serial Studio 频谱频率标记(FFT Frequency Markers):基于 spec 0019 的谱线标注、报警频带与监视体系实战指南

Serial Studio 频谱频率标记(FFT Frequency Markers):基于 spec 0019 的谱线标注、报警频带与监视体系实战指南

【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio

本指南围绕 Serial Studio 的doc/claude/specs/0019-fft-freq-markers规范(spec / plan / tasks 三阶段文档)展开:如何在 FFT 频谱与 Waterfall 频谱图上为振动、声学等工程场景添加带名称、颜色和 dB 阈值的频率标记与报警频带,并通过项目编辑器与远程 API 持久化管理。读完本文,你将掌握标记的数据模型与序列化规则、编辑器对话框的完整操作、FFT/Waterfall 双视图的渲染与实时监视原理,以及 API 读写与校验测试的实战用法。

1. 背景:为什么频谱需要一个"看名单"(Spectral Watchlist)

振动与声学工程关心的是已知频率:齿轮啮合线(如 800 Hz)、1 倍转频(1x-RPM)的不平衡线、轴承缺陷频带、需要避开的共振点。在引入本特性之前,Serial Studio 的 FFT 控件只绘制整条频谱曲线,把每一个赫兹等同对待——用户需要靠肉眼去定位"800 附近的那个鼓包",记住它代表什么,再凭经验判断其电平是否合格。这带来三个痛点:

  1. 无法给有工程意义的频率打上名称标签;
  2. 无法一眼看出被监视的谱线是否越过了告警或报警电平;
  3. 项目文件中不保存任何此类领域知识,换台机器、换个项目就要重来。

有趣的是,数据集在幅度域早已解决了等价问题:报警频带(alarm bands,带严重级别、颜色和标签的彩色 min/max 区域)可以在 Project Editor 的专用对话框中编辑、持久化到项目文件,并驱动 Bar/Gauge/Meter/LED 等控件。spec 0019 的目标正是让频率域获得同等对待——参照 DAW 均衡器和频谱分析仪的交互范式(带柔和渐变的半透明彩色区域、带标签的频率线),再加上超出阈值即点亮的逐标记电平阈值。

该规范由 4 个阶段文档构成:doc/claude/specs/0019-fft-freq-markers/spec.md(WHAT/WHY)、plan.md(HOW)、tasks.md(有序清单)以及收尾的架构笔记doc/claude/architecture/dashboard.md。全部 9 个任务(T1–T9)已在 2026-07-17 完成实现并标记为 done。

2. 数据模型与持久化:FrequencyMarker结构与 JSON 序列化(T1)

2.1 结构定义

标记挂在每个开启 FFT 的数据集上。核心结构定义在 core/Core/DataModel/Frame.h:

/** * @brief Labeled frequency marker (point or band) attached to an FFT-enabled Dataset. */ 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");

要点:

  • 点标记 vs 频带endFrequency <= frequency(或未设置)即点标记,否则为频带;
  • 阈值为 NaN 语义warningDb/alarmDb默认是quiet_NaN(),表示"未设置"。NaN 永远不会被序列化,也不会参与比较;
  • 对齐保证alignas(8)+static_assert保证结构与数据集的 8 字节对齐约定一致,与旁边的AlarmBand结构(Frame.h)保持同样的纪律。

数据集侧在 Frame.h 新增成员std::vector<FrequencyMarker> fftMarkers;(空 = 无标记),紧邻已有的std::vector<AlarmBand> alarmBands;。注意数据集本身还携带 FFT 相关配置:fftSamples(默认 256)、fftSamplingRate(默认 100)、fftBallisticsRelease(默认 300 ms 显示衰减)、fftWindow(默认 5 = Blackman-Harris)、fftLogX(对数频率轴开关)等,这些决定了标记的监视范围与显示方式。

2.2 JSON 键(Keys)

新增键定义在 core/Core/DataModel/FrameKeys.h:

inline constexpr KeyView FFTMarkers("fftMarkers"); inline constexpr KeyView EndFrequency("endFreq"); inline constexpr KeyView WarningDb("warningDb"); inline constexpr KeyView AlarmDb("alarmDb");

其中FrequencyLabelColor复用既有键。一个典型标记的 JSON 形态(来自 plan.md):

{"freq": 800, "endFreq": 850, "label": "Gear mesh", "color": "#ff5722", "warningDb": -40, "alarmDb": -25}

freq外所有字段都可选;可选字段在写出时被省略(NaN 阈值、空字符串、endFreq <= freq永不序列化)。

2.3 序列化规则(写路径)

serialize(const FrequencyMarker&)是内联实现,见 core/Core/DataModel/Frame.h:

[[nodiscard]] inline QJsonObject serialize(const FrequencyMarker& m) { QJsonObject obj; obj.insert(Keys::Frequency, m.frequency); if (m.endFrequency > m.frequency) obj.insert(Keys::EndFrequency, m.endFrequency); if (!std::isnan(m.warningDb)) obj.insert(Keys::WarningDb, m.warningDb); if (!std::isnan(m.alarmDb)) obj.insert(Keys::AlarmDb, m.alarmDb); if (!m.color.isEmpty()) obj.insert(Keys::Color, m.color); if (!m.label.isEmpty()) obj.insert(Keys::Label, m.label); return obj; }

关键的加法式模式纪律(additive-schema discipline,沿袭 spec 0014/0017):

  • fftMarkers数组仅在非空时写入数据集 JSON——未加标记的旧项目保存后与旧版本逐字节一致(T8 的 pytest 专门验证"无标记项目不含fftMarkers键");
  • 可选子键(endFreq、warningDb、alarmDb、color、label)一律省略而非写null或空串;
  • 无需 schema 版本升级:旧构建读到未知键会直接忽略,老客户端不受影响。

2.4 反序列化与校验(读路径)

DataModel::read(FrequencyMarker&, const QJsonObject&)实现在 core/Core/DataModel/Frame.cpp,校验规则非常明确:

bool DataModel::read(FrequencyMarker& m, const QJsonObject& obj) { constexpr double nan = std::numeric_limits<double>::quiet_NaN(); constexpr double max_freqHz = 2147483648.0; if (obj.isEmpty()) return false; m.frequency = SerialStudio::toDouble(ss_jsr(obj, Keys::Frequency, 0)); m.endFrequency = SerialStudio::toDouble(ss_jsr(obj, Keys::EndFrequency, 0)); m.warningDb = obj.contains(Keys::WarningDb) ? SerialStudio::toDouble(obj.value(Keys::WarningDb)) : nan; m.alarmDb = obj.contains(Keys::AlarmDb) ? SerialStudio::toDouble(obj.value(Keys::AlarmDb)) : nan; m.color = ss_jsr(obj, Keys::Color, "").toString().simplified(); m.label = ss_jsr(obj, Keys::Label, "").toString().simplified(); if (!std::isfinite(m.frequency) || m.frequency <= 0.0 || m.frequency > max_freqHz) return false; if (!std::isfinite(m.endFrequency) || m.endFrequency <= m.frequency) m.endFrequency = 0.0; else m.endFrequency = qMin(m.endFrequency, max_freqHz); if (std::isfinite(m.warningDb) && std::isfinite(m.alarmDb) && m.warningDb > m.alarmDb) std::swap(m.warningDb, m.alarmDb); if (!PropertyHooks::isValidColor(m.color)) m.color.clear(); return true; }

四层校验逻辑:

校验项规则违规处理
频率合法性有限数、> 0、且 ≤max_freqHz(int 采样率下最大可能 Nyquist)返回 false,条目被丢弃
反向频带endFrequency <= frequency降级为点标记(endFreq 清零)
阈值倒置warningDb > alarmDb交换两者(normalize 而非丢弃)
颜色合法性PropertyHooks::isValidColor非法颜色清空(回退主题自动配色)

批量入口readDatasetFrequencyMarkers(Frame.cpp)清空后逐条解析,无效条目静默丢弃,行为与AlarmBand契约一致,在read(Dataset&)中于readDatasetAlarmBands之后调用。

3. Project Editor:FrequencyMarkersEditor 对话框与提交管线(T2、T6)

3.1 启动信号与数据装配

编辑器侧的 C++ 装配实现在 core/Ui/ProjectEditor/EditorForms.cpp。点击 DatasetView 的 "Freq. Markers" 功能区按钮后:

void EditorForms::openFrequencyMarkersEditorForSelection() { const double nyquist = qMax(1, m_editor.m_selectedDataset.fftSamplingRate) * 0.5; QVariantList markers; markers.reserve(static_cast<int>(m_editor.m_selectedDataset.fftMarkers.size())); for (const auto& m : m_editor.m_selectedDataset.fftMarkers) { QVariantMap entry; entry.insert(QStringLiteral("freq"), m.frequency); entry.insert(QStringLiteral("endFreq"), m.endFrequency); entry.insert(QStringLiteral("label"), m.label); entry.insert(QStringLiteral("color"), m.color); entry.insert(QStringLiteral("warningDb"), std::isfinite(m.warningDb) ? QVariant(m.warningDb) : QVariant()); entry.insert(QStringLiteral("alarmDb"), std::isfinite(m.alarmDb) ? QVariant(m.alarmDb) : QVariant()); markers.append(entry); } Q_EMIT m_editor.openFrequencyMarkersEditor( m_editor.m_selectedDataset.groupId, m_editor.m_selectedDataset.datasetId, nyquist, markers); }

关键点:

  • 可编辑范围 = 0…Nyquist,其中Nyquist = fftSamplingRate * 0.5qMax(1, …)兜底采样率 ≤ 0 的异常情况);
  • 未设置的阈值(NaN)在 QVariantList 里以无效 QVariant表达,QML 侧据此显示空输入;
  • 信号openFrequencyMarkersEditor(groupId, datasetId, nyquist, markers)声明于 core/Ui/ProjectEditor/ProjectEditor.h。

3.2 提交槽:与 commitAlarmBands 完全同构

提交实现在 core/Ui/ProjectEditor/EditorCommit.cpp,与读路径共享同一套校验逻辑(非正/越界频率丢弃、反向频带降级、倒置阈值交换),然后:

m_editor.m_selectedDataset.fftMarkers.push_back(std::move(marker)); ... auto& pm = m_model; pm.updateDataset(m_editor.m_selectedDataset.groupId, m_editor.m_selectedDataset.datasetId, m_editor.m_selectedDataset, false); m_editor.m_forms.buildDatasetModel(m_editor.m_selectedDataset);

pm.updateDataset(..., false)+buildDatasetModel——与commitAlarmBands完全相同的形状,不触碰任何 ProjectModel 构造函数可达的代码(规避启动期风险)。仪表盘的实时拾取走既有的 modified → autosave →syncRuntime()路径(与报警频带一致),无需重启即可看到更新。

3.3 QML 对话框(T6)

对话框是 app/qml/ProjectEditor/Dialogs/FrequencyMarkersEditor.qml,从 AlarmBandsEditor 的语法克隆而来,基于Widgets.SmartDialog

  • 预置卡片:静态预置项(超出 Nyquist 的条目自动跳过),源码中可见三组:
    • "Mains Hum (50 Hz + harmonics)":50 Hz Mains(#ffb300)、150 Hz 3x(#f4511e);
    • "Mains Hum (60 Hz + harmonics)":60 Hz + 3x 谐波;
    • 1/3 倍频程频带组(如 178–355 Hz "250 Hz"#fdd835、355–708 Hz "500 Hz"#7cb342、11200–22400 Hz "16 kHz"#d81b60);
  • 标记表格:Start Hz、End Hz(留空 = 点标记)、Label、Color 色块 + 共享 ColorDialog + 右键重置、Warn dB、Alarm dB、上移/下移/删除;
  • 内联校验:编辑时 Hz 钳制到 0…Nyquist,收集时warn <= alarm归一化,空白/非法数字视为未设置;
  • 实时预览条:0 Hz 到 Nyquist 的刻度条上,频带渲染为半透明区域、点标记渲染为刻度线;
  • 底部 Cancel / Apply:Apply 调用Cpp_JSON_ProjectEditor.commitFrequencyMarkers(root.collectMarkers())(见该文件第 792 行)。

DatasetView 侧(app/qml/ProjectEditor/Views/DatasetView.qml)用懒加载 Loader +ConnectionsonOpenFrequencyMarkersEditor)挂接对话框,Behavior 分区新增 "Freq. Markers" 功能区按钮,仅当所选数据集启用了DatasetFFT || DatasetWaterfall时可用。新 QML 文件已在 app/CMakeLists.txt 注册(紧邻 AlarmBandsEditor 的条目)。

4. FFT 控件:标记渲染与逐 tick 监视(T4、T5)

4.1 模型侧:bin 窗口解析与零分配监视

C++ 模型(core/Ui/UI/Widgets/FFTPlot.cpp)在构造时通过loadMarkers()复制数据集标记(m_markerRt运行时数组 + 一次性构建的m_markerConfigQVariantList),随后rebuildMarkerBins()把每个标记解析为闭包的 FFT bin 窗口(FFTPlot.cpp):

void Widgets::FFTPlot::rebuildMarkerBins() { constexpr double pointHalfWindow = 2.0; ... const int spectrumSize = m_size / 2; const double freqStep = static_cast<double>(m_samplingRate) / qMax(1, m_size); const double lastBin = qMax(0, spectrumSize - 1); for (auto& rt : m_markerRt) { double lo = 0.0; double hi = 0.0; if (rt.freqHi > rt.freqLo) { lo = std::floor(rt.freqLo / freqStep); hi = std::ceil(rt.freqHi / freqStep); } else { const double center = std::round(rt.freqLo / freqStep); lo = center - pointHalfWindow; hi = center + pointHalfWindow; } rt.binLo = static_cast<int>(qBound(0.0, lo, lastBin)); rt.binHi = static_cast<int>(qBound(static_cast<double>(rt.binLo), hi, lastBin)); } }

要点:

  • 点标记取 ±2 bins 邻域——轻微失谐的谱线仍能登记峰值(spec Decisions 中明确的计划期常量);
  • bin 宽度随 FFT 尺寸变化,因此每次rebuildFftPlan(FFT 大小改变时)都要重新解析,否则标记会"静默瞄偏"(架构笔记中特别强调此点);
  • bin 数学在转换为 int 之前于 double 域钳制(qBound),避免不可表示 double 强转 int 的未定义行为(qt-cpp-review 确认修复项)。

逐 tick 监视在updateData()computeBinSpectrum之后调用updateMarkerValues()(FFTPlot.cpp):

bool Widgets::FFTPlot::updateMarkerValues(const int spectrumSize) { ... bool changed = false; for (auto& rt : m_markerRt) { const int hi = qMin(rt.binHi, spectrumSize - 1); float peak = kSpectrumFloorDb; for (int i = qMin(rt.binLo, hi); i <= hi; ++i) peak = std::max(peak, m_binDb[static_cast<std::size_t>(i)]); const int previous_state = rt.state; const int previous_label = qRound(rt.peakDb * 10.0f); rt.peakDb = peak; if (std::isfinite(rt.alarmDb) && peak >= rt.alarmDb) rt.state = 2; else if (std::isfinite(rt.warningDb) && peak >= rt.warningDb) rt.state = 1; else rt.state = 0; changed |= rt.state != previous_state || qRound(rt.peakDb * 10.0f) != previous_label; } return changed; }

设计约束与验证重点(tasks T4 "Verify"):

  • 零稳态分配:marker/bin/state 向量在构造和rebuildFftPlan时预分配,per-tick 循环只比较浮点;markersQVariantList 只构建一次(CONSTANT 式配置);
  • 只评估显示 dB:峰值来自后弹道(post-ballistics)处理的显示频谱m_binDb——所见即所判(WYSIWYG),与--benchmark-hotpath门禁无关;
  • NaN 感知比较std::isfinite守卫后再比较,未设阈值永不上报状态;
  • 状态语义:0 = normal / 1 = warning / 2 = alarm(FFTPlot.cpp),越界索引返回频谱底噪 dB 或 0 状态;
  • 信号只在状态或读数(0.1 dB 分辨率取整比较)真正变化时触发——避免每个 tick 都发信号(qt-cpp-review 发现并修复的问题 F15)。

4.2 QML 侧:双层叠加渲染(T5)

渲染全部在 app/qml/Widgets/Dashboard/FFTPlot.qml 中完成,采用双层叠加

下层(频带/谱线)parent: plot.curveLayerz: -1(FFTPlot.qml),所以标记渲染在频谱曲线笔画之下,永不遮挡曲线:

  • 频带 = 带水平渐变的半透明矩形(边缘 0.04 → 核心 0.22 不透明度)+ 1 px 边缘描边(DAW-EQ 风格);
  • 点标记 = 2 px 竖线 + 8 px 柔光矩形背景。

上层(标签芯片):独立于曲线之上的胶囊标签层(z: 1000),文本格式为label · −42.1 dB实时读数;告警时通过Cpp_ThemeManager.alarmColorForSeverity重新着色(warning 用 severity 2、alarm 用 severity 3),alarm 状态叠加透明度闪烁(SequentialAnimation,见 FFTPlot.qml 附近)。芯片是可点击的:点击 = 瞬时聚光(root.selectedMarker高亮当前、淡化其他),且芯片的 MouseArea 透传滚轮保证缩放仍可用。芯片横向重叠时采用简单的贪心行分配(JS 遍历 marker x 位置,少量条目开销可忽略)。

Hz → x 映射:通过plot.xVisibleMin/xVisibleRangelogX ? log10(freq) : freq——与 PlotCurve/光标完全相同的变换,因此缩放/平移自动跟随,无需额外同步。

工具栏开关(R5):FFTPlot 顶部工具栏的DashboardToolButton切换(labels.svg图标),设置以showFrequencyMarkers(默认开启)持久化到 widgetSettings(FFTPlot.qml)。芯片文本只使用带编号的.arg()占位符,规避%n翻译陷阱(common-mistakes 约定)。

5. Waterfall 控件:同一标记的双视图一致性(T7)

Waterfall(专业版,QQuickPaintedItem)在 core/Ui/UI/Widgets/Waterfall.cpp 与 app/qml/Widgets/Dashboard/Waterfall.qml 中实现同样的标记体系:

  • 构造时复制标记markers.reserve(dataset.fftMarkers.size()),见 Waterfall.cpp);
  • 逐行峰值/状态updateData()从新写入的m_smoothed行计算每标记峰值,m_overlay.updateMarkerStates(...)仅当markersVisible()时执行(Waterfall.cpp);
  • 逐 paint 绘制paint()在缓存的轴层合成之后绘制频带填充/谱线/标签芯片(Waterfall.cpp)——因为升级色调按行变化,放进缓存的轴层会过期;每次 paint 额外画几条线相对图像 blit 成本可忽略;
  • Hz→x 映射单一来源:抽取本地辅助函数visibleFreqWindow(),同时被drawXAxis、悬停光标和标记通道使用——从机制上杜绝两条映射漂移(plan.md 风险清单明确项);标记监视始终在线性 bin 空间进行,显示轴(线性/对数)绝不改变测量对象;
  • markersVisible Q_PROPERTY(默认 true,Waterfall.cpp),QML 工具栏开关以同名showFrequencyMarkers持久化(Waterfall.qml);
  • 芯片同样可点击聚光:drawMarkerChip逐 paint 记录命中矩形(m_chipHitRects),mousePressEvent在启动拖拽平移之前先命中测试,悬停显示手型光标(Waterfall.cpp)。

验收要点(AC6):同一组标记在 Waterfall 的缩放/平移下出现在正确频率上;无标记时drawXAxis输出逐字节不变(tasks T7 Verify 回读项)。

6. 远程 API:原子读写与数据集更新键(T3)

6.1 命令注册与语义

API 表面完全镜像报警频带对:

  • 原子命令project.dataset.getFFTMarkers/project.dataset.setFFTMarkers,注册在 core/Api/API/Handlers/ProjectDatasetFieldCommands.cpp。setFFTMarkers的语义是"原子替换整个 fftMarkers 数组",无效条目静默丢弃并计入result.droppedInvalid(ProjectDatasetFieldCommands.cpp),result.count返回存储后的条数;
  • 数据集更新键fftMarkers分支加入applyDatasetNumericFields,数组逐条经DataModel::read(FrequencyMarker&)解析,无效条目丢弃,键被消费以保证未知字段告警的诚实性;
  • 免费特性:无BUILD_COMMERCIAL门禁——schema 与 FFT 控件为 GPL 免费部分,仅 Waterfall 渲染器本身已在控件层面按 Pro 门控;
  • 遵循既有"按键白名单 + 专用命令"模式,新键不破坏老客户端(旧构建读取时忽略未知键)。

命令元数据也同步到了脚本侧(app/rcc/api/SerialStudio.js、app/rcc/api/SerialStudio.lua、app/rcc/api/api-schema.json)以及 gRPC 类型化 proto(doc/grpc/serialstudio-typed.proto),SDK 由scripts/sanitize-commit.py的 generate-sdk 步骤再生。

6.2 API 用法示例(基于测试与注册语义)

以下调用形态参考集成测试 tests/integration/test_fft_markers.py 与命令注册描述:

写入标记列表(原子替换)

{ "command": "project.dataset.setFFTMarkers", "data": { "groupId": 1, "datasetId": 2, "markers": [ {"freq": 800, "endFreq": 850, "label": "Gear mesh", "color": "#ff5722", "warningDb": -40, "alarmDb": -25}, {"freq": 50, "label": "Mains", "warningDb": -45} ] } }

响应中count为实际存储条数,droppedInvalid为被丢弃的非法条目数。

读取标记列表

{ "command": "project.dataset.getFFTMarkers", "data": {"groupId": 1, "datasetId": 2} }

通过通用数据集更新键:在project.dataset.update的字段中携带fftMarkers数组,语义与setFFTMarkers一致(字段级语义细节见 core/Api/API/Handlers/ProjectUpdateCommands.cpp 的说明文本)。

7. 集成测试与验收(T8、AC1/AC2)

新增的 tests/integration/test_fft_markers.py 覆盖以下场景(遵循tests/utils/api_client.py模式与 tests/README.md 约定):

测试覆盖点
test_set_get_round_tripAC2:setFFTMarkers原样存储、getFFTMarkers原样回读,无droppedInvalid
test_empty_array_clears空数组清空标记列表
test_invalid_entries_dropped混合垃圾载荷,droppedInvalid == 4(负频率、坏类型等)
test_reversed_band_becomes_point反向频带降级为点标记
test_reversed_thresholds_swapped倒置 warn/alarm 阈值被交换
test_dataset_update_keyproject.dataset.updatefftMarkers键往返
test_markers_survive_save/test_markers_survive_reloadAC1:保存/重载后标记保持
test_no_markers_means_no_key无标记项目序列化不含fftMarkers键(字节级兼容)

运行方式(维护者执行):

python -m py_compile tests/integration/test_fft_markers.py pytest tests/integration/test_fft_markers.py -v # 需应用 + API 服务已启动

其余验收项为维护者运行时检查(tasks.md 的 maintainer AC3–AC7):AC3 用音频输入实测 800 Hz 点标记与频带在两种轴模式下的渲染/缩放/深浅主题;AC4 验证阈值越过/回升时的升级/降级;AC5 对照 AlarmBands 对话框核对观感、越界校验与 Cancel/Apply 语义;AC6 核对 Waterfall 同频渲染;AC7 确认--benchmark-hotpath门禁无改动。热路径(帧摄取/解析)零接触已由"无 ingest-path diff"(tasks.md Definition of Done)与架构笔记共同确认。

8. 约束、权衡与设计决策(规格要点)

8.1 非目标(Non-Goals,明确边界)

  • 无全局报警系统集成:标记报警是控件局部的视觉表现,不接入应用级报警监视器、MQTT 或导出(FFT 只在控件可见时计算);
  • 无自动峰值检测/跟踪:标记固定在用户配置的频率上,不找峰、不追 RPM、不移动标记;
  • 无谐波/边带游标(1x/2x/3x 族):用户可自行添加各次谐波,族生成属未来工作;
  • 无逐标记历史/日志:读数仅实时;
  • 无新依赖、无新文件格式:标记搭乘既有项目 JSON schema。

8.2 关键决策记录(Decisions)

  • 标记存数据集 JSON 而非 widgetSettings:工程配置属于项目且须可被 API 寻址,而非按控件的装饰性状态;
  • 阈值取显示 dB(FFT 控件原生 Y 单位):与用户在图上读到的值一致,不做线性单位阈值;
  • 点标记监视窗口 = ±2 bins 固定邻域:轻微失谐仍可登记;
  • Waterfall 只做渲染 + 廉价读数,不强制升级视觉:其 C++ 绘制的轴层让逐 tick 标签工作成本高昂,超出行/带/标签的一致性属于打磨而非契约。

8.3 风险与缓解

  • 字节级序列化回归(0014/0017 纪律):FFTMarkers仅非空时写出、可选子键省略,由 pytest 往返 + 未触碰项目 diff 覆盖;
  • %n/.arg()翻译陷阱:芯片文本仅用编号占位符;
  • 缩放下 QML 叠加漂移:映射与 PlotCurve/光标一致(xVisibleMin/xVisibleRange),并裁剪到图层防越界出血;
  • Waterfall Hz 映射漂移drawXAxis与标记通道共享同一本地辅助函数;
  • 多选提交commitFrequencyMarkerscommitAlarmBands一样仅作用于m_selectedDataset,按钮启用条件对齐,行为一致;
  • Apply 后仪表盘陈旧:沿用 alarm bands 的 updateDataset → autosave →syncRuntime()路径,AC5 验证实时拾取。

9. 落地路径与速查

按 tasks.md 的依赖顺序(T1 无依赖,T2/T3/T7 依赖 T1,T4 依赖 T1,T5 依赖 T4,T6 依赖 T2,T8 依赖 T3,T9 依赖 T1–T8),完整实现共 9 个任务、横跨约 14 个文件,全部已完成。速查索引:

  • 数据模型:core/Core/DataModel/Frame.h、core/Core/DataModel/Frame.cpp、core/Core/DataModel/FrameKeys.h
  • 编辑器 C++:core/Ui/ProjectEditor/EditorForms.cpp、core/Ui/ProjectEditor/EditorCommit.cpp、core/Ui/ProjectEditor/ProjectEditor.h
  • 编辑器 QML:app/qml/ProjectEditor/Dialogs/FrequencyMarkersEditor.qml、app/qml/ProjectEditor/Views/DatasetView.qml
  • FFT 控件:core/Ui/UI/Widgets/FFTPlot.cpp、app/qml/Widgets/Dashboard/FFTPlot.qml
  • Waterfall 控件:core/Ui/UI/Widgets/Waterfall.cpp、app/qml/Widgets/Dashboard/Waterfall.qml
  • API:core/Api/API/Handlers/ProjectDatasetFieldCommands.cpp、core/Api/API/Handlers/ProjectUpdateCommands.cpp
  • 测试:tests/integration/test_fft_markers.py
  • 架构笔记:doc/claude/architecture/dashboard.md
  • 规范文档:doc/claude/specs/0019-fft-freq-markers/spec.md、plan.md、tasks.md

典型使用流程一句话总结:在 Project Editor 中选中 FFT/Waterfall 数据集 → 点击 "Freq. Markers" 打开对话框 → 从预置(50/60 Hz 电源谐波、倍频程带)或手工添加标记(频率/频带 + 标签 + 颜色 + 可选 dB 阈值)→ Apply 提交(commitFrequencyMarkers重建向量并更新数据集模型)→ 仪表盘 FFT 与 Waterfall 实时渲染标记并逐 tick 监视峰值,越过 warning/alarm 阈值即视觉升级;整个列表可随时通过project.dataset.get/setFFTMarkersfftMarkers更新键经远程 API 读写,并随项目文件持久化——未加标记的旧项目保存后字节级不变。

【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 2:17:38

Dify离线部署插件全指南:从下载到导入的完整流程

做私有化部署的同行应该都有这种感觉&#xff1a;Dify本身装起来不难&#xff0c;真正闹心的是插件。上个月我帮客户做一套完全隔离内网环境下的Dify交付&#xff0c;平台装好、大模型也对接完了&#xff0c;结果到了插件这一步卡了整整两天——插件市场连不上网&#xff0c;页…

作者头像 李华
网站建设 2026/9/17 2:16:41

Python+NBA球员数据:从爬虫抓取到交互式可视化全流程

简介&#xff1a;这是一套面向Python初学者与数据分析爱好者的NBA球员数据可视化项目资料&#xff0c;完整演示如何从网页爬取球员数据&#xff0c;经清洗与预处理后&#xff0c;使用NumPy、SciPy进行统计计算&#xff0c;再借助matplotlib、seaborn绘制分布图、箱线图与散点图…

作者头像 李华
网站建设 2026/9/17 2:16:25

蜂鸟级极简操作系统colibri:从设计原理到QEMU实操复现

colibri这个词&#xff0c;懂点外语的朋友应该不陌生&#xff0c;在法语和西班牙语里是“蜂鸟”的意思。技术圈里拿它命名的项目不少&#xff0c;但我印象最深的&#xff0c;是一个把“蜂鸟式极致轻量”做到了骨子里的极简系统项目。整个系统镜像只有个位数MB&#xff0c;运行内…

作者头像 李华
网站建设 2026/9/17 2:13:39

深入解读 MongoDB 内置 WiredTiger 的 C/C++ 编码规范与贡献流程

深入解读 MongoDB 内置 WiredTiger 的 C/C 编码规范与贡献流程 【免费下载链接】mongo The MongoDB Database 项目地址: https://gitcode.com/GitHub_Trending/mo/mongo WiredTiger 是 MongoDB 默认的存储引擎&#xff0c;其完整源码以第三方库形式内嵌于本仓库的 src/t…

作者头像 李华
网站建设 2026/9/17 2:13:37

版本管理不只是一串数字:从SolidWorks PDM到对象级PLM的演进

1. 为什么版本管理总被当成“给文件名1”先说一个我亲眼见过的场景。工艺部门接到现场投诉&#xff1a;批量装配时发现一个支架零件装不上&#xff0c;防转销孔的位置差了不到 0.05mm。我去查图纸&#xff0c;发现发到车间的PDF图纸文件名是“支架_V2”&#xff0c;车间老张电脑…

作者头像 李华