diff --git a/build.sh b/build.sh index c63e7daa..f5eae190 100755 --- a/build.sh +++ b/build.sh @@ -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|\ diff --git a/docs/firmware_memory_budget.md b/docs/firmware_memory_budget.md index cdbc94c3..f7315c65 100644 --- a/docs/firmware_memory_budget.md +++ b/docs/firmware_memory_budget.md @@ -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 diff --git a/docs/full_companion_features.md b/docs/full_companion_features.md index 84e4d83f..bc1a2a93 100644 --- a/docs/full_companion_features.md +++ b/docs/full_companion_features.md @@ -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 diff --git a/src/helpers/esp32/WiFiOtaSeeder.cpp b/src/helpers/esp32/WiFiOtaSeeder.cpp index 390d363c..a7758e8d 100644 --- a/src/helpers/esp32/WiFiOtaSeeder.cpp +++ b/src/helpers/esp32/WiFiOtaSeeder.cpp @@ -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(); diff --git a/src/helpers/ota/OtaContext.h b/src/helpers/ota/OtaContext.h index a4a02529..33d80b33 100644 --- a/src/helpers/ota/OtaContext.h +++ b/src/helpers/ota/OtaContext.h @@ -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*)); diff --git a/src/helpers/ota/OtaManager.cpp b/src/helpers/ota/OtaManager.cpp index 14467491..5b5fef67 100644 --- a/src/helpers/ota/OtaManager.cpp +++ b/src/helpers/ota/OtaManager.cpp @@ -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() { diff --git a/src/helpers/ota/OtaMemoryPolicy.h b/src/helpers/ota/OtaMemoryPolicy.h index 95c64dd2..07937e10 100644 --- a/src/helpers/ota/OtaMemoryPolicy.h +++ b/src/helpers/ota/OtaMemoryPolicy.h @@ -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 diff --git a/test/fixtures/shared_mota_queue/mocks/WiFi.h b/test/fixtures/shared_mota_queue/mocks/WiFi.h new file mode 100644 index 00000000..3d6d5253 --- /dev/null +++ b/test/fixtures/shared_mota_queue/mocks/WiFi.h @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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 received; + std::function(const uint8_t*, size_t)> respond; +}; + +class WiFiClient : public Stream { +public: + std::shared_ptr state; + WiFiClient() = default; + explicit WiFiClient(std::shared_ptr 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 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; } +} diff --git a/test/fixtures/shared_mota_queue/test_shared_mota_queue.cpp b/test/fixtures/shared_mota_queue/test_shared_mota_queue.cpp index d3f3416c..083efb64 100644 --- a/test/fixtures/shared_mota_queue/test_shared_mota_queue.cpp +++ b/test/fixtures/shared_mota_queue/test_shared_mota_queue.cpp @@ -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 bytes; }; static std::deque 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 } diff --git a/test/fixtures/shared_mota_queue/test_wifi_shared_queue.h b/test/fixtures/shared_mota_queue/test_wifi_shared_queue.h new file mode 100644 index 00000000..80c7f14e --- /dev/null +++ b/test/fixtures/shared_mota_queue/test_wifi_shared_queue.h @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include +#include +#include + +// 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 connect_host() { + auto state = std::make_shared(); + 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 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 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 +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(); +} diff --git a/test/test_firmware_ram.py b/test/test_firmware_ram.py index 341bcea9..9547964c 100644 --- a/test/test_firmware_ram.py +++ b/test/test_firmware_ram.py @@ -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) diff --git a/test/test_shared_mota_queue.py b/test/test_shared_mota_queue.py index 080b5470..b112c80b 100644 --- a/test/test_shared_mota_queue.py +++ b/test/test_shared_mota_queue.py @@ -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),