Laelaps  2.3.5
RoadNarrows Robotics Small Outdoor Mobile Robot Project
laelaps_diag.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 
3 ###############################################################################
4 #
5 # Package: RoadNarrows Robotics Laelaps Robotic Mobile Platform Package
6 #
7 # File: laelaps_diag.py
8 #
9 ## \file
10 ##
11 ## $LastChangedDate: 2016-02-22 18:11:12 -0700 (Mon, 22 Feb 2016) $
12 ## $Rev: 4328 $
13 ##
14 ## \brief Perform Laelaps diagnostics (python version).
15 ##
16 ## \note The sensors and watchdog interface is via I2C. A simple I2C python
17 ## module needs to be found or written.
18 ##
19 ## \author Robin Knight (robin.knight@roadnarrows.com)
20 ##
21 ## \par Copyright
22 ## \h_copy 2015-2017. RoadNarrows LLC.\n
23 ## http://www.roadnarrows.com\n
24 ## All Rights Reserved
25 ##
26 # @EulaBegin@
27 #
28 # Unless otherwise stated explicitly, all materials contained are copyrighted
29 # and may not be used without RoadNarrows LLC's written consent,
30 # except as provided in these terms and conditions or in the copyright
31 # notice (documents and software) or other proprietary notice provided with
32 # the relevant materials.
33 #
34 # IN NO EVENT SHALL THE AUTHOR, ROADNARROWS LLC, OR ANY
35 # MEMBERS/EMPLOYEES/CONTRACTORS OF ROADNARROWS OR DISTRIBUTORS OF THIS SOFTWARE
36 # BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR
37 # CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
38 # DOCUMENTATION, EVEN IF THE AUTHORS OR ANY OF THE ABOVE PARTIES HAVE BEEN
39 # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40 #
41 # THE AUTHORS AND ROADNARROWS LLC SPECIFICALLY DISCLAIM ANY WARRANTIES,
42 # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
43 # FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN
44 # "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO
45 # PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
46 #
47 # @EulaEnd@
48 #
49 ###############################################################################
50 
51 import sys
52 import os
53 import time
54 import math
55 import random
56 import struct
57 import getopt
58 
59 from serial.serialutil import SerialException, SerialTimeoutException
60 
61 import Laelaps.SysConf as SysConf
62 import Laelaps.RoboClaw as RoboClaw
63 
64 Diagnostics = ['motors', 'tofs', 'imu', 'watchdog']
65 DiagSep = "\
66 -------------------------------------------------------------------------------"
67 
68 # ------------------------------------------------------------------------------
69 # Fake Motor Controller Class
70 # ------------------------------------------------------------------------------
71 
72 class fakemc():
73  def __init__(self):
74  pass
75 
76  def open(self, dev, baud, fnChipSelect=None):
77  self.dev = dev
78  self.baud = baud
79  self.m1_speed = {}
80  self.m2_speed = {}
81  self.m1_encoder = {}
82  self.m2_encoder = {}
83 
84  def close(self):
85  pass
86 
87  def readVersion(self, addr):
88  return "1.5.6 firmwareXYZ"
89 
90  def readMainBatterySettings(self, addr):
91  return (60.1, 324.0)
92 
93  def readLogicBatterySettings(self, addr):
94  raise SerialTimeoutException('bogus')
95 
96  def readEncoderMode(self, addr):
97  return (0, 0)
98 
99  def readM1Pidq(self, addr):
100  return (0x00010000, 0x00008000, 0x00004000, 10000)
101 
102  def readM2Pidq(self, addr):
103  return (0x00010000, 0x00008000, 0x00004000, 10000)
104 
105  def readErrorState(self, addr):
106  return 0
107 
108  def readTemperature(self, addr):
109  return 288
110 
111  def readMainBattery(self, addr):
112  return 111
113 
114  def readLogicBattery(self, addr):
115  return 49
116 
117  def readCurrents(self, addr):
118  return (75.0, 49.0)
119 
120  def readM1Speed(self, addr):
121  if not self.m1_speed.has_key(addr):
122  self.m1_speed[addr] = 0
123  return (self.m1_speed[addr], 0x00)
124 
125  def readM2Speed(self, addr):
126  if not self.m2_speed.has_key(addr):
127  self.m2_speed[addr] = 0
128  return (self.m2_speed[addr], 0x00)
129 
130  def setM1Speed(self, addr, speed):
131  self.m1_speed[addr] = speed
132 
133  def setM2Speed(self, addr, speed):
134  self.m2_speed[addr] = speed
135 
136  def readM1Encoder(self, addr):
137  if not self.m1_encoder.has_key(addr):
138  self.m1_encoder[addr] = random.randint(0, 1000000)
139  return (self.m1_encoder[addr], 0)
140 
141  def readM2Encoder(self, addr):
142  if not self.m2_encoder.has_key(addr):
143  self.m2_encoder[addr] = random.randint(0, 1000000)
144  return (self.m2_encoder[addr], 0x02)
145 
146  def resetEncoderCnts(self, addr):
147  self.m1_encoder[addr] = 0
148  self.m2_encoder[addr] = 0
149 
150 
151 # ------------------------------------------------------------------------------
152 # Motors Diagnostics Class
153 # ------------------------------------------------------------------------------
154 
155 class DiagMotors():
156  def __init__(self, kwargs):
157  self.kwargs = kwargs
158 
159  self.ctlrKeys = ['front', 'rear']
160  self.ctlrInfo = {
161  'front': {'addr': SysConf.MotorCtlrAddrFront, 'name': 'front'},
162  'rear': {'addr': SysConf.MotorCtlrAddrRear, 'name': 'rear'}
163  }
164 
165  self.motorKeys = ['left_front', 'right_front', 'left_rear', 'right_rear']
166  self.motorInfo = {
167  'left_front':
168  {'ctlr_key': 'front', 'motor_index': 0, 'name': 'left_front'},
169  'right_front':
170  {'ctlr_key': 'front', 'motor_index': 1, 'name': 'right_front'},
171  'left_rear':
172  {'ctlr_key': 'rear', 'motor_index': 0, 'name': 'left_rear'},
173  'right_rear':
174  {'ctlr_key': 'rear', 'motor_index': 1, 'name': 'right_rear'},
175  }
176 
177  def run(self):
178  print
179  print DiagSep
180  print "*** Laelaps Motors Diagnostics ***"
181  print
182 
183  testCnt = 0
184  passCnt = 0
185 
186  # Create motor controller object
187  if self.kwargs['fake']:
188  motorctlr = fakemc()
189  else:
190  motorctlr = RoboClaw.RoboClaw()
191 
192  # Open communiction on serial bus to Laelaps multi-dropped motor controllers
193  device = SysConf.MotorCtlrDevName
194  baud = SysConf.MotorCtlrBaudRate
195  cs = SysConf.MotorCtlrChipSelectGpio
196  testCnt += 1
197  print "Test: Open motor controller serial interface %s@%d" % (device, baud)
198  try:
199  motorctlr.open(device, baud, cs)
200  passCnt += 1
201  except RoboClaw.RoboClawException as inst:
202  print 'Error:', inst.message
203  print
204  return 0
205  print " ... %d/%d tests passed." % (passCnt, testCnt)
206  print
207 
208  for k in self.ctlrKeys:
209  m, n = self.testMotorCtlrState(motorctlr, self.ctlrInfo[k]['addr'],
210  self.ctlrInfo[k]['name'])
211  passCnt += m
212  testCnt += n
213 
214  for k in self.motorKeys:
215  m, n = self.testMotorState(motorctlr,
216  self.ctlrInfo[self.motorInfo[k]['ctlr_key']]['addr'],
217  self.motorInfo[k]['motor_index'],
218  self.motorInfo[k]['name'])
219  passCnt += m
220  testCnt += n
221 
222 
223  m, n = self.testMotors(motorctlr)
224  passCnt += m
225  testCnt += n
226 
227  motorctlr.close()
228 
229  print "Summary:"
230  print " ... %d/%d motors diagnostic tests passed." % (passCnt, testCnt)
231 
232  if passCnt == testCnt:
233  return 1
234  else:
235  return 0
236 
237  def testMotorCtlrState(self, motorctlr, addr, name):
238  print "Test: Read %s motor controller state (address=0x%02x)." % \
239  (name, addr)
240 
241  testCnt = 0
242  passCnt = 0
243 
244  tests = [
245  [" Version: ",
246  self.serReadHelper, (motorctlr.readVersion, addr),
247  self.good,
248  lambda v: v[:-3] ],
249  [" Main Battery Range: ",
250  self.serReadHelper, (motorctlr.readMainBatterySettings, addr),
251  self.good,
252  lambda v: "%.1fV, %.1fV" % (v[0]/10.0, v[1]/10.0) ],
253  [" Logic Battery Range: ",
254  self.serReadHelper, (motorctlr.readLogicBatterySettings, addr),
255  self.good,
256  lambda v: "%.1fV, %.1fV" % (v[0]/10.0, v[1]/10.0) ],
257  [" Error State: ",
258  self.serReadHelper, (motorctlr.readErrorState, addr),
259  self.good,
260  lambda v: "0x%02x" % (v) ],
261  [" Temperature: ",
262  self.serReadHelper, (motorctlr.readTemperature, addr),
263  self.good,
264  lambda v: "%.1fC" % (v/10.0) ],
265  [" Main Battery: ",
266  self.serReadHelper, (motorctlr.readMainBattery, addr),
267  self.good,
268  lambda v: "%.1fV" % (v/10.0) ],
269  [" Logic Battery: ",
270  self.serReadHelper, (motorctlr.readLogicBattery, addr),
271  self.good,
272  lambda v: "%.1fV" % (v/10.0) ],
273  ]
274 
275  # run tests
276  for t in tests:
277  testCnt += 1
278  if self.runTest(t[0], t[1], t[2], t[3], t[4]):
279  passCnt +=1
280 
281  print " ... %d/%d tests passed." % (passCnt, testCnt)
282  print
283 
284  return (passCnt, testCnt)
285 
286  def testMotorState(self, motorctlr, addr, motorIndex, name):
287  print "Test: Read %s motor state." % (name)
288 
289  testCnt = 0
290  passCnt = 0
291 
292  if motorIndex == 0:
293  readPidq = motorctlr.readM1Pidq
294  readEncoder = motorctlr.readM1Encoder
295  readSpeed = motorctlr.readM1Speed
296  else:
297  readPidq = motorctlr.readM2Pidq
298  readEncoder = motorctlr.readM2Encoder
299  readSpeed = motorctlr.readM2Speed
300 
301  tests = [
302  [" Encoder Mode: ",
303  self.serReadHelper, (motorctlr.readEncoderMode, addr),
304  lambda v: self.chkEncoderMode(motorIndex, v),
305  lambda v: self.fmtEncoderMode(motorIndex, v) ],
306  [" Vel. PID: ",
307  self.serReadHelper, (readPidq, addr),
308  self.good,
309  lambda v: self.fmtPidq(motorIndex, v) ],
310  [" Encoder: ",
311  self.serReadHelper, (readEncoder, addr),
312  self.good,
313  lambda v: "%u, 0x%02x" % (v[0], v[1]) ],
314  [" Speed: ",
315  self.serReadHelper, (readSpeed, addr),
316  self.good,
317  lambda v: "%u, 0x%02x" % (v[0], v[1]) ],
318  [" Current: ",
319  self.serReadHelper, (motorctlr.readCurrents, addr),
320  self.good,
321  lambda v: "%.3fA" % (v[motorIndex]/100.0) ],
322  ]
323 
324  # run tests
325  for t in tests:
326  testCnt += 1
327  if self.runTest(t[0], t[1], t[2], t[3], t[4]):
328  passCnt +=1
329 
330  print " ... %d/%d tests passed." % (passCnt, testCnt)
331  print
332 
333  return (passCnt, testCnt)
334 
335  def testMotors(self, motorctlr):
336  print "Test: Drive motors."
337 
338  testCnt = 0
339  passCnt = 0
340 
341  addrFront = self.ctlrInfo['front']['addr']
342  addrRear = self.ctlrInfo['rear']['addr']
343 
344  speedProf = [500, 1000, 2500, 5000, 2500, 1000, 500,
345  -500, -1000, -2500, -5000, -2500, -1000, -500,
346  0]
347 
348  testCnt += 1
349  if self.runTest(" All stop: ",
350  self.stopHelper, (motorctlr,),
351  self.good,
352  lambda v: "[PASS]"):
353  passCnt += 1
354 
355  testCnt += 1
356  if self.runTest(" Reset front encoders: ",
357  self.serWriteHelper, (motorctlr.resetEncoderCnts, addrFront),
358  self.good,
359  lambda v: "[PASS]"):
360  passCnt += 1
361 
362  testCnt += 1
363  if self.runTest(" Reset rear encoders: ",
364  self.serWriteHelper, (motorctlr.resetEncoderCnts, addrRear),
365  self.good,
366  lambda v: "[PASS]"):
367  passCnt += 1
368 
369  print " Loop through speed profile:"
370 
371  for speed in speedProf:
372  for k in self.motorKeys:
373  testCnt += 1
374  if self.runTest(" Set %*s motor speed = %-6d: " % (11, k, speed),
375  self.setSpeedHelper, (motorctlr, k, speed),
376  self.good,
377  lambda v: "[PASS] (speed = %d)" % (v)):
378  passCnt += 1
379  print
380  time.sleep(1.0)
381 
382  print " ... %d/%d tests passed." % (passCnt, testCnt)
383  print
384 
385  return (passCnt, testCnt)
386 
387  def runTest(self, what, fnExec, argsExec, fnChk, fnFmt):
388  v, pf, e = fnExec(*argsExec)
389  if pf:
390  pf, e = fnChk(v)
391  if pf:
392  s = fnFmt(v)
393  elif e is not None:
394  s = "[FAIL] (%s)" % (e)
395  else:
396  s = '[FAIL]'
397  print "%s%s" % (what, s)
398  return pf
399 
400  def serReadHelper(self, fnRead, *argsRead):
401  try:
402  v = fnRead(*argsRead)
403  pf = True
404  e = None
405  except RoboClaw.RoboClawException as inst:
406  v = None
407  pf = False
408  e = inst.message
409  return (v, pf, e)
410 
411  def serWriteHelper(self, fnWrite, *argsWrite):
412  try:
413  fnWrite(*argsWrite)
414  pf = True
415  e = None
416  except RoboClaw.RoboClawException as inst:
417  pf = False
418  e = inst.message
419  return (None, pf, e)
420 
421  def stopHelper(self, motorctlr):
422  try:
423  motorctlr.setM1Speed(self.ctlrInfo['front']['addr'], 0)
424  motorctlr.setM2Speed(self.ctlrInfo['front']['addr'], 0)
425  motorctlr.setM1Speed(self.ctlrInfo['rear']['addr'], 0)
426  motorctlr.setM2Speed(self.ctlrInfo['rear']['addr'], 0)
427  v = (0, 0, 0, 0)
428  pf = True
429  e = None
430  except RoboClaw.RoboClawException as inst:
431  v = None
432  pf = False
433  e = inst.message
434  return (v, pf, e)
435 
436  def setSpeedHelper(self, motorctlr, motorkey, speed):
437  addr = self.ctlrInfo[self.motorInfo[motorkey]['ctlr_key']]['addr']
438  mi = self.motorInfo[motorkey]['motor_index']
439  if mi == 0:
440  setSpeed = motorctlr.setM1Speed
441  readSpeed = motorctlr.readM1Speed
442  else:
443  setSpeed = motorctlr.setM2Speed
444  readSpeed = motorctlr.readM2Speed
445  try:
446  setSpeed(addr, speed)
447  except RoboClaw.RoboClawException as inst:
448  return (None, False, inst.message)
449  curspeed = 0
450  for i in range(0, 10):
451  try:
452  curspeed, status = readSpeed(addr)
453  if speed == 0:
454  if abs(curspeed) < 10:
455  return (curspeed, True, None)
456  else:
457  if math.fabs(speed-curspeed)/math.fabs(speed) <= 0.05:
458  return (curspeed, True, None)
459  except RoboClaw.RoboClawException as inst:
460  return (None, False, inst.message)
461  time.sleep(0.1)
462  return (curspeed, False, "timeout reaching speed, curspeed=%d" % (curspeed))
463 
464  def good(self, args):
465  return (True, None)
466 
467  def chkEncoderMode(self, mi, v):
468  if v[mi] == 0x00:
469  return (True, None)
470  else:
471  e = "0x%02x != required quadrature encoding" % (v[mi])
472  return (False, e)
473 
474  def fmtEncoderMode(self, mi, v):
475  s = "0x%02x " % (v[mi])
476  if v[mi] == 0:
477  s += "(quadrature encoding)"
478  return s
479 
480  def fmtPidq(self, motorIndex, v):
481  f = 65536.0
482  s = "Kp=%f, Ki=%f, Kd=%f, max_qpps=%u" % (v[0]/f, v[1]/f, v[2]/f, v[3])
483  return s
484 
485 
486 # ------------------------------------------------------------------------------
487 # Time-of-Flight Diagnostics Class
488 # ------------------------------------------------------------------------------
489 
490 class DiagToFs():
491  def __init__(self, kwargs):
492  self.kwargs = kwargs
493 
494  def run(self):
495  print
496  print DiagSep
497  print "*** Laelaps Time-of-Flight Diagnostics ***"
498  print
499 
500  print "Diagnostics not supported yet: [FAIL]"
501  print
502  return 0
503 
504 
505 # ------------------------------------------------------------------------------
506 # Inertia Measurement Unit Diagnostics Class
507 # ------------------------------------------------------------------------------
508 
509 class DiagImu():
510  def __init__(self, kwargs):
511  self.kwargs = kwargs
512 
513  def run(self):
514  print
515  print DiagSep
516  print "*** Laelaps Inertia Measurement Unit Diagnostics ***"
517  print
518 
519  print "Diagnostics not supported yet: [FAIL]"
520  print
521  return 0
522 
523 
524 # ------------------------------------------------------------------------------
525 # Arduino Watchdog Diagnostics Class
526 # ------------------------------------------------------------------------------
527 
528 class DiagWatchdog():
529  def __init__(self, kwargs):
530  self.kwargs = kwargs
531 
532  def run(self):
533  print
534  print DiagSep
535  print "*** Laelaps Arduino Watchdog Diagnostics ***"
536  print
537 
538  print "Diagnostics not supported yet: [FAIL]"
539  print
540  return 0
541 
542 
543 # ------------------------------------------------------------------------------
544 # Exception Class usage
545 # ------------------------------------------------------------------------------
546 
547 ##
548 ## \brief Usage exception class.
549 ##
550 ## Raise usage exception.
551 ##
552 class usage(Exception):
553 
554  ##
555  ## \brief Constructor.
556  ##
557  ## \param msg Error message string.
558  ##
559  def __init__(self, msg):
560  ## error message attribute
561  self.msg = msg
562 
563 
564 # ------------------------------------------------------------------------------
565 # Class application
566 # ------------------------------------------------------------------------------
567 
568 ##
569 ## \brief Laelaps diagnositcs application class.
570 ##
571 class application():
572 
573  #
574  ## \brief Constructor.
575  #
576  def __init__(self):
577  self._Argv0 = os.path.basename(__file__)
578  self.m_win = None
579 
580  #
581  ## \brief Print usage error.
582  ##
583  ## \param emsg Error message string.
584  #
585  def printUsageErr(self, emsg):
586  if emsg:
587  print "%s: Error: %s" % (self._Argv0, emsg)
588  else:
589  print "%s: Error" % (self._Argv0)
590  print "Try '%s --help' for more information." % (self._Argv0)
591 
592  ## \brief Print Command-Line Usage Message.
593  def printUsage(self):
594  print \
595 """
596 usage: %s [OPTIONS] DIAG [DIAG ...]
597  %s --help
598 
599 Run Laelaps diagnostic(s).
600 
601 Options and arguments:
602  --fake : Run diagnostics on fake hardware."
603 -h, --help : Display this help and exit.
604 
605 DIAG : Diagnostic to run. One of:
606  all - Run all dignostics.
607  motors - Run motors diagnotics.
608  tofs - Run time-of-flight sensors diagnostics.
609  imu - Run Inertia Measurement Unit diagnostics.
610  watchdog - Run Arduino watchdog sub-processor diagnostics.
611 
612 Exit Status:
613  The number of diagnostics passed. An exit status of 128 indicates usage
614  error.
615 """ % (self._Argv0, self._Argv0)
616 
617  #
618  ## \brief Get command-line options
619  ##
620  ## \param argv Argument list. If not None, then overrides
621  ## command-line arguments.
622  ## \param [out] kwargs Keyword argument list.
623  ##
624  ## \return Parsed keyword arguments.
625  #
626  def getOptions(self, argv=None, **kwargs):
627  if argv is None:
628  argv = sys.argv
629 
630  self._Argv0 = os.path.basename(kwargs.get('argv0', __file__))
631 
632  # defaults
633  kwargs['debug'] = False
634  kwargs['fake'] = False
635 
636  # parse command-line options
637  try:
638  opts, args = getopt.getopt(argv[1:], "f?h",
639  ['help', 'fake', ''])
640  except getopt.error, msg:
641  raise usage(msg)
642  for opt, optarg in opts:
643  if opt in ('-f', '--fake'):
644  kwargs['fake'] = True
645  elif opt in ('-h', '--help', '-?'):
646  self.printUsage()
647  sys.exit(0)
648 
649  if len(args) < 1:
650  self.printUsageErr("No diagnostic(s) specified.")
651  sys.exit(128)
652  try:
653  args.index('all')
654  kwargs['diags'] = Diagnostics
655  except ValueError:
656  kwargs['diags'] = args
657 
658  return kwargs
659 
660  #
661  ## \brief Run application.
662  ##
663  ## \param argv Optional argument list to override command-line arguments.
664  ## \param kwargs Optional keyword argument list.
665  ##
666  ## \return Exit code.
667  #
668  def run(self, argv=None, **kwargs):
669 
670  # parse command-line options and arguments
671  try:
672  self.kwargs = self.getOptions(argv, **kwargs)
673  except usage, e:
674  print e.msg
675  return 128
676 
677  passCnt = 0
678  diagCnt = 0
679  for diag in self.kwargs['diags']:
680  try:
681  Diagnostics.index(diag)
682  except ValueError:
683  print "%s: Unknown diagnostic - ignoring" % (diag)
684  continue
685  diagCnt += 1
686  if diag == 'motors':
687  diag = DiagMotors(self.kwargs)
688  elif diag == 'tofs':
689  diag = DiagToFs(self.kwargs)
690  elif diag == 'imu':
691  diag = DiagImu(self.kwargs)
692  elif diag == 'watchdog':
693  diag = DiagWatchdog(self.kwargs)
694  passCnt += diag.run()
695 
696  print
697  print " ~~~"
698  print "%d/%d diagnostics passed." % (passCnt, diagCnt)
699  print
700 
701  return passCnt
702 
703 
704 # ------------------------------------------------------------------------------
705 # main
706 # ------------------------------------------------------------------------------
707 if __name__ == '__main__':
708  app = application();
709  sys.exit( app.run() );
def good(self, args)
def testMotorState(self, motorctlr, addr, motorIndex, name)
def getOptions(self, argv=None, kwargs)
Get command-line options.
def serReadHelper(self, fnRead, argsRead)
def __init__(self)
Constructor.
def fmtPidq(self, motorIndex, v)
def printUsageErr(self, emsg)
Print usage error.
def run(self, argv=None, kwargs)
Run application.
def stopHelper(self, motorctlr)
def __init__(self, msg)
Constructor.
def fmtEncoderMode(self, mi, v)
Usage exception class.
def serWriteHelper(self, fnWrite, argsWrite)
def setSpeedHelper(self, motorctlr, motorkey, speed)
def chkEncoderMode(self, mi, v)
def runTest(self, what, fnExec, argsExec, fnChk, fnFmt)
def testMotors(self, motorctlr)
def testMotorCtlrState(self, motorctlr, addr, name)
msg
error message attribute
Laelaps diagnositcs application class.
def printUsage(self)
Print Command-Line Usage Message.