Skip to content

Commit

Permalink
Merge branch 'develop' of github.com:hbldh/bleak into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
hbldh committed Aug 1, 2019
2 parents df66159 + 24923e2 commit fb40825
Show file tree
Hide file tree
Showing 40 changed files with 1,706 additions and 55 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ Ready to contribute? Here's how to set up `bleak` for local development.
$ cd bleak/
$ python setup.py develop

4. Create a branch for local development::
4. Create a branch for local development, originating from the `develop` branch::

$ git checkout -b name-of-your-bugfix-or-feature
$ git checkout -b name-of-your-bugfix-or-feature develop

Now you can make your changes locally.

Expand Down
11 changes: 7 additions & 4 deletions bleak/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,13 @@
) # noqa
elif platform.system() == "Darwin":
# TODO: Check if macOS version has Core Bluetooth, raise error otherwise.
from bleak.backends.corebluetooth import (
BleakClientCoreBluetooth as BleakClient,
discover,
) # noqa
from Foundation import NSClassFromString
if NSClassFromString("CBPeripheral") is None:
raise BleakError("Bleak requires the CoreBluetooth Framework")

from bleak.backends.corebluetooth.discovery import discover
from bleak.backends.corebluetooth.client import BleakClientCoreBluetooth as BleakClient

elif platform.system() == "Windows":
# Requires Windows 10 Creators update at least, i.e. Window 10.0.16299
_vtup = platform.win32_ver()[1].split(".")
Expand Down
56 changes: 52 additions & 4 deletions bleak/backends/bluezdbus/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ def __init__(self, address, loop=None, **kwargs):
self._device_path = None
self._bus = None
self._rules = {}
self._subscriptions = list()

self._disconnected_callback = None

self._char_path_to_uuid = {}

Expand All @@ -51,6 +54,20 @@ def __init__(self, address, loop=None, **kwargs):

# Connectivity methods

def set_disconnected_callback(
self, callback: Callable[[BaseBleakClient], None], **kwargs
) -> None:
"""Set the disconnected callback.
The callback will be called on DBus PropChanged event with
the 'Connected' key set to False.
Args:
callback: callback to be called on disconnection.
"""

self._disconnected_callback = callback

async def connect(self, **kwargs) -> bool:
"""Connect to the specified GATT server.
Expand Down Expand Up @@ -118,6 +135,14 @@ def _services_resolved_callback(message):
)
return True

async def _cleanup(self) -> None:
for rule_name, rule_id in self._rules.items():
logger.debug("Removing rule {0}, ID: {1}".format(rule_name, rule_id))
await self._bus.delMatch(rule_id).asFuture(self.loop)

await asyncio.gather(
*(self.stop_notify(_uuid) for _uuid in self._subscriptions))

async def disconnect(self) -> bool:
"""Disconnect from the specified GATT server.
Expand All @@ -126,9 +151,9 @@ async def disconnect(self) -> bool:
"""
logger.debug("Disconnecting from BLE device...")
for rule_name, rule_id in self._rules.items():
logger.debug("Removing rule {0}, ID: {1}".format(rule_name, rule_id))
await self._bus.delMatch(rule_id).asFuture(self.loop)

await self._cleanup()

await self._bus.callRemote(
self._device_path,
"Disconnect",
Expand Down Expand Up @@ -442,6 +467,8 @@ def callback(sender, data):
callback, self._char_path_to_uuid
) # noqa | E123 error in flake8...

self._subscriptions.append(_uuid)

async def stop_notify(self, _uuid: str) -> None:
"""Deactivate notification/indication on a specified characteristic.
Expand All @@ -461,6 +488,8 @@ async def stop_notify(self, _uuid: str) -> None:
).asFuture(self.loop)
self._notification_callbacks.pop(characteristic.path, None)

self._subscriptions.remove(_uuid)

# DBUS introspection method for characteristics.

async def get_all_for_characteristic(self, _uuid) -> dict:
Expand Down Expand Up @@ -521,6 +550,12 @@ def _properties_changed_callback(self, message):
the new data on the GATT Characteristic.
"""

logger.debug('DBUS: path: {}, domain: {}, body: {}'
.format(message.path,
message.body[0],
message.body[1]))

if message.body[0] == defs.GATT_CHARACTERISTIC_INTERFACE:
if message.path in self._notification_callbacks:
logger.info(
Expand All @@ -531,7 +566,20 @@ def _properties_changed_callback(self, message):
self._notification_callbacks[message.path](
message.path, message.body[1]
)

elif message.body[0] == defs.DEVICE_INTERFACE:
device_path = '/org/bluez/%s/dev_%s' % (self.device,
self.address.replace(':', '_'))
if message.path == device_path:
message_body_map = message.body[1]
if 'Connected' in message_body_map and \
not message_body_map['Connected']:
logger.debug("Device {} disconnected."
.format(self.address))

self.loop.create_task(self._cleanup())

if self._disconnected_callback is not None:
self._disconnected_callback(self)

def _data_notification_wrapper(func, char_map):
@wraps(func)
Expand Down
19 changes: 13 additions & 6 deletions bleak/backends/bluezdbus/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ async def discover(timeout=5.0, loop=None, **kwargs):
loop = loop if loop else asyncio.get_event_loop()
cached_devices = {}
devices = {}
rules = list()

def parse_msg(message):
if message.member == "InterfacesAdded":
Expand Down Expand Up @@ -124,21 +125,21 @@ def parse_msg(message):
bus = await client.connect(reactor, "system").asFuture(loop)

# Add signal listeners
await bus.addMatch(
rules.append(await bus.addMatch(
parse_msg,
interface="org.freedesktop.DBus.ObjectManager",
member="InterfacesAdded",
).asFuture(loop)
await bus.addMatch(
).asFuture(loop))
rules.append(await bus.addMatch(
parse_msg,
interface="org.freedesktop.DBus.ObjectManager",
member="InterfacesRemoved",
).asFuture(loop)
await bus.addMatch(
).asFuture(loop))
rules.append(await bus.addMatch(
parse_msg,
interface="org.freedesktop.DBus.Properties",
member="PropertiesChanged",
).asFuture(loop)
).asFuture(loop))

# Find the HCI device to use for scanning and get cached device properties
objects = await bus.callRemote(
Expand Down Expand Up @@ -199,4 +200,10 @@ def parse_msg(message):
manufacturer_data = props.get('ManufacturerData', {})
discovered_devices.append(BLEDevice(address, name, {"path": path, "props": props}, uuids=uuids,
manufacturer_data=manufacturer_data))

for rule in rules:
await bus.delMatch(rule).asFuture(loop)

bus.disconnect()

return discovered_devices
24 changes: 24 additions & 0 deletions bleak/backends/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):

# Connectivity methods

@abc.abstractmethod
async def set_disconnected_callback(
self, callback: Callable[['BaseBleakClient'], None], **kwargs
) -> None:
"""Set the disconnect callback.
The callback will only be called on unsolicited disconnect event.
Callbacks must accept one input which is the client object itself.
.. code-block:: python
def callback(client):
print("Client with address {} got disconnected!".format(client.address))
client.set_disconnected_callback(callback)
client.connect()
Args:
callback: callback to be called on disconnection.
"""

raise NotImplementedError()

@abc.abstractmethod
async def connect(self, **kwargs) -> bool:
"""Connect to the specified GATT server.
Expand Down
177 changes: 177 additions & 0 deletions bleak/backends/corebluetooth/CentralManagerDelegate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
CentralManagerDelegate will implement the CBCentralManagerDelegate protocol to
manage CoreBluetooth serivces and resources on the Central End
Created on June, 25 2019 by kevincar <[email protected]>
"""

import asyncio
import logging
from enum import Enum
from typing import List

import objc
from Foundation import NSObject, \
CBCentralManager, \
CBPeripheral, \
CBUUID, \
NSArray, \
NSDictionary, \
NSNumber, \
NSError

from bleak.backends.corebluetooth.PeripheralDelegate import PeripheralDelegate

# logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

CBCentralManagerDelegate = objc.protocolNamed('CBCentralManagerDelegate')

class CMDConnectionState(Enum):
DISCONNECTED = 0
PENDING = 1
CONNECTED = 2

class CentralManagerDelegate(NSObject):
"""macOS conforming python class for managing the CentralManger for BLE"""
___pyobjc_protocols__ = [CBCentralManagerDelegate]

def init(self):
"""macOS init function for NSObject"""
self = objc.super(CentralManagerDelegate, self).init()

if self is None:
return None

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

self.connected_peripheral_delegate = None
self.connected_peripheral = None
self._connection_state = CMDConnectionState.DISCONNECTED

self.ready = False
self.peripheral_list = []
self.peripheral_delegate_list = []
self.advertisement_data_list = []

if not self.compliant():
logger.warning("CentralManagerDelegate is not compliant")

return self

# User defined functions

def compliant(self):
"""Determins whether the class adheres to the CBCentralManagerDelegate protocol"""
return CentralManagerDelegate.pyobjc_classMethods.conformsToProtocol_(CBCentralManagerDelegate)

@property
def enabled(self):
"""Check if the bluetooth device is on and running"""
return self.central_manager.state() == 5

@property
def isConnected(self) -> bool:
# Validate this
return self.connected_peripheral != None

async def is_ready(self):
"""is_ready allows an asynchronous way to wait and ensure the
CentralManager has processed it's inputs before moving on"""
while not self.ready:
await asyncio.sleep(0)
return self.ready

async def scanForPeripherals_(self, scan_options) -> List[CBPeripheral]:
"""
Scan for peripheral devices
scan_options = { service_uuids, timeout }
"""
service_uuids = []
if 'service_uuids' in scan_options:
service_uuids_str = scan_options['service_uuids']
service_uuids = NSArray.alloc().initWithArray_(list(map(string2uuid, service_uuids_str)))

timeout = None
if 'timeout' in scan_options:
timeout = scan_options['timeout']

self.central_manager.scanForPeripheralsWithServices_options_(service_uuids, None)

if timeout is None or type(timeout) not in (int, float):
return

await asyncio.sleep(timeout)
self.central_manager.stopScan()

return []

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

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

self.connected_peripheral = peripheral

return self._connection_state == CMDConnectionState.CONNECTED

async def disconnect(self) -> bool:
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

# Protocol Functions

def centralManagerDidUpdateState_(self, centralManager):
if centralManager.state() == 0:
logger.debug("Cannot detect bluetooth device")
elif centralManager.state() == 1:
logger.debug("Bluetooth is resetting")
elif centralManager.state() == 2:
logger.debug("Bluetooth is unsupported")
elif centralManager.state() == 3:
logger.debug("Bluetooth is unauthorized")
elif centralManager.state() == 4:
logger.debug("Bluetooth powered off")
elif centralManager.state() == 5:
logger.debug("Bluetooth powered on")

self.ready = True


def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self,
central: CBCentralManager,
peripheral: CBPeripheral,
advertisementData: NSDictionary,
RSSI: NSNumber):
uuid_string = peripheral.identifier().UUIDString()
if uuid_string not in list(map(lambda x: x.identifier().UUIDString(), self.peripheral_list)):
self.peripheral_list.append(peripheral)
self.advertisement_data_list.append(advertisementData)
logger.debug("Discovered device {}: {} @ RSSI: {}".format(uuid_string, peripheral.name() or 'Unknown', RSSI))

def centralManager_didConnectPeripheral_(self, central, peripheral):
logger.debug("Successfully connected to device uuid {}".format(peripheral.identifier().UUIDString()))
peripheralDelegate = PeripheralDelegate.alloc().initWithPeripheral_(peripheral)
self.connected_peripheral_delegate = peripheralDelegate
self._connection_state = CMDConnectionState.CONNECTED

def centralManager_didFailToConnectPeripheral_error_(self, centralManager: CBCentralManager, peripheral: CBPeripheral, error: NSError):
logger.debug("Failed to connect to device uuid {}".format(peripheral.identifier().UUIDString()))
self._connection_state = CMDConnectionState.DISCONNECTED

def centralManager_didDisconnectPeripheral_error_(self, central: CBCentralManager, peripheral: CBPeripheral, error: NSError):
logger.debug("Peripheral Device disconnected!")
self._connection_state = CMDConnectionState.DISCONNECTED

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

Loading

0 comments on commit fb40825

Please sign in to comment.