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 import os
00034 import rospkg
00035 import roslib
00036
00037 from python_qt_binding import loadUi
00038 from python_qt_binding.QtCore import Qt, QTimer, qWarning, Slot
00039 from python_qt_binding.QtGui import QIcon
00040 from python_qt_binding.QtWidgets import QAction, QMenu, QWidget
00041
00042 import rospy
00043
00044 from rqt_py_common.topic_completer import TopicCompleter
00045 from rqt_py_common import topic_helpers
00046
00047 from . rosplot import ROSData, RosPlotException
00048
00049 def get_plot_fields(topic_name):
00050 topic_type, real_topic, _ = topic_helpers.get_topic_type(topic_name)
00051 if topic_type is None:
00052 message = "topic %s does not exist" % ( topic_name )
00053 return [], message
00054 field_name = topic_name[len(real_topic)+1:]
00055
00056 slot_type, is_array, array_size = roslib.msgs.parse_type(topic_type)
00057 field_class = roslib.message.get_message_class(slot_type)
00058
00059 fields = [f for f in field_name.split('/') if f]
00060
00061 for field in fields:
00062
00063 try:
00064 field, _, field_index = roslib.msgs.parse_type(field)
00065 except roslib.msgs.MsgSpecException:
00066 message = "invalid field %s in topic %s" % ( field, real_topic )
00067 return [], message
00068
00069 if field not in getattr(field_class, '__slots__', []):
00070 message = "no field %s in topic %s" % ( field_name, real_topic )
00071 return [], message
00072 slot_type = field_class._slot_types[field_class.__slots__.index(field)]
00073 slot_type, slot_is_array, array_size = roslib.msgs.parse_type(slot_type)
00074 is_array = slot_is_array and field_index is None
00075
00076 field_class = topic_helpers.get_type_class(slot_type)
00077
00078 if field_class in (int, float, bool):
00079 topic_kind = 'boolean' if field_class == bool else 'numeric'
00080 if is_array:
00081 if array_size is not None:
00082 message = "topic %s is fixed-size %s array" % ( topic_name, topic_kind )
00083 return [ "%s[%d]" % (topic_name, i) for i in range(array_size) ], message
00084 else:
00085 message = "topic %s is variable-size %s array" % ( topic_name, topic_kind )
00086 return [], message
00087 else:
00088 message = "topic %s is %s" % ( topic_name, topic_kind )
00089 return [ topic_name ], message
00090 else:
00091 if not roslib.msgs.is_valid_constant_type(slot_type):
00092 numeric_fields = []
00093 for i, slot in enumerate(field_class.__slots__):
00094 slot_type = field_class._slot_types[i]
00095 slot_type, is_array, array_size = roslib.msgs.parse_type(slot_type)
00096 slot_class = topic_helpers.get_type_class(slot_type)
00097 if slot_class in (int, float) and not is_array:
00098 numeric_fields.append(slot)
00099 message = ""
00100 if len(numeric_fields) > 0:
00101 message = "%d plottable fields in %s" % ( len(numeric_fields), topic_name )
00102 else:
00103 message = "No plottable fields in %s" % ( topic_name )
00104 return [ "%s/%s" % (topic_name, f) for f in numeric_fields ], message
00105 else:
00106 message = "Topic %s is not numeric" % ( topic_name )
00107 return [], message
00108
00109 def is_plottable(topic_name):
00110 fields, message = get_plot_fields(topic_name)
00111 return len(fields) > 0, message
00112
00113 class PlotWidget(QWidget):
00114 _redraw_interval = 40
00115
00116 def __init__(self, initial_topics=None, start_paused=False):
00117 super(PlotWidget, self).__init__()
00118 self.setObjectName('PlotWidget')
00119
00120 self._initial_topics = initial_topics
00121
00122 rp = rospkg.RosPack()
00123 ui_file = os.path.join(rp.get_path('rqt_plot'), 'resource', 'plot.ui')
00124 loadUi(ui_file, self)
00125 self.subscribe_topic_button.setIcon(QIcon.fromTheme('list-add'))
00126 self.remove_topic_button.setIcon(QIcon.fromTheme('list-remove'))
00127 self.pause_button.setIcon(QIcon.fromTheme('media-playback-pause'))
00128 self.clear_button.setIcon(QIcon.fromTheme('edit-clear'))
00129 self.data_plot = None
00130
00131 self.subscribe_topic_button.setEnabled(False)
00132 if start_paused:
00133 self.pause_button.setChecked(True)
00134
00135 self._topic_completer = TopicCompleter(self.topic_edit)
00136 self._topic_completer.update_topics()
00137 self.topic_edit.setCompleter(self._topic_completer)
00138
00139 self._start_time = rospy.get_time()
00140 self._rosdata = {}
00141 self._remove_topic_menu = QMenu()
00142
00143
00144 self._update_plot_timer = QTimer(self)
00145 self._update_plot_timer.timeout.connect(self.update_plot)
00146
00147 def switch_data_plot_widget(self, data_plot):
00148 self.enable_timer(enabled=False)
00149
00150 self.data_plot_layout.removeWidget(self.data_plot)
00151 if self.data_plot is not None:
00152 self.data_plot.close()
00153
00154 self.data_plot = data_plot
00155 self.data_plot_layout.addWidget(self.data_plot)
00156 self.data_plot.autoscroll(self.autoscroll_checkbox.isChecked())
00157
00158
00159 self.data_plot.dropEvent = self.dropEvent
00160 self.data_plot.dragEnterEvent = self.dragEnterEvent
00161
00162 if self._initial_topics:
00163 for topic_name in self._initial_topics:
00164 self.add_topic(topic_name)
00165 self._initial_topics = None
00166 else:
00167 for topic_name, rosdata in self._rosdata.items():
00168 data_x, data_y = rosdata.next()
00169 self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
00170
00171 self._subscribed_topics_changed()
00172
00173 @Slot('QDragEnterEvent*')
00174 def dragEnterEvent(self, event):
00175
00176 if not event.mimeData().hasText():
00177 if not hasattr(event.source(), 'selectedItems') or len(event.source().selectedItems()) == 0:
00178 qWarning('Plot.dragEnterEvent(): not hasattr(event.source(), selectedItems) or len(event.source().selectedItems()) == 0')
00179 return
00180 item = event.source().selectedItems()[0]
00181 topic_name = item.data(0, Qt.UserRole)
00182 if topic_name == None:
00183 qWarning('Plot.dragEnterEvent(): not hasattr(item, ros_topic_name_)')
00184 return
00185 else:
00186 topic_name = str(event.mimeData().text())
00187
00188
00189 plottable, message = is_plottable(topic_name)
00190 if plottable:
00191 event.acceptProposedAction()
00192 else:
00193 qWarning('Plot.dragEnterEvent(): rejecting: "%s"' % (message))
00194
00195 @Slot('QDropEvent*')
00196 def dropEvent(self, event):
00197 if event.mimeData().hasText():
00198 topic_name = str(event.mimeData().text())
00199 else:
00200 droped_item = event.source().selectedItems()[0]
00201 topic_name = str(droped_item.data(0, Qt.UserRole))
00202 self.add_topic(topic_name)
00203
00204 @Slot(str)
00205 def on_topic_edit_textChanged(self, topic_name):
00206
00207 if topic_name in ('', '/'):
00208 self._topic_completer.update_topics()
00209
00210 plottable, message = is_plottable(topic_name)
00211 self.subscribe_topic_button.setEnabled(plottable)
00212 self.subscribe_topic_button.setToolTip(message)
00213
00214 @Slot()
00215 def on_topic_edit_returnPressed(self):
00216 if self.subscribe_topic_button.isEnabled():
00217 self.add_topic(str(self.topic_edit.text()))
00218
00219 @Slot()
00220 def on_subscribe_topic_button_clicked(self):
00221 self.add_topic(str(self.topic_edit.text()))
00222
00223 @Slot(bool)
00224 def on_pause_button_clicked(self, checked):
00225 self.enable_timer(not checked)
00226
00227 @Slot(bool)
00228 def on_autoscroll_checkbox_clicked(self, checked):
00229 self.data_plot.autoscroll(checked)
00230 if checked:
00231 self.data_plot.redraw()
00232
00233 @Slot()
00234 def on_clear_button_clicked(self):
00235 self.clear_plot()
00236
00237 def update_plot(self):
00238 if self.data_plot is not None:
00239 needs_redraw = False
00240 for topic_name, rosdata in self._rosdata.items():
00241 try:
00242 data_x, data_y = rosdata.next()
00243 if data_x or data_y:
00244 self.data_plot.update_values(topic_name, data_x, data_y)
00245 needs_redraw = True
00246 except RosPlotException as e:
00247 qWarning('PlotWidget.update_plot(): error in rosplot: %s' % e)
00248 if needs_redraw:
00249 self.data_plot.redraw()
00250
00251 def _subscribed_topics_changed(self):
00252 self._update_remove_topic_menu()
00253 if not self.pause_button.isChecked():
00254
00255 self.enable_timer(self._rosdata)
00256 self.data_plot.redraw()
00257
00258 def _update_remove_topic_menu(self):
00259 def make_remove_topic_function(x):
00260 return lambda: self.remove_topic(x)
00261
00262 self._remove_topic_menu.clear()
00263 for topic_name in sorted(self._rosdata.keys()):
00264 action = QAction(topic_name, self._remove_topic_menu)
00265 action.triggered.connect(make_remove_topic_function(topic_name))
00266 self._remove_topic_menu.addAction(action)
00267
00268 if len(self._rosdata) > 1:
00269 all_action = QAction('All', self._remove_topic_menu)
00270 all_action.triggered.connect(self.clean_up_subscribers)
00271 self._remove_topic_menu.addAction(all_action)
00272
00273 self.remove_topic_button.setMenu(self._remove_topic_menu)
00274
00275 def add_topic(self, topic_name):
00276 topics_changed = False
00277 for topic_name in get_plot_fields(topic_name)[0]:
00278 if topic_name in self._rosdata:
00279 qWarning('PlotWidget.add_topic(): topic already subscribed: %s' % topic_name)
00280 continue
00281 self._rosdata[topic_name] = ROSData(topic_name, self._start_time)
00282 if self._rosdata[topic_name].error is not None:
00283 qWarning(str(self._rosdata[topic_name].error))
00284 del self._rosdata[topic_name]
00285 else:
00286 data_x, data_y = self._rosdata[topic_name].next()
00287 self.data_plot.add_curve(topic_name, topic_name, data_x, data_y)
00288 topics_changed = True
00289
00290 if topics_changed:
00291 self._subscribed_topics_changed()
00292
00293 def remove_topic(self, topic_name):
00294 self._rosdata[topic_name].close()
00295 del self._rosdata[topic_name]
00296 self.data_plot.remove_curve(topic_name)
00297
00298 self._subscribed_topics_changed()
00299
00300 def clear_plot(self):
00301 for topic_name, _ in self._rosdata.items():
00302 self.data_plot.clear_values(topic_name)
00303 self.data_plot.redraw()
00304
00305 def clean_up_subscribers(self):
00306 for topic_name, rosdata in self._rosdata.items():
00307 rosdata.close()
00308 self.data_plot.remove_curve(topic_name)
00309 self._rosdata = {}
00310
00311 self._subscribed_topics_changed()
00312
00313 def enable_timer(self, enabled=True):
00314 if enabled:
00315 self._update_plot_timer.start(self._redraw_interval)
00316 else:
00317 self._update_plot_timer.stop()