Документ взят из кэша поисковой машины. Адрес оригинального документа : http://www.apo.nmsu.edu/Telescopes/TCC/html/axis_device_8py_source.html
Дата изменения: Tue Sep 15 02:25:37 2015
Дата индексирования: Sun Apr 10 02:08:29 2016
Кодировка:
lsst.tcc: python/tcc/axis/axisDevice.py Source File
lsst.tcc  1.2.2-3-g89ecb63
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Pages
axisDevice.py
Go to the documentation of this file.
1 from __future__ import division, absolute_import
2 import re
3 import copy
4 
5 import numpy
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
10 
11 from tcc.base import tai
12 from tcc.util import dateFromSecInDay, RunningStats
13 
14 __all__ = ["AxisDevice"]
15 
16 InitReplyDelay = 0.5 # time to wait after seeing OK to be sure it is for INIT
17  # and not some other command that INIT superseded
18 
19 TrackAdvTimeBufferSize = 20 # number of entries for <axis>TrackAdvTime rolling average
20 TrackAdvTimeReportEvery = 10 # output timing <axis>TrackAdvTime every this many MOVE P V T commands
21 
22 class AxisStatus(object):
23  """!Axis controller status
24  """
25  def __init__(self):
26  """!Construct an AxisStatus
27  """
28  self.pvt = PVT()
29  self.indexPos = numpy.nan
30  self.statusWord = 0
31  self.dTime = numpy.nan
32  self.taiDate = 0 # date as reported by axis controller (expanded to full date)
33 
34  def copy(self):
35  """!Return a copy of this object
36  """
37  return copy.copy(self)
38 
39  def setFromReply(self, reply):
40  """!Set values from status reply
41 
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
45  """
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,))
50  try:
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])
56  except ValueError:
57  raise RuntimeError("Could not parse %s as status" % (" ".join(dataList),))
58  taiDate = dateFromSecInDay(secInDay)
59  self.pvt = PVT(pos, vel, taiDate)
60  self.statusWord = statusWord
61  self.indexPosition = indexPosition
62  self.dTime = tai() - taiDate
63  self.taiDate = taiDate
64 
65  def wordOrDTimeChanged(self, rhs):
66  """!Return True if the status word has changed or dTime has changed significantly
67 
68  @param[in] rhs another AxisStatus, against which to compare
69 
70  Use to determine if new status should be reported. Thus "significantly" for dTime
71  means that the formatted value will look different.
72  """
73  return (self.statusWord != rhs.statusWord) or (abs(self.dTime-rhs.dTime)>0.1)
74 
75  def formatStatus(self, name, axeLim):
76  """!Format status message
77 
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
82 
83  @return two items:
84  - message severity ("i" if status OK, "w" if not)
85  - status in keyword-variable format
86  """
87  titleName = name.title()
88  statusElts = []
89  if self.statusWord & axeLim.badStatusMask != 0:
90  msgCode = "w"
91  statusElts.append("Bad%sStatus" % (titleName,))
92  elif self.statusWord & axeLim.warnStatusMask != 0:
93  msgCode = "w"
94  statusElts.append("Warn%sStatus" % (titleName,))
95  else:
96  msgCode = "i"
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)
100 
101 class AxisDevice(TCPDevice):
102  """!An Axis Device
103 
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)
106 
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
112  """
113  DefaultTimeLim = 5 # default time limit, in seconds
114  DefaultInitTimeLim = 10 # default time limit for init, connection and disconnect
115  def __init__(self, name, host, port, callFunc=None):
116  """!Construct an AxisDevice
117 
118  Inputs:
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.
125  """
126  TCPDevice.__init__(self,
127  name = name,
128  host = host,
129  port = port,
130  callFunc = callFunc,
131  cmdInfo = (),
132  )
133  self._initialSetup()
134 
135 
136  def _initialSetup(self):
137  # break out initializatin here
138  # so subclasses can use
139  self.initCallFunc = None # called after MOVE or INIT are started
140  self.statusCallFunc = None # called after STATUS is successfully parsed
141  self.driftCallFunc = None # called after DRIFT reply is successfully parsed
142  self.replyBuffer = []
144 
145  self._lastMoveTime = 0 # time of last MOVE P V T (TAI, MJD sec), or 0 after a DRIFT, MOVE (with no arguments), STOP or INIT
146  self._moveRE = re.compile(r"MOVE +([0-9.+-]+) +([0-9.+-]+) +([0-9.+-]+)\b", re.I) # for parsing move command echo
147  self._clearRE = re.compile(r"(DRIFT|MOVE|STOP|INIT)\b", re.I)
148  self.runningStats = RunningStats(
149  bufferSize = TrackAdvTimeBufferSize,
150  callEvery = TrackAdvTimeReportEvery,
151  callFunc = self._printTrackAdvTime,
152  )
153 
154  self.StatusInitVerb = "statusinit"
155  self.cmdQueue = CommandQueue(
156  priorityDict = {
157  "init" : CommandQueue.Immediate,
158  self.StatusInitVerb: 4,
159  "set.time": 4,
160  "stop": 3,
161  "drift" : 2,
162  "move" : 2,
163  "+move" : 2,
164  "ms.on" : 2,
165  "ms.off" : 2,
166  "status" : 1,
167  }
168  )
169  allCmdsExceptStop = ["drift", "move", "+move", "ms.on", "ms.off", "status"]
170 
171  # a stop command will clear all other queued commands except "stop", "init", "statusinit", and "set.time"
172  # "init", "set.time", and "statusinit" are all part of the axis initialization procedure.
173  self.cmdQueue.addRule(
174  action = CommandQueue.CancelQueued,
175  newCmds = ["stop"],
176  queuedCmds = allCmdsExceptStop,
177  )
178 
179  # a drift command will clear all other queued commands except "stop" and "init"
180  self.cmdQueue.addRule(
181  action = CommandQueue.CancelQueued,
182  newCmds = ["drift"],
183  queuedCmds = allCmdsExceptStop,
184  )
185 
186  def connect(self, userCmd=None, timeLim=DefaultInitTimeLim):
187  """Connecting to the axes
188 
189  Overridden because connecting can be slow, presumably due to the multiplexor
190  """
191  return TCPDevice.connect(self, userCmd=userCmd, timeLim=timeLim)
192 
193  def disconnect(self, userCmd=None, timeLim=DefaultInitTimeLim):
194  """Connecting to the axes
195 
196  Overridden because disconnecting can be slow, presumably due to the multiplexor
197  """
198  return TCPDevice.disconnect(self, userCmd=userCmd, timeLim=timeLim)
199 
200  def init(self, userCmd=None, timeLim=DefaultInitTimeLim, getStatus=True):
201  """!Initialize the device and cancel all pending commands
202 
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)
207  """
208  userCmd = expandUserCmd(userCmd)
209  log.info("%s.init(userCmd=%s, timeLim=%s, getStatus=%s)" % (self, userCmd, timeLim, getStatus))
210  if getStatus:
211  # put a status on the queue after the init. Use StatusInitVerb
212  # to run the status at high priority, so it cannot be interrupted by a MOVE
213  # of similar command, which would make the returned userCmd fail
214  devCmd0 = self.startCmd("INIT", timeLim=timeLim)
215  devCmd1 = self.startCmd("STATUS", timeLim=timeLim, devCmdVerb=self.StatusInitVerb)
216  LinkCommands(mainCmd=userCmd, subCmdList=[devCmd0, devCmd1])
217  else:
218  self.startCmd("INIT", userCmd=userCmd, timeLim=timeLim)
219  return userCmd
220 
221  def checkCmdEcho(self, ind=0):
222  """!Check command echo
223  """
224  if len(self.replyBuffer) == 0:
225  raise RuntimeError("Cannot check echo of command=%r: no replies" % \
226  (self.cmdQueue.currExeCmd.cmdStr,))
227 
228  if self.cmdQueue.currExeCmd.cmdStr != self.replyBuffer[ind]:
229  raise RuntimeError("command=%r != echo=%r" % \
230  (self.cmdQueue.currExeCmd.cmdStr, self.replyBuffer[ind]))
231 
232  def checkNumReplyLines(self, numLines):
233  """!Check number of reply lines
234  """
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))
238 
239  def prepReplyStr(self, reply):
240  """!Strip unwanted characters from reply string
241 
242  @param[in] reply: a reply string (from a device)
243  """
244  return reply.strip(' ;\r\n\x00\x01\x02\x03\x18\xfa\xfb\xfc\xfd\xfe\xff')
245 
246  def handleReply(self, reply):
247  """!Handle a line of output from the device. Called whenever the device outputs a new line of data.
248 
249  @param[in] reply the reply, minus any terminating \n
250  """
251  log.info('%s read %r; curr cmd=%r %s' % (self, reply, self.cmdQueue.currExeCmd.cmd.cmdStr, self.cmdQueue.currExeCmd.cmd.state))
252  reply = self.prepReplyStr(reply)
253  if (not reply) or self.cmdQueue.currExeCmd.isDone:
254  # ignore blank lines and unsolicited input
255  return
256 
257  self._checkRunStats(reply)
258  # is there a terminating OK on the line?
259  gotOK = False
260  if reply == "OK":
261  reply = ""
262  gotOK = True
263  elif reply.endswith(" OK"):
264  gotOK = True
265  reply = reply[:-3]
266  if reply:
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)
272  if gotOK:
273  # all expected output has been received
274  # now parse command echo, and any additional info
275  self.parseReplyBuffer()
276 
277 
278  def parseReplyBuffer(self):
279  """!Parse the contents of the reply buffer
280  """
281  log.info("%s.parseReplyBuffer replyBuffer=%r cmdVerb=%r" % (self, self.replyBuffer, self.cmdQueue.currExeCmd.cmdVerb))
282  descrStr = "%s.parseReplyBuffer" % (self,)
283  try:
284  # use this inner try/finally block to make sure the reply buffer is emptied
285  try:
286  if self.cmdQueue.currExeCmd.isDone:
287  log.warn("parseReplyBuffer called with replies=%r for done command=%r" % \
288  (self.replyBuffer, self.cmdQueue.currExeCmd))
289  return
290 
291  cmdVerb = self.cmdQueue.currExeCmd.cmdVerb
292 
293  if cmdVerb == "init":
294  try:
295  # the reply buffer may have replies from other commands that were executing
296  # when the INIT was sent; only the last reply is relevant
297  self.checkCmdEcho(-1)
298  except RuntimeError:
299  # last reply in buffer should be init!, if not
300  # do nothing, wait for another reply
301  # this echo could have been associated with a
302  # previously running command
303  return
304  else:
305  if self.initCallFunc:
306  safeCall2(descrStr, self.initCallFunc, self)
307  else:
308  self.checkCmdEcho()
309 
310  if "status" in cmdVerb: #statusinit is it's own verb
311  self.checkNumReplyLines(2)
312  self.status.setFromReply(self.replyBuffer[1])
313  if self.statusCallFunc:
314  safeCall2(descrStr, self.statusCallFunc, self)
315 
316  elif cmdVerb == "drift":
317  self.checkNumReplyLines(2)
318  driftReply = self.replyBuffer[1]
319  try:
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,))
323  taiDate = dateFromSecInDay(secInDay)
324  self.status.pvt = PVT(pos, vel, taiDate)
325  if self.driftCallFunc:
326  safeCall2(descrStr, self.driftCallFunc, self)
327 
328  elif cmdVerb in ("move", "+move", "ms.on", "ms.off", "stop"):
329  if cmdVerb == "stop" or self.cmdQueue.currExeCmd.cmdStr.lower() == "move":
330  if self.initCallFunc:
331  safeCall2(descrStr, self.initCallFunc, self)
332  self.checkNumReplyLines(1)
333  finally:
334  self.replyBuffer = []
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))
340 
341  def startCmd(self, cmdStr, callFunc=None, userCmd=None, timeLim=DefaultTimeLim, showReplies=False,
342  devCmdVerb=None):
343  """!Queue a new command
344 
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
353 
354  @return devCmd: the device command that was started (and may already have failed)
355  """
356  log.info("%s.startCmd(cmdStr=%s, userCmd=%s, timeLim=%s)" % (self, cmdStr, userCmd, timeLim))
357  devCmd = self.cmdClass(
358  cmdStr = cmdStr,
359  callFunc = callFunc,
360  userCmd = userCmd,
361  timeLim = timeLim,
362  dev = self,
363  showReplies = showReplies,
364  )
365  if devCmdVerb:
366  devCmd.cmdVerb = devCmdVerb
367  else:
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))
372  return devCmd
373 
374  def sendCmd(self, devCmd):
375  """!Execute the command
376 
377  @param[in] devCmd a device command
378  """
379  # clear the buffer
380  self.replyBuffer = []
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")
384  return
385 
386  # add failure callback after checking connection, because INIT is useless if not connected
387  if "init" != devCmd.cmdVerb.lower():
388  # only non-init commands get an automatic init-on-failure
389  devCmd.addCallback(self._devCmdFailInit)
390  self.writeToDev(devCmd.cmdStr)
391 
392  def writeToDev(self, line):
393  log.info("%s writing %r" % (self, line))
394  self.conn.writeLine(line)
395 
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
399 
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
402  """
403  moveMatch = self._moveRE.match(cmdStr)
404  if moveMatch:
405  moveSecInDay = float(moveMatch.group(3))
406  moveTime = dateFromSecInDay(moveSecInDay)
407  if self._lastMoveTime > 0:
408  # measure how early P V Ts of a path are, relative to last P V T sent
409  advTime = self._lastMoveTime - tai()
410  else:
411  # measure how early the first P V T of a new path is, relative to "now"
412  advTime = moveTime - tai()
413  self.runningStats.addValue(advTime)
414  self._lastMoveTime = moveTime
415  else:
416  clearMatch = self._clearRE.match(cmdStr)
417  if clearMatch:
418  self._lastMoveTime = 0
419 
420  def _connCallback(self, conn=None):
421  """!Call when the connection state changes
422 
423  Kill all executing and queued commands, preferably without running the command queue
424  """
425  TCPDevice._connCallback(self, conn=conn)
426  if self.conn.isDisconnected:
427  self.cmdQueue.killAll()
428 
429  def _devCmdFailInit(self, devCmd):
430  """!Call init if device command fails
431 
432  @warning only add this callback when the device command is ready to run
433  """
434  if devCmd.state==devCmd.Failed:
435  # note if devCmd.state==Cancelled, this code doesn't run
436  # only init may set device command to cancelled
437  # so this is the desired behavior.
438  # only auto-init if the devCmd failed, and was not explicitly canceled
439  # by an init
440  msgStr="%s._devCmdFailInit(devCmd=%s). DevCmd failed, sending init"%(self,devCmd)
441  log.warn(msgStr)
442  self.init(getStatus=False)
443 
444  def _printTrackAdvTime(self, runStatObj):
445  """!Callback for self.runningStats; print info
446  """
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"]
450  )
451  self.writeToUsers("i", msgStr=msgStr)
def handleReply
Handle a line of output from the device.
Definition: axisDevice.py:246
def __init__
Construct an AxisDevice.
Definition: axisDevice.py:115
def startCmd
Queue a new command.
Definition: axisDevice.py:342
def _printTrackAdvTime
Callback for self.runningStats; print info.
Definition: axisDevice.py:444
def checkCmdEcho
Check command echo.
Definition: axisDevice.py:221
def setFromReply
Set values from status reply.
Definition: axisDevice.py:39
def parseReplyBuffer
Parse the contents of the reply buffer.
Definition: axisDevice.py:278
def _devCmdFailInit
Call init if device command fails.
Definition: axisDevice.py:429
def wordOrDTimeChanged
Return True if the status word has changed or dTime has changed significantly.
Definition: axisDevice.py:65
def _checkRunStats
Determine if cmdStr is a MOVE PVT command.
Definition: axisDevice.py:396
def init
Initialize the device and cancel all pending commands.
Definition: axisDevice.py:200
def copy
Return a copy of this object.
Definition: axisDevice.py:34
def checkNumReplyLines
Check number of reply lines.
Definition: axisDevice.py:232
def formatStatus
Format status message.
Definition: axisDevice.py:75
def __init__
Construct an AxisStatus.
Definition: axisDevice.py:25
Axis controller status.
Definition: axisDevice.py:22
def prepReplyStr
Strip unwanted characters from reply string.
Definition: axisDevice.py:239
def sendCmd
Execute the command.
Definition: axisDevice.py:374
def dateFromSecInDay
Convert TAI seconds in the day to a full TAI date (MJD, seconds)
Definition: time.py:27
double tai()
Definition: tai.cc:7