21  BLE Code Examples and Simulators

In 60 Seconds

BLE programming follows a client-server model: peripherals (GATT servers) advertise services and characteristics, while centrals (clients) discover, connect, and exchange data. Python’s bleak library enables scanning from a PC, while ESP32 Arduino code creates embedded BLE peripherals.

Key Concepts
  • NimBLE GATT Server Definition: C struct array ble_gatt_svc_def[] declaring services with characteristics, properties, and callbacks; registered via ble_gatts_count_cfg() and ble_gatts_add_svcs()
  • esp_ble_gap_config_adv_data(): Bluedroid API for configuring BLE advertisement payload including service UUIDs, device name, TX power, and appearance
  • ble_gattc_notify_custom(): NimBLE function for sending GATT notifications from server to subscribed clients; returns error if client has not enabled notifications
  • Python bleak Library: Cross-platform (Windows/macOS/Linux) async BLE library using asyncio; supports scan, connect, GATT read/write/notify on all major OS BLE stacks
  • BLEDevice.connect(): bleak API to initiate a BLE connection; returns BLEClient context manager for service discovery and characteristic operations
  • Wokwi BLE Simulation: Browser-based ESP32 BLE simulation supporting advertising, connection, and GATT operations; uses virtual BLE devices to test without hardware
  • AT Command BLE: Some BLE modules (HC-08, HM-10, AT-09) expose BLE configuration via UART AT commands; simplifies integration but limits performance to module’s fixed profiles
  • BLE Sniffer (nRF Sniffer, Ubertooth): Hardware tool for passive BLE packet capture; essential for debugging pairing failures, connection parameter negotiations, and GATT transactions
Minimum Viable Understanding

BLE programming follows a client-server model: peripherals (GATT servers) advertise services and characteristics, while centrals (clients) discover, connect, and exchange data. Python’s bleak library enables scanning and connecting from a PC, while ESP32 Arduino code creates embedded BLE peripherals.

21.1 Learning Objectives

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

  • Implement Python BLE Scanners: Apply the bleak library to discover and connect to nearby BLE devices
  • Construct ESP32 BLE Beacons: Build Arduino-based GATT servers that advertise sensor data to connected clients
  • Explain GATT Structure: Describe how services, characteristics, and descriptors are organized and accessed
  • Configure BLE Notifications: Enable real-time data push from peripheral to central using CCCD descriptors
  • Evaluate Simulator Outputs: Analyze Wokwi-based BLE device simulations to diagnose behavior and verify correctness

What you’ll learn: This chapter provides working code examples for BLE development - both Python scripts for scanning/connecting and ESP32 Arduino code for creating BLE peripherals.

Prerequisites:

  • Basic Python or C/Arduino programming
  • Understanding of BLE concepts (GATT, services, characteristics)
  • Review Bluetooth Fundamentals first

Key takeaway: BLE programming follows a client-server model where peripherals (servers) advertise services and centrals (clients) discover and connect to read/write data.

“Time to write real code!” Sammy the Sensor cheered. “With Python and the bleak library, you can turn your computer into a BLE scanner. Just a few lines of code and suddenly your laptop can see every Bluetooth device around you – fitness trackers, smart bulbs, headphones, everything!”

“And on the ESP32 side,” Lila the LED added, “you write Arduino code to make the microcontroller advertise itself as a BLE device. You create GATT services and characteristics, just like setting up a little data store that phones can browse. I love it when someone writes code that lets a phone change my color over BLE!”

Max the Microcontroller explained, “The key concept is the client-server model. The ESP32 is the server – it holds the data and waits for connections. The phone or computer is the client – it connects and reads or writes data. Once you understand this pattern, you can build anything from a temperature monitor to a remote-controlled robot.”

“The Wokwi simulator is brilliant for beginners,” Bella the Battery said. “You do not even need real hardware! You can write BLE code, run it in a virtual ESP32, and see the results right in your browser. It is the perfect way to learn without worrying about wires or batteries.”

21.2 Prerequisites

Before working through these examples:

  • Bluetooth Fundamentals and Architecture: Understanding BLE protocol stack, GATT services, characteristics, and the master/slave model
  • Basic Programming Skills: Familiarity with Python (asyncio) or Arduino C++ for embedded development

21.3 Python BLE Scanner

A minimal BLE scanner using the bleak library:

import asyncio
from bleak import BleakScanner

async def scan_devices():
    """Scan for BLE devices"""
    print("Scanning for BLE devices...")
    devices = await BleakScanner.discover(timeout=10.0)

    print(f"\nFound {len(devices)} devices:\n")
    for device in devices:
        print(f"Name: {device.name or 'Unknown'}")
        print(f"Address: {device.address}")
        print(f"RSSI: {device.rssi} dBm\n")

# Run scanner
asyncio.run(scan_devices())

Install: pip install bleak

21.4 Mid-Chapter Check: Python Scanner Concepts

21.4.1 Knowledge Check: bleak Scanner API

21.5 Arduino ESP32 BLE Beacon

Create a BLE GATT server on ESP32 that advertises a temperature sensor service:

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

#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHAR_UUID    "beb5483e-36e1-4688-b7f5-ea07361b26a8"

BLECharacteristic *pCharacteristic;
bool deviceConnected = false;

class ServerCB : public BLEServerCallbacks {
  void onConnect(BLEServer* s)    { deviceConnected = true; }
  void onDisconnect(BLEServer* s) {
    deviceConnected = false;
    BLEDevice::startAdvertising(); // Accept new clients
  }
};

void setup() {
  Serial.begin(115200);
  BLEDevice::init("ESP32-Sensor");
  BLEServer* srv = BLEDevice::createServer();
  srv->setCallbacks(new ServerCB());

  BLEService* svc = srv->createService(SERVICE_UUID);
  pCharacteristic = svc->createCharacteristic(CHAR_UUID,
      BLECharacteristic::PROPERTY_READ |
      BLECharacteristic::PROPERTY_NOTIFY);
  pCharacteristic->addDescriptor(new BLE2902());
  svc->start();

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

void loop() {
  if (deviceConnected) {
    char buf[8];
    dtostrf(20.0 + random(0, 100) / 10.0, 4, 2, buf);
    pCharacteristic->setValue(buf);
    pCharacteristic->notify();
  }
  delay(2000);
}
Interactive Simulator: BLE Advertiser (GATT Server)

Try it yourself! See how ESP32 broadcasts BLE advertisements and serves data to connected clients.

What This Simulates: An ESP32 acting as a BLE GATT server, advertising a temperature sensor service and notifying connected clients with readings.

How to Use:

  1. Click Start Simulation
  2. Watch the Serial Monitor show “BLE beacon advertising!”
  3. Observe temperature readings being generated
  4. See connection status when clients connect
  5. Notice GATT service and characteristic UUIDs

Learning Points

Observe:

  • BLEDevice::init(): Initializes BLE stack with device name
  • createService(): Creates GATT service with UUID
  • createCharacteristic(): Defines data attribute (readable, notifiable)
  • BLE2902: Client Characteristic Configuration Descriptor (enables notifications)
  • Advertising: Broadcasts service UUID so clients can discover device
  • notify(): Pushes data to connected clients without polling

GATT (Generic Attribute Profile) Structure:

GATT hierarchy diagram showing BLE device ESP32-Sensor containing a service with UUID 4fafc201, which contains a characteristic with UUID beb5483e that has READ and NOTIFY properties and holds a temperature value (23.5 degrees C). The characteristic includes a CCCD descriptor for enabling/disabling notifications.
Figure 21.1: BLE GATT hierarchy: device, service, characteristic, and descriptor
BLE power consumption timeline comparing advertising mode (low duty cycle, approximately 1 percent active) versus connected mode with notifications (connection interval driven). Shorter connection intervals increase responsiveness but reduce battery life.
Figure 21.2: BLE power consumption timeline comparing advertising mode versus connected mode with notifications

BLE Communication Flow:

1. ESP32 advertises: "ESP32-Sensor service available!"
2. Client (phone app) scans and discovers ESP32
3. Client connects to ESP32 GATT server
4. Client reads service/characteristic info
5. Client enables notifications (writes to CCCD)
6. ESP32 pushes temperature updates every 2 seconds
7. Client receives notifications without polling

UUID Standards:

  • 16-bit UUIDs: Standard Bluetooth SIG services (0x180F = Battery Service)
  • 128-bit UUIDs: Custom services (use UUID generator for unique IDs)
  • Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Real-World Applications:

  • Fitness Trackers: Heart rate monitor broadcasts HR data to phone app
  • Smart Home: Temperature sensors advertise readings to central hub
  • Asset Tracking: BLE beacons broadcast location info (iBeacon, Eddystone)
  • Medical Devices: Blood glucose meters send readings to patient apps
  • Wearables: Smartwatches notify phone of incoming calls/messages

Experiment:

  • Add battery level characteristic (standard UUID 0x2A19)
  • Implement multiple characteristics (temp, humidity, pressure)
  • Add WRITE property for remote control
  • Implement custom advertising data (manufacturer data, tx power)
  • Add security: require pairing before connection

21.6 Interactive Simulator: BLE Scanner (Central/Client)

BLE Scanner Simulator

What This Simulates: ESP32 scanning for nearby BLE devices and reading their advertised data

BLE Scanning Flow:

Step 1: ESP32 starts BLE scan
   - Sets scan parameters (interval, window, active/passive)

Step 2: Receives advertisements from devices
   - Device Name
   - RSSI (signal strength)
   - Service UUIDs
   - Manufacturer data

Step 3: Filters results
   - By name pattern
   - By RSSI threshold
   - By service UUID

Step 4: Optionally connects to device
   - Read characteristics
   - Subscribe to notifications

How to Use:

  1. Click Start Simulation
  2. Watch ESP32 discover nearby BLE devices
  3. See device names, addresses, and RSSI values
  4. Observe scan results filtering
  5. Monitor connection attempts to specific devices

Learning Points

What You’ll Observe:

  1. Active vs Passive Scanning - Active sends scan requests for more data
  2. RSSI Measurement - Signal strength can be a rough distance proxy
  3. Advertisement Data - Devices broadcast identity without connection
  4. Scan Parameters - Interval and window affect power vs discovery speed
  5. Service Discovery - Finding devices by UUID without connection

BLE Scan Parameters:

Scan Interval: 100 ms (example; time between scan windows)
Scan Window:   99 ms (example; active scanning time)
Scan Type:     Active (request additional data)

Power Consumption (varies by platform):
- Passive Scan: lower
- Active Scan:  higher
- Continuous:   High power, fast discovery
- Periodic:     Low power, slower discovery

RSSI (Received Signal Strength Indicator):

RSSI Value (dBm) Distance Quality
-30 to -50 < 1m Excellent
-50 to -70 1-5m Good
-70 to -85 5-15m Fair
-85 to -100 > 15m Poor

Note: RSSI-to-distance estimates are highly environment-dependent (multipath, antenna orientation, body attenuation). Treat this as a rough heuristic unless you calibrate for your setting.

Distance Formula (simplified log-distance model): Distance = 10 ^ ((TxPower - RSSI) / (10 * N)) - N = Path loss exponent (2.0 in free space, 2.7-4.3 indoors)

The log-distance path loss model estimates distance from RSSI using measured signal strength. The basic formula:

\[ \begin{aligned} \text{RSSI}(d) &= P_{\text{TX}} - 10n \log_{10}\left(\frac{d}{d_0}\right) + X_\sigma \\[0.4em] \text{where:} \quad &P_{\text{TX}} = \text{transmit power at reference distance } d_0 \text{ (typically 1 m)} \\ &n = \text{path loss exponent (2.0 free space, 2.7–4.3 indoors)} \\ &X_\sigma = \text{Gaussian noise (0 dB typical, ±4–8 dB in practice)} \end{aligned} \]

Solving for distance: \[ d = d_0 \times 10^{\frac{P_{\text{TX}} - \text{RSSI}}{10n}} \]

Example 1: Office environment (n = 3.0) \[ \begin{aligned} P_{\text{TX}} &= -59\text{ dBm (measured at 1 m)},\quad \text{RSSI} = -75\text{ dBm} \\[0.4em] d &= 1 \times 10^{\frac{-59 - (-75)}{10 \times 3.0}} = 10^{\frac{16}{30}} = 10^{0.533} \approx 3.4\text{ m} \end{aligned} \]

Example 2: Warehouse with metal shelving (n = 4.0) \[ \begin{aligned} P_{\text{TX}} &= -59\text{ dBm},\quad \text{RSSI} = -85\text{ dBm} \\[0.4em] d &= 1 \times 10^{\frac{-59 - (-85)}{10 \times 4.0}} = 10^{\frac{26}{40}} = 10^{0.65} \approx 4.5\text{ m} \end{aligned} \]

RSSI smoothing with exponential moving average (reduce noise): \[ \text{RSSI}_{\text{filtered}}[k] = \alpha \times \text{RSSI}_{\text{raw}}[k] + (1 - \alpha) \times \text{RSSI}_{\text{filtered}}[k-1] \] where \(\alpha = 0.1\) to \(0.3\) provides good noise rejection with acceptable lag.

Example 3: RSSI smoothing \[ \begin{aligned} \text{Raw samples:} \quad &[-72, -68, -75, -70]\text{ dBm} \\ \alpha &= 0.2 \\[0.4em] \text{RSSI}_{\text{filtered}}[0] &= -72 \\ \text{RSSI}_{\text{filtered}}[1] &= 0.2 \times (-68) + 0.8 \times (-72) = -71.2 \\ \text{RSSI}_{\text{filtered}}[2] &= 0.2 \times (-75) + 0.8 \times (-71.2) = -71.96 \\ \text{RSSI}_{\text{filtered}}[3] &= 0.2 \times (-70) + 0.8 \times (-71.96) = -71.57 \end{aligned} \]

Key insight: RSSI-to-distance estimation is highly environment-dependent. Calibrate \(P_{\text{TX}}\) and \(n\) for your specific deployment. Without calibration, expect ±50% distance error due to multipath, antenna orientation, and body attenuation. Use RSSI for relative proximity (near/medium/far zones) rather than precise distance measurements.

Advertisement Packet Structure:

BLE advertisement packet structure diagram showing the packet fields: preamble (1 byte), access address (4 bytes), PDU header (2 bytes), payload (6 to 37 bytes), and CRC (3 bytes). The payload expands to show common data types such as flags, device name, service UUIDs, TX power, and manufacturer data.
Figure 21.3: BLE advertisement packet structure with payload fields

Real-World Applications:

  1. Asset Tracking - Scan for BLE beacons to locate equipment
  2. Healthcare - Discover medical sensors (glucose, heart rate)
  3. Smart Home - Find nearby BLE light bulbs, sensors
  4. Retail - Detect iBeacons for proximity marketing
  5. Industrial - Monitor factory sensors via BLE mesh

Experiments to Try:

  1. Filter by RSSI - Only show devices within 2 meters (> -60 dBm)
  2. Service UUID Filter - Find devices advertising specific services
  3. Connect and Read - Connect to found device and read characteristics
  4. Scan Duration - Compare continuous vs periodic scanning power
  5. Duplicate Filtering - Remove duplicate advertisements

Central vs Peripheral:

Peripheral (Server):         Central (Scanner):
- Advertises presence       - Scans for devices
- Waits for connections     - Initiates connections
- Provides data/services    - Consumes data
- Lower power (sleeps)      - Higher power (active)
- Example: Sensor, beacon   - Example: Phone, gateway

Connection:
Peripheral <-advertising-> Central
Peripheral <-connection<- Central (initiates)
Peripheral ->data-> Central (after connection)

21.7 Video Tutorial

21.8 BLE Development Stack Overview

Understanding the complete development stack helps you choose the right tools and libraries:

BLE development stack diagram showing layers from hardware (ESP32/nRF52) through firmware SDK (ESP-IDF/Zephyr), protocol stack (HCI, L2CAP, GATT), to application layer (Python bleak, mobile apps). Each layer shows common tools and libraries.
Figure 21.4: BLE development stack showing the relationship between application code, protocol layers, firmware SDKs, and hardware platforms.

21.8.1 Knowledge Check: GATT Architecture

21.8.2 Knowledge Check: BLE Notifications

Common Mistake: Forgetting to Enable Notifications Before Reading

The Error:

// WRONG: Attempting to receive notifications without enabling them
void setup() {
  pCharacteristic = pService->createCharacteristic(
    CHAR_UUID,
    BLECharacteristic::PROPERTY_NOTIFY
  );
  // Missing: addDescriptor(new BLE2902())
}

Why It Fails: BLE notifications require the Client Characteristic Configuration Descriptor (CCCD, UUID 0x2902) to be present. Without it, the central device cannot subscribe to notifications, and notify() calls silently fail or cause connection drops.

The Fix:

// CORRECT: Always add CCCD for notifiable characteristics
void setup() {
  pCharacteristic = pService->createCharacteristic(
    CHAR_UUID,
    BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
  );
  pCharacteristic->addDescriptor(new BLE2902());  // REQUIRED for notifications
  pService->start();
}

Client-Side Requirement: The central must write value 0x01 (enable notifications) to the CCCD before receiving notifications. This is handled automatically by iOS/Android BLE APIs (CBPeripheral, BluetoothGatt), but must be explicit when using desktop BLE libraries like bleak or noble.

Debugging Tip: If notifications never arrive, check Wireshark captures for the CCCD write operation (write to handle 0x2902 with value 0x0001). Missing this write means the peripheral’s notify() calls have no subscribers.

21.9 Summary

This chapter provided practical BLE code examples:

  • Python Scanner: Using bleak library to discover nearby BLE devices with RSSI readings
  • ESP32 GATT Server: Creating a BLE peripheral that advertises services and notifies connected clients
  • Interactive Simulators: Wokwi-based ESP32 simulations for hands-on experimentation
  • GATT Structure: Understanding services, characteristics, descriptors, and properties
  • Development Stack: Overview of tools from hardware to application layer

Common Pitfalls

NimBLE functions like ble_gattc_notify_custom() are not thread-safe and must be called from the NimBLE host task context. Calling them from a FreeRTOS timer callback or application task without proper synchronization causes race conditions and assertion failures. Use ble_npl_callout (NimBLE timer) or post work to the NimBLE event queue via ble_hs_sched_sync() for operations initiated outside the BLE task.

Copy-pasting code examples with hardcoded connection intervals (e.g., min=0x0010 / 20ms, max=0x0020 / 40ms) without understanding the impact is a common mistake. A 20ms interval wakes the radio 50 times per second, draining a 230 mAh coin cell in ~4 days instead of the expected 2 years. Set connection parameters based on application data rate: once-per-minute sensor → 1 s interval; interactive control → 50–100 ms.

BLE connections drop unexpectedly due to interference, device sleep, or range. bleak raises BleakError or asyncio.TimeoutError on failed operations and DisconnectedError on connection loss. Python code without proper try/except and reconnection logic crashes or hangs on connection loss. Wrap all bleak operations in try/except with exponential backoff reconnection: wait 1 s, 2 s, 4 s before each retry.

Python bleak is async-only; calling bleak functions with asyncio.run() inside a Tkinter or Flask event loop causes deadlocks. JavaScript BLE (Web Bluetooth API) is also async. Use proper async/await patterns with a dedicated asyncio event loop in a separate thread for GUI applications, or use a BLE library designed for your specific framework (e.g., PyBluez for synchronous Python, not bleak).

21.10 What’s Next

Topic Link Why Read It
Python BLE Implementations bt-impl-python.html Production-ready Python code with device filtering, GATT exploration, and beacon management
BLE Security and Pairing bt-security-pairing.html Add authentication and encryption to protect BLE connections
BLE Performance Optimization bt-performance.html Tune connection intervals, MTU size, and scan parameters for throughput and battery life
BLE Mesh Networking bt-mesh.html Extend BLE beyond point-to-point to multi-node mesh topologies
BLE Testing and Debugging bt-testing-debugging.html Use nRF Sniffer, Wireshark, and nRF Connect to diagnose BLE issues
Bluetooth Fundamentals bluetooth-fundamentals-and-architecture.html Revisit the protocol stack and GATT model underpinning all code in this chapter