Laelaps  2.3.5
RoadNarrows Robotics Small Outdoor Mobile Robot Project
laelaps_speed.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_speed
8 #
9 ## \file
10 ##
11 ## $LastChangedDate: 2015-09-09 14:00:46 -0600 (Wed, 09 Sep 2015) $
12 ## $Rev: 4080 $
13 ##
14 ## \brief Run one motor at the given speed.
15 ##
16 ## \author Robin Knight (robin.knight@roadnarrows.com)
17 ##
18 ## \par Copyright
19 ## \h_copy 2015-2017. RoadNarrows LLC.\n
20 ## http://www.roadnarrows.com\n
21 ## All Rights Reserved
22 ##
23 # @EulaBegin@
24 #
25 # Unless otherwise stated explicitly, all materials contained are copyrighted
26 # and may not be used without RoadNarrows LLC's written consent,
27 # except as provided in these terms and conditions or in the copyright
28 # notice (documents and software) or other proprietary notice provided with
29 # the relevant materials.
30 #
31 # IN NO EVENT SHALL THE AUTHOR, ROADNARROWS LLC, OR ANY
32 # MEMBERS/EMPLOYEES/CONTRACTORS OF ROADNARROWS OR DISTRIBUTORS OF THIS SOFTWARE
33 # BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR
34 # CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
35 # DOCUMENTATION, EVEN IF THE AUTHORS OR ANY OF THE ABOVE PARTIES HAVE BEEN
36 # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 #
38 # THE AUTHORS AND ROADNARROWS LLC SPECIFICALLY DISCLAIM ANY WARRANTIES,
39 # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
40 # FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN
41 # "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE NO OBLIGATION TO
42 # PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
43 #
44 # @EulaEnd@
45 #
46 ###############################################################################
47 
48 import sys
49 import os
50 import termios
51 import fcntl
52 import time
53 import math
54 import random
55 import getopt
56 
57 from serial.serialutil import SerialException, SerialTimeoutException
58 
59 import Laelaps.SysConf as SysConf
60 import Laelaps.WatchDog as WatchDog
61 import Laelaps.RoboClaw as RoboClaw
62 
63 MotorNames = ['left_front', 'right_front', 'left_rear', 'right_rear']
64 TestSep = "\
65 -------------------------------------------------------------------------------"
66 
67 # ------------------------------------------------------------------------------
68 # Keyboard Class
69 # ------------------------------------------------------------------------------
70 
71 class keyboard():
72  def __init__(self):
73  fd = sys.stdin.fileno()
74  self.oldterm = termios.tcgetattr(fd)
75  self.oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
76 
77  def __del__(self):
78  self.sane()
79 
80  def sane(self):
81  fd = sys.stdin.fileno()
82  termios.tcsetattr(fd, termios.TCSAFLUSH, self.oldterm)
83  fcntl.fcntl(fd, fcntl.F_SETFL, self.oldflags)
84 
85  def setNonBlocking(self):
86  fd = sys.stdin.fileno()
87  newattr = termios.tcgetattr(fd)
88  newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
89  termios.tcsetattr(fd, termios.TCSANOW, newattr)
90  fcntl.fcntl(fd, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK)
91 
92  def getchar(self):
93  c = None
94  try:
95  c = sys.stdin.read(1)
96  except IOError:
97  pass
98  return c
99 
100 # ------------------------------------------------------------------------------
101 # Fake Motor Controller Class
102 # ------------------------------------------------------------------------------
103 
104 class fakemc():
105  def __init__(self):
106  pass
107 
108  def open(self, dev, baud, fnChipSelect=None):
109  self.dev = dev
110  self.baud = baud
111  self.m1_speed = {}
112  self.m2_speed = {}
113  self.m1_encoder = {}
114  self.m2_encoder = {}
115 
116  def close(self):
117  pass
118 
119  def readVersion(self, addr):
120  return "1.5.6 firmwareXYZ"
121 
122  def readMainBatterySettings(self, addr):
123  return (60.1, 324.0)
124 
125  def readLogicBatterySettings(self, addr):
126  raise SerialTimeoutException('bogus')
127 
128  def readEncoderMode(self, addr):
129  return (0, 0)
130 
131  def readM1Pidq(self, addr):
132  return (0x00010000, 0x00008000, 0x00004000, 10000)
133 
134  def readM2Pidq(self, addr):
135  return (0x00010000, 0x00008000, 0x00004000, 10000)
136 
137  def readErrorState(self, addr):
138  return 0
139 
140  def readTemperature(self, addr):
141  return 288
142 
143  def readMainBattery(self, addr):
144  return 111
145 
146  def readLogicBattery(self, addr):
147  return 49
148 
149  def readCurrents(self, addr):
150  return (75.0, 49.0)
151 
152  def readM1Speed(self, addr):
153  if not self.m1_speed.has_key(addr):
154  self.m1_speed[addr] = 0
155  return (self.m1_speed[addr], 0x00)
156 
157  def readM2Speed(self, addr):
158  if not self.m2_speed.has_key(addr):
159  self.m2_speed[addr] = 0
160  return (self.m2_speed[addr], 0x00)
161 
162  def setM1Speed(self, addr, speed):
163  self.m1_speed[addr] = speed
164 
165  def setM2Speed(self, addr, speed):
166  self.m2_speed[addr] = speed
167 
168  def readM1Encoder(self, addr):
169  if not self.m1_encoder.has_key(addr):
170  self.m1_encoder[addr] = random.randint(0, 1000000)
171  return (self.m1_encoder[addr], 0)
172 
173  def readM2Encoder(self, addr):
174  if not self.m2_encoder.has_key(addr):
175  self.m2_encoder[addr] = random.randint(0, 1000000)
176  return (self.m2_encoder[addr], 0x02)
177 
178  def resetEncoderCnts(self, addr):
179  self.m1_encoder[addr] = 0
180  self.m2_encoder[addr] = 0
181 
182 
183 # ------------------------------------------------------------------------------
184 # Speed Class
185 # ------------------------------------------------------------------------------
186 
187 class Speed():
188  def __init__(self, kwargs):
189  self.kwargs = kwargs
190 
191  self.ctlrKeys = ['front', 'rear']
192  self.ctlrInfo = {
193  'front': {'addr': SysConf.MotorCtlrAddrFront, 'name': 'front'},
194  'rear': {'addr': SysConf.MotorCtlrAddrRear, 'name': 'rear'}
195  }
196 
197  self.motorKeys = ['left_front', 'right_front', 'left_rear', 'right_rear']
198  self.motorInfo = {
199  'left_front':
200  {'ctlr_key': 'front', 'motor_index': 0, 'name': 'left_front'},
201  'right_front':
202  {'ctlr_key': 'front', 'motor_index': 1, 'name': 'right_front'},
203  'left_rear':
204  {'ctlr_key': 'rear', 'motor_index': 0, 'name': 'left_rear'},
205  'right_rear':
206  {'ctlr_key': 'rear', 'motor_index': 1, 'name': 'right_rear'},
207  }
208 
209  def run(self, keepalive):
210  motorkey = self.kwargs['motor']
211  goalspeed = self.kwargs['speed']
212 
213  print
214  print TestSep
215  print "*** Laelaps %s(%d) motor speed=%d test ***" % \
216  (motorkey, self.motorKeys.index(motorkey), goalspeed)
217  print
218 
219  # Create motor controller object
220  if self.kwargs['fake']:
221  motorctlr = fakemc()
222  else:
223  motorctlr = RoboClaw.RoboClaw()
224 
225  # Open communiction on serial bus to Laelaps multi-dropped motor controllers
226  device = SysConf.MotorCtlrDevName
227  baud = SysConf.MotorCtlrBaudRate
228 
229  print "Open motor controller serial interface %s@%d" % (device, baud)
230  try:
231  motorctlr.open(device, baud, None)
232  except RoboClaw.RoboClawException as inst:
233  print 'Error:', inst.message
234  print
235 
236  if motorctlr.isOpen():
237  addr = self.ctlrInfo[self.motorInfo[motorkey]['ctlr_key']]['addr']
238  mi = self.motorInfo[motorkey]['motor_index']
239 
240  if mi == 0:
241  setSpeed = motorctlr.setM1Speed
242  readSpeed = motorctlr.readM1Speed
243  readEncoder = motorctlr.readM1Encoder
244  else:
245  setSpeed = motorctlr.setM2Speed
246  readSpeed = motorctlr.readM2Speed
247  readEncoder = motorctlr.readM2Encoder
248 
249  motorctlr.resetEncoderCnts(addr)
250 
251  try:
252  setSpeed(addr, goalspeed)
253  except RoboClaw.RoboClawException as inst:
254  print "Failed to set speed"
255  return
256 
257  print
258  print "Press any key to abort"
259  print
260 
261  kb = keyboard()
262  kb.setNonBlocking()
263 
264  n = 0
265  nErrs = 0
266  while kb.getchar() is None:
267  try:
268  curspeed, status = readSpeed(addr)
269  curenc, status = readEncoder(addr)
270  print "%5d. %10d %14d\r" % (n, curspeed, curenc),
271  sys.stdout.flush()
272  n += 1
273  if nErrs > 0:
274  nErrs -= 1
275  except RoboClaw.RoboClawException as inst:
276  nErrs += 1
277  if nErrs >= 5:
278  break
279  time.sleep(0.1)
280  keepalive()
281 
282  print " \r"
283 
284  try:
285  setSpeed(addr, 0)
286  print "Stopped"
287  except RoboClaw.RoboClawException as inst:
288  print "Failed to stop"
289 
290  motorctlr.close()
291 
292 
293 # ------------------------------------------------------------------------------
294 # MotorCtlrEnable Class
295 # ------------------------------------------------------------------------------
296 
298  def __init__(self):
299  # The WatchDog subprocessor controls the enable lines to the motor
300  # controllers.
301  self.m_wd = WatchDog.WatchDog()
302  self.m_doDisable = False
303 
304  def enable(self):
305  if not self.m_wd.open(SysConf.SensorDevName):
306  print 'Error: Failed to open connection to watchdog subprocessor.'
307  print ' Continuing...'
308  return
309 
310  # get firmware version
311  self.m_wd.cmdGetFwVersion()
312 
313  # read motor controller enable lines state
314  ret = self.m_wd.cmdReadEnables()
315  if ret['rc'] != 'ok':
316  print 'Error: Failed to read enable lines.'
317  print ' Continuing...'
318  return
319 
320  if ret['enables']['motor_ctlr_en']:
321  print "Warning: Motor controllers already enabled - may be in use."
322  return
323 
324  self.m_doDisable = True
325 
326  ret = self.m_wd.cmdEnableMotorCtlrs(True)
327  if ret['rc'] != 'ok':
328  print 'Error: Failed to enable motor controllers.'
329  print ' Continuing...'
330  return
331 
332  time.sleep(0.5)
333 
334  print "Motor controllers enabled."
335 
336  def keepalive(self):
337  if self.m_wd.isOpen():
338  self.m_wd.cmdPetTheDog()
339 
340  def cleanup(self):
341  if self.m_doDisable:
342  ret = self.m_wd.cmdEnableMotorCtlrs(False)
343  if ret['rc'] == 'ok':
344  print "Motor controllers disabled."
345  self.m_wd.close()
346 
347 
348 # ------------------------------------------------------------------------------
349 # Exception Class usage
350 # ------------------------------------------------------------------------------
351 
352 ##
353 ## \brief Unit test command-line exception class.
354 ##
355 ## Raise usage excpetion.
356 ##
357 class usage(Exception):
358 
359  ##
360  ## \brief Constructor.
361  ##
362  ## \param msg Error message string.
363  ##
364  def __init__(self, msg):
365  ## error message attribute
366  self.msg = msg
367 
368 
369 # ------------------------------------------------------------------------------
370 # Class application
371 # ------------------------------------------------------------------------------
372 
373 ##
374 ## \brief Laelaps diagnositcs application class.
375 ##
376 class application():
377 
378  #
379  ## \brief Constructor.
380  #
381  def __init__(self):
382  self._Argv0 = os.path.basename(__file__)
383  self.m_win = None
384 
385  #
386  ## \brief Print usage error.
387  ##
388  ## \param emsg Error message string.
389  #
390  def printUsageErr(self, emsg):
391  if emsg:
392  print "%s: Error: %s" % (self._Argv0, emsg)
393  else:
394  print "%s: Error" % (self._Argv0)
395  print "Try '%s --help' for more information." % (self._Argv0)
396 
397  ## \brief Print Command-Line Usage Message.
398  def printUsage(self):
399  print \
400 """
401 usage: %s [OPTIONS] MOTOR SPEED
402  %s --help
403 
404 Run Laelaps speed test.
405 
406 Options and arguments:
407  --fake : Run diagnostics on fake hardware."
408 -h, --help : Display this help and exit.
409 
410 MOTOR : Motor name/id to test. One of:
411  left_front right_front left_rear right_rear
412  or
413  0 1 2 3
414 SPEED : Signed qpps speed of motor, with positive being forwards.
415 
416 Exit Status:
417  On success, 0. An exit status of 128 indicates usage
418  error.
419 """ % (self._Argv0, self._Argv0)
420 
421  #
422  ## \brief Get command-line options
423  ##
424  ## \param argv Argument list. If not None, then overrides
425  ## command-line arguments.
426  ## \param [out] kwargs Keyword argument list.
427  ##
428  ## \return Parsed keyword arguments.
429  #
430  def getOptions(self, argv=None, **kwargs):
431  if argv is None:
432  argv = sys.argv
433 
434  self._Argv0 = os.path.basename(kwargs.get('argv0', __file__))
435 
436  # defaults
437  kwargs['debug'] = False
438  kwargs['fake'] = False
439 
440  # parse command-line options
441  try:
442  opts, args = getopt.getopt(argv[1:], "f?h",
443  ['help', 'fake', ''])
444  except getopt.error, msg:
445  raise usage(msg)
446  for opt, optarg in opts:
447  if opt in ('-f', '--fake'):
448  kwargs['fake'] = True
449  elif opt in ('-h', '--help', '-?'):
450  self.printUsage()
451  sys.exit(0)
452 
453  if len(args) < 1:
454  self.printUsageErr("No motor specified.")
455  sys.exit(128)
456  elif len(args) < 2:
457  self.printUsageErr("No speed specified.")
458  sys.exit(128)
459 
460  kwargs['motor'] = None
461  try:
462  i = int(args[0])
463  if (i >= 0) and (i < len(MotorNames)):
464  kwargs['motor'] = MotorNames[i]
465  except ValueError:
466  try:
467  i = MotorNames.index(args[0])
468  kwargs['motor'] = args[0]
469  except ValueError:
470  pass
471 
472  if kwargs['motor'] is None:
473  self.printUsageErr("{0}: Not a motor.".format(args[0]))
474  sys.exit(128)
475 
476  try:
477  kwargs['speed'] = int(args[1])
478  except ValueError:
479  self.printUsageErr("%s: Not an integer." % (args[1]))
480  sys.exit(128)
481 
482  return kwargs
483 
484  #
485  ## \brief Run application.
486  ##
487  ## \param argv Optional argument list to override command-line arguments.
488  ## \param kwargs Optional keyword argument list.
489  ##
490  ## \return Exit code.
491  #
492  def run(self, argv=None, **kwargs):
493 
494  # parse command-line options and arguments
495  try:
496  self.kwargs = self.getOptions(argv, **kwargs)
497  except usage, e:
498  print e.msg
499  return 128
500 
501  en = MotorCtlrEnable()
502  en.enable()
503 
504  go = Speed(self.kwargs)
505  go.run(en.keepalive)
506 
507  en.cleanup()
508 
509 
510 # ------------------------------------------------------------------------------
511 # main
512 # ------------------------------------------------------------------------------
513 if __name__ == '__main__':
514  app = application();
515  sys.exit( app.run() );
def printUsageErr(self, emsg)
Print usage error.
Laelaps diagnositcs application class.
msg
error message attribute
def __init__(self)
Constructor.
def getOptions(self, argv=None, kwargs)
Get command-line options.
def __init__(self, msg)
Constructor.
def printUsage(self)
Print Command-Line Usage Message.
def run(self, argv=None, kwargs)
Run application.
Unit test command-line exception class.