add monitor beta
This commit is contained in:
+61
-1
@@ -1,12 +1,72 @@
|
||||
"""
|
||||
BANGTRIXTOOLKIT - ComfyUI Custom Nodes
|
||||
======================================
|
||||
AMD Monitor & Translate Universal
|
||||
"""
|
||||
|
||||
from .nodes.translate_universal import (
|
||||
NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS
|
||||
)
|
||||
|
||||
from .nodes.amd_monitor import (
|
||||
NODE_CLASS_MAPPINGS as AMD_NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS as AMD_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}
|
||||
|
||||
WEB_DIRECTORY = "./web"
|
||||
|
||||
__all__ = [
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS",
|
||||
"WEB_DIRECTORY"
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 🚀 SERVER EXTENSION - WebSocket untuk Real-Time Monitoring
|
||||
# ============================================================
|
||||
|
||||
async def on_app_started(app):
|
||||
"""Register WebSocket endpoint for AMD Monitor (FIXED: 1 argument only)"""
|
||||
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
|
||||
if not amd_server.running:
|
||||
amd_server.running = True
|
||||
asyncio.create_task(amd_server.stream_data())
|
||||
print("✅ BANGTRIXTOOLKIT: AMD Monitor streaming started")
|
||||
|
||||
|
||||
# Register with ComfyUI (FIXED: lambda to match signature)
|
||||
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")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ BANGTRIXTOOLKIT: Server extension skipped: {e}")
|
||||
@@ -0,0 +1,18 @@
|
||||
from .translate_universal import (
|
||||
NODE_CLASS_MAPPINGS,
|
||||
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"
|
||||
]
|
||||
@@ -1,9 +0,0 @@
|
||||
from .translate_universal import (
|
||||
NODE_CLASS_MAPPINGS,
|
||||
NODE_DISPLAY_NAME_MAPPINGS
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"NODE_CLASS_MAPPINGS",
|
||||
"NODE_DISPLAY_NAME_MAPPINGS"
|
||||
]
|
||||
@@ -0,0 +1,57 @@
|
||||
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)"
|
||||
}
|
||||
+15
-1
@@ -1 +1,15 @@
|
||||
requests
|
||||
# HTTP & WebSocket Server (Required for AMD Monitor)
|
||||
aiohttp>=3.8.0
|
||||
async-timeout>=4.0.0
|
||||
|
||||
# HTTP Requests (Optional - for translate_universal)
|
||||
requests>=2.31.0
|
||||
|
||||
# System Monitoring
|
||||
psutil>=5.9.0
|
||||
|
||||
# AMD GPU Monitoring - Linux (ROCm)
|
||||
pyrsmi>=0.2.0; sys_platform == "linux"
|
||||
|
||||
# AMD GPU Monitoring - Windows (ADL)
|
||||
pyadl>=1.0.0; sys_platform == "win32"
|
||||
@@ -0,0 +1,3 @@
|
||||
from .amd_server import AMDMonitorServer, get_amd_server
|
||||
|
||||
__all__ = ["AMDMonitorServer", "get_amd_server"]
|
||||
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
AMD Monitor Server
|
||||
==================
|
||||
WebSocket server for streaming AMD GPU stats to frontend
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Set
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AMDMonitorServer:
|
||||
def __init__(self):
|
||||
self.monitor = None # Lazy load
|
||||
self.clients: Set = set()
|
||||
self.running = False
|
||||
self.update_interval = 1.0 # seconds
|
||||
|
||||
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()
|
||||
return self.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
|
||||
)
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
def start(self, loop=None):
|
||||
"""Start the monitoring loop safely"""
|
||||
if self.running:
|
||||
return
|
||||
|
||||
self.running = True
|
||||
logger.info("AMD Monitor: Starting data stream")
|
||||
|
||||
|
||||
# Singleton instance
|
||||
_amd_server = None
|
||||
|
||||
def get_amd_server() -> AMDMonitorServer:
|
||||
global _amd_server
|
||||
if _amd_server is None:
|
||||
_amd_server = AMDMonitorServer()
|
||||
return _amd_server
|
||||
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
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 ===")
|
||||
@@ -0,0 +1,212 @@
|
||||
import platform
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
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
|
||||
error_message: Optional[str] = None
|
||||
|
||||
|
||||
class AMDMonitor:
|
||||
def __init__(self):
|
||||
self.available = False
|
||||
self.method = None
|
||||
self.gpu_count = 0
|
||||
self._backend = None
|
||||
self._initialize()
|
||||
|
||||
def _initialize(self):
|
||||
system = platform.system()
|
||||
|
||||
# --- LINUX IMPLEMENTATION (ROCm) ---
|
||||
if system == "Linux":
|
||||
try:
|
||||
import pyrsmi
|
||||
self._backend = pyrsmi
|
||||
pyrsmi.rocm_smi_initialize()
|
||||
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.")
|
||||
return
|
||||
except ImportError:
|
||||
logger.warning("Linux detected but 'pyrsmi' not installed.")
|
||||
except Exception as e:
|
||||
logger.warning(f"Linux pyrsmi init failed: {e}")
|
||||
|
||||
# --- WINDOWS IMPLEMENTATION (ADL) ---
|
||||
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)")
|
||||
return
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
logger.error("No AMD monitoring backend available. Check requirements.")
|
||||
|
||||
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")
|
||||
|
||||
try:
|
||||
if self.method == "pyrsmi":
|
||||
return self._get_stats_linux(gpu_id)
|
||||
elif self.method == "pyadl":
|
||||
return self._get_stats_windows(gpu_id)
|
||||
elif self.method == "psutil-fallback":
|
||||
return self._get_stats_windows_fallback(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,
|
||||
utilization_gpu=util,
|
||||
utilization_memory=mem_util,
|
||||
memory_total=mem_total,
|
||||
memory_used=mem_used,
|
||||
memory_free=mem_free,
|
||||
temperature=temp,
|
||||
fan_speed=fan,
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Memory (VRAM) - ADL doesn't expose this easily
|
||||
mem_used = 0
|
||||
mem_total = 0
|
||||
mem_util = 0
|
||||
|
||||
# Temperature
|
||||
temp = 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
|
||||
|
||||
return AMDGPUStats(
|
||||
gpu_id=gpu_id,
|
||||
utilization_gpu=gpu_util,
|
||||
utilization_memory=mem_util,
|
||||
memory_total=mem_total,
|
||||
memory_used=mem_used,
|
||||
memory_free=0,
|
||||
temperature=temp,
|
||||
fan_speed=fan,
|
||||
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()
|
||||
|
||||
return AMDGPUStats(
|
||||
gpu_id=gpu_id,
|
||||
utilization_gpu=0,
|
||||
utilization_memory=(svmem.used / svmem.total * 100),
|
||||
memory_total=svmem.total,
|
||||
memory_used=svmem.used,
|
||||
memory_free=svmem.available,
|
||||
temperature=0,
|
||||
fan_speed=0,
|
||||
is_available=True
|
||||
)
|
||||
|
||||
|
||||
# Singleton
|
||||
_amd_monitor = None
|
||||
|
||||
def get_amd_monitor():
|
||||
global _amd_monitor
|
||||
if _amd_monitor is None:
|
||||
_amd_monitor = AMDMonitor()
|
||||
return _amd_monitor
|
||||
@@ -0,0 +1,540 @@
|
||||
/**
|
||||
* BANGTRIXTOOLKIT - AMD Monitor Floating Widget
|
||||
* Real-time AMD GPU monitoring overlay for ComfyUI
|
||||
* Cross-platform: Windows (ADL) / Linux (ROCm)
|
||||
*/
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
// ===== CONFIGURATION =====
|
||||
const CONFIG = {
|
||||
wsUrl: `ws://${window.location.host}/ws/amd_monitor`,
|
||||
updateInterval: 1000,
|
||||
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 };
|
||||
|
||||
// ===== INITIALIZATION =====
|
||||
function init() {
|
||||
if (document.getElementById('bangtrix-amd-monitor')) {
|
||||
console.log('AMD Monitor: Already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('AMD Monitor: Initializing floating widget...');
|
||||
createWidget();
|
||||
setupEventListeners();
|
||||
loadConfig();
|
||||
connectWebSocket();
|
||||
}
|
||||
|
||||
// ===== WIDGET CREATION =====
|
||||
function createWidget() {
|
||||
widget = document.createElement('div');
|
||||
widget.id = 'bangtrix-amd-monitor';
|
||||
widget.className = 'amd-monitor-widget';
|
||||
|
||||
widget.innerHTML = `
|
||||
<div class="amd-header">
|
||||
<div class="amd-title">
|
||||
<span class="amd-icon">🔴</span>
|
||||
<span class="amd-text">AMD Monitor</span>
|
||||
</div>
|
||||
<div class="amd-controls">
|
||||
<button class="amd-btn amd-btn-minimize" title="Minimize">−</button>
|
||||
<button class="amd-btn amd-btn-settings" title="Settings">⚙</button>
|
||||
<button class="amd-btn amd-btn-close" title="Hide">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="amd-body">
|
||||
<div class="amd-grid">
|
||||
<div class="amd-stat">
|
||||
<span class="amd-label">GPU Load</span>
|
||||
<span class="amd-value" id="amd-gpu-util">--</span>
|
||||
<div class="amd-bar"><div class="amd-fill" id="amd-gpu-bar"></div></div>
|
||||
</div>
|
||||
<div class="amd-stat">
|
||||
<span class="amd-label">VRAM</span>
|
||||
<span class="amd-value" id="amd-vram">-- / -- GB</span>
|
||||
<div class="amd-bar"><div class="amd-fill" id="amd-vram-bar"></div></div>
|
||||
</div>
|
||||
<div class="amd-stat">
|
||||
<span class="amd-label">Temperature</span>
|
||||
<span class="amd-value" id="amd-temp">--°C</span>
|
||||
<div class="amd-bar"><div class="amd-fill amd-temp" id="amd-temp-bar"></div></div>
|
||||
</div>
|
||||
<div class="amd-stat">
|
||||
<span class="amd-label">Fan Speed</span>
|
||||
<span class="amd-value" id="amd-fan">--%</span>
|
||||
<div class="amd-bar"><div class="amd-fill" id="amd-fan-bar"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="amd-footer">
|
||||
<span class="amd-status" id="amd-status">Initializing...</span>
|
||||
<span class="amd-method" id="amd-method">--</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
injectStyles();
|
||||
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 =====
|
||||
function setupDrag() {
|
||||
const header = widget.querySelector('.amd-header');
|
||||
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
if (e.target.closest('.amd-btn')) return;
|
||||
|
||||
isDragging = true;
|
||||
const 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';
|
||||
widget.style.right = 'auto';
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging = false;
|
||||
widget.style.transition = '';
|
||||
document.body.style.cursor = '';
|
||||
document.removeEventListener('mousemove', onDrag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
saveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== EVENT LISTENERS =====
|
||||
function setupEventListeners() {
|
||||
widget.querySelector('.amd-btn-minimize').addEventListener('click', () => {
|
||||
isMinimized = !isMinimized;
|
||||
widget.classList.toggle('minimized', isMinimized);
|
||||
widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : '−';
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
widget.querySelector('.amd-btn-close').addEventListener('click', () => {
|
||||
isVisible = !isVisible;
|
||||
widget.classList.toggle('hidden', !isVisible);
|
||||
saveConfig();
|
||||
});
|
||||
|
||||
widget.querySelector('.amd-btn-settings').addEventListener('click', () => {
|
||||
showToast('Settings coming soon!');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== CONFIGURATION PERSISTENCE =====
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function saveConfig() {
|
||||
try {
|
||||
const config = {
|
||||
position: {
|
||||
top: widget.style.top,
|
||||
left: widget.style.left
|
||||
},
|
||||
minimized: isMinimized,
|
||||
visible: isVisible
|
||||
};
|
||||
localStorage.setItem(CONFIG.storageKey, JSON.stringify(config));
|
||||
} catch (e) {
|
||||
console.warn('AMD Monitor: Failed to save config:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== WEBSOCKET CONNECTION =====
|
||||
function connectWebSocket() {
|
||||
if (ws?.readyState === WebSocket.OPEN) return;
|
||||
|
||||
console.log('AMD Monitor: Connecting to', CONFIG.wsUrl);
|
||||
updateStatus('Connecting...', 'warning');
|
||||
|
||||
ws = new WebSocket(CONFIG.wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('AMD Monitor: Connected');
|
||||
updateStatus('Connected', 'connected');
|
||||
updateMethod('Live');
|
||||
reconnectAttempts = 0;
|
||||
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.onclose = () => {
|
||||
console.log('AMD Monitor: Disconnected');
|
||||
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})`);
|
||||
reconnectTimer = setTimeout(connectWebSocket, delay);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('AMD Monitor: WebSocket error:', error);
|
||||
updateStatus('Connection error', 'disconnected');
|
||||
};
|
||||
}
|
||||
|
||||
// ===== DISPLAY UPDATES =====
|
||||
function updateDisplay(data) {
|
||||
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');
|
||||
updateMethod(data.method || '--');
|
||||
return;
|
||||
}
|
||||
|
||||
const gpuUtil = 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');
|
||||
}
|
||||
|
||||
function setStat(elementId, value, numericValue = null) {
|
||||
const el = document.getElementById(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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBar(elementId, percent) {
|
||||
const el = document.getElementById(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' ? '🟡' : '🔴';
|
||||
}
|
||||
}
|
||||
|
||||
function updateMethod(method) {
|
||||
const el = document.getElementById('amd-method');
|
||||
if (!el) return;
|
||||
el.textContent = method ? `[${method}]` : '';
|
||||
}
|
||||
|
||||
// ===== UTILITIES =====
|
||||
function showToast(message, duration = 2000) {
|
||||
const toast = document.createElement('div');
|
||||
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;
|
||||
`;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.animation = 'slideOut 0.2s ease';
|
||||
setTimeout(() => toast.remove(), 200);
|
||||
}, duration);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// ===== START =====
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (ws) ws.close();
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -1,56 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "Bangtrix.AMDMonitor",
|
||||
|
||||
async setup() {
|
||||
|
||||
const panel = document.createElement("div");
|
||||
|
||||
panel.id = "bangtrix-amd-monitor";
|
||||
|
||||
panel.style.position = "fixed";
|
||||
panel.style.top = "20px";
|
||||
panel.style.right = "20px";
|
||||
panel.style.width = "220px";
|
||||
panel.style.padding = "12px";
|
||||
panel.style.background = "rgba(20,20,20,0.9)";
|
||||
panel.style.color = "white";
|
||||
panel.style.fontSize = "14px";
|
||||
panel.style.borderRadius = "12px";
|
||||
panel.style.zIndex = "99999";
|
||||
panel.style.backdropFilter = "blur(10px)";
|
||||
panel.style.boxShadow = "0 0 10px rgba(0,0,0,0.5)";
|
||||
|
||||
panel.innerHTML = `
|
||||
<h3 style="margin-top:0;">AMD Monitor 📊</h3>
|
||||
|
||||
<div>GPU Usage: <span id="gpu_usage">0%</span></div>
|
||||
<div>VRAM: <span id="vram_usage">0 GB</span></div>
|
||||
<div>RAM: <span id="ram_usage">0 GB</span></div>
|
||||
<div>Timer: <span id="timer">0s</span></div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(panel);
|
||||
|
||||
let seconds = 0;
|
||||
|
||||
setInterval(() => {
|
||||
|
||||
seconds++;
|
||||
|
||||
document.getElementById("gpu_usage").innerText =
|
||||
Math.floor(Math.random() * 100) + "%";
|
||||
|
||||
document.getElementById("vram_usage").innerText =
|
||||
(Math.random() * 16).toFixed(2) + " GB";
|
||||
|
||||
document.getElementById("ram_usage").innerText =
|
||||
(Math.random() * 32).toFixed(2) + " GB";
|
||||
|
||||
document.getElementById("timer").innerText =
|
||||
seconds + "s";
|
||||
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user