Skip to main content BLE GATT/ATT Protocol Analysis | IoT Worker

BLE GATT/ATT Protocol Analysis

A successful connection does not automatically make the data plane clear. Many GATT problems are not caused by packets being lost. They are caused by not knowing what object is being accessed in the first place: Handle, UUID, Characteristic Value, and CCCD often get mixed together in logs.

This article focuses on the BLE 4.2 and later scenarios that matter most in development, integration, and packet analysis. Advertising and connection setup, pairing and bonding, security levels, and PHY/Data Length Extension are not expanded unless they directly affect GATT behavior.

First Separate What the Data Plane Is Actually Accessing

When reading GATT/ATT, keep this split in mind:

  • ATT handles how attributes are accessed by Handle
  • GATT handles how those attributes are organized into Service and Characteristic

The most direct engineering picture is a numbered data table:

App
  -> GATT: defines how Service / Characteristic / Descriptor are organized
    -> ATT: defines how reads, writes, discovery, and notifications work by Handle
      -> L2CAP: carries ATT PDUs

What is actually transmitted on the wire is an ATT PDU, not the abstract idea of a Service or Characteristic. GATT tells both sides which Handle range belongs to which Service, which Handle contains the business value, and which Descriptor holds the notification switch.

Do not confuse the common roles

  • Central / Peripheral are link roles: who initiates the connection and who is being connected
  • GATT Client / Server are data-access roles: who sends ATT requests and who maintains the attribute table

In most products:

  • Phone or gateway: Central + GATT Client
  • Sensor or peripheral: Peripheral + GATT Server

These are not rigidly tied together, but they are the most common pairing.

What a typical session looks like

After connection, the common path is usually:

  1. Optional Exchange MTU
  2. Client discovers Primary Services
  3. Client discovers Characteristics and Descriptors
  4. Client reads static data or writes configuration
  5. Client writes CCCD to enable Notification or Indication
  6. Server later pushes data or continues answering reads and writes

When a capture is hard to understand, first decide which of these six steps you are in.

What an Attribute Table Looks Like

Every Attribute has only three core elements

Element Length Purpose
Handle 16 bit The access entry point
UUID 16 bit or 128 bit The type identifier
Value 0 to 512 bytes The actual content

The easy thing to miss is this:

  • Handle is the access address
  • UUID is the type
  • The read or write operation targets the Value behind that Handle

So when a packet says Read Request(handle=0x0025), it means “access the Value of attribute 0x0025,” not “read the UUID itself.”

GATT structure is really an organization rule for Attributes

The usual GATT hierarchy is:

Profile
  -> Service
    -> Characteristic
      -> Descriptor

But on the wire it becomes a linear Attribute Table. A notify-capable Characteristic usually occupies at least these entries:

Handle   UUID     Meaning
0x0001   0x2800   Primary Service Declaration
0x0002   0x2803   Characteristic Declaration
0x0003   0x2A37   Characteristic Value
0x0004   0x2902   CCCD

Two things matter:

  • 0x2803 is a declaration, not the value itself
  • The client reads and writes the Value Handle, not the Declaration Handle

Why a Characteristic usually needs at least two Handles

A Characteristic minimally includes:

  1. Characteristic Declaration
  2. Characteristic Value

If it supports Notification or Indication, it usually also needs CCCD (0x2902). So the common pattern is:

  • Declaration Handle
  • Value Handle
  • CCCD Handle

They sit next to each other, but they do different jobs.

Properties are not the same as permissions

Characteristic Properties describe which operations are supported:

Bit Property Meaning
1 Read Supports read
2 Write Without Response Supports Write Command
3 Write Supports Write Request
4 Notify Supports Notification
5 Indicate Supports Indication

But actual access still depends on permissions, for example:

  • Read is allowed only after encryption
  • Write is allowed only after authentication

So it is not contradictory to see Read in Properties and still receive Insufficient Authentication (0x05).

Engineering conclusion

  • When reading a GATT database, find the Value Handle and CCCD first
  • When a Characteristic behaves strangely, separate Properties, permissions, and CCCD
  • When designing custom services, keep Handle layout simple and contiguous

The Most Common Post-Connection Flow

The usual path after connection is MTU negotiation -> Service Discovery -> Characteristic Discovery -> Descriptor Discovery.

Start with MTU negotiation

The default ATT MTU is 23 bytes, which limits how much a single PDU can carry:

  • Read Response can carry at most 22 bytes of Value
  • Write Request can carry at most 20 bytes, because Opcode and Handle also consume space

Typical negotiation:

Client -> Exchange MTU Request (247)
Server -> Exchange MTU Response (185)
Final ATT MTU = min(247, 185) = 185

What matters most is:

  • The negotiated result is the minimum of the two sides
  • MTU is usually negotiated once per connection
  • All later ATT PDUs are constrained by this MTU

Service Discovery: find the boundaries first

The client usually sends:

  • Read By Group Type Request
  • Type = 0x2800 for Primary Service

The important results are:

  • Start Handle of the Service
  • End Handle of the Service
  • Service UUID

The real value of Service Discovery is not “reading the service name.” It is cutting the attribute table into service-sized segments.

Characteristic Discovery: find the value Handle

Once the service range is known, the client sends:

  • Read By Type Request
  • Type = 0x2803 for Characteristic Declaration

The important result is not the declaration Handle itself, but:

  • Properties
  • Value Handle
  • Characteristic UUID

A very common mistake is to treat the 0x2803 Handle as the business-data Handle. In most cases, the actual data access target is the returned Value Handle.

Descriptor Discovery: mostly to find CCCD

If a Characteristic supports Notify or Indicate, the next common step is:

  • Find Information Request

The goal is usually to find 0x2902, the CCCD.

Quick capture checklist

  1. Did Exchange MTU happen?
  2. Did Read By Group Type find the target Service?
  3. Did Read By Type find the target Characteristic Value Handle?
  4. Did Find Information find 0x2902?
  5. Are later reads, writes, or notifications using the correct Handle?

If step 2 already fails to find the target Service, all later read/write failures are only symptoms, not the root cause.

Why Service Changed matters

If a device updates firmware and the GATT database changes while the client still uses an old cache, it is easy to end up with:

  • Reading the wrong Handle
  • Writing to an old Handle
  • Writing CCCD successfully but seeing no effect

At that point, pay attention to Generic Attribute Service (0x1801) and its Service Changed (0x2A05). It tells a client that caches the database: “the Handle layout changed, you need to rediscover it.” This kind of message is usually sent through Indication, because it should not be dropped silently.

Engineering conclusion

  • The discovery flow is really about building a mapping from UUID to Handle
  • Service Discovery finds boundaries, Characteristic Discovery finds the Value Handle, and Descriptor Discovery mostly looks for CCCD
  • If the firmware upgrade changes the GATT table structure, handle Service Changed or an equivalent versioning strategy seriously

Reads, Writes, and Long Data Transfer

The most common read operation: Read Request

The most basic read is:

Client -> Read Request(handle)
Server -> Read Response(value)

It is suitable for:

  • Device name
  • Firmware version
  • Current configuration value
  • Low-frequency state data

If the Value length does not exceed MTU - 1, this is the most direct read method.

When the Value is too long, use Read Blob

When a single Read Response cannot carry the full data, the client keeps sending:

  • Read Blob Request(handle, offset)

Then it reads the data piece by piece using offsets.

The most common mistakes here are:

  • Assuming “the characteristic value can be up to 512 bytes, so one Read can fetch it all,” when it still depends on MTU
  • Hitting offset overflow, which commonly returns Invalid Offset (0x07) or Attribute Not Long (0x0B)

How to choose between Write Request and Write Command

The difference is not just “one has a response and one does not.” The engineering meaning matters too.

Operation Characteristic Suitable for
Write Request Has a response, more reliable Configuration parameters, control commands, important one-shot writes
Write Command No response, higher link utilization High-frequency streaming data, cases where occasional loss is acceptable

If you are writing:

  • Mode switches
  • On/off control
  • Calibration parameters
  • Persistent configuration

then Write Request is the default first choice. Only use Write Command after you confirm the business logic can tolerate loss, retransmission, and no confirmation.

Long writes and reliable writes: Prepare / Execute Write

When the data is too large for a single packet, or you need “either everything succeeds or nothing takes effect,” use:

  • Prepare Write Request
  • Execute Write Request

This path is not common in normal sensor data streams, but it appears in:

  • Large configuration writes
  • Some custom protocol packaging for firmware fragments
  • Multi-field updates that need atomic commit

Unless you really need atomic commit or segmented caching, do not start with Prepare/Execute Write. The implementation, buffering, and error handling are all more complicated.

Signed write is rarely the main path

Signed Write Command depends on CSRK and is not commonly the main solution in modern products. In most projects, a more common and easier-to-maintain approach is:

  • Establish an encrypted connection directly
  • Do ordinary read/write after the security level is sufficient

If there is no legacy compatibility constraint, focus on encrypted connections instead of Signed Write.

Engineering conclusion

  • Use Read first for static data and low-frequency state
  • Use Write Request first for important writes
  • Use Notification for high-frequency upstream data instead of polling with frequent Read
  • For long-data scenarios, first calculate MTU, fragmentation, buffer, and timeout before deciding whether Read Blob or Prepare/Execute Write is necessary

Notification, Indication, and CCCD

The essential difference between Notification and Indication

Both are server-initiated pushes to the client, but the reliability model is different.

Mechanism Confirmation needed Throughput Suitable for
Notification No Higher High-frequency sensor data, continuous status reporting
Indication Yes, Handle Value Confirmation Lower Critical events, information that must not be dropped silently

From a capture perspective:

  • After a Notification is sent, the server does not wait for an application-layer confirmation
  • After an Indication is sent, the next similar send usually waits for a Confirmation from the client

So if the reporting rate is visibly blocked, suspect whether Indication was used by mistake.

CCCD is the client-side subscription switch

Support for Notify/Indicate alone is not enough. The client must also write CCCD (0x2902) to truly subscribe.

Common values:

Value Meaning
0x0000 Everything off
0x0001 Enable Notification
0x0002 Enable Indication
0x0003 Enable both

This is also a high-frequency checkpoint in packet analysis:

  • If the server never pushes data, first check whether the client successfully wrote CCCD
  • If CCCD was written but there is still no data, then check whether the correct CCCD Handle was written

Why many “notification failures” are not actually Notification problems

Common root causes are usually:

  • The wrong Handle was written, and the Value Handle was mistaken for the CCCD Handle
  • The Characteristic supports only Notify, but the client wrote 0x0002
  • Permissions were missing, and CCCD write was rejected
  • CCCD was not restored after reconnect, but the application assumed it was still enabled
  • The firmware upgraded and Handle values changed, but the client still used an old cache

Whether CCCD persists depends on your bonding strategy

If the device supports Bonding, it usually persists configuration associated with the peer, such as:

  • Bond information
  • Identity information
  • CCCD configuration
  • Whether Service Changed must be sent

If there is no Bonding, do not assume CCCD survives a disconnect and reconnect. The safest engineering approach is:

  • After every connection, have the client explicitly check or reconfigure subscription state

Engineering conclusion

  • Use Notification by default for high-frequency data; only consider Indication for critical state changes
  • When Notification does not work, check CCCD first, then the Handle, then permissions
  • As soon as reconnect, bonding, or upgrade is involved, consider CCCD restoration and cache invalidation together

The Most Common Development Errors and Where to Start Debugging

ATT Error Response is useful not just because it says “failed,” but because it tells you where to start.

High-frequency error codes

Error code Name Priority for investigation
0x01 Invalid Handle Handle wrong, cache stale, database changed
0x02 Read Not Permitted The attribute does not support read, or you are reading the wrong object
0x03 Write Not Permitted The attribute does not support write, or you wrote to the wrong Handle
0x05 Insufficient Authentication Authentication is required, usually pairing or bonding is needed
0x07 Invalid Offset Blob offset error
0x0A Attribute Not Found Target UUID is not in the current search range
0x0D Invalid Attribute Value Length Write length does not match the requirement
0x0F Insufficient Encryption Encrypted link required
0x11 Insufficient Resources Not enough device-side buffer, queue, or memory

How to judge first when an error appears

Use this order:

  1. Is the problem a wrong Handle or a permission problem?
  2. Is the protocol unsupported, or is the current security level insufficient?
  3. Is it a length/offset problem, or is the expected MTU wrong?
  4. Is the client cache wrong, or did the server database really change?

In particular, 0x05 and 0x0F are usually not GATT logic bugs. They mean the security preconditions were not met.

The fields most worth checking in a capture

  • Opcode
  • Handle
  • Value Handle
  • UUID
  • Error Code
  • MTU
  • CCCD Value

Many development problems do not require starting from the specification. Connect these fields first, and the root cause often becomes much smaller.

Engineering conclusion

  • Invalid Handle usually means cache and table-structure changes should be suspected first
  • Not Permitted usually means the wrong Handle or operation type was used
  • Authentication / Encryption errors should take you back to pairing and security level, not just GATT code

Custom Service Design Recommendations

  • When a standard Service or Characteristic can be reused, prefer the SIG-assigned UUID
  • When custom UUIDs are required, keep them in one stable 128-bit UUID plan; do not generate them randomly and separately
  • Do not let the UUID semantics of one Service or Characteristic change across firmware versions; add a new entry when new capability is needed
  • Separate “read configuration,” “write configuration,” “status reporting,” and “large data stream” into different Characteristics instead of letting one Value do everything
  • If data needs continuous reporting, design it as a Notification path first, rather than forcing the client to poll with frequent reads
  • If firmware may be upgraded and the table structure may change, design cache invalidation and Service Changed behavior in advance

When MTU, DLE, throughput, and connection-parameter trade-offs are involved, go back to BLE Link Layer / PHY and BLE GAP Protocol Analysis.

References

Bluetooth Core Specification

  • Bluetooth Core Specification
    • Volume 3, Part F: Attribute Protocol (ATT)
      • Section 3: Protocol requirements
      • Section 4: Protocol data units
    • Volume 3, Part G: Generic Attribute Profile (GATT)
      • Section 3: Service interoperability requirements
      • Section 4: Profile hierarchy and data relationships
      • Section 5: Sub-procedures and feature requirements
  • Official entry: https://www.bluetooth.com/specifications/specs/

Assigned Numbers

Further Reading