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:
- BLE Code Examples and Simulators: Basic GATT server/client patterns
- BLE Python Implementations: Scanner and proximity detection code
- Hardware: ESP32 development board(s) or Wokwi simulator access
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
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:
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)
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
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 }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
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.
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:
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
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
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
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
Revised battery life calculation:
- Battery life: 2,240 / 0.74 = 3,027 days = 8.3 years
- Still exceeds 2-year requirement with large margin
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
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.