Bluetooth Call Control Profile (CCP)

πŸ“ž Bluetooth Call Control Profile (CCP)
A complete, student-friendly guide to understanding how wireless audio devices manage telephone calls using BLE GATT β€” from roles and services to opcodes and security.
v1.0
Spec Version
BT 5.2+
Core Compatibility
2 Roles
Server & Client
7 Opcodes
Call Control Actions

🏷 Key Concepts Covered

Call Control Profile CCP TBS GTBS GATT BLE Profile Call Control Point Bearer Service Call State Notifications Security Mode 1 GAP Peripheral Bonding BlueZ

What Is the Call Control Profile?

Imagine you are on a phone call and you press a button on your wireless headset to put the call on hold. Behind that single button press there is a well-defined Bluetooth protocol exchange. That exchange is governed by the Call Control Profile (CCP).

CCP is a BLE-based profile that sits on top of the Generic Attribute Profile (GATT). It defines the rules β€” the roles, the characteristics to read/write, and the procedures to follow β€” so that two Bluetooth devices can together manage telephone calls: answering, hanging up, placing on hold, merging, and originating calls.

The spec was adopted by the Bluetooth SIG in March 2021 and is part of the Bluetooth LE Audio ecosystem (Bluetooth Core Spec 5.2 or later required).

πŸ’‘ Real-World Analogy Before We Dive In

Think of a hotel front-desk phone system. The front desk (a Call Control Server) manages all the phone lines β€” it knows which lines are ringing, which are on hold, and which are active. The guest rooms (a Call Control Client) have a handset that can pick up a ringing line, ask to hold a call, or dial out β€” but the actual phone carrier is managed at the front desk.

In the Bluetooth world, your smartphone is the front desk (Server) and your wireless headset is the guest-room handset (Client). The “phone lines” are the bearer services (TBS/GTBS).

πŸ“š What You Will Learn

Theory

  • Roles: Server vs Client
  • Services: TBS and GTBS
  • Profile dependencies (GATT)
  • Call state machine
  • GATT sub-procedure requirements
  • Security requirements (BLE & BR/EDR)
  • GAP connection procedures

Practical

  • How call procedures map to GATT writes
  • All 7 Call Control Point opcodes
  • Characteristic notifications
  • BlueZ gatttool / bluetoothctl usage
  • C code sketch for GATT read/write
  • Security setup in BlueZ
  • Connection interval tuning

πŸ“„ Section 1 β€” Where CCP Lives in the Bluetooth Stack

Before learning the profile itself, it is important to see where CCP fits in the overall Bluetooth architecture. CCP is an application-layer profile that relies on GATT for its transport. GATT in turn sits on top of ATT (Attribute Protocol), which runs on the BLE L2CAP layer.

πŸ“ž Call Control Profile (CCP) β€” Application Layer
Telephone Bearer Service (TBS) & Generic TBS (GTBS)
GATT β€” Generic Attribute Profile
ATT β€” Attribute Protocol
L2CAP β€” Logical Link Control and Adaptation Protocol
BLE Link Layer / HCI

Figure 1: CCP within the Bluetooth protocol stack

Key Dependency: CCP mandates the Generic Attribute Profile (GATT). Without GATT there is no CCP. Every characteristic read, write, and notification goes through standard GATT procedures.

πŸ‘€ Section 2 β€” The Two CCP Roles

CCP defines exactly two roles. Every device involved in a CCP interaction must play one of them.

πŸ“± Call Control Server

  • Acts as a GATT Server
  • Hosts the TBS and/or GTBS services
  • Has actual phone bearers (SIM / VoIP)
  • Responds to call control commands
  • Sends notifications on call state changes

Examples: smartphones, tablets, laptops with cellular/VoIP apps

🎧 Call Control Client

  • Acts as a GATT Client
  • Discovers and reads bearer characteristics
  • Writes to the Call Control Point
  • Receives call state notifications
  • No phone bearer of its own

Examples: wireless headsets, earbuds, smartwatches, car infotainment systems

Call Control Client
GATT Client
Headset / Watch
βž” Write Call Control Point
β†– Notifications / Reads
Call Control Server
GATT Server
Smartphone
GTBS (M) TBS (O)

Figure 2: CCP Role interaction β€” Client writes commands, Server sends back notifications

πŸ“ž Section 3 β€” TBS and GTBS: The Bearer Services

CCP does not define the GATT characteristics directly β€” those are defined in a companion document called the Telephone Bearer Service (TBS) specification. CCP tells you which procedures to use and when; TBS tells you what the characteristics look like.

What Is a Bearer?

A bearer is a specific telephone channel. Your phone might have a cellular bearer (your SIM card) and a VoIP bearer (a WhatsApp/Teams call). Each bearer is an independent path for making calls.

TBS β€” Telephone Bearer Service

  • Represents one specific bearer (e.g., your cellular SIM)
  • A Server can have zero or more TBS instances
  • Each TBS instance is an independent GATT primary service
  • Useful when the Client wants per-bearer control
  • Example: control only the cellular call, not the VoIP call

GTBS β€” Generic TBS

  • A single, unified view of all bearers
  • Always exactly one instance on the Server (mandatory)
  • Lightweight clients use only GTBS
  • Acts as a single point of access for all call activity
  • Example: headset just wants to answer/hang up β€” doesn’t care which bearer

Call Control Server (e.g., your Smartphone)
GTBS
Mandatory (Γ—1)
TBS #1
Cellular
TBS #2
VoIP/Teams
TBS #n
All instances are GATT Primary Services discoverable via Service Discovery

Figure 3: GTBS (mandatory, Γ—1) + multiple optional TBS instances on a single Server

Student Tip: A simple headset implementation only needs to discover and use GTBS. It doesn’t need to care about TBS at all. This keeps the implementation small. Richer clients (like a car infotainment system) might interact with each TBS to show per-app call UI.

πŸ”„ Section 4 β€” GATT Sub-Procedures the Client Must Support

The Client must implement a specific set of GATT sub-procedures. Think of these as the “vocabulary” the Client uses to talk to the Server. The table below summarises requirements (M = Mandatory, O = Optional, C = Conditional).

# GATT Sub-Procedure Requirement Notes
1 Discover All Primary Services C.1 Needed to find TBS/GTBS
2 Discover Primary Services by UUID C.2 Alternative to row 1; at least one is mandatory
3 Find Included Services O
4 Discover All Characteristics of a Service C.3
5 Discover Characteristics by UUID C.4
6 Discover All Characteristic Descriptors M Needed to find CCCDs
7 Notifications M Core mechanism for call state updates
8 Read Characteristic Value M Read current call state, bearer info, etc.
9 Read Long Characteristic Values M For values > ATT_MTU–3 bytes
10 Write Characteristic Value C.5 At least one of Write / Write Without Response
11 Write Without Response C.5 At least one of Write / Write Without Response
12 Read Characteristic Descriptors M
13 Write Characteristic Descriptors M Needed to enable notifications (write CCCD = 0x0001)
What is a CCCD? The Client Characteristic Configuration Descriptor (CCCD) is a per-characteristic descriptor that you write to enable notifications or indications from the server. Write 0x01 0x00 to enable notifications, 0x00 0x00 to disable them.

BlueZ β€” Enabling Notifications via gatttool

After discovering the CCCD handle for the Call State characteristic, enable notifications:

# Step 1: Connect to the CCP Server (smartphone / peripheral)
gatttool -b AA:BB:CC:DD:EE:FF -I

# Step 2: Inside gatttool interactive shell
[AA:BB:CC:DD:EE:FF][LE]> connect
Connection successful

# Step 3: Discover all primary services to find GTBS
[AA:BB:CC:DD:EE:FF][LE]> primary
# Look for GTBS UUID: 0x184C  (assigned number)
# Look for TBS  UUID: 0x184B

# Step 4: Discover characteristics within GTBS service handle range
[AA:BB:CC:DD:EE:FF][LE]> characteristics 0x0010 0x0040

# Step 5: Find the CCCD handle for Call State characteristic
# Call State char UUID = 0x2BBE
[AA:BB:CC:DD:EE:FF][LE]> char-desc 0x0020 0x0025
# Identify the descriptor handle (type = 0x2902 = CCCD)

# Step 6: Write 0x0100 to CCCD handle to enable notifications
[AA:BB:CC:DD:EE:FF][LE]> char-write-req 0x0024 0100
# Server will now push Call State updates whenever a call changes

BlueZ C β€” Reading a Characteristic Value (GLib/D-Bus pattern)

/*
 * Sketch: Read the Bearer Provider Name characteristic using BlueZ D-Bus API.
 * Real BlueZ GATT client code uses GDBus; this shows the logical flow.
 *
 * Characteristic UUID: Bearer Provider Name = 0x2BB3
 */

#include <gio/gio.h>
#include <stdio.h>

void read_bearer_provider_name(GDBusConnection *conn,
                               const char *char_path)
{
    GError  *err  = NULL;
    GVariant *ret = NULL;

    /* Call org.bluez.GattCharacteristic1.ReadValue */
    ret = g_dbus_connection_call_sync(
        conn,
        "org.bluez",           /* bus name   */
        char_path,             /* object path, e.g. /org/bluez/hci0/dev_AA_.../serviceXX/charYY */
        "org.bluez.GattCharacteristic1",
        "ReadValue",
        g_variant_new("(a{sv})", NULL),  /* options dict, empty */
        G_VARIANT_TYPE("(ay)"),          /* returns byte array  */
        G_DBUS_CALL_FLAGS_NONE,
        -1, NULL, &err);

    if (err) {
        g_printerr("ReadValue failed: %s\n", err->message);
        g_clear_error(&err);
        return;
    }

    GVariant *bytes_v;
    g_variant_get(ret, "(@ay)", &bytes_v);

    gsize len;
    const guint8 *data = g_variant_get_fixed_array(bytes_v, &len, 1);

    printf("Bearer Provider Name (%zu bytes): ", len);
    for (gsize i = 0; i < len; i++)
        printf("%c", (char)data[i]);   /* UTF-8 string */
    printf("\n");

    g_variant_unref(bytes_v);
    g_variant_unref(ret);
}

πŸ“‹ Section 5 β€” Important TBS/GTBS Characteristics

All characteristics are defined in the TBS specification, but CCP’s procedures revolve around them. Here is a quick reference of the key characteristics you will encounter as a developer.

Characteristic UUID Access Notify Purpose
Bearer Provider Name 0x2BB3 Read βœ“ Carrier name (e.g., “Airtel”, “Teams”)
Bearer UCI 0x2BB4 Read Uniform Caller Identifier for the bearer
Bearer Technology 0x2BB5 Read βœ“ Network type: 3G, LTE, VoIP, etc.
Bearer Signal Strength 0x2BB7 Read βœ“ Signal level 0–100 (0xFF = no signal)
Bearer List Current Calls 0x2BB9 Read βœ“ List of all ongoing calls with their states
Status Flags 0x2BBB Read βœ“ Inband ringtone, silent mode flags
Call State 0x2BBE Read βœ“ State of each call (Incoming/Active/Held…)
Call Control Point 0x2BBF Write / W-NR βœ“ Main control interface β€” send opcodes here
Termination Reason 0x2BC0 Notify only βœ“ Why a call ended (busy, failed, etc.)
Incoming Call 0x2BC1 Read βœ“ URI/number of incoming call
Call Friendly Name 0x2BC2 Read βœ“ Display name for the call (contact name)
Content Control ID (CCID) 0x2BBA Read Links this service to audio content control
About ATT_MTU–3: Many characteristics carry variable-length strings (URIs, names). If a characteristic value is longer than ATT_MTU – 3 bytes (default MTU is 23, so max payload = 20 bytes), a notification only delivers the first ATT_MTU–3 bytes. The Client must use the Read Long Characteristic Value procedure to get the rest.

πŸ”„ Section 6 β€” Understanding the Call State Machine

Each call on a bearer has a state. The Call State characteristic encodes the current state of every active call. State changes are delivered to the Client via GATT notifications. Here are the fundamental call states:

Call State Transitions
INCOMING
β†’
ACTIVE
Answer opcode
↓ ↓
DIALING
Terminate
ALERTING
Hold/Retrieve
LOCALLY HELD
↓
REMOTELY HELD
LOCALLY & REMOTELY HELD
← transitions both ways
Any state β†’ TERMINATED via Terminate opcode

Figure 4: Call state transitions (simplified)

Each entry in the Call State characteristic contains three fields: Call_Index (1 byte, unique ID for the call), Call_State (1 byte, the state enum above), and Call_Flags (1 byte, whether URI/name is withheld).

πŸ‘‰ Section 7 β€” Call Control Point Procedures (The 7 Opcodes)

The Call Control Point (CCP) characteristic is the command channel. The Client writes an opcode plus parameters to this characteristic to perform a call action. The Server responds with a notification on the same characteristic (if the Client has enabled notifications on it).

Write format: [Opcode (1 byte)] [Call_Index (1 byte)] [optional parameters]

0x00 Accept

Answer Incoming Call β€” Write this opcode with the Call_Index of the ringing call. The Server transitions that call from INCOMING β†’ ACTIVE. The headset user pressed the green button.
0x01 Terminate

Terminate Call β€” Write with Call_Index to end a call in any state. Works for rejecting an incoming call, ending an active call, or cancelling a dialling attempt.
0x02 Local Hold

Move Call To Local Hold β€” Places an active or alerting call on local hold. The remote party stays connected but audio is paused from the local side. State β†’ LOCALLY HELD.
0x03 Local Retrieve

Move Locally Held Call To Active β€” Retrieves a locally held call back to active. Also used in the “Move Locally And Remotely Held Call To Remotely Held” sub-procedure (same opcode, different starting state).
0x04 Originate

Originate Call β€” Make an outgoing call. Write this opcode with the destination URI (e.g., tel:+919876543210). No Call_Index is needed β€” the Server assigns one and notifies the Client.
0x05 Join

Join Calls β€” Merge multiple calls into a conference. Write this opcode with a list of Call_Index values. Calls with state INCOMING must not be in the join list.
Optional Opcodes: Local Hold, Local Retrieve, and Join are considered optional at the Call Control Server level. The Client can discover which optional opcodes the Server supports by reading the Call Control Point Optional Opcodes characteristic (UUID 0x2BC3) before attempting to use them.

BlueZ β€” Writing an Opcode to the Call Control Point

/*
 * Write "Terminate" opcode (0x01) for Call_Index 0x01
 * to the Call Control Point characteristic.
 *
 * Using gatttool command-line:
 *   char-write-req <handle> <value>
 *
 * Byte format: [opcode] [call_index]
 *              0x01     0x01
 */

/* ---- gatttool interactive session ---- */
// [AA:BB:CC:DD:EE:FF][LE]> char-write-req 0x0030 0101
// Characteristic value was written successfully
// notification handle = 0x0030 value: 01 01 00
//   ^-- Server responded: opcode=Accept(0x00?), index=0x01, result=Success(0x00)

/*
 * C snippet: write using BlueZ D-Bus GattCharacteristic1.WriteValue
 */
#include <gio/gio.h>

#define CCP_OPCODE_TERMINATE   0x01
#define CCP_OPCODE_ANSWER      0x00
#define CCP_OPCODE_LOCAL_HOLD  0x02
#define CCP_OPCODE_ORIGINATE   0x04

int ccp_write_opcode(GDBusConnection *conn,
                     const char *ccp_char_path,
                     guint8 opcode,
                     guint8 call_index)
{
    GError   *err = NULL;
    guint8    buf[2] = { opcode, call_index };
    GVariant *bytes  = g_variant_new_fixed_array(
                            G_VARIANT_TYPE_BYTE, buf, 2, 1);
    GVariant *opts   = g_variant_new("a{sv}", NULL);
    GVariant *params = g_variant_new("(@ay@a{sv})", bytes, opts);

    g_dbus_connection_call_sync(
        conn,
        "org.bluez",
        ccp_char_path,
        "org.bluez.GattCharacteristic1",
        "WriteValue",
        params,
        NULL,
        G_DBUS_CALL_FLAGS_NONE,
        -1, NULL, &err);

    if (err) {
        g_printerr("CCP WriteValue error: %s\n", err->message);
        g_clear_error(&err);
        return -1;
    }
    return 0;
}

/* Usage: terminate call index 1 */
// ccp_write_opcode(conn, "/org/bluez/.../char0030",
//                  CCP_OPCODE_TERMINATE, 0x01);

πŸ”” Section 8 β€” Notification Flow: How the Client Stays Updated

Many TBS/GTBS characteristics are notify-capable. The Client should subscribe to the ones it cares about so the Server can push updates without the Client polling. This is critical for real-time call management.

Call Control Client (Headset) Call Control Server (Phone)
1. GATT Connect β†’
2. Discover GTBS service β†’
← 3. Return service handle range
4. Discover chars & descriptors β†’
5. Write CCCD 0x0001 for
Call State & Termination Reason
β†’
6. Incoming call detected! ← 6. NOTIFY: Call State = INCOMING, idx=1
7. Write CCP: Accept, idx=1 β†’
← 8. NOTIFY: Call State = ACTIVE, idx=1
← 9. NOTIFY CCP: opcode=0, idx=1, result=Success

Figure 5: Typical notification flow β€” Client subscribes, Server pushes call state updates

Termination Reason Characteristic: This is a notify-only characteristic β€” it has no readable value. The Client must configure its CCCD. When a call ends, the Server pushes a notification containing the Call_Index and a reason code (e.g., call ended by remote party, line busy, no answer, etc.).

BlueZ β€” Receiving Notifications via bluetoothctl

# Using bluetoothctl (BlueZ 5.x) to monitor GATT notifications

$ bluetoothctl

# Scan and connect
[bluetooth]# scan on
[bluetooth]# connect AA:BB:CC:DD:EE:FF
[bluetooth]# menu gatt

# List attributes discovered after connection
[AA:BB:CC:DD:EE:FF]# list-attributes AA:BB:CC:DD:EE:FF

# Select the Call State characteristic
[AA:BB:CC:DD:EE:FF]# select-attribute /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0010/char001e

# Enable notifications (writes 0x0100 to CCCD automatically)
[AA:BB:CC:DD:EE:FF:/service0010/char001e]# notify on

# Now incoming calls / state changes appear in the terminal:
# [CHG] Attribute /org/bluez/.../char001e Value:
#         01 01 00
#   Call_Index=0x01, Call_State=0x00 (INCOMING), Call_Flags=0x00

# Answer the call β€” write Accept opcode
[AA:BB:CC:DD:EE:FF:/service0010/char001e]# back
# Select the Call Control Point characteristic
[AA:BB:CC:DD:EE:FF]# select-attribute /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0010/char0030
[AA:BB:CC:DD:EE:FF:/service0010/char0030]# write 0x00 0x01
# 0x00 = Accept opcode, 0x01 = Call_Index

πŸ”’ Section 9 β€” Security Requirements

Phone calls contain sensitive information. CCP enforces encryption and bonding. Here is what you need to know as a developer.

BLE (LE Transport)

  • Minimum: Security Mode 1 Level 2 (encrypted link, bonded)
  • Encryption key must have 128-bit entropy
  • Key source: LE Secure Connections, CTKD from BR/EDR SC, or OOB
  • Both Client and Server must support bondable mode
  • Link Layer Privacy should be used
  • SM1 L1 (no encryption) is excluded

BR/EDR Transport

  • Minimum: Security Mode 4 Level 2
  • Encryption key must have 128-bit entropy
  • Key source: BR/EDR Secure Connections, CTKD from LE SC, or OOB
  • Devices should bond during connection
  • Initiating device should start bonding procedure
⚠ Developer Alert: If you try to access a TBS/GTBS characteristic on an unencrypted link, the Server will respond with an ATT error code 0x0F (Insufficient Encryption) or 0x05 (Insufficient Authentication). Make sure pairing and bonding complete before accessing any characteristic.

BlueZ β€” Setting Up Pairing and Bonding

# Enable Bluetooth agent for pairing
$ bluetoothctl
[bluetooth]# agent on
[bluetooth]# default-agent

# Set pairing capability: KeyboardDisplay for SSP/LE Secure Connections
[bluetooth]# power on
[bluetooth]# pairable on
[bluetooth]# scan on

# Once device appears:
[bluetooth]# pair AA:BB:CC:DD:EE:FF
# Confirm passkey / accept pairing request when prompted

# Trust the device so it auto-connects in future
[bluetooth]# trust AA:BB:CC:DD:EE:FF

# Verify security: check if link is encrypted
$ hcitool con
# Should show: <handle> AA:BB:CC:DD:EE:FF  type:LE  encrypted:yes

# In BlueZ source (C): enforce minimum security level before GATT operations
# bt_att_set_security(att, BT_SECURITY_MEDIUM);
# /* BT_SECURITY_MEDIUM = SM1 L2 = encrypted + bonded */

πŸ“Ά Section 10 β€” GAP Requirements: Advertising and Connection

The Generic Access Profile (GAP) layer handles device discovery and connection setup. CCP has specific requirements at this layer.

Server (Peripheral) β€” Advertising

  • 1
    Must use extended advertising PDUs (not legacy). Advertise in Limited or General Discoverable mode when looking for new connections.
  • 2
    Should include Service UUID AD type containing TBS UUID (0x184B) and/or GTBS UUID (0x184C) so Clients can discover it efficiently.
  • 3
    For bonded devices: use Directed Connectable mode or Undirected Connectable mode with an advertising filter to accept only the bonded peer.

Client (Central) β€” Connection

  • 1
    Use Limited or General Discovery procedure to find the Server.
  • 2
    Connect using any GAP connection procedure (Auto, General, Selective, or Direct).
  • 3
    Negotiate a tight connection interval to reduce call control latency.

    Recommended Connection Interval: 10 ms – 30 ms
Why 10–30 ms? Each call control action (answer, hang up, hold) is a GATT write. At a 10 ms connection interval, the write completes within one or two connection events β€” a sub-50 ms round trip. At 100 ms the user perceives noticeable delay between pressing the headset button and the call actually accepting.

BlueZ β€” Requesting Connection Parameters

/*
 * Request a tighter connection interval after connecting.
 * Uses HCI LE Connection Update command via BlueZ socket API.
 * Values are in units of 1.25 ms.
 * 10 ms  = 0x0008  (8 * 1.25 = 10 ms)
 * 30 ms  = 0x0018  (24 * 1.25 = 30 ms)
 */

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

void request_ccp_connection_params(int hci_sock, uint16_t conn_handle)
{
    struct hci_request        rq  = { 0 };
    le_connection_update_cp   cp  = { 0 };
    uint8_t                   status;

    cp.handle           = htobs(conn_handle);
    cp.min_interval     = htobs(0x0008); /* 10 ms  */
    cp.max_interval     = htobs(0x0018); /* 30 ms  */
    cp.latency          = htobs(0x0000); /* no slave latency */
    cp.supervision_timeout = htobs(0x01F4); /* 5 s   */
    cp.min_ce_length    = htobs(0x0000);
    cp.max_ce_length    = htobs(0x0000);

    rq.ogf    = OGF_LE_CTL;
    rq.ocf    = OCF_LE_CONN_UPDATE;
    rq.cparam = &cp;
    rq.clen   = LE_CONN_UPDATE_CP_SIZE;
    rq.rparam = &status;
    rq.rlen   = 1;

    if (hci_send_req(hci_sock, &rq, 2000) < 0)
        perror("hci_send_req LE_CONN_UPDATE failed");
}

🀝 Section 11 β€” Putting It All Together: Complete Flow

Here is the full lifecycle from boot to an answered call, showing every layer of CCP working together.

Complete CCP Session Lifecycle

PHASE 1: DISCOVERY Server advertises with GTBS UUID β†’ Client scans β†’ Client finds Server by UUID filter

PHASE 2: CONNECTION BLE connection established β†’ Connection interval negotiated (10–30 ms)

PHASE 3: SECURITY Pairing β†’ Bonding β†’ LE Secure Connections β†’ 128-bit encrypted link established

PHASE 4: GATT SETUP Discover GTBS primary service β†’ Discover all chars β†’ Discover CCCDs β†’ Write CCCDs to enable notifications

PHASE 5: READ INITIAL STATE Read Bearer Provider Name β†’ Read Bearer Technology β†’ Read Call State β†’ Sync initial state

PHASE 6: CALL ARRIVES Server sends Call State notification (INCOMING) β†’ Client rings/vibrates β†’ User presses button

PHASE 7: CALL CONTROL Client writes Accept opcode β†’ Server confirms via CCP notification β†’ Call State β†’ ACTIVE

PHASE 8: CALL ENDS Client writes Terminate β†’ Server sends Termination Reason notification β†’ Call State characteristic cleared

πŸ”Œ Section 12 β€” Common Pitfalls for Students and New Developers
✘ Mistake 1: Not subscribing to Termination Reason notifications
Termination Reason has no readable value β€” it is notify-only. Developers sometimes skip writing its CCCD, then wonder why they never know why a call ended. Always write 0x0001 to its CCCD.
✘ Mistake 2: Forgetting the CCCD for Call Control Point
Writing an opcode to the Call Control Point sends the command, but the Server’s response (success or error code) comes back as a notification on the same characteristic. If you didn’t enable CCP notifications, you won’t know if your command succeeded.
✘ Mistake 3: Including INCOMING calls in a Join request
The spec explicitly states: “The list of Call_Index fields provided in the Join opcode write shall not include any Call_Index fields with the Call State of Incoming.” Including one will result in an error response from the Server.
✘ Mistake 4: Assuming TBS is present
Only GTBS is mandatory on the Server. TBS instances are optional and may be zero. Always check via service discovery before trying to interact with TBS.
✘ Mistake 5: Ignoring the “Value Changed During Read Long” error
While doing a Read Long Characteristic Values procedure (for values > ATT_MTU–3), if you get ATT error Value Changed During Read Long, the partial data you already read is invalid. Restart from offset 0 with a fresh Read.

πŸ“Œ Section 13 β€” Quick Reference Cheat Sheet

Service UUIDs

Service UUID
TBS 0x184B
GTBS 0x184C

CCP Opcodes

Opcode Value
Accept 0x00
Terminate 0x01
Local Hold 0x02
Local Retrieve 0x03
Originate 0x04
Join 0x05

Call States

State Value
Incoming 0x00
Dialing 0x01
Alerting 0x02
Active 0x03
Locally Held 0x04
Remotely Held 0x05
Locally & Remotely Held 0x06

Security Summary

Transport Min Security
LE SM1 Level 2 + 128b key
BR/EDR SM4 Level 2 + 128b key
Connection Interval 10 – 30 ms

Keep Learning at EmbeddedPathashala

Explore more BLE profile tutorials, Linux kernel programming, and hands-on BlueZ guides β€” all free, all practical.

Visit EmbeddedPathashala More BLE Tutorials

Leave a Reply

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