diff --git a/configure_interface_widget.py b/configure_interface_widget.py index 559dd8c..4d82caf 100644 --- a/configure_interface_widget.py +++ b/configure_interface_widget.py @@ -177,6 +177,13 @@ class ConfigureInterfaceWidget(QWidget): 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() @@ -349,7 +356,7 @@ class ConfigureInterfaceWidget(QWidget): 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_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, @@ -369,25 +376,29 @@ class ConfigureInterfaceWidget(QWidget): 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[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) + 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[13])) - self.packet_start.setText(row[14] or "") - self.packet_length.setValue(row[15] or 17) - self.packet_end.setText(row[16] or "") + 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[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.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) @@ -458,19 +469,23 @@ class ConfigureInterfaceWidget(QWidget): 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 + # 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 = ( @@ -481,6 +496,7 @@ class ConfigureInterfaceWidget(QWidget): 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(), @@ -502,7 +518,7 @@ class ConfigureInterfaceWidget(QWidget): 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_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=?, @@ -517,13 +533,13 @@ class ConfigureInterfaceWidget(QWidget): 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_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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, values) self.current_profile_id = cursor.lastrowid msg = "Profile created" @@ -536,6 +552,26 @@ class ConfigureInterfaceWidget(QWidget): 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) diff --git a/configure_session_widget.py b/configure_session_widget.py index 6d73427..065b9a8 100644 --- a/configure_session_widget.py +++ b/configure_session_widget.py @@ -8,7 +8,7 @@ Visual command sequence builder. from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QLineEdit, QPushButton, QListWidget, QListWidgetItem, - QMessageBox, QSplitter, QFormLayout, QSpinBox, QComboBox + QMessageBox, QSplitter, QFormLayout, QSpinBox, QComboBox, QCheckBox ) from PyQt6.QtCore import Qt, pyqtSignal import json @@ -84,6 +84,15 @@ class ConfigureSessionWidget(QWidget): form.addRow("Profile Name:", self.name_input) self.desc_input = QLineEdit() form.addRow("Description:", self.desc_input) + + # Print RX checkbox + self.print_rx_checkbox = QCheckBox("Print Command RX (disable packet detection & DB saving)") + self.print_rx_checkbox.setToolTip( + "Enable: Print raw RX buffer to data monitor, skip database saving\n" + "Disable: Normal packet detection and database logging" + ) + form.addRow("", self.print_rx_checkbox) + layout.addLayout(form) # Add command section @@ -170,7 +179,7 @@ class ConfigureSessionWidget(QWidget): def _load_profile_details(self, profile_id: int): """Load profile into editor.""" cursor = self.db_conn.execute( - "SELECT profile_name, description, command_sequence FROM session_profiles WHERE profile_id = ?", + "SELECT profile_name, description, command_sequence, print_command_rx FROM session_profiles WHERE profile_id = ?", (profile_id,) ) row = cursor.fetchone() @@ -178,6 +187,10 @@ class ConfigureSessionWidget(QWidget): self.name_input.setText(row[0] or "") self.desc_input.setText(row[1] or "") + # Load print_command_rx checkbox + print_rx = bool(row[3]) if len(row) > 3 and row[3] is not None else False + self.print_rx_checkbox.setChecked(print_rx) + # Parse JSON and populate sequence try: json_obj = json.loads(row[2] or "{}") @@ -267,6 +280,7 @@ class ConfigureSessionWidget(QWidget): """Handle add button.""" self.name_input.clear() self.desc_input.clear() + self.print_rx_checkbox.setChecked(False) # Default to disabled self.command_sequence = [] self._update_sequence_list() @@ -320,6 +334,7 @@ class ConfigureSessionWidget(QWidget): return desc = self.desc_input.text().strip() + print_rx = 1 if self.print_rx_checkbox.isChecked() else 0 # Build JSON json_obj = { @@ -334,15 +349,15 @@ class ConfigureSessionWidget(QWidget): if self.current_profile_id: self.db_conn.execute( "UPDATE session_profiles SET profile_name=?, description=?, " - "command_sequence=?, last_modified=datetime('now') WHERE profile_id=?", - (name, desc, json_text, self.current_profile_id) + "command_sequence=?, print_command_rx=?, last_modified=datetime('now') WHERE profile_id=?", + (name, desc, json_text, print_rx, self.current_profile_id) ) msg = "Profile updated" else: cursor = self.db_conn.execute( - "INSERT INTO session_profiles (profile_name, description, command_sequence) " - "VALUES (?, ?, ?)", - (name, desc, json_text) + "INSERT INTO session_profiles (profile_name, description, command_sequence, print_command_rx) " + "VALUES (?, ?, ?, ?)", + (name, desc, json_text, print_rx) ) self.current_profile_id = cursor.lastrowid msg = "Profile created" diff --git a/database/ehinge.db b/database/ehinge.db index 4f59f59..46361b8 100644 Binary files a/database/ehinge.db and b/database/ehinge.db differ diff --git a/database/init_database.py b/database/init_database.py index 2f44b65..bbc3fcc 100644 --- a/database/init_database.py +++ b/database/init_database.py @@ -86,6 +86,7 @@ CREATE TABLE IF NOT EXISTS "interface_profiles" ( "uart_command_timeout_ms" INTEGER, -- UART Logger Interface + "uart_logger_enable" BOOLEAN DEFAULT 1, "uart_logger_mode" TEXT, "uart_logger_port" TEXT, "uart_logger_baud" INTEGER, @@ -116,6 +117,7 @@ CREATE TABLE IF NOT EXISTS "session_profiles" ( "profile_id" INTEGER PRIMARY KEY AUTOINCREMENT, "profile_name" TEXT UNIQUE NOT NULL, "description" TEXT, + "print_command_rx BOOLEAN DEFAULT 0; "command_sequence" TEXT, "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "last_modified" TIMESTAMP diff --git a/run.py b/run.py index d9e8306..77639c4 100644 --- a/run.py +++ b/run.py @@ -76,7 +76,8 @@ class RunExecutor: run_no: int, command_id: int, command_hex: str, - uart_port: UARTPort, + uart_command_port: UARTPort, + uart_logger_port: Optional[UARTPort], i2c_port: Optional[I2CHandle], packet_config: PacketConfig, stop_timeout_ms: int = 5000, @@ -91,7 +92,8 @@ class RunExecutor: run_no: Run number (1, 2, 3, ...) command_id: UART command ID from database command_hex: Command hex string (e.g., "DD 22 50 48...") - uart_port: UART port (already open and reader running) + uart_command_port: UART command port (TX/RX for commands) + uart_logger_port: UART logger port (RX for telemetry, None if disabled) i2c_port: I2C port (optional, for angle readings) packet_config: Packet detection configuration stop_timeout_ms: Maximum wait time for stop condition @@ -103,71 +105,75 @@ class RunExecutor: - error_msg: Error message if status="error", empty otherwise """ try: - # Clear previous packets - uart_clear_detected_packets(uart_port) + # Clear previous packets (only if logger port exists) + if uart_logger_port: + uart_clear_detected_packets(uart_logger_port) self.i2c_readings.clear() # Record run start time run_start_ns = time.time_ns() # ================================================================ - # 1. Configure packet detection with callback + # 1. Configure packet detection with callback (LOGGER PORT) # ================================================================ - # Create callback for I2C triggering - def on_uart_packet_detected(timestamp_ns: int): - """ - Called immediately when UART packet detected. - Triggers I2C read for timestamp correlation. - """ - if i2c_port: - # Read I2C angle immediately - # Note: i2c_read_block requires (handle, addr, reg, length) - # But we're using the handle's default address - status, i2c_bytes = i2c_read_block( - i2c_port, - i2c_port.config.address, # Use configured address - 0xFE, # Angle register - 2 # Read 2 bytes - ) - - if status == I2CStatus.OK: - # Store with correlated timestamp - self.i2c_readings.append({ - 'timestamp_ns': timestamp_ns, - 'i2c_bytes': i2c_bytes - }) - - # Create packet config with callback - packet_config_with_callback = PacketConfig( - enable=packet_config.enable, - start_marker=packet_config.start_marker, - packet_length=packet_config.packet_length, - end_marker=packet_config.end_marker, - on_packet_callback=on_uart_packet_detected if i2c_port else None - ) - - # Start listening with packet detection - status = uart_start_listening_with_packets(uart_port, packet_config_with_callback) - - if status != UARTStatus.OK: - return ("error", 0, "Failed to start UART packet detection") + if uart_logger_port and packet_config.enable: + # Create callback for I2C triggering + def on_uart_packet_detected(timestamp_ns: int): + """ + Called immediately when UART packet detected. + Triggers I2C read for timestamp correlation. + """ + if i2c_port: + # Read I2C angle immediately + # Note: i2c_read_block requires (handle, addr, reg, length) + # But we're using the handle's default address + status, i2c_bytes = i2c_read_block( + i2c_port, + i2c_port.config.address, # Use configured address + 0xFE, # Angle register + 2 # Read 2 bytes + ) + + if status == I2CStatus.OK: + # Store with correlated timestamp + self.i2c_readings.append({ + 'timestamp_ns': timestamp_ns, + 'i2c_bytes': i2c_bytes + }) + + # Create packet config with callback + packet_config_with_callback = PacketConfig( + enable=packet_config.enable, + start_marker=packet_config.start_marker, + packet_length=packet_config.packet_length, + end_marker=packet_config.end_marker, + on_packet_callback=on_uart_packet_detected if i2c_port else None + ) + + # Start listening with packet detection on LOGGER PORT + status = uart_start_listening_with_packets(uart_logger_port, packet_config_with_callback) + + if status != UARTStatus.OK: + return ("error", 0, "Failed to start UART packet detection") # ================================================================ - # 2. Send UART command + # 2. Send UART command (COMMAND PORT) # ================================================================ # Parse hex string to bytes command_bytes = self._parse_hex_string(command_hex) if not command_bytes: - uart_stop_listening(uart_port) + if uart_logger_port: + uart_stop_listening(uart_logger_port) return ("error", 0, f"Invalid command hex string: {command_hex}") - # Send command - status, written = uart_write(uart_port, command_bytes) + # Send command via COMMAND PORT + status, written = uart_write(uart_command_port, command_bytes) if status != UARTStatus.OK: - uart_stop_listening(uart_port) + if uart_logger_port: + uart_stop_listening(uart_logger_port) return ("error", 0, "Failed to send UART command") # Emit TX data (command sent) @@ -187,8 +193,8 @@ class RunExecutor: # ================================================================ if not packet_config.enable: - # No packet detection - read raw buffer - status_read, raw_data = uart_read_buffer(uart_port) + # No packet detection - read raw buffer from COMMAND PORT (ACK/response) + status_read, raw_data = uart_read_buffer(uart_command_port) if status_read == UARTStatus.OK and raw_data: # Emit RX data @@ -196,14 +202,19 @@ class RunExecutor: hex_rx = ' '.join(f'{b:02X}' for b in raw_data) raw_data_callback("RX", hex_rx) - # Stop listening - uart_stop_listening(uart_port) + # Stop listening on logger port (if active) + if uart_logger_port: + uart_stop_listening(uart_logger_port) # ================================================================ - # 4. Get detected packets + # 4. Get detected packets (from LOGGER PORT if exists) # ================================================================ - uart_packets = uart_get_detected_packets(uart_port) + uart_packets = [] + if uart_logger_port: + uart_packets = uart_get_detected_packets(uart_logger_port) + else: + uart_packets = [] packet_count = len(uart_packets) if packet_count == 0 and packet_config.enable: @@ -243,9 +254,10 @@ class RunExecutor: return ("success", packet_count, "") except Exception as e: - # Stop listening if still active + # Stop listening if still active (logger port) try: - uart_stop_listening(uart_port) + if uart_logger_port: + uart_stop_listening(uart_logger_port) except: pass @@ -395,7 +407,8 @@ def execute_run( run_no: int, command_id: int, command_hex: str, - uart_port: UARTPort, + uart_command_port: UARTPort, + uart_logger_port: Optional[UARTPort], i2c_port: Optional[I2CHandle], packet_config: PacketConfig, stop_timeout_ms: int = 5000, @@ -411,7 +424,8 @@ def execute_run( run_no: Run number command_id: UART command ID command_hex: Command hex string - uart_port: UART port (open and ready) + uart_command_port: UART command port (TX/RX for commands) + uart_logger_port: UART logger port (RX for telemetry, optional) i2c_port: I2C port (optional) packet_config: Packet detection configuration stop_timeout_ms: Stop condition timeout @@ -427,7 +441,8 @@ def execute_run( run_no=run_no, command_id=command_id, command_hex=command_hex, - uart_port=uart_port, + uart_command_port=uart_command_port, + uart_logger_port=uart_logger_port, i2c_port=i2c_port, packet_config=packet_config, stop_timeout_ms=stop_timeout_ms, diff --git a/session.py b/session.py index badbd49..930663b 100644 --- a/session.py +++ b/session.py @@ -131,7 +131,8 @@ class Session(QObject): self.session_profile_id: Optional[int] = None # Hardware ports (managed by this class) - self.uart_port: Optional[UARTPort] = None + self.uart_command_port: Optional[UARTPort] = None # For sending commands (TX/RX) + self.uart_logger_port: Optional[UARTPort] = None # For telemetry packets (RX only, optional) self.i2c_handle: Optional[I2CHandle] = None self.packet_config: Optional[PacketConfig] = None @@ -184,7 +185,7 @@ class Session(QObject): 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_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, @@ -210,22 +211,23 @@ class Session(QObject): 'uart_command_parity': row[5], 'uart_command_timeout_ms': row[6], # UART Logger interface - 'uart_logger_port': row[7], - 'uart_logger_baud': row[8], - 'uart_logger_data_bits': row[9], - 'uart_logger_stop_bits': row[10], - 'uart_logger_parity': row[11], - 'uart_logger_timeout_ms': row[12], + 'uart_logger_enable': bool(row[7]) if row[7] is not None else True, + 'uart_logger_port': row[8], + 'uart_logger_baud': row[9], + 'uart_logger_data_bits': row[10], + 'uart_logger_stop_bits': row[11], + 'uart_logger_parity': row[12], + 'uart_logger_timeout_ms': row[13], # Packet detection - 'packet_detect_enable': row[13], - 'packet_detect_start': row[14], - 'packet_detect_length': row[15], - 'packet_detect_end': row[16], + 'packet_detect_enable': row[14], + 'packet_detect_start': row[15], + 'packet_detect_length': row[16], + 'packet_detect_end': row[17], # I2C configuration - 'i2c_port': row[17], - 'i2c_slave_address': row[18], - 'i2c_slave_read_register': row[19], - 'i2c_slave_read_length': row[20] + 'i2c_port': row[18], + 'i2c_slave_address': row[19], + 'i2c_slave_read_register': row[20], + 'i2c_slave_read_length': row[21] } # =================================================================== @@ -233,7 +235,7 @@ class Session(QObject): # =================================================================== cursor = self.db_conn.execute(""" - SELECT profile_name, command_sequence, description + SELECT profile_name, command_sequence, description, print_command_rx FROM session_profiles WHERE profile_id = ? """, (session_profile_id,)) @@ -246,6 +248,7 @@ class Session(QObject): self.session_profile_id = session_profile_id profile_name = row[0] command_sequence_json = row[1] + self.print_command_rx = bool(row[3]) if len(row) > 3 else False # Parse JSON command sequence try: @@ -319,8 +322,8 @@ class Session(QObject): Open UART and I2C ports based on interface configuration. This method: - 1. Opens UART logger port (for packet detection) - 2. Starts UART reader thread + 1. Opens UART Command port (TX/RX - for sending commands, always needed) + 2. Opens UART Logger port (RX - for telemetry, optional) 3. Opens I2C port (for angle readings) 4. Creates PacketConfig from interface profile @@ -329,44 +332,104 @@ class Session(QObject): """ try: # =================================================================== - # 1. Open UART Logger Port (for packet detection) + # 1. Open UART Command Port (TX/RX - ALWAYS NEEDED) # =================================================================== - # Create UART config (only device and baudrate are required) - uart_config = UARTConfig( - device=self.interface_config['uart_logger_port'], - baudrate=self.interface_config['uart_logger_baud'] + # Create UART command config + cmd_uart_config = UARTConfig( + device=self.interface_config['uart_command_port'], + baudrate=self.interface_config['uart_command_baud'] ) - # Create UART port - status, self.uart_port = uart_create(uart_config) + # Create UART command port + status, self.uart_command_port = uart_create(cmd_uart_config) - if status != UARTStatus.OK or self.uart_port is None: - self.error_occurred.emit(f"Failed to create UART port") + if status != UARTStatus.OK or self.uart_command_port is None: + self.error_occurred.emit(f"Failed to create UART command port") return False - # Open UART port - status = uart_open(self.uart_port) + # Open UART command port + status = uart_open(self.uart_command_port) if status != UARTStatus.OK: - self.error_occurred.emit(f"Failed to open UART port {uart_config.device}") + self.error_occurred.emit(f"Failed to open UART command port {cmd_uart_config.device}") return False - # Start UART reader thread - status = uart_start_reader(self.uart_port) + # Start UART command reader thread + status = uart_start_reader(self.uart_command_port) if status != UARTStatus.OK: - uart_close(self.uart_port) - self.uart_port = None - self.error_occurred.emit("Failed to start UART reader") + uart_close(self.uart_command_port) + self.uart_command_port = None + self.error_occurred.emit("Failed to start UART command reader") return False - self.status_changed.emit(f"UART opened: {uart_config.device}") + self.status_changed.emit(f"UART Command opened: {cmd_uart_config.device}") # =================================================================== - # 2. Create PacketConfig from interface profile + # 2. Open UART Logger Port (RX - OPTIONAL) + # =================================================================== + + if self.interface_config['uart_logger_enable']: + # Logger enabled - open logger port + + # Create UART logger config + log_uart_config = UARTConfig( + device=self.interface_config['uart_logger_port'], + baudrate=self.interface_config['uart_logger_baud'] + ) + + # Create UART logger port + status, self.uart_logger_port = uart_create(log_uart_config) + + if status != UARTStatus.OK or self.uart_logger_port is None: + self.error_occurred.emit(f"Failed to create UART logger port") + # Close command port before returning + uart_stop_reader(self.uart_command_port) + uart_close(self.uart_command_port) + self.uart_command_port = None + return False + + # Open UART logger port + status = uart_open(self.uart_logger_port) + + if status != UARTStatus.OK: + self.error_occurred.emit(f"Failed to open UART logger port {log_uart_config.device}") + # Close command port before returning + uart_stop_reader(self.uart_command_port) + uart_close(self.uart_command_port) + self.uart_command_port = None + return False + + # Start UART logger reader thread + status = uart_start_reader(self.uart_logger_port) + if status != UARTStatus.OK: + uart_close(self.uart_logger_port) + self.uart_logger_port = None + # Close command port before returning + uart_stop_reader(self.uart_command_port) + uart_close(self.uart_command_port) + self.uart_command_port = None + self.error_occurred.emit("Failed to start UART logger reader") + return False + + self.status_changed.emit(f"UART Logger opened: {log_uart_config.device}") + else: + # Logger disabled - skip logger port + self.uart_logger_port = None + self.status_changed.emit("UART Logger disabled (command port only)") + + # =================================================================== + # 3. Create PacketConfig from interface profile # =================================================================== - if self.interface_config['packet_detect_enable']: + # Check if print_command_rx is enabled (session profile override) + if hasattr(self, 'print_command_rx') and self.print_command_rx: + # Force packet detection OFF - just print TX/RX to data monitor + self.packet_config = PacketConfig(enable=False) + elif not self.interface_config['uart_logger_enable']: + # Logger disabled - no packet detection + self.packet_config = PacketConfig(enable=False) + elif self.interface_config['packet_detect_enable']: # Parse hex markers start_marker = bytes.fromhex( self.interface_config['packet_detect_start'].replace(' ', '') @@ -387,7 +450,7 @@ class Session(QObject): self.packet_config = PacketConfig(enable=False) # =================================================================== - # 3. Open I2C Port (optional - for angle readings) + # 4. Open I2C Port (optional - for angle readings) # =================================================================== if self.interface_config['i2c_port']: @@ -444,7 +507,11 @@ class Session(QObject): # 2. Create PacketConfig from interface profile # =================================================================== - if self.interface_config['packet_detect_enable']: + # Check if print_command_rx is enabled (session profile override) + if hasattr(self, 'print_command_rx') and self.print_command_rx: + # Force packet detection OFF - just print TX/RX to data monitor + self.packet_config = PacketConfig(enable=False) + elif self.interface_config['packet_detect_enable']: # Parse hex markers start_marker = bytes.fromhex( self.interface_config['packet_detect_start'].replace(' ', '') @@ -509,12 +576,19 @@ class Session(QObject): Ensures clean shutdown of hardware interfaces. """ try: - # Close UART - if self.uart_port: - uart_stop_reader(self.uart_port) - uart_close(self.uart_port) - self.uart_port = None - self.status_changed.emit("UART closed") + # Close UART Command port + if self.uart_command_port: + uart_stop_reader(self.uart_command_port) + uart_close(self.uart_command_port) + self.uart_command_port = None + self.status_changed.emit("UART Command closed") + + # Close UART Logger port + if self.uart_logger_port: + uart_stop_reader(self.uart_logger_port) + uart_close(self.uart_logger_port) + self.uart_logger_port = None + self.status_changed.emit("UART Logger closed") # Close I2C if self.i2c_handle: @@ -659,7 +733,8 @@ class Session(QObject): run_no=cmd_index, command_id=cmd['command_id'], command_hex=cmd['hex_string'], - uart_port=self.uart_port, + uart_command_port=self.uart_command_port, + uart_logger_port=self.uart_logger_port, i2c_port=self.i2c_handle, # Note: run.py calls it i2c_port but it's actually I2CHandle packet_config=self.packet_config, stop_timeout_ms=5000, diff --git a/session_widget.py b/session_widget.py index f501d02..1de2c7d 100644 --- a/session_widget.py +++ b/session_widget.py @@ -33,6 +33,7 @@ from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtGui import QFont from session import Session +from session_worker import SessionWorker from database.init_database import DatabaseManager @@ -93,6 +94,9 @@ class SessionWidget(QWidget): # Create session object self.session = Session(db_manager) + # Worker thread for non-blocking execution + self.worker = None + # Connect signals from session to widget slots self._connect_signals() @@ -332,20 +336,20 @@ class SessionWidget(QWidget): self.session_profile_combo.setEnabled(False) self.session_name_input.setEnabled(False) - # Start session - success = self.session.start_session() + # Create worker thread for non-blocking execution + # Pass database path for thread-local connection + self.worker = SessionWorker(self.session, self.db_manager.db_path) - if success: - # Enable pause/stop buttons - self.pause_button.setEnabled(True) - self.stop_button.setEnabled(True) - else: - # Re-enable start button on failure - self.start_button.setEnabled(True) - self.load_button.setEnabled(True) - self.interface_profile_combo.setEnabled(True) - self.session_profile_combo.setEnabled(True) - self.session_name_input.setEnabled(True) + # Connect worker signals + self.worker.finished.connect(self._on_worker_finished) + + # Start worker thread + self.worker.start() + + # Enable pause/stop buttons immediately + # (worker is running, session will start soon) + self.pause_button.setEnabled(True) + self.stop_button.setEnabled(True) def _on_pause_clicked(self): """Handle pause button click.""" @@ -354,14 +358,54 @@ class SessionWidget(QWidget): def _on_resume_clicked(self): """Handle resume button click.""" - self.session.resume_session() - # Button states will be updated by signal handlers + # Disable resume button + self.resume_button.setEnabled(False) + + # Create worker thread for resume + # Pass database path for thread-local connection + self.worker = SessionWorker(self.session, self.db_manager.db_path, resume_mode=True) + + # Connect worker signals + self.worker.finished.connect(self._on_worker_finished) + + # Start worker thread + self.worker.start() + + # Enable pause/stop buttons + self.pause_button.setEnabled(True) + self.stop_button.setEnabled(True) def _on_stop_clicked(self): """Handle stop button click.""" self.session.stop_session() # Button states will be updated by signal handlers + @pyqtSlot(bool) + def _on_worker_finished(self, success: bool): + """ + Handle worker thread finished. + + Called when session execution completes (success or failure). + Re-enables UI controls for next session. + """ + # Cleanup worker + if self.worker: + self.worker = None + + # Re-enable controls + self.start_button.setEnabled(True) + self.load_button.setEnabled(True) + self.interface_profile_combo.setEnabled(True) + self.session_profile_combo.setEnabled(True) + self.session_name_input.setEnabled(True) + + # Disable pause/stop buttons + self.pause_button.setEnabled(False) + self.stop_button.setEnabled(False) + + if not success: + self._log_error("Session execution failed") + # ========================================================================= # SIGNAL HANDLERS (SLOTS) # ========================================================================= diff --git a/session_worker.py b/session_worker.py new file mode 100644 index 0000000..e969034 --- /dev/null +++ b/session_worker.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Session Worker Thread - vzug-e-hinge +==================================== +QThread worker for non-blocking session execution. + +This worker runs session.start_session() or session.resume_session() +in a background thread, preventing GUI freezing and enabling +real-time status updates. + +IMPORTANT: Creates thread-local database connection to avoid SQLite +threading errors. + +Author: Kynsight +Version: 1.1.0 +Date: 2025-11-09 +""" + +from PyQt6.QtCore import QThread, pyqtSignal +import sqlite3 + + +class SessionWorker(QThread): + """ + Worker thread for session execution. + + Runs session.start_session() or resume_session() in background + to prevent GUI blocking. Emits finished signal when session completes. + + CRITICAL: Creates a new database connection in the worker thread + to avoid SQLite's "objects created in a thread can only be used + in that same thread" error. + """ + + # Signals + finished = pyqtSignal(bool) # success/failure + + def __init__(self, session, db_path, resume_mode=False): + """ + Initialize worker. + + Args: + session: Session instance to execute + db_path: Path to database file (for thread-local connection) + resume_mode: True to resume, False to start (default) + """ + super().__init__() + self.session = session + self.db_path = db_path + self.resume_mode = resume_mode + + def run(self): + """ + Execute session in background thread. + + This method runs in a separate thread, keeping the GUI responsive. + Creates a thread-local database connection to avoid SQLite errors. + All session signals (status_changed, command_started, etc.) are + automatically thread-safe thanks to Qt's signal/slot mechanism. + """ + try: + # Create thread-local database connection + thread_db_conn = sqlite3.connect(self.db_path) + + # Replace session's database connection with thread-local one + old_conn = self.session.db_conn + self.session.db_conn = thread_db_conn + + try: + if self.resume_mode: + # Resume paused session + self.session.resume_session() + success = True + else: + # Start new session + success = self.session.start_session() + + # Emit result + self.finished.emit(success) + + finally: + # Close thread-local connection + thread_db_conn.close() + + # Restore original connection + self.session.db_conn = old_conn + + except Exception as e: + print(f"[ERROR] Worker exception: {e}") + self.finished.emit(False) diff --git a/vzug-e-hinge.zip b/vzug-e-hinge.zip index 774df55..b5059e5 100644 Binary files a/vzug-e-hinge.zip and b/vzug-e-hinge.zip differ