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, Slot
00041 from python_qt_binding.QtGui import QPalette
00042 from python_qt_binding.QtWidgets import QWidget, QTreeWidgetItem
00043 import rospy
00044
00045 from rqt_robot_monitor.inspector_window import InspectorWindow
00046 from rqt_robot_monitor.status_item import StatusItem
00047 from rqt_robot_monitor.timeline_pane import TimelinePane
00048 from rqt_robot_monitor.timeline import Timeline
00049 import rqt_robot_monitor.util_robot_monitor as util
00050
00051
00052 class RobotMonitorWidget(QWidget):
00053 """
00054 NOTE: RobotMonitorWidget.shutdown function needs to be called
00055 when the instance of this class terminates.
00056
00057 RobotMonitorWidget itself doesn't store previous diagnostic states.
00058 It instead delegates that function to Timeline class.
00059 """
00060
00061 _TREE_ALL = 1
00062 _TREE_WARN = 2
00063 _TREE_ERR = 3
00064
00065 _message_updated = Signal(dict)
00066 _queue_updated = Signal()
00067
00068 def __init__(self, context, topic=None):
00069 """
00070 :param context: plugin context hook to enable adding widgets as a
00071 ROS_GUI pane, 'PluginContext'
00072 :param topic: Diagnostic topic to subscribe to 'str'
00073 """
00074
00075 super(RobotMonitorWidget, self).__init__()
00076 rp = rospkg.RosPack()
00077 ui_file = os.path.join(rp.get_path('rqt_robot_monitor'), 'resource',
00078 'robotmonitor_mainwidget.ui')
00079 loadUi(ui_file, self)
00080
00081 obj_name = 'Robot Monitor'
00082 self.setObjectName(obj_name)
00083 self.setWindowTitle(obj_name)
00084
00085 self._message_updated_processing = False
00086 self._queue_updated_processing = False
00087
00088
00089
00090 if topic:
00091
00092 self._timeline = Timeline(topic, DiagnosticArray)
00093 self._timeline.message_updated.connect(
00094 self.message_updated, Qt.DirectConnection)
00095 self._timeline.queue_updated.connect(
00096 self.queue_updated, Qt.DirectConnection)
00097 self._message_updated.connect(
00098 self._signal_message_updated, Qt.QueuedConnection)
00099 self._queue_updated.connect(
00100 self._signal_queue_updated, Qt.QueuedConnection)
00101
00102
00103 self._timeline_pane = TimelinePane(self, self._timeline.paused)
00104 self._timeline_pane.pause_changed.connect(self._timeline.set_paused)
00105 self._timeline_pane.position_changed.connect(self._timeline.set_position)
00106 self._timeline.pause_changed.connect(self._timeline_pane.set_paused)
00107 self._timeline.position_changed.connect(self._timeline_pane.set_position)
00108
00109 self.vlayout_top.addWidget(self._timeline_pane)
00110 self._timeline_pane.show()
00111 else:
00112 self._timeline = None
00113 self._timeline_pane = None
00114
00115 self._inspectors = {}
00116
00117 self.tree_all_devices.itemDoubleClicked.connect(self._tree_clicked)
00118 self.warn_flattree.itemDoubleClicked.connect(self._tree_clicked)
00119 self.err_flattree.itemDoubleClicked.connect(self._tree_clicked)
00120
00121
00122 self._is_stale = False
00123
00124 self._timer = QTimer()
00125 self._timer.timeout.connect(self._update_message_state)
00126 self._timer.start(1000)
00127
00128 palette = self.tree_all_devices.palette()
00129 self._original_base_color = palette.base().color()
00130 self._original_alt_base_color = palette.alternateBase().color()
00131
00132 self._tree = StatusItem(self.tree_all_devices.invisibleRootItem())
00133 self._warn_tree = StatusItem(self.warn_flattree.invisibleRootItem())
00134 self._err_tree = StatusItem(self.err_flattree.invisibleRootItem())
00135
00136 @Slot(dict)
00137 def message_updated(self, status):
00138 '''
00139 This method just calls _signal_message_updated in 'best effort' manner.
00140 This method should be called by signal with DirectConnection.
00141 '''
00142 if self._message_updated_processing:
00143 return
00144 self._message_updated_processing = True
00145 self._message_updated.emit(status)
00146
00147 @Slot(dict)
00148 def _signal_message_updated(self, status):
00149 """ DiagnosticArray message callback """
00150
00151
00152 for name, status in status.items():
00153
00154 path = name.split('/')
00155 if path[0] == '':
00156 path = path[1:]
00157 tmp_tree = self._tree
00158 for p in path:
00159 tmp_tree = tmp_tree[p]
00160 tmp_tree.update(status, util.get_resource_name(name))
00161
00162
00163 if status.level == DiagnosticStatus.WARN:
00164 self._warn_tree[name].update(status, name)
00165
00166
00167 if (status.level == DiagnosticStatus.ERROR or
00168 status.level == DiagnosticStatus.STALE):
00169 self._err_tree[name].update(status, name)
00170
00171
00172 self._tree.prune()
00173 self._warn_tree.prune()
00174 self._err_tree.prune()
00175
00176
00177
00178
00179
00180
00181
00182
00183
00184 self.tree_all_devices.resizeColumnToContents(0)
00185 self.warn_flattree.resizeColumnToContents(0)
00186 self.err_flattree.resizeColumnToContents(0)
00187
00188 self._message_updated_processing = False
00189
00190 @Slot()
00191 def queue_updated(self):
00192 '''
00193 This method just calls _signal_queue_updated in 'best effort' manner.
00194 This method should be called by signal with DirectConnection.
00195 '''
00196 if self._queue_updated_processing:
00197 return
00198 self._queue_updated_processing = True
00199 self._queue_updated.emit()
00200
00201 @Slot()
00202 def _signal_queue_updated(self):
00203
00204
00205 levels = [max([s.level for s in s.values()]) for s in self._timeline]
00206 self._timeline_pane.set_levels(levels)
00207 self._timeline_pane.redraw.emit()
00208 self._queue_updated_processing = False
00209
00210 def resizeEvent(self, evt):
00211 """Overridden from QWidget"""
00212 rospy.logdebug('RobotMonitorWidget resizeEvent')
00213 if self._timeline_pane:
00214 self._timeline_pane.redraw.emit()
00215
00216 @Slot(str)
00217 def _inspector_closed(self, name):
00218 """ Called when an inspector window is closed """
00219 if name in self._inspectors:
00220 del self._inspectors[name]
00221
00222 @Slot(QTreeWidgetItem, int)
00223 def _tree_clicked(self, item, column):
00224 """
00225 Slot to QTreeWidget.itemDoubleClicked
00226
00227 :type item: QTreeWidgetItem
00228 :type column: int
00229 """
00230 rospy.logdebug('RobotMonitorWidget _tree_clicked col=%d', column)
00231
00232 if item.name in self._inspectors:
00233 self._inspectors[item.name].activateWindow()
00234 else:
00235 insp = InspectorWindow(None, item.name, self._timeline)
00236 insp.show()
00237 insp.closed.connect(self._inspector_closed)
00238 self._inspectors[item.name] = insp
00239
00240 def _update_message_state(self):
00241 """ Update the display if it's stale """
00242 if self._timeline is not None:
00243 if self._timeline.has_messages:
00244 previous_stale_state = self._is_stale
00245 self._is_stale = self._timeline.is_stale
00246
00247 time_diff = int(self._timeline.data_age)
00248
00249 msg_template = "Last message received %s %s ago"
00250 if time_diff == 1:
00251 msg = msg_template % (time_diff, "second")
00252 else:
00253 msg = msg_template % (time_diff, "seconds")
00254 self._timeline_pane._msg_label.setText(msg)
00255 if previous_stale_state != self._is_stale:
00256 self._update_background_color()
00257 else:
00258
00259 self._timeline_pane._msg_label.setText("No messages received")
00260
00261 def _update_background_color(self):
00262 """ Update the background color based on staleness """
00263 p = self.tree_all_devices.palette()
00264 if self._is_stale:
00265 p.setColor(QPalette.Base, Qt.darkGray)
00266 p.setColor(QPalette.AlternateBase, Qt.lightGray)
00267 else:
00268 p.setColor(QPalette.Base, self._original_base_color)
00269 p.setColor(QPalette.AlternateBase, self._original_alt_base_color)
00270 self.tree_all_devices.setPalette(p)
00271 self.warn_flattree.setPalette(p)
00272 self.err_flattree.setPalette(p)
00273
00274 def shutdown(self):
00275 """
00276 This needs to be called whenever this class terminates.
00277 This closes all the instances on all trees.
00278 Also unregisters ROS' subscriber, stops timer.
00279 """
00280 rospy.logdebug('RobotMonitorWidget in shutdown')
00281
00282 names = self._inspectors.keys()
00283 for name in names:
00284 self._inspectors[name].close()
00285
00286 if self._timeline:
00287 self._timeline.shutdown()
00288
00289 self._timer.stop()
00290 del self._timer
00291
00292 def save_settings(self, plugin_settings, instance_settings):
00293 instance_settings.set_value('splitter', self.splitter.saveState())
00294
00295
00296
00297 def restore_settings(self, plugin_settings, instance_settings):
00298 if instance_settings.contains('splitter'):
00299 self.splitter.restoreState(instance_settings.value('splitter'))
00300 else:
00301 self.splitter.setSizes([100, 100, 200])
00302