You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vguz_v2/configure_interface_widget.py

590 lines
22 KiB

#!/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", "256000" ,"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()
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()
# Enable Logger checkbox
self.logger_enable = QCheckBox("Enable UART Logger")
self.logger_enable.setChecked(True) # Default enabled
self.logger_enable.setToolTip("Uncheck to skip logger port configuration (command port only)")
self.logger_enable.stateChanged.connect(self._on_logger_enable_changed)
layout.addWidget(self.logger_enable)
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_enable, 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 Enable
logger_enabled = bool(row[7]) if row[7] is not None else True # Default enabled
self.logger_enable.setChecked(logger_enabled)
# UART Logger
self._set_combo_value(self.log_port, row[8])
self.log_baud.setCurrentText(str(row[9] or 115200))
self.log_data_bits.setValue(row[10] or 8)
self.log_stop_bits.setValue(row[11] or 1)
self.log_parity.setCurrentText(row[12] or "N")
self.log_timeout.setValue(row[13] or 1000)
# Packet detection
self.packet_enable.setChecked(bool(row[14]))
self.packet_start.setText(row[15] or "")
self.packet_length.setValue(row[16] or 17)
self.packet_end.setText(row[17] or "")
# I2C
self.i2c_bus.setCurrentText(row[18] or "")
self.i2c_address.setText(row[19] or "")
self.i2c_register.setText(row[20] or "")
self.i2c_length.setValue(row[21] 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
# Get logger enable state
logger_enabled = self.logger_enable.isChecked()
# Validate: UART Command and UART Logger must use different ports (only if logger enabled)
if logger_enabled:
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(),
int(logger_enabled), # uart_logger_enable
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_enable=?, 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_enable, 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 _on_logger_enable_changed(self, state):
"""Handle logger enable checkbox change."""
enabled = (state == Qt.CheckState.Checked.value)
# Enable/disable all logger fields
self.log_port.setEnabled(enabled)
self.log_baud.setEnabled(enabled)
self.log_data_bits.setEnabled(enabled)
self.log_stop_bits.setEnabled(enabled)
self.log_parity.setEnabled(enabled)
self.log_timeout.setEnabled(enabled)
# Enable/disable packet detection
self.packet_enable.setEnabled(enabled)
self.packet_start.setEnabled(enabled)
self.packet_length.setEnabled(enabled)
self.packet_end.setEnabled(enabled)
self._enable_save()
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())