Skip to content

Commit

Permalink
New version 1.5.0 (soapy_sdr backend now fully functioning)
Browse files Browse the repository at this point in the history
  • Loading branch information
xmikos committed Mar 10, 2017
1 parent dc27e90 commit 47ea0b9
Show file tree
Hide file tree
Showing 17 changed files with 444 additions and 141 deletions.
14 changes: 10 additions & 4 deletions PKGBUILD
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# Maintainer: Michal Krenek (Mikos) <[email protected]>
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() {
Expand Down
39 changes: 26 additions & 13 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------
Expand All @@ -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
Expand Down Expand Up @@ -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>`_.

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
82 changes: 60 additions & 22 deletions qspectrumanalyzer/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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
Expand All @@ -27,6 +28,7 @@ def __init__(self, parent=None):
# Initialize UI
super().__init__(parent)
self.setupUi(self)
self.help_dialog = None

# Load settings
settings = QtCore.QSettings()
Expand All @@ -40,15 +42,16 @@ def __init__(self, parent=None):
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)
Expand All @@ -63,6 +66,23 @@ def on_executableButton_clicked(self):
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"""
Expand All @@ -74,6 +94,7 @@ def on_backendComboBox_currentIndexChanged(self, text):
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)
Expand All @@ -84,11 +105,25 @@ def accept(self):
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):
Expand Down Expand Up @@ -185,6 +220,7 @@ def __init__(self, parent=None):
self.prev_data_timestamp = None
self.data_storage = None
self.power_thread = None
self.backend = None
self.setup_power_thread()

self.update_buttons()
Expand Down Expand Up @@ -213,27 +249,29 @@ def setup_power_thread(self):
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)
Expand Down
22 changes: 17 additions & 5 deletions qspectrumanalyzer/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import os, glob
import os, glob, subprocess

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
Expand All @@ -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):
Expand Down
11 changes: 9 additions & 2 deletions qspectrumanalyzer/backends/hackrf_sweep.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -102,6 +101,10 @@ def process_start(self):
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)

Expand Down Expand Up @@ -138,6 +141,10 @@ def run(self):
buf = self.process.stdout.read(record_length)
if buf:
self.parse_output(buf)
else:
break
else:
break

self.process_stop()
self.alive = False
Expand Down
6 changes: 5 additions & 1 deletion qspectrumanalyzer/backends/rtl_power.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import subprocess, pprint
import subprocess, pprint, shlex

import numpy as np
from PyQt4 import QtCore
Expand Down Expand Up @@ -57,6 +57,10 @@ def process_start(self):
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)

Expand Down
6 changes: 5 additions & 1 deletion qspectrumanalyzer/backends/rtl_power_fftw.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import subprocess, math, pprint
import subprocess, math, pprint, shlex

from PyQt4 import QtCore

Expand Down Expand Up @@ -84,6 +84,10 @@ def process_start(self):
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)

Expand Down
Loading

0 comments on commit 47ea0b9

Please sign in to comment.