fix(nimble): Added l2cap_coc throughput examples

This commit is contained in:
Astha Verma
2026-04-06 11:39:55 +05:30
committed by Rahul Tank
parent a41a481fdc
commit 1265a63943
51 changed files with 1607 additions and 22 deletions

View File

@@ -466,14 +466,10 @@ examples/bluetooth/nimble/throughput_app:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
depends_components+:
- esp_driver_gpio
- esp_driver_uart
depends_filepatterns:
- examples/bluetooth/nimble/common/**/*
- examples/bluetooth/nimble/throughput_app/blecent_throughput/components/**/*
examples/bluetooth/nimble/throughput_app/blecent_throughput:
examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
@@ -482,4 +478,20 @@ examples/bluetooth/nimble/throughput_app/blecent_throughput:
- esp_driver_uart
depends_filepatterns:
- examples/bluetooth/nimble/common/**/*
- examples/bluetooth/nimble/throughput_app/blecent_throughput/components/**/*
- examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/**/*
examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
depends_filepatterns:
- examples/bluetooth/nimble/common/**/*
- examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/**/*
examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
depends_filepatterns:
- examples/bluetooth/nimble/common/**/*
- examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/**/*

View File

@@ -1,22 +1,38 @@
# Throughput Demo Examples
There are two example folders inside this `throughput_app`: `bleprph_throughput` (peripheral) and `blecent_throughput` (central). These examples demonstrate BLE GATT throughput measurement using NimBLE on ESP32. Two ESP32 boards are needed to run this demo. The `blecent_throughput` example has CLI support to select GATT operation from READ/WRITE/NOTIFY and configure connection parameters at runtime. More details can be found in respective READMEs.
This folder contains BLE throughput measurement examples for NimBLE on ESP32, organized into two sub-folders by protocol:
## Using the Examples
```
throughput_app/
├── gatt/
│ ├── blecent_throughput/ — GATT central (initiator)
│ └── bleprph_throughput/ — GATT peripheral (responder)
└── l2cap_coc/
├── l2cap_coc_cent/ — L2CAP CoC central (sender)
└── l2cap_coc_prph/ — L2CAP CoC peripheral (receiver)
```
---
## gatt/
There are two example folders inside `gatt/`: `bleprph_throughput` (peripheral) and `blecent_throughput` (central). These examples demonstrate BLE GATT throughput measurement using NimBLE on ESP32. Two ESP32 boards are needed to run this demo. The `blecent_throughput` example has CLI support to select GATT operation from READ/WRITE/NOTIFY and configure connection parameters at runtime. More details can be found in respective READMEs.
### Using the Examples
Build and flash two ESP32 boards with `bleprph_throughput` and `blecent_throughput` examples. The central automatically scans and connects to the peripheral based on device name (`nimble_prph`). After connection, the user may optionally configure connection parameters (`MTU`, `connection interval`, `latency`, `supervision timeout`, `connection event length`). Then the user specifies the throughput test type (`read`, `write` or `notify`) and test duration in seconds.
Below are sample throughput numbers for a 60-second test run (MTU = 512, conn itvl = 7.5ms, DLE = 251 bytes, 1M PHY):
|GATT Method | Measurement Time | Application Throughput|
|--- | --- | ---|
|NOTIFY | 60 seconds | ~340 Kbps|
|READ | 60 seconds | ~200 Kbps|
|WRITE | 60 seconds | ~500 Kbps|
|----------- | ---------------- | ----------------------|
|NOTIFY | 60 seconds | ~340 Kbps |
|READ | 60 seconds | ~200 Kbps |
|WRITE | 60 seconds | ~500 Kbps |
The notify throughput output is displayed on the `bleprph_throughput` console, while read/write throughput results are shown on the `blecent_throughput` console.
## Throughput Optimization
### Throughput Optimization
The following parameters have the most significant impact on throughput:
@@ -35,3 +51,29 @@ The following parameters have the most significant impact on throughput:
7. **MSYS Buffer Count**: Both peripheral and central are configured with 50 MSYS blocks (`CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=50`) to provide sufficient buffer space for high-throughput operations.
8. **PHY**: On BLE 5.0 supported chipsets, 2M PHY can be selected to double the air data rate. Use Extended Advertising mode and specify PHY in the throughput CLI command.
---
## l2cap_coc/
There are two example folders inside `l2cap_coc/`: `l2cap_coc_prph` (peripheral/receiver) and `l2cap_coc_cent` (central/sender). These examples demonstrate BLE L2CAP Connection-Oriented Channel (CoC) throughput measurement using NimBLE on ESP32. Two ESP32 boards are needed to run this demo. More details can be found in respective READMEs.
### How It Works
L2CAP CoC provides a direct channel between two devices without the ATT/GATT overhead, making it more efficient for bulk data transfer.
- The peripheral (`l2cap_coc_prph`) advertises with UUID 0x1812 and registers an L2CAP CoC server on PSM 0x1002. On connection it pre-grants receive credits to the central so the central can pipeline multiple SDUs immediately.
- The central (`l2cap_coc_cent`) scans for UUID 0x1812, connects, enables Data Length Extension (DLE), then opens an L2CAP CoC channel and continuously sends SDUs to the peripheral.
- Data flows **central → peripheral**. The central controls PHY selection, cycling through all enabled PHYs (1M, 2M, Coded S2, Coded S8) in sequence and printing a TX throughput summary after each test interval.
- The peripheral tracks RX throughput per PHY, printing a per-PHY summary box each time the central switches PHY, and a live per-second RX rate while data is flowing.
### Using the Examples
Build and flash two ESP32 boards with `l2cap_coc_prph` and `l2cap_coc_cent` examples. The central automatically scans and connects — no user input required. The test runs continuously, cycling through enabled PHYs.
Below are sample throughput numbers (MTU = 2048, DLE = 251 bytes, conn itvl = 7.5ms, ESP32-C6):
| PHY | Measurement Time | Application Throughput |
|-----|-----------------|------------------------|
| 1M | 8 seconds | ~741 kbps |
| 2M | 8 seconds | ~1310 kbps |

View File

@@ -5,9 +5,6 @@
# BT config (universal across all ESP32 variants)
#
CONFIG_BT_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
CONFIG_BTDM_CTRL_MODE_BTDM=n
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y

View File

@@ -5,9 +5,6 @@
# BT config (universal across all ESP32 variants)
#
CONFIG_BT_ENABLED=y
CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n
CONFIG_BTDM_CTRL_MODE_BTDM=n
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=y

View File

@@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(MINIMAL_BUILD ON)
project(l2cap_coc_cent)

View File

@@ -0,0 +1,94 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | --------- |
# L2CAP COC Throughput Central Example
`l2cap_coc_cent` demonstrates the central (initiator) side of an L2CAP Connection-Oriented Channel (COC) throughput test using NimBLE on ESP32. It passively scans for a peripheral advertising UUID 0x1812, establishes a GAP connection, enables Data Length Extension (DLE), then opens an L2CAP COC channel over PSM 0x1002 and continuously sends SDUs to measure TX throughput.
The central automatically cycles through all enabled PHYs (1M, 2M, Coded S2, Coded S8) in sequence, printing a throughput summary box after each test interval. It must be used together with the `l2cap_coc_prph` example which acts as the receiving side.
It uses ESP32's Bluetooth controller and NimBLE stack based BLE host.
## How to Use Example
Before project configuration and build, be sure to set the correct chip target using:
```bash
idf.py set-target <chip_name>
```
### Hardware Required
* Two development boards, one flashed with `l2cap_coc_cent` and the other with `l2cap_coc_prph`.
* A USB cable for power supply and programming.
See [Development Boards](https://www.espressif.com/en/products/devkits) for more information.
### Configure the Project
Open the project configuration menu:
```bash
idf.py menuconfig
```
In the `L2CAP COC Throughput Configuration` menu:
| Option | Default | Description |
|--------|---------|-------------|
| `EXAMPLE_L2CAP_COC_MTU` | 2048 | L2CAP CoC SDU MTU size in bytes (central receive buffer). Data flows cent → prph in this test, so throughput is governed by the peripheral's MTU. This value only limits how much the peripheral can send back and does not affect TX throughput. |
| `EXAMPLE_EXTENDED_ADV` | y (BLE 5.0 chips) | Enable extended scanning to find peripherals using extended advertising. Required for Coded PHY testing on ESP32-C6/H2. |
| `EXAMPLE_TEST_PHY_1M` | n | Enable throughput test on 1M PHY. |
| `EXAMPLE_TEST_PHY_2M` | y | Enable throughput test on 2M PHY (BLE 5.0 chips only). |
| `EXAMPLE_TEST_PHY_CODED_S2` | n | Enable throughput test on Coded PHY S2 (500 kbps, BLE 5.0 chips only). |
| `EXAMPLE_TEST_PHY_CODED_S8` | n | Enable throughput test on Coded PHY S8 (125 kbps, BLE 5.0 chips only). |
| `EXAMPLE_TEST_DURATION_1M` | 8 | Test duration in seconds for 1M PHY. |
| `EXAMPLE_TEST_DURATION_2M` | 8 | Test duration in seconds for 2M PHY. |
| `EXAMPLE_TEST_DURATION_CODED_S2` | 8 | Test duration in seconds for Coded S2 PHY. |
| `EXAMPLE_TEST_DURATION_CODED_S8` | 8 | Test duration in seconds for Coded S8 PHY. |
### Build and Flash
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects.
## Example Output
On successful connection and throughput test, the central prints a per-PHY summary box after each test interval, then loops back to the first enabled PHY continuously:
```
I (xxx) l2cap_coc_cent: BLE Host Task started
I (xxx) l2cap_coc_cent: Device Address: xx:xx:xx:xx:xx:xx
I (xxx) l2cap_coc_cent: Connecting to xx:xx:xx:xx:xx:xx (addr_type=0)
I (xxx) l2cap_coc_cent: Connected; handle=0 peer=xx:xx:xx:xx:xx:xx
I (xxx) l2cap_coc_cent: L2CAP COC connected, chan=0xxxxxxxxx
I (xxx) l2cap_coc_cent: L2CAP COC Throughput — TX side (central sends to peripheral)
I (xxx) l2cap_coc_cent: Number of enabled PHYs: x
I (xxx) l2cap_coc_cent: PHY updated: tx=2 rx=2 status=0
I (xxx) l2cap_coc_cent: [2M PHY] Sending for 8 s
I (xxx) l2cap_coc_cent: +-------------------------------------------------+
I (xxx) l2cap_coc_cent: | PHY : 2M |
I (xxx) l2cap_coc_cent: | TX : xxxx kbps |
I (xxx) l2cap_coc_cent: | Bytes : xxxxxxx |
I (xxx) l2cap_coc_cent: | Time : 8 s |
I (xxx) l2cap_coc_cent: +-------------------------------------------------+
I (xxx) l2cap_coc_cent: Cycle complete. Looping back to first PHY...
I (xxx) l2cap_coc_cent: PHY updated: tx=2 rx=2 status=0
I (xxx) l2cap_coc_cent: [2M PHY] Sending for 8 s
I (xxx) l2cap_coc_cent: +-------------------------------------------------+
I (xxx) l2cap_coc_cent: | PHY : 2M |
I (xxx) l2cap_coc_cent: | TX : xxxx kbps |
I (xxx) l2cap_coc_cent: | Bytes : xxxxxxx |
I (xxx) l2cap_coc_cent: | Time : 8 s |
I (xxx) l2cap_coc_cent: +-------------------------------------------------+
I (xxx) l2cap_coc_cent: Cycle complete. Looping back to first PHY...
```
> **Note:** The above output was captured on ESP32-H2 with only 2M PHY enabled. With additional PHYs enabled (1M, Coded S2, Coded S8), the central cycles through each in sequence before looping back.
## Troubleshooting
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "main.c"
PRIV_REQUIRES bt nvs_flash esp_timer
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,67 @@
menu "L2CAP COC Throughput Configuration"
config EXAMPLE_L2CAP_COC_MTU
int "L2CAP CoC MTU size in bytes"
default 2048
range 512 65511
help
L2CAP CoC SDU MTU size used by the central device.
Note: memory pool allocates 6 buffers of this size; total pool
memory = MTU x 6. On chips without PSRAM ensure sufficient heap
is available before increasing this value.
Use idf.py size-components to verify.
config EXAMPLE_EXTENDED_ADV
bool
depends on SOC_BLE_50_SUPPORTED && BT_NIMBLE_50_FEATURE_SUPPORT
default y if SOC_ESP_NIMBLE_CONTROLLER
select BT_NIMBLE_EXT_ADV
prompt "Enable Extended Scanning"
help
Use extended scanning on chips that support BLE 5.0
config EXAMPLE_TEST_PHY_1M
bool "Test on 1M PHY"
default y if !SOC_BLE_50_SUPPORTED
default n
config EXAMPLE_TEST_PHY_2M
bool "Test on 2M PHY"
default y
depends on SOC_BLE_50_SUPPORTED
config EXAMPLE_TEST_PHY_CODED_S2
bool "Test on Coded PHY S2"
default n
depends on SOC_BLE_50_SUPPORTED
config EXAMPLE_TEST_PHY_CODED_S8
bool "Test on Coded PHY S8"
default n
depends on SOC_BLE_50_SUPPORTED
config EXAMPLE_TEST_DURATION_1M
int "Test duration for 1M PHY (sec)"
default 8
range 1 3600
depends on EXAMPLE_TEST_PHY_1M
config EXAMPLE_TEST_DURATION_2M
int "Test duration for 2M PHY (sec)"
default 8
range 1 3600
depends on EXAMPLE_TEST_PHY_2M
config EXAMPLE_TEST_DURATION_CODED_S2
int "Test duration for Coded S2 PHY (sec)"
default 8
range 1 3600
depends on EXAMPLE_TEST_PHY_CODED_S2
config EXAMPLE_TEST_DURATION_CODED_S8
int "Test duration for Coded S8 PHY (sec)"
default 8
range 1 3600
depends on EXAMPLE_TEST_PHY_CODED_S8
endmenu

View File

@@ -0,0 +1,3 @@
dependencies:
nimble_central_utils:
path: ${IDF_PATH}/examples/bluetooth/nimble/common/nimble_central_utils

View File

@@ -0,0 +1,738 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
#include "host/ble_esp_gap.h"
static const char *TAG = "l2cap_coc_cent";
#define L2CAP_COC_PSM 0x1002
#define L2CAP_COC_MTU CONFIG_EXAMPLE_L2CAP_COC_MTU
#define COC_BUF_COUNT (6 * MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM))
/* Block size must include mbuf headers so each SDU fits in one pool entry. */
#define SDU_BLOCK_SIZE (L2CAP_COC_MTU + sizeof(struct os_mbuf_pkthdr) + sizeof(struct os_mbuf))
#define LL_PACKET_LENGTH 251
#define LL_PACKET_TIME 2120
#define L2CAP_COC_UUID 0x1812
/* EventGroup bits */
#define PHY_UPDATED_BIT (1 << 0)
#define COC_CONNECTED_BIT (1 << 1)
#define CONN_UPDATED_BIT (1 << 2)
#define TX_UNSTALLED_BIT (1 << 3)
/* Timeout / interval constants */
#define CONN_PARAM_UPDATE_TIMEOUT_MS 5000
#define PREDRAIN_TIMEOUT_MS 20000
#define POSTDRAIN_TIMEOUT_MS 5000
#define TX_YIELD_INTERVAL 50
static EventGroupHandle_t coc_event_group;
static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE;
static struct ble_l2cap_chan *coc_chan = NULL;
static bool ci_is_slow = false;
static bool l2cap_connecting = false; /* guards double L2CAP connect */
static volatile bool chan_stalled = false;
static uint32_t *cent_seg_tx_done = NULL; /* points to segment SDU counter for async TX_UNSTALLED */
static uint32_t *cent_seg_tx_drop = NULL; /* counts SDUs dropped (TX_UNSTALLED status != 0) */
static uint16_t cent_tx_sdu_len = L2CAP_COC_MTU; /* min(local, peer) after COC connect */
static const struct ble_gap_upd_params conn_params = {
.itvl_min = 6,
.itvl_max = 6,
.latency = 0,
.supervision_timeout = 2000,
.min_ce_len = 12,
.max_ce_len = 24,
};
void ble_store_config_init(void);
static os_membuf_t sdu_coc_mem[OS_MEMPOOL_SIZE(COC_BUF_COUNT, SDU_BLOCK_SIZE)];
static struct os_mempool sdu_coc_mempool;
static struct os_mbuf_pool sdu_os_mbuf_pool;
typedef struct {
uint8_t tx_phys;
uint8_t rx_phys;
uint8_t phy_opts; /* 0=none, 1=S2, 2=S8 */
int duration_s;
const char *name;
bool is_coded_s8;
} phy_entry_t;
static const phy_entry_t phy_list[] = {
#if CONFIG_EXAMPLE_TEST_PHY_1M
{ BLE_HCI_LE_PHY_1M_PREF_MASK, BLE_HCI_LE_PHY_1M_PREF_MASK, 0,
CONFIG_EXAMPLE_TEST_DURATION_1M, "1M", false },
#endif
#if CONFIG_EXAMPLE_TEST_PHY_2M
{ BLE_HCI_LE_PHY_2M_PREF_MASK, BLE_HCI_LE_PHY_2M_PREF_MASK, 0,
CONFIG_EXAMPLE_TEST_DURATION_2M, "2M", false },
#endif
#if CONFIG_EXAMPLE_TEST_PHY_CODED_S2
{ BLE_HCI_LE_PHY_CODED_PREF_MASK, BLE_HCI_LE_PHY_CODED_PREF_MASK, 0x01,
CONFIG_EXAMPLE_TEST_DURATION_CODED_S2, "Coded S2", false },
#endif
#if CONFIG_EXAMPLE_TEST_PHY_CODED_S8
{ BLE_HCI_LE_PHY_CODED_PREF_MASK, BLE_HCI_LE_PHY_CODED_PREF_MASK, 0x02,
CONFIG_EXAMPLE_TEST_DURATION_CODED_S8, "Coded S8", true },
#endif
};
#define PHY_LIST_LEN ((int)(sizeof(phy_list) / sizeof(phy_list[0])))
static int cent_gap_event(struct ble_gap_event *event, void *arg);
static int cent_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg);
static void cent_l2cap_coc_mem_init(void)
{
int rc;
rc = os_mempool_init(&sdu_coc_mempool, COC_BUF_COUNT, SDU_BLOCK_SIZE,
sdu_coc_mem, "cent_coc_pool");
assert(rc == 0);
rc = os_mbuf_pool_init(&sdu_os_mbuf_pool, &sdu_coc_mempool,
SDU_BLOCK_SIZE, COC_BUF_COUNT);
assert(rc == 0);
}
static void cent_l2cap_coc_connect(uint16_t conn_handle)
{
struct ble_gap_conn_desc desc;
if (ble_gap_conn_find(conn_handle, &desc) != 0) {
ESP_LOGE(TAG, "L2CAP COC connect: connection %d not found", conn_handle);
l2cap_connecting = false;
return;
}
struct os_mbuf *sdu_rx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0);
if (!sdu_rx) {
ESP_LOGE(TAG, "Failed to alloc sdu_rx for L2CAP connect");
l2cap_connecting = false;
return;
}
int rc = ble_l2cap_connect(conn_handle, L2CAP_COC_PSM, L2CAP_COC_MTU,
sdu_rx, cent_l2cap_coc_event_cb, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "L2CAP COC connect failed; rc=%d", rc);
l2cap_connecting = false;
/* EINVAL: NimBLE returns before chan alloc, sdu_rx not consumed — free it.
* ENOTCONN: NimBLE frees sdu_rx on all ENOTCONN paths (early !conn check
* and late TX failure via ble_l2cap_coc_cleanup_chan). Do not free here. */
if (rc == BLE_HS_EINVAL) {
os_mbuf_free_chain(sdu_rx);
}
}
}
static void cent_scan(void)
{
struct ble_gap_disc_params disc_params = {
.filter_duplicates = 1,
.passive = 1,
};
uint8_t own_addr_type;
int rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "Error inferring addr type; rc=%d", rc);
return;
}
rc = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, &disc_params,
cent_gap_event, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "Error starting scan; rc=%d", rc);
}
}
static int cent_should_connect(const struct ble_gap_disc_desc *disc)
{
struct ble_hs_adv_fields fields;
if (disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_ADV_IND &&
disc->event_type != BLE_HCI_ADV_RPT_EVTYPE_DIR_IND) {
return 0;
}
int rc = ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data);
if (rc != 0) {
return 0;
}
for (int i = 0; i < fields.num_uuids16; i++) {
if (ble_uuid_u16(&fields.uuids16[i].u) == L2CAP_COC_UUID) {
return 1;
}
}
return 0;
}
static void cent_connect_if_interesting(const struct ble_gap_disc_desc *disc)
{
if (!cent_should_connect(disc)) {
return;
}
int rc = ble_gap_disc_cancel();
if (rc != 0) {
return;
}
uint8_t own_addr_type;
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "Error inferring addr type; rc=%d", rc);
cent_scan();
return;
}
ESP_LOGI(TAG, "Connecting to %02x:%02x:%02x:%02x:%02x:%02x (addr_type=%d)",
disc->addr.val[5], disc->addr.val[4], disc->addr.val[3],
disc->addr.val[2], disc->addr.val[1], disc->addr.val[0],
disc->addr.type);
rc = ble_gap_connect(own_addr_type, &disc->addr, 30000, NULL,
cent_gap_event, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "Connect failed; rc=%d", rc);
cent_scan();
}
}
#if CONFIG_EXAMPLE_EXTENDED_ADV
static void cent_connect_if_interesting_ext(const struct ble_gap_ext_disc_desc *disc)
{
if (!(disc->props & BLE_HCI_ADV_CONN_MASK)) {
return;
}
struct ble_hs_adv_fields fields;
if (ble_hs_adv_parse_fields(&fields, disc->data, disc->length_data) != 0) {
return;
}
int found = 0;
for (int i = 0; i < fields.num_uuids16; i++) {
if (ble_uuid_u16(&fields.uuids16[i].u) == L2CAP_COC_UUID) {
found = 1;
break;
}
}
if (!found) {
return;
}
int rc = ble_gap_disc_cancel();
if (rc != 0) {
return;
}
uint8_t own_addr_type;
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "Error inferring addr type; rc=%d", rc);
cent_scan();
return;
}
ESP_LOGI(TAG, "Connecting to %02x:%02x:%02x:%02x:%02x:%02x (addr_type=%d)",
disc->addr.val[5], disc->addr.val[4], disc->addr.val[3],
disc->addr.val[2], disc->addr.val[1], disc->addr.val[0],
disc->addr.type);
rc = ble_gap_connect(own_addr_type, &disc->addr, 30000, NULL,
cent_gap_event, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "Connect failed; rc=%d", rc);
cent_scan();
}
}
#endif /* CONFIG_EXAMPLE_EXTENDED_ADV */
static int cent_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg)
{
switch (event->type) {
case BLE_L2CAP_EVENT_COC_CONNECTED: {
struct ble_l2cap_chan_info info;
uint16_t peer_mtu;
if (event->connect.status != 0) {
ESP_LOGE(TAG, "L2CAP COC connect status: %d,terminating GAP to restart", event->connect.status);
l2cap_connecting = false;
ble_gap_terminate(conn_handle, BLE_ERR_REM_USER_CONN_TERM);
return 0;
}
ESP_LOGI(TAG, "L2CAP COC connected, chan=%p", event->connect.chan);
coc_chan = event->connect.chan;
peer_mtu = 0;
cent_tx_sdu_len = L2CAP_COC_MTU;
if (ble_l2cap_get_chan_info(coc_chan, &info) == 0) {
peer_mtu = info.peer_coc_mtu;
if (peer_mtu > 0 && peer_mtu < cent_tx_sdu_len) {
cent_tx_sdu_len = peer_mtu;
}
}
ESP_LOGI(TAG, "TX SDU size: %u bytes (peer CoC MTU %u)",
cent_tx_sdu_len, peer_mtu);
l2cap_connecting = false;
xEventGroupSetBits(coc_event_group, COC_CONNECTED_BIT);
return 0;
}
case BLE_L2CAP_EVENT_COC_DISCONNECTED:
ESP_LOGI(TAG, "L2CAP COC disconnected");
coc_chan = NULL;
chan_stalled = false;
cent_seg_tx_done = NULL;
cent_seg_tx_drop = NULL;
l2cap_connecting = false;
xEventGroupClearBits(coc_event_group, COC_CONNECTED_BIT);
xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT | CONN_UPDATED_BIT | PHY_UPDATED_BIT);
return 0;
case BLE_L2CAP_EVENT_COC_TX_UNSTALLED:
/* status==0: SDU delivered; status!=0: NimBLE dropped it (ENOMEM) — don't count. */
chan_stalled = false;
if (event->tx_unstalled.status == 0) {
if (cent_seg_tx_done) {
(*cent_seg_tx_done)++;
}
} else {
if (cent_seg_tx_drop) {
(*cent_seg_tx_drop)++;
}
ESP_LOGD(TAG, "TX_UNSTALLED status=%d: SDU dropped", event->tx_unstalled.status);
}
xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT);
return 0;
case BLE_L2CAP_EVENT_COC_DATA_RECEIVED: {
struct os_mbuf *sdu_rx;
int rc;
if (event->receive.sdu_rx) {
os_mbuf_free_chain(event->receive.sdu_rx);
}
sdu_rx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0);
if (sdu_rx) {
rc = ble_l2cap_recv_ready(event->receive.chan, sdu_rx);
if (rc != 0) {
os_mbuf_free_chain(sdu_rx);
}
} else {
ESP_LOGE(TAG, "DATA_RECEIVED: no mbuf for recv_ready; RX may stall");
}
return 0;
}
default:
return 0;
}
}
static void wait_unstall(uint32_t timeout_ms)
{
xEventGroupWaitBits(coc_event_group, TX_UNSTALLED_BIT, pdTRUE, pdTRUE, pdMS_TO_TICKS(timeout_ms));
}
static void cent_send_task(void *arg)
{
static uint8_t value[L2CAP_COC_MTU];
int rc;
for (int i = 0; i < L2CAP_COC_MTU; i++) {
value[i] = i & 0xFF;
}
xEventGroupWaitBits(coc_event_group, COC_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
ESP_LOGI(TAG, "L2CAP COC Throughput — TX side (central sends to peripheral)");
ESP_LOGI(TAG, "Number of enabled PHYs: %d", (int)PHY_LIST_LEN);
if (PHY_LIST_LEN == 0) {
ESP_LOGE(TAG, "No test PHY enabled; enable at least one EXAMPLE_TEST_PHY_* in menuconfig");
vTaskDelete(NULL);
return;
}
while (1) {
bool lost_connection = false;
for (int i = 0; i < PHY_LIST_LEN && !lost_connection; i++) {
const phy_entry_t *phy = &phy_list[i];
xEventGroupClearBits(coc_event_group, PHY_UPDATED_BIT);
#if CONFIG_SOC_BLE_50_SUPPORTED
rc = ble_gap_set_prefered_le_phy(conn_handle,
phy->tx_phys,
phy->rx_phys,
phy->phy_opts);
if (rc != 0) {
ESP_LOGE(TAG, "PHY switch to %s failed; rc=%d — continuing anyway",
phy->name, rc);
xEventGroupSetBits(coc_event_group, PHY_UPDATED_BIT);
}
#else
xEventGroupSetBits(coc_event_group, PHY_UPDATED_BIT);
#endif
EventBits_t bits = xEventGroupWaitBits(coc_event_group, PHY_UPDATED_BIT,
pdTRUE, pdTRUE,
pdMS_TO_TICKS(5000));
if (!(bits & PHY_UPDATED_BIT)) {
ESP_LOGW(TAG, "PHY update timeout for %s; continuing anyway", phy->name);
}
if (phy->is_coded_s8) {
/* CI=40ms, CE=32.5-40ms: fits 2 Coded S8 K-frames per CI and prevents credit starvation */
struct ble_gap_upd_params s8_params = {
.itvl_min = 32,
.itvl_max = 32,
.latency = 0,
.supervision_timeout = 2000,
.min_ce_len = 52,
.max_ce_len = 64,
};
xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT);
rc = ble_gap_update_params(conn_handle, &s8_params);
if (rc == 0) {
ESP_LOGI(TAG, "Coded S8: updating CI");
ci_is_slow = true;
xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE,
pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS));
} else {
ESP_LOGW(TAG, "S8 CI update failed (rc=%d)", rc);
}
} else if (phy->tx_phys == BLE_HCI_LE_PHY_CODED_PREF_MASK) {
/* CI=20ms, CE=10-20ms: fits 2 Coded S2 K-frames per CI */
struct ble_gap_upd_params s2_params = {
.itvl_min = 16,
.itvl_max = 16,
.latency = 0,
.supervision_timeout = 2000,
.min_ce_len = 16,
.max_ce_len = 32,
};
xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT);
rc = ble_gap_update_params(conn_handle, &s2_params);
if (rc == 0) {
ESP_LOGI(TAG, "Coded S2: updating CI");
ci_is_slow = true;
xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE,
pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS));
} else {
ESP_LOGW(TAG, "S2 CI update failed (rc=%d)", rc);
}
} else if (ci_is_slow) {
ci_is_slow = false;
xEventGroupClearBits(coc_event_group, CONN_UPDATED_BIT);
rc = ble_gap_update_params(conn_handle, &conn_params);
if (rc == 0) {
ESP_LOGI(TAG, "%s: restoring CI to 6 ms", phy->name);
xEventGroupWaitBits(coc_event_group, CONN_UPDATED_BIT, pdTRUE, pdTRUE,
pdMS_TO_TICKS(CONN_PARAM_UPDATE_TIMEOUT_MS));
} else {
ESP_LOGW(TAG, "CI restore failed (rc=%d)", rc);
}
}
if (chan_stalled) {
ESP_LOGI(TAG, "Pre-drain: waiting for unstall");
wait_unstall(PREDRAIN_TIMEOUT_MS);
if (chan_stalled) {
ESP_LOGW(TAG, "Pre-drain timed out; forcing clear");
chan_stalled = false;
}
}
int64_t start_us = esp_timer_get_time();
int64_t end_us = start_us + (int64_t)phy->duration_s * 1000000LL;
/* Both the send task and TX_UNSTALLED callback update these counters; a
* lost update is possible on dual-core but harmless for throughput stats. */
uint32_t segment_sdus = 0;
uint32_t segment_drops = 0;
cent_seg_tx_done = &segment_sdus;
cent_seg_tx_drop = &segment_drops;
xEventGroupClearBits(coc_event_group, TX_UNSTALLED_BIT); /* clear stale signal from previous segment */
ESP_LOGI(TAG, "[%s PHY] Sending for %d s", phy->name, phy->duration_s);
while (!lost_connection && esp_timer_get_time() < end_us) {
/* Snapshot coc_chan — NimBLE host task can NULL it between check and send. */
struct ble_l2cap_chan *chan = coc_chan;
if (!chan) {
ESP_LOGW(TAG, "COC channel lost during test");
cent_seg_tx_done = NULL;
cent_seg_tx_drop = NULL;
lost_connection = true;
break;
}
struct os_mbuf *sdu_tx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0);
if (!sdu_tx) {
vTaskDelay(1);
continue;
}
rc = os_mbuf_append(sdu_tx, value, cent_tx_sdu_len);
if (rc != 0) {
os_mbuf_free_chain(sdu_tx);
continue;
}
rc = ble_l2cap_send(chan, sdu_tx);
if (rc == 0) {
segment_sdus++;
} else if (rc == BLE_HS_ESTALLED) {
chan_stalled = true;
xEventGroupWaitBits(coc_event_group, TX_UNSTALLED_BIT, pdTRUE, pdTRUE, pdMS_TO_TICKS(100));
continue;
} else if (rc == BLE_HS_EBUSY) {
os_mbuf_free_chain(sdu_tx);
if (chan_stalled) {
xEventGroupWaitBits(coc_event_group, TX_UNSTALLED_BIT, pdTRUE, pdTRUE, pdMS_TO_TICKS(100));
} else {
taskYIELD();
}
continue;
} else if (rc == BLE_HS_ENOMEM) {
vTaskDelay(1);
continue;
} else {
ESP_LOGE(TAG, "Send failed; rc=%d", rc);
if (rc == BLE_HS_EBADDATA) {
os_mbuf_free_chain(sdu_tx);
}
break;
}
if (segment_sdus % TX_YIELD_INTERVAL == 0 && segment_sdus > 0) {
vTaskDelay(1);
}
}
if (!coc_chan && !lost_connection) {
cent_seg_tx_done = NULL;
cent_seg_tx_drop = NULL;
lost_connection = true;
}
if (lost_connection) {
break;
}
if (chan_stalled) {
ESP_LOGI(TAG, "Post-drain: waiting for unstall");
wait_unstall(POSTDRAIN_TIMEOUT_MS);
if (chan_stalled) {
ESP_LOGW(TAG, "Post-drain timed out; pre-drain will retry");
}
}
int64_t elapsed_us = esp_timer_get_time() - start_us;
if (elapsed_us < 1) { elapsed_us = 1; }
uint64_t bytes_sent = (uint64_t)segment_sdus * cent_tx_sdu_len;
uint32_t elapsed_ms = (uint32_t)(elapsed_us / 1000);
if (elapsed_ms == 0) { elapsed_ms = 1; }
uint32_t tp_kbps = (uint32_t)((bytes_sent * 8ULL) / elapsed_ms);
uint32_t dropped = segment_drops;
cent_seg_tx_done = NULL;
cent_seg_tx_drop = NULL;
ESP_LOGI(TAG, "+-------------------------------------------------+");
ESP_LOGI(TAG, "| PHY : %-39s|", phy->name);
ESP_LOGI(TAG, "| TX : %-6" PRIu32 " kbps |", tp_kbps);
ESP_LOGI(TAG, "| Bytes : %-10" PRIu64 " |", bytes_sent);
ESP_LOGI(TAG, "| Time : %-5" PRIu32 " s |", elapsed_ms / 1000);
ESP_LOGI(TAG, "+-------------------------------------------------+");
if (dropped > 0) {
ESP_LOGW(TAG, "%" PRIu32 " SDUs dropped (ENOMEM); raise BT_NIMBLE_MSYS_1_BLOCK_COUNT",
dropped);
}
}
if (lost_connection) {
ESP_LOGI(TAG, "Waiting for L2CAP COC reconnection...");
/* disconnect handler already cleared COC_CONNECTED_BIT; clearing it
* again here could cancel a bit set by a reconnect that raced ahead. */
xEventGroupWaitBits(coc_event_group, COC_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY);
} else {
ESP_LOGI(TAG, "Cycle complete. Looping back to first PHY...");
}
}
}
static int cent_gap_event(struct ble_gap_event *event, void *arg)
{
int rc;
switch (event->type) {
case BLE_GAP_EVENT_DISC:
cent_connect_if_interesting(&event->disc);
return 0;
#if CONFIG_EXAMPLE_EXTENDED_ADV
case BLE_GAP_EVENT_EXT_DISC:
cent_connect_if_interesting_ext(&event->ext_disc);
return 0;
#endif
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status == 0) {
struct ble_gap_conn_desc desc;
if (ble_gap_conn_find(event->connect.conn_handle, &desc) == 0) {
ESP_LOGI(TAG, "Connected; handle=%d peer=%02x:%02x:%02x:%02x:%02x:%02x",
event->connect.conn_handle,
desc.peer_id_addr.val[5], desc.peer_id_addr.val[4],
desc.peer_id_addr.val[3], desc.peer_id_addr.val[2],
desc.peer_id_addr.val[1], desc.peer_id_addr.val[0]);
}
conn_handle = event->connect.conn_handle;
l2cap_connecting = false;
rc = ble_hs_hci_util_set_data_len(conn_handle,
LL_PACKET_LENGTH, LL_PACKET_TIME);
if (rc != 0) {
/* DATA_LEN_CHG won't fire — connect L2CAP directly as fallback */
ESP_LOGE(TAG, "Set packet length failed; rc=%d, connecting L2CAP directly", rc);
l2cap_connecting = true;
cent_l2cap_coc_connect(conn_handle);
} else {
/* DLE accepted; connect L2CAP now — DATA_LEN_CHG may not fire if
* the peer's data length is already at the requested value */
l2cap_connecting = true;
cent_l2cap_coc_connect(conn_handle);
}
} else {
ESP_LOGE(TAG, "Connection failed; status=%d", event->connect.status);
cent_scan();
}
return 0;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(TAG, "Disconnected; reason=%d", event->disconnect.reason);
conn_handle = BLE_HS_CONN_HANDLE_NONE;
coc_chan = NULL;
chan_stalled = false;
ci_is_slow = false;
l2cap_connecting = false;
xEventGroupClearBits(coc_event_group, COC_CONNECTED_BIT);
xEventGroupSetBits(coc_event_group, TX_UNSTALLED_BIT | CONN_UPDATED_BIT | PHY_UPDATED_BIT);
cent_scan();
return 0;
case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE:
ESP_LOGI(TAG, "PHY updated: tx=%d rx=%d status=%d",
event->phy_updated.tx_phy,
event->phy_updated.rx_phy,
event->phy_updated.status);
if (event->phy_updated.status != 0) {
ESP_LOGW(TAG, "PHY update failed; status=%d", event->phy_updated.status);
}
xEventGroupSetBits(coc_event_group, PHY_UPDATED_BIT);
return 0;
case BLE_GAP_EVENT_CONN_UPDATE:
ESP_LOGI(TAG, "Conn params updated; status=%d", event->conn_update.status);
if (event->conn_update.status != 0) {
ESP_LOGW(TAG, "Connection parameter update failed (status=%d)",
event->conn_update.status);
}
xEventGroupSetBits(coc_event_group, CONN_UPDATED_BIT);
return 0;
case BLE_GAP_EVENT_DATA_LEN_CHG:
/* fires for TX and RX; guard ensures connect called only once */
if (!l2cap_connecting && coc_chan == NULL) {
l2cap_connecting = true;
cent_l2cap_coc_connect(conn_handle);
}
return 0;
case BLE_GAP_EVENT_DISC_COMPLETE:
ESP_LOGI(TAG, "Discovery complete; reason=%d", event->disc_complete.reason);
return 0;
default:
return 0;
}
}
static void cent_on_reset(int reason)
{
ESP_LOGE(TAG, "Host reset; reason=%d", reason);
}
static void cent_on_sync(void)
{
int rc = ble_hs_util_ensure_addr(0);
assert(rc == 0);
uint8_t own_addr_type;
uint8_t addr[6] = {0};
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "Error inferring addr type; rc=%d", rc);
return;
}
ble_hs_id_copy_addr(own_addr_type, addr, NULL);
ESP_LOGI(TAG, "Device Address: %02x:%02x:%02x:%02x:%02x:%02x",
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
cent_scan();
}
static void cent_host_task(void *param)
{
ESP_LOGI(TAG, "BLE Host Task started");
nimble_port_run();
nimble_port_freertos_deinit();
}
void app_main(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
coc_event_group = xEventGroupCreate();
assert(coc_event_group);
ret = nimble_port_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "nimble_port_init failed; rc=%d", ret);
return;
}
cent_l2cap_coc_mem_init();
ble_hs_cfg.reset_cb = cent_on_reset;
ble_hs_cfg.sync_cb = cent_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
#if CONFIG_BT_NIMBLE_GAP_SERVICE
int rc = ble_svc_gap_device_name_set("l2cap-coc-cent");
assert(rc == 0);
#endif
ble_store_config_init();
if (xTaskCreate(cent_send_task, "cent_send_task", 4096, NULL, 5, NULL) != pdPASS) {
ESP_LOGE(TAG, "Failed to create cent_send_task");
return;
}
nimble_port_freertos_init(cent_host_task);
}

View File

@@ -0,0 +1,16 @@
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=512
CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=255
CONFIG_BT_NIMBLE_LOG_LEVEL=4
CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=400
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=255
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=50
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_SIZE=260
CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=1
CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT=12
CONFIG_EXAMPLE_L2CAP_COC_MTU=2048
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30

View File

@@ -0,0 +1,4 @@
CONFIG_EXAMPLE_L2CAP_COC_MTU=2048
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=96
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=48
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10

View File

@@ -0,0 +1,6 @@
# ACL_FROM_LL_COUNT kept at 67 (not reduced like MSYS pools) to avoid 0 kbps
# throughput after Coded S2 <-> S8 PHY switches on this RAM-limited target.
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=292
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=0
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67

View File

@@ -0,0 +1,4 @@
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=160
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=48
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24
CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=8

View File

@@ -0,0 +1,3 @@
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=120
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=48
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67

View File

@@ -0,0 +1,3 @@
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=120
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=48
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67

View File

@@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.22)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(MINIMAL_BUILD ON)
project(l2cap_coc_prph)

View File

@@ -0,0 +1,77 @@
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-S3 | ESP32-S31 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | --------- |
# L2CAP COC Throughput Peripheral Example
`l2cap_coc_prph` demonstrates the peripheral side of an L2CAP Connection-Oriented Channel (COC) throughput test using NimBLE on ESP32. It advertises with UUID 0x1812, accepts an incoming GAP connection from `l2cap_coc_cent`, registers an L2CAP COC server on PSM 0x1002, and measures RX throughput as the central sends SDUs.
The peripheral tracks throughput per PHY — each time the central switches PHY, the peripheral prints a per-PHY throughput summary box and resets its counters. A background stats task also prints a live per-second RX rate while data is flowing. It must be used together with the `l2cap_coc_cent` example which acts as the sending side.
It uses ESP32's Bluetooth controller and NimBLE stack based BLE host.
## How to Use Example
Before project configuration and build, be sure to set the correct chip target using:
```bash
idf.py set-target <chip_name>
```
### Hardware Required
* Two development boards, one flashed with `l2cap_coc_prph` and the other with `l2cap_coc_cent`.
* A USB cable for power supply and programming.
See [Development Boards](https://www.espressif.com/en/products/devkits) for more information.
### Configure the Project
Open the project configuration menu:
```bash
idf.py menuconfig
```
In the `L2CAP COC Throughput Configuration` menu:
| Option | Default | Description |
|---------|---------|-------------|
| `EXAMPLE_L2CAP_COC_MTU` | `16384` | Peripheral L2CAP CoC SDU MTU size in bytes. |
| `EXAMPLE_EXTENDED_ADV` | `y` (BLE 5.0 chips) | Enable extended advertising for BLE 5.0 capable devices. Required for Coded PHY testing on ESP32-C6 and ESP32-H2. |
> **Note:** Throughput in the central → peripheral direction is primarily determined by the peripheral MTU. With the default configuration (`MTU=16384`, `MPS=247`), NimBLE grants approximately 67 initial credits to the central sender, allowing multiple packets to remain in flight and maximizing link throughput.
### Build and Flash
Run `idf.py -p PORT flash monitor` to build, flash and monitor the project.
(To exit the serial monitor, type ``Ctrl-]``.)
See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects.
## Example Output
On successful connection and data reception, the peripheral prints a live per-second RX rate while data flows:
```
I (xxx) l2cap_coc_prph: BLE Host Task started
I (xxx) l2cap_coc_prph: Device Address: xx:xx:xx:xx:xx:xx
I (xxx) l2cap_coc_prph: Extended advertising started
I (xxx) l2cap_coc_prph: Connected; handle=0
I (xxx) l2cap_coc_prph: L2CAP COC connected, chan=0xxxxxxxxx
I (xxx) l2cap_coc_prph: PHY updated: tx=2 rx=2 status=0
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
I (xxx) l2cap_coc_prph: | RX : xxxx kbps |
```
> **Note:** The peripheral prints one RX line per second. The central controls PHY selection and test duration; the peripheral tracks and displays throughput continuously as long as data is flowing.
## Troubleshooting
For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon.

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "main.c"
PRIV_REQUIRES bt nvs_flash esp_timer
INCLUDE_DIRS ".")

View File

@@ -0,0 +1,23 @@
menu "L2CAP COC Throughput Configuration"
config EXAMPLE_L2CAP_COC_MTU
int "L2CAP CoC MTU size in bytes"
default 16384
range 512 65511
help
L2CAP CoC SDU MTU size in bytes.
Note: memory pool allocates 6 buffers of this size; total pool
memory = MTU x 6. On chips without PSRAM ensure sufficient heap
is available before increasing this value.
Use idf.py size-components to verify.
config EXAMPLE_EXTENDED_ADV
bool
depends on SOC_BLE_50_SUPPORTED && BT_NIMBLE_50_FEATURE_SUPPORT
default y if SOC_ESP_NIMBLE_CONTROLLER
select BT_NIMBLE_EXT_ADV
prompt "Enable Extended Advertising"
help
Enable BLE 5.0 extended advertising.
endmenu

View File

@@ -0,0 +1,445 @@
/*
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
static const char *TAG = "l2cap_coc_prph";
#define L2CAP_COC_PSM 0x1002
#define L2CAP_COC_MTU CONFIG_EXAMPLE_L2CAP_COC_MTU
#define COC_BUF_COUNT (6 * MYNEWT_VAL(BLE_L2CAP_COC_MAX_NUM))
/* Block size must include mbuf headers so each SDU fits in one pool entry. */
#define SDU_BLOCK_SIZE (L2CAP_COC_MTU + sizeof(struct os_mbuf_pkthdr) + sizeof(struct os_mbuf))
#define LL_PACKET_LENGTH 251
#define LL_PACKET_TIME 2120
#define L2CAP_COC_UUID 0x1812
static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE;
static struct ble_l2cap_chan *coc_chan = NULL;
static uint8_t own_addr_type;
static int64_t phy_start_time = 0;
static volatile uint32_t rx_bytes = 0;
static uint32_t rx_packets = 0;
static volatile bool coc_active = false;
static const char *phy_name = "1M";
static uint8_t current_phy = BLE_HCI_LE_PHY_1M;
void ble_store_config_init(void);
static os_membuf_t sdu_coc_mem[OS_MEMPOOL_SIZE(COC_BUF_COUNT, SDU_BLOCK_SIZE)];
static struct os_mempool sdu_coc_mempool;
static struct os_mbuf_pool sdu_os_mbuf_pool;
static int prph_gap_event(struct ble_gap_event *event, void *arg);
static const char *prph_phy_str(uint8_t phy)
{
switch (phy) {
case BLE_HCI_LE_PHY_2M: return "2M";
case BLE_HCI_LE_PHY_CODED: return "Coded";
default: return "1M";
}
}
static void prph_report_phy(int64_t end_time, int64_t start_time,
uint32_t bytes, uint32_t packets,
const char *phy_name)
{
if (packets == 0 || start_time == 0) {
return;
}
int64_t elapsed_ms = (end_time - start_time) / 1000;
if (elapsed_ms == 0) { elapsed_ms = 1; }
uint32_t kbps = (uint32_t)((uint64_t)bytes * 8ULL
/ (uint64_t)elapsed_ms);
ESP_LOGI(TAG, "+-------------------------------------------------+");
ESP_LOGI(TAG, "| PHY : %-39s|", phy_name);
ESP_LOGI(TAG, "| RX : %-6" PRIu32 " kbps |", kbps);
ESP_LOGI(TAG, "| Bytes : %-10" PRIu32 " |", bytes);
ESP_LOGI(TAG, "| Time : %-5lld s |", elapsed_ms / 1000);
ESP_LOGI(TAG, "+-------------------------------------------------+");
}
#if CONFIG_EXAMPLE_EXTENDED_ADV
static uint8_t ext_adv_pattern[] = {
0x02, BLE_HS_ADV_TYPE_FLAGS, 0x06,
0x03, BLE_HS_ADV_TYPE_COMP_UUIDS16, 0x12, 0x18,
0x11, BLE_HS_ADV_TYPE_COMP_NAME,
'l','2','c','a','p','-','c','o','c','-','p','r','p','h','-','e',
};
static void prph_advertise(void)
{
struct ble_gap_ext_adv_params params;
struct os_mbuf *data;
uint8_t instance = 0;
int rc;
memset(&params, 0, sizeof(params));
params.connectable = 1;
params.own_addr_type = own_addr_type;
params.primary_phy = BLE_HCI_LE_PHY_1M;
params.secondary_phy = BLE_HCI_LE_PHY_1M;
params.tx_power = 127;
params.sid = 1;
params.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
params.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MIN;
rc = ble_gap_ext_adv_configure(instance, &params, NULL, prph_gap_event, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "ext_adv_configure failed; rc=%d", rc);
return;
}
data = os_msys_get_pkthdr(sizeof(ext_adv_pattern), 0);
if (!data) {
ESP_LOGE(TAG, "ext_adv: failed to alloc adv data mbuf");
return;
}
rc = os_mbuf_append(data, ext_adv_pattern, sizeof(ext_adv_pattern));
if (rc != 0) {
ESP_LOGE(TAG, "ext_adv: mbuf_append failed; rc=%d", rc);
os_mbuf_free_chain(data);
return;
}
rc = ble_gap_ext_adv_set_data(instance, data);
if (rc != 0) {
ESP_LOGE(TAG, "ext_adv_set_data failed; rc=%d", rc);
return;
}
rc = ble_gap_ext_adv_start(instance, 0, 0);
if (rc != 0) {
ESP_LOGE(TAG, "ext_adv_start failed; rc=%d", rc);
return;
}
ESP_LOGI(TAG, "Extended advertising started");
}
#else
static void prph_advertise(void)
{
struct ble_gap_adv_params adv_params;
struct ble_hs_adv_fields fields;
int rc;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.tx_pwr_lvl_is_present = 1;
fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
#if CONFIG_BT_NIMBLE_GAP_SERVICE
const char *name = ble_svc_gap_device_name();
fields.name = (uint8_t *)name;
fields.name_len = strlen(name);
fields.name_is_complete = 1;
#endif
fields.uuids16 = (ble_uuid16_t[]){ BLE_UUID16_INIT(L2CAP_COC_UUID) };
fields.num_uuids16 = 1;
fields.uuids16_is_complete = 1;
rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
ESP_LOGE(TAG, "Error setting adv data; rc=%d", rc);
return;
}
memset(&adv_params, 0, sizeof(adv_params));
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER,
&adv_params, prph_gap_event, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "Error starting adv; rc=%d", rc);
}
}
#endif /* CONFIG_EXAMPLE_EXTENDED_ADV */
static void prph_l2cap_coc_mem_init(void)
{
int rc;
rc = os_mempool_init(&sdu_coc_mempool, COC_BUF_COUNT, SDU_BLOCK_SIZE,
sdu_coc_mem, "prph_coc_pool");
assert(rc == 0);
rc = os_mbuf_pool_init(&sdu_os_mbuf_pool, &sdu_coc_mempool, SDU_BLOCK_SIZE,
COC_BUF_COUNT);
assert(rc == 0);
}
static int prph_l2cap_coc_accept(struct ble_l2cap_chan *chan)
{
struct os_mbuf *sdu_rx = os_mbuf_get_pkthdr(&sdu_os_mbuf_pool, 0);
if (!sdu_rx) {
return BLE_HS_ENOMEM;
}
int rc = ble_l2cap_recv_ready(chan, sdu_rx);
if (rc != 0) {
os_mbuf_free_chain(sdu_rx);
}
return rc;
}
static int prph_l2cap_coc_event_cb(struct ble_l2cap_event *event, void *arg)
{
switch (event->type) {
case BLE_L2CAP_EVENT_COC_CONNECTED:
if (event->connect.status != 0) {
ESP_LOGE(TAG, "L2CAP COC connect error: %d", event->connect.status);
return 0;
}
ESP_LOGI(TAG, "L2CAP COC connected, chan=%p", event->connect.chan);
coc_chan = event->connect.chan;
phy_start_time = 0; /* anchored on first data SDU, not connect */
rx_bytes = 0;
rx_packets = 0;
coc_active = true;
phy_name = prph_phy_str(current_phy);
return 0;
case BLE_L2CAP_EVENT_COC_DISCONNECTED:
coc_active = false;
coc_chan = NULL;
current_phy = BLE_HCI_LE_PHY_1M;
{
int64_t end = esp_timer_get_time();
int64_t st = phy_start_time;
uint32_t by = rx_bytes;
uint32_t pk = rx_packets;
prph_report_phy(end, st, by, pk, phy_name);
rx_bytes = 0; rx_packets = 0; phy_start_time = 0;
}
ESP_LOGI(TAG, "L2CAP COC disconnected");
return 0;
case BLE_L2CAP_EVENT_COC_ACCEPT: {
/* Pre-grant 2 receive buffers so the central can pipeline 2 SDUs. */
int rc = prph_l2cap_coc_accept(event->accept.chan);
if (rc != 0) {
return rc;
}
/* Second buffer is best-effort; one buffer is enough for the channel to operate. */
if (prph_l2cap_coc_accept(event->accept.chan) != 0) {
ESP_LOGW(TAG, "L2CAP COC accept: second RX buffer unavailable, running with one");
}
return 0;
}
case BLE_L2CAP_EVENT_COC_DATA_RECEIVED:
if (event->receive.sdu_rx) {
if (rx_packets == 0) {
phy_start_time = esp_timer_get_time();
}
rx_bytes += OS_MBUF_PKTLEN(event->receive.sdu_rx);
rx_packets += 1;
os_mbuf_free_chain(event->receive.sdu_rx);
}
if (prph_l2cap_coc_accept(event->receive.chan) != 0) {
ESP_LOGE(TAG, "DATA_RECEIVED: no mbuf for recv_ready; RX may stall");
}
return 0;
default:
return 0;
}
}
static void prph_stats_task(void *arg)
{
uint32_t prev_bytes = 0;
int64_t prev_time = 0;
while (1) {
vTaskDelay(pdMS_TO_TICKS(1000));
if (!coc_active) {
prev_bytes = 0;
prev_time = 0;
continue;
}
int64_t now = esp_timer_get_time();
uint32_t bytes = rx_bytes;
if (prev_time > 0) {
if (bytes < prev_bytes) {
prev_bytes = bytes;
prev_time = now;
continue;
}
int64_t dt_us = now - prev_time;
uint32_t dt_bytes = bytes - prev_bytes;
uint32_t kbps = (uint32_t)((uint64_t)dt_bytes * 8ULL * 1000000ULL
/ (uint64_t)dt_us / 1000ULL);
ESP_LOGI(TAG, "| RX : %-6" PRIu32 " kbps |", kbps);
}
prev_bytes = bytes;
prev_time = now;
}
}
static int prph_gap_event(struct ble_gap_event *event, void *arg)
{
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
if (event->connect.status != 0) {
ESP_LOGE(TAG, "Connection failed; status=%d", event->connect.status);
prph_advertise();
return 0;
}
ESP_LOGI(TAG, "Connected; handle=%d", event->connect.conn_handle);
conn_handle = event->connect.conn_handle;
return 0;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(TAG, "Disconnected; reason=%d", event->disconnect.reason);
conn_handle = BLE_HS_CONN_HANDLE_NONE;
coc_chan = NULL;
coc_active = false;
current_phy = BLE_HCI_LE_PHY_1M;
phy_name = "1M";
#if CONFIG_EXAMPLE_EXTENDED_ADV
ble_gap_ext_adv_stop(0);
#endif
prph_advertise();
return 0;
case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE:
ESP_LOGI(TAG, "PHY updated: tx=%d rx=%d status=%d",
event->phy_updated.tx_phy,
event->phy_updated.rx_phy,
event->phy_updated.status);
if (event->phy_updated.status == 0) {
if (coc_active) {
int64_t end = esp_timer_get_time();
int64_t st = phy_start_time;
uint32_t by = rx_bytes;
uint32_t pk = rx_packets;
prph_report_phy(end, st, by, pk, phy_name);
rx_bytes = 0; rx_packets = 0; phy_start_time = 0;
}
current_phy = event->phy_updated.rx_phy;
phy_name = prph_phy_str(event->phy_updated.rx_phy);
}
return 0;
case BLE_GAP_EVENT_CONN_UPDATE:
ESP_LOGI(TAG, "Conn params updated; status=%d", event->conn_update.status);
if (event->conn_update.status == 0 && coc_active &&
current_phy == BLE_HCI_LE_PHY_CODED) {
struct ble_gap_conn_desc desc;
if (ble_gap_conn_find(conn_handle, &desc) == 0) {
if (desc.conn_itvl != 16 && desc.conn_itvl != 32) {
return 0;
}
if (strcmp(phy_name, "Coded") != 0) {
int64_t end = esp_timer_get_time();
uint32_t by = rx_bytes;
uint32_t pk = rx_packets;
prph_report_phy(end, phy_start_time, by, pk, phy_name);
rx_bytes = 0; rx_packets = 0; phy_start_time = 0;
}
phy_name = (desc.conn_itvl >= 32) ? "Coded S8" : "Coded S2";
ESP_LOGI(TAG, "Coding scheme updated to %s (CI=%u × 1.25ms)",
phy_name, desc.conn_itvl);
}
}
return 0;
case BLE_GAP_EVENT_ADV_COMPLETE:
#if !CONFIG_EXAMPLE_EXTENDED_ADV
prph_advertise();
#endif
return 0;
default:
return 0;
}
}
static void prph_on_reset(int reason)
{
ESP_LOGE(TAG, "Host reset; reason=%d", reason);
}
static void prph_on_sync(void)
{
int rc;
rc = ble_hs_util_ensure_addr(0);
assert(rc == 0);
rc = ble_hs_id_infer_auto(0, &own_addr_type);
assert(rc == 0);
rc = ble_l2cap_create_server(L2CAP_COC_PSM, L2CAP_COC_MTU,
prph_l2cap_coc_event_cb, NULL);
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGE(TAG, "Failed to create L2CAP COC server; rc=%d", rc);
return;
}
uint8_t addr[6] = {0};
ble_hs_id_copy_addr(own_addr_type, addr, NULL);
ESP_LOGI(TAG, "Device Address: %02x:%02x:%02x:%02x:%02x:%02x",
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
prph_advertise();
}
static void prph_host_task(void *param)
{
ESP_LOGI(TAG, "BLE Host Task started");
nimble_port_run();
nimble_port_freertos_deinit();
}
void app_main(void)
{
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ret = nimble_port_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "nimble_port_init failed; rc=%d", ret);
return;
}
prph_l2cap_coc_mem_init();
ble_hs_cfg.reset_cb = prph_on_reset;
ble_hs_cfg.sync_cb = prph_on_sync;
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
#if CONFIG_BT_NIMBLE_GAP_SERVICE
int rc = ble_svc_gap_device_name_set("l2cap-coc-prph");
assert(rc == 0);
#endif
ble_store_config_init();
if (xTaskCreate(prph_stats_task, "prph_stats", 4096, NULL, 5, NULL) != pdPASS) {
ESP_LOGE(TAG, "Failed to create stats task");
}
nimble_port_freertos_init(prph_host_task);
}

View File

@@ -0,0 +1,15 @@
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=512
CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=255
CONFIG_BT_NIMBLE_LOG_LEVEL=4
CONFIG_BT_NIMBLE_LOG_LEVEL_NONE=y
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=400
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=255
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=50
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_SIZE=260
CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=1
CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT=12
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30

View File

@@ -0,0 +1,4 @@
CONFIG_EXAMPLE_L2CAP_COC_MTU=8192
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=20
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=20
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=10

View File

@@ -0,0 +1,7 @@
# MTU is intentionally left at the Kconfig default (16384) for throughput.
# Reducing to 2048 cuts credits from ~67 to ~9, dropping throughput ~8x.
# If the build fails due to RAM pressure (~96 KB static SDU pool), lower MTU here.
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=292
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=0
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24

View File

@@ -0,0 +1,6 @@
# MTU is intentionally left at the Kconfig default (16384) for throughput.
# Reducing MTU lowers L2CAP credit flow, which directly cuts throughput.
# If the build fails due to RAM pressure (~98 KB static SDU pool), lower MTU here.
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=20
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24
CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=8

View File

@@ -0,0 +1,2 @@
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=30
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67

View File

@@ -0,0 +1,2 @@
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=30
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=67

View File

@@ -103,8 +103,10 @@ KNOWN_MISSING = {
'bluetooth/nimble/bleprph_host_only',
'bluetooth/nimble/bleprph_wifi_coex',
'bluetooth/nimble/hci',
'bluetooth/nimble/throughput_app/blecent_throughput',
'bluetooth/nimble/throughput_app/bleprph_throughput',
'bluetooth/nimble/throughput_app/gatt/blecent_throughput',
'bluetooth/nimble/throughput_app/gatt/bleprph_throughput',
'bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent',
'bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph',
# TODO IDF-15385: add :example: references for build_system examples
'build_system/cmake/import_prebuilt/prebuilt',
'build_system/cmakev2/features/component_manager',