first session run correctly

main
Kynsight 1 month ago
parent a520d8f17f
commit 0277eec76a

@ -177,6 +177,13 @@ class ConfigureInterfaceWidget(QWidget):
container = QWidget() container = QWidget()
layout = QVBoxLayout() 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") uart_group = QGroupBox("UART Logger Settings")
form = QFormLayout() form = QFormLayout()
@ -349,7 +356,7 @@ class ConfigureInterfaceWidget(QWidget):
SELECT profile_name, SELECT profile_name,
uart_command_port, uart_command_baud, uart_command_data_bits, uart_command_port, uart_command_baud, uart_command_data_bits,
uart_command_stop_bits, uart_command_parity, uart_command_timeout_ms, 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_stop_bits, uart_logger_parity, uart_logger_timeout_ms,
uart_logger_packet_detect_enable, uart_logger_packet_detect_start, uart_logger_packet_detect_enable, uart_logger_packet_detect_start,
uart_logger_packet_detect_length, uart_logger_packet_detect_end, 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_parity.setCurrentText(row[5] or "N")
self.cmd_timeout.setValue(row[6] or 1000) 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 # UART Logger
self._set_combo_value(self.log_port, row[7]) self._set_combo_value(self.log_port, row[8])
self.log_baud.setCurrentText(str(row[8] or 115200)) self.log_baud.setCurrentText(str(row[9] or 115200))
self.log_data_bits.setValue(row[9] or 8) self.log_data_bits.setValue(row[10] or 8)
self.log_stop_bits.setValue(row[10] or 1) self.log_stop_bits.setValue(row[11] or 1)
self.log_parity.setCurrentText(row[11] or "N") self.log_parity.setCurrentText(row[12] or "N")
self.log_timeout.setValue(row[12] or 1000) self.log_timeout.setValue(row[13] or 1000)
# Packet detection # Packet detection
self.packet_enable.setChecked(bool(row[13])) self.packet_enable.setChecked(bool(row[14]))
self.packet_start.setText(row[14] or "") self.packet_start.setText(row[15] or "")
self.packet_length.setValue(row[15] or 17) self.packet_length.setValue(row[16] or 17)
self.packet_end.setText(row[16] or "") self.packet_end.setText(row[17] or "")
# I2C # I2C
self.i2c_bus.setCurrentText(row[17] or "") self.i2c_bus.setCurrentText(row[18] or "")
self.i2c_address.setText(row[18] or "") self.i2c_address.setText(row[19] or "")
self.i2c_register.setText(row[19] or "") self.i2c_register.setText(row[20] or "")
self.i2c_length.setValue(row[20] or 2) self.i2c_length.setValue(row[21] or 2)
self.current_profile_id = profile_id self.current_profile_id = profile_id
self.btn_delete.setEnabled(True) self.btn_delete.setEnabled(True)
@ -458,19 +469,23 @@ class ConfigureInterfaceWidget(QWidget):
QMessageBox.warning(self, "Error", "Profile name required") QMessageBox.warning(self, "Error", "Profile name required")
return return
# Validate: UART Command and UART Logger must use different ports # Get logger enable state
cmd_port = self.cmd_port.currentData() or self.cmd_port.currentText() logger_enabled = self.logger_enable.isChecked()
log_port = self.log_port.currentData() or self.log_port.currentText()
# Validate: UART Command and UART Logger must use different ports (only if logger enabled)
if cmd_port and log_port and cmd_port == log_port: if logger_enabled:
QMessageBox.warning( cmd_port = self.cmd_port.currentData() or self.cmd_port.currentText()
self, log_port = self.log_port.currentData() or self.log_port.currentText()
"Port Conflict",
"UART Command and UART Logger cannot use the same port!\n\n" if cmd_port and log_port and cmd_port == log_port:
f"Both are set to: {cmd_port}\n\n" QMessageBox.warning(
"Please select different ports." self,
) "Port Conflict",
return "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: try:
values = ( values = (
@ -481,6 +496,7 @@ class ConfigureInterfaceWidget(QWidget):
self.cmd_stop_bits.value(), self.cmd_stop_bits.value(),
self.cmd_parity.currentText(), self.cmd_parity.currentText(),
self.cmd_timeout.value(), self.cmd_timeout.value(),
int(logger_enabled), # uart_logger_enable
self.log_port.currentData() or self.log_port.currentText(), self.log_port.currentData() or self.log_port.currentText(),
int(self.log_baud.currentText()), int(self.log_baud.currentText()),
self.log_data_bits.value(), self.log_data_bits.value(),
@ -502,7 +518,7 @@ class ConfigureInterfaceWidget(QWidget):
UPDATE interface_profiles SET UPDATE interface_profiles SET
profile_name=?, uart_command_port=?, uart_command_baud=?, profile_name=?, uart_command_port=?, uart_command_baud=?,
uart_command_data_bits=?, uart_command_stop_bits=?, uart_command_parity=?, 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_data_bits=?, uart_logger_stop_bits=?, uart_logger_parity=?,
uart_logger_timeout_ms=?, uart_logger_packet_detect_enable=?, uart_logger_timeout_ms=?, uart_logger_packet_detect_enable=?,
uart_logger_packet_detect_start=?, uart_logger_packet_detect_length=?, uart_logger_packet_detect_start=?, uart_logger_packet_detect_length=?,
@ -517,13 +533,13 @@ class ConfigureInterfaceWidget(QWidget):
INSERT INTO interface_profiles ( INSERT INTO interface_profiles (
profile_name, uart_command_port, uart_command_baud, profile_name, uart_command_port, uart_command_baud,
uart_command_data_bits, uart_command_stop_bits, uart_command_parity, 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_data_bits, uart_logger_stop_bits, uart_logger_parity,
uart_logger_timeout_ms, uart_logger_packet_detect_enable, uart_logger_timeout_ms, uart_logger_packet_detect_enable,
uart_logger_packet_detect_start, uart_logger_packet_detect_length, uart_logger_packet_detect_start, uart_logger_packet_detect_length,
uart_logger_packet_detect_end, i2c_port, i2c_slave_address, uart_logger_packet_detect_end, i2c_port, i2c_slave_address,
i2c_slave_read_register, i2c_slave_read_length i2c_slave_read_register, i2c_slave_read_length
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", values) """, values)
self.current_profile_id = cursor.lastrowid self.current_profile_id = cursor.lastrowid
msg = "Profile created" msg = "Profile created"
@ -536,6 +552,26 @@ class ConfigureInterfaceWidget(QWidget):
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Error", f"Save failed: {str(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): def _enable_save(self):
"""Enable save button.""" """Enable save button."""
self.btn_save.setEnabled(True) self.btn_save.setEnabled(True)

@ -8,7 +8,7 @@ Visual command sequence builder.
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel,
QLineEdit, QPushButton, QListWidget, QListWidgetItem, QLineEdit, QPushButton, QListWidget, QListWidgetItem,
QMessageBox, QSplitter, QFormLayout, QSpinBox, QComboBox QMessageBox, QSplitter, QFormLayout, QSpinBox, QComboBox, QCheckBox
) )
from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtCore import Qt, pyqtSignal
import json import json
@ -84,6 +84,15 @@ class ConfigureSessionWidget(QWidget):
form.addRow("Profile Name:", self.name_input) form.addRow("Profile Name:", self.name_input)
self.desc_input = QLineEdit() self.desc_input = QLineEdit()
form.addRow("Description:", self.desc_input) 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) layout.addLayout(form)
# Add command section # Add command section
@ -170,7 +179,7 @@ class ConfigureSessionWidget(QWidget):
def _load_profile_details(self, profile_id: int): def _load_profile_details(self, profile_id: int):
"""Load profile into editor.""" """Load profile into editor."""
cursor = self.db_conn.execute( 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,) (profile_id,)
) )
row = cursor.fetchone() row = cursor.fetchone()
@ -178,6 +187,10 @@ class ConfigureSessionWidget(QWidget):
self.name_input.setText(row[0] or "") self.name_input.setText(row[0] or "")
self.desc_input.setText(row[1] 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 # Parse JSON and populate sequence
try: try:
json_obj = json.loads(row[2] or "{}") json_obj = json.loads(row[2] or "{}")
@ -267,6 +280,7 @@ class ConfigureSessionWidget(QWidget):
"""Handle add button.""" """Handle add button."""
self.name_input.clear() self.name_input.clear()
self.desc_input.clear() self.desc_input.clear()
self.print_rx_checkbox.setChecked(False) # Default to disabled
self.command_sequence = [] self.command_sequence = []
self._update_sequence_list() self._update_sequence_list()
@ -320,6 +334,7 @@ class ConfigureSessionWidget(QWidget):
return return
desc = self.desc_input.text().strip() desc = self.desc_input.text().strip()
print_rx = 1 if self.print_rx_checkbox.isChecked() else 0
# Build JSON # Build JSON
json_obj = { json_obj = {
@ -334,15 +349,15 @@ class ConfigureSessionWidget(QWidget):
if self.current_profile_id: if self.current_profile_id:
self.db_conn.execute( self.db_conn.execute(
"UPDATE session_profiles SET profile_name=?, description=?, " "UPDATE session_profiles SET profile_name=?, description=?, "
"command_sequence=?, last_modified=datetime('now') WHERE profile_id=?", "command_sequence=?, print_command_rx=?, last_modified=datetime('now') WHERE profile_id=?",
(name, desc, json_text, self.current_profile_id) (name, desc, json_text, print_rx, self.current_profile_id)
) )
msg = "Profile updated" msg = "Profile updated"
else: else:
cursor = self.db_conn.execute( cursor = self.db_conn.execute(
"INSERT INTO session_profiles (profile_name, description, command_sequence) " "INSERT INTO session_profiles (profile_name, description, command_sequence, print_command_rx) "
"VALUES (?, ?, ?)", "VALUES (?, ?, ?, ?)",
(name, desc, json_text) (name, desc, json_text, print_rx)
) )
self.current_profile_id = cursor.lastrowid self.current_profile_id = cursor.lastrowid
msg = "Profile created" msg = "Profile created"

Binary file not shown.

@ -86,6 +86,7 @@ CREATE TABLE IF NOT EXISTS "interface_profiles" (
"uart_command_timeout_ms" INTEGER, "uart_command_timeout_ms" INTEGER,
-- UART Logger Interface -- UART Logger Interface
"uart_logger_enable" BOOLEAN DEFAULT 1,
"uart_logger_mode" TEXT, "uart_logger_mode" TEXT,
"uart_logger_port" TEXT, "uart_logger_port" TEXT,
"uart_logger_baud" INTEGER, "uart_logger_baud" INTEGER,
@ -116,6 +117,7 @@ CREATE TABLE IF NOT EXISTS "session_profiles" (
"profile_id" INTEGER PRIMARY KEY AUTOINCREMENT, "profile_id" INTEGER PRIMARY KEY AUTOINCREMENT,
"profile_name" TEXT UNIQUE NOT NULL, "profile_name" TEXT UNIQUE NOT NULL,
"description" TEXT, "description" TEXT,
"print_command_rx BOOLEAN DEFAULT 0;
"command_sequence" TEXT, "command_sequence" TEXT,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"last_modified" TIMESTAMP "last_modified" TIMESTAMP

133
run.py

@ -76,7 +76,8 @@ class RunExecutor:
run_no: int, run_no: int,
command_id: int, command_id: int,
command_hex: str, command_hex: str,
uart_port: UARTPort, uart_command_port: UARTPort,
uart_logger_port: Optional[UARTPort],
i2c_port: Optional[I2CHandle], i2c_port: Optional[I2CHandle],
packet_config: PacketConfig, packet_config: PacketConfig,
stop_timeout_ms: int = 5000, stop_timeout_ms: int = 5000,
@ -91,7 +92,8 @@ class RunExecutor:
run_no: Run number (1, 2, 3, ...) run_no: Run number (1, 2, 3, ...)
command_id: UART command ID from database command_id: UART command ID from database
command_hex: Command hex string (e.g., "DD 22 50 48...") 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) i2c_port: I2C port (optional, for angle readings)
packet_config: Packet detection configuration packet_config: Packet detection configuration
stop_timeout_ms: Maximum wait time for stop condition stop_timeout_ms: Maximum wait time for stop condition
@ -103,71 +105,75 @@ class RunExecutor:
- error_msg: Error message if status="error", empty otherwise - error_msg: Error message if status="error", empty otherwise
""" """
try: try:
# Clear previous packets # Clear previous packets (only if logger port exists)
uart_clear_detected_packets(uart_port) if uart_logger_port:
uart_clear_detected_packets(uart_logger_port)
self.i2c_readings.clear() self.i2c_readings.clear()
# Record run start time # Record run start time
run_start_ns = time.time_ns() 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 if uart_logger_port and packet_config.enable:
def on_uart_packet_detected(timestamp_ns: int): # 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. Called immediately when UART packet detected.
""" Triggers I2C read for timestamp correlation.
if i2c_port: """
# Read I2C angle immediately if i2c_port:
# Note: i2c_read_block requires (handle, addr, reg, length) # Read I2C angle immediately
# But we're using the handle's default address # Note: i2c_read_block requires (handle, addr, reg, length)
status, i2c_bytes = i2c_read_block( # But we're using the handle's default address
i2c_port, status, i2c_bytes = i2c_read_block(
i2c_port.config.address, # Use configured address i2c_port,
0xFE, # Angle register i2c_port.config.address, # Use configured address
2 # Read 2 bytes 0xFE, # Angle register
) 2 # Read 2 bytes
)
if status == I2CStatus.OK:
# Store with correlated timestamp if status == I2CStatus.OK:
self.i2c_readings.append({ # Store with correlated timestamp
'timestamp_ns': timestamp_ns, self.i2c_readings.append({
'i2c_bytes': i2c_bytes 'timestamp_ns': timestamp_ns,
}) 'i2c_bytes': i2c_bytes
})
# Create packet config with callback
packet_config_with_callback = PacketConfig( # Create packet config with callback
enable=packet_config.enable, packet_config_with_callback = PacketConfig(
start_marker=packet_config.start_marker, enable=packet_config.enable,
packet_length=packet_config.packet_length, start_marker=packet_config.start_marker,
end_marker=packet_config.end_marker, packet_length=packet_config.packet_length,
on_packet_callback=on_uart_packet_detected if i2c_port else None 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) # 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") 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 # Parse hex string to bytes
command_bytes = self._parse_hex_string(command_hex) command_bytes = self._parse_hex_string(command_hex)
if not command_bytes: 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}") return ("error", 0, f"Invalid command hex string: {command_hex}")
# Send command # Send command via COMMAND PORT
status, written = uart_write(uart_port, command_bytes) status, written = uart_write(uart_command_port, command_bytes)
if status != UARTStatus.OK: 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") return ("error", 0, "Failed to send UART command")
# Emit TX data (command sent) # Emit TX data (command sent)
@ -187,8 +193,8 @@ class RunExecutor:
# ================================================================ # ================================================================
if not packet_config.enable: if not packet_config.enable:
# No packet detection - read raw buffer # No packet detection - read raw buffer from COMMAND PORT (ACK/response)
status_read, raw_data = uart_read_buffer(uart_port) status_read, raw_data = uart_read_buffer(uart_command_port)
if status_read == UARTStatus.OK and raw_data: if status_read == UARTStatus.OK and raw_data:
# Emit RX data # Emit RX data
@ -196,14 +202,19 @@ class RunExecutor:
hex_rx = ' '.join(f'{b:02X}' for b in raw_data) hex_rx = ' '.join(f'{b:02X}' for b in raw_data)
raw_data_callback("RX", hex_rx) raw_data_callback("RX", hex_rx)
# Stop listening # Stop listening on logger port (if active)
uart_stop_listening(uart_port) 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) packet_count = len(uart_packets)
if packet_count == 0 and packet_config.enable: if packet_count == 0 and packet_config.enable:
@ -243,9 +254,10 @@ class RunExecutor:
return ("success", packet_count, "") return ("success", packet_count, "")
except Exception as e: except Exception as e:
# Stop listening if still active # Stop listening if still active (logger port)
try: try:
uart_stop_listening(uart_port) if uart_logger_port:
uart_stop_listening(uart_logger_port)
except: except:
pass pass
@ -395,7 +407,8 @@ def execute_run(
run_no: int, run_no: int,
command_id: int, command_id: int,
command_hex: str, command_hex: str,
uart_port: UARTPort, uart_command_port: UARTPort,
uart_logger_port: Optional[UARTPort],
i2c_port: Optional[I2CHandle], i2c_port: Optional[I2CHandle],
packet_config: PacketConfig, packet_config: PacketConfig,
stop_timeout_ms: int = 5000, stop_timeout_ms: int = 5000,
@ -411,7 +424,8 @@ def execute_run(
run_no: Run number run_no: Run number
command_id: UART command ID command_id: UART command ID
command_hex: Command hex string 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) i2c_port: I2C port (optional)
packet_config: Packet detection configuration packet_config: Packet detection configuration
stop_timeout_ms: Stop condition timeout stop_timeout_ms: Stop condition timeout
@ -427,7 +441,8 @@ def execute_run(
run_no=run_no, run_no=run_no,
command_id=command_id, command_id=command_id,
command_hex=command_hex, command_hex=command_hex,
uart_port=uart_port, uart_command_port=uart_command_port,
uart_logger_port=uart_logger_port,
i2c_port=i2c_port, i2c_port=i2c_port,
packet_config=packet_config, packet_config=packet_config,
stop_timeout_ms=stop_timeout_ms, stop_timeout_ms=stop_timeout_ms,

@ -131,7 +131,8 @@ class Session(QObject):
self.session_profile_id: Optional[int] = None self.session_profile_id: Optional[int] = None
# Hardware ports (managed by this class) # 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.i2c_handle: Optional[I2CHandle] = None
self.packet_config: Optional[PacketConfig] = None self.packet_config: Optional[PacketConfig] = None
@ -184,7 +185,7 @@ class Session(QObject):
profile_name, profile_name,
uart_command_port, uart_command_baud, uart_command_data_bits, uart_command_port, uart_command_baud, uart_command_data_bits,
uart_command_stop_bits, uart_command_parity, uart_command_timeout_ms, 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_stop_bits, uart_logger_parity, uart_logger_timeout_ms,
uart_logger_packet_detect_enable, uart_logger_packet_detect_start, uart_logger_packet_detect_enable, uart_logger_packet_detect_start,
uart_logger_packet_detect_length, uart_logger_packet_detect_end, uart_logger_packet_detect_length, uart_logger_packet_detect_end,
@ -210,22 +211,23 @@ class Session(QObject):
'uart_command_parity': row[5], 'uart_command_parity': row[5],
'uart_command_timeout_ms': row[6], 'uart_command_timeout_ms': row[6],
# UART Logger interface # UART Logger interface
'uart_logger_port': row[7], 'uart_logger_enable': bool(row[7]) if row[7] is not None else True,
'uart_logger_baud': row[8], 'uart_logger_port': row[8],
'uart_logger_data_bits': row[9], 'uart_logger_baud': row[9],
'uart_logger_stop_bits': row[10], 'uart_logger_data_bits': row[10],
'uart_logger_parity': row[11], 'uart_logger_stop_bits': row[11],
'uart_logger_timeout_ms': row[12], 'uart_logger_parity': row[12],
'uart_logger_timeout_ms': row[13],
# Packet detection # Packet detection
'packet_detect_enable': row[13], 'packet_detect_enable': row[14],
'packet_detect_start': row[14], 'packet_detect_start': row[15],
'packet_detect_length': row[15], 'packet_detect_length': row[16],
'packet_detect_end': row[16], 'packet_detect_end': row[17],
# I2C configuration # I2C configuration
'i2c_port': row[17], 'i2c_port': row[18],
'i2c_slave_address': row[18], 'i2c_slave_address': row[19],
'i2c_slave_read_register': row[19], 'i2c_slave_read_register': row[20],
'i2c_slave_read_length': row[20] 'i2c_slave_read_length': row[21]
} }
# =================================================================== # ===================================================================
@ -233,7 +235,7 @@ class Session(QObject):
# =================================================================== # ===================================================================
cursor = self.db_conn.execute(""" cursor = self.db_conn.execute("""
SELECT profile_name, command_sequence, description SELECT profile_name, command_sequence, description, print_command_rx
FROM session_profiles FROM session_profiles
WHERE profile_id = ? WHERE profile_id = ?
""", (session_profile_id,)) """, (session_profile_id,))
@ -246,6 +248,7 @@ class Session(QObject):
self.session_profile_id = session_profile_id self.session_profile_id = session_profile_id
profile_name = row[0] profile_name = row[0]
command_sequence_json = row[1] command_sequence_json = row[1]
self.print_command_rx = bool(row[3]) if len(row) > 3 else False
# Parse JSON command sequence # Parse JSON command sequence
try: try:
@ -319,8 +322,8 @@ class Session(QObject):
Open UART and I2C ports based on interface configuration. Open UART and I2C ports based on interface configuration.
This method: This method:
1. Opens UART logger port (for packet detection) 1. Opens UART Command port (TX/RX - for sending commands, always needed)
2. Starts UART reader thread 2. Opens UART Logger port (RX - for telemetry, optional)
3. Opens I2C port (for angle readings) 3. Opens I2C port (for angle readings)
4. Creates PacketConfig from interface profile 4. Creates PacketConfig from interface profile
@ -329,44 +332,104 @@ class Session(QObject):
""" """
try: 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) # Create UART command config
uart_config = UARTConfig( cmd_uart_config = UARTConfig(
device=self.interface_config['uart_logger_port'], device=self.interface_config['uart_command_port'],
baudrate=self.interface_config['uart_logger_baud'] baudrate=self.interface_config['uart_command_baud']
) )
# Create UART port # Create UART command port
status, self.uart_port = uart_create(uart_config) status, self.uart_command_port = uart_create(cmd_uart_config)
if status != UARTStatus.OK or self.uart_port is None: if status != UARTStatus.OK or self.uart_command_port is None:
self.error_occurred.emit(f"Failed to create UART port") self.error_occurred.emit(f"Failed to create UART command port")
return False return False
# Open UART port # Open UART command port
status = uart_open(self.uart_port) status = uart_open(self.uart_command_port)
if status != UARTStatus.OK: 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 return False
# Start UART reader thread # Start UART command reader thread
status = uart_start_reader(self.uart_port) status = uart_start_reader(self.uart_command_port)
if status != UARTStatus.OK: if status != UARTStatus.OK:
uart_close(self.uart_port) uart_close(self.uart_command_port)
self.uart_port = None self.uart_command_port = None
self.error_occurred.emit("Failed to start UART reader") self.error_occurred.emit("Failed to start UART command reader")
return False 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 # Parse hex markers
start_marker = bytes.fromhex( start_marker = bytes.fromhex(
self.interface_config['packet_detect_start'].replace(' ', '') self.interface_config['packet_detect_start'].replace(' ', '')
@ -387,7 +450,7 @@ class Session(QObject):
self.packet_config = PacketConfig(enable=False) 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']: if self.interface_config['i2c_port']:
@ -444,7 +507,11 @@ class Session(QObject):
# 2. Create PacketConfig from interface profile # 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 # Parse hex markers
start_marker = bytes.fromhex( start_marker = bytes.fromhex(
self.interface_config['packet_detect_start'].replace(' ', '') self.interface_config['packet_detect_start'].replace(' ', '')
@ -509,12 +576,19 @@ class Session(QObject):
Ensures clean shutdown of hardware interfaces. Ensures clean shutdown of hardware interfaces.
""" """
try: try:
# Close UART # Close UART Command port
if self.uart_port: if self.uart_command_port:
uart_stop_reader(self.uart_port) uart_stop_reader(self.uart_command_port)
uart_close(self.uart_port) uart_close(self.uart_command_port)
self.uart_port = None self.uart_command_port = None
self.status_changed.emit("UART closed") 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 # Close I2C
if self.i2c_handle: if self.i2c_handle:
@ -659,7 +733,8 @@ class Session(QObject):
run_no=cmd_index, run_no=cmd_index,
command_id=cmd['command_id'], command_id=cmd['command_id'],
command_hex=cmd['hex_string'], 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 i2c_port=self.i2c_handle, # Note: run.py calls it i2c_port but it's actually I2CHandle
packet_config=self.packet_config, packet_config=self.packet_config,
stop_timeout_ms=5000, stop_timeout_ms=5000,

@ -33,6 +33,7 @@ from PyQt6.QtCore import Qt, pyqtSlot
from PyQt6.QtGui import QFont from PyQt6.QtGui import QFont
from session import Session from session import Session
from session_worker import SessionWorker
from database.init_database import DatabaseManager from database.init_database import DatabaseManager
@ -93,6 +94,9 @@ class SessionWidget(QWidget):
# Create session object # Create session object
self.session = Session(db_manager) self.session = Session(db_manager)
# Worker thread for non-blocking execution
self.worker = None
# Connect signals from session to widget slots # Connect signals from session to widget slots
self._connect_signals() self._connect_signals()
@ -332,20 +336,20 @@ class SessionWidget(QWidget):
self.session_profile_combo.setEnabled(False) self.session_profile_combo.setEnabled(False)
self.session_name_input.setEnabled(False) self.session_name_input.setEnabled(False)
# Start session # Create worker thread for non-blocking execution
success = self.session.start_session() # Pass database path for thread-local connection
self.worker = SessionWorker(self.session, self.db_manager.db_path)
if success: # Connect worker signals
# Enable pause/stop buttons self.worker.finished.connect(self._on_worker_finished)
self.pause_button.setEnabled(True)
self.stop_button.setEnabled(True) # Start worker thread
else: self.worker.start()
# Re-enable start button on failure
self.start_button.setEnabled(True) # Enable pause/stop buttons immediately
self.load_button.setEnabled(True) # (worker is running, session will start soon)
self.interface_profile_combo.setEnabled(True) self.pause_button.setEnabled(True)
self.session_profile_combo.setEnabled(True) self.stop_button.setEnabled(True)
self.session_name_input.setEnabled(True)
def _on_pause_clicked(self): def _on_pause_clicked(self):
"""Handle pause button click.""" """Handle pause button click."""
@ -354,14 +358,54 @@ class SessionWidget(QWidget):
def _on_resume_clicked(self): def _on_resume_clicked(self):
"""Handle resume button click.""" """Handle resume button click."""
self.session.resume_session() # Disable resume button
# Button states will be updated by signal handlers 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): def _on_stop_clicked(self):
"""Handle stop button click.""" """Handle stop button click."""
self.session.stop_session() self.session.stop_session()
# Button states will be updated by signal handlers # 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) # SIGNAL HANDLERS (SLOTS)
# ========================================================================= # =========================================================================

@ -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)

Binary file not shown.
Loading…
Cancel
Save