You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

659 lines
24 KiB

#!/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}")
# 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_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()