File: usage.md

package info (click to toggle)
bleak-retry-connector 4.4.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 540 kB
  • sloc: python: 3,486; makefile: 12
file content (378 lines) | stat: -rw-r--r-- 12,234 bytes parent folder | download
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())
```