#include "api.h" #include #include #include "commonFS.h" #include #include "debug.h" #include "scale.h" #include "nfc.h" #include volatile spoolmanApiStateType spoolmanApiState = API_IDLE; //bool spoolman_connected = false; String spoolmanUrl = ""; bool octoEnabled = false; bool sendOctoUpdate = false; String octoUrl = ""; String octoToken = ""; uint16_t remainingWeight = 0; uint16_t createdVendorId = 0; // Store ID of newly created vendor uint16_t foundVendorId = 0; // Store ID of found vendor uint16_t foundFilamentId = 0; // Store ID of found filament uint16_t createdFilamentId = 0; // Store ID of newly created filament uint16_t createdSpoolId = 0; // Store ID of newly created spool uint16_t updateOctoSpoolId = 0; // Store spool ID for OctoPrint update bool spoolmanConnected = false; bool spoolmanExtraFieldsChecked = false; TaskHandle_t* apiTask; struct SendToApiParams { SpoolmanApiRequestType requestType; String httpType; String spoolsUrl; String updatePayload; String octoToken; // Weight update parameters for sequential execution bool triggerWeightUpdate; String spoolIdForWeight; uint16_t weightValue; }; JsonDocument fetchSingleSpoolInfo(int spoolId) { HTTPClient http; String spoolsUrl = spoolmanUrl + apiUrl + "/spool/" + spoolId; Serial.print("Rufe Spool-Daten von: "); Serial.println(spoolsUrl); http.begin(spoolsUrl); int httpCode = http.GET(); JsonDocument filteredDoc; if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); JsonDocument doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print("Fehler beim Parsen der JSON-Antwort: "); Serial.println(error.c_str()); } else { String filamentType = doc["filament"]["material"].as(); String filamentBrand = doc["filament"]["vendor"]["name"].as(); int nozzle_temp_min = 0; int nozzle_temp_max = 0; if (doc["filament"]["extra"]["nozzle_temperature"].is()) { String tempString = doc["filament"]["extra"]["nozzle_temperature"].as(); tempString.replace("[", ""); tempString.replace("]", ""); int commaIndex = tempString.indexOf(','); if (commaIndex != -1) { nozzle_temp_min = tempString.substring(0, commaIndex).toInt(); nozzle_temp_max = tempString.substring(commaIndex + 1).toInt(); } } String filamentColor = doc["filament"]["color_hex"].as(); filamentColor.toUpperCase(); String tray_info_idx = doc["filament"]["extra"]["bambu_idx"].as(); tray_info_idx.replace("\"", ""); String cali_idx = doc["filament"]["extra"]["bambu_cali_id"].as(); // "\"153\"" cali_idx.replace("\"", ""); String bambu_setting_id = doc["filament"]["extra"]["bambu_setting_id"].as(); // "\"PFUSf40e9953b40d3d\"" bambu_setting_id.replace("\"", ""); doc.clear(); filteredDoc["color"] = filamentColor; filteredDoc["type"] = filamentType; filteredDoc["nozzle_temp_min"] = nozzle_temp_min; filteredDoc["nozzle_temp_max"] = nozzle_temp_max; filteredDoc["brand"] = filamentBrand; filteredDoc["tray_info_idx"] = tray_info_idx; filteredDoc["cali_idx"] = cali_idx; filteredDoc["bambu_setting_id"] = bambu_setting_id; } } else { Serial.print("Fehler beim Abrufen der Spool-Daten. HTTP-Code: "); Serial.println(httpCode); } http.end(); return filteredDoc; } void sendToApi(void *parameter) { HEAP_DEBUG_MESSAGE("sendToApi begin"); // Wait until API is IDLE while(spoolmanApiState != API_IDLE){ vTaskDelay(100 / portTICK_PERIOD_MS); yield(); } spoolmanApiState = API_TRANSMITTING; SendToApiParams* params = (SendToApiParams*)parameter; // Extract values including weight update parameters SpoolmanApiRequestType requestType = params->requestType; String httpType = params->httpType; String spoolsUrl = params->spoolsUrl; String updatePayload = params->updatePayload; String octoToken = params->octoToken; bool triggerWeightUpdate = params->triggerWeightUpdate; String spoolIdForWeight = params->spoolIdForWeight; uint16_t weightValue = params->weightValue; // Retry mechanism with configurable parameters const uint8_t MAX_RETRIES = 3; const uint16_t RETRY_DELAY_MS = 1000; // 1 second between retries const uint16_t HTTP_TIMEOUT_MS = 10000; // 10 second HTTP timeout bool success = false; int httpCode = -1; String responsePayload = ""; // Try request with retries for (uint8_t attempt = 1; attempt <= MAX_RETRIES && !success; attempt++) { Serial.printf("API Request attempt %d/%d to: %s\n", attempt, MAX_RETRIES, spoolsUrl.c_str()); HTTPClient http; http.setReuse(false); http.setTimeout(HTTP_TIMEOUT_MS); // Set HTTP timeout http.begin(spoolsUrl); http.addHeader("Content-Type", "application/json"); if (octoEnabled && octoToken != "") http.addHeader("X-Api-Key", octoToken); // Execute HTTP request based on type if (httpType == "PATCH") httpCode = http.PATCH(updatePayload); else if (httpType == "POST") httpCode = http.POST(updatePayload); else if (httpType == "GET") httpCode = http.GET(); else httpCode = http.PUT(updatePayload); // Check if request was successful if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_CREATED) { responsePayload = http.getString(); success = true; Serial.printf("API Request successful on attempt %d, HTTP Code: %d\n", attempt, httpCode); } else { Serial.printf("API Request failed on attempt %d, HTTP Code: %d (%s)\n", attempt, httpCode, http.errorToString(httpCode).c_str()); // Don't retry on certain error codes (client errors) if (httpCode >= 400 && httpCode < 500 && httpCode != 408 && httpCode != 429) { Serial.println("Client error detected, stopping retries"); break; } // Wait before retry (except on last attempt) if (attempt < MAX_RETRIES) { Serial.printf("Waiting %dms before retry...\n", RETRY_DELAY_MS); http.end(); vTaskDelay(RETRY_DELAY_MS / portTICK_PERIOD_MS); continue; } } http.end(); } // Process successful response if (success) { Serial.println("Spoolman Abfrage erfolgreich"); // Restgewicht der Spule auslesen JsonDocument doc; DeserializationError error = deserializeJson(doc, responsePayload); if (error) { Serial.print("Fehler beim Parsen der JSON-Antwort: "); Serial.println(error.c_str()); } else { switch(requestType){ case API_REQUEST_SPOOL_WEIGHT_UPDATE: remainingWeight = doc["remaining_weight"].as(); Serial.print("Aktuelles Gewicht: "); Serial.println(remainingWeight); //oledShowMessage("Remaining: " + String(remaining_weight) + "g"); if(!octoEnabled){ // TBD: Do not use Strings... //oledShowProgressBar(1, 1, "Spool Tag", ("Done: " + String(remainingWeight) + " g remain").c_str()); oledShowMessage("Remaining: " + String(remainingWeight) + "g"); remainingWeight = 0; }else{ // ocoto is enabled, trigger octo update sendOctoUpdate = true; } break; case API_REQUEST_SPOOL_LOCATION_UPDATE: oledShowProgressBar(1, 1, "Loc. Tag", "Done!"); break; case API_REQUEST_SPOOL_TAG_ID_UPDATE: oledShowProgressBar(1, 1, "Write Tag", "Done!"); break; case API_REQUEST_OCTO_SPOOL_UPDATE: // TBD: Do not use Strings... //oledShowProgressBar(5, 5, "Spool Tag", ("Done: " + String(remainingWeight) + " g remain").c_str()); oledShowMessage("Remaining: " + String(remainingWeight) + "g"); remainingWeight = 0; break; case API_REQUEST_VENDOR_CREATE: Serial.println("Vendor successfully created!"); createdVendorId = doc["id"].as(); Serial.print("Created Vendor ID: "); Serial.println(createdVendorId); oledShowProgressBar(1, 1, "Vendor", "Created!"); break; case API_REQUEST_VENDOR_CHECK: if (doc.isNull() || doc.size() == 0) { Serial.println("Vendor not found in response"); foundVendorId = 0; } else { foundVendorId = doc[0]["id"].as(); Serial.print("Found Vendor ID: "); Serial.println(foundVendorId); } break; case API_REQUEST_FILAMENT_CHECK: if (doc.isNull() || doc.size() == 0) { Serial.println("Filament not found in response"); foundFilamentId = 0; } else { foundFilamentId = doc[0]["id"].as(); Serial.print("Found Filament ID: "); Serial.println(foundFilamentId); } break; case API_REQUEST_FILAMENT_CREATE: Serial.println("Filament successfully created!"); createdFilamentId = doc["id"].as(); Serial.print("Created Filament ID: "); Serial.println(createdFilamentId); oledShowProgressBar(1, 1, "Filament", "Created!"); break; case API_REQUEST_SPOOL_CREATE: Serial.println("Spool successfully created!"); createdSpoolId = doc["id"].as(); Serial.print("Created Spool ID: "); Serial.println(createdSpoolId); oledShowProgressBar(1, 1, "Spool", "Created!"); break; } } doc.clear(); } else if (httpCode == HTTP_CODE_CREATED) { Serial.println("Spoolman erfolgreich erstellt"); // Parse response for created resources JsonDocument doc; DeserializationError error = deserializeJson(doc, responsePayload); if (error) { Serial.print("Fehler beim Parsen der JSON-Antwort: "); Serial.println(error.c_str()); } else { switch(requestType){ case API_REQUEST_VENDOR_CREATE: Serial.println("Vendor successfully created!"); createdVendorId = doc["id"].as(); Serial.print("Created Vendor ID: "); Serial.println(createdVendorId); oledShowProgressBar(1, 1, "Vendor", "Created!"); break; case API_REQUEST_FILAMENT_CREATE: Serial.println("Filament successfully created!"); createdFilamentId = doc["id"].as(); Serial.print("Created Filament ID: "); Serial.println(createdFilamentId); oledShowProgressBar(1, 1, "Filament", "Created!"); break; case API_REQUEST_SPOOL_CREATE: Serial.println("Spool successfully created!"); createdSpoolId = doc["id"].as(); Serial.print("Created Spool ID: "); Serial.println(createdSpoolId); oledShowProgressBar(1, 1, "Spool", "Created!"); break; default: // Handle other create operations if needed break; } } doc.clear(); // Execute weight update if requested and tag update was successful if (triggerWeightUpdate && requestType == API_REQUEST_SPOOL_TAG_ID_UPDATE && weightValue > 10) { Serial.println("Executing weight update after successful tag update"); // Prepare weight update request String weightUrl = spoolmanUrl + apiUrl + "/spool/" + spoolIdForWeight + "/measure"; JsonDocument weightDoc; weightDoc["weight"] = weightValue; String weightPayload; serializeJson(weightDoc, weightPayload); Serial.print("Weight update URL: "); Serial.println(weightUrl); Serial.print("Weight update payload: "); Serial.println(weightPayload); // Execute weight update HTTPClient weightHttp; weightHttp.setReuse(false); weightHttp.setTimeout(HTTP_TIMEOUT_MS); weightHttp.begin(weightUrl); weightHttp.addHeader("Content-Type", "application/json"); int weightHttpCode = weightHttp.PUT(weightPayload); if (weightHttpCode == HTTP_CODE_OK) { Serial.println("Weight update successful"); String weightResponse = weightHttp.getString(); JsonDocument weightResponseDoc; DeserializationError weightError = deserializeJson(weightResponseDoc, weightResponse); if (!weightError) { remainingWeight = weightResponseDoc["remaining_weight"].as(); Serial.print("Updated weight: "); Serial.println(remainingWeight); if (!octoEnabled) { oledShowProgressBar(1, 1, "Spool Tag", ("Done: " + String(remainingWeight) + " g remain").c_str()); remainingWeight = 0; } else { sendOctoUpdate = true; } } weightResponseDoc.clear(); } else { Serial.print("Weight update failed with HTTP code: "); Serial.println(weightHttpCode); oledShowProgressBar(1, 1, "Failure!", "Weight update"); } weightHttp.end(); weightDoc.clear(); } } else { switch(requestType){ case API_REQUEST_SPOOL_WEIGHT_UPDATE: case API_REQUEST_SPOOL_LOCATION_UPDATE: case API_REQUEST_SPOOL_TAG_ID_UPDATE: oledShowProgressBar(1, 1, "Failure!", "Spoolman update"); break; case API_REQUEST_OCTO_SPOOL_UPDATE: oledShowProgressBar(1, 1, "Failure!", "Octoprint update"); break; case API_REQUEST_BAMBU_UPDATE: oledShowProgressBar(1, 1, "Failure!", "Bambu update"); break; case API_REQUEST_VENDOR_CHECK: oledShowProgressBar(1, 1, "Failure!", "Vendor check"); foundVendorId = 0; // Set to 0 to indicate error/not found break; case API_REQUEST_VENDOR_CREATE: oledShowProgressBar(1, 1, "Failure!", "Vendor create"); createdVendorId = 0; // Set to 0 to indicate error break; case API_REQUEST_FILAMENT_CHECK: oledShowProgressBar(1, 1, "Failure!", "Filament check"); foundFilamentId = 0; // Set to 0 to indicate error/not found break; case API_REQUEST_FILAMENT_CREATE: oledShowProgressBar(1, 1, "Failure!", "Filament create"); createdFilamentId = 0; // Set to 0 to indicate error break; case API_REQUEST_SPOOL_CREATE: oledShowProgressBar(1, 1, "Failure!", "Spool create"); createdSpoolId = 0; // Set to 0 to indicate error instead of hanging break; } Serial.println("Fehler beim Senden an Spoolman! HTTP Code: " + String(httpCode)); vTaskDelay(2000 / portTICK_PERIOD_MS); nfcReaderState = NFC_IDLE; // Reset NFC state to allow retry } vTaskDelay(50 / portTICK_PERIOD_MS); // Speicher freigeben delete params; HEAP_DEBUG_MESSAGE("sendToApi end"); spoolmanApiState = API_IDLE; vTaskDelete(NULL); } bool updateSpoolTagId(String uidString, const char* payload) { oledShowProgressBar(2, 3, "Write Tag", "Update Spoolman"); JsonDocument doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print("Fehler beim JSON-Parsing: "); Serial.println(error.c_str()); return false; } // Überprüfe, ob die erforderlichen Felder vorhanden sind if (!doc["sm_id"].is() || doc["sm_id"].as() == "") { Serial.println("Keine Spoolman-ID gefunden."); return false; } String spoolId = doc["sm_id"].as(); String spoolsUrl = spoolmanUrl + apiUrl + "/spool/" + spoolId; Serial.print("Update Spule mit URL: "); Serial.println(spoolsUrl); doc.clear(); // Update Payload erstellen JsonDocument updateDoc; updateDoc["extra"]["nfc_id"] = "\""+uidString+"\""; 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 false; } params->requestType = API_REQUEST_SPOOL_TAG_ID_UPDATE; params->httpType = "PATCH"; params->spoolsUrl = spoolsUrl; params->updatePayload = updatePayload; // Add weight update parameters for sequential execution params->triggerWeightUpdate = (weight > 10); params->spoolIdForWeight = spoolId; params->weightValue = weight; // Erstelle die Task mit erhöhter Stackgröße für zusätzliche HTTP-Anfrage BaseType_t result = xTaskCreate( sendToApi, // Task-Funktion "SendToApiTask", // Task-Name 8192, // Erhöhte Stackgröße für zusätzliche HTTP-Anfrage (void*)params, // Parameter 0, // Priorität apiTask // Task-Handle (nicht benötigt) ); updateDoc.clear(); // Update Spool weight now handled sequentially in sendToApi task // to prevent parallel API access issues return true; } uint8_t updateSpoolWeight(String spoolId, uint16_t weight) { HEAP_DEBUG_MESSAGE("updateSpoolWeight begin"); oledShowProgressBar(3, octoEnabled?5:4, "Spool Tag", "Spoolman update"); String spoolsUrl = spoolmanUrl + apiUrl + "/spool/" + spoolId + "/measure"; Serial.print("Update Spule mit URL: "); Serial.println(spoolsUrl); // Update Payload erstellen JsonDocument updateDoc; updateDoc["weight"] = weight; String updatePayload; serializeJson(updateDoc, updatePayload); Serial.print("Update Payload: "); Serial.println(updatePayload); SendToApiParams* params = new SendToApiParams(); if (params == nullptr) { // TBD: reset ESP instead of showing a message 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; // 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 (nicht benötigt) ); updateDoc.clear(); HEAP_DEBUG_MESSAGE("updateSpoolWeight end"); return 1; } uint8_t updateSpoolLocation(String spoolId, String location){ HEAP_DEBUG_MESSAGE("updateSpoolLocation begin"); oledShowProgressBar(3, octoEnabled?5:4, "Loc. Tag", "Spoolman update"); 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; 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!"); } updateDoc.clear(); HEAP_DEBUG_MESSAGE("updateSpoolLocation end"); return 1; } bool updateSpoolOcto(int spoolId) { oledShowProgressBar(4, octoEnabled?5:4, "Spool Tag", "Octoprint update"); String spoolsUrl = octoUrl + "/plugin/Spoolman/selectSpool"; Serial.print("Update Spule in Octoprint mit URL: "); Serial.println(spoolsUrl); JsonDocument updateDoc; updateDoc["spool_id"] = spoolId; updateDoc["tool"] = "tool0"; 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 false; } params->requestType = API_REQUEST_OCTO_SPOOL_UPDATE; params->httpType = "POST"; params->spoolsUrl = spoolsUrl; params->updatePayload = updatePayload; params->octoToken = octoToken; // 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 (nicht benötigt) ); updateDoc.clear(); return true; } bool updateSpoolBambuData(String payload) { JsonDocument doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print("Fehler beim JSON-Parsing: "); Serial.println(error.c_str()); return false; } String spoolsUrl = spoolmanUrl + apiUrl + "/filament/" + doc["filament_id"].as(); Serial.print("Update Spule mit URL: "); Serial.println(spoolsUrl); JsonDocument updateDoc; updateDoc["extra"]["bambu_setting_id"] = "\"" + doc["setting_id"].as() + "\""; updateDoc["extra"]["bambu_cali_id"] = "\"" + doc["cali_idx"].as() + "\""; updateDoc["extra"]["bambu_idx"] = "\"" + doc["tray_info_idx"].as() + "\""; updateDoc["extra"]["nozzle_temperature"] = "[" + doc["temp_min"].as() + "," + doc["temp_max"].as() + "]"; String updatePayload; serializeJson(updateDoc, updatePayload); doc.clear(); updateDoc.clear(); 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 false; } params->requestType = API_REQUEST_BAMBU_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 apiTask // Task-Handle (nicht benötigt) ); return true; } // #### Brand Filament uint16_t createVendor(const JsonDocument& payload) { oledShowProgressBar(2, 5, "New Brand", "Create new Vendor"); // Create new vendor in Spoolman database using task system // Note: Due to async nature, the ID will be stored in createdVendorId global variable // Note: This function assumes that the caller has already ensured API is IDLE createdVendorId = 65535; // Reset previous value String spoolsUrl = spoolmanUrl + apiUrl + "/vendor"; Serial.print("Create vendor with URL: "); Serial.println(spoolsUrl); // Create JSON payload for vendor creation JsonDocument vendorDoc; vendorDoc["name"] = payload["b"].as(); // Extract domain from URL if present, otherwise use brand name String externalId = ""; if (payload["u"].is()) { String url = payload["u"].as(); // Extract domain from URL (e.g., "https://www.blubb.de/f1234/?suche=irgendwas" -> "https://www.blubb.de") int protocolEnd = url.indexOf("://"); if (protocolEnd != -1) { int pathStart = url.indexOf("/", protocolEnd + 3); externalId = (pathStart != -1) ? url.substring(0, pathStart) : url; } else { externalId = url; // No protocol found, use as is } } else { externalId = payload["b"].as(); } vendorDoc["comment"] = externalId; String vendorPayload; serializeJson(vendorDoc, vendorPayload); Serial.print("Vendor Payload: "); Serial.println(vendorPayload); SendToApiParams* params = new SendToApiParams(); if (params == nullptr) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); vendorDoc.clear(); return 0; } params->requestType = API_REQUEST_VENDOR_CREATE; params->httpType = "POST"; params->spoolsUrl = spoolsUrl; params->updatePayload = vendorPayload; // Create task without additional API state check since caller ensures synchronization 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 (result != pdPASS) { Serial.println("Failed to create vendor task!"); delete params; vendorDoc.clear(); return 0; } vendorDoc.clear(); // Delay for Display Bar vTaskDelay(1000 / portTICK_PERIOD_MS); // Wait for task completion and return the created vendor ID // Note: createdVendorId will be set by sendToApi when response is received while(createdVendorId == 65535) { vTaskDelay(50 / portTICK_PERIOD_MS); } return createdVendorId; } uint16_t checkVendor(const JsonDocument& payload) { oledShowProgressBar(1, 5, "New Brand", "Check Vendor"); // Check if vendor exists using task system foundVendorId = 65535; // Reset to invalid value to detect when API response is received String vendorName = payload["b"].as(); vendorName.trim(); vendorName.replace(" ", "+"); String spoolsUrl = spoolmanUrl + apiUrl + "/vendor?name=" + vendorName; Serial.print("Check vendor with URL: "); Serial.println(spoolsUrl); SendToApiParams* params = new SendToApiParams(); if (params == nullptr) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); return 0; } params->requestType = API_REQUEST_VENDOR_CHECK; params->httpType = "GET"; params->spoolsUrl = spoolsUrl; params->updatePayload = ""; // Empty for GET request // Check if API is idle before creating task while (spoolmanApiState != API_IDLE) { vTaskDelay(100 / portTICK_PERIOD_MS); } // 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) ); // Wait until foundVendorId is updated by the API response (not 65535 anymore) while (foundVendorId == 65535) { vTaskDelay(50 / portTICK_PERIOD_MS); } // Check if vendor was found if (foundVendorId == 0) { Serial.println("Vendor not found, creating new vendor..."); uint16_t vendorId = createVendor(payload); if (vendorId == 0) { Serial.println("Failed to create vendor, returning 0."); return 0; // Failed to create vendor } else { Serial.println("Vendor created with ID: " + String(vendorId)); return vendorId; } } else { Serial.println("Vendor found: " + payload["b"].as()); Serial.print("Vendor ID: "); Serial.println(foundVendorId); return foundVendorId; } } uint16_t createFilament(uint16_t vendorId, const JsonDocument& payload) { oledShowProgressBar(4, 5, "New Brand", "Create Filament"); // Create new filament in Spoolman database using task system // Note: Due to async nature, the ID will be stored in createdFilamentId global variable // Note: This function assumes that the caller has already ensured API is IDLE createdFilamentId = 65535; // Reset previous value String spoolsUrl = spoolmanUrl + apiUrl + "/filament"; Serial.print("Create filament with URL: "); Serial.println(spoolsUrl); // Create JSON payload for filament creation JsonDocument filamentDoc; filamentDoc["name"] = payload["cn"].as(); filamentDoc["vendor_id"] = String(vendorId); filamentDoc["material"] = payload["t"].as(); filamentDoc["density"] = (payload["de"].is() && payload["de"].as().length() > 0) ? payload["de"].as() : "1.24"; filamentDoc["diameter"] = (payload["di"].is() && payload["di"].as().length() > 0) ? payload["di"].as() : "1.75"; filamentDoc["weight"] = String(weight); filamentDoc["spool_weight"] = payload["sw"].as(); filamentDoc["article_number"] = payload["an"].as(); filamentDoc["settings_extruder_temp"] = payload["et"].is() ? payload["et"].as() : ""; filamentDoc["settings_bed_temp"] = payload["bt"].is() ? payload["bt"].as() : ""; if (payload["an"].is()) { filamentDoc["external_id"] = payload["an"].as(); filamentDoc["comment"] = payload["u"].is() ? payload["u"].as() + payload["an"].as() : "automatically generated"; } else { filamentDoc["comment"] = payload["u"].is() ? payload["u"].as() : "automatically generated"; } if (payload["mc"].is()) { filamentDoc["multi_color_hexes"] = payload["mc"].as(); filamentDoc["multi_color_direction"] = payload["mcd"].is() ? payload["mcd"].as() : ""; } else { filamentDoc["color_hex"] = (payload["c"].is() && payload["c"].as().length() >= 6) ? payload["c"].as() : "FFFFFF"; } String filamentPayload; serializeJson(filamentDoc, filamentPayload); Serial.print("Filament Payload: "); Serial.println(filamentPayload); SendToApiParams* params = new SendToApiParams(); if (params == nullptr) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); filamentDoc.clear(); return 0; } params->requestType = API_REQUEST_FILAMENT_CREATE; params->httpType = "POST"; params->spoolsUrl = spoolsUrl; params->updatePayload = filamentPayload; // Create task without additional API state check since caller ensures synchronization 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 (result != pdPASS) { Serial.println("Failed to create filament task!"); delete params; filamentDoc.clear(); return 0; } filamentDoc.clear(); // Delay for Display Bar vTaskDelay(1000 / portTICK_PERIOD_MS); // Wait for task completion and return the created filament ID // Note: createdFilamentId will be set by sendToApi when response is received while(createdFilamentId == 65535) { vTaskDelay(50 / portTICK_PERIOD_MS); } return createdFilamentId; } uint16_t checkFilament(uint16_t vendorId, const JsonDocument& payload) { oledShowProgressBar(3, 5, "New Brand", "Check Filament"); // Check if filament exists using task system foundFilamentId = 65535; // Reset to invalid value to detect when API response is received String spoolsUrl = spoolmanUrl + apiUrl + "/filament?vendor.id=" + String(vendorId) + "&external_id=" + String(payload["artnr"].as()); Serial.print("Check filament with URL: "); Serial.println(spoolsUrl); SendToApiParams* params = new SendToApiParams(); if (params == nullptr) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); return 0; } params->requestType = API_REQUEST_FILAMENT_CHECK; params->httpType = "GET"; params->spoolsUrl = spoolsUrl; params->updatePayload = ""; // Empty for GET request // 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) ); // Wait until foundFilamentId is updated by the API response (not 65535 anymore) while (foundFilamentId == 65535) { vTaskDelay(50 / portTICK_PERIOD_MS); } // Check if filament was found if (foundFilamentId == 0) { Serial.println("Filament not found, creating new filament..."); uint16_t filamentId = createFilament(vendorId, payload); if (filamentId == 0) { Serial.println("Failed to create filament, returning 0."); return 0; // Failed to create filament } else { Serial.println("Filament created with ID: " + String(filamentId)); return filamentId; } } else { Serial.println("Filament found for vendor ID: " + String(vendorId)); Serial.print("Filament ID: "); Serial.println(foundFilamentId); return foundFilamentId; } } uint16_t createSpool(uint16_t vendorId, uint16_t filamentId, JsonDocument& payload, String uidString) { oledShowProgressBar(5, 5, "New Brand", "Create new Spool"); // Create new spool in Spoolman database using task system // Note: Due to async nature, the ID will be stored in createdSpoolId global variable // Note: This function assumes that the caller has already ensured API is IDLE createdSpoolId = 65535; // Reset to invalid value to detect when API response is received String spoolsUrl = spoolmanUrl + apiUrl + "/spool"; Serial.print("Create spool with URL: "); Serial.println(spoolsUrl); // Create JSON payload for spool creation JsonDocument spoolDoc; spoolDoc["filament_id"] = String(filamentId); spoolDoc["initial_weight"] = weight > 10 ? String(weight - payload["sw"].as()) : "1000"; spoolDoc["spool_weight"] = (payload["sw"].is() && payload["sw"].as().length() > 0) ? payload["sw"].as() : "180"; spoolDoc["remaining_weight"] = spoolDoc["initial_weight"]; spoolDoc["lot_nr"] = (payload["an"].is() && payload["an"].as().length() > 0) ? payload["an"].as() : ""; spoolDoc["comment"] = "automatically generated"; spoolDoc["extra"]["nfc_id"] = "\"" + uidString + "\""; String spoolPayload; serializeJson(spoolDoc, spoolPayload); Serial.print("Spool Payload: "); Serial.println(spoolPayload); spoolDoc.clear(); SendToApiParams* params = new SendToApiParams(); if (params == nullptr) { Serial.println("Fehler: Kann Speicher für Task-Parameter nicht allokieren."); spoolDoc.clear(); return 0; } params->requestType = API_REQUEST_SPOOL_CREATE; params->httpType = "POST"; params->spoolsUrl = spoolsUrl; params->updatePayload = spoolPayload; // Create task without additional API state check since caller ensures synchronization 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 (result != pdPASS) { Serial.println("Failed to create spool task!"); delete params; return 0; } // Wait for task completion and return the created spool ID // Note: createdSpoolId will be set by sendToApi when response is received while(createdSpoolId == 65535) { vTaskDelay(50 / portTICK_PERIOD_MS); } // Check if spool creation was successful if (createdSpoolId == 0) { Serial.println("ERROR: Spool creation failed"); nfcReaderState = NFC_IDLE; // Reset NFC state return 0; } // Write data to tag with startWriteJsonToTag // void startWriteJsonToTag(const bool isSpoolTag, const char* payload); // Create optimized JSON structure with sm_id at the beginning for fast-path detection JsonDocument optimizedPayload; optimizedPayload["sm_id"] = String(createdSpoolId); // Place sm_id first for fast scanning optimizedPayload["b"] = payload["b"].as(); optimizedPayload["cn"] = payload["an"].as(); String payloadString; serializeJson(optimizedPayload, payloadString); Serial.println("Optimized JSON with sm_id first:"); Serial.println(payloadString); optimizedPayload.clear(); nfcReaderState = NFC_IDLE; // Delay for Display Bar vTaskDelay(1000 / portTICK_PERIOD_MS); startWriteJsonToTag(true, payloadString.c_str()); return createdSpoolId; } bool createBrandFilament(JsonDocument& payload, String uidString) { uint16_t vendorId = checkVendor(payload); if (vendorId == 0) { Serial.println("ERROR: Failed to create/find vendor"); return false; } uint16_t filamentId = checkFilament(vendorId, payload); if (filamentId == 0) { Serial.println("ERROR: Failed to create/find filament"); return false; } uint16_t spoolId = createSpool(vendorId, filamentId, payload, uidString); if (spoolId == 0) { Serial.println("ERROR: Failed to create spool"); return false; } Serial.println("SUCCESS: Brand filament created with Spool ID: " + String(spoolId)); return true; } // #### Spoolman init bool checkSpoolmanExtraFields() { // 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 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 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/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(); 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 (!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]); 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; } //http.end(); } yield(); vTaskDelay(100 / portTICK_PERIOD_MS); } } doc.clear(); } } Serial.println("-------- ENDE Prüfe Felder --------"); Serial.println(); http.end(); spoolmanExtraFieldsChecked = true; return true; }else{ return true; } } bool checkSpoolmanInstance() { HTTPClient http; bool returnValue = false; // 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"; Serial.print("Checking spoolman instance: "); Serial.println(healthUrl); http.begin(healthUrl); int httpCode = http.GET(); 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(); 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; } doc.clear(); }else{ spoolmanConnected = false; } } else { spoolmanConnected = false; Serial.println("Error contacting spoolman instance! HTTP Code: " + String(httpCode)); } http.end(); spoolmanApiState = API_IDLE; } else { // If the check is skipped, return the previous status Serial.println("Skipping spoolman healthcheck, API is active."); returnValue = spoolmanConnected; } Serial.println("Healthcheck completed!"); return returnValue; } bool saveSpoolmanUrl(const String& url, bool octoOn, const String& octo_url, const String& octoTk) { Preferences preferences; preferences.begin(NVS_NAMESPACE_API, false); // false = readwrite preferences.putString(NVS_KEY_SPOOLMAN_URL, url); preferences.putBool(NVS_KEY_OCTOPRINT_ENABLED, octoOn); preferences.putString(NVS_KEY_OCTOPRINT_URL, octo_url); preferences.putString(NVS_KEY_OCTOPRINT_TOKEN, octoTk); preferences.end(); //TBD: This could be handled nicer in the future spoolmanExtraFieldsChecked = false; spoolmanUrl = url; octoEnabled = octoOn; octoUrl = octo_url; octoToken = octoTk; return checkSpoolmanInstance(); } String loadSpoolmanUrl() { Preferences preferences; preferences.begin(NVS_NAMESPACE_API, true); String spoolmanUrl = preferences.getString(NVS_KEY_SPOOLMAN_URL, ""); octoEnabled = preferences.getBool(NVS_KEY_OCTOPRINT_ENABLED, false); if(octoEnabled) { octoUrl = preferences.getString(NVS_KEY_OCTOPRINT_URL, ""); octoToken = preferences.getString(NVS_KEY_OCTOPRINT_TOKEN, ""); } preferences.end(); return spoolmanUrl; } bool initSpoolman() { oledShowProgressBar(3, 7, DISPLAY_BOOT_TEXT, "Spoolman init"); spoolmanUrl = loadSpoolmanUrl(); bool success = checkSpoolmanInstance(); if (!success) { Serial.println("Spoolman not available"); return false; } oledShowTopRow(); return true; }