21 BLE Code Examples and Simulators
- 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
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);
}
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:
- Click Start Simulation
- Watch the Serial Monitor show “BLE beacon advertising!”
- Observe temperature readings being generated
- See connection status when clients connect
- Notice GATT service and characteristic UUIDs
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:
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:
- Click Start Simulation
- Watch ESP32 discover nearby BLE devices
- See device names, addresses, and RSSI values
- Observe scan results filtering
- Monitor connection attempts to specific 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
- 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 |