예제 #1
0
    def _cmdCallback(self, cmdVar=None):
        """!Command callback
        """
        # call the callback, if justified:
        # function exists, last message code matches and command does not have a reportable failure
        if (self.callFunc is not None) and (
                self.cmdVar.lastCode
                in self.callCodes) and not self._reportFailure:
            # either the command succeeded, or checking for success is disabled
            try:
                self.callFunc(self.cmdVar)
            except Exception as e:
                # callback failed; fail this command
                traceback.print_exc(file=sys.stderr)  # is this always needed?
                Timer(0, self._finish, e)
                return

        if self.cmdVar.isDone:
            if self._reportFailure:
                try:
                    reason = self.cmdVar.lastReply.string
                except Exception:
                    reason = "why? (no lastReply)"
                msgStr = "Command %s failed: %s" % (self.cmdVar, reason)
                print(msgStr)
                Timer(0, self._finish, RuntimeError(msgStr))
            else:
                print("Command %s done" % (self.cmdVar, ))
                Timer(0, self._finish)
예제 #2
0
    def __init__(
        self,
        cmdStr,
        userID=0,
        cmdID=0,
        callFunc=None,
        timeLim=None,
    ):
        """Construct a BaseCmd

        @param[in] cmdStr  command string
        @param[in] userID  user ID number
        @param[in] cmdID  command ID number
        @param[in] callFunc  function to call when command changes state;
            receives one argument: this command
        @param[in] timeLim  time limit for command (sec); if None or 0 then no time limit
        """
        self._cmdStr = cmdStr
        self.userID = int(userID)
        self.cmdID = int(cmdID)
        self._state = self.Ready
        self._textMsg = ""
        self._hubMsg = ""
        self._cmdToTrack = None
        self._linkedCommands = []
        self._parentCmd = None
        self._writeToUsers = None  # set by baseActor.ExpandCommand
        # set by baseActor.newCmd to flag this as a command created
        # from socket input
        self.userCommanded = False
        self._timeoutTimer = Timer()
        self.setTimeLimit(timeLim)

        RO.AddCallback.BaseMixin.__init__(self, callFunc)
예제 #3
0
    def __init__(self, dev, userCmd, timeLim):
        """!Start connecting a device

        @param[in] dev  device
        @param[in] userCmd  user command associated with the connection, or None
        @param[in] timeLim  time limit (sec) to make this connection
        """
        self.dev = dev
        self._timeLim = timeLim
        self.userCmd = expandUserCmd(userCmd)
        self._connTimer = Timer()
        self._addedConnCallback = False

        if self.dev.isConnected and self.dev.conn.isConnected:
            # already done; don't send init
            self.finish()
            return

        self.dev._ignoreConnCallback = True
        self.dev.setState(self.dev.Connecting)
        self.dev.conn.addStateCallback(self.connCallback)
        self._addedConnCallback = True
        if self.dev.conn.mayConnect:
            self.dev.conn.connect(timeLim=timeLim)
        else:
            if self._timeLim:
                # start timer for the connection that is occurring now
                self._connTimer.start(self._timeLim, self.finish,
                                      "timed out waiting for connection")
예제 #4
0
    def __init__(self, dev, userCmd, timeLim):
        """!Start disconnecting a device
        """
        self.dev = dev
        self.userCmd = expandUserCmd(userCmd)
        self._timeLim = timeLim
        self._connTimer = Timer()
        self._addedConnCallback = False

        if self.dev.conn.isDisconnected:
            if self.dev.state != self.dev.Disconnected:
                self.dev.setState(self.dev.Disconnected, "socket disconnected")
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)
            return

        self.dev._ignoreConnCallback = True
        if self.dev.state != self.dev.Disconnected:
            self.dev.setState(self.dev.Disconnecting)

        if self.dev.conn.isConnected:
            initUserCmd = UserCmd(cmdStr="Init via device.DisconnectDevice",
                                  callFunc=self.initCallback,
                                  timeLim=timeLim)
            self.dev.init(userCmd=initUserCmd,
                          timeLim=timeLim,
                          getStatus=False)
        else:
            # not fully connected, so cannot send init, but not fully disconnected yet, so finish disconnecting
            textMsg = "%s connection state=%s; cannot initialize before disconnecting" % (
                self.dev.name, self.dev.conn.state)
            self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg), ))
            self.startDisconnect()
            return
예제 #5
0
    def __init__(self, name, port):
        """!Construct a fake LCO scale controller

        @param[in] name  name of scale controller
        @param[in] port  port on which to command scale controller
        """
        global GlobalScalePosition
        self.isMoving = False
        self.moveRange = [0., 40.]
        self.desPosition = GlobalScalePosition # wake up in mid travel
        self.position = GlobalScalePosition
        self.speed = 0.5
        self.moveTimer = Timer()
        self.posSw1, self.posSw2, self.posSw3 = (1, 1, 1)
        self.cartID = 0
        self.lockPos = 18
        # faults
        self.trOT = False
        self.trHWF, self.trIF = 0, 0
        self.lockHWF, self.lockIF = 0, 0
        self.winchHWF, self.winchIF = 0, 0
        self.positionTimer = Timer()
        self.userSock = None # created upon connection
        FakeDev.__init__(self,
            name = name,
            port = port,
            doEcho = True,
        )
 def __init__(self):
     """A pure python fake camera
     """
     self.state = Idle
     self.currExposure = Exposure(None, None, None)
     self.expTimer = Timer()
     self.readTimer = Timer()
     self.expBegin = None
     self.expAccumulated = None
     self.setConfig(CameraConfig())
     self._expName = ""
     self.failExposure = False # for unittesting
예제 #7
0
class FilterWheel(object):
    def __init__(self):
        self.moveTimer = Timer()

    def stopMove(self):
        self.moveTimer.cancel()
        StatusDict["motor"]=0
        # StatusDict["motor"]=8

    def connect(self):
        """
        initiate filter wheel communication
        """
        pass

    def home(self):
        """
        home filter wheel and set the filter wheel id
        """
        # emulate the block
        time.sleep(3)
        # StatusDict["id"] = 1
        return StatusDict["id"]

    def stop(self):
        """
        stop filter wheel motion
        """
        return self.stopMove()

    def moveToPosition(self, pos):
        """
        move to a specific filter wheel position
        arguments:
            int pos - filter position between 1 and 6
        status:
            0 - fail
            1 - succeed
            -1 - unknown
        """
        StatusDict["position"] = int(pos)
        # StatusDict["motor"] = 1
        StatusDict["motor"] = 3
        self.moveTimer.start(MoveTime, self.stopMove)
        return self.status()

    def status(self):
        """
        return current status of filter wheel
        """
        return StatusDict
예제 #8
0
    def __init__(
        self,
        endpoint=None,
        protocol=None,
        state=BaseSocket.Connected,
        readCallback=nullCallback,
        stateCallback=nullCallback,
        timeLim=None,
        name="",
    ):
        """Construct a Socket

        Inputs:
        - endpoint  a Twisted endpoint, e.g. twisted.internet.endpoints.TCP4ClientEndpoint;
        - protocol  a Twisted protocol;
            you must either specify endpoint or protocol, but not both
        - state     the initial state
        - readCallback  function to call when data read; receives: self
        - stateCallback a state callback function; see addStateCallback for details
        - timeLim   time limit to make connection (sec); no limit if None or 0
        - name      a string to identify this socket; strictly optional
        """
        #print "Socket(name=%r, endpoint=%r, protocol=%r, state=%r, readCallback=%r, stateCallback=%r)" % \
        #    (name, endpoint, protocol, state, readCallback, stateCallback)
        if bool(endpoint is None) == bool(protocol is None):
            raise RuntimeError("Must provide one of endpoint or protocol")
        self._endpoint = endpoint
        self._endpointDeferred = None
        self._protocol = None
        self._data = None
        self._connectTimer = Timer()
        BaseSocket.__init__(self,
                            state=state,
                            readCallback=readCallback,
                            stateCallback=stateCallback,
                            name=name)
        if protocol is not None:
            self._connectionMade(protocol)
        else:
            if timeLim:
                self._connectTimer.start(timeLim, self._connectTimeout)
            self._setState(BaseSocket.Connecting)
            self._endpointDeferred = self._endpoint.connect(
                _SocketProtocolFactory())
            setCallbacks(
                deferred=self._endpointDeferred,
                callback=self._connectionMade,
                errback=self._connectionLost,
            )
예제 #9
0
    def __init__(self,
        cmdStr,
        userID = 0,
        cmdID = 0,
        callFunc = None,
        timeLim = None,
    ):
        """Construct a BaseCmd

        @param[in] cmdStr  command string
        @param[in] userID  user ID number
        @param[in] cmdID  command ID number
        @param[in] callFunc  function to call when command changes state;
            receives one argument: this command
        @param[in] timeLim  time limit for command (sec); if None or 0 then no time limit
        """
        self._cmdStr = cmdStr
        self.userID = int(userID)
        self.cmdID = int(cmdID)
        self._state = self.Ready
        self._textMsg = ""
        self._hubMsg = ""
        self._cmdToTrack = None

        self._timeoutTimer = Timer()
        self.setTimeLimit(timeLim)

        RO.AddCallback.BaseMixin.__init__(self, callFunc)
예제 #10
0
    def __init__(self, dev, userCmd, timeLim):
        """!Start connecting a device

        @param[in] dev  device
        @param[in] userCmd  user command associated with the connection, or None
        @param[in] timeLim  time limit (sec) to make this connection
        """
        self.dev = dev
        self._timeLim = timeLim
        self.userCmd = expandUserCmd(userCmd)
        self._connTimer = Timer()
        self._addedConnCallback = False

        if self.dev.isConnected and self.dev.conn.isConnected:
            # already done; don't send init
            self.finish()
            return

        self.dev._ignoreConnCallback = True
        self.dev.setState(self.dev.Connecting)
        self.dev.conn.addStateCallback(self.connCallback)
        self._addedConnCallback = True
        if self.dev.conn.mayConnect:
            self.dev.conn.connect(timeLim=timeLim)
        else:
            if self._timeLim:
                # start timer for the connection that is occurring now
                self._connTimer.start(self._timeLim, self.finish, "timed out waiting for connection")
예제 #11
0
    def __init__(self, dev, userCmd, timeLim):
        """!Start disconnecting a device
        """
        self.dev = dev
        self.userCmd = expandUserCmd(userCmd)
        self._timeLim = timeLim
        self._connTimer = Timer()
        self._addedConnCallback = False

        if self.dev.conn.isDisconnected:
            if self.dev.state != self.dev.Disconnected:
                self.dev.setState(self.dev.Disconnected, "socket disconnected")
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)
            return

        self.dev._ignoreConnCallback = True
        if self.dev.state != self.dev.Disconnected:
            self.dev.setState(self.dev.Disconnecting)

        if self.dev.conn.isConnected:
            initUserCmd = UserCmd(cmdStr="Init via device.DisconnectDevice", callFunc=self.initCallback, timeLim=timeLim)
            self.dev.init(userCmd=initUserCmd, timeLim=timeLim, getStatus=False)
        else:
            # not fully connected, so cannot send init, but not fully disconnected yet, so finish disconnecting
            textMsg = "%s connection state=%s; cannot initialize before disconnecting" % (self.dev.name, self.dev.conn.state)
            self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg),))
            self.startDisconnect()
            return
예제 #12
0
    def __init__(self,
        cmdStr,
        userID = 0,
        cmdID = 0,
        callFunc = None,
        timeLim = None,
    ):
        """Construct a BaseCmd

        @param[in] cmdStr  command string
        @param[in] userID  user ID number
        @param[in] cmdID  command ID number
        @param[in] callFunc  function to call when command changes state;
            receives one argument: this command
        @param[in] timeLim  time limit for command (sec); if None or 0 then no time limit
        """
        self._cmdStr = cmdStr
        self.userID = int(userID)
        self.cmdID = int(cmdID)
        self._state = self.Ready
        self._textMsg = ""
        self._hubMsg = ""
        self._cmdToTrack = None
        self._linkedCommands = []
        self._parentCmd = None
        self._writeToUsers = None # set by baseActor.ExpandCommand
        # set by baseActor.newCmd to flag this as a command created
        # from socket input
        self.userCommanded = False
        self._timeoutTimer = Timer()
        self.setTimeLimit(timeLim)

        RO.AddCallback.BaseMixin.__init__(self, callFunc)
예제 #13
0
    def runQueue(self):
        if self.isBusy:
            return

        if self.cmdQueue:
            self.currCmdWrapper = self.cmdQueue.popleft()
            self.currCmdWrapper.setStateFunc(self._cmdWrapperDone)
            #            self.currCmdWrapper.startCmd(self.dispatcher)
            Timer(0, self.currCmdWrapper.startCmd, self.dispatcher)
예제 #14
0
    def __init__(self, name, port):
        """!Construct a fake LCO M2

        @param[in] name  name of M2 controller
        @param[in] port  port on which to command M2

        State=DONE Ori=12500.0,70.0,-12.0,-600.1,925.0 Lamps=off Galil=off
        """
        self.orientation = [15,70.0,-12.0,-600.1,925.0]
        self.targOrientation = [15,70.0,-12.0,-600.1,925.0]
        self.moveState = self.Done
        self.lamps = self.Off
        self.galil = self.Off
        self.speed = 25.0
        self.moveTimer = Timer()
        self.galilTimer = Timer()

        FakeDev.__init__(self,
            name = name,
            port = port,
        )
예제 #15
0
    def __init__(self, name, port):
        """!Construct a fake LCO TCS

        @param[in] name  name of TCS controller
        @param[in] port  port on which to command TCS
        """
        self.rstop = 0
        self.ractive = 0
        self.rmoving = 0
        self.rtracking = 0
        self.dstop = 0
        self.dactive = 0
        self.dmoving = 0
        self.dtracking = 0
        self.istop = 0
        self.iactive = 0
        self.imoving = 0

        self.isClamped = 1
        self.targRot = 0.
        self.rot = 0.
        self.focus = 0.
        self.targFocus = 0.
        self.ra = 0.
        self.dec = 0.
        self.ha = 0.
        self.targRA = 0.
        self.targDec = 0.
        self.offDec = 0.
        self.offRA = 0.
        self.epoch = 2000
        self.telState = self.Idle
        self.focusTimer = Timer()
        self.slewTimer = Timer()
        self.rotTimer = Timer()

        FakeDev.__init__(self,
            name = name,
            port = port,
        )
예제 #16
0
    def initCallback(self, userCmd):
        """!Callback for device initialization
        """
        # print("%s.initCallback(userCmd=%r); _callbacks=%s" % (self, userCmd, userCmd._callbacks))
        if not userCmd.isDone:
            return

        if userCmd.didFail:
            reason = userCmd.getMsg(
            ) or "init command failed for unknown reasons"
        else:
            reason = None
        Timer(0, self.finish, reason)
예제 #17
0
파일: tcsDevice.py 프로젝트: csayres/lcoTCC
    def __init__(self, name, host, port, callFunc=None):
        """!Construct a LCODevice

        Inputs:
        @param[in] name  name of device
        @param[in] host  host address of tcs controller
        @param[in] port  port of tcs controller
        @param[in] callFunc  function to call when state of device changes;
                note that it is NOT called when the connection state changes;
                register a callback with "conn" for that task.
        """
        self.status = Status()
        self._statusTimer = Timer()

        self.waitRotCmd = UserCmd()
        self.waitRotCmd.setState(self.waitRotCmd.Done)
        self.waitRotTimer = Timer()

        # self.waitFocusCmd = UserCmd()
        # self.waitFocusCmd.setState(self.waitFocusCmd.Done)

        self.waitOffsetCmd = UserCmd()
        self.waitOffsetCmd.setState(self.waitOffsetCmd.Done)
        self.waitOffsetTimer = Timer()
        self.rotDelay = False

        self.devCmdQueue = CommandQueue({}) # all commands of equal priority

        self.lastGuideRotApplied = None

        self.doGuideRot = True

        TCPDevice.__init__(self,
            name = name,
            host = host,
            port = port,
            callFunc = callFunc,
            cmdInfo = (),
        )
예제 #18
0
파일: TwistedSocket.py 프로젝트: r-owen/RO
    def __init__(self,
        endpoint = None,
        protocol = None,
        state = BaseSocket.Connected,
        readCallback = nullCallback,
        stateCallback = nullCallback,
        timeLim = None,
        name = "",
        lineTerminator = "\r\n",
    ):
        """Construct a Socket

        Inputs:
        - endpoint  a Twisted endpoint, e.g. twisted.internet.endpoints.TCP4ClientEndpoint;
        - protocol  a Twisted protocol;
            you must either specify endpoint or protocol, but not both
        - state     the initial state
        - readCallback  function to call when data read; receives: self
        - stateCallback a state callback function; see addStateCallback for details
        - timeLim   time limit to make connection (sec); no limit if None or 0
        - name      a string to identify this socket; strictly optional
        - lineTerminator  specifies the terminator used in writeLine
        """
        #print "Socket(name=%r, endpoint=%r, protocol=%r, state=%r, readCallback=%r, stateCallback=%r)" % \
        #    (name, endpoint, protocol, state, readCallback, stateCallback)
        if bool(endpoint is None) == bool(protocol is None):
            raise RuntimeError("Must provide one of endpoint or protocol")
        self._endpoint = endpoint
        self._endpointDeferred = None
        self._protocol = None
        self._data = None
        self._connectTimer = Timer()
        BaseSocket.__init__(self,
            state = state,
            readCallback = readCallback,
            stateCallback = stateCallback,
            name = name,
            lineTerminator = lineTerminator
        )
        if protocol is not None:
            self._connectionMade(protocol)
        else:
            if timeLim:
                self._connectTimer.start(timeLim, self._connectTimeout)
            self._setState(BaseSocket.Connecting)
            self._endpointDeferred = self._endpoint.connect(_SocketProtocolFactory())
            setCallbacks(
                deferred = self._endpointDeferred,
                callback = self._connectionMade,
                errback = self._connectionLost,
            )
예제 #19
0
    def __init__(self, name, port):
        """!Construct a fake device controller

        @param[in] name  name of device controller
        @param[in] port  port on which to command device controller
        """
        self.replyTimer = Timer()
        TCPServer.__init__(
            self,
            port=port,
            stateCallback=self.stateCallback,
            sockReadCallback=self.sockReadCallback,
            sockStateCallback=self.sockStateCallback,
        )
예제 #20
0
    def startDisconnect(self):
        """!Start disconnecting the connection
        """
        if self.dev.conn.isDone and not self.dev.conn.isConnected:
            # fully disconnected; no more to be done
            Timer(0, self.finish)
        else:
            if self._timeLim:
                # start timer for disconnection
                self._connTimer.start(self._timeLim, self.finish,
                                      "timed out waiting for disconnection")

            self.dev.conn.addStateCallback(self.connCallback)
            self._addedConnCallback = True
            self.dev.conn.disconnect()
 def __init__(self, priorityDict, killFunc=None):
     """ This is an object which keeps track of commands and smartly handles
         command collisions based on rules chosen by you.
         @param[in] priorityDict a dictionary keyed by cmdVerb, with integer values or Immediate
         @ param[in] killFunc: a function to call when a running command needs to be
             killed.  Accepts 2 parameters, the command to be canceled, and the command doing the killing.
             This function must eventually ensure that the running command is canceled safely
             allowing for the next queued command to go. Or None
     """
     self.cmdQueue = []
     dumCmd = UserCmd()
     dumCmd.setState(dumCmd.Done)
     dumCmd.cmdVerb = 'dummy'
     self.currExeCmd = QueuedCommand(dumCmd, 0, lambda cmdVar: None)
     self.priorityDict = priorityDict
     self.killFunc = killFunc
     self.ruleDict = {}
     self.queueTimer = Timer()
     self._enabled = True
    def __init__(self,
        filterWheelDev,
        shutterDev,
        name="arcticICC",
        userPort = UserPort,
        test=False,
    ):
        """!Construct an ArcticActor

        @param[in] filterWheelDev  a FilterWheelDevice instance
        @param[in] shutterDev  a ShutterDevice instance
        @param[in] name  actor name; used for logging
        @param[in] userPort port on which this service runs
        @param[in] test bool. If true, use a fake camera.
        """
        self.imageDir = ImageDir
        self.test = test
        self.setCamera()
        self.filterWheelDev = filterWheelDev
        self.shutterDev = shutterDev
        self._tempSetpoint = None
        self.expNum = 0
        self.exposeCmd = UserCmd()
        self.exposeCmd.setState(UserCmd.Done)
        self.pollTimer = Timer()
        self.expName = None
        self.comment = None
        self.expStartTime = None
        self.expType = None
        self.expTime = None
        self.resetConfig = None
        Actor.__init__(self,
            userPort = userPort,
            maxUsers = 1,
            devs = (filterWheelDev, shutterDev),
            name = name,
            version = __version__,
            doConnect = True,
            doDevNameCmds = False,
            doDebugMsgs = self.test,
            )
예제 #23
0
    def finish(self, reason=None):
        """!Call on success or failure to finish the command

        @param[in] reason: reason for failure (if non-empty then failure is assumed)
        """
        self._connTimer.cancel()
        self.dev._ignoreConnCallback = False
        if self._addedConnCallback:
            self.dev.conn.removeStateCallback(self.connCallback)
        if reason or not self.dev.conn.isConnected:
            reason = reason or "unknown reason"
            self.dev.setState(self.dev.Failed, reason)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Failed,
                                      textMsg="%s failed to connect: %s" %
                                      (self.dev, reason))
            Timer(0, self.dev.conn.disconnect)
        else:
            self.dev.setState(self.dev.Connected)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)
예제 #24
0
파일: m2Device.py 프로젝트: csayres/lcoTCC
    def __init__(self, name, host, port, callFunc=None):
        """!Construct a LCODevice

        Inputs:
        @param[in] name  name of device
        @param[in] host  host address of m2 controller
        @param[in] port  port of m2 controller
        @param[in] callFunc  function to call when state of device changes;
                note that it is NOT called when the connection state changes;
                register a callback with "conn" for that task.
        """
        self.status = Status()
        self._statusTimer = Timer()
        self.waitMoveCmd = UserCmd()
        self.waitMoveCmd.setState(self.waitMoveCmd.Done)
        # self.waitGalilCmd = UserCmd()
        # self.waitGalilCmd.setState(self.waitGalilCmd.Done)
        # give status commands equal priority so they don't kill eachother
        priorityDict = {
            "status": 1,
            "speed": 1,
            "stop": 1,
            "move": 1,
            "galil": 1,
            "offset": 1,
        }

        self.devCmdQueue = CommandQueue(priorityDict) # all commands of equal priority

        TCPDevice.__init__(self,
            name = name,
            host = host,
            port = port,
            callFunc = callFunc,
            cmdInfo = (),
        )
예제 #25
0
    def __init__(self,
        userPort,
        tcsDev,
        scaleDev,
        m2Dev,
        measScaleDev,
        name = "tcc",
    ):
        """Construct a TCCActor

        @param[in] userPort  port on which to listen for users
        @param[in] tcsDev a TCSDevice instance
        @param[in] scaleDev  a ScaleDevice instance
        @param[in] m2Dev a M2Device instance
        @param[in] m2Dev a MeasScaleDevice instance
        @param[in] name  actor name; used for logging
        """
        self.tcsDev = tcsDev
        self.tcsDev.writeToUsers = self.writeToUsers
        self.scaleDev = scaleDev
        self.scaleDev.writeToUsers = self.writeToUsers
        self.secDev = m2Dev
        self.secDev.writeToUsers = self.writeToUsers
        self.measScaleDev = measScaleDev
        self.measScaleDev.writeToUsers = self.writeToUsers
        # auto connection looks for self.dev
        self.dev = DeviceCollection([self.tcsDev, self.scaleDev, self.secDev, self.measScaleDev])
        # connect devices
        self.tcsDev.connect()
        self.scaleDev.connect()
        self.secDev.connect()
        self.measScaleDev.connect()
        self.cmdParser = TCCLCOCmdParser()
        self.collimationModel = CollimationModel()
        self.collimateTimer = Timer(0, self.updateCollimation)
        BaseActor.__init__(self, userPort=userPort, maxUsers=1, name=name, version=__version__)
class ArcticActor(Actor):
    Facility = syslog.LOG_LOCAL1
    DefaultTimeLim = 5 # default time limit, in seconds
    def __init__(self,
        filterWheelDev,
        shutterDev,
        name="arcticICC",
        userPort = UserPort,
        test=False,
    ):
        """!Construct an ArcticActor

        @param[in] filterWheelDev  a FilterWheelDevice instance
        @param[in] shutterDev  a ShutterDevice instance
        @param[in] name  actor name; used for logging
        @param[in] userPort port on which this service runs
        @param[in] test bool. If true, use a fake camera.
        """
        self.imageDir = ImageDir
        self.test = test
        self.setCamera()
        self.filterWheelDev = filterWheelDev
        self.shutterDev = shutterDev
        self._tempSetpoint = None
        self.expNum = 0
        self.exposeCmd = UserCmd()
        self.exposeCmd.setState(UserCmd.Done)
        self.pollTimer = Timer()
        self.expName = None
        self.comment = None
        self.expStartTime = None
        self.expType = None
        self.expTime = None
        self.resetConfig = None
        Actor.__init__(self,
            userPort = userPort,
            maxUsers = 1,
            devs = (filterWheelDev, shutterDev),
            name = name,
            version = __version__,
            doConnect = True,
            doDevNameCmds = False,
            doDebugMsgs = self.test,
            )

    def init(self, userCmd=None, getStatus=True, timeLim=DefaultTimeLim):
        """! Initialize all devices, and get status if wanted
        @param[in]  userCmd  a UserCmd or None
        @param[in]  getStatus if true, query all devices for status
        @param[in]  timeLim
        """
        userCmd = expandUserCmd(userCmd)
        log.info("%s.init(userCmd=%s, timeLim=%s, getStatus=%s)" % (self, userCmd, timeLim, getStatus))
        # initialize camera
        self.setCamera()
        subCmdList = []
        # connect (if not connected) and initialize devices
        filterDevCmd = expandUserCmd(None)
        shutterDevCmd = expandUserCmd(None)
        subCmdList.append(filterDevCmd)
        subCmdList.append(shutterDevCmd)
        if not self.filterWheelDev.isConnected:
            self.filterWheelDev.connect(userCmd=filterDevCmd)
        else:
            self.filterWheelDev.init(userCmd=filterDevCmd)

        if not self.shutterDev.isConnected:
            self.shutterDev.connect(userCmd=shutterDevCmd)
        else:
            self.shutterDev.init(userCmd=shutterDevCmd)
        if getStatus:
            # get status only when the conn/initialization is done
            def getStatus(foo):
                if userCmd.isDone:
                    self.getStatus()
            userCmd.addCallback(getStatus)
        LinkCommands(mainCmd=userCmd, subCmdList=subCmdList)
        if not self.exposeCmd.isDone:
            self.exposeCmd.setState(self.exposeCmd.Failed, "currently running exposure killed via init")
            self.exposeCleanup()
        return userCmd

    def setCamera(self):
        self.camera = None
        if self.test:
            # use fake camera
            self.camera = FakeCamera()
        else:
            self.camera = arctic.Camera()

    @property
    def tempSetpoint(self):
        return self._tempSetpoint

    @property
    def exposureStateKW(self):
        arcticExpState = self.camera.getExposureState()
        arcticStatusInt = arcticExpState.state
        #translate from arctic status string to output that hub/tui expect
        if arcticStatusInt == arctic.Idle:
            expStateStr = "done"
        elif arcticStatusInt == arctic.Exposing:
            expStateStr = "integrating"
        elif arcticStatusInt == arctic.Paused:
            expStateStr = "paused"
        elif arcticStatusInt in [arctic.Reading, arctic.ImageRead]:
            expStateStr = "reading"
        if arcticStatusInt == arctic.Reading:
            # use modeled exposure time
            fullTime = self.getReadTime()
        else:
            fullTime = arcticExpState.fullTime
        return "exposureState=%s, %.4f"%(expStateStr, fullTime)

    def getReadTime(self):
        """Determine the read time for the current camera configuration
        """
        config = self.camera.getConfig()
        width = int(config.getBinnedWidth())
        height = int(config.getBinnedHeight())
        totalPix = width*height
        readRate = ReadoutRateEnumNameDict[config.readoutRate]
        readAmps = ReadoutAmpsEnumNameDict[config.readoutAmps]
        if readAmps != Quad:
            readAmps = Single
        return dcOff[readAmps] + rowMult[readAmps] * height + pixMult[readAmps][readRate] * totalPix

    # def setTemp(self, tempSetpoint):
    #     """Set the temperature setpoint
    #     @param[in] tempSetpoint: float, the desired temperature setpoint
    #     """
    #     self._tempSetpoint = float(tempSetpoint)


    def parseAndDispatchCmd(self, cmd):
        """Dispatch the user command, parse the cmd string and append the result as a parsedCommand attribute
        on cmd

        @param[in] cmd  user command (a twistedActor.UserCmd)
        """
        log.info("%s.parseAndDispatchCmd cmdBody=%r"%(self, cmd.cmdBody))
        if not cmd.cmdBody:
            # echo to show alive
            self.writeToOneUser(":", "", cmd=cmd)
            return

        parsedCommand = arcticCommandSet.parse(cmd.cmdBody)

        # append the parsedCommand to the cmd object, and send along
        cmd.parsedCommand = parsedCommand
        return Actor.parseAndDispatchCmd(self, cmd)

    def cmd_camera(self, userCmd):
        """! Implement the camera command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        arg = userCmd.parsedCommand.parsedPositionalArgs[0]
        if arg == "status":
            self.getStatus(userCmd=userCmd, doCamera=True, doFilter=False, doShutter=False)
        else:
            # how to init the camera, just rebuild it?
            # assert arg == "initialize"
            self.setCamera()
            userCmd.setState(userCmd.Done)
        return True

    def cmd_filter(self, userCmd):
        """! Implement the filter command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        subCmd = userCmd.parsedCommand.subCommand
        userCmd = expandUserCmd(userCmd)
        if subCmd.cmdName == "initialize":
            if not self.filterWheelDev.isConnected:
                self.filterWheelDev.connect(userCmd)
            else:
                self.filterWheelDev.init(userCmd)
        elif subCmd.cmdName == "connect":
            self.filterWheelDev.connect(userCmd)
        elif subCmd.cmdName == "disconnect":
            self.filterWheelDev.disconnect(userCmd)
        elif subCmd.cmdName == "status":
            self.filterWheelDev.startCmd("status", userCmd=userCmd)
        elif subCmd.cmdName == "home":
            self.filterWheelDev.startCmd("home", userCmd=userCmd)
        elif subCmd.cmdName == "talk":
            talkTxt = subCmd.parsedPositionalArgs[0]
            self.filterWheelDev.startCmd(talkTxt, userCmd=userCmd)
        else:
            userCmd.setState(userCmd.Failed, "unhandled sub command: %s. bug"%(subCmd.cmdName,))
        return True

    def cmd_init(self, userCmd):
        """! Implement the init command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        self.init(userCmd, getStatus=True)
        # userCmd.setState(userCmd.Done)
        return True

    def cmd_ping(self, userCmd):
        """! Implement the ping command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        userCmd.setState(userCmd.Done, textMsg="alive")
        return True

    def cmd_expose(self, userCmd):
        """! Implement the expose command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        subCmd = userCmd.parsedCommand.subCommand
        if subCmd.cmdName not in ["pause", "resume", "stop", "abort"]:
            # no current exposure, start one
            basename = subCmd.parsedFloatingArgs.get("basename", [None])[0]
            comment = subCmd.parsedFloatingArgs.get("comment", [None])[0]
            ccdBin = subCmd.parsedFloatingArgs.get("bin", None)
            window = subCmd.parsedFloatingArgs.get("window", None)
            if ccdBin is not None or window is not None:
                # if bin or window is present, use single readout mode
                # and set evertying back once done
                config = self.camera.getConfig()
                prevBin = [config.binFacCol, config.binFacRow]
                prevWindow = self.getBinnedCCDWindow(config)
                prevAmps = ReadoutAmpsEnumNameDict[config.readoutAmps]
                prevRead = ReadoutRateEnumNameDict[config.readoutRate]
                print("prev config", prevBin, prevWindow, prevAmps)
                def setConfigBack():
                    self.setCameraConfig(ccdBin=prevBin, amps=[prevAmps], window=prevWindow)
                self.resetConfig = setConfigBack
                self.setCameraConfig(ccdBin=ccdBin, window=window, amps=[LL], readoutRate=[Fast])
            if subCmd.cmdName == "Bias":
                expTime = 0
            else:
                expTime = subCmd.parsedFloatingArgs["time"][0]
            self.doExpose(userCmd, expType=subCmd.cmdName, expTime=expTime, basename=basename, comment=comment)
            return True
        # there is a current exposure
        if subCmd.cmdName == "pause":
            self.camera.pauseExposure()
        elif subCmd.cmdName == "resume":
            self.camera.resumeExposure()
        elif subCmd.cmdName == "stop":
            self.camera.stopExposure()
        else:
            assert subCmd.cmdName == "abort"
            self.camera.abortExposure()
            # explicilty write abort exposure state
            self.writeToUsers("i", "exposureState=aborted,0")
            self.exposeCleanup()
        self.writeToUsers("i", self.exposureStateKW, userCmd)
        userCmd.setState(userCmd.Done)
        return True

    def doExpose(self, userCmd, expType, expTime, basename=None, comment=None):
        """!Begin a camera exposure

        @param[in] userCmd: a twistedActor UserCmd instance
        @param[in] expType: string, one of object, flat, dark, bias
        @param[in] expTime: float, exposure time.
        """

        if not self.exposeCmd.isDone:
            userCmd.setState(userCmd.Failed, "cannot start new exposure, self.exposeCmd not done")
            return
        if self.pollTimer.isActive:
            userCmd.setState(userCmd.Failed, "cannot start new exposure, self.pollTimer is active - bug")
            return
        self.exposeCmd = userCmd
        expTypeEnum = ExpTypeDict.get(expType)
        if basename:
            # save location was specified
            expName = basename
        else:
            # wasn't specified choose a default place/name
            if not os.path.exists(self.imageDir):
                os.makedirs(self.imageDir)
            expName = os.path.abspath("%s_%d.fits" % (expType, self.expNum))
            expName = "%s_%d.fits" % (expType, self.expNum)
            expName = os.path.join(self.imageDir, expName)
        # print "startExposure(%r, %r, %r)" % (expTime, expTypeEnum, expName)
        self.expStartTime = datetime.datetime.now()
        log.info("startExposure(%r, %r, %r)" % (expTime, expTypeEnum, expName))
        self.expName = expName
        self.comment = comment
        self.expType = expType
        self.expTime = expTime
        self.readingFlag = False
        try:
            self.camera.startExposure(expTime, expTypeEnum, expName)
            self.writeToUsers("i", self.exposureStateKW, self.exposeCmd)
            if expType.lower() in ["object", "flat"]:
                self.writeToUsers("i", "shutter=open") # fake shutter
            self.expNum += 1
            self.pollCamera()
        except RuntimeError as e:
            self.exposeCmd.setState(self.exposeCmd.Failed, str(e))
            # note, check what state the exposure kw is in after a runtime error here?
            self.exposeCleanup()

    def pollCamera(self):
        """Begin continuously polling the camera for exposure status, write the image when ready.
        """
        expState = self.camera.getExposureState()
        if expState.state == arctic.Reading and not self.readingFlag:
            self.readingFlag = True
            self.writeToUsers("i", "shutter=closed") # fake shutter
            # self.startReadTime = time.time()
            self.writeToUsers("i", self.exposureStateKW, self.exposeCmd)
        if expState.state == arctic.ImageRead:
            log.info("saving image: exposure %s"%self.expName)
            self.camera.saveImage() # saveImage sets camera exp state to idle
            # write headers
            self.writeHeaders()
            # clean up
            log.info("exposure %s complete"%self.expName)
            self.exposeCleanup()
        elif expState.state == arctic.Idle:
            log.warn("pollCamera() called but exposure state is idle.  Should be harmless, but why did it happen?")
            self.exposeCleanup()
        else:
            # if the camera is not idle continue polling
            self.pollTimer.start(0.05, self.pollCamera)

    def writeHeaders(self):
        config = self.camera.getConfig()
        # note comment header is written by hub, so we don't
        # do it here
        # http://astropy.readthedocs.org/en/latest/io/fits/
        with fits.open(self.expName, mode='update') as hdulist:
            prihdr = hdulist[0].header
            # timestamp
            prihdr["date-obs"] = self.expStartTime.isoformat(), "TAI time at the start of the exposure"
            # filter info
            try:
                filterPos = int(self.filterWheelDev.filterPos)
            except:
                filterPos = "unknown"
            prihdr["filpos"] = filterPos, "filter position"
            prihdr["filter"] = self.filterWheelDev.filterName, "filter name"
            # explicitly add in BINX BINY BEGX BEGY for WCS computation made by hub
            prihdr["begx"] = config.winStartCol + 1, "beginning column of CCD window"
            prihdr["begy"] = config.winStartRow + 1, "beginning row of CCD window"
            prihdr["binx"] = config.binFacCol, "column bin factor"
            prihdr["biny"] = config.binFacRow, "row bin factor"
            prihdr["ccdbin1"] = config.binFacCol, "column bin factor" #duplicate of binx
            prihdr["ccdbin2"] = config.binFacRow, "row bin factor" #duplicate of biny
            prihdr["imagetyp"] = self.expType, "exposure type"
            expTimeComment = "exposure time (sec)"
            if self.expTime > 0:
                expTimeComment = "estimated " + expTimeComment
            prihdr["exptime"] = self.expTime, expTimeComment
            prihdr["readamps"] = ReadoutAmpsEnumNameDict[config.readoutAmps], "readout amplifier(s)"
            prihdr["readrate"] = ReadoutRateEnumNameDict[config.readoutRate], "readout rate"

            # DATASEC and BIASSEC
            # for the bias region: use all overscan except the first two columns (closest to the data)
            # amp names are <x><y> e.g. 11, 12, 21, 22
            prescanWidth = 3 if config.binFacCol == 3 else 2
            prescanHeight = 1 if config.binFacRow == 3 else 0
            if config.readoutAmps == arctic.Quad:
                # all 4 amps are being read out
                ampDataList = AmpDataMap.values()
                ampXYList = " ".join([ampData.xyName for ampData in ampDataList])
                prihdr["amplist"] = (ampXYList, "amplifiers read <x><y> e.g. 12=LR")
                overscanWidth  = config.getBinnedWidth()  - ((2 * prescanWidth) + config.winWidth) # total, not per amp
                overscanHeight = config.getBinnedHeight() - ((2 * prescanHeight) + config.winHeight) # total, not per amp
                for ampData in ampDataList:
                    # CSEC is the section of the CCD covered by the data (unbinned)
                    csecWidth  = config.winWidth  * config.binFacCol / 2
                    csecHeight = config.winHeight * config.binFacRow / 2
                    csecStartCol = 1 + csecWidth if ampData.isRightHalf else 1
                    csecStartRow = 1 + csecHeight if ampData.isTopHalf else 1
                    csecEndCol = csecStartCol + csecWidth  - 1
                    csecEndRow = csecStartRow + csecHeight - 1
                    csecKey = "csec" + ampData.xyName
                    csecValue = "[%i:%i,%i:%i]"%(csecStartCol, csecEndCol, csecStartRow, csecEndRow)
                    prihdr[csecKey] = csecValue, "data section of CCD (unbinned)"

                    # DSEC is the section of the image that is data (binned)
                    dsecStartCol = 1 + prescanWidth
                    if ampData.isRightHalf:
                        dsecStartCol = dsecStartCol + (config.winWidth / 2) + overscanWidth
                    dsecStartRow = 1 + prescanHeight
                    if ampData.isTopHalf:
                        dsecStartRow = dsecStartRow + (config.winHeight / 2) + overscanHeight
                    dsecEndCol = dsecStartCol + (config.winWidth / 2) - 1
                    dsecEndRow = dsecStartRow + (config.winHeight / 2) - 1
                    dsecKey = "dsec" + ampData.xyName
                    dsecValue = "[%i:%i,%i:%i]"%(dsecStartCol, dsecEndCol, dsecStartRow, dsecEndRow)
                    prihdr[dsecKey] = dsecValue, "data section of image (binned)"

                    biasWidth = (overscanWidth / 2) - 2 # "- 2" to skip first two columns of overscan
                    colBiasEnd = config.getBinnedWidth() / 2
                    if ampData.isRightHalf:
                        colBiasEnd += biasWidth
                    colBiasStart = 1 + colBiasEnd - biasWidth
                    bsecKey = "bsec" + ampData.xyName
                    bsecValue = "[%i:%i,%i:%i]"%(colBiasStart,colBiasEnd,dsecStartRow, dsecEndRow)
                    prihdr[bsecKey] = bsecValue, "bias section of image (binned)"
                    prihdr["gtgain"+ampData.xyName] = ampData.getGain(config.readoutRate), "predicted gain (e-/ADU)"
                    prihdr["gtron"+ampData.xyName] = ampData.getReadNoise(config.readoutRate), "predicted read noise (e-)"

            else:
                # single amplifier readout
                ampData = AmpDataMap[config.readoutAmps]
                prihdr["amplist"] = ampData.xyName, "amplifiers read <x><y> e.g. 12=LR"

                overscanWidth  = config.getBinnedWidth()  - (prescanWidth + config.winWidth)
                overscanHeight = config.getBinnedHeight() - (prescanHeight + config.winHeight)

                csecWidth  = config.winWidth  * config.binFacCol
                csecHeight = config.winHeight * config.binFacRow
                csecStartCol = 1 + (config.winStartCol * config.binFacCol)
                csecStartRow = 1 + (config.winStartRow * config.binFacRow)
                csecEndCol = csecStartCol + csecWidth  - 1
                csecEndRow = csecStartRow + csecHeight - 1
                csecKey = "csec" + ampData.xyName
                csecValue = "[%i:%i,%i:%i]"%(csecStartCol, csecEndCol, csecStartRow, csecEndRow)
                prihdr[csecKey] = csecValue, "data section of CCD (unbinned)" #?

                dsecStartCol = 1 + config.winStartCol + prescanWidth
                dsecStartRow = 1 + config.winStartRow + prescanHeight
                dsecEndCol = dsecStartCol + config.winWidth  - 1
                dsecEndRow = dsecStartRow + config.winHeight - 1
                dsecKey = "dsec" + ampData.xyName
                dsecValue = "[%i:%i,%i:%i]"%(dsecStartCol, dsecEndCol, dsecStartRow, dsecEndRow)
                prihdr[dsecKey] = dsecValue, "data section of image (binned)"

                biasWidth = overscanWidth - 2 # "- 2" to skip first two columns of overscan
                colBiasEnd = config.getBinnedWidth()
                colBiasStart = 1 + colBiasEnd - biasWidth
                bsecKey = "bsec" + ampData.xyName
                bsecValue = "[%i:%i,%i:%i]"%(colBiasStart, colBiasEnd, dsecStartRow, dsecEndRow)
                prihdr[bsecKey] = bsecValue, "bias section of image (binned)"
                prihdr["gtgain"+ampData.xyName] = ampData.getGain(config.readoutRate), "predicted gain (e-/ADU)"
                prihdr["gtron"+ampData.xyName] = ampData.getReadNoise(config.readoutRate), "predicted read noise (e-)"


    def exposeCleanup(self):
        self.pollTimer.cancel() # just in case
        self.writeToUsers("i", self.exposureStateKW, self.exposeCmd)
        self.writeToUsers("i", "shutter=closed") # fake shutter
        # if configuration requires setting back do it now
        if self.resetConfig is not None:
            print("resetting config")
            self.resetConfig()
            self.resetConfig = None
        if not self.exposeCmd.isDone:
            self.exposeCmd.setState(self.exposeCmd.Done)
        self.expName = None
        self.comment = None
        self.expStartTime = None
        self.expType = None
        self.expTime = None
        self.readingFlag = False

    def maxCoord(self, binFac=(1,1)):
        """Returns the maximum binned CCD coordinate, given a bin factor.
        """
        assert len(binFac) == 2, "binFac must have 2 elements; binFac = %r" % binFac
        # The value is even for both amplifiers, even if only using single readout,
        # just to keep the system more predictable. The result is that the full image size
        # is the same for 3x3 binning regardless of whether you read one amp or four,
        # and as a result you lose one row and one column when you read one amp.
        return [(4096, 4096)[ind] // int(2 * binFac[ind]) * 2 for ind in range(2)]

    def minCoord(self, binFac=(1,1)):
        """Returns the minimum binned CCD coordinate, given a bin factor.
        """
        assert len(binFac) == 2, "binFac must have 2 elements; binFac = %r" % binFac
        return (1, 1)

    def unbin(self, binnedCoords, binFac):
        """Copied from TUI

        Converts binned to unbinned coordinates.

        The output is constrained to be in range (but such constraint is only
        needed if the input was out of range).

        A binned coordinate can be be converted to multiple unbinned choices (if binFac > 1).
        The first two coords are converted to the smallest choice,
        the second two (if supplied) are converted to the largest choice.
        Thus 4 coordinates are treated as a window with LL, UR coordinates, inclusive.

        Inputs:
        - binnedCoords: 2 or 4 coords; see note above

        If any element of binnedCoords or binFac is None, all returned elements are None.
        """
        assert len(binnedCoords) in (2, 4), "binnedCoords must have 2 or 4 elements; binnedCoords = %r" % binnedCoords
        if len(binFac) == 1:
            binFac = binFac*2
        assert len(binFac) == 2, "binFac must have 2 elements; binFac = %r" % binFac

        if None in binnedCoords or None in binFac:
            return (None,)*len(binnedCoords)

        binXYXY = binFac * 2
        subadd = (1, 1, 0, 0)

        # compute value ignoring limits
        unbinnedCoords = [((binnedCoords[ind] - subadd[ind]) * binXYXY[ind]) + subadd[ind]
            for ind in range(len(binnedCoords))]

        # apply limits
        minUnbinXYXY = self.minCoord()*2
        maxUnbinXYXY = self.maxCoord()*2
        unbinnedCoords = [min(max(unbinnedCoords[ind], minUnbinXYXY[ind]), maxUnbinXYXY[ind])
            for ind in range(len(unbinnedCoords))]
        return unbinnedCoords

    def bin(self, unbinnedCoords, binFac):
        """Copied from TUI

        Converts unbinned to binned coordinates.

        The output is constrained to be in range for the given bin factor
        (if a dimension does not divide evenly by the bin factor
        then some valid unbinned coords are out of range when binned).

        Inputs:
        - unbinnedCoords: 2 or 4 coords

        If any element of binnedCoords or binFac is None, all returned elements are None.
        """
        assert len(unbinnedCoords) in (2, 4), "unbinnedCoords must have 2 or 4 elements; unbinnedCoords = %r" % unbinnedCoords
        if len(binFac) == 1:
            binFac = binFac*2
        assert len(binFac) == 2, "binFac must have 2 elements; binFac = %r" % binFac

        if None in unbinnedCoords or None in binFac:
            return (None,)*len(unbinnedCoords)

        binXYXY = binFac * 2

        # compute value ignoring limits
        binnedCoords = [1 + ((unbinnedCoords[ind] - 1) // int(binXYXY[ind]))
            for ind in range(len(unbinnedCoords))]

        # apply limits
        minBinXYXY = self.minCoord(binFac)*2
        maxBinXYXY = self.maxCoord(binFac)*2
        binnedCoords = [min(max(binnedCoords[ind], minBinXYXY[ind]), maxBinXYXY[ind])
            for ind in range(len(binnedCoords))]
        return binnedCoords

    def getCurrentBinnedCCDWindow(self, config):
        wind0 = config.winStartCol + 1
        wind1 = config.winStartRow + 1
        # wind2 = config.winWidth + wind0
        # wind3 = config.winHeight + wind1
        wind2 = config.winWidth + config.winStartCol
        wind3 = config.winHeight + config.winStartRow
        return [wind0, wind1, wind2, wind3]

    def getUnbinnedCCDWindow(self, config):
        window = self.getCurrentBinnedCCDWindow(config)
        return self.unbin(window, [config.binFacCol, config.binFacRow])

    def getBinnedCCDWindow(self, config, newBin=None):
        """Return a binned CCD window.  If newBin specified return window with
        new bin factor, otherwise use the current bin factor.
        """
        if newBin is None:
            return self.getCurrentBinnedCCDWindow(config)
        else:
            # determine new window size from new bin factor
            unbinnedWindow = self.getUnbinnedCCDWindow(config)
            print("max coord", self.maxCoord(binFac=newBin))
            return self.bin(unbinnedWindow, newBin)

    def setCCDWindow(self, config, window):
        """Window is a set of 4 integers, or a list of 1 element: ["full"]
        """
        if str(window[0]).lower() == "full":
            config.setFullWindow()
        else:
            try:
                window = [int(x) for x in window]
                assert len(window)==4
                # explicitly handle the off by 1 issue with 3x3 binning
                # note this is also handled in self.getBinnedCCDWindow
                # if now window was passed via the command string
                # if config.binFacCol == 3:
                #     window[2] = window[2] - 1
                # if config.binFacRow == 3:
                #     window[3] = window[3] - 1
                print("windowExplicit", window)
            except:
                raise ParseError("window must be 'full' or a list of 4 integers")
            config.winStartCol = window[0]-1 # leach is 0 indexed
            config.winStartRow = window[1]-1
            config.winWidth = window[2] - config.winStartCol # window width includes start and end row!
            config.winHeight = window[3] - config.winStartRow

    def setCameraConfig(self, ccdBin=None, amps=None, window=None, readoutRate=None):
        """! Set parameters on the camera config object

        ccdBin = None, or a list of 1 or 2 integers
        amps = None, or 1 element list [LL], [UL], [UR], [LR], or [Quad]
        window = None, ["full"], or [int, int, int, int]
        readoutRate = None, or 1 element list: [Fast], [Medium], or [Slow]
        """
        # begin replacing/and checking config values
        config = self.camera.getConfig()

        if readoutRate is not None:
            config.readoutRate = ReadoutRateNameEnumDict[readoutRate[0]]
        if ccdBin is not None:
            newColBin = ccdBin[0]
            newRowBin = newColBin if len(ccdBin) == 1 else ccdBin[1]
            if window is None:
                # calculate the new window with the new bin factors
                window = self.getBinnedCCDWindow(config, newBin=[newColBin, newRowBin])
            # set new bin factors
            config.binFacCol = newColBin
            config.binFacRow = newRowBin
        # windowing and amps need some careful handling...
        if window is not None:
            self.setCCDWindow(config, window)
            # if amps were not specified be sure this window works
            # with the current amp configuration, else yell
            # force the amps check
            if amps is None:
                amps = [ReadoutAmpsEnumNameDict[config.readoutAmps]]
        if amps is not None:
            # quad amp only valid for full window
            amps = amps[0]
            isFullWindow = config.isFullWindow()
            if not isFullWindow and amps==Quad:
                raise ParseError("amps=quad may only be specified with a full window")
            if isFullWindow and amps==Auto:
                config.readoutAmps = ReadoutAmpsNameEnumDict[Quad]
            elif not isFullWindow and amps==Auto:
                config.readoutAmps = ReadoutAmpsNameEnumDict[LL]
            else:
                config.readoutAmps = ReadoutAmpsNameEnumDict[amps]

        # set camera configuration if a configuration change was requested
        if True in [x is not None for x in [readoutRate, ccdBin, window, amps]]:
            # camera config was changed set it and output new camera status
            self.camera.setConfig(config)
            self.getStatus(doCamera=True, doFilter=False, doShutter=False)

    def cmd_set(self, userCmd):
        """! Implement the set command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        argDict = userCmd.parsedCommand.parsedFloatingArgs
        if not argDict:
            # nothing was passed?
            raise ParseError("no arguments received for set command")
        ccdBin = argDict.get("bin", None)
        amps = argDict.get("amps", None)
        window = argDict.get("window", None)
        readoutRate = argDict.get("readoutRate", None)
        temp = argDict.get("temp", None)
        filterPos = argDict.get("filter", None)

        self.setCameraConfig(ccdBin=ccdBin, amps=amps, window=window, readoutRate=readoutRate)

        if temp is not None:
            self.setTemp(argDict["temp"][0])
        # move wants an int, maybe some translation should happend here
        # or some mapping between integers and filter names
        if filterPos is not None:
            # only set command done if move finishes successfully
            def setDoneAfterMove(mvCmd):
                if mvCmd.isDone:
                    # did the move fail? if so fail the userCmd and ask for status
                    if mvCmd.didFail:
                        userCmd.setState(userCmd.Failed, "Filter move failed: %s"%mvCmd.textMsg)
                    else:
                        userCmd.setState(userCmd.Done)
            pos = int(filterPos[0])
            # output commanded position keywords here (move to filterWheelActor eventually?)
            self.filterWheelDev.startCmd("move %i"%(pos,), callFunc=setDoneAfterMove) # userCmd set done in callback after status
        else:
            # done: output the new configuration
            userCmd.setState(userCmd.Done)
        return True


    def cmd_status(self, userCmd):
        """! Implement the status command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        # statusStr = self.getCameraStatus()
        # self.writeToUsers("i", statusStr, cmd=userCmd)
        self.getStatus(userCmd)
        return True

    def getCameraStatus(self):
        """! Return a formatted string of current camera
        status
        """
        config = self.camera.getConfig()
        keyVals = []
        # exposure state
        keyVals.append(self.exposureStateKW)
        keyVals.append("ccdState=ok") # a potential lie?
        keyVals.append("ccdSize=%i,%i"%(arctic.CCDWidth, arctic.CCDHeight))

        # bin
        ccdBin = (config.binFacCol, config.binFacRow)
        keyVals.append("ccdBin=%i,%i"%(ccdBin))
        # window
        keyVals.append("shutter=%s"%("open" if self.camera.getExposureState().state == arctic.Exposing else "closed"))
        # ccdWindow = (
        #     config.winStartCol + 1, # add one to adhere to tui's convention
        #     config.winStartRow + 1,
        #     config.winStartCol + config.winWidth,
        #     config.winStartRow + config.winHeight,
        # )
        ccdWindow = tuple(self.getBinnedCCDWindow(config))
        ccdUBWindow = tuple(self.unbin(ccdWindow, ccdBin))

        keyVals.append("ccdWindow=%i,%i,%i,%i"%(ccdWindow))
        keyVals.append("ccdUBWindow=%i,%i,%i,%i"%(ccdUBWindow))
        keyVals.append("ccdOverscan=%i,0"%arctic.XOverscan)
        keyVals.append("ampNames=%s,%s"%(LL,Quad))
        keyVals.append("ampName="+ReadoutAmpsEnumNameDict[config.readoutAmps])
        keyVals.append("readoutRateNames="+", ".join([x for x in ReadoutRateEnumNameDict.values()]))
        keyVals.append("readoutRateName=%s"%ReadoutRateEnumNameDict[config.readoutRate])
        # add these when they become available?
        # keyVals.append("ccdTemp=?")
        # if self.tempSetpoint is None:
        #     ts = "None"
        # else:
        #     ts = "%.2f"%self.tempSetpoint
        # keyVals.append("tempSetpoint=%s"%ts)
        return "; ".join(keyVals)

    def getStatus(self, userCmd=None, doCamera=True, doFilter=True, doShutter=True):
        """! A generic status command, arguments specify which statuses are wanted
        @param[in] userCmd a twistedActor UserCmd or none
        @param[in] doCamera: bool, if true get camera status
        @param[in] doFilter: bool, if true get filter wheel status
        @param[in] doShutter: bool, if true get shutter status
        """
        assert True in [doFilter, doShutter, doCamera]
        userCmd = expandUserCmd(userCmd)
        if doCamera:
            statusStr = self.getCameraStatus()
            self.writeToUsers("i", statusStr, cmd=userCmd)
        subCmdList = []
        if doShutter:
            subCmdList.append(self.shutterDev.getStatus())
        if doFilter:
            subCmdList.append(self.filterWheelDev.startCmd("status"))
        if subCmdList:
            LinkCommands(userCmd, subCmdList)
        else:
            userCmd.setState(userCmd.Done)
        return userCmd
예제 #27
0
class FilterWheel(object):
    def __init__(self):
        self.moveTimer = Timer()

    def stopMove(self):
        self.moveTimer.cancel()
        StatusDict["motor"] = 0
        # StatusDict["motor"]=8

    def connect(self):
        """
        initiate filter wheel communication
        """
        pass

    def home(self):
        """
        home filter wheel and set the filter wheel id
        """
        # emulate the block
        time.sleep(3)
        # StatusDict["id"] = 1
        return StatusDict["id"]

    def stop(self):
        """
        stop filter wheel motion
        """
        return self.stopMove()

    def moveToPosition(self, pos):
        """
        move to a specific filter wheel position
        arguments:
            int pos - filter position between 1 and 6
        status:
            0 - fail
            1 - succeed
            -1 - unknown
        """
        StatusDict["position"] = int(pos)
        # StatusDict["motor"] = 1
        StatusDict["motor"] = 3
        self.moveTimer.start(MoveTime, self.stopMove)
        return self.status()

    def _setDiffuPos(self, val):
        StatusDict["diffuInBeam"] = val

    def startDiffuRot(self):
        StatusDict["diffuRotating"] = 1

    def stopDiffuRot(self):
        StatusDict["diffuRotating"] = 0

    def diffuIn(self):
        Timer(0.2, self._setDiffuPos, 1)  # set on a timer to emulate move time

    def diffuOut(self):
        Timer(0.2, self._setDiffuPos, 0)  # set on a timer to emulate move time

    def status(self):
        """
        return current status of filter wheel
        """
        return StatusDict
예제 #28
0
class BaseCmd(RO.AddCallback.BaseMixin):
    """Base class for commands of all types (user and device).
    """
    # state constants
    Done = "done"
    Cancelled = "cancelled"  # including superseded
    Failed = "failed"
    Ready = "ready"
    Running = "running"
    Cancelling = "cancelling"
    Failing = "failing"
    ActiveStates = frozenset((Running, Cancelling, Failing))
    FailedStates = frozenset((Cancelled, Failed))
    FailingStates = frozenset((Cancelling, Failing))
    DoneStates = frozenset((Done, )) | FailedStates
    AllStates = frozenset((Ready, )) | ActiveStates | DoneStates
    _MsgCodeDict = dict(
        ready="i",
        running="i",
        cancelling="w",
        failing="w",
        cancelled="f",
        failed="f",
        debug="d",
        done=":",
    )
    _InvMsgCodeDict = dict((val, key) for key, val in _MsgCodeDict.iteritems())

    def __init__(
        self,
        cmdStr,
        userID=0,
        cmdID=0,
        callFunc=None,
        timeLim=None,
    ):
        """Construct a BaseCmd

        @param[in] cmdStr  command string
        @param[in] userID  user ID number
        @param[in] cmdID  command ID number
        @param[in] callFunc  function to call when command changes state;
            receives one argument: this command
        @param[in] timeLim  time limit for command (sec); if None or 0 then no time limit
        """
        self._cmdStr = cmdStr
        self.userID = int(userID)
        self.cmdID = int(cmdID)
        self._state = self.Ready
        self._textMsg = ""
        self._hubMsg = ""
        self._cmdToTrack = None
        self._linkedCommands = []
        self._parentCmd = None
        self._writeToUsers = None  # set by baseActor.ExpandCommand
        # set by baseActor.newCmd to flag this as a command created
        # from socket input
        self.userCommanded = False
        self._timeoutTimer = Timer()
        self.setTimeLimit(timeLim)

        RO.AddCallback.BaseMixin.__init__(self, callFunc)

    @property
    def parentCmd(self):
        return self._parentCmd

    @property
    def eldestParentCmd(self):
        if self.parentCmd is None:
            return self
        else:
            return self.parentCmd.eldestParentCmd

    @property
    def timeLim(self):
        return self._timeLim

    @property
    def cmdStr(self):
        return self._cmdStr

    @property
    def didFail(self):
        """Command failed or was cancelled
        """
        return self._state in self.FailedStates

    @property
    def isActive(self):
        """Command is running, canceling or failing
        """
        return self._state in self.ActiveStates

    @property
    def isDone(self):
        """Command is done (whether successfully or not)
        """
        return self._state in self.DoneStates

    @property
    def isFailing(self):
        """Command is being cancelled or is failing
        """
        return self._state in self.FailingStates

    @property
    def msgCode(self):
        """The hub message code appropriate to the current state
        """
        return self._MsgCodeDict[self._state]

    @property
    def hubMsg(self):
        """The hub message or "" if none
        """
        return self._hubMsg

    @property
    def textMsg(self):
        """The text message or "" if none
        """
        return self._textMsg

    @property
    def state(self):
        """The state of the command, as a string which is one of the state constants, e.g. self.Done
        """
        return self._state

    def setWriteToUsers(self, writeToUsersFunc):
        if self._writeToUsers is not None:
            raise RuntimeError("Write to users is already set")
        else:
            self._writeToUsers = writeToUsersFunc

    def writeToUsers(self, msgCode, msgStr, userID=None, cmdID=None):
        # see doc in baseActor.
        # self._writeToUsers is set in BaseActor.newCmd()
        # or is is set at the time of any command linkage
        # get the top most command for writing to users
        # it will be the original cmd
        topCmd = self.eldestParentCmd
        if topCmd._writeToUsers is None:
            print("%s writeToUsers not set: " % str(self), msgCode, msgStr,
                  topCmd, userID, cmdID, "!!!")
        else:
            topCmd._writeToUsers(msgCode, msgStr, topCmd, userID, cmdID)

    def addCallback(self, callFunc, callNow=False):
        """Add a callback function

        @param[in] callFunc  callback function:
        - it receives one argument: this command
        - it is called whenever the state changes, and immediately if the command is already done
            or callNow is True
        @param[in] callNow  if True, call callFunc immediately
        """
        if self.isDone:
            RO.AddCallback.safeCall2("%s.addCallback callFunc =" % (self, ),
                                     callFunc, self)
        else:
            RO.AddCallback.BaseMixin.addCallback(self,
                                                 callFunc,
                                                 callNow=callNow)

    def getMsg(self):
        """Get minimal message in simple format, prefering _textMsg

        @return _textMsg (if available), else _hubMsg (which may be blank).
        """
        return self._textMsg or self._hubMsg

    def getKeyValMsg(self, textPrefix=""):
        """Get full message data as (msgCode, msgStr), where msgStr is in keyword-value format

        @param[in] textPrefix  a prefix added to self._textMsg
        @return two values:
        - msgCode: message code (e.g. "W")
        - msgStr: message string: a combination of _textMsg and _hubMsg in keyword-value format.
            Warning: he "Text" keyword will be repeated if _textMsg is non-empty and _hubMsg contains "Text="
        """
        msgCode = self._MsgCodeDict[self._state]
        msgInfo = []
        if self._hubMsg:
            msgInfo.append(self._hubMsg)
        if self._textMsg or textPrefix:
            msgInfo.append("text=%s" %
                           (quoteStr(textPrefix + self._textMsg), ))
        msgStr = "; ".join(msgInfo)
        return (msgCode, msgStr)

    def setState(self, newState, textMsg=None, hubMsg=None):
        """Set the state of the command and call callbacks.

        If new state is done then remove all callbacks (after calling them).

        @param[in] newState  new state of command
        @param[in] textMsg  a message to be printed using the Text keyword; if None then not changed
        @param[in] hubMsg  a message in keyword=value format (without a header); if None then not changed

        You can set both textMsg and hubMsg, but typically only one or the other will be displayed
        (depending on the situation).

        If the new state is Failed then please supply a textMsg and/or hubMsg.

        Error conditions:
        - Raise RuntimeError if this command is finished.
        """
        # print("%r.setState(newState=%s, textMsg=%r, hubMsg=%r); self._cmdToTrack=%r" % (self, newState, textMsg, hubMsg, self._cmdToTrack))
        if self.isDone:
            raise RuntimeError("Command %s is done; cannot change state" %
                               str(self))
        if newState not in self.AllStates:
            raise RuntimeError("Unknown state %s" % newState)
        if self._state == self.Ready and newState in self.ActiveStates and self._timeLim:
            self._timeoutTimer.start(self._timeLim, self._timeout)
        self._state = newState
        if textMsg is not None:
            self._textMsg = str(textMsg)
        if hubMsg is not None:
            self._hubMsg = str(hubMsg)
        log.info(str(self))
        self._basicDoCallbacks(self)
        if self.isDone:
            self._timeoutTimer.cancel()
            self._removeAllCallbacks()
            self.untrackCmd()

    def setTimeLimit(self, timeLim):
        """Set a new time limit

        If the new limit is 0 or None then there is no time limit.
        If the new limit is < 0, it is ignored and a warning is printed to stderr

        If the command is has not started running, then the timer starts when the command starts running.
        If the command is running the timer starts now (any time spent before now is ignored).
        If the command is done then the new time limit is silently ignored.
        """
        if timeLim and float(timeLim) < 0:
            sys.stderr.write(
                "Negative time limit received: %0.2f, and ignored\n" % timeLim)
            return
        self._timeLim = float(timeLim) if timeLim else None
        if self._timeLim:
            if self.isActive:
                self._timeoutTimer.start(self._timeLim, self._timeout)
        else:
            self._timeoutTimer.cancel()

    def trackCmd(self, cmdToTrack):
        """Tie the state of this command to another command

        When the state of cmdToTrack changes then state, textMsg and hubMsg are copied to this command.

        @warning: if this command times out before trackCmd is finished,
        or if the state of this command is set finished, then the link is broken.
        """
        if self.isDone:
            raise RuntimeError("Finished; cannot track a command")
        if self._cmdToTrack:
            raise RuntimeError("Already tracking a command")
        self._cmdToTrack = cmdToTrack
        if cmdToTrack.isDone:
            self._cmdCallback(cmdToTrack)
        else:
            cmdToTrack.addCallback(self._cmdCallback)

    def untrackCmd(self):
        """Stop tracking a command if tracking one, else do nothing
        """
        if self._cmdToTrack:
            self._cmdToTrack.removeCallback(self._cmdCallback)
            self._cmdToTrack = None

    def removeChildren(self):
        for cmd in self._linkedCommands:
            cmd.removeCallback(self.linkCmdCallback)
        self._linkedCommands = []

    def setParentCmd(self, cmd):
        self._parentCmd = cmd

    def linkCommands(self, cmdList):
        """Tie the state of this command to a list of commands

        If any command in the list fails, so will this command
        """
        if self.isDone:
            raise RuntimeError("Finished; cannot link commands")
        if self._cmdToTrack:
            raise RuntimeError("Already tracking a command")
        self._linkedCommands.extend(cmdList)
        for cmd in cmdList:
            cmd.setParentCmd(self)
            if not cmd.isDone:
                cmd.addCallback(self.linkCmdCallback)
        # call right away in case all sub-commands are already done
        self.linkCmdCallback()

    def linkCmdCallback(self, dumCmd=None):
        """!Callback to be added to each device cmd

        @param[in] dumCmd  sub-command issuing the callback (ignored)
        """
        # if any linked commands have become active and this command is not yet active
        # set it cto the running state!
        if self.state == self.Ready and True in [
                linkedCommand.isActive
                for linkedCommand in self._linkedCommands
        ]:
            self.setState(self.Running)

        if not all(linkedCommand.isDone
                   for linkedCommand in self._linkedCommands):
            # not all device commands have terminated so keep waiting
            return

        failedCmdSummary = "; ".join(
            "%s: %s" % (linkedCommand.cmdStr, linkedCommand.getMsg())
            for linkedCommand in self._linkedCommands if linkedCommand.didFail)
        if failedCmdSummary:
            # at least one device command failed, fail the user command and say why
            # note, do we want to match the type of failure? If a subcommand was cancelled
            # should the mainCmd state be cancelled too?
            state = self.Failed
            textMsg = failedCmdSummary
        else:
            # all device commands terminated successfully
            # set user command to done
            state = self.Done
            textMsg = ""
        self.setState(state, textMsg=textMsg)

    @classmethod
    def stateFromMsgCode(cls, msgCode):
        """Return the command state associated with a particular message code
        """
        return cls._InvMsgCodeDict[msgCode]

    def _cmdCallback(self, cmdToTrack):
        """Tracked command's state has changed; copy state, textMsg and hubMsg
        """
        self.setState(cmdToTrack.state,
                      textMsg=cmdToTrack.textMsg,
                      hubMsg=cmdToTrack.hubMsg)

    def _timeout(self):
        """Call when command has timed out
        """
        if not self.isDone:
            self.setState(self.Failed, textMsg="Timed out")

    def _getDescrList(self, doFull=False):
        """Get list of descriptive strings for __str__ and __repr__
        """
        descrList = [
            repr(self.cmdStr),
            "state=%s" % (self._state, ),
        ]
        if doFull:
            descrList += [
                "userID=%r" % (self.userID, ),
                "cmdID=%r" % (self.cmdID, ),
                "timeLim=%r" % (self._timeLim, ),
            ]
        if self._textMsg:
            descrList.append("textMsg=%r" % (self._textMsg, ))
        if self._hubMsg:
            descrList.append("hubMsg=%r" % (self._hubMsg, ))
        if doFull and self._cmdToTrack:
            descrList.append("cmdToTrack=%r" % (self._cmdToTrack, ))
        return descrList

    def __str__(self):
        return "%s(%s)" % (self.__class__.__name__, ", ".join(
            self._getDescrList(doFull=False)))

    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, ", ".join(
            self._getDescrList(doFull=True)))
예제 #29
0
파일: m2Device.py 프로젝트: csayres/lcoTCC
class M2Device(TCPDevice):
    """!A Device for communicating with the M2 process."""
    def __init__(self, name, host, port, callFunc=None):
        """!Construct a LCODevice

        Inputs:
        @param[in] name  name of device
        @param[in] host  host address of m2 controller
        @param[in] port  port of m2 controller
        @param[in] callFunc  function to call when state of device changes;
                note that it is NOT called when the connection state changes;
                register a callback with "conn" for that task.
        """
        self.status = Status()
        self._statusTimer = Timer()
        self.waitMoveCmd = UserCmd()
        self.waitMoveCmd.setState(self.waitMoveCmd.Done)
        # self.waitGalilCmd = UserCmd()
        # self.waitGalilCmd.setState(self.waitGalilCmd.Done)
        # give status commands equal priority so they don't kill eachother
        priorityDict = {
            "status": 1,
            "speed": 1,
            "stop": 1,
            "move": 1,
            "galil": 1,
            "offset": 1,
        }

        self.devCmdQueue = CommandQueue(priorityDict) # all commands of equal priority

        TCPDevice.__init__(self,
            name = name,
            host = host,
            port = port,
            callFunc = callFunc,
            cmdInfo = (),
        )

    @property
    def isBusy(self):
        return self.status.state == Moving

    @property
    def isOff(self):
        return self.status.galil == Off

    @property
    def isDone(self):
        # only done when state=done and galil=off
        return not self.isBusy and self.isOff

    @property
    def currExeDevCmd(self):
        return self.devCmdQueue.currExeCmd.cmd

    @property
    def currDevCmdStr(self):
        return self.currExeDevCmd.cmdStr

    # @property
    # def atFocus(self):
    #     if self.status.statusFieldDict["focus"].value and abs(self.targFocus - self.status.statusFieldDict["focus"].value) < FocusPosTol:
    #         return True
    #     else:
    #         return False

    def init(self, userCmd=None, timeLim=None, getStatus=True):
        """Called automatically on startup after the connection is established.
        Only thing to do is query for status or connect if not connected
        """
        log.info("%s.init(userCmd=%s, timeLim=%s, getStatus=%s)" % (self, userCmd, timeLim, getStatus))
        # print("%s.init(userCmd=%s, timeLim=%s, getStatus=%s)" % (self, userCmd, timeLim, getStatus))
        userCmd = expandUserCmd(userCmd)
        # if not self.isConnected:
        #     return self.connect(userCmd=userCmd)
        # get the speed on startup
        # ignore getStatus flag, just do it always
        self.queueDevCmd("speed")
        return self.getStatus(userCmd=userCmd)
        # userCmd.setState(userCmd.Done)
        # return userCmd

    def getStatus(self, userCmd=None):
        """Return current telescope status. Continuously poll.
        """
        log.info("%s.getStatus(userCmd=%s)" % (self, userCmd)) # logging this will flood the log
        # print("%s.getStatus(userCmd=%s)" % (self, userCmd))
        userCmd = expandUserCmd(userCmd)
        if not self.conn.isConnected:
            userCmd.setState(userCmd.Failed, "Not Connected to M2")
            return userCmd
        self._statusTimer.cancel() # incase a status is pending
        userCmd = expandUserCmd(userCmd)
        # userCmd.addCallback(self._statusCallback)
        # gather list of status elements to get
        statusCmd = self.queueDevCmd("status", userCmd)
        LinkCommands(userCmd, [statusCmd])
        return userCmd

    def processStatus(self, replyStr):
        # print("procesStatus", replyStr)

        self.status.parseStatus(replyStr)
        # do we want status output so frequently? probabaly not.
        # perhaps only write status if it has changed...
        # but so far status is a small amount of values
        # so its probably ok
        statusStr = self.status.getStatusStr()
        if statusStr:
            userCmd = self.currExeDevCmd.userCmd
            if self.waitMoveCmd.isActive:
                userCmd = self.waitMoveCmd
                # write as debug during moves
                self.writeToUsers("d", statusStr, userCmd)
            else:
                self.writeToUsers("i", statusStr, userCmd)

        if self.waitMoveCmd.isActive:
            if not self.isBusy:
                # move is done
                if not self.isOff:
                    # move just finished but galil is not off, turn it off
                    self.queueDevCmd("galil off")
                else:
                    # move is done and galil is off, set wait move command as done
                    self.waitMoveCmd.setState(self.waitMoveCmd.Done)
        if not self.isDone:
            # keep polling until done
            self._statusTimer.start(PollTime, self.getStatus)

    def stop(self, userCmd=None):
        userCmd = expandUserCmd(userCmd)
        if not self.waitMoveCmd.isDone:
            self.waitMoveCmd.setState(self.waitMoveCmd.Cancelled, "Stop commanded")
        #print("sec stop commanded")
        stopCmd = self.queueDevCmd("stop", userCmd)
        galilOffCmd = self.queueDevCmd("galil off", userCmd)
        status = self.queueDevCmd("status", userCmd)
        status2 = self.queueDevCmd("status", userCmd)
        # first status gets the error state
        # second status clears it
        LinkCommands(userCmd, [stopCmd, status, galilOffCmd, status2])
        return userCmd

    def focus(self, focusValue, offset=False, userCmd=None):
        """Command an offset or absolute focus move

        @param[in] focusValue: float, focus value in microns
        @param[in] offset, if true this is offset, else absolute
        @param[in] userCmd: a twistedActor BaseCommand

        WARNING!!!
        At APO increasing focus corresponds to decreasing M1-M2 dist.
        The mirror controller at LCO convention is the opposite.
        """
        log.info("%s.focus(userCmd=%s, focusValue=%.2f, offset=%s)" % (self, userCmd, focusValue, str(bool(offset))))
        # if this focus value is < 50 microns
        userCmd = expandUserCmd(userCmd)
        if offset:
            deltaFocus = focusValue
        else:
            deltaFocus = self.status.secFocus - focusValue


        if abs(deltaFocus) < MIN_FOCUS_MOVE:
            # should focus be cancelled or just set to done?
            self.writeToUsers("w", "Focus offset below threshold of < %.2f, moving anyways."%MIN_FOCUS_MOVE, userCmd)
            # userCmd.setState(userCmd.Done)
            # return userCmd

        # focusDir = 1 # use M2's natural coordinates
        # focusDir = -1 # use convention at APO
        return self.move(valueList=[focusValue], offset=offset, userCmd=userCmd)

    def move(self, valueList, offset=False, userCmd=None):
        """Command an offset or absolute orientation move

        @param[in] valueList: list of 1 to 5 values specifying pistion(um), tiltx("), tilty("), transx(um), transy(um)
        @param[in] offset, if true this is offset, else absolute
        @param[in] userCmd: a twistedActor BaseCommand

        Note: increasing distance eg pistion means increasing spacing between primary and
        secondary mirrors.
        """
        log.info("%s.move(userCmd=%s, valueList=%s, offset=%s)" % (self, userCmd, str(valueList), str(bool(offset))))
        userCmd = expandUserCmd(userCmd)
        if not self.waitMoveCmd.isDone:
            userCmd.setState(userCmd.Failed, "Mirror currently moving")
            return userCmd
        if not 1<=len(valueList)<=5:
            userCmd.setState(userCmd.Failed, "Must specify 1 to 5 numbers for a move")
            return userCmd
        self.waitMoveCmd = UserCmd()
        self.waitMoveCmd.userCmd = userCmd # for write to users
        self.status.desOrientation = self.status.orientation[:]
        if offset:
            # if offset is specified, offset from current value
            for ii, value in enumerate(valueList):
                self.status.desOrientation[ii] += value
        else:
            # if absolute is wanted, overwrite all those
            # specified
            for ii, value in enumerate(valueList):
                self.status.desOrientation[ii] = value
        cmdType = "offset" if offset else "move"
        strValList = " ".join(["%.2f"%val for val in valueList])
        cmdStr = "%s %s"%(cmdType, strValList)
        moveCmd = self.queueDevCmd(cmdStr, userCmd)
        statusCmd = self.queueDevCmd("status", userCmd)
        # status immediately to see moving state
        # determine total time for move
        # just use focus distance as proxy (ignore)
        galilOverHead = 2 # galil take roughly 2 secs to boot up.
        extraOverHead = 2 #
        self.status._moveTimeTotal = self.getTimeForMove()
        timeout = self.status._moveTimeTotal+galilOverHead+extraOverHead
        userCmd.setTimeLimit(timeout)
        LinkCommands(userCmd, [moveCmd, statusCmd, self.waitMoveCmd])
        return userCmd

    def getTimeForMove(self):
        dist2Move = numpy.max(numpy.abs(numpy.subtract(self.status.desOrientation, self.status.orientation)))
        time4Move = dist2Move / self.status.speed
        return time4Move

    def handleReply(self, replyStr):
        """Handle a line of output from the device. Called whenever the device outputs a new line of data.

        @param[in] replyStr   the reply, minus any terminating \n

        Tasks include:
        - Parse the reply
        - Manage the pending commands
        - Parse status to update the model parameters
        """
        log.info("%s read %r, currCmdStr: %s" % (self, replyStr, self.currDevCmdStr))
        # print("%s read %r, currCmdStr: %s" % (self, replyStr, self.currDevCmdStr))
        replyStr = replyStr.strip()
        if not replyStr:
            return
        if self.currExeDevCmd.isDone:
            # ignore unsolicited ouput
            return
        if "error" in replyStr.lower():
            # error
            self.writeToUsers("w", "Error in M2 reply: %s, current cmd: %s"%(replyStr, self.currExeDevCmd.cmdStr))
        # if this was a speed command, set it
        if self.currDevCmdStr.lower() == "speed":
            self.status.speed = float(replyStr)
        elif self.currDevCmdStr.lower() == "status":
            self.processStatus(replyStr)
        # only one line is ever returned after a request
        # so if we got one, then the request is done
        self.currExeDevCmd.setState(self.currExeDevCmd.Done)


    def queueDevCmd(self, cmdStr, userCmd=None):
        """Add a device command to the device command queue

        @param[in] cmdStr, string to send to the device.
        """
        log.info("%s.queueDevCmd(cmdStr=%r, cmdQueue: %r"%(self, cmdStr, self.devCmdQueue))
        # print("%s.queueDevCmd(devCmd=%r, devCmdStr=%r, cmdQueue: %r"%(self, devCmd, devCmd.cmdStr, self.devCmdQueue))
        # append a cmdVerb for the command queue (other wise all get the same cmdVerb and cancel eachother)
        # could change the default behavior in CommandQueue?
        userCmd = expandUserCmd(userCmd)
        devCmd = DevCmd(cmdStr)
        devCmd.cmdVerb = cmdStr.split()[0]
        devCmd.userCmd = userCmd
        def queueFunc(devCmd):
            self.startDevCmd(devCmd)
        self.devCmdQueue.addCmd(devCmd, queueFunc)
        return devCmd


    def startDevCmd(self, devCmd):
        """
        @param[in] devCmdStr a line of text to send to the device
        """
        devCmdStr = devCmd.cmdStr.lower() # m2 uses all lower case
        log.info("%s.startDevCmd(%r)" % (self, devCmdStr))
        try:
            if self.conn.isConnected:
                log.info("%s writing %r" % (self, devCmdStr))
                # set move command to running now. Bug if set earlier race condition
                # with status
                if "move" in devCmdStr.lower() or "offset" in devCmdStr.lower():
                    self.waitMoveCmd.setState(self.waitMoveCmd.Running)
                    self.status.state = Moving
                    self.writeToUsers("i", self.status.secStateStr(), devCmd.userCmd)
                # if "galil" in devCmdStr.lower():
                #     self.waitGalilCmd.setState(self.waitGalilCmd.Running)
                self.conn.writeLine(devCmdStr)
            else:
                self.currExeDevCmd.setState(self.currExeDevCmd.Failed, "Not connected to M2")
        except Exception as e:
            self.currExeDevCmd.setState(self.currExeDevCmd.Failed, textMsg=strFromException(e))
예제 #30
0
파일: tcsDevice.py 프로젝트: csayres/lcoTCC
class TCSDevice(TCPDevice):
    """!A Device for communicating with the LCO TCS."""
    def __init__(self, name, host, port, callFunc=None):
        """!Construct a LCODevice

        Inputs:
        @param[in] name  name of device
        @param[in] host  host address of tcs controller
        @param[in] port  port of tcs controller
        @param[in] callFunc  function to call when state of device changes;
                note that it is NOT called when the connection state changes;
                register a callback with "conn" for that task.
        """
        self.status = Status()
        self._statusTimer = Timer()

        self.waitRotCmd = UserCmd()
        self.waitRotCmd.setState(self.waitRotCmd.Done)
        self.waitRotTimer = Timer()

        # self.waitFocusCmd = UserCmd()
        # self.waitFocusCmd.setState(self.waitFocusCmd.Done)

        self.waitOffsetCmd = UserCmd()
        self.waitOffsetCmd.setState(self.waitOffsetCmd.Done)
        self.waitOffsetTimer = Timer()
        self.rotDelay = False

        self.devCmdQueue = CommandQueue({}) # all commands of equal priority

        self.lastGuideRotApplied = None

        self.doGuideRot = True

        TCPDevice.__init__(self,
            name = name,
            host = host,
            port = port,
            callFunc = callFunc,
            cmdInfo = (),
        )

    @property
    def currExeDevCmd(self):
        return self.devCmdQueue.currExeCmd.cmd

    @property
    def currDevCmdStr(self):
        return self.currExeDevCmd.cmdStr

    # @property
    # def atFocus(self):
    #     if self.status.statusFieldDict["focus"].value and abs(self.targFocus - self.status.statusFieldDict["focus"].value) < FocusPosTol:
    #         return True
    #     else:
    #         return False

    @property
    def isTracking(self):
        if not self.waitOffsetCmd.isDone:
            # offsets do not trigger tcs tracking state, so we fake it here
            return False
        return self.status.statusFieldDict["state"].value == Tracking

    @property
    def isSlewing(self):
        if not self.waitOffsetCmd.isDone or not self.status.isClamped:
            # if clamp is not on, then we are moving the rotator
            return True
        else:
            return False

    def init(self, userCmd=None, timeLim=None, getStatus=True):
        """Called automatically on startup after the connection is established.
        Only thing to do is query for status or connect if not connected
        """
        log.info("%s.init(userCmd=%s, timeLim=%s, getStatus=%s)" % (self, userCmd, timeLim, getStatus))
        # print("%s.init(userCmd=%s, timeLim=%s, getStatus=%s)" % (self, userCmd, timeLim, getStatus))
        userCmd = expandUserCmd(userCmd)
        # if not self.isConnected:
        #     # time lim handled by lco.deviceCmd
        #     return self.connect(userCmd=userCmd)
        if getStatus:
            return self.getStatus(userCmd=userCmd)
        else:
            userCmd.setState(userCmd.Done)
            return userCmd

    def getStatus(self, userCmd=None):
        """Return current telescope status. Continuously poll.
        """
        log.info("%s.getStatus(userCmd=%s)" % (self, userCmd)) # logging this will flood the log
        userCmd = expandUserCmd(userCmd)
        if not self.conn.isConnected:
            userCmd.setState(userCmd.Failed, "Not Connected to TCS")
            return userCmd
        self._statusTimer.cancel() # incase a status is pending
        userCmd = expandUserCmd(userCmd)
        userCmd.addCallback(self._statusCallback)
        # record the present RA, DEC (for determining when offsets are done)
        # self.status.previousRA = self.status.statusFieldDict["ra"].value
        # self.status.previousDec = self.status.statusFieldDict["dec"].value
        if self.status.statusFieldDict["mpos"].value is None:
            self.status.previousRA, self.status.previousDec = None, None
        else:
            self.status.previousRA = self.status.statusFieldDict["mpos"].value[0]
            self.status.previousDec = self.status.statusFieldDict["mpos"].value[1]
        # gather list of status elements to get
        devCmdList = [DevCmd(cmdStr=cmdVerb) for cmdVerb in self.status.statusFieldDict.keys()]
        LinkCommands(userCmd, devCmdList)
        for devCmd in devCmdList:
            self.queueDevCmd(devCmd)
        if not self.status.isClamped or self.rotDelay:
            # rotator is moving, get status frequently
            pollTime = PollTimeRot
        elif self.isSlewing:
            # slewing, get status kinda frequently
            pollTime = PollTimeSlew
        elif self.isTracking:
            # tracking, get status less frequently
            pollTime = PollTimeTrack
        else:
            # idle, get status infrequently (as things shouldn't be changing fast)
            pollTime = PollTimeIdle
        self._statusTimer.start(pollTime, self.getStatus)
        return userCmd

    def _statusCallback(self, cmd):
        """! When status command is complete, send info to users, and check if any
        wait commands need to be set done
        """
        # print("tcs status callback", cmd)
        if cmd.isDone and not cmd.didFail:
            # do we want status output so frequently? probabaly not.
            # perhaps only write status if it has changed...
            statusStr = self.status.getStatusStr()
            if statusStr:
                self.writeToUsers("i", statusStr, cmd)

            if self.waitOffsetCmd.isActive and not True in self.status.axesSlewing():
                self.waitOffsetCmd.setState(self.waitOffsetCmd.Done)

            if self.waitRotCmd.isActive and not self.rotDelay and self.status.isClamped: #not self.status.rotMoving: #and self.status.rotOnTarget :
                print("set rot command done", self.rotDelay, self.status.isClamped, self.status.rotMoving)
                self.waitRotCmd.setState(self.waitRotCmd.Done)

    def target(self, ra, dec, doHA, userCmd=None):
        """Set coordinates for a slew.

        @param[in] ra: right ascension decimal degrees
        @param[in] dec: declination decimal degrees
        @param[in] doHA: if True, use degrees in hour angle rather than ra.
        @param[in] userCmd: a twistedActor BaseCommand.
        """
        log.info("%s.slew(userCmd=%s, ra=%.2f, dec=%.2f)" % (self, userCmd, ra, dec))
        userCmd = expandUserCmd(userCmd)
        if not self.conn.isConnected:
            userCmd.setState(userCmd.Failed, "Not Connected to TCS")
            return userCmd
        if doHA:
            enterRa = "HAD %.8f"%ra
        else:
            enterRa = "RAD %.8f"%ra
        enterDec = "DECD %.8f"%dec
        enterEpoch = "MP %.2f"%2000 # LCO: HACK should coords always be 2000?
        devCmdList = [DevCmd(cmdStr=cmdStr) for cmdStr in [enterRa, enterDec, enterEpoch]]#, cmdSlew]]
        # set userCmd done only when each device command finishes
        # AND the pending slew is also done.
        # when the last dev cmd is done (the slew), set axis cmd statue to slewing

        LinkCommands(userCmd, devCmdList) #LCO: HACK don't wait for a slew to finish + [self.waitSlewCmd])
        for devCmd in devCmdList:
            self.queueDevCmd(devCmd)

        statusStr = self.status.getStatusStr()
        if statusStr:
            self.writeToUsers("i", statusStr, userCmd)
        return userCmd

    def slewOffset(self, ra, dec, userCmd=None):
        """Offset telescope in right ascension and declination.

        @param[in] ra: right ascension in decimal degrees
        @param[in] dec: declination in decimal degrees
        @param[in] userCmd a twistedActor BaseCommand

        @todo, consolidate similar code with self.target?
        """
        log.info("%s.slewOffset(userCmd=%s, ra=%.6f, dec=%.6f)" % (self, userCmd, ra, dec))
        userCmd = expandUserCmd(userCmd)
        # zero the delta computation so the offset isn't marked done immediately
        self.status.previousDec = ForceSlew
        self.status.previousRA = ForceSlew
        if not self.conn.isConnected:
            userCmd.setState(userCmd.Failed, "Not Connected to TCS")
            return userCmd
        if not self.waitOffsetCmd.isDone:
            self.waitOffsetCmd.setState(self.waitOffsetCmd.Cancelled, "Superseded by new offset")
        waitOffsetCmd = UserCmd()
        self.waitOffsetCmd = waitOffsetCmd
        enterRa = "OFRA %.8f"%(ra*ArcSecPerDeg)
        enterDec = "OFDC %.8f"%(dec*ArcSecPerDeg) #lcohack
        devCmdList = [DevCmd(cmdStr=cmdStr) for cmdStr in [enterRa, enterDec, CMDOFF]]
        # set userCmd done only when each device command finishes
        # AND the pending slew is also done.
        # set an offset done after 6 seconds no matter what
        def setWaitOffsetCmdDone(aWaitingOffsetCmd):
            print("wait offset command state", aWaitingOffsetCmd.state)
            if not aWaitingOffsetCmd.isDone:
                print("Wait offset timed out!!!!")
                self.writeToUsers("w", "Text=OFFSET SET DONE ON TIMER.")
                aWaitingOffsetCmd.setState(aWaitingOffsetCmd.Done, "offset set done on a timer")
        self.waitOffsetTimer.start(8, setWaitOffsetCmdDone, waitOffsetCmd)
        # self.waitOffsetCmd.setTimeLimit(6)
        LinkCommands(userCmd, devCmdList + [self.waitOffsetCmd])
        for devCmd in devCmdList:
            self.queueDevCmd(devCmd)
        statusStr = self.status.getStatusStr()
        if statusStr:
            self.writeToUsers("i", statusStr, userCmd)
        return userCmd

    def rotOffset(self, rot, userCmd=None, force=False):
        """Offset telescope rotator.  USE APGCIR cmd
        which holds current

        @param[in] rot: in decimal degrees
        @param[in] userCmd a twistedActor BaseCommand
        """
        # LCOHACK: allow offsets only if over 5 arcseconds!

        # if True:
        #     #! lupton!
        #     userCmd = expandUserCmd(userCmd)
        #     self.writeToUsers("w", "Rotator offset %.6f bypassed"%rot)
        #     userCmd.setState(userCmd.Done)
        #     return userCmd

        if not self.conn.isConnected:
            userCmd.setState(userCmd.Failed, "Not Connected to TCS")
            return userCmd
        if not self.waitRotCmd.isDone:
            # rotator is unclamped, a move is in progress
            userCmd.setState(userCmd.Failed, "Rotator is unclamped (already moving?)")
            return userCmd
        # if abs(rot) < MinRotOffset and not force:
        if not self.doGuideRot:
            # set command done, rotator offset is miniscule
            self.writeToUsers("w", "Guide rot not enabled, not applying", userCmd)
            userCmd.setState(userCmd.Done)
            return userCmd
        if abs(rot) > MaxRotOffset:
            # set command failed, rotator offset is too big
            self.writeToUsers("w", "Rot offset greater than max threshold", userCmd)
            userCmd.setState(userCmd.Failed, "Rot offset %.4f > %.4f"%(rot, MaxRotOffset))
            return userCmd
        ### print time since last rot applied from guider command
        if not force:
            if self.lastGuideRotApplied is None:
                self.lastGuideRotApplied = time.time()
            else:
                tnow = time.time()
                infoStr = "time since last guide rot update: %.2f"%(tnow-self.lastGuideRotApplied)
                print(infoStr)
                log.info(infoStr)
                self.lastGuideRotApplied = tnow

        # apgcir requires absolute position, calculate it
        # first get status
        newPos = self.status.rotPos - rot
        # rotStart = time.time()
        # def printRotSlewTime(aCmd):
        #     if aCmd.isDone:
        #         rotTime = time.time() - rotStart
        #         print("rot: off, time, speed: %.5f %.5f %5f"%(newPos, rotTime, newPos/rotTime))
        waitRotCmd = UserCmd()
        self.waitRotCmd = waitRotCmd
        # calculate time limit for rot move:
        rotTimeLimBuffer = 2 # check for clamp after 4 seconds
        self.rotDelay = True
        print("setting rot delay true")
        def setRotBufferOff():
            print("setting rot delay false")
            print("rot buffer Off (clamped?)", self.status.isClamped)
            self.rotDelay = False
        self.waitRotTimer.start(rotTimeLimBuffer, setRotBufferOff)
        self.waitOffsetCmd.setTimeLimit(rotTimeLimBuffer + 20)
        self.status.setRotOffsetTarg(rot)
        enterAPGCIR = DevCmd(cmdStr="APGCIR %.8f"%(newPos))
        LinkCommands(userCmd, [enterAPGCIR, self.waitRotCmd])
        # begin the dominos game
        self.queueDevCmd(enterAPGCIR)
        statusStr = self.status.getStatusStr()
        if statusStr:
            self.writeToUsers("i", statusStr, userCmd)
        return userCmd


    def handleReply(self, replyStr):
        """Handle a line of output from the device. Called whenever the device outputs a new line of data.

        @param[in] replyStr   the reply, minus any terminating \n

        Tasks include:
        - Parse the reply
        - Manage the pending commands
        - Output data to users
        - Parse status to update the model parameters
        - If a command has finished, call the appropriate command callback
        """
        log.info("%s read %r, currCmdStr: %s" % (self, replyStr, self.currDevCmdStr))
        replyStr = replyStr.strip()
        if replyStr == "-1":
            # error
            errorStr = "handleReply failed for %s with -1"%self.currDevCmdStr
            if self.waitOffsetCmd.isActive:
                self.waitOffsetCmd.setState(self.waitOffsetCmd.Failed, errorStr)
            if self.waitRotCmd.isActive:
                # note the clamp should still execute!!!!
                self.waitRotCmd.setState(self.waitRotCmd.Failed, errorStr)
            self.currExeDevCmd.setState(self.currExeDevCmd.Failed, errorStr)
            return
        statusField = self.status.statusFieldDict.get(self.currDevCmdStr, None)
        if statusField:
            # a piece of status was requested, parse it
            # how to check for error condition? parsing -1 as a float will still work.
            statusField.setValue(replyStr)
            self.currExeDevCmd.setState(self.currExeDevCmd.Done)
        elif replyStr == "0":
            # this was a command, a "0" is expected
            self.currExeDevCmd.setState(self.currExeDevCmd.Done)
        else:
            log.info("unexpected reply" % replyStr)
            #self.currExeDevCmd.setState(self.currExeDevCmd.Failed, "Unexpected reply %s for %s"%(replyStr, self.currDevCmdStr))


    def queueDevCmd(self, devCmd):
        """Add a device command to the device command queue

        @param[in] devCmd: a twistedActor DevCmd.
        """
        log.info("%s.queueDevCmd(devCmd=%r, devCmdStr=%r, cmdQueue: %r"%(self, devCmd, devCmd.cmdStr, self.devCmdQueue))
        # append a cmdVerb for the command queue (other wise all get the same cmdVerb and cancel eachother)
        # could change the default behavior in CommandQueue?
        devCmd.cmdVerb = devCmd.cmdStr
        def queueFunc(devCmd):
            # all tcs commands return immediately so set a short timeout
            devCmd.setTimeLimit(SEC_TIMEOUT)
            devCmd.setState(devCmd.Running)
            self.startDevCmd(devCmd.cmdStr)
        self.devCmdQueue.addCmd(devCmd, queueFunc)


    def startDevCmd(self, devCmdStr):
        """
        @param[in] devCmdStr a line of text to send to the device
        """
        devCmdStr = devCmdStr.upper() # lco uses all upper case
        log.info("%s.startDevCmd(%r)" % (self, devCmdStr))
        try:
            if self.conn.isConnected:
                log.info("%s writing %r" % (self, devCmdStr))
                if CMDOFF.upper() == devCmdStr:
                    self.waitOffsetCmd.setState(self.waitOffsetCmd.Running)
                elif "CIR" in devCmdStr:
                    self.waitRotCmd.setState(self.waitRotCmd.Running)
                self.conn.writeLine(devCmdStr)
            else:
                self.currExeDevCmd.setState(self.currExeDevCmd.Failed, "Not connected to TCS")
        except Exception as e:
            self.currExeDevCmd.setState(self.currExeDevCmd.Failed, textMsg=strFromException(e))
예제 #31
0
class BaseCmd(RO.AddCallback.BaseMixin):
    """Base class for commands of all types (user and device).
    """
    # state constants
    Done = "done"
    Cancelled = "cancelled" # including superseded
    Failed = "failed"
    Ready = "ready"
    Running = "running"
    Cancelling = "cancelling"
    Failing = "failing"
    ActiveStates = frozenset((Running, Cancelling, Failing))
    FailedStates = frozenset((Cancelled, Failed))
    FailingStates = frozenset((Cancelling, Failing))
    DoneStates = frozenset((Done,)) | FailedStates
    AllStates = frozenset((Ready,)) | ActiveStates | DoneStates
    _MsgCodeDict = dict(
        ready = "i",
        running = "i",
        cancelling = "w",
        failing = "w",
        cancelled = "f",
        failed = "f",
        debug = "d",
        done = ":",
    )
    _InvMsgCodeDict = dict((val, key) for key, val in _MsgCodeDict.iteritems())
    def __init__(self,
        cmdStr,
        userID = 0,
        cmdID = 0,
        callFunc = None,
        timeLim = None,
    ):
        """Construct a BaseCmd

        @param[in] cmdStr  command string
        @param[in] userID  user ID number
        @param[in] cmdID  command ID number
        @param[in] callFunc  function to call when command changes state;
            receives one argument: this command
        @param[in] timeLim  time limit for command (sec); if None or 0 then no time limit
        """
        self._cmdStr = cmdStr
        self.userID = int(userID)
        self.cmdID = int(cmdID)
        self._state = self.Ready
        self._textMsg = ""
        self._hubMsg = ""
        self._cmdToTrack = None
        self._linkedCommands = []
        self._parentCmd = None
        self._writeToUsers = None # set by baseActor.ExpandCommand
        # set by baseActor.newCmd to flag this as a command created
        # from socket input
        self.userCommanded = False
        self._timeoutTimer = Timer()
        self.setTimeLimit(timeLim)

        RO.AddCallback.BaseMixin.__init__(self, callFunc)

    @property
    def parentCmd(self):
        return self._parentCmd

    @property
    def eldestParentCmd(self):
        if self.parentCmd is None:
            return self
        else:
            return self.parentCmd.eldestParentCmd

    @property
    def timeLim(self):
        return self._timeLim

    @property
    def cmdStr(self):
        return self._cmdStr

    @property
    def didFail(self):
        """Command failed or was cancelled
        """
        return self._state in self.FailedStates

    @property
    def isActive(self):
        """Command is running, canceling or failing
        """
        return self._state in self.ActiveStates

    @property
    def isDone(self):
        """Command is done (whether successfully or not)
        """
        return self._state in self.DoneStates

    @property
    def isFailing(self):
        """Command is being cancelled or is failing
        """
        return self._state in self.FailingStates

    @property
    def msgCode(self):
        """The hub message code appropriate to the current state
        """
        return self._MsgCodeDict[self._state]

    @property
    def hubMsg(self):
        """The hub message or "" if none
        """
        return self._hubMsg

    @property
    def textMsg(self):
        """The text message or "" if none
        """
        return self._textMsg

    @property
    def state(self):
        """The state of the command, as a string which is one of the state constants, e.g. self.Done
        """
        return self._state

    def setWriteToUsers(self, writeToUsersFunc):
        if self._writeToUsers is not None:
            raise RuntimeError("Write to users is already set")
        else:
            self._writeToUsers = writeToUsersFunc

    def writeToUsers(self, msgCode, msgStr, userID=None, cmdID=None):
        # see doc in baseActor.
        # self._writeToUsers is set in BaseActor.newCmd()
        # or is is set at the time of any command linkage
        # get the top most command for writing to users
        # it will be the original cmd
        topCmd = self.eldestParentCmd
        if topCmd._writeToUsers is None:
            print("%s writeToUsers not set: "%str(self), msgCode, msgStr, topCmd, userID, cmdID, "!!!")
        else:
            topCmd._writeToUsers(msgCode, msgStr, topCmd, userID, cmdID)

    def addCallback(self, callFunc, callNow=False):
        """Add a callback function

        @param[in] callFunc  callback function:
        - it receives one argument: this command
        - it is called whenever the state changes, and immediately if the command is already done
            or callNow is True
        @param[in] callNow  if True, call callFunc immediately
        """
        if self.isDone:
            RO.AddCallback.safeCall2("%s.addCallback callFunc =" % (self,), callFunc, self)
        else:
            RO.AddCallback.BaseMixin.addCallback(self, callFunc, callNow=callNow)

    def getMsg(self):
        """Get minimal message in simple format, prefering _textMsg

        @return _textMsg (if available), else _hubMsg (which may be blank).
        """
        return self._textMsg or self._hubMsg

    def getKeyValMsg(self, textPrefix=""):
        """Get full message data as (msgCode, msgStr), where msgStr is in keyword-value format

        @param[in] textPrefix  a prefix added to self._textMsg
        @return two values:
        - msgCode: message code (e.g. "W")
        - msgStr: message string: a combination of _textMsg and _hubMsg in keyword-value format.
            Warning: he "Text" keyword will be repeated if _textMsg is non-empty and _hubMsg contains "Text="
        """
        msgCode = self._MsgCodeDict[self._state]
        msgInfo = []
        if self._hubMsg:
            msgInfo.append(self._hubMsg)
        if self._textMsg or textPrefix:
            msgInfo.append("text=%s" % (quoteStr(textPrefix + self._textMsg),))
        msgStr = "; ".join(msgInfo)
        return (msgCode, msgStr)

    def setState(self, newState, textMsg=None, hubMsg=None):
        """Set the state of the command and call callbacks.

        If new state is done then remove all callbacks (after calling them).

        @param[in] newState  new state of command
        @param[in] textMsg  a message to be printed using the Text keyword; if None then not changed
        @param[in] hubMsg  a message in keyword=value format (without a header); if None then not changed

        You can set both textMsg and hubMsg, but typically only one or the other will be displayed
        (depending on the situation).

        If the new state is Failed then please supply a textMsg and/or hubMsg.

        Error conditions:
        - Raise RuntimeError if this command is finished.
        """
        # print("%r.setState(newState=%s, textMsg=%r, hubMsg=%r); self._cmdToTrack=%r" % (self, newState, textMsg, hubMsg, self._cmdToTrack))
        if self.isDone:
            raise RuntimeError("Command %s is done; cannot change state" % str(self))
        if newState not in self.AllStates:
            raise RuntimeError("Unknown state %s" % newState)
        if self._state == self.Ready and newState in self.ActiveStates and self._timeLim:
            self._timeoutTimer.start(self._timeLim, self._timeout)
        self._state = newState
        if textMsg is not None:
            self._textMsg = str(textMsg)
        if hubMsg is not None:
            self._hubMsg = str(hubMsg)
        log.info(str(self))
        self._basicDoCallbacks(self)
        if self.isDone:
            self._timeoutTimer.cancel()
            self._removeAllCallbacks()
            self.untrackCmd()

    def setTimeLimit(self, timeLim):
        """Set a new time limit

        If the new limit is 0 or None then there is no time limit.
        If the new limit is < 0, it is ignored and a warning is printed to stderr

        If the command is has not started running, then the timer starts when the command starts running.
        If the command is running the timer starts now (any time spent before now is ignored).
        If the command is done then the new time limit is silently ignored.
        """
        if timeLim and float(timeLim) < 0:
            sys.stderr.write("Negative time limit received: %0.2f, and ignored\n"%timeLim)
            return
        self._timeLim = float(timeLim) if timeLim else None
        if self._timeLim:
            if self.isActive:
                self._timeoutTimer.start(self._timeLim, self._timeout)
        else:
            self._timeoutTimer.cancel()

    def trackCmd(self, cmdToTrack):
        """Tie the state of this command to another command

        When the state of cmdToTrack changes then state, textMsg and hubMsg are copied to this command.

        @warning: if this command times out before trackCmd is finished,
        or if the state of this command is set finished, then the link is broken.
        """
        if self.isDone:
            raise RuntimeError("Finished; cannot track a command")
        if self._cmdToTrack:
            raise RuntimeError("Already tracking a command")
        self._cmdToTrack = cmdToTrack
        if cmdToTrack.isDone:
            self._cmdCallback(cmdToTrack)
        else:
            cmdToTrack.addCallback(self._cmdCallback)

    def untrackCmd(self):
        """Stop tracking a command if tracking one, else do nothing
        """
        if self._cmdToTrack:
            self._cmdToTrack.removeCallback(self._cmdCallback)
            self._cmdToTrack = None

    def removeChildren(self):
        for cmd in self._linkedCommands:
            cmd.removeCallback(self.linkCmdCallback)
        self._linkedCommands = []

    def setParentCmd(self, cmd):
        self._parentCmd = cmd

    def linkCommands(self, cmdList):
        """Tie the state of this command to a list of commands

        If any command in the list fails, so will this command
        """
        if self.isDone:
            raise RuntimeError("Finished; cannot link commands")
        if self._cmdToTrack:
            raise RuntimeError("Already tracking a command")
        self._linkedCommands.extend(cmdList)
        for cmd in cmdList:
            cmd.setParentCmd(self)
            if not cmd.isDone:
                cmd.addCallback(self.linkCmdCallback)
        # call right away in case all sub-commands are already done
        self.linkCmdCallback()

    def linkCmdCallback(self, dumCmd=None):
        """!Callback to be added to each device cmd

        @param[in] dumCmd  sub-command issuing the callback (ignored)
        """
        # if any linked commands have become active and this command is not yet active
        # set it cto the running state!
        if self.state == self.Ready and True in [linkedCommand.isActive for linkedCommand in self._linkedCommands]:
            self.setState(self.Running)

        if not all(linkedCommand.isDone for linkedCommand in self._linkedCommands):
            # not all device commands have terminated so keep waiting
            return

        failedCmdSummary = "; ".join("%s: %s" % (linkedCommand.cmdStr, linkedCommand.getMsg()) for linkedCommand in self._linkedCommands if linkedCommand.didFail)
        if failedCmdSummary:
            # at least one device command failed, fail the user command and say why
            # note, do we want to match the type of failure? If a subcommand was cancelled
            # should the mainCmd state be cancelled too?
            state = self.Failed
            textMsg = failedCmdSummary
        else:
            # all device commands terminated successfully
            # set user command to done
            state = self.Done
            textMsg = ""
        self.setState(state, textMsg = textMsg)

    @classmethod
    def stateFromMsgCode(cls, msgCode):
        """Return the command state associated with a particular message code
        """
        return cls._InvMsgCodeDict[msgCode]

    def _cmdCallback(self, cmdToTrack):
        """Tracked command's state has changed; copy state, textMsg and hubMsg
        """
        self.setState(cmdToTrack.state, textMsg=cmdToTrack.textMsg, hubMsg=cmdToTrack.hubMsg)

    def _timeout(self):
        """Call when command has timed out
        """
        if not self.isDone:
            self.setState(self.Failed, textMsg="Timed out")

    def _getDescrList(self, doFull=False):
        """Get list of descriptive strings for __str__ and __repr__
        """
        descrList = [
            repr(self.cmdStr),
            "state=%s" % (self._state,),
        ]
        if doFull:
            descrList += [
                "userID=%r" % (self.userID,),
                "cmdID=%r" % (self.cmdID,),
                "timeLim=%r" % (self._timeLim,),
            ]
        if self._textMsg:
            descrList.append("textMsg=%r" % (self._textMsg,))
        if self._hubMsg:
            descrList.append("hubMsg=%r" % (self._hubMsg,))
        if doFull and self._cmdToTrack:
            descrList.append("cmdToTrack=%r" % (self._cmdToTrack,))
        return descrList

    def __str__(self):
        return "%s(%s)" % (self.__class__.__name__, ", ".join(self._getDescrList(doFull=False)))

    def __repr__(self):
        return "%s(%s)" % (self.__class__.__name__, ", ".join(self._getDescrList(doFull=True)))
예제 #32
0
class TCCLCOActor(BaseActor):
    """!TCC actor for the LCO telescope
    """
    # SCALE_PER_MM = 8.45e-05 * -1# Scaling ring convention (higher abs values )
    SCALE_PER_MM = 8.45e-05 # more MM per scale
    SCALE_RATIO = 1/7. #Sec dist = SCALE_RATIO * scaling ring dist
    # MAX_SF = 1.0008 # max scale factor from tcc25m/inst/default.dat
    MAX_SF = 1.02
    MIN_SF = 1./MAX_SF  # min scale factor
    def __init__(self,
        userPort,
        tcsDev,
        scaleDev,
        m2Dev,
        measScaleDev,
        name = "tcc",
    ):
        """Construct a TCCActor

        @param[in] userPort  port on which to listen for users
        @param[in] tcsDev a TCSDevice instance
        @param[in] scaleDev  a ScaleDevice instance
        @param[in] m2Dev a M2Device instance
        @param[in] m2Dev a MeasScaleDevice instance
        @param[in] name  actor name; used for logging
        """
        self.tcsDev = tcsDev
        self.tcsDev.writeToUsers = self.writeToUsers
        self.scaleDev = scaleDev
        self.scaleDev.writeToUsers = self.writeToUsers
        self.secDev = m2Dev
        self.secDev.writeToUsers = self.writeToUsers
        self.measScaleDev = measScaleDev
        self.measScaleDev.writeToUsers = self.writeToUsers
        # auto connection looks for self.dev
        self.dev = DeviceCollection([self.tcsDev, self.scaleDev, self.secDev, self.measScaleDev])
        # connect devices
        self.tcsDev.connect()
        self.scaleDev.connect()
        self.secDev.connect()
        self.measScaleDev.connect()
        self.cmdParser = TCCLCOCmdParser()
        self.collimationModel = CollimationModel()
        self.collimateTimer = Timer(0, self.updateCollimation)
        BaseActor.__init__(self, userPort=userPort, maxUsers=1, name=name, version=__version__)
        # Actor.__init__(self, userPort=userPort, maxUsers=1, name=name, devs=(tcsDev, scaleDev), version=__version__)

    @property
    def currentScaleFactor(self):
        return self.mm2scale(self.scaleDev.encPos)

    def scale2mm(self, scaleValue):
        # scale=1 device is at zero point
        return -1 * (scaleValue - 1.0) / self.SCALE_PER_MM + self.scaleDev.scaleZeroPos

    def mm2scale(self, mm):
        return -1 * (mm - self.scaleDev.scaleZeroPos) * self.SCALE_PER_MM + 1.0

    def scaleMult2mm(self, multiplier):
        return self.scale2mm(self.currentScaleFactor*multiplier)

    def scaleMult2mmStable(self, multiplier):
        # this may be more numerically stable,
        # according to unittests self.scaleMult2mm
        # works just fine, and it is simpler
        m = multiplier
        z = self.scaleDev.scaleZeroPos
        p = self.scaleDev.encPos
        alpha = self.SCALE_PER_MM
        return m*(p-z)+(1.0/alpha)*(m-1.0)+z

    def parseAndDispatchCmd(self, cmd):
        """Dispatch the user command

        @param[in] cmd  user command (a twistedActor.UserCmd)
        """
        if not cmd.cmdBody:
            # echo to show alive
            self.writeToOneUser(":", "", cmd=cmd)
            return
        try:
            cmd.parsedCmd = self.cmdParser.parseLine(cmd.cmdBody)
        except Exception as e:
            cmd.setState(cmd.Failed, "Could not parse %r: %s" % (cmd.cmdBody, strFromException(e)))
            return

        #cmd.parsedCmd.printData()
        if cmd.parsedCmd.callFunc:
            cmd.setState(cmd.Running)
            try:
                cmd.parsedCmd.callFunc(self, cmd)
            except CommandError as e:
                cmd.setState("failed", textMsg=strFromException(e))
                return
            except Exception as e:
                sys.stderr.write("command %r failed\n" % (cmd.cmdStr,))
                sys.stderr.write("function %s raised %s\n" % (cmd.parsedCmd.callFunc, strFromException(e)))
                traceback.print_exc(file=sys.stderr)
                textMsg = strFromException(e)
                hubMsg = "Exception=%s" % (e.__class__.__name__,)
                cmd.setState("failed", textMsg=textMsg, hubMsg=hubMsg)
        else:
            raise RuntimeError("Command %r not yet implemented" % (cmd.parsedCmd.cmdVerb,))

    def updateCollimation(self, cmd=None, force=False):
        """

        LCO HACK!!! clean this stuff up!!!!
        """
        cmd = expandUserCmd(cmd)
        if not self.collimationModel.doCollimate and not force:
            cmd.setState(cmd.Failed, "collimation is disabled")
            return
        if "Halted" in self.tcsDev.status.statusFieldDict["state"].value[:2]:
            # either RA or Dec axis is halted
            cmd.setState(cmd.Canceled("RA or Dec axis halted, not applying collimation."))
            return
        self.collimateTimer.cancel() # incase one is pending
        # query for current telescope coords
        statusCmd = self.tcsDev.getStatus()
        # when status returns determine current coords
        def moveMirrorCallback(statusCmd):
            if statusCmd.didFail:
                cmd.setState(cmd.Failed, "status command failed")
            elif statusCmd.isDone:
                # ha = self.tcsDev.status.statusFieldDict["ha"].value
                # dec = self.tcsDev.status.statusFieldDict["dec"].value
                # if an axis is slewing collimate to the target
                if "Slewing" in self.tcsDev.status.statusFieldDict["state"].value[:2]:
                    # ra or dec is slewing
                    # get target coords
                    # st and ra in degrees
                    st = self.tcsDev.status.statusFieldDict["st"].value
                    ra = self.tcsDev.status.statusFieldDict["inpra"].value
                    ha = st - ra
                    dec = self.tcsDev.status.statusFieldDict["inpdc"].value
                    self.writeToUsers("i", "collimate for target ha=%.2f, dec=%.2f"%(ha, dec))
                else:
                    # get current coords
                    ha, dec = self.tcsDev.status.statusFieldDict["pos"].value
                    self.writeToUsers("i", "collimate for current ha=%.2f, dec=%.2f"%(ha, dec))
                # self.writeToUsers("i", "pos collimate for ha=%.2f, dec=%.2f"%(pos[0], pos[1]))

                newOrient = self.collimationModel.getOrientation(ha, dec)
                orient = self.secDev.status.orientation[:]
                # check if mirror move is wanted based on tolerances
                dFocus = None if newOrient[0] is None else newOrient[0]-orient[0]
                dtiltX = newOrient[1]-orient[1]
                dtiltY = newOrient[2]-orient[2]
                dtransX = newOrient[3]-orient[3]
                dtransY = newOrient[4]-orient[4]
                doFlex = numpy.max(numpy.abs([dtiltX, dtiltY])) > self.collimationModel.minTilt or numpy.max(numpy.abs([dtransX, dtransY])) > self.collimationModel.minTrans

                if force:
                    self.writeToUsers("i", "collimation update forced")
                if not doFlex and not force:
                    self.writeToUsers("i", "collimation flex update too small: dTiltX=%.2f, dTiltY=%.2f, dTransX=%.2f, dTransY=%.2f"%(dtiltX, dtiltY, dtransX, dtransY))
                    cmd.setState(cmd.Done)
                else:
                    # update flex values
                    orient[1:] = newOrient[1:] # keep existing focus
                    self.writeToUsers("i", "collimation update: Focus=%.2f, TiltX=%.2f, TiltY=%.2f, TransX=%.2f, TransY=%.2f"%tuple(orient), cmd=cmd)
                    self.secDev.move(orient, userCmd=cmd)


        statusCmd.addCallback(moveMirrorCallback)

        # remove timer for now
        if self.collimationModel.doCollimate:
            self.collimateTimer.start(self.collimationModel.collimateInterval, self.updateCollimation)
        else:
            self.collimateTimer.cancel()
예제 #33
0
class ConnectDevice(object):
    """!Connect a device and execute dev.init

    If the device is already connected then generate a new userCmd, if needed,
    and sets userCmd's state to userCmd.Done.

    Public attributes:
    - dev: the provided device
    - userCmd: the provided userCmd, or a new one if none provided
    """
    def __init__(self, dev, userCmd, timeLim):
        """!Start connecting a device

        @param[in] dev  device
        @param[in] userCmd  user command associated with the connection, or None
        @param[in] timeLim  time limit (sec) to make this connection
        """
        self.dev = dev
        self._timeLim = timeLim
        self.userCmd = expandUserCmd(userCmd)
        self._connTimer = Timer()
        self._addedConnCallback = False

        if self.dev.isConnected and self.dev.conn.isConnected:
            # already done; don't send init
            self.finish()
            return

        self.dev._ignoreConnCallback = True
        self.dev.setState(self.dev.Connecting)
        self.dev.conn.addStateCallback(self.connCallback)
        self._addedConnCallback = True
        if self.dev.conn.mayConnect:
            self.dev.conn.connect(timeLim=timeLim)
        else:
            if self._timeLim:
                # start timer for the connection that is occurring now
                self._connTimer.start(self._timeLim, self.finish, "timed out waiting for connection")

    def initCallback(self, userCmd):
        """!Callback for device initialization
        """
        # print("%s.initCallback(userCmd=%r); _callbacks=%s" % (self, userCmd, userCmd._callbacks))
        if not userCmd.isDone:
            return

        if userCmd.didFail:
            reason = userCmd.getMsg() or "init command failed for unknown reasons"
        else:
            reason = None
        Timer(0, self.finish, reason)

    def connCallback(self, conn):
        """!Callback for device connection state
        """
        if self.dev.conn.isConnected:
            self._connTimer.cancel()
            initUserCmd = UserCmd(cmdStr="connect %s" % (self.dev.name,), callFunc=self.initCallback)
            self.dev.init(userCmd=initUserCmd, timeLim=self._timeLim, getStatus=True)
        elif self.dev.conn.didFail:
            self._connTimer.cancel()
            self.finish("connection failed")

    def finish(self, reason=None):
        """!Call on success or failure to finish the command

        @param[in] reason: reason for failure (if non-empty then failure is assumed)
        """
        self._connTimer.cancel()
        self.dev._ignoreConnCallback = False
        if self._addedConnCallback:
            self.dev.conn.removeStateCallback(self.connCallback)
        if reason or not self.dev.conn.isConnected:
            reason = reason or "unknown reason"
            self.dev.setState(self.dev.Failed, reason)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Failed, textMsg="%s failed to connect: %s" % (self.dev, reason))
            Timer(0, self.dev.conn.disconnect)
        else:
            self.dev.setState(self.dev.Connected)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)

    def __repr__(self):
        return "%s(dev.name=%s)" % (type(self).__name__, self.dev.name)
예제 #34
0
class FakeM2Ctrl(FakeDev):
    """!A server that emulates the LCO M2 Controller
    """
    Done = "DONE"
    Error = "ERROR"
    Moving = "MOVING"
    On = "on"
    Off = "off"
    def __init__(self, name, port):
        """!Construct a fake LCO M2

        @param[in] name  name of M2 controller
        @param[in] port  port on which to command M2

        State=DONE Ori=12500.0,70.0,-12.0,-600.1,925.0 Lamps=off Galil=off
        """
        self.orientation = [15,70.0,-12.0,-600.1,925.0]
        self.targOrientation = [15,70.0,-12.0,-600.1,925.0]
        self.moveState = self.Done
        self.lamps = self.Off
        self.galil = self.Off
        self.speed = 25.0
        self.moveTimer = Timer()
        self.galilTimer = Timer()

        FakeDev.__init__(self,
            name = name,
            port = port,
        )

    def statusStr(self):
        return "State=%s Ori=%s Lamps=%s Galil=%s"%(
                self.moveState,
                ",".join(["%.2f"%val for val in self.orientation]),
                self.lamps,
                self.galil
            )

    def parseCmdStr(self, cmdStr):
        """Parse an incoming command, make it somewhat
        like the way c100.cpp does things.

        how to offsets behave, are they sticky, not here
        """
        try:
            tokens = cmdStr.strip().split(" ")

            # status requests
            if tokens[0].lower() == "status":
                # status
               self.userSock.writeLine(self.statusStr())
            elif tokens[0].lower() == "speed":
                self.userSock.writeLine("%.1f"%self.speed)
            elif tokens[0].lower() in ["move", "offset"] and len(tokens)==1:
                self.userSock.writeLine(" ".join(["%.2f"%val for val in self.orientation]))
            elif tokens[0].lower() in ["focus", "dfocus"] and len(tokens)==1:
                self.userSock.writeLine("%.1f"%self.orientation[0])

            # commands
            elif tokens[0].lower() == "stop":
                self.doMove(stop=True)
                self.userSock.writeLine("OK")
            elif tokens[0].lower() in ["move", "focus", "offset", "dfocus"]:
                isOffset = tokens[0].lower() in ["offset", "dfocus"]
                for ind, value in enumerate(tokens[1:]):
                    if isOffset:
                        self.targOrientation[ind] += float(value)
                    else:
                        self.targOrientation[ind] = float(value)
                self.doMove()
                self.userSock.writeLine("OK")
            elif tokens[0].lower() == "galil":
                if tokens[1].lower() == "on":
                    self.powerup()
                else:
                    self.powerdown()
                self.userSock.writeLine("OK")


            else:
                # unknown command?
                raise RuntimeError("Unknown Command: %s"%cmdStr)
        except Exception as e:
            self.userSock.writeLine("-1") # error!
            print("Error: ", e)

    def powerdown(self):
        self.galil = self.Off

    def powerup(self, doMove=False):
        self.moveState = self.Moving
        self.galilTimer.start(2., self.setDone, doMove)

    def setDone(self, doMove=False):
        self.galil = self.On
        if doMove:
            self.doMove()
        else:
            self.moveState = self.Done

    def doMove(self, stop=False):
        """stop: halt focus at it's current location
        """
        if stop:
            self.moveTimer.cancel()
            self.moveState = self.Done
            self.galil = self.Off
            return
        if not self.galil == self.On:
            # simulate power up time
            self.powerup(doMove=True)
            # move will start after powerup
            return
        self.moveState = self.Moving
        focus = self.incrementPosition(self.targOrientation[0], self.orientation[0], FocusStepSize)
        self.orientation[0] = focus
        if focus != self.targOrientation[0]:
            # continue moving
            self.moveTimer.start(TimerDelay, self.doMove)
        else:
            self.orientation = self.targOrientation[:] # copy is necessary!!!
            self.moveState = self.Done

    def stateCallback(self, server=None):
        if self.isReady:
            # self.readyDeferred.callback(None)
            print("Fake M2 controller %s running on port %s" % (self.name, self.port))
        elif self.didFail and not self.readyDeferred.called:
            errMsg = "Fake M2 controller %s failed to start on port %s" % (self.name, self.port)
            print(errMsg)
예제 #35
0
class FakeScaleCtrl(FakeDev):
    """!A server that emulates the LCO scale
    """
    def __init__(self, name, port):
        """!Construct a fake LCO scale controller

        @param[in] name  name of scale controller
        @param[in] port  port on which to command scale controller
        """
        global GlobalScalePosition
        self.isMoving = False
        self.moveRange = [0., 40.]
        self.desPosition = GlobalScalePosition # wake up in mid travel
        self.position = GlobalScalePosition
        self.speed = 0.5
        self.moveTimer = Timer()
        self.posSw1, self.posSw2, self.posSw3 = (1, 1, 1)
        self.cartID = 0
        self.lockPos = 18
        # faults
        self.trOT = False
        self.trHWF, self.trIF = 0, 0
        self.lockHWF, self.lockIF = 0, 0
        self.winchHWF, self.winchIF = 0, 0
        self.positionTimer = Timer()
        self.userSock = None # created upon connection
        FakeDev.__init__(self,
            name = name,
            port = port,
            doEcho = True,
        )
        # self.sendPositionLoop()
        # self.moveLoop()

    def moveLoop(self):
        # loops always
        global GlobalScalePosition
        self.position = self.incrementPosition(self.desPosition, self.position, self.speed*TimerDelay)
        GlobalScalePosition = self.position
        if self.position == self.desPosition:
            # move is done, send OK
            self.userSock.writeLine("OK")
            self.isMoving = False
        else:
            # keep moving
            self.moveTimer.start(TimerDelay/10., self.moveLoop)

    def parseCmdStr(self, cmdStr):
        if "status" in cmdStr.lower():
            self.sendStatusAndOK()
        elif "move" in cmdStr.lower():
            desPos = float(cmdStr.split()[-1])
            if not self.moveRange[0] <= desPos <= self.moveRange[1]:
                self.userSock.writeLine("ERROR OUT_OF_RANGE")
                self.userSock.writeLine("OK")
            else:
                self.desPosition = desPos
                self.isMoving = True
                self.moveTimer.start(0, self.moveLoop)
                self.sendPositionLoop()
        elif "speed" in cmdStr.lower():
            self.speed = float(cmdStr.split()[-1])
            self.userSock.writeLine("OK")
        elif "stop" in cmdStr.lower():
            self.stop()
        else:
            # unrecognized command
            self.userSock.writeLine("ERROR INVALID_COMMAND")
            self.userSock.writeLine("OK")

    def stop(self):
        # moveLoop will send the ok
        self.desPosition = self.position
        self.moveTimer.start(0, self.moveLoop)

    def sendPositionLoop(self):
        # only write if we are "moving"
        if self.userSock is not None and self.isMoving == True:
            currPosStr = "__ACTUAL_POSITION %.6f"%self.position
            self.userSock.writeLine(currPosStr)
            self.positionTimer.start(TimerDelay, self.sendPositionLoop)

    # def sendStatusAndOK(self):
    #     statusLines = [
    #         "CURRENT_POSITION %.6f"%self.position,
    #         "TARGET_POSITION %.6f"%self.desPosition,
    #         "CARTRIDGE_ID 4",
    #         "DRIVE_SPEED 23",
    #         "DRIVE_ACCEL 3",
    #         "MOVE_RANGE %.1f - %.1f"%(self.moveRange[0], self.moveRange[1]),
    #         "HARDWARE FAULT NONE",
    #         "INSTRUCTION_FAULT NONE",
    #         "OK"
    #     ]
    #     for line in statusLines:
    #         self.userSock.writeLine(line)

    def sendStatusAndOK(self):
        # global munge
        # munge = munge*-1
        # if munge == 1:
        #
        # else:
        #     pos = "MUNGED"
        pos = "%.7f"%self.position
        statusLines = [
            "THREAD_RING_AXIS:",
            "__ACTUAL_POSITION %s"%pos,
            "__TARGET_POSITION 0.20000000",
            "__DRIVE_STATUS: OFF",
            "__MOTOR_CURRENT: -0.39443308",
            "__DRIVE_SPEED %.7f"%self.speed,
            "__DRIVE_ACCEL 20",
            "__DRIVE_DECEL 20",
            "__MOVE_RANGE %.4f - %.4f"%tuple(self.moveRange),
            "__HARDWARE_FAULT %i"%(self.trHWF),
            "__INSTRUCTION_FAULT %i"%(self.trIF),
            "__THREADRING_OVERTRAVEL_%s"%("ON" if bool(self.trOT) else "OFF"),
            "LOCK_RING_AXIS:",
            "__ACTUAL_POSITION %.7f"%(self.lockPos),
            "__TARGET_POSITION 18.0000000",
            "__OPEN_SETPOINT: 150.000000",
            "__LOCKED_SETPOINT: 18.0000000",
            "__DRIVE_STATUS: OFF",
            "__MOTOR_CURRENT: 0.0",
            "__DRIVE_SPEED 50.0000000",
            "__DRIVE_ACCEL 20",
            "__DRIVE_DECEL 20",
            "__MOVE_RANGE 0.0 - 152.399994",
            "__HARDWARE_FAULT %i"%(self.lockHWF),
            "__INSTRUCTION_FAULT %i"%(self.lockIF),
            "WINCH_AXIS:",
            "__ACTUAL_POSITION -1840.48157",
            "__TARGET_POSITION 1652.00000",
            "__UP_SETPOINT: 23.0000000",
            "__TO_CART_SETPOINT: 1560.00000",
            "__ON_CART_SETPOINT: 1652.00000",
            "__RELEASE_SETPOINT: 1695.00000",
            "__DRIVE_STATUS: OFF",
            "__MOTOR_CURRENT: -0.02553883",
            "__DRIVE_SPEED 50.0000000",
            "__DRIVE_ACCEL 2",
            "__DRIVE_DECEL 2",
            "__MOVE_RANGE 0.0 - 3000.00000",
            "__HARDWARE_FAULT %i"%self.winchHWF,
            "__INSTRUCTION_FAULT %i"%self.winchIF,
            "SCALE_1: 1.70607793",
            "SCALE 2: 1.66883636",
            "SCALE 3: -0.07550588",
            "CARTRIDGE_ID %i"%self.cartID,
            "__ID_SW: 0 1 2 3 4 5 6 7 8",
            "         0 0 0 0 0 0 0 0 0",
            "__POS_SW: 1 2 3",
            "          %i %i %i"%(self.posSw1, self.posSw2, self.posSw3),
            "WINCH_HOOK_SENSOR: OFF",
            "WINCH_ENCODER_1_POS: 0.0",
            "WINCH_ENCODER_2_POS: 0.0",
            "WINCH_ENCODER_3_POS: 0.0",
            "OK",
        ]
        for line in statusLines:
            self.userSock.writeLine(line)


    def stateCallback(self, server=None):
        if self.isReady:
            # self.readyDeferred.callback(None)
            print("Fake scale controller %s running on port %s" % (self.name, self.port))
        elif self.didFail and not self.readyDeferred.called:
            errMsg = "Fake scale controller %s failed to start on port %s" % (self.name, self.port)
            print(errMsg)
예제 #36
0
 def connCallback(self, conn):
     """!Callback for device connection state
     """
     if self.dev.conn.isDone:
         Timer(0, self.finish)
예제 #37
0
 def __init__(self):
     self.moveTimer = Timer()
예제 #38
0
class DisconnectDevice(object):
    """!Execute dev.init (if the device is fully connected) and disconnect a device

    If the device is already disconnected then generate a new userCmd, if needed,
    and sets userCmd's state to userCmd.Done.

    Public attributes:
    - dev: the provided device
    - userCmd: the provided userCmd, or a new one if none provided
    """
    def __init__(self, dev, userCmd, timeLim):
        """!Start disconnecting a device
        """
        self.dev = dev
        self.userCmd = expandUserCmd(userCmd)
        self._timeLim = timeLim
        self._connTimer = Timer()
        self._addedConnCallback = False

        if self.dev.conn.isDisconnected:
            if self.dev.state != self.dev.Disconnected:
                self.dev.setState(self.dev.Disconnected, "socket disconnected")
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)
            return

        self.dev._ignoreConnCallback = True
        if self.dev.state != self.dev.Disconnected:
            self.dev.setState(self.dev.Disconnecting)

        if self.dev.conn.isConnected:
            initUserCmd = UserCmd(cmdStr="Init via device.DisconnectDevice",
                                  callFunc=self.initCallback,
                                  timeLim=timeLim)
            self.dev.init(userCmd=initUserCmd,
                          timeLim=timeLim,
                          getStatus=False)
        else:
            # not fully connected, so cannot send init, but not fully disconnected yet, so finish disconnecting
            textMsg = "%s connection state=%s; cannot initialize before disconnecting" % (
                self.dev.name, self.dev.conn.state)
            self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg), ))
            self.startDisconnect()
            return

    def startDisconnect(self):
        """!Start disconnecting the connection
        """
        if self.dev.conn.isDone and not self.dev.conn.isConnected:
            # fully disconnected; no more to be done
            Timer(0, self.finish)
        else:
            if self._timeLim:
                # start timer for disconnection
                self._connTimer.start(self._timeLim, self.finish,
                                      "timed out waiting for disconnection")

            self.dev.conn.addStateCallback(self.connCallback)
            self._addedConnCallback = True
            self.dev.conn.disconnect()

    def initCallback(self, initUserCmd):
        """!Callback for device initialization
        """
        if not initUserCmd.isDone:
            return

        if initUserCmd.didFail:
            textMsg = "%s initialization failed: %s" % (
                self.dev.name,
                initUserCmd.textMsg,
            )
            self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg), ))
        self.startDisconnect()

    def connCallback(self, conn):
        """!Callback for device connection state
        """
        if self.dev.conn.isDone:
            Timer(0, self.finish)

    def finish(self, reason=None):
        """!Call on success or failure to finish the command

        @param[in] reason: reason for failure (if non-empty then failure is assumed)
        """
        self._connTimer.cancel()
        self.dev._ignoreConnCallback = False
        if self._addedConnCallback:
            self.dev.conn.removeStateCallback(self.connCallback)
        if reason or not self.dev.conn.isDone or not self.dev.conn.isDisconnected:
            reason = reason or "unknown reasons"
            self.dev.setState(self.dev.Failed, reason)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Failed,
                                      textMsg="%s failed to disconnect: %s" %
                                      (self.dev, reason))
        else:
            if self.dev.state != self.dev.Disconnected:
                self.dev.setState(self.dev.Disconnected)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)
        self.dev.cleanup()

    def __repr__(self):
        return "%s(dev.name=%s)" % (type(self).__name__, self.dev.name)
예제 #39
0
 def diffuOut(self):
     Timer(0.2, self._setDiffuPos, 0)  # set on a timer to emulate move time
예제 #40
0
 def __init__(self):
     self.moveTimer = Timer()
예제 #41
0
class DisconnectDevice(object):
    """!Execute dev.init (if the device is fully connected) and disconnect a device

    If the device is already disconnected then generate a new userCmd, if needed,
    and sets userCmd's state to userCmd.Done.

    Public attributes:
    - dev: the provided device
    - userCmd: the provided userCmd, or a new one if none provided
    """
    def __init__(self, dev, userCmd, timeLim):
        """!Start disconnecting a device
        """
        self.dev = dev
        self.userCmd = expandUserCmd(userCmd)
        self._timeLim = timeLim
        self._connTimer = Timer()
        self._addedConnCallback = False

        if self.dev.conn.isDisconnected:
            if self.dev.state != self.dev.Disconnected:
                self.dev.setState(self.dev.Disconnected, "socket disconnected")
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)
            return

        self.dev._ignoreConnCallback = True
        if self.dev.state != self.dev.Disconnected:
            self.dev.setState(self.dev.Disconnecting)

        if self.dev.conn.isConnected:
            initUserCmd = UserCmd(cmdStr="Init via device.DisconnectDevice", callFunc=self.initCallback, timeLim=timeLim)
            self.dev.init(userCmd=initUserCmd, timeLim=timeLim, getStatus=False)
        else:
            # not fully connected, so cannot send init, but not fully disconnected yet, so finish disconnecting
            textMsg = "%s connection state=%s; cannot initialize before disconnecting" % (self.dev.name, self.dev.conn.state)
            self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg),))
            self.startDisconnect()
            return

    def startDisconnect(self):
        """!Start disconnecting the connection
        """
        if self.dev.conn.isDone and not self.dev.conn.isConnected:
            # fully disconnected; no more to be done
            Timer(0, self.finish)
        else:
            if self._timeLim:
                # start timer for disconnection
                self._connTimer.start(self._timeLim, self.finish, "timed out waiting for disconnection")

            self.dev.conn.addStateCallback(self.connCallback)
            self._addedConnCallback = True
            self.dev.conn.disconnect()

    def initCallback(self, initUserCmd):
        """!Callback for device initialization
        """
        if not initUserCmd.isDone:
            return

        if initUserCmd.didFail:
            textMsg = "%s initialization failed: %s" % (self.dev.name, initUserCmd.textMsg,)
            self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg),))
        self.startDisconnect()

    def connCallback(self, conn):
        """!Callback for device connection state
        """
        if self.dev.conn.isDone:
            Timer(0, self.finish)

    def finish(self, reason=None):
        """!Call on success or failure to finish the command

        @param[in] reason: reason for failure (if non-empty then failure is assumed)
        """
        self._connTimer.cancel()
        self.dev._ignoreConnCallback = False
        if self._addedConnCallback:
            self.dev.conn.removeStateCallback(self.connCallback)
        if reason or not self.dev.conn.isDone or not self.dev.conn.isDisconnected:
            reason = reason or "unknown reasons"
            self.dev.setState(self.dev.Failed, reason)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Failed, textMsg="%s failed to disconnect: %s" % (self.dev, reason))
        else:
            if self.dev.state != self.dev.Disconnected:
                self.dev.setState(self.dev.Disconnected)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)
        self.dev.cleanup()

    def __repr__(self):
        return "%s(dev.name=%s)" % (type(self).__name__, self.dev.name)
예제 #42
0
class Socket(BaseSocket):
    """A socket using Twisted framework.
    """
    def __init__(self,
        endpoint = None,
        protocol = None,
        state = BaseSocket.Connected,
        readCallback = nullCallback,
        stateCallback = nullCallback,
        timeLim = None,
        name = "",
    ):
        """Construct a Socket

        Inputs:
        - endpoint  a Twisted endpoint, e.g. twisted.internet.endpoints.TCP4ClientEndpoint;
        - protocol  a Twisted protocol;
            you must either specify endpoint or protocol, but not both
        - state     the initial state
        - readCallback  function to call when data read; receives: self
        - stateCallback a state callback function; see addStateCallback for details
        - timeLim   time limit to make connection (sec); no limit if None or 0
        - name      a string to identify this socket; strictly optional
        """
        #print "Socket(name=%r, endpoint=%r, protocol=%r, state=%r, readCallback=%r, stateCallback=%r)" % \
        #    (name, endpoint, protocol, state, readCallback, stateCallback)
        if bool(endpoint is None) == bool(protocol is None):
            raise RuntimeError("Must provide one of endpoint or protocol")
        self._endpoint = endpoint
        self._endpointDeferred = None
        self._protocol = None
        self._data = None
        self._connectTimer = Timer()
        BaseSocket.__init__(self,
            state = state,
            readCallback = readCallback,
            stateCallback = stateCallback,
            name = name
        )
        if protocol is not None:
            self._connectionMade(protocol)
        else:
            if timeLim:
                self._connectTimer.start(timeLim, self._connectTimeout)
            self._setState(BaseSocket.Connecting)
            self._endpointDeferred = self._endpoint.connect(_SocketProtocolFactory())
            setCallbacks(self._endpointDeferred, self._connectionMade, self._connectionLost)

    def _clearCallbacks(self):
        """Clear any callbacks added by this class. Called just after the socket is closed.
        """
        BaseSocket._clearCallbacks(self)
        self._connCallback = None
        if self._endpointDeferred is not None:
            self._endpointDeferred.cancel()
            self._endpointDeferred = None
        if self._protocol is not None:
            self._protocol.roAbort()
            self._protocol = None

    def _connectionLost(self, reason):
        """Connection lost callback
        """
        #print "%s._connectionLost(reason=%r)" % (self, reason)
        if reason is None:
            reasonStrOrNone = None
        elif isinstance(reason, ConnectionDone):
            # connection closed cleanly; no need for a reason
            # use getattr in case reason as no type attribute
            reasonStrOrNone = None
        else:
            reasonStrOrNone = str(reason)

        if self._state == BaseSocket.Closing:
            self._setState(BaseSocket.Closed, reasonStrOrNone)
        else:
            self._setState(BaseSocket.Failed, reasonStrOrNone)
    
    def _connectionMade(self, protocol):
        """Callback when connection made
        """
        #print "%s._connectionMade(protocol=%r)" % (self, protocol)
        self._protocol = protocol
        self._protocol.roSetCallbacks(
            readCallback = self._doRead,
            connectionLostCallback = self._connectionLost,
        )
        self._setState(BaseSocket.Connected)

    def _basicClose(self):
        """Close the socket.
        """
        if self._protocol is not None:
            self._protocol.transport.loseConnection()

    @property
    def host(self):
        """Return the address, or None if not known
        """
        if self._protocol:
            return getattr(self._protocol.transport.getPeer(), "host", None)
        return None

    @property
    def port(self):
        """Return the port, or None if unknown
        """
        if self._protocol:
            return getattr(self._protocol.transport.getPeer(), "port", None)
        return None
    
    def read(self, nChar=None):
        """Read data. Do not block.
        
        Inputs:
        - nChar: maximum number of chars to return; if None then all available data is returned.
        
        Raise RuntimeError if the socket is not connected.
        """
        if not self.isReady:
            raise RuntimeError("%s not connected" % (self,))
        return self._protocol.read(nChar)
    
    def readLine(self, default=None):
        """Read one line of data, not including the end-of-line indicator. Do not block.
        
        Any of \r\n, \r or \n are treated as end of line.
        
        Inputs:
        - default   value to return if a full line is not available
                    (in which case no data is read)
        
        Raise RuntimeError if the socket is not connected.
        """
        if not self.isReady:
            raise RuntimeError("%s not connected" % (self,))
        return self._protocol.readLine(default)
    
    def write(self, data):
        """Write data to the socket (without blocking)
        
        Safe to call as soon as you call connect, but of course
        no data is sent until the connection is made.
        
        Raise UnicodeError if the data cannot be expressed as ascii.
        Raise RuntimeError if the socket is not connecting or connected.
        If an error occurs while sending the data, the socket is closed,
        the state is set to Failed and _reason is set.
        
        An alternate technique (from Craig):
        turn } into \}; consider escaping null and all but
        the final \n in the same fashion
        (to do this it probably makes sense to supply a writeLine
        that escapes \n and \r and then appends \n).
        Then:
        self._tk.eval('puts -nonewline %s { %s }' % (self._sock, escData))
        """
        #print "%s.write(%r)" % (self, data)
        if not self.isReady:
            raise RuntimeError("%s.write(%r) failed: not connected" % (self, data))
        self._protocol.transport.write(str(data))
    
    def writeLine(self, data):
        """Write a line of data terminated by standard newline
        """
        #print "%s.writeLine(data=%r)" % (self, data)
        self.write(data + "\r\n")
    
    def _connectTimeout(self):
        """Call if connection times out
        """
        if not self.isReady or self.isDone:
            self.close(isOK=False, reason="timeout")
    
    def _doRead(self, sock):
        """Called when there is data to read
        """
        if not self.isReady:
            return
        if self._readCallback:
            try:
                self._readCallback(self)
            except Exception as e:
                sys.stderr.write("%s read callback %s failed: %s\n" % (self, self._readCallback, e,))
                traceback.print_exc(file=sys.stderr)

    def _setState(self, *args, **kwargs):
        BaseSocket._setState(self, *args, **kwargs)
        if self.isReady or self.isDone:
            self._connectTimer.cancel()
예제 #43
0
class FakeTCS(FakeDev):
    """!A server that emulates the LCO TCS

    defined here: http://espejo.lco.cl/operations/TCS_communication.html
    """
    Idle = 1
    Tracking = 2
    Slewing = 3
    Stop = 4
    def __init__(self, name, port):
        """!Construct a fake LCO TCS

        @param[in] name  name of TCS controller
        @param[in] port  port on which to command TCS
        """
        self.rstop = 0
        self.ractive = 0
        self.rmoving = 0
        self.rtracking = 0
        self.dstop = 0
        self.dactive = 0
        self.dmoving = 0
        self.dtracking = 0
        self.istop = 0
        self.iactive = 0
        self.imoving = 0

        self.isClamped = 1
        self.targRot = 0.
        self.rot = 0.
        self.focus = 0.
        self.targFocus = 0.
        self.ra = 0.
        self.dec = 0.
        self.ha = 0.
        self.targRA = 0.
        self.targDec = 0.
        self.offDec = 0.
        self.offRA = 0.
        self.epoch = 2000
        self.telState = self.Idle
        self.focusTimer = Timer()
        self.slewTimer = Timer()
        self.rotTimer = Timer()

        FakeDev.__init__(self,
            name = name,
            port = port,
        )

    @property
    def onTarget(self):
        return self.targRA == self.ra and self.targDec == self.dec

    def parseCmdStr(self, cmdStr):
        """Parse an incoming command, make it somewhat
        like the way c100.cpp does things.

        how to offsets behave, are they sticky, not here
        """
        try:
            tokens = cmdStr.strip().split(" ")

            # status requests
            if tokens[0] == "RA" and len(tokens) == 1:
                # report ra in HMS
               self.userSock.writeLine(dmsStrFromDeg(self.ra / 15.))
            elif tokens[0] ==  "DEC" and len(tokens) == 1:
               self.userSock.writeLine(dmsStrFromDeg(self.dec))
            elif tokens[0] ==  "HA" and len(tokens) == 1:
               self.userSock.writeLine(dmsStrFromDeg(self.ha))
            elif tokens[0] ==  "POS" and len(tokens) == 1:
               self.userSock.writeLine("%.4f %.4f"%(numpy.radians(self.ha), numpy.radians(self.dec)))
            elif tokens[0] ==  "MPOS" and len(tokens) == 1:
               self.userSock.writeLine("%.4f %.4f"%(numpy.radians(self.ra), numpy.radians(self.dec)))
            elif tokens[0] ==  "EPOCH" and len(tokens) == 1:
               self.userSock.writeLine("%.2f"%(2000))
            elif tokens[0] ==  "ZD" and len(tokens) == 1:
               self.userSock.writeLine("%.2f"%(80))
            elif tokens[0] == "STATE" and len(tokens) == 1:
               self.userSock.writeLine(str(self.telState))
            elif tokens[0] == "INPRA" and len(tokens) == 1:
               self.userSock.writeLine(str(self.targRA))
            elif tokens[0] == "INPDC" and len(tokens) == 1:
               self.userSock.writeLine(str(self.targDec))
            elif tokens[0] == "TELEL" and len(tokens) == 1:
                self.userSock.writeLine(str(85.2)) # placeholder
            elif tokens[0] == "TELAZ" and len(tokens) == 1:
                self.userSock.writeLine(str(40.6)) #placeholder
            elif tokens[0] == "ROT" and len(tokens) == 1:
                self.userSock.writeLine(str(30.6)) #placeholder
            elif tokens[0] == "MRP" and len(tokens) == 1:
                mrpLine = "%i 0 0 1 3"%(self.isClamped)
                self.userSock.writeLine(mrpLine)
            elif tokens[0] == "TEMPS" and len(tokens) == 1:
                self.userSock.writeLine("18.8 10.8 12.0 11.5 8.8 13.1 -273.1 -273.1")
            elif tokens[0] == "ST" and len(tokens) == 1:
                self.userSock.writeLine("06:59:29")
            elif tokens[0] == "TTRUSS" and len(tokens) == 1:
                self.userSock.writeLine("10.979")
            elif tokens[0] == "INPHA" and len(tokens) == 1:
                self.userSock.writeLine("0")
            elif tokens[0] == "RAWPOS" and len(tokens) == 1:
                self.userSock.writeLine("1 1 1 1 1")
            elif tokens[0] == "AXISSTATUS" and len(tokens) == 1:
                axisLine = "%i %i %i %i %i %i %i %i %i %i %i" % (
                    self.rstop, self.ractive, self.rmoving, self.rtracking,
                    self.dstop, self.dactive, self.dmoving, self.dtracking,
                    self.istop, self.iactive, self.imoving
                )
                self.userSock.writeLine(axisLine)

            # commands
            elif tokens[0] == "RAD":
                assert len(tokens) == 2, "Error Parsing RAD"
                self.targRA = float(tokens[1])
                self.userSock.writeLine("0")
            elif tokens[0] == "DECD":
                assert len(tokens) == 2, "Error Parsing DECD"
                self.targDec = float(tokens[1])
                self.userSock.writeLine("0")
            elif tokens[0] == "OFDC":
                assert len(tokens) == 2, "Error Parsing OFDC"
                # convert from arcseconds to degrees
                self.offDec = float(tokens[1]) / ArcsecPerDeg
                self.userSock.writeLine("0")
            elif tokens[0] == "OFRA":
                assert len(tokens) == 2, "Error Parsing OFRA"
                # convert from arcseconds to degrees
                self.offRA = float(tokens[1]) / ArcsecPerDeg
                self.userSock.writeLine("0")
            elif tokens[0] == "OFFP":
                assert len(tokens) == 1, "Error Parsising Offset Execute"
                self.targRA += self.offRA
                self.targDec += self.offDec
                self.offRA, self.offDec = 0., 0.
                self.doSlew()
                self.userSock.writeLine("0")
            elif tokens[0] == "UNCLAMP":
                self.isClamped = 0
                self.userSock.writeLine("0")
            elif tokens[0] == "CLAMP":
                self.isClamped = 1
                self.userSock.writeLine("0")
            elif tokens[0] == "APGCIR":
                assert len(tokens) == 2, "Error Parsising APGCIR Execute"
                self.targRot = float(tokens[1])
                self.doRot()
                self.userSock.writeLine("0")
            elif tokens[0] == "DCIR":
                assert len(tokens) == 2, "Error Parsising DCIR Execute"
                assert self.isClamped == 0, "Rotator is clamped"
                self.targRot += float(tokens[1])
                self.doRot()
                self.userSock.writeLine("0")
            elif tokens[0] == "SLEW":
                raise RuntimeError("SLEWS NOT ALLOWED")
                # slew to target
                self.doSlew()
                self.userSock.writeLine("0")
            elif tokens[0] == "MP":
                # set epoch
                self.MP = float(tokens[1])
                self.userSock.writeLine("0")
            elif tokens[0] == "FOCUS":
                raise RuntimeError("DON'T USE TCS FOR FOCUS!")
                if len(tokens) == 1:
                   self.userSock.writeLine(str(self.focus))
                else:
                    assert len(tokens) == 2, "Error Parsing Focus"
                    focusArg = tokens[1].upper()
                    if focusArg == "STOP":
                        self.doFocus(stop=True)
                    elif focusArg == "GO":
                        self.doFocus()
                    else:
                        # input new desired focus value and move
                        self.targFocus = float(focusArg)
                        self.doFocus()
                    self.userSock.writeLine("0")
            elif tokens[0] == "DFOCUS":
                assert len(tokens) == 2, "Error Parsing DFocus"
                # input new desired focus value and move
                self.targFocus += float(tokens[1])
                self.doFocus()
                self.userSock.writeLine("0")

            else:
                # unknown command?
                raise RuntimeError("Unknown Command: %s"%cmdStr)
        except Exception as e:
            self.userSock.writeLine("-1") # error!
            print("Error: ", e)

    def doSlew(self, offset=False):
        if not offset:
            # offset doesn't trigger slewing state
            self.telState = self.Slewing
        self.dec = self.incrementPosition(self.targDec, self.dec, AxisStepSize)
        self.ra = self.incrementPosition(self.targRA, self.ra, AxisStepSize)
        if self.onTarget:
            # slew done!
            self.telState = self.Tracking
        else:
            self.slewTimer.start(TimerDelay, self.doSlew)

    def doRot(self):
        self.rot = self.incrementPosition(self.targRot, self.rot, RotStepSize)
        if self.rot != self.targRot:
            self.rotTimer.start(TimerDelay, self.doRot)

    def doFocus(self, stop=False):
        """stop: halt focus at it's current location
        """
        if stop:
            self.focusTimer.cancel()
            return
        self.focus = self.incrementPosition(self.targFocus, self.focus, FocusStepSize)
        if self.focus != self.targFocus:
            # continue moving
            self.focusTimer.start(TimerDelay, self.doFocus)


    def stateCallback(self, server=None):
        if self.isReady:
            # self.readyDeferred.callback(None)
            print("Fake TCS controller %s running on port %s" % (self.name, self.port))
        elif self.didFail and not self.readyDeferred.called:
            errMsg = "Fake TCS controller %s failed to start on port %s" % (self.name, self.port)
            print(errMsg)
예제 #44
0
class Camera(object):

    def __init__(self):
        """A pure python fake camera
        """
        self.state = Idle
        self.currExposure = Exposure(None, None, None)
        self.expTimer = Timer()
        self.readTimer = Timer()
        self.expBegin = None
        self.expAccumulated = None
        self.setConfig(CameraConfig())
        self._expName = ""
        self.failExposure = False # for unittesting

    def isBusy(self):
        return self.state != Idle

    def startExposure(self, expTime, expType, name):
        if self.failExposure:
            raise RuntimeError("startExposure() failed!")
        assert expType in [Bias, Dark, Flat, Object]
        assert not self.isBusy()
        self.currExposure = Exposure(expTime, expType, name)
        self.expAccumulated = 0
        self._expName = name
        self._startOrResumeExposure(expTime)

    def pauseExposure(self):
        assert self.state == Exposing
        self.expAccumulated = self.expAccumulated + time.time() - self.expBegin
        self._cancelTimers()
        self._setState(Paused)

    def resumeExposure(self):
        assert self.state == self.Paused
        remExpTime = self.currExposure.expTime - self.expAccumulated
        self._startOrResumeExposure(remExpTime)

    def abortExposure(self):
        assert self.state == Exposing
        self._cancelTimers()
        self._setState(Idle)

    def stopExposure(self):
        assert self.state == Exposing
        self._cancelTimers()
        self._setState(ImageRead)

    def getExposureState(self):
        expTime = self.currExposure.expTime
        if self.state != Idle:
            timeRemaining = expTime - (time.time() - self.expBegin)
        else:
            timeRemaining = 0.
        return ExposureState(self.state, expTime or 0., timeRemaining)

    def getConfig(self):
        return self._config

    def setConfig(self, config):
        config.assertValid()
        self._assertIdle()
        self._config = config;

    def saveImage(self, expTime=-1):
        # save an empty file
        # fileName = "%s_%s_%s.fakeImage"%(self.currExposure.expType, self.currExposure.expTime, self.currExposure.name)
        # print "exptype", self.currExposure.expType
        # print "expTime", self.currExposure.expTime
        # print "expName", self.currExposure.name
        n = numpy.arange(100.0) # a simple sequence of floats from 0.0 to 99.9
        hdu = fits.PrimaryHDU(n)
        hdulist = fits.HDUList([hdu])
        hdulist[0].header["comment"] = "This is a fake image, used for testing."
        hdulist.writeto(self.currExposure.name)
        # open(self.currExposure.name, "w").close()
        self.state = Idle

    def openShutter(self):
        pass

    def closeShutter(self):
        pass

    def _setState(self, state):
        assert state in [Idle, Exposing, Paused, Reading, ImageRead]
        self.state = state

    def _startOrResumeExposure(self, expTime):
        self._setState(Exposing)
        self.expTimer.start(expTime, self._setState, Reading)
        self.readTimer.start(expTime+readTime, self._setState, ImageRead)
        self.expBegin = time.time()

    def _cancelTimers(self):
        self.readTimer.cancel()
        self.expTimer.cancel()

    def _assertIdle(self):
        if not self.state == Idle:
            raise RuntimeError("busy")
예제 #45
0
class CommandQueue(object):
    """!A command queue.  Default behavior is to queue all commands and
    execute them one at a time in order of priority.  Equal priority commands are
    executed in the order received.  Special rules may be defined for handling special cases
    of command collisions.
    """
    Immediate = 'immediate'
    CancelNew = 'cancelnew'
    CancelQueued = 'cancelqueued'
    KillRunning = 'killrunning'
    _AddActions = frozenset((CancelNew, CancelQueued, KillRunning))
    def __init__(self, priorityDict, killFunc=None):
        """ This is an object which keeps track of commands and smartly handles
            command collisions based on rules chosen by you.
            @param[in] priorityDict a dictionary keyed by cmdVerb, with integer values or Immediate
            @ param[in] killFunc: a function to call when a running command needs to be
                killed.  Accepts 2 parameters, the command to be canceled, and the command doing the killing.
                This function must eventually ensure that the running command is canceled safely
                allowing for the next queued command to go. Or None
        """
        self.cmdQueue = []
        dumCmd = UserCmd()
        dumCmd.setState(dumCmd.Done)
        dumCmd.cmdVerb = 'dummy'
        self.currExeCmd = QueuedCommand(dumCmd, 0, lambda cmdVar: None)
        self.priorityDict = priorityDict
        self.killFunc = killFunc
        self.ruleDict = {}
        self.queueTimer = Timer()
        self._enabled = True

    def __getitem__(self, ind):
        return self.cmdQueue[ind]

    def __len__(self):
        return len(self.cmdQueue)

    def addRule(self, action, newCmds="all", queuedCmds="all"):
        """!Add special case rules for collisions.

        @param[in] action  one of CancelNew, CancelQueued, KillRunning
        @param[in] newCmds  a list of incoming commands to which this rule applies or "all"
        @param[in] queuedCmds  a list of the commands (queued or running) to which this rule applies or "all"

        See documentation for the addCommand method to learn exactly how rules are evaluated
        """
        if action == self.KillRunning and self.killFunc is None:
            raise RuntimeError("must supply killFunc in CommandQueue constructor before specifying a KillRunning rule")
        checkCmds = []
        if newCmds != "all":
            checkCmds = checkCmds + newCmds
        else:
            newCmds = ["all"]
        if queuedCmds != "all":
            checkCmds = checkCmds + queuedCmds
        else:
            queuedCmds = ["all"]
        for cmdName in checkCmds:
            if cmdName not in self.priorityDict:
                raise RuntimeError('Cannot add rule to unrecognized command: %s' % (cmdName,))
        if action not in self._AddActions:
            raise RuntimeError("Rule action=%r must be one of %s" % (action, sorted(self._AddActions)))
        for nc in newCmds:
            if not nc in self.ruleDict:
                if nc == 'all':
                    # ensure any rules defined in self.ruleDict[:]["all"]
                    # are the same action:
                    for existingDict in self.ruleDict.itervalues():
                        if "all" in existingDict and action != existingDict["all"]:
                            raise RuntimeError("May not specifiy conflicting rules pertaining to all queued commands and all new commands")
                self.ruleDict[nc] = {}
            for qc in queuedCmds:
                if qc in self.ruleDict[nc] and self.ruleDict[nc][qc] != action:
                    raise RuntimeError(
                        'Cannot set rule=%r for new command=%s vs. queued command=%s: already set to %s' % \
                        (action, nc, qc, self.ruleDict[nc][qc])
                    )
                if qc == "all":
                    if "all" in self.ruleDict:
                        for value in self.ruleDict["all"].itervalues():
                            if value != action:
                                raise RuntimeError("May not specifiy conflicting rules pertaining to all queued commands and all new commands")
                self.ruleDict[nc][qc] = action

    def getRule(self, newCmd, queuedCmd):
        """!Get the rule for a specific new command vs. a specific queued command.

        @param[in] newCmd  the incoming command verb
        @param[in] queuedCmd  a command verb currently on the queue
        @return a rule, one of self._AddActions

        Note: there is some logic to determine which rule is grabbed:

        If a specific rule exists between the newCmd and queuedCmd, that is returned. This trumps
        the situation in which there may be a rule defined involving newCmd or queuedCmd pertaining to
        all new commands or all queued commands.

        If newCmd and queuedCmd are the same command and are not present in the ruleDict nor priorityDict
        return the default rule of CancelQueued (effectively the new cmd will replace the one on queue)

        If no specific rule exists between newCmd and queuedCmd, check if a rule is defined between both:
            1. newCmd and all queued commands
            2. all new commands and queuedCmd
        If a different rule is present for both 1 and 2, raise a RuntimeError, we have over defined things.
        Else if a rule is defined for either 1 or 2 (or the same rule is present for 1 and 2), use it.
        """
        if (newCmd in self.ruleDict) and (queuedCmd in self.ruleDict[newCmd]):
            # a command was specifically defined for these two
            # this trumps any rules that may apply to "all"
            return self.ruleDict[newCmd][queuedCmd]
        if (newCmd == queuedCmd) and (newCmd not in self.priorityDict) and (newCmd not in self.ruleDict):
            return self.CancelQueued
        if ("all" in self.ruleDict) and (queuedCmd in self.ruleDict["all"]):
            # a command was defined for all incoming commands when
            # encountering this specific command on the queue
            rule = self.ruleDict["all"][queuedCmd]
            # now for paranoia, make sure a different rule was not
            # defined for the reverse set
            if (newCmd in self.ruleDict) and ("all" in self.ruleDict[newCmd]):
                if self.ruleDict[newCmd]["all"] != rule:
                    raise RuntimeError("Conflict when trying to apply a rule 'all' commands on queue. This should have been caught in CommandQueue.addRule")
            return rule
        elif (newCmd in self.ruleDict) and ("all" in self.ruleDict[newCmd]):
            # newCmd has rule defined for all queued commands
            return self.ruleDict[newCmd]["all"]
        elif ("all" in self.ruleDict) and ("all" in self.ruleDict["all"]):
            # the rule always applies!!!
            return self.ruleDict["all"]["all"]
        else:
            return None

    def addCmd(self, cmd, runFunc):
        """ Add a command to the queue, taking rules and priority into account.

            @param[in] cmd  a twistedActor command object
            @param[in] runFunc  function that runs the command; called once, when the command is ready to run,
                just after cmd's state is set to cmd.Running; receives one argument: cmd

            Here's the logic:
            If cmd has an unrecognized priority (not defined in self.priorityDict), assign it a priority of 0
            If cmd as a priority of Immediate:
                - Completely clear the queue, and kill the currently executing command (if it is still active)
                - Add the command to the now empty queue
            Else:
                Look for rules.  You may want to read the documentation for getRule, as there is some logic in the
                way rules are selected.  First, run through all commands currently on the queue.  If any queued
                command has a rule CancelNew pertaining to this command, then cancel the incoming command and return
                (it never reaches the queue). If the command wasn't canceled, run through the queue again to determine
                if this command should cancel any commands on the queue.

                Then check to see if this cmd should kill any currently executing command.

                Lastly insert the command in the queue in order based on it's priority.
        """
        if not hasattr(cmd, "cmdVerb"):
            # give it a dummy command verb
            cmd.cmdVerb = "dummy"
        if cmd.cmdVerb not in self.priorityDict:
            # no priority pre-defined.  Assign lowest priority
            priority = 0
        else:
            priority = self.priorityDict[cmd.cmdVerb]

        toQueue = QueuedCommand(
            cmd = cmd,
            priority = priority,
            runFunc = runFunc,
        )
        if toQueue.priority == CommandQueue.Immediate:
            # cancel each command in the cmdQueue;
            # iterate over a copy because the queue is updated for each cancelled command,
            # and extract the cmd from the queuedCmd since we don't need the wrapped command
            cmdList = [queuedCmd.cmd for queuedCmd in self.cmdQueue]
            for sadCmd in cmdList:
                if not sadCmd.isDone:
                    sadCmd.setState(
                        sadCmd.Cancelled,
                        textMsg = "Cancelled on queue by immediate priority command %r" % (cmd.cmdStr,),
                    )
            if not self.currExeCmd.cmd.isDone:
                self.currExeCmd.cmd.setState(
                    self.currExeCmd.cmd.Cancelled,
                    textMsg = "Killed by immediate priority command %r" % (cmd.cmdStr,),
                )
        else:
            # check new command against queued commands
            # iterate over a copy because the queue is updated for each cancelled command,
            # and extract the cmd from the queuedCmd since we don't need the wrapped command
            cmdList = [queuedCmd.cmd for queuedCmd in self.cmdQueue]
            for queuedCmd in cmdList:
                # first check if toQueue should be cancelled by any existing
                # command on the queue
                if queuedCmd.isDone:
                    # ignore completed commands (not that any on the stack will have been run yet,
                    # but they can be cancelled elsewhere)
                    continue

                action = self.getRule(toQueue.cmd.cmdVerb, queuedCmd.cmdVerb)
                if action == self.CancelNew:
                    toQueue.cmd.setState(
                        toQueue.cmd.Cancelled,
                        "Cancelled before queueing by queued command %r" % (queuedCmd.cmdStr),
                    )
                    return # queue not altered; no need to do anything else
            for queuedCmd in cmdList:
                # next check if toQueue should cancel any commands existing on the
                # queue
                if queuedCmd.isDone:
                    # ignore completed commands
                    continue
                action = self.getRule(toQueue.cmd.cmdVerb, queuedCmd.cmdVerb)
                if action in (self.CancelQueued, self.KillRunning):
                    queuedCmd.setState(
                        queuedCmd.Cancelled,
                        "Cancelled while queued by new command %r" % (toQueue.cmd.cmdStr),
                    )

            # should new command kill currently executing command?
            if not self.currExeCmd.cmd.isDone:
                action = self.getRule(toQueue.cmd.cmdVerb, self.currExeCmd.cmd.cmdVerb)
                if action == self.CancelNew:
                    toQueue.cmd.setState(
                        toQueue.cmd.Cancelled,
                        "Cancelled before queueing by running command %r" % (self.currExeCmd.cmd.cmdStr),
                    )
                    return # queue not altered; no need to do anything else
                if action == self.KillRunning:
                    self.killFunc(self.currExeCmd.cmd, toQueue.cmd)

        insort_left(self.cmdQueue, toQueue) # inserts in sorted order
        self.scheduleRunQueue()

    def killAll(self):
        """!Kill all commands without trying to execute any

        Use when there is no hope of sending commands, e.g. at shutdown
        """
        self._enabled = False
        try:
            cmdList = [queuedCmd.cmd for queuedCmd in self.cmdQueue]
            for cmd in cmdList:
                if not cmd.isDone:
                    cmd.setState(cmd.Failed, textMsg="disconnected")
            self.cmdQueue = []
            if not self.currExeCmd.cmd.isDone:
                self.currExeCmd.setState(self.currExeCmd.Failed, textMsg="disconnected")
        finally:
            self._enabled = True

    def scheduleRunQueue(self, cmd=None):
        """!Run the queue on a zero second timer

        @param[in] cmd  command; if provided and not Done then the queue is not run (a BaseCmd);
            this allows use of scheduleRunQueue as a command callback
        """
        if not self._enabled:
            return
        if cmd and not cmd.isDone:
            return
        self.queueTimer.start(0., self.runQueue)

    def runQueue(self):
        """ Manage Executing commands
        """
        if not self._enabled:
            return
        # prune the queue, throw out done commands
        self.cmdQueue = [qc for qc in self.cmdQueue if not qc.cmd.isDone]
        if len(self.cmdQueue) == 0:
            # the command queue is empty, nothing to run
            pass
        elif self.currExeCmd.cmd.isDone:
            # begin the next command on the queue
            self.currExeCmd = self.cmdQueue.pop(-1)
            self.currExeCmd.setRunning()
            self.currExeCmd.cmd.addCallback(self.scheduleRunQueue)

    def __repr__(self):
        cmdList = ", ".join([x.cmdStr for x in self.cmdQueue])
        return "[" + cmdList + "]"
예제 #46
0
 def diffuIn(self):
     Timer(0.2, self._setDiffuPos, 1)  # set on a timer to emulate move time
예제 #47
0
class ConnectDevice(object):
    """!Connect a device and execute dev.init

    If the device is already connected then generate a new userCmd, if needed,
    and sets userCmd's state to userCmd.Done.

    Public attributes:
    - dev: the provided device
    - userCmd: the provided userCmd, or a new one if none provided
    """
    def __init__(self, dev, userCmd, timeLim):
        """!Start connecting a device

        @param[in] dev  device
        @param[in] userCmd  user command associated with the connection, or None
        @param[in] timeLim  time limit (sec) to make this connection
        """
        self.dev = dev
        self._timeLim = timeLim
        self.userCmd = expandUserCmd(userCmd)
        self._connTimer = Timer()
        self._addedConnCallback = False

        if self.dev.isConnected and self.dev.conn.isConnected:
            # already done; don't send init
            self.finish()
            return

        self.dev._ignoreConnCallback = True
        self.dev.setState(self.dev.Connecting)
        self.dev.conn.addStateCallback(self.connCallback)
        self._addedConnCallback = True
        if self.dev.conn.mayConnect:
            self.dev.conn.connect(timeLim=timeLim)
        else:
            if self._timeLim:
                # start timer for the connection that is occurring now
                self._connTimer.start(self._timeLim, self.finish,
                                      "timed out waiting for connection")

    def initCallback(self, userCmd):
        """!Callback for device initialization
        """
        # print("%s.initCallback(userCmd=%r); _callbacks=%s" % (self, userCmd, userCmd._callbacks))
        if not userCmd.isDone:
            return

        if userCmd.didFail:
            reason = userCmd.getMsg(
            ) or "init command failed for unknown reasons"
        else:
            reason = None
        Timer(0, self.finish, reason)

    def connCallback(self, conn):
        """!Callback for device connection state
        """
        if self.dev.conn.isConnected:
            self._connTimer.cancel()
            initUserCmd = UserCmd(cmdStr="connect %s" % (self.dev.name, ),
                                  callFunc=self.initCallback)
            self.dev.init(userCmd=initUserCmd,
                          timeLim=self._timeLim,
                          getStatus=True)
        elif self.dev.conn.didFail:
            self._connTimer.cancel()
            self.finish("connection failed")

    def finish(self, reason=None):
        """!Call on success or failure to finish the command

        @param[in] reason: reason for failure (if non-empty then failure is assumed)
        """
        self._connTimer.cancel()
        self.dev._ignoreConnCallback = False
        if self._addedConnCallback:
            self.dev.conn.removeStateCallback(self.connCallback)
        if reason or not self.dev.conn.isConnected:
            reason = reason or "unknown reason"
            self.dev.setState(self.dev.Failed, reason)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Failed,
                                      textMsg="%s failed to connect: %s" %
                                      (self.dev, reason))
            Timer(0, self.dev.conn.disconnect)
        else:
            self.dev.setState(self.dev.Connected)
            if not self.userCmd.isDone:
                self.userCmd.setState(self.userCmd.Done)

    def __repr__(self):
        return "%s(dev.name=%s)" % (type(self).__name__, self.dev.name)