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 0x0001 (notifications) or 0x0002 (indications) to the CCCD descriptor (UUID 0x2902):
// Central/Client side - MUST enable notifications
void on_characteristic_discovered(uint16_t char_handle, uint16_t cccd_handle) {
// Write 0x0001 to CCCD to enable notifications
uint8_t enable_notify[2] = {0x01, 0x00}; // Little-endian
ble_gattc_write(conn_handle, cccd_handle, enable_notify, 2);
}
// Peripheral side - check if notifications are enabled
void send_sensor_reading(float value) {
if (cccd_enabled) { // Only notify if client subscribed
ble_gatts_hvx(conn_handle, &hvx_params);
}
// Otherwise, client must poll via READ
}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:
// WRONG: Custom UUID for temperature
#define TEMP_SERVICE_UUID "12345678-1234-5678-1234-123456789abc"
// Requires custom app, no ecosystem support
// CORRECT: Standard Environmental Sensing Service
#define ENV_SENSING_SERVICE 0x181A // Bluetooth SIG standard
#define TEMPERATURE_CHAR 0x2A6E // Standard characteristic
// Standard data format for temperature (per GATT spec):
// sint16: temperature in 0.01 degrees Celsius
int16_t temp_value = (int16_t)(celsius * 100);
ble_gatts_notify(conn_handle, temp_char_handle, &temp_value, 2);
// Benefits of standard services:
// - Works with nRF Connect, LightBlue, and generic BLE apps
// - iOS/Android can display values in system Bluetooth settings
// - Interoperable with fitness apps, health platforms
// - Defined data encoding eliminates ambiguityWhen 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. The BLE specification requires:
\[ \begin{aligned} T_{\text{supervision}} &> (1 + \text{latency}) \times T_{\text{interval}} \times 2 \\[0.4em] \text{where:} \quad &T_{\text{interval}} \in [7.5\text{ ms}, 4\text{ s}] \\ &\text{latency} \in [0, 499] \\ &T_{\text{supervision}} \in [100\text{ ms}, 32\text{ s}] \end{aligned} \]
Example 1: Power-optimized sensor \[ \begin{aligned} T_{\text{interval}} &= 400\text{ ms},\quad \text{latency} = 4 \\[0.4em] T_{\text{min}} &= (1 + 4) \times 400\text{ ms} \times 2 = 4000\text{ ms} = 4\text{ s} \\[0.4em] T_{\text{recommended}} &= 4\text{ s} \times 1.5 \text{ (safety margin)} = 6\text{ s} \end{aligned} \]
Example 2: Common mistake (underestimated timeout) \[ \begin{aligned} T_{\text{interval}} &= 200\text{ ms},\quad \text{latency} = 10,\quad T_{\text{supervision}} = 2\text{ s} \\[0.4em] T_{\text{worst-case}} &= (1 + 10) \times 200\text{ ms} = 2.2\text{ s} > 2\text{ s} \quad \color{red}{\text{FAIL}} \\[0.4em] \text{Result:} \quad &\text{Random disconnections when peripheral uses full latency} \end{aligned} \]
Example 3: iOS-compatible parameters \[ \begin{aligned} T_{\text{interval}} &= 100\text{ ms} \text{ (iOS prefers ≤ 200 ms)},\quad \text{latency} = 4 \\[0.4em] T_{\text{min}} &= (1 + 4) \times 100\text{ ms} \times 2 = 1\text{ s} \\[0.4em] T_{\text{recommended}} &= \max(2\text{ s}, T_{\text{min}} \times 1.5) = 2\text{ s} \text{ (iOS minimum)} \end{aligned} \]
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. Use this formula:
Minimum supervision timeout = (1 + peripheral_latency) × connection_interval × 2
Example calculations:
Scenario A (fast response, no latency):
- Connection interval: 50ms
- Peripheral latency: 0 (respond every event)
- Minimum timeout: (1 + 0) × 50ms × 2 = 100ms
- Recommended: 500ms-1s (margin for retries)
Scenario B (power-optimized sensor):
- Connection interval: 400ms (maximum for iOS)
- Peripheral latency: 4 (skip up to 4 events when idle)
- Minimum timeout: (1 + 4) × 400ms × 2 = 4000ms
- Recommended: 6-10 seconds
Scenario C (ultra-low-power with max latency):
- Connection interval: 4s (BLE maximum)
- Peripheral latency: 0 (required at max interval)
- Minimum timeout: (1 + 0) × 4s × 2 = 8s
- Recommended: 16-32 seconds (BLE max is 32s)
Common mistake:
- CI: 200ms, Latency: 10, Timeout: 2s
- Worst-case response: (1+10) × 200ms = 2.2s > 2s timeout
- Result: Random disconnections when peripheral is idle!
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/embedded if needed:
Apple's BLE Connection Parameter Requirements (as of iOS 17):
Connection Interval:
- Minimum: 15ms (BLE spec allows 7.5ms)
- Maximum: 2s (was 4s before iOS 11, then 2s)
- Must be multiple of 15ms
Peripheral Latency:
- Maximum: 30 (spec allows 499)
- Constraint: latency × interval ≤ 2 seconds
Supervision Timeout:
- Minimum: 2 seconds
- Maximum: 6 seconds
- Constraint: timeout > (1 + latency) × interval × 2
Recommended cross-platform parameters:
For responsive devices (wearables, input devices):
min_interval: 15ms (12 × 1.25ms)
max_interval: 30ms (24 × 1.25ms)
latency: 0
timeout: 2000ms
For power-optimized sensors:
min_interval: 100ms (80 × 1.25ms)
max_interval: 200ms (160 × 1.25ms)
latency: 4
timeout: 4000ms
For ultra-low-power (environmental sensors):
min_interval: 400ms (320 × 1.25ms)
max_interval: 500ms (400 × 1.25ms)
latency: 3
timeout: 6000ms
Common mistake: CI=4s (max BLE spec)
- Android: Works fine
- iOS: Silently reduced to 2s, doubling power consumption
- Embedded: Works fine
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
// App code - Check if NOTIFY property is enabled
BluetoothGattCharacteristic hrChar = service.getCharacteristic(HR_MEASUREMENT_UUID);
int properties = hrChar.getProperties();
Log.d("BLE", "Characteristic properties: " + properties);
// Expected: PROPERTY_NOTIFY (0x10) should be set
// If properties = 0x02 (READ only), notifications impossible!Step 2: Check CCCD Subscription Status
// The MISSING step: App must write to CCCD to enable notifications!
BluetoothGattDescriptor cccd = hrChar.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
);
if (cccd == null) {
Log.e("BLE", "ERROR: CCCD descriptor not found!");
// Root cause: Peripheral didn't add BLE2902 descriptor to characteristic
} else {
Log.d("BLE", "CCCD found, current value: " + Arrays.toString(cccd.getValue()));
// If value is null or [0x00, 0x00], notifications are disabled
}Step 3: Enable Notifications (THE FIX)
// Required steps to enable notifications:
// 1. Tell Android BLE stack to route notifications to callback
gatt.setCharacteristicNotification(hrChar, true);
// 2. Write 0x0001 to CCCD to tell peripheral to start sending
BluetoothGattDescriptor cccd = hrChar.getDescriptor(CCCD_UUID);
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); // [0x01, 0x00]
boolean success = gatt.writeDescriptor(cccd);
Log.d("BLE", "CCCD write initiated: " + success);
// 3. Wait for write confirmation in callback
@Override
public void onDescriptorWrite(BluetoothGatt gatt,
BluetoothGattDescriptor descriptor,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d("BLE", "Notifications enabled!");
// NOW notifications will start arriving
} else {
Log.e("BLE", "Failed to enable notifications: " + status);
}
}Step 4: Verify Peripheral CCCD Handling
// nRF52 peripheral code - Check CCCD write handler
void on_ble_evt(ble_evt_t const * p_ble_evt) {
switch (p_ble_evt->header.evt_id) {
case BLE_GATTS_EVT_WRITE: {
ble_gatts_evt_write_t const * p_evt_write =
&p_ble_evt->evt.gatts_evt.params.write;
// Check if write was to HR Measurement CCCD
if (p_evt_write->handle == m_hrs.hrm_handles.cccd_handle) {
uint16_t cccd_value = uint16_decode(p_evt_write->data);
if (cccd_value == BLE_GATT_HVX_NOTIFICATION) {
m_notifications_enabled = true;
NRF_LOG_INFO("Client subscribed to notifications");
} else {
m_notifications_enabled = false;
NRF_LOG_INFO("Client unsubscribed from notifications");
}
}
break;
}
}
}
// Only send notifications when subscribed
void send_heart_rate() {
if (m_notifications_enabled) {
// Send notification
ble_gatts_hvx_params_t hvx_params;
// ... setup params ...
sd_ble_gatts_hvx(m_conn_handle, &hvx_params);
} else {
NRF_LOG_WARNING("Notifications not enabled, skipping send");
}
}Step 5: Full Diagnostic Checklist
✓ 1. Characteristic has PROPERTY_NOTIFY flag
✓ 2. Peripheral adds CCCD descriptor (UUID 0x2902) to characteristic
✓ 3. App calls setCharacteristicNotification(char, true)
✓ 4. App writes 0x0001 to CCCD descriptor
✓ 5. App waits for onDescriptorWrite() success callback
✓ 6. Peripheral handles CCCD write event
✓ 7. Peripheral sets internal flag: notifications_enabled = true
✓ 8. Peripheral checks flag before calling notify()
✓ 9. App implements onCharacteristicChanged() callback
✓ 10. Connection is still active (not disconnected mid-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 peripheral sending every second
After: Steady stream of heart rate notifications arriving in onCharacteristicChanged()
Debug time: 2 hours → could have been 10 minutes with 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
| Chapter | Why It Matters |
|---|---|
| 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 the pitfall knowledge from this chapter 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 |