#include "bambu.h" #include #include #include #include #include "bambu_cert.h" #include "website.h" #include "nfc.h" #include "commonFS.h" #include "esp_task_wdt.h" #include "config.h" #include "display.h" WiFiClient espClient; SSLClient sslClient(&espClient); PubSubClient client(sslClient); TaskHandle_t BambuMqttTask; String report_topic = ""; //String request_topic = ""; const char* bambu_username = "bblp"; const char* bambu_ip = nullptr; const char* bambu_accesscode = nullptr; const char* bambu_serialnr = nullptr; String g_bambu_ip = ""; String g_bambu_accesscode = ""; String g_bambu_serialnr = ""; bool bambu_connected = false; bool autoSendToBambu = false; int autoSetToBambuSpoolId = 0; // Globale Variablen für AMS-Daten int ams_count = 0; String amsJsonData; // Speichert das fertige JSON für WebSocket-Clients AMSData ams_data[MAX_AMS]; // Definition des Arrays; bool saveBambuCredentials(const String& ip, const String& serialnr, const String& accesscode, bool autoSend, const String& autoSendTime) { if (BambuMqttTask) { vTaskDelete(BambuMqttTask); } JsonDocument doc; doc["bambu_ip"] = ip; doc["bambu_accesscode"] = accesscode; doc["bambu_serialnr"] = serialnr; doc["autoSendToBambu"] = autoSend; doc["autoSendTime"] = (autoSendTime != "") ? autoSendTime.toInt() : autoSetBambuAmsCounter; if (!saveJsonValue("/bambu_credentials.json", doc)) { Serial.println("Fehler beim Speichern der Bambu-Credentials."); return false; } // Dynamische Speicherallokation für die globalen Pointer bambu_ip = ip.c_str(); bambu_accesscode = accesscode.c_str(); bambu_serialnr = serialnr.c_str(); autoSendToBambu = autoSend; autoSetBambuAmsCounter = autoSendTime.toInt(); vTaskDelay(100 / portTICK_PERIOD_MS); if (!setupMqtt()) return false; return true; } bool loadBambuCredentials() { JsonDocument doc; if (loadJsonValue("/bambu_credentials.json", doc) && doc["bambu_ip"].is()) { // Temporäre Strings für die Werte String ip = doc["bambu_ip"].as(); String code = doc["bambu_accesscode"].as(); String serial = doc["bambu_serialnr"].as(); g_bambu_ip = ip; g_bambu_accesscode = code; g_bambu_serialnr = serial; if (doc["autoSendToBambu"].is()) autoSendToBambu = doc["autoSendToBambu"].as(); if (doc["autoSendTime"].is()) autoSetBambuAmsCounter = doc["autoSendTime"].as(); ip.trim(); code.trim(); serial.trim(); // Dynamische Speicherallokation für die globalen Pointer bambu_ip = g_bambu_ip.c_str(); bambu_accesscode = g_bambu_accesscode.c_str(); bambu_serialnr = g_bambu_serialnr.c_str(); report_topic = "device/" + String(bambu_serialnr) + "/report"; //request_topic = "device/" + String(bambu_serialnr) + "/request"; return true; } Serial.println("Keine gültigen Bambu-Credentials gefunden."); return false; } struct FilamentResult { String key; String type; }; FilamentResult findFilamentIdx(String brand, String type) { // JSON-Dokument für die Filament-Daten erstellen JsonDocument doc; // Laden der own_filaments.json String ownFilament = ""; if (!loadJsonValue("/own_filaments.json", doc)) { Serial.println("Fehler beim Laden der eigenen Filament-Daten"); } else { // Durchsuche direkt nach dem Type als Schlüssel if (doc[type].is()) { ownFilament = doc[type].as(); } doc.clear(); } doc.clear(); // Laden der bambu_filaments.json if (!loadJsonValue("/bambu_filaments.json", doc)) { Serial.println("Fehler beim Laden der Filament-Daten"); return {"GFL99", "PLA"}; // Fallback auf Generic PLA } // Wenn eigener Typ if (ownFilament != "") { if (doc[ownFilament].is()) { return {ownFilament, doc[ownFilament].as()}; } } // 1. Erst versuchen wir die exakte Brand + Type Kombination zu finden String searchKey; if (brand == "Bambu" || brand == "Bambulab") { searchKey = "Bambu " + type; } else if (brand == "PolyLite") { searchKey = "PolyLite " + type; } else if (brand == "eSUN") { searchKey = "eSUN " + type; } else if (brand == "Overture") { searchKey = "Overture " + type; } else if (brand == "PolyTerra") { searchKey = "PolyTerra " + type; } // Durchsuche alle Einträge nach der Brand + Type Kombination for (JsonPair kv : doc.as()) { if (kv.value().as() == searchKey) { return {kv.key().c_str(), kv.value().as()}; } } // 2. Wenn nicht gefunden, zerlege den type String in Wörter und suche nach jedem Wort // Sammle alle vorhandenen Filamenttypen aus der JSON std::vector knownTypes; for (JsonPair kv : doc.as()) { String value = kv.value().as(); // Extrahiere den Typ ohne Markennamen if (value.indexOf(" ") != -1) { value = value.substring(value.indexOf(" ") + 1); } if (!value.isEmpty()) { knownTypes.push_back(value); } } // Zerlege den Input-Type in Wörter String typeStr = type; typeStr.trim(); // Durchsuche für jedes bekannte Filament, ob es im Input vorkommt for (const String& knownType : knownTypes) { if (typeStr.indexOf(knownType) != -1) { // Suche nach diesem Typ in der Original-JSON for (JsonPair kv : doc.as()) { String value = kv.value().as(); if (value.indexOf(knownType) != -1) { return {kv.key().c_str(), knownType}; } } } } // 3. Wenn immer noch nichts gefunden, gebe GFL99 zurück (Generic PLA) return {"GFL99", "PLA"}; } bool sendMqttMessage(const String& payload) { Serial.println("Sending MQTT message"); Serial.println(payload); if (client.publish(report_topic.c_str(), payload.c_str())) { return true; } return false; } bool setBambuSpool(String payload) { Serial.println("Spool settings in"); Serial.println(payload); // Parse the JSON JsonDocument doc; DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.print("Error parsing JSON: "); Serial.println(error.c_str()); return false; } int amsId = doc["amsId"]; int trayId = doc["trayId"]; String color = doc["color"].as(); color.toUpperCase(); int minTemp = doc["nozzle_temp_min"]; int maxTemp = doc["nozzle_temp_max"]; String type = doc["type"].as(); (type == "PLA+") ? type = "PLA" : type; String brand = doc["brand"].as(); String tray_info_idx = (doc["tray_info_idx"].as() != "-1") ? doc["tray_info_idx"].as() : ""; if (tray_info_idx == "") { if (brand != "" && type != "") { FilamentResult result = findFilamentIdx(brand, type); tray_info_idx = result.key; type = result.type; // Aktualisiere den type mit dem gefundenen Basistyp } } String setting_id = doc["bambu_setting_id"].as(); String cali_idx = doc["cali_idx"].as(); doc.clear(); doc["print"]["sequence_id"] = "0"; doc["print"]["command"] = "ams_filament_setting"; doc["print"]["ams_id"] = amsId < 200 ? amsId : 255; doc["print"]["tray_id"] = trayId < 200 ? trayId : 254; doc["print"]["tray_color"] = color.length() == 8 ? color : color+"FF"; doc["print"]["nozzle_temp_min"] = minTemp; doc["print"]["nozzle_temp_max"] = maxTemp; doc["print"]["tray_type"] = type; //doc["print"]["cali_idx"] = (cali_idx != "") ? cali_idx : ""; doc["print"]["tray_info_idx"] = tray_info_idx; doc["print"]["setting_id"] = setting_id; // Serialize the JSON String output; serializeJson(doc, output); if (sendMqttMessage(output)) { Serial.println("Spool successfully set"); } else { Serial.println("Failed to set spool"); return false; } doc.clear(); yield(); if (cali_idx != "") { yield(); doc["print"]["sequence_id"] = "0"; doc["print"]["command"] = "extrusion_cali_sel"; doc["print"]["filament_id"] = tray_info_idx; doc["print"]["nozzle_diameter"] = "0.4"; doc["print"]["cali_idx"] = cali_idx.toInt(); doc["print"]["tray_id"] = trayId < 200 ? trayId : 254; //doc["print"]["ams_id"] = amsId < 200 ? amsId : 255; // Serialize the JSON String output; serializeJson(doc, output); if (sendMqttMessage(output)) { Serial.println("Extrusion calibration successfully set"); } else { Serial.println("Failed to set extrusion calibration"); return false; } doc.clear(); yield(); } return true; } void autoSetSpool(int spoolId, uint8_t trayId) { // wenn neue spule erkannt und autoSetToBambu > 0 JsonDocument spoolInfo = fetchSingleSpoolInfo(spoolId); if (!spoolInfo.isNull()) { // AMS und TRAY id ergänzen spoolInfo["amsId"] = 0; spoolInfo["trayId"] = trayId; Serial.println("Auto set spool"); Serial.println(spoolInfo.as()); setBambuSpool(spoolInfo.as()); oledShowMessage("Spool set"); } // id wieder zurücksetzen damit abgeschlossen autoSetToBambuSpoolId = 0; } void updateAmsWsData(JsonDocument& doc, JsonArray& amsArray, int& ams_count, JsonObject& vtTray) { // Fortfahren mit der bestehenden Verarbeitung, da Änderungen gefunden wurden ams_count = amsArray.size(); for (int i = 0; i < ams_count && i < 16; i++) { JsonObject amsObj = amsArray[i]; JsonArray trayArray = amsObj["tray"].as(); ams_data[i].ams_id = i; // Setze die AMS-ID for (int j = 0; j < trayArray.size() && j < 4; j++) { // Annahme: Maximal 4 Trays pro AMS JsonObject trayObj = trayArray[j]; ams_data[i].trays[j].id = trayObj["id"].as(); ams_data[i].trays[j].tray_info_idx = trayObj["tray_info_idx"].as(); ams_data[i].trays[j].tray_type = trayObj["tray_type"].as(); ams_data[i].trays[j].tray_sub_brands = trayObj["tray_sub_brands"].as(); ams_data[i].trays[j].tray_color = trayObj["tray_color"].as(); ams_data[i].trays[j].nozzle_temp_min = trayObj["nozzle_temp_min"].as(); ams_data[i].trays[j].nozzle_temp_max = trayObj["nozzle_temp_max"].as(); if (trayObj["tray_type"].as() == "") ams_data[i].trays[j].setting_id = ""; ams_data[i].trays[j].cali_idx = trayObj["cali_idx"].as(); } } // Setze ams_count auf die Anzahl der normalen AMS ams_count = amsArray.size(); // Wenn externe Spule vorhanden, füge sie hinzu if (doc["print"]["vt_tray"].is()) { //JsonObject vtTray = doc["print"]["vt_tray"]; int extIdx = ams_count; // Index für externe Spule ams_data[extIdx].ams_id = 255; // Spezielle ID für externe Spule ams_data[extIdx].trays[0].id = 254; // Spezielle ID für externes Tray ams_data[extIdx].trays[0].tray_info_idx = vtTray["tray_info_idx"].as(); ams_data[extIdx].trays[0].tray_type = vtTray["tray_type"].as(); ams_data[extIdx].trays[0].tray_sub_brands = vtTray["tray_sub_brands"].as(); ams_data[extIdx].trays[0].tray_color = vtTray["tray_color"].as(); ams_data[extIdx].trays[0].nozzle_temp_min = vtTray["nozzle_temp_min"].as(); ams_data[extIdx].trays[0].nozzle_temp_max = vtTray["nozzle_temp_max"].as(); if (doc["print"]["vt_tray"]["tray_type"].as() != "") { //ams_data[extIdx].trays[0].setting_id = vtTray["setting_id"].as(); ams_data[extIdx].trays[0].cali_idx = vtTray["cali_idx"].as(); } else { ams_data[extIdx].trays[0].setting_id = ""; ams_data[extIdx].trays[0].cali_idx = ""; } ams_count++; // Erhöhe ams_count für die externe Spule } // Erstelle JSON für WebSocket-Clients JsonDocument wsDoc; JsonArray wsArray = wsDoc.to(); for (int i = 0; i < ams_count; i++) { JsonObject amsObj = wsArray.add(); amsObj["ams_id"] = ams_data[i].ams_id; JsonArray trays = amsObj["tray"].to(); int maxTrays = (ams_data[i].ams_id == 255) ? 1 : 4; for (int j = 0; j < maxTrays; j++) { JsonObject trayObj = trays.add(); trayObj["id"] = ams_data[i].trays[j].id; trayObj["tray_info_idx"] = ams_data[i].trays[j].tray_info_idx; trayObj["tray_type"] = ams_data[i].trays[j].tray_type; trayObj["tray_sub_brands"] = ams_data[i].trays[j].tray_sub_brands; trayObj["tray_color"] = ams_data[i].trays[j].tray_color; trayObj["nozzle_temp_min"] = ams_data[i].trays[j].nozzle_temp_min; trayObj["nozzle_temp_max"] = ams_data[i].trays[j].nozzle_temp_max; trayObj["setting_id"] = ams_data[i].trays[j].setting_id; trayObj["cali_idx"] = ams_data[i].trays[j].cali_idx; } } serializeJson(wsArray, amsJsonData); wsDoc.clear(); Serial.println("AMS data updated"); sendAmsData(nullptr); } // init void mqtt_callback(char* topic, byte* payload, unsigned int length) { String message; for (int i = 0; i < length; i++) { message += (char)payload[i]; } // JSON-Dokument parsen JsonDocument doc; DeserializationError error = deserializeJson(doc, message); message = ""; if (error) { Serial.print("Fehler beim Parsen des JSON: "); Serial.println(error.c_str()); return; } // Prüfen, ob "print->upgrade_state" und "print.ams.ams" existieren if (doc["print"]["upgrade_state"].is() || (doc["print"]["command"].is() && doc["print"]["command"] == "push_status")) { // Prüfen ob AMS-Daten vorhanden sind if (!doc["print"]["ams"].is() || !doc["print"]["ams"]["ams"].is()) { return; } JsonArray amsArray = doc["print"]["ams"]["ams"].as(); // Prüfe ob sich die AMS-Daten geändert haben bool hasChanges = false; // Vergleiche jedes AMS und seine Trays for (int i = 0; i < amsArray.size() && !hasChanges; i++) { JsonObject amsObj = amsArray[i]; int amsId = amsObj["id"].as(); JsonArray trayArray = amsObj["tray"].as(); // Finde das entsprechende AMS in unseren Daten int storedIndex = -1; for (int k = 0; k < ams_count; k++) { if (ams_data[k].ams_id == amsId) { storedIndex = k; break; } } if (storedIndex == -1) { hasChanges = true; break; } // Vergleiche die Trays for (int j = 0; j < trayArray.size() && j < 4 && !hasChanges; j++) { JsonObject trayObj = trayArray[j]; if (trayObj["tray_type"].as() == "") ams_data[storedIndex].trays[j].setting_id = ""; if (trayObj["setting_id"].isNull()) trayObj["setting_id"] = ""; if (trayObj["tray_info_idx"].as() != ams_data[storedIndex].trays[j].tray_info_idx || trayObj["tray_type"].as() != ams_data[storedIndex].trays[j].tray_type || trayObj["tray_color"].as() != ams_data[storedIndex].trays[j].tray_color || (trayObj["setting_id"].as() != "" && trayObj["setting_id"].as() != ams_data[storedIndex].trays[j].setting_id) || trayObj["cali_idx"].as() != ams_data[storedIndex].trays[j].cali_idx) { hasChanges = true; if (autoSendToBambu && autoSetToBambuSpoolId > 0 && hasChanges) { autoSetSpool(autoSetToBambuSpoolId, ams_data[storedIndex].trays[j].id); } break; } } } // Prüfe die externe Spule JsonObject vtTray = doc["print"]["vt_tray"]; if (doc["print"]["vt_tray"].is()) { for (int i = 0; i < ams_count; i++) { if (ams_data[i].ams_id == 255) { if (vtTray["tray_type"].as() == "") ams_data[i].trays[0].setting_id = ""; if (vtTray["setting_id"].isNull()) vtTray["setting_id"] = ""; if (vtTray["tray_info_idx"].as() != ams_data[i].trays[0].tray_info_idx || vtTray["tray_type"].as() != ams_data[i].trays[0].tray_type || vtTray["tray_color"].as() != ams_data[i].trays[0].tray_color || (vtTray["setting_id"].as() != "" && vtTray["setting_id"].as() != ams_data[i].trays[0].setting_id) || (vtTray["tray_type"].as() != "" && vtTray["cali_idx"].as() != ams_data[i].trays[0].cali_idx)) { hasChanges = true; if (autoSendToBambu && autoSetToBambuSpoolId > 0 && hasChanges) { autoSetSpool(autoSetToBambuSpoolId, 254); } } break; } } } if (!hasChanges) return; updateAmsWsData(doc, amsArray, ams_count, vtTray); } // Neue Bedingung für ams_filament_setting if (doc["print"]["command"] == "ams_filament_setting") { int amsId = doc["print"]["ams_id"].as(); int trayId = doc["print"]["tray_id"].as(); String settingId = (doc["print"]["setting_id"].is()) ? doc["print"]["setting_id"].as() : ""; // Finde das entsprechende AMS und Tray for (int i = 0; i < ams_count; i++) { if (ams_data[i].ams_id == amsId) { if (trayId == 254) { // Suche AMS mit ID 255 (externe Spule) for (int j = 0; j < ams_count; j++) { if (ams_data[j].ams_id == 255) { ams_data[j].trays[0].setting_id = settingId; break; } } } else { ams_data[i].trays[trayId].setting_id = settingId; } // Sende an WebSocket Clients Serial.println("Filament setting updated"); sendAmsData(nullptr); break; } } } } void reconnect() { // Loop until we're reconnected uint8_t retries = 0; while (!client.connected()) { Serial.println("Attempting MQTT re/connection..."); bambu_connected = false; oledShowTopRow(); // Attempt to connect if (client.connect(bambu_serialnr, bambu_username, bambu_accesscode)) { Serial.println("MQTT re/connected"); client.subscribe(report_topic.c_str()); bambu_connected = true; oledShowTopRow(); } else { Serial.print("failed, rc="); Serial.print(client.state()); Serial.println(" try again in 5 seconds"); bambu_connected = false; oledShowTopRow(); yield(); vTaskDelay(5000 / portTICK_PERIOD_MS); if (retries > 5) { Serial.println("Disable Bambu MQTT Task after 5 retries"); //vTaskSuspend(BambuMqttTask); vTaskDelete(BambuMqttTask); break; } retries++; } } } void mqtt_loop(void * parameter) { Serial.println("Bambu MQTT Task gestartet"); for(;;) { if (pauseBambuMqttTask) { vTaskDelay(10000); } if (!client.connected()) { reconnect(); yield(); esp_task_wdt_reset(); vTaskDelay(100); } client.loop(); yield(); esp_task_wdt_reset(); vTaskDelay(100); } } bool setupMqtt() { // Wenn Bambu Daten vorhanden bool success = loadBambuCredentials(); vTaskDelay(100 / portTICK_PERIOD_MS); if (!success) { Serial.println("Failed to load Bambu credentials"); oledShowMessage("Bambu Credentials Missing"); vTaskDelay(2000 / portTICK_PERIOD_MS); return false; } if (success && bambu_ip != "" && bambu_accesscode != "" && bambu_serialnr != "") { sslClient.setCACert(root_ca); sslClient.setInsecure(); client.setServer(bambu_ip, 8883); // Verbinden mit dem MQTT-Server bool connected = true; if (client.connect(bambu_serialnr, bambu_username, bambu_accesscode)) { client.setCallback(mqtt_callback); client.setBufferSize(5120); // Optional: Topic abonnieren client.subscribe(report_topic.c_str()); //client.subscribe(request_topic.c_str()); Serial.println("MQTT-Client initialisiert"); oledShowMessage("Bambu Connected"); bambu_connected = true; oledShowTopRow(); xTaskCreatePinnedToCore( mqtt_loop, /* Function to implement the task */ "BambuMqtt", /* Name of the task */ 8192, /* Stack size in words */ NULL, /* Task input parameter */ mqttTaskPrio, /* Priority of the task */ &BambuMqttTask, /* Task handle. */ mqttTaskCore); /* Core where the task should run */ } else { Serial.println("Fehler: Konnte sich nicht beim MQTT-Server anmelden"); oledShowMessage("Bambu Connection Failed"); vTaskDelay(2000 / portTICK_PERIOD_MS); connected = false; oledShowTopRow(); } if (!connected) return false; } else { Serial.println("Fehler: Keine MQTT-Daten vorhanden"); oledShowMessage("Bambu Credentials Missing"); oledShowTopRow(); vTaskDelay(2000 / portTICK_PERIOD_MS); return false; } return true; } void bambu_restart() { if (BambuMqttTask) { vTaskDelete(BambuMqttTask); } setupMqtt(); }