Skip to content
This repository has been archived by the owner on Mar 29, 2023. It is now read-only.

Commit

Permalink
Replace NSRunLoop with dispatch queue
Browse files Browse the repository at this point in the history
This replaces the NSRunLoop integration in the corebluetooth backend
with a dispatch queue. This causes callbacks to be dispatched on a
background thread instead of on the main dispatch queue on the main
thread. `call_soon_threadsafe()` is used to synchronize the events
with the event loop where the central manager was created.

The NSRunLoop caused problems because it had to manually be called from
the main thread. This left an asyncio task that had to be manually
stopped at the end of a program to prevent errors about the still
running task (issue: hbldh#111). The NSRunLoop implementation was
also not very efficient since it was waking up the event loop every
millisecond to check for events.
  • Loading branch information
dlech committed Jun 26, 2020
1 parent 815e503 commit 6064096
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 39 deletions.
79 changes: 69 additions & 10 deletions bleak/backends/corebluetooth/CentralManagerDelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
NSNumber,
NSError,
)
from libdispatch import dispatch_queue_create, DISPATCH_QUEUE_SERIAL

from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate
from bleak.backends.corebluetooth.device import BLEDeviceCoreBluetooth
Expand Down Expand Up @@ -58,10 +59,7 @@ def init(self):
if self is None:
return None

self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_(
self, None
)

self.event_loop = asyncio.get_event_loop()
self.connected_peripheral_delegate = None
self.connected_peripheral = None
self._connection_state = CMDConnectionState.DISCONNECTED
Expand All @@ -75,6 +73,10 @@ def init(self):
if not self.compliant():
logger.warning("CentralManagerDelegate is not compliant")

self.central_manager = CBCentralManager.alloc().initWithDelegate_queue_(
self, dispatch_queue_create(b"bleak.corebluetooth", DISPATCH_QUEUE_SERIAL)
)

return self

# User defined functions
Expand Down Expand Up @@ -152,7 +154,8 @@ async def disconnect(self) -> bool:

# Protocol Functions

def centralManagerDidUpdateState_(self, centralManager):
@objc.python_method
def did_update_state(self, centralManager):
if centralManager.state() == CBManagerStateUnknown:
logger.debug("Cannot detect bluetooth device")
elif centralManager.state() == CBManagerStateResetting:
Expand All @@ -171,7 +174,15 @@ def centralManagerDidUpdateState_(self, centralManager):
else:
self.powered_on_event.clear()

def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
def centralManagerDidUpdateState_(self, centralManager):
logger.debug("centralManagerDidUpdateState_")
self.event_loop.call_soon_threadsafe(
self.did_update_state,
centralManager,
)

@objc.python_method
def did_discover_peripheral(
self,
central: CBCentralManager,
peripheral: CBPeripheral,
Expand Down Expand Up @@ -211,7 +222,24 @@ def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
logger.debug("Discovered device {}: {} @ RSSI: {} (kCBAdvData {})".format(
uuid_string, device.name, RSSI, advertisementData.keys()))

def centralManager_didConnectPeripheral_(self, central, peripheral):
def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(
self,
central: CBCentralManager,
peripheral: CBPeripheral,
advertisementData: NSDictionary,
RSSI: NSNumber,
):
logger.debug("centralManager_didDiscoverPeripheral_advertisementData_RSSI_")
self.event_loop.call_soon_threadsafe(
self.did_discover_peripheral,
central,
peripheral,
advertisementData,
RSSI,
)

@objc.python_method
def did_connect_peripheral(self, central, peripheral):
logger.debug(
"Successfully connected to device uuid {}".format(
peripheral.identifier().UUIDString()
Expand All @@ -221,7 +249,16 @@ def centralManager_didConnectPeripheral_(self, central, peripheral):
self.connected_peripheral_delegate = peripheralDelegate
self._connection_state = CMDConnectionState.CONNECTED

def centralManager_didFailToConnectPeripheral_error_(
def centralManager_didConnectPeripheral_(self, central, peripheral):
logger.debug("centralManager_didConnectPeripheral_")
self.event_loop.call_soon_threadsafe(
self.did_connect_peripheral,
central,
peripheral,
)

@objc.python_method
def did_fail_to_connect_peripheral(
self, centralManager: CBCentralManager, peripheral: CBPeripheral, error: NSError
):
logger.debug(
Expand All @@ -231,7 +268,19 @@ def centralManager_didFailToConnectPeripheral_error_(
)
self._connection_state = CMDConnectionState.DISCONNECTED

def centralManager_didDisconnectPeripheral_error_(
def centralManager_didFailToConnectPeripheral_error_(
self, centralManager: CBCentralManager, peripheral: CBPeripheral, error: NSError
):
logger.debug("centralManager_didFailToConnectPeripheral_error_")
self.event_loop.call_soon_threadsafe(
self.did_fail_to_connect_peripheral,
centralManager,
peripheral,
error,
)

@objc.python_method
def did_disconnect_peripheral(
self, central: CBCentralManager, peripheral: CBPeripheral, error: NSError
):
logger.debug("Peripheral Device disconnected!")
Expand All @@ -240,8 +289,18 @@ def centralManager_didDisconnectPeripheral_error_(
if self.disconnected_callback is not None:
self.disconnected_callback()

def centralManager_didDisconnectPeripheral_error_(
self, central: CBCentralManager, peripheral: CBPeripheral, error: NSError
):
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)

112 changes: 104 additions & 8 deletions bleak/backends/corebluetooth/PeripheralDelegate.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def initWithPeripheral_(self, peripheral: CBPeripheral):
self.peripheral = peripheral
self.peripheral.setDelegate_(self)

self._event_loop = asyncio.get_event_loop()
self._services_discovered_event = asyncio.Event()

self._service_characteristic_discovered_events = _EventDict()
Expand Down Expand Up @@ -208,7 +209,9 @@ async def stopNotify_(self, characteristic: CBCharacteristic) -> bool:
return True

# Protocol Functions
def peripheral_didDiscoverServices_(

@objc.python_method
def did_discover_services(
self, peripheral: CBPeripheral, error: NSError
) -> None:
if error is not None:
Expand All @@ -217,7 +220,18 @@ def peripheral_didDiscoverServices_(
logger.debug("Services discovered")
self._services_discovered_event.set()

def peripheral_didDiscoverCharacteristicsForService_error_(
def peripheral_didDiscoverServices_(
self, peripheral: CBPeripheral, error: NSError
) -> None:
logger.debug("peripheral_didDiscoverServices_")
self._event_loop.call_soon_threadsafe(
self.did_discover_services,
peripheral,
error,
)

@objc.python_method
def did_discover_characteristics_for_service(
self, peripheral: CBPeripheral, service: CBService, error: NSError
):
sUUID = service.UUID().UUIDString()
Expand All @@ -233,7 +247,19 @@ def peripheral_didDiscoverCharacteristicsForService_error_(
else:
logger.debug("Unexpected event didDiscoverCharacteristicsForService")

def peripheral_didDiscoverDescriptorsForCharacteristic_error_(
def peripheral_didDiscoverCharacteristicsForService_error_(
self, peripheral: CBPeripheral, service: CBService, error: NSError
):
logger.debug("peripheral_didDiscoverCharacteristicsForService_error_")
self._event_loop.call_soon_threadsafe(
self.did_discover_characteristics_for_service,
peripheral,
service,
error,
)

@objc.python_method
def did_discover_descriptors_for_characteristic(
self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
):
cUUID = characteristic.UUID().UUIDString()
Expand All @@ -251,7 +277,19 @@ def peripheral_didDiscoverDescriptorsForCharacteristic_error_(
else:
logger.warning("Unexpected event didDiscoverDescriptorsForCharacteristic")

def peripheral_didUpdateValueForCharacteristic_error_(
def peripheral_didDiscoverDescriptorsForCharacteristic_error_(
self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
):
logger.debug("peripheral_didDiscoverDescriptorsForCharacteristic_error_")
self._event_loop.call_soon_threadsafe(
self.did_discover_descriptors_for_characteristic,
peripheral,
characteristic,
error,
)

@objc.python_method
def did_update_value_for_characteristic(
self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
):
cUUID = characteristic.UUID().UUIDString()
Expand All @@ -272,7 +310,19 @@ def peripheral_didUpdateValueForCharacteristic_error_(
# only expected on read
pass

def peripheral_didUpdateValueForDescriptor_error_(
def peripheral_didUpdateValueForCharacteristic_error_(
self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
):
logger.debug("peripheral_didUpdateValueForCharacteristic_error_")
self._event_loop.call_soon_threadsafe(
self.did_update_value_for_characteristic,
peripheral,
characteristic,
error,
)

@objc.python_method
def did_update_value_for_descriptor(
self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError
):
dUUID = descriptor.UUID().UUIDString()
Expand All @@ -288,7 +338,19 @@ def peripheral_didUpdateValueForDescriptor_error_(
else:
logger.warning("Unexpected event didUpdateValueForDescriptor")

def peripheral_didWriteValueForCharacteristic_error_(
def peripheral_didUpdateValueForDescriptor_error_(
self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError
):
logger.debug("peripheral_didUpdateValueForDescriptor_error_")
self._event_loop.call_soon_threadsafe(
self.did_update_value_for_descriptor,
peripheral,
descriptor,
error,
)

@objc.python_method
def did_write_value_for_characteristic(
self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
):
cUUID = characteristic.UUID().UUIDString()
Expand All @@ -305,7 +367,19 @@ def peripheral_didWriteValueForCharacteristic_error_(
# event only expected on write with response
pass

def peripheral_didWriteValueForDescriptor_error_(
def peripheral_didWriteValueForCharacteristic_error_(
self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
):
logger.debug("peripheral_didWriteValueForCharacteristic_error_")
self._event_loop.call_soon_threadsafe(
self.did_write_value_for_characteristic,
peripheral,
characteristic,
error,
)

@objc.python_method
def did_write_value_for_descriptor(
self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError
):
dUUID = descriptor.UUID().UUIDString()
Expand All @@ -319,7 +393,19 @@ def peripheral_didWriteValueForDescriptor_error_(
else:
logger.warning("Unexpected event didWriteValueForDescriptor")

def peripheral_didUpdateNotificationStateForCharacteristic_error_(
def peripheral_didWriteValueForDescriptor_error_(
self, peripheral: CBPeripheral, descriptor: CBDescriptor, error: NSError
):
logger.debug("peripheral_didWriteValueForDescriptor_error_")
self._event_loop.call_soon_threadsafe(
self.did_write_value_for_descriptor,
peripheral,
descriptor,
error,
)

@objc.python_method
def did_update_notification_for_characteristic(
self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
):
cUUID = characteristic.UUID().UUIDString()
Expand All @@ -339,3 +425,13 @@ def peripheral_didUpdateNotificationStateForCharacteristic_error_(
"Unexpected event didUpdateNotificationStateForCharacteristic"
)

def peripheral_didUpdateNotificationStateForCharacteristic_error_(
self, peripheral: CBPeripheral, characteristic: CBCharacteristic, error: NSError
):
logger.debug("peripheral_didUpdateNotificationStateForCharacteristic_error_")
self._event_loop.call_soon_threadsafe(
self.did_update_notification_for_characteristic,
peripheral,
characteristic,
error,
)
21 changes: 0 additions & 21 deletions bleak/backends/corebluetooth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"""

import asyncio
from Foundation import NSDate, NSDefaultRunLoopMode, NSRunLoop
from .CentralManagerDelegate import CentralManagerDelegate
import objc

Expand All @@ -19,29 +18,9 @@ class Application:
This is a temporary application class responsible for running the NSRunLoop
so that events within CoreBluetooth are appropriately handled
"""

ns_run_loop_done = False
ns_run_loop_interval = 0.001

def __init__(self):
self.main_loop = asyncio.get_event_loop()
self.main_loop.create_task(self._handle_nsrunloop())

self.nsrunloop = NSRunLoop.currentRunLoop()

self.central_manager_delegate = CentralManagerDelegate.alloc().init()

def __del__(self):
self.ns_run_loop_done = True

async def _handle_nsrunloop(self):
while not self.ns_run_loop_done:
time_interval = NSDate.alloc().initWithTimeIntervalSinceNow_(
self.ns_run_loop_interval
)
self.nsrunloop.runMode_beforeDate_(NSDefaultRunLoopMode, time_interval)
await asyncio.sleep(0)


# Restructure this later: Global isn't the prettiest way of doing this...
global CBAPP
Expand Down

0 comments on commit 6064096

Please sign in to comment.