1 from __future__
import division, absolute_import
6 from coordConv
import PVT
7 from twistedActor
import TCPDevice, log, CommandQueue, LinkCommands, expandUserCmd
8 from RO.AddCallback
import safeCall2
9 from RO.StringUtil
import strFromException, quoteStr
12 from tcc.util import dateFromSecInDay, RunningStats
14 __all__ = [
"AxisDevice"]
19 TrackAdvTimeBufferSize = 20
20 TrackAdvTimeReportEvery = 10
23 """!Axis controller status
26 """!Construct an AxisStatus
35 """!Return a copy of this object
37 return copy.copy(self)
40 """!Set values from status reply
42 @param[in] reply reply from STATUS command, in format:
43 pos vel secInDay statusWord indexPosition
44 all values are float except statusWord is an int
46 log.info(
"%s.setFromReply(reply=%s)"%(self, reply))
47 dataList = reply.split()
48 if len(dataList) != 5:
49 raise RuntimeError(
"Expected 5 values for status but got %r" % (reply,))
51 pos = float(dataList[0])
52 vel = float(dataList[1])
53 secInDay = float(dataList[2])
54 statusWord = int(dataList[3])
55 indexPosition = float(dataList[4])
57 raise RuntimeError(
"Could not parse %s as status" % (
" ".join(dataList),))
59 self.
pvt = PVT(pos, vel, taiDate)
66 """!Return True if the status word has changed or dTime has changed significantly
68 @param[in] rhs another AxisStatus, against which to compare
70 Use to determine if new status should be reported. Thus "significantly" for dTime
71 means that the formatted value will look different.
73 return (self.
statusWord != rhs.statusWord)
or (abs(self.
dTime-rhs.dTime)>0.1)
76 """!Format status message
78 @param[in] name prefix for keyword; full keyword is <i>Name</i>Stat,
79 where name is cast to titlecase
80 @param[in] axeLim axeLim block;
81 reads badStatusMask and warnStatusMask
84 - message severity ("i" if status OK, "w" if not)
85 - status in keyword-variable format
87 titleName = name.title()
89 if self.
statusWord & axeLim.badStatusMask != 0:
91 statusElts.append(
"Bad%sStatus" % (titleName,))
92 elif self.
statusWord & axeLim.warnStatusMask != 0:
94 statusElts.append(
"Warn%sStatus" % (titleName,))
97 statusElts.append(
"%sStat=%0.6f, %0.6f, %0.5f, 0x%08x; %sDTime=%0.2f" % \
98 (titleName, self.pvt.pos, self.pvt.vel, self.pvt.t, self.
statusWord, titleName, self.
dTime))
99 return msgCode,
"; ".join(statusElts)
104 You may modify where output is sent by setting writeToUsers to a function that accepts these arguments:
105 msgCode, msgStr, cmd=None, userID=None, cmdID=None (see twistedActor.baseActor.writeToUsers for details)
107 Several callback functions may be defined by setting the following attributes
108 or cleared by setting them None. If specified, each receives one positional argument: this device.
109 initCallFunc: a callback function called after INIT, MOVE (with no arguments) or STOP is started
110 statusCallFunc: a callback function called after STATUS is successfully parsed
111 driftCallFunc: a callback function called after the reply to DRIFT is successfully parsed
114 DefaultInitTimeLim = 10
115 def __init__(self, name, host, port, callFunc=None):
116 """!Construct an AxisDevice
119 @param[in] name name of device
120 @param[in] host host address of Galil controller
121 @param[in] port port of Galil controller
122 @param[in] callFunc function to call when state of device changes;
123 note that it is NOT called when the connection state changes;
124 register a callback with "conn" for that task.
126 TCPDevice.__init__(self,
136 def _initialSetup(self):
146 self.
_moveRE = re.compile(
r"MOVE +([0-9.+-]+) +([0-9.+-]+) +([0-9.+-]+)\b", re.I)
147 self.
_clearRE = re.compile(
r"(DRIFT|MOVE|STOP|INIT)\b", re.I)
149 bufferSize = TrackAdvTimeBufferSize,
150 callEvery = TrackAdvTimeReportEvery,
157 "init" : CommandQueue.Immediate,
169 allCmdsExceptStop = [
"drift",
"move",
"+move",
"ms.on",
"ms.off",
"status"]
173 self.cmdQueue.addRule(
174 action = CommandQueue.CancelQueued,
176 queuedCmds = allCmdsExceptStop,
180 self.cmdQueue.addRule(
181 action = CommandQueue.CancelQueued,
183 queuedCmds = allCmdsExceptStop,
186 def connect(self, userCmd=None, timeLim=DefaultInitTimeLim):
187 """Connecting to the axes
189 Overridden because connecting can be slow, presumably due to the multiplexor
191 return TCPDevice.connect(self, userCmd=userCmd, timeLim=timeLim)
193 def disconnect(self, userCmd=None, timeLim=DefaultInitTimeLim):
194 """Connecting to the axes
196 Overridden because disconnecting can be slow, presumably due to the multiplexor
198 return TCPDevice.disconnect(self, userCmd=userCmd, timeLim=timeLim)
200 def init(self, userCmd=None, timeLim=DefaultInitTimeLim, getStatus=True):
201 """!Initialize the device and cancel all pending commands
203 @param[in] userCmd user command that tracks this command, if any
204 @param[in] timeLim maximum time before command expires, in sec; None for no limit
205 @param[in] getStatus if true then get status after init
206 @return userCmd (a new one if the provided userCmd is None)
208 userCmd = expandUserCmd(userCmd)
209 log.info(
"%s.init(userCmd=%s, timeLim=%s, getStatus=%s)" % (self, userCmd, timeLim, getStatus))
214 devCmd0 = self.
startCmd(
"INIT", timeLim=timeLim)
216 LinkCommands(mainCmd=userCmd, subCmdList=[devCmd0, devCmd1])
218 self.
startCmd(
"INIT", userCmd=userCmd, timeLim=timeLim)
222 """!Check command echo
225 raise RuntimeError(
"Cannot check echo of command=%r: no replies" % \
226 (self.cmdQueue.currExeCmd.cmdStr,))
228 if self.cmdQueue.currExeCmd.cmdStr != self.
replyBuffer[ind]:
229 raise RuntimeError(
"command=%r != echo=%r" % \
230 (self.cmdQueue.currExeCmd.cmdStr, self.
replyBuffer[ind]))
233 """!Check number of reply lines
235 if numLines
is not None and len(self.
replyBuffer) != numLines:
236 raise RuntimeError(
"For command %r expected %d replies but got %s" % \
237 (self.cmdQueue.currExeCmd.cmdStr, numLines, self.
replyBuffer))
240 """!Strip unwanted characters from reply string
242 @param[in] reply: a reply string (from a device)
244 return reply.strip(
' ;\r\n\x00\x01\x02\x03\x18\xfa\xfb\xfc\xfd\xfe\xff')
247 """!Handle a line of output from the device. Called whenever the device outputs a new line of data.
249 @param[in] reply the reply, minus any terminating \n
251 log.info(
'%s read %r; curr cmd=%r %s' % (self, reply, self.cmdQueue.currExeCmd.cmd.cmdStr, self.cmdQueue.currExeCmd.cmd.state))
253 if (
not reply)
or self.cmdQueue.currExeCmd.isDone:
263 elif reply.endswith(
" OK"):
267 self.replyBuffer.append(reply)
268 log.info(
'%s replyBuffer=%r; curr cmd=%r %s'%(self, self.
replyBuffer, self.cmdQueue.currExeCmd.cmd.cmdStr, self.cmdQueue.currExeCmd.cmd.state))
269 if self.cmdQueue.currExeCmd.cmd.showReplies:
270 msgStr =
"%sReply=%s" % (self.name, quoteStr(reply),)
271 self.writeToUsers(msgCode=
'i', msgStr=msgStr)
279 """!Parse the contents of the reply buffer
281 log.info(
"%s.parseReplyBuffer replyBuffer=%r cmdVerb=%r" % (self, self.
replyBuffer, self.cmdQueue.currExeCmd.cmdVerb))
282 descrStr =
"%s.parseReplyBuffer" % (self,)
286 if self.cmdQueue.currExeCmd.isDone:
287 log.warn(
"parseReplyBuffer called with replies=%r for done command=%r" % \
291 cmdVerb = self.cmdQueue.currExeCmd.cmdVerb
293 if cmdVerb ==
"init":
310 if "status" in cmdVerb:
316 elif cmdVerb ==
"drift":
320 pos, vel, secInDay = [float(strVal)
for strVal
in driftReply.split()]
321 except Exception
as e:
322 raise RuntimeError(
"Could not parse %r as pos vel secInDay" % (driftReply,))
324 self.status.pvt = PVT(pos, vel, taiDate)
328 elif cmdVerb
in (
"move",
"+move",
"ms.on",
"ms.off",
"stop"):
329 if cmdVerb ==
"stop" or self.cmdQueue.currExeCmd.cmdStr.lower() ==
"move":
335 self.cmdQueue.currExeCmd.setState(self.cmdQueue.currExeCmd.Done)
336 except Exception
as e:
337 log.error(
"%s %s"%(self, strFromException(e)))
338 if not self.cmdQueue.currExeCmd.isDone:
339 self.cmdQueue.currExeCmd.setState(self.cmdQueue.currExeCmd.Failed, strFromException(e))
341 def startCmd(self, cmdStr, callFunc=None, userCmd=None, timeLim=DefaultTimeLim, showReplies=False,
343 """!Queue a new command
345 @param[in] cmdStr command string
346 @param[in] callFunc callback function: function to call when command succeeds or fails, or None;
347 if specified it receives one argument: a device command
348 @param[in] userCmd user command that should track this command, if any
349 @param[in] timeLim time limit, in seconds
350 @param[in] showReplies show all replies as plain text?
351 @param[in] devCmdVerb if specified, use to set device command verb,
352 which in turn sets the priority of the command
354 @return devCmd: the device command that was started (and may already have failed)
356 log.info(
"%s.startCmd(cmdStr=%s, userCmd=%s, timeLim=%s)" % (self, cmdStr, userCmd, timeLim))
357 devCmd = self.cmdClass(
363 showReplies = showReplies,
366 devCmd.cmdVerb = devCmdVerb
368 devCmd.cmdVerb = cmdStr.partition(
" ")[0].lower()
369 self.cmdQueue.addCmd(devCmd, self.
sendCmd)
370 cmdQueueFmt =
"[%s]"%(
", ".join([
"%s(%s)"%(cmd.cmdStr, cmd.cmd.state)
for cmd
in self.
cmdQueue]))
371 log.info(
"%s.startCmd(cmdQueue=%s)"%(self, cmdQueueFmt))
375 """!Execute the command
377 @param[in] devCmd a device command
381 if not self.conn.isConnected:
382 log.error(
"%s cannot write %r: not connected" % (self, devCmd.cmdStr))
383 devCmd.setState(devCmd.Failed,
"not connected")
387 if "init" != devCmd.cmdVerb.lower():
393 log.info(
"%s writing %r" % (self, line))
394 self.conn.writeLine(line)
396 def _checkRunStats(self, cmdStr):
397 """!Determine if cmdStr is a MOVE PVT command. If so parse it, determine advance
398 time, and update self.runningStats
400 Note that advance time = time of previous MOVE P V T - tai(),
401 and that it is reset based on DRIFT, MOVE (with no arguments), STOP or INIT
403 moveMatch = self._moveRE.match(cmdStr)
405 moveSecInDay = float(moveMatch.group(3))
412 advTime = moveTime -
tai()
413 self.runningStats.addValue(advTime)
416 clearMatch = self._clearRE.match(cmdStr)
420 def _connCallback(self, conn=None):
421 """!Call when the connection state changes
423 Kill all executing and queued commands, preferably without running the command queue
425 TCPDevice._connCallback(self, conn=conn)
426 if self.conn.isDisconnected:
427 self.cmdQueue.killAll()
429 def _devCmdFailInit(self, devCmd):
430 """!Call init if device command fails
432 @warning only add this callback when the device command is ready to run
434 if devCmd.state==devCmd.Failed:
440 msgStr=
"%s._devCmdFailInit(devCmd=%s). DevCmd failed, sending init"%(self,devCmd)
442 self.
init(getStatus=
False)
444 def _printTrackAdvTime(self, runStatObj):
445 """!Callback for self.runningStats; print info
447 stats = runStatObj.getStats()
448 msgStr =
"%sTrackAdvTime=%i, %.2f, %.2f, %.2f, %.2f" % (
449 self.name.title(), stats[
"num"], stats[
"min"], stats[
"max"], stats[
"median"], stats[
"stdDev"]
451 self.writeToUsers(
"i", msgStr=msgStr)
def handleReply
Handle a line of output from the device.
def __init__
Construct an AxisDevice.
def startCmd
Queue a new command.
def _printTrackAdvTime
Callback for self.runningStats; print info.
def checkCmdEcho
Check command echo.
def setFromReply
Set values from status reply.
def parseReplyBuffer
Parse the contents of the reply buffer.
def _devCmdFailInit
Call init if device command fails.
def wordOrDTimeChanged
Return True if the status word has changed or dTime has changed significantly.
def _checkRunStats
Determine if cmdStr is a MOVE PVT command.
def init
Initialize the device and cancel all pending commands.
def copy
Return a copy of this object.
def checkNumReplyLines
Check number of reply lines.
def formatStatus
Format status message.
def __init__
Construct an AxisStatus.
def prepReplyStr
Strip unwanted characters from reply string.
def sendCmd
Execute the command.
def dateFromSecInDay
Convert TAI seconds in the day to a full TAI date (MJD, seconds)