boothifier/webSock/my_wifi.cpp
2025-09-07 23:38:56 -07:00

1165 lines
38 KiB
C++

#include "my_wifi.h"
#include <Wifi.h>
#include <esp_wifi.h>
#include <esp_task_wdt.h>
#include <ESPmDNS.h>
#include <DNSServer.h>
#include "esp_log.h"
#include <FS.h>
#include <LittleFS.h>
#include <SD.h>
#include "common/fileSystem.h"
#include "global.h"
#include "my_buzzer.h"
#include <UriDecode.h>
#include <ArduinoJson.h>
#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<JsonObject>();
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<JsonObject>();
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<JsonArray>();
for (int i = 0; i < networkCount; i++) {
auto network = networks.add<JsonObject>();
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 += "<tr id=\"file-row\"><td>Dir: ";
listedFiles += directoryList[i];
listedFiles += "/</td><td>-</td></tr>\n";
filesDropdownOptions += "<option value=\"";
filesDropdownOptions += directoryList[i];
filesDropdownOptions += "\">";
filesDropdownOptions += directoryList[i];
filesDropdownOptions += "/</option>\n";
dirDropdownOptions += "<option value=\"";
dirDropdownOptions += directoryList[i];
dirDropdownOptions += "\">";
dirDropdownOptions += directoryList[i];
dirDropdownOptions += "/</option>\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 += "<tr id=\"file-row\"><td>&emsp;<button onclick=\"window.location.href='/download?file=";
listedFiles += file.path();
listedFiles += "\" \"\">&#8681;</button>&emsp;";
listedFiles += fileName;
listedFiles += "</td><td>";
listedFiles += convertFileSize(file.size());
listedFiles += "</td></tr>\n";
filesDropdownOptions += "<option value=\"";
filesDropdownOptions += file.path();
filesDropdownOptions += "\">&emsp;&emsp;";
filesDropdownOptions += fileName;
filesDropdownOptions += "</option>\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
}