| |
| """ |
| Enhanced GPU Desktop Monitor |
| |
| Provides multiple display modes for GPU monitoring: |
| - Overlay: Floating transparent window |
| - System Tray: Minimal tray icon with tooltips |
| - Dashboard: Full-featured desktop application |
| """ |
|
|
| import sys |
| import os |
| import time |
| import json |
| import logging |
| import threading |
| from typing import Dict, List, Optional, Tuple |
| from dataclasses import dataclass |
| from pathlib import Path |
|
|
| from PyQt5.QtWidgets import ( |
| QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, |
| QSystemTrayIcon, QMenu, QAction, QMainWindow, QTabWidget, QGridLayout, |
| QGroupBox, QProgressBar, QMessageBox, QComboBox, QCheckBox, QSpinBox, |
| QDoubleSpinBox, QFrame, QScrollArea |
| ) |
| from PyQt5.QtCore import QTimer, Qt, QPoint, QThread, pyqtSignal, QObject |
| from PyQt5.QtGui import QIcon, QFont, QColor, QPainter, QPen, QPixmap |
| import matplotlib.pyplot as plt |
| from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas |
| import numpy as np |
|
|
| from gpu_monitoring import GPUManager, GPUStatus |
| from gpu_fan_controller import FanController, FanMode, ProfileType |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class GPUDataThread(QThread): |
| """Background thread for collecting GPU data.""" |
| data_updated = pyqtSignal(dict) |
| error_occurred = pyqtSignal(str) |
| |
| def __init__(self, gpu_manager: GPUManager, update_interval: float = 1.0): |
| super().__init__() |
| self.gpu_manager = gpu_manager |
| self.update_interval = update_interval |
| self.running = False |
| |
| def run(self): |
| """Main thread loop.""" |
| self.running = True |
| while self.running: |
| try: |
| status = self.gpu_manager.get_status() |
| self.data_updated.emit(status) |
| time.sleep(self.update_interval) |
| except Exception as e: |
| self.error_occurred.emit(str(e)) |
| time.sleep(1) |
| |
| def stop(self): |
| """Stop the thread.""" |
| self.running = False |
| self.wait() |
|
|
|
|
| class FanControlThread(QThread): |
| """Background thread for fan control.""" |
| status_updated = pyqtSignal(object) |
| |
| def __init__(self, fan_controller: FanController, update_interval: float = 2.0): |
| super().__init__() |
| self.fan_controller = fan_controller |
| self.update_interval = update_interval |
| self.running = False |
| |
| def run(self): |
| """Main thread loop.""" |
| self.running = True |
| while self.running: |
| try: |
| status = self.fan_controller.get_status() |
| if status: |
| self.status_updated.emit(status) |
| time.sleep(self.update_interval) |
| except Exception as e: |
| logger.error(f"Fan control thread error: {e}") |
| time.sleep(1) |
| |
| def stop(self): |
| """Stop the thread.""" |
| self.running = False |
| self.wait() |
|
|
|
|
| class GPUOverlayWindow(QWidget): |
| """Floating overlay window for GPU monitoring.""" |
| |
| def __init__(self, config: Dict): |
| super().__init__() |
| self.config = config |
| self.gpu_manager = GPUManager() |
| self.fan_controller = FanController() |
| |
| self.init_ui() |
| self.init_data_collection() |
| |
| |
| self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.Tool) |
| self.setAttribute(Qt.WA_TranslucentBackground) |
| self.setFixedSize(200, 160) |
| |
| |
| self.old_pos = None |
| |
| |
| self.gpu_manager.initialize() |
| self.fan_controller.initialize() |
| |
| def init_ui(self): |
| """Initialize the overlay UI.""" |
| |
| layout = QVBoxLayout() |
| layout.setContentsMargins(10, 10, 10, 10) |
| layout.setSpacing(5) |
| |
| |
| self.lbl_title = QLabel("GPU Monitor") |
| self.lbl_title.setStyleSheet(""" |
| QLabel { |
| color: #3498db; |
| font-size: 14px; |
| font-weight: bold; |
| border-bottom: 1px solid #3498db; |
| padding-bottom: 5px; |
| } |
| """) |
| layout.addWidget(self.lbl_title) |
| |
| |
| self.lbl_gpu = QLabel("GPU: --") |
| self.lbl_gpu.setStyleSheet("color: #ecf0f1; font-size: 12px;") |
| layout.addWidget(self.lbl_gpu) |
| |
| |
| temp_layout = QHBoxLayout() |
| self.lbl_temp_label = QLabel("Temp:") |
| self.lbl_temp_label.setStyleSheet("color: #ecf0f1; font-size: 12px;") |
| self.lbl_temp_value = QLabel("--°C") |
| self.lbl_temp_value.setStyleSheet("color: #e74c3c; font-size: 12px; font-weight: bold;") |
| temp_layout.addWidget(self.lbl_temp_label) |
| temp_layout.addStretch() |
| temp_layout.addWidget(self.lbl_temp_value) |
| layout.addLayout(temp_layout) |
| |
| |
| load_layout = QHBoxLayout() |
| self.lbl_load_label = QLabel("Load:") |
| self.lbl_load_label.setStyleSheet("color: #ecf0f1; font-size: 12px;") |
| self.lbl_load_value = QLabel("--%") |
| self.lbl_load_value.setStyleSheet("color: #f1c40f; font-size: 12px; font-weight: bold;") |
| load_layout.addWidget(self.lbl_load_label) |
| load_layout.addStretch() |
| load_layout.addWidget(self.lbl_load_value) |
| layout.addLayout(load_layout) |
| |
| |
| fan_layout = QHBoxLayout() |
| self.lbl_fan_label = QLabel("Fan:") |
| self.lbl_fan_label.setStyleSheet("color: #ecf0f1; font-size: 12px;") |
| self.lbl_fan_value = QLabel("-- RPM") |
| self.lbl_fan_value.setStyleSheet("color: #2ecc71; font-size: 12px; font-weight: bold;") |
| fan_layout.addWidget(self.lbl_fan_label) |
| fan_layout.addStretch() |
| fan_layout.addWidget(self.lbl_fan_value) |
| layout.addLayout(fan_layout) |
| |
| |
| power_layout = QHBoxLayout() |
| self.lbl_power_label = QLabel("Power:") |
| self.lbl_power_label.setStyleSheet("color: #ecf0f1; font-size: 12px;") |
| self.lbl_power_value = QLabel("-- W") |
| self.lbl_power_value.setStyleSheet("color: #9b59b6; font-size: 12px; font-weight: bold;") |
| power_layout.addWidget(self.lbl_power_label) |
| power_layout.addStretch() |
| power_layout.addWidget(self.lbl_power_value) |
| layout.addLayout(power_layout) |
| |
| |
| mem_layout = QHBoxLayout() |
| self.lbl_mem_label = QLabel("VRAM:") |
| self.lbl_mem_label.setStyleSheet("color: #ecf0f1; font-size: 12px;") |
| self.lbl_mem_value = QLabel("-- MB") |
| self.lbl_mem_value.setStyleSheet("color: #1abc9c; font-size: 12px; font-weight: bold;") |
| mem_layout.addWidget(self.lbl_mem_label) |
| mem_layout.addStretch() |
| mem_layout.addWidget(self.lbl_mem_value) |
| layout.addLayout(mem_layout) |
| |
| |
| self.btn_close = QPushButton("×") |
| self.btn_close.setFixedSize(20, 20) |
| self.btn_close.setStyleSheet(""" |
| QPushButton { |
| background-color: transparent; |
| color: #e74c3c; |
| font-size: 18px; |
| font-weight: bold; |
| border: none; |
| margin: 0; |
| padding: 0; |
| } |
| QPushButton:hover { |
| color: #ff0000; |
| } |
| """) |
| self.btn_close.clicked.connect(self.close) |
| |
| |
| close_layout = QHBoxLayout() |
| close_layout.addStretch() |
| close_layout.addWidget(self.btn_close) |
| layout.addLayout(close_layout) |
| |
| self.setLayout(layout) |
| |
| |
| self.setStyleSheet(""" |
| QWidget { |
| background-color: rgba(0, 0, 0, 200); |
| border-radius: 8px; |
| border: 1px solid #3498db; |
| } |
| """) |
| |
| def init_data_collection(self): |
| """Initialize data collection threads.""" |
| |
| self.gpu_thread = GPUDataThread(self.gpu_manager, self.config['update_interval']) |
| self.gpu_thread.data_updated.connect(self.update_gpu_data) |
| self.gpu_thread.error_occurred.connect(self.handle_error) |
| self.gpu_thread.start() |
| |
| |
| self.fan_thread = FanControlThread(self.fan_controller, 2.0) |
| self.fan_thread.status_updated.connect(self.update_fan_data) |
| self.fan_thread.start() |
| |
| def update_gpu_data(self, status_dict: Dict[str, Optional[GPUStatus]]): |
| """Update GPU data display.""" |
| for gpu_name, gpu_status in status_dict.items(): |
| if gpu_status: |
| |
| self.lbl_gpu.setText(f"GPU: {gpu_name}") |
| |
| |
| temp_color = self.get_temp_color(gpu_status.temperature) |
| self.lbl_temp_value.setText(f"{gpu_status.temperature:.1f}°C") |
| self.lbl_temp_value.setStyleSheet(f"color: {temp_color}; font-size: 12px; font-weight: bold;") |
| |
| |
| load_color = self.get_load_color(gpu_status.load) |
| self.lbl_load_value.setText(f"{gpu_status.load:.1f}%") |
| self.lbl_load_value.setStyleSheet(f"color: {load_color}; font-size: 12px; font-weight: bold;") |
| |
| |
| fan_rpm = self.calculate_fan_rpm(gpu_status.fan_pwm) |
| self.lbl_fan_value.setText(f"{fan_rpm} RPM ({gpu_status.fan_pwm}%)") |
| |
| |
| self.lbl_power_value.setText(f"{gpu_status.power_draw:.1f} W") |
| |
| |
| self.lbl_mem_value.setText(f"{gpu_status.memory_used}/{gpu_status.memory_total} MB") |
| |
| break |
| |
| def update_fan_data(self, fan_status): |
| """Update fan control data.""" |
| |
| pass |
| |
| def handle_error(self, error_msg: str): |
| """Handle data collection errors.""" |
| logger.error(f"Data collection error: {error_msg}") |
| |
| |
| def get_temp_color(self, temperature: float) -> str: |
| """Get color based on temperature.""" |
| if temperature < 60: |
| return "#2ecc71" |
| elif temperature < 75: |
| return "#f1c40f" |
| else: |
| return "#e74c3c" |
| |
| def get_load_color(self, load: float) -> str: |
| """Get color based on load.""" |
| if load < 50: |
| return "#2ecc71" |
| elif load < 80: |
| return "#f1c40f" |
| else: |
| return "#e74c3c" |
| |
| def calculate_fan_rpm(self, pwm: int) -> int: |
| """Calculate approximate RPM from PWM.""" |
| return int((pwm / 255) * 4000) |
| |
| def mousePressEvent(self, event): |
| """Handle mouse press for dragging.""" |
| if event.button() == Qt.LeftButton: |
| self.old_pos = event.globalPos() |
| |
| def mouseMoveEvent(self, event): |
| """Handle mouse move for dragging.""" |
| if self.old_pos is not None: |
| delta = QPoint(event.globalPos() - self.old_pos) |
| self.move(self.x() + delta.x(), self.y() + delta.y()) |
| self.old_pos = event.globalPos() |
| |
| def mouseReleaseEvent(self, event): |
| """Handle mouse release.""" |
| self.old_pos = None |
| |
| def closeEvent(self, event): |
| """Clean up threads on close.""" |
| self.gpu_thread.stop() |
| self.fan_thread.stop() |
| super().closeEvent(event) |
|
|
|
|
| class SystemTrayMonitor(QSystemTrayIcon): |
| """System tray icon for GPU monitoring.""" |
| |
| def __init__(self, config: Dict): |
| super().__init__() |
| |
| self.config = config |
| self.gpu_manager = GPUManager() |
| self.fan_controller = FanController() |
| |
| self.init_ui() |
| self.init_data_collection() |
| |
| |
| self.gpu_manager.initialize() |
| self.fan_controller.initialize() |
| |
| def init_ui(self): |
| """Initialize system tray UI.""" |
| |
| self.setIcon(QIcon.fromTheme("video-display")) |
| |
| |
| menu = QMenu() |
| |
| |
| self.action_status = QAction("Status: --", self) |
| self.action_status.setEnabled(False) |
| menu.addAction(self.action_status) |
| |
| menu.addSeparator() |
| |
| |
| self.profile_group = QAction("Profiles", self) |
| menu.addAction(self.profile_group) |
| |
| self.action_silent = QAction("Silent", self) |
| self.action_silent.triggered.connect(lambda: self.set_profile("silent")) |
| menu.addAction(self.action_silent) |
| |
| self.action_balanced = QAction("Balanced", self) |
| self.action_balanced.triggered.connect(lambda: self.set_profile("balanced")) |
| menu.addAction(self.action_balanced) |
| |
| self.action_performance = QAction("Performance", self) |
| self.action_performance.triggered.connect(lambda: self.set_profile("performance")) |
| menu.addAction(self.action_performance) |
| |
| menu.addSeparator() |
| |
| |
| self.action_dashboard = QAction("Open Dashboard", self) |
| self.action_dashboard.triggered.connect(self.open_dashboard) |
| menu.addAction(self.action_dashboard) |
| |
| menu.addSeparator() |
| |
| |
| self.action_exit = QAction("Exit", self) |
| self.action_exit.triggered.connect(QApplication.instance().quit) |
| menu.addAction(self.action_exit) |
| |
| self.setContextMenu(menu) |
| self.activated.connect(self.on_tray_activated) |
| |
| self.show() |
| |
| def init_data_collection(self): |
| """Initialize data collection threads.""" |
| |
| self.gpu_thread = GPUDataThread(self.gpu_manager, self.config['update_interval']) |
| self.gpu_thread.data_updated.connect(self.update_tray_tooltip) |
| self.gpu_thread.start() |
| |
| def update_tray_tooltip(self, status_dict: Dict[str, Optional[GPUStatus]]): |
| """Update system tray tooltip.""" |
| tooltip = "GPU Monitor\n" |
| |
| for gpu_name, gpu_status in status_dict.items(): |
| if gpu_status: |
| tooltip += f"\n{gpu_name}:" |
| tooltip += f"\n Temp: {gpu_status.temperature:.1f}°C" |
| tooltip += f"\n Load: {gpu_status.load:.1f}%" |
| tooltip += f"\n Fan: {self.calculate_fan_rpm(gpu_status.fan_pwm)} RPM" |
| tooltip += f"\n Power: {gpu_status.power_draw:.1f} W" |
| tooltip += f"\n VRAM: {gpu_status.memory_used}/{gpu_status.memory_total} MB" |
| break |
| |
| self.setToolTip(tooltip) |
| |
| |
| if gpu_status: |
| self.action_status.setText(f"Status: {gpu_status.temperature:.1f}°C, {gpu_status.load:.1f}%") |
| |
| def on_tray_activated(self, reason): |
| """Handle tray icon activation.""" |
| if reason == QSystemTrayIcon.Trigger: |
| |
| pass |
| |
| def set_profile(self, profile_name: str): |
| """Set fan profile.""" |
| if self.fan_controller.set_profile(profile_name): |
| QMessageBox.information(None, "Profile Changed", f"Switched to {profile_name} profile") |
| |
| def open_dashboard(self): |
| """Open the main dashboard window.""" |
| |
| pass |
| |
| def calculate_fan_rpm(self, pwm: int) -> int: |
| """Calculate approximate RPM from PWM.""" |
| return int((pwm / 255) * 4000) |
| |
| def closeEvent(self, event): |
| """Clean up threads on close.""" |
| self.gpu_thread.stop() |
| super().closeEvent(event) |
|
|
|
|
| class DashboardWindow(QMainWindow): |
| """Full-featured dashboard window.""" |
| |
| def __init__(self, config: Dict): |
| super().__init__() |
| |
| self.config = config |
| self.gpu_manager = GPUManager() |
| self.fan_controller = FanController() |
| |
| self.init_ui() |
| self.init_data_collection() |
| |
| |
| self.gpu_manager.initialize() |
| self.fan_controller.initialize() |
| |
| self.setWindowTitle("GPU Monitoring Dashboard") |
| self.setGeometry(100, 100, 800, 600) |
| |
| def init_ui(self): |
| """Initialize dashboard UI.""" |
| |
| main_widget = QWidget() |
| main_layout = QVBoxLayout() |
| |
| |
| self.tabs = QTabWidget() |
| |
| |
| self.overview_tab = self.create_overview_tab() |
| self.tabs.addTab(self.overview_tab, "Overview") |
| |
| |
| self.charts_tab = self.create_charts_tab() |
| self.tabs.addTab(self.charts_tab, "Charts") |
| |
| |
| self.fan_tab = self.create_fan_tab() |
| self.tabs.addTab(self.fan_tab, "Fan Control") |
| |
| |
| self.settings_tab = self.create_settings_tab() |
| self.tabs.addTab(self.settings_tab, "Settings") |
| |
| main_layout.addWidget(self.tabs) |
| main_widget.setLayout(main_layout) |
| self.setCentralWidget(main_widget) |
| |
| def create_overview_tab(self): |
| """Create the overview tab.""" |
| widget = QWidget() |
| layout = QGridLayout() |
| |
| |
| gpu_group = QGroupBox("GPU Information") |
| gpu_layout = QGridLayout() |
| |
| self.lbl_gpu_name = QLabel("--") |
| self.lbl_gpu_name.setStyleSheet("font-weight: bold; font-size: 16px;") |
| gpu_layout.addWidget(QLabel("GPU:"), 0, 0) |
| gpu_layout.addWidget(self.lbl_gpu_name, 0, 1) |
| |
| self.lbl_temp = QLabel("-- °C") |
| self.lbl_temp.setStyleSheet("font-size: 14px; color: #e74c3c;") |
| gpu_layout.addWidget(QLabel("Temperature:"), 1, 0) |
| gpu_layout.addWidget(self.lbl_temp, 1, 1) |
| |
| self.lbl_load = QLabel("-- %") |
| self.lbl_load.setStyleSheet("font-size: 14px; color: #f1c40f;") |
| gpu_layout.addWidget(QLabel("Load:"), 2, 0) |
| gpu_layout.addWidget(self.lbl_load, 2, 1) |
| |
| self.lbl_power = QLabel("-- W") |
| self.lbl_power.setStyleSheet("font-size: 14px; color: #9b59b6;") |
| gpu_layout.addWidget(QLabel("Power:"), 3, 0) |
| gpu_layout.addWidget(self.lbl_power, 3, 1) |
| |
| self.lbl_vram = QLabel("-- MB") |
| self.lbl_vram.setStyleSheet("font-size: 14px; color: #1abc9c;") |
| gpu_layout.addWidget(QLabel("VRAM:"), 4, 0) |
| gpu_layout.addWidget(self.lbl_vram, 4, 1) |
| |
| gpu_group.setLayout(gpu_layout) |
| layout.addWidget(gpu_group, 0, 0) |
| |
| |
| fan_group = QGroupBox("Fan Control") |
| fan_layout = QGridLayout() |
| |
| self.lbl_fan_mode = QLabel("--") |
| fan_layout.addWidget(QLabel("Mode:"), 0, 0) |
| fan_layout.addWidget(self.lbl_fan_mode, 0, 1) |
| |
| self.lbl_fan_profile = QLabel("--") |
| fan_layout.addWidget(QLabel("Profile:"), 1, 0) |
| fan_layout.addWidget(self.lbl_fan_profile, 1, 1) |
| |
| self.lbl_fan_speed = QLabel("-- RPM") |
| fan_layout.addWidget(QLabel("Speed:"), 2, 0) |
| fan_layout.addWidget(self.lbl_fan_speed, 2, 1) |
| |
| self.lbl_fan_pwm = QLabel("-- %") |
| fan_layout.addWidget(QLabel("PWM:"), 3, 0) |
| fan_layout.addWidget(self.lbl_fan_pwm, 3, 1) |
| |
| fan_group.setLayout(fan_layout) |
| layout.addWidget(fan_group, 0, 1) |
| |
| widget.setLayout(layout) |
| return widget |
| |
| def create_charts_tab(self): |
| """Create the charts tab.""" |
| widget = QWidget() |
| layout = QVBoxLayout() |
| |
| |
| self.figure, self.axes = plt.subplots(2, 2, figsize=(10, 8)) |
| self.canvas = FigureCanvas(self.figure) |
| |
| layout.addWidget(self.canvas) |
| widget.setLayout(layout) |
| return widget |
| |
| def create_fan_tab(self): |
| """Create the fan control tab.""" |
| widget = QWidget() |
| layout = QVBoxLayout() |
| |
| |
| profile_layout = QHBoxLayout() |
| profile_layout.addWidget(QLabel("Select Profile:")) |
| self.combo_profile = QComboBox() |
| self.combo_profile.addItems(["Silent", "Balanced", "Performance"]) |
| self.combo_profile.currentTextChanged.connect(self.on_profile_changed) |
| profile_layout.addWidget(self.combo_profile) |
| |
| layout.addLayout(profile_layout) |
| |
| |
| manual_group = QGroupBox("Manual Control") |
| manual_layout = QHBoxLayout() |
| |
| self.spin_manual_pwm = QSpinBox() |
| self.spin_manual_pwm.setRange(0, 255) |
| self.spin_manual_pwm.setValue(0) |
| manual_layout.addWidget(QLabel("Manual PWM:")) |
| manual_layout.addWidget(self.spin_manual_pwm) |
| |
| self.btn_set_manual = QPushButton("Set Manual") |
| self.btn_set_manual.clicked.connect(self.on_set_manual) |
| manual_layout.addWidget(self.btn_set_manual) |
| |
| manual_group.setLayout(manual_layout) |
| layout.addWidget(manual_group) |
| |
| widget.setLayout(layout) |
| return widget |
| |
| def create_settings_tab(self): |
| """Create the settings tab.""" |
| widget = QWidget() |
| layout = QVBoxLayout() |
| |
| |
| interval_layout = QHBoxLayout() |
| interval_layout.addWidget(QLabel("Update Interval (seconds):")) |
| self.spin_interval = QDoubleSpinBox() |
| self.spin_interval.setRange(0.1, 10.0) |
| self.spin_interval.setValue(self.config['update_interval']) |
| self.spin_interval.valueChanged.connect(self.on_interval_changed) |
| interval_layout.addWidget(self.spin_interval) |
| layout.addLayout(interval_layout) |
| |
| |
| display_group = QGroupBox("Display Options") |
| display_layout = QVBoxLayout() |
| |
| self.chk_show_temp = QCheckBox("Show Temperature") |
| self.chk_show_temp.setChecked(self.config['show_temperature']) |
| display_layout.addWidget(self.chk_show_temp) |
| |
| self.chk_show_load = QCheckBox("Show Load") |
| self.chk_show_load.setChecked(self.config['show_gpu_load']) |
| display_layout.addWidget(self.chk_show_load) |
| |
| self.chk_show_fan = QCheckBox("Show Fan Speed") |
| self.chk_show_fan.setChecked(self.config['show_fan_speed']) |
| display_layout.addWidget(self.chk_show_fan) |
| |
| display_group.setLayout(display_layout) |
| layout.addWidget(display_group) |
| |
| widget.setLayout(layout) |
| return widget |
| |
| def init_data_collection(self): |
| """Initialize data collection threads.""" |
| |
| self.gpu_thread = GPUDataThread(self.gpu_manager, self.config['update_interval']) |
| self.gpu_thread.data_updated.connect(self.update_dashboard) |
| self.gpu_thread.start() |
| |
| |
| self.fan_thread = FanControlThread(self.fan_controller, 2.0) |
| self.fan_thread.status_updated.connect(self.update_fan_status) |
| self.fan_thread.start() |
| |
| def update_dashboard(self, status_dict: Dict[str, Optional[GPUStatus]]): |
| """Update dashboard display.""" |
| for gpu_name, gpu_status in status_dict.items(): |
| if gpu_status: |
| |
| self.lbl_gpu_name.setText(gpu_name) |
| self.lbl_temp.setText(f"{gpu_status.temperature:.1f} °C") |
| self.lbl_load.setText(f"{gpu_status.load:.1f} %") |
| self.lbl_power.setText(f"{gpu_status.power_draw:.1f} W") |
| self.lbl_vram.setText(f"{gpu_status.memory_used}/{gpu_status.memory_total} MB") |
| break |
| |
| def update_fan_status(self, fan_status): |
| """Update fan status display.""" |
| if fan_status: |
| self.lbl_fan_mode.setText(fan_status.mode.value) |
| self.lbl_fan_profile.setText(fan_status.profile) |
| self.lbl_fan_speed.setText(f"{self.calculate_fan_rpm(fan_status.current_pwm)} RPM") |
| self.lbl_fan_pwm.setText(f"{fan_status.current_pwm}%") |
| |
| def on_profile_changed(self, profile_name: str): |
| """Handle profile change.""" |
| self.fan_controller.set_profile(profile_name.lower()) |
| |
| def on_set_manual(self): |
| """Handle manual PWM setting.""" |
| pwm = self.spin_manual_pwm.value() |
| self.fan_controller.set_manual_pwm(pwm) |
| |
| def on_interval_changed(self, value: float): |
| """Handle update interval change.""" |
| self.config['update_interval'] = value |
| self.gpu_thread.update_interval = value |
| |
| def calculate_fan_rpm(self, pwm: int) -> int: |
| """Calculate approximate RPM from PWM.""" |
| return int((pwm / 255) * 4000) |
| |
| def closeEvent(self, event): |
| """Clean up threads on close.""" |
| self.gpu_thread.stop() |
| self.fan_thread.stop() |
| super().closeEvent(event) |
|
|
|
|
| class GPUApplication: |
| """Main application class.""" |
| |
| def __init__(self): |
| self.config = self.load_config() |
| self.app = QApplication(sys.argv) |
| |
| |
| self.app.setStyle("Fusion") |
| |
| |
| if self.config['display_mode'] == 'overlay': |
| self.window = GPUOverlayWindow(self.config) |
| elif self.config['display_mode'] == 'tray': |
| self.window = SystemTrayMonitor(self.config) |
| else: |
| self.window = DashboardWindow(self.config) |
| |
| def load_config(self) -> Dict: |
| """Load configuration from file.""" |
| config_path = Path("config/monitoring.json") |
| if config_path.exists(): |
| with open(config_path, 'r') as f: |
| return json.load(f) |
| else: |
| |
| return { |
| "update_interval": 1.0, |
| "display_mode": "overlay", |
| "show_gpu_load": True, |
| "show_temperature": True, |
| "show_fan_speed": True, |
| "show_power": True, |
| "show_vram": True |
| } |
| |
| def run(self): |
| """Run the application.""" |
| if hasattr(self.window, 'show'): |
| self.window.show() |
| |
| sys.exit(self.app.exec_()) |
|
|
|
|
| if __name__ == "__main__": |
| app = GPUApplication() |
| app.run() |