Preserve unread messages when the OTA wizard borrows queue space

This commit is contained in:
mikecarper committed 2026-10-26 21:45:18 -07:00
1 parent 02d15876e6
commit d64a4ef234
5 files changed
+64 -5

No files matched your search

+4 -1
View File
@@ -30,7 +30,10 @@ version 2 direct admin support. An older Full Companion may serve OTA files but
provide this guarded admin connection; the wizard will ask for a source
firmware upgrade instead of proceeding with that connection.
Check `get ota.direct` from its USB console for `v=2`. Update replies use a
separate mailbox; ordinary queued messages and saved contact routes stay intact.
separate mailbox; the wizard does not consume ordinary queued messages or change
saved contact routes. Some Full Companion boards borrow queue space for OTA and
retain up to 128 unread messages during an update. If more are waiting, sync them
with an app first; the wizard refuses to discard them to make room.
For this wizard, use an ESP32 source instead of RAK4631 Full. The RAK image's
current flash limit prevents adding the new direct admin helper. RAK4631
+9 -2
View File
@@ -3738,7 +3738,11 @@ bool MyMesh::scheduleTempRadio(float freq, float bw, uint8_t sf, uint8_t cr,
}
#if defined(OTA_SHARED_COMPANION_QUEUE)
uint16_t drained = 0;
if (!mesh::ota::ota_acquire_context(reply, reply_size, true, &drained)) return false;
bool drain_queue = true;
#if COMPANION_FEATURE_DIRECT_OTA_ADMIN
drain_queue = !_ota_direct_admin.active(_ms->getMillis());
#endif
if (!mesh::ota::ota_acquire_context(reply, reply_size, drain_queue, &drained)) return false;
mesh::ota::ota_ctx().release_when_idle = false;
#endif
_temp_radio_freq = freq;
@@ -5185,7 +5189,7 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
while (*action == ' ') ++action;
// Only an explicit transfer, catalog search, or source attach may evict
// unread offline frames. Read-only OTA queries leave the queue intact.
bool const drain_queue = strcmp(action, "folder on") == 0
bool drain_queue = strcmp(action, "folder on") == 0
|| strcmp(action, "ls") == 0 || strncmp(action, "ls ", 3) == 0
|| strncmp(action, "pull ", 5) == 0
|| strncmp(action, "get ", 4) == 0
@@ -5195,6 +5199,9 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|| strcmp(action, "applydelta") == 0
|| strncmp(action, "bootloader install ", 19) == 0
|| strncmp(action, "rescue install ", 15) == 0;
#if COMPANION_FEATURE_DIRECT_OTA_ADMIN
if (_ota_direct_admin.active(_ms->getMillis())) drain_queue = false;
#endif
uint16_t drained = 0;
if (!mesh::ota::ota_acquire_context(ota_reply, sizeof(ota_reply), drain_queue, &drained)) {
snprintf(reply, reply_size, "%s", ota_reply);
+32 -2
View File
@@ -1,6 +1,7 @@
#include <helpers/BorrowableFrameBuffer.h>
#include <helpers/ota/OtaContext.h>
#include <helpers/CompanionMotaControl.h>
#include <helpers/CLICommandUtils.h>
#include <helpers/ota/MotaStreamWritePolicy.h>
#include <cassert>
#include <cstring>
@@ -42,6 +43,21 @@ struct Queue {
}
};
#define COMPANION_FEATURE_DIRECT_OTA_ADMIN 1
struct MyMesh {
struct Guard { bool enabled = false; bool active(uint32_t) const { return enabled; } } _ota_direct_admin;
struct Timer { uint32_t getMillis() const { return 100; } } timer;
Timer* _ms = &timer;
bool _primary_radio_blocked = false;
float _temp_radio_freq = 0, _temp_radio_bw = 0;
uint8_t _temp_radio_sf = 0, _temp_radio_cr = 0, _temp_radio_failures = 0;
uint32_t _temp_radio_set_at = 0, _temp_radio_revert_at = 0, _temp_radio_retry_at = 0;
uint32_t futureMillis(uint32_t delay) const { return _ms->getMillis() + delay; }
void appendRxPowerSavingAdjustmentNote(char*, size_t, uint8_t, float) {}
bool scheduleTempRadio(float, float, uint8_t, uint8_t, uint32_t, char*, size_t);
};
#include "temp_radio_under_test.h"
class Folder : public MotaSource {
public:
uint8_t count() override { return 1; }
@@ -162,11 +178,25 @@ int main() {
assert(strstr(reply, "sync unread messages"));
assert(q.head == 191 && q.count == 129 && q.buffer.capacity() == 256);
check_messages();
MyMesh companion;
companion._ota_direct_admin.enabled = true;
assert(!companion.scheduleTempRadio(910.525f, 62.5f, 7, 5, 60, reply, sizeof reply));
assert(strstr(reply, "sync unread messages"));
assert(q.head == 191 && q.count == 129 && q.buffer.capacity() == 256);
assert(!companion._temp_radio_set_at && !ota_context_if_active());
check_messages();
companion._ota_direct_admin.enabled = false;
uint16_t drained = 0;
assert(ota_acquire_context(reply, sizeof reply, true, &drained));
assert(drained == 1 && q.count == 128 && q.head == 0);
assert(companion.scheduleTempRadio(910.525f, 62.5f, 7, 5, 60, reply, sizeof reply));
assert(strstr(reply, "OTA cleared 1 oldest queued messages"));
assert(q.count == 128 && q.head == 0);
first_retained = 1;
check_messages();
companion._ota_direct_admin.enabled = true;
assert(companion.scheduleTempRadio(910.525f, 62.5f, 7, 5, 60, reply, sizeof reply));
assert(!strstr(reply, "cleared") && q.count == 128);
check_messages();
ota_ctx().release_when_idle = true;
for (unsigned cycle = 0; cycle < 8; ++cycle) {
assert(ota_acquire_context(reply, sizeof reply));
+15
View File
@@ -81,6 +81,11 @@ static bool handle_ota_command(const char* command, char* reply, int) {
return config(rest, reply, context);
}
} }
#if COMPANION_FEATURE_DIRECT_OTA_ADMIN
static struct Guard { bool enabled=false; bool active(uint32_t) const { return enabled; } } _ota_direct_admin;
static struct Timer { uint32_t getMillis() const { return 100; } } timer;
static Timer* _ms=&timer;
#endif
static bool companion(const char* command, char* reply, size_t reply_size) {
int board=0;
@WRAPPER@
@@ -124,6 +129,14 @@ int main() {
reboot(fs);
command("ota folder on", "OK folder attached");
assert(drain_requested);
#if COMPANION_FEATURE_DIRECT_OTA_ADMIN
_ota_direct_admin.enabled=true;
command("ota folder on", "OK folder attached");
assert(!drain_requested);
_ota_direct_admin.enabled=false;
command("ota folder on", "OK folder attached");
assert(drain_requested);
#endif
command("ota config", "ota config: speed=1x packet=0.5x");
assert(!drain_requested);
for (const auto* setting : {"hops", "checkpoint", "advert"}) {
@@ -295,6 +308,8 @@ class CompanionOtaConfigTest(unittest.TestCase):
for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM", "STM32_PLATFORM")
for seeder in ([], ["-DOTA_SEEDER_ONLY=1"])]
variants.append(("ESP32_PLATFORM", [], "-DCOMPANION_FEATURE_OTA_CLI=1"))
variants.append(("ESP32_PLATFORM", ["-DCOMPANION_FEATURE_DIRECT_OTA_ADMIN=1"],
"-DCOMPANION_RADIO_FULL=1"))
for platform, seeder, capability in variants:
with self.subTest(platform=platform, seeder=seeder, capability=capability):
binary = Path(directory) / "config"
+4
View File
@@ -24,6 +24,9 @@ class SharedMotaQueueTest(unittest.TestCase):
control = method((ROOT / "examples/companion_radio/main.cpp").read_text(),
"class Nrf52BleMotaSourceControl")
(Path(temp) / "ble_control_under_test.h").write_text(control + ";\n")
(Path(temp) / "temp_radio_under_test.h").write_text(method(
(ROOT / "examples/companion_radio/MyMesh.cpp").read_text(),
"bool MyMesh::scheduleTempRadio("))
subprocess.run([
"cc", "-DENABLE_OTA=1", "-fsanitize=address,undefined", "-g",
"-c", str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(tinf),
@@ -45,6 +48,7 @@ class SharedMotaQueueTest(unittest.TestCase):
"c++", "-std=c++17", "-fsanitize=address,undefined", "-g",
*flags, "-DOTA_SEEDER_ONLY=1",
"-DCOMPANION_RADIO_FULL=1", "-DOTA_SHARED_COMPANION_QUEUE=1",
"-DMESH_ENABLE_HOST_KISS=0",
"-DENABLE_OTA=1", "-I", str(ROOT / "src"),
"-I", str(ROOT / "test/fixtures/shared_mota_queue/mocks"),
"-I", str(ROOT / "test/mocks"), "-I", temp,