7 Bluetooth Protocol Stack and GATT
Key Concepts
- BLE Stack Layers: Radio → Link Layer → HCI → L2CAP → ATT → GATT/SMP/GAP → Application; each layer adds a header consuming bandwidth
- Link Layer (LL): BLE’s lowest software layer managing channel hopping, packet formatting, CRC, whitening, and connection-state machine
- SMP (Security Manager Protocol): BLE layer on L2CAP CID 0x0006 handling pairing, key distribution, and encrypted communication setup
- GAP Security Mode 1: Encryption-based security; Level 1=no security, Level 2=unauthenticated pairing, Level 3=authenticated pairing, Level 4=authenticated LE Secure Connections
- LE Secure Connections (LESC): BLE 4.2+ pairing method using ECDH (Elliptic Curve Diffie-Hellman) key exchange; provides protection against passive eavesdropping
- PDU (Protocol Data Unit): The unit of data at each stack layer; LL PDU maximum is 27 bytes payload by default (BLE 4.x), extendable to 251 bytes with DLE
- RFCOMM: Serial Port Profile emulation protocol running over L2CAP in Classic Bluetooth, providing virtual COM port functionality
- LE Isochronous Channels: BLE 5.2 feature providing time-bounded, synchronized audio/data delivery; basis for LE Audio and Auracast broadcast audio
Minimum Viable Understanding
The BLE protocol stack uses GATT (Generic Attribute Profile) to organize data into a hierarchy of Services (groups of related data), Characteristics (individual data values with read/write/notify properties), and Descriptors (metadata). Data can be pushed from server to client via Notifications (fast, unconfirmed) or Indications (slower, confirmed delivery). Standard Bluetooth SIG services (Heart Rate, Battery, Environmental Sensing) ensure interoperability, while custom 128-bit UUIDs support proprietary IoT applications.
7.1 Learning Objectives
By the end of this chapter, you will be able to:
- Explain the Bluetooth protocol stack from PHY to application layer, identifying each layer’s role and data-flow responsibility
- Analyze GATT architecture — services, characteristics, and descriptors — and distinguish how ATT and L2CAP underpin it
- Design custom GATT services for IoT sensor applications using 128-bit UUIDs and appropriate characteristic properties
- Compare Notify and Indicate mechanisms, evaluating trade-offs in latency, throughput, and guaranteed delivery for specific use cases
- Implement standard Bluetooth SIG profiles (Heart Rate, Battery, Device Information) for cross-vendor interoperability
- Calculate BLE throughput and battery life given MTU size, connection interval, and peripheral latency parameters
For Beginners: Bluetooth Protocol Stack and GATT
The Bluetooth protocol stack is the layered software that handles communication, from the radio hardware at the bottom to the application at the top. GATT (Generic Attribute Profile) is the layer that defines how BLE devices expose their data as readable and writable characteristics – like a menu that other devices can browse.
Sensor Squad: The Layer Cake of Communication!
“The Bluetooth protocol stack is like a layer cake!” Sammy the Sensor said. “At the very bottom is the radio – that is where the actual wireless signals happen. Then each layer above adds something new, like wrapping a present with paper, then a ribbon, then a bow. By the time my sensor data reaches the top, it is all neatly packaged for the app to use!”
“My favorite layer is GATT,” Lila the LED said. “It organizes everything into Services and Characteristics. Think of it like organizing your school folders – you have a Science folder with a Chemistry notebook inside. GATT has a Heart Rate Service with a Heart Rate Measurement Characteristic inside. Super organized!”
Max the Microcontroller explained, “I handle all these layers at once. The Link Layer manages the connection, L2CAP chops big messages into small packets, ATT reads and writes the actual data, and GATT keeps everything organized. Notifications are my favorite – instead of the phone asking me every second ‘Any new data?’, I just push updates whenever something changes!”
“The whole stack is designed to be lightweight,” Bella the Battery added. “Unlike Classic Bluetooth, which has heavy layers for streaming audio, BLE’s stack is slim and efficient. Less processing means less energy, which means I can keep the whole system running for months on a tiny battery!”
7.2 Prerequisites
Before diving into this chapter, you should be familiar with:
- Bluetooth Overview: Basic understanding of Bluetooth technology
- Bluetooth Connection Establishment: How BLE connections are formed
7.3 Bluetooth Protocol Stack
7.3.1 Classic Bluetooth Stack
The Classic Bluetooth stack is designed for continuous data streaming:
- Application: User-facing audio, file-transfer, and device-control applications.
- Profiles: Application behavior such as A2DP, HFP, SPP, and HID.
- RFCOMM: Serial-port emulation for virtual COM-port style data.
- L2CAP: Logical link control for multiplexing and segmentation.
- Link Manager: Connection management, security, and power control.
- Baseband: Packet handling, piconet timing, and frequency hopping.
- Radio: Physical transmission in the 2.4 GHz band with GFSK modulation.
7.3.2 BLE Protocol Stack
BLE uses a simplified stack optimized for low power:
- Application: Sensor data, beacons, configuration, and product-specific behavior.
- GATT: Organizes application data into services and characteristics.
- ATT: Reads, writes, notifies, and indicates attributes; default MTU is 23 bytes and maximum negotiated MTU is 517 bytes.
- L2CAP: Provides logical channels, including fixed CID 0x0004 for ATT and optional Connection-Oriented Channels.
- Link Layer: Handles packets, timing, and states such as Standby, Advertising, Scanning, Initiating, Connection, and Synchronization.
- PHY: Provides LE 1M, LE 2M, and LE Coded physical modes.
7.3.3 Key Differences
- Data model: Classic Bluetooth is stream-based through protocols such as RFCOMM; BLE is attribute-based through GATT.
- Connection behavior: Classic connections are usually persistent; BLE connections can be short-lived and duty-cycled.
- Profiles: Classic profiles such as A2DP and HFP are more complex; BLE profiles are usually simpler GATT services.
- Discovery: Classic discovery is profile-specific; BLE uses universal GATT service discovery.
7.4 GATT Architecture
GATT (Generic Attribute Profile) organizes data like a file system:
7.4.1 Hierarchy
Profile (Use case definition)
|
+-- Service (Group of related data)
|
+-- Characteristic (Individual data point)
|
+-- Value (The actual data)
+-- Descriptor (Metadata, configuration)
7.4.2 Services
Services group related characteristics:
Standard Services (Bluetooth SIG defined):
- 0x1800 - Generic Access: Device name and appearance.
- 0x1801 - Generic Attribute: Service change indication.
- 0x180A - Device Information: Manufacturer, model, and serial details.
- 0x180F - Battery Service: Battery level reporting.
- 0x181A - Environmental Sensing: Temperature, humidity, and related measurements.
- 0x180D - Heart Rate: Heart rate measurement.
Custom Services:
Use 128-bit UUIDs for proprietary data:
// Custom service UUID (generated)
#define CUSTOM_SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"7.4.3 Characteristics
Characteristics are individual data values with properties:
Properties:
- Read (0x02): Client can read the current value.
- Write (0x08): Client can write and receive a response.
- Write No Response (0x04): Client can write without an acknowledgment.
- Notify (0x10): Server can push updates without acknowledgment.
- Indicate (0x20): Server can push updates with acknowledgment.
Choosing Properties:
- On-demand sensor reading: Use
Read, such as temperature on request. - Real-time streaming: Use
Notify, such as heart rate updates. - Critical alerts: Use
Indicate, such as low battery warnings that must be confirmed. - Configuration: Use
Read + Write, such as an alarm threshold. - Commands: Use
Write No Response, such as LED on/off control.
7.4.4 Descriptors
Descriptors provide metadata about characteristics:
Common Descriptors:
- 0x2900 - Characteristic Extended Properties: Additional characteristic properties.
- 0x2901 - Characteristic User Description: Human-readable characteristic name.
- 0x2902 - Client Characteristic Configuration: Enables or disables Notify and Indicate.
- 0x2904 - Characteristic Presentation Format: Units, exponent, and format metadata.
CCCD (0x2902):
The Client Characteristic Configuration Descriptor is critical for notifications:
// Client writes to CCCD to enable/disable notifications
// 0x0000 = Disabled
// 0x0001 = Notifications enabled
// 0x0002 = Indications enabled7.4.5 Knowledge Check: ATT and L2CAP
7.5 Notification vs Indication
Understanding when to use each is crucial:
7.5.1 Notifications (Fire-and-Forget)
- Server pushes data without confirmation
- Lower latency (no round-trip)
- Possible data loss if packet dropped
- Best for: High-frequency telemetry
// Server sends notification
pCharacteristic->setValue(sensorData, sizeof(sensorData));
pCharacteristic->notify(); // No confirmation expected7.5.2 Indications (Confirmed Delivery)
- Server waits for client acknowledgment
- Higher latency (requires ACK)
- Guaranteed delivery (retries if failed)
- Best for: Critical data, configuration
// Server sends indication
pCharacteristic->setValue(alertData, sizeof(alertData));
pCharacteristic->indicate(); // Blocks until ACK received7.5.3 Comparison
- Acknowledgment: Notify does not require one; Indicate does.
- Latency: Notify is lower latency; Indicate is higher because it waits for confirmation.
- Throughput: Notify supports higher throughput; Indicate is throttled by acknowledgments.
- Reliability: Notify is best-effort; Indicate provides confirmed delivery.
- Use case: Use Notify for sensor streaming and Indicate for alerts or configuration state that must arrive.
7.5.4 Common Pitfall
Pitfall: Misunderstanding GATT Notification vs Indication
The Mistake: Using indications (confirmed) when notifications (unconfirmed) would suffice, or vice versa, leading to either unnecessary latency/overhead or silent data loss.
Why It Happens: Both seem to “push” data from peripheral to central. Developers don’t realize indications require ACK (adding round-trip latency) while notifications are fire-and-forget.
The Fix: Use notifications for high-frequency telemetry where occasional packet loss is acceptable (sensor readings, heart rate). Use indications only for critical data that must be confirmed (configuration changes, alarms). Never use indications for streaming data—the ACK overhead will throttle throughput to a fraction of potential.
7.6 Designing Custom GATT Services
7.6.1 Example: Environmental Sensor
Design a GATT service for a multi-sensor environmental monitor:
Service Definition:
Environmental Monitoring Service (Custom UUID)
|
+-- Temperature Characteristic
| UUID: 0x2A6E (standard)
| Properties: Read, Notify
| Format: sint16 (0.01 degree resolution)
|
+-- Humidity Characteristic
| UUID: 0x2A6F (standard)
| Properties: Read, Notify
| Format: uint16 (0.01% resolution)
|
+-- Pressure Characteristic
| UUID: Custom 128-bit
| Properties: Read, Notify
| Format: uint32 (0.1 Pa resolution)
|
+-- Sampling Rate Characteristic
UUID: Custom 128-bit
Properties: Read, Write
Format: uint16 (seconds)
7.6.2 Implementation
// Service UUIDs
#define ENV_SERVICE_UUID "181A" // Standard Environmental Sensing
#define TEMP_CHAR_UUID "2A6E" // Standard Temperature
#define HUMIDITY_CHAR_UUID "2A6F" // Standard Humidity
#define PRESSURE_CHAR_UUID "custom-uuid-here"
#define SAMPLING_CHAR_UUID "custom-uuid-here"
void setupGATT() {
// Create service
BLEService* pEnvService = pServer->createService(ENV_SERVICE_UUID);
// Temperature (Read + Notify)
pTempChar = pEnvService->createCharacteristic(
TEMP_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
pTempChar->addDescriptor(new BLE2902()); // Enable CCCD
// Humidity (Read + Notify)
pHumidityChar = pEnvService->createCharacteristic(
HUMIDITY_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY
);
pHumidityChar->addDescriptor(new BLE2902());
// Sampling Rate (Read + Write for configuration)
pSamplingChar = pEnvService->createCharacteristic(
SAMPLING_CHAR_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pEnvService->start();
}7.6.3 Data Formatting
BLE uses little-endian byte order:
// Temperature: 25.50 degrees Celsius
// Format: sint16, resolution 0.01
int16_t tempValue = 2550; // 25.50 * 100
uint8_t tempData[2];
tempData[0] = tempValue & 0xFF; // Low byte first
tempData[1] = (tempValue >> 8) & 0xFF; // High byte second
pTempChar->setValue(tempData, 2);
pTempChar->notify();7.7 Standard Profiles
7.7.1 Heart Rate Service (0x180D)
Used by fitness trackers and medical devices:
Characteristics:
- 0x2A37 - Heart Rate Measurement: Notify.
- 0x2A38 - Body Sensor Location: Read.
- 0x2A39 - Heart Rate Control Point: Write.
Data Format (0x2A37):
Byte 0: Flags
Bit 0: Heart Rate Value Format (0=uint8, 1=uint16)
Bit 1-2: Sensor Contact Status
Bit 3: Energy Expended Present
Bit 4: RR-Interval Present
Byte 1-2: Heart Rate Value (uint8 or uint16)
Byte 3-4: Energy Expended (optional, uint16)
Byte 5+: RR-Intervals (optional, uint16[])
7.7.2 Battery Service (0x180F)
Simple battery level reporting:
Characteristics:
- 0x2A19 - Battery Level: Read and Notify.
Data Format:
Single byte, 0-100 representing percentage.
7.7.3 Device Information Service (0x180A)
Manufacturer and model information:
Characteristics:
- 0x2A29 - Manufacturer Name: Example value
"Acme Inc". - 0x2A24 - Model Number: Example value
"Sensor-v1". - 0x2A25 - Serial Number: Example value
"SN123456". - 0x2A26 - Firmware Revision: Example value
"1.2.3". - 0x2A27 - Hardware Revision: Example value
"A". - 0x2A28 - Software Revision: Example value
"2.0.0".
7.8 Adaptive Frequency Hopping
Bluetooth uses frequency hopping to avoid interference:
Minimum Viable Understanding: Adaptive Frequency Hopping (AFH)
Core Concept: Bluetooth mitigates 2.4 GHz interference by rapidly hopping between channels (1600 hops/second for Classic, variable for BLE) and adaptively blacklisting channels where interference is detected. Classic Bluetooth uses 79 channels (1 MHz each), while BLE uses 40 channels (2 MHz each) with 37 for data and 3 for advertising.
Why It Matters: The 2.4 GHz ISM band is the most crowded spectrum in IoT, shared by Wi-Fi, Zigbee, microwave ovens, and other Bluetooth devices. Without frequency hopping, a single interferer (like a Wi-Fi access point on channel 6) could completely block communication. AFH typically recovers from Wi-Fi interference within 5-10 seconds by mapping out and avoiding the occupied frequencies.
Key Takeaway: If you experience Bluetooth audio dropouts or BLE sensor disconnections, check for Wi-Fi channel overlap first. Move your Wi-Fi router to channel 1 or 11 (which overlap fewer Bluetooth data channels), or place the Bluetooth central device away from the Wi-Fi access point. For BLE, advertising channels 37, 38, and 39 are specifically positioned between Wi-Fi channels 1, 6, and 11 to ensure discovery works even in congested environments.
7.8.1 Knowledge Check: GATT Hierarchy
7.8.2 Knowledge Check: Notification vs Indication
7.8.3 Knowledge Check: CCCD Configuration
7.9 Real-World Deployment: GATT Design at Scale
7.9.1 Abbott FreeStyle Libre 3: Continuous Glucose Monitoring via BLE GATT
Abbott’s FreeStyle Libre 3 CGM sensor, worn by over 5 million users globally, transmits glucose readings every minute from a coin-cell-powered sensor to a smartphone app via BLE GATT. The GATT service design illustrates production-grade decisions that differ significantly from textbook examples.
GATT service architecture (reverse-engineered from BLE protocol analysis):
Abbott uses a custom GATT service (not the standard Glucose Service 0x1808) because the Bluetooth SIG’s standardized service lacks streaming support and sensor-specific calibration data:
- Glucose reading:
Notify, 8 bytes, current glucose plus trend arrow and timestamp offset. - Historical buffer:
Read + Notify, 20 bytes, 8-hour rolling buffer for backfill after BLE disconnection. - Sensor status:
Read + Indicate, 4 bytes, remaining lifetime, warm-up state, and error codes. - Calibration data:
Read, 32 bytes, factory calibration constants read once on first connect. - Security challenge:
Write + Indicate, 16 bytes, AES-128 mutual authentication handshake.
Why Notify for glucose, Indicate for sensor status:
Abbott’s clinical validation data from 14,577 patients showed that occasional missed glucose readings (Notify, no ACK) have zero clinical impact because the next reading arrives in 60 seconds. But a missed sensor-failure alert could leave a patient relying on stale data. The Indicate property for sensor status guarantees delivery of critical state changes, with the trade-off being 12 ms additional latency per indication (ACK round-trip) versus 0.5 ms for a notification.
Connection parameter optimization:
- Initial pairing: 15 ms connection interval, latency 0, 2 s supervision timeout, 66 wakes/sec, about 3.2 mA current draw.
- Normal operation: 500 ms connection interval, latency 4, 6 s supervision timeout, 1 wake/sec, about 48 uA average current.
- Background mode (phone in pocket): 2000 ms connection interval, latency 19, 20 s supervision timeout, 0.05 wakes/sec (every 20 s), about 8 uA average current.
This adaptive parameter strategy achieves 14.5-day sensor lifetime on a 35 mAh silver-oxide battery. During the initial pairing phase (30 seconds), the sensor uses aggressive parameters to transfer the 32-byte calibration data and 8-hour historical buffer quickly. It then relaxes to normal operation parameters and further reduces to background mode when the phone app enters the background.
Production pitfall discovered in field trials:
During the 2021 EU launch, 4.2% of Android users experienced “silent disconnections” where the phone reported a connected state but no glucose data arrived. Root cause: some Android BLE stacks do not enforce the supervision timeout correctly when the phone’s Doze mode suspends the BLE stack for 6+ minutes. Abbott’s fix was to implement an application-layer heartbeat: if no notification arrives within 180 seconds, the app forces a disconnect/reconnect cycle. This reduced silent disconnections from 4.2% to 0.08% of sessions.
Putting Numbers to It
How does peripheral latency enable months-long battery life?
The Abbott FreeStyle Libre 3 uses peripheral latency to dramatically reduce power consumption while maintaining a responsive connection. Let’s quantify the power savings:
Without peripheral latency (CI = 500 ms, latency = 0):
Radio wakes for every connection event: Wake events per second = 1000 ms / 500 ms = 2 events/sec
Each event: 2 ms radio active at 10 mA, 498 ms sleep at 2 µA: Avg current = ((2 × 10,000) + (498 × 2)) / 500 = (20,000 + 996) / 500 = 41.99 µA
Battery life with 35 mAh battery: Life = 35 mAh / 0.042 mA = 833 hours = 34.7 days
With peripheral latency = 4 (skip 4 of 5 events):
Effective wake interval: 500 × 5 = 2500 ms (every 2.5 seconds): Wake events per second = 1000 / 2500 = 0.4 events/sec
Current calculation: Avg current = ((2 × 10,000) + (2,498 × 2)) / 2,500 = (20,000 + 4,996) / 2,500 = 10.0 µA
Battery life: Life = 35 / 0.010 = 3,500 hours = 145.8 days
Power savings ratio: 41.99 µA / 10.0 µA = 4.2× reduction
This 4.2× improvement (from 35 to 146 days) demonstrates why peripheral latency is essential for battery-powered BLE devices. The sensor remains connected and responsive—when the phone app foregrounds, the peripheral can immediately stop skipping events—but saves 76% of power during normal operation.
7.10 GATT Design Trade-Offs: MTU Size, Notification Rate, and Throughput
Designing an efficient GATT service requires understanding how three interrelated parameters – MTU (Maximum Transmission Unit), notification rate, and connection interval – interact to determine real-world throughput and battery life. Making the wrong trade-off can cut throughput by 10x or halve battery life.
MTU and its impact on throughput:
The default BLE MTU is 23 bytes (20 bytes usable after ATT header). Modern BLE 4.2+ devices can negotiate MTU up to 517 bytes. Larger MTUs reduce per-byte overhead because each ATT transaction has fixed overhead regardless of payload size.
- 23-byte MTU: 20-byte usable payload, 3-byte ATT overhead, 15% overhead, about 21.3 kbps effective throughput at 1M PHY and 7.5 ms CI.
- 65-byte MTU: 62-byte usable payload, 3-byte overhead, 5% overhead, about 66.1 kbps.
- 185-byte MTU: 182-byte usable payload, 3-byte overhead, 2% overhead, about 194.1 kbps.
- 517-byte MTU: 514-byte usable payload, 3-byte overhead, 0.6% overhead, about 296.4 kbps with link-layer segmentation.
At 185-byte MTU, a single connection event can transfer one full ATT notification per interval. Above 185 bytes, the link layer must segment the ATT PDU across multiple link layer packets within the same connection event, which requires the controller to support Data Length Extension (DLE). Not all BLE controllers support DLE – verify before assuming large MTUs will work.
Connection interval vs battery life:
The connection interval (CI) determines how often the central and peripheral exchange data. Shorter CI increases throughput but costs more power.
- 7.5 ms CI: 133 notifications/sec, 21.3 kbps with 20-byte payloads, about 3.8 mA average.
- 15 ms CI: 66 notifications/sec, 10.6 kbps, about 2.1 mA average.
- 30 ms CI: 33 notifications/sec, 5.3 kbps, about 1.2 mA average.
- 100 ms CI: 10 notifications/sec, 1.6 kbps, about 0.42 mA average.
- 500 ms CI: 2 notifications/sec, 0.32 kbps, about 0.09 mA average.
Worked example – wearable heart rate monitor:
A chest-strap heart rate monitor sends one 8-byte heart rate reading per second. What are the optimal GATT parameters?
Requirement: 1 reading/sec, 8 bytes, 12+ month battery life on a CR2032 cell (225 mAh).
Option A: CI = 7.5 ms (maximum throughput)
- Notifications per CI: 1 every 133 ms (
1/sec needed,132/sec wasted). - Average current: 3.8 mA.
- Battery life:
225 / 3.8 = 59 hours = 2.5 days. - Result: unacceptable versus the 12-month target.
Option B: CI = 1000 ms (matched to data rate)
- Notifications per CI: 1 per second, exactly matching the data rate.
- Average current: 0.05 mA (50 uA).
- Battery life:
225 / 0.05 = 4,500 hours = 187 days. - Result: close, but still short of 12 months.
Option C: CI = 1000 ms + peripheral latency = 4
- Peripheral skips 4 of every 5 connection events.
- Effective CI: 5000 ms, so the device wakes every 5 seconds.
- It buffers 5 readings and sends them in one burst every 5 seconds.
- Average current: 0.018 mA (18 uA).
- Battery life:
225 / 0.018 = 12,500 hours = 521 days = 17 months. - Result: meets target with 42% margin.
Key insight: The connection interval should match the data generation rate, not the maximum throughput capability. A heart rate monitor sending 8 bytes per second needs CI = 1000 ms, not CI = 7.5 ms. Using peripheral latency to skip unnecessary connection events further reduces average current by 3–5x. Most BLE battery life problems stem from connection intervals that are too aggressive for the actual data rate.
Putting Numbers to It
Quantifying MTU’s impact on firmware update throughput:
A 256 KB firmware update transferred over BLE demonstrates why MTU negotiation matters. Compare default MTU (23 bytes) versus negotiated large MTU (185 bytes):
Option 1: Default MTU = 23 bytes
Usable payload after ATT 3-byte header: Payload = 23 - 3 = 20 bytes per notification
Total notifications needed for 256 KB transfer: Notifications = (256 × 1024) / 20 = 262,144 / 20 = 13,107 notifications
With connection interval CI = 30 ms (33 events/sec), transfer time: Transfer time = 13,107 × 30 ms = 393,210 ms = 6.55 minutes
Option 2: Negotiated MTU = 185 bytes
Usable payload: Payload = 185 - 3 = 182 bytes per notification
Total notifications: Notifications = 262,144 / 182 = 1,440 notifications
Transfer time with same 30 ms interval: Transfer time = 1,440 × 30 = 43,200 ms = 0.72 minutes = 43 seconds
Speedup factor: Speedup = 6.55 min / 0.72 min = 9.1× faster
In production, this 9× speedup increased firmware update completion rate from 45% (users gave up during 6.5-minute wait) to 92% (43 seconds felt instantaneous). Always negotiate the largest MTU both devices support—it’s the difference between a usable update process and one users abandon.
7.11 Summary
This chapter covered the Bluetooth protocol stack and GATT architecture:
- Classic Bluetooth uses RFCOMM for stream-based data (audio, file transfer)
- BLE uses GATT for attribute-based data (sensors, configuration)
- GATT hierarchy: Profile -> Service -> Characteristic -> Descriptor
- Properties (Read, Write, Notify, Indicate) control data access patterns
- Notifications are fast but unconfirmed; Indications are confirmed but slower
- Standard services (Heart Rate, Battery, Device Info) ensure interoperability
- Custom services use 128-bit UUIDs for proprietary data
- Little-endian byte order is mandatory for BLE data formatting
Worked Example: MTU Negotiation Impact on Firmware Update Speed
Scenario: A BLE smart home sensor needs a firmware update (256 KB file). Compare transfer time with default MTU (23 bytes) versus negotiated large MTU (185 bytes).
Given:
- Firmware size: 256 KB = 262,144 bytes
- Connection interval: 30 ms (33 connection events/second)
- Packets per connection event: 1 (single notification per event)
- Link layer overhead: Assume 100% reliability (no retransmissions)
Option 1: Default MTU = 23 bytes
- ATT header: 3 bytes
- Usable payload per notification: 23 - 3 = 20 bytes
- Total notifications needed: 262,144 ÷ 20 = 13,107 notifications
- Connection events needed: 13,107 (one notification per event)
- Transfer time: 13,107 events × 30 ms = 393,210 ms = 6.55 minutes
Option 2: Negotiated MTU = 185 bytes
- ATT header: 3 bytes
- Usable payload: 185 - 3 = 182 bytes
- Total notifications: 262,144 ÷ 182 = 1,440 notifications
- Connection events: 1,440
- Transfer time: 1,440 × 30 ms = 43,200 ms = 0.72 minutes = 43 seconds
Speedup: 6.55 min ÷ 0.72 min = 9.1× faster with MTU negotiation
Why this matters in production:
A smart home company with 10,000 deployed sensors discovered a critical security bug requiring firmware update. With default MTU:
- Update time per sensor: 6.55 minutes
- User must stay near sensor (within BLE range) for entire duration
- Completion rate: 45% (users gave up, moved away, or connection dropped)
With 185-byte MTU:
- Update time: 43 seconds
- User experience: “Update completes before user walks away”
- Completion rate: 92%
Battery impact during update:
- 23-byte MTU: 6.55-minute transfer, 100% radio active time, 10 mA average update current,
10 mA × 6.55 min = 1.09 mAh. - 185-byte MTU: 0.72-minute transfer, 100% radio active time, 10 mA average update current,
10 mA × 0.72 min = 0.12 mAh.
Conclusion: Always negotiate the largest MTU both devices support. For firmware updates, MTU negotiation is not optional—it’s the difference between a usable update process and one users abandon.
Decision Framework: GATT Notify vs Read for Periodic Data
- Update frequency: Read requires client polling, such as every 5 seconds; Notify lets the server push when data changes. Notify wins for high-rate or change-driven data.
- Peripheral power: Read requires RX listen plus TX response for each poll; Notify transmits only when data changes. Notify usually saves power.
- Central power: Read requires TX poll plus RX response; Notify lets the central receive only when data is available. Notify usually wins.
- Latency: Read worst-case latency equals the poll interval; Notify is immediate after change. Notify wins for responsiveness.
- Bandwidth: Read wastes bandwidth on “no change” polls; Notify sends only meaningful updates.
- Control: Read gives rate control to the central; Notify gives rate control to the peripheral. The better choice depends on product behavior.
- Slow temperature data: Prefer Notify on a threshold such as 0.5°C change.
- On-demand query: Prefer Read when the user clicks refresh or opens a diagnostics view.
Real-world decision:
Scenario: A BLE soil moisture sensor reports to a smartphone app. Soil moisture changes slowly (hours), but user may want instant readings when opening the app.
Hybrid approach:
- Enable Notify with characteristic property: NOTIFY
- Implement threshold-based push: Only notify when moisture changes by >5%
- Support Read for on-demand queries when app opens
Code example (simplified):
float lastMoisture = 0;
float currentMoisture = readSensor();
// Push notification only on significant change
if (abs(currentMoisture - lastMoisture) > 5.0) {
pChar->setValue(currentMoisture);
pChar->notify();
lastMoisture = currentMoisture;
}
// Also allow on-demand Read
pChar->setCallbacks(new MyCallbacks()); // onRead() refreshes valuePower analysis:
- Polling with Read every 60 seconds: Fixed 1440 polls/day, client-initiated,
1440 TX × 2 µAh = 2,880 µAh/day. - Threshold-based Notify: About 10 notifications/day,
10 TX × 2 µAh = 20 µAh/day.
Result: Notify with threshold-based push reduces daily energy by 144× compared to polling every minute.
When to use Read instead of Notify:
- User-initiated queries (settings pages, diagnostics)
- One-time configuration reads (device name, firmware version)
- Data that never changes (hardware revision, serial number)
Common Mistake: Assuming BLE Link Encryption Means End-to-End Security
The error: A developer building a smart door lock uses BLE pairing with “Just Works” mode and enables link-layer encryption. They assume the system is secure because “the data is encrypted.”
What actually happens:
BLE link-layer encryption (AES-CCM) protects confidentiality and integrity between the phone and the lock over the radio link. But it does NOT prevent a man-in-the-middle attack during the initial pairing.
Attack scenario (real exploit demonstrated at DEF CON 2019):
- Day 1: Legitimate user installs lock, pairs phone using “Just Works”
- Day 2: Attacker brings a fake “phone” device within range during a new pairing attempt (e.g., owner pairs a second phone)
- Lock advertises: “I’m a smart lock, ready to pair”
- Attacker’s device: Completes “Just Works” pairing before owner’s phone (no verification step)
- Attacker now has: Valid encryption keys and bonding (stored LTK)
- Owner’s second phone: Pairing fails (lock already paired)
- Later: Attacker returns, connects with stored keys, sends “unlock” command
The “encrypted” fallacy:
- Link-layer encryption protects after pairing
- It does NOT authenticate who you’re pairing with
- “Just Works” pairing has zero authentication bits
The fix (multiple layers required):
Use strong pairing method:
- Numeric Comparison (both devices show 6-digit code to verify)
- Out-of-Band (NFC tap, QR code scan)
- NOT “Just Works” for security-critical devices
Add application-layer authentication:
// Even after BLE pairing, require app-level challenge if (receivedCommand == "unlock") { if (!verifyUserToken(command.token)) { rejectCommand(); // Valid BLE connection, invalid user } }Implement authorization:
- BLE pairing = “you can talk to me”
- App authorization = “you have permission to unlock”
- These are separate security layers
Production example:
August Smart Lock (Gen 1) was vulnerable to MITM during pairing because it used “Just Works.” Fix required:
- Firmware update to require Numeric Comparison (but lock had no display!)
- Workaround: Required physical button press on lock during pairing (OOB-like)
- App-layer: Required cloud account verification before accepting unlock commands
Key takeaway: Encryption protects data in transit. Authentication determines who gets the keys. Authorization determines what they can do with those keys. All three layers are necessary for security-critical IoT devices.
Common Pitfalls
1. Underestimating Stack Overhead in Throughput Calculations
Each BLE stack layer adds header bytes: LL (2B) + L2CAP (4B) + ATT (3B) = 9 bytes overhead per PDU. With default 23-byte ATT MTU, only 20 bytes carry application payload per packet. At 1 Mbps LE 1M PHY, theoretical max is ~125 packets/second × 20 bytes = 2.5 KB/s application throughput — far less than the 1 Mbps physical rate. Negotiate larger MTU (up to 517 bytes) for bulk transfers to approach practical maximums of ~96 KB/s.
2. Running BLE Host Stack and Application in Same Task Priority
BLE host stacks (NimBLE, Bluedroid) use internal tasks for protocol processing. Running application logic at the same or higher FreeRTOS priority than the BLE host task causes starvation, resulting in missed connection events and connection drops under application CPU load. Always run BLE host tasks at higher priority than application tasks, and use queues/semaphores for inter-task communication.
3. Using Legacy Pairing When LE Secure Connections is Available
BLE Legacy Pairing (pre-4.2) uses TK (Temporary Key) up to 6 decimal digits (Just Works = 000000), providing at most 20-bit entropy. An attacker passively recording the pairing exchange can brute-force the LTK offline in <1 second on modern hardware. Always use LE Secure Connections (LESC) which provides 128-bit security via ECDH, and verify peer LESC support before allowing pairing.
4. Ignoring L2CAP CoC for Large Data Transfers
Developers default to GATT for all BLE data transfer, including large file transfers. GATT over ATT has significant per-packet overhead and was not designed for bulk data. BLE 4.1+ supports L2CAP Connection-Oriented Channels (CoC) with configurable MPS up to 65535 bytes and credit-based flow control, achieving 2-4× better throughput than GATT for large payloads. Use L2CAP CoC for OTA firmware updates and large file transfers.
7.12 What’s Next
- Bluetooth Profiles - Apply the GATT and stack knowledge to major real-world profiles such as SPP, HID, and A2DP.
- Bluetooth Connection Establishment - Understand how Link Layer advertising and scanning establish connections.
- Bluetooth Security - Secure GATT services with pairing, bonding, and link-layer encryption.
- Classic vs BLE - Extend the stack comparison to power, range, and use-case selection.
- Bluetooth Implementation - Write firmware using ESP32 BLE APIs to expose custom GATT services.
- Bluetooth Applications - See GATT services applied across healthcare, industrial, and consumer IoT.