#include "EggDuino.h" #include #include namespace { const char *kConfigPath = "/config.json"; const size_t kConfigJsonCapacity = 4096; WebServer server(80); bool configStoreReady = false; 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
)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 handleNotFound() { if (server.uri().startsWith("/api/")) { server.send(404, "text/plain", "API endpoint not found"); return; } handleRoot(); } } // 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", "servoPin", &g_iServoPin, "Servo Pin", kDefaultServoPin, "", 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(); } StaticJsonDocument doc; 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; } StaticJsonDocument doc; 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() { StaticJsonDocument doc; 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) { StaticJsonDocument doc; 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; if (!g_sWifiSsid.isEmpty()) { WiFi.mode(WIFI_STA); WiFi.begin(g_sWifiSsid.c_str(), g_sWifiPassword.c_str()); const unsigned long connectStart = millis(); const unsigned long connectTimeoutMs = 10000; while (WiFi.status() != WL_CONNECTED && millis() - connectStart < connectTimeoutMs) { delay(250); } staConnected = (WiFi.status() == WL_CONNECTED); if (staConnected) { Serial.println(String("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.onNotFound(handleNotFound); server.begin(); startWifiProtocolInterface(); } if (!staConnected) { WiFi.mode(WIFI_AP); if (WiFi.softAP("EggDuino")) { Serial.println(String("AP aktiv: EggDuino / http://") + WiFi.softAPIP().toString()); } 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.onNotFound(handleNotFound); server.begin(); } void handleWebInterface() { server.handleClient(); }