21 BLE Code Examples and Simulators
- 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
bleakLibrary: 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.
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);
}
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:
- Open the project in Wokwi.
- Start the simulation and watch the Serial Monitor for advertising status.
- Observe generated temperature values.
- Compare the sketch’s service UUID, characteristic UUID, and notification logic with the ESP32 code above.
- When testing on hardware, connect with a phone BLE tool or Python
bleakclient and verify notification delivery.
21.6 Interactive Simulator: BLE Scanner (Central/Client)
What This Simulates: ESP32 scanning for nearby BLE devices and reading their advertised data
BLE Scanning Flow:
- The central starts a BLE scan and sets the scan interval, scan window, and active/passive mode.
- It receives advertisements containing fields such as device name, RSSI, service UUIDs, and manufacturer data.
- It filters results by name, RSSI threshold, service UUID, or manufacturer payload.
- 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.
- Start the simulation.
- Watch the ESP32 discover nearby BLE devices.
- Compare the reported device names, addresses, RSSI values, and advertised services.
- Observe scan-result filtering.
- Review any connection attempts to selected devices.
21.7 Video Tutorial
21.8 BLE Development Stack Overview
Understanding the complete development stack helps you choose the right tools and libraries:
21.8.1 Knowledge Check: GATT Architecture
21.8.2 Knowledge Check: BLE Notifications
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:
- Python BLE Implementations: bt-impl-python.html for production-ready Python code with device filtering, GATT exploration, and beacon management.
- BLE Implementation Labs: bt-impl-labs.html for hands-on lab workflows that move from code examples to tested builds.
- BLE Security and Pairing: bt-security-pairing-methods.html for authentication and encryption choices.
- BLE Encryption Keys: bt-security-encryption-keys.html for key material, bonding, and storage behavior.
- BLE Mesh Networking: bluetooth-mesh-advanced.html for multi-node Bluetooth topologies.
- Bluetooth Fundamentals: bluetooth-fundamentals-and-architecture.html if you need to revisit the protocol stack and GATT model.