bleak icon indicating copy to clipboard operation
bleak copied to clipboard

Pairing on windows successful, but is blocked until it times out

Open mgagic opened this issue 6 months ago • 8 comments

python version: 3.12.0 bleak version: 0.22.3, 1.0.1 windows 11

I am connecting with an nRF52840 device and pairing always takes exactly 1 minute to finish, even though I can see on the device that it's been successful after a few seconds.

Is there a way to check the status of the pairing and finish this as soon as it succeeds? It seems to be triggered by some timeout as it's always 1 minute, is this a library or the OS problem? My use case consists of connecting to a lot of devices consecutively so every second counts.

as far as I can see, wireshark logs show pairing is done quite fast and there's no communication after it until ~1min passes (09:02:49.371851 to 09:02:53.719327)

395	2025-07-04 09:02:48.614635	localhost ()	c5:1f:b9:b4:3e:97	ATT	38	Sent Exchange MTU Request, Client Rx MTU: 527
399	2025-07-04 09:02:48.707769	c5:1f:b9:b4:3e:97	localhost ()	ATT	38	Rcvd Exchange MTU Response, Server Rx MTU: 498
401	2025-07-04 09:02:48.708490	localhost ()	c5:1f:b9:b4:3e:97	ATT	42	Sent Read By Type Request, Database Hash, Handles: 0x0001..0xffff
405	2025-07-04 09:02:48.805365	c5:1f:b9:b4:3e:97	localhost ()	ATT	55	Rcvd Read By Type Response, Attribute List Length: 1
407	2025-07-04 09:02:48.806164	localhost ()	c5:1f:b9:b4:3e:97	ATT	40	Sent Write Request, Handle: 0x0004 (Unknown)
411	2025-07-04 09:02:48.896089	localhost ()	c5:1f:b9:b4:3e:97	SMP	42	Sent Pairing Request: AuthReq: Bonding, SecureConnection, Reserved | Initiator Key(s): IRK, CSRK, Linkkey | Responder Key(s): LTK, IRK, CSRK, Linkkey
413	2025-07-04 09:02:48.902871	c5:1f:b9:b4:3e:97	localhost ()	ATT	36	Rcvd Write Response, Handle: 0x0004 (Unknown)
417	2025-07-04 09:02:48.903612	localhost ()	c5:1f:b9:b4:3e:97)	ATT	39	Sent Write Request, Handle: 0x0006 (Unknown)
421	2025-07-04 09:02:48.952399	c5:1f:b9:b4:3e:97	localhost ()	SMP	42	Rcvd Pairing Response: AuthReq: No Bonding, SecureConnection | Initiator Key(s): <none> | Responder Key(s): <none>
423	2025-07-04 09:02:48.953083	localhost ()	c5:1f:b9:b4:3e:97	SMP	100	Sent Pairing Public Key
425	2025-07-04 09:02:49.001037	c5:1f:b9:b4:3e:97	localhost ()	ATT	36	Rcvd Write Response, Handle: 0x0006 (Unknown)
431	2025-07-04 09:02:49.002548	localhost ()	c5:1f:b9:b4:3e:97	ATT	38	Sent Read Request, Handle: 0x000f (Unknown)
437	2025-07-04 09:02:49.195966	c5:1f:b9:b4:3e:97	localhost ()	HCI_ACL	58	Rcvd  [Reassembled in #441]
439	2025-07-04 09:02:49.196121	c5:1f:b9:b4:3e:97	localhost ()	HCI_ACL	58	Rcvd  [Continuation to #437] [Reassembled in #441]
441	2025-07-04 09:02:49.196698	c5:1f:b9:b4:3e:97	localhost ()	SMP	46	Rcvd Pairing Public Key
443	2025-07-04 09:02:49.197214	c5:1f:b9:b4:3e:97	localhost ()	SMP	52	Rcvd Pairing Confirm
445	2025-07-04 09:02:49.197755	localhost ()	c5:1f:b9:b4:3e:97	SMP	52	Sent Pairing Random
447	2025-07-04 09:02:49.197898	c5:1f:b9:b4:3e:97	localhost ()	ATT	44	Rcvd Read Response, Handle: 0x000f (Unknown)
449	2025-07-04 09:02:49.198333	localhost ()	c5:1f:b9:b4:3e:97	ATT	38	Sent Read Request, Handle: 0x000d (Unknown)
455	2025-07-04 09:02:49.293079	c5:1f:b9:b4:3e:97	localhost ()	SMP	52	Rcvd Pairing Random
457	2025-07-04 09:02:49.293397	c5:1f:b9:b4:3e:97	localhost ()	ATT	38	Rcvd Read Response, Handle: 0x000d (Unknown)
459	2025-07-04 09:02:49.294821	localhost ()	c5:1f:b9:b4:3e:97	ATT	38	Sent Read Request, Handle: 0x000b (Unknown)
461	2025-07-04 09:02:49.297173	localhost ()	c5:1f:b9:b4:3e:97	SMP	52	Sent Pairing DHKey Check
465	2025-07-04 09:02:49.344870	c5:1f:b9:b4:3e:97	localhost ()	ATT	55	Rcvd Read Response, Handle: 0x000b (Unknown)
471	2025-07-04 09:02:49.346071	localhost ()	c5:1f:b9:b4:3e:97	ATT	42	Sent Read By Type Request, Database Hash, Handles: 0x0001..0xffff
475	2025-07-04 09:02:49.358275	c5:1f:b9:b4:3e:97	localhost ()	SMP	52	Rcvd Pairing DHKey Check
481	2025-07-04 09:02:49.371851	c5:1f:b9:b4:3e:97	localhost ()	ATT	55	Rcvd Read By Type Response, Attribute List Length: 1
500	2025-07-04 09:02:53.719327	c5:1f:b9:b4:3e:97	localhost ()	L2CAP	47	Rcvd Connection Parameter Update Request
505	2025-07-04 09:02:53.719448	localhost ()	c5:1f:b9:b4:3e:97	L2CAP	41	Sent Connection Parameter Update Response (Accepted)

I added some logs to bleak's client.py and I can see the delay is in the pair() method

2025-07-04 09:02:48 INFO pairing

2025-07-04 09:03:49 INFO paired

the logs surround the call to

pairing_result = (
    await custom_pairing.pair_with_protection_level_async(
        ceremony, level
    )
)

mgagic avatar Jul 04 '25 07:07 mgagic

Without seeing the full program, it is hard to tell. For example, if the asyncio event loop isn't running this could explain the delay.

But if the Windows API really takes this long to return, there isn't going to be anything we can do about it.

In my own testing, this pairing function returns quickly. I have a nRF52840 development board, so if you can make a minimal reproducible test case firmware and bleak program, I could look into it more. But at this point, I kind of suspect the issue could be with the Windows Bluetooth adpater/driver.

dlech avatar Jul 04 '25 15:07 dlech

Thanks for looking into it!

Here is a minimal example

import asyncio
from bleak import BleakScanner, BleakClient
import argparse
import logging
logger = logging.getLogger(__name__)
logging.basicConfig(
    level=logging.INFO,
    format="\n%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)


async def pairing_example(target_id):
    logger.info("Scanning for BLE devices...")
    found_devices = await BleakScanner.discover(return_adv=True)

    target_device = None
    for addr, (device, adv) in found_devices.items():
        if target_id == device.name or target_id == addr:
            target_device = device
            break
    
    if not target_device:
        logger.warn(f"Device '{target_identifier}' not found.")
        return
    
    logger.info(f"Found device: {target_device.name} ({target_device.address})")
    
    async with BleakClient(target_device) as client:
        await client.unpair()

        try:
            logger.info("Pairing")
            await client.pair()
        except Exception as e:
            logger.error(f"Problem pairing the device, exception: {e}")
        
        if not client.is_connected:
            logger.error("Failed to connect.")
            return
        
        logger.info("Connected successfully.")
        for service in client.services:
            logger.info(service)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Connect to a BLE device with bleak")
    parser.add_argument(
        "identifier", 
        help="Device name or MAC address (e.g. 'someDeviceName' or '12:34:56:78:9A:BC')"
    )
    args = parser.parse_args()
    asyncio.run(pairing_example(args.identifier))

maybe important - our devices don't have bonding enabled, with bonding it works quicker, but we need the bonding disabled

we needed to add a call to unpair for windows because it seems device_information.pairing.is_paired is always True, even if the device is "forgotten" and then wee see the error Could not start notify on 0011: Protocol Error 0x05: Insufficient Authentication when communicating with it. (won't be seen in this example, I removed the communication as we're using a custom service, and communication protocol)

Let me know if I can provide any more information.

mgagic avatar Jul 07 '25 06:07 mgagic

`` for windows because it seems device_information.pairing.is_paired is always True

What happens if we remove the test for this in the pair() method and let it try to pair anyway?

even if the device is "forgotten"

Can you explain what this means in technical terms?

dlech avatar Jul 07 '25 14:07 dlech

`` for windows because it seems device_information.pairing.is_paired is always True

What happens if we remove the test for this in the pair() method and let it try to pair anyway?

I tried that as well, pair() exits instantly, but the error when accessing characteristics is exactly the same Judging by the wireshark logs, pairing doesn't seem to actually be performed

505	2025-07-07 20:34:51.375418	localhost ()	c5:1f:b9:b4:3e:97	ATT	38	Sent Exchange MTU Request, Client Rx MTU: 527
509	2025-07-07 20:34:51.470422	c5:1f:b9:b4:3e:97	localhost ()	ATT	38	Rcvd Exchange MTU Response, Server Rx MTU: 498
511	2025-07-07 20:34:51.471630	localhost ()	c5:1f:b9:b4:3e:97	ATT	42	Sent Read By Type Request, Database Hash, Handles: 0x0001..0xffff
515	2025-07-07 20:34:51.568003	c5:1f:b9:b4:3e:97	localhost ()	ATT	55	Rcvd Read By Type Response, Attribute List Length: 1
517	2025-07-07 20:34:51.568841	localhost ()	c5:1f:b9:b4:3e:97	ATT	40	Sent Write Request, Handle: 0x0004 (Unknown)
521	2025-07-07 20:34:51.664956	c5:1f:b9:b4:3e:97	localhost ()	ATT	36	Rcvd Write Response, Handle: 0x0004 (Unknown)
523	2025-07-07 20:34:51.665675	localhost ()	c5:1f:b9:b4:3e:97	ATT	39	Sent Write Request, Handle: 0x0006 (Unknown)
527	2025-07-07 20:34:51.762941	c5:1f:b9:b4:3e:97	localhost ()	ATT	36	Rcvd Write Response, Handle: 0x0006 (Unknown)
531	2025-07-07 20:34:51.764853	localhost ()	c5:1f:b9:b4:3e:97	ATT	38	Sent Read Request, Handle: 0x000f (Unknown)
537	2025-07-07 20:34:51.909449	c5:1f:b9:b4:3e:97	localhost ()	ATT	44	Rcvd Read Response, Handle: 0x000f (Unknown)
539	2025-07-07 20:34:51.910126	localhost ()	c5:1f:b9:b4:3e:97	ATT	38	Sent Read Request, Handle: 0x000d (Unknown)
543	2025-07-07 20:34:52.006697	c5:1f:b9:b4:3e:97	localhost ()	ATT	38	Rcvd Read Response, Handle: 0x000d (Unknown)
545	2025-07-07 20:34:52.007610	localhost ()	c5:1f:b9:b4:3e:97	ATT	38	Sent Read Request, Handle: 0x000b (Unknown)
549	2025-07-07 20:34:52.111706	c5:1f:b9:b4:3e:97	localhost ()	ATT	55	Rcvd Read Response, Handle: 0x000b (Unknown)
553	2025-07-07 20:34:52.113576	localhost ()	c5:1f:b9:b4:3e:97	ATT	42	Sent Read By Type Request, Database Hash, Handles: 0x0001..0xffff
557	2025-07-07 20:34:52.139347	c5:1f:b9:b4:3e:97	localhost ()	ATT	55	Rcvd Read By Type Response, Attribute List Length: 1
559	2025-07-07 20:34:52.140106	localhost ()	c5:1f:b9:b4:3e:97	ATT	40	Sent Write Request, Handle: 0x0013 (Unknown)
563	2025-07-07 20:34:52.166687	c5:1f:b9:b4:3e:97	localhost ()	ATT	40	Rcvd Error Response - Insufficient Authentication, Handle: 0x0013 (Unknown)

looking at it now again, pairing_result.status seems to be returned as AlreadyPaired which I guess makes sense, but it prevents (re)authentication

I also tried removing ENCRYPTION_AND_AUTHENTICATION from the loop

for level in (
                      DevicePairingProtectionLevel.ENCRYPTION_AND_AUTHENTICATION,
                    DevicePairingProtectionLevel.ENCRYPTION,
                ):

I'm not quite sure why it's there since it always seems to be used (the second iteration never happens) and there's no way to select one, but the behavior in this case is exactly the same whichever is used there.

I moved unpair() to the end of my client script, when closing the connection, so that it doesn't mess up the initial connection flow, but that still leaves me with the 1min delay in pairing

even if the device is "forgotten"

Can you explain what this means in technical terms?

The devices are not shown in the regular windows bluetooth settings, or at least I'm not able to find exact location where windows would show them, so I've used chrome://bluetooth-internals/#devices to "forget" them, which I expect should remove any information about that connection/device from the bluetooth controller and practically unpair them?

mgagic avatar Jul 07 '25 19:07 mgagic

I don't think that "forget" in the Chrome internals is quite the same as "unpair()` - at least on Windows. According to the source code, it isn't implemented.

void BluetoothDeviceWinrt::Forget(base::OnceClosure callback,
                                  ErrorCallback error_callback) {
  NOTIMPLEMENTED();
}

I always use the Windows Settings to remove Bluetooth devices when needed. This is essentially the same as unpair().

Image

If the pairing is removed on the peripheral, it is normal to have to manually remove it in Windows like this before you can use the device again.

But if the peripheral is just not remembering the pairing, then it sounds like a problem with the peripheral.

dlech avatar Jul 08 '25 15:07 dlech

Thanks for the hint. Windows menu doesn't show my devices (it shows peripherals, headsets, .... but not these) so I thought browser access could do the same

If the pairing is removed on the peripheral, it is normal to have to manually remove it in Windows like this before you can use the device again.

I would expect that if bonding were enabled, but at least on my devices it's not, and I don't see an option to manage that on the client side.

If it seems like OS is the culprit, I guess this issue can be closed. Thanks for your help!

mgagic avatar Jul 10 '25 06:07 mgagic

I used this as workaround:

try:
    # On windows client.pair() takes way too long, although connection is working.
    # The wait_for is used as workaround
    await asyncio.wait_for(client.pair(), timeout=4.0)
except asyncio.TimeoutError: 
    pass

chrisgwerder avatar Jul 14 '25 06:07 chrisgwerder

But this workaround seems to not work anymore with bleak 1.0.1 🥲, only with 0.22.3

chrisgwerder avatar Jul 14 '25 08:07 chrisgwerder