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

+12 -7
View File
@@ -278,7 +278,10 @@ void Mesh::loop() {
bool Mesh::hasPendingOtaApply() const {
#if defined(ENABLE_OTA) && !defined(OTA_SEEDER_ONLY)
return ota::ota_ctx().apply_pending;
// A released dynamic context cannot hold a pending apply: the context is only
// handed back once apply_pending is clear.
const ota::OtaContext* oc = ota::ota_context_if_active();
return oc && oc->apply_pending;
#else
return false;
#endif
@@ -328,6 +331,14 @@ void __attribute__((noinline)) Mesh::serviceLoopMaintenance() {
}
}
#if defined(ENABLE_OTA)
#if OTA_DYNAMIC_CONTEXT
// Nothing below can run without storage, and a released context holds no
// pending apply or egress. Bail before any ota_ctx() dereference.
if (!ota::ota_context_if_active()) {
_ota_temp_was_active = false;
return;
}
#endif
#if !defined(OTA_SEEDER_ONLY)
// Deferred apply-reboot: a verified `ota applydelta` approves the update but does NOT reboot inline,
// so its "verified; applying" reply can be delivered first (over LoRa that reply is the operator's
@@ -356,12 +367,6 @@ void __attribute__((noinline)) Mesh::serviceLoopMaintenance() {
}
}
}
#endif
#if defined(OTA_SHARED_COMPANION_QUEUE)
if (!ota::ota_context_if_active()) {
_ota_temp_was_active = false;
return;
}
#endif
const bool ota_active = isTempRadioActive();
if (!ota_active) {