diff --git a/qspectrumanalyzer/__main__.py b/qspectrumanalyzer/__main__.py index 780746d..f7a05b8 100755 --- a/qspectrumanalyzer/__main__.py +++ b/qspectrumanalyzer/__main__.py @@ -5,7 +5,7 @@ import sys, signal, time from PyQt4 import QtCore, QtGui from qspectrumanalyzer.version import __version__ -from qspectrumanalyzer.backend import RtlPowerThread, RtlPowerFftwThread, SoapyPowerThread, RxPowerThread +from qspectrumanalyzer.backend import RtlPowerThread, RtlPowerFftwThread, SoapyPowerThread, RxPowerThread, HackRFSweepThread from qspectrumanalyzer.data import DataStorage from qspectrumanalyzer.plot import SpectrumPlotWidget, WaterfallPlotWidget from qspectrumanalyzer.utils import color_to_str, str_to_color @@ -57,6 +57,15 @@ class QSpectrumAnalyzerSettings(QtGui.QDialog, Ui_QSpectrumAnalyzerSettings): self.executableEdit.setText(text) self.deviceEdit.setText("") + if text == "hackrf_sweep": + self.sampleRateSpinBox.setMinimum(20000000) + self.sampleRateSpinBox.setMaximum(20000000) + self.sampleRateSpinBox.setValue(20000000) + else: + self.sampleRateSpinBox.setMinimum(0) + self.sampleRateSpinBox.setMaximum(25000000) + self.sampleRateSpinBox.setValue(2560000) + def accept(self): """Save settings when dialog is accepted""" settings = QtCore.QSettings() @@ -193,6 +202,29 @@ class QSpectrumAnalyzerMainWindow(QtGui.QMainWindow, Ui_QSpectrumAnalyzerMainWin self.power_thread = RxPowerThread(self.data_storage) elif backend == "rtl_power_fftw": self.power_thread = RtlPowerFftwThread(self.data_storage) + elif backend == "hackrf_sweep": + self.gainSpinBox.setMinimum(0) + self.gainSpinBox.setMaximum(102) + self.gainSpinBox.setValue(40) + self.startFreqSpinBox.setMinimum(0) + self.startFreqSpinBox.setMaximum(7230) + self.startFreqSpinBox.setValue(0) + self.stopFreqSpinBox.setMinimum(0) + self.stopFreqSpinBox.setMaximum(7250) + self.stopFreqSpinBox.setValue(6000) + self.binSizeSpinBox.setMinimum(40) + self.binSizeSpinBox.setMaximum(5000) + self.binSizeSpinBox.setValue(1000) + self.intervalSpinBox.setMinimum(0) + self.intervalSpinBox.setMaximum(0) + self.intervalSpinBox.setValue(0) + self.ppmSpinBox.setMinimum(0) + self.ppmSpinBox.setMaximum(0) + self.ppmSpinBox.setValue(0) + self.cropSpinBox.setMinimum(0) + self.cropSpinBox.setMaximum(0) + self.cropSpinBox.setValue(0) + self.power_thread = HackRFSweepThread(self.data_storage) else: self.power_thread = RtlPowerThread(self.data_storage) diff --git a/qspectrumanalyzer/backend.py b/qspectrumanalyzer/backend.py index 9c7d1be..99d6e82 100644 --- a/qspectrumanalyzer/backend.py +++ b/qspectrumanalyzer/backend.py @@ -2,6 +2,7 @@ import subprocess, math, pprint, re import numpy as np from PyQt4 import QtCore +import struct class BasePowerThread(QtCore.QThread): @@ -147,6 +148,8 @@ class RtlPowerThread(BasePowerThread): def setup(self, start_freq, stop_freq, bin_size, interval=10.0, gain=-1, ppm=0, crop=0, single_shot=False, device=0, sample_rate=2560000): """Setup rtl_power params""" + if bin_size > 2800: + bin_size = 2800 self.params = { "start_freq": start_freq, "stop_freq": stop_freq, @@ -235,6 +238,8 @@ class RtlPowerFftwThread(BasePowerThread): min_overhang = sample_rate * overlap * 0.01 hops = math.ceil((freq_range - min_overhang) / (sample_rate - min_overhang)) overhang = (hops * sample_rate - freq_range) / (hops - 1) if hops > 1 else 0 + if bin_size > 2800: + bin_size = 2800 bins = math.ceil(sample_rate / (bin_size * 1e3)) crop_freq = sample_rate * crop * 0.01 @@ -450,3 +455,112 @@ class SoapyPowerThread(BasePowerThread): self.databuffer_hop["y"].append(power) self.prev_line = line + + +class HackRFSweepThread(BasePowerThread): + """Thread which runs hackrf_sweep process""" + def setup(self, start_freq=0, stop_freq=6000, bin_size=1000, + interval=0.0, gain=40, ppm=0, crop=0, single_shot=False, + device_index=0, sample_rate=20000000): + """Setup hackrf_sweep params""" + # theoretically we can support bins smaller than 40 kHz, but it is + # unlikely to result in acceptable performance + if bin_size < 40: + bin_size = 40 + if bin_size > 5000: + bin_size = 5000 + + # We only support whole numbers of steps with bandwidth equal to the + # sample rate. + step_bandwidth = sample_rate / 1000000 + total_bandwidth = stop_freq - start_freq + step_count = 1 + (total_bandwidth - 1) // step_bandwidth + total_bandwidth = step_count * step_bandwidth + stop_freq = start_freq + total_bandwidth + + # distribute gain between two analog gain stages + if gain < 0: + gain = 0 + if gain > 102: + gain = 102 + lna_gain = 8 * (gain // 18) + vga_gain = 2 * ((gain - lna_gain) // 2) + + self.params = { + "start_freq": start_freq, # MHz + "stop_freq": stop_freq, # MHz + "hops": 0, + "device_index": 0, + "sample_rate": 20e6, # sps + "bin_size": bin_size, # kHz + "interval": 0, # seconds + "gain": gain, + "lna_gain": lna_gain, + "vga_gain": vga_gain, + "ppm": 0, + "crop": 0, + "single_shot": single_shot + } + self.databuffer = {"timestamp": [], "x": [], "y": []} + + print("hackrf_sweep params:") + pprint.pprint(self.params) + print() + + def process_start(self): + """Start hackrf_sweep process""" + if not self.process and self.params: + settings = QtCore.QSettings() + cmdline = [ + settings.value("executable", "hackrf_sweep"), + "-f", "{}:{}".format(int(self.params["start_freq"]), + int(self.params["stop_freq"])), + "-B", + "-w", "{}".format(int(self.params["bin_size"]*1000)), + "-l", "{}".format(int(self.params["lna_gain"])), + "-g", "{}".format(int(self.params["vga_gain"])), + ] + + if self.params["single_shot"]: + cmdline.append("-1") + + self.process = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + universal_newlines=False) + + def parse_output(self, buf): + """Parse one buf of output from hackrf_sweep""" + (low_edge, high_edge) = struct.unpack('QQ', buf[:16]) + data = np.fromstring(buf[16:], dtype='= (self.params["stop_freq"]): + # We've reached the end of a pass, so sort and display it. + sorted_data = sorted(zip(self.databuffer["x"], self.databuffer["y"])) + self.databuffer["x"], self.databuffer["y"] = [list(x) for x in zip(*sorted_data)] + self.data_storage.update(self.databuffer) + + def run(self): + """hackrf_sweep thread main loop""" + self.process_start() + self.alive = True + self.powerThreadStarted.emit() + + while self.alive: + buf = self.process.stdout.read(4) + if buf: + (record_length,) = struct.unpack('I', buf) + buf = self.process.stdout.read(record_length) + if buf: + self.parse_output(buf) + + self.process_stop() + self.alive = False + self.powerThreadStopped.emit() diff --git a/qspectrumanalyzer/languages/qspectrumanalyzer_cs.ts b/qspectrumanalyzer/languages/qspectrumanalyzer_cs.ts index 90c721e..ada777e 100644 --- a/qspectrumanalyzer/languages/qspectrumanalyzer_cs.ts +++ b/qspectrumanalyzer/languages/qspectrumanalyzer_cs.ts @@ -81,7 +81,22 @@ - + + Frequency hops: {} | Sweep time: {:.2f} s | FPS: {:.2f} + + + + + N/A + + + + + About - QSpectrumAnalyzer + + + + QSpectrumAnalyzer {} @@ -111,23 +126,18 @@ - - &Settings... + + Start: - - &Quit + + Stop: - - Ctrl+Q - - - - - &About + + Bin size: @@ -151,18 +161,28 @@ - - Start: + + Main curve - - Stop: + + Colors... - - Bin size: + + Max. hold + + + + + Min. hold + + + + + Average @@ -175,49 +195,29 @@ ... - - - N/A - - - - - About - QSpectrumAnalyzer - - Persistence - - Average + + &Settings... - - Colors... + + &Quit - - Main curve + + Ctrl+Q - - Frequency hops: {} | Sweep time: {:.2f} s | FPS: {:.2f} - - - - - Max. hold - - - - - Min. hold + + &About @@ -228,11 +228,6 @@ Persistence - QSpectrumAnalyzer - - - Persistence length: - - Decay function: @@ -248,72 +243,87 @@ exponential + + + Persistence length: + + QSpectrumAnalyzerSettings - - - rtl_power - - - - - ... - - - - - rtl_power_fftw - - - - - &Backend: - - - - - E&xecutable: - - - - - &Waterfall history size: - - - - - Sa&mple rate: - - Select executable - QSpectrumAnalyzer - + Settings - QSpectrumAnalyzer - - soapy_power + + &Backend: + soapy_power + + + + + rx_power + + + + + rtl_power_fftw + + + + + rtl_power + + + + + hackrf_sweep + + + + + E&xecutable: + + + + + ... + + + + Device: - - rx_power + + Sa&mple rate: + + + + + &Waterfall history size: QSpectrumAnalyzerSmooth + + + Smoothing - QSpectrumAnalyzer + + &Window function: @@ -349,10 +359,5 @@ Window len&gth: - - - Smoothing - QSpectrumAnalyzer - - diff --git a/qspectrumanalyzer/plot.py b/qspectrumanalyzer/plot.py index 4fb284a..f2e1721 100644 --- a/qspectrumanalyzer/plot.py +++ b/qspectrumanalyzer/plot.py @@ -37,7 +37,7 @@ class SpectrumPlotWidget: self.posLabel = self.layout.addLabel(row=0, col=0, justify="right") self.plot = self.layout.addPlot(row=1, col=0) self.plot.showGrid(x=True, y=True) - self.plot.setLabel("left", "Power", units="dBm") + self.plot.setLabel("left", "Power", units="dB") self.plot.setLabel("bottom", "Frequency", units="Hz") self.plot.setLimits(xMin=0) self.plot.showButtons() @@ -204,7 +204,7 @@ class SpectrumPlotWidget: if self.plot.sceneBoundingRect().contains(pos): mousePoint = self.plot.vb.mapSceneToView(pos) self.posLabel.setText( - "f={:0.3f} MHz, P={:0.3f} dBm".format( + "f={:0.3f} MHz, P={:0.3f} dB".format( mousePoint.x() / 1e6, mousePoint.y() ) diff --git a/qspectrumanalyzer/qspectrumanalyzer_settings.ui b/qspectrumanalyzer/qspectrumanalyzer_settings.ui index 1ae149c..83e3877 100644 --- a/qspectrumanalyzer/qspectrumanalyzer_settings.ui +++ b/qspectrumanalyzer/qspectrumanalyzer_settings.ui @@ -48,6 +48,11 @@ rtl_power + + + hackrf_sweep + + diff --git a/qspectrumanalyzer/ui_qspectrumanalyzer_settings.py b/qspectrumanalyzer/ui_qspectrumanalyzer_settings.py index b14979e..fe5f280 100644 --- a/qspectrumanalyzer/ui_qspectrumanalyzer_settings.py +++ b/qspectrumanalyzer/ui_qspectrumanalyzer_settings.py @@ -39,6 +39,7 @@ class Ui_QSpectrumAnalyzerSettings(object): self.backendComboBox.addItem(_fromUtf8("")) self.backendComboBox.addItem(_fromUtf8("")) self.backendComboBox.addItem(_fromUtf8("")) + self.backendComboBox.addItem(_fromUtf8("")) self.formLayout.setWidget(0, QtGui.QFormLayout.FieldRole, self.backendComboBox) self.label = QtGui.QLabel(QSpectrumAnalyzerSettings) self.label.setObjectName(_fromUtf8("label")) @@ -109,6 +110,7 @@ class Ui_QSpectrumAnalyzerSettings(object): self.backendComboBox.setItemText(1, _translate("QSpectrumAnalyzerSettings", "rx_power", None)) self.backendComboBox.setItemText(2, _translate("QSpectrumAnalyzerSettings", "rtl_power_fftw", None)) self.backendComboBox.setItemText(3, _translate("QSpectrumAnalyzerSettings", "rtl_power", None)) + self.backendComboBox.setItemText(4, _translate("QSpectrumAnalyzerSettings", "hackrf_sweep", None)) self.label.setText(_translate("QSpectrumAnalyzerSettings", "E&xecutable:", None)) self.executableEdit.setText(_translate("QSpectrumAnalyzerSettings", "soapy_power", None)) self.executableButton.setText(_translate("QSpectrumAnalyzerSettings", "...", None))