From 0d417dd5548c9d8af885579cf503443be989d579 Mon Sep 17 00:00:00 2001 From: "Dr. Michael Lauer" Date: Sun, 23 Nov 2025 16:47:05 +0100 Subject: [PATCH 1/6] feat(examples): update L2CAP stress test client/server with memory monitoring This commit improves the L2CAP testing framework to help debug (suspected) L2CAP issues in the NimBLE stack. The examples implement a client-server pair that can stress test L2CAP connections with progressive payload sizes. Client (L2CAP_Client): - Connects to devices advertising with name "l2cap" - Uses PSM 192 for L2CAP connection - Implements framed protocol: [seqno:8bit][length:16bit BE][payload] - Starts with 64-byte payloads, doubles every 50 blocks up to 1024 bytes - Sends data continuously without delays for maximum throughput - Includes real-time bandwidth and performance monitoring Server (L2CAP_Server): - Advertises as "l2cap" to match client scanning - Listens on PSM 192 - Parses framed protocol with proper buffering for fragmented frames - Validates sequence numbers and reports errors - Tracks frames per second, total bytes, and bandwidth Both examples include: - Heap memory monitoring with leak detection - Warns after 10 consecutive heap decreases - Status updates every second showing: - Bandwidth in KB/s and Mbps - Current/minimum heap memory - Memory used since start - Frame/block statistics Testing results: - Stable operation at ~84 KB/s (0.69 Mbps) with 1024-byte payloads - Identified crash when attempting 2048-byte payloads (memory allocation issue) - No memory leaks detected during extended operation This framework enables systematic debugging of L2CAP implementation issues by providing detailed metrics and stress testing capabilities. --- examples/L2CAP/L2CAP_Client/main/main.cpp | 156 +++++++++++++++------ examples/L2CAP/L2CAP_Server/main/main.cpp | 157 +++++++++++++++++----- 2 files changed, 238 insertions(+), 75 deletions(-) diff --git a/examples/L2CAP/L2CAP_Client/main/main.cpp b/examples/L2CAP/L2CAP_Client/main/main.cpp index e4f21913..d15f5b76 100644 --- a/examples/L2CAP/L2CAP_Client/main/main.cpp +++ b/examples/L2CAP/L2CAP_Client/main/main.cpp @@ -1,15 +1,11 @@ #include +#include -// See the following for generating UUIDs: -// https://www.uuidgenerator.net/ - -// The remote service we wish to connect to. -static BLEUUID serviceUUID("dcbc7255-1e9e-49a0-a360-b0430b6c6905"); -// The characteristic of the remote service we are interested in. -static BLEUUID charUUID("371a55c8-f251-4ad2-90b3-c7c195b049be"); - -#define L2CAP_CHANNEL 150 +#define L2CAP_PSM 192 #define L2CAP_MTU 5000 +#define INITIAL_PAYLOAD_SIZE 64 +#define BLOCKS_BEFORE_DOUBLE 50 +#define MAX_PAYLOAD_SIZE 1024 const BLEAdvertisedDevice* theDevice = NULL; BLEClient* theClient = NULL; @@ -17,6 +13,15 @@ BLEL2CAPChannel* theChannel = NULL; size_t bytesSent = 0; size_t bytesReceived = 0; +size_t currentPayloadSize = INITIAL_PAYLOAD_SIZE; +uint32_t blocksSent = 0; +uint64_t startTime = 0; + +// Heap monitoring +size_t initialHeap = 0; +size_t lastHeap = 0; +size_t heapDecreaseCount = 0; +const size_t HEAP_LEAK_THRESHOLD = 10; // Warn after 10 consecutive decreases class L2CAPChannelCallbacks: public BLEL2CAPChannelCallbacks { @@ -43,7 +48,7 @@ class MyClientCallbacks: public BLEClientCallbacks { printf("GAP connected\n"); pClient->setDataLen(251); - theChannel = BLEL2CAPChannel::connect(pClient, L2CAP_CHANNEL, L2CAP_MTU, new L2CAPChannelCallbacks()); + theChannel = BLEL2CAPChannel::connect(pClient, L2CAP_PSM, L2CAP_MTU, new L2CAPChannelCallbacks()); } void onDisconnect(BLEClient* pClient, int reason) { @@ -61,17 +66,62 @@ class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { if (theDevice) { return; } printf("BLE Advertised Device found: %s\n", advertisedDevice->toString().c_str()); - if (!advertisedDevice->haveServiceUUID()) { return; } - if (!advertisedDevice->isAdvertisingService(serviceUUID)) { return; } - - printf("Found the device we're interested in!\n"); - BLEDevice::getScan()->stop(); - - // Hand over the device to the other task - theDevice = advertisedDevice; + // Look for device named "l2cap" + if (advertisedDevice->haveName() && advertisedDevice->getName() == "l2cap") { + printf("Found l2cap device!\n"); + BLEDevice::getScan()->stop(); + theDevice = advertisedDevice; + } } }; +void statusTask(void *pvParameters) { + while (true) { + vTaskDelay(1000 / portTICK_PERIOD_MS); + + if (startTime > 0 && blocksSent > 0) { + uint64_t currentTime = esp_timer_get_time(); + double elapsedSeconds = (currentTime - startTime) / 1000000.0; + double bytesPerSecond = bytesSent / elapsedSeconds; + double kbPerSecond = bytesPerSecond / 1024.0; + + // Heap monitoring + size_t currentHeap = esp_get_free_heap_size(); + size_t minHeap = esp_get_minimum_free_heap_size(); + + // Track heap for leak detection + if (initialHeap == 0) { + initialHeap = currentHeap; + lastHeap = currentHeap; + } + + // Check for consistent heap decrease + if (currentHeap < lastHeap) { + heapDecreaseCount++; + if (heapDecreaseCount >= HEAP_LEAK_THRESHOLD) { + printf("\n⚠️ WARNING: POSSIBLE MEMORY LEAK DETECTED! ⚠️\n"); + printf("Heap has decreased %zu times in a row\n", heapDecreaseCount); + printf("Initial heap: %zu, Current heap: %zu, Lost: %zu bytes\n", + initialHeap, currentHeap, initialHeap - currentHeap); + } + } else if (currentHeap >= lastHeap) { + heapDecreaseCount = 0; // Reset counter if heap stabilizes or increases + } + lastHeap = currentHeap; + + printf("\n=== STATUS UPDATE ===\n"); + printf("Blocks sent: %lu\n", (unsigned long)blocksSent); + printf("Total bytes sent: %zu\n", bytesSent); + printf("Current payload size: %zu bytes\n", currentPayloadSize); + printf("Elapsed time: %.1f seconds\n", elapsedSeconds); + printf("Bandwidth: %.2f KB/s (%.2f Mbps)\n", kbPerSecond, (bytesPerSecond * 8) / 1000000.0); + printf("Heap: %zu free (min: %zu), Used since start: %zu\n", + currentHeap, minHeap, initialHeap > 0 ? initialHeap - currentHeap : 0); + printf("==================\n\n"); + } + } +} + void connectTask(void *pvParameters) { uint8_t sequenceNumber = 0; @@ -112,22 +162,58 @@ void connectTask(void *pvParameters) { } while (theChannel->isConnected()) { - - /* - static auto initialDelay = true; - if (initialDelay) { - printf("Waiting gracefully 3 seconds before sending data\n"); - vTaskDelay(3000 / portTICK_PERIOD_MS); - initialDelay = false; - }; -*/ - std::vector data(5000, sequenceNumber++); - if (theChannel->write(data)) { - bytesSent += data.size(); + // Create framed packet: [seqno 8bit] [16bit payload length] [payload] + std::vector packet; + packet.reserve(3 + currentPayloadSize); + + // Add sequence number (8 bits) + packet.push_back(sequenceNumber); + + // Add payload length (16 bits, big endian - network byte order) + uint16_t payloadLen = currentPayloadSize; + packet.push_back((payloadLen >> 8) & 0xFF); // High byte first + packet.push_back(payloadLen & 0xFF); // Low byte second + + // Add payload + for (size_t i = 0; i < currentPayloadSize; i++) { + packet.push_back(i & 0xFF); + } + + if (theChannel->write(packet)) { + if (startTime == 0) { + startTime = esp_timer_get_time(); + } + bytesSent += packet.size(); + blocksSent++; + + // Print every block since we're sending slowly now + printf("Sent block %lu (seq=%d, payload=%zu bytes, frame_size=%zu)\n", + (unsigned long)blocksSent, sequenceNumber, currentPayloadSize, packet.size()); + + sequenceNumber++; + + // After every 50 blocks, double payload size + if (blocksSent % BLOCKS_BEFORE_DOUBLE == 0) { + size_t newSize = currentPayloadSize * 2; + + // Cap at maximum safe payload size + if (newSize > MAX_PAYLOAD_SIZE) { + if (currentPayloadSize < MAX_PAYLOAD_SIZE) { + currentPayloadSize = MAX_PAYLOAD_SIZE; + printf("\n=== Reached maximum payload size of %zu bytes after %lu blocks ===\n", currentPayloadSize, (unsigned long)blocksSent); + } + // Already at max, don't increase further + } else { + currentPayloadSize = newSize; + printf("\n=== Doubling payload size to %zu bytes after %lu blocks ===\n", currentPayloadSize, (unsigned long)blocksSent); + } + } } else { printf("failed to send!\n"); abort(); } + + // No delay - send as fast as possible } vTaskDelay(1000 / portTICK_PERIOD_MS); @@ -139,6 +225,7 @@ void app_main(void) { printf("Starting L2CAP client example\n"); xTaskCreate(connectTask, "connectTask", 5000, NULL, 1, NULL); + xTaskCreate(statusTask, "statusTask", 3000, NULL, 1, NULL); BLEDevice::init("L2CAP-Client"); BLEDevice::setMTU(BLE_ATT_MTU_MAX); @@ -151,15 +238,8 @@ void app_main(void) { scan->setActiveScan(true); scan->start(25 * 1000, false); - int numberOfSeconds = 0; - - while (bytesSent == 0) { - vTaskDelay(10 / portTICK_PERIOD_MS); - } - + // Main task just waits while (true) { vTaskDelay(1000 / portTICK_PERIOD_MS); - int bytesSentPerSeconds = bytesSent / ++numberOfSeconds; - printf("Bandwidth: %d b/sec = %d KB/sec\n", bytesSentPerSeconds, bytesSentPerSeconds / 1024); } } diff --git a/examples/L2CAP/L2CAP_Server/main/main.cpp b/examples/L2CAP/L2CAP_Server/main/main.cpp index 47639072..fb9cb5ec 100644 --- a/examples/L2CAP/L2CAP_Server/main/main.cpp +++ b/examples/L2CAP/L2CAP_Server/main/main.cpp @@ -1,13 +1,15 @@ #include +#include -// See the following for generating UUIDs: -// https://www.uuidgenerator.net/ - -#define SERVICE_UUID "dcbc7255-1e9e-49a0-a360-b0430b6c6905" -#define CHARACTERISTIC_UUID "371a55c8-f251-4ad2-90b3-c7c195b049be" -#define L2CAP_CHANNEL 150 +#define L2CAP_PSM 192 #define L2CAP_MTU 5000 +// Heap monitoring +size_t initialHeap = 0; +size_t lastHeap = 0; +size_t heapDecreaseCount = 0; +const size_t HEAP_LEAK_THRESHOLD = 10; // Warn after 10 consecutive decreases + class GATTCallbacks: public BLEServerCallbacks { public: @@ -23,29 +25,79 @@ class L2CAPChannelCallbacks: public BLEL2CAPChannelCallbacks { public: bool connected = false; - size_t numberOfReceivedBytes; - uint8_t nextSequenceNumber; + size_t totalBytesReceived = 0; + size_t totalFramesReceived = 0; + size_t totalPayloadBytes = 0; + uint8_t expectedSequenceNumber = 0; + size_t sequenceErrors = 0; + size_t frameErrors = 0; + uint64_t startTime = 0; + std::vector buffer; // Buffer for incomplete frames public: void onConnect(NimBLEL2CAPChannel* channel) { - printf("L2CAP connection established\n"); + printf("L2CAP connection established on PSM %d\n", L2CAP_PSM); connected = true; - numberOfReceivedBytes = nextSequenceNumber = 0; + totalBytesReceived = 0; + totalFramesReceived = 0; + totalPayloadBytes = 0; + expectedSequenceNumber = 0; + sequenceErrors = 0; + frameErrors = 0; + startTime = esp_timer_get_time(); + buffer.clear(); } void onRead(NimBLEL2CAPChannel* channel, std::vector& data) { - numberOfReceivedBytes += data.size(); - size_t sequenceNumber = data[0]; - printf("L2CAP read %d bytes w/ sequence number %d", data.size(), sequenceNumber); - if (sequenceNumber != nextSequenceNumber) { - printf("(wrong sequence number %d, expected %d)\n", sequenceNumber, nextSequenceNumber); - } else { - printf("\n"); - nextSequenceNumber++; + // Append new data to buffer + buffer.insert(buffer.end(), data.begin(), data.end()); + totalBytesReceived += data.size(); + + // Process complete frames from buffer + while (buffer.size() >= 3) { // Minimum frame size: seqno(1) + len(2) + // Parse frame header + uint8_t seqno = buffer[0]; + uint16_t payloadLen = (buffer[1] << 8) | buffer[2]; // Big-endian + + size_t frameSize = 3 + payloadLen; + + // Check if we have complete frame + if (buffer.size() < frameSize) { + break; // Wait for more data + } + + // Validate and process frame + totalFramesReceived++; + totalPayloadBytes += payloadLen; + + // Check sequence number + if (seqno != expectedSequenceNumber) { + sequenceErrors++; + printf("Frame %lu: Sequence error - got %d, expected %d (payload=%d bytes)\n", + totalFramesReceived, seqno, expectedSequenceNumber, payloadLen); + } + + // Update expected sequence number (wraps at 256) + expectedSequenceNumber = (seqno + 1) & 0xFF; + + // Remove processed frame from buffer + buffer.erase(buffer.begin(), buffer.begin() + frameSize); + + // Print progress every 100 frames + if (totalFramesReceived % 100 == 0) { + printf("Received %lu frames (%lu payload bytes)\n", totalFramesReceived, totalPayloadBytes); + } } } + void onDisconnect(NimBLEL2CAPChannel* channel) { - printf("L2CAP disconnected\n"); + printf("\nL2CAP disconnected\n"); + printf("Final statistics:\n"); + printf(" Total frames: %lu\n", totalFramesReceived); + printf(" Total bytes: %lu\n", totalBytesReceived); + printf(" Payload bytes: %lu\n", totalPayloadBytes); + printf(" Sequence errors: %lu\n", sequenceErrors); + printf(" Frame errors: %lu\n", frameErrors); connected = false; } }; @@ -54,37 +106,68 @@ extern "C" void app_main(void) { printf("Starting L2CAP server example [%lu free] [%lu min]\n", esp_get_free_heap_size(), esp_get_minimum_free_heap_size()); - BLEDevice::init("L2CAP-Server"); + BLEDevice::init("l2cap"); // Match the name the client is looking for BLEDevice::setMTU(BLE_ATT_MTU_MAX); auto cocServer = BLEDevice::createL2CAPServer(); auto l2capChannelCallbacks = new L2CAPChannelCallbacks(); - auto channel = cocServer->createService(L2CAP_CHANNEL, L2CAP_MTU, l2capChannelCallbacks); + auto channel = cocServer->createService(L2CAP_PSM, L2CAP_MTU, l2capChannelCallbacks); auto server = BLEDevice::createServer(); server->setCallbacks(new GATTCallbacks()); - auto service = server->createService(SERVICE_UUID); - auto characteristic = service->createCharacteristic(CHARACTERISTIC_UUID, NIMBLE_PROPERTY::READ); - characteristic->setValue(L2CAP_CHANNEL); - service->start(); + auto advertising = BLEDevice::getAdvertising(); - advertising->addServiceUUID(SERVICE_UUID); - advertising->enableScanResponse(true); + advertising->setScanResponse(true); // Important for name visibility BLEDevice::startAdvertising(); printf("Server waiting for connection requests [%lu free] [%lu min]\n", esp_get_free_heap_size(), esp_get_minimum_free_heap_size()); - // Wait until transfer actually starts... - while (!l2capChannelCallbacks->numberOfReceivedBytes) { - vTaskDelay(10 / portTICK_PERIOD_MS); - } - printf("\n\n\n"); - int numberOfSeconds = 0; - + // Status reporting loop while (true) { vTaskDelay(1000 / portTICK_PERIOD_MS); - if (!l2capChannelCallbacks->connected) { continue; } - int bps = l2capChannelCallbacks->numberOfReceivedBytes / ++numberOfSeconds; - printf("Bandwidth: %d b/sec = %d KB/sec [%lu free] [%lu min]\n", bps, bps / 1024, esp_get_free_heap_size(), esp_get_minimum_free_heap_size()); + + if (l2capChannelCallbacks->connected && l2capChannelCallbacks->totalBytesReceived > 0) { + uint64_t currentTime = esp_timer_get_time(); + double elapsedSeconds = (currentTime - l2capChannelCallbacks->startTime) / 1000000.0; + + if (elapsedSeconds > 0) { + double bytesPerSecond = l2capChannelCallbacks->totalBytesReceived / elapsedSeconds; + double framesPerSecond = l2capChannelCallbacks->totalFramesReceived / elapsedSeconds; + + // Heap monitoring + size_t currentHeap = esp_get_free_heap_size(); + size_t minHeap = esp_get_minimum_free_heap_size(); + + // Track heap for leak detection + if (initialHeap == 0) { + initialHeap = currentHeap; + lastHeap = currentHeap; + } + + // Check for consistent heap decrease + if (currentHeap < lastHeap) { + heapDecreaseCount++; + if (heapDecreaseCount >= HEAP_LEAK_THRESHOLD) { + printf("\n⚠️ WARNING: POSSIBLE MEMORY LEAK DETECTED! ⚠️\n"); + printf("Heap has decreased %zu times in a row\n", heapDecreaseCount); + printf("Initial heap: %zu, Current heap: %zu, Lost: %zu bytes\n", + initialHeap, currentHeap, initialHeap - currentHeap); + } + } else if (currentHeap >= lastHeap) { + heapDecreaseCount = 0; // Reset counter if heap stabilizes or increases + } + lastHeap = currentHeap; + + printf("\n=== STATUS UPDATE ===\n"); + printf("Frames received: %lu (%.1f fps)\n", l2capChannelCallbacks->totalFramesReceived, framesPerSecond); + printf("Total bytes: %lu\n", l2capChannelCallbacks->totalBytesReceived); + printf("Payload bytes: %lu\n", l2capChannelCallbacks->totalPayloadBytes); + printf("Bandwidth: %.2f KB/s (%.2f Mbps)\n", bytesPerSecond / 1024.0, (bytesPerSecond * 8) / 1000000.0); + printf("Sequence errors: %lu\n", l2capChannelCallbacks->sequenceErrors); + printf("Heap: %zu free (min: %zu), Used since start: %zu\n", + currentHeap, minHeap, initialHeap > 0 ? initialHeap - currentHeap : 0); + printf("==================\n"); + } + } } } From f3559e4fdc64a069068e385c46e86d730ce03823 Mon Sep 17 00:00:00 2001 From: "Dr. Michael Lauer" Date: Mon, 24 Nov 2025 17:14:30 +0100 Subject: [PATCH 2/6] examples/L2CAP_Client: raise the maximum payload and depend on esp-hpl for minimal-impact-debug-logs --- examples/L2CAP/L2CAP_Client/main/idf_component.yml | 2 ++ examples/L2CAP/L2CAP_Client/main/main.cpp | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/L2CAP/L2CAP_Client/main/idf_component.yml b/examples/L2CAP/L2CAP_Client/main/idf_component.yml index 66f47cad..8fd7c7e1 100644 --- a/examples/L2CAP/L2CAP_Client/main/idf_component.yml +++ b/examples/L2CAP/L2CAP_Client/main/idf_component.yml @@ -1,3 +1,5 @@ dependencies: local/esp-nimble-cpp: path: ../../../../../esp-nimble-cpp/ + mickeyl/esp-hpl: + git: https://github.com/mickeyl/esp-hpl.git diff --git a/examples/L2CAP/L2CAP_Client/main/main.cpp b/examples/L2CAP/L2CAP_Client/main/main.cpp index d15f5b76..d842ac4a 100644 --- a/examples/L2CAP/L2CAP_Client/main/main.cpp +++ b/examples/L2CAP/L2CAP_Client/main/main.cpp @@ -1,11 +1,12 @@ #include +#include #include #define L2CAP_PSM 192 #define L2CAP_MTU 5000 #define INITIAL_PAYLOAD_SIZE 64 #define BLOCKS_BEFORE_DOUBLE 50 -#define MAX_PAYLOAD_SIZE 1024 +#define MAX_PAYLOAD_SIZE 4900 const BLEAdvertisedDevice* theDevice = NULL; BLEClient* theClient = NULL; @@ -222,6 +223,9 @@ void connectTask(void *pvParameters) { extern "C" void app_main(void) { + // Install high performance logging before any output + esp_hpl::HighPerformanceLogger::init(); + printf("Starting L2CAP client example\n"); xTaskCreate(connectTask, "connectTask", 5000, NULL, 1, NULL); From e7fee6fda46dd87804d026e8e32638a52e3ab623 Mon Sep 17 00:00:00 2001 From: "Dr. Michael Lauer" Date: Mon, 24 Nov 2025 17:15:20 +0100 Subject: [PATCH 3/6] Fix CoC tx mbuf ownership on transient send errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Handle ENOMEM/EAGAIN from `ble_l2cap_send` as “mbuf consumed” and only free tx buffers on EBUSY; prevents double-free pool corruption during CoC stress. ESP-IDF NimBLE host change needed separately: In components/bt/host/nimble/nimble/nimble/host/src/ble_l2cap_coc.c, when an SDU completes (RX path), clear rx->sdus[current_sdu_idx] = NULL before advancing the index so cleanup won’t free a buffer already handed to the application. --- src/NimBLEL2CAPChannel.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/NimBLEL2CAPChannel.cpp b/src/NimBLEL2CAPChannel.cpp index 4a155860..a6db8600 100644 --- a/src/NimBLEL2CAPChannel.cpp +++ b/src/NimBLEL2CAPChannel.cpp @@ -134,8 +134,14 @@ int NimBLEL2CAPChannel::writeFragment(std::vector::const_iterator begin case BLE_HS_ENOMEM: case BLE_HS_EAGAIN: + /* ble_l2cap_send already consumed and freed txd on these errors */ + NIMBLE_LOGD(LOG_TAG, "ble_l2cap_send returned %d (consumed buffer). Retrying shortly...", res); + ble_npl_time_delay(ble_npl_time_ms_to_ticks32(RetryTimeout)); + continue; + case BLE_HS_EBUSY: - NIMBLE_LOGD(LOG_TAG, "ble_l2cap_send returned %d. Retrying shortly...", res); + /* Channel busy; txd not consumed */ + NIMBLE_LOGD(LOG_TAG, "ble_l2cap_send returned %d (busy). Retrying shortly...", res); os_mbuf_free_chain(txd); ble_npl_time_delay(ble_npl_time_ms_to_ticks32(RetryTimeout)); continue; From 181c72552a3bf66f63b613c88b086423115a77dd Mon Sep 17 00:00:00 2001 From: "Dr. Michael Lauer" Date: Mon, 24 Nov 2025 17:47:34 +0100 Subject: [PATCH 4/6] examples/L2CAP_Server: depend on esp-hpl and fix compiling --- .../L2CAP/L2CAP_Server/main/idf_component.yml | 3 + examples/L2CAP/L2CAP_Server/main/main.cpp | 55 +++++++++++++++---- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/examples/L2CAP/L2CAP_Server/main/idf_component.yml b/examples/L2CAP/L2CAP_Server/main/idf_component.yml index 66f47cad..0c454b0f 100644 --- a/examples/L2CAP/L2CAP_Server/main/idf_component.yml +++ b/examples/L2CAP/L2CAP_Server/main/idf_component.yml @@ -1,3 +1,6 @@ dependencies: local/esp-nimble-cpp: path: ../../../../../esp-nimble-cpp/ + mickeyl/esp-hpl: + git: https://github.com/mickeyl/esp-hpl.git + version: "master" diff --git a/examples/L2CAP/L2CAP_Server/main/main.cpp b/examples/L2CAP/L2CAP_Server/main/main.cpp index fb9cb5ec..af9711fb 100644 --- a/examples/L2CAP/L2CAP_Server/main/main.cpp +++ b/examples/L2CAP/L2CAP_Server/main/main.cpp @@ -1,4 +1,5 @@ #include +#include #include #define L2CAP_PSM 192 @@ -52,6 +53,9 @@ class L2CAPChannelCallbacks: public BLEL2CAPChannelCallbacks { // Append new data to buffer buffer.insert(buffer.end(), data.begin(), data.end()); totalBytesReceived += data.size(); + if (startTime == 0) { + startTime = esp_timer_get_time(); // start measuring once data flows + } // Process complete frames from buffer while (buffer.size() >= 3) { // Minimum frame size: seqno(1) + len(2) @@ -73,7 +77,7 @@ class L2CAPChannelCallbacks: public BLEL2CAPChannelCallbacks { // Check sequence number if (seqno != expectedSequenceNumber) { sequenceErrors++; - printf("Frame %lu: Sequence error - got %d, expected %d (payload=%d bytes)\n", + printf("Frame %zu: Sequence error - got %d, expected %d (payload=%d bytes)\n", totalFramesReceived, seqno, expectedSequenceNumber, payloadLen); } @@ -85,25 +89,49 @@ class L2CAPChannelCallbacks: public BLEL2CAPChannelCallbacks { // Print progress every 100 frames if (totalFramesReceived % 100 == 0) { - printf("Received %lu frames (%lu payload bytes)\n", totalFramesReceived, totalPayloadBytes); + double elapsedSeconds = (esp_timer_get_time() - startTime) / 1000000.0; + double bytesPerSecond = elapsedSeconds > 0 ? totalBytesReceived / elapsedSeconds : 0.0; + printf("Received %zu frames (%zu payload bytes) - Bandwidth: %.2f KB/s (%.2f Mbps)\n", + totalFramesReceived, totalPayloadBytes, + bytesPerSecond / 1024.0, (bytesPerSecond * 8) / 1000000.0); } } } void onDisconnect(NimBLEL2CAPChannel* channel) { printf("\nL2CAP disconnected\n"); + double elapsedSeconds = startTime > 0 ? (esp_timer_get_time() - startTime) / 1000000.0 : 0.0; + double bytesPerSecond = elapsedSeconds > 0 ? totalBytesReceived / elapsedSeconds : 0.0; + printf("Final statistics:\n"); - printf(" Total frames: %lu\n", totalFramesReceived); - printf(" Total bytes: %lu\n", totalBytesReceived); - printf(" Payload bytes: %lu\n", totalPayloadBytes); - printf(" Sequence errors: %lu\n", sequenceErrors); - printf(" Frame errors: %lu\n", frameErrors); + printf(" Total frames: %zu\n", totalFramesReceived); + printf(" Total bytes: %zu\n", totalBytesReceived); + printf(" Payload bytes: %zu\n", totalPayloadBytes); + printf(" Sequence errors: %zu\n", sequenceErrors); + printf(" Frame errors: %zu\n", frameErrors); + printf(" Bandwidth: %.2f KB/s (%.2f Mbps)\n", bytesPerSecond / 1024.0, (bytesPerSecond * 8) / 1000000.0); + + // Reset state for the next connection + buffer.clear(); + totalBytesReceived = 0; + totalFramesReceived = 0; + totalPayloadBytes = 0; + expectedSequenceNumber = 0; + sequenceErrors = 0; + frameErrors = 0; + startTime = 0; connected = false; + + // Restart advertising so another client can connect + BLEDevice::startAdvertising(); } }; extern "C" void app_main(void) { + // Install high performance logging before any other output + esp_hpl::HighPerformanceLogger::init(); + printf("Starting L2CAP server example [%lu free] [%lu min]\n", esp_get_free_heap_size(), esp_get_minimum_free_heap_size()); BLEDevice::init("l2cap"); // Match the name the client is looking for @@ -112,12 +140,15 @@ void app_main(void) { auto cocServer = BLEDevice::createL2CAPServer(); auto l2capChannelCallbacks = new L2CAPChannelCallbacks(); auto channel = cocServer->createService(L2CAP_PSM, L2CAP_MTU, l2capChannelCallbacks); + (void)channel; // prevent unused warning auto server = BLEDevice::createServer(); server->setCallbacks(new GATTCallbacks()); auto advertising = BLEDevice::getAdvertising(); - advertising->setScanResponse(true); // Important for name visibility + NimBLEAdvertisementData scanData; + scanData.setName("l2cap"); + advertising->setScanResponseData(scanData); BLEDevice::startAdvertising(); printf("Server waiting for connection requests [%lu free] [%lu min]\n", esp_get_free_heap_size(), esp_get_minimum_free_heap_size()); @@ -159,11 +190,11 @@ void app_main(void) { lastHeap = currentHeap; printf("\n=== STATUS UPDATE ===\n"); - printf("Frames received: %lu (%.1f fps)\n", l2capChannelCallbacks->totalFramesReceived, framesPerSecond); - printf("Total bytes: %lu\n", l2capChannelCallbacks->totalBytesReceived); - printf("Payload bytes: %lu\n", l2capChannelCallbacks->totalPayloadBytes); + printf("Frames received: %zu (%.1f fps)\n", l2capChannelCallbacks->totalFramesReceived, framesPerSecond); + printf("Total bytes: %zu\n", l2capChannelCallbacks->totalBytesReceived); + printf("Payload bytes: %zu\n", l2capChannelCallbacks->totalPayloadBytes); printf("Bandwidth: %.2f KB/s (%.2f Mbps)\n", bytesPerSecond / 1024.0, (bytesPerSecond * 8) / 1000000.0); - printf("Sequence errors: %lu\n", l2capChannelCallbacks->sequenceErrors); + printf("Sequence errors: %zu\n", l2capChannelCallbacks->sequenceErrors); printf("Heap: %zu free (min: %zu), Used since start: %zu\n", currentHeap, minHeap, initialHeap > 0 ? initialHeap - currentHeap : 0); printf("==================\n"); From 36e59d649ce0bc57401191c9f02ed8a81c8404fa Mon Sep 17 00:00:00 2001 From: "Dr. Michael Lauer" Date: Tue, 13 Jan 2026 11:05:51 +0100 Subject: [PATCH 5/6] feat(L2CAP): Add disconnect as per https://github.com/h2zero/esp-nimble-cpp/issues/391 --- src/NimBLEL2CAPChannel.cpp | 19 +++++++++++++++++++ src/NimBLEL2CAPChannel.h | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/src/NimBLEL2CAPChannel.cpp b/src/NimBLEL2CAPChannel.cpp index a6db8600..a1631e7e 100644 --- a/src/NimBLEL2CAPChannel.cpp +++ b/src/NimBLEL2CAPChannel.cpp @@ -203,6 +203,25 @@ bool NimBLEL2CAPChannel::write(const std::vector& bytes) { return true; } +bool NimBLEL2CAPChannel::disconnect() { + if (!this->channel) { + NIMBLE_LOGW(LOG_TAG, "L2CAP Channel not open"); + return false; + } + + int rc = ble_l2cap_disconnect(this->channel); + if (rc != 0 && rc != BLE_HS_ENOTCONN && rc != BLE_HS_EALREADY) { + NIMBLE_LOGE(LOG_TAG, "ble_l2cap_disconnect failed: rc=%d %s", rc, NimBLEUtils::returnCodeToString(rc)); + return false; + } + + return true; +} + +uint16_t NimBLEL2CAPChannel::getConnHandle() const { + return ble_l2cap_get_conn_handle(this->channel); +} + // private int NimBLEL2CAPChannel::handleConnectionEvent(struct ble_l2cap_event* event) { channel = event->connect.chan; diff --git a/src/NimBLEL2CAPChannel.h b/src/NimBLEL2CAPChannel.h index b7fafa4a..b0a8be3e 100644 --- a/src/NimBLEL2CAPChannel.h +++ b/src/NimBLEL2CAPChannel.h @@ -56,6 +56,14 @@ class NimBLEL2CAPChannel { /// NOTE: This function will block until the data has been sent or an error occurred. bool write(const std::vector& bytes); + /// @brief Disconnect this L2CAP channel. + /// @return true on success, false on failure. + bool disconnect(); + + /// @brief Get the connection handle associated with this channel. + /// @return Connection handle, or BLE_HS_CONN_HANDLE_NONE if not connected. + uint16_t getConnHandle() const; + /// @return True, if the channel is connected. False, otherwise. bool isConnected() const { return !!channel; } From 27ff34b30526553a63fbe990d7f645724512989b Mon Sep 17 00:00:00 2001 From: "Dr. Michael Lauer" Date: Mon, 2 Mar 2026 10:50:23 +0100 Subject: [PATCH 6/6] examples/L2CAP: pin esp-hpl tag and clean whitespace --- .../L2CAP/L2CAP_Client/main/idf_component.yml | 1 + examples/L2CAP/L2CAP_Client/main/main.cpp | 40 +++++++++---------- .../L2CAP/L2CAP_Server/main/idf_component.yml | 2 +- examples/L2CAP/L2CAP_Server/main/main.cpp | 40 +++++++++---------- 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/examples/L2CAP/L2CAP_Client/main/idf_component.yml b/examples/L2CAP/L2CAP_Client/main/idf_component.yml index 8fd7c7e1..383b7f31 100644 --- a/examples/L2CAP/L2CAP_Client/main/idf_component.yml +++ b/examples/L2CAP/L2CAP_Client/main/idf_component.yml @@ -3,3 +3,4 @@ dependencies: path: ../../../../../esp-nimble-cpp/ mickeyl/esp-hpl: git: https://github.com/mickeyl/esp-hpl.git + version: "1.1.0" diff --git a/examples/L2CAP/L2CAP_Client/main/main.cpp b/examples/L2CAP/L2CAP_Client/main/main.cpp index d842ac4a..574ab411 100644 --- a/examples/L2CAP/L2CAP_Client/main/main.cpp +++ b/examples/L2CAP/L2CAP_Client/main/main.cpp @@ -79,44 +79,44 @@ class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks { void statusTask(void *pvParameters) { while (true) { vTaskDelay(1000 / portTICK_PERIOD_MS); - + if (startTime > 0 && blocksSent > 0) { uint64_t currentTime = esp_timer_get_time(); double elapsedSeconds = (currentTime - startTime) / 1000000.0; double bytesPerSecond = bytesSent / elapsedSeconds; double kbPerSecond = bytesPerSecond / 1024.0; - + // Heap monitoring size_t currentHeap = esp_get_free_heap_size(); size_t minHeap = esp_get_minimum_free_heap_size(); - + // Track heap for leak detection if (initialHeap == 0) { initialHeap = currentHeap; lastHeap = currentHeap; } - + // Check for consistent heap decrease if (currentHeap < lastHeap) { heapDecreaseCount++; if (heapDecreaseCount >= HEAP_LEAK_THRESHOLD) { printf("\n⚠️ WARNING: POSSIBLE MEMORY LEAK DETECTED! ⚠️\n"); printf("Heap has decreased %zu times in a row\n", heapDecreaseCount); - printf("Initial heap: %zu, Current heap: %zu, Lost: %zu bytes\n", + printf("Initial heap: %zu, Current heap: %zu, Lost: %zu bytes\n", initialHeap, currentHeap, initialHeap - currentHeap); } } else if (currentHeap >= lastHeap) { heapDecreaseCount = 0; // Reset counter if heap stabilizes or increases } lastHeap = currentHeap; - + printf("\n=== STATUS UPDATE ===\n"); printf("Blocks sent: %lu\n", (unsigned long)blocksSent); printf("Total bytes sent: %zu\n", bytesSent); printf("Current payload size: %zu bytes\n", currentPayloadSize); printf("Elapsed time: %.1f seconds\n", elapsedSeconds); printf("Bandwidth: %.2f KB/s (%.2f Mbps)\n", kbPerSecond, (bytesPerSecond * 8) / 1000000.0); - printf("Heap: %zu free (min: %zu), Used since start: %zu\n", + printf("Heap: %zu free (min: %zu), Used since start: %zu\n", currentHeap, minHeap, initialHeap > 0 ? initialHeap - currentHeap : 0); printf("==================\n\n"); } @@ -128,7 +128,7 @@ void connectTask(void *pvParameters) { uint8_t sequenceNumber = 0; while (true) { - + if (!theDevice) { vTaskDelay(1000 / portTICK_PERIOD_MS); continue; @@ -147,7 +147,7 @@ void connectTask(void *pvParameters) { break; } vTaskDelay(2000 / portTICK_PERIOD_MS); - continue; + continue; } if (!theChannel) { @@ -166,37 +166,37 @@ void connectTask(void *pvParameters) { // Create framed packet: [seqno 8bit] [16bit payload length] [payload] std::vector packet; packet.reserve(3 + currentPayloadSize); - + // Add sequence number (8 bits) packet.push_back(sequenceNumber); - + // Add payload length (16 bits, big endian - network byte order) uint16_t payloadLen = currentPayloadSize; packet.push_back((payloadLen >> 8) & 0xFF); // High byte first packet.push_back(payloadLen & 0xFF); // Low byte second - + // Add payload for (size_t i = 0; i < currentPayloadSize; i++) { packet.push_back(i & 0xFF); } - + if (theChannel->write(packet)) { if (startTime == 0) { startTime = esp_timer_get_time(); } bytesSent += packet.size(); blocksSent++; - + // Print every block since we're sending slowly now - printf("Sent block %lu (seq=%d, payload=%zu bytes, frame_size=%zu)\n", + printf("Sent block %lu (seq=%d, payload=%zu bytes, frame_size=%zu)\n", (unsigned long)blocksSent, sequenceNumber, currentPayloadSize, packet.size()); - + sequenceNumber++; - + // After every 50 blocks, double payload size if (blocksSent % BLOCKS_BEFORE_DOUBLE == 0) { size_t newSize = currentPayloadSize * 2; - + // Cap at maximum safe payload size if (newSize > MAX_PAYLOAD_SIZE) { if (currentPayloadSize < MAX_PAYLOAD_SIZE) { @@ -211,9 +211,9 @@ void connectTask(void *pvParameters) { } } else { printf("failed to send!\n"); - abort(); + abort(); } - + // No delay - send as fast as possible } diff --git a/examples/L2CAP/L2CAP_Server/main/idf_component.yml b/examples/L2CAP/L2CAP_Server/main/idf_component.yml index 0c454b0f..383b7f31 100644 --- a/examples/L2CAP/L2CAP_Server/main/idf_component.yml +++ b/examples/L2CAP/L2CAP_Server/main/idf_component.yml @@ -3,4 +3,4 @@ dependencies: path: ../../../../../esp-nimble-cpp/ mickeyl/esp-hpl: git: https://github.com/mickeyl/esp-hpl.git - version: "master" + version: "1.1.0" diff --git a/examples/L2CAP/L2CAP_Server/main/main.cpp b/examples/L2CAP/L2CAP_Server/main/main.cpp index af9711fb..9d161536 100644 --- a/examples/L2CAP/L2CAP_Server/main/main.cpp +++ b/examples/L2CAP/L2CAP_Server/main/main.cpp @@ -56,37 +56,37 @@ class L2CAPChannelCallbacks: public BLEL2CAPChannelCallbacks { if (startTime == 0) { startTime = esp_timer_get_time(); // start measuring once data flows } - + // Process complete frames from buffer while (buffer.size() >= 3) { // Minimum frame size: seqno(1) + len(2) // Parse frame header uint8_t seqno = buffer[0]; uint16_t payloadLen = (buffer[1] << 8) | buffer[2]; // Big-endian - + size_t frameSize = 3 + payloadLen; - + // Check if we have complete frame if (buffer.size() < frameSize) { break; // Wait for more data } - + // Validate and process frame totalFramesReceived++; totalPayloadBytes += payloadLen; - + // Check sequence number if (seqno != expectedSequenceNumber) { sequenceErrors++; - printf("Frame %zu: Sequence error - got %d, expected %d (payload=%d bytes)\n", + printf("Frame %zu: Sequence error - got %d, expected %d (payload=%d bytes)\n", totalFramesReceived, seqno, expectedSequenceNumber, payloadLen); } - + // Update expected sequence number (wraps at 256) expectedSequenceNumber = (seqno + 1) & 0xFF; - + // Remove processed frame from buffer buffer.erase(buffer.begin(), buffer.begin() + frameSize); - + // Print progress every 100 frames if (totalFramesReceived % 100 == 0) { double elapsedSeconds = (esp_timer_get_time() - startTime) / 1000000.0; @@ -97,7 +97,7 @@ class L2CAPChannelCallbacks: public BLEL2CAPChannelCallbacks { } } } - + void onDisconnect(NimBLEL2CAPChannel* channel) { printf("\nL2CAP disconnected\n"); double elapsedSeconds = startTime > 0 ? (esp_timer_get_time() - startTime) / 1000000.0 : 0.0; @@ -141,10 +141,10 @@ void app_main(void) { auto l2capChannelCallbacks = new L2CAPChannelCallbacks(); auto channel = cocServer->createService(L2CAP_PSM, L2CAP_MTU, l2capChannelCallbacks); (void)channel; // prevent unused warning - + auto server = BLEDevice::createServer(); server->setCallbacks(new GATTCallbacks()); - + auto advertising = BLEDevice::getAdvertising(); NimBLEAdvertisementData scanData; scanData.setName("l2cap"); @@ -156,46 +156,46 @@ void app_main(void) { // Status reporting loop while (true) { vTaskDelay(1000 / portTICK_PERIOD_MS); - + if (l2capChannelCallbacks->connected && l2capChannelCallbacks->totalBytesReceived > 0) { uint64_t currentTime = esp_timer_get_time(); double elapsedSeconds = (currentTime - l2capChannelCallbacks->startTime) / 1000000.0; - + if (elapsedSeconds > 0) { double bytesPerSecond = l2capChannelCallbacks->totalBytesReceived / elapsedSeconds; double framesPerSecond = l2capChannelCallbacks->totalFramesReceived / elapsedSeconds; - + // Heap monitoring size_t currentHeap = esp_get_free_heap_size(); size_t minHeap = esp_get_minimum_free_heap_size(); - + // Track heap for leak detection if (initialHeap == 0) { initialHeap = currentHeap; lastHeap = currentHeap; } - + // Check for consistent heap decrease if (currentHeap < lastHeap) { heapDecreaseCount++; if (heapDecreaseCount >= HEAP_LEAK_THRESHOLD) { printf("\n⚠️ WARNING: POSSIBLE MEMORY LEAK DETECTED! ⚠️\n"); printf("Heap has decreased %zu times in a row\n", heapDecreaseCount); - printf("Initial heap: %zu, Current heap: %zu, Lost: %zu bytes\n", + printf("Initial heap: %zu, Current heap: %zu, Lost: %zu bytes\n", initialHeap, currentHeap, initialHeap - currentHeap); } } else if (currentHeap >= lastHeap) { heapDecreaseCount = 0; // Reset counter if heap stabilizes or increases } lastHeap = currentHeap; - + printf("\n=== STATUS UPDATE ===\n"); printf("Frames received: %zu (%.1f fps)\n", l2capChannelCallbacks->totalFramesReceived, framesPerSecond); printf("Total bytes: %zu\n", l2capChannelCallbacks->totalBytesReceived); printf("Payload bytes: %zu\n", l2capChannelCallbacks->totalPayloadBytes); printf("Bandwidth: %.2f KB/s (%.2f Mbps)\n", bytesPerSecond / 1024.0, (bytesPerSecond * 8) / 1000000.0); printf("Sequence errors: %zu\n", l2capChannelCallbacks->sequenceErrors); - printf("Heap: %zu free (min: %zu), Used since start: %zu\n", + printf("Heap: %zu free (min: %zu), Used since start: %zu\n", currentHeap, minHeap, initialHeap > 0 ? initialHeap - currentHeap : 0); printf("==================\n"); }