0

I am currently working on a project of simple spectrum analyzer that cooperates with PlutoSDR, which is programmable radio. I created a simple GUI with QTDesigner, then pyuic'ed it into python code (my version is 3.8) Here's my code: (I've cut out some irrelevant parts)


# Form implementation generated from reading ui file 'signal_analyzer.ui'
#
# Created by: PyQt5 UI code generator 5.15.6
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again.  Do not edit this file unless you know what you are doing.
import pyqtgraph
from PyQt5 import QtCore, QtGui, QtWidgets
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.widgets import PlotWidget
import sys
import numpy as np
import argparse
import adi
import time
import threading
import pyqtgraph as pg

np.seterr(divide='ignore')

class FakePluto:
    # this is the class that simulates the radio if it's not connected to my computer
    """
    perform some operations
    """
    # output vector of data
    return samples[:self.rx_buffer_size]

class SpectrumAnalyzerThread(threading.Thread):
    def __init__(self, sdr, *args, **kwargs):
        super().__init__(*args, **kwargs)
        """
        set some initial parmeters
        """
        self.sdr = sdr
        elf.start_freq = int(70e6)
        self.end_freq = int(150e6)
        self.settings_changed = True
        self.result = None

    @property
    def steps_per_scan(self):
        return (self.end_freq - self.start_freq) // self.step_size

    def setStop(self):
        self.stop = True
        print("stop")

    def setStart(self):
        self.stop = False
        print("start")

    def setStopFreq(self, win):
        self.stop_freq = int(win.lineEdit.text)
        self.start_freq += (self.stop_freq - self.start_freq) % self.step_size

    def setStartFreq(self, win):
        self.start_freq = int(win.lineEdit_2.text)
        self.stop_freq += (self.stop_freq - self.start_freq) % self.step_size

    def reconfigure(self, start_freq, end_freq, step_size):
        self.start_freq = int(start_freq)
        self.end_freq = int(end_freq)
        self.step_size = int(step_size)
        if (self.end_freq - self.start_freq) % self.step_size != 0:
            raise Exception('range is not a multiple of the step size')
        self.settings_changed = True

    def run(self):
        while not self.stop:
            print("2", end = "\r")
            if self.settings_changed:
                self._current_freq = self.start_freq

                self.result = np.zeros(self.steps_per_scan * self.samples_per_step)

                self.sdr.gain_control_mode_chan0 = 'manual'
                self.sdr.rx_hardwaregain_chan0 = self.gain
                self.sdr.sample_rate = self.step_size
                self.sdr.rx_rf_bandwidth = self.step_size
                self.sdr.rx_buffer_size = self.samples_per_step

                self.settings_changed = False
            else:
                if self._current_freq + self.step_size >= self.end_freq:
                    self._current_freq = self.start_freq
                else:
                    self._current_freq += self.step_size
            self.sdr.rx_lo = self._current_freq + self.step_size//2

            self.sdr.rx()  # skip one sample
            rx_samples = self.sdr.rx()

            psd = np.abs(np.fft.fftshift(np.fft.fft(rx_samples))) ** 2
            assert len(psd) == self.samples_per_step
            start_idx = (self._current_freq - self.start_freq) // self.step_size * self.samples_per_step
            self.result[start_idx:start_idx + self.samples_per_step] = psd
            #print(self.result.tolist())

class Ui_MainWindow(QtWidgets.QMainWindow):
    def setupUi(self, MainWindow, analyzer_thread):
        """
        define UI elements - this part was generated from .UI file
        """
        self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
        self.lineEdit.setGeometry(QtCore.QRect(800, 140, 231, 25))
        self.lineEdit.setObjectName("lineEdit")
        
        self.runButton = QtWidgets.QPushButton(self.centralwidget)
        self.runButton.setGeometry(QtCore.QRect(790, 310, 161, 51))
        self.runButton.setObjectName("runButton")
        
        self.stopButton = QtWidgets.QPushButton(self.centralwidget)
        self.stopButton.setGeometry(QtCore.QRect(790, 380, 161, 51))
        self.stopButton.setObjectName("stopButton")
        

        self.retranslateUi(MainWindow)

        # connect gui elements with functions
        self.stopButton.clicked.connect(analyzer_thread.setStop) # type: ignore
        self.runButton.clicked.connect(analyzer_thread.setStart) # type: ignore
        self.lineEdit.textChanged[str].connect(analyzer_thread.setStartFreq)
        #self.lineEdit_2.textChanged[str].connect(analyzer_thread.setStopFreq)
        #self.sweepButton.clicked.connect(self.widget.singleSweep) # type: ignore
        #self.lineEdit_3.modified.connect()
        QtCore.QMetaObject.connectSlotsByName(self)

    # function below is just for testing
    def tomatoes(self, analyzer_thread):
        print(analyzer_thread.start_freq)

    def retranslateUi(self, MainWindow):
        """
        UI retranslation
        """

def main():

    # this part checks input script arguments, currently I'm using --simulate
    parser = argparse.ArgumentParser(description='SDR Spectrum Analyzer for PlutoSDR')
    parser.add_argument('pluto_uri', nargs='?', default=adi.Pluto._uri_auto,
                        help=f'URI of the PlutoSDR device (default: "{adi.Pluto._uri_auto}")')
    parser.add_argument('--simulate', action='store_true',
                        help='Simulate by generating random noise instead of querying the Pluto device')
    args = parser.parse_args()
    if args.simulate:
        sdr = FakePluto()
    else:
        sdr = adi.Pluto(args.pluto_uri)

    # create and start the thread
    analyzer_thread = SpectrumAnalyzerThread(sdr)
    analyzer_thread.start()


    app = QtGui.QApplication(sys.argv)
    win = Ui_MainWindow()
    win.show()
    win.setupUi(win, analyzer_thread)

    # this is the function that refreshes the plotWindow in GUI
    def update():
        if analyzer_thread.result is not None and not analyzer_thread.settings_changed:
            print("1", end = "\r")
            psd = analyzer_thread.result
            num_bins = analyzer_thread.samples_per_step
            if len(psd) % num_bins != 0:
                raise Exception('num_bins is not a multiple of sample count')

            binned = psd.reshape((-1, len(psd) // num_bins)).sum(axis=1)

            binned_dB = 10 * np.log10(binned)
            f = np.linspace(analyzer_thread.start_freq, analyzer_thread.end_freq, len(binned_dB))
            #spectrum_trace.setData(f, binned_dB)
            win.graphicsView.clear()
            win.graphicsView.plot(f, binned_dB)
            dispMax = str(np.amax(binned_dB))
            win.currentMaxAmp.display(dispMax[0:5])

    # update function is connected to the timer
    timer = QtCore.QTimer()
    timer.timeout.connect(update)
    timer.start(1)

    win.tomatoes(analyzer_thread)

    app.exec()

    analyzer_thread.stop = True
    analyzer_thread.join()

if __name__ == '__main__':
    main()

Now, I have two independent problems.

First When I press "Stop" on the GUI, which sets analyzer_thread stop value to 'true', this works correctly (i. e. run function from analyzer_thread stops executing), though the update function is still running - the plot keeps refreshing but with the same values. Yet when I hit "start", the analyzer_thread doesn't start over. I have no idea what causes that.

Second When I try to change stop frequency value, which should call analyzer_thread.setStopFreq I get this message:

Traceback (most recent call last):
  File "main_window_ui.py", line 77, in setStopFreq
    self.stop_freq = int(win.lineEdit.text)
AttributeError: 'str' object has no attribute 'lineEdit'

which I think is my mistake while connecting GUI objects to functions, but I couldn't figure out how to fix it. It seems there's a problem with function's arguments, yet when I call tomatoes function from main, it works despite having the same argument (analyzer_thread).

I know that my code is messy and sorry for that. The processing part was done by another person, my role is to make it work with GUI. I am attaching the main window view: https://i.stack.imgur.com/vUZjj.png

I'll be thankful for any help :)

wojteq45
  • 1
  • 1
  • 2
    The argument of `textChanged` is a string, just do `def setStopFreq(self, value):` `self.stop_freq = int(value)`. Other than that, understanding your problem is a bit difficult, as your code is very convoluted (and with unknown imports). Two important suggestions, though: 1. move `import pyqtgraph` *after* the PyQt import; 2. you should consider the warning on top of the file more seriously: pyuic generated files should *never* be manually edited; read how to properly use those files in the official guidelines on [using Designer](//www.riverbankcomputing.com/static/Docs/PyQt5/designer.html). – musicamante Jan 28 '22 at 19:24
  • Thank you, it worked! However, now that I wanted to use `editingFinished` signal (which has no arguments) instead of `textChanged`, I cannot pass string to the function I'm calling. The message is ```there is no matching overloaded signal``` which makes sense, but I have no idea how to overload it or work it around another way. As for editing UI, I actually feel pretty confident about what I can and what I cannot do within it. So far everything worked well for me. – wojteq45 Jan 31 '22 at 11:47

1 Answers1

0

Your code wont resume running, because when you set self.stop to True once, whole ,,run'' loop terminates and self.run function ends its job, terminating thread. You should rather try something like:

while True:
    if not self.stop:
        #do your code
    else:
        time.sleep(0.001) #

And add other possibility to terminate thread completly.

Your functions should rather be named suspend/resume, not stop and start.

Maciej Wrobel
  • 640
  • 4
  • 11
  • That is very sensible answer! Thank you for the explanation. Just out of curiosity - is that the 'proper' way to handle pauses in thread execution, or is there any other, more elegant way of doing it? I am satisfied with your solution though. – wojteq45 Jan 31 '22 at 11:52
  • actually it is rather poor's man solution - and it will consume some of CPU time, but for common solutions it may be enough. There are more elegant solutions, i.e. using queues or signals. See for example discussions of https://stackoverflow.com/questions/3262346/how-to-pause-and-resume-a-thread-using-the-threading-module – Maciej Wrobel Jan 31 '22 at 13:29