00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042 PKG = 'mtrace_tools'
00043
00044 import roslib; roslib.load_manifest(PKG)
00045
00046 from diagnostic_msgs.msg import DiagnosticStatus, DiagnosticArray, KeyValue
00047
00048 import rosrecord
00049 import time
00050 import sys
00051 import os
00052 import re
00053 from collections import deque
00054 import unittest
00055 import getopt
00056 import rospy
00057 import itertools
00058
00059 from diag_error import BaseError, GenericError, ErrorDelay, NoError
00060 from delay_queue import DelayQueue
00061
00062
00063 """
00064 usage: %(progname)s [-h] <bagfile>
00065 Goes through diagnostic <bagfile> and annotates errors
00066
00067 Options:
00068 -h : show this help
00069 """
00070
00071 def usage(progname):
00072 print __doc__ % vars()
00073
00074
00075 class ConvertVar:
00076 def __init__(self, var_name, convert_func, default):
00077 if var_name[0] == '_':
00078 raise Exception("Cannot use var_name begining with '_'")
00079 self.var_name = var_name
00080 self.convert_func = convert_func
00081 self.default = default
00082
00083 def convert(self, obj, value):
00084 setattr(obj, self.var_name, self.convert_func(value))
00085
00086 def set_default(self, obj):
00087 setattr(obj, self.var_name, self.default)
00088
00089 class ConvertList:
00090 def __init__(self, var_name, convert_func, index, default):
00091 if var_name[0] == '_':
00092 raise Exception("Cannot use var_name begining with '_'")
00093 self.var_name = var_name
00094 self.convert_func = convert_func
00095 self.index = index
00096 self.default = default
00097
00098 def _set(self, obj, value):
00099 l = getattr(obj, self.var_name, [])
00100 if len(l) < (self.index+1):
00101 for i in range(self.index+1-len(l)):
00102 l.append(None)
00103 l[self.index] = self.default
00104 setattr(obj, self.var_name, l)
00105
00106 def convert(self, obj, value):
00107 self._set(obj, self.convert_func(value))
00108
00109 def set_default(self, obj):
00110 self._set(obj, self.default)
00111
00112 class VarStorage:
00113 """ Used to store variables """
00114
00115 class KeyValueList:
00116 def __init__(self):
00117 self._fields = {}
00118
00119 def add( self, key_name, convert_obj):
00120 """ Add conversion to list """
00121 self._fields[key_name] = convert_obj
00122
00123 def convert(self, msg, obj):
00124 """ Convert values contained in DiagnosticStatus message into variable in obj """
00125 for kv in msg.values:
00126 convert_obj = self._fields.get(kv.key)
00127 if convert_obj != None:
00128 convert_obj.convert(obj, kv.value)
00129
00130 def set_defaults(self, obj):
00131 """ Sets default variable values in object """
00132 for convert_obj in self._fields.itervalues():
00133 convert_obj.set_default(obj)
00134
00135 class OnlyMotorHaltedError(BaseError):
00136 """ Represents error from master of just 'Motors Halted' """
00137 def __init__(self, name, t, desc):
00138 BaseError.__init__(self, name, t, desc)
00139
00140 class MasterDiag:
00141 """ Looks for errors in EtherCAT Master """
00142 def __init__(self, diag_map):
00143 self.name = 'EtherCAT Master'
00144 diag_map[self.name] = self
00145
00146 self.level = 0
00147 self.message = "OK"
00148
00149 kvl = KeyValueList()
00150 kvl.add('Dropped Packets', ConvertVar('dropped_packets', int, 0))
00151 kvl.add('RX Late Packet', ConvertVar('late_packets', int, 0))
00152 self.kvl = kvl
00153
00154 self.old = VarStorage()
00155 kvl.set_defaults(self.old)
00156
00157 def is_match(self, msg):
00158 """ Returns true if msg should handled by this component """
00159 return msg.name == self.name
00160
00161 def process(self, msg, t):
00162 """ Returns array of error descriptions this message my have caused """
00163 error_list = []
00164
00165 old = self.old
00166 new = VarStorage()
00167 self.kvl.convert(msg, new)
00168
00169 if msg.level > self.level:
00170 if msg.level==2 and msg.message == 'Motors halted':
00171 error = OnlyMotorHaltedError(self.name, t, msg.message)
00172 else:
00173 error = GenericError(self.name, t, "transitioned into level %d : %s" % (msg.level, msg.message))
00174 error_list.append(error)
00175 elif msg.level != 0 and msg.message != self.message:
00176 error_list.append(GenericError(self.name, t, "message changed to %s" % (msg.message)))
00177
00178 if new.dropped_packets != old.dropped_packets:
00179 error_list.append(GenericError(self.name, t, "dropped %d packets" % (new.dropped_packets - old.dropped_packets)))
00180
00181 if new.late_packets != old.late_packets:
00182 error_list.append(GenericError(self.name, t, "%d late packets" % (new.late_packets - old.late_packets)))
00183
00184 self.level = msg.level
00185 self.message = msg.message
00186
00187 self.old = new
00188
00189 return error_list
00190
00191
00192
00193 class SafetyDisableError(BaseError):
00194 """ Represents any type of EtherCAT device safety disable """
00195 def __init__(self, name, t, desc):
00196 BaseError.__init__(self, name, t, desc)
00197
00198 class UndervoltageLockoutError(BaseError):
00199 """ Represents undervoltage lockout of EtherCAT device """
00200 def __init__(self, name, t, desc):
00201 BaseError.__init__(self, name, t, desc)
00202
00203
00204 class SafetyDisableStatus:
00205 def __init__(self, str):
00206 self.undervoltage = str.find("UNDERVOLTAGE") != -1
00207 self.over_current = str.find("OVER_CURRENT") != -1
00208 self.board_overtemp = str.find("BOARD_OVER_TEMP") != -1
00209 self.bridge_overtemp = str.find("HBRIDGE_OVER_TEMP") != -1
00210 self.operational = str.find("OPERATIONAL") != -1
00211 self.watchdog = str.find("WATCHDOG") != -1
00212 self.disabled = str.find("DISABLED") != -1
00213 self.enabled = str.find("ENABLED") != -1
00214 self.str = str
00215
00216 if (self.disabled == self.enabled):
00217 raise Exception("disabled and enabled both set in %s", str)
00218
00219 def compare(self, old):
00220 """ Compares this safety disable status against old status.
00221 returns true if safety disable is set that was not in old.
00222 """
00223 return (self.undervoltage and not old.undervoltage) or \
00224 (self.watchdog and not old.watchdog) or \
00225 (self.bridge_overtemp and not old.bridge_overtemp) or \
00226 (self.board_overtemp and not old.board_overtemp) or \
00227 (self.over_current and not old.over_current) or \
00228 (self.operational and not old.operational) or \
00229 (self.disabled and not old.disabled)
00230
00231 def just_undervoltage(self):
00232 """ Returns true if safety disable status is just undervoltage """
00233 return self.undervoltage and not (self.over_current or self.board_overtemp or self.bridge_overtemp or self.operational or self.watchdog)
00234
00235 def to_str(self):
00236 return self.str
00237
00238
00239 def decode_safety_disable(value):
00240 """ Return safety disable status object """
00241 return SafetyDisableStatus(value)
00242
00243
00244 class EthercatDeviceDiag:
00245 """ Looks for errors in a specific EtherCAT Device """
00246 def __init__(self, diag_map, name, num_ports, has_encoder):
00247 self.name = name
00248 diag_map[self.name] = self
00249 self.num_ports = num_ports
00250 self.has_encoder = has_encoder
00251
00252 kvl = KeyValueList()
00253 kvl.add('Safety Disable Status Hold', ConvertVar('safety_disable_status_hold', decode_safety_disable, SafetyDisableStatus("ENABLED (00)")))
00254 kvl.add('Num encoder_errors', ConvertVar('encoder_errors', int, 0))
00255 for i in range(4):
00256 kvl.add('RX Error Port %d'%i, ConvertList('rx_error', int, i, 0))
00257 kvl.add('Lost Link Port %d'%i, ConvertList('lost_link', int, i, 0))
00258 self.kvl = kvl
00259
00260 self.old = VarStorage()
00261 kvl.set_defaults(self.old)
00262
00263 def process(self, msg, t):
00264 error_list = []
00265
00266 name = self.name
00267 old = self.old
00268 new = VarStorage()
00269 self.kvl.convert(msg, new)
00270
00271 if self.has_encoder and new.encoder_errors != old.encoder_errors:
00272 error_list.append(GenericError(name, t, "%d new encoder errors %d" % (new.encoder_errors - old.encoder_errors)))
00273
00274 if (new.safety_disable_status_hold.compare(old.safety_disable_status_hold)):
00275 if (new.safety_disable_status_hold.just_undervoltage()):
00276 error_list.append(UndervoltageLockoutError(name, t, "undervoltage lockout"))
00277 else:
00278 error_list.append(GenericError(name, t, "safety disable status changed to %s" % (new.safety_disable_status_hold.to_str())))
00279
00280 num_ports = len(new.rx_error)
00281 if num_ports != self.num_ports:
00282 error_list.append(GenericError(name, t, "changing number of ports from %d to %d" % (self.num_ports, num_ports)))
00283 self.num_ports = num_ports
00284
00285 for i in range(self.num_ports):
00286 if new.rx_error[i] != old.rx_error[i]:
00287 error_list.append(GenericError(name,t,"%d RX errors" % (new.rx_error[i] - old.rx_error[i])))
00288 if new.lost_link[i] != old.lost_link[i]:
00289 error_list.append(GenericError(name,t,"%d lost links" % (new.lost_link[i] - old.lost_link[i])))
00290
00291 self.old = new
00292
00293 return error_list
00294
00295
00296 class EthercatDeviceAddDiag:
00297 """ Looks for EtherCAT devices that are not already present and adds a new EtherCAT Device Diag for them """
00298
00299 def __init__(self, diag_list, diag_map):
00300 self.name = 'EthercatDeviceAddDiag'
00301 diag_list.append(self)
00302
00303 self.diag_list = diag_list
00304 self.diag_map = diag_map
00305
00306 self.is_ethercat_device = re.compile("EtherCAT Device \(\w+\)")
00307
00308 def is_match(self, msg):
00309 m = self.is_ethercat_device.match(msg.name)
00310 return m != None
00311
00312 def process(self, msg, t):
00313 name = msg.name
00314
00315
00316
00317 num_ports = 1
00318 has_encoder = False
00319 if (re.match("68-05005-[0-9]{5}$", msg.hardware_id)):
00320 num_ports = 2
00321 has_encoder = True
00322 elif (re.match("68-05006-[0-9]{5}$", msg.hardware_id)):
00323 num_ports = 1
00324 has_encoder = True
00325 elif (re.match("68-05014-[0-9]{5}$", msg.hardware_id)):
00326 num_ports = 4
00327 elif (re.match("68-05021-[0-9]{5}$", msg.hardware_id)):
00328 num_ports = 2
00329 else:
00330 print "Don't understand hardware_id = ", msg.hardware_id
00331
00332 dev = EthercatDeviceDiag(self.diag_map, name, num_ports, has_encoder)
00333
00334 return dev.process(msg,t)
00335
00336
00337 class RealtimeControlLoopDiag:
00338 """ Looks for issues occurring in 'Realtime Control Loop' """
00339 def __init__(self, diag_map):
00340 self.name = "Realtime Control Loop"
00341 diag_map[self.name] = self
00342
00343 self.level = 0
00344 self.message = "OK"
00345
00346 kvl = KeyValueList()
00347 kvl.add('Control Loop Overruns', ConvertVar('control_loop_overruns', int, 0))
00348 kvl.add('Max EtherCAT roundtrip (us)', ConvertVar('max_ethercat_roundtrip', float, 0.0))
00349 kvl.add('Max Controller Manager roundtrip (us)', ConvertVar('max_controller_manager_roundtrip', float, 0.0))
00350
00351 self.kvl = kvl
00352 self.old = VarStorage()
00353 kvl.set_defaults(self.old)
00354
00355 def process(self, msg, t):
00356 error_list = []
00357
00358 name = self.name
00359 old = self.old
00360 new = VarStorage()
00361 self.kvl.convert(msg, new)
00362
00363
00364
00365
00366
00367 if (new.max_ethercat_roundtrip > old.max_ethercat_roundtrip) and (new.max_ethercat_roundtrip > 1000):
00368 error_list.append(GenericError(name, t, "Max ethercat roundtrip %f" % (new.max_ethercat_roundtrip)))
00369
00370 if (new.max_controller_manager_roundtrip > old.max_controller_manager_roundtrip) and (new.max_controller_manager_roundtrip > 1000):
00371 error_list.append(GenericError(name, t, "Max controller roundtrip %f" % (new.max_controller_manager_roundtrip)))
00372
00373 self.old = new
00374 self.level = msg.level
00375 self.message = msg.message
00376
00377 return error_list
00378
00379
00380 class RunStopError(BaseError):
00381 """ Represents Run-Stop being pressed. """
00382 def __init__(self, name, t, desc):
00383 BaseError.__init__(self, name, t, desc)
00384 self.undervoltage_errors = []
00385 self.motors_halted = None
00386 def short_desc(self):
00387 desc = self.desc
00388 desc += ' + motors halted' if (self.motors_halted != None) else ''
00389 if len(self.undervoltage_errors) > 0:
00390 desc += ' + ' + str(len(self.undervoltage_errors)) + ' undervoltage errors'
00391 return desc
00392
00393 def str_to_bool(str):
00394 if (str == "True"):
00395 return True
00396 elif (str == "False"):
00397 return False
00398 else:
00399 raise Exception("Not boolean : %s" % str)
00400
00401 class PowerBoardDiag:
00402 """ Looks for issues occurring in 'Power Board' """
00403 def __init__(self, diag_map, name):
00404 self.name = name
00405 diag_map[self.name] = self
00406
00407 self.level = 0
00408 self.message = "Running"
00409
00410 kvl = KeyValueList()
00411 kvl.add('RunStop Button Status', ConvertVar('runstop_button_status', str_to_bool, True))
00412 kvl.add('RunStop Wireless Status', ConvertVar('runstop_wireless_status', str_to_bool, True))
00413
00414 self.kvl = kvl
00415 self.old = VarStorage()
00416 kvl.set_defaults(self.old)
00417
00418 def process(self, msg, t):
00419 error_list = []
00420
00421 name = self.name
00422 old = self.old
00423 new = VarStorage()
00424 self.kvl.convert(msg, new)
00425
00426 if (not new.runstop_button_status and old.runstop_button_status) or \
00427 (not new.runstop_wireless_status and old.runstop_wireless_status):
00428 error_list.append(RunStopError(name, t, "Runstop"))
00429
00430 self.old = new
00431 self.level = msg.level
00432 self.message = msg.message
00433
00434 return error_list
00435
00436
00437 class PowerBoardAddDiag:
00438 """ Looks for Power Board Devices and adds a PowerBoard for them """
00439 def __init__(self, diag_list, diag_map):
00440 self.name = 'PowerBoardAddDiag'
00441 diag_list.append(self)
00442 self.diag_list = diag_list
00443 self.diag_map = diag_map
00444 self.is_power_board = re.compile("Power board [0-9]{4}$")
00445
00446 def is_match(self, msg):
00447 m = self.is_power_board.match(msg.name)
00448 return m != None
00449
00450 def process(self, msg, t):
00451 name = msg.name
00452
00453 dev = PowerBoardDiag(self.diag_map, name)
00454 return dev.process(msg,t)
00455
00456 class RunStopErrorMerge:
00457 """ Merges Undervoltage errors and MotorsHalted error into Runstop error """
00458 def __init__(self):
00459 def get_t(obj):
00460 return obj.t
00461 past = rospy.Duration.from_sec(2)
00462 future = rospy.Duration.from_sec(5)
00463 self.dq = DelayQueue(self.merge, get_t, future, past)
00464
00465 def process(self,error_list):
00466 return self.dq.process(error_list)
00467
00468 def merge(self, error, future, past):
00469 if type(error).__name__ != 'RunStopError':
00470 return
00471 for error2 in itertools.chain(future,past):
00472 typename = type(error2).__name__
00473 if typename == 'UndervoltageLockoutError':
00474 error.sub_errors.append(error2)
00475 error.undervoltage_errors.append(error2)
00476 error2.parents.append(error)
00477 elif typename == 'OnlyMotorHaltedError' and error.motors_halted == None:
00478 error.sub_errors.append(error2)
00479 error.motors_halted = error2
00480 error2.parents.append(error)
00481
00482 class PrintErrors:
00483 def __init__(self, duration):
00484 self.duration = duration
00485 self.last_time = rospy.Time(0)
00486 self.last_name = "NO_ERROR"
00487
00488 def process(self, error_list):
00489 if len(error_list) == 0:
00490 return
00491
00492 for error in error_list:
00493 if type(error).__name__ == 'NoError':
00494 continue
00495 if len(error.parents) > 0:
00496 continue
00497 if (error.t - self.last_time).to_sec() > self.duration:
00498 print "On %s" % time.strftime("%a, %b %d, %I:%M:%S %p", time.localtime(error.t.to_sec()))
00499
00500
00501
00502 self.last_time = error.t
00503 print ' ', error.name
00504 self.last_name = error.name
00505
00506 if error.name != self.last_name:
00507 print ' ', error.name
00508 self.last_name = error.name
00509 print ' ', error.short_desc()
00510
00511
00512 def main(argv):
00513 progname = argv[0]
00514 optlist, argv = getopt.getopt(argv[1:], "ht", ["help", "test"])
00515 for (opt, val) in optlist:
00516 if opt == "--help" or opt == '-h':
00517 usage(progname)
00518 return 0
00519 elif opt == "--test" or opt == '-t':
00520 return 0
00521 else:
00522 print "Internal error : unhandled option '%s'"%opt
00523 return 1
00524
00525 inbag_filename = argv[0]
00526 if not os.path.isfile(inbag_filename):
00527 print >> sys.stderr, "Cannot locate input bag file [%s]" % inbag_filename
00528 return 2
00529
00530 diag_list = []
00531 diag_map = {}
00532
00533 MasterDiag(diag_map)
00534 RealtimeControlLoopDiag(diag_map)
00535 EthercatDeviceAddDiag(diag_list, diag_map)
00536 PowerBoardAddDiag(diag_list, diag_map)
00537
00538
00539 reorder = ErrorDelay(30.0, True)
00540 merge_list = []
00541 merge_list.append(RunStopErrorMerge())
00542
00543 print_errors = PrintErrors(1.0)
00544 last_error_time = rospy.Time(0)
00545
00546
00547 for topic, msg, tbag in rosrecord.logplayer(inbag_filename):
00548 t = msg.header.stamp
00549 header = False
00550 error_list = []
00551 for status in msg.status:
00552 if status.name in diag_map:
00553 error_list += diag_map[status.name].process(status, t)
00554 else:
00555 for diag in diag_list:
00556 if diag.is_match(status):
00557 error_list += diag.process(status, t)
00558 break
00559
00560
00561
00562
00563
00564
00565
00566
00567
00568
00569
00570
00571 if len(error_list) > 0:
00572 last_error_time = t
00573 elif (t-last_error_time).to_sec() > 10.0:
00574
00575 error_list.append(NoError("flush", t, "10second flush"))
00576 last_error_time = t
00577
00578
00579 error_list = reorder.process(error_list)
00580
00581 len(error_list)
00582
00583
00584 for merge in merge_list:
00585 error_list = merge.process(error_list)
00586
00587
00588 print_errors.process(error_list)
00589
00590
00591 print "Log ends %s" % time.strftime("%a, %b %d, %I:%M:%S %p", time.localtime(t.to_sec()))
00592 return 0
00593
00594
00595 if __name__ == '__main__':
00596 sys.exit(main(sys.argv))