BLE ATT protocol MTU In ATT, ATT Read Methods

 

ATT Read Methods — All Request/Response Operations

Chapter 12 Part 2 of 4 · Exchange MTU · Find Info · Find By Type · Read By Type · Read · Read Blob · Read Multiple · Read By Group Type

23 octetsDefault ATT_MTU
Table 12.1All 11 request pairs
Read BlobLong value pagination
Group TypeService discovery
SEO Keywords:

BLE ATT Exchange MTU 23 octets negotiation BLE Find Information Request handle UUID pairs BLE Read By Type Request handle value ascending BLE Read Blob Request Value Offset long attribute BLE Read By Group Type Request primary service discovery BLE ATT error response attribute not found BlueZ gatttool bluetoothctl GATT read attributes

12.4.1.1 — Exchange MTU Request & Response

Negotiating a Bigger Packet Size

The default ATT_MTU for BLE is 23 octets. This means by default, the largest ATT payload in a single PDU is 23 bytes. For many sensors (temperature, heart rate) this is plenty. But for applications that transfer firmware updates, audio data, or larger records, 23 bytes per round-trip is very slow.

The Exchange MTU procedure lets the client tell the server what receive size it supports, and learn what size the server supports. Both sides then use the smaller of the two values for the rest of the connection.

Figure 12.5 — Exchange MTU Negotiation
Client Server
Client
Exchange MTU Request (Client Rx MTU = 512)
Server
Exchange MTU Response (Server Rx MTU = 23)
Final ATT_MTU = min(512, 23) = 23 octets — both sides use 23 for all further PDUs

The Exchange MTU Request is sent only once per connection and only by the client. The server cannot initiate this exchange. If the server’s MTU is larger than the default, it waits for the client to ask.

/* Set larger MTU with BlueZ gatttool                        */
gatttool -b AA:BB:CC:DD:EE:FF --mtu=512 -I

/* Or with bluetoothctl (MTU negotiated automatically)       */
/* BlueZ will use the maximum supported by the adapter       */

/* Check negotiated MTU in kernel log:                       */
/* dmesg | grep -i "mtu"                                     */
/* [  123.456] Bluetooth: hci0: ATT MTU negotiated: 247      */

/* Programmatic MTU request with BlueZ D-Bus:                */
/* org.bluez.GattCharacteristic1 uses the negotiated MTU     */
/* automatically — no manual ATT MTU request needed          */

12.4.1.3 — Find Information Request & Response

Getting Handle-Type Pairs for a Range of Attributes

This is how a client learns the type (UUID) of every attribute in a handle range without reading their values. It is most useful during service discovery when a client needs to identify what kind of attribute lives at each handle before deciding whether to read it.

Figure 12.6 — Find Information Request and Response
ClientServer
Find Information Request (Starting Handle, Ending Handle)
Find Information Response (Format, Attribute Data List)

Response details: The Format field tells the client whether the UUIDs in the response are 16-bit or 128-bit (both cannot be mixed in one response). The Attribute Data List contains handle-UUID pairs in ascending handle order. If the range is long, the response may span multiple PDUs — but individual pairs are never split across PDUs.

12.4.1.5 — Find By Type Value Request & Response

Locating Attributes Matching Both Type AND Value

More selective than Find Information. The client specifies a 16-bit UUID for the type and a specific value to match. The server returns only the handles of attributes that match both. This is the operation GATT uses internally to discover services by UUID — for example, find all attributes where Type = Primary Service AND Value = Heart Rate Service UUID.

Figure 12.7 — Find By Type Value
ClientServer
Find By Type Value Request (Start Handle, End Handle, UUID, Value)
Find By Type Value Response (Handle Information List)

UUID restriction: The Attribute Type in this request can only be a 16-bit UUID. 128-bit UUIDs are not supported in this particular operation.

12.4.1.7 — Read By Type Request & Response

Reading All Attributes of a Given Type in One Pass

Instead of discovering handles then reading each one individually, Read By Type lets a client get the handle and value of every attribute with a specified UUID in one round trip. GATT uses this to discover all characteristics within a service — the client asks “give me all attributes with Type = Characteristic and I get back every characteristic declaration with its properties and value handle.”

Figure 12.8 — Read By Type Request and Response
ClientServer
Read By Type Request (Start Handle, End Handle, Attribute Type UUID)
Read By Type Response (Length, Attribute Data List — handle-value pairs)

Length field: The response includes a Length field indicating the byte size of each handle-value pair. All pairs in a single response have the same length. Pairs are returned in ascending handle order and are never split across PDUs. UUID can be 16-bit or 128-bit.

12.4.1.9 — Read Request & Response

Reading a Single Attribute by Handle

The most basic ATT operation. The client already knows the handle (from a previous discovery) and wants the current value. The server responds with the value. If the value is larger than ATT_MTU minus 1 bytes, only the first ATT_MTU-1 bytes are returned. The remainder must be fetched with Read Blob.

Figure 12.9 — Read Request and Response
ClientServer
Read Request (Attribute Handle)
Read Response (Attribute Value — up to ATT_MTU-1 bytes)
/* Read a characteristic value with gatttool                 */
gatttool -b AA:BB:CC:DD:EE:FF --char-read --handle=0x002A

/* With bluetoothctl:                                        */
[AA:BB:CC:DD:EE:FF]# select-attribute /org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0010/char0011
[AA:BB:CC:DD:EE:FF]# read

/* With BlueZ D-Bus / Python:                               */
import dbus
bus = dbus.SystemBus()
char = bus.get_object('org.bluez', '/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF/service0010/char0011')
char_iface = dbus.Interface(char, 'org.bluez.GattCharacteristic1')
value = char_iface.ReadValue({})
print(bytes(value))                                          

12.4.1.11 — Read Blob Request & Response

Reading Long Attribute Values One Chunk at a Time

When a value is larger than ATT_MTU-1 bytes, the Read Request only returns the first chunk. Read Blob continues from a specified offset. The client keeps incrementing the offset and sending Read Blob Requests until the response comes back shorter than ATT_MTU-1 bytes — that signals the end of the value.

Figure 12.10 — Read Blob Pagination for Long Attribute Values
ClientServer
Value = 60 bytes, ATT_MTU = 23, so 22 bytes per Read/Blob
Read Request (Handle) → gets bytes 0–21
Read Response (22 bytes — value[0..21])
Read Blob Request (Handle, Value Offset = 22)
Read Blob Response (22 bytes — value[22..43])
Read Blob Request (Handle, Value Offset = 44)
Read Blob Response (16 bytes — value[44..59]) ← shorter = end of value
Full 60-byte value assembled by client

Value Offset is zero-based. Offset=0 means start from the first byte. This is the same information the server used in the Read Request (offset 0 is implicit there). The client must reassemble the chunks in order.

12.4.1.13 — Read Multiple Request & Response

Reading Several Attributes in One Round Trip

Instead of a separate Read Request for each attribute, Read Multiple sends a set of two or more handles in one PDU. The server concatenates the values in the same order as the handles provided and returns them all in one Response. This is efficient when polling multiple fixed-length values simultaneously — for example, reading temperature, humidity, and pressure in a single ATT transaction.

Figure 12.11 — Read Multiple
ClientServer
Read Multiple Request (Set of Handles: 0x0025, 0x0027, 0x0029)
Read Multiple Response (value[0x0025] || value[0x0027] || value[0x0029])

Important limitation: Values are concatenated without any delimiters or length markers. For this to work correctly, all requested attribute values must be of known, fixed length so the client can split the concatenated blob correctly. Variable-length values cannot be reliably decoded from a concatenated response.

12.4.1.15 — Read By Group Type Request & Response

Discovering All Service Boundaries at Once

This is the backbone of service discovery in BLE. Read By Group Type lets a client ask “give me all the Primary Service declarations and their handle ranges”. The server returns each service’s start handle, end handle (the last attribute in that service), and service UUID. The client iterates through the full handle space until it gets an Error Response meaning there are no more services.

Figure 12.12 — Read By Group Type Sequence
ClientServer
Read By Group Type Request (Start=0x0001, End=0xFFFF, Group UUID=Primary Service)
Read By Group Type Response (Length=6, Attribute Data List)

The response’s Attribute Data List contains tuples of (Start Handle, End Group Handle, Service UUID). The End Group Handle is the handle of the last attribute inside that service definition. When the client sees the last handle in the response, it sends another request starting from that handle + 1, and repeats until it gets an Error Response.

Figures 12.20–12.25 — Practical Primary Service Discovery Walk-Through

Request 1 — Start Handle = 0x0001
→ Read By Group Type (0x0001 → 0xFFFF, UUID=Primary Service 0x2800)
← Response: 3 services (Length=6 per entry):
• Generic Access Profile — handles 0x0001 to 0x0007
• Generic Attribute Profile — handles 0x0016 to 0x0019
• Link Loss Alert — handles 0x0080 to 0x0082
Last handle = 0x0082 → next request starts at 0x0083

Request 2 — Start Handle = 0x0083
→ Read By Group Type (0x0083 → 0xFFFF, UUID=Primary Service 0x2800)
← Response: 2 more services:
• Immediate Alert — handles 0x0083 to 0x0085
• Tx Power — handles 0x0086 to 0x0088
Last handle = 0x0088 → next request starts at 0x0089

Request 3 — Start Handle = 0x0089
→ Read By Group Type (0x0089 → 0xFFFF, UUID=Primary Service 0x2800)
← Error Response: Attribute Not Found — no more Primary Services
Client knows it has received ALL primary services: GAP, GATT, Link Loss, Immediate Alert, Tx Power
/* Discovering all primary services with gatttool             */
gatttool -b AA:BB:CC:DD:EE:FF --primary

/* Output example:                                           */
/* attr handle: 0x0001, end grp handle: 0x0007 uuid: 1800   */  
/* attr handle: 0x0016, end grp handle: 0x0019 uuid: 1801   */
/* attr handle: 0x0080, end grp handle: 0x0082 uuid: 1803   */
/* attr handle: 0x0083, end grp handle: 0x0085 uuid: 1802   */

/* gatttool fires multiple Read By Group Type Requests       */
/* automatically until Error Response received               */

/* Finding a specific service by UUID:                       */
gatttool -b AA:BB:CC:DD:EE:FF --primary --uuid=0x180D
/* Returns only the Heart Rate Service entry                 */

Next — Write Methods, Commands, Notifications & Indications

Part 3 covers Write Request/Response, Prepare Write for long values, Execute Write, Write Command, Signed Write, Handle Value Notification, and Handle Value Indication/Confirmation.

Next: Part 3 Write & Notify → ← Part 1

Leave a Reply

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