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