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
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, 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 = (), )
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 __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()
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
def makeCmd(self, cmd): newCmd = UserCmd(userID = 0, cmdStr = cmd) newCmd.cmdVerb = cmd newCmd.addCallback(self.cmdCallback) return newCmd
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))
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()