#!/usr/bin/env python """ Oscilloscope + spectrum analyser in Python for the NIOS server. Modified version from the original code by R. Fearick. Giuseppe Venturini, July 2012-2013 Original copyright notice follows. The same license applies. ------------------------------------------------------------ Copyright (C) 2008, Roger Fearick, University of Cape Town This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ------------------------------------------------------------ Version 0.1 This code provides a two-channel oscilloscope and spectrum analyzer. Dependencies: Python 2.6+ numpy -- numerics, fft PyQt4, PyQwt5 -- gui, graphics Optional packages: pyspectrum -- expert mode spectrum calculation Typically, a modification of the Python path and ld library is necessary, like this: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. export PYTHONPATH=$PYTHONPATH:. The code can be adjusted for different sampling rates and chunks lengths. The interface, based on qwt, uses a familar 'knob based' layout so that it approximates an analogue scope. Traces can be averaged to reduce influence of noise. A cross hair status display permits the reading of values off the screen. Printing and exporting CSV and PDF files are provided. FFT options - by default we use the periogram algorithm from pyspectrum [1] - not in Debian stable but available through pypi and easy_install. [1] https://www.assembla.com/spaces/PySpectrum/wiki - If 'pyspectrum' is not available, we fallback to using the FFT method from numpy to compute the PSD. - Using numpy to calculate the FFT can be forced setting: USE_NUMPY_FFT = True in the following code. - additionally, it is possible to use matplotlib.psd(). -> you need to modify the sources to do so. INSTALLING pyspectrum The package pyspectrum can be installed with either: 'pip install spectrum' """ import sys import struct import subprocess import time import os.path import ConfigParser import importlib from PyQt4 import Qt from PyQt4 import Qwt5 as Qwt import numpy as np import numpy.fft as FFT # part of this package -- csv interface and toolbar icons from . import csvlib, icons, utils import dualscope123.probes from dualscope123.probes import eth_nios # scope configuration CHANNELS = 2 DEFAULT_TIMEBASE = 0.01 BOTH12 = 0 CH1 = 1 CH2 = 2 scopeheight = 500 #px scopewidth = 800 #px SELECTEDCH = BOTH12 TIMEPENWIDTH = 1 FFTPENWIDTH = 2 # status messages freezeInfo = 'Freeze: Press mouse button and drag' cursorInfo = 'Cursor Pos: Press mouse button in plot region' # FFT CONFIG USE_NUMPY_FFT = False try: import spectrum print "(II) spectrum MODULE FOUND" SPECTRUM_MODULE = True except ImportError: print "(WW) PSD: spectrum MODULE NOT FOUND" SPECTRUM_MODULE = False if USE_NUMPY_FFT: print "(WW) SPECTRUM MODULE DISABLED in source" SPECTRUM_MODULE = False if not SPECTRUM_MODULE: print "(WW) PSD: using FFTs through NUMPY.fftpack" # utility classes class LogKnob(Qwt.QwtKnob): """ Provide knob with log scale """ def __init__(self, *args): apply(Qwt.QwtKnob.__init__, (self,) + args) self.setScaleEngine(Qwt.QwtLog10ScaleEngine()) def setRange(self, minR, maxR, step=.333333): self.setScale(minR, maxR) Qwt.QwtKnob.setRange(self, np.log10(minR), np.log10(maxR), step) def setValue(self, val): Qwt.QwtKnob.setValue(self, np.log10(val)) class LblKnob: """ Provide knob with a label """ def __init__(self, wgt, x, y, name, logscale=0): if logscale: self.knob = LogKnob(wgt) else: self.knob = Qwt.QwtKnob(wgt) color = Qt.QColor(200, 200, 210) self.knob.palette().setColor(Qt.QPalette.Active, Qt.QPalette.Button, color) self.lbl = Qt.QLabel(name, wgt) self.knob.setGeometry(x, y, 140, 100) # oooh, eliminate this ... if name[0] == 'o': self.knob.setKnobWidth(40) self.lbl.setGeometry(x, y+90, 140, 15) self.lbl.setAlignment(Qt.Qt.AlignCenter) def setRange(self, *args): apply(self.knob.setRange, args) def setValue(self, *args): apply(self.knob.setValue, args) def setScaleMaxMajor(self, *args): apply(self.knob.setScaleMaxMajor, args) class Scope(Qwt.QwtPlot): """ Oscilloscope display widget """ def __init__(self, *args): apply(Qwt.QwtPlot.__init__, (self,) + args) self.setTitle('Scope') self.setCanvasBackground(Qt.Qt.white) # grid self.grid = Qwt.QwtPlotGrid() self.grid.enableXMin(True) self.grid.setMajPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine)) self.grid.attach(self) # axes self.enableAxis(Qwt.QwtPlot.yRight) self.setAxisTitle(Qwt.QwtPlot.xBottom, 'Time [s]') self.setAxisTitle(Qwt.QwtPlot.yLeft, 'Amplitude []') self.setAxisMaxMajor(Qwt.QwtPlot.xBottom, 10) self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 0) self.setAxisScaleEngine(Qwt.QwtPlot.yRight, Qwt.QwtLinearScaleEngine()) self.setAxisMaxMajor(Qwt.QwtPlot.yLeft, 10) self.setAxisMaxMinor(Qwt.QwtPlot.yLeft, 0) self.setAxisMaxMajor(Qwt.QwtPlot.yRight, 10) self.setAxisMaxMinor(Qwt.QwtPlot.yRight, 0) # curves for scope traces: 2 first so 1 is on top self.curve2 = Qwt.QwtPlotCurve('Trace2') self.curve2.setSymbol(Qwt.QwtSymbol(Qwt.QwtSymbol.Ellipse, Qt.QBrush(2), Qt.QPen(Qt.Qt.darkMagenta), Qt.QSize(3, 3))) self.curve2.setPen(Qt.QPen(Qt.Qt.magenta, TIMEPENWIDTH)) self.curve2.setYAxis(Qwt.QwtPlot.yRight) self.curve2.attach(self) self.curve1 = Qwt.QwtPlotCurve('Trace1') self.curve1.setSymbol(Qwt.QwtSymbol(Qwt.QwtSymbol.Ellipse, Qt.QBrush(2), Qt.QPen(Qt.Qt.darkBlue), Qt.QSize(3, 3))) self.curve1.setPen(Qt.QPen(Qt.Qt.blue, TIMEPENWIDTH)) self.curve1.setYAxis(Qwt.QwtPlot.yLeft) self.curve1.attach(self) # default settings self.triggerval = 0.10 self.triggerCH = None self.triggerslope = 0 self.maxamp = 100.0 self.maxamp2 = 100.0 self.freeze = 0 self.average = 0 self.autocorrelation = 0 self.avcount = 0 self.datastream = None self.offset1 = 0.0 self.offset2 = 0.0 self.maxtime = 0.1 # set data # NumPy: f, g, a and p are arrays! self.dt = 1.0/samplerate self.f = np.arange(0.0, 10.0, self.dt) self.a1 = 0.0*self.f self.a2 = 0.0*self.f self.curve1.setData(self.f, self.a1) self.curve2.setData(self.f, self.a2) # start self.timerEvent() callbacks running self.timer_id = self.startTimer(self.maxtime*100+50) # plot self.replot() # convenience methods for knob callbacks def setMaxAmp(self, val): self.maxamp = val def setMaxAmp2(self, val): self.maxamp2 = val def setMaxTime(self, val): self.maxtime = val def setOffset1(self, val): self.offset1 = val def setOffset2(self, val): self.offset2 = val def setTriggerLevel(self, val): self.triggerval = val def setTriggerCH(self, val): self.triggerCH = val def setTriggerSlope(self, val): self.triggerslope = val # plot scope traces def setDisplay(self): l = len(self.a1) if SELECTEDCH == BOTH12: self.curve1.setData(self.f[0:l], self.a1[:l]+self.offset1*self.maxamp) self.curve2.setData(self.f[0:l], self.a2[:l]+self.offset2*self.maxamp2) elif SELECTEDCH == CH2: self.curve1.setData([0.0,0.0], [0.0,0.0]) self.curve2.setData(self.f[0:l], self.a2[:l]+self.offset2*self.maxamp2) elif SELECTEDCH == CH1: self.curve1.setData(self.f[0:l], self.a1[:l]+self.offset1*self.maxamp) self.curve2.setData([0.0,0.0], [0.0,0.0]) self.replot() def getValue(self, index): return self.f[index], self.a[index] def setAverage(self, state): self.average = state self.avcount = 0 def setAutoc(self, state): self.autocorrelation = state self.avcount = 0 def setFreeze(self, freeze): self.freeze = freeze def setDatastream(self, datastream): self.datastream = datastream def updateTimer(self): self.killTimer(self.timer_id) self.timer_id = self.startTimer(self.maxtime*100 + 50) # timer callback that does the work def timerEvent(self,e): # Scope global fftbuffersize if self.datastream == None: return if self.freeze == 1: return points = int(np.ceil(self.maxtime*samplerate)) if self.triggerCH or self.autocorrelation: # we read twice as much data to be sure to be able to display data for all time points. # independently of trigger point location. read_points = 2*points else: read_points = points fftbuffersize = read_points if SELECTEDCH == BOTH12: channel = 12 if verbose: print "Reading %d frames" % (read_points) X, Y = self.datastream.read(channel, read_points, verbose) if X is None or not len(X): return if len(X) == 0: return i=0 data_CH1 = X data_CH2 = Y elif SELECTEDCH == CH1: channel = 1 if verbose: print "Reading %d frames" % (read_points) X = self.datastream.read(channel, read_points, verbose) if X is None or not len(X): return if len(X) == 0: return i=0 data_CH1 = X data_CH2 = np.zeros((points,)) if SELECTEDCH == CH2: channel = 2 if verbose: print "Reading %d frames" % (read_points) X = self.datastream.read(channel, read_points, verbose) if X is None or not len(X): return data_CH2 = X data_CH1 = np.zeros((points,)) if self.triggerCH == 1 and (SELECTEDCH == BOTH12 or SELECTEDCH == CH1): print "Waiting for CH1 trigger..." if self.triggerslope == 0: zero_crossings = np.where(np.diff(np.sign(data_CH1[points/2:-points/2] - self.triggerval*self.maxamp)) != 0)[0] if self.triggerslope == 1: zero_crossings = np.where(np.diff(np.sign(data_CH1[points/2:-points/2] - self.triggerval*self.maxamp)) > 0)[0] if self.triggerslope == 2: zero_crossings = np.where(np.diff(np.sign(data_CH1[points/2:-points/2] - self.triggerval*self.maxamp)) < 0)[0] if not len(zero_crossings): return print "Triggering on sample", zero_crossings[0] imin = zero_crossings[0] imax = zero_crossings[0] + points data_CH1 = data_CH1[imin:imax] elif self.triggerCH == 2 and (SELECTEDCH == BOTH12 or SELECTEDCH == CH2): print "Waiting for CH2 trigger..." if self.triggerslope == 0: zero_crossings = np.where(np.diff(np.sign(data_CH2[points/2:-points/2] - self.triggerval*self.maxamp2)) != 0)[0] if self.triggerslope == 1: zero_crossings = np.where(np.diff(np.sign(data_CH2[points/2:-points/2] - self.triggerval*self.maxamp2)) > 0)[0] if self.triggerslope == 2: zero_crossings = np.where(np.diff(np.sign(data_CH2[points/2:-points/2] - self.triggerval*self.maxamp2)) < 0)[0] if not len(zero_crossings): return print "Triggering on sample", zero_crossings[0] imin = zero_crossings[0] imax = zero_crossings[0] + points data_CH2 = data_CH2[imin:imax] if self.autocorrelation: if SELECTEDCH == BOTH12 or SELECTEDCH == CH1: data_CH1 = utils.autocorrelation(data_CH1[:2*points])[:points] else: data_CH1 = np.zeros((points,)) if SELECTEDCH == BOTH12 or SELECTEDCH == CH2: data_CH2 = utils.autocorrelation(data_CH2[:2*points])[:points] else: data_CH2 = np.zeros((points,)) if self.average == 0: self.a1 = data_CH1 self.a2 = data_CH2 else: self.avcount += 1 if self.avcount == 1: self.sumCH1 = np.array(data_CH1, dtype=np.float_) self.sumCH2 = np.array(data_CH2, dtype=np.float_) else: if SELECTEDCH==BOTH12: assert len(data_CH1) == len(data_CH2) lp = len(data_CH1) if len(self.sumCH1) == lp and len(self.sumCH2) == lp: self.sumCH1 = self.sumCH1[:lp] + np.array(data_CH1[:lp], dtype=np.float_) self.sumCH2 = self.sumCH2[:lp] + np.array(data_CH2[:lp], dtype=np.float_) else: self.sumCH1 = np.array(data_CH1, dtype=np.float_) self.sumCH2 = np.array(data_CH2, dtype=np.float_) self.avcount = 1 elif SELECTEDCH == CH1: lp = len(data_CH1) if len(self.sumCH1) == lp: self.sumCH1 = self.sumCH1[:lp] + np.array(data_CH1[:lp], dtype=np.float_) else: self.sumCH1 = np.array(data_CH1, dtype=np.float_) self.avcount = 1 elif SELECTEDCH==CH2: lp = len(data_CH2) if len(self.sumCH2) == lp: self.sumCH2 = self.sumCH2[:lp] + np.array(data_CH2[:lp], dtype=np.float_) else: self.sumCH2 = np.array(data_CH2, dtype=np.float_) self.avcount = 1 self.a1 = self.sumCH1/self.avcount self.a2 = self.sumCH2/self.avcount self.setDisplay() inittime=0.01 initamp=100 class ScopeFrame(Qt.QFrame): """ Oscilloscope widget --- contains controls + display """ def __init__(self, *args): apply(Qt.QFrame.__init__, (self,) + args) # the following: setPal.. doesn't seem to work on Win try: self.setPaletteBackgroundColor( QColor(240,240,245)) except: pass hknobpos=scopewidth+20 vknobpos=scopeheight+30 self.setFixedSize(scopewidth+150, scopeheight+150) self.freezeState = 0 self.triggerComboBox = Qt.QComboBox(self) self.triggerComboBox.setGeometry(hknobpos+10, 50, 100, 40)#"Channel: ") self.triggerComboBox.addItem("Trigger off") self.triggerComboBox.addItem("CH1") self.triggerComboBox.addItem("CH2") self.triggerComboBox.setCurrentIndex(0) self.triggerSlopeComboBox = Qt.QComboBox(self) self.triggerSlopeComboBox.setGeometry(hknobpos+10, 100, 100, 40)#"Channel: ") self.triggerSlopeComboBox.addItem("Any Slope") self.triggerSlopeComboBox.addItem("Positive") self.triggerSlopeComboBox.addItem("Negative") self.triggerSlopeComboBox.setCurrentIndex(0) self.knbLevel = LblKnob(self, hknobpos, 160,"Trigger level (%FS)") self.knbTime = LblKnob(self, hknobpos, 300,"Time", 1) self.knbSignal = LblKnob(self, 150, vknobpos, "Signal1",1) self.knbSignal2 = LblKnob(self, 450, vknobpos, "Signal2",1) self.knbOffset1=LblKnob(self, 10, vknobpos, "offset1") self.knbOffset2=LblKnob(self, 310, vknobpos, "offset2") self.knbTime.setRange(0.0001, 1.0) self.knbTime.setValue(DEFAULT_TIMEBASE) self.knbSignal.setRange(1, 1e6, 1) self.knbSignal.setValue(100.0) self.knbSignal2.setRange(1, 1e6, 1) self.knbSignal2.setValue(100.0) self.knbOffset2.setRange(-1.0, 1.0, 0.1) self.knbOffset2.setValue(0.0) self.knbOffset1.setRange(-1.0, 1.0, 0.1) self.knbOffset1.setValue(0.0) self.knbLevel.setRange(-1.0, 1.0, 0.1) self.knbLevel.setValue(0.1) self.knbLevel.setScaleMaxMajor(10) self.plot = Scope(self) self.plot.setGeometry(10, 10, scopewidth, scopeheight) self.picker = Qwt.QwtPlotPicker( Qwt.QwtPlot.xBottom, Qwt.QwtPlot.yLeft, Qwt.QwtPicker.PointSelection | Qwt.QwtPicker.DragSelection, Qwt.QwtPlotPicker.CrossRubberBand, Qwt.QwtPicker.ActiveOnly, #AlwaysOn, self.plot.canvas()) self.picker.setRubberBandPen(Qt.QPen(Qt.Qt.green)) self.picker.setTrackerPen(Qt.QPen(Qt.Qt.cyan)) self.connect(self.knbTime.knob, Qt.SIGNAL("valueChanged(double)"), self.setTimebase) self.knbTime.setValue(0.01) self.connect(self.knbSignal.knob, Qt.SIGNAL("valueChanged(double)"), self.setAmplitude) self.connect(self.knbSignal2.knob, Qt.SIGNAL("valueChanged(double)"), self.setAmplitude2) #self.knbSignal.setValue(0.1) self.connect(self.knbLevel.knob, Qt.SIGNAL("valueChanged(double)"), self.setTriggerlevel) self.connect(self.knbOffset1.knob, Qt.SIGNAL("valueChanged(double)"), self.plot.setOffset1) self.connect(self.knbOffset2.knob, Qt.SIGNAL("valueChanged(double)"), self.plot.setOffset2) self.connect(self.triggerComboBox, Qt.SIGNAL('currentIndexChanged(int)'), self.setTriggerCH) self.connect(self.triggerSlopeComboBox, Qt.SIGNAL('currentIndexChanged(int)'), self.plot.setTriggerSlope) self.knbLevel.setValue(0.1) self.plot.setAxisScale( Qwt.QwtPlot.xBottom, 0.0, 10.0*inittime) self.plot.setAxisScale( Qwt.QwtPlot.yLeft, -initamp, initamp) self.plot.setAxisScale( Qwt.QwtPlot.yRight, -initamp, initamp) self.plot.show() def _calcKnobVal(self, val): ival = np.floor(val) frac = val - ival if frac >= 0.9: frac = 1.0 elif frac >= 0.66: frac = np.log10(5.0) elif frac >= np.log10(2.0): frac = np.log10(2.0) else: frac = 0.0 dt = 10**frac*10**ival return dt def setTimebase(self, val): dt = self._calcKnobVal(val) self.plot.setAxisScale( Qwt.QwtPlot.xBottom, 0.0, 10.0*dt) self.plot.setMaxTime(dt*10.0) self.plot.replot() def setAmplitude(self, val): dt = self._calcKnobVal(val) self.plot.setAxisScale( Qwt.QwtPlot.yLeft, -dt, dt) self.plot.setMaxAmp(dt) self.plot.replot() def setAmplitude2(self, val): dt = self._calcKnobVal(val) self.plot.setAxisScale( Qwt.QwtPlot.yRight, -dt, dt) self.plot.setMaxAmp2(dt) self.plot.replot() def setTriggerlevel(self, val): self.plot.setTriggerLevel(val) self.plot.setDisplay() def setTriggerCH(self, val): if val == 0: val = None self.plot.setTriggerCH(val) self.plot.setDisplay() #-------------------------------------------------------------------- class FScope(Qwt.QwtPlot): """ Power spectrum display widget """ def __init__(self, *args): apply(Qwt.QwtPlot.__init__, (self,) + args) self.setTitle('Power spectrum'); self.setCanvasBackground(Qt.Qt.white) # grid self.grid = Qwt.QwtPlotGrid() self.grid.enableXMin(True) self.grid.setMajPen(Qt.QPen(Qt.Qt.gray, 0, Qt.Qt.SolidLine)); self.grid.attach(self) # axes self.setAxisTitle(Qwt.QwtPlot.xBottom, 'Frequency [Hz]'); self.setAxisTitle(Qwt.QwtPlot.yLeft, 'Power Spectrum [dBc/Hz]'); self.setAxisMaxMajor(Qwt.QwtPlot.xBottom, 10); self.setAxisMaxMinor(Qwt.QwtPlot.xBottom, 0); self.setAxisMaxMajor(Qwt.QwtPlot.yLeft, 10); self.setAxisMaxMinor(Qwt.QwtPlot.yLeft, 0); # curves self.curve2 = Qwt.QwtPlotCurve('PSTrace2') self.curve2.setPen(Qt.QPen(Qt.Qt.magenta,FFTPENWIDTH)) self.curve2.setYAxis(Qwt.QwtPlot.yLeft) self.curve2.attach(self) self.curve1 = Qwt.QwtPlotCurve('PSTrace1') self.curve1.setPen(Qt.QPen(Qt.Qt.blue,FFTPENWIDTH)) self.curve1.setYAxis(Qwt.QwtPlot.yLeft) self.curve1.attach(self) self.triggerval=0.0 self.maxamp=100.0 self.maxamp2=100.0 self.freeze=0 self.average=0 self.avcount=0 self.logy=1 self.datastream=None self.dt=1.0/samplerate self.df=1.0/(fftbuffersize*self.dt) self.f = np.arange(0.0, samplerate, self.df) self.a1 = 0.0*self.f self.a2 = 0.0*self.f self.curve1.setData(self.f, self.a1) self.curve2.setData(self.f, self.a2) self.setAxisScale( Qwt.QwtPlot.xBottom, 0.0, 12.5*initfreq) self.setAxisScale( Qwt.QwtPlot.yLeft, -120.0, 0.0) self.startTimer(100) self.replot() def resetBuffer(self): self.df=1.0/(fftbuffersize*self.dt) self.f = np.arange(0.0, samplerate, self.df) self.a1 = 0.0*self.f self.a2 = 0.0*self.f self.curve1.setData(self.curve1, self.f, self.a1) self.curve1.setData(self.curve1, self.f, self.a2) def setMaxAmp(self, val): if val>0.6: self.setAxisScale( Qwt.QwtPlot.yLeft, -120.0, 0.0) self.logy=1 else: self.setAxisScale( Qwt.QwtPlot.yLeft, 0.0, 10.0*val) self.logy=0 self.maxamp=val def setMaxTime(self, val): self.maxtime=val self.updateTimer() def setTriggerLevel(self, val): self.triggerval=val def setDisplay(self): n=fftbuffersize/2 if SELECTEDCH==BOTH12: self.curve1.setData(self.f[0:n], self.a1[:n]) self.curve2.setData(self.f[0:n], self.a2[:n]) elif SELECTEDCH==CH2: self.curve1.setData([0.0,0.0], [0.0,0.0]) self.curve2.setData(self.f[0:n], self.a2[:n]) elif SELECTEDCH==CH1: self.curve1.setData(self.f[0:n], self.a1[:n]) self.curve2.setData([0.0,0.0], [0.0,0.0]) self.replot() def getValue(self, index): return self.f[index],self.a1[index] def setAverage(self, state): self.average = state self.avcount=0 def setFreeze(self, freeze): self.freeze = freeze def setDatastream(self, datastream): self.datastream = datastream def timerEvent(self,e): # FFT global fftbuffersize if self.datastream == None: return if self.freeze == 1: return if SELECTEDCH == BOTH12: channel = 12 X, Y = self.datastream.read(channel, fftbuffersize, verbose) if X is None or not len(X): return data_CH1 = X[:fftbuffersize] data_CH2 = Y[:fftbuffersize] elif SELECTEDCH == CH1: channel = 1 X = self.datastream.read(channel, fftbuffersize, verbose) if X is None or not len(X): return data_CH1 = X[:fftbuffersize] data_CH2 = np.ones((fftbuffersize,)) elif SELECTEDCH == CH2: channel = 2 X = self.datastream.read(channel, fftbuffersize, verbose) if X is None or not len(X): return data_CH2 = X[:fftbuffersize] data_CH1 = np.ones((fftbuffersize,)) self.df = 1.0/(fftbuffersize*self.dt) self.setAxisTitle(Qwt.QwtPlot.xBottom, 'Frequency [Hz] - Bin width %g Hz' % (self.df,)) self.f = np.arange(0.0, samplerate, self.df) if not SPECTRUM_MODULE: lenX = fftbuffersize window = np.blackman(lenX) sumw = np.sum(window*window) A = FFT.fft(data_CH1*window) #lenX B = (A*np.conjugate(A)).real A = FFT.fft(data_CH2*window) #lenX B2 = (A*np.conjugate(A)).real sumw *= 2.0 # sym about Nyquist (*4); use rms (/2) sumw /= self.dt # sample rate B /= sumw B2 /= sumw else: print "FFT buffer size: %d points" % (fftbuffersize,) B = spectrum.Periodogram(np.array(data_CH1, dtype=float64), samplerate) B.sides = 'onesided' B.run() B = B.get_converted_psd('onesided') B2 = spectrum.Periodogram(np.array(data_CH2, dtype=float64), samplerate) B2.sides = 'onesided' B2.run() B2 = B2.get_converted_psd('onesided') if self.logy: P1 = np.log10(B)*10.0 P2 = np.log10(B2)*10.0 P1 -= P1.max() P2 -= P2.max() else: P1 = B P2 = B2 if not self.average: self.a1 = P1 self.a2 = P2 self.avcount = 0 else: self.avcount += 1 if self.avcount == 1: self.sumP1 = P1 self.sumP2 = P2 elif self.sumP1.shape != P1.shape or self.sumP1.shape != P1.shape: self.avcount = 1 self.sumP1 = P1 self.sumP2 = P2 else: self.sumP1 += P1 self.sumP2 += P2 self.a1 = self.sumP1/self.avcount self.a2 = self.sumP2/self.avcount self.setDisplay() initfreq = 100.0 class FScopeFrame(Qt.QFrame): """ Power spectrum widget --- contains controls + display """ def __init__(self , *args): apply(Qt.QFrame.__init__, (self,) + args) vknobpos=scopeheight+30 hknobpos=scopewidth+10 # the following: setPal.. doesn't seem to work on Ein try: self.setPaletteBackgroundColor( QColor(240,240,245)) except: pass self.setFixedSize(scopewidth+160, scopeheight+160) self.freezeState = 0 self.knbSignal = LblKnob(self,160, vknobpos, "Signal",1) self.knbTime = LblKnob(self,310, vknobpos,"Frequency", 1) self.knbTime.setRange(1.0, 1250.0) self.knbSignal.setRange(100, 1000000) self.plot = FScope(self) self.plot.setGeometry(12.5, 10, scopewidth+120, scopeheight) self.picker = Qwt.QwtPlotPicker( Qwt.QwtPlot.xBottom, Qwt.QwtPlot.yLeft, Qwt.QwtPicker.PointSelection | Qwt.QwtPicker.DragSelection, Qwt.QwtPlotPicker.CrossRubberBand, Qwt.QwtPicker.ActiveOnly, #AlwaysOn, self.plot.canvas()) self.picker.setRubberBandPen(Qt.QPen(Qt.Qt.green)) self.picker.setTrackerPen(Qt.QPen(Qt.Qt.cyan)) self.connect(self.knbTime.knob, Qt.SIGNAL("valueChanged(double)"), self.setTimebase) self.knbTime.setValue(1000.0) self.connect(self.knbSignal.knob, Qt.SIGNAL("valueChanged(double)"), self.setAmplitude) self.knbSignal.setValue(1000000) self.plot.show() def _calcKnobVal(self,val): ival = np.floor(val) frac = val - ival if frac >= 0.9: frac = 1.0 elif frac >= 0.66: frac = np.log10(5.0) elif frac >= np.log10(2.0): frac = np.log10(2.0) else: frac = 0.0 dt = 10**frac*10**ival return dt def setTimebase(self, val): dt = self._calcKnobVal(val) self.plot.setAxisScale(Qwt.QwtPlot.xBottom, 0.0, 12.5*dt) self.plot.replot() def setAmplitude(self, val): minp = self._calcKnobVal(val) self.plot.setAxisScale(Qwt.QwtPlot.yLeft, -int(np.log10(minp)*20), 0.0) self.plot.replot() #--------------------------------------------------------------------- class FScopeDemo(Qt.QMainWindow): """ Application container widget Contains scope and power spectrum analyser in tabbed windows. Enables switching between the two. Handles toolbar and status. """ def __init__(self, *args): apply(Qt.QMainWindow.__init__, (self,) + args) self.freezeState = 0 self.changeState = 0 self.averageState = 0 self.autocState = 0 self.scope = ScopeFrame(self) self.current = self.scope self.pwspec = FScopeFrame(self) self.pwspec.hide() self.stack=Qt.QTabWidget(self) self.stack.addTab(self.scope,"scope") self.stack.addTab(self.pwspec,"fft") self.setCentralWidget(self.stack) toolBar = Qt.QToolBar(self) self.addToolBar(toolBar) sb=self.statusBar() sbfont=Qt.QFont("Helvetica",12) sb.setFont(sbfont) self.btnFreeze = Qt.QToolButton(toolBar) self.btnFreeze.setText("Freeze") self.btnFreeze.setIcon(Qt.QIcon(Qt.QPixmap(icons.stopicon))) self.btnFreeze.setCheckable(True) self.btnFreeze.setToolButtonStyle(Qt.Qt.ToolButtonTextUnderIcon) toolBar.addWidget(self.btnFreeze) self.btnSave = Qt.QToolButton(toolBar) self.btnSave.setText("Save CSV") self.btnSave.setIcon(Qt.QIcon(Qt.QPixmap(icons.save))) self.btnSave.setToolButtonStyle(Qt.Qt.ToolButtonTextUnderIcon) toolBar.addWidget(self.btnSave) self.btnPDF = Qt.QToolButton(toolBar) self.btnPDF.setText("Export PDF") self.btnPDF.setIcon(Qt.QIcon(Qt.QPixmap(icons.pdf))) self.btnPDF.setToolButtonStyle(Qt.Qt.ToolButtonTextUnderIcon) toolBar.addWidget(self.btnPDF) self.btnPrint = Qt.QToolButton(toolBar) self.btnPrint.setText("Print") self.btnPrint.setIcon(Qt.QIcon(Qt.QPixmap(icons.print_xpm))) self.btnPrint.setToolButtonStyle(Qt.Qt.ToolButtonTextUnderIcon) toolBar.addWidget(self.btnPrint) self.btnMode = Qt.QToolButton(toolBar) self.btnMode.setText("fft") self.btnMode.setIcon(Qt.QIcon(Qt.QPixmap(icons.pwspec))) self.btnMode.setCheckable(True) self.btnMode.setToolButtonStyle(Qt.Qt.ToolButtonTextUnderIcon) toolBar.addWidget(self.btnMode) self.btnAvge = Qt.QToolButton(toolBar) self.btnAvge.setText("average") self.btnAvge.setIcon(Qt.QIcon(Qt.QPixmap(icons.avge))) self.btnAvge.setCheckable(True) self.btnAvge.setToolButtonStyle(Qt.Qt.ToolButtonTextUnderIcon) toolBar.addWidget(self.btnAvge) self.btnAutoc = Qt.QToolButton(toolBar) self.btnAutoc.setText("autocorrelation") self.btnAutoc.setIcon(Qt.QIcon(Qt.QPixmap(icons.avge))) self.btnAutoc.setCheckable(True) self.btnAutoc.setToolButtonStyle(Qt.Qt.ToolButtonTextUnderIcon) toolBar.addWidget(self.btnAutoc) #self.lstLabl = Qt.QLabel("Buffer:",toolBar) #toolBar.addWidget(self.lstLabl) #self.lstChan = Qt.QComboBox(toolBar) #self.lstChan.insertItem(0,"8192") #self.lstChan.insertItem(1,"16k") #self.lstChan.insertItem(2,"32k") #toolBar.addWidget(self.lstChan) self.lstLR = Qt.QLabel("Channels:",toolBar) toolBar.addWidget(self.lstLR) self.lstLRmode = Qt.QComboBox(toolBar) self.lstLRmode.insertItem(0,"1&2") self.lstLRmode.insertItem(1,"CH1") self.lstLRmode.insertItem(2,"CH2") toolBar.addWidget(self.lstLRmode) self.connect(self.btnPrint, Qt.SIGNAL('clicked()'), self.printPlot) self.connect(self.btnSave, Qt.SIGNAL('clicked()'), self.saveData) self.connect(self.btnPDF, Qt.SIGNAL('clicked()'), self.printPDF) self.connect(self.btnFreeze, Qt.SIGNAL('toggled(bool)'), self.freeze) self.connect(self.btnMode, Qt.SIGNAL('toggled(bool)'), self.mode) self.connect(self.btnAvge, Qt.SIGNAL('toggled(bool)'), self.average) self.connect(self.btnAutoc, Qt.SIGNAL('toggled(bool)'), self.autocorrelation) #self.connect(self.lstChan, Qt.SIGNAL('activated(int)'), self.fftsize) self.connect(self.lstLRmode, Qt.SIGNAL('activated(int)'), self.channel) self.connect(self.scope.picker, Qt.SIGNAL('moved(const QPoint&)'), self.moved) self.connect(self.scope.picker, Qt.SIGNAL('appended(const QPoint&)'), self.appended) self.connect(self.pwspec.picker, Qt.SIGNAL('moved(const QPoint&)'), self.moved) self.connect(self.pwspec.picker, Qt.SIGNAL('appended(const QPoint&)'), self.appended) self.connect(self.stack, Qt.SIGNAL('currentChanged(int)'), self.mode) self.showInfo(cursorInfo) #self.showFullScreen() #print self.size() def showInfo(self, text): self.statusBar().showMessage(text) def printPlot(self): printer = Qt.QPrinter(Qt.QPrinter.HighResolution) printer.setOutputFileName('scope-plot.ps') printer.setCreator('Ethernet Scope') printer.setOrientation(Qt.QPrinter.Landscape) printer.setColorMode(Qt.QPrinter.Color) docName = self.current.plot.title().text() if not docName.isEmpty(): docName.replace(Qt.QRegExp(Qt.QString.fromLatin1('\n')), self.tr(' -- ')) printer.setDocName(docName) dialog = Qt.QPrintDialog(printer) if dialog.exec_(): # filter = Qwt.PrintFilter() # if (Qt.QPrinter.GrayScale == printer.colorMode()): # filter.setOptions( # Qwt.QwtPlotPrintFilter.PrintAll # & ~Qwt.QwtPlotPrintFilter.PrintBackground # | Qwt.QwtPlotPrintFilter.PrintFrameWithScales) self.current.plot.print_(printer) #p = Qt.QPrinter() #if p.setup(): # self.current.plot.printPlot(p)#, Qwt.QwtFltrDim(200)); def printPDF(self): fileName = Qt.QFileDialog.getSaveFileName( self, 'Export File Name', '', 'PDF Documents (*.pdf)') if not fileName.isEmpty(): printer = Qt.QPrinter() printer.setOutputFormat(Qt.QPrinter.PdfFormat) printer.setOrientation(Qt.QPrinter.Landscape) printer.setOutputFileName(fileName) printer.setCreator('Ethernet Scope') self.current.plot.print_(printer) # p = QPrinter() # if p.setup(): # self.current.plot.printPlot(p)#, Qwt.QwtFltrDim(200)); def saveData(self): fileName = Qt.QFileDialog.getSaveFileName( self, 'Export File Name', '', 'CSV Documents (*.csv)') if not fileName.isEmpty(): csvlib.write_csv(fileName, np.vstack(( np.arange(self.current.plot.a1.shape[0], dtype=int32)/samplerate, self.current.plot.a1, self.current.plot.a2)), ("TIME", "CH1", "CH2")) def channel(self, item): global SELECTEDCH if item == 1: SELECTEDCH = CH1 elif item == 2: SELECTEDCH = CH2 else: SELECTEDCH = BOTH12 self.scope.plot.avcount = 0 self.pwspec.plot.avcount = 0 def freeze(self, on, changeIcon=True): if on: self.freezeState = 1 if changeIcon: self.btnFreeze.setText("Run") self.btnFreeze.setIcon(Qt.QIcon(Qt.QPixmap(icons.goicon))) else: self.freezeState = 0 if changeIcon: self.btnFreeze.setText("Freeze") self.btnFreeze.setIcon(Qt.QIcon(Qt.QPixmap(icons.stopicon))) self.scope.plot.setFreeze(self.freezeState) self.pwspec.plot.setFreeze(self.freezeState) def average(self, on): if on: self.averageState = 1 self.btnAvge.setText("single") self.btnAvge.setIcon(Qt.QIcon(Qt.QPixmap(icons.single))) else: self.averageState = 0 self.btnAvge.setText("average") self.btnAvge.setIcon(Qt.QIcon(Qt.QPixmap(icons.avge))) self.scope.plot.setAverage(self.averageState) self.pwspec.plot.setAverage(self.averageState) def autocorrelation(self, on): if on: self.autocState = 1 self.btnAutoc.setText("normal") self.btnAutoc.setIcon(Qt.QIcon(Qt.QPixmap(icons.single))) else: self.autocState = 0 self.btnAutoc.setText("autocorrelation") self.btnAutoc.setIcon(Qt.QIcon(Qt.QPixmap(icons.avge))) self.scope.plot.setAutoc(self.autocState) def mode(self, on): if on: self.changeState=1 self.current=self.pwspec self.btnMode.setText("scope") self.btnMode.setIcon(Qt.QIcon(Qt.QPixmap(icons.scope))) self.btnMode.setChecked(True) else: self.changeState=0 self.current=self.scope self.btnMode.setText("fft") self.btnMode.setIcon(Qt.QIcon(Qt.QPixmap(icons.pwspec))) self.btnMode.setChecked(False) if self.changeState==1: self.stack.setCurrentIndex(self.changeState) self.scope.plot.setDatastream(None) self.pwspec.plot.setDatastream(stream) else: self.stack.setCurrentIndex(self.changeState) self.pwspec.plot.setDatastream(None) self.scope.plot.setDatastream(stream) def moved(self, e): if self.changeState==1: name='Freq' else: name='Time' frequency = self.current.plot.invTransform(Qwt.QwtPlot.xBottom, e.x()) amplitude = self.current.plot.invTransform(Qwt.QwtPlot.yLeft, e.y()) if name=='Time': df=self.scope.plot.dt i=int(frequency/df) ampa=self.scope.plot.a1[i] ampb=self.scope.plot.a2[i] else: df=self.pwspec.plot.df i=int(frequency/df) ampa=self.pwspec.plot.a1[i] ampb=self.pwspec.plot.a2[i] self.showInfo('%s=%g, cursor=%g, A=%g, B=%g' % (name,frequency, amplitude,ampa,ampb)) def appended(self, e): print 's' # Python semantics: self.pos = e.pos() does not work; force a copy self.xpos = e.x() self.ypos = e.y() self.moved(e) # fake a mouse move to show the cursor position def load_cfg(): default = '.audio' # default probe conf_path = os.path.expanduser('~/.dualscope123') conf = ConfigParser.ConfigParser() print "Loaded config file %s" % (conf_path,) if not os.path.isfile(conf_path): conf.add_section('probes') conf.set("probes", "probe", 'audio') conf.set("DEFAULT", "verbose", 'false') with open(conf_path, 'w') as fp: conf.write(fp) return load_cfg() else: conf.read([conf_path]) if not 'probes' in conf.sections() or 'DEFAULT' in conf.sections(): raise ConfigParser.NoSectionError("Malformed config file.") try: probe_name = conf.get('probes', 'probe').strip("\"'").strip() except ConfigParser.NoOptionError: probe = default[1:] try: verbose = conf.get('DEFAULT', 'verbose').strip("\"'").strip() except ConfigParser.NoOptionError: verbose = False try: probe_module = importlib.import_module("."+probe_name, "dualscope123.probes") except ImportError: probe_module = importlib.import_module(default, "dualscope123.probes") probe_name = default[1:] if verbose in ('true', 'True', '1', 'on', 'yes', 'YES', 'Yes', 'On'): print "Loaded probe %s" % probe_name verbose = True else: verbose = False return probe_module, verbose def main(): global verbose, samplerate, CHUNK, fftbuffersize, stream probe, verbose = load_cfg() stream = probe.Probe() stream.open() samplerate = stream.RATE CHUNK = stream.CHUNK fftbuffersize = CHUNK app = Qt.QApplication(sys.argv) demo = FScopeDemo() demo.scope.plot.setDatastream(stream) demo.show() app.exec_() stream.close() if __name__ == '__main__': main()