diff options
Diffstat (limited to 'octave/fskdemodgui.py')
| -rw-r--r-- | octave/fskdemodgui.py | 220 |
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) |
