mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-03 03:31:41 +03:00
feat(ble_audio): Support ISO & LE Audio functionalities (Preview)
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# The following lines of boilerplate have to be in your project's CMakeLists
|
||||
# in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
project(cis_central)
|
||||
@@ -0,0 +1,55 @@
|
||||
| Supported Targets | ESP32-H4 |
|
||||
| ----------------- | -------- |
|
||||
|
||||
# BLE CIS Central Example
|
||||
|
||||
(See the README.md file in the upper level `examples` directory for more information about examples.)
|
||||
|
||||
This example demonstrates how to use a **Connected Isochronous Stream (CIS)** as a central. It scans for a peripheral, establishes an ACL connection, creates a Connected Isochronous Group (CIG) and a CIS to the peer, and then sends isochronous data over the CIS every 10 ms. Run it together with the [cis_peripheral](../cis_peripheral) example on another device as the peer.
|
||||
|
||||
The implementation uses the NimBLE host stack with ISO support and the ESP-BLE-ISO APIs (CIG create, CIS connect, data path, channel send). It is intended for chips that support BLE 5.2 ISO (e.g. ESP32-H4). The target peripheral name is hardcoded as `CIS Peripheral` and the connection security level is hardcoded as `ESP_BLE_ISO_SECURITY_MITM` (Level 3: encryption and authentication); the central initiates pairing after the ACL connection.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A board with Bluetooth LE 5.2 and ISO support (e.g. ESP32-H4)
|
||||
* Another device running the [cis_peripheral](../cis_peripheral) example, which advertises and accepts the ACL and CIS
|
||||
|
||||
## How to Use Example
|
||||
|
||||
Before project configuration and build, set the correct chip target:
|
||||
|
||||
```bash
|
||||
idf.py set-target esp32h4
|
||||
```
|
||||
|
||||
### Build and Flash
|
||||
|
||||
Run the following to build, flash and monitor:
|
||||
|
||||
```bash
|
||||
idf.py -p PORT flash monitor
|
||||
```
|
||||
|
||||
(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.
|
||||
|
||||
## Example Flow
|
||||
|
||||
1. **Initialization**: NVS, Bluetooth stack (NimBLE), and ISO common layer (`esp_ble_iso_common_init`) with GAP callback for scan, ACL, and security events.
|
||||
2. **Extended scan**: Start passive extended scanning. On scan report, parse the complete local name; when it matches the hardcoded target name `CIS Peripheral`, initiate an ACL connection to that device.
|
||||
3. **ACL connection**: On connection established, initiate pairing (security level is hardcoded as `ESP_BLE_ISO_SECURITY_MITM`); the CIG and CIS are created after security is established.
|
||||
4. **Security**: On security change (after pairing), create the CIG and CIS.
|
||||
5. **CIG and CIS**: Create a CIG with one CIS (10 ms latency each direction, 10 ms SDU interval, sequential packing, unframed), then connect the CIS over the ACL handle.
|
||||
6. **Send ISO data**: When the CIS is connected and the input data path is set up, a periodic TX scheduler based on `k_work_delayable` sends an SDU on the CIS every 10 ms in the ISO task context; sequence numbers and drift handling are applied. Note that the scheduler timer resolution is in milliseconds, which may not match the exact SDU interval for all configurations.
|
||||
|
||||
## Example Output
|
||||
|
||||
```
|
||||
I (xxx) CIS_CEN: connection established, handle 0 status 0x00
|
||||
I (xxx) CIS_CEN: ISO channel 0x... connected
|
||||
I (xxx) CIS_CEN: Transmitted 1000 ISO data packets (chan 0x...)
|
||||
...
|
||||
```
|
||||
|
||||
If connection or security fails, relevant status or error messages are logged.
|
||||
@@ -0,0 +1,4 @@
|
||||
set(srcs "main.c")
|
||||
|
||||
idf_component_register(SRCS "${srcs}"
|
||||
REQUIRES bt nvs_flash)
|
||||
@@ -0,0 +1,5 @@
|
||||
dependencies:
|
||||
example_init:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_iso/common_components/example_init
|
||||
example_utils:
|
||||
path: ${IDF_PATH}/examples/bluetooth/esp_ble_iso/common_components/example_utils
|
||||
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021-2022 Nordic Semiconductor ASA
|
||||
* SPDX-FileContributor: 2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
|
||||
#include "host/ble_gap.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
|
||||
#include "esp_ble_iso_common_api.h"
|
||||
|
||||
#include "ble_iso_example_init.h"
|
||||
#include "ble_iso_example_utils.h"
|
||||
|
||||
#define TAG "CIS_CEN"
|
||||
|
||||
#define LOCAL_DEVICE_NAME "CIS Central"
|
||||
|
||||
#define TARGET_DEVICE_NAME "CIS Peripheral"
|
||||
#define TARGET_DEVICE_NAME_LEN (sizeof(TARGET_DEVICE_NAME) - 1)
|
||||
|
||||
#define SCAN_INTERVAL 160 /* 100ms */
|
||||
#define SCAN_WINDOW 160 /* 100ms */
|
||||
|
||||
#define INIT_SCAN_INTERVAL 16 /* 10ms */
|
||||
#define INIT_SCAN_WINDOW 16 /* 10ms */
|
||||
#define CONN_INTERVAL 64 /* 64 * 1.25 = 80ms */
|
||||
#define CONN_LATENCY 0
|
||||
#define CONN_TIMEOUT 500 /* 500 * 10ms = 5s */
|
||||
#define CONN_MAX_CE_LEN 0xFFFF
|
||||
#define CONN_MIN_CE_LEN 0xFFFF
|
||||
#define CONN_DURATION 10000 /* 10s */
|
||||
|
||||
#define SECURITY_LEVEL ESP_BLE_ISO_SECURITY_MITM
|
||||
|
||||
#define CIG_LATENCY_MS 10 /* 10ms */
|
||||
#define CIG_SDU_INTERVAL_US 10000 /* 10ms */
|
||||
#define CIG_SCA ESP_BLE_ISO_SCA_UNKNOWN
|
||||
#define CIG_PACKING 0 /* 0 - sequential, 1 - interleaved */
|
||||
#define CIG_FRAMING 0 /* 0 - unframed, 1 - framed */
|
||||
#define CIS_PHY ESP_BLE_ISO_PHY_2M
|
||||
#define CIS_RTN 2
|
||||
#define CIS_SDU_SIZE 120
|
||||
|
||||
static bool acl_connected;
|
||||
|
||||
static uint16_t iso_seq_num;
|
||||
static uint8_t iso_data[CIS_SDU_SIZE];
|
||||
|
||||
static esp_ble_iso_cig_t *out_cig;
|
||||
|
||||
static example_iso_tx_scheduler_t tx_scheduler;
|
||||
|
||||
static void iso_chan_send(void);
|
||||
|
||||
static void iso_connected_cb(esp_ble_iso_chan_t *chan)
|
||||
{
|
||||
const esp_ble_iso_chan_path_t data_path = {
|
||||
.pid = ESP_BLE_ISO_DATA_PATH_HCI,
|
||||
.format = ESP_BLE_ISO_CODING_FORMAT_TRANSPARENT,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "ISO channel %p connected", chan);
|
||||
|
||||
err = esp_ble_iso_setup_data_path(chan, ESP_BLE_ISO_DATA_PATH_DIR_INPUT, &data_path);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to setup ISO data path, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
iso_seq_num = 0;
|
||||
example_iso_tx_scheduler_reset(&tx_scheduler);
|
||||
|
||||
/* Note: esp timer is not accurate enough */
|
||||
err = example_iso_tx_scheduler_start(&tx_scheduler, CIG_SDU_INTERVAL_US);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start tx scheduler, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
iso_chan_send();
|
||||
}
|
||||
|
||||
static void iso_disconnected_cb(esp_ble_iso_chan_t *chan, uint8_t reason)
|
||||
{
|
||||
esp_err_t err;
|
||||
|
||||
ESP_LOGI(TAG, "ISO channel %p disconnected, reason 0x%02x", chan, reason);
|
||||
|
||||
err = example_iso_tx_scheduler_stop(&tx_scheduler);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop tx scheduler, err %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
static void iso_sent_cb(esp_ble_iso_chan_t *chan, void *user_data)
|
||||
{
|
||||
example_iso_tx_scheduler_on_sent(&tx_scheduler, user_data, TAG, "chan", chan);
|
||||
}
|
||||
|
||||
static esp_ble_iso_chan_ops_t iso_ops = {
|
||||
.connected = iso_connected_cb,
|
||||
.disconnected = iso_disconnected_cb,
|
||||
.sent = iso_sent_cb,
|
||||
};
|
||||
|
||||
static esp_ble_iso_chan_io_qos_t iso_tx_qos = {
|
||||
.sdu = CIS_SDU_SIZE,
|
||||
.phy = CIS_PHY,
|
||||
.rtn = CIS_RTN,
|
||||
};
|
||||
|
||||
static esp_ble_iso_chan_qos_t iso_qos = {
|
||||
.tx = &iso_tx_qos,
|
||||
.rx = NULL,
|
||||
};
|
||||
|
||||
static esp_ble_iso_chan_t iso_chan = {
|
||||
.ops = &iso_ops,
|
||||
.qos = &iso_qos,
|
||||
.required_sec_level = SECURITY_LEVEL,
|
||||
};
|
||||
|
||||
static void create_cig_and_cis(uint16_t acl_handle)
|
||||
{
|
||||
esp_ble_iso_connect_param_t connect_param = {0};
|
||||
esp_ble_iso_cig_param_t cig_param = {0};
|
||||
esp_ble_iso_chan_t *channels[1] = {0};
|
||||
int err;
|
||||
|
||||
if (out_cig) {
|
||||
goto connect;
|
||||
}
|
||||
|
||||
channels[0] = &iso_chan;
|
||||
|
||||
cig_param.cis_channels = channels;
|
||||
cig_param.num_cis = ARRAY_SIZE(channels);
|
||||
cig_param.sca = CIG_SCA;
|
||||
cig_param.packing = CIG_PACKING;
|
||||
cig_param.framing = CIG_FRAMING;
|
||||
cig_param.c_to_p_latency = CIG_LATENCY_MS;
|
||||
cig_param.p_to_c_latency = CIG_LATENCY_MS;
|
||||
cig_param.c_to_p_interval = CIG_SDU_INTERVAL_US;
|
||||
cig_param.p_to_c_interval = CIG_SDU_INTERVAL_US;
|
||||
|
||||
err = esp_ble_iso_cig_create(&cig_param, &out_cig);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create CIG, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
connect:
|
||||
connect_param.iso_chan = &iso_chan;
|
||||
|
||||
err = esp_ble_iso_chan_connect(&connect_param, acl_handle, 1);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create CIS, err %d", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static void iso_chan_send(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
memset(iso_data, (uint8_t)iso_seq_num, sizeof(iso_data));
|
||||
|
||||
err = esp_ble_iso_chan_send(&iso_chan,
|
||||
iso_data,
|
||||
sizeof(iso_data),
|
||||
iso_seq_num);
|
||||
if (err) {
|
||||
ESP_LOGD(TAG, "Failed to transmit data on channel %p", &iso_chan);
|
||||
return;
|
||||
}
|
||||
|
||||
iso_seq_num++;
|
||||
}
|
||||
|
||||
static void tx_scheduler_cb(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
iso_chan_send();
|
||||
}
|
||||
|
||||
static void ext_scan_start(void)
|
||||
{
|
||||
struct ble_gap_disc_params params = {0};
|
||||
uint8_t own_addr_type;
|
||||
int err;
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
params.passive = 1;
|
||||
params.itvl = SCAN_INTERVAL;
|
||||
params.window = SCAN_WINDOW;
|
||||
|
||||
err = ble_gap_disc(own_addr_type, BLE_HS_FOREVER, ¶ms,
|
||||
example_iso_gap_event_cb, NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to start scanning, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Extended scan started");
|
||||
}
|
||||
|
||||
static int conn_create(uint8_t addr_type, uint8_t addr[6])
|
||||
{
|
||||
struct ble_gap_conn_params params = {0};
|
||||
uint8_t own_addr_type = 0;
|
||||
ble_addr_t dst = {0};
|
||||
int err;
|
||||
|
||||
err = ble_gap_disc_cancel();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to stop scanning, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = ble_hs_id_infer_auto(0, &own_addr_type);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to determine address type, err %d", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
params.scan_itvl = INIT_SCAN_INTERVAL;
|
||||
params.scan_window = INIT_SCAN_WINDOW;
|
||||
params.itvl_min = CONN_INTERVAL;
|
||||
params.itvl_max = CONN_INTERVAL;
|
||||
params.latency = CONN_LATENCY;
|
||||
params.supervision_timeout = CONN_TIMEOUT;
|
||||
params.max_ce_len = CONN_MAX_CE_LEN;
|
||||
params.min_ce_len = CONN_MIN_CE_LEN;
|
||||
|
||||
dst.type = addr_type;
|
||||
memcpy(dst.val, addr, sizeof(dst.val));
|
||||
|
||||
return ble_gap_connect(own_addr_type, &dst, CONN_DURATION, ¶ms,
|
||||
example_iso_gap_event_cb, NULL);
|
||||
}
|
||||
|
||||
static int pairing_start(uint16_t conn_handle)
|
||||
{
|
||||
return ble_gap_security_initiate(conn_handle);
|
||||
}
|
||||
|
||||
static bool data_cb(uint8_t type, const uint8_t *data,
|
||||
uint8_t data_len, void *user_data)
|
||||
{
|
||||
bool *target_matched = user_data;
|
||||
|
||||
assert(target_matched);
|
||||
|
||||
switch (type) {
|
||||
case EXAMPLE_AD_TYPE_NAME_COMPLETE:
|
||||
*target_matched = (data_len == TARGET_DEVICE_NAME_LEN) &&
|
||||
!memcmp(data, TARGET_DEVICE_NAME, TARGET_DEVICE_NAME_LEN);
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static void ext_scan_recv(esp_ble_iso_gap_app_event_t *event)
|
||||
{
|
||||
bool target_matched = false;
|
||||
int err;
|
||||
|
||||
esp_ble_iso_data_parse(event->ext_scan_recv.data,
|
||||
event->ext_scan_recv.data_len,
|
||||
data_cb, &target_matched);
|
||||
|
||||
/* Skip if target not found or ACL already connected */
|
||||
if (!target_matched || acl_connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
err = conn_create(event->ext_scan_recv.addr.type, event->ext_scan_recv.addr.val);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to create connection, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
acl_connected = true;
|
||||
}
|
||||
|
||||
static void acl_connect(esp_ble_iso_gap_app_event_t *event)
|
||||
{
|
||||
int err;
|
||||
|
||||
if (event->acl_connect.status) {
|
||||
ESP_LOGE(TAG, "connection failed, status %d", event->acl_connect.status);
|
||||
acl_connected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Conn established:");
|
||||
ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u peer %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
event->acl_connect.conn_handle, event->acl_connect.status,
|
||||
event->acl_connect.role, event->acl_connect.dst.val[5],
|
||||
event->acl_connect.dst.val[4], event->acl_connect.dst.val[3],
|
||||
event->acl_connect.dst.val[2], event->acl_connect.dst.val[1],
|
||||
event->acl_connect.dst.val[0]);
|
||||
|
||||
if (iso_chan.required_sec_level == ESP_BLE_ISO_SECURITY_NO_MITM ||
|
||||
iso_chan.required_sec_level == ESP_BLE_ISO_SECURITY_MITM) {
|
||||
err = pairing_start(event->acl_connect.conn_handle);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initiate security, err %d", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
create_cig_and_cis(event->acl_connect.conn_handle);
|
||||
}
|
||||
|
||||
static void acl_disconnect(esp_ble_iso_gap_app_event_t *event)
|
||||
{
|
||||
ESP_LOGI(TAG, "Conn terminated: conn_handle 0x%04x reason 0x%02x",
|
||||
event->acl_disconnect.conn_handle, event->acl_disconnect.reason);
|
||||
|
||||
acl_connected = false;
|
||||
|
||||
ext_scan_start();
|
||||
}
|
||||
|
||||
static void security_change(esp_ble_iso_gap_app_event_t *event)
|
||||
{
|
||||
if (event->security_change.status) {
|
||||
ESP_LOGE(TAG, "security change failed, status %d", event->security_change.status);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Security change:");
|
||||
ESP_LOGI(TAG, "conn_handle 0x%04x status 0x%02x role %u sec_level %u bonded %u "
|
||||
"peer %02x:%02x:%02x:%02x:%02x:%02x",
|
||||
event->security_change.conn_handle, event->security_change.status,
|
||||
event->security_change.role, event->security_change.sec_level,
|
||||
event->security_change.bonded, event->security_change.dst.val[5],
|
||||
event->security_change.dst.val[4], event->security_change.dst.val[3],
|
||||
event->security_change.dst.val[2], event->security_change.dst.val[1],
|
||||
event->security_change.dst.val[0]);
|
||||
|
||||
create_cig_and_cis(event->security_change.conn_handle);
|
||||
}
|
||||
|
||||
static void iso_gap_app_cb(esp_ble_iso_gap_app_event_t *event)
|
||||
{
|
||||
switch (event->type) {
|
||||
case ESP_BLE_ISO_GAP_EVENT_EXT_SCAN_RECV:
|
||||
ext_scan_recv(event);
|
||||
break;
|
||||
case ESP_BLE_ISO_GAP_EVENT_ACL_CONNECT:
|
||||
acl_connect(event);
|
||||
break;
|
||||
case ESP_BLE_ISO_GAP_EVENT_ACL_DISCONNECT:
|
||||
acl_disconnect(event);
|
||||
break;
|
||||
case ESP_BLE_ISO_GAP_EVENT_SECURITY_CHANGE:
|
||||
security_change(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(void)
|
||||
{
|
||||
esp_ble_iso_init_info_t info = {
|
||||
.gap_cb = iso_gap_app_cb,
|
||||
};
|
||||
esp_err_t err;
|
||||
|
||||
/* Initialize NVS — it is used to store PHY calibration data */
|
||||
err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
|
||||
err = bluetooth_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize BLE, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_ble_iso_common_init(&info);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize ISO, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ble_svc_gap_device_name_set(LOCAL_DEVICE_NAME);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to set device name, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = example_iso_tx_scheduler_init(&tx_scheduler,
|
||||
tx_scheduler_cb,
|
||||
NULL);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to init tx scheduler, err %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
ext_scan_start();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
|
||||
CONFIG_BT_ENABLED=y
|
||||
CONFIG_BT_BLUEDROID_ENABLED=n
|
||||
CONFIG_BT_NIMBLE_ENABLED=y
|
||||
CONFIG_BT_NIMBLE_EXT_ADV=y
|
||||
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
|
||||
CONFIG_BT_NIMBLE_ISO=y
|
||||
CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING=y
|
||||
|
||||
CONFIG_BT_ISO_CENTRAL=y
|
||||
|
||||
CONFIG_FREERTOS_HZ=1000
|
||||
@@ -0,0 +1,6 @@
|
||||
# Override some defaults so BT stack is enabled
|
||||
# by default in this example
|
||||
|
||||
CONFIG_IDF_TARGET="esp32h4"
|
||||
|
||||
CONFIG_BT_LE_ISO_SUPPORT=y
|
||||
Reference in New Issue
Block a user