def __init__(self, parent: QWidget, commander: Commander): from . import STOP_SHORTCUT super(RunControlWidget, self).__init__(parent) self._commander = commander self._last_status: typing.Optional[TaskSpecificStatusReport.Run] = None # noinspection PyTypeChecker self._named_control_policies: typing.Dict[ str, _ControlPolicy ] = _make_named_control_policies() self._guru_mode_checkbox = QCheckBox("Guru", self) self._guru_mode_checkbox.setToolTip( "The Guru Mode is dangerous! " "Use it only if you know what you're doing, and be ready for problems." ) self._guru_mode_checkbox.setStatusTip(self._guru_mode_checkbox.toolTip()) self._guru_mode_checkbox.setIcon(get_icon("guru")) self._guru_mode_checkbox.toggled.connect(self._on_guru_mode_toggled) self._setpoint_control = SpinboxLinkedWithSlider( self, slider_orientation=SpinboxLinkedWithSlider.SliderOrientation.HORIZONTAL, ) self._setpoint_control.tool_tip = ( f"To stop the motor, press {STOP_SHORTCUT} or click the Stop button" ) self._setpoint_control.status_tip = self._setpoint_control.tool_tip self._setpoint_control.value_change_event.connect(self._on_setpoint_changed) self._setpoint_control.spinbox.setKeyboardTracking(False) self._mode_selector = QComboBox(self) self._mode_selector.setEditable(False) self._mode_selector.currentIndexChanged.connect( lambda *_: self._on_control_mode_changed() ) for name, cp in self._named_control_policies.items(): if not cp.only_for_guru: self._mode_selector.addItem(get_icon(cp.icon_name), name) self._on_control_mode_changed() self.setLayout( lay_out_vertically( lay_out_horizontally( QLabel("Control mode", self), self._mode_selector, (None, 1), QLabel("Setpoint", self), (self._setpoint_control.spinbox, 1), (None, 1), self._guru_mode_checkbox, ), self._setpoint_control.slider, (None, 1), ) )
def __init__(self, parent: QWidget, commander: Commander): super(LowLevelManipulationControlWidget, self).__init__(parent) self._last_seen_timestamped_general_status: typing.Optional[ typing.Tuple[float, GeneralStatusView]] = None self._tabs = QTabWidget(self) for widget_type in _LLM_MODE_TO_WIDGET_TYPE_MAPPING.values(): widget = widget_type(self, commander) tab_name, icon_name = widget.get_widget_name_and_icon_name() self._tabs.addTab(widget, get_icon(icon_name), tab_name) self._current_widget: LowLevelManipulationControlSubWidgetBase = self._tabs.currentWidget( ) self._tabs.currentChanged.connect(self._on_current_widget_changed) # Presentation configuration. # We have only one widget here, so we reduce the margins to the bare minimum in order to # conserve the valuable screen space. # We also change the default appearance of the tab widget to make it look okay with the small margins. self._tabs.setTabPosition(QTabWidget.South) self._tabs.setDocumentMode(True) self.setLayout(lay_out_vertically((self._tabs, 1))) self.layout().setContentsMargins(0, 0, 0, 0)
def __init__(self, parent: QWidget, commander: Commander): super(TelegaControlWidget, self).__init__(parent) self._dc_quantities_widget = DCQuantitiesWidget(self) self._temperature_widget = TemperatureWidget(self) self._hardware_flag_counters_widget = HardwareFlagCountersWidget(self) self._device_status_widget = DeviceStatusWidget(self) self._vsi_status_widget = VSIStatusWidget(self) self._active_alerts_widget = ActiveAlertsWidget(self) self._task_specific_status_widget = TaskSpecificStatusWidget(self) self._control_widget = ControlWidget(self, commander) self.setLayout( lay_out_vertically( lay_out_horizontally( (self._dc_quantities_widget, 1), (self._temperature_widget, 1), (self._hardware_flag_counters_widget, 1), (self._vsi_status_widget, 1), (self._active_alerts_widget, 2), ), lay_out_horizontally( (self._device_status_widget, 1), (self._task_specific_status_widget, 5), ), self._control_widget, ), )
def __init__(self, parent: QWidget): super(Widget, self).__init__(parent) self._last_displayed: TaskSpecificStatusReport.Fault = None self._line_height = QFontMetrics(QFont()).height() self._task_icon_display = QLabel(self) self._task_name_display = self._make_line_display() self._error_code_dec = self._make_line_display('Exit code in decimal') self._error_code_hex = self._make_line_display( 'Same exit code in hexadecimal') self._error_code_bin = self._make_line_display( 'Same exit code in binary, for extra convenience') self._error_description_display = self._make_line_display( 'Error description', False) self._error_comment_display = self._make_text_display() self.setLayout( lay_out_vertically( lay_out_horizontally(QLabel('The task', self), self._task_icon_display, (self._task_name_display, 3)), lay_out_horizontally(QLabel('has failed with exit code', self), (self._error_code_dec, 1), (self._error_code_hex, 1), (self._error_code_bin, 2), QLabel('which means:', self)), lay_out_horizontally((self._error_description_display, 1)), lay_out_horizontally((self._error_comment_display, 1)), (None, 1), ))
def __init__(self, parent: QWidget): super(Widget, self).__init__(parent) self._progress_bar = QProgressBar(self) self._progress_bar.setMinimum(0) self._progress_bar.setMaximum(100) self.setLayout(lay_out_vertically(self._progress_bar))
def __init__(self, parent: QWidget, commander: Commander): super(MiscControlWidget, self).__init__(parent) self._commander = commander self._performer_should_stop = True self._frequency_input = QDoubleSpinBox(self) self._frequency_input.setRange(100, 15000) self._frequency_input.setValue(3000) self._frequency_input.setSuffix(" Hz") self._frequency_input.setToolTip("Beep frequency, in hertz") self._frequency_input.setStatusTip(self._frequency_input.toolTip()) self._duration_input = QDoubleSpinBox(self) self._duration_input.setRange(0.01, 3) self._duration_input.setValue(0.5) self._duration_input.setSuffix(" s") self._duration_input.setToolTip("Beep duration, in seconds") self._duration_input.setStatusTip(self._duration_input.toolTip()) self._go_button = make_button( self, text="Beep", icon_name="speaker", tool_tip="Sends a beep command to the device once", on_clicked=self._beep_once, ) def stealthy( icon_name: str, music_factory: typing.Callable[[], typing.Iterable["_Note"]] ) -> QWidget: b = make_button( self, icon_name=icon_name, on_clicked=lambda: self._begin_performance(music_factory()), ) b.setFlat(True) b.setFixedSize(4, 4) return b self.setLayout( lay_out_vertically( lay_out_horizontally( QLabel("Frequency", self), self._frequency_input, QLabel("Duration", self), self._duration_input, self._go_button, (None, 1), ), (None, 1), lay_out_horizontally( (None, 1), stealthy("darth-vader", _get_imperial_march), ), ))
def display_warning_message(title: str, text: str, parent: WidgetBase, unwritten_registers: list): _warning = QDialog(parent) _warning.setWindowTitle(title) _tableWidget = QTableWidget(_warning) _tableWidget.setFont(get_monospace_font()) _tableWidget.setRowCount(len(unwritten_registers)) _tableWidget.setColumnCount(3) _tableWidget.setHorizontalHeaderLabels( ["Full name", "Current value", "Requested value"]) _tableWidget.horizontalHeader().setStretchLastSection(True) _header = _tableWidget.horizontalHeader() _header.setSectionResizeMode(0, QHeaderView.ResizeToContents) _header.setSectionResizeMode(1, QHeaderView.Stretch) _header.setSectionResizeMode(2, QHeaderView.Stretch) _tableWidget.verticalHeader().hide() _tableWidget.verticalHeader().setSectionResizeMode( _tableWidget.verticalHeader().ResizeToContents) for i in range(len(unwritten_registers)): _name = unwritten_registers[i][0] _current_value = unwritten_registers[i][1] _requested_value = unwritten_registers[i][2] _tableWidget.setItem(i, 0, QTableWidgetItem(_name + " ")) _tableWidget.setItem( i, 1, QTableWidgetItem(", ".join(str(e) for e in _current_value))) _tableWidget.setItem( i, 2, QTableWidgetItem(", ".join(str(e) for e in _requested_value))) _btn_ok = QPushButton(_warning) _btn_ok.setText("Ok") _btn_ok.clicked.connect(_warning.close) _warning.setLayout( lay_out_vertically( lay_out_horizontally(QLabel(text, _warning)), lay_out_horizontally( QLabel("Some configuration parameters could not be written:", _warning)), lay_out_horizontally(_tableWidget), lay_out_horizontally(_btn_ok), )) _warning.show()
def __init__(self, parent: QWidget, commander: Commander): super(Widget, self).__init__(parent) self._commander = commander self.setLayout( lay_out_vertically( lay_out_horizontally( QLabel("Calibrate the VSI hardware", self), make_button( self, text="Calibrate", icon_name="scales", on_clicked=self._execute, ), (None, 1), ), (None, 1), ) )
def __init__(self, parent: QWidget, commander: Commander): super(MotorIdentificationControlWidget, self).__init__(parent) self._commander = commander self._mode_map = { _humanize(mid): mid for mid in MotorIdentificationMode } self._mode_selector = QComboBox(self) self._mode_selector.setEditable(False) # noinspection PyTypeChecker self._mode_selector.addItems( map( _humanize, sorted( MotorIdentificationMode, key=lambda x: x != MotorIdentificationMode.R_L_PHI, ), )) go_button = QPushButton(get_icon("play"), "Launch", self) go_button.clicked.connect(self._execute) self.setLayout( lay_out_vertically( lay_out_horizontally( QLabel("Select parameters to estimate:", self), self._mode_selector, (None, 1), ), lay_out_horizontally( QLabel("Then click", self), go_button, QLabel( "and wait. The process will take a few minutes to complete.", self, ), (None, 1), ), (None, 1), ))
def _unittest_spinbox_linked_with_slider(): import time from PyQt5.QtWidgets import QApplication, QMainWindow, QLayout from kucher.view.utils import lay_out_horizontally, lay_out_vertically app = QApplication([]) instances: typing.List[SpinboxLinkedWithSlider] = [] def make(minimum: float, maximum: float, step: float) -> QLayout: o = SpinboxLinkedWithSlider( widget, minimum=minimum, maximum=maximum, step=step, slider_orientation=SpinboxLinkedWithSlider.SliderOrientation. HORIZONTAL, ) instances.append(o) return lay_out_horizontally((o.slider, 1), o.spinbox) win = QMainWindow() widget = QWidget(win) widget.setLayout( lay_out_vertically(make(0, 100, 1), make(-10, 10, 0.01), make(-99999, 100, 100))) win.setCentralWidget(widget) win.show() def run_a_bit(): for _ in range(1000): time.sleep(0.005) app.processEvents() run_a_bit() instances[0].minimum = -1000 instances[2].step = 10 run_a_bit() win.close()
def __init__(self, parent: QWidget, commander: Commander): super(HardwareTestControlWidget, self).__init__(parent) self._commander = commander self.setLayout( lay_out_vertically( QLabel( "The motor must be connected in order for the self test to succeed.", self, ), lay_out_horizontally( (None, 1), make_button( self, text="Run self-test", icon_name="play", on_clicked=self._execute, ), (None, 1), ), (None, 1), ))
def __init__(self, parent: QWidget): super(Widget, self).__init__(parent) self._energy_conversion_efficiency_estimate = ( None # Used for filtering before displaying ) self._stall_count_display = self._make_display( "Stalls", "Number of times the rotor stalled since task activation" ) self._estimated_active_power_display = self._make_display( "P<sub>active</sub>", "For well-balanced systems, the estimated active power equals the DC power", ) self._demand_factor_display = self._make_display( "Demand", "Total powertrain demand factor" ) self._mechanical_rpm_display = self._make_display( "\u03C9<sub>mechanical</sub>", "Mechanical revolutions per minute" ) self._current_frequency_display = self._make_display( "f<sub>electrical</sub>", "Frequency of three-phase currents and voltages" ) self._dq_display = _DQDisplayWidget(self) self._torque_display = self._make_display( "\u03C4", "Estimated torque at the shaft" ) self._mechanical_power_display = self._make_display( "P<sub>mechanical</sub>", "Estimated mechanical power delivered to the shaft", ) self._loss_power_display = self._make_display( "P<sub>loss</sub>", "Estimated power loss, DC power input to motor shaft" ) self._energy_conversion_efficiency_display = self._make_display( "\u03B7<sub>DC-S</sub>", "Estimated energy conversion efficiency, DC power input to motor shaft", ) self._control_mode_display = self._make_display( "Ctrl. mode", "Control mode used by the controller", True ) self._reverse_flag_display = self._make_display( "Direction", "Direction of rotation", True ) self._spinup_flag_display = self._make_display( "Started?", "Whether the motor has started or still starting", True ) self._saturation_flag_display = self._make_display( "CSSW", "Control System Saturation Warning", True ) self.setLayout( lay_out_horizontally( ( lay_out_vertically( self._mechanical_rpm_display, self._current_frequency_display, self._stall_count_display, self._demand_factor_display, ), 4, ), _make_vertical_separator(self), ( lay_out_vertically( self._dq_display, self._estimated_active_power_display, (None, 1), ), 4, ), _make_vertical_separator(self), ( lay_out_vertically( self._torque_display, self._mechanical_power_display, self._loss_power_display, self._energy_conversion_efficiency_display, ), 3, ), _make_vertical_separator(self), ( lay_out_vertically( self._control_mode_display, self._reverse_flag_display, self._spinup_flag_display, self._saturation_flag_display, ), 4, ), ) )
def __init__(self, parent: QWidget): super(RegisterViewWidget, self).__init__(parent) self._registers = [] self._running_task: asyncio.Task = None self._visibility_selector = QComboBox(self) self._visibility_selector.addItem("Show all registers", lambda _: True) self._visibility_selector.addItem("Only configuration parameters", lambda r: r.mutable and r.persistent) # noinspection PyUnresolvedReferences self._visibility_selector.currentIndexChanged.connect( lambda _: self._on_visibility_changed()) self._reset_selected_button = make_button( self, "Reset selected", icon_name="clear-symbol", tool_tip=f"Reset the currently selected registers to their default " f"values. The restored values will be committed " f"immediately. This function is available only if a " f"default value is defined. [{RESET_SELECTED_SHORTCUT}]", on_clicked=self._do_reset_selected, ) self._reset_all_button = make_button( self, "Reset all", icon_name="skull-crossbones", tool_tip=f"Reset the all registers to their default " f"values. The restored values will be committed " f"immediately.", on_clicked=self._do_reset_all, ) self._read_selected_button = make_button( self, "Read selected", icon_name="process", tool_tip=f"Read the currently selected registers only " f"[{READ_SELECTED_SHORTCUT}]", on_clicked=self._do_read_selected, ) self._read_all_button = make_button( self, "Read all", icon_name="process-plus", tool_tip="Read all registers from the device", on_clicked=self._do_read_all, ) self._export_button = make_button( self, "Export", icon_name="export", tool_tip="Export configuration parameters", on_clicked=self._do_export, ) self._import_button = make_button( self, "Import", icon_name="import", tool_tip="Import configuration parameters", on_clicked=self._do_import, ) self._expand_all_button = make_button( self, "", icon_name="expand-arrow", tool_tip="Expand all namespaces", on_clicked=lambda: self._tree.expandAll(), ) self._collapse_all_button = make_button( self, "", icon_name="collapse-arrow", tool_tip="Collapse all namespaces", on_clicked=lambda: self._tree.collapseAll(), ) self._status_display = QLabel(self) self._status_display.setWordWrap(True) self._reset_selected_button.setEnabled(False) self._reset_all_button.setEnabled(False) self._read_selected_button.setEnabled(False) self._read_all_button.setEnabled(False) self._export_button.setEnabled(False) self._import_button.setEnabled(False) self._tree = QTreeView(self) self._tree.setVerticalScrollMode(QTreeView.ScrollPerPixel) self._tree.setHorizontalScrollMode(QTreeView.ScrollPerPixel) self._tree.setAnimated(True) self._tree.setSelectionMode(QAbstractItemView.ExtendedSelection) self._tree.setAlternatingRowColors(True) self._tree.setContextMenuPolicy(Qt.ActionsContextMenu) # Not sure about this one. This hardcoded value may look bad on some platforms. self._tree.setIndentation(20) def add_action( callback: typing.Callable[[], None], icon_name: str, name: str, shortcut: typing.Optional[str] = None, ): action = QAction(get_icon(icon_name), name, self) # noinspection PyUnresolvedReferences action.triggered.connect(callback) if shortcut: action.setShortcut(shortcut) action.setAutoRepeat(False) try: action.setShortcutVisibleInContextMenu(True) except AttributeError: pass # This feature is not available in PyQt before 5.10 self._tree.addAction(action) add_action(self._do_read_all, "process-plus", "Read all registers") add_action( self._do_read_selected, "process", "Read selected registers", READ_SELECTED_SHORTCUT, ) add_action( self._do_reset_selected, "clear-symbol", "Reset selected to default", RESET_SELECTED_SHORTCUT, ) self._tree.setItemDelegateForColumn( int(Model.ColumnIndices.VALUE), EditorDelegate(self._tree, self._display_status), ) # It doesn't seem to be explicitly documented, but it seems to be necessary to select either top or bottom # decoration position in order to be able to use center alignment. Left or right positions do not work here. self._tree.setItemDelegateForColumn( int(Model.ColumnIndices.FLAGS), StyleOptionModifyingDelegate( self._tree, decoration_position=QStyleOptionViewItem.Top, # Important decoration_alignment=Qt.AlignCenter, ), ) header: QHeaderView = self._tree.header() header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setStretchLastSection( False) # Horizontal scroll bar doesn't work if this is enabled buttons_layout = QGridLayout() buttons_layout.addWidget(self._read_selected_button, 0, 0) buttons_layout.addWidget(self._reset_selected_button, 0, 2) buttons_layout.addWidget(self._read_all_button, 1, 0) buttons_layout.addWidget(self._reset_all_button, 1, 2) buttons_layout.addWidget(self._import_button, 2, 0) buttons_layout.addWidget(self._export_button, 2, 2) for col in range(3): buttons_layout.setColumnStretch(col, 1) layout = lay_out_vertically( (self._tree, 1), buttons_layout, lay_out_horizontally( self._visibility_selector, (None, 1), self._expand_all_button, self._collapse_all_button, ), self._status_display, ) self.setLayout(layout)
def __init__(self, parent: QWidget, commander: Commander): super(ControlWidget, self).__init__(parent, 'Controls', 'adjust') self._commander: Commander = commander self._last_seen_timestamped_general_status: typing.Optional[ typing.Tuple[float, GeneralStatusView]] = None self._run_widget = RunControlWidget(self, commander) self._motor_identification_widget = MotorIdentificationControlWidget( self, commander) self._hardware_test_widget = HardwareTestControlWidget(self, commander) self._misc_widget = MiscControlWidget(self, commander) self._low_level_manipulation_widget = LowLevelManipulationControlWidget( self, commander) self._panel = QTabWidget(self) self._panel.addTab(self._run_widget, get_icon('running'), 'Run') self._panel.addTab(self._motor_identification_widget, get_icon('caliper'), 'Motor identification') self._panel.addTab(self._hardware_test_widget, get_icon('pass-fail'), 'Self-test') self._panel.addTab(self._misc_widget, get_icon('ellipsis'), 'Miscellaneous') self._panel.addTab(self._low_level_manipulation_widget, get_icon('hand-button'), 'Low-level manipulation') self._current_widget: SpecializedControlWidgetBase = self._hardware_test_widget # Selecting which widget to show by default - let it be Run widget, it's easy to understand and it's first self._panel.setCurrentWidget(self._run_widget) # Shared buttons self._stop_button =\ make_button(self, text='Stop', icon_name='stop', tool_tip=f'Sends a regular stop command which instructs the controller to abandon the current' f'task and activate the Idle task [{STOP_SHORTCUT}]', on_clicked=self._do_regular_stop) self._stop_button.setSizePolicy(QSizePolicy().MinimumExpanding, QSizePolicy().MinimumExpanding) self._emergency_button =\ make_button(self, text='EMERGENCY\nSHUTDOWN', tool_tip=f'Unconditionally disables and locks down the VSI until restarted ' f'[{EMERGENCY_SHORTCUT}]', on_clicked=self._do_emergency_stop) self._emergency_button.setSizePolicy(QSizePolicy().MinimumExpanding, QSizePolicy().MinimumExpanding) small_font = QFont() small_font.setPointSize(round(small_font.pointSize() * 0.8)) self._emergency_button.setFont(small_font) # Observe that the shortcuts are children of the window! This is needed to make them global. self._stop_shortcut = QShortcut(QKeySequence(STOP_SHORTCUT), self.window()) self._stop_shortcut.setAutoRepeat(False) self._emergency_shortcut = QShortcut(QKeySequence(EMERGENCY_SHORTCUT), self.window()) self._emergency_shortcut.setAutoRepeat(False) self.setEnabled(False) # Layout def make_tiny_label(text: str, alignment: int) -> QLabel: lbl = QLabel(text, self) lbl.setAlignment(alignment | Qt.AlignHCenter) font: QFont = lbl.font() font.setPointSize(round(font.pointSize() * 0.7)) lbl.setFont(font) return lbl self.setLayout( lay_out_horizontally( (self._panel, 1), lay_out_vertically( (self._stop_button, 1), make_tiny_label(f'\u2191 {STOP_SHORTCUT} \u2191', Qt.AlignTop), make_tiny_label(f'\u2193 {EMERGENCY_SHORTCUT} \u2193', Qt.AlignBottom), (self._emergency_button, 1), ))) # Configuring the event handler in the last order, because it might fire while we're configuring the widgets! self._panel.currentChanged.connect(self._on_current_widget_changed) # Invoking the handler to complete initialization self._on_current_widget_changed(self._panel.currentIndex())