'''
Created on Jun 14, 2012

@author: manuel
'''
from PhysioMonitor.SyringePump import SyringePump, SyringePumpException, \
    unforseenException, invalidCommandException
import serial
import re
import time
from PhysioMonitor.SyringePump import pumpInvalidAnswerException, \
    valueOORException

DEBUG = True


class AladdinPump(SyringePump):
    _units = ['mL/hr', 'mL/min', 'uL/hr', 'uL/min']
    _directions = ['STOP', 'INFUSION', 'WITHDRAWAL']
    _readTimeout = 1  # second
    # ******* PUMP ANSWERS ******
    __ANS_TRUE = '1'
    __ANS_FALSE = '0'
    # direction
    __ANS_DIR_INF = 'INF'
    __ANS_DIR_WDR = 'WDR'
    __ANS_DIR_REV = 'REV'
    # units
    __ANS_UNITS_ML = 'ML'
    __ANS_UNITS_UL = 'UL'
    __ANS_UNITS_ULMIN = 'UM'
    __ANS_UNITS_ULHR = 'UH'
    __ANS_UNITS_MLMIN = 'MM'
    __ANS_UNITS_MLHR = 'MH'
    __ANS_UNITS = [__ANS_UNITS_MLHR, __ANS_UNITS_MLMIN, __ANS_UNITS_ULHR, __ANS_UNITS_ULMIN]
    # status message
    __ANS_STATUS_INFUSING = 'I'
    __ANS_STATUS_WITHDRAWING = 'W'
    __ANS_STATUS_STOPPED = 'S'
    __ANS_STATUS_PAUSED = 'P'
    __ANS_STATUS_PAUSEPHASE = 'T'
    __ANS_STATUS_TRIGGERWAIT = 'U'
    # alarm message
    __ANS_ALARM_RESET = 'R'
    __ANS_ALARM_STALLED = 'S'
    __ANS_ALARM_TIMEOUT = 'T'
    __ANS_ALARM_PROGERROR = 'E'
    __ANS_ALARM_PHASEOOR = 'O'
    # regexp to extract status and message in response packet 
    __ANS_PATTERN = r'''^\x02([0-9]{2})([''' + \
                    __ANS_STATUS_INFUSING + \
                    __ANS_STATUS_WITHDRAWING + \
                    __ANS_STATUS_STOPPED + \
                    __ANS_STATUS_PAUSED + \
                    __ANS_STATUS_PAUSEPHASE + \
                    __ANS_STATUS_TRIGGERWAIT + \
                    ''']|A\?[''' + \
                    __ANS_ALARM_RESET + \
                    __ANS_ALARM_STALLED + \
                    __ANS_ALARM_TIMEOUT + \
                    __ANS_ALARM_PROGERROR + \
                    __ANS_ALARM_PHASEOOR + \
                    '''])(.*)\x03$'''
    # regexp to extract model and version numbers 
    __ANS_VER_PATTERN = r'^NE([0-9]+)V([0-9]+).([0-9]+)$'
    # regexp to extract dispensed volume
    __ANS_DISVOL_PATTERN = r'^I([0-9\.]+)W([0-9\.]+)([UML]{2})$'
    # error codes
    __ANS_ERROR_UNRECOGNIZED = '?'
    __ANS_ERROR_NOTAPPLICABLE = '?NA'
    __ANS_ERROR_OOR = '?OOR'
    __ANS_ERROR_COMERR = '?COM'
    __ANS_ERROR_IGNORED = '?IGN'
    # trigger modes
    __ANS_TRIG_FOOT = 'FT'
    __ANS_TRIG_TTL = 'LE'
    __ANS_TRIG_START = 'ST'

    # ******* Commands *******
    # GET commands
    __CMD_GET_DIAMETER = 'DIA\r'  # get the syringe diameter
    __CMD_GET_PHASE = 'PHN\r'  # get the phase number
    __CMD_GET_PHASEFUNCTION = 'FUN\r'  # get the program's phase function
    __CMD_GET_RATE = 'RAT\r'  # get the rate of inf/withd, including unit
    __CMD_GET_TARVOL = 'VOL\r'  # get target volume, incl units
    __CMD_GET_DIR = 'DIR\r'  # get the direction of the pump
    __CMD_GET_DISVOL = 'DIS\r'  # get the volume dispensed in infusion as well as in withdrawal
    __CMD_GET_ALARM = 'AL\r'  # get current alarm mode
    __CMD_GET_POWERFAIL = 'PF\r'  # get current power failure mode
    __CMD_GET_TRIGMODE = 'TRG\r'  # get current trigger mode
    __CMD_GET_TTLDIR = 'DIN\r'  # get TTL directional control mode
    __CMD_GET_TTLOUT = 'ROM\r'  # get TTL output mode
    __CMD_GET_KEYLOCK = 'LOC\r'  # get state of keypad lock
    __CMD_GET_KEYBEEP = 'BP\r'  # get keypad beep mode
    __CMD_GET_TTLIO = 'IN\r'  # get ttl level of TTL I/O connector
    __CMD_GET_BUZZ = 'BUZ\r'  # gets whether buzzer is buzzing
    __CMD_GET_VERSION = 'VER\r'  # gets the model number and the firmware version
    # SET commands
    __CMD_SET_DIAMETER = 'DIA%s\r'  # set the syringe diameter
    __CMD_SET_PHASE = 'PHN%d\r'  # set the phase number
    __CMD_SET_PHASEFUNCTION = 'FUN%d\r'  # set the program's phase function
    # ... bunch of other instructions could be here. cf p50 of the manual
    __CMD_SET_RATE = 'RAT%s%s\r'  # set the rate of inf/withdraw, including unit
    __CMD_SET_TARVOL = 'VOL%s\r'  # set target volume, units depends on diameter
    __CMD_SET_DIR = 'DIR%s\r'  # set the direction of the pump
    __CMD_SET_RUNPHASE = 'RUN%d\r'  # start the pumping program
    __CMD_SET_STOP = 'STP\r'  # stops the pump
    __CMD_CLEAR_DISVOL = 'CLD%s\r'  # clears dispensed volume in INF or WITHDR
    __CMD_SET_ADDRESS = 'ADR%02d\r'  # sets the network address
    __CMD_SET_SAFEMODE = 'SAF%03d\r'  # enables safe mode communication
    __CMD_SET_ALARM = 'AL%d\r'  # set the alarm mode
    __CMD_SET_POWERFAIL = 'PF%d\r'  # set current power failure mode
    __CMD_SET_TRIGMODE = 'TRG%s\r'  # set current trigger mode
    __CMD_SET_TTLDIR = 'DIN%d\r'  # set TTL directional control mode
    __CMD_SET_TTLOUT = 'ROM%d\r'  # set TTL output mode
    __CMD_SET_KEYLOCK = 'LOC%d\r'  # set state of keypad lock
    __CMD_SET_KEYBEEP = 'BP%d\r'  # set keypad beep mode
    __CMD_SET_TTLIO = 'OUT5%s\r'  # set ttl level of TTL I/O connector pin 5
    __CMD_SET_BUZZ = 'BUZ%d%d\r'  # sets whether buzzer is buzzing

    def __init__(self, inPort=0, inBaudRate=19200, inAddress=0):
        self.port = inPort
        self.baudRate = inBaudRate
        self.address = inAddress
        self.ansParser = re.compile(self.__ANS_PATTERN)
        self.serial = None
        self.serial = serial.Serial(self.port, self.baudRate,
                                    bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE,
                                    stopbits=serial.STOPBITS_TWO,
                                    timeout=0)
        self.serial.flush()
        self.serial.flushInput()
        self.serial.flushOutput()

    def __del__(self):
        if self.serial is not None:
            self.sendCommand(self.__CMD_SET_BUZZ % (1, 1))
            self.serial.flush()
            self.serial.flushInput()
            self.serial.flushOutput()
            self.serial.close()

    @staticmethod
    def formatFloat(inValue):
        return '{:04f}'.format(inValue)[:5]

    def getInfo(self):
        port = self.serial.portstr
        version = self.getVersion()
        diameter = self.getDiameter()
        rate = self.getRate()
        units = self._units[self.getUnits()]
        target = self.getTargetVolume()
        volume = self.getAccumulatedVolume()
        direction = self._directions[self.getDirection()]
        return "Syringe Pump %s (%s) {direction: %s, diameter: %04f mm, rate: %04f %s, accumulated volume: %04f, target volume: %04f}" % (
        version, port, direction, diameter, rate, units, volume, target)

    def sendCommand(self, inCommand, returnAll=False):
        self.serial.flush()
        self.serial.flushInput()
        self.serial.flushOutput()
        if DEBUG: print "~~sending command \"%02d%s\"..." % (self.address, inCommand.replace('\r', '\\r'))  # DEBUG
        self.serial.write(inCommand)
        sendTime = time.time();
        ans = ''
        nbBytes = 0
        while (ans[-1:] <> '\x03'):
            nbChar = self.serial.inWaiting()
            nbBytes += nbChar
            ans += str(self.serial.read(nbChar))
            if (time.time() - sendTime) > self._readTimeout:
                raise readTimeoutException("Timeout while waiting for an answer")
        self.serial.flush()
        self.serial.flushInput()
        self.serial.flushOutput()
        if DEBUG: print "~~reading %d bytes in response: \"%s\"" % (nbBytes, repr(ans))  # DEBUG
        address, status, message = self.parse(ans)
        if 'A?' in status:
            raise alarmException(status)
        if '?' in message:
            raise errorException(message)
        if returnAll:
            return (address, status, message)
        else:
            return message

    def parse(self, inVal):
        m = self.ansParser.match(inVal)
        if not m:
            raise pumpInvalidAnswerException
        if DEBUG: print "~~received valid answer from pump [%02s]. Status is '%s' and answer is '%s'" % m.groups()
        return m.groups()

    def start(self):
        _, status, _ = self.sendCommand(self.__CMD_SET_RUNPHASE % (1), True)
        if 'A?' in status:
            self.alarm(status)
        if not ('I' in status or 'W' in status):
            raise unforseenException("Pump did not start!")

    def stop(self):
        _, status, _ = self.sendCommand(self.__CMD_SET_STOP, True)
        if 'A?' in status:
            self.alarm(status)
        if '?' in status:
            self.error(status)
        if not 'P' in status:
            raise unforseenException("Pump did not stop")

    def reverse(self):
        self.sendCommand(self.__CMD_SET_DIR % self.__ANS_DIR_REV)

    def isRunning(self):
        _, status, _ = self.sendCommand('\r', True)
        return (status == self.__ANS_STATUS_INFUSING or status == self.__ANS_STATUS_WITHDRAWING)

    def clearAccumulatedVolume(self):
        self.sendCommand(self.__CMD_CLEAR_DISVOL % (self.__ANS_DIR_INF))
        self.sendCommand(self.__CMD_CLEAR_DISVOL % (self.__ANS_DIR_WDR))

    def clearTargetVolume(self):
        self.sendCommand(self.__CMD_SET_TARVOL % (0.0))

    def setDirection(self, inValue):
        if inValue == 1:
            self.sendCommand(self.__CMD_SET_DIR % (self.__ANS_DIR_INF))
        elif inValue == 2:
            self.sendCommand(self.__CMD_SET_DIR % (self.__ANS_DIR_WDR))
        else:
            raise invalidCommandException()

    def setSyringeDiameter(self, inValue):
        if inValue <= 0:
            raise valueOORException("Diameter must be a positive float value")
        else:
            self.sendCommand(self.__CMD_SET_DIAMETER % (self.formatFloat(inValue)))

    def setRate(self, inValue, inUnits):
        if inValue <= 0:
            raise valueOORException("Rate must be a positive value")
        if inUnits < 0 or inUnits > len(self._units) - 1:
            raise valueOORException("Units must be an integer between %d and %d" % (0, len(self._units) - 1))
        ans = self.sendCommand(self.__CMD_SET_RATE % (self.formatFloat(inValue), self.__ANS_UNITS[int(inUnits)]))
        if '?' in ans:
            raise invalidCommandException(ans)

    def setTargetVolume(self, inValue):
        if inValue <= 0:
            raise valueOORException("Target volume must be a positive float value")
        else:
            self.sendCommand(self.__CMD_SET_TARVOL % (self.formatFloat(float(inValue))))

    def getDiameter(self):
        ans = self.sendCommand(self.__CMD_GET_DIAMETER)
        return float(ans)

    def getRate(self):
        ans = self.sendCommand(self.__CMD_GET_RATE)
        return float(ans[:-2])  # strips units

    def getUnits(self):
        ans = self.sendCommand(self.__CMD_GET_RATE)
        # [-2:] keeps only the last two char, corresponding to the units
        if ans[-2:] == self.__ANS_UNITS_MLHR:
            return 0
        elif ans[-2:] == self.__ANS_UNITS_MLMIN:
            return 1
        elif ans[-2:] == self.__ANS_UNITS_ULHR:
            return 2
        elif ans[-2:] == self.__ANS_UNITS_ULMIN:
            return 3
        else:
            raise unforseenException("ERROR while parsing rate value")

    def getAccumulatedInfusionVolume(self):
        ans = self.sendCommand(self.__CMD_GET_DISVOL)
        m = re.match(self.__ANS_DISVOL_PATTERN, ans)
        if m:
            return float(m.group(1))
        else:
            raise unforseenException("Error while parsing accumulated volume")

    def getAccumulatedWithdrawalVolume(self):
        ans = self.sendCommand(self.__CMD_GET_DISVOL)
        m = re.match(self.__ANS_DISVOL_PATTERN, ans)
        if m:
            return float(m.group(2))
        else:
            raise unforseenException("Error while parsing accumulated volume")

    def getAccumulatedVolume(self):
        return self.getAccumulatedInfusionVolume()

    def getTargetVolume(self):
        ans = self.sendCommand(self.__CMD_GET_TARVOL)
        return float(ans[:-2])  # strips units

    def getTargetUnits(self):
        ans = self.sendCommand(self.__CMD_GET_TARVOL)
        return ans[-2:]

    def getDirection(self):
        if self.isRunning():
            ans = self.sendCommand(self.__CMD_GET_DIR)
            if ans == self.__ANS_DIR_INF:
                return 1
            elif ans == self.__ANS_DIR_WDR:
                return 2
            else:
                return LookupError('Error while parsing direction')
        else:
            return 0

    def getPossibleUnits(self):
        return self._units

    def getVersion(self):
        ans = self.sendCommand(self.__CMD_GET_VERSION)
        m = re.match(self.__ANS_VER_PATTERN, ans)
        if not m:
            raise unforseenException("Error while parsing version number")
        else:
            return 'Model #%s, firmware v%s.%s' % (m.groups())

    def doBeep(self, nbBeeps=1):
        self.sendCommand(self.__CMD_SET_BUZZ % (1, nbBeeps))


class errorException(SyringePumpException):
    def __init__(self, status):
        if status == '?COM':
            Exception.__init__(self, "Invalid communications packet received")
        elif status == '?IGN':
            Exception.__init__(self, "Command ignored due to a simultaneous new phase start")
        elif status == '?NA':
            Exception.__init__(self, "Command is not currently applicable")
        elif status == '?OOR':
            Exception.__init__(self, "Command data is out of range")
        elif status == '?':
            Exception.__init__(self, "Command is not recognized")
        else:
            Exception.__init__(self, status)


class alarmException(SyringePumpException):
    pass


class readTimeoutException(SyringePumpException):
    pass
