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
2026-09-09 01:24:00 -07:00
parent f778d14c76
commit 0d42d3c8e1
10 changed files with 175 additions and 63 deletions
+70 -46
View File
@@ -4,6 +4,7 @@
#include <helpers/radiolib/RadioPowerLimits.h>
#include <helpers/radiolib/RxBoostedGainDefaults.h>
#include <algorithm>
#include <new> // std::nothrow (heap-allocated flood rule table)
#include <stdlib.h> // for qsort()
#include <helpers/CLICommandUtils.h>
#include <helpers/ClockSyncUtils.h>
@@ -3329,6 +3330,15 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
, bridge(&_prefs, _mgr, &rtc)
#endif
{
// Global constructors run before setup(), while the heap is still
// unfragmented. A failed allocation leaves flood_packet_filter_slots at 0:
// every rule loop is bounded by it, so the node forwards unfiltered instead
// of dereferencing a null table. saveFloodPacketFilters() refuses to write
// in that state so a stored ruleset is never overwritten with an empty one.
flood_packet_filters =
new (std::nothrow) FloodPacketFilterEntry[FLOOD_PACKET_FILTER_SLOTS];
flood_packet_filter_slots = flood_packet_filters ? FLOOD_PACKET_FILTER_SLOTS : 0;
static_cast<StaticPoolPacketManager*>(_mgr)->setFloodScopePreference(
scoreFloodTransportScope, this);
last_millis = 0;
@@ -3380,7 +3390,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
recv_pkt_channel_scope_bypass = false;
recv_pkt_channel_scope_rejected = false;
recv_pkt_filter_match_mask = 0;
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots);
flood_packet_filter_blacklist_count = 0;
memset(flood_packet_filter_blacklist, 0, sizeof(flood_packet_filter_blacklist));
memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes));
@@ -5667,6 +5677,9 @@ static void formatFloodModerationPath(
const uint8_t path[FLOOD_GROUP_MODERATION_PATH_BYTES_MAX]);
void MyMesh::seedDefaultFloodPacketFilters() {
// Slots 0 and 1 carry the built-in defaults; without a table there is
// nothing to seed and the node forwards unfiltered.
if (flood_packet_filter_slots < 2) return;
auto& entry = flood_packet_filters[0];
memset(&entry, 0, sizeof(entry));
entry.active = true;
@@ -5713,7 +5726,7 @@ bool MyMesh::loadFloodPacketFilters() {
enum class FileState : uint8_t { Missing, Valid, Invalid, Unreadable };
auto loadFile = [this](const char* filename) -> FileState {
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots);
flood_channel_data_rule_slot = 0xFF;
flood_channel_data_rule_max_hops = FLOOD_CHANNEL_HOPS_ALL;
memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes));
@@ -5737,7 +5750,7 @@ bool MyMesh::loadFloodPacketFilters() {
success = (version_6 || version_7)
&& file.read(&count, sizeof(count)) == sizeof(count)
&& FloodFilterPolicy::forwardPersistenceCountSupported(
count, FLOOD_PACKET_FILTER_SLOTS);
count, flood_packet_filter_slots);
for (int i = 0; success && i < count; i++) {
uint8_t active = 0;
@@ -6072,7 +6085,7 @@ bool MyMesh::loadFloodPacketFilters() {
}
file.close();
if (!success) {
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots);
memset(flood_channel_scopes, 0, sizeof(flood_channel_scopes));
memset(flood_channel_direct_scopes, 0,
sizeof(flood_channel_direct_scopes));
@@ -6177,7 +6190,7 @@ bool MyMesh::isFloodChannelDataRule(
}
int MyMesh::findFloodChannelDataRule() const {
if (flood_channel_data_rule_slot >= FLOOD_PACKET_FILTER_SLOTS) return -1;
if (flood_channel_data_rule_slot >= flood_packet_filter_slots) return -1;
return isFloodChannelDataRule(
flood_packet_filters[flood_channel_data_rule_slot])
? flood_channel_data_rule_slot : -1;
@@ -6230,7 +6243,7 @@ void MyMesh::setFloodChannelData(const char* value, char* reply) {
int current = findFloodChannelDataRule();
int slot = current;
if (!enable && slot < 0) {
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
if (!flood_packet_filters[i].active) {
slot = i;
break;
@@ -6311,7 +6324,7 @@ bool MyMesh::migrateLegacyFloodChannelData() {
if (_prefs.flood_channel_data_enabled) return true;
int slot = -1;
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
if (!flood_packet_filters[i].active) {
slot = i;
break;
@@ -6437,7 +6450,7 @@ bool MyMesh::migrateLegacyFloodChannelBlocks() {
bool duplicate = false;
int free_slot = -1;
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
const auto& entry = flood_packet_filters[i];
if (!entry.active) {
if (free_slot < 0) free_slot = i;
@@ -6498,6 +6511,9 @@ bool MyMesh::migrateLegacyFloodChannelBlocks() {
bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase,
bool empty_forward_phase) {
if (_fs == NULL) return false;
// Without a rule table there is nothing to persist. Writing the file anyway
// would replace the operator's stored ruleset with an empty one.
if (flood_packet_filter_slots == 0) return false;
// Recovery owns transaction remnants; overwriting one could erase the only
// complete image after a failed publish boundary.
if (_fs->exists(FLOOD_PACKET_FILTER_TEMP_FILE)
@@ -6520,7 +6536,7 @@ bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase,
const uint8_t magic[4] = {'F', 'P', 'F', '7'};
const uint8_t count = FloodFilterPolicy::forwardPersistenceCount(
flood_packet_filters, FLOOD_PACKET_FILTER_SLOTS,
flood_packet_filters, flood_packet_filter_slots,
empty_forward_phase);
bool success = writeExact(magic, sizeof(magic))
&& writeExact(&count, sizeof(count));
@@ -6643,7 +6659,7 @@ bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase,
}
#else
bool MyMesh::loadFloodPacketFilters() {
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots);
if (_fs == NULL) {
seedDefaultFloodPacketFilters();
return true;
@@ -6663,7 +6679,7 @@ bool MyMesh::loadFloodPacketFilters() {
bool version_6 = success && memcmp(magic, "FPF6", sizeof(magic)) == 0;
success = version_6
&& file.read(&count, sizeof(count)) == sizeof(count)
&& count <= FLOOD_PACKET_FILTER_SLOTS;
&& count <= flood_packet_filter_slots;
for (int i = 0; success && i < count; i++) {
uint8_t active = 0;
@@ -6728,7 +6744,7 @@ bool MyMesh::loadFloodPacketFilters() {
file.close();
// A truncated or invalid file fails open; filtering must never be enabled by corrupt bytes.
if (!success) memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
if (!success) memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots);
return success;
}
@@ -6736,16 +6752,18 @@ bool MyMesh::saveFloodPacketFilters(bool empty_scope_phase,
bool empty_forward_phase) {
(void)empty_scope_phase;
if (_fs == NULL) return false;
// As above: never replace a stored ruleset with an empty file.
if (flood_packet_filter_slots == 0) return false;
File file = openFloodSettingsWrite(_fs, FLOOD_PACKET_FILTER_FILE);
if (!file) return false;
const uint8_t magic[4] = {'F', 'P', 'F', '6'};
uint8_t count = FLOOD_PACKET_FILTER_SLOTS;
uint8_t count = flood_packet_filter_slots;
bool success = file.write(magic, sizeof(magic)) == sizeof(magic)
&& file.write(&count, sizeof(count)) == sizeof(count);
FloodPacketFilterEntry empty_entry;
memset(&empty_entry, 0, sizeof(empty_entry));
for (int i = 0; success && i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; success && i < flood_packet_filter_slots; i++) {
const auto& entry = empty_forward_phase
? empty_entry : flood_packet_filters[i];
uint8_t active = entry.active ? 1 : 0;
@@ -6862,7 +6880,7 @@ bool MyMesh::authenticateFloodPacketFilterChannel(
}
bool MyMesh::hasFloodPacketFilterRetryRules() const {
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
if (flood_packet_filters[i].active
&& flood_packet_filters[i].retry_on_match) return true;
}
@@ -6870,7 +6888,7 @@ bool MyMesh::hasFloodPacketFilterRetryRules() const {
}
bool MyMesh::floodPacketFilterAllowsRetry(uint64_t match_mask) const {
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
if ((match_mask & ((uint64_t)1U << i)) != 0
&& flood_packet_filters[i].retry_on_match) return true;
}
@@ -6881,14 +6899,14 @@ int MyMesh::nextFloodPacketFilterMatch(uint64_t match_mask,
uint64_t visited_mask) const {
uint8_t priorities[FLOOD_PACKET_FILTER_SLOTS];
uint8_t specificities[FLOOD_PACKET_FILTER_SLOTS];
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
priorities[i] = flood_packet_filters[i].priority;
specificities[i] = FloodFilterPolicy::channelMatcherSpecificity(
flood_packet_filters[i].channel_key_len);
}
return FloodFilterPolicy::nextOrderedRule(
match_mask, visited_mask, priorities, specificities,
FLOOD_PACKET_FILTER_SLOTS);
flood_packet_filter_slots);
}
bool MyMesh::resolveFloodPacketFilterTargetRegion(
@@ -6910,7 +6928,7 @@ uint64_t MyMesh::applyFloodPacketFilterStop(uint64_t match_mask) {
uint8_t priorities[FLOOD_PACKET_FILTER_SLOTS];
uint8_t specificities[FLOOD_PACKET_FILTER_SLOTS];
uint8_t stop_flags[FLOOD_PACKET_FILTER_SLOTS];
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
priorities[i] = flood_packet_filters[i].priority;
const auto& entry = flood_packet_filters[i];
specificities[i] = FloodFilterPolicy::channelMatcherSpecificity(
@@ -6928,7 +6946,7 @@ uint64_t MyMesh::applyFloodPacketFilterStop(uint64_t match_mask) {
}
return FloodFilterPolicy::truncateRulesAtStop(
match_mask, priorities, specificities, stop_flags,
FLOOD_PACKET_FILTER_SLOTS);
flood_packet_filter_slots);
}
uint64_t MyMesh::evaluateFloodPacketFilterMatches(
@@ -6944,7 +6962,7 @@ uint64_t MyMesh::evaluateFloodPacketFilterMatches(
bool channel_auth_checked[FLOOD_PACKET_FILTER_SLOTS] = { false };
bool channel_auth_valid[FLOOD_PACKET_FILTER_SLOTS] = { false };
uint64_t matches = 0;
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
const auto& entry = flood_packet_filters[i];
if (!floodPacketFilterFieldsMatch(
entry, packet, incoming_is_scoped, incoming_transport_code,
@@ -7056,7 +7074,7 @@ bool MyMesh::shouldBlockFloodPacketForward(const mesh::Packet* packet) const {
void MyMesh::commitFloodPacketFilterRates(const mesh::Packet* packet) {
if (packet == NULL || !packet->isRouteFlood()) return;
uint32_t now = _ms->getMillis();
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
auto& entry = flood_packet_filters[i];
if ((recv_pkt_filter_match_mask & ((uint64_t)1U << i)) == 0
|| !entry.rate_limit_enabled) {
@@ -7098,7 +7116,7 @@ uint64_t MyMesh::evaluateFloodPacketFilterMatches(
const RegionEntry* incoming_region) {
(void)incoming_region;
uint64_t matches = 0;
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
const auto& entry = flood_packet_filters[i];
if (floodPacketFilterFieldsMatch(entry, packet, false, 0,
incoming_region_allowed, NULL)
@@ -7118,7 +7136,7 @@ bool MyMesh::applyFloodPacketFilterScope(mesh::Packet* packet,
scope_set = false;
fast_track = false;
if (packet == NULL || !packet->isRouteFlood()) return false;
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
const auto& entry = flood_packet_filters[i];
if ((match_mask & ((uint64_t)1U << i)) == 0
|| entry.scope_name[0] == 0) continue;
@@ -7145,7 +7163,7 @@ bool MyMesh::shouldBlockFloodPacketForward(const mesh::Packet* packet) const {
if (packet == NULL || !packet->isRouteFlood()) return false;
uint8_t type = packet->getPayloadType();
uint8_t hops = packet->getPathHashCount();
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
const auto& entry = flood_packet_filters[i];
if ((recv_pkt_filter_match_mask & ((uint64_t)1U << i)) != 0
&& entry.scope_name[0] == 0) {
@@ -7175,7 +7193,7 @@ static bool floodRuleRegionNamePresent(const RegionMap& map,
}
void MyMesh::formatFloodPacketFilterDetail(int index, char* reply, size_t reply_len) const {
if (index < 0 || index >= FLOOD_PACKET_FILTER_SLOTS || !flood_packet_filters[index].active) {
if (index < 0 || index >= flood_packet_filter_slots || !flood_packet_filters[index].active) {
snprintf(reply, reply_len, "Err - empty filter slot");
return;
}
@@ -7336,8 +7354,8 @@ void MyMesh::formatFloodPacketFilters(const char* args, char* reply) const {
if (*selector == '.') selector = skipFloodFilterSpaces(selector + 1);
if (*selector != 0) {
uint8_t slot;
if (!parseFloodFilterUnsigned(selector, FLOOD_PACKET_FILTER_SLOTS, slot) || slot == 0) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", FLOOD_PACKET_FILTER_SLOTS);
if (!parseFloodFilterUnsigned(selector, flood_packet_filter_slots, slot) || slot == 0) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", flood_packet_filter_slots);
return;
}
formatFloodPacketFilterDetail(slot - 1, reply, 160);
@@ -7347,7 +7365,7 @@ void MyMesh::formatFloodPacketFilters(const char* args, char* reply) const {
size_t used = (size_t)snprintf(reply, 160, ">");
int active_count = 0;
bool truncated = false;
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
const auto& entry = flood_packet_filters[i];
if (!entry.active) continue;
active_count++;
@@ -7407,14 +7425,14 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
size_t slot_len = (size_t)(cursor - slot_start);
char slot_text[8];
if (slot_len == 0 || slot_len >= sizeof(slot_text)) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", FLOOD_PACKET_FILTER_SLOTS);
snprintf(reply, 160, "Err - filter slot must be 1-%d", flood_packet_filter_slots);
return;
}
memcpy(slot_text, slot_start, slot_len);
slot_text[slot_len] = 0;
uint8_t slot;
if (!parseFloodFilterUnsigned(slot_text, FLOOD_PACKET_FILTER_SLOTS, slot) || slot == 0) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", FLOOD_PACKET_FILTER_SLOTS);
if (!parseFloodFilterUnsigned(slot_text, flood_packet_filter_slots, slot) || slot == 0) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", flood_packet_filter_slots);
return;
}
requested_slot = slot - 1;
@@ -7829,7 +7847,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
int slot = requested_slot;
if (slot < 0) {
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
// Keep the compatibility-owned row distinct from an ordinary rule with
// identical match/action fields. Otherwise a generic, unnumbered set
// would silently detach flood.channel.data from its own row.
@@ -7845,7 +7863,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
}
}
if (slot < 0) {
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
if (!flood_packet_filters[i].active) {
slot = i;
break;
@@ -7876,7 +7894,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
}
#else
void MyMesh::formatFloodPacketFilterDetail(int index, char* reply, size_t reply_len) const {
if (index < 0 || index >= FLOOD_PACKET_FILTER_SLOTS || !flood_packet_filters[index].active) {
if (index < 0 || index >= flood_packet_filter_slots || !flood_packet_filters[index].active) {
snprintf(reply, reply_len, "Err - empty filter slot");
return;
}
@@ -7909,8 +7927,8 @@ void MyMesh::formatFloodPacketFilters(const char* args, char* reply) const {
if (*selector == '.') selector = skipFloodFilterSpaces(selector + 1);
if (*selector != 0) {
uint8_t slot;
if (!parseFloodFilterUnsigned(selector, FLOOD_PACKET_FILTER_SLOTS, slot) || slot == 0) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", FLOOD_PACKET_FILTER_SLOTS);
if (!parseFloodFilterUnsigned(selector, flood_packet_filter_slots, slot) || slot == 0) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", flood_packet_filter_slots);
return;
}
formatFloodPacketFilterDetail(slot - 1, reply, 160);
@@ -7920,7 +7938,7 @@ void MyMesh::formatFloodPacketFilters(const char* args, char* reply) const {
size_t used = (size_t)snprintf(reply, 160, ">");
int active_count = 0;
bool truncated = false;
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
const auto& entry = flood_packet_filters[i];
if (!entry.active) continue;
active_count++;
@@ -7962,14 +7980,14 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
size_t slot_len = (size_t)(cursor - slot_start);
char slot_text[8];
if (slot_len == 0 || slot_len >= sizeof(slot_text)) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", FLOOD_PACKET_FILTER_SLOTS);
snprintf(reply, 160, "Err - filter slot must be 1-%d", flood_packet_filter_slots);
return;
}
memcpy(slot_text, slot_start, slot_len);
slot_text[slot_len] = 0;
uint8_t slot;
if (!parseFloodFilterUnsigned(slot_text, FLOOD_PACKET_FILTER_SLOTS, slot) || slot == 0) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", FLOOD_PACKET_FILTER_SLOTS);
if (!parseFloodFilterUnsigned(slot_text, flood_packet_filter_slots, slot) || slot == 0) {
snprintf(reply, 160, "Err - filter slot must be 1-%d", flood_packet_filter_slots);
return;
}
requested_slot = slot - 1;
@@ -8081,7 +8099,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
int slot = requested_slot;
if (slot < 0) {
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
const auto& entry = flood_packet_filters[i];
if (entry.active && entry.payload_type == payload_type
&& entry.min_hops == min_hops && entry.max_hops == max_hops
@@ -8095,7 +8113,7 @@ void MyMesh::setFloodPacketFilter(const char* args, char* reply,
}
}
if (slot < 0) {
for (int i = 0; i < FLOOD_PACKET_FILTER_SLOTS; i++) {
for (int i = 0; i < flood_packet_filter_slots; i++) {
if (!flood_packet_filters[i].active) {
slot = i;
break;
@@ -8137,7 +8155,7 @@ void MyMesh::deleteFloodPacketFilter(const char* args, char* reply) {
if (!saveFloodPacketFilters(false, true)) {
strcpy(reply, "Err - unable to save flood filter");
} else {
memset(flood_packet_filters, 0, sizeof(flood_packet_filters));
memset(flood_packet_filters, 0, sizeof(FloodPacketFilterEntry) * flood_packet_filter_slots);
#if MESH_ENABLE_FLOOD_RULE_ENGINE
flood_channel_data_rule_slot = 0xFF;
#endif
@@ -8147,8 +8165,8 @@ void MyMesh::deleteFloodPacketFilter(const char* args, char* reply) {
}
uint8_t slot;
if (!parseFloodFilterUnsigned(selector, FLOOD_PACKET_FILTER_SLOTS, slot) || slot == 0) {
snprintf(reply, 160, "Err - use: del flood.filter.<1-%d>|all", FLOOD_PACKET_FILTER_SLOTS);
if (!parseFloodFilterUnsigned(selector, flood_packet_filter_slots, slot) || slot == 0) {
snprintf(reply, 160, "Err - use: del flood.filter.<1-%d>|all", flood_packet_filter_slots);
return;
}
int index = slot - 1;
@@ -12017,6 +12035,12 @@ void MyMesh::loop() {
_cli.loop();
processDeferredCliCommand();
servicePostMeshLoop();
#if defined(ENABLE_OTA) && OTA_DYNAMIC_CONTEXT
// Hand the OTA workspace back once no transfer, apply or folder link needs
// it. A repeater is idle almost all of its life, so this is where the bulk
// of the saving actually lands.
mesh::ota::ota_release_context_if_idle(isTempRadioActive());
#endif
}
#if MESH_ENABLE_TELEMETRY_HISTORY
+8 -1
View File
@@ -462,7 +462,14 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
};
mutable FloodRetryBridgeState flood_retry_bridge_states[MAX_FLOOD_RETRY_SLOTS];
FloodRetryBridgeReachability flood_retry_bridge_reachability[FLOOD_RETRY_BRIDGE_BUCKETS + 1];
FloodPacketFilterEntry flood_packet_filters[FLOOD_PACKET_FILTER_SLOTS];
// The rule table is the single largest member of this object. It is heap
// allocated in the constructor (before setup(), while the heap is still
// unfragmented) so classic ESP32's small link-time static DRAM window does
// not have to hold it. flood_packet_filter_slots is the live capacity, and
// is 0 when the allocation failed: every loop below is bounded by it, so a
// zero-capacity node simply forwards without rule filtering.
FloodPacketFilterEntry* flood_packet_filters;
uint8_t flood_packet_filter_slots;
uint8_t flood_packet_filter_blacklist_count;
uint8_t flood_packet_filter_blacklist[FLOOD_PACKET_FILTER_BLACKLIST_MAX]
[FLOOD_PACKET_FILTER_PATH_ID_SIZE];
+13
View File
@@ -24,6 +24,19 @@
#include <helpers/nrf52/EthernetCLI.h>
#endif
#if defined(ESP32) && defined(CONFIG_BT_ENABLED) && !defined(BLE_PIN_CODE)
// A repeater has no Bluetooth transport, but the Arduino SDK is built with the
// BT controller enabled, so its memory stays reserved unless the application
// says otherwise. initArduino() calls esp_bt_controller_mem_release() when
// this weak hook returns false, handing that region to the heap. Note this
// only grows the *runtime heap*: the same reservation is also carved out of
// the linker's dram0_0_seg (0xdb5c on classic ESP32), and no runtime call can
// give those static bytes back. It is what makes the heap-allocated tables
// above comfortable, not a substitute for them.
extern "C" bool btInUse();
extern "C" bool btInUse() { return false; }
#endif
StdRNG fast_rng;
#if MAX_RECENT_REPEATERS > 0
#if defined(ESP32)
+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) {
+3 -3
View File
@@ -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
View File
@@ -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;
}
+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;
+24 -3
View File
@@ -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);
+1
View File
@@ -38,6 +38,7 @@ build_flags =
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D FLOOD_CHANNEL_SCOPE_SLOTS=31 ; classic ESP32 DRAM cannot fit the default 255 with LoRa OTA
-D OTA_HEAP_CONTEXT=1 ; keep the idle mOTA workspace out of static DRAM
; -D MESH_PACKET_LOGGING=1
; -D MESH_DEBUG=1
build_src_filter = ${Heltec_lora32_v2.build_src_filter}
@@ -171,6 +171,7 @@ build_flags =
-D MAX_NEIGHBOURS=50
-D WITH_MQTT_BRIDGE=1
-D FLOOD_CHANNEL_SCOPE_SLOTS=31 ; retain scope support within this observer's tight internal DRAM
-D OTA_HEAP_CONTEXT=1 ; keep the idle mOTA workspace out of static DRAM
-D OTA_MANIFEST_BASE='"https://observer.gessaman.com/v"'
-D MQTT_MAX_PACKET_SIZE=1024
; -D MQTT_DEBUG=1