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