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.

937 lines
34 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,
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()