1#!/usr/bin/env python
2
3import sys, signal, time
4
5from PyQt5 import QtCore, QtGui, QtWidgets
6
7from qspectrumanalyzer import backends
8from qspectrumanalyzer.version import __version__
9from qspectrumanalyzer.data import DataStorage
10from qspectrumanalyzer.plot import SpectrumPlotWidget, WaterfallPlotWidget
11from qspectrumanalyzer.utils import color_to_str, str_to_color
12
13from qspectrumanalyzer.ui_qspectrumanalyzer_settings import Ui_QSpectrumAnalyzerSettings
14from qspectrumanalyzer.ui_qspectrumanalyzer_settings_help import Ui_QSpectrumAnalyzerSettingsHelp
15from qspectrumanalyzer.ui_qspectrumanalyzer_smooth import Ui_QSpectrumAnalyzerSmooth
16from qspectrumanalyzer.ui_qspectrumanalyzer_persistence import Ui_QSpectrumAnalyzerPersistence
17from qspectrumanalyzer.ui_qspectrumanalyzer_colors import Ui_QSpectrumAnalyzerColors
18from qspectrumanalyzer.ui_qspectrumanalyzer import Ui_QSpectrumAnalyzerMainWindow
19
20# Allow CTRL+C and/or SIGTERM to kill us (PyQt blocks it otherwise)
21signal.signal(signal.SIGINT, signal.SIG_DFL)
22signal.signal(signal.SIGTERM, signal.SIG_DFL)
23
24
25class QSpectrumAnalyzerSettings(QtWidgets.QDialog, Ui_QSpectrumAnalyzerSettings):
26    """QSpectrumAnalyzer settings dialog"""
27    def __init__(self, parent=None):
28        # Initialize UI
29        super().__init__(parent)
30        self.setupUi(self)
31        self.params_help_dialog = None
32        self.device_help_dialog = None
33
34        # Load settings
35        settings = QtCore.QSettings()
36        self.executableEdit.setText(settings.value("executable", "soapy_power"))
37        self.deviceEdit.setText(settings.value("device", ""))
38        self.lnbSpinBox.setValue(settings.value("lnb_lo", 0, float) / 1e6)
39        self.waterfallHistorySizeSpinBox.setValue(settings.value("waterfall_history_size", 100, int))
40
41        backend = settings.value("backend", "soapy_power")
42        try:
43            backend_module = getattr(backends, backend)
44        except AttributeError:
45            backend_module = backends.soapy_power
46
47        self.paramsEdit.setText(settings.value("params", backend_module.Info.additional_params))
48        self.deviceHelpButton.setEnabled(bool(backend_module.Info.help_device))
49
50        self.sampleRateSpinBox.setMinimum(backend_module.Info.sample_rate_min / 1e6)
51        self.sampleRateSpinBox.setMaximum(backend_module.Info.sample_rate_max / 1e6)
52        self.sampleRateSpinBox.setValue(settings.value("sample_rate", backend_module.Info.sample_rate, float) / 1e6)
53
54        self.bandwidthSpinBox.setMinimum(backend_module.Info.bandwidth_min / 1e6)
55        self.bandwidthSpinBox.setMaximum(backend_module.Info.bandwidth_max / 1e6)
56        self.bandwidthSpinBox.setValue(settings.value("bandwidth", backend_module.Info.bandwidth, float) / 1e6)
57
58        self.backendComboBox.blockSignals(True)
59        self.backendComboBox.clear()
60        for b in sorted(backends.__all__):
61            self.backendComboBox.addItem(b)
62
63        i = self.backendComboBox.findText(backend)
64        if i == -1:
65            self.backendComboBox.setCurrentIndex(0)
66        else:
67            self.backendComboBox.setCurrentIndex(i)
68        self.backendComboBox.blockSignals(False)
69
70    @QtCore.pyqtSlot()
71    def on_executableButton_clicked(self):
72        """Open file dialog when button is clicked"""
73        filename = QtWidgets.QFileDialog.getOpenFileName(self, self.tr("Select executable - QSpectrumAnalyzer"))[0]
74        if filename:
75            self.executableEdit.setText(filename)
76
77    @QtCore.pyqtSlot()
78    def on_paramsHelpButton_clicked(self):
79        """Open additional parameters help dialog when button is clicked"""
80        try:
81            backend_module = getattr(backends, self.backendComboBox.currentText())
82        except AttributeError:
83            backend_module = backends.soapy_power
84
85        self.params_help_dialog = QSpectrumAnalyzerSettingsHelp(
86            backend_module.Info.help_params(self.executableEdit.text()),
87            parent=self
88        )
89
90        self.params_help_dialog.show()
91        self.params_help_dialog.raise_()
92        self.params_help_dialog.activateWindow()
93
94    @QtCore.pyqtSlot()
95    def on_deviceHelpButton_clicked(self):
96        """Open device help dialog when button is clicked"""
97        try:
98            backend_module = getattr(backends, self.backendComboBox.currentText())
99        except AttributeError:
100            backend_module = backends.soapy_power
101
102        self.device_help_dialog = QSpectrumAnalyzerSettingsHelp(
103            backend_module.Info.help_device(self.executableEdit.text(), self.deviceEdit.text()),
104            parent=self
105        )
106
107        self.device_help_dialog.show()
108        self.device_help_dialog.raise_()
109        self.device_help_dialog.activateWindow()
110
111    @QtCore.pyqtSlot(str)
112    def on_backendComboBox_currentIndexChanged(self, text):
113        """Change executable when backend is changed"""
114        self.executableEdit.setText(text)
115        self.deviceEdit.setText("")
116
117        try:
118            backend_module = getattr(backends, text)
119        except AttributeError:
120            backend_module = backends.soapy_power
121
122        self.paramsEdit.setText(backend_module.Info.additional_params)
123        self.deviceHelpButton.setEnabled(bool(backend_module.Info.help_device))
124        self.sampleRateSpinBox.setMinimum(backend_module.Info.sample_rate_min / 1e6)
125        self.sampleRateSpinBox.setMaximum(backend_module.Info.sample_rate_max / 1e6)
126        self.sampleRateSpinBox.setValue(backend_module.Info.sample_rate / 1e6)
127        self.bandwidthSpinBox.setMinimum(backend_module.Info.bandwidth_min / 1e6)
128        self.bandwidthSpinBox.setMaximum(backend_module.Info.bandwidth_max / 1e6)
129        self.bandwidthSpinBox.setValue(backend_module.Info.bandwidth / 1e6)
130
131    def accept(self):
132        """Save settings when dialog is accepted"""
133        settings = QtCore.QSettings()
134        settings.setValue("backend", self.backendComboBox.currentText())
135        settings.setValue("executable", self.executableEdit.text())
136        settings.setValue("params", self.paramsEdit.text())
137        settings.setValue("device", self.deviceEdit.text())
138        settings.setValue("sample_rate", self.sampleRateSpinBox.value() * 1e6)
139        settings.setValue("bandwidth", self.bandwidthSpinBox.value() * 1e6)
140        settings.setValue("lnb_lo", self.lnbSpinBox.value() * 1e6)
141        settings.setValue("waterfall_history_size", self.waterfallHistorySizeSpinBox.value())
142        QtWidgets.QDialog.accept(self)
143
144
145class QSpectrumAnalyzerSettingsHelp(QtWidgets.QDialog, Ui_QSpectrumAnalyzerSettingsHelp):
146    """QSpectrumAnalyzer settings help dialog"""
147    def __init__(self, text, parent=None):
148        # Initialize UI
149        super().__init__(parent)
150        self.setupUi(self)
151
152        monospace_font = QtGui.QFont('monospace')
153        monospace_font.setStyleHint(QtGui.QFont.Monospace)
154        self.helpTextEdit.setFont(monospace_font)
155        self.helpTextEdit.setPlainText(text)
156
157
158class QSpectrumAnalyzerSmooth(QtWidgets.QDialog, Ui_QSpectrumAnalyzerSmooth):
159    """QSpectrumAnalyzer spectrum smoothing dialog"""
160    def __init__(self, parent=None):
161        # Initialize UI
162        super().__init__(parent)
163        self.setupUi(self)
164
165        # Load settings
166        settings = QtCore.QSettings()
167        self.windowLengthSpinBox.setValue(settings.value("smooth_length", 11, int))
168
169        window_function = settings.value("smooth_window", "hanning")
170        i = self.windowFunctionComboBox.findText(window_function)
171        if i == -1:
172            self.windowFunctionComboBox.setCurrentIndex(0)
173        else:
174            self.windowFunctionComboBox.setCurrentIndex(i)
175
176    def accept(self):
177        """Save settings when dialog is accepted"""
178        settings = QtCore.QSettings()
179        settings.setValue("smooth_length", self.windowLengthSpinBox.value())
180        settings.setValue("smooth_window", self.windowFunctionComboBox.currentText())
181        QtWidgets.QDialog.accept(self)
182
183
184class QSpectrumAnalyzerPersistence(QtWidgets.QDialog, Ui_QSpectrumAnalyzerPersistence):
185    """QSpectrumAnalyzer spectrum persistence dialog"""
186    def __init__(self, parent=None):
187        # Initialize UI
188        super().__init__(parent)
189        self.setupUi(self)
190
191        # Load settings
192        settings = QtCore.QSettings()
193        self.persistenceLengthSpinBox.setValue(settings.value("persistence_length", 5, int))
194
195        decay_function = settings.value("persistence_decay", "exponential")
196        i = self.decayFunctionComboBox.findText(decay_function)
197        if i == -1:
198            self.decayFunctionComboBox.setCurrentIndex(0)
199        else:
200            self.decayFunctionComboBox.setCurrentIndex(i)
201
202    def accept(self):
203        """Save settings when dialog is accepted"""
204        settings = QtCore.QSettings()
205        settings.setValue("persistence_length", self.persistenceLengthSpinBox.value())
206        settings.setValue("persistence_decay", self.decayFunctionComboBox.currentText())
207        QtWidgets.QDialog.accept(self)
208
209
210class QSpectrumAnalyzerColors(QtWidgets.QDialog, Ui_QSpectrumAnalyzerColors):
211    """QSpectrumAnalyzer colors dialog"""
212    def __init__(self, parent=None):
213        # Initialize UI
214        super().__init__(parent)
215        self.setupUi(self)
216
217        # Load settings
218        settings = QtCore.QSettings()
219        self.mainColorButton.setColor(str_to_color(settings.value("main_color", "255, 255, 0, 255")))
220        self.peakHoldMaxColorButton.setColor(str_to_color(settings.value("peak_hold_max_color", "255, 0, 0, 255")))
221        self.peakHoldMinColorButton.setColor(str_to_color(settings.value("peak_hold_min_color", "0, 0, 255, 255")))
222        self.averageColorButton.setColor(str_to_color(settings.value("average_color", "0, 255, 255, 255")))
223        self.persistenceColorButton.setColor(str_to_color(settings.value("persistence_color", "0, 255, 0, 255")))
224
225    def accept(self):
226        """Save settings when dialog is accepted"""
227        settings = QtCore.QSettings()
228        settings.setValue("main_color", color_to_str(self.mainColorButton.color()))
229        settings.setValue("peak_hold_max_color", color_to_str(self.peakHoldMaxColorButton.color()))
230        settings.setValue("peak_hold_min_color", color_to_str(self.peakHoldMinColorButton.color()))
231        settings.setValue("average_color", color_to_str(self.averageColorButton.color()))
232        settings.setValue("persistence_color", color_to_str(self.persistenceColorButton.color()))
233        QtWidgets.QDialog.accept(self)
234
235
236class QSpectrumAnalyzerMainWindow(QtWidgets.QMainWindow, Ui_QSpectrumAnalyzerMainWindow):
237    """QSpectrumAnalyzer main window"""
238    def __init__(self, parent=None):
239        # Initialize UI
240        super().__init__(parent)
241        self.setupUi(self)
242
243        # Create plot widgets and update UI
244        self.spectrumPlotWidget = SpectrumPlotWidget(self.mainPlotLayout)
245        self.waterfallPlotWidget = WaterfallPlotWidget(self.waterfallPlotLayout, self.histogramPlotLayout)
246
247        # Link main spectrum plot to waterfall plot
248        self.spectrumPlotWidget.plot.setXLink(self.waterfallPlotWidget.plot)
249
250        # Setup power thread and connect signals
251        self.prev_data_timestamp = None
252        self.data_storage = None
253        self.power_thread = None
254        self.backend = None
255        self.setup_power_thread()
256
257        self.update_buttons()
258        self.load_settings()
259
260    def setup_power_thread(self):
261        """Create power_thread and connect signals to slots"""
262        if self.power_thread:
263            self.stop()
264
265        settings = QtCore.QSettings()
266        self.data_storage = DataStorage(max_history_size=settings.value("waterfall_history_size", 100, int))
267        self.data_storage.data_updated.connect(self.update_data)
268        self.data_storage.data_updated.connect(self.spectrumPlotWidget.update_plot)
269        self.data_storage.data_updated.connect(self.spectrumPlotWidget.update_persistence)
270        self.data_storage.data_recalculated.connect(self.spectrumPlotWidget.recalculate_plot)
271        self.data_storage.data_recalculated.connect(self.spectrumPlotWidget.recalculate_persistence)
272        self.data_storage.history_updated.connect(self.waterfallPlotWidget.update_plot)
273        self.data_storage.average_updated.connect(self.spectrumPlotWidget.update_average)
274        self.data_storage.peak_hold_max_updated.connect(self.spectrumPlotWidget.update_peak_hold_max)
275        self.data_storage.peak_hold_min_updated.connect(self.spectrumPlotWidget.update_peak_hold_min)
276
277        # Setup default values and limits in case that backend is changed
278        backend = settings.value("backend", "soapy_power")
279        try:
280            backend_module = getattr(backends, backend)
281        except AttributeError:
282            backend_module = backends.soapy_power
283
284        if self.backend is None or backend != self.backend:
285            self.backend = backend
286            self.gainSpinBox.setMinimum(backend_module.Info.gain_min)
287            self.gainSpinBox.setMaximum(backend_module.Info.gain_max)
288            self.gainSpinBox.setValue(backend_module.Info.gain)
289            self.startFreqSpinBox.setMinimum(backend_module.Info.start_freq_min)
290            self.startFreqSpinBox.setMaximum(backend_module.Info.start_freq_max)
291            self.startFreqSpinBox.setValue(backend_module.Info.start_freq)
292            self.stopFreqSpinBox.setMinimum(backend_module.Info.stop_freq_min)
293            self.stopFreqSpinBox.setMaximum(backend_module.Info.stop_freq_max)
294            self.stopFreqSpinBox.setValue(backend_module.Info.stop_freq)
295            self.binSizeSpinBox.setMinimum(backend_module.Info.bin_size_min)
296            self.binSizeSpinBox.setMaximum(backend_module.Info.bin_size_max)
297            self.binSizeSpinBox.setValue(backend_module.Info.bin_size)
298            self.intervalSpinBox.setMinimum(backend_module.Info.interval_min)
299            self.intervalSpinBox.setMaximum(backend_module.Info.interval_max)
300            self.intervalSpinBox.setValue(backend_module.Info.interval)
301            self.ppmSpinBox.setMinimum(backend_module.Info.ppm_min)
302            self.ppmSpinBox.setMaximum(backend_module.Info.ppm_max)
303            self.ppmSpinBox.setValue(backend_module.Info.ppm)
304            self.cropSpinBox.setMinimum(backend_module.Info.crop_min)
305            self.cropSpinBox.setMaximum(backend_module.Info.crop_max)
306            self.cropSpinBox.setValue(backend_module.Info.crop)
307
308        # Setup default values and limits in case that LNB LO is changed
309        lnb_lo = settings.value("lnb_lo", 0, float) / 1e6
310
311        start_freq_min = backend_module.Info.start_freq_min + lnb_lo
312        start_freq_max = backend_module.Info.start_freq_max + lnb_lo
313        start_freq = self.startFreqSpinBox.value()
314        stop_freq_min = backend_module.Info.stop_freq_min + lnb_lo
315        stop_freq_max = backend_module.Info.stop_freq_max + lnb_lo
316        stop_freq = self.stopFreqSpinBox.value()
317
318        self.startFreqSpinBox.setMinimum(start_freq_min if start_freq_min > 0 else 0)
319        self.startFreqSpinBox.setMaximum(start_freq_max)
320        if start_freq < start_freq_min or start_freq > start_freq_max:
321            self.startFreqSpinBox.setValue(start_freq_min)
322
323        self.stopFreqSpinBox.setMinimum(stop_freq_min if stop_freq_min > 0 else 0)
324        self.stopFreqSpinBox.setMaximum(stop_freq_max)
325        if stop_freq < stop_freq_min or stop_freq > stop_freq_max:
326            self.stopFreqSpinBox.setValue(stop_freq_max)
327
328        self.power_thread = backend_module.PowerThread(self.data_storage)
329        self.power_thread.powerThreadStarted.connect(self.update_buttons)
330        self.power_thread.powerThreadStopped.connect(self.update_buttons)
331
332    def set_dock_size(self, dock, width, height):
333        """Ugly hack for resizing QDockWidget (because it doesn't respect minimumSize / sizePolicy set in Designer)
334           Link: https://stackoverflow.com/questions/2722939/c-resize-a-docked-qt-qdockwidget-programmatically"""
335        old_min_size = dock.minimumSize()
336        old_max_size = dock.maximumSize()
337
338        if width >= 0:
339            if dock.width() < width:
340                dock.setMinimumWidth(width)
341            else:
342                dock.setMaximumWidth(width)
343
344        if height >= 0:
345            if dock.height() < height:
346                dock.setMinimumHeight(height)
347            else:
348                dock.setMaximumHeight(height)
349
350        QtCore.QTimer.singleShot(0, lambda: self.set_dock_size_callback(dock, old_min_size, old_max_size))
351
352    def set_dock_size_callback(self, dock, old_min_size, old_max_size):
353        """Return to original QDockWidget minimumSize and maximumSize after running set_dock_size()"""
354        dock.setMinimumSize(old_min_size)
355        dock.setMaximumSize(old_max_size)
356
357    def load_settings(self):
358        """Restore spectrum analyzer settings and window geometry"""
359        settings = QtCore.QSettings()
360        self.startFreqSpinBox.setValue(settings.value("start_freq", 87.0, float))
361        self.stopFreqSpinBox.setValue(settings.value("stop_freq", 108.0, float))
362        self.binSizeSpinBox.setValue(settings.value("bin_size", 10.0, float))
363        self.intervalSpinBox.setValue(settings.value("interval", 10.0, float))
364        self.gainSpinBox.setValue(settings.value("gain", 0, int))
365        self.ppmSpinBox.setValue(settings.value("ppm", 0, int))
366        self.cropSpinBox.setValue(settings.value("crop", 0, int))
367        self.mainCurveCheckBox.setChecked(settings.value("main_curve", 1, int))
368        self.peakHoldMaxCheckBox.setChecked(settings.value("peak_hold_max", 0, int))
369        self.peakHoldMinCheckBox.setChecked(settings.value("peak_hold_min", 0, int))
370        self.averageCheckBox.setChecked(settings.value("average", 0, int))
371        self.smoothCheckBox.setChecked(settings.value("smooth", 0, int))
372        self.persistenceCheckBox.setChecked(settings.value("persistence", 0, int))
373
374        # Restore window state
375        if settings.value("window_state"):
376            self.restoreState(settings.value("window_state"))
377        if settings.value("plotsplitter_state"):
378            self.plotSplitter.restoreState(settings.value("plotsplitter_state"))
379
380        # Migration from older version of config file
381        if settings.value("config_version", 1, int) < 2:
382            # Make tabs from docks when started for first time
383            self.tabifyDockWidget(self.settingsDockWidget, self.levelsDockWidget)
384            self.settingsDockWidget.raise_()
385            self.set_dock_size(self.controlsDockWidget, 0, 0)
386            self.set_dock_size(self.frequencyDockWidget, 0, 0)
387            # Update config version
388            settings.setValue("config_version", 2)
389
390        # Window geometry has to be restored only after show(), because initial
391        # maximization doesn't work otherwise (at least not in some window managers on X11)
392        self.show()
393        if settings.value("window_geometry"):
394            self.restoreGeometry(settings.value("window_geometry"))
395
396    def save_settings(self):
397        """Save spectrum analyzer settings and window geometry"""
398        settings = QtCore.QSettings()
399        settings.setValue("start_freq", self.startFreqSpinBox.value())
400        settings.setValue("stop_freq", self.stopFreqSpinBox.value())
401        settings.setValue("bin_size", self.binSizeSpinBox.value())
402        settings.setValue("interval", self.intervalSpinBox.value())
403        settings.setValue("gain", self.gainSpinBox.value())
404        settings.setValue("ppm", self.ppmSpinBox.value())
405        settings.setValue("crop", self.cropSpinBox.value())
406        settings.setValue("main_curve", int(self.mainCurveCheckBox.isChecked()))
407        settings.setValue("peak_hold_max", int(self.peakHoldMaxCheckBox.isChecked()))
408        settings.setValue("peak_hold_min", int(self.peakHoldMinCheckBox.isChecked()))
409        settings.setValue("average", int(self.averageCheckBox.isChecked()))
410        settings.setValue("smooth", int(self.smoothCheckBox.isChecked()))
411        settings.setValue("persistence", int(self.persistenceCheckBox.isChecked()))
412
413        # Save window state and geometry
414        settings.setValue("window_geometry", self.saveGeometry())
415        settings.setValue("window_state", self.saveState())
416        settings.setValue("plotsplitter_state", self.plotSplitter.saveState())
417
418    def show_status(self, message, timeout=2000):
419        """Show message in status bar"""
420        self.statusbar.showMessage(message, timeout)
421
422    def update_buttons(self):
423        """Update state of control buttons"""
424        self.startButton.setEnabled(not self.power_thread.alive)
425        self.singleShotButton.setEnabled(not self.power_thread.alive)
426        self.stopButton.setEnabled(self.power_thread.alive)
427
428    def update_data(self, data_storage):
429        """Update GUI when new data is received"""
430        # Show number of hops and how much time did the sweep really take
431        timestamp = time.time()
432        sweep_time = timestamp - self.prev_data_timestamp
433        self.prev_data_timestamp = timestamp
434
435        status = []
436        if self.power_thread.params["hops"]:
437            status.append(self.tr("Frequency hops: {}").format(self.power_thread.params["hops"]))
438        status.append(self.tr("Sweep time: {:.2f} s | FPS: {:.2f}").format(sweep_time, 1 / sweep_time))
439        self.show_status(" | ".join(status), timeout=0)
440
441    def start(self, single_shot=False):
442        """Start power thread"""
443        settings = QtCore.QSettings()
444        self.prev_data_timestamp = time.time()
445
446        self.data_storage.reset()
447        self.data_storage.set_smooth(
448            bool(self.smoothCheckBox.isChecked()),
449            settings.value("smooth_length", 11, int),
450            settings.value("smooth_window", "hanning"),
451            recalculate=False
452        )
453
454        self.waterfallPlotWidget.history_size = settings.value("waterfall_history_size", 100, int)
455        self.waterfallPlotWidget.clear_plot()
456
457        self.spectrumPlotWidget.main_curve = bool(self.mainCurveCheckBox.isChecked())
458        self.spectrumPlotWidget.main_color = str_to_color(settings.value("main_color", "255, 255, 0, 255"))
459        self.spectrumPlotWidget.peak_hold_max = bool(self.peakHoldMaxCheckBox.isChecked())
460        self.spectrumPlotWidget.peak_hold_max_color = str_to_color(settings.value("peak_hold_max_color", "255, 0, 0, 255"))
461        self.spectrumPlotWidget.peak_hold_min = bool(self.peakHoldMinCheckBox.isChecked())
462        self.spectrumPlotWidget.peak_hold_min_color = str_to_color(settings.value("peak_hold_min_color", "0, 0, 255, 255"))
463        self.spectrumPlotWidget.average = bool(self.averageCheckBox.isChecked())
464        self.spectrumPlotWidget.average_color = str_to_color(settings.value("average_color", "0, 255, 255, 255"))
465        self.spectrumPlotWidget.persistence = bool(self.persistenceCheckBox.isChecked())
466        self.spectrumPlotWidget.persistence_length = settings.value("persistence_length", 5, int)
467        self.spectrumPlotWidget.persistence_decay = settings.value("persistence_decay", "exponential")
468        self.spectrumPlotWidget.persistence_color = str_to_color(settings.value("persistence_color", "0, 255, 0, 255"))
469        self.spectrumPlotWidget.clear_plot()
470        self.spectrumPlotWidget.clear_peak_hold_max()
471        self.spectrumPlotWidget.clear_peak_hold_min()
472        self.spectrumPlotWidget.clear_average()
473        self.spectrumPlotWidget.clear_persistence()
474
475        if not self.power_thread.alive:
476            self.power_thread.setup(float(self.startFreqSpinBox.value()),
477                                    float(self.stopFreqSpinBox.value()),
478                                    float(self.binSizeSpinBox.value()),
479                                    interval=float(self.intervalSpinBox.value()),
480                                    gain=int(self.gainSpinBox.value()),
481                                    ppm=int(self.ppmSpinBox.value()),
482                                    crop=int(self.cropSpinBox.value()) / 100.0,
483                                    single_shot=single_shot,
484                                    device=settings.value("device", ""),
485                                    sample_rate=settings.value("sample_rate", 2560000, float),
486                                    bandwidth=settings.value("bandwidth", 0, float),
487                                    lnb_lo=settings.value("lnb_lo", 0, float))
488            self.power_thread.start()
489
490    def stop(self):
491        """Stop power thread"""
492        if self.power_thread.alive:
493            self.power_thread.stop()
494
495    @QtCore.pyqtSlot()
496    def on_startButton_clicked(self):
497        self.start()
498
499    @QtCore.pyqtSlot()
500    def on_singleShotButton_clicked(self):
501        self.start(single_shot=True)
502
503    @QtCore.pyqtSlot()
504    def on_stopButton_clicked(self):
505        self.stop()
506
507    @QtCore.pyqtSlot(bool)
508    def on_mainCurveCheckBox_toggled(self, checked):
509        self.spectrumPlotWidget.main_curve = checked
510        if self.spectrumPlotWidget.curve.xData is None:
511            self.spectrumPlotWidget.update_plot(self.data_storage)
512        self.spectrumPlotWidget.curve.setVisible(checked)
513
514    @QtCore.pyqtSlot(bool)
515    def on_peakHoldMaxCheckBox_toggled(self, checked):
516        self.spectrumPlotWidget.peak_hold_max = checked
517        if self.spectrumPlotWidget.curve_peak_hold_max.xData is None:
518            self.spectrumPlotWidget.update_peak_hold_max(self.data_storage)
519        self.spectrumPlotWidget.curve_peak_hold_max.setVisible(checked)
520
521    @QtCore.pyqtSlot(bool)
522    def on_peakHoldMinCheckBox_toggled(self, checked):
523        self.spectrumPlotWidget.peak_hold_min = checked
524        if self.spectrumPlotWidget.curve_peak_hold_min.xData is None:
525            self.spectrumPlotWidget.update_peak_hold_min(self.data_storage)
526        self.spectrumPlotWidget.curve_peak_hold_min.setVisible(checked)
527
528    @QtCore.pyqtSlot(bool)
529    def on_averageCheckBox_toggled(self, checked):
530        self.spectrumPlotWidget.average = checked
531        if self.spectrumPlotWidget.curve_average.xData is None:
532            self.spectrumPlotWidget.update_average(self.data_storage)
533        self.spectrumPlotWidget.curve_average.setVisible(checked)
534
535    @QtCore.pyqtSlot(bool)
536    def on_persistenceCheckBox_toggled(self, checked):
537        self.spectrumPlotWidget.persistence = checked
538        if self.spectrumPlotWidget.persistence_curves[0].xData is None:
539            self.spectrumPlotWidget.recalculate_persistence(self.data_storage)
540        for curve in self.spectrumPlotWidget.persistence_curves:
541            curve.setVisible(checked)
542
543    @QtCore.pyqtSlot(bool)
544    def on_smoothCheckBox_toggled(self, checked):
545        settings = QtCore.QSettings()
546        self.data_storage.set_smooth(
547            checked,
548            settings.value("smooth_length", 11, int),
549            settings.value("smooth_window", "hanning"),
550            recalculate=True
551        )
552
553    @QtCore.pyqtSlot()
554    def on_smoothButton_clicked(self):
555        dialog = QSpectrumAnalyzerSmooth(self)
556        if dialog.exec_():
557            settings = QtCore.QSettings()
558            self.data_storage.set_smooth(
559                bool(self.smoothCheckBox.isChecked()),
560                settings.value("smooth_length", 11, int),
561                settings.value("smooth_window", "hanning"),
562                recalculate=True
563            )
564
565    @QtCore.pyqtSlot()
566    def on_persistenceButton_clicked(self):
567        prev_persistence_length = self.spectrumPlotWidget.persistence_length
568        dialog = QSpectrumAnalyzerPersistence(self)
569        if dialog.exec_():
570            settings = QtCore.QSettings()
571            persistence_length = settings.value("persistence_length", 5, int)
572            self.spectrumPlotWidget.persistence_length = persistence_length
573            self.spectrumPlotWidget.persistence_decay = settings.value("persistence_decay", "exponential")
574
575            # If only decay function has been changed, just reset colors
576            if persistence_length == prev_persistence_length:
577                self.spectrumPlotWidget.set_colors()
578            else:
579                self.spectrumPlotWidget.recalculate_persistence(self.data_storage)
580
581    @QtCore.pyqtSlot()
582    def on_colorsButton_clicked(self):
583        dialog = QSpectrumAnalyzerColors(self)
584        if dialog.exec_():
585            settings = QtCore.QSettings()
586            self.spectrumPlotWidget.main_color = str_to_color(settings.value("main_color", "255, 255, 0, 255"))
587            self.spectrumPlotWidget.peak_hold_max_color = str_to_color(settings.value("peak_hold_max_color", "255, 0, 0, 255"))
588            self.spectrumPlotWidget.peak_hold_min_color = str_to_color(settings.value("peak_hold_min_color", "0, 0, 255, 255"))
589            self.spectrumPlotWidget.average_color = str_to_color(settings.value("average_color", "0, 255, 255, 255"))
590            self.spectrumPlotWidget.persistence_color = str_to_color(settings.value("persistence_color", "0, 255, 0, 255"))
591            self.spectrumPlotWidget.set_colors()
592
593    @QtCore.pyqtSlot()
594    def on_action_Settings_triggered(self):
595        dialog = QSpectrumAnalyzerSettings(self)
596        if dialog.exec_():
597            self.setup_power_thread()
598
599    @QtCore.pyqtSlot()
600    def on_action_About_triggered(self):
601        QtWidgets.QMessageBox.information(self, self.tr("About - QSpectrumAnalyzer"),
602                                          self.tr("QSpectrumAnalyzer {}").format(__version__))
603
604    @QtCore.pyqtSlot()
605    def on_action_Quit_triggered(self):
606        self.close()
607
608    def closeEvent(self, event):
609        """Save settings when main window is closed"""
610        self.stop()
611        self.save_settings()
612
613
614def main():
615    app = QtWidgets.QApplication(sys.argv)
616    app.setOrganizationName("QSpectrumAnalyzer")
617    app.setOrganizationDomain("qspectrumanalyzer.eutopia.cz")
618    app.setApplicationName("QSpectrumAnalyzer")
619    window = QSpectrumAnalyzerMainWindow()
620    sys.exit(app.exec_())
621
622
623if __name__ == "__main__":
624    main()
625