annotate_diag.py
Go to the documentation of this file.
00001 #! /usr/bin/env python
00002 
00003 #
00004 # Software License Agreement (BSD License)
00005 #
00006 # Copyright (c) 2008, Willow Garage, Inc.
00007 # All rights reserved.
00008 #
00009 # Redistribution and use in source and binary forms, with or without
00010 # modification, are permitted provided that the following conditions
00011 # are met:
00012 #
00013 #  * Redistributions of source code must retain the above copyright
00014 #    notice, this list of conditions and the following disclaimer.
00015 #  * Redistributions in binary form must reproduce the above
00016 #    copyright notice, this list of conditions and the following
00017 #    disclaimer in the documentation and/or other materials provided
00018 #    with the distribution.
00019 #  * Neither the name of Willow Garage, Inc. nor the names of its
00020 #    contributors may be used to endorse or promote products derived
00021 #    from this software without specific prior written permission.
00022 #
00023 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00024 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00025 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00026 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00027 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00028 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00029 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00030 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00031 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00032 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00033 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00034 # POSSIBILITY OF SUCH DAMAGE.
00035 #
00036 
00037 
00038 ##\author Derek King
00039 ##\brief Annotates errors in diagnostic bag files.
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         #print "Found EtherCAT Device %s" % name
00315 
00316         # Use the HW id to figure number of ports and whether device has encoder
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         #There are too many control loop overruns, don't print this out for now
00364         #if new.control_loop_overruns != old.control_loop_overruns:
00365         #    error_list.append("%d new control_loop_overruns" % (new.control_loop_overruns - old.control_loop_overruns))
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         #print "New Power Board : %s" % name
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                 #t1 = time.strftime("%a, %b %d, %I:%M:%S %p", time.localtime(self.last_time.to_sec()))
00500                 #t2 = time.strftime("%a, %b %d, %I:%M:%S %p", time.localtime(error.t.to_sec())) 
00501                 #print "Between %s and %s" % (t1,t2)
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     # Use 30 second reordering buffer.
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 # use timestamp from diagnostic msg head instead of bag timestamp
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  # stop searching when first match is found
00559         
00560         #if len(error_list) > 0:
00561         #    last_name = error_list[0].name
00562         #    print "On %s" % time.strftime("%a, %b %d, %I:%M:%S %p", time.localtime(t.to_sec())) 
00563         #    for error in error_list:
00564         #        if error.name != last_name:
00565         #            print ' ', error.name
00566         #            last_name = error.name
00567         #        print '  ', error.desc
00568         
00569 
00570         # Generate no-errors every 10seconds to keep merge queues moving
00571         if len(error_list) > 0:
00572             last_error_time = t
00573         elif (t-last_error_time).to_sec() > 10.0:
00574             #print "FLUSH ------------------------"
00575             error_list.append(NoError("flush", t, "10second flush"))
00576             last_error_time = t
00577 
00578         # Put messages into reordering queue
00579         error_list = reorder.process(error_list)
00580 
00581         len(error_list)
00582 
00583         # Process error list
00584         for merge in merge_list:
00585             error_list = merge.process(error_list)            
00586 
00587         # Process list
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))


mtrace_tools
Author(s): Derek
autogenerated on Sat Dec 28 2013 17:58:07