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.
891 lines
35 KiB
891 lines
35 KiB
#!/usr/bin/env python3
|
|
"""
|
|
Session Module - vzug-e-hinge
|
|
==============================
|
|
Orchestrates session execution with queued pause/stop behavior.
|
|
|
|
This module coordinates the entire automated test session flow:
|
|
- Loading interface and session profiles from database
|
|
- Opening UART/I2C ports based on configuration
|
|
- Executing command sequences via run.py
|
|
- Managing pause/stop requests (queued after current run)
|
|
- Providing real-time status updates via Qt signals
|
|
- Handling session lifecycle (create, pause, resume, abort)
|
|
|
|
Key Features:
|
|
- Queued pause/stop (executes AFTER current run completes)
|
|
- Countdown during delays with second-by-second updates
|
|
- Real-time timestamp correlation (UART/I2C)
|
|
- Automatic port management
|
|
- Comprehensive error handling
|
|
- Database integration with status tracking
|
|
|
|
Author: Kynsight
|
|
Version: 2.0.0
|
|
Date: 2025-11-09
|
|
"""
|
|
|
|
# =============================================================================
|
|
# IMPORTS
|
|
# =============================================================================
|
|
|
|
from PyQt6.QtCore import QObject, pyqtSignal
|
|
from typing import Optional, Dict, List, Any
|
|
import sqlite3
|
|
import json
|
|
import time
|
|
from datetime import datetime
|
|
|
|
# UART core
|
|
from uart.uart_kit.uart_core import (
|
|
UARTPort,
|
|
UARTConfig,
|
|
PacketConfig,
|
|
uart_create,
|
|
uart_open,
|
|
uart_close,
|
|
uart_start_reader,
|
|
uart_stop_reader,
|
|
Status as UARTStatus
|
|
)
|
|
|
|
# I2C core
|
|
from i2c.i2c_kit.i2c_core import (
|
|
I2CHandle,
|
|
I2CConfig,
|
|
i2c_create,
|
|
i2c_open,
|
|
i2c_close,
|
|
Status as I2CStatus
|
|
)
|
|
|
|
# Run executor
|
|
from run import execute_run
|
|
|
|
# Database manager
|
|
from database.init_database import DatabaseManager
|
|
|
|
|
|
# =============================================================================
|
|
# SESSION CLASS
|
|
# =============================================================================
|
|
|
|
class Session(QObject):
|
|
"""
|
|
Session orchestration class.
|
|
|
|
Manages the complete lifecycle of an automated test session:
|
|
1. Load profiles (interface + session) from database
|
|
2. Open hardware ports (UART + I2C)
|
|
3. Execute command sequence
|
|
4. Handle pause/stop requests (queued)
|
|
5. Provide real-time status updates
|
|
6. Close ports and finalize session
|
|
|
|
Signals:
|
|
session_started: Emitted when session starts (session_id)
|
|
command_started: Emitted when command starts (command_no, command_name)
|
|
run_completed: Emitted when run completes (run_no, packet_count)
|
|
delay_countdown: Emitted during delay countdown (seconds_remaining)
|
|
session_paused: Emitted when session pauses
|
|
session_finished: Emitted when session completes successfully
|
|
error_occurred: Emitted on error (error_message)
|
|
status_changed: Emitted on status change (status_text)
|
|
"""
|
|
|
|
# =========================================================================
|
|
# SIGNALS (for GUI updates)
|
|
# =========================================================================
|
|
|
|
session_started = pyqtSignal(str) # session_id
|
|
command_started = pyqtSignal(int, str) # command_no, command_name
|
|
run_completed = pyqtSignal(int, int) # run_no, packet_count
|
|
delay_countdown = pyqtSignal(int) # seconds_remaining
|
|
session_paused = pyqtSignal()
|
|
session_finished = pyqtSignal()
|
|
error_occurred = pyqtSignal(str) # error_message
|
|
status_changed = pyqtSignal(str) # status_text
|
|
raw_data_received = pyqtSignal(str, str) # direction (TX/RX), hex_string
|
|
|
|
# =========================================================================
|
|
# CONSTRUCTOR
|
|
# =========================================================================
|
|
|
|
def __init__(self, db_manager: DatabaseManager):
|
|
"""
|
|
Initialize session manager.
|
|
|
|
Args:
|
|
db_manager: Database manager instance
|
|
"""
|
|
super().__init__()
|
|
|
|
# Database connection
|
|
self.db_manager = db_manager
|
|
self.db_conn = db_manager.get_connection()
|
|
|
|
# Session state
|
|
self.current_session_id: Optional[str] = None
|
|
self.session_name: Optional[str] = None
|
|
self.interface_profile_id: Optional[int] = None
|
|
self.session_profile_id: Optional[int] = None
|
|
|
|
# Hardware ports (managed by this class)
|
|
self.uart_port: Optional[UARTPort] = None
|
|
self.i2c_handle: Optional[I2CHandle] = None
|
|
self.packet_config: Optional[PacketConfig] = None
|
|
|
|
# Command sequence (loaded from session_profile)
|
|
self.commands: List[Dict[str, Any]] = []
|
|
self.total_commands: int = 0
|
|
self.current_command_index: int = 0
|
|
|
|
# Execution control flags
|
|
self.is_running: bool = False
|
|
self.is_paused: bool = False
|
|
self.pause_queued: bool = False
|
|
self.stop_queued: bool = False
|
|
|
|
# =========================================================================
|
|
# PROFILE LOADING
|
|
# =========================================================================
|
|
|
|
def load_session(
|
|
self,
|
|
interface_profile_id: int,
|
|
session_profile_id: int,
|
|
session_name: Optional[str] = None
|
|
) -> bool:
|
|
"""
|
|
Load session and interface profiles from database.
|
|
|
|
This method:
|
|
1. Reads interface_profile (UART/I2C config, packet detection)
|
|
2. Reads session_profile (command sequence JSON)
|
|
3. Parses command sequence
|
|
4. Validates all commands exist in uart_commands table
|
|
5. Stores configuration for execution
|
|
|
|
Args:
|
|
interface_profile_id: ID from interface_profiles table
|
|
session_profile_id: ID from session_profiles table
|
|
session_name: Custom session name (auto-generated if None)
|
|
|
|
Returns:
|
|
True if successful, False on error
|
|
"""
|
|
try:
|
|
# ===================================================================
|
|
# 1. Load interface profile (UART/I2C configuration)
|
|
# ===================================================================
|
|
|
|
cursor = self.db_conn.execute("""
|
|
SELECT
|
|
profile_name,
|
|
uart_command_port, uart_command_baud, uart_command_data_bits,
|
|
uart_command_stop_bits, uart_command_parity, uart_command_timeout_ms,
|
|
uart_logger_port, uart_logger_baud, uart_logger_data_bits,
|
|
uart_logger_stop_bits, uart_logger_parity, uart_logger_timeout_ms,
|
|
uart_logger_packet_detect_enable, uart_logger_packet_detect_start,
|
|
uart_logger_packet_detect_length, uart_logger_packet_detect_end,
|
|
i2c_port, i2c_slave_address, i2c_slave_read_register, i2c_slave_read_length
|
|
FROM interface_profiles
|
|
WHERE profile_id = ?
|
|
""", (interface_profile_id,))
|
|
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
self.error_occurred.emit(f"Interface profile {interface_profile_id} not found")
|
|
return False
|
|
|
|
# Store interface configuration
|
|
self.interface_profile_id = interface_profile_id
|
|
self.interface_config = {
|
|
'profile_name': row[0],
|
|
# UART Command interface
|
|
'uart_command_port': row[1],
|
|
'uart_command_baud': row[2],
|
|
'uart_command_data_bits': row[3],
|
|
'uart_command_stop_bits': row[4],
|
|
'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],
|
|
# Packet detection
|
|
'packet_detect_enable': row[13],
|
|
'packet_detect_start': row[14],
|
|
'packet_detect_length': row[15],
|
|
'packet_detect_end': row[16],
|
|
# I2C configuration
|
|
'i2c_port': row[17],
|
|
'i2c_slave_address': row[18],
|
|
'i2c_slave_read_register': row[19],
|
|
'i2c_slave_read_length': row[20]
|
|
}
|
|
|
|
# ===================================================================
|
|
# 2. Load session profile (command sequence)
|
|
# ===================================================================
|
|
|
|
cursor = self.db_conn.execute("""
|
|
SELECT profile_name, command_sequence, description
|
|
FROM session_profiles
|
|
WHERE profile_id = ?
|
|
""", (session_profile_id,))
|
|
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
self.error_occurred.emit(f"Session profile {session_profile_id} not found")
|
|
return False
|
|
|
|
self.session_profile_id = session_profile_id
|
|
profile_name = row[0]
|
|
command_sequence_json = row[1]
|
|
|
|
# Parse JSON command sequence
|
|
try:
|
|
command_sequence = json.loads(command_sequence_json)
|
|
self.commands = command_sequence.get('commands', [])
|
|
self.total_commands = len(self.commands)
|
|
except json.JSONDecodeError as e:
|
|
self.error_occurred.emit(f"Invalid JSON in session profile: {str(e)}")
|
|
return False
|
|
|
|
if self.total_commands == 0:
|
|
self.error_occurred.emit("Session profile has no commands")
|
|
return False
|
|
|
|
# ===================================================================
|
|
# 3. Validate all commands exist in database
|
|
# ===================================================================
|
|
|
|
for cmd in self.commands:
|
|
cmd_id = cmd.get('command_id')
|
|
if not cmd_id:
|
|
self.error_occurred.emit("Command missing command_id")
|
|
return False
|
|
|
|
# Check if command exists
|
|
cursor = self.db_conn.execute("""
|
|
SELECT command_name, hex_string
|
|
FROM uart_commands
|
|
WHERE command_id = ?
|
|
""", (cmd_id,))
|
|
|
|
row = cursor.fetchone()
|
|
if not row:
|
|
self.error_occurred.emit(f"UART command {cmd_id} not found")
|
|
return False
|
|
|
|
# Store command details
|
|
cmd['command_name'] = row[0]
|
|
cmd['hex_string'] = row[1]
|
|
|
|
# ===================================================================
|
|
# 4. Generate session name if not provided
|
|
# ===================================================================
|
|
|
|
if session_name is None or session_name.strip() == "":
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
self.session_name = f"{profile_name}_{timestamp}"
|
|
else:
|
|
self.session_name = session_name
|
|
|
|
# ===================================================================
|
|
# 5. Emit success status
|
|
# ===================================================================
|
|
|
|
self.status_changed.emit(f"Session loaded: {self.session_name}")
|
|
self.status_changed.emit(f"Interface: {self.interface_config['profile_name']}")
|
|
self.status_changed.emit(f"Commands: {self.total_commands}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Failed to load session: {str(e)}")
|
|
return False
|
|
|
|
# =========================================================================
|
|
# PORT MANAGEMENT
|
|
# =========================================================================
|
|
|
|
def _open_ports(self) -> bool:
|
|
"""
|
|
Open UART and I2C ports based on interface configuration.
|
|
|
|
This method:
|
|
1. Opens UART logger port (for packet detection)
|
|
2. Starts UART reader thread
|
|
3. Opens I2C port (for angle readings)
|
|
4. Creates PacketConfig from interface profile
|
|
|
|
Returns:
|
|
True if successful, False on error
|
|
"""
|
|
try:
|
|
# ===================================================================
|
|
# 1. Open UART Logger Port (for packet detection)
|
|
# ===================================================================
|
|
|
|
# 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 port
|
|
status, self.uart_port = uart_create(uart_config)
|
|
|
|
if status != UARTStatus.OK or self.uart_port is None:
|
|
self.error_occurred.emit(f"Failed to create UART port")
|
|
return False
|
|
|
|
# Open UART port
|
|
status = uart_open(self.uart_port)
|
|
|
|
if status != UARTStatus.OK:
|
|
self.error_occurred.emit(f"Failed to open UART port {uart_config.device}")
|
|
return False
|
|
|
|
# Start UART reader thread
|
|
status = uart_start_reader(self.uart_port)
|
|
if status != UARTStatus.OK:
|
|
uart_close(self.uart_port)
|
|
self.uart_port = None
|
|
self.error_occurred.emit("Failed to start UART reader")
|
|
return False
|
|
|
|
self.status_changed.emit(f"UART opened: {uart_config.device}")
|
|
|
|
# ===================================================================
|
|
# 2. Create PacketConfig from interface profile
|
|
# ===================================================================
|
|
|
|
if self.interface_config['packet_detect_enable']:
|
|
# Parse hex markers
|
|
start_marker = bytes.fromhex(
|
|
self.interface_config['packet_detect_start'].replace(' ', '')
|
|
) if self.interface_config['packet_detect_start'] else None
|
|
|
|
end_marker = bytes.fromhex(
|
|
self.interface_config['packet_detect_end'].replace(' ', '')
|
|
) if self.interface_config['packet_detect_end'] else None
|
|
|
|
self.packet_config = PacketConfig(
|
|
enable=True,
|
|
start_marker=start_marker,
|
|
packet_length=self.interface_config['packet_detect_length'],
|
|
end_marker=end_marker,
|
|
on_packet_callback=None # Will be set by run.py
|
|
)
|
|
else:
|
|
self.packet_config = PacketConfig(enable=False)
|
|
|
|
# ===================================================================
|
|
# 3. Open I2C Port (optional - for angle readings)
|
|
# ===================================================================
|
|
|
|
if self.interface_config['i2c_port']:
|
|
# Parse I2C address
|
|
|
|
# Create I2C config
|
|
i2c_config = I2CConfig(
|
|
bus_id=int(self.interface_config["i2c_port"]),
|
|
)
|
|
|
|
# Create I2C handle
|
|
status, self.i2c_handle = i2c_create(i2c_config)
|
|
|
|
if status != I2CStatus.OK or self.i2c_handle is None:
|
|
# I2C is optional, just warn
|
|
self.status_changed.emit("Warning: Could not create I2C handle")
|
|
self.i2c_handle = None
|
|
else:
|
|
# Open I2C
|
|
status = i2c_open(self.i2c_handle)
|
|
|
|
if status != I2CStatus.OK:
|
|
# I2C is optional, just warn
|
|
self.status_changed.emit("Warning: I2C port not available")
|
|
self.i2c_handle = None
|
|
else:
|
|
self.status_changed.emit(f"I2C opened: bus {self.interface_config['i2c_port']}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Failed to open ports: {str(e)}")
|
|
self._close_ports()
|
|
return False
|
|
|
|
# Open UART port
|
|
status = uart_open(self.uart_port)
|
|
|
|
if status != UARTStatus.OK:
|
|
self.error_occurred.emit(f"Failed to open UART port {uart_config.port}")
|
|
return False
|
|
|
|
# Start UART reader thread
|
|
status = uart_start_reader(self.uart_port)
|
|
if status != UARTStatus.OK:
|
|
uart_close(self.uart_port)
|
|
self.uart_port = None
|
|
self.error_occurred.emit("Failed to start UART reader")
|
|
return False
|
|
|
|
self.status_changed.emit(f"UART opened: {uart_config.port}")
|
|
|
|
# ===================================================================
|
|
# 2. Create PacketConfig from interface profile
|
|
# ===================================================================
|
|
|
|
if self.interface_config['packet_detect_enable']:
|
|
# Parse hex markers
|
|
start_marker = bytes.fromhex(
|
|
self.interface_config['packet_detect_start'].replace(' ', '')
|
|
) if self.interface_config['packet_detect_start'] else None
|
|
|
|
end_marker = bytes.fromhex(
|
|
self.interface_config['packet_detect_end'].replace(' ', '')
|
|
) if self.interface_config['packet_detect_end'] else None
|
|
|
|
self.packet_config = PacketConfig(
|
|
enable=True,
|
|
start_marker=start_marker,
|
|
packet_length=self.interface_config['packet_detect_length'],
|
|
end_marker=end_marker,
|
|
on_packet_callback=None # Will be set by run.py
|
|
)
|
|
else:
|
|
self.packet_config = PacketConfig(enable=False)
|
|
|
|
# ===================================================================
|
|
# 3. Open I2C Port (optional - for angle readings)
|
|
# ===================================================================
|
|
|
|
if self.interface_config['i2c_port']:
|
|
# Parse I2C address
|
|
|
|
# Create I2C config
|
|
i2c_config = I2CConfig(
|
|
bus_id=int(self.interface_config["i2c_port"]),
|
|
)
|
|
|
|
# Create I2C handle
|
|
status, self.i2c_handle = i2c_create(i2c_config)
|
|
|
|
if status != I2CStatus.OK or self.i2c_handle is None:
|
|
# I2C is optional, just warn
|
|
self.status_changed.emit("Warning: Could not create I2C handle")
|
|
self.i2c_handle = None
|
|
else:
|
|
# Open I2C
|
|
status = i2c_open(self.i2c_handle)
|
|
|
|
if status != I2CStatus.OK:
|
|
# I2C is optional, just warn
|
|
self.status_changed.emit("Warning: I2C port not available")
|
|
self.i2c_handle = None
|
|
else:
|
|
self.status_changed.emit(f"I2C opened: bus {self.interface_config['i2c_port']}")
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Failed to open ports: {str(e)}")
|
|
self._close_ports()
|
|
return False
|
|
|
|
def _close_ports(self):
|
|
"""
|
|
Close UART and I2C ports.
|
|
|
|
Called at end of session or on error.
|
|
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 I2C
|
|
if self.i2c_handle:
|
|
i2c_close(self.i2c_handle)
|
|
self.i2c_handle = None
|
|
self.status_changed.emit("I2C closed")
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Error closing ports: {str(e)}")
|
|
|
|
# =========================================================================
|
|
# SESSION EXECUTION
|
|
# =========================================================================
|
|
|
|
def start_session(self) -> bool:
|
|
"""
|
|
Start session execution.
|
|
|
|
This method:
|
|
1. Creates session record in database
|
|
2. Opens hardware ports
|
|
3. Executes command loop
|
|
4. Handles delays with countdown
|
|
5. Checks pause/stop queue between runs
|
|
6. Finalizes session on completion
|
|
|
|
Returns:
|
|
True if started successfully, False on error
|
|
"""
|
|
if self.is_running:
|
|
self.error_occurred.emit("Session already running")
|
|
return False
|
|
|
|
if not self.commands:
|
|
self.error_occurred.emit("No session loaded")
|
|
return False
|
|
|
|
try:
|
|
# ===================================================================
|
|
# 1. Create session record in database
|
|
# ===================================================================
|
|
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
self.current_session_id = f"session_{timestamp}"
|
|
session_date = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
self.db_conn.execute("""
|
|
INSERT INTO sessions (
|
|
session_id, session_name, session_date,
|
|
interface_profile_id, session_profile_id,
|
|
status, total_runs
|
|
) VALUES (?, ?, ?, ?, ?, 'active', 0)
|
|
""", (
|
|
self.current_session_id,
|
|
self.session_name,
|
|
session_date,
|
|
self.interface_profile_id,
|
|
self.session_profile_id
|
|
))
|
|
|
|
self.db_conn.commit()
|
|
|
|
# ===================================================================
|
|
# 2. Open hardware ports
|
|
# ===================================================================
|
|
|
|
if not self._open_ports():
|
|
self._finalize_session('error')
|
|
return False
|
|
|
|
# ===================================================================
|
|
# 3. Set execution state
|
|
# ===================================================================
|
|
|
|
self.is_running = True
|
|
self.is_paused = False
|
|
self.pause_queued = False
|
|
self.stop_queued = False
|
|
self.current_command_index = 0
|
|
|
|
# Emit session started
|
|
self.session_started.emit(self.current_session_id)
|
|
self.status_changed.emit(f"Session started: {self.session_name}")
|
|
|
|
# ===================================================================
|
|
# 4. Execute command loop
|
|
# ===================================================================
|
|
|
|
self._execute_command_loop()
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Failed to start session: {str(e)}")
|
|
self._finalize_session('error')
|
|
return False
|
|
|
|
def _execute_command_loop(self):
|
|
"""
|
|
Execute all commands in sequence.
|
|
|
|
This is the main execution loop that:
|
|
1. Iterates through all commands
|
|
2. Calls run.py for each command
|
|
3. Handles delays with countdown
|
|
4. Checks pause/stop queue between runs
|
|
5. Updates database and emits signals
|
|
|
|
CRITICAL: Pause/Stop are QUEUED and only execute between runs
|
|
during the delay phase, never during a run itself.
|
|
"""
|
|
try:
|
|
# Loop through all commands
|
|
for cmd_index, cmd in enumerate(self.commands, 1):
|
|
self.current_command_index = cmd_index
|
|
|
|
# ===============================================================
|
|
# 1. Check if stop was queued (before starting new run)
|
|
# ===============================================================
|
|
|
|
if self.stop_queued:
|
|
self.status_changed.emit("Session stopped by user")
|
|
self._finalize_session('aborted')
|
|
return
|
|
|
|
# ===============================================================
|
|
# 2. Emit command started
|
|
# ===============================================================
|
|
|
|
command_name = cmd['command_name']
|
|
self.command_started.emit(cmd_index, command_name)
|
|
self.status_changed.emit(f"Command {cmd_index}/{self.total_commands}: {command_name}")
|
|
|
|
# ===============================================================
|
|
# 3. Execute run via run.py
|
|
# ===============================================================
|
|
|
|
status, packet_count, error_msg = execute_run(
|
|
db_connection=self.db_conn,
|
|
session_id=self.current_session_id,
|
|
session_name=self.session_name,
|
|
run_no=cmd_index,
|
|
command_id=cmd['command_id'],
|
|
command_hex=cmd['hex_string'],
|
|
uart_port=self.uart_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,
|
|
raw_data_callback=lambda direction, hex_str: self.raw_data_received.emit(direction, hex_str)
|
|
)
|
|
|
|
# ===============================================================
|
|
# 4. Handle run result
|
|
# ===============================================================
|
|
|
|
if status == "error":
|
|
# Run failed - abort session
|
|
self.error_occurred.emit(f"Run {cmd_index} failed: {error_msg}")
|
|
self._finalize_session('error')
|
|
return
|
|
|
|
# Run succeeded - emit completion
|
|
self.run_completed.emit(cmd_index, packet_count)
|
|
self.status_changed.emit(f"Run {cmd_index} complete: {packet_count} packets")
|
|
|
|
# Update total runs in database
|
|
self.db_conn.execute("""
|
|
UPDATE sessions SET total_runs = ? WHERE session_id = ?
|
|
""", (cmd_index, self.current_session_id))
|
|
self.db_conn.commit()
|
|
|
|
# ===============================================================
|
|
# 5. Delay between commands (with countdown and queue check)
|
|
# ===============================================================
|
|
|
|
# Get delay from command (default 3000ms if not specified)
|
|
delay_ms = cmd.get('delay_ms', 3000)
|
|
|
|
# Only delay if not last command
|
|
if cmd_index < self.total_commands:
|
|
self._execute_delay(delay_ms)
|
|
|
|
# Check if pause/stop was queued during delay
|
|
if self.pause_queued:
|
|
self._finalize_session('paused')
|
|
self.is_paused = True
|
|
self.session_paused.emit()
|
|
self.status_changed.emit("Session paused")
|
|
return
|
|
|
|
if self.stop_queued:
|
|
self._finalize_session('aborted')
|
|
self.status_changed.emit("Session stopped by user")
|
|
return
|
|
|
|
# ===================================================================
|
|
# 6. All commands completed successfully
|
|
# ===================================================================
|
|
|
|
self._finalize_session('completed')
|
|
self.session_finished.emit()
|
|
self.status_changed.emit("Session completed successfully")
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Exception during session: {str(e)}")
|
|
self._finalize_session('error')
|
|
|
|
def _execute_delay(self, delay_ms: int):
|
|
"""
|
|
Execute delay with countdown.
|
|
|
|
Emits delay_countdown signal every second so GUI can display
|
|
countdown timer. Also checks pause/stop queue each second.
|
|
|
|
Args:
|
|
delay_ms: Delay in milliseconds
|
|
"""
|
|
# Convert to seconds
|
|
delay_sec = delay_ms / 1000.0
|
|
|
|
# Countdown in 1-second steps
|
|
for remaining in range(int(delay_sec), 0, -1):
|
|
# Emit countdown
|
|
self.delay_countdown.emit(remaining)
|
|
self.status_changed.emit(f"Waiting... ({remaining}s remaining)")
|
|
|
|
# Sleep for 1 second
|
|
time.sleep(1.0)
|
|
|
|
# Check if pause/stop was queued
|
|
if self.pause_queued or self.stop_queued:
|
|
return # Exit delay early
|
|
|
|
# Handle fractional second at end
|
|
fractional = delay_sec - int(delay_sec)
|
|
if fractional > 0:
|
|
time.sleep(fractional)
|
|
|
|
def _finalize_session(self, status: str):
|
|
"""
|
|
Finalize session and cleanup.
|
|
|
|
Args:
|
|
status: 'completed', 'paused', 'aborted', or 'error'
|
|
"""
|
|
try:
|
|
# Update session status in database
|
|
self.db_conn.execute("""
|
|
UPDATE sessions
|
|
SET status = ?, ended_at = datetime('now')
|
|
WHERE session_id = ?
|
|
""", (status, self.current_session_id))
|
|
|
|
self.db_conn.commit()
|
|
|
|
# Close hardware ports
|
|
self._close_ports()
|
|
|
|
# Reset execution state (unless paused)
|
|
if status != 'paused':
|
|
self.is_running = False
|
|
self.current_session_id = None
|
|
|
|
except Exception as e:
|
|
self.error_occurred.emit(f"Error finalizing session: {str(e)}")
|
|
|
|
# =========================================================================
|
|
# PAUSE/RESUME/STOP CONTROL
|
|
# =========================================================================
|
|
|
|
def pause_session(self):
|
|
"""
|
|
Queue pause request.
|
|
|
|
CRITICAL: Pause is QUEUED and executes AFTER current run completes.
|
|
User can press pause anytime, but it only takes effect during
|
|
the delay phase between runs.
|
|
"""
|
|
if not self.is_running or self.is_paused:
|
|
return
|
|
|
|
self.pause_queued = True
|
|
self.status_changed.emit("Pause queued (will execute after current run)")
|
|
|
|
def resume_session(self):
|
|
"""
|
|
Resume paused session.
|
|
|
|
Continues execution from where it left off, immediately proceeding
|
|
to the next command without delay.
|
|
"""
|
|
if not self.is_paused:
|
|
return
|
|
|
|
# Clear pause flags
|
|
self.is_paused = False
|
|
self.pause_queued = False
|
|
|
|
# Update database
|
|
self.db_conn.execute("""
|
|
UPDATE sessions SET status = 'active' WHERE session_id = ?
|
|
""", (self.current_session_id,))
|
|
self.db_conn.commit()
|
|
|
|
# Reopen ports
|
|
if not self._open_ports():
|
|
self.error_occurred.emit("Failed to reopen ports")
|
|
return
|
|
|
|
# Resume command loop from where we left off
|
|
self.status_changed.emit("Session resumed")
|
|
self._execute_command_loop()
|
|
|
|
def stop_session(self):
|
|
"""
|
|
Queue stop request.
|
|
|
|
CRITICAL: Stop is QUEUED and executes AFTER current run completes.
|
|
User can press stop anytime, but it only takes effect during
|
|
the delay phase between runs.
|
|
"""
|
|
if not self.is_running:
|
|
return
|
|
|
|
self.stop_queued = True
|
|
self.status_changed.emit("Stop queued (will execute after current run)")
|
|
|
|
# =========================================================================
|
|
# STATUS QUERIES
|
|
# =========================================================================
|
|
|
|
def get_current_session_id(self) -> Optional[str]:
|
|
"""Get current session ID."""
|
|
return self.current_session_id
|
|
|
|
def get_session_name(self) -> Optional[str]:
|
|
"""Get current session name."""
|
|
return self.session_name
|
|
|
|
def is_session_running(self) -> bool:
|
|
"""Check if session is running."""
|
|
return self.is_running
|
|
|
|
def is_session_paused(self) -> bool:
|
|
"""Check if session is paused."""
|
|
return self.is_paused
|
|
|
|
|
|
# =============================================================================
|
|
# MAIN (for testing)
|
|
# =============================================================================
|
|
|
|
if __name__ == "__main__":
|
|
print("Session Module - vzug-e-hinge")
|
|
print("=" * 60)
|
|
print()
|
|
print("This module orchestrates automated test sessions.")
|
|
print()
|
|
print("Features:")
|
|
print(" ✓ Profile loading (interface + session)")
|
|
print(" ✓ Automatic port management")
|
|
print(" ✓ Command sequence execution")
|
|
print(" ✓ Queued pause/stop (executes after current run)")
|
|
print(" ✓ Real-time status updates via signals")
|
|
print(" ✓ Comprehensive error handling")
|
|
print()
|
|
print("Usage:")
|
|
print(" from session import Session")
|
|
print(" session = Session(db_manager)")
|
|
print(" session.load_session(interface_id, session_id)")
|
|
print(" session.start_session()")
|
|
print()
|
|
print("Ready to be used by session_widget.py!")
|