hbldh / bleak

A cross platform Bluetooth Low Energy Client for Python using asyncio
MIT License
1.53k stars 271 forks source link

Android: Permission Missing #1542

Open brinata opened 1 week ago

brinata commented 1 week ago

Trying to run the basic example given for android https://github.com/hbldh/bleak/tree/develop/examples/kivy

Added Java files from https://github.com/hbldh/bleak/tree/develop/bleak/backends/p4android/java/com/github/hbldh/bleak to local folder java and in buildozer.spec modifed android.add_src = java

log:

04-22 10:53:06.613 12555 12647 I python : [INFO ] [example ]scanning 04-22 10:53:06.614 12555 12647 I python : [DEBUG ] Starting BTLE scan 04-22 10:53:06.710 12555 12647 I python : [INFO ] [example ]ERROR User denied access to ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION', 'android.permission.ACCESS_BACKGROUND_LOCATION'] 04-22 10:53:06.710 12555 12647 I python : [ERROR ] [Exception in callback None() 04-22 10:53:06.710 12555 12647 I python : handle] 04-22 10:53:06.710 12555 12647 I python : Traceback (most recent call last): 04-22 10:53:06.710 12555 12647 I python : File "/home/ronni/progetti/app_bleak/.buildozer/android/platform/build-arm64-v8a/build/other_builds/python3/arm64-v8a__ndk_target_21/python3/Lib/asyncio/events.py", line 80, in _run 04-22 10:53:06.710 12555 12647 I python : TypeError: 'NoneType' object is not callable

brinata commented 1 week ago

After some tests, the permission request function implemented inside the scanner for android is not compatibile with the api version = 33, most probably because the p4a is changed.

the api version 33 doesn't like the request ACCESS_BACKGROUND_LOCATION.

after removing it for API version >= 30, the scanner is working, but the client give out the followind error:

    if not descriptor:
        raise BleakError(f"Descriptor {desc_specifier} was not found!")

after connecting, when calling :

await client.start_notify(UART_TX_CHAR_UUID, self.handle_rx)

brinata commented 1 week ago

Please find the logs:

04-22 18:07:23.649 26795 26958 I python : Start Scanning 04-22 18:07:23.968 26795 26958 I python : [DEBUG ] Starting BTLE scan 04-22 18:07:25.110 26795 26795 I python : [True, True] 04-22 18:07:27.559 26795 26795 I python : [True, True, True, True] 04-22 18:07:27.609 26795 26958 I python : [DEBUG ] Waiting for android api onScan 04-22 18:07:27.710 26795 26958 I python : [DEBUG ] Java state transfer onScan error=None data=(<android.bluetooth.le.ScanResult at 0x7434cafcb0 jclass=android/bluetooth/le/ScanResult jself=<LocalRef obj=0xab0e at 0x7434cab090>>,) 04-22 18:07:27.710 26795 26958 I python : [DEBUG ] onScan succeeded (<android.bluetooth.le.ScanResult at 0x7434cafcb0 jclass=android/bluetooth/le/ScanResult jself=<LocalRef obj=0xab0e at 0x7434cab090>>,) 04-22 18:07:27.826 26795 26958 I python : Nulla !!! 04-22 18:07:27.875 26795 26958 I python : trovato! 04-22 18:07:27.875 26795 26958 I python : [DEBUG ] Stopping BTLE scan 04-22 18:07:27.881 26795 26958 I python : Device found 04-22 18:07:27.884 26795 26958 I python : Attesa connessione 04-22 18:07:27.887 26795 26958 I python : [DEBUG ] [Connecting to BLE device @ BB]3D:7A:F6:4F:78 04-22 18:07:27.888 26795 26958 I python : [DEBUG ] Waiting for android api onConnectionStateChange 04-22 18:07:28.588 26795 26958 I python : [DEBUG ] Java state transfer onConnectionStateChange error=None data=(2,) 04-22 18:07:28.588 26795 26958 I python : [DEBUG ] onConnectionStateChange succeeded () 04-22 18:07:28.589 26795 26958 I python : [DEBUG ] Connection successful. 04-22 18:07:28.590 26795 26958 I python : [DEBUG ] requesting mtu... 04-22 18:07:28.590 26795 26958 I python : [DEBUG ] Waiting for android api onMtuChanged 04-22 18:07:29.316 26795 26958 I python : [DEBUG ] Java state transfer onMtuChanged error=None data=(247,) 04-22 18:07:29.318 26795 26958 I python : [DEBUG ] onMtuChanged succeeded (247,) 04-22 18:07:29.318 26795 26958 I python : [DEBUG ] discovering services... 04-22 18:07:29.319 26795 26958 I python : [DEBUG ] Waiting for android api onServicesDiscovered 04-22 18:07:29.338 26795 26958 I python : [DEBUG ] Java state transfer onServicesDiscovered error=None data=() 04-22 18:07:29.340 26795 26958 I python : [DEBUG ] onServicesDiscovered succeeded () 04-22 18:07:29.341 26795 26958 I python : [DEBUG ] Get Services... 04-22 18:07:29.371 26795 26958 I python : Connected, start typing and press ENTER... 04-22 18:07:29.375 26795 26958 I python : ERROR Descriptor None was not found! 04-22 18:07:29.400 26795 26958 I python : ERROR Descriptor None was not found! 04-22 18:07:29.415 26795 26958 I python : [DEBUG ] BTLE scan already stopped

brinata commented 1 week ago

and the implemented python code:

async def ble_service(self):

    await asyncio.sleep(1)
    print("Start Scanning")
    device = None

    while True:
        if self.scanning :
            try:
                #cancello il device se già esistente
                if device is not None:
                    del device

                device = await BleakScanner.find_device_by_filter(self.match_nus_uuid)

                if device is None:
                    print("Device not present")

                else:
                    print("Device found")
                    try:
                        client = BleakClient(device, disconnected_callback=self.handle_disconnect, timeout=5)
                        print("Attesa connessione")
                        self.connected = await client.connect()
                        if self.connected is True:
                            self.topbar_status_color = 'green'
                            print("Connected, start typing and press ENTER...")
                            await client.start_notify(UART_TX_CHAR_UUID, self.handle_rx)   
                            # ottengo il servizio di interesse
                            print("uno")
                            nus = client.services.get_service(UART_SERVICE_UUID)
                            # ottengo la caratteristica di interesse
                            rx_char = nus.get_characteristic(UART_RX_CHAR_UUID)
                            print("due")

                            await asyncio.sleep(0.5)
                            self.invia_comando(BLE.BLE_ACT_READ)
        #                    Clock.schedule_once(self.invia_comando, 0.5)

                            print("entro nel loop")
                            while True:
                                await asyncio.sleep(0.25)

                                if self.end_app is True:
                                    return

                                if self.connected is False:
        #                        if client.is_connected() is False:
                                    print("dispositivo disconnesso, esco dal loop")
                                    while not self.tx_queue.empty():
                                        # Depending on your program, you may want to
                                        # catch QueueEmpty
                                        self.tx_queue.get_nowait()
                                        self.tx_queue.task_done()
                                    await client.disconnect()
                                    break

                                if self.tx_queue.empty() is False:
                                    data = self.tx_queue.get_nowait()

                                    print(data)

                                    data = bytes([STX]) + data + bytes([ETX])

                                    minimo = min(len(data), 20, rx_char.max_write_without_response_size)

                                    self.ms = time.time()    

                                    for s in sliced(data, minimo):
                                        await client.write_gatt_char(rx_char, s)

                                print("sent:", data)                        
                    except BleakError as e:
                        print(f"ERROR {e}")
                        Clock.schedule_once(lambda dt: self.scanning_dialog(f"ERROR {e}"), 0.05)
                        await asyncio.sleep(2)

            except BleakError as e:

                print(f"ERROR {e}")
                Clock.schedule_once(lambda dt: self.scanning_dialog(f"ERROR {e}"), 0.05)
                await asyncio.sleep(2)

            except jnius.jnius.JavaException as e:
                print(f"ERROR {e}")
                Clock.schedule_once(lambda dt: self.scanning_dialog(f"ERROR {e}"), 0.05)
                await asyncio.sleep(2)

        else:
            await asyncio.sleep(2)
dlech commented 1 week ago

Permission issue is possible duplicate of #1363.

Descriptor issues is possible duplicate of/related to #883

brinata commented 1 week ago

the BLE define:

UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

the print result inside the start_notify:

print(characteristic) --> 6e400003-b5a3-f393-e0a9-e50e24dcca9e (Handle: 14): Nordic UART TX

brinata commented 1 week ago

before calling start_noify:

print(client.services.characteristics) -->

04-22 18:44:38.587 32277 32401 I python : {3: <bleak.backends.p4android.characteristic.BleakGATTCharacteristicP4Android object at 0x7434bf7010>, 5: <bleak.backends.p4android.characteristic.BleakGATTCharacteristicP4Android object at 0x7434bf7050>, 8: <bleak.backends.p4android.characteristic.BleakGATTCharacteristicP4Android object at 0x7434bbbb10>, 12: <bleak.backends.p4android.characteristic.BleakGATTCharacteristicP4Android object at 0x7434bbbad0>, 14: <bleak.backends.p4android.characteristic.BleakGATTCharacteristicP4Android object at 0x7434bb9250>}

brinata commented 1 week ago

Can you suggest any workaround ?

brinata commented 1 week ago

Descriptor issue:

After some test i discovered that the issue is in the add_descriptor function ( \bleak\backends\p4android\characteristic.py)

i added some debug print and modification as follows:

    def add_descriptor(self, descriptor: BleakGATTDescriptor):
        """Add a :py:class:`~BleakGATTDescriptor` to the characteristic.

        Should not be used by end user, but rather by `bleak` itself.
        """
        self.__descriptors.append(descriptor)
        print("descriptor.uuid...", descriptor.uuid)
        print("defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID...", defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID)
#        if descriptor.uuid == defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID:
        if defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID in descriptor.uuid:
            self.__notification_descriptor = descriptor

        print(self.__notification_descriptor)

it works.

the problem is that the fileds "descriptor.uuid" and "defs.CLIENT_CHARACTERISTIC_CONFIGURATION_UUID" are different, so the operation "self.__notification_descriptor = descriptor" never happens

Permission issue:

i modified the "start" function of the scanner as follow:

    async def start(self) -> None:
        if BleakScannerP4Android.__scanner is not None:
            raise BleakError("A BleakScanner is already scanning on this adapter.")

        logger.debug("Starting BTLE scan")

        loop = asyncio.get_running_loop()

        if self.__javascanner is None:
            if self.__callback is None:
                self.__callback = _PythonScanCallback(self, loop)

            permission_acknowledged = loop.create_future()

            def handle_permissions(permissions, grantResults):
                if any(grantResults):
                    print(grantResults)
                    loop.call_soon_threadsafe(
                        permission_acknowledged.set_result, grantResults
                    )
                else:
                    loop.call_soon_threadsafe(
                        permission_acknowledged.set_exception(
                            BleakError("User denied access to " + str(permissions))
                        )
                    )

            request_permissions(
                [
                    Permission.ACCESS_FINE_LOCATION,
                    Permission.ACCESS_COARSE_LOCATION
#                    "android.permission.ACCESS_BACKGROUND_LOCATION",
                ],
                handle_permissions,
            )
            await permission_acknowledged

            VERSION = autoclass('android.os.Build$VERSION')

            if VERSION.SDK_INT < 30:
                permission_acknowledged.cancel()
                permission_acknowledged = loop.create_future()
                request_permissions(
                    [
    #                    Permission.ACCESS_FINE_LOCATION,
    #                    Permission.ACCESS_COARSE_LOCATION,
                        "android.permission.ACCESS_BACKGROUND_LOCATION"
                    ],
                    handle_permissions,
                )
                await permission_acknowledged

            permission_acknowledged.cancel()
            permission_acknowledged = loop.create_future()

            request_permissions(
                [
                        Permission.BLUETOOTH_SCAN,
                        Permission.BLUETOOTH_CONNECT,
                        Permission.BLUETOOTH_ADMIN,
                        Permission.BLUETOOTH
                ],
                handle_permissions,
            )
            await permission_acknowledged

            self.__adapter = defs.BluetoothAdapter.getDefaultAdapter()
            if self.__adapter is None:
                raise BleakError("Bluetooth is not supported on this hardware platform")
            if self.__adapter.getState() != defs.BluetoothAdapter.STATE_ON:
                raise BleakError("Bluetooth is not turned on")

            self.__javascanner = self.__adapter.getBluetoothLeScanner()

        BleakScannerP4Android.__scanner = self

        filters = cast("java.util.List", defs.List())
        if self._service_uuids:
            for uuid in self._service_uuids:
                filters.add(
                    defs.ScanFilterBuilder()
                    .setServiceUuid(defs.ParcelUuid.fromString(uuid))
                    .build()
                )

        scanfuture = self.__callback.perform_and_wait(
            dispatchApi=self.__javascanner.startScan,
            dispatchParams=(
                filters,
                defs.ScanSettingsBuilder()
                .setScanMode(self.__scan_mode)
                .setReportDelay(0)
                .setPhy(defs.ScanSettings.PHY_LE_ALL_SUPPORTED)
                .setNumOfMatches(defs.ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
                .setMatchMode(defs.ScanSettings.MATCH_MODE_AGGRESSIVE)
                .setCallbackType(defs.ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
                .build(),
                self.__callback.java,
            ),
            resultApi="onScan",
            return_indicates_status=False,
        )
        self.__javascanner.flushPendingScanResults(self.__callback.java)

        try:
            async with async_timeout(0.2):
                await scanfuture
        except asyncio.exceptions.TimeoutError:
            pass
        except BleakError as bleakerror:
            await self.stop()
            if bleakerror.args != (
                "onScan",
                "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED",
            ):
                raise bleakerror
            else:
                # there might be a clearer solution to this if android source and vendor
                # documentation are reviewed for the meaning of the error
                # https://stackoverflow.com/questions/27516399/solution-for-ble-scans-scan-failed-application-registration-failed
                warnings.warn(
                    "BT API gave SCAN_FAILED_APPLICATION_REGISTRATION_FAILED.  Resetting adapter."
                )

                def handlerWaitingForState(state, stateFuture):
                    def handleAdapterStateChanged(context, intent):
                        adapter_state = intent.getIntExtra(
                            defs.BluetoothAdapter.EXTRA_STATE,
                            defs.BluetoothAdapter.STATE_ERROR,
                        )
                        if adapter_state == defs.BluetoothAdapter.STATE_ERROR:
                            loop.call_soon_threadsafe(
                                stateOffFuture.set_exception,
                                BleakError(f"Unexpected adapter state {adapter_state}"),
                            )
                        elif adapter_state == state:
                            loop.call_soon_threadsafe(
                                stateFuture.set_result, adapter_state
                            )

                    return handleAdapterStateChanged

                logger.info(
                    "disabling bluetooth adapter to handle SCAN_FAILED_APPLICATION_REGSTRATION_FAILED ..."
                )
                stateOffFuture = loop.create_future()
                receiver = BroadcastReceiver(
                    handlerWaitingForState(
                        defs.BluetoothAdapter.STATE_OFF, stateOffFuture
                    ),
                    actions=[defs.BluetoothAdapter.ACTION_STATE_CHANGED],
                )
                receiver.start()
                try:
                    self.__adapter.disable()
                    await stateOffFuture
                finally:
                    receiver.stop()

                logger.info("re-enabling bluetooth adapter ...")
                stateOnFuture = loop.create_future()
                receiver = BroadcastReceiver(
                    handlerWaitingForState(
                        defs.BluetoothAdapter.STATE_ON, stateOnFuture
                    ),
                    actions=[defs.BluetoothAdapter.ACTION_STATE_CHANGED],
                )
                receiver.start()
                try:
                    self.__adapter.enable()
                    await stateOnFuture
                finally:
                    receiver.stop()
                logger.debug("restarting scan ...")

                return await self.start()

it seems it works. the issue is in the api version : if you ask for all the postion permissions simultaneously the OS always replys immediatly with a negative answare. If yoy ask for the ACCESS_BACKGROUND_LOCATION later it works