diff --git a/__init__.py b/__init__.py index 317696d..d5fadce 100644 --- a/__init__.py +++ b/__init__.py @@ -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" -] \ No newline at end of file +] + + +# ============================================================ +# š 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}") \ No newline at end of file diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..e4337b0 --- /dev/null +++ b/nodes/__init__.py @@ -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" +] \ No newline at end of file diff --git a/nodes/_init_.py b/nodes/_init_.py deleted file mode 100644 index dc8c771..0000000 --- a/nodes/_init_.py +++ /dev/null @@ -1,9 +0,0 @@ -from .translate_universal import ( - NODE_CLASS_MAPPINGS, - NODE_DISPLAY_NAME_MAPPINGS -) - -__all__ = [ - "NODE_CLASS_MAPPINGS", - "NODE_DISPLAY_NAME_MAPPINGS" -] \ No newline at end of file diff --git a/nodes/amd_monitor.py b/nodes/amd_monitor.py new file mode 100644 index 0000000..9d80652 --- /dev/null +++ b/nodes/amd_monitor.py @@ -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)" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 663bd1f..68220a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,15 @@ -requests \ No newline at end of file +# 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" \ No newline at end of file diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..aa59ec2 --- /dev/null +++ b/server/__init__.py @@ -0,0 +1,3 @@ +from .amd_server import AMDMonitorServer, get_amd_server + +__all__ = ["AMDMonitorServer", "get_amd_server"] \ No newline at end of file diff --git a/server/amd_server.py b/server/amd_server.py new file mode 100644 index 0000000..fffeb7a --- /dev/null +++ b/server/amd_server.py @@ -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 \ No newline at end of file diff --git a/test_amd_monitor.py b/test_amd_monitor.py new file mode 100644 index 0000000..fcaface --- /dev/null +++ b/test_amd_monitor.py @@ -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 ===") \ No newline at end of file diff --git a/utils/amd_utils.py b/utils/amd_utils.py new file mode 100644 index 0000000..e61f5f8 --- /dev/null +++ b/utils/amd_utils.py @@ -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 \ No newline at end of file diff --git a/web/amd_monitor.js b/web/amd_monitor.js new file mode 100644 index 0000000..9248b9a --- /dev/null +++ b/web/amd_monitor.js @@ -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 = ` +