#include #include #include #include #include #include #include #include #include static unsigned critical_depth; struct Guard { Guard() { ++critical_depth; } ~Guard() { --critical_depth; } }; static std::function during_copy; static void* scheduled_copy(void* dest, const void* src, size_t length) { void* result = std::memcpy(dest, src, length); if (during_copy) { auto operation = std::move(during_copy); during_copy = nullptr; operation(); } return result; } #define memcpy scheduled_copy #include #undef memcpy #define MAX_FRAME_SIZE 176 #define FRAME_QUEUE_SIZE 12 #define BLE_CONN_HANDLE_INVALID 0xffff #define BLE_RETRY_THROTTLE_MS 250 #define BLE_DEBUG_PRINTLN(...) assert(!critical_depth) static std::function trace_hook; static void trace_event(uint32_t event) { assert(!critical_depth); if (trace_hook) trace_hook(event); } #define meshBleTrace(event, ...) trace_event(event) #define NRF_SUCCESS 0 #define NRF_ERROR_INVALID_STATE 8 #define BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION 0x13 static std::vector disconnect_requests; static uint32_t sd_ble_gap_disconnect(uint16_t handle, uint8_t) { assert(!critical_depth); disconnect_requests.push_back(handle); return NRF_SUCCESS; } static uint32_t now_ms = 100; static uint32_t millis() { return now_ms; } struct BLEConnection { uint16_t mtu = 23; bool connected() const { assert(!critical_depth); return true; } uint16_t getMtu() const { assert(!critical_depth); return mtu; } }; static struct { BLEConnection connection; BLEConnection* Connection(uint16_t handle) { assert(!critical_depth); return handle == BLE_CONN_HANDLE_INVALID ? nullptr : &connection; } unsigned connected() const { assert(!critical_depth); return 1; } } Bluefruit; struct SerialBLEInterface; static SerialBLEInterface* instance; struct SerialBLEInterface { struct Frame { uint8_t len = 0; uint8_t buf[MAX_FRAME_SIZE] = {}; }; using RxQueueGuard = Guard; using RxQueue = mesh_nrf52::BleRxFrameQueue; RxQueue recv_queue; uint8_t send_queue_len = 0; Frame send_queue[FRAME_QUEUE_SIZE]; std::atomic _tx_reset_pending{false}; std::atomic _companionDataSeen{true}; uint32_t _send_queue_generation = 0; uint16_t _tx_recovery_handle = BLE_CONN_HANDLE_INVALID; uint32_t _tx_recovery_generation = 0; uint16_t _conn_handle = 1; bool _isEnabled = true, _isDeviceConnected = true; unsigned long _last_retry_attempt = 0; mesh::BleTxStallWatchdog _tx_stall_watchdog; mesh::BleDisconnectRecovery _tx_disconnect_recovery; SecuritySessionTimer _companion_start_timer; SecuritySessionTimer _security_timer; struct MotaStream { bool active = false; void setActive(bool value) { active = value; } } _mota_stream; bool _peer_address_valid = true; struct Uart { struct Chunk { uint16_t handle; std::vector bytes; }; std::vector chunks; std::vector rx_bytes; std::function after_write; unsigned attempts = 0, fail_on_attempt = 0; bool notifyEnabled(uint16_t) const { assert(!critical_depth); return true; } void flush() { assert(!critical_depth); rx_bytes.clear(); } int available() const { assert(!critical_depth); return rx_bytes.size(); } int read(uint8_t* destination, size_t length) { assert(!critical_depth && length <= rx_bytes.size()); std::memcpy(destination, rx_bytes.data(), length); rx_bytes.erase(rx_bytes.begin(), rx_bytes.begin() + length); return length; } size_t write(uint16_t handle, const uint8_t* data, size_t length) { assert(!critical_depth); if (++attempts == fail_on_attempt) return 0; chunks.push_back({handle, std::vector(data, data + length)}); if (after_write) { auto operation = std::move(after_write); after_write = nullptr; operation(); } return length; } } bleuart; SerialBLEInterface() { instance = this; open(1); } void open(uint16_t handle) { _conn_handle = handle; recv_queue.setEnabled(true); recv_queue.connect(handle); recv_queue.secure(handle); _isEnabled = _isDeviceConnected = true; } void reconnect(uint16_t handle, bool deferred_callback = true) { recv_queue.disconnect(_conn_handle); open(handle); if (deferred_callback) clearBuffers(); } void serviceBondedOnlyTransition() {} bool advertisingAllowed() const { return false; } bool isAdvertising() const { return false; } bool startAdvertising(const char*) { assert(false); return false; } void serviceTxRecovery(uint32_t); void clearBuffers(); void resetTxState(); bool servicePendingTxReset(); void shiftSendQueueLeft(); size_t writeBleUartFrame(const Frame&); bool hasRequiredReplyCapacity() const; size_t writeFrame(const uint8_t*, size_t); size_t checkRecvFrame(uint8_t*); static void onBleUartRX(uint16_t); void setMotaStreamActive(bool); void recoverStalledTx(const char*, uint16_t = BLE_CONN_HANDLE_INVALID, uint32_t = 0); bool isConnected() const; bool isWriteBusy() const; bool hasPendingIO() const; }; @METHODS@ static void application_start() { for (bool required : {false, true}) { SerialBLEInterface port; const size_t requests = disconnect_requests.size(); now_ms = 100; Bluefruit.connection.mtu = 23; port._companionDataSeen.store(false); port._companion_start_timer.start(now_ms); uint8_t frame[75] = {uint8_t(required ? 0x87 : 0x88)}; uint8_t received[MAX_FRAME_SIZE] = {}; port.bleuart.fail_on_attempt = 4; // Three default-MTU slots accept 60 bytes. assert(port.writeFrame(frame, sizeof(frame)) == sizeof(frame)); for (uint32_t elapsed : {0U, 250U, 10001U}) { now_ms = 100 + elapsed; assert(port.checkRecvFrame(received) == 0); assert(port.bleuart.chunks.empty() && !port.bleuart.attempts); assert(port.send_queue_len == 1 && !port._last_retry_attempt); assert(!port._tx_stall_watchdog.active() && !port._tx_disconnect_recovery.pending()); assert(port._companion_start_timer.pending()); assert(disconnect_requests.size() == requests); } // Even a terminal's first newline opens the gate through the real RX // callback, independently of checkRecvFrame's application-task polling. Bluefruit.connection.mtu = 247; port.bleuart.rx_bytes = {'\n'}; SerialBLEInterface::onBleUartRX(1); assert(port._companionDataSeen && !port._companion_start_timer.pending()); assert(port.checkRecvFrame(received) == 1 && received[0] == '\n'); assert(port.send_queue_len == 0 && port.bleuart.chunks.size() == 1); assert(port.bleuart.chunks[0].bytes == std::vector(frame, frame + sizeof(frame))); assert(disconnect_requests.size() == requests); } // A Full Companion's explicit mOTA activation establishes readiness without // requiring a Nordic UART command, retaining the separate mOTA transport. SerialBLEInterface port; port._companionDataSeen.store(false); port._companion_start_timer.start(now_ms); const uint8_t frame[] = {0x83}; uint8_t received[MAX_FRAME_SIZE] = {}; assert(port.writeFrame(frame, sizeof(frame)) == sizeof(frame)); assert(port.checkRecvFrame(received) == 0 && port.bleuart.chunks.empty()); port.setMotaStreamActive(true); assert(port._mota_stream.active && port._companionDataSeen); assert(!port._companion_start_timer.pending()); assert(port.checkRecvFrame(received) == 0 && port.send_queue_len == 0); assert(port.bleuart.chunks.size() == 1); Bluefruit.connection.mtu = 23; } static void previous_prestart_partial() { SerialBLEInterface port; const size_t requests = disconnect_requests.size(); Bluefruit.connection.mtu = 23; port._companionDataSeen.store(false); uint8_t frame[75] = {0x88}, received[MAX_FRAME_SIZE] = {}; port.bleuart.fail_on_attempt = 4; assert(port.writeFrame(frame, sizeof(frame)) == sizeof(frame)); assert(port.checkRecvFrame(received) == 0); assert(port.bleuart.chunks.size() == 3); size_t accepted = 0; for (const auto& chunk : port.bleuart.chunks) accepted += chunk.bytes.size(); assert(accepted == 60 && port._tx_disconnect_recovery.pending()); assert(disconnect_requests.size() == requests + 1); } static void enqueue_reset() { SerialBLEInterface port; const uint8_t old_reply[] = {1, 11}, interrupted[] = {2, 22}, fresh[] = {3, 33}; assert(port.writeFrame(old_reply, sizeof(old_reply)) == sizeof(old_reply)); port._last_retry_attempt = 77; port._tx_stall_watchdog.noteBlocked(100); during_copy = [&]() { port.clearBuffers(); }; assert(port.writeFrame(interrupted, sizeof(interrupted)) == 0); assert(port.send_queue_len == 0 && !port._last_retry_attempt); assert(!port._tx_stall_watchdog.active() && !port._tx_disconnect_recovery.pending()); assert(port.writeFrame(fresh, sizeof(fresh)) == sizeof(fresh)); uint8_t received[MAX_FRAME_SIZE]; assert(port.checkRecvFrame(received) == 0 && port.send_queue_len == 0); assert(port.bleuart.chunks.size() == 1 && port.bleuart.chunks[0].bytes[1] == 33); } static void callback_ownership() { SerialBLEInterface port; const uint8_t reply[] = {1, 11}; for (unsigned i = 0; i < FRAME_QUEUE_SIZE; ++i) assert(port.writeFrame(reply, sizeof(reply)) == sizeof(reply)); assert(port.isWriteBusy() && port.hasPendingIO()); port.clearBuffers(); // A callback invalidates acceptance immediately without mutating memory // that the interrupted main-task enqueue/copy may still be using. assert(port.send_queue_len == FRAME_QUEUE_SIZE && port._tx_reset_pending); assert(!port.isWriteBusy() && !port.hasPendingIO() && port.hasRequiredReplyCapacity()); assert(port.servicePendingTxReset() && !port.servicePendingTxReset()); assert(port.send_queue_len == 0); } static void connection_changes() { for (uint16_t replacement : {uint16_t(1), uint16_t(2)}) for (bool deferred_callback : {false, true}) { SerialBLEInterface port; const size_t requests = disconnect_requests.size(); uint8_t frame[40]; for (unsigned i = 0; i < sizeof(frame); ++i) frame[i] = i; assert(port.writeFrame(frame, sizeof(frame)) == sizeof(frame)); port.bleuart.after_write = [&]() { port.reconnect(replacement, deferred_callback); }; uint8_t received[MAX_FRAME_SIZE]; assert(port.checkRecvFrame(received) == 0); assert(port.bleuart.chunks.size() == 1); assert(port.bleuart.chunks[0].handle == 1 && port.bleuart.chunks[0].bytes.size() == 20); assert(port.send_queue_len == 0 && !port._tx_disconnect_recovery.pending()); // Old partial completion must not recover/disconnect the new session. assert(disconnect_requests.size() == requests && port.isConnected()); const uint8_t fresh[] = {1, 99}; assert(port.writeFrame(fresh, sizeof(fresh)) == sizeof(fresh)); assert(port.checkRecvFrame(received) == 0 && port.send_queue_len == 0); assert(port.bleuart.chunks.size() == 2); assert(port.bleuart.chunks[1].handle == replacement && port.bleuart.chunks[1].bytes == std::vector(fresh, fresh + sizeof(fresh))); } } static void old_queue_and_recovery() { uint8_t received[MAX_FRAME_SIZE]; const uint8_t reply[] = {1, 11}; { SerialBLEInterface port; assert(port.writeFrame(reply, sizeof(reply)) == sizeof(reply)); port.reconnect(2, false); // Physical event precedes its deferred callback. assert(port.checkRecvFrame(received) == 0 && port.bleuart.chunks.empty()); assert(port.send_queue_len == 0 && !port._tx_disconnect_recovery.pending()); } { SerialBLEInterface port; const size_t requests = disconnect_requests.size(); uint8_t frame[40] = {1}; assert(port.writeFrame(frame, sizeof(frame)) == sizeof(frame)); port.bleuart.fail_on_attempt = 2; assert(port.checkRecvFrame(received) == 0); assert(port.bleuart.chunks.size() == 1 && port._tx_disconnect_recovery.pending()); assert(port.send_queue_len == 0 && disconnect_requests.size() == requests+1); assert(disconnect_requests.back() == 1); assert(port.checkRecvFrame(received) == 0 && port._tx_disconnect_recovery.pending()); assert(disconnect_requests.size() == requests+1); // Own recovery persists and throttles retries. now_ms += 1001; assert(port.checkRecvFrame(received) == 0 && port._tx_disconnect_recovery.pending()); assert(disconnect_requests.size() == requests+2 && disconnect_requests.back() == 1); port.reconnect(2); assert(port.isConnected()); // Old recovery cannot poison a secured new link. assert(port.writeFrame(reply, sizeof(reply)) == sizeof(reply)); assert(port.checkRecvFrame(received) == 0 && !port._tx_disconnect_recovery.pending()); } } static void recovery_connection_changes() { for (uint32_t scheduling_point : {0xf031U, 0xf025U, 0xf020U}) for (uint16_t replacement : {uint16_t(1), uint16_t(2)}) { SerialBLEInterface port; const size_t requests = disconnect_requests.size(); uint8_t frame[40] = {1}, received[MAX_FRAME_SIZE]; assert(port.writeFrame(frame, sizeof(frame)) == sizeof(frame)); port.bleuart.fail_on_attempt = 2; bool changed = false; trace_hook = [&](uint32_t event) { if (!changed && event == scheduling_point) { changed = true; port.reconnect(replacement); } }; assert(port.checkRecvFrame(received) == 0 && changed); trace_hook = nullptr; assert(disconnect_requests.size() == requests); assert(port.checkRecvFrame(received) == 0 && !port._tx_disconnect_recovery.pending()); assert(port.isConnected() && disconnect_requests.size() == requests); } // A physical connect event may precede its deferred callback, even when // SoftDevice reuses a handle. A retry must still not disconnect that link. for (uint16_t replacement : {uint16_t(1), uint16_t(2)}) { SerialBLEInterface port; uint8_t frame[40] = {1}, received[MAX_FRAME_SIZE]; assert(port.writeFrame(frame, sizeof(frame)) == sizeof(frame)); port.bleuart.fail_on_attempt = 2; assert(port.checkRecvFrame(received) == 0 && port._tx_disconnect_recovery.pending()); const size_t requests = disconnect_requests.size(); port.reconnect(replacement, false); now_ms += 1001; assert(port.checkRecvFrame(received) == 0 && !port._tx_disconnect_recovery.pending()); assert(disconnect_requests.size() == requests && port.isConnected()); } } int main(int argc, char** argv) { assert(argc == 2); if (!std::strcmp(argv[1], "enqueue")) enqueue_reset(); else if (!std::strcmp(argv[1], "connection")) connection_changes(); else if (!std::strcmp(argv[1], "application-start")) application_start(); else if (!std::strcmp(argv[1], "prestart-partial")) previous_prestart_partial(); else { enqueue_reset(); callback_ownership(); connection_changes(); old_queue_and_recovery(); recovery_connection_changes(); } assert(!critical_depth && !during_copy); }