class FilterWheel(object): def __init__(self): self.moveTimer = Timer() def stopMove(self): self.moveTimer.cancel() StatusDict["motor"]=0 # StatusDict["motor"]=8 def connect(self): """ initiate filter wheel communication """ pass def home(self): """ home filter wheel and set the filter wheel id """ # emulate the block time.sleep(3) # StatusDict["id"] = 1 return StatusDict["id"] def stop(self): """ stop filter wheel motion """ return self.stopMove() def moveToPosition(self, pos): """ move to a specific filter wheel position arguments: int pos - filter position between 1 and 6 status: 0 - fail 1 - succeed -1 - unknown """ StatusDict["position"] = int(pos) # StatusDict["motor"] = 1 StatusDict["motor"] = 3 self.moveTimer.start(MoveTime, self.stopMove) return self.status() def status(self): """ return current status of filter wheel """ return StatusDict
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
class CommandQueue(object): """!A command queue. Default behavior is to queue all commands and execute them one at a time in order of priority. Equal priority commands are executed in the order received. Special rules may be defined for handling special cases of command collisions. """ Immediate = 'immediate' CancelNew = 'cancelnew' CancelQueued = 'cancelqueued' KillRunning = 'killrunning' _AddActions = frozenset((CancelNew, CancelQueued, KillRunning)) def __init__(self, priorityDict, killFunc=None): """ This is an object which keeps track of commands and smartly handles command collisions based on rules chosen by you. @param[in] priorityDict a dictionary keyed by cmdVerb, with integer values or Immediate @ param[in] killFunc: a function to call when a running command needs to be killed. Accepts 2 parameters, the command to be canceled, and the command doing the killing. This function must eventually ensure that the running command is canceled safely allowing for the next queued command to go. Or None """ self.cmdQueue = [] dumCmd = UserCmd() dumCmd.setState(dumCmd.Done) dumCmd.cmdVerb = 'dummy' self.currExeCmd = QueuedCommand(dumCmd, 0, lambda cmdVar: None) self.priorityDict = priorityDict self.killFunc = killFunc self.ruleDict = {} self.queueTimer = Timer() self._enabled = True def __getitem__(self, ind): return self.cmdQueue[ind] def __len__(self): return len(self.cmdQueue) def addRule(self, action, newCmds="all", queuedCmds="all"): """!Add special case rules for collisions. @param[in] action one of CancelNew, CancelQueued, KillRunning @param[in] newCmds a list of incoming commands to which this rule applies or "all" @param[in] queuedCmds a list of the commands (queued or running) to which this rule applies or "all" See documentation for the addCommand method to learn exactly how rules are evaluated """ if action == self.KillRunning and self.killFunc is None: raise RuntimeError("must supply killFunc in CommandQueue constructor before specifying a KillRunning rule") checkCmds = [] if newCmds != "all": checkCmds = checkCmds + newCmds else: newCmds = ["all"] if queuedCmds != "all": checkCmds = checkCmds + queuedCmds else: queuedCmds = ["all"] for cmdName in checkCmds: if cmdName not in self.priorityDict: raise RuntimeError('Cannot add rule to unrecognized command: %s' % (cmdName,)) if action not in self._AddActions: raise RuntimeError("Rule action=%r must be one of %s" % (action, sorted(self._AddActions))) for nc in newCmds: if not nc in self.ruleDict: if nc == 'all': # ensure any rules defined in self.ruleDict[:]["all"] # are the same action: for existingDict in self.ruleDict.itervalues(): if "all" in existingDict and action != existingDict["all"]: raise RuntimeError("May not specifiy conflicting rules pertaining to all queued commands and all new commands") self.ruleDict[nc] = {} for qc in queuedCmds: if qc in self.ruleDict[nc] and self.ruleDict[nc][qc] != action: raise RuntimeError( 'Cannot set rule=%r for new command=%s vs. queued command=%s: already set to %s' % \ (action, nc, qc, self.ruleDict[nc][qc]) ) if qc == "all": if "all" in self.ruleDict: for value in self.ruleDict["all"].itervalues(): if value != action: raise RuntimeError("May not specifiy conflicting rules pertaining to all queued commands and all new commands") self.ruleDict[nc][qc] = action def getRule(self, newCmd, queuedCmd): """!Get the rule for a specific new command vs. a specific queued command. @param[in] newCmd the incoming command verb @param[in] queuedCmd a command verb currently on the queue @return a rule, one of self._AddActions Note: there is some logic to determine which rule is grabbed: If a specific rule exists between the newCmd and queuedCmd, that is returned. This trumps the situation in which there may be a rule defined involving newCmd or queuedCmd pertaining to all new commands or all queued commands. If newCmd and queuedCmd are the same command and are not present in the ruleDict nor priorityDict return the default rule of CancelQueued (effectively the new cmd will replace the one on queue) If no specific rule exists between newCmd and queuedCmd, check if a rule is defined between both: 1. newCmd and all queued commands 2. all new commands and queuedCmd If a different rule is present for both 1 and 2, raise a RuntimeError, we have over defined things. Else if a rule is defined for either 1 or 2 (or the same rule is present for 1 and 2), use it. """ if (newCmd in self.ruleDict) and (queuedCmd in self.ruleDict[newCmd]): # a command was specifically defined for these two # this trumps any rules that may apply to "all" return self.ruleDict[newCmd][queuedCmd] if (newCmd == queuedCmd) and (newCmd not in self.priorityDict) and (newCmd not in self.ruleDict): return self.CancelQueued if ("all" in self.ruleDict) and (queuedCmd in self.ruleDict["all"]): # a command was defined for all incoming commands when # encountering this specific command on the queue rule = self.ruleDict["all"][queuedCmd] # now for paranoia, make sure a different rule was not # defined for the reverse set if (newCmd in self.ruleDict) and ("all" in self.ruleDict[newCmd]): if self.ruleDict[newCmd]["all"] != rule: raise RuntimeError("Conflict when trying to apply a rule 'all' commands on queue. This should have been caught in CommandQueue.addRule") return rule elif (newCmd in self.ruleDict) and ("all" in self.ruleDict[newCmd]): # newCmd has rule defined for all queued commands return self.ruleDict[newCmd]["all"] elif ("all" in self.ruleDict) and ("all" in self.ruleDict["all"]): # the rule always applies!!! return self.ruleDict["all"]["all"] else: return None def addCmd(self, cmd, runFunc): """ Add a command to the queue, taking rules and priority into account. @param[in] cmd a twistedActor command object @param[in] runFunc function that runs the command; called once, when the command is ready to run, just after cmd's state is set to cmd.Running; receives one argument: cmd Here's the logic: If cmd has an unrecognized priority (not defined in self.priorityDict), assign it a priority of 0 If cmd as a priority of Immediate: - Completely clear the queue, and kill the currently executing command (if it is still active) - Add the command to the now empty queue Else: Look for rules. You may want to read the documentation for getRule, as there is some logic in the way rules are selected. First, run through all commands currently on the queue. If any queued command has a rule CancelNew pertaining to this command, then cancel the incoming command and return (it never reaches the queue). If the command wasn't canceled, run through the queue again to determine if this command should cancel any commands on the queue. Then check to see if this cmd should kill any currently executing command. Lastly insert the command in the queue in order based on it's priority. """ if not hasattr(cmd, "cmdVerb"): # give it a dummy command verb cmd.cmdVerb = "dummy" if cmd.cmdVerb not in self.priorityDict: # no priority pre-defined. Assign lowest priority priority = 0 else: priority = self.priorityDict[cmd.cmdVerb] toQueue = QueuedCommand( cmd = cmd, priority = priority, runFunc = runFunc, ) if toQueue.priority == CommandQueue.Immediate: # cancel each command in the cmdQueue; # iterate over a copy because the queue is updated for each cancelled command, # and extract the cmd from the queuedCmd since we don't need the wrapped command cmdList = [queuedCmd.cmd for queuedCmd in self.cmdQueue] for sadCmd in cmdList: if not sadCmd.isDone: sadCmd.setState( sadCmd.Cancelled, textMsg = "Cancelled on queue by immediate priority command %r" % (cmd.cmdStr,), ) if not self.currExeCmd.cmd.isDone: self.currExeCmd.cmd.setState( self.currExeCmd.cmd.Cancelled, textMsg = "Killed by immediate priority command %r" % (cmd.cmdStr,), ) else: # check new command against queued commands # iterate over a copy because the queue is updated for each cancelled command, # and extract the cmd from the queuedCmd since we don't need the wrapped command cmdList = [queuedCmd.cmd for queuedCmd in self.cmdQueue] for queuedCmd in cmdList: # first check if toQueue should be cancelled by any existing # command on the queue if queuedCmd.isDone: # ignore completed commands (not that any on the stack will have been run yet, # but they can be cancelled elsewhere) continue action = self.getRule(toQueue.cmd.cmdVerb, queuedCmd.cmdVerb) if action == self.CancelNew: toQueue.cmd.setState( toQueue.cmd.Cancelled, "Cancelled before queueing by queued command %r" % (queuedCmd.cmdStr), ) return # queue not altered; no need to do anything else for queuedCmd in cmdList: # next check if toQueue should cancel any commands existing on the # queue if queuedCmd.isDone: # ignore completed commands continue action = self.getRule(toQueue.cmd.cmdVerb, queuedCmd.cmdVerb) if action in (self.CancelQueued, self.KillRunning): queuedCmd.setState( queuedCmd.Cancelled, "Cancelled while queued by new command %r" % (toQueue.cmd.cmdStr), ) # should new command kill currently executing command? if not self.currExeCmd.cmd.isDone: action = self.getRule(toQueue.cmd.cmdVerb, self.currExeCmd.cmd.cmdVerb) if action == self.CancelNew: toQueue.cmd.setState( toQueue.cmd.Cancelled, "Cancelled before queueing by running command %r" % (self.currExeCmd.cmd.cmdStr), ) return # queue not altered; no need to do anything else if action == self.KillRunning: self.killFunc(self.currExeCmd.cmd, toQueue.cmd) insort_left(self.cmdQueue, toQueue) # inserts in sorted order self.scheduleRunQueue() def killAll(self): """!Kill all commands without trying to execute any Use when there is no hope of sending commands, e.g. at shutdown """ self._enabled = False try: cmdList = [queuedCmd.cmd for queuedCmd in self.cmdQueue] for cmd in cmdList: if not cmd.isDone: cmd.setState(cmd.Failed, textMsg="disconnected") self.cmdQueue = [] if not self.currExeCmd.cmd.isDone: self.currExeCmd.setState(self.currExeCmd.Failed, textMsg="disconnected") finally: self._enabled = True def scheduleRunQueue(self, cmd=None): """!Run the queue on a zero second timer @param[in] cmd command; if provided and not Done then the queue is not run (a BaseCmd); this allows use of scheduleRunQueue as a command callback """ if not self._enabled: return if cmd and not cmd.isDone: return self.queueTimer.start(0., self.runQueue) def runQueue(self): """ Manage Executing commands """ if not self._enabled: return # prune the queue, throw out done commands self.cmdQueue = [qc for qc in self.cmdQueue if not qc.cmd.isDone] if len(self.cmdQueue) == 0: # the command queue is empty, nothing to run pass elif self.currExeCmd.cmd.isDone: # begin the next command on the queue self.currExeCmd = self.cmdQueue.pop(-1) self.currExeCmd.setRunning() self.currExeCmd.cmd.addCallback(self.scheduleRunQueue) def __repr__(self): cmdList = ", ".join([x.cmdStr for x in self.cmdQueue]) return "[" + cmdList + "]"
class DisconnectDevice(object): """!Execute dev.init (if the device is fully connected) and disconnect a device If the device is already disconnected then generate a new userCmd, if needed, and sets userCmd's state to userCmd.Done. Public attributes: - dev: the provided device - userCmd: the provided userCmd, or a new one if none provided """ def __init__(self, dev, userCmd, timeLim): """!Start disconnecting a device """ self.dev = dev self.userCmd = expandUserCmd(userCmd) self._timeLim = timeLim self._connTimer = Timer() self._addedConnCallback = False if self.dev.conn.isDisconnected: if self.dev.state != self.dev.Disconnected: self.dev.setState(self.dev.Disconnected, "socket disconnected") if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Done) return self.dev._ignoreConnCallback = True if self.dev.state != self.dev.Disconnected: self.dev.setState(self.dev.Disconnecting) if self.dev.conn.isConnected: initUserCmd = UserCmd(cmdStr="Init via device.DisconnectDevice", callFunc=self.initCallback, timeLim=timeLim) self.dev.init(userCmd=initUserCmd, timeLim=timeLim, getStatus=False) else: # not fully connected, so cannot send init, but not fully disconnected yet, so finish disconnecting textMsg = "%s connection state=%s; cannot initialize before disconnecting" % (self.dev.name, self.dev.conn.state) self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg),)) self.startDisconnect() return def startDisconnect(self): """!Start disconnecting the connection """ if self.dev.conn.isDone and not self.dev.conn.isConnected: # fully disconnected; no more to be done Timer(0, self.finish) else: if self._timeLim: # start timer for disconnection self._connTimer.start(self._timeLim, self.finish, "timed out waiting for disconnection") self.dev.conn.addStateCallback(self.connCallback) self._addedConnCallback = True self.dev.conn.disconnect() def initCallback(self, initUserCmd): """!Callback for device initialization """ if not initUserCmd.isDone: return if initUserCmd.didFail: textMsg = "%s initialization failed: %s" % (self.dev.name, initUserCmd.textMsg,) self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg),)) self.startDisconnect() def connCallback(self, conn): """!Callback for device connection state """ if self.dev.conn.isDone: Timer(0, self.finish) def finish(self, reason=None): """!Call on success or failure to finish the command @param[in] reason: reason for failure (if non-empty then failure is assumed) """ self._connTimer.cancel() self.dev._ignoreConnCallback = False if self._addedConnCallback: self.dev.conn.removeStateCallback(self.connCallback) if reason or not self.dev.conn.isDone or not self.dev.conn.isDisconnected: reason = reason or "unknown reasons" self.dev.setState(self.dev.Failed, reason) if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Failed, textMsg="%s failed to disconnect: %s" % (self.dev, reason)) else: if self.dev.state != self.dev.Disconnected: self.dev.setState(self.dev.Disconnected) if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Done) self.dev.cleanup() def __repr__(self): return "%s(dev.name=%s)" % (type(self).__name__, self.dev.name)
class ConnectDevice(object): """!Connect a device and execute dev.init If the device is already connected then generate a new userCmd, if needed, and sets userCmd's state to userCmd.Done. Public attributes: - dev: the provided device - userCmd: the provided userCmd, or a new one if none provided """ def __init__(self, dev, userCmd, timeLim): """!Start connecting a device @param[in] dev device @param[in] userCmd user command associated with the connection, or None @param[in] timeLim time limit (sec) to make this connection """ self.dev = dev self._timeLim = timeLim self.userCmd = expandUserCmd(userCmd) self._connTimer = Timer() self._addedConnCallback = False if self.dev.isConnected and self.dev.conn.isConnected: # already done; don't send init self.finish() return self.dev._ignoreConnCallback = True self.dev.setState(self.dev.Connecting) self.dev.conn.addStateCallback(self.connCallback) self._addedConnCallback = True if self.dev.conn.mayConnect: self.dev.conn.connect(timeLim=timeLim) else: if self._timeLim: # start timer for the connection that is occurring now self._connTimer.start(self._timeLim, self.finish, "timed out waiting for connection") def initCallback(self, userCmd): """!Callback for device initialization """ # print("%s.initCallback(userCmd=%r); _callbacks=%s" % (self, userCmd, userCmd._callbacks)) if not userCmd.isDone: return if userCmd.didFail: reason = userCmd.getMsg() or "init command failed for unknown reasons" else: reason = None Timer(0, self.finish, reason) def connCallback(self, conn): """!Callback for device connection state """ if self.dev.conn.isConnected: self._connTimer.cancel() initUserCmd = UserCmd(cmdStr="connect %s" % (self.dev.name,), callFunc=self.initCallback) self.dev.init(userCmd=initUserCmd, timeLim=self._timeLim, getStatus=True) elif self.dev.conn.didFail: self._connTimer.cancel() self.finish("connection failed") def finish(self, reason=None): """!Call on success or failure to finish the command @param[in] reason: reason for failure (if non-empty then failure is assumed) """ self._connTimer.cancel() self.dev._ignoreConnCallback = False if self._addedConnCallback: self.dev.conn.removeStateCallback(self.connCallback) if reason or not self.dev.conn.isConnected: reason = reason or "unknown reason" self.dev.setState(self.dev.Failed, reason) if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Failed, textMsg="%s failed to connect: %s" % (self.dev, reason)) Timer(0, self.dev.conn.disconnect) else: self.dev.setState(self.dev.Connected) if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Done) def __repr__(self): return "%s(dev.name=%s)" % (type(self).__name__, self.dev.name)
class DisconnectDevice(object): """!Execute dev.init (if the device is fully connected) and disconnect a device If the device is already disconnected then generate a new userCmd, if needed, and sets userCmd's state to userCmd.Done. Public attributes: - dev: the provided device - userCmd: the provided userCmd, or a new one if none provided """ def __init__(self, dev, userCmd, timeLim): """!Start disconnecting a device """ self.dev = dev self.userCmd = expandUserCmd(userCmd) self._timeLim = timeLim self._connTimer = Timer() self._addedConnCallback = False if self.dev.conn.isDisconnected: if self.dev.state != self.dev.Disconnected: self.dev.setState(self.dev.Disconnected, "socket disconnected") if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Done) return self.dev._ignoreConnCallback = True if self.dev.state != self.dev.Disconnected: self.dev.setState(self.dev.Disconnecting) if self.dev.conn.isConnected: initUserCmd = UserCmd(cmdStr="Init via device.DisconnectDevice", callFunc=self.initCallback, timeLim=timeLim) self.dev.init(userCmd=initUserCmd, timeLim=timeLim, getStatus=False) else: # not fully connected, so cannot send init, but not fully disconnected yet, so finish disconnecting textMsg = "%s connection state=%s; cannot initialize before disconnecting" % ( self.dev.name, self.dev.conn.state) self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg), )) self.startDisconnect() return def startDisconnect(self): """!Start disconnecting the connection """ if self.dev.conn.isDone and not self.dev.conn.isConnected: # fully disconnected; no more to be done Timer(0, self.finish) else: if self._timeLim: # start timer for disconnection self._connTimer.start(self._timeLim, self.finish, "timed out waiting for disconnection") self.dev.conn.addStateCallback(self.connCallback) self._addedConnCallback = True self.dev.conn.disconnect() def initCallback(self, initUserCmd): """!Callback for device initialization """ if not initUserCmd.isDone: return if initUserCmd.didFail: textMsg = "%s initialization failed: %s" % ( self.dev.name, initUserCmd.textMsg, ) self.dev.writeToUsers("w", "text=%s" % (quoteStr(textMsg), )) self.startDisconnect() def connCallback(self, conn): """!Callback for device connection state """ if self.dev.conn.isDone: Timer(0, self.finish) def finish(self, reason=None): """!Call on success or failure to finish the command @param[in] reason: reason for failure (if non-empty then failure is assumed) """ self._connTimer.cancel() self.dev._ignoreConnCallback = False if self._addedConnCallback: self.dev.conn.removeStateCallback(self.connCallback) if reason or not self.dev.conn.isDone or not self.dev.conn.isDisconnected: reason = reason or "unknown reasons" self.dev.setState(self.dev.Failed, reason) if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Failed, textMsg="%s failed to disconnect: %s" % (self.dev, reason)) else: if self.dev.state != self.dev.Disconnected: self.dev.setState(self.dev.Disconnected) if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Done) self.dev.cleanup() def __repr__(self): return "%s(dev.name=%s)" % (type(self).__name__, self.dev.name)
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 TCCLCOActor(BaseActor): """!TCC actor for the LCO telescope """ # SCALE_PER_MM = 8.45e-05 * -1# Scaling ring convention (higher abs values ) SCALE_PER_MM = 8.45e-05 # more MM per scale SCALE_RATIO = 1/7. #Sec dist = SCALE_RATIO * scaling ring dist # MAX_SF = 1.0008 # max scale factor from tcc25m/inst/default.dat MAX_SF = 1.02 MIN_SF = 1./MAX_SF # min scale factor def __init__(self, userPort, tcsDev, scaleDev, m2Dev, measScaleDev, name = "tcc", ): """Construct a TCCActor @param[in] userPort port on which to listen for users @param[in] tcsDev a TCSDevice instance @param[in] scaleDev a ScaleDevice instance @param[in] m2Dev a M2Device instance @param[in] m2Dev a MeasScaleDevice instance @param[in] name actor name; used for logging """ self.tcsDev = tcsDev self.tcsDev.writeToUsers = self.writeToUsers self.scaleDev = scaleDev self.scaleDev.writeToUsers = self.writeToUsers self.secDev = m2Dev self.secDev.writeToUsers = self.writeToUsers self.measScaleDev = measScaleDev self.measScaleDev.writeToUsers = self.writeToUsers # auto connection looks for self.dev self.dev = DeviceCollection([self.tcsDev, self.scaleDev, self.secDev, self.measScaleDev]) # connect devices self.tcsDev.connect() self.scaleDev.connect() self.secDev.connect() self.measScaleDev.connect() self.cmdParser = TCCLCOCmdParser() self.collimationModel = CollimationModel() self.collimateTimer = Timer(0, self.updateCollimation) BaseActor.__init__(self, userPort=userPort, maxUsers=1, name=name, version=__version__) # Actor.__init__(self, userPort=userPort, maxUsers=1, name=name, devs=(tcsDev, scaleDev), version=__version__) @property def currentScaleFactor(self): return self.mm2scale(self.scaleDev.encPos) def scale2mm(self, scaleValue): # scale=1 device is at zero point return -1 * (scaleValue - 1.0) / self.SCALE_PER_MM + self.scaleDev.scaleZeroPos def mm2scale(self, mm): return -1 * (mm - self.scaleDev.scaleZeroPos) * self.SCALE_PER_MM + 1.0 def scaleMult2mm(self, multiplier): return self.scale2mm(self.currentScaleFactor*multiplier) def scaleMult2mmStable(self, multiplier): # this may be more numerically stable, # according to unittests self.scaleMult2mm # works just fine, and it is simpler m = multiplier z = self.scaleDev.scaleZeroPos p = self.scaleDev.encPos alpha = self.SCALE_PER_MM return m*(p-z)+(1.0/alpha)*(m-1.0)+z def parseAndDispatchCmd(self, cmd): """Dispatch the user command @param[in] cmd user command (a twistedActor.UserCmd) """ if not cmd.cmdBody: # echo to show alive self.writeToOneUser(":", "", cmd=cmd) return try: cmd.parsedCmd = self.cmdParser.parseLine(cmd.cmdBody) except Exception as e: cmd.setState(cmd.Failed, "Could not parse %r: %s" % (cmd.cmdBody, strFromException(e))) return #cmd.parsedCmd.printData() if cmd.parsedCmd.callFunc: cmd.setState(cmd.Running) try: cmd.parsedCmd.callFunc(self, cmd) except CommandError as e: cmd.setState("failed", textMsg=strFromException(e)) return except Exception as e: sys.stderr.write("command %r failed\n" % (cmd.cmdStr,)) sys.stderr.write("function %s raised %s\n" % (cmd.parsedCmd.callFunc, strFromException(e))) traceback.print_exc(file=sys.stderr) textMsg = strFromException(e) hubMsg = "Exception=%s" % (e.__class__.__name__,) cmd.setState("failed", textMsg=textMsg, hubMsg=hubMsg) else: raise RuntimeError("Command %r not yet implemented" % (cmd.parsedCmd.cmdVerb,)) def updateCollimation(self, cmd=None, force=False): """ LCO HACK!!! clean this stuff up!!!! """ cmd = expandUserCmd(cmd) if not self.collimationModel.doCollimate and not force: cmd.setState(cmd.Failed, "collimation is disabled") return if "Halted" in self.tcsDev.status.statusFieldDict["state"].value[:2]: # either RA or Dec axis is halted cmd.setState(cmd.Canceled("RA or Dec axis halted, not applying collimation.")) return self.collimateTimer.cancel() # incase one is pending # query for current telescope coords statusCmd = self.tcsDev.getStatus() # when status returns determine current coords def moveMirrorCallback(statusCmd): if statusCmd.didFail: cmd.setState(cmd.Failed, "status command failed") elif statusCmd.isDone: # ha = self.tcsDev.status.statusFieldDict["ha"].value # dec = self.tcsDev.status.statusFieldDict["dec"].value # if an axis is slewing collimate to the target if "Slewing" in self.tcsDev.status.statusFieldDict["state"].value[:2]: # ra or dec is slewing # get target coords # st and ra in degrees st = self.tcsDev.status.statusFieldDict["st"].value ra = self.tcsDev.status.statusFieldDict["inpra"].value ha = st - ra dec = self.tcsDev.status.statusFieldDict["inpdc"].value self.writeToUsers("i", "collimate for target ha=%.2f, dec=%.2f"%(ha, dec)) else: # get current coords ha, dec = self.tcsDev.status.statusFieldDict["pos"].value self.writeToUsers("i", "collimate for current ha=%.2f, dec=%.2f"%(ha, dec)) # self.writeToUsers("i", "pos collimate for ha=%.2f, dec=%.2f"%(pos[0], pos[1])) newOrient = self.collimationModel.getOrientation(ha, dec) orient = self.secDev.status.orientation[:] # check if mirror move is wanted based on tolerances dFocus = None if newOrient[0] is None else newOrient[0]-orient[0] dtiltX = newOrient[1]-orient[1] dtiltY = newOrient[2]-orient[2] dtransX = newOrient[3]-orient[3] dtransY = newOrient[4]-orient[4] doFlex = numpy.max(numpy.abs([dtiltX, dtiltY])) > self.collimationModel.minTilt or numpy.max(numpy.abs([dtransX, dtransY])) > self.collimationModel.minTrans if force: self.writeToUsers("i", "collimation update forced") if not doFlex and not force: self.writeToUsers("i", "collimation flex update too small: dTiltX=%.2f, dTiltY=%.2f, dTransX=%.2f, dTransY=%.2f"%(dtiltX, dtiltY, dtransX, dtransY)) cmd.setState(cmd.Done) else: # update flex values orient[1:] = newOrient[1:] # keep existing focus self.writeToUsers("i", "collimation update: Focus=%.2f, TiltX=%.2f, TiltY=%.2f, TransX=%.2f, TransY=%.2f"%tuple(orient), cmd=cmd) self.secDev.move(orient, userCmd=cmd) statusCmd.addCallback(moveMirrorCallback) # remove timer for now if self.collimationModel.doCollimate: self.collimateTimer.start(self.collimationModel.collimateInterval, self.updateCollimation) else: self.collimateTimer.cancel()
class FakeM2Ctrl(FakeDev): """!A server that emulates the LCO M2 Controller """ Done = "DONE" Error = "ERROR" Moving = "MOVING" On = "on" Off = "off" def __init__(self, name, port): """!Construct a fake LCO M2 @param[in] name name of M2 controller @param[in] port port on which to command M2 State=DONE Ori=12500.0,70.0,-12.0,-600.1,925.0 Lamps=off Galil=off """ self.orientation = [15,70.0,-12.0,-600.1,925.0] self.targOrientation = [15,70.0,-12.0,-600.1,925.0] self.moveState = self.Done self.lamps = self.Off self.galil = self.Off self.speed = 25.0 self.moveTimer = Timer() self.galilTimer = Timer() FakeDev.__init__(self, name = name, port = port, ) def statusStr(self): return "State=%s Ori=%s Lamps=%s Galil=%s"%( self.moveState, ",".join(["%.2f"%val for val in self.orientation]), self.lamps, self.galil ) def parseCmdStr(self, cmdStr): """Parse an incoming command, make it somewhat like the way c100.cpp does things. how to offsets behave, are they sticky, not here """ try: tokens = cmdStr.strip().split(" ") # status requests if tokens[0].lower() == "status": # status self.userSock.writeLine(self.statusStr()) elif tokens[0].lower() == "speed": self.userSock.writeLine("%.1f"%self.speed) elif tokens[0].lower() in ["move", "offset"] and len(tokens)==1: self.userSock.writeLine(" ".join(["%.2f"%val for val in self.orientation])) elif tokens[0].lower() in ["focus", "dfocus"] and len(tokens)==1: self.userSock.writeLine("%.1f"%self.orientation[0]) # commands elif tokens[0].lower() == "stop": self.doMove(stop=True) self.userSock.writeLine("OK") elif tokens[0].lower() in ["move", "focus", "offset", "dfocus"]: isOffset = tokens[0].lower() in ["offset", "dfocus"] for ind, value in enumerate(tokens[1:]): if isOffset: self.targOrientation[ind] += float(value) else: self.targOrientation[ind] = float(value) self.doMove() self.userSock.writeLine("OK") elif tokens[0].lower() == "galil": if tokens[1].lower() == "on": self.powerup() else: self.powerdown() self.userSock.writeLine("OK") else: # unknown command? raise RuntimeError("Unknown Command: %s"%cmdStr) except Exception as e: self.userSock.writeLine("-1") # error! print("Error: ", e) def powerdown(self): self.galil = self.Off def powerup(self, doMove=False): self.moveState = self.Moving self.galilTimer.start(2., self.setDone, doMove) def setDone(self, doMove=False): self.galil = self.On if doMove: self.doMove() else: self.moveState = self.Done def doMove(self, stop=False): """stop: halt focus at it's current location """ if stop: self.moveTimer.cancel() self.moveState = self.Done self.galil = self.Off return if not self.galil == self.On: # simulate power up time self.powerup(doMove=True) # move will start after powerup return self.moveState = self.Moving focus = self.incrementPosition(self.targOrientation[0], self.orientation[0], FocusStepSize) self.orientation[0] = focus if focus != self.targOrientation[0]: # continue moving self.moveTimer.start(TimerDelay, self.doMove) else: self.orientation = self.targOrientation[:] # copy is necessary!!! self.moveState = self.Done def stateCallback(self, server=None): if self.isReady: # self.readyDeferred.callback(None) print("Fake M2 controller %s running on port %s" % (self.name, self.port)) elif self.didFail and not self.readyDeferred.called: errMsg = "Fake M2 controller %s failed to start on port %s" % (self.name, self.port) print(errMsg)
class FakeTCS(FakeDev): """!A server that emulates the LCO TCS defined here: http://espejo.lco.cl/operations/TCS_communication.html """ Idle = 1 Tracking = 2 Slewing = 3 Stop = 4 def __init__(self, name, port): """!Construct a fake LCO TCS @param[in] name name of TCS controller @param[in] port port on which to command TCS """ self.rstop = 0 self.ractive = 0 self.rmoving = 0 self.rtracking = 0 self.dstop = 0 self.dactive = 0 self.dmoving = 0 self.dtracking = 0 self.istop = 0 self.iactive = 0 self.imoving = 0 self.isClamped = 1 self.targRot = 0. self.rot = 0. self.focus = 0. self.targFocus = 0. self.ra = 0. self.dec = 0. self.ha = 0. self.targRA = 0. self.targDec = 0. self.offDec = 0. self.offRA = 0. self.epoch = 2000 self.telState = self.Idle self.focusTimer = Timer() self.slewTimer = Timer() self.rotTimer = Timer() FakeDev.__init__(self, name = name, port = port, ) @property def onTarget(self): return self.targRA == self.ra and self.targDec == self.dec def parseCmdStr(self, cmdStr): """Parse an incoming command, make it somewhat like the way c100.cpp does things. how to offsets behave, are they sticky, not here """ try: tokens = cmdStr.strip().split(" ") # status requests if tokens[0] == "RA" and len(tokens) == 1: # report ra in HMS self.userSock.writeLine(dmsStrFromDeg(self.ra / 15.)) elif tokens[0] == "DEC" and len(tokens) == 1: self.userSock.writeLine(dmsStrFromDeg(self.dec)) elif tokens[0] == "HA" and len(tokens) == 1: self.userSock.writeLine(dmsStrFromDeg(self.ha)) elif tokens[0] == "POS" and len(tokens) == 1: self.userSock.writeLine("%.4f %.4f"%(numpy.radians(self.ha), numpy.radians(self.dec))) elif tokens[0] == "MPOS" and len(tokens) == 1: self.userSock.writeLine("%.4f %.4f"%(numpy.radians(self.ra), numpy.radians(self.dec))) elif tokens[0] == "EPOCH" and len(tokens) == 1: self.userSock.writeLine("%.2f"%(2000)) elif tokens[0] == "ZD" and len(tokens) == 1: self.userSock.writeLine("%.2f"%(80)) elif tokens[0] == "STATE" and len(tokens) == 1: self.userSock.writeLine(str(self.telState)) elif tokens[0] == "INPRA" and len(tokens) == 1: self.userSock.writeLine(str(self.targRA)) elif tokens[0] == "INPDC" and len(tokens) == 1: self.userSock.writeLine(str(self.targDec)) elif tokens[0] == "TELEL" and len(tokens) == 1: self.userSock.writeLine(str(85.2)) # placeholder elif tokens[0] == "TELAZ" and len(tokens) == 1: self.userSock.writeLine(str(40.6)) #placeholder elif tokens[0] == "ROT" and len(tokens) == 1: self.userSock.writeLine(str(30.6)) #placeholder elif tokens[0] == "MRP" and len(tokens) == 1: mrpLine = "%i 0 0 1 3"%(self.isClamped) self.userSock.writeLine(mrpLine) elif tokens[0] == "TEMPS" and len(tokens) == 1: self.userSock.writeLine("18.8 10.8 12.0 11.5 8.8 13.1 -273.1 -273.1") elif tokens[0] == "ST" and len(tokens) == 1: self.userSock.writeLine("06:59:29") elif tokens[0] == "TTRUSS" and len(tokens) == 1: self.userSock.writeLine("10.979") elif tokens[0] == "INPHA" and len(tokens) == 1: self.userSock.writeLine("0") elif tokens[0] == "RAWPOS" and len(tokens) == 1: self.userSock.writeLine("1 1 1 1 1") elif tokens[0] == "AXISSTATUS" and len(tokens) == 1: axisLine = "%i %i %i %i %i %i %i %i %i %i %i" % ( self.rstop, self.ractive, self.rmoving, self.rtracking, self.dstop, self.dactive, self.dmoving, self.dtracking, self.istop, self.iactive, self.imoving ) self.userSock.writeLine(axisLine) # commands elif tokens[0] == "RAD": assert len(tokens) == 2, "Error Parsing RAD" self.targRA = float(tokens[1]) self.userSock.writeLine("0") elif tokens[0] == "DECD": assert len(tokens) == 2, "Error Parsing DECD" self.targDec = float(tokens[1]) self.userSock.writeLine("0") elif tokens[0] == "OFDC": assert len(tokens) == 2, "Error Parsing OFDC" # convert from arcseconds to degrees self.offDec = float(tokens[1]) / ArcsecPerDeg self.userSock.writeLine("0") elif tokens[0] == "OFRA": assert len(tokens) == 2, "Error Parsing OFRA" # convert from arcseconds to degrees self.offRA = float(tokens[1]) / ArcsecPerDeg self.userSock.writeLine("0") elif tokens[0] == "OFFP": assert len(tokens) == 1, "Error Parsising Offset Execute" self.targRA += self.offRA self.targDec += self.offDec self.offRA, self.offDec = 0., 0. self.doSlew() self.userSock.writeLine("0") elif tokens[0] == "UNCLAMP": self.isClamped = 0 self.userSock.writeLine("0") elif tokens[0] == "CLAMP": self.isClamped = 1 self.userSock.writeLine("0") elif tokens[0] == "APGCIR": assert len(tokens) == 2, "Error Parsising APGCIR Execute" self.targRot = float(tokens[1]) self.doRot() self.userSock.writeLine("0") elif tokens[0] == "DCIR": assert len(tokens) == 2, "Error Parsising DCIR Execute" assert self.isClamped == 0, "Rotator is clamped" self.targRot += float(tokens[1]) self.doRot() self.userSock.writeLine("0") elif tokens[0] == "SLEW": raise RuntimeError("SLEWS NOT ALLOWED") # slew to target self.doSlew() self.userSock.writeLine("0") elif tokens[0] == "MP": # set epoch self.MP = float(tokens[1]) self.userSock.writeLine("0") elif tokens[0] == "FOCUS": raise RuntimeError("DON'T USE TCS FOR FOCUS!") if len(tokens) == 1: self.userSock.writeLine(str(self.focus)) else: assert len(tokens) == 2, "Error Parsing Focus" focusArg = tokens[1].upper() if focusArg == "STOP": self.doFocus(stop=True) elif focusArg == "GO": self.doFocus() else: # input new desired focus value and move self.targFocus = float(focusArg) self.doFocus() self.userSock.writeLine("0") elif tokens[0] == "DFOCUS": assert len(tokens) == 2, "Error Parsing DFocus" # input new desired focus value and move self.targFocus += float(tokens[1]) self.doFocus() self.userSock.writeLine("0") else: # unknown command? raise RuntimeError("Unknown Command: %s"%cmdStr) except Exception as e: self.userSock.writeLine("-1") # error! print("Error: ", e) def doSlew(self, offset=False): if not offset: # offset doesn't trigger slewing state self.telState = self.Slewing self.dec = self.incrementPosition(self.targDec, self.dec, AxisStepSize) self.ra = self.incrementPosition(self.targRA, self.ra, AxisStepSize) if self.onTarget: # slew done! self.telState = self.Tracking else: self.slewTimer.start(TimerDelay, self.doSlew) def doRot(self): self.rot = self.incrementPosition(self.targRot, self.rot, RotStepSize) if self.rot != self.targRot: self.rotTimer.start(TimerDelay, self.doRot) def doFocus(self, stop=False): """stop: halt focus at it's current location """ if stop: self.focusTimer.cancel() return self.focus = self.incrementPosition(self.targFocus, self.focus, FocusStepSize) if self.focus != self.targFocus: # continue moving self.focusTimer.start(TimerDelay, self.doFocus) def stateCallback(self, server=None): if self.isReady: # self.readyDeferred.callback(None) print("Fake TCS controller %s running on port %s" % (self.name, self.port)) elif self.didFail and not self.readyDeferred.called: errMsg = "Fake TCS controller %s failed to start on port %s" % (self.name, self.port) print(errMsg)
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 BaseCmd(RO.AddCallback.BaseMixin): """Base class for commands of all types (user and device). """ # state constants Done = "done" Cancelled = "cancelled" # including superseded Failed = "failed" Ready = "ready" Running = "running" Cancelling = "cancelling" Failing = "failing" ActiveStates = frozenset((Running, Cancelling, Failing)) FailedStates = frozenset((Cancelled, Failed)) FailingStates = frozenset((Cancelling, Failing)) DoneStates = frozenset((Done, )) | FailedStates AllStates = frozenset((Ready, )) | ActiveStates | DoneStates _MsgCodeDict = dict( ready="i", running="i", cancelling="w", failing="w", cancelled="f", failed="f", debug="d", done=":", ) _InvMsgCodeDict = dict((val, key) for key, val in _MsgCodeDict.iteritems()) def __init__( self, cmdStr, userID=0, cmdID=0, callFunc=None, timeLim=None, ): """Construct a BaseCmd @param[in] cmdStr command string @param[in] userID user ID number @param[in] cmdID command ID number @param[in] callFunc function to call when command changes state; receives one argument: this command @param[in] timeLim time limit for command (sec); if None or 0 then no time limit """ self._cmdStr = cmdStr self.userID = int(userID) self.cmdID = int(cmdID) self._state = self.Ready self._textMsg = "" self._hubMsg = "" self._cmdToTrack = None self._linkedCommands = [] self._parentCmd = None self._writeToUsers = None # set by baseActor.ExpandCommand # set by baseActor.newCmd to flag this as a command created # from socket input self.userCommanded = False self._timeoutTimer = Timer() self.setTimeLimit(timeLim) RO.AddCallback.BaseMixin.__init__(self, callFunc) @property def parentCmd(self): return self._parentCmd @property def eldestParentCmd(self): if self.parentCmd is None: return self else: return self.parentCmd.eldestParentCmd @property def timeLim(self): return self._timeLim @property def cmdStr(self): return self._cmdStr @property def didFail(self): """Command failed or was cancelled """ return self._state in self.FailedStates @property def isActive(self): """Command is running, canceling or failing """ return self._state in self.ActiveStates @property def isDone(self): """Command is done (whether successfully or not) """ return self._state in self.DoneStates @property def isFailing(self): """Command is being cancelled or is failing """ return self._state in self.FailingStates @property def msgCode(self): """The hub message code appropriate to the current state """ return self._MsgCodeDict[self._state] @property def hubMsg(self): """The hub message or "" if none """ return self._hubMsg @property def textMsg(self): """The text message or "" if none """ return self._textMsg @property def state(self): """The state of the command, as a string which is one of the state constants, e.g. self.Done """ return self._state def setWriteToUsers(self, writeToUsersFunc): if self._writeToUsers is not None: raise RuntimeError("Write to users is already set") else: self._writeToUsers = writeToUsersFunc def writeToUsers(self, msgCode, msgStr, userID=None, cmdID=None): # see doc in baseActor. # self._writeToUsers is set in BaseActor.newCmd() # or is is set at the time of any command linkage # get the top most command for writing to users # it will be the original cmd topCmd = self.eldestParentCmd if topCmd._writeToUsers is None: print("%s writeToUsers not set: " % str(self), msgCode, msgStr, topCmd, userID, cmdID, "!!!") else: topCmd._writeToUsers(msgCode, msgStr, topCmd, userID, cmdID) def addCallback(self, callFunc, callNow=False): """Add a callback function @param[in] callFunc callback function: - it receives one argument: this command - it is called whenever the state changes, and immediately if the command is already done or callNow is True @param[in] callNow if True, call callFunc immediately """ if self.isDone: RO.AddCallback.safeCall2("%s.addCallback callFunc =" % (self, ), callFunc, self) else: RO.AddCallback.BaseMixin.addCallback(self, callFunc, callNow=callNow) def getMsg(self): """Get minimal message in simple format, prefering _textMsg @return _textMsg (if available), else _hubMsg (which may be blank). """ return self._textMsg or self._hubMsg def getKeyValMsg(self, textPrefix=""): """Get full message data as (msgCode, msgStr), where msgStr is in keyword-value format @param[in] textPrefix a prefix added to self._textMsg @return two values: - msgCode: message code (e.g. "W") - msgStr: message string: a combination of _textMsg and _hubMsg in keyword-value format. Warning: he "Text" keyword will be repeated if _textMsg is non-empty and _hubMsg contains "Text=" """ msgCode = self._MsgCodeDict[self._state] msgInfo = [] if self._hubMsg: msgInfo.append(self._hubMsg) if self._textMsg or textPrefix: msgInfo.append("text=%s" % (quoteStr(textPrefix + self._textMsg), )) msgStr = "; ".join(msgInfo) return (msgCode, msgStr) def setState(self, newState, textMsg=None, hubMsg=None): """Set the state of the command and call callbacks. If new state is done then remove all callbacks (after calling them). @param[in] newState new state of command @param[in] textMsg a message to be printed using the Text keyword; if None then not changed @param[in] hubMsg a message in keyword=value format (without a header); if None then not changed You can set both textMsg and hubMsg, but typically only one or the other will be displayed (depending on the situation). If the new state is Failed then please supply a textMsg and/or hubMsg. Error conditions: - Raise RuntimeError if this command is finished. """ # print("%r.setState(newState=%s, textMsg=%r, hubMsg=%r); self._cmdToTrack=%r" % (self, newState, textMsg, hubMsg, self._cmdToTrack)) if self.isDone: raise RuntimeError("Command %s is done; cannot change state" % str(self)) if newState not in self.AllStates: raise RuntimeError("Unknown state %s" % newState) if self._state == self.Ready and newState in self.ActiveStates and self._timeLim: self._timeoutTimer.start(self._timeLim, self._timeout) self._state = newState if textMsg is not None: self._textMsg = str(textMsg) if hubMsg is not None: self._hubMsg = str(hubMsg) log.info(str(self)) self._basicDoCallbacks(self) if self.isDone: self._timeoutTimer.cancel() self._removeAllCallbacks() self.untrackCmd() def setTimeLimit(self, timeLim): """Set a new time limit If the new limit is 0 or None then there is no time limit. If the new limit is < 0, it is ignored and a warning is printed to stderr If the command is has not started running, then the timer starts when the command starts running. If the command is running the timer starts now (any time spent before now is ignored). If the command is done then the new time limit is silently ignored. """ if timeLim and float(timeLim) < 0: sys.stderr.write( "Negative time limit received: %0.2f, and ignored\n" % timeLim) return self._timeLim = float(timeLim) if timeLim else None if self._timeLim: if self.isActive: self._timeoutTimer.start(self._timeLim, self._timeout) else: self._timeoutTimer.cancel() def trackCmd(self, cmdToTrack): """Tie the state of this command to another command When the state of cmdToTrack changes then state, textMsg and hubMsg are copied to this command. @warning: if this command times out before trackCmd is finished, or if the state of this command is set finished, then the link is broken. """ if self.isDone: raise RuntimeError("Finished; cannot track a command") if self._cmdToTrack: raise RuntimeError("Already tracking a command") self._cmdToTrack = cmdToTrack if cmdToTrack.isDone: self._cmdCallback(cmdToTrack) else: cmdToTrack.addCallback(self._cmdCallback) def untrackCmd(self): """Stop tracking a command if tracking one, else do nothing """ if self._cmdToTrack: self._cmdToTrack.removeCallback(self._cmdCallback) self._cmdToTrack = None def removeChildren(self): for cmd in self._linkedCommands: cmd.removeCallback(self.linkCmdCallback) self._linkedCommands = [] def setParentCmd(self, cmd): self._parentCmd = cmd def linkCommands(self, cmdList): """Tie the state of this command to a list of commands If any command in the list fails, so will this command """ if self.isDone: raise RuntimeError("Finished; cannot link commands") if self._cmdToTrack: raise RuntimeError("Already tracking a command") self._linkedCommands.extend(cmdList) for cmd in cmdList: cmd.setParentCmd(self) if not cmd.isDone: cmd.addCallback(self.linkCmdCallback) # call right away in case all sub-commands are already done self.linkCmdCallback() def linkCmdCallback(self, dumCmd=None): """!Callback to be added to each device cmd @param[in] dumCmd sub-command issuing the callback (ignored) """ # if any linked commands have become active and this command is not yet active # set it cto the running state! if self.state == self.Ready and True in [ linkedCommand.isActive for linkedCommand in self._linkedCommands ]: self.setState(self.Running) if not all(linkedCommand.isDone for linkedCommand in self._linkedCommands): # not all device commands have terminated so keep waiting return failedCmdSummary = "; ".join( "%s: %s" % (linkedCommand.cmdStr, linkedCommand.getMsg()) for linkedCommand in self._linkedCommands if linkedCommand.didFail) if failedCmdSummary: # at least one device command failed, fail the user command and say why # note, do we want to match the type of failure? If a subcommand was cancelled # should the mainCmd state be cancelled too? state = self.Failed textMsg = failedCmdSummary else: # all device commands terminated successfully # set user command to done state = self.Done textMsg = "" self.setState(state, textMsg=textMsg) @classmethod def stateFromMsgCode(cls, msgCode): """Return the command state associated with a particular message code """ return cls._InvMsgCodeDict[msgCode] def _cmdCallback(self, cmdToTrack): """Tracked command's state has changed; copy state, textMsg and hubMsg """ self.setState(cmdToTrack.state, textMsg=cmdToTrack.textMsg, hubMsg=cmdToTrack.hubMsg) def _timeout(self): """Call when command has timed out """ if not self.isDone: self.setState(self.Failed, textMsg="Timed out") def _getDescrList(self, doFull=False): """Get list of descriptive strings for __str__ and __repr__ """ descrList = [ repr(self.cmdStr), "state=%s" % (self._state, ), ] if doFull: descrList += [ "userID=%r" % (self.userID, ), "cmdID=%r" % (self.cmdID, ), "timeLim=%r" % (self._timeLim, ), ] if self._textMsg: descrList.append("textMsg=%r" % (self._textMsg, )) if self._hubMsg: descrList.append("hubMsg=%r" % (self._hubMsg, )) if doFull and self._cmdToTrack: descrList.append("cmdToTrack=%r" % (self._cmdToTrack, )) return descrList def __str__(self): return "%s(%s)" % (self.__class__.__name__, ", ".join( self._getDescrList(doFull=False))) def __repr__(self): return "%s(%s)" % (self.__class__.__name__, ", ".join( self._getDescrList(doFull=True)))
class Camera(object): def __init__(self): """A pure python fake camera """ self.state = Idle self.currExposure = Exposure(None, None, None) self.expTimer = Timer() self.readTimer = Timer() self.expBegin = None self.expAccumulated = None self.setConfig(CameraConfig()) self._expName = "" self.failExposure = False # for unittesting def isBusy(self): return self.state != Idle def startExposure(self, expTime, expType, name): if self.failExposure: raise RuntimeError("startExposure() failed!") assert expType in [Bias, Dark, Flat, Object] assert not self.isBusy() self.currExposure = Exposure(expTime, expType, name) self.expAccumulated = 0 self._expName = name self._startOrResumeExposure(expTime) def pauseExposure(self): assert self.state == Exposing self.expAccumulated = self.expAccumulated + time.time() - self.expBegin self._cancelTimers() self._setState(Paused) def resumeExposure(self): assert self.state == self.Paused remExpTime = self.currExposure.expTime - self.expAccumulated self._startOrResumeExposure(remExpTime) def abortExposure(self): assert self.state == Exposing self._cancelTimers() self._setState(Idle) def stopExposure(self): assert self.state == Exposing self._cancelTimers() self._setState(ImageRead) def getExposureState(self): expTime = self.currExposure.expTime if self.state != Idle: timeRemaining = expTime - (time.time() - self.expBegin) else: timeRemaining = 0. return ExposureState(self.state, expTime or 0., timeRemaining) def getConfig(self): return self._config def setConfig(self, config): config.assertValid() self._assertIdle() self._config = config; def saveImage(self, expTime=-1): # save an empty file # fileName = "%s_%s_%s.fakeImage"%(self.currExposure.expType, self.currExposure.expTime, self.currExposure.name) # print "exptype", self.currExposure.expType # print "expTime", self.currExposure.expTime # print "expName", self.currExposure.name n = numpy.arange(100.0) # a simple sequence of floats from 0.0 to 99.9 hdu = fits.PrimaryHDU(n) hdulist = fits.HDUList([hdu]) hdulist[0].header["comment"] = "This is a fake image, used for testing." hdulist.writeto(self.currExposure.name) # open(self.currExposure.name, "w").close() self.state = Idle def openShutter(self): pass def closeShutter(self): pass def _setState(self, state): assert state in [Idle, Exposing, Paused, Reading, ImageRead] self.state = state def _startOrResumeExposure(self, expTime): self._setState(Exposing) self.expTimer.start(expTime, self._setState, Reading) self.readTimer.start(expTime+readTime, self._setState, ImageRead) self.expBegin = time.time() def _cancelTimers(self): self.readTimer.cancel() self.expTimer.cancel() def _assertIdle(self): if not self.state == Idle: raise RuntimeError("busy")
class FilterWheel(object): def __init__(self): self.moveTimer = Timer() def stopMove(self): self.moveTimer.cancel() StatusDict["motor"] = 0 # StatusDict["motor"]=8 def connect(self): """ initiate filter wheel communication """ pass def home(self): """ home filter wheel and set the filter wheel id """ # emulate the block time.sleep(3) # StatusDict["id"] = 1 return StatusDict["id"] def stop(self): """ stop filter wheel motion """ return self.stopMove() def moveToPosition(self, pos): """ move to a specific filter wheel position arguments: int pos - filter position between 1 and 6 status: 0 - fail 1 - succeed -1 - unknown """ StatusDict["position"] = int(pos) # StatusDict["motor"] = 1 StatusDict["motor"] = 3 self.moveTimer.start(MoveTime, self.stopMove) return self.status() def _setDiffuPos(self, val): StatusDict["diffuInBeam"] = val def startDiffuRot(self): StatusDict["diffuRotating"] = 1 def stopDiffuRot(self): StatusDict["diffuRotating"] = 0 def diffuIn(self): Timer(0.2, self._setDiffuPos, 1) # set on a timer to emulate move time def diffuOut(self): Timer(0.2, self._setDiffuPos, 0) # set on a timer to emulate move time def status(self): """ return current status of filter wheel """ return StatusDict
class Socket(BaseSocket): """A socket using Twisted framework. """ def __init__(self, endpoint = None, protocol = None, state = BaseSocket.Connected, readCallback = nullCallback, stateCallback = nullCallback, timeLim = None, name = "", ): """Construct a Socket Inputs: - endpoint a Twisted endpoint, e.g. twisted.internet.endpoints.TCP4ClientEndpoint; - protocol a Twisted protocol; you must either specify endpoint or protocol, but not both - state the initial state - readCallback function to call when data read; receives: self - stateCallback a state callback function; see addStateCallback for details - timeLim time limit to make connection (sec); no limit if None or 0 - name a string to identify this socket; strictly optional """ #print "Socket(name=%r, endpoint=%r, protocol=%r, state=%r, readCallback=%r, stateCallback=%r)" % \ # (name, endpoint, protocol, state, readCallback, stateCallback) if bool(endpoint is None) == bool(protocol is None): raise RuntimeError("Must provide one of endpoint or protocol") self._endpoint = endpoint self._endpointDeferred = None self._protocol = None self._data = None self._connectTimer = Timer() BaseSocket.__init__(self, state = state, readCallback = readCallback, stateCallback = stateCallback, name = name ) if protocol is not None: self._connectionMade(protocol) else: if timeLim: self._connectTimer.start(timeLim, self._connectTimeout) self._setState(BaseSocket.Connecting) self._endpointDeferred = self._endpoint.connect(_SocketProtocolFactory()) setCallbacks(self._endpointDeferred, self._connectionMade, self._connectionLost) def _clearCallbacks(self): """Clear any callbacks added by this class. Called just after the socket is closed. """ BaseSocket._clearCallbacks(self) self._connCallback = None if self._endpointDeferred is not None: self._endpointDeferred.cancel() self._endpointDeferred = None if self._protocol is not None: self._protocol.roAbort() self._protocol = None def _connectionLost(self, reason): """Connection lost callback """ #print "%s._connectionLost(reason=%r)" % (self, reason) if reason is None: reasonStrOrNone = None elif isinstance(reason, ConnectionDone): # connection closed cleanly; no need for a reason # use getattr in case reason as no type attribute reasonStrOrNone = None else: reasonStrOrNone = str(reason) if self._state == BaseSocket.Closing: self._setState(BaseSocket.Closed, reasonStrOrNone) else: self._setState(BaseSocket.Failed, reasonStrOrNone) def _connectionMade(self, protocol): """Callback when connection made """ #print "%s._connectionMade(protocol=%r)" % (self, protocol) self._protocol = protocol self._protocol.roSetCallbacks( readCallback = self._doRead, connectionLostCallback = self._connectionLost, ) self._setState(BaseSocket.Connected) def _basicClose(self): """Close the socket. """ if self._protocol is not None: self._protocol.transport.loseConnection() @property def host(self): """Return the address, or None if not known """ if self._protocol: return getattr(self._protocol.transport.getPeer(), "host", None) return None @property def port(self): """Return the port, or None if unknown """ if self._protocol: return getattr(self._protocol.transport.getPeer(), "port", None) return None def read(self, nChar=None): """Read data. Do not block. Inputs: - nChar: maximum number of chars to return; if None then all available data is returned. Raise RuntimeError if the socket is not connected. """ if not self.isReady: raise RuntimeError("%s not connected" % (self,)) return self._protocol.read(nChar) def readLine(self, default=None): """Read one line of data, not including the end-of-line indicator. Do not block. Any of \r\n, \r or \n are treated as end of line. Inputs: - default value to return if a full line is not available (in which case no data is read) Raise RuntimeError if the socket is not connected. """ if not self.isReady: raise RuntimeError("%s not connected" % (self,)) return self._protocol.readLine(default) def write(self, data): """Write data to the socket (without blocking) Safe to call as soon as you call connect, but of course no data is sent until the connection is made. Raise UnicodeError if the data cannot be expressed as ascii. Raise RuntimeError if the socket is not connecting or connected. If an error occurs while sending the data, the socket is closed, the state is set to Failed and _reason is set. An alternate technique (from Craig): turn } into \}; consider escaping null and all but the final \n in the same fashion (to do this it probably makes sense to supply a writeLine that escapes \n and \r and then appends \n). Then: self._tk.eval('puts -nonewline %s { %s }' % (self._sock, escData)) """ #print "%s.write(%r)" % (self, data) if not self.isReady: raise RuntimeError("%s.write(%r) failed: not connected" % (self, data)) self._protocol.transport.write(str(data)) def writeLine(self, data): """Write a line of data terminated by standard newline """ #print "%s.writeLine(data=%r)" % (self, data) self.write(data + "\r\n") def _connectTimeout(self): """Call if connection times out """ if not self.isReady or self.isDone: self.close(isOK=False, reason="timeout") def _doRead(self, sock): """Called when there is data to read """ if not self.isReady: return if self._readCallback: try: self._readCallback(self) except Exception as e: sys.stderr.write("%s read callback %s failed: %s\n" % (self, self._readCallback, e,)) traceback.print_exc(file=sys.stderr) def _setState(self, *args, **kwargs): BaseSocket._setState(self, *args, **kwargs) if self.isReady or self.isDone: self._connectTimer.cancel()
class BaseCmd(RO.AddCallback.BaseMixin): """Base class for commands of all types (user and device). """ # state constants Done = "done" Cancelled = "cancelled" # including superseded Failed = "failed" Ready = "ready" Running = "running" Cancelling = "cancelling" Failing = "failing" ActiveStates = frozenset((Running, Cancelling, Failing)) FailedStates = frozenset((Cancelled, Failed)) FailingStates = frozenset((Cancelling, Failing)) DoneStates = frozenset((Done,)) | FailedStates AllStates = frozenset((Ready,)) | ActiveStates | DoneStates _MsgCodeDict = dict( ready = "i", running = "i", cancelling = "w", failing = "w", cancelled = "f", failed = "f", debug = "d", done = ":", ) _InvMsgCodeDict = dict((val, key) for key, val in _MsgCodeDict.iteritems()) def __init__(self, cmdStr, userID = 0, cmdID = 0, callFunc = None, timeLim = None, ): """Construct a BaseCmd @param[in] cmdStr command string @param[in] userID user ID number @param[in] cmdID command ID number @param[in] callFunc function to call when command changes state; receives one argument: this command @param[in] timeLim time limit for command (sec); if None or 0 then no time limit """ self._cmdStr = cmdStr self.userID = int(userID) self.cmdID = int(cmdID) self._state = self.Ready self._textMsg = "" self._hubMsg = "" self._cmdToTrack = None self._linkedCommands = [] self._parentCmd = None self._writeToUsers = None # set by baseActor.ExpandCommand # set by baseActor.newCmd to flag this as a command created # from socket input self.userCommanded = False self._timeoutTimer = Timer() self.setTimeLimit(timeLim) RO.AddCallback.BaseMixin.__init__(self, callFunc) @property def parentCmd(self): return self._parentCmd @property def eldestParentCmd(self): if self.parentCmd is None: return self else: return self.parentCmd.eldestParentCmd @property def timeLim(self): return self._timeLim @property def cmdStr(self): return self._cmdStr @property def didFail(self): """Command failed or was cancelled """ return self._state in self.FailedStates @property def isActive(self): """Command is running, canceling or failing """ return self._state in self.ActiveStates @property def isDone(self): """Command is done (whether successfully or not) """ return self._state in self.DoneStates @property def isFailing(self): """Command is being cancelled or is failing """ return self._state in self.FailingStates @property def msgCode(self): """The hub message code appropriate to the current state """ return self._MsgCodeDict[self._state] @property def hubMsg(self): """The hub message or "" if none """ return self._hubMsg @property def textMsg(self): """The text message or "" if none """ return self._textMsg @property def state(self): """The state of the command, as a string which is one of the state constants, e.g. self.Done """ return self._state def setWriteToUsers(self, writeToUsersFunc): if self._writeToUsers is not None: raise RuntimeError("Write to users is already set") else: self._writeToUsers = writeToUsersFunc def writeToUsers(self, msgCode, msgStr, userID=None, cmdID=None): # see doc in baseActor. # self._writeToUsers is set in BaseActor.newCmd() # or is is set at the time of any command linkage # get the top most command for writing to users # it will be the original cmd topCmd = self.eldestParentCmd if topCmd._writeToUsers is None: print("%s writeToUsers not set: "%str(self), msgCode, msgStr, topCmd, userID, cmdID, "!!!") else: topCmd._writeToUsers(msgCode, msgStr, topCmd, userID, cmdID) def addCallback(self, callFunc, callNow=False): """Add a callback function @param[in] callFunc callback function: - it receives one argument: this command - it is called whenever the state changes, and immediately if the command is already done or callNow is True @param[in] callNow if True, call callFunc immediately """ if self.isDone: RO.AddCallback.safeCall2("%s.addCallback callFunc =" % (self,), callFunc, self) else: RO.AddCallback.BaseMixin.addCallback(self, callFunc, callNow=callNow) def getMsg(self): """Get minimal message in simple format, prefering _textMsg @return _textMsg (if available), else _hubMsg (which may be blank). """ return self._textMsg or self._hubMsg def getKeyValMsg(self, textPrefix=""): """Get full message data as (msgCode, msgStr), where msgStr is in keyword-value format @param[in] textPrefix a prefix added to self._textMsg @return two values: - msgCode: message code (e.g. "W") - msgStr: message string: a combination of _textMsg and _hubMsg in keyword-value format. Warning: he "Text" keyword will be repeated if _textMsg is non-empty and _hubMsg contains "Text=" """ msgCode = self._MsgCodeDict[self._state] msgInfo = [] if self._hubMsg: msgInfo.append(self._hubMsg) if self._textMsg or textPrefix: msgInfo.append("text=%s" % (quoteStr(textPrefix + self._textMsg),)) msgStr = "; ".join(msgInfo) return (msgCode, msgStr) def setState(self, newState, textMsg=None, hubMsg=None): """Set the state of the command and call callbacks. If new state is done then remove all callbacks (after calling them). @param[in] newState new state of command @param[in] textMsg a message to be printed using the Text keyword; if None then not changed @param[in] hubMsg a message in keyword=value format (without a header); if None then not changed You can set both textMsg and hubMsg, but typically only one or the other will be displayed (depending on the situation). If the new state is Failed then please supply a textMsg and/or hubMsg. Error conditions: - Raise RuntimeError if this command is finished. """ # print("%r.setState(newState=%s, textMsg=%r, hubMsg=%r); self._cmdToTrack=%r" % (self, newState, textMsg, hubMsg, self._cmdToTrack)) if self.isDone: raise RuntimeError("Command %s is done; cannot change state" % str(self)) if newState not in self.AllStates: raise RuntimeError("Unknown state %s" % newState) if self._state == self.Ready and newState in self.ActiveStates and self._timeLim: self._timeoutTimer.start(self._timeLim, self._timeout) self._state = newState if textMsg is not None: self._textMsg = str(textMsg) if hubMsg is not None: self._hubMsg = str(hubMsg) log.info(str(self)) self._basicDoCallbacks(self) if self.isDone: self._timeoutTimer.cancel() self._removeAllCallbacks() self.untrackCmd() def setTimeLimit(self, timeLim): """Set a new time limit If the new limit is 0 or None then there is no time limit. If the new limit is < 0, it is ignored and a warning is printed to stderr If the command is has not started running, then the timer starts when the command starts running. If the command is running the timer starts now (any time spent before now is ignored). If the command is done then the new time limit is silently ignored. """ if timeLim and float(timeLim) < 0: sys.stderr.write("Negative time limit received: %0.2f, and ignored\n"%timeLim) return self._timeLim = float(timeLim) if timeLim else None if self._timeLim: if self.isActive: self._timeoutTimer.start(self._timeLim, self._timeout) else: self._timeoutTimer.cancel() def trackCmd(self, cmdToTrack): """Tie the state of this command to another command When the state of cmdToTrack changes then state, textMsg and hubMsg are copied to this command. @warning: if this command times out before trackCmd is finished, or if the state of this command is set finished, then the link is broken. """ if self.isDone: raise RuntimeError("Finished; cannot track a command") if self._cmdToTrack: raise RuntimeError("Already tracking a command") self._cmdToTrack = cmdToTrack if cmdToTrack.isDone: self._cmdCallback(cmdToTrack) else: cmdToTrack.addCallback(self._cmdCallback) def untrackCmd(self): """Stop tracking a command if tracking one, else do nothing """ if self._cmdToTrack: self._cmdToTrack.removeCallback(self._cmdCallback) self._cmdToTrack = None def removeChildren(self): for cmd in self._linkedCommands: cmd.removeCallback(self.linkCmdCallback) self._linkedCommands = [] def setParentCmd(self, cmd): self._parentCmd = cmd def linkCommands(self, cmdList): """Tie the state of this command to a list of commands If any command in the list fails, so will this command """ if self.isDone: raise RuntimeError("Finished; cannot link commands") if self._cmdToTrack: raise RuntimeError("Already tracking a command") self._linkedCommands.extend(cmdList) for cmd in cmdList: cmd.setParentCmd(self) if not cmd.isDone: cmd.addCallback(self.linkCmdCallback) # call right away in case all sub-commands are already done self.linkCmdCallback() def linkCmdCallback(self, dumCmd=None): """!Callback to be added to each device cmd @param[in] dumCmd sub-command issuing the callback (ignored) """ # if any linked commands have become active and this command is not yet active # set it cto the running state! if self.state == self.Ready and True in [linkedCommand.isActive for linkedCommand in self._linkedCommands]: self.setState(self.Running) if not all(linkedCommand.isDone for linkedCommand in self._linkedCommands): # not all device commands have terminated so keep waiting return failedCmdSummary = "; ".join("%s: %s" % (linkedCommand.cmdStr, linkedCommand.getMsg()) for linkedCommand in self._linkedCommands if linkedCommand.didFail) if failedCmdSummary: # at least one device command failed, fail the user command and say why # note, do we want to match the type of failure? If a subcommand was cancelled # should the mainCmd state be cancelled too? state = self.Failed textMsg = failedCmdSummary else: # all device commands terminated successfully # set user command to done state = self.Done textMsg = "" self.setState(state, textMsg = textMsg) @classmethod def stateFromMsgCode(cls, msgCode): """Return the command state associated with a particular message code """ return cls._InvMsgCodeDict[msgCode] def _cmdCallback(self, cmdToTrack): """Tracked command's state has changed; copy state, textMsg and hubMsg """ self.setState(cmdToTrack.state, textMsg=cmdToTrack.textMsg, hubMsg=cmdToTrack.hubMsg) def _timeout(self): """Call when command has timed out """ if not self.isDone: self.setState(self.Failed, textMsg="Timed out") def _getDescrList(self, doFull=False): """Get list of descriptive strings for __str__ and __repr__ """ descrList = [ repr(self.cmdStr), "state=%s" % (self._state,), ] if doFull: descrList += [ "userID=%r" % (self.userID,), "cmdID=%r" % (self.cmdID,), "timeLim=%r" % (self._timeLim,), ] if self._textMsg: descrList.append("textMsg=%r" % (self._textMsg,)) if self._hubMsg: descrList.append("hubMsg=%r" % (self._hubMsg,)) if doFull and self._cmdToTrack: descrList.append("cmdToTrack=%r" % (self._cmdToTrack,)) return descrList def __str__(self): return "%s(%s)" % (self.__class__.__name__, ", ".join(self._getDescrList(doFull=False))) def __repr__(self): return "%s(%s)" % (self.__class__.__name__, ", ".join(self._getDescrList(doFull=True)))
class FakeScaleCtrl(FakeDev): """!A server that emulates the LCO scale """ def __init__(self, name, port): """!Construct a fake LCO scale controller @param[in] name name of scale controller @param[in] port port on which to command scale controller """ global GlobalScalePosition self.isMoving = False self.moveRange = [0., 40.] self.desPosition = GlobalScalePosition # wake up in mid travel self.position = GlobalScalePosition self.speed = 0.5 self.moveTimer = Timer() self.posSw1, self.posSw2, self.posSw3 = (1, 1, 1) self.cartID = 0 self.lockPos = 18 # faults self.trOT = False self.trHWF, self.trIF = 0, 0 self.lockHWF, self.lockIF = 0, 0 self.winchHWF, self.winchIF = 0, 0 self.positionTimer = Timer() self.userSock = None # created upon connection FakeDev.__init__(self, name = name, port = port, doEcho = True, ) # self.sendPositionLoop() # self.moveLoop() def moveLoop(self): # loops always global GlobalScalePosition self.position = self.incrementPosition(self.desPosition, self.position, self.speed*TimerDelay) GlobalScalePosition = self.position if self.position == self.desPosition: # move is done, send OK self.userSock.writeLine("OK") self.isMoving = False else: # keep moving self.moveTimer.start(TimerDelay/10., self.moveLoop) def parseCmdStr(self, cmdStr): if "status" in cmdStr.lower(): self.sendStatusAndOK() elif "move" in cmdStr.lower(): desPos = float(cmdStr.split()[-1]) if not self.moveRange[0] <= desPos <= self.moveRange[1]: self.userSock.writeLine("ERROR OUT_OF_RANGE") self.userSock.writeLine("OK") else: self.desPosition = desPos self.isMoving = True self.moveTimer.start(0, self.moveLoop) self.sendPositionLoop() elif "speed" in cmdStr.lower(): self.speed = float(cmdStr.split()[-1]) self.userSock.writeLine("OK") elif "stop" in cmdStr.lower(): self.stop() else: # unrecognized command self.userSock.writeLine("ERROR INVALID_COMMAND") self.userSock.writeLine("OK") def stop(self): # moveLoop will send the ok self.desPosition = self.position self.moveTimer.start(0, self.moveLoop) def sendPositionLoop(self): # only write if we are "moving" if self.userSock is not None and self.isMoving == True: currPosStr = "__ACTUAL_POSITION %.6f"%self.position self.userSock.writeLine(currPosStr) self.positionTimer.start(TimerDelay, self.sendPositionLoop) # def sendStatusAndOK(self): # statusLines = [ # "CURRENT_POSITION %.6f"%self.position, # "TARGET_POSITION %.6f"%self.desPosition, # "CARTRIDGE_ID 4", # "DRIVE_SPEED 23", # "DRIVE_ACCEL 3", # "MOVE_RANGE %.1f - %.1f"%(self.moveRange[0], self.moveRange[1]), # "HARDWARE FAULT NONE", # "INSTRUCTION_FAULT NONE", # "OK" # ] # for line in statusLines: # self.userSock.writeLine(line) def sendStatusAndOK(self): # global munge # munge = munge*-1 # if munge == 1: # # else: # pos = "MUNGED" pos = "%.7f"%self.position statusLines = [ "THREAD_RING_AXIS:", "__ACTUAL_POSITION %s"%pos, "__TARGET_POSITION 0.20000000", "__DRIVE_STATUS: OFF", "__MOTOR_CURRENT: -0.39443308", "__DRIVE_SPEED %.7f"%self.speed, "__DRIVE_ACCEL 20", "__DRIVE_DECEL 20", "__MOVE_RANGE %.4f - %.4f"%tuple(self.moveRange), "__HARDWARE_FAULT %i"%(self.trHWF), "__INSTRUCTION_FAULT %i"%(self.trIF), "__THREADRING_OVERTRAVEL_%s"%("ON" if bool(self.trOT) else "OFF"), "LOCK_RING_AXIS:", "__ACTUAL_POSITION %.7f"%(self.lockPos), "__TARGET_POSITION 18.0000000", "__OPEN_SETPOINT: 150.000000", "__LOCKED_SETPOINT: 18.0000000", "__DRIVE_STATUS: OFF", "__MOTOR_CURRENT: 0.0", "__DRIVE_SPEED 50.0000000", "__DRIVE_ACCEL 20", "__DRIVE_DECEL 20", "__MOVE_RANGE 0.0 - 152.399994", "__HARDWARE_FAULT %i"%(self.lockHWF), "__INSTRUCTION_FAULT %i"%(self.lockIF), "WINCH_AXIS:", "__ACTUAL_POSITION -1840.48157", "__TARGET_POSITION 1652.00000", "__UP_SETPOINT: 23.0000000", "__TO_CART_SETPOINT: 1560.00000", "__ON_CART_SETPOINT: 1652.00000", "__RELEASE_SETPOINT: 1695.00000", "__DRIVE_STATUS: OFF", "__MOTOR_CURRENT: -0.02553883", "__DRIVE_SPEED 50.0000000", "__DRIVE_ACCEL 2", "__DRIVE_DECEL 2", "__MOVE_RANGE 0.0 - 3000.00000", "__HARDWARE_FAULT %i"%self.winchHWF, "__INSTRUCTION_FAULT %i"%self.winchIF, "SCALE_1: 1.70607793", "SCALE 2: 1.66883636", "SCALE 3: -0.07550588", "CARTRIDGE_ID %i"%self.cartID, "__ID_SW: 0 1 2 3 4 5 6 7 8", " 0 0 0 0 0 0 0 0 0", "__POS_SW: 1 2 3", " %i %i %i"%(self.posSw1, self.posSw2, self.posSw3), "WINCH_HOOK_SENSOR: OFF", "WINCH_ENCODER_1_POS: 0.0", "WINCH_ENCODER_2_POS: 0.0", "WINCH_ENCODER_3_POS: 0.0", "OK", ] for line in statusLines: self.userSock.writeLine(line) def stateCallback(self, server=None): if self.isReady: # self.readyDeferred.callback(None) print("Fake scale controller %s running on port %s" % (self.name, self.port)) elif self.didFail and not self.readyDeferred.called: errMsg = "Fake scale controller %s failed to start on port %s" % (self.name, self.port) print(errMsg)
class Socket(BaseSocket): """A socket using Twisted framework. """ def __init__( self, endpoint=None, protocol=None, state=BaseSocket.Connected, readCallback=nullCallback, stateCallback=nullCallback, timeLim=None, name="", ): """Construct a Socket Inputs: - endpoint a Twisted endpoint, e.g. twisted.internet.endpoints.TCP4ClientEndpoint; - protocol a Twisted protocol; you must either specify endpoint or protocol, but not both - state the initial state - readCallback function to call when data read; receives: self - stateCallback a state callback function; see addStateCallback for details - timeLim time limit to make connection (sec); no limit if None or 0 - name a string to identify this socket; strictly optional """ #print "Socket(name=%r, endpoint=%r, protocol=%r, state=%r, readCallback=%r, stateCallback=%r)" % \ # (name, endpoint, protocol, state, readCallback, stateCallback) if bool(endpoint is None) == bool(protocol is None): raise RuntimeError("Must provide one of endpoint or protocol") self._endpoint = endpoint self._endpointDeferred = None self._protocol = None self._data = None self._connectTimer = Timer() BaseSocket.__init__(self, state=state, readCallback=readCallback, stateCallback=stateCallback, name=name) if protocol is not None: self._connectionMade(protocol) else: if timeLim: self._connectTimer.start(timeLim, self._connectTimeout) self._setState(BaseSocket.Connecting) self._endpointDeferred = self._endpoint.connect( _SocketProtocolFactory()) setCallbacks(self._endpointDeferred, self._connectionMade, self._connectionLost) def _clearCallbacks(self): """Clear any callbacks added by this class. Called just after the socket is closed. """ BaseSocket._clearCallbacks(self) self._connCallback = None if self._endpointDeferred is not None: self._endpointDeferred.cancel() self._endpointDeferred = None if self._protocol is not None: self._protocol.roAbort() self._protocol = None def _connectionLost(self, reason): """Connection lost callback """ #print "%s._connectionLost(reason=%r)" % (self, reason) if reason is None: reasonStrOrNone = None elif isinstance(reason, ConnectionDone): # connection closed cleanly; no need for a reason # use getattr in case reason as no type attribute reasonStrOrNone = None else: reasonStrOrNone = str(reason) if self._state == BaseSocket.Closing: self._setState(BaseSocket.Closed, reasonStrOrNone) else: self._setState(BaseSocket.Failed, reasonStrOrNone) def _connectionMade(self, protocol): """Callback when connection made """ #print "%s._connectionMade(protocol=%r)" % (self, protocol) self._protocol = protocol self._protocol.roSetCallbacks( readCallback=self._doRead, connectionLostCallback=self._connectionLost, ) self._setState(BaseSocket.Connected) def _basicClose(self): """Close the socket. """ if self._protocol is not None: self._protocol.transport.loseConnection() @property def host(self): """Return the address, or None if not known """ if self._protocol: return getattr(self._protocol.transport.getPeer(), "host", None) return None @property def port(self): """Return the port, or None if unknown """ if self._protocol: return getattr(self._protocol.transport.getPeer(), "port", None) return None def read(self, nChar=None): """Read data. Do not block. Inputs: - nChar: maximum number of chars to return; if None then all available data is returned. Raise RuntimeError if the socket is not connected. """ if not self.isReady: raise RuntimeError("%s not connected" % (self, )) return self._protocol.read(nChar) def readLine(self, default=None): """Read one line of data, not including the end-of-line indicator. Do not block. Any of \r\n, \r or \n are treated as end of line. Inputs: - default value to return if a full line is not available (in which case no data is read) Raise RuntimeError if the socket is not connected. """ if not self.isReady: raise RuntimeError("%s not connected" % (self, )) return self._protocol.readLine(default) def write(self, data): """Write data to the socket (without blocking) Safe to call as soon as you call connect, but of course no data is sent until the connection is made. Raise UnicodeError if the data cannot be expressed as ascii. Raise RuntimeError if the socket is not connecting or connected. If an error occurs while sending the data, the socket is closed, the state is set to Failed and _reason is set. An alternate technique (from Craig): turn } into \}; consider escaping null and all but the final \n in the same fashion (to do this it probably makes sense to supply a writeLine that escapes \n and \r and then appends \n). Then: self._tk.eval('puts -nonewline %s { %s }' % (self._sock, escData)) """ #print "%s.write(%r)" % (self, data) if not self.isReady: raise RuntimeError("%s.write(%r) failed: not connected" % (self, data)) self._protocol.transport.write(str(data)) def writeLine(self, data): """Write a line of data terminated by standard newline """ #print "%s.writeLine(data=%r)" % (self, data) self.write(data + "\r\n") def _connectTimeout(self): """Call if connection times out """ if not self.isReady or self.isDone: self.close(isOK=False, reason="timeout") def _doRead(self, sock): """Called when there is data to read """ if not self.isReady: return if self._readCallback: try: self._readCallback(self) except Exception as e: sys.stderr.write("%s read callback %s failed: %s\n" % ( self, self._readCallback, e, )) traceback.print_exc(file=sys.stderr) def _setState(self, *args, **kwargs): BaseSocket._setState(self, *args, **kwargs) if self.isReady or self.isDone: self._connectTimer.cancel()
class ConnectDevice(object): """!Connect a device and execute dev.init If the device is already connected then generate a new userCmd, if needed, and sets userCmd's state to userCmd.Done. Public attributes: - dev: the provided device - userCmd: the provided userCmd, or a new one if none provided """ def __init__(self, dev, userCmd, timeLim): """!Start connecting a device @param[in] dev device @param[in] userCmd user command associated with the connection, or None @param[in] timeLim time limit (sec) to make this connection """ self.dev = dev self._timeLim = timeLim self.userCmd = expandUserCmd(userCmd) self._connTimer = Timer() self._addedConnCallback = False if self.dev.isConnected and self.dev.conn.isConnected: # already done; don't send init self.finish() return self.dev._ignoreConnCallback = True self.dev.setState(self.dev.Connecting) self.dev.conn.addStateCallback(self.connCallback) self._addedConnCallback = True if self.dev.conn.mayConnect: self.dev.conn.connect(timeLim=timeLim) else: if self._timeLim: # start timer for the connection that is occurring now self._connTimer.start(self._timeLim, self.finish, "timed out waiting for connection") def initCallback(self, userCmd): """!Callback for device initialization """ # print("%s.initCallback(userCmd=%r); _callbacks=%s" % (self, userCmd, userCmd._callbacks)) if not userCmd.isDone: return if userCmd.didFail: reason = userCmd.getMsg( ) or "init command failed for unknown reasons" else: reason = None Timer(0, self.finish, reason) def connCallback(self, conn): """!Callback for device connection state """ if self.dev.conn.isConnected: self._connTimer.cancel() initUserCmd = UserCmd(cmdStr="connect %s" % (self.dev.name, ), callFunc=self.initCallback) self.dev.init(userCmd=initUserCmd, timeLim=self._timeLim, getStatus=True) elif self.dev.conn.didFail: self._connTimer.cancel() self.finish("connection failed") def finish(self, reason=None): """!Call on success or failure to finish the command @param[in] reason: reason for failure (if non-empty then failure is assumed) """ self._connTimer.cancel() self.dev._ignoreConnCallback = False if self._addedConnCallback: self.dev.conn.removeStateCallback(self.connCallback) if reason or not self.dev.conn.isConnected: reason = reason or "unknown reason" self.dev.setState(self.dev.Failed, reason) if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Failed, textMsg="%s failed to connect: %s" % (self.dev, reason)) Timer(0, self.dev.conn.disconnect) else: self.dev.setState(self.dev.Connected) if not self.userCmd.isDone: self.userCmd.setState(self.userCmd.Done) def __repr__(self): return "%s(dev.name=%s)" % (type(self).__name__, self.dev.name)