1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 from datetime import datetime
35 from python_qt_binding.QtCore import Qt, QUrl, QTimer, Signal
36 from python_qt_binding.QtGui import QIcon, QTextDocument
37 try:
38 from python_qt_binding.QtGui import QApplication, QHBoxLayout, QVBoxLayout, QSpacerItem, QSizePolicy
39 from python_qt_binding.QtGui import QCheckBox, QComboBox, QDialog, QLabel, QTextBrowser, QToolButton, QWidget
40 except:
41 from python_qt_binding.QtWidgets import QApplication, QHBoxLayout, QVBoxLayout, QSpacerItem, QSizePolicy
42 from python_qt_binding.QtWidgets import QCheckBox, QComboBox, QDialog, QLabel, QTextBrowser, QToolButton, QWidget
43
44 import math
45 import threading
46 import time
47
48 from roslib import message
49 import rospy
50
51 import gui_resources
52 import node_manager_fkie as nm
53
54
55
57
58 MESSAGE_LINE_LIMIT = 128
59 MESSAGE_HZ_LIMIT = 10
60 MAX_DISPLAY_MSGS = 25
61 STATISTIC_QUEUE_LEN = 5000
62
63 '''
64 This dialog shows the output of a topic.
65 '''
66
67 finished_signal = Signal(str)
68 '''
69 finished_signal has as parameter the name of the topic and is emitted, if this
70 dialog was closed.
71 '''
72
73 msg_signal = Signal(object, bool)
74 '''
75 msg_signal is a signal, which is emitted, if a new message was received.
76 '''
77
78 text_hz_signal = Signal(str)
79 text_signal = Signal(str)
80 '''
81 text_signal is a signal, which is emitted, if a new text to display was received.
82 '''
83
84 text_error_signal = Signal(str)
85 '''
86 text_error_signal is a signal, which is emitted, if a new error text to display was received.
87 '''
88
89 request_pw = Signal(object)
90
91 - def __init__(self, topic, msg_type, show_only_rate=False, masteruri=None, use_ssh=False, parent=None):
92 '''
93 Creates an input dialog.
94 @param topic: the name of the topic
95 @type topic: C{str}
96 @param msg_type: the type of the topic
97 @type msg_type: C{str}
98 @raise Exception: if no topic class was found for the given type
99 '''
100 QDialog.__init__(self, parent=parent)
101 self._masteruri = masteruri
102 masteruri_str = '' if masteruri is None else '[%s]' % masteruri
103 self.setObjectName(' - '.join(['EchoDialog', topic, masteruri_str]))
104 self.setAttribute(Qt.WA_DeleteOnClose, True)
105 self.setWindowFlags(Qt.Window)
106 self.setWindowTitle('%s %s %s' % ('Echo --- ' if not show_only_rate else 'Hz --- ', topic, masteruri_str))
107 self.resize(728, 512)
108 self.verticalLayout = QVBoxLayout(self)
109 self.verticalLayout.setObjectName("verticalLayout")
110 self.verticalLayout.setContentsMargins(1, 1, 1, 1)
111 self.mIcon = QIcon(":/icons/crystal_clear_prop_run_echo.png")
112 self.setWindowIcon(self.mIcon)
113
114 self.topic = topic
115 self.show_only_rate = show_only_rate
116 self.lock = threading.RLock()
117 self.last_printed_count = 0
118 self.msg_t0 = -1.
119 self.msg_tn = 0
120 self.times = []
121
122 self.message_count = 0
123 self._rate_message = ''
124 self._scrapped_msgs = 0
125 self._scrapped_msgs_sl = 0
126
127 self._last_received_ts = 0
128 self.receiving_hz = self.MESSAGE_HZ_LIMIT
129 self.line_limit = self.MESSAGE_LINE_LIMIT
130
131 self.field_filter_fn = None
132
133 options = QWidget(self)
134 if not show_only_rate:
135 hLayout = QHBoxLayout(options)
136 hLayout.setContentsMargins(1, 1, 1, 1)
137 self.no_str_checkbox = no_str_checkbox = QCheckBox('Hide strings')
138 no_str_checkbox.toggled.connect(self.on_no_str_checkbox_toggled)
139 hLayout.addWidget(no_str_checkbox)
140 self.no_arr_checkbox = no_arr_checkbox = QCheckBox('Hide arrays')
141 no_arr_checkbox.toggled.connect(self.on_no_arr_checkbox_toggled)
142 hLayout.addWidget(no_arr_checkbox)
143 self.combobox_reduce_ch = QComboBox(self)
144 self.combobox_reduce_ch.addItems([str(self.MESSAGE_LINE_LIMIT), '0', '80', '256', '1024'])
145 self.combobox_reduce_ch.activated[str].connect(self.combobox_reduce_ch_activated)
146 self.combobox_reduce_ch.setEditable(True)
147 self.combobox_reduce_ch.setToolTip("Set maximum line width. 0 disables the limit.")
148 hLayout.addWidget(self.combobox_reduce_ch)
149
150
151
152 spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
153 hLayout.addItem(spacerItem)
154
155 self.combobox_displ_hz = QComboBox(self)
156 self.combobox_displ_hz.addItems([str(self.MESSAGE_HZ_LIMIT), '0', '0.1', '1', '50', '100', '1000'])
157 self.combobox_displ_hz.activated[str].connect(self.on_combobox_hz_activated)
158 self.combobox_displ_hz.setEditable(True)
159 hLayout.addWidget(self.combobox_displ_hz)
160 displ_hz_label = QLabel('Hz', self)
161 hLayout.addWidget(displ_hz_label)
162
163 self.combobox_msgs_count = QComboBox(self)
164 self.combobox_msgs_count.addItems([str(self.MAX_DISPLAY_MSGS), '0', '50', '100'])
165 self.combobox_msgs_count.activated[str].connect(self.on_combobox_count_activated)
166 self.combobox_msgs_count.setEditable(True)
167 self.combobox_msgs_count.setToolTip("Set maximum displayed message count. 0 disables the limit.")
168 hLayout.addWidget(self.combobox_msgs_count)
169 displ_count_label = QLabel('#', self)
170 hLayout.addWidget(displ_count_label)
171
172 self.topic_control_button = QToolButton(self)
173 self.topic_control_button.setText('stop')
174 self.topic_control_button.setIcon(QIcon(':/icons/deleket_deviantart_stop.png'))
175 self.topic_control_button.clicked.connect(self.on_topic_control_btn_clicked)
176 hLayout.addWidget(self.topic_control_button)
177
178 clearButton = QToolButton(self)
179 clearButton.setText('clear')
180 clearButton.clicked.connect(self.on_clear_btn_clicked)
181 hLayout.addWidget(clearButton)
182 self.verticalLayout.addWidget(options)
183
184 self.display = QTextBrowser(self)
185 self.display.setReadOnly(True)
186 self.verticalLayout.addWidget(self.display)
187 self.display.document().setMaximumBlockCount(500)
188 self.max_displayed_msgs = self.MAX_DISPLAY_MSGS
189 self._blocks_in_msg = None
190 self.display.setOpenLinks(False)
191 self.display.anchorClicked.connect(self._on_display_anchorClicked)
192
193 self.status_label = QLabel('0 messages', self)
194 self.verticalLayout.addWidget(self.status_label)
195
196
197 errmsg = ''
198 try:
199 self.__msg_class = message.get_message_class(msg_type)
200 if not self.__msg_class:
201 errmsg = "Cannot load message class for [%s]. Did you build messages?" % msg_type
202
203 except Exception as e:
204 self.__msg_class = None
205 errmsg = "Cannot load message class for [%s]. Did you build messagest?\nError: %s" % (msg_type, e)
206
207
208 self.msg_signal.connect(self._append_message)
209 self.sub = None
210
211
212 self.ssh_output_file = None
213 self.ssh_error_file = None
214 self.ssh_input_file = None
215 self.text_signal.connect(self._append_text)
216 self.text_hz_signal.connect(self._append_text_hz)
217 self._current_msg = ''
218 self._current_errmsg = ''
219 self.text_error_signal.connect(self._append_error_text)
220
221
222 if use_ssh:
223 self.__msg_class = None
224 self._on_display_anchorClicked(QUrl(self._masteruri))
225 elif self.__msg_class is None:
226 errtxt = '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">\n%s</pre>' % (errmsg)
227 self.display.setText('<a href="%s">open using SSH</a>' % (masteruri))
228 self.display.append(errtxt)
229 else:
230 self.sub = rospy.Subscriber(self.topic, self.__msg_class, self._msg_handle)
231
232 self.print_hz_timer = QTimer()
233 self.print_hz_timer.timeout.connect(self._on_calc_hz)
234 self.print_hz_timer.start(1000)
235
236
237
238
239
240
241
242
243
245 if self.sub is not None:
246 self.sub.unregister()
247 del self.sub
248 try:
249 self.ssh_output_file.close()
250 self.ssh_error_file.close()
251
252 self.ssh_input_file.write('%s\n' % chr(3))
253 self.ssh_input_file.close()
254 except:
255 pass
256 self.finished_signal.emit(self.topic)
257 if self.parent() is None:
258 QApplication.quit()
259
260
261
263 def field_filter(val):
264 try:
265
266
267 for f, t in zip(val.__slots__, val._slot_types):
268 if echo_noarr and '[' in t:
269 continue
270 elif echo_nostr and 'string' in t:
271 continue
272 yield f
273 except:
274 pass
275 return field_filter
276
279
282
284 try:
285 self.line_limit = int(ch_txt)
286 except ValueError:
287 try:
288 self.line_limit = float(ch_txt)
289 except ValueError:
290 self.combobox_reduce_ch.setEditText(str(self.line_limit))
291
293 try:
294 self.receiving_hz = int(hz_txt)
295 except ValueError:
296 try:
297 self.receiving_hz = float(hz_txt)
298 except ValueError:
299 self.combobox_displ_hz.setEditText(str(self.receiving_hz))
300
302 try:
303 self.max_displayed_msgs = int(count_txt)
304 self._blocks_in_msg = None
305 except ValueError:
306 self.combobox_msgs_count.setEditText(str(self.max_displayed_msgs))
307
309 self.display.clear()
310 with self.lock:
311 self.message_count = 0
312 self._scrapped_msgs = 0
313 del self.times[:]
314
316 try:
317 if self.sub is None and self.ssh_output_file is None:
318 if self.__msg_class:
319 self.sub = rospy.Subscriber(self.topic, self.__msg_class, self._msg_handle)
320 else:
321 self._on_display_anchorClicked(QUrl(self._masteruri))
322 self.topic_control_button.setText('stop')
323 self.topic_control_button.setIcon(QIcon(':/icons/deleket_deviantart_stop.png'))
324 else:
325 if self.sub is not None:
326 self.sub.unregister()
327 self.sub = None
328 elif self.ssh_output_file is not None:
329 self.ssh_output_file.close()
330 self.ssh_error_file.close()
331 self.ssh_output_file = None
332 self.topic_control_button.setText('play')
333 self.topic_control_button.setIcon(QIcon(':/icons/deleket_deviantart_play.png'))
334 self.no_str_checkbox.setEnabled(True)
335 self.no_arr_checkbox.setEnabled(True)
336 except Exception as e:
337 rospy.logwarn('Error while stop/play echo for topic %s: %s' % (self.topic, e))
338
341
343 '''
344 Adds a label to the dialog's layout and shows the given text.
345 @param msg: the text to add to the dialog
346 @type msg: message object
347 '''
348 current_time = time.time()
349 self._count_messages(current_time)
350
351 if self._last_received_ts != 0 and self.receiving_hz != 0:
352 if not latched and current_time - self._last_received_ts < 1.0 / self.receiving_hz:
353 self._scrapped_msgs += 1
354 self._scrapped_msgs_sl += 1
355 return
356 self._last_received_ts = current_time
357 if not self.show_only_rate:
358
359 msg = message.strify_message(msg, field_filter=self.field_filter_fn)
360 if isinstance(msg, tuple):
361 msg = msg[0]
362 msg = self._trim_width(msg)
363 msg = msg.replace('<', '<').replace('>', '>')
364
365 if self._scrapped_msgs_sl > 0:
366 txt = '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">scrapped %s message because of Hz-settings</pre>' % self._scrapped_msgs_sl
367 self.display.append(txt)
368 self._scrapped_msgs_sl = 0
369 txt = '<pre style="background-color:#FFFCCC; color:#000000;font-family:Fixedsys,Courier; padding:10px;">---------- %s --------------------\n%s</pre>' % (datetime.now().strftime("%d.%m.%Y %H:%M:%S.%f"), msg)
370
371 self._update_max_msg_count(txt)
372 self.display.append(txt)
373 self._print_status()
374
376 '''
377 Counts the received messages. Call this method only on receive message.
378 '''
379 current_time = ts
380 with self.lock:
381
382 if self.msg_t0 < 0 or self.msg_t0 > current_time:
383 self.msg_t0 = current_time
384 self.msg_tn = current_time
385 self.times = []
386 else:
387 self.times.append(current_time - self.msg_tn)
388 self.msg_tn = current_time
389
390 if len(self.times) > self.STATISTIC_QUEUE_LEN:
391 self.times.pop(0)
392 self.message_count += 1
393
395 '''
396 reduce line width to current limit
397 :param msg: the message
398 :type msg: str
399 :return: trimmed message
400 '''
401 result = msg
402 if self.line_limit != 0:
403 a = ''
404 for l in msg.splitlines():
405 a = a + (l if len(l) <= self.line_limit else l[0:self.line_limit - 3] + '...') + '\n'
406 result = a
407 return result
408
410 '''
411 set the count of the displayed messages on receiving the first message
412 :param txt: text of the message, which will be added to the document
413 :type txt: str
414 '''
415 if self._blocks_in_msg is None:
416 td = QTextDocument(txt)
417 self._blocks_in_msg = td.blockCount()
418 self.display.document().setMaximumBlockCount(self._blocks_in_msg * self.max_displayed_msgs)
419
421 if rospy.is_shutdown():
422 self.close()
423 return
424 if self.message_count == self.last_printed_count:
425 return
426 with self.lock:
427
428 n = len(self.times)
429 if n < 2:
430 return
431 mean = sum(self.times) / n
432 rate = 1. / mean if mean > 0. else 0
433
434 std_dev = math.sqrt(sum((x - mean) ** 2 for x in self.times) / n)
435
436 max_delta = max(self.times)
437 min_delta = min(self.times)
438 self.last_printed_count = self.message_count
439 self._rate_message = "average rate: %.3f\tmin: %.3fs max: %.3fs std dev: %.5fs window: %s" % (rate, min_delta, max_delta, std_dev, n + 1)
440 if self._scrapped_msgs > 0:
441 self._rate_message += " --- scrapped msgs: %s" % self._scrapped_msgs
442 self._print_status()
443 if self.show_only_rate:
444 self.display.append(self._rate_message)
445
447 self.status_label.setText('%s messages %s' % (self.message_count, self._rate_message))
448
449 - def _append_text(self, text):
450 '''
451 Append echo text received through the SSH.
452 '''
453 with self.lock:
454 self._current_msg += text
455 if self._current_msg.find('---') != -1:
456 messages = self._current_msg.split('---')
457 for m in messages[:-1]:
458 current_time = time.time()
459 self._count_messages(current_time)
460
461 m = self._trim_width(m)
462 txt = '<pre style="background-color:#FFFCCC; color:#000000;font-family:Fixedsys,Courier; padding:10px;">---------- %s --------------------\n%s</pre>' % (datetime.now().strftime("%d.%m.%Y %H:%M:%S.%f"), m)
463
464 self._update_max_msg_count(txt)
465 self.display.append(txt)
466 self._current_msg = messages[-1]
467 self._print_status()
468
469 - def _append_error_text(self, text):
470 '''
471 Append error text received through the SSH.
472 '''
473 with self.lock:
474 self._current_errmsg += text
475 if self._current_errmsg.find('\n') != -1:
476 messages = self._current_errmsg.split('\n')
477 for m in messages[:-1]:
478 txt = '<pre style="color:red; font-family:Fixedsys,Courier,monospace; padding:10px;">%s</pre>' % m
479 self.display.append(txt)
480 self._current_errmsg = messages[-1]
481
482 - def _append_text_hz(self, text):
483 '''
484 Append text received through the SSH for hz view.
485 '''
486 with self.lock:
487 self._current_msg += text
488 if self._current_msg.find('\n') != -1:
489 messages = self._current_msg.split('\n')
490 for m in messages[:-1]:
491 txt = '<div style="font-family:Fixedsys,Courier;">%s</div>' % (m)
492 self.display.append(txt)
493 self._current_msg = messages[-1]
494
496 try:
497 ok = False
498 if self.show_only_rate:
499 self.ssh_input_file, self.ssh_output_file, self.ssh_error_file, ok = nm.ssh().ssh_exec(url.host(), ['rostopic hz %s' % (self.topic)], user, pw, auto_pw_request=True, get_pty=True)
500 self.status_label.setText('connected to %s over SSH' % url.host())
501 else:
502 self.combobox_displ_hz.setEnabled(False)
503 nostr = '--nostr' if self.no_str_checkbox.isChecked() else ''
504 noarr = '--noarr' if self.no_arr_checkbox.isChecked() else ''
505 self.ssh_input_file, self.ssh_output_file, self.ssh_error_file, ok = nm.ssh().ssh_exec(url.host(), ['rostopic echo %s %s %s' % (nostr, noarr, self.topic)], user, pw, auto_pw_request=True, get_pty=True)
506 if ok:
507 self.display.clear()
508 target = self._read_output_hz if self.show_only_rate else self._read_output
509 thread = threading.Thread(target=target, args=((self.ssh_output_file,)))
510 thread.setDaemon(True)
511 thread.start()
512 thread = threading.Thread(target=self._read_error, args=((self.ssh_error_file,)))
513 thread.setDaemon(True)
514 thread.start()
515 elif self.ssh_output_file:
516 self.ssh_output_file.close()
517 self.ssh_error_file.close()
518 except Exception as e:
519 self._append_error_text('%s\n' % e)
520
521
522
524 try:
525 while not output_file.closed:
526 text = output_file.read(1)
527 if text:
528 self.text_hz_signal.emit(text)
529 except:
530 pass
531
532
533
535 while not output_file.closed:
536 text = output_file.read(1)
537 if text:
538 self.text_signal.emit(text)
539
541 try:
542 while not error_file.closed:
543 text = error_file.read(1)
544 if text:
545 self.text_error_signal.emit(text)
546 except:
547 pass
548
549
550