786 lines
32 KiB
Python
786 lines
32 KiB
Python
"""
|
|
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()
|