7  Bluetooth Protocol Stack and GATT

In 60 Seconds

The BLE protocol stack uses GATT to organize data into Services, Characteristics, and Descriptors. Data flows via Notifications (fast, unconfirmed) or Indications (slower, confirmed delivery). Standard Bluetooth SIG services ensure interoperability, while custom 128-bit UUIDs support proprietary IoT applications.

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

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.

“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:

7.3 Bluetooth Protocol Stack

7.3.1 Classic Bluetooth Stack

The Classic Bluetooth stack is designed for continuous data streaming:

Layer Purpose Key Components
Application User applications Audio, file transfer
Profiles Application behavior A2DP, HFP, SPP, HID
RFCOMM Serial port emulation Virtual COM ports
L2CAP Logical link control Multiplexing, segmentation
Link Manager Connection management Security, power control
Baseband Packet handling Piconet, frequency hopping
Radio Physical transmission 2.4 GHz, GFSK modulation

7.3.2 BLE Protocol Stack

BLE uses a simplified stack optimized for low power:

Layer Purpose Key Components
Application User applications Sensor data, beacons
GATT Data organization Services, characteristics
ATT Attribute protocol Read, write, notify; default MTU 23 bytes (max 517 bytes)
L2CAP Logical link Fixed channels (CID 0x0004 for ATT), CoC
Link Layer Packets, connections States: Standby, Advertising, Scanning, Initiating, Connection, Synchronization
PHY Physical layer LE 1M (1 Mbps), LE 2M (2 Mbps), LE Coded (125/500 kbps long-range)

7.3.3 Key Differences

Aspect Classic Bluetooth BLE
Data Model Stream-based (RFCOMM) Attribute-based (GATT)
Connection Persistent Can be transient
Profiles Complex (A2DP, HFP) Simple (GATT services)
Discovery Profile-specific Universal GATT discovery
Try It: BLE Protocol Stack Explorer

Select a layer of the BLE protocol stack to see its role, key components, and how data flows through it.

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):

UUID Service Description
0x1800 Generic Access Device name, appearance
0x1801 Generic Attribute Service change indication
0x180A Device Information Manufacturer, model, serial
0x180F Battery Service Battery level
0x181A Environmental Sensing Temperature, humidity
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:

Property Code Description
Read 0x02 Client can read value
Write 0x08 Client can write (with response)
Write No Response 0x04 Client can write (no ack)
Notify 0x10 Server can push (no ack)
Indicate 0x20 Server can push (with ack)

Choosing Properties:

Use Case Properties Example
On-demand sensor reading Read Temperature on request
Real-time streaming Notify Heart rate updates
Critical alerts Indicate Low battery warning
Configuration Read + Write Alarm threshold
Commands Write No Response LED on/off
Try It: GATT Characteristic Property Builder

Select properties for a BLE characteristic and see the resulting property byte value, typical use cases, and power/latency implications.

7.4.4 Descriptors

Descriptors provide metadata about characteristics:

Common Descriptors:

UUID Name Purpose
0x2900 Characteristic Extended Properties Additional properties
0x2901 Characteristic User Description Human-readable name
0x2902 Client Characteristic Configuration Enable notify/indicate
0x2904 Characteristic Presentation Format Units, exponent

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 enabled

7.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 expected

7.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 received

7.5.3 Comparison

Aspect Notify Indicate
Acknowledgment No Yes
Latency Lower Higher
Throughput Higher Lower
Reliability Best-effort Guaranteed
Use Case Sensor streaming Alerts, config
Try It: Notify vs Indicate Throughput Calculator

Adjust the connection interval and payload size to compare the effective throughput and latency of Notifications versus Indications. See why Indicate is unsuitable for streaming.

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:

UUID Name Properties
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:

UUID Name Properties
0x2A19 Battery Level Read, Notify

Data Format:

Single byte, 0-100 representing percentage.

7.7.3 Device Information Service (0x180A)

Manufacturer and model information:

Characteristics:

UUID Name Content
0x2A29 Manufacturer Name “Acme Inc”
0x2A24 Model Number “Sensor-v1”
0x2A25 Serial Number “SN123456”
0x2A26 Firmware Revision “1.2.3”
0x2A27 Hardware Revision “A”
0x2A28 Software Revision “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:

Characteristic Properties Size Purpose
Glucose reading Notify 8 bytes Current glucose + trend arrow + 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, 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:

Parameter Initial pairing Normal operation Background (phone in pocket)
Connection interval 15 ms 500 ms 2000 ms
Peripheral latency 0 4 19
Supervision timeout 2 s 6 s 20 s
Effective wake rate 66/sec 1/sec 0.05/sec (every 20 s)
Current draw 3.2 mA 48 uA average 8 uA average

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.

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: $ = = 2 $

Each event: 2 ms radio active at 10 mA, 498 ms sleep at 2 µA: $ = = = 41.99 $

Battery life with 35 mAh battery: $ = = 833 = 34.7 $

With peripheral latency = 4 (skip 4 of 5 events):

Effective wake interval: \(500 \times 5 = 2500\) ms (every 2.5 seconds): $ = = 0.4 $

Current calculation: $ = = = 10.0 $

Battery life: $ = = 3{,}500 = 145.8 $

Power savings ratio: $ = 4.2 $

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.

MTU Usable payload ATT overhead Overhead % Effective throughput (1M PHY, 7.5ms CI)
23 bytes 20 bytes 3 bytes 15% 21.3 kbps
65 bytes 62 bytes 3 bytes 5% 66.1 kbps
185 bytes 182 bytes 3 bytes 2% 194.1 kbps
517 bytes 514 bytes 3 bytes 0.6% 296.4 kbps (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.

Connection interval Max notifications/sec Throughput (20B payload) Current draw (nRF52840)
7.5 ms 133 21.3 kbps 3.8 mA average
15 ms 66 10.6 kbps 2.1 mA average
30 ms 33 5.3 kbps 1.2 mA average
100 ms 10 1.6 kbps 0.42 mA average
500 ms 2 0.32 kbps 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 CR2032 (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 (2.5 days vs 12 month target)

Option B: CI = 1000 ms (matched to data rate)
  Notifications per CI: 1 per second (exact match)
  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 (wakes every 5 seconds)
  Buffers 5 readings, sends in single 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.

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: $ = 23 - 3 = 20 $

Total notifications needed for 256 KB transfer: $ = = = 13{,}107 $

With connection interval CI = 30 ms (33 events/sec), transfer time: $ = 13{,}107 = 393{,}210 = 6.55 $

Option 2: Negotiated MTU = 185 bytes

Usable payload: $ = 185 - 3 = 182 $

Total notifications: $ = = 1{,}440 $

Transfer time with same 30 ms interval: $ = 1{,}440 = 43{,}200 = 0.72 = 43 $

Speedup factor: $ = = 9.1 $

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.

Try It: BLE Throughput and Battery Life Calculator

Adjust MTU, connection interval, and battery capacity to see how GATT parameter choices affect throughput and device lifetime. Experiment to find the sweet spot for your IoT application.

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

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:

MTU Transfer time Radio active Average current (update) Energy consumed
23 bytes 6.55 min 100% 10 mA 10 mA × 6.55 min = 1.09 mAh
185 bytes 0.72 min 100% 10 mA 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.

Criterion Client-initiated READ Server-initiated NOTIFY Winner
Update frequency Client polls (e.g., every 5 sec) Server pushes (when data changes) Notify for high-rate
Power (peripheral) RX listen + TX response each poll TX only when data changes Notify (sleep between updates)
Power (central) TX poll + RX response RX only when data available Notify
Latency Worst-case = poll interval Instant (sub-millisecond after change) Notify
Bandwidth Wastes bandwidth on “no change” polls Only sends when data changes Notify
Control Central controls rate Peripheral controls rate Context-dependent
Use case: temperature (changes slowly) Notify (push on 0.5°C change)
Use case: on-demand query Read (user clicks “refresh”)

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:

  1. Enable Notify with characteristic property: NOTIFY
  2. Implement threshold-based push: Only notify when moisture changes by >5%
  3. 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 value

Power analysis:

Approach Update frequency Daily notifications Energy (µAh/day)
Polling (Read every 60s) Fixed 1440/day N/A (client-initiated) 1440 TX × 2 µAh = 2,880 µAh
Notify (threshold) Variable (~10/day) 10 10 TX × 2 µAh = 20 µAh

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):

  1. Day 1: Legitimate user installs lock, pairs phone using “Just Works”
  2. Day 2: Attacker brings a fake “phone” device within range during a new pairing attempt (e.g., owner pairs a second phone)
  3. Lock advertises: “I’m a smart lock, ready to pair”
  4. Attacker’s device: Completes “Just Works” pairing before owner’s phone (no verification step)
  5. Attacker now has: Valid encryption keys and bonding (stored LTK)
  6. Owner’s second phone: Pairing fails (lock already paired)
  7. 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):

  1. 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
  2. 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
        }
    }
  3. 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

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.

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.

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.

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

Topic Chapter Why Read Next
Bluetooth Profiles (SPP, HID, A2DP) Bluetooth Profiles Apply the GATT and stack knowledge to major real-world profiles
BLE Connection Establishment Bluetooth Connection Establishment Understand how Link Layer advertising and scanning establish connections
Bluetooth Security & Pairing Bluetooth Security Secure GATT services with pairing, bonding, and link-layer encryption
BLE Classic vs BLE Comparison Classic vs BLE Extend the stack comparison to power, range, and use-case selection
BLE Implementation & Labs Bluetooth Implementation Write firmware using ESP32 BLE APIs to expose custom GATT services
Bluetooth Applications Bluetooth Applications See GATT services applied across healthcare, industrial, and consumer IoT