#!/usr/bin/env python # # fsk_demod Statistics GUI # Accepts the stats output from fsk_demod on stdin, and plots it. # # Mark Jessop 2016-03-13 # # 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: # | ./fsk_demod --cu8 -s --stats=100 2 $SDR_RATE $BAUD_RATE - - 2> >(python fskdemodgui.py --wide) | # # import sys, time, json, argparse from threading import Thread from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg 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,25) 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)*np.nan ppm_data = np.zeros(history_size)*np.nan fest_data = np.zeros((4,history_size))*np.nan # 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).encode('ascii')) 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().decode('ascii') # 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)