Files
HaloKeymind/src/helpers/ota/OtaContext.cpp
T
mikecarper fa010196b1 Merge PR #7 with reviewed ExpressLRS power and memory fixes
Integrate ExpressLRS TX module support and its stacked ESP32 heap changes.
Use the approved Linkflow calibration (17-30 dBm), preserve the PA drive
and output path after radio recovery, and keep LoRa OTA enabled.

Preserve stored ACLs and filters on allocation failure, release owned
client/filter buffers, service heap OTA contexts on Companions, release
self-serving workspaces after TempRadio, and reset staged-resume state
when a context is released. Keep manual staging and active operations
alive. Account for all moved allocations in the runtime RAM gate.

Validation: 1,393 native cases; radio-power, heap-context, ACL persistence,
shared-queue transfer, display/inbox, radio receive, and memory regressions.
Firmware builds passed for Linkflow, Heltec V2 Companion, T-Beam MQTT
repeater, Heltec V4 R8 MQTT repeater, RAK4631 repeater, and Indicator Full.
Physical verification awaits access to the currently offline lab Pi.
2026-09-10 22:06:16 -07:00

155 lines
5.1 KiB
C++

#include "OtaContext.h"
#include <assert.h>
#if OTA_DYNAMIC_CONTEXT && defined(OTA_HEAP_CONTEXT)
#include <new>
#endif
namespace mesh {
namespace ota {
#if OTA_DYNAMIC_CONTEXT
namespace {
OtaContext* active_context = nullptr;
void* storage_owner = nullptr;
OtaContext* (*acquire_storage)(void*) = nullptr;
void (*release_storage)(void*) = nullptr;
OtaSend saved_send = nullptr;
void* saved_send_ctx = nullptr;
uint32_t saved_target = 0;
char saved_hw[33] = {0};
uint8_t saved_seeder_id[4] = {0};
// Keep user policy across workspace reuse; discovered catalogs and transfers
// belong to the temporary mOTA session. No allocation is needed while idle.
SignerAllowlist saved_allow;
uint8_t saved_autofetch = OtaManager::AUTOFETCH_OFF;
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*),
void (*release)(void*)) {
assert(!active_context);
storage_owner = owner;
acquire_storage = acquire;
release_storage = release;
}
OtaContext& ota_ctx() {
assert(active_context); // Only explicit OTA entry points acquire storage.
return *active_context;
}
OtaContext* ota_context_if_active() { return active_context; }
void ota_begin_context(uint32_t target, OtaSend send, void* ctx,
const char* hw, const uint8_t* seeder_id) {
saved_target = target;
saved_send = send;
saved_send_ctx = ctx;
strncpy(saved_hw, hw ? hw : "", sizeof(saved_hw) - 1);
saved_hw[sizeof(saved_hw) - 1] = 0;
if (seeder_id) memcpy(saved_seeder_id, seeder_id, sizeof(saved_seeder_id));
}
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;
}
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;
c.begin(saved_target, saved_send, saved_send_ctx, saved_hw);
c.manager.set_seeder_id(saved_seeder_id);
c.manager.set_autofetch(saved_autofetch);
c.manager.set_checkpoint_blocks(saved_checkpoint);
c.manager.set_advert_mins(saved_advert);
c.manager.set_max_hops(saved_hops);
c.autoinstall = saved_autoinstall;
c.allow = saved_allow;
return true;
}
uint8_t ota_hop_limit() {
return active_context ? active_context->manager.max_hops() : saved_hops;
}
void ota_release_context_if_idle(bool temporary_radio_active) {
if (!active_context) return;
OtaContext& c = *active_context;
if (c.folder_active || c.folder_dest || c.apply_pending) return;
#if defined(OTA_HEAP_CONTEXT)
// Self-serving ends with the temporary radio window. It must not keep the
// heap workspace forever after the first announcement. Manual staging is
// different: preserve its bytes across separate CLI commands until reset.
if (c.serve_expected != 0) return;
#else
if (c.serving) return;
#endif
if (temporary_radio_active && !c.release_when_idle) return;
// No host source/destination remains. Discard pending transfer work before
// the queue reuses these bytes, including dynamic discovery/diff buffers.
saved_autofetch = c.manager.autofetch();
saved_checkpoint = c.manager.checkpoint_blocks();
saved_advert = c.manager.advert_mins();
saved_hops = c.manager.max_hops();
saved_autoinstall = c.autoinstall;
saved_allow = c.allow;
c.manager.clearPendingEgress();
c.manager.reset_session();
active_context = nullptr;
release_storage(storage_owner);
}
#else
OtaContext& ota_ctx() {
static OtaContext ctx;
return ctx;
}
OtaContext* ota_context_if_active() { return &ota_ctx(); }
bool ota_acquire_context(char*, size_t) { return true; }
void ota_begin_context(uint32_t target, OtaSend send, void* ctx,
const char* hw, const uint8_t* seeder_id) {
ota_ctx().begin(target, send, ctx, hw);
ota_ctx().manager.set_seeder_id(seeder_id);
}
uint8_t ota_hop_limit() { return ota_ctx().manager.max_hops(); }
#endif
} // namespace ota
} // namespace mesh