#!/usr/bin/env python3 """ Configure Interface Widget - vzug-e-hinge ========================================== Manage interface profiles (UART/I2C configuration). """ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem, QMessageBox, QSplitter, QFormLayout, QSpinBox, QComboBox, QCheckBox, QScrollArea, QTabWidget ) from PyQt6.QtCore import Qt, pyqtSignal from database.init_database import DatabaseManager try: from serial.tools import list_ports SERIAL_AVAILABLE = True except ImportError: SERIAL_AVAILABLE = False import os import glob # Standard UART settings BAUD_RATES = ["9600", "19200", "38400", "57600", "115200", "230400", "460800", "921600"] PARITY_OPTIONS = ["N", "E", "O", "M", "S"] class ConfigureInterfaceWidget(QWidget): """Widget for managing interface profiles.""" profile_saved = pyqtSignal(int) profile_deleted = pyqtSignal(int) def __init__(self, db_manager: DatabaseManager): super().__init__() self.db_manager = db_manager self.db_conn = db_manager.get_connection() self.current_profile_id = None self._init_ui() self._load_profiles() def _init_ui(self): """Initialize UI.""" layout = QVBoxLayout() title = QLabel("Configure Interface Profiles") title.setStyleSheet("font-size: 16px; font-weight: bold;") layout.addWidget(title) splitter = QSplitter(Qt.Orientation.Horizontal) splitter.addWidget(self._create_list_panel()) splitter.addWidget(self._create_editor_panel()) splitter.setSizes([300, 700]) layout.addWidget(splitter) self.setLayout(layout) def _create_list_panel(self): """Create profile list panel.""" widget = QWidget() layout = QVBoxLayout() layout.addWidget(QLabel("Interface Profiles:")) self.profile_list = QListWidget() self.profile_list.currentItemChanged.connect(self._on_profile_selected) layout.addWidget(self.profile_list) btn_layout = QHBoxLayout() self.btn_add = QPushButton("Add New") self.btn_add.clicked.connect(self._on_add_clicked) btn_layout.addWidget(self.btn_add) self.btn_delete = QPushButton("Delete") self.btn_delete.clicked.connect(self._on_delete_clicked) self.btn_delete.setEnabled(False) btn_layout.addWidget(self.btn_delete) layout.addLayout(btn_layout) widget.setLayout(layout) return widget def _create_editor_panel(self): """Create editor panel with tabs.""" widget = QWidget() layout = QVBoxLayout() name_layout = QHBoxLayout() name_layout.addWidget(QLabel("Profile Name:")) self.name_input = QLineEdit() self.name_input.textChanged.connect(self._enable_save) name_layout.addWidget(self.name_input) layout.addLayout(name_layout) self.tabs = QTabWidget() self.tabs.addTab(self._create_uart_command_tab(), "UART Command") self.tabs.addTab(self._create_uart_logger_tab(), "UART Logger") self.tabs.addTab(self._create_i2c_tab(), "I2C") layout.addWidget(self.tabs) btn_layout = QHBoxLayout() self.btn_save = QPushButton("Save Profile") self.btn_save.clicked.connect(self._on_save_clicked) self.btn_save.setEnabled(False) btn_layout.addWidget(self.btn_save) btn_layout.addStretch() layout.addLayout(btn_layout) widget.setLayout(layout) return widget def _create_uart_command_tab(self): """Create UART Command tab (no packet detection).""" scroll = QScrollArea() scroll.setWidgetResizable(True) container = QWidget() layout = QVBoxLayout() uart_group = QGroupBox("UART Command Settings") form = QFormLayout() # Port with refresh port_layout = QHBoxLayout() self.cmd_port = QComboBox() self.cmd_port.setEditable(True) port_layout.addWidget(self.cmd_port) btn_refresh_cmd = QPushButton("🔄") btn_refresh_cmd.setMaximumWidth(40) btn_refresh_cmd.clicked.connect(lambda: self._refresh_uart_ports(self.cmd_port)) port_layout.addWidget(btn_refresh_cmd) form.addRow("Port:", port_layout) self.cmd_baud = QComboBox() self.cmd_baud.addItems(BAUD_RATES) self.cmd_baud.setCurrentText("115200") form.addRow("Baud Rate:", self.cmd_baud) self.cmd_data_bits = QSpinBox() self.cmd_data_bits.setRange(5, 8) self.cmd_data_bits.setValue(8) form.addRow("Data Bits:", self.cmd_data_bits) self.cmd_stop_bits = QSpinBox() self.cmd_stop_bits.setRange(1, 2) self.cmd_stop_bits.setValue(1) form.addRow("Stop Bits:", self.cmd_stop_bits) self.cmd_parity = QComboBox() self.cmd_parity.addItems(PARITY_OPTIONS) form.addRow("Parity:", self.cmd_parity) self.cmd_timeout = QSpinBox() self.cmd_timeout.setRange(100, 10000) self.cmd_timeout.setValue(1000) self.cmd_timeout.setSuffix(" ms") form.addRow("Timeout:", self.cmd_timeout) uart_group.setLayout(form) layout.addWidget(uart_group) layout.addStretch() container.setLayout(layout) scroll.setWidget(container) # Initial port scan self._refresh_uart_ports(self.cmd_port) return scroll def _create_uart_logger_tab(self): """Create UART Logger tab (with packet detection).""" scroll = QScrollArea() scroll.setWidgetResizable(True) container = QWidget() layout = QVBoxLayout() uart_group = QGroupBox("UART Logger Settings") form = QFormLayout() # Port with refresh port_layout = QHBoxLayout() self.log_port = QComboBox() self.log_port.setEditable(True) port_layout.addWidget(self.log_port) btn_refresh_log = QPushButton("🔄") btn_refresh_log.setMaximumWidth(40) btn_refresh_log.clicked.connect(lambda: self._refresh_uart_ports(self.log_port)) port_layout.addWidget(btn_refresh_log) form.addRow("Port:", port_layout) self.log_baud = QComboBox() self.log_baud.addItems(BAUD_RATES) self.log_baud.setCurrentText("115200") form.addRow("Baud Rate:", self.log_baud) self.log_data_bits = QSpinBox() self.log_data_bits.setRange(5, 8) self.log_data_bits.setValue(8) form.addRow("Data Bits:", self.log_data_bits) self.log_stop_bits = QSpinBox() self.log_stop_bits.setRange(1, 2) self.log_stop_bits.setValue(1) form.addRow("Stop Bits:", self.log_stop_bits) self.log_parity = QComboBox() self.log_parity.addItems(PARITY_OPTIONS) form.addRow("Parity:", self.log_parity) self.log_timeout = QSpinBox() self.log_timeout.setRange(100, 10000) self.log_timeout.setValue(1000) self.log_timeout.setSuffix(" ms") form.addRow("Timeout:", self.log_timeout) uart_group.setLayout(form) layout.addWidget(uart_group) # Packet Detection packet_group = QGroupBox("Packet Detection") packet_form = QFormLayout() self.packet_enable = QCheckBox("Enable Packet Detection") self.packet_enable.stateChanged.connect(self._enable_save) packet_form.addRow("", self.packet_enable) self.packet_start = QLineEdit("EF FE") packet_form.addRow("Start Marker:", self.packet_start) self.packet_length = QSpinBox() self.packet_length.setRange(1, 1024) self.packet_length.setValue(17) packet_form.addRow("Packet Length:", self.packet_length) self.packet_end = QLineEdit("EE") packet_form.addRow("End Marker:", self.packet_end) packet_group.setLayout(packet_form) layout.addWidget(packet_group) layout.addStretch() container.setLayout(layout) scroll.setWidget(container) # Initial port scan self._refresh_uart_ports(self.log_port) return scroll def _create_i2c_tab(self): """Create I2C tab.""" scroll = QScrollArea() scroll.setWidgetResizable(True) container = QWidget() layout = QVBoxLayout() i2c_group = QGroupBox("I2C Settings") form = QFormLayout() # Bus with refresh bus_layout = QHBoxLayout() self.i2c_bus = QComboBox() self.i2c_bus.setEditable(True) bus_layout.addWidget(self.i2c_bus) btn_refresh_i2c = QPushButton("🔄") btn_refresh_i2c.setMaximumWidth(40) btn_refresh_i2c.clicked.connect(self._refresh_i2c_buses) bus_layout.addWidget(btn_refresh_i2c) form.addRow("Bus:", bus_layout) self.i2c_address = QLineEdit("0x40") form.addRow("Slave Address:", self.i2c_address) self.i2c_register = QLineEdit("0xFE") form.addRow("Read Register:", self.i2c_register) self.i2c_length = QSpinBox() self.i2c_length.setRange(1, 256) self.i2c_length.setValue(2) form.addRow("Read Length:", self.i2c_length) i2c_group.setLayout(form) layout.addWidget(i2c_group) layout.addStretch() container.setLayout(layout) scroll.setWidget(container) # Initial bus scan self._refresh_i2c_buses() return scroll def _refresh_uart_ports(self, combo: QComboBox): """Refresh UART ports in combo box.""" current = combo.currentText().split(' - ')[0] if combo.currentText() else None combo.clear() if SERIAL_AVAILABLE: ports = list_ports.comports() for port in ports: display = f"{port.device} - {port.description}" combo.addItem(display, port.device) else: # Fallback: scan /dev/tty* for pattern in ['/dev/ttyUSB*', '/dev/ttyACM*', '/dev/ttyS*']: for port in glob.glob(pattern): combo.addItem(port, port) # Restore selection if exists if current: for i in range(combo.count()): if combo.itemData(i) == current: combo.setCurrentIndex(i) break def _refresh_i2c_buses(self): """Refresh I2C buses.""" current = self.i2c_bus.currentText() self.i2c_bus.clear() # Scan for I2C buses for i in range(10): # Check buses 0-9 bus_path = f"/dev/i2c-{i}" if os.path.exists(bus_path): self.i2c_bus.addItem(str(i)) # Restore selection if current: idx = self.i2c_bus.findText(current) if idx >= 0: self.i2c_bus.setCurrentIndex(idx) def _load_profiles(self): """Load profiles from database.""" self.profile_list.clear() cursor = self.db_conn.execute( "SELECT profile_id, profile_name FROM interface_profiles ORDER BY profile_name" ) for row in cursor.fetchall(): item = QListWidgetItem(row[1]) item.setData(Qt.ItemDataRole.UserRole, row[0]) self.profile_list.addItem(item) def _load_profile_details(self, profile_id: int): """Load profile into editor.""" cursor = self.db_conn.execute(""" SELECT profile_name, uart_command_port, uart_command_baud, uart_command_data_bits, uart_command_stop_bits, uart_command_parity, uart_command_timeout_ms, uart_logger_port, uart_logger_baud, uart_logger_data_bits, uart_logger_stop_bits, uart_logger_parity, uart_logger_timeout_ms, uart_logger_packet_detect_enable, uart_logger_packet_detect_start, uart_logger_packet_detect_length, uart_logger_packet_detect_end, i2c_port, i2c_slave_address, i2c_slave_read_register, i2c_slave_read_length FROM interface_profiles WHERE profile_id = ? """, (profile_id,)) row = cursor.fetchone() if row: self.name_input.setText(row[0] or "") # UART Command self._set_combo_value(self.cmd_port, row[1]) self.cmd_baud.setCurrentText(str(row[2] or 115200)) self.cmd_data_bits.setValue(row[3] or 8) self.cmd_stop_bits.setValue(row[4] or 1) self.cmd_parity.setCurrentText(row[5] or "N") self.cmd_timeout.setValue(row[6] or 1000) # UART Logger self._set_combo_value(self.log_port, row[7]) self.log_baud.setCurrentText(str(row[8] or 115200)) self.log_data_bits.setValue(row[9] or 8) self.log_stop_bits.setValue(row[10] or 1) self.log_parity.setCurrentText(row[11] or "N") self.log_timeout.setValue(row[12] or 1000) # Packet detection self.packet_enable.setChecked(bool(row[13])) self.packet_start.setText(row[14] or "") self.packet_length.setValue(row[15] or 17) self.packet_end.setText(row[16] or "") # I2C self.i2c_bus.setCurrentText(row[17] or "") self.i2c_address.setText(row[18] or "") self.i2c_register.setText(row[19] or "") self.i2c_length.setValue(row[20] or 2) self.current_profile_id = profile_id self.btn_delete.setEnabled(True) self.btn_save.setEnabled(True) # Always allow saving def _set_combo_value(self, combo: QComboBox, value: str): """Set combo value by matching device.""" if not value: return for i in range(combo.count()): if combo.itemData(i) == value: combo.setCurrentIndex(i) return # Not found, set as text combo.setCurrentText(value) def _on_profile_selected(self, current, previous): """Handle profile selection.""" if current: profile_id = current.data(Qt.ItemDataRole.UserRole) self._load_profile_details(profile_id) def _on_add_clicked(self): """Handle add button.""" self.name_input.clear() self._refresh_uart_ports(self.cmd_port) self._refresh_uart_ports(self.log_port) self._refresh_i2c_buses() self.profile_list.clearSelection() self.current_profile_id = None self.btn_delete.setEnabled(False) self.btn_save.setEnabled(True) self.name_input.setFocus() def _on_delete_clicked(self): """Handle delete button.""" if not self.current_profile_id: return reply = QMessageBox.question( self, "Confirm Delete", f"Delete profile '{self.name_input.text()}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return try: self.db_conn.execute( "DELETE FROM interface_profiles WHERE profile_id = ?", (self.current_profile_id,) ) self.db_conn.commit() self.profile_deleted.emit(self.current_profile_id) self._load_profiles() self.current_profile_id = None self.btn_save.setEnabled(False) self.btn_delete.setEnabled(False) QMessageBox.information(self, "Success", "Profile deleted") except Exception as e: QMessageBox.critical(self, "Error", f"Delete failed: {str(e)}") def _on_save_clicked(self): """Handle save button.""" name = self.name_input.text().strip() if not name: QMessageBox.warning(self, "Error", "Profile name required") return # Validate: UART Command and UART Logger must use different ports cmd_port = self.cmd_port.currentData() or self.cmd_port.currentText() log_port = self.log_port.currentData() or self.log_port.currentText() if cmd_port and log_port and cmd_port == log_port: QMessageBox.warning( self, "Port Conflict", "UART Command and UART Logger cannot use the same port!\n\n" f"Both are set to: {cmd_port}\n\n" "Please select different ports." ) return try: values = ( name, self.cmd_port.currentData() or self.cmd_port.currentText(), int(self.cmd_baud.currentText()), self.cmd_data_bits.value(), self.cmd_stop_bits.value(), self.cmd_parity.currentText(), self.cmd_timeout.value(), self.log_port.currentData() or self.log_port.currentText(), int(self.log_baud.currentText()), self.log_data_bits.value(), self.log_stop_bits.value(), self.log_parity.currentText(), self.log_timeout.value(), int(self.packet_enable.isChecked()), self.packet_start.text(), self.packet_length.value(), self.packet_end.text(), self.i2c_bus.currentText(), self.i2c_address.text(), self.i2c_register.text(), self.i2c_length.value() ) if self.current_profile_id: self.db_conn.execute(""" UPDATE interface_profiles SET profile_name=?, uart_command_port=?, uart_command_baud=?, uart_command_data_bits=?, uart_command_stop_bits=?, uart_command_parity=?, uart_command_timeout_ms=?, uart_logger_port=?, uart_logger_baud=?, uart_logger_data_bits=?, uart_logger_stop_bits=?, uart_logger_parity=?, uart_logger_timeout_ms=?, uart_logger_packet_detect_enable=?, uart_logger_packet_detect_start=?, uart_logger_packet_detect_length=?, uart_logger_packet_detect_end=?, i2c_port=?, i2c_slave_address=?, i2c_slave_read_register=?, i2c_slave_read_length=?, last_modified=datetime('now') WHERE profile_id=? """, values + (self.current_profile_id,)) msg = "Profile updated" else: cursor = self.db_conn.execute(""" INSERT INTO interface_profiles ( profile_name, uart_command_port, uart_command_baud, uart_command_data_bits, uart_command_stop_bits, uart_command_parity, uart_command_timeout_ms, uart_logger_port, uart_logger_baud, uart_logger_data_bits, uart_logger_stop_bits, uart_logger_parity, uart_logger_timeout_ms, uart_logger_packet_detect_enable, uart_logger_packet_detect_start, uart_logger_packet_detect_length, uart_logger_packet_detect_end, i2c_port, i2c_slave_address, i2c_slave_read_register, i2c_slave_read_length ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, values) self.current_profile_id = cursor.lastrowid msg = "Profile created" self.db_conn.commit() self.profile_saved.emit(self.current_profile_id) self._load_profiles() QMessageBox.information(self, "Success", msg) self.btn_save.setEnabled(False) except Exception as e: QMessageBox.critical(self, "Error", f"Save failed: {str(e)}") def _enable_save(self): """Enable save button.""" self.btn_save.setEnabled(True) if __name__ == "__main__": import sys from PyQt6.QtWidgets import QApplication app = QApplication(sys.argv) db = DatabaseManager("database/ehinge.db") db.initialize() widget = ConfigureInterfaceWidget(db) widget.setWindowTitle("Configure Interface Profiles") widget.resize(1000, 600) widget.show() sys.exit(app.exec())