This commit is contained in:
2025-02-12 21:10:25 +01:00
commit ec0d7d63de
44 changed files with 8699 additions and 0 deletions

70
html/bambu_filaments.json Normal file
View File

@ -0,0 +1,70 @@
{
"GFU99": "Generic TPU",
"GFN99": "Generic PA",
"GFN98": "Generic PA-CF",
"GFA01": "Bambu PLA Matte",
"GFA00": "Bambu PLA Basic",
"GFA09": "Bambu PLA Tough",
"GFA07": "Bambu PLA Marble",
"GFA08": "Bambu PLA Sparkle",
"GFA02": "Bambu PLA Metal",
"GFA05": "Bambu PLA Silk",
"GFS00": "Bambu Support W",
"GFL03": "eSUN PLA+",
"GFL01": "PolyTerra PLA",
"GFL00": "PolyLite PLA",
"GFL99": "Generic PLA",
"GFL96": "Generic PLA Silk",
"GFL98": "Generic PLA-CF",
"GFA50": "Bambu PLA-CF",
"GFS02": "Bambu Support For PLA",
"GFA11": "Bambu PLA Aero",
"GFL04": "Overture PLA",
"GFL05": "Overture Matte PLA",
"GFL95": "Generic PLA High Speed",
"GFA12": "Bambu PLA Glow",
"GFA13": "Bambu PLA Dynamic",
"GFA15": "Bambu PLA Galaxy",
"GFS05": "Bambu Support For PLA/PETG",
"GFU01": "Bambu TPU 95A",
"GFU00": "Bambu TPU 95A HF",
"GFG00": "Bambu PETG Basic",
"GFT01": "Bambu PET-CF",
"GFG99": "Generic PETG",
"GFG98": "Generic PETG-CF",
"GFG50": "Bambu PETG-CF",
"GFG60": "PolyLite PETG",
"GFG01": "Bambu PETG Translucent",
"GFG97": "Generic PCTG",
"GFB00": "Bambu ABS",
"GFB99": "Generic ABS",
"GFB60": "PolyLite ABS",
"GFB50": "Bambu ABS-GF",
"GFC00": "Bambu PC",
"GFC99": "Generic PC",
"GFB98": "Generic ASA",
"GFB01": "Bambu ASA",
"GFB61": "PolyLite ASA",
"GFB02": "Bambu ASA-Aero",
"GFS99": "Generic PVA",
"GFS04": "Bambu PVA",
"GFS01": "Bambu Support G",
"GFN03": "Bambu PA-CF",
"GFN04": "Bambu PAHT-CF",
"GFS03": "Bambu Support For PA/PET",
"GFN05": "Bambu PA6-CF",
"GFN08": "Bambu PA6-GF",
"GFS98": "Generic HIPS",
"GFT98": "Generic PPS-CF",
"GFT97": "Generic PPS",
"GFN97": "Generic PPA-CF",
"GFN96": "Generic PPA-GF",
"GFP99": "Generic PE",
"GFP98": "Generic PE-CF",
"GFP97": "Generic PP",
"GFP96": "Generic PP-CF",
"GFP95": "Generic PP-GF",
"GFR99": "Generic EVA",
"GFR98": "Generic PHA",
"GFS97": "Generic BVOH"
}

BIN
html/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

51
html/header.html Normal file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FilaMan - Filament Management Tool</title>
<link rel="icon" type="image/png" href="/favicon.ico">
<link rel="stylesheet" href="style.css">
<style>
.status-container {
float: right;
display: flex;
gap: 10px;
align-items: center;
margin-right: 10px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 4px;
}
.status-item {
display: flex;
align-items: center;
font-size: 0.8em;
color: #fff;
}
.online { background-color: #2ecc71; }
.offline { background-color: #e74c3c; }
.ram-status { color: #fff; font-size: 0.8em; }
</style>
</head>
<body>
<div class="navbar">
<img src="/logo.png" alt="FilaMan Logo" class="logo">
<a href="/">Start</a>
<a href="/waage">Scale</a>
<a href="/spoolman">Spoolman/Bambu</a>
<a href="/about">About</a>
<div class="status-container">
<div class="status-item">
<span class="status-dot" id="bambuDot"></span>B
</div>
<div class="status-item">
<span class="status-dot" id="spoolmanDot"></span>S
</div>
<div class="ram-status" id="ramStatus"></div>
</div>
</div>

37
html/index.html Normal file
View File

@ -0,0 +1,37 @@
{{header}}
<div class="container">
<h1>FilaMan</h1>
<p>Filament Management Tool</p>
<p>Your smart solution for <strong>Filament Management</strong> in 3D printing.</p>
<h2>About FilaMan</h2>
<p>
FilaMan is a tool designed to simplify filament spool management. It allows you to identify and weigh filament spools,
automatically sync data with the self-hosted <a href="https://github.com/Donkie/Spoolman" target="_blank">Spoolman</a> platform,
and ensure compatibility with <a href="https://github.com/spuder/OpenSpool" target="_blank">OpenSpool</a> for Bambu printers.
</p>
<div class="features">
<div class="feature">
<h3>Spool Identification</h3>
<p>Easily identify filament spools using NFC tags (NTag215 or larger).</p>
</div>
<div class="feature">
<h3>Automatic Syncing</h3>
<p>Seamlessly update spool data with Spoolman for accurate tracking.</p>
</div>
<div class="feature">
<h3>OpenSpool Compatibility</h3>
<p>Works with OpenSpool to recognize and activate spools on Bambu printers.</p>
</div>
</div>
<h2>Future Plans</h2>
<p>
We are working on expanding compatibility to support smaller NFC tags like NTag213
and developing custom software to enhance the OpenSpool experience.
</p>
</div>
</body>
</html>

BIN
html/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

114
html/rfid.html Normal file
View File

@ -0,0 +1,114 @@
{{header}}
<div class="connection-status hidden">
<div class="spinner"></div>
<span>Connection lost. Trying to reconnect...</span>
</div>
<div class="content">
<div class="three-column-layout">
<!-- Linke Spalte -->
<div class="column">
<div class="feature-box">
<div class="statistics-header">
<h2>Statistics</h2>
<button id="refreshSpoolman" class="refresh-button">
<span>Refresh Spoolman</span>
</button>
</div>
<div class="statistics-column">
<h3>Spools</h3>
<div class="spool-stat" style="display: flex; justify-content: center; align-items: center;">
<span class="stat-label">total:</span>
<span class="stat-value" id="totalSpools"></span>
<div style="width: auto;"></div>
<span class="stat-label">without Tag:</span>
<span class="stat-value" id="spoolsWithoutTag"></span>
</div>
</div>
<div class="statistics-grid">
<div class="statistics-column">
<h3>Overview</h3>
<ul class="statistics-list">
<li>
<span class="stat-label">Manufacturer:</span>
<span class="stat-value" id="totalVendors"></span>
</li>
<li>
<span class="stat-label">Weight:</span>
<span class="stat-value"><span id="totalWeight"></span> kg</span>
</li>
<li>
<span class="stat-label">Length:</span>
<span class="stat-value"><span id="totalLength"></span> m</span>
</li>
</ul>
</div>
<div class="statistics-column">
<h3>Materials</h3>
<ul class="statistics-list" id="materialsList">
<!-- Wird dynamisch befüllt -->
</ul>
</div>
</div>
</div>
<div class="feature-box">
<div class="nfc-header">
<h2>NFC-Tag</h2>
<span id="nfcStatusIndicator" class="status-circle"></span>
</div>
<div class="nfc-status-display"></div>
</div>
</div>
<!-- Mittlere Spalte -->
<div class="column">
<div class="feature-box">
<h2>Spoolman Spools</h2>
<h2>1. select Manufacturer</h2>
<label for="vendorSelect">Manufacturer:</label>
<div style="display: flex; justify-content: space-between; align-items: center;">
<select id="vendorSelect" class="styled-select">
<option value="">Please choose...</option>
</select>
<label style="margin-left: 10px;">
<input type="checkbox" id="onlyWithoutSmId" checked onchange="updateFilamentDropdown()">
Only Spools without SM ID
</label>
</div>
</div>
<div id="filamentSection" class="feature-box hidden">
<h2>2. Select Spool</h2>
<label>Spool / Filament:</label>
<div class="custom-dropdown">
<div class="dropdown-button" onclick="toggleFilamentDropdown()">
<div class="selected-color" id="selected-color"></div>
<span id="selected-filament">Please choose...</span>
<span class="dropdown-arrow"></span>
</div>
<div class="dropdown-content" id="filament-dropdown-content">
<!-- Optionen werden dynamisch hinzugefügt -->
</div>
</div>
<p id="nfcInfo" class="nfc-status"></p>
<button id="writeNfcButton" class="btn btn-primary hidden" onclick="writeNfcTag()">Write Tag</button>
</div>
</div>
<!-- Rechte Spalte -->
<div class="column">
<div class="feature-box">
<h2>Bambu AMS</h2>
<div id="amsDataContainer">
<div class="amsData" id="amsData">Wait for AMS-Data...</div>
</div>
</div>
</div>
</div>
</div>
<script src="spoolman.js"></script>
<script src="rfid.js"></script>
</body>
</html>

570
html/rfid.js Normal file
View File

@ -0,0 +1,570 @@
// 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');
}
if (spoolmanDot) {
spoolmanDot.className = 'status-dot ' + (data.spoolman_connected ? 'online' : 'offline');
}
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 && selectedSpool.extra.nfc_id) {
nfcInfo.textContent = "NFC Tag assigned";
nfcInfo.classList.add("nfc-success");
nfcInfo.classList.remove("nfc-error");
} else {
nfcInfo.textContent = "No NFC-Tag assigned";
nfcInfo.classList.add("nfc-error");
nfcInfo.classList.remove("nfc-success");
}
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'];
const hasAnyContent = relevantFields.some(field =>
tray[field] !== null &&
tray[field] !== undefined &&
tray[field] !== '' &&
tray[field] !== 'null'
);
if (!hasAnyContent) {
return `
<div class="tray">
<p><b>Tray ${tray.id}</b></p>
<p>Empty</p>
</div>
<hr>`;
}
// Nur für nicht-leere Trays den Button-HTML erstellen
const buttonHtml = `
<button class="spool-button" onclick="handleSpoolIn(${ams.ams_id}, ${tray.id})"
style="position: absolute; top: 5px; left: 5px;
background: none; border: none; padding: 0;
cursor: pointer; display: none;">
<img src="spool_in.png" alt="Spool In" style="width: 48px; height: 48px;">
</button>`;
// Generiere den Type mit Color-Box zusammen
const typeWithColor = tray.tray_type ?
`<p>Typ: ${tray.tray_type} ${tray.tray_color ? `<span style="
background-color: #${tray.tray_color};
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
border: 1px solid #333;
border-radius: 3px;
margin-left: 5px;"></span>` : ''}</p>` : '';
// Array mit restlichen Tray-Eigenschaften
const trayProperties = [
{ key: 'tray_sub_brands', label: 'Sub Brands' },
{ key: 'tray_info_idx', label: 'Filament Index' },
{ key: 'setting_id', label: 'Setting ID' }
];
// 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 => `<p>${prop.label}: ${tray[prop.key]}</p>`)
.join('');
// Temperaturen nur anzeigen, wenn beide nicht 0 sind
const tempHTML = (tray.nozzle_temp_min > 0 && tray.nozzle_temp_max > 0)
? `<p>Nozzle Temp: ${tray.nozzle_temp_min}°C - ${tray.nozzle_temp_max}°C</p>`
: '';
// Bestimme den Anzeigenamen für das Tray
const trayDisplayName = (ams.ams_id === 255) ? 'External' : `Tray ${tray.id}`;
return `
<div class="tray" ${tray.tray_color ? `style="border-left: 4px solid #${tray.tray_color};"` : 'style="border-left: 4px solid #007bff;"'}>
<div style="position: relative;">
${buttonHtml}
<p><b>${trayDisplayName}</b></p>
${typeWithColor}
${trayDetails}
${tempHTML}
</div>
</div>
<hr>`;
}).join('');
const amsInfo = `
<div class="feature">
<h3>${amsDisplayName}:</h3>
<div id="trayContainer">
${trayHTML}
</div>
</div>`;
amsDataContainer.innerHTML += amsInfo;
});
}
// Neue Funktion zum Anzeigen/Ausblenden der Spool-Buttons
function updateSpoolButtons(show) {
const spoolButtons = document.querySelectorAll('.spool-button');
spoolButtons.forEach(button => {
button.style.display = show ? 'block' : 'none';
});
}
// Neue Funktion zum Behandeln des Spool-In-Klicks
function handleSpoolIn(amsId, trayId) {
// Prüfe WebSocket Verbindung zuerst
if (!socket || socket.readyState !== WebSocket.OPEN) {
showNotification("No active WebSocket connection!", false);
console.error("WebSocket not connected");
return;
}
// Hole das ausgewählte Filament
const selectedText = document.getElementById("selected-filament").textContent;
if (selectedText === "Please choose...") {
showNotification("Choose Filament first", false);
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) {
showNotification("Selected Spool not found", false);
return;
}
// Temperaturwerte extrahieren
let minTemp = "175";
let maxTemp = "275";
if (Array.isArray(selectedSpool.filament.nozzle_temperature) &&
selectedSpool.filament.nozzle_temperature.length >= 2) {
minTemp = selectedSpool.filament.nozzle_temperature[0];
maxTemp = selectedSpool.filament.nozzle_temperature[1];
}
// Erstelle Payload
const payload = {
type: 'setBambuSpool',
payload: {
amsId: amsId,
trayId: trayId,
color: selectedSpool.filament.color_hex || "FFFFFF",
nozzle_temp_min: parseInt(minTemp),
nozzle_temp_max: parseInt(maxTemp),
type: selectedSpool.filament.material,
brand: selectedSpool.filament.vendor.name
}
};
// Debug logging
console.log("Sende WebSocket Nachricht:", payload);
try {
socket.send(JSON.stringify(payload));
showNotification(`Spool set in AMS ${amsId} Tray ${trayId}. Pls wait`, true);
} catch (error) {
console.error("Fehler beim Senden der WebSocket Nachricht:", error);
showNotification("Fehler beim Senden der Daten", false);
}
}
function updateNfcStatusIndicator(data) {
const indicator = document.getElementById('nfcStatusIndicator');
if (data.found === 0) {
// Kein NFC Tag gefunden
indicator.className = 'status-circle';
} else if (data.found === 1) {
// NFC Tag erfolgreich gelesen
indicator.className = 'status-circle success';
} else {
// Fehler beim Lesen
indicator.className = 'status-circle error';
}
}
function updateNfcData(data) {
// Den Container für den NFC Status finden
const nfcStatusContainer = document.querySelector('.nfc-status-display');
// Bestehende Daten-Anzeige entfernen falls vorhanden
const existingData = nfcStatusContainer.querySelector('.nfc-data');
if (existingData) {
existingData.remove();
}
// Neues div für die Datenanzeige erstellen
const nfcDataDiv = document.createElement('div');
nfcDataDiv.className = 'nfc-data';
// Wenn ein Fehler vorliegt oder keine Daten vorhanden sind
if (data.error || data.info || !data || Object.keys(data).length === 0) {
// Zeige Fehlermeldung oder leere Nachricht
if (data.error || data.info) {
if (data.error) {
nfcDataDiv.innerHTML = `
<div class="error-message" style="margin-top: 10px; color: #dc3545;">
<p><strong>Error:</strong> ${data.error}</p>
</div>`;
} else {
nfcDataDiv.innerHTML = `
<div class="info-message" style="margin-top: 10px; color:rgb(18, 210, 0);">
<p><strong>Info:</strong> ${data.info}</p>
</div>`;
}
} else {
nfcDataDiv.innerHTML = '<div style="margin-top: 10px;"></div>';
}
nfcStatusContainer.appendChild(nfcDataDiv);
return;
}
// HTML für die Datenanzeige erstellen
let html = `
<div style="margin-top: 10px;">
<p><strong>Brand:</strong> ${data.brand || 'N/A'}</p>
<p><strong>Type:</strong> ${data.type || 'N/A'} ${data.color_hex ? `<span style="
background-color: #${data.color_hex};
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
border: 1px solid #333;
border-radius: 3px;
margin-left: 5px;
"></span>` : ''}</p>
`;
// Spoolman ID anzeigen
html += `<p><strong>Spoolman ID:</strong> ${data.sm_id || 'No Spoolman ID'}</p>`;
// 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 += '</div>';
nfcDataDiv.innerHTML = html;
// Neues div zum Container hinzufügen
nfcStatusContainer.appendChild(nfcDataDiv);
}
function writeNfcTag() {
const selectedText = document.getElementById("selected-filament").textContent;
if (selectedText === "Please choose...") {
alert('Please select a Spool first.');
return;
}
const spoolsData = window.getSpoolData();
const selectedSpool = spoolsData.find(spool =>
`${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText
);
if (!selectedSpool) {
alert('Ausgewählte Spule konnte nicht gefunden werden.');
return;
}
// Temperaturwerte korrekt extrahieren
let minTemp = "175";
let maxTemp = "275";
if (Array.isArray(selectedSpool.filament.nozzle_temperature) &&
selectedSpool.filament.nozzle_temperature.length >= 2) {
minTemp = String(selectedSpool.filament.nozzle_temperature[0]);
maxTemp = String(selectedSpool.filament.nozzle_temperature[1]);
}
// Erstelle das NFC-Datenpaket mit korrekten Datentypen
const nfcData = {
version: "2.0",
protocol: "openspool",
color_hex: selectedSpool.filament.color_hex || "FFFFFF",
type: selectedSpool.filament.material,
min_temp: minTemp,
max_temp: maxTemp,
brand: selectedSpool.filament.vendor.name,
sm_id: String(selectedSpool.id) // Konvertiere zu String
};
if (socket?.readyState === WebSocket.OPEN) {
const writeButton = document.getElementById("writeNfcButton");
writeButton.classList.add("writing");
writeButton.textContent = "Writing";
socket.send(JSON.stringify({
type: 'writeNfcTag',
payload: nfcData
}));
} else {
alert('Not connected to Server. Please check connection.');
}
}
function handleWriteNfcTagResponse(success) {
const writeButton = document.getElementById("writeNfcButton");
writeButton.classList.remove("writing");
writeButton.classList.add(success ? "success" : "error");
writeButton.textContent = success ? "Write success" : "Write failed";
setTimeout(() => {
writeButton.classList.remove("success", "error");
writeButton.textContent = "Write Tag";
}, 5000);
}
function showNotification(message, isSuccess) {
const notification = document.createElement('div');
notification.className = `notification ${isSuccess ? 'success' : 'error'}`;
notification.textContent = message;
document.body.appendChild(notification);
// Nach 3 Sekunden ausblenden
setTimeout(() => {
notification.classList.add('fade-out');
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}

BIN
html/spool_in.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

80
html/spoolman.html Normal file
View File

@ -0,0 +1,80 @@
{{header}}
<script>
window.onload = function() {
if (spoolmanUrl && spoolmanUrl.trim() !== "") {
document.getElementById('spoolmanUrl').value = spoolmanUrl;
}
};
function checkSpoolmanInstance() {
const url = document.getElementById('spoolmanUrl').value;
fetch(`/api/checkSpoolman?url=${encodeURIComponent(url)}`)
.then(response => response.json())
.then(data => {
if (data.healthy) {
document.getElementById('statusMessage').innerText = 'Spoolman-Instance is availabe and healthy!';
} else {
document.getElementById('statusMessage').innerText = 'Spoolman-Instance not available.';
}
})
.catch(error => {
document.getElementById('statusMessage').innerText = 'Error while connecting to Spoolman-Instance: ' + error.message;
});
}
function saveBambuCredentials() {
const ip = document.getElementById('bambuIp').value;
const serial = document.getElementById('bambuSerial').value;
const code = document.getElementById('bambuCode').value;
fetch(`/api/bambu?bambu_ip=${encodeURIComponent(ip)}&bambu_serialnr=${encodeURIComponent(serial)}&bambu_accesscode=${encodeURIComponent(code)}`)
.then(response => response.json())
.then(data => {
if (data.healthy) {
document.getElementById('bambuStatusMessage').innerText = 'Bambu Credentials saved!';
// Erstelle und zeige den Reboot-Button
const rebootBtn = document.createElement('button');
rebootBtn.innerText = 'Reboot now';
rebootBtn.className = 'reboot-button';
rebootBtn.onclick = () => window.location.href = '/reboot';
document.getElementById('bambuStatusMessage').appendChild(rebootBtn);
} else {
document.getElementById('bambuStatusMessage').innerText = 'Error while saving Bambu Credentials.';
}
})
.catch(error => {
document.getElementById('bambuStatusMessage').innerText = 'Error while saving: ' + error.message;
});
}
</script>
<script>
var spoolmanUrl = "{{spoolmanUrl}}";
</script>
<div class="content">
<h1>Spoolman API URL / Bambu Credentials</h1>
<label for="spoolmanUrl">Set URL/IP to your Spoolman-Instanz:</label>
<input type="text" id="spoolmanUrl" placeholder="http://ip-or-url-of-your-spoolman-instanz:port">
<button onclick="checkSpoolmanInstance()">Save Spoolman URL</button>
<p id="statusMessage"></p>
<h2>Bambu Lab Printer Credentials</h2>
<div class="bambu-settings">
<div class="input-group">
<label for="bambuIp">Bambu Drucker IP-Adresse:</label>
<input type="text" id="bambuIp" placeholder="192.168.1.xxx" value="{{bambuIp}}">
</div>
<div class="input-group">
<label for="bambuSerial">Drucker Seriennummer:</label>
<input type="text" id="bambuSerial" placeholder="BBLXXXXXXXX" value="{{bambuSerial}}">
</div>
<div class="input-group">
<label for="bambuCode">Access Code:</label>
<input type="text" id="bambuCode" placeholder="Access Code vom Drucker" value="{{bambuCode}}">
</div>
<button onclick="saveBambuCredentials()">Save Bambu Credentials</button>
<p id="bambuStatusMessage"></p>
</div>
</div>
</body>
</html>

308
html/spoolman.js Normal file
View File

@ -0,0 +1,308 @@
// Globale Variablen
let spoolmanUrl = '';
let spoolsData = [];
// Hilfsfunktionen für Datenmanipulation
function processSpoolData(data) {
return data.map(spool => ({
id: spool.id,
remaining_weight: spool.remaining_weight,
remaining_length: spool.remaining_length,
filament: spool.filament,
extra: spool.extra
}));
}
// Dropdown-Funktionen
function populateVendorDropdown(data, selectedSmId = null) {
const vendorSelect = document.getElementById("vendorSelect");
if (!vendorSelect) {
console.error('vendorSelect Element nicht gefunden');
return;
}
const onlyWithoutSmId = document.getElementById("onlyWithoutSmId");
if (!onlyWithoutSmId) {
console.error('onlyWithoutSmId Element nicht gefunden');
return;
}
// Separate Objekte für alle Hersteller und gefilterte Hersteller
const allVendors = {};
const filteredVendors = {};
vendorSelect.innerHTML = '<option value="">Bitte wählen...</option>';
let vendorIdToSelect = null;
let totalSpools = 0;
let spoolsWithoutTag = 0;
let totalWeight = 0;
let totalLength = 0;
// Neues Objekt für Material-Gruppierung
const materials = {};
data.forEach(spool => {
if (!spool.filament || !spool.filament.vendor) {
return;
}
totalSpools++;
// Material zählen und gruppieren
if (spool.filament.material) {
const material = spool.filament.material.toUpperCase(); // Normalisierung
materials[material] = (materials[material] || 0) + 1;
}
// Addiere Gewicht und Länge
if (spool.remaining_weight) {
totalWeight += spool.remaining_weight;
}
if (spool.remaining_length) {
totalLength += spool.remaining_length;
}
console.log("Länge gesamt: " + spool.remaining_length);
console.log("Gewicht gesamt" + spool.remaining_weight);
const vendor = spool.filament.vendor;
const hasValidNfcId = spool.extra &&
spool.extra.nfc_id &&
spool.extra.nfc_id !== '""' &&
spool.extra.nfc_id !== '"\\"\\"\\""';
if (!hasValidNfcId) {
spoolsWithoutTag++;
}
// Alle Hersteller sammeln
if (!allVendors[vendor.id]) {
allVendors[vendor.id] = vendor.name;
}
// Gefilterte Hersteller für Dropdown
if (!filteredVendors[vendor.id]) {
if (!onlyWithoutSmId.checked || !hasValidNfcId) {
filteredVendors[vendor.id] = vendor.name;
}
}
});
// Dropdown mit gefilterten Herstellern befüllen
Object.entries(filteredVendors).forEach(([id, name]) => {
const option = document.createElement("option");
option.value = id;
option.textContent = name;
vendorSelect.appendChild(option);
});
document.getElementById("totalSpools").textContent = totalSpools;
document.getElementById("spoolsWithoutTag").textContent = spoolsWithoutTag;
// Zeige die Gesamtzahl aller Hersteller an
document.getElementById("totalVendors").textContent = Object.keys(allVendors).length;
// Neue Statistiken hinzufügen
document.getElementById("totalWeight").textContent = (totalWeight / 1000).toFixed(2);
document.getElementById("totalLength").textContent = (totalLength / 1000).toFixed(2);
// Material-Statistiken zum DOM hinzufügen
const materialsList = document.getElementById("materialsList");
materialsList.innerHTML = '';
Object.entries(materials)
.sort(([,a], [,b]) => b - a) // Sortiere nach Anzahl absteigend
.forEach(([material, count]) => {
const li = document.createElement("li");
li.textContent = `${material}: ${count} ${count === 1 ? 'Spule' : 'Spulen'}`;
materialsList.appendChild(li);
});
if (vendorIdToSelect) {
vendorSelect.value = vendorIdToSelect;
updateFilamentDropdown(selectedSmId);
}
}
function updateFilamentDropdown(selectedSmId = null) {
const vendorId = document.getElementById("vendorSelect").value;
const dropdownContentInner = document.getElementById("filament-dropdown-content");
const filamentSection = document.getElementById("filamentSection");
const onlyWithoutSmId = document.getElementById("onlyWithoutSmId").checked;
const selectedText = document.getElementById("selected-filament");
const selectedColor = document.getElementById("selected-color");
dropdownContentInner.innerHTML = '';
selectedText.textContent = "Bitte wählen...";
selectedColor.style.backgroundColor = '#FFFFFF';
if (vendorId) {
const filteredFilaments = spoolsData.filter(spool => {
const hasValidNfcId = spool.extra &&
spool.extra.nfc_id &&
spool.extra.nfc_id !== '""' &&
spool.extra.nfc_id !== '"\\"\\"\\""';
return spool.filament.vendor.id == vendorId &&
(!onlyWithoutSmId || !hasValidNfcId);
});
filteredFilaments.forEach(spool => {
const option = document.createElement("div");
option.className = "dropdown-option";
option.setAttribute("data-value", spool.filament.id);
option.setAttribute("data-nfc-id", spool.extra.nfc_id || "");
const colorHex = spool.filament.color_hex || 'FFFFFF';
option.innerHTML = `
<div class="option-color" style="background-color: #${colorHex}"></div>
<span>${spool.id} | ${spool.filament.name} (${spool.filament.material})</span>
`;
option.onclick = () => selectFilament(spool);
dropdownContentInner.appendChild(option);
});
filamentSection.classList.remove("hidden");
} else {
filamentSection.classList.add("hidden");
}
}
function selectFilament(spool) {
const selectedColor = document.getElementById("selected-color");
const selectedText = document.getElementById("selected-filament");
const dropdownContent = document.getElementById("filament-dropdown-content");
selectedColor.style.backgroundColor = `#${spool.filament.color_hex || 'FFFFFF'}`;
selectedText.textContent = `${spool.id} | ${spool.filament.name} (${spool.filament.material})`;
dropdownContent.classList.remove("show");
document.dispatchEvent(new CustomEvent('filamentSelected', {
detail: spool
}));
}
// Initialisierung und Event-Handler
async function initSpoolman() {
try {
const response = await fetch('/api/url');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data.spoolman_url) {
throw new Error('spoolman_url nicht in der Antwort gefunden');
}
spoolmanUrl = data.spoolman_url;
const fetchedData = await fetchSpoolData();
spoolsData = processSpoolData(fetchedData);
document.dispatchEvent(new CustomEvent('spoolDataLoaded', {
detail: spoolsData
}));
} catch (error) {
console.error('Fehler beim Initialisieren von Spoolman:', error);
document.dispatchEvent(new CustomEvent('spoolmanError', {
detail: { message: error.message }
}));
}
}
async function fetchSpoolData() {
try {
if (!spoolmanUrl) {
throw new Error('Spoolman URL ist nicht initialisiert');
}
const response = await fetch(`${spoolmanUrl}/api/v1/spool`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Fehler beim Abrufen der Spulen-Daten:', error);
return [];
}
}
/*
// Exportiere Funktionen
window.getSpoolData = () => spoolsData;
window.reloadSpoolData = initSpoolman;
window.populateVendorDropdown = populateVendorDropdown;
window.updateFilamentDropdown = updateFilamentDropdown;
window.toggleFilamentDropdown = () => {
const content = document.getElementById("filament-dropdown-content");
content.classList.toggle("show");
};
*/
// Event Listener
document.addEventListener('DOMContentLoaded', () => {
initSpoolman();
const vendorSelect = document.getElementById('vendorSelect');
if (vendorSelect) {
vendorSelect.addEventListener('change', () => updateFilamentDropdown());
}
const onlyWithoutSmId = document.getElementById('onlyWithoutSmId');
if (onlyWithoutSmId) {
onlyWithoutSmId.addEventListener('change', () => {
populateVendorDropdown(spoolsData);
updateFilamentDropdown();
});
}
document.addEventListener('spoolDataLoaded', (event) => {
populateVendorDropdown(event.detail);
});
window.onclick = function(event) {
if (!event.target.closest('.custom-dropdown')) {
const dropdowns = document.getElementsByClassName("dropdown-content");
for (let dropdown of dropdowns) {
dropdown.classList.remove("show");
}
}
};
const refreshButton = document.getElementById('refreshSpoolman');
if (refreshButton) {
refreshButton.addEventListener('click', async () => {
try {
refreshButton.disabled = true;
refreshButton.textContent = 'Wird aktualisiert...';
await initSpoolman();
refreshButton.textContent = 'Refresh Spoolman';
} finally {
refreshButton.disabled = false;
}
});
}
});
// Exportiere Funktionen
window.getSpoolData = () => spoolsData;
window.setSpoolData = (data) => { spoolsData = data; };
window.reloadSpoolData = initSpoolman;
window.populateVendorDropdown = populateVendorDropdown;
window.updateFilamentDropdown = updateFilamentDropdown;
window.toggleFilamentDropdown = () => {
const content = document.getElementById("filament-dropdown-content");
content.classList.toggle("show");
};
// Event Listener für Click außerhalb Dropdown
window.onclick = function(event) {
if (!event.target.closest('.custom-dropdown')) {
const dropdowns = document.getElementsByClassName("dropdown-content");
for (let dropdown of dropdowns) {
dropdown.classList.remove("show");
}
}
};

901
html/style.css Normal file
View File

@ -0,0 +1,901 @@
/* Allgemeine Stile */
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f8f9fa;
color: #333;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
text-align: center;
}
.logo {
height: 40px; /* Anpassen an die Navbar-Höhe */
width: auto;
margin-right: 15px;
margin-left: 10px;
}
/* Navigationsleiste */
.navbar {
background-color: #007bff;
width: 100%;
display: flex;
justify-content: center; /* Zentriert die Navigation */
padding: 10px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: fixed;
top: 0;
left: 0;
z-index: 1000;
}
.navbar a {
display: inline-block;
color: white;
text-align: center;
padding: 14px 20px;
text-decoration: none;
font-weight: bold;
transition: background 0.3s, color 0.3s;
cursor: pointer !important; /* Wichtig: cursor-Definition für Nav-Links */
}
.navbar a:hover {
background-color: #0056b3;
color: #fff;
cursor: pointer !important;
}
/* Inhalt */
.container {
padding: 20px;
width: 100%;
max-width: none;
background: white;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin-top: 20px; /* Platz für die fixe Navbar */
}
/* Überschriften */
h1 {
color: #007bff;
text-align: center;
}
h3 {
color: #007bff;
font-size: 24px;
margin-top: 5px;
margin-bottom: 5px;
font-weight: bold;
}
/* Formulare */
form {
display: flex;
flex-direction: column;
gap: 10px;
padding: 20px;
}
label {
font-weight: bold;
}
input[type="text"], input[type="submit"] {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
input[type="text"]:focus {
border-color: #007bff;
outline: none;
}
input[type="submit"] {
background-color: #007bff;
color: white;
border: none;
cursor: pointer;
transition: background 0.3s;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
/* Buttons */
button {
padding: 10px 15px;
border: none;
border-radius: 5px;
background-color: #007bff;
color: white;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background-color: #0056b3;
}
/* Statusnachricht */
#statusMessage {
margin-top: 10px;
padding: 10px;
border-radius: 5px;
background-color: #8cc4fd;
text-align: center;
font-weight: bold;
}
.features {
display: flex;
justify-content: space-between;
margin-top: 30px;
text-align: left;
}
.feature {
flex: 1;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
margin: 0 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
.feature h3 {
font-size: 1.4rem;
margin-bottom: 10px;
color: #007bff;
}
.feature p {
font-size: 1rem;
color: #555;
}
p {
font-size: 1rem;
color: #555;
}
a {
color: #007bff;
text-decoration: none;
font-weight: bold;
cursor: pointer;
}
a:hover {
text-decoration: underline;
}
/* Karten-Stil für optische Trennung */
.card {
background: #f9f9f9;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Versteckte Elemente */
.hidden {
display: none;
}
/* Dropdown-Stil */
.styled-select {
width: 100%;
padding: 12px 15px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background-color: #fff;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 15px center;
background-size: 15px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.styled-select:hover {
border-color: #007bff;
box-shadow: 0 3px 6px rgba(0, 123, 255, 0.1);
}
.styled-select:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
.styled-select:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
opacity: 0.7;
}
/* NFC-Status */
.nfc-status {
font-weight: bold;
margin-top: 10px;
}
.nfc-success {
color: green;
}
.nfc-error {
color: red;
}
/* Füge diese neuen Styles zu deiner style.css hinzu */
.three-column-layout {
display: flex;
justify-content: space-between;
gap: 20px;
margin-top: 20px;
width: 100%;
}
.column {
flex: 1;
min-width: 0; /* Verhindert Überlauf bei flex-Elementen */
}
.feature-box {
background: white;
padding: 5px 20px 20px 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.feature-box h2 {
color: #007bff;
font-size: 1.4rem;
margin-bottom: 15px;
}
.feature-box ul {
list-style: none;
padding: 0;
margin: 0;
}
.feature-box ul li {
padding: 8px 5px 5px 5px;
border-bottom: 1px solid #eee;
}
.content {
width: 95%;
max-width: 1400px;
margin: 0 auto;
padding-top: 60px;
padding-bottom: 20px;;
}
.tray {
background: #ffffff;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
border-left: 4px solid #ffffff;
}
.tray p {
margin: 5px 0;
}
.tray b {
color: #007bff;
}
/* Responsive Design */
@media (max-width: 1024px) {
.three-column-layout {
flex-direction: column;
}
.column {
width: 100%;
}
}
.nfc-status-display {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 0;
}
.status-circle {
width: 20px;
height: 20px;
border-radius: 50%;
display: inline-block;
border: 2px solid #ccc;
background-color: #ffffff;
}
.status-circle.success {
background-color: #28a745;
border-color: #218838;
}
.status-circle.error {
background-color: #dc3545;
border-color: #c82333;
}
.nfc-data {
padding: 10px;
background-color: #f8f9fa;
border-radius: 4px;
margin-top: 5px;
width: 100%;
}
.nfc-data p {
margin: 5px 0;
font-size: 0.9em;
}
.nfc-status-display {
display: flex;
flex-direction: column;
gap: 10px;
}
.error-message {
padding: 10px;
background-color: #fff3f3;
border-radius: 4px;
border-left: 4px solid #dc3545;
}
.info-message {
padding: 10px;
background-color: #fff3f3;
border-radius: 4px;
border-left: 4px solid #39d82e;
}
.nfc-header {
display: grid;
grid-template-columns: 40px 1fr 40px;
align-items: center;
margin-bottom: 10px;
}
.nfc-header h2 {
margin: 0;
grid-column: 2;
text-align: center;
}
.nfc-header .status-circle {
grid-column: 3;
justify-self: end;
}
.content-header {
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.connection-status {
display: flex;
align-items: center;
gap: 10px;
background-color: #fff3f3;
border: 1px solid #dc3545;
border-radius: 4px;
padding: 10px 15px;
margin: 15px auto;
color: #dc3545;
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
max-width: 90%;
opacity: 0;
transition: opacity 0.3s ease;
}
.connection-status.visible {
opacity: 1;
}
.spinner {
flex-shrink: 0;
width: 16px;
height: 16px;
border: 2px solid rgba(220, 53, 69, 0.2);
border-top-color: #dc3545;
border-radius: 50%;
}
.connection-status.visible .spinner {
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.connection-status.hidden {
display: none;
}
.nfc-actions {
display: flex;
gap: 10px;
justify-content: center; /* Zentriert das div */
}
.btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: bold;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
/* Filament Select Styling */
#filamentSelect {
width: 100%;
padding: 12px 15px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background-color: #fff;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 15px center;
background-size: 15px;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
#filamentSelect:hover {
border-color: #007bff;
box-shadow: 0 3px 6px rgba(0, 123, 255, 0.1);
}
#filamentSelect:focus {
border-color: #007bff;
outline: none;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
#filamentSelect option {
padding: 8px 15px;
font-size: 16px;
background-color: #fff;
color: #000; /* Standard Textfarbe für alles außer dem Farbblock */
}
#filamentSelect option::first-letter {
font-size: 16px;
margin-right: 5px;
}
#filamentSelect option::before {
content: '';
display: inline-block;
width: 12px;
margin-right: 8px;
}
/* Color Box im Select */
.color-box {
display: inline-block;
width: 12px;
height: 12px;
border: 1px solid #333;
border-radius: 2px;
margin-right: 5px;
vertical-align: middle;
}
#filamentSelect option span {
display: inline-block;
pointer-events: none;
}
#filamentSelect option span:first-child {
margin-right: 5px;
font-size: 16px;
}
/* Filament Select Option Styling */
#filamentSelect option span.color-circle {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
border: 1px solid #333;
vertical-align: middle;
}
/* Custom Dropdown */
.custom-dropdown {
position: relative;
width: 100%;
font-family: inherit;
cursor: default; /* Container selbst soll normalen Cursor haben */
}
.dropdown-button {
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background-color: #fff;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.dropdown-button:hover {
border-color: #007bff;
}
.selected-color {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid #333;
flex-shrink: 0;
}
.dropdown-arrow {
margin-left: auto;
color: #007bff;
font-size: 12px;
}
.dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-top: 4px;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dropdown-content.show {
display: block;
}
.dropdown-option {
padding: 10px 15px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.dropdown-option:hover {
background-color: #f8f9fa;
}
.option-color {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1px solid #333;
flex-shrink: 0;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 4px;
color: white;
z-index: 1000;
animation: slideIn 0.3s ease-out;
}
.notification.success {
background-color: #28a745;
}
.notification.error {
background-color: #dc3545;
}
.notification.fade-out {
opacity: 0;
transition: opacity 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Neue Styles für die Statistiken */
.statistics-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 15px;
}
.statistics-column {
background: #f8f9fa;
padding: 0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.statistics-column h3 {
color: #007bff;
margin-bottom: 5px;
padding-bottom: 8px;
border-bottom: 2px solid #e9ecef;
font-size: 1.1rem;
}
.statistics-list {
list-style: none;
padding: 0;
margin: 0;
}
.statistics-list li {
display: flex;
justify-content: space-between;
padding: 8px 5px 0 5px;
border-bottom: 1px solid #e9ecef;
}
.statistics-list li:last-child {
border-bottom: none;
}
.stat-label {
color: #495057;
font-weight: 500;
}
.stat-value {
font-weight: bold;
color: #007bff;
}
/* Responsive Design Anpassung */
@media (max-width: 768px) {
.statistics-grid {
grid-template-columns: 1fr;
}
}
.statistics-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 0;
border-bottom: 1px solid #e9ecef;
}
.refresh-button {
display: flex;
align-items: center;
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
.refresh-button:hover {
background-color: #0056b3;
}
.refresh-button:active {
background-color: #004494;
}
.spools-info {
display: flex;
justify-content: flex-start;
gap: 20px;
margin-bottom: 15px;
}
.spool-stat {
display: flex;
align-items: center;
gap: 8px;
}
.spool-stat .stat-label {
color: #495057;
font-weight: 500;
white-space: nowrap;
}
.spool-stat .stat-value {
font-weight: bold;
color: #007bff;
}
/* Buttons und klickbare Elemente */
button,
input[type="submit"],
.dropdown-button,
.dropdown-option,
.refresh-button,
.btn,
.styled-select,
select,
a {
cursor: pointer !important;
}
/* Disabled Zustände */
button:disabled,
input[type="submit"]:disabled,
.btn:disabled,
.styled-select:disabled {
cursor: not-allowed !important;
opacity: 0.7;
}
/* Schreib-Button */
#writeNfcButton {
background-color: #007bff;
color: white;
transition: background-color 0.3s, color 0.3s;
width: 160px;
}
#writeNfcButton.writing {
background-color: #ffc107;
color: black;
width: 160px;
}
#writeNfcButton.success {
background-color: #28a745;
color: white;
width: 160px;
}
#writeNfcButton.error {
background-color: #dc3545;
color: white;
width: 160px;
}
@keyframes dots {
0% { content: ""; }
33% { content: "."; }
66% { content: ".."; }
100% { content: "..."; }
}
#writeNfcButton.writing::after {
content: "...";
animation: dots 1s steps(3, end) infinite;
}
.reboot-button {
background-color: #ff0000;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
margin-left: 10px;
cursor: pointer;
}
.reboot-button:hover {
background-color: #cc0000;
}
/* Bambu Settings Erweiterung */
.bambu-settings {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
max-width: 400px;
margin: 20px auto;
}
.bambu-settings .input-group {
margin-bottom: 15px;
text-align: left;
}
.bambu-settings .input-group label {
display: block;
margin-bottom: 5px;
}
.bambu-settings .input-group input {
width: 100%;
}
#bambuStatusMessage {
margin-top: 15px;
display: flex;
align-items: center;
gap: 10px;
justify-content: center;
}
.tray {
position: relative;
}
.spool-button:hover {
opacity: 0.8;
}

99
html/waage.html Normal file
View File

@ -0,0 +1,99 @@
{{header}}
<div class="content">
<h1>Scale Configuration Page</h1>
<div class="card">
<div class="card-body">
<h5 class="card-title">Sacle Calibration</h5>
<button id="calibrateBtn" class="btn btn-primary">Calibrate Scale</button>
<button id="tareBtn" class="btn btn-secondary">Tare Scale</button>
<div id="statusMessage" class="mt-3"></div>
</div>
</div>
<!-- Neue Kalibrierungskarte -->
<div id="calibrationCard" class="card mt-3" style="display: none;">
<div class="card-body">
<h5 class="card-title">Calibration done</h5>
<p>Please follow these steps:</p>
<ol>
<li>Make sure the scale is empty</li>
<li>Have a 500g calibration weight ready</li>
<li>Click on "Start Calibration"</li>
<li>Follow the further instructions</li>
</ol>
<ol>
<li>Step 1: Empty the scale</li>
<li>Step 2: Place the 500g weight on the scale</li>
<li>Step 3: Remove weight from Scale</li>
</ol>
<button id="startCalibrationBtn" class="btn btn-danger">Start Calibration</button>
</div>
</div>
</div>
<script>
let ws = null;
const statusMessage = document.getElementById('statusMessage');
function connectWebSocket() {
ws = new WebSocket(`ws://${window.location.hostname}/ws`);
ws.onopen = () => {
console.log('WebSocket verbunden');
statusMessage.innerHTML = 'Scale connected';
enableButtons(true);
};
ws.onclose = () => {
console.log('WebSocket getrennt');
statusMessage.innerHTML = 'Scale connection lost';
enableButtons(false);
setTimeout(connectWebSocket, 2000);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'scale') {
if (data.payload === 'success') {
statusMessage.innerHTML = 'Well done';
statusMessage.className = 'alert alert-success';
} else if (data.payload === 'error') {
statusMessage.innerHTML = 'Error while action';
statusMessage.className = 'alert alert-danger';
}
}
};
}
function enableButtons(enabled) {
document.getElementById('calibrateBtn').disabled = !enabled;
document.getElementById('tareBtn').disabled = !enabled;
}
document.getElementById('calibrateBtn').addEventListener('click', () => {
// Kalibrierungskarte anzeigen
document.getElementById('calibrationCard').style.display = 'block';
});
document.getElementById('startCalibrationBtn').addEventListener('click', () => {
ws.send(JSON.stringify({
type: 'scale',
payload: 'calibrate'
}));
// Optional: Kalibrierungskarte nach dem Start ausblenden
document.getElementById('calibrationCard').style.display = 'none';
});
document.getElementById('tareBtn').addEventListener('click', () => {
ws.send(JSON.stringify({
type: 'scale',
payload: 'tare'
}));
});
// WebSocket-Verbindung beim Laden der Seite initiieren
connectWebSocket();
</script>
</body>
</html>

12
html/wifi.html Normal file
View File

@ -0,0 +1,12 @@
{{header}}
<div class="content">
<h1>WiFi Configuration Page</h1>
<form action="/setToken" method="post">
<label for="deviceToken">Device Token:</label><br>
<input type="text" id="deviceToken" name="deviceToken"><br>
<input type="submit" value="Set Token">
</form>
<p>Configure your WiFi settings here.</p>
</div>
</body>
</html>