Skip to content

Commit

Permalink
Merge branch 'develop' into feature/stop-event-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
hbldh authored Jun 11, 2021
2 parents 31fa5ff + d003467 commit 53a6d13
Show file tree
Hide file tree
Showing 25 changed files with 826 additions and 374 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Added
* WinRT backend added
* Added ``BleakScanner.discovered_devices`` property.
* Added an event to await when stopping scanners in WinRT and pythonnet backends. Fixes #556.
* Added ``BleakScanner.find_device_by_filter`` static method.
* Added ``scanner_byname.py`` example.
* Added optional command line argument to specify device to all applicable examples.


Changed
~~~~~~~
Expand All @@ -27,6 +31,9 @@ Changed
* Added capability to handle async functions as detection callbacks in ``BleakScanner``.
* Added error description in addition to error name when ``BleakDBusError`` is converted to string
* Change typing of data parameter in write methods to ``Union[bytes, bytearray, memoryview]``
* Improved type hints in CoreBluetooth backend.
* Use delegate callbacks for get_rssi() on CoreBluetooth backend.
* Use ``@objc.python_method`` where possible in ``PeripheralDelegate`` class.

Fixed
~~~~~
Expand All @@ -43,6 +50,15 @@ Fixed
* Fixed write without response on BlueZ < 5.51.
* Fixed error propagation for CoreBluetooth events
* Fixed failed import on CI server when BlueZ is not installed.
* Fixed notification ``value`` should be ``bytearray`` on CoreBluetooth. Fixes #560.
* Fixed crash when cancelling connection when Python runtime shuts down on
CoreBluetooth backend. Fixes #538
* Fixed connecting to multiple devices using a single ``BleakScanner`` on
CoreBluetooth backend.
* Fixed deadlock in CoreBluetooth backend when device disconnects while
callbacks are pending. Fixes #535.
* Fixed deadlock when using more than one service, characteristic or descriptor
with the same UUID on CoreBluetooth backend.


`0.11.0`_ (2021-03-17)
Expand Down
4 changes: 2 additions & 2 deletions bleak/backends/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import abc
import asyncio
import uuid
from typing import Callable, Union
from typing import Callable, Optional, Union
from warnings import warn

from bleak.backends.service import BleakGATTServiceCollection
Expand Down Expand Up @@ -67,7 +67,7 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
# Connectivity methods

def set_disconnected_callback(
self, callback: Union[Callable[["BaseBleakClient"], None], None], **kwargs
self, callback: Optional[Callable[["BaseBleakClient"], None]], **kwargs
) -> None:
"""Set the disconnect callback.
The callback will only be called on unsolicited disconnect event.
Expand Down
203 changes: 94 additions & 109 deletions bleak/backends/corebluetooth/CentralManagerDelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,30 @@
import logging
import platform
import threading
from enum import Enum
from typing import List
from typing import Any, Callable, Dict, List, Optional

import objc
from CoreBluetooth import (
CBManagerStateUnknown,
CBManagerStateResetting,
CBManagerStateUnsupported,
CBManagerStateUnauthorized,
CBCentralManager,
CBManagerStatePoweredOff,
CBManagerStatePoweredOn,
)
from Foundation import (
NSObject,
CBCentralManager,
CBManagerStateResetting,
CBManagerStateUnauthorized,
CBManagerStateUnknown,
CBManagerStateUnsupported,
CBPeripheral,
CBUUID,
)
from Foundation import (
NSArray,
NSDictionary,
NSNumber,
NSError,
NSNumber,
NSObject,
NSUUID,
)
from libdispatch import dispatch_queue_create, DISPATCH_QUEUE_SERIAL

from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate
from bleak.backends.corebluetooth.device import BLEDeviceCoreBluetooth
from bleak.exc import BleakError

Expand All @@ -48,35 +47,31 @@
_mac_version = ""
_IS_PRE_10_13 = False


class CMDConnectionState(Enum):
DISCONNECTED = 0
PENDING = 1
CONNECTED = 2
DisconnectCallback = Callable[[], None]


class CentralManagerDelegate(NSObject):
"""macOS conforming python class for managing the CentralManger for BLE"""

___pyobjc_protocols__ = [CBCentralManagerDelegate]

def init(self):
def init(self) -> Optional["CentralManagerDelegate"]:
"""macOS init function for NSObject"""
self = objc.super(CentralManagerDelegate, self).init()

if self is None:
return None

self.event_loop = asyncio.get_event_loop()
self.connected_peripheral_delegate = None
self.connected_peripheral = None
self._connection_state = CMDConnectionState.DISCONNECTED
self._connect_futures: Dict[NSUUID, asyncio.Future] = {}

self.devices = {}
self.devices: Dict[str, BLEDeviceCoreBluetooth] = {}

self.callbacks = {}
self.disconnected_callback = None
self._connection_state_changed = asyncio.Event()
self.callbacks: Dict[
int, Callable[[CBPeripheral, Dict[str, Any], int], None]
] = {}
self._disconnect_callbacks: Dict[NSUUID, DisconnectCallback] = {}
self._disconnect_futures: Dict[NSUUID, asyncio.Future] = {}

self._did_update_state_event = threading.Event()
self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_(
Expand All @@ -100,19 +95,15 @@ def init(self):

# User defined functions

@property
def isConnected(self) -> bool:
return self._connection_state == CMDConnectionState.CONNECTED

@objc.python_method
def start_scan(self, scan_options):
def start_scan(self, scan_options) -> None:
# remove old
self.devices = {}
service_uuids = []
service_uuids = None
if "service_uuids" in scan_options:
service_uuids_str = scan_options["service_uuids"]
service_uuids = NSArray.alloc().initWithArray_(
list(map(string2uuid, service_uuids_str))
list(map(CBUUID.UUIDWithString_, service_uuids_str))
)

self.central_manager.scanForPeripheralsWithServices_options_(
Expand All @@ -136,50 +127,38 @@ async def stop_scan(self) -> List[CBPeripheral]:
return []

@objc.python_method
async def scanForPeripherals_(self, scan_options) -> List[CBPeripheral]:
"""
Scan for peripheral devices
scan_options = { service_uuids, timeout }
"""

self.start_scan(scan_options)
await asyncio.sleep(float(scan_options.get("timeout", 0.0)))
return await self.stop_scan()

async def connect_(self, peripheral: CBPeripheral, timeout=10.0) -> bool:
self._connection_state = CMDConnectionState.PENDING
self._connection_state_changed.clear()
self.central_manager.connectPeripheral_options_(peripheral, None)

async def connect(
self,
peripheral: CBPeripheral,
disconnect_callback: DisconnectCallback,
timeout=10.0,
) -> None:
try:
await asyncio.wait_for(
self._connection_state_changed.wait(), timeout=timeout
)
self._disconnect_callbacks[peripheral.identifier()] = disconnect_callback
future = self.event_loop.create_future()
self._connect_futures[peripheral.identifier()] = future
self.central_manager.connectPeripheral_options_(peripheral, None)
await asyncio.wait_for(future, timeout=timeout)
except asyncio.TimeoutError:
logger.debug(f"Connection timed out after {timeout} seconds.")
del self._disconnect_callbacks[peripheral.identifier()]
future = self.event_loop.create_future()
self._disconnect_futures[peripheral.identifier()] = future
self.central_manager.cancelPeripheralConnection_(peripheral)
await future
raise

self.connected_peripheral = peripheral

return self._connection_state == CMDConnectionState.CONNECTED

async def disconnect(self) -> bool:
# Is a peripheral even connected?
if self.connected_peripheral is None:
return True

self._connection_state = CMDConnectionState.PENDING
self.central_manager.cancelPeripheralConnection_(self.connected_peripheral)

while self._connection_state == CMDConnectionState.PENDING:
await asyncio.sleep(0)

return self._connection_state == CMDConnectionState.DISCONNECTED
@objc.python_method
async def disconnect(self, peripheral: CBPeripheral) -> None:
future = self.event_loop.create_future()
self._disconnect_futures[peripheral.identifier()] = future
self.central_manager.cancelPeripheralConnection_(peripheral)
await future
del self._disconnect_callbacks[peripheral.identifier()]

# Protocol Functions

def centralManagerDidUpdateState_(self, centralManager):
def centralManagerDidUpdateState_(self, centralManager: CBCentralManager) -> None:
logger.debug("centralManagerDidUpdateState_")
if centralManager.state() == CBManagerStateUnknown:
logger.debug("Cannot detect bluetooth device")
Expand All @@ -203,7 +182,7 @@ def did_discover_peripheral(
peripheral: CBPeripheral,
advertisementData: NSDictionary,
RSSI: NSNumber,
):
) -> None:
# 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.
Expand Down Expand Up @@ -251,7 +230,7 @@ def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
peripheral: CBPeripheral,
advertisementData: NSDictionary,
RSSI: NSNumber,
):
) -> None:
logger.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_")
self.event_loop.call_soon_threadsafe(
self.did_discover_peripheral,
Expand All @@ -262,21 +241,16 @@ def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
)

@objc.python_method
def did_connect_peripheral(self, central, peripheral):
logger.debug(
"Successfully connected to device uuid {}".format(
peripheral.identifier().UUIDString()
)
)
if self._connection_state != CMDConnectionState.CONNECTED:
peripheralDelegate = PeripheralDelegate.alloc().initWithPeripheral_(
peripheral
)
self.connected_peripheral_delegate = peripheralDelegate
self._connection_state = CMDConnectionState.CONNECTED
self._connection_state_changed.set()

def centralManager_didConnectPeripheral_(self, central, peripheral):
def did_connect_peripheral(
self, central: CBCentralManager, peripheral: CBPeripheral
) -> None:
future = self._connect_futures.pop(peripheral.identifier(), None)
if future is not None:
future.set_result(True)

def centralManager_didConnectPeripheral_(
self, central: CBCentralManager, peripheral: CBPeripheral
) -> None:
logger.debug("centralManager_didConnectPeripheral_")
self.event_loop.call_soon_threadsafe(
self.did_connect_peripheral,
Expand All @@ -286,19 +260,24 @@ def centralManager_didConnectPeripheral_(self, central, peripheral):

@objc.python_method
def did_fail_to_connect_peripheral(
self, centralManager: CBCentralManager, peripheral: CBPeripheral, error: NSError
):
logger.debug(
"Failed to connect to device uuid {}".format(
peripheral.identifier().UUIDString()
)
)
self._connection_state = CMDConnectionState.DISCONNECTED
self._connection_state_changed.set()
self,
centralManager: CBCentralManager,
peripheral: CBPeripheral,
error: Optional[NSError],
) -> None:
future = self._connect_futures.pop(peripheral.identifier(), None)
if future is not None:
if error is not None:
future.set_exception(BleakError(f"failed to connect: {error}"))
else:
future.set_result(False)

def centralManager_didFailToConnectPeripheral_error_(
self, centralManager: CBCentralManager, peripheral: CBPeripheral, error: NSError
):
self,
centralManager: CBCentralManager,
peripheral: CBPeripheral,
error: Optional[NSError],
) -> None:
logger.debug("centralManager_didFailToConnectPeripheral_error_")
self.event_loop.call_soon_threadsafe(
self.did_fail_to_connect_peripheral,
Expand All @@ -309,28 +288,34 @@ def centralManager_didFailToConnectPeripheral_error_(

@objc.python_method
def did_disconnect_peripheral(
self, central: CBCentralManager, peripheral: CBPeripheral, error: NSError
):
self,
central: CBCentralManager,
peripheral: CBPeripheral,
error: Optional[NSError],
) -> None:
logger.debug("Peripheral Device disconnected!")
self.connected_peripheral_delegate = None
self.connected_peripheral = None
self._connection_state = CMDConnectionState.DISCONNECTED

if self.disconnected_callback is not None:
self.disconnected_callback()
future = self._disconnect_futures.pop(peripheral.identifier(), None)
if future is not None:
if error is not None:
future.set_exception(BleakError(f"disconnect failed: {error}"))
else:
future.set_result(None)

callback = self._disconnect_callbacks.get(peripheral.identifier())
if callback is not None:
callback()

def centralManager_didDisconnectPeripheral_error_(
self, central: CBCentralManager, peripheral: CBPeripheral, error: NSError
):
self,
central: CBCentralManager,
peripheral: CBPeripheral,
error: Optional[NSError],
) -> None:
logger.debug("centralManager_didDisconnectPeripheral_error_")
self.event_loop.call_soon_threadsafe(
self.did_disconnect_peripheral,
central,
peripheral,
error,
)


def string2uuid(uuid_str: str) -> CBUUID:
"""Convert a string to a uuid"""
return CBUUID.UUIDWithString_(uuid_str)
Loading

0 comments on commit 53a6d13

Please sign in to comment.