Example #1
0
    def create_GUI(self):
        self.pbtn_step_activate = create_Toggle_button("Disabled")
        self.qled_step_size = QtWid.QLineEdit("1.0",
                                              alignment=QtCore.Qt.AlignRight)
        self.qled_step_size.setFixedWidth(70)

        grid_sub = QtWid.QGridLayout()
        grid_sub.addWidget(self.pbtn_step_activate, 0, 0, 1, 3)
        grid_sub.addWidget(QtWid.QLabel("Step size"), 1, 0)
        grid_sub.addWidget(self.qled_step_size, 1, 1)
        grid_sub.addWidget(QtWid.QLabel("mm"), 1, 2)

        font_1 = QtGui.QFont("", 20)
        font_2 = QtGui.QFont("", 30)
        p1 = {"font": font_1, "enabled": False}
        p2 = {"font": font_2, "enabled": False}
        self.pbtn_step_up = QtWid.QPushButton(chr(0x25B2), **p1)
        self.pbtn_step_up.setFixedSize(50, 50)
        self.pbtn_step_down = QtWid.QPushButton(chr(0x25BC), **p1)
        self.pbtn_step_down.setFixedSize(50, 50)
        self.pbtn_step_left = QtWid.QPushButton(chr(0x25C0), **p2)
        self.pbtn_step_left.setFixedSize(50, 50)
        self.pbtn_step_right = QtWid.QPushButton(chr(0x25B6), **p2)
        self.pbtn_step_right.setFixedSize(50, 50)

        self.pted_focus_trap = QtWid.QPlainTextEdit("", enabled=False)
        self.pted_focus_trap.setFixedSize(0, 0)

        # fmt: off
        grid = QtWid.QGridLayout()
        grid.addLayout(grid_sub, 0, 0, 1, 4)
        grid.addItem(QtWid.QSpacerItem(1, 12), 1, 0)
        grid.addWidget(self.pbtn_step_up, 2, 1, QtCore.Qt.AlignHCenter)
        grid.addWidget(self.pbtn_step_left, 3, 0, QtCore.Qt.AlignHCenter)
        grid.addWidget(self.pbtn_step_right, 3, 2, QtCore.Qt.AlignHCenter)
        grid.addWidget(self.pbtn_step_down, 4, 1, QtCore.Qt.AlignHCenter)
        grid.addWidget(self.pted_focus_trap, 5, 1, QtCore.Qt.AlignHCenter)
        # fmt: on

        self.grpb = QtWid.QGroupBox("Move single step")
        self.grpb.eventFilter = self.eventFilter
        self.grpb.installEventFilter(self.grpb)
        self.grpb.setStyleSheet(SS_GROUP)
        self.grpb.setLayout(grid)
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)

        self.setWindowTitle("Arduino & PyQt multithread demo")
        self.setStyleSheet(SS_TEXTBOX_READ_ONLY + SS_GROUP)

        if USE_LARGER_TEXT:
            self.setGeometry(50, 50, 1024, 768)
        else:
            self.setGeometry(350, 50, 960, 660)

        # -------------------------
        #   Top frame
        # -------------------------

        # Left box
        self.qlbl_update_counter = QtWid.QLabel("0")
        self.qlbl_DAQ_rate = QtWid.QLabel("DAQ: nan Hz")
        self.qlbl_DAQ_rate.setStyleSheet("QLabel {min-width: 7em}")

        vbox_left = QtWid.QVBoxLayout()
        vbox_left.addWidget(self.qlbl_update_counter, stretch=0)
        vbox_left.addStretch(1)
        vbox_left.addWidget(self.qlbl_DAQ_rate, stretch=0)

        # Middle box
        self.qlbl_title = QtWid.QLabel(
            "Arduino & PyQt multithread demo",
            font=QtGui.QFont(
                "Palatino",
                20 if USE_LARGER_TEXT else 14,
                weight=QtGui.QFont.Bold,
            ),
        )
        self.qlbl_title.setAlignment(QtCore.Qt.AlignCenter)
        self.qlbl_cur_date_time = QtWid.QLabel("00-00-0000    00:00:00")
        self.qlbl_cur_date_time.setAlignment(QtCore.Qt.AlignCenter)
        self.qpbt_record = create_Toggle_button(
            "Click to start recording to file")
        self.qpbt_record.clicked.connect(lambda state: log.record(state))

        vbox_middle = QtWid.QVBoxLayout()
        vbox_middle.addWidget(self.qlbl_title)
        vbox_middle.addWidget(self.qlbl_cur_date_time)
        vbox_middle.addWidget(self.qpbt_record)

        # Right box
        self.qpbt_exit = QtWid.QPushButton("Exit")
        self.qpbt_exit.clicked.connect(self.close)
        self.qpbt_exit.setMinimumHeight(30)
        self.qlbl_recording_time = QtWid.QLabel(alignment=QtCore.Qt.AlignRight)

        vbox_right = QtWid.QVBoxLayout()
        vbox_right.addWidget(self.qpbt_exit, stretch=0)
        vbox_right.addStretch(1)
        vbox_right.addWidget(self.qlbl_recording_time, stretch=0)

        # Round up top frame
        hbox_top = QtWid.QHBoxLayout()
        hbox_top.addLayout(vbox_left, stretch=0)
        hbox_top.addStretch(1)
        hbox_top.addLayout(vbox_middle, stretch=0)
        hbox_top.addStretch(1)
        hbox_top.addLayout(vbox_right, stretch=0)

        # -------------------------
        #   Bottom frame
        # -------------------------

        # GraphicsLayoutWidget
        self.gw = pg.GraphicsLayoutWidget()
        self.plot = self.gw.addPlot()

        p = {
            "color": "#EEE",
            "font-size": "20pt" if USE_LARGER_TEXT else "10pt",
        }
        self.plot.setClipToView(True)
        self.plot.showGrid(x=1, y=1)
        self.plot.setLabel("bottom", text="history (sec)", **p)
        self.plot.setLabel("left", text="amplitude", **p)
        self.plot.setRange(
            xRange=[-1.04 * CHART_HISTORY_TIME, CHART_HISTORY_TIME * 0.04],
            yRange=[-1.1, 1.1],
            disableAutoRange=True,
        )

        if USE_LARGER_TEXT:
            font = QtGui.QFont()
            font.setPixelSize(26)
            self.plot.getAxis("bottom").setTickFont(font)
            self.plot.getAxis("bottom").setStyle(tickTextOffset=20)
            self.plot.getAxis("bottom").setHeight(90)
            self.plot.getAxis("left").setTickFont(font)
            self.plot.getAxis("left").setStyle(tickTextOffset=20)
            self.plot.getAxis("left").setWidth(120)

        self.history_chart_curve = HistoryChartCurve(
            capacity=round(CHART_HISTORY_TIME * 1e3 / DAQ_INTERVAL_MS),
            linked_curve=self.plot.plot(
                pen=pg.mkPen(color=[255, 255, 0], width=3)),
        )

        # 'Readings'
        p = {"readOnly": True, "maximumWidth": 7 * em}
        self.qlin_reading_t = QtWid.QLineEdit(**p)
        self.qlin_reading_1 = QtWid.QLineEdit(**p)
        self.qpbt_running = create_Toggle_button("Running", checked=True)
        self.qpbt_running.clicked.connect(
            lambda state: self.qpbt_running.setText("Running"
                                                    if state else "Run"))

        # fmt: off
        grid = QtWid.QGridLayout()
        grid.addWidget(self.qpbt_running, 0, 0, 1, 2)
        grid.addWidget(QtWid.QLabel("time"), 1, 0)
        grid.addWidget(self.qlin_reading_t, 1, 1)
        grid.addWidget(QtWid.QLabel("#01"), 2, 0)
        grid.addWidget(self.qlin_reading_1, 2, 1)
        grid.setAlignment(QtCore.Qt.AlignTop)
        # fmt: on

        qgrp_readings = QtWid.QGroupBox("Readings")
        qgrp_readings.setLayout(grid)

        # 'Wave type'
        self.qpbt_wave_sine = QtWid.QPushButton("Sine")
        self.qpbt_wave_sine.clicked.connect(self.process_qpbt_wave_sine)
        self.qpbt_wave_square = QtWid.QPushButton("Square")
        self.qpbt_wave_square.clicked.connect(self.process_qpbt_wave_square)
        self.qpbt_wave_sawtooth = QtWid.QPushButton("Sawtooth")
        self.qpbt_wave_sawtooth.clicked.connect(
            self.process_qpbt_wave_sawtooth)

        # fmt: off
        grid = QtWid.QGridLayout()
        grid.addWidget(self.qpbt_wave_sine, 0, 0)
        grid.addWidget(self.qpbt_wave_square, 1, 0)
        grid.addWidget(self.qpbt_wave_sawtooth, 2, 0)
        grid.setAlignment(QtCore.Qt.AlignTop)
        # fmt: on

        qgrp_wave_type = QtWid.QGroupBox("Wave type")
        qgrp_wave_type.setLayout(grid)

        # 'Chart'
        self.plot_manager = PlotManager(parent=self)
        self.plot_manager.add_autorange_buttons(linked_plots=self.plot)
        self.plot_manager.add_preset_buttons(
            linked_plots=self.plot,
            linked_curves=self.history_chart_curve,
            presets=[
                {
                    "button_label": "0.100",
                    "x_axis_label": "history (msec)",
                    "x_axis_divisor": 1e-3,
                    "x_axis_range": (-101, 0),
                },
                {
                    "button_label": "0:05",
                    "x_axis_label": "history (sec)",
                    "x_axis_divisor": 1,
                    "x_axis_range": (-5.05, 0),
                },
                {
                    "button_label": "0:10",
                    "x_axis_label": "history (sec)",
                    "x_axis_divisor": 1,
                    "x_axis_range": (-10.1, 0),
                },
            ],
        )
        self.plot_manager.add_clear_button(
            linked_curves=self.history_chart_curve)
        self.plot_manager.perform_preset(1)

        qgrp_chart = QtWid.QGroupBox("Chart")
        qgrp_chart.setLayout(self.plot_manager.grid)

        vbox = QtWid.QVBoxLayout()
        vbox.addWidget(qgrp_readings)
        vbox.addWidget(qgrp_wave_type)
        vbox.addWidget(qgrp_chart)
        vbox.addStretch()

        # Round up bottom frame
        hbox_bot = QtWid.QHBoxLayout()
        hbox_bot.addWidget(self.gw, 1)
        hbox_bot.addLayout(vbox, 0)

        # -------------------------
        #   Round up full window
        # -------------------------

        vbox = QtWid.QVBoxLayout(self)
        vbox.addLayout(hbox_top, stretch=0)
        vbox.addSpacerItem(QtWid.QSpacerItem(0, 10))
        vbox.addLayout(hbox_bot, stretch=1)
Example #3
0
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)

        self.setWindowTitle("Keysight 3497xA control")
        self.setGeometry(600, 120, 1200, 600)
        self.setStyleSheet(SS_TEXTBOX_READ_ONLY + SS_GROUP)

        # ----------------------------------------------------------------------
        #   Top grid
        # ----------------------------------------------------------------------

        self.qlbl_title = QtWid.QLabel(
            "Keysight 3497xA control",
            font=QtGui.QFont("Palatino", 14, weight=QtGui.QFont.Bold),
        )
        self.qlbl_cur_date_time = QtWid.QLabel("00-00-0000    00:00:00")
        self.qpbt_record = create_Toggle_button(
            "Click to start recording to file", minimumHeight=40)
        self.qpbt_record.setMinimumWidth(400)
        # fmt: off
        self.qpbt_record.clicked.connect(lambda state: log.record(state))  # pylint: disable=unnecessary-lambda
        # fmt: on

        self.qpbt_exit = QtWid.QPushButton("Exit")
        self.qpbt_exit.clicked.connect(self.close)
        self.qpbt_exit.setMinimumHeight(30)

        grid_top = QtWid.QGridLayout()
        # fmt: off
        grid_top.addWidget(self.qlbl_title, 0, 0, QtCore.Qt.AlignCenter)
        grid_top.addWidget(self.qpbt_exit, 0, 2, QtCore.Qt.AlignRight)
        grid_top.addWidget(self.qlbl_cur_date_time, 1, 0,
                           QtCore.Qt.AlignCenter)
        grid_top.addWidget(self.qpbt_record, 2, 0, QtCore.Qt.AlignCenter)
        # fmt: on
        grid_top.setColumnMinimumWidth(0, 420)
        grid_top.setColumnStretch(1, 1)

        # ----------------------------------------------------------------------
        #   Chart: Mux readings
        # ----------------------------------------------------------------------

        # GraphicsLayoutWidget
        self.gw_mux = pg.GraphicsLayoutWidget()

        p = {"color": "#EEE", "font-size": "12pt"}
        self.pi_mux = self.gw_mux.addPlot()
        self.pi_mux.setClipToView(True)
        self.pi_mux.showGrid(x=1, y=1)
        self.pi_mux.setTitle("Mux readings", **p)
        self.pi_mux.setLabel("bottom", "history (min)", **p)
        self.pi_mux.setLabel("left", "misc. units", **p)
        self.pi_mux.setMenuEnabled(True)
        self.pi_mux.enableAutoRange(axis=pg.ViewBox.XAxis, enable=False)
        self.pi_mux.enableAutoRange(axis=pg.ViewBox.YAxis, enable=True)
        self.pi_mux.setAutoVisible(y=True)

        # Placeholder to be populated depending on the number of scan channels
        self.tscurves_mux = list()  # List of `HistoryChartCurve`

        # ----------------------------------------------------------------------
        #   Legend
        # ----------------------------------------------------------------------

        self.qgrp_legend = QtWid.QGroupBox("Legend")

        # ----------------------------------------------------------------------
        #   PlotManager
        # ----------------------------------------------------------------------

        self.plot_manager = PlotManager(parent=self)
        self.plot_manager.add_autorange_buttons(linked_plots=self.pi_mux)
        self.plot_manager.add_preset_buttons(
            linked_plots=self.pi_mux,
            linked_curves=self.tscurves_mux,
            presets=[
                {
                    "button_label": "0:30",
                    "x_axis_label": "history (sec)",
                    "x_axis_divisor": 1,
                    "x_axis_range": (-30, 0),
                },
                {
                    "button_label": "01:00",
                    "x_axis_label": "history (sec)",
                    "x_axis_divisor": 1,
                    "x_axis_range": (-60, 0),
                },
                {
                    "button_label": "03:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-3, 0),
                },
                {
                    "button_label": "05:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-5, 0),
                },
                {
                    "button_label": "10:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-10, 0),
                },
                {
                    "button_label": "30:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-30, 0),
                },
            ],
        )
        self.plot_manager.add_clear_button(linked_curves=self.tscurves_mux)

        qgrp_history = QtWid.QGroupBox("History")
        qgrp_history.setLayout(self.plot_manager.grid)

        # ----------------------------------------------------------------------
        #   Right-panel grid
        # ----------------------------------------------------------------------

        vbox1 = QtWid.QVBoxLayout()
        vbox1.addWidget(self.qgrp_legend,
                        stretch=0,
                        alignment=QtCore.Qt.AlignTop)
        vbox1.addWidget(qgrp_history, stretch=0, alignment=QtCore.Qt.AlignTop)
        vbox1.addStretch(1)

        # ----------------------------------------------------------------------
        #   Round up full window
        # ----------------------------------------------------------------------

        hbox1 = QtWid.QHBoxLayout()
        hbox1.addWidget(mux_qdev.qgrp, stretch=0, alignment=QtCore.Qt.AlignTop)
        hbox1.addWidget(self.gw_mux, stretch=1)
        hbox1.addLayout(vbox1)

        vbox = QtWid.QVBoxLayout(self)
        vbox.addLayout(grid_top)
        vbox.addLayout(hbox1)
        vbox.addStretch(1)
Example #4
0
    def create_GUI(self):
        p = {"alignment": QtCore.Qt.AlignCenter, "font": FONT_MONOSPACE}
        p2 = {"alignment": QtCore.Qt.AlignCenter + QtCore.Qt.AlignVCenter}
        # self.qlbl_mux = QtWid.QLabel("Keysight 34972a", **p2)
        self.qlbl_mux_state = QtWid.QLabel("Offline", **p)
        self.qpbt_start_scan = create_Toggle_button("Start scan")

        self.qpte_SCPI_commands = QtWid.QPlainTextEdit(
            "", readOnly=True, lineWrapMode=False
        )
        self.qpte_SCPI_commands.setStyleSheet(SS_TEXTBOX_READ_ONLY)
        self.qpte_SCPI_commands.setMaximumHeight(152)
        self.qpte_SCPI_commands.setMinimumWidth(200)
        self.qpte_SCPI_commands.setFont(FONT_MONOSPACE_SMALL)

        p = {"alignment": QtCore.Qt.AlignRight, "readOnly": True}
        self.qled_scanning_interval_ms = QtWid.QLineEdit("", **p)
        self.qled_obtained_interval_ms = QtWid.QLineEdit("", **p)

        self.qpte_errors = QtWid.QPlainTextEdit("", lineWrapMode=False)
        self.qpte_errors.setStyleSheet(SS_TEXTBOX_ERRORS)
        self.qpte_errors.setMaximumHeight(90)

        # fmt: off
        self.qpbt_ackn_errors    = QtWid.QPushButton("Acknowledge errors")
        self.qpbt_reinit         = QtWid.QPushButton("Reinitialize")
        self.qlbl_update_counter = QtWid.QLabel("0")
        self.qpbt_debug_test     = QtWid.QPushButton("Debug test")
        # fmt: on

        self.qpbt_start_scan.clicked.connect(self.process_qpbt_start_scan)
        self.qpbt_ackn_errors.clicked.connect(self.process_qpbt_ackn_errors)
        self.qpbt_reinit.clicked.connect(self.process_qpbt_reinit)
        self.qpbt_debug_test.clicked.connect(self.process_qpbt_debug_test)

        i = 0
        p = {"alignment": QtCore.Qt.AlignLeft + QtCore.Qt.AlignVCenter}

        grid = QtWid.QGridLayout()
        grid.setVerticalSpacing(4)
        # fmt: off
        # grid.addWidget(self.qlbl_mux                    , i, 0, 1, 3); i+=1
        grid.addWidget(QtWid.QLabel("Only scan when necessary.", **p2)
                                                          , i, 0, 1, 2); i+=1
        grid.addWidget(QtWid.QLabel("It wears down the multiplexer.", **p2)
                                                          , i, 0, 1, 2); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 3)              , i, 0)      ; i+=1
        grid.addWidget(self.qlbl_mux_state                , i, 0, 1, 2); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 3)              , i, 0)      ; i+=1
        grid.addWidget(self.qpbt_start_scan               , i, 0, 1, 2); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 4)              , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("SCPI scan commands:"), i, 0, 1, 2); i+=1
        grid.addWidget(self.qpte_SCPI_commands            , i, 0, 1, 2); i+=1
        grid.addWidget(QtWid.QLabel("Scanning interval [ms]"), i, 0)
        grid.addWidget(self.qled_scanning_interval_ms     , i, 1)      ; i+=1
        grid.addWidget(QtWid.QLabel("Obtained [ms]")      , i, 0)
        grid.addWidget(self.qled_obtained_interval_ms     , i, 1)      ; i+=1
        grid.addWidget(self.qpbt_reinit                   , i, 0, 1, 2); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 12)             , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("Errors:")            , i, 0, 1, 2); i+=1
        grid.addWidget(self.qpte_errors                   , i, 0, 1, 2); i+=1
        grid.addWidget(self.qpbt_ackn_errors              , i, 0, 1, 2); i+=1
        grid.addWidget(self.qlbl_update_counter           , i, 0, 1, 2); i+=1
        # grid.addWidget(self.qpbt_debug_test             , i, 0, 1, 2); i+=1
        # fmt: on

        #  Table widget containing the readings of the current scan cycle
        # ----------------------------------------------------------------
        self.qtbl_readings = QtWid.QTableWidget(columnCount=1)
        self.qtbl_readings.setHorizontalHeaderLabels(["Readings"])
        self.qtbl_readings.horizontalHeaderItem(0).setFont(FONT_MONOSPACE_SMALL)
        self.qtbl_readings.verticalHeader().setFont(FONT_MONOSPACE_SMALL)
        self.qtbl_readings.verticalHeader().setDefaultSectionSize(24)
        self.qtbl_readings.setFont(FONT_MONOSPACE_SMALL)
        # self.qtbl_readings.setMinimumHeight(600)
        self.qtbl_readings.setFixedWidth(180)
        self.qtbl_readings.setColumnWidth(0, 110)

        grid.addWidget(self.qtbl_readings, 0, 2, i, 1)

        self.qgrp = QtWid.QGroupBox("%s" % self.dev.name)
        self.qgrp.setStyleSheet(SS_GROUP)
        self.qgrp.setLayout(grid)
Example #5
0
    def create_GUI(self):
        # Safety
        p = {
            "alignment": QtCore.Qt.AlignRight,
            "minimumWidth": 60,
            "maximumWidth": 30,
        }
        self.safe_sens = QtWid.QLineEdit("nan", **p, readOnly=True)
        self.safe_temp = QtWid.QLineEdit("nan", **p, readOnly=True)
        self.sub_temp = QtWid.QLineEdit("nan", **p)
        self.sub_temp.editingFinished.connect(self.send_sub_temp_from_textbox)
        self.over_temp = QtWid.QLineEdit("nan", **p)
        self.over_temp.editingFinished.connect(
            self.send_over_temp_from_textbox)

        # Control
        self.pbtn_running = create_Toggle_button("OFFLINE")
        self.pbtn_running.clicked.connect(self.process_pbtn_running)
        self.send_setpoint = QtWid.QLineEdit("nan", **p)
        self.send_setpoint.editingFinished.connect(
            self.send_setpoint_from_textbox)
        self.read_setpoint = QtWid.QLineEdit("nan", **p, readOnly=True)
        self.bath_temp = QtWid.QLineEdit("nan", **p, readOnly=True)
        self.pt100_temp = QtWid.QLineEdit("nan", **p, readOnly=True)
        self.status = QtWid.QPlainTextEdit(maximumWidth=180, readOnly=True)
        self.status.setStyleSheet(SS_TEXTBOX_ERRORS)
        self.update_counter = QtWid.QLabel("0")

        i = 0
        p = {"alignment": QtCore.Qt.AlignLeft}
        lbl_temp_unit = "\u00b0%s" % self.dev.state.temp_unit

        # fmt: off
        grid = QtWid.QGridLayout()
        grid.setVerticalSpacing(4)

        grid.addWidget(QtWid.QLabel("<b>Safety</b>"), i, 0, 1, 3)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 6), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("Safe sensor"), i, 0)
        grid.addWidget(self.safe_sens, i, 1)
        grid.addWidget(QtWid.QLabel(lbl_temp_unit), i, 2)
        i += 1
        grid.addWidget(QtWid.QLabel("Safe temp."), i, 0)
        grid.addWidget(self.safe_temp, i, 1)
        grid.addWidget(QtWid.QLabel(lbl_temp_unit), i, 2)
        i += 1
        grid.addWidget(QtWid.QLabel("Sub temp."), i, 0)
        grid.addWidget(self.sub_temp, i, 1)
        grid.addWidget(QtWid.QLabel(lbl_temp_unit), i, 2)
        i += 1
        grid.addWidget(QtWid.QLabel("Over temp."), i, 0)
        grid.addWidget(self.over_temp, i, 1)
        grid.addWidget(QtWid.QLabel(lbl_temp_unit), i, 2)
        i += 1

        grid.addItem(QtWid.QSpacerItem(1, 10), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("<b>Control</b>"), i, 0, 1, 3)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 6), i, 0)
        i += 1
        grid.addWidget(self.pbtn_running, i, 0, 1, 3)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 8), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("Send setpoint"), i, 0)
        grid.addWidget(self.send_setpoint, i, 1)
        grid.addWidget(QtWid.QLabel(lbl_temp_unit), i, 2)
        i += 1
        grid.addWidget(QtWid.QLabel("Read setpoint"), i, 0)
        grid.addWidget(self.read_setpoint, i, 1)
        grid.addWidget(QtWid.QLabel(lbl_temp_unit), i, 2)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 8), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("Bath temp."), i, 0)
        grid.addWidget(self.bath_temp, i, 1)
        grid.addWidget(QtWid.QLabel(lbl_temp_unit), i, 2)
        i += 1
        grid.addWidget(QtWid.QLabel("Pt100 temp."), i, 0)
        grid.addWidget(self.pt100_temp, i, 1)
        grid.addWidget(QtWid.QLabel(lbl_temp_unit), i, 2)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 8), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("Status"), i, 0, 1, 3)
        i += 1
        grid.addWidget(self.status, i, 0, 1, 3)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 4), i, 0)
        i += 1
        grid.addWidget(self.update_counter, i, 0, 1, 3)
        i += 1
        # fmt: on

        grid.setColumnStretch(0, 0)
        grid.setColumnStretch(1, 0)
        grid.setColumnStretch(2, 1)
        grid.setAlignment(QtCore.Qt.AlignTop)
        self.grid = grid

        self.grpb = QtWid.QGroupBox("%s" % self.dev.name)
        self.grpb.setLayout(self.grid)
    def create_GUI(self):
        # Measure
        p = {"alignment": QtCore.Qt.AlignRight, "font": FONT_MONOSPACE}
        self.V_meas = QtWid.QLabel("0.00  V   ", **p)
        self.I_meas = QtWid.QLabel("0.000 A   ", **p)
        self.P_meas = QtWid.QLabel("0.00  W   ", **p)

        # Source
        p = {"maximumWidth": 60, "alignment": QtCore.Qt.AlignRight}
        self.pbtn_ENA_output = create_Toggle_button("Output OFF")
        self.V_source = QtWid.QLineEdit("0.00", **p)
        self.I_source = QtWid.QLineEdit("0.000", **p)
        self.P_source = QtWid.QLineEdit("0.00", **p)
        self.pbtn_ENA_PID = create_Toggle_button(
            "OFF", minimumHeight=28, minimumWidth=60
        )

        # Protection
        self.OVP_level = QtWid.QLineEdit("0.000", **p)
        self.pbtn_ENA_OCP = create_Toggle_button(
            "OFF", minimumHeight=28, minimumWidth=60
        )

        # Questionable condition status registers
        self.status_QC_OV = create_tiny_error_LED()
        self.status_QC_OC = create_tiny_error_LED()
        self.status_QC_PF = create_tiny_error_LED()
        self.status_QC_OT = create_tiny_error_LED()
        self.status_QC_INH = create_tiny_error_LED()
        self.status_QC_UNR = create_tiny_error_LED()

        # Operation condition status registers
        self.status_OC_WTG = create_tiny_error_LED()
        self.status_OC_CV = create_tiny_error_LED()
        self.status_OC_CC = create_tiny_error_LED()

        # Final elements
        self.errors = QtWid.QLineEdit("")
        self.errors.setStyleSheet(SS_TEXTBOX_ERRORS)
        self.pbtn_ackn_errors = QtWid.QPushButton("Acknowledge errors")
        self.pbtn_reinit = QtWid.QPushButton("Reinitialize")
        self.pbtn_save_defaults = QtWid.QPushButton("Save")
        self.pbtn_debug_test = QtWid.QPushButton("Debug test")
        self.lbl_update_counter = QtWid.QLabel("0")

        i = 0
        p = {"alignment": QtCore.Qt.AlignLeft + QtCore.Qt.AlignVCenter}

        grid = QtWid.QGridLayout()
        grid.setVerticalSpacing(0)
        # fmt: off
        grid.addWidget(self.V_meas                       , i, 0, 1, 4); i+=1
        grid.addWidget(self.I_meas                       , i, 0, 1, 4); i+=1
        grid.addWidget(self.P_meas                       , i, 0, 1, 4); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 8)             , i, 0)      ; i+=1
        grid.addWidget(self.pbtn_ENA_output              , i, 0, 1, 4); i+=1

        grid.addItem(QtWid.QSpacerItem(1, 10)            , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("Source:")           , i, 0, 1, 4); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 4)             , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("Voltage")           , i, 0, 1, 2)
        grid.addWidget(self.V_source                     , i, 2)
        grid.addWidget(QtWid.QLabel("V", **p)            , i, 3)      ; i+=1
        grid.addItem(QtWid.QSpacerItem(1, 2)             , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("Current")           , i, 0, 1, 2)
        grid.addWidget(self.I_source                     , i, 2)
        grid.addWidget(QtWid.QLabel("A", **p)            , i, 3)      ; i+=1
        grid.addItem(QtWid.QSpacerItem(1, 2)             , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("Power PID")         , i, 0, 1, 2)
        grid.addWidget(self.pbtn_ENA_PID                 , i, 2,
                                                  QtCore.Qt.AlignLeft); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 2)             , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("Power")             , i, 0, 1, 2)
        grid.addWidget(self.P_source                     , i, 2)
        grid.addWidget(QtWid.QLabel("W", **p)            , i, 3)      ; i+=1

        grid.addItem(QtWid.QSpacerItem(1, 10)            , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("Protection:")       , i, 0, 1, 4); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 4)             , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("OVP")               , i, 0, 1, 2)
        grid.addWidget(self.OVP_level                    , i, 2)
        grid.addWidget(QtWid.QLabel("V", **p)            , i, 3)      ; i+=1
        grid.addItem(QtWid.QSpacerItem(1, 2)             , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("OCP")               , i, 0, 1, 2)
        grid.addWidget(self.pbtn_ENA_OCP                 , i, 2,
                                                  QtCore.Qt.AlignLeft); i+=1

        grid.addItem(QtWid.QSpacerItem(1, 10)            , i, 0)      ; i+=1
        grid.addWidget(self.status_QC_OV                 , i, 0)
        grid.addWidget(QtWid.QLabel("OV")                , i, 1)
        grid.addWidget(QtWid.QLabel("| over-voltage")    , i, 2, 1, 2); i+=1
        grid.addWidget(self.status_QC_OC                 , i, 0)
        grid.addWidget(QtWid.QLabel("OC")                , i, 1)
        grid.addWidget(QtWid.QLabel("| over-current")    , i, 2, 1, 2); i+=1
        grid.addWidget(self.status_QC_PF                 , i, 0)
        grid.addWidget(QtWid.QLabel("PF")                , i, 1)
        grid.addWidget(QtWid.QLabel("| AC power failure"), i, 2, 1, 2); i+=1
        grid.addWidget(self.status_QC_OT                 , i, 0)
        grid.addWidget(QtWid.QLabel("OT")                , i, 1)
        grid.addWidget(QtWid.QLabel("| over-temperature"), i, 2, 1, 2); i+=1
        grid.addWidget(self.status_QC_INH                , i, 0)
        grid.addWidget(QtWid.QLabel("INH")               , i, 1)
        grid.addWidget(QtWid.QLabel("| output inhibited"), i, 2, 1, 2); i+=1
        grid.addWidget(self.status_QC_UNR                , i, 0)
        grid.addWidget(QtWid.QLabel("UNR")               , i, 1)
        grid.addWidget(QtWid.QLabel("| unregulated")     , i, 2, 1, 2); i+=1

        grid.addItem(QtWid.QSpacerItem(1, 10)            , i, 0)      ; i+=1
        grid.addWidget(self.status_OC_WTG                , i, 0)
        grid.addWidget(QtWid.QLabel("WTG")               , i, 1)
        grid.addWidget(QtWid.QLabel("| waiting for trigger"), i, 2, 1, 2); i+=1
        grid.addWidget(self.status_OC_CV                 , i, 0)
        grid.addWidget(QtWid.QLabel("CV")                , i, 1)
        grid.addWidget(QtWid.QLabel("| constant voltage"), i, 2, 1, 2); i+=1
        grid.addWidget(self.status_OC_CC                 , i, 0)
        grid.addWidget(QtWid.QLabel("CC")                , i, 1)
        grid.addWidget(QtWid.QLabel("| constant current"), i, 2, 1, 2); i+=1

        grid.addItem(QtWid.QSpacerItem(1, 10)            , i, 0)      ; i+=1
        grid.addWidget(QtWid.QLabel("Errors")            , i, 0, 1, 2)
        grid.addWidget(self.errors                       , i, 2, 1, 2); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 4)             , i, 0)      ; i+=1
        grid.addWidget(self.pbtn_ackn_errors             , i, 0, 1, 4); i+=1

        hbox = QtWid.QHBoxLayout()
        hbox.addWidget(self.pbtn_save_defaults)
        hbox.addWidget(self.pbtn_reinit)
        grid.addLayout(hbox                              , i, 0, 1, 4); i+=1
        grid.addItem(QtWid.QSpacerItem(1, 4)             , i, 0)      ; i+=1
        grid.addWidget(self.lbl_update_counter           , i, 0, 1, 4); i+=1
        # grid.addWidget(self.pbtn_debug_test              , i, 0, 1, 4); i+=1
        # fmt: on

        grid.setColumnStretch(0, 0)
        grid.setColumnStretch(1, 0)
        grid.setColumnStretch(2, 0)
        grid.setColumnStretch(3, 1)
        grid.setAlignment(QtCore.Qt.AlignTop)
        # grid.setAlignment(QtCore.Qt.AlignLeft)
        self.grid = grid

        self.grpb = QtWid.QGroupBox("%s" % self.dev.name)
        self.grpb.setStyleSheet(SS_GROUP)
        self.grpb.setLayout(self.grid)
    def create_GUI(self):
        # Measure
        p = {"alignment": QtCore.Qt.AlignCenter, "font": FONT_MONOSPACE}
        self.V_meas = QtWid.QLabel(" 0.000 V", **p)
        self.I_meas = QtWid.QLabel(" 0.000 A", **p)
        self.P_meas = QtWid.QLabel(" 0.000 W", **p)

        # Source
        p = {"maximumWidth": 60, "alignment": QtCore.Qt.AlignRight}
        self.pbtn_ENA_output = create_Toggle_button("Output OFF")
        self.pbtn_ENA_output.clicked.connect(self.process_pbtn_ENA_output)
        self.V_source = QtWid.QLineEdit("0.000", **p)
        self.V_source.editingFinished.connect(self.send_V_source_from_textbox)
        self.I_source = QtWid.QLineEdit("0.000", **p)
        self.I_source.editingFinished.connect(self.send_I_source_from_textbox)

        # Protection
        self.OVP_level = QtWid.QLineEdit("0.0", **p)
        self.OVP_level.editingFinished.connect(
            self.send_OVP_level_from_textbox)
        self.OCP_level = QtWid.QLineEdit("0.00", **p)
        self.OCP_level.editingFinished.connect(
            self.send_OCP_level_from_textbox)

        # Trips
        # self.status_LSR_TRIP_AUX = create_tiny_error_LED()
        self.status_LSR_TRIP_SENSE = create_tiny_error_LED()
        self.status_LSR_TRIP_OTP = create_tiny_error_LED()
        self.status_LSR_TRIP_OCP = create_tiny_error_LED()
        self.status_LSR_TRIP_OVP = create_tiny_error_LED()

        # Modes
        # self.status_LSR_MODE_AUX_CC = create_tiny_error_LED()
        self.status_LSR_MODE_CC = create_tiny_error_LED()
        self.status_LSR_MODE_CV = create_tiny_error_LED()

        # Final elements
        self.pbtn_reset_trips = QtWid.QPushButton("Reset trips")
        self.pbtn_reset_trips.clicked.connect(self.process_pbtn_reset_trips)
        self.pbtn_save_settings = QtWid.QPushButton("Save settings")
        self.pbtn_save_settings.clicked.connect(
            self.process_pbtn_save_settings)
        self.pbtn_load_settings = QtWid.QPushButton("Load settings")
        self.pbtn_load_settings.clicked.connect(
            self.process_pbtn_load_settings)
        self.lbl_update_counter = QtWid.QLabel("0")

        i = 0
        p = {"alignment": QtCore.Qt.AlignLeft + QtCore.Qt.AlignVCenter}

        grid = QtWid.QGridLayout()
        grid.setVerticalSpacing(0)
        # fmt: off
        grid.addWidget(self.V_meas, i, 0, 1, 4)
        i += 1
        grid.addWidget(self.I_meas, i, 0, 1, 4)
        i += 1
        grid.addWidget(self.P_meas, i, 0, 1, 4)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 8), i, 0)
        i += 1
        grid.addWidget(self.pbtn_ENA_output, i, 0, 1, 4)
        i += 1

        grid.addItem(QtWid.QSpacerItem(1, 10), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("Source:"), i, 0, 1, 4)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 4), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("Voltage"), i, 0, 1, 2)
        grid.addWidget(self.V_source, i, 2)
        grid.addWidget(QtWid.QLabel("V", **p), i, 3)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 2), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("Current"), i, 0, 1, 2)
        grid.addWidget(self.I_source, i, 2)
        grid.addWidget(QtWid.QLabel("A", **p), i, 3)
        i += 1

        grid.addItem(QtWid.QSpacerItem(1, 10), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("Protection:"), i, 0, 1, 4)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 4), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("OVP"), i, 0, 1, 2)
        grid.addWidget(self.OVP_level, i, 2)
        grid.addWidget(QtWid.QLabel("V", **p), i, 3)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 2), i, 0)
        i += 1
        grid.addWidget(QtWid.QLabel("OCP"), i, 0, 1, 2)
        grid.addWidget(self.OCP_level, i, 2)
        grid.addWidget(QtWid.QLabel("A", **p), i, 3)
        i += 1

        grid.addItem(QtWid.QSpacerItem(1, 10), i, 0)
        i += 1
        grid.addWidget(self.status_LSR_TRIP_OVP, i, 0)
        grid.addWidget(QtWid.QLabel("over-voltage trip"), i, 1, 1, 3)
        i += 1
        grid.addWidget(self.status_LSR_TRIP_OCP, i, 0)
        grid.addWidget(QtWid.QLabel("over-current trip"), i, 1, 1, 3)
        i += 1
        grid.addWidget(self.status_LSR_TRIP_OTP, i, 0)
        grid.addWidget(QtWid.QLabel("over-temperature trip"), i, 1, 1, 3)
        i += 1
        grid.addWidget(self.status_LSR_TRIP_SENSE, i, 0)
        grid.addWidget(QtWid.QLabel("sense trip"), i, 1, 1, 3)
        i += 1

        grid.addItem(QtWid.QSpacerItem(1, 10), i, 0)
        i += 1
        grid.addWidget(self.status_LSR_MODE_CV, i, 0)
        grid.addWidget(QtWid.QLabel("constant-voltage mode"), i, 1, 1, 3)
        i += 1
        grid.addWidget(self.status_LSR_MODE_CC, i, 0)
        grid.addWidget(QtWid.QLabel("constant-current mode"), i, 1, 1, 3)
        i += 1

        grid.addItem(QtWid.QSpacerItem(1, 10), i, 0)
        i += 1
        grid.addWidget(self.pbtn_reset_trips, i, 0, 1, 4)
        i += 1

        grid.addItem(QtWid.QSpacerItem(1, 10), i, 0)
        i += 1
        grid.addWidget(self.pbtn_save_settings, i, 0, 1, 4)
        i += 1
        grid.addWidget(self.pbtn_load_settings, i, 0, 1, 4)
        i += 1
        grid.addItem(QtWid.QSpacerItem(1, 4), i, 0)
        i += 1
        grid.addWidget(self.lbl_update_counter, i, 0, 1, 4)
        i += 1
        # fmt: on

        grid.setColumnStretch(0, 0)
        grid.setColumnStretch(1, 0)
        grid.setColumnStretch(2, 0)
        grid.setColumnStretch(3, 1)
        grid.setAlignment(QtCore.Qt.AlignTop)
        # grid.setAlignment(QtCore.Qt.AlignLeft)
        self.grid = grid

        self.grpb = QtWid.QGroupBox("%s" % self.dev.name)
        self.grpb.setLayout(self.grid)
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)

        self.setWindowTitle("Dodecahedron control")
        self.setGeometry(350, 60, 1200, 900)
        self.setStyleSheet(SS_TEXTBOX_READ_ONLY + SS_GROUP)

        # -------------------------
        #   Top frame
        # -------------------------

        # Left box
        self.qlbl_update_counter = QtWid.QLabel("0")
        self.qlbl_DAQ_rate = QtWid.QLabel("DAQ: nan Hz")
        self.qlbl_DAQ_rate.setStyleSheet("QLabel {min-width: 7em}")

        vbox_left = QtWid.QVBoxLayout()
        vbox_left.addWidget(self.qlbl_update_counter, stretch=0)
        vbox_left.addStretch(1)
        vbox_left.addWidget(self.qlbl_DAQ_rate, stretch=0)

        # Middle box
        self.qlbl_title = QtWid.QLabel(
            "Dodecahedron control",
            font=QtGui.QFont("Palatino", 14, weight=QtGui.QFont.Bold),
        )
        self.qlbl_title.setAlignment(QtCore.Qt.AlignCenter)
        self.qlbl_cur_date_time = QtWid.QLabel("00-00-0000    00:00:00")
        self.qlbl_cur_date_time.setAlignment(QtCore.Qt.AlignCenter)
        self.qpbt_record = create_Toggle_button(
            "Click to start recording to file"
        )
        # fmt: off
        self.qpbt_record.clicked.connect(lambda state: log.record(state)) # pylint: disable=unnecessary-lambda
        # fmt: on

        vbox_middle = QtWid.QVBoxLayout()
        vbox_middle.addWidget(self.qlbl_title)
        vbox_middle.addWidget(self.qlbl_cur_date_time)
        vbox_middle.addWidget(self.qpbt_record)

        # Right box
        self.qpbt_exit = QtWid.QPushButton("Exit")
        self.qpbt_exit.clicked.connect(self.close)
        self.qpbt_exit.setMinimumHeight(30)
        self.qlbl_recording_time = QtWid.QLabel(alignment=QtCore.Qt.AlignRight)

        vbox_right = QtWid.QVBoxLayout()
        vbox_right.addWidget(self.qpbt_exit, stretch=0)
        vbox_right.addStretch(1)
        vbox_right.addWidget(self.qlbl_recording_time, stretch=0)

        # Round up top frame
        hbox_top = QtWid.QHBoxLayout()
        hbox_top.addLayout(vbox_left, stretch=0)
        hbox_top.addStretch(1)
        hbox_top.addLayout(vbox_middle, stretch=0)
        hbox_top.addStretch(1)
        hbox_top.addLayout(vbox_right, stretch=0)

        # -------------------------
        #   Bottom frame
        # -------------------------

        #  Charts
        # -------------------------

        self.gw = pg.GraphicsLayoutWidget()

        # Plot: Julabo temperatures
        p = {"color": "#EEE", "font-size": "10pt"}
        self.pi_julabo = self.gw.addPlot(row=0, col=0)
        self.pi_julabo.setLabel("left", text="temperature (°C)", **p)

        # Plot: Arduino temperatures
        self.pi_temp = self.gw.addPlot(row=1, col=0)
        self.pi_temp.setLabel("left", text="temperature (°C)", **p)

        # Plot: Arduino humidity
        self.pi_humi = self.gw.addPlot(row=2, col=0)
        self.pi_humi.setLabel("left", text="humidity (%)", **p)

        # Plot: Arduino pressure
        self.pi_pres = self.gw.addPlot(row=3, col=0)
        self.pi_pres.setLabel("left", text="pressure (mbar)", **p)

        self.plots = [self.pi_julabo, self.pi_temp, self.pi_humi, self.pi_pres]
        for plot in self.plots:
            plot.setClipToView(True)
            plot.showGrid(x=1, y=1)
            plot.setLabel("bottom", text="history (s)", **p)
            plot.setMenuEnabled(True)
            plot.enableAutoRange(axis=pg.ViewBox.XAxis, enable=False)
            plot.enableAutoRange(axis=pg.ViewBox.YAxis, enable=True)
            plot.setAutoVisible(y=True)
            plot.setRange(xRange=[-CHART_HISTORY_TIME, 0])

        # Curves
        capacity = round(CHART_HISTORY_TIME * 1e3 / DAQ_INTERVAL_MS)
        PEN_01 = pg.mkPen(color=[255, 255, 0], width=3)
        PEN_02 = pg.mkPen(color=[252, 15, 192], width=3)
        PEN_03 = pg.mkPen(color=[0, 255, 255], width=3)
        PEN_04 = pg.mkPen(color=[255, 255, 255], width=3)
        PEN_05 = pg.mkPen(color=[255, 127, 39], width=3)
        PEN_06 = pg.mkPen(color=[0, 255, 0], width=3)

        self.tscurve_julabo_setp = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_julabo.plot(pen=PEN_05, name="Julabo setp."),
        )

        self.tscurve_julabo_bath = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_julabo.plot(pen=PEN_06, name="Julabo bath"),
        )

        self.tscurve_ds_temp = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_temp.plot(pen=PEN_01, name="DS temp."),
        )
        self.tscurve_bme_temp = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_temp.plot(pen=PEN_02, name="BME temp."),
        )
        self.tscurve_bme_humi = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_humi.plot(pen=PEN_03, name="BME humi."),
        )
        self.tscurve_bme_pres = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_pres.plot(pen=PEN_04, name="BME pres."),
        )

        self.tscurves = [
            self.tscurve_julabo_setp,
            self.tscurve_julabo_bath,
            self.tscurve_ds_temp,
            self.tscurve_bme_temp,
            self.tscurve_bme_humi,
            self.tscurve_bme_pres,
        ]

        #  Group `Readings`
        # -------------------------

        legend = LegendSelect(
            linked_curves=self.tscurves, hide_toggle_button=True
        )

        p = {
            "readOnly": True,
            "alignment": QtCore.Qt.AlignRight,
            "maximumWidth": 54,
        }
        self.qlin_ds_temp = QtWid.QLineEdit(**p)
        self.qlin_bme_temp = QtWid.QLineEdit(**p)
        self.qlin_bme_humi = QtWid.QLineEdit(**p)
        self.qlin_bme_pres = QtWid.QLineEdit(**p)

        # fmt: off
        legend.grid.setHorizontalSpacing(6)
        legend.grid.addWidget(self.qlin_ds_temp       , 2, 2)
        legend.grid.addWidget(QtWid.QLabel("± 0.5 °C"), 2, 3)
        legend.grid.addWidget(self.qlin_bme_temp      , 3, 2)
        legend.grid.addWidget(QtWid.QLabel("± 0.5 °C"), 3, 3)
        legend.grid.addWidget(self.qlin_bme_humi      , 4, 2)
        legend.grid.addWidget(QtWid.QLabel("± 3 %")   , 4, 3)
        legend.grid.addWidget(self.qlin_bme_pres      , 5, 2)
        legend.grid.addWidget(QtWid.QLabel("± 1 mbar"), 5, 3)
        # fmt: on

        qgrp_readings = QtWid.QGroupBox("Readings")
        qgrp_readings.setLayout(legend.grid)

        #  Group 'Log comments'
        # -------------------------

        self.qtxt_comments = QtWid.QTextEdit()
        grid = QtWid.QGridLayout()
        grid.addWidget(self.qtxt_comments, 0, 0)

        qgrp_comments = QtWid.QGroupBox("Log comments")
        qgrp_comments.setLayout(grid)

        #  Group 'Charts'
        # -------------------------

        self.plot_manager = PlotManager(parent=self)
        self.plot_manager.add_autorange_buttons(linked_plots=self.plots)
        self.plot_manager.add_preset_buttons(
            linked_plots=self.plots,
            linked_curves=self.tscurves,
            presets=[
                {
                    "button_label": "01:00",
                    "x_axis_label": "history (sec)",
                    "x_axis_divisor": 1,
                    "x_axis_range": (-60, 0),
                },
                {
                    "button_label": "10:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-10, 0),
                },
                {
                    "button_label": "30:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-30, 0),
                },
                {
                    "button_label": "60:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-60, 0),
                },
                {
                    "button_label": "120:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-120, 0),
                },
            ],
        )
        self.plot_manager.add_clear_button(linked_curves=self.tscurves)
        self.plot_manager.perform_preset(1)

        qgrp_chart = QtWid.QGroupBox("Charts")
        qgrp_chart.setLayout(self.plot_manager.grid)

        vbox = QtWid.QVBoxLayout()
        vbox.addWidget(qgrp_readings)
        vbox.addWidget(qgrp_comments)
        vbox.addWidget(qgrp_chart, alignment=QtCore.Qt.AlignLeft)
        vbox.addStretch()

        # Round up bottom frame
        hbox_bot = QtWid.QHBoxLayout()
        hbox_bot.addWidget(self.gw, 1)
        hbox_bot.addLayout(vbox, 0)
        hbox_bot.addWidget(
            qdev_julabo.grpb, alignment=QtCore.Qt.AlignTop, stretch=0
        )

        # -------------------------
        #   Round up full window
        # -------------------------

        vbox = QtWid.QVBoxLayout(self)
        vbox.addLayout(hbox_top, stretch=0)
        vbox.addSpacerItem(QtWid.QSpacerItem(0, 10))
        vbox.addLayout(hbox_bot, stretch=1)
Example #9
0
    def create_GUI(self):
        # Groupbox "Alarm values"
        # -----------------------
        p = {
            "alignment": QtCore.Qt.AlignRight,
            "minimumWidth": 50,
            "maximumWidth": 30,
            "readOnly": True,
        }
        self.LO_flow = QtWid.QLineEdit(**p)
        self.HI_flow = QtWid.QLineEdit(**p)
        self.LO_pres = QtWid.QLineEdit(**p)
        self.HI_pres = QtWid.QLineEdit(**p)
        self.LO_temp = QtWid.QLineEdit(**p)
        self.HI_temp = QtWid.QLineEdit(**p)
        self.pbtn_read_alarm_values = QtWid.QPushButton("Read")
        self.pbtn_read_alarm_values.setMinimumSize(50, 30)

        p = {"alignment": QtCore.Qt.AlignCenter}
        grid = QtWid.QGridLayout()
        # fmt: off
        grid.addWidget(
            QtWid.QLabel("Values can be set in the chiller's menu", **p), 0, 0,
            1, 4)
        grid.addWidget(QtWid.QLabel("LO"), 1, 1)
        grid.addWidget(QtWid.QLabel("HI"), 1, 2)
        grid.addWidget(QtWid.QLabel("Flow rate"), 2, 0)
        grid.addWidget(self.LO_flow, 2, 1)
        grid.addWidget(self.HI_flow, 2, 2)
        grid.addWidget(QtWid.QLabel("LPM"), 2, 3)
        grid.addWidget(QtWid.QLabel("Pressure"), 3, 0)
        grid.addWidget(self.LO_pres, 3, 1)
        grid.addWidget(self.HI_pres, 3, 2)
        grid.addWidget(QtWid.QLabel("bar"), 3, 3)
        grid.addWidget(QtWid.QLabel("Temperature"), 4, 0)
        grid.addWidget(self.LO_temp, 4, 1)
        grid.addWidget(self.HI_temp, 4, 2)
        grid.addWidget(QtWid.QLabel(CHAR_DEG_C), 4, 3)
        grid.addWidget(self.pbtn_read_alarm_values, 5, 0)
        # fmt: on

        self.grpb_alarms = QtWid.QGroupBox("Alarm values")
        self.grpb_alarms.setStyleSheet(SS_GROUP)
        self.grpb_alarms.setLayout(grid)

        # Groupbox "PID feedback"
        # -----------------------
        p = {
            "alignment": QtCore.Qt.AlignRight,
            "minimumWidth": 50,
            "maximumWidth": 30,
            "readOnly": True,
        }
        self.PID_P = QtWid.QLineEdit(**p)
        self.PID_I = QtWid.QLineEdit(**p)
        self.PID_D = QtWid.QLineEdit(**p)
        self.pbtn_read_PID_values = QtWid.QPushButton("Read")
        self.pbtn_read_PID_values.setMinimumSize(50, 30)

        p = {"alignment": QtCore.Qt.AlignCenter}
        grid = QtWid.QGridLayout()
        # fmt: off
        grid.addWidget(
            QtWid.QLabel("Values can be set in the chiller's menu", **p), 0, 0,
            1, 3)
        grid.addWidget(QtWid.QLabel("P", **p), 1, 0)
        grid.addWidget(self.PID_P, 1, 1)
        grid.addWidget(QtWid.QLabel("% span of 100" + CHAR_DEG_C), 1, 2)
        grid.addWidget(QtWid.QLabel("I", **p), 2, 0)
        grid.addWidget(self.PID_I, 2, 1)
        grid.addWidget(QtWid.QLabel("repeats/minute"), 2, 2)
        grid.addWidget(QtWid.QLabel("D", **p), 3, 0)
        grid.addWidget(self.PID_D, 3, 1)
        grid.addWidget(QtWid.QLabel("minutes"), 3, 2)
        grid.addWidget(self.pbtn_read_PID_values, 4, 0)
        # fmt: on

        self.grpb_PID = QtWid.QGroupBox("PID feedback")
        self.grpb_PID.setStyleSheet(SS_GROUP)
        self.grpb_PID.setLayout(grid)

        # Groupbox "Status bits"
        # ----------------------
        # fmt: off
        self.SB_tripped = create_error_LED()
        self.SB_tripped.setText("No faults")
        self.SB_high_temp_fixed = create_tiny_error_LED()
        self.SB_low_temp_fixed = create_tiny_error_LED()
        self.SB_high_temp = create_tiny_error_LED()
        self.SB_low_temp = create_tiny_error_LED()
        self.SB_high_pressure = create_tiny_error_LED()
        self.SB_low_pressure = create_tiny_error_LED()
        self.SB_drip_pan = create_tiny_error_LED()
        self.SB_high_level = create_tiny_error_LED()
        self.SB_phase_monitor = create_tiny_error_LED()
        self.SB_motor_overload = create_tiny_error_LED()
        self.SB_LPC = create_tiny_error_LED()
        self.SB_HPC = create_tiny_error_LED()
        self.SB_external_EMO = create_tiny_error_LED()
        self.SB_local_EMO = create_tiny_error_LED()
        self.SB_low_flow = create_tiny_error_LED()
        self.SB_low_level = create_tiny_error_LED()
        self.SB_sense_5V = create_tiny_error_LED()
        self.SB_invalid_level = create_tiny_error_LED()
        self.SB_low_fixed_flow_warning = create_tiny_error_LED()
        self.SB_high_pressure_factory = create_tiny_error_LED()
        self.SB_low_pressure_factory = create_tiny_error_LED()

        p = {'alignment': QtCore.Qt.AlignRight}
        grid = QtWid.QGridLayout()
        grid.addWidget(self.SB_tripped, 0, 0, 1, 2)
        grid.addItem(QtWid.QSpacerItem(1, 12), 1, 0)
        grid.addWidget(QtWid.QLabel("high temp fixed fault", **p), 2, 0)
        grid.addWidget(self.SB_high_temp_fixed, 2, 1)
        grid.addWidget(QtWid.QLabel("low temp fixed fault", **p), 3, 0)
        grid.addWidget(self.SB_low_temp_fixed, 3, 1)
        grid.addWidget(QtWid.QLabel("high temp fault/warning", **p), 4, 0)
        grid.addWidget(self.SB_high_temp, 4, 1)
        grid.addWidget(QtWid.QLabel("low temp fault/warning", **p), 5, 0)
        grid.addWidget(self.SB_low_temp, 5, 1)
        grid.addWidget(QtWid.QLabel("high pressure fault/warning", **p), 6, 0)
        grid.addWidget(self.SB_high_pressure, 6, 1)
        grid.addWidget(QtWid.QLabel("low pressure fault/warning", **p), 7, 0)
        grid.addWidget(self.SB_low_pressure, 7, 1)
        grid.addWidget(QtWid.QLabel("drip pan fault", **p), 8, 0)
        grid.addWidget(self.SB_drip_pan, 8, 1)
        grid.addWidget(QtWid.QLabel("high level fault", **p), 9, 0)
        grid.addWidget(self.SB_high_level, 9, 1)
        grid.addWidget(QtWid.QLabel("phase monitor fault", **p), 10, 0)
        grid.addWidget(self.SB_phase_monitor, 10, 1)
        grid.addWidget(QtWid.QLabel("motor overload fault", **p), 11, 0)
        grid.addWidget(self.SB_motor_overload, 11, 1)
        grid.addWidget(QtWid.QLabel("LPC fault", **p), 12, 0)
        grid.addWidget(self.SB_LPC, 12, 1)
        grid.addWidget(QtWid.QLabel("HPC fault", **p), 13, 0)
        grid.addWidget(self.SB_HPC, 13, 1)
        grid.addWidget(QtWid.QLabel("external EMO fault", **p), 14, 0)
        grid.addWidget(self.SB_external_EMO, 14, 1)
        grid.addWidget(QtWid.QLabel("local EMO fault", **p), 15, 0)
        grid.addWidget(self.SB_local_EMO, 15, 1)
        grid.addWidget(QtWid.QLabel("low flow fault", **p), 16, 0)
        grid.addWidget(self.SB_low_flow, 16, 1)
        grid.addWidget(QtWid.QLabel("low level fault", **p), 17, 0)
        grid.addWidget(self.SB_low_level, 17, 1)
        grid.addWidget(QtWid.QLabel("sense 5V fault", **p), 18, 0)
        grid.addWidget(self.SB_sense_5V, 18, 1)
        grid.addWidget(QtWid.QLabel("invalid level fault", **p), 19, 0)
        grid.addWidget(self.SB_invalid_level, 19, 1)
        grid.addWidget(QtWid.QLabel("low fixed flow warning", **p), 20, 0)
        grid.addWidget(self.SB_low_fixed_flow_warning, 20, 1)
        grid.addWidget(QtWid.QLabel("high pressure factory fault", **p), 21, 0)
        grid.addWidget(self.SB_high_pressure_factory, 21, 1)
        grid.addWidget(QtWid.QLabel("low pressure factory fault", **p), 22, 0)
        grid.addWidget(self.SB_low_pressure_factory, 22, 1)
        # fmt: on

        self.grpb_SBs = QtWid.QGroupBox("Status bits")
        self.grpb_SBs.setStyleSheet(SS_GROUP)
        self.grpb_SBs.setLayout(grid)

        # Groupbox "Control"
        # ------------------
        p = {
            "alignment": QtCore.Qt.AlignRight,
            "minimumWidth": 50,
            "maximumWidth": 30,
        }

        self.lbl_offline = QtWid.QLabel(
            "OFFLINE",
            visible=False,
            font=QtGui.QFont("Palatino", 14, weight=QtGui.QFont.Bold),
            alignment=QtCore.Qt.AlignCenter,
        )
        # fmt: off
        self.pbtn_on = create_Toggle_button("Off")
        self.powering_down = create_tiny_error_LED()
        self.send_setpoint = QtWid.QLineEdit(**p)
        self.read_setpoint = QtWid.QLineEdit(**p, readOnly=True)
        self.read_temp = QtWid.QLineEdit(**p, readOnly=True)
        self.read_flow = QtWid.QLineEdit(**p, readOnly=True)
        self.read_supply = QtWid.QLineEdit(**p, readOnly=True)
        self.read_suction = QtWid.QLineEdit(**p, readOnly=True)
        self.lbl_update_counter = QtWid.QLabel("0")

        grid = QtWid.QGridLayout()
        grid.addWidget(self.lbl_offline, 0, 0, 1, 3)
        grid.addWidget(self.pbtn_on, 1, 0, 1, 3)
        grid.addWidget(
            QtWid.QLabel("Is powering up/down?",
                         alignment=QtCore.Qt.AlignRight), 2, 0, 1, 2)
        grid.addWidget(self.powering_down, 2, 2)
        grid.addItem(QtWid.QSpacerItem(1, 12), 3, 0)
        grid.addWidget(QtWid.QLabel("Send setpoint"), 4, 0)
        grid.addWidget(QtWid.QLabel("Read setpoint"), 5, 0)
        grid.addWidget(self.send_setpoint, 4, 1)
        grid.addWidget(self.read_setpoint, 5, 1)
        grid.addWidget(QtWid.QLabel(CHAR_DEG_C), 4, 2)
        grid.addWidget(QtWid.QLabel(CHAR_DEG_C), 5, 2)
        grid.addItem(QtWid.QSpacerItem(1, 12), 6, 0)
        grid.addWidget(QtWid.QLabel("Read temp"), 7, 0)
        grid.addWidget(self.read_temp, 7, 1)
        grid.addWidget(QtWid.QLabel(CHAR_DEG_C), 7, 2)
        grid.addWidget(QtWid.QLabel("Read flow"), 8, 0)
        grid.addWidget(self.read_flow, 8, 1)
        grid.addWidget(QtWid.QLabel("LPM"), 8, 2)
        grid.addWidget(QtWid.QLabel("Read supply"), 9, 0)
        grid.addWidget(self.read_supply, 9, 1)
        grid.addWidget(QtWid.QLabel("bar"), 9, 2)
        grid.addWidget(QtWid.QLabel("Read suction"), 10, 0)
        grid.addWidget(self.read_suction, 10, 1)
        grid.addWidget(QtWid.QLabel("bar"), 10, 2)

        grid.addItem(QtWid.QSpacerItem(1, 12), 11, 0)
        grid.addWidget(QtWid.QLabel("Nominal values @ 15-02-2018:"), 12, 0, 1,
                       3)
        grid.addWidget(QtWid.QLabel("Read flow"), 13, 0)
        grid.addWidget(QtWid.QLabel("80  ", alignment=QtCore.Qt.AlignRight),
                       13, 1)
        grid.addWidget(QtWid.QLabel("LPM"), 13, 2)
        grid.addWidget(QtWid.QLabel("Read supply"), 14, 0)
        grid.addWidget(QtWid.QLabel("2.9  ", alignment=QtCore.Qt.AlignRight),
                       14, 1)
        grid.addWidget(QtWid.QLabel("bar"), 14, 2)
        grid.addWidget(QtWid.QLabel("Read suction"), 15, 0)
        grid.addWidget(QtWid.QLabel("40  ", alignment=QtCore.Qt.AlignRight),
                       15, 1)
        grid.addWidget(QtWid.QLabel("bar"), 15, 2)
        grid.addWidget(self.lbl_update_counter, 16, 0, 1, 2)
        # fmt: on

        self.grpb_control = QtWid.QGroupBox("Control")
        self.grpb_control.setStyleSheet(SS_GROUP)
        self.grpb_control.setLayout(grid)

        # --------------------------------------
        #   Round up final QtWid.QHBoxLayout()
        # --------------------------------------

        vbox = QtWid.QVBoxLayout()
        vbox.addWidget(self.grpb_alarms)
        vbox.addWidget(self.grpb_PID)
        vbox.setAlignment(self.grpb_alarms, QtCore.Qt.AlignTop)
        vbox.setAlignment(self.grpb_PID, QtCore.Qt.AlignTop)
        vbox.setAlignment(QtCore.Qt.AlignTop)

        self.hbly_GUI = QtWid.QHBoxLayout()
        self.hbly_GUI.addLayout(vbox)
        self.hbly_GUI.addWidget(self.grpb_SBs)
        self.hbly_GUI.addWidget(self.grpb_control)
        self.hbly_GUI.addStretch(1)
        self.hbly_GUI.setAlignment(self.grpb_SBs, QtCore.Qt.AlignTop)
        self.hbly_GUI.setAlignment(self.grpb_control, QtCore.Qt.AlignTop)
        self.hbly_GUI.setAlignment(QtCore.Qt.AlignTop)
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)

        self.setWindowTitle("E. coli sauna")
        self.setGeometry(350, 50, 960, 800)
        self.setStyleSheet(SS_TEXTBOX_READ_ONLY + SS_GROUP)

        # -------------------------
        #   Top frame
        # -------------------------

        # Left box
        self.qlbl_update_counter = QtWid.QLabel("0")
        self.qlbl_DAQ_rate = QtWid.QLabel("DAQ: nan Hz")
        self.qlbl_DAQ_rate.setStyleSheet("QLabel {min-width: 7em}")

        vbox_left = QtWid.QVBoxLayout()
        vbox_left.addWidget(self.qlbl_update_counter, stretch=0)
        vbox_left.addStretch(1)
        vbox_left.addWidget(self.qlbl_DAQ_rate, stretch=0)

        # Middle box
        self.qlbl_title = QtWid.QLabel(
            "",
            font=QtGui.QFont("Palatino", 14, weight=QtGui.QFont.Bold),
        )
        self.qlbl_title.setAlignment(QtCore.Qt.AlignCenter)
        self.qlbl_cur_date_time = QtWid.QLabel("00-00-0000    00:00:00")
        self.qlbl_cur_date_time.setAlignment(QtCore.Qt.AlignCenter)
        self.qpbt_record = create_Toggle_button(
            "Click to start recording to file")
        self.qlbl_warning = QtWid.QLabel(
            "Do not exceed 5.5 W as the heaters will reach >90 °C.",
            font=QtGui.QFont("Palatino", 8, weight=QtGui.QFont.Bold),
        )
        self.qlbl_warning.setStyleSheet(
            "QLabel {color: darkred; alignment: AlignCenter}")
        self.qlbl_warning.setAlignment(QtCore.Qt.AlignCenter)
        self.qpbt_record.clicked.connect(lambda state: log.record(state))

        vbox_middle = QtWid.QVBoxLayout()
        vbox_middle.addWidget(self.qlbl_title)
        vbox_middle.addWidget(self.qlbl_cur_date_time)
        vbox_middle.addWidget(self.qpbt_record)
        vbox_middle.addWidget(self.qlbl_warning)

        # Right box
        self.qpbt_exit = QtWid.QPushButton("Exit")
        self.qpbt_exit.clicked.connect(self.close)
        self.qpbt_exit.setMinimumHeight(30)
        self.qlbl_recording_time = QtWid.QLabel(alignment=QtCore.Qt.AlignRight)

        vbox_right = QtWid.QVBoxLayout()
        vbox_right.addWidget(self.qpbt_exit, stretch=0)
        vbox_right.addStretch(1)
        vbox_right.addWidget(self.qlbl_recording_time, stretch=0)

        # Round up top frame
        hbox_top = QtWid.QHBoxLayout()
        hbox_top.addLayout(vbox_left, stretch=0)
        hbox_top.addStretch(1)
        hbox_top.addLayout(vbox_middle, stretch=0)
        hbox_top.addStretch(1)
        hbox_top.addLayout(vbox_right, stretch=0)

        # -------------------------
        #   Bottom frame
        # -------------------------

        #  Group 'PID control'
        # -------------------------

        p = {
            "alignment": QtCore.Qt.AlignRight,
            "maximumWidth": 48,
        }
        self.qpbt_pid_enabled = create_Toggle_button("PID OFF")
        self.qpbt_pid_enabled.clicked.connect(
            lambda state: self.process_qpbt_pid_enabled(state))

        self.qlin_pid_temp_setp = QtWid.QLineEdit("%.1f" % PID_TEMP_SETPOINT,
                                                  **p)
        self.qlin_pid_temp_setp.editingFinished.connect(
            self.process_qlin_pid_temp_setp)

        self.qlin_pid_Kp = QtWid.QLineEdit("%.1f" % PID_Kp, **p)
        self.qlin_pid_Kp.editingFinished.connect(self.process_qlin_pid_Kp)

        self.qlin_pid_Ki = QtWid.QLineEdit("%.0e" % PID_Ki, **p)
        self.qlin_pid_Ki.editingFinished.connect(self.process_qlin_pid_Ki)

        self.qlin_pid_V_clamp = QtWid.QLineEdit("%.1f" % PID_V_clamp, **p)
        self.qlin_pid_V_clamp.editingFinished.connect(
            self.process_qlin_pid_V_clamp)

        # fmt: off
        grid = QtWid.QGridLayout()
        grid.setVerticalSpacing(2)
        grid.addWidget(self.qpbt_pid_enabled, 0, 0, 1, 3)
        grid.addItem(QtWid.QSpacerItem(1, 2), 1, 0)
        grid.addWidget(QtWid.QLabel("Setpoint"), 2, 0)
        grid.addWidget(self.qlin_pid_temp_setp, 2, 1)
        grid.addWidget(QtWid.QLabel("°C"), 2, 2)
        grid.addItem(QtWid.QSpacerItem(1, 10), 3, 0)
        grid.addWidget(QtWid.QLabel("Parameters:"), 4, 0, 1, 3)
        grid.addItem(QtWid.QSpacerItem(1, 4), 5, 0)
        grid.addWidget(QtWid.QLabel("K_p"), 6, 0)
        grid.addWidget(self.qlin_pid_Kp, 6, 1)
        grid.addWidget(QtWid.QLabel("K_i"), 7, 0)
        grid.addWidget(self.qlin_pid_Ki, 7, 1)
        grid.addWidget(QtWid.QLabel("1/s"), 7, 2)
        grid.addWidget(QtWid.QLabel("Output clamp"), 8, 0)
        grid.addWidget(self.qlin_pid_V_clamp, 8, 1)
        grid.addWidget(QtWid.QLabel("V"), 8, 2)
        # fmt: on

        qgrp_PID = QtWid.QGroupBox("PID feedback")
        qgrp_PID.setLayout(grid)

        self.vbox_control = QtWid.QVBoxLayout()
        self.vbox_control.addWidget(qdev_psu.grpb, stretch=0)
        self.vbox_control.addWidget(qgrp_PID, stretch=0)
        self.vbox_control.addStretch()

        #  Charts
        # -------------------------

        self.gw = pg.GraphicsLayoutWidget()

        # Plot: Temperatures
        p = {"color": "#EEE", "font-size": "10pt"}
        self.pi_temp = self.gw.addPlot(row=0, col=0)
        self.pi_temp.setLabel("left", text="temperature (°C)", **p)

        # Plot: Humidity
        self.pi_humi = self.gw.addPlot(row=1, col=0)
        self.pi_humi.setLabel("left", text="humidity (%)", **p)

        # Plot: heater power
        self.pi_power = self.gw.addPlot(row=2, col=0)
        self.pi_power.setLabel("left", text="power (W)", **p)

        self.plots = [self.pi_temp, self.pi_humi, self.pi_power]
        for plot in self.plots:
            plot.setClipToView(True)
            plot.showGrid(x=1, y=1)
            plot.setLabel("bottom", text="history (s)", **p)
            plot.setMenuEnabled(True)
            plot.enableAutoRange(axis=pg.ViewBox.XAxis, enable=False)
            plot.enableAutoRange(axis=pg.ViewBox.YAxis, enable=True)
            plot.setAutoVisible(y=True)
            plot.setRange(xRange=[-CHART_HISTORY_TIME, 0])

        # Curves
        capacity = round(CHART_HISTORY_TIME * 1e3 / DAQ_INTERVAL_MS)
        PEN_01 = pg.mkPen(color=[255, 255, 0], width=3)
        PEN_02 = pg.mkPen(color=[0, 255, 255], width=3)
        PEN_03 = pg.mkPen(color=[255, 0, 0], width=3)

        self.tscurve_dht22_temp = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_temp.plot(pen=PEN_01, name="temperature"),
        )
        self.tscurve_dht22_humi = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_humi.plot(pen=PEN_02, name="humidity"),
        )
        self.tscurve_power = HistoryChartCurve(
            capacity=capacity,
            linked_curve=self.pi_power.plot(pen=PEN_03, name="heater power"),
        )

        self.tscurves = [
            self.tscurve_dht22_temp,
            self.tscurve_dht22_humi,
            self.tscurve_power,
        ]

        #  Group `Readings`
        # -------------------------

        legend = LegendSelect(linked_curves=self.tscurves,
                              hide_toggle_button=True)

        p = {
            "readOnly": True,
            "alignment": QtCore.Qt.AlignRight,
            "maximumWidth": 54,
        }
        self.qlin_ds_temp = QtWid.QLineEdit(**p)
        self.qlin_dht22_temp = QtWid.QLineEdit(**p)
        self.qlin_dht22_humi = QtWid.QLineEdit(**p)
        self.qlin_power = QtWid.QLineEdit(**p)

        # fmt: off
        legend.grid.setHorizontalSpacing(6)
        legend.grid.addWidget(self.qlin_dht22_temp, 0, 2)
        legend.grid.addWidget(QtWid.QLabel("± 0.5 °C"), 0, 3)
        legend.grid.addWidget(self.qlin_dht22_humi, 1, 2)
        legend.grid.addWidget(QtWid.QLabel("± 3 %"), 1, 3)
        legend.grid.addWidget(self.qlin_power, 2, 2)
        legend.grid.addWidget(QtWid.QLabel("± 0.05 W"), 2, 3)
        # fmt: on

        qgrp_readings = QtWid.QGroupBox("Readings")
        qgrp_readings.setLayout(legend.grid)

        #  Group 'Log comments'
        # -------------------------

        self.qtxt_comments = QtWid.QTextEdit()
        grid = QtWid.QGridLayout()
        grid.addWidget(self.qtxt_comments, 0, 0)

        qgrp_comments = QtWid.QGroupBox("Log comments")
        qgrp_comments.setLayout(grid)

        #  Group 'Charts'
        # -------------------------

        self.plot_manager = PlotManager(parent=self)
        self.plot_manager.add_autorange_buttons(linked_plots=self.plots)
        self.plot_manager.add_preset_buttons(
            linked_plots=self.plots,
            linked_curves=self.tscurves,
            presets=[
                {
                    "button_label": "03:00",
                    "x_axis_label": "history (sec)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-3, 0),
                },
                {
                    "button_label": "10:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-10, 0),
                },
                {
                    "button_label": "30:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-30, 0),
                },
                {
                    "button_label": "60:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-60, 0),
                },
                {
                    "button_label": "120:00",
                    "x_axis_label": "history (min)",
                    "x_axis_divisor": 60,
                    "x_axis_range": (-120, 0),
                },
            ],
        )
        self.plot_manager.add_clear_button(linked_curves=self.tscurves)
        self.plot_manager.perform_preset(1)

        qgrp_chart = QtWid.QGroupBox("Charts")
        qgrp_chart.setLayout(self.plot_manager.grid)

        vbox = QtWid.QVBoxLayout()
        vbox.addWidget(qgrp_readings)
        vbox.addWidget(qgrp_comments)
        vbox.addWidget(qgrp_chart, alignment=QtCore.Qt.AlignLeft)
        vbox.addStretch()

        # -------------------------
        #   Round up full window
        # -------------------------

        self.hbox_bot = QtWid.QHBoxLayout()
        self.hbox_bot.addLayout(self.vbox_control, 0)
        self.hbox_bot.addWidget(self.gw, 1)
        self.hbox_bot.addLayout(vbox, 0)

        vbox = QtWid.QVBoxLayout(self)
        vbox.addLayout(hbox_top, stretch=0)
        vbox.addSpacerItem(QtWid.QSpacerItem(0, 10))
        vbox.addLayout(self.hbox_bot, stretch=1)