Restore Wireless Paper Full capacity with shared mOTA queue storage
This commit is contained in:
1 parent
e0a229fb91
commit
1e4d1e167a
12 files changed
+379
-42
No files matched your search
@@ -3608,8 +3608,15 @@ apply_companion_radio_full_profile() {
|
||||
# measured-safe tables for FULL OTA without changing ordinary USB/BLE/WiFi
|
||||
# companion builds.
|
||||
case "${env_name,,}" in
|
||||
heltec_wireless_paper_companion_radio_full)
|
||||
# The cold mOTA workspace fits in half the offline queue. Keep the
|
||||
# board's 350 contacts and all simultaneous Full transports.
|
||||
append_platformio_build_unflags "-DMAX_CONTACTS=150 -DOFFLINE_QUEUE_SIZE=512 -DOFFLINE_QUEUE_SIZE=128 -DOFFLINE_QUEUE_SIZE=16"
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMAX_CONTACTS=350 -DOFFLINE_QUEUE_SIZE=256 -DOTA_SHARED_COMPANION_QUEUE=1"
|
||||
record_build_reduction \
|
||||
"Wireless Paper Full: 350 contacts; 256 offline frames normally, 128 while mOTA borrows queue storage"
|
||||
;;
|
||||
generic_espnow_companion_radio_full|\
|
||||
heltec_wireless_paper_companion_radio_full|\
|
||||
heltec_wireless_tracker_companion_radio_full|\
|
||||
heltec_ct62_companion_radio_full|\
|
||||
heltec_v3_companion_radio_full|\
|
||||
|
||||
@@ -40,6 +40,13 @@ closed. `MESH_MIN_RUNTIME_HEAP` can raise a profile's requirement; it cannot
|
||||
lower the calculated requirement. Add an allocation allowance when adding a
|
||||
display, transport or other substantial feature.
|
||||
|
||||
Wireless Paper Full keeps 350 contacts and 256 offline frames by lending the
|
||||
upper 128 queue slots to its mOTA workspace during a session. An idle WiFi
|
||||
mOTA listener leaves all 256 slots available. More than 128 unread frames
|
||||
refuses the loan; sync messages with an app first. USB/TCP source detach or
|
||||
disconnect returns all 256 slots and releases the ESP32 proof/leaf scratch
|
||||
buffers. The display and simultaneous USB, Bluetooth and WiFi remain enabled.
|
||||
|
||||
## Release evidence and regression tests
|
||||
|
||||
Each newly built firmware has a matching `.memory.json` report. It records
|
||||
@@ -65,7 +72,9 @@ binding and the published T096 failing budget. Shared mOTA tests exercise
|
||||
complete transfers, queue wraparound, unread-message order, source ownership,
|
||||
stop/disconnect and repeated reuse. Bluetooth tests inject task and service
|
||||
startup failures. The manual staging buffer also has allocation-failure and
|
||||
repeated release tests.
|
||||
repeated release tests. ESP32 tests run the actual WiFi mOTA listener and source
|
||||
framing through complete transfers, idle polling, queue-full refusal, network
|
||||
loss, CLI detach and USB/TCP ownership changes under address/leak sanitizers.
|
||||
|
||||
For older releases without saved ELFs, an audit can compare their ESP allocator
|
||||
tables against a matching pinned SDK ELF and read reservations from the
|
||||
|
||||
@@ -10,6 +10,15 @@ not require rebuilding it. USB remains the normal way to update the Companion
|
||||
itself. WiFi/Bluetooth updates are optional, and Full does not install LoRa
|
||||
MOTA packages on itself.
|
||||
|
||||
The Wireless Paper Full memory-sharing update keeps 350 contacts and 256
|
||||
queued messages normally. During an mOTA session, 128 message slots are
|
||||
available while the other half holds its workspace. If more than 128 unread
|
||||
messages are queued, sync them with an app before starting mOTA. Disconnecting
|
||||
the source or turning it off returns all 256 slots. Idle WiFi mOTA listening
|
||||
does not borrow the queue. See the [1.17.1.5 release guide](releases/1.17.1.5.md)
|
||||
for the corrected download; the earlier `aa20e927` Wireless Paper image has
|
||||
150 contacts.
|
||||
|
||||
## Open the text terminal
|
||||
|
||||
Close the app or serial monitor using the USB port, then open a terminal at
|
||||
|
||||
@@ -39,11 +39,11 @@ bool networkReady() {
|
||||
|
||||
void detachTcpFolder() {
|
||||
if (!tcp_folder_attached) return;
|
||||
OtaContext& context = ota_ctx();
|
||||
if (context.folderLink() == OtaContext::FOLDER_LINK_TCP) {
|
||||
context.detach_folder();
|
||||
context.clear_folder_dest();
|
||||
context.manager.announce();
|
||||
OtaContext* context = ota_context_if_active();
|
||||
if (context && context->folderLink() == OtaContext::FOLDER_LINK_TCP) {
|
||||
context->detach_folder();
|
||||
context->clear_folder_dest();
|
||||
context->manager.announce();
|
||||
}
|
||||
tcp_folder_attached = false;
|
||||
}
|
||||
@@ -74,11 +74,15 @@ void WiFiOtaSeeder::loop() {
|
||||
|
||||
if (!listener_active) return;
|
||||
|
||||
OtaContext& context = ota_ctx();
|
||||
// An idle listener must leave all 256 queue slots available. A CLI detach
|
||||
// can also return the shared context before the next WiFi poll.
|
||||
OtaContext* active = ota_context_if_active();
|
||||
if (WiFiOtaSeederPolicy::tcpFolderWasDetached(tcp_folder_attached,
|
||||
context.folder_active)) {
|
||||
active && active->folder_active &&
|
||||
active->folderLink() == OtaContext::FOLDER_LINK_TCP)) {
|
||||
if (seeder_client) seeder_client.stop();
|
||||
context.clear_folder_dest();
|
||||
// A different host transport may already own the replacement context.
|
||||
if (active && !active->folder_active) active->clear_folder_dest();
|
||||
tcp_folder_attached = false;
|
||||
}
|
||||
if (seeder_client && seeder_client.connected()) return;
|
||||
@@ -89,6 +93,14 @@ void WiFiOtaSeeder::loop() {
|
||||
|
||||
incoming.setNoDelay(true); // tiny framed requests should leave immediately
|
||||
|
||||
char attach_reply[120];
|
||||
if (!ota_acquire_context(attach_reply, sizeof(attach_reply))) {
|
||||
incoming.stop();
|
||||
mesh::usbLoggingPort().printf(
|
||||
"OTA seeder rejected TCP client: %s\n", attach_reply);
|
||||
return;
|
||||
}
|
||||
OtaContext& context = ota_ctx();
|
||||
if (!WiFiOtaSeederPolicy::canAttachTcpFolder(context.folder_active,
|
||||
tcp_folder_attached)) {
|
||||
incoming.stop();
|
||||
@@ -98,7 +110,6 @@ void WiFiOtaSeeder::loop() {
|
||||
}
|
||||
|
||||
seeder_client = incoming;
|
||||
char attach_reply[120];
|
||||
if (!context.attach_folder_source(&seeder_source, OtaContext::FOLDER_LINK_TCP,
|
||||
"tcp", attach_reply, sizeof(attach_reply))) {
|
||||
seeder_client.stop();
|
||||
|
||||
@@ -637,8 +637,9 @@ void ota_begin_context(uint32_t target, OtaSend send, void* ctx,
|
||||
uint8_t ota_hop_limit();
|
||||
|
||||
#if defined(OTA_SHARED_COMPANION_QUEUE)
|
||||
#if !defined(NRF52_PLATFORM) || !defined(OTA_SEEDER_ONLY) || !defined(COMPANION_RADIO_FULL)
|
||||
#error "Shared mOTA queue storage requires an nRF52 Full source-only Companion"
|
||||
#if (!defined(NRF52_PLATFORM) && !defined(ESP32_PLATFORM)) || \
|
||||
!defined(OTA_SEEDER_ONLY) || !defined(COMPANION_RADIO_FULL)
|
||||
#error "Shared mOTA queue storage requires an nRF52 or ESP32 Full source-only Companion"
|
||||
#endif
|
||||
void ota_set_context_storage(void* owner, OtaContext* (*acquire)(void*),
|
||||
void (*release)(void*));
|
||||
|
||||
@@ -12,6 +12,13 @@ namespace ota {
|
||||
|
||||
OtaManager::~OtaManager() {
|
||||
free(_catalog_heap);
|
||||
free(_leaves_buf);
|
||||
#if defined(ESP32_PLATFORM)
|
||||
// A shared Companion workspace is destroyed between mOTA sessions. Return
|
||||
// the lazy source/proof buffers before its queue slots are reused.
|
||||
free(_src_leaves);
|
||||
free(_scratch);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OtaManager::expandCatalog() {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
// Full nRF52 Companions stream host-provided firmware and need no permanent
|
||||
// Qualified Full Companions stream host-provided firmware and need no permanent
|
||||
// mOTA workspace. Apply the same policy to build.sh and direct PlatformIO builds.
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SEEDER_ONLY) && defined(COMPANION_RADIO_FULL)
|
||||
#if defined(OTA_SEEDER_ONLY) && defined(COMPANION_RADIO_FULL) && \
|
||||
(defined(NRF52_PLATFORM) || \
|
||||
(defined(ESP32_PLATFORM) && defined(HELTEC_WIRELESS_PAPER)))
|
||||
#ifndef OTA_SHARED_COMPANION_QUEUE
|
||||
#define OTA_SHARED_COMPANION_QUEUE 1
|
||||
#endif
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <cstdarg>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum wifi_mode_t { WIFI_OFF, WIFI_STA, WIFI_AP, WIFI_AP_STA };
|
||||
constexpr int WL_CONNECTED = 3;
|
||||
struct IPAddress {
|
||||
uint32_t value = 0;
|
||||
operator uint32_t() const { return value; }
|
||||
std::string toString() const { return "192.0.2.1"; }
|
||||
};
|
||||
struct MockWiFi {
|
||||
bool connected = false;
|
||||
wifi_mode_t mode = WIFI_OFF;
|
||||
IPAddress ap;
|
||||
int status() const { return connected ? WL_CONNECTED : 0; }
|
||||
wifi_mode_t getMode() const { return mode; }
|
||||
IPAddress softAPIP() const { return ap; }
|
||||
};
|
||||
inline MockWiFi WiFi;
|
||||
|
||||
struct MockTcpState {
|
||||
bool connected = true;
|
||||
bool no_delay = false;
|
||||
unsigned requests = 0, flushes = 0;
|
||||
std::deque<uint8_t> received;
|
||||
std::function<std::vector<uint8_t>(const uint8_t*, size_t)> respond;
|
||||
};
|
||||
|
||||
class WiFiClient : public Stream {
|
||||
public:
|
||||
std::shared_ptr<MockTcpState> state;
|
||||
WiFiClient() = default;
|
||||
explicit WiFiClient(std::shared_ptr<MockTcpState> s) : state(s) {}
|
||||
explicit operator bool() const { return state && state->connected; }
|
||||
bool connected() const { return bool(*this); }
|
||||
void stop() { if (state) state->connected = false; }
|
||||
void setNoDelay(bool on) { if (state) state->no_delay = on; }
|
||||
IPAddress remoteIP() const { return {1}; }
|
||||
int available() override { return state ? state->received.size() : 0; }
|
||||
int read() override {
|
||||
if (!available()) return -1;
|
||||
int byte = state->received.front();
|
||||
state->received.pop_front();
|
||||
return byte;
|
||||
}
|
||||
size_t write(const uint8_t* bytes, size_t length) override {
|
||||
if (!connected()) return 0;
|
||||
++state->requests;
|
||||
const auto reply = state->respond(bytes, length);
|
||||
state->received.insert(state->received.end(), reply.begin(), reply.end());
|
||||
return length;
|
||||
}
|
||||
void flush() override {
|
||||
++state->flushes;
|
||||
state->received.clear(); // Real WiFiClient flush discards received bytes.
|
||||
}
|
||||
};
|
||||
|
||||
class WiFiServer {
|
||||
public:
|
||||
inline static std::deque<WiFiClient> pending;
|
||||
explicit WiFiServer(uint16_t) {}
|
||||
void begin() {}
|
||||
void end() { pending.clear(); }
|
||||
WiFiClient available() {
|
||||
if (pending.empty()) return {};
|
||||
WiFiClient result = pending.front();
|
||||
pending.pop_front();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
namespace mesh {
|
||||
struct MockLog {
|
||||
std::string output;
|
||||
void println(const char* text) { output += text; output += '\n'; }
|
||||
void printf(const char* format, ...) {
|
||||
char line[256];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(line, sizeof line, format, args);
|
||||
va_end(args);
|
||||
output += line;
|
||||
}
|
||||
};
|
||||
inline MockLog& usbLoggingPort() { static MockLog log; return log; }
|
||||
}
|
||||
+36
-22
@@ -61,6 +61,7 @@ public:
|
||||
|
||||
// The actual BLE source controller below sees a ready/not-ready link and a
|
||||
// source with real container bytes. Hardware GATT/serial framing is separate.
|
||||
#if !defined(ESP32_PLATFORM)
|
||||
struct Bluetooth {
|
||||
bool ready = false, active = false;
|
||||
int motaStream() { return 0; }
|
||||
@@ -78,6 +79,7 @@ public:
|
||||
};
|
||||
} }
|
||||
#include "ble_control_under_test.h"
|
||||
#endif
|
||||
|
||||
struct Packet { bool to_source; std::vector<uint8_t> bytes; };
|
||||
static std::deque<Packet> packets;
|
||||
@@ -86,6 +88,35 @@ static bool send(void* to_source, const uint8_t* bytes, uint16_t length, bool) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static void transfer(OtaContext& context, void* reply_route) {
|
||||
OtaManager receiver;
|
||||
OtaStoreRam<8192> destination;
|
||||
receiver.begin(EXP_TARGET_ID, send, reply_route);
|
||||
receiver.set_fetch_store(&destination);
|
||||
assert(receiver.pull(EXP_MERKLE_ROOT, EXP_TARGET_ID) == OtaManager::PULL_STARTED);
|
||||
for (uint32_t time = 100; time < 300000 && receiver.fetchState() != OtaManager::COMPLETE;
|
||||
time += 100) {
|
||||
context.manager.set_clock(time);
|
||||
receiver.set_clock(time);
|
||||
context.manager.serviceEgress();
|
||||
receiver.serviceEgress();
|
||||
if (time % 1000 == 0) { context.manager.loop(); receiver.loop(); }
|
||||
while (!packets.empty()) {
|
||||
Packet packet = packets.front();
|
||||
packets.pop_front();
|
||||
(packet.to_source ? context.manager : receiver).on_message(
|
||||
packet.bytes.data(), packet.bytes.size());
|
||||
}
|
||||
}
|
||||
assert(receiver.fetchState() == OtaManager::COMPLETE);
|
||||
assert(destination.staged_size() == MOTA_VEC_LEN);
|
||||
assert(memcmp(destination.data(), MOTA_VEC, MOTA_VEC_LEN) == 0);
|
||||
}
|
||||
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include "test_wifi_shared_queue.h"
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
Queue q;
|
||||
for (int i = 0; i < q.count; ++i) {
|
||||
@@ -145,28 +176,7 @@ int main() {
|
||||
"wrong owner", reply, sizeof reply));
|
||||
assert(context.folderLink() == link);
|
||||
|
||||
OtaManager receiver;
|
||||
OtaStoreRam<8192> destination;
|
||||
receiver.begin(EXP_TARGET_ID, send, &q);
|
||||
receiver.set_fetch_store(&destination);
|
||||
assert(receiver.pull(EXP_MERKLE_ROOT, EXP_TARGET_ID) == OtaManager::PULL_STARTED);
|
||||
for (uint32_t time = 100; time < 300000 && receiver.fetchState() != OtaManager::COMPLETE;
|
||||
time += 100) {
|
||||
context.manager.set_clock(time);
|
||||
receiver.set_clock(time);
|
||||
context.manager.serviceEgress();
|
||||
receiver.serviceEgress();
|
||||
if (time % 1000 == 0) { context.manager.loop(); receiver.loop(); }
|
||||
while (!packets.empty()) {
|
||||
Packet packet = packets.front();
|
||||
packets.pop_front();
|
||||
(packet.to_source ? context.manager : receiver).on_message(
|
||||
packet.bytes.data(), packet.bytes.size());
|
||||
}
|
||||
}
|
||||
assert(receiver.fetchState() == OtaManager::COMPLETE);
|
||||
assert(destination.staged_size() == MOTA_VEC_LEN);
|
||||
assert(memcmp(destination.data(), MOTA_VEC, MOTA_VEC_LEN) == 0);
|
||||
transfer(context, &q);
|
||||
check_messages();
|
||||
context.detach_folder(); // same cleanup for USB/BLE stop and disconnect
|
||||
context.manager.announce(); // callers may still use the context until loop boundary
|
||||
@@ -195,6 +205,9 @@ int main() {
|
||||
assert(!ota_context_if_active() && q.buffer.capacity() == 256);
|
||||
check_messages();
|
||||
|
||||
#if defined(ESP32_PLATFORM)
|
||||
test_wifi_shared_queue(q, check_messages);
|
||||
#else
|
||||
Nrf52BleMotaSourceControl ble;
|
||||
assert(!ble.start(reply, sizeof reply)); // no subscription, no storage loan
|
||||
assert(!ota_context_if_active() && q.buffer.capacity() == 256);
|
||||
@@ -235,4 +248,5 @@ int main() {
|
||||
ota_release_context_if_idle(true);
|
||||
assert(!ota_context_if_active() && q.buffer.capacity() == 256);
|
||||
check_messages();
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <helpers/esp32/WiFiOtaSeeder.h>
|
||||
#include <helpers/ota/FolderMotaStore.h>
|
||||
#include <helpers/ota/MotaSeederProto.h>
|
||||
#include <helpers/ota/OtaByteIO.h>
|
||||
|
||||
// Simulate only the TCP endpoint/host. The listener, stream framing, context,
|
||||
// queue, mOTA manager, source reader and complete transferred bytes are real.
|
||||
static std::shared_ptr<MockTcpState> connect_host() {
|
||||
auto state = std::make_shared<MockTcpState>();
|
||||
state->respond = [](const uint8_t* request, size_t length) {
|
||||
assert(length >= 4 && request[0] == 'M' && request[1] == 'S');
|
||||
uint8_t checksum = 0;
|
||||
for (size_t i = 2; i < length; ++i) checksum ^= request[i];
|
||||
assert(checksum == 0);
|
||||
std::vector<uint8_t> payload;
|
||||
const uint8_t op = request[2];
|
||||
if (op == MS_OP_COUNT) {
|
||||
payload.push_back(1);
|
||||
} else if (op == MS_OP_DESCRIBE) {
|
||||
assert(request[3] == 0);
|
||||
MotaDesc desc;
|
||||
Folder folder;
|
||||
assert(folder.describe(0, desc));
|
||||
payload.resize(MOTA_DESC_WIRE);
|
||||
memcpy(payload.data(), desc.mid, 4);
|
||||
wr_u32le(payload.data() + 4, desc.target_id);
|
||||
wr_u32le(payload.data() + 8, desc.fw_version);
|
||||
payload[12] = desc.codec_id;
|
||||
payload[13] = desc.flags;
|
||||
wr_u32le(payload.data() + 14, desc.total_size);
|
||||
wr_u32le(payload.data() + 18, desc.leaves_off);
|
||||
wr_u32le(payload.data() + 22, desc.block_count);
|
||||
wr_u32le(payload.data() + 26, desc.payload_off);
|
||||
wr_u32le(payload.data() + 30, desc.payload_size);
|
||||
payload[34] = desc.block_size_log2;
|
||||
} else {
|
||||
assert(op == MS_OP_READ && length == 11 && request[3] == 0);
|
||||
uint32_t offset = rd_u32le(request + 4);
|
||||
uint16_t count = request[8] | (uint16_t(request[9]) << 8);
|
||||
assert(uint64_t(offset) + count <= MOTA_VEC_LEN);
|
||||
payload.assign(MOTA_VEC + offset, MOTA_VEC + offset + count);
|
||||
}
|
||||
std::vector<uint8_t> reply = {'m', 's', op, MS_STATUS_OK};
|
||||
reply.insert(reply.end(), payload.begin(), payload.end());
|
||||
checksum = 0;
|
||||
for (uint8_t byte : reply) checksum ^= byte;
|
||||
reply.push_back(checksum);
|
||||
return reply;
|
||||
};
|
||||
WiFiServer::pending.emplace_back(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
template<class CheckMessages>
|
||||
static void test_wifi_shared_queue(Queue& q, CheckMessages check_messages) {
|
||||
WiFiOtaSeeder::stop(); // stopping before startup never allocates
|
||||
WiFiOtaSeeder::loop();
|
||||
assert(!WiFiOtaSeeder::isListening() && !ota_context_if_active());
|
||||
WiFi.connected = true;
|
||||
for (int i = 0; i < 100; ++i) WiFiOtaSeeder::loop();
|
||||
assert(WiFiOtaSeeder::isListening() && !ota_context_if_active());
|
||||
assert(q.buffer.capacity() == 256);
|
||||
|
||||
q.count = 129;
|
||||
Frame& extra = q.buffer.at((q.head + 128) % 256);
|
||||
extra.len = 176;
|
||||
memset(extra.buf, 128, sizeof extra.buf);
|
||||
auto refused = connect_host();
|
||||
WiFiOtaSeeder::loop();
|
||||
assert(!refused->connected && refused->requests == 0);
|
||||
assert(!ota_context_if_active() && q.buffer.capacity() == 256);
|
||||
assert(mesh::usbLoggingPort().output.find("sync unread messages") != std::string::npos);
|
||||
check_messages();
|
||||
--q.count;
|
||||
|
||||
for (int cycle = 0; cycle < 12; ++cycle) {
|
||||
WiFi.connected = true;
|
||||
auto host = connect_host();
|
||||
WiFiOtaSeeder::loop();
|
||||
assert(host->connected && host->no_delay && WiFiOtaSeeder::isAttached());
|
||||
assert(q.buffer.capacity() == 128 && ota_ctx().folder_dest);
|
||||
assert(ota_ctx().folderLink() == OtaContext::FOLDER_LINK_TCP);
|
||||
assert(ota_ctx().manager.servedCount() == 1);
|
||||
transfer(ota_ctx(), &q);
|
||||
assert(host->requests > 2 && host->flushes == 0);
|
||||
ota_release_context_if_idle(false);
|
||||
assert(ota_context_if_active()); // a connected host owns the loan
|
||||
check_messages();
|
||||
switch (cycle % 4) {
|
||||
case 0: // TCP disconnect while listener remains available
|
||||
host->connected = false;
|
||||
WiFiOtaSeeder::loop();
|
||||
break;
|
||||
case 1: // WiFi loss
|
||||
WiFi.connected = false;
|
||||
WiFiOtaSeeder::loop();
|
||||
assert(!WiFiOtaSeeder::isListening());
|
||||
break;
|
||||
case 2: // CLI off can return the context before the listener sees it
|
||||
ota_ctx().detach_folder();
|
||||
ota_ctx().clear_folder_dest();
|
||||
ota_release_context_if_idle(true);
|
||||
assert(!ota_context_if_active());
|
||||
WiFiOtaSeeder::loop();
|
||||
break;
|
||||
case 3: WiFiOtaSeeder::stop(); break;
|
||||
}
|
||||
ota_release_context_if_idle(true);
|
||||
assert(!host->connected && !WiFiOtaSeeder::isAttached());
|
||||
assert(!ota_context_if_active() && q.buffer.capacity() == 256);
|
||||
check_messages();
|
||||
packets.clear();
|
||||
}
|
||||
|
||||
// An incoming TCP client and listener stop must not detach a USB source.
|
||||
char reply[160];
|
||||
assert(ota_acquire_context(reply, sizeof reply));
|
||||
Folder usb;
|
||||
assert(ota_ctx().attach_folder_source(&usb, OtaContext::FOLDER_LINK_SERIAL,
|
||||
"USB", reply, sizeof reply));
|
||||
WiFi.connected = true;
|
||||
auto conflict = connect_host();
|
||||
WiFiOtaSeeder::loop();
|
||||
assert(!conflict->connected && !WiFiOtaSeeder::isAttached());
|
||||
WiFiOtaSeeder::stop();
|
||||
ota_release_context_if_idle(false);
|
||||
assert(ota_ctx().folderLink() == OtaContext::FOLDER_LINK_SERIAL);
|
||||
ota_ctx().detach_folder();
|
||||
ota_release_context_if_idle(true);
|
||||
assert(!ota_context_if_active() && q.buffer.capacity() == 256);
|
||||
|
||||
// TCP's stale attached flag must not clear a new USB capture destination.
|
||||
auto old_host = connect_host();
|
||||
WiFiOtaSeeder::loop();
|
||||
assert(WiFiOtaSeeder::isAttached());
|
||||
ota_ctx().detach_folder();
|
||||
ota_ctx().clear_folder_dest();
|
||||
ota_release_context_if_idle(true);
|
||||
assert(ota_acquire_context(reply, sizeof reply));
|
||||
assert(ota_ctx().attach_folder_source(&usb, OtaContext::FOLDER_LINK_SERIAL,
|
||||
"USB", reply, sizeof reply));
|
||||
WiFiClient usb_stream;
|
||||
FolderMotaStore usb_dest(usb_stream, MotaStreamWritePolicy::NoFlush);
|
||||
ota_ctx().set_folder_dest(&usb_dest, "USB");
|
||||
WiFiOtaSeeder::loop();
|
||||
assert(!old_host->connected && !WiFiOtaSeeder::isAttached());
|
||||
assert(ota_ctx().folder_dest == &usb_dest);
|
||||
assert(ota_ctx().folderLink() == OtaContext::FOLDER_LINK_SERIAL);
|
||||
ota_ctx().detach_folder();
|
||||
ota_ctx().clear_folder_dest();
|
||||
ota_release_context_if_idle(true);
|
||||
WiFiOtaSeeder::stop();
|
||||
assert(!ota_context_if_active() && q.buffer.capacity() == 256);
|
||||
check_messages();
|
||||
packets.clear();
|
||||
}
|
||||
@@ -218,13 +218,16 @@ int main() {
|
||||
with self.assertRaises(ValueError):
|
||||
ram.requirements(platform, definitions, "companion")
|
||||
|
||||
def test_nrf52_queue_sharing_applies_to_direct_full_builds_only(self):
|
||||
def test_queue_sharing_applies_to_qualified_direct_full_builds_only(self):
|
||||
source = '#include "src/helpers/ota/OtaMemoryPolicy.h"\n#ifdef OTA_SHARED_COMPANION_QUEUE\nSHARING_ENABLED\n#endif\n'
|
||||
for flags, expected in (
|
||||
(["NRF52_PLATFORM", "COMPANION_RADIO_FULL", "OTA_SEEDER_ONLY"], True),
|
||||
(["NRF52_PLATFORM", "OTA_SEEDER_ONLY"], False),
|
||||
(["NRF52_PLATFORM", "COMPANION_RADIO_FULL"], False),
|
||||
(["ESP32_PLATFORM", "COMPANION_RADIO_FULL", "OTA_SEEDER_ONLY"], False),
|
||||
(["ESP32_PLATFORM", "HELTEC_WIRELESS_PAPER", "COMPANION_RADIO_FULL", "OTA_SEEDER_ONLY"], True),
|
||||
(["ESP32_PLATFORM", "HELTEC_WIRELESS_PAPER", "OTA_SEEDER_ONLY"], False),
|
||||
(["ESP32_PLATFORM", "HELTEC_WIRELESS_PAPER", "COMPANION_RADIO_FULL"], False),
|
||||
):
|
||||
result = subprocess.run(["c++", "-x", "c++", "-E", "-I", str(ROOT),
|
||||
*("-D" + flag for flag in flags), "-"], input=source,
|
||||
@@ -233,7 +236,8 @@ int main() {
|
||||
|
||||
def test_affected_esp32_full_overlay_keeps_queue_and_both_transports(self):
|
||||
for target in ("Heltec_v3_companion_radio_full", "Xiao_C3_companion_radio_full",
|
||||
"heltec_tracker_v2_companion_radio_full_femon"):
|
||||
"heltec_tracker_v2_companion_radio_full_femon",
|
||||
"Heltec_Wireless_Paper_companion_radio_full"):
|
||||
result = subprocess.run(["bash", "-c", '''
|
||||
source build.sh
|
||||
PIO_ENV_PLATFORM_BY_NAME["$1"]=ESP32_PLATFORM
|
||||
@@ -242,8 +246,15 @@ requires_esp32_companion_full_ota_fallback() { return 1; }
|
||||
apply_companion_radio_full_profile "$1" "$1"
|
||||
printf '%s\\n' "$PLATFORMIO_BUILD_FLAGS"
|
||||
''', "test", target], cwd=ROOT, text=True, capture_output=True, check=True)
|
||||
self.assertIn("-DMAX_CONTACTS=150", result.stdout)
|
||||
self.assertNotIn("-DOFFLINE_QUEUE_SIZE=", result.stdout)
|
||||
if target == "Heltec_Wireless_Paper_companion_radio_full":
|
||||
self.assertIn("-DMAX_CONTACTS=350", result.stdout)
|
||||
self.assertNotIn("-DMAX_CONTACTS=150", result.stdout)
|
||||
self.assertIn("-DOFFLINE_QUEUE_SIZE=256", result.stdout)
|
||||
self.assertIn("-DOTA_SHARED_COMPANION_QUEUE=1", result.stdout)
|
||||
else:
|
||||
self.assertIn("-DMAX_CONTACTS=150", result.stdout)
|
||||
self.assertNotIn("-DOFFLINE_QUEUE_SIZE=", result.stdout)
|
||||
self.assertNotIn("-DOTA_SHARED_COMPANION_QUEUE", result.stdout)
|
||||
self.assertIn("-DWIFI_OTA_SEEDER=1", result.stdout)
|
||||
self.assertIn("-DBLE_PIN_CODE=123456", result.stdout)
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
class SharedMotaQueueTest(unittest.TestCase):
|
||||
def test_context_lifecycle_and_real_transfers_preserve_unread_messages(self):
|
||||
self.run_transfer_test("NRF52_PLATFORM")
|
||||
|
||||
def test_wireless_paper_wifi_listener_and_transfers_share_only_when_needed(self):
|
||||
self.run_transfer_test("ESP32_PLATFORM")
|
||||
|
||||
def run_transfer_test(self, platform):
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-mota-queue-") as temp:
|
||||
binary = Path(temp) / "transfer"
|
||||
tinf = Path(temp) / "tinf.o"
|
||||
@@ -29,11 +35,18 @@ class SharedMotaQueueTest(unittest.TestCase):
|
||||
"src/helpers/ota/MerkleTree.cpp", "src/helpers/ota/OtaDeflate.cpp",
|
||||
"src/Utils.cpp",
|
||||
]
|
||||
flags = ["-D" + platform + "=1"]
|
||||
if platform == "ESP32_PLATFORM":
|
||||
flags += ["-DHELTEC_WIRELESS_PAPER=1", "-DWIFI_OTA_SEEDER=1"]
|
||||
sources += ["src/helpers/esp32/WiFiOtaSeeder.cpp",
|
||||
"src/helpers/ota/MotaSourceSerial.cpp",
|
||||
"src/helpers/ota/FolderMotaStore.cpp"]
|
||||
built = subprocess.run([
|
||||
"c++", "-std=c++17", "-fsanitize=address,undefined", "-g",
|
||||
"-DNRF52_PLATFORM=1", "-DOTA_SEEDER_ONLY=1",
|
||||
*flags, "-DOTA_SEEDER_ONLY=1",
|
||||
"-DCOMPANION_RADIO_FULL=1", "-DOTA_SHARED_COMPANION_QUEUE=1",
|
||||
"-DENABLE_OTA=1", "-I", str(ROOT / "src"),
|
||||
"-I", str(ROOT / "test/fixtures/shared_mota_queue/mocks"),
|
||||
"-I", str(ROOT / "test/mocks"), "-I", temp,
|
||||
*[str(ROOT / source) for source in sources], str(tinf),
|
||||
"-o", str(binary),
|
||||
|
||||
Reference in new issue
Block a user