ATT Write, Commands, Notifications & Indications
Chapter 12 Part 3 of 4 · Write Request · Prepare/Execute Write · Write Command · Signed Write · Notification · Indication · Confirmation
12.4.1.17 — Write Request & Response
A Confirmed Write — Server Must Acknowledge
The most common write operation. The client sends a new value for an attribute and waits for the server to acknowledge that it was written successfully. If anything goes wrong — wrong permissions, attribute doesn’t exist, value out of range — the server sends an Error Response instead of a Write Response.
/* Write with acknowledgement using gatttool */
gatttool -b AA:BB:CC:DD:EE:FF --char-write-req \
--handle=0x0011 --value=01
/* Python with BlueZ D-Bus: */
char_iface.WriteValue([0x01], {}) # acknowledged write */
/* C with BlueZ socket API — Write Request opcode = 0x12: */
uint8_t pdu[] = {
0x12, /* Write Request opcode */
0x11, 0x00, /* Attribute Handle (little-endian) */
0x01 /* Value to write */
};
send(att_sock, pdu, sizeof(pdu), 0);
/* Receive Write Response (opcode 0x13) to confirm */
12.4.1.19 — Prepare Write Request & Execute Write
Writing Long Values That Don’t Fit One PDU
When an attribute value is too large for a single Write Request (limited to ATT_MTU-3 bytes), the client uses Prepare Write + Execute Write. The client sends the value in chunks with Prepare Write Requests, the server queues each chunk, and finally the client sends an Execute Write Request to commit everything atomically.
This two-phase commit guarantees the attribute either gets fully updated or not at all. The server never applies partial writes — the attribute’s value only changes once Execute Write with Flags=0x01 arrives.
Discard all queued Prepare Write data without writing anything. Used when the client detects an error mid-sequence and wants to abort cleanly.
Write all queued data to the attribute in the order it was queued. The attribute’s new value becomes live immediately after this response.
Multi-client isolation: When several clients are simultaneously connected, each client’s Prepare Write queue is completely separate. Client A’s Execute Write does not affect Client B’s pending writes at all.
What the echo response does: The server’s Prepare Write Response echoes back the exact Handle, Offset, and data that was received. This lets the client verify the server received the data correctly before sending the next chunk. If there is a mismatch, the client can cancel and retry.
12.4.2.1 — Write Command
Fire and Forget — No Acknowledgement Ever
The Write Command is a one-way write with no acknowledgement. The server never sends a Write Response, and critically, it also does not send an Error Response even if the write fails. The client gets no feedback whatsoever. This makes Write Command an unreliable operation — but that is intentional for certain use cases.
Typical use: Control point attributes — attributes whose purpose is to trigger an action rather than store a value. Examples include starting/stopping device discovery, triggering a LED blink, or sending a command to reset a measurement counter. For these, the latency reduction from skipping the acknowledgement matters more than guaranteed delivery.
/* Write command (no response) with gatttool */
gatttool -b AA:BB:CC:DD:EE:FF --char-write \
--handle=0x0025 --value=01
/* --char-write sends Write Command (opcode 0x52, CMD=1) */
/* --char-write-req sends Write Request (opcode 0x12, ACK) */
/* C example — Write Command opcode = 0x52: */
uint8_t pdu[] = {
0x52, /* Write Command opcode (CMD bit=1) */
0x25, 0x00, /* Attribute Handle (little-endian) */
0x01 /* Value */
};
send(att_sock, pdu, sizeof(pdu), 0);
/* No recv() needed — no response coming */
12.4.2.2 — Signed Write Command
A Write Command with a 12-byte authentication signature appended. The signature is computed using the CSRK (Connection Signature Resolving Key) that was distributed during SM Phase 3 pairing. The server verifies the signature against its stored CSRK for this client before processing the write.
This is the BLE mechanism for data integrity without link encryption — useful for scenarios where encrypting the entire link is not desirable (e.g., high-frequency sensor data where encryption latency matters) but you still need to prove the data came from a legitimate source and was not tampered with.
The signature covers the opcode, handle, value, and a counter that increments with every signed write. The counter prevents replay attacks — replaying an old signed PDU will fail verification because the expected counter has advanced.
12.4.3.1 — Handle Value Notification
Server Pushes Data — Client Does Not Confirm
A Notification is the server pushing data to the client unprompted. The client does not send any response. This makes Notifications the lowest-latency way to deliver data — there is no round-trip wait. It is also unreliable: if the client is busy, the notification may be dropped without the server knowing.
This is perfect for streaming data where individual missed readings are acceptable. A heart rate monitor, for example, sends a new heart rate measurement via Notification every second. If one notification is missed, the next one arrives a second later and the gap goes unnoticed. The client configures which notifications it wants to receive by writing to the Client Characteristic Configuration Descriptor (CCCD).
/* Subscribe to notifications with bluetoothctl */
[AA:BB:CC:DD:EE:FF]# select-attribute /org/bluez/hci0/dev_.../char0010
[AA:BB:CC:DD:EE:FF]# notify on
/* This writes 0x0001 to the CCCD on handle 0x0011 */
/* Server now sends notifications whenever value changes */
/* With gatttool: */
gatttool -b AA:BB:CC:DD:EE:FF --char-write-req \
--handle=0x0011 --value=0100
/* 0x0100 little-endian = CCCD value enabling notifications */
/* Listen for incoming notifications: */
gatttool -b AA:BB:CC:DD:EE:FF --listen
/* Python BlueZ D-Bus notification callback: */
def properties_changed(interface, changed, invalidated, path):
if 'Value' in changed:
print("Notification:", bytes(changed['Value']))
bus.add_signal_receiver(properties_changed,
dbus_interface='org.freedesktop.DBus.Properties',
signal_name='PropertiesChanged',
path_keyword='path')
12.4.4 — Handle Value Indication & Confirmation
Server Pushes Data — Client Must Confirm Receipt
An Indication is identical to a Notification in payload but requires the client to send back a Handle Value Confirmation. Only after that confirmation arrives does the server proceed to send the next Indication. This guarantees delivery at the ATT level — the server knows the data reached the client.
The tradeoff is latency. Every indication requires a round trip before the next one can be sent. Notifications are lower latency but unreliable. Indications are reliable but slower. Use Indications for critical data where missing a value is not acceptable — like a blood pressure measurement that should always be delivered intact.
| Property | Notification | Indication |
| Reliable? | No | Yes |
| Client response? | None | Confirmation |
| Throughput? | Higher | Lower (RTT per packet) |
| CCCD value? | 0x0001 | 0x0002 |
| Example use | Heart rate, steps | Blood pressure, glucose |
/* Enable indications on a characteristic (CCCD = 0x0002): */
gatttool -b AA:BB:CC:DD:EE:FF --char-write-req \
--handle=0x0012 --value=0200
/* 0x0200 little-endian = CCCD value enabling indications */
/* BlueZ handles the Confirmation automatically */
/* The stack sends Handle Value Confirmation (opcode 0x1E) */
/* transparently when an Indication PDU (opcode 0x1D) is RX */
/* If you need to detect whether characteristic supports */
/* Notification vs Indication — check the CCCD descriptor */
/* and the Properties byte in the Characteristic Declaration */
/* Properties byte: bit 4 = Notify, bit 5 = Indicate */
/* Value 0x10 = Notify only, 0x20 = Indicate only */
/* Value 0x30 = both supported */
Next — ATT Practical Examples & Chapter Summary
Part 4 covers the real sniffer captures of Exchange MTU and service discovery with the iterative Read By Group Type pattern, plus the complete Chapter 12 summary.
