/* wifi is started in STATION mode and tries to connected to AP saved in the creds.json file. It will keep trying to connect forever. If it connects then the board LEDS will toggle, the buzzer will play a tune and the IP address will be sent to the serial port. Also the device will be discoverable as "http://atadev.local/" If credentials need to be changed then double reset can be done to trigger the AP mode and a wifi config page will be available on address 192.168.4.1. Credentials can be entered and applied then you can reset the device so it connects with the correct credentials. */ #include "my_wifi.h" #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include #include #include #include #include #include #include #include #include "global.h" #include #include "common/my_buzzer.h" #include "my_board.h" #include "common/led_animation.h" #include "led_strip.h" #include "common/fileSystem.h" #include "JsonConstrain.h" #include "led_strip.h" #include "my_oled.h" #include "BTSerial.h" #include "esp_log.h" #include "firmware_html.h" int RebootSystem = 0; #define WIFI_TIMEOUT_MS 10000 static const char* tag = "wifi"; bool WifiClientConnected = false; AsyncWebServer webServer(80); DNSServer *dnsServer; #define DNS_PORT 53 #define StartDelayedRebooth RebootSystem = 1000/BUTTON_UPDATE_PERIOD; // start reboot countdown for 1sec String ssid; String passPhrase; String AP_SSID; String AP_Pass; String mDnsName; String HostName; IPAddress local_IP(192,168,10,1); IPAddress gateway(192,168,10,200); 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"; //const char* jquery = "/jquery-3.6.3.min.js"; void Init_Wifi_Task(void) { File file = LittleFS.open("/cfg/wifi.json", "r"); if(!file){ ESP_LOGE(tag, "Failed to open wifi.json!"); file.close(); return; } // Parse the JSON file StaticJsonDocument<1024> doc; DeserializationError error = deserializeJson(doc, file); if(!error){ JsonObject wifiJson = doc["wifi"]; file.close(); mDnsName = jsonConstrainString(tag, wifiJson,"mdns-name", "atadev"); ESP_LOGV(tag, "mDnsName: %s", mDnsName.c_str()); if(jsonConstrainBool(tag, wifiJson, "en", false)){ JsonObject j = doc["cred1"]; Wifi_Read_Credentials( j ); xTaskCreatePinnedToCore(Wifi_Task, "Wifi_Task", 1024*10, NULL, 1, NULL, CONFIG_ARDUINO_RUNNING_CORE); ESP_LOGV(tag, "Initialized WiFi (task created)..."); } JsonObject ap = doc["ap"]; AP_SSID = jsonConstrainString(tag, ap, "ssid", "ATA_AP"); if(jsonConstrainBool(tag, ap, "append-id", true)){ String macHex = String(chipInfo.macByte[1], HEX) + String(chipInfo.macByte[0], HEX); AP_SSID += "_"; macHex.toUpperCase(); AP_SSID += macHex; } AP_Pass = jsonConstrainString(tag, ap, "pass", "12345678"); }else{ ESP_LOGE(tag, "Deserialization error for wifi.json"); } } void onWiFiEvent(WiFiEvent_t event) { //Serial.printf("[WiFi-event] event: %d\n", event); switch (event) { case ARDUINO_EVENT_WIFI_READY: //Serial.println("WiFi interface ready"); break; case ARDUINO_EVENT_WIFI_SCAN_DONE: //Serial.println("Completed scan for access points"); break; case ARDUINO_EVENT_WIFI_STA_START: //Serial.println("WiFi client started"); break; case ARDUINO_EVENT_WIFI_STA_STOP: //Serial.println("WiFi clients stopped"); break; case ARDUINO_EVENT_WIFI_STA_CONNECTED: WifiClientConnected = true; //Serial.println("Connected to access point"); break; case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: WifiClientConnected = false; ESP_LOGV(tag,"WiFi Disconnected"); //Buzzer_Play_Tune(TUNE_WIFI_DISCONNECTED); break; case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE: //Serial.println("Authentication mode of access point has changed"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP: ESP_LOGV(tag,"My IP: %s", WiFi.localIP().toString()); Wifi_Start_MDNS(); Buzzer_Play_Tune(TUNE_WIFI_CONNECTED); break; case ARDUINO_EVENT_WIFI_STA_LOST_IP: //Serial.println("Lost IP address and IP address is reset to 0"); break; case ARDUINO_EVENT_WPS_ER_SUCCESS: //Serial.println("WiFi Protected Setup (WPS): succeeded in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_FAILED: //Serial.println("WiFi Protected Setup (WPS): failed in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_TIMEOUT: // Serial.println("WiFi Protected Setup (WPS): timeout in enrollee mode"); break; case ARDUINO_EVENT_WPS_ER_PIN: //Serial.println("WiFi Protected Setup (WPS): pin code in enrollee mode"); break; case ARDUINO_EVENT_WIFI_AP_START: //Serial.println("WiFi access point started"); break; case ARDUINO_EVENT_WIFI_AP_STOP: //Serial.println("WiFi access point stopped"); break; case ARDUINO_EVENT_WIFI_AP_STACONNECTED: //Serial.println("Client connected"); break; case ARDUINO_EVENT_WIFI_AP_STADISCONNECTED: ESP_LOGV(tag,"SoftAP Client Disconnected"); //Buzzer_Play_Tune(TUNE_WIFI_DISCONNECTED); break; case ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED: ESP_LOGV(tag,"SoftAP Client Connected"); Buzzer_Play_Tune(TUNE_WIFI_CONNECTED); break; case ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED: //Serial.println("Received probe request"); break; case ARDUINO_EVENT_WIFI_AP_GOT_IP6: //Serial.println("AP IPv6 is preferred"); break; case ARDUINO_EVENT_WIFI_STA_GOT_IP6: //Serial.println("STA IPv6 is preferred"); break; case ARDUINO_EVENT_ETH_GOT_IP6: //Serial.println("Ethernet IPv6 is preferred"); break; case ARDUINO_EVENT_ETH_START: //Serial.println("Ethernet started"); break; case ARDUINO_EVENT_ETH_STOP: //Serial.println("Ethernet stopped"); break; case ARDUINO_EVENT_ETH_CONNECTED: //Serial.println("Ethernet connected"); break; case ARDUINO_EVENT_ETH_DISCONNECTED: //Serial.println("Ethernet disconnected"); break; case ARDUINO_EVENT_ETH_GOT_IP: // Serial.println("Obtained IP address"); break; default: break; } } void Wifi_Task(void *parameters) { // Extend watchdog timer esp_task_wdt_init(2, false); Setup_WebServer_Handlers(&webServer); WiFi.onEvent(onWiFiEvent); WiFi.setHostname(mDnsName.c_str()); WiFi.softAPConfig(local_IP, gateway, subnet); WiFi.softAP(AP_SSID.c_str(), AP_Pass.c_str()); Wifi_Start_MDNS(); vTaskDelay(100); webServer.begin(); // Choose WIFI Mode if(commMode == COMM_WIFI_AP_BLE){ // AP Only WiFi.mode(WIFI_AP); while(1){ vTaskDelay(500 / portTICK_PERIOD_MS); } }else{ // AP & Client (dslrbooth) esp_wifi_set_ps(WIFI_PS_NONE); WiFi.mode(WIFI_AP_STA); Wifi_Client_Loop(); } } void Wifi_Client_Loop(){ while(true){ // Wifi status check loop if(WiFi.status() == WL_CONNECTED){ vTaskDelay(250); continue; } // Start WiFi Client ESP_LOGD(tag, "Wifi trying stored creds: %s..%s",ssid.c_str(), passPhrase.c_str()); WiFi.begin(ssid.c_str(), passPhrase.c_str()); //Wait until connected or timeout ESP_LOGV(tag, "WiFi Connecting..."); unsigned long startAttemptTime = millis(); while(WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS){ vTaskDelay(100 / portTICK_PERIOD_MS); } // Delay if wifi connection fails and continue if(WiFi.status() != WL_CONNECTED){ ESP_LOGW(tag, "WIFI Connection Failed!"); for(int i = 0; i < WIFI_TIMEOUT_MS/100; i++){ vTaskDelay(100); } continue; } } } 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); } } void Wifi_Read_Credentials(const JsonObject &credJson){ if(credJson.isNull()){ // set generic defaults on error ESP_LOGE(tag, "Failed to find wifi key"); ssid = "Generic"; passPhrase = "password"; //strncpy(ssid, "Generic", sizeof(ssid)-1); //strncpy(passPhrase, "password", sizeof(passPhrase)-1); return; } else{ // TODO Restore String ssid = jsonConstrainString(tag, credJson, "ssid", "default"); passPhrase = jsonConstrainString(tag, credJson, "pass", "password"); //strncpy(ssid, jsonConstrainString(tag, credJson, "ssid", "default").c_str(), sizeof(ssid)-1); //strncpy(passPhrase, jsonConstrainString(tag, credJson, "pass", "password").c_str(), sizeof(passPhrase)-1); } } void Wifi_Save_Credentials(String newSSID, String newPass) { // Open the credentials file writing File credsFile = LittleFS.open("/cfg/wifi.json", "r+"); if(!credsFile){ ESP_LOGE(tag, "Failed to open wifi.json\n"); return; } // Parse the JSON file JsonDocument doc; DeserializationError error = deserializeJson(doc, credsFile); if(!error){ JsonObject cred1Json = doc.createNestedObject("cred1"); if(cred1Json){ cred1Json["ssid"] = newSSID; cred1Json["pass"] = newPass; // Clear file contents before saving the modified JSON credsFile.seek(0); int written = serializeJson(doc, credsFile); // save to file ESP_LOGD(tag, "Credentials saved... ssid: %s, pass: %s, size written: %d", newSSID, newPass, written); } credsFile.close(); }else{ ESP_LOGE(tag, "Deserialization error for wifi.json"); } } void Setup_WebServer_Handlers(AsyncWebServer* serv) { serv->on("/dslrbooth", HTTP_GET, handleGET_DSLRBooth); serv->on("/", HTTP_GET, [](AsyncWebServerRequest *request){ if(WiFi.getMode() == WIFI_AP && !WifiClientConnected){ request->redirect("/wifi"); }else{ request->redirect("/home"); } }); serv->on("/home", HTTP_GET, [](AsyncWebServerRequest *request){ sendHtmlFile("/www/home.html", request, HomeHtmlProcessor); }); serv->on("/lights", HTTP_GET, [](AsyncWebServerRequest *request){ sendHtmlFile("/www/lights.html", request, htmlProcessor); }); serv->on("/setup", HTTP_GET, [](AsyncWebServerRequest *request){ if(!request->authenticate(http_username, http_password)){ return request->requestAuthentication(); } sendHtmlFile("/www/setup.html", request, htmlProcessor); }); serv->on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request){ if(WiFi.getMode() == WIFI_MODE_APSTA){ // TODO Disable navigation bar } sendHtmlFile("/www/wifi.html", request, htmlProcessor); }); serv->on("/files", HTTP_GET, [](AsyncWebServerRequest *request){ if(!request->authenticate(http_username, http_password)){ return request->requestAuthentication(); } sendHtmlFile("/www/files.html", request, htmlProcessor); }); serv->on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) { request->send(200); }, handlePOST_Upload); serv->on("/download", HTTP_GET, [](AsyncWebServerRequest *request){ if (request->hasParam("file")) { String filename = request->getParam("file")->value(); if (LittleFS.exists(filename)) { fs::File file = LittleFS.open(filename, "r"); if (file) { request->send(LittleFS, filename, "application/octet-stream"); file.close(); return; } } } request->send(404, "text/plain", "File not found!"); }); serv->on("/delete", HTTP_GET, [](AsyncWebServerRequest *request){ if(!request->authenticate(http_username, http_password)){ return request->requestAuthentication(); } String filename = request->getParam(param_delete_path)->value(); if(filename !="choose"){ LittleFS.remove(filename.c_str()); ESP_LOGD(tag, "Deleted file: %s", filename.c_str()); } request->redirect("/files"); }); serv->on("/edit", HTTP_GET, [](AsyncWebServerRequest *request){ if(!request->authenticate(http_username, http_password)){ return request->requestAuthentication(); } String fileName = request->getParam(param_edit_path)->value(); if(fileName =="new"){ savePath = "/new.txt"; } else{ savePath = fileName; } ESP_LOGD(tag, "Edit file: %s", savePath.c_str()); sendHtmlFile("/www/edit.html", request, htmlProcessor); }); serv->on("/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 = request->getParam(param_save_path)->value(); } writeFile(LittleFS, savePath.c_str(), inputMessage.c_str()); request->redirect("/files"); }); serv->on("/update", HTTP_POST, handlePOST_Update, updateCallback); // Request Data serv->on("/get", HTTP_GET, handleGET_Get); // Post Data serv->on("/post", HTTP_POST, handlePOST_Post, postFileUpload, postBody); serv->on("/generate_204", HTTP_GET, [](AsyncWebServerRequest *request){ ESP_LOGD(tag, "/generate_204 ... redirect to /wifi"); request->redirect("/wifi"); }); serv->on("/hotspot-detect.html", HTTP_GET, [](AsyncWebServerRequest *request){ ESP_LOGD(tag, "/hotspot-detect.html ... redirect to /wifi"); request->redirect("/wifi"); }); serv->on("/msftconnecttest.txt", HTTP_GET, [](AsyncWebServerRequest *request){ ESP_LOGD(tag, "/msftconnecttest.txt ... redirect to /wifi"); request->redirect("/wifi"); }); serv->on("/reg-stick", HTTP_POST, handlePOST_RegStick); serv->on("/firmware", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", firmware_html_page); }); serv->onNotFound([](AsyncWebServerRequest *request){ if(WiFi.getMode() == WIFI_MODE_APSTA){ ESP_LOGD(tag, "OnNotFound Redirect to /wifi"); request->redirect("/wifi"); }else if(WiFi.getMode() == WIFI_MODE_AP){ ESP_LOGD(tag, "OnNotFound Redirect to /home"); request->redirect("/home"); }else{ request->send(404); } }); serv->on("/*", HTTP_GET, handleGET_SendFile); // send any file } void handlePOST_RegStick(AsyncWebServerRequest *request){ } bool getpostSuccess = false; // void handleGET_Get(AsyncWebServerRequest *request){ if(request->hasParam("type", false, false)) { const char* dataType = request->getParam("type", false, false)->value().c_str(); if(!strcmp(dataType, "app-events")){ // send json file request->send(LittleFS, "/cfg/app-events.json", "application/json"); return; } if(!strcmp(dataType, "anim-profiles")){ // send json file request->send(LittleFS, "/cfg/anim-profiles.json", "application/json"); return; } if(!strcmp(dataType, "anim-list")){ request->send(LittleFS, "/cfg/anim-list.json", "application/json"); return; } if(!strcmp(dataType, "sys-summary")){ JsonDocument doc; CreateSysSummmaryPacket(doc); String summary; serializeJson(doc, summary); request->send(200, "application/json", summary); return; } if(!strcmp(dataType, "wifi")){ JsonDocument jsdoc; jsdoc["ssid"] = ssid; //jsdoc["pass"] = (int)strlen(passPhrase); jsdoc["pass"] = passPhrase; String jsStr; serializeJson(jsdoc, jsStr); request->send(200, "application/json", jsStr); return; } } request->send(400, "text/plain", "Failed"); } void CreateSysSummmaryPacket(JsonDocument &doc){ JsonObject booth = doc.createNestedObject("booth"); booth["mode"] = ""; booth["app"] = sysProps.appName; booth["strip1"] = (strip1) ? true : false, booth["strip2"] = (strip2) ? true : false, booth["front-light"] = true; booth["rear-light"] = false; booth["oled"] = (oled) ? true : false; //ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getHeapSize()-ESP.getFreeHeap()); JsonObject Temperature = doc.createNestedObject("temperature"); Temperature["temp"] = sysProps.t_sensor.temperature; Temperature["sp1"] = sysProps.t_sensor.Setpoint1; doc["system"]["firm-ver"] = FIRMWARE_VER; JsonObject chip = doc.createNestedObject("chip"); chip["flash-size"] = ""; chip["flash-free"] = ""; chip["ram-size"] = ESP.getHeapSize(); chip["ram-free"] = ESP.getFreeHeap(); // Get the Wi-Fi RSSI wifi_ap_record_t wifi_ap_info; esp_err_t rssi_result = esp_wifi_sta_get_ap_info(&wifi_ap_info); int rssi = 0; int wifi_ch = 0; String encryp = ""; if (rssi_result == ESP_OK) { rssi = wifi_ap_info.rssi; wifi_ch = wifi_ap_info.primary; wifi_auth_mode_t auth_mode = wifi_ap_info.authmode; switch (auth_mode) { case WIFI_AUTH_OPEN: encryp = "Open"; break; case WIFI_AUTH_WEP: encryp = "WEP"; break; case WIFI_AUTH_WPA_PSK: encryp = "WPA_PSK"; break; case WIFI_AUTH_WPA2_PSK: encryp = "WPA2_PSK"; break; case WIFI_AUTH_WPA_WPA2_PSK: encryp = "WPA_WPA2_PSK"; break; case WIFI_AUTH_WPA2_ENTERPRISE: encryp = "WPA2_ENT"; break; default: encryp = "UNKNOWN"; break; } } JsonObject wifi = doc.createNestedObject("wifi"); wifi["client-ip"] = "XXX.XXX.XXX.XXX"; wifi["mac-addr"] = chipInfo.macStr; wifi["ch"] = wifi_ch; wifi["rssi"] = rssi; wifi["encryp"] = encryp; JsonObject ble = doc.createNestedObject("ble"); ble["en"] = (sysProps.appIndex == DSLRBOOTH_INDEX)? false : true; ble["clients"] = (BTDeviceConnected)? 1 : 0; ble["ssid"] = BLEDeviceName; } void handlePOST_Post(AsyncWebServerRequest *request){ ESP_LOGD(tag, "posts request.."); if(!getpostSuccess) { request->send(400, "text/plain", "Error"); } request->send(200, "text/plain", "Ok"); } void postFileUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final) { getpostSuccess = false; Serial.println("post file"); } // POST Requests int packets, postTotal; char postType[24]; char *postData; void postBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ if (!index){ packets = 0; int sectors = (total + 1023 + 64) / 1024; postData = new char[1024 * sectors]; } // Accumulate Data Chunks memcpy(postData + index, data, len); packets++; if((index + len) >= total){ ESP_LOGD(tag, "index: %d, len: %d, total: %d", index, len, total); ESP_LOGD(tag, "packets: %d", packets); getpostSuccess = false; // Check if the request contains the necessary parameters if (request->hasParam("type", false, false)) { const char* dataType = request->getParam("type", false, false)->value().c_str(); ESP_LOGD(tag, "data type: %s", dataType); if(!strcmp(dataType, "anim-profile-common") || !strcmp(dataType, "set-countdown")){ JsonDocument profJson(1 * 1024); DeserializationError error = deserializeJson(profJson, postData, total); if(!error){ animProps.profileIndex = profJson["profile-index"].as(); JsonObject countdownJson = profJson["countdown"]; animProps.frontLight.minDuty = countdownJson["min"].as(); animProps.frontLight.maxDuty = countdownJson["max"].as(); animProps.frontLight.holdTime = countdownJson["hold"].as(); animProps.frontLight.rampTime = countdownJson["ramp"].as(); int indexChanged = animProps.profileIndex - countdownJson["profile-index"].as(); animProps.profileIndex = countdownJson["profile-index"].as(); if(indexChanged){ load_animation_profileByIndex(animProps.profileIndex); } // Save to disk/flash if(dataType[0] == 'a'){ if(updateJsonDocument(profJson, "/cfg/anim-profile-common.json")){ Buzzer_Beep(150, 3000); } } else{ ESP_LOGD(tag, "Post:set-countdown, index: %d, min: %.1f, max: %.1f, hold: %d, ramp: %d", animProps.profileIndex, animProps.frontLight.minDuty, animProps.frontLight.maxDuty, animProps.frontLight.holdTime, animProps.frontLight.rampTime); Buzzer_Beep(150, 3000); } }else{ ESP_LOGE(tag, "Deserialization error for wifi.json"); } getpostSuccess = true; //goto done; } else if(!strncmp(dataType, "anim-profile", 12)){ if (strlen(dataType) > 12) { int profile_index = (dataType[12] - '0' - 1) % 8; // extract index, assuming dataType has enough characters //DynamicJsonDocument profJson(4 * 1024); JsonDocument profJson; DeserializationError error = deserializeJson(profJson, postData, total); if(!error){ JsonArray eventsArray = profJson["events"]; for(int i = 0; i < eventsArray.size(); i++ ){ animProps.event[i].selfIndex = i; animProps.event[i].animIndex = eventsArray[i]["anim"].as(); animProps.event[i].hue = eventsArray[i]["hue"].as(); animProps.event[i].hueRange = eventsArray[i]["hue-range"].as(); animProps.event[i].speed = eventsArray[i]["speed"].as(); animProps.event[i].param1 = eventsArray[i]["param1"].as(); animProps.event[i].param2 = eventsArray[i]["param2"].as(); animProps.event[i].check1 = eventsArray[i]["check1"].as(); animProps.event[i].check2 = eventsArray[i]["check2"].as(); animProps.event[i].check3 = eventsArray[i]["check3"].as(); animProps.event[i].check4 = eventsArray[i]["check4"].as(); animProps.event[i].countDown = 0; } ESP_LOGD(tag, "Extracted/Updated active profile to animProps."); // Save to disk/flash String filePath = "/cfg/anim-profile" + String(profile_index + 1) + ".json"; if(updateJsonDocument(profJson, filePath.c_str())){ Buzzer_Beep(150, 3000); } }else{ ESP_LOGE(tag, "Deserialization error for wifi.json"); } getpostSuccess = true; } } else if(!strcmp(dataType, "play-anim")){ JsonDocument animData; DeserializationError error = deserializeJson(animData, postData, total); if(!error){ ANIMATION_EVENT testEvent; testEvent.selfIndex = animData["index"].as(); testEvent.animIndex = animData["anim"].as(); testEvent.hue = animData["hue"].as(); testEvent.hueRange = animData["hue-range"].as(); testEvent.speed = animData["speed"].as(); testEvent.param1 = animData["param1"].as(); testEvent.param2 = animData["param2"].as(); testEvent.check1 = animData["check1"].as(); testEvent.check2 = animData["check2"].as(); testEvent.check3 = animData["check3"].as(); testEvent.check4 = animData["check4"].as(); if(testEvent.selfIndex != 0){ testEvent.selfIndex = 1; } ESP_LOGI(tag, "Post:play-anim, index: %d, anim: %d, hue: %d, rng: %d, speed: %d, par1: %d, par2: %d, chk1: %d, chk2: %d, chk3: %d, chk4: %d", testEvent.selfIndex, testEvent.animIndex, testEvent.hue, testEvent.hueRange, testEvent.speed, testEvent.param1, testEvent.param2, testEvent.check1, testEvent.check2, testEvent.check3, testEvent.check4); testEvent.countDown = 0; // set to zero so param1 determines countdown time testEvent.type = EV_TEST; PostNewEvent(testEvent); // eventIndex always = 1 Buzzer_Beep(150, 3000); }else{ ESP_LOGE(tag, "Deserialization error for play-anim"); } getpostSuccess = true; } else if(!strcmp(dataType, "wifi")){ JsonDocument wifiCreds; DeserializationError error = deserializeJson(wifiCreds, postData, total); if(!error){ //read credentials //const char* new_ssid = wifiCreds["ssid"]; //const char* new_pass = wifiCreds["pass"]; String new_ssid = wifiCreds["ssid"]; String new_pass = wifiCreds["pass"]; ESP_LOGV(tag, "SSID: %s PASS: %s", new_ssid, new_pass); // Save Credentials if different //if(strcmp(new_ssid, ssid)!=0 || strcmp(new_pass, passPhrase)!=0){ if(new_ssid != ssid || new_pass != passPhrase) Wifi_Save_Credentials(new_ssid, new_pass); StartDelayedRebooth; } }else{ ESP_LOGE(tag, "Deserialization error for wifi"); } getpostSuccess = true; } else if(!strcmp(dataType, "set-pixel")){ JsonDocument js; DeserializationError error = deserializeJson(js, postData, total); if(!error){ ANIMATION_EVENT testEvent; testEvent.selfIndex = 1; testEvent.animIndex = 81; testEvent.hue = js["hue"].as(); testEvent.param1 = js["index"].as(); testEvent.type = EV_TEST; PostNewEvent(testEvent); // eventIndex always = 1 ESP_LOGD(tag, "Post: set-pixel, hue: %d, pixel: %d", testEvent.hue, testEvent.param1); Buzzer_Beep(150, 3000); }else{ ESP_LOGE(tag, "Deserialization error for set-pixel"); } Buzzer_Beep(50, 3000); getpostSuccess = true; } else if(!strcmp(dataType, "clear-strip")){ ANIMATION_EVENT testEvent; testEvent.selfIndex = 1; testEvent.animIndex = 80; ESP_LOGD(tag, "Post: clear-strip"); testEvent.type = EV_TEST; PostNewEvent(testEvent); // eventIndex always = 1 getpostSuccess = true; Buzzer_Beep(50, 3000); } else if(!strcmp(dataType, "setup-save")){ JsonDocument js; DeserializationError error = deserializeJson(js, postData, total); if(!error){ // If app index is different open app-events.json and update if(sysProps.appIndex != js["appindex"].as()){ File file = LittleFS.open("/cfg/app-events.json", "r+"); if(!file){ ESP_LOGE(tag, "Failed to open app-events.json"); file.close(); return; } JsonDocument doc; DeserializationError error = deserializeJson(doc, file); file.close(); if(!error){ // Update index value doc["index"] = js["appindex"].as(); ESP_LOGD(tag, "New App Index = %d", doc["index"].as()); if(updateJsonDocument(doc, "/cfg/app-events.json")){ Buzzer_Beep(150, 3000); } }else{ ESP_LOGE(tag, "Deserialization error for app-events.json"); } } }else{ ESP_LOGE(tag, "Deserialization error for seteup-save"); } // if any of the values are diiferent then open led-devices.json and update //if app index is different open app-events.json and update bool editJson = false; //if(js["en1"].as() != strip1->size) { editJson = true; } if(js["count1"].as() != strip1->size) { editJson = true; } else if(js["shift1"].as() != strip1->shift) { editJson = true; } else if(js["offset1"].as() != strip1->offset) { editJson = true; } else if(js["rgb1"].as() != strip1->ledOrder) { editJson = true; } else if(js["power1"].as() != strip1->powerDiv) { editJson = true; } if(editJson){ File file = LittleFS.open("/cfg/led-devices.json", "r+"); if(!file){ ESP_LOGE(tag, "Failed to open led-devices.json"); file.close(); return; } JsonDocument doc; DeserializationError error = deserializeJson(doc, file); file.close(); if(!error){ JsonObject jsStrip1 = doc.createNestedObject("strip1"); // nested so it doesn't create a copy jsStrip1["en"] = true; jsStrip1["size"] = js["count1"].as(); jsStrip1["shift"] = js["shift1"].as(); jsStrip1["offset"] = js["offset1"].as(); jsStrip1["power-div"] = js["power1"].as(); jsStrip1["rgb-order"] = js["rgb1"]; // Save Json File if(updateJsonDocument(doc, "/cfg/led-devices.json")){ Buzzer_Beep(150, 3000); } }else{ ESP_LOGE(tag, "Deserialization error for led-devices.json"); } } ESP_LOGD(tag, "Post: setup-save"); getpostSuccess = true; Buzzer_Beep(150, 3000); } else if(!strcmp(dataType, "restart")){ StartDelayedRebooth; ESP_LOGD(tag, "Post: restart"); getpostSuccess = true; Buzzer_Beep(150, 3000); } } // Delete the allocated buffer (owned by the request->_tempObject) delete[] postData; } } bool isInternetConnected() { if (WiFi.status() == WL_CONNECTED) { // check if WiFi is connected WiFiClient client; const int httpPort = 80; if (client.connect("www.google.com", httpPort)) { // try to connect to Google client.stop(); return true; // Internet access available } } return false; // Internet access not available } void handlePOST_Upload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { if (!index && request->hasParam("dir-path", true, false)) { AsyncWebParameter* p = request->getParam("dir-path",true, false); String path = p->value() + "/" + filename; ESP_LOGD(tag, "upload path: %s", path.c_str()); request->_tempFile= LittleFS.open(path, "w"); //fs::File file = LittleFS.open(path, "w"); if (!request->_tempFile) { ESP_LOGE(tag, "Failed to create file!"); return; } }else{ ESP_LOGE(tag, "dir-path Param not found.."); } if(len && request->_tempFile){ request->_tempFile.write(data, len); } if(final){ request->_tempFile.close(); request->redirect("/files"); ESP_LOGD(tag, "UploadEnd: %s, %u B", filename.c_str(), index+len); } } void handleGET_DSLRBooth(AsyncWebServerRequest *request) { static int lastCountdown = 1000; request->send(200, "text/plain"); // send this right away to avoid comm delays if(request->args() >= 1){ int x = 0; const char* event_type = request->arg( x ).c_str(); if(strcmp("countdown", event_type) == 0){ int p = request->arg(1).toInt(); if(p > 0 && p <= 100){ animStatus.countStatus = p; } // in case "coundown_start" was missed if(animStatus.EventsIndex != ANIM_WHITEFILL_INDEX){ animProps.event[0].countDown = lastCountdown; animProps.event[0].type = EV_NORMAL; PostNewEvent(animProps.event[0]); } } else if(strcmp("countdown_start", event_type) == 0){ // index=0 int count = request->arg(1).toInt(); // retrieve countdown value //Log.traceln(" countdown = %d", count); if(count < 1){count = 1;} count *= 1000; lastCountdown = count - 500; animProps.event[0].countDown = lastCountdown; animProps.event[0].type = EV_NORMAL; PostNewEvent(animProps.event[0]); } else if(strcmp("sharing_screen", event_type) == 0){ animProps.event[2].type = EV_NORMAL; PostNewEvent(animProps.event[2]); } else if(strcmp("session_end", event_type) == 0){ //index=1 animProps.event[1].type = EV_NORMAL; PostNewEvent(animProps.event[1]); } else{ // else if(strcmp_P("session_start", event_type) == 0){ // TODO determine if ramp down is active depending on session type /* const char* param1 = request->arg(1).c_str(); if(strcmp("PrintOnly", param1)){ animStatus.Animation = ANIM_START_PRINTONLY; xTaskNotifyGive( Strip1_Task_Handle ); //Serial.println("ses_print..\n\r"); }else if(strcmp("PrintAndGIF", param1)){ animStatus.Animation = ANIM_START_PRINTGIF; xTaskNotifyGive( Strip1_Task_Handle ); //Serial.println("ses_prnt_gif..\n\r"); }else if(strcmp("OnlyGIF", param1)){ animStatus.Animation = ANIM_START_GIFONLY; xTaskNotifyGive( Strip1_Task_Handle ); //Serial.println("ses_gif..\n\r"); }else if(strcmp("Boomerang", param1)){ animStatus.Animation = ANIM_START_BOOM; xTaskNotifyGive( Strip1_Task_Handle ); //Serial.println("ses_boom..\n\r"); }else if(strcmp("Video", param1)){ animStatus.Animation = ANIM_START_VIDEO; xTaskNotifyGive( Strip1_Task_Handle ); //Serial.println("ses_vid..\n\r"); } */ } } } bool isFirmware = true; bool updateSuccessful = false; void handlePOST_Update(AsyncWebServerRequest *request) { bool hasError = Update.hasError(); String responseContent = hasError ? "failed" : "success"; String responseType = hasError ? "text/plain" : "text/html"; int responseCode = hasError ? 500 : 200; AsyncWebServerResponse *response = request->beginResponse(responseCode, responseType, responseContent); response->addHeader("status", responseContent); request->send(response); } // handlePOST_Update Callback function to install the firmware or file system bin files void updateCallback(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { // Do Once Code if (!index) { size_t fileSize = UPDATE_SIZE_UNKNOWN; // Retrieve file-size parameter if(request->hasParam("file-size", true, false)) { // make sure param is from POST request or it will not find it fileSize = request->getParam("file-size", true, false)->value().toInt(); } // register progress callback function Update.onProgress(updateFirmwareProgress); updateSuccessful = false; //file name checks if (filename.startsWith("ata_fw")) { isFirmware = true; if (!Update.begin(fileSize, U_FLASH, BoardLED2)) { ESP_LOGD(tag, "Firmware update failed to start"); return; } ESP_LOGD(tag, "Updating Firmware with: %s Size:%d ", filename.c_str(), fileSize); } else if (filename.startsWith("ata_fs")) { isFirmware = false; if (!Update.begin(fileSize, U_SPIFFS, BoardLED2)) { ESP_LOGD(tag, "LittleFS update failed to start"); return; } ESP_LOGD(tag, "Updating File System with: %s Size:%d ", filename.c_str(), fileSize); } else { ESP_LOGD(tag, "Unsupported file type"); return; } } // Writing... if (!Update.hasError()) { if (Update.write(data, len) != len) { Update.printError(Serial); } } // All Done... if (final) { vTaskDelay(100); if(isFirmware) { // firmware update if (Update.end(true)) { RebootSystem = true; updateSuccessful = true; ESP_LOGD(tag, "firmware update successful: %d", index + len); } else { Update.printError(Serial); } } else{ // file system update if(Update.end(true)) { updateSuccessful = true; ESP_LOGD(tag, "file system update successful!"); } else { Update.printError(Serial); } } } } // Server requested files that aren't template processed void handleGET_SendFile(AsyncWebServerRequest *request)// try this later "^/(img|favicon).*" { String filePath = request->url(); const char* ext = getFileExtension(filePath.c_str()); const char* contentType = getFileType(ext); if( contentType == NULL ){ request->send(404); return; } if(filePath.c_str()[0] != '/'){ filePath = '/' + filePath; } if (!strcmp(ext,"html") || !strcmp(ext, "htm") || !strcmp(ext, "css") || !strcmp(ext, "js")) { if(!filePath.startsWith("/www/")){ filePath = "/www" + filePath; } } ESP_LOGD(tag, "Sent: %s", filePath.c_str()); request->send(LittleFS, filePath, contentType); } String listDir(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; } char* readFile(fs::FS &fs, const char* path) { File file = fs.open(path, "r"); if (!file || file.isDirectory()) { return nullptr; } size_t fileSize = file.size(); char* fileContent = new char[fileSize + 1]; // +1 for null-terminator size_t bytesRead = file.readBytes(fileContent, fileSize); file.close(); fileContent[bytesRead] = '\0'; // Set null-terminating character if (bytesRead != fileSize) { delete[] fileContent; return nullptr; } return fileContent; } void writeFile(fs::FS &fs, const char * path, const char * message) { File file = fs.open(path, "w"); if(!file){ return; } file.print(message); file.close(); } // Send html file with template processing {{VAR}} void sendHtmlFile(const char* filePath, AsyncWebServerRequest *request, String (*callback)(const String&)) { ESP_LOGD(tag, "Sent file: %s", filePath); File file = LittleFS.open(filePath, "r"); const char* htmlFile = readFile(LittleFS, filePath); //String processedData = varReplace(htmlFile, htmlProcessor); String processedData = varReplace(htmlFile, callback); request->send(200, "text/html", processedData); } const char* convertFileSize(const size_t bytes) { static char fileSizeBuffer[16]; // PreAllocated 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", static_cast(bytes) / 1024.0); } else if (bytes < 1024*1024*1024) { snprintf(fileSizeBuffer, sizeof(fileSizeBuffer), "%.2f MB", static_cast(bytes) / 1048576.0); } else { fileSizeBuffer[0] = '\0'; // Empty string if the file size is too large } return fileSizeBuffer; } // Callback template processor String htmlProcessor(const String& var) { if(var == "NAVBAR"){ return String("\n"); } 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 == "LISTED_FILES"){ filesDropdownOptions = ""; // clear out dirDropdownOptions = ""; // clear out String directories[8]; int dirCount = 0; getAllDirectories(directories, dirCount); return listDir(directories, dirCount); } if(var == "EDIT-DEL_FILES"){ return filesDropdownOptions; } if(var == "DIR_LIST"){ return dirDropdownOptions; } if(var == "SAVE_PATH_INPUT"){ if(savePath == "/new.txt"){ return String(""; } else{ return String(""; } } if(var == "FIRM_VER"){ return FIRMWARE_VER; } return var; } String HomeHtmlProcessor(const String& var) { if(var == "NAVBAR"){ return String("\n"); } if (var == "APP_NAME") { return sysProps.appName; } if (var == "OLED") { return "No"; } if (var == "STRIP1") { return (strip1) ? "Yes" : "No"; } if (var == "STRIP2") { return (strip2) ? "Yes" : "No"; } if (var == "FRONT_LIGHT") { return (animProps.frontLight.enabled) ? "Yes" : "No"; } if (var == "REAR_LIGHT") { return (animProps.rearLight.enabled) ? "Yes" : "No"; } if (var == "FIRMWARE") { return FIRMWARE_VER; } if (var == "BOOTH_T") { return String(sysProps.t_sensor.temperature) + "F"; } if (var == "SETPOINT") { return String(sysProps.t_sensor.Setpoint1) + "F"; } 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 : ""; } if (var == "BLE_CLIENTS") { return (BTDeviceConnected) ? "1" : "0"; } if (var == "AP_MAC") { return getSoftAPMacAddress(); } // Return an empty string if the variable is not recognized return var; } 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; } // Finds segments between {{VAR}} and calls a callback function to replace VAR with new content String varReplace(const String& input, String (*callback)(const String&)) { if (input.isEmpty()) { return input; } String result; int startPos = 0; int start = input.indexOf("{{", startPos); while (start != -1) { result += input.substring(startPos, start); int end = input.indexOf("}}", start + 2); if (end == -1) { break; } String segment = input.substring(start + 2, end); int segmentLength = segment.length(); if (segmentLength <= 32) { String replacement = callback(segment); result += replacement; } else { result += input.substring(start, end + 2); // Include the original segment if it exceeds the limit } startPos = end + 2; start = input.indexOf("{{", startPos); } result += input.substring(startPos); return result; } const char* getFileExtension(const char* filename) { size_t dotPos = strlen(filename); while (dotPos > 0 && filename[dotPos - 1] != '.') { dotPos--; } if (dotPos != 0 && dotPos < strlen(filename) - 1) { return &filename[dotPos]; } return ""; // Empty string if no extension found } const char* getFileType(const char* ext) { if (strcmp(ext, "png") == 0) { return "image/png"; } else if (strcmp(ext, "jpg") == 0 || strcmp(ext, "jpeg") == 0) { return "image/jpeg"; } else if (strcmp(ext, "gif") == 0) { return "image/gif"; } else if (strcmp(ext, "ico") == 0) { return "image/x-icon"; } else if (strcmp(ext, "txt") == 0) { return "text/plain"; } else if (strcmp(ext, "css") == 0) { return "text/css"; } else if (strcmp(ext, "htm") == 0 || strcmp(ext, "html") == 0) { return "text/html"; } else if (strcmp(ext, "js") == 0) { return "text/javascript"; } else if (strcmp(ext, "json") == 0) { return "application/json"; } else { return ""; } } // Serial print firmware or file system update progress void updateFirmwareProgress(size_t progress, size_t total) { static int lastProg = -1; int firmwareProgress = (progress * 100)/ total; if(firmwareProgress != lastProg){ Serial.printf("Progress: %u%%\r", firmwareProgress); lastProg = firmwareProgress; //TODO Add buzzer tune while uploading firmware //Buzzer_Play_Tune(TUNE_DOWNLOADING,1,true,false); } }