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, QSlider, QAbstractItemView
40 
41 
42 import roslib
43 import rospkg
44 import rospy
45 from rospy.exceptions import ROSException
46 
47 from .topic_info import TopicInfo
48 
49 import numpy as np
50 
51 
52 class TopicWidget(QWidget):
53  """
54  main class inherits from the ui window class.
55 
56  You can specify the topics that the topic pane.
57 
58  TopicWidget.start must be called in order to update topic pane.
59  """
60 
61  SELECT_BY_NAME = 0
62  SELECT_BY_MSGTYPE = 1
63 
64  # _column_names = ['topic', 'type', 'bandwidth', 'rate', 'value', 'checkbox']
65  _column_names = ['topic', 'type', 'value', 'checkbox']
66 
67  selectionChanged = Signal(list, name='selectionChanged')
68  topicsRefreshed = Signal(name='topicsRefreshed')
69 
70 
71  def __init__(self, plugin=None, selected_topics=None, select_topic_type=SELECT_BY_NAME):
72  """
73  @type selected_topics: list of tuples.
74  @param selected_topics: [($NAME_TOPIC$, $TYPE_TOPIC$), ...]
75  @type select_topic_type: int
76  @param select_topic_type: Can specify either the name of topics or by
77  the type of topic, to filter the topics to
78  show. If 'select_topic_type' argument is
79  None, this arg shouldn't be meaningful.
80  """
81  super(TopicWidget, self).__init__()
82 
83  self._select_topic_type = select_topic_type
84 
85  rp = rospkg.RosPack()
86  ui_file = os.path.join(rp.get_path('rqt_dyn_tune'), 'resource', 'TopicWidget.ui')
87  loadUi(ui_file, self)
88  self._plugin = plugin
89  self.topics_tree_widget.sortByColumn(0, Qt.AscendingOrder)
90  header = self.topics_tree_widget.header()
91  try:
92  setSectionResizeMode = header.setSectionResizeMode # Qt5
93  except AttributeError:
94  setSectionResizeMode = header.setResizeMode # Qt4
95  for i in range(len(self._column_names)):
96  setSectionResizeMode(i, QHeaderView.Stretch)
97 
98  header.setStretchLastSection(False)
99  setSectionResizeMode(0, QHeaderView.Stretch)
100  setSectionResizeMode(3, QHeaderView.ResizeToContents)
101 
102  self.topics_tree_widget.setSelectionMode(QAbstractItemView.MultiSelection)
103 
104  # Whether to get all topics or only the topics that are set in advance.
105  # Can be also set by the setter method "set_selected_topics".
106  self._selected_topics = selected_topics
107 
108  self._selected_items = []
109 
111  self._topics = {}
112  self._tree_items = {}
113  self._column_index = {}
114 
115  self.values = {}
116 
117  for column_name in self._column_names:
118  self._column_index[column_name] = len(self._column_index)
119 
120  self.topics_tree_widget.itemExpanded.connect(self.expanded)
121  self.topics_tree_widget.itemCollapsed.connect(self.collapsed)
122 
123 
124  # init and start update timer
125  self._timer_refresh_topics = QTimer(self)
126  self._timer_refresh_topics.timeout.connect(self.refresh_topics)
127 
128  self.topics_tree_widget.setAlternatingRowColors(True)
129 
130  self.topics_tree_widget.header().resizeSection(3, 50)
131 
132  self.topics_tree_widget.itemSelectionChanged.connect(self.selection_changed)
133 
134 
135 
136  def set_topic_specifier(self, specifier):
137  self._select_topic_type = specifier
138 
139  def start(self):
140  """
141  This method needs to be called to start updating topic pane.
142  """
143  self._timer_refresh_topics.start(1000)
144 
145 
146  @Slot()
147  def selection_changed(self):
148 
149  selected = self.get_selected()
150 
151  for key, item in self._tree_items.items():
152  if item.isSelected():
153  item.setText(3, '{%d}' % selected.index(key))
154  else:
155  item.setText(3, '')
156 
157  self.selectionChanged.emit(selected)
158 
159  pass
160 
161  @Slot('QTreeWidgetItem')
162  def expanded(self, item):
163 
164  name = item.data(0, Qt.UserRole)
165 
166  if not isinstance(item, TreeWidgetItem) or not item._is_topic:
167  return
168 
169  print "expanded", name
170  self._topics[item._topic_name]['info'].start_monitoring()
171 
172 
173  @Slot('QTreeWidgetItem')
174  def collapsed(self, item):
175 
176  name = item.data(0, Qt.UserRole)
177 
178  if not isinstance(item, TreeWidgetItem) or not item._is_topic:
179  return
180 
181  print "collapsed", name
182  self._topics[item._topic_name]['info'].stop_monitoring()
183 
184  def unselect(self, items):
185  if not hasattr(items, "__iter__"):
186  items = [items]
187  for item in items:
188  if item in self._selected_items:
189  self._selected_items.remove(item)
190  self._tree_items[item].setData(self._column_names.index("checkbox"), Qt.CheckStateRole, False)
191 
192 
193  def clear_selection(self):
194  self.topics_tree_widget.clearSelection()
195 
196  items = self.get_selected()
197  for item in items:
198  self._tree_items[item].setData(self._column_names.index("checkbox"), Qt.CheckStateRole, False)
199  self._selected_items = []
200 
201  def get_selected(self):
202  # param_name:desc
203  self._selected_items = [ item._topic_name for item in self.topics_tree_widget.selectedItems() ]
204  return self._selected_items
205 
207  # param_name:desc
208  self.get_selected()
209  items = self._selected_items
210  values = {item: self.values[item] for item in items if item in self.values}
211  return values
212 
214  self.get_selected()
215  f = lambda item: self._tree_items[item].data(self._column_index['type'], Qt.EditRole)
216  selected_types = map(f, self._selected_items)
217  return selected_types
218 
219 
220  @Slot()
221  def refresh_topics(self):
222  """
223  refresh tree view items
224  """
225 
226  try:
227  if self._selected_topics is None:
228  topic_list = rospy.get_published_topics()
229  if topic_list is None:
230  rospy.logerr('Not even a single published topic found. Check network configuration')
231  return
232  else: # Topics to show are specified.
233  topic_list = self._selected_topics
234  topic_specifiers_server_all = None
235  topic_specifiers_required = None
236 
237  rospy.logdebug('refresh_topics) self._selected_topics=%s' % (topic_list,))
238 
239  if self._select_topic_type == self.SELECT_BY_NAME:
240  topic_specifiers_server_all = [name for name, type in rospy.get_published_topics()]
241  topic_specifiers_required = [name for name, type in topic_list]
242  elif self._select_topic_type == self.SELECT_BY_MSGTYPE:
243  # The topics that are required (by whoever uses this class).
244  topic_specifiers_required = [type for name, type in topic_list]
245 
246  # The required topics that match with published topics.
247  topics_match = [(name, type) for name, type in rospy.get_published_topics() if type in topic_specifiers_required]
248  topic_list = topics_match
249  rospy.logdebug('selected & published topic types=%s' % (topic_list,))
250 
251  rospy.logdebug('server_all=%s\nrequired=%s\ntlist=%s' % (topic_specifiers_server_all, topic_specifiers_required, topic_list))
252  if len(topic_list) == 0:
253  rospy.logerr('None of the following required topics are found.\n(NAME, TYPE): %s' % (self._selected_topics,))
254  return
255  except IOError as e:
256  rospy.logerr("Communication with rosmaster failed: {0}".format(e.strerror))
257  return
258 
259  if self._current_topic_list != topic_list:
260  self._current_topic_list = topic_list
261 
262 
263 
264  # start new topic dict
265  new_topics = {}
266 
267  for topic_name, topic_type in topic_list:
268  # if topic is new or has changed its type
269  if topic_name not in self._topics or \
270  self._topics[topic_name]['type'] != topic_type:
271  # create new TopicInfo
272  topic_info = TopicInfo(topic_name, topic_type)
273  message_instance = None
274  if topic_info.message_class is not None:
275  message_instance = topic_info.message_class()
276  # add it to the dict and tree view
277  topic_item = self._recursive_create_widget_items(self.topics_tree_widget, topic_name, topic_type, message_instance)
278  new_topics[topic_name] = {
279  'item': topic_item,
280  'info': topic_info,
281  'type': topic_type,
282  }
283  else:
284  # if topic has been seen before, copy it to new dict and
285  # remove it from the old one
286  new_topics[topic_name] = self._topics[topic_name]
287  del self._topics[topic_name]
288 
289  # clean up old topics
290  for topic_name in self._topics.keys():
291  self._topics[topic_name]['info'].stop_monitoring()
292  index = self.topics_tree_widget.indexOfTopLevelItem(
293  self._topics[topic_name]['item'])
294  self.topics_tree_widget.takeTopLevelItem(index)
295  del self._topics[topic_name]
296 
297  # switch to new topic dict
298  self._topics = new_topics
299 
300  self.selectionChanged.emit(self.get_selected())
301 
302  self._update_topics_data()
303 
304  self.topicsRefreshed.emit()
305 
306  def remove(self, key):
307  item = self._tree_items[key]
308  index = self.topics_tree_widget.indexOfTopLevelItem(item)
309  self.topics_tree_widget.takeTopLevelItem(index)
310  del self._tree_items[key]
311 
313 
314  selected_dict = { self._selected_items[index] : index for index in range(len(self._selected_items)) }
315  # print selected_dict
316 
317 
318  for topic in self._topics.values():
319  topic_info = topic['info']
320  if topic_info.monitoring:
321  # update rate
322  rate, _, _, _ = topic_info.get_hz()
323  rate_text = '%1.2f' % rate if rate != None else 'unknown'
324 
325  # update bandwidth
326  bytes_per_s, _, _, _ = topic_info.get_bw()
327  if bytes_per_s is None:
328  bandwidth_text = 'unknown'
329  elif bytes_per_s < 1000:
330  bandwidth_text = '%.2fB/s' % bytes_per_s
331  elif bytes_per_s < 1000000:
332  bandwidth_text = '%.2fKB/s' % (bytes_per_s / 1000.)
333  else:
334  bandwidth_text = '%.2fMB/s' % (bytes_per_s / 1000000.)
335 
336  # update values
337  value_text = ''
338  self.update_value(topic_info._topic_name, topic_info.last_message)
339 
340  else:
341  rate_text = ''
342  bandwidth_text = ''
343  value_text = 'not monitored' if topic_info.error is None else topic_info.error
344 
345  self._tree_items[topic_info._topic_name].setText(self._column_index['value'], value_text)
346 
347  def update_value(self, topic_name, message, vtype = None):
348 
349  value = message
350  if hasattr(value, '__iter__'):
351  try:
352  value = np.array(value)
353  pass
354  except:
355  pass
356 
357  self.values.update({topic_name:value})
358 
359  if hasattr(message, '__slots__') and hasattr(message, '_slot_types'):
360  slots = zip(message.__slots__, message._slot_types)
361  for slot_name, slot_type in slots:
362  self.update_value(topic_name + '/' + slot_name, getattr(message, slot_name), vtype = slot_type)
363 
364  elif type(message) in (list, tuple) and (len(message) > 0):# and hasattr(message[0], '__slots__'):
365 
366  for index, slot in enumerate(message):
367  if topic_name + '[%d]' % index in self._tree_items:
368  self.update_value(topic_name + '[%d]' % index, slot)
369  else:
370  base_type_str, _ = self._extract_array_info(self._tree_items[topic_name].text(self._column_index['type']))
371  self._recursive_create_widget_items(self._tree_items[topic_name], topic_name + '[%d]' % index, base_type_str, slot)
372  # remove obsolete children
373  if len(message) < self._tree_items[topic_name].childCount():
374  for i in range(len(message), self._tree_items[topic_name].childCount()):
375  item_topic_name = topic_name + '[%d]' % i
376  self._recursive_delete_widget_items(self._tree_items[item_topic_name])
377  else:
378  if topic_name in self._tree_items:
379  self._tree_items[topic_name].setText(self._column_index['value'], repr(message))
380 
381  def _extract_array_info(self, type_str):
382  array_size = None
383  if '[' in type_str and type_str[-1] == ']':
384  type_str, array_size_str = type_str.split('[', 1)
385  array_size_str = array_size_str[:-1]
386  if len(array_size_str) > 0:
387  array_size = int(array_size_str)
388  else:
389  array_size = 0
390 
391  return type_str, array_size
392 
393  def _recursive_create_widget_items(self, parent, topic_name, type_name, message):
394  if parent is self.topics_tree_widget:
395  # show full topic name with preceding namespace on toplevel item
396  topic_text = topic_name
397 
398  _item = parent
399  topic_names = topic_name.split('/')
400  name_space = ""
401  for name in topic_names:
402  if name == "":
403  continue
404  _name = "/" + name
405  name_space = name_space + _name
406  if name_space not in self._tree_items:
407 
408  is_topic = False
409 
410  if name_space == topic_name:
411  is_topic = True
412 
413  _item = TreeWidgetItem(self._toggle_monitoring, name_space, _item, is_topic = is_topic)
414  _item.setText(self._column_index['topic'], _name)
415  _item.setText(self._column_index['type'], type_name)
416  _item.setData(0, Qt.UserRole, name_space)
417 
418  self._tree_items[name_space] = _item
419 
420  _item = self._tree_items[name_space]
421 
422  item = _item
423 
424 
425 
426  else:
427  topic_text = topic_name.split('/')[-1]
428  if '[' in topic_text:
429  topic_text = topic_text[topic_text.index('['):]
430 
431 
432  item = TreeWidgetItem(self._toggle_monitoring, topic_name, parent)
433  item.setText(self._column_index['topic'], topic_text)
434  item.setText(self._column_index['type'], type_name)
435  item.setData(0, Qt.UserRole, topic_name)
436  self._tree_items[topic_name] = item
437 
438 
439 
440  if hasattr(message, '__slots__') and hasattr(message, '_slot_types'):
441  for slot_name, type_name in zip(message.__slots__, message._slot_types):
442  self._recursive_create_widget_items(item, topic_name + '/' + slot_name, type_name, getattr(message, slot_name))
443 
444  else:
445  base_type_str, array_size = self._extract_array_info(type_name)
446  try:
447  base_instance = roslib.message.get_message_class(base_type_str)()
448  except (ValueError, TypeError):
449  base_instance = None
450  if array_size is not None and hasattr(base_instance, '__slots__'):
451  for index in range(array_size):
452  self._recursive_create_widget_items(item, topic_name + '[%d]' % index, base_type_str, base_instance)
453  return item
454 
455 
456 
457  def _toggle_monitoring(self, topic_name):
458  self.get_selected()
459 
460  sel = self._selected_items
461  for i in range(len(sel)):
462  _item = self._tree_items[sel[i]]
463  _item.setText(3, '{%d}' % i )
464 
465  self.selectionChanged.emit(self.get_selected())
466 
467 
469  def _recursive_remove_items_from_tree(item):
470  for index in reversed(range(item.childCount())):
471  _recursive_remove_items_from_tree(item.child(index))
472  topic_name = item.data(0, Qt.UserRole)
473  del self._tree_items[topic_name]
474  _recursive_remove_items_from_tree(item)
475  item.parent().removeChild(item)
476 
477  @Slot('QPoint')
479  header = self.topics_tree_widget.header()
480 
481  # show context menu
482  menu = QMenu(self)
483  action_toggle_auto_resize = menu.addAction('Toggle Auto-Resize')
484  action = menu.exec_(header.mapToGlobal(pos))
485 
486  # evaluate user action
487  if action is action_toggle_auto_resize:
488  if header.resizeMode(0) == QHeaderView.ResizeToContents:
489  header.setResizeMode(QHeaderView.Interactive)
490  else:
491  header.setResizeMode(QHeaderView.ResizeToContents)
492 
493  @Slot('QPoint')
495  item = self.topics_tree_widget.itemAt(pos)
496  if item is None:
497  return
498 
499  # show context menu
500  menu = QMenu(self)
501  action_item_expand = menu.addAction(QIcon.fromTheme('zoom-in'), 'Expand All Children')
502  action_item_collapse = menu.addAction(QIcon.fromTheme('zoom-out'), 'Collapse All Children')
503  action = menu.exec_(self.topics_tree_widget.mapToGlobal(pos))
504 
505  # evaluate user action
506  if action in (action_item_expand, action_item_collapse):
507  expanded = (action is action_item_expand)
508 
509  def recursive_set_expanded(item):
510  item.setExpanded(expanded)
511  for index in range(item.childCount()):
512  recursive_set_expanded(item.child(index))
513  recursive_set_expanded(item)
514 
515  def shutdown_plugin(self):
516  for topic in self._topics.values():
517  topic['info'].stop_monitoring()
518  self._timer_refresh_topics.stop()
519 
520  def set_selected_topics(self, selected_topics):
521  """
522  @param selected_topics: list of tuple. [(topic_name, topic_type)]
523  @type selected_topics: []
524  """
525  rospy.logdebug('set_selected_topics topics={}'.format(
526  len(selected_topics)))
527  self._selected_topics = selected_topics
528 
529  # TODO(Enhancement) Save/Restore tree expansion state
530  def save_settings(self, plugin_settings, instance_settings):
531  header_state = self.topics_tree_widget.header().saveState()
532  instance_settings.set_value('tree_widget_header_state', header_state)
533 
534  def restore_settings(self, pluggin_settings, instance_settings):
535  if instance_settings.contains('tree_widget_header_state'):
536  header_state = instance_settings.value('tree_widget_header_state')
537  if not self.topics_tree_widget.header().restoreState(header_state):
538  rospy.logwarn("rqt_dyn_tune: Failed to restore header state.")
539 
540 class TreeWidgetItem(QTreeWidgetItem):
541 
542  def __init__(self, check_state_changed_callback, topic_name, parent=None, is_topic = False):
543  super(TreeWidgetItem, self).__init__(parent)
544  self._check_state_changed_callback = check_state_changed_callback
545  self._topic_name = topic_name
546  # self.setCheckState(3, Qt.Unchecked)
547  self._is_topic = is_topic
548 
549 
550  def setData(self, column, role, value):
551  super(TreeWidgetItem, self).setData(column, role, value)
552 
553 
554 
555 
def on_topics_tree_widget_customContextMenuRequested(self, pos)
def set_selected_topics(self, selected_topics)
def __init__(self, check_state_changed_callback, topic_name, parent=None, is_topic=False)
def handle_header_view_customContextMenuRequested(self, pos)
def _recursive_create_widget_items(self, parent, topic_name, type_name, message)
def __init__(self, plugin=None, selected_topics=None, select_topic_type=SELECT_BY_NAME)
Definition: topic_widget.py:71
def save_settings(self, plugin_settings, instance_settings)
def set_topic_specifier(self, specifier)
def format(color, style='')
Definition: syntax.py:8
def _toggle_monitoring(self, topic_name)
def setData(self, column, role, value)
def _extract_array_info(self, type_str)
def _recursive_delete_widget_items(self, item)
def restore_settings(self, pluggin_settings, instance_settings)
def update_value(self, topic_name, message, vtype=None)


rqt_dyn_tune
Author(s):
autogenerated on Mon Jun 10 2019 14:52:08