Skip to content

Commit

Permalink
Merge pull request adafruit#21 from dhalbert/macos-fixes
Browse files Browse the repository at this point in the history
Fix MacOS issues, and make compatible with bleak 0.8.0
  • Loading branch information
ladyada authored Oct 17, 2020
2 parents 2660cc9 + 4352dad commit 088be25
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
source actions-ci/install.sh
- name: Pip install pylint, black, & Sphinx
run: |
pip install --force-reinstall pylint black==19.10b0 Sphinx sphinx-rtd-theme
pip install --force-reinstall pylint black Sphinx sphinx-rtd-theme
- name: Library version
run: git describe --dirty --always --tags
- name: Check formatting
Expand Down
66 changes: 52 additions & 14 deletions _bleio/adapter_.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@
* Author(s): Dan Halbert for Adafruit Industries
"""
from __future__ import annotations
from typing import Iterable, Union
from typing import Iterable, Optional, Union

import asyncio
import platform
import threading
import time

from bleak import BleakClient, BleakScanner
from bleak.backends.device import BLEDevice

from _bleio.address import Address
from _bleio.connection import Connection
Expand All @@ -53,7 +54,7 @@
adapter = None # pylint: disable=invalid-name


class Adapter:
class Adapter: # pylint: disable=too-many-instance-attributes
# Do blocking scans in chunks of this interval.
_SCAN_INTERVAL = 0.25

Expand All @@ -63,6 +64,8 @@ def __init__(self):
self._name = platform.node()
# Unbounded FIFO for scan results
self._scanning_in_progress = False
# Created on demand in self._bleak_thread context.
self._scanner = None
self._connections = []
self._bleak_loop = None
self._bleak_thread = threading.Thread(target=self._run_bleak_loop)
Expand All @@ -77,6 +80,10 @@ def __init__(self):
self._hcitool_is_usable = None
self._hcitool = None

# Keep a cache of recently scanned devices, to avoid doing double
# device scanning.
self._cached_devices = {}

@property
def _use_hcitool(self):
if self._hcitool_is_usable is None:
Expand Down Expand Up @@ -155,7 +162,7 @@ def start_advertising(
*,
scan_response: Buf = None,
connectable: bool = True,
interval: float = 0.1
interval: float = 0.1,
) -> None:
raise NotImplementedError("Advertising not implemented")

Expand All @@ -173,7 +180,7 @@ def start_scan(
interval: float = 0.1, # pylint: disable=unused-argument
window: float = 0.1, # pylint: disable=unused-argument
minimum_rssi: int = -80,
active: bool = True # pylint: disable=unused-argument
active: bool = True, # pylint: disable=unused-argument
) -> Iterable:
"""
Starts a BLE scan and returns an iterator of results. Advertisements and scan responses are
Expand All @@ -196,25 +203,34 @@ def start_scan(
:returns: an iterable of `_bleio.ScanEntry` objects
:rtype: iterable"""

# Remember only the most recently advertised devices.
# In the future, we might remember these for a few minutes.
self._clear_device_cache()

if self._use_hcitool:
for scan_entry in self._start_scan_hcitool(
prefixes, timeout=timeout, minimum_rssi=minimum_rssi, active=active,
prefixes,
timeout=timeout,
minimum_rssi=minimum_rssi,
active=active,
):
yield scan_entry
return

scanner = BleakScanner(loop=self._bleak_loop)
self._scanning_in_progress = True

start = time.time()
while self._scanning_in_progress and (
timeout is None or time.time() - start < timeout
):
for device in self.await_bleak(
self._scan_for_interval(scanner, self._SCAN_INTERVAL)
self._scan_for_interval(self._SCAN_INTERVAL)
):
if not device or device.rssi < minimum_rssi:
if not device or (
device.rssi is not None and device.rssi < minimum_rssi
):
continue
self._cache_device(device)
scan_entry = ScanEntry._from_bleak( # pylint: disable=protected-access
device
)
Expand Down Expand Up @@ -263,7 +279,12 @@ def _parse_hcidump_data(buffered, prefixes, minimum_rssi, active):
return None

def _start_scan_hcitool(
self, prefixes: Buf, *, timeout: float, minimum_rssi, active: bool,
self,
prefixes: Buf,
*,
timeout: float,
minimum_rssi,
active: bool,
) -> Iterable:
"""hcitool scanning (only on Linux)"""
# hcidump outputs the full advertisement data, assuming it's run privileged.
Expand Down Expand Up @@ -305,14 +326,17 @@ def _start_scan_hcitool(
returncode = self._hcitool.poll()
self.stop_scan()

async def _scan_for_interval(self, scanner, interval: float) -> Iterable[ScanEntry]:
async def _scan_for_interval(self, interval: float) -> Iterable[ScanEntry]:
"""Scan advertisements for the given interval and return ScanEntry objects
for all advertisements heard.
"""
await scanner.start()
if not self._scanner:
self._scanner = BleakScanner(loop=self._bleak_loop)

await self._scanner.start()
await asyncio.sleep(interval)
await scanner.stop()
return await scanner.get_discovered_devices()
await self._scanner.stop()
return await self._scanner.get_discovered_devices()

def stop_scan(self) -> None:
"""Stop scanning before timeout may have occurred."""
Expand All @@ -322,6 +346,7 @@ def stop_scan(self) -> None:
self._hcitool.wait()
self._hcitool = None
self._scanning_in_progress = False
self._scanner = None

@property
def connected(self):
Expand All @@ -336,7 +361,10 @@ def connect(self, address: Address, *, timeout: float) -> None:

# pylint: disable=protected-access
async def _connect_async(self, address: Address, *, timeout: float) -> None:
client = BleakClient(address._bleak_address)
device = self._cached_device(address)
# Use cached device if possible, to avoid having BleakClient do
# a scan again.
client = BleakClient(device if device else address._bleak_address)
# connect() takes a timeout, but it's a timeout to do a
# discover() scan, not an actual connect timeout.
# TODO: avoid the second discovery.
Expand All @@ -362,6 +390,16 @@ def erase_bonding(self) -> None:
"Use the host computer's BLE comamnds to reset bonding information"
)

def _cached_device(self, address: Address) -> Optional[BLEDevice]:
"""Return a device recently found during scanning with the given address."""
return self._cached_devices.get(address)

def _clear_device_cache(self):
self._cached_devices.clear()

def _cache_device(self, device: BLEDevice):
self._cached_devices[device.address] = device


# Create adapter singleton.
adapter = Adapter()
Expand Down
2 changes: 1 addition & 1 deletion _bleio/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def __eq__(self, other: Any) -> bool:
return False

def __hash__(self) -> int:
return hash(self.address_bytes) ^ hash(self.type)
return hash(self.string) ^ hash(self.type)

def __repr__(self) -> str:
return f'Address(string="{self.string}")'
22 changes: 12 additions & 10 deletions _bleio/characteristic.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
* Author(s): Dan Halbert for Adafruit Industries
"""
from __future__ import annotations
from typing import Any, Callable, Tuple, Union
from typing import Any, Callable, Optional, Tuple, Union

from bleak.backends.characteristic import (
BleakGATTCharacteristic,
Expand All @@ -44,7 +44,7 @@

class Characteristic:
"""Stores information about a BLE service characteristic and allows reading
and writing of the characteristic's value."""
and writing of the characteristic's value."""

BROADCAST = 0x1
"""property: allowed in advertising packets"""
Expand All @@ -68,7 +68,7 @@ def __init__(
write_perm: int = Attribute.OPEN,
max_length: int = 20,
fixed_length: bool = False,
initial_value: Buf = None
initial_value: Buf = None,
):
"""There is no regular constructor for a Characteristic. A
new local Characteristic can be created and attached to a
Expand Down Expand Up @@ -98,7 +98,7 @@ def add_to_service(
write_perm: int = Attribute.OPEN,
max_length: int = 20,
fixed_length: bool = False,
initial_value: Buf = None
initial_value: Buf = None,
) -> "Characteristic":
"""Create a new Characteristic object, and add it to this Service.
Expand Down Expand Up @@ -166,7 +166,7 @@ def properties(self) -> int:
"""An int bitmask representing which properties are set, specified as bitwise or'ing of
of these possible values.
`BROADCAST`, `INDICATE`, `NOTIFY`, `READ`, `WRITE`, `WRITE_NO_RESPONSE`.
"""
"""
return self._properties

@property
Expand Down Expand Up @@ -219,7 +219,8 @@ def set_cccd(self, *, notify: bool = False, indicate: bool = False) -> Any:
if notify:
adap.adapter.await_bleak(
self._service.connection._bleak_client.start_notify(
self._bleak_gatt_characteristic.uuid, self._notify_callback,
self._bleak_gatt_characteristic.uuid,
self._notify_callback,
)
)
else:
Expand All @@ -237,11 +238,12 @@ def _remove_notify_callback(self, callback: Callable[[Buf], None]):
"""Remove a callback to call when a notify happens on this characteristic."""
self._notify_callbacks.remove(callback)

def _notify_callback(self, _bleak_uuid: str, data: Buf):
# pylint: disable=unused-argument
def _notify_callback(self, handle: Optional[int], data: Buf):
# pylint: disable=protected-access
if _bleak_uuid == self.uuid._bleak_uuid:
for callback in self._notify_callbacks:
callback(data)
# TODO: Right now we can't vet the handle, because it may be None.
for callback in self._notify_callbacks:
callback(data)

def __repr__(self) -> str:
if self.uuid:
Expand Down
12 changes: 9 additions & 3 deletions _bleio/characteristic_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ def readinto(self, buf: Buf) -> Union[int, None]:

return idx

def readline(self,) -> Buf:
def readline(
self,
) -> Buf:
"""Read a line, ending in a newline character.
:return: the line read
Expand All @@ -135,12 +137,16 @@ def in_waiting(self) -> int:
"""The number of bytes in the input buffer, available to be read"""
return self._queue.qsize()

def reset_input_buffer(self,) -> None:
def reset_input_buffer(
self,
) -> None:
"""Discard any unread characters in the input buffer."""
while not self._queue.empty():
self._queue.get_nowait()

def deinit(self,) -> None:
def deinit(
self,
) -> None:
"""Disable permanently."""
# pylint: disable=protected-access
self._characteristic._remove_notify_callback(self._notify_callback)
28 changes: 16 additions & 12 deletions _bleio/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
import _bleio.adapter_ as adap
import _bleio.address
import _bleio.service

import _bleio.uuid_

Buf = Union[bytes, bytearray, memoryview]

Expand Down Expand Up @@ -133,21 +133,25 @@ async def _discover_remote_services_async(
service or characteristic to be discovered. Creating the UUID causes the UUID to be
registered for use. (This restriction may be lifted in the future.)
:return: A tuple of `_bleio.Service` objects provided by the remote peripheral."""
_bleak_service_uuids_whitelist = ()
:return: A tuple of `_bleio.Service` objects provided by the remote peripheral.
"""

# Fetch the services.
bleak_services = await self.__bleak_client.get_services()

# pylint: disable=protected-access
if service_uuids_whitelist:
_bleak_service_uuids_whitelist = tuple(
# pylint: disable=protected-access
uuid._bleak_uuid
for uuid in service_uuids_whitelist
filtered_bleak_services = tuple(
s
for s in bleak_services
if _bleio.UUID(s.uuid) in service_uuids_whitelist
)
else:
filtered_bleak_services = bleak_services

_bleak_services = await self.__bleak_client.get_services()
# pylint: disable=protected-access
return tuple(
_bleio.service.Service._from_bleak(self, _bleak_service)
for _bleak_service in _bleak_services
if _bleak_service.uuid.lower() in _bleak_service_uuids_whitelist
_bleio.service.Service._from_bleak(self, bleak_service)
for bleak_service in filtered_bleak_services
)

@property
Expand Down
42 changes: 21 additions & 21 deletions _bleio/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,27 +86,27 @@ def add_to_characteristic(

"""Create a new Descriptor object, and add it to this Service.
:param Characteristic characteristic: The characteristic that will hold this descriptor
:param UUID uuid: The uuid of the descriptor
:param int read_perm: Specifies whether the descriptor can be read by a client,
and if so, which security mode is required.
Must be one of the integer values
`_bleio.Attribute.NO_ACCESS`, `_bleio.Attribute.OPEN`,
`_bleio.Attribute.ENCRYPT_NO_MITM`, `_bleio.Attribute.ENCRYPT_WITH_MITM`,
`_bleio.Attribute.LESC_ENCRYPT_WITH_MITM`,
`_bleio.Attribute.SIGNED_NO_MITM`, or `_bleio.Attribute.SIGNED_WITH_MITM`.
:param int write_perm: Specifies whether the descriptor can be written by a client,
and if so, which security mode is required.
Values allowed are the same as ``read_perm``.
:param int max_length: Maximum length in bytes of the descriptor value.
The maximum allowed is 512, or possibly 510 if ``fixed_length`` is False.
The default, 20, is the maximum
number of data bytes that fit in a single BLE 4.x ATT packet.
:param bool fixed_length: True if the descriptor value is of fixed length.
:param buf initial_value: The initial value for this descriptor.
:return: the new Descriptor.
"""
:param Characteristic characteristic: The characteristic that will hold this descriptor
:param UUID uuid: The uuid of the descriptor
:param int read_perm: Specifies whether the descriptor can be read by a client,
and if so, which security mode is required.
Must be one of the integer values
`_bleio.Attribute.NO_ACCESS`, `_bleio.Attribute.OPEN`,
`_bleio.Attribute.ENCRYPT_NO_MITM`, `_bleio.Attribute.ENCRYPT_WITH_MITM`,
`_bleio.Attribute.LESC_ENCRYPT_WITH_MITM`,
`_bleio.Attribute.SIGNED_NO_MITM`, or `_bleio.Attribute.SIGNED_WITH_MITM`.
:param int write_perm: Specifies whether the descriptor can be written by a client,
and if so, which security mode is required.
Values allowed are the same as ``read_perm``.
:param int max_length: Maximum length in bytes of the descriptor value.
The maximum allowed is 512, or possibly 510 if ``fixed_length`` is False.
The default, 20, is the maximum
number of data bytes that fit in a single BLE 4.x ATT packet.
:param bool fixed_length: True if the descriptor value is of fixed length.
:param buf initial_value: The initial value for this descriptor.
:return: the new Descriptor.
"""
desc = Descriptor(
uuid=uuid,
read_perm=read_perm,
Expand Down
2 changes: 1 addition & 1 deletion _bleio/scan_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(
advertisement_bytes: Buf = None,
connectable: bool,
scan_response: bool,
data_dict=None
data_dict=None,
):
"""Should not be instantiated directly. Use `_bleio.Adapter.start_scan`."""
self._address = address
Expand Down
Loading

0 comments on commit 088be25

Please sign in to comment.