29 Bluetooth GATT Review
29.1 Learning Objectives
- Diagnose and fix the CCCD enablement requirement for BLE notifications using descriptor writes
- Apply standard GATT services (Environmental Sensing, Heart Rate) instead of custom UUIDs for interoperability
- Calculate supervision timeout values using the formula: timeout > (1 + latency) × interval × 2
- Design BLE connection parameters within iOS constraints (15ms-2s interval, max 30 latency)
- Implement bonding key persistence using non-volatile storage to survive device reboots
GATT (Generic Attribute Profile) is how BLE devices organize and share their data. This review covers common mistakes and misconceptions about GATT, like confusing services with characteristics or misunderstanding notification behavior. Avoiding these pitfalls saves hours of debugging in real BLE projects.
- GATT Caching: BLE 5.1 feature where a client stores the peer’s GATT service structure (with a database hash) to skip service discovery on reconnection, reducing connection setup latency
- ATT Error Response: 9-byte PDU returned when an ATT operation fails; error codes include 0x01 (Invalid Handle), 0x02 (Read Not Permitted), 0x0F (Insufficient Encryption)
- BLE Privacy: Feature using Resolvable Private Addresses (RPAs) that change periodically; prevents device tracking by passive scanners while allowing bonded devices to recognize each other
- LE Secure Connections OOB: Pairing method where ECDH public keys are exchanged out-of-band (NFC, QR code) before BLE connection, providing the highest level of MITM protection
- Characteristic Value Truncation: Silent data loss that occurs when a notification payload exceeds (ATT MTU - 3) bytes without negotiating larger MTU; no error is returned to the application
- BLE Connection Supervision: Link Layer mechanism that drops a connection after supervision_timeout milliseconds without a valid packet, independent of application layer health checks
- Pairing vs Bonding: Pairing establishes a temporary session key (STK/LTK) for the current connection; bonding additionally stores the LTK in non-volatile memory for future reconnections without re-pairing
- BLE Whitelist (Filter Accept List): Controller-level filter that only responds to advertising or connection requests from listed device addresses, used to implement private networks
The most common BLE implementation mistakes involve forgetting to write to the CCCD before expecting notifications, using custom UUIDs when standard GATT services exist, and miscalculating supervision timeout relative to connection interval and peripheral latency. Designing for Apple iOS parameter constraints first ensures cross-platform compatibility.
This is Part 3 of the Bluetooth Comprehensive Review series:
- Overview and Visualizations - Protocol stacks, state machines, initial scenarios
- Advanced Scenarios - Medical security, battery drain, smart locks
- GATT Pitfalls (this chapter) - Common implementation mistakes
- Assessment - Visual gallery and understanding checks
29.2 Common GATT Implementation Pitfalls
The Mistake: Creating a BLE peripheral with NOTIFY characteristics, but notifications never arrive at the central device. The central connects, discovers services, but never receives pushed data even though the peripheral is calling notify().
Why It Happens: BLE notifications require the client to explicitly enable them by writing to the Client Characteristic Configuration Descriptor (CCCD). This is a security and power-saving feature - peripherals don’t spam data until the client opts in. Many developers forget this step, especially when transitioning from other protocols.
The Fix: After discovering characteristics, the central must write to the CCCD descriptor (UUID 0x2902):
- Discover the CCCD handle for the notifying characteristic, not only the characteristic value handle.
- Enable the client callback path in the platform stack. On Android, this means calling
setCharacteristicNotification(characteristic, true). - Write the subscription value to the CCCD:
0x01 0x00for notifications or0x02 0x00for indications. The byte order is little-endian. - Wait for the descriptor-write success callback before expecting notification callbacks.
- Gate peripheral notifications on CCCD state. The peripheral should update an internal
notifications_enabledflag when the CCCD is written and callnotify()only when that flag is true. Otherwise, clients must poll withREAD.
Common symptoms: Works in nRF Connect (which auto-enables CCCD) but fails in custom apps. Debug by checking CCCD value after connection.
The Mistake: Creating custom 128-bit UUIDs for common sensor data (temperature, heart rate, battery level) instead of using Bluetooth SIG standard services, breaking interoperability with existing apps and tools.
Why It Happens: Developers either don’t know standard services exist, or want “full control” over data format. Custom UUIDs require custom apps on every platform, while standard services work with generic BLE tools and OS integrations.
The Fix: Check the Bluetooth SIG GATT specifications before creating custom services. Use standard UUIDs with defined data formats whenever they fit:
- Temperature and humidity: Environmental Sensing Service
0x181A, Temperature0x2A6E, Humidity0x2A6F. - Battery level: Battery Service
0x180F, Battery Level0x2A19. - Heart rate: Heart Rate Service
0x180D, Heart Rate Measurement0x2A37. - Data encoding: Follow the SIG-defined representation. For example, temperature uses a signed 16-bit value in 0.01 degrees Celsius, so 23.45 C is sent as
2345. - Interoperability payoff: Generic tools such as nRF Connect and LightBlue can parse the value, operating systems can recognize it, and client developers do not need a private README to decode the payload.
When to use custom UUIDs: Only for truly proprietary functionality that has no standard equivalent (e.g., device-specific configuration, firmware update protocol, vendor-specific commands).
Supervision timeout must accommodate worst-case response time with peripheral latency. Use:
T_supervision > (1 + latency) x T_interval x 2
BLE permits connection intervals from 7.5 ms to 4 s, peripheral latency from 0 to 499, and supervision timeout from 100 ms to 32 s. Platform policies can be stricter.
Example 1: Power-optimized sensor
- Interval: 400 ms.
- Latency: 4.
- Minimum timeout:
(1 + 4) x 400 ms x 2 = 4000 ms = 4 s. - Recommended timeout: about 6 s after adding a 50% margin.
Example 2: Common underestimated timeout
- Interval: 200 ms.
- Latency: 10.
- Configured timeout: 2 s.
- Worst-case response gap:
(1 + 10) x 200 ms = 2.2 s, which is already longer than the timeout. - Result: random idle disconnects when the peripheral uses its full latency allowance.
Example 3: iOS-compatible parameters
- Interval: 100 ms.
- Latency: 4.
- Minimum timeout:
(1 + 4) x 100 ms x 2 = 1 s. - Recommended timeout: 2 s, because iOS expects at least a 2 s supervision timeout.
Key insight: The factor-of-2 multiplier accounts for connection event retries. Always verify that your timeout exceeds the worst-case response time by at least 50% to handle RF interference and packet loss.
The Mistake: Using a short supervision timeout (e.g., 1 second) for a BLE connection to a peripheral that uses peripheral latency to skip connection events, resulting in unexpected disconnections during normal operation when no data is being exchanged.
Why It Happens: Developers set aggressive timeouts thinking “faster disconnect detection is better,” without accounting for the interaction between connection interval, peripheral latency, and supervision timeout. If peripheral latency allows skipping 10 events at 400ms intervals, the peripheral may not respond for 4 seconds—triggering a 1-second timeout.
The Fix: The supervision timeout must accommodate the worst-case response time based on your connection parameters:
minimum supervision timeout = (1 + peripheral_latency) x connection_interval x 2
Use these checks during design review:
- Fast response, no latency: 50 ms interval, latency 0, minimum timeout 100 ms. Use 500 ms to 1 s in practice to allow retries and RF loss.
- Power-optimized sensor: 400 ms interval, latency 4, minimum timeout 4 s. Use 6 to 10 s for a stable field deployment.
- Ultra-low-power peripheral: 4 s interval, latency 0, minimum timeout 8 s. Use 16 to 32 s if the application can tolerate slow disconnect detection.
- Common mistake: 200 ms interval, latency 10, timeout 2 s. Worst-case response is 2.2 s before the factor-of-2 safety check, so the central can disconnect while the peripheral is behaving correctly.
Always verify: timeout > (1 + latency) × interval × safety_factor where safety_factor >= 2.
The Mistake: Designing a BLE peripheral with connection parameters optimized for Android or embedded gateways (e.g., 500ms-4s connection intervals), then discovering that iPhones reject or override these parameters, causing connection failures or poor battery life on iOS.
Why It Happens: Apple enforces stricter connection parameter ranges than the BLE specification allows. Peripherals requesting parameters outside Apple’s limits will have their requests rejected or modified, leading to unexpected behavior that only appears during iOS testing.
The Fix: Design for Apple’s constraints first, then relax for Android or embedded gateways only after testing:
- Connection interval: use 15 ms minimum and 2 s maximum for iOS compatibility. BLE permits 7.5 ms to 4 s, but iOS policy is stricter.
- Peripheral latency: keep latency at or below 30, and keep
latency x interval <= 2 s. - Supervision timeout: use 2 to 6 s and still satisfy
timeout > (1 + latency) x interval x 2. - Responsive devices: 15 to 30 ms interval, latency 0, timeout 2000 ms.
- Power-optimized sensors: 100 to 200 ms interval, latency 4, timeout 4000 ms.
- Environmental sensors: 400 to 500 ms interval, latency 3, timeout 6000 ms.
- Common mistake: a 4 s interval may work on Android and embedded centrals, but iOS can silently reduce it to 2 s, doubling radio wakeups and battery drain.
Always test on iOS devices early in development—parameter negotiation failures are silent!
29.3 Chapter Summary
Bluetooth has evolved from cable replacement to sophisticated IoT protocol. BLE revolutionized wireless sensors by enabling years of battery life.
Key Points:
- Classic BT: Continuous connections (audio)
- BLE: Intermittent, ultra-low power
- Piconets: 7 active slaves max
- Mesh: Scalable building automation
- Profiles: Application-specific behavior
- Security: Modern encryption & authentication
29.4 Original Source Figures (Alternative Views)
The following figures from the CP IoT System Design Guide provide alternative perspectives on Bluetooth concepts for review and comparison.
Source: CP IoT System Design Guide, Chapter 4 - Networking
Source: CP IoT System Design Guide, Chapter 4 - Networking
Source: CP IoT System Design Guide, Chapter 4 - Networking
Source: CP IoT System Design Guide, Chapter 4 - Networking
Source: CP IoT System Design Guide, Chapter 4 - Networking
Source: CP IoT System Design Guide, Chapter 4 - Networking
29.5 Visual Reference Gallery
The BLE stack separates controller (radio) and host (protocol) functions, enabling flexible hardware/software partitioning.
GATT provides standardized data organization through services and characteristics, ensuring cross-vendor interoperability.
BLE’s frame format supports both advertising (broadcast) and data channel (connected) communication modes.
Frequency hopping across 79 channels (Classic) or 37 channels (BLE) provides resilience against interference and multipath fading.
Three advertising channels are strategically placed between Wi-Fi channels to minimize interference during device discovery.
Pairing mode selection determines security level, with Numeric Comparison and OOB providing strongest protection against MITM attacks.
Max the Microcontroller has seen developers make these mistakes over and over. Let me explain using a postal service analogy:
Mistake 1: Forgetting to subscribe for notifications. Imagine you want a newspaper delivered to your door. You cannot just wait and expect it to arrive – you have to fill out a subscription form first! In BLE, that subscription form is called the CCCD (Client Characteristic Configuration Descriptor). Your phone must write a special code (0x0001) to say “Yes, please send me updates.” Without it, the sensor will never push data to you.
Mistake 2: Inventing your own mailbox when a standard one exists. BLE has pre-made “mailbox designs” for common data types – temperature, heart rate, battery level. Using these standards means ANY BLE app can read your sensor. Making up your own custom format is like creating a mailbox that only YOUR mail carrier understands.
Lila the LED warns about Mistake 3: “If you set the wrong timeout value, it is like telling the post office ‘If I do not pick up my mail within 2 seconds, cancel my subscription.’ You will keep losing your connection! The formula is: timeout must be greater than (1 + latency) times the connection interval times 2.”
Sammy the Sensor adds: “Always design for Apple iPhones first – they have the strictest rules about connection timing. If it works on iPhone, it will work everywhere!”
Scenario: A developer creates a BLE heart rate sensor that connects successfully to an Android app, but notifications never arrive. The peripheral code calls notify() every second, and the app subscribes to the characteristic. What’s wrong?
Given:
- BLE peripheral: nRF52832 running Heart Rate Service (0x180D)
- Central: Android phone with custom app using Android BLE API
- Symptom: Connection succeeds, service discovery works, but
onCharacteristicChanged()callback never fires - Peripheral logs show: “Sending notification… Notification sent” every second
- App logs show: “Connected to device, discovered Heart Rate Service”
Diagnostic Steps:
Step 1: Verify characteristic configuration
Read the Heart Rate Measurement characteristic properties and confirm PROPERTY_NOTIFY (0x10) is present. If the characteristic only reports READ (0x02), the peripheral GATT definition is wrong and notifications are impossible.
Step 2: Check CCCD subscription status
Look up descriptor UUID 00002902-0000-1000-8000-00805f9b34fb on the notifying characteristic. If the descriptor is missing, the peripheral did not add the CCCD. If the descriptor value is 0x00 0x00, the client has not subscribed yet.
Step 3: Enable notifications on the Android client
The app must do three things in order: call setCharacteristicNotification(hrChar, true), write BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE (0x01 0x00) to the CCCD, and wait for onDescriptorWrite(..., GATT_SUCCESS) before expecting onCharacteristicChanged() callbacks.
Step 4: Verify peripheral CCCD handling
On the nRF52 side, handle the CCCD write event, decode the written value, set m_notifications_enabled = true only for BLE_GATT_HVX_NOTIFICATION, and call sd_ble_gatts_hvx() only when that flag is true.
Step 5: Full diagnostic checklist
- Characteristic has the
PROPERTY_NOTIFYflag. - Peripheral adds CCCD descriptor UUID
0x2902to the characteristic. - App calls
setCharacteristicNotification(characteristic, true). - App writes
0x0001to the CCCD descriptor. - App waits for the
onDescriptorWrite()success callback. - Peripheral handles the CCCD write event.
- Peripheral sets
notifications_enabled = true. - Peripheral checks that flag before calling
notify(). - App implements
onCharacteristicChanged(). - Connection remains active during the test.
Root Cause Found: The app was missing steps 3-5. It called setCharacteristicNotification() (step 3) but never wrote to the CCCD (step 4). This is a silent failure – no error is raised, notifications just never arrive.
Result After Fix:
- Before: 0 notifications received despite the peripheral sending every second.
- After: a steady stream of heart rate notifications arrives in
onCharacteristicChanged(). - Debug impact: the issue that took two hours could have been found in about 10 minutes with the checklist.
Key Insight: BLE notifications require a two-way handshake that many developers miss. The peripheral must offer NOTIFY capability (descriptor), and the client must explicitly subscribe (CCCD write). Use a diagnostic checklist to verify every step – the error is almost always a missing CCCD write or missing descriptor on the peripheral.
29.6 Summary
- CCCD subscription is mandatory for BLE notifications – clients must write
0x0001to the Client Characteristic Configuration Descriptor (UUID 0x2902) before the peripheral will push data - Use standard GATT services (e.g., Environmental Sensing 0x181A, Heart Rate 0x180D) whenever possible to ensure interoperability with generic BLE apps and OS integrations
- Calculate supervision timeout carefully using the formula:
timeout > (1 + peripheral_latency) x connection_interval x 2to avoid random disconnections during idle periods - Design for iOS constraints first – Apple enforces stricter connection parameter limits (max 2s interval, max 30 latency) that silently override requests outside the allowed range
- Store bonding keys in non-volatile storage (NVS/flash) to persist across reboots and avoid requiring re-pairing after device restarts
- Enforce security at the device level, not just in the app – other BLE clients can bypass app-only controls, so GATT characteristic permissions and device-side timeouts are essential for regulated devices
Common Pitfalls
Setting all GATT characteristic permissions to “Read/Write No Security” exposes sensitive data (device configuration, health metrics, access credentials) to any nearby BLE device without authentication. Implement minimum required permissions: read-only data → Read without security; configuration data → Authenticated Write; keys/credentials → Encrypted Authenticated Read/Write.
BLE GATT clients that assume all operations succeed and do not parse ATT Error Responses will silently fail when a server returns errors. Common causes: insufficient encryption (write to characteristic requiring authenticated pairing), attribute handle changed after firmware update (handle stale from cache), or write value out of characteristic value range. Always implement ATT error handler and log the opcode, handle, and error code.
BLE pairing produces both a Short Term Key (STK, used only for the current pairing session) and, during bonding, a Long Term Key (LTK, stored for future sessions). Using the STK beyond its intended scope or misunderstanding that the LTK must be securely stored in NVS is a common implementation error. The STK should be discarded after the pairing session; only the LTK (and IRK, CSRK) should be persisted.
BLE GATT Write (without response) is fire-and-forget with no acknowledgment; packet loss is silent. GATT Write (with response) provides ATT acknowledgment but no application-level confirmation of processing. For reliable command delivery, implement an application-level acknowledgment: send a command via Write With Response, wait for a confirmation characteristic notification within 500 ms, and retry up to 3 times on timeout.
29.7 What’s Next
Continue with these chapters to apply and reinforce the GATT debugging patterns from this review:
- Bluetooth Review: Assessment: Test your understanding with a visual gallery and comprehensive knowledge checks covering all four review parts.
- Bluetooth Review: Overview: Revisit protocol stacks, GAP state machines, and initial deployment scenarios to consolidate the full picture.
- Bluetooth Review: Advanced Scenarios: Apply this chapter’s pitfall knowledge to medical security, battery drain, and smart lock case studies.
- Bluetooth Fundamentals and Architecture: Analyze GATT profile design in depth and distinguish correct connection parameter negotiation from common mistakes.
- Bluetooth Security: Evaluate bonding, pairing modes, key storage strategies, and device-side enforcement for regulated BLE deployments.
- Bluetooth Applications: Construct real-world BLE deployment patterns that avoid the pitfalls covered in this chapter.