BLE GATT Discovery Procedures MTU Exchange service discovery characteristic discovery

 

GATT Discovery Procedures

Chapter 13 · Part 3 of 5 · Sections 13.6.1–13.6.5 · Exchange MTU · Service Discovery · Characteristic Discovery · Descriptor Discovery

5Discovery features
ATT 0x2800Primary Service UUID
ATT 0x2803Characteristic UUID
Find InfoDescriptor discovery
SEO Keywords

BLE GATT Discover All Primary Services iterative BLE Discover Primary Services By UUID Find By Type Value BLE Find Included Services Read By Type Include BLE Discover All Characteristics service UUID Device Name BLE Discover Characteristics By UUID matching BLE Discover Characteristic Descriptors Find Information BLE Exchange MTU phone thermometer negotiation BlueZ gatttool GATT discovery C code

13.6.1 — Server Configuration: Exchange MTU

Setting the PDU Size Before Any GATT Work Begins

The very first GATT operation in a new connection should be MTU negotiation. Once agreed, this size applies to every ATT PDU exchanged during that connection — service discovery, characteristic reads, notification payloads, everything. The default of 23 bytes is tiny; negotiating a larger value dramatically improves throughput for any characteristic with multi-byte values.

Figure 13.11 — Exchange MTU in GATT Context
Client (Mobile Phone)Server (Thermometer)
Exchange MTU Request (Client Rx MTU = 300 octets)
Exchange MTU Response (Server Rx MTU = 40 octets)
Final ATT_MTU = min(300, 40) = 40 octets — both sides use this for all remaining PDUs
If server sends Error Response instead → ATT_MTU stays at default 23 octets

The phone offered 300 bytes — it has plenty of RAM. The thermometer only supports 40 bytes — it is a constrained sensor. Both sides must use the smaller value. The phone gains nothing from its larger buffer in this session, but the protocol remains correct. Only the client initiates this exchange, and only once per connection.

/* BlueZ: set preferred MTU before connecting                 */
/* The kernel negotiates MTU during L2CAP connection setup    */

/* Check what MTU was negotiated after connect:               */
sudo btmon 2>&1 | grep -i "mtu\|att_mtu"
/* [  12.345] ATT: Exchange MTU Request (0x02) len 2          */
/*              Client RX MTU: 517                            */
/* [  12.347] ATT: Exchange MTU Response (0x03) len 2         */
/*              Server RX MTU: 40                             */

/* In BlueZ source — att_get_mtu() returns negotiated MTU:    */
/* src/shared/att.c                                           */
uint16_t bt_att_get_mtu(struct bt_att *att)
{
    if (!att)
        return 0;
    return att->mtu;    /* set to min(client_mtu, server_mtu) */
}

13.6.2.1 — Discover All Primary Services

Walking the Entire Handle Space for Services

The client has no idea what services the server has when the connection first starts. This procedure maps them all out. It keeps firing Read By Group Type Requests, each time starting just above the last handle received, until the server has nothing left to return. A single Error Response with “Attribute Not Found” is the signal that discovery is complete.

ATT operation:Read By Group Type Request
Attribute Group Type:UUID for <<Primary Service>> = 0x2800
Starting Handle:0x0001 on first request, Last_End_Handle + 1 on subsequent
Ending Handle:0xFFFF always
Figure 13.12 — Iterative Discover All Primary Services
ClientServer
Read By Group Type Request (Start=0x0001, End=0xFFFF, UUID=0x2800)
Response: [Handle 1→7, UUID=1800] [Handle 16→19, UUID=1801] [Handle 80→82, UUID=1803]
Last End Handle = 0x0082 → next request starts at 0x0083
Read By Group Type Request (Start=0x0083, End=0xFFFF, UUID=0x2800)
Response: [Handle 83→85, UUID=1802] [Handle 86→88, UUID=1804]
Last End Handle = 0x0088 → next request starts at 0x0089
Read By Group Type Request (Start=0x0089, End=0xFFFF, UUID=0x2800)
Error Response — Attribute Not Found at handle 0x0089
Discovery complete — 5 primary services found

Figure 13.13 — Air Capture Analysis

The sniffer capture shows exactly this pattern. The client starts at handle 1, receives three services in one response (the packet fits three entries at 6 bytes each within the 23-byte default MTU: 1 + 3×6 = 19 bytes overhead leaves room for three). Then two more in the second pass. The third request returns nothing:

■ Air capture — Discover All Primary Services (Figure 13.13)
Pass 1 →Read By Group Type Request: handle 1 to 65535
Pass 1 ←Response: Start=1,End=7 | Start=16,End=19 | Start=80,End=82 (truncated in capture)
Pass 2 →Read By Group Type Request: handle 83 to 65535
Pass 2 ←Response: Start=83,End=85 | Start=86,End=88
Pass 3 →Read By Group Type Request: handle 89 to 65535
Pass 3 ←Error Response: Attribute Not Found — 5 services total retrieved
/* BlueZ gatttool discovers all primary services:             */
gatttool -b AA:BB:CC:DD:EE:FF --primary

/* Watch the actual ATT frames with btmon:                    */
sudo btmon

/* Expected output:                                           */
/* ATT: Read By Group Type Request (0x10)                     */
/*   Handle range: 0x0001-0xffff                              */
/*   Attribute group type: Primary Service (0x2800)           */
/* ATT: Read By Group Type Response (0x11)                    */
/*   Attribute data length: 6                                 */
/*   Attribute group list: 3 entries                          */
/*   handle: 0x0001, end: 0x0007, uuid: 0x1800 [Generic Access] */
/*   handle: 0x0010, end: 0x0013, uuid: 0x1801 [Generic Attribute] */
/*   handle: 0x0050, end: 0x0052, uuid: 0x1803 [Link Loss]   */

/* BlueZ C: programmatic service discovery via D-Bus          */
GDBusProxy *proxy = g_dbus_proxy_new_sync(...);
GVariant *result = g_dbus_proxy_call_sync(
    proxy, "GetManagedObjects", NULL, ...);
/* Parse result for org.bluez.GattService1 interface entries  */

13.6.2.2 — Discover Primary Service By Service UUID

Find One Specific Service Without Scanning Everything

When the client already knows what service it needs — say, it knows it wants to talk to the Heart Rate Service (UUID 0x180D) — there is no reason to walk the entire attribute database. Find By Type Value Request targets only attributes of type Primary Service whose value matches the requested UUID. This is faster and uses fewer packets.

ATT operation:Find By Type Value Request
Attribute Type:UUID for <<Primary Service>> = 0x2800
Attribute Value:Service UUID to find (16-bit only — Find By Type Value is restricted to 16-bit UUIDs)
Handle range:0x0001 → 0xFFFF, updated iteratively same as Discover All
Figure 13.14 — Discover Primary Service By UUID
ClientServer
Find By Type Value Req (Start=0x0001, End=0xFFFF, Type=0x2800, Value=0x180D)
Find By Type Value Response (Handle Info List: Start=0x000A, End=0x0013)
Find By Type Value Req (Start=0x0014, End=0xFFFF, Type=0x2800, Value=0x180D)
Error Response — Attribute Not Found → only one Heart Rate Service on this device

The response contains Handle Information List entries — each entry is a pair of (Starting Handle, Ending Handle) for one matching service. After all instances are found the server replies with Error Response.

/* Discover only Heart Rate Service (UUID 0x180D):            */
gatttool -b AA:BB:CC:DD:EE:FF --primary --uuid=0x180D

/* Output:                                                    */
/* attr handle: 0x000a, end grp handle: 0x0013 uuid: 0x180d  */

/* With bluetoothctl — filter by UUID:                        */
[AA:BB:CC:DD:EE:FF]# list-attributes
/* Look for entries labelled "Heart Rate" (0x180d)            */

13.6.3 — Relationship Discovery: Find Included Services

Discovering Which Other Services a Service References

A service can embed references to other services inside itself using Include Definitions. This procedure reveals those cross-references. Once the client has a service’s start and end handles (from primary service discovery), it scans the attributes in that range looking for Include Declaration attributes (UUID 0x2802). Each one found points to another service the current service depends on.

ATT operation:Read By Type Request
Attribute Type:UUID for <<Include>> = 0x2802
Starting Handle:Service start handle (from Primary Service Discovery)
Ending Handle:Service end handle (from Primary Service Discovery)
Figure 13.15 — Find Included Services with 128-bit UUID Fallback
ClientServer
Read By Type Request (Start=svc_start, End=svc_end, Type=0x2802)
Read By Type Response (Handle-Value pairs of included service declarations)
Each response value contains: Included Svc Handle + End Group Handle + UUID
Read Request (Attribute Handle) — only needed if included service has 128-bit UUID
Read Response (128-bit UUID value)
Continue iterating until…
Error Response — Attribute Not Found
16-bit vs 128-bit UUID handling: When the included service has a 16-bit UUID, that UUID fits inside the Read By Type Response value field directly. When the included service has a 128-bit UUID, the response value field cannot hold it — only the attribute handle of the included service declaration is returned. The client must then issue a separate ATT Read Request to that handle to retrieve the full 128-bit UUID.

Figure 13.16 — Air Log: No Included Services Found

The air log shows the client checking all five previously discovered services one by one. Every single Read By Type Request for included services comes back with an Error Response. This is the normal case for simple devices — included services are relatively rare and used mainly in complex device profiles.

/* Check included services for a specific service range:      */
gatttool -b AA:BB:CC:DD:EE:FF \
         --char-read --uuid=0x2802 \
         --start=0x0001 --end=0x0007

/* If nothing returned — no included services in that range   */
/* Expected: Error: Attribute Not Found                       */

/* BlueZ C code — scan for Include declarations:              */
/* src/shared/gatt-client.c: discover_included()              */
/* Uses bt_gatt_discover_included() internally which sends    */
/* ATT_OP_READ_BY_TYPE_REQ with type = BT_ATT_UUID_INCLUDE    */
#define BT_ATT_UUID_INCLUDE  0x2802

13.6.4.1 — Discover All Characteristics of a Service

Finding Every Characteristic in a Given Service

Now that the client knows which services exist and their handle ranges, it needs to find the characteristics inside each service. Characteristic Declarations (UUID 0x2803) are the markers — each one announces a new characteristic with its Properties byte, the handle where the actual value lives, and the characteristic’s UUID. The client reads all these declarations iteratively within the service’s handle range.

ATT operation:Read By Type Request
Attribute Type:UUID for <<Characteristic>> = 0x2803
Starting Handle:Service start handle
Ending Handle:Service end handle
Response value:Properties (1 byte) + Value Handle (2 bytes) + Characteristic UUID (2 or 16 bytes)
Figure 13.17 — Iterative Characteristic Discovery Within a Service
ClientServer
Read By Type Request (Start=1, End=7, Type=0x2803)
Response: [Handle=2, Props=Read, ValueHandle=3, UUID=Device Name] [Handle=4, …]
Last handle returned = 4 → next request starts at 5
Read By Type Request (Start=5, End=7, Type=0x2803)
Response: [Handle=6, Props=…, ValueHandle=7, UUID=Appearance]
Last handle = 6 → next request starts at 7
Read By Type Request (Start=7, End=7, Type=0x2803)
Error Response — Attribute Not Found → all characteristics found

Figure 13.18 Air Log — Decoding the First Response

The left panel of the sniffer capture shows the first response decoded. The Device Name characteristic sits at Attribute Handle 2. Its actual value (the device name string) is stored at Value Handle 3. The Properties byte says Read=Yes, all other operations (Write, Notify, Indicate) are not permitted on this characteristic.

Frame #878 — First Read By Type Response (Service 1: handles 1–7)
Characteristic 1 — Attribute Handle 2
Properties: Read=Yes, Write=No, Notify=No
Value Handle: 0x0003
UUID: 0x2A00 (Device Name)
Characteristic 2 — Attribute Handle 4
Properties: Read=Yes
Value Handle: 0x0005
UUID: 0x2A01 (Appearance)
/* Discover all characteristics in GAP service (handles 1-7): */
gatttool -b AA:BB:CC:DD:EE:FF \
         --characteristics --start=0x0001 --end=0x0007

/* Output:                                                    */
/* handle: 0x0002, char properties: 0x02, char value handle: */
/*   0x0003, uuid: 00002a00-0000-1000-8000-00805f9b34fb     */
/* handle: 0x0004, char properties: 0x02, char value handle: */
/*   0x0005, uuid: 00002a01-0000-1000-8000-00805f9b34fb     */

/* Properties 0x02 = bit 1 = Read permitted                  */
/* Properties 0x0a = bits 1+3 = Read + Write                 */
/* Properties 0x1a = bits 1+3+4 = Read + Write + Notify      */

/* BlueZ source: discover_chrcs() in src/shared/gatt-client.c */
/* Uses bt_gatt_discover_characteristics() which sends        */
/* ATT_OP_READ_BY_TYPE_REQ with BT_ATT_UUID_CHARACTERISTIC    */
#define BT_ATT_UUID_CHARACTERISTIC  0x2803

13.6.4.2 — Discover Characteristics By UUID

Targeted Characteristic Search When UUID Is Known

Same Read By Type sequence as Discover All Characteristics, but this time the client compares each returned characteristic’s UUID against the target UUID. The protocol does not filter at the ATT level — the server still returns all characteristics in the range. The client performs the UUID matching itself on the received data. The search always continues to the end of the service range even after a match is found, because the same UUID could theoretically appear multiple times in one service.

Figure 13.19 — Client-Side UUID Matching During Characteristic Discovery
ClientServer
Read By Type Request (Start, End, Type=0x2803)
Response: [Handle=X, Props, ValueHandle, UUID] [Handle=Y, Props, ValueHandle, UUID]
Client checks each UUID against target — match found → record it, keep going
Read By Type Request (Start=last_handle+1, End, Type=0x2803)
Error Response — all characteristics have been checked
/* Find Heart Rate Measurement characteristic (UUID 0x2A37):  */
gatttool -b AA:BB:CC:DD:EE:FF \
         --characteristics --uuid=0x2A37

/* Returns only the matching characteristic:                  */
/* handle: 0x000b, char properties: 0x10, char value handle: */
/*   0x000c, uuid: 00002a37-0000-1000-8000-00805f9b34fb     */
/* Properties 0x10 = bit 4 = Notify only                     */

13.6.5 — Characteristic Descriptor Discovery

Finding CCCD and Other Descriptors Attached to a Characteristic

Descriptors live in the attribute space after the characteristic’s value declaration and before the next characteristic declaration. To discover them, the client uses Find Information Request starting from (value handle + 1) through the last handle of the service. The response gives handle-UUID pairs for every attribute in that slot — each is a descriptor.

The most important descriptor is the Client Characteristic Configuration Descriptor (CCCD) with UUID 0x2902. Without writing to the CCCD, the client will never receive notifications or indications from the server, no matter how the characteristic’s Properties byte is set.

ATT operation:Find Information Request
Starting Handle:Characteristic Value Handle + 1
Ending Handle:End of the enclosing service (or next characteristic’s handle − 1)
Response Format:0x01 = all UUIDs 16-bit, 0x02 = all UUIDs 128-bit (cannot mix in one response)
Figure 13.20 — Discover All Characteristic Descriptors
ClientServer
Find Information Request (Start = value_handle+1, End = svc_end)
Find Information Response (Format=0x01, [[0x000d, 0x2902], [0x000e, 0x2901]])
Client found: CCCD at 0x000d, User Description at 0x000e
Find Information Request (Start = 0x000f, End = svc_end)
Error Response — no more descriptors
CCCD (0x2902)
Write 0x0001 to enable notifications
Write 0x0002 to enable indications
Write 0x0000 to disable both
Must be discovered before subscribing
User Description (0x2901)
UTF-8 string — human-readable label
Example: “Room Temperature”
Read-only on most devices
Presentation Format (0x2904)
Format (uint8/int16/float…) + unit
Exponent for SI scaling
Allows generic value parsers
/* Discover all descriptors for a characteristic:             */
gatttool -b AA:BB:CC:DD:EE:FF --char-desc \
         --start=0x000c --end=0x0013

/* Typical output for HR Measurement characteristic:          */
/* handle: 0x000c, uuid: 0x2a37 (Heart Rate Measurement)     */
/* handle: 0x000d, uuid: 0x2902 (CCCD ← enable notify here)  */

/* BlueZ: Find Information uses ATT opcode 0x04               */
/* Response opcode 0x05                                       */
/* src/shared/gatt-client.c: discover_descs()                 */
/* bt_gatt_discover_descriptors(client->att,                  */
/*     chrc->value_handle + 1, end_handle,                    */
/*     discover_desc_cb, op, NULL)                            */

Next — GATT Read & Write Operations

Part 4 covers all four Characteristic Value Read sub-procedures and all five Characteristic Value Write sub-procedures with complete MSC diagrams.

Part 4: Read & Write → ← Part 2

Leave a Reply

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