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 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  # Qt5
00085         except AttributeError:
00086             setSectionResizeMode = header.setResizeMode  # Qt4
00087         setSectionResizeMode(QHeaderView.ResizeToContents)
00088         header.customContextMenuRequested.connect(
00089             self.handle_header_view_customContextMenuRequested)
00090         header.setContextMenuPolicy(Qt.CustomContextMenu)
00091 
00092         # Whether to get all topics or only the topics that are set in advance.
00093         # Can be also set by the setter method "set_selected_topics".
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         # self.refresh_topics()
00104 
00105         # init and start update timer
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:  # Topics to show are specified.
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                     # The topics that are required (by whoever uses this class).
00143                     topic_specifiers_required = [type for name, type in topic_list]
00144 
00145                     # The required topics that match with published topics.
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             # start new topic dict
00166             new_topics = {}
00167 
00168             for topic_name, topic_type in topic_list:
00169                 # if topic is new or has changed its type
00170                 if topic_name not in self._topics or \
00171                    self._topics[topic_name]['type'] != topic_type:
00172                     # create new TopicInfo
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                     # add it to the dict and tree view
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                     # if topic has been seen before, copy it to new dict and
00187                     # remove it from the old one
00188                     new_topics[topic_name] = self._topics[topic_name]
00189                     del self._topics[topic_name]
00190 
00191             # clean up old topics
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             # switch to new topic dict
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                 # update rate
00208                 rate, _, _, _ = topic_info.get_hz()
00209                 rate_text = '%1.2f' % rate if rate != None else 'unknown'
00210 
00211                 # update bandwidth
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                 # update values
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             # remove obsolete children
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             # show full topic name with preceding namespace on toplevel item
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         # show context menu
00331         menu = QMenu(self)
00332         action_toggle_auto_resize = menu.addAction('Toggle Auto-Resize')
00333         action = menu.exec_(header.mapToGlobal(pos))
00334 
00335         # evaluate user action
00336         if action is action_toggle_auto_resize:
00337             try:
00338                 sectionResizeMode = header.sectionResizeMode  # Qt5
00339                 setSectionResizeMode = header.setSectionResizeMode  # Qt5
00340             except AttributeError:
00341                 sectionResizeMode = header.resizeMode  # Qt4
00342                 setSectionResizeMode = header.setResizeMode  # Qt4
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         # show context menu
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         # evaluate user action
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     # TODO(Enhancement) Save/Restore tree expansion state
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)


rqt_topic
Author(s): Dorian Scholz
autogenerated on Thu Jun 6 2019 21:50:16