def __init__(
        self,
        name,
        userPort=UserPort,
        commandSet=arcticFWCommandSet,
    ):
        """!Construct an ArcticFWActor

        @param[in] name  actor name
        @param[in] userPort  int, a port on which this thing runs
        @param[in] commandSet  a twistedActor.parse.CommandSet used for command def, and parsing
        @param[in] fakeFilterWheel  bool.  If true use a fake filter wheel device, for safe testing.
        """
        self.status = ArcticFWStatus()
        self.fwHomeCmd = UserCmd()
        self.fwHomeCmd.setState(self.fwHomeCmd.Done)
        self.fwMoveCmd = UserCmd()
        self.fwMoveCmd.setState(self.fwMoveCmd.Done)
        self.status.addHomeCallback(self.homeCallback)
        self.status.addMoveCallback(self.moveCallback)
        # init the filterWheel
        # this sets self.filterWheel
        Actor.__init__(
            self,
            userPort=userPort,
            maxUsers=1,
            name=name,
            version=__version__,
            commandSet=commandSet,
        )
        init()
 def commandActor(self, cmdStr, shouldFail=False):
     d = Deferred()
     cmd = UserCmd(cmdStr=cmdStr)
     def fireDeferred(cbCmd):
         if cbCmd.isDone:
             d.callback("done")
     def checkCmdState(cb):
         self.assertTrue(shouldFail==cmd.didFail)
     cmd.addCallback(fireDeferred)
     d.addCallback(checkCmdState)
     self.arcticActor.parseAndDispatchCmd(cmd)
     return d
    def commandActor(self, cmdStr, shouldFail=False):
        d = Deferred()
        cmd = UserCmd(cmdStr=cmdStr)

        def fireDeferred(cbCmd):
            if cbCmd.isDone:
                d.callback("done")

        def checkCmdState(cb):
            self.assertTrue(shouldFail == cmd.didFail)

        cmd.addCallback(fireDeferred)
        d.addCallback(checkCmdState)
        self.arcticFWActor.parseAndDispatchCmd(cmd)
        return d
Esempio n. 4
0
    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,
            )
Esempio n. 5
0
    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 = (),
        )
Esempio n. 6
0
    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
Esempio n. 7
0
    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 = (),
        )
def expandCommand():
    return UserCmd()
Esempio n. 9
0
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
Esempio n. 10
0
 def makeCmd(self, cmd):
     newCmd = UserCmd(userID = 0, cmdStr = cmd)
     newCmd.cmdVerb = cmd
     newCmd.addCallback(self.cmdCallback)
     return newCmd
Esempio n. 11
0
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))
Esempio n. 12
0
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))
class ArcticFWActor(Actor):
    Facility = syslog.LOG_LOCAL1
    DefaultTimeLim = 5  # default time limit, in seconds
    MoveRange = range(1, 7)

    # State options
    def __init__(
        self,
        name,
        userPort=UserPort,
        commandSet=arcticFWCommandSet,
    ):
        """!Construct an ArcticFWActor

        @param[in] name  actor name
        @param[in] userPort  int, a port on which this thing runs
        @param[in] commandSet  a twistedActor.parse.CommandSet used for command def, and parsing
        @param[in] fakeFilterWheel  bool.  If true use a fake filter wheel device, for safe testing.
        """
        self.status = ArcticFWStatus()
        self.fwHomeCmd = UserCmd()
        self.fwHomeCmd.setState(self.fwHomeCmd.Done)
        self.fwMoveCmd = UserCmd()
        self.fwMoveCmd.setState(self.fwMoveCmd.Done)
        self.status.addHomeCallback(self.homeCallback)
        self.status.addMoveCallback(self.moveCallback)
        # init the filterWheel
        # this sets self.filterWheel
        Actor.__init__(
            self,
            userPort=userPort,
            maxUsers=1,
            name=name,
            version=__version__,
            commandSet=commandSet,
        )
        init()

    @property
    def filterWheelMoving(self):
        return self.status.isMoving

    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))
        if getStatus:
            self.cmd_status(userCmd)  # sets done
        else:
            userCmd.setState(userCmd.Done)
        return userCmd

    def cmd_init(self, userCmd):
        """! Implement the init command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        log.info("%s.cmd_init(userCmd=%s)" % (self, str(userCmd)))
        # print("%s.cmd_init(userCmd=%s)"%(self, str(userCmd)))
        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
        """
        log.info("%s.cmd_ping(userCmd=%s)" % (self, str(userCmd)))
        # print("%s.cmd_ping(userCmd=%s)"%(self, str(userCmd)))
        userCmd.setState(userCmd.Done, textMsg="alive")
        return True

    def cmd_stopWheel(self, userCmd):
        log.info("%s.cmd_stop(userCmd=%s)" % (self, str(userCmd)))
        # print("%s.cmd_stop(userCmd=%s)"%(self, str(userCmd)))
        if not self.fwMoveCmd.isDone:
            self.fwMoveCmd.setState(self.fwMoveCmd.Failed, "stop commanded")
        if not self.fwHomeCmd.isDone:
            self.fwHomeCmd.setState(self.fwHomeCmd.Failed, "stop commanded")
        stop()
        userCmd.setState(userCmd.Done)
        return True

    def moveCallback(self):
        self.getStatus()
        self.writeToUsers("i", self.status.statusStr, cmd=self.fwMoveCmd)
        if self.fwMoveCmd.isDone:
            # probably cancelled by stop
            return
        if self.status.inPosition:
            self.fwMoveCmd.setState(self.fwMoveCmd.Done)
        else:
            self.fwMoveCmd.setState(self.fwMoveCmd.Failed,
                                    "Failed to stop at filter sensor")

    def homeCallback(self):
        self.getStatus()
        self.writeToUsers("i", self.status.statusStr, cmd=self.fwHomeCmd)
        if self.fwHomeCmd.isDone:
            # don't do anything (probably cancelled by a stop?)
            return
        if self.status.inPosition and self.status.atHome:
            self.fwHomeCmd.setState(self.fwHomeCmd.Done)
        else:
            self.fwHomeCmd.setState(self.fwHomeCmd.Failed,
                                    "Failed to stop at home sensor")

    def cmd_move(self, userCmd):
        desPos = int(userCmd.parsedCommand.parsedPositionalArgs[0])
        log.info("%s.cmd_move(userCmd=%s) desPos: %i" %
                 (self, userCmd, desPos))
        # print("%s.cmd_move(userCmd=%s) desPos: %i"%(self, userCmd, desPos))
        if desPos not in self.MoveRange:
            # raise ParseError("desPos must be one of %s for move command"%(str(self.MoveRange),))
            userCmd.setState(
                userCmd.Failed, "desPos must be one of %s for move command" %
                (str(self.MoveRange), ))
        elif not self.status.isHomed:
            userCmd.setState(userCmd.Failed,
                             "cannot command move, home filter wheel first.")
        elif not self.fwMoveCmd.isDone:
            userCmd.setState(userCmd.Failed, "filter wheel is moving")
        elif desPos == self.status.currentPos:
            userCmd.setState(userCmd.Done, "Filter currently in position")
        else:
            # get a fresh status
            self.getStatus()
            self.fwMoveCmd = userCmd
            if not self.fwMoveCmd.isActive:
                self.fwMoveCmd.setState(self.fwMoveCmd.Running)
            moveToFilter(desPos)
            self.getStatus()
            self.writeToUsers("i", self.status.moveStr, cmd=userCmd)
        return True

    def cmd_diffuIn(self, userCmd):
        """Move the diffuser into the beam
        """
        log.info("%s.cmd_diffuIn(userCmd=%s)" % (self, userCmd))
        if self.status.diffuCover == 0:
            setDiffuserIn()
            time.sleep(3)
            self.getStatus()
            self.writeToUsers("i", self.status.statusStr, cmd=userCmd)
            userCmd.setState(userCmd.Done)

        return True

    def cmd_diffuOut(self, userCmd):
        """Move the diffuser out of the beam
        """
        setDiffuserOut()
        self.getStatus()
        self.writeToUsers("i", self.status.statusStr, cmd=userCmd)
        userCmd.setState(userCmd.Done)
        return True

    def cmd_startDiffuRot(self, userCmd):
        """Move the diffuser into the beam
        """
        if self.status.diffuserIn == 1:
            setRotationStart()
            self.getStatus()
            self.writeToUsers("i", self.status.statusStr, cmd=userCmd)
            userCmd.setState(userCmd.Done)
        else:  #functionaly this works but the status below never makes it back to the users in the hub or on the gui.
            self.writeToUsers("f", "Diffuser is not in Beam", cmd=userCmd)
            self.getStatus()
            userCmd.setState(userCmd.Failed)

        return True

    def cmd_stopDiffuRot(self, userCmd):
        #always allow diffuser to stop
        setRotationStop()
        self.getStatus()
        self.writeToUsers("i", self.status.statusStr, cmd=userCmd)
        userCmd.setState(userCmd.Done)
        return True

    def cmd_home(self, userCmd):
        log.info("%s.cmd_home(userCmd=%s)" % (self, str(userCmd)))
        # print("%s.cmd_home(userCmd=%s)"%(self, str(userCmd)))
        if False in [self.fwMoveCmd.isDone, self.fwHomeCmd]:
            userCmd.setState(userCmd.Failed, "filter wheel is moving")
        else:
            self.fwHomeCmd = userCmd
            if not self.fwHomeCmd.isActive:
                self.fwHomeCmd.setState(self.fwHomeCmd.Running)
            home()
            self.cmd_status(userCmd, setDone=False)
            return True

    def cmd_status(self, userCmd=None, setDone=True):
        """! Implement the status command
        @param[in]  userCmd  a twistedActor command with a parsedCommand attribute
        """
        log.info("%s.cmd_status(userCmd=%s)" % (self, str(userCmd)))
        userCmd = expandUserCmd(userCmd)
        self.getStatus()
        self.writeToUsers("i", self.status.statusStr, cmd=userCmd)
        if setDone:
            userCmd.setState(userCmd.Done)
        return True

    def getStatus(self):
        """! A generic status command
        @param[in] userCmd a twistedActor UserCmd or none
        """
        self.status.update()