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 from __future__ import division
00034 import os
00035
00036 from python_qt_binding import loadUi
00037 from python_qt_binding.QtCore import Qt, QTimer, Signal, Slot
00038 from python_qt_binding.QtGui import QHeaderView, QIcon, QMenu, QTreeWidgetItem, QWidget
00039 import roslib
00040 import rospkg
00041 import rospy
00042 from rospy.exceptions import ROSException
00043
00044 from .topic_info import TopicInfo
00045
00046
00047 class TopicWidget(QWidget):
00048 """
00049 main class inherits from the ui window class.
00050
00051 You can specify the topics that the topic pane.
00052
00053 TopicWidget.start must be called in order to update topic pane.
00054 """
00055
00056 SELECT_BY_NAME = 0
00057 SELECT_BY_MSGTYPE = 1
00058
00059 _column_names = ['topic', 'type', 'bandwidth', 'rate', 'value']
00060
00061 def __init__(self, plugin=None, selected_topics=None, select_topic_type=SELECT_BY_NAME):
00062 """
00063 @type selected_topics: list of tuples.
00064 @param selected_topics: [($NAME_TOPIC$, $TYPE_TOPIC$), ...]
00065 @type select_topic_type: int
00066 @param select_topic_type: Can specify either the name of topics or by
00067 the type of topic, to filter the topics to
00068 show. If 'select_topic_type' argument is
00069 None, this arg shouldn't be meaningful.
00070 """
00071 super(TopicWidget, self).__init__()
00072
00073 self._select_topic_type = select_topic_type
00074
00075 rp = rospkg.RosPack()
00076 ui_file = os.path.join(rp.get_path('rqt_topic'), 'resource', 'TopicWidget.ui')
00077 loadUi(ui_file, self)
00078 self._plugin = plugin
00079 self.topics_tree_widget.sortByColumn(0, Qt.AscendingOrder)
00080 header = self.topics_tree_widget.header()
00081 header.setResizeMode(QHeaderView.ResizeToContents)
00082 header.customContextMenuRequested.connect(self.handle_header_view_customContextMenuRequested)
00083 header.setContextMenuPolicy(Qt.CustomContextMenu)
00084
00085
00086
00087 self._selected_topics = selected_topics
00088
00089 self._current_topic_list = []
00090 self._topics = {}
00091 self._tree_items = {}
00092 self._column_index = {}
00093 for column_name in self._column_names:
00094 self._column_index[column_name] = len(self._column_index)
00095
00096
00097
00098
00099 self._timer_refresh_topics = QTimer(self)
00100 self._timer_refresh_topics.timeout.connect(self.refresh_topics)
00101
00102 def set_topic_specifier(self, specifier):
00103 self._select_topic_type = specifier
00104
00105 def start(self):
00106 """
00107 This method needs to be called to start updating topic pane.
00108 """
00109 self._timer_refresh_topics.start(1000)
00110
00111 @Slot()
00112 def refresh_topics(self):
00113 """
00114 refresh tree view items
00115 """
00116
00117 if self._selected_topics is None:
00118 topic_list = rospy.get_published_topics()
00119 if topic_list is None:
00120 rospy.logerr('Not even a single published topic found. Check network configuration')
00121 return
00122 else:
00123 topic_list = self._selected_topics
00124 topic_specifiers_server_all = None
00125 topic_specifiers_required = None
00126
00127 rospy.logdebug('refresh_topics) self._selected_topics=%s' % (topic_list,))
00128
00129 if self._select_topic_type == self.SELECT_BY_NAME:
00130 topic_specifiers_server_all = [name for name, type in rospy.get_published_topics()]
00131 topic_specifiers_required = [name for name, type in topic_list]
00132 elif self._select_topic_type == self.SELECT_BY_MSGTYPE:
00133
00134 topic_specifiers_required = [type for name, type in topic_list]
00135
00136
00137 topics_match = [(name, type) for name, type in rospy.get_published_topics() if type in topic_specifiers_required]
00138 topic_list = topics_match
00139 rospy.logdebug('selected & published topic types=%s' % (topic_list,))
00140
00141 rospy.logdebug('server_all=%s\nrequired=%s\ntlist=%s' % (topic_specifiers_server_all, topic_specifiers_required, topic_list))
00142 if len(topic_list) == 0:
00143 rospy.logerr('None of the following required topics are found.\n(NAME, TYPE): %s' % (self._selected_topics,))
00144 return
00145
00146 if self._current_topic_list != topic_list:
00147 self._current_topic_list = topic_list
00148
00149
00150 new_topics = {}
00151
00152 for topic_name, topic_type in topic_list:
00153
00154 if topic_name not in self._topics or \
00155 self._topics[topic_name]['type'] != topic_type:
00156
00157 topic_info = TopicInfo(topic_name, topic_type)
00158 message_instance = None
00159 if topic_info.message_class is not None:
00160 message_instance = topic_info.message_class()
00161
00162 topic_item = self._recursive_create_widget_items(self.topics_tree_widget, topic_name, topic_type, message_instance)
00163 new_topics[topic_name] = {
00164 'item': topic_item,
00165 'info': topic_info,
00166 'type': topic_type,
00167 }
00168 else:
00169
00170
00171 new_topics[topic_name] = self._topics[topic_name]
00172 del self._topics[topic_name]
00173
00174
00175 for topic_name in self._topics.keys():
00176 self._topics[topic_name]['info'].stop_monitoring()
00177 index = self.topics_tree_widget.indexOfTopLevelItem(
00178 self._topics[topic_name]['item'])
00179 self.topics_tree_widget.takeTopLevelItem(index)
00180 del self._topics[topic_name]
00181
00182
00183 self._topics = new_topics
00184
00185 self._update_topics_data()
00186
00187 def _update_topics_data(self):
00188 for topic in self._topics.values():
00189 topic_info = topic['info']
00190 if topic_info.monitoring:
00191
00192 rate, _, _, _ = topic_info.get_hz()
00193 rate_text = '%1.2f' % rate if rate != None else 'unknown'
00194
00195
00196 bytes_per_s, _, _, _ = topic_info.get_bw()
00197 if bytes_per_s is None:
00198 bandwidth_text = 'unknown'
00199 elif bytes_per_s < 1000:
00200 bandwidth_text = '%.2fB/s' % bytes_per_s
00201 elif bytes_per_s < 1000000:
00202 bandwidth_text = '%.2fKB/s' % (bytes_per_s / 1000.)
00203 else:
00204 bandwidth_text = '%.2fMB/s' % (bytes_per_s / 1000000.)
00205
00206
00207 value_text = ''
00208 self.update_value(topic_info._topic_name, topic_info.last_message)
00209
00210 else:
00211 rate_text = ''
00212 bandwidth_text = ''
00213 value_text = 'not monitored' if topic_info.error is None else topic_info.error
00214
00215 self._tree_items[topic_info._topic_name].setText(self._column_index['rate'], rate_text)
00216 self._tree_items[topic_info._topic_name].setText(self._column_index['bandwidth'], bandwidth_text)
00217 self._tree_items[topic_info._topic_name].setText(self._column_index['value'], value_text)
00218
00219 def update_value(self, topic_name, message):
00220 if hasattr(message, '__slots__') and hasattr(message, '_slot_types'):
00221 for slot_name in message.__slots__:
00222 self.update_value(topic_name + '/' + slot_name, getattr(message, slot_name))
00223
00224 elif type(message) in (list, tuple) and (len(message) > 0) and hasattr(message[0], '__slots__'):
00225
00226 for index, slot in enumerate(message):
00227 if topic_name + '[%d]' % index in self._tree_items:
00228 self.update_value(topic_name + '[%d]' % index, slot)
00229 else:
00230 base_type_str, _ = self._extract_array_info(self._tree_items[topic_name].text(self._column_index['type']))
00231 self._recursive_create_widget_items(self._tree_items[topic_name], topic_name + '[%d]' % index, base_type_str, slot)
00232
00233 if len(message) < self._tree_items[topic_name].childCount():
00234 for i in range(len(message), self._tree_items[topic_name].childCount()):
00235 item_topic_name = topic_name + '[%d]' % i
00236 self._recursive_delete_widget_items(self._tree_items[item_topic_name])
00237 else:
00238 if topic_name in self._tree_items:
00239 self._tree_items[topic_name].setText(self._column_index['value'], repr(message))
00240
00241 def _extract_array_info(self, type_str):
00242 array_size = None
00243 if '[' in type_str and type_str[-1] == ']':
00244 type_str, array_size_str = type_str.split('[', 1)
00245 array_size_str = array_size_str[:-1]
00246 if len(array_size_str) > 0:
00247 array_size = int(array_size_str)
00248 else:
00249 array_size = 0
00250
00251 return type_str, array_size
00252
00253 def _recursive_create_widget_items(self, parent, topic_name, type_name, message):
00254 if parent is self.topics_tree_widget:
00255
00256 topic_text = topic_name
00257 item = TreeWidgetItem(self._toggle_monitoring, topic_name, parent)
00258 else:
00259 topic_text = topic_name.split('/')[-1]
00260 if '[' in topic_text:
00261 topic_text = topic_text[topic_text.index('['):]
00262 item = QTreeWidgetItem(parent)
00263 item.setText(self._column_index['topic'], topic_text)
00264 item.setText(self._column_index['type'], type_name)
00265 item.setData(0, Qt.UserRole, topic_name)
00266 self._tree_items[topic_name] = item
00267 if hasattr(message, '__slots__') and hasattr(message, '_slot_types'):
00268 for slot_name, type_name in zip(message.__slots__, message._slot_types):
00269 self._recursive_create_widget_items(item, topic_name + '/' + slot_name, type_name, getattr(message, slot_name))
00270
00271 else:
00272 base_type_str, array_size = self._extract_array_info(type_name)
00273 try:
00274 base_instance = roslib.message.get_message_class(base_type_str)()
00275 except (ValueError, TypeError):
00276 base_instance = None
00277 if array_size is not None and hasattr(base_instance, '__slots__'):
00278 for index in range(array_size):
00279 self._recursive_create_widget_items(item, topic_name + '[%d]' % index, base_type_str, base_instance)
00280 return item
00281
00282 def _toggle_monitoring(self, topic_name):
00283 item = self._tree_items[topic_name]
00284 if item.checkState(0):
00285 self._topics[topic_name]['info'].start_monitoring()
00286 else:
00287 self._topics[topic_name]['info'].stop_monitoring()
00288
00289 def _recursive_delete_widget_items(self, item):
00290 def _recursive_remove_items_from_tree(item):
00291 for index in reversed(range(item.childCount())):
00292 _recursive_remove_items_from_tree(item.child(index))
00293 topic_name = item.data(0, Qt.UserRole)
00294 del self._tree_items[topic_name]
00295 _recursive_remove_items_from_tree(item)
00296 item.parent().removeChild(item)
00297
00298 @Slot('QPoint')
00299 def handle_header_view_customContextMenuRequested(self, pos):
00300 header = self.topics_tree_widget.header()
00301
00302
00303 menu = QMenu(self)
00304 action_toggle_auto_resize = menu.addAction('Toggle Auto-Resize')
00305 action = menu.exec_(header.mapToGlobal(pos))
00306
00307
00308 if action is action_toggle_auto_resize:
00309 if header.resizeMode(0) == QHeaderView.ResizeToContents:
00310 header.setResizeMode(QHeaderView.Interactive)
00311 else:
00312 header.setResizeMode(QHeaderView.ResizeToContents)
00313
00314 @Slot('QPoint')
00315 def on_topics_tree_widget_customContextMenuRequested(self, pos):
00316 item = self.topics_tree_widget.itemAt(pos)
00317 if item is None:
00318 return
00319
00320
00321 menu = QMenu(self)
00322 action_item_expand = menu.addAction(QIcon.fromTheme('zoom-in'), 'Expand All Children')
00323 action_item_collapse = menu.addAction(QIcon.fromTheme('zoom-out'), 'Collapse All Children')
00324 action = menu.exec_(self.topics_tree_widget.mapToGlobal(pos))
00325
00326
00327 if action in (action_item_expand, action_item_collapse):
00328 expanded = (action is action_item_expand)
00329
00330 def recursive_set_expanded(item):
00331 item.setExpanded(expanded)
00332 for index in range(item.childCount()):
00333 recursive_set_expanded(item.child(index))
00334 recursive_set_expanded(item)
00335
00336 def shutdown_plugin(self):
00337 for topic in self._topics.values():
00338 topic['info'].stop_monitoring()
00339 self._timer_refresh_topics.stop()
00340
00341 def set_selected_topics(self, selected_topics):
00342 """
00343 @param selected_topics: list of tuple. [(topic_name, topic_type)]
00344 @type selected_topics: []
00345 """
00346 rospy.logdebug('set_selected_topics topics={}'.format(
00347 len(selected_topics)))
00348 self._selected_topics = selected_topics
00349
00350
00351 def save_settings(self, plugin_settings, instance_settings):
00352 header_state = self.topics_tree_widget.header().saveState()
00353 instance_settings.set_value('tree_widget_header_state', header_state)
00354
00355 def restore_settings(self, pluggin_settings, instance_settings):
00356 if instance_settings.contains('tree_widget_header_state'):
00357 header_state = instance_settings.value('tree_widget_header_state')
00358 if not self.topics_tree_widget.header().restoreState(header_state):
00359 rospy.logwarn("rqt_topic: Failed to restore header state.")
00360
00361 class TreeWidgetItem(QTreeWidgetItem):
00362
00363 def __init__(self, check_state_changed_callback, topic_name, parent=None):
00364 super(TreeWidgetItem, self).__init__(parent)
00365 self._check_state_changed_callback = check_state_changed_callback
00366 self._topic_name = topic_name
00367 self.setCheckState(0, Qt.Unchecked)
00368
00369 def setData(self, column, role, value):
00370 if role == Qt.CheckStateRole:
00371 state = self.checkState(column)
00372 super(TreeWidgetItem, self).setData(column, role, value)
00373 if role == Qt.CheckStateRole and state != self.checkState(column):
00374 self._check_state_changed_callback(self._topic_name)