aboutsummaryrefslogtreecommitdiff
path: root/octave/fskdemodgui.py
diff options
context:
space:
mode:
Diffstat (limited to 'octave/fskdemodgui.py')
-rw-r--r--octave/fskdemodgui.py220
1 files changed, 220 insertions, 0 deletions
diff --git a/octave/fskdemodgui.py b/octave/fskdemodgui.py
new file mode 100644
index 0000000..f1a4f29
--- /dev/null
+++ b/octave/fskdemodgui.py
@@ -0,0 +1,220 @@
+#!/usr/bin/env python3
+#
+# fsk_demod Statistics GUI
+# Accepts the stats output from fsk_demod on stdin, and plots it.
+#
+# Mark Jessop 2016-03-13 <[email protected]>
+#
+# NOTE: This is intended to be run on a 'live' stream of samples, and hence expects
+# updates at about 10Hz. Anything faster will fill up the input queue and be discarded.
+#
+# Call using:
+# <producer>| ./fsk_demod --cu8 -s --stats=100 2 $SDR_RATE $BAUD_RATE - - 2> >(python fskdemodgui.py --wide) | <consumer>
+#
+# Dependencies:
+# * Python (written for 2.7, only tested recently on 3+)
+# * numpy
+# * pyqtgraph
+# * PyQt5 (Or some Qt5 backend compatible with pyqtgraph)
+#
+#
+import sys, time, json, argparse
+from threading import Thread
+try:
+ from pyqtgraph.Qt import QtGui, QtCore
+except ImportError:
+ print("Could not import PyQt5 - is it installed?")
+ sys.exit(1)
+
+try:
+ import numpy as np
+except ImportError:
+ print("Could not import numpy - is it installed?")
+ sys.exit(1)
+
+try:
+ import pyqtgraph as pg
+except ImportError:
+ print("Could not import pyqtgraph - is it installed?")
+ sys.exit(1)
+
+try:
+ # Python 2
+ from Queue import Queue
+except ImportError:
+ # Python 3
+ from queue import Queue
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--wide", action="store_true", default=False, help="Alternate wide arrangement of widgets, for placement at bottom of 4:3 screen.")
+args = parser.parse_args()
+
+# Some settings...
+update_rate = 2 # Hz
+history_size = 100 # 10 seconds at 10Hz...
+history_scale = np.linspace((-1*history_size+1)/float(update_rate),0,history_size)
+
+# Input queue
+in_queue = Queue(1) # 1-element FIFO...
+
+win = pg.GraphicsWindow()
+win.setWindowTitle('FSK Demodulator Modem Statistics')
+
+
+# Plot objects
+ebno_plot = win.addPlot(title="Eb/No")
+ppm_plot = win.addPlot(title="Sample Clock Offset")
+if args.wide == False:
+ win.nextRow()
+else:
+ win.resize(1024,200)
+fest_plot =pg.PlotItem() # win.addPlot(title="Tone Frequency Estimation")
+eye_plot = win.addPlot(title="Eye Diagram")
+# Disable auto-ranging on eye plot and fix axes for a big speedup...
+spec_plot = win.addPlot(title="Spectrum")
+spec_plot.setYRange(0,40)
+spec_plot.setLabel('left','SNR (dB)')
+spec_plot.setLabel('bottom','FFT Bin')
+# Configure plot labels and scales.
+ebno_plot.setLabel('left','Eb/No (dB)')
+ebno_plot.setLabel('bottom','Time (seconds)')
+ebno_plot.setYRange(0,30)
+ppm_plot.setLabel('left','Clock Offset (ppm)')
+ppm_plot.setLabel('bottom','Time (seconds)')
+fest_plot.setLabel('left','Frequency (Hz)')
+fest_plot.setLabel('bottom','Time (seconds)')
+eye_plot.disableAutoRange()
+eye_plot.setYRange(0,1)
+eye_plot.setXRange(0,15)
+eye_xr = 15
+
+# Data arrays...
+ebno_data = np.zeros(history_size)
+ppm_data = np.zeros(history_size)
+fest_data = np.zeros((4,history_size))
+
+# Curve objects, so we can update them...
+spec_curve = spec_plot.plot([0])
+ebno_curve = ebno_plot.plot(x=history_scale,y=ebno_data)
+ppm_curve = ppm_plot.plot(x=history_scale,y=ppm_data)
+fest1_curve = fest_plot.plot(x=history_scale,y=fest_data[0,:],pen='r') # f1 = Red
+fest2_curve = fest_plot.plot(x=history_scale,y=fest_data[1,:],pen='g') # f2 = Blue
+fest3_curve = fest_plot.plot(x=history_scale,y=fest_data[2,:],pen='b') # f3 = Greem
+fest4_curve = fest_plot.plot(x=history_scale,y=fest_data[3,:],pen='m') # f4 = Magenta
+
+# Plot update function. Reads from queue, processes and updates plots.
+def update_plots():
+ global timeout,timeout_counter,eye_plot,ebno_curve, ppm_curve, fest1_curve, fest2_curve, ebno_data, ppm_data, fest_data, in_queue, eye_xr, spec_curve
+
+ try:
+ if in_queue.empty():
+ return
+ in_data = in_queue.get_nowait()
+ in_data = json.loads(in_data)
+ except Exception as e:
+
+ sys.stderr.write(str(e))
+ return
+
+ # Roll data arrays
+ ebno_data[:-1] = ebno_data[1:]
+ ppm_data[:-1] = ppm_data[1:]
+ fest_data = np.roll(fest_data,-1,axis=1)
+
+
+ # Try reading in the new data points from the dictionary.
+ try:
+ new_ebno = in_data['EbNodB']
+ new_ppm = in_data['ppm']
+ new_fest1 = in_data['f1_est']
+ new_fest2 = in_data['f2_est']
+ new_spec = in_data['samp_fft']
+ except Exception as e:
+ print("ERROR reading dict: %s" % e)
+
+ # Try reading in the other 2 tones.
+ try:
+ new_fest3 = in_data['f3_est']
+ new_fest4 = in_data['f4_est']
+ fest_data[2,-1] = new_fest3
+ fest_data[3,-1] = new_fest4
+ except:
+ # If we can't read these tones out of the dict, fill with NaN
+ fest_data[2,-1] = np.nan
+ fest_data[3,-1] = np.nan
+
+ # Add in new data points
+ ebno_data[-1] = new_ebno
+ ppm_data[-1] = new_ppm
+ fest_data[0,-1] = new_fest1
+ fest_data[1,-1] = new_fest2
+
+
+ # Update plots
+ spec_data_log = 20*np.log10(np.array(new_spec)+0.01)
+ spec_curve.setData(spec_data_log)
+ spec_plot.setYRange(spec_data_log.max()-50,spec_data_log.max()+10)
+ ebno_curve.setData(x=history_scale,y=ebno_data)
+ ppm_curve.setData(x=history_scale,y=ppm_data)
+ fest1_curve.setData(x=history_scale,y=fest_data[0,:],pen='r') # f1 = Red
+ fest2_curve.setData(x=history_scale,y=fest_data[1,:],pen='g') # f2 = Blue
+ fest3_curve.setData(x=history_scale,y=fest_data[2,:],pen='b') # f3 = Green
+ fest4_curve.setData(x=history_scale,y=fest_data[3,:],pen='m') # f4 = Magenta
+
+ #Now try reading in and plotting the eye diagram
+ try:
+ eye_data = np.array(in_data['eye_diagram'])
+
+ #eye_plot.disableAutoRange()
+ eye_plot.clear()
+ col_index = 0
+ for line in eye_data:
+ eye_plot.plot(line,pen=(col_index,eye_data.shape[0]))
+ col_index += 1
+ #eye_plot.autoRange()
+
+ #Quick autoranging for x-axis to allow for differing P and Ts values
+ if eye_xr != len(eye_data[0]) - 1:
+ eye_xr = len(eye_data[0]) - 1
+ eye_plot.setXRange(0,len(eye_data[0])-1)
+
+ except Exception as e:
+ pass
+
+
+timer = pg.QtCore.QTimer()
+timer.timeout.connect(update_plots)
+timer.start(1000/update_rate)
+
+
+# Thread to read from stdin and push into a queue to be processed.
+def read_input():
+ global in_queue
+
+ while True:
+ in_line = sys.stdin.readline()
+ if type(in_line) == bytes:
+ in_line = in_line.decode()
+
+ # Only push actual data into the queue...
+ # This stops sending heaps of empty strings into the queue when fsk_demod closes.
+ if in_line == "":
+ time.sleep(0.1)
+ continue
+
+ if not in_queue.full():
+ in_queue.put_nowait(in_line)
+
+
+read_thread = Thread(target=read_input)
+read_thread.daemon = True # Set as daemon, so when all other threads die, this one gets killed too.
+read_thread.start()
+
+## Start Qt event loop unless running in interactive mode or using pyside.
+if __name__ == '__main__':
+ import sys
+ if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
+ try:
+ QtGui.QApplication.instance().exec_()
+ except KeyboardInterrupt:
+ sys.exit(0)