#!/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 ) from PyQt6.QtCore import Qt, QTimer from PyQt6.QtGui import QAction # Import database from database.init_database import DatabaseManager # Import widgets (will be created next) # from session_widget import SessionWidget # from uart_widget import UARTWidget # from uart_logger_widget import UARTLoggerWidget # from i2c_widget import I2CWidget # from i2c_logger_widget import I2CLoggerWidget # from graph_widget import GraphWidget # Import session manager (will be created next) # from session_manager import SessionManager 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.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 self.tab_graph = self._create_placeholder_tab("Graph Visualization") self.tabs.addTab(self.tab_graph, "Graph") 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_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) database_menu.addSeparator() action_cleanup = QAction("&Cleanup Old Data...", self) action_cleanup.triggered.connect(self._on_cleanup_data) database_menu.addAction(action_cleanup) # 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}") # ========================================================================= # 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_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 database information.""" size_bytes, percentage, status = self.db_manager.check_size() info = self.db_manager.get_table_info() msg = f"Database: {self.db_path}\n\n" msg += f"Size: {self.db_manager.format_size(size_bytes)} ({percentage:.1f}% of 2 GB)\n" msg += f"Status: {status}\n\n" msg += "Table Row Counts:\n" for table, count in info.items(): msg += f" {table}: {count}\n" QMessageBox.information(self, "Database Info", msg) def _on_cleanup_data(self): """Cleanup old data.""" # TODO: Implement QMessageBox.information(self, "Cleanup", "Data cleanup dialog (coming soon)") 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()