Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
`Unreleased`_
=============

Added
-----
* Added ``bleak.backends.get_default_backend()`` and ``BleakBackend`` enum for a centralized backend detection.
* Added ``BleakClient().backend_id`` and ``BleakScanner().backend_id`` properties to identify the backend in use.

Changed
-------
* Raise new ``BleakBluetoothNotAvailableError`` when Bluetooth is not supported, turned off or permission is denied.
Expand Down
39 changes: 35 additions & 4 deletions bleak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from bleak.args.bluez import BlueZScannerArgs
from bleak.args.corebluetooth import CBScannerArgs, CBStartNotifyArgs
from bleak.args.winrt import WinRTClientArgs
from bleak.backends import BleakBackend
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type
from bleak.backends.descriptor import BleakGATTDescriptor
Expand Down Expand Up @@ -123,8 +124,10 @@ def __init__(
backend: Optional[type[BaseBleakScanner]] = None,
**kwargs: Any,
) -> None:
PlatformBleakScanner = (
get_platform_scanner_backend_type() if backend is None else backend
PlatformBleakScanner, backend_id = (
get_platform_scanner_backend_type()
if backend is None
else (backend, backend.__name__)
)

self._backend = PlatformBleakScanner(
Expand All @@ -135,6 +138,19 @@ def __init__(
cb=cb,
**kwargs,
) # type: ignore
self._backend_id = backend_id

@property
def backend_id(self) -> BleakBackend | str:
"""
Gets the identifier of the backend in use.

The value is one of the :class:`BleakBackend` enum values in case of
built-in backends, or a string identifying a custom backend.

.. versionadded:: unreleased
"""
return self._backend_id

async def __aenter__(self) -> Self:
await self._backend.start()
Expand Down Expand Up @@ -500,8 +516,10 @@ def __init__(
backend: Optional[type[BaseBleakClient]] = None,
**kwargs: Any,
) -> None:
PlatformBleakClient = (
get_platform_client_backend_type() if backend is None else backend
PlatformBleakClient, backend_id = (
get_platform_client_backend_type()
if backend is None
else (backend, backend.__name__)
)

self._backend = PlatformBleakClient(
Expand All @@ -519,6 +537,19 @@ def __init__(
**kwargs,
)
self._pair_before_connect = pair
self._backend_id = backend_id

@property
def backend_id(self) -> BleakBackend | str:
"""
Gets the identifier of the backend in use.

The value is one of the :class:`BleakBackend` enum values in case of
built-in backends, or a string identifying a custom backend.

.. versionadded:: unreleased
"""
return self._backend_id

# device info

Expand Down
76 changes: 73 additions & 3 deletions bleak/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,75 @@
# -*- coding: utf-8 -*-
# Created on 2017-11-19 by hbldh <[email protected]>
"""
__init__.py
Communicating with Bluetooth hardware requires calling OS-specific APIs. These
are abstracted as "backends" in Bleak.

The backend will be automatically selected based on the operating system Bleak
is running on. In some cases, this may also depend on a specific runtime, like
Pythonista on iOS.
"""

import enum
import os
import platform
import sys

from bleak.exc import BleakError


class BleakBackend(str, enum.Enum):
"""
Identifiers for available built-in Bleak backends.

.. versionadded:: unreleased
"""

P4ANDROID = "p4android"
"""
Python for Android backend.
"""

BLUEZ_DBUS = "bluez_dbus"
"""
BlueZ D-Bus backend for Linux.
"""

PYTHONISTA_CB = "pythonista_cb"
"""
Pythonista CoreBluetooth backend for iOS and macOS.
"""

CORE_BLUETOOTH = "core_bluetooth"
"""
CoreBluetooth backend for macOS.
"""

WIN_RT = "win_rt"
"""
Windows Runtime backend for Windows.
"""


def get_default_backend() -> BleakBackend:
"""
Returns the preferred backend for the current platform/environment.

.. versionadded:: unreleased
"""
if os.environ.get("P4A_BOOTSTRAP") is not None:
return BleakBackend.P4ANDROID

if platform.system() == "Linux":
return BleakBackend.BLUEZ_DBUS

if sys.platform == "ios" and "Pythonista3.app" in sys.executable:
# Must be resolved before checking for "Darwin" (macOS),
# as both the Pythonista app for iOS and macOS
# return "Darwin" from platform.system()
return BleakBackend.PYTHONISTA_CB

if platform.system() == "Darwin":
return BleakBackend.CORE_BLUETOOTH

if platform.system() == "Windows":
return BleakBackend.WIN_RT

raise BleakError(f"Unsupported platform: {platform.system()}")
53 changes: 26 additions & 27 deletions bleak/backends/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
Base class for backend clients.
"""
import abc
import os
import platform
import sys
from collections.abc import Callable
from typing import Any, Optional, Union
Expand All @@ -15,6 +13,7 @@
else:
from collections.abc import Buffer

from bleak.backends import BleakBackend, get_default_backend
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.descriptor import BleakGATTDescriptor
from bleak.backends.device import BLEDevice
Expand Down Expand Up @@ -209,41 +208,41 @@ async def stop_notify(self, characteristic: BleakGATTCharacteristic) -> None:
raise NotImplementedError()


def get_platform_client_backend_type() -> type[BaseBleakClient]:
def get_platform_client_backend_type() -> tuple[type[BaseBleakClient], BleakBackend]:
"""
Gets the platform-specific :class:`BaseBleakClient` type.
"""
if os.environ.get("P4A_BOOTSTRAP") is not None:
from bleak.backends.p4android.client import BleakClientP4Android
backend = get_default_backend()
match backend:
case BleakBackend.P4ANDROID:
from bleak.backends.p4android.client import BleakClientP4Android

return BleakClientP4Android
return (BleakClientP4Android, backend)

if platform.system() == "Linux":
from bleak.backends.bluezdbus.client import BleakClientBlueZDBus
case BleakBackend.BLUEZ_DBUS:
from bleak.backends.bluezdbus.client import BleakClientBlueZDBus

return BleakClientBlueZDBus
return (BleakClientBlueZDBus, backend)

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

return BleakClientPythonistaCB
except ImportError as e:
raise ImportError(
"Ensure you have `bleak-pythonista` package installed."
) from e
return (BleakClientPythonistaCB, backend)
except ImportError as e:
raise ImportError(
"Ensure you have `bleak-pythonista` package installed."
) from e

if platform.system() == "Darwin":
from bleak.backends.corebluetooth.client import BleakClientCoreBluetooth
case BleakBackend.CORE_BLUETOOTH:
from bleak.backends.corebluetooth.client import BleakClientCoreBluetooth

return BleakClientCoreBluetooth
return (BleakClientCoreBluetooth, backend)

if platform.system() == "Windows":
from bleak.backends.winrt.client import BleakClientWinRT
case BleakBackend.WIN_RT:
from bleak.backends.winrt.client import BleakClientWinRT

return BleakClientWinRT
return (BleakClientWinRT, backend)

raise BleakError(f"Unsupported platform: {platform.system()}")
case _:
raise BleakError(f"Unsupported backend: {backend}")
54 changes: 26 additions & 28 deletions bleak/backends/scanner.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import abc
import asyncio
import inspect
import os
import platform
import sys
from collections.abc import Callable, Coroutine, Hashable
from typing import Any, NamedTuple, Optional

from bleak.backends import BleakBackend, get_default_backend
from bleak.backends.device import BLEDevice
from bleak.exc import BleakError

Expand Down Expand Up @@ -282,41 +280,41 @@ async def stop(self) -> None:
raise NotImplementedError()


def get_platform_scanner_backend_type() -> type[BaseBleakScanner]:
def get_platform_scanner_backend_type() -> tuple[type[BaseBleakScanner], BleakBackend]:
"""
Gets the platform-specific :class:`BaseBleakScanner` type.
"""
if os.environ.get("P4A_BOOTSTRAP") is not None:
from bleak.backends.p4android.scanner import BleakScannerP4Android
backend = get_default_backend()
match backend:
case BleakBackend.P4ANDROID:
from bleak.backends.p4android.scanner import BleakScannerP4Android

return BleakScannerP4Android
return (BleakScannerP4Android, backend)

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

return BleakScannerBlueZDBus
return (BleakScannerBlueZDBus, backend)

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

return BleakScannerPythonistaCB
except ImportError as e:
raise ImportError(
"Ensure you have `bleak-pythonista` package installed."
) from e
return (BleakScannerPythonistaCB, backend)
except ImportError as e:
raise ImportError(
"Ensure you have `bleak-pythonista` package installed."
) from e

if platform.system() == "Darwin":
from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth
case BleakBackend.CORE_BLUETOOTH:
from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth

return BleakScannerCoreBluetooth
return (BleakScannerCoreBluetooth, backend)

if platform.system() == "Windows":
from bleak.backends.winrt.scanner import BleakScannerWinRT
case BleakBackend.WIN_RT:
from bleak.backends.winrt.scanner import BleakScannerWinRT

return BleakScannerWinRT
return (BleakScannerWinRT, backend)

raise BleakError(f"Unsupported platform: {platform.system()}")
case _:
raise BleakError(f"Unsupported backend: {backend}")
1 change: 1 addition & 0 deletions docs/api/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Device information

.. autoproperty:: bleak.BleakClient.mtu_size

.. autoproperty:: bleak.BleakClient.backend_id

----------------------
GATT Client Operations
Expand Down
7 changes: 7 additions & 0 deletions docs/api/scanner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,10 @@ Otherwise, you can use one of the properties below after scanning has stopped.

.. autoproperty:: bleak.BleakScanner.discovered_devices
.. autoproperty:: bleak.BleakScanner.discovered_devices_and_advertisement_data


-----------------
Extra information
-----------------

.. autoproperty:: bleak.BleakScanner.backend_id
7 changes: 7 additions & 0 deletions docs/backends/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ Contents:
android
pythonista

Backend selection
-----------------

.. automodule:: bleak.backends
:members:


Shared Backend API
------------------

Expand Down
4 changes: 2 additions & 2 deletions examples/mtu_size.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import asyncio

from bleak import BleakClient, BleakScanner
from bleak import BleakBackend, BleakClient, BleakScanner
from bleak.backends.scanner import AdvertisementData, BLEDevice

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

print("MTU:", client.mtu_size)
Expand Down
Loading
Loading