示例#1
0
文件: window.py 项目: UAVCAN/gui_tool
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()
示例#2
0
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_()
示例#3
0
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()