1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
|
# Usage Guide
## Why Use bleak-retry-connector?
This package provides robust retry logic and intelligent backoff strategies for establishing BLE connections. Key benefits include:
- **Automatic retry with backoff** - Handles transient connection failures with intelligent retry timing
- **Connection slot management** - Critical for ESPHome Bluetooth proxies that have limited connection slots
- **Service caching** - Speeds up reconnections by caching GATT services
- **Platform-specific optimizations** - Special handling for Linux/BlueZ, macOS, and ESP32 devices
- **Error categorization** - Distinguishes between transient errors, missing devices, and out-of-slots conditions
### Essential for ESPHome Bluetooth Proxies
If you're using ESPHome Bluetooth proxies, this package is **critical** because:
1. **Proper slot management** - ESP32 devices have limited connection slots that must be carefully managed
2. **Handles ESP-specific errors** - Recognizes ESP32 error codes like `ESP_GATT_CONN_CONN_CANCEL` (out of slots)
3. **Appropriate backoff timing** - Uses longer backoff (4 seconds) when slots are exhausted to allow proper cleanup
4. **Prevents slot exhaustion** - Manages connection attempts to avoid overwhelming the proxy
## BleakClientWithServiceCache
`BleakClientWithServiceCache` is a subclass of `BleakClient` that provides service caching capabilities for faster reconnections.
### Basic Usage
```python
from bleak_retry_connector import BleakClientWithServiceCache
from bleak.backends.device import BLEDevice
async def connect_with_cache(device: BLEDevice):
client = BleakClientWithServiceCache(device)
await client.connect()
# Use the client normally
services = client.services
# Clear cache if needed (e.g., after service changes)
await client.clear_cache()
await client.disconnect()
```
### Key Features
- **Automatic service caching**: Services are cached between connections for faster reconnections
- **Cache clearing**: Call `clear_cache()` to force a fresh service discovery
- **Drop-in replacement**: Can be used anywhere `BleakClient` is used
## establish_connection
`establish_connection` is the main function for establishing robust BLE connections with automatic retry logic.
### Function Signature
```python
async def establish_connection(
client_class: type[BleakClient],
device: BLEDevice,
name: str,
disconnected_callback: Callable[[BleakClient], None] | None = None,
max_attempts: int = 4,
cached_services: BleakGATTServiceCollection | None = None,
ble_device_callback: Callable[[], BLEDevice] | None = None,
use_services_cache: bool = True,
**kwargs: Any
) -> BleakClient
```
### Parameters
- **client_class**: The BleakClient class to use (typically `BleakClientWithServiceCache`)
- **device**: The BLE device to connect to
- **name**: A descriptive name for the device (used in logging)
- **disconnected_callback**: Optional callback when device disconnects unexpectedly
- **max_attempts**: Maximum connection attempts before giving up (default: 4)
- **cached_services**: Pre-cached services to use (deprecated, use `use_services_cache`)
- **ble_device_callback**: Callback to get updated device info if it changes
- **use_services_cache**: Whether to use service caching (default: True)
- **kwargs**: Additional arguments passed to the client class constructor
### Return Value
Returns the connected client instance of the specified `client_class`.
### Exceptions
`establish_connection` can raise the following exceptions after exhausting retry attempts:
- **BleakNotFoundError**: Device was not found or disappeared
- Raised when the device cannot be found
- Raised on `asyncio.TimeoutError` after all retries
- Raised when `BleakDeviceNotFoundError` occurs
- Raised when device is missing from the adapter
- **BleakOutOfConnectionSlotsError**: Adapter/proxy has no available connection slots
- Raised when local Bluetooth adapters or ESP32 proxies are out of connection slots
- Common with errors containing "ESP_GATT_CONN_CONN_CANCEL", "connection slot", or "available connection"
- For local adapters: disconnect unused devices or use a different adapter
- For ESP32 proxies: add more proxies or disconnect other devices
- **BleakAbortedError**: Connection was aborted due to interference or range issues
- Raised for transient connection failures that suggest environmental issues
- Common with errors like "le-connection-abort-by-local", "br-connection-canceled"
- Indicates interference, range problems, or USB 3.0 port interference
- **BleakConnectionError**: General connection failure after all retries
- Raised for any other connection errors that don't fit the above categories
- The fallback exception when connection cannot be established
### Basic Example
```python
from bleak_retry_connector import establish_connection, BleakClientWithServiceCache
from bleak.backends.device import BLEDevice
async def connect_to_device(device: BLEDevice):
# Simple connection with retry
client = await establish_connection(
BleakClientWithServiceCache,
device,
name=device.name or device.address
)
# Use the client
services = client.services
# Disconnect when done
await client.disconnect()
return client
```
### Example with Disconnection Callback
```python
async def connect_with_callback(device: BLEDevice):
def on_disconnect(client):
print(f"Device {device.address} disconnected unexpectedly")
client = await establish_connection(
BleakClientWithServiceCache,
device,
name=device.name or device.address,
disconnected_callback=on_disconnect,
max_attempts=5 # Try up to 5 times
)
return client
```
### Example with Device Callback
Use a device callback when the device information might change (e.g., path changes on Linux):
```python
class DeviceTracker:
def __init__(self, initial_device: BLEDevice):
self.device = initial_device
def get_device(self) -> BLEDevice:
return self.device
def update_device(self, new_device: BLEDevice):
self.device = new_device
async def connect_with_device_tracking(tracker: DeviceTracker):
client = await establish_connection(
BleakClientWithServiceCache,
tracker.device,
name="TrackedDevice",
ble_device_callback=tracker.get_device
)
return client
```
### Example with Custom Client Class
```python
from bleak import BleakClient
class CustomClient(BleakClient):
async def custom_method(self):
# Custom functionality
pass
async def connect_with_custom_client(device: BLEDevice):
client = await establish_connection(
CustomClient,
device,
name=device.name,
max_attempts=3
)
# Use custom methods
await client.custom_method()
return client
```
### Error Handling Example
```python
from bleak_retry_connector import (
establish_connection,
BleakClientWithServiceCache,
BleakNotFoundError,
BleakOutOfConnectionSlotsError,
BleakAbortedError,
BleakConnectionError
)
async def connect_with_error_handling(device: BLEDevice):
try:
client = await establish_connection(
BleakClientWithServiceCache,
device,
name=device.name
)
return client
except BleakNotFoundError:
print("Device not found - it may have moved out of range")
return None
except BleakOutOfConnectionSlotsError:
print("No connection slots available - try disconnecting other devices")
return None
except BleakAbortedError:
print("Connection aborted - check for interference or move closer")
return None
except BleakConnectionError as e:
print(f"Connection failed: {e}")
return None
```
### Example with Cache Clearing on Missing Characteristic
When a device's firmware changes or services are updated, you might encounter missing characteristics. Here's how to handle this scenario by clearing the cache and retrying:
```python
from bleak_retry_connector import establish_connection, BleakClientWithServiceCache
from bleak.exc import BleakError
class CharacteristicMissingError(Exception):
"""Raised when a required characteristic is missing."""
pass
async def connect_and_validate_services(device: BLEDevice):
"""Connect and validate required characteristics exist."""
client = await establish_connection(
BleakClientWithServiceCache,
device,
name=device.name or device.address,
use_services_cache=True
)
try:
# Check for required characteristics
required_service_uuid = "cba20d00-224d-11e6-9fb8-0002a5d5c51b"
required_char_uuid = "cba20002-224d-11e6-9fb8-0002a5d5c51b"
service = client.services.get_service(required_service_uuid)
if not service:
raise CharacteristicMissingError(f"Service {required_service_uuid} not found")
char = service.get_characteristic(required_char_uuid)
if not char:
raise CharacteristicMissingError(f"Characteristic {required_char_uuid} not found")
except (CharacteristicMissingError, KeyError) as ex:
# Services might have changed, clear cache and reconnect
print(f"Characteristic missing, clearing cache: {ex}")
await client.clear_cache()
await client.disconnect()
# Reconnect without cache
client = await establish_connection(
BleakClientWithServiceCache,
device,
name=device.name or device.address,
use_services_cache=False # Force fresh service discovery
)
# Validate again
service = client.services.get_service(required_service_uuid)
if not service:
await client.disconnect()
raise CharacteristicMissingError(f"Service {required_service_uuid} still not found after cache clear")
char = service.get_characteristic(required_char_uuid)
if not char:
await client.disconnect()
raise CharacteristicMissingError(f"Characteristic {required_char_uuid} still not found after cache clear")
return client
```
### Advanced Configuration
```python
async def connect_with_full_options(device: BLEDevice):
client = await establish_connection(
BleakClientWithServiceCache,
device,
name="MyDevice",
disconnected_callback=lambda c: print("Disconnected"),
max_attempts=6, # More attempts for difficult devices
use_services_cache=True, # Use caching for faster reconnects
timeout=30.0 # Pass additional kwargs to BleakClient
)
return client
```
## Complete Working Example
```python
import asyncio
from bleak import BleakScanner
from bleak_retry_connector import (
establish_connection,
BleakClientWithServiceCache,
BleakNotFoundError,
BleakOutOfConnectionSlotsError,
BleakAbortedError,
BleakConnectionError
)
async def main():
# Scan for devices
print("Scanning for devices...")
devices = await BleakScanner.discover()
if not devices:
print("No devices found")
return
# Connect to the first device found
device = devices[0]
print(f"Connecting to {device.name or device.address}...")
try:
# Establish connection with retry
client = await establish_connection(
BleakClientWithServiceCache,
device,
name=device.name or device.address,
max_attempts=4
)
print("Connected successfully!")
# List services
for service in client.services:
print(f" Service: {service.uuid}")
for char in service.characteristics:
print(f" Characteristic: {char.uuid}")
# Disconnect
await client.disconnect()
print("Disconnected")
except (BleakNotFoundError, BleakOutOfConnectionSlotsError,
BleakAbortedError, BleakConnectionError) as e:
print(f"Failed to connect: {e}")
if __name__ == "__main__":
asyncio.run(main())
```
|