920  BLE Hands-On Labs

920.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Build Complete BLE Projects: Implement end-to-end BLE sensor applications
  • Create Standard GATT Services: Implement Heart Rate Service and other Bluetooth SIG specifications
  • Develop Python Dashboards: Build real-time monitoring applications with bleak
  • Implement Indoor Positioning: Use BLE beacons for trilateration-based location estimation
  • Simulate Mesh Networks: Model BLE mesh message flooding and routing

What youโ€™ll build: Four complete projects that demonstrate real-world BLE applications.

Before starting: - Review BLE Code Examples for basic patterns - Review BLE Python Implementations for library usage - Have hardware ready: ESP32 dev boards, sensors, or use Wokwi simulator

Time commitment: Each lab takes 1-3 hours depending on your experience level.

920.2 Prerequisites

Before starting these labs:

920.3 Lab 1: ESP32 BLE Heart Rate Monitor

Objective: Build a BLE heart rate monitor using standard Heart Rate Service (0x180D).

Materials: - ESP32 development board - Heart rate sensor (MAX30102) or simulate with potentiometer - Breadboard and wires - nRF Connect app (Android/iOS)

Circuit Diagram:

MAX30102       ESP32
--------       -----
VIN    ------>  3.3V
GND    ------>  GND
SDA    ------>  GPIO 21 (I2C SDA)
SCL    ------>  GPIO 22 (I2C SCL)

Complete Code:

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>

// Heart Rate Service UUID (standard)
#define SERVICE_UUID        "0000180d-0000-1000-8000-00805f9b34fb"
#define CHARACTERISTIC_UUID "00002a37-0000-1000-8000-00805f9b34fb"

BLEServer* pServer = NULL;
BLECharacteristic* pCharacteristic = NULL;
bool deviceConnected = false;
bool oldDeviceConnected = false;
uint8_t heartRate = 72;  // Initial heart rate

class ServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) {
      deviceConnected = true;
      Serial.println("Client connected");
    };

    void onDisconnect(BLEServer* pServer) {
      deviceConnected = false;
      Serial.println("Client disconnected");
    }
};

void setup() {
  Serial.begin(115200);
  Serial.println("BLE Heart Rate Monitor Starting...");

  // Initialize BLE
  BLEDevice::init("HR-Monitor-ESP32");

  // Create BLE Server
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());

  // Create Heart Rate Service
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // Create Heart Rate Measurement Characteristic
  pCharacteristic = pService->createCharacteristic(
                      CHARACTERISTIC_UUID,
                      BLECharacteristic::PROPERTY_READ |
                      BLECharacteristic::PROPERTY_NOTIFY
                    );

  // Add descriptor for notifications
  pCharacteristic->addDescriptor(new BLE2902());

  // Start the service
  pService->start();

  // Start advertising
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  pAdvertising->setMinPreferred(0x06);
  pAdvertising->setMinPreferred(0x12);
  BLEDevice::startAdvertising();

  Serial.println("BLE Heart Rate Monitor ready!");
  Serial.println("Open nRF Connect app to connect");
}

void loop() {
  // Handle connection state changes
  if (deviceConnected) {
    // Simulate heart rate (in real app, read from sensor)
    heartRate = random(60, 100);

    // Heart Rate Measurement format:
    // Byte 0: Flags (0x00 = uint8 BPM, sensor contact detected)
    // Byte 1: Heart Rate Value (BPM)
    uint8_t hrData[2] = {0x00, heartRate};

    pCharacteristic->setValue(hrData, 2);
    pCharacteristic->notify();

    Serial.printf("Heart Rate: %d BPM\n", heartRate);

    delay(1000);  // Update every second
  }

  // Handle reconnection
  if (!deviceConnected && oldDeviceConnected) {
    delay(500);
    pServer->startAdvertising();
    Serial.println("Advertising restarted");
    oldDeviceConnected = deviceConnected;
  }

  if (deviceConnected && !oldDeviceConnected) {
    oldDeviceConnected = deviceConnected;
  }
}

Expected Output (Serial Monitor):

BLE Heart Rate Monitor Starting...
BLE Heart Rate Monitor ready!
Open nRF Connect app to connect
Client connected
Heart Rate: 72 BPM
Heart Rate: 78 BPM
Heart Rate: 65 BPM
Client disconnected
Advertising restarted

Testing with nRF Connect: 1. Open nRF Connect app 2. Scan for devices - Find โ€œHR-Monitor-ESP32โ€ 3. Connect to device 4. Find Heart Rate Service (0x180D) 5. Enable notifications on Heart Rate Measurement characteristic 6. Watch real-time heart rate updates

Learning Outcomes: - Implement standard BLE GATT services - Handle BLE server callbacks (connect/disconnect) - Use BLE notifications for real-time data - Work with standard Bluetooth SIG services - Test BLE devices with professional tools

Challenges: 1. Add Battery Service (0x180F) with battery level notifications 2. Implement actual MAX30102 heart rate sensor reading 3. Add energy expended calculation (per BLE HRS specification) 4. Implement RR-Interval measurements for heart rate variability


920.4 Lab 2: Python BLE Environmental Monitor Dashboard

Objective: Create Python dashboard that connects to BLE environmental sensors and displays data in real-time.

Materials: - Python 3.7+ - ESP32 with BLE (from Lab 1 or separate sensor) - bleak library (pip install bleak)

Expected Output:

============================================================
  BLE Environmental Monitor Dashboard
============================================================

Scanning for devices matching 'HR-Monitor'...
Found: HR-Monitor-ESP32 (A4:CF:12:34:56:78)

Connecting to HR-Monitor-ESP32...
Connected to HR-Monitor-ESP32

Subscribed to Heart Rate notifications
Warning: Battery service not available

Monitoring for 30 seconds...

[14:23:10] Heart Rate: 72 BPM
[14:23:11] Heart Rate: 78 BPM
[14:23:12] Heart Rate: 65 BPM
[14:23:13] Heart Rate: 82 BPM
...
Disconnected

Learning Outcomes: - Use Python bleak library for BLE communication - Connect to BLE GATT servers - Subscribe to BLE notifications - Parse standard BLE data formats - Handle asynchronous BLE operations


920.5 Lab 3: BLE Beacon-Based Indoor Positioning

Objective: Use multiple BLE beacons to estimate indoor position using RSSI trilateration.

Materials: - 3+ ESP32 boards (as iBeacon transmitters) - 1 ESP32 or Python device (as scanner/receiver) - Known beacon positions

Beacon Setup (ESP32 #1, #2, #3):

#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLEBeacon.h>

#define BEACON_UUID "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"

// Different MAJOR/MINOR for each beacon
#define BEACON_MAJOR 1
#define BEACON_MINOR 101  // Change to 102, 103 for other beacons

BLEAdvertising *pAdvertising;

void setup() {
  Serial.begin(115200);
  Serial.println("BLE iBeacon Starting...");

  BLEDevice::init("iBeacon");

  // Create BLE Server
  BLEServer *pServer = BLEDevice::createServer();

  // Configure iBeacon
  BLEBeacon beacon;
  beacon.setManufacturerId(0x4C00);  // Apple's ID
  beacon.setProximityUUID(BLEUUID(BEACON_UUID));
  beacon.setMajor(BEACON_MAJOR);
  beacon.setMinor(BEACON_MINOR);
  beacon.setSignalPower(-59);  // Calibrated RSSI at 1m

  // Get advertising object
  pAdvertising = BLEDevice::getAdvertising();

  // Set beacon data
  BLEAdvertisementData advData;
  advData.setFlags(0x04);  // BR_EDR_NOT_SUPPORTED
  advData.setManufacturerData(beacon.getData());

  pAdvertising->setAdvertisementData(advData);
  pAdvertising->setScanResponseData(advData);

  // Start advertising
  pAdvertising->start();

  Serial.printf("iBeacon broadcasting (Major: %d, Minor: %d)\n",
                BEACON_MAJOR, BEACON_MINOR);
}

void loop() {
  // Nothing to do - just advertise
  delay(1000);
}

Expected Output (Python Positioning System):

Indoor Positioning System
============================================================

Known Beacon Positions:
  Beacon 101: (0.0m, 0.0m)
  Beacon 102: (5.0m, 0.0m)
  Beacon 103: (0.0m, 5.0m)

Scanning for beacons...

Scan #1:
  Beacon 101: RSSI -52 dBm -> 1.78m
  Beacon 102: RSSI -65 dBm -> 4.47m
  Beacon 103: RSSI -58 dBm -> 2.82m
Estimated Position: (1.2m, 1.5m)

Scan #2:
  Beacon 101: RSSI -55 dBm -> 2.24m
  Beacon 102: RSSI -62 dBm -> 3.55m
  Beacon 103: RSSI -60 dBm -> 3.16m
Estimated Position: (1.4m, 1.7m)
...

Learning Outcomes: - Implement iBeacon protocol on ESP32 - Parse BLE beacon advertisement packets - Convert RSSI to distance using path loss model - Implement 2D trilateration algorithm - Build indoor positioning systems - Understand RSSI limitations and filtering

Challenges: 1. Add Kalman filtering for smoother position estimates 2. Calibrate path loss exponent (n) for your environment 3. Add 4th beacon for 3D positioning 4. Implement zone-based proximity instead of exact coordinates 5. Create visual map showing beacons and estimated position


920.6 Lab 4: BLE Mesh Network Simulation

Objective: Simulate a BLE mesh network with multiple nodes relaying messages.

Materials: - 3+ ESP32 boards - ESP-IDF with BLE Mesh support (or use simulation library)

Example Output (Mesh Simulator):

BLE Mesh Network Simulator
============================================================

Network Topology:
  Node1 (switch): Neighbors -> Node2
  Node2 (relay): Neighbors -> Node1, Node4
  Node3 (light): Neighbors -> Node2, Node5
  Node4 (sensor): Neighbors -> Node2, Node5
  Node5 (light): Neighbors -> Node3, Node4

============================================================
Test 1: Switch broadcasts LIGHT_SET command
============================================================
  Node3 (Light): ON
  Node5 (Light): ON

Message reached via 4 paths:
  Node1 -> Node2 -> Node3
  Node1 -> Node2 -> Node4 -> Node5
  Node1 -> Node2 -> Node4
  Node1 -> Node2

============================================================
Test 2: Sensor sends data to relay
============================================================
  Node2 received sensor data: {'temperature': 22.5, 'humidity': 45}

Message reached via 1 path(s):
  Node4 -> Node2

============================================================
Final Node States:
============================================================
  Node3 (light): {'on': True}
  Node5 (light): {'on': True}

Learning Outcomes: - Understand BLE mesh network topology - Implement message flooding with TTL - Prevent routing loops with message caching - Model multi-hop communication - Simulate real-world mesh scenarios

Challenges: 1. Implement managed flooding (more efficient than flooding) 2. Add friend/low-power node relationships 3. Implement publish/subscribe model 4. Add network provisioning and key distribution 5. Simulate node failures and self-healing

920.7 Worked Examples

NoteWorked Example: Optimizing BLE Throughput with MTU Negotiation for Firmware OTA Update

Scenario: A smart lock manufacturer needs to implement over-the-air (OTA) firmware updates via BLE. The firmware image is 256 KB and must transfer in under 10 minutes to avoid user frustration. The target device uses Nordic nRF52840 with BLE 5.0 support.

Given: - Firmware size: 256 KB (262,144 bytes) - Target transfer time: < 10 minutes (600 seconds) - Default ATT MTU: 23 bytes (20 bytes usable payload) - Maximum supported MTU: 247 bytes (244 bytes usable payload) - Connection interval: 15 ms (configurable) - Data Length Extension (DLE): supported (251 byte PDU) - BLE 5.0 2M PHY: supported

Steps:

  1. Calculate minimum required throughput:

    • Data to transfer: 262,144 bytes
    • Time available: 600 seconds
    • Minimum throughput: 262,144 / 600 = 437 bytes/second (approximately 3.5 kbps)
  2. Calculate throughput with default settings (no optimization):

    • MTU: 23 bytes - 20 bytes payload per ATT packet
    • Connection interval: 15 ms - 66 connection events/second
    • Assuming 1 packet per connection event: 20 x 66 = 1,320 bytes/second
    • Transfer time: 262,144 / 1,320 = 199 seconds (approximately 3.3 minutes)
    • This meets the requirement, but letโ€™s optimize further for better UX
  3. Optimize with MTU exchange:

    // After connection established, request MTU exchange
    void on_connected(uint16_t conn_handle) {
        // Request 247-byte MTU (maximum for BLE 4.2+)
        sd_ble_gattc_exchange_mtu_request(conn_handle, 247);
    }
    
    void on_mtu_exchanged(uint16_t conn_handle, uint16_t mtu) {
        // Negotiated MTU (minimum of both sides)
        // Usable payload = MTU - 3 (ATT header)
        g_max_payload = mtu - 3;  // 244 bytes with 247 MTU
    }
  4. Calculate optimized throughput:

    • MTU: 247 bytes - 244 bytes payload
    • With DLE enabled: can send 244 bytes in single LL packet
    • Packets per connection event: up to 6 (with 15ms CI)
    • Conservative estimate (4 packets/event): 244 x 4 x 66 = 64,416 bytes/second
    • Transfer time: 262,144 / 64,416 = 4.1 seconds
  5. Enable 2M PHY for additional speed:

    // Request PHY update to 2M after connection
    ble_gap_phys_t phys = {
        .tx_phys = BLE_GAP_PHY_2MBPS,
        .rx_phys = BLE_GAP_PHY_2MBPS
    };
    sd_ble_gap_phy_update(conn_handle, &phys);
    • 2M PHY doubles bit rate: ~128 kB/second theoretical
    • Transfer time: 262,144 / 128,000 = 2 seconds (theoretical maximum)

Result: With MTU 247, DLE, 2M PHY, and Write Without Response, the 256 KB firmware transfers in approximately 5-8 seconds in practice (accounting for protocol overhead and flow control). This represents a 25-40x improvement over default settings.

Key Insight: BLE throughput optimization requires enabling multiple features together: MTU exchange (12x payload increase), Data Length Extension (fewer LL packets), 2M PHY (2x bit rate), and Write Without Response (eliminates ACK latency). Each feature independently provides improvement, but they multiply when combined.

NoteWorked Example: Power Budget Analysis for Battery-Powered BLE Sensor Node

Scenario: An agricultural sensor node monitors soil moisture, temperature, and light levels every 15 minutes and transmits data to a gateway. The device must operate for 2 years on 2x AA batteries without maintenance. The node uses an ESP32-C3 module.

Given: - Battery capacity: 2x AA (2,800 mAh @ 1.5V = 4,200 mWh at 3V effective after boost converter, 85% efficiency) - ESP32-C3 deep sleep current: 5 microA - ESP32-C3 active current: 80 mA (Wi-Fi/BLE radio) - Soil moisture sensor: 15 mA for 50ms per reading - Temperature sensor (I2C): 0.5 mA for 10ms - Light sensor: 0.2 mA for 5ms - BLE advertising TX: 12 mA for 3ms (connectable advertising) - BLE connection event: 15 mA for 5ms (TX + RX) - Report interval: 15 minutes

Steps:

  1. Calculate energy per measurement cycle:

    Sensor readings:

    • Wake from deep sleep: 80 mA x 2ms = 0.16 mAms
    • Soil moisture: 15 mA x 50ms = 0.75 mAms
    • Temperature: 0.5 mA x 10ms = 0.005 mAms
    • Light: 0.2 mA x 5ms = 0.001 mAms
    • Sensor subtotal: 0.92 mAms

    BLE transmission (advertising + connection):

    • Start advertising: 12 mA x 3ms x 10 events = 0.36 mAms
    • Connection event (data TX): 15 mA x 5ms x 3 events = 0.225 mAms
    • BLE subtotal: 0.59 mAms

    MCU processing:

    • Data processing: 30 mA x 5ms = 0.15 mAms
    • Total per cycle: 0.92 + 0.59 + 0.15 = 1.66 mAms = 0.00166 mAh
  2. Calculate daily energy consumption:

    • Cycles per day: 24h x 4/hour = 96 cycles
    • Active energy: 96 x 0.00166 mAh = 0.159 mAh/day
    • Deep sleep energy: 5 microA x 24h = 0.12 mAh/day
    • Total daily: 0.159 + 0.12 = 0.28 mAh/day
  3. Calculate battery life:

    • Usable capacity (80% of rated): 2,800 x 0.8 = 2,240 mAh
    • Battery life: 2,240 / 0.28 = 8,000 days = 21.9 years
  4. Reality check - identify hidden consumers:

    • Voltage regulator quiescent: ~10 microA - adds 0.24 mAh/day
    • RTC crystal oscillator: ~1 microA - adds 0.024 mAh/day
    • Leakage currents: ~2 microA - adds 0.048 mAh/day
    • Self-discharge (2%/year for alkaline): ~4.7 mAh/month = 0.15 mAh/day
    • Revised daily: 0.28 + 0.24 + 0.024 + 0.048 + 0.15 = 0.74 mAh/day
  5. Revised battery life calculation:

    • Battery life: 2,240 / 0.74 = 3,027 days = 8.3 years
    • Still exceeds 2-year requirement with large margin
  6. Add margin for real-world degradation:

    • Temperature effects (cold reduces capacity 20%): 0.8x factor
    • Battery aging (10% capacity loss/year): ~0.85x over 2 years
    • Connection failures (retry overhead): 1.2x energy estimate
    • Worst-case daily: 0.74 x 1.2 = 0.89 mAh/day
    • Worst-case capacity: 2,240 x 0.8 x 0.85 = 1,523 mAh
    • Conservative estimate: 1,523 / 0.89 = 1,711 days = 4.7 years

Result: The design achieves 4.7+ year battery life under worst-case assumptions, comfortably exceeding the 2-year requirement. Key design decisions: 15-minute report interval (not continuous), deep sleep between readings, short BLE connection using bonding (no re-pairing), and efficient sensor duty-cycling.

Key Insight: Sleep current dominates long-term battery life for infrequent reporting sensors. Always measure actual deep sleep current with a microA-capable meter - leakage from GPIO configuration, pull-ups, and connected sensors often exceeds datasheet MCU values.

920.8 Summary

This chapter provided four complete BLE project implementations:

  • Lab 1 - Heart Rate Monitor: Standard GATT service implementation with ESP32 and nRF Connect testing
  • Lab 2 - Environmental Dashboard: Python bleak client for real-time monitoring with notifications
  • Lab 3 - Indoor Positioning: iBeacon deployment and trilateration-based location estimation
  • Lab 4 - Mesh Network: BLE mesh simulation with message flooding and multi-hop routing
  • Worked Examples: MTU optimization for OTA updates and power budget analysis for battery devices

920.9 Knowledge Check

Question: What is the purpose of GATT in BLE implementations?

Explanation: B. GATT defines services/characteristics and how a central reads, writes, and subscribes to data on a peripheral.

Question: What typically happens when you increase the BLE connection interval?

Explanation: B. Fewer connection events save power, but reduce how often data can be exchanged.

Question: What is the purpose of BLE advertising packets?

Explanation: B. Advertising lets devices announce presence/capabilities so centrals can discover and connect.

Question: In GATT, what allows a peripheral to push sensor updates to a connected central without polling?

Explanation: C. Notifications let the peripheral send updates asynchronously once the central subscribes (CCCD).

920.10 Whatโ€™s Next

The next chapter explores Zigbee Fundamentals and Architecture, covering mesh networking protocols designed for IoT applications. Youโ€™ll learn how Zigbee differs from BLE in terms of network topology, device roles (Coordinator, Router, End Device), and use cases requiring larger device counts and self-healing mesh capabilities.