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 pyrf.devices.thinkrf import WSA4000
from pyrf.util import read_data_and_reflevel
from pyrf.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 = [1] + [2 ** x for x in range(2, 10)]
for d in decimation_values:
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)