diff --git a/.gitignore b/.gitignore
index 5320293..41df370 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
__pycache__/
*.py[cod]
*$py.class
+*.pyc
# Virtual environment
.venv/
@@ -18,8 +19,10 @@ Thumbs.db
# IDE
.vscode/
.idea/
+*.swp
+*.swo
-# Python build
+# Build
build/
dist/
*.egg-info/
@@ -28,12 +31,18 @@ dist/
temp/
tmp/
-# ComfyUI cache
+# ComfyUI
*.cache
-
-# Output files
output/
input/
# PyInstaller
-*.spec
\ No newline at end of file
+*.spec
+
+# Bundled LibreHardwareMonitor
+monitor/libre_hardware_monitor/
+monitor/*.zip
+monitor/*.exe
+
+# Test files we don't want in repo
+_detect_adlx.py
\ No newline at end of file
diff --git a/README.md b/README.md
index 3153b9c..7561ae1 100644
--- a/README.md
+++ b/README.md
@@ -1,254 +1,334 @@
-# BangtrixToolkit
+
-Advanced Custom Node Toolkit for ComfyUI focused on:
-- AI Prompt Translation
-- AMD ROCm Utilities
-- Workflow Enhancements
-- Performance Utilities
+# ๐ BangtrixToolkit
+
+**Advanced Custom Node Toolkit for ComfyUI**
+
+
+
+
+
+
+> *Translate your prompts & monitor your AMD GPU โ all inside ComfyUI*
---
-
-
-# ๐ Bangtrix Translate Universal
+[๐ Translate](#-bangtrix-translate-universal) โข
+[๐ฅ๏ธ AMD Monitor](#-amd-monitor--real-time-gpu-overlay-) โข
+[๐ฆ Installation](#-installation) โข
+[๐ Roadmap](#-roadmap)
-
+---
+
+
+
+---
+
+
+๐ Bangtrix Translate Universal โ Click to expand
+
+
Bangtrix Translate Universal is an advanced translation node for ComfyUI that automatically translates prompts into English and directly outputs them into CLIP conditioning.
-Designed for:
-- Stable Diffusion
-- Flux
-- SDXL
-- Pony
-- Anime Models
-- Realistic Models
-- Any CLIP-based workflow
+Designed for: **Stable Diffusion ยท Flux ยท SDXL ยท Pony ยท Anime Models ยท Realistic Models ยท Any CLIP-based workflow**
---
-# โจ Features
+### โจ Features
-## ๐ Translation
-- Automatic Google Translate integration
-- Multi-language support
-- Real-time prompt translation
-- Offline-safe fallback
+| Category | Features |
+|----------|----------|
+| ๐ **Translation** | Google Translate integration ยท Multi-language support ยท Offline-safe fallback |
+| ๐ง **Enhancement** | Quality presets ยท Style presets ยท Prompt cleaning ยท Enhancement system |
+| โ **Auto Negative** | Anatomy fixes ยท Blur reduction ยท Watermark filtering ยท Artifact reduction ยท Style-based negatives |
+| โก **Performance** | Translation cache ยท Fast repeated translation ยท Lightweight ยท Universal compatibility |
---
-## ๐ง Prompt Enhancement
-- Quality presets
-- Style presets
-- Prompt cleaning
-- Prompt enhancement system
+### ๐ Supported Languages
+
+
+
+| Code | Language | Code | Language |
+|------|----------|------|----------|
+| `auto` | Auto Detect | `en` | English |
+| `id` | Indonesian | `ja` | Japanese |
+| `ko` | Korean | `zh-CN` | Chinese (Simplified) |
+| `zh-TW` | Chinese (Traditional) | `fr` | French |
+| `de` | German | `es` | Spanish |
+| `pt` | Portuguese | `ru` | Russian |
+| `it` | Italian | `ar` | Arabic |
+| `tr` | Turkish | `hi` | Hindi |
+| `th` | Thai | `vi` | Vietnamese |
+
+
---
-## โ Auto Negative
-Automatically generates optimized negative prompts.
+### ๐จ Presets
-Supports:
-- anatomy fixes
-- blur reduction
-- watermark filtering
-- artifact reduction
-- style-based negatives
+
+
+| Quality | Style | Auto Negative |
+|---------|-------|---------------|
+| `normal` โ Standard | `none` โ No style | `normal` โ Basic |
+| `high` โ Better details | `anime` ยท `realistic` ยท `cinematic` | `strong` โ Reduced artifacts |
+| `ultra` โ Maximum | `photography` ยท `product` ยท `fantasy` ยท `portrait` | `ultra` โ Maximum cleanup |
+
+
---
-## โก Performance
-- Translation cache system
-- Fast repeated translation
-- Lightweight architecture
-- Universal model compatibility
+### โ๏ธ Workflow Example
----
+
+See node inputs & outputs
-# ๐ Supported Languages
+
-| Code | Language |
-|------|------|
-| auto | Auto Detect |
-| id | Indonesian |
-| en | English |
-| ja | Japanese |
-| ko | Korean |
-| zh-CN | Simplified Chinese |
-| zh-TW | Traditional Chinese |
-| fr | French |
-| de | German |
-| es | Spanish |
-| pt | Portuguese |
-| ru | Russian |
-| it | Italian |
-| ar | Arabic |
-| tr | Turkish |
-| hi | Hindi |
-| th | Thai |
-| vi | Vietnamese |
+**Inputs:** `positive_prompt` ยท `negative_prompt` ยท `source_language` ยท `target_language` ยท `quality_preset` ยท `style_preset` ยท `enable_translate` ยท `enable_enhance` ยท `enable_clean` ยท `enable_cache` ยท `enable_auto_negative` ยท `auto_negative_strength` ยท `show_original`
----
-
-# ๐จ Quality Presets
-
-| Preset | Description |
-|------|------|
-| normal | Standard quality enhancement |
-| high | Better details and sharpness |
-| ultra | Maximum quality enhancement |
-
----
-
-# ๐ญ Style Presets
-
-| Style | Description |
-|------|------|
-| none | No style enhancement |
-| anime | Anime style optimization |
-| realistic | Realistic photography enhancement |
-| cinematic | Cinematic lighting and shadows |
-| photography | Professional photography style |
-| product | Product photography optimization |
-| fantasy | Fantasy art enhancement |
-| portrait | Portrait optimization |
-
----
-
-# โ Auto Negative Strength
-
-| Strength | Description |
-|------|------|
-| normal | Basic negative prompt |
-| strong | Improved artifact reduction |
-| ultra | Maximum cleanup and artifact filtering |
-
----
-
----
-
-# โ๏ธ Node Inputs
-
-| Input | Description |
-|------|------|
-| positive_prompt | Positive prompt |
-| negative_prompt | Negative prompt |
-| source_language | Original language |
-| target_language | Translation target language |
-| quality_preset | Quality enhancement |
-| style_preset | Style enhancement |
-| enable_translate | Enable translation |
-| enable_enhance | Enable prompt enhancement |
-| enable_clean | Clean newline formatting |
-| enable_cache | Enable translation cache |
-| enable_auto_negative | Enable automatic negative prompts |
-| auto_negative_strength | Auto negative strength |
-| show_original | Show original + translated text |
-
----
-
-# ๐ค Outputs
-
-| Output | Description |
-|------|------|
-| positive_conditioning | CLIP positive conditioning |
-| negative_conditioning | CLIP negative conditioning |
-| positive_prompt_final | Final translated positive prompt |
-| negative_prompt_final | Final translated negative prompt |
-
----
-
-# ๐ฅ Workflow Example
-
-
-
-
-
----
-
-## Indonesian Input
-
-```text
-wanita cantik memakai kimono di malam hari
-```
-
-## Final English Prompt
-
-```text
-beautiful woman wearing kimono at night, cinematic lighting, high quality, detailed
-```
-
----
-
-# ๐ก Tips
-
-- Use `show_original` to debug translations
-- Enable `enable_cache` for faster workflows
-- Use `ultra` quality for SDXL and Flux
-- Use `anime` preset for anime models
-- Use `realistic` or `photography` for realism models
-
----
+**Outputs:** `positive_conditioning` ยท `negative_conditioning` ยท `positive_prompt_final` ยท `negative_prompt_final`
-# ๐ฆ Installation
+
-## Manual Installation
+
-Go to your ComfyUI custom_nodes folder:
+| Indonesian Input | Final English Prompt |
+|-----------------|---------------------|
+| `wanita cantik memakai kimono di malam hari` | `beautiful woman wearing kimono at night, cinematic lighting, high quality, detailed` |
+
+
+
+---
+
+
+
+
+
+
+
+---
+
+### ๐ก Tips
+
+> ๐น Use `show_original` to debug translations
+> ๐น Enable `enable_cache` for faster workflows
+> ๐น Use `ultra` quality for SDXL and Flux
+> ๐น Use `anime` preset for anime models
+> ๐น Use `realistic` or `photography` for realism models
+
+
+
+---
+
+
+๐ฅ๏ธ AMD Monitor โ Real-Time GPU Overlay โ Click to expand
+
+
+
+> **Zero configuration. Plug & play.** AMD Monitor auto-loads when ComfyUI starts โ no node needed.
+
+---
+
+### โจ Features
+
+
+
+| | Feature | Description |
+|:---:|---------|-------------|
+| ๐ | **GPU Load** | Real-time utilization % with animated bar |
+| ๐พ | **VRAM Usage** | Used / Total in GB + utilization bar |
+| ๐ก๏ธ | **Temperature** | GPU temp in ยฐC/ยฐF *(requires LibreHardwareMonitor)* |
+| ๐จ | **Fan Speed** | Fan RPM/percentage *(requires LibreHardwareMonitor)* |
+| ๐ท๏ธ | **GPU Name** | Auto-detects your GPU model *(e.g. RX 7800 XT)* |
+| ๐ | **Sparkline Chart** | 30-second GPU Load history graph |
+| โก | **Generation Stats** | Duration, RAM Peak, Delta & CPU Peak |
+| ๐ฅ๏ธ | **Multi-GPU** | Chip selector for multi-GPU systems |
+| โ ๏ธ | **VRAM Alert** | Toast + inline banner on threshold breach |
+| ๐ฑ๏ธ | **Draggable** | Click & drag to reposition |
+| โ | **Minimize** | Toggle with `โ` button |
+| โจ๏ธ | **Hide/Show** | `Ctrl + Shift + M` shortcut |
+| โ๏ธ | **Settings Panel** | Interval, Alert %, Temp unit, Sparkline toggle |
+| ๐พ | **Persist Config** | Position & settings saved in localStorage |
+
+
+
+---
+
+### ๐ธ Preview
+
+
+
+
+```
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ ๐ด AMD Monitor โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
+โ AMD Radeon RX 7800 XT โ
+โ โโโโโโโโโฌโโโโโโโโโโโ โ
+โ โGPU โ VRAM โ โ
+โ โ 1.2% โ1.1/4.0 GBโ โ
+โ โ โโโโโโโโโโโโโโโโโ โ
+โ โโโโโโโโโผโโโโโโโโโโโค โ
+โ โ Temp โ Fan โ โ
+โ โ 45ยฐC โ1200 RPM โ โ
+โ โ โโโโโโโ โโโโโโโโโ โ
+โ โโโโโโโโโดโโโโโโโโโโโ โ
+โ โฑ GPU Load History โฒ โ
+โ โโโโโโโโโโโโโโโโโโโโโโ โ
+โ ๐ข RX 7800 XT โ [ps] โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+---
+
+### โ๏ธ Architecture
+
+```
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ ComfyUI Server โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ on_app_started() โ โ
+โ โ โโโ WS Route โ /ws/amd_monitor โ โ
+โ โ โโโ Stream loop โ every 1s โ โ
+โ โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ WebSocket โ
+โ โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ Backend Auto-Detect โ โ
+โ โ โ โ
+โ โ 1. LibreHardwareMonitor โ WMI โ โ
+โ โ โ Temp, Fan, Clock, Power โ โ
+โ โ โ โ
+โ โ 2. PowerShell Counters โ
ACTIVE โ โ
+โ โ โ GPU Load, VRAM, GPU Name โ โ
+โ โ โ โ
+โ โ 3. ADL (atiadlxx.dll) โ โ
+โ โ โ Temp, Fan (partial) โ โ
+โ โ โ โ
+โ โ 4. psutil (system RAM) โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ Comfy Process Monitor โ โ
+โ โ Auto-detect generations โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ โ JSON every 1s
+โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ Browser (ComfyUI) โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โ โ Floating Overlay (amd_monitor.js) โ โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+---
+
+### ๐๏ธ Backend Priority
+
+
+
+| Priority | Backend | Metrics | Platform |
+|:--------:|---------|---------|----------|
+| ๐ฅ | **LibreHardwareMonitor** | Temp, Fan, Clock, Power | Windows |
+| ๐ฅ | **PowerShell Counters** | Load, VRAM, Name | Windows |
+| ๐ฅ | **ADL** (atiadlxx.dll) | Temp, Fan | Windows |
+| 4 | **psutil** | System RAM only | All |
+
+
+
+---
+
+### ๐ก๏ธ LibreHardwareMonitor Setup
+
+To enable **Temperature, Fan Speed, Clock speeds & Power Draw**:
+
+1. **Auto-download** โ LHM portable (8.5MB) is downloaded automatically on first ComfyUI start
+ ```
+ ๐ BangtrixToolkit/monitor/libre_hardware_monitor/LibreHardwareMonitor.exe
+ ```
+
+2. **Run as Administrator** (one-time):
+ ```powershell
+ .\monitor\libre_hardware_monitor\LibreHardwareMonitor.exe --wmi
+ ```
+
+3. **Restart ComfyUI** โ Backend auto-switches to LHM ๐ฏ
+
+> โ ๏ธ **Without LHM**, core metrics still work via PowerShell Counters: GPU Load, VRAM, GPU Name.
+
+---
+
+### โจ๏ธ Quick Reference
+
+| Action | Keyboard / Button |
+|--------|------------------|
+| Toggle overlay | `Ctrl + Shift + M` |
+| Minimize | `โ` button |
+| Settings | `โ` button |
+
+---
+
+### ๐ Requirements
+
+| Package | Needed | Purpose |
+|---------|:------:|---------|
+| `aiohttp` | โ
**Yes** | WebSocket server |
+| `psutil` | โ
Recommended | Process Monitor |
+| `requests` | โ Optional | Translate node fallback |
+
+
+
+---
+
+## ๐ฆ Installation
+
+
+See installation guide
+
+
+
+### ๐ง Manual
```bash
cd "ComfyUI/custom_nodes"
-```
-
-Clone repository:
-
-```bash
git clone https://github.com/Anonymzx/BangtrixToolkit.git
-```
-
-Install dependencies:
-
-```bash
pip install -r requirements.txt
```
-Restart ComfyUI.
+> **Restart ComfyUI** after installation.
+
+### ๐ฅ ComfyUI Manager
+
+1. Open **ComfyUI Manager**
+2. Go to **Custom Nodes Manager**
+3. Search for **BangtrixToolkit**
+4. Click **Install**
+5. **Restart** ComfyUI
+
+
---
-# ๐งฉ Installation via ComfyUI Manager
+## ๐ Roadmap
-1. Open ComfyUI
-2. Open ComfyUI Manager
-3. Open:
- - Custom Nodes Manager
-4. Search:
- - BangtrixToolkit
-5. Click:
- - Install
-6. Restart ComfyUI
+
+
+| Version | Features |
+|:-------:|----------|
+| **v1** โ
| Bangtrix Translate Universal |
+| **v2** โ
| AMD Monitor ยท ROCm Utilities |
+| **v3** ๐ | Smart Cache ยท Performance Optimizer ยท Benchmark Tools |
+
+
---
-# ๐ Roadmap
+
-## v1
-- Bangtrix Translate Universal
+### โค๏ธ Created by [Anonymzx](https://github.com/Anonymzx)
-## v2
-- AMD Monitor
-- ROCm Utilities
-
-## v3
-- Smart Cache
-- Performance Optimizer
-- Benchmark Tools
-
----
-
-# โค๏ธ Credits
-
-Created by:
-- Anonymzx
\ No newline at end of file
+
\ No newline at end of file
diff --git a/__init__.py b/__init__.py
index d5fadce..52f63aa 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,24 +1,26 @@
"""
BANGTRIXTOOLKIT - ComfyUI Custom Nodes
======================================
-AMD Monitor & Translate Universal
+Translate Universal + AMD Monitor Overlay
"""
-from .nodes.translate_universal import (
- NODE_CLASS_MAPPINGS,
- NODE_DISPLAY_NAME_MAPPINGS
-)
+import importlib
-from .nodes.amd_monitor import (
- NODE_CLASS_MAPPINGS as AMD_NODE_CLASS_MAPPINGS,
- NODE_DISPLAY_NAME_MAPPINGS as AMD_NODE_DISPLAY_NAME_MAPPINGS
-)
+# Dynamic import โ hanya load node yang benar-benar ada
+NODE_CLASS_MAPPINGS = {}
+NODE_DISPLAY_NAME_MAPPINGS = {}
-# Gabungkan semua node mappings
-NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **AMD_NODE_CLASS_MAPPINGS}
-NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **AMD_NODE_DISPLAY_NAME_MAPPINGS}
+try:
+ _mod = importlib.import_module(".nodes.translate_universal", package=__package__)
+ if hasattr(_mod, "NODE_CLASS_MAPPINGS"):
+ NODE_CLASS_MAPPINGS.update(_mod.NODE_CLASS_MAPPINGS)
+ if hasattr(_mod, "NODE_DISPLAY_NAME_MAPPINGS"):
+ NODE_DISPLAY_NAME_MAPPINGS.update(_mod.NODE_DISPLAY_NAME_MAPPINGS)
+ print("โ
BANGTRIXTOOLKIT: Loaded translate_universal")
+except Exception as e:
+ print(f"โ ๏ธ BANGTRIXTOOLKIT: Skipped translate_universal: {e}")
-WEB_DIRECTORY = "./web"
+WEB_DIRECTORY = "web"
__all__ = [
"NODE_CLASS_MAPPINGS",
@@ -28,45 +30,39 @@ __all__ = [
# ============================================================
-# ๐ SERVER EXTENSION - WebSocket untuk Real-Time Monitoring
+# ๐ SERVER EXTENSION โ AMD Monitor Overlay (WebSocket)
# ============================================================
async def on_app_started(app):
- """Register WebSocket endpoint for AMD Monitor (FIXED: 1 argument only)"""
+ """Register WebSocket endpoint for AMD Monitor overlay"""
import asyncio
from aiohttp import web
from .server.amd_server import get_amd_server
-
+
amd_server = get_amd_server()
-
+
async def websocket_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
await amd_server.handle_client(ws)
return ws
-
- # Register route (safe: won't error if already registered)
+
try:
app.router.add_get('/ws/amd_monitor', websocket_handler)
print("๐ด BANGTRIXTOOLKIT: AMD Monitor WebSocket at /ws/amd_monitor")
except RuntimeError:
- pass # Route already exists
-
- # Start monitoring in background
+ pass # Route already registered
+
if not amd_server.running:
- amd_server.running = True
- asyncio.create_task(amd_server.stream_data())
- print("โ
BANGTRIXTOOLKIT: AMD Monitor streaming started")
+ amd_server.start_streaming(asyncio.get_event_loop())
+ print("โ
BANGTRIXTOOLKIT: AMD Monitor overlay streaming started")
-# Register with ComfyUI (FIXED: lambda to match signature)
+# Register with ComfyUI
try:
from server import PromptServer
prompt_server = PromptServer.instance
-
- # Append our async handler (ComfyUI only passes 'app')
prompt_server.app.on_startup.append(on_app_started)
- print("โ
BANGTRIXTOOLKIT: Server extension registered")
-
+ print("โ
BANGTRIXTOOLKIT: Server extension registered (AMD Monitor overlay)")
except Exception as e:
print(f"โ ๏ธ BANGTRIXTOOLKIT: Server extension skipped: {e}")
\ No newline at end of file
diff --git a/monitor/__init__.py b/monitor/__init__.py
new file mode 100644
index 0000000..e4ca51b
--- /dev/null
+++ b/monitor/__init__.py
@@ -0,0 +1,23 @@
+"""
+BangtrixToolkit โ Hardware Monitoring Package
+===============================================
+Auto-detects best GPU monitoring backend.
+
+Usage:
+ from monitor import get_amd_monitor
+ monitor = get_amd_monitor()
+ stats = monitor.get_gpu_stats(0)
+ print(stats.gpu_name, stats.temperature)
+"""
+
+from .amd_monitor import get_amd_monitor, AMDMonitor, AMDGPUStats
+from .process_monitor import get_process_monitor, ComfyProcessMonitor, GenerationRecord
+
+__all__ = [
+ "get_amd_monitor",
+ "AMDMonitor",
+ "AMDGPUStats",
+ "get_process_monitor",
+ "ComfyProcessMonitor",
+ "GenerationRecord",
+]
\ No newline at end of file
diff --git a/monitor/amd_monitor.py b/monitor/amd_monitor.py
new file mode 100644
index 0000000..3715c9e
--- /dev/null
+++ b/monitor/amd_monitor.py
@@ -0,0 +1,115 @@
+"""
+AMD Monitor - Unified GPU Monitoring
+=====================================
+Auto-detects best available backend:
+1. LibreHardwareMonitor (Temperature, Fan, Load, Clocks, Power)
+2. ADL (AMD Display Library)
+3. PowerShell Counters (Utilization, VRAM, GPU name)
+4. psutil (System RAM fallback)
+
+Usage:
+ from monitor import get_amd_monitor
+ monitor = get_amd_monitor()
+ stats = monitor.get_gpu_stats(0)
+ print(stats.gpu_name, stats.utilization_gpu, stats.temperature)
+"""
+
+import logging
+from dataclasses import dataclass
+from typing import Optional, List
+
+from .backends import (
+ MonitorBackend, AMDGPUStats,
+ PowerShellBackend,
+ ADLBackend,
+ LibreHardwareBackend,
+ PsutilBackend,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class AMDMonitor:
+ """Unified monitor that auto-selects best available backend"""
+
+ def __init__(self):
+ self.available = False
+ self.method = None
+ self.gpu_count = 0
+ self.gpu_names: List[str] = []
+ self._backend: Optional[MonitorBackend] = None
+ self._selected_backend_name: Optional[str] = None
+ self._initialize()
+
+ def _initialize(self):
+ """Try backends in priority order"""
+ backends = [
+ ("libre-hardware-monitor", LibreHardwareBackend),
+ ("powershell-counters", PowerShellBackend),
+ ("adl", ADLBackend),
+ ("psutil-fallback", PsutilBackend),
+ ]
+
+ for name, cls in backends:
+ try:
+ backend = cls()
+ if backend.initialize():
+ self._backend = backend
+ self._selected_backend_name = name
+ self.available = True
+ self.gpu_count = backend.gpu_count
+ self.gpu_names = backend.gpu_names
+ self.method = name
+ logger.info(f"AMD Monitor: using {name} โ {self.gpu_count} GPU(s)")
+ return
+ except Exception as e:
+ logger.debug(f"AMD Monitor: {name} failed: {e}")
+
+ logger.error("AMD Monitor: no backend available")
+
+ def get_gpu_stats(self, gpu_id: int = 0) -> AMDGPUStats:
+ """Get stats for a specific GPU"""
+ if not self._backend or not self.available:
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ is_available=False,
+ error_message="No backend available"
+ )
+ try:
+ stats = self._backend.get_stats(gpu_id)
+ stats.gpu_name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else stats.gpu_name
+ return stats
+ except Exception as e:
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ is_available=False,
+ error_message=str(e)
+ )
+
+ def get_all_gpu_stats(self) -> List[AMDGPUStats]:
+ """Get stats for all detected GPUs"""
+ if not self._backend or self.gpu_count == 0:
+ return []
+ return [self.get_gpu_stats(i) for i in range(self.gpu_count)]
+
+ def close(self):
+ """Cleanup backend resources"""
+ if self._backend:
+ self._backend.close()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args):
+ self.close()
+
+
+# Singleton
+_amd_monitor = None
+
+
+def get_amd_monitor() -> AMDMonitor:
+ global _amd_monitor
+ if _amd_monitor is None:
+ _amd_monitor = AMDMonitor()
+ return _amd_monitor
\ No newline at end of file
diff --git a/monitor/backends/__init__.py b/monitor/backends/__init__.py
new file mode 100644
index 0000000..53b9315
--- /dev/null
+++ b/monitor/backends/__init__.py
@@ -0,0 +1,19 @@
+"""
+Backend monitoring modules for AMD GPU stats.
+Auto-detects best available backend.
+"""
+
+from .base import MonitorBackend, AMDGPUStats
+from .powershell_backend import PowerShellBackend
+from .adl_backend import ADLBackend
+from .libre_hardware import LibreHardwareBackend
+from .psutil_backend import PsutilBackend
+
+__all__ = [
+ "MonitorBackend",
+ "AMDGPUStats",
+ "PowerShellBackend",
+ "ADLBackend",
+ "LibreHardwareBackend",
+ "PsutilBackend",
+]
\ No newline at end of file
diff --git a/monitor/backends/adl_backend.py b/monitor/backends/adl_backend.py
new file mode 100644
index 0000000..ad65a77
--- /dev/null
+++ b/monitor/backends/adl_backend.py
@@ -0,0 +1,113 @@
+"""
+ADL (AMD Display Library) Backend
+==================================
+Direct ctypes binding to atiadlxx.dll on Windows.
+"""
+
+import ctypes
+import logging
+import platform
+from ctypes import byref, c_int, c_void_p, c_char, POINTER, Structure
+
+from .base import MonitorBackend, AMDGPUStats
+
+logger = logging.getLogger(__name__)
+
+
+class ADLTemperature(Structure):
+ _fields_ = [("iSize", c_int), ("iTemperature", c_int)]
+
+
+class ADLFanSpeedValue(Structure):
+ _fields_ = [("iSize", c_int), ("iSpeedType", c_int), ("iFanSpeed", c_int), ("iFlags", c_int)]
+
+
+class ADLPMActivity(Structure):
+ _fields_ = [
+ ("iSize", c_int), ("iEngineClock", c_int), ("iMemoryClock", c_int),
+ ("iVddc", c_int), ("iActivityPercent", c_int), ("iCurrentPerformanceLevel", c_int),
+ ("iCurrentBusSpeed", c_int), ("iCurrentBusLanes", c_int), ("iUnknown", c_int * 4),
+ ]
+
+
+class ADLBackend(MonitorBackend):
+ name = "adl"
+
+ def __init__(self):
+ super().__init__()
+ self._dll = None
+ self._context = c_int(0)
+
+ def initialize(self) -> bool:
+ if platform.system() != "Windows":
+ return False
+ try:
+ self._dll = ctypes.windll.LoadLibrary("atiadlxx.dll")
+ except:
+ try:
+ self._dll = ctypes.windll.LoadLibrary("atiadlxy.dll")
+ except:
+ logger.debug("ADL Backend: atiadlxx.dll not found")
+ return False
+
+ # Try to initialize ADL
+ try:
+ create = self._dll.ADL_Main_Control_Create
+ create.restype = c_int
+ result = create(0) # ADL_Main_Control_Create with default
+ if result != 0:
+ logger.debug(f"ADL Backend: init failed: {result}")
+ return False
+ self.available = True
+ self.gpu_count = 1
+ self.gpu_names = ["AMD GPU (ADL)"]
+ logger.info("ADL Backend: initialized")
+ return True
+ except Exception as e:
+ logger.debug(f"ADL Backend: init error: {e}")
+ return False
+
+ def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
+ stats = AMDGPUStats(gpu_id=gpu_id, gpu_name=self.gpu_names[0], is_available=True)
+
+ try:
+ # Temperature
+ temp = ADLTemperature()
+ temp.iSize = ctypes.sizeof(ADLTemperature)
+ get_temp = self._dll.ADL_Overdrive5_Temperature_Get
+ get_temp.restype = c_int
+ if get_temp(gpu_id, 0, byref(temp)) == 0:
+ stats.temperature = temp.iTemperature / 1000.0
+
+ # Activity
+ activity = ADLPMActivity()
+ activity.iSize = ctypes.sizeof(ADLPMActivity)
+ get_act = self._dll.ADL_Overdrive5_CurrentActivity_Get
+ get_act.restype = c_int
+ if get_act(gpu_id, byref(activity)) == 0:
+ stats.utilization_gpu = float(activity.iActivityPercent)
+ stats.core_clock = activity.iEngineClock
+ stats.memory_clock = activity.iMemoryClock
+
+ # Fan speed
+ fan = ADLFanSpeedValue()
+ fan.iSize = ctypes.sizeof(ADLFanSpeedValue)
+ fan.iSpeedType = 0
+ get_fan = self._dll.ADL_Overdrive5_FanSpeed_Get
+ get_fan.restype = c_int
+ if get_fan(gpu_id, 0, byref(fan)) == 0:
+ stats.fan_speed = fan.iFanSpeed
+
+ except Exception as e:
+ stats.is_available = False
+ stats.error_message = str(e)
+
+ return stats
+
+ def close(self):
+ try:
+ destroy = self._dll.ADL_Main_Control_Destroy
+ destroy.restype = c_int
+ destroy()
+ except:
+ pass
\ No newline at end of file
diff --git a/monitor/backends/base.py b/monitor/backends/base.py
new file mode 100644
index 0000000..8e8da3e
--- /dev/null
+++ b/monitor/backends/base.py
@@ -0,0 +1,69 @@
+"""
+Base classes for monitoring backends.
+"""
+
+from dataclasses import dataclass, field
+from typing import Optional, List
+
+
+@dataclass
+class AMDGPUStats:
+ """Normalized GPU statistics from any backend"""
+ gpu_id: int = 0
+ gpu_name: str = ""
+ utilization_gpu: float = 0.0 # 0-100%
+ utilization_memory: float = 0.0 # 0-100%
+ memory_total: int = 0 # Bytes
+ memory_used: int = 0 # Bytes
+ memory_free: int = 0 # Bytes
+ temperature: float = 0.0 # Celsius
+ fan_speed: int = 0 # 0-100%
+ core_clock: int = 0 # MHz
+ memory_clock: int = 0 # MHz
+ power_draw: float = 0.0 # Watts
+ is_available: bool = True
+ error_message: Optional[str] = None
+
+ def to_dict(self) -> dict:
+ return {
+ 'gpu_id': self.gpu_id,
+ 'gpu_name': self.gpu_name,
+ 'utilization_gpu': round(self.utilization_gpu, 1),
+ 'utilization_memory': round(self.utilization_memory, 1),
+ 'memory_total': self.memory_total,
+ 'memory_used': self.memory_used,
+ 'memory_free': self.memory_free,
+ 'temperature': round(self.temperature, 1),
+ 'fan_speed': self.fan_speed,
+ 'core_clock': self.core_clock,
+ 'memory_clock': self.memory_clock,
+ 'power_draw': round(self.power_draw, 1),
+ 'is_available': self.is_available,
+ }
+
+
+class MonitorBackend:
+ """Base class for all monitoring backends"""
+
+ name = "base"
+
+ def __init__(self):
+ self.available = False
+ self.gpu_count = 0
+ self.gpu_names: List[str] = []
+
+ def initialize(self) -> bool:
+ """Detect hardware and prepare backend. Returns True if available."""
+ raise NotImplementedError
+
+ def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
+ """Get current stats for a GPU."""
+ raise NotImplementedError
+
+ def get_all_stats(self) -> List[AMDGPUStats]:
+ """Get stats for all GPUs."""
+ return [self.get_stats(i) for i in range(self.gpu_count)]
+
+ def close(self):
+ """Cleanup resources."""
+ pass
\ No newline at end of file
diff --git a/monitor/backends/libre_hardware.py b/monitor/backends/libre_hardware.py
new file mode 100644
index 0000000..a705f5e
--- /dev/null
+++ b/monitor/backends/libre_hardware.py
@@ -0,0 +1,175 @@
+"""
+LibreHardwareMonitor Backend
+============================
+Integrates LibreHardwareMonitor (LHM) for full GPU telemetry.
+- Auto-detects if LHM is running via WMI
+- Auto-downloads LHM portable if not present
+- Does NOT auto-run (requires admin) โ user runs installer once
+- Reads: Temperature, Fan Speed, GPU Load, VRAM, Clocks, Power
+"""
+
+import logging
+import subprocess
+import platform
+import os
+import json
+import urllib.request
+import zipfile
+
+from .base import MonitorBackend, AMDGPUStats
+
+logger = logging.getLogger(__name__)
+
+LHM_DOWNLOAD_URL = "https://github.com/LibreHardwareMonitor/LibreHardwareMonitor/releases/download/v0.9.6/LibreHardwareMonitor.NET.10.zip"
+
+
+class LibreHardwareBackend(MonitorBackend):
+ name = "libre-hardware-monitor"
+
+ def __init__(self):
+ super().__init__()
+ self._lhm_exe = None
+
+ def initialize(self) -> bool:
+ if platform.system() != "Windows":
+ return False
+
+ # Check if LHM/OHM sensors are accessible via WMI
+ if self._detect_wmi_sensors():
+ logger.info("LHM Backend: detected running instance via WMI")
+ self.available = True
+ self.gpu_count = 1
+ self.gpu_names = ["AMD GPU (LHM)"]
+ return True
+
+ # Download LHM if missing (user just needs to run it)
+ self._ensure_downloaded()
+
+ # Check if exe exists (downloaded but not running)
+ if self._lhm_exe and os.path.exists(self._lhm_exe):
+ logger.info(
+ "LHM Backend: downloaded but not running. "
+ "Run as Admin: 'monitor\\libre_hardware_monitor\\LibreHardwareMonitor.exe --wmi'"
+ )
+
+ return False
+
+ def _get_lhm_dir(self) -> str:
+ """LHM storage directory"""
+ base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+ return os.path.join(base, "libre_hardware_monitor")
+
+ def _detect_wmi_sensors(self) -> bool:
+ """Check via PowerShell if LHM/OHM WMI sensors exist"""
+ namespaces = ['root/LibreHardwareMonitor', 'root/OpenHardwareMonitor']
+ for ns in namespaces:
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ f"Get-WmiObject -Namespace '{ns}' -Class Sensor "
+ f"2>$null | Select-Object -First 1 | ConvertTo-Json"
+ ]
+ result = subprocess.run(
+ cmd, capture_output=True, text=True, timeout=3,
+ creationflags=subprocess.CREATE_NO_WINDOW
+ )
+ if result.returncode == 0 and result.stdout.strip() and 'null' not in result.stdout:
+ return True
+ except:
+ pass
+ return False
+
+ def _ensure_downloaded(self):
+ """Download LHM portable if not already present"""
+ lhm_dir = self._get_lhm_dir()
+ os.makedirs(lhm_dir, exist_ok=True)
+
+ # Find existing exe
+ for root, dirs, files in os.walk(lhm_dir):
+ for f in files:
+ if f.lower() == "librehardwaremonitor.exe":
+ self._lhm_exe = os.path.join(root, f)
+ return
+
+ # Download
+ zip_path = os.path.join(lhm_dir, "lhm_download.zip")
+ logger.info("LHM: downloading portable (8.5MB)...")
+
+ try:
+ req = urllib.request.Request(
+ LHM_DOWNLOAD_URL,
+ headers={'User-Agent': 'BangtrixToolkit/1.0'}
+ )
+ with urllib.request.urlopen(req, timeout=120) as resp:
+ with open(zip_path, 'wb') as f:
+ f.write(resp.read())
+
+ if os.path.getsize(zip_path) < 500000:
+ logger.error("LHM: download failed (too small)")
+ os.remove(zip_path)
+ return
+
+ logger.info("LHM: extracting...")
+ with zipfile.ZipFile(zip_path, 'r') as zf:
+ zf.extractall(lhm_dir)
+ os.remove(zip_path)
+
+ # Find exe location after extract
+ for root, dirs, files in os.walk(lhm_dir):
+ for f in files:
+ if f.lower() == "librehardwaremonitor.exe":
+ self._lhm_exe = os.path.join(root, f)
+ logger.info(f"LHM: downloaded to {self._lhm_exe}")
+ return
+
+ except Exception as e:
+ logger.warning(f"LHM download error: {e}")
+ if os.path.exists(zip_path):
+ os.remove(zip_path)
+
+ def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
+ """Get GPU stats from LHM/OHM via WMI"""
+ name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
+ stats = AMDGPUStats(gpu_id=gpu_id, gpu_name=name, is_available=True)
+
+ # Only works if LHM is running
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "$sensors = Get-WmiObject -Namespace 'root/LibreHardwareMonitor' -Class Sensor "
+ "2>$null; if (-not $sensors) { $sensors = Get-WmiObject -Namespace "
+ "'root/OpenHardwareMonitor' -Class Sensor 2>$null }; "
+ "$sensors | Where-Object { $_.Value -gt 0 } | "
+ "Select-Object SensorType, Name, Value | ConvertTo-Json"
+ ]
+ result = subprocess.run(
+ cmd, capture_output=True, text=True, timeout=5,
+ creationflags=subprocess.CREATE_NO_WINDOW
+ )
+
+ if result.returncode == 0 and result.stdout.strip():
+ data = json.loads(result.stdout.strip())
+ if isinstance(data, dict):
+ data = [data]
+ for item in data:
+ stype = item.get('SensorType', '')
+ sname = item.get('Name', '')
+ svalue = float(item.get('Value', 0))
+ if stype == 'Temperature' and 'GPU' in sname:
+ stats.temperature = svalue
+ elif stype == 'Fan' and 'GPU' in sname:
+ stats.fan_speed = int(svalue)
+ elif stype == 'Load' and 'GPU' in sname:
+ if 'Memory' not in sname:
+ stats.utilization_gpu = svalue
+ else:
+ stats.utilization_memory = svalue
+ elif stype == 'Clock' and 'GPU' in sname:
+ if 'Memory' in sname:
+ stats.memory_clock = int(svalue)
+ elif 'Core' in sname:
+ stats.core_clock = int(svalue)
+ except Exception as e:
+ logger.warning(f"LHM sensor error: {e}")
+
+ return stats
\ No newline at end of file
diff --git a/monitor/backends/powershell_backend.py b/monitor/backends/powershell_backend.py
new file mode 100644
index 0000000..2ac6864
--- /dev/null
+++ b/monitor/backends/powershell_backend.py
@@ -0,0 +1,224 @@
+"""
+PowerShell Counter Backend
+==========================
+Reads GPU stats via Windows Performance Counters + WMI.
+No extra dependencies (uses built-in PowerShell).
+"""
+
+import logging
+import subprocess
+import platform
+import json
+import re
+
+from .base import MonitorBackend, AMDGPUStats
+
+logger = logging.getLogger(__name__)
+
+
+class PowerShellBackend(MonitorBackend):
+ name = "powershell-counters"
+
+ def initialize(self) -> bool:
+ if platform.system() != "Windows":
+ logger.debug("PS Backend: Windows only")
+ return False
+ return self._detect_gpus()
+
+ def _detect_gpus(self) -> bool:
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject Win32_VideoController | "
+ "Where-Object { $_.AdapterRAM -gt 0 } | "
+ "Select-Object Name, AdapterRAM | ConvertTo-Json"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
+ if result.returncode == 0 and result.stdout.strip():
+ data = json.loads(result.stdout.strip())
+ if isinstance(data, dict):
+ data = [data]
+ for gpu in data:
+ name = gpu.get('Name', 'Unknown GPU').strip()
+ self.gpu_names.append(name)
+ self.gpu_count = len(self.gpu_names)
+ self.available = self.gpu_count > 0
+ if self.available:
+ logger.info(f"PS Backend: {self.gpu_count} GPU(s): {self.gpu_names}")
+ return self.available
+ except Exception as e:
+ logger.debug(f"PS Backend detect error: {e}")
+ return False
+
+ def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
+ name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
+
+ gpu_util = self._get_gpu_utilization()
+ temp = self._get_gpu_temperature()
+ fan = self._get_gpu_fan_speed()
+
+ # Get VRAM โ try multiple methods
+ mem_total, mem_used = self._get_vram_info()
+
+ # Only fallback to system RAM if absolutely no GPU info
+ if mem_total == 0:
+ try:
+ import psutil
+ svmem = psutil.virtual_memory()
+ mem_total = svmem.total
+ mem_used = svmem.used
+ except:
+ pass
+
+ mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
+
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ gpu_name=name,
+ utilization_gpu=gpu_util,
+ utilization_memory=mem_util,
+ memory_total=mem_total,
+ memory_used=mem_used,
+ memory_free=mem_total - mem_used,
+ temperature=temp,
+ fan_speed=fan,
+ is_available=True,
+ )
+
+ def _get_vram_info(self) -> tuple:
+ """
+ Get VRAM total & used in bytes.
+ Tries multiple methods due to WMI AdapterRAM 4GB limit bug.
+ """
+ # Method 1: Direct registry query for accurate VRAM
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "$gpu = Get-WmiObject Win32_VideoController | Select-Object -First 1; "
+ "[PSCustomObject]@{"
+ " Total = if ($gpu.AdapterRAM -and $gpu.AdapterRAM -gt 4GB) { $gpu.AdapterRAM } else { "
+ " # Try registry for accurate VRAM > 4GB\n"
+ " $paths = Get-ChildItem 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}\\*' "
+ "-ErrorAction SilentlyContinue | Where-Object { $_.GetValue('HardwareInformation.AdapterString') -match 'Radeon|AMD' }; "
+ " foreach ($p in $paths) { "
+ " $vram = $p.GetValue('HardwareInformation.qwMemorySize'); "
+ " if ($vram -and $vram -gt 0) { $vram; break } "
+ " } "
+ " }; "
+ " Used = 0 "
+ "} | ConvertTo-Json"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5,
+ creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ data = json.loads(result.stdout.strip())
+ total = int(data.get('Total', 0))
+ used = int(data.get('Used', 0))
+ if total > 0:
+ return total, used
+ except:
+ pass
+
+ # Method 2: Try reading registry directly via PowerShell
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "$paths = Get-ChildItem 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\"
+ "{4d36e968-e325-11ce-bfc1-08002be10318}\\*' -ErrorAction SilentlyContinue; "
+ "foreach ($p in $paths) { "
+ " $str = $p.GetValue('HardwareInformation.AdapterString'); "
+ " if ($str -match 'Radeon|AMD|RDNA') { "
+ " $mem = $p.GetValue('HardwareInformation.qwMemorySize'); "
+ " if ($mem) { $mem; break } "
+ " } "
+ "}"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5,
+ creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ total = int(result.stdout.strip())
+ if total > 0:
+ return total, 0
+ except:
+ pass
+
+ # Method 3: AdapterRAM from WMI (may be capped at 4GB for some drivers)
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "(Get-WmiObject Win32_VideoController | Select-Object -First 1).AdapterRAM"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3,
+ creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ total = int(result.stdout.strip())
+ if total > 0:
+ return total, 0
+ except:
+ pass
+
+ return 0, 0
+
+ def _get_gpu_utilization(self) -> float:
+ try:
+ # Only query 3D engine type to avoid noise from other engines
+ ps_cmd = (
+ "@(Get-Counter '\\GPU Engine(*engtype_3D)\\*' -ErrorAction SilentlyContinue "
+ "| Select-Object -ExpandProperty CounterSamples "
+ "| Where-Object { $_.CookedValue -gt 0 }).CookedValue "
+ "| Measure-Object -Average | Select-Object -ExpandProperty Average"
+ )
+ result = subprocess.run(
+ ["powershell", "-NoProfile", "-Command", ps_cmd],
+ capture_output=True, text=True, timeout=5,
+ creationflags=subprocess.CREATE_NO_WINDOW
+ )
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ # Counter returns nanoseconds per sample
+ # Normalize: typical 60fps = 16.67ms per frame = 16,670,000 ns
+ # 100% utilization on a single engine โ 16,670,000 ns
+ # Multiple GPUs/engines sum โ cap at 100
+ pct = (val / 16670000.0) * 100.0
+ return min(100.0, max(0.0, pct))
+ except:
+ pass
+ return 0.0
+
+ def _get_gpu_temperature(self) -> float:
+ for ns in ['root/LibreHardwareMonitor', 'root/OpenHardwareMonitor']:
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ f"Get-WmiObject -Namespace '{ns}' -Class Sensor 2>$null | "
+ f"Where-Object {{ $_.SensorType -eq 'Temperature' -and "
+ f"$_.Name -match 'GPU' }} | Select-Object -First 1 -ExpandProperty Value"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3,
+ creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ if val > 0:
+ return val
+ except:
+ pass
+ return 0.0
+
+ def _get_gpu_fan_speed(self) -> int:
+ for ns in ['root/LibreHardwareMonitor', 'root/OpenHardwareMonitor']:
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ f"Get-WmiObject -Namespace '{ns}' -Class Sensor 2>$null | "
+ f"Where-Object {{ $_.SensorType -eq 'Fan' -and "
+ f"$_.Name -match 'GPU' }} | Select-Object -First 1 -ExpandProperty Value"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3,
+ creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ if val > 0:
+ return int(val)
+ except:
+ pass
+ return 0
\ No newline at end of file
diff --git a/monitor/backends/psutil_backend.py b/monitor/backends/psutil_backend.py
new file mode 100644
index 0000000..f6b3d04
--- /dev/null
+++ b/monitor/backends/psutil_backend.py
@@ -0,0 +1,40 @@
+"""
+Psutil Fallback Backend
+=======================
+Uses psutil for basic system memory info (last resort).
+"""
+
+import logging
+from .base import MonitorBackend, AMDGPUStats
+
+logger = logging.getLogger(__name__)
+
+
+class PsutilBackend(MonitorBackend):
+ name = "psutil-fallback"
+
+ def initialize(self) -> bool:
+ try:
+ import psutil
+ self.available = True
+ self.gpu_count = 1
+ self.gpu_names = ["System RAM (no GPU backend)"]
+ logger.info("Psutil Backend: fallback active")
+ return True
+ except ImportError:
+ logger.debug("Psutil Backend: psutil not installed")
+ return False
+
+ def get_stats(self, gpu_id: int = 0) -> AMDGPUStats:
+ import psutil
+ svmem = psutil.virtual_memory()
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ gpu_name=self.gpu_names[0],
+ utilization_gpu=0.0,
+ utilization_memory=(svmem.used / svmem.total * 100) if svmem.total > 0 else 0,
+ memory_total=svmem.total,
+ memory_used=svmem.used,
+ memory_free=svmem.available,
+ is_available=True,
+ )
\ No newline at end of file
diff --git a/monitor/process_monitor.py b/monitor/process_monitor.py
new file mode 100644
index 0000000..9eee99b
--- /dev/null
+++ b/monitor/process_monitor.py
@@ -0,0 +1,226 @@
+"""
+ComfyUI Process Monitor
+=======================
+Monitors ComfyUI process during image generation.
+Auto-detects generation start/end and records peak memory/CPU.
+
+Usage:
+ from monitor import get_process_monitor
+ monitor = get_process_monitor()
+ monitor.start_monitoring()
+ # ... generate images ...
+ print(monitor.get_summary())
+"""
+
+import logging
+import time
+import threading
+from dataclasses import dataclass
+from typing import Optional, List
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class GenerationRecord:
+ """Stats for a single generation run"""
+ timestamp: float = 0.0
+ duration: float = 0.0
+ vram_peak_mb: float = 0.0
+ vram_start_mb: float = 0.0
+ vram_end_mb: float = 0.0
+ vram_delta_mb: float = 0.0
+ ram_peak_mb: float = 0.0
+ ram_start_mb: float = 0.0
+ ram_end_mb: float = 0.0
+ cpu_peak: float = 0.0
+ error: Optional[str] = None
+
+
+@dataclass
+class ProcessSnapshot:
+ """Point-in-time process metrics"""
+ timestamp: float
+ memory_rss_mb: float
+ memory_vms_mb: float
+ cpu_percent: float
+ num_threads: int
+
+
+class ComfyProcessMonitor:
+ """Monitors ComfyUI process for generation metrics"""
+
+ def __init__(self, poll_interval: float = 0.5):
+ self.poll_interval = poll_interval
+ self._process = None
+ self._process_pid = None
+ self._running = False
+ self._thread = None
+ self._is_generating = False
+ self._gen_start_time = 0.0
+ self._current_record: Optional[GenerationRecord] = None
+ self._prev_memory_mb = 0.0
+ self.history: List[GenerationRecord] = []
+ self.current: Optional[GenerationRecord] = None
+ self._snapshots: List[ProcessSnapshot] = []
+ self._lock = threading.Lock()
+
+ # Detection thresholds (tuned)
+ self._MEMORY_START_THRESHOLD_MB = 50
+ self._CPU_START_THRESHOLD = 15
+ self._MEMORY_END_THRESHOLD_MB = -30
+ self._CPU_END_THRESHOLD = 8
+
+ def find_comfy_process(self) -> bool:
+ """Auto-detect ComfyUI Python process"""
+ try:
+ import psutil
+ pid = None
+ current = psutil.Process()
+ cmdline = ' '.join(current.cmdline()).lower()
+
+ # Check if WE are the ComfyUI process
+ if 'comfy' in cmdline or 'main.py' in cmdline:
+ pid = current.pid
+
+ # Search by name
+ if not pid:
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
+ try:
+ name = (proc.info['name'] or '').lower()
+ cl = ' '.join(proc.info['cmdline'] or []).lower()
+ if ('python' in name or 'python' in cl) and ('comfy' in cl or 'main.py' in cl):
+ pid = proc.info['pid']
+ break
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+
+ if pid:
+ self._process_pid = pid
+ self._process = psutil.Process(pid)
+ logger.info(f"Process Monitor: found PID {pid}")
+ return True
+
+ logger.warning("Process Monitor: ComfyUI not found")
+ return False
+ except Exception as e:
+ logger.warning(f"Process Monitor: find error: {e}")
+ return False
+
+ def start_monitoring(self):
+ """Begin background monitoring thread"""
+ if self._running:
+ return
+ if not self.find_comfy_process():
+ return
+ self._running = True
+ self._thread = threading.Thread(target=self._poll_loop, daemon=True)
+ self._thread.start()
+ logger.info("Process Monitor: started")
+
+ def stop_monitoring(self):
+ """Stop background monitoring"""
+ self._running = False
+ if self._thread:
+ self._thread.join(timeout=2)
+
+ def _poll_loop(self):
+ try:
+ import psutil
+ except ImportError:
+ self._running = False
+ return
+
+ while self._running:
+ try:
+ if not self._process or not self._process.is_running():
+ if not self.find_comfy_process():
+ time.sleep(self.poll_interval * 10)
+ continue
+
+ mem = self._process.memory_info()
+ cpu = self._process.cpu_percent(interval=0)
+ rss_mb = mem.rss / (1024 * 1024)
+ delta = rss_mb - self._prev_memory_mb
+ snap = ProcessSnapshot(time.time(), rss_mb, mem.vms / (1024*1024), cpu, self._process.num_threads())
+
+ with self._lock:
+ if not self._is_generating:
+ if delta > self._MEMORY_START_THRESHOLD_MB or cpu > self._CPU_START_THRESHOLD:
+ self._is_generating = True
+ self._gen_start_time = time.time()
+ self._current_record = GenerationRecord(
+ timestamp=self._gen_start_time, duration=0,
+ vram_start_mb=rss_mb, vram_peak_mb=rss_mb,
+ ram_start_mb=rss_mb, ram_peak_mb=rss_mb,
+ )
+ self._snapshots = [snap]
+ logger.info(f"Process Monitor: START (RAM: {rss_mb:.0f}MB)")
+ else:
+ if self._current_record:
+ self._current_record.vram_peak_mb = max(self._current_record.vram_peak_mb, rss_mb)
+ self._current_record.ram_peak_mb = max(self._current_record.ram_peak_mb, rss_mb)
+ self._current_record.cpu_peak = max(self._current_record.cpu_peak, cpu)
+ self._snapshots.append(snap)
+
+ if delta < self._MEMORY_END_THRESHOLD_MB and cpu < self._CPU_END_THRESHOLD:
+ self._finalize(rss_mb)
+ logger.info(f"Process Monitor: END ({self.current.duration:.1f}s)")
+
+ self._prev_memory_mb = rss_mb
+ except Exception as e:
+ logger.debug(f"Process Monitor: {e}")
+ time.sleep(self.poll_interval)
+
+ def _finalize(self, end_mb: float):
+ if not self._current_record:
+ return
+ self._current_record.duration = time.time() - self._gen_start_time
+ self._current_record.vram_end_mb = end_mb
+ self._current_record.ram_end_mb = end_mb
+ self._current_record.vram_delta_mb = self._current_record.vram_peak_mb - self._current_record.vram_start_mb
+ self.current = self._current_record
+ self.history.append(self._current_record)
+ if len(self.history) > 50:
+ self.history = self.history[-50:]
+ self._is_generating = False
+ self._current_record = None
+ self._snapshots = []
+
+ @property
+ def is_generating(self) -> bool:
+ return self._is_generating
+
+ def get_current_generation(self) -> GenerationRecord:
+ with self._lock:
+ if self._is_generating and self._current_record:
+ return self._current_record
+ return GenerationRecord()
+
+ def get_last_generation(self) -> Optional[GenerationRecord]:
+ if self.history:
+ return self.history[-1]
+ return None
+
+ def get_summary(self) -> str:
+ if not self.history:
+ return "No generations recorded."
+ lines = ["=== ComfyUI Process Monitor ==="]
+ total_time = peak_ram = 0
+ for gen in self.history[-5:]:
+ total_time += gen.duration
+ peak_ram = max(peak_ram, gen.ram_peak_mb)
+ lines.append(f" #{len(lines)}: {gen.duration:.1f}s | RAM: {gen.ram_start_mb:.0f}โ{gen.ram_peak_mb:.0f}MB")
+ lines.append(f" --- Total: {total_time:.1f}s | Peak RAM: {peak_ram:.0f}MB")
+ return "\n".join(lines)
+
+
+# Singleton
+_process_monitor = None
+
+
+def get_process_monitor() -> ComfyProcessMonitor:
+ global _process_monitor
+ if _process_monitor is None:
+ _process_monitor = ComfyProcessMonitor()
+ return _process_monitor
\ No newline at end of file
diff --git a/nodes/__init__.py b/nodes/__init__.py
index e4337b0..dc8c771 100644
--- a/nodes/__init__.py
+++ b/nodes/__init__.py
@@ -3,15 +3,6 @@ from .translate_universal import (
NODE_DISPLAY_NAME_MAPPINGS
)
-from .amd_monitor import (
- NODE_CLASS_MAPPINGS as AMD_CLASS,
- NODE_DISPLAY_NAME_MAPPINGS as AMD_DISPLAY
-)
-
-# Gabungkan mappings
-NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **AMD_CLASS}
-NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **AMD_DISPLAY}
-
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS"
diff --git a/nodes/amd_monitor.py b/nodes/amd_monitor.py
deleted file mode 100644
index 9d80652..0000000
--- a/nodes/amd_monitor.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from ..utils.amd_utils import get_amd_monitor
-
-
-class AMDMonitorNode:
- """
- A node to monitor AMD GPU statistics.
- Outputs: GPU Usage (%), VRAM Used (MB), VRAM Total (MB), Temperature (C)
- """
-
- @classmethod
- def INPUT_TYPES(s):
- return {
- "required": {
- "gpu_index": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1}),
- }
- }
-
- RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "INT", "INT", "STRING")
- RETURN_NAMES = ("gpu_utilization", "vram_usage_pct", "temperature", "vram_used_mb", "vram_total_mb", "status_msg")
- FUNCTION = "monitor"
- CATEGORY = "BANGTRIXTOOLKIT/System"
-
- def monitor(self, gpu_index):
- monitor = get_amd_monitor()
-
- if not monitor.available:
- return (0.0, 0.0, 0.0, 0, 0, "Error: AMD Backend not found")
-
- stats = monitor.get_gpu_stats(gpu_index)
-
- if not stats.is_available:
- return (0.0, 0.0, 0.0, 0, 0, f"Error: {stats.error_message}")
-
- # Convert bytes to MB for easier reading
- vram_used_mb = stats.memory_used / (1024 * 1024)
- vram_total_mb = stats.memory_total / (1024 * 1024)
-
- status = "OK"
-
- return (
- float(stats.utilization_gpu),
- float(stats.utilization_memory),
- float(stats.temperature),
- int(vram_used_mb),
- int(vram_total_mb),
- status
- )
-
-
-# Registry
-NODE_CLASS_MAPPINGS = {
- "BANGTRIX_AMD_Monitor": AMDMonitorNode
-}
-
-NODE_DISPLAY_NAME_MAPPINGS = {
- "BANGTRIX_AMD_Monitor": "AMD Monitor (BANGTRIX)"
-}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 68220a3..5a9948d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,15 +1,47 @@
-# HTTP & WebSocket Server (Required for AMD Monitor)
+# ============================================
+# BANGTRIXTOOLKIT - Dependencies
+# ============================================
+# Install with: pip install -r requirements.txt
+
+# ============================================
+# ๐ก WebSocket & HTTP Server (WAJIB)
+# ============================================
+# Diperlukan untuk AMD Monitor WebSocket + streaming
aiohttp>=3.8.0
async-timeout>=4.0.0
-# HTTP Requests (Optional - for translate_universal)
+# ============================================
+# ๐ HTTP Client (Opsional - translate node)
+# ============================================
+# Google Translate API + GitHub API (LHM download)
+# NOTE: Kalau tidak diinstall, Python urllib akan dipakai
requests>=2.31.0
-# System Monitoring
+# ============================================
+# ๐ฅ๏ธ System Monitoring (DIREKOMENDASIKAN)
+# ============================================
+# Diperlukan untuk:
+# - Process Monitor (deteksi generasi ComfyUI)
+# - VRAM/memory fallback
+# - CPU utilization tracking
psutil>=5.9.0
-# AMD GPU Monitoring - Linux (ROCm)
+# ============================================
+# ๐ง AMD GPU - Linux (ROCm)
+# ============================================
+# Hanya untuk Linux dengan driver ROCm terinstall
pyrsmi>=0.2.0; sys_platform == "linux"
-# AMD GPU Monitoring - Windows (ADL)
-pyadl>=1.0.0; sys_platform == "win32"
\ No newline at end of file
+# ============================================
+# ๐ช AMD GPU - Windows (Legacy ADL)
+# ============================================
+# Opsional: untuk GPU temperature/fan via ADL
+# NOTE: Sebagian driver AMD terbaru mungkin tidak kompatibel
+pyadl>=1.0.0; sys_platform == "win32"
+
+# ============================================
+# ๐ก๏ธ LibreHardwareMonitor (Windows)
+# ============================================
+# TIDAK PERLU install! Auto-download dari GitHub saat runtime.
+# Baca: Temperature, Fan, Clock, Power, Load via WMI.
+# Minimal: Python 3.8+ (built-in: subprocess, wmi, powershell)
\ No newline at end of file
diff --git a/server/amd_server.py b/server/amd_server.py
index fffeb7a..ea2d037 100644
--- a/server/amd_server.py
+++ b/server/amd_server.py
@@ -1,13 +1,16 @@
"""
AMD Monitor Server
==================
-WebSocket server for streaming AMD GPU stats to frontend
+WebSocket server for streaming AMD GPU stats to frontend.
+Supports multi-GPU, commands, history, and ComfyUI process monitoring.
+Default interval: 5s
"""
import asyncio
import json
import logging
-from typing import Set
+from collections import deque
+from typing import Set, Dict
logger = logging.getLogger(__name__)
@@ -17,89 +20,252 @@ class AMDMonitorServer:
self.monitor = None # Lazy load
self.clients: Set = set()
self.running = False
- self.update_interval = 1.0 # seconds
+ self.update_interval = 1.0 # seconds โ real-time
+ self._stream_task = None
+
+ # Process monitor
+ self._process_monitor = None
+
+ # Multi-GPU history
+ self.history_maxlen = 30
+ self.gpu_history: Dict[int, deque] = {}
def _get_monitor(self):
- """Lazy load monitor to avoid import issues at startup"""
if self.monitor is None:
- from ..utils.amd_utils import get_amd_monitor
- self.monitor = get_amd_monitor()
+ try:
+ from ..monitor import get_amd_monitor
+ self.monitor = get_amd_monitor()
+ except ImportError:
+ from ..utils.amd_utils import get_amd_monitor as _legacy_monitor
+ self.monitor = _legacy_monitor()
return self.monitor
+ def _get_process_monitor(self):
+ if self._process_monitor is None:
+ try:
+ from ..monitor.process_monitor import get_process_monitor
+ self._process_monitor = get_process_monitor()
+ self._process_monitor.start_monitoring()
+ except ImportError:
+ from ..utils.amd_process_monitor import get_process_monitor as _legacy_pm
+ self._process_monitor = _legacy_pm()
+ self._process_monitor.start_monitoring()
+ except Exception as e:
+ logger.debug(f"Process monitor init: {e}")
+ return self._process_monitor
+
async def broadcast(self, message: str):
- """Send data to all connected clients"""
- if self.clients:
- await asyncio.gather(
- *[client.send_str(message) for client in self.clients],
- return_exceptions=True
- )
+ if not self.clients:
+ return
+ results = await asyncio.gather(
+ *[client.send_str(message) for client in self.clients.copy()],
+ return_exceptions=True
+ )
+ for i, result in enumerate(results):
+ if isinstance(result, Exception):
+ client_list = list(self.clients)
+ if i < len(client_list):
+ logger.debug(f"AMD Monitor: Removing dead client: {result}")
+ self.clients.discard(client_list[i])
async def handle_client(self, ws):
- """Handle WebSocket client connection"""
self.clients.add(ws)
logger.info(f"AMD Monitor: Client connected. Total clients: {len(self.clients)}")
-
+
try:
async for msg in ws:
- if msg.type == 1: # Text message
- if msg.data == "ping":
- await ws.send_str("pong")
+ if msg.type == 1:
+ await self._handle_message(ws, msg.data)
+ except asyncio.CancelledError:
+ pass
except Exception as e:
logger.error(f"AMD Monitor: Client error: {e}")
finally:
self.clients.discard(ws)
logger.info(f"AMD Monitor: Client disconnected. Total clients: {len(self.clients)}")
- async def stream_data(self):
- """Stream GPU stats periodically"""
- monitor = self._get_monitor()
-
- while self.running:
- try:
- if monitor.available:
- stats = monitor.get_gpu_stats(0) # GPU 0
-
- data = {
- "type": "amd_stats",
- "gpu_id": stats.gpu_id,
- "gpu_utilization": stats.utilization_gpu,
- "vram_usage_pct": stats.utilization_memory,
- "vram_used_mb": stats.memory_used / (1024 * 1024),
- "vram_total_mb": stats.memory_total / (1024 * 1024),
- "temperature": stats.temperature,
- "fan_speed": stats.fan_speed,
- "is_available": stats.is_available,
- "method": monitor.method,
- "gpu_count": monitor.gpu_count
- }
- else:
- data = {
- "type": "amd_stats",
- "is_available": False,
- "error": "AMD backend not available",
- "method": None,
- "gpu_count": 0
- }
-
- await self.broadcast(json.dumps(data))
-
- except Exception as e:
- logger.error(f"AMD Monitor: Stream error: {e}")
-
- await asyncio.sleep(self.update_interval)
+ async def _handle_message(self, ws, raw: str):
+ try:
+ cmd = json.loads(raw)
+ cmd_type = cmd.get("type", "")
- def start(self, loop=None):
- """Start the monitoring loop safely"""
+ if cmd_type == "ping":
+ await ws.send_str(json.dumps({"type": "pong"}))
+
+ elif cmd_type == "set_interval":
+ interval = float(cmd.get("interval", 5.0))
+ self.update_interval = max(0.5, min(30.0, interval))
+ await ws.send_str(json.dumps({
+ "type": "config_updated",
+ "update_interval": self.update_interval
+ }))
+
+ elif cmd_type == "get_history":
+ gpu_id = cmd.get("gpu_id", 0)
+ history = list(self.gpu_history.get(gpu_id, []))
+ await ws.send_str(json.dumps({
+ "type": "history",
+ "gpu_id": gpu_id,
+ "values": history
+ }))
+
+ elif cmd_type == "request_stats":
+ monitor = self._get_monitor()
+ data = self._build_stats_data(monitor)
+ process = self._get_process_monitor()
+ if process:
+ data["process"] = self._build_process_data(process)
+ await ws.send_str(json.dumps(data))
+
+ else:
+ await ws.send_str(json.dumps({
+ "type": "error",
+ "message": f"Unknown command: {cmd_type}"
+ }))
+
+ except json.JSONDecodeError:
+ if raw == "ping":
+ await ws.send_str("pong")
+
+ def _build_process_data(self, process):
+ """Build process monitoring payload"""
+ data = {
+ "is_generating": process.is_generating,
+ }
+
+ if process.is_generating:
+ gen = process.get_current_generation()
+ data["generation"] = {
+ "duration": round(gen.duration, 1),
+ "vram_peak_mb": round(gen.vram_peak_mb, 0),
+ "ram_start_mb": round(gen.ram_start_mb, 0),
+ "ram_peak_mb": round(gen.ram_peak_mb, 0),
+ "cpu_peak": round(gen.cpu_peak, 1),
+ }
+ else:
+ last = process.get_last_generation()
+ if last:
+ data["last_generation"] = {
+ "duration": round(last.duration, 1),
+ "vram_peak_mb": round(last.vram_peak_mb, 0),
+ "vram_delta_mb": round(last.vram_delta_mb, 0),
+ "ram_start_mb": round(last.ram_start_mb, 0),
+ "ram_peak_mb": round(last.ram_peak_mb, 0),
+ "ram_end_mb": round(last.ram_end_mb, 0),
+ "cpu_peak": round(last.cpu_peak, 1),
+ }
+
+ # Generation count
+ data["generation_count"] = len(process.history) if process.history else 0
+
+ return data
+
+ def _build_stats_data(self, monitor, gpu_id: int = 0):
+ if not monitor.available:
+ return {
+ "type": "amd_stats",
+ "gpu_id": gpu_id,
+ "gpu_name": "",
+ "gpu_count": monitor.gpu_count,
+ "is_available": False,
+ "error": "AMD backend not available",
+ "method": monitor.method,
+ "history": []
+ }
+
+ stats = monitor.get_gpu_stats(gpu_id)
+
+ # Update history
+ if gpu_id not in self.gpu_history:
+ self.gpu_history[gpu_id] = deque(maxlen=self.history_maxlen)
+ self.gpu_history[gpu_id].append(stats.utilization_gpu)
+
+ data = {
+ "type": "amd_stats",
+ "gpu_id": stats.gpu_id,
+ "gpu_name": stats.gpu_name or f"AMD GPU {stats.gpu_id}",
+ "gpu_count": monitor.gpu_count,
+ "method": monitor.method or "unknown",
+ "is_available": stats.is_available,
+ "gpu_utilization": round(stats.utilization_gpu, 1),
+ "vram_usage_pct": round(stats.utilization_memory, 1),
+ "vram_used_mb": round(stats.memory_used / (1024 * 1024), 0),
+ "vram_total_mb": round(stats.memory_total / (1024 * 1024), 0),
+ "temperature": round(stats.temperature, 1),
+ "fan_speed": stats.fan_speed,
+ "core_clock_mhz": stats.core_clock,
+ "memory_clock_mhz": stats.memory_clock,
+ "power_draw_watts": round(stats.power_draw, 1),
+ "history": list(self.gpu_history.get(gpu_id, [])),
+ }
+
+ if not stats.is_available and stats.error_message:
+ data["error"] = stats.error_message
+
+ return data
+
+ async def stream_data(self):
+ monitor = self._get_monitor()
+ process_mon = self._get_process_monitor()
+ logger.info(f"AMD Monitor: Stream data loop started (interval={self.update_interval}s)")
+
+ try:
+ while self.running:
+ try:
+ if monitor.available and monitor.gpu_count > 0:
+ for gpu_id in range(monitor.gpu_count):
+ data = self._build_stats_data(monitor, gpu_id)
+
+ # Attach process monitoring data
+ if process_mon:
+ data["process"] = self._build_process_data(process_mon)
+
+ await self.broadcast(json.dumps(data))
+ else:
+ data = {
+ "type": "amd_stats",
+ "gpu_id": 0,
+ "gpu_name": "",
+ "gpu_count": monitor.gpu_count,
+ "is_available": False,
+ "error": "AMD backend not available",
+ "method": monitor.method,
+ "history": []
+ }
+ await self.broadcast(json.dumps(data))
+
+ except asyncio.CancelledError:
+ break
+ except Exception as e:
+ logger.error(f"AMD Monitor: Stream error: {e}")
+
+ await asyncio.sleep(self.update_interval)
+ finally:
+ logger.info("AMD Monitor: Stream data loop ended")
+
+ def start_streaming(self, loop=None):
if self.running:
- return
-
+ return self._stream_task
+
self.running = True
logger.info("AMD Monitor: Starting data stream")
+ loop = loop or asyncio.get_event_loop()
+ self._stream_task = asyncio.ensure_future(self.stream_data(), loop=loop)
+ return self._stream_task
+
+ def stop_streaming(self):
+ self.running = False
+ if self._stream_task and not self._stream_task.done():
+ self._stream_task.cancel()
+ self._stream_task = None
+ logger.info("AMD Monitor: Stream stopped")
+
# Singleton instance
_amd_server = None
+
def get_amd_server() -> AMDMonitorServer:
global _amd_server
if _amd_server is None:
diff --git a/test_amd_monitor.py b/test_amd_monitor.py
deleted file mode 100644
index fcaface..0000000
--- a/test_amd_monitor.py
+++ /dev/null
@@ -1,54 +0,0 @@
-"""
-Quick test script for AMD Monitor backend
-Run: python_embeded\python.exe test_amd_monitor.py
-"""
-
-import sys
-import os
-
-# Add project to path
-sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
-
-print("=== BANGTRIXTOOLKIT AMD Monitor Test ===\n")
-
-# Test 1: Import utils
-print("1. Testing amd_utils import...")
-try:
- from utils.amd_utils import get_amd_monitor, AMDGPUStats
- print(" โ
amd_utils imported")
-except Exception as e:
- print(f" โ amd_utils import failed: {e}")
- sys.exit(1)
-
-# Test 2: Initialize monitor
-print("\n2. Initializing AMD monitor...")
-monitor = get_amd_monitor()
-print(f" Available: {monitor.available}")
-print(f" Method: {monitor.method}")
-print(f" GPU Count: {monitor.gpu_count}")
-
-# Test 3: Get stats
-if monitor.available:
- print("\n3. Fetching GPU stats...")
- stats = monitor.get_gpu_stats(0)
- print(f" GPU ID: {stats.gpu_id}")
- print(f" Utilization: {stats.utilization_gpu}%")
- print(f" VRAM: {stats.memory_used / 1024 / 1024:.0f} MB / {stats.memory_total / 1024 / 1024:.0f} MB")
- print(f" Temperature: {stats.temperature}ยฐC")
- print(f" Fan: {stats.fan_speed}%")
- print(f" Status: {'โ
OK' if stats.is_available else 'โ Error: ' + str(stats.error_message)}")
-else:
- print("\nโ ๏ธ AMD backend not available - check requirements & drivers")
-
-# Test 4: Check dependencies
-print("\n4. Dependency check:")
-deps = ['aiohttp', 'psutil', 'pyadl', 'pyrsmi']
-for dep in deps:
- try:
- __import__(dep)
- ver = getattr(__import__(dep), '__version__', 'unknown')
- print(f" โ
{dep}: {ver}")
- except ImportError:
- print(f" โช {dep}: not installed")
-
-print("\n=== Test Complete ===")
\ No newline at end of file
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/utils/__init__.py
@@ -0,0 +1 @@
+
diff --git a/utils/amd_adl.py b/utils/amd_adl.py
new file mode 100644
index 0000000..9d8da85
--- /dev/null
+++ b/utils/amd_adl.py
@@ -0,0 +1,348 @@
+"""
+AMD ADL (Display Library) Direct Binding
+=========================================
+Low-level ctypes binding to atiadlxx.dll for GPU monitoring on Windows.
+No dependencies required (uses ctypes which is built-in Python).
+
+Provides: GPU Utilization, Temperature, Fan Speed, GPU Name, Core/Memory Clocks
+"""
+
+import ctypes
+import ctypes.util
+import logging
+import platform
+from ctypes import byref, c_int, c_void_p, c_char_p, c_char, POINTER, Structure
+
+logger = logging.getLogger(__name__)
+
+# =============================================
+# DLL LOADING
+# =============================================
+
+_adl_dll = None
+_adl_initialized = False
+
+
+def _load_adl():
+ global _adl_dll
+ if _adl_dll is not None:
+ return True
+
+ system = platform.system()
+ if system != "Windows":
+ return False
+
+ try:
+ _adl_dll = ctypes.windll.LoadLibrary("atiadlxx.dll")
+ logger.debug("ADL: loaded atiadlxx.dll from System32")
+ return True
+ except:
+ try:
+ _adl_dll = ctypes.windll.LoadLibrary("atiadlxy.dll")
+ logger.debug("ADL: loaded atiadlxy.dll (fallback)")
+ return True
+ except:
+ logger.debug("ADL: cannot load atiadlxx.dll / atiadlxy.dll")
+ return False
+
+
+# =============================================
+# ADL TYPES
+# =============================================
+
+class ADLAdapterInfo(Structure):
+ _fields_ = [
+ ("strAdapterName", c_char * 256),
+ ("iAdapterIndex", c_int),
+ ("iBusNumber", c_int),
+ ("iDeviceNumber", c_int),
+ ("iFunctionNumber", c_int),
+ ("iVendorID", c_int),
+ ("strAdapterName_256", c_char * 256),
+ ("isPresent", c_int),
+ ("iDisplayType", c_int * 4),
+ ("iFormFactor", c_int),
+ ("iClusterInfo", c_int),
+ ("strBusString", c_char * 64),
+ ("strBusNumber", c_char * 64),
+ ("iFunction", c_int),
+ ("iRevisionID", c_int),
+ ("iExtendedRevisionID", c_int),
+ ("iMemorySizeMB", c_int),
+ ]
+
+
+class ADLTemperature(Structure):
+ _fields_ = [
+ ("iSize", c_int),
+ ("iTemperature", c_int),
+ ]
+
+
+class ADLFanSpeedValue(Structure):
+ _fields_ = [
+ ("iSize", c_int),
+ ("iSpeedType", c_int),
+ ("iFanSpeed", c_int),
+ ("iFlags", c_int),
+ ]
+
+
+class ADLPMActivity(Structure):
+ _fields_ = [
+ ("iSize", c_int),
+ ("iEngineClock", c_int),
+ ("iMemoryClock", c_int),
+ ("iVddc", c_int),
+ ("iActivityPercent", c_int),
+ ("iCurrentPerformanceLevel", c_int),
+ ("iCurrentBusSpeed", c_int),
+ ("iCurrentBusLanes", c_int),
+ ("iUnknown", c_int * 4),
+ ]
+
+
+class ADLODParameters(Structure):
+ _fields_ = [
+ ("iSize", c_int),
+ ("iEngineClock", c_int),
+ ("iMemoryClock", c_int),
+ ("iVddc", c_int),
+ ("iActivityPercent", c_int),
+ ("iCurrentPerformanceLevel", c_int),
+ ("iCurrentMinEngineClock", c_int),
+ ("iCurrentMinMemoryClock", c_int),
+ ("iCurrentMaxEngineClock", c_int),
+ ("iCurrentMaxMemoryClock", c_int),
+ ]
+
+
+# =============================================
+# ADL CONTEXT
+# =============================================
+
+class ADLContext:
+ """Manages ADL lifecycle and GPU adapter info"""
+
+ def __init__(self):
+ self.available = False
+ self.num_adapters = 0
+ self.adapters = []
+ self.adapter_names = []
+ self._context = None
+
+ def initialize(self) -> bool:
+ if not _load_adl():
+ return False
+
+ try:
+ # ADL2_Main_Control_Create(callback, enumConnectedAdapters)
+ ADL2_Main_Control_Create = _adl_dll.ADL2_Main_Control_Create
+ ADL2_Main_Control_Create.argtypes = [c_void_p, c_int]
+ ADL2_Main_Control_Create.restype = c_int
+
+ context = c_int(0)
+ result = ADL2_Main_Control_Create(None, byref(context))
+ if result != 0:
+ logger.warning(f"ADL: Main_Control_Create failed: {result}")
+ return False
+
+ self._context = context
+
+ # Get adapter count
+ ADL2_Adapter_NumberOfAdapters_Get = _adl_dll.ADL2_Adapter_NumberOfAdapters_Get
+ ADL2_Adapter_NumberOfAdapters_Get.argtypes = [c_int, POINTER(c_int)]
+ ADL2_Adapter_NumberOfAdapters_Get.restype = c_int
+
+ num = c_int(0)
+ result = ADL2_Adapter_NumberOfAdapters_Get(context.value, byref(num))
+ if result != 0:
+ logger.warning(f"ADL: NumberOfAdapters_Get failed: {result}")
+ self._destroy()
+ return False
+
+ self.num_adapters = num.value
+
+ # Query adapter info for all adapters
+ if self.num_adapters > 0:
+ self._query_adapters()
+
+ self.available = True
+ logger.info(f"ADL: Initialized โ {len(self.adapters)} adapter(s)")
+ return True
+
+ except Exception as e:
+ logger.warning(f"ADL: Init error: {e}")
+ return False
+
+ def _query_adapters(self):
+ """Get adapter info for all adapters"""
+ try:
+ ADL2_Adapter_AdapterInfo_Get = _adl_dll.ADL2_Adapter_AdapterInfo_Get
+ ADL2_Adapter_AdapterInfo_Get.argtypes = [c_int, c_void_p, POINTER(c_int)]
+ ADL2_Adapter_AdapterInfo_Get.restype = c_int
+
+ # Allocate memory via ADL
+ ADL2_Main_Memory_Alloc = _adl_dll.ADL2_Main_Memory_Alloc
+ ADL2_Main_Memory_Alloc.argtypes = [c_int, c_int, c_void_p]
+ ADL2_Main_Memory_Alloc.restype = c_int
+
+ buf_size = c_int(ctypes.sizeof(ADLAdapterInfo) * self.num_adapters)
+ buffer = ctypes.create_string_buffer(buf_size.value)
+
+ # Use our own buffer, ADL will read present adapters
+ result = ADL2_Adapter_AdapterInfo_Get(
+ self._context.value,
+ buffer,
+ byref(buf_size)
+ )
+
+ if result == 0:
+ info_array = (ADLAdapterInfo * self.num_adapters).from_buffer(buffer)
+ total = min(self.num_adapters, buf_size.value // ctypes.sizeof(ADLAdapterInfo))
+ for i in range(total):
+ info = info_array[i]
+ if info.isPresent or info.iAdapterIndex >= 0:
+ name = info.strAdapterName.decode('utf-8', errors='replace').strip()
+ if not name:
+ name = info.strAdapterName_256.decode('utf-8', errors='replace').strip()
+ self.adapters.append({
+ 'index': info.iAdapterIndex,
+ 'name': name,
+ })
+ self.adapter_names.append(name)
+
+ # Fallback: if no adapters found, try ADL2_Adapter_AdapterInfo_Get_All
+ if len(self.adapters) == 0:
+ self._query_adapters_fallback()
+
+ except Exception as e:
+ logger.warning(f"ADL: AdapterInfo error: {e}")
+ self.adapters = [{'index': 0, 'name': 'AMD GPU 0'}]
+ self.adapter_names = ['AMD GPU 0']
+
+ def _query_adapters_fallback(self):
+ """Fallback: just create virtual adapters based on count"""
+ for i in range(self.num_adapters):
+ self.adapters.append({'index': i, 'name': f'AMD GPU {i}'})
+ self.adapter_names.append(f'AMD GPU {i}')
+
+ def get_temperature(self, adapter_index: int) -> float:
+ """Get GPU temperature in Celsius"""
+ try:
+ ADL2_Overdrive6_Temperature_Get = _adl_dll.ADL2_Overdrive6_Temperature_Get
+ ADL2_Overdrive6_Temperature_Get.argtypes = [c_int, c_int, POINTER(ADLTemperature)]
+ ADL2_Overdrive6_Temperature_Get.restype = c_int
+
+ temp = ADLTemperature()
+ temp.iSize = ctypes.sizeof(ADLTemperature)
+ result = ADL2_Overdrive6_Temperature_Get(self._context.value, adapter_index, byref(temp))
+ if result == 0:
+ return temp.iTemperature / 1000.0
+ except:
+ pass
+ return 0.0
+
+ def get_fan_speed(self, adapter_index: int) -> int:
+ """Get GPU fan speed in percent"""
+ try:
+ ADL2_Overdrive6_FanSpeed_Get = _adl_dll.ADL2_Overdrive6_FanSpeed_Get
+ ADL2_Overdrive6_FanSpeed_Get.argtypes = [c_int, c_int, POINTER(ADLFanSpeedValue)]
+ ADL2_Overdrive6_FanSpeed_Get.restype = c_int
+
+ fan = ADLFanSpeedValue()
+ fan.iSize = ctypes.sizeof(ADLFanSpeedValue)
+ fan.iSpeedType = 0 # ADL_DL_FANCTRL_SPEED_TYPE_PERCENT
+ result = ADL2_Overdrive6_FanSpeed_Get(self._context.value, adapter_index, byref(fan))
+ if result == 0:
+ return fan.iFanSpeed
+ except:
+ pass
+ return 0
+
+ def get_activity(self, adapter_index: int):
+ """Get GPU activity: utilization, engine clock, memory clock"""
+ try:
+ ADL2_Overdrive6_CurrentActivity_Get = _adl_dll.ADL2_Overdrive6_CurrentActivity_Get
+ ADL2_Overdrive6_CurrentActivity_Get.argtypes = [c_int, c_int, POINTER(ADLPMActivity)]
+ ADL2_Overdrive6_CurrentActivity_Get.restype = c_int
+
+ activity = ADLPMActivity()
+ activity.iSize = ctypes.sizeof(ADLPMActivity)
+ result = ADL2_Overdrive6_CurrentActivity_Get(self._context.value, adapter_index, byref(activity))
+ if result == 0:
+ return {
+ 'utilization': activity.iActivityPercent,
+ 'engine_clock': activity.iEngineClock,
+ 'memory_clock': activity.iMemoryClock,
+ }
+ except:
+ pass
+ return {'utilization': 0, 'engine_clock': 0, 'memory_clock': 0}
+
+ def get_memory_info(self, adapter_index: int):
+ """Get VRAM size in MB"""
+ # Memory info from adapter info
+ for adapter in self.adapters:
+ if adapter['index'] == adapter_index:
+ # We stored the adapter index, but not memory size directly
+ pass
+ return 0
+
+ def _destroy(self):
+ """Clean up ADL context"""
+ try:
+ if self._context is not None:
+ ADL2_Main_Control_Destroy = _adl_dll.ADL2_Main_Control_Destroy
+ ADL2_Main_Control_Destroy.argtypes = [c_int]
+ ADL2_Main_Control_Destroy.restype = c_int
+ ADL2_Main_Control_Destroy(self._context.value)
+ self._context = None
+ except:
+ pass
+
+ def close(self):
+ self._destroy()
+
+
+# =============================================
+# GLOBAL ADL INSTANCE
+# =============================================
+
+_adl_context = None
+
+
+def get_adl_context() -> ADLContext:
+ global _adl_context
+ if _adl_context is None:
+ ctx = ADLContext()
+ if ctx.initialize():
+ _adl_context = ctx
+ else:
+ _adl_context = ctx # Return anyway, .available will be False
+ return _adl_context
+
+
+# =============================================
+# TEST
+# =============================================
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.DEBUG)
+ ctx = get_adl_context()
+ if ctx.available:
+ print(f"ADL: {len(ctx.adapters)} adapter(s)")
+ for i, adapter in enumerate(ctx.adapters):
+ print(f" GPU {i}: {adapter['name']} (index={adapter['index']})")
+
+ temp = ctx.get_temperature(adapter['index'])
+ fan = ctx.get_fan_speed(adapter['index'])
+ activity = ctx.get_activity(adapter['index'])
+
+ print(f" Temp: {temp:.1f}ยฐC")
+ print(f" Fan: {fan}%")
+ print(f" Utilization: {activity['utilization']}%")
+ print(f" Engine Clock: {activity['engine_clock']} MHz")
+ print(f" Memory Clock: {activity['memory_clock']} MHz")
+ else:
+ print("ADL: Not available")
\ No newline at end of file
diff --git a/utils/amd_gpu_counter.py b/utils/amd_gpu_counter.py
new file mode 100644
index 0000000..98339ef
--- /dev/null
+++ b/utils/amd_gpu_counter.py
@@ -0,0 +1,255 @@
+"""
+GPU Performance Counters via PowerShell
+========================================
+Windows-only: reads GPU utilization, VRAM usage, and temperature
+via built-in performance counters and WMI.
+No dependencies required beyond built-in Python + PowerShell.
+"""
+
+import subprocess
+import logging
+import platform
+import json
+import re
+
+logger = logging.getLogger(__name__)
+
+
+class GPUCounterMonitor:
+ """Monitor GPU metrics via PowerShell performance counters and WMI"""
+
+ def __init__(self):
+ self.available = False
+ self.gpu_count = 0
+ self.gpu_names = []
+ self.method = "powershell-counters"
+ self._detect()
+
+ def _detect(self):
+ """Detect GPU availability via WMI"""
+ if platform.system() != "Windows":
+ return
+
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject Win32_VideoController | "
+ "Where-Object { $_.AdapterRAM -gt 0 } | "
+ "Select-Object Name, AdapterRAM | ConvertTo-Json"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
+ if result.returncode == 0 and result.stdout.strip():
+ data = json.loads(result.stdout.strip())
+ if isinstance(data, dict):
+ data = [data]
+ for gpu in data:
+ name = gpu.get('Name', 'Unknown GPU').strip()
+ self.gpu_names.append(name)
+ self.gpu_count = len(self.gpu_names)
+ if self.gpu_count > 0:
+ self.available = True
+ logger.info(f"GPU Counters: {self.gpu_count} GPU(s): {self.gpu_names}")
+ return
+ except Exception as e:
+ logger.debug(f"GPU detection error: {e}")
+
+ def get_gpu_utilization(self, gpu_id: int = 0) -> float:
+ """Get GPU utilization % via performance counters"""
+ try:
+ # Get average 3D engine usage
+ ps_cmd = (
+ "Get-Counter '\\GPU Engine(*engtype_3D)\\*' -SampleInterval 0 -MaxSamples 1 "
+ "2>$null | Select-Object -ExpandProperty CounterSamples | "
+ "Measure-Object -Property CookedValue -Average | "
+ "Select-Object -ExpandProperty Average"
+ )
+ result = subprocess.run(
+ ["powershell", "-NoProfile", "-Command", ps_cmd],
+ capture_output=True, text=True, timeout=3
+ )
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ if val > 0:
+ return val
+ except Exception as e:
+ logger.debug(f"GPU utilization error: {e}")
+ return 0.0
+
+ def _detect_hardware_monitors(self):
+ """Detect which hardware monitoring tools are installed"""
+ monitors = []
+
+ # LibreHardwareMonitor / OpenHardwareMonitor WMI namespace
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject -Namespace 'root/LibreHardwareMonitor' -Class Sensor 2>$null | "
+ "Select-Object -First 1 | ConvertTo-Json"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3)
+ if result.returncode == 0 and result.stdout.strip() and result.stdout.strip() != 'null':
+ monitors.append("LibreHardwareMonitor")
+ except:
+ pass
+
+ if 'LibreHardwareMonitor' not in monitors:
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject -Namespace 'root/OpenHardwareMonitor' -Class Sensor 2>$null | "
+ "Select-Object -First 1 | ConvertTo-Json"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3)
+ if result.returncode == 0 and result.stdout.strip() and result.stdout.strip() != 'null':
+ monitors.append("OpenHardwareMonitor")
+ except:
+ pass
+
+ # MSI Afterburner shared memory
+ try:
+ import ctypes
+ try:
+ # Try to open the shared memory file
+ import ctypes.wintypes
+ handle = ctypes.windll.kernel32.OpenFileMappingW(
+ 0x0001, False, "MSE_Afterburner_SharedMemory"
+ )
+ if handle:
+ monitors.append("MSIAfterburner")
+ ctypes.windll.kernel32.CloseHandle(handle)
+ except:
+ pass
+ except:
+ pass
+
+ self._hw_monitors = monitors
+ if monitors:
+ logger.info(f"GPU Counters: Detected HW monitors: {monitors}")
+ return monitors
+
+ def get_gpu_temperature(self, gpu_id: int = 0) -> float:
+ """Get GPU temperature via LibreHardwareMonitor, OpenHardwareMonitor, or other tools"""
+ # Try LibreHardwareMonitor first (if installed)
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject -Namespace 'root/LibreHardwareMonitor' -Class Sensor "
+ "2>$null | Where-Object { $_.SensorType -eq 'Temperature' -and "
+ "$_.Name -match 'GPU' } | Select-Object -First 1 -ExpandProperty Value"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3, creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ if val > 0:
+ return val
+ except:
+ pass
+
+ # Try OpenHardwareMonitor
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject -Namespace 'root/OpenHardwareMonitor' -Class Sensor "
+ "2>$null | Where-Object { $_.SensorType -eq 'Temperature' -and "
+ "$_.Name -match 'GPU' } | Select-Object -First 1 -ExpandProperty Value"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3, creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ if val > 0:
+ return val
+ except:
+ pass
+
+ return 0.0
+
+ def get_fan_speed(self, gpu_id: int = 0) -> int:
+ """Get fan speed from hardware monitors"""
+ # LibreHardwareMonitor
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject -Namespace 'root/LibreHardwareMonitor' -Class Sensor "
+ "2>$null | Where-Object { $_.SensorType -eq 'Fan' -and "
+ "$_.Name -match 'GPU' } | Select-Object -First 1 -ExpandProperty Value"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3, creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ if val > 0:
+ return int(val)
+ except:
+ pass
+
+ # OpenHardwareMonitor
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject -Namespace 'root/OpenHardwareMonitor' -Class Sensor "
+ "2>$null | Where-Object { $_.SensorType -eq 'Fan' -and "
+ "$_.Name -match 'GPU' } | Select-Object -First 1 -ExpandProperty Value"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3, creationflags=subprocess.CREATE_NO_WINDOW)
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ if val > 0:
+ return int(val)
+ except:
+ pass
+
+ return 0
+
+ def get_gpu_vram_mb(self, gpu_id: int = 0) -> dict:
+ """Get VRAM info from WMI"""
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "(Get-WmiObject Win32_VideoController | Select-Object -First 1).AdapterRAM"
+ ]
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=3)
+ if result.returncode == 0 and result.stdout.strip():
+ total_bytes = int(result.stdout.strip())
+ total_mb = total_bytes / (1024 * 1024)
+ # We can't get actual VRAM usage without ADLX/ADL
+ # Estimate via process memory if needed
+ return {'total_mb': total_mb, 'used_mb': 0, 'free_mb': total_mb}
+ except:
+ pass
+ return {'total_mb': 0, 'used_mb': 0, 'free_mb': 0}
+
+ def get_all_metrics(self, gpu_id: int = 0) -> dict:
+ """Get all metrics for a GPU in one call"""
+ return {
+ 'utilization': self.get_gpu_utilization(gpu_id),
+ 'temperature': self.get_gpu_temperature(gpu_id),
+ 'vram': self.get_gpu_vram_mb(gpu_id),
+ 'fan_speed': self.get_fan_speed(gpu_id),
+ 'gpu_name': self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"GPU {gpu_id}",
+ }
+
+
+# Singleton
+_gpu_counter = None
+
+
+def get_gpu_counter() -> GPUCounterMonitor:
+ global _gpu_counter
+ if _gpu_counter is None:
+ _gpu_counter = GPUCounterMonitor()
+ return _gpu_counter
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.DEBUG)
+ mon = get_gpu_counter()
+ if mon.available:
+ print(f"GPU Counters: {mon.gpu_count} GPU(s)")
+ for i, name in enumerate(mon.gpu_names):
+ print(f"\n GPU {i}: {name}")
+ metrics = mon.get_all_metrics(i)
+ print(f" Utilization: {metrics['utilization']:.1f}%")
+ print(f" Temperature: {metrics['temperature']:.1f}ยฐC")
+ print(f" VRAM: {metrics['vram']['total_mb']:.0f} MB total")
+ print(f" Fan: {metrics['fan_speed']}%")
+ else:
+ print("GPU Counters: Not available")
\ No newline at end of file
diff --git a/utils/amd_process_monitor.py b/utils/amd_process_monitor.py
new file mode 100644
index 0000000..cdea3b1
--- /dev/null
+++ b/utils/amd_process_monitor.py
@@ -0,0 +1,337 @@
+"""
+AMD Process Monitor
+===================
+Monitors ComfyUI Python process memory/GPU usage during generations.
+Auto-detects when generation starts/ends and records peak metrics.
+"""
+
+import logging
+import time
+import threading
+from dataclasses import dataclass, field
+from typing import Optional, List
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class GenerationRecord:
+ """Stats for a single generation run"""
+ timestamp: float
+ duration: float # seconds
+ vram_peak_mb: float = 0
+ vram_start_mb: float = 0
+ vram_end_mb: float = 0
+ vram_delta_mb: float = 0 # peak - start
+ ram_peak_mb: float = 0
+ ram_start_mb: float = 0
+ ram_end_mb: float = 0
+ cpu_peak: float = 0 # CPU utilization peak during generation
+ error: Optional[str] = None
+
+
+@dataclass
+class ProcessSnapshot:
+ """Snapshot of a process at a point in time"""
+ timestamp: float
+ memory_rss_mb: float
+ memory_vms_mb: float
+ cpu_percent: float
+ num_threads: int
+
+
+class ComfyProcessMonitor:
+ """Monitors the ComfyUI main process for generation metrics"""
+
+ def __init__(self, poll_interval: float = 0.5):
+ self.poll_interval = poll_interval
+ self._process = None
+ self._process_pid = None
+ self._running = False
+ self._thread = None
+
+ # Generation detection
+ self._is_generating = False
+ self._gen_start_time = 0
+ self._current_record = None
+ self._prev_memory_mb = 0
+
+ # History
+ self.history: List[GenerationRecord] = []
+ self.current: Optional[GenerationRecord] = None
+
+ # Snapshots during current generation (for graph)
+ self._snapshots: List[ProcessSnapshot] = []
+
+ # Mutex for thread safety
+ self._lock = threading.Lock()
+
+ # Threshold tuning โ lebih sensitif
+ self._MEMORY_START_THRESHOLD_MB = 50 # sebelumnya 100
+ self._CPU_START_THRESHOLD = 15 # sebelumnya 20
+ self._MEMORY_END_THRESHOLD_MB = -30 # sebelumnya -50
+ self._CPU_END_THRESHOLD = 8 # sebelumnya 10
+
+ def find_comfy_process(self) -> bool:
+ """Find the ComfyUI main process"""
+ try:
+ import psutil
+ current_pid = None
+
+ # Method 1: Current process could be the ComfyUI worker
+ proc = psutil.Process()
+ proc_name = proc.name().lower()
+
+ # Check if we're running inside ComfyUI
+ cmdline = proc.cmdline()
+ cmdline_str = ' '.join(cmdline).lower()
+ if 'comfyui' in cmdline_str or 'main.py' in cmdline_str or 'comfy' in cmdline_str:
+ current_pid = proc.pid
+
+ # Method 2: Find by name
+ if not current_pid:
+ for p in psutil.process_iter(['pid', 'name', 'cmdline']):
+ try:
+ name = (p.info['name'] or '').lower()
+ cl = ' '.join(p.info['cmdline'] or []).lower()
+ if ('python' in name or 'python' in cl) and ('comfy' in cl or 'main.py' in cl):
+ current_pid = p.info['pid']
+ break
+ # Also catch 'nodes' or 'comfy' in path
+ if ('python' in name) and ('comfy' in cl):
+ current_pid = p.info['pid']
+ break
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+
+ if current_pid:
+ self._process_pid = current_pid
+ self._process = psutil.Process(current_pid)
+ logger.info(f"Process Monitor: Found ComfyUI PID={current_pid}")
+ return True
+
+ logger.warning("Process Monitor: Could not find ComfyUI process")
+ return False
+
+ except ImportError:
+ logger.warning("Process Monitor: psutil not installed")
+ return False
+ except Exception as e:
+ logger.warning(f"Process Monitor: Find error: {e}")
+ return False
+
+ def start_monitoring(self):
+ """Start background polling thread"""
+ if self._running:
+ return
+ if not self.find_comfy_process():
+ return
+
+ self._running = True
+ self._thread = threading.Thread(target=self._poll_loop, daemon=True)
+ self._thread.start()
+ logger.info("Process Monitor: Started monitoring")
+
+ def stop_monitoring(self):
+ """Stop background polling"""
+ self._running = False
+ if self._thread:
+ self._thread.join(timeout=2)
+ self._thread = None
+
+ def _poll_loop(self):
+ """Main polling loop"""
+ try:
+ import psutil
+ except ImportError:
+ self._running = False
+ return
+
+ while self._running:
+ try:
+ if not self._process or not self._process.is_running():
+ logger.warning("Process Monitor: Process died, restarting...")
+ if not self.find_comfy_process():
+ time.sleep(self.poll_interval * 10)
+ continue
+
+ # Get memory info
+ mem_info = self._process.memory_info()
+ cpu_percent = self._process.cpu_percent(interval=0)
+ rss_mb = mem_info.rss / (1024 * 1024)
+ vms_mb = mem_info.vms / (1024 * 1024)
+ num_threads = self._process.num_threads()
+
+ snapshot = ProcessSnapshot(
+ timestamp=time.time(),
+ memory_rss_mb=rss_mb,
+ memory_vms_mb=vms_mb,
+ cpu_percent=cpu_percent,
+ num_threads=num_threads,
+ )
+
+ # Detect generation: significant & sustained memory increase
+ mem_delta = rss_mb - self._prev_memory_mb
+
+ with self._lock:
+ if not self._is_generating:
+ # Start condition: tuned thresholds
+ if mem_delta > self._MEMORY_START_THRESHOLD_MB or cpu_percent > self._CPU_START_THRESHOLD:
+ self._is_generating = True
+ self._gen_start_time = time.time()
+ self._current_record = GenerationRecord(
+ timestamp=self._gen_start_time,
+ duration=0,
+ vram_start_mb=rss_mb,
+ vram_peak_mb=rss_mb,
+ ram_start_mb=rss_mb,
+ ram_peak_mb=rss_mb,
+ )
+ self._snapshots = [snapshot]
+ logger.info(
+ f"Process Monitor: Generation START "
+ f"(RAM: {rss_mb:.0f}MB, CPU: {cpu_percent:.1f}%)"
+ )
+ else:
+ # In middle of generation โ track peak
+ if self._current_record:
+ self._current_record.vram_peak_mb = max(
+ self._current_record.vram_peak_mb, rss_mb
+ )
+ self._current_record.ram_peak_mb = max(
+ self._current_record.ram_peak_mb, rss_mb
+ )
+ self._current_record.cpu_peak = max(
+ self._current_record.cpu_peak, cpu_percent
+ )
+ self._snapshots.append(snapshot)
+
+ # End condition: tuned thresholds
+ if mem_delta < self._MEMORY_END_THRESHOLD_MB and cpu_percent < self._CPU_END_THRESHOLD:
+ self._finalize_generation(rss_mb)
+ logger.info(
+ f"Process Monitor: Generation END "
+ f"(duration={self.current.duration:.1f}s, "
+ f"peak RAM={self.current.ram_peak_mb:.0f}MB)"
+ )
+
+ self._prev_memory_mb = rss_mb
+
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ logger.warning("Process Monitor: Process access error")
+ time.sleep(self.poll_interval * 5)
+ except Exception as e:
+ logger.debug(f"Process Monitor: Poll error: {e}")
+
+ time.sleep(self.poll_interval)
+
+ def _finalize_generation(self, end_memory_mb: float):
+ """Complete a generation record"""
+ if not self._current_record:
+ return
+
+ self._current_record.duration = time.time() - self._gen_start_time
+ self._current_record.vram_end_mb = end_memory_mb
+ self._current_record.ram_end_mb = end_memory_mb
+ self._current_record.vram_delta_mb = (
+ self._current_record.vram_peak_mb - self._current_record.vram_start_mb
+ )
+
+ self.current = self._current_record
+ self.history.append(self._current_record)
+
+ # Keep last 50 records
+ if len(self.history) > 50:
+ self.history = self.history[-50:]
+
+ # Reset
+ self._is_generating = False
+ self._current_record = None
+ self._snapshots = []
+
+ def get_current_generation(self) -> GenerationRecord:
+ """Get current in-progress generation record"""
+ with self._lock:
+ if self._is_generating and self._current_record:
+ return self._current_record
+ return GenerationRecord(timestamp=0, duration=0)
+
+ def get_last_generation(self) -> Optional[GenerationRecord]:
+ """Get the most recent finished generation"""
+ if self.history:
+ return self.history[-1]
+ return None
+
+ def get_summary(self) -> str:
+ """Get a text summary of all generations"""
+ if not self.history:
+ return "No generations recorded yet."
+
+ lines = [f"=== ComfyUI Process Monitor ==="]
+ total_time = 0
+ peak_vram = 0
+ peak_ram = 0
+
+ for i, gen in enumerate(self.history[-5:]): # Last 5
+ total_time += gen.duration
+ peak_vram = max(peak_vram, gen.vram_peak_mb)
+ peak_ram = max(peak_ram, gen.ram_peak_mb)
+
+ lines.append(
+ f" #{i + 1}: {gen.duration:.1f}s "
+ f"| RAM: {gen.ram_start_mb:.0f}โ{gen.ram_peak_mb:.0f}โ{gen.ram_end_mb:.0f}MB"
+ )
+ if gen.vram_delta_mb > 0:
+ lines[-1] += f" (+{gen.vram_delta_mb:.0f}MB peak)"
+
+ lines.append(
+ f" --- Total: {total_time:.1f}s | "
+ f"Peak RAM: {peak_ram:.0f}MB | "
+ f"Peak VRAM: {peak_vram:.0f}MB"
+ )
+
+ return "\n".join(lines)
+
+ @property
+ def is_generating(self) -> bool:
+ return self._is_generating
+
+
+# Singleton
+_process_monitor = None
+
+
+def get_process_monitor() -> ComfyProcessMonitor:
+ global _process_monitor
+ if _process_monitor is None:
+ _process_monitor = ComfyProcessMonitor()
+ return _process_monitor
+
+
+if __name__ == "__main__":
+ logging.basicConfig(level=logging.INFO)
+ mon = get_process_monitor()
+ mon.start_monitoring()
+
+ print("Monitoring ComfyUI process...")
+ print("Run a generation in ComfyUI to see stats.")
+ print("Press Ctrl+C to stop and see summary.\n")
+
+ try:
+ while True:
+ time.sleep(5)
+ if mon.is_generating:
+ gen = mon.get_current_generation()
+ print(f" โก Generating... {gen.duration:.1f}s | "
+ f"Peak: {gen.vram_peak_mb:.0f}MB")
+ else:
+ last = mon.get_last_generation()
+ if last:
+ print(f" Last gen: {last.duration:.1f}s | "
+ f"Peak: {last.vram_peak_mb:.0f}MB | "
+ f"Delta: {last.vram_delta_mb:.0f}MB")
+ except KeyboardInterrupt:
+ pass
+ finally:
+ mon.stop_monitoring()
+ print("\n" + mon.get_summary())
\ No newline at end of file
diff --git a/utils/amd_utils.py b/utils/amd_utils.py
index e61f5f8..beb349c 100644
--- a/utils/amd_utils.py
+++ b/utils/amd_utils.py
@@ -1,7 +1,16 @@
+"""
+AMD Utils - GPU Monitoring Backend
+===================================
+Multi-platform AMD GPU monitoring.
+Supports: Linux (ROCm/pyrsmi), Windows (ADL/ADLX, WMI), Fallback (psutil)
+"""
+
import platform
import logging
+import subprocess
+import re
from dataclasses import dataclass
-from typing import Optional
+from typing import Optional, List
logger = logging.getLogger(__name__)
@@ -9,14 +18,18 @@ logger = logging.getLogger(__name__)
@dataclass
class AMDGPUStats:
gpu_id: int
- utilization_gpu: float
- utilization_memory: float
- memory_total: int # Bytes
- memory_used: int # Bytes
- memory_free: int # Bytes
- temperature: float
- fan_speed: int
- is_available: bool
+ gpu_name: str = ""
+ utilization_gpu: float = 0.0
+ utilization_memory: float = 0.0
+ memory_total: int = 0 # Bytes
+ memory_used: int = 0 # Bytes
+ memory_free: int = 0 # Bytes
+ temperature: float = 0.0 # Celsius
+ fan_speed: int = 0 # Percent
+ core_clock: int = 0 # MHz
+ memory_clock: int = 0 # MHz
+ power_draw: float = 0.0 # Watts
+ is_available: bool = True
error_message: Optional[str] = None
@@ -25,13 +38,14 @@ class AMDMonitor:
self.available = False
self.method = None
self.gpu_count = 0
+ self.gpu_names: List[str] = []
self._backend = None
self._initialize()
def _initialize(self):
system = platform.system()
-
- # --- LINUX IMPLEMENTATION (ROCm) ---
+
+ # --- LINUX: ROCm / pyrsmi ---
if system == "Linux":
try:
import pyrsmi
@@ -40,86 +54,240 @@ class AMDMonitor:
self.method = "pyrsmi"
self.available = True
self.gpu_count = pyrsmi.rocm_smi_get_num_devices()
- logger.info(f"AMD Monitor: Linux detected, using pyrsmi. Found {self.gpu_count} GPUs.")
+ self.gpu_names = [f"AMD GPU {i}" for i in range(self.gpu_count)]
+ logger.info(f"AMD Monitor: ROCm/pyrsmi โ {self.gpu_count} GPU(s)")
return
except ImportError:
- logger.warning("Linux detected but 'pyrsmi' not installed.")
+ logger.debug("Linux: pyrsmi not installed")
except Exception as e:
- logger.warning(f"Linux pyrsmi init failed: {e}")
+ logger.warning(f"Linux pyrsmi: {e}")
- # --- WINDOWS IMPLEMENTATION (ADL) ---
+ # --- WINDOWS ---
elif system == "Windows":
- try:
- import pyadl
- self._backend = pyadl
-
- # Try init without custom allocator (safer)
- try:
- res = pyadl.ADL2_Main_Control_Create(0) # Use default allocator
- except:
- res = 0 # Assume success if function doesn't exist
-
- if res == 0:
- adapters = pyadl.ADL2_Adapter_AdapterInfo_Get_All()
- self.gpu_count = len([a for a in adapters if getattr(a, 'is_present', True)])
- self.method = "pyadl"
- self.available = True
- logger.info(f"AMD Monitor: Windows + pyadl. Found {self.gpu_count} GPUs.")
- return
- except ImportError:
- logger.warning("Windows: pyadl not installed")
- except Exception as e:
- logger.warning(f"Windows pyadl error: {e}")
-
- # Fallback: Try psutil for basic system info
- try:
- import psutil
- self.method = "psutil-fallback"
- self.available = True # Partial availability
- self.gpu_count = 1 # Assume 1 GPU for fallback
- logger.info("AMD Monitor: Using psutil fallback (limited AMD info)")
+ # Try ADLX
+ if self._try_adlx():
return
- except ImportError:
- pass
+ # Try pyadl
+ if self._try_pyadl():
+ return
+ # WMI-based detection + monitoring (best fallback on Windows)
+ if self._try_wmi():
+ return
+ # Last resort: psutil
+ self._try_psutil()
- logger.error("No AMD monitoring backend available. Check requirements.")
+ if self.available:
+ return
+
+ if not self.available:
+ logger.error(f"AMD Monitor: No backend available on {system}")
+
+ # ==============================================
+ # BACKEND DETECTION
+ # ==============================================
+
+ def _try_adlx(self):
+ try:
+ import ctypes
+ paths = [
+ "C:/Program Files/AMD/ADLX/lib/ADLX.dll",
+ "C:/Program Files/AMD/ADLX/bin/ADLX.dll",
+ ]
+ for path in paths:
+ try:
+ self._adlx_dll = ctypes.CDLL(path)
+ self.method = "adlx"
+ self.available = True
+ self._detect_gpu_names_wmi() or self.gpu_names.__setitem__(slice(None), ["AMD GPU 0"])
+ self.gpu_count = max(len(self.gpu_names), 1)
+ logger.info(f"AMD Monitor: ADLX at {path}")
+ return True
+ except:
+ continue
+ except:
+ pass
+ return False
+
+ def _try_pyadl(self):
+ try:
+ import pyadl
+ self._backend = pyadl
+ try:
+ pyadl.ADL2_Main_Control_Create(0)
+ except:
+ pass
+ adapters = pyadl.ADL2_Adapter_AdapterInfo_Get_All()
+ present = [a for a in adapters if getattr(a, 'is_present', True)]
+ self.gpu_count = len(present)
+ self.gpu_names = [getattr(a, 'strAdapterName', f'AMD GPU {i}') for i, a in enumerate(present)]
+ self.method = "pyadl"
+ self.available = True
+ logger.info(f"AMD Monitor: pyadl โ {self.gpu_count} GPU(s): {self.gpu_names}")
+ return True
+ except ImportError:
+ pass
+ except AttributeError as e:
+ logger.debug(f"pyadl API mismatch: {e}")
+ except Exception as e:
+ logger.warning(f"pyadl error: {e}")
+ return False
+
+ def _try_wmi(self):
+ """WMI-based detection + monitoring on Windows"""
+ # Import the GPU counter monitor module
+ success = self._detect_gpu_names_wmi()
+ if not success or self.gpu_count == 0:
+ return False
+
+ # Try to use the PowerShell counter backend for real utilization data
+ try:
+ from .amd_gpu_counter import get_gpu_counter
+ counter_mon = get_gpu_counter()
+ if counter_mon.available:
+ self._counter_mon = counter_mon
+ self.method = "powershell-counters"
+ self.available = True
+ # Use names from counter monitor (more accurate)
+ if counter_mon.gpu_names:
+ self.gpu_names = counter_mon.gpu_names
+ self.gpu_count = len(self.gpu_names)
+ logger.info(f"AMD Monitor: PowerShell counters โ {self.gpu_count} GPU(s): {self.gpu_names}")
+ return True
+ except Exception as e:
+ logger.debug(f"GPU counter import failed: {e}")
+
+ # Fallback: basic WMI with no real-time metrics
+ self.method = "wmi-basic"
+ self.available = True
+ logger.info(f"AMD Monitor: WMI basic โ {self.gpu_count} GPU(s): {self.gpu_names}")
+ return True
+
+ def _try_psutil(self):
+ try:
+ import psutil
+ self.method = "psutil-fallback"
+ self.available = True
+ self.gpu_count = 1
+ self.gpu_names = ["System RAM (no GPU backend)"]
+ logger.info("AMD Monitor: psutil-fallback")
+ except ImportError:
+ pass
+
+ def _detect_gpu_names_wmi(self) -> bool:
+ """Use PowerShell to get AMD GPU names via WMI"""
+ try:
+ cmd = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject Win32_VideoController | "
+ "Where-Object { $_.Name -match 'AMD|Radeon|RDNA' } | "
+ "Select-Object -ExpandProperty Name"
+ ]
+ result = subprocess.run(
+ cmd, capture_output=True, text=True, timeout=5
+ )
+ if result.returncode == 0 and result.stdout.strip():
+ names = [n.strip() for n in result.stdout.strip().split('\n') if n.strip()]
+ if names:
+ self.gpu_names = names
+ self.gpu_count = len(names)
+ return True
+
+ # Fallback: get ALL video controllers
+ cmd2 = [
+ "powershell", "-NoProfile", "-Command",
+ "Get-WmiObject Win32_VideoController | Select-Object -ExpandProperty Name"
+ ]
+ result2 = subprocess.run(
+ cmd2, capture_output=True, text=True, timeout=5
+ )
+ if result2.returncode == 0 and result2.stdout.strip():
+ names = [n.strip() for n in result2.stdout.strip().split('\n') if n.strip()]
+ if names:
+ self.gpu_names = names
+ self.gpu_count = len(names)
+ return True
+ except Exception as e:
+ logger.debug(f"WMI detection error: {e}")
+ return False
+
+ # ==============================================
+ # GET STATS
+ # ==============================================
def get_gpu_stats(self, gpu_id: int = 0) -> AMDGPUStats:
if not self.available:
- return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, "Backend not available")
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ gpu_name="",
+ is_available=False,
+ error_message="Backend not available"
+ )
try:
if self.method == "pyrsmi":
return self._get_stats_linux(gpu_id)
+ elif self.method == "adlx":
+ return self._get_stats_adlx(gpu_id)
elif self.method == "pyadl":
- return self._get_stats_windows(gpu_id)
+ return self._get_stats_pyadl(gpu_id)
+ elif self.method in ("wmi", "wmi-basic"):
+ return self._get_stats_wmi(gpu_id)
+ elif self.method == "powershell-counters":
+ return self._get_stats_powershell(gpu_id)
elif self.method == "psutil-fallback":
- return self._get_stats_windows_fallback(gpu_id)
+ return self._get_stats_psutil(gpu_id)
except Exception as e:
- return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, str(e))
-
- def _get_stats_linux(self, gpu_id: int) -> AMDGPUStats:
- rsmi = self._backend
- # Utilization
- util = rsmi.rocm_smi_get_gpu_utilization(gpu_id)
-
- # Memory (ROCm returns KB, convert to Bytes)
- mem_total_kb = rsmi.rocm_smi_get_gpu_memory_total(gpu_id)
- mem_used_kb = rsmi.rocm_smi_get_gpu_memory_used(gpu_id)
-
- mem_total = mem_total_kb * 1024
- mem_used = mem_used_kb * 1024
- mem_free = mem_total - mem_used
- mem_util = (mem_used / mem_total) * 100 if mem_total > 0 else 0
-
- # Temp (ROCm returns mC, convert to C)
- temp = rsmi.rocm_smi_get_temp(gpu_id, 0) / 1000.0
-
- # Fan
- fan = rsmi.rocm_smi_get_fan_speed(gpu_id, 0)
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ gpu_name=self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else "",
+ is_available=False,
+ error_message=str(e)
+ )
return AMDGPUStats(
gpu_id=gpu_id,
+ gpu_name=self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else "",
+ is_available=False,
+ error_message="No matching backend handler"
+ )
+
+ def get_all_gpu_stats(self) -> List[AMDGPUStats]:
+ if not self.available or self.gpu_count == 0:
+ return []
+ return [self.get_gpu_stats(i) for i in range(self.gpu_count)]
+
+ # ==============================================
+ # BACKEND: Linux / ROCm
+ # ==============================================
+
+ def _get_stats_linux(self, gpu_id: int) -> AMDGPUStats:
+ rsmi = self._backend
+
+ util = float(rsmi.rocm_smi_get_gpu_utilization(gpu_id))
+ mem_total_kb = float(rsmi.rocm_smi_get_gpu_memory_total(gpu_id))
+ mem_used_kb = float(rsmi.rocm_smi_get_gpu_memory_used(gpu_id))
+ mem_total = int(mem_total_kb * 1024)
+ mem_used = int(mem_used_kb * 1024)
+ mem_free = mem_total - mem_used
+ mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
+ temp = float(rsmi.rocm_smi_get_temp(gpu_id, 0)) / 1000.0
+ fan = int(rsmi.rocm_smi_get_fan_speed(gpu_id, 0))
+
+ core_clk = 0
+ mem_clk = 0
+ power = 0.0
+ try:
+ core_clk = int(rsmi.rocm_smi_get_gpu_clk_freq(gpu_id, 0))
+ mem_clk = int(rsmi.rocm_smi_get_gpu_clk_freq(gpu_id, 1))
+ power = float(rsmi.rocm_smi_get_power_avg(gpu_id)) / 1000.0
+ except:
+ pass
+
+ name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
+
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ gpu_name=name,
utilization_gpu=util,
utilization_memory=mem_util,
memory_total=mem_total,
@@ -127,84 +295,293 @@ class AMDMonitor:
memory_free=mem_free,
temperature=temp,
fan_speed=fan,
+ core_clock=core_clk,
+ memory_clock=mem_clk,
+ power_draw=power,
is_available=True
)
- def _get_stats_windows(self, gpu_id: int) -> AMDGPUStats:
- adl = self._backend
- adapters = adl.ADL2_Adapter_AdapterInfo_Get_All()
- active_adapters = [a for a in adapters if getattr(a, 'is_present', True)]
-
- if gpu_id >= len(active_adapters):
- return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, "GPU Index out of range")
-
- adapter = active_adapters[gpu_id]
- adapter_index = adapter.adapter_index
+ # ==============================================
+ # BACKEND: Windows / WMI
+ # ==============================================
- # Utilization (Overdrive6 is for GCN and newer)
- gpu_util = 0
- try:
- usage = adl.ADL2_Overdrive6_CurrentUsage_Get(adapter_index, 0)
- gpu_util = usage.iEngineClock
- except:
- pass
+ def _get_stats_wmi(self, gpu_id: int) -> AMDGPUStats:
+ """Get GPU stats via PowerShell WMI queries"""
+ name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
- # Memory (VRAM) - ADL doesn't expose this easily
+ # Try to get GPU utilization via performance counters
+ gpu_util = 0.0
+ temp = 0.0
+ fan = 0
mem_used = 0
mem_total = 0
- mem_util = 0
-
- # Temperature
- temp = 0
+ mem_free = 0
+ mem_util = 0.0
+
try:
- temp_data = adl.ADL2_Overdrive6_Temperature_Get(adapter_index, 0)
- temp = temp_data.iTemperature / 1000.0 # ADL returns in milli-Celsius
- except:
- pass
-
- # Fan Speed
- fan = 0
- try:
- fan_data = adl.ADL2_Overdrive6_FanSpeed_Get(adapter_index, 0)
- fan = fan_data.iFanSpeedPercent
- except:
- pass
-
+ # GPU Utilization via PowerShell Performance Counters
+ ps_cmd = (
+ "Get-Counter '\\GPU Engine(*engtype_3D)\\*' -SampleInterval 0 -MaxSamples 1 "
+ "2>$null | Select-Object -ExpandProperty CounterSamples | "
+ "Measure-Object -Property CookedValue -Average | "
+ "Select-Object -ExpandProperty Average"
+ )
+ result = subprocess.run(
+ ["powershell", "-NoProfile", "-Command", ps_cmd],
+ capture_output=True, text=True, timeout=3
+ )
+ if result.returncode == 0 and result.stdout.strip():
+ val = float(result.stdout.strip())
+ if val > 0:
+ gpu_util = val
+
+ # GPU Temperature via WMI (AMD-specific)
+ temp_cmd = (
+ "Get-WmiObject -Namespace 'root/OpenHardwareMonitor' -Class Sensor "
+ "2>$null | Where-Object { $_.SensorType -eq 'Temperature' -and "
+ "$_.Name -match 'GPU' -and $_.Value -gt 0 } | "
+ "Select-Object -First 1 -ExpandProperty Value"
+ )
+ result_temp = subprocess.run(
+ ["powershell", "-NoProfile", "-Command", temp_cmd],
+ capture_output=True, text=True, timeout=3
+ )
+ if result_temp.returncode == 0 and result_temp.stdout.strip():
+ temp = float(result_temp.stdout.strip())
+
+ # Fallback temperature: read from GPU registry via AMDPowerProfiling
+ if temp == 0:
+ reg_cmd = (
+ "Get-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\"
+ "{4d36e968-e325-11ce-bfc1-08002be10318}\\*' -Name 'AdpaterDesc' -ErrorAction SilentlyContinue "
+ "2>$null | Select-Object -ExpandProperty AdpaterDesc -First 1"
+ )
+
+ # Memory (VRAM) via WMI
+ vram_cmd = (
+ "Get-WmiObject Win32_VideoController | "
+ "Select-Object -First 1 -Property AdapterRAM, CurrentHorizontalResolution"
+ )
+ result_vram = subprocess.run(
+ ["powershell", "-NoProfile", "-Command",
+ "(Get-WmiObject Win32_VideoController | Select-Object -First 1).AdapterRAM"],
+ capture_output=True, text=True, timeout=3
+ )
+ if result_vram.returncode == 0 and result_vram.stdout.strip():
+ mem_total = int(result_vram.stdout.strip())
+ # We can't get precise VRAM usage without ADLX, but estimate via process
+ if mem_total > 0:
+ mem_util = psutil_vram_estimate()
+ mem_used = int(mem_total * mem_util / 100)
+ mem_free = mem_total - mem_used
+
+ except Exception as e:
+ logger.debug(f"WMI stats error: {e}")
+
+ # Fallback to system RAM estimation for VRAM
+ if mem_total == 0:
+ try:
+ import psutil
+ svmem = psutil.virtual_memory()
+ mem_total = svmem.total
+ mem_used = svmem.used
+ mem_free = svmem.available
+ mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
+ except:
+ pass
+
return AMDGPUStats(
gpu_id=gpu_id,
- utilization_gpu=gpu_util,
+ gpu_name=name,
+ utilization_gpu=gpu_util,
utilization_memory=mem_util,
memory_total=mem_total,
memory_used=mem_used,
- memory_free=0,
+ memory_free=mem_free,
temperature=temp,
fan_speed=fan,
+ core_clock=0,
+ memory_clock=0,
+ power_draw=0,
is_available=True
)
- def _get_stats_windows_fallback(self, gpu_id: int) -> AMDGPUStats:
- """Fallback using psutil when ADL fails"""
- import psutil
-
- # Basic system memory (not GPU-specific, but better than nothing)
- svmem = psutil.virtual_memory()
-
+ # ==============================================
+ # BACKEND: Windows / pyadl
+ # ==============================================
+
+ def _get_stats_pyadl(self, gpu_id: int) -> AMDGPUStats:
+ adl = self._backend
+ name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
+
+ try:
+ adapters = adl.ADL2_Adapter_AdapterInfo_Get_All()
+ active = [a for a in adapters if getattr(a, 'is_present', True)]
+ if gpu_id >= len(active):
+ return AMDGPUStats(gpu_id=gpu_id, gpu_name=name, is_available=False, error_message="GPU out of range")
+
+ idx = active[gpu_id].adapter_index
+
+ gpu_util = 0.0
+ try:
+ usage = adl.ADL2_Overdrive6_CurrentUsage_Get(idx, 0)
+ gpu_util = float(usage.iEngineClock) / 100.0
+ except:
+ pass
+
+ temp = 0.0
+ try:
+ td = adl.ADL2_Overdrive6_Temperature_Get(idx, 0)
+ temp = float(td.iTemperature) / 1000.0
+ except:
+ pass
+
+ fan = 0
+ try:
+ fd = adl.ADL2_Overdrive6_FanSpeed_Get(idx, 0)
+ fan = int(fd.iFanSpeedPercent)
+ except:
+ pass
+
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ gpu_name=name,
+ utilization_gpu=gpu_util,
+ temperature=temp,
+ fan_speed=fan,
+ is_available=True
+ )
+ except Exception as e:
+ return AMDGPUStats(gpu_id=gpu_id, gpu_name=name, is_available=False, error_message=str(e))
+
+ # ==============================================
+ # BACKEND: Windows / ADLX (placeholder)
+ # ==============================================
+
+ def _get_stats_adlx(self, gpu_id: int) -> AMDGPUStats:
+ name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}"
return AMDGPUStats(
gpu_id=gpu_id,
- utilization_gpu=0,
- utilization_memory=(svmem.used / svmem.total * 100),
+ gpu_name=name,
+ is_available=False,
+ error_message="ADLX full binding not yet implemented"
+ )
+
+ # ==============================================
+ # BACKEND: Windows / PowerShell Counters
+ # ==============================================
+
+ def _get_stats_powershell(self, gpu_id: int) -> AMDGPUStats:
+ """Get GPU stats via PowerShell performance counters"""
+ try:
+ from .amd_gpu_counter import get_gpu_counter
+ except ImportError:
+ return self._get_stats_psutil(gpu_id)
+
+ try:
+ counter = get_gpu_counter()
+ metrics = counter.get_all_metrics(gpu_id)
+
+ name = metrics['gpu_name'] or (self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else f"AMD GPU {gpu_id}")
+
+ # VRAM
+ vram = metrics['vram']
+ mem_total = int(vram['total_mb'] * 1024 * 1024) if vram['total_mb'] > 0 else 0
+ mem_used = int(vram['used_mb'] * 1024 * 1024) if vram['used_mb'] > 0 else 0
+ mem_free = int(vram['free_mb'] * 1024 * 1024) if vram['free_mb'] > 0 else mem_total
+
+ # If no VRAM used data, estimate from psutil process memory
+ if mem_used == 0 and mem_total > 0:
+ mem_util = psutil_vram_estimate()
+ mem_used = int(mem_total * mem_util / 100)
+ mem_free = mem_total - mem_used
+ elif mem_total > 0:
+ mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
+ else:
+ # No VRAM data at all โ use system RAM as fallback
+ try:
+ import psutil
+ svmem = psutil.virtual_memory()
+ mem_total = svmem.total
+ mem_used = svmem.used
+ mem_free = svmem.available
+ mem_util = (mem_used / mem_total * 100) if mem_total > 0 else 0
+ if not name:
+ name = "System RAM (GPU VRAM unavailable)"
+ except:
+ mem_util = 0
+
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ gpu_name=name,
+ utilization_gpu=metrics['utilization'],
+ utilization_memory=mem_util,
+ memory_total=mem_total,
+ memory_used=mem_used,
+ memory_free=mem_free,
+ temperature=metrics['temperature'],
+ fan_speed=metrics['fan_speed'],
+ core_clock=0,
+ memory_clock=0,
+ power_draw=0,
+ is_available=True
+ )
+ except Exception as e:
+ logger.warning(f"PowerShell counter error: {e}")
+ return self._get_stats_psutil(gpu_id)
+
+ # ==============================================
+ # BACKEND: psutil fallback
+ # ==============================================
+
+ def _get_stats_psutil(self, gpu_id: int) -> AMDGPUStats:
+ import psutil
+ svmem = psutil.virtual_memory()
+ name = self.gpu_names[gpu_id] if gpu_id < len(self.gpu_names) else "System RAM"
+
+ return AMDGPUStats(
+ gpu_id=gpu_id,
+ gpu_name=name,
+ utilization_gpu=0.0,
+ utilization_memory=(svmem.used / svmem.total * 100) if svmem.total > 0 else 0,
memory_total=svmem.total,
memory_used=svmem.used,
memory_free=svmem.available,
- temperature=0,
+ temperature=0.0,
fan_speed=0,
is_available=True
)
+# ==============================================
+# HELPERS
+# ==============================================
+
+def psutil_vram_estimate() -> float:
+ """Rough VRAM usage estimate from process memory"""
+ try:
+ import psutil
+ total = 0
+ for proc in psutil.process_iter(['memory_info', 'name']):
+ try:
+ info = proc.info
+ if info.get('memory_info'):
+ total += info['memory_info'].rss
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ pass
+ svmem = psutil.virtual_memory()
+ return (total / svmem.total * 100) if svmem.total > 0 else 0
+ except:
+ return 0
+
+
+# ==============================================
# Singleton
+# ==============================================
+
_amd_monitor = None
+
def get_amd_monitor():
global _amd_monitor
if _amd_monitor is None:
diff --git a/web/amd_monitor.js b/web/amd_monitor.js
index 9248b9a..8048d0e 100644
--- a/web/amd_monitor.js
+++ b/web/amd_monitor.js
@@ -1,44 +1,44 @@
/**
- * BANGTRIXTOOLKIT - AMD Monitor Floating Widget
+ * BANGTRIXTOOLKIT - AMD Monitor Overlay v3.2
* Real-time AMD GPU monitoring overlay for ComfyUI
- * Cross-platform: Windows (ADL) / Linux (ROCm)
+ * Features: GPU name, sparkline, generation stats, settings, multi-GPU
*/
-(function() {
+(function () {
"use strict";
- // ===== CONFIGURATION =====
- const CONFIG = {
- wsUrl: `ws://${window.location.host}/ws/amd_monitor`,
- updateInterval: 1000,
+ var CONFIG = {
storageKey: 'bangtrix_amd_monitor_config',
- defaultPosition: { top: '60px', right: '20px' }
};
- // ===== STATE =====
- let ws = null;
- let reconnectTimer = null;
- let reconnectAttempts = 0;
- const MAX_RECONNECT = 5;
-
- // Widget state
- let widget = null;
- let isMinimized = false;
- let isVisible = true;
- let isDragging = false;
- let dragOffset = { x: 0, y: 0 };
+ var ws = null;
+ var reconnectTimer = null;
+ var reconnectAttempts = 0;
+ var MAX_RECONNECT = 15;
- // ===== INITIALIZATION =====
+ var widget = null;
+ var isMinimized = false;
+ var isVisible = true;
+ var isDragging = false;
+ var dragOffset = { x: 0, y: 0 };
+
+ var gpuDataMap = {};
+ var selectedGpuId = 0;
+ var gpuCount = 1;
+ var historyData = [];
+
+ var savedConfig = {};
+
+ // ===== UTILITY =====
+ function $(id) { return document.getElementById(id); }
+
+ // ===== INIT =====
function init() {
- if (document.getElementById('bangtrix-amd-monitor')) {
- console.log('AMD Monitor: Already initialized');
- return;
- }
-
- console.log('AMD Monitor: Initializing floating widget...');
- createWidget();
- setupEventListeners();
+ console.log('[AMD Monitor] Initializing v3.2...');
loadConfig();
+ createWidget();
+ applyConfigToWidget();
+ setupEventListeners();
connectWebSocket();
}
@@ -47,235 +47,151 @@
widget = document.createElement('div');
widget.id = 'bangtrix-amd-monitor';
widget.className = 'amd-monitor-widget';
-
- widget.innerHTML = `
-
-
-
-
-
-
-
-
Temperature
-
--ยฐC
-
-
-
-
-
-
-
- `;
- injectStyles();
+ widget.innerHTML =
+ '' +
+ '' +
+ '
' +
+ '
GPU: ' +
+ '
' +
+ '
' +
+ '
AMD GPU
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
GPU Load History
' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '
' +
+ '
Duration --
' +
+ '
RAM Peak --
' +
+ '
RAM Delta --
' +
+ '
CPU Peak --
' +
+ '
' +
+ '
' +
+ '
' +
+ 'โ ๏ธ VRAM Alert ' +
+ '
' +
+ '
' +
+ '
Interval 1s 5s 10s 30s
' +
+ '
VRAM Alert %
' +
+ '
Temp Unit ยฐC ยฐF
' +
+ '
Sparkline
' +
+ '
' +
+ '' +
+ '
';
+
+ var style = document.createElement('style');
+ style.id = 'bangtrix-amd-monitor-styles';
+ style.textContent =
+ '#bangtrix-amd-monitor{position:fixed;top:60px;right:20px;width:260px;' +
+ 'background:rgba(18,18,24,0.92);border:1px solid rgba(196,48,43,0.3);border-radius:10px;' +
+ 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:11px;' +
+ 'color:#e0e0e0;z-index:9999;box-shadow:0 8px 32px rgba(0,0,0,0.4);' +
+ 'backdrop-filter:blur(8px);user-select:none;}' +
+ '#bangtrix-amd-monitor.hidden{display:none}' +
+ '#bangtrix-amd-monitor.minimized .amd-body{display:none}' +
+ '.amd-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;' +
+ 'background:linear-gradient(135deg,rgba(196,48,43,0.2)0%,rgba(139,32,29,0.1)100%);' +
+ 'border-radius:10px 10px 0 0;cursor:move;border-bottom:1px solid rgba(255,255,255,0.05)}' +
+ '.amd-title{display:flex;align-items:center;gap:8px;font-weight:600;font-size:12px;color:#fff}' +
+ '.amd-icon{font-size:14px;animation:pulse 2s infinite}' +
+ '@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}}' +
+ '.amd-controls{display:flex;gap:4px}' +
+ '.amd-btn{background:rgba(255,255,255,0.1);border:none;color:#ccc;width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:12px}' +
+ '.amd-btn:hover{background:rgba(255,255,255,0.2);color:#fff}' +
+ '.amd-btn-close:hover{background:rgba(255,80,80,0.3)}' +
+ '.amd-body{padding:12px 14px}' +
+ '.amd-gpu-name{text-align:center;font-size:11px;font-weight:600;color:#ff6b6b;margin-bottom:8px;' +
+ 'padding:4px 8px;border-radius:4px;background:rgba(255,255,255,0.04);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
+ '.amd-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}' +
+ '.amd-stat{display:flex;flex-direction:column;gap:2px}' +
+ '.amd-label{color:#888;font-size:10px;text-transform:uppercase}' +
+ '.amd-label-small{color:#888;font-size:10px}' +
+ '.amd-value{font-weight:600;font-size:13px;color:#00e676}' +
+ '.amd-value.na{color:#666}' +
+ '.amd-value.warning{color:#ffaa00}' +
+ '.amd-value.critical{color:#ff4444}' +
+ '.amd-bar{height:4px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;margin-top:2px}' +
+ '.amd-fill{height:100%;border-radius:2px;transition:width 0.3s ease;width:0%;background:linear-gradient(90deg,#00e676,#00c853)}' +
+ '.amd-fill.amd-temp{background:linear-gradient(90deg,#00e676,#ffaa00,#ff4444)}' +
+ '.amd-sparkline-container{margin-top:8px}' +
+ '.amd-sparkline{width:100%;height:40px;border-radius:4px;background:rgba(0,0,0,0.3)}' +
+ '.amd-process{margin-top:8px;padding:6px 8px;background:rgba(0,0,0,0.2);border-radius:6px;border:1px solid rgba(255,255,255,0.06)}' +
+ '.amd-process-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}' +
+ '.amd-process-status{font-size:10px}' +
+ '.amd-process-status.generating{color:#00e676}' +
+ '.amd-process-status.idle{color:#888}' +
+ '.amd-process-body{display:grid;grid-template-columns:1fr 1fr;gap:2px 8px}' +
+ '.amd-stat-row{display:flex;justify-content:space-between}' +
+ '.amd-label-xs{color:#666;font-size:9px}' +
+ '.amd-value-sm{color:#ccc;font-size:10px}' +
+ '.amd-alert{display:flex;align-items:center;gap:6px;padding:6px 10px;margin-top:8px;' +
+ 'background:rgba(255,80,80,0.15);border:1px solid rgba(255,80,80,0.3);border-radius:6px;font-size:11px;color:#ff6b6b}' +
+ '.amd-settings{margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05)}' +
+ '.amd-settings-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}' +
+ '.amd-select,.amd-input{background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.15);color:#e0e0e0;padding:2px 6px;border-radius:4px;font-size:10px;width:70px}' +
+ '.amd-input{width:50px}' +
+ '.amd-toggle{position:relative;width:32px;height:16px;cursor:pointer}' +
+ '.amd-toggle input{display:none}' +
+ '.amd-toggle-slider{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.15);border-radius:8px;transition:0.2s}' +
+ '.amd-toggle-slider:before{content:"";position:absolute;width:12px;height:12px;left:2px;bottom:2px;background:#ccc;border-radius:50%;transition:0.2s}' +
+ '.amd-toggle input:checked+.amd-toggle-slider{background:rgba(196,48,43,0.6)}' +
+ '.amd-toggle input:checked+.amd-toggle-slider:before{transform:translateX(16px);background:#fff}' +
+ '.amd-footer{display:flex;justify-content:space-between;margin-top:10px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05);font-size:10px;color:#666}' +
+ '.amd-status{transition:color 0.2s}' +
+ '.amd-status.connected{color:#00e676}' +
+ '.amd-status.disconnected{color:#ff4444}' +
+ '.amd-status.warning{color:#ffaa00}' +
+ '.amd-method{color:#888;font-family:monospace;font-size:9px}';
+ document.head.appendChild(style);
+
document.body.appendChild(widget);
setupDrag();
-
- return widget;
}
- function injectStyles() {
- const style = document.createElement('style');
- style.id = 'bangtrix-amd-monitor-styles';
- style.textContent = `
- #bangtrix-amd-monitor {
- position: fixed;
- top: 60px;
- right: 20px;
- width: 240px;
- background: rgba(18, 18, 24, 0.92);
- border: 1px solid rgba(196, 48, 43, 0.3);
- border-radius: 10px;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- font-size: 11px;
- color: #e0e0e0;
- z-index: 9999;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
- backdrop-filter: blur(8px);
- user-select: none;
- transition: opacity 0.2s, transform 0.2s;
- }
-
- #bangtrix-amd-monitor.hidden { display: none; }
- #bangtrix-amd-monitor.minimized .amd-body { display: none; }
-
- .amd-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 14px;
- background: linear-gradient(135deg, rgba(196, 48, 43, 0.2) 0%, rgba(139, 32, 29, 0.1) 100%);
- border-radius: 10px 10px 0 0;
- cursor: move;
- border-bottom: 1px solid rgba(255,255,255,0.05);
- }
-
- .amd-title {
- display: flex;
- align-items: center;
- gap: 8px;
- font-weight: 600;
- font-size: 12px;
- color: #fff;
- }
-
- .amd-icon { font-size: 14px; animation: pulse 2s infinite; }
-
- @keyframes pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.6; }
- }
-
- .amd-controls { display: flex; gap: 4px; }
-
- .amd-btn {
- background: rgba(255,255,255,0.1);
- border: none;
- color: #ccc;
- width: 22px;
- height: 22px;
- border-radius: 4px;
- cursor: pointer;
- font-size: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.15s;
- }
-
- .amd-btn:hover { background: rgba(255,255,255,0.2); color: #fff; }
- .amd-btn-close:hover { background: rgba(255, 80, 80, 0.3); color: #fff; }
-
- .amd-body { padding: 12px 14px; }
- .amd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; }
-
- .amd-stat {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
-
- .amd-label {
- color: #888;
- font-size: 10px;
- text-transform: uppercase;
- letter-spacing: 0.3px;
- }
-
- .amd-value {
- font-weight: 600;
- font-size: 13px;
- color: #00e676;
- transition: color 0.2s;
- }
-
- .amd-value.warning { color: #ffaa00; }
- .amd-value.critical { color: #ff4444; }
-
- .amd-bar {
- height: 4px;
- background: rgba(255,255,255,0.1);
- border-radius: 2px;
- overflow: hidden;
- margin-top: 2px;
- }
-
- .amd-fill {
- height: 100%;
- background: linear-gradient(90deg, #00e676, #00c853);
- border-radius: 2px;
- transition: width 0.3s ease;
- width: 0%;
- }
-
- .amd-fill.amd-temp {
- background: linear-gradient(90deg, #00e676, #ffaa00, #ff4444);
- }
-
- .amd-footer {
- display: flex;
- justify-content: space-between;
- margin-top: 14px;
- padding-top: 10px;
- border-top: 1px solid rgba(255,255,255,0.05);
- font-size: 10px;
- color: #666;
- }
-
- .amd-status { transition: color 0.2s; }
- .amd-status.connected { color: #00e676; }
- .amd-status.disconnected { color: #ff4444; }
- .amd-status.warning { color: #ffaa00; }
-
- .amd-method { color: #888; font-family: monospace; }
- .amd-header:active { cursor: grabbing; }
-
- @media (max-width: 768px) {
- #bangtrix-amd-monitor { width: 200px; font-size: 10px; }
- .amd-grid { grid-template-columns: 1fr; }
- }
- `;
- document.head.appendChild(style);
- }
-
- // ===== DRAG FUNCTIONALITY =====
+ // ===== DRAG =====
function setupDrag() {
- const header = widget.querySelector('.amd-header');
-
- header.addEventListener('mousedown', (e) => {
+ var header = widget.querySelector('.amd-header');
+ if (!header) return;
+ header.addEventListener('mousedown', function(e) {
if (e.target.closest('.amd-btn')) return;
-
isDragging = true;
- const rect = widget.getBoundingClientRect();
+ var rect = widget.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
-
widget.style.transition = 'none';
document.body.style.cursor = 'grabbing';
-
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
});
-
function onDrag(e) {
if (!isDragging) return;
-
- const x = e.clientX - dragOffset.x;
- const y = e.clientY - dragOffset.y;
-
- const maxX = window.innerWidth - widget.offsetWidth + 20;
- const maxY = window.innerHeight - widget.offsetHeight + 20;
-
- widget.style.left = Math.max(10, Math.min(x, maxX)) + 'px';
- widget.style.top = Math.max(10, Math.min(y, maxY)) + 'px';
+ var maxX = window.innerWidth - widget.offsetWidth + 20;
+ var maxY = window.innerHeight - widget.offsetHeight + 20;
+ widget.style.left = Math.max(10, Math.min(e.clientX - dragOffset.x, maxX)) + 'px';
+ widget.style.top = Math.max(10, Math.min(e.clientY - dragOffset.y, maxY)) + 'px';
widget.style.right = 'auto';
}
-
function stopDrag() {
isDragging = false;
widget.style.transition = '';
@@ -288,253 +204,376 @@
// ===== EVENT LISTENERS =====
function setupEventListeners() {
- widget.querySelector('.amd-btn-minimize').addEventListener('click', () => {
+ var minBtn = widget.querySelector('.amd-btn-minimize');
+ if (minBtn) minBtn.addEventListener('click', function() {
isMinimized = !isMinimized;
widget.classList.toggle('minimized', isMinimized);
- widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : 'โ';
+ minBtn.textContent = isMinimized ? '+' : 'โ';
saveConfig();
});
-
- widget.querySelector('.amd-btn-close').addEventListener('click', () => {
- isVisible = !isVisible;
- widget.classList.toggle('hidden', !isVisible);
+
+ var closeBtn = widget.querySelector('.amd-btn-close');
+ if (closeBtn) closeBtn.addEventListener('click', function() {
+ isVisible = false;
+ widget.classList.add('hidden');
+ saveConfig();
+ showToast('Hidden. Ctrl+Shift+M to show.');
+ });
+
+ var settingsBtn = widget.querySelector('.amd-btn-settings');
+ var settingsPanel = $('amd-settings');
+ if (settingsBtn && settingsPanel) settingsBtn.addEventListener('click', function() {
+ var isOpen = settingsPanel.style.display !== 'none';
+ settingsPanel.style.display = isOpen ? 'none' : 'block';
+ settingsBtn.classList.toggle('active', !isOpen);
+ });
+
+ var intervalSelect = $('amd-settings-interval');
+ if (intervalSelect) intervalSelect.addEventListener('change', function(e) {
+ sendCommand({type:'set_interval',interval:parseFloat(e.target.value)});
saveConfig();
});
-
- widget.querySelector('.amd-btn-settings').addEventListener('click', () => {
- showToast('Settings coming soon!');
+
+ var vramInput = $('amd-settings-vram-threshold');
+ if (vramInput) vramInput.addEventListener('change', function(e) {
+ savedConfig.vramThreshold = parseInt(e.target.value) || 90;
+ saveConfig();
+ });
+
+ var tempSelect = $('amd-settings-temp-unit');
+ if (tempSelect) tempSelect.addEventListener('change', function(e) {
+ savedConfig.tempUnit = e.target.value;
+ saveConfig();
+ if (gpuDataMap[selectedGpuId]) refreshDisplay(selectedGpuId);
+ });
+
+ var sparkCheck = $('amd-settings-sparkline');
+ if (sparkCheck) sparkCheck.addEventListener('change', function(e) {
+ var container = $('amd-sparkline-container');
+ if (container) container.style.display = e.target.checked ? 'block' : 'none';
+ saveConfig();
+ });
+
+ document.addEventListener('keydown', function(e) {
+ if (e.ctrlKey && e.shiftKey && e.key === 'M') {
+ e.preventDefault();
+ isVisible = !isVisible;
+ widget.classList.toggle('hidden', !isVisible);
+ if (isVisible) connectWebSocket();
+ saveConfig();
+ }
});
}
- // ===== CONFIGURATION PERSISTENCE =====
+ // ===== CONFIG =====
function loadConfig() {
- try {
- const saved = localStorage.getItem(CONFIG.storageKey);
- if (saved) {
- const config = JSON.parse(saved);
- if (config.position) {
- if (config.position.left) {
- widget.style.left = config.position.left;
- widget.style.right = 'auto';
- }
- if (config.position.top) {
- widget.style.top = config.position.top;
- }
- }
- if (config.minimized !== undefined) {
- isMinimized = config.minimized;
- widget.classList.toggle('minimized', isMinimized);
- widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : 'โ';
- }
- if (config.visible !== undefined) {
- isVisible = config.visible;
- widget.classList.toggle('hidden', !isVisible);
- }
- }
- } catch (e) {
- console.warn('AMD Monitor: Failed to load config:', e);
+ try { var saved = localStorage.getItem(CONFIG.storageKey); if (saved) savedConfig = JSON.parse(saved); } catch(e) {}
+ }
+
+ function applyConfigToWidget() {
+ if (!widget) return;
+ var c = savedConfig;
+ if (c.position) {
+ if (c.position.left) { widget.style.left = c.position.left; widget.style.right = 'auto'; }
+ if (c.position.top) widget.style.top = c.position.top;
}
+ if (c.minimized !== undefined) {
+ isMinimized = c.minimized;
+ widget.classList.toggle('minimized', isMinimized);
+ var btn = widget.querySelector('.amd-btn-minimize');
+ if (btn) btn.textContent = isMinimized ? '+' : 'โ';
+ }
+ if (c.visible !== undefined) { isVisible = c.visible; widget.classList.toggle('hidden', !isVisible); }
+ var sel = $('amd-settings-interval'); if (sel) sel.value = String(c.updateInterval || 5);
+ var inp = $('amd-settings-vram-threshold'); if (inp) inp.value = c.vramThreshold || 90;
+ var sel2 = $('amd-settings-temp-unit'); if (sel2) sel2.value = c.tempUnit || 'celsius';
+ var chk = $('amd-settings-sparkline');
+ if (chk) { chk.checked = c.showSparkline !== false; var cont = $('amd-sparkline-container'); if (cont) cont.style.display = chk.checked ? 'block' : 'none'; }
}
function saveConfig() {
try {
- const config = {
- position: {
- top: widget.style.top,
- left: widget.style.left
- },
- minimized: isMinimized,
- visible: isVisible
+ var config = {
+ position: { top: widget.style.top, left: widget.style.left },
+ minimized: isMinimized, visible: isVisible,
+ updateInterval: parseFloat($('amd-settings-interval')?.value) || 5,
+ vramThreshold: parseInt($('amd-settings-vram-threshold')?.value) || 90,
+ tempUnit: $('amd-settings-temp-unit')?.value || 'celsius',
+ showSparkline: $('amd-settings-sparkline')?.checked !== false
};
localStorage.setItem(CONFIG.storageKey, JSON.stringify(config));
- } catch (e) {
- console.warn('AMD Monitor: Failed to save config:', e);
- }
+ } catch(e) {}
}
- // ===== WEBSOCKET CONNECTION =====
+ // ===== WEBSOCKET =====
function connectWebSocket() {
- if (ws?.readyState === WebSocket.OPEN) return;
-
- console.log('AMD Monitor: Connecting to', CONFIG.wsUrl);
+ if (ws && ws.readyState === WebSocket.OPEN) return;
+ if (!isVisible) return;
+ var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ var url = protocol + '//' + window.location.host + '/ws/amd_monitor';
+ console.log('[AMD Monitor] Connecting...');
updateStatus('Connecting...', 'warning');
-
- ws = new WebSocket(CONFIG.wsUrl);
-
- ws.onopen = () => {
- console.log('AMD Monitor: Connected');
+ ws = new WebSocket(url);
+ ws.onopen = function() {
+ console.log('[AMD Monitor] Connected');
updateStatus('Connected', 'connected');
updateMethod('Live');
reconnectAttempts = 0;
- if (reconnectTimer) {
- clearTimeout(reconnectTimer);
- reconnectTimer = null;
- }
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
};
-
- ws.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
- if (data.type === 'amd_stats') {
- updateDisplay(data);
- }
- } catch (e) {
- console.error('AMD Monitor: Parse error:', e);
- }
+ ws.onmessage = function(event) {
+ try { var data = JSON.parse(event.data); if (data.type === 'amd_stats') handleStatsData(data); } catch(e) {}
};
-
- ws.onclose = () => {
- console.log('AMD Monitor: Disconnected');
+ ws.onclose = function() {
updateStatus('Disconnected', 'disconnected');
updateMethod('--');
-
if (reconnectAttempts < MAX_RECONNECT) {
reconnectAttempts++;
- const delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 8000);
- console.log(`AMD Monitor: Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`);
+ var delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 10000);
reconnectTimer = setTimeout(connectWebSocket, delay);
}
};
-
- ws.onerror = (error) => {
- console.error('AMD Monitor: WebSocket error:', error);
- updateStatus('Connection error', 'disconnected');
- };
+ ws.onerror = function() {};
}
- // ===== DISPLAY UPDATES =====
- function updateDisplay(data) {
+ function sendCommand(cmd) { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(cmd)); }
+
+ // ===== DATA HANDLING =====
+ function handleStatsData(data) {
+ gpuDataMap[data.gpu_id] = data;
+ if (data.gpu_count && gpuCount !== data.gpu_count) { gpuCount = data.gpu_count; updateGpuSelector(); }
+ if (data.gpu_id === selectedGpuId) refreshDisplay(selectedGpuId);
+ }
+
+ function refreshDisplay(gpuId) {
+ var data = gpuDataMap[gpuId];
+ if (!data) return;
+
+ var nameEl = $('amd-gpu-name');
+ if (nameEl) nameEl.textContent = data.gpu_name || ('AMD GPU ' + gpuId);
+
if (!data.is_available) {
- setStat('amd-gpu-util', 'N/A');
- setStat('amd-vram', 'N/A');
- setStat('amd-temp', 'N/A');
- setStat('amd-fan', 'N/A');
- setBar('amd-gpu-bar', 0);
- setBar('amd-vram-bar', 0);
- setBar('amd-temp-bar', 0);
- setBar('amd-fan-bar', 0);
- updateStatus(data.error || 'AMD backend unavailable', 'warning');
+ ['amd-gpu-util','amd-vram','amd-temp','amd-fan'].forEach(function(id) { setStat(id, 'N/A'); });
+ ['amd-gpu-bar','amd-vram-bar','amd-temp-bar','amd-fan-bar'].forEach(function(id) { setBar(id, 0); });
+ updateStatus(data.error || 'Unavailable', 'warning');
updateMethod(data.method || '--');
return;
}
- const gpuUtil = data.gpu_utilization || 0;
- setStat('amd-gpu-util', `${gpuUtil.toFixed(1)}%`, gpuUtil);
+ // GPU Load
+ var gpuUtil = data.gpu_utilization !== undefined ? data.gpu_utilization : 0;
+ setStat('amd-gpu-util', gpuUtil.toFixed(1) + '%', gpuUtil);
setBar('amd-gpu-bar', gpuUtil);
-
- const vramUsedGB = (data.vram_used_mb || 0) / 1024;
- const vramTotalGB = (data.vram_total_mb || 0) / 1024;
- const vramPct = data.vram_usage_pct || (vramTotalGB > 0 ? (vramUsedGB / vramTotalGB * 100) : 0);
- setStat('amd-vram', `${vramUsedGB.toFixed(1)} / ${vramTotalGB.toFixed(1)} GB`, vramPct);
- setBar('amd-vram-bar', vramPct);
-
- const temp = data.temperature || 0;
- setStat('amd-temp', `${temp.toFixed(1)}ยฐC`, temp);
- setBar('amd-temp-bar', Math.min(temp, 100));
-
- const fan = data.fan_speed || 0;
- setStat('amd-fan', `${fan}%`, fan);
- setBar('amd-fan-bar', fan);
-
- updateStatus(`GPU ${data.gpu_id}`, 'connected');
- updateMethod(data.method || 'unknown');
+
+ // VRAM
+ var vramUsedMB = data.vram_used_mb || 0;
+ var vramTotalMB = data.vram_total_mb || 0;
+ var vramPct = data.vram_usage_pct || (vramTotalMB > 0 ? (vramUsedMB / vramTotalMB * 100) : 0);
+ if (vramTotalMB > 0) {
+ setStat('amd-vram', (vramUsedMB / 1024).toFixed(1) + ' / ' + (vramTotalMB / 1024).toFixed(1) + ' GB', vramPct);
+ setBar('amd-vram-bar', vramPct);
+ } else { setStat('amd-vram', 'N/A'); setBar('amd-vram-bar', 0); }
+
+ // Temperature
+ var temp = data.temperature || 0;
+ if (temp > 0) {
+ var tUnit = savedConfig.tempUnit || 'celsius';
+ var tempDisplay = temp;
+ var tempUnitStr = 'ยฐC';
+ if (tUnit === 'fahrenheit') { tempDisplay = temp * 9/5 + 32; tempUnitStr = 'ยฐF'; }
+ setStat('amd-temp', tempDisplay.toFixed(1) + tempUnitStr, temp);
+ setBar('amd-temp-bar', Math.min(temp, 100));
+ } else { setStat('amd-temp', 'N/A'); setBar('amd-temp-bar', 0); }
+
+ // Fan
+ var fan = data.fan_speed || 0;
+ if (fan > 0) { setStat('amd-fan', fan + '%', fan); setBar('amd-fan-bar', fan); }
+ else { setStat('amd-fan', 'N/A'); setBar('amd-fan-bar', 0); }
+
+ // Status
+ var methodInfo = data.method || 'unknown';
+ if (data.core_clock_mhz > 0) methodInfo += ' | ' + data.core_clock_mhz + 'MHz';
+ if (data.power_draw_watts > 0) methodInfo += ' | ' + data.power_draw_watts + 'W';
+ updateMethod(methodInfo);
+
+ var statusText = data.gpu_name || ('GPU ' + gpuId);
+ if (gpuCount > 1) statusText += ' (' + (gpuId + 1) + '/' + gpuCount + ')';
+ updateStatus(statusText, 'connected');
+
+ // Sparkline
+ if (data.history && data.history.length > 0) {
+ historyData = data.history;
+ drawSparkline(historyData);
+ var sparkContainer = $('amd-sparkline-container');
+ if (sparkContainer) {
+ var chk = $('amd-settings-sparkline');
+ sparkContainer.style.display = (chk && chk.checked) ? 'block' : 'none';
+ }
+ }
+
+ // Process stats
+ updateProcessDisplay(data);
+ checkVramAlert(vramPct);
}
- function setStat(elementId, value, numericValue = null) {
- const el = document.getElementById(elementId);
+ function updateProcessDisplay(data) {
+ var process = data.process;
+ if (!process) return;
+ var statusEl = $('amd-process-status');
+ var durEl = $('amd-gen-duration');
+ var ramPeakEl = $('amd-gen-ram-peak');
+ var ramDeltaEl = $('amd-gen-ram-delta');
+ var cpuPeakEl = $('amd-gen-cpu-peak');
+ if (!statusEl) return;
+
+ if (process.is_generating && process.generation) {
+ var gen = process.generation;
+ statusEl.textContent = 'โ generating';
+ statusEl.className = 'amd-process-status generating';
+ if (durEl) durEl.textContent = gen.duration + 's';
+ if (ramPeakEl) ramPeakEl.textContent = gen.ram_peak_mb + ' MB';
+ if (ramDeltaEl) ramDeltaEl.textContent = '+' + (gen.ram_peak_mb - gen.ram_start_mb) + ' MB';
+ if (cpuPeakEl) cpuPeakEl.textContent = gen.cpu_peak + '%';
+ } else if (process.last_generation) {
+ var last = process.last_generation;
+ statusEl.textContent = 'โ idle (' + (process.generation_count || 0) + ' gens)';
+ statusEl.className = 'amd-process-status idle';
+ if (durEl) durEl.textContent = last.duration + 's';
+ if (ramPeakEl) ramPeakEl.textContent = last.ram_peak_mb + ' MB';
+ if (ramDeltaEl) ramDeltaEl.textContent = '+' + last.vram_delta_mb + ' MB';
+ if (cpuPeakEl) cpuPeakEl.textContent = last.cpu_peak + '%';
+ } else {
+ statusEl.textContent = 'โ idle';
+ statusEl.className = 'amd-process-status idle';
+ if (durEl) durEl.textContent = '--'; if (ramPeakEl) ramPeakEl.textContent = '--';
+ if (ramDeltaEl) ramDeltaEl.textContent = '--'; if (cpuPeakEl) cpuPeakEl.textContent = '--';
+ }
+ }
+
+ function updateGpuSelector() {
+ var selector = $('amd-gpu-selector');
+ var chips = $('amd-gpu-chips');
+ if (!selector || !chips) return;
+ if (gpuCount <= 1) { selector.style.display = 'none'; return; }
+ selector.style.display = 'block';
+ chips.innerHTML = '';
+ for (var i = 0; i < gpuCount; i++) (function(idx) {
+ var chip = document.createElement('span');
+ chip.className = 'amd-chip' + (idx === selectedGpuId ? ' active' : '');
+ chip.textContent = 'GPU ' + idx;
+ chip.addEventListener('click', function() {
+ selectedGpuId = idx;
+ chips.querySelectorAll('.amd-chip').forEach(function(c) { c.classList.remove('active'); });
+ chip.classList.add('active');
+ refreshDisplay(selectedGpuId);
+ });
+ chips.appendChild(chip);
+ })(i);
+ }
+
+ // ===== SPARKLINE =====
+ function drawSparkline(values) {
+ var canvas = $('amd-sparkline');
+ if (!canvas) return;
+ var ctx = canvas.getContext('2d');
+ var w = canvas.width, h = canvas.height, padding = 2;
+ ctx.clearRect(0, 0, w, h);
+ if (values.length < 2 || values.every(function(v) { return v === 0; })) {
+ ctx.fillStyle = '#555'; ctx.font = '10px monospace'; ctx.textAlign = 'center';
+ ctx.fillText('awaiting data...', w / 2, h / 2 + 3); return;
+ }
+ var max = Math.max.apply(null, values); if (max < 1) max = 1;
+ var stepX = (w - padding * 2) / (values.length - 1);
+ ctx.beginPath(); ctx.strokeStyle = '#00e676'; ctx.lineWidth = 1.5; ctx.lineJoin = 'round';
+ for (var i = 0; i < values.length; i++) {
+ var x = padding + i * stepX;
+ var y = h - padding - (values[i] / max) * (h - padding * 2);
+ if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
+ }
+ ctx.stroke();
+ ctx.lineTo(padding + (values.length - 1) * stepX, h - padding);
+ ctx.lineTo(padding, h - padding); ctx.closePath();
+ ctx.fillStyle = 'rgba(0,230,118,0.1)'; ctx.fill();
+ var lastVal = values[values.length - 1];
+ ctx.fillStyle = '#00e676'; ctx.font = 'bold 9px monospace'; ctx.textAlign = 'right';
+ ctx.fillText(lastVal.toFixed(0) + '%', w - padding, 10);
+ }
+
+ // ===== ALERTS =====
+ var lastAlertTime = 0;
+ function checkVramAlert(vramPct) {
+ var threshold = savedConfig.vramThreshold || 90;
+ if (vramPct >= threshold && Date.now() - lastAlertTime > 10000) {
+ lastAlertTime = Date.now();
+ var alertEl = $('amd-alert');
+ var alertText = $('amd-alert-text');
+ if (alertEl && alertText) {
+ alertText.textContent = 'VRAM ' + vramPct.toFixed(0) + '% >= ' + threshold + '%';
+ alertEl.style.display = 'flex';
+ setTimeout(function() { if (alertEl) alertEl.style.display = 'none'; }, 5000);
+ }
+ }
+ }
+
+ // ===== UI HELPERS =====
+ function setStat(elementId, value, numericValue) {
+ var el = $(elementId);
if (!el) return;
el.textContent = value;
-
- el.classList.remove('warning', 'critical');
- if (numericValue !== null) {
- if (elementId === 'amd-temp' && numericValue > 80) {
- el.classList.add('critical');
- } else if (elementId === 'amd-temp' && numericValue > 65) {
- el.classList.add('warning');
- } else if ((elementId === 'amd-gpu-util' || elementId === 'amd-vram') && numericValue > 90) {
- el.classList.add('critical');
- } else if ((elementId === 'amd-gpu-util' || elementId === 'amd-vram') && numericValue > 75) {
- el.classList.add('warning');
- }
+ el.classList.remove('warning', 'critical', 'na');
+ if (value === 'N/A') { el.classList.add('na'); }
+ else if (numericValue !== null && numericValue !== undefined) {
+ if ((elementId === 'amd-temp') && numericValue > 80) el.classList.add('critical');
+ else if ((elementId === 'amd-temp') && numericValue > 65) el.classList.add('warning');
+ else if ((elementId === 'amd-gpu-util' || elementId.indexOf('vram') >= 0) && numericValue > 90) el.classList.add('critical');
+ else if ((elementId === 'amd-gpu-util' || elementId.indexOf('vram') >= 0) && numericValue > 75) el.classList.add('warning');
}
}
function setBar(elementId, percent) {
- const el = document.getElementById(elementId);
- if (!el) return;
- el.style.width = `${Math.min(100, Math.max(0, percent))}%`;
+ var el = $(elementId); if (!el) return;
+ el.style.width = Math.min(100, Math.max(0, percent)) + '%';
}
function updateStatus(message, className) {
- const el = document.getElementById('amd-status');
- if (!el) return;
- el.textContent = message;
- el.className = `amd-status ${className}`;
-
- const icon = widget.querySelector('.amd-icon');
- if (icon) {
- icon.textContent = className === 'connected' ? '๐ข' :
- className === 'warning' ? '๐ก' : '๐ด';
- }
+ var el = $('amd-status'); if (!el) return;
+ el.textContent = message; el.className = 'amd-status ' + className;
+ var icon = widget.querySelector('.amd-icon');
+ if (icon) icon.textContent = className === 'connected' ? '๐ข' : className === 'warning' ? '๐ก' : '๐ด';
}
- function updateMethod(method) {
- const el = document.getElementById('amd-method');
- if (!el) return;
- el.textContent = method ? `[${method}]` : '';
- }
+ function updateMethod(method) { var el = $('amd-method'); if (el) el.textContent = method || ''; }
- // ===== UTILITIES =====
- function showToast(message, duration = 2000) {
- const toast = document.createElement('div');
+ function showToast(message, duration) {
+ duration = duration || 2000;
+ var old = document.querySelector('.bangtrix-toast'); if (old) old.remove();
+ var toast = document.createElement('div');
+ toast.className = 'bangtrix-toast';
toast.textContent = message;
- toast.style.cssText = `
- position: fixed;
- bottom: 20px;
- right: 20px;
- background: rgba(30, 30, 40, 0.95);
- color: #fff;
- padding: 10px 16px;
- border-radius: 6px;
- font-size: 12px;
- z-index: 10001;
- border: 1px solid rgba(196, 48, 43, 0.5);
- animation: slideIn 0.2s ease;
- `;
+ toast.style.cssText = 'position:fixed;bottom:20px;right:20px;background:rgba(30,30,40,0.95);color:#fff;padding:10px 16px;border-radius:6px;font-size:12px;z-index:10001;border:1px solid rgba(196,48,43,0.5);animation:slideIn 0.2s ease;max-width:300px;';
document.body.appendChild(toast);
-
- setTimeout(() => {
- toast.style.animation = 'slideOut 0.2s ease';
- setTimeout(() => toast.remove(), 200);
- }, duration);
+ setTimeout(function() { toast.style.animation = 'slideOut 0.2s ease'; setTimeout(function() { toast.remove(); }, 200); }, duration);
}
+ // ===== ANIMATIONS =====
if (!document.getElementById('amd-monitor-animations')) {
- const animStyle = document.createElement('style');
- animStyle.id = 'amd-monitor-animations';
- animStyle.textContent = `
- @keyframes slideIn {
- from { transform: translateX(100%); opacity: 0; }
- to { transform: translateX(0); opacity: 1; }
- }
- @keyframes slideOut {
- from { transform: translateX(0); opacity: 1; }
- to { transform: translateX(100%); opacity: 0; }
- }
- `;
- document.head.appendChild(animStyle);
+ var s = document.createElement('style');
+ s.id = 'amd-monitor-animations';
+ s.textContent = '@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}@keyframes slideOut{from{transform:translateX(0);opacity:1}to{transform:translateX(100%);opacity:0}}';
+ document.head.appendChild(s);
+ }
+
+ // ===== AMD CHIP SELECTOR =====
+ if (!document.getElementById('amd-chip-styles')) {
+ var cs = document.createElement('style');
+ cs.id = 'amd-chip-styles';
+ cs.textContent = '.amd-chip{padding:2px 8px;border-radius:10px;font-size:10px;background:rgba(255,255,255,0.08);cursor:pointer;transition:all 0.15s;border:1px solid transparent}.amd-chip:hover{background:rgba(255,255,255,0.15)}.amd-chip.active{background:rgba(196,48,43,0.3);border-color:rgba(196,48,43,0.5);color:#fff}.amd-gpu-selector{margin-bottom:6px}.amd-chip-group{display:flex;gap:4px;flex-wrap:wrap}';
+ document.head.appendChild(cs);
}
// ===== START =====
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
-
- window.addEventListener('beforeunload', () => {
- if (ws) ws.close();
- if (reconnectTimer) clearTimeout(reconnectTimer);
- });
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
+ else init();
+ window.addEventListener('beforeunload', function() { if (ws) ws.close(); if (reconnectTimer) clearTimeout(reconnectTimer); });
})();
\ No newline at end of file