gpu_monitoring_system / gpu_monitor_desktop.py
meccatronis's picture
Upload gpu_monitor_desktop.py with huggingface_hub
c90e583 verified
#!/usr/bin/env python3
"""
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) # Wait before retrying
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()
# Window properties
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setFixedSize(200, 160)
# Drag functionality
self.old_pos = None
# Start monitoring
self.gpu_manager.initialize()
self.fan_controller.initialize()
def init_ui(self):
"""Initialize the overlay UI."""
# Main layout
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
# Title
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)
# GPU Info
self.lbl_gpu = QLabel("GPU: --")
self.lbl_gpu.setStyleSheet("color: #ecf0f1; font-size: 12px;")
layout.addWidget(self.lbl_gpu)
# Temperature
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
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
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
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)
# Memory
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)
# Close button
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)
# Add close button to layout
close_layout = QHBoxLayout()
close_layout.addStretch()
close_layout.addWidget(self.btn_close)
layout.addLayout(close_layout)
self.setLayout(layout)
# Apply styling
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."""
# GPU data thread
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()
# Fan control thread
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:
# Update GPU name
self.lbl_gpu.setText(f"GPU: {gpu_name}")
# Update temperature
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;")
# Update load
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;")
# Update fan
fan_rpm = self.calculate_fan_rpm(gpu_status.fan_pwm)
self.lbl_fan_value.setText(f"{fan_rpm} RPM ({gpu_status.fan_pwm}%)")
# Update power
self.lbl_power_value.setText(f"{gpu_status.power_draw:.1f} W")
# Update memory
self.lbl_mem_value.setText(f"{gpu_status.memory_used}/{gpu_status.memory_total} MB")
break # Only show first GPU
def update_fan_data(self, fan_status):
"""Update fan control data."""
# Fan status updates can be handled here if needed
pass
def handle_error(self, error_msg: str):
"""Handle data collection errors."""
logger.error(f"Data collection error: {error_msg}")
# Could show error notification here
def get_temp_color(self, temperature: float) -> str:
"""Get color based on temperature."""
if temperature < 60:
return "#2ecc71" # Green
elif temperature < 75:
return "#f1c40f" # Yellow
else:
return "#e74c3c" # Red
def get_load_color(self, load: float) -> str:
"""Get color based on load."""
if load < 50:
return "#2ecc71" # Green
elif load < 80:
return "#f1c40f" # Yellow
else:
return "#e74c3c" # Red
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()
# Start monitoring
self.gpu_manager.initialize()
self.fan_controller.initialize()
def init_ui(self):
"""Initialize system tray UI."""
# Create icon
self.setIcon(QIcon.fromTheme("video-display"))
# Create menu
menu = QMenu()
# Status actions
self.action_status = QAction("Status: --", self)
self.action_status.setEnabled(False)
menu.addAction(self.action_status)
menu.addSeparator()
# Profile actions
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()
# Open dashboard
self.action_dashboard = QAction("Open Dashboard", self)
self.action_dashboard.triggered.connect(self.open_dashboard)
menu.addAction(self.action_dashboard)
menu.addSeparator()
# Exit action
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."""
# GPU data thread
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)
# Update menu status
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:
# Show/hide dashboard
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."""
# This would need to be implemented to show the main 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()
# Start monitoring
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 and layout
main_widget = QWidget()
main_layout = QVBoxLayout()
# Tab widget
self.tabs = QTabWidget()
# Overview tab
self.overview_tab = self.create_overview_tab()
self.tabs.addTab(self.overview_tab, "Overview")
# Charts tab
self.charts_tab = self.create_charts_tab()
self.tabs.addTab(self.charts_tab, "Charts")
# Fan Control tab
self.fan_tab = self.create_fan_tab()
self.tabs.addTab(self.fan_tab, "Fan Control")
# Settings tab
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 Information Group
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 Information Group
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()
# Create matplotlib figure
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 selection
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 control
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()
# Update interval
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 options
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."""
# GPU data thread
self.gpu_thread = GPUDataThread(self.gpu_manager, self.config['update_interval'])
self.gpu_thread.data_updated.connect(self.update_dashboard)
self.gpu_thread.start()
# Fan control thread
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:
# Update GPU info
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)
# Set application style
self.app.setStyle("Fusion")
# Create appropriate window based on mode
if self.config['display_mode'] == 'overlay':
self.window = GPUOverlayWindow(self.config)
elif self.config['display_mode'] == 'tray':
self.window = SystemTrayMonitor(self.config)
else: # dashboard
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 default config
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()