Package node_manager_fkie :: Module echo_dialog
[frames] | no frames]

Source Code for Module node_manager_fkie.echo_dialog

  1  # Software License Agreement (BSD License) 
  2  # 
  3  # Copyright (c) 2012, Fraunhofer FKIE/US, Alexander Tiderko 
  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 Fraunhofer 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 OWNER 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   
 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  # import roslib 
56 -class EchoDialog(QDialog):
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 # reduce_ch_label = QLabel('ch', self) 150 # hLayout.addWidget(reduce_ch_label) 151 # add spacer 152 spacerItem = QSpacerItem(515, 20, QSizePolicy.Expanding, QSizePolicy.Minimum) 153 hLayout.addItem(spacerItem) 154 # add combobox for displaying frequency of messages 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 # add combobox for count of displayed messages 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 # add topic control button for unsubscribe and subscribe 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 # add clear button 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 # subscribe to the topic 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 # raise Exception("Cannot load message class for [%s]. Did you build messages?"%msg_type) 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 # raise Exception("Cannot load message class for [%s]. Did you build messagest?\nError: %s"%(msg_type, e)) 207 # variables for Subscriber 208 self.msg_signal.connect(self._append_message) 209 self.sub = None 210 211 # vairables for SSH connection 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 # decide, which connection to open 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 # print "======== create", self.objectName() 237 # 238 # def __del__(self): 239 # print "******* destroy", self.objectName() 240 241 # def hideEvent(self, event): 242 # self.close() 243
244 - def closeEvent(self, event):
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 # send Ctrl+C to remote process 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 # else: 260 # self.setParent(None) 261
262 - def create_field_filter(self, echo_nostr, echo_noarr):
263 def field_filter(val): 264 try: 265 # fields = val.__slots__ 266 # field_types = val._slot_types 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
277 - def on_no_str_checkbox_toggled(self, state):
278 self.field_filter_fn = self.create_field_filter(state, self.no_arr_checkbox.isChecked())
279
280 - def on_no_arr_checkbox_toggled(self, state):
281 self.field_filter_fn = self.create_field_filter(self.no_str_checkbox.isChecked(), state)
282
283 - def combobox_reduce_ch_activated(self, ch_txt):
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
292 - def on_combobox_hz_activated(self, hz_txt):
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
301 - def on_combobox_count_activated(self, count_txt):
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
308 - def on_clear_btn_clicked(self):
309 self.display.clear() 310 with self.lock: 311 self.message_count = 0 312 self._scrapped_msgs = 0 313 del self.times[:]
314
315 - def on_topic_control_btn_clicked(self):
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
339 - def _msg_handle(self, data):
340 self.msg_signal.emit(data, (data._connection_header['latching'] != '0'))
341
342 - def _append_message(self, msg, latched):
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 # skip messages, if they are received often then MESSAGE_HZ_LIMIT 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 # convert message to string and reduce line width to current limit 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('<', '&lt;').replace('>', '&gt;') 364 # create a notification about scrapped messages 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 # set the count of the displayed messages on receiving the first message 371 self._update_max_msg_count(txt) 372 self.display.append(txt) 373 self._print_status()
374
375 - def _count_messages(self, ts=time.time()):
376 ''' 377 Counts the received messages. Call this method only on receive message. 378 ''' 379 current_time = ts 380 with self.lock: 381 # time reset 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 # keep only statistics for the last 5000 messages so as not to run out of memory 390 if len(self.times) > self.STATISTIC_QUEUE_LEN: 391 self.times.pop(0) 392 self.message_count += 1
393
394 - def _trim_width(self, msg):
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
409 - def _update_max_msg_count(self, txt):
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
420 - def _on_calc_hz(self):
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 # the code from ROS rostopic 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 # std dev 434 std_dev = math.sqrt(sum((x - mean) ** 2 for x in self.times) / n) 435 # min and max 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
446 - def _print_status(self):
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 # limit the displayed text width 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 # set the count of the displayed messages on receiving the first message 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
495 - def _on_display_anchorClicked(self, url, user=None, pw=None):
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 # import traceback 521 # print traceback.format_exc() 522
523 - def _read_output_hz(self, output_file):
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 # import traceback 532 # print traceback.format_exc() 533
534 - def _read_output(self, output_file):
535 while not output_file.closed: 536 text = output_file.read(1) 537 if text: 538 self.text_signal.emit(text)
539
540 - def _read_error(self, error_file):
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 # import traceback 549 # print traceback.format_exc() 550