From b95497aec23a76e4ceaa45badc7a1e13cf8374b9 Mon Sep 17 00:00:00 2001 From: Jan Philipp Ecker Date: Thu, 7 Aug 2025 21:12:01 +0200 Subject: [PATCH 1/6] Further improvements on NFC writing Fixes some issues related to tag writing. Allos writing of tags that are already on the scale when pressing the write button, but introduces a confirmation dialog before doing so. Also first test to fix reset issue when trying to write tags. --- html/rfid.js | 133 +++++++++++++++++++++++++++------------------------ src/nfc.cpp | 28 +++++++---- 2 files changed, 90 insertions(+), 71 deletions(-) diff --git a/html/rfid.js b/html/rfid.js index 52866ea..d6dfa75 100644 --- a/html/rfid.js +++ b/html/rfid.js @@ -7,6 +7,7 @@ let heartbeatTimer = null; let lastHeartbeatResponse = Date.now(); const HEARTBEAT_TIMEOUT = 20000; let reconnectTimer = null; +let spoolDetected = false; // WebSocket Funktionen function startHeartbeat() { @@ -508,12 +509,15 @@ function updateNfcStatusIndicator(data) { if (data.found === 0) { // Kein NFC Tag gefunden indicator.className = 'status-circle'; + spoolDetected = false; } else if (data.found === 1) { // NFC Tag erfolgreich gelesen indicator.className = 'status-circle success'; + spoolDetected = true; } else { // Fehler beim Lesen indicator.className = 'status-circle error'; + spoolDetected = true; } } @@ -618,78 +622,83 @@ function updateNfcData(data) { } function writeNfcTag() { - const selectedText = document.getElementById("selected-filament").textContent; - if (selectedText === "Please choose...") { - alert('Please select a Spool first.'); - return; - } + if(!spoolDetected || confirm("Are you sure you want to overwrite the Tag?") == true){ + 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 - ); + 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; - } + 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]); - } + // 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 = { - 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 - }; + // 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, + 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', - tagType: 'spool', - payload: nfcData - })); - } else { - alert('Not connected to Server. Please check connection.'); + if (socket?.readyState === WebSocket.OPEN) { + const writeButton = document.getElementById("writeNfcButton"); + writeButton.classList.add("writing"); + writeButton.textContent = "Writing"; + socket.send(JSON.stringify({ + type: 'writeNfcTag', + tagType: 'spool', + payload: nfcData + })); + } else { + alert('Not connected to Server. Please check connection.'); + } } } 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(!spoolDetected || confirm("Are you sure you want to overwrite the Tag?") == true){ + 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', - tagType: 'location', - payload: nfcData - })); - } else { - alert('Not connected to Server. Please check connection.'); + + if (socket?.readyState === WebSocket.OPEN) { + const writeButton = document.getElementById("writeLocationNfcButton"); + writeButton.classList.add("writing"); + writeButton.textContent = "Writing"; + socket.send(JSON.stringify({ + type: 'writeNfcTag', + tagType: 'location', + payload: nfcData + })); + } else { + alert('Not connected to Server. Please check connection.'); + } } } diff --git a/src/nfc.cpp b/src/nfc.cpp index fe1a486..a68c90a 100644 --- a/src/nfc.cpp +++ b/src/nfc.cpp @@ -20,6 +20,7 @@ String lastSpoolId = ""; String nfcJsonData = ""; bool tagProcessed = false; volatile bool pauseBambuMqttTask = false; +volatile bool suspendNfcReading = false; struct NfcWriteParameterType { bool tagType; @@ -278,8 +279,10 @@ void writeJsonToTag(void *parameter) { Serial.println(params->payload); nfcReaderState = NFC_WRITING; - vTaskSuspend(RfidReaderTask); - vTaskDelay(50 / portTICK_PERIOD_MS); + suspendNfcReading = true; + //vTaskSuspend(RfidReaderTask); + // make sure to wait 600ms, after that the reading task should be waiting + vTaskDelay(1000 / portTICK_PERIOD_MS); //pauseBambuMqttTask = true; // aktualisieren der Website wenn sich der Status ändert @@ -372,7 +375,8 @@ void writeJsonToTag(void *parameter) { sendWriteResult(nullptr, success); sendNfcData(); - vTaskResume(RfidReaderTask); + suspendNfcReading = false; + //vTaskResume(RfidReaderTask); pauseBambuMqttTask = false; vTaskDelete(NULL); @@ -384,7 +388,7 @@ void startWriteJsonToTag(const bool isSpoolTag, const char* payload) { parameters->payload = strdup(payload); // Task nicht mehrfach starten - if (nfcReaderState == NFC_IDLE) { + if (nfcReaderState == NFC_IDLE || nfcReaderState == NFC_READ_ERROR || nfcReaderState == NFC_READ_SUCCESS) { oledShowProgressBar(0, 1, "Write Tag", "Place tag now"); // Erstelle die Task xTaskCreate( @@ -405,7 +409,7 @@ void scanRfidTask(void * parameter) { Serial.println("RFID Task gestartet"); for(;;) { // Wenn geschrieben wird Schleife aussetzen - if (nfcReaderState != NFC_WRITING) + if (nfcReaderState != NFC_WRITING && !suspendNfcReading) { yield(); @@ -413,7 +417,7 @@ void scanRfidTask(void * parameter) { uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID uint8_t uidLength; - success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 1000); + success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 500); foundNfcTag(nullptr, success); @@ -430,8 +434,8 @@ void scanRfidTask(void * parameter) { oledShowProgressBar(0, octoEnabled?5:4, "Reading", "Detecting tag"); - vTaskDelay(500 / portTICK_PERIOD_MS); - + //vTaskDelay(500 / portTICK_PERIOD_MS); + if (uidLength == 7) { uint16_t tagSize = readTagSize(); @@ -487,7 +491,7 @@ void scanRfidTask(void * parameter) { } } - if (!success && nfcReaderState != NFC_IDLE) + if (!success && nfcReaderState != NFC_IDLE && !suspendNfcReading) { nfcReaderState = NFC_IDLE; //uidString = ""; @@ -500,6 +504,12 @@ void scanRfidTask(void * parameter) { // aktualisieren der Website wenn sich der Status ändert sendNfcData(); } + else + { + // TBD: Debug only: + Serial.println("NFC Reading disabled"); + vTaskDelay(1000 / portTICK_PERIOD_MS); + } yield(); } } From 89a5728cc06c04374e883cbd41b9ff1b857b66f8 Mon Sep 17 00:00:00 2001 From: Jan Philipp Ecker Date: Fri, 8 Aug 2025 15:33:08 +0200 Subject: [PATCH 2/6] Improves NFC writing workaround and removes debug output Improved version of the NFC writing workaround. The task is no longer suspended. There is now a suspend request and a suspend state variable that is used to communicate between the writing and the reading task. The reading is stopped gracefully to prevent resets during writing. --- src/nfc.cpp | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/nfc.cpp b/src/nfc.cpp index a68c90a..a95f9ff 100644 --- a/src/nfc.cpp +++ b/src/nfc.cpp @@ -20,7 +20,8 @@ String lastSpoolId = ""; String nfcJsonData = ""; bool tagProcessed = false; volatile bool pauseBambuMqttTask = false; -volatile bool suspendNfcReading = false; +volatile bool nfcReadingTaskSuspendRequest = false; +volatile bool nfcReadingTaskSuspendState = false; struct NfcWriteParameterType { bool tagType; @@ -279,21 +280,21 @@ void writeJsonToTag(void *parameter) { Serial.println(params->payload); nfcReaderState = NFC_WRITING; - suspendNfcReading = true; - //vTaskSuspend(RfidReaderTask); - // make sure to wait 600ms, after that the reading task should be waiting - vTaskDelay(1000 / portTICK_PERIOD_MS); + + // First request the reading task to be suspended and than wait until it responds + nfcReadingTaskSuspendRequest = true; + while(nfcReadingTaskSuspendState == false){ + vTaskDelay(100 / portTICK_PERIOD_MS); + } //pauseBambuMqttTask = true; // aktualisieren der Website wenn sich der Status ändert sendNfcData(); vTaskDelay(100 / portTICK_PERIOD_MS); - Serial.println("CP 1"); // Wait 10sec for tag uint8_t success = 0; String uidString = ""; for (uint16_t i = 0; i < 20; i++) { - Serial.println("CP 2"); uint8_t uid[] = { 0, 0, 0, 0, 0, 0, 0 }; // Buffer to store the returned UID uint8_t uidLength; // yield before potentially waiting for 400ms @@ -301,7 +302,6 @@ void writeJsonToTag(void *parameter) { esp_task_wdt_reset(); success = nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength, 400); if (success) { - Serial.println("CP 3.1"); for (uint8_t i = 0; i < uidLength; i++) { //TBD: Rework to remove all the string operations uidString += String(uid[i], HEX); @@ -311,8 +311,6 @@ void writeJsonToTag(void *parameter) { } foundNfcTag(nullptr, success); break; - }else{ - Serial.println("CP 3.2"); } yield(); @@ -375,8 +373,7 @@ void writeJsonToTag(void *parameter) { sendWriteResult(nullptr, success); sendNfcData(); - suspendNfcReading = false; - //vTaskResume(RfidReaderTask); + nfcReadingTaskSuspendRequest = false; pauseBambuMqttTask = false; vTaskDelete(NULL); @@ -409,8 +406,9 @@ void scanRfidTask(void * parameter) { Serial.println("RFID Task gestartet"); for(;;) { // Wenn geschrieben wird Schleife aussetzen - if (nfcReaderState != NFC_WRITING && !suspendNfcReading) + if (nfcReaderState != NFC_WRITING && !nfcReadingTaskSuspendRequest) { + nfcReadingTaskSuspendState = false; yield(); uint8_t success; @@ -491,7 +489,7 @@ void scanRfidTask(void * parameter) { } } - if (!success && nfcReaderState != NFC_IDLE && !suspendNfcReading) + if (!success && nfcReaderState != NFC_IDLE && !nfcReadingTaskSuspendRequest) { nfcReaderState = NFC_IDLE; //uidString = ""; @@ -506,7 +504,7 @@ void scanRfidTask(void * parameter) { } else { - // TBD: Debug only: + nfcReadingTaskSuspendState = true; Serial.println("NFC Reading disabled"); vTaskDelay(1000 / portTICK_PERIOD_MS); } From a7c99d3f269b75b1d5b24480a91964008d4466c6 Mon Sep 17 00:00:00 2001 From: Jan Philipp Ecker Date: Fri, 8 Aug 2025 15:39:10 +0200 Subject: [PATCH 3/6] Improves init - NFC reading now only starts after boot is finished NFC tags that are on the scale during startup will only be read after the boot sequence is finished. --- src/nfc.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nfc.cpp b/src/nfc.cpp index a95f9ff..83dfb5e 100644 --- a/src/nfc.cpp +++ b/src/nfc.cpp @@ -8,6 +8,7 @@ #include "esp_task_wdt.h" #include "scale.h" #include "bambu.h" +#include "main.h" //Adafruit_PN532 nfc(PN532_SCK, PN532_MISO, PN532_MOSI, PN532_SS); Adafruit_PN532 nfc(PN532_IRQ, PN532_RESET); @@ -406,7 +407,7 @@ void scanRfidTask(void * parameter) { Serial.println("RFID Task gestartet"); for(;;) { // Wenn geschrieben wird Schleife aussetzen - if (nfcReaderState != NFC_WRITING && !nfcReadingTaskSuspendRequest) + if (nfcReaderState != NFC_WRITING && !nfcReadingTaskSuspendRequest && !booting) { nfcReadingTaskSuspendState = false; yield(); From 5509d98969e60dd5bab526a7ce6e5e9f90cbb787 Mon Sep 17 00:00:00 2001 From: Jan Philipp Ecker Date: Fri, 8 Aug 2025 16:16:39 +0200 Subject: [PATCH 4/6] Fixes issue that scale not calibrated message was not shown There was no warning any more if the scale is not calibrated. This change fixes that. --- src/config.cpp | 1 - src/config.h | 1 + src/main.cpp | 139 ++++++++++++++++++++++++------------------------- src/scale.cpp | 27 +++++----- src/scale.h | 3 +- 5 files changed, 85 insertions(+), 86 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 359031a..60a8803 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -16,7 +16,6 @@ const uint8_t LOADCELL_DOUT_PIN = 16; //16; const uint8_t LOADCELL_SCK_PIN = 17; //17; const uint8_t calVal_eepromAdress = 0; const uint16_t SCALE_LEVEL_WEIGHT = 500; -uint16_t defaultScaleCalibrationValue = 430; // ***** HX711 // ***** TTP223 (Touch Sensor) diff --git a/src/config.h b/src/config.h index 88c57c0..a6468a7 100644 --- a/src/config.h +++ b/src/config.h @@ -22,6 +22,7 @@ #define NVS_NAMESPACE_SCALE "scale" #define NVS_KEY_CALIBRATION "cal_value" #define NVS_KEY_AUTOTARE "auto_tare" +#define SCALE_DEFAULT_CALIBRATION_VALUE 430.0f; #define BAMBU_USERNAME "bblp" diff --git a/src/main.cpp b/src/main.cpp index f160e95..d301583 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -156,94 +156,93 @@ void loop() { } } - // Wenn Waage nicht Kalibriert - if (scaleCalibrated == 3) + // If scale is not calibrated, only show a warning + if (!scaleCalibrated) { - oledShowMessage("Scale not calibrated!"); - vTaskDelay(5000 / portTICK_PERIOD_MS); - yield(); - esp_task_wdt_reset(); - - return; - } - - // Ausgabe der Waage auf Display - if(pauseMainTask == 0) - { - if (mainTaskWasPaused || (weight != lastWeight && nfcReaderState == NFC_IDLE && (!bambuCredentials.autosend_enable || autoSetToBambuSpoolId == 0))) - { - (weight < 2) ? ((weight < -2) ? oledShowMessage("!! -0") : oledShowWeight(0)) : oledShowWeight(weight); + // Do not show the warning if the calibratin process is onging + if(!scaleCalibrationActive){ + oledShowMessage("Scale not calibrated"); + vTaskDelay(1000 / portTICK_PERIOD_MS); } - mainTaskWasPaused = false; - } - else - { - mainTaskWasPaused = true; - } - - - // Wenn Timer abgelaufen und nicht gerade ein RFID-Tag geschrieben wird - if (currentMillis - lastWeightReadTime >= weightReadInterval && nfcReaderState < NFC_WRITING) - { - lastWeightReadTime = currentMillis; - - // Prüfen ob die Waage korrekt genullt ist - // Abweichung von 2g ignorieren - if (autoTare && (weight > 2 && weight < 7) || weight < -2) + }else{ + // Ausgabe der Waage auf Display + if(pauseMainTask == 0) { - scale_tare_counter++; + if (mainTaskWasPaused || (weight != lastWeight && nfcReaderState == NFC_IDLE && (!bambuCredentials.autosend_enable || autoSetToBambuSpoolId == 0))) + { + (weight < 2) ? ((weight < -2) ? oledShowMessage("!! -0") : oledShowWeight(0)) : oledShowWeight(weight); + } + mainTaskWasPaused = false; } else { - scale_tare_counter = 0; + mainTaskWasPaused = true; } - // Prüfen ob das Gewicht gleich bleibt und dann senden - if (abs(weight - lastWeight) <= 2 && weight > 5) + + // Wenn Timer abgelaufen und nicht gerade ein RFID-Tag geschrieben wird + if (currentMillis - lastWeightReadTime >= weightReadInterval && nfcReaderState < NFC_WRITING) { - weigthCouterToApi++; - } - else + lastWeightReadTime = currentMillis; + + // Prüfen ob die Waage korrekt genullt ist + // Abweichung von 2g ignorieren + if (autoTare && (weight > 2 && weight < 7) || weight < -2) + { + scale_tare_counter++; + } + else + { + scale_tare_counter = 0; + } + + // Prüfen ob das Gewicht gleich bleibt und dann senden + if (abs(weight - lastWeight) <= 2 && weight > 5) + { + weigthCouterToApi++; + } + else + { + weigthCouterToApi = 0; + weightSend = 0; + } + } + + // reset weight counter after writing tag + // TBD: what exactly is the logic behind this? + if (currentMillis - lastWeightReadTime >= weightReadInterval && nfcReaderState != NFC_IDLE && nfcReaderState != NFC_READ_SUCCESS) { weigthCouterToApi = 0; - weightSend = 0; } - } + + lastWeight = weight; - // reset weight counter after writing tag - // TBD: what exactly is the logic behind this? - if (currentMillis - lastWeightReadTime >= weightReadInterval && nfcReaderState != NFC_IDLE && nfcReaderState != NFC_READ_SUCCESS) - { - weigthCouterToApi = 0; - } - - lastWeight = weight; + // Wenn ein Tag mit SM id erkannte wurde und der Waage Counter anspricht an SM Senden + if (activeSpoolId != "" && weigthCouterToApi > 3 && weightSend == 0 && nfcReaderState == NFC_READ_SUCCESS && tagProcessed == false && spoolmanApiState == API_IDLE) { + // set the current tag as processed to prevent it beeing processed again + tagProcessed = true; - // Wenn ein Tag mit SM id erkannte wurde und der Waage Counter anspricht an SM Senden - if (activeSpoolId != "" && weigthCouterToApi > 3 && weightSend == 0 && nfcReaderState == NFC_READ_SUCCESS && tagProcessed == false && spoolmanApiState == API_IDLE) { - // set the current tag as processed to prevent it beeing processed again - tagProcessed = true; - - if (updateSpoolWeight(activeSpoolId, weight)) - { - weightSend = 1; - + if (updateSpoolWeight(activeSpoolId, weight)) + { + weightSend = 1; + + } + else + { + oledShowIcon("failed"); + vTaskDelay(2000 / portTICK_PERIOD_MS); + } } - else - { - oledShowIcon("failed"); - vTaskDelay(2000 / portTICK_PERIOD_MS); - } - } - if(sendOctoUpdate && spoolmanApiState == API_IDLE){ - autoSetToBambuSpoolId = activeSpoolId.toInt(); + if(sendOctoUpdate && spoolmanApiState == API_IDLE){ + autoSetToBambuSpoolId = activeSpoolId.toInt(); - if(octoEnabled) - { - updateSpoolOcto(autoSetToBambuSpoolId); + if(octoEnabled) + { + updateSpoolOcto(autoSetToBambuSpoolId); + } + sendOctoUpdate = false; } - sendOctoUpdate = false; } esp_task_wdt_reset(); diff --git a/src/scale.cpp b/src/scale.cpp index 4be30c7..201618d 100644 --- a/src/scale.cpp +++ b/src/scale.cpp @@ -17,8 +17,9 @@ uint8_t weigthCouterToApi = 0; uint8_t scale_tare_counter = 0; bool scaleTareRequest = false; uint8_t pauseMainTask = 0; -uint8_t scaleCalibrated = 1; +bool scaleCalibrated; bool autoTare = true; +bool scaleCalibrationActive = false; // ##### Funktionen für Waage ##### uint8_t setAutoTare(bool autoTareValue) { @@ -88,7 +89,13 @@ void start_scale(bool touchSensorConnected) { // NVS lesen Preferences preferences; preferences.begin(NVS_NAMESPACE_SCALE, true); // true = readonly - calibrationValue = preferences.getFloat(NVS_KEY_CALIBRATION, defaultScaleCalibrationValue); + if(preferences.isKey(NVS_KEY_CALIBRATION)){ + calibrationValue = preferences.getFloat(NVS_KEY_CALIBRATION); + scaleCalibrated = true; + }else{ + calibrationValue = SCALE_DEFAULT_CALIBRATION_VALUE; + scaleCalibrated = false; + } // auto Tare // Wenn Touch Sensor verbunden, dann autoTare auf false setzen @@ -103,18 +110,6 @@ void start_scale(bool touchSensorConnected) { scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN); - if (isnan(calibrationValue) || calibrationValue < 1) { - calibrationValue = defaultScaleCalibrationValue; - scaleCalibrated = 0; - - oledShowMessage("Scale not calibrated!"); - for (uint16_t i = 0; i < 50000; i++) { - yield(); - vTaskDelay(pdMS_TO_TICKS(1)); - esp_task_wdt_reset(); - } - } - oledShowProgressBar(6, 7, DISPLAY_BOOT_TEXT, "Tare scale"); for (uint16_t i = 0; i < 2000; i++) { yield(); @@ -152,6 +147,8 @@ uint8_t calibrate_scale() { uint8_t returnState = 0; float newCalibrationValue; + scaleCalibrationActive = true; + vTaskSuspend(RfidReaderTask); vTaskSuspend(ScaleTask); @@ -228,6 +225,7 @@ uint8_t calibrate_scale() { esp_task_wdt_reset(); } + scaleCalibrated = true; returnState = 1; } else @@ -262,6 +260,7 @@ uint8_t calibrate_scale() { vTaskResume(ScaleTask); pauseBambuMqttTask = false; pauseMainTask = 0; + scaleCalibrationActive = false; return returnState; } diff --git a/src/scale.h b/src/scale.h index 96f59a4..c38583a 100644 --- a/src/scale.h +++ b/src/scale.h @@ -15,8 +15,9 @@ extern uint8_t weigthCouterToApi; extern uint8_t scale_tare_counter; extern uint8_t scaleTareRequest; extern uint8_t pauseMainTask; -extern uint8_t scaleCalibrated; +extern bool scaleCalibrated; extern bool autoTare; +extern bool scaleCalibrationActive; extern TaskHandle_t ScaleTask; From 4706152022defdb2c83fc212bda7b348e7753e41 Mon Sep 17 00:00:00 2001 From: Jan Philipp Ecker Date: Fri, 8 Aug 2025 18:00:25 +0200 Subject: [PATCH 5/6] Introduces periodic Spoolman Healthcheck Introduces a spoolman healthcheck that is executed every 60 seconds. Also fixes a bug with the periodic wifi update. --- src/api.cpp | 395 +++++++++++++++++++++++++++------------------------ src/api.h | 2 +- src/config.h | 52 +++---- src/main.cpp | 13 +- src/nfc.cpp | 2 - 5 files changed, 249 insertions(+), 215 deletions(-) diff --git a/src/api.cpp b/src/api.cpp index 4b65f4b..8770fd2 100644 --- a/src/api.cpp +++ b/src/api.cpp @@ -5,7 +5,7 @@ #include #include "debug.h" -volatile spoolmanApiStateType spoolmanApiState = API_INIT; +volatile spoolmanApiStateType spoolmanApiState = API_IDLE; //bool spoolman_connected = false; String spoolmanUrl = ""; bool octoEnabled = false; @@ -14,6 +14,8 @@ String octoUrl = ""; String octoToken = ""; uint16_t remainingWeight = 0; bool spoolmanConnected = false; +bool spoolmanExtraFieldsChecked = false; +TaskHandle_t* apiTask; struct SendToApiParams { SpoolmanApiRequestType requestType; @@ -94,6 +96,11 @@ JsonDocument fetchSingleSpoolInfo(int spoolId) { void sendToApi(void *parameter) { HEAP_DEBUG_MESSAGE("sendToApi begin"); + // Wait until API is IDLE + while(spoolmanApiState != API_IDLE){ + Serial.println("Waiting!"); + yield(); + } spoolmanApiState = API_TRANSMITTING; SendToApiParams* params = (SendToApiParams*)parameter; @@ -236,7 +243,7 @@ bool updateSpoolTagId(String uidString, const char* payload) { 6144, // Stackgröße in Bytes (void*)params, // Parameter 0, // Priorität - NULL // Task-Handle (nicht benötigt) + apiTask // Task-Handle (nicht benötigt) ); updateDoc.clear(); @@ -282,7 +289,7 @@ uint8_t updateSpoolWeight(String spoolId, uint16_t weight) { 6144, // Stackgröße in Bytes (void*)params, // Parameter 0, // Priorität - NULL // Task-Handle (nicht benötigt) + apiTask // Task-Handle (nicht benötigt) ); updateDoc.clear(); @@ -319,17 +326,17 @@ uint8_t updateSpoolLocation(String spoolId, String location){ params->spoolsUrl = spoolsUrl; params->updatePayload = updatePayload; - if(spoolmanApiState == API_IDLE){ - // 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) - ); + if(apiTask == nullptr){ + // Erstelle die Task + BaseType_t result = xTaskCreate( + sendToApi, // Task-Funktion + "SendToApiTask", // Task-Name + 6144, // Stackgröße in Bytes + (void*)params, // Parameter + 0, // Priorität + apiTask // Task-Handle + ); }else{ Serial.println("Not spawning new task, API still active!"); } @@ -374,7 +381,7 @@ bool updateSpoolOcto(int spoolId) { 6144, // Stackgröße in Bytes (void*)params, // Parameter 0, // Priorität - NULL // Task-Handle (nicht benötigt) + apiTask // Task-Handle (nicht benötigt) ); updateDoc.clear(); @@ -427,7 +434,7 @@ bool updateSpoolBambuData(String payload) { 6144, // Stackgröße in Bytes (void*)params, // Parameter 0, // Priorität - NULL // Task-Handle (nicht benötigt) + apiTask // Task-Handle (nicht benötigt) ); return true; @@ -435,198 +442,222 @@ bool updateSpoolBambuData(String payload) { // #### Spoolman init bool checkSpoolmanExtraFields() { - HTTPClient http; - String checkUrls[] = { - spoolmanUrl + apiUrl + "/field/spool", - spoolmanUrl + apiUrl + "/field/filament" - }; + // Only check extra fields if they have not been checked before + if(!spoolmanExtraFieldsChecked){ + HTTPClient http; + String checkUrls[] = { + spoolmanUrl + apiUrl + "/field/spool", + spoolmanUrl + apiUrl + "/field/filament" + }; - String spoolExtra[] = { - "nfc_id" - }; + String spoolExtra[] = { + "nfc_id" + }; - String filamentExtra[] = { - "nozzle_temperature", - "price_meter", - "price_gramm", - "bambu_setting_id", - "bambu_cali_id", - "bambu_idx", - "bambu_k", - "bambu_flow_ratio", - "bambu_max_volspeed" - }; + String filamentExtra[] = { + "nozzle_temperature", + "price_meter", + "price_gramm", + "bambu_setting_id", + "bambu_cali_id", + "bambu_idx", + "bambu_k", + "bambu_flow_ratio", + "bambu_max_volspeed" + }; - String spoolExtraFields[] = { - "{\"name\": \"NFC ID\"," - "\"key\": \"nfc_id\"," - "\"field_type\": \"text\"}" - }; + String spoolExtraFields[] = { + "{\"name\": \"NFC ID\"," + "\"key\": \"nfc_id\"," + "\"field_type\": \"text\"}" + }; - String filamentExtraFields[] = { - "{\"name\": \"Nozzle Temp\"," - "\"unit\": \"°C\"," - "\"field_type\": \"integer_range\"," - "\"default_value\": \"[190,230]\"," - "\"key\": \"nozzle_temperature\"}", + String filamentExtraFields[] = { + "{\"name\": \"Nozzle Temp\"," + "\"unit\": \"°C\"," + "\"field_type\": \"integer_range\"," + "\"default_value\": \"[190,230]\"," + "\"key\": \"nozzle_temperature\"}", - "{\"name\": \"Price/m\"," - "\"unit\": \"€\"," - "\"field_type\": \"float\"," - "\"key\": \"price_meter\"}", + "{\"name\": \"Price/m\"," + "\"unit\": \"€\"," + "\"field_type\": \"float\"," + "\"key\": \"price_meter\"}", + + "{\"name\": \"Price/g\"," + "\"unit\": \"€\"," + "\"field_type\": \"float\"," + "\"key\": \"price_gramm\"}", + + "{\"name\": \"Bambu Setting ID\"," + "\"field_type\": \"text\"," + "\"key\": \"bambu_setting_id\"}", + + "{\"name\": \"Bambu Cali ID\"," + "\"field_type\": \"text\"," + "\"key\": \"bambu_cali_id\"}", + + "{\"name\": \"Bambu Filament IDX\"," + "\"field_type\": \"text\"," + "\"key\": \"bambu_idx\"}", + + "{\"name\": \"Bambu k\"," + "\"field_type\": \"float\"," + "\"key\": \"bambu_k\"}", + + "{\"name\": \"Bambu Flow Ratio\"," + "\"field_type\": \"float\"," + "\"key\": \"bambu_flow_ratio\"}", + + "{\"name\": \"Bambu Max Vol. Speed\"," + "\"unit\": \"mm3/s\"," + "\"field_type\": \"integer\"," + "\"default_value\": \"12\"," + "\"key\": \"bambu_max_volspeed\"}" + }; + + Serial.println("Überprüfe Extrafelder..."); + + int urlLength = sizeof(checkUrls) / sizeof(checkUrls[0]); + + for (uint8_t i = 0; i < urlLength; i++) { + Serial.println(); + Serial.println("-------- Prüfe Felder für "+checkUrls[i]+" --------"); + http.begin(checkUrls[i]); + int httpCode = http.GET(); - "{\"name\": \"Price/g\"," - "\"unit\": \"€\"," - "\"field_type\": \"float\"," - "\"key\": \"price_gramm\"}", + if (httpCode == HTTP_CODE_OK) { + String payload = http.getString(); + JsonDocument doc; + DeserializationError error = deserializeJson(doc, payload); + if (!error) { + String* extraFields; + String* extraFieldData; + u16_t extraLength; - "{\"name\": \"Bambu Setting ID\"," - "\"field_type\": \"text\"," - "\"key\": \"bambu_setting_id\"}", - - "{\"name\": \"Bambu Cali ID\"," - "\"field_type\": \"text\"," - "\"key\": \"bambu_cali_id\"}", - - "{\"name\": \"Bambu Filament IDX\"," - "\"field_type\": \"text\"," - "\"key\": \"bambu_idx\"}", - - "{\"name\": \"Bambu k\"," - "\"field_type\": \"float\"," - "\"key\": \"bambu_k\"}", - - "{\"name\": \"Bambu Flow Ratio\"," - "\"field_type\": \"float\"," - "\"key\": \"bambu_flow_ratio\"}", - - "{\"name\": \"Bambu Max Vol. Speed\"," - "\"unit\": \"mm3/s\"," - "\"field_type\": \"integer\"," - "\"default_value\": \"12\"," - "\"key\": \"bambu_max_volspeed\"}" - }; - - Serial.println("Überprüfe Extrafelder..."); - - int urlLength = sizeof(checkUrls) / sizeof(checkUrls[0]); - - for (uint8_t i = 0; i < urlLength; i++) { - Serial.println(); - Serial.println("-------- Prüfe Felder für "+checkUrls[i]+" --------"); - http.begin(checkUrls[i]); - int httpCode = http.GET(); - - if (httpCode == HTTP_CODE_OK) { - String payload = http.getString(); - JsonDocument doc; - DeserializationError error = deserializeJson(doc, payload); - if (!error) { - String* extraFields; - String* extraFieldData; - u16_t extraLength; - - if (i == 0) { - extraFields = spoolExtra; - extraFieldData = spoolExtraFields; - extraLength = sizeof(spoolExtra) / sizeof(spoolExtra[0]); - } else { - extraFields = filamentExtra; - extraFieldData = filamentExtraFields; - extraLength = sizeof(filamentExtra) / sizeof(filamentExtra[0]); - } - - for (uint8_t s = 0; s < extraLength; s++) { - bool found = false; - for (JsonObject field : doc.as()) { - if (field["key"].is() && field["key"] == extraFields[s]) { - Serial.println("Feld gefunden: " + extraFields[s]); - found = true; - break; - } + if (i == 0) { + extraFields = spoolExtra; + extraFieldData = spoolExtraFields; + extraLength = sizeof(spoolExtra) / sizeof(spoolExtra[0]); + } else { + extraFields = filamentExtra; + extraFieldData = filamentExtraFields; + extraLength = sizeof(filamentExtra) / sizeof(filamentExtra[0]); } - if (!found) { - Serial.println("Feld nicht gefunden: " + extraFields[s]); - // Extrafeld hinzufügen - http.begin(checkUrls[i] + "/" + extraFields[s]); - http.addHeader("Content-Type", "application/json"); - int httpCode = http.POST(extraFieldData[s]); + for (uint8_t s = 0; s < extraLength; s++) { + bool found = false; + for (JsonObject field : doc.as()) { + if (field["key"].is() && field["key"] == extraFields[s]) { + Serial.println("Feld gefunden: " + extraFields[s]); + found = true; + break; + } + } + if (!found) { + Serial.println("Feld nicht gefunden: " + extraFields[s]); - if (httpCode > 0) { - // Antwortscode und -nachricht abrufen - String response = http.getString(); - //Serial.println("HTTP-Code: " + String(httpCode)); - //Serial.println("Antwort: " + response); - if (httpCode != HTTP_CODE_OK) { + // Extrafeld hinzufügen + http.begin(checkUrls[i] + "/" + extraFields[s]); + http.addHeader("Content-Type", "application/json"); + int httpCode = http.POST(extraFieldData[s]); + if (httpCode > 0) { + // Antwortscode und -nachricht abrufen + String response = http.getString(); + //Serial.println("HTTP-Code: " + String(httpCode)); + //Serial.println("Antwort: " + response); + if (httpCode != HTTP_CODE_OK) { + + return false; + } + } else { + // Fehler beim Senden der Anfrage + Serial.println("Fehler beim Senden der Anfrage: " + String(http.errorToString(httpCode))); return false; } - } else { - // Fehler beim Senden der Anfrage - Serial.println("Fehler beim Senden der Anfrage: " + String(http.errorToString(httpCode))); - return false; + //http.end(); } - //http.end(); + yield(); + vTaskDelay(100 / portTICK_PERIOD_MS); } - yield(); - vTaskDelay(100 / portTICK_PERIOD_MS); } + doc.clear(); } - doc.clear(); } + + Serial.println("-------- ENDE Prüfe Felder --------"); + Serial.println(); + + http.end(); + + spoolmanExtraFieldsChecked = true; + return true; + }else{ + return true; } - - Serial.println("-------- ENDE Prüfe Felder --------"); - Serial.println(); - - http.end(); - - return true; } -bool checkSpoolmanInstance(const String& url) { +bool checkSpoolmanInstance() { HTTPClient http; - String healthUrl = url + apiUrl + "/health"; + bool returnValue = false; - Serial.print("Überprüfe Spoolman-Instanz unter: "); - Serial.println(healthUrl); + // Only do the spoolman instance check if there is no active API request going on + if(spoolmanApiState == API_IDLE){ + spoolmanApiState = API_TRANSMITTING; + String healthUrl = spoolmanUrl + apiUrl + "/health"; - http.begin(healthUrl); - int httpCode = http.GET(); + Serial.print("Checking spoolman instance: "); + Serial.println(healthUrl); - if (httpCode > 0) { - if (httpCode == HTTP_CODE_OK) { - String payload = http.getString(); - JsonDocument doc; - DeserializationError error = deserializeJson(doc, payload); - if (!error && doc["status"].is()) { - const char* status = doc["status"]; - http.end(); + http.begin(healthUrl); + int httpCode = http.GET(); - if (!checkSpoolmanExtraFields()) { - Serial.println("Fehler beim Überprüfen der Extrafelder."); + if (httpCode > 0) { + if (httpCode == HTTP_CODE_OK) { + String payload = http.getString(); + JsonDocument doc; + DeserializationError error = deserializeJson(doc, payload); + if (!error && doc["status"].is()) { + const char* status = doc["status"]; + http.end(); - // TBD - oledShowMessage("Spoolman Error creating Extrafields"); - vTaskDelay(2000 / portTICK_PERIOD_MS); - - return false; + if (!checkSpoolmanExtraFields()) { + Serial.println("Fehler beim Überprüfen der Extrafelder."); + + // TBD + oledShowMessage("Spoolman Error creating Extrafields"); + vTaskDelay(2000 / portTICK_PERIOD_MS); + + return false; + } + + spoolmanApiState = API_IDLE; + oledShowTopRow(); + spoolmanConnected = true; + returnValue = strcmp(status, "healthy") == 0; + }else{ + spoolmanConnected = false; } - spoolmanApiState = API_IDLE; - oledShowTopRow(); - spoolmanConnected = true; - return strcmp(status, "healthy") == 0; + doc.clear(); + }else{ + spoolmanConnected = false; } - - doc.clear(); + } else { + spoolmanConnected = false; + Serial.println("Error contacting spoolman instance! HTTP Code: " + String(httpCode)); } - } else { - Serial.println("Error contacting spoolman instance! HTTP Code: " + String(httpCode)); + http.end(); + returnValue = false; + spoolmanApiState = API_IDLE; + }else{ + // If the check is skipped, return the previous status + Serial.println("Skipping spoolman healthcheck, API is active."); + returnValue = spoolmanConnected; } - http.end(); - return false; + Serial.println("Healthcheck completed!"); + return returnValue; } bool saveSpoolmanUrl(const String& url, bool octoOn, const String& octo_url, const String& octoTk) { @@ -639,12 +670,13 @@ bool saveSpoolmanUrl(const String& url, bool octoOn, const String& octo_url, con preferences.end(); //TBD: This could be handled nicer in the future + spoolmanExtraFieldsChecked = false; spoolmanUrl = url; octoEnabled = octoOn; octoUrl = octo_url; octoToken = octoTk; - return true; + return checkSpoolmanInstance(); } String loadSpoolmanUrl() { @@ -664,15 +696,10 @@ String loadSpoolmanUrl() { bool initSpoolman() { oledShowProgressBar(3, 7, DISPLAY_BOOT_TEXT, "Spoolman init"); spoolmanUrl = loadSpoolmanUrl(); - spoolmanUrl.trim(); - if (spoolmanUrl == "") { - Serial.println("Keine Spoolman-URL gefunden."); - return false; - } - - bool success = checkSpoolmanInstance(spoolmanUrl); + + bool success = checkSpoolmanInstance(); if (!success) { - Serial.println("Spoolman nicht erreichbar."); + Serial.println("Spoolman not available"); return false; } diff --git a/src/api.h b/src/api.h index 23fb6d8..4ad427f 100644 --- a/src/api.h +++ b/src/api.h @@ -29,7 +29,7 @@ extern String octoUrl; extern String octoToken; extern bool spoolmanConnected; -bool checkSpoolmanInstance(const String& url); +bool checkSpoolmanInstance(); bool saveSpoolmanUrl(const String& url, bool octoOn, const String& octoWh, const String& octoTk); String loadSpoolmanUrl(); // Neue Funktion zum Laden der URL bool checkSpoolmanExtraFields(); // Neue Funktion zum Überprüfen der Extrafelder diff --git a/src/config.h b/src/config.h index a6468a7..cd79e38 100644 --- a/src/config.h +++ b/src/config.h @@ -3,37 +3,39 @@ #include -#define BAMBU_DEFAULT_AUTOSEND_TIME 60 +#define BAMBU_DEFAULT_AUTOSEND_TIME 60 +#define NVS_NAMESPACE_API "api" +#define NVS_KEY_SPOOLMAN_URL "spoolmanUrl" +#define NVS_KEY_OCTOPRINT_ENABLED "octoEnabled" +#define NVS_KEY_OCTOPRINT_URL "octoUrl" +#define NVS_KEY_OCTOPRINT_TOKEN "octoToken" -#define NVS_NAMESPACE_API "api" -#define NVS_KEY_SPOOLMAN_URL "spoolmanUrl" -#define NVS_KEY_OCTOPRINT_ENABLED "octoEnabled" -#define NVS_KEY_OCTOPRINT_URL "octoUrl" -#define NVS_KEY_OCTOPRINT_TOKEN "octoToken" +#define NVS_NAMESPACE_BAMBU "bambu" +#define NVS_KEY_BAMBU_IP "bambuIp" +#define NVS_KEY_BAMBU_ACCESSCODE "bambuCode" +#define NVS_KEY_BAMBU_SERIAL "bambuSerial" +#define NVS_KEY_BAMBU_AUTOSEND_ENABLE "autosendEnable" +#define NVS_KEY_BAMBU_AUTOSEND_TIME "autosendTime" -#define NVS_NAMESPACE_BAMBU "bambu" -#define NVS_KEY_BAMBU_IP "bambuIp" -#define NVS_KEY_BAMBU_ACCESSCODE "bambuCode" -#define NVS_KEY_BAMBU_SERIAL "bambuSerial" -#define NVS_KEY_BAMBU_AUTOSEND_ENABLE "autosendEnable" -#define NVS_KEY_BAMBU_AUTOSEND_TIME "autosendTime" +#define NVS_NAMESPACE_SCALE "scale" +#define NVS_KEY_CALIBRATION "cal_value" +#define NVS_KEY_AUTOTARE "auto_tare" +#define SCALE_DEFAULT_CALIBRATION_VALUE 430.0f; -#define NVS_NAMESPACE_SCALE "scale" -#define NVS_KEY_CALIBRATION "cal_value" -#define NVS_KEY_AUTOTARE "auto_tare" -#define SCALE_DEFAULT_CALIBRATION_VALUE 430.0f; +#define BAMBU_USERNAME "bblp" -#define BAMBU_USERNAME "bblp" - -#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) -#define SCREEN_ADDRESS 0x3CU // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 -#define SCREEN_WIDTH 128U -#define SCREEN_HEIGHT 64U -#define SCREEN_TOP_BAR_HEIGHT 16U -#define SCREEN_PROGRESS_BAR_HEIGHT 12U -#define DISPLAY_BOOT_TEXT "FilaMan" +#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) +#define SCREEN_ADDRESS 0x3CU // See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 +#define SCREEN_WIDTH 128U +#define SCREEN_HEIGHT 64U +#define SCREEN_TOP_BAR_HEIGHT 16U +#define SCREEN_PROGRESS_BAR_HEIGHT 12U +#define DISPLAY_BOOT_TEXT "FilaMan" +#define WIFI_CHECK_INTERVAL 60000U +#define DISPLAY_UPDATE_INTERVAL 1000U +#define SPOOLMAN_HEALTHCHECK_INTERVAL 60000U extern const uint8_t PN532_IRQ; extern const uint8_t PN532_RESET; diff --git a/src/main.cpp b/src/main.cpp index d301583..2b75b6c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -97,7 +97,8 @@ int16_t lastWeight = 0; // WIFI check variables unsigned long lastWifiCheckTime = 0; -const unsigned long wifiCheckInterval = 60000; // Überprüfe alle 60 Sekunden (60000 ms) +unsigned long lastTopRowUpdateTime = 0; +unsigned long lastSpoolmanHealcheckTime = 0; // Button debounce variables unsigned long lastButtonPress = 0; @@ -115,17 +116,23 @@ void loop() { } // Überprüfe regelmäßig die WLAN-Verbindung - if (intervalElapsed(currentMillis, lastWifiCheckTime, wifiCheckInterval)) + if (intervalElapsed(currentMillis, lastWifiCheckTime, WIFI_CHECK_INTERVAL)) { checkWiFiConnection(); } // Periodic display update - if (intervalElapsed(currentMillis, lastWifiCheckTime, 1000)) + if (intervalElapsed(currentMillis, lastTopRowUpdateTime, DISPLAY_UPDATE_INTERVAL)) { oledShowTopRow(); } + // Periodic spoolman health check + if (intervalElapsed(currentMillis, lastSpoolmanHealcheckTime, SPOOLMAN_HEALTHCHECK_INTERVAL)) + { + checkSpoolmanInstance(); + } + // Wenn Bambu auto set Spool aktiv if (bambuCredentials.autosend_enable && autoSetToBambuSpoolId > 0) { diff --git a/src/nfc.cpp b/src/nfc.cpp index 83dfb5e..5a8bdc1 100644 --- a/src/nfc.cpp +++ b/src/nfc.cpp @@ -241,8 +241,6 @@ bool decodeNdefAndReturnJson(const byte* encodedMessage) { Serial.println("SPOOL-ID gefunden: " + doc["sm_id"].as()); activeSpoolId = doc["sm_id"].as(); lastSpoolId = activeSpoolId; - - Serial.println("Api state: " + String(spoolmanApiState)); } else if(doc["location"].is() && doc["location"] != "") { From 5fa93f2695dd0bc1104379eba958c03eb4a5672b Mon Sep 17 00:00:00 2001 From: Jan Philipp Ecker Date: Fri, 8 Aug 2025 18:14:26 +0200 Subject: [PATCH 6/6] Adds a link to the spool in spoolman when reading a spool tag Adds a link to the website that lets the user directly jump to the spool in spoolman that is currently scanned. --- html/rfid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html/rfid.js b/html/rfid.js index d6dfa75..ae9cd68 100644 --- a/html/rfid.js +++ b/html/rfid.js @@ -578,7 +578,7 @@ function updateNfcData(data) { `; // Spoolman ID anzeigen - html += `

Spoolman ID: ${data.sm_id || 'No Spoolman ID'}

`; + html += `

Spoolman ID: ${data.sm_id} (Open in Spoolman)

`; } else if(data.location) {