| Supported Targets | ESP32-H4 | ESP32-S31 |
|---|
TMAP Peripheral Example
(See the README.md file in the upper level examples directory for more information about examples.)
Overview
This example takes the TMAP Call Terminal (CT) and Unicast Media Receiver (UMR) roles, registered together via esp_ble_audio_tmap_register(ESP_BLE_AUDIO_TMAP_ROLE_CT | ESP_BLE_AUDIO_TMAP_ROLE_UMR). It runs connectable extended advertising at a 200 ms interval that includes the GAP Earbud appearance, the ASCS/CAS/TMAS UUIDs, an ASCS targeted unicast announcement carrying sink and source contexts (UNSPECIFIED | CONVERSATIONAL | MEDIA | GAME | INSTRUCTIONAL), a CAS targeted announcement, the TMAS UMR|CT role payload, and the device name tmap_peripheral. When the Duo earbuds option is selected, a CSIS RSI is added to the advertisement.
The example uses the ESP-BLE-AUDIO library pieces for: BAP Unicast Server with PACS (LC3 cap with 16/32/48 kHz, 7.5/10 ms duration, 30–155 octets), VCP Volume Renderer (initial volume 10, unmuted, with VOCS+AICS instances built from Kconfig counts), TBS client (CCP Call Terminal — discovers GTBS, reads URI list, originates/terminates calls), MCC controller (reads player name, track title/duration/position, playback/seeking speed, playing order, media state, opcodes, CCID, plus a mcp_send_cmd for PLAY/PAUSE), and optionally CSIP Set Member with SIRK and rank from Kconfig. Sink streams are auto-started from the enabled callback. Device name is set to TMAP Peripheral.
Requirements
- A board with BLE 5.2, ISO, and LE Audio support (e.g. ESP32-H4, ESP32-S31)
- Peer device running the paired example
Configuration
idf.py menuconfig
This board must be built to match the topology tmap_central is built for. Neither side negotiates it: the central looks for a fixed number of peers, each exposing a fixed number of sink ASEs with a specific channel. A mismatch stalls in discovery or fails to build the unicast group.
| Central Unicast topology | Boards of this example | Earbuds type | Earbud Location |
|---|---|---|---|
1 peripheral, 1 CIS (TX+RX) |
1 | Single headset |
Mono |
1 peripheral, 2 CISes (TX+RX, TX) (default) |
1 | Single headset (default) |
Stereo (default) |
2 peripherals (coordinated set), 2 CISes (TX+RX, TX) |
2 | Duo headset on both |
Left on one, Right on the other |
Under Example: TMAP Peripheral (CT & UMR):
- Earbuds type —
Single headset(standalone: no CSIS, no RSI) orDuo headset(selectsBT_CSIP_SET_MEMBERandBT_CAP_ACCEPTOR_SET_MEMBER, enabling CSIS and adding RSI to the advertisement). - Earbud Location — sets the PACS sink and source location bitmap, and this is what the central uses to pick the channel it sends here. The options offered follow the earbuds type, so the two choices together can only express a topology the central supports:
- with
Single headset— a whole device, so mono or stereo:Mono— empty bitmap; the central sends one mono stream. Pair with the central's1 peripheral, 1 CIStopology.Stereo(default) —FRONT_LEFT | FRONT_RIGHT, so one board renders both channels. Matches the central's default1 peripheral, 2 CISestopology.
- with
Duo headset— one of a pair, so left or right:Left(default) —FRONT_LEFT, and rank 1.Right—FRONT_RIGHT, and rank 2.
- with
- Device rank in set — integer 1–2, only when Duo is selected; written into the CSIS register parameters. The CSIS instance is registered as lockable, which requires a non-zero rank that is unique within the set: duplicate ranks break the CSIP ordered access procedure the central locks the set with. The default follows Earbud Location (left → 1, right → 2), and a set member can only be left or right, so building the two boards for their respective ears already gives them distinct ranks. Rank orders the set lock and decides which member the central gives its bidirectional CIS to; it does not say which ear this board is.
The ASE counts come from sdkconfig.defaults: two sink ASEs and one source ASE, which is what the central's topologies need (two sinks on one device, or one sink per device, plus a source for the bidirectional CIS). Each ASE gets its own stream slot, RX metrics and — for source ASEs — its own TX scheduler, buffer and sequence number.
CONFIG_BT_ASCS_MAX_ACTIVE_ASES is pinned to 3 there as well, and it is not the same thing as the CIS count. It bounds how many ASEs may be non-idle at once, and a bidirectional CIS carries two ASEs on one CIS — so 1 peripheral, 2 CISes needs 3 active ASEs (two sinks plus one source) over only 2 CIS. Its Kconfig default is BT_ISO_MAX_CHAN, i.e. 2, which is one short; leaving it at the default makes the third codec configuration fail with ASCS response 0x0D (Insufficient Resources) and AscsAseNewFail in the library log, and the central reports Unicast start completed, err -77.
Security & Pairing
Just-Works pairing (LE Secure Connections, no MITM, no I/O capability) with bonding enabled, inherited from ../../common_components/example_init/ble_audio_example_init.c.
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.
A coordinated set needs this example on two boards, one built for the Left
ear and the other for Right.
(Exit serial monitor with Ctrl-].)
Example Flow
app_maininitializes NVS, Bluetooth, and the audio common layer, then registers TMAP CT+UMR.- If
BT_CSIP_SET_MEMBERis on, the CSIP set member registers via the CAP acceptor, generates an RSI, and is plumbed intostart_info.csis_insts[0]. - VCP volume renderer, BAP unicast server (PACS, ASCS callbacks, stream ops), CCP call control (TBS client), and MCP controller (MCC) are initialized.
- The audio stack starts, device name is set, and extended advertising starts on handle 0.
- On ACL connect the connection handle is stored; on MTU change GATT service discovery starts; on discovery complete
tmap_discover_tmasruns. - TMAP discovery records whether the peer is CG and/or UMS, then
ccp_discover_tbstriggers TBS discovery. - After GTBS is discovered the TBS URI list is read;
read_uri_schemes_string_cbsaves the first URI and chains intomcp_discover_mcs. - MCS discovery walks player name → track title → duration → position → playback speed → seeking speed → playing order → orders supported → media state → opcodes → CCID.
- ASCS callbacks accept config/QoS/enable/start; the
enabledstream op auto-starts sink streams (receiver-start-ready); RX metrics are reset on start and updated per received SDU, per sink ASE. - A source ASE that reaches "started" arms its own TX scheduler at the QoS interval and transmits dummy SDUs back to the central — this is the uplink half of the central's bidirectional CIS. The scheduler and buffer are released when the stream stops, when its CIS disconnects, or when the ASE is released.
- Helpers
initiate_call/terminate_call(gated on peer CG) andplay_media/pause_media(gated on peer UMS) issue TBS and MCC commands. - On disconnect, the connection handle is cleared and advertising restarts.
Expected Log
Initialization phase:
TMAP_PER: TMAP CT UMR initialized
TMAP_PER: CSIP set member initialized
TMAP_PER: PRSI: 0x<hex>
TMAP_PER: vcp vol renderer, vocs_cnt <n> aics_cnt <n>
TMAP_PER: VCP volume renderer initialized
TMAP_PER: BAP unicast server initialized: 2 sink / 1 source ASE
TMAP_PER: CCP call controller initialized
TMAP_PER: MCP controller initialized
TMAP_PER: Advertising started (handle 0)
Connection / discovery:
TMAP_PER: Connected: handle <h> role <r> peer <addr>
TMAP_PER: Security: handle <h> level <l> bonded <b>
TMAP_PER: MTU updated: handle <h> mtu <m>
TMAP_PER: Service discovery started: handle <h>
TMAP_PER: Service discovery complete: handle <h> status 0
TMAP_PER: TMAP discovery done
TMAP_PER: Discovered GTBS
TMAP_PER: Discovered remote URI <uri>
TMAP_PER: Discover MCS succeeded
TMAP_PER: Read player name succeeded, name <name>
TMAP_PER: Read track title succeeded, title <title>
TMAP_PER: Read media state succeeded, state <s>
TMAP_PER: Read content control id succeeded, ccid <c>
ASCS / unicast streaming. Which streams appear depends on the topology the central was built for — a second SNK #1 when it drives two CISes onto this device, and SRC #0 whenever it pairs a source into the bidirectional CIS:
TMAP_PER: [SNK] Config request:
TMAP_PER: [SNK #0] QoS request:
TMAP_PER: [SNK #0] Enable request (meta_len <n>)
TMAP_PER: [SNK #0] Stream enabled
TMAP_PER: [SNK #0] Start request
TMAP_PER: [SNK #0] Stream started
TMAP_PER: [SNK #0] RX: <count> packets
TMAP_PER: [SRC #0] Stream started
TMAP_PER: [SRC #0] TX: <count> packets
TMAP_PER: [SNK #0] Stop request
TMAP_PER: [SNK #0] Stream stopped, reason 0x<rr>
TMAP_PER: [SNK #0] Disable request
TMAP_PER: [SNK #0] Release request
Each ASE keeps its own counters, so [SNK #0] and [SNK #1] advance independently rather than sharing one total.
VCP / call control / disconnect:
TMAP_PER: VCS volume <v>, mute <m>
TMAP_PER: VCS flags 0x<ff>
TMAP_PER: Call <i> originated
TMAP_PER: Call <i> terminated
TMAP_PER: Disconnected: handle <h> reason 0x<rr>
Note — disconnect race. On peer-initiated disconnect (e.g. supervision timeout, peer drops the link) with active streams, an additional Bluedroid error may interleave:
W BT_APPL: gattc_conn_cb: if=4 st=0 id=4 rsn=0x8
W BT_HCI: hcif disc complete: hdl 0x0, rsn 0x8 dev_find 1
TMAP_PER: [SNK #0] ISO disconnected, reason 0x08
TMAP_PER: [SNK #0] Stream disabled
E BT_APPL: Unknown connection ID: 3 fail sending notification
TMAP_PER: [SNK #0] Stream stopped, reason 0x08
Unknown connection ID is harmless. GATT notifications for the ASE state changes are queued for Bluedroid to send. If Bluedroid clears the BTA connection on the ACL disconnect before those queued sends drain, they fail with this error. The peer has already disconnected, so the missed notifications have no effect on either side.
Tag is TMAP_PER.
Peer Pairing
Run tmap_central on another board. How many boards of this example are needed, and how they must be configured, follows the topology the central is built for:
| Central topology | Boards of this example | Configuration |
|---|---|---|
1P_1CIS |
1 | Single headset with the Mono location, so the central sends one mono stream |
1P_2CIS |
1 | Stereo location, so PACS advertises both channels |
2P_2CIS |
2 | Duo headset on both, Left on one and Right on the other — the rank default follows, giving them 1 and 2 |
- Flash this peripheral and start it; advertising begins on handle 0.
- Flash and start the central; it scans for TMAS+UMR (plus a matching CSIS RSI in a coordinated-set build) and connects to this device.
- After pairing and MTU exchange, both sides complete TMAP discovery; this peripheral then discovers GTBS and reads the URI list.
- The MCC chain reads the central's media proxy state; the central's CAP initiator configures and starts the streams.
- The peripheral auto-starts each sink stream from the
enabledcallback and logs its received SDU metrics; a configured source stream transmits back on the bidirectional CIS. - Use
initiate_call/terminate_callto drive the central's TBS, andplay_media/pause_mediato send PLAY/PAUSE via MCC.
For the two-board set, the CSIS instance is lockable, so the central takes the set lock by rank before configuring the streams and releases it afterwards. Each board logs Client <p> locked the lock / ... released the lock.