topic_widget.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 # Copyright (c) 2011, Dorian Scholz, TU Darmstadt
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions
00008 # are met:
00009 #
00010 #   * Redistributions of source code must retain the above copyright
00011 #     notice, this list of conditions and the following disclaimer.
00012 #   * Redistributions in binary form must reproduce the above
00013 #     copyright notice, this list of conditions and the following
00014 #     disclaimer in the documentation and/or other materials provided
00015 #     with the distribution.
00016 #   * Neither the name of the TU Darmstadt nor the names of its
00017 #     contributors may be used to endorse or promote products derived
00018 #     from this software without specific prior written permission.
00019 #
00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00024 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00031 # POSSIBILITY OF SUCH DAMAGE.
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         # Whether to get all topics or only the topics that are set in advance.
00086         # Can be also set by the setter method "set_selected_topics".
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         # self.refresh_topics()
00097 
00098         # init and start update timer
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:  # Topics to show are specified.
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                 # The topics that are required (by whoever uses this class).
00134                 topic_specifiers_required = [type for name, type in topic_list]
00135 
00136                 # The required topics that match with published topics.
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             # start new topic dict
00150             new_topics = {}
00151 
00152             for topic_name, topic_type in topic_list:
00153                 # if topic is new or has changed its type
00154                 if topic_name not in self._topics or \
00155                    self._topics[topic_name]['type'] != topic_type:
00156                     # create new TopicInfo
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                     # add it to the dict and tree view
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                     # if topic has been seen before, copy it to new dict and
00170                     # remove it from the old one
00171                     new_topics[topic_name] = self._topics[topic_name]
00172                     del self._topics[topic_name]
00173 
00174             # clean up old topics
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             # switch to new topic dict
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                 # update rate
00192                 rate, _, _, _ = topic_info.get_hz()
00193                 rate_text = '%1.2f' % rate if rate != None else 'unknown'
00194 
00195                 # update bandwidth
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                 # update values
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             # remove obsolete children
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             # show full topic name with preceding namespace on toplevel item
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         # show context menu
00303         menu = QMenu(self)
00304         action_toggle_auto_resize = menu.addAction('Toggle Auto-Resize')
00305         action = menu.exec_(header.mapToGlobal(pos))
00306 
00307         # evaluate user action
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         # show context menu
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         # evaluate user action
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     # TODO(Enhancement) Save/Restore tree expansion state
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)


rqt_topic
Author(s): Dorian Scholz
autogenerated on Wed Sep 16 2015 06:58:23