From 2e4f1a0216d19de6ac82b4048a284ee216dadfd8 Mon Sep 17 00:00:00 2001 From: Clive Blackledge Date: Sat, 27 Sep 2025 22:49:14 -0700 Subject: [PATCH] Add moar stored messages when oflfine as well. Also using psram. --- src/mesh/MemoryPool.h | 70 ++++++++++++++++++++++++++++++++++++ src/mesh/MeshService.cpp | 12 +++++-- src/mesh/Router.cpp | 18 ++++++++++ src/mesh/mesh-pb-constants.h | 39 +++++++++++++++++--- 4 files changed, 132 insertions(+), 7 deletions(-) diff --git a/src/mesh/MemoryPool.h b/src/mesh/MemoryPool.h index eb5ac5109..c77227904 100644 --- a/src/mesh/MemoryPool.h +++ b/src/mesh/MemoryPool.h @@ -8,6 +8,10 @@ #include "PointerQueue.h" #include "configuration.h" // For LOG_WARN, LOG_DEBUG, LOG_HEAP +#if defined(ARCH_ESP32) +#include +#endif + template class Allocator { @@ -159,3 +163,69 @@ template class MemoryPool : public Allocator return nullptr; } }; + +#if defined(ARCH_ESP32) +// Simple fixed-size allocator that uses PSRAM. Used on ESP32-S3 builds so the +// large MeshPacket pool can live off-chip and free internal RAM. +template class PsramMemoryPool : public Allocator +{ + private: + T *pool; + bool used[MaxSize]; + + public: + PsramMemoryPool() : pool(nullptr), used{} + { + pool = static_cast(heap_caps_malloc(sizeof(T) * MaxSize, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)); + if (pool) { + memset(pool, 0, sizeof(T) * MaxSize); + } else { + LOG_WARN("Failed to allocate PSRAM pool of %d elements", MaxSize); + } + } + + ~PsramMemoryPool() override + { + if (pool) { + heap_caps_free(pool); + } + } + + bool isValid() const { return pool != nullptr; } + + void release(T *p) override + { + if (!pool || !p) { + LOG_DEBUG("Failed to release PSRAM memory, pointer is null or pool unavailable"); + return; + } + + int index = static_cast(p - pool); + if (index >= 0 && index < MaxSize) { + assert(used[index]); + used[index] = false; + LOG_HEAP("Released PSRAM pool item %d at 0x%x", index, p); + } else { + LOG_WARN("Pointer 0x%x not from PSRAM pool!", p); + } + } + + protected: + T *alloc(TickType_t maxWait) override + { + if (!pool) + return nullptr; + + for (int i = 0; i < MaxSize; i++) { + if (!used[i]) { + used[i] = true; + LOG_HEAP("Allocated PSRAM pool item %d at 0x%x", i, &pool[i]); + return &pool[i]; + } + } + + LOG_WARN("No free slots available in PSRAM memory pool!"); + return nullptr; + } +}; +#endif diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 1b2af082d..6b4504b05 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -305,15 +305,21 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p) #endif #endif - if (toPhoneQueue.numFree() == 0) { + // MAX_RX_TOPHONE is sized for PSRAM-backed builds. Fall back to a smaller + // runtime limit if the helper detects <2MB of PSRAM at boot. + const int queueLimit = get_rx_tophone_limit(); + const bool runtimeLimitReached = queueLimit > 0 && toPhoneQueue.numUsed() >= queueLimit; + + if (toPhoneQueue.numFree() == 0 || runtimeLimitReached) { + const bool runtimeControlled = runtimeLimitReached && queueLimit < MAX_RX_TOPHONE; if (p->decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || p->decoded.portnum == meshtastic_PortNum_RANGE_TEST_APP) { - LOG_WARN("ToPhone queue is full, discard oldest"); + LOG_WARN("ToPhone queue %s, discard oldest", runtimeControlled ? "reached runtime limit" : "is full"); meshtastic_MeshPacket *d = toPhoneQueue.dequeuePtr(0); if (d) releaseToPool(d); } else { - LOG_WARN("ToPhone queue is full, drop packet"); + LOG_WARN("ToPhone queue %s, drop packet", runtimeControlled ? "reached runtime limit" : "is full"); releaseToPool(p); fromNum++; // Make sure to notify observers in case they are reconnected so they can get the packets return; diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 60637cbd1..0ba0cf715 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -43,9 +43,19 @@ Allocator &packetPool = dynamicPool; (MAX_RX_TOPHONE + MAX_RX_FROMRADIO + 2 * MAX_TX_QUEUE + \ 2) // max number of packets which can be in flight (either queued from reception or queued for sending) +#if defined(CONFIG_IDF_TARGET_ESP32S3) +// Try to put the heavy MeshPacket pool into PSRAM. If that fails we fall back to +// heap allocation so the radio stays functional (at the cost of fewer packets). +static PsramMemoryPool psramPool; +static MemoryDynamic fallbackPool; +Allocator &packetPool = psramPool.isValid() + ? static_cast &>(psramPool) + : static_cast &>(fallbackPool); +#else static MemoryPool staticPool; Allocator &packetPool = staticPool; #endif +#endif static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); @@ -56,6 +66,14 @@ static uint8_t bytes[MAX_LORA_PAYLOAD_LEN + 1] __attribute__((__aligned__)); */ Router::Router() : concurrency::OSThread("Router"), fromRadioQueue(MAX_RX_FROMRADIO) { +#if defined(CONFIG_IDF_TARGET_ESP32S3) + if (!psramPool.isValid()) { + LOG_WARN("PSRAM packet pool unavailable, falling back to heap allocations"); + } + if (!has_psram() && MAX_RX_TOPHONE > get_rx_tophone_limit()) { + LOG_WARN("Detected <2MB PSRAM, limiting phone queue to %d packets", get_rx_tophone_limit()); + } +#endif // This is called pre main(), don't touch anything here, the following code is not safe /* LOG_DEBUG("Size of NodeInfo %d", sizeof(NodeInfo)); diff --git a/src/mesh/mesh-pb-constants.h b/src/mesh/mesh-pb-constants.h index e392ed964..190a443eb 100644 --- a/src/mesh/mesh-pb-constants.h +++ b/src/mesh/mesh-pb-constants.h @@ -1,6 +1,7 @@ #pragma once #include +#include "memGet.h" #include "mesh/generated/meshtastic/admin.pb.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "mesh/generated/meshtastic/localonly.pb.h" @@ -11,11 +12,42 @@ // Tricky macro to let you find the sizeof a type member #define member_size(type, member) sizeof(((type *)0)->member) +// Minimum PSRAM the firmware expects before enabling the "expanded" queues that +// rely on off-chip RAM instead of internal DRAM. Currently set to 2MB to +// accommodate Heltec WiFi LoRa 32 V4 boards (and others) +static constexpr size_t PSRAM_LARGE_THRESHOLD_BYTES = 2 * 1024 * 1024; + +inline bool has_psram(size_t minimumBytes = PSRAM_LARGE_THRESHOLD_BYTES) +{ +#if defined(ARCH_ESP32) || defined(ARCH_PORTDUINO) + return memGet.getPsramSize() >= minimumBytes; +#else + (void)minimumBytes; + return false; +#endif +} + +// Runtime cap used to keep the BLE message queue from overflowing low-memory +// S3 variants if PSRAM is smaller than expected or temporarily unavailable. +inline int get_rx_tophone_limit() +{ +#if defined(CONFIG_IDF_TARGET_ESP32S3) + return has_psram() ? 200 : 32; +#elif defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) + return 8; +#else + return 32; +#endif +} + /// max number of packets which can be waiting for delivery to android - note, this value comes from mesh.options protobuf // FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in // RAM #define MAX_RX_TOPHONE (member_size(DeviceState, receive_queue) / member_size(DeviceState, receive_queue[0])) #ifndef MAX_RX_TOPHONE -#if defined(ARCH_ESP32) && !(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3)) +#if defined(CONFIG_IDF_TARGET_ESP32S3) +static constexpr int MAX_RX_TOPHONE_WITH_PSRAM = 200; +#define MAX_RX_TOPHONE MAX_RX_TOPHONE_WITH_PSRAM +#elif defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) #define MAX_RX_TOPHONE 8 #else #define MAX_RX_TOPHONE 32 @@ -51,9 +83,8 @@ static_assert(sizeof(meshtastic_NodeInfoLite) <= 200, "NodeInfoLite size increas #include "Esp.h" static inline int get_max_num_nodes() { - uint32_t psram_size = ESP.getPsramSize() / (1024 * 1024); // Convert Bytes to MB - if (psram_size >= 2) { - return 800; + if (has_psram()) { + return 5000; } uint32_t flash_size = ESP.getFlashChipSize() / (1024 * 1024); // Fallback based on flash size