#include "my_wifi.h" #include #include #include #include #include #include "esp_log.h" #include #include #include #include "common/fileSystem.h" #include "global.h" #include "my_buzzer.h" #include #include #include "jsonconstrain.h" #include "AppUpgrade.h" static const char *tag = "WIFI"; volatile bool WifiClientConnected = false; AsyncWebServer webServer(80); AsyncWebSocket wsUpgradeProgress("/upgrade-progress"); //AsyncEventSource eventUpgradeProgress("/upgrade-progress"); //DNSServer *dnsServer; //#define DNS_PORT 53 String client_ssid; String client_pass; String ap_ssid; String ap_pass; String mDnsName; String HostName; IPAddress local_IP(192,168,10,1); IPAddress gateway(192,168,10,1); IPAddress subnet(255,255,255,0); // for file manager page String filesDropdownOptions((char*)0); String dirDropdownOptions((char*)0); String savePath((char*)0); // needed for storing file when editing a file String savePathInput((char*)0); const char* http_username = "admin"; const char* http_password = "admin"; const char* param_delete_path = "delete-path"; const char* param_edit_path = "edit-path"; const char* param_dir_pad = "dir-path"; const char* param_edit_textarea = "edit-textarea"; const char* param_save_path = "save-path"; String allowedExtensionsForEdit = "txt, h, htm, html, css, cpp, js, json, ini, cfg"; volatile bool scanInProgress = false; static String networkList = ""; int networkCount = 0; volatile int scanStatus = 0; // 0=none, 1=scanning, 2=complete, -1=error String scanResults = ""; // Store scan results globally TaskHandle_t Wifi_Task_Handle; void Wifi_Task(void* parameter) { for(;;) { static String last_ssid = ""; if (!WifiClientConnected || last_ssid != client_ssid) { WifiClientConnected = false; ESP_LOGD(tag, "Wifi trying to connect to: %s", client_ssid.c_str()); WiFi.disconnect(); vTaskDelay(1000); WiFi.setAutoReconnect(false); WiFi.begin(client_ssid.c_str(), client_pass.c_str()); vTaskDelay(1000); // wait up to 10 iterations for connection int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 10) { wl_status_t status = WiFi.status(); switch (status) { case WL_NO_SSID_AVAIL: ESP_LOGW(tag, "No AP with SSID %s found", client_ssid.c_str()); break; case WL_CONNECT_FAILED: ESP_LOGW(tag, "Connection failed (wrong password?)"); break; case WL_DISCONNECTED: ESP_LOGD(tag, "Not connected yet... (attempt %d/10)", attempts + 1); break; default: ESP_LOGD(tag, "Status: %d", status); break; } vTaskDelay(2000); attempts++; } if(WiFi.status() == WL_CONNECTED){ ESP_LOGD(tag, "Connected to %s", client_ssid.c_str()); //mDnsName = WiFi.hostname(); WifiClientConnected = true; WiFi.setAutoReconnect(true); last_ssid = client_ssid; // Save the WiFi settings Wifi_Save_Credentials("/system/wifi.json"); } else { ESP_LOGW(tag, "Failed to connect to %s", client_ssid.c_str()); } } vTaskSuspend(Wifi_Task_Handle); } vTaskDelete(NULL); } void Wifi_Save_Credentials(String path) { // Load existing JSON JsonDocument doc; File readFile = LittleFS.open(path, "r"); if (readFile) { DeserializationError error = deserializeJson(doc, readFile); readFile.close(); if (error) { ESP_LOGE(tag, "Failed to parse existing JSON"); return; } } // Update or create wifi-client section JsonObject wifiClient = doc["wifi-client"].to(); wifiClient["ssid"] = client_ssid; wifiClient["pass"] = client_pass; // Save updated JSON File writeFile = LittleFS.open(path, "w"); if (!writeFile) { ESP_LOGE(tag, "Error opening %s for writing", path.c_str()); return; } // Serialize JSON with pretty formatting if (serializeJsonPretty(doc, writeFile) == 0) { ESP_LOGE(tag, "Failed to write JSON to file"); } writeFile.close(); } void Wifi_Init() { // Initialize LittleFS if (!LittleFS.begin(true)) { ESP_LOGE(tag, "LittleFS mount failed"); return; } // Set Wi-Fi task to run on Core 1 esp_wifi_set_ps(WIFI_PS_NONE); // Disable power save mode for better responsiveness // Set WiFi to AP+STA mode WiFi.mode(WIFI_MODE_APSTA); Wifi_Scan_for_Networks(); // Configure and start AP WiFi.softAPConfig(local_IP, gateway, subnet); if (!WiFi.softAP(ap_ssid, ap_pass)) { ESP_LOGE(tag, "AP start failed"); return; } // Add CORS headers DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, PUT"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type"); Setup_WebServer_Handlers(webServer); WiFi.onEvent(onWiFiEvent); WiFi.setHostname(mDnsName.c_str()); webServer.begin(); ESP_LOGD(tag, "AP started with IP: %s", WiFi.softAPIP().toString().c_str()); // Start the WiFi task xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*6, NULL, 1, &Wifi_Task_Handle, 0); } void Wifi_Load_Settings(String path){ // Load WiFi settings File file = LittleFS.open(path, "r"); if (!file) { ESP_LOGE(tag, "Error opening %s", path.c_str()); return; } JsonDocument doc; DeserializationError error = deserializeJson(doc, file); file.close(); if (error) { ESP_LOGE(tag, "Failed to deserialize %s", path.c_str()); return; } JsonObject wifiJson = doc.as(); if (wifiJson.isNull()) { ESP_LOGE(tag, "%s is empty", path.c_str()); return; } // Load AP settings JsonObject apJson = wifiJson["wifi-ap"]; if (!apJson.isNull()) { ap_ssid = jsonConstrainString(tag, apJson, "ssid", "ATA-AP"); ap_pass = jsonConstrainString(tag, apJson, "pass", "12345678"); local_IP.fromString(jsonConstrainString(tag, apJson, "ip", "192.168.10.1")); gateway.fromString(jsonConstrainString(tag, apJson, "gateway", "192.168.10.1")); subnet.fromString(jsonConstrainString(tag, apJson, "subnet", "255.255.255.0")); } // Load Client settings JsonObject clientJson = wifiJson["wifi-client"]; if (!apJson.isNull()) { client_ssid = jsonConstrainString(tag, clientJson, "ssid", "none"); client_pass = jsonConstrainString(tag, clientJson, "pass", "12345678"); } } void Wifi_Scan_for_Networks(){ // Start a scan for available networks WiFi.scanNetworks(false, false); while (WiFi.scanComplete() == WIFI_SCAN_RUNNING) { vTaskDelay(100); // Wait for scan to complete } networkCount = WiFi.scanComplete(); if (networkCount >= 0) { JsonDocument doc; JsonArray networks = doc["networks"].to(); for (int i = 0; i < networkCount; i++) { auto network = networks.add(); network["ssid"] = WiFi.SSID(i); network["rssi"] = WiFi.RSSI(i); network["encryption"] = WiFi.encryptionType(i) != WIFI_AUTH_OPEN; } String jsonString; serializeJson(doc, jsonString); networkList = jsonString; WiFi.scanDelete(); } else { ESP_LOGE(tag, "WiFi scan failed"); } } void Setup_WebServer_Handlers(AsyncWebServer& server){ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(LittleFS, "/www/index.html", "text/html"); }); server.on("/home", HTTP_GET, [](AsyncWebServerRequest *request){ sendHtmlFile("/www/home.html", request, HomeHtmlProcessor); }); server.on("/setup", HTTP_GET, [](AsyncWebServerRequest *request){ if(!request->authenticate(http_username, http_password)){ return request->requestAuthentication(); } //sendHtmlFile("/www/setup.html", request, htmlProcessor); }); server.on("/about", HTTP_GET, [](AsyncWebServerRequest *request){ sendHtmlFile("/www/about.html", request, fileManagerHtmlProcessor); //request->send(LittleFS, "/www/about.html", "text/html"); }); // Wifi related handlers server.on("/wifi/connect", HTTP_POST, [](AsyncWebServerRequest *request){ if (request->hasParam("ssid", false, false) && request->hasParam("pass", false, false)) { client_ssid = request->getParam("ssid", false, false)->value().c_str(); if (Wifi_Task_Handle != NULL && eTaskGetState(Wifi_Task_Handle) == eSuspended) { ESP_LOGD(tag, "Resuming WiFi task"); WifiClientConnected = false; vTaskResume(Wifi_Task_Handle); } else { ESP_LOGE(tag, "Failed to resume WiFi task: invalid handle or task not suspended"); } vTaskResume(Wifi_Task_Handle); request->send(200, "application/json", "{\"status\":\"connecting\"}"); } else { ESP_LOGE(tag, "Missing ssid or pass parameter"); request->send(400, "application/json", "{\"error\":\"Missing ssid or pass parameter\"}"); } }); server.on("/wifi/status", HTTP_GET, [](AsyncWebServerRequest *request){ String jsonStr = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\",\"ip\":\"" + WiFi.localIP().toString() + "\"}"; request->send(200, "application/json", jsonStr); }); server.on("/wifi/scans", HTTP_GET, [](AsyncWebServerRequest *request){ if(networkCount <= 0) { request->send(400, "application/json", "{\"error\":\"No scan results\"}"); return; } request->send(200, "application/json", networkList); }); server.on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request){ if(WiFi.getMode() == WIFI_MODE_APSTA){ // TODO Disable navigation bar } //sendHtmlFile("/www/wifi.html", request, htmlProcessor); request->send(LittleFS, "/www/wifi.html", "text/html"); }); // File Manager related handlers server.on("/files/upload", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, handleFilesUpload_OnBody); server.on("/files/download", HTTP_GET, [](AsyncWebServerRequest *request){ if (!request->hasParam("file")) { ESP_LOGE(tag, "Missing file parameter"); request->send(400, "text/plain", "Missing file parameter"); return; } try { String filename = uriDecode(request->getParam("file")->value()); ESP_LOGD(tag, "Download request for: %s", filename.c_str()); request->send(LittleFS, filename, "application/octet-stream"); } catch (const std::exception& e) { ESP_LOGE(tag, "Download failed: %s", e.what()); request->send(404, "text/plain", "File not found!"); } }); server.on("/files/delete", HTTP_GET, [](AsyncWebServerRequest *request) { static const char* tag = "DeleteHandler"; // Authentication check if (!request->authenticate(http_username, http_password)) { ESP_LOGW(tag, "Authentication failed for delete request"); return request->requestAuthentication(); } // Parameter validation if (!request->hasParam(param_delete_path)) { ESP_LOGE(tag, "Missing delete path parameter"); request->send(400, "text/plain", "Missing file path"); return; } // Get and validate filename String filename = uriDecode(request->getParam(param_delete_path)->value()); if (filename == "choose") { request->redirect("/files"); return; } // Ensure path starts with / if (!filename.startsWith("/")) { filename = "/" + filename; } try { if (LittleFS.remove(filename.c_str())) { ESP_LOGI(tag, "Successfully deleted file: %s", filename.c_str()); } else { ESP_LOGE(tag, "Failed to delete file: %s", filename.c_str()); request->send(500, "text/plain", "Delete failed"); return; } } catch (const std::exception& e) { ESP_LOGE(tag, "Exception during delete: %s", e.what()); request->send(500, "text/plain", "Delete failed"); return; } request->redirect("/files"); }); server.on("/files/edit", HTTP_GET, [](AsyncWebServerRequest *request) { static const char* tag = "EditHandler"; // Authentication check if (!request->authenticate(http_username, http_password)) { ESP_LOGW(tag, "Authentication failed"); return request->requestAuthentication(); } // Parameter validation if (!request->hasParam(param_edit_path)) { ESP_LOGE(tag, "Missing edit path parameter"); request->send(400, "text/plain", "Missing file path"); return; } // Get and decode filename String fileName = uriDecode(request->getParam(param_edit_path)->value()); ESP_LOGD(tag, "Edit request for file: %s", fileName.c_str()); // Set save path savePath = (fileName == "new") ? "/new.txt" : fileName; // Validate path if (!savePath.startsWith("/")) { savePath = "/" + savePath; } ESP_LOGD(tag, "Save path set to: %s", savePath.c_str()); try { sendHtmlFile("/www/edit.html", request, fileManagerHtmlProcessor); } catch (const std::exception& e) { ESP_LOGE(tag, "Failed to send edit page: %s", e.what()); request->send(500, "text/plain", "Internal server error"); } }); server.on("/files/save", HTTP_GET, [](AsyncWebServerRequest *request){ if(!request->authenticate(http_username, http_password)){ return request->requestAuthentication(); } String inputMessage((char*)0); if (request->hasParam(param_edit_textarea)) { inputMessage = request->getParam(param_edit_textarea)->value(); } if (request->hasParam(param_save_path)) { savePath = uriDecode(request->getParam(param_save_path)->value()); } writeFile(LittleFS, savePath.c_str(), inputMessage.c_str()); request->redirect("/files"); }); server.on("/files", HTTP_GET, [](AsyncWebServerRequest *request){ if(!request->authenticate(http_username, http_password)){ return request->requestAuthentication(); } sendHtmlFile("/www/files.html", request, fileManagerHtmlProcessor); }); // Lights related handlers server.on("/lights/settings", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200); }); server.on("/lights/settings", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }); server.on("/lights/animation", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }); server.on("/lights/setpixel", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }); // System and LED related handlers server.on("/system/summary", HTTP_GET, [](AsyncWebServerRequest *request){ String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; request->send(200, "application/json", response); }); server.on("/leds/settings", HTTP_GET, [](AsyncWebServerRequest *request){ /* //CreateSysSummmaryPacket(doc); String summary; serializeJson(jsDoc, summary); request->send(200, "application/json", summary); */ String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; request->send(200, "application/json", response); }); server.on("/leds/settings", HTTP_POST, [](AsyncWebServerRequest *request){ String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; request->send(200, "application/json", response); }); // LightStik related handlers server.on("/lightstik/settings", HTTP_GET, [](AsyncWebServerRequest *request){ String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; request->send(200, "application/json", response); }); server.on("/lightstik/settings", HTTP_POST, [](AsyncWebServerRequest *request){ String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; request->send(200, "application/json", response); }); server.on("/lightstik/register", HTTP_POST, [](AsyncWebServerRequest *request){ String response = "{\"status\":\"" + String(WifiClientConnected ? "Connected" : "Disconnected") + "\"}"; request->send(200, "application/json", response); }); // Firmware Update Handlers server.on("/upgrade/check", HTTP_GET, [](AsyncWebServerRequest *request) { JsonDocument doc; doc["currentVersion"] = FIRMWARE_VERSION; doc["latestVersion"] = "1.1.0"; doc["updateAvailable"] = true; String response; serializeJson(doc, response); request->send(200, "application/json", response); }); // Start update process server.on("/upgrade/start", HTTP_POST, [](AsyncWebServerRequest *request) { //if (!request->authenticate(http_username, http_password)) { // return request->requestAuthentication(); //} startFirmwareUpdateTask(&wsUpgradeProgress); request->send(200); }); //server.on("/upgrade/progress", HTTP_GET, [](AsyncWebServerRequest *request) { /* if (!request->authenticate(http_username, http_password)) { return request->requestAuthentication(); } AsyncWebServerResponse *response = request->beginResponse(200, "text/event-stream"); response->addHeader("Cache-Control", "no-cache"); response->addHeader("Connection", "keep-alive"); request->send(response); */ //}); server.on("/upgrade", HTTP_GET, [](AsyncWebServerRequest *request) { if(!request->authenticate(http_username, http_password)){ return request->requestAuthentication(); } request->send(LittleFS, "/www/upgrade.html", "text/html"); }); wsUpgradeProgress.onEvent(onWsUpdateProgressEvent); server.addHandler(&wsUpgradeProgress); // Basic Connection status check server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(200, "application/json", "{\"status\":\"connected\"}"); }); // Server requested files that aren't template processed server.on("/*", HTTP_GET, [](AsyncWebServerRequest *request) { // handle file uploads // Validate request if (!request) { ESP_LOGE(tag, "Invalid request"); return; } // Get and validate file path String filePath = request->url(); if (filePath.isEmpty()) { ESP_LOGE(tag, "Empty file path"); request->send(400, "text/plain", "Invalid file path"); return; } // Ensure path starts with '/' if (!filePath.startsWith("/")) { filePath = "/" + filePath; } try { // Get content type once const char* contentType = getFileType(getFileExtension(filePath.c_str())); if (!contentType) { ESP_LOGW(tag, "Unknown file type: %s", filePath.c_str()); contentType = "application/octet-stream"; } ESP_LOGD(tag, "Sending file: %s (%s)", filePath.c_str(), contentType); request->send(LittleFS, filePath, contentType); } catch (const std::runtime_error& e) { ESP_LOGE(tag, "FileSystem error: %s for path: %s", e.what(), filePath.c_str()); request->send(404, "text/plain", "File not found"); } catch (const std::exception& e) { ESP_LOGE(tag, "Error: %s for path: %s", e.what(), filePath.c_str()); request->send(500, "text/plain", "Internal server error"); } }); // 404 handler server.onNotFound([](AsyncWebServerRequest *request) { ESP_LOGE(tag, "404: %s", request->url().c_str()); request->send(404, "text/plain", "Not found"); }); } void onWsUpdateProgressEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { switch (type) { case WS_EVT_CONNECT: ESP_LOGD(tag,"WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); break; case WS_EVT_DISCONNECT: ESP_LOGD(tag, "WebSocket client #%u disconnected\n", client->id()); break; case WS_EVT_DATA: ESP_LOGD(tag, "WebSocket client #%u data\n", client->id()); //handleWebSocketMessage(arg, data, len); break; case WS_EVT_PONG: ESP_LOGD(tag, "WebSocket client #%u pong\n", client->id()); break; case WS_EVT_ERROR: ESP_LOGD(tag, "WebSocket client #%u error\n", client->id()); break; } } void handleFilesUpload_OnBody(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { static const size_t MAX_UPLOAD_SIZE = 1024 * 1024; // 1MB limit if (!index) { // Initial upload chunk if (!request->hasParam("dir-path", true, false)) { ESP_LOGE(tag, "Missing dir-path parameter"); request->send(400, "text/plain", "Missing dir-path"); return; } AsyncWebParameter* p = request->getParam("dir-path", true, false); String path = p->value() + "/" + filename; ESP_LOGD(tag, "Starting upload: %s", path.c_str()); // Validate path if (!path.startsWith("/")) { path = "/" + path; } request->_tempFile = LittleFS.open(path, "w"); if (!request->_tempFile) { ESP_LOGE(tag, "Failed to create file: %s", path.c_str()); request->send(500, "text/plain", "Failed to create file"); return; } } // Write chunk if (len && request->_tempFile) { if (index + len > MAX_UPLOAD_SIZE) { request->_tempFile.close(); LittleFS.remove(request->_tempFile.name()); ESP_LOGE(tag, "Upload too large"); request->send(413, "text/plain", "File too large"); return; } if (!request->_tempFile.write(data, len)) { ESP_LOGE(tag, "Write failed"); request->_tempFile.close(); request->send(500, "text/plain", "Write failed"); return; } } if (final) { request->_tempFile.close(); ESP_LOGD(tag, "Upload complete: %s, %u bytes", filename.c_str(), index + len); request->redirect("/files"); } } // Send html file with template processing {{VAR}} void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&)) { try { const char* htmlFile = readFile(LittleFS, filePath); if (!htmlFile) { ESP_LOGE(tag, "Failed to read file: %s", filePath); request->send(404, "text/plain", "File not found"); return; } String processedData = varReplace(htmlFile, callback); delete[] htmlFile; // Clean up allocated memory ESP_LOGD(tag, "Sent file: %s", filePath); request->send(200, "text/html", processedData); } catch (const std::exception& e) { ESP_LOGE(tag, "Error processing file %s: %s", filePath, e.what()); request->send(500, "text/plain", "Server error"); } } const char* getFileExtension(const char* filename) { // Input validation if (!filename) { ESP_LOGW(tag, "Null filename provided"); return ""; } // Find last dot const char* lastDot = strrchr(filename, '.'); if (!lastDot || lastDot == filename || *(lastDot + 1) == '\0') { ESP_LOGD(tag, "No valid extension found in: %s", filename); return ""; } ESP_LOGD(tag, "Found extension: %s", lastDot + 1); return lastDot + 1; } const char* getFileType(const char* ext) { if (!ext) return "application/octet-stream"; ESP_LOGD(tag, "Getting file type for extension: %s", ext); if (strcmp(ext, "png") == 0) return "image/png"; if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) return "image/jpeg"; if (strcmp(ext, "gif") == 0) return "image/gif"; if (strcmp(ext, "ico") == 0) return "image/x-icon"; if (strcmp(ext, "txt") == 0) return "text/plain"; if (strcmp(ext, "css") == 0) return "text/css"; if (strcmp(ext, "htm") == 0 || strcmp(ext, "html") == 0) return "text/html"; if (strcmp(ext, "js") == 0) return "text/javascript"; if (strcmp(ext, "json") == 0) return "application/json"; ESP_LOGW(tag, "Unknown file extension: %s", ext); return "application/octet-stream"; } // Finds segments between {{VAR}} and calls a callback function to replace VAR with new content String varReplace(const String& input, String (*callback)(const String&)) { static const char* tag = "varReplace"; // Validate inputs if (input.isEmpty() || !callback) { ESP_LOGW(tag, "Empty input or null callback"); return input; } // Pre-allocate result string with estimated size String result; result.reserve(input.length() * 1.2); // Add 20% for potential replacements const int maxSegmentLength = 32; int startPos = 0; // Process all segments while (true) { // Find next variable int start = input.indexOf("{{", startPos); if (start == -1) { break; } // Add text before variable result += input.substring(startPos, start); // Find end of variable int end = input.indexOf("}}", start + 2); if (end == -1) { ESP_LOGW(tag, "Unmatched {{ at position %d", start); result += input.substring(start); break; } // Extract and validate segment String segment = input.substring(start + 2, end); if (segment.length() <= maxSegmentLength) { try { String replacement = callback(segment); result += replacement; } catch (const std::exception& e) { ESP_LOGE(tag, "Callback error: %s", e.what()); result += input.substring(start, end + 2); } } else { ESP_LOGW(tag, "Segment too long: %d chars", segment.length()); result += input.substring(start, end + 2); } startPos = end + 2; } // Add remaining text if (startPos < input.length()) { result += input.substring(startPos); } return result; } const char* convertFileSize(const size_t bytes) { static char fileSizeBuffer[16]; // Pre-allocated buffer for the file size if (bytes < 1024) { snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%d B", bytes); } else if (bytes < 1024 * 1024) { snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f kB", bytes / 1024.0); } else if (bytes < 1024 * 1024 * 1024) { snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f MB", bytes / (1024.0 * 1024.0)); } else { snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0)); } return fileSizeBuffer; } void onWiFiEvent(WiFiEvent_t event) { //Serial.printf("[WiFi-event] event: %d\n", event); switch (event) { case ARDUINO_EVENT_WIFI_READY: ESP_LOGD(tag, "WiFi interface ready"); break; case ARDUINO_EVENT_WIFI_SCAN_DONE: ESP_LOGD(tag,"Completed scan for access points"); break; case ARDUINO_EVENT_WIFI_STA_START: ESP_LOGD(tag,"WiFi client started"); break; case ARDUINO_EVENT_WIFI_STA_STOP: ESP_LOGD(tag,"WiFi clients stopped"); break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: ESP_LOGD(tag,"Connected to AP"); break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: WifiClientConnected = false; ESP_LOGD(tag, "WiFi Disconnected"); Buzzer_Play_Tune(TUNE_DISCONNECTED); break; case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: ESP_LOGD(tag,"Authentication mode of access point has changed"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: WifiClientConnected = true; ESP_LOGD(tag,"My IP: %s", WiFi.localIP().toString()); //Wifi_Start_MDNS(); Buzzer_Play_Tune(TUNE_CONNECTED); break; case ARDUINO_EVENT_WIFI_STA_LOST_IP: WifiClientConnected = false; ESP_LOGD(tag,"Lost IP address and IP address is reset to 0"); break; case ARDUINO_EVENT_WPS_ER_SUCCESS: ESP_LOGD(tag,"WiFi Protected Setup (WPS): succeeded in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_FAILED: ESP_LOGD(tag,"WiFi Protected Setup (WPS): failed in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_TIMEOUT: ESP_LOGD(tag,"WiFi Protected Setup (WPS): timeout in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_PIN: ESP_LOGD(tag,"WiFi Protected Setup (WPS): pin code in enrollee mode"); break; case ARDUINO_EVENT_WIFI_AP_START: ESP_LOGD(tag, "WiFi access point started"); break; case ARDUINO_EVENT_WIFI_AP_STOP: ESP_LOGD(tag, "WiFi access point stopped"); break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: ESP_LOGD(tag, "Client connected"); break; case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: ESP_LOGD(tag, "SoftAP Client Disconnected"); Buzzer_Play_Tune(TUNE_DISCONNECTED); break; case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: ESP_LOGD(tag,"SoftAP Client Connected"); Buzzer_Play_Tune(TUNE_CONNECTED); break; case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: ESP_LOGD(tag,"Received probe request"); break; case ARDUINO_EVENT_WIFI_AP_GOT_IP6: ESP_LOGD(tag,"AP IPv6 is preferred"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP6: ESP_LOGD(tag,"STA IPv6 is preferred"); break; case ARDUINO_EVENT_ETH_GOT_IP6: ESP_LOGD(tag,"Ethernet IPv6 is preferred"); break; case ARDUINO_EVENT_ETH_START: ESP_LOGD(tag,"Ethernet started"); break; case ARDUINO_EVENT_ETH_STOP: ESP_LOGD(tag,"Ethernet stopped"); break; case ARDUINO_EVENT_ETH_CONNECTED: ESP_LOGD(tag,"Ethernet connected"); break; case ARDUINO_EVENT_ETH_DISCONNECTED: ESP_LOGD(tag,"Ethernet disconnected"); break; case ARDUINO_EVENT_ETH_GOT_IP: ESP_LOGD(tag,"Obtained IP address"); break; default: break; } } void Wifi_Start_MDNS(void) { ESP_LOGV(tag, "Initializing MDNS: %s", mDnsName.c_str()); if (!MDNS.begin(mDnsName.c_str())) { ESP_LOGE(tag, "Error setting up MDNS responder!"); }else{ ESP_LOGV(tag, "You can access device via http://%s.local", mDnsName); } } bool writeFile(fs::FS &fs, const char* path, const char* message) { // Validate inputs if (!path || !message) { ESP_LOGE(tag, "Invalid parameters: path=%p message=%p", path, message); return false; } // Open file with error checking File file = fs.open(path, "w"); if (!file) { ESP_LOGE(tag, "Failed to open file: %s", path); return false; } // Write with error handling try { size_t bytesWritten = file.print(message); if (bytesWritten == 0) { ESP_LOGE(tag, "Failed to write to file: %s", path); file.close(); return false; } // Ensure all data is written file.flush(); file.close(); ESP_LOGD(tag, "Successfully wrote %u bytes to %s", bytesWritten, path); return true; } catch (const std::exception& e) { ESP_LOGE(tag, "Exception while writing file %s: %s", path, e.what()); file.close(); return false; } } char* readFile(fs::FS &fs, const char* path) { static const char* tag = "readFile"; static const size_t MAX_FILE_SIZE = 1024 * 1024; // 1MB limit // Validate input if (!path) { ESP_LOGE(tag, "Invalid path parameter"); return nullptr; } // Open file File file = fs.open(path, "r"); if (!file || file.isDirectory()) { ESP_LOGE(tag, "Failed to open file: %s", path); return nullptr; } // Check file size size_t fileSize = file.size(); if (fileSize == 0 || fileSize > MAX_FILE_SIZE) { ESP_LOGE(tag, "Invalid file size: %u bytes", fileSize); file.close(); return nullptr; } // Allocate memory char* fileContent = new (std::nothrow) char[fileSize + 1]; if (!fileContent) { ESP_LOGE(tag, "Memory allocation failed for size: %u", fileSize + 1); file.close(); return nullptr; } // Read file size_t bytesRead = file.readBytes(fileContent, fileSize); file.close(); if (bytesRead != fileSize) { ESP_LOGE(tag, "Read failed: expected %u bytes, got %u", fileSize, bytesRead); delete[] fileContent; return nullptr; } // Null terminate fileContent[bytesRead] = '\0'; ESP_LOGD(tag, "Successfully read %u bytes from %s", bytesRead, path); return fileContent; } String getSoftAPMacAddress() { uint8_t mac[6]; WiFi.softAPmacAddress(mac); String macString = ""; for (int i = 0; i < 6; i++) { macString += String(mac[i], HEX); if (i < 5) macString += ":"; } return macString; } String listDirAsHtml(String directoryList[], int count) { String listedFiles; for (int i = 0; i < count; i++) { // directory html listedFiles += "Dir: "; listedFiles += directoryList[i]; listedFiles += "/-\n"; filesDropdownOptions += "\n"; dirDropdownOptions += "\n"; File dir = LittleFS.open(directoryList[i]); File file = dir.openNextFile(); while (file) { String fileName = file.name(); if (!file.isDirectory()) { //Serial.println(" File: " + String(file.name())); listedFiles += "  "; listedFiles += fileName; listedFiles += ""; listedFiles += convertFileSize(file.size()); listedFiles += "\n"; filesDropdownOptions += "\n"; } file = dir.openNextFile(); } dir.close(); } return listedFiles; } /******************** Specific Html Processors ********************/ // file manager html processor String fileManagerHtmlProcessor(const String& var){ if(var == "ALLOWED_EXTENSIONS_EDIT"){ return allowedExtensionsForEdit; } if(var == "FS_FREE_BYTES"){ return convertFileSize(LittleFS.totalBytes() - LittleFS.usedBytes()); } if(var == "FS_USED_BYTES"){ return convertFileSize(LittleFS.usedBytes()); } if(var == "FS_TOTAL_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } if(var == "RAM_FREE_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } if(var == "RAM_USED_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } if(var == "RAM_TOTAL_BYTES"){ return convertFileSize(LittleFS.totalBytes()); } if(var == "LISTED_FILES"){ filesDropdownOptions = ""; // clear out dirDropdownOptions = ""; // clear out String directories[MAX_DIRECTORIES]; int dirCount = 0; getAllDirectories(directories, dirCount); return listDirAsHtml(directories, dirCount); } if(var == "EDIT-DEL_FILES"){ return filesDropdownOptions; } if(var == "DIR_LIST"){ return dirDropdownOptions; } if(var == "SAVE_PATH_INPUT"){ return savePath; } if(var == "FIRM_VER"){ return FIRMWARE_VERSION; } return var; } String HomeHtmlProcessor(const String& var) { if (var == "APP_NAME") { //return sysProps.appName; return "N/A"; } if (var == "OLED") { return "N/A"; } if (var == "STRIP1") { //return (strip1) ? "Yes" : "No"; return "N/A"; } if (var == "STRIP2") { //return (strip2) ? "Yes" : "No"; return "N/A"; } if (var == "FRONT_LIGHT") { //return (animProps.frontLight.enabled) ? "Yes" : "No"; return "N/A"; } if (var == "REAR_LIGHT") { //return (animProps.rearLight.enabled) ? "Yes" : "No"; return "N/A"; } if (var == "FIRMWARE") { return FIRMWARE_VERSION; } if (var == "BOOTH_T") { //return String(sysProps.t_sensor.temperature) + "F"; return "N/A"; } if (var == "SETPOINT") { //return String(sysProps.t_sensor.Setpoint1) + "F"; return "N/A"; } if (var == "FLASH_SIZE") { return convertFileSize(ESP.getSketchSize());} if (var == "FLASH_FREE") { return convertFileSize(ESP.getFreeSketchSpace());} if (var == "HEAP_SIZE") { return convertFileSize(ESP.getHeapSize()); } if (var == "HEAP_FREE") { return convertFileSize(ESP.getFreeHeap()); } if (var == "CPU_FREQ") { return String(ESP.getCpuFreqMHz()) + "Mhz"; } if (var == "IP") { return WiFi.localIP().toString(); } if (var == "MAC") { return chipInfo.macStr; } if (var == "SSID") { return WiFi.SSID(); } if (var == "RSSI") { return String(WiFi.RSSI()); } if (var == "WIFI_CH") { return String(WiFi.channel()); } if (var == "ENCRYP") { return String(WiFi.encryptionType(0)); } if (var == "AP_SSID") { return WiFi.softAPSSID(); } if (var == "AP_CLIENTS") { return String(WiFi.softAPgetStationNum()); } if (var == "BLE") { return (commMode == COMM_WIFI_AP_BLE) ? "Yes" : "No"; } if (var == "BLE_SSID") { //return (commMode == COMM_WIFI_AP_BLE) ? BLEDeviceName : ""; return "N/A"; } if (var == "BLE_CLIENTS") { //return (BTDeviceConnected) ? "1" : "0"; return "N/A"; } if (var == "AP_MAC") { return getSoftAPMacAddress(); } // Return an empty string if the variable is not recognized return var; } void handleUpdateProgress(AsyncWebServerRequest *request) { static const char* tag = "UpdateProgress"; //if (!request->authenticate(http_username, http_password)) { // return request->requestAuthentication(); //} request->send(200, "text/plain", "Update progress"); // Send a simple response }