diff --git a/project-words.txt b/project-words.txt index e04c14ef7..aba4a3bb0 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1336,4 +1336,8 @@ handshaked startm netifs testemail -sendmail \ No newline at end of file +sendmail +serialises +SPIRAM +optimisations +IILE \ No newline at end of file diff --git a/src/ESP32React/APStatus.cpp b/src/ESP32React/APStatus.cpp index 09d8c39b0..c04a39cdc 100644 --- a/src/ESP32React/APStatus.cpp +++ b/src/ESP32React/APStatus.cpp @@ -10,7 +10,7 @@ APStatus::APStatus(AsyncWebServer * server, SecurityManager * securityManager, A } void APStatus::apStatus(AsyncWebServerRequest * request) { - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); root["status"] = _apSettingsService->getAPNetworkStatus(); diff --git a/src/ESP32React/AuthenticationService.cpp b/src/ESP32React/AuthenticationService.cpp index 4ef1308a0..c4c02ec6b 100644 --- a/src/ESP32React/AuthenticationService.cpp +++ b/src/ESP32React/AuthenticationService.cpp @@ -1,5 +1,7 @@ #include "AuthenticationService.h" +#include "../core/psram_async_json_response.h" + AuthenticationService::AuthenticationService(AsyncWebServer * server, SecurityManager * securityManager) : _securityManager(securityManager) { // none of these need authentication @@ -23,7 +25,7 @@ void AuthenticationService::signIn(AsyncWebServerRequest * request, JsonVariant Authentication authentication = _securityManager->authenticate(username, password); if (authentication.authenticated) { User * user = authentication.user; - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject jsonObject = response->getRoot(); jsonObject["access_token"] = _securityManager->generateJWT(user); response->setLength(); diff --git a/src/ESP32React/HttpEndpoint.h b/src/ESP32React/HttpEndpoint.h index 3b46a9e18..06c902bdb 100644 --- a/src/ESP32React/HttpEndpoint.h +++ b/src/ESP32React/HttpEndpoint.h @@ -7,6 +7,7 @@ #include "SecurityManager.h" #include "StatefulService.h" +#include "../core/psram_async_json_response.h" #define HTTP_ENDPOINT_ORIGIN_ID "http" @@ -58,7 +59,7 @@ class HttpEndpoint { } } - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject jsonObject = response->getRoot().to(); _statefulService->read(jsonObject, _stateReader); response->setLength(); diff --git a/src/ESP32React/MqttStatus.cpp b/src/ESP32React/MqttStatus.cpp index 6923eeff8..f93091ba9 100644 --- a/src/ESP32React/MqttStatus.cpp +++ b/src/ESP32React/MqttStatus.cpp @@ -10,7 +10,7 @@ MqttStatus::MqttStatus(AsyncWebServer * server, MqttSettingsService * mqttSettin } void MqttStatus::mqttStatus(AsyncWebServerRequest * request) { - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); root["enabled"] = _mqttSettingsService->isEnabled(); diff --git a/src/ESP32React/NTPStatus.cpp b/src/ESP32React/NTPStatus.cpp index 9abe0f9ba..424a51c60 100644 --- a/src/ESP32React/NTPStatus.cpp +++ b/src/ESP32React/NTPStatus.cpp @@ -30,7 +30,7 @@ String toLocalTimeString(tm * time) { } void NTPStatus::ntpStatus(AsyncWebServerRequest * request) { - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); // grab the current instant in unix seconds diff --git a/src/ESP32React/NetworkStatus.cpp b/src/ESP32React/NetworkStatus.cpp index e01ee2fb5..8f99cfa74 100644 --- a/src/ESP32React/NetworkStatus.cpp +++ b/src/ESP32React/NetworkStatus.cpp @@ -13,7 +13,7 @@ NetworkStatus::NetworkStatus(AsyncWebServer * server, SecurityManager * security } void NetworkStatus::networkStatus(AsyncWebServerRequest * request) { - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); wl_status_t wifi_status = WiFi.status(); diff --git a/src/ESP32React/SecuritySettingsService.cpp b/src/ESP32React/SecuritySettingsService.cpp index bae1c0d74..c88b8e88c 100644 --- a/src/ESP32React/SecuritySettingsService.cpp +++ b/src/ESP32React/SecuritySettingsService.cpp @@ -1,5 +1,7 @@ #include "SecuritySettingsService.h" +#include "../core/psram_async_json_response.h" + SecuritySettingsService::SecuritySettingsService(AsyncWebServer * server, FS * fs) : _httpEndpoint(SecuritySettings::read, SecuritySettings::update, this, server, SECURITY_SETTINGS_PATH, this) , _fsPersistence(SecuritySettings::read, SecuritySettings::update, this, fs, SECURITY_SETTINGS_FILE) @@ -112,7 +114,7 @@ void SecuritySettingsService::generateToken(AsyncWebServerRequest * request) { auto usernameParam = request->getParam("username"); for (const User & _user : _state.users) { if (_user.username == usernameParam->value()) { - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); root["token"] = generateJWT(&_user); response->setLength(); diff --git a/src/ESP32React/UploadFileService.cpp b/src/ESP32React/UploadFileService.cpp index 3561546ca..4e58b0d8b 100644 --- a/src/ESP32React/UploadFileService.cpp +++ b/src/ESP32React/UploadFileService.cpp @@ -189,7 +189,7 @@ void UploadFileService::uploadComplete(AsyncWebServerRequest * request) { // add MD5 to the response if (strlen(_md5.data()) == _md5.size() - 1) { - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); root["md5"] = _md5.data(); response->setLength(); diff --git a/src/ESP32React/WiFiScanner.cpp b/src/ESP32React/WiFiScanner.cpp index 6b222e107..d5ceb13e2 100644 --- a/src/ESP32React/WiFiScanner.cpp +++ b/src/ESP32React/WiFiScanner.cpp @@ -1,5 +1,7 @@ #include "WiFiScanner.h" +#include "../core/psram_async_json_response.h" + WiFiScanner::WiFiScanner(AsyncWebServer * server, SecurityManager * securityManager) { securityManager->addEndpoint(server, SCAN_NETWORKS_SERVICE_PATH, AuthenticationPredicates::IS_ADMIN, [this](AsyncWebServerRequest * request) { scanNetworks(request); @@ -22,7 +24,7 @@ void WiFiScanner::scanNetworks(AsyncWebServerRequest * request) { void WiFiScanner::listNetworks(AsyncWebServerRequest * request) { const int numNetworks = WiFi.scanComplete(); if (numNetworks > -1) { - auto * response = new AsyncJsonResponse(false); + auto * response = new emsesp::PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); JsonArray networks = root["networks"].to(); for (uint8_t i = 0; i < numNetworks; i++) { diff --git a/src/core/emsesp.h b/src/core/emsesp.h index e4f49da79..920f16309 100644 --- a/src/core/emsesp.h +++ b/src/core/emsesp.h @@ -57,6 +57,7 @@ #include "../web/WebModulesService.h" #include "psram_json_allocator.h" +#include "psram_async_json_response.h" #include "emsdevicevalue.h" #include "emsdevice.h" #include "emsfactory.h" diff --git a/src/core/psram_async_json_response.h b/src/core/psram_async_json_response.h new file mode 100644 index 000000000..ccd79b0c6 --- /dev/null +++ b/src/core/psram_async_json_response.h @@ -0,0 +1,143 @@ +/* + * EMS-ESP - https://github.com/emsesp/EMS-ESP + * Copyright 2020-2025 emsesp.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef EMSESP_PSRAM_ASYNC_JSON_RESPONSE_H +#define EMSESP_PSRAM_ASYNC_JSON_RESPONSE_H + +#include "psram_json_allocator.h" + +#ifndef EMSESP_STANDALONE +#include +#include +#else +#include +#endif + +namespace emsesp { + +// AsyncJsonResponse subclass whose JsonDocument lives in PSRAM instead of +// internal SRAM. +// +// Why: every web API response goes through AsyncJsonResponse. The library's +// base class declares `JsonDocument _jsonBuffer;` with the *default* +// allocator, which on ESP32 means malloc() → internal heap. For large +// payloads (Dashboard, /rest/coreData, /rest/sensorData, full settings, +// customizations, etc.) this transiently consumes many KB of the same +// internal heap that LwIP / AsyncTCP / mbedTLS also need. Each concurrent +// browser tab compounds the cost. +// +// We can't change the base class's _jsonBuffer allocator (the upstream +// constructor doesn't take one), but we can route around it: keep our own +// PSRAM-backed document, override the virtual setLength()/_fillBuffer() so +// the framework serialises *our* document, and name-hide getRoot() so +// callers populate *our* document. The base's _jsonBuffer stays empty +// (just one root slot, <~32 bytes). +// +// Callers must use the derived type (or `auto`) when calling getRoot(), +// because getRoot() is non-virtual in the base. `request->send(response)` +// works as-is because setLength()/_fillBuffer() ARE virtual in the +// AsyncAbstractResponse grandparent. +// +// On standalone the lib_standalone AsyncJsonResponse stub never actually +// serves responses, so this whole class still compiles and behaves +// identically (allocator falls back to malloc anyway). +class PsramAsyncJsonResponse : public ::AsyncJsonResponse { + public: + explicit PsramAsyncJsonResponse(bool isArray = false) + : ::AsyncJsonResponse(isArray) + , psram_doc_(PsramJsonAllocator::instance()) { + if (isArray) { + psram_root_ = psram_doc_.add(); + } else { + psram_root_ = psram_doc_.add(); + } + } + + // Hides AsyncJsonResponse::getRoot(). Must be called through a + // derived-type pointer/reference (the framework's base pointer keeps + // pointing at the empty base _jsonBuffer, which is intentional). + JsonVariant getRoot() { + return psram_root_; + } + +#ifndef EMSESP_STANDALONE + size_t setLength() override { + _contentLength = measureJson(psram_root_); + if (_contentLength) { + _isValid = true; + } + return _contentLength; + } + + size_t _fillBuffer(uint8_t * data, size_t len) override { + ChunkPrint dest(data, _sentLength, len); + serializeJson(psram_root_, dest); + return dest.written(); + } +#endif + + private: + JsonDocument psram_doc_; + JsonVariant psram_root_; +}; + +#if !defined(EMSESP_STANDALONE) && defined(ASYNC_MSG_PACK_SUPPORT) && ASYNC_MSG_PACK_SUPPORT == 1 +// MessagePack equivalent — same routing trick but serialises with MsgPack. +class PsramAsyncMessagePackResponse : public ::AsyncMessagePackResponse { + public: + explicit PsramAsyncMessagePackResponse(bool isArray = false) + : ::AsyncMessagePackResponse(isArray) + , psram_doc_(PsramJsonAllocator::instance()) { + if (isArray) { + psram_root_ = psram_doc_.add(); + } else { + psram_root_ = psram_doc_.add(); + } + } + + JsonVariant getRoot() { + return psram_root_; + } + + size_t setLength() override { + _contentLength = measureMsgPack(psram_root_); + if (_contentLength) { + _isValid = true; + } + return _contentLength; + } + + size_t _fillBuffer(uint8_t * data, size_t len) override { + ChunkPrint dest(data, _sentLength, len); + serializeMsgPack(psram_root_, dest); + return dest.written(); + } + + private: + JsonDocument psram_doc_; + JsonVariant psram_root_; +}; +#else +// Standalone or no msgpack support: alias to plain JSON response so the +// codebase compiles unchanged. +using PsramAsyncMessagePackResponse = PsramAsyncJsonResponse; +#endif + +} // namespace emsesp + +#endif diff --git a/src/web/WebAPIService.cpp b/src/web/WebAPIService.cpp index 9f0a551ac..4ffbf5d3b 100644 --- a/src/web/WebAPIService.cpp +++ b/src/web/WebAPIService.cpp @@ -108,7 +108,7 @@ void WebAPIService::parse(AsyncWebServerRequest * request, JsonObject input) { EMSESP::system_.refreshHeapMem(); // output json buffer - auto response = new AsyncJsonResponse(); + auto response = new PsramAsyncJsonResponse(); // add more mem if needed - won't be needed in ArduinoJson 7 // while (!response->getSize()) { diff --git a/src/web/WebActivityService.cpp b/src/web/WebActivityService.cpp index cfc4ba79c..bde0c1f53 100644 --- a/src/web/WebActivityService.cpp +++ b/src/web/WebActivityService.cpp @@ -27,7 +27,7 @@ WebActivityService::WebActivityService(AsyncWebServer * server, SecurityManager } void WebActivityService::webActivityService(AsyncWebServerRequest * request) { - auto * response = new AsyncJsonResponse(false); + auto * response = new PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); JsonArray statsJson = root["stats"].to(); diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index 5ea045dde..00e87c303 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -58,7 +58,7 @@ WebDataService::WebDataService(AsyncWebServer * server, SecurityManager * securi // this is used in the Devices page and contains all EMS device information // /coreData endpoint void WebDataService::core_data(AsyncWebServerRequest * request) { - auto * response = new AsyncJsonResponse(false); + auto * response = new PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); // list is already sorted by device type @@ -70,7 +70,7 @@ void WebDataService::core_data(AsyncWebServerRequest * request) { obj["id"] = emsdevice->unique_id(); // a unique id obj["tn"] = emsdevice->device_type_2_device_name_translated(); // translated device type name obj["t"] = emsdevice->device_type(); // device type number - obj["b"] = emsdevice->brand_to_cstr(); // brand + obj["b"] = emsdevice->brand_to_char(); // brand (std::string → copied into doc, safe across async serialize) obj["n"] = emsdevice->name(); // custom name obj["d"] = emsdevice->device_id(); // deviceid obj["p"] = emsdevice->product_id(); // productid @@ -104,7 +104,7 @@ void WebDataService::core_data(AsyncWebServerRequest * request) { // sensor data - sends back to web // /sensorData endpoint void WebDataService::sensor_data(AsyncWebServerRequest * request) { - auto * response = new AsyncJsonResponse(false); + auto * response = new PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); // temperature sensors @@ -176,7 +176,7 @@ void WebDataService::device_data(AsyncWebServerRequest * request) { if (request->hasParam(F_(id))) { id = Helpers::atoint(request->getParam(F_(id))->value().c_str()); // get id from url - auto * response = new AsyncMessagePackResponse(); + auto * response = new PsramAsyncMessagePackResponse(); // check size // while (!response) { @@ -217,6 +217,9 @@ void WebDataService::device_data(AsyncWebServerRequest * request) { return; } #endif + // no matching device and not CUSTOM_UID: we never called request->send(response), + // so AsyncWebServer never took ownership. Delete it ourselves to avoid leaking. + delete response; } // invalid @@ -269,7 +272,7 @@ void WebDataService::write_device_value(AsyncWebServerRequest * request, JsonVar return; } // create JSON for output - auto * response = new AsyncJsonResponse(false); + auto * response = new PsramAsyncJsonResponse(false); JsonObject output = response->getRoot(); // the data could be in any format, but we need string // authenticated is always true diff --git a/src/web/WebLogService.cpp b/src/web/WebLogService.cpp index 05bef58a1..42df5cc6e 100644 --- a/src/web/WebLogService.cpp +++ b/src/web/WebLogService.cpp @@ -193,7 +193,7 @@ void WebLogService::transmit(const QueuedLogMessage & message) { void WebLogService::getSetValues(AsyncWebServerRequest * request, JsonVariant json) { if ((request->method() == HTTP_GET) || (!json.is())) { // GET - return the values - auto * response = new AsyncJsonResponse(false); + auto * response = new PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); root["level"] = level_; root["max_messages"] = maximum_log_messages_; diff --git a/src/web/WebSettingsService.cpp b/src/web/WebSettingsService.cpp index cbbfadfd2..93d3f956b 100644 --- a/src/web/WebSettingsService.cpp +++ b/src/web/WebSettingsService.cpp @@ -414,7 +414,7 @@ void WebSettingsService::board_profile(AsyncWebServerRequest * request) { if (request->hasParam("boardProfile")) { std::string board_profile = request->getParam("boardProfile")->value().c_str(); - auto * response = new AsyncJsonResponse(false); + auto * response = new PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); // 0=led, 1=dallas, 2=rx, 3=tx, 4=button, 5=phy_type, 6=eth_power, 7=eth_phy_addr, 8=eth_clock_mode, 9=led_type diff --git a/src/web/WebStatusService.cpp b/src/web/WebStatusService.cpp index c723a3059..7523241a6 100644 --- a/src/web/WebStatusService.cpp +++ b/src/web/WebStatusService.cpp @@ -47,7 +47,7 @@ WebStatusService::WebStatusService(AsyncWebServer * server, SecurityManager * se void WebStatusService::systemStatus(AsyncWebServerRequest * request) { EMSESP::system_.refreshHeapMem(); // refresh free heap and max alloc heap - auto * response = new AsyncJsonResponse(false); + auto * response = new PsramAsyncJsonResponse(false); JsonObject root = response->getRoot(); // @@ -187,7 +187,7 @@ void WebStatusService::systemStatus(AsyncWebServerRequest * request) { // generic action handler - as a POST void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json) { - auto * response = new AsyncJsonResponse(); + auto * response = new PsramAsyncJsonResponse(); JsonObject root = response->getRoot(); // param is optional - https://arduinojson.org/news/2024/09/18/arduinojson-7-2/