diff --git a/PKGBUILD b/PKGBUILD index 46318ea..a4e0d22 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,14 +1,20 @@ # Maintainer: Michal Krenek (Mikos) pkgname=qspectrumanalyzer -pkgver=1.4.0 +pkgver=1.5.0 pkgrel=1 -pkgdesc="Spectrum analyzer for RTL-SDR (GUI for rtl_power based on PyQtGraph)" +pkgdesc="Spectrum analyzer for multiple SDR platforms (PyQtGraph based GUI for soapy_power, rx_power, rtl_power, hackrf_sweep and other backends)" arch=('any') url="https://github.com/xmikos/qspectrumanalyzer" license=('GPL3') -depends=('python-pyqt4' 'python-pyqtgraph' 'rtl-sdr') +depends=('python-pyqt4' 'python-pyqtgraph' 'soapy_power') makedepends=('python-setuptools') -optdepends=('rtl_power_fftw-git: alternative rtl_power implementation using FFTW library') +optdepends=( + 'rtl_power_fftw-git: alternative RTL-SDR backend using FFTW library (much faster than rtl_power)' + 'rtl-sdr-keenerd-git: better version of rtl_power backend' + 'rtl-sdr: original rtl_power backend (slightly broken, use rtl-sdr-keenerd-git instead)' + 'rx_tools: rx_power backend (universal SoapySDR based backend, but seems slow and buggy)' + 'hackrf: hackrf_sweep backend (wideband spectrum monitoring with sweep rate of 8 GHz/s)' +) source=(https://github.com/xmikos/qspectrumanalyzer/archive/v$pkgver.tar.gz) build() { diff --git a/README.rst b/README.rst index 7930083..e2e1c13 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,8 @@ Requirements - Python >= 3.3 - PyQt >= 4.5 - PyQtGraph (http://www.pyqtgraph.org) -- soapy_power / rx_tools / rtl-sdr / rtl_power_fftw / hackrf +- soapy_power (https://github.com/xmikos/soapy_power) +- Optional: rx_tools / rtl-sdr / rtl_power_fftw / hackrf Backends -------- @@ -36,7 +37,7 @@ USRP and some other SDR devices). ``rx_power`` (part of ``rx_tools``) is also based on SoapySDR and therefore supports nearly all SDR platforms, but it is much slower than soapy_power, doesn't support -near real-time continuous measurement (minimum interval is 1 second - same as ``rtl_power``) +near real-time continuous measurement (minimum interval is 1 second, same as ``rtl_power``) and is little buggy. RTL-SDR backends @@ -73,14 +74,14 @@ Usage Start QSpectrumAnalyzer by running ``qspectrumanalyzer``. You can choose which backend you want to use in *File* -> *Settings* -(default is ``soapy_power``). Sample rate and path to backend executable -can be also manually specified there. You can also set waterfall plot -history size. Default is 100 lines, be aware that really large sweeps -(with a lot of bins) would require a lot of system memory, -so don't make this number too big. +(default is ``soapy_power``). Sample rate, path to backend executable +and additional backend parameters can be also manually specified there. +You can also set waterfall plot history size. Default is 100 lines, be aware +that really large sweeps (with a lot of bins) would require a lot of system +memory, so don't make this number too big. Controls should be intuitive, but if you want consistent results, you should -turn off automatic gain control (set it to some fixed number) and also set +turn off automatic gain control (set gain to some fixed number) and also set crop to 20% or more. For finding out ppm correction factor for your rtl-sdr stick, use `kalibrate-rtl `_. @@ -108,17 +109,30 @@ Git master branch: cd qspectrumanalyzer-git makepkg -sri -Or simply use `pacaur `_ (or any other AUR helper): +Or simply use `pacaur `_ (or any other AUR helper) +which will also automatically install all QSpectrumAnalyzer dependencies: :: pacaur -S qspectrumanalyzer pacaur -S qspectrumanalyzer-git -Debian / Ubuntu: -**************** +Ubuntu: +******* :: - sudo apt-get install python3-pip python3-pyqt4 python3-numpy + # Add SoapySDR PPA to your system + sudo add-apt-repository -y ppa:myriadrf/drivers + + # Update list of packages + sudo apt-get update + + # Install basic dependencies + sudo apt-get install python3-pip python3-pyqt4 python3-numpy soapysdr python3-soapysdr + + # Install SoapySDR drivers for your hardware (e.g. RTL-SDR, Airspy, HackRF, LimeSDR, etc.) + sudo apt-get install soapysdr-module-rtlsdr soapysdr-module-airspy soapysdr-module-hackrf soapysdr-module-lms7 + + # Install QSpectrumAnalyzer sudo pip3 install qspectrumanalyzer Warning! ``pip`` will install packages system-wide by default, but you @@ -142,7 +156,6 @@ If you want to install QSpectrumAnalyzer directly from Git master branch, you ca Todo: ----- -- finish soapy_power backend (new universal default backend) - show scan progress - allow setting LNB LO frequency - save & load FFT history (allow big waterfall plot saved to file) diff --git a/qspectrumanalyzer/__main__.py b/qspectrumanalyzer/__main__.py index 95ca7f5..086c546 100755 --- a/qspectrumanalyzer/__main__.py +++ b/qspectrumanalyzer/__main__.py @@ -11,6 +11,7 @@ from qspectrumanalyzer.plot import SpectrumPlotWidget, WaterfallPlotWidget from qspectrumanalyzer.utils import color_to_str, str_to_color from qspectrumanalyzer.ui_qspectrumanalyzer_settings import Ui_QSpectrumAnalyzerSettings +from qspectrumanalyzer.ui_qspectrumanalyzer_settings_help import Ui_QSpectrumAnalyzerSettingsHelp from qspectrumanalyzer.ui_qspectrumanalyzer_smooth import Ui_QSpectrumAnalyzerSmooth from qspectrumanalyzer.ui_qspectrumanalyzer_persistence import Ui_QSpectrumAnalyzerPersistence from qspectrumanalyzer.ui_qspectrumanalyzer_colors import Ui_QSpectrumAnalyzerColors @@ -27,6 +28,7 @@ class QSpectrumAnalyzerSettings(QtGui.QDialog, Ui_QSpectrumAnalyzerSettings): # Initialize UI super().__init__(parent) self.setupUi(self) + self.help_dialog = None # Load settings settings = QtCore.QSettings() @@ -40,15 +42,16 @@ class QSpectrumAnalyzerSettings(QtGui.QDialog, Ui_QSpectrumAnalyzerSettings): except AttributeError: backend_module = backends.soapy_power + self.paramsEdit.setText(settings.value("params", backend_module.Info.additional_params)) self.sampleRateSpinBox.setMinimum(backend_module.Info.sample_rate_min) self.sampleRateSpinBox.setMaximum(backend_module.Info.sample_rate_max) self.sampleRateSpinBox.setValue(settings.value("sample_rate", backend_module.Info.sample_rate, int)) + self.backendComboBox.blockSignals(True) self.backendComboBox.clear() for b in sorted(backends.__all__): self.backendComboBox.addItem(b) - self.backendComboBox.blockSignals(True) i = self.backendComboBox.findText(backend) if i == -1: self.backendComboBox.setCurrentIndex(0) @@ -63,6 +66,23 @@ class QSpectrumAnalyzerSettings(QtGui.QDialog, Ui_QSpectrumAnalyzerSettings): if filename: self.executableEdit.setText(filename) + @QtCore.pyqtSlot() + def on_helpButton_clicked(self): + """Open help dialog when button is clicked""" + try: + backend_module = getattr(backends, self.backendComboBox.currentText()) + except AttributeError: + backend_module = backends.soapy_power + + self.help_dialog = QSpectrumAnalyzerSettingsHelp( + backend_module.Info.help(self.executableEdit.text()), + parent=self + ) + + self.help_dialog.show() + self.help_dialog.raise_() + self.help_dialog.activateWindow() + @QtCore.pyqtSlot(str) def on_backendComboBox_currentIndexChanged(self, text): """Change executable when backend is changed""" @@ -74,6 +94,7 @@ class QSpectrumAnalyzerSettings(QtGui.QDialog, Ui_QSpectrumAnalyzerSettings): except AttributeError: backend_module = backends.soapy_power + self.paramsEdit.setText(backend_module.Info.additional_params) self.sampleRateSpinBox.setMinimum(backend_module.Info.sample_rate_min) self.sampleRateSpinBox.setMaximum(backend_module.Info.sample_rate_max) self.sampleRateSpinBox.setValue(backend_module.Info.sample_rate) @@ -84,11 +105,25 @@ class QSpectrumAnalyzerSettings(QtGui.QDialog, Ui_QSpectrumAnalyzerSettings): settings.setValue("executable", self.executableEdit.text()) settings.setValue("waterfall_history_size", self.waterfallHistorySizeSpinBox.value()) settings.setValue("device", self.deviceEdit.text()) + settings.setValue("params", self.paramsEdit.text()) settings.setValue("sample_rate", self.sampleRateSpinBox.value()) settings.setValue("backend", self.backendComboBox.currentText()) QtGui.QDialog.accept(self) +class QSpectrumAnalyzerSettingsHelp(QtGui.QDialog, Ui_QSpectrumAnalyzerSettingsHelp): + """QSpectrumAnalyzer settings help dialog""" + def __init__(self, text, parent=None): + # Initialize UI + super().__init__(parent) + self.setupUi(self) + + monospace_font = QtGui.QFont('monospace') + monospace_font.setStyleHint(QtGui.QFont.Monospace) + self.helpTextEdit.setFont(monospace_font) + self.helpTextEdit.setPlainText(text) + + class QSpectrumAnalyzerSmooth(QtGui.QDialog, Ui_QSpectrumAnalyzerSmooth): """QSpectrumAnalyzer spectrum smoothing dialog""" def __init__(self, parent=None): @@ -185,6 +220,7 @@ class QSpectrumAnalyzerMainWindow(QtGui.QMainWindow, Ui_QSpectrumAnalyzerMainWin self.prev_data_timestamp = None self.data_storage = None self.power_thread = None + self.backend = None self.setup_power_thread() self.update_buttons() @@ -213,27 +249,29 @@ class QSpectrumAnalyzerMainWindow(QtGui.QMainWindow, Ui_QSpectrumAnalyzerMainWin except AttributeError: backend_module = backends.soapy_power - self.gainSpinBox.setMinimum(backend_module.Info.gain_min) - self.gainSpinBox.setMaximum(backend_module.Info.gain_max) - self.gainSpinBox.setValue(backend_module.Info.gain) - self.startFreqSpinBox.setMinimum(backend_module.Info.start_freq_min) - self.startFreqSpinBox.setMaximum(backend_module.Info.start_freq_max) - self.startFreqSpinBox.setValue(backend_module.Info.start_freq) - self.stopFreqSpinBox.setMinimum(backend_module.Info.stop_freq_min) - self.stopFreqSpinBox.setMaximum(backend_module.Info.stop_freq_max) - self.stopFreqSpinBox.setValue(backend_module.Info.stop_freq) - self.binSizeSpinBox.setMinimum(backend_module.Info.bin_size_min) - self.binSizeSpinBox.setMaximum(backend_module.Info.bin_size_max) - self.binSizeSpinBox.setValue(backend_module.Info.bin_size) - self.intervalSpinBox.setMinimum(backend_module.Info.interval_min) - self.intervalSpinBox.setMaximum(backend_module.Info.interval_max) - self.intervalSpinBox.setValue(backend_module.Info.interval) - self.ppmSpinBox.setMinimum(backend_module.Info.ppm_min) - self.ppmSpinBox.setMaximum(backend_module.Info.ppm_max) - self.ppmSpinBox.setValue(backend_module.Info.ppm) - self.cropSpinBox.setMinimum(backend_module.Info.crop_min) - self.cropSpinBox.setMaximum(backend_module.Info.crop_max) - self.cropSpinBox.setValue(backend_module.Info.crop) + if not self.backend or backend != self.backend: + self.backend = backend + self.gainSpinBox.setMinimum(backend_module.Info.gain_min) + self.gainSpinBox.setMaximum(backend_module.Info.gain_max) + self.gainSpinBox.setValue(backend_module.Info.gain) + self.startFreqSpinBox.setMinimum(backend_module.Info.start_freq_min) + self.startFreqSpinBox.setMaximum(backend_module.Info.start_freq_max) + self.startFreqSpinBox.setValue(backend_module.Info.start_freq) + self.stopFreqSpinBox.setMinimum(backend_module.Info.stop_freq_min) + self.stopFreqSpinBox.setMaximum(backend_module.Info.stop_freq_max) + self.stopFreqSpinBox.setValue(backend_module.Info.stop_freq) + self.binSizeSpinBox.setMinimum(backend_module.Info.bin_size_min) + self.binSizeSpinBox.setMaximum(backend_module.Info.bin_size_max) + self.binSizeSpinBox.setValue(backend_module.Info.bin_size) + self.intervalSpinBox.setMinimum(backend_module.Info.interval_min) + self.intervalSpinBox.setMaximum(backend_module.Info.interval_max) + self.intervalSpinBox.setValue(backend_module.Info.interval) + self.ppmSpinBox.setMinimum(backend_module.Info.ppm_min) + self.ppmSpinBox.setMaximum(backend_module.Info.ppm_max) + self.ppmSpinBox.setValue(backend_module.Info.ppm) + self.cropSpinBox.setMinimum(backend_module.Info.crop_min) + self.cropSpinBox.setMaximum(backend_module.Info.crop_max) + self.cropSpinBox.setValue(backend_module.Info.crop) self.power_thread = backend_module.PowerThread(self.data_storage) self.power_thread.powerThreadStarted.connect(self.update_buttons) diff --git a/qspectrumanalyzer/backends/__init__.py b/qspectrumanalyzer/backends/__init__.py index ce85c85..e91ae55 100644 --- a/qspectrumanalyzer/backends/__init__.py +++ b/qspectrumanalyzer/backends/__init__.py @@ -1,4 +1,4 @@ -import os, glob +import os, glob, subprocess from PyQt4 import QtCore @@ -6,16 +6,16 @@ from PyQt4 import QtCore class BaseInfo: """Default device metadata""" sample_rate_min = 0 - sample_rate_max = 61440000 + sample_rate_max = 3200000 sample_rate = 2560000 gain_min = -1 gain_max = 49 - gain = -1 + gain = 37 start_freq_min = 24 - start_freq_max = 1766 + start_freq_max = 2200 start_freq = 87 stop_freq_min = 24 - stop_freq_max = 1766 + stop_freq_max = 2200 stop_freq = 108 bin_size_min = 0 bin_size_max = 2800 @@ -29,6 +29,18 @@ class BaseInfo: crop_min = 0 crop_max = 99 crop = 0 + additional_params = '' + + @classmethod + def help(cls, executable): + try: + p = subprocess.run([executable, '-h'], universal_newlines=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + env=dict(os.environ, COLUMNS='125')) + text = p.stdout + except OSError: + text = '{} executable not found!'.format(executable) + return text class BasePowerThread(QtCore.QThread): diff --git a/qspectrumanalyzer/backends/hackrf_sweep.py b/qspectrumanalyzer/backends/hackrf_sweep.py index 367acfa..4f5eeb9 100644 --- a/qspectrumanalyzer/backends/hackrf_sweep.py +++ b/qspectrumanalyzer/backends/hackrf_sweep.py @@ -1,8 +1,7 @@ -import subprocess, pprint +import subprocess, pprint, struct, shlex import numpy as np from PyQt4 import QtCore -import struct from qspectrumanalyzer.backends import BaseInfo, BasePowerThread @@ -102,6 +101,10 @@ class PowerThread(BasePowerThread): if self.params["single_shot"]: cmdline.append("-1") + additional_params = settings.value("params", Info.additional_params) + if additional_params: + cmdline.extend(shlex.split(additional_params)) + self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, universal_newlines=False) @@ -138,6 +141,10 @@ class PowerThread(BasePowerThread): buf = self.process.stdout.read(record_length) if buf: self.parse_output(buf) + else: + break + else: + break self.process_stop() self.alive = False diff --git a/qspectrumanalyzer/backends/rtl_power.py b/qspectrumanalyzer/backends/rtl_power.py index 0c339a7..1ae0298 100644 --- a/qspectrumanalyzer/backends/rtl_power.py +++ b/qspectrumanalyzer/backends/rtl_power.py @@ -1,4 +1,4 @@ -import subprocess, pprint +import subprocess, pprint, shlex import numpy as np from PyQt4 import QtCore @@ -57,6 +57,10 @@ class PowerThread(BasePowerThread): if self.params["single_shot"]: cmdline.append("-1") + additional_params = settings.value("params", Info.additional_params) + if additional_params: + cmdline.extend(shlex.split(additional_params)) + self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, universal_newlines=True) diff --git a/qspectrumanalyzer/backends/rtl_power_fftw.py b/qspectrumanalyzer/backends/rtl_power_fftw.py index 7939979..78500d2 100644 --- a/qspectrumanalyzer/backends/rtl_power_fftw.py +++ b/qspectrumanalyzer/backends/rtl_power_fftw.py @@ -1,4 +1,4 @@ -import subprocess, math, pprint +import subprocess, math, pprint, shlex from PyQt4 import QtCore @@ -84,6 +84,10 @@ class PowerThread(BasePowerThread): if not self.params["single_shot"]: cmdline.append("-c") + additional_params = settings.value("params", Info.additional_params) + if additional_params: + cmdline.extend(shlex.split(additional_params)) + self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, universal_newlines=True) diff --git a/qspectrumanalyzer/backends/rx_power.py b/qspectrumanalyzer/backends/rx_power.py index 51609f1..ff7583f 100644 --- a/qspectrumanalyzer/backends/rx_power.py +++ b/qspectrumanalyzer/backends/rx_power.py @@ -1,4 +1,4 @@ -import subprocess, pprint +import subprocess, pprint, shlex import numpy as np from PyQt4 import QtCore @@ -8,7 +8,12 @@ from qspectrumanalyzer.backends import BaseInfo, BasePowerThread class Info(BaseInfo): """rx_power device metadata""" - pass + sample_rate_min = 0 + sample_rate_max = 61440000 + start_freq_min = 0 + start_freq_max = 6000 + stop_freq_min = 0 + stop_freq_max = 6000 class PowerThread(BasePowerThread): @@ -55,6 +60,10 @@ class PowerThread(BasePowerThread): if self.params["single_shot"]: cmdline.append("-1") + additional_params = settings.value("params", Info.additional_params) + if additional_params: + cmdline.extend(shlex.split(additional_params)) + self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, universal_newlines=True) diff --git a/qspectrumanalyzer/backends/soapy_power.py b/qspectrumanalyzer/backends/soapy_power.py index 55426ce..778a057 100644 --- a/qspectrumanalyzer/backends/soapy_power.py +++ b/qspectrumanalyzer/backends/soapy_power.py @@ -1,19 +1,27 @@ -import subprocess, pprint, re +import subprocess, pprint, sys, shlex +import numpy as np from PyQt4 import QtCore from qspectrumanalyzer.backends import BaseInfo, BasePowerThread +from soapypower.writer import SoapyPowerBinFormat + +formatter = SoapyPowerBinFormat() class Info(BaseInfo): """soapy_power device metadata""" - pass + sample_rate_min = 0 + sample_rate_max = 61440000 + start_freq_min = 0 + start_freq_max = 6000 + stop_freq_min = 0 + stop_freq_max = 6000 + additional_params = '--even --fft-window boxcar --remove-dc' class PowerThread(BasePowerThread): """Thread which runs soapy_power process""" - re_two_floats = re.compile(r'^[-+\d.eE]+\s+[-+\d.eE]+$') - def setup(self, start_freq, stop_freq, bin_size, interval=10.0, gain=-1, ppm=0, crop=0, single_shot=False, device="", sample_rate=2560000): """Setup soapy_power params""" @@ -31,10 +39,7 @@ class PowerThread(BasePowerThread): "single_shot": single_shot } self.databuffer = {"timestamp": [], "x": [], "y": []} - self.databuffer_hop = {"timestamp": [], "x": [], "y": []} - self.hop = 0 - self.run = 0 - self.prev_line = "" + self.min_freq = 0 print("soapy_power params:") pprint.pprint(self.params) @@ -53,6 +58,7 @@ class PowerThread(BasePowerThread): "-d", "{}".format(self.params["device"]), "-r", "{}".format(self.params["sample_rate"]), "-p", "{}".format(self.params["ppm"]), + "-F", "soapy_power_bin", ] if self.params["gain"] >= 0: @@ -62,50 +68,59 @@ class PowerThread(BasePowerThread): if not self.params["single_shot"]: cmdline.append("-c") + additional_params = settings.value("params", Info.additional_params) + if additional_params: + cmdline.extend(shlex.split(additional_params)) + self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, - universal_newlines=True) + universal_newlines=False) - def parse_output(self, line): - """Parse one line of output from soapy_power""" - line = line.strip() + def parse_output(self, data): + """Parse data from soapy_power""" + header, y_axis = data - # One empty line => new hop - if not line and self.prev_line: - self.hop += 1 - print(' => HOP:', self.hop) - self.databuffer["x"].extend(self.databuffer_hop["x"]) - self.databuffer["y"].extend(self.databuffer_hop["y"]) - self.databuffer_hop = {"timestamp": [], "x": [], "y": []} + timestamp = header.timestamp + start_freq = header.start + stop_freq = header.stop + step = header.step + samples = header.samples - # Two empty lines => new set - elif not line and not self.prev_line: - self.hop = 0 - self.run += 1 - print(' * RUN:', self.run) + x_axis = np.arange(start_freq, stop_freq, step) + if len(x_axis) != len(y_axis): + print("ERROR: len(x_axis) != len(y_axis)") + + if not self.min_freq: + self.min_freq = start_freq + + if start_freq == self.min_freq: + self.databuffer = {"timestamp": timestamp, + "x": list(x_axis), + "y": list(y_axis)} + else: + self.databuffer["x"].extend(x_axis) + self.databuffer["y"].extend(y_axis) + + if stop_freq > (self.params["stop_freq"] * 1e6) - step: self.data_storage.update(self.databuffer) - self.databuffer = {"timestamp": [], "x": [], "y": []} - # Get timestamp for new hop and set - elif line.startswith("# Acquisition start:"): - timestamp = line.split(":", 1)[1].strip() - if not self.databuffer_hop["timestamp"]: - self.databuffer_hop["timestamp"] = timestamp - if not self.databuffer["timestamp"]: - self.databuffer["timestamp"] = timestamp + def run(self): + """soapy_power thread main loop""" + self.process_start() + self.alive = True + self.powerThreadStarted.emit() - # Skip other comments - elif line.startswith("#"): - pass - - # Parse frequency and power - elif self.re_two_floats.match(line): + while self.alive: try: - freq, power = line.split() - except ValueError: - return + data = formatter.read(self.process.stdout) + except ValueError as e: + print(e, file=sys.stderr) + break - freq, power = float(freq), float(power) - self.databuffer_hop["x"].append(freq) - self.databuffer_hop["y"].append(power) + if data: + self.parse_output(data) + else: + break - self.prev_line = line + self.process_stop() + self.alive = False + self.powerThreadStopped.emit() diff --git a/qspectrumanalyzer/languages/qspectrumanalyzer_cs.ts b/qspectrumanalyzer/languages/qspectrumanalyzer_cs.ts index ada777e..6b35f1a 100644 --- a/qspectrumanalyzer/languages/qspectrumanalyzer_cs.ts +++ b/qspectrumanalyzer/languages/qspectrumanalyzer_cs.ts @@ -81,22 +81,22 @@ - + Frequency hops: {} | Sweep time: {:.2f} s | FPS: {:.2f} - + N/A - + About - QSpectrumAnalyzer - + QSpectrumAnalyzer {} @@ -252,70 +252,88 @@ QSpectrumAnalyzerSettings - + Select executable - QSpectrumAnalyzer - + Settings - QSpectrumAnalyzer - + &Backend: - + soapy_power - + rx_power - + rtl_power_fftw - + rtl_power - + hackrf_sweep - + E&xecutable: - + ... - - Device: - - - - + Sa&mple rate: - + &Waterfall history size: + + + &Device: + + + + + Additional &parameters: + + + + + ? + + + + + QSpectrumAnalyzerSettingsHelp + + + Help - QSpectrumAnalyzer + + QSpectrumAnalyzerSmooth diff --git a/qspectrumanalyzer/plot.py b/qspectrumanalyzer/plot.py index f2e1721..9efef4d 100644 --- a/qspectrumanalyzer/plot.py +++ b/qspectrumanalyzer/plot.py @@ -42,6 +42,9 @@ class SpectrumPlotWidget: self.plot.setLimits(xMin=0) self.plot.showButtons() + #self.plot.setDownsampling(mode="peak") + #self.plot.setClipToView(True) + self.create_persistence_curves() self.create_average_curve() self.create_peak_hold_min_curve() @@ -264,6 +267,7 @@ class WaterfallPlotWidget: self.plot.setLimits(xMin=0, yMax=0) self.plot.showButtons() #self.plot.setAspectLocked(True) + #self.plot.setDownsampling(mode="peak") #self.plot.setClipToView(True) diff --git a/qspectrumanalyzer/qspectrumanalyzer_settings.ui b/qspectrumanalyzer/qspectrumanalyzer_settings.ui index 83e3877..7ed6c90 100644 --- a/qspectrumanalyzer/qspectrumanalyzer_settings.ui +++ b/qspectrumanalyzer/qspectrumanalyzer_settings.ui @@ -6,8 +6,8 @@ 0 0 - 420 - 255 + 600 + 310 @@ -86,7 +86,7 @@ - Device: + &Device: deviceEdit @@ -96,7 +96,7 @@ - + Sa&mple rate: @@ -106,7 +106,7 @@ - + 0 @@ -122,7 +122,7 @@ - + &Waterfall history size: @@ -132,7 +132,7 @@ - + 1 @@ -145,6 +145,30 @@ + + + + Additional &parameters: + + + paramsEdit + + + + + + + + + + + + ? + + + + + @@ -177,6 +201,8 @@ executableEdit executableButton deviceEdit + paramsEdit + helpButton sampleRateSpinBox waterfallHistorySizeSpinBox buttonBox @@ -190,8 +216,8 @@ accept() - 242 - 248 + 248 + 303 157 @@ -206,8 +232,8 @@ reject() - 310 - 248 + 316 + 303 286 diff --git a/qspectrumanalyzer/qspectrumanalyzer_settings_help.ui b/qspectrumanalyzer/qspectrumanalyzer_settings_help.ui new file mode 100644 index 0000000..ccdc819 --- /dev/null +++ b/qspectrumanalyzer/qspectrumanalyzer_settings_help.ui @@ -0,0 +1,78 @@ + + + QSpectrumAnalyzerSettingsHelp + + + + 0 + 0 + 1200 + 700 + + + + Help - QSpectrumAnalyzer + + + + + + false + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + helpTextEdit + buttonBox + + + + + buttonBox + accepted() + QSpectrumAnalyzerSettingsHelp + accept() + + + 224 + 672 + + + 157 + 274 + + + + + buttonBox + rejected() + QSpectrumAnalyzerSettingsHelp + reject() + + + 292 + 678 + + + 286 + 274 + + + + + diff --git a/qspectrumanalyzer/ui_qspectrumanalyzer_settings.py b/qspectrumanalyzer/ui_qspectrumanalyzer_settings.py index fe5f280..7ccda66 100644 --- a/qspectrumanalyzer/ui_qspectrumanalyzer_settings.py +++ b/qspectrumanalyzer/ui_qspectrumanalyzer_settings.py @@ -25,7 +25,7 @@ except AttributeError: class Ui_QSpectrumAnalyzerSettings(object): def setupUi(self, QSpectrumAnalyzerSettings): QSpectrumAnalyzerSettings.setObjectName(_fromUtf8("QSpectrumAnalyzerSettings")) - QSpectrumAnalyzerSettings.resize(420, 255) + QSpectrumAnalyzerSettings.resize(600, 310) self.verticalLayout = QtGui.QVBoxLayout(QSpectrumAnalyzerSettings) self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.formLayout = QtGui.QFormLayout() @@ -61,23 +61,35 @@ class Ui_QSpectrumAnalyzerSettings(object): self.formLayout.setWidget(2, QtGui.QFormLayout.FieldRole, self.deviceEdit) self.label_4 = QtGui.QLabel(QSpectrumAnalyzerSettings) self.label_4.setObjectName(_fromUtf8("label_4")) - self.formLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.label_4) + self.formLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.label_4) self.sampleRateSpinBox = QtGui.QSpinBox(QSpectrumAnalyzerSettings) self.sampleRateSpinBox.setMinimum(0) self.sampleRateSpinBox.setMaximum(25000000) self.sampleRateSpinBox.setSingleStep(10000) self.sampleRateSpinBox.setProperty("value", 2560000) self.sampleRateSpinBox.setObjectName(_fromUtf8("sampleRateSpinBox")) - self.formLayout.setWidget(3, QtGui.QFormLayout.FieldRole, self.sampleRateSpinBox) + self.formLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.sampleRateSpinBox) self.label_2 = QtGui.QLabel(QSpectrumAnalyzerSettings) self.label_2.setObjectName(_fromUtf8("label_2")) - self.formLayout.setWidget(4, QtGui.QFormLayout.LabelRole, self.label_2) + self.formLayout.setWidget(5, QtGui.QFormLayout.LabelRole, self.label_2) self.waterfallHistorySizeSpinBox = QtGui.QSpinBox(QSpectrumAnalyzerSettings) self.waterfallHistorySizeSpinBox.setMinimum(1) self.waterfallHistorySizeSpinBox.setMaximum(10000000) self.waterfallHistorySizeSpinBox.setProperty("value", 100) self.waterfallHistorySizeSpinBox.setObjectName(_fromUtf8("waterfallHistorySizeSpinBox")) - self.formLayout.setWidget(4, QtGui.QFormLayout.FieldRole, self.waterfallHistorySizeSpinBox) + self.formLayout.setWidget(5, QtGui.QFormLayout.FieldRole, self.waterfallHistorySizeSpinBox) + self.label_6 = QtGui.QLabel(QSpectrumAnalyzerSettings) + self.label_6.setObjectName(_fromUtf8("label_6")) + self.formLayout.setWidget(3, QtGui.QFormLayout.LabelRole, self.label_6) + self.horizontalLayout_3 = QtGui.QHBoxLayout() + self.horizontalLayout_3.setObjectName(_fromUtf8("horizontalLayout_3")) + self.paramsEdit = QtGui.QLineEdit(QSpectrumAnalyzerSettings) + self.paramsEdit.setObjectName(_fromUtf8("paramsEdit")) + self.horizontalLayout_3.addWidget(self.paramsEdit) + self.helpButton = QtGui.QToolButton(QSpectrumAnalyzerSettings) + self.helpButton.setObjectName(_fromUtf8("helpButton")) + self.horizontalLayout_3.addWidget(self.helpButton) + self.formLayout.setLayout(3, QtGui.QFormLayout.FieldRole, self.horizontalLayout_3) self.verticalLayout.addLayout(self.formLayout) spacerItem = QtGui.QSpacerItem(20, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) self.verticalLayout.addItem(spacerItem) @@ -91,6 +103,7 @@ class Ui_QSpectrumAnalyzerSettings(object): self.label_5.setBuddy(self.deviceEdit) self.label_4.setBuddy(self.sampleRateSpinBox) self.label_2.setBuddy(self.waterfallHistorySizeSpinBox) + self.label_6.setBuddy(self.paramsEdit) self.retranslateUi(QSpectrumAnalyzerSettings) QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), QSpectrumAnalyzerSettings.accept) @@ -99,7 +112,9 @@ class Ui_QSpectrumAnalyzerSettings(object): QSpectrumAnalyzerSettings.setTabOrder(self.backendComboBox, self.executableEdit) QSpectrumAnalyzerSettings.setTabOrder(self.executableEdit, self.executableButton) QSpectrumAnalyzerSettings.setTabOrder(self.executableButton, self.deviceEdit) - QSpectrumAnalyzerSettings.setTabOrder(self.deviceEdit, self.sampleRateSpinBox) + QSpectrumAnalyzerSettings.setTabOrder(self.deviceEdit, self.paramsEdit) + QSpectrumAnalyzerSettings.setTabOrder(self.paramsEdit, self.helpButton) + QSpectrumAnalyzerSettings.setTabOrder(self.helpButton, self.sampleRateSpinBox) QSpectrumAnalyzerSettings.setTabOrder(self.sampleRateSpinBox, self.waterfallHistorySizeSpinBox) QSpectrumAnalyzerSettings.setTabOrder(self.waterfallHistorySizeSpinBox, self.buttonBox) @@ -114,7 +129,9 @@ class Ui_QSpectrumAnalyzerSettings(object): self.label.setText(_translate("QSpectrumAnalyzerSettings", "E&xecutable:", None)) self.executableEdit.setText(_translate("QSpectrumAnalyzerSettings", "soapy_power", None)) self.executableButton.setText(_translate("QSpectrumAnalyzerSettings", "...", None)) - self.label_5.setText(_translate("QSpectrumAnalyzerSettings", "Device:", None)) + self.label_5.setText(_translate("QSpectrumAnalyzerSettings", "&Device:", None)) self.label_4.setText(_translate("QSpectrumAnalyzerSettings", "Sa&mple rate:", None)) self.label_2.setText(_translate("QSpectrumAnalyzerSettings", "&Waterfall history size:", None)) + self.label_6.setText(_translate("QSpectrumAnalyzerSettings", "Additional ¶meters:", None)) + self.helpButton.setText(_translate("QSpectrumAnalyzerSettings", "?", None)) diff --git a/qspectrumanalyzer/ui_qspectrumanalyzer_settings_help.py b/qspectrumanalyzer/ui_qspectrumanalyzer_settings_help.py new file mode 100644 index 0000000..122a647 --- /dev/null +++ b/qspectrumanalyzer/ui_qspectrumanalyzer_settings_help.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'qspectrumanalyzer/qspectrumanalyzer_settings_help.ui' +# +# Created by: PyQt4 UI code generator 4.12 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +try: + _fromUtf8 = QtCore.QString.fromUtf8 +except AttributeError: + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) + +class Ui_QSpectrumAnalyzerSettingsHelp(object): + def setupUi(self, QSpectrumAnalyzerSettingsHelp): + QSpectrumAnalyzerSettingsHelp.setObjectName(_fromUtf8("QSpectrumAnalyzerSettingsHelp")) + QSpectrumAnalyzerSettingsHelp.resize(1200, 700) + self.verticalLayout = QtGui.QVBoxLayout(QSpectrumAnalyzerSettingsHelp) + self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.helpTextEdit = QtGui.QPlainTextEdit(QSpectrumAnalyzerSettingsHelp) + self.helpTextEdit.setUndoRedoEnabled(False) + self.helpTextEdit.setTextInteractionFlags(QtCore.Qt.TextSelectableByKeyboard|QtCore.Qt.TextSelectableByMouse) + self.helpTextEdit.setObjectName(_fromUtf8("helpTextEdit")) + self.verticalLayout.addWidget(self.helpTextEdit) + self.buttonBox = QtGui.QDialogButtonBox(QSpectrumAnalyzerSettingsHelp) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Close) + self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.verticalLayout.addWidget(self.buttonBox) + + self.retranslateUi(QSpectrumAnalyzerSettingsHelp) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("accepted()")), QSpectrumAnalyzerSettingsHelp.accept) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL(_fromUtf8("rejected()")), QSpectrumAnalyzerSettingsHelp.reject) + QtCore.QMetaObject.connectSlotsByName(QSpectrumAnalyzerSettingsHelp) + QSpectrumAnalyzerSettingsHelp.setTabOrder(self.helpTextEdit, self.buttonBox) + + def retranslateUi(self, QSpectrumAnalyzerSettingsHelp): + QSpectrumAnalyzerSettingsHelp.setWindowTitle(_translate("QSpectrumAnalyzerSettingsHelp", "Help - QSpectrumAnalyzer", None)) + diff --git a/qspectrumanalyzer/version.py b/qspectrumanalyzer/version.py index 3e8d9f9..5b60188 100644 --- a/qspectrumanalyzer/version.py +++ b/qspectrumanalyzer/version.py @@ -1 +1 @@ -__version__ = "1.4.0" +__version__ = "1.5.0" diff --git a/setup.py b/setup.py index 3343da9..a6e9d24 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,8 @@ from qspectrumanalyzer.version import __version__ setup( name="QSpectrumAnalyzer", version=__version__, - description="Spectrum analyzer for RTL-SDR (GUI for rtl_power based on PyQtGraph)", + description="Spectrum analyzer for multiple SDR platforms (PyQtGraph based GUI for soapy_power, rx_power, rtl_power, hackrf_sweep and other backends)", + long_description=open('README.rst').read(), author="Michal Krenek (Mikos)", author_email="m.krenek@gmail.com", url="https://github.com/xmikos/qspectrumanalyzer", @@ -29,6 +30,7 @@ setup( ], }, install_requires=[ + "soapy_power", "pyqtgraph" ], classifiers=[