Skip to content

Commit a6fea5a

Browse files
authored
backends: Centralize backend detection
Add new backend enum and detection function. This de-duplicates the backend detection logic. Also add new `backend_id` property to `BleakClient` and `BleakScanner` to get the backend used by each instance.
1 parent 5d0593d commit a6fea5a

File tree

10 files changed

+213
-66
lines changed

10 files changed

+213
-66
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
1010
`Unreleased`_
1111
=============
1212

13+
Added
14+
-----
15+
* Added ``bleak.backends.get_default_backend()`` and ``BleakBackend`` enum for a centralized backend detection.
16+
* Added ``BleakClient().backend_id`` and ``BleakScanner().backend_id`` properties to identify the backend in use.
17+
1318
Changed
1419
-------
1520
* Raise new ``BleakBluetoothNotAvailableError`` when Bluetooth is not supported, turned off or permission is denied.

bleak/__init__.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from bleak.args.bluez import BlueZScannerArgs
3434
from bleak.args.corebluetooth import CBScannerArgs, CBStartNotifyArgs
3535
from bleak.args.winrt import WinRTClientArgs
36+
from bleak.backends import BleakBackend
3637
from bleak.backends.characteristic import BleakGATTCharacteristic
3738
from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type
3839
from bleak.backends.descriptor import BleakGATTDescriptor
@@ -123,8 +124,10 @@ def __init__(
123124
backend: Optional[type[BaseBleakScanner]] = None,
124125
**kwargs: Any,
125126
) -> None:
126-
PlatformBleakScanner = (
127-
get_platform_scanner_backend_type() if backend is None else backend
127+
PlatformBleakScanner, backend_id = (
128+
get_platform_scanner_backend_type()
129+
if backend is None
130+
else (backend, backend.__name__)
128131
)
129132

130133
self._backend = PlatformBleakScanner(
@@ -135,6 +138,19 @@ def __init__(
135138
cb=cb,
136139
**kwargs,
137140
) # type: ignore
141+
self._backend_id = backend_id
142+
143+
@property
144+
def backend_id(self) -> BleakBackend | str:
145+
"""
146+
Gets the identifier of the backend in use.
147+
148+
The value is one of the :class:`BleakBackend` enum values in case of
149+
built-in backends, or a string identifying a custom backend.
150+
151+
.. versionadded:: unreleased
152+
"""
153+
return self._backend_id
138154

139155
async def __aenter__(self) -> Self:
140156
await self._backend.start()
@@ -500,8 +516,10 @@ def __init__(
500516
backend: Optional[type[BaseBleakClient]] = None,
501517
**kwargs: Any,
502518
) -> None:
503-
PlatformBleakClient = (
504-
get_platform_client_backend_type() if backend is None else backend
519+
PlatformBleakClient, backend_id = (
520+
get_platform_client_backend_type()
521+
if backend is None
522+
else (backend, backend.__name__)
505523
)
506524

507525
self._backend = PlatformBleakClient(
@@ -519,6 +537,19 @@ def __init__(
519537
**kwargs,
520538
)
521539
self._pair_before_connect = pair
540+
self._backend_id = backend_id
541+
542+
@property
543+
def backend_id(self) -> BleakBackend | str:
544+
"""
545+
Gets the identifier of the backend in use.
546+
547+
The value is one of the :class:`BleakBackend` enum values in case of
548+
built-in backends, or a string identifying a custom backend.
549+
550+
.. versionadded:: unreleased
551+
"""
552+
return self._backend_id
522553

523554
# device info
524555

bleak/backends/__init__.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,75 @@
1-
# -*- coding: utf-8 -*-
2-
# Created on 2017-11-19 by hbldh <[email protected]>
31
"""
4-
__init__.py
2+
Communicating with Bluetooth hardware requires calling OS-specific APIs. These
3+
are abstracted as "backends" in Bleak.
4+
5+
The backend will be automatically selected based on the operating system Bleak
6+
is running on. In some cases, this may also depend on a specific runtime, like
7+
Pythonista on iOS.
58
"""
9+
10+
import enum
11+
import os
12+
import platform
13+
import sys
14+
15+
from bleak.exc import BleakError
16+
17+
18+
class BleakBackend(str, enum.Enum):
19+
"""
20+
Identifiers for available built-in Bleak backends.
21+
22+
.. versionadded:: unreleased
23+
"""
24+
25+
P4ANDROID = "p4android"
26+
"""
27+
Python for Android backend.
28+
"""
29+
30+
BLUEZ_DBUS = "bluez_dbus"
31+
"""
32+
BlueZ D-Bus backend for Linux.
33+
"""
34+
35+
PYTHONISTA_CB = "pythonista_cb"
36+
"""
37+
Pythonista CoreBluetooth backend for iOS and macOS.
38+
"""
39+
40+
CORE_BLUETOOTH = "core_bluetooth"
41+
"""
42+
CoreBluetooth backend for macOS.
43+
"""
44+
45+
WIN_RT = "win_rt"
46+
"""
47+
Windows Runtime backend for Windows.
48+
"""
49+
50+
51+
def get_default_backend() -> BleakBackend:
52+
"""
53+
Returns the preferred backend for the current platform/environment.
54+
55+
.. versionadded:: unreleased
56+
"""
57+
if os.environ.get("P4A_BOOTSTRAP") is not None:
58+
return BleakBackend.P4ANDROID
59+
60+
if platform.system() == "Linux":
61+
return BleakBackend.BLUEZ_DBUS
62+
63+
if sys.platform == "ios" and "Pythonista3.app" in sys.executable:
64+
# Must be resolved before checking for "Darwin" (macOS),
65+
# as both the Pythonista app for iOS and macOS
66+
# return "Darwin" from platform.system()
67+
return BleakBackend.PYTHONISTA_CB
68+
69+
if platform.system() == "Darwin":
70+
return BleakBackend.CORE_BLUETOOTH
71+
72+
if platform.system() == "Windows":
73+
return BleakBackend.WIN_RT
74+
75+
raise BleakError(f"Unsupported platform: {platform.system()}")

bleak/backends/client.py

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
Base class for backend clients.
55
"""
66
import abc
7-
import os
8-
import platform
97
import sys
108
from collections.abc import Callable
119
from typing import Any, Optional, Union
@@ -15,6 +13,7 @@
1513
else:
1614
from collections.abc import Buffer
1715

16+
from bleak.backends import BleakBackend, get_default_backend
1817
from bleak.backends.characteristic import BleakGATTCharacteristic
1918
from bleak.backends.descriptor import BleakGATTDescriptor
2019
from bleak.backends.device import BLEDevice
@@ -209,41 +208,41 @@ async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
209208
raise NotImplementedError()
210209

211210

212-
def get_platform_client_backend_type() -> type[BaseBleakClient]:
211+
def get_platform_client_backend_type() -> tuple[type[BaseBleakClient], BleakBackend]:
213212
"""
214213
Gets the platform-specific :class:`BaseBleakClient` type.
215214
"""
216-
if os.environ.get("P4A_BOOTSTRAP") is not None:
217-
from bleak.backends.p4android.client import BleakClientP4Android
215+
backend = get_default_backend()
216+
match backend:
217+
case BleakBackend.P4ANDROID:
218+
from bleak.backends.p4android.client import BleakClientP4Android
218219

219-
return BleakClientP4Android
220+
return (BleakClientP4Android, backend)
220221

221-
if platform.system() == "Linux":
222-
from bleak.backends.bluezdbus.client import BleakClientBlueZDBus
222+
case BleakBackend.BLUEZ_DBUS:
223+
from bleak.backends.bluezdbus.client import BleakClientBlueZDBus
223224

224-
return BleakClientBlueZDBus
225+
return (BleakClientBlueZDBus, backend)
225226

226-
if sys.platform == "ios" and "Pythonista3.app" in sys.executable:
227-
# Must be resolved before checking for "Darwin" (macOS),
228-
# as both the Pythonista app for iOS and macOS
229-
# return "Darwin" from platform.system()
230-
try:
231-
from bleak_pythonista import BleakClientPythonistaCB
227+
case BleakBackend.PYTHONISTA_CB:
228+
try:
229+
from bleak_pythonista import BleakClientPythonistaCB
232230

233-
return BleakClientPythonistaCB
234-
except ImportError as e:
235-
raise ImportError(
236-
"Ensure you have `bleak-pythonista` package installed."
237-
) from e
231+
return (BleakClientPythonistaCB, backend)
232+
except ImportError as e:
233+
raise ImportError(
234+
"Ensure you have `bleak-pythonista` package installed."
235+
) from e
238236

239-
if platform.system() == "Darwin":
240-
from bleak.backends.corebluetooth.client import BleakClientCoreBluetooth
237+
case BleakBackend.CORE_BLUETOOTH:
238+
from bleak.backends.corebluetooth.client import BleakClientCoreBluetooth
241239

242-
return BleakClientCoreBluetooth
240+
return (BleakClientCoreBluetooth, backend)
243241

244-
if platform.system() == "Windows":
245-
from bleak.backends.winrt.client import BleakClientWinRT
242+
case BleakBackend.WIN_RT:
243+
from bleak.backends.winrt.client import BleakClientWinRT
246244

247-
return BleakClientWinRT
245+
return (BleakClientWinRT, backend)
248246

249-
raise BleakError(f"Unsupported platform: {platform.system()}")
247+
case _:
248+
raise BleakError(f"Unsupported backend: {backend}")

bleak/backends/scanner.py

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import abc
22
import asyncio
33
import inspect
4-
import os
5-
import platform
6-
import sys
74
from collections.abc import Callable, Coroutine, Hashable
85
from typing import Any, NamedTuple, Optional
96

7+
from bleak.backends import BleakBackend, get_default_backend
108
from bleak.backends.device import BLEDevice
119
from bleak.exc import BleakError
1210

@@ -282,41 +280,41 @@ async def stop(self) -> None:
282280
raise NotImplementedError()
283281

284282

285-
def get_platform_scanner_backend_type() -> type[BaseBleakScanner]:
283+
def get_platform_scanner_backend_type() -> tuple[type[BaseBleakScanner], BleakBackend]:
286284
"""
287285
Gets the platform-specific :class:`BaseBleakScanner` type.
288286
"""
289-
if os.environ.get("P4A_BOOTSTRAP") is not None:
290-
from bleak.backends.p4android.scanner import BleakScannerP4Android
287+
backend = get_default_backend()
288+
match backend:
289+
case BleakBackend.P4ANDROID:
290+
from bleak.backends.p4android.scanner import BleakScannerP4Android
291291

292-
return BleakScannerP4Android
292+
return (BleakScannerP4Android, backend)
293293

294-
if platform.system() == "Linux":
295-
from bleak.backends.bluezdbus.scanner import BleakScannerBlueZDBus
294+
case BleakBackend.BLUEZ_DBUS:
295+
from bleak.backends.bluezdbus.scanner import BleakScannerBlueZDBus
296296

297-
return BleakScannerBlueZDBus
297+
return (BleakScannerBlueZDBus, backend)
298298

299-
if sys.platform == "ios" and "Pythonista3.app" in sys.executable:
300-
# Must be resolved before checking for "Darwin" (macOS),
301-
# as both the Pythonista app for iOS and macOS
302-
# return "Darwin" from platform.system()
303-
try:
304-
from bleak_pythonista import BleakScannerPythonistaCB
299+
case BleakBackend.PYTHONISTA_CB:
300+
try:
301+
from bleak_pythonista import BleakScannerPythonistaCB
305302

306-
return BleakScannerPythonistaCB
307-
except ImportError as e:
308-
raise ImportError(
309-
"Ensure you have `bleak-pythonista` package installed."
310-
) from e
303+
return (BleakScannerPythonistaCB, backend)
304+
except ImportError as e:
305+
raise ImportError(
306+
"Ensure you have `bleak-pythonista` package installed."
307+
) from e
311308

312-
if platform.system() == "Darwin":
313-
from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth
309+
case BleakBackend.CORE_BLUETOOTH:
310+
from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth
314311

315-
return BleakScannerCoreBluetooth
312+
return (BleakScannerCoreBluetooth, backend)
316313

317-
if platform.system() == "Windows":
318-
from bleak.backends.winrt.scanner import BleakScannerWinRT
314+
case BleakBackend.WIN_RT:
315+
from bleak.backends.winrt.scanner import BleakScannerWinRT
319316

320-
return BleakScannerWinRT
317+
return (BleakScannerWinRT, backend)
321318

322-
raise BleakError(f"Unsupported platform: {platform.system()}")
319+
case _:
320+
raise BleakError(f"Unsupported backend: {backend}")

docs/api/client.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Device information
5555

5656
.. autoproperty:: bleak.BleakClient.mtu_size
5757

58+
.. autoproperty:: bleak.BleakClient.backend_id
5859

5960
----------------------
6061
GATT Client Operations

docs/api/scanner.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,10 @@ Otherwise, you can use one of the properties below after scanning has stopped.
8181

8282
.. autoproperty:: bleak.BleakScanner.discovered_devices
8383
.. autoproperty:: bleak.BleakScanner.discovered_devices_and_advertisement_data
84+
85+
86+
-----------------
87+
Extra information
88+
-----------------
89+
90+
.. autoproperty:: bleak.BleakScanner.backend_id

docs/backends/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ Contents:
2222
android
2323
pythonista
2424

25+
Backend selection
26+
-----------------
27+
28+
.. automodule:: bleak.backends
29+
:members:
30+
31+
2532
Shared Backend API
2633
------------------
2734

examples/mtu_size.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import asyncio
66

7-
from bleak import BleakClient, BleakScanner
7+
from bleak import BleakBackend, BleakClient, BleakScanner
88
from bleak.backends.scanner import AdvertisementData, BLEDevice
99

1010
# replace with real characteristic UUID
@@ -26,7 +26,7 @@ def callback(device: BLEDevice, adv: AdvertisementData) -> None:
2626
# BlueZ doesn't have a proper way to get the MTU, so we have this hack.
2727
# If this doesn't work for you, you can set the client._mtu_size attribute
2828
# to override the value instead.
29-
if client._backend.__class__.__name__ == "BleakClientBlueZDBus": # type: ignore
29+
if client.backend_id == BleakBackend.BLUEZ_DBUS:
3030
await client._backend._acquire_mtu() # type: ignore
3131

3232
print("MTU:", client.mtu_size)

0 commit comments

Comments
 (0)