from matplotlib.backends.backend_wxagg import Toolbar, FigureCanvasWxAgg
from matplotlib.figure import Figure
import comedi #@UnresolvedImport
import matplotlib
import numpy
import os
import struct
import sys
import threading
import wx.lib.newevent
import time
matplotlib.use("WXAgg")
matplotlib.interactive(True)

ID_STARTBUTTON = 101
ID_STOPBUTTON = 102
ID_CLEARBUTTON = 103
ID_OKBUTTON = 104
ID_CONFIGUREBUTTON = 105

#default parameters
DEVICE = '/dev/comedi0'
NBCHANS = 2
CHANS = [0, 1]
RANGES = [0, 0]
REFS = [comedi.AREF_GROUND, comedi.AREF_GROUND]
SAMPLE_FREQ = 1000

#global variables
BUFFER_SIZE = 1000
PLOT_BUFFER_SIZE = 5000
DEBUG = True

def deinterleaveData(inData, nChan):
    '''
    takes a linear array with data points interleaved [a1,b1,c1,a2,b2,c2,....aN-1,bN-1,cN-1,aN,bN]
    and return a (nChan,N) numpy.array with the data deinterleaved
    [[a1,a2,a3,...aN-1],
     [b1,b2,b3,...bN-1],
     [c1,c2,c3,...cN-1]]
    
    if the length of the input array was not a multiple of nChan, the remaining points are returned in remainData.
    Otherwise, remainData is an empty array
    '''
    n = len(inData)
    nToKeep = n-(n % nChan)
    outData = numpy.array(inData[:nToKeep],dtype=int).reshape((-1,nChan)).transpose()
    remainData = inData[nToKeep:]
    return outData,remainData

def convertInVolts(inData, dev, subdev, chans, inRanges):
    '''
    converts the int values obtained from comedi to a physical value
    arguments:
    inData: a (nChan,N) numpy array
    dev: the comedi device used to obtain the data
    subdev: the comedi subdevice used to obtain the data
    chans: a (nChan,1) array containing the channels from which the data was obtained
    inRanges: a (nChan,1) array containing the id of the chanRange used for each channel. the actual chanRange will be obtained
              from comedi using comedi_get_range()
    '''
    nChan,_ = inData.shape
    maxDatas = []
    physicalRanges = []
    for chan,chanRange in zip(chans,inRanges):
        maxDatas.append(comedi.comedi_get_maxdata(dev,subdev,chan))
        physicalRanges.append(comedi.comedi_get_range(dev,subdev,chan,chanRange))
    out = numpy.array(inData,dtype=float)
    for i in range(nChan):
        out[i] = out[i]*((float(physicalRanges[i].max)-float(physicalRanges[i].min))/float(maxDatas[i])) + float(physicalRanges[i].min)
    return out



# this creates an UpdateCanvasEvent even class that will get created
# and sent from the worker thread to the main thread (which is the
# only place that updates to the GUI should happen...lest there be crashes)
(UpdateCanvasEvent, EVT_UPDATE_CANVAS) = wx.lib.newevent.NewEvent()

class MainWindow(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1, "Free Period Calculation")

        self.arrayLock = threading.Semaphore(1)
        self.canvasLock = threading.Semaphore(1)
        self.dev = None
        self.subdev = None
        self.nbChans = NBCHANS
            
        #create the preferences window
        self.prefwindow = self.createPreferencesWindow()
        #get default configuration
        self.configuration = self.prefwindow.getConfiguration()


        self.figure = Figure((5, 4), 75)
        self.canvas = FigureCanvasWxAgg(self, -1, self.figure)
        self.toolbar = Toolbar(self.canvas)
        self.toolbar.Realize()

        self.sizerHoriz = wx.BoxSizer(wx.HORIZONTAL)

        self.startButton = wx.Button(self, ID_STARTBUTTON, "Start")
        wx.EVT_BUTTON(self, ID_STARTBUTTON, self.pressStartButton);

        self.stopButton = wx.Button(self, ID_STOPBUTTON, "Stop")
        self.stopButton.Enable(0)
        wx.EVT_BUTTON(self, ID_STOPBUTTON, self.pressStopButton);

        self.clearButton = wx.Button(self, ID_CLEARBUTTON, "Clear")
        wx.EVT_BUTTON(self, ID_CLEARBUTTON, self.pressClearButton);

        self.prefButton = wx.Button(self, ID_CONFIGUREBUTTON, "Configure")
        wx.EVT_BUTTON(self, ID_CONFIGUREBUTTON, self.pressConfigureButton)

        self.sizerHoriz.Add(self.startButton, 0, wx.ALL, 5)
        self.sizerHoriz.Add(self.stopButton, 0, wx.ALL, 5)
        self.sizerHoriz.Add(self.clearButton, 0, wx.ALL, 5)
        self.sizerHoriz.Add(self.prefButton, 0, wx.ALL, 5)

        # Use some sizers to see layout options
        self.sizerVert = wx.BoxSizer(wx.VERTICAL)
        self.sizerVert.Add(self.sizerHoriz, 0, wx.EXPAND)
        self.sizerVert.Add(self.canvas, 1, wx.EXPAND)
        self.sizerVert.Add(self.toolbar, 0, wx.GROW)

        #Bind the update canvas event to some function:
        self.Bind(EVT_UPDATE_CANVAS, self.onUpdateCanvas)
        
        self.Bind(wx.EVT_CLOSE, self.onCloseWindow)

        #Layout sizers
        self.SetSizer(self.sizerVert)
        self.SetAutoLayout(1)
        self.sizerVert.Fit(self)
        self.initialize()
        self.Show(1)

    def GetToolBar(self):
        return self.toolbar

    def pressStartButton(self, event):
        if DEBUG: print "Start button has been pressed"
        self.startButton.Enable(0)
        self.prefButton.Enable(0)
        self.configuration = self.prefwindow.getConfiguration()
        
        #configure the comedi device
        self.dev = comedi.comedi_open(str(self.configuration['device']))
        if not self.dev:
            print "FATAL ERROR: cannot open comedi device %s: %s"%\
                (self.configuration['device'], comedi.comedi_strerror(comedi.comedi_errno()))
            sys.exit(1)
        #and get an appropriate subdevice
        self.subdev = comedi.comedi_find_subdevice_by_type(self.dev, comedi.COMEDI_SUBD_AI, 0)
        if self.subdev<0:
            print "FATAL ERROR: cannot find a suitable analog input subdevice: %s" %\
                comedi.comedi_strerror(comedi.comedi_errno())
            sys.exit(1)
        #create channel list
        myChanList = comedi.chanlist(self.nbChans)
        for i in range(self.nbChans):
            myChanList[i] = comedi.cr_pack(self.configuration['analog'][i][0], self.configuration['analog'][i][1], self.configuration['analog'][i][2])
        #create a command structure
        cmd = comedi.comedi_cmd_struct()
        ret = comedi.comedi_get_cmd_generic_timed(self.dev, self.subdev, cmd, self.nbChans, int(1e9 / SAMPLE_FREQ))
        if ret: raise Exception("Error comedi_get_cmd_generic failed")
        cmd.chanlist = myChanList # adjust for our particular context
        cmd.chanlist_len = self.nbChans
        cmd.scan_end_arg = self.nbChans
        cmd.stop_src = comedi.TRIG_NONE #never stop
        
        #test our comedi command a few times.
        ret = 0
        for i in range(2):
            ret = comedi.comedi_command_test(self.dev, cmd)
            if ret < 0: raise Exception("comedi_command_test failed: %s" % (comedi.comedi_strerror(comedi.comedi_errno())))

        #Start the command
        ret = comedi.comedi_command(self.dev, cmd)
        if ret <> 0: raise Exception("comedi_command failed... %s" % (comedi.comedi_strerror(comedi.comedi_errno())))
        
        #start reader thread
        if DEBUG: print "creating reader thread"
        self.helper = plotterThread(self, self.dev, self.subdev, [self.configuration['analog'][0][0],self.configuration['analog'][1][0]], [self.configuration['analog'][0][1],self.configuration['analog'][1][1]])        
        self.helper.start()
        
        self.stopButton.Enable(1)
        if DEBUG: print "Start button complete."

    def pressStopButton(self, event):
        self.stopButton.Enable(0)
        if self.helper is not None:
            self.helper.reading = False
            self.helper.join()
            if DEBUG: print "Background thread stopped."
            self.helper = None
        comedi.comedi_cancel(self.dev,self.subdev)
        self.startButton.Enable(1)
        self.prefButton.Enable(1)

    def pressClearButton(self, event):
        self.arrayLock.acquire()
        self.currPos = 0
        self.yarray = numpy.zeros((self.nbChans,PLOT_BUFFER_SIZE))
        self.trigPlot.cla()
        self.lines = self.trigPlot.plot(self.yarray.transpose(), '-')
        #self.trigPlot.autoscale(True,'y') #does not work
        self.arrayLock.release()
        evt = UpdateCanvasEvent()
        wx.PostEvent(self, evt)

    def pressConfigureButton(self, event):
        self.prefwindow.Show(1)
        self.prefwindow.MakeModal(1)

    def initialize(self):
        self.trigPlot = self.figure.add_subplot(111)
        self.pressClearButton(None)
    
    def addData(self, inData):
        if DEBUG: print "#### in addData() ####"
        self.arrayLock.acquire()
        _, nbPoints = inData.shape
        if DEBUG: print "received %d data points. current pos in buffer: %s"%(nbPoints,self.currPos)
        maxPoints = PLOT_BUFFER_SIZE-self.currPos
        if nbPoints<=maxPoints:
            maxPoints = nbPoints
        if DEBUG: print "will add %d points to the buffer"%(maxPoints)   
        self.yarray[:,self.currPos:(self.currPos+maxPoints)] = inData[:,:maxPoints]
        self.currPos += maxPoints
        if DEBUG: print "current position in the buffer is now %d"%(self.currPos)
        if self.currPos>PLOT_BUFFER_SIZE-1:
            #we've filled the whole buffer, we need to wrap around
            if DEBUG: print "wrapping around..."
            _,nPointsLeft = inData[:,maxPoints:].shape
            if DEBUG: print "we have %s points left to add to the beginning of the buffer"%nPointsLeft
            self.yarray[:,:nPointsLeft] = inData[:,maxPoints:]
            self.currPos = nPointsLeft
        for i in range(len(self.lines)):
            self.lines[i].set_data(numpy.linspace(1,PLOT_BUFFER_SIZE,PLOT_BUFFER_SIZE),self.yarray[i,:].tolist())
        self.arrayLock.release()

    def onUpdateCanvas(self, evt):
        if DEBUG: print "Updating the canvas..."
        self.canvasLock.acquire()
        self.canvas.draw()
        #self.trigPlot.autoscale_view(None, False, True) #does not work
        self.toolbar.update()
        self.canvasLock.release()
    
    def onCloseWindow(self, evt):
        self.pressStopButton(evt)
        self.Destroy()

    def createPreferencesWindow(self):
        prefwindow = PreferencesFrame(self, "preferences")
        prefwindow.CentreOnParent(wx.BOTH)
        return prefwindow

class plotterThread(threading.Thread):
    def __init__(self, window, dev, subdev, chans, ranges):
        if DEBUG: print "in plotterThread __init__"
        threading.Thread.__init__(self)
        
        self.window = window
        self.dev = dev
        self.subdev = subdev
        self.chans = chans
        self.ranges = ranges
        
        #get a file-descriptor for reading
        self.fd = comedi.comedi_fileno(self.dev)
        if self.fd <= 0: raise Exception("Error obtaining Comedi device file descriptor: %s" % (comedi.comedi_strerror(comedi.comedi_errno())))
        self.nbChans = len(chans)
        self.reading = False
        self.currPos = 0
        
    def run(self):
        if DEBUG: print "Reader thread running..."
        self.reading = True
        while (self.reading):
            data=None
            n=0
            try:
                if DEBUG: print "reading..."
                data = os.read(self.fd,BUFFER_SIZE)
                n = len(data)/2 # 2 bytes per 'H'                
                if DEBUG: print "read %d data points"%n
                if DEBUG: print "unpacking..."
                data = struct.unpack('%dH'%n,data)
                if DEBUG: print "de-interleaving..."
                out,data = deinterleaveData(data,self.nbChans)
                if DEBUG: print "returned a (%d,%d) array and kept %d for next round"%(out.shape[0],out.shape[1],len(data))
                if DEBUG: print "transforming into physical units"
                out = convertInVolts(out,self.dev,self.subdev,self.chans,self.ranges)
                self.window.addData(out)
                evt = UpdateCanvasEvent()
                wx.PostEvent(self.window, evt)
                time.sleep(0.1)
            except IOError:
                raise Exception("Fatal Error: %s"%(comedi.comedi_strerror(comedi.comedi_errno())))


class PreferencesFrame(wx.Frame):
    def __init__(self, parent, title):
        wx.Frame.__init__(self, parent, -1, title)

        #p = wx.Panel(self, -1)
        vertSizer = wx.BoxSizer(wx.VERTICAL)
        titleLabel = wx.StaticText(self, -1, "Comedi Configuration")
        okButton = wx.Button(self, ID_OKBUTTON, "Save Changes")
        wx.EVT_BUTTON(self, ID_OKBUTTON, self.PressOkButton)

        sizer = wx.GridSizer(6, 2, 2, 2) #rows, columns, hgap, vgap

        vertSizer.Add(titleLabel, 0, wx.ALIGN_CENTER | wx.ALL, 8)
        vertSizer.Add(sizer)
        vertSizer.Add(okButton, 0, wx.ALIGN_RIGHT | wx.ALL, 15)

        deviceNameLabel = wx.StaticText(self, -1, "Device: ")
        self.deviceNameTextBox = wx.TextCtrl(self, -1, "/dev/comedi0")
        sizer.Add(deviceNameLabel, 0, wx.ALIGN_RIGHT)
        sizer.Add(self.deviceNameTextBox)

        xInputLabel = wx.StaticText(self, -1, "X Input Channel: ")
        self.xInputBox = wx.Choice(self, -1, choices=['0', '1', '2', '3', '4', '5', '6', '7'])
        self.xInputBox.SetSelection(0)
        sizer.Add(xInputLabel, 0, wx.ALIGN_RIGHT)
        sizer.Add(self.xInputBox)

        xInputRangeLabel = wx.StaticText(self, -1, "X Input Range: ")
        self.xInputRangeBox = wx.Choice(self, -1, choices=['0', '1', '2', '3'])
        self.xInputRangeBox.SetSelection(1)
        sizer.Add(xInputRangeLabel, 0, wx.ALIGN_RIGHT)
        sizer.Add(self.xInputRangeBox)

        xInputTypeLabel = wx.StaticText(self, -1, "X Input Type: ")
        self.xInputTypeBox = wx.Choice(self, -1, choices=['AREF_GROUND', 'AREF_COMMON', 'AREF_DIFF'])
        self.xInputTypeBox.SetSelection(2)
        sizer.Add(xInputTypeLabel, 0, wx.ALIGN_RIGHT)
        sizer.Add(self.xInputTypeBox)

        yInputLabel = wx.StaticText(self, -1, "Y Input Channel: ")
        self.yInputBox = wx.Choice(self, -1, choices=['0', '1', '2', '3', '4', '5', '6', '7'])
        self.yInputBox.SetSelection(1)
        sizer.Add(yInputLabel, 0, wx.ALIGN_RIGHT)
        sizer.Add(self.yInputBox)

        yInputRangeLabel = wx.StaticText(self, -1, "Y Input Range: ")
        self.yInputRangeBox = wx.Choice(self, -1, choices=['0', '1', '2', '3'])
        self.yInputRangeBox.SetSelection(1)
        sizer.Add(yInputRangeLabel, 0, wx.ALIGN_RIGHT)
        sizer.Add(self.yInputRangeBox)

        yInputTypeLabel = wx.StaticText(self, -1, "Y Input Type: ")
        self.yInputTypeBox = wx.Choice(self, -1, choices=['AREF_GROUND', 'AREF_COMMON', 'AREF_DIFF'])
        self.yInputTypeBox.SetSelection(2)
        sizer.Add(yInputTypeLabel, 0, wx.ALIGN_RIGHT)
        sizer.Add(self.yInputTypeBox)

        inputPeriodLabel = wx.StaticText(self, -1, "Samples per second: ")
        self.inputPeriodBox = wx.TextCtrl(self, -1, "3000")
        sizer.Add(inputPeriodLabel, 0, wx.ALIGN_RIGHT)
        sizer.Add(self.inputPeriodBox)

        self.SetSizer(vertSizer)
        self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
        self.Fit()

    def PressOkButton(self, event):
        self.Close(1)

    def OnCloseWindow(self, event):
        self.MakeModal(False)
        self.Show(0)
        print self.getConfiguration()
        #self.Destroy()

    def getConfiguration(self):
        config = dict()
        config["device"] = self.deviceNameTextBox.GetValue()

        config["analog_period_ns"] = int(1e9 / float(self.inputPeriodBox.GetValue()))

        x_channel = int(self.xInputBox.GetStringSelection())
        x_range = int(self.xInputRangeBox.GetStringSelection())
        x_aref_string = self.xInputTypeBox.GetStringSelection()
        x_aref=0
        if x_aref_string=='AREF_GROUND':
            x_aref=comedi.AREF_GROUND
        elif x_aref_string=='AREF_COMMON':
            x_aref=comedi.AREF_COMMON
        elif x_aref_string=='AREF_DIFF':
            x_aref=comedi.AREF_DIFF
        else:
            x_aref=comedi.AREF_OTHER

        y_channel = int(self.yInputBox.GetStringSelection())
        y_range = int(self.yInputRangeBox.GetStringSelection())
        y_aref_string = self.yInputTypeBox.GetStringSelection()
        y_aref=0
        if y_aref_string=='AREF_GROUND':
            y_aref=comedi.AREF_GROUND
        elif y_aref_string=='AREF_COMMON':
            y_aref=comedi.AREF_COMMON
        elif y_aref_string=='AREF_DIFF':
            y_aref=comedi.AREF_DIFF
        else:
            y_aref=comedi.AREF_OTHER

        config["analog"] = [[x_channel, x_range, x_aref], [y_channel, y_range, y_aref]]
        return config

app = wx.PySimpleApp()
frame = MainWindow()
app.MainLoop()
