topic_widget.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 # Copyright (c) 2011, Dorian Scholz, TU Darmstadt
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 # * Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials provided
15 # with the distribution.
16 # * Neither the name of the TU Darmstadt nor the names of its
17 # contributors may be used to endorse or promote products derived
18 # from this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 # POSSIBILITY OF SUCH DAMAGE.
32 
33 from __future__ import division
34 import os
35 
36 from python_qt_binding import loadUi
37 from python_qt_binding.QtCore import Qt, QTimer, Signal, Slot
38 from python_qt_binding.QtGui import QIcon
39 from python_qt_binding.QtWidgets import QHeaderView, QMenu, QTreeWidgetItem, QWidget
40 import roslib
41 import rospkg
42 import rospy
43 from rospy.exceptions import ROSException
44 
45 from .topic_info import TopicInfo
46 
47 
48 class TopicWidget(QWidget):
49 
50  """
51  main class inherits from the ui window class.
52 
53  You can specify the topics that the topic pane.
54 
55  TopicWidget.start must be called in order to update topic pane.
56  """
57 
58  SELECT_BY_NAME = 0
59  SELECT_BY_MSGTYPE = 1
60 
61  _column_names = ['topic', 'type', 'bandwidth', 'rate', 'value']
62 
63  def __init__(self, plugin=None, selected_topics=None, select_topic_type=SELECT_BY_NAME):
64  """
65  @type selected_topics: list of tuples.
66  @param selected_topics: [($NAME_TOPIC$, $TYPE_TOPIC$), ...]
67  @type select_topic_type: int
68  @param select_topic_type: Can specify either the name of topics or by
69  the type of topic, to filter the topics to
70  show. If 'select_topic_type' argument is
71  None, this arg shouldn't be meaningful.
72  """
73  super(TopicWidget, self).__init__()
74 
75  self._select_topic_type = select_topic_type
76 
77  rp = rospkg.RosPack()
78  ui_file = os.path.join(rp.get_path('rqt_topic'), 'resource', 'TopicWidget.ui')
79  loadUi(ui_file, self)
80  self._plugin = plugin
81  self.topics_tree_widget.sortByColumn(0, Qt.AscendingOrder)
82  header = self.topics_tree_widget.header()
83  try:
84  setSectionResizeMode = header.setSectionResizeMode # Qt5
85  except AttributeError:
86  setSectionResizeMode = header.setResizeMode # Qt4
87  setSectionResizeMode(QHeaderView.ResizeToContents)
88  header.customContextMenuRequested.connect(
90  header.setContextMenuPolicy(Qt.CustomContextMenu)
91 
92  # Whether to get all topics or only the topics that are set in advance.
93  # Can be also set by the setter method "set_selected_topics".
94  self._selected_topics = selected_topics
95 
97  self._topics = {}
98  self._tree_items = {}
99  self._column_index = {}
100  for column_name in self._column_names:
101  self._column_index[column_name] = len(self._column_index)
102 
103  # self.refresh_topics()
104 
105  # init and start update timer
106  self._timer_refresh_topics = QTimer(self)
107  self._timer_refresh_topics.timeout.connect(self.refresh_topics)
108 
109  def set_topic_specifier(self, specifier):
110  self._select_topic_type = specifier
111 
112  def start(self):
113  """
114  This method needs to be called to start updating topic pane.
115  """
116  self._timer_refresh_topics.start(1000)
117 
118  @Slot()
119  def refresh_topics(self):
120  """
121  refresh tree view items
122  """
123  try:
124  if self._selected_topics is None:
125  topic_list = rospy.get_published_topics()
126  if topic_list is None:
127  rospy.logerr(
128  'Not even a single published topic found. Check network configuration')
129  return
130  else: # Topics to show are specified.
131  topic_list = self._selected_topics
132  topic_specifiers_server_all = None
133  topic_specifiers_required = None
134 
135  rospy.logdebug('refresh_topics) self._selected_topics=%s' % (topic_list,))
136 
137  if self._select_topic_type == self.SELECT_BY_NAME:
138  topic_specifiers_server_all = \
139  [name for name, type in rospy.get_published_topics()]
140  topic_specifiers_required = [name for name, type in topic_list]
141  elif self._select_topic_type == self.SELECT_BY_MSGTYPE:
142  # The topics that are required (by whoever uses this class).
143  topic_specifiers_required = [type for name, type in topic_list]
144 
145  # The required topics that match with published topics.
146  topics_match = [(name, type) for name, type in rospy.get_published_topics()
147  if type in topic_specifiers_required]
148  topic_list = topics_match
149  rospy.logdebug('selected & published topic types=%s' % (topic_list,))
150 
151  rospy.logdebug('server_all=%s\nrequired=%s\ntlist=%s' %
152  (topic_specifiers_server_all, topic_specifiers_required, topic_list))
153  if len(topic_list) == 0:
154  rospy.logerr(
155  'None of the following required topics are found.\n(NAME, TYPE): %s' %
156  (self._selected_topics,))
157  return
158  except IOError as e:
159  rospy.logerr("Communication with rosmaster failed: {0}".format(e.strerror))
160  return
161 
162  if self._current_topic_list != topic_list:
163  self._current_topic_list = topic_list
164 
165  # start new topic dict
166  new_topics = {}
167 
168  for topic_name, topic_type in topic_list:
169  # if topic is new or has changed its type
170  if topic_name not in self._topics or \
171  self._topics[topic_name]['type'] != topic_type:
172  # create new TopicInfo
173  topic_info = TopicInfo(topic_name, topic_type)
174  message_instance = None
175  if topic_info.message_class is not None:
176  message_instance = topic_info.message_class()
177  # add it to the dict and tree view
178  topic_item = self._recursive_create_widget_items(
179  self.topics_tree_widget, topic_name, topic_type, message_instance)
180  new_topics[topic_name] = {
181  'item': topic_item,
182  'info': topic_info,
183  'type': topic_type,
184  }
185  else:
186  # if topic has been seen before, copy it to new dict and
187  # remove it from the old one
188  new_topics[topic_name] = self._topics[topic_name]
189  del self._topics[topic_name]
190 
191  # clean up old topics
192  for topic_name in self._topics.keys():
193  self._topics[topic_name]['info'].stop_monitoring()
194  index = self.topics_tree_widget.indexOfTopLevelItem(
195  self._topics[topic_name]['item'])
196  self.topics_tree_widget.takeTopLevelItem(index)
197 
198  # switch to new topic dict
199  self._topics = new_topics
200 
201  self._update_topics_data()
202 
204  for topic in self._topics.values():
205  topic_info = topic['info']
206  if topic_info.monitoring:
207  # update rate
208  rate, _, _, _ = topic_info.get_hz()
209  rate_text = '%1.2f' % rate if rate != None else 'unknown'
210 
211  # update bandwidth
212  bytes_per_s, _, _, _ = topic_info.get_bw()
213  if bytes_per_s is None:
214  bandwidth_text = 'unknown'
215  elif bytes_per_s < 1000:
216  bandwidth_text = '%.2fB/s' % bytes_per_s
217  elif bytes_per_s < 1000000:
218  bandwidth_text = '%.2fKB/s' % (bytes_per_s / 1000.)
219  else:
220  bandwidth_text = '%.2fMB/s' % (bytes_per_s / 1000000.)
221 
222  # update values
223  value_text = ''
224  self.update_value(topic_info._topic_name, topic_info.last_message)
225 
226  else:
227  rate_text = ''
228  bytes_per_s = None
229  bandwidth_text = ''
230  value_text = 'not monitored' if topic_info.error is None else topic_info.error
231 
232  self._tree_items[topic_info._topic_name].setText(self._column_index['rate'], rate_text)
233  self._tree_items[topic_info._topic_name].setData(
234  self._column_index['bandwidth'], Qt.UserRole, bytes_per_s)
235  self._tree_items[topic_info._topic_name].setText(
236  self._column_index['bandwidth'], bandwidth_text)
237  self._tree_items[topic_info._topic_name].setText(
238  self._column_index['value'], value_text)
239 
240  def update_value(self, topic_name, message):
241  if hasattr(message, '__slots__') and hasattr(message, '_slot_types'):
242  for slot_name in message.__slots__:
243  self.update_value(topic_name + '/' + slot_name, getattr(message, slot_name))
244 
245  elif type(message) in (list, tuple) and \
246  (len(message) > 0) and \
247  hasattr(message[0], '__slots__'):
248 
249  for index, slot in enumerate(message):
250  if topic_name + '[%d]' % index in self._tree_items:
251  self.update_value(topic_name + '[%d]' % index, slot)
252  else:
253  base_type_str, _ = self._extract_array_info(
254  self._tree_items[topic_name].text(self._column_index['type']))
256  self._tree_items[topic_name],
257  topic_name + '[%d]' % index, base_type_str, slot)
258  # remove obsolete children
259  if len(message) < self._tree_items[topic_name].childCount():
260  for i in range(len(message), self._tree_items[topic_name].childCount()):
261  item_topic_name = topic_name + '[%d]' % i
262  self._recursive_delete_widget_items(self._tree_items[item_topic_name])
263  else:
264  if topic_name in self._tree_items:
265  self._tree_items[topic_name].setText(self._column_index['value'], repr(message))
266 
267  def _extract_array_info(self, type_str):
268  array_size = None
269  if '[' in type_str and type_str[-1] == ']':
270  type_str, array_size_str = type_str.split('[', 1)
271  array_size_str = array_size_str[:-1]
272  if len(array_size_str) > 0:
273  array_size = int(array_size_str)
274  else:
275  array_size = 0
276 
277  return type_str, array_size
278 
279  def _recursive_create_widget_items(self, parent, topic_name, type_name, message):
280  if parent is self.topics_tree_widget:
281  # show full topic name with preceding namespace on toplevel item
282  topic_text = topic_name
283  item = TreeWidgetItem(self._toggle_monitoring, topic_name, parent)
284  else:
285  topic_text = topic_name.split('/')[-1]
286  if '[' in topic_text:
287  topic_text = topic_text[topic_text.index('['):]
288  item = QTreeWidgetItem(parent)
289  item.setText(self._column_index['topic'], topic_text)
290  item.setText(self._column_index['type'], type_name)
291  item.setData(0, Qt.UserRole, topic_name)
292  self._tree_items[topic_name] = item
293  if hasattr(message, '__slots__') and hasattr(message, '_slot_types'):
294  for slot_name, type_name in zip(message.__slots__, message._slot_types):
296  item, topic_name + '/' + slot_name, type_name, getattr(message, slot_name))
297 
298  else:
299  base_type_str, array_size = self._extract_array_info(type_name)
300  try:
301  base_instance = roslib.message.get_message_class(base_type_str)()
302  except (ValueError, TypeError):
303  base_instance = None
304  if array_size is not None and hasattr(base_instance, '__slots__'):
305  for index in range(array_size):
307  item, topic_name + '[%d]' % index, base_type_str, base_instance)
308  return item
309 
310  def _toggle_monitoring(self, topic_name):
311  item = self._tree_items[topic_name]
312  if item.checkState(0):
313  self._topics[topic_name]['info'].start_monitoring()
314  else:
315  self._topics[topic_name]['info'].stop_monitoring()
316 
318  def _recursive_remove_items_from_tree(item):
319  for index in reversed(range(item.childCount())):
320  _recursive_remove_items_from_tree(item.child(index))
321  topic_name = item.data(0, Qt.UserRole)
322  del self._tree_items[topic_name]
323  _recursive_remove_items_from_tree(item)
324  item.parent().removeChild(item)
325 
326  @Slot('QPoint')
328  header = self.topics_tree_widget.header()
329 
330  # show context menu
331  menu = QMenu(self)
332  action_toggle_auto_resize = menu.addAction('Toggle Auto-Resize')
333  action = menu.exec_(header.mapToGlobal(pos))
334 
335  # evaluate user action
336  if action is action_toggle_auto_resize:
337  try:
338  sectionResizeMode = header.sectionResizeMode # Qt5
339  setSectionResizeMode = header.setSectionResizeMode # Qt5
340  except AttributeError:
341  sectionResizeMode = header.resizeMode # Qt4
342  setSectionResizeMode = header.setResizeMode # Qt4
343  if sectionResizeMode(0) == QHeaderView.ResizeToContents:
344  setSectionResizeMode(QHeaderView.Interactive)
345  else:
346  setSectionResizeMode(QHeaderView.ResizeToContents)
347 
348  @Slot('QPoint')
350  item = self.topics_tree_widget.itemAt(pos)
351  if item is None:
352  return
353 
354  # show context menu
355  menu = QMenu(self)
356  action_item_expand = menu.addAction(QIcon.fromTheme('zoom-in'), 'Expand All Children')
357  action_item_collapse = menu.addAction(QIcon.fromTheme('zoom-out'), 'Collapse All Children')
358  action = menu.exec_(self.topics_tree_widget.mapToGlobal(pos))
359 
360  # evaluate user action
361  if action in (action_item_expand, action_item_collapse):
362  expanded = (action is action_item_expand)
363 
364  def recursive_set_expanded(item):
365  item.setExpanded(expanded)
366  for index in range(item.childCount()):
367  recursive_set_expanded(item.child(index))
368  recursive_set_expanded(item)
369 
370  def shutdown_plugin(self):
371  for topic in self._topics.values():
372  topic['info'].stop_monitoring()
373  self._timer_refresh_topics.stop()
374 
375  def set_selected_topics(self, selected_topics):
376  """
377  @param selected_topics: list of tuple. [(topic_name, topic_type)]
378  @type selected_topics: []
379  """
380  rospy.logdebug('set_selected_topics topics={}'.format(len(selected_topics)))
381  self._selected_topics = selected_topics
382 
383  # TODO(Enhancement) Save/Restore tree expansion state
384  def save_settings(self, plugin_settings, instance_settings):
385  header_state = self.topics_tree_widget.header().saveState()
386  instance_settings.set_value('tree_widget_header_state', header_state)
387 
388  def restore_settings(self, pluggin_settings, instance_settings):
389  if instance_settings.contains('tree_widget_header_state'):
390  header_state = instance_settings.value('tree_widget_header_state')
391  if not self.topics_tree_widget.header().restoreState(header_state):
392  rospy.logwarn("rqt_topic: Failed to restore header state.")
393 
394 
395 class TreeWidgetItem(QTreeWidgetItem):
396 
397  def __init__(self, check_state_changed_callback, topic_name, parent=None):
398  super(TreeWidgetItem, self).__init__(parent)
399  self._check_state_changed_callback = check_state_changed_callback
400  self._topic_name = topic_name
401  self.setCheckState(0, Qt.Unchecked)
402 
403  def setData(self, column, role, value):
404  if role == Qt.CheckStateRole:
405  state = self.checkState(column)
406  super(TreeWidgetItem, self).setData(column, role, value)
407  if role == Qt.CheckStateRole and state != self.checkState(column):
409 
410  def __lt__(self, other_item):
411  column = self.treeWidget().sortColumn()
412  if column == TopicWidget._column_names.index('bandwidth'):
413  return self.data(column, Qt.UserRole) < other_item.data(column, Qt.UserRole)
414  return super(TreeWidgetItem, self).__lt__(other_item)
def handle_header_view_customContextMenuRequested(self, pos)
def _recursive_create_widget_items(self, parent, topic_name, type_name, message)
def _toggle_monitoring(self, topic_name)
def update_value(self, topic_name, message)
def save_settings(self, plugin_settings, instance_settings)
def __init__(self, plugin=None, selected_topics=None, select_topic_type=SELECT_BY_NAME)
Definition: topic_widget.py:63
def on_topics_tree_widget_customContextMenuRequested(self, pos)
def _recursive_delete_widget_items(self, item)
def restore_settings(self, pluggin_settings, instance_settings)
def setData(self, column, role, value)
def _extract_array_info(self, type_str)
def set_topic_specifier(self, specifier)
def __init__(self, check_state_changed_callback, topic_name, parent=None)
def set_selected_topics(self, selected_topics)


rqt_topic
Author(s): Dirk Thomas, Dorian Scholz
autogenerated on Sat Mar 20 2021 02:41:10