Wiki / Protocols

ATT (Attribute Protocol)

The BLE wire protocol underneath GATT: every read, write, and notification is one small PDU addressed to a numeric attribute handle.

ATT Write Request PDU
Opcode (1 byte) 0x12
Handle (2 bytes, little-endian) 14 00
Value (rest of the PDU) 01
A write to handle 0x0014 with the value 0x01 is four bytes on the air: 12 14 00 01. There is no length field for the value, it simply runs to the end of the PDU, which is why the negotiated MTU is what bounds how much you can write in one go.

ATT (the Attribute Protocol) is the layer that actually moves data on a BLE connection. It knows nothing about services, characteristics, or device features. It knows a flat table of numbered attributes and a short list of operations to read and write them. Everything you do to a BLE peripheral, from the vendor app to gatttool, ends up as ATT PDUs on the air.

What it is

A BLE peripheral holds one attribute table. Each row is an attribute with three things: a handle (a 16-bit number, the address), a type (a UUID saying what the row means), and a value (raw bytes). That is the whole model. The table is ordered by handle, handles are assigned by the peripheral, and there are no gaps you can rely on.

ATT is a client/server protocol on top of the L2CAP fixed channel for attributes. The client (your laptop, the phone) sends requests; the server (the device) answers. Each PDU starts with a one-byte opcode, followed by opcode-specific fields. The operations split into a few families:

Family Behaviour
Request / Response Client asks, server must answer. One outstanding request at a time.
Command Client sends, server never answers. Fire and forget.
Notification Server pushes a handle and value, client does not acknowledge.
Indication / Confirmation Server pushes, client must confirm before the next one.

The ones you meet constantly:

Opcode PDU Note
0x0A Read Request Returns the value of one handle
0x12 Write Request Writes a handle, server sends a Write Response
0x52 Write Command Writes a handle, no response, no confirmation
0x1B Handle Value Notification Server pushes a value to the client
0x1D Handle Value Indication Same, but the client must confirm with 0x1E
0x01 Error Response Carries the failing opcode, handle, and an error code

Why it matters

Two consequences fall straight out of this design, and both are where devices fail.

First, everything is handle-addressed. The app's notion of "the unlock button" is a label it invented; on the wire there is only a handle and some bytes. If you know the handle and the value, you can send the same PDU the app sends, from any client, with no app involved and no UUID lookup. This is why an app that hides a control behind a login screen has hidden nothing.

Second, access control is per attribute. Each row carries its own permissions: readable, writable, and whether that requires encryption, authentication, or authorisation. Nothing enforces consistency across the table, and nothing forces a vendor to set anything at all. The default for an attribute the developer did not think about is usually open. A device can therefore be "encrypted" and still expose a writable control attribute to anyone who connects.

How to work with it

You rarely speak raw ATT by hand; you drive it through a GATT client and read the result as ATT. The useful skill is being able to move between the two views.

# Enumerate the table, handles included.
gatttool -b AA:BB:CC:DD:EE:FF --characteristics

# Read one handle directly. This is a Read Request, opcode 0x0A.
gatttool -b AA:BB:CC:DD:EE:FF --char-read --handle 0x0011

# Write one handle directly. --char-write-req is 0x12 (acknowledged),
# --char-write is 0x52 (no response).
gatttool -b AA:BB:CC:DD:EE:FF --char-write-req --handle 0x0014 --value 01

In Wireshark, the dissector is btatt. Set btatt as the display filter and a captured write appears decoded as Write Request, Handle: 0x0014, Value: 01, with the raw 12 14 00 01 underneath. Reading a capture at this layer is often faster than reverse engineering the app, because the app's obfuscation does not survive the trip to the wire.

MTU

The ATT MTU is the maximum size of a single ATT PDU, negotiated with Exchange MTU Request at the start of a connection. The default is 23 bytes, and a Write Request spends 3 of them on the opcode and the handle, which leaves 20 bytes of payload. That 20 is where the number comes from when a vendor protocol suddenly starts splitting messages. Because the value field of a write simply runs to the end of the PDU, the MTU sets how many bytes fit in one write. Anything longer has to use the long-write flow (Prepare Write plus Execute Write) or the device's own chunking scheme on top. If a device's protocol looks like it sends the same command in odd-sized fragments, that is usually the MTU showing through, not a checksum.

Pitfalls

  • A handle is not a UUID and is not stable. Handles are assigned by the peripheral and can change across firmware versions, and sometimes across bonds. Hardcoding a handle you found once will break; rediscover it, or resolve the UUID to a handle at runtime.
  • A characteristic occupies more than one handle. The declaration and the value live at different handles, and descriptors take their own. Writing to the declaration handle instead of the value handle is the classic off-by-one and returns an error, not a hint.
  • An Error Response is information. Insufficient Authentication means the attribute exists and is gated; Attribute Not Found or Read Not Permitted mean something else entirely. Read the error code rather than treating every failure as "it did not work".
  • Notification is not delivery confirmation. A notification is unacknowledged by design. If you need to know the peripheral saw something, look at what it does next, not at the absence of an error.
  • Write Command is silent on failure. A 0x52 write to a handle that rejects it produces nothing at all. If a write seems to do nothing, retry it as a Write Request so the device is forced to answer.

Further reading