BLE GATT Notify, Indicate, Descriptors & GATT Service

 

GATT Notify, Indicate, Descriptors & GATT Service

Chapter 13 · Part 5 of 5 · Sections 13.6.8–13.8.1 · Notifications · Indications · Descriptor Ops · Timeouts · Service Changed

0x2902CCCD enables push
30 secProcedure timeout
0x2A05Service Changed UUID
0x26SC char properties
SEO Keywords

BLE GATT Notification Handle Value unreliable server push BLE GATT Indication Handle Value Confirmation reliable BLE Characteristic Descriptor Read Write Long operations BLE GATT 30 second timeout ATT bearer reestablish BLE Service Changed Characteristic 0x2A05 Properties 0x26 BLE bonded clients service changed reconnect firmware BlueZ notify indicate CCCD code

13.6.8 — Characteristic Value Notification

Server Pushes Data Without Waiting for Acknowledgement

A Notification is the server telling the client “here is some data” without any ceremony. There is no confirmation, no delivery guarantee. If the BLE radio drops the packet or the client is busy, the data is simply gone. This makes Notifications the lowest overhead, highest frequency data delivery mechanism in BLE — ideal for streaming sensor readings that are continuously updated.

The client must explicitly enable Notifications before the server will send them. This is done by writing the value 0x0001 to the Client Characteristic Configuration Descriptor (CCCD, UUID 0x2902) associated with the characteristic.

ATT opcode:Handle Value Notification (0x1B)
Direction:Server → Client (server-initiated)
Client response:None — no response required or expected
Enable by:Writing 0x0001 to CCCD (UUID 0x2902)
Figure 13.30 — Handle Value Notification (No Response)
ClientServer
Client has written 0x0001 to CCCD — notifications now active
Handle Value Notification (Attribute Handle, Attribute Value)
No response from client — server continues without waiting
Handle Value Notification (Attribute Handle, Attribute Value)
Handle Value Notification (Attribute Handle, Attribute Value)
Server sends as fast as the connection interval allows — no flow control
/* Enable notifications — write 0x0001 to CCCD:               */
gatttool -b AA:BB:CC:DD:EE:FF --char-write-req \
         --handle=0x000d --value=0100

/* Listen for incoming notifications in the same process:     */
gatttool -b AA:BB:CC:DD:EE:FF \
         --char-write-req --handle=0x000d --value=0100 --listen

/* With bluetoothctl:                                         */
[AA:BB:CC:DD:EE:FF]# select-attribute /org/bluez/hci0/.../char000b
[AA:BB:CC:DD:EE:FF]# notify on
/* Notifications: [CHG] Attribute Value: 48 00 ...            */

/* Python D-Bus notification handler:                         */
def prop_changed(iface, changed, invalidated, path):
    if 'Value' in changed:
        data = bytes(changed['Value'])
        print(f"Notification from {path}: {data.hex()}")

bus.add_signal_receiver(prop_changed,
    dbus_interface='org.freedesktop.DBus.Properties',
    signal_name='PropertiesChanged',
    path_keyword='path')
mainloop.run()

/* CCCD values summary:                                       */
/* 0x0000 = disabled                                          */
/* 0x0001 = Notifications enabled                             */
/* 0x0002 = Indications enabled                               */
/* 0x0003 = Both enabled (if characteristic supports both)    */

13.6.9 — Characteristic Value Indication

Server Pushes Data and Waits for Client Confirmation

An Indication is identical to a Notification in intent — the server has new data for the client — but adds a mandatory acknowledgement step. The client must send back a Handle Value Confirmation after every Indication. Only after that confirmation arrives can the server send the next Indication. This guarantee of delivery comes at the cost of throughput: each value requires a full round trip instead of a fire-and-forget push.

Enable Indications by writing 0x0002 to the CCCD. BlueZ handles sending the Confirmation automatically in the kernel driver — the application layer just sees the value arrive, without needing to manually manage the confirmation PDU.

ATT opcode:Handle Value Indication (0x1D)
Client response:Handle Value Confirmation (0x1E) — required, no parameters
Server behaviour:Must wait for Confirmation before sending next Indication
Enable by:Writing 0x0002 to CCCD (UUID 0x2902)
Figure 13.31 — Handle Value Indication and Confirmation
ClientServer
Client has written 0x0002 to CCCD — indications now active
Handle Value Indication (Attribute Handle, Attribute Value)
Handle Value Confirmation () — no parameters, just ACK
Server receives Confirmation → now allowed to send next Indication
Handle Value Indication (next value)
Handle Value Confirmation ()
Use Notification when:

  • Continuous streaming data
  • Missing one sample is OK
  • High frequency (1–100 Hz)
  • Heart rate, accelerometer, steps
Use Indication when:

  • Every reading is critical
  • Low frequency (once per session)
  • Blood pressure, glucose, OTA update
  • Service Changed notifications
/* Enable indications — write 0x0002 to CCCD:                 */
gatttool -b AA:BB:CC:DD:EE:FF --char-write-req \
         --handle=0x000d --value=0200

/* BlueZ sends Handle Value Confirmation (0x1E) automatically */
/* The confirmation is handled transparently in               */
/* src/shared/att.c: handle_indication() function             */

/* BlueZ att.c source excerpt:                                */
static void handle_indication(struct bt_att *att,
                               struct bt_att_pdu_hdr *pdu,
                               uint16_t len) {
    /* Dispatch to registered indication handler first        */
    bt_att_invoke_indication(att, ...);
    /* Then automatically send confirmation:                  */
    bt_att_send(att, BT_ATT_OP_HANDLE_VAL_CONF, NULL, 0,
                confirm_sent_cb, NULL, NULL);
}

13.6.10 — Characteristic Descriptors Operations

Reading and Writing Descriptor Values

Descriptors are attributes attached to characteristics. The most commonly written descriptor is the CCCD (to enable notify/indicate), but profiles can define readable and writable descriptors for other purposes — user descriptions, presentation formats, valid range definitions, etc. All four operations require the Characteristic Descriptor Handle, which was obtained in the Descriptor Discovery procedure.

13.6.10.1 — Read Characteristic Descriptor

Uses ATT Read Request (0x0A) with the descriptor handle. Returns up to ATT_MTU-1 bytes. The value is profile-specific — for a CCCD, it returns the current 0x0000/0x0001/0x0002 enable state. For User Description (0x2901), it returns a UTF-8 string.

Figure 13.32
Read Request (Descriptor Handle)
Read Response (Descriptor Value)
13.6.10.2 — Read Long Characteristic Descriptor

Uses Read Blob Request (0x0C) for descriptor values larger than ATT_MTU-1 bytes. Same offset-based pagination as Read Long Characteristic Value. An error is returned if the descriptor value is actually shorter than ATT_MTU-1 bytes.

Figure 13.33
Read Blob Request (Handle, Offset=0)
Read Blob Response (bytes from offset)
Read Blob Request (Handle, Offset=N)
Read Blob Response (next chunk)
13.6.10.3 — Write Characteristic Descriptor

Uses ATT Write Request (0x12) with the descriptor handle and value to write. The server returns Write Response on success. Error Response if write is not permitted, authentication is insufficient, or the value is invalid or wrong size.

Figure 13.34
Write Request (Descriptor Handle, Value)
Write Response ()
13.6.10.4 — Write Long Characteristic Descriptor

Uses the same Prepare Write + Execute Write pattern as Write Long Characteristic Value. Used when the descriptor value is larger than ATT_MTU-3 bytes. The echoed Prepare Write Response does not need to be verified (unlike Reliable Writes).

Figure 13.35
Prepare Write Request (Handle, Offset, Chunk)
Prepare Write Response (echoed)
Execute Write Request (Flags=0x01)
Execute Write Response ()
/* Read CCCD to check current notify/indicate state:          */
gatttool -b AA:BB:CC:DD:EE:FF --char-read --handle=0x000d
/* 0000 = disabled | 0100 = notify | 0200 = indicate          */

/* Write CCCD to enable notifications:                        */
gatttool -b AA:BB:CC:DD:EE:FF --char-write-req \
         --handle=0x000d --value=0100

/* Read User Description descriptor (0x2901):                 */
gatttool -b AA:BB:CC:DD:EE:FF --char-read --handle=0x000e
/* Characteristic value/descriptor: 48 65 61 72 74 20 52 61.. */
/* Decodes to: "Heart Rate" (UTF-8)                           */

/* BlueZ D-Bus: descriptor access via GattDescriptor1:        */
desc_proxy = bus.get_object('org.bluez', '/org/bluez/.../desc000d')
desc_iface = dbus.Interface(desc_proxy, 'org.bluez.GattDescriptor1')
value = desc_iface.ReadValue({})
desc_iface.WriteValue([0x01, 0x00], {})  # enable notifications

13.7 — Timeouts

What Happens When a Procedure Does Not Complete

Every GATT procedure that involves a Request/Response or Indication/Confirmation pair has a 30-second timeout. If the client sends a request and 30 seconds pass without a response, GATT assumes the ATT bearer (the underlying L2CAP connection) has gone down. No further GATT procedures can be attempted on this bearer.

Recovery requires re-establishing a new ATT bearer — meaning the BLE connection must drop and reconnect before any further GATT work can happen. This is intentional: GATT does not try to recover mid-procedure because a partial state (e.g., half the chunks of a Write Long operation delivered) could corrupt the attribute value.

Practical implication: If a BLE peripheral firmware hangs or reboots mid-transaction, the central’s GATT layer will not retry — it will wait up to 30 seconds then declare the link dead. Fast disconnection detection (shorter supervision timeout in the connection parameters) helps recover sooner, but does not bypass the 30-second GATT transaction timeout.
/* BlueZ transaction timeout is 30000ms (30 seconds)          */
/* Defined in src/shared/att.c:                               */
#define ATT_TIMEOUT_INTERVAL    30000   /* milliseconds        */

/* Timeout callback in bt_att_send():                         */
static bool timeout_cb(void *user_data) {
    struct bt_att *att = user_data;
    /* Cancel all pending requests                            */
    bt_att_cancel_all(att);
    /* Mark ATT bearer as timed out — no more ops permitted   */
    att->timed_out = true;
    /* Signal disconnect to higher layers                     */
    if (att->disconn_func)
        att->disconn_func(0, att->disconn_data);
    return false;
}

13.8 — GATT Service

The Built-In Service Every GATT Server Should Expose

In addition to any profile-specific services, a GATT server may expose the GATT Service itself. This is a primary service with UUID set to <<Generic Attribute Profile>>. Its purpose is purely housekeeping — it contains exactly one characteristic: the Service Changed characteristic. When a server includes this service, it signals to clients that the server’s attribute database may change over the device’s lifetime.

Figure 13.36 — GATT Service and Service Changed Characteristic Definition
Service Declaration (Primary Service)
Attribute Type = 0x2800 (Primary Service)
Attribute Value = 0x1801 (Generic Attribute Profile UUID)
Permissions = Read Only, No Auth, No Authz
Characteristic Declaration
Attribute Type = 0x2803 (Characteristic)
Properties = 0x26
= 0x20 (Indicate) | 0x04 (Write Without Response) | 0x02 (Read)
Value Handle = [next attribute handle]
UUID = 0x2A05 (Service Changed)
Permissions = No Auth, No Authz
Characteristic Value Declaration
Attribute Type = 0x2A05 (Service Changed)
Start Handle: 0xAAAA → first changed attribute handle
End Handle: 0xBBBB → last changed attribute handle
Permissions = Not Readable, Not Writable, No Auth, No Authz
The value is delivered only via Indication — never directly readable

13.8.1 — Service Changed Characteristic

Telling Bonded Clients That the Attribute Database Has Changed

The Service Changed characteristic is a mechanism for the server to tell its bonded clients: “my service list has changed — the handle ranges I told you about before are no longer valid.” This matters because bonded clients cache the attribute database to avoid re-discovering it on every reconnection. Without this notification, a client might use stale handles after a firmware update that added, removed, or rearranged services.

The characteristic value itself contains just two handles — the start and end of the affected handle range. The client uses these to know which portion of its cache is stale and needs to be re-discovered. Everything outside that range remains valid.

Service Changed: Three Client Scenarios
Scenario 1 — Client is currently connected
Server sends Service Changed Indication immediately
Client sends Confirmation, then re-discovers affected handles
Scenario 2 — Client is bonded but not connected
Server queues the Service Changed Indication
When client reconnects, server sends Indication before any other operation
Client re-discovers only the affected handle range
Scenario 3 — Client is not bonded
No persistent cache — client does full discovery on every connection anyway
Service Changed mechanism irrelevant — fresh discovery always
Properties byte 0x26 decoded
0x20 Indicate Primary delivery mechanism — bonded clients receive via Indication
0x04 Write W/O Rsp Allows client to write (though rarely used in practice)
0x02 Read Value can be read (though attribute permissions say not readable)
0x26 = 0x20 | 0x04 | 0x02 = Indicate + Write W/O Rsp + Read
/* Check if GATT service is exposed by a device:              */
gatttool -b AA:BB:CC:DD:EE:FF --primary --uuid=0x1801
/* If present: attr handle: 0x0010, end grp handle: 0x0013    */
/* uuid: 0x1801 (Generic Attribute Profile)                   */

/* Find Service Changed characteristic (UUID 0x2A05):         */
gatttool -b AA:BB:CC:DD:EE:FF \
         --characteristics --uuid=0x2A05
/* handle: 0x0011, char properties: 0x26, char value handle: */
/*   0x0012, uuid: 0x2a05                                    */

/* Enable Service Changed Indication (CCCD at 0x0013):        */
gatttool -b AA:BB:CC:DD:EE:FF --char-write-req \
         --handle=0x0013 --value=0200

/* When a firmware update changes services, the client will   */
/* receive an Indication at 0x0012 with two 16-bit values:    */
/* bytes 0-1: start of affected handle range (little-endian)  */
/* bytes 2-3: end of affected handle range (little-endian)    */
/* e.g.: 01 00 ff ff = handles 0x0001 to 0xFFFF = full rescan */

/* BlueZ handles Service Changed in gatt-client.c:            */
/* service_changed_cb() is registered as the indication       */
/* handler for UUID 0x2A05 and triggers a partial or full     */
/* re-discovery of the affected attribute handle range.        */

Chapter 13 Complete — GATT Summary

GATT is the layer that turns ATT’s flat list of attributes into a navigable structure. Every BLE application — from a heart rate monitor to a firmware update service — is built on the eleven GATT features covered across this five-part series.

Discovery (Part 3)

  • Discover All Primary Services
  • Discover Service By UUID
  • Find Included Services
  • Discover All Characteristics
  • Discover Characteristics By UUID
  • Discover All Descriptors
Read/Write (Part 4)

  • Read single value
  • Read Long (blob pagination)
  • Read by UUID / Multiple
  • Write Without Response
  • Write + Signed Write
  • Write Long + Reliable Writes
Push & Housekeeping (Part 5)

  • Notifications (unreliable push)
  • Indications (reliable push)
  • Descriptor Read + Long
  • Descriptor Write + Long
  • 30-second timeout
  • Service Changed characteristic

Chapters 11, 12 & 13 — Complete

Security Manager → Attribute Protocol → Generic Attribute Profile — the complete BLE application stack fully covered with sniffer traces and BlueZ code examples.

← Part 4 Back to GATT Intro

Leave a Reply

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