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:
1 parent
f778d14c76
commit
0d42d3c8e1
10 files changed
+175
-63
No files matched your search
+12
-7
@@ -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) {
|
||||
|
||||
@@ -640,7 +640,7 @@ void ClientACL::load(FILESYSTEM* fs, const mesh::LocalIdentity& self_id) {
|
||||
c.last_timestamp = UINT32_MAX;
|
||||
}
|
||||
self_id.calcSharedSecret(c.shared_secret, pub_key); // recalculate shared secrets in case our private key changed
|
||||
if (num_clients < MAX_CLIENTS) {
|
||||
if (num_clients < capacity) {
|
||||
clients[num_clients++] = c;
|
||||
} else {
|
||||
full = true;
|
||||
@@ -803,7 +803,7 @@ bool ClientACL::clear() {
|
||||
const bool files_cleared = !_fs->exists("/s_contacts")
|
||||
&& !_fs->exists("/s_contacts.tmp")
|
||||
&& !_fs->exists("/s_contacts.bak");
|
||||
memset(clients, 0, sizeof(clients));
|
||||
if (clients) memset(clients, 0, sizeof(ClientInfo) * (size_t)capacity);
|
||||
num_clients = 0;
|
||||
return files_cleared;
|
||||
}
|
||||
@@ -828,7 +828,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) {
|
||||
}
|
||||
|
||||
ClientInfo* c;
|
||||
if (num_clients < MAX_CLIENTS) {
|
||||
if (num_clients < capacity) {
|
||||
c = &clients[num_clients++];
|
||||
} else {
|
||||
if (oldest == NULL) return NULL; // every entry is protected
|
||||
|
||||
+12
-2
@@ -3,6 +3,7 @@
|
||||
#include <Arduino.h> // needed for PlatformIO
|
||||
#include <Mesh.h>
|
||||
#include <helpers/IdentityStore.h>
|
||||
#include <new> // std::nothrow (heap-allocated client table)
|
||||
|
||||
#define PERM_ACL_ROLE_MASK 7 // lower 3 bits
|
||||
#define PERM_ACL_GUEST 0
|
||||
@@ -60,14 +61,23 @@ struct ClientLoginReplayClampResult {
|
||||
|
||||
class ClientACL {
|
||||
FILESYSTEM* _fs;
|
||||
ClientInfo clients[MAX_CLIENTS];
|
||||
ClientInfo* clients;
|
||||
int capacity; // 0 when the table could not be allocated
|
||||
int num_clients;
|
||||
bool login_replay_store_available;
|
||||
|
||||
public:
|
||||
// MAX_CLIENTS entries run to several kilobytes. Classic ESP32's link-time
|
||||
// static DRAM window is much smaller than its runtime heap, so the table is
|
||||
// allocated here instead of living in .bss. This constructor runs before
|
||||
// setup(), while the heap is still unfragmented. A failed allocation leaves
|
||||
// a zero-capacity ACL that refuses new clients rather than writing through
|
||||
// a null table; putClient() returns NULL and callers already handle that.
|
||||
ClientACL() {
|
||||
_fs = NULL;
|
||||
memset(clients, 0, sizeof(clients));
|
||||
clients = new (std::nothrow) ClientInfo[MAX_CLIENTS];
|
||||
capacity = clients ? MAX_CLIENTS : 0;
|
||||
if (clients) memset(clients, 0, sizeof(ClientInfo) * (size_t)capacity);
|
||||
num_clients = 0;
|
||||
login_replay_store_available = false;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,6 +11,19 @@
|
||||
#include "OtaFormat.h"
|
||||
#include "OtaSelf.h" // ota_self_firmware() - prefer self-describing EndF identity at begin()
|
||||
#include "OtaBlInfo.h" // bootloader OTA-apply capability marker (nRF52); cached after first read
|
||||
|
||||
// Storage policy for the mOTA context. A "dynamic" context is created on demand
|
||||
// and handed back once idle, so its multi-kilobyte workspace only occupies RAM
|
||||
// while an OTA operation is actually in flight:
|
||||
// OTA_SHARED_COMPANION_QUEUE - borrows the Companion's offline message queue
|
||||
// OTA_HEAP_CONTEXT - allocates from the heap, failing softly
|
||||
// Every other build keeps the plain .bss singleton.
|
||||
#if defined(OTA_SHARED_COMPANION_QUEUE) || defined(OTA_HEAP_CONTEXT)
|
||||
#define OTA_DYNAMIC_CONTEXT 1
|
||||
#else
|
||||
#define OTA_DYNAMIC_CONTEXT 0
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
#include "OtaStoreQspiNrf52.h"
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
@@ -82,7 +95,7 @@ class FolderMotaStore; // pull destination over the seeder link (full type onl
|
||||
#endif
|
||||
|
||||
struct OtaContext {
|
||||
#if defined(OTA_SHARED_COMPANION_QUEUE)
|
||||
#if OTA_DYNAMIC_CONTEXT
|
||||
// Release at a main-loop boundary, after callers finish using this context.
|
||||
bool release_when_idle = false;
|
||||
#endif
|
||||
@@ -409,7 +422,7 @@ struct OtaContext {
|
||||
return false;
|
||||
}
|
||||
folder_active = true;
|
||||
#if defined(OTA_SHARED_COMPANION_QUEUE)
|
||||
#if OTA_DYNAMIC_CONTEXT
|
||||
release_when_idle = false;
|
||||
#endif
|
||||
_folder_link = link;
|
||||
@@ -439,7 +452,7 @@ struct OtaContext {
|
||||
folder_active = false;
|
||||
_folder_link = FOLDER_LINK_NONE;
|
||||
_folder_source = nullptr;
|
||||
#if defined(OTA_SHARED_COMPANION_QUEUE)
|
||||
#if OTA_DYNAMIC_CONTEXT
|
||||
release_when_idle = true;
|
||||
#endif
|
||||
}
|
||||
@@ -641,6 +654,14 @@ uint8_t ota_hop_limit();
|
||||
!defined(OTA_SEEDER_ONLY) || !defined(COMPANION_RADIO_FULL)
|
||||
#error "Shared mOTA queue storage requires an nRF52 or ESP32 Full source-only Companion"
|
||||
#endif
|
||||
#if defined(OTA_HEAP_CONTEXT)
|
||||
#error "OTA_HEAP_CONTEXT and OTA_SHARED_COMPANION_QUEUE both own the context storage"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if OTA_DYNAMIC_CONTEXT
|
||||
// ota_ctx() is only valid while storage is held. Callers that can run before a
|
||||
// successful ota_acquire_context() must gate on ota_context_if_active() first.
|
||||
void ota_set_context_storage(void* owner, OtaContext* (*acquire)(void*),
|
||||
void (*release)(void*));
|
||||
void ota_release_context_if_idle(bool temporary_radio_active);
|
||||
|
||||
Reference in new issue
Block a user