From 1a260a85d666308ceec4f953aa2a50873d014c2e Mon Sep 17 00:00:00 2001 From: thedemoncat Date: Thu, 12 Mar 2026 10:40:40 +0600 Subject: [PATCH 1/3] =?UTF-8?q?Migration=20PsychicHttp=20=E2=86=92=20ESPAs?= =?UTF-8?q?yncWebServer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/framework/APSettingsService.cpp | 2 +- lib/framework/APSettingsService.h | 4 +- lib/framework/APStatus.cpp | 11 +- lib/framework/APStatus.h | 9 +- lib/framework/AuthenticationService.cpp | 23 +-- lib/framework/AuthenticationService.h | 6 +- lib/framework/CoreDump.cpp | 65 ++----- lib/framework/CoreDump.h | 8 +- lib/framework/DownloadFirmwareService.cpp | 12 +- lib/framework/DownloadFirmwareService.h | 8 +- lib/framework/ESP32SvelteKit.cpp | 56 +++---- lib/framework/ESP32SvelteKit.h | 9 +- lib/framework/EthernetSettingsService.cpp | 2 +- lib/framework/EthernetSettingsService.h | 6 +- lib/framework/EthernetStatus.cpp | 11 +- lib/framework/EthernetStatus.h | 9 +- lib/framework/EventEndpoint.h | 2 +- lib/framework/EventSocket.cpp | 108 +++++++----- lib/framework/EventSocket.h | 17 +- lib/framework/FactoryResetService.cpp | 8 +- lib/framework/FactoryResetService.h | 8 +- lib/framework/FeaturesService.cpp | 11 +- lib/framework/FeaturesService.h | 7 +- lib/framework/HttpEndpoint.h | 88 +++++----- lib/framework/MqttSettingsService.cpp | 2 +- lib/framework/MqttSettingsService.h | 4 +- lib/framework/MqttStatus.cpp | 11 +- lib/framework/MqttStatus.h | 9 +- lib/framework/NTPSettingsService.cpp | 9 +- lib/framework/NTPSettingsService.h | 6 +- lib/framework/NTPStatus.cpp | 11 +- lib/framework/NTPStatus.h | 9 +- lib/framework/RestartService.cpp | 9 +- lib/framework/RestartService.h | 8 +- lib/framework/SecurityManager.h | 14 +- lib/framework/SecuritySettingsService.cpp | 92 +++++----- lib/framework/SecuritySettingsService.h | 24 +-- lib/framework/SleepService.cpp | 12 +- lib/framework/SleepService.h | 8 +- lib/framework/SystemStatus.cpp | 11 +- lib/framework/SystemStatus.h | 9 +- lib/framework/UploadFirmwareService.cpp | 196 ++++++++++------------ lib/framework/UploadFirmwareService.h | 79 ++------- lib/framework/WebSocketServer.h | 95 ++++++----- lib/framework/WiFiScanner.cpp | 19 ++- lib/framework/WiFiScanner.h | 11 +- lib/framework/WiFiSettingsService.cpp | 2 +- lib/framework/WiFiSettingsService.h | 6 +- lib/framework/WiFiStatus.cpp | 11 +- lib/framework/WiFiStatus.h | 9 +- platformio.ini | 194 +++++++++++---------- src/LightMqttSettingsService.cpp | 2 +- src/LightMqttSettingsService.h | 2 +- src/LightStateService.cpp | 2 +- src/LightStateService.h | 2 +- src/main.cpp | 6 +- 56 files changed, 659 insertions(+), 715 deletions(-) diff --git a/lib/framework/APSettingsService.cpp b/lib/framework/APSettingsService.cpp index 76b87c91a..7c695062e 100644 --- a/lib/framework/APSettingsService.cpp +++ b/lib/framework/APSettingsService.cpp @@ -14,7 +14,7 @@ #include -APSettingsService::APSettingsService(PsychicHttpServer *server, +APSettingsService::APSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager) : _server(server), _securityManager(securityManager), diff --git a/lib/framework/APSettingsService.h b/lib/framework/APSettingsService.h index 354fa9ba1..8de614fe4 100644 --- a/lib/framework/APSettingsService.h +++ b/lib/framework/APSettingsService.h @@ -146,7 +146,7 @@ class APSettings class APSettingsService : public StatefulService { public: - APSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager); + APSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager); void begin(); void loop(); @@ -154,7 +154,7 @@ class APSettingsService : public StatefulService void recoveryMode(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; diff --git a/lib/framework/APStatus.cpp b/lib/framework/APStatus.cpp index 38818d52d..cc186c7a1 100644 --- a/lib/framework/APStatus.cpp +++ b/lib/framework/APStatus.cpp @@ -14,7 +14,7 @@ #include -APStatus::APStatus(PsychicHttpServer *server, +APStatus::APStatus(AsyncWebServer *server, SecurityManager *securityManager, APSettingsService *apSettingsService) : _server(server), _securityManager(securityManager), @@ -31,17 +31,18 @@ void APStatus::begin() ESP_LOGV(SVK_TAG, "Registered GET endpoint: %s", AP_STATUS_SERVICE_PATH); } -esp_err_t APStatus::apStatus(PsychicRequest *request) +void APStatus::apStatus(AsyncWebServerRequest *request) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); root["status"] = _apSettingsService->getAPNetworkStatus(); root["ip_address"] = WiFi.softAPIP().toString(); root["mac_address"] = WiFi.softAPmacAddress(); root["station_num"] = WiFi.softAPgetStationNum(); - return response.send(); + response->setLength(); + request->send(response); } bool APStatus::isActive() diff --git a/lib/framework/APStatus.h b/lib/framework/APStatus.h index f7fbe202e..d4ef83051 100644 --- a/lib/framework/APStatus.h +++ b/lib/framework/APStatus.h @@ -18,7 +18,8 @@ #include #include -#include +#include +#include #include #include #include @@ -28,17 +29,17 @@ class APStatus { public: - APStatus(PsychicHttpServer *server, SecurityManager *securityManager, APSettingsService *apSettingsService); + APStatus(AsyncWebServer *server, SecurityManager *securityManager, APSettingsService *apSettingsService); void begin(); bool isActive(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; APSettingsService *_apSettingsService; - esp_err_t apStatus(PsychicRequest *request); + void apStatus(AsyncWebServerRequest *request); }; #endif // end APStatus_h diff --git a/lib/framework/AuthenticationService.cpp b/lib/framework/AuthenticationService.cpp index dbc3cc971..9e2d2c07f 100644 --- a/lib/framework/AuthenticationService.cpp +++ b/lib/framework/AuthenticationService.cpp @@ -13,39 +13,40 @@ **/ #include +#include #if FT_ENABLED(FT_SECURITY) -AuthenticationService::AuthenticationService(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server), - _securityManager(securityManager) +AuthenticationService::AuthenticationService(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), + _securityManager(securityManager) { } void AuthenticationService::begin() { - // Signs in a user if the username and password match. Provides a JWT to be used in the Authorization header in subsequent requests - _server->on(SIGN_IN_PATH, HTTP_POST, [this](PsychicRequest *request, JsonVariant &json) + _server->on(SIGN_IN_PATH, HTTP_POST, [this](AsyncWebServerRequest *request, JsonVariant &json) { if (json.is()) { String username = json["username"]; String password = json["password"]; Authentication authentication = _securityManager->authenticate(username, password); if (authentication.authenticated) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); root["access_token"] = _securityManager->generateJWT(authentication.user); - return response.send(); + response->setLength(); + request->send(response); + return; } } - return request->reply(401); }); + request->send(401); }); ESP_LOGV(SVK_TAG, "Registered POST endpoint: %s", SIGN_IN_PATH); - // Verifies that the request supplied a valid JWT - _server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, [this](PsychicRequest *request) + _server->on(VERIFY_AUTHORIZATION_PATH, HTTP_GET, [this](AsyncWebServerRequest *request) { Authentication authentication = _securityManager->authenticateRequest(request); - return request->reply(authentication.authenticated ? 200 : 401); }); + request->send(authentication.authenticated ? 200 : 401); }); ESP_LOGV(SVK_TAG, "Registered GET endpoint: %s", VERIFY_AUTHORIZATION_PATH); } diff --git a/lib/framework/AuthenticationService.h b/lib/framework/AuthenticationService.h index f3c710cb3..66a3f4880 100644 --- a/lib/framework/AuthenticationService.h +++ b/lib/framework/AuthenticationService.h @@ -16,7 +16,7 @@ **/ #include -#include +#include #include #define VERIFY_AUTHORIZATION_PATH "/rest/verifyAuthorization" @@ -27,13 +27,13 @@ class AuthenticationService { public: - AuthenticationService(PsychicHttpServer *server, SecurityManager *securityManager); + AuthenticationService(AsyncWebServer *server, SecurityManager *securityManager); void begin(); private: SecurityManager *_securityManager; - PsychicHttpServer *_server; + AsyncWebServer *_server; }; #endif // end FT_ENABLED(FT_SECURITY) diff --git a/lib/framework/CoreDump.cpp b/lib/framework/CoreDump.cpp index 5667b6923..ca57ca106 100644 --- a/lib/framework/CoreDump.cpp +++ b/lib/framework/CoreDump.cpp @@ -21,7 +21,7 @@ #define MIN(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; }) -CoreDump::CoreDump(PsychicHttpServer *server, +CoreDump::CoreDump(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), _securityManager(securityManager) { @@ -37,60 +37,29 @@ void CoreDump::begin() ESP_LOGV("CoreDump", "Registered GET endpoint: %s", CORE_DUMP_SERVICE_PATH); } -esp_err_t CoreDump::coreDump(PsychicRequest *request) +void CoreDump::coreDump(AsyncWebServerRequest *request) { size_t coredump_addr; size_t coredump_size; esp_err_t err = esp_core_dump_image_get(&coredump_addr, &coredump_size); if (err != ESP_OK) { - request->reply(500, "application/json", "{\"status\":\"error\",\"message\":\"core dump not available\"}"); - return err; + request->send(500, "application/json", "{\"status\":\"error\",\"message\":\"core dump not available\"}"); + return; } - size_t const chunk_len = 3 * 16; // must be multiple of 3 - size_t const b64_len = chunk_len / 3 * 4 + 4; - uint8_t *const chunk = (uint8_t *)malloc(chunk_len); - char *const b64 = (char *)malloc(b64_len); - assert(chunk && b64); - - /*if (write_cfg->start) { - if ((err = write_cfg->start(write_cfg->priv)) != ESP_OK) { - return err; - } - }*/ - ESP_LOGI(SVK_TAG, "Coredump is %u bytes", coredump_size); - httpd_resp_set_status(request->request(), "200 OK"); - PsychicResponse response(request); - response.setCode(200); - response.setContentType("application/octet-stream"); - response.sendHeaders(); - for (size_t offset = 0; offset < coredump_size; offset += chunk_len) - { - uint const read_len = MIN(chunk_len, coredump_size - offset); - if (esp_flash_read(esp_flash_default_chip, chunk, coredump_addr + offset, read_len)) - { - ESP_LOGE(SVK_TAG, "Coredump read failed"); - break; - } - err = response.sendChunk(chunk, read_len); - if (err != ESP_OK) - { - break; - } - } - free(chunk); - free(b64); - - err = response.finishChunking(); - /*uint32_t sec_num = coredump_size / SPI_FLASH_SEC_SIZE; - if (coredump_size % SPI_FLASH_SEC_SIZE) { - sec_num++; - } - err = esp_flash_erase_region(esp_flash_default_chip, coredump_addr, sec_num * SPI_FLASH_SEC_SIZE); - if (err != ESP_OK) { - ESP_LOGE(SVK_TAG, "Failed to erase coredump (%d)!", err); - }*/ - return err; + AsyncWebServerResponse *response = request->beginChunkedResponse("application/octet-stream", + [coredump_addr, coredump_size](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + if (index >= coredump_size) { + return 0; + } + size_t read_len = MIN(maxLen, coredump_size - index); + if (esp_flash_read(esp_flash_default_chip, buffer, coredump_addr + index, read_len)) { + ESP_LOGE(SVK_TAG, "Coredump read failed"); + return 0; + } + return read_len; + }); + request->send(response); } \ No newline at end of file diff --git a/lib/framework/CoreDump.h b/lib/framework/CoreDump.h index b21cba9b9..9c6045ed4 100644 --- a/lib/framework/CoreDump.h +++ b/lib/framework/CoreDump.h @@ -17,7 +17,7 @@ #include #include -#include +#include #include #include @@ -26,14 +26,14 @@ class CoreDump { public: - CoreDump(PsychicHttpServer *server, SecurityManager *securityManager); + CoreDump(AsyncWebServer *server, SecurityManager *securityManager); void begin(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; - esp_err_t coreDump(PsychicRequest *request); + void coreDump(AsyncWebServerRequest *request); }; #endif // end CoreDump_h \ No newline at end of file diff --git a/lib/framework/DownloadFirmwareService.cpp b/lib/framework/DownloadFirmwareService.cpp index 672281f7d..66162e304 100644 --- a/lib/framework/DownloadFirmwareService.cpp +++ b/lib/framework/DownloadFirmwareService.cpp @@ -183,7 +183,7 @@ void updateTask(void *param) vTaskDelete(NULL); } -DownloadFirmwareService::DownloadFirmwareService(PsychicHttpServer *server, +DownloadFirmwareService::DownloadFirmwareService(AsyncWebServer *server, SecurityManager *securityManager, EventSocket *socket) : _server(server), _securityManager(securityManager), @@ -209,11 +209,12 @@ void DownloadFirmwareService::begin() ESP_LOGV(SVK_TAG, "Registered POST endpoint: %s", GITHUB_FIRMWARE_PATH); } -esp_err_t DownloadFirmwareService::downloadUpdate(PsychicRequest *request, JsonVariant &json) +void DownloadFirmwareService::downloadUpdate(AsyncWebServerRequest *request, JsonVariant &json) { if (!json.is()) { - return request->reply(400); + request->send(400); + return; } String downloadURL = json["download_url"]; @@ -249,7 +250,8 @@ esp_err_t DownloadFirmwareService::downloadUpdate(PsychicRequest *request, JsonV #ifdef SERIAL_INFO Serial.println("Couldn't create download OTA task"); #endif - return request->reply(500); + request->send(500); + return; } - return request->reply(200); + request->send(200); } diff --git a/lib/framework/DownloadFirmwareService.h b/lib/framework/DownloadFirmwareService.h index 48558a514..917e03ce5 100644 --- a/lib/framework/DownloadFirmwareService.h +++ b/lib/framework/DownloadFirmwareService.h @@ -19,7 +19,7 @@ #include #include #include -#include +#include #include #include @@ -32,13 +32,13 @@ class DownloadFirmwareService { public: - DownloadFirmwareService(PsychicHttpServer *server, SecurityManager *securityManager, EventSocket *socket); + DownloadFirmwareService(AsyncWebServer *server, SecurityManager *securityManager, EventSocket *socket); void begin(); private: SecurityManager *_securityManager; - PsychicHttpServer *_server; + AsyncWebServer *_server; EventSocket *_socket; - esp_err_t downloadUpdate(PsychicRequest *request, JsonVariant &json); + void downloadUpdate(AsyncWebServerRequest *request, JsonVariant &json); }; diff --git a/lib/framework/ESP32SvelteKit.cpp b/lib/framework/ESP32SvelteKit.cpp index 5eb5ff2d8..539dd0e41 100644 --- a/lib/framework/ESP32SvelteKit.cpp +++ b/lib/framework/ESP32SvelteKit.cpp @@ -14,9 +14,8 @@ #include -ESP32SvelteKit::ESP32SvelteKit(PsychicHttpServer *server, unsigned int numberEndpoints) : _server(server), - _numberEndpoints(numberEndpoints), - _featureService(server, &_socket), +ESP32SvelteKit::ESP32SvelteKit(AsyncWebServer *server) : _server(server), + _featureService(server, &_socket), _securitySettingsService(server, &ESPFS), _wifiSettingsService(server, &ESPFS, &_securitySettingsService, &_socket), _wifiScanner(server, &_securitySettingsService), @@ -75,10 +74,7 @@ void ESP32SvelteKit::begin() _wifiSettingsService.initWiFi(); - // SvelteKit uses a lot of handlers, so we need to increase the max_uri_handlers - // WWWData has 77 Endpoints, Framework has 27, and Lighstate Demo has 4 - _server->config.max_uri_handlers = _numberEndpoints; - _server->listen(80); + _server->begin(); #ifdef EMBED_WWW // Serve static resources from PROGMEM @@ -86,25 +82,25 @@ void ESP32SvelteKit::begin() WWWData::registerRoutes( [&](const String &uri, const String &contentType, const uint8_t *content, size_t len) { - PsychicHttpRequestCallback requestHandler = [contentType, content, len](PsychicRequest *request) - { - PsychicResponse response(request); - response.setCode(200); - response.setContentType(contentType.c_str()); - response.addHeader("Content-Encoding", "gzip"); - response.addHeader("Cache-Control", "public, immutable, max-age=31536000"); - response.setContent(content, len); - return response.send(); - }; - PsychicWebHandler *handler = new PsychicWebHandler(); - handler->onRequest(requestHandler); - _server->on(uri.c_str(), HTTP_GET, handler); + _server->on(uri.c_str(), HTTP_GET, [contentType, content, len](AsyncWebServerRequest *request) + { + AsyncWebServerResponse *response = request->beginResponse(200, contentType, content, len); + response->addHeader("Content-Encoding", "gzip"); + response->addHeader("Cache-Control", "public, immutable, max-age=31536000"); + request->send(response); }); - // Set default end-point for all non matching requests - // this is easier than using webServer.onNotFound() if (uri.equals("/index.html")) { - _server->defaultEndpoint->setHandler(handler); + _server->onNotFound([content, len](AsyncWebServerRequest *request) + { + if (request->method() == HTTP_GET) { + AsyncWebServerResponse *response = request->beginResponse(200, "text/html", content, len); + response->addHeader("Content-Encoding", "gzip"); + response->addHeader("Cache-Control", "public, immutable, max-age=31536000"); + request->send(response); + } else { + request->send(404); + } }); } }); #else @@ -112,14 +108,12 @@ void ESP32SvelteKit::begin() ESP_LOGV(SVK_TAG, "Registering routes from FS /www/ static resources"); _server->serveStatic("/_app/", ESPFS, "/www/_app/"); _server->serveStatic("/favicon.png", ESPFS, "/www/favicon.png"); - // Serving all other get requests with "/www/index.htm" - _server->onNotFound([](PsychicRequest *request) + _server->onNotFound([](AsyncWebServerRequest *request) { if (request->method() == HTTP_GET) { - PsychicFileResponse response(request, ESPFS, "/www/index.html", "text/html"); - return response.send(); - // String url = "http://" + request->host() + "/index.html"; - // request->redirect(url.c_str()); + request->send(ESPFS, "/www/index.html", "text/html"); + } else { + request->send(404); } }); #endif @@ -195,10 +189,6 @@ void ESP32SvelteKit::begin() _sleepService.begin(); _sleepService.attachOnSleepCallback([&]() { ESP_LOGI(SVK_TAG, "Attempting to stop server"); - for (auto client : _server->getClientList()) - { - client->close(); - } vTaskDelete(_loopTaskHandle); ESP_LOGI(SVK_TAG, "Server stopped"); }); #if FT_ENABLED(FT_MQTT) diff --git a/lib/framework/ESP32SvelteKit.h b/lib/framework/ESP32SvelteKit.h index 804201b56..11befcc2f 100644 --- a/lib/framework/ESP32SvelteKit.h +++ b/lib/framework/ESP32SvelteKit.h @@ -45,7 +45,7 @@ #include #include #include -#include +#include #include #ifdef EMBED_WWW @@ -89,7 +89,7 @@ enum class ConnectionStatus class ESP32SvelteKit { public: - ESP32SvelteKit(PsychicHttpServer *server, unsigned int numberEndpoints = 115); + ESP32SvelteKit(AsyncWebServer *server); void begin(); @@ -103,7 +103,7 @@ class ESP32SvelteKit return &ESPFS; } - PsychicHttpServer *getServer() + AsyncWebServer *getServer() { return _server; } @@ -204,9 +204,8 @@ class ESP32SvelteKit } private: - PsychicHttpServer *_server; + AsyncWebServer *_server; TaskHandle_t _loopTaskHandle; - unsigned int _numberEndpoints; FeaturesService _featureService; SecuritySettingsService _securitySettingsService; WiFiSettingsService _wifiSettingsService; diff --git a/lib/framework/EthernetSettingsService.cpp b/lib/framework/EthernetSettingsService.cpp index 6b0ec505a..539f3efe0 100644 --- a/lib/framework/EthernetSettingsService.cpp +++ b/lib/framework/EthernetSettingsService.cpp @@ -16,7 +16,7 @@ #if FT_ENABLED(FT_ETHERNET) -EthernetSettingsService::EthernetSettingsService(PsychicHttpServer *server, +EthernetSettingsService::EthernetSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket) : _server(server), diff --git a/lib/framework/EthernetSettingsService.h b/lib/framework/EthernetSettingsService.h index 788ecb79f..9bd8c4a92 100644 --- a/lib/framework/EthernetSettingsService.h +++ b/lib/framework/EthernetSettingsService.h @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #ifndef FACTORY_ETHERNET_HOSTNAME @@ -104,7 +104,7 @@ class EthernetSettings class EthernetSettingsService : public StatefulService { public: - EthernetSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket); + EthernetSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket); void initEthernet(); void begin(); @@ -113,7 +113,7 @@ class EthernetSettingsService : public StatefulService String getIP(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; diff --git a/lib/framework/EthernetStatus.cpp b/lib/framework/EthernetStatus.cpp index 472088196..1a2b7e3b3 100644 --- a/lib/framework/EthernetStatus.cpp +++ b/lib/framework/EthernetStatus.cpp @@ -16,7 +16,7 @@ #if FT_ENABLED(FT_ETHERNET) -EthernetStatus::EthernetStatus(PsychicHttpServer *server, +EthernetStatus::EthernetStatus(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), _securityManager(securityManager) { @@ -61,10 +61,10 @@ void EthernetStatus::onGotIP(WiFiEvent_t event, WiFiEventInfo_t info) #endif } -esp_err_t EthernetStatus::ethernetStatus(PsychicRequest *request) +void EthernetStatus::ethernetStatus(AsyncWebServerRequest *request) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); bool isConnected = ETH.connected(); root["connected"] = isConnected; if (isConnected) @@ -86,7 +86,8 @@ esp_err_t EthernetStatus::ethernetStatus(PsychicRequest *request) root["link_speed"] = ETH.linkSpeed(); } - return response.send(); + response->setLength(); + request->send(response); } bool EthernetStatus::isConnected() diff --git a/lib/framework/EthernetStatus.h b/lib/framework/EthernetStatus.h index 38eb62ae9..2071742e7 100644 --- a/lib/framework/EthernetStatus.h +++ b/lib/framework/EthernetStatus.h @@ -19,7 +19,8 @@ #include #include -#include +#include +#include #include #include @@ -30,14 +31,14 @@ class EthernetStatus { public: - EthernetStatus(PsychicHttpServer *server, SecurityManager *securityManager); + EthernetStatus(AsyncWebServer *server, SecurityManager *securityManager); void begin(); bool isConnected(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; // static functions for logging Ethernet events to the UART @@ -45,7 +46,7 @@ class EthernetStatus static void onConnected(WiFiEvent_t event, WiFiEventInfo_t info); static void onDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); static void onGotIP(WiFiEvent_t event, WiFiEventInfo_t info); - esp_err_t ethernetStatus(PsychicRequest *request); + void ethernetStatus(AsyncWebServerRequest *request); }; #endif // end FT_ENABLED(FT_ETHERNET) diff --git a/lib/framework/EventEndpoint.h b/lib/framework/EventEndpoint.h index e35bcac83..4c70ed972 100644 --- a/lib/framework/EventEndpoint.h +++ b/lib/framework/EventEndpoint.h @@ -16,7 +16,7 @@ **/ #include -#include +#include #include #include diff --git a/lib/framework/EventSocket.cpp b/lib/framework/EventSocket.cpp index 84bfd4110..2311b69da 100644 --- a/lib/framework/EventSocket.cpp +++ b/lib/framework/EventSocket.cpp @@ -2,9 +2,10 @@ SemaphoreHandle_t clientSubscriptionsMutex = xSemaphoreCreateMutex(); -EventSocket::EventSocket(PsychicHttpServer *server, +EventSocket::EventSocket(AsyncWebServer *server, SecurityManager *securityManager, AuthenticationPredicate authenticationPredicate) : _server(server), + _socket(EVENT_SERVICE_PATH), _securityManager(securityManager), _authenticationPredicate(authenticationPredicate) { @@ -13,10 +14,11 @@ EventSocket::EventSocket(PsychicHttpServer *server, void EventSocket::begin() { _socket.setFilter(_securityManager->filterRequest(_authenticationPredicate)); - _socket.onOpen((std::bind(&EventSocket::onWSOpen, this, std::placeholders::_1))); - _socket.onClose(std::bind(&EventSocket::onWSClose, this, std::placeholders::_1)); - _socket.onFrame(std::bind(&EventSocket::onFrame, this, std::placeholders::_1, std::placeholders::_2)); - _server->on(EVENT_SERVICE_PATH, &_socket); + _socket.onEvent(std::bind(&EventSocket::onWSEvent, this, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6)); + _server->addHandler(&_socket); ESP_LOGV(SVK_TAG, "Registered event socket endpoint: %s", EVENT_SERVICE_PATH); } @@ -34,42 +36,68 @@ void EventSocket::registerEvent(String event) } } -void EventSocket::onWSOpen(PsychicWebSocketClient *client) +void EventSocket::onWSEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, + AwsEventType type, void *arg, uint8_t *data, size_t len) { - ESP_LOGI(SVK_TAG, "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->socket()); + switch (type) + { + case WS_EVT_CONNECT: + onWSOpen(client); + break; + case WS_EVT_DISCONNECT: + onWSClose(client); + break; + case WS_EVT_DATA: + { + AwsFrameInfo *info = (AwsFrameInfo *)arg; + if (info->final && info->index == 0 && info->len == len) + { + onFrame(client, info, data, len); + } + break; + } + default: + break; + } } -void EventSocket::onWSClose(PsychicWebSocketClient *client) +void EventSocket::onWSOpen(AsyncWebSocketClient *client) { + ESP_LOGI(SVK_TAG, "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->id()); +} + +void EventSocket::onWSClose(AsyncWebSocketClient *client) +{ + int clientId = (int)client->id(); xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY); for (auto &event_subscriptions : client_subscriptions) { - event_subscriptions.second.remove(client->socket()); + event_subscriptions.second.remove(clientId); } xSemaphoreGive(clientSubscriptionsMutex); - ESP_LOGI(SVK_TAG, "ws[%s][%u] disconnect", client->remoteIP().toString().c_str(), client->socket()); + ESP_LOGI(SVK_TAG, "ws[%s][%u] disconnect", client->remoteIP().toString().c_str(), client->id()); } -esp_err_t EventSocket::onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame) +void EventSocket::onFrame(AsyncWebSocketClient *client, AwsFrameInfo *info, uint8_t *data, size_t len) { - ESP_LOGV(SVK_TAG, "ws[%s][%u] opcode[%d]", request->client()->remoteIP().toString().c_str(), - request->client()->socket(), frame->type); + int clientId = (int)client->id(); + ESP_LOGV(SVK_TAG, "ws[%s][%u] opcode[%d]", client->remoteIP().toString().c_str(), client->id(), info->opcode); JsonDocument doc; #if FT_ENABLED(EVENT_USE_JSON) - if (frame->type == HTTPD_WS_TYPE_TEXT) + if (info->opcode == WS_TEXT) { - ESP_LOGV(SVK_TAG, "ws[%s][%u] request: %s", request->client()->remoteIP().toString().c_str(), - request->client()->socket(), (char *)frame->payload); + ESP_LOGV(SVK_TAG, "ws[%s][%u] request: %s", client->remoteIP().toString().c_str(), + client->id(), (char *)data); - DeserializationError error = deserializeJson(doc, (char *)frame->payload, frame->len); + DeserializationError error = deserializeJson(doc, (char *)data, len); #else - if (frame->type == HTTPD_WS_TYPE_BINARY) + if (info->opcode == WS_BINARY) { - ESP_LOGV(SVK_TAG, "ws[%s][%u] request: %s", request->client()->remoteIP().toString().c_str(), - request->client()->socket(), (char *)frame->payload); + ESP_LOGV(SVK_TAG, "ws[%s][%u] request: %s", client->remoteIP().toString().c_str(), + client->id(), (char *)data); - DeserializationError error = deserializeMsgPack(doc, (char *)frame->payload, frame->len); + DeserializationError error = deserializeMsgPack(doc, (char *)data, len); #endif if (!error && doc.is()) @@ -77,11 +105,10 @@ esp_err_t EventSocket::onFrame(PsychicWebSocketRequest *request, httpd_ws_frame String event = doc["event"]; if (event == "subscribe") { - // only subscribe to events that are registered if (isEventValid(doc["data"].as())) { - client_subscriptions[doc["data"]].push_back(request->client()->socket()); - handleSubscribeCallbacks(doc["data"], String(request->client()->socket())); + client_subscriptions[doc["data"]].push_back(clientId); + handleSubscribeCallbacks(doc["data"], String(clientId)); } else { @@ -90,23 +117,21 @@ esp_err_t EventSocket::onFrame(PsychicWebSocketRequest *request, httpd_ws_frame } else if (event == "unsubscribe") { - client_subscriptions[doc["data"]].remove(request->client()->socket()); + client_subscriptions[doc["data"]].remove(clientId); } else { JsonObject jsonObject = doc["data"].as(); - handleEventCallbacks(event, jsonObject, request->client()->socket()); + handleEventCallbacks(event, jsonObject, clientId); } - return ESP_OK; + return; } - ESP_LOGW(SVK_TAG, "Error[%d] parsing JSON: %s", error, (char *)frame->payload); + ESP_LOGW(SVK_TAG, "Error[%d] parsing JSON: %s", error, (char *)data); } - return ESP_OK; } void EventSocket::emitEvent(String event, JsonObject &jsonObject, const char *originId, bool onlyToSameOrigin) { - // Only process valid events if (!isEventValid(String(event))) { ESP_LOGW(SVK_TAG, "Method tried to emit unregistered event: %s", event); @@ -140,41 +165,38 @@ void EventSocket::emitEvent(String event, JsonObject &jsonObject, const char *or serializeMsgPack(doc, output, len); #endif - // null terminate the string output[len] = '\0'; - // if onlyToSameOrigin == true, send the message back to the origin if (onlyToSameOrigin && originSubscriptionId > 0) { - auto *client = _socket.getClient(originSubscriptionId); + auto *client = _socket.client((uint32_t)originSubscriptionId); if (client) { - ESP_LOGV(SVK_TAG, "Emitting event: %s to %s[%u], Message[%d]: %s", event, client->remoteIP().toString().c_str(), client->socket(), len, output); + ESP_LOGV(SVK_TAG, "Emitting event: %s to %s[%u], Message[%d]: %s", event, client->remoteIP().toString().c_str(), client->id(), len, output); #if FT_ENABLED(EVENT_USE_JSON) - client->sendMessage(HTTPD_WS_TYPE_TEXT, output, len); + client->text(output, len); #else - client->sendMessage(HTTPD_WS_TYPE_BINARY, output, len); + client->binary(output, len); #endif } } else - { // else send the message to all other clients - + { for (int subscription : client_subscriptions[event]) { if (subscription == originSubscriptionId) continue; - auto *client = _socket.getClient(subscription); + auto *client = _socket.client((uint32_t)subscription); if (!client) { subscriptions.remove(subscription); continue; } - ESP_LOGV(SVK_TAG, "Emitting event: %s to %s[%u], Message[%d]: %s", event, client->remoteIP().toString().c_str(), client->socket(), len, output); + ESP_LOGV(SVK_TAG, "Emitting event: %s to %s[%u], Message[%d]: %s", event, client->remoteIP().toString().c_str(), client->id(), len, output); #if FT_ENABLED(EVENT_USE_JSON) - client->sendMessage(HTTPD_WS_TYPE_TEXT, output, len); + client->text(output, len); #else - client->sendMessage(HTTPD_WS_TYPE_BINARY, output, len); + client->binary(output, len); #endif } } @@ -227,5 +249,5 @@ bool EventSocket::isEventValid(String event) unsigned int EventSocket::getConnectedClients() { - return (unsigned int)_socket.getClientList().size(); + return (unsigned int)_socket.count(); } diff --git a/lib/framework/EventSocket.h b/lib/framework/EventSocket.h index 06ecda77e..7c903aeb4 100644 --- a/lib/framework/EventSocket.h +++ b/lib/framework/EventSocket.h @@ -15,7 +15,7 @@ * the terms of the LGPL v3 license. See the LICENSE file for details. **/ -#include +#include #include #include #include @@ -30,7 +30,7 @@ typedef std::function SubscribeCallback; class EventSocket { public: - EventSocket(PsychicHttpServer *server, SecurityManager *_securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_AUTHENTICATED); + EventSocket(AsyncWebServer *server, SecurityManager *_securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_AUTHENTICATED); void begin(); @@ -41,15 +41,14 @@ class EventSocket void onSubscribe(String event, SubscribeCallback callback); void emitEvent(String event, JsonObject &jsonObject, const char *originId = "", bool onlyToSameOrigin = false); - // if onlyToSameOrigin == true, the message will be sent to the originId only, otherwise it will be broadcasted to all clients except the originId bool isEventValid(String event); unsigned int getConnectedClients(); private: - PsychicHttpServer *_server; - PsychicWebSocketHandler _socket; + AsyncWebServer *_server; + AsyncWebSocket _socket; SecurityManager *_securityManager; AuthenticationPredicate _authenticationPredicate; @@ -60,9 +59,11 @@ class EventSocket void handleEventCallbacks(String event, JsonObject &jsonObject, int originId); void handleSubscribeCallbacks(String event, const String &originId); - void onWSOpen(PsychicWebSocketClient *client); - void onWSClose(PsychicWebSocketClient *client); - esp_err_t onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame); + void onWSEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, + AwsEventType type, void *arg, uint8_t *data, size_t len); + void onWSOpen(AsyncWebSocketClient *client); + void onWSClose(AsyncWebSocketClient *client); + void onFrame(AsyncWebSocketClient *client, AwsFrameInfo *info, uint8_t *data, size_t len); }; #endif diff --git a/lib/framework/FactoryResetService.cpp b/lib/framework/FactoryResetService.cpp index 4c30d4258..f0daea564 100644 --- a/lib/framework/FactoryResetService.cpp +++ b/lib/framework/FactoryResetService.cpp @@ -16,7 +16,7 @@ using namespace std::placeholders; -FactoryResetService::FactoryResetService(PsychicHttpServer *server, +FactoryResetService::FactoryResetService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager) : _server(server), fs(fs), @@ -33,12 +33,10 @@ void FactoryResetService::begin() ESP_LOGV(SVK_TAG, "Registered POST endpoint: %s", FACTORY_RESET_SERVICE_PATH); } -esp_err_t FactoryResetService::handleRequest(PsychicRequest *request) +void FactoryResetService::handleRequest(AsyncWebServerRequest *request) { - request->reply(200); + request->send(200); factoryReset(); - - return ESP_OK; } /** diff --git a/lib/framework/FactoryResetService.h b/lib/framework/FactoryResetService.h index 03359da69..56bd42f24 100644 --- a/lib/framework/FactoryResetService.h +++ b/lib/framework/FactoryResetService.h @@ -17,7 +17,7 @@ #include -#include +#include #include #include #include @@ -30,15 +30,15 @@ class FactoryResetService FS *fs; public: - FactoryResetService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager); + FactoryResetService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager); void begin(); void factoryReset(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; - esp_err_t handleRequest(PsychicRequest *request); + void handleRequest(AsyncWebServerRequest *request); }; #endif // end FactoryResetService_h diff --git a/lib/framework/FeaturesService.cpp b/lib/framework/FeaturesService.cpp index 036ba73a8..151f909f2 100644 --- a/lib/framework/FeaturesService.cpp +++ b/lib/framework/FeaturesService.cpp @@ -14,18 +14,19 @@ #include -FeaturesService::FeaturesService(PsychicHttpServer *server, EventSocket *eventsocket) : _server(server), _socket(eventsocket) +FeaturesService::FeaturesService(AsyncWebServer *server, EventSocket *eventsocket) : _server(server), _socket(eventsocket) { } void FeaturesService::begin() { - _server->on(FEATURES_SERVICE_PATH, HTTP_GET, [&](PsychicRequest *request) + _server->on(FEATURES_SERVICE_PATH, HTTP_GET, [&](AsyncWebServerRequest *request) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); createJSON(root); - return response.send(); }); + response->setLength(); + request->send(response); }); ESP_LOGV(SVK_TAG, "Registered GET endpoint: %s", FEATURES_SERVICE_PATH); diff --git a/lib/framework/FeaturesService.h b/lib/framework/FeaturesService.h index 28f84cc09..c4cbd90e7 100644 --- a/lib/framework/FeaturesService.h +++ b/lib/framework/FeaturesService.h @@ -19,7 +19,8 @@ #include #include -#include +#include +#include #include #include @@ -35,14 +36,14 @@ typedef struct class FeaturesService { public: - FeaturesService(PsychicHttpServer *server, EventSocket *socket); + FeaturesService(AsyncWebServer *server, EventSocket *socket); void begin(); void addFeature(String feature, bool enabled); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; EventSocket *_socket; std::vector userFeatures; diff --git a/lib/framework/HttpEndpoint.h b/lib/framework/HttpEndpoint.h index 71e87ece1..2249723b5 100644 --- a/lib/framework/HttpEndpoint.h +++ b/lib/framework/HttpEndpoint.h @@ -3,7 +3,8 @@ #include -#include +#include +#include #include #include @@ -22,14 +23,14 @@ class HttpEndpoint StatefulService *_statefulService; SecurityManager *_securityManager; AuthenticationPredicate _authenticationPredicate; - PsychicHttpServer *_server; + AsyncWebServer *_server; const char *_servicePath; public: HttpEndpoint(JsonStateReader stateReader, JsonStateUpdater stateUpdater, StatefulService *statefulService, - PsychicHttpServer *server, + AsyncWebServer *server, const char *servicePath, SecurityManager *securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : _stateReader(stateReader), @@ -51,9 +52,9 @@ class HttpEndpoint _server->on(_servicePath, HTTP_OPTIONS, _securityManager->wrapRequest( - [this](PsychicRequest *request) + [this](AsyncWebServerRequest *request) { - return request->reply(200); + request->send(200); }, AuthenticationPredicates::IS_AUTHENTICATED)); #endif @@ -62,48 +63,55 @@ class HttpEndpoint _server->on(_servicePath, HTTP_GET, _securityManager->wrapRequest( - [this](PsychicRequest *request) + [this](AsyncWebServerRequest *request) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject jsonObject = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject jsonObject = response->getRoot(); _statefulService->read(jsonObject, _stateReader); - return response.send(); + response->setLength(); + request->send(response); }, _authenticationPredicate)); ESP_LOGV(SVK_TAG, "Registered GET endpoint: %s", _servicePath); // POST - _server->on(_servicePath, - HTTP_POST, - _securityManager->wrapCallback( - [this](PsychicRequest *request, JsonVariant &json) - { - if (!json.is()) - { - return request->reply(400); - } - - JsonObject jsonObject = json.as(); - StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater, _servicePath); - - if (outcome == StateUpdateResult::ERROR) - { - return request->reply(400); - } - else if ((outcome == StateUpdateResult::CHANGED)) - { - // persist the changes to the FS - _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); - } - - PsychicJsonResponse response = PsychicJsonResponse(request, false); - jsonObject = response.getRoot(); - - _statefulService->read(jsonObject, _stateReader); - - return response.send(); - }, - _authenticationPredicate)); + { + auto wrappedCallback = _securityManager->wrapCallback( + [this](AsyncWebServerRequest *request, JsonVariant &json) + { + if (!json.is()) + { + request->send(400); + return; + } + + JsonObject jsonObject = json.as(); + StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater, _servicePath); + + if (outcome == StateUpdateResult::ERROR) + { + request->send(400); + return; + } + else if ((outcome == StateUpdateResult::CHANGED)) + { + _statefulService->callUpdateHandlers(HTTP_ENDPOINT_ORIGIN_ID); + } + + AsyncJsonResponse *response = new AsyncJsonResponse(); + jsonObject = response->getRoot(); + + _statefulService->read(jsonObject, _stateReader); + + response->setLength(); + request->send(response); + }, + _authenticationPredicate); + + AsyncCallbackJsonWebHandler *jsonHandler = new AsyncCallbackJsonWebHandler(_servicePath, wrappedCallback); + jsonHandler->setMethod(HTTP_POST); + _server->addHandler(jsonHandler); + } ESP_LOGV(SVK_TAG, "Registered POST endpoint: %s", _servicePath); } diff --git a/lib/framework/MqttSettingsService.cpp b/lib/framework/MqttSettingsService.cpp index d74076253..fd287561a 100644 --- a/lib/framework/MqttSettingsService.cpp +++ b/lib/framework/MqttSettingsService.cpp @@ -42,7 +42,7 @@ static char *retainCstr(const char *cstr, char **ptr) return *ptr; } -MqttSettingsService::MqttSettingsService(PsychicHttpServer *server, +MqttSettingsService::MqttSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager) : _server(server), _securityManager(securityManager), diff --git a/lib/framework/MqttSettingsService.h b/lib/framework/MqttSettingsService.h index db536abee..6fb97058d 100644 --- a/lib/framework/MqttSettingsService.h +++ b/lib/framework/MqttSettingsService.h @@ -123,7 +123,7 @@ class MqttSettings class MqttSettingsService : public StatefulService { public: - MqttSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager); + MqttSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager); ~MqttSettingsService(); void begin(); @@ -141,7 +141,7 @@ class MqttSettingsService : public StatefulService void onConfigUpdated(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; diff --git a/lib/framework/MqttStatus.cpp b/lib/framework/MqttStatus.cpp index af4756895..dab11ba8b 100644 --- a/lib/framework/MqttStatus.cpp +++ b/lib/framework/MqttStatus.cpp @@ -14,7 +14,7 @@ #include -MqttStatus::MqttStatus(PsychicHttpServer *server, +MqttStatus::MqttStatus(AsyncWebServer *server, MqttSettingsService *mqttSettingsService, SecurityManager *securityManager) : _server(server), _securityManager(securityManager), @@ -32,17 +32,18 @@ void MqttStatus::begin() ESP_LOGV(SVK_TAG, "Registered GET endpoint: %s", MQTT_STATUS_SERVICE_PATH); } -esp_err_t MqttStatus::mqttStatus(PsychicRequest *request) +void MqttStatus::mqttStatus(AsyncWebServerRequest *request) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); root["enabled"] = _mqttSettingsService->isEnabled(); root["connected"] = _mqttSettingsService->isConnected(); root["client_id"] = _mqttSettingsService->getClientId(); root["last_error"] = _mqttSettingsService->getLastError(); - return response.send(); + response->setLength(); + request->send(response); } bool MqttStatus::isConnected() diff --git a/lib/framework/MqttStatus.h b/lib/framework/MqttStatus.h index 750ba3531..33170201b 100644 --- a/lib/framework/MqttStatus.h +++ b/lib/framework/MqttStatus.h @@ -19,7 +19,8 @@ #include #include -#include +#include +#include #include #define MQTT_STATUS_SERVICE_PATH "/rest/mqttStatus" @@ -27,18 +28,18 @@ class MqttStatus { public: - MqttStatus(PsychicHttpServer *server, MqttSettingsService *mqttSettingsService, SecurityManager *securityManager); + MqttStatus(AsyncWebServer *server, MqttSettingsService *mqttSettingsService, SecurityManager *securityManager); void begin(); bool isConnected(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; MqttSettingsService *_mqttSettingsService; - esp_err_t mqttStatus(PsychicRequest *request); + void mqttStatus(AsyncWebServerRequest *request); }; #endif // end MqttStatus_h diff --git a/lib/framework/NTPSettingsService.cpp b/lib/framework/NTPSettingsService.cpp index 0c9d922eb..fd8dabd48 100644 --- a/lib/framework/NTPSettingsService.cpp +++ b/lib/framework/NTPSettingsService.cpp @@ -17,7 +17,7 @@ #include #endif -NTPSettingsService::NTPSettingsService(PsychicHttpServer *server, +NTPSettingsService::NTPSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager) : _server(server), _securityManager(securityManager), @@ -110,7 +110,7 @@ void NTPSettingsService::configureNTP() } } -esp_err_t NTPSettingsService::configureTime(PsychicRequest *request, JsonVariant &json) +void NTPSettingsService::configureTime(AsyncWebServerRequest *request, JsonVariant &json) { if (!sntp_enabled() && json.is()) { @@ -122,8 +122,9 @@ esp_err_t NTPSettingsService::configureTime(PsychicRequest *request, JsonVariant time_t time = mktime(&tm); struct timeval now = {.tv_sec = time}; settimeofday(&now, nullptr); - return request->reply(200); + request->send(200); + return; } } - return request->reply(400); + request->send(400); } diff --git a/lib/framework/NTPSettingsService.h b/lib/framework/NTPSettingsService.h index eda1f8e45..c19d7d724 100644 --- a/lib/framework/NTPSettingsService.h +++ b/lib/framework/NTPSettingsService.h @@ -76,12 +76,12 @@ class NTPSettings class NTPSettingsService : public StatefulService { public: - NTPSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager); + NTPSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager); void begin(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; @@ -89,7 +89,7 @@ class NTPSettingsService : public StatefulService void onNetworkGotIP(WiFiEvent_t event, WiFiEventInfo_t info); void onNetworkDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); void configureNTP(); - esp_err_t configureTime(PsychicRequest *request, JsonVariant &json); + void configureTime(AsyncWebServerRequest *request, JsonVariant &json); }; #endif // end NTPSettingsService_h diff --git a/lib/framework/NTPStatus.cpp b/lib/framework/NTPStatus.cpp index 8373834c1..10c732265 100644 --- a/lib/framework/NTPStatus.cpp +++ b/lib/framework/NTPStatus.cpp @@ -14,7 +14,7 @@ #include -NTPStatus::NTPStatus(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server), +NTPStatus::NTPStatus(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), _securityManager(securityManager) { } @@ -51,10 +51,10 @@ String toLocalTimeString(tm *time) return formatTime(time, "%FT%T"); } -esp_err_t NTPStatus::ntpStatus(PsychicRequest *request) +void NTPStatus::ntpStatus(AsyncWebServerRequest *request) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); // grab the current instant in unix seconds time_t now = time(nullptr); @@ -74,5 +74,6 @@ esp_err_t NTPStatus::ntpStatus(PsychicRequest *request) // device uptime in seconds root["uptime"] = millis() / 1000; - return response.send(); + response->setLength(); + request->send(response); } diff --git a/lib/framework/NTPStatus.h b/lib/framework/NTPStatus.h index 7112aaf08..6d0931ab9 100644 --- a/lib/framework/NTPStatus.h +++ b/lib/framework/NTPStatus.h @@ -20,7 +20,8 @@ #include #include -#include +#include +#include #include #define NTP_STATUS_SERVICE_PATH "/rest/ntpStatus" @@ -28,14 +29,14 @@ class NTPStatus { public: - NTPStatus(PsychicHttpServer *server, SecurityManager *securityManager); + NTPStatus(AsyncWebServer *server, SecurityManager *securityManager); void begin(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; - esp_err_t ntpStatus(PsychicRequest *request); + void ntpStatus(AsyncWebServerRequest *request); }; #endif // end NTPStatus_h diff --git a/lib/framework/RestartService.cpp b/lib/framework/RestartService.cpp index 3770c95bf..490bf43f5 100644 --- a/lib/framework/RestartService.cpp +++ b/lib/framework/RestartService.cpp @@ -14,8 +14,8 @@ #include -RestartService::RestartService(PsychicHttpServer *server, SecurityManager *securityManager) : _server(server), - _securityManager(securityManager) +RestartService::RestartService(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), + _securityManager(securityManager) { } @@ -29,9 +29,8 @@ void RestartService::begin() ESP_LOGV(SVK_TAG, "Registered POST endpoint: %s", RESTART_SERVICE_PATH); } -esp_err_t RestartService::restart(PsychicRequest *request) +void RestartService::restart(AsyncWebServerRequest *request) { - request->reply(200); + request->send(200); restartNow(); - return ESP_OK; } diff --git a/lib/framework/RestartService.h b/lib/framework/RestartService.h index fd778e5bf..2a63805f7 100644 --- a/lib/framework/RestartService.h +++ b/lib/framework/RestartService.h @@ -18,7 +18,7 @@ #include #include -#include +#include #include #define RESTART_SERVICE_PATH "/rest/restart" @@ -26,7 +26,7 @@ class RestartService { public: - RestartService(PsychicHttpServer *server, SecurityManager *securityManager); + RestartService(AsyncWebServer *server, SecurityManager *securityManager); void begin(); @@ -41,9 +41,9 @@ class RestartService } private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; - esp_err_t restart(PsychicRequest *request); + void restart(AsyncWebServerRequest *request); }; #endif // end RestartService_h diff --git a/lib/framework/SecurityManager.h b/lib/framework/SecurityManager.h index 50ac0dae6..8c0e56b79 100644 --- a/lib/framework/SecurityManager.h +++ b/lib/framework/SecurityManager.h @@ -17,7 +17,7 @@ #include #include -#include +#include #include #define SVK_TAG "🐼" @@ -62,6 +62,10 @@ class Authentication typedef std::function AuthenticationPredicate; +typedef std::function ArHttpRequestCallback; +typedef std::function ArJsonRequestCallback; +typedef std::function ArRequestFilterFunc; + class AuthenticationPredicates { public: @@ -98,22 +102,22 @@ class SecurityManager /* * Check the request header for the Authorization token */ - virtual Authentication authenticateRequest(PsychicRequest *request) = 0; + virtual Authentication authenticateRequest(AsyncWebServerRequest *request) = 0; /** * Filter a request with the provided predicate, only returning true if the predicate matches. */ - virtual PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate) = 0; + virtual ArRequestFilterFunc filterRequest(AuthenticationPredicate predicate) = 0; /** * Wrap the provided request to provide validation against an AuthenticationPredicate. */ - virtual PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate) = 0; + virtual ArHttpRequestCallback wrapRequest(ArHttpRequestCallback onRequest, AuthenticationPredicate predicate) = 0; /** * Wrap the provided json request callback to provide validation against an AuthenticationPredicate. */ - virtual PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate) = 0; + virtual ArJsonRequestCallback wrapCallback(ArJsonRequestCallback onRequest, AuthenticationPredicate predicate) = 0; }; #endif // end SecurityManager_h diff --git a/lib/framework/SecuritySettingsService.cpp b/lib/framework/SecuritySettingsService.cpp index 31c8815cb..f8362e21e 100644 --- a/lib/framework/SecuritySettingsService.cpp +++ b/lib/framework/SecuritySettingsService.cpp @@ -16,10 +16,10 @@ #if FT_ENABLED(FT_SECURITY) -SecuritySettingsService::SecuritySettingsService(PsychicHttpServer *server, FS *fs) : _server(server), - _httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this), - _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE), - _jwtHandler(FACTORY_JWT_SECRET) +SecuritySettingsService::SecuritySettingsService(AsyncWebServer *server, FS *fs) : _server(server), + _httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this), + _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE), + _jwtHandler(FACTORY_JWT_SECRET) { addUpdateHandler([&](const String &originId) { configureJWTHandler(); }, @@ -40,13 +40,11 @@ void SecuritySettingsService::begin() configureJWTHandler(); } -Authentication SecuritySettingsService::authenticateRequest(PsychicRequest *request) +Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest *request) { - // Load the parameters from the request, as they are only loaded later with the regular handler if (request->hasHeader(AUTHORIZATION_HEADER)) { - auto value = request->header(AUTHORIZATION_HEADER); - // ESP_LOGV(SVK_TAG, "Authorization header: %s", value.c_str()); + String value = request->getHeader(AUTHORIZATION_HEADER)->value(); if (value.startsWith(AUTHORIZATION_HEADER_PREFIX)) { value = value.substring(AUTHORIZATION_HEADER_PREFIX_LEN); @@ -56,7 +54,6 @@ Authentication SecuritySettingsService::authenticateRequest(PsychicRequest *requ else if (request->hasParam(ACCESS_TOKEN_PARAMATER)) { String value = request->getParam(ACCESS_TOKEN_PARAMATER)->value(); - // ESP_LOGV(SVK_TAG, "Access token parameter: %s", value.c_str()); return authenticateJWT(value); } return Authentication(); @@ -120,109 +117,98 @@ String SecuritySettingsService::generateJWT(User *user) return _jwtHandler.buildJWT(payload); } -PsychicRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) +ArRequestFilterFunc SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) { - return [this, predicate](PsychicRequest *request) + return [this, predicate](AsyncWebServerRequest *request) { - // ESP_LOGV(SVK_TAG, "Authenticating filter request: %s", request->uri().c_str()); - // ESP_LOGV(SVK_TAG, "Request Method: %s", request->methodStr().c_str()); - - // TODO: This is a hack to allow bogus websocket filter requests to pass through - // This is a temporary fix until the PsychicHttp websocket handler is fixed to not send a bogus filter request - - // Check if we have a bogus filter request and return true - if (request->uri().isEmpty() && request->method() == HTTP_DELETE) - { - // ESP_LOGV(SVK_TAG, "Bogus filter request - allowing"); - return true; - } - else - request->loadParams(); - Authentication authentication = authenticateRequest(request); - bool result = predicate(authentication); - // ESP_LOGV(SVK_TAG, "Filter Request %s", result ? "allowed" : "denied"); - return result; + return predicate(authentication); }; } -PsychicHttpRequestCallback SecuritySettingsService::wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate) +ArHttpRequestCallback SecuritySettingsService::wrapRequest(ArHttpRequestCallback onRequest, AuthenticationPredicate predicate) { - return [this, onRequest, predicate](PsychicRequest *request) + return [this, onRequest, predicate](AsyncWebServerRequest *request) { Authentication authentication = authenticateRequest(request); if (!predicate(authentication)) { - return request->reply(401); + request->send(401); + return; } - return onRequest(request); + onRequest(request); }; } -PsychicJsonRequestCallback SecuritySettingsService::wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate) +ArJsonRequestCallback SecuritySettingsService::wrapCallback(ArJsonRequestCallback onRequest, AuthenticationPredicate predicate) { - return [this, onRequest, predicate](PsychicRequest *request, JsonVariant &json) + return [this, onRequest, predicate](AsyncWebServerRequest *request, JsonVariant &json) { Authentication authentication = authenticateRequest(request); if (!predicate(authentication)) { - return request->reply(401); + request->send(401); + return; } - return onRequest(request, json); + onRequest(request, json); }; } -esp_err_t SecuritySettingsService::generateToken(PsychicRequest *request) +void SecuritySettingsService::generateToken(AsyncWebServerRequest *request) { + if (!request->hasParam("username")) + { + request->send(400); + return; + } String usernameParam = request->getParam("username")->value(); for (User _user : _state.users) { if (_user.username == usernameParam) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); root["token"] = generateJWT(&_user); - return response.send(); + response->setLength(); + request->send(response); + return; } } - return request->reply(401); + request->send(401); } #else User ADMIN_USER = User(FACTORY_ADMIN_USERNAME, FACTORY_ADMIN_PASSWORD, true); -SecuritySettingsService::SecuritySettingsService(PsychicHttpServer *server, FS *fs) : SecurityManager() +SecuritySettingsService::SecuritySettingsService(AsyncWebServer *server, FS *fs) : SecurityManager() { } SecuritySettingsService::~SecuritySettingsService() { } -PsychicRequestFilterFunction SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) +ArRequestFilterFunc SecuritySettingsService::filterRequest(AuthenticationPredicate predicate) { - return [this, predicate](PsychicRequest *request) + return [this, predicate](AsyncWebServerRequest *request) { - // ESP_LOGV(SVK_TAG, "Security disabled - all requests are allowed"); return true; }; } -// Return the admin user on all request - disabling security features -Authentication SecuritySettingsService::authenticateRequest(PsychicRequest *request) +Authentication SecuritySettingsService::authenticateRequest(AsyncWebServerRequest *request) { return Authentication(ADMIN_USER); } -// Return the function unwrapped -PsychicHttpRequestCallback SecuritySettingsService::wrapRequest(PsychicHttpRequestCallback onRequest, - AuthenticationPredicate predicate) +ArHttpRequestCallback SecuritySettingsService::wrapRequest(ArHttpRequestCallback onRequest, + AuthenticationPredicate predicate) { return onRequest; } -PsychicJsonRequestCallback SecuritySettingsService::wrapCallback(PsychicJsonRequestCallback onRequest, - AuthenticationPredicate predicate) +ArJsonRequestCallback SecuritySettingsService::wrapCallback(ArJsonRequestCallback onRequest, + AuthenticationPredicate predicate) { return onRequest; } diff --git a/lib/framework/SecuritySettingsService.h b/lib/framework/SecuritySettingsService.h index 3d5f011f2..34d1edf57 100644 --- a/lib/framework/SecuritySettingsService.h +++ b/lib/framework/SecuritySettingsService.h @@ -96,27 +96,27 @@ class SecuritySettings class SecuritySettingsService : public StatefulService, public SecurityManager { public: - SecuritySettingsService(PsychicHttpServer *server, FS *fs); + SecuritySettingsService(AsyncWebServer *server, FS *fs); void begin(); // Functions to implement SecurityManager Authentication authenticate(const String &username, const String &password); - Authentication authenticateRequest(PsychicRequest *request); + Authentication authenticateRequest(AsyncWebServerRequest *request); String generateJWT(User *user); - PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate); - PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate); - PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate); + ArRequestFilterFunc filterRequest(AuthenticationPredicate predicate); + ArHttpRequestCallback wrapRequest(ArHttpRequestCallback onRequest, AuthenticationPredicate predicate); + ArJsonRequestCallback wrapCallback(ArJsonRequestCallback onRequest, AuthenticationPredicate predicate); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; ArduinoJsonJWT _jwtHandler; - esp_err_t generateToken(PsychicRequest *request); + void generateToken(AsyncWebServerRequest *request); void configureJWTHandler(); @@ -136,14 +136,14 @@ class SecuritySettingsService : public StatefulService, public class SecuritySettingsService : public SecurityManager { public: - SecuritySettingsService(PsychicHttpServer *server, FS *fs); + SecuritySettingsService(AsyncWebServer *server, FS *fs); ~SecuritySettingsService(); // minimal set of functions to support framework with security settings disabled - Authentication authenticateRequest(PsychicRequest *request); - PsychicRequestFilterFunction filterRequest(AuthenticationPredicate predicate); - PsychicHttpRequestCallback wrapRequest(PsychicHttpRequestCallback onRequest, AuthenticationPredicate predicate); - PsychicJsonRequestCallback wrapCallback(PsychicJsonRequestCallback onRequest, AuthenticationPredicate predicate); + Authentication authenticateRequest(AsyncWebServerRequest *request); + ArRequestFilterFunc filterRequest(AuthenticationPredicate predicate); + ArHttpRequestCallback wrapRequest(ArHttpRequestCallback onRequest, AuthenticationPredicate predicate); + ArJsonRequestCallback wrapCallback(ArJsonRequestCallback onRequest, AuthenticationPredicate predicate); }; #endif // end FT_ENABLED(FT_SECURITY) diff --git a/lib/framework/SleepService.cpp b/lib/framework/SleepService.cpp index ad101a487..8ecb329c5 100644 --- a/lib/framework/SleepService.cpp +++ b/lib/framework/SleepService.cpp @@ -19,7 +19,7 @@ u_int64_t _wakeUpPin = WAKEUP_PIN_NUMBER; bool _wakeUpSignal = WAKEUP_SIGNAL; pinTermination _wakeUpTermination = pinTermination::FLOATING; -SleepService::SleepService(PsychicHttpServer *server, +SleepService::SleepService(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), _securityManager(securityManager) { @@ -32,9 +32,9 @@ void SleepService::begin() _server->on(SLEEP_SERVICE_PATH, HTTP_OPTIONS, _securityManager->wrapRequest( - [this](PsychicRequest *request) + [this](AsyncWebServerRequest *request) { - return request->reply(200); + request->send(200); }, AuthenticationPredicates::IS_AUTHENTICATED)); #endif @@ -47,12 +47,10 @@ void SleepService::begin() ESP_LOGV(SVK_TAG, "Registered POST endpoint: %s", SLEEP_SERVICE_PATH); } -esp_err_t SleepService::sleep(PsychicRequest *request) +void SleepService::sleep(AsyncWebServerRequest *request) { - request->reply(200); + request->send(200); sleepNow(); - - return ESP_OK; } void SleepService::sleepNow() diff --git a/lib/framework/SleepService.h b/lib/framework/SleepService.h index 8c7e1a779..3e3531b67 100644 --- a/lib/framework/SleepService.h +++ b/lib/framework/SleepService.h @@ -16,7 +16,7 @@ #include #include -#include +#include #include #include "driver/rtc_io.h" #include @@ -44,7 +44,7 @@ typedef std::function sleepCallback; class SleepService { public: - SleepService(PsychicHttpServer *server, SecurityManager *securityManager); + SleepService(AsyncWebServer *server, SecurityManager *securityManager); void begin(); @@ -58,9 +58,9 @@ class SleepService void setWakeUpPin(int pin, bool level, pinTermination termination = pinTermination::FLOATING); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; - esp_err_t sleep(PsychicRequest *request); + void sleep(AsyncWebServerRequest *request); protected: static std::vector _sleepCallbacks; diff --git a/lib/framework/SystemStatus.cpp b/lib/framework/SystemStatus.cpp index ea0f60f6a..6d6f28641 100644 --- a/lib/framework/SystemStatus.cpp +++ b/lib/framework/SystemStatus.cpp @@ -112,7 +112,7 @@ String verbosePrintResetReason(int reason) } } -SystemStatus::SystemStatus(PsychicHttpServer *server, +SystemStatus::SystemStatus(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), _securityManager(securityManager) { @@ -128,10 +128,10 @@ void SystemStatus::begin() ESP_LOGV(SVK_TAG, "Registered GET endpoint: %s", SYSTEM_STATUS_SERVICE_PATH); } -esp_err_t SystemStatus::systemStatus(PsychicRequest *request) +void SystemStatus::systemStatus(AsyncWebServerRequest *request) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); root["esp_platform"] = ESP_TARGET; root["firmware_version"] = APP_VERSION; @@ -162,5 +162,6 @@ esp_err_t SystemStatus::systemStatus(PsychicRequest *request) root["cpu_reset_reason"] = verbosePrintResetReason(esp_reset_reason()); root["uptime"] = millis() / 1000; - return response.send(); + response->setLength(); + request->send(response); } diff --git a/lib/framework/SystemStatus.h b/lib/framework/SystemStatus.h index fba2123f1..0230d484c 100644 --- a/lib/framework/SystemStatus.h +++ b/lib/framework/SystemStatus.h @@ -18,7 +18,8 @@ #include #include -#include +#include +#include #include #include @@ -27,14 +28,14 @@ class SystemStatus { public: - SystemStatus(PsychicHttpServer *server, SecurityManager *securityManager); + SystemStatus(AsyncWebServer *server, SecurityManager *securityManager); void begin(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; - esp_err_t systemStatus(PsychicRequest *request); + void systemStatus(AsyncWebServerRequest *request); }; #endif // end SystemStatus_h diff --git a/lib/framework/UploadFirmwareService.cpp b/lib/framework/UploadFirmwareService.cpp index 76a1b47bc..87550dc85 100644 --- a/lib/framework/UploadFirmwareService.cpp +++ b/lib/framework/UploadFirmwareService.cpp @@ -22,13 +22,13 @@ using namespace std::placeholders; // for `_1` etc -UploadFirmwareService::UploadFirmwareService(PsychicHttpServer *server, +UploadFirmwareService::UploadFirmwareService(AsyncWebServer *server, SecurityManager *securityManager, EventSocket *socket) : _server(server), _securityManager(securityManager), _socket(socket) { - _md5[0] = '\0'; // Initialize MD5 buffer + _md5[0] = '\0'; } void UploadFirmwareService::begin() @@ -37,13 +37,9 @@ void UploadFirmwareService::begin() { _socket->registerEvent(EVENT_OTA_UPDATE); } - - // Set PsychicHttp's limit to max to avoid connection reset on oversized files - // We'll validate the size ourselves in handleUpload() to provide proper error handling - _server->maxUploadSize = SIZE_MAX; + _maxFirmwareSize = getMaxFirmwareSize(); - // Setup progress callback for Update library Update.onProgress([this](size_t progress, size_t total) { if (_socket && total > 0) { int percentComplete = (progress * 100) / total; @@ -53,23 +49,23 @@ void UploadFirmwareService::begin() doc["progress"] = percentComplete; doc["bytes_written"] = progress; doc["total_bytes"] = total; - + JsonObject jsonObject = doc.as(); _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject); - + ESP_LOGV(SVK_TAG, "Firmware upload process at %d of %d bytes... (%d %%)", progress, total, percentComplete); - + _previousProgress = percentComplete; } } }); - PsychicUploadHandler *uploadHandler = new PsychicUploadHandler(); - - uploadHandler->onUpload(std::bind(&UploadFirmwareService::handleUpload, this, _1, _2, _3, _4, _5, _6)); - uploadHandler->onRequest(std::bind(&UploadFirmwareService::uploadComplete, this, _1)); // gets called after upload has been handled - uploadHandler->onClose(std::bind(&UploadFirmwareService::handleEarlyDisconnect, this)); // gets called if client disconnects - _server->on(UPLOAD_FIRMWARE_PATH, HTTP_POST, uploadHandler); + _server->on(UPLOAD_FIRMWARE_PATH, HTTP_POST, + [this](AsyncWebServerRequest *request) { uploadComplete(request); }, + [this](AsyncWebServerRequest *request, String filename, size_t index, + uint8_t *data, size_t len, bool final) { + handleUpload(request, filename, index, data, len, final); + }); ESP_LOGV(SVK_TAG, "Registered POST endpoint: %s", UPLOAD_FIRMWARE_PATH); } @@ -109,83 +105,80 @@ bool UploadFirmwareService::validateChipType(uint8_t *data, size_t len) return true; } -esp_err_t UploadFirmwareService::handleUpload(PsychicRequest *request, - const String &filename, - uint64_t index, - uint8_t *data, - size_t len, - bool final) +void UploadFirmwareService::handleUpload(AsyncWebServerRequest *request, + const String &filename, + size_t index, + uint8_t *data, + size_t len, + bool final) { - // quit if not authorized Authentication authentication = _securityManager->authenticateRequest(request); if (!AuthenticationPredicates::IS_ADMIN(authentication)) { - return handleError(request, 403, "Insufficient permissions to upload firmware"); + handleError(request, 403, "Insufficient permissions to upload firmware"); + return; } - if (index == 0) // Are we at the start of a new upload? + if (index == 0) { - // check details of the file, to see if its a valid bin or md5 file std::string fname(filename.c_str()); auto position = fname.find_last_of("."); - - // Check if extension exists to avoid undefined behavior + if (position == std::string::npos) { - return handleError(request, 406, "File has no extension"); + handleError(request, 406, "File has no extension"); + return; } - + std::string extension = fname.substr(position + 1); size_t fsize = request->contentLength(); _fileType = ft_none; - if (strcasecmp(extension.c_str(), "md5") == 0) // Are we processing an MD5 file? + if (strcasecmp(extension.c_str(), "md5") == 0) { _fileType = ft_md5; - - if (len == MD5_LENGTH) // This implicitely checks that fsize is also 32 + + if (len == MD5_LENGTH) { - // Safe: _md5[MD5_LENGTH + 1] has space for 32 bytes + null terminator memcpy(_md5, data, MD5_LENGTH); _md5[MD5_LENGTH] = '\0'; - - return ESP_OK; // Finished processing MD5 file + return; } else { - _md5[0] = '\0'; // Clear any previously stored MD5 on invalid upload - return handleError(request, 422, "MD5 must be exactly 32 bytes"); + _md5[0] = '\0'; + handleError(request, 422, "MD5 must be exactly 32 bytes"); + return; } } - else if (strcasecmp(extension.c_str(), "bin") == 0) // Are we processing a firmware binary? + else if (strcasecmp(extension.c_str(), "bin") == 0) { _fileType = ft_firmware; - - // Validate file size before processing + if (fsize > _maxFirmwareSize) { char errorMsg[64]; - snprintf(errorMsg, sizeof(errorMsg), - "Firmware too large: %.2f MB (max: %.2f MB)", + snprintf(errorMsg, sizeof(errorMsg), + "Firmware too large: %.2f MB (max: %.2f MB)", fsize / 1024.0 / 1024.0, _maxFirmwareSize / 1024.0 / 1024.0); - return handleError(request, 413, errorMsg); + handleError(request, 413, errorMsg); + return; } - + ESP_LOGI(SVK_TAG, "Starting firmware upload: %s (%d bytes)", filename.c_str(), fsize); #ifdef SERIAL_INFO Serial.printf("Starting firmware upload: %s (%d bytes)\n", filename.c_str(), fsize); #endif - - // Validate firmware header (magic byte and chip type) + if (!validateChipType(data, len)) { - return handleError(request, 503, "Wrong firmware for this device"); + handleError(request, 503, "Wrong firmware for this device"); + return; } - + if (Update.begin(fsize - sizeof(esp_image_header_t))) { - // Emit preparing status after validation succeeds if (_socket) { JsonDocument doc; @@ -194,7 +187,7 @@ esp_err_t UploadFirmwareService::handleUpload(PsychicRequest *request, JsonObject jsonObject = doc.as(); _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject); } - + if (strlen(_md5) == MD5_LENGTH) { Update.setMD5(_md5); @@ -202,29 +195,30 @@ esp_err_t UploadFirmwareService::handleUpload(PsychicRequest *request, #ifdef SERIAL_INFO Serial.printf("MD5 hash for validation: %s\n", _md5); #endif - _md5[0] = '\0'; // clear md5 after setting it in Arduino Updater + _md5[0] = '\0'; } } else { - return handleError(request, 507, "Insufficient storage space"); + handleError(request, 507, "Insufficient storage space"); + return; } } - else // Are we processing an unsupported file type? + else { - return handleError(request, 406, "File not a firmware binary or MD5 hash"); + handleError(request, 406, "File not a firmware binary or MD5 hash"); + return; } } - else // we are continuing an existing upload + else { - // Resumable upload: verify that Update was already started if (_fileType == ft_none || !Update.isRunning()) { - return handleError(request, 400, "Upload not initialized"); + handleError(request, 400, "Upload not initialized"); + return; } } - // if we haven't dealt with an error so far, continue with the firmware update if (!request->_tempObject) { if (_fileType == ft_firmware) @@ -232,53 +226,51 @@ esp_err_t UploadFirmwareService::handleUpload(PsychicRequest *request, if (Update.write(data, len) != len) { Update.abort(); - return handleError(request, 500, "Firmware write failed"); + handleError(request, 500, "Firmware write failed"); + return; } if (final) { if (!Update.end(true)) { - // Get specific error message from Update library (includes MD5 mismatch) String errorMsg = "Firmware update failed"; if (Update.hasError()) { errorMsg = Update.errorString(); } Update.abort(); - return handleError(request, 500, errorMsg.c_str()); + handleError(request, 500, errorMsg.c_str()); + return; } } } } - - return ESP_OK; } -esp_err_t UploadFirmwareService::uploadComplete(PsychicRequest *request) +void UploadFirmwareService::uploadComplete(AsyncWebServerRequest *request) { - // if we already handled an error in handleUpload, do nothing if (request->_tempObject) { - return ESP_OK; + return; } - - // if we completed uploading a md5 file create a JSON response + if (_fileType == ft_md5) { if (strlen(_md5) == MD5_LENGTH) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); root["md5"] = _md5; - return response.send(); + response->setLength(); + request->send(response); + return; } - return ESP_OK; + request->send(200); + return; } - // if no error, send the success response if (_fileType == ft_firmware) { - // Emit finished event if (_socket) { JsonDocument doc; @@ -286,57 +278,51 @@ esp_err_t UploadFirmwareService::uploadComplete(PsychicRequest *request) doc["progress"] = 100; JsonObject jsonObject = doc.as(); _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject); - vTaskDelay(100 / portTICK_PERIOD_MS); // Give time for event to be sent + vTaskDelay(100 / portTICK_PERIOD_MS); } - + ESP_LOGI(SVK_TAG, "Firmware upload successful - Restarting"); #ifdef SERIAL_INFO Serial.println("Firmware upload successful - Restarting"); #endif - - // Reset progress tracker for next upload + _previousProgress = 0; - - request->reply(200); + + request->send(200); RestartService::restartNow(); - return ESP_OK; + return; } - // if updated has an error send 500 response and log on Serial if (Update.hasError()) { - // Get specific error message from Update library String errorMsg = Update.errorString(); if (errorMsg.length() == 0) { errorMsg = "Unknown update error"; } - - // Reset progress tracker + _previousProgress = 0; - + ESP_LOGE(SVK_TAG, "Update error: %s", errorMsg.c_str()); #ifdef SERIAL_INFO Update.printError(Serial); #endif Update.abort(); - - // handleError will emit the WebSocket event and send HTTP response - return handleError(request, 500, errorMsg.c_str()); + + handleError(request, 500, errorMsg.c_str()); + return; } - return ESP_OK; + request->send(200); } -esp_err_t UploadFirmwareService::handleError(PsychicRequest *request, int code, const char *message) +void UploadFirmwareService::handleError(AsyncWebServerRequest *request, int code, const char *message) { - // if we have had an error already, do nothing if (request->_tempObject) { - return ESP_OK; + return; } - // Emit WebSocket error event for BIN files (skip for MD5 files) if (_fileType == ft_firmware && _socket && message) { JsonDocument doc; @@ -345,8 +331,7 @@ esp_err_t UploadFirmwareService::handleError(PsychicRequest *request, int code, JsonObject jsonObject = doc.as(); _socket->emitEvent(EVENT_OTA_UPDATE, jsonObject); } - - // Log error + if (message) { ESP_LOGE(SVK_TAG, "Firmware upload failed (%d): %s", code, message); @@ -362,23 +347,18 @@ esp_err_t UploadFirmwareService::handleError(PsychicRequest *request, int code, #endif } - // Reset state to allow new upload attempts _fileType = ft_none; _previousProgress = 0; - - // Abort any ongoing Update to clear error state + Update.abort(); - - // Mark this request as having encountered an error using _tempObject as a flag - // (The pointer value itself is not used, only checked for NULL vs non-NULL) - // The allocated memory is freed on request destruction (see PsychicRequest::~PsychicRequest()) + + // _tempObject freed by AsyncWebServerRequest destructor request->_tempObject = malloc(sizeof(int)); - return request->reply(code); + request->send(code); } -esp_err_t UploadFirmwareService::handleEarlyDisconnect() +void UploadFirmwareService::handleEarlyDisconnect() { - // if updated has not ended on connection close, abort it if (!Update.end(true)) { ESP_LOGE(SVK_TAG, "Update error on early disconnect:"); @@ -386,7 +366,5 @@ esp_err_t UploadFirmwareService::handleEarlyDisconnect() Update.printError(Serial); #endif Update.abort(); - return ESP_OK; } - return ESP_OK; } diff --git a/lib/framework/UploadFirmwareService.h b/lib/framework/UploadFirmwareService.h index 97a6de023..aefec4e1f 100644 --- a/lib/framework/UploadFirmwareService.h +++ b/lib/framework/UploadFirmwareService.h @@ -21,7 +21,8 @@ #include #include -#include +#include +#include #include #include #include @@ -62,83 +63,33 @@ enum FileType class UploadFirmwareService { public: - /** - * @brief Construct firmware upload service - * @param server PsychicHttpServer instance for handling HTTP requests - * @param securityManager Security manager for authentication - * @param socket EventSocket for emitting real-time progress updates - */ - UploadFirmwareService(PsychicHttpServer *server, SecurityManager *securityManager, EventSocket *socket); - - /** - * @brief Initialize the service and register HTTP endpoints - * Sets up upload handlers and progress callbacks - */ + UploadFirmwareService(AsyncWebServer *server, SecurityManager *securityManager, EventSocket *socket); + void begin(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; EventSocket *_socket; - // Upload state (per-instance to support concurrent uploads) char _md5[MD5_LENGTH + 1]; FileType _fileType = ft_none; int _previousProgress = 0; size_t _maxFirmwareSize = 0; - /** - * @brief Get maximum firmware size from OTA partition - * @return Size of OTA partition in bytes, or 2MB fallback if not available - */ size_t getMaxFirmwareSize(); - - /** - * @brief Validate firmware chip type matches target device - * @param data First chunk of firmware data - * @param len Length of data chunk - * @return true if magic byte and chip ID are valid, false otherwise - */ bool validateChipType(uint8_t *data, size_t len); - /** - * @brief Handle incoming firmware upload chunks. Called once per chunk. - * @param request HTTP request object - * @param filename Uploaded file name - * @param index Byte offset of this chunk (0 for first chunk) - * @param data Chunk data buffer - * @param len Chunk length in bytes - * @param final true if this is the last chunk - * @return ESP_OK on success, error code on failure - */ - esp_err_t handleUpload(PsychicRequest *request, - const String &filename, - uint64_t index, - uint8_t *data, - size_t len, - bool final); - - /** - * @brief Called after upload finished (i.e. all chunks received) - * @param request HTTP request object - * @return ESP_OK on success, error code on failure - */ - esp_err_t uploadComplete(PsychicRequest *request); - - /** - * @brief Handle upload errors and emit error events - * @param request HTTP request object - * @param code HTTP status code to return - * @param message Optional error message (default: nullptr) - * @return ESP_OK (error already handled) - */ - esp_err_t handleError(PsychicRequest *request, int code, const char *message = nullptr); - - /** - * @brief Handle client disconnection during upload - * @return ESP_OK on successful cleanup - */ - esp_err_t handleEarlyDisconnect(); + void handleUpload(AsyncWebServerRequest *request, + const String &filename, + size_t index, + uint8_t *data, + size_t len, + bool final); + + void uploadComplete(AsyncWebServerRequest *request); + void handleError(AsyncWebServerRequest *request, int code, const char *message = nullptr); + void handleEarlyDisconnect(); }; #endif // end UploadFirmwareService_h diff --git a/lib/framework/WebSocketServer.h b/lib/framework/WebSocketServer.h index 43f76180f..8852fb3e6 100644 --- a/lib/framework/WebSocketServer.h +++ b/lib/framework/WebSocketServer.h @@ -16,7 +16,7 @@ **/ #include -#include +#include #include #define WEB_SOCKET_ORIGIN "wsserver" @@ -29,7 +29,7 @@ class WebSocketServer WebSocketServer(JsonStateReader stateReader, JsonStateUpdater stateUpdater, StatefulService *statefulService, - PsychicHttpServer *server, + AsyncWebServer *server, const char *webSocketPath, SecurityManager *securityManager, AuthenticationPredicate authenticationPredicate = AuthenticationPredicates::IS_ADMIN) : _stateReader(stateReader), @@ -37,6 +37,7 @@ class WebSocketServer _statefulService(statefulService), _server(server), _webSocketPath(webSocketPath), + _webSocket(webSocketPath), _authenticationPredicate(authenticationPredicate), _securityManager(securityManager) { @@ -49,59 +50,78 @@ class WebSocketServer void begin() { _webSocket.setFilter(_securityManager->filterRequest(_authenticationPredicate)); - _webSocket.onOpen(std::bind(&WebSocketServer::onWSOpen, - this, - std::placeholders::_1)); - _webSocket.onClose(std::bind(&WebSocketServer::onWSClose, - this, - std::placeholders::_1)); - _webSocket.onFrame(std::bind(&WebSocketServer::onWSFrame, + _webSocket.onEvent(std::bind(&WebSocketServer::onWSEvent, this, std::placeholders::_1, - std::placeholders::_2)); - _server->on(_webSocketPath.c_str(), &_webSocket); + std::placeholders::_2, + std::placeholders::_3, + std::placeholders::_4, + std::placeholders::_5, + std::placeholders::_6)); + _server->addHandler(&_webSocket); ESP_LOGV(SVK_TAG, "Registered WebSocket handler: %s", _webSocketPath.c_str()); } - void onWSOpen(PsychicWebSocketClient *client) + void onWSEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, + AwsEventType type, void *arg, uint8_t *data, size_t len) { + switch (type) + { + case WS_EVT_CONNECT: + onWSOpen(client); + break; + case WS_EVT_DISCONNECT: + onWSClose(client); + break; + case WS_EVT_DATA: + { + AwsFrameInfo *info = (AwsFrameInfo *)arg; + if (info->final && info->index == 0 && info->len == len) + { + onWSFrame(client, info, data, len); + } + break; + } + default: + break; + } + } - // when a client connects, we transmit it's id and the current payload + void onWSOpen(AsyncWebSocketClient *client) + { transmitId(client); transmitData(client, WEB_SOCKET_ORIGIN); - ESP_LOGI(SVK_TAG, "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->socket()); + ESP_LOGI(SVK_TAG, "ws[%s][%u] connect", client->remoteIP().toString().c_str(), client->id()); } - void onWSClose(PsychicWebSocketClient *client) + void onWSClose(AsyncWebSocketClient *client) { - ESP_LOGI(SVK_TAG, "ws[%s][%u] disconnect", client->remoteIP().toString().c_str(), client->socket()); + ESP_LOGI(SVK_TAG, "ws[%s][%u] disconnect", client->remoteIP().toString().c_str(), client->id()); } - esp_err_t onWSFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame) + void onWSFrame(AsyncWebSocketClient *client, AwsFrameInfo *info, uint8_t *data, size_t len) { - ESP_LOGV(SVK_TAG, "ws[%s][%u] opcode[%d]", request->client()->remoteIP().toString().c_str(), request->client()->socket(), frame->type); + ESP_LOGV(SVK_TAG, "ws[%s][%u] opcode[%d]", client->remoteIP().toString().c_str(), client->id(), info->opcode); - if (frame->type == HTTPD_WS_TYPE_TEXT) + if (info->opcode == WS_TEXT) { - ESP_LOGV(SVK_TAG, "ws[%s][%u] request: %s", request->client()->remoteIP().toString().c_str(), request->client()->socket(), (char *)frame->payload); + ESP_LOGV(SVK_TAG, "ws[%s][%u] request: %s", client->remoteIP().toString().c_str(), client->id(), (char *)data); JsonDocument jsonDocument; - DeserializationError error = deserializeJson(jsonDocument, (char *)frame->payload, frame->len); + DeserializationError error = deserializeJson(jsonDocument, (char *)data, len); if (!error && jsonDocument.is()) { JsonObject jsonObject = jsonDocument.as(); - _statefulService->update(jsonObject, _stateUpdater, clientId(request->client())); - return ESP_OK; + _statefulService->update(jsonObject, _stateUpdater, clientId(client)); } } - return ESP_OK; } - String clientId(PsychicWebSocketClient *client) + String clientId(AsyncWebSocketClient *client) { - return WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX + String(client->socket()); + return WEB_SOCKET_ORIGIN_CLIENT_ID_PREFIX + String(client->id()); } private: @@ -110,31 +130,23 @@ class WebSocketServer StatefulService *_statefulService; AuthenticationPredicate _authenticationPredicate; SecurityManager *_securityManager; - PsychicHttpServer *_server; - PsychicWebSocketHandler _webSocket; + AsyncWebServer *_server; String _webSocketPath; + AsyncWebSocket _webSocket; - void transmitId(PsychicWebSocketClient *client) + void transmitId(AsyncWebSocketClient *client) { JsonDocument jsonDocument; JsonObject root = jsonDocument.to(); root["type"] = "id"; root["id"] = clientId(client); - // serialize the json to a string String buffer; serializeJson(jsonDocument, buffer); - client->sendMessage(buffer.c_str()); + client->text(buffer.c_str()); } - /** - * Broadcasts the payload to the destination, if provided. Otherwise broadcasts to all clients except the origin, if - * specified. - * - * Original implementation sent clients their own IDs so they could ignore updates they initiated. This approach - * simplifies the client and the server implementation but may not be sufficient for all use-cases. - */ - void transmitData(PsychicWebSocketClient *client, const String &originId) + void transmitData(AsyncWebSocketClient *client, const String &originId) { JsonDocument jsonDocument; JsonObject root = jsonDocument.to(); @@ -142,15 +154,14 @@ class WebSocketServer _statefulService->read(root, _stateReader); - // serialize the json to a string serializeJson(jsonDocument, buffer); if (client) { - client->sendMessage(buffer.c_str()); + client->text(buffer.c_str()); } else { - _webSocket.sendAll(buffer.c_str()); + _webSocket.textAll(buffer.c_str()); } } }; diff --git a/lib/framework/WiFiScanner.cpp b/lib/framework/WiFiScanner.cpp index 4e703c21b..23d0ed4cf 100644 --- a/lib/framework/WiFiScanner.cpp +++ b/lib/framework/WiFiScanner.cpp @@ -14,7 +14,7 @@ #include -WiFiScanner::WiFiScanner(PsychicHttpServer *server, +WiFiScanner::WiFiScanner(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), _securityManager(securityManager) { @@ -37,23 +37,23 @@ void WiFiScanner::begin() ESP_LOGV(SVK_TAG, "Registered GET endpoint: %s", LIST_NETWORKS_SERVICE_PATH); } -esp_err_t WiFiScanner::scanNetworks(PsychicRequest *request) +void WiFiScanner::scanNetworks(AsyncWebServerRequest *request) { if (WiFi.scanComplete() != -1) { WiFi.scanDelete(); WiFi.scanNetworks(true); } - return request->reply(202); + request->send(202); } -esp_err_t WiFiScanner::listNetworks(PsychicRequest *request) +void WiFiScanner::listNetworks(AsyncWebServerRequest *request) { int numNetworks = WiFi.scanComplete(); if (numNetworks > -1) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); JsonArray networks = root["networks"].to(); for (int i = 0; i < numNetworks; i++) { @@ -65,14 +65,15 @@ esp_err_t WiFiScanner::listNetworks(PsychicRequest *request) network["encryption_type"] = (uint8_t)WiFi.encryptionType(i); } - return response.send(); + response->setLength(); + request->send(response); } else if (numNetworks == -1) { - return request->reply(202); + request->send(202); } else { - return scanNetworks(request); + scanNetworks(request); } } diff --git a/lib/framework/WiFiScanner.h b/lib/framework/WiFiScanner.h index a198b8d29..c9bf8ca0a 100644 --- a/lib/framework/WiFiScanner.h +++ b/lib/framework/WiFiScanner.h @@ -18,7 +18,8 @@ #include #include -#include +#include +#include #include #define SCAN_NETWORKS_SERVICE_PATH "/rest/scanNetworks" @@ -27,16 +28,16 @@ class WiFiScanner { public: - WiFiScanner(PsychicHttpServer *server, SecurityManager *securityManager); + WiFiScanner(AsyncWebServer *server, SecurityManager *securityManager); void begin(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; - esp_err_t scanNetworks(PsychicRequest *request); - esp_err_t listNetworks(PsychicRequest *request); + void scanNetworks(AsyncWebServerRequest *request); + void listNetworks(AsyncWebServerRequest *request); }; #endif // end WiFiScanner_h diff --git a/lib/framework/WiFiSettingsService.cpp b/lib/framework/WiFiSettingsService.cpp index bf63dc913..351142969 100644 --- a/lib/framework/WiFiSettingsService.cpp +++ b/lib/framework/WiFiSettingsService.cpp @@ -14,7 +14,7 @@ #include -WiFiSettingsService::WiFiSettingsService(PsychicHttpServer *server, +WiFiSettingsService::WiFiSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket) : _server(server), diff --git a/lib/framework/WiFiSettingsService.h b/lib/framework/WiFiSettingsService.h index 7cac71337..3bc5ea2e9 100644 --- a/lib/framework/WiFiSettingsService.h +++ b/lib/framework/WiFiSettingsService.h @@ -24,7 +24,7 @@ #include #include #include -#include +#include #include #ifndef FACTORY_WIFI_SSID @@ -212,7 +212,7 @@ class WiFiSettings class WiFiSettingsService : public StatefulService { public: - WiFiSettingsService(PsychicHttpServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket); + WiFiSettingsService(AsyncWebServer *server, FS *fs, SecurityManager *securityManager, EventSocket *socket); void initWiFi(); void begin(); @@ -222,7 +222,7 @@ class WiFiSettingsService : public StatefulService String getIP(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; HttpEndpoint _httpEndpoint; FSPersistence _fsPersistence; diff --git a/lib/framework/WiFiStatus.cpp b/lib/framework/WiFiStatus.cpp index 0105b7258..6fd2f90bd 100644 --- a/lib/framework/WiFiStatus.cpp +++ b/lib/framework/WiFiStatus.cpp @@ -14,7 +14,7 @@ #include -WiFiStatus::WiFiStatus(PsychicHttpServer *server, +WiFiStatus::WiFiStatus(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), _securityManager(securityManager) { @@ -61,10 +61,10 @@ void WiFiStatus::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) #endif } -esp_err_t WiFiStatus::wifiStatus(PsychicRequest *request) +void WiFiStatus::wifiStatus(AsyncWebServerRequest *request) { - PsychicJsonResponse response = PsychicJsonResponse(request, false); - JsonObject root = response.getRoot(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); wl_status_t status = WiFi.status(); root["status"] = (uint8_t)status; if (status == WL_CONNECTED) @@ -89,7 +89,8 @@ esp_err_t WiFiStatus::wifiStatus(PsychicRequest *request) } } - return response.send(); + response->setLength(); + request->send(response); } bool WiFiStatus::isConnected() diff --git a/lib/framework/WiFiStatus.h b/lib/framework/WiFiStatus.h index ebde90ddc..78b2b3cf6 100644 --- a/lib/framework/WiFiStatus.h +++ b/lib/framework/WiFiStatus.h @@ -18,7 +18,8 @@ #include #include -#include +#include +#include #include #include @@ -27,21 +28,21 @@ class WiFiStatus { public: - WiFiStatus(PsychicHttpServer *server, SecurityManager *securityManager); + WiFiStatus(AsyncWebServer *server, SecurityManager *securityManager); void begin(); bool isConnected(); private: - PsychicHttpServer *_server; + AsyncWebServer *_server; SecurityManager *_securityManager; // static functions for logging WiFi events to the UART static void onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info); static void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info); static void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info); - esp_err_t wifiStatus(PsychicRequest *request); + void wifiStatus(AsyncWebServerRequest *request); }; #endif // end WiFiStatus_h diff --git a/platformio.ini b/platformio.ini index 419d736d7..de0594615 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,133 +22,143 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ build_flags = ${factory_settings.build_flags} ${features.build_flags} - -D BUILD_TARGET=\"$PIOENV\" - -D APP_NAME=\"ESP32-Sveltekit\" ; Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename - -D APP_VERSION=\"0.6.0\" ; semver compatible version string - - ; Move all networking stuff to the protocol core 0 and leave business logic on application core 1 - -D ESP32SVELTEKIT_RUNNING_CORE=0 - - ; Uncomment EMBED_WWW to embed the WWW data in the firmware binary - -D EMBED_WWW - - ; Uncomment to configure Cross-Origin Resource Sharing - ; -D ENABLE_CORS - ; -D CORS_ORIGIN=\"*\" - - ; Uncomment to enable informations from ESP32-Sveltekit in Serial Monitor - -D SERIAL_INFO - - ; Uncomment to skip SSL certificate verification when downloading firmware updates - -D DOWNLOAD_OTA_SKIP_CERT_VERIFY - - ; D E B U G B U I L D F L A G S - ; =============================== - ; These build flags are only for debugging purposes and should not be used in production - -D CONFIG_ARDUHAL_LOG_COLORS - - ; Uncomment to show log messages from the ESP Arduino Core and ESP32-SvelteKit + -D BUILD_TARGET=\"$PIOENV\" + -D APP_NAME=\"ESP32-Sveltekit\" + -D APP_VERSION=\"0.6.0\" + + -D ESP32SVELTEKIT_RUNNING_CORE=0 + + -D EMBED_WWW + + + -D SERIAL_INFO + + -D DOWNLOAD_OTA_SKIP_CERT_VERIFY + + -D CONFIG_ARDUHAL_LOG_COLORS + -D CORE_DEBUG_LEVEL=4 - - ; Serve config files from flash and access at /config/filename.json - ;-D SERVE_CONFIG_FILES - - ; Uncomment to teleplot all task high watermarks to Serial - ; -D TELEPLOT_TASKS - - ; Uncomment to use JSON instead of MessagePack for event messages. Default is MessagePack. - ; -D EVENT_USE_JSON=1 - lib_compat_mode = strict - -; Uncomment to include the a Root CA SSL Certificate Bundle for all SSL needs -; Needs -D FT_DOWNLOAD_FIRMWARE=1 and -D FT_NTP=1 board_build.embed_files = src/certs/x509_crt_bundle.bin -; Source for SSL Cert Store can bei either downloaded from Mozilla with 'mozilla' ('https://curl.se/ca/cacert.pem') -; or from a curated Adafruit repository with 'adafruit' (https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-filtered.pem) -; or complied from a 'folder' full of *.pem / *.dem files stored in the ./ssl_certs folder -;board_ssl_cert_source = mozilla board_ssl_cert_source = adafruit - monitor_speed = 115200 monitor_filters = esp32_exception_decoder - log2file + log2file board_build.filesystem = littlefs extra_scripts = - pre:scripts/build_interface.py - pre:scripts/generate_cert_bundle.py - scripts/merge_bin.py - scripts/rename_fw.py - scripts/save_elf.py + pre:scripts/build_interface.py + pre:scripts/generate_cert_bundle.py + scripts/merge_bin.py + scripts/rename_fw.py + scripts/save_elf.py lib_deps = ArduinoJson@>=7.0.0 - elims/PsychicMqttClient@^0.2.4 - ESP32Async/ESPAsyncWebServer#v3.10.0 + elims/PsychicMqttClient@^0.2.4 + ESP32Async/ESPAsyncWebServer @ ^3.10.0 [env:esp32-c3-devkitm-1] board = esp32-c3-devkitm-1 board_build.mcu = esp32c3 -; Uncomment min_spiffs.csv setting if using EMBED_WWW with ESP32 board_build.partitions = min_spiffs.csv -; Use USB CDC for firmware upload and serial terminal -; board_upload.before_reset = usb_reset -; build_flags = -; ${env.build_flags} -; -DARDUINO_USB_CDC_ON_BOOT=1 -; -DARDUINO_USB_MODE=1 +platform_packages = platformio/toolchain-xtensa-esp-elf@14.2.0+20241119 [env:esp32-s3-devkitc-1] board = esp32-s3-devkitc-1 board_build.mcu = esp32s3 board_build.partitions = default_8MB.csv -; Use USB CDC for firmware upload and serial terminal -; board_upload.before_reset = usb_reset build_flags = - ${env.build_flags} - -DARDUINO_USB_CDC_ON_BOOT=1 - -DARDUINO_USB_MODE=1 + ${env.build_flags} + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 [env:esp32dev] -; Works for nodemcu-32s, devkit-v1 boards and probably others. You can change the pin defines below if needed. board = esp32dev board_build.partitions = min_spiffs.csv -build_flags = - ${env.build_flags} - -D LED_BUILTIN=2 - -D KEY_BUILTIN=0 +build_flags = + ${env.build_flags} + -D LED_BUILTIN=2 + -D KEY_BUILTIN=0 [env:Kincony-B16M] -; Works for the Kincony B16M smart home controller with Ethernet support board = esp32-s3-devkitc-1 board_build.partitions = default_16MB.csv board_upload.before_reset = usb-reset build_flags = - ${env.build_flags} - -DARDUINO_USB_CDC_ON_BOOT=1 - -DARDUINO_USB_MODE=1 - -DFT_ETHERNET=1 - -DUSE_TWO_ETH_PORTS=0 - -DETH_PHY_TYPE=ETH_PHY_W5500 - -DETH_PHY_ADDR=1 - -DETH_PHY_CS=41 - -DETH_PHY_IRQ=2 ; -1 if you won't wire - -DETH_PHY_RST=1 ; -1 if you won't wire - -DETH_SPI_SCK=42 - -DETH_SPI_MISO=44 - -DETH_SPI_MOSI=43 + ${env.build_flags} + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + -DFT_ETHERNET=1 + -DUSE_TWO_ETH_PORTS=0 + -DETH_PHY_TYPE=ETH_PHY_W5500 + -DETH_PHY_ADDR=1 + -DETH_PHY_CS=41 + -DETH_PHY_IRQ=2 + -DETH_PHY_RST=1 + -DETH_SPI_SCK=42 + -DETH_SPI_MISO=44 + -DETH_SPI_MOSI=43 [env:esp32-wt32-eth01] -; Works for the WT32-ETH01 board with Ethernet support board = wt32-eth01 board_build.partitions = min_spiffs.csv -build_flags = - ${env.build_flags} - ; Not an LED but an unused GPIO pin - -DLED_BUILTIN=14 - -DKEY_BUILTIN=0 - -DFT_ETHERNET=1 -; Only use the filtered Adafruit SSL cert bundle as the full Mozilla bundle is too large for this board with the whole demo app +build_flags = + ${env.build_flags} + -DLED_BUILTIN=14 + -DKEY_BUILTIN=0 + -DFT_ETHERNET=1 board_ssl_cert_source = adafruit - +[factory_settings] +build_flags = + -D FACTORY_WIFI_SSID=\"\" + -D FACTORY_WIFI_PASSWORD=\"\" + -D FACTORY_WIFI_HOSTNAME=\"#{platform}-#{unique_id}\" + -D FACTORY_WIFI_RSSI_THRESHOLD=-80 + + -D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED + -D FACTORY_AP_SSID=\"ESP32-SvelteKit-#{unique_id}\" + -D FACTORY_AP_PASSWORD=\"esp-sveltekit\" + -D FACTORY_AP_CHANNEL=1 + -D FACTORY_AP_SSID_HIDDEN=false + -D FACTORY_AP_MAX_CLIENTS=4 + -D FACTORY_AP_LOCAL_IP=\"192.168.4.1\" + -D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\" + -D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\" + + -D FACTORY_ADMIN_USERNAME=\"admin\" + -D FACTORY_ADMIN_PASSWORD=\"admin\" + -D FACTORY_GUEST_USERNAME=\"guest\" + -D FACTORY_GUEST_PASSWORD=\"guest\" + + -D FACTORY_NTP_ENABLED=true + -D FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/Berlin\" + -D FACTORY_NTP_TIME_ZONE_FORMAT=\"GMT0BST,M3.5.0/1,M10.5.0\" + -D FACTORY_NTP_SERVER=\"time.google.com\" + + -D FACTORY_MQTT_ENABLED=false + -D FACTORY_MQTT_URI=\"mqtts://broker.hivemq.com:8883\" + -D FACTORY_MQTT_USERNAME=\"\" + -D FACTORY_MQTT_PASSWORD=\"\" + -D FACTORY_MQTT_CLIENT_ID=\"#{platform}-#{unique_id}\" + -D FACTORY_MQTT_KEEP_ALIVE=120 + -D FACTORY_MQTT_CLEAN_SESSION=true + -D FACTORY_MQTT_STATUS_TOPIC=\"esp32sveltekit/#{unique_id}/status\" + -D FACTORY_MQTT_MIN_MESSAGE_INTERVAL_MS=0 + + -D FACTORY_JWT_SECRET=\"#{random}-#{random}\" + + -D WAKEUP_PIN_NUMBER=0 + -D WAKEUP_SIGNAL=0 + +[features] +build_flags = + -D FT_SECURITY=1 + -D FT_MQTT=1 + -D FT_NTP=1 + -D FT_UPLOAD_FIRMWARE=1 + -D FT_DOWNLOAD_FIRMWARE=1 + -D FT_SLEEP=1 + -D FT_BATTERY=0 + -D FT_ANALYTICS=1 + -D FT_COREDUMP=1 diff --git a/src/LightMqttSettingsService.cpp b/src/LightMqttSettingsService.cpp index 038100d52..6cd8ea8ff 100644 --- a/src/LightMqttSettingsService.cpp +++ b/src/LightMqttSettingsService.cpp @@ -14,7 +14,7 @@ #include -LightMqttSettingsService::LightMqttSettingsService(PsychicHttpServer *server, +LightMqttSettingsService::LightMqttSettingsService(AsyncWebServer *server, ESP32SvelteKit *sveltekit) : _httpEndpoint(LightMqttSettings::read, LightMqttSettings::update, this, diff --git a/src/LightMqttSettingsService.h b/src/LightMqttSettingsService.h index 6203e8eaa..4b8506ac8 100644 --- a/src/LightMqttSettingsService.h +++ b/src/LightMqttSettingsService.h @@ -59,7 +59,7 @@ class LightMqttSettings class LightMqttSettingsService : public StatefulService { public: - LightMqttSettingsService(PsychicHttpServer *server, ESP32SvelteKit *sveltekit); + LightMqttSettingsService(AsyncWebServer *server, ESP32SvelteKit *sveltekit); void begin(); void onConfigUpdated(); diff --git a/src/LightStateService.cpp b/src/LightStateService.cpp index ee9ebc1b2..37c562fab 100644 --- a/src/LightStateService.cpp +++ b/src/LightStateService.cpp @@ -14,7 +14,7 @@ #include -LightStateService::LightStateService(PsychicHttpServer *server, +LightStateService::LightStateService(AsyncWebServer *server, ESP32SvelteKit *sveltekit, LightMqttSettingsService *lightMqttSettingsService) : _httpEndpoint(LightState::read, LightState::update, diff --git a/src/LightStateService.h b/src/LightStateService.h index 7f95cc9d8..7e3075a58 100644 --- a/src/LightStateService.h +++ b/src/LightStateService.h @@ -84,7 +84,7 @@ class LightState class LightStateService : public StatefulService { public: - LightStateService(PsychicHttpServer *server, + LightStateService(AsyncWebServer *server, ESP32SvelteKit *sveltekit, LightMqttSettingsService *lightMqttSettingsService); diff --git a/src/main.cpp b/src/main.cpp index e4b8ef23c..ee179d619 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,13 +15,13 @@ #include #include #include -#include +#include #define SERIAL_BAUD_RATE 115200 -PsychicHttpServer server; +AsyncWebServer server(80); -ESP32SvelteKit esp32sveltekit(&server, 70); +ESP32SvelteKit esp32sveltekit(&server); LightMqttSettingsService lightMqttSettingsService = LightMqttSettingsService(&server, &esp32sveltekit); From fe8d8519b3c1aea2023398552e2f7161a3f99200 Mon Sep 17 00:00:00 2001 From: thedemoncat Date: Fri, 13 Mar 2026 09:59:22 +0600 Subject: [PATCH 2/3] fix coderabbitai linter --- docs/coredump.md | 335 +++++++++++++++++++ lib/framework/AuthenticationService.cpp | 8 +- lib/framework/CoreDump.cpp | 30 +- lib/framework/DownloadFirmwareService.cpp | 1 - lib/framework/ESP32SvelteKit.cpp | 5 +- lib/framework/EventSocket.cpp | 67 ++-- lib/framework/RestartService.cpp | 3 +- lib/framework/UploadFirmwareService.cpp | 8 +- mkdocs.yml | 2 + platformio.ini | 194 +++++------ tools/analyze_dump.py | 389 ++++++++++++++++++++++ 11 files changed, 887 insertions(+), 155 deletions(-) create mode 100644 docs/coredump.md create mode 100644 tools/analyze_dump.py diff --git a/docs/coredump.md b/docs/coredump.md new file mode 100644 index 000000000..f0d91ad38 --- /dev/null +++ b/docs/coredump.md @@ -0,0 +1,335 @@ +# Core dump analysis + +This page describes what ESP32 core dumps are, how to obtain and analyse them, and how to use the `analyze_dump.py` script. It also includes an example of investigating a stack overflow using the TCB (Task Control Block) table. + +--- + +## What are core dumps? + +A **core dump** is a snapshot of the device state at the moment of a crash or fatal exception. It includes: + +- **CPU register state** (e.g. PC, SP, A0–A15 on Xtensa) +- **Memory regions** (RAM, parts of flash as configured) +- **Task Control Blocks (TCB)** and their **backtraces** for each task + +ESP-IDF can write this snapshot to flash (coredump partition), to a host over UART, or to a file. You then analyse it on the host with the same **ELF** binary that was running on the device so that addresses can be resolved to symbols (functions, line numbers). + +### Why use them? + +- **Post-mortem debugging**: Understand why the device crashed without needing a debugger attached. +- **Field diagnostics**: Get a dump from a device in the field (e.g. via UART or stored in flash) and analyse it later with the matching ELF. +- **Stack overflows**: See which task overflowed (e.g. TCB6) and what the call stack was. + +### How they are generated + +1. **Enable core dumps** in menuconfig: + **Component config → ESP System Settings → Core dump destination** (e.g. Flash, UART, or Disable). +2. When a fatal exception occurs (crash, abort, panic), the **core dump component** runs and writes the snapshot to the chosen destination. +3. You **retrieve the dump** (read from flash, capture from UART, or use a file) and save it as a binary file (e.g. `coredump.bin`). + +--- + +## Working with core dumps + +### 1. Get the dump file + +- **Flash**: Use `espcoredump.py` or your IDF/PlatformIO workflow to read the coredump partition and save to a file. +- **UART**: Configure “Core dump destination → UART” and capture the binary stream to a file when the device panics. +- **File**: If you already have a `.bin` file from flash or UART, use it as the `core-dump-file` argument for the script. + +### 2. Have the matching ELF + +The dump was produced by a specific firmware build. You **must** use the **same** ELF that was flashed (or one built from the same commit). The script can search for it by: + +- **SHA256 hash** stored in the dump (recommended), or +- **Standard names** (e.g. `firmware.elf`, `ssvc_open_connect.elf`) or “latest” ELF in the project. + +If the ELF does not match, addresses may resolve to wrong symbols or “???”. + +### 3. Run the analysis script + +The project script `tools/analyze_dump.py`: + +1. Loads the core dump and reads its metadata (e.g. SHA256). +2. Finds a suitable GDB (e.g. `xtensa-esp32s3-elf-gdb`) and, on Windows, can set up the ESP-IDF environment. +3. Locates the matching ELF (by hash, name, or fallback). +4. Runs the ESP-IDF coredump analysis (backtraces, panic reason, etc.). +5. Prints a **GDB command** so you can open the core file in GDB for interactive inspection. + +--- + +## Script usage and configuration + +### Basic usage + +```bash +python tools/analyze_dump.py +``` + +Example: + +```bash +python tools/analyze_dump.py coredump.bin +``` + +The script will: + +- On **Windows**: Try to initialize the ESP-IDF environment using the default PowerShell profile (if present). +- Search for **GDB** in standard ESP-IDF locations and in `PATH`. +- Load the dump, extract the SHA256, and search for an ELF with that hash in the project (e.g. `build`, `build/elf`, etc.). +- Run the core dump decoder and print the summary plus the GDB command. + +### Arguments + +| Argument | Short | Description | +|----------|--------|-------------| +| `core-dump-file` | — | **Required.** Path to the core dump file (e.g. `coredump.bin`). | +| `--prog` | — | Path to the application ELF file. If omitted, the script searches by hash or standard names. | +| `--gdb` | `-g` | Path to the GDB executable (e.g. `xtensa-esp32s3-elf-gdb`). If omitted, the script searches automatically. | +| `--elf-dir` | `-e` | Additional directory to search for ELF files. Can be repeated. | +| `--chip` | — | Target chip (`auto`, or a supported target). Default: `auto`. | +| `--port` | `-p` | Serial port (for workflows that need it). | +| `--baud` | `-b` | Baud rate. Default: 115200. | +| `--ps-profile` | — | **(Windows)** Path to ESP-IDF PowerShell profile. Default: standard Espressif path. | +| `--skip-esp-setup` | — | Skip ESP-IDF environment setup (e.g. if PATH is already set). | + +### Configuration tips + +- **ELF not found**: Use `--prog path/to/firmware.elf` or add search dirs with `--elf-dir build --elf-dir ../other/build`. +- **GDB not found**: Install the ESP-IDF toolchain or pass GDB explicitly: + `--gdb /path/to/xtensa-esp32s3-elf-gdb` (Linux/macOS) or + `--gdb "C:\Espressif\tools\...\xtensa-esp32s3-elf-gdb.exe"` (Windows). +- **Linux/macOS**: The script does not run the Windows PowerShell profile. Source your ESP-IDF environment in the shell if needed, e.g. `source $IDF_PATH/export.sh`, so that `xtensa-esp32s3-elf-gdb` is in `PATH` if you do not pass `--gdb`. + +### Example commands + +```bash +# Automatic ELF and GDB detection +python tools/analyze_dump.py coredump.bin + +# Specify ELF and GDB +python tools/analyze_dump.py --prog build/firmware.elf --gdb /opt/esp/tools/xtensa-esp32s3-elf-gdb coredump.bin + +# Add extra directories for ELF search +python tools/analyze_dump.py --elf-dir build --elf-dir ../firmware/build coredump.bin + +# Skip ESP-IDF setup (e.g. already in correct environment) +python tools/analyze_dump.py --skip-esp-setup coredump.bin +``` + +--- + +## Example: investigating a stack overflow using the TCB table + +When a task’s stack overflows, the panic output and the core dump often point to a specific **TCB** (Task Control Block). The decoder prints backtraces **per task**; the one that overflowed is usually the crashing task (e.g. **TCB6** in this example). + +### 1. Typical panic output + +You might see something like: + +```text +CORRUPT HEAP: Bad head at 0x3fcxxxxx. Expected 0xabba1234 got 0x... +... +Task with corrupted stack: TCB6 +``` + +or a **Stack canary watchpoint** or **LoadProhibited** in the context of a task name that corresponds to one of the TCBs. + +### 2. Run the analysis script + +```bash +python tools/analyze_dump.py coredump.bin +``` + +The script will print a summary that includes **backtraces for each task**, often labelled by TCB index (e.g. “Backtrace for TCB0”, “Backtrace for TCB6”, …). + +### 3. Find the faulty task (e.g. TCB6) + +- In the script output, locate the backtrace for **TCB6** (or the TCB number mentioned in the panic). +- If the panic reason says “Stack canary” or “corrupted stack” and points to TCB6, that task’s stack overflowed. + +Example of what you might see: + +```text +Backtrace for TCB6: 0x4008abcd:0x3fc9f000 0x40081234:0x3fc9f020 ... +``` + +### 4. Resolve addresses to symbols + +The script uses the ELF to resolve these addresses. In the summary you should see function names (and possibly files) for each frame. If not, run GDB as suggested at the end of the script output: + +```bash +python -m esp_coredump dbg_corefile --gdb "/path/to/xtensa-esp32s3-elf-gdb" --core coredump.bin build/firmware.elf +``` + +In GDB: + +- **Backtrace for the crashing task**: + `thread apply 6 bt full` + (if TCB6 is thread 6; adjust the thread number to match your decoder output). +- **Frame and locals**: + `f 0` then `info locals`. +- **All threads**: + `info threads` then `thread N` and `bt full` for the overflowing task. + +### 5. Interpret the result for TCB6 + +- **Cause**: The stack of the task corresponding to TCB6 grew beyond its allocated size (e.g. large local buffers, deep recursion, or big structures on the stack). +- **Fix**: + - Increase this task’s stack size in the code (e.g. `xTaskCreate(..., stack_size, ...)` or `CONFIG_..._TASK_STACK_SIZE` in menuconfig), or + - Reduce stack usage: move large buffers to heap, reduce recursion depth, or split work. + +### 6. Summary table (example) + +| Step | Action | +|------|--------| +| 1 | Reproduce crash and capture core dump (flash/UART/file). | +| 2 | Run `python tools/analyze_dump.py coredump.bin`. | +| 3 | In the output, find the TCB mentioned in the panic (e.g. TCB6) and its backtrace. | +| 4 | Resolve symbols (script output or GDB: `bt full`, `thread apply N bt full`). | +| 5 | Identify the task and the functions that use most stack (large locals, recursion). | +| 6 | Increase that task’s stack size or reduce stack usage. | + +--- + +## Example: Task Watchdog timeout + +When a task does not yield for too long (no `vTaskDelay`, blocking calls, or heavy computation in a tight loop), the **Task Watchdog** can fire and reset the chip. + +### 1. Typical panic output + +You may see something similar to: + +```text +Task watchdog got triggered. The following tasks did not reset the watchdog in time: + - idle (TCB3) + - comm-task (TCB6) +... +abort() was called at PC 0x400e1234 on core 0 +``` + +### 2. Run the analysis script + +```bash +python tools/analyze_dump.py coredump.bin +``` + +Look for the **backtrace of the task mentioned in the panic** (e.g. `comm-task` / `TCB6`). The decoder output will usually contain a section with backtraces per TCB. + +### 3. Inspect the blocking code in GDB + +Open the core dump in GDB using the command printed by the script, then: + +```gdb +info threads # list all tasks +thread N # select the thread corresponding to comm-task / TCB6 +bt full # see the full backtrace and local variables +``` + +Check where the task is stuck: + +- Tight loop without `vTaskDelay` or yielding. +- Long-running computation on the main / communication task. +- Waiting on a mutex/semaphore from ISR-unsafe context. + +### 4. Fix + +- Insert **periodic delays** or yields inside long loops (e.g. `vTaskDelay(1)`). +- Offload heavy computation to a **dedicated worker task** with its own stack and lower priority. +- Ensure blocking calls have **timeouts** and that you handle them properly. + +--- + +## Example: Heap corruption / use-after-free + +Heap corruption is harder to debug because the crash often happens **later** than the actual bug. Core dumps help by showing the state of the heap and backtraces of all tasks at the moment of failure. + +### 1. Typical panic output + +You might see one of: + +```text +CORRUPT HEAP: Bad head at 0x3fcxxxxx. Expected 0xabba1234 got 0x... +assertion "heap != NULL && "free() target pointer is outside heap areas"" failed +``` + +### 2. Analyse the dump + +```bash +python tools/analyze_dump.py coredump.bin +``` + +Then, open GDB: + +```bash +python -m esp_coredump dbg_corefile --gdb /path/to/xtensa-esp32s3-elf-gdb --core coredump.bin build/firmware.elf +``` + +In GDB: + +```gdb +bt full # if crash is in malloc/free/heap functions +info threads # inspect other threads that might be freeing the same memory +``` + +Look for: + +- Double `free()` on the same pointer. +- Free from one task and later use (dereference) from another (**use-after-free**). +- Writing past the end of a dynamically allocated buffer. + +### 3. Fix + +- Use **ownership rules**: one task “owns” a buffer and is responsible for freeing it. +- Replace raw pointers with safer patterns (e.g. reference counting, ownership flags). +- Add **bounds checks** for all buffer writes, especially when parsing external data. + +--- + +## Example: ASSERT / abort (ESP_ERROR_CHECK) + +ESP-IDF often calls `abort()` when an `assert()` or `ESP_ERROR_CHECK()` fails. Core dumps make it easy to jump **exactly** to the source line that triggered the abort. + +### 1. Typical panic output + +```text +abort() was called at PC 0x400d5678 on core 1 + +ELF file SHA256: ... +Backtrace: 0x400d5678:0x3ffb1f10 0x4008abcd:0x3ffb1f30 ... +``` + +### 2. Find the abort location + +Run: + +```bash +python tools/analyze_dump.py coredump.bin +``` + +Then open GDB (using the script’s suggested command) and run: + +```gdb +bt full # see the full stack +f 0 # go to the top frame where abort() was called +list # show the source code around the current line +``` + +You will typically land inside `ESP_ERROR_CHECK`/`assert` or just above it, seeing: + +- The **function** that failed. +- The **error code** or condition that was not met. + +### 3. Fix + +- If it is an `ESP_ERROR_CHECK`, handle the error instead of unconditionally aborting (log, retry, fail gracefully). +- If it is an `assert`, either: + - Fix the logic so the invariant holds, or + - Replace `assert` with a **runtime check** that reports a controlled error instead of aborting in production. + +--- + +## References + +- **ESP-IDF Core dump documentation**: [Core Dump](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/core_dump.html) (Espressif). +- **Project script**: `tools/analyze_dump.py` (English, supports Windows and Linux/macOS). diff --git a/lib/framework/AuthenticationService.cpp b/lib/framework/AuthenticationService.cpp index 9e2d2c07f..929d3fedb 100644 --- a/lib/framework/AuthenticationService.cpp +++ b/lib/framework/AuthenticationService.cpp @@ -24,8 +24,8 @@ AuthenticationService::AuthenticationService(AsyncWebServer *server, SecurityMan void AuthenticationService::begin() { - _server->on(SIGN_IN_PATH, HTTP_POST, [this](AsyncWebServerRequest *request, JsonVariant &json) - { + auto signInCallback = [this](AsyncWebServerRequest *request, JsonVariant &json) + { if (json.is()) { String username = json["username"]; String password = json["password"]; @@ -39,7 +39,9 @@ void AuthenticationService::begin() return; } } - request->send(401); }); + request->send(401); + }; + _server->on(SIGN_IN_PATH, HTTP_POST, _securityManager->wrapCallback(signInCallback, AuthenticationPredicates::NONE_REQUIRED)); ESP_LOGV(SVK_TAG, "Registered POST endpoint: %s", SIGN_IN_PATH); diff --git a/lib/framework/CoreDump.cpp b/lib/framework/CoreDump.cpp index ca57ca106..3a3defd9f 100644 --- a/lib/framework/CoreDump.cpp +++ b/lib/framework/CoreDump.cpp @@ -19,8 +19,6 @@ #include "esp_partition.h" #include "esp_flash.h" -#define MIN(a, b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; }) - CoreDump::CoreDump(AsyncWebServer *server, SecurityManager *securityManager) : _server(server), _securityManager(securityManager) @@ -49,17 +47,21 @@ void CoreDump::coreDump(AsyncWebServerRequest *request) } ESP_LOGI(SVK_TAG, "Coredump is %u bytes", coredump_size); - AsyncWebServerResponse *response = request->beginChunkedResponse("application/octet-stream", - [coredump_addr, coredump_size](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { - if (index >= coredump_size) { - return 0; - } - size_t read_len = MIN(maxLen, coredump_size - index); - if (esp_flash_read(esp_flash_default_chip, buffer, coredump_addr + index, read_len)) { - ESP_LOGE(SVK_TAG, "Coredump read failed"); - return 0; - } - return read_len; - }); + uint8_t *buffer = (uint8_t *)malloc(coredump_size); + if (!buffer) + { + request->send(500, "text/plain", "coredump alloc failed"); + return; + } + err = esp_flash_read(esp_flash_default_chip, buffer, coredump_addr, coredump_size); + if (err != ESP_OK) + { + ESP_LOGE(SVK_TAG, "Coredump read failed"); + free(buffer); + request->send(500, "text/plain", "coredump read failed"); + return; + } + AsyncWebServerResponse *response = request->beginResponse(200, "application/octet-stream", buffer, coredump_size); request->send(response); + free(buffer); } \ No newline at end of file diff --git a/lib/framework/DownloadFirmwareService.cpp b/lib/framework/DownloadFirmwareService.cpp index 66162e304..4cb44b23c 100644 --- a/lib/framework/DownloadFirmwareService.cpp +++ b/lib/framework/DownloadFirmwareService.cpp @@ -57,7 +57,6 @@ const char *githubCACertificate = "-----BEGIN CERTIFICATE-----\n" static EventSocket *_socket = nullptr; static int previousProgress = 0; -static String *otaURL = nullptr; JsonDocument doc; void update_started() diff --git a/lib/framework/ESP32SvelteKit.cpp b/lib/framework/ESP32SvelteKit.cpp index 539dd0e41..a0b2fdaf6 100644 --- a/lib/framework/ESP32SvelteKit.cpp +++ b/lib/framework/ESP32SvelteKit.cpp @@ -74,8 +74,6 @@ void ESP32SvelteKit::begin() _wifiSettingsService.initWiFi(); - _server->begin(); - #ifdef EMBED_WWW // Serve static resources from PROGMEM ESP_LOGV(SVK_TAG, "Registering routes from PROGMEM static resources"); @@ -189,6 +187,7 @@ void ESP32SvelteKit::begin() _sleepService.begin(); _sleepService.attachOnSleepCallback([&]() { ESP_LOGI(SVK_TAG, "Attempting to stop server"); + _server->end(); vTaskDelete(_loopTaskHandle); ESP_LOGI(SVK_TAG, "Server stopped"); }); #if FT_ENABLED(FT_MQTT) @@ -205,6 +204,8 @@ void ESP32SvelteKit::begin() _analyticsService.begin(); #endif + _server->begin(); + // Start the loop task ESP_LOGV(SVK_TAG, "Starting loop task"); xTaskCreatePinnedToCore( diff --git a/lib/framework/EventSocket.cpp b/lib/framework/EventSocket.cpp index 2311b69da..4a74bc544 100644 --- a/lib/framework/EventSocket.cpp +++ b/lib/framework/EventSocket.cpp @@ -139,14 +139,8 @@ void EventSocket::emitEvent(String event, JsonObject &jsonObject, const char *or } int originSubscriptionId = originId[0] ? atoi(originId) : -1; - xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY); - auto &subscriptions = client_subscriptions[event]; - if (subscriptions.empty()) - { - xSemaphoreGive(clientSubscriptionsMutex); - return; - } + std::vector targetClients; JsonDocument doc; doc["event"] = event; doc["data"] = jsonObject; @@ -167,42 +161,55 @@ void EventSocket::emitEvent(String event, JsonObject &jsonObject, const char *or output[len] = '\0'; + xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY); + auto &subscriptions = client_subscriptions[event]; + if (subscriptions.empty()) + { + xSemaphoreGive(clientSubscriptionsMutex); + delete[] output; + return; + } + if (onlyToSameOrigin && originSubscriptionId > 0) { - auto *client = _socket.client((uint32_t)originSubscriptionId); - if (client) + targetClients.push_back(originSubscriptionId); + } + else + { + for (int subscription : subscriptions) { - ESP_LOGV(SVK_TAG, "Emitting event: %s to %s[%u], Message[%d]: %s", event, client->remoteIP().toString().c_str(), client->id(), len, output); -#if FT_ENABLED(EVENT_USE_JSON) - client->text(output, len); -#else - client->binary(output, len); -#endif + if (subscription != originSubscriptionId) + targetClients.push_back(subscription); } } - else + xSemaphoreGive(clientSubscriptionsMutex); + + std::vector toRemove; + for (int subscription : targetClients) { - for (int subscription : client_subscriptions[event]) + auto *client = _socket.client((uint32_t)subscription); + if (!client) { - if (subscription == originSubscriptionId) - continue; - auto *client = _socket.client((uint32_t)subscription); - if (!client) - { - subscriptions.remove(subscription); - continue; - } - ESP_LOGV(SVK_TAG, "Emitting event: %s to %s[%u], Message[%d]: %s", event, client->remoteIP().toString().c_str(), client->id(), len, output); + toRemove.push_back(subscription); + continue; + } + ESP_LOGV(SVK_TAG, "Emitting event: %s to %s[%u], Message[%d]: %s", event, client->remoteIP().toString().c_str(), client->id(), len, output); #if FT_ENABLED(EVENT_USE_JSON) - client->text(output, len); + client->text(output, len); #else - client->binary(output, len); + client->binary(output, len); #endif - } + } + + if (!toRemove.empty()) + { + xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY); + for (int id : toRemove) + client_subscriptions[event].remove(id); + xSemaphoreGive(clientSubscriptionsMutex); } delete[] output; - xSemaphoreGive(clientSubscriptionsMutex); } void EventSocket::handleEventCallbacks(String event, JsonObject &jsonObject, int originId) diff --git a/lib/framework/RestartService.cpp b/lib/framework/RestartService.cpp index 490bf43f5..cb9e2568f 100644 --- a/lib/framework/RestartService.cpp +++ b/lib/framework/RestartService.cpp @@ -31,6 +31,7 @@ void RestartService::begin() void RestartService::restart(AsyncWebServerRequest *request) { + request->onDisconnect([]() + { restartNow(); }); request->send(200); - restartNow(); } diff --git a/lib/framework/UploadFirmwareService.cpp b/lib/framework/UploadFirmwareService.cpp index 87550dc85..c0661a452 100644 --- a/lib/framework/UploadFirmwareService.cpp +++ b/lib/framework/UploadFirmwareService.cpp @@ -177,8 +177,10 @@ void UploadFirmwareService::handleUpload(AsyncWebServerRequest *request, return; } - if (Update.begin(fsize - sizeof(esp_image_header_t))) + if (Update.begin(fsize)) { + request->onDisconnect([this]() { handleEarlyDisconnect(); }); + if (_socket) { JsonDocument doc; @@ -359,6 +361,10 @@ void UploadFirmwareService::handleError(AsyncWebServerRequest *request, int code void UploadFirmwareService::handleEarlyDisconnect() { + if (!Update.isRunning()) + { + return; + } if (!Update.end(true)) { ESP_LOGE(SVK_TAG, "Update error on early disconnect:"); diff --git a/mkdocs.yml b/mkdocs.yml index f7e2d94dd..2f1136ae3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,8 @@ nav: - "Build Tools": - gettingstarted.md - buildprocess.md + - "Debugging": + - coredump.md - "Front End": - sveltekit.md - structure.md diff --git a/platformio.ini b/platformio.ini index de0594615..3bff943c9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,143 +22,131 @@ platform = https://github.com/pioarduino/platform-espressif32/releases/download/ build_flags = ${factory_settings.build_flags} ${features.build_flags} - -D BUILD_TARGET=\"$PIOENV\" - -D APP_NAME=\"ESP32-Sveltekit\" - -D APP_VERSION=\"0.6.0\" - - -D ESP32SVELTEKIT_RUNNING_CORE=0 - - -D EMBED_WWW - - - -D SERIAL_INFO - - -D DOWNLOAD_OTA_SKIP_CERT_VERIFY - - -D CONFIG_ARDUHAL_LOG_COLORS - + -D BUILD_TARGET=\"$PIOENV\" + -D APP_NAME=\"ESP32-Sveltekit\" ; Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename + -D APP_VERSION=\"0.6.0\" ; semver compatible version string + + ; Move all networking stuff to the protocol core 0 and leave business logic on application core 1 + -D ESP32SVELTEKIT_RUNNING_CORE=0 + + ; Uncomment EMBED_WWW to embed the WWW data in the firmware binary + -D EMBED_WWW + + ; Uncomment to configure Cross-Origin Resource Sharing + ; -D ENABLE_CORS + ; -D CORS_ORIGIN=\"*\" + + ; Uncomment to enable informations from ESP32-Sveltekit in Serial Monitor + -D SERIAL_INFO + + ; Uncomment to skip SSL certificate verification when downloading firmware updates + -D DOWNLOAD_OTA_SKIP_CERT_VERIFY + + ; D E B U G B U I L D F L A G S + ; =============================== + ; These build flags are only for debugging purposes and should not be used in production + -D CONFIG_ARDUHAL_LOG_COLORS + + ; Uncomment to show log messages from the ESP Arduino Core and ESP32-SvelteKit -D CORE_DEBUG_LEVEL=4 + + ; Serve config files from flash and access at /config/filename.json + ;-D SERVE_CONFIG_FILES + + ; Uncomment to teleplot all task high watermarks to Serial + ; -D TELEPLOT_TASKS + + ; Uncomment to use JSON instead of MessagePack for event messages. Default is MessagePack. + ; -D EVENT_USE_JSON=1 + lib_compat_mode = strict + +; Uncomment to include the a Root CA SSL Certificate Bundle for all SSL needs +; Needs -D FT_DOWNLOAD_FIRMWARE=1 and -D FT_NTP=1 board_build.embed_files = src/certs/x509_crt_bundle.bin +; Source for SSL Cert Store can bei either downloaded from Mozilla with 'mozilla' ('https://curl.se/ca/cacert.pem') +; or from a curated Adafruit repository with 'adafruit' (https://raw.githubusercontent.com/adafruit/certificates/main/data/roots-filtered.pem) +; or complied from a 'folder' full of *.pem / *.dem files stored in the ./ssl_certs folder +;board_ssl_cert_source = mozilla board_ssl_cert_source = adafruit + monitor_speed = 115200 monitor_filters = esp32_exception_decoder - log2file + log2file board_build.filesystem = littlefs extra_scripts = - pre:scripts/build_interface.py - pre:scripts/generate_cert_bundle.py - scripts/merge_bin.py - scripts/rename_fw.py - scripts/save_elf.py + pre:scripts/build_interface.py + pre:scripts/generate_cert_bundle.py + scripts/merge_bin.py + scripts/rename_fw.py + scripts/save_elf.py lib_deps = ArduinoJson@>=7.0.0 - elims/PsychicMqttClient@^0.2.4 - ESP32Async/ESPAsyncWebServer @ ^3.10.0 + elims/PsychicMqttClient@^0.2.4 + ESP32Async/ESPAsyncWebServer#v3.10.0 [env:esp32-c3-devkitm-1] board = esp32-c3-devkitm-1 board_build.mcu = esp32c3 +; Uncomment min_spiffs.csv setting if using EMBED_WWW with ESP32 board_build.partitions = min_spiffs.csv -platform_packages = platformio/toolchain-xtensa-esp-elf@14.2.0+20241119 +; Use USB CDC for firmware upload and serial terminal +; board_upload.before_reset = usb_reset +; build_flags = +; ${env.build_flags} +; -DARDUINO_USB_CDC_ON_BOOT=1 +; -DARDUINO_USB_MODE=1 [env:esp32-s3-devkitc-1] board = esp32-s3-devkitc-1 board_build.mcu = esp32s3 board_build.partitions = default_8MB.csv +; Use USB CDC for firmware upload and serial terminal +; board_upload.before_reset = usb_reset build_flags = - ${env.build_flags} - -DARDUINO_USB_CDC_ON_BOOT=1 - -DARDUINO_USB_MODE=1 + ${env.build_flags} + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 [env:esp32dev] +; Works for nodemcu-32s, devkit-v1 boards and probably others. You can change the pin defines below if needed. board = esp32dev board_build.partitions = min_spiffs.csv -build_flags = - ${env.build_flags} - -D LED_BUILTIN=2 - -D KEY_BUILTIN=0 +build_flags = + ${env.build_flags} + -D LED_BUILTIN=2 + -D KEY_BUILTIN=0 [env:Kincony-B16M] +; Works for the Kincony B16M smart home controller with Ethernet support board = esp32-s3-devkitc-1 board_build.partitions = default_16MB.csv board_upload.before_reset = usb-reset build_flags = - ${env.build_flags} - -DARDUINO_USB_CDC_ON_BOOT=1 - -DARDUINO_USB_MODE=1 - -DFT_ETHERNET=1 - -DUSE_TWO_ETH_PORTS=0 - -DETH_PHY_TYPE=ETH_PHY_W5500 - -DETH_PHY_ADDR=1 - -DETH_PHY_CS=41 - -DETH_PHY_IRQ=2 - -DETH_PHY_RST=1 - -DETH_SPI_SCK=42 - -DETH_SPI_MISO=44 - -DETH_SPI_MOSI=43 + ${env.build_flags} + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + -DFT_ETHERNET=1 + -DUSE_TWO_ETH_PORTS=0 + -DETH_PHY_TYPE=ETH_PHY_W5500 + -DETH_PHY_ADDR=1 + -DETH_PHY_CS=41 + -DETH_PHY_IRQ=2 ; -1 if you won't wire + -DETH_PHY_RST=1 ; -1 if you won't wire + -DETH_SPI_SCK=42 + -DETH_SPI_MISO=44 + -DETH_SPI_MOSI=43 [env:esp32-wt32-eth01] +; Works for the WT32-ETH01 board with Ethernet support board = wt32-eth01 board_build.partitions = min_spiffs.csv -build_flags = - ${env.build_flags} - -DLED_BUILTIN=14 - -DKEY_BUILTIN=0 - -DFT_ETHERNET=1 +build_flags = + ${env.build_flags} + ; Not an LED but an unused GPIO pin + -DLED_BUILTIN=14 + -DKEY_BUILTIN=0 + -DFT_ETHERNET=1 +; Only use the filtered Adafruit SSL cert bundle as the full Mozilla bundle is too large for this board with the whole demo app board_ssl_cert_source = adafruit - -[factory_settings] -build_flags = - -D FACTORY_WIFI_SSID=\"\" - -D FACTORY_WIFI_PASSWORD=\"\" - -D FACTORY_WIFI_HOSTNAME=\"#{platform}-#{unique_id}\" - -D FACTORY_WIFI_RSSI_THRESHOLD=-80 - - -D FACTORY_AP_PROVISION_MODE=AP_MODE_DISCONNECTED - -D FACTORY_AP_SSID=\"ESP32-SvelteKit-#{unique_id}\" - -D FACTORY_AP_PASSWORD=\"esp-sveltekit\" - -D FACTORY_AP_CHANNEL=1 - -D FACTORY_AP_SSID_HIDDEN=false - -D FACTORY_AP_MAX_CLIENTS=4 - -D FACTORY_AP_LOCAL_IP=\"192.168.4.1\" - -D FACTORY_AP_GATEWAY_IP=\"192.168.4.1\" - -D FACTORY_AP_SUBNET_MASK=\"255.255.255.0\" - - -D FACTORY_ADMIN_USERNAME=\"admin\" - -D FACTORY_ADMIN_PASSWORD=\"admin\" - -D FACTORY_GUEST_USERNAME=\"guest\" - -D FACTORY_GUEST_PASSWORD=\"guest\" - - -D FACTORY_NTP_ENABLED=true - -D FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/Berlin\" - -D FACTORY_NTP_TIME_ZONE_FORMAT=\"GMT0BST,M3.5.0/1,M10.5.0\" - -D FACTORY_NTP_SERVER=\"time.google.com\" - - -D FACTORY_MQTT_ENABLED=false - -D FACTORY_MQTT_URI=\"mqtts://broker.hivemq.com:8883\" - -D FACTORY_MQTT_USERNAME=\"\" - -D FACTORY_MQTT_PASSWORD=\"\" - -D FACTORY_MQTT_CLIENT_ID=\"#{platform}-#{unique_id}\" - -D FACTORY_MQTT_KEEP_ALIVE=120 - -D FACTORY_MQTT_CLEAN_SESSION=true - -D FACTORY_MQTT_STATUS_TOPIC=\"esp32sveltekit/#{unique_id}/status\" - -D FACTORY_MQTT_MIN_MESSAGE_INTERVAL_MS=0 - - -D FACTORY_JWT_SECRET=\"#{random}-#{random}\" - - -D WAKEUP_PIN_NUMBER=0 - -D WAKEUP_SIGNAL=0 - -[features] -build_flags = - -D FT_SECURITY=1 - -D FT_MQTT=1 - -D FT_NTP=1 - -D FT_UPLOAD_FIRMWARE=1 - -D FT_DOWNLOAD_FIRMWARE=1 - -D FT_SLEEP=1 - -D FT_BATTERY=0 - -D FT_ANALYTICS=1 - -D FT_COREDUMP=1 diff --git a/tools/analyze_dump.py b/tools/analyze_dump.py new file mode 100644 index 000000000..3e9788da7 --- /dev/null +++ b/tools/analyze_dump.py @@ -0,0 +1,389 @@ +#!/usr/bin/python3 +import re +import sys +import os +import argparse +import glob +import subprocess +from construct import (Bytes, Int32ul, Struct) +from esp_coredump.corefile.elf import ESPCoreDumpElfFile +from esp_coredump.corefile.loader import ESPCoreDumpFileLoader +from esp_coredump.corefile import SUPPORTED_TARGETS +from esp_coredump.corefile.gdb import DEFAULT_GDB_TIMEOUT_SEC +from esp_coredump import CoreDump,__version__ + +try: + from esptool.loader import ESPLoader +except (AttributeError, ModuleNotFoundError): + from esptool import ESPLoader + +def arg_auto_int(x): + return int(x, 0) + +def setup_esp_idf_environment(ps_profile_path): + """Configures ESP-IDF environment using PowerShell profile (Windows only).""" + if sys.platform != 'win32': + return False + if not ps_profile_path or not os.path.exists(ps_profile_path): + print(f"WARNING: PowerShell profile file not found: {ps_profile_path}") + return False + + print(f"Initializing ESP-IDF using profile: {ps_profile_path}") + + try: + cmd = [ + "powershell.exe", + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "-Command", + f". '{ps_profile_path}'; $env:PATH" + ] + + result = subprocess.run(cmd, capture_output=True, text=True, timeout=15) + + if result.returncode == 0: + print("✓ ESP-IDF environment initialized successfully") + + path_lines = result.stdout.strip().split('\n') + if path_lines: + new_path = path_lines[-1] + os.environ['PATH'] = new_path + print(f"✓ PATH updated ({len(new_path.split(';'))} entries)") + + return True + else: + print(f"✗ Error initializing ESP-IDF: {result.stderr}") + return False + + except subprocess.TimeoutExpired: + print("✗ Timeout while initializing ESP-IDF") + return False + except Exception as e: + print(f"✗ Error running PowerShell: {e}") + return False + +def find_elf_by_partial_hash(partial_hash, search_dirs=None): + """Finds an ELF file by partial SHA256 hash in the filename.""" + if search_dirs is None: + search_dirs = ['.', 'build', 'build/elf', 'build/elf/esp32-s3-devkitc-1-16m'] + + all_elf_files = [] + + for search_dir in search_dirs: + if os.path.isdir(search_dir): + pattern = os.path.join(search_dir, "**", "*.elf") + elf_files = glob.glob(pattern, recursive=True) + all_elf_files.extend(elf_files) + + pattern = os.path.join(search_dir, "*.elf") + elf_files = glob.glob(pattern, recursive=False) + all_elf_files.extend(elf_files) + + all_elf_files = list(set(all_elf_files)) + + print(f"Found {len(all_elf_files)} ELF files") + + matching_files = [] + for elf_file in all_elf_files: + filename = os.path.basename(elf_file) + filename_no_ext = os.path.splitext(filename)[0] + + if partial_hash.lower() in filename_no_ext.lower(): + matching_files.append(elf_file) + + return matching_files + +def find_gdb_in_esp_idf(): + """Finds GDB in typical ESP-IDF installation paths (Windows and Linux).""" + if sys.platform == 'win32': + possible_paths = [ + "C:\\Espressif\\tools\\xtensa-esp32s3-elf\\esp-*\\xtensa-esp32s3-elf\\bin\\xtensa-esp32s3-elf-gdb.exe", + "C:\\Espressif\\tools\\riscv32-esp-elf\\esp-*\\riscv32-esp-elf\\bin\\riscv32-esp-elf-gdb.exe", + os.path.expanduser("~/.espressif/tools/**/*gdb*.exe"), + "gdb", "xtensa-esp32s3-elf-gdb", "riscv32-esp-elf-gdb" + ] + else: + possible_paths = [ + os.path.expanduser("~/.espressif/tools/xtensa-esp-elf/*/xtensa-esp-elf/bin/xtensa-esp-elf-gdb"), + os.path.expanduser("~/.espressif/tools/xtensa-esp32s3-elf/*/xtensa-esp32s3-elf/bin/xtensa-esp32s3-elf-gdb"), + os.path.expanduser("~/.espressif/tools/riscv32-esp-elf/*/riscv32-esp-elf/bin/riscv32-esp-elf-gdb"), + "/opt/esp/tools/xtensa-esp32s3-elf/*/xtensa-esp32s3-elf/bin/xtensa-esp32s3-elf-gdb", + "xtensa-esp32s3-elf-gdb", "riscv32-esp-elf-gdb", "gdb" + ] + + for pattern in possible_paths: + try: + files = glob.glob(pattern, recursive=True) + for file in files: + if 'gdb' in os.path.basename(file).lower() and os.path.isfile(file): + print(f"Found GDB: {file}") + return file + except Exception: + continue + + return None + +# ====== ESP-IDF ENVIRONMENT INITIALIZATION (Windows only) ====== +PS_PROFILE_PATH = "C:\\Espressif\\tools\\Microsoft.v5.5.2.PowerShell_profile.ps1" if sys.platform == 'win32' else "" + +print("=" * 60) +print("ESP-IDF ENVIRONMENT INITIALIZATION") +print("=" * 60) + +if sys.platform == 'win32': + if not os.path.exists(PS_PROFILE_PATH): + print(f"WARNING: Profile file not found: {PS_PROFILE_PATH}") + print(" Ensure ESP-IDF is installed correctly.") + print(" You can specify another path with --ps-profile") + else: + setup_success = setup_esp_idf_environment(PS_PROFILE_PATH) + if not setup_success: + print("\nWARNING: Continuing without full ESP-IDF initialization...") + print(" Some features may be unavailable.") +else: + print("Linux/macOS: ESP-IDF PowerShell setup skipped (use 'source export.sh' in your shell if needed).") + +print("\n" + "=" * 60) + +parser = argparse.ArgumentParser(description=f'coredump.py - ESP32 Core Dump Utility') +parser.add_argument('--chip', default='auto', choices=['auto'] + SUPPORTED_TARGETS, help='Target chip type') +parser.add_argument('--port', '-p', help='Serial port device') +parser.add_argument('--baud', '-b', type=int, default=115200, help='Serial port baud rate') +parser.add_argument('--prog', help='Path to application ELF file') +parser.add_argument('--gdb', '-g', help='Path to GDB executable') +parser.add_argument('--elf-dir', '-e', action='append', help='Additional directories to search for ELF files') +parser.add_argument('--ps-profile', default=PS_PROFILE_PATH or None, + help='Path to ESP-IDF PowerShell profile (Windows only)') +parser.add_argument('--skip-esp-setup', action='store_true', + help='Skip ESP-IDF environment setup') +parser.add_argument('core-dump-file', help='Path to core dump file') + +args = parser.parse_args() +kwargs = {k: v for k, v in vars(args).items() if v is not None} +kwargs["core"] = kwargs.pop('core-dump-file') + +# ====== 1. RE-INITIALIZE ESP-IDF (if user specified another profile) ====== +if not kwargs.get('skip_esp_setup') and sys.platform == 'win32': + ps_profile_path = kwargs.get('ps_profile') or PS_PROFILE_PATH + if ps_profile_path != PS_PROFILE_PATH or 'Espressif' not in os.environ.get('PATH', ''): + if ps_profile_path and os.path.exists(ps_profile_path): + print(f"\nUsing custom profile: {ps_profile_path}") + setup_esp_idf_environment(ps_profile_path) + elif not os.path.exists(PS_PROFILE_PATH or ''): + print(f"WARNING: Profile file not found: {PS_PROFILE_PATH}") + print(" Trying to find GDB without environment setup...") + +# ====== 2. FIND GDB ====== +if 'gdb' not in kwargs or not kwargs['gdb']: + print("\nSearching for GDB...") + + if sys.platform == 'win32': + espressif_paths = [ + "C:\\Espressif\\tools\\xtensa-esp32s3-elf\\esp-*\\xtensa-esp32s3-elf\\bin\\xtensa-esp32s3-elf-gdb.exe", + "C:\\Espressif\\tools\\riscv32-esp-elf\\esp-*\\riscv32-esp-elf\\bin\\riscv32-esp-elf-gdb.exe", + "C:\\Espressif\\frameworks\\esp-idf-v*\\tools\\**\\*gdb*.exe" + ] + else: + espressif_paths = [ + os.path.expanduser("~/.espressif/tools/xtensa-esp32s3-elf/*/xtensa-esp32s3-elf/bin/xtensa-esp32s3-elf-gdb"), + os.path.expanduser("~/.espressif/tools/riscv32-esp-elf/*/riscv32-esp-elf/bin/riscv32-esp-elf-gdb"), + ] + + for pattern in espressif_paths: + try: + files = glob.glob(pattern, recursive=True) + for file in files: + if os.path.isfile(file): + kwargs['gdb'] = file + print(f"Found GDB in ESP-IDF: {file}") + break + if kwargs.get('gdb'): + break + except Exception: + continue + + if not kwargs.get('gdb'): + found_gdb = find_gdb_in_esp_idf() + if found_gdb: + kwargs['gdb'] = found_gdb + + if not kwargs.get('gdb'): + gdb_names = ['xtensa-esp32s3-elf-gdb', 'riscv32-esp-elf-gdb', 'gdb'] + which_cmd = 'where' if sys.platform == 'win32' else 'which' + for gdb_name in gdb_names: + try: + result = subprocess.run([which_cmd, gdb_name], capture_output=True, text=True) + if result.returncode == 0 and result.stdout.strip(): + kwargs['gdb'] = result.stdout.strip().split('\n')[0].strip() + print(f"Found GDB in PATH: {kwargs['gdb']}") + break + except Exception: + continue + +if kwargs.get('gdb'): + if not os.path.exists(kwargs['gdb']): + print(f"ERROR: GDB not found at: {kwargs['gdb']}") + print("\nPlease specify GDB path in one of these ways:") + print("1. Install ESP-IDF toolchain") + print("2. Specify GDB explicitly: --gdb /path/to/xtensa-esp32s3-elf-gdb") + sys.exit(1) + print(f"Using GDB: {kwargs['gdb']}") +else: + print("WARNING: GDB not found!") + print("Analysis may fail.") + +# ====== 3. LOAD CORE DUMP ====== +print(f"\nLoading core dump: {kwargs['core']}") +loader = ESPCoreDumpFileLoader(kwargs["core"]) +loader._load_core_src() + +loader.core_elf_file = loader._create_temp_file() +with open(loader.core_elf_file, 'wb') as fw: + fw.write(loader.core_src.data) + +core_elf = ESPCoreDumpElfFile(loader.core_elf_file, e_machine=ESPCoreDumpElfFile.EM_XTENSA) + +core_sha_trimmed = None +for seg in core_elf.note_segments: + for note_sec in seg.note_secs: + if note_sec.name == b'ESP_CORE_DUMP_INFO' and note_sec.type == ESPCoreDumpElfFile.PT_ESP_INFO: + coredump_sha256_struct = Struct('ver' / Int32ul, 'sha256' / Bytes(64)) + coredump_sha256 = coredump_sha256_struct.parse(note_sec.desc[:coredump_sha256_struct.sizeof()]) + core_sha_trimmed = coredump_sha256.sha256.rstrip(b'\x00').decode() + print(f'Core dump SHA256: {core_sha_trimmed}') + +# ====== 4. FIND ELF FILE ====== +if 'prog' not in kwargs or not kwargs['prog']: + search_dirs = ['.', 'build', 'build/elf', 'build/elf/esp32-s3-devkitc-1-16m', '../build', '../../build'] + + if 'elf_dir' in kwargs: + if isinstance(kwargs['elf_dir'], list): + search_dirs.extend(kwargs['elf_dir']) + else: + search_dirs.append(kwargs['elf_dir']) + + if core_sha_trimmed: + print(f"\nSearching for ELF file with partial hash: {core_sha_trimmed}") + matching_files = find_elf_by_partial_hash(core_sha_trimmed, search_dirs) + + if matching_files: + if len(matching_files) == 1: + kwargs["prog"] = matching_files[0] + print(f"Found ELF file: {matching_files[0]}") + else: + print(f"Found multiple matching ELF files:") + for i, elf_file in enumerate(matching_files): + print(f" {i+1}. {elf_file}") + + kwargs["prog"] = matching_files[0] + print(f"Using: {matching_files[0]}") + else: + print("\nNo ELF files with this hash found, searching for standard names...") + + standard_elf_names = [ + "ssvc_open_connect.elf", + "firmware.elf", + "main.elf", + "app.elf" + ] + + for search_dir in search_dirs: + if os.path.isdir(search_dir): + for elf_name in standard_elf_names: + elf_path = os.path.join(search_dir, elf_name) + if os.path.exists(elf_path): + kwargs["prog"] = elf_path + print(f"Found standard ELF: {elf_path}") + break + if 'prog' in kwargs: + break + + if 'prog' not in kwargs: + print("\nSearching for any ELF file in project...") + all_elf_files = [] + for search_dir in search_dirs: + if os.path.isdir(search_dir): + pattern = os.path.join(search_dir, "**", "*.elf") + elf_files = glob.glob(pattern, recursive=True) + all_elf_files.extend(elf_files) + + if all_elf_files: + all_elf_files.sort(key=lambda x: os.path.getmtime(x) if os.path.exists(x) else 0, reverse=True) + + print(f"Found {len(all_elf_files)} ELF files") + print("Latest 5 files:") + from datetime import datetime + for i, elf_file in enumerate(all_elf_files[:5]): + mtime = os.path.getmtime(elf_file) + mtime_str = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S') + print(f" {i+1}. {os.path.basename(elf_file)} - {mtime_str}") + + kwargs["prog"] = all_elf_files[0] + print(f"\nUsing latest ELF file: {all_elf_files[0]}") + else: + print("\nERROR: No ELF file found!") + print("Please specify the ELF file explicitly:") + print(" python analyze_dump.py --prog path/to/file.elf coredump.bin") + print("\nOr specify search directories:") + print(" python analyze_dump.py --elf-dir build --elf-dir ../build coredump.bin") + sys.exit(1) + +print(f"\nUsing ELF file: {kwargs['prog']}") + +# ====== 5. RUN ANALYSIS ====== +print("\nRunning core dump analysis...") +print("=" * 60) + +final_elf = kwargs.get('prog') +final_core = kwargs.get('core') + +for key in ['elf_dir', 'ps_profile', 'skip_esp_setup']: + if key in kwargs: + del kwargs[key] + +try: + espcoredump = CoreDump(**kwargs) + temp_core_files = espcoredump.info_corefile() + + if temp_core_files: + for f in temp_core_files: + try: + os.remove(f) + except OSError: + pass + + print("\n✓ Analysis completed successfully!") + + print("\n" + "=" * 60) + print("GDB COMMAND (INTERACTIVE DEBUGGING)") + print("=" * 60) + + gdb_path = kwargs.get('gdb') + + if gdb_path: + debug_cmd = f"python -m esp_coredump dbg_corefile --gdb \"{gdb_path}\" --core {final_core} {final_elf}" + else: + debug_cmd = f"python -m esp_coredump dbg_corefile --core {final_core} {final_elf}" + + print("To inspect variables and stack in GDB, run:") + print(f"\n{debug_cmd}\n") + print("Useful GDB commands:") + print(" bt full - show backtrace and all variable values") + print(" f 0 - switch to crash frame") + print("=" * 60) + +except Exception as e: + print(f"\n✗ Error analyzing core dump: {e}") + + if "GDB executable not found" in str(e): + print("\nGDB issue. Try:") + print("1. Specify GDB path: --gdb /path/to/xtensa-esp32s3-elf-gdb") + if sys.platform == 'win32': + print("2. Ensure ESP-IDF profile exists: C:\\Espressif\\tools\\Microsoft.v5.5.2.PowerShell_profile.ps1") + print("3. Run C:\\Espressif\\tools\\idf-env.exe or install via ESP-IDF Tools Installer") + + print("\nTry specifying the ELF file explicitly:") + print(f" python analyze_dump.py --prog build/ssvc_open_connect.elf coredump.bin") + + sys.exit(1) + From a4dce4ce701d7279693e3a944ca40e9d47168098 Mon Sep 17 00:00:00 2001 From: thedemoncat Date: Fri, 13 Mar 2026 10:10:12 +0600 Subject: [PATCH 3/3] Dependencies --- docs/coredump.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/coredump.md b/docs/coredump.md index f0d91ad38..ef2a67045 100644 --- a/docs/coredump.md +++ b/docs/coredump.md @@ -4,6 +4,43 @@ This page describes what ESP32 core dumps are, how to obtain and analyse them, a --- +## Dependencies + +The script `tools/analyze_dump.py` requires Python 3.7+ and the following packages: + +| Package | Purpose | +|---------|--------| +| **esp-coredump** | Load and decode ESP32 core dumps, run GDB. | +| *construct* | Parsing binary structures (installed with esp-coredump). | +| *esptool* | Optional; used by esp-coredump for some operations. | + +### Install with pip + +From the project root: + +Install the main package only (dependencies will be pulled in): + +```bash +pip install esp-coredump +``` + +Use a virtual environment to avoid conflicts with other projects: + +```bash +python -m venv .venv +# Windows: +.venv\Scripts\activate +# Linux/macOS: +source .venv/bin/activate +pip install esp-coredump +``` + +### GDB (xtensa toolchain) + +The script also needs a GDB for your chip (e.g. `xtensa-esp32s3-elf-gdb`). It is usually provided by the **ESP-IDF toolchain**. If you use PlatformIO or ESP-IDF, the toolchain is often already installed; the script will try to find GDB automatically. Otherwise install the [ESP-IDF tools](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/) or pass GDB explicitly with `--gdb /path/to/xtensa-esp32s3-elf-gdb`. + +--- + ## What are core dumps? A **core dump** is a snapshot of the device state at the moment of a crash or fatal exception. It includes: