22 BLE Python Implementations
Key Concepts
- bleak (Bluetooth Low Energy platform Agnostic Klient): Python async BLE library supporting Windows (WinRT), macOS (CoreBluetooth), and Linux (BlueZ) backends
- BleakClient: bleak class representing a connection to a BLE peripheral; provides methods for service discovery, read, write, start_notify, stop_notify
- BleakScanner: bleak class for BLE device discovery; supports filtering by service UUID, device name, and RSSI threshold
- asyncio.run(): Python coroutine runner; required for bleak operations which are all async; use asyncio.get_event_loop() for integration with existing async frameworks
- UUID String Format: bleak accepts both 16-bit UUIDs as “0000xxxx-0000-1000-8000-00805f9b34fb” (128-bit expanded form) and short “xxxx” strings; use full 128-bit format for custom services
- characteristic.properties: bleak property set of enabled operations: {‘read’, ‘write’, ‘notify’, ‘indicate’, ‘write-without-response’} — check before attempting operations
- client.start_notify(uuid, callback): Registers a Python callback function called when a BLE notification arrives; callback receives (sender_handle, bytearray_data)
- GATT Error Codes in bleak: BleakError wraps ATT error codes; common causes: device not paired (0x05), wrong UUID (0x01), characteristic not found (service discovery needed)
Minimum Viable Understanding
BLE development in Python centers on the bleak library, which provides cross-platform async APIs for scanning, connecting, and interacting with BLE devices. Production BLE applications need RSSI filtering for reliable device discovery, GATT service exploration for data access, and exponential smoothing for stable proximity detection – zone-based classification (immediate/near/far) is far more reliable than precise distance calculations due to inherent RSSI variability.
22.1 Learning Objectives
By the end of this chapter, you will be able to:
- Implement Production BLE Scanners: Configure device filtering by RSSI threshold and name patterns using the bleak library
- Analyze GATT Services: Connect to BLE devices and enumerate services and characteristics to assess device capabilities
- Distinguish Beacon Protocols: Compare iBeacon and Eddystone advertisement packet structures and justify protocol selection for a given use case
- Design Proximity Detection Systems: Apply RSSI smoothing algorithms and construct zone-based presence detection with exponential moving averages
- Develop Async BLE Applications: Construct event-driven Python programs using asyncio and diagnose disconnection handling for production reliability
For Beginners: Python BLE Development
What you’ll learn: Production-ready Python implementations for common BLE tasks using the bleak library.
Prerequisites:
- Basic Python with asyncio understanding
- BLE concepts from Bluetooth Fundamentals
- Code examples from BLE Code Examples
Why Python for BLE? Python’s bleak library provides cross-platform BLE support (Windows, macOS, Linux) with clean async APIs, making it ideal for gateways, data collection, and prototyping.
Sensor Squad: Python Meets Bluetooth!
“Python makes BLE development so easy!” Sammy the Sensor said. “With just a few lines of code using the bleak library, you can scan for devices, connect to sensors, and read data. It is like having a universal remote for every Bluetooth device nearby!”
“My favorite part is RSSI filtering,” Lila the LED said. “RSSI stands for Received Signal Strength Indicator – basically how loud a device’s signal is. A strong signal means the device is close, and a weak signal means it is far away. Python code can filter devices by signal strength so you only see nearby ones!”
Max the Microcontroller added, “The async programming with Python’s asyncio makes everything smooth. Instead of your program freezing while it waits for a BLE scan to complete, it can do other things at the same time. That is how production BLE applications work – always responsive, never stuck waiting.”
“Zone-based detection is really clever,” Bella the Battery shared. “Instead of trying to calculate exact distances from signal strength, which is unreliable, smart programs classify devices into zones: immediate, near, and far. It is much more practical and uses less processing power, which means less energy wasted on complicated math!”
22.2 Prerequisites
Before working through these implementations:
- BLE Code Examples and Simulators: Basic Python scanner and GATT concepts
- Python Environment: Python 3.7+ with
pip install bleak
22.3 BLE Scanner with Device Filtering
A production scanner with RSSI filtering and statistics:
Example Output:
Scanning for 15.0s (RSSI > -70 dBm)...
Found: ESP32-Sensor (A4:CF:12:34:56:78)
RSSI: -45 dBm | Distance: ~1.78m
Found: ESP32-Beacon (B8:27:EB:12:34:56)
RSSI: -62 dBm | Distance: ~6.31m
Scan complete: 2 devices found
ESP32-Sensor (A4:CF:12:34:56:78)
Samples: 8
RSSI Range: -48 to -42 dBm
Mean: -45.3 dBm (+/-1.89)
The implementation filters devices by minimum RSSI threshold, collects multiple samples per device, and calculates statistics for more reliable readings.
22.4 BLE GATT Server Explorer
Connect to a BLE device and enumerate its services and characteristics:
Example Output:
{
"device_name": "HR-Monitor-001",
"services": [
{
"uuid": "0000180d-0000-1000-8000-00805f9b34fb",
"characteristics": [
{
"uuid": "00002a37-0000-1000-8000-00805f9b34fb",
"properties": ["READ", "NOTIFY"],
"value": "0052"
}
]
},
{
"uuid": "0000180f-0000-1000-8000-00805f9b34fb",
"characteristics": [
{
"uuid": "00002a19-0000-1000-8000-00805f9b34fb",
"properties": ["READ", "NOTIFY"],
"value": "55"
}
]
}
]
}Standard service UUIDs: - 0x180D - Heart Rate Service - 0x180F - Battery Service - 0x181A - Environmental Sensing - 0x1816 - Cycling Speed and Cadence
22.5 BLE Beacon Manager
Parse and manage iBeacon and Eddystone beacon advertisements:
Example Output:
{
"total_beacons": 3,
"by_type": {
"iBeacon": 2,
"Eddystone-URL": 1
},
"beacons": [
{
"type": "iBeacon",
"tx_power": -59,
"uuid": "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
"major": 1,
"minor": 101
},
{
"type": "iBeacon",
"tx_power": -59,
"uuid": "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0",
"major": 1,
"minor": 201
},
{
"type": "Eddystone-URL",
"tx_power": -59,
"url": "https://mystore.com/offer"
}
]
}Beacon Protocol Differences:
| Feature | iBeacon | Eddystone |
|---|---|---|
| Developer | Apple | |
| Frame Types | Single | UID, URL, TLM, EID |
| UUID Format | 128-bit + Major/Minor | 10-byte namespace + 6-byte instance |
| URL Support | No | Yes (Eddystone-URL) |
| Telemetry | No | Yes (TLM frame) |
22.6 BLE Proximity Detector
Zone-based proximity detection with RSSI smoothing:
Example Output:
Tracking device approaching...
RSSI: -75 dBm -> Smoothed: -75.0 dBm -> Distance: 7.08m -> Zone: far
FAR proximity: A4:CF:12:34:56:78 at 7.08m
RSSI: -72 dBm -> Smoothed: -73.5 dBm -> Distance: 5.62m -> Zone: far
FAR proximity: A4:CF:12:34:56:78 at 5.62m
RSSI: -68 dBm -> Smoothed: -71.7 dBm -> Distance: 4.47m -> Zone: far
FAR proximity: A4:CF:12:34:56:78 at 4.47m
RSSI: -65 dBm -> Smoothed: -70.0 dBm -> Distance: 3.55m -> Zone: far
FAR proximity: A4:CF:12:34:56:78 at 3.55m
RSSI: -62 dBm -> Smoothed: -68.4 dBm -> Distance: 2.82m -> Zone: near
NEAR proximity: A4:CF:12:34:56:78 at 2.82m
RSSI: -58 dBm -> Smoothed: -66.0 dBm -> Distance: 2.00m -> Zone: near
NEAR proximity: A4:CF:12:34:56:78 at 2.0m
RSSI: -55 dBm -> Smoothed: -63.6 dBm -> Distance: 1.41m -> Zone: near
NEAR proximity: A4:CF:12:34:56:78 at 1.41m
RSSI: -52 dBm -> Smoothed: -61.0 dBm -> Distance: 1.00m -> Zone: near
NEAR proximity: A4:CF:12:34:56:78 at 1.0m
RSSI: -48 dBm -> Smoothed: -58.2 dBm -> Distance: 0.71m -> Zone: near
NEAR proximity: A4:CF:12:34:56:78 at 0.71m
RSSI: -45 dBm -> Smoothed: -55.2 dBm -> Distance: 0.45m -> Zone: immediate
IMMEDIATE proximity: A4:CF:12:34:56:78 at 0.45m
Zone Thresholds:
| Zone | RSSI Range | Typical Distance |
|---|---|---|
| Immediate | > -55 dBm | < 0.5m |
| Near | -55 to -70 dBm | 0.5 - 3m |
| Far | < -70 dBm | > 3m |
RSSI Smoothing Algorithm:
The exponential moving average (EMA) filter reduces RSSI noise:
smoothed_rssi = alpha * new_rssi + (1 - alpha) * prev_smoothed
Where alpha = 0.3 provides good balance between responsiveness and stability
Putting Numbers to It
The EMA filter’s effective window length and response time are:
\[N_{effective} = \frac{2}{\alpha} - 1 \quad \text{and} \quad t_{response} = \frac{-\ln(0.05)}{\alpha \times f_{sample}}\]
where \(\alpha\) is the smoothing factor and \(f_{sample}\) is the sampling rate (Hz).
Example: RSSI sampling at 1 Hz (once per second) with \(\alpha = 0.3\): - Effective window: \(N_{effective} = \frac{2}{0.3} - 1 = 5.67 \approx 6\) samples - Time to reach 95% of new value: \(t_{response} = \frac{-\ln(0.05)}{0.3 \times 1} = \frac{3.0}{0.3} = 10\) seconds
Compare with \(\alpha = 0.1\) (more smoothing): - Effective window: \(\frac{2}{0.1} - 1 = 19\) samples - Response time: \(\frac{3.0}{0.1} = 30\) seconds
Lower \(\alpha\) smooths more aggressively but reacts slower to real movement. For proximity detection, \(\alpha = 0.2\text{-}0.3\) balances noise reduction with reasonable tracking speed.
RSSI Limitations
RSSI-based distance estimation has inherent limitations:
- Multipath fading: Reflections cause +/-6 dBm variance
- Body shadowing: Human body attenuates 5-15 dBm
- Antenna orientation: Different orientations vary +/-10 dBm
- Environmental factors: Walls, furniture, humidity affect signal
Recommendation: Use zone-based classification (immediate/near/far) rather than precise distance calculations. For sub-meter accuracy, consider UWB technology instead.
22.6.1 Knowledge Check: EMA Smoothing Parameters
22.7 BLE Power Optimization Decision Flow
When building battery-powered BLE devices, power optimization is critical:
22.8 Visual Reference Gallery
Visual: BLE Module Architecture
BLE modules integrate radio, processor, and antenna for easy integration into IoT device designs.
Visual: BLE GATT Profile Implementation
GATT implementation requires defining services and characteristics with appropriate properties for your application’s data model.
Visual: BLE Connection Flow
Understanding the connection flow helps optimize connection latency and power consumption in BLE applications.
Visual: BLE Stack Architecture
The BLE stack provides standardized interfaces for application developers to build interoperable devices.
Visual: Bluetooth Serial Port Profile
SPP enables legacy serial applications to communicate wirelessly, useful for debugging and configuration interfaces.
22.8.1 Knowledge Check: BLE Scanning and Filtering
22.8.2 Knowledge Check: GATT Service Exploration
22.8.3 Knowledge Check: BLE Proximity Detection
22.9 Real-World Deployment: BLE Proximity System for Retail Analytics
A UK-based retail chain deployed BLE proximity detection across 45 stores to measure customer dwell time and foot traffic patterns. The system uses Python gateways running on Raspberry Pi 4 units positioned at store entrances and key departments.
System Specifications:
| Parameter | Value | Rationale |
|---|---|---|
| Scan interval | 2 seconds | Balances detection speed vs CPU load |
| RSSI threshold | -75 dBm | Filters devices beyond 5m radius |
| EMA alpha | 0.2 | Prioritizes stability over responsiveness for dwell time |
| Zone boundaries | -55 / -70 dBm | Immediate (<1m) / Near (1-4m) / Far (>4m) |
| Min samples | 3 | Requires 3 consecutive readings before zone assignment |
| Gateway count per store | 4-6 | Covers ~400 sqm average store footprint |
Why EMA Alpha = 0.2 (Not 0.3)?
The standard alpha of 0.3 works well for single-device tracking, but in a crowded retail environment with 50-200 simultaneous BLE advertisers, lower alpha reduces false zone transitions caused by body shadowing. A customer stepping behind a display rack causes a sudden 10-15 dBm drop. With alpha 0.3, the smoothed RSSI reacts in 2 readings (4 seconds), potentially triggering a false “far” classification. With alpha 0.2, it takes 4 readings (8 seconds) – long enough for the customer to move again, preventing a spurious zone change.
Battery Impact on Beacons:
The stores use Estimote LTE beacons (1000 mAh battery) advertising at 1 Hz:
Beacon power budget:
- Advertising current: 8 mA per event
- Event duration: 3 ms (including ramp-up)
- Daily energy: 8 mA x 0.003s x 86,400 events = 2.07 mAh/day
- Battery life: 1000 mAh / 2.07 mAh = 483 days = 1.3 years
At 10 Hz (for faster detection):
- Daily energy: 20.7 mAh/day
- Battery life: 48 days (unacceptable for retail)
This is why the system uses 1 Hz advertising with gateway-side EMA smoothing rather than faster beacon rates – a 10x advertising rate would reduce battery life from 16 months to 7 weeks, requiring quarterly battery changes across thousands of beacons.
Concept Relationships:
| Concept | Relates To | Why It Matters |
|---|---|---|
| RSSI Filtering | Zone-Based Detection | Threshold filtering reduces noise from distant devices, enabling proximity zones |
| EMA Smoothing | Proximity Detection | Exponential moving average stabilizes noisy RSSI readings for reliable distance estimates |
| GATT Explorer | Service Discovery | Enumerating UUIDs maps device capabilities before data access |
| Beacon Protocols | Indoor Positioning | iBeacon/Eddystone standards enable cross-platform location services |
| bleak Library | Cross-Platform Support | Async Python API works on Windows/Mac/Linux with identical code |
22.10 See Also
- BLE Hands-On Labs - Complete project implementations with heart rate monitors and positioning
- BLE Code Examples - Basic Python scanner patterns and GATT concepts
- Bluetooth Security - Understanding secure device pairing
- Bluetooth Applications - Real-world BLE deployment case studies
22.11 Summary
This chapter covered production Python BLE implementations:
- Scanner with Filtering: RSSI thresholds and name pattern matching for targeted device discovery
- GATT Explorer: Enumerating services and characteristics on connected devices
- Beacon Management: Parsing iBeacon and Eddystone advertisement formats
- Proximity Detection: Zone-based presence detection with exponential smoothing
- Power Optimization: Decision framework for connection intervals and advertising parameters
Worked Example: RSSI-to-Distance Calculation and Zone Classification
Scenario: A Python BLE proximity system measures RSSI from iBeacons to determine customer location in a retail store. Calculate distance and classify into zones.
Given beacon parameters:
- TX Power at 1 meter: -59 dBm (calibrated value from manufacturer)
- Path loss exponent (n): 2.5 (typical retail environment with shelves)
- RSSI measurements (5-sample moving average): [-68, -72, -65, -70, -66] dBm
Step 1: Calculate smoothed RSSI using exponential moving average
alpha = 0.3 # EMA smoothing factor
rssi_samples = [-68, -72, -65, -70, -66]
smoothed = rssi_samples[0] # Initialize with first sample
for rssi in rssi_samples[1:]:
smoothed = alpha * rssi + (1 - alpha) * smoothed
print(f"RSSI {rssi} → Smoothed {smoothed:.1f}")Output:
RSSI -72 → Smoothed -69.2
RSSI -65 → Smoothed -67.9
RSSI -70 → Smoothed -68.6
RSSI -66 → Smoothed -67.8
Smoothed RSSI: -67.8 dBm
Step 2: Calculate distance using log-distance path loss model
Formula: d = 10 ^ ((TxPower - RSSI) / (10 * n))
Where: - TxPower = -59 dBm (calibrated at 1 meter) - RSSI = -67.8 dBm (smoothed) - n = 2.5 (path loss exponent)
import math
tx_power = -59
rssi = -67.8
n = 2.5
distance = 10 ** ((tx_power - rssi) / (10 * n))
print(f"Distance: {distance:.2f} meters")Calculation:
- (−59 − (−67.8)) / (10 × 2.5) = 8.8 / 25 = 0.352
- 10^0.352 = 2.25 meters
Step 3: Classify into proximity zones
def classify_zone(rssi):
if rssi > -55:
return "immediate", "< 0.5m"
elif rssi > -70:
return "near", "0.5 - 3m"
else:
return "far", "> 3m"
zone, range_desc = classify_zone(-67.8)
print(f"Zone: {zone} ({range_desc})")Result: Zone = “near” (0.5 - 3m), calculated distance = 2.25m ✓
Step 4: Account for uncertainty
RSSI variance in retail environments (from field study, 100 beacons): - Standard deviation: ±6 dBm - Distance error at 2m: ±0.8 meters (40% error)
Conclusion: The beacon is 2.25 ± 0.8 meters away, classified as “near” zone. For applications requiring sub-meter accuracy (asset tracking), use UWB instead of BLE RSSI.
Decision Framework: Choosing BLE Python Library (bleak vs alternatives)
| Feature | bleak | bluepy | pybluez | pygatt |
|---|---|---|---|---|
| Platform support | Win/Mac/Linux | Linux only | Linux (Classic BT) | Linux/Windows |
| Async support | Native asyncio ✓ | Blocking | Blocking | Blocking |
| BLE GATT | Full support | Full | No (Classic only) | Full |
| Classic BT | No | No | Yes (SPP, A2DP) | No |
| Maintained | Active (2024) | Abandoned (2018) | Minimal updates | Minimal |
| Learning curve | Medium (async) | Easy (sync) | Easy | Easy |
| Best for | Production apps | Legacy code | Classic BT projects | Simple scripts |
Decision scenarios:
| Your Requirement | Recommended Library | Why |
|---|---|---|
| Cross-platform BLE scanner | bleak | Only library with full Win/Mac/Linux support |
| Async event-driven app | bleak | Native asyncio integration |
| Quick prototyping (Linux) | bluepy (if still available) | Simple synchronous API |
| Classic Bluetooth SPP | pybluez | Only Python lib with Classic support |
| Production IoT gateway | bleak | Active maintenance, async for concurrent devices |
Real-world example:
A smart home company migrated from bluepy to bleak in 2022:
Before (bluepy on Linux only):
from bluepy import btle
scanner = btle.Scanner()
devices = scanner.scan(10) # Blocks for 10 seconds
for dev in devices:
print(dev.addr, dev.rssi)Issues:
- Only worked on Linux (80% of users on Windows/Mac couldn’t use it)
- Blocking calls prevented UI responsiveness
- Abandoned library (no BLE 5 features)
After (bleak cross-platform):
import asyncio
from bleak import BleakScanner
async def scan():
devices = await BleakScanner.discover(timeout=10)
for dev in devices:
print(dev.address, dev.rssi)
asyncio.run(scan())Benefits:
- Runs on all platforms (100% user coverage)
- Async allows concurrent scanning + UI updates
- Active development (BLE 5 long-range scanning support added 2023)
Verdict: Use bleak for new projects unless you specifically need Classic Bluetooth (then use pybluez).
Common Mistake: Not Handling BLE Disconnections in Long-Running Scripts
The error: A Python script using bleak connects to a BLE temperature sensor, reads data in a loop, but doesn’t handle disconnections. After 15 minutes, the script crashes when the sensor goes to sleep.
What happens:
import asyncio
from bleak import BleakClient
async def monitor_temperature():
address = "A4:CF:12:34:56:78"
async with BleakClient(address) as client:
while True:
# Read temperature characteristic
temp_bytes = await client.read_gatt_char("0x2A6E")
temp = int.from_bytes(temp_bytes, 'little') / 100.0
print(f"Temperature: {temp}°C")
await asyncio.sleep(60) # Read every minute
asyncio.run(monitor_temperature())Failure scenario:
- Script connects successfully
- Reads temperature for 15 minutes
- Sensor enters low-power mode (connection supervision timeout)
- Line
temp_bytes = await client.read_gatt_char()raisesBleakError: Not connected - Script crashes with unhandled exception
The fix (production-grade with reconnection):
import asyncio
from bleak import BleakClient
from bleak.exc import BleakError
async def monitor_temperature():
address = "A4:CF:12:34:56:78"
while True: # Outer loop for reconnection
try:
async with BleakClient(address, timeout=20) as client:
print(f"Connected to {address}")
while True: # Inner loop for reading
try:
temp_bytes = await client.read_gatt_char("0x2A6E")
temp = int.from_bytes(temp_bytes, 'little') / 100.0
print(f"Temperature: {temp}°C")
await asyncio.sleep(60)
except BleakError as e:
print(f"Read error: {e}, will reconnect")
break # Exit inner loop to trigger reconnect
except BleakError as e:
print(f"Connection failed: {e}, retrying in 5s")
await asyncio.sleep(5)
except KeyboardInterrupt:
print("Stopped by user")
break
asyncio.run(monitor_temperature())What this adds:
- Outer while loop: Retries connection if it fails initially or drops
- Inner try/except: Catches read errors, triggers reconnection
- Timeout parameter: Prevents hanging on slow connections
- KeyboardInterrupt: Allows graceful shutdown with Ctrl+C
- Backoff delay: 5-second wait between reconnect attempts (prevents busy loop)
Production enhancement (exponential backoff):
retry_delay = 5
max_delay = 60
while True:
try:
async with BleakClient(address) as client:
retry_delay = 5 # Reset on successful connect
# ... reading loop ...
except BleakError:
print(f"Retrying in {retry_delay}s")
await asyncio.sleep(retry_delay)
retry_delay = min(retry_delay * 2, max_delay) # Exponential backoffMeasured reliability improvement:
A data logging project ran for 30 days: - Without reconnection logic: 12 crashes (script stopped after first disconnect) - With reconnection: 0 crashes, 99.2% uptime (0.8% was unavoidable sensor reboot time)
Rule of thumb: All production BLE scripts need reconnection logic. BLE is wireless and inherently unreliable—disconnections are normal, not exceptions.
Common Pitfalls
1. Not Awaiting bleak Coroutines
bleak is fully asynchronous; calling client.read_gatt_char(uuid) without await returns a coroutine object, not the data. Comparing a coroutine object to expected values always produces False. Every bleak operation must use await: data = await client.read_gatt_char(uuid). If you see <coroutine object…> in print output, you forgot await.
2. Scanning by Name in Noisy Environments
Using BleakScanner.find_device_by_name(“MySensor”) in an environment with many BLE devices is slow and unreliable — it scans until timeout if the device is temporarily out of range. Use BleakScanner.find_device_by_filter() with a service UUID filter instead: scanner.find_device_by_filter(lambda d, adv: SERVICE_UUID in adv.service_uuids). This is more specific and faster than name-matching.
3. Blocking the asyncio Event Loop in Notification Callbacks
bleak notification callbacks run in the asyncio event loop thread. Calling blocking operations (time.sleep(), file.write() with large payloads, synchronous DB writes) inside callbacks freezes BLE processing and causes missed notifications. Use asyncio.create_task() to schedule data processing, or write to an asyncio.Queue() and process in a separate coroutine.
4. Assuming Consistent Service/Characteristic Handle Order
GATT service discovery order is not guaranteed to be consistent across firmware versions or device resets. Caching the handle integer directly (e.g., handle = 0x000E) and using it in subsequent sessions is fragile. Always use UUID-based access: client.read_gatt_char(“0000xxxx-0000-1000-8000-00805f9b34fb”). Let bleak resolve the handle internally on each connection.
22.12 What’s Next
| Chapter | Focus | Why Read It |
|---|---|---|
| BLE Hands-On Labs | Complete project implementations | Apply bleak scanning and GATT reads in full heart rate monitor and indoor positioning projects |
| BLE Code Examples | Foundational scanner patterns | Consolidate understanding of basic BleakScanner and BleakClient usage before extending to production code |
| Bluetooth Security | Pairing, bonding, and encryption | Evaluate security requirements for the production BLE applications you built in this chapter |
| Bluetooth Applications | Real-world BLE deployment cases | Compare retail, healthcare, and industrial deployments against the proximity detection patterns covered here |
| RFID, NFC and UWB | Short-range identification technologies | Assess when UWB sub-meter accuracy justifies replacing BLE RSSI proximity detection |
| Bluetooth Fundamentals and Architecture | BLE protocol stack and GAP/GATT theory | Diagnose advanced GATT service issues by referencing the underlying protocol architecture |