New version 1.5.0 (soapy_sdr backend now fully functioning)

This commit is contained in:
Michal Krenek (Mikos) 2017-03-10 17:39:04 +01:00
parent dc27e90653
commit 47ea0b99b5
17 changed files with 439 additions and 136 deletions

View File

@ -1,14 +1,20 @@
# Maintainer: Michal Krenek (Mikos) <m.krenek@gmail.com>
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() {

View File

@ -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 <https://github.com/steve-m/kalibrate-rtl>`_.
@ -108,17 +109,30 @@ Git master branch:
cd qspectrumanalyzer-git
makepkg -sri
Or simply use `pacaur <https://aur.archlinux.org/packages/pacaur>`_ (or any other AUR helper):
Or simply use `pacaur <https://aur.archlinux.org/packages/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)

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -81,22 +81,22 @@
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../__main__.py" line="337"/>
<location filename="../__main__.py" line="383"/>
<source>Frequency hops: {} | Sweep time: {:.2f} s | FPS: {:.2f}</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../__main__.py" line="337"/>
<location filename="../__main__.py" line="383"/>
<source>N/A</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../__main__.py" line="504"/>
<location filename="../__main__.py" line="550"/>
<source>About - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../__main__.py" line="504"/>
<location filename="../__main__.py" line="550"/>
<source>QSpectrumAnalyzer {}</source>
<translation type="unfinished"></translation>
</message>
@ -252,70 +252,88 @@
<context>
<name>QSpectrumAnalyzerSettings</name>
<message>
<location filename="../__main__.py" line="50"/>
<location filename="../__main__.py" line="65"/>
<source>Select executable - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="107"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="122"/>
<source>Settings - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="108"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="123"/>
<source>&amp;Backend:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="115"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="130"/>
<source>soapy_power</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="110"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="125"/>
<source>rx_power</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="111"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="126"/>
<source>rtl_power_fftw</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="112"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="127"/>
<source>rtl_power</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="113"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="128"/>
<source>hackrf_sweep</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="114"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="129"/>
<source>E&amp;xecutable:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="116"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="131"/>
<source>...</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="117"/>
<source>Device:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="118"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="133"/>
<source>Sa&amp;mple rate:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="119"/>
<location filename="../ui_qspectrumanalyzer_settings.py" line="134"/>
<source>&amp;Waterfall history size:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="132"/>
<source>&amp;Device:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="135"/>
<source>Additional &amp;parameters:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="../ui_qspectrumanalyzer_settings.py" line="136"/>
<source>?</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QSpectrumAnalyzerSettingsHelp</name>
<message>
<location filename="../ui_qspectrumanalyzer_settings_help.py" line="49"/>
<source>Help - QSpectrumAnalyzer</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QSpectrumAnalyzerSmooth</name>

View File

@ -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)

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>420</width>
<height>255</height>
<width>600</width>
<height>310</height>
</rect>
</property>
<property name="windowTitle">
@ -86,7 +86,7 @@
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Device:</string>
<string>&amp;Device:</string>
</property>
<property name="buddy">
<cstring>deviceEdit</cstring>
@ -96,7 +96,7 @@
<item row="2" column="1">
<widget class="QLineEdit" name="deviceEdit"/>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Sa&amp;mple rate:</string>
@ -106,7 +106,7 @@
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QSpinBox" name="sampleRateSpinBox">
<property name="minimum">
<number>0</number>
@ -122,7 +122,7 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>&amp;Waterfall history size:</string>
@ -132,7 +132,7 @@
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QSpinBox" name="waterfallHistorySizeSpinBox">
<property name="minimum">
<number>1</number>
@ -145,6 +145,30 @@
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Additional &amp;parameters:</string>
</property>
<property name="buddy">
<cstring>paramsEdit</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLineEdit" name="paramsEdit"/>
</item>
<item>
<widget class="QToolButton" name="helpButton">
<property name="text">
<string>?</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
@ -177,6 +201,8 @@
<tabstop>executableEdit</tabstop>
<tabstop>executableButton</tabstop>
<tabstop>deviceEdit</tabstop>
<tabstop>paramsEdit</tabstop>
<tabstop>helpButton</tabstop>
<tabstop>sampleRateSpinBox</tabstop>
<tabstop>waterfallHistorySizeSpinBox</tabstop>
<tabstop>buttonBox</tabstop>
@ -190,8 +216,8 @@
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>242</x>
<y>248</y>
<x>248</x>
<y>303</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
@ -206,8 +232,8 @@
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>310</x>
<y>248</y>
<x>316</x>
<y>303</y>
</hint>
<hint type="destinationlabel">
<x>286</x>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QSpectrumAnalyzerSettingsHelp</class>
<widget class="QDialog" name="QSpectrumAnalyzerSettingsHelp">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1200</width>
<height>700</height>
</rect>
</property>
<property name="windowTitle">
<string>Help - QSpectrumAnalyzer</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPlainTextEdit" name="helpTextEdit">
<property name="undoRedoEnabled">
<bool>false</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>helpTextEdit</tabstop>
<tabstop>buttonBox</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>QSpectrumAnalyzerSettingsHelp</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>224</x>
<y>672</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QSpectrumAnalyzerSettingsHelp</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>292</x>
<y>678</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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 &parameters:", None))
self.helpButton.setText(_translate("QSpectrumAnalyzerSettings", "?", None))

View File

@ -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))

View File

@ -1 +1 @@
__version__ = "1.4.0"
__version__ = "1.5.0"

View File

@ -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=[