Pairing on windows successful, but is blocked until it times out
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
)
)
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.
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.
`` for windows because it seems
device_information.pairing.is_pairedis alwaysTrue
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?
`` for windows because it seems
device_information.pairing.is_pairedis alwaysTrueWhat 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?
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().
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.
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!
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
But this workaround seems to not work anymore with bleak 1.0.1 🥲, only with 0.22.3