EmbeddedPathashala · Bluetooth LE Audio Series · Part 3
CSIS Inclusion, SDP Interoperability Record, and Complete BlueZ Implementation
Coordinated Sets — The Concept
A Coordinated Set is a group of Bluetooth devices that function together as a single logical audio unit. The most familiar example is a pair of True Wireless Stereo (TWS) earbuds — left and right — that the user perceives as one device. Each earbud is a separate BLE device with its own MAC address and GATT server, but from the CAP Initiator’s perspective they should be treated as a unit: same volume, same stream start/stop, same lock state.
The Coordinated Set Identification Service (CSIS) provides the mechanism for this grouping. CAS links to CSIS through an Include Declaration, giving the CAP Initiator the path to read the set membership information immediately after discovering the device is a CAP Acceptor.
CSIS Characteristics — What CAS Includes
When a device is part of a Coordinated Set, CAS includes CSIS via an Include Declaration. The CSIS service itself has three characteristics. Understanding these is essential because the CAP Initiator reads them to discover and connect to all members of the set.
| Characteristic | UUID | Size | Description | Required |
|---|---|---|---|---|
| Set Identity Resolving Key (SIRK) | 0x2B84 | 17 bytes | 1 byte type (Encrypted SIRK = 0x01) + 16 bytes key. The key is used by the Initiator to identify and authenticate other members of the same set. | Mandatory |
| Coordinated Set Size | 0x2B85 | 1 byte | Total number of members in the Coordinated Set. For a pair of earbuds, this is 0x02. The Initiator uses this to know when it has connected to all members. | Optional |
| Member Lock | 0x2B86 | 1 byte | 0x01 = Unlocked, 0x02 = Locked. The Initiator writes Lock before starting audio procedures to prevent another Initiator from connecting and interfering. | Optional |
| Member Rank | 0x2B87 | 1 byte | Unique rank within the set (1 = first member, 2 = second, etc.). Helps the Initiator assign roles (e.g. left earbud = rank 1, right = rank 2). | Optional |
Full Picture: CAS + CSIS for a TWS Earbud Pair
|
Left Earbud — GATT Server
CAS UUID: 0x1853
Primary Service
↳ Include Declaration (0x2802)
Points to CSIS at handle 0x0030
CSIS UUID: 0x1846
— SIRK: TYPE=01 KEY=AB…XY
— Set Size: 0x02
— Member Rank: 0x01 (Left = rank 1)
— Member Lock: 0x01 (Unlocked)
+ PACS, ASCS, VCP, MCS…
|
SIRK match
⇄
Same key on both devices identifies them as a pair
Coordinated Set
Size = 2
Same SIRK |
Right Earbud — GATT Server
CAS UUID: 0x1853
Primary Service
↳ Include Declaration (0x2802)
Points to CSIS at handle 0x0030
CSIS UUID: 0x1846
— SIRK: TYPE=01 KEY=AB…XY ✓ Same!
— Set Size: 0x02
— Member Rank: 0x02 (Right = rank 2)
— Member Lock: 0x01 (Unlocked)
+ PACS, ASCS, VCP, MCS…
|
ⓘ Both earbuds have identical SIRK values. The CAP Initiator reads the SIRK from the first earbud, then scans for any advertising device that matches that SIRK — that is how it finds the second earbud.
SDP Interoperability Record
CAS is primarily a BLE service. However, when a device also supports BR/EDR (Classic Bluetooth) and exposes CAS over BR/EDR using ATT over L2CAP, it must also register a corresponding SDP record. This allows Classic Bluetooth devices to discover CAS using the Service Discovery Protocol (SDP) — the traditional BR/EDR service discovery mechanism.
| SDP Attribute | Type | Value | Status |
|---|---|---|---|
| Service Class ID List | — | Container | M |
| ↳ Service Class #0 | UUID | «Common Audio Service» (0x1853) | M |
| Protocol Descriptor List | Data Element Sequence | Container | M |
| ↳ Protocol #0 | UUID | L2CAP | M |
| Parameter: PSM | uint16 | PSM = ATT (0x001F) | M |
| ↳ Protocol #1 | UUID | ATT | M |
| Additional Protocol Descriptor List | Data Element Sequence | For EATT support | C.1 |
| ↳ Protocol #0 (EATT) | UUID | L2CAP, PSM = EATT | C.1 |
| BrowseGroupList | — | PublicBrowseRoot | M |
BlueZ Implementation
In BlueZ, the CAS Acceptor role is implemented in profiles/audio/cas.c. The service is registered in the GATT database using BlueZ’s internal gatt-db API. Below are the core code patterns you will see when working with this file.
/* profiles/audio/cas.c */
#include <stdint.h>
#include <stdbool.h>
#include "lib/bluetooth.h"
#include "lib/uuid.h"
#include "src/shared/gatt-db.h"
#include "src/shared/gatt-server.h"
#include "profiles/audio/csis.h"
/* 16-bit UUID for Common Audio Service */
#define CAS_UUID16 0x1853
/* 16-bit UUID for Coordinated Set Identification Service */
#define CSIS_UUID16 0x1846
/* Per-adapter CAS data */
struct cas_data {
struct btd_adapter *adapter;
struct gatt_db *db;
struct gatt_db_attribute *service; /* CAS primary service attr */
struct gatt_db_attribute *csis_attr; /* Pointer to CSIS service */
bool is_set_member; /* Part of Coordinated Set? */
};
The gatt_db_add_service() call creates the Primary Service declaration attribute with the specified UUID. The third argument true marks it as a primary service. The fourth argument is the number of attribute handles to reserve.
static void cas_add_service(struct cas_data *data)
{
struct gatt_db_attribute *service;
bt_uuid_t uuid;
/*
* Create the CAS Primary Service declaration.
* UUID: 0x1853
* is_primary: true
* num_handles: 4 — enough for:
* 1 x Primary Service Declaration
* 1 x Include Declaration (CSIS, conditional)
* 2 x spare handles
*/
bt_uuid16_create(&uuid, CAS_UUID16);
service = gatt_db_add_service(data->db, &uuid, true, 4);
if (!service) {
error("CAS: Failed to create service in GATT database");
return;
}
data->service = service;
/*
* CAS has no characteristics of its own.
* If this device is part of a Coordinated Set,
* we add an Include Declaration pointing to CSIS.
*/
if (data->is_set_member && data->csis_attr) {
cas_include_csis(data);
}
/* Activate the service so it is visible to remote clients */
gatt_db_service_set_active(service, true);
}
The gatt_db_service_add_included() API adds an Include Declaration (UUID 0x2802) attribute to the parent service (CAS), storing the handle range of the child service (CSIS). This is the BlueZ mechanism for the GATT Include procedure.
static void cas_include_csis(struct cas_data *data)
{
struct gatt_db_attribute *included;
/*
* Add an Include Declaration inside CAS pointing to CSIS.
*
* gatt_db_service_add_included() adds attribute type 0x2802
* (Include Declaration) to data->service (CAS), with its value
* containing the start handle, end handle, and UUID of
* data->csis_attr (CSIS).
*
* The spec rule: CAS shall include no more than one CSIS instance.
* We call this at most once, guarded by is_set_member check.
*/
included = gatt_db_service_add_included(data->service,
data->csis_attr);
if (!included) {
error("CAS: Failed to add CSIS as included service");
return;
}
DBG("CAS: CSIS included (device is Coordinated Set member)");
}
The adapter_probe callback is called by BlueZ when the Bluetooth adapter is ready. This is where CAS initialises its data and calls cas_add_service().
static int cas_adapter_probe(struct btd_adapter *adapter)
{
struct cas_data *data;
struct gatt_db *db;
db = btd_adapter_get_gatt_db(adapter);
if (!db) {
error("CAS: Cannot get GATT database from adapter");
return -EINVAL;
}
data = g_new0(struct cas_data, 1);
data->adapter = btd_adapter_ref(adapter);
data->db = gatt_db_ref(db);
/*
* Determine if this device is configured as a Coordinated Set
* member (e.g. via /etc/bluetooth/main.conf or a config file).
* In practice, this is determined by whether CSIS has been
* registered by the CSIS plugin before CAS initialises.
*/
data->csis_attr = csis_get_service_attribute(adapter);
data->is_set_member = (data->csis_attr != NULL);
/* Register CAS in the GATT database */
cas_add_service(data);
btd_adapter_set_data(adapter, data);
DBG("CAS: Registered on adapter %s (set_member=%d)",
btd_adapter_get_path(adapter), data->is_set_member);
return 0;
}
static void cas_adapter_remove(struct btd_adapter *adapter)
{
struct cas_data *data = btd_adapter_get_data(adapter);
if (!data)
return;
/* Remove CAS from the GATT database */
if (data->service)
gatt_db_remove_service(data->db, data->service);
gatt_db_unref(data->db);
btd_adapter_unref(data->adapter);
g_free(data);
}
/* Profile registration: CAS runs as an adapter-level plugin */
static const struct btd_profile cas_profile = {
.name = "cas",
.priority = BTD_PROFILE_PRIORITY_MEDIUM,
.adapter_probe = cas_adapter_probe,
.adapter_remove = cas_adapter_remove,
};
/* BlueZ plugin descriptor — tells bluetoothd about this plugin */
static int cas_plugin_init(void)
{
DBG("CAS plugin initialising");
return btd_profile_register(&cas_profile);
}
static void cas_plugin_exit(void)
{
DBG("CAS plugin exiting");
btd_profile_unregister(&cas_profile);
}
/* Macro that registers this as a BlueZ plugin with the daemon */
BLUETOOTH_PLUGIN_DEFINE(cas, VERSION,
BLUETOOTH_PLUGIN_PRIORITY_DEFAULT,
cas_plugin_init,
cas_plugin_exit)
Verifying CAS with BlueZ Tools
Once connected to a CAP Acceptor device, use the GATT menu in bluetoothctl to list all services and look for the CAS UUID.
# Start bluetoothd in experimental mode first:
sudo bluetoothd --experimental &
# Launch bluetoothctl
bluetoothctl
# Enable LE scanning and find a CAP Acceptor
[bluetooth]# scan le
[NEW] Device AA:BB:CC:DD:EE:FF TWS_Earbuds_L
# Connect
[bluetooth]# connect AA:BB:CC:DD:EE:FF
Connection successful
# Switch to GATT menu
[bluetooth]# menu gatt
# List all attributes (services + characteristics)
[bluetooth]# list-attributes AA:BB:CC:DD:EE:FF
# Expected output (look for these UUIDs):
#
# Primary Service (Handle 0x0020)
# /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0020
# 00001853-0000-1000-8000-00805f9b34fb <-- CAS UUID !!
#
# Included Service (Handle 0x0021) <-- if Coordinated Set member
# 00001846-0000-1000-8000-00805f9b34fb <-- CSIS UUID !!
#
# Primary Service (Handle 0x0030)
# 00001846-0000-1000-8000-00805f9b34fb <-- CSIS service
#
# Characteristic (Handle 0x0031)
# 00002b84-0000-1000-8000-00805f9b34fb <-- SIRK
#
# Characteristic (Handle 0x0033)
# 00002b85-0000-1000-8000-00805f9b34fb <-- Set Size
#
# Characteristic (Handle 0x0035)
# 00002b86-0000-1000-8000-00805f9b34fb <-- Member Lock
# Still in bluetoothctl GATT menu:
# Read the Coordinated Set Size characteristic
[bluetooth]# read /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0030/char0033
# Response: 0x02 (set has 2 members — a pair)
# Read the Member Rank
[bluetooth]# read /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0030/char0037
# Response: 0x01 (this is member rank 1 = Left earbud)
# Reading SIRK requires an encrypted connection (bonded device)
[bluetooth]# read /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0030/char0031
# Response: 01 AB CD EF ... (1 byte type + 16 byte key)
# ^^ type 0x01 = Encrypted SIRK
gatttool lets you inspect raw ATT PDUs. This is useful for understanding exactly what bytes are on the wire when CAS is discovered.
# Connect and list all primary services
gatttool -b AA:BB:CC:DD:EE:FF -I
> connect
> primary
# Output example:
# attr handle: 0x0001, end grp handle: 0x0009 uuid: 00001800-... (GAP)
# attr handle: 0x000a, end grp handle: 0x000d uuid: 00001801-... (GATT)
# attr handle: 0x0020, end grp handle: 0x0021 uuid: 00001853-... (CAS) <--
# attr handle: 0x0030, end grp handle: 0x0038 uuid: 00001846-... (CSIS) <--
# attr handle: 0x0040, end grp handle: 0x0060 uuid: 00001850-... (PACS)
# attr handle: 0x0070, end grp handle: 0x0090 uuid: 0000184e-... (ASCS)
# Read the CAS service declaration attribute directly (handle 0x0020)
> char-read-hnd 0x0020
# Characteristic value/descriptor: 53 18
# ^^ ^^ = 0x1853 in little-endian
# Find Include Declaration inside CAS (handle 0x0021)
> char-read-hnd 0x0021
# Characteristic value/descriptor: 30 00 38 00 46 18
# ^^^^^ start handle of CSIS = 0x0030
# ^^^^^ end handle of CSIS = 0x0038
# ^^^^^ CSIS UUID = 0x1846
Common Mistakes When Implementing CAS
| # | Mistake | Impact | Correct Approach |
|---|---|---|---|
| 1 | Registering CAS as a Secondary Service | CAP Initiator’s primary service discovery will not find CAS. Device will not be recognised as a CAP Acceptor. | Always pass true for the is_primary parameter in gatt_db_add_service(). |
| 2 | Registering more than one CAS instance | Violates the spec. Remote devices may behave unpredictably when they find multiple instances of UUID 0x1853. | Guard with a check — if CAS already exists in the GATT DB, skip registration. |
| 3 | Including CSIS in CAS but not registering CSIS as Primary Service | The Include Declaration points to a non-existent or inactive service. Reading the Include handle returns invalid handle references. | Always register and activate CSIS as a Primary Service before calling gatt_db_service_add_included(). |
| 4 | Including two CSIS instances in one CAS | Violates the spec rule that CAS shall include no more than one instance of CSIS. The Initiator expects exactly one. | Call gatt_db_service_add_included() for CSIS exactly once. |
| 5 | Not advertising UUID 0x1853 | Initiators that filter scan results by the CAS UUID will not discover the device. The device must be connected first for GATT discovery. | Include CAS UUID in the Service UUIDs AD type (0x03 or 0x07) in the advertisement payload. |
The Complete CAS Picture — Summary Diagram
| CAP Initiator Action — Reading CAS on Remote Device | ||
|
Step 1: Scan
Filter for CAS UUID 0x1853 in advertisement payload. Device may include it in the Service UUID AD type to be filterable before connection.
|
Step 2: Connect + Discover
After ACL connection, run GATT Primary Service Discovery. Confirm CAS (0x1853) is a Primary Service on the remote GATT server.
|
Step 3: Check for CSIS
Read Include Declarations inside CAS. If an Include pointing to CSIS (0x1846) is found, device is a Coordinated Set member — read SIRK to find the other member.
|
| After CAS discovery: Proceed with PACS (codec capabilities) → ASCS (stream control) → ISO CIS setup (audio data). CAS itself plays no role in this subsequent phase — its job is done after identification. | ||
Key Takeaways — CAS Part 2
- A device exposes CAS but has no Include Declaration inside it. What does this tell the CAP Initiator?
- What is the SIRK, and why must the Initiator resolve it before it can connect to the second earbud?
- If a device supports EATT, what additional entry must appear in the CAS SDP record?
- In the BlueZ GATT API, what does passing
trueas the third argument togatt_db_add_service()do? - How does the CAP Initiator know that it has connected to all members of a Coordinated Set?
- What ATT PDU type does the Initiator use to discover Primary Services?
Show Answers
- The device is a CAP Acceptor but is not part of any Coordinated Set. It is a standalone audio device (e.g. a single Bluetooth speaker or monaural headset).
- SIRK is the Set Identity Resolving Key — a 16-byte value shared by all members of a set. The Initiator reads it from the first earbud, then uses it to verify that the second advertising device it finds belongs to the same set (by matching the SIRK).
- An Additional Protocol Descriptor List entry with L2CAP PSM = EATT must be added to the SDP record (Conditional C.1).
- It registers the service as a Primary Service (discoverable via ATT_READ_BY_GROUP_TYPE with UUID 0x2800), as opposed to a Secondary Service (UUID 0x2801).
- It reads the Coordinated Set Size characteristic in CSIS. When it has connected to that many members (each with a matching SIRK), it knows the set is complete.
ATT_READ_BY_GROUP_TYPE_REQwith attribute type UUID 0x2800 (Primary Service).
Common Audio Service — Series Complete
You now understand CAP (the orchestration framework), CAS (the identity service), Coordinated Sets (CSIS), the SDP record for BR/EDR, and the BlueZ implementation. The next articles in the LE Audio series cover PACS and ASCS — the services that handle capability negotiation and audio stream control.
Next: PACS — Published Audio Capabilities → ← Part 2: CAS Introduction
