diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml
index bc4bac3c..16008896 100644
--- a/.github/workflows/build_and_test.yml
+++ b/.github/workflows/build_and_test.yml
@@ -13,10 +13,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: [3.5, 3.6, 3.7, 3.8]
- exclude:
- - os: macos-latest
- python-version: 3.5
+ python-version: [3.6, 3.7, 3.8]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
@@ -32,12 +29,14 @@ jobs:
run: pip install -r requirements.txt
- name: Install develoment dependencies
run: pip install -r requirements_dev.txt
+ - name: Check code formatting with black
+ run: black . --diff
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+ # exit-zero treats all errors as warnings. Default line length of black is 88
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
- name: Test with pytest
run: |
pytest tests --junitxml=junit/test-results-${{ matrix.os }}-${{ matrix.python-version }}.xml --cov=com --cov-report=xml --cov-report=html
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 119b94c3..34bb2f96 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,12 +7,14 @@ All notable changes to this project will be documented in this file.
The format is based on `Keep a Changelog `_,
and this project adheres to `Semantic Versioning `_.
-`0.8.0`_ (2020-09-02)
+
+`0.8.0`_ (2020-09-22)
---------------------
Added
~~~~~
+* Implemented ``set_disconnected_callback`` in the .NET backend ``BleakClient`` implementation.
* Added ``find_device_by_address`` method to the ``BleakScanner`` interface, for stopping scanning
when a desired address is found.
* Implemented ``find_device_by_address`` in the .NET backend ``BleakScanner`` implementation and
@@ -26,15 +28,24 @@ Added
* Implemented pairing method in .NET backend.
* Implemented pairing method in the BlueZ backend.
* Added stumps and ``NotImplementedError`` on pairing in macOS backend.
+* Added the possibility to connect using ``BLEDevice`` instead of a string address. This
+ allows for skipping the discovery call when connecting.
+
+Removed
+~~~~~~~
+
+* Support for Python 3.5.
Changed
~~~~~~~
* **BREAKING CHANGE** All notifications now have the characteristic's integer **handle** instead of its UUID as a
string as the first argument ``sender`` sent to notification callbacks. This provides the uniqueness of
sender in notifications as well.
+* Renamed ``BleakClient`` argument ``address`` to ``address_or_ble_device``.
* Version 0.5.0 of BleakUWPBridge, with some modified methods and implementing ``IDisposable``.
* Merged #224. All storing and passing of event loops in bleak is removed.
* Removed Objective C delegate compliance checks. Merged #253.
+* Made context managers for .NET ``DataReader`` and ``DataWriter``.
Fixed
~~~~~
@@ -56,7 +67,7 @@ Fixed
Changed
~~~~~~~
-* Improved, more explantory error on BlueZ backend when ``BleakClient`` cannot find the desired device when trying to connect. (#238)
+* Improved, more explanatory error on BlueZ backend when ``BleakClient`` cannot find the desired device when trying to connect. (#238)
* Better-than-nothing documentation about scanning filters added (#230).
* Ran black on code which was forgotten in 0.7.0. Large diffs due to that.
* Re-adding Python 3.8 CI "tests" on Windows again.
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index d58c1e95..82e558b0 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -102,7 +102,7 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the
feature to the list in README.rst.
-3. The pull request should work for Python 3.5+ on the following platforms:
+3. The pull request should work for Python 3.6+ on the following platforms:
- Windows 10, version 16299 (Fall Creators Update) and greater
- Linux distributions with BlueZ >= 5.43
- OS X / macOS >= 10.11
diff --git a/README.rst b/README.rst
index dfbcf46b..d765fdac 100644
--- a/README.rst
+++ b/README.rst
@@ -2,13 +2,12 @@
bleak
=====
-.. image:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png
+.. figure:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png
:target: https://github.com/hbldh/bleak
:alt: Bleak Logo
:scale: 50%
-
.. image:: https://github.com/hbldh/bleak/workflows/Build%20and%20Test/badge.svg
:target: https://github.com/hbldh/bleak/actions?query=workflow%3A%22Build+and+Test%22
:alt: Build and Test
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 6e8b752e..d5203fba 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -13,8 +13,6 @@ jobs:
vmImage: 'Ubuntu 16.04'
strategy:
matrix:
- Python35-x64:
- python.version: '3.5'
python.architecture: 'x64'
Python36-x64:
python.version: '3.6'
@@ -53,8 +51,6 @@ jobs:
vmImage: 'windows-2019'
strategy:
matrix:
- Python35-x64:
- python.version: '3.5'
python.architecture: 'x64'
Python36-x64:
python.version: '3.6'
@@ -92,8 +88,6 @@ jobs:
strategy:
matrix:
- Python35-x64:
- python.version: '3.5'
python.architecture: 'x64'
Python36-x64:
python.version: '3.6'
@@ -131,8 +125,6 @@ jobs:
strategy:
matrix:
- Python35-x64:
- python.version: '3.5'
python.architecture: 'x64'
Python36-x64:
python.version: '3.6'
diff --git a/bleak/backends/bluezdbus/characteristic.py b/bleak/backends/bluezdbus/characteristic.py
index badcfcfc..46454333 100644
--- a/bleak/backends/bluezdbus/characteristic.py
+++ b/bleak/backends/bluezdbus/characteristic.py
@@ -1,4 +1,3 @@
-import re
from uuid import UUID
from typing import Union, List
@@ -27,8 +26,6 @@
# "authorize"
}
-_handle_regex = re.compile("/char([0-9a-fA-F]*)")
-
class BleakGATTCharacteristicBlueZDBus(BleakGATTCharacteristic):
"""GATT Characteristic implementation for the BlueZ DBus backend"""
@@ -39,17 +36,8 @@ def __init__(self, obj: dict, object_path: str, service_uuid: str):
self.__path = object_path
self.__service_uuid = service_uuid
- # The `Handle` attribute is added in BlueZ Release 5.51. Empirically,
- # it seems to hold true that the "/charYYYY" that is at the end of the
- # DBUS path actually is the desired handle. Using regex to extract
- # that and using as handle, since handle is mostly used for keeping
- # track of characteristics (internally in bleak anyway).
- self._handle = self.obj.get("Handle")
- if not self._handle:
- _handle_from_path = _handle_regex.search(self.path)
- if _handle_from_path:
- self._handle = int(_handle_from_path.groups()[0], 16)
- self._handle = int(self._handle)
+ # D-Bus object path contains handle as last 4 characters of 'charYYYY'
+ self._handle = int(object_path[-4:], 16)
@property
def service_uuid(self) -> str:
diff --git a/bleak/backends/bluezdbus/client.py b/bleak/backends/bluezdbus/client.py
index 4742ebaf..908d3e74 100644
--- a/bleak/backends/bluezdbus/client.py
+++ b/bleak/backends/bluezdbus/client.py
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
+"""
+BLE Client for BlueZ on Linux
+"""
import logging
import asyncio
import os
@@ -12,6 +15,7 @@
from twisted.internet.error import ConnectionDone
+from bleak.backends.device import BLEDevice
from bleak.backends.service import BleakGATTServiceCollection
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.exc import BleakError
@@ -36,27 +40,28 @@ class BleakClientBlueZDBus(BaseBleakClient):
Implemented by using the `BlueZ DBUS API `_.
Args:
- address (str): The address of the BLE peripheral to connect to.
+ address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.
Keyword Args:
timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.
"""
- def __init__(self, address, **kwargs):
- super(BleakClientBlueZDBus, self).__init__(address, **kwargs)
+ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
+ super(BleakClientBlueZDBus, self).__init__(address_or_ble_device, **kwargs)
self.device = kwargs.get("device") if kwargs.get("device") else "hci0"
- self.address = address
+ self.address = address_or_ble_device
# Backend specific, TXDBus objects and data
- self._device_path = None
+ if isinstance(address_or_ble_device, BLEDevice):
+ self._device_path = address_or_ble_device.details["path"]
+ else:
+ self._device_path = None
self._bus = None
self._reactor = None
self._rules = {}
self._subscriptions = list()
- self._disconnected_callback = None
-
# This maps DBus paths of GATT Characteristics to their BLE handles.
self._char_path_to_handle = {}
@@ -69,34 +74,6 @@ def __init__(self, address, **kwargs):
# Connectivity methods
- def set_disconnected_callback(
- self, callback: Callable[[BaseBleakClient, Future], None], **kwargs
- ) -> None:
- """Set the disconnected callback.
-
- The callback will be called on DBus PropChanged event with
- the 'Connected' key set to False.
-
- A disconnect callback must accept two positional arguments,
- the BleakClient and the Future that called it.
-
- Example:
-
- .. code-block::python
-
- async with BleakClient(mac_addr) as client:
- def disconnect_callback(client, future):
- print(f"Disconnected callback called on {client}!")
-
- client.set_disconnected_callback(disconnect_callback)
-
- Args:
- callback: callback to be called on disconnection.
-
- """
-
- self._disconnected_callback = callback
-
async def connect(self, **kwargs) -> bool:
"""Connect to the specified GATT server.
@@ -109,17 +86,19 @@ async def connect(self, **kwargs) -> bool:
"""
# A Discover must have been run before connecting to any devices.
# Find the desired device before trying to connect.
- timeout = kwargs.get("timeout", self._timeout)
- device = await BleakScannerBlueZDBus.find_device_by_address(
- self.address, timeout=timeout, device=self.device)
-
- if device:
- self._device_path = device.details["path"]
- else:
- raise BleakError(
- "Device with address {0} was not found.".format(self.address)
+ if self._device_path is None:
+ timeout = kwargs.get("timeout", self._timeout)
+ device = await BleakScannerBlueZDBus.find_device_by_address(
+ self.address, timeout=timeout, device=self.device
)
+ if device:
+ self._device_path = device.details["path"]
+ else:
+ raise BleakError(
+ "Device with address {0} was not found.".format(self.address)
+ )
+
loop = asyncio.get_event_loop()
self._reactor = get_reactor(loop)
@@ -341,7 +320,9 @@ async def unpair(self) -> bool:
Boolean regarding success of unpairing.
"""
- warnings.warn("Unpairing is seemingly unavailable in the BlueZ DBus API at the moment.")
+ warnings.warn(
+ "Unpairing is seemingly unavailable in the BlueZ DBus API at the moment."
+ )
return False
async def is_connected(self) -> bool:
@@ -429,7 +410,9 @@ async def get_services(self) -> BleakGATTServiceCollection:
self.services.add_characteristic(
BleakGATTCharacteristicBlueZDBus(char, object_path, _service[0].uuid)
)
- self._char_path_to_handle[object_path] = char.get("Handle")
+
+ # D-Bus object path contains handle as last 4 characters of 'charYYYY'
+ self._char_path_to_handle[object_path] = int(object_path[-4:], 16)
for desc, object_path in _descs:
_characteristic = list(
@@ -569,14 +552,16 @@ async def write_gatt_char(
) -> None:
"""Perform a write operation on the specified GATT characteristic.
- NB: the version check below is for the "type" option to the
- "Characteristic.WriteValue" method that was added to Bluez in 5.51
- https://git.kernel.org/pub/scm/bluetooth/bluez.git/commit?id=fa9473bcc48417d69cc9ef81d41a72b18e34a55a
- Before that commit, "Characteristic.WriteValue" was only "Write with
- response". "Characteristic.AcquireWrite" was added in Bluez 5.46
- https://git.kernel.org/pub/scm/bluetooth/bluez.git/commit/doc/gatt-api.txt?id=f59f3dedb2c79a75e51a3a0d27e2ae06fefc603e
- which can be used to "Write without response", but for older versions
- of Bluez, it is not possible to "Write without response".
+ .. note::
+
+ The version check below is for the "type" option to the
+ "Characteristic.WriteValue" method that was added to `Bluez in 5.51
+ `_
+ Before that commit, ``Characteristic.WriteValue`` was only "Write with
+ response". ``Characteristic.AcquireWrite`` was `added in Bluez 5.46
+ `_
+ which can be used to "Write without response", but for older versions
+ of Bluez, it is not possible to "Write without response".
Args:
char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to write
diff --git a/bleak/backends/bluezdbus/scanner.py b/bleak/backends/bluezdbus/scanner.py
index 2a333b95..01d2e46d 100644
--- a/bleak/backends/bluezdbus/scanner.py
+++ b/bleak/backends/bluezdbus/scanner.py
@@ -217,7 +217,9 @@ def register_detection_callback(self, callback: Callable):
self._callback = callback
@classmethod
- async def find_device_by_address(cls, device_identifier: str, timeout: float = 10.0, **kwargs) -> BLEDevice:
+ async def find_device_by_address(
+ cls, device_identifier: str, timeout: float = 10.0, **kwargs
+ ) -> BLEDevice:
"""A convenience method for obtaining a ``BLEDevice`` object specified by Bluetooth address.
Args:
@@ -237,10 +239,15 @@ async def find_device_by_address(cls, device_identifier: str, timeout: float = 1
scanner = cls(timeout=timeout)
def stop_if_detected(message):
- if any(device.get("Address", "").lower() == device_identifier for device in scanner._devices.values()):
+ if any(
+ device.get("Address", "").lower() == device_identifier
+ for device in scanner._devices.values()
+ ):
loop.call_soon_threadsafe(stop_scanning_event.set)
- return await scanner._find_device_by_address(device_identifier, stop_scanning_event, stop_if_detected, timeout)
+ return await scanner._find_device_by_address(
+ device_identifier, stop_scanning_event, stop_if_detected, timeout
+ )
# Helper methods
diff --git a/bleak/backends/bluezdbus/utils.py b/bleak/backends/bluezdbus/utils.py
index ab985f1a..997f3583 100644
--- a/bleak/backends/bluezdbus/utils.py
+++ b/bleak/backends/bluezdbus/utils.py
@@ -50,21 +50,21 @@ def get_device_object_path(hci_device, address):
def get_gatt_service_path(hci_device, address, service_id):
"""Get object path for a GATT Service for a Bluetooth device.
- Service org.bluez
- Service org.bluez
- Interface org.bluez.GattService1
- Object path [variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/serviceXX
+ Service org.bluez
+ Service org.bluez
+ Interface org.bluez.GattService1
+ Object path [variable prefix]/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/serviceXX
- Args:
- hci_device (str): Which bluetooth adapter to connect with.
- address (str): The Bluetooth address of the bluetooth device.
- service_id (int):
+ Args:
+ hci_device (str): Which bluetooth adapter to connect with.
+ address (str): The Bluetooth address of the bluetooth device.
+ service_id (int):
- Returns:
- String representation of GATT service object path on format
- `/org/bluez/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/serviceXX`.
+ Returns:
+ String representation of GATT service object path on format
+ `/org/bluez/{hci0,hci1,...}/dev_XX_XX_XX_XX_XX_XX/serviceXX`.
- """
+ """
base = get_device_object_path(hci_device, address)
return base + "{0}/service{1:02d}".format(base, service_id)
diff --git a/bleak/backends/characteristic.py b/bleak/backends/characteristic.py
index fb3c81c5..d7bfc230 100644
--- a/bleak/backends/characteristic.py
+++ b/bleak/backends/characteristic.py
@@ -27,9 +27,7 @@ class GattCharacteristicsFlags(enum.Enum):
class BleakGATTCharacteristic(abc.ABC):
- """Interface for the Bleak representation of a GATT Characteristic
-
- """
+ """Interface for the Bleak representation of a GATT Characteristic"""
def __init__(self, obj: Any):
self.obj = obj
diff --git a/bleak/backends/client.py b/bleak/backends/client.py
index 9933aab0..b93fd2f2 100644
--- a/bleak/backends/client.py
+++ b/bleak/backends/client.py
@@ -12,6 +12,7 @@
from bleak.backends.service import BleakGATTServiceCollection
from bleak.backends.characteristic import BleakGATTCharacteristic
+from bleak.backends.device import BLEDevice
class BaseBleakClient(abc.ABC):
@@ -19,13 +20,21 @@ class BaseBleakClient(abc.ABC):
The documentation of this interface should thus be safe to use as a reference for your implementation.
- Keyword Args:
- timeout (float): Timeout for required ``discover`` call. Defaults to 2.0.
+ Args:
+ address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.
+ Keyword Args:
+ timeout (float): Timeout for required ``discover`` call. Defaults to 10.0.
+ disconnected_callback (callable): Callback that will be scheduled in the
+ event loop when the client is disconnected. The callable must take one
+ argument, which will be this client object.
"""
- def __init__(self, address, **kwargs):
- self.address = address
+ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
+ if isinstance(address_or_ble_device, BLEDevice):
+ self.address = address_or_ble_device.address
+ else:
+ self.address = address_or_ble_device
self.services = BleakGATTServiceCollection()
@@ -33,12 +42,17 @@ def __init__(self, address, **kwargs):
self._notification_callbacks = {}
self._timeout = kwargs.get("timeout", 10.0)
+ self._disconnected_callback = kwargs.get("disconnected_callback")
def __str__(self):
return "{0}, {1}".format(self.__class__.__name__, self.address)
def __repr__(self):
- return "<{0}, {1}, {2}>".format(self.__class__.__name__, self.address, super(BaseBleakClient, self).__repr__())
+ return "<{0}, {1}, {2}>".format(
+ self.__class__.__name__,
+ self.address,
+ super(BaseBleakClient, self).__repr__(),
+ )
# Async Context managers
@@ -51,15 +65,16 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
# Connectivity methods
- @abc.abstractmethod
- async def set_disconnected_callback(
- self, callback: Callable[["BaseBleakClient"], None], **kwargs
+ def set_disconnected_callback(
+ self, callback: Union[Callable[["BaseBleakClient"], None], None], **kwargs
) -> None:
"""Set the disconnect callback.
The callback will only be called on unsolicited disconnect event.
Callbacks must accept one input which is the client object itself.
+ Set the callback to ``None`` to remove any existing callback.
+
.. code-block:: python
def callback(client):
@@ -72,8 +87,7 @@ def callback(client):
callback: callback to be called on disconnection.
"""
-
- raise NotImplementedError()
+ self._disconnected_callback = callback
@abc.abstractmethod
async def connect(self, **kwargs) -> bool:
diff --git a/bleak/backends/corebluetooth/CentralManagerDelegate.py b/bleak/backends/corebluetooth/CentralManagerDelegate.py
index 62b37417..3a205c08 100644
--- a/bleak/backends/corebluetooth/CentralManagerDelegate.py
+++ b/bleak/backends/corebluetooth/CentralManagerDelegate.py
@@ -41,8 +41,12 @@
CBCentralManagerDelegate = objc.protocolNamed("CBCentralManagerDelegate")
-_mac_version = list(map(int, platform.mac_ver()[0].split(".")))
-_IS_PRE_10_13 = _mac_version[0] == 10 and _mac_version[1] < 13
+try:
+ _mac_version = list(map(int, platform.mac_ver()[0].split(".")))
+ _IS_PRE_10_13 = _mac_version[0] == 10 and _mac_version[1] < 13
+except: # noqa For building docs
+ _mac_version = ""
+ _IS_PRE_10_13 = False
class CMDConnectionState(Enum):
@@ -195,6 +199,8 @@ def did_discover_peripheral(
RSSI: NSNumber,
):
# Note: this function might be called several times for same device.
+ # This can happen for instance when an active scan is done, and the
+ # second call with contain the data from the BLE scan response.
# Example a first time with the following keys in advertisementData:
# ['kCBAdvDataLocalName', 'kCBAdvDataIsConnectable', 'kCBAdvDataChannel']
# ... and later a second time with other keys (and values) such as:
@@ -210,6 +216,9 @@ def did_discover_peripheral(
if uuid_string in self.devices:
device = self.devices[uuid_string]
+ # It could be the device did not have a name previously but now it does.
+ if peripheral.name():
+ device.name = peripheral.name()
else:
address = uuid_string
name = peripheral.name() or None
@@ -250,7 +259,9 @@ def did_connect_peripheral(self, central, peripheral):
)
)
if self._connection_state != CMDConnectionState.CONNECTED:
- peripheralDelegate = PeripheralDelegate.alloc().initWithPeripheral_(peripheral)
+ peripheralDelegate = PeripheralDelegate.alloc().initWithPeripheral_(
+ peripheral
+ )
self.connected_peripheral_delegate = peripheralDelegate
self._connection_state = CMDConnectionState.CONNECTED
diff --git a/bleak/backends/corebluetooth/PeripheralDelegate.py b/bleak/backends/corebluetooth/PeripheralDelegate.py
index a347b463..2ba1390e 100644
--- a/bleak/backends/corebluetooth/PeripheralDelegate.py
+++ b/bleak/backends/corebluetooth/PeripheralDelegate.py
@@ -35,7 +35,7 @@
class _EventDict(dict):
def get_cleared(self, xUUID) -> asyncio.Event:
- """ Convenience method.
+ """Convenience method.
Returns a cleared (False) event. Creates it if doesn't exits.
"""
if xUUID not in self:
@@ -128,9 +128,9 @@ async def readCharacteristic_(
self.peripheral.readValueForCharacteristic_(characteristic)
await asyncio.wait_for(event.wait(), timeout=5)
if characteristic.value():
- return characteristic.value()
+ return characteristic.value()
else:
- return b''
+ return b""
async def readDescriptor_(
self, descriptor: CBDescriptor, use_cached=True
diff --git a/bleak/backends/corebluetooth/client.py b/bleak/backends/corebluetooth/client.py
index 302fa840..32cf4fed 100644
--- a/bleak/backends/corebluetooth/client.py
+++ b/bleak/backends/corebluetooth/client.py
@@ -1,7 +1,7 @@
"""
BLE Client for CoreBluetooth on macOS
-Created on 2019-6-26 by kevincar
+Created on 2019-06-26 by kevincar
"""
import logging
@@ -20,9 +20,9 @@
BleakGATTCharacteristicCoreBluetooth,
)
from bleak.backends.corebluetooth.descriptor import BleakGATTDescriptorCoreBluetooth
-from bleak.backends.corebluetooth.discovery import discover
from bleak.backends.corebluetooth.scanner import BleakScannerCoreBluetooth
from bleak.backends.corebluetooth.service import BleakGATTServiceCoreBluetooth
+from bleak.backends.device import BLEDevice
from bleak.backends.service import BleakGATTServiceCollection
from bleak.backends.characteristic import BleakGATTCharacteristic
@@ -35,22 +35,26 @@ class BleakClientCoreBluetooth(BaseBleakClient):
"""CoreBluetooth class interface for BleakClient
Args:
- address (str): The uuid of the BLE peripheral to connect to.
+ address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.
Keyword Args:
- timeout (float): Timeout for required ``discover`` call during connect. Defaults to 10.0.
+ timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.
"""
- def __init__(self, address: str, **kwargs):
- super(BleakClientCoreBluetooth, self).__init__(address, **kwargs)
+
+ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
+ super(BleakClientCoreBluetooth, self).__init__(address_or_ble_device, **kwargs)
+
+ if isinstance(address_or_ble_device, BLEDevice):
+ self._device_info = address_or_ble_device.details
+ else:
+ self._device_info = None
self._device_info = None
self._requester = None
self._callbacks = {}
self._services = None
- self._disconnected_callback = None
-
def __str__(self):
return "BleakClientCoreBluetooth ({})".format(self.address)
@@ -58,34 +62,46 @@ async def connect(self, **kwargs) -> bool:
"""Connect to a specified Peripheral
Keyword Args:
- timeout (float): Timeout for required ``discover`` call. Defaults to 10.0.
+ timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.
Returns:
Boolean representing connection status.
"""
- timeout = kwargs.get("timeout", self._timeout)
- device = await BleakScannerCoreBluetooth.find_device_by_address(
- self.address, timeout=timeout)
-
- if device:
- self._device_info = device.details
- else:
- raise BleakError(
- "Device with address {} was not found".format(self.address)
+ if self._device_info is None:
+ timeout = kwargs.get("timeout", self._timeout)
+ device = await BleakScannerCoreBluetooth.find_device_by_address(
+ self.address, timeout=timeout
)
+ if device:
+ self._device_info = device.details
+ else:
+ raise BleakError(
+ "Device with address {} was not found".format(self.address)
+ )
+
logger.debug("Connecting to BLE device @ {}".format(self.address))
manager = self._device_info.manager().delegate()
await manager.connect_(self._device_info)
- manager.disconnected_callback = self._disconnect_callback_client
+ manager.disconnected_callback = self._disconnected_callback_client
# Now get services
await self.get_services()
return True
+ def _disconnected_callback_client(self):
+ """
+ Callback for device disconnection. Bleak callback sends one argument as client. This is wrapper function
+ that gets called from the CentralManager and call actual disconnected_callback by sending client as argument
+ """
+ logger.debug("Received disconnection callback...")
+
+ if self._disconnected_callback is not None:
+ self._disconnected_callback(self)
+
async def disconnect(self) -> bool:
"""Disconnect from the peripheral device"""
manager = self._device_info.manager().delegate()
@@ -101,26 +117,6 @@ async def is_connected(self) -> bool:
manager = self._device_info.manager().delegate()
return manager.isConnected
- def set_disconnected_callback(
- self, callback: Callable[[BaseBleakClient], None], **kwargs
- ) -> None:
- """Set the disconnected callback.
- Args:
- callback: callback to be called on disconnection.
-
- """
- self._disconnected_callback = callback
-
- def _disconnect_callback_client(self):
- """
- Callback for device disconnection. Bleak callback sends one argument as client. This is wrapper function
- that gets called from the CentralManager and call actual disconnected_callback by sending client as argument
- """
- logger.debug("Received disconnection callback...")
-
- if self._disconnected_callback is not None:
- self._disconnected_callback(self)
-
async def pair(self, *args, **kwargs) -> bool:
"""Attempt to pair with a peripheral.
@@ -132,10 +128,9 @@ async def pair(self, *args, **kwargs) -> bool:
Reference:
- - https://stackoverflow.com/questions/25254932/can-you-pair-a-bluetooth-le-device-in-an-ios-app
- - https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral/BestPracticesForSettingUpYourIOSDeviceAsAPeripheral.html#//apple_ref/doc/uid/TP40013257-CH5-SW1
- - https://stackoverflow.com/questions/47546690/ios-bluetooth-pairing-request-dialog-can-i-know-the-users-choice
-
+ - `Apple Docs `_
+ - `Stack Overflow post #1 `_
+ - `Stack Overflow post #2 `_
Returns:
Boolean regarding success of pairing.
diff --git a/bleak/backends/corebluetooth/device.py b/bleak/backends/corebluetooth/device.py
index 453c437b..2e0e022e 100644
--- a/bleak/backends/corebluetooth/device.py
+++ b/bleak/backends/corebluetooth/device.py
@@ -47,7 +47,13 @@ def _update_uuids(self, advertisementData: NSDictionary):
if not cbuuids:
return
# converting to lower case to match other platforms
- self.metadata["uuids"] = [str(u).lower() for u in cbuuids]
+ chuuids = [str(u).lower() for u in cbuuids]
+ if "uuids" in self.metadata:
+ for uuid in chuuids:
+ if not uuid in self.metadata["uuids"]:
+ self.metadata["uuids"].append(uuid)
+ else:
+ self.metadata["uuids"] = chuuids
def _update_manufacturer(self, advertisementData: NSDictionary):
mfg_bytes = advertisementData.get("kCBAdvDataManufacturerData")
diff --git a/bleak/backends/corebluetooth/scanner.py b/bleak/backends/corebluetooth/scanner.py
index 6951803b..9faf3841 100644
--- a/bleak/backends/corebluetooth/scanner.py
+++ b/bleak/backends/corebluetooth/scanner.py
@@ -62,6 +62,17 @@ async def stop(self):
logger.warning("stopScan method could not be called: {0}".format(e))
async def set_scanning_filter(self, **kwargs):
+ """Set scanning filter for the scanner.
+
+ .. note::
+
+ This is not implemented for macOS yet.
+
+ Raises:
+
+ ``NotImplementedError``
+
+ """
raise NotImplementedError(
"Need to evaluate which macOS versions to support first..."
)
@@ -142,7 +153,9 @@ def stop_if_detected(peripheral, advertisement_data, rssi):
if str(peripheral.identifier().UUIDString()).lower() == device_identifier:
loop.call_soon_threadsafe(stop_scanning_event.set)
- return await scanner._find_device_by_address(device_identifier, stop_scanning_event, stop_if_detected, timeout)
+ return await scanner._find_device_by_address(
+ device_identifier, stop_scanning_event, stop_if_detected, timeout
+ )
# macOS specific methods
diff --git a/bleak/backends/device.py b/bleak/backends/device.py
index 275bd12f..bdd45a1e 100644
--- a/bleak/backends/device.py
+++ b/bleak/backends/device.py
@@ -14,19 +14,23 @@ class BLEDevice(object):
a `discover` call.
- When using Windows backend, `details` attribute is a
- `Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisement` object, unless
+ ``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisement`` object, unless
it is created with the Windows.Devices.Enumeration discovery method, then is is a
- `Windows.Devices.Enumeration.DeviceInformation`
- - When using Linux backend, `details` attribute is a
- dict with keys `path` which has the string path to the DBus device object and `props`
+ ``Windows.Devices.Enumeration.DeviceInformation``.
+ - When using Linux backend, ``details`` attribute is a
+ dict with keys ``path`` which has the string path to the DBus device object and ``props``
which houses the properties dictionary of the D-Bus Device.
- - When using macOS backend, `details` attribute will be a CBPeripheral object
+ - When using macOS backend, ``details`` attribute will be a CBPeripheral object.
"""
def __init__(self, address, name, details=None, **kwargs):
+ #: The Bluetooth address of the device on this machine.
self.address = address
+ #: The advertised name of the device.
self.name = name if name else "Unknown"
+ #: The OS native details required for connecting to the device.
self.details = details
+ #: Device specific details. Contains a ``uuids`` key which is a list of service UUIDs and a ``manufacturer_data`` field with a bytes-object from the advertised data.
self.metadata = kwargs
@property
diff --git a/bleak/backends/dotnet/client.py b/bleak/backends/dotnet/client.py
index 11258aa5..64e16a85 100644
--- a/bleak/backends/dotnet/client.py
+++ b/bleak/backends/dotnet/client.py
@@ -11,11 +11,15 @@
from functools import wraps
from typing import Callable, Any, Union
+from bleak.backends.device import BLEDevice
from bleak.backends.dotnet.scanner import BleakScannerDotNet
from bleak.exc import BleakError, BleakDotNetTaskError, CONTROLLER_ERROR_CODES
from bleak.backends.client import BaseBleakClient
-from bleak.backends.dotnet.discovery import discover
-from bleak.backends.dotnet.utils import wrap_IAsyncOperation
+from bleak.backends.dotnet.utils import (
+ BleakDataReader,
+ BleakDataWriter,
+ wrap_IAsyncOperation,
+)
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.service import BleakGATTServiceCollection
@@ -29,7 +33,7 @@
from BleakBridge import Bridge
# Import of other CLR components needed.
-from System import Array, Byte, UInt64
+from System import UInt64, Object
from Windows.Foundation import IAsyncOperation, TypedEventHandler
from Windows.Storage.Streams import DataReader, DataWriter, IBuffer
from Windows.Devices.Enumeration import (
@@ -91,18 +95,21 @@ class BleakClientDotNet(BaseBleakClient):
Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel.
Args:
- address (str): The Bluetooth address of the BLE peripheral to connect to.
+ address_or_ble_device (`BLEDevice` or str): The Bluetooth address of the BLE peripheral to connect to or the `BLEDevice` object representing it.
Keyword Args:
- timeout (float): Timeout for required ``discover`` call. Defaults to 2.0.
+ timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.
"""
- def __init__(self, address: str, **kwargs):
- super(BleakClientDotNet, self).__init__(address, **kwargs)
+ def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs):
+ super(BleakClientDotNet, self).__init__(address_or_ble_device, **kwargs)
# Backend specific. Python.NET objects.
- self._device_info = None
+ if isinstance(address_or_ble_device, BLEDevice):
+ self._device_info = address_or_ble_device.details.BluetoothAddress
+ else:
+ self._device_info = None
self._requester = None
self._bridge = None
@@ -122,7 +129,7 @@ async def connect(self, **kwargs) -> bool:
"""Connect to the specified GATT server.
Keyword Args:
- timeout (float): Timeout for required ``find_device_by_address`` call. Defaults to maximally 10.0 seconds.
+ timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0.
Returns:
Boolean representing connection status.
@@ -132,17 +139,19 @@ async def connect(self, **kwargs) -> bool:
self._bridge = Bridge()
# Try to find the desired device.
- timeout = kwargs.get("timeout", self._timeout)
- device = await BleakScannerDotNet.find_device_by_address(
- self.address, timeout=timeout)
-
- if device:
- self._device_info = device.details.BluetoothAddress
- else:
- raise BleakError(
- "Device with address {0} was not found.".format(self.address)
+ if self._device_info is None:
+ timeout = kwargs.get("timeout", self._timeout)
+ device = await BleakScannerDotNet.find_device_by_address(
+ self.address, timeout=timeout
)
+ if device:
+ self._device_info = device.details.BluetoothAddress
+ else:
+ raise BleakError(
+ "Device with address {0} was not found.".format(self.address)
+ )
+
logger.debug("Connecting to BLE device @ {0}".format(self.address))
args = [UInt64(self._device_info)]
@@ -159,8 +168,17 @@ async def connect(self, **kwargs) -> bool:
return_type=BluetoothLEDevice,
)
+ loop = asyncio.get_event_loop()
+
def _ConnectionStatusChanged_Handler(sender, args):
- logger.debug("_ConnectionStatusChanged_Handler: " + args.ToString())
+ logger.debug(
+ "_ConnectionStatusChanged_Handler: %d", sender.ConnectionStatus
+ )
+ if (
+ sender.ConnectionStatus == BluetoothConnectionStatus.Disconnected
+ and self._disconnected_callback
+ ):
+ loop.call_soon_threadsafe(self._disconnected_callback, self)
self._requester.ConnectionStatusChanged += _ConnectionStatusChanged_Handler
@@ -238,19 +256,6 @@ async def is_connected(self) -> bool:
else:
return False
- def set_disconnected_callback(
- self, callback: Callable[[BaseBleakClient], None], **kwargs
- ) -> None:
- """Set the disconnected callback.
-
- N.B. This is not implemented in the .NET backend yet.
-
- Args:
- callback: callback to be called on disconnection.
-
- """
- raise NotImplementedError("This is not implemented in the .NET backend yet")
-
async def pair(self, protection_level=None, **kwargs) -> bool:
"""Attempts to pair with the device.
@@ -376,7 +381,9 @@ async def get_services(self) -> BleakGATTServiceCollection:
"Could not get GATT services: {0} (Error: 0x{1:02X}: {2})".format(
_communication_statues.get(services_result.Status, ""),
services_result.ProtocolError,
- CONTROLLER_ERROR_CODES.get(services_result.ProtocolError, "Unknown")
+ CONTROLLER_ERROR_CODES.get(
+ services_result.ProtocolError, "Unknown"
+ ),
)
)
else:
@@ -406,7 +413,9 @@ async def get_services(self) -> BleakGATTServiceCollection:
characteristics_result.Status, ""
),
characteristics_result.ProtocolError,
- CONTROLLER_ERROR_CODES.get(characteristics_result.ProtocolError, "Unknown")
+ CONTROLLER_ERROR_CODES.get(
+ characteristics_result.ProtocolError, "Unknown"
+ ),
)
)
else:
@@ -441,8 +450,8 @@ async def get_services(self) -> BleakGATTServiceCollection:
),
descriptors_result.ProtocolError,
CONTROLLER_ERROR_CODES.get(
- descriptors_result.ProtocolError,
- "Unknown")
+ descriptors_result.ProtocolError, "Unknown"
+ ),
)
)
else:
@@ -506,11 +515,8 @@ async def read_gatt_char(
return_type=GattReadResult,
)
if read_result.Status == GattCommunicationStatus.Success:
- reader = DataReader.FromBuffer(IBuffer(read_result.Value))
- output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength)
- reader.ReadBytes(output)
- value = bytearray(output)
- reader.Dispose()
+ with BleakDataReader(read_result.Value) as reader:
+ value = bytearray(reader.read())
logger.debug(
"Read Characteristic {0} : {1}".format(characteristic.uuid, value)
)
@@ -522,8 +528,8 @@ async def read_gatt_char(
_communication_statues.get(read_result.Status, ""),
read_result.ProtocolError,
CONTROLLER_ERROR_CODES.get(
- read_result.ProtocolError,
- "Unknown")
+ read_result.ProtocolError, "Unknown"
+ ),
)
)
else:
@@ -564,11 +570,8 @@ async def read_gatt_descriptor(
return_type=GattReadResult,
)
if read_result.Status == GattCommunicationStatus.Success:
- reader = DataReader.FromBuffer(IBuffer(read_result.Value))
- output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength)
- reader.ReadBytes(output)
- value = bytearray(output)
- reader.Dispose()
+ with BleakDataReader(read_result.Value) as reader:
+ value = bytearray(reader.read())
logger.debug("Read Descriptor {0} : {1}".format(handle, value))
else:
if read_result.Status == GattCommunicationStatus.ProtocolError:
@@ -578,8 +581,8 @@ async def read_gatt_descriptor(
_communication_statues.get(read_result.Status, ""),
read_result.ProtocolError,
CONTROLLER_ERROR_CODES.get(
- read_result.ProtocolError,
- "Unknown")
+ read_result.ProtocolError, "Unknown"
+ ),
)
)
else:
@@ -615,21 +618,21 @@ async def write_gatt_char(
if not characteristic:
raise BleakError("Characteristic {} was not found!".format(char_specifier))
- writer = DataWriter()
- writer.WriteBytes(Array[Byte](data))
- response = (
- GattWriteOption.WriteWithResponse
- if response
- else GattWriteOption.WriteWithoutResponse
- )
- write_result = await wrap_IAsyncOperation(
- IAsyncOperation[GattWriteResult](
- characteristic.obj.WriteValueWithResultAsync(
- writer.DetachBuffer(), response
- )
- ),
- return_type=GattWriteResult,
- )
+ with BleakDataWriter(data) as writer:
+ response = (
+ GattWriteOption.WriteWithResponse
+ if response
+ else GattWriteOption.WriteWithoutResponse
+ )
+ write_result = await wrap_IAsyncOperation(
+ IAsyncOperation[GattWriteResult](
+ characteristic.obj.WriteValueWithResultAsync(
+ writer.detach_buffer(), response
+ )
+ ),
+ return_type=GattWriteResult,
+ )
+
if write_result.Status == GattCommunicationStatus.Success:
logger.debug(
"Write Characteristic {0} : {1}".format(characteristic.uuid, data)
@@ -643,8 +646,8 @@ async def write_gatt_char(
_communication_statues.get(write_result.Status, ""),
write_result.ProtocolError,
CONTROLLER_ERROR_CODES.get(
- write_result.ProtocolError,
- "Unknown")
+ write_result.ProtocolError, "Unknown"
+ ),
)
)
else:
@@ -668,14 +671,14 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None:
if not descriptor:
raise BleakError("Descriptor with handle {0} was not found!".format(handle))
- writer = DataWriter()
- writer.WriteBytes(Array[Byte](data))
- write_result = await wrap_IAsyncOperation(
- IAsyncOperation[GattWriteResult](
- descriptor.obj.WriteValueAsync(writer.DetachBuffer())
- ),
- return_type=GattWriteResult,
- )
+ with BleakDataWriter(data) as writer:
+ write_result = await wrap_IAsyncOperation(
+ IAsyncOperation[GattWriteResult](
+ descriptor.obj.WriteValueAsync(writer.DetachBuffer())
+ ),
+ return_type=GattWriteResult,
+ )
+
if write_result.Status == GattCommunicationStatus.Success:
logger.debug("Write Descriptor {0} : {1}".format(handle, data))
else:
@@ -687,8 +690,8 @@ async def write_gatt_descriptor(self, handle: int, data: bytearray) -> None:
_communication_statues.get(write_result.Status, ""),
write_result.ProtocolError,
CONTROLLER_ERROR_CODES.get(
- write_result.ProtocolError,
- "Unknown")
+ write_result.ProtocolError, "Unknown"
+ ),
)
)
else:
@@ -856,10 +859,8 @@ def _notification_wrapper(func: Callable, loop: asyncio.AbstractEventLoop):
def dotnet_notification_parser(sender: Any, args: Any):
# Return only the UUID string representation as sender.
# Also do a conversion from System.Bytes[] to bytearray.
- reader = DataReader.FromBuffer(args.CharacteristicValue)
- output = Array.CreateInstance(Byte, reader.UnconsumedBufferLength)
- reader.ReadBytes(output)
- reader.Dispose()
+ with BleakDataReader(args.CharacteristicValue) as reader:
+ output = reader.read()
return loop.call_soon_threadsafe(
func, sender.AttributeHandle, bytearray(output)
diff --git a/bleak/backends/dotnet/discovery.py b/bleak/backends/dotnet/discovery.py
index 5a105335..56e39c46 100644
--- a/bleak/backends/dotnet/discovery.py
+++ b/bleak/backends/dotnet/discovery.py
@@ -23,7 +23,8 @@
BluetoothLEScanningMode,
BluetoothLEAdvertisementType,
)
-from Windows.Storage.Streams import DataReader, IBuffer
+
+from bleak.backends.dotnet.utils import BleakDataReader
logger = logging.getLogger(__name__)
_here = pathlib.Path(__file__).parent
@@ -116,12 +117,8 @@ def AdvertisementWatcher_Stopped(sender, e):
uuids.append(u.ToString())
data = {}
for m in d.Advertisement.ManufacturerData:
- md = IBuffer(m.Data)
- b = Array.CreateInstance(Byte, md.Length)
- reader = DataReader.FromBuffer(md)
- reader.ReadBytes(b)
- data[m.CompanyId] = bytes(b)
- reader.Dispose()
+ with BleakDataReader(m.Data) as reader:
+ data[m.CompanyId] = reader.read()
local_name = d.Advertisement.LocalName
if not local_name and d.BluetoothAddress in scan_responses:
local_name = scan_responses[d.BluetoothAddress].Advertisement.LocalName
diff --git a/bleak/backends/dotnet/scanner.py b/bleak/backends/dotnet/scanner.py
index a63b3c32..066097e1 100644
--- a/bleak/backends/dotnet/scanner.py
+++ b/bleak/backends/dotnet/scanner.py
@@ -6,20 +6,19 @@
from typing import Callable, Any, Union, List
from bleak.backends.device import BLEDevice
+from bleak.backends.dotnet.utils import BleakDataReader
from bleak.exc import BleakError, BleakDotNetTaskError
from bleak.backends.scanner import BaseBleakScanner
# Import of Bleak CLR->UWP Bridge. It is not needed here, but it enables loading of Windows.Devices
from BleakBridge import Bridge
-from System import Array, Byte
-from Windows.Devices import Enumeration
from Windows.Devices.Bluetooth.Advertisement import (
BluetoothLEAdvertisementWatcher,
BluetoothLEScanningMode,
BluetoothLEAdvertisementType,
)
-from Windows.Storage.Streams import DataReader, IBuffer
+from Windows.Foundation import TypedEventHandler
logger = logging.getLogger(__name__)
_here = pathlib.Path(__file__).parent
@@ -41,17 +40,20 @@ def _format_event_args(e):
class BleakScannerDotNet(BaseBleakScanner):
"""The native Windows Bleak BLE Scanner.
- Implemented using `pythonnet `_, a package that provides an integration to the .NET
- Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel.
+ Implemented using `pythonnet `_, a package that provides an integration to
+ the .NET Common Language Runtime (CLR). Therefore, much of the code below has a distinct C# feel.
Keyword Args:
- scanning mode (str): Set to "Passive" to avoid the "Active" scanning mode.
- SignalStrengthFilter (Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter): A
- BluetoothSignalStrengthFilter object used for configuration of Bluetooth
- LE advertisement filtering that uses signal strength-based filtering.
- AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A
- BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE
- advertisement filtering that uses payload section-based filtering.
+
+ scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode.
+
+ SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A
+ BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement
+ filtering that uses signal strength-based filtering.
+
+ AdvertisementFilter (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter``): A
+ BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE advertisement
+ filtering that uses payload section-based filtering.
"""
@@ -120,12 +122,12 @@ async def set_scanning_filter(self, **kwargs):
"""Set a scanning filter for the BleakScanner.
Keyword Args:
- SignalStrengthFilter (Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter): A
- BluetoothSignalStrengthFilter object used for configuration of Bluetooth
- LE advertisement filtering that uses signal strength-based filtering.
- AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A
- BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE
- advertisement filtering that uses payload section-based filtering.
+ SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A
+ BluetoothSignalStrengthFilter object used for configuration of Bluetooth
+ LE advertisement filtering that uses signal strength-based filtering.
+ AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A
+ BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE
+ advertisement filtering that uses payload section-based filtering.
"""
if "SignalStrengthFilter" in kwargs:
@@ -158,11 +160,8 @@ def parse_eventargs(event_args):
uuids.append(u.ToString())
data = {}
for m in event_args.Advertisement.ManufacturerData:
- md = IBuffer(m.Data)
- b = Array.CreateInstance(Byte, md.Length)
- reader = DataReader.FromBuffer(md)
- reader.ReadBytes(b)
- data[m.CompanyId] = bytes(b)
+ with BleakDataReader(m.Data) as reader:
+ data[m.CompanyId] = reader.read()
local_name = event_args.Advertisement.LocalName
return BLEDevice(
bdaddr, local_name, event_args, uuids=uuids, manufacturer_data=data
@@ -176,8 +175,8 @@ def register_detection_callback(self, callback: Callable):
Args:
callback: Function accepting two arguments:
- sender (Windows.Devices.Bluetooth.AdvertisementBluetoothLEAdvertisementWatcher) and
- eventargs (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementReceivedEventArgs)
+ sender (``Windows.Devices.Bluetooth.AdvertisementBluetoothLEAdvertisementWatcher``) and
+ eventargs (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementReceivedEventArgs``)
"""
self._callback = callback
@@ -215,19 +214,26 @@ async def find_device_by_address(
"""A convenience method for obtaining a ``BLEDevice`` object specified by Bluetooth address.
Args:
+
device_identifier (str): The Bluetooth address of the Bluetooth peripheral.
- timeout (float): Optional timeout to wait for detection of specified peripheral before giving up. Defaults to 10.0 seconds.
+
+ timeout (float): Optional timeout to wait for detection of specified peripheral
+ before giving up. Defaults to 10.0 seconds.
Keyword Args:
- scanning mode (str): Set to "Passive" to avoid the "Active" scanning mode.
- SignalStrengthFilter (Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter): A
- BluetoothSignalStrengthFilter object used for configuration of Bluetooth
- LE advertisement filtering that uses signal strength-based filtering.
- AdvertisementFilter (Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter): A
- BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE
- advertisement filtering that uses payload section-based filtering.
+
+ scanning mode (str): Set to ``Passive`` to avoid the ``Active`` scanning mode.
+
+ SignalStrengthFilter (``Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter``): A
+ BluetoothSignalStrengthFilter object used for configuration of Bluetooth LE advertisement
+ filtering that uses signal strength-based filtering.
+
+ AdvertisementFilter (``Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter``): A
+ BluetoothLEAdvertisementFilter object used for configuration of Bluetooth LE
+ advertisement filtering that uses payload section-based filtering.
Returns:
+
The ``BLEDevice`` sought or ``None`` if not detected.
"""
@@ -241,4 +247,6 @@ def stop_if_detected(sender, event_args):
if event_args.BluetoothAddress == ulong_id:
loop.call_soon_threadsafe(stop_scanning_event.set)
- return await scanner._find_device_by_address(device_identifier, stop_scanning_event, stop_if_detected, timeout)
+ return await scanner._find_device_by_address(
+ device_identifier, stop_scanning_event, stop_if_detected, timeout
+ )
diff --git a/bleak/backends/dotnet/utils.py b/bleak/backends/dotnet/utils.py
index 1b43cc02..f8af8acc 100644
--- a/bleak/backends/dotnet/utils.py
+++ b/bleak/backends/dotnet/utils.py
@@ -18,6 +18,8 @@
IAsyncOperation,
AsyncStatus,
)
+from System import Array, Byte
+from Windows.Storage.Streams import DataReader, DataWriter, IBuffer
async def wrap_Task(task):
@@ -74,3 +76,48 @@ async def wrap_IAsyncOperation(op, return_type):
else:
# TODO: Handle IsCancelled.
raise BleakDotNetTaskError("IAsyncOperation Status: {0}".format(op.Status))
+
+
+class BleakDataReader:
+ def __init__(self, buffer_com_object):
+
+ self.reader = None
+ self.buffer = IBuffer(buffer_com_object)
+
+ def __enter__(self):
+ self.reader = DataReader.FromBuffer(self.buffer)
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.reader.DetachBuffer()
+ self.reader.Dispose()
+ self.reader = None
+ self.buffer = None
+
+ def read(self) -> bytes:
+ b = Array.CreateInstance(Byte, self.reader.UnconsumedBufferLength)
+ self.reader.ReadBytes(b)
+ py_b = bytes(b)
+ del b
+ return py_b
+
+
+class BleakDataWriter:
+ def __init__(self, data):
+ self.data = data
+
+ def __enter__(self):
+ self.writer = DataWriter()
+ self.writer.WriteBytes(Array[Byte](self.data))
+ return self
+
+ def detach_buffer(self):
+ return self.writer.DetachBuffer()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ try:
+ self.writer.Dispose()
+ except:
+ pass
+ del self.writer
+ self.writer = None
diff --git a/bleak/backends/scanner.py b/bleak/backends/scanner.py
index 8bde1f6e..cf77b3bf 100644
--- a/bleak/backends/scanner.py
+++ b/bleak/backends/scanner.py
@@ -74,7 +74,9 @@ async def get_discovered_devices(self) -> List[BLEDevice]:
@classmethod
@abc.abstractmethod
- async def find_device_by_address(cls, device_identifier: str, timeout: float = 10.0) -> BLEDevice:
+ async def find_device_by_address(
+ cls, device_identifier: str, timeout: float = 10.0
+ ) -> BLEDevice:
"""A convenience method for obtaining a ``BLEDevice`` object specified by Bluetooth address or (macOS) UUID address.
Args:
@@ -87,7 +89,9 @@ async def find_device_by_address(cls, device_identifier: str, timeout: float = 1
"""
raise NotImplementedError()
- async def _find_device_by_address(self, device_identifier, stop_scanning_event, stop_if_detected_callback, timeout):
+ async def _find_device_by_address(
+ self, device_identifier, stop_scanning_event, stop_if_detected_callback, timeout
+ ):
"""Internal method for performing find by address work."""
self.register_detection_callback(stop_if_detected_callback)
@@ -107,4 +111,3 @@ async def _find_device_by_address(self, device_identifier, stop_scanning_event,
await self.stop()
return device
-
diff --git a/bleak/backends/service.py b/bleak/backends/service.py
index 79c678aa..1865ff71 100644
--- a/bleak/backends/service.py
+++ b/bleak/backends/service.py
@@ -147,8 +147,8 @@ def get_characteristic(
def add_descriptor(self, descriptor: BleakGATTDescriptor):
"""Add a :py:class:`~BleakGATTDescriptor` to the service collection.
- Should not be used by end user, but rather by `bleak` itself.
- """
+ Should not be used by end user, but rather by `bleak` itself.
+ """
if descriptor.handle not in self.__descriptors:
self.__descriptors[descriptor.handle] = descriptor
self.__characteristics[descriptor.characteristic_handle].add_descriptor(
diff --git a/bleak/uuids.py b/bleak/uuids.py
index 8d800379..0e02bdaf 100644
--- a/bleak/uuids.py
+++ b/bleak/uuids.py
@@ -636,71 +636,71 @@
0xFFFE: "Alliance for Wireless Power (A4WP)",
0xFFFD: "Fast IDentity Online Alliance (FIDO)",
# Mesh Characteristics
- 0x2AE0: 'Average Current',
- 0x2AE1: 'Average Voltage',
- 0x2AE2: 'Boolean',
- 0x2AE3: 'Chromatic Distance From Planckian',
- 0x2B1C: 'Chromaticity Coordinate',
- 0x2AE4: 'Chromaticity Coordinates',
- 0x2AE5: 'Chromaticity In CCT And Duv Values',
- 0x2AE6: 'Chromaticity Tolerance',
- 0x2AE7: 'CIE 13.3-1995 Color Rendering Index',
- 0x2AE8: 'Coefficient',
- 0x2AE9: 'Correlated Color Temperature',
- 0x2AEA: 'Count 16',
- 0x2AEB: 'Count 24',
- 0x2AEC: 'Country Code',
- 0x2AED: 'Date UTC',
- 0x2AEE: 'Electric Current',
- 0x2AEF: 'Electric Current Range',
- 0x2AF0: 'Electric Current Specification',
- 0x2AF1: 'Electric Current Statistics',
- 0x2AF2: 'Energy',
- 0x2AF3: 'Energy In A Period Of Day',
- 0x2AF4: 'Event Statistics',
- 0x2AF5: 'Fixed String 16',
- 0x2AF6: 'Fixed String 24',
- 0x2AF7: 'Fixed String 36',
- 0x2AF8: 'Fixed String 8',
- 0x2AF9: 'Generic Level',
- 0x2AFA: 'Global Trade Item Number',
- 0x2AFB: 'Illuminance',
- 0x2AFC: 'Luminous Efficacy',
- 0x2AFD: 'Luminous Energy',
- 0x2AFE: 'Luminous Exposure',
- 0x2AFF: 'Luminous Flux',
- 0x2B00: 'Luminous Flux Range',
- 0x2B01: 'Luminous Intensity',
- 0x2B02: 'Mass Flow',
- 0x2ADB: 'Mesh Provisioning Data In',
- 0x2ADC: 'Mesh Provisioning Data Out',
- 0x2ADD: 'Mesh Proxy Data In',
- 0x2ADE: 'Mesh Proxy Data Out',
- 0x2B03: 'Perceived Lightness',
- 0x2B04: 'Percentage 8',
- 0x2B05: 'Power',
- 0x2B06: 'Power Specification',
- 0x2B07: 'Relative Runtime In A Current Range',
- 0x2B08: 'Relative Runtime In A Generic Level Range',
- 0x2B0B: 'Relative Value In A Period of Day',
- 0x2B0C: 'Relative Value In A Temperature Range',
- 0x2B09: 'Relative Value In A Voltage Range',
- 0x2B0A: 'Relative Value In An Illuminance Range',
- 0x2B0D: 'Temperature 8',
- 0x2B0E: 'Temperature 8 In A Period Of Day',
- 0x2B0F: 'Temperature 8 Statistics',
- 0x2B10: 'Temperature Range',
- 0x2B11: 'Temperature Statistics',
- 0x2B12: 'Time Decihour 8',
- 0x2B13: 'Time Exponential 8',
- 0x2B14: 'Time Hour 24',
- 0x2B15: 'Time Millisecond 24',
- 0x2B16: 'Time Second 16',
- 0x2B17: 'Time Second 8',
- 0x2B18: 'Voltage',
- 0x2B19: 'Voltage Specification',
- 0x2B1A: 'Voltage Statistics',
- 0x2B1B: 'Volume Flow',
+ 0x2AE0: "Average Current",
+ 0x2AE1: "Average Voltage",
+ 0x2AE2: "Boolean",
+ 0x2AE3: "Chromatic Distance From Planckian",
+ 0x2B1C: "Chromaticity Coordinate",
+ 0x2AE4: "Chromaticity Coordinates",
+ 0x2AE5: "Chromaticity In CCT And Duv Values",
+ 0x2AE6: "Chromaticity Tolerance",
+ 0x2AE7: "CIE 13.3-1995 Color Rendering Index",
+ 0x2AE8: "Coefficient",
+ 0x2AE9: "Correlated Color Temperature",
+ 0x2AEA: "Count 16",
+ 0x2AEB: "Count 24",
+ 0x2AEC: "Country Code",
+ 0x2AED: "Date UTC",
+ 0x2AEE: "Electric Current",
+ 0x2AEF: "Electric Current Range",
+ 0x2AF0: "Electric Current Specification",
+ 0x2AF1: "Electric Current Statistics",
+ 0x2AF2: "Energy",
+ 0x2AF3: "Energy In A Period Of Day",
+ 0x2AF4: "Event Statistics",
+ 0x2AF5: "Fixed String 16",
+ 0x2AF6: "Fixed String 24",
+ 0x2AF7: "Fixed String 36",
+ 0x2AF8: "Fixed String 8",
+ 0x2AF9: "Generic Level",
+ 0x2AFA: "Global Trade Item Number",
+ 0x2AFB: "Illuminance",
+ 0x2AFC: "Luminous Efficacy",
+ 0x2AFD: "Luminous Energy",
+ 0x2AFE: "Luminous Exposure",
+ 0x2AFF: "Luminous Flux",
+ 0x2B00: "Luminous Flux Range",
+ 0x2B01: "Luminous Intensity",
+ 0x2B02: "Mass Flow",
+ 0x2ADB: "Mesh Provisioning Data In",
+ 0x2ADC: "Mesh Provisioning Data Out",
+ 0x2ADD: "Mesh Proxy Data In",
+ 0x2ADE: "Mesh Proxy Data Out",
+ 0x2B03: "Perceived Lightness",
+ 0x2B04: "Percentage 8",
+ 0x2B05: "Power",
+ 0x2B06: "Power Specification",
+ 0x2B07: "Relative Runtime In A Current Range",
+ 0x2B08: "Relative Runtime In A Generic Level Range",
+ 0x2B0B: "Relative Value In A Period of Day",
+ 0x2B0C: "Relative Value In A Temperature Range",
+ 0x2B09: "Relative Value In A Voltage Range",
+ 0x2B0A: "Relative Value In An Illuminance Range",
+ 0x2B0D: "Temperature 8",
+ 0x2B0E: "Temperature 8 In A Period Of Day",
+ 0x2B0F: "Temperature 8 Statistics",
+ 0x2B10: "Temperature Range",
+ 0x2B11: "Temperature Statistics",
+ 0x2B12: "Time Decihour 8",
+ 0x2B13: "Time Exponential 8",
+ 0x2B14: "Time Hour 24",
+ 0x2B15: "Time Millisecond 24",
+ 0x2B16: "Time Second 16",
+ 0x2B17: "Time Second 8",
+ 0x2B18: "Voltage",
+ 0x2B19: "Voltage Specification",
+ 0x2B1A: "Voltage Statistics",
+ 0x2B1B: "Volume Flow",
}
uuid128_dict = {
diff --git a/docs/api.rst b/docs/api.rst
index 8dbbdb47..8bfa4fd2 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -1,28 +1,59 @@
Interfaces, exceptions and utils
================================
-Connection Client Interface
----------------------------
+Connection Clients
+------------------
-.. automodule:: bleak.backends.client
+Windows
+~~~~~~~
+
+.. automodule:: bleak.backends.dotnet.client
+ :members:
+
+macOS
+~~~~~
+
+.. automodule:: bleak.backends.corebluetooth.client
:members:
-Scanning Client Interface
--------------------------
+Linux Distributions with BlueZ
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: bleak.backends.bluezdbus.client
+ :members:
+
+Scanning Clients
+----------------
+
+Windows
+~~~~~~~
-.. automodule:: bleak.backends.scanner
+.. automodule:: bleak.backends.dotnet.scanner
:members:
-Interface for BLE devices
--------------------------
+macOS
+~~~~~
+
+.. automodule:: bleak.backends.corebluetooth.scanner
+ :members:
+
+Linux Distributions with BlueZ
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. automodule:: bleak.backends.bluezdbus.scanner
+ :members:
+
+
+Class representing BLE devices
+------------------------------
Generated by :py:meth:`bleak.discover` and :py:class:`bleak.backends.scanning.BaseBleakScanner`.
.. automodule:: bleak.backends.device
:members:
-Interfaces for GATT objects
----------------------------
+GATT objects
+------------
.. automodule:: bleak.backends.service
:members:
diff --git a/docs/conf.py b/docs/conf.py
index 37ed91f0..451b463a 100755
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -13,6 +13,26 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
+
+windows_autodoc_mock_import = ["clr", "Windows", "System"]
+linux_autodoc_mock_import = [
+ "twisted",
+ "txdbus",
+]
+macos_autodoc_mock_import = [
+ "objc",
+ "Foundation",
+ "CoreBluetooth",
+ "libdispatch",
+]
+autodoc_mock_imports = list(
+ set(
+ windows_autodoc_mock_import
+ + macos_autodoc_mock_import
+ + linux_autodoc_mock_import
+ )
+)
+
import sys
import os
@@ -40,8 +60,7 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"]
-
+extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.napoleon"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -56,7 +75,7 @@
# General information about the project.
project = u"bleak"
-copyright = u"2018, Henrik Blidh"
+copyright = u"2020, Henrik Blidh"
# The version info for the project you're documenting, acts as replacement
# for |version| and |release|, also used in various other places throughout
@@ -112,6 +131,7 @@
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = "default"
+html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a
# theme further. For a list of options available for each theme, see the
diff --git a/docs/index.rst b/docs/index.rst
index 318d3054..989a49ac 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,7 +1,7 @@
bleak
=====
-.. image:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png
+.. figure:: https://raw.githubusercontent.com/hbldh/bleak/master/Bleak_logo.png
:target: https://github.com/hbldh/bleak
:alt: Bleak Logo
:width: 50%
diff --git a/docs/scanning.rst b/docs/scanning.rst
index 036ad602..dc3dff80 100644
--- a/docs/scanning.rst
+++ b/docs/scanning.rst
@@ -128,6 +128,6 @@ To be written. In the meantime, check
Scanning filter examples in Core Bluetooth backend
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To be implemented. Exists in a draft in `PR #209 `_.
diff --git a/examples/connect_by_bledevice.py b/examples/connect_by_bledevice.py
new file mode 100644
index 00000000..fe9978e5
--- /dev/null
+++ b/examples/connect_by_bledevice.py
@@ -0,0 +1,24 @@
+"""
+Connect by BLEDevice
+"""
+
+import asyncio
+import platform
+
+from bleak import BleakClient, BleakScanner
+
+
+async def print_services(mac_addr: str):
+ device = await BleakScanner.find_device_by_address(mac_addr)
+ async with BleakClient(device) as client:
+ svcs = await client.get_services()
+ print("Services:", svcs)
+
+
+mac_addr = (
+ "24:71:89:cc:09:05"
+ if platform.system() != "Darwin"
+ else "B9EA5233-37EF-4DD6-87A8-2A875E821C46"
+)
+loop = asyncio.get_event_loop()
+loop.run_until_complete(print_services(mac_addr))
diff --git a/examples/disconnect_callback.py b/examples/disconnect_callback.py
index 15002e56..14cbd51c 100644
--- a/examples/disconnect_callback.py
+++ b/examples/disconnect_callback.py
@@ -2,7 +2,7 @@
Disconnect callback
-------------------
-An example showing how the `set_disconnect_callback` can be used on BlueZ backend.
+An example showing how the `set_disconnected_callback` can be used on BlueZ backend.
Updated on 2019-09-07 by hbldh
@@ -10,19 +10,25 @@
import asyncio
-from bleak import BleakClient
+from bleak import BleakClient, discover
-async def show_disconnect_handling(mac_addr: str):
- async with BleakClient(mac_addr) as client:
- disconnected_event = asyncio.Event()
+async def show_disconnect_handling():
+ devs = await discover()
+ if not devs:
+ print("No devices found, try again later.")
+ return
- def disconnect_callback(client, future):
- print("Disconnected callback called!")
- asyncio.get_event_loop().call_soon_threadsafe(disconnected_event.set)
+ disconnected_event = asyncio.Event()
- client.set_disconnected_callback(disconnect_callback)
- print("Sleeping until device disconnects according to BlueZ...")
+ def disconnected_callback(client):
+ print("Disconnected callback called!")
+ disconnected_event.set()
+
+ async with BleakClient(
+ devs[0], disconnected_callback=disconnected_callback
+ ) as client:
+ print("Sleeping until device disconnects...")
await disconnected_event.wait()
print("Connected: {0}".format(await client.is_connected()))
await asyncio.sleep(
@@ -30,6 +36,5 @@ def disconnect_callback(client, future):
) # Sleep a bit longer to allow _cleanup to remove all BlueZ notifications nicely...
-mac_addr = "24:71:89:cc:09:05"
loop = asyncio.get_event_loop()
-loop.run_until_complete(show_disconnect_handling(mac_addr))
+loop.run_until_complete(show_disconnect_handling())
diff --git a/examples/sensortag.py b/examples/sensortag.py
index b8472ec5..32982313 100644
--- a/examples/sensortag.py
+++ b/examples/sensortag.py
@@ -87,7 +87,7 @@
BATTERY_LEVEL_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(
uuid16_dict.get("Battery Level")
)
-KEY_PRESS_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0xffe1)
+KEY_PRESS_UUID = "0000{0:x}-0000-1000-8000-00805f9b34fb".format(0xFFE1)
# I/O test points on SensorTag.
IO_DATA_CHAR_UUID = "f000aa65-0451-4000-b000-000000000000"
IO_CONFIG_CHAR_UUID = "f000aa66-0451-4000-b000-000000000000"
@@ -141,7 +141,7 @@ async def run(address, debug=False):
def keypress_handler(sender, data):
print("{0}: {1}".format(sender, data))
- write_value = bytearray([0xa0])
+ write_value = bytearray([0xA0])
value = await client.read_gatt_char(IO_DATA_CHAR_UUID)
print("I/O Data Pre-Write Value: {0}".format(value))
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 59ed53cd..789fe38a 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -2,6 +2,7 @@ pip>=18.0
bump2version==1.0.0
wheel>=0.32.2
watchdog>=0.8.3
+black>=20.8b1
flake8>=3.5.0
tox>=3.1.3
coverage>=4.5.1
diff --git a/setup.py b/setup.py
index 9dae2bd3..adcb0a1e 100644
--- a/setup.py
+++ b/setup.py
@@ -107,7 +107,6 @@ def run(self):
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",