AMD WORK NOW

This commit is contained in:
Anonymzx
2026-05-19 01:13:50 +07:00
parent feb6cf3201
commit 4e60b40366
4 changed files with 416 additions and 363 deletions
+67 -20
View File
@@ -1,15 +1,24 @@
# -*- coding: utf-8 -*-
"""
BANGTRIXTOOLKIT - ComfyUI Custom Nodes
======================================
Translate Universal + Universal Hardware Monitor Overlay
"""
import asyncio
import importlib
import logging
import sys as _sys
import os as _os
# Ensure this package is on sys.path for absolute imports to work
_pkg_dir = _os.path.dirname(_os.path.realpath(__file__))
if _pkg_dir not in _sys.path:
_sys.path.insert(0, _pkg_dir)
logger = logging.getLogger(__name__)
# Dynamic import — hanya load node yang benar-benar ada
# Dynamic import - hanya load node yang benar-benar ada
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
@@ -19,9 +28,9 @@ try:
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")
print("[BANGTRIX] Loaded translate_universal")
except Exception as e:
print(f"⚠️ BANGTRIXTOOLKIT: Skipped translate_universal: {e}")
print(f"[BANGTRIX] Skipped translate_universal: {e}")
WEB_DIRECTORY = "web"
@@ -33,36 +42,74 @@ __all__ = [
# ============================================================
# 🚀 SERVER EXTENSION — Universal Hardware Monitor WebSocket
# SERVER EXTENSION - Universal Hardware Monitor REST API
# ============================================================
# Register WebSocket route. Uses direct file path to avoid
# Python module naming issues with ComfyUI's load mechanism.
import sys as _sys, os as _os
# Provides:
# GET /bangtrix/hw/stats -> JSON GPU data (polling)
# WS /ws/hw_monitor -> WebSocket streaming (fallback)
import sys as _sys
import os as _os
from importlib import util as _util
try:
import server as _comfy_server
_PromptServer = _comfy_server.PromptServer
if _PromptServer.instance:
_app = _PromptServer.instance.app
_ps = _comfy_server.PromptServer
if _ps.instance:
_app = _ps.instance.app
from aiohttp import web
# Import hw_server directly via absolute file path
# Load hw_server module via file path
_hws_path = _os.path.join(_os.path.dirname(__file__), "monitor", "hw_server.py")
_spec = _util.spec_from_file_location("BangtrixToolkit_hw_server", _hws_path)
_hws_mod = _util.module_from_spec(_spec)
_sys.modules['BangtrixToolkit_hw_server'] = _hws_mod
_sys.modules["BangtrixToolkit_hw_server"] = _hws_mod
_spec.loader.exec_module(_hws_mod)
_get_hw_server = _hws_mod.get_hw_server
hw_server = _get_hw_server()
_get_hw = _hws_mod.get_hw_server
_hw_srv = _get_hw()
# --- REST API endpoint (primary) ---
async def rest_stats_handler(request):
data = _hw_srv.get_stats_json()
return web.json_response(data)
_app.router.add_get("/bangtrix/hw/stats", rest_stats_handler)
print("[BANGTRIX] REST API at /bangtrix/hw/stats")
# --- WebSocket endpoint (fallback) ---
async def ws_handler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
await hw_server.handle_client(ws)
_running = True
try:
while _running:
try:
data = _hw_srv.get_stats_json()
await ws.send_json(data)
except asyncio.CancelledError:
_running = False
break
except Exception:
pass
await asyncio.sleep(1.0)
except asyncio.CancelledError:
pass
except Exception:
pass
return ws
_app.router.add_get('/ws/hw_monitor', ws_handler)
print("🖥️ BANGTRIXTOOLKIT: HW Monitor WebSocket at /ws/hw_monitor")
_app.router.add_get("/ws/hw_monitor", ws_handler)
print("[BANGTRIX] WS at /ws/hw_monitor")
# --- Quick health check ---
async def health_handler(request):
return web.json_response({"status": "ok"})
_app.router.add_get("/bangtrix/hw/health", health_handler)
print("[BANGTRIX] Server extension registered")
else:
print("🖥️ BANGTRIXTOOLKIT: PromptServer pending")
print("[BANGTRIX] PromptServer pending")
except Exception as e:
print(f"⚠️ BANGTRIXTOOLKIT: Server extension skipped: {e}")
print(f"[BANGTRIX] Server extension skipped: {e}")
+18 -17
View File
@@ -28,24 +28,25 @@ class HardwareStats:
error_message: Optional[str] = None
def to_dict(self) -> dict:
"""Convert to dict with explicit Python native types for safe JSON."""
return {
'gpu_id': self.gpu_id,
'gpu_name': self.gpu_name,
'vendor': self.vendor,
'driver': self.driver,
'is_apu': self.is_apu,
'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,
'memory_shared': self.memory_shared,
'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,
'gpu_id': int(self.gpu_id),
'gpu_name': str(self.gpu_name or ""),
'vendor': str(self.vendor or "unknown"),
'driver': str(self.driver or ""),
'is_apu': bool(self.is_apu),
'utilization_gpu': round(float(self.utilization_gpu or 0), 1),
'utilization_memory': round(float(self.utilization_memory or 0), 1),
'memory_total': int(self.memory_total or 0),
'memory_used': int(self.memory_used or 0),
'memory_free': int(self.memory_free or 0),
'memory_shared': int(self.memory_shared or 0),
'temperature': round(float(self.temperature or 0), 1),
'fan_speed': int(self.fan_speed or 0),
'core_clock': int(self.core_clock or 0),
'memory_clock': int(self.memory_clock or 0),
'power_draw': round(float(self.power_draw or 0), 1),
'is_available': bool(self.is_available),
}
def to_mb(self, value: int) -> float:
+85 -136
View File
@@ -1,165 +1,114 @@
"""
Universal Hardware Monitor Server
==================================
WebSocket server for streaming GPU/Hardware stats to frontend.
Provides GPU/Hardware stats via:
1. REST API (primary) — GET /bangtrix/hw/stats returns JSON
2. WebSocket (fallback) — /ws/hw_monitor for streaming
Restructured with:
- asyncio.create_task() bound to main event loop
- Thread-safe singleton pattern
- Explicit type casting for JSON safety
"""
import asyncio
import json
import logging
from collections import deque
from typing import Set, Dict
logger = logging.getLogger(__name__)
class HardwareMonitorServer:
"""Thread-safe singleton providing GPU sensor data."""
_instance = None
_lock = None # Will be set from __init__.py
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if getattr(self, '_initialized', False):
return
self._initialized = True
self.monitor = None
self.clients: Set = set()
self.history = deque(maxlen=60)
self.running = False
self.update_interval = 0.5
self._stream_task = None
self._process_monitor = None
self.history_maxlen = 60
self.gpu_history: Dict[int, deque] = {}
self._task = None
def _get_monitor(self):
if self.monitor is None:
from . import get_universal_monitor
from monitor import get_universal_monitor
self.monitor = get_universal_monitor()
return self.monitor
def _get_process_monitor(self):
if self._process_monitor is None:
try:
from .process_monitor import get_process_monitor
self._process_monitor = get_process_monitor()
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):
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):
self.clients.discard(client_list[i])
async def handle_client(self, ws):
self.clients.add(ws)
# Auto-start streaming when first client connects
self.start_streaming()
logger.info(f"HW Monitor: Client connected. Total clients: {len(self.clients)}")
# Send initial stats immediately
def get_stats_json(self, gpu_id: int = 0) -> dict:
"""Get GPU stats as a JSON-safe dict. Call from any thread."""
try:
monitor = self._get_monitor()
data = self._build_stats_data(monitor)
await ws.send_str(json.dumps(data))
except Exception:
pass
try:
async for msg in ws:
if msg.type == 1:
await self._handle_message(ws, msg.data)
except asyncio.CancelledError:
pass
if not monitor.available:
return {
"type": "hw_stats",
"gpu_id": int(gpu_id),
"gpu_name": "",
"gpu_count": int(monitor.gpu_count),
"vendor": str(monitor.vendor),
"os_type": str(monitor.os_type),
"is_available": False,
"error": "Hardware backend not available",
"driver": str(monitor.driver),
"history": [],
}
stats = monitor.get_gpu_stats(gpu_id)
util = round(float(stats.utilization_gpu or 0), 1)
self.history.append(util)
return {
"type": "hw_stats",
"gpu_id": int(stats.gpu_id),
"gpu_name": str(stats.gpu_name or f"GPU {gpu_id}"),
"gpu_count": int(monitor.gpu_count),
"vendor": str(stats.vendor or monitor.vendor),
"os_type": str(monitor.os_type),
"driver": str(stats.driver or monitor.driver or "unknown"),
"is_apu": bool(stats.is_apu),
"is_available": bool(stats.is_available),
"gpu_utilization": util,
"vram_usage_pct": round(float(stats.safe_memory_pct() or 0), 1),
"vram_used_mb": int(round(stats.memory_used / (1024 * 1024), 0)) if stats.memory_used > 0 else 0,
"vram_total_mb": int(round(stats.memory_total / (1024 * 1024), 0)) if stats.memory_total > 0 else 0,
"vram_shared_mb": int(round(stats.memory_shared / (1024 * 1024), 0)) if stats.memory_shared > 0 else 0,
"temperature": round(float(stats.temperature or 0), 1),
"fan_speed": int(stats.fan_speed or 0),
"core_clock_mhz": int(stats.core_clock or 0),
"power_draw_watts": round(float(stats.power_draw or 0), 1),
"history": list(self.history),
"debug_info": {
"memory_total_raw": int(stats.memory_total),
"memory_used_raw": int(stats.memory_used),
"temperature_raw": float(stats.temperature),
"utilization_gpu_raw": float(stats.utilization_gpu),
"vendor_raw": str(stats.vendor),
"monitor_vendor": str(monitor.vendor),
},
}
except Exception as e:
logger.error(f"HW Monitor: Client error: {e}")
finally:
self.clients.discard(ws)
async def _handle_message(self, ws, raw: str):
try:
cmd = json.loads(raw)
cmd_type = cmd.get("type", "")
if cmd_type == "ping":
await ws.send_str(json.dumps({"type": "pong"}))
elif cmd_type == "request_stats":
monitor = self._get_monitor()
data = self._build_stats_data(monitor)
await ws.send_str(json.dumps(data))
except json.JSONDecodeError:
if raw == "ping":
await ws.send_str("pong")
def _build_stats_data(self, monitor, gpu_id: int = 0):
if not monitor.available:
return {"type": "hw_stats", "gpu_id": gpu_id, "gpu_name": "", "gpu_count": monitor.gpu_count,
"vendor": monitor.vendor, "os_type": monitor.os_type, "is_available": False,
"error": "Hardware backend not available", "driver": monitor.driver, "history": []}
stats = monitor.get_gpu_stats(gpu_id)
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)
return {
"type": "hw_stats",
"gpu_id": stats.gpu_id,
"gpu_name": stats.gpu_name or f"GPU {stats.gpu_id}",
"gpu_count": monitor.gpu_count,
"vendor": stats.vendor or monitor.vendor,
"os_type": monitor.os_type,
"driver": stats.driver or monitor.driver or "unknown",
"is_apu": stats.is_apu,
"is_available": stats.is_available,
"gpu_utilization": round(stats.utilization_gpu, 1),
"vram_usage_pct": round(stats.safe_memory_pct(), 1),
"vram_used_mb": round(stats.memory_used / (1024 * 1024), 0) if stats.memory_used > 0 else 0,
"vram_total_mb": round(stats.memory_total / (1024 * 1024), 0) if stats.memory_total > 0 else 0,
"vram_shared_mb": round(stats.memory_shared / (1024 * 1024), 0) if stats.memory_shared > 0 else 0,
"temperature": round(stats.temperature, 1) if stats.temperature > 0 else 0,
"fan_speed": stats.fan_speed if stats.fan_speed > 0 else 0,
"core_clock_mhz": stats.core_clock or 0,
"power_draw_watts": round(stats.power_draw, 1) if stats.power_draw > 0 else 0,
"history": list(self.gpu_history.get(gpu_id, [])),
}
async def stream_data(self):
monitor = self._get_monitor()
logger.info(f"HW Monitor: Stream 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):
await self.broadcast(json.dumps(self._build_stats_data(monitor, gpu_id)))
else:
await self.broadcast(json.dumps({"type": "hw_stats", "gpu_id": 0,
"is_available": False, "error": "No GPU detected", "history": []}))
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"HW Monitor: Stream error: {e}")
await asyncio.sleep(self.update_interval)
finally:
logger.info("HW Monitor: Stream ended")
def start_streaming(self):
if self.running:
return self._stream_task
self.running = True
self._stream_task = asyncio.ensure_future(self.stream_data())
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.error(f"HW Monitor: get_stats_json error: {e}")
return {
"type": "hw_stats",
"gpu_id": int(gpu_id),
"is_available": False,
"error": str(e),
"history": [],
}
_hw_server = None
def get_hw_server() -> HardwareMonitorServer:
global _hw_server
if _hw_server is None:
_hw_server = HardwareMonitorServer()
return _hw_server
"""Get singleton instance."""
return HardwareMonitorServer()
+246 -190
View File
@@ -1,47 +1,66 @@
/**
* BANGTRIXTOOLKIT — Universal Hardware Monitor Overlay v5.0
* Real-time GPU monitoring overlay for ComfyUI
* Supports: AMD, NVIDIA, Intel, APU/iGPU
* Platform: Windows, Linux
* BangtrixToolkit — Universal Hardware Monitor Overlay
* ComfyUI Extension
*
* WebSocket: /ws/hw_monitor
* Strategy: REST API polling via GET /bangtrix/hw/stats every 1s.
* Fallback: WebSocket /ws/hw_monitor.
* Toggle: Ctrl+Shift+M
*/
(function () {
(function() {
"use strict";
// === STATE ===
var ws = null;
var reconnectTimer = null;
var reconnectAttempts = 0;
const MAX_RECONNECT = 15;
console.log("🖥️ Bangtrix HW Monitor: loading...");
var widget = null;
var isMinimized = false;
var isVisible = true;
var isDragging = false;
var dragStart = { x: 0, y: 0 };
// ========== STATE ==========
let widget = null;
let isMinimized = false;
let isVisible = true;
let isDragging = false;
let dragStart = { x: 0, y: 0 };
let pollInterval = null;
let pollRetries = 0;
const MAX_RETRIES = 30; // 30 seconds max retry
var currentGpu = 0;
var gpuCount = 1;
var gpuData = {};
var historyData = [];
// === INIT ===
function init() {
createWidget();
loadConfig();
applyConfig();
bindEvents();
connect();
function $id(id) {
const el = document.getElementById(id);
if (!el) console.error("🖥️ DOM Element tidak ditemukan:", id);
return el;
}
// === WIDGET ===
function createWidget() {
// ========== INIT ==========
function init() {
console.log("🖥️ Bangtrix HW Monitor: initializing...");
// Cek duplikasi
if (document.getElementById('bangtrix-hw-monitor')) {
console.log("🖥️ Bangtrix HW Monitor: widget already exists, skipping");
return;
}
buildWidget();
bindEvents();
// Cek apakah element DOM terbuat
const checkEl = $id('hw-gpu-name');
console.log("🖥️ Bangtrix HW Monitor: hw-gpu-name element:", checkEl ? "✅ found" : "❌ NOT FOUND");
if (checkEl) {
checkEl.textContent = "Loading...";
}
// Start REST polling
startPolling();
}
// ========== WIDGET ==========
function buildWidget() {
console.log("🖥️ Bangtrix HW Monitor: building widget...");
widget = document.createElement('div');
widget.id = 'bangtrix-hw-monitor';
widget.innerHTML =
widget.innerHTML =
'<div class="hw-header">' +
'<span class="hw-icon">🖥️</span>' +
'<span class="hw-title">HW Monitor</span>' +
@@ -59,19 +78,22 @@
'<div class="hw-stat"><div class="hw-stat-label">Temp</div><div class="hw-stat-value" id="hw-temp">--</div><div class="hw-bar"><div class="hw-fill hw-temp-fill" id="hw-temp-bar"></div></div></div>' +
'<div class="hw-stat"><div class="hw-stat-label">Fan</div><div class="hw-stat-value" id="hw-fan">--</div><div class="hw-bar"><div class="hw-fill" id="hw-fan-bar"></div></div></div>' +
'</div>' +
'<canvas class="hw-sparkline" id="hw-sparkline" width="232" height="36"></canvas>' +
'<div class="hw-sparkline-container">' +
'<canvas class="hw-sparkline" id="hw-sparkline" width="232" height="36"></canvas>' +
'</div>' +
'<div class="hw-status-bar">' +
'<span class="hw-status" id="hw-status">Connecting...</span>' +
'<span class="hw-status" id="hw-status">Starting...</span>' +
'<span class="hw-method" id="hw-method"></span>' +
'</div>' +
'</div>';
var css = document.createElement('style');
css.textContent =
// Inject CSS
const css = document.createElement('style');
css.textContent =
'#bangtrix-hw-monitor{position:fixed;top:60px;right:20px;width:260px;' +
'background:rgba(18,18,24,0.92);border:1px solid rgba(100,150,255,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);' +
'color:#e0e0e0;z-index:99999;box-shadow:0 8px 32px rgba(0,0,0,0.4);' +
'backdrop-filter:blur(8px);user-select:none;}' +
'#bangtrix-hw-monitor.hidden{display:none}' +
'#bangtrix-hw-monitor.minimized .hw-body{display:none}' +
@@ -97,7 +119,8 @@
'.hw-bar{height:3px;background:rgba(255,255,255,0.08);border-radius:2px;overflow:hidden}' +
'.hw-fill{height:100%;width:0%;background:#00e676;border-radius:2px;transition:width 0.3s ease}' +
'.hw-fill.hw-temp-fill{background:linear-gradient(90deg,#00e676,#ffaa00,#ff4444)}' +
'.hw-sparkline{width:100%;height:36px;margin-top:6px;border-radius:4px;background:rgba(0,0,0,0.2)}' +
'.hw-sparkline-container{margin-top:6px}' +
'.hw-sparkline{width:100%;height:36px;border-radius:4px;background:rgba(0,0,0,0.2)}' +
'.hw-status-bar{display:flex;justify-content:space-between;margin-top:6px;padding-top:6px;' +
'border-top:1px solid rgba(255,255,255,0.04);font-size:9px;color:#666}' +
'.hw-status.live{color:#00e676;animation:livePulse 1.5s infinite}' +
@@ -105,13 +128,16 @@
'@keyframes livePulse{0%,100%{color:#00e676}50%{color:#00e67688}}';
document.head.appendChild(css);
document.body.appendChild(widget);
console.log("🖥️ Bangtrix HW Monitor: widget built ✅");
makeDraggable();
}
// === DRAG ===
// ========== DRAG ==========
function makeDraggable() {
var header = widget.querySelector('.hw-header');
header.addEventListener('mousedown', function (e) {
const header = widget.querySelector('.hw-header');
if (!header) return;
header.addEventListener('mousedown', function(e) {
if (e.target.closest('.hw-btn')) return;
isDragging = true;
dragStart.x = e.clientX - widget.offsetLeft;
@@ -123,8 +149,8 @@
});
function onDrag(e) {
if (!isDragging) return;
var x = Math.max(10, Math.min(e.clientX - dragStart.x, window.innerWidth - widget.offsetWidth + 10));
var y = Math.max(10, Math.min(e.clientY - dragStart.y, window.innerHeight - widget.offsetHeight + 10));
const x = Math.max(10, Math.min(e.clientX - dragStart.x, window.innerWidth - widget.offsetWidth + 10));
const y = Math.max(10, Math.min(e.clientY - dragStart.y, window.innerHeight - widget.offsetHeight + 10));
widget.style.left = x + 'px';
widget.style.top = y + 'px';
widget.style.right = 'auto';
@@ -135,119 +161,156 @@
document.body.style.cursor = '';
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
saveConfig();
}
}
// === EVENTS ===
// ========== EVENTS ==========
function bindEvents() {
document.getElementById('hw-btn-min').onclick = function () {
const minBtn = document.getElementById('hw-btn-min');
if (minBtn) minBtn.onclick = function() {
isMinimized = !isMinimized;
widget.classList.toggle('minimized', isMinimized);
this.textContent = isMinimized ? '+' : '−';
saveConfig();
};
document.getElementById('hw-btn-close').onclick = function () {
const closeBtn = document.getElementById('hw-btn-close');
if (closeBtn) closeBtn.onclick = function() {
isVisible = false;
widget.classList.add('hidden');
saveConfig();
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
};
document.addEventListener('keydown', function (e) {
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'M') {
e.preventDefault();
isVisible = !isVisible;
widget.classList.toggle('hidden', !isVisible);
if (isVisible) connect();
saveConfig();
if (isVisible) startPolling();
}
});
}
// === CONFIG ===
function loadConfig() {
try {
var c = JSON.parse(localStorage.getItem('bangtrix_hw_config') || '{}');
if (c.pos) { widget.style.left = c.pos.x; widget.style.top = c.pos.y; widget.style.right = 'auto'; }
if (c.min) { isMinimized = c.min; widget.classList.toggle('minimized', true);
var b = document.getElementById('hw-btn-min'); if (b) b.textContent = '+'; }
if (c.vis === false) { isVisible = false; widget.classList.add('hidden'); }
} catch (e) {}
}
function saveConfig() {
try {
localStorage.setItem('bangtrix_hw_config', JSON.stringify({
pos: { x: widget.style.left, y: widget.style.top },
min: isMinimized,
vis: isVisible
}));
} catch (e) {}
}
function applyConfig() {
// Minimal - no settings panel needed
// ========== REST POLLING (PRIMARY) ==========
function startPolling() {
console.log("🖥️ Bangtrix HW Monitor: starting REST polling...");
setStatus("Connecting...", "");
// Immediate first poll
fetchStats();
// Then every 1s
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(fetchStats, 1000);
console.log("🖥️ Bangtrix HW Monitor: polling every 1000ms ✅");
}
// === WEBSOCKET ===
function connect() {
function fetchStats() {
fetch('/bangtrix/hw/stats')
.then(function(response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
})
.then(function(data) {
console.log("🖥️ Fetch HW data:", JSON.stringify(data).substring(0, 200) + "...");
pollRetries = 0;
if (data && data.type === 'hw_stats') {
setStatus("● LIVE", "live");
setMethod("REST 1s");
updateDisplay(data);
} else {
console.warn("🖥️ Fetch HW data: unexpected format", data);
}
})
.catch(function(err) {
pollRetries++;
console.warn("🖥️ Fetch HW error (" + pollRetries + "/" + MAX_RETRIES + "):", err.message);
setStatus("Retry " + pollRetries, "err");
setMethod("");
// After 5 failed retries with REST, try WebSocket fallback
if (pollRetries === 5) {
console.log("🖥️ REST failed, trying WebSocket fallback...");
startWebSocket();
}
});
}
// ========== WEBSOCKET FALLBACK ==========
let ws = null;
let wsRetries = 0;
const MAX_WS_RETRIES = 10;
function startWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) return;
if (!isVisible) return;
var url = (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/ws/hw_monitor';
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = protocol + '//' + location.host + '/ws/hw_monitor';
console.log("🖥️ Trying WebSocket fallback:", url);
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
setStatus('Connecting...', '');
ws = new WebSocket(url);
ws.onopen = function () {
setStatus('● LIVE', 'live');
setMethod('500ms');
reconnectAttempts = 0;
ws.onopen = function() {
console.log("🖥️ WebSocket connected ✅");
setStatus("● LIVE", "live");
setMethod("WS 1s");
wsRetries = 0;
};
ws.onmessage = function (ev) {
ws.onmessage = function(ev) {
try {
var d = JSON.parse(ev.data);
if (d.type === 'hw_stats') onStats(d);
} catch (e) {}
};
ws.onclose = function () {
setStatus('Disconnected', 'err');
setMethod('');
if (reconnectAttempts < MAX_RECONNECT) {
reconnectAttempts++;
setTimeout(connect, Math.min(1000 * Math.pow(1.5, reconnectAttempts), 10000));
const d = JSON.parse(ev.data);
console.log("🖥️ WS data received");
if (d.type === 'hw_stats') updateDisplay(d);
} catch(e) {
console.warn("🖥️ WS JSON error:", e.message);
}
};
ws.onerror = function () {
if (reconnectAttempts === 0) setTimeout(connect, 2000);
ws.onclose = function() {
console.log("🖥️ WebSocket disconnected");
setStatus("WS Disc.", "err");
if (wsRetries < MAX_WS_RETRIES) {
wsRetries++;
setTimeout(startWebSocket, 2000);
}
};
ws.onerror = function() {
console.warn("🖥️ WebSocket error");
};
}
// === DATA ===
function onStats(d) {
gpuData[d.gpu_id] = d;
if (d.gpu_count && d.gpu_count !== gpuCount) gpuCount = d.gpu_count;
if (d.gpu_id === currentGpu) updateDisplay(d);
}
function $id(id) { return document.getElementById(id); }
// ========== DISPLAY UPDATE ==========
function updateDisplay(d) {
var nameEl = $id('hw-gpu-name');
var vendorEl = $id('hw-vendor-line');
if (nameEl) nameEl.textContent = d.gpu_name || ('GPU ' + d.gpu_id);
// === GPU Name ===
const nameEl = $id('hw-gpu-name');
if (nameEl) {
const newName = d.gpu_name || ('GPU ' + (d.gpu_id || 0));
nameEl.textContent = newName;
console.log("🖥️ GPU name set to:", newName);
}
// === Vendor ===
const vendorEl = $id('hw-vendor-line');
if (vendorEl) {
var parts = [];
const parts = [];
if (d.vendor) parts.push(d.vendor.toUpperCase());
if (d.driver) parts.push(d.driver);
if (d.is_apu) parts.push('APU');
if (d.os_type) parts.push(d.os_type);
vendorEl.textContent = parts.join(' | ');
vendorEl.textContent = parts.join(' | ') || '-';
}
// === Unavailable ===
if (!d.is_available) {
setUtil('hw-gpu-util', '--', 0);
setUtil('hw-vram', '--', 0);
setUtil('hw-temp', '--', 0);
setUtil('hw-fan', '--', 0);
console.warn("🖥️ GPU unavailable:", d.error || "no error");
setUtil('hw-gpu-util', d.error || '--', 0);
setUtil('hw-vram', d.error || '--', 0);
setUtil('hw-temp', d.error || '--', 0);
setUtil('hw-fan', d.error || '--', 0);
setBar('hw-gpu-bar', 0);
setBar('hw-vram-bar', 0);
setBar('hw-temp-bar', 0);
@@ -256,43 +319,37 @@
return;
}
// GPU Util
var util = d.gpu_utilization || 0;
setUtil('hw-gpu-util', util.toFixed(1) + '%', util);
// === GPU ===
const util = d.gpu_utilization || 0;
setUtil('hw-gpu-util', Number(util).toFixed(1) + '%', util);
setBar('hw-gpu-bar', util);
console.log("🖥️ GPU util:", util);
// VRAM
var vramUsed = d.vram_used_mb || 0;
var vramTotal = d.vram_total_mb || 0;
var vramShared = d.vram_shared_mb || 0;
var vramPct = d.vram_usage_pct || 0;
if (vramTotal > 0 && vramUsed > 0) {
setUtil('hw-vram', (vramUsed/1024).toFixed(2) + ' / ' + (vramTotal/1024).toFixed(1) + ' GB', vramPct);
// === VRAM ===
const vramUsed = d.vram_used_mb || 0;
const vramTotal = d.vram_total_mb || 0;
const vramPct = d.vram_usage_pct || 0;
if (vramTotal > 0) {
setUtil('hw-vram', (vramUsed / 1024).toFixed(2) + ' / ' + (vramTotal / 1024).toFixed(1) + ' GB', vramPct);
setBar('hw-vram-bar', vramPct);
} else if (vramShared > 0) {
setUtil('hw-vram', (vramUsed/1024).toFixed(2) + ' / ' + (vramShared/1024).toFixed(1) + ' GB (shared)', vramPct);
setBar('hw-vram-bar', vramPct);
} else if (vramTotal > 0) {
setUtil('hw-vram', '0 / ' + (vramTotal/1024).toFixed(1) + ' GB', 0);
setBar('hw-vram-bar', 0);
console.log("🖥️ VRAM:", vramUsed, "/", vramTotal, "MB =", vramPct, "%");
} else {
setUtil('hw-vram', 'N/A', 0);
setBar('hw-vram-bar', 0);
}
// Temp
var temp = d.temperature || 0;
// === Temp ===
const temp = d.temperature || 0;
if (temp > 0) {
setUtil('hw-temp', temp.toFixed(1) + '°C', temp);
setUtil('hw-temp', Number(temp).toFixed(1) + '°C', temp);
setBar('hw-temp-bar', Math.min(temp, 100));
} else {
setUtil('hw-temp', 'N/A', 0);
setBar('hw-temp-bar', 0);
}
// Fan
var fan = d.fan_speed || 0;
// === Fan ===
const fan = d.fan_speed || 0;
if (fan > 0) {
setUtil('hw-fan', fan + '%', fan);
setBar('hw-fan-bar', fan);
@@ -301,110 +358,109 @@
setBar('hw-fan-bar', 0);
}
// Method info
var info = d.driver || 'unknown';
// === Method info ===
let info = d.driver || 'unknown';
if (d.core_clock_mhz > 0) info += ' | ' + d.core_clock_mhz + 'MHz';
if (d.power_draw_watts > 0) info += ' | ' + d.power_draw_watts + 'W';
setMethod(info);
setStatus('● LIVE', 'live');
// Sparkline
// === Sparkline ===
if (d.history && d.history.length > 0) {
historyData = d.history;
drawSparkline(historyData);
drawSparkline(d.history);
}
}
function setUtil(id, text, pct) {
var el = $id(id);
if (!el) return;
const el = $id(id);
if (!el) {
console.error("🖥️ DOM Element untuk", id, "tidak ditemukan!");
return;
}
el.textContent = text;
el.className = 'hw-stat-value';
if (pct !== undefined && pct !== null) {
if (pct > 85) el.classList.add('crit');
else if (pct > 70) el.classList.add('warn');
}
if (pct > 85) el.classList.add('crit');
else if (pct > 70) el.classList.add('warn');
}
function setBar(id, pct) {
var el = $id(id);
const el = $id(id);
if (el) el.style.width = Math.min(100, Math.max(0, pct || 0)) + '%';
}
function setStatus(text, cls) {
var el = $id('hw-status');
const el = $id('hw-status');
if (el) { el.textContent = text; el.className = 'hw-status ' + (cls || ''); }
var icon = widget.querySelector('.hw-icon');
if (icon) icon.textContent = cls === 'live' ? '🟢' : cls === 'err' ? '🔴' : '🟡';
if (widget) {
const icon = widget.querySelector('.hw-icon');
if (icon) icon.textContent = cls === 'live' ? '🟢' : cls === 'err' ? '🔴' : '🟡';
}
}
function setMethod(text) {
var el = $id('hw-method');
const el = $id('hw-method');
if (el) el.textContent = text || '';
}
// === SPARKLINE ===
// ========== SPARKLINE ==========
function drawSparkline(values) {
var canvas = $id('hw-sparkline');
if (!canvas || values.length < 2) return;
var ctx = canvas.getContext('2d');
var w = canvas.width, h = canvas.height, pad = 3;
const canvas = $id('hw-sparkline');
if (!canvas || !values || values.length < 2) return;
const ctx = canvas.getContext('2d');
const w = canvas.width, h = canvas.height, pad = 3;
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.lineWidth = 1;
for (var y = 0; y <= 3; y++) {
ctx.beginPath(); ctx.moveTo(0, pad + (y/3)*(h-pad*2)); ctx.lineTo(w, pad + (y/3)*(h-pad*2)); ctx.stroke();
}
const dataMax = Math.max.apply(null, values);
const max = dataMax > 80 ? 100 : (dataMax < 1 ? 100 : dataMax * 1.2);
const pw = w - pad * 2, ph = h - pad * 2;
var dataMax = Math.max.apply(null, values);
var max = dataMax > 80 ? 100 : (dataMax < 1 ? 100 : dataMax * 1.2);
var pw = w - pad * 2, ph = h - pad * 2;
// Area fill
ctx.beginPath();
ctx.moveTo(pad, h - pad);
for (var i = 0; i < values.length; i++) {
var x = pad + (i / (values.length - 1)) * pw;
var y = h - pad - (values[i] / max) * ph;
for (let i = 0; i < values.length; i++) {
const x = pad + (i / (values.length - 1)) * pw;
const y = h - pad - (values[i] / max) * ph;
ctx.lineTo(x, y);
}
ctx.lineTo(pad + pw, h - pad);
ctx.closePath();
var grad = ctx.createLinearGradient(0, 0, 0, h);
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, '#00e67633');
grad.addColorStop(1, '#00e67605');
ctx.fillStyle = grad;
ctx.fill();
// Line
ctx.beginPath();
ctx.strokeStyle = '#00e676';
ctx.lineWidth = 1.5;
ctx.lineJoin = 'round';
for (var i = 0; i < values.length; i++) {
var x = pad + (i / (values.length - 1)) * pw;
var y = h - pad - (values[i] / max) * ph;
for (let i = 0; i < values.length; i++) {
const x = pad + (i / (values.length - 1)) * pw;
const y = h - pad - (values[i] / max) * ph;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
// Dot
var lx = pad + pw;
var ly = h - pad - (values[values.length-1] / max) * ph;
ctx.beginPath(); ctx.arc(lx, ly, 3, 0, Math.PI * 2);
const lx = pad + pw;
const ly = h - pad - (values[values.length - 1] / max) * ph;
ctx.beginPath();
ctx.arc(lx, ly, 3, 0, Math.PI * 2);
ctx.fillStyle = '#00e676';
ctx.fill();
// Label
ctx.fillStyle = '#00e676';
ctx.font = 'bold 10px monospace';
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.fillText(values[values.length-1].toFixed(0) + '%', w - pad, pad);
}
// === START ===
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
// ========== START ==========
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// Fallback startup - try again if first attempt missed
setTimeout(function() {
if (!document.getElementById('bangtrix-hw-monitor')) {
console.log("🖥️ Bangtrix HW Monitor: fallback init...");
init();
}
}, 3000);
console.log("🖥️ Bangtrix HW Monitor: loaded ✅");
})();