""" GRIFO_M_PBIT_gui.py - Real-Time Test Monitor GUI This module provides a tkinter-based GUI to monitor test execution in real-time. Works with both real hardware and simulation mode. Features: - Live test progress visualization - Current scenario information - Real-time statistics (B6/B8/Serial) - Color-coded status indicators - Event log with filtering - Thread-safe updates via queue Requirements: - Python 3.7+ - tkinter 8.6+ (included in embedded Python) Usage: # In test script: from GRIFO_M_PBIT_gui import TestMonitorGUI gui = TestMonitorGUI() gui.start() # Update from test code: gui.update_status('run', current=5, total=10) gui.update_scenario('processor_fail', 'Processor Failure', {...}) gui.log_event('info', 'BIT completed in 15.3s') # Cleanup: gui.stop() Author: Test Automation Team Date: 2026-01-30 """ import tkinter as tk from tkinter import ttk, scrolledtext import threading import queue import time import gc import os from typing import Dict, Any, Optional class TestMonitorGUI: """ Real-time GUI monitor for GRIFO PBIT test execution. Thread-safe GUI that displays: - Test progress (runs, scenarios) - Live statistics (B6, B8, Serial) - Scenario information - Event log """ def __init__(self): """Initialize GUI components (creates window but doesn't show it yet).""" self.root = None self.thread = None self.running = False self.update_queue = queue.Queue() # Data storage self.data = { 'run_current': 0, 'run_total': 0, 'scenario_name': 'Initializing...', 'scenario_description': '', 'pbit_time': 0.0, 'pbit_available': False, 'power_on': False, 'b6_total': 0, 'b6_pass': 0, 'b6_fail': 0, 'b6_known': 0, 'b8_checked': 0, 'b8_pass': 0, 'b8_fail': 0, 'serial_total': 0, 'serial_errors': 0, 'serial_fatal': 0, 'serial_recycles': 0, 'success_rate': 0.0, 'expected_failures': [], 'expected_passes': [], 'notes': '', } # GUI components (will be created in GUI thread) self.widgets = {} def start(self): """Start GUI in separate thread.""" if self.running: return self.running = True # Start GUI thread as non-daemon so the process remains alive until user closes the GUI self.thread = threading.Thread(target=self._run_gui, daemon=False) self.thread.start() # Wait for GUI to initialize time.sleep(0.5) def stop(self): """Stop GUI and close window.""" # Request shutdown from GUI thread to avoid calling tkinter methods # from the main thread (which can trigger Tcl_AsyncDelete warnings). if not self.running: return self.update_queue.put(('shutdown', {})) # Wait for GUI thread to exit (safe join); allow longer timeout for slow systems try: if self.thread and self.thread.is_alive(): self.thread.join(timeout=15.0) except Exception: pass self.running = False def _run_gui(self): """GUI thread main loop.""" self.root = tk.Tk() self.root.title("GRIFO M-PBIT Test Monitor") # Increase default window size so log and runs table are visible without manual resize self.root.geometry("1100x900") self.root.minsize(900, 700) self.root.protocol("WM_DELETE_WINDOW", self._on_closing) # Configure styles style = ttk.Style() style.theme_use('clam') # Create GUI layout self._create_widgets() # Start update polling self._poll_updates() # Run main loop self.root.mainloop() def _create_widgets(self): """Create all GUI widgets.""" # Main container main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) # Configure grid weights main_frame.columnconfigure(0, weight=1) main_frame.columnconfigure(1, weight=1) # Give more vertical space to log (row 4) and runs table (row 5) main_frame.rowconfigure(4, weight=2) main_frame.rowconfigure(5, weight=1) # ===== ROW 0: HEADER ===== header_frame = ttk.Frame(main_frame) header_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) title_label = ttk.Label(header_frame, text="GRIFO M-PBIT Test Monitor", font=('Arial', 16, 'bold')) title_label.pack(side=tk.LEFT) self.widgets['status_indicator'] = ttk.Label(header_frame, text="● IDLE", font=('Arial', 12, 'bold'), foreground='gray') self.widgets['status_indicator'].pack(side=tk.RIGHT) # ===== ROW 1: PROGRESS SECTION ===== progress_frame = ttk.LabelFrame(main_frame, text="Test Progress", padding="10") progress_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) progress_frame.columnconfigure(1, weight=1) # Run info ttk.Label(progress_frame, text="Current Run:").grid(row=0, column=0, sticky=tk.W) self.widgets['run_label'] = ttk.Label(progress_frame, text="0 / 0", font=('Arial', 10, 'bold')) self.widgets['run_label'].grid(row=0, column=1, sticky=tk.W, padx=(10, 0)) # Progress bar self.widgets['progress_bar'] = ttk.Progressbar(progress_frame, mode='determinate') self.widgets['progress_bar'].grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(5, 0)) # PBIT timer ttk.Label(progress_frame, text="PBIT Time:").grid(row=2, column=0, sticky=tk.W, pady=(5, 0)) self.widgets['pbit_label'] = ttk.Label(progress_frame, text="0.0s", font=('Arial', 10)) self.widgets['pbit_label'].grid(row=2, column=1, sticky=tk.W, padx=(10, 0), pady=(5, 0)) # ===== ROW 2: SCENARIO INFO ===== scenario_frame = ttk.LabelFrame(main_frame, text="Current Scenario", padding="10") scenario_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) scenario_frame.columnconfigure(1, weight=1) # Scenario name ttk.Label(scenario_frame, text="Scenario:").grid(row=0, column=0, sticky=tk.W) self.widgets['scenario_label'] = ttk.Label(scenario_frame, text="Initializing...", font=('Arial', 10, 'bold')) self.widgets['scenario_label'].grid(row=0, column=1, sticky=tk.W, padx=(10, 0)) # Description ttk.Label(scenario_frame, text="Description:").grid(row=1, column=0, sticky=tk.W, pady=(5, 0)) self.widgets['scenario_desc'] = ttk.Label(scenario_frame, text="", font=('Arial', 9), wraplength=600) self.widgets['scenario_desc'].grid(row=1, column=1, sticky=tk.W, padx=(10, 0), pady=(5, 0)) # Expected failures ttk.Label(scenario_frame, text="Expected Failures:").grid(row=2, column=0, sticky=(tk.W, tk.N), pady=(5, 0)) self.widgets['expected_fail'] = tk.Text(scenario_frame, height=2, width=60, font=('Courier', 8), wrap=tk.WORD, background='#fff5f5') self.widgets['expected_fail'].grid(row=2, column=1, sticky=(tk.W, tk.E), padx=(10, 0), pady=(5, 0)) # ===== ROW 3: STATISTICS ===== stats_frame = ttk.Frame(main_frame) stats_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) stats_frame.columnconfigure(0, weight=1) stats_frame.columnconfigure(1, weight=1) stats_frame.columnconfigure(2, weight=1) # B6 Stats b6_frame = ttk.LabelFrame(stats_frame, text="B6 LRU Status", padding="10") b6_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5)) self.widgets['b6_stats'] = ttk.Label(b6_frame, text="Total: 0\nPass: 0\nFail: 0\nKnown: 0", font=('Courier', 9), justify=tk.LEFT) self.widgets['b6_stats'].pack() # B8 Stats b8_frame = ttk.LabelFrame(stats_frame, text="B8 Diagnostics", padding="10") b8_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=5) self.widgets['b8_stats'] = ttk.Label(b8_frame, text="Checked: 0\nPass: 0\nFail: 0", font=('Courier', 9), justify=tk.LEFT) self.widgets['b8_stats'].pack() # Serial Stats serial_frame = ttk.LabelFrame(stats_frame, text="Serial Messages", padding="10") serial_frame.grid(row=0, column=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0)) self.widgets['serial_stats'] = ttk.Label(serial_frame, text="Total: 0\nErrors: 0\nFatal: 0\nRecycles: 0", font=('Courier', 9), justify=tk.LEFT) self.widgets['serial_stats'].pack() # ===== ROW 4: EVENT LOG ===== log_frame = ttk.LabelFrame(main_frame, text="Event Log", padding="10") log_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S)) log_frame.columnconfigure(0, weight=1) log_frame.rowconfigure(0, weight=1) self.widgets['log_text'] = scrolledtext.ScrolledText(log_frame, height=15, width=80, font=('Courier', 8), wrap=tk.WORD) self.widgets['log_text'].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # Configure text tags for color coding self.widgets['log_text'].tag_config('info', foreground='black') self.widgets['log_text'].tag_config('success', foreground='green') self.widgets['log_text'].tag_config('warning', foreground='orange') self.widgets['log_text'].tag_config('error', foreground='red') self.widgets['log_text'].tag_config('debug', foreground='gray') # ===== ROW 5: PER-RUN TABLE ===== runs_frame = ttk.LabelFrame(main_frame, text="Runs Overview", padding="5") runs_frame.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0)) runs_frame.columnconfigure(0, weight=1) cols = ('run', 'result', 'pbit', 'b6_fail', 'b8_fail', 'known', 'fail_summary', 'serial_events', 'target_done', 'target_result') # taller default table so more runs are visible self.widgets['runs_table'] = ttk.Treeview(runs_frame, columns=cols, show='headings', height=10) self.widgets['runs_table'].grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) # Column headers: Result = Overall PBIT result, Target Done and Target Result are new columns for c, title in zip(cols, ['Run', 'Result', 'PBIT (s)', 'B6 Fail', 'B8 Fail', 'Known', 'Failures', 'Serial', 'Tgt Test', 'Tgt Result']): self.widgets['runs_table'].heading(c, text=title) self.widgets['runs_table'].column(c, width=100, anchor=tk.CENTER) # Horizontal and vertical scrollbars for table hscroll = ttk.Scrollbar(runs_frame, orient=tk.HORIZONTAL, command=self.widgets['runs_table'].xview) vscroll = ttk.Scrollbar(runs_frame, orient=tk.VERTICAL, command=self.widgets['runs_table'].yview) self.widgets['runs_table'].configure(xscrollcommand=hscroll.set, yscrollcommand=vscroll.set) hscroll.grid(row=1, column=0, sticky=(tk.W, tk.E)) vscroll.grid(row=0, column=1, sticky=(tk.N, tk.S)) def _poll_updates(self): """Poll update queue and apply changes to GUI.""" if not self.running: return try: # Process all queued updates while not self.update_queue.empty(): update_type, data = self.update_queue.get_nowait() if update_type == 'status': self._update_status(data) elif update_type == 'scenario': self._update_scenario(data) elif update_type == 'statistics': self._update_statistics(data) elif update_type == 'run_row': self._update_run_row(data) elif update_type == 'show_results': self._show_results_dialog(data) elif update_type == 'shutdown': # Perform clean shutdown from the GUI thread try: self.running = False if self.root: try: self.root.quit() except Exception: pass try: self.root.destroy() except Exception: pass # CRITICAL: Clear all Tk references on GUI thread to avoid # Tcl_AsyncDelete warnings when main thread cleans up try: self.root = None self.widgets.clear() except Exception: pass except Exception: pass return elif update_type == 'gc_collect': # Run garbage collection on the GUI thread so that any # PhotoImage/ImageTk destructors that call into Tk run # on the correct thread and avoid Tcl_AsyncDelete issues. try: gc.collect() except Exception: pass elif update_type == 'log': self._add_log_entry(data) except queue.Empty: pass except Exception as e: print(f"[GUI] Error processing update: {e}") # Schedule next poll if self.root: self.root.after(100, self._poll_updates) def _update_status(self, data: Dict[str, Any]): """Update test status display.""" if 'run_current' in data: self.data['run_current'] = data['run_current'] if 'run_total' in data: self.data['run_total'] = data['run_total'] if 'pbit_time' in data: self.data['pbit_time'] = data['pbit_time'] if 'pbit_available' in data: self.data['pbit_available'] = data['pbit_available'] if 'power_on' in data: self.data['power_on'] = data['power_on'] # Update labels self.widgets['run_label'].config(text=f"{self.data['run_current']} / {self.data['run_total']}") self.widgets['pbit_label'].config(text=f"{self.data['pbit_time']:.1f}s") # Update progress bar if self.data['run_total'] > 0: progress = (self.data['run_current'] / self.data['run_total']) * 100 self.widgets['progress_bar']['value'] = progress # Update status indicator if self.data['power_on']: status_text = "● RUNNING" status_color = 'green' else: status_text = "● IDLE" status_color = 'gray' self.widgets['status_indicator'].config(text=status_text, foreground=status_color) # Initialize runs table when run_total is set if self.data['run_total'] and 'runs_table_initialized' not in self.widgets: self._init_runs_table(self.data['run_total']) def _update_scenario(self, data: Dict[str, Any]): """Update scenario information display.""" if 'name' in data: self.data['scenario_name'] = data['name'] if 'description' in data: self.data['scenario_description'] = data['description'] if 'expected_failures' in data: self.data['expected_failures'] = data['expected_failures'] if 'expected_passes' in data: self.data['expected_passes'] = data['expected_passes'] if 'notes' in data: self.data['notes'] = data['notes'] # Update labels self.widgets['scenario_label'].config(text=self.data['scenario_name']) self.widgets['scenario_desc'].config(text=self.data['scenario_description']) # Update expected failures text box (enable for edits then disable) txt = self.widgets['expected_fail'] try: txt.config(state='normal') except Exception: pass txt.delete('1.0', tk.END) if self.data['expected_failures']: fail_text = '\n'.join([f"✗ {f}" for f in self.data['expected_failures']]) else: fail_text = "(none expected)" txt.insert('1.0', fail_text) txt.config(state='disabled') def _update_statistics(self, data: Dict[str, Any]): """Update statistics display.""" # Update data for key in ['b6_total', 'b6_pass', 'b6_fail', 'b6_known', 'b8_checked', 'b8_pass', 'b8_fail', 'serial_total', 'serial_errors', 'serial_fatal', 'serial_recycles']: if key in data: self.data[key] = data[key] # Update B6 stats b6_text = f"Total: {self.data['b6_total']}\nPass: {self.data['b6_pass']}\nFail: {self.data['b6_fail']}\nKnown: {self.data['b6_known']}" self.widgets['b6_stats'].config(text=b6_text) # Update B8 stats b8_text = f"Checked: {self.data['b8_checked']}\nPass: {self.data['b8_pass']}\nFail: {self.data['b8_fail']}" self.widgets['b8_stats'].config(text=b8_text) # Update Serial stats serial_text = f"Total: {self.data['serial_total']}\nErrors: {self.data['serial_errors']}\nFatal: {self.data['serial_fatal']}\nRecycles: {self.data['serial_recycles']}" self.widgets['serial_stats'].config(text=serial_text) def _add_log_entry(self, data: Dict[str, str]): """Add entry to event log.""" level = data.get('level', 'info') message = data.get('message', '') timestamp = time.strftime('%H:%M:%S') # Format message with timestamp log_line = f"[{timestamp}] {message}\n" # Insert with appropriate tag self.widgets['log_text'].insert(tk.END, log_line, level) self.widgets['log_text'].see(tk.END) # Limit log size (keep last 500 lines) line_count = int(self.widgets['log_text'].index('end-1c').split('.')[0]) if line_count > 500: self.widgets['log_text'].delete('1.0', '100.0') def _on_closing(self): """Handle window close event.""" self.running = False self.root.destroy() # ===== PUBLIC API FOR TEST CODE ===== def update_status(self, run_current: Optional[int] = None, run_total: Optional[int] = None, pbit_time: Optional[float] = None, pbit_available: Optional[bool] = None, power_on: Optional[bool] = None): """Update test status (thread-safe).""" data = {} if run_current is not None: data['run_current'] = run_current if run_total is not None: data['run_total'] = run_total if pbit_time is not None: data['pbit_time'] = pbit_time if pbit_available is not None: data['pbit_available'] = pbit_available if power_on is not None: data['power_on'] = power_on if data: self.update_queue.put(('status', data)) def update_scenario(self, name: str, description: str, expected_failures: list, expected_passes: list, notes: str): """Update scenario information (thread-safe).""" data = { 'name': name, 'description': description, 'expected_failures': expected_failures, 'expected_passes': expected_passes, 'notes': notes, } self.update_queue.put(('scenario', data)) def update_statistics(self, **kwargs): """Update statistics (thread-safe).""" self.update_queue.put(('statistics', kwargs)) def _init_runs_table(self, total_runs: int): """Pre-populate runs table with empty rows for visual overview.""" table = self.widgets.get('runs_table') if not table: return # Clear existing for r in table.get_children(): table.delete(r) for i in range(1, total_runs + 1): table.insert('', 'end', iid=str(i), values=(f'{i}/{total_runs}', '', '', '', '', '', '', '', '', '')) self.widgets['runs_table_initialized'] = True def update_run(self, run: int, result: Optional[str] = None, pbit: Optional[float] = None, b6_fail: Optional[int] = None, b8_fail: Optional[int] = None, known: Optional[int] = None, fail_summary: Optional[str] = None, serial_events: Optional[str] = None, target_done: Optional[str] = None, target_result: Optional[str] = None): """Thread-safe request to update one row in runs table.""" data = {'run': run} if result is not None: data['result'] = result if pbit is not None: data['pbit'] = pbit if b6_fail is not None: data['b6_fail'] = b6_fail if b8_fail is not None: data['b8_fail'] = b8_fail if known is not None: data['known'] = known if fail_summary is not None: data['fail_summary'] = fail_summary if serial_events is not None: data['serial_events'] = serial_events if target_done is not None: data['target_done'] = target_done if target_result is not None: data['target_result'] = target_result self.update_queue.put(('run_row', data)) def _update_run_row(self, data: Dict[str, Any]): """Apply an update to the runs table row specified by 'run'.""" table = self.widgets.get('runs_table') if not table: return run_num = str(data.get('run')) if not table.exists(run_num): # Insert if missing total = self.data.get('run_total', 0) table.insert('', 'end', iid=run_num, values=(f'{run_num}/{total}', '', '', '', '', '', '', '', '', '')) # Read old values old = table.item(run_num).get('values', []) if not old: old = [''] * 10 # Updated to 10 columns # Map columns new = list(old) for k, v in data.items(): if k == 'run': new[0] = f"{v}/{self.data.get('run_total', '')}" elif k == 'result': new[1] = v elif k == 'pbit': new[2] = f"{v:.1f}" if isinstance(v, (int, float)) else str(v) elif k == 'b6_fail': new[3] = str(v) elif k == 'b8_fail': new[4] = str(v) elif k == 'known': new[5] = str(v) elif k == 'fail_summary': new[6] = str(v) elif k == 'serial_events': new[7] = str(v) elif k == 'target_done': new[8] = str(v) elif k == 'target_result': new[9] = str(v) table.item(run_num, values=new) # Ensure the updated row is visible (scroll to it) try: table.see(run_num) except Exception: # Fallback: move view to bottom try: table.yview_moveto(1.0) except Exception: pass def log_event(self, level: str, message: str): """Add log entry (thread-safe). Args: level: 'info', 'success', 'warning', 'error', 'debug' message: Log message text """ data = {'level': level, 'message': message} self.update_queue.put(('log', data)) def show_results(self, folder: str): """Request GUI to show results folder dialog (thread-safe). This enqueues a request processed in the GUI thread. """ self.update_queue.put(('show_results', {'folder': folder})) def _show_results_dialog(self, data: dict): """Create a professional dialog showing test completion and output locations.""" folder = data.get('folder') if isinstance(data, dict) else data if not folder: return try: dlg = tk.Toplevel(self.root) dlg.title('GRIFO M-PBIT Test Completed') dlg.transient(self.root) # Main container with padding main_frame = ttk.Frame(dlg, padding="20") main_frame.pack(fill=tk.BOTH, expand=True) # Header with icon and title header_frame = ttk.Frame(main_frame) header_frame.pack(fill=tk.X, pady=(0, 15)) title_label = ttk.Label(header_frame, text="✓ Test Execution Completed", font=('Arial', 14, 'bold'), foreground='green') title_label.pack(anchor=tk.W) # Separator sep1 = ttk.Separator(main_frame, orient=tk.HORIZONTAL) sep1.pack(fill=tk.X, pady=(0, 15)) # Info section info_frame = ttk.LabelFrame(main_frame, text="Test Results", padding="15") info_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 15)) msg_text = ( "The test execution has been completed successfully.\n\n" "The following output files have been generated:" ) msg_label = ttk.Label(info_frame, text=msg_text, justify=tk.LEFT) msg_label.pack(anchor=tk.W, pady=(0, 10)) # File types list files_frame = ttk.Frame(info_frame) files_frame.pack(fill=tk.X, pady=(0, 10)) file_items = [ ("PDF Report", "Complete test report with analysis"), ("CSV Export", "Detailed statistics and per-run data"), ("Log Files", "Execution logs and debug information") ] for icon_text, description in file_items: item_frame = ttk.Frame(files_frame) item_frame.pack(fill=tk.X, pady=2) ttk.Label(item_frame, text=f" {icon_text}:", font=('Arial', 9, 'bold')).pack(side=tk.LEFT) ttk.Label(item_frame, text=f" {description}", font=('Arial', 9)).pack(side=tk.LEFT) # Location section with folder path location_frame = ttk.Frame(info_frame) location_frame.pack(fill=tk.X, pady=(10, 0)) ttk.Label(location_frame, text="Location:", font=('Arial', 9, 'bold')).pack(anchor=tk.W) path_text = tk.Text(location_frame, height=2, wrap=tk.WORD, font=('Courier', 8), relief=tk.FLAT, background='#f0f0f0', padx=5, pady=5) path_text.insert('1.0', folder) path_text.config(state='disabled') path_text.pack(fill=tk.X, pady=(5, 0)) # Separator sep2 = ttk.Separator(main_frame, orient=tk.HORIZONTAL) sep2.pack(fill=tk.X, pady=(0, 15)) # Button frame btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X) def _open_folder(): try: if os.name == 'nt': os.startfile(folder) else: import subprocess subprocess.Popen(['xdg-open', folder]) except Exception: pass def _close_all(): try: dlg.destroy() except Exception: pass try: self._on_closing() except Exception: pass # Buttons with better styling open_btn = ttk.Button(btn_frame, text='Open folder', command=_open_folder) open_btn.pack(side=tk.RIGHT, padx=(10, 0)) close_btn = ttk.Button(btn_frame, text='Close Test', command=_close_all) close_btn.pack(side=tk.RIGHT) # Update window to calculate final size, then center on parent and show dlg.update_idletasks() # Set minimum size based on content dlg_width = max(600, dlg.winfo_reqwidth()) dlg_height = max(400, dlg.winfo_reqheight()) # Calculate position to center on parent window (use root's screen coordinates) try: parent_x = self.root.winfo_rootx() parent_y = self.root.winfo_rooty() parent_width = self.root.winfo_width() or self.root.winfo_reqwidth() parent_height = self.root.winfo_height() or self.root.winfo_reqheight() except Exception: parent_x = 0 parent_y = 0 parent_width = self.root.winfo_screenwidth() parent_height = self.root.winfo_screenheight() x = parent_x + max(0, (parent_width - dlg_width) // 2) y = parent_y + max(0, (parent_height - dlg_height) // 2) # Clamp to screen bounds screen_w = self.root.winfo_screenwidth() screen_h = self.root.winfo_screenheight() x = max(0, min(x, screen_w - dlg_width)) y = max(0, min(y, screen_h - dlg_height)) # Apply geometry and make non-resizable dlg.geometry(f'{dlg_width}x{dlg_height}+{x}+{y}') dlg.resizable(False, False) # Now grab focus dlg.grab_set() # Ensure dialog is on top dlg.lift() dlg.focus_force() except Exception as e: print(f"Error showing results dialog: {e}") # ===== SIMPLE TEST/DEMO ===== if __name__ == '__main__': print("Starting GUI test...") gui = TestMonitorGUI() gui.start() # Simulate test execution gui.update_status(run_current=0, run_total=10, power_on=True) gui.log_event('info', 'Test started') time.sleep(1) for i in range(1, 11): # Update run gui.update_status(run_current=i, run_total=10) gui.log_event('info', f'Starting run {i}/10') # Update scenario scenarios = ['normal', 'processor_fail', 'transmitter_fail'] scenario = scenarios[i % 3] gui.update_scenario( name=scenario.replace('_', ' ').title(), description=f"Test scenario {scenario}", expected_failures=['Field A', 'Field B'] if scenario != 'normal' else [], expected_passes=['All other fields'], notes='This is a test scenario' ) # Simulate PBIT execution for t in range(15): gui.update_status(pbit_time=t + 1.0) time.sleep(0.2) # Update statistics gui.update_statistics( b6_total=12, b6_pass=10, b6_fail=1, b6_known=1, b8_checked=0 if scenario == 'normal' else 50, b8_pass=0 if scenario == 'normal' else 48, b8_fail=0 if scenario == 'normal' else 2, serial_total=i * 5, serial_errors=i, serial_fatal=0, serial_recycles=0 ) gui.log_event('success' if scenario == 'normal' else 'warning', f'Run {i} completed') time.sleep(1) gui.log_event('success', 'All tests completed!') # Keep GUI open input("Press Enter to close GUI...") gui.stop()