#include "EggDuino.h" #include #include #include namespace { const char *kConfigPath = "/config.json"; const size_t kConfigJsonCapacity = 4096; const byte kDnsPort = 53; const unsigned long kWifiReconnectIntervalMs = 10000; using ConfigJsonDocument = StaticJsonDocument; WebServer server(80); DNSServer dnsServer; bool configStoreReady = false; bool apModeActive = false; bool staReconnectEnabled = false; bool staConnectionKnown = false; unsigned long lastStaReconnectAttemptMs = 0; ConfigJsonDocument g_configJsonScratch; ConfigJsonDocument &configJsonScratch() { // Keep the large config JSON buffer off ESP32 Arduino's 8 KB loopTask stack. g_configJsonScratch.clear(); return g_configJsonScratch; } void redirectToRoot() { server.sendHeader("Location", String("http://") + WiFi.softAPIP().toString() + "/", true); server.send(302, "text/plain", ""); } bool isIntType(const ConfigParameter ¶m) { return strcmp(param.type, "int") == 0; } int *asIntPtr(ConfigParameter ¶m) { return static_cast(param.value); } String *asStringPtr(ConfigParameter ¶m) { return static_cast(param.value); } ConfigParameter *findParameter(const String &key) { for (size_t i = 0; i < configParameterCount; ++i) { if (key.equals(configParameters[i].key)) { return &configParameters[i]; } } return nullptr; } void applyDefaults() { for (size_t i = 0; i < configParameterCount; ++i) { if (isIntType(configParameters[i])) { *asIntPtr(configParameters[i]) = configParameters[i].defaultValue; } else { const char *fallback = configParameters[i].defaultText == nullptr ? "" : configParameters[i].defaultText; *asStringPtr(configParameters[i]) = String(fallback); } } } void handleRoot() { static const char kPage[] PROGMEM = R"HTML( EggDuino Konfiguration

EggDuino Parameter

Logs
Incoming print log herunterladen
)HTML"; server.send(200, "text/html", kPage); } void handleGetConfig() { if (!configStoreReady && !initConfigStore()) { server.send(500, "text/plain", "Config storage not available"); return; } server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "0"); server.send(200, "application/json", buildConfigJson()); } void handlePostConfig() { if (!configStoreReady && !initConfigStore()) { server.send(500, "text/plain", "Config storage not available"); return; } if (!server.hasArg("plain")) { server.send(400, "text/plain", "Missing JSON body"); return; } String error; if (!applyConfigJson(server.arg("plain"), error)) { Log(String("Config JSON fehlerhaft: ") + error); server.send(400, "text/plain", error); return; } if (!saveConfigToFile()) { Log("Config konnte nicht gespeichert werden"); server.send(500, "text/plain", "Could not save config"); return; } server.send(200, "application/json", buildConfigJson()); } void handleGetLogs() { uint32_t since = 0; if (server.hasArg("since")) { since = static_cast(server.arg("since").toInt()); } server.send(200, "application/json", buildLogsJson(since)); } void handleDownloadPrintLog() { server.sendHeader("Cache-Control", "no-store, no-cache, must-revalidate"); server.sendHeader("Pragma", "no-cache"); server.sendHeader("Expires", "0"); server.sendHeader("Content-Disposition", "attachment; filename=\"incoming-print-log.txt\""); server.send(200, "text/plain; charset=utf-8", buildIncomingPrintLogText()); } void handleNotFound() { if (apModeActive) { redirectToRoot(); return; } if (server.uri().startsWith("/api/")) { server.send(404, "text/plain", "API endpoint not found"); return; } handleRoot(); } void handleWifiReconnect() { if (!staReconnectEnabled || apModeActive) { return; } const wl_status_t wifiStatus = WiFi.status(); if (wifiStatus == WL_CONNECTED) { if (!staConnectionKnown) { staConnectionKnown = true; Log(String("WLAN verbunden: ") + WiFi.localIP().toString()); } return; } if (staConnectionKnown) { staConnectionKnown = false; Log("WLAN Verbindung verloren"); } const unsigned long now = millis(); if (now - lastStaReconnectAttemptMs < kWifiReconnectIntervalMs) { return; } lastStaReconnectAttemptMs = now; if (!WiFi.reconnect()) { WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str()); } Log("WLAN Reconnect versucht"); } } // namespace ConfigParameter configParameters[] = { {"int", "penUpPos", &g_iPenUpPos, "Pen Up Position", 40, "", false}, {"int", "penDownPos", &g_iPenDownPos, "Pen Down Position", 10, "", false}, {"int", "rotStepPin", &g_iRotStepPin, "Rotational Stepper Step Pin", kDefaultRotStepPin, "", false}, {"int", "rotDirPin", &g_iRotDirPin, "Rotational Stepper Direction Pin", kDefaultRotDirPin, "", false}, {"int", "rotEnablePin", &g_iRotEnablePin, "Rotational Stepper Enable Pin", kDefaultRotEnablePin, "", false}, {"int", "penStepPin", &g_iPenStepPin, "Pen Stepper Step Pin", kDefaultPenStepPin, "", false}, {"int", "penDirPin", &g_iPenDirPin, "Pen Stepper Direction Pin", kDefaultPenDirPin, "", false}, {"int", "penEnablePin", &g_iPenEnablePin, "Pen Stepper Enable Pin", kDefaultPenEnablePin, "", false}, {"int", "rotMicrostep", &g_iRotMicrostep, "Rotational Stepper Microsteps", kDefaultRotMicrostep, "", false}, {"int", "penMicrostep", &g_iPenMicrostep, "Pen Stepper Microsteps", kDefaultPenMicrostep, "", false}, {"int", "iMaxAcclSpeed", &g_iMaxAcclSpeed, "max. Stepper acceleration speed", 10000, "", false}, {"int", "servoPin", &g_iServoPin, "Servo Pin", kDefaultServoPin, "", false}, {"text", "Name", &g_sHostname, "Name", 0, "EggDuino", false}, {"text", "wifiSsid", &g_sWifiSsid, "WLAN SSID", 0, "", false}, {"password", "wifiPassword", &g_sWifiPassword, "WLAN Passwort", 0, "", true}, }; const size_t configParameterCount = sizeof(configParameters) / sizeof(configParameters[0]); bool initConfigStore() { if (!SPIFFS.begin(true)) { configStoreReady = false; return false; } configStoreReady = loadConfigFromFile(); return configStoreReady; } bool loadConfigFromFile() { applyDefaults(); File file = SPIFFS.open(kConfigPath, "r"); if (!file) { Log("config.json fehlt, defaults werden gespeichert"); return saveConfigToFile(); } ConfigJsonDocument &doc = configJsonScratch(); DeserializationError err = deserializeJson(doc, file); file.close(); if (err) { Log(String("config.json ist ungueltig (") + err.c_str() + "), defaults werden gespeichert"); return saveConfigToFile(); } JsonArray params = doc["parameters"].as(); for (JsonObject item : params) { const char *key = item["key"]; if (key == nullptr) { continue; } ConfigParameter *param = findParameter(String(key)); if (param == nullptr) { continue; } if (item.containsKey("value")) { if (isIntType(*param)) { *asIntPtr(*param) = item["value"].as(); } else { *asStringPtr(*param) = item["value"].as(); } } if (item.containsKey("description")) { param->description = item["description"].as(); } } updateStepCorrectionFactors(); Log(String("Config geladen: penUpPos=") + g_iPenUpPos + ", penDownPos=" + g_iPenDownPos); return true; } bool saveConfigToFile() { File file = SPIFFS.open(kConfigPath, "w"); if (!file) { Log("SPIFFS open write failed"); return false; } ConfigJsonDocument &doc = configJsonScratch(); JsonArray params = doc.createNestedArray("parameters"); for (size_t i = 0; i < configParameterCount; ++i) { JsonObject item = params.createNestedObject(); item["key"] = configParameters[i].key; item["type"] = configParameters[i].type; item["secret"] = configParameters[i].secret; if (isIntType(configParameters[i])) { item["value"] = *asIntPtr(configParameters[i]); } else { item["value"] = *asStringPtr(configParameters[i]); } item["description"] = configParameters[i].description; } if (doc.overflowed()) { Log("Config JSON Dokument zu klein beim Speichern"); file.close(); return false; } bool ok = serializeJsonPretty(doc, file) > 0; file.flush(); file.close(); if (!ok) { Log("serializeJsonPretty failed"); } return ok; } String buildConfigJson() { ConfigJsonDocument &doc = configJsonScratch(); JsonArray params = doc.createNestedArray("parameters"); for (size_t i = 0; i < configParameterCount; ++i) { JsonObject item = params.createNestedObject(); item["key"] = configParameters[i].key; item["type"] = configParameters[i].type; item["secret"] = configParameters[i].secret; if (isIntType(configParameters[i])) { item["value"] = *asIntPtr(configParameters[i]); } else if (configParameters[i].secret) { item["value"] = ""; item["hasValue"] = !asStringPtr(configParameters[i])->isEmpty(); } else { item["value"] = *asStringPtr(configParameters[i]); } item["description"] = configParameters[i].description; } if (doc.overflowed()) { Log("Config JSON Dokument zu klein beim Lesen"); return String("{\"parameters\":[]}"); } String output; serializeJson(doc, output); return output; } bool applyConfigJson(const String &payload, String &errorMessage) { ConfigJsonDocument &doc = configJsonScratch(); DeserializationError err = deserializeJson(doc, payload); if (err) { errorMessage = "Invalid JSON payload"; return false; } JsonArray params = doc["parameters"].as(); if (params.isNull()) { errorMessage = "JSON must contain 'parameters' array"; return false; } for (JsonObject item : params) { const char *key = item["key"]; if (key == nullptr) { continue; } ConfigParameter *param = findParameter(String(key)); if (param == nullptr) { continue; } if (!item.containsKey("value")) { continue; } if (isIntType(*param)) { *asIntPtr(*param) = item["value"].as(); } else { *asStringPtr(*param) = item["value"].as(); } if (item.containsKey("description")) { param->description = item["description"].as(); } } updateStepCorrectionFactors(); return true; } void startWebInterface() { initConfigStore(); bool staConnected = false; apModeActive = false; staReconnectEnabled = !g_sWifiSsid.isEmpty(); staConnectionKnown = false; lastStaReconnectAttemptMs = millis(); dnsServer.stop(); char deviceName[32] = {0}; buildDeviceName(deviceName, sizeof(deviceName)); String hostName = String(deviceName); if (hostName.isEmpty()) { hostName = "EggBot_UNKNOWN"; } g_sHostname = hostName; if (!g_sWifiSsid.isEmpty()) { WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.setHostname(hostName.c_str()); WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str()); const unsigned long connectStart = millis(); const unsigned long connectTimeoutMs = 20000; Serial.println(String("Trying to connect to Wifi: ") + g_sWifiSsid.c_str() + g_sWifiPassword.c_str()); while (WiFi.status() != WL_CONNECTED && millis() - connectStart < connectTimeoutMs) { delay(250); } staConnected = (WiFi.status() == WL_CONNECTED); staConnectionKnown = staConnected; if (staConnected) { Serial.println(String("Connected: http://") + WiFi.localIP().toString()); } else { Serial.println("WLAN Verbindung fehlgeschlagen"); } server.on("/", HTTP_GET, handleRoot); server.on("/api/config", HTTP_GET, handleGetConfig); server.on("/api/config", HTTP_POST, handlePostConfig); server.on("/api/logs", HTTP_GET, handleGetLogs); server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog); server.onNotFound(handleNotFound); server.begin(); startWifiProtocolInterface(); } if (!staConnected) { WiFi.mode(WIFI_AP); #ifdef ESP32 WiFi.softAPsetHostname(hostName.c_str()); #endif if (WiFi.softAP(hostName.c_str())) { apModeActive = true; dnsServer.start(kDnsPort, "*", WiFi.softAPIP()); Serial.println(String("AP aktiv: ") + hostName + " / http://" + WiFi.softAPIP().toString() + " (Name: " + hostName + ")"); } else { Serial.println("AP konnte nicht gestartet werden"); } } server.on("/", HTTP_GET, handleRoot); server.on("/api/config", HTTP_GET, handleGetConfig); server.on("/api/config", HTTP_POST, handlePostConfig); server.on("/api/logs", HTTP_GET, handleGetLogs); server.on("/api/print-log.txt", HTTP_GET, handleDownloadPrintLog); server.on("/generate_204", HTTP_GET, redirectToRoot); server.on("/gen_204", HTTP_GET, redirectToRoot); server.on("/hotspot-detect.html", HTTP_GET, redirectToRoot); server.on("/connecttest.txt", HTTP_GET, redirectToRoot); server.on("/ncsi.txt", HTTP_GET, redirectToRoot); server.on("/fwlink", HTTP_GET, redirectToRoot); server.onNotFound(handleNotFound); server.begin(); } void handleWebInterface() { handleWifiReconnect(); if (apModeActive) { dnsServer.processNextRequest(); } server.handleClient(); }