Skip to content

Commit

Permalink
Fix connecting to multiple devices on Mac
Browse files Browse the repository at this point in the history
This fixes connecting to multiple devices when using a single
BleakScanner in the CoreBluetooth backend. Previously, the central
manager held a reference to a single peripheral, so if a second
peripheral was connected using the same central manager, it would
overwrite the first.

This removes the coupling so that the peripheral is passed as an
argument to the central manager methods instead.
  • Loading branch information
dlech committed Jun 5, 2021
1 parent 010ceff commit 1ee6520
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 96 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Fixed
* Fixed failed import on CI server when BlueZ is not installed.
* 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.


`0.11.0`_ (2021-03-17)
Expand Down
39 changes: 18 additions & 21 deletions bleak/backends/corebluetooth/CentralManagerDelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
)
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,6 +47,8 @@
_mac_version = ""
_IS_PRE_10_13 = False

DisconnectCallback = Callable[[], None]


class CentralManagerDelegate(NSObject):
"""macOS conforming python class for managing the CentralManger for BLE"""
Expand All @@ -62,16 +63,14 @@ def init(self) -> Optional["CentralManagerDelegate"]:
return None

self.event_loop = asyncio.get_event_loop()
self.connected_peripheral_delegate: Optional[PeripheralDelegate] = None
self.connected_peripheral: Optional[CBPeripheral] = None
self._connect_futures: Dict[NSUUID, asyncio.Future] = {}

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

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

self._did_update_state_event = threading.Event()
Expand Down Expand Up @@ -139,35 +138,34 @@ async def scanForPeripherals_(self, scan_options) -> List[CBPeripheral]:
return await self.stop_scan()

@objc.python_method
async def connect(self, peripheral: CBPeripheral, timeout=10.0) -> None:
delegate = PeripheralDelegate.alloc().initWithPeripheral_(peripheral)

async def connect(
self,
peripheral: CBPeripheral,
disconnect_callback: DisconnectCallback,
timeout=10.0,
) -> None:
try:
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
self.connected_peripheral_delegate = delegate

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

async def disconnect(self, peripheral: CBPeripheral) -> None:
future = self.event_loop.create_future()
self._disconnect_futures[self.connected_peripheral.identifier()] = future
self.central_manager.cancelPeripheralConnection_(self.connected_peripheral)
self._disconnect_futures[peripheral.identifier()] = future
self.central_manager.cancelPeripheralConnection_(peripheral)
await future
del self._disconnect_callbacks[peripheral.identifier()]

# Protocol Functions

Expand Down Expand Up @@ -307,8 +305,6 @@ def did_disconnect_peripheral(
error: Optional[NSError],
) -> None:
logger.debug("Peripheral Device disconnected!")
self.connected_peripheral_delegate = None
self.connected_peripheral = None

future = self._disconnect_futures.pop(peripheral.identifier(), None)
if future is not None:
Expand All @@ -317,8 +313,9 @@ def did_disconnect_peripheral(
else:
future.set_result(None)

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

def centralManager_didDisconnectPeripheral_error_(
self,
Expand Down
Loading

0 comments on commit 1ee6520

Please sign in to comment.