#!/usr/bin/env python3 """ vzug-e-hinge Main Application ============================== Entry point for the integrated test and control system. Integrates: - Session management - UART control - I2C control - UART Logger - I2C Logger - Graph visualization - Test profile execution Author: Kynsight Version: 1.0.0 """ import sys import os from pathlib import Path # Force X11 backend for Qt (Wayland workaround) # os.environ['QT_QPA_PLATFORM'] = 'xcb' from PyQt6.QtWidgets import ( QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QMenuBar, QMenu, QMessageBox, QStatusBar, QPushButton ) from PyQt6.QtCore import Qt, QTimer from PyQt6.QtGui import QAction, QFont # Import database from database.init_database import DatabaseManager # Import widgets from session_widget import SessionWidget from configure_interface_widget import ConfigureInterfaceWidget from configure_session_widget import ConfigureSessionWidget from uart.uart_integrated_widget import UARTControlWidget from i2c.i2c_integrated_widget import I2CControlWidget from graph.graph_kit.graph_core_widget import GraphWidget class MainWindow(QMainWindow): """ Main application window with tabbed interface. Tabs: - Session: Control panel, command execution, profile management - UART: Direct UART control - UART Logger: UART logging interface - I2C: Direct I2C control - I2C Logger: I2C logging interface - Graph: Data visualization """ def __init__(self, db_path: str = "./database/ehinge.db"): super().__init__() self.db_path = db_path self.db_manager = None # Initialize database self._init_database() # Setup UI self._init_ui() # Setup session manager self._init_session_manager() # Setup connections self._setup_connections() # Status update timer self.status_timer = QTimer() self.status_timer.timeout.connect(self._update_status) self.status_timer.start(1000) # Update every second def _init_database(self): """Initialize database connection.""" self.db_manager = DatabaseManager(self.db_path) # Check if database exists if not os.path.exists(self.db_path): reply = QMessageBox.question( self, "Database Not Found", f"Database not found at:\n{self.db_path}\n\nCreate new database?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: success = self.db_manager.initialize_database() if not success: QMessageBox.critical( self, "Database Error", "Failed to create database. Application cannot continue." ) sys.exit(1) else: sys.exit(0) else: # Connect to existing database if not self.db_manager.connect(): QMessageBox.critical( self, "Database Error", "Failed to connect to database. Application cannot continue." ) sys.exit(1) def _init_ui(self): """Initialize user interface.""" self.setWindowTitle("vzug-e-hinge Test & Control System") self.setGeometry(100, 100, 1400, 900) # Central widget with tabs central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout() central_widget.setLayout(layout) # Session info bar (shows current session) self.session_info_bar = self._create_session_info_bar() layout.addWidget(self.session_info_bar) # Tab widget self.tabs = QTabWidget() layout.addWidget(self.tabs) # Create placeholder tabs (will be replaced with actual widgets) self._create_tabs() # Connect tab change to refresh profiles self.tabs.currentChanged.connect(self._on_tab_changed) # Menu bar self._create_menu_bar() # Status bar self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage("Ready") def _create_session_info_bar(self) -> QWidget: """Create session information bar at top.""" widget = QWidget() widget.setStyleSheet("background-color: #1e1e1e; padding: 5px;") layout = QHBoxLayout() widget.setLayout(layout) # Session info self.lbl_session_name = QLabel("Session: None") self.lbl_session_name.setStyleSheet("color: #ffffff; font-weight: bold;") layout.addWidget(self.lbl_session_name) layout.addStretch() # Status indicator self.lbl_session_status = QLabel("Status: Idle") self.lbl_session_status.setStyleSheet("color: #00ff00;") layout.addWidget(self.lbl_session_status) # Run counter self.lbl_run_count = QLabel("Runs: 0") self.lbl_run_count.setStyleSheet("color: #ffffff;") layout.addWidget(self.lbl_run_count) # Database size self.lbl_db_size = QLabel("DB: 0%") self.lbl_db_size.setStyleSheet("color: #00ff00;") layout.addWidget(self.lbl_db_size) return widget def _create_tabs(self): """Create tab widgets.""" # Tab 1: Session Control (integrate SessionWidget) try: from session_widget import SessionWidget self.session_widget = SessionWidget(self.db_manager) self.session_widget.set_main_window(self) # Pass reference for tab control self.tabs.addTab(self.session_widget, "Session") except ImportError as e: # Fallback to placeholder if SessionWidget not available print(f"Warning: SessionWidget not available: {e}") self.tab_session = self._create_placeholder_tab("Session Control") self.tabs.addTab(self.tab_session, "Session") self.session_widget = None # Tab 2: Configure Session Profiles try: from configure_session_widget import ConfigureSessionWidget self.configure_session_widget = ConfigureSessionWidget(self.db_manager) self.tabs.addTab(self.configure_session_widget, "Configure Session") except ImportError as e: print(f"Warning: ConfigureSessionWidget not available: {e}") self.tab_configure_session = self._create_placeholder_tab("Configure Session") self.tabs.addTab(self.tab_configure_session, "Configure Session") self.configure_session_widget = None # Tab 3: Configure Interface Profiles try: from configure_interface_widget import ConfigureInterfaceWidget self.configure_interface_widget = ConfigureInterfaceWidget(self.db_manager) self.tabs.addTab(self.configure_interface_widget, "Configure Interface") except ImportError as e: print(f"Warning: ConfigureInterfaceWidget not available: {e}") self.tab_configure_interface = self._create_placeholder_tab("Configure Interface") self.tabs.addTab(self.tab_configure_interface, "Configure Interface") self.configure_interface_widget = None # Tab 4: UART (integrated: table + core) try: from uart.uart_integrated_widget import UARTControlWidget self.uart_widget = UARTControlWidget(self.db_manager.get_connection()) self.tabs.addTab(self.uart_widget, "UART") except ImportError as e: print(f"UART widget import error: {e}") self.tab_uart = self._create_placeholder_tab("UART Control") self.tabs.addTab(self.tab_uart, "UART") self.uart_widget = None # Tab 5: I2C (integrated: table + core) try: from i2c.i2c_integrated_widget import I2CControlWidget self.i2c_widget = I2CControlWidget(self.db_manager.get_connection()) self.tabs.addTab(self.i2c_widget, "I2C") except ImportError as e: print(f"I2C widget import error: {e}") self.tab_i2c = self._create_placeholder_tab("I2C Control") self.tabs.addTab(self.tab_i2c, "I2C") self.i2c_widget = None # Tab 6: Graph try: self.graph_widget = GraphWidget(db_path=self.db_path) self.tabs.addTab(self.graph_widget, "Graph") except Exception as e: print(f"[WARN] Failed to load Graph widget: {e}") self.tab_graph = self._create_placeholder_tab("Graph Visualization\n(Error loading)") self.tabs.addTab(self.tab_graph, "Graph") self.graph_widget = None # Tab 7: Database Manager try: from database.database_manager_widget import DatabaseManagerWidget self.db_manager_widget = DatabaseManagerWidget(db_manager=self.db_manager) self.tabs.addTab(self.db_manager_widget, "Database Manager") except Exception as e: print(f"[WARN] Failed to load Database Manager widget: {e}") import traceback traceback.print_exc() self.tab_db_manager = self._create_placeholder_tab("Database Manager\n(Error loading)") self.tabs.addTab(self.tab_db_manager, "Database Manager") self.db_manager_widget = None def _create_placeholder_tab(self, title: str) -> QWidget: """Create placeholder tab widget.""" widget = QWidget() layout = QVBoxLayout() widget.setLayout(layout) label = QLabel(f"{title}\n\n(Coming soon...)") label.setAlignment(Qt.AlignmentFlag.AlignCenter) label.setStyleSheet("font-size: 16pt; color: #888888;") layout.addWidget(label) return widget def _create_menu_bar(self): """Create menu bar.""" menubar = self.menuBar() # File menu file_menu = menubar.addMenu("&File") action_new_session = QAction("&New Session", self) action_new_session.setShortcut("Ctrl+N") action_new_session.triggered.connect(self._on_new_session) file_menu.addAction(action_new_session) action_end_session = QAction("&End Session", self) action_end_session.setShortcut("Ctrl+E") action_end_session.triggered.connect(self._on_end_session) file_menu.addAction(action_end_session) file_menu.addSeparator() action_export_session = QAction("E&xport Session...", self) action_export_session.triggered.connect(self._on_export_session) file_menu.addAction(action_export_session) file_menu.addSeparator() action_exit = QAction("E&xit", self) action_exit.setShortcut("Ctrl+Q") action_exit.triggered.connect(self.close) file_menu.addAction(action_exit) # Config menu config_menu = menubar.addMenu("&Config") action_uart_config = QAction("&UART Configuration...", self) action_uart_config.triggered.connect(self._on_uart_config) config_menu.addAction(action_uart_config) action_i2c_config = QAction("&I2C Configuration...", self) action_i2c_config.triggered.connect(self._on_i2c_config) config_menu.addAction(action_i2c_config) action_debugger_config = QAction("&Debugger Configuration...", self) action_debugger_config.triggered.connect(self._on_debugger_config) config_menu.addAction(action_debugger_config) config_menu.addSeparator() action_gui_profile = QAction("&GUI Profile...", self) action_gui_profile.triggered.connect(self._on_gui_profile) config_menu.addAction(action_gui_profile) # Profiles menu profiles_menu = menubar.addMenu("&Profiles") action_manage_profiles = QAction("&Manage Test Profiles...", self) action_manage_profiles.triggered.connect(self._on_manage_profiles) profiles_menu.addAction(action_manage_profiles) action_new_profile = QAction("&New Profile...", self) action_new_profile.triggered.connect(self._on_new_profile) profiles_menu.addAction(action_new_profile) # Database menu database_menu = menubar.addMenu("&Database") action_change_db = QAction("&Change Database...", self) action_change_db.setShortcut("Ctrl+D") action_change_db.triggered.connect(self._on_change_database) database_menu.addAction(action_change_db) database_menu.addSeparator() action_backup = QAction("&Backup Database...", self) action_backup.setShortcut("Ctrl+B") action_backup.triggered.connect(self._on_backup_database) database_menu.addAction(action_backup) database_menu.addSeparator() action_vacuum = QAction("&Vacuum Database", self) action_vacuum.triggered.connect(self._on_vacuum_database) database_menu.addAction(action_vacuum) action_db_info = QAction("Database &Info...", self) action_db_info.triggered.connect(self._on_database_info) database_menu.addAction(action_db_info) # Help menu help_menu = menubar.addMenu("&Help") action_about = QAction("&About", self) action_about.triggered.connect(self._on_about) help_menu.addAction(action_about) action_docs = QAction("&Documentation", self) action_docs.triggered.connect(self._on_documentation) help_menu.addAction(action_docs) def _init_session_manager(self): """Initialize session manager (coordination logic).""" # NOTE: Session management is now integrated into SessionWidget # The Session class is instantiated by SessionWidget internally # No need for separate SessionManager anymore self.session_manager = None def _setup_connections(self): """Setup signal/slot connections between components.""" # NOTE: Session management is now self-contained in SessionWidget # All signals/slots are handled internally by the widget # Main window just needs to monitor session state if desired if self.session_widget: # Connect to session widget's session object signals for monitoring try: self.session_widget.session.session_started.connect(self._on_session_started_internal) self.session_widget.session.session_finished.connect(self._on_session_finished_internal) self.session_widget.session.status_changed.connect(self._on_session_status_changed_internal) except Exception as e: print(f"Warning: Could not connect to session signals: {e}") # Connect UART widget TX/RX to Session widget's data monitor if hasattr(self, 'uart_widget') and self.uart_widget and self.session_widget: try: # Connect TX signal (bytes) → convert to hex string → display self.uart_widget.uart_core.data_sent.connect( lambda data: self.session_widget._on_raw_data_received("TX", data.hex(' ').upper()) ) # Connect RX signal (bytes, info) → convert to hex string → display self.uart_widget.uart_core.data_received_display.connect( lambda data, info: self.session_widget._on_raw_data_received("RX", data.hex(' ').upper()) ) print("[Main] Connected UART widget TX/RX to Session data monitor") except Exception as e: print(f"Warning: Could not connect UART signals to data monitor: {e}") # ========================================================================= # Signal Handlers - Session Widget Actions # ========================================================================= # ========================================================================= # OLD Signal Handlers - NO LONGER USED (kept for reference) # Session management is now self-contained in SessionWidget # ========================================================================= # def _on_execute_command_requested(self, command_type: str, command_id: int): # """Handle execute command request from session widget.""" # # NO LONGER USED # pass # def _on_execute_profile_requested(self, profile_id: int): # """Handle execute profile request from session widget.""" # # NO LONGER USED # pass # def _on_pause_profile_requested(self): # """Handle pause profile request.""" # # NO LONGER USED # pass # def _on_resume_profile_requested(self): # """Handle resume profile request.""" # # NO LONGER USED # pass # def _on_abort_profile_requested(self): # """Handle abort profile request.""" # # NO LONGER USED # pass # def _on_session_created(self, session_id: str): # """Handle session created.""" # # NO LONGER USED # pass # def _on_session_ended(self, session_id: str, status: str): # """Handle session ended.""" # # NO LONGER USED # pass self.lbl_session_status.setText("Status: Idle") self.lbl_session_status.setStyleSheet("color: #888888;") # def _on_run_started(self, session_id: str, run_no: int): # """Handle run started.""" # # NO LONGER USED # pass # def _on_run_completed(self, session_id: str, run_no: int, status: str): # """Handle run completed.""" # # NO LONGER USED # pass # def _on_profile_step_changed(self, current_repeat: int, current_step: int): # """Handle profile step changed.""" # # NO LONGER USED # pass # ========================================================================= # Internal Session Monitoring (from new Session class) # ========================================================================= def _on_session_started_internal(self, session_id: str): """Handle session started (internal monitoring).""" if self.session_widget: session_name = self.session_widget.session.get_session_name() self.lbl_session_name.setText(f"Session: {session_name}") self.lbl_session_status.setText("Status: Running") self.lbl_session_status.setStyleSheet("color: #00ff00;") self.status_bar.showMessage(f"Session started: {session_id}") def _on_session_finished_internal(self): """Handle session finished (internal monitoring).""" self.lbl_session_status.setText("Status: Finished") self.lbl_session_status.setStyleSheet("color: #00ff00;") self.status_bar.showMessage("Session completed successfully") def _on_session_status_changed_internal(self, status_text: str): """Handle session status changes (internal monitoring).""" # Update status bar with session status self.status_bar.showMessage(status_text) def _update_status(self): """Update status bar and session info (called every second).""" # Update database size try: size_bytes, percentage, status = self.db_manager.check_size() color = "#00ff00" # Green if status == "warning": color = "#ffaa00" # Orange elif status == "critical": color = "#ff0000" # Red self.lbl_db_size.setText(f"DB: {percentage:.1f}%") self.lbl_db_size.setStyleSheet(f"color: {color};") except: pass # Update run count from session widget if self.session_widget and self.session_widget.session: try: # Get current session from widget session_id = self.session_widget.session.get_current_session_id() if session_id: # Query database for run count cursor = self.db_manager.get_connection().execute(""" SELECT total_runs FROM sessions WHERE session_id = ? """, (session_id,)) row = cursor.fetchone() if row: self.lbl_run_count.setText(f"Runs: {row[0]}") except: pass # ========================================================================= # Menu Actions # ========================================================================= def _on_new_session(self): """Start new session.""" # TODO: Implement QMessageBox.information(self, "New Session", "New session dialog (coming soon)") def _on_end_session(self): """End current session.""" # TODO: Implement QMessageBox.information(self, "End Session", "End session (coming soon)") def _on_export_session(self): """Export session data.""" # TODO: Implement QMessageBox.information(self, "Export", "Export session (coming soon)") def _on_uart_config(self): """Configure UART.""" # TODO: Implement QMessageBox.information(self, "UART Config", "UART configuration dialog (coming soon)") def _on_i2c_config(self): """Configure I2C.""" # TODO: Implement QMessageBox.information(self, "I2C Config", "I2C configuration dialog (coming soon)") def _on_debugger_config(self): """Configure debugger.""" # TODO: Implement QMessageBox.information(self, "Debugger Config", "Debugger configuration dialog (coming soon)") def _on_gui_profile(self): """Manage GUI profile.""" # TODO: Implement QMessageBox.information(self, "GUI Profile", "GUI profile settings (coming soon)") def _on_manage_profiles(self): """Manage test profiles.""" # TODO: Implement QMessageBox.information(self, "Manage Profiles", "Test profile manager (coming soon)") def _on_new_profile(self): """Create new test profile.""" # TODO: Implement QMessageBox.information(self, "New Profile", "Create test profile (coming soon)") def _on_change_database(self): """Change database file.""" from PyQt6.QtWidgets import QFileDialog # Show file dialog new_db_path, _ = QFileDialog.getOpenFileName( self, "Select Database File", "./database", "Database Files (*.db *.sqlite);;All Files (*)" ) if not new_db_path: return # User cancelled # Confirm change reply = QMessageBox.question( self, "Change Database", f"Switch to database:\n{new_db_path}\n\nThis will reload all data. Continue?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply != QMessageBox.StandardButton.Yes: return try: # Close current database if self.db_manager: self.db_manager.close() # Update path and reconnect self.db_path = new_db_path self._init_database() # Update all widgets with new database connection if self.session_widget: self.session_widget.db_manager = self.db_manager self.session_widget.db_conn = self.db_manager.get_connection() self.session_widget.refresh_profiles() if self.configure_session_widget: self.configure_session_widget.db_manager = self.db_manager self.configure_session_widget.db_conn = self.db_manager.get_connection() self.configure_session_widget._load_profiles() if self.configure_interface_widget: self.configure_interface_widget.db_manager = self.db_manager self.configure_interface_widget.db_conn = self.db_manager.get_connection() self.configure_interface_widget._load_profiles() if self.uart_widget: self.uart_widget.db_conn = self.db_manager.get_connection() if self.i2c_widget: self.i2c_widget.db_conn = self.db_manager.get_connection() if self.graph_widget: self.graph_widget.set_database_path(new_db_path) QMessageBox.information( self, "Success", f"Database changed successfully!\n\nNow using: {new_db_path}" ) self.status_bar.showMessage(f"Database changed to: {new_db_path}") except Exception as e: QMessageBox.critical( self, "Error", f"Failed to change database:\n{str(e)}" ) def _on_backup_database(self): """Backup database to selected location.""" from PyQt6.QtWidgets import QFileDialog import shutil from datetime import datetime # Generate default backup filename with timestamp timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") db_basename = os.path.basename(self.db_path) db_name_no_ext = os.path.splitext(db_basename)[0] default_name = f"{db_name_no_ext}_backup_{timestamp}.db" # Open save dialog backup_path, _ = QFileDialog.getSaveFileName( self, "Backup Database", os.path.join("./database", default_name), "Database Files (*.db);;All Files (*)" ) if not backup_path: return # User cancelled try: # Ensure .db extension if not backup_path.endswith('.db'): backup_path += '.db' # Copy database file shutil.copy2(self.db_path, backup_path) # Get file size backup_size = os.path.getsize(backup_path) size_str = self.db_manager.format_size(backup_size) QMessageBox.information( self, "Backup Complete", f"Database backed up successfully!\n\n" f"Location: {backup_path}\n" f"Size: {size_str}" ) except Exception as e: QMessageBox.critical( self, "Backup Failed", f"Failed to backup database:\n{str(e)}" ) def _on_vacuum_database(self): """Vacuum database.""" reply = QMessageBox.question( self, "Vacuum Database", "Vacuum database to reclaim space?\n\nThis may take a few seconds.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: self.db_manager.vacuum() QMessageBox.information(self, "Success", "Database vacuumed successfully!") def _on_database_info(self): """Show comprehensive database statistics.""" from PyQt6.QtWidgets import QDialog, QVBoxLayout, QTextEdit try: conn = self.db_manager.get_connection() # === Database File Info === size_bytes, percentage, status = self.db_manager.check_size() # === Session Statistics === cursor = conn.execute("SELECT COUNT(*) FROM sessions") total_sessions = cursor.fetchone()[0] cursor = conn.execute("SELECT SUM(total_runs) FROM sessions") total_runs = cursor.fetchone()[0] or 0 cursor = conn.execute(""" SELECT session_id, session_name, total_runs, created_at FROM sessions ORDER BY total_runs DESC LIMIT 1 """) most_active = cursor.fetchone() cursor = conn.execute(""" SELECT MIN(created_at), MAX(created_at) FROM sessions """) date_range = cursor.fetchone() # === Command Statistics === cursor = conn.execute("SELECT COUNT(*) FROM uart_commands") uart_commands = cursor.fetchone()[0] cursor = conn.execute("SELECT COUNT(*) FROM i2c_commands") i2c_commands = cursor.fetchone()[0] # === Profile Statistics === cursor = conn.execute("SELECT COUNT(*) FROM interface_profiles") interface_profiles = cursor.fetchone()[0] cursor = conn.execute("SELECT COUNT(*) FROM session_profiles") session_profiles = cursor.fetchone()[0] # === Telemetry Statistics === cursor = conn.execute("SELECT COUNT(*) FROM telemetry_raw") raw_records = cursor.fetchone()[0] cursor = conn.execute("SELECT COUNT(*) FROM telemetry_decoded") decoded_records = cursor.fetchone()[0] # Build info message msg = "=" * 60 + "\n" msg += "DATABASE OVERVIEW\n" msg += "=" * 60 + "\n\n" # Database File msg += "📁 DATABASE FILE\n" msg += "-" * 60 + "\n" msg += f"Path: {self.db_path}\n" msg += f"Size: {self.db_manager.format_size(size_bytes)} / 2.0 GB ({percentage:.1f}%)\n" msg += f"Status: {status}\n\n" # Sessions msg += "📊 SESSIONS\n" msg += "-" * 60 + "\n" msg += f"Total Sessions: {total_sessions}\n" msg += f"Total Runs: {total_runs}\n" if total_sessions > 0: msg += f"Average Runs/Session: {total_runs / total_sessions:.1f}\n" if most_active: msg += f"Most Active Session: {most_active[1]} ({most_active[2]} runs)\n" if date_range[0] and date_range[1]: msg += f"Date Range: {date_range[0]} to {date_range[1]}\n" msg += "\n" # Commands msg += "⚙️ COMMANDS\n" msg += "-" * 60 + "\n" msg += f"UART Commands: {uart_commands}\n" msg += f"I2C Commands: {i2c_commands}\n" msg += f"Total Commands: {uart_commands + i2c_commands}\n\n" # Profiles msg += "📝 PROFILES\n" msg += "-" * 60 + "\n" msg += f"Interface Profiles: {interface_profiles}\n" msg += f"Session Profiles: {session_profiles}\n\n" # Telemetry msg += "📈 TELEMETRY DATA\n" msg += "-" * 60 + "\n" msg += f"Raw Records: {raw_records:,}\n" msg += f"Decoded Records: {decoded_records:,}\n" msg += f"Total Data Points: {(raw_records + decoded_records):,}\n" if decoded_records > 0: msg += f"Avg Data Points/Run: {decoded_records / max(1, total_runs):.0f}\n" msg += "\n" # Table sizes msg += "🗄️ TABLE DETAILS\n" msg += "-" * 60 + "\n" # Get all table names cursor = conn.execute(""" SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name """) tables = cursor.fetchall() # Get row count for each table for table_row in tables: table_name = table_row[0] try: cursor = conn.execute(f"SELECT COUNT(*) FROM {table_name}") count = cursor.fetchone()[0] msg += f"{table_name:30s}: {count:>10,} rows\n" except Exception as e: msg += f"{table_name:30s}: Error\n" # Create dialog dialog = QDialog(self) dialog.setWindowTitle("Database Information") dialog.resize(700, 600) layout = QVBoxLayout() dialog.setLayout(layout) # Text display text_edit = QTextEdit() text_edit.setReadOnly(True) text_edit.setPlainText(msg) text_edit.setFont(QFont("Monospace", 9)) layout.addWidget(text_edit) # Close button close_btn = QPushButton("Close") close_btn.clicked.connect(dialog.accept) layout.addWidget(close_btn) dialog.exec() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to get database info:\n{str(e)}") def _on_about(self): """Show about dialog.""" msg = "vzug-e-hinge Test & Control System\n\n" msg += "Version 1.0.0\n\n" msg += "Integrated test and control system for e-hinge devices.\n\n" msg += "Author: Kynsight\n" msg += "© 2025" QMessageBox.about(self, "About", msg) def _on_documentation(self): """Show documentation.""" # TODO: Open documentation QMessageBox.information(self, "Documentation", "Documentation (coming soon)") def _on_tab_changed(self, index: int): """ Handle tab change event. Refresh profiles when Session tab becomes active. Args: index: Index of newly selected tab """ # Check if Session tab (index 0) is now active if index == 0 and hasattr(self, 'session_widget') and self.session_widget: self.session_widget.refresh_profiles() def closeEvent(self, event): """Handle application close.""" reply = QMessageBox.question( self, "Exit", "Are you sure you want to exit?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: # Close database if self.db_manager: self.db_manager.close() event.accept() else: event.ignore() def main(): """Main entry point.""" # Parse command line arguments db_path = "./database/ehinge.db" if len(sys.argv) > 1: db_path = sys.argv[1] # Create application app = QApplication(sys.argv) # Set application metadata app.setApplicationName("vzug-e-hinge") app.setOrganizationName("Kynsight") app.setOrganizationDomain("kynsight.com") # Create main window window = MainWindow(db_path) window.show() # Run application sys.exit(app.exec()) if __name__ == "__main__": main()