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 import os
00036 import rospkg
00037
00038 from diagnostic_msgs.msg import DiagnosticArray, DiagnosticStatus
00039 from python_qt_binding import loadUi
00040 from python_qt_binding.QtCore import QTimer, Signal, Qt
00041 from python_qt_binding.QtGui import QColor, QPalette
00042 import rospy
00043
00044 from .abst_status_widget import AbstractStatusWidget
00045 from .chronologic_state import InstantaneousState, StatusItem
00046 from .time_pane import TimelinePane
00047 from .util_robot_monitor import Util
00048
00049
00050 class RobotMonitorWidget(AbstractStatusWidget):
00051 """
00052 NOTE: RobotMonitorWidget.shutdown function needs to be called
00053 when the instance of this class terminates.
00054
00055 RobotMonitorWidget itself doesn't store previous diagnostic states.
00056 It instead delegates that function to TimelinePane class.
00057 """
00058
00059 _sig_tree_nodes_updated = Signal(int)
00060 _sig_new_diagnostic = Signal(DiagnosticArray)
00061 _TREE_ALL = 1
00062 _TREE_WARN = 2
00063 _TREE_ERR = 3
00064
00065 def __init__(self, context, topic):
00066 """
00067 :param context: plugin context hook to enable adding widgets as a
00068 ROS_GUI pane, 'PluginContext'
00069 :param topic: Diagnostic topic to subscribe to 'str'
00070 """
00071
00072 super(RobotMonitorWidget, self).__init__()
00073 rp = rospkg.RosPack()
00074 ui_file = os.path.join(rp.get_path('rqt_robot_monitor'), 'resource',
00075 'robotmonitor_mainwidget.ui')
00076 loadUi(ui_file, self, {'TimelinePane': TimelinePane})
00077
00078
00079 obj_name = 'Robot Monitor'
00080 self.setObjectName(obj_name)
00081 self.setWindowTitle(obj_name)
00082
00083 self._toplevel_statitems = []
00084 self._warn_statusitems = []
00085
00086 self._err_statusitems = []
00087
00088
00089
00090 self.timeline_pane.set_timeline_data(Util.SECONDS_TIMELINE,
00091 self.get_color_for_value,
00092 self.on_pause)
00093
00094 self.tree_all_devices.itemDoubleClicked.connect(self._tree_clicked)
00095 self.warn_flattree.itemDoubleClicked.connect(self._tree_clicked)
00096 self.err_flattree.itemDoubleClicked.connect(self._tree_clicked)
00097
00098 self.tree_all_devices.resizeColumnToContents(0)
00099
00100 self._sig_tree_nodes_updated.connect(self._tree_nodes_updated)
00101
00102 self.vlayout_top.addWidget(self.timeline_pane)
00103 self.timeline_pane.show()
00104
00105 self._paused = False
00106 self._is_stale = None
00107 self._last_message_time = 0.0
00108
00109 self._timer = QTimer()
00110
00111 self._timer.timeout.connect(self._update_message_state)
00112 self._timer.start(1000)
00113
00114 self._sub = rospy.Subscriber(
00115 topic,
00116 DiagnosticArray,
00117 self._cb)
00118
00119 self._sig_new_diagnostic.connect(self.new_diagnostic)
00120 self._original_base_color = self.tree_all_devices.palette().base().color()
00121 self._original_alt_base_color = self.tree_all_devices.palette().alternateBase().color()
00122
00123 def _cb(self, msg):
00124 """
00125 Intended to be called from non-Qt thread,
00126 ie. ROS Subscriber in particular.
00127
00128 :type msg: DiagnosticArray
00129 """
00130
00131
00132
00133 self._sig_new_diagnostic.emit(msg)
00134
00135 def new_diagnostic(self, msg, is_forced=False):
00136 """
00137 Overridden from AbstractStatusWidget.
00138
00139 When monitoring not paused, this public method updates all the
00140 treewidgets contained in this class, and also notifies the StatusItem
00141 instances that are stored in the all-device-tree, which eventually
00142 updates the InspectorWindows in them.
00143
00144 :type msg: DiagnosticArray
00145 :param is_forced: Intended for non-incoming-msg trigger
00146 (in particular, from child object like TimelinePane).
00147 @author: Isaac Saito
00148 """
00149 if not self._paused and not is_forced:
00150 self.timeline_pane.new_diagnostic(msg)
00151 self._update_devices_tree(msg)
00152 self._update_warns_errors(msg)
00153 self._on_new_message_received(msg)
00154
00155 self._notify_statitems(msg)
00156
00157 rospy.logdebug(' RobotMonitorWidget _cb stamp=%s',
00158 msg.header.stamp)
00159 elif is_forced:
00160 self._update_devices_tree(msg)
00161 self._update_warns_errors(msg)
00162
00163 def _notify_statitems(self, diag_arr):
00164 """
00165 Notify new message arrival to all existing InespectorWindow instances
00166 that are encapsulated in StatusItem instances contained in
00167 self._toplevel_statitems.
00168 """
00169
00170 for statitem_new in diag_arr.status:
00171 corresp = Util.get_correspondent(statitem_new.name,
00172 self._toplevel_statitems)
00173 statitem_prev = corresp[Util._DICTKEY_STATITEM]
00174 if statitem_prev and statitem_prev.inspector:
00175 rospy.logdebug(' RobotMonitorWidget _notify_statitems ' +
00176 'name=%s len toplv items=%d',
00177 statitem_new.name, len(self._toplevel_statitems))
00178 return
00179
00180 def resizeEvent(self, evt):
00181 """Overridden from QWidget"""
00182 rospy.logdebug('RobotMonitorWidget resizeEvent')
00183 self.timeline_pane.redraw()
00184
00185 def _tree_clicked(self, item, column):
00186 """
00187 Slot to QTreeWidget.itemDoubleClicked
00188
00189 :type item: QTreeWidgetItem
00190 :type column: int
00191 """
00192 rospy.logdebug('RobotMonitorWidget _tree_clicked col=%d', column)
00193 item.on_click()
00194
00195 def _update_devices_tree(self, diag_array):
00196 """
00197 Update the tree from the bottom
00198
00199 :type diag_array: DiagnosticArray
00200 """
00201
00202
00203
00204
00205 statusnames_curr_toplevel = [Util.get_grn_resource_name(status.name)
00206 for status in self._toplevel_statitems]
00207
00208
00209
00210 for status_new in self._get_toplevel_diagnosticstat(diag_array):
00211 name = Util.get_grn_resource_name(status_new.name)
00212 rospy.logdebug('_update_devices_tree 0 name @ toplevel %s', name)
00213 dict_status = 0
00214 if name in statusnames_curr_toplevel:
00215
00216 statusitem = self._toplevel_statitems[
00217 statusnames_curr_toplevel.index(name)]
00218
00219 dict_status = statusitem.update_children(status_new,
00220 diag_array)
00221 times_errors = dict_status[Util._DICTKEY_TIMES_ERROR]
00222 times_warnings = dict_status[Util._DICTKEY_TIMES_WARN]
00223 Util.update_status_images(status_new, statusitem)
00224
00225
00226 base_text = Util.gen_headline_status_green(statusitem.status)
00227
00228 rospy.logdebug('_update_devices_tree warn_id= %s\n\t\t\t' +
00229 'diagnostic_status.name = %s\n\t\t\t\t' +
00230 'Normal status diag_array = %s',
00231 statusitem.warning_id,
00232 status_new.name, base_text)
00233
00234 if (times_errors > 0 or times_warnings > 0):
00235 base_text = "(Err: %s, Wrn: %s) %s %s" % (
00236 times_errors,
00237 times_warnings,
00238 Util.get_grn_resource_name(status_new.name),
00239 status_new.message)
00240 rospy.logdebug('_update_dev_tree 1 text to show=%s',
00241 base_text)
00242 statusitem.setText(0, base_text)
00243 statusitem.setText(1, status_new.message)
00244 else:
00245 rospy.logdebug('_update_dev_tree 2 text to show=%s',
00246 base_text)
00247 statusitem.setText(0, base_text)
00248 statusitem.setText(1, 'OK')
00249
00250 else:
00251 new_status_item = StatusItem(status_new)
00252
00253 new_status_item.update_children(status_new,
00254 diag_array)
00255
00256
00257
00258
00259 Util.update_status_images(status_new,
00260 new_status_item)
00261
00262 self._toplevel_statitems.append(new_status_item)
00263
00264 rospy.logdebug(' _update_devices_tree 2 ' +
00265 'status_new.name %s',
00266 new_status_item.name)
00267 self.tree_all_devices.addTopLevelItem(new_status_item)
00268
00269 self._sig_tree_nodes_updated.emit(self._TREE_ALL)
00270
00271 def _tree_nodes_updated(self, tree_type):
00272 tree_obj = None
00273 if self._TREE_ALL == tree_type:
00274 tree_obj = self.tree_all_devices
00275 elif self._TREE_WARN == tree_type:
00276 tree_obj = self.warn_flattree
00277 if self._TREE_ERR == tree_type:
00278 tree_obj = self.err_flattree
00279 tree_obj.resizeColumnToContents(0)
00280
00281 def _get_toplevel_diagnosticstat(self, diag_array):
00282 """
00283 Return an array that contains DiagnosticStatus only at the top level of
00284 the given msg.
00285
00286 :type msg: DiagnosticArray
00287 :rtype: DiagnosticStatus[]
00288 """
00289
00290 ret = []
00291 for diagnostic_status in diag_array.status:
00292 if len(diagnostic_status.name.split('/')) == 2:
00293 rospy.logdebug(" _get_toplevel_diagnosticstat " +
00294 "TOP lev %s ", diagnostic_status.name)
00295 ret.append(diagnostic_status)
00296 else:
00297 rospy.logdebug(" _get_toplevel_diagnosticstat " +
00298 "Not top lev %s ", diagnostic_status.name)
00299 return ret
00300
00301 def _update_warns_errors(self, diag_array):
00302 """
00303 Update the warning and error trees.
00304
00305 Unlike _update_devices_tree function where all DiagnosticStatus
00306 need to be inspected constantly, this function is used in a trial
00307 to reduce unnecessary inspection of the status level for all
00308 DiagnosticStatus contained in the incoming DiagnosticArray msg.
00309
00310 :type msg: DiagnosticArray
00311 """
00312
00313 self._update_flat_tree(diag_array)
00314
00315 def pause(self, msg):
00316 """
00317 Do nothing if already being _paused.
00318
00319 :type msg: DiagnosticArray
00320 """
00321
00322 if not self._paused:
00323 self._paused = True
00324 self.new_diagnostic(msg)
00325
00326 def unpause(self, msg=None):
00327 """
00328 :type msg: DiagnosticArray
00329 """
00330
00331 self._paused = False
00332
00333 def on_pause(self, paused, diagnostic_arr):
00334 """
00335 Check if InspectorWindows are set. If they are, pause them.
00336
00337 Pay attention not to confuse with RobotMonitorWidget.pause.
00338
00339 :type paused: bool
00340 :type diagnostic_arr: DiagnosticArray
00341 """
00342
00343 if paused:
00344 self.pause(diagnostic_arr)
00345 elif (len(self._toplevel_statitems) > 0):
00346 diag_array_queue = self.timeline_pane._get_diagnosticarray()
00347 statitems = []
00348 for diag_arr in diag_array_queue:
00349 state_instant = InstantaneousState()
00350 state_instant.update(diag_arr)
00351 statitems.append(state_instant)
00352
00353 for statitem_toplv in self._toplevel_statitems:
00354 if (paused):
00355 statitem_toplv.disable()
00356 else:
00357 statitem_toplv.enable()
00358 for state_instant in statitems:
00359 items = state_instant.get_items()
00360 if statitem_toplv.get_name() in items:
00361 statitem_toplv.update(
00362 items[statitem_toplv.get_name()].status)
00363
00364 def _update_flat_tree(self, diag_arr):
00365 """
00366 Update the given flat tree (ie. tree that doesn't show children nodes -
00367 all of its elements will be shown on top level) with
00368 all the DiagnosticStatus instances contained in the given
00369 DiagnosticArray, regardless of the level of the device in a device
00370 category.
00371
00372 For both warn / error trees, StatusItem instances are newly generated.
00373
00374 :type diag_arr: DiagnosticArray
00375 """
00376
00377 for diag_stat_new in diag_arr.status:
00378
00379
00380
00381 stat_lv_new = diag_stat_new.level
00382 dev_name = diag_stat_new.name
00383 correspondent_warn_curr = Util.get_correspondent(
00384 Util.get_grn_resource_name(dev_name),
00385 self._warn_statusitems)
00386 dev_index_warn_curr = correspondent_warn_curr[Util._DICTKEY_INDEX]
00387 rospy.logdebug(' dev_index_warn_curr=%s dev_name=%s',
00388 dev_index_warn_curr, dev_name)
00389 correspondent_err_curr = Util.get_correspondent(
00390 Util.get_grn_resource_name(dev_name),
00391 self._err_statusitems)
00392 dev_index_err_curr = correspondent_err_curr[Util._DICTKEY_INDEX]
00393 headline = "%s" % diag_stat_new.name
00394 if DiagnosticStatus.OK == stat_lv_new:
00395 if 0 <= dev_index_warn_curr:
00396 rospy.logdebug('dev_index_warn_curr=%s name=%s, ' +
00397 'stat_lv_new=%d', dev_index_warn_curr,
00398 dev_name, stat_lv_new)
00399 statitem_curr = self._get_statitem(dev_index_warn_curr,
00400 self._warn_statusitems,
00401 self.warn_flattree, 1)
00402 statitem_curr.warning_id = None
00403 elif 0 <= dev_index_err_curr:
00404 statitem_curr = self._get_statitem(dev_index_err_curr,
00405 self._err_statusitems,
00406 self.err_flattree, 1)
00407 statitem_curr.error_id = None
00408 elif DiagnosticStatus.WARN == stat_lv_new:
00409 statitem = None
00410 if 0 <= dev_index_err_curr:
00411
00412
00413 statitem = self._get_statitem(dev_index_err_curr,
00414 self._err_statusitems,
00415 self.err_flattree)
00416 self._add_statitem(statitem, self._warn_statusitems,
00417 self.warn_flattree, headline,
00418 diag_stat_new.message, stat_lv_new)
00419 elif (dev_index_warn_curr < 0 and dev_index_err_curr < 0):
00420
00421
00422 statitem = StatusItem(diag_stat_new)
00423 self._add_statitem(statitem, self._warn_statusitems,
00424 self.warn_flattree, headline,
00425 diag_stat_new.message, stat_lv_new)
00426 self._warn_statusitems.append(statitem)
00427 elif (0 < dev_index_warn_curr):
00428
00429
00430 statitem = self._get_statitem(dev_index_warn_curr,
00431 self._warn_statusitems)
00432
00433 if statitem:
00434
00435 statitem.update_children(diag_stat_new,
00436 diag_arr)
00437 elif ((DiagnosticStatus.ERROR == stat_lv_new) or
00438 (DiagnosticStatus.STALE == stat_lv_new)):
00439 statitem = None
00440 if 0 <= dev_index_warn_curr:
00441
00442
00443 statitem = self._get_statitem(dev_index_warn_curr,
00444 self._warn_statusitems,
00445 self.warn_flattree)
00446 self._add_statitem(statitem, self._err_statusitems,
00447 self.err_flattree, headline,
00448 diag_stat_new.message, stat_lv_new)
00449 elif (0 <= dev_index_err_curr):
00450
00451
00452 statitem = self._get_statitem(dev_index_err_curr,
00453 self._err_statusitems)
00454 elif (dev_index_warn_curr < 0 and dev_index_err_curr < 0):
00455
00456
00457 statitem = StatusItem(diag_stat_new)
00458 self._add_statitem(statitem, self._err_statusitems,
00459 self.err_flattree, headline,
00460 diag_stat_new.message, stat_lv_new)
00461
00462 if statitem:
00463
00464 statitem.update_children(diag_stat_new,
00465 diag_arr)
00466
00467 self._sig_tree_nodes_updated.emit(self._TREE_WARN)
00468 self._sig_tree_nodes_updated.emit(self._TREE_ERR)
00469
00470 def _add_statitem(self, statusitem, statitem_list,
00471 tree, headline, statusmsg, statlevel):
00472
00473 if 'Warning' == statusmsg or 'Error' == statusmsg:
00474 return
00475
00476 statusitem.setText(0, headline)
00477 statusitem.setText(1, statusmsg)
00478 statusitem.setIcon(0, Util.IMG_DICT[statlevel])
00479 statitem_list.append(statusitem)
00480 tree.addTopLevelItem(statusitem)
00481 rospy.logdebug(' _add_statitem statitem_list length=%d',
00482 len(statitem_list))
00483
00484 def _get_statitem(self, item_index, item_list, tree=None, mode=2):
00485 """
00486 :param mode: 1 = remove from given list, 2 = w/o removing.
00487 """
00488 statitem_existing = item_list[item_index]
00489 if 1 == mode:
00490 tree.takeTopLevelItem(tree.indexOfTopLevelItem(statitem_existing))
00491 item_list.pop(item_index)
00492 return statitem_existing
00493
00494 def _on_new_message_received(self, msg):
00495 """
00496 Called whenever a new message is received by the timeline.
00497 Different from new_message in that it is called even if the timeline is
00498 _paused, and only when a new message is received, not when the timeline
00499 is scrubbed
00500 """
00501 self._last_message_time = rospy.get_time()
00502
00503 def _update_message_state(self):
00504
00505 current_time = rospy.get_time()
00506 time_diff = current_time - self._last_message_time
00507 rospy.logdebug('_update_message_state time_diff= %s ' +
00508 'self._last_message_time=%s', time_diff,
00509 self._last_message_time)
00510
00511 previous_stale_state = self._is_stale
00512 if (time_diff > 10.0):
00513 self.timeline_pane._msg_label.setText("Last message received " +
00514 "%s seconds ago"
00515 % (int(time_diff)))
00516 self._is_stale = True
00517 else:
00518 seconds_string = "seconds"
00519 if (int(time_diff) == 1):
00520 seconds_string = "second"
00521 self.timeline_pane._msg_label.setText(
00522 "Last message received %s %s ago" % (int(time_diff),
00523 seconds_string))
00524 self._is_stale = False
00525 if previous_stale_state != self._is_stale:
00526 self._update_background_color()
00527
00528 def _update_background_color(self):
00529 p = self.tree_all_devices.palette()
00530 if self._is_stale:
00531 p.setColor(QPalette.Base, Qt.darkGray)
00532 p.setColor(QPalette.AlternateBase, Qt.lightGray)
00533 else:
00534 p.setColor(QPalette.Base, self._original_base_color)
00535 p.setColor(QPalette.AlternateBase, self._original_alt_base_color)
00536 self.tree_all_devices.setPalette(p)
00537 self.warn_flattree.setPalette(p)
00538 self.err_flattree.setPalette(p)
00539
00540 def shutdown(self):
00541 """
00542 This needs to be called whenever this class terminates.
00543 This closes all the instances on all trees.
00544 Also unregisters ROS' subscriber, stops timer.
00545 """
00546
00547 rospy.logdebug('RobotMonitorWidget in shutdown')
00548
00549
00550
00551 for item in self._err_statusitems:
00552 item.close()
00553 for item in self._warn_statusitems:
00554 item.close()
00555 for item in self._toplevel_statitems:
00556 item.close()
00557
00558 self._sub.unregister()
00559
00560 self._timer.stop()
00561 del self._timer
00562
00563 def save_settings(self, plugin_settings, instance_settings):
00564 instance_settings.set_value('splitter', self.splitter.saveState())
00565
00566 def restore_settings(self, plugin_settings, instance_settings):
00567 if instance_settings.contains('splitter'):
00568 self.splitter.restoreState(instance_settings.value('splitter'))
00569 else:
00570 self.splitter.setSizes([100, 100, 200])
00571
00572 def _clear(self):
00573 rospy.logdebug(' RobotMonitorWidget _clear called ')
00574 self.err_flattree.clear()
00575 self.warn_flattree.clear()
00576
00577 def get_color_for_value(self, queue_diagnostic, color_index):
00578 """
00579 Overridden from AbstractStatusWidget.
00580
00581 :type color_index: int
00582 """
00583
00584 len_q = len(queue_diagnostic)
00585 rospy.logdebug(' get_color_for_value color_index=%d len_q=%d',
00586 color_index, len_q)
00587 if (color_index == 1 and len_q == 0):
00588
00589 return QColor('grey')
00590 return Util._get_color_for_message(queue_diagnostic[color_index - 1])
00591
00592