/* * 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_JSON_ALLOCATOR_H #define EMSESP_PSRAM_JSON_ALLOCATOR_H #include #ifndef EMSESP_STANDALONE #include #endif namespace emsesp { // PSRAM-backed ArduinoJson allocator with internal-heap fallback. // // Rationale: by default ArduinoJson allocates with malloc(), which on the // ESP32 lands in internal DRAM. Large transient JsonDocuments (full MQTT // payload, HA discovery configs, Web API responses, settings load/save) // were eating multiple KB of the same internal heap that LwIP, mbedTLS and // AsyncTCP also need. Routing them through SPIRAM via heap_caps_malloc // keeps internal heap available for the network stack, at the cost of a // small latency penalty on PSRAM reads/writes (a few cycles per access, // negligible for JSON build-up which is dominated by string formatting). // // On the standalone (Linux) build, PSRAM doesn't exist; the allocator // silently falls through to plain malloc/free/realloc. // // Usage: // JsonDocument doc(emsesp::PsramJsonAllocator::instance()); // or with the convenience macro: // JsonDocument doc(PSRAM_DOC); class PsramJsonAllocator : public ArduinoJson::Allocator { public: void * allocate(size_t size) override { #ifdef EMSESP_STANDALONE return malloc(size); #else // Try SPIRAM first; fall back to internal heap so we never fail // on boards without PSRAM or when PSRAM is full. void * p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM); if (p == nullptr) { p = malloc(size); } return p; #endif } void deallocate(void * ptr) override { #ifdef EMSESP_STANDALONE free(ptr); #else // heap_caps_free handles both PSRAM- and internal-heap pointers. heap_caps_free(ptr); #endif } void * reallocate(void * ptr, size_t new_size) override { #ifdef EMSESP_STANDALONE return realloc(ptr, new_size); #else // Prefer keeping the block in PSRAM; heap_caps_realloc will move // the data if the original region can't be grown in-place. void * p = heap_caps_realloc(ptr, new_size, MALLOC_CAP_SPIRAM); if (p == nullptr) { p = realloc(ptr, new_size); } return p; #endif } static ArduinoJson::Allocator * instance() { static PsramJsonAllocator inst; return &inst; } private: PsramJsonAllocator() = default; ~PsramJsonAllocator() = default; }; } // namespace emsesp // Convenience shorthand. Use only for *large* or *transient* JsonDocuments // (MQTT publish payloads, HA discovery, full API responses, big settings // load/save). For small hot-path docs (single-command output, parse of a // short HTTP body), keep the default allocator: PSRAM has higher access // latency than internal SRAM, so tiny docs are faster on the regular heap. #define PSRAM_DOC emsesp::PsramJsonAllocator::instance() #endif