GUI Example: wsa4000demo

wsa4000demo is a cross-platform GUI application built with the Qt toolkit and PySide bindings for Python.

You may run application by launching the wsa4000demo.py script in the examples/gui directory.

You may specify a device on the command line or open a device after the GUI has launched. Adding --reset to the command line parameters will cause the device to be reset to defaults after connecting.

wsa4000demo.py

This is the script that launches the application.

#!/usr/bin/env python

import sys
from PySide import QtGui
from gui import MainWindow

app = QtGui.QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())

gui.py

The main application window and GUI controls are created here.

MainWindow creates and handles the File | Open Device menu and wraps the MainPanel widget responsible for most of the interface.

All the buttons and controls and their callback functions are built in MainPanel and arranged on a grid. A SpectrumView is created and placed to left of the controls.

Note

This version calls MainPanel.update_screen() in a timer loop 20 times a second. This method makes a blocking call to capture the next packet and render it all in the same thread as the application. This can make the interface slow or completely unresponsive if you lose connection to the device.

The next release will move the blocking call and data processing into a separate process.

import sys
import socket

from PySide import QtGui, QtCore
from spectrum import SpectrumView
from util import frequency_text

from thinkrf.devices import WSA4000
from thinkrf.util import read_data_and_reflevel
from thinkrf.numpy_util import compute_fft

DEVICE_FULL_SPAN = 125e6
REFRESH_CHARTS = 0.05

class MainWindow(QtGui.QMainWindow):
    def __init__(self, name=None):
        super(MainWindow, self).__init__()
        self.initUI()

        self.dut = None
        if len(sys.argv) > 1:
            self.open_device(sys.argv[1])
        else:
            self.open_device_dialog()
        self.show()

        timer = QtCore.QTimer(self)
        timer.timeout.connect(self.update_charts)
        timer.start(REFRESH_CHARTS)

    def initUI(self):
        openAction = QtGui.QAction('&Open Device', self)
        openAction.triggered.connect(self.open_device_dialog)
        exitAction = QtGui.QAction('&Exit', self)
        exitAction.setShortcut('Ctrl+Q')
        exitAction.triggered.connect(self.close)
        menubar = self.menuBar()
        fileMenu = menubar.addMenu('&File')
        fileMenu.addAction(openAction)
        fileMenu.addAction(exitAction)

        self.setWindowTitle('ThinkRF WSA4000')

    def open_device_dialog(self):
        name, ok = QtGui.QInputDialog.getText(self, 'Open Device',
            'Enter a hostname or IP address:')
        while True:
            if not ok:
                return

            try:
                self.open_device(name)
                break
            except socket.error:
                name, ok = QtGui.QInputDialog.getText(self, 'Open Device',
                    'Connection Failed, please try again\n\n'
                    'Enter a hostname or IP address:')

    def open_device(self, name):
        dut = WSA4000()
        dut.connect(name)
        dut.request_read_perm()
        if '--reset' in sys.argv:
            dut.reset()

        self.dut = dut
        self.setCentralWidget(MainPanel(dut))
        self.setWindowTitle('ThinkRF WSA4000: %s' % name)

    def update_charts(self):
        if self.dut is None:
            return
        self.centralWidget().update_screen()


class MainPanel(QtGui.QWidget):

    def __init__(self, dut):
        super(MainPanel, self).__init__()
        self.dut = dut
        self.get_freq_mhz()
        self.get_decimation()
        self.decimation_points = None
        data, reflevel = read_data_and_reflevel(dut)
        self.screen = SpectrumView(
            compute_fft(dut, data, reflevel),
            self.center_freq,
            self.decimation_factor)
        self.initUI()

    def initUI(self):
        grid = QtGui.QGridLayout()
        grid.setSpacing(10)
        grid.addWidget(self.screen, 0, 0, 8, 1)
        grid.setColumnMinimumWidth(0, 400)

        y = 0
        grid.addWidget(self._antenna_control(), y, 1, 1, 2)
        grid.addWidget(self._bpf_control(), y, 3, 1, 2)
        y += 1
        grid.addWidget(self._gain_control(), y, 1, 1, 2)
        grid.addWidget(QtGui.QLabel('IF Gain:'), y, 3, 1, 1)
        grid.addWidget(self._ifgain_control(), y, 4, 1, 1)
        y += 1
        freq, steps, freq_plus, freq_minus = self._freq_controls()
        grid.addWidget(QtGui.QLabel('Center Freq:'), y, 1, 1, 1)
        grid.addWidget(freq, y, 2, 1, 2)
        grid.addWidget(QtGui.QLabel('MHz'), y, 4, 1, 1)
        y += 1
        grid.addWidget(steps, y, 2, 1, 2)
        grid.addWidget(freq_minus, y, 1, 1, 1)
        grid.addWidget(freq_plus, y, 4, 1, 1)
        y += 1
        span, rbw = self._span_rbw_controls()
        grid.addWidget(span, y, 1, 1, 2)
        grid.addWidget(rbw, y, 3, 1, 2)

        self.setLayout(grid)
        self.show()

    def _antenna_control(self):
        antenna = QtGui.QComboBox(self)
        antenna.addItem("Antenna 1")
        antenna.addItem("Antenna 2")
        antenna.setCurrentIndex(self.dut.antenna() - 1)
        def new_antenna():
            self.dut.antenna(int(antenna.currentText().split()[-1]))
        antenna.currentIndexChanged.connect(new_antenna)
        return antenna

    def _bpf_control(self):
        bpf = QtGui.QComboBox(self)
        bpf.addItem("BPF On")
        bpf.addItem("BPF Off")
        bpf.setCurrentIndex(0 if self.dut.preselect_filter() else 1)
        def new_bpf():
            self.dut.preselect_filter("On" in bpf.currentText())
        bpf.currentIndexChanged.connect(new_bpf)
        return bpf

    def _gain_control(self):
        gain = QtGui.QComboBox(self)
        gain_values = ['High', 'Med', 'Low', 'VLow']
        for g in gain_values:
            gain.addItem("RF Gain: %s" % g)
        gain_index = [g.lower() for g in gain_values].index(self.dut.gain())
        gain.setCurrentIndex(gain_index)
        def new_gain():
            self.dut.gain(gain.currentText().split()[-1].lower())
        gain.currentIndexChanged.connect(new_gain)
        return gain

    def _ifgain_control(self):
        ifgain = QtGui.QSpinBox(self)
        ifgain.setRange(-10, 34)
        ifgain.setSuffix(" dB")
        ifgain.setValue(int(self.dut.ifgain()))
        def new_ifgain():
            self.dut.ifgain(ifgain.value())
        ifgain.valueChanged.connect(new_ifgain)
        return ifgain

    def _freq_controls(self):
        freq = QtGui.QLineEdit("")
        def read_freq():
            freq.setText("%0.1f" % self.get_freq_mhz())
        read_freq()
        def write_freq():
            try:
                f = float(freq.text())
            except ValueError:
                return
            self.set_freq_mhz(f)
        freq.editingFinished.connect(write_freq)

        steps = QtGui.QComboBox(self)
        steps.addItem("Adjust: 1 MHz")
        steps.addItem("Adjust: 2.5 MHz")
        steps.addItem("Adjust: 10 MHz")
        steps.addItem("Adjust: 25 MHz")
        steps.addItem("Adjust: 100 MHz")
        steps.setCurrentIndex(2)
        def freq_step(factor):
            try:
                f = float(freq.text())
            except ValueError:
                return read_freq()
            delta = float(steps.currentText().split()[1]) * factor
            freq.setText("%0.1f" % (f + delta))
            write_freq()
        freq_minus = QtGui.QPushButton('-')
        freq_minus.clicked.connect(lambda: freq_step(-1))
        freq_plus = QtGui.QPushButton('+')
        freq_plus.clicked.connect(lambda: freq_step(1))

        return freq, steps, freq_plus, freq_minus

    def _span_rbw_controls(self):
        span = QtGui.QComboBox(self)
        decimation_values = [0] + [2 ** x for x in range(2, 10)]
        span.addItem("Span: %s" % frequency_text(DEVICE_FULL_SPAN)) # 0 is special
        for d in decimation_values[1:]:
            span.addItem("Span: %s" % frequency_text(DEVICE_FULL_SPAN / d))
        span.setCurrentIndex(decimation_values.index(self.dut.decimation()))
        def new_span():
            self.set_decimation(decimation_values[span.currentIndex()])
            build_rbw()
        span.currentIndexChanged.connect(new_span)

        rbw = QtGui.QComboBox(self)
        points_values = [2 ** x for x in range(8, 16)]
        rbw.addItems([str(p) for p in points_values])
        def build_rbw():
            d = self.decimation_factor
            for i, p in enumerate(points_values):
                r = DEVICE_FULL_SPAN / d / p
                rbw.setItemText(i, "RBW: %s" % frequency_text(r))
                if self.decimation_points and self.decimation_points == d * p:
                    rbw.setCurrentIndex(i)
            self.points = points_values[rbw.currentIndex()]
        build_rbw()
        def new_rbw():
            self.points = points_values[rbw.currentIndex()]
            self.decimation_points = self.decimation_factor * self.points
        rbw.setCurrentIndex(points_values.index(1024))
        new_rbw()
        rbw.currentIndexChanged.connect(new_rbw)

        return span, rbw


    def update_screen(self):
        data, reflevel = read_data_and_reflevel(
            self.dut,
            self.points)
        self.screen.update_data(
            compute_fft(self.dut, data, reflevel),
            self.center_freq,
            self.decimation_factor)

    def get_freq_mhz(self):
        self.center_freq = self.dut.freq()
        return self.center_freq / 1e6

    def set_freq_mhz(self, f):
        self.center_freq = f * 1e6
        self.dut.freq(self.center_freq)

    def get_decimation(self):
        d = self.dut.decimation()
        self.decimation_factor = 1 if d == 0 else d

    def set_decimation(self, d):
        self.decimation_factor = 1 if d == 0 else d
        self.dut.decimation(d)

spectrum.py

The SpectrumView widget is divided into three parts:

  • SpectrumViewPlot contains the plotted spectrum data.
  • SpectrumViewLeftAxis displays the dBm axis along the left.
  • SpectrumViewBottomAxis displays the MHz axis across the bottom.

The utility functions dBm_labels and MHz_labels compute the positions and labels for each axis.

import numpy
import itertools
from PySide import QtGui, QtCore

TOP_MARGIN = 20
RIGHT_MARGIN = 20
LEFT_AXIS_WIDTH = 70
BOTTOM_AXIS_HEIGHT = 40
AXIS_THICKNESS = 1

DBM_TOP = 20
DBM_BOTTOM = -140
DBM_STEPS = 9

class SpectrumView(QtGui.QWidget):
    """
    A complete spectrum view with left/bottom axis and plot
    """

    def __init__(self, powdata, center_freq, decimation_factor):
        super(SpectrumView, self).__init__()
        self.plot = SpectrumViewPlot(powdata, center_freq, decimation_factor)
        self.left = SpectrumViewLeftAxis()
        self.bottom = SpectrumViewBottomAxis()
        self.bottom.update_params(center_freq, decimation_factor)
        self.initUI()

    def initUI(self):
        grid = QtGui.QGridLayout()
        grid.setSpacing(0)
        grid.addWidget(self.left, 0, 0, 2, 1)
        grid.addWidget(self.plot, 0, 1, 1, 1)
        grid.addWidget(self.bottom, 1, 1, 1, 1)
        grid.setRowStretch(0, 1)
        grid.setColumnStretch(1, 1)
        grid.setColumnMinimumWidth(0, LEFT_AXIS_WIDTH)
        grid.setRowMinimumHeight(1, BOTTOM_AXIS_HEIGHT)

        grid.setContentsMargins(0, 0, 0, 0)
        self.setLayout(grid)

    def update_data(self, powdata, center_freq, decimation_factor):
        if (self.plot.center_freq, self.plot.decimation_factor) != (
                center_freq, decimation_factor):
            self.bottom.update_params(center_freq, decimation_factor)
        self.plot.update_data(powdata, center_freq, decimation_factor)


def dBm_labels(height):
    """
    return a list of (position, label_text) tuples where position
    is a value between 0 (top) and height (bottom).
    """
    # simple, fixed implementation for now
    dBm_labels = [str(d) for d in
        numpy.linspace(DBM_TOP, DBM_BOTTOM, DBM_STEPS)]
    y_values = numpy.linspace(0, height, DBM_STEPS)
    return zip(y_values, dBm_labels)

class SpectrumViewLeftAxis(QtGui.QWidget):
    """
    The left axis of a spectrum view showing dBm range

    This widget includes the space to the left of the bottom axis
    """
    def paintEvent(self, e):
        qp = QtGui.QPainter()
        qp.begin(self)
        size = self.size()
        self.drawAxis(qp, size.width(), size.height())
        qp.end()

    def drawAxis(self, qp, width, height):
        qp.fillRect(0, 0, width, height, QtCore.Qt.black)
        qp.setPen(QtCore.Qt.gray)
        qp.fillRect(
            width - AXIS_THICKNESS,
            TOP_MARGIN,
            AXIS_THICKNESS,
            height - BOTTOM_AXIS_HEIGHT + AXIS_THICKNESS - TOP_MARGIN,
            QtCore.Qt.gray)

        for y, txt in dBm_labels(height - BOTTOM_AXIS_HEIGHT - TOP_MARGIN):
            qp.drawText(
                0,
                y + TOP_MARGIN - 10,
                LEFT_AXIS_WIDTH - 5,
                20,
                QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter,
                txt)

def MHz_labels(width, center_freq, decimation_factor):
    """
    return a list of (position, label_text) tuples where position
    is a value between 0 (left) and width (right).
    """
    df = float(decimation_factor)
    # simple, fixed implementation for now
    offsets = (-50, -25, 0, 25, 50)
    freq_labels = [str(center_freq / 1e6 + d/df) for d in offsets]
    x_values = [(d + 62.5) * width / 125 for d in offsets]
    return zip(x_values, freq_labels)

class SpectrumViewBottomAxis(QtGui.QWidget):
    """
    The bottom axis of a spectrum view showing frequencies
    """
    def update_params(self, center_freq, decimation_factor):
        self.center_freq = center_freq
        self.decimation_factor = decimation_factor
        self.update()

    def paintEvent(self, e):
        qp = QtGui.QPainter()
        qp.begin(self)
        size = self.size()
        self.drawAxis(qp, size.width(), size.height())
        qp.end()

    def drawAxis(self, qp, width, height):
        qp.fillRect(0, 0, width, height, QtCore.Qt.black)
        qp.setPen(QtCore.Qt.gray)
        qp.fillRect(
            0,
            0,
            width - RIGHT_MARGIN,
            AXIS_THICKNESS,
            QtCore.Qt.gray)

        for x, txt in MHz_labels(
                width - RIGHT_MARGIN,
                self.center_freq,
                self.decimation_factor):
            qp.drawText(
                x - 40,
                5,
                80,
                BOTTOM_AXIS_HEIGHT - 10,
                QtCore.Qt.AlignTop | QtCore.Qt.AlignHCenter,
                txt)



class SpectrumViewPlot(QtGui.QWidget):
    """
    The data plot of a spectrum view
    """
    def __init__(self, powdata, center_freq, decimation_factor):
        super(SpectrumViewPlot, self).__init__()
        self.powdata = powdata
        self.center_freq = center_freq
        self.decimation_factor = decimation_factor

    def update_data(self, powdata, center_freq, decimation_factor):
        self.powdata = powdata
        self.center_freq = center_freq
        self.decimation_factor = decimation_factor
        self.update()

    def paintEvent(self, e):
        qp = QtGui.QPainter()
        qp.begin(self)
        self.drawLines(qp)
        qp.end()

    def drawLines(self, qp):
        size = self.size()
        width = size.width()
        height = size.height()
        qp.fillRect(0, 0, width, height, QtCore.Qt.black)

        qp.setPen(QtGui.QPen(QtCore.Qt.gray, 1, QtCore.Qt.DotLine))
        for y, txt in dBm_labels(height - TOP_MARGIN):
            qp.drawLine(
                0,
                y + TOP_MARGIN,
                width - RIGHT_MARGIN - 1,
                y + TOP_MARGIN)
        for x, txt in MHz_labels(
                width - RIGHT_MARGIN,
                self.center_freq,
                self.decimation_factor):
            qp.drawLine(
                x,
                TOP_MARGIN,
                x,
                height - 1)

        qp.setPen(QtCore.Qt.green)

        y_values = height - 1 - (self.powdata - DBM_BOTTOM) * (
            float(height - TOP_MARGIN) / (DBM_TOP - DBM_BOTTOM))
        x_values = numpy.linspace(0, width - 1 - RIGHT_MARGIN,
            len(self.powdata))

        path = QtGui.QPainterPath()
        points = itertools.izip(x_values, y_values)
        path.moveTo(*next(points))
        for x,y in points:
            path.lineTo(x, y)
        qp.drawPath(path)

util.py

Pretty-print frequency values

def frequency_text(hz):
    """
    return hz as readable text in Hz, kHz, MHz or GHz
    """
    if hz < 1e3:
        return "%.3f Hz" % hz
    elif hz < 1e6:
        return "%.3f kHz" % (hz / 1e3)
    elif hz < 1e9:
        return "%.3f MHz" % (hz / 1e6)
    return "%.3f GHz" % (hz / 1e9)

Project Versions

Table Of Contents

Previous topic

Basic Examples

This Page