Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 54 additions & 63 deletions bleekWare/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,7 @@
from android.os import Build

from . import BLEDevice, BLEGattService
from . import bleekWareError, bleekWareCharacteristicNotFoundError


received_data = []
status_message = []
services = []
async_callbacks = set()
from . import bleekWareError, bleekWareCharacteristicNotFoundError, logger

# Client Characteristic Configuration Descriptor
CCCD = '00002902-0000-1000-8000-00805f9b34fb'
Expand All @@ -46,12 +40,11 @@ def onConnectionStateChange(self, gatt, status, newState):
This is the callback function for Android's 'device.ConnectGatt'.
"""
if newState == BluetoothProfile.STATE_CONNECTED:
status_message.append('connected')
logger.info('connected')
gatt.discoverServices()
elif newState == BluetoothProfile.STATE_DISCONNECTED:
status_message.append('disconnected')
logger.info('disconnected')
gatt = None
services.clear()
if self.client.disconnected_callback:
self.client.disconnected_callback()

Expand All @@ -61,9 +54,16 @@ def onServicesDiscovered(self, gatt, status):

This is the callback function for Android's 'gatt.discoverServices'.
"""
services.extend(gatt.getServices().toArray())
# getServices returns an ArrayList, must be converted to Array to work
# with Python
services = list()
for gatt_service in gatt.getServices().toArray():
service = BLEGattService(gatt_service)
gatt_chars = gatt_service.getCharacteristics().toArray()
service.characteristics = [
str(char.getUuid()) for char in gatt_chars
]
services.append(service)

self.client.services = services

@Override(
jvoid,
Expand All @@ -85,7 +85,7 @@ def onCharacteristicRead(self, gatt, characteristic, *args):
else:
value = args[0]
if status == BluetoothGatt.GATT_SUCCESS:
received_data.append(value)
self.client._received_data.append(value)

@Override(
jvoid, [BluetoothGatt, BluetoothGattCharacteristic, jarray(jbyte)]
Expand All @@ -95,7 +95,22 @@ def onCharacteristicChanged(self, gatt, characteristic, value):

This is the callback function for notifying services.
"""
received_data.append(characteristic.getValue())
if self.client.notification_callback:
data = characteristic.getValue()
if inspect.iscoroutinefunction(self.client.notification_callback):
task = self.client.loop.create_task(
self.client.notification_callback(
characteristic, bytearray(data)
)
)
# Make 'hard' reference to avoid GCing of the task
self.client._async_callbacks.add(task)
task.add_done_callback(self.client._async_callbacks.discard)
else:
self.client.notification_callback(
characteristic, bytearray(data)
)
# self.client._received_data.append(characteristic.getValue())

@Override(jvoid, [BluetoothGatt, jint, jint])
def onMtuChanged(self, gatt, mtu, status):
Expand All @@ -119,6 +134,10 @@ def __init__(
services=None,
**kwargs,
):
self._async_callbacks = set()
self._received_data = list()
self.__services = list()

self.activity = self.context = jclass(
'org.beeware.android.MainActivity'
).singletonThis
Expand All @@ -141,7 +160,6 @@ def __init__(
raise NotImplementedError()
self.adapter = None
self.gatt = None
self._services = []
self.mtu = 23

def __str__(self):
Expand All @@ -165,20 +183,20 @@ async def connect(self, **kwargs):
if self.gatt is not None:
self.gatt.connect()
else:
# Make a reference for external access
Client.client = self
# The services list will be re-filled by a callback later on.
self.__services.clear()

# Create a GATT connection
self.gatt_callback = _PythonGattCallback(Client.client)
self.gatt_callback = _PythonGattCallback(self)
self.gatt = self.device.connectGatt(
self.activity, False, self.gatt_callback
)
self.gatt_callback.gatt = self.gatt

# Read the services
while not services:
# Wait for the services to be received through the
# _PythonGattCallback.onServicesDiscovered call.
while not self.__services:
await asyncio.sleep(0.1)
self._services = await self._get_services()

# Ask for max Mtu size
self.gatt.requestMtu(517)
Expand All @@ -193,14 +211,11 @@ async def disconnect(self):
self.gatt.disconnect()
self.gatt.close()
except Exception as e:
status_message.append(e)
logger.error(f'Error disconnecting from client: "{e}"')

self.gatt = None
self._services.clear()
services.clear()
status_message.clear()
received_data.clear()
Client.client = None
self._received_data.clear()
self.__services.clear()

return True # For Bleak backwards compatibility

Expand All @@ -216,28 +231,14 @@ async def start_notify(self, uuid, callback, **kwargs):
self.notification_callback = callback
characteristic = self._find_characteristic(uuid)
if characteristic:
self.loop = asyncio.get_event_loop()
self.gatt.setCharacteristicNotification(characteristic, True)
descriptor = characteristic.getDescriptor(UUID.fromString(CCCD))
descriptor.setValue(
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
)
self.gatt.writeDescriptor(descriptor)

# Send received data to callback function
while self.notification_callback:
if received_data:
data = received_data.pop()
if inspect.iscoroutinefunction(callback):
task = asyncio.create_task(
callback(characteristic, bytearray(data))
)
# Make 'hard' reference to avoid GCing of the task
async_callbacks.add(task)
task.add_done_callback(async_callbacks.discard)
else:
callback(characteristic, bytearray(data))
await asyncio.sleep(0.1)

async def stop_notify(self, uuid):
"""Stop notification of a notifying characteristic."""
characteristic = self._find_characteristic(uuid)
Expand All @@ -260,9 +261,9 @@ async def read_gatt_char(self, uuid):
characteristic = self._find_characteristic(uuid)
if characteristic:
self.gatt.readCharacteristic(characteristic)
while not received_data:
while not self._received_data:
await asyncio.sleep(0.1)
return bytearray(received_data.pop())
return bytearray(self._received_data.pop())
else:
raise bleekWareCharacteristicNotFoundError(uuid)

Expand Down Expand Up @@ -319,36 +320,26 @@ def services(self):

As list of BLEGattService objects.
"""
if not self._services:
if not self.__services:
raise bleekWareError(
'Service Discovery has not been performed yet'
)

return self._services
return self.__services

async def _get_services(self):
"""Read and store the announced services of a GATT server. PRIVAT.

The characteristics of the services are also read. Both are
stored in a list of BLEGattService objects.
"""
if self._services:
return self._services
for service in services:
new_service = BLEGattService(service)
characts = service.getCharacteristics().toArray()
for charact in characts:
new_service.characteristics.append(str(charact.getUuid()))
self._services.append(new_service)
return self._services
@services.setter
def services(self, value):
"""Update the list of services."""
self.__services.clear()
self.__services.extend(value)

def _find_characteristic(self, uuid):
"""Find and return characteristic object by UUID. PRIVATE."""
if len(uuid) == 4:
uuid = f'0000{uuid}-0000-1000-8000-00805f9b34fb'
elif len(uuid) == 8:
uuid = f'{uuid}-0000-1000-8000-00805f9b34fb'
for service in self._services:
for service in self.__services:
if uuid in service.characteristics:
return service.service.getCharacteristic(UUID.fromString(uuid))
return None
6 changes: 6 additions & 0 deletions bleekWare/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
MIT license
"""

import logging
from java import jclass
from android.os import Build


# Set up logging for this module.
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(name='bleakWare')


class BLEDevice:
"""Class to hold data of a BLE device.

Expand Down