CAS — Coordinated Sets, SDP & BlueZ

EmbeddedPathashala · Bluetooth LE Audio Series · Part 3

CAS — Coordinated Sets, SDP & BlueZ

CSIS Inclusion, SDP Interoperability Record, and Complete BlueZ Implementation

CSIS
Included Service
UUID: 0x1846
SIRK
Set Identifier
16-byte encrypted key
SDP
BR/EDR Record
ATT + optional EATT
BlueZ
Implementation
gatt-db + cas.c

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.

CSIS Characteristics Reference
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.

CAS SDP Record Structure
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
C.1: Mandatory to support if EATT (Enhanced ATT) is supported, otherwise Excluded. EATT provides multiple parallel ATT bearer channels over L2CAP, improving throughput for GATT operations.

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.

1. UUID Definitions and Data Structures
/* 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? */
};

2. Registering CAS as a Primary Service in the GATT Database

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);
}

3. Including CSIS Inside CAS (Coordinated Set Members Only)

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)");
}

4. Adapter Probe — Called When Adapter Initialises

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,
};

5. Plugin Entry and Exit Points
/* 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

Using bluetoothctl to Discover CAS

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
Reading CSIS Characteristics After Discovery
# 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
Low-Level ATT Inspection with gatttool

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

CSIS UUID = 0x1846
SIRK = 16-byte set key
Set Size = number of members
Member Lock = prevent interference
Include = GATT 0x2802
SDP needed for BR/EDR
PSM = ATT in SDP record
EATT = conditional C.1
gatt_db_add_service() for CAS
gatt_db_service_add_included() for CSIS
Advertise UUID 0x1853
Max 1 CSIS include in CAS

Self-Check Questions
  1. A device exposes CAS but has no Include Declaration inside it. What does this tell the CAP Initiator?
  2. What is the SIRK, and why must the Initiator resolve it before it can connect to the second earbud?
  3. If a device supports EATT, what additional entry must appear in the CAS SDP record?
  4. In the BlueZ GATT API, what does passing true as the third argument to gatt_db_add_service() do?
  5. How does the CAP Initiator know that it has connected to all members of a Coordinated Set?
  6. What ATT PDU type does the Initiator use to discover Primary Services?
Show Answers
  1. 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).
  2. 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).
  3. An Additional Protocol Descriptor List entry with L2CAP PSM = EATT must be added to the SDP record (Conditional C.1).
  4. 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).
  5. 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.
  6. ATT_READ_BY_GROUP_TYPE_REQ with 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

Leave a Reply

Your email address will not be published. Required fields are marked *