#  %W%  %G% CSS
#  "pyspec" Release %R%
#
#$Id: SpecMotor.py,v 1.6 2005/02/08 13:17:21 guijarro Exp $
"""SpecMotor module

This module defines the classes for motor objects

Classes:
SpecMotor -- class representing a motor in Spec
SpecMotorA -- class representing a motor in Spec, to be used with a GUI
"""

__author__ = 'Matias Guijarro'
__version__ = '1.0'

import math

from pyspec.css_logger import log

from SpecConnection import SpecConnection
from SpecCommand import  SpecCommandA
from SpecWaitObject import SpecWaitObject
import SpecEventsDispatcher as SpecEventsDispatcher

(NOTINITIALIZED, UNUSABLE, READY, MOVESTARTED, MOVING, ONLIMIT) = (0,1,2,3,4,5)
(NOLIMIT, LOWLIMIT, HIGHLIMIT) = (0,2,4)

class SpecMotorA(object):
    """SpecMotorA class"""
    def __init__(self, specName = None, specVersion = None, callbacks={}):
        """Constructor

        Keyword arguments:
        specName -- name of the motor in Spec (defaults to None)
        specVersion -- 'host:port' string representing a Spec server to connect to (defaults to None)
        """
        self.motorState = NOTINITIALIZED
        self.limit = NOLIMIT
        self.limits = (None, None)
        self.chanNamePrefix = ''
        self.connection = None
        self.__old_position = None
        # the callbacks listed below can be set directly using the 'callbacks' keyword argument ;
        # when the event occurs, the corresponding callback will be called automatically
        self.__callbacks = {
          'connected': None,
          'disconnected': None,
          'motorLimitsChanged': None,
          'motorPositionChanged': None,
          'motorStateChanged': None
        }
        for cb_name in iter(self.__callbacks.keys()):
          if callable(callbacks.get(cb_name)):
            self.__callbacks[cb_name] = SpecEventsDispatcher.callableObjectRef(callbacks[cb_name])

        if specName is not None and specVersion is not None:
            self.connectToSpec(specName, specVersion)
        else:
            self.specName = None
            self.specVersion = None


    def connectToSpec(self, specName, specVersion):
        """Connect to a remote Spec

        Connect to Spec and register channels of interest for the specified motor

        Arguments:
        specName -- name of the motor in Spec
        specVersion -- 'host:port' string representing a Spec server to connect to
        """
        self.specName = specName
        self.specVersion = specVersion
        self.chanNamePrefix = 'motor/%s/%%s' % specName

        if isinstance(self.specVersion, str):
            self.connection = SpecConnection(specVersion)
        else:
            self.connection = self.specVersion

        self.connection.connect('connected', self.__connected)
        self.connection.connect('disconnected', self.__disconnected)

        if self.connection.isSpecConnected():
            self.__connected()

    def update(self):
        self.connection.update()

    def __connected(self):
        """Private callback triggered by a 'connected' event from Spec."""
        #
        # register channels
        #
        self.connection.registerChannel(self.chanNamePrefix % 'low_limit',
                                        self._motorLimitsChanged)
        self.connection.registerChannel(self.chanNamePrefix % 'high_limit',
                                        self._motorLimitsChanged)
        self.connection.registerChannel(self.chanNamePrefix % 'position',
                                        self.__motorPositionChanged,
                                        dispatchMode=SpecEventsDispatcher.FIREEVENT)
        self.connection.registerChannel(self.chanNamePrefix % 'move_done',
                                        self.motorMoveDone,
                                        dispatchMode = SpecEventsDispatcher.FIREEVENT)
        self.connection.registerChannel(self.chanNamePrefix % 'high_lim_hit', self.__motorLimitHit)
        self.connection.registerChannel(self.chanNamePrefix % 'low_lim_hit', self.__motorLimitHit)
        self.connection.registerChannel(self.chanNamePrefix % 'sync_check', self.__syncQuestion)
        self.connection.registerChannel(self.chanNamePrefix % 'unusable', self.__motorUnusable)
        self.connection.registerChannel(self.chanNamePrefix % 'offset', self.motorOffsetChanged)
        self.connection.registerChannel(self.chanNamePrefix % 'sign', self.signChanged)
        #self.connection.registerChannel(self.chanNamePrefix % 'dial_position', self.dialPositionChanged)

        # trigger an event on socket => will have no effect (spec reply will be ignored), but SpecEventsDispatcher.dispatch
        # will get called
        self.connection.send_msg_hello()
 
        try: 
          if self.__callbacks.get("connected"):
            cb = self.__callbacks["connected"]()
            if cb is not None:
              cb()
        finally:
          self.connected()


    def connected(self):
        """Callback triggered by a 'connected' event from Spec

        To be extended by derivated classes.
        """
        pass


    def __disconnected(self):
        """Private callback triggered by a 'disconnected' event from Spec

        Put the motor in NOTINITIALIZED state.
        """
        self.__changeMotorState(NOTINITIALIZED)

        try:
          if self.__callbacks.get("disconnected"):
            cb = self.__callbacks["disconnected"]()
            if cb is not None:
              cb()
        finally:
          self.disconnected()


    def disconnected(self):
        """Callback triggered by a 'disconnected' event from Spec

        To be extended by derivated classes.
        """
        pass


    #def dialPositionChanged(self, dial_position):
    #    pass


    def signChanged(self, sign):
        self._motorLimitsChanged()


    def motorOffsetChanged(self, offset):
        self._motorLimitsChanged()


    def _motorLimitsChanged(self):
        try:
          if self.__callbacks.get("motorLimitsChanged"):
            cb = self.__callbacks["motorLimitsChanged"]()
            if cb is not None:
              cb()
        finally:
          self.motorLimitsChanged()


    def motorLimitsChanged(self):
        """Callback triggered by a 'low_limit' or a 'high_limit' channel update,
        or when the sign or offset for motor changes

        To be extended by derivated classes.
        """
        pass


    def motorMoveDone(self, channelValue):
        """Callback triggered when motor starts or stops moving

        Change the motor state accordingly.

        Arguments:
        channelValue -- value of the channel
        """

        if channelValue:
            self.__changeMotorState(MOVING)
        elif self.motorState == MOVING or self.motorState == MOVESTARTED or self.motorState == NOTINITIALIZED:
            self.__changeMotorState(READY)


    def __motorLimitHit(self, channelValue, channelName):
        """Private callback triggered by a 'low_lim_hit' or a 'high_lim_hit' channel update

        Update the motor state accordingly.

        Arguments:
        channelValue -- value of the channel
        channelName -- name of the channel (either 'low_lim_hit' or 'high_lim_hit')
        """
        if channelValue:
            if channelName.endswith('low_lim_hit'):
                self.limit = self.limit | LOWLIMIT
                self.__changeMotorState(ONLIMIT)
            else:
                self.limit = self.limit | HIGHLIMIT
                self.__changeMotorState(ONLIMIT)


    def __motorPositionChanged(self, absolutePosition):
        if self.__old_position is None:
           self.__old_position = absolutePosition
        else:
           if math.fabs(absolutePosition - self.__old_position) > 1E-6:
              self.__old_position = absolutePosition
           else:
              return
        try:
          if self.__callbacks.get("motorPositionChanged"):
            cb = self.__callbacks["motorPositionChanged"]()
            if cb is not None:
              cb(absolutePosition)
        finally:
          self.motorPositionChanged(absolutePosition)


    def motorPositionChanged(self, absolutePosition):
        """Callback triggered by a position channel update

        To be extended by derivated classes.

        Arguments:
        absolutePosition -- motor absolute position
        """
        pass


    def setOffset(self, offset):
        """Set the motor offset value"""
        c = self.connection.getChannel(self.chanNamePrefix % 'offset')

        c.write(offset)


    def getOffset(self):
        c = self.connection.getChannel(self.chanNamePrefix % 'offset')

        return c.read()


    def getSign(self):
        c = self.connection.getChannel(self.chanNamePrefix % 'sign')

        return c.read()

    def __syncQuestion(self, channelValue):
        """Callback triggered by a 'sync_check' channel update

        Call the self.syncQuestionAnswer method and reply to the sync question.

        Arguments:
        channelValue -- value of the channel
        """
        if type(channelValue) == type(''):
            steps = channelValue.split()
            specSteps = steps[0]
            controllerSteps = steps[1]

            a = self.syncQuestionAnswer(specSteps, controllerSteps)

            if a is not None:
                c = self.connection.getChannel(self.chanNamePrefix % 'sync_check')
                c.write(a)


    def syncQuestionAnswer(self, specSteps, controllerSteps):
        """Answer to the sync. question

        Return either '1' (YES) or '0' (NO)

        Arguments:
        specSteps -- steps measured by Spec
        controllerSteps -- steps indicated by the controller
        """
        pass


    def __motorUnusable(self, unusable):
        """Private callback triggered by a 'unusable' channel update

        Update the motor state accordingly

        Arguments:
        unusable -- value of the channel
        """
        if unusable:
            self.__changeMotorState(UNUSABLE)
        else:
            self.__changeMotorState(READY)


    def __changeMotorState(self, state):
        """Private method for changing the SpecMotor object's internal state

        Arguments:
        state -- the motor state
        """
        self.motorState = state

        try:
          if self.__callbacks.get("motorStateChanged"):
            cb = self.__callbacks["motorStateChanged"]()
            if cb is not None:
              cb(state)
        finally:
          self.motorStateChanged(state)


    def motorStateChanged(self, state):
        """Callback to take into account a motor state update

        To be extended by derivated classes

        Arguments:
        state -- the motor state
        """
        pass


    def move(self, absolutePosition):
        """Move the motor to the required position

        Arguments:
        absolutePosition -- position to move to
        """
        if not isinstance(absolutePosition, float) and not isinstance(absolutePosition,int):
            log.error("Cannot move %s: position '%s' is not a number", self.specName, absolutePosition)

        self.__changeMotorState(MOVESTARTED)

        c = self.connection.getChannel(self.chanNamePrefix % 'start_one')
        
        c.write(absolutePosition)


    def moveRelative(self, relativePosition):
        self.move(self.getPosition() + relativePosition)


    def moveToLimit(self, limit):
        cmdObject = SpecCommandA("_mvc", self.connection)

        if cmdObject.isSpecReady():
            if limit:
                cmdObject(1)
            else:
                cmdObject(-1)


    def stop(self):
        """Stop the current motor

        Send an 'abort' message to the remote Spec
        """
        self.connection.abort()


    def stopMoveToLimit(self):
        c = self.connection.getChannel("var/_MVC_CONTINUE_MOVING")
        c.write(0)


    def getParameter(self, param):
        c = self.connection.getChannel(self.chanNamePrefix % param)
        return c.read()


    def setParameter(self, param, value):
        c = self.connection.getChannel(self.chanNamePrefix % param)
        c.write(value)


    def getPosition(self):
        """Return the current position of the motor."""
        c = self.connection.getChannel(self.chanNamePrefix % 'position')

        return c.read()


    def getState(self):
        """Return the current motor state."""
        return self.motorState


    def getLimits(self):
        """Return a (low limit, high limit) tuple in user units."""
        lims = [x * self.getSign() + self.getOffset() for x in (self.connection.getChannel(self.chanNamePrefix % 'low_limit').read(), \
                                                                self.connection.getChannel(self.chanNamePrefix % 'high_limit').read())]
        return (min(lims), max(lims))


    def getDialPosition(self):
        """Return the motor dial position."""
        c = self.connection.getChannel(self.chanNamePrefix % 'dial_position')

        return c.read()


class SpecMotor(object):
    """Spec Motor"""
    def __init__(self, specName = None, specVersion = None, timeout = None):
        """Constructor

        Keyword arguments:
        specName -- name of the motor in Spec (defaults to None)
        specVersion -- 'host:port' string representing a Spec server to connect to (defaults to None)
        timeout -- optional timeout for the connection (defaults to None)
        """
        self.chanNamePrefix = ''
        self.connection = None

        if specName is not None and specVersion is not None:
            self.connectToSpec(specName, specVersion, timeout)
        else:
            self.specName = None
            self.specVersion = None

    def connectToSpec(self, specName, specVersion, timeout = None):
        """Connect to a remote Spec

        Block until Spec is connected or timeout occurs

        Arguments:
        specName -- name of the motor in Spec
        specVersion -- 'host:port' string representing a Spec server to connect to
        timeout -- optional timeout for the connection (defaults to None)
        """
        self.specName = specName
        self.specVersion = specVersion
        self.chanNamePrefix = 'motor/%s/%%s' % specName

        if isinstance(specVersion,str):
            self.connection = SpecConnection(specVersion)
        else:
            self.connection = specVersion

        w = SpecWaitObject(self.connection)
        w.waitConnection(timeout)


    def unusable(self):
        """Return whether the motor is unusable or not."""
        if self.connection is not None:
            c = self.connection.getChannel(self.chanNamePrefix % 'unusable')

            return c.read()


    def lowLimitHit(self):
        """Return if low limit has been hit."""
        if self.connection is not None:
            c = self.connection.getChannel(self.chanNamePrefix % 'low_lim_hit')

            return c.read()


    def highLimitHit(self):
        """Return if high limit has been hit."""
        if self.connection is not None:
            c = self.connection.getChannel(self.chanNamePrefix % 'high_lim_hit')

            return c.read()


    def move(self, absolutePosition):
        """Move the motor

        Block until the move is finished

        Arguments:
        absolutePosition -- position where to move the motor to
        """
        if self.connection is not None:
            c = self.connection.getChannel(self.chanNamePrefix % 'start_one')

            c.write(absolutePosition)

            w = SpecWaitObject.SpecWaitObject(self.connection)
            w.waitChannelUpdate(self.chanNamePrefix % 'move_done', waitValue = 0) #move_done is set to 0 when move has finished


    def moveRelative(self, relativePosition):
        self.move(self.getPosition() + relativePosition)


    def moveToLimit(self, limit):
        if self.connection is not None:
            cmdObject = SpecCommandA("_mvc", self.connection)

            if cmdObject.isSpecReady():
                if limit:
                    cmdObject(self.specName, 1)
                else:
                    cmdObject(self.specName, -1)


    def stop(self):
        """Stop the current motor

        Send an 'abort' message to the remote Spec
        """
        self.connection.abort()


    def stopMoveToLimit(self):
        if self.connection is not None:
            c = self.connection.getChannel("var/_MVC_CONTINUE_MOVING")
            c.write(0)


    def getPosition(self):
        """Return the current absolute position for the motor."""
        if self.connection is not None:
            c = self.connection.getChannel(self.chanNamePrefix % 'position')

            return c.read()


    def setOffset(self, offset):
        """Set the motor offset value"""
        if self.connection is not None:
             c = self.connection.getChannel(self.chanNamePrefix % 'offset')

             c.write(offset)


    def getOffset(self):
        if self.connection is not None:
            c = self.connection.getChannel(self.chanNamePrefix % 'offset')

            return c.read()


    def getSign(self):
        if self.connection is not None:
            c = self.connection.getChannel(self.chanNamePrefix % 'sign')

            return c.read()


    def getDialPosition(self):
        if self.connection is not None:
            c = self.connection.getChannel(self.chanNamePrefix % 'dial_position')

            return c.read()


    def getLimits(self):
        if self.connection is not None:
            lims = [x * self.getSign() + self.getOffset() for x in (self.connection.getChannel(self.chanNamePrefix % 'low_limit').read(), \
                                                                    self.connection.getChannel(self.chanNamePrefix % 'high_limit').read())]
            return (min(lims), max(lims))
















