hbldh / bleak

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

AttributeError: 'BleakScannerWinRT' object has no attribute 'call_detection_callbacks' #1536

Closed mantisbug closed 2 weeks ago

mantisbug commented 1 month ago

This is regarding: https://github.com/hbldh/bleak/issues/1470#issue-2018400969 I'm using the same program on both computers. I work over BLE just fine with a Lenovo ThinkPad 32GB RAM Windows 10. The odd hanging now and then.

With this other computer (Windows 11), though, my program hangs when trying to BLE connect. Scanning devices work every time. No problem with discovering devices when they are advertising. I can find them but the connection fails.

What I Did

As simple as:

wearable = comms_ble.AfonWearable(w_address)
connected = False
while not connected:
    try:
        await wearable.connect()
        connected = wearable.is_connected
    except Exception as e:
        logger.info(f'Error trying to connect {w_address}: {e}')

I tried to debug it, but it just hangs in await wearable.connect() It does not come back. I have not been able to make it run in Windows 11.

It's the same with:

async with comms_ble.AfonWearable(w_address) as w:
    logger.info(f'Subscribing to characteristic {NOTIFY}.')
    subscribed = await w.subscribe_to_characteristic(NOTIFY)

    if subscribed:
        pass

I've found the following error in my log:

AttributeError: 'BleakScannerWinRT' object has no attribute 'call_detection_callbacks'

Logs

15Apr2024 14:09:52: Start logging...
15Apr2024 14:09:52: === Configure device 80:E1:26:08:E7:A4: _NAME_OF_MY_DEVICE_ ===
15Apr2024 14:09:55: Requesting resonance(s) set up...
15Apr2024 14:10:06: Exception in callback BleakScannerWinRT._received_handler(<_bleak_winrt...001B645EE5950>, <_bleak_winrt...001B645EE7E90>)
handle: <Handle BleakScannerWinRT._received_handler(<_bleak_winrt...001B645EE5950>, <_bleak_winrt...001B645EE7E90>)>
Traceback (most recent call last):
  File "C:\Users\lab.user2\AppData\Local\Programs\Python\Python311\Lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "C:\Users\lab.user2\AppData\Local\Programs\Python\Python311\Lib\site-packages\bleak\backends\winrt\scanner.py", line 212, in _received_handler
    self.call_detection_callbacks(device, advertisement_data)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'BleakScannerWinRT' object has no attribute 'call_detection_callbacks'
15Apr2024 14:10:06: Exception in callback BleakScannerWinRT._received_handler(<_bleak_winrt...001B645DA14F0>, <_bleak_winrt...001B645EE5CD0>)
handle: <Handle BleakScannerWinRT._received_handler(<_bleak_winrt...001B645DA14F0>, <_bleak_winrt...001B645EE5CD0>)>
Traceback (most recent call last):
  File "C:\Users\lab.user2\AppData\Local\Programs\Python\Python311\Lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "C:\Users\lab.user2\AppData\Local\Programs\Python\Python311\Lib\site-packages\bleak\backends\winrt\scanner.py", line 212, in _received_handler
    self.call_detection_callbacks(device, advertisement_data)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'BleakScannerWinRT' object has no attribute 'call_detection_callbacks'
15Apr2024 14:10:06: Exception in callback BleakScannerWinRT._received_handler(<_bleak_winrt...001B645EE5D90>, <_bleak_winrt...001B645EE65D0>)
handle: <Handle BleakScannerWinRT._received_handler(<_bleak_winrt...001B645EE5D90>, <_bleak_winrt...001B645EE65D0>)>
Traceback (most recent call last):
  File "C:\Users\lab.user2\AppData\Local\Programs\Python\Python311\Lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "C:\Users\lab.user2\AppData\Local\Programs\Python\Python311\Lib\site-packages\bleak\backends\winrt\scanner.py", line 212, in _received_handler
    self.call_detection_callbacks(device, advertisement_data)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'BleakScannerWinRT' object has no attribute 'call_detection_callbacks'
15Apr2024 14:10:06: Exception in callback BleakScannerWinRT._received_handler(<_bleak_winrt...001B645EE6450>, <_bleak_winrt...001B645EE5E70>)
handle: <Handle BleakScannerWinRT._received_handler(<_bleak_winrt...001B645EE6450>, <_bleak_winrt...001B645EE5E70>)>
Traceback (most recent call last):
  File "C:\Users\lab.user2\AppData\Local\Programs\Python\Python311\Lib\asyncio\events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "C:\Users\lab.user2\AppData\Local\Programs\Python\Python311\Lib\site-packages\bleak\backends\winrt\scanner.py", line 212, in _received_handler
    self.call_detection_callbacks(device, advertisement_data)

Could anybody who has experienced this please help?

Thank you.

dlech commented 1 month ago

As simple as:

Can you share the full script? This snippet looks like an infinite loop that keeps connecting without waiting for disconnect.

It does not come back. I have not been able to make it run in Windows 11.

This sounds like #1262 and #1470

AttributeError: 'BleakScannerWinRT' object has no attribute 'call_detection_callbacks'

Can't say on this one without seeing the full script either.

mantisbug commented 1 month ago

As simple as:

Can you share the full script? This snippet looks like an infinite loop that keeps connecting without waiting for disconnect.

It does not come back. I have not been able to make it run in Windows 11.

This sounds like #1262 and #1470

AttributeError: 'BleakScannerWinRT' object has no attribute 'call_detection_callbacks'

Can't say on this one without seeing the full script either.

I have taken off the disconnected_callback parameter from my BleakClient connection.

I have written a class for my program... I am figuring out how to put it here.

class TheNameOfMyClass(BleakClient):
    def __init__(self, device):
        super().__init__(address_or_ble_device=device, timeout=12, disconnected_callback=self.callback_disconnect)
        self.buffer = {}  # To receive data from BLE device
        # Some configuration for my app here

    def callback_disconnect(self, event):
        """
            Callback that will be scheduled in the event loop when
            the client is disconnected.
            The callable must take one argument which will be this client object.
        """
        self.device_connected = False
        logger.debug(f'DISCONNECTED [{event}].')

    async def notification_handler(self,  sender: BleakGATTCharacteristic,  data_received: bytearray):
        """
        The function to be called on notification.
        The data received will be stored in a dictionary:
        list[timestamp, bytearray] (Timestamp and data received)

        :param sender: The characteristic to activate notifications on
        the characteristic specified by integer handle.
        :param data_received: (bytearray) Data received.
        :return: None. The data is saved in variable self.buffer[self.index]
        """
        self.buffer_index += 1
        logger.debug(f'Received from {sender} buffer index: '
                     f'[{self.buffer_index}]:\n{data_received}')
        self.buffer[self.buffer_index] = {'packet': data_received}

    async def subscribe_to_characteristic(self):
        """
        Activate notifications/indications on a characteristic.
        Callbacks must accept two inputs.
        The first will be the characteristic, and the second will be
        a bytearray containing the data received.

        :return: Boolean success
        """
        try:
            await self.start_notify(char_specifier=self.notify_handler, callback=self.notification_handler)

            logger.debug(f'Characteristic {self.notify_handler} notified.')
            return True

        except Exception as error:
            logger.debug(traceback.format_exc())
            logger.debug(f'Error notifying characteristic '
                         f'{self.notify_handler}:\n{error}')
            return False

After getting rid of the disconnected_callback=self.callback_disconnect, I got the following (different) error:

Log

15Apr2024 16:43:44: 26 devices found. Watcher status: <BluetoothLEAdvertisementWatcherStatus.STOPPED: 3>.
15Apr2024 16:43:44: Connecting to BLE device @ 80:E1:26:08:EB:3A
15Apr2024 16:43:44: getting services (service_cache_mode=None, cache_mode=None)...
15Apr2024 16:43:45: session_status_changed_event_handler: id: BluetoothLE#BluetoothLE84:1b:77:6c:19:88-80:e1:26:08:eb:3a, error: <BluetoothError.SUCCESS: 0>, status: <GattSessionStatus.ACTIVE: 1>
15Apr2024 16:43:45: max_pdu_size_changed_handler: 156
15Apr2024 16:43:45: 80:E1:26:08:EB:3A: services changed
15Apr2024 16:43:46: 80:E1:26:08:EB:3A: services changed

That services changed I get it in my other laptop when it hangs (not so much lately, but it happens once in a while).

mantisbug commented 1 month ago

From the log, this is happening over and over and over again:

15Apr2024 17:13:01: Connecting to BLE device @ 80:E1:26:08:EB:3A
15Apr2024 17:13:01: getting services (service_cache_mode=None, cache_mode=None)...
15Apr2024 17:13:01: session_status_changed_event_handler: id: BluetoothLE#BluetoothLE84:1b:77:6c:19:88-80:e1:26:08:eb:3a, error: <BluetoothError.SUCCESS: 0>, status: <GattSessionStatus.ACTIVE: 1>
15Apr2024 17:13:01: max_pdu_size_changed_handler: 156
15Apr2024 17:13:02: 80:E1:26:08:EB:3A: services changed
15Apr2024 17:13:03: 80:E1:26:08:EB:3A: services changed

I will write a simple little program to test the basics.

mantisbug commented 1 month ago

Well, it works on this computer with the very basic:

import asyncio
from bleak import BleakClient

address = "80:E1:26:08:EB:3A"

async def main(address):
    async with BleakClient(address) as client:
        print(f'Connected?: {client.is_connected}')

asyncio.run(main(address))

Result Connected?: True

mantisbug commented 1 month ago

With my class it also works:

import asyncio
from comms.comms_ble import TheNameOfMyClass

address = "80:E1:26:08:EB:3A"

async def main(address):
    async with TheNameOfMyClass(address) as client:
        print(f'Connected?: {client.is_connected}')

asyncio.run(main(address))

Result Connected?: True

mantisbug commented 1 month ago

Alright. I have to go through my program line per line to see what's going on. If I find something to change, I'll document it here.

mantisbug commented 1 month ago

Found it:

I am using:

  1. Tkinter for a graphic interface.

  2. A cross-platform BLE Client for Python using asyncio: "Testing the idea of providing an off-the-shelf Tk event loop for asyncio" https://github.com/smontanaro/tk-asyncio With help from: @dlech, @smontanaro, @JoeEveryman

  3. Bleak

Some of my buttons call async functions. For example:

    # tkinter GUI configuration here...
    # various widgets
    self.btn_stop = tk.Button(self.frm_configuration,
                              text=' Stop ',
                              command=lambda: self.add_button_coro(function_here()))

async def functio_here():
    # Bleak comms here

I have changed them to:

    self.btn_stop = tk.Button(self.frm_configuration,
                              text=' Stop ',
                              command=function_that_calls_the_function)

def function_that_calls_the_function():
    asyncio.create_task(function_here())

async def functio_here():
    # Bleak comms here

It has been running smoothly on Windows 11 for +2 hours now (continuously on a cycle connect/disconnect every 25 seconds).

dlech commented 1 month ago

FYI, according to the Python docs for asyncio.create_task() you should be keeping a handle to the task that is returned so that it doesn't get GCed.

https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task (See "Important:" paragraph)

mantisbug commented 1 month ago

Thank you, @dlech. Working on that.

Using the add_button_coro(self, coro) was giving me that problem I don't know how to recover from. Look, I use this from @smontanaro https://github.com/smontanaro/tk-asyncio:

class AsyncTk(Tk):
    def __init__(self):
        super().__init__()
        self.running = True
        self.runners = [self.tk_loop()]
        self.button_presses = []

    async def tk_loop(self):
        while self.running:
            self.update()
            await asyncio.sleep(0.05)
            if len(self.button_presses) > 0:
                await self.button_presses.pop(0)

    def stop(self):
        self.running = False

    async def run(self):
        await asyncio.gather(*self.runners)

    def add_button_coro(self, coro):
        task = asyncio.create_task(coro)
        self.button_presses.append(task)

So I know now that using that call within a button gets me in trouble with Windows 11. That's what I found out last night. I am now figuring out how to queue the task into the usual button_presses stack as cleanly as possible: I have some cascading frames. Not proud of my solution so far.

smontanaro commented 1 month ago

Note that tk-asyncio is just the result of me messing around. I was curious about the idea, wasn't even scratching an itch. It no doubt has shortcomings which would need to be addressed for anything more than trivial applications.

mantisbug commented 1 month ago

Thank you, @smontanaro, this solution of yours has helped me a lot when building GUIs to test our homemade devices. Those make Microwave Engineers' lives easier and had worked beautifully until we got these new Windows 11 laptops.

I have tried changing the calls from bleak-related buttons as:

def function_that_calls_the_function(self):
    self.parent.add_button_coro(self.function_here())

Once my sript starts running, I can see in the console and the debug logger that it is doing what it's expected to: connects to the device, gets data, saves data to disk, but it is not plotting results and I cannot press any button nor tabs or editable fields any more: the GUI gets frozen.

I will step back and will document if I find another way. So far we are using it with:

def function_that_calls_the_function():
    asyncio.create_task(function_here())
dlech commented 2 weeks ago

So, is there an actual bug here that still needs to be fixed?

mantisbug commented 2 weeks ago

So, is there an actual bug here that still needs to be fixed?

I don't think so, David.

Thank you, though for: "https://github.com/hbldh/bleak/pull/1548: This adds a check to give a useful error message instead of silently failing if Bluetooth is off." That helps.

dlech commented 2 weeks ago

OK, great, we'll close this one then.