class BusMonitorWindow(QMainWindow): DEFAULT_PLOT_X_RANGE = 120 BUS_LOAD_PLOT_MAX_SAMPLES = 50000 def __init__(self, get_frame, iface_name): super(BusMonitorWindow, self).__init__() self.setWindowTitle('CAN bus monitor (%s)' % iface_name.split(os.path.sep)[-1]) self.setWindowIcon(get_app_icon()) self._get_frame = get_frame self._log_widget = RealtimeLogWidget(self, columns=COLUMNS, font=get_monospace_font(), pre_redraw_hook=self._redraw_hook) self._log_widget.on_selection_changed = self._update_measurement_display self._log_widget.table.cellClicked.connect(lambda row, col: self._decode_transfer_at_row(row)) self._log_widget.table.setContextMenuPolicy(Qt.CustomContextMenu) self._log_widget.table.customContextMenuRequested.connect(self._context_menu_requested) self._stat_display = QLabel('0 / 0 / 0', self) stat_display_label = QLabel('TX / RX / FPS: ', self) stat_display_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self._log_widget.custom_area_layout.addWidget(stat_display_label) self._log_widget.custom_area_layout.addWidget(self._stat_display) def flip_row_mark(row, col): if col == 0: item = self._log_widget.table.item(row, col) if item.icon().isNull(): item.setIcon(get_icon('circle')) flash(self, 'Row %d was marked, click again to unmark', row, duration=3) else: item.setIcon(QIcon()) self._log_widget.table.cellPressed.connect(flip_row_mark) self._stat_update_timer = QTimer(self) self._stat_update_timer.setSingleShot(False) self._stat_update_timer.timeout.connect(self._update_stat) self._stat_update_timer.start(500) self._traffic_stat = TrafficStatCounter() self._decoded_message_box = QPlainTextEdit(self) self._decoded_message_box.setReadOnly(True) self._decoded_message_box.setFont(get_monospace_font()) self._decoded_message_box.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self._decoded_message_box.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self._decoded_message_box.setPlainText('Click on a row to see decoded transfer') self._decoded_message_box.setLineWrapMode(QPlainTextEdit.NoWrap) self._decoded_message_box.setWordWrapMode(QTextOption.NoWrap) self._load_plot = PlotWidget(background=(0, 0, 0)) self._load_plot.setRange(xRange=(0, self.DEFAULT_PLOT_X_RANGE), padding=0) self._load_plot.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self._load_plot.showGrid(x=True, y=True, alpha=0.4) self._load_plot.setToolTip('Frames per second') self._load_plot.getPlotItem().getViewBox().setMouseEnabled(x=True, y=False) self._load_plot.enableAutoRange() self._bus_load_plot = self._load_plot.plot(name='Frames per second', pen=mkPen(QColor(Qt.lightGray), width=1)) self._bus_load_samples = [], [] self._started_at_mono = time.monotonic() self._footer_splitter = QSplitter(Qt.Horizontal, self) self._footer_splitter.addWidget(self._decoded_message_box) self._decoded_message_box.setMinimumWidth(400) self._footer_splitter.addWidget(self._load_plot) self._load_plot.setMinimumWidth(200) splitter = QSplitter(Qt.Vertical, self) splitter.addWidget(self._log_widget) self._log_widget.setMinimumHeight(200) splitter.addWidget(self._footer_splitter) widget = QWidget(self) layout = QHBoxLayout(widget) layout.addWidget(splitter) widget.setLayout(layout) self.setCentralWidget(widget) self.setMinimumWidth(700) self.resize(800, 600) # Calling directly from the constructor gets you wrong size information # noinspection PyCallByClass,PyTypeChecker QTimer.singleShot(500, self._update_widget_sizes) def _update_widget_sizes(self): max_footer_height = self.centralWidget().height() * 0.4 self._footer_splitter.setMaximumHeight(max_footer_height) def resizeEvent(self, qresizeevent): super(BusMonitorWindow, self).resizeEvent(qresizeevent) self._update_widget_sizes() def _update_stat(self): bus_load, ts_mono = self._traffic_stat.get_frames_per_second() if len(self._bus_load_samples[0]) >= self.BUS_LOAD_PLOT_MAX_SAMPLES: self._bus_load_samples[0].pop(0) self._bus_load_samples[1].pop(0) self._bus_load_samples[1].append(bus_load) self._bus_load_samples[0].append(ts_mono - self._started_at_mono) self._bus_load_plot.setData(*self._bus_load_samples) (xmin, xmax), _ = self._load_plot.viewRange() diff = xmax - xmin xmax = self._bus_load_samples[0][-1] xmin = self._bus_load_samples[0][-1] - diff self._load_plot.setRange(xRange=(xmin, xmax), padding=0) def _redraw_hook(self): while True: item = self._get_frame() if item is None: break direction, frame = item self._traffic_stat.add_frame(direction, frame) # There is no need to maintain a second queue actually; should be refactored self._log_widget.add_item_async((direction, frame)) bus_load, _ = self._traffic_stat.get_frames_per_second() self._stat_display.setText('%d / %d / %d' % (self._traffic_stat.tx, self._traffic_stat.rx, bus_load)) def _decode_transfer_at_row(self, row): try: rows, text = decode_transfer_from_frame(row, partial(row_to_frame, self._log_widget.table)) except Exception as ex: text = 'Transfer could not be decoded:\n' + str(ex) rows = [row] self._decoded_message_box.setPlainText(text.strip()) def _update_measurement_display(self, selected_rows_cols): if not selected_rows_cols: return min_row = min([row for row, _ in selected_rows_cols]) max_row = max([row for row, _ in selected_rows_cols]) if min_row == max_row: self._decode_transfer_at_row(min_row) def get_ts_diff(row_earlier, row_later): e = self._log_widget.table.item(row_earlier, 1).text() l = self._log_widget.table.item(row_later, 1).text() return TimestampRenderer.compute_timestamp_difference(e, l) def get_load_str(num_frames, dt): if dt >= 1e-6: return 'average load %.1f FPS' % (max(num_frames - 1, 1) / dt) return 'average load is unknown' if min_row == max_row: num_frames = min_row dt = get_ts_diff(0, min_row) flash(self, '%d frames from beginning, %.3f sec since first frame, %s', num_frames, dt, get_load_str(num_frames, dt)) else: num_frames = max_row - min_row + 1 dt = get_ts_diff(min_row, max_row) flash(self, '%d frames, timedelta %.6f sec, %s', num_frames, dt, get_load_str(num_frames, dt)) def _context_menu_requested(self, pos): menu = QMenu(self) row_index = self._log_widget.table.rowAt(pos.y()) if row_index >= 0: action_show_definition = QAction(get_icon('file-code-o'), 'Open data type &definition', self) action_show_definition.triggered.connect(lambda: self._show_data_type_definition(row_index)) menu.addAction(action_show_definition) menu.popup(self._log_widget.table.mapToGlobal(pos)) def _show_data_type_definition(self, row): try: data_type_name = self._log_widget.table.item(row, self._log_widget.table.columnCount() - 1).text() definition = uavcan.TYPENAMES[data_type_name].source_text except Exception as ex: show_error('Data type lookup error', 'Could not load data type definition', ex, self) return win = QDialog(self) win.setAttribute(Qt.WA_DeleteOnClose) view = QPlainTextEdit(win) view.setReadOnly(True) view.setFont(get_monospace_font()) view.setPlainText(definition) view.setLineWrapMode(QPlainTextEdit.NoWrap) layout = QVBoxLayout(win) layout.addWidget(view) win.setWindowTitle('Data type definition [%s]' % data_type_name) win.setLayout(layout) win.resize(600, 300) win.show()
class PythonScriptEditor(QWidget): onReload = pyqtSignal() def __init__(self, parent, main_window): super(PythonScriptEditor, self).__init__(parent) self.main_window = main_window self.inner = QMainWindow() self.setLayout(QVBoxLayout()) self.setWindowTitle("Script Editor") self.editor = CodePlainTextEditor(self.inner) self.output = CodePlainTextEditor(self.inner) self.output.setReadOnly(True) self.pipeline_script = None #type: PipelineScript self.central = QSplitter(Qt.Vertical, self.inner) self.central.setMaximumHeight(400) self.central.addWidget(self.editor) self.central.addWidget(self.output) self.highlighter = PythonHighlighter(self.editor.document()) self.central.setStretchFactor(0, 4) self.central.setStretchFactor(1, 1) self.inner.setCentralWidget(self.central) self.current_file_path = "" self.layout().addWidget(self.inner) self.m_file = self.inner.menuBar().addMenu("File") self.a_new = self.m_file.addAction("New Script") self.a_load = self.m_file.addAction("Load") self.a_save = self.m_file.addAction("Save") self.a_export = self.m_file.addAction("Save As / Export") self.a_new.triggered.connect(self.new) self.a_load.triggered.connect(partial(self.load, None)) self.a_save.triggered.connect(partial(self.save, None, False)) self.a_export.triggered.connect(partial(self.save, None, True)) if sys.platform == "darwin": self.font = QFont("Consolas") else: self.font = QFont("Lucida Console") self.font.setPointSize(10) self.toolbar = self.inner.addToolBar("ScriptEditor Toolbar") self.a_reload = self.toolbar.addAction("Reload") self.a_reload.triggered.connect(self.reload) self.editor.setFont(self.font) self.editor.setTabStopWidth( int(QFontMetricsF(self.editor.font()).width(' ')) * 4) def new(self): dialog = NewScriptDialog(self, self.main_window) dialog.show() # self.load("data/default_pipeline.py") # # self.current_file_path = "" def load(self, pipeline: PipelineScript = None): if pipeline is None: file_path = QFileDialog.getOpenFileName(self, filter="*.py")[0] try: with open(file_path, "r") as f: script = f.read() dialog = NewScriptDialog(self, self.main_window, script) dialog.show() except Exception as e: self.output.setPlainText(traceback.print_exc()) pass else: self.pipeline_script = pipeline self.editor.setPlainText(pipeline.script) self.reload() def save(self, file_path=None, save_as=False): if self.pipeline_script is None: return if save_as: file_path = QFileDialog.getSaveFileName(self, caption="Select Path", filter="*.py")[0] self.pipeline_script.script = self.editor.toPlainText().replace( "\t", " ") self.pipeline_script.save_script(file_path) def reload(self): self.pipeline_script.script = self.editor.toPlainText().replace( "\t", " ") self.pipeline_script.save_script() self.editor.setPlainText(self.pipeline_script.script) message = self.pipeline_script.import_pipeline() self.output.setPlainText(message) self.onReload.emit() @pyqtSlot(str) def print_exception(self, e): self.output.setPlainText(e) self.raise_()
class BusMonitorWindow(QMainWindow): DEFAULT_PLOT_X_RANGE = 120 BUS_LOAD_PLOT_MAX_SAMPLES = 50000 def __init__(self, get_frame, iface_name): super(BusMonitorWindow, self).__init__() self.setWindowTitle('CAN bus monitor (%s)' % iface_name.split(os.path.sep)[-1]) self.setWindowIcon(get_app_icon()) # get dsdl_directory from parent process, if set dsdl_directory = os.environ.get('UAVCAN_CUSTOM_DSDL_PATH', None) if dsdl_directory: uavcan.load_dsdl(dsdl_directory) self._get_frame = get_frame self._log_widget = RealtimeLogWidget(self, columns=COLUMNS, font=get_monospace_font(), pre_redraw_hook=self._redraw_hook) self._log_widget.on_selection_changed = self._update_measurement_display self._log_widget.table.cellClicked.connect( lambda row, col: self._decode_transfer_at_row(row)) self._log_widget.table.setContextMenuPolicy(Qt.CustomContextMenu) self._log_widget.table.customContextMenuRequested.connect( self._context_menu_requested) self._stat_display = QLabel('0 / 0 / 0', self) stat_display_label = QLabel('TX / RX / FPS: ', self) stat_display_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self._log_widget.custom_area_layout.addWidget(stat_display_label) self._log_widget.custom_area_layout.addWidget(self._stat_display) def flip_row_mark(row, col): if col == 0: item = self._log_widget.table.item(row, col) if item.icon().isNull(): item.setIcon(get_icon('circle')) flash(self, 'Row %d was marked, click again to unmark', row, duration=3) else: item.setIcon(QIcon()) self._log_widget.table.cellPressed.connect(flip_row_mark) self._stat_update_timer = QTimer(self) self._stat_update_timer.setSingleShot(False) self._stat_update_timer.timeout.connect(self._update_stat) self._stat_update_timer.start(500) self._traffic_stat = TrafficStatCounter() self._decoded_message_box = QPlainTextEdit(self) self._decoded_message_box.setReadOnly(True) self._decoded_message_box.setFont(get_monospace_font()) self._decoded_message_box.setVerticalScrollBarPolicy( Qt.ScrollBarAsNeeded) self._decoded_message_box.setHorizontalScrollBarPolicy( Qt.ScrollBarAsNeeded) self._decoded_message_box.setPlainText( 'Click on a row to see decoded transfer') self._decoded_message_box.setLineWrapMode(QPlainTextEdit.NoWrap) self._decoded_message_box.setWordWrapMode(QTextOption.NoWrap) self._load_plot = PlotWidget(background=(0, 0, 0)) self._load_plot.setRange(xRange=(0, self.DEFAULT_PLOT_X_RANGE), padding=0) self._load_plot.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self._load_plot.showGrid(x=True, y=True, alpha=0.4) self._load_plot.setToolTip('Frames per second') self._load_plot.getPlotItem().getViewBox().setMouseEnabled(x=True, y=False) self._load_plot.enableAutoRange() self._bus_load_plot = self._load_plot.plot(name='Frames per second', pen=mkPen(QColor( Qt.lightGray), width=1)) self._bus_load_samples = [], [] self._started_at_mono = time.monotonic() self._footer_splitter = QSplitter(Qt.Horizontal, self) self._footer_splitter.addWidget(self._decoded_message_box) self._decoded_message_box.setMinimumWidth(400) self._footer_splitter.addWidget(self._load_plot) self._load_plot.setMinimumWidth(200) splitter = QSplitter(Qt.Vertical, self) splitter.addWidget(self._log_widget) self._log_widget.setMinimumHeight(200) splitter.addWidget(self._footer_splitter) widget = QWidget(self) layout = QHBoxLayout(widget) layout.addWidget(splitter) widget.setLayout(layout) self.setCentralWidget(widget) self.setMinimumWidth(700) self.resize(800, 600) # Calling directly from the constructor gets you wrong size information # noinspection PyCallByClass,PyTypeChecker QTimer.singleShot(500, self._update_widget_sizes) def _update_widget_sizes(self): max_footer_height = self.centralWidget().height() * 0.4 self._footer_splitter.setMaximumHeight(max_footer_height) def resizeEvent(self, qresizeevent): super(BusMonitorWindow, self).resizeEvent(qresizeevent) self._update_widget_sizes() def _update_stat(self): bus_load, ts_mono = self._traffic_stat.get_frames_per_second() if len(self._bus_load_samples[0]) >= self.BUS_LOAD_PLOT_MAX_SAMPLES: self._bus_load_samples[0].pop(0) self._bus_load_samples[1].pop(0) self._bus_load_samples[1].append(bus_load) self._bus_load_samples[0].append(ts_mono - self._started_at_mono) self._bus_load_plot.setData(*self._bus_load_samples) (xmin, xmax), _ = self._load_plot.viewRange() diff = xmax - xmin xmax = self._bus_load_samples[0][-1] xmin = self._bus_load_samples[0][-1] - diff self._load_plot.setRange(xRange=(xmin, xmax), padding=0) def _redraw_hook(self): while True: item = self._get_frame() if item is None: break direction, frame = item self._traffic_stat.add_frame(direction, frame) # There is no need to maintain a second queue actually; should be refactored self._log_widget.add_item_async((direction, frame)) bus_load, _ = self._traffic_stat.get_frames_per_second() self._stat_display.setText( '%d / %d / %d' % (self._traffic_stat.tx, self._traffic_stat.rx, bus_load)) def _decode_transfer_at_row(self, row): try: rows, text = decode_transfer_from_frame( row, partial(row_to_frame, self._log_widget.table)) except Exception as ex: text = 'Transfer could not be decoded:\n' + str(ex) rows = [row] self._decoded_message_box.setPlainText(text.strip()) def _update_measurement_display(self, selected_rows_cols): if not selected_rows_cols: return min_row = min([row for row, _ in selected_rows_cols]) max_row = max([row for row, _ in selected_rows_cols]) if min_row == max_row: self._decode_transfer_at_row(min_row) def get_ts_diff(row_earlier, row_later): e = self._log_widget.table.item(row_earlier, 1).text() l = self._log_widget.table.item(row_later, 1).text() return TimestampRenderer.compute_timestamp_difference(e, l) def get_load_str(num_frames, dt): if dt >= 1e-6: return 'average load %.1f FPS' % (max(num_frames - 1, 1) / dt) return 'average load is unknown' if min_row == max_row: num_frames = min_row dt = get_ts_diff(0, min_row) flash(self, '%d frames from beginning, %.3f sec since first frame, %s', num_frames, dt, get_load_str(num_frames, dt)) else: num_frames = max_row - min_row + 1 dt = get_ts_diff(min_row, max_row) flash(self, '%d frames, timedelta %.6f sec, %s', num_frames, dt, get_load_str(num_frames, dt)) def _context_menu_requested(self, pos): menu = QMenu(self) row_index = self._log_widget.table.rowAt(pos.y()) if row_index >= 0: action_show_definition = QAction(get_icon('file-code-o'), 'Open data type &definition', self) action_show_definition.triggered.connect( lambda: self._show_data_type_definition(row_index)) menu.addAction(action_show_definition) menu.popup(self._log_widget.table.mapToGlobal(pos)) def _show_data_type_definition(self, row): try: data_type_name = self._log_widget.table.item( row, self._log_widget.table.columnCount() - 1).text() definition = uavcan.TYPENAMES[data_type_name].source_text except Exception as ex: show_error('Data type lookup error', 'Could not load data type definition', ex, self) return win = QDialog(self) win.setAttribute(Qt.WA_DeleteOnClose) view = QPlainTextEdit(win) view.setReadOnly(True) view.setFont(get_monospace_font()) view.setPlainText(definition) view.setLineWrapMode(QPlainTextEdit.NoWrap) layout = QVBoxLayout(win) layout.addWidget(view) win.setWindowTitle('Data type definition [%s]' % data_type_name) win.setLayout(layout) win.resize(600, 300) win.show()