Move large repeater tables off classic ESP32 static DRAM

Classic ESP32 links into a dram0_0_seg of only 124,580 bytes: memory.ld
carves the BT controller's 0xdb5c (56,156) reservation off the 0x2c200
window before the application gets any. Three large objects dominated what
was left, and Tbeam_SX1262_repeater_observer_mqtt overran the build's 8 KiB
static reserve by 4,744 bytes.

Heap-allocate them instead, each with a defined behaviour when the
allocation fails:

- OtaContext: generalize the Companion's borrowed-storage path behind
  OTA_DYNAMIC_CONTEXT and add OTA_HEAP_CONTEXT, which allocates the context
  on first use and frees it once no transfer, apply or folder link needs it.
  A repeater is idle nearly all the time, so the workspace is usually absent.
  Exhaustion is reported to the caller and the operation declines.
- ClientACL: the client table becomes a pointer with a live capacity;
  capacity 0 refuses new clients rather than writing through a null table.
- MyMesh flood packet filters: likewise, with flood_packet_filter_slots as
  the live bound for every rule loop. At capacity 0 the node forwards
  unfiltered, and both save paths refuse to write so a stored ruleset is
  never replaced by an empty file.

Also override the Arduino SDK's weak btInUse() in simple_repeater so
initArduino() releases the BT controller memory to the heap. That grows the
runtime heap these tables now come from; it cannot recover the same
reservation from the linker's static window.

Static DRAM, classic ESP32:
  Heltec_v2_repeater                   108,332 -> 70,732
  Tbeam_SX1262_repeater_observer_mqtt  121,132 -> 83,532  (was failing)
This commit is contained in:
MUSTARDTIGERFPV committed 2026-09-09 01:24:00 -07:00
1 parent f778d14c76
commit 0d42d3c8e1
10 files changed
+175 -63

No files matched your search

+31 -1
View File
@@ -1,10 +1,13 @@
#include "OtaContext.h"
#include <assert.h>
#if OTA_DYNAMIC_CONTEXT && defined(OTA_HEAP_CONTEXT)
#include <new>
#endif
namespace mesh {
namespace ota {
#if defined(OTA_SHARED_COMPANION_QUEUE)
#if OTA_DYNAMIC_CONTEXT
namespace {
OtaContext* active_context = nullptr;
void* storage_owner = nullptr;
@@ -23,6 +26,23 @@ uint8_t saved_autoinstall = OtaContext::AUTOINSTALL_OFF;
uint8_t saved_hops = OTA_HOP_LIMIT_DEFAULT;
uint16_t saved_checkpoint = OTA_CHECKPOINT_BLOCKS;
uint16_t saved_advert = OTA_ADVERT_INTERVAL_MINS;
#if defined(OTA_HEAP_CONTEXT)
// Default storage for roles with no borrowable workspace (repeaters, room
// servers). The context is several kilobytes; keeping it off .bss matters most
// on classic ESP32, whose static DRAM window is far smaller than its heap.
OtaContext* heap_context = nullptr;
OtaContext* acquireHeapContext(void*) {
if (!heap_context) heap_context = new (std::nothrow) OtaContext();
return heap_context; // nullptr on exhaustion; the caller reports and bails
}
void releaseHeapContext(void*) {
delete heap_context;
heap_context = nullptr;
}
#endif
}
void ota_set_context_storage(void* owner, OtaContext* (*acquire)(void*),
@@ -52,6 +72,12 @@ void ota_begin_context(uint32_t target, OtaSend send, void* ctx,
bool ota_acquire_context(char* reply, size_t cap) {
if (active_context) return true;
#if defined(OTA_HEAP_CONTEXT)
if (!acquire_storage) { // no owner registered one: fall back to the heap
acquire_storage = acquireHeapContext;
release_storage = releaseHeapContext;
}
#endif
if (!acquire_storage || !release_storage || !saved_send) {
if (reply && cap) snprintf(reply, cap, "ERR mOTA storage is not ready");
return false;
@@ -59,7 +85,11 @@ bool ota_acquire_context(char* reply, size_t cap) {
active_context = acquire_storage(storage_owner);
if (!active_context) {
if (reply && cap) snprintf(reply, cap,
#if defined(OTA_HEAP_CONTEXT)
"ERR mOTA is out of memory; retry when the node is less busy");
#else
"ERR mOTA needs 128 free queue slots; sync unread messages with an app first");
#endif
return false;
}
OtaContext& c = *active_context;