meccatronis commited on
Commit
c90e583
·
verified ·
1 Parent(s): 8d4e0c1

Upload gpu_monitor_desktop.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. gpu_monitor_desktop.py +754 -0
gpu_monitor_desktop.py ADDED
@@ -0,0 +1,754 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Enhanced GPU Desktop Monitor
4
+
5
+ Provides multiple display modes for GPU monitoring:
6
+ - Overlay: Floating transparent window
7
+ - System Tray: Minimal tray icon with tooltips
8
+ - Dashboard: Full-featured desktop application
9
+ """
10
+
11
+ import sys
12
+ import os
13
+ import time
14
+ import json
15
+ import logging
16
+ import threading
17
+ from typing import Dict, List, Optional, Tuple
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+ from PyQt5.QtWidgets import (
22
+ QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
23
+ QSystemTrayIcon, QMenu, QAction, QMainWindow, QTabWidget, QGridLayout,
24
+ QGroupBox, QProgressBar, QMessageBox, QComboBox, QCheckBox, QSpinBox,
25
+ QDoubleSpinBox, QFrame, QScrollArea
26
+ )
27
+ from PyQt5.QtCore import QTimer, Qt, QPoint, QThread, pyqtSignal, QObject
28
+ from PyQt5.QtGui import QIcon, QFont, QColor, QPainter, QPen, QPixmap
29
+ import matplotlib.pyplot as plt
30
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
31
+ import numpy as np
32
+
33
+ from gpu_monitoring import GPUManager, GPUStatus
34
+ from gpu_fan_controller import FanController, FanMode, ProfileType
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ class GPUDataThread(QThread):
40
+ """Background thread for collecting GPU data."""
41
+ data_updated = pyqtSignal(dict)
42
+ error_occurred = pyqtSignal(str)
43
+
44
+ def __init__(self, gpu_manager: GPUManager, update_interval: float = 1.0):
45
+ super().__init__()
46
+ self.gpu_manager = gpu_manager
47
+ self.update_interval = update_interval
48
+ self.running = False
49
+
50
+ def run(self):
51
+ """Main thread loop."""
52
+ self.running = True
53
+ while self.running:
54
+ try:
55
+ status = self.gpu_manager.get_status()
56
+ self.data_updated.emit(status)
57
+ time.sleep(self.update_interval)
58
+ except Exception as e:
59
+ self.error_occurred.emit(str(e))
60
+ time.sleep(1) # Wait before retrying
61
+
62
+ def stop(self):
63
+ """Stop the thread."""
64
+ self.running = False
65
+ self.wait()
66
+
67
+
68
+ class FanControlThread(QThread):
69
+ """Background thread for fan control."""
70
+ status_updated = pyqtSignal(object)
71
+
72
+ def __init__(self, fan_controller: FanController, update_interval: float = 2.0):
73
+ super().__init__()
74
+ self.fan_controller = fan_controller
75
+ self.update_interval = update_interval
76
+ self.running = False
77
+
78
+ def run(self):
79
+ """Main thread loop."""
80
+ self.running = True
81
+ while self.running:
82
+ try:
83
+ status = self.fan_controller.get_status()
84
+ if status:
85
+ self.status_updated.emit(status)
86
+ time.sleep(self.update_interval)
87
+ except Exception as e:
88
+ logger.error(f"Fan control thread error: {e}")
89
+ time.sleep(1)
90
+
91
+ def stop(self):
92
+ """Stop the thread."""
93
+ self.running = False
94
+ self.wait()
95
+
96
+
97
+ class GPUOverlayWindow(QWidget):
98
+ """Floating overlay window for GPU monitoring."""
99
+
100
+ def __init__(self, config: Dict):
101
+ super().__init__()
102
+ self.config = config
103
+ self.gpu_manager = GPUManager()
104
+ self.fan_controller = FanController()
105
+
106
+ self.init_ui()
107
+ self.init_data_collection()
108
+
109
+ # Window properties
110
+ self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint | Qt.Tool)
111
+ self.setAttribute(Qt.WA_TranslucentBackground)
112
+ self.setFixedSize(200, 160)
113
+
114
+ # Drag functionality
115
+ self.old_pos = None
116
+
117
+ # Start monitoring
118
+ self.gpu_manager.initialize()
119
+ self.fan_controller.initialize()
120
+
121
+ def init_ui(self):
122
+ """Initialize the overlay UI."""
123
+ # Main layout
124
+ layout = QVBoxLayout()
125
+ layout.setContentsMargins(10, 10, 10, 10)
126
+ layout.setSpacing(5)
127
+
128
+ # Title
129
+ self.lbl_title = QLabel("GPU Monitor")
130
+ self.lbl_title.setStyleSheet("""
131
+ QLabel {
132
+ color: #3498db;
133
+ font-size: 14px;
134
+ font-weight: bold;
135
+ border-bottom: 1px solid #3498db;
136
+ padding-bottom: 5px;
137
+ }
138
+ """)
139
+ layout.addWidget(self.lbl_title)
140
+
141
+ # GPU Info
142
+ self.lbl_gpu = QLabel("GPU: --")
143
+ self.lbl_gpu.setStyleSheet("color: #ecf0f1; font-size: 12px;")
144
+ layout.addWidget(self.lbl_gpu)
145
+
146
+ # Temperature
147
+ temp_layout = QHBoxLayout()
148
+ self.lbl_temp_label = QLabel("Temp:")
149
+ self.lbl_temp_label.setStyleSheet("color: #ecf0f1; font-size: 12px;")
150
+ self.lbl_temp_value = QLabel("--°C")
151
+ self.lbl_temp_value.setStyleSheet("color: #e74c3c; font-size: 12px; font-weight: bold;")
152
+ temp_layout.addWidget(self.lbl_temp_label)
153
+ temp_layout.addStretch()
154
+ temp_layout.addWidget(self.lbl_temp_value)
155
+ layout.addLayout(temp_layout)
156
+
157
+ # Load
158
+ load_layout = QHBoxLayout()
159
+ self.lbl_load_label = QLabel("Load:")
160
+ self.lbl_load_label.setStyleSheet("color: #ecf0f1; font-size: 12px;")
161
+ self.lbl_load_value = QLabel("--%")
162
+ self.lbl_load_value.setStyleSheet("color: #f1c40f; font-size: 12px; font-weight: bold;")
163
+ load_layout.addWidget(self.lbl_load_label)
164
+ load_layout.addStretch()
165
+ load_layout.addWidget(self.lbl_load_value)
166
+ layout.addLayout(load_layout)
167
+
168
+ # Fan
169
+ fan_layout = QHBoxLayout()
170
+ self.lbl_fan_label = QLabel("Fan:")
171
+ self.lbl_fan_label.setStyleSheet("color: #ecf0f1; font-size: 12px;")
172
+ self.lbl_fan_value = QLabel("-- RPM")
173
+ self.lbl_fan_value.setStyleSheet("color: #2ecc71; font-size: 12px; font-weight: bold;")
174
+ fan_layout.addWidget(self.lbl_fan_label)
175
+ fan_layout.addStretch()
176
+ fan_layout.addWidget(self.lbl_fan_value)
177
+ layout.addLayout(fan_layout)
178
+
179
+ # Power
180
+ power_layout = QHBoxLayout()
181
+ self.lbl_power_label = QLabel("Power:")
182
+ self.lbl_power_label.setStyleSheet("color: #ecf0f1; font-size: 12px;")
183
+ self.lbl_power_value = QLabel("-- W")
184
+ self.lbl_power_value.setStyleSheet("color: #9b59b6; font-size: 12px; font-weight: bold;")
185
+ power_layout.addWidget(self.lbl_power_label)
186
+ power_layout.addStretch()
187
+ power_layout.addWidget(self.lbl_power_value)
188
+ layout.addLayout(power_layout)
189
+
190
+ # Memory
191
+ mem_layout = QHBoxLayout()
192
+ self.lbl_mem_label = QLabel("VRAM:")
193
+ self.lbl_mem_label.setStyleSheet("color: #ecf0f1; font-size: 12px;")
194
+ self.lbl_mem_value = QLabel("-- MB")
195
+ self.lbl_mem_value.setStyleSheet("color: #1abc9c; font-size: 12px; font-weight: bold;")
196
+ mem_layout.addWidget(self.lbl_mem_label)
197
+ mem_layout.addStretch()
198
+ mem_layout.addWidget(self.lbl_mem_value)
199
+ layout.addLayout(mem_layout)
200
+
201
+ # Close button
202
+ self.btn_close = QPushButton("×")
203
+ self.btn_close.setFixedSize(20, 20)
204
+ self.btn_close.setStyleSheet("""
205
+ QPushButton {
206
+ background-color: transparent;
207
+ color: #e74c3c;
208
+ font-size: 18px;
209
+ font-weight: bold;
210
+ border: none;
211
+ margin: 0;
212
+ padding: 0;
213
+ }
214
+ QPushButton:hover {
215
+ color: #ff0000;
216
+ }
217
+ """)
218
+ self.btn_close.clicked.connect(self.close)
219
+
220
+ # Add close button to layout
221
+ close_layout = QHBoxLayout()
222
+ close_layout.addStretch()
223
+ close_layout.addWidget(self.btn_close)
224
+ layout.addLayout(close_layout)
225
+
226
+ self.setLayout(layout)
227
+
228
+ # Apply styling
229
+ self.setStyleSheet("""
230
+ QWidget {
231
+ background-color: rgba(0, 0, 0, 200);
232
+ border-radius: 8px;
233
+ border: 1px solid #3498db;
234
+ }
235
+ """)
236
+
237
+ def init_data_collection(self):
238
+ """Initialize data collection threads."""
239
+ # GPU data thread
240
+ self.gpu_thread = GPUDataThread(self.gpu_manager, self.config['update_interval'])
241
+ self.gpu_thread.data_updated.connect(self.update_gpu_data)
242
+ self.gpu_thread.error_occurred.connect(self.handle_error)
243
+ self.gpu_thread.start()
244
+
245
+ # Fan control thread
246
+ self.fan_thread = FanControlThread(self.fan_controller, 2.0)
247
+ self.fan_thread.status_updated.connect(self.update_fan_data)
248
+ self.fan_thread.start()
249
+
250
+ def update_gpu_data(self, status_dict: Dict[str, Optional[GPUStatus]]):
251
+ """Update GPU data display."""
252
+ for gpu_name, gpu_status in status_dict.items():
253
+ if gpu_status:
254
+ # Update GPU name
255
+ self.lbl_gpu.setText(f"GPU: {gpu_name}")
256
+
257
+ # Update temperature
258
+ temp_color = self.get_temp_color(gpu_status.temperature)
259
+ self.lbl_temp_value.setText(f"{gpu_status.temperature:.1f}°C")
260
+ self.lbl_temp_value.setStyleSheet(f"color: {temp_color}; font-size: 12px; font-weight: bold;")
261
+
262
+ # Update load
263
+ load_color = self.get_load_color(gpu_status.load)
264
+ self.lbl_load_value.setText(f"{gpu_status.load:.1f}%")
265
+ self.lbl_load_value.setStyleSheet(f"color: {load_color}; font-size: 12px; font-weight: bold;")
266
+
267
+ # Update fan
268
+ fan_rpm = self.calculate_fan_rpm(gpu_status.fan_pwm)
269
+ self.lbl_fan_value.setText(f"{fan_rpm} RPM ({gpu_status.fan_pwm}%)")
270
+
271
+ # Update power
272
+ self.lbl_power_value.setText(f"{gpu_status.power_draw:.1f} W")
273
+
274
+ # Update memory
275
+ self.lbl_mem_value.setText(f"{gpu_status.memory_used}/{gpu_status.memory_total} MB")
276
+
277
+ break # Only show first GPU
278
+
279
+ def update_fan_data(self, fan_status):
280
+ """Update fan control data."""
281
+ # Fan status updates can be handled here if needed
282
+ pass
283
+
284
+ def handle_error(self, error_msg: str):
285
+ """Handle data collection errors."""
286
+ logger.error(f"Data collection error: {error_msg}")
287
+ # Could show error notification here
288
+
289
+ def get_temp_color(self, temperature: float) -> str:
290
+ """Get color based on temperature."""
291
+ if temperature < 60:
292
+ return "#2ecc71" # Green
293
+ elif temperature < 75:
294
+ return "#f1c40f" # Yellow
295
+ else:
296
+ return "#e74c3c" # Red
297
+
298
+ def get_load_color(self, load: float) -> str:
299
+ """Get color based on load."""
300
+ if load < 50:
301
+ return "#2ecc71" # Green
302
+ elif load < 80:
303
+ return "#f1c40f" # Yellow
304
+ else:
305
+ return "#e74c3c" # Red
306
+
307
+ def calculate_fan_rpm(self, pwm: int) -> int:
308
+ """Calculate approximate RPM from PWM."""
309
+ return int((pwm / 255) * 4000)
310
+
311
+ def mousePressEvent(self, event):
312
+ """Handle mouse press for dragging."""
313
+ if event.button() == Qt.LeftButton:
314
+ self.old_pos = event.globalPos()
315
+
316
+ def mouseMoveEvent(self, event):
317
+ """Handle mouse move for dragging."""
318
+ if self.old_pos is not None:
319
+ delta = QPoint(event.globalPos() - self.old_pos)
320
+ self.move(self.x() + delta.x(), self.y() + delta.y())
321
+ self.old_pos = event.globalPos()
322
+
323
+ def mouseReleaseEvent(self, event):
324
+ """Handle mouse release."""
325
+ self.old_pos = None
326
+
327
+ def closeEvent(self, event):
328
+ """Clean up threads on close."""
329
+ self.gpu_thread.stop()
330
+ self.fan_thread.stop()
331
+ super().closeEvent(event)
332
+
333
+
334
+ class SystemTrayMonitor(QSystemTrayIcon):
335
+ """System tray icon for GPU monitoring."""
336
+
337
+ def __init__(self, config: Dict):
338
+ super().__init__()
339
+
340
+ self.config = config
341
+ self.gpu_manager = GPUManager()
342
+ self.fan_controller = FanController()
343
+
344
+ self.init_ui()
345
+ self.init_data_collection()
346
+
347
+ # Start monitoring
348
+ self.gpu_manager.initialize()
349
+ self.fan_controller.initialize()
350
+
351
+ def init_ui(self):
352
+ """Initialize system tray UI."""
353
+ # Create icon
354
+ self.setIcon(QIcon.fromTheme("video-display"))
355
+
356
+ # Create menu
357
+ menu = QMenu()
358
+
359
+ # Status actions
360
+ self.action_status = QAction("Status: --", self)
361
+ self.action_status.setEnabled(False)
362
+ menu.addAction(self.action_status)
363
+
364
+ menu.addSeparator()
365
+
366
+ # Profile actions
367
+ self.profile_group = QAction("Profiles", self)
368
+ menu.addAction(self.profile_group)
369
+
370
+ self.action_silent = QAction("Silent", self)
371
+ self.action_silent.triggered.connect(lambda: self.set_profile("silent"))
372
+ menu.addAction(self.action_silent)
373
+
374
+ self.action_balanced = QAction("Balanced", self)
375
+ self.action_balanced.triggered.connect(lambda: self.set_profile("balanced"))
376
+ menu.addAction(self.action_balanced)
377
+
378
+ self.action_performance = QAction("Performance", self)
379
+ self.action_performance.triggered.connect(lambda: self.set_profile("performance"))
380
+ menu.addAction(self.action_performance)
381
+
382
+ menu.addSeparator()
383
+
384
+ # Open dashboard
385
+ self.action_dashboard = QAction("Open Dashboard", self)
386
+ self.action_dashboard.triggered.connect(self.open_dashboard)
387
+ menu.addAction(self.action_dashboard)
388
+
389
+ menu.addSeparator()
390
+
391
+ # Exit action
392
+ self.action_exit = QAction("Exit", self)
393
+ self.action_exit.triggered.connect(QApplication.instance().quit)
394
+ menu.addAction(self.action_exit)
395
+
396
+ self.setContextMenu(menu)
397
+ self.activated.connect(self.on_tray_activated)
398
+
399
+ self.show()
400
+
401
+ def init_data_collection(self):
402
+ """Initialize data collection threads."""
403
+ # GPU data thread
404
+ self.gpu_thread = GPUDataThread(self.gpu_manager, self.config['update_interval'])
405
+ self.gpu_thread.data_updated.connect(self.update_tray_tooltip)
406
+ self.gpu_thread.start()
407
+
408
+ def update_tray_tooltip(self, status_dict: Dict[str, Optional[GPUStatus]]):
409
+ """Update system tray tooltip."""
410
+ tooltip = "GPU Monitor\n"
411
+
412
+ for gpu_name, gpu_status in status_dict.items():
413
+ if gpu_status:
414
+ tooltip += f"\n{gpu_name}:"
415
+ tooltip += f"\n Temp: {gpu_status.temperature:.1f}°C"
416
+ tooltip += f"\n Load: {gpu_status.load:.1f}%"
417
+ tooltip += f"\n Fan: {self.calculate_fan_rpm(gpu_status.fan_pwm)} RPM"
418
+ tooltip += f"\n Power: {gpu_status.power_draw:.1f} W"
419
+ tooltip += f"\n VRAM: {gpu_status.memory_used}/{gpu_status.memory_total} MB"
420
+ break
421
+
422
+ self.setToolTip(tooltip)
423
+
424
+ # Update menu status
425
+ if gpu_status:
426
+ self.action_status.setText(f"Status: {gpu_status.temperature:.1f}°C, {gpu_status.load:.1f}%")
427
+
428
+ def on_tray_activated(self, reason):
429
+ """Handle tray icon activation."""
430
+ if reason == QSystemTrayIcon.Trigger:
431
+ # Show/hide dashboard
432
+ pass
433
+
434
+ def set_profile(self, profile_name: str):
435
+ """Set fan profile."""
436
+ if self.fan_controller.set_profile(profile_name):
437
+ QMessageBox.information(None, "Profile Changed", f"Switched to {profile_name} profile")
438
+
439
+ def open_dashboard(self):
440
+ """Open the main dashboard window."""
441
+ # This would need to be implemented to show the main window
442
+ pass
443
+
444
+ def calculate_fan_rpm(self, pwm: int) -> int:
445
+ """Calculate approximate RPM from PWM."""
446
+ return int((pwm / 255) * 4000)
447
+
448
+ def closeEvent(self, event):
449
+ """Clean up threads on close."""
450
+ self.gpu_thread.stop()
451
+ super().closeEvent(event)
452
+
453
+
454
+ class DashboardWindow(QMainWindow):
455
+ """Full-featured dashboard window."""
456
+
457
+ def __init__(self, config: Dict):
458
+ super().__init__()
459
+
460
+ self.config = config
461
+ self.gpu_manager = GPUManager()
462
+ self.fan_controller = FanController()
463
+
464
+ self.init_ui()
465
+ self.init_data_collection()
466
+
467
+ # Start monitoring
468
+ self.gpu_manager.initialize()
469
+ self.fan_controller.initialize()
470
+
471
+ self.setWindowTitle("GPU Monitoring Dashboard")
472
+ self.setGeometry(100, 100, 800, 600)
473
+
474
+ def init_ui(self):
475
+ """Initialize dashboard UI."""
476
+ # Main widget and layout
477
+ main_widget = QWidget()
478
+ main_layout = QVBoxLayout()
479
+
480
+ # Tab widget
481
+ self.tabs = QTabWidget()
482
+
483
+ # Overview tab
484
+ self.overview_tab = self.create_overview_tab()
485
+ self.tabs.addTab(self.overview_tab, "Overview")
486
+
487
+ # Charts tab
488
+ self.charts_tab = self.create_charts_tab()
489
+ self.tabs.addTab(self.charts_tab, "Charts")
490
+
491
+ # Fan Control tab
492
+ self.fan_tab = self.create_fan_tab()
493
+ self.tabs.addTab(self.fan_tab, "Fan Control")
494
+
495
+ # Settings tab
496
+ self.settings_tab = self.create_settings_tab()
497
+ self.tabs.addTab(self.settings_tab, "Settings")
498
+
499
+ main_layout.addWidget(self.tabs)
500
+ main_widget.setLayout(main_layout)
501
+ self.setCentralWidget(main_widget)
502
+
503
+ def create_overview_tab(self):
504
+ """Create the overview tab."""
505
+ widget = QWidget()
506
+ layout = QGridLayout()
507
+
508
+ # GPU Information Group
509
+ gpu_group = QGroupBox("GPU Information")
510
+ gpu_layout = QGridLayout()
511
+
512
+ self.lbl_gpu_name = QLabel("--")
513
+ self.lbl_gpu_name.setStyleSheet("font-weight: bold; font-size: 16px;")
514
+ gpu_layout.addWidget(QLabel("GPU:"), 0, 0)
515
+ gpu_layout.addWidget(self.lbl_gpu_name, 0, 1)
516
+
517
+ self.lbl_temp = QLabel("-- °C")
518
+ self.lbl_temp.setStyleSheet("font-size: 14px; color: #e74c3c;")
519
+ gpu_layout.addWidget(QLabel("Temperature:"), 1, 0)
520
+ gpu_layout.addWidget(self.lbl_temp, 1, 1)
521
+
522
+ self.lbl_load = QLabel("-- %")
523
+ self.lbl_load.setStyleSheet("font-size: 14px; color: #f1c40f;")
524
+ gpu_layout.addWidget(QLabel("Load:"), 2, 0)
525
+ gpu_layout.addWidget(self.lbl_load, 2, 1)
526
+
527
+ self.lbl_power = QLabel("-- W")
528
+ self.lbl_power.setStyleSheet("font-size: 14px; color: #9b59b6;")
529
+ gpu_layout.addWidget(QLabel("Power:"), 3, 0)
530
+ gpu_layout.addWidget(self.lbl_power, 3, 1)
531
+
532
+ self.lbl_vram = QLabel("-- MB")
533
+ self.lbl_vram.setStyleSheet("font-size: 14px; color: #1abc9c;")
534
+ gpu_layout.addWidget(QLabel("VRAM:"), 4, 0)
535
+ gpu_layout.addWidget(self.lbl_vram, 4, 1)
536
+
537
+ gpu_group.setLayout(gpu_layout)
538
+ layout.addWidget(gpu_group, 0, 0)
539
+
540
+ # Fan Information Group
541
+ fan_group = QGroupBox("Fan Control")
542
+ fan_layout = QGridLayout()
543
+
544
+ self.lbl_fan_mode = QLabel("--")
545
+ fan_layout.addWidget(QLabel("Mode:"), 0, 0)
546
+ fan_layout.addWidget(self.lbl_fan_mode, 0, 1)
547
+
548
+ self.lbl_fan_profile = QLabel("--")
549
+ fan_layout.addWidget(QLabel("Profile:"), 1, 0)
550
+ fan_layout.addWidget(self.lbl_fan_profile, 1, 1)
551
+
552
+ self.lbl_fan_speed = QLabel("-- RPM")
553
+ fan_layout.addWidget(QLabel("Speed:"), 2, 0)
554
+ fan_layout.addWidget(self.lbl_fan_speed, 2, 1)
555
+
556
+ self.lbl_fan_pwm = QLabel("-- %")
557
+ fan_layout.addWidget(QLabel("PWM:"), 3, 0)
558
+ fan_layout.addWidget(self.lbl_fan_pwm, 3, 1)
559
+
560
+ fan_group.setLayout(fan_layout)
561
+ layout.addWidget(fan_group, 0, 1)
562
+
563
+ widget.setLayout(layout)
564
+ return widget
565
+
566
+ def create_charts_tab(self):
567
+ """Create the charts tab."""
568
+ widget = QWidget()
569
+ layout = QVBoxLayout()
570
+
571
+ # Create matplotlib figure
572
+ self.figure, self.axes = plt.subplots(2, 2, figsize=(10, 8))
573
+ self.canvas = FigureCanvas(self.figure)
574
+
575
+ layout.addWidget(self.canvas)
576
+ widget.setLayout(layout)
577
+ return widget
578
+
579
+ def create_fan_tab(self):
580
+ """Create the fan control tab."""
581
+ widget = QWidget()
582
+ layout = QVBoxLayout()
583
+
584
+ # Profile selection
585
+ profile_layout = QHBoxLayout()
586
+ profile_layout.addWidget(QLabel("Select Profile:"))
587
+ self.combo_profile = QComboBox()
588
+ self.combo_profile.addItems(["Silent", "Balanced", "Performance"])
589
+ self.combo_profile.currentTextChanged.connect(self.on_profile_changed)
590
+ profile_layout.addWidget(self.combo_profile)
591
+
592
+ layout.addLayout(profile_layout)
593
+
594
+ # Manual control
595
+ manual_group = QGroupBox("Manual Control")
596
+ manual_layout = QHBoxLayout()
597
+
598
+ self.spin_manual_pwm = QSpinBox()
599
+ self.spin_manual_pwm.setRange(0, 255)
600
+ self.spin_manual_pwm.setValue(0)
601
+ manual_layout.addWidget(QLabel("Manual PWM:"))
602
+ manual_layout.addWidget(self.spin_manual_pwm)
603
+
604
+ self.btn_set_manual = QPushButton("Set Manual")
605
+ self.btn_set_manual.clicked.connect(self.on_set_manual)
606
+ manual_layout.addWidget(self.btn_set_manual)
607
+
608
+ manual_group.setLayout(manual_layout)
609
+ layout.addWidget(manual_group)
610
+
611
+ widget.setLayout(layout)
612
+ return widget
613
+
614
+ def create_settings_tab(self):
615
+ """Create the settings tab."""
616
+ widget = QWidget()
617
+ layout = QVBoxLayout()
618
+
619
+ # Update interval
620
+ interval_layout = QHBoxLayout()
621
+ interval_layout.addWidget(QLabel("Update Interval (seconds):"))
622
+ self.spin_interval = QDoubleSpinBox()
623
+ self.spin_interval.setRange(0.1, 10.0)
624
+ self.spin_interval.setValue(self.config['update_interval'])
625
+ self.spin_interval.valueChanged.connect(self.on_interval_changed)
626
+ interval_layout.addWidget(self.spin_interval)
627
+ layout.addLayout(interval_layout)
628
+
629
+ # Display options
630
+ display_group = QGroupBox("Display Options")
631
+ display_layout = QVBoxLayout()
632
+
633
+ self.chk_show_temp = QCheckBox("Show Temperature")
634
+ self.chk_show_temp.setChecked(self.config['show_temperature'])
635
+ display_layout.addWidget(self.chk_show_temp)
636
+
637
+ self.chk_show_load = QCheckBox("Show Load")
638
+ self.chk_show_load.setChecked(self.config['show_gpu_load'])
639
+ display_layout.addWidget(self.chk_show_load)
640
+
641
+ self.chk_show_fan = QCheckBox("Show Fan Speed")
642
+ self.chk_show_fan.setChecked(self.config['show_fan_speed'])
643
+ display_layout.addWidget(self.chk_show_fan)
644
+
645
+ display_group.setLayout(display_layout)
646
+ layout.addWidget(display_group)
647
+
648
+ widget.setLayout(layout)
649
+ return widget
650
+
651
+ def init_data_collection(self):
652
+ """Initialize data collection threads."""
653
+ # GPU data thread
654
+ self.gpu_thread = GPUDataThread(self.gpu_manager, self.config['update_interval'])
655
+ self.gpu_thread.data_updated.connect(self.update_dashboard)
656
+ self.gpu_thread.start()
657
+
658
+ # Fan control thread
659
+ self.fan_thread = FanControlThread(self.fan_controller, 2.0)
660
+ self.fan_thread.status_updated.connect(self.update_fan_status)
661
+ self.fan_thread.start()
662
+
663
+ def update_dashboard(self, status_dict: Dict[str, Optional[GPUStatus]]):
664
+ """Update dashboard display."""
665
+ for gpu_name, gpu_status in status_dict.items():
666
+ if gpu_status:
667
+ # Update GPU info
668
+ self.lbl_gpu_name.setText(gpu_name)
669
+ self.lbl_temp.setText(f"{gpu_status.temperature:.1f} °C")
670
+ self.lbl_load.setText(f"{gpu_status.load:.1f} %")
671
+ self.lbl_power.setText(f"{gpu_status.power_draw:.1f} W")
672
+ self.lbl_vram.setText(f"{gpu_status.memory_used}/{gpu_status.memory_total} MB")
673
+ break
674
+
675
+ def update_fan_status(self, fan_status):
676
+ """Update fan status display."""
677
+ if fan_status:
678
+ self.lbl_fan_mode.setText(fan_status.mode.value)
679
+ self.lbl_fan_profile.setText(fan_status.profile)
680
+ self.lbl_fan_speed.setText(f"{self.calculate_fan_rpm(fan_status.current_pwm)} RPM")
681
+ self.lbl_fan_pwm.setText(f"{fan_status.current_pwm}%")
682
+
683
+ def on_profile_changed(self, profile_name: str):
684
+ """Handle profile change."""
685
+ self.fan_controller.set_profile(profile_name.lower())
686
+
687
+ def on_set_manual(self):
688
+ """Handle manual PWM setting."""
689
+ pwm = self.spin_manual_pwm.value()
690
+ self.fan_controller.set_manual_pwm(pwm)
691
+
692
+ def on_interval_changed(self, value: float):
693
+ """Handle update interval change."""
694
+ self.config['update_interval'] = value
695
+ self.gpu_thread.update_interval = value
696
+
697
+ def calculate_fan_rpm(self, pwm: int) -> int:
698
+ """Calculate approximate RPM from PWM."""
699
+ return int((pwm / 255) * 4000)
700
+
701
+ def closeEvent(self, event):
702
+ """Clean up threads on close."""
703
+ self.gpu_thread.stop()
704
+ self.fan_thread.stop()
705
+ super().closeEvent(event)
706
+
707
+
708
+ class GPUApplication:
709
+ """Main application class."""
710
+
711
+ def __init__(self):
712
+ self.config = self.load_config()
713
+ self.app = QApplication(sys.argv)
714
+
715
+ # Set application style
716
+ self.app.setStyle("Fusion")
717
+
718
+ # Create appropriate window based on mode
719
+ if self.config['display_mode'] == 'overlay':
720
+ self.window = GPUOverlayWindow(self.config)
721
+ elif self.config['display_mode'] == 'tray':
722
+ self.window = SystemTrayMonitor(self.config)
723
+ else: # dashboard
724
+ self.window = DashboardWindow(self.config)
725
+
726
+ def load_config(self) -> Dict:
727
+ """Load configuration from file."""
728
+ config_path = Path("config/monitoring.json")
729
+ if config_path.exists():
730
+ with open(config_path, 'r') as f:
731
+ return json.load(f)
732
+ else:
733
+ # Return default config
734
+ return {
735
+ "update_interval": 1.0,
736
+ "display_mode": "overlay",
737
+ "show_gpu_load": True,
738
+ "show_temperature": True,
739
+ "show_fan_speed": True,
740
+ "show_power": True,
741
+ "show_vram": True
742
+ }
743
+
744
+ def run(self):
745
+ """Run the application."""
746
+ if hasattr(self.window, 'show'):
747
+ self.window.show()
748
+
749
+ sys.exit(self.app.exec_())
750
+
751
+
752
+ if __name__ == "__main__":
753
+ app = GPUApplication()
754
+ app.run()