#!/usr/bin/env python3 """Execute production nRF52 BLE queue/dispatch methods with simulated UART IO. This covers real admission, RX-dequeue, retry-throttle and recovery ordering, not Bluefruit/SoftDevice callbacks or physical ATT delivery. """ from pathlib import Path import os import subprocess import tempfile import unittest from test_t096_full_memory import method ROOT = Path(__file__).resolve().parents[1] SOURCE = ROOT / "src/helpers/nrf52/SerialBLEInterface.cpp" HARNESS = r''' #include #include #include #include #include #include #define MAX_FRAME_SIZE 176 #define FRAME_QUEUE_SIZE 12 #define BLE_RETRY_THROTTLE_MS 250 #define BLE_DEBUG_PRINTLN(...) do {} while (0) #define meshBleTrace(...) do {} while (0) static uint32_t current_millis = 100; uint32_t millis() { return current_millis; } struct SerialBLEInterface { struct Frame { uint8_t len = 0; uint8_t buf[MAX_FRAME_SIZE] = {}; }; uint8_t send_queue_len = 0, recv_queue_len = 0; Frame send_queue[FRAME_QUEUE_SIZE], recv_queue[FRAME_QUEUE_SIZE]; unsigned long _last_retry_attempt = 0; mesh::BleTxStallWatchdog _tx_stall_watchdog; mesh::BleDisconnectRecovery _tx_disconnect_recovery; bool connected = true, allow_write = false, partial_write = false; unsigned write_attempts = 0, recovery_services = 0, recoveries = 0; std::vector> transmitted; bool isConnected() const { return connected && !_tx_disconnect_recovery.pending(); } void serviceBondedOnlyTransition() {} void serviceTxRecovery(uint32_t) { ++recovery_services; } void recoverStalledTx(const char*) { ++recoveries; _tx_disconnect_recovery.begin(); } size_t writeBleUartFrame(const Frame& frame) { ++write_attempts; if (partial_write) return 1; if (!allow_write) return 0; transmitted.emplace_back(frame.buf, frame.buf + frame.len); return frame.len; } void receive(const uint8_t* bytes, size_t size) { assert(recv_queue_len < FRAME_QUEUE_SIZE && size <= MAX_FRAME_SIZE); recv_queue[recv_queue_len].len = size; memcpy(recv_queue[recv_queue_len++].buf, bytes, size); } void shiftSendQueueLeft(); void shiftRecvQueueLeft(); bool hasRequiredReplyCapacity() const; bool isWriteBusy() const; size_t writeFrame(const uint8_t*, size_t); size_t checkRecvFrame(uint8_t*); }; @METHODS@ static void full_required_queue() { current_millis = 100; SerialBLEInterface link; for (uint8_t i = 0; i < FRAME_QUEUE_SIZE; ++i) { const uint8_t reply[] = {0x0d, i}; assert(link.writeFrame(reply, sizeof(reply)) == sizeof(reply)); } const uint8_t command[] = {66, 'A', '7', '|', 'v', 'e', 'r'}; link.receive(command, sizeof(command)); uint8_t received[MAX_FRAME_SIZE] = {}; assert(link.checkRecvFrame(received) == 0); assert(link.recv_queue_len == 1 && link.send_queue_len == FRAME_QUEUE_SIZE); assert(link.write_attempts == 1 && link.transmitted.empty()); ++current_millis; assert(link.checkRecvFrame(received) == 0); assert(link.write_attempts == 1); // Existing 250 ms retry throttle is retained. assert(link.recv_queue_len == 1 && link.isWriteBusy()); current_millis = 351; link.allow_write = true; // A completed TX frees admission on this SAME call, before RX is examined. assert(link.checkRecvFrame(received) == sizeof(command)); assert(memcmp(received, command, sizeof(command)) == 0); assert(link.recv_queue_len == 0 && link.send_queue_len == FRAME_QUEUE_SIZE - 1); const uint8_t reply[] = {29, 'A', '7', '|', 'O', 'K'}; assert(link.writeFrame(reply, sizeof(reply)) == sizeof(reply)); for (unsigned i = 0; i < FRAME_QUEUE_SIZE + 2; ++i) { ++current_millis; assert(link.checkRecvFrame(received) == 0); } assert(link.transmitted.size() == FRAME_QUEUE_SIZE + 1); for (uint8_t i = 0; i < FRAME_QUEUE_SIZE; ++i) assert((link.transmitted[i] == std::vector{0x0d, i})); assert(link.transmitted.back() == std::vector(reply, reply + sizeof(reply))); assert(link.send_queue_len == 0 && link.recv_queue_len == 0); } static void best_effort_eviction() { current_millis = 100; SerialBLEInterface link; for (uint8_t i = 0; i < FRAME_QUEUE_SIZE - 1; ++i) { const uint8_t log[] = {0x88, i}; assert(link.writeFrame(log, sizeof(log)) == sizeof(log)); } const uint8_t log[] = {0x88, 99}; assert(link.writeFrame(log, sizeof(log)) == 0); // One required slot stays reserved. const uint8_t previous[] = {0x0d, 99}; assert(link.writeFrame(previous, sizeof(previous)) == sizeof(previous)); const uint8_t command[] = {66, 'B', '8', '|', 'v', 'e', 'r'}; link.receive(command, sizeof(command)); uint8_t received[MAX_FRAME_SIZE] = {}; assert(link.isWriteBusy()); // Preserve the nRF52 >=8/12 producer high-water mark. assert(link.checkRecvFrame(received) == sizeof(command)); assert(memcmp(received, command, sizeof(command)) == 0); const uint8_t reply[] = {29, 'B', '8', '|', 'O', 'K'}; assert(link.writeFrame(reply, sizeof(reply)) == sizeof(reply)); current_millis = 351; link.allow_write = true; for (unsigned i = 0; i < FRAME_QUEUE_SIZE + 2; ++i) { assert(link.checkRecvFrame(received) == 0); ++current_millis; } assert(link.transmitted.size() == FRAME_QUEUE_SIZE); assert(link.transmitted[0] == std::vector(previous, previous + sizeof(previous))); assert(link.transmitted[1] == std::vector(reply, reply + sizeof(reply))); for (uint8_t i = 0; i < FRAME_QUEUE_SIZE - 2; ++i) assert((link.transmitted[i + 2] == std::vector{0x88, i})); } static void producer_threshold_and_recovery_ordering() { current_millis = 100; SerialBLEInterface link; const uint8_t reply[] = {0x0d, 1}; for (unsigned i = 0; i < 7; ++i) assert(link.writeFrame(reply, sizeof(reply)) == sizeof(reply)); assert(!link.isWriteBusy()); assert(link.writeFrame(reply, sizeof(reply)) == sizeof(reply)); assert(link.isWriteBusy()); const uint8_t command[] = {66, 'v'}; link.receive(command, sizeof(command)); link._tx_disconnect_recovery.begin(); uint8_t received[MAX_FRAME_SIZE] = {}; assert(link.checkRecvFrame(received) == 0); assert(link.recovery_services == 1 && link.write_attempts == 0 && link.recv_queue_len == 1); SerialBLEInterface partial; partial.partial_write = true; assert(partial.writeFrame(reply, sizeof(reply)) == sizeof(reply)); partial.receive(command, sizeof(command)); assert(partial.checkRecvFrame(received) == 0); assert(partial.recoveries == 1 && partial.write_attempts == 1); assert(partial.recv_queue_len == 1); // Recovery is invoked instead of dispatch. } int main() { full_required_queue(); best_effort_eviction(); producer_threshold_and_recovery_ordering(); } ''' class Nrf52BleBackpressureTests(unittest.TestCase): def test_real_queue_dispatch_throttle_and_recovery_under_sanitizers(self): source = SOURCE.read_text() signatures = ["void SerialBLEInterface::shiftSendQueueLeft()", "void SerialBLEInterface::shiftRecvQueueLeft()", "size_t SerialBLEInterface::writeFrame(", "size_t SerialBLEInterface::checkRecvFrame(", "bool SerialBLEInterface::isWriteBusy() const"] capacity = "bool SerialBLEInterface::hasRequiredReplyCapacity() const" if capacity in source: signatures.append(capacity) extracted = "\n".join(method(source, signature) for signature in signatures) sanitizers = [] if os.name == "nt" else ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"] with tempfile.TemporaryDirectory(prefix="meshcore-nrf-ble-backpressure-") as temp: binary = Path(temp) / "backpressure" built = subprocess.run(["c++", "-std=c++17", "-x", "c++", "-", "-I" + str(ROOT / "src"), *sanitizers, "-o", str(binary)], input=HARNESS.replace("@METHODS@", extracted), capture_output=True, text=True, timeout=30) self.assertEqual(built.returncode, 0, built.stderr) result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10) self.assertEqual(result.returncode, 0, result.stderr) def test_queue_capacity_remains_twelve(self): header = (ROOT / "src/helpers/nrf52/SerialBLEInterface.h").read_text() self.assertRegex(header, r"#define\s+FRAME_QUEUE_SIZE\s+12\b") if __name__ == "__main__": unittest.main()