00001
00002
00003 """ diagnostic_updater for Python.
00004 @author Brice Rebsamen <brice [dot] rebsamen [gmail]>
00005 """
00006
00007 import roslib
00008 roslib.load_manifest('diagnostic_updater')
00009 import rospy
00010 from ._diagnostic_updater import *
00011
00012
00013 class FrequencyStatusParam:
00014 """A structure that holds the constructor parameters for the FrequencyStatus
00015 class.
00016
00017 Implementation note: the min_freq and max_freq parameters in the C += 1 code
00018 are stored as pointers, so that if they are updated, the new values are used.
00019 To emulate this behavior, we here use a dictionary to hold them: {'min','max'}
00020
00021 freq_bound is a dictionary with keys 'min' and 'max', containing the min
00022 and max acceptable frequencies.
00023
00024 tolerance is the tolerance with which bounds must be satisfied. Acceptable
00025 values are from freq_bound['min'] * (1 - torelance) to
00026 freq_bound['max'] * (1 + tolerance). Common use cases are to set
00027 tolerance to zero, or to assign the same value to freq_bound['min'] and
00028 freq_bound['max']
00029
00030 window_size is the number of events to consider in the statistics.
00031 """
00032
00033 def __init__(self, freq_bound, tolerance = 0.1, window_size = 5):
00034 """Creates a filled-out FrequencyStatusParam."""
00035 self.freq_bound = freq_bound
00036 self.tolerance = tolerance
00037 self.window_size = window_size
00038
00039
00040 class FrequencyStatus(DiagnosticTask):
00041 """A diagnostic task that monitors the frequency of an event.
00042
00043 This diagnostic task monitors the frequency of calls to its tick method,
00044 and creates corresponding diagnostics. It will report a warning if the
00045 frequency is outside acceptable bounds, and report an error if there have
00046 been no events in the latest window.
00047 """
00048
00049 def __init__(self, params):
00050 """Constructs a FrequencyStatus class with the given parameters."""
00051 DiagnosticTask.__init__(self, "Frequency Status")
00052 self.params = params
00053 self.lock = threading.Lock()
00054 self.clear()
00055
00056 def clear(self):
00057 """Resets the statistics."""
00058 with self.lock:
00059 self.count = 0
00060 curtime = rospy.Time.now()
00061 self.times = [curtime for i in range(self.params.window_size)]
00062 self.seq_nums = [0 for i in range(self.params.window_size)]
00063 self.hist_indx = 0
00064
00065 def tick(self):
00066 """Signals that an event has occurred."""
00067 with self.lock:
00068
00069 self.count += 1
00070
00071 def run(self, stat):
00072 with self.lock:
00073 curtime = rospy.Time.now()
00074 curseq = self.count
00075 events = curseq - self.seq_nums[self.hist_indx]
00076 window = (curtime - self.times[self.hist_indx]).to_sec()
00077 freq = events / window
00078 self.seq_nums[self.hist_indx] = curseq
00079 self.times[self.hist_indx] = curtime
00080 self.hist_indx = (self.hist_indx + 1) % self.params.window_size
00081
00082 if events == 0:
00083 stat.summary(2, "No events recorded.")
00084 elif freq < self.params.freq_bound['min'] * (1 - self.params.tolerance):
00085 stat.summary(1, "Frequency too low.")
00086 elif freq > self.params.freq_bound['max'] * (1 + self.params.tolerance):
00087 stat.summary(1, "Frequency too high.")
00088 else:
00089 stat.summary(0, "Desired frequency met")
00090
00091 stat.add("Events in window", "%d" % events)
00092 stat.add("Events since startup", "%d" % self.count)
00093 stat.add("Duration of window (s)", "%f" % window)
00094 stat.add("Actual frequency (Hz)", "%f" % freq)
00095 if self.params.freq_bound['min'] == self.params.freq_bound['max']:
00096 stat.add("Target frequency (Hz)", "%f" % self.params.freq_bound['min'])
00097 if self.params.freq_bound['min'] > 0:
00098 stat.add("Minimum acceptable frequency (Hz)", "%f" % (self.params.freq_bound['min'] * (1 - self.params.tolerance)))
00099 if self.params.freq_bound['max']:
00100 stat.add("Maximum acceptable frequency (Hz)", "%f" % (self.params.freq_bound['max'] * (1 + self.params.tolerance)))
00101
00102 return stat
00103
00104
00105 class TimeStampStatusParam:
00106 """A structure that holds the constructor parameters for the TimeStampStatus class.
00107
00108 max_acceptable: maximum acceptable difference between two timestamps.
00109 min_acceptable: minimum acceptable difference between two timestamps.
00110 """
00111
00112 def __init__(self, min_acceptable = -1, max_acceptable = 5):
00113 """Creates a filled-out TimeStampStatusParam."""
00114 self.max_acceptable = max_acceptable
00115 self.min_acceptable = min_acceptable
00116
00117
00118 class TimeStampStatus(DiagnosticTask):
00119 """Diagnostic task to monitor the interval between events.
00120
00121 This diagnostic task monitors the difference between consecutive events,
00122 and creates corresponding diagnostics. An error occurs if the interval
00123 between consecutive events is too large or too small. An error condition
00124 will only be reported during a single diagnostic report unless it
00125 persists. Tallies of errors are also maintained to keep track of errors
00126 in a more persistent way.
00127 """
00128
00129 def __init__(self, params = TimeStampStatusParam()):
00130 """Constructs the TimeStampStatus with the given parameters."""
00131 DiagnosticTask.__init__(self, "Timestamp Status")
00132 self.params = params
00133 self.lock = threading.Lock()
00134 self.early_count = 0
00135 self.late_count = 0
00136 self.zero_count = 0
00137 self.zero_seen = False
00138 self.max_delta = 0
00139 self.min_delta = 0
00140 self.deltas_valid = False
00141
00142 def tick(self, stamp):
00143 """Signals an event.
00144 @param stamp The timestamp of the event that will be used in computing
00145 intervals. Can be either a double or a ros::Time.
00146 """
00147 if isinstance(stamp, rospy.Time):
00148 stamp = stamp.to_sec()
00149
00150 with self.lock:
00151 if stamp == 0:
00152 self.zero_seen = True
00153 else:
00154 delta = rospy.Time.now().to_sec() - stamp
00155 if not self.deltas_valid or delta > self.max_delta:
00156 self.max_delta = delta
00157 if not self.deltas_valid or delta < self.min_delta:
00158 self.min_delta = delta
00159 self.deltas_valid = True
00160
00161 def run(self, stat):
00162 with self.lock:
00163
00164 stat.summary(0, "Timestamps are reasonable.")
00165 if not self.deltas_valid:
00166 stat.summary(1, "No data since last update.")
00167 else:
00168 if self.min_delta < self.params.min_acceptable:
00169 stat.summary(2, "Timestamps too far in future seen.")
00170 self.early_count += 1
00171 if self.max_delta > self.params.max_acceptable:
00172 stat.summary(2, "Timestamps too far in past seen.")
00173 self.late_count += 1
00174 if self.zero_seen:
00175 stat.summary(2, "Zero timestamp seen.")
00176 self.zero_count += 1
00177
00178 stat.add("Earliest timestamp delay:", "%f" % self.min_delta)
00179 stat.add("Latest timestamp delay:", "%f" % self.max_delta)
00180 stat.add("Earliest acceptable timestamp delay:", "%f" % self.params.min_acceptable)
00181 stat.add("Latest acceptable timestamp delay:", "%f" % self.params.max_acceptable)
00182 stat.add("Late diagnostic update count:", "%i" % self.late_count)
00183 stat.add("Early diagnostic update count:", "%i" % self.early_count)
00184 stat.add("Zero seen diagnostic update count:", "%i" % self.zero_count)
00185
00186 self.deltas_valid = False
00187 self.min_delta = 0
00188 self.max_delta = 0
00189 self.zero_seen = False
00190
00191 return stat