Skip to content

Commit

Permalink
Merge branch 'develop' into local_abort
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco authored Jul 25, 2023
2 parents 436ddc6 + 8a720ab commit 447fad7
Show file tree
Hide file tree
Showing 28 changed files with 537 additions and 395 deletions.
31 changes: 26 additions & 5 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,40 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
`Unreleased`_
=============

- Fixed ``KeyError`` being raised during waiting for service
Added
-----
* Added ``bleak.uuids.normalize_uuid_16()`` function.
* Added ``bleak.uuids.normalize_uuid_32()`` function.

Changed
-------
* Improved error messages when failing to get services in WinRT backend.
* Improved error messages with enum values in WinRT backend. Fixes #1284.
* Scanner backends modified to allow multiple advertisement callbacks. Merged #1367.
* Changed default handling of the ``response`` argument in ``BleakClient.write_gatt_char``.
Fixes #909.
* Added ``advertisement_data()`` async iterator method to ``BleakScanner``. Merged #1361.
* Added ``scan_iterator.py`` example.

Fixed
-----
* Fix handling all access denied errors when enumerating characteristics on Windows. Fixes #1291.
* Added support for 32bit UUIDs. Fixes #1314.
* Fixed typing for ``BaseBleakScanner`` detection callback.
* Fixed possible crash in ``_stopped_handler()`` in WinRT backend. Fixes #1330.
* Fixed ``KeyError`` being raised during waiting for service
discovery in BlueZ backend.
- Fixed waiting on services to be resolved forever if the device is
* Fixed waiting on services to be resolved forever if the device is
removed from the bus in BlueZ backend.

`0.20.2`_ (2023-04-19)
======================

Fixed
-----
- Fixed ``org.bluez.Error.InProgress`` in characteristic and descriptor read and
* Fixed ``org.bluez.Error.InProgress`` in characteristic and descriptor read and
write methods in BlueZ backend.
- Fixed ``OSError: [WinError -2147483629] The object has been closed`` when
* Fixed ``OSError: [WinError -2147483629] The object has been closed`` when
connecting on Windows. Fixes #1280.

`0.20.1`_ (2023-03-24)
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Connect to a Bluetooth device and read its model number:
from bleak import BleakClient
address = "24:71:89:cc:09:05"
MODEL_NBR_UUID = "00002a24-0000-1000-8000-00805f9b34fb"
MODEL_NBR_UUID = "2A24"
async def main(address):
async with BleakClient(address) as client:
Expand Down
107 changes: 91 additions & 16 deletions bleak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import uuid
from typing import (
TYPE_CHECKING,
AsyncGenerator,
Awaitable,
Callable,
Dict,
Expand Down Expand Up @@ -175,7 +176,17 @@ def register_detection_callback(
FutureWarning,
stacklevel=2,
)
self._backend.register_detection_callback(callback)

try:
unregister = getattr(self, "_unregister_")
except AttributeError:
pass
else:
unregister()

if callback is not None:
unregister = self._backend.register_detection_callback(callback)
setattr(self, "_unregister_", unregister)

async def start(self):
"""Start scanning for devices"""
Expand Down Expand Up @@ -204,6 +215,31 @@ def set_scanning_filter(self, **kwargs):
)
self._backend.set_scanning_filter(**kwargs)

async def advertisement_data(
self,
) -> AsyncGenerator[Tuple[BLEDevice, AdvertisementData], None]:
"""
Yields devices and associated advertising data packets as they are discovered.
.. note::
Ensure that scanning is started before calling this method.
Returns:
An async iterator that yields tuples (:class:`BLEDevice`, :class:`AdvertisementData`).
.. versionadded:: 0.21
"""
devices = asyncio.Queue()

unregister_callback = self._backend.register_detection_callback(
lambda bd, ad: devices.put_nowait((bd, ad))
)
try:
while True:
yield await devices.get()
finally:
unregister_callback()

@overload
@classmethod
async def discover(
Expand All @@ -214,7 +250,7 @@ async def discover(
@overload
@classmethod
async def discover(
cls, timeout: float = 5.0, *, return_adv: Literal[True] = True, **kwargs
cls, timeout: float = 5.0, *, return_adv: Literal[True], **kwargs
) -> Dict[str, Tuple[BLEDevice, AdvertisementData]]:
...

Expand Down Expand Up @@ -360,16 +396,12 @@ async def find_device_by_filter(
the timeout.
"""
found_device_queue: asyncio.Queue[BLEDevice] = asyncio.Queue()

def apply_filter(d: BLEDevice, ad: AdvertisementData):
if filterfunc(d, ad):
found_device_queue.put_nowait(d)

async with cls(detection_callback=apply_filter, **kwargs):
async with cls(**kwargs) as scanner:
try:
async with async_timeout(timeout):
return await found_device_queue.get()
async for bd, ad in scanner.advertisement_data():
if filterfunc(bd, ad):
return bd
except asyncio.TimeoutError:
return None

Expand Down Expand Up @@ -640,23 +672,66 @@ async def write_gatt_char(
self,
char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID],
data: Union[bytes, bytearray, memoryview],
response: bool = False,
response: bool = None,
) -> None:
"""
Perform a write operation on the specified GATT characteristic.
There are two possible kinds of writes. *Write with response* (sometimes
called a *Request*) will write the data then wait for a response from
the remote device. *Write without response* (sometimes called *Command*)
will queue data to be written and return immediately.
Each characteristic may support one kind or the other or both or neither.
Consult the device's documentation or inspect the properties of the
characteristic to find out which kind of writes are supported.
.. tip:: Explicit is better than implicit. Best practice is to always
include an explicit ``response=True`` or ``response=False``
when calling this method.
Args:
char_specifier:
The characteristic to write to, specified by either integer
handle, UUID or directly by the BleakGATTCharacteristic object
representing it.
handle, UUID or directly by the :class:`~bleak.backends.characteristic.BleakGATTCharacteristic`
object representing it. If a device has more than one characteristic
with the same UUID, then attempting to use the UUID wil fail and
a characteristic object must be used instead.
data:
The data to send.
The data to send. When a write-with-response operation is used,
the length of the data is limited to 512 bytes. When a
write-without-response operation is used, the length of the
data is limited to :attr:`~bleak.backends.characteristic.BleakGATTCharacteristic.max_write_without_response_size`.
Any type that supports the buffer protocol can be passed.
response:
If write-with-response operation should be done. Defaults to ``False``.
If ``True``, a write-with-response operation will be used. If
``False``, a write-without-response operation will be used.
If omitted or ``None``, the "best" operation will be used
based on the reported properties of the characteristic.
.. versionchanged:: 0.21
The default behavior when ``response=`` is omitted was changed.
Example::
MY_CHAR_UUID = "1234"
...
await client.write_gatt_char(MY_CHAR_UUID, b"\x00\x01\x02\x03", response=True)
"""
await self._backend.write_gatt_char(char_specifier, data, response)
if isinstance(char_specifier, BleakGATTCharacteristic):
characteristic = char_specifier
else:
characteristic = self.services.get_characteristic(char_specifier)

if not characteristic:
raise BleakError("Characteristic {char_specifier} was not found!")

if response is None:
# if not specified, prefer write-with-response over write-without-
# response if it is available since it is the more reliable write.
response = "write" in characteristic.properties

await self._backend.write_gatt_char(characteristic, data, response)

async def start_notify(
self,
Expand Down
55 changes: 3 additions & 52 deletions bleak/backends/bluezdbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,66 +811,17 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray:

async def write_gatt_char(
self,
char_specifier: Union[BleakGATTCharacteristicBlueZDBus, int, str, UUID],
characteristic: BleakGATTCharacteristic,
data: Union[bytes, bytearray, memoryview],
response: bool = False,
response: bool,
) -> None:
"""Perform a write operation on the specified GATT characteristic.
.. note::
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".
Args:
char_specifier (BleakGATTCharacteristicBlueZDBus, int, str or UUID): The characteristic to write
to, specified by either integer handle, UUID or directly by the
BleakGATTCharacteristicBlueZDBus object representing it.
data (bytes or bytearray): The data to send.
response (bool): If write-with-response operation should be done. Defaults to `False`.
"""
if not self.is_connected:
raise BleakError("Not connected")

if not isinstance(char_specifier, BleakGATTCharacteristicBlueZDBus):
characteristic = self.services.get_characteristic(char_specifier)
else:
characteristic = char_specifier

if not characteristic:
raise BleakError("Characteristic {0} was not found!".format(char_specifier))
if (
"write" not in characteristic.properties
and "write-without-response" not in characteristic.properties
):
raise BleakError(
"Characteristic %s does not support write operations!"
% str(characteristic.uuid)
)
if not response and "write-without-response" not in characteristic.properties:
response = True
# Force response here, since the device only supports that.
if (
response
and "write" not in characteristic.properties
and "write-without-response" in characteristic.properties
):
response = False
logger.warning(
"Characteristic %s does not support Write with response. Trying without..."
% str(characteristic.uuid)
)

# See docstring for details about this handling.
if not response and not BlueZFeatures.can_write_without_response:
raise BleakError("Write without response requires at least BlueZ 5.46")

if response or not BlueZFeatures.write_without_response_workaround_needed:
while True:
assert self._bus
Expand Down
5 changes: 1 addition & 4 deletions bleak/backends/bluezdbus/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,7 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None:
advertisement_data,
)

if self._callback is None:
return

self._callback(device, advertisement_data)
self.call_detection_callbacks(device, advertisement_data)

def _handle_device_removed(self, device_path: str) -> None:
"""
Expand Down
16 changes: 7 additions & 9 deletions bleak/backends/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,19 +183,17 @@ async def read_gatt_descriptor(self, handle: int, **kwargs) -> bytearray:
@abc.abstractmethod
async def write_gatt_char(
self,
char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID],
characteristic: BleakGATTCharacteristic,
data: Union[bytes, bytearray, memoryview],
response: bool = False,
response: bool,
) -> None:
"""Perform a write operation on the specified GATT characteristic.
"""
Perform a write operation on the specified GATT characteristic.
Args:
char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write
to, specified by either integer handle, UUID or directly by the
BleakGATTCharacteristic object representing it.
data (bytes or bytearray): The data to send.
response (bool): If write-with-response operation should be done. Defaults to `False`.
characteristic: The characteristic to write to.
data: The data to send.
response: If write-with-response operation should be done.
"""
raise NotImplementedError()

Expand Down
21 changes: 2 additions & 19 deletions bleak/backends/corebluetooth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,27 +307,10 @@ async def read_gatt_descriptor(

async def write_gatt_char(
self,
char_specifier: Union[BleakGATTCharacteristic, int, str, uuid.UUID],
characteristic: BleakGATTCharacteristic,
data: Union[bytes, bytearray, memoryview],
response: bool = False,
response: bool,
) -> None:
"""Perform a write operation of the specified GATT characteristic.
Args:
char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write
to, specified by either integer handle, UUID or directly by the
BleakGATTCharacteristic object representing it.
data (bytes or bytearray): The data to send.
response (bool): If write-with-response operation should be done. Defaults to `False`.
"""
if not isinstance(char_specifier, BleakGATTCharacteristic):
characteristic = self.services.get_characteristic(char_specifier)
else:
characteristic = char_specifier
if not characteristic:
raise BleakError("Characteristic {} was not found!".format(char_specifier))

value = NSData.alloc().initWithBytes_length_(data, len(data))
await self._delegate.write_characteristic(
characteristic.obj,
Expand Down
8 changes: 3 additions & 5 deletions bleak/backends/corebluetooth/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ class CBScannerArgs(TypedDict, total=False):
If true, use Bluetooth address instead of UUID.
.. warning:: This uses an undocumented IOBluetooth API to get the Bluetooth
address and may break in the future macOS releases.
address and may break in the future macOS releases. `It is known to not
work on macOS 10.15 <https://github.com/hbldh/bleak/issues/1286>`_.
"""


Expand Down Expand Up @@ -146,10 +147,7 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None:
advertisement_data,
)

if not self._callback:
return

self._callback(device, advertisement_data)
self.call_detection_callbacks(device, advertisement_data)

self._manager.callbacks[id(self)] = callback
await self._manager.start_scan(self._service_uuids)
Expand Down
Loading

0 comments on commit 447fad7

Please sign in to comment.