| Supported Targets | ESP32-H4 | ESP32-S31 |
|---|
BLE CIS Central Example
(See the README.md file in the upper level examples directory for more information about examples.)
Overview
This is a raw BLE Connected Isochronous Stream (CIS) example operating directly at the ISO transport layer over either the NimBLE or Bluedroid host (selected at build time via Kconfig). It is not a BAP/CAP (BLE Audio profile) example — it does not implement Unicast Server/Client, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIG/CIS plumbing.
The central scans for a peer advertising the name CIS Peripheral, opens an ACL link, optionally pairs (security level ESP_BLE_ISO_SECURITY_NO_MITM), creates a two-CIS CIG (10 ms SDU interval, 2M PHY, RTN 2, 120-byte SDU, sequential/unframed), connects both CIS, configures each input data path to the HCI in transparent format, and then drives one software TX scheduler per CIS that submits one SDU every 10 ms.
Both CIS ride the same ACL connection to the same peer, which is what esp_ble_iso_chan_connect() expresses: it takes one ACL handle plus a channel count. Each stream is otherwise independent — its own timer, sequence numbering and TX counters — because the controller establishes them one after another, so a shared scheduler would send on a CIS that is not up yet and would report both streams under a single name.
The transmitted payload is a dummy buffer filled with the current sequence number byte — there is no real audio data, the example just demonstrates the ISO transport mechanics via the esp_ble_iso_* APIs.
Bluedroid host status (phase 1): scanning and AUTH_CMPL forwarding work; ACL connection initiation and pairing kick-off (
conn_create/pairing_start) currently returnESP_ERR_NOT_SUPPORTEDand will be wired up against the public Bluedroid APIs in a follow-up. Use the NimBLE overlay for end-to-end runtime testing.
Requirements
- A board with BLE 5.2 and ISO support (e.g. ESP32-H4, ESP32-S31)
- Peer device running the paired example
Configuration
idf.py menuconfig
No menuconfig options — runtime defaults are baked into source. The number of CIS is the CIS_COUNT macro in main/main.c; raising it also requires CONFIG_BT_ISO_MAX_CHAN in sdkconfig.defaults to be at least that value (the host default is 1, so a second CIS is rejected without it).
Security & Pairing
Just-Works pairing (LE Secure Connections, no MITM, IO capability = None) with bonding enabled. On NimBLE the configuration is inherited from the shared host init in ../common_components/example_init/ble_iso_example_init.c; on Bluedroid the equivalent SMP setup will live in main/bluedroid/scan.c once pairing_start is implemented. ISO examples do not register any custom GATT services.
Build & Flash
The base sdkconfig.defaults defaults to the Bluedroid host; idf.py automatically merges the per-target overlay (sdkconfig.defaults.$IDF_TARGET). To build with NimBLE host instead, layer sdkconfig.defaults.nimble on top via -DSDKCONFIG_DEFAULTS.
Bluedroid host (default)
idf.py set-target esp32h4
idf.py -p PORT flash monitor
NimBLE host
idf.py set-target esp32h4
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32h4;sdkconfig.defaults.nimble" -p PORT flash monitor
For esp32s31, replace the chip overlay accordingly.
(Exit serial monitor with Ctrl-].)
Example Flow
- Initialize NVS, the selected BLE host, and the ISO common layer with a GAP callback.
- Start passive extended scanning for a device whose Complete Local Name is
CIS Peripheral. - Cancel scan and create an ACL connection (interval 80 ms, supervision 5 s) once the target is matched.
- On ACL connect, initiate pairing because the configured security level is
ESP_BLE_ISO_SECURITY_NO_MITM. - After the security change, call
esp_ble_iso_cig_create(two CIS, 10 ms latencies and SDU interval, sequential/unframed, SCA unknown) andesp_ble_iso_chan_connectonce for both channels over the same ACL handle. - The controller establishes the CIS one at a time. On each CIS connect, set up that channel's input data path (HCI / transparent) and start its own periodic TX scheduler.
- Each scheduler invokes
esp_ble_iso_chan_sendevery 10 ms on its own channel, with a per-CIS incrementing sequence number on a 120-byte dummy SDU.
Expected Log
TAG: CIS_CEN
Scan and connection phase:
I CIS_CEN: Scanning for peripheral...
I CIS_CEN: Connected: handle <h> role <r> peer XX:XX:XX:XX:XX:XX
I CIS_CEN: Security: handle <h> level <l> bonded <b>
CIS setup and streaming phase. The two CIS come up a couple of hundred milliseconds apart, and each reports its own count (TX log emitted every LOG_INTERVAL_PACKETS SDUs by the shared utility):
I CIS_CEN: [CIS #0] Connected
I CIS_CEN: [CIS #1] Connected
I CIS_CEN: [CIS #0] TX: <count> packets
I CIS_CEN: [CIS #1] TX: <count> packets
There should be no IsoSendChanNotConn errors in the gap between the two Connected lines: each CIS only starts its timer once it is established, so nothing is submitted to a stream that is still connecting.
Disconnect path (per CIS, then the ACL):
I CIS_CEN: [CIS #0] Disconnected, reason 0x<rr>
I CIS_CEN: [CIS #1] Disconnected, reason 0x<rr>
I CIS_CEN: Disconnected: handle <h> reason 0x<rr>
Peer Pairing
Run cis_peripheral on a second board.
- Flash and run
cis_peripheralfirst; it begins extended advertising asCIS Peripheral. - Flash and run
cis_centralon the second board; it scans and matches that name. - The central creates the ACL connection and initiates pairing.
- After the security change, the central creates the CIG and connects both CIS over that one ACL.
- Both sides set up a data path per CIS (input on central, output on peripheral).
- The central streams 120-byte SDUs every 10 ms on each CIS; the peripheral reports
RX: <count> packetsper CIS periodically.