From 27ef8399e48a99ef51116480212aa6f0241daeff Mon Sep 17 00:00:00 2001 From: Jan Philipp Ecker Date: Thu, 19 Jun 2025 10:08:15 +0200 Subject: [PATCH 1/2] Adds slight debouncing to the scale loop weight logic Adds slight debouncing to the scale loop to prevent jitter of the weight displayed on the screen. --- src/scale.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/scale.cpp b/src/scale.cpp index 5808829..83edeeb 100644 --- a/src/scale.cpp +++ b/src/scale.cpp @@ -74,7 +74,11 @@ void scale_loop(void * parameter) { scaleTareRequest = false; } - weight = round(scale.get_units()); + // Only update weight if median changed more than 1 + int16_t newWeight = round(scale.get_units()); + if(abs(weight-newWeight) > 1){ + weight = newWeight; + } } vTaskDelay(pdMS_TO_TICKS(100)); From eab937d6cacaa3a5e796772da82a773cabe72bc8 Mon Sep 17 00:00:00 2001 From: Jan Philipp Ecker Date: Mon, 21 Jul 2025 21:03:55 +0200 Subject: [PATCH 2/2] Adds new feature to write and read location tags Location tags can be written via the website. If a location tag is read after reading a spool tag, the location of the spool will be updated in spoolman to the location from the tag. --- html/rfid.html | 12 +++++++++ html/rfid.js | 64 ++++++++++++++++++++++++++++++++++++--------- html/spoolman.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++- html/style.css | 14 ++++++---- src/api.cpp | 52 ++++++++++++++++++++++++++++++++++++- src/api.h | 9 +++++++ src/nfc.cpp | 18 +++++++++++-- 7 files changed, 215 insertions(+), 21 deletions(-) diff --git a/html/rfid.html b/html/rfid.html index a481c04..cfffff8 100644 --- a/html/rfid.html +++ b/html/rfid.html @@ -139,6 +139,18 @@

+ +
+

Spoolman Locations

+ +
+ +
+

+ +
diff --git a/html/rfid.js b/html/rfid.js index 3761f51..6bdcea2 100644 --- a/html/rfid.js +++ b/html/rfid.js @@ -626,11 +626,11 @@ function writeNfcTag() { // Erstelle das NFC-Datenpaket mit korrekten Datentypen const nfcData = { - color_hex: selectedSpool.filament.color_hex || "FFFFFF", - type: selectedSpool.filament.material, - min_temp: minTemp, - max_temp: maxTemp, - brand: selectedSpool.filament.vendor.name, + //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 }; @@ -647,16 +647,56 @@ function writeNfcTag() { } } +function writeLocationNfcTag() { + const selectedText = document.getElementById("locationSelect").value; + if (selectedText === "Please choose...") { + alert('Please select a location first.'); + return; + } + // Erstelle das NFC-Datenpaket mit korrekten Datentypen + const nfcData = { + location: String(selectedText) + }; + + if (socket?.readyState === WebSocket.OPEN) { + const writeButton = document.getElementById("writeLocationNfcButton"); + 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"; + const writeLocationButton = document.getElementById("writeLocationNfcButton"); + if(writeButton.classList.contains("writing")){ + 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); + setTimeout(() => { + writeButton.classList.remove("success", "error"); + writeButton.textContent = "Write Tag"; + }, 5000); + } + + if(writeLocationButton.classList.contains("writing")){ + writeLocationButton.classList.remove("writing"); + writeLocationButton.classList.add(success ? "success" : "error"); + writeLocationButton.textContent = success ? "Write success" : "Write failed"; + + setTimeout(() => { + writeLocationButton.classList.remove("success", "error"); + writeLocationButton.textContent = "Write Location Tag"; + }, 5000); + } + + } function showNotification(message, isSuccess) { diff --git a/html/spoolman.js b/html/spoolman.js index 8193984..aba63f0 100644 --- a/html/spoolman.js +++ b/html/spoolman.js @@ -1,6 +1,7 @@ // Globale Variablen let spoolmanUrl = ''; let spoolsData = []; +let locationData = []; // Hilfsfunktionen für Datenmanipulation function processSpoolData(data) { @@ -133,6 +134,26 @@ function populateVendorDropdown(data, selectedSmId = null) { } } +// Dropdown-Funktionen +function populateLocationDropdown(data) { + const locationSelect = document.getElementById("locationSelect"); + if (!locationSelect) { + console.error('locationSelect Element nicht gefunden'); + return; + } + + locationSelect.innerHTML = ''; + // Dropdown mit gefilterten Herstellern befüllen - alphabetisch sortiert + Object.entries(data) + .sort(([, nameA], [, nameB]) => nameA.localeCompare(nameB)) // Sort vendors alphabetically by name + .forEach(([id, name]) => { + const option = document.createElement("option"); + option.value = name; + option.textContent = name; + locationSelect.appendChild(option); + }); +} + function updateFilamentDropdown(selectedSmId = null) { const vendorId = document.getElementById("vendorSelect").value; const dropdownContentInner = document.getElementById("filament-dropdown-content"); @@ -208,6 +229,13 @@ function updateFilamentDropdown(selectedSmId = null) { } } +function updateLocationSelect(){ + const writeLocationNfcButton = document.getElementById('writeLocationNfcButton'); + if(writeLocationNfcButton){ + writeLocationNfcButton.classList.remove("hidden"); + } +} + function selectFilament(spool) { const selectedColor = document.getElementById("selected-color"); const selectedText = document.getElementById("selected-filament"); @@ -261,10 +289,18 @@ async function initSpoolman() { const fetchedData = await fetchSpoolData(); spoolsData = processSpoolData(fetchedData); - + document.dispatchEvent(new CustomEvent('spoolDataLoaded', { detail: spoolsData })); + + locationData = await fetchLocationData(); + + document.dispatchEvent(new CustomEvent('locationDataLoaded', { + detail: locationData + })); + + } catch (error) { console.error('Fehler beim Initialisieren von Spoolman:', error); document.dispatchEvent(new CustomEvent('spoolmanError', { @@ -292,6 +328,25 @@ async function fetchSpoolData() { } } +async function fetchLocationData() { + try { + if (!spoolmanUrl) { + throw new Error('Spoolman URL ist nicht initialisiert'); + } + + const response = await fetch(`${spoolmanUrl}/api/v1/location`); + 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 Location-Daten:', error); + return []; + } +} + // Event Listener document.addEventListener('DOMContentLoaded', () => { initSpoolman(); @@ -300,6 +355,11 @@ document.addEventListener('DOMContentLoaded', () => { if (vendorSelect) { vendorSelect.addEventListener('change', () => updateFilamentDropdown()); } + + const locationSelect = document.getElementById('locationSelect'); + if (locationSelect) { + locationSelect.addEventListener('change', () => updateLocationSelect()); + } const onlyWithoutSmId = document.getElementById('onlyWithoutSmId'); if (onlyWithoutSmId) { @@ -312,6 +372,10 @@ document.addEventListener('DOMContentLoaded', () => { document.addEventListener('spoolDataLoaded', (event) => { populateVendorDropdown(event.detail); }); + + document.addEventListener('locationDataLoaded', (event) => { + populateLocationDropdown(event.detail); + }); window.onclick = function(event) { if (!event.target.closest('.custom-dropdown')) { @@ -342,6 +406,7 @@ window.getSpoolData = () => spoolsData; window.setSpoolData = (data) => { spoolsData = data; }; window.reloadSpoolData = initSpoolman; window.populateVendorDropdown = populateVendorDropdown; +window.populateLocationDropdown = populateLocationDropdown; window.updateFilamentDropdown = updateFilamentDropdown; window.toggleFilamentDropdown = () => { const content = document.getElementById("filament-dropdown-content"); diff --git a/html/style.css b/html/style.css index e0f37aa..d51700c 100644 --- a/html/style.css +++ b/html/style.css @@ -971,31 +971,35 @@ input[type="submit"]:disabled, } /* Schreib-Button */ -#writeNfcButton { +#writeNfcButton, #writeLocationNfcButton { background-color: #007bff; color: white; transition: background-color 0.3s, color 0.3s; width: 160px; } -#writeNfcButton.writing { +#writeNfcButton.writing, #writeLocationNfcButton.writing { background-color: #ffc107; color: black; width: 160px; } -#writeNfcButton.success { +#writeNfcButton.success, #writeLocationNfcButton.success { background-color: #28a745; color: white; width: 160px; } -#writeNfcButton.error { +#writeNfcButton.error, #writeLocationNfcButton.error { background-color: #dc3545; color: white; width: 160px; } +#writeLocationNfcButton{ + width: 250px; +} + @keyframes dots { 0% { content: ""; } 33% { content: "."; } @@ -1003,7 +1007,7 @@ input[type="submit"]:disabled, 100% { content: "..."; } } -#writeNfcButton.writing::after { +#writeNfcButton.writing::after, #writeLocationNfcButton.writing::after { content: "..."; animation: dots 1s steps(3, end) infinite; } diff --git a/src/api.cpp b/src/api.cpp index a8b3e2d..a100239 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -11,6 +11,7 @@ String octoUrl = ""; String octoToken = ""; struct SendToApiParams { + SpoolmanApiRequestType requestType; String httpType; String spoolsUrl; String updatePayload; @@ -90,6 +91,7 @@ void sendToApi(void *parameter) { SendToApiParams* params = (SendToApiParams*)parameter; // Extrahiere die Werte + SpoolmanApiRequestType requestType = params->requestType; String httpType = params->httpType; String spoolsUrl = params->spoolsUrl; String updatePayload = params->updatePayload; @@ -118,12 +120,15 @@ void sendToApi(void *parameter) { Serial.print("Fehler beim Parsen der JSON-Antwort: "); Serial.println(error.c_str()); } else { - if (httpType == "PUT") { + if (requestType == API_REQUEST_SPOOL_WEIGHT_UPDATE) { uint16_t remaining_weight = doc["remaining_weight"].as(); Serial.print("Aktuelles Gewicht: "); Serial.println(remaining_weight); oledShowMessage("Remaining: " + String(remaining_weight) + "g"); } + else if ( requestType == API_REQUEST_SPOOL_LOCATION_UPDATE) { + oledShowMessage("Location updated!"); + } vTaskDelay(3000 / portTICK_PERIOD_MS); doc.clear(); @@ -178,6 +183,7 @@ bool updateSpoolTagId(String uidString, const char* payload) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); return false; } + params->requestType = API_REQUEST_SPOOL_TAG_ID_UPDATE; params->httpType = "PATCH"; params->spoolsUrl = spoolsUrl; params->updatePayload = updatePayload; @@ -219,6 +225,7 @@ uint8_t updateSpoolWeight(String spoolId, uint16_t weight) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); return 0; } + params->requestType = API_REQUEST_SPOOL_WEIGHT_UPDATE; params->httpType = "PUT"; params->spoolsUrl = spoolsUrl; params->updatePayload = updatePayload; @@ -238,6 +245,45 @@ uint8_t updateSpoolWeight(String spoolId, uint16_t weight) { return 1; } +uint8_t updateSpoolLocation(String spoolId, String location){ + String spoolsUrl = spoolmanUrl + apiUrl + "/spool/" + spoolId; + Serial.print("Update Spule mit URL: "); + Serial.println(spoolsUrl); + + // Update Payload erstellen + JsonDocument updateDoc; + updateDoc["location"] = location; + + String updatePayload; + serializeJson(updateDoc, updatePayload); + Serial.print("Update Payload: "); + Serial.println(updatePayload); + + SendToApiParams* params = new SendToApiParams(); + if (params == nullptr) { + Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); + return 0; + } + params->requestType = API_REQUEST_SPOOL_LOCATION_UPDATE; + params->httpType = "PATCH"; + params->spoolsUrl = spoolsUrl; + params->updatePayload = updatePayload; + + // Erstelle die Task + BaseType_t result = xTaskCreate( + sendToApi, // Task-Funktion + "SendToApiTask", // Task-Name + 6144, // Stackgröße in Bytes + (void*)params, // Parameter + 0, // Priorität + NULL // Task-Handle (nicht benötigt) + ); + + updateDoc.clear(); + + return 1; +} + bool updateSpoolOcto(int spoolId) { String spoolsUrl = octoUrl + "/plugin/Spoolman/selectSpool"; Serial.print("Update Spule in Octoprint mit URL: "); @@ -257,6 +303,7 @@ bool updateSpoolOcto(int spoolId) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); return false; } + params->requestType = API_REQUEST_OCTO_SPOOL_UPDATE; params->httpType = "POST"; params->spoolsUrl = spoolsUrl; params->updatePayload = updatePayload; @@ -306,6 +353,7 @@ bool updateSpoolBambuData(String payload) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); return false; } + params->requestType = API_REQUEST_BAMBU_UPDATE; params->httpType = "PATCH"; params->spoolsUrl = spoolsUrl; params->updatePayload = updatePayload; @@ -510,6 +558,8 @@ bool checkSpoolmanInstance(const String& url) { return strcmp(status, "healthy") == 0; } } + } else { + Serial.println("Error contacting spoolman instance! HTTP Code: " + String(httpCode)); } http.end(); return false; diff --git a/src/api.h b/src/api.h index 88853c8..5311dfc 100644 --- a/src/api.h +++ b/src/api.h @@ -12,6 +12,14 @@ typedef enum { API_TRANSMITTING } spoolmanApiStateType; +typedef enum { + API_REQUEST_OCTO_SPOOL_UPDATE, + API_REQUEST_BAMBU_UPDATE, + API_REQUEST_SPOOL_TAG_ID_UPDATE, + API_REQUEST_SPOOL_WEIGHT_UPDATE, + API_REQUEST_SPOOL_LOCATION_UPDATE +} SpoolmanApiRequestType; + extern volatile spoolmanApiStateType spoolmanApiState; extern bool spoolman_connected; extern String spoolmanUrl; @@ -26,6 +34,7 @@ bool checkSpoolmanExtraFields(); // Neue Funktion zum Überprüfen der Extrafeld JsonDocument fetchSingleSpoolInfo(int spoolId); // API-Funktion für die Webseite bool updateSpoolTagId(String uidString, const char* payload); // Neue Funktion zum Aktualisieren eines Spools uint8_t updateSpoolWeight(String spoolId, uint16_t weight); // Neue Funktion zum Aktualisieren des Gewichts +uint8_t updateSpoolLocation(String spoolId, String location); bool initSpoolman(); // Neue Funktion zum Initialisieren von Spoolman bool updateSpoolBambuData(String payload); // Neue Funktion zum Aktualisieren der Bambu-Daten bool updateSpoolOcto(int spoolId); // Neue Funktion zum Aktualisieren der Octo-Daten diff --git a/src/nfc.cpp b/src/nfc.cpp index 855405f..ea10267 100644 --- a/src/nfc.cpp +++ b/src/nfc.cpp @@ -218,11 +218,25 @@ bool decodeNdefAndReturnJson(const byte* encodedMessage) { // Sende die aktualisierten AMS-Daten an alle WebSocket-Clients Serial.println("JSON-Dokument erfolgreich verarbeitet"); Serial.println(doc.as()); - if (doc["sm_id"] != "") + if (doc.containsKey("sm_id") && doc["sm_id"] != "") { Serial.println("SPOOL-ID gefunden: " + doc["sm_id"].as()); spoolId = doc["sm_id"].as(); - } + } + else if(doc.containsKey("location") && doc["location"] != "") + { + Serial.println("Location Tag found!"); + String location = doc["location"].as(); + if(spoolId != ""){ + updateSpoolLocation(spoolId, location); + } + else + { + Serial.println("Location update tag scanned without scanning spool before!"); + oledShowMessage("No spool scanned before!"); + } + + } else { Serial.println("Keine SPOOL-ID gefunden.");