23  BLE Hands-On Labs

In 60 Seconds

Building real BLE applications requires combining GATT service design, notification-based data push, and power-aware connection management. Standard Bluetooth SIG profiles (like Heart Rate Service) ensure cross-platform interoperability, while custom services provide flexibility for unique IoT use cases.

Key Concepts
  • BLE Lab Setup: Minimum requirements: ESP32 development board (with BLE), USB cable, nRF Connect app on smartphone, and IDE (VS Code + ESP-IDF or Arduino IDE)
  • Service Discovery Procedure: Client sends ATT Read By Group Type Request to enumerate all primary services, then ATT Read By Type Request to enumerate characteristics within each service
  • Characteristic Handle: Unique 16-bit identifier assigned by the GATT server to each attribute (service declaration, characteristic declaration, value, descriptor)
  • Notification Enable Sequence: Client writes 0x0001 to the CCCD (at characteristic handle + 1 or + 2) to subscribe; server calls ble_gattc_notify_custom() to push data
  • OTA DFU Lab: Firmware update over BLE using Nordic NRF DFU protocol (Image Info service + Packet characteristic); requires bootloader in flash, DFU trigger, and nRF Connect DFU function
  • BLE Traffic Capture Lab: Using nRF Sniffer USB dongle with Wireshark dissector plugin to capture and decode BLE advertising + connection packets for debugging
  • Power Profiling Lab: Using Nordic PPK2 (Power Profiler Kit 2) or Otii Arc to measure BLE duty cycle power consumption, identifying wakeup overhead and connection event energy
  • BLE Latency Measurement: GPIO toggle at TX and RX points + logic analyzer to measure actual BLE notification latency from sensor event to central receipt
Minimum Viable Understanding

Building real BLE applications requires combining GATT service design, notification-based data push, and power-aware connection management. Standard Bluetooth SIG profiles (like Heart Rate Service) ensure interoperability across platforms, while custom services provide flexibility for unique IoT use cases.

23.1 Learning Objectives

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

  • Implement Complete BLE Projects: Construct end-to-end BLE sensor applications using ESP32 and the Arduino BLE library
  • Configure Standard GATT Services: Implement Heart Rate Service (0x180D) and other Bluetooth SIG specifications with correct characteristic properties
  • Develop Python Dashboards: Build real-time monitoring applications using the bleak library with asynchronous notification handling
  • Apply Indoor Positioning: Configure BLE beacons and apply trilateration algorithms to estimate indoor locations from RSSI measurements
  • Analyze Mesh Network Behavior: Evaluate BLE mesh message flooding, TTL propagation, and relay node impact on network traffic

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

Before starting:

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

Use the labs as a staged implementation path:

  • Lab 1 builds a standard Heart Rate Service so the device can be inspected by existing BLE tools without a custom phone app.
  • Lab 2 shifts to the central side by subscribing to notifications from Python.
  • Lab 3 uses BLE beacons for indoor positioning, which adds RSSI filtering and calibration concerns.
  • Lab 4 introduces Bluetooth Mesh behavior, where relay choices and TTL values decide whether messages remain efficient or flood the network.

In Bluetooth Mesh, relay density and Time-To-Live (TTL) control message cost.

  • Inputs: 50 total nodes, about 40 relay-capable nodes, average fanout near 3 neighbors, and TTL set to 7.
  • Naive upper bound: if every relay forwarded to three new neighbors at every hop, the rough upper bound is 40 * 3^7 = 87,480 attempted transmissions.
  • Practical behavior: Bluetooth Mesh uses message caching so nodes do not repeatedly relay the same message. In a dense lab network, the observed count is usually closer to hundreds of radio events, not tens of thousands.
  • Design rule: keep relay enabled on mains-powered nodes, lower TTL for local control traffic, and avoid making small battery devices relay routine messages.

23.2 Prerequisites

Before starting these labs:

23.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)

Wiring:

  • MAX30102 VIN to ESP32 3.3V.
  • MAX30102 GND to ESP32 GND.
  • MAX30102 SDA to ESP32 GPIO 21.
  • MAX30102 SCL to ESP32 GPIO 22.

Complete Code:

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

// Standard Heart Rate Service and Measurement UUIDs
#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;

class ServerCB : public BLEServerCallbacks {
  void onConnect(BLEServer* s)    { deviceConnected = true; }
  void onDisconnect(BLEServer* s) { deviceConnected = false; }
};

void setup() {
  Serial.begin(115200);
  BLEDevice::init("HR-Monitor-ESP32");
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new ServerCB());

  BLEService* pSvc = pServer->createService(SERVICE_UUID);
  pCharacteristic = pSvc->createCharacteristic(CHARACTERISTIC_UUID,
      BLECharacteristic::PROPERTY_READ |
      BLECharacteristic::PROPERTY_NOTIFY);
  pCharacteristic->addDescriptor(new BLE2902());
  pSvc->start();

  BLEDevice::getAdvertising()->addServiceUUID(SERVICE_UUID);
  BLEDevice::getAdvertising()->start();
}

void loop() {
  if (deviceConnected) {
    // HR Measurement: Byte 0 = flags, Byte 1 = BPM
    uint8_t hrData[2] = {0x00, (uint8_t)random(60, 100)};
    pCharacteristic->setValue(hrData, 2);
    pCharacteristic->notify();
    delay(1000);
  } else {
    static bool wasConnected = false;
    if (wasConnected) { pServer->startAdvertising(); wasConnected = false; }
  }
}

Objective: Run the BLE Heart Rate Monitor code on a simulated ESP32 and observe the GATT service lifecycle: initialization, advertising, and simulated heart rate notifications.

Open the simulator directly: Wokwi ESP32 starter project.

Paste the Heart Rate Monitor code above into the simulator. Watch the Serial Monitor for BLE initialization and simulated heart rate values. Note: Wokwi simulates BLE stack output but does not have a virtual BLE client, so you will see advertising start and simulated readings in the serial output.

What to Observe:

  1. BLE initialization creates a GATT server with the standard Heart Rate Service UUID (0x180D)
  2. The characteristic supports both READ and NOTIFY properties
  3. Heart rate values are sent as a 2-byte array (flags + BPM value)
  4. Try modifying random(60, 100) to simulate exercise (e.g., random(120, 180))

Expected Output (Serial Monitor):

  • The sketch prints that the BLE heart rate monitor is starting.
  • It confirms that advertising is ready.
  • After a central connects, simulated heart-rate values such as 72, 78, and 65 BPM appear.
  • After disconnect, the device restarts advertising so another central can connect.

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

23.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:

  • The dashboard scans for devices matching HR-Monitor.
  • It reports the discovered device name and address.
  • It connects to the device and subscribes to Heart Rate notifications.
  • It warns if optional services, such as Battery Service, are not present.
  • It prints timestamped heart-rate updates for the monitoring window and then disconnects cleanly.

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

23.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 <BLEBeacon.h>

#define BEACON_UUID  "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"
#define BEACON_MAJOR 1
#define BEACON_MINOR 101  // Change to 102, 103 for other beacons

void setup() {
  Serial.begin(115200);
  BLEDevice::init("iBeacon");
  BLEDevice::createServer();

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

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

  BLEAdvertising* pAdv = BLEDevice::getAdvertising();
  pAdv->setAdvertisementData(advData);
  pAdv->start();
}

void loop() { delay(1000); }

Expected Output (Python Positioning System):

  • Known beacon positions are loaded, such as Beacon 101 at (0.0 m, 0.0 m), Beacon 102 at (5.0 m, 0.0 m), and Beacon 103 at (0.0 m, 5.0 m).
  • Each scan reports RSSI and estimated distance for each beacon.
  • The trilateration step prints an estimated receiver position, such as (1.2 m, 1.5 m).
  • Repeated scans should move gradually after filtering rather than jumping sharply between positions.

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

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

  • The simulator prints each node role and neighbor list.
  • A switch broadcast should reach both light nodes through relay paths.
  • A sensor message should reach the relay with a single local path when TTL and topology allow it.
  • Final node states should show which lights changed state and which sensor payloads were received.

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

23.7 Worked Examples

Worked 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 the connection is established, request an ATT MTU of 247 bytes.
    • Use the negotiated value returned by the stack, because the final MTU is the minimum supported by both devices.
    • Subtract the 3-byte ATT header from the negotiated MTU to get usable payload. With MTU 247, the usable payload is 244 bytes.
  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 a PHY update after connection.
    • Set both transmit and receive PHY preferences to 2M when the central and peripheral support BLE 5.0.
    • Fall back to 1M PHY when the peer or environment cannot support the faster mode reliably.
    • 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.

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

Scenario: A smart thermostat sends temperature updates to a smartphone app. The design team debates whether to use a 7.5ms connection interval (fast response) or 100ms interval (low power). Calculate the trade-offs.

Given:

  • BLE connection event duration: 3ms (includes TX, RX, and turnaround time)
  • Peripheral sleep current: 2 microA
  • Peripheral active current (during connection event): 15mA @ 3V
  • Battery: 1000mAh coin cell
  • Update rate: Temperature change every 30 seconds

Steps:

1. Calculate average latency for user-initiated read:

  • 7.5 ms interval: average wait is about half the interval, or 3.75 ms.
  • 100 ms interval: average wait is about 50 ms.
  • Interpretation: when the user taps refresh, the central usually waits until the next connection event.

2. Calculate energy per connection event:

  • Active energy: 15 mA * 3 V * 3 ms = 135 microjoules.
  • Sleep energy: 2 microA * 3 V * 97 ms = about 0.6 microjoules.
  • Total per event: about 135.6 microjoules.

3. Calculate daily energy consumption:

  • 7.5 ms interval: about 11,520,000 events/day, or about 1,562 joules/day.
  • 100 ms interval: about 864,000 events/day, or about 117 joules/day.

4. Calculate battery life:

  • Battery capacity: 1000 mAh * 3 V * 3600 s = 10,800 joules.
  • 7.5 ms interval: 10,800 / 1,562 = 6.9 days.
  • 100 ms interval: 10,800 / 117 = 92.3 days, or about 3 months.

Result: The 7.5ms interval provides 13x faster response but drains battery 13x faster. For a thermostat (infrequent user interaction), the 100ms interval is far superior.

Key Insight: Connection interval is the dominant factor in BLE power consumption. For applications with infrequent updates, using the longest acceptable interval (typically 100-500ms) extends battery life by 10-50x compared to aggressive intervals. Only use fast intervals (<20ms) for latency-critical applications like wireless audio, gaming controllers, or fitness trackers with real-time step counting.

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

23.8.1 Knowledge Check: GATT Service Standards

23.8.2 Knowledge Check: BLE Indoor Positioning

23.8.3 Knowledge Check: BLE Power Budget

23.9 Knowledge Check

Common Pitfalls

ESP32 OTA firmware update labs require the OTA bootloader partition table (CONFIG_PARTITION_TABLE_TWO_OTA) and a partition large enough for two firmware images. Flashing a standard single-partition firmware and attempting OTA results in a partition write error. Always use ota_2.csv partition table and verify partition layout with esptool.py read_flash before starting OTA labs.

Arduino BLE libraries (ESP32 BLE Arduino, ArduinoBLE) use different internal architectures than native ESP-IDF NimBLE/Bluedroid. Attempting to use Arduino BLE functions alongside ESP-IDF BLE calls causes duplicate stack initialization and crashes. Choose one framework for the entire project and do not mix API calls across the Arduino and ESP-IDF BLE layers.

If a GATT characteristic requires authenticated write permission for its CCCD, writing 0x0001 without prior pairing and bonding returns ATT Error 0x05 (Insufficient Authentication). The lab must include pairing steps (bonding via nRF Connect: Pair button) before subscribing. Log the ATT error response in nRF Connect to identify authentication failures rather than assuming the notification subscription failed for other reasons.

BLE power optimization labs with a battery-powered device that has low charge will show artificially high power consumption as the voltage regulator drops below its efficient operating range. Always start power profiling labs with a fully charged or bench-power-supplied device at the nominal supply voltage, and document the supply voltage used to ensure reproducible results.

23.10 What’s Next

Prioritize these follow-up chapters based on which lab you want to deepen: