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
  • Peripheral / GATT Server: The embedded device advertises services and exposes characteristics that a central can read, write, or subscribe to.
  • Central / GATT Client: A phone, laptop, or gateway scans, connects, discovers services, and exchanges data with the peripheral.
  • Python bleak Library: Cross-platform async BLE library for scanning, connecting, reading, writing, and subscribing to notifications.
  • ESP32 Arduino BLE: Embedded library pattern that initializes the BLE stack, creates a server, registers services and characteristics, and starts advertising.
  • CCCD (BLE2902): Descriptor that lets a central subscribe to notifications from a notifiable characteristic.
  • Wokwi BLE Simulation: Browser-based ESP32 simulation useful for trying BLE sketches without hardware.
  • BLE Sniffer: Hardware-assisted packet capture used when you need to inspect pairing, connection parameter negotiation, or 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.

BLE code is easier to reason about when you separate the two roles:

  • Central code scans first, chooses a device, connects, discovers services, and then reads, writes, or subscribes to characteristics.
  • Peripheral code initializes the radio stack, creates a GATT database, advertises enough identity for centrals to find it, and responds to callbacks.
  • Simulators are useful for learning the peripheral flow, but always verify timing, range, and connection recovery on real hardware before production.

In this chapter, the Python examples show the central/client side and the ESP32 examples show the peripheral/server side.

21.2 Prerequisites

Before working through these examples:

  • Bluetooth Fundamentals and Architecture: Understanding BLE protocol stack, GATT services, characteristics, and the central/peripheral 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, return_adv=True)

    print(f"\nFound {len(devices)} devices:\n")
    for address, (device, adv) in devices.items():
        print(f"Name: {device.name or 'Unknown'}")
        print(f"Address: {address}")
        print(f"RSSI: {adv.rssi} dBm")
        print(f"Service UUIDs: {adv.service_uuids}\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.

Open the simulator directly: BLE advertiser Wokwi project.

How to Use:

  1. Open the project in Wokwi.
  2. Start the simulation and watch the Serial Monitor for advertising status.
  3. Observe generated temperature values.
  4. Compare the sketch’s service UUID, characteristic UUID, and notification logic with the ESP32 code above.
  5. When testing on hardware, connect with a phone BLE tool or Python bleak client and verify notification delivery.
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. The ESP32 advertises that the ESP32-Sensor service is available.
  2. A central device scans and discovers the advertisement.
  3. The central connects to the ESP32 GATT server.
  4. The central discovers the service and characteristic.
  5. The central enables notifications by writing to the CCCD.
  6. The ESP32 updates the characteristic and sends notifications.
  7. The central receives updates 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:

  1. The central starts a BLE scan and sets the scan interval, scan window, and active/passive mode.
  2. It receives advertisements containing fields such as device name, RSSI, service UUIDs, and manufacturer data.
  3. It filters results by name, RSSI threshold, service UUID, or manufacturer payload.
  4. It optionally connects to a selected peripheral to read characteristics or subscribe to notifications.

How to Use:

Open the simulator directly: BLE scanner Wokwi project.

  1. Start the simulation.
  2. Watch the ESP32 discover nearby BLE devices.
  3. Compare the reported device names, addresses, RSSI values, and advertised services.
  4. Observe scan-result filtering.
  5. Review any connection attempts to selected 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: example 100 ms; the time between scan windows.
  • Scan window: example 99 ms; the time spent actively listening during each interval.
  • Scan type: active scanning requests extra advertisement data; passive scanning only listens.
  • Power tradeoff: continuous active scanning discovers devices quickly but burns power; periodic passive scanning is slower but better for batteries.

RSSI (Received Signal Strength Indicator):

  • -30 to -50 dBm: very close range, often less than 1 m in open space.
  • -50 to -70 dBm: nearby device, often usable across a room.
  • -70 to -85 dBm: weaker link, more sensitive to orientation, walls, and interference.
  • -85 to -100 dBm: marginal link; expect missed advertisements or connection instability.

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.

Simplified distance estimate: distance = 10 ^ ((TxPower - RSSI) / (10 * N)), where N is the path-loss exponent. Use about 2.0 in free space and 2.7 to 4.3 indoors.

  • Office example: with transmit power calibrated at -59 dBm at 1 m, measured RSSI of -75 dBm, and indoor path loss N = 3.0, the estimate is about 3.4 m.
  • Warehouse example: with the same calibrated transmit power, RSSI of -85 dBm, and path loss N = 4.0, the estimate is about 4.5 m.
  • Smoothing: use an exponential moving average such as filtered = 0.2 * raw + 0.8 * previous to reduce jumpy readings.
  • Engineering rule: RSSI is good for near/medium/far zones. It is not reliable enough for precise distance without site calibration.

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: advertises presence, waits for connections, provides data and services, and can sleep aggressively between radio events.
  • Central/client: scans for devices, initiates connections, consumes data, and usually spends more energy during discovery.
  • Connection sequence: the peripheral advertises, the central initiates the connection, and application data flows through GATT after discovery.

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
  • Simulator Practice: Direct Wokwi projects for hands-on experimentation in a separate simulator workspace
  • 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 operations can raise BleakError, asyncio.TimeoutError, or disconnect callbacks depending on where the failure occurs. Python code without proper exception handling and reconnection logic crashes or hangs on connection loss. Wrap BLE operations in try/except, register a disconnected callback for long sessions, and use exponential backoff before retrying.

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

Prioritize these follow-up chapters based on what you are building next: