From 52c4dcbf706c686e719302aac559c2b3f7f872c9 Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Wed, 27 Aug 2025 14:53:38 -0400 Subject: [PATCH 01/10] Preserving cursor state, and cursor freeze --- src/cursor.cpp | 28 ++++++++++++++++++---------- src/cursor.h | 3 +++ src/cursors.cpp | 5 +++++ src/cursors.h | 3 +++ src/mainwindow.cpp | 1 + src/plotview.cpp | 34 ++++++++++++++++++++++++++++++++-- src/plotview.h | 6 ++++++ src/spectrogramcontrols.cpp | 2 ++ src/spectrogramcontrols.h | 1 + 9 files changed, 71 insertions(+), 12 deletions(-) diff --git a/src/cursor.cpp b/src/cursor.cpp index b9ac71d0..02c31585 100644 --- a/src/cursor.cpp +++ b/src/cursor.cpp @@ -25,6 +25,9 @@ Cursor::Cursor(Qt::Orientation orientation, Qt::CursorShape mouseCursorShape, QO } +void Cursor::frozen(bool enable) { + isFrozen = enable; +} int Cursor::fromPoint(QPoint point) { return (orientation == Qt::Vertical) ? point.x() : point.y(); @@ -39,17 +42,22 @@ bool Cursor::pointOverCursor(QPoint point) bool Cursor::mouseEvent(QEvent::Type type, QMouseEvent event) { + if (isFrozen) { + return false; + } // If the mouse pointer moves over a cursor, display a resize pointer - if (pointOverCursor(event.pos()) && type != QEvent::Leave) { - if (!cursorOverrided) { - cursorOverrided = true; - QApplication::setOverrideCursor(QCursor(cursorShape)); - } - // Restore pointer if it moves off the cursor, or leaves the widget - } else if (cursorOverrided) { - cursorOverrided = false; - QApplication::restoreOverrideCursor(); - } + if (pointOverCursor(event.pos()) && type != QEvent::Leave) { + if (!cursorOverrided) { + cursorOverrided = true; + QApplication::setOverrideCursor(QCursor(cursorShape)); + } + + // Restore pointer if it moves off the cursor, or leaves the widget + } else if (cursorOverrided) { + cursorOverrided = false; + QApplication::restoreOverrideCursor(); + } + // Start dragging on left mouse button press, if over a cursor if (type == QEvent::MouseButtonPress) { diff --git a/src/cursor.h b/src/cursor.h index aceb6089..b4f7bb66 100644 --- a/src/cursor.h +++ b/src/cursor.h @@ -34,6 +34,8 @@ class Cursor : public QObject void setPos(int newPos); bool mouseEvent(QEvent::Type type, QMouseEvent event); + void frozen(bool enable); + signals: void posChanged(); @@ -45,5 +47,6 @@ class Cursor : public QObject Qt::CursorShape cursorShape; bool dragging = false; bool cursorOverrided = false; + bool isFrozen = false; int cursorPosition = 0; }; diff --git a/src/cursors.cpp b/src/cursors.cpp index 8087f031..cedf4a35 100644 --- a/src/cursors.cpp +++ b/src/cursors.cpp @@ -29,6 +29,11 @@ Cursors::Cursors(QObject * parent) : QObject::QObject(parent) connect(maxCursor, &Cursor::posChanged, this, &Cursors::cursorMoved); } +void Cursors::frozen(bool enable) { + minCursor->frozen(enable); + maxCursor->frozen(enable); +} + void Cursors::cursorMoved() { // Swap cursors if one has been dragged past the other diff --git a/src/cursors.h b/src/cursors.h index 9e7e5728..a12491ed 100644 --- a/src/cursors.h +++ b/src/cursors.h @@ -39,6 +39,9 @@ class Cursors : public QObject void setSegments(int segments); void setSelection(range_t selection); + void frozen(bool enable); + + public slots: void cursorMoved(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index dc62d769..74b098c2 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -49,6 +49,7 @@ MainWindow::MainWindow() connect(dock->powerMaxSlider, &QSlider::valueChanged, plots, &PlotView::setPowerMax); connect(dock->powerMinSlider, &QSlider::valueChanged, plots, &PlotView::setPowerMin); connect(dock->cursorsCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableCursors); + connect(dock->cursorsFreezeCheckBox, &QCheckBox::stateChanged, plots, &PlotView::freezeCursors); connect(dock->scalesCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableScales); connect(dock->annosCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableAnnotations); connect(dock->annosCheckBox, &QCheckBox::stateChanged, dock, &SpectrogramControls::enableAnnotations); diff --git a/src/plotview.cpp b/src/plotview.cpp index 839ecfff..b39e8de0 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -37,13 +37,14 @@ #include #include "plots.h" -PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}) +PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}), selectedSamples({0,0}) { mainSampleSource = input; setDragMode(QGraphicsView::ScrollHandDrag); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setMouseTracking(true); enableCursors(false); + freezeCursors(false); connect(&cursors, &Cursors::cursorsMoved, this, &PlotView::cursorsMoved); spectrogramPlot = new SpectrogramPlot(std::shared_ptr>>(mainSampleSource)); @@ -98,6 +99,7 @@ void PlotView::updateAnnotationTooltip(QMouseEvent *event) } } + void PlotView::contextMenuEvent(QContextMenuEvent * event) { QMenu menu; @@ -208,12 +210,40 @@ void PlotView::emitTimeSelection() emit timeSelectionChanged(selectionTime); } +void PlotView::freezeCursors(bool enabled) { + + cursorsFrozen.enabled = enabled; + if (cursorsEnabled) { + if (enabled) { + cursorsFrozen.range = cursors.selection(); + } + + cursors.frozen(enabled); + } +} void PlotView::enableCursors(bool enabled) { cursorsEnabled = enabled; if (enabled) { int margin = viewport()->rect().width() / 3; - cursors.setSelection({viewport()->rect().left() + margin, viewport()->rect().right() - margin}); + + + // Update cursors + int startColumn = viewport()->rect().left() + margin; + if (selectedSamples.minimum != selectedSamples.maximum) { + + int colWidth = sampleToColumn(selectedSamples.maximum) - sampleToColumn(selectedSamples.minimum); + + range_t newSelection = { + startColumn, + startColumn + colWidth + }; + cursors.setSelection(newSelection); + } else { + cursors.setSelection({startColumn, viewport()->rect().right() - margin}); + } + + // cursorsMoved(); } viewport()->update(); diff --git a/src/plotview.h b/src/plotview.h index 326d547f..0c5d0ffe 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -29,6 +29,10 @@ #include "spectrogramplot.h" #include "traceplot.h" +typedef struct PlotViewFrozenCursors { + bool enabled; + range_t range; +} FrozenCursors; class PlotView : public QGraphicsView, Subscriber { Q_OBJECT @@ -45,6 +49,7 @@ class PlotView : public QGraphicsView, Subscriber public slots: void cursorsMoved(); void enableCursors(bool enabled); + void freezeCursors(bool enabled); void enableScales(bool enabled); void enableAnnotations(bool enabled); void enableAnnotationCommentsTooltips(bool enabled); @@ -80,6 +85,7 @@ public slots: int powerMin; int powerMax; bool cursorsEnabled; + FrozenCursors cursorsFrozen; double sampleRate = 0.0; bool timeScaleEnabled; int scrollZoomStepsAccumulated = 0; diff --git a/src/spectrogramcontrols.cpp b/src/spectrogramcontrols.cpp index 0e0f9aa2..a9c1d313 100644 --- a/src/spectrogramcontrols.cpp +++ b/src/spectrogramcontrols.cpp @@ -75,6 +75,8 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent cursorsCheckBox = new QCheckBox(widget); layout->addRow(new QLabel(tr("Enable cursors:")), cursorsCheckBox); + cursorsFreezeCheckBox = new QCheckBox(widget); + layout->addRow(new QLabel(tr("Freeze cursors:")), cursorsFreezeCheckBox); cursorSymbolsSpinBox = new QSpinBox(); cursorSymbolsSpinBox->setMinimum(1); diff --git a/src/spectrogramcontrols.h b/src/spectrogramcontrols.h index 69e7d608..3b4564e2 100644 --- a/src/spectrogramcontrols.h +++ b/src/spectrogramcontrols.h @@ -69,6 +69,7 @@ private slots: QSlider *powerMinSlider; QCheckBox *cursorsCheckBox; QSpinBox *cursorSymbolsSpinBox; + QCheckBox *cursorsFreezeCheckBox; QLabel *rateLabel; QLabel *periodLabel; QLabel *symbolRateLabel; From 7491bb5293e3e18bb3bad40d20ec4863feca1825 Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Wed, 27 Aug 2025 16:27:21 -0400 Subject: [PATCH 02/10] support for specifying the center frequency --- src/inputsource.cpp | 10 ++++++ src/inputsource.h | 3 ++ src/main.cpp | 17 +++++++++- src/mainwindow.cpp | 27 ++++++++++++++++ src/mainwindow.h | 2 ++ src/plotview.cpp | 11 +++++++ src/plotview.h | 2 ++ src/spectrogramcontrols.cpp | 9 ++++++ src/spectrogramcontrols.h | 1 + src/spectrogramplot.cpp | 64 ++++++++++++++++++++++++------------- src/spectrogramplot.h | 2 ++ 11 files changed, 125 insertions(+), 23 deletions(-) diff --git a/src/inputsource.cpp b/src/inputsource.cpp index ebc28402..4385bbb7 100644 --- a/src/inputsource.cpp +++ b/src/inputsource.cpp @@ -461,6 +461,16 @@ double InputSource::rate() return sampleRate; } +void InputSource::setCenterFrequency(double freq) +{ + centerFreq = freq; + invalidate(); +} + +double InputSource::centerFrequency() +{ + return centerFreq; +} std::unique_ptr[]> InputSource::getSamples(size_t start, size_t length) { if (inputFile == nullptr) diff --git a/src/inputsource.h b/src/inputsource.h index c0d67612..a9654285 100644 --- a/src/inputsource.h +++ b/src/inputsource.h @@ -37,6 +37,7 @@ class InputSource : public SampleSource> QFile *inputFile = nullptr; size_t sampleCount = 0; double sampleRate = 0.0; + double centerFreq = 0.0; uchar *mmapData = nullptr; std::unique_ptr sampleAdapter; std::string _fmt; @@ -54,8 +55,10 @@ class InputSource : public SampleSource> return sampleCount; }; void setSampleRate(double rate); + void setCenterFrequency(double freq); void setFormat(std::string fmt); double rate(); + double centerFrequency(); bool realSignal() { return _realSignal; }; diff --git a/src/main.cpp b/src/main.cpp index 7d3e90b6..ae44c838 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -45,6 +45,11 @@ int main(int argc, char *argv[]) QCoreApplication::translate("main", "fmt")); parser.addOption(formatOption); + QCommandLineOption centerFreqOption(QStringList() << "c" << "centerfreq", + QCoreApplication::translate("main", "Set center frequency."), + QCoreApplication::translate("main", "Hz")); + parser.addOption(centerFreqOption); + // Process the actual command line parser.process(a); @@ -57,8 +62,8 @@ int main(int argc, char *argv[]) if (args.size()>=1) mainWin.openFile(args.at(0)); + bool ok; if (parser.isSet(rateOption)) { - bool ok; auto rate = parser.value(rateOption).toDouble(&ok); if(!ok) { fputs("ERROR: could not parse rate\n", stderr); @@ -67,6 +72,16 @@ int main(int argc, char *argv[]) mainWin.setSampleRate(rate); } + + if (parser.isSet(centerFreqOption)) { + auto centerfreq = parser.value(centerFreqOption).toDouble(&ok); + if(!ok) { + fputs("ERROR: could not parse center frequency\n", stderr); + return 1; + } + mainWin.setCenterFrequency(centerfreq); + } + mainWin.show(); return a.exec(); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 74b098c2..5ff207eb 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -44,7 +44,9 @@ MainWindow::MainWindow() // Connect dock inputs connect(dock, &SpectrogramControls::openFile, this, &MainWindow::openFile); + connect(dock->sampleRate, static_cast(&QLineEdit::textChanged), this, static_cast(&MainWindow::setSampleRate)); + connect(dock->centerFrequency, static_cast(&QLineEdit::textChanged), this, static_cast(&MainWindow::setCenterFrequency)); connect(dock, static_cast(&SpectrogramControls::fftOrZoomChanged), plots, &PlotView::setFFTAndZoom); connect(dock->powerMaxSlider, &QSlider::valueChanged, plots, &PlotView::setPowerMax); connect(dock->powerMinSlider, &QSlider::valueChanged, plots, &PlotView::setPowerMin); @@ -88,6 +90,14 @@ void MainWindow::openFile(QString fileName) if (!ss.fail()) { setSampleRate(rate); } + + + std::stringstream ssfreq(centerfreq.toUtf8().constData()); + double freq; + ssfreq >> freq; + if (!ssfreq.fail()) { + setCenterFrequency(freq); + } } try @@ -104,6 +114,7 @@ void MainWindow::openFile(QString fileName) void MainWindow::invalidateEvent() { plots->setSampleRate(input->rate()); + plots->setCenterFrequency(input->centerFrequency()); // Only update the text box if it is not already representing // the current value. Otherwise the cursor might jump or the @@ -130,6 +141,22 @@ void MainWindow::setSampleRate(double rate) dock->sampleRate->setText(QString::number(rate)); } +void MainWindow::setCenterFrequency(QString freq) +{ + auto centerFreq = freq.toDouble(); + input->setCenterFrequency(centerFreq); + plots->setCenterFrequency(centerFreq); + + // Save the sample rate in settings as we're likely to be opening the same file across multiple runs + QSettings settings; + settings.setValue("CenterFrequency", centerFreq); +} + +void MainWindow::setCenterFrequency(double freq) +{ + dock->centerFrequency->setText(QString::number(freq)); +} + void MainWindow::setFormat(QString fmt) { input->setFormat(fmt.toUtf8().constData()); diff --git a/src/mainwindow.h b/src/mainwindow.h index 4c7cb7f9..346cbe59 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -36,6 +36,8 @@ public slots: void openFile(QString fileName); void setSampleRate(QString rate); void setSampleRate(double rate); + void setCenterFrequency(QString centerfreq); + void setCenterFrequency(double centerfreq); void setFormat(QString fmt); void invalidateEvent() override; diff --git a/src/plotview.cpp b/src/plotview.cpp index b39e8de0..88f5d943 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -664,6 +664,17 @@ void PlotView::setSampleRate(double rate) emitTimeSelection(); } + +void PlotView::setCenterFrequency(double freq) { + + centerFrequency = freq; + + if (spectrogramPlot != nullptr) + spectrogramPlot->setCenterFrequency(freq); +#warning "PSY EMIT WHAT?" + emitTimeSelection(); +} + void PlotView::enableScales(bool enabled) { timeScaleEnabled = enabled; diff --git a/src/plotview.h b/src/plotview.h index 0c5d0ffe..c87c0c6c 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -40,6 +40,7 @@ class PlotView : public QGraphicsView, Subscriber public: PlotView(InputSource *input); void setSampleRate(double rate); + void setCenterFrequency(double freq); signals: void timeSelectionChanged(float time); @@ -87,6 +88,7 @@ public slots: bool cursorsEnabled; FrozenCursors cursorsFrozen; double sampleRate = 0.0; + double centerFrequency = 0.0; bool timeScaleEnabled; int scrollZoomStepsAccumulated = 0; bool annotationCommentsEnabled; diff --git a/src/spectrogramcontrols.cpp b/src/spectrogramcontrols.cpp index a9c1d313..c8cc6b1f 100644 --- a/src/spectrogramcontrols.cpp +++ b/src/spectrogramcontrols.cpp @@ -41,6 +41,15 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent sampleRate->setValidator(double_validator); layout->addRow(new QLabel(tr("Sample rate:")), sampleRate); + + + + centerFrequency = new QLineEdit(); + auto freq_double_validator = new QDoubleValidator(this); + freq_double_validator->setBottom(0.0); + centerFrequency->setValidator(freq_double_validator); + layout->addRow(new QLabel(tr("Center frequency:")), centerFrequency); + // Spectrogram settings layout->addRow(new QLabel()); // TODO: find a better way to add an empty row? layout->addRow(new QLabel(tr("Spectrogram"))); diff --git a/src/spectrogramcontrols.h b/src/spectrogramcontrols.h index 3b4564e2..92d54a84 100644 --- a/src/spectrogramcontrols.h +++ b/src/spectrogramcontrols.h @@ -63,6 +63,7 @@ private slots: public: QPushButton *fileOpenButton; QLineEdit *sampleRate; + QLineEdit *centerFrequency; QSlider *fftSizeSlider; QSlider *zoomLevelSlider; QSlider *powerMaxSlider; diff --git a/src/spectrogramplot.cpp b/src/spectrogramplot.cpp index 3d42c0be..088c7de1 100644 --- a/src/spectrogramplot.cpp +++ b/src/spectrogramplot.cpp @@ -40,6 +40,7 @@ SpectrogramPlot::SpectrogramPlot(std::shared_ptrrealSignal()) - painter.drawLine(0, tickny, 30, tickny); - painter.drawLine(0, tickpy, 30, tickpy); + int64_t tfreq = (bottomFreq + tick)/divisor; + painter.drawLine(0, tickpy, 30, tickpy); + if (!inputSource->realSignal()) + painter.drawLine(0, tickny, 30, tickny); - if (tick != 0) { - char buf[128]; + if (tfreq != 0) { + char buf[128]; + snprintf(buf, sizeof(buf), "%li %s",tfreq, suffixBuf); + if (tfreq > centerFrequency/divisor) { - if (bwPerTick % 1000000000 == 0) { - snprintf(buf, sizeof(buf), "-%lu GHz", tick / 1000000000); - } else if (bwPerTick % 1000000 == 0) { - snprintf(buf, sizeof(buf), "-%lu MHz", tick / 1000000); - } else if(bwPerTick % 1000 == 0) { - snprintf(buf, sizeof(buf), "-%lu kHz", tick / 1000); - } else { - snprintf(buf, sizeof(buf), "-%lu Hz", tick); - } + painter.drawText(5, ticky + 15, buf); + } else { + painter.drawText(5, ticky, buf); - if (!inputSource->realSignal()) - painter.drawText(5, tickny - 5, buf); - - buf[0] = ' '; - painter.drawText(5, tickpy + 15, buf); + } } - tick += bwPerTick; + } + // Draw small ticks bwPerTick /= 10; - if (bwPerTick >= 1 ) { tick = 0; while (tick <= sampleRate / 2) { @@ -158,6 +173,7 @@ void SpectrogramPlot::paintFrequencyScale(QPainter &painter, QRect &rect) tick += bwPerTick; } } + painter.restore(); } @@ -410,6 +426,10 @@ void SpectrogramPlot::setSampleRate(double rate) sampleRate = rate; } + +void SpectrogramPlot::setCenterFrequency(double freq) { + centerFrequency = freq; +} void SpectrogramPlot::enableScales(bool enabled) { frequencyScaleEnabled = enabled; diff --git a/src/spectrogramplot.h b/src/spectrogramplot.h index 2e52cb4b..dd806fbb 100644 --- a/src/spectrogramplot.h +++ b/src/spectrogramplot.h @@ -49,6 +49,7 @@ class SpectrogramPlot : public Plot bool mouseEvent(QEvent::Type type, QMouseEvent event) override; std::shared_ptr>> input() { return inputSource; }; void setSampleRate(double sampleRate); + void setCenterFrequency(double freq); bool tunerEnabled(); void enableScales(bool enabled); void enableAnnotations(bool enabled); @@ -79,6 +80,7 @@ public slots: float powerMax; float powerMin; double sampleRate; + double centerFrequency; bool frequencyScaleEnabled; bool sigmfAnnotationsEnabled; From 6959dcddeea93f820c421b68f50b844d57704c26 Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Wed, 27 Aug 2025 17:06:21 -0400 Subject: [PATCH 03/10] less jiggly set cursor segments --- src/plotview.cpp | 16 ++++++++++------ src/plotview.h | 2 ++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/plotview.cpp b/src/plotview.cpp index 88f5d943..fe7d8b8b 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -199,6 +199,8 @@ void PlotView::cursorsMoved() columnToSample(horizontalScrollBar()->value() + cursors.selection().maximum) }; + samplesPerSegment = selectedSamples.length() / cursors.segments(); + emitTimeSelection(); viewport()->update(); } @@ -451,14 +453,17 @@ void PlotView::repaint() viewport()->update(); } +#include void PlotView::setCursorSegments(int segments) { - // Calculate number of samples per segment - float sampPerSeg = (float)selectedSamples.length() / cursors.segments(); - // Alter selection to keep samples per segment the same - selectedSamples.maximum = selectedSamples.minimum + (segments * sampPerSeg + 0.5f); + int curSegments = cursors.segments(); + if (curSegments != segments) { + int deltaSegments = segments - curSegments; + // Calculate number of samples per segment + selectedSamples.maximum = selectedSamples.maximum + (deltaSegments * samplesPerSegment); + } cursors.setSegments(segments); updateView(); emitTimeSelection(); @@ -671,8 +676,7 @@ void PlotView::setCenterFrequency(double freq) { if (spectrogramPlot != nullptr) spectrogramPlot->setCenterFrequency(freq); -#warning "PSY EMIT WHAT?" - emitTimeSelection(); + } void PlotView::enableScales(bool enabled) diff --git a/src/plotview.h b/src/plotview.h index c87c0c6c..db9f9052 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -78,6 +78,8 @@ public slots: std::vector> plots; range_t viewRange; range_t selectedSamples; + size_t samplesPerSegment; + int zoomPos; size_t zoomSample; From 4dad35ec8987737356d2807763eba4f22b382957 Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Wed, 27 Aug 2025 18:29:54 -0400 Subject: [PATCH 04/10] Feed symbols to external program stdin --- src/CMakeLists.txt | 1 + src/plotview.cpp | 113 ++++++++++++++++++++++++++++++++++++ src/plotview.h | 3 + src/spectrogramcontrols.cpp | 3 + src/symbolprogoutput.cpp | 47 +++++++++++++++ src/symbolprogoutput.h | 27 +++++++++ 6 files changed, 194 insertions(+) create mode 100644 src/symbolprogoutput.cpp create mode 100644 src/symbolprogoutput.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 05b20e76..5962ae92 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -45,6 +45,7 @@ list(APPEND inspectrum_sources samplesource.cpp spectrogramcontrols.cpp spectrogramplot.cpp + symbolprogoutput.cpp threshold.cpp traceplot.cpp tuner.cpp diff --git a/src/plotview.cpp b/src/plotview.cpp index fe7d8b8b..7e79b65c 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -35,7 +35,15 @@ #include #include #include +#include +#include +#include +#include + #include "plots.h" +#include "symbolprogoutput.h" + + PlotView::PlotView(InputSource *input) : cursors(this), viewRange({0, 0}), selectedSamples({0,0}) { @@ -150,6 +158,25 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) extract->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float))); extractMenu->addAction(extract); + + + // Add action to run external program + auto runProgramAction = new QAction("Feed to External...", extractMenu); + connect( + runProgramAction, &QAction::triggered, + this, [=]() { + feedSymbolsToExternalProgram(src); + } + ); + + runProgramAction->setEnabled(cursorsEnabled && (src->sampleType() == typeid(float))); + extractMenu->addAction(runProgramAction); + + + + + + // Add action to extract symbols from selected plot to clipboard auto extractClipboard = new QAction("Copy to clipboard", extractMenu); connect( @@ -190,6 +217,7 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) updateViewRange(false); if(menu.exec(event->globalPos())) updateView(false); + } void PlotView::cursorsMoved() @@ -316,6 +344,91 @@ bool PlotView::viewportEvent(QEvent *event) { return QGraphicsView::viewportEvent(event); } +QByteArray vectorToTextByteArray(const std::vector& data) +{ + QString text; + for (float value : data) { + text += QString::number(value, 'f', 6) + ","; // One float per line, 6 decimal places + } + return text.toUtf8(); // Convert to QByteArray with UTF-8 encoding +} + +void PlotView::feedSymbolsToExternalProgram(std::shared_ptr src) { + + if (!cursorsEnabled) + return; + // Step 1: Open a file dialog to select the program + QFileDialog dialog(this); + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setWindowTitle("Select External Program"); + + + QSettings settings; + QString lastProgramPath = settings.value("lastProgramPath", QDir::homePath()).toString(); + dialog.selectFile(lastProgramPath); + // Optional: Filter for executable files (platform-specific) +#ifdef Q_OS_WIN + dialog.setNameFilter("Executables (*.exe);;All Files (*)"); +#else + dialog.setNameFilter("All Files (*)"); +#endif + + if (dialog.exec() != QDialog::Accepted || dialog.selectedFiles().isEmpty()) { + return; // User canceled or didn't select a file + } + + QString programPath = dialog.selectedFiles().first(); + + // Save the selected program path to QSettings + settings.setValue("lastProgramPath", programPath); + + + // Step 2: Execute the program and pipe data + QProcess process(this); + + // Configure the process to allow writing to stdin + process.setProcessChannelMode(QProcess::MergedChannels); // Merge stdout and stderr for simplicity + process.start(programPath, QStringList()); // No arguments, adjust if needed + + if (!process.waitForStarted()) { + QMessageBox::critical(this, "Error", "Failed to start program: " + process.errorString()); + return; + } + + + auto floatSrc = std::dynamic_pointer_cast>(src); + if (!floatSrc) + return; + auto samples = floatSrc->getSamples(selectedSamples.minimum, selectedSamples.length()); + auto step = (float)selectedSamples.length() / cursors.segments(); + auto symbols = std::vector(); + for (auto i = step / 2; i < selectedSamples.length(); i += step) + { + symbols.push_back(samples[i]); + } + + // Step 3: Pipe data to the program's stdin + // Assume data is a QString or QByteArray from your class + QByteArray dataToSend = vectorToTextByteArray(symbols); + process.write(dataToSend); + process.closeWriteChannel(); // Signal EOF to stdin + + // Step 4: Wait for the process to finish (optional, depending on your needs) + if (!process.waitForFinished()) { + QMessageBox::critical(this, "Error", "Program failed: " + process.errorString()); + return; + } + + // Optional: Read output if needed + QByteArray output = process.readAllStandardOutput(); + if (!output.isEmpty()) { + SymbolProgOutput outputDialog(QString(output), this); + outputDialog.exec(); + } + + +} + void PlotView::extractSymbols(std::shared_ptr src, bool toClipboard) { diff --git a/src/plotview.h b/src/plotview.h index db9f9052..dc9b18f4 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -98,6 +98,7 @@ public slots: void addPlot(Plot *plot); void emitTimeSelection(); void extractSymbols(std::shared_ptr src, bool toClipboard); + void feedSymbolsToExternalProgram(std::shared_ptr src); void exportSamples(std::shared_ptr src); template void exportSamples(std::shared_ptr src); int plotsHeight(); @@ -110,3 +111,5 @@ public slots: int sampleToColumn(size_t sample); size_t columnToSample(int col); }; + + diff --git a/src/spectrogramcontrols.cpp b/src/spectrogramcontrols.cpp index c8cc6b1f..7d5239ca 100644 --- a/src/spectrogramcontrols.cpp +++ b/src/spectrogramcontrols.cpp @@ -157,6 +157,9 @@ void SpectrogramControls::setDefaults() powerMaxSlider->setValue(settings.value("PowerMax", 0).toInt()); powerMinSlider->setValue(settings.value("PowerMin", -100).toInt()); zoomLevelSlider->setValue(settings.value("ZoomLevel", 0).toInt()); + + int savedFreq = settings.value("CenterFrequency", 0).toInt(); + centerFrequency->setText(QString::number(savedFreq)); } void SpectrogramControls::fftOrZoomChanged(void) diff --git a/src/symbolprogoutput.cpp b/src/symbolprogoutput.cpp new file mode 100644 index 00000000..a514bffd --- /dev/null +++ b/src/symbolprogoutput.cpp @@ -0,0 +1,47 @@ +/* + * symbolprogoutput.cpp, part of the Inspectrum project + * + * Created on: Aug 27, 2025 + * Author: Pat Deegan + * Copyright (C) 2025 Pat Deegan, https://psychogenic.com + */ + +#include "symbolprogoutput.h" +SymbolProgOutput::SymbolProgOutput(const QString &output, QWidget *parent, size_t width, size_t height) : + QDialog(parent) +{ + setWindowTitle("Program Output"); + setMinimumSize(width, height); + + // Create layout + QVBoxLayout *layout = new QVBoxLayout(this); + + // Create text edit for output (scrollable) + textEdit = new QTextEdit(this); + textEdit->setReadOnly(true); // Prevent editing + textEdit->setText(output); // Set the output text + layout->addWidget(textEdit); + + // Create buttons + QHBoxLayout *buttonLayout = new QHBoxLayout(); + QPushButton *copyButton = new QPushButton("Copy to Clipboard", this); + QPushButton *closeButton = new QPushButton("Close", this); + buttonLayout->addWidget(copyButton); + buttonLayout->addWidget(closeButton); + layout->addLayout(buttonLayout); + + // Connect buttons + connect(copyButton, &QPushButton::clicked, this, + &SymbolProgOutput::copyToClipboard); + connect(closeButton, &QPushButton::clicked, this, &QDialog::accept); + + // Make dialog resizable + setSizeGripEnabled(true); +} + +void SymbolProgOutput::copyToClipboard() { + textEdit->selectAll(); + textEdit->copy(); + textEdit->textCursor().clearSelection(); // Deselect after copying +} + diff --git a/src/symbolprogoutput.h b/src/symbolprogoutput.h new file mode 100644 index 00000000..607c357b --- /dev/null +++ b/src/symbolprogoutput.h @@ -0,0 +1,27 @@ +/* + * symbolprogoutput.h, part of the Inspectrum project + * + * Created on: Aug 27, 2025 + * Author: Pat Deegan + * Copyright (C) 2025 Pat Deegan, https://psychogenic.com + */ + +#pragma once +#include +#include +#include +#include + +class SymbolProgOutput : public QDialog +{ + Q_OBJECT +public: + SymbolProgOutput(const QString& output, QWidget* parent = nullptr, size_t width = 700, size_t height = 500); + +private slots: + void copyToClipboard(); + +private: + QTextEdit* textEdit; +}; + From 8d7bb0142c5be2bda08c52a1158e092e1c43aed4 Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Wed, 27 Aug 2025 18:50:24 -0400 Subject: [PATCH 05/10] scroll to... scroll. With SHIFT modifier, page scroll --- src/plotview.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/plotview.cpp b/src/plotview.cpp index 7e79b65c..939e390d 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -283,10 +283,17 @@ bool PlotView::viewportEvent(QEvent *event) { // Handle wheel events for zooming (before the parent's handler to stop normal scrolling) if (event->type() == QEvent::Wheel) { QWheelEvent *wheelEvent = (QWheelEvent*)event; - if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + int delta = wheelEvent->angleDelta().y(); + + if (QApplication::keyboardModifiers() & Qt::ShiftModifier) { + QScrollBar *scrollBar = horizontalScrollBar(); + scrollBar->setValue(scrollBar->value() + scrollBar->pageStep() * -1 * delta); + + return true; + + } else if (QApplication::keyboardModifiers() & Qt::ControlModifier) { bool canZoomIn = zoomLevel < fftSize; bool canZoomOut = zoomLevel > 1; - int delta = wheelEvent->angleDelta().y(); if ((delta > 0 && canZoomIn) || (delta < 0 && canZoomOut)) { scrollZoomStepsAccumulated += delta; @@ -307,6 +314,9 @@ bool PlotView::viewportEvent(QEvent *event) { } } return true; + } else { + QScrollBar *scrollBar = horizontalScrollBar(); + scrollBar->setValue(scrollBar->value() - delta); } } From 4d9db91e923497b17cc4d71d1d57252302c128dc Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Thu, 28 Aug 2025 15:12:26 -0400 Subject: [PATCH 06/10] Squelch for frequency plots --- src/frequencydemod.cpp | 41 ++++++++++++++++++++++++++++++++++++- src/mainwindow.cpp | 1 + src/plotview.cpp | 7 +++++++ src/plotview.h | 2 ++ src/spectrogramcontrols.cpp | 15 ++++++++++++++ src/spectrogramcontrols.h | 2 ++ src/spectrogramplot.cpp | 11 ++++++++++ src/spectrogramplot.h | 2 ++ 8 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/frequencydemod.cpp b/src/frequencydemod.cpp index 17bdf6bf..93d40a2c 100644 --- a/src/frequencydemod.cpp +++ b/src/frequencydemod.cpp @@ -20,6 +20,8 @@ #include "frequencydemod.h" #include #include "util.h" +#include + FrequencyDemod::FrequencyDemod(std::shared_ptr>> src) : SampleBuffer(src) { @@ -28,11 +30,48 @@ FrequencyDemod::FrequencyDemod(std::shared_ptr> void FrequencyDemod::work(void *input, void *output, int count, size_t sampleid) { + float power_window[10]; + unsigned int window_size = 10; + unsigned int window_index = 0; + float power_sum = 0.0f; + float avg_power = 0.0f; + auto in = static_cast*>(input); auto out = static_cast(output); freqdem fdem = freqdem_create(relativeBandwidth() / 2.0); + + QSettings settings; + float squelch_threshold = 100.0 * settings.value("Squelch", 0).toInt(); + bool using_squelch = (squelch_threshold > 1) ? true : false; + + if (using_squelch) { + // Initialize power window + for (unsigned int i = 0; i < window_size; i++) { + power_window[i] = 0.0f; + } + } + + for (int i = 0; i < count; i++) { - freqdem_demodulate(fdem, in[i], &out[i]); + + if (using_squelch) { + float power = in[i].real() * in[i].real() + + in[i].imag() * in[i].imag(); + // Update power averaging window + power_sum -= power_window[window_index]; // Subtract oldest power + power_window[window_index] = power; // Add new power + power_sum += power; // Update sum + window_index = (window_index + 1) % window_size; // Circular buffer index + // Compute average power + avg_power = power_sum / window_size; + } + // Check if average power exceeds squelch threshold + + if ( (!using_squelch) || (avg_power > squelch_threshold)) { + freqdem_demodulate(fdem, in[i], &out[i]); + } else { + out[i] = 0; + } } freqdem_destroy(fdem); } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5ff207eb..382b6893 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -50,6 +50,7 @@ MainWindow::MainWindow() connect(dock, static_cast(&SpectrogramControls::fftOrZoomChanged), plots, &PlotView::setFFTAndZoom); connect(dock->powerMaxSlider, &QSlider::valueChanged, plots, &PlotView::setPowerMax); connect(dock->powerMinSlider, &QSlider::valueChanged, plots, &PlotView::setPowerMin); + connect(dock->squelchSlider, &QSlider::valueChanged, plots, &PlotView::setSquelch); connect(dock->cursorsCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableCursors); connect(dock->cursorsFreezeCheckBox, &QCheckBox::stateChanged, plots, &PlotView::freezeCursors); connect(dock->scalesCheckBox, &QCheckBox::stateChanged, plots, &PlotView::enableScales); diff --git a/src/plotview.cpp b/src/plotview.cpp index 939e390d..34ace97d 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -621,6 +621,13 @@ void PlotView::setPowerMin(int power) updateView(); } +void PlotView::setSquelch(int sq) { + squelch = sq; + if (spectrogramPlot != nullptr) + spectrogramPlot->setSquelch(sq); + updateView(); +} + void PlotView::setPowerMax(int power) { powerMax = power; diff --git a/src/plotview.h b/src/plotview.h index dc9b18f4..643e0073 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -60,6 +60,7 @@ public slots: void setFFTAndZoom(int fftSize, int zoomLevel); void setPowerMin(int power); void setPowerMax(int power); + void setSquelch(int squelch); protected: void mouseMoveEvent(QMouseEvent *event) override; @@ -87,6 +88,7 @@ public slots: int zoomLevel = 1; int powerMin; int powerMax; + int squelch; bool cursorsEnabled; FrozenCursors cursorsFrozen; double sampleRate = 0.0; diff --git a/src/spectrogramcontrols.cpp b/src/spectrogramcontrols.cpp index 7d5239ca..9cc4372f 100644 --- a/src/spectrogramcontrols.cpp +++ b/src/spectrogramcontrols.cpp @@ -74,6 +74,12 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent powerMinSlider->setRange(-140, 10); layout->addRow(new QLabel(tr("Power min:")), powerMinSlider); + squelchSlider = new QSlider(Qt::Horizontal, widget); + squelchSlider->setRange(0, 10); + layout->addRow(new QLabel(tr("Squelch:")), squelchSlider); + + + scalesCheckBox = new QCheckBox(widget); scalesCheckBox->setCheckState(Qt::Checked); layout->addRow(new QLabel(tr("Scales:")), scalesCheckBox); @@ -122,6 +128,7 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent connect(cursorsCheckBox, &QCheckBox::stateChanged, this, &SpectrogramControls::cursorsStateChanged); connect(powerMinSlider, &QSlider::valueChanged, this, &SpectrogramControls::powerMinChanged); connect(powerMaxSlider, &QSlider::valueChanged, this, &SpectrogramControls::powerMaxChanged); + connect(squelchSlider, &QSlider::valueChanged, this, &SpectrogramControls::squelchChanged); } void SpectrogramControls::clearCursorLabels() @@ -156,6 +163,7 @@ void SpectrogramControls::setDefaults() fftSizeSlider->setValue(settings.value("FFTSize", 9).toInt()); powerMaxSlider->setValue(settings.value("PowerMax", 0).toInt()); powerMinSlider->setValue(settings.value("PowerMin", -100).toInt()); + squelchSlider->setValue(settings.value("Squelch", 0).toInt()); zoomLevelSlider->setValue(settings.value("ZoomLevel", 0).toInt()); int savedFreq = settings.value("CenterFrequency", 0).toInt(); @@ -189,6 +197,13 @@ void SpectrogramControls::powerMinChanged(int value) settings.setValue("PowerMin", value); } +void SpectrogramControls::squelchChanged(int value) +{ + QSettings settings; + settings.setValue("Squelch", value); +} + + void SpectrogramControls::powerMaxChanged(int value) { QSettings settings; diff --git a/src/spectrogramcontrols.h b/src/spectrogramcontrols.h index 92d54a84..5f5949e0 100644 --- a/src/spectrogramcontrols.h +++ b/src/spectrogramcontrols.h @@ -51,6 +51,7 @@ private slots: void zoomLevelChanged(int value); void powerMinChanged(int value); void powerMaxChanged(int value); + void squelchChanged(int value); void fileOpenButtonClicked(); void cursorsStateChanged(int state); @@ -68,6 +69,7 @@ private slots: QSlider *zoomLevelSlider; QSlider *powerMaxSlider; QSlider *powerMinSlider; + QSlider *squelchSlider; QCheckBox *cursorsCheckBox; QSpinBox *cursorSymbolsSpinBox; QCheckBox *cursorsFreezeCheckBox; diff --git a/src/spectrogramplot.cpp b/src/spectrogramplot.cpp index 088c7de1..ec2d6dac 100644 --- a/src/spectrogramplot.cpp +++ b/src/spectrogramplot.cpp @@ -137,13 +137,17 @@ void SpectrogramPlot::paintFrequencyScale(QPainter &painter, QRect &rect) int tickny = plotHeight / 2 + tick / bwPerPixel + y; int64_t tfreq = (bottomFreq + tick)/divisor; + /* painter.drawLine(0, tickpy, 30, tickpy); if (!inputSource->realSignal()) painter.drawLine(0, tickny, 30, tickny); + */ if (tfreq != 0) { char buf[128]; snprintf(buf, sizeof(buf), "%li %s",tfreq, suffixBuf); + + painter.drawLine(0, ticky, 40, ticky); if (tfreq > centerFrequency/divisor) { painter.drawText(5, ticky + 15, buf); @@ -416,6 +420,13 @@ void SpectrogramPlot::setPowerMin(int power) pixmapCache.clear(); } +void SpectrogramPlot::setSquelch(int sq) +{ + squelch = sq; + pixmapCache.clear(); + + tunerMoved(); +} void SpectrogramPlot::setZoomLevel(int zoom) { zoomLevel = zoom; diff --git a/src/spectrogramplot.h b/src/spectrogramplot.h index dd806fbb..23243c76 100644 --- a/src/spectrogramplot.h +++ b/src/spectrogramplot.h @@ -60,6 +60,7 @@ public slots: void setFFTSize(int size); void setPowerMax(int power); void setPowerMin(int power); + void setSquelch(int power); void setZoomLevel(int zoom); void tunerMoved(); @@ -79,6 +80,7 @@ public slots: int zoomLevel; float powerMax; float powerMin; + float squelch; double sampleRate; double centerFrequency; bool frequencyScaleEnabled; From efda2b52c3bda0c02c404e3007a9e4d470126244 Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Thu, 28 Aug 2025 17:09:40 -0400 Subject: [PATCH 07/10] Hotkeys: precise cursor settings, instant frequency plot, key scrolling (2 speeds) and program exit --- src/mainwindow.cpp | 16 ++++++++++++ src/mainwindow.h | 1 + src/plotview.cpp | 64 +++++++++++++++++++++++++++++++++++++++++++++- src/plotview.h | 3 ++- 4 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 382b6893..6f553035 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -112,6 +112,22 @@ void MainWindow::openFile(QString fileName) } } +void MainWindow::keyPressEvent(QKeyEvent *event) { + switch (event->key()) { + case Qt::Key_C: + dock->cursorsCheckBox->setChecked(! plots->cursorsAreEnabled()); + break; + case Qt::Key_W: + if (QApplication::keyboardModifiers() & Qt::ControlModifier) { + QApplication::closeAllWindows(); + } + break; + + default: + QMainWindow::keyPressEvent(event); // Pass to base class + } +} + void MainWindow::invalidateEvent() { plots->setSampleRate(input->rate()); diff --git a/src/mainwindow.h b/src/mainwindow.h index 346cbe59..eca1960b 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -31,6 +31,7 @@ class MainWindow : public QMainWindow, Subscriber public: MainWindow(); void changeSampleRate(double rate); + void keyPressEvent(QKeyEvent *event) override; public slots: void openFile(QString fileName); diff --git a/src/plotview.cpp b/src/plotview.cpp index 34ace97d..65efc117 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -278,7 +278,69 @@ void PlotView::enableCursors(bool enabled) } viewport()->update(); } - +void PlotView::keyPressEvent(QKeyEvent *event) { + bool shiftMod = QApplication::keyboardModifiers() & Qt::ShiftModifier; + bool ctrlMod = QApplication::keyboardModifiers() & Qt::ControlModifier; + QScrollBar *scrollBar = horizontalScrollBar(); + std::shared_ptr plot_src; + + switch (event->key()) { + case Qt::Key_Left: + if (ctrlMod) { + scrollBar->setValue(scrollBar->value() + scrollBar->pageStep() * -1); + } else { + scrollBar->setValue(scrollBar->value() + scrollBar->singleStep() * -1); + } + + break; + case Qt::Key_Right: + if (ctrlMod) { + scrollBar->setValue(scrollBar->value() + scrollBar->pageStep()); + } else { + scrollBar->setValue(scrollBar->value() + scrollBar->singleStep()); + } + break; + + case Qt::Key_Up: + if (! (cursorsEnabled || ctrlMod) ) { + QGraphicsView::keyPressEvent(event); // Pass to base class + break; + } + if (shiftMod) + cursors.setSelection({cursors.selection().minimum,cursors.selection().maximum+10}); + else + cursors.setSelection({cursors.selection().minimum,cursors.selection().maximum+1}); + + cursorsMoved(); + break; + case Qt::Key_Down: + if (! (cursorsEnabled || ctrlMod) ) { + QGraphicsView::keyPressEvent(event); // Pass to base class + break; + } + if (shiftMod) { + if (cursors.selection().maximum - cursors.selection().minimum > 10) + cursors.setSelection({cursors.selection().minimum,cursors.selection().maximum-10}); + } else { + if (cursors.selection().maximum - cursors.selection().minimum > 2) + cursors.setSelection({cursors.selection().minimum,cursors.selection().maximum-1}); + } + + + cursorsMoved(); + break; + case Qt::Key_F: + if (spectrogramPlot->tunerEnabled()) { + break; + } + plot_src = spectrogramPlot->output(); + addPlot(Plots::frequencyPlot(plot_src)); + repaint(); + break; + default: + QGraphicsView::keyPressEvent(event); // Pass to base class + } +} bool PlotView::viewportEvent(QEvent *event) { // Handle wheel events for zooming (before the parent's handler to stop normal scrolling) if (event->type() == QEvent::Wheel) { diff --git a/src/plotview.h b/src/plotview.h index 643e0073..ae0639c5 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -41,7 +41,8 @@ class PlotView : public QGraphicsView, Subscriber PlotView(InputSource *input); void setSampleRate(double rate); void setCenterFrequency(double freq); - + void keyPressEvent(QKeyEvent *event) override; + bool cursorsAreEnabled() { return cursorsEnabled;} signals: void timeSelectionChanged(float time); void zoomIn(); From 93c68aa03c8fd79ed2755d2d44c4788fa6ded1de Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Thu, 28 Aug 2025 18:17:19 -0400 Subject: [PATCH 08/10] Time display and delta calculation: ctrl click and drag/release, or ctrl click then ctrl+shift click to see time calculations --- src/mainwindow.cpp | 3 +++ src/plotview.cpp | 17 ++++++++++++++ src/plotview.h | 2 ++ src/spectrogramcontrols.cpp | 44 +++++++++++++++++++++++++++++++++++++ src/spectrogramcontrols.h | 10 +++++++++ 5 files changed, 76 insertions(+) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 6f553035..b0b40480 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -63,6 +63,9 @@ MainWindow::MainWindow() connect(plots, &PlotView::timeSelectionChanged, dock, &SpectrogramControls::timeSelectionChanged); connect(plots, &PlotView::zoomIn, dock, &SpectrogramControls::zoomIn); connect(plots, &PlotView::zoomOut, dock, &SpectrogramControls::zoomOut); + connect(plots, &PlotView::coordinateClick, dock, &SpectrogramControls::coordinateClick); + + void coordinateClick(double time_position, double frequency); // Set defaults after making connections so everything is in sync dock->setDefaults(); diff --git a/src/plotview.cpp b/src/plotview.cpp index 65efc117..ca36e277 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -343,6 +343,7 @@ void PlotView::keyPressEvent(QKeyEvent *event) { } bool PlotView::viewportEvent(QEvent *event) { // Handle wheel events for zooming (before the parent's handler to stop normal scrolling) + if (event->type() == QEvent::Wheel) { QWheelEvent *wheelEvent = (QWheelEvent*)event; int delta = wheelEvent->angleDelta().y(); @@ -390,7 +391,23 @@ bool PlotView::viewportEvent(QEvent *event) { QMouseEvent *mouseEvent = static_cast(event); + int plotY = -verticalScrollBar()->value(); + + if ( (QApplication::keyboardModifiers() & Qt::ControlModifier) && + (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)) { + double clickTime = (columnToSample(mouseEvent->pos().x()) + viewRange.minimum) / sampleRate; + + // don't know how to translate mouse coords to frequency: TODO:FIXME -- setting at 0 for now + if (QApplication::keyboardModifiers() & Qt::ShiftModifier) { + emit coordinateClick(clickTime, 0, false); + } else { + emit coordinateClick(clickTime, 0, event->type() == QEvent::MouseButtonPress); + } + return true; + } + + for (auto&& plot : plots) { bool result = plot->mouseEvent( event->type(), diff --git a/src/plotview.h b/src/plotview.h index ae0639c5..cda6af55 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -47,6 +47,8 @@ class PlotView : public QGraphicsView, Subscriber void timeSelectionChanged(float time); void zoomIn(); void zoomOut(); + void coordinateClick(double time_position, double frequency, bool down); + public slots: void cursorsMoved(); diff --git a/src/spectrogramcontrols.cpp b/src/spectrogramcontrols.cpp index 9cc4372f..e99af34b 100644 --- a/src/spectrogramcontrols.cpp +++ b/src/spectrogramcontrols.cpp @@ -119,6 +119,25 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent commentsCheckBox = new QCheckBox(widget); layout->addRow(new QLabel(tr("Display annotation comments tooltips:")), commentsCheckBox); + + + // SigMF selection settings + layout->addRow(new QLabel()); // TODO: find a better way to add an empty row? + layout->addRow(new QLabel(tr("Time (ctrl-click, drag)"))); + + startTimeLabel = new QLabel(); + layout->addRow(new QLabel(tr("Start:")), startTimeLabel); + endTimeLabel = new QLabel(); + layout->addRow(new QLabel(tr("End:")), endTimeLabel); + deltaTimeLabel = new QLabel(); + layout->addRow(new QLabel(tr("Delta:")), deltaTimeLabel); + + + + + + + widget->setLayout(layout); setWidget(widget); @@ -258,6 +277,31 @@ void SpectrogramControls::timeSelectionChanged(float time) } } +void SpectrogramControls::coordinateClick(double time_pos, double freq_pos, bool down) { + + if (down) { + endTimeLabel->setText(""); + deltaTimeLabel->setText(""); + startTimeLabel->setText(QString::fromStdString(formatSIValue(time_pos)) + "s"); + startTime = time_pos; + + } else { + endTime = time_pos; + if (endTime == startTime) { + deltaTimeLabel->setText(""); + endTimeLabel->setText(""); + } else { + if (endTime < startTime) { + deltaTimeLabel->setText(QString("-") + QString::fromStdString(formatSIValue(startTime - endTime)) + "s"); + } else { + deltaTimeLabel->setText(QString::fromStdString(formatSIValue(endTime - startTime)) + "s"); + } + endTimeLabel->setText(QString::fromStdString(formatSIValue(time_pos)) + "s"); + } + } +} + + void SpectrogramControls::zoomIn() { zoomLevelSlider->setValue(zoomLevelSlider->value() + 1); diff --git a/src/spectrogramcontrols.h b/src/spectrogramcontrols.h index 5f5949e0..151be529 100644 --- a/src/spectrogramcontrols.h +++ b/src/spectrogramcontrols.h @@ -45,6 +45,8 @@ public slots: void zoomIn(); void zoomOut(); void enableAnnotations(bool enabled); + void coordinateClick(double time_pos, double freq_pos, bool down); + private slots: void fftSizeChanged(int value); @@ -77,6 +79,14 @@ private slots: QLabel *periodLabel; QLabel *symbolRateLabel; QLabel *symbolPeriodLabel; + + QLabel *startTimeLabel; + double startTime; + double endTime; + QLabel *endTimeLabel; + QLabel *deltaTimeLabel; + + QCheckBox *scalesCheckBox; QCheckBox *annosCheckBox; QCheckBox *commentsCheckBox; From b5a87e611fd71e0d390c4ff1e764ed0c787db294 Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Fri, 29 Aug 2025 03:33:49 -0400 Subject: [PATCH 09/10] squelch improvement, better range --- src/frequencydemod.cpp | 13 +++++++------ src/spectrogramcontrols.cpp | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/frequencydemod.cpp b/src/frequencydemod.cpp index 93d40a2c..9abdc692 100644 --- a/src/frequencydemod.cpp +++ b/src/frequencydemod.cpp @@ -30,19 +30,20 @@ FrequencyDemod::FrequencyDemod(std::shared_ptr> void FrequencyDemod::work(void *input, void *output, int count, size_t sampleid) { - float power_window[10]; + double power_window[10]; unsigned int window_size = 10; unsigned int window_index = 0; - float power_sum = 0.0f; - float avg_power = 0.0f; + double power_sum = 0.0f; + double avg_power = 0.0f; auto in = static_cast*>(input); auto out = static_cast(output); freqdem fdem = freqdem_create(relativeBandwidth() / 2.0); QSettings settings; - float squelch_threshold = 100.0 * settings.value("Squelch", 0).toInt(); - bool using_squelch = (squelch_threshold > 1) ? true : false; + int sqval = settings.value("Squelch", 0).toInt(); + double squelch_threshold = pow(2, sqval+2); // 100.0 * settings.value("Squelch", 0).toInt(); + bool using_squelch = sqval ? true : false; if (using_squelch) { // Initialize power window @@ -55,7 +56,7 @@ void FrequencyDemod::work(void *input, void *output, int count, size_t sampleid) for (int i = 0; i < count; i++) { if (using_squelch) { - float power = in[i].real() * in[i].real() + double power = in[i].real() * in[i].real() + in[i].imag() * in[i].imag(); // Update power averaging window power_sum -= power_window[window_index]; // Subtract oldest power diff --git a/src/spectrogramcontrols.cpp b/src/spectrogramcontrols.cpp index e99af34b..cf706150 100644 --- a/src/spectrogramcontrols.cpp +++ b/src/spectrogramcontrols.cpp @@ -75,7 +75,7 @@ SpectrogramControls::SpectrogramControls(const QString & title, QWidget * parent layout->addRow(new QLabel(tr("Power min:")), powerMinSlider); squelchSlider = new QSlider(Qt::Horizontal, widget); - squelchSlider->setRange(0, 10); + squelchSlider->setRange(0, 21); layout->addRow(new QLabel(tr("Squelch:")), squelchSlider); From bd746e0402c6818c55145748d24b6aa79486bffb Mon Sep 17 00:00:00 2001 From: Pat Deegan Date: Fri, 29 Aug 2025 03:34:34 -0400 Subject: [PATCH 10/10] Repeat hotkey for feeding symbols to external script --- src/plotview.cpp | 81 ++++++++++++++++++++++++++++++------------------ src/plotview.h | 6 +++- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/plotview.cpp b/src/plotview.cpp index ca36e277..04dd47a3 100644 --- a/src/plotview.cpp +++ b/src/plotview.cpp @@ -131,6 +131,7 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) // that are compatible with selectedPlot's output QMenu *plotsMenu = menu.addMenu("Add derived plot"); auto src = selectedPlot->output(); + last_src_used = src; auto compatiblePlots = as_range(Plots::plots.equal_range(src->sampleType())); for (auto p : compatiblePlots) { auto plotInfo = p.second; @@ -165,7 +166,7 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) connect( runProgramAction, &QAction::triggered, this, [=]() { - feedSymbolsToExternalProgram(src); + selectAndFeedExternalProgram(src); } ); @@ -207,6 +208,7 @@ void PlotView::contextMenuEvent(QContextMenuEvent * event) connect( rem, &QAction::triggered, this, [=]() { + last_src_used = nullptr; plots.erase(it); } ); @@ -279,6 +281,9 @@ void PlotView::enableCursors(bool enabled) viewport()->update(); } void PlotView::keyPressEvent(QKeyEvent *event) { + + QSettings settings; + bool shiftMod = QApplication::keyboardModifiers() & Qt::ShiftModifier; bool ctrlMod = QApplication::keyboardModifiers() & Qt::ControlModifier; QScrollBar *scrollBar = horizontalScrollBar(); @@ -337,6 +342,17 @@ void PlotView::keyPressEvent(QKeyEvent *event) { addPlot(Plots::frequencyPlot(plot_src)); repaint(); break; + case Qt::Key_R: + if (last_src_used && cursorsEnabled) { + + QString lastProgramPath = settings.value("lastProgramPath", QString("")).toString(); + if (lastProgramPath != "") { + feedSymbolsToExternalProgram(lastProgramPath, last_src_used); + } + } + break; + + default: QGraphicsView::keyPressEvent(event); // Pass to base class } @@ -442,37 +458,15 @@ QByteArray vectorToTextByteArray(const std::vector& data) return text.toUtf8(); // Convert to QByteArray with UTF-8 encoding } -void PlotView::feedSymbolsToExternalProgram(std::shared_ptr src) { +void PlotView::feedSymbolsToExternalProgram(QString programPath, std::shared_ptr src) { if (!cursorsEnabled) return; - // Step 1: Open a file dialog to select the program - QFileDialog dialog(this); - dialog.setFileMode(QFileDialog::ExistingFile); - dialog.setWindowTitle("Select External Program"); - - - QSettings settings; - QString lastProgramPath = settings.value("lastProgramPath", QDir::homePath()).toString(); - dialog.selectFile(lastProgramPath); - // Optional: Filter for executable files (platform-specific) -#ifdef Q_OS_WIN - dialog.setNameFilter("Executables (*.exe);;All Files (*)"); -#else - dialog.setNameFilter("All Files (*)"); -#endif - - if (dialog.exec() != QDialog::Accepted || dialog.selectedFiles().isEmpty()) { - return; // User canceled or didn't select a file - } - - QString programPath = dialog.selectedFiles().first(); - - // Save the selected program path to QSettings - settings.setValue("lastProgramPath", programPath); + auto floatSrc = std::dynamic_pointer_cast>(src); + if (!floatSrc) + return; - // Step 2: Execute the program and pipe data QProcess process(this); // Configure the process to allow writing to stdin @@ -485,9 +479,6 @@ void PlotView::feedSymbolsToExternalProgram(std::shared_ptr>(src); - if (!floatSrc) - return; auto samples = floatSrc->getSamples(selectedSamples.minimum, selectedSamples.length()); auto step = (float)selectedSamples.length() / cursors.segments(); auto symbols = std::vector(); @@ -515,7 +506,37 @@ void PlotView::feedSymbolsToExternalProgram(std::shared_ptr src) { + + if (!cursorsEnabled) + return; + // Step 1: Open a file dialog to select the program + QFileDialog dialog(this); + dialog.setFileMode(QFileDialog::ExistingFile); + dialog.setWindowTitle("Select External Program"); + + + QSettings settings; + QString lastProgramPath = settings.value("lastProgramPath", QDir::homePath()).toString(); + dialog.selectFile(lastProgramPath); + // Optional: Filter for executable files (platform-specific) +#ifdef Q_OS_WIN + dialog.setNameFilter("Executables (*.exe);;All Files (*)"); +#else + dialog.setNameFilter("All Files (*)"); +#endif + + if (dialog.exec() != QDialog::Accepted || dialog.selectedFiles().isEmpty()) { + return; // User canceled or didn't select a file + } + + QString programPath = dialog.selectedFiles().first(); + + // Save the selected program path to QSettings + settings.setValue("lastProgramPath", programPath); + feedSymbolsToExternalProgram(programPath, src); } void PlotView::extractSymbols(std::shared_ptr src, diff --git a/src/plotview.h b/src/plotview.h index cda6af55..7abae2ee 100644 --- a/src/plotview.h +++ b/src/plotview.h @@ -99,11 +99,15 @@ public slots: bool timeScaleEnabled; int scrollZoomStepsAccumulated = 0; bool annotationCommentsEnabled; + std::shared_ptr last_src_used; + void addPlot(Plot *plot); void emitTimeSelection(); void extractSymbols(std::shared_ptr src, bool toClipboard); - void feedSymbolsToExternalProgram(std::shared_ptr src); + + void selectAndFeedExternalProgram(std::shared_ptr src); + void feedSymbolsToExternalProgram(QString programPath, std::shared_ptr src); void exportSamples(std::shared_ptr src); template void exportSamples(std::shared_ptr src); int plotsHeight();