// WebSocket Variablen let socket; let isConnected = false; const RECONNECT_INTERVAL = 5000; const HEARTBEAT_INTERVAL = 10000; let heartbeatTimer = null; let lastHeartbeatResponse = Date.now(); const HEARTBEAT_TIMEOUT = 20000; let reconnectTimer = null; // WebSocket Funktionen function startHeartbeat() { if (heartbeatTimer) clearInterval(heartbeatTimer); heartbeatTimer = setInterval(() => { // Prüfe ob zu lange keine Antwort kam if (Date.now() - lastHeartbeatResponse > HEARTBEAT_TIMEOUT) { isConnected = false; updateConnectionStatus(); if (socket) { socket.close(); socket = null; } return; } if (!socket || socket.readyState !== WebSocket.OPEN) { isConnected = false; updateConnectionStatus(); return; } try { // Sende Heartbeat socket.send(JSON.stringify({ type: 'heartbeat' })); } catch (error) { isConnected = false; updateConnectionStatus(); if (socket) { socket.close(); socket = null; } } }, HEARTBEAT_INTERVAL); } function initWebSocket() { // Clear any existing reconnect timer if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; } // Wenn eine existierende Verbindung besteht, diese erst schließen if (socket) { socket.close(); socket = null; } try { socket = new WebSocket('ws://' + window.location.host + '/ws'); socket.onopen = function() { isConnected = true; updateConnectionStatus(); startHeartbeat(); // Starte Heartbeat nach erfolgreicher Verbindung }; socket.onclose = function() { isConnected = false; updateConnectionStatus(); if (heartbeatTimer) clearInterval(heartbeatTimer); // Nur neue Verbindung versuchen, wenn kein Timer läuft if (!reconnectTimer) { reconnectTimer = setTimeout(() => { initWebSocket(); }, RECONNECT_INTERVAL); } }; socket.onerror = function(error) { isConnected = false; updateConnectionStatus(); if (heartbeatTimer) clearInterval(heartbeatTimer); // Bei Fehler Verbindung schließen und neu aufbauen if (socket) { socket.close(); socket = null; } }; socket.onmessage = function(event) { lastHeartbeatResponse = Date.now(); // Aktualisiere Zeitstempel bei jeder Server-Antwort const data = JSON.parse(event.data); if (data.type === 'amsData') { displayAmsData(data.payload); } else if (data.type === 'nfcTag') { updateNfcStatusIndicator(data.payload); } else if (data.type === 'nfcData') { updateNfcData(data.payload); } else if (data.type === 'writeNfcTag') { handleWriteNfcTagResponse(data.success); } else if (data.type === 'heartbeat') { // Optional: Spezifische Behandlung von Heartbeat-Antworten // Update status dots const bambuDot = document.getElementById('bambuDot'); const spoolmanDot = document.getElementById('spoolmanDot'); const ramStatus = document.getElementById('ramStatus'); if (bambuDot) { bambuDot.className = 'status-dot ' + (data.bambu_connected ? 'online' : 'offline'); // Add click handler only when offline if (!data.bambu_connected) { bambuDot.style.cursor = 'pointer'; bambuDot.onclick = function() { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'reconnect', payload: 'bambu' })); } }; } else { bambuDot.style.cursor = 'default'; bambuDot.onclick = null; } } if (spoolmanDot) { spoolmanDot.className = 'status-dot ' + (data.spoolman_connected ? 'online' : 'offline'); // Add click handler only when offline if (!data.spoolman_connected) { spoolmanDot.style.cursor = 'pointer'; spoolmanDot.onclick = function() { if (socket && socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'reconnect', payload: 'spoolman' })); } }; } else { spoolmanDot.style.cursor = 'default'; spoolmanDot.onclick = null; } } if (ramStatus) { ramStatus.textContent = `${data.freeHeap}k`; } } }; } catch (error) { isConnected = false; updateConnectionStatus(); // Nur neue Verbindung versuchen, wenn kein Timer läuft if (!reconnectTimer) { reconnectTimer = setTimeout(() => { initWebSocket(); }, RECONNECT_INTERVAL); } } } function updateConnectionStatus() { const statusElement = document.querySelector('.connection-status'); if (!isConnected) { statusElement.classList.remove('hidden'); // Verzögerung hinzufügen, damit die CSS-Transition wirken kann setTimeout(() => { statusElement.classList.add('visible'); }, 10); } else { statusElement.classList.remove('visible'); // Warte auf das Ende der Fade-out Animation bevor wir hidden setzen setTimeout(() => { statusElement.classList.add('hidden'); }, 300); } } // Event Listeners document.addEventListener("DOMContentLoaded", function() { initWebSocket(); // Event Listener für Checkbox document.getElementById("onlyWithoutSmId").addEventListener("change", function() { const spoolsData = window.getSpoolData(); window.populateVendorDropdown(spoolsData); }); }); // Event Listener für Spoolman Events document.addEventListener('spoolDataLoaded', function(event) { window.populateVendorDropdown(event.detail); }); document.addEventListener('spoolmanError', function(event) { showNotification(`Spoolman Error: ${event.detail.message}`, false); }); document.addEventListener('filamentSelected', function(event) { updateNfcInfo(); // Zeige Spool-Buttons wenn ein Filament ausgewählt wurde const selectedText = document.getElementById("selected-filament").textContent; updateSpoolButtons(selectedText !== "Please choose..."); }); // Hilfsfunktion für kontrastreiche Textfarbe function getContrastColor(hexcolor) { // Konvertiere Hex zu RGB const r = parseInt(hexcolor.substr(0,2),16); const g = parseInt(hexcolor.substr(2,2),16); const b = parseInt(hexcolor.substr(4,2),16); // Berechne Helligkeit (YIQ Formel) const yiq = ((r*299)+(g*587)+(b*114))/1000; // Return schwarz oder weiß basierend auf Helligkeit return (yiq >= 128) ? '#000000' : '#FFFFFF'; } function updateNfcInfo() { const selectedText = document.getElementById("selected-filament").textContent; const nfcInfo = document.getElementById("nfcInfo"); const writeButton = document.getElementById("writeNfcButton"); if (selectedText === "Please choose...") { nfcInfo.textContent = "No Filament selected"; nfcInfo.classList.remove("nfc-success", "nfc-error"); writeButton.classList.add("hidden"); return; } // Finde die ausgewählte Spule in den Daten const selectedSpool = spoolsData.find(spool => `${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText ); if (selectedSpool) { writeButton.classList.remove("hidden"); } else { writeButton.classList.add("hidden"); } } function displayAmsData(amsData) { const amsDataContainer = document.getElementById('amsData'); amsDataContainer.innerHTML = ''; amsData.forEach((ams) => { // Bestimme den Anzeigenamen für das AMS const amsDisplayName = ams.ams_id === 255 ? 'External Spool' : `AMS ${ams.ams_id}`; const trayHTML = ams.tray.map(tray => { // Prüfe ob überhaupt Daten vorhanden sind const relevantFields = ['tray_type', 'tray_sub_brands', 'tray_info_idx', 'setting_id', 'cali_idx']; const hasAnyContent = relevantFields.some(field => tray[field] !== null && tray[field] !== undefined && tray[field] !== '' && tray[field] !== 'null' ); // Bestimme den Anzeigenamen für das Tray const trayDisplayName = (ams.ams_id === 255) ? 'External' : `Tray ${tray.id}`; // Nur für nicht-leere Trays den Button-HTML erstellen const buttonHtml = ` `; // Nur für nicht-leere Trays den Button-HTML erstellen const outButtonHtml = ` `; if (!hasAnyContent) { return `
${trayDisplayName}
${(ams.ams_id === 255 && tray.tray_type === '') ? buttonHtml : ''} Empty
Typ: ${tray.tray_type} ${tray.tray_color ? `` : ''}
` : ''; // Array mit restlichen Tray-Eigenschaften const trayProperties = [ { key: 'tray_sub_brands', label: 'Sub Brands' }, { key: 'tray_info_idx', label: 'Filament IDX' }, { key: 'setting_id', label: 'Setting ID' }, { key: 'cali_idx', label: 'Calibration IDX' } ]; // Nur gültige Felder anzeigen const trayDetails = trayProperties .filter(prop => tray[prop.key] !== null && tray[prop.key] !== undefined && tray[prop.key] !== '' && tray[prop.key] !== 'null' ) .map(prop => { // Spezielle Behandlung für setting_id if (prop.key === 'cali_idx' && tray[prop.key] === '-1') { return `${prop.label}: not calibrated
`; } return `${prop.label}: ${tray[prop.key]}
`; }) .join(''); // Temperaturen nur anzeigen, wenn beide nicht 0 sind const tempHTML = (tray.nozzle_temp_min > 0 && tray.nozzle_temp_max > 0) ? `Nozzle Temp: ${tray.nozzle_temp_min}°C - ${tray.nozzle_temp_max}°C
` : ''; return `${trayDisplayName}
${typeWithColor} ${trayDetails} ${tempHTML} ${(ams.ams_id === 255 && tray.tray_type !== '') ? outButtonHtml : ''}Brand: ${data.brand || 'N/A'}
Type: ${data.type || 'N/A'} ${data.color_hex ? `` : ''}
`; // Spoolman ID anzeigen html += `Spoolman ID: ${data.sm_id || 'No Spoolman ID'}
`; // Nur wenn eine sm_id vorhanden ist, aktualisiere die Dropdowns if (data.sm_id) { const matchingSpool = spoolsData.find(spool => spool.id === parseInt(data.sm_id)); if (matchingSpool) { // Zuerst Hersteller-Dropdown aktualisieren document.getElementById("vendorSelect").value = matchingSpool.filament.vendor.id; // Dann Filament-Dropdown aktualisieren und Spule auswählen updateFilamentDropdown(); setTimeout(() => { // Warte kurz bis das Dropdown aktualisiert wurde selectFilament(matchingSpool); }, 100); } } html += '