| Supported Targets | ESP32-H4 | ESP32-S31 |
|---|
BLE CIS Peripheral 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, ASCS, PACS, or any LC3 codec; it only exercises the underlying CIS accept/receive plumbing.
The peripheral starts connectable extended advertising under the name CIS Peripheral (1M primary / 2M secondary PHY, 200 ms interval), registers an ISO server with security level ESP_BLE_ISO_SECURITY_NO_MITM and an accept callback that hands out the next free CIS channel slot, sets up the output data path to the HCI in transparent format when each CIS connects, and tallies received SDUs through the shared RX-metrics helper.
It accepts two CIS from the central, both on the same ACL connection. The central establishes them one after another, so the accept callback is invoked twice and returns a different channel each time; each channel keeps its own RX metrics so the two streams are reported separately.
There is no audio decoding — incoming SDUs are simply counted (valid / error / lost / null), so any 120-byte dummy payload from the central is consumed as opaque data via the esp_ble_iso_* APIs.
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 accepted is the CIS_COUNT macro in main/main.c; it must be matched by CONFIG_BT_ISO_MAX_CHAN in sdkconfig.defaults (the host default is 1, so a second CIS would be rejected) and by the central's own CIS_COUNT.
Security & Pairing
Just-Works pairing (LE Secure Connections, no MITM, IO capability = None) with bonding enabled. The configuration is set up in the shared host init ../common_components/example_init/ble_iso_example_init.c for both NimBLE (ble_hs_cfg.sm_*) and Bluedroid (esp_ble_gap_set_security_param()). 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.
- Register an ISO server (
esp_ble_iso_server_register) whose accept callback returns the next CIS channel that is not already carrying one. - Build flags + complete-local-name AD payload and start connectable extended advertising on handle 0 at a 200 ms interval.
- On ACL connect, log the peer; the central drives subsequent pairing and CIG/CIS creation.
- Each CIS request invokes the accept callback, which supplies a free channel; on CIS connect, set up that channel's output data path (HCI / transparent) and reset its RX metrics.
- Each received SDU updates the valid/error/lost counters of the channel it arrived on; periodic per-CIS RX summaries are logged by the shared utility.
- On ACL disconnect, restart extended advertising.
Expected Log
TAG: CIS_PER
Advertising and connection phase:
I CIS_PER: Advertising started (handle 0)
I CIS_PER: Connected: handle <h> role <r> peer XX:XX:XX:XX:XX:XX
I CIS_PER: Security: handle <h> level <l> bonded <b>
CIS accept and streaming phase. Both requests arrive on the same ACL handle, one after the other, and each CIS reports its own count (RX log emitted every LOG_INTERVAL_PACKETS SDUs by the shared utility):
I CIS_PER: Incoming CIS request from handle <h>
I CIS_PER: [CIS #0] Connected
I CIS_PER: Incoming CIS request from handle <h>
I CIS_PER: [CIS #1] Connected
I CIS_PER: [CIS #0] RX: <count> packets
I CIS_PER: [CIS #1] RX: <count> packets
A No channels available error on a request means more CIS were offered than CIS_COUNT provides for.
Disconnect path (per CIS, then the ACL):
I CIS_PER: [CIS #0] Disconnected, reason 0x<rr>
I CIS_PER: [CIS #1] Disconnected, reason 0x<rr>
I CIS_PER: Disconnected: handle <h> reason 0x<rr>
Peer Pairing
Run cis_central on a second board.
- Flash and run
cis_peripheralfirst; it advertises asCIS Peripheralon extended-advertising handle 0. - Flash and run
cis_centralon the second board; it scans and matches the name. - The central opens the ACL connection and initiates pairing; this peripheral handles the security change passively.
- The central creates the CIG and connects both CIS over that one ACL; the peripheral's accept callback returns a free local channel for each.
- The peripheral configures an output data path per CIS as each one connects.
- Incoming 120-byte SDUs are counted per CIS and
[CIS #<n>] RX: <count> packetsis logged periodically for each.