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
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.
/* 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.
- Continuous streaming data
- Missing one sample is OK
- High frequency (1–100 Hz)
- Heart rate, accelerometer, steps
- 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.
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.
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.
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.
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).
/* 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.
/* 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.
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.
/* 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.
- Discover All Primary Services
- Discover Service By UUID
- Find Included Services
- Discover All Characteristics
- Discover Characteristics By UUID
- Discover All Descriptors
- Read single value
- Read Long (blob pagination)
- Read by UUID / Multiple
- Write Without Response
- Write + Signed Write
- Write Long + Reliable Writes
- 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.
