22  BLE Python Implementations

In 60 Seconds

BLE development in Python uses the bleak library for cross-platform async scanning, connecting, and GATT interaction. Production apps need RSSI filtering, GATT service exploration, and exponential smoothing for proximity – zone-based classification (immediate/near/far) is far more reliable than precise distance calculations due to RSSI variability.

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

What you’ll learn: Production-ready Python implementations for common BLE tasks using the bleak library.

Prerequisites:

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.

“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:

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.

Try It: BLE RSSI Filter Simulator

Adjust the RSSI threshold and observe which simulated BLE devices pass the filter. Devices with RSSI below the threshold are filtered out as too distant.

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

Try It: GATT Service UUID Lookup

Select a standard BLE GATT service to see its UUID, characteristics, and typical use case. This demonstrates the service discovery process that the GATT Explorer performs.

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 Google
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)
Try It: Beacon Advertisement Decoder

Configure a simulated beacon and see how its advertisement packet is structured. Compare iBeacon and Eddystone formats to understand the protocol differences.

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
Try It: RSSI Smoothing and Zone Classification

Experiment with EMA smoothing parameters and see how they affect proximity zone detection. Adjust the alpha value and watch the smoothed RSSI converge, then observe zone classification in real time.

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

Objective: Run an ESP32 as a BLE peripheral that advertises a custom service. In a real setup, you would connect to this device using the Python bleak scanner code above.

Code to Try:

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

#define SERVICE_UUID       "181A0000-0000-1000-8000-00805f9b34fb"
#define TEMP_CHAR_UUID     "2A6E0000-0000-1000-8000-00805f9b34fb"
#define HUMIDITY_CHAR_UUID "2A6F0000-0000-1000-8000-00805f9b34fb"

BLECharacteristic *pTempChar, *pHumChar;
bool deviceConnected = false;

class CB : public BLEServerCallbacks {
  void onConnect(BLEServer* s)    { deviceConnected = true; }
  void onDisconnect(BLEServer* s) {
    deviceConnected = false;
    BLEDevice::startAdvertising();
  }
};

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

  BLEService* svc = srv->createService(SERVICE_UUID);
  pTempChar = svc->createCharacteristic(TEMP_CHAR_UUID,
      BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
  pHumChar  = svc->createCharacteristic(HUMIDITY_CHAR_UUID,
      BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
  svc->start();

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

void loop() {
  float temp = 22.0 + random(-30, 30) / 10.0;
  float hum  = 55.0 + random(-100, 100) / 10.0;
  int16_t  tBLE = (int16_t)(temp * 100);  // 0.01 C units
  uint16_t hBLE = (uint16_t)(hum * 100);
  pTempChar->setValue((uint8_t*)&tBLE, 2);
  pHumChar->setValue((uint8_t*)&hBLE, 2);
  if (deviceConnected) { pTempChar->notify(); pHumChar->notify(); }
  delay(2000);
}

What to Observe:

  1. The ESP32 advertises as “ESP32-EnvSensor” with a custom Environmental Sensing service
  2. Temperature and humidity values are encoded as BLE-standard int16 in 0.01-degree units
  3. When a client connects, notifications push data automatically every 2 seconds
  4. Try changing the device name in BLEDevice::init() and observe how it affects discovery

22.7 BLE Power Optimization Decision Flow

When building battery-powered BLE devices, power optimization is critical:

Flowchart for BLE power optimization decisions: Starting with data rate needs, branches to connection interval selection, then advertising interval for beacon mode, sleep mode configuration, and TX power tuning. Each path shows typical values and power impact.
Figure 22.1: Decision flowchart for BLE power optimization showing trade-offs between connection interval, TX power, and sleep modes.

22.8 Visual Reference Gallery

Modern diagram of BLE module architecture showing radio transceiver, baseband processor, host controller interface, antenna matching, and power management for embedded IoT applications

BLE module hardware components

BLE modules integrate radio, processor, and antenna for easy integration into IoT device designs.

Geometric representation of GATT profile implementation showing service hierarchy, characteristic UUIDs, and read/write/notify properties for custom BLE applications

GATT service and characteristic structure

GATT implementation requires defining services and characteristics with appropriate properties for your application’s data model.

Artistic sequence diagram of BLE connection flow from device advertising through central scanning, connection request, and GATT service discovery for data exchange

BLE connection establishment sequence

Understanding the connection flow helps optimize connection latency and power consumption in BLE applications.

Geometric breakdown of BLE stack layers including PHY, Link Layer, L2CAP, ATT, GATT, and GAP with HCI separating controller and host functions

BLE protocol stack layers

The BLE stack provides standardized interfaces for application developers to build interoperable devices.

Modern diagram of Bluetooth Serial Port Profile showing virtual COM port emulation for wireless UART communication between microcontrollers and host computers

Bluetooth SPP for serial communication

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

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

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.

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:

  1. Script connects successfully
  2. Reads temperature for 15 minutes
  3. Sensor enters low-power mode (connection supervision timeout)
  4. Line temp_bytes = await client.read_gatt_char() raises BleakError: Not connected
  5. 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:

  1. Outer while loop: Retries connection if it fails initially or drops
  2. Inner try/except: Catches read errors, triggers reconnection
  3. Timeout parameter: Prevents hanging on slow connections
  4. KeyboardInterrupt: Allows graceful shutdown with Ctrl+C
  5. 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 backoff

Measured 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

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.

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.

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.

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