From bf71f4ec0a75ca25d6c12d402eeaec6be182b582 Mon Sep 17 00:00:00 2001 From: Helio Perroni Filho Date: Tue, 16 Jul 2024 14:16:05 -0400 Subject: [PATCH] Moved global collections into the Client class Previously, the Client class relied on global collections to exchange data with the Android callback objects. This would cause problems when trying to simultaneously connect to multiple devices. Fixed by moving the global collections into the Client class. A note about member naming conventions: in Python, "true" private members are prefixed by two underscores ("__"), while members prefixed with only one underscore ("_") are still public, but by convention should not be accessed by external code unless strictly necessary.The committed changes follow this convention, e.g. Client._received_data is kept accessible so it can be modified by Android callbacks, while Client.__services is only accessed from inside the class itself. References: * https://docs.python.org/3/tutorial/classes.html#private-variables Signed-off-by: Helio Perroni Filho --- bleekWare/Client.py | 92 +++++++++++++++++++------------------------ bleekWare/__init__.py | 6 +++ 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/bleekWare/Client.py b/bleekWare/Client.py index 0e06c94..d6654cb 100644 --- a/bleekWare/Client.py +++ b/bleekWare/Client.py @@ -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' @@ -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() @@ -61,9 +54,14 @@ 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, @@ -85,7 +83,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)] @@ -95,7 +93,7 @@ def onCharacteristicChanged(self, gatt, characteristic, value): This is the callback function for notifying services. """ - received_data.append(characteristic.getValue()) + self.client._received_data.append(characteristic.getValue()) @Override(jvoid, [BluetoothGatt, jint, jint]) def onMtuChanged(self, gatt, mtu, status): @@ -119,6 +117,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 @@ -141,7 +143,6 @@ def __init__( raise NotImplementedError() self.adapter = None self.gatt = None - self._services = [] self.mtu = 23 def __str__(self): @@ -165,20 +166,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) @@ -193,14 +194,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 @@ -225,15 +223,15 @@ async def start_notify(self, uuid, callback, **kwargs): # Send received data to callback function while self.notification_callback: - if received_data: - data = received_data.pop() + if self._received_data: + data = self._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) + self.__async_callbacks.add(task) + task.add_done_callback(self.__async_callbacks.discard) else: callback(characteristic, bytearray(data)) await asyncio.sleep(0.1) @@ -260,9 +258,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) @@ -319,28 +317,18 @@ 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 - - async def _get_services(self): - """Read and store the announced services of a GATT server. PRIVAT. + return self.__services - 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.""" @@ -348,7 +336,7 @@ def _find_characteristic(self, uuid): 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 diff --git a/bleekWare/__init__.py b/bleekWare/__init__.py index 38689f7..5682891 100644 --- a/bleekWare/__init__.py +++ b/bleekWare/__init__.py @@ -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.