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 附近的那个鼓包",记住它代表什么,再凭经验判断其电平是否合格。这带来三个痛点:
- 无法给有工程意义的频率打上名称标签;
- 无法一眼看出被监视的谱线是否越过了告警或报警电平;
- 项目文件中不保存任何此类领域知识,换台机器、换个项目就要重来。
有趣的是,数据集在幅度域早已解决了等价问题:报警频带(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");其中Frequency、Label、Color复用既有键。一个典型标记的 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.5(qMax(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);
- "Mains Hum (50 Hz + harmonics)":50 Hz Mains(
- 标记表格: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 +Connections(onOpenFrequencyMarkersEditor)挂接对话框,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.curveLayer且z: -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/xVisibleRange,logX ? 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_trip | AC2: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_key | project.dataset.update的fftMarkers键往返 |
test_markers_survive_save/test_markers_survive_reload | AC1:保存/重载后标记保持 |
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与标记通道共享同一本地辅助函数; - 多选提交:
commitFrequencyMarkers与commitAlarmBands一样仅作用于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/setFFTMarkers或fftMarkers更新键经远程 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),仅供参考