add monitor beta

This commit is contained in:
Anonymzx
2026-05-15 17:22:00 +07:00
parent c05d34cd6f
commit 994eaee55a
11 changed files with 1067 additions and 67 deletions
+61 -1
View File
@@ -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}")
+18
View File
@@ -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"
]
-9
View File
@@ -1,9 +0,0 @@
from .translate_universal import (
NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS
)
__all__ = [
"NODE_CLASS_MAPPINGS",
"NODE_DISPLAY_NAME_MAPPINGS"
]
+57
View File
@@ -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
View File
@@ -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"
+3
View File
@@ -0,0 +1,3 @@
from .amd_server import AMDMonitorServer, get_amd_server
__all__ = ["AMDMonitorServer", "get_amd_server"]
+107
View File
@@ -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
+54
View File
@@ -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 ===")
+212
View File
@@ -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
+540
View File
@@ -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);
});
})();
-56
View File
@@ -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);
}
});