#include "globals.h" #include "webserver.h" // Maps settings for which a validator is available to the invocation thereof const std::map CWebServer::settingValidators {}; std::vector CWebServer::deviceSettingSpecs{}; // Member function template specialzations // Push param that represents a bool. Values considered true are text "true" and any whole number not equal to 0 template<> bool CWebServer::PushPostParamIfPresent(AsyncWebServerRequest * pRequest, const String ¶mName, ValueSetter setter) { return PushPostParamIfPresent(pRequest, paramName, setter, [](AsyncWebParameter * param) constexpr { const String& value = param->value(); return value == "true" || strtol(value.c_str(), NULL, 10); }); } // Push param that represents a size_t template<> bool CWebServer::PushPostParamIfPresent(AsyncWebServerRequest * pRequest, const String ¶mName, ValueSetter setter) { return PushPostParamIfPresent(pRequest, paramName, setter, [](AsyncWebParameter * param) constexpr { return strtoul(param->value().c_str(), NULL, 10); }); } // Add CORS header to and send JSON response template<> void CWebServer::AddCORSHeaderAndSendResponse(AsyncWebServerRequest * pRequest, AsyncJsonResponse * pResponse) { pResponse->setLength(); AddCORSHeaderAndSendResponse(pRequest, pResponse); } // Member function implementations bool CWebServer::IsPostParamTrue(AsyncWebServerRequest * pRequest, const String & paramName) { bool returnValue = false; PushPostParamIfPresent(pRequest, paramName, [&returnValue](auto value) { returnValue = value; return true; }); return returnValue; } void CWebServer::GetStatistics(AsyncWebServerRequest * pRequest) { log_v("GetStatistics"); auto response = new AsyncJsonResponse(false, JSON_BUFFER_BASE_SIZE); auto j = response->getRoot(); j["HEAP_SIZE"] = _staticStats.HeapSize; j["HEAP_FREE"] = ESP.getFreeHeap(); j["HEAP_MIN"] = ESP.getMinFreeHeap(); j["DMA_SIZE"] = _staticStats.DmaHeapSize; j["DMA_FREE"] = heap_caps_get_free_size(MALLOC_CAP_DMA); j["DMA_MIN"] = heap_caps_get_largest_free_block(MALLOC_CAP_DMA); j["PSRAM_SIZE"] = _staticStats.PsramSize; j["PSRAM_FREE"] = ESP.getFreePsram(); j["PSRAM_MIN"] = ESP.getMinFreePsram(); j["CHIP_MODEL"] = _staticStats.ChipModel; j["CHIP_CORES"] = _staticStats.ChipCores; j["CHIP_SPEED"] = _staticStats.CpuFreqMHz; j["PROG_SIZE"] = _staticStats.SketchSize; j["CODE_SIZE"] = _staticStats.SketchSize; j["CODE_FREE"] = _staticStats.FreeSketchSpace; j["FLASH_SIZE"] = _staticStats.FlashChipSize; j["CPU_USED"] = g_TaskManager.GetCPUUsagePercent(); j["CPU_USED_CORE0"] = g_TaskManager.GetCPUUsagePercent(0); j["CPU_USED_CORE1"] = g_TaskManager.GetCPUUsagePercent(1); AddCORSHeaderAndSendResponse(pRequest, response); } void CWebServer::SendSettingSpecsResponse(AsyncWebServerRequest * pRequest, const std::vector & settingSpecs) { static size_t jsonBufferSize = JSON_BUFFER_BASE_SIZE; bool bufferOverflow; do { bufferOverflow = false; auto response = std::make_unique(false, jsonBufferSize); auto jsonArray = response->getRoot().to(); for (auto& spec : settingSpecs) { auto specObject = jsonArray.createNestedObject(); StaticJsonDocument<384> jsonDoc; jsonDoc["name"] = spec.Name; jsonDoc["friendlyName"] = spec.FriendlyName; jsonDoc["description"] = spec.Description; jsonDoc["type"] = to_value(spec.Type); jsonDoc["typeName"] = spec.ToName(spec.Type); if (!specObject.set(jsonDoc.as())) { bufferOverflow = true; jsonBufferSize += JSON_BUFFER_INCREMENT; log_v("JSON response buffer overflow! Increased buffer to %zu bytes", jsonBufferSize); break; } } if (!bufferOverflow) AddCORSHeaderAndSendResponse(pRequest, response.release()); } while (bufferOverflow); } const std::vector & CWebServer::LoadDeviceSettingSpecs() { if (deviceSettingSpecs.size() == 0) { auto deviceConfigSpecs = g_ptrDeviceConfig->GetSettingSpecs(); deviceSettingSpecs.insert(deviceSettingSpecs.end(), deviceConfigSpecs.begin(), deviceConfigSpecs.end()); } return deviceSettingSpecs; } void CWebServer::GetSettingSpecs(AsyncWebServerRequest * pRequest) { SendSettingSpecsResponse(pRequest, LoadDeviceSettingSpecs()); } // Responds with current config, excluding any sensitive values void CWebServer::GetSettings(AsyncWebServerRequest * pRequest) { log_v("GetSettings"); auto response = new AsyncJsonResponse(false, JSON_BUFFER_BASE_SIZE); response->addHeader("Server","NightDriverStrip"); auto root = response->getRoot(); JsonObject jsonObject = root.to(); // We get the serialized JSON for the device config, without any sensitive values g_ptrDeviceConfig->SerializeToJSON(jsonObject, false); AddCORSHeaderAndSendResponse(pRequest, response); } // Support function that silently sets whatever settings are included in the request passed. // Composing a response is left to the invoker! void CWebServer::SetSettingsIfPresent(AsyncWebServerRequest * pRequest) { PushPostParamIfPresent(pRequest, DeviceConfig::NTPServerTag, SET_VALUE(g_ptrDeviceConfig->SetNTPServer(value))); } // Set settings and return resulting config void CWebServer::SetSettings(AsyncWebServerRequest * pRequest) { log_v("SetSettings"); SetSettingsIfPresent(pRequest); // We return the current config in response GetSettings(pRequest); } // Save the posted file to SPIFFS void CWebServer::SaveFile(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) { log_v("SaveFile"); } #if USE_OAT // Update the current firmware void CWebServer::UpdateFileSystemImage(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { log_v("UpdateFileSystemImage"); HandleUpdate(request, filename, index, data, len, final, (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000, U_FLASH); } // Update the current firmware void CWebServer::UpdateFirmware(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { log_v("UpdateFirmware"); HandleUpdate(request, filename, index, data, len, final, UPDATE_SIZE_UNKNOWN, U_SPIFFS); } // Handles updating the filesystem image or firmware void CWebServer::HandleUpdate(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final, size_t upload_size, uint8_t upload_index) { if (!index) { log_i("Update Start: %s\n", filename.c_str()); log_i("Uploading: %d", upload_index); if (!Update.begin(upload_size, upload_index)) { Update.printError(Serial); } } if (!Update.hasError()) { if (Update.write(data, len) != len) { Update.printError(Serial); } } if (final) { if (Update.end(true)) { log_i("Update Success: %uB\n", index + len); } else { Update.printError(Serial); } } } // Ensures the request gets send to the client void CWebServer::UpdateRequestHandler(AsyncWebServerRequest* request){ bool success = !Update.hasError(); AsyncWebServerResponse* response = request->beginResponse(200, "text/plain", success ? "OK" : "FAIL"); response->addHeader("Connection", "close"); request->send(response); if (success) { delay(250); ESP.restart(); } } #endif // Reset effect config, device config and/or the board itself void CWebServer::Reset(AsyncWebServerRequest * pRequest) { if (IsPostParamTrue(pRequest, "deviceConfig")) { log_i("Removing DeviceConfig"); g_ptrDeviceConfig->RemovePersisted(); } bool boardResetRequested = IsPostParamTrue(pRequest, "board"); AddCORSHeaderAndSendOKResponse(pRequest); if (boardResetRequested) { log_w("Resetting device at API request!"); delay(1000); // Give the response a second to be sent throw new std::runtime_error("Resetting device at API request"); } } String dir(void) { String x = ""; File root = SPIFFS.open("/"); if(!root) { return "Failed to open directory"; } if(!root.isDirectory()) { return "Not a directory"; } File file = root.openNextFile(); while(file) { x += "
"+String(file.name()); if(file.isDirectory()){ x += "DIR"; } else { x += ""+String(file.size()); } file = root.openNextFile(); } x += "
Occupied space"+String(SPIFFS.usedBytes()); x += "
Total space"+String(SPIFFS.totalBytes()); return x+"
"; } // Send Gzip SPA void CWebServer::GzipSpa(AsyncWebServerRequest * pRequest) { if(!SPIFFS.exists("/index.html.gz")){ log_e("Gzipped SPA not found"); pRequest->send(404, "text/html", dir()); return; } AsyncWebServerResponse *response = pRequest->beginResponse(SPIFFS, "/index.html.gz", "text/html", false); response->addHeader("Content-Encoding", "gzip"); response->addHeader("Cache-Control"," max-age=86400"); AddCORSHeaderAndSendResponse(pRequest, response); } enum Action { SERVO_UPDATE, }; void handleWebSocketJson(uint8_t* data) { const uint8_t size = JSON_OBJECT_SIZE(1); StaticJsonDocument<100> json; DeserializationError err = deserializeJson(json, data); if (err) { Serial.print(F("deserializeJson() failed with code ")); Serial.println(err.c_str()); return; } const uint8_t action = json["action"]; if(action == 0) { const uint8_t servoIndex = json["servo"]; const int16_t pwm = json["pwm"]; log_i("Moving servo:%u to pwm%u", servoIndex, pwm); g_ptrServo->setPWM(servoIndex, 0, pwm); } } void handleWebSocketBuffer(uint8_t* data) { if(data[0] == 0) g_ptrServo->setBody(data[1], data[2], data[3], data[4], data[5], data[6]); else if(data[0] == 1) { log_i("About to update all servos"); int16_t* angles = (int16_t*)data+1; g_ptrServo->SetAngles(angles); } } void handleWebSocketMessage(void* arg, uint8_t* data, size_t len, AsyncWebSocket* server, AsyncWebSocketClient* client) { AwsFrameInfo* info = (AwsFrameInfo*)arg; if(info->final && info->index == 0 && info->len == len){ if(info->opcode == WS_TEXT) handleWebSocketJson(data); else handleWebSocketBuffer(data); } } // Reset effect config, device config and/or the board itself void CWebServer::HandleWsMessage(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { //if (type == WS_EVT_CONNECT) handleNewConnection(server, client); /*else*/ if (type == WS_EVT_DISCONNECT) log_i("ws[%s][%u] disconnect\n", server->url(), client->id()); else if (type == WS_EVT_ERROR) log_i("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); else if (type == WS_EVT_PONG) log_i("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len) ? (char*)data : ""); else if (type == WS_EVT_DATA) handleWebSocketMessage(arg, data, len, server, client); }