From dbb4e6141dbae0069b7fa04b499c86ebd01c8495 Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 11:11:32 -0600 Subject: [PATCH 1/9] Windows bonded BLE devices, connect via MAC --- src/smpclient/transport/ble.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index 0cc2c3f..cd79d99 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -98,6 +98,23 @@ async def connect(self, address: str, timeout_s: float) -> None: winrt=self._winrt, disconnected_callback=self._set_disconnected_event, ) + elif sys.platform == "win32" and MAC_ADDRESS_PATTERN.match(address): + # Bonded devices on Windows may not be actively advertising so + # find_device_by_address() returns None. BleakClientWinRT skips its + # internal scan and calls from_bluetooth_address_async() directly when + # _device_info (mac int) is pre-set before connect(). + logger.warning( + f"Device '{address}' not found via BLE scan; " + "trying Windows bonded-device connection" + ) + self._client = BleakClient( + address, + services=(str(SMP_SERVICE_UUID),), + winrt=self._winrt, + disconnected_callback=self._set_disconnected_event, + ) + if self._winrt_backend(self._client._backend): + self._client._backend._device_info = int(address.replace(":", ""), 16) else: raise SMPBLETransportDeviceNotFound(f"Device '{address}' not found") From 2602358f58c6a95e02ef024f32ae7330f7a7d5d7 Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 11:27:20 -0600 Subject: [PATCH 2/9] upkeep(test) --- tests/test_smp_ble_transport.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_smp_ble_transport.py b/tests/test_smp_ble_transport.py index c451876..8efcc1c 100644 --- a/tests/test_smp_ble_transport.py +++ b/tests/test_smp_ble_transport.py @@ -1,6 +1,7 @@ """Tests for `SMPBLETransport`.""" import asyncio +import sys from typing import cast from unittest.mock import AsyncMock, MagicMock, patch from uuid import UUID @@ -99,11 +100,21 @@ async def test_connect( ) mock_find_device_by_address.reset_mock() - # assert that it raises an exception if the device is not found + # assert that it raises an exception if the device is not found (non-MAC or non-Windows) mock_find_device_by_address.return_value = None + mock_find_device_by_name.return_value = None with pytest.raises(SMPBLETransportDeviceNotFound): - await SMPBLETransport().connect("00:00:00:00:00:00", 1.0) - mock_find_device_by_address.reset_mock() + await SMPBLETransport().connect("device name", 1.0) + mock_find_device_by_name.reset_mock() + + # assert that on Windows with MAC, the bonded-device fallback is attempted + if sys.platform == "win32": + mock_find_device_by_address.return_value = None + t = SMPBLETransport() + await t.connect("00:00:00:00:00:00", 1.0) + # Verify that BleakClient was called with the MAC address (fallback attempt) + assert t._client is not None + mock_find_device_by_address.reset_mock() # assert that connect is awaited t = SMPBLETransport() From 402ba71572fef2aa52d143943e85a8c8b1b9dfb0 Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 11:43:01 -0600 Subject: [PATCH 3/9] upkeep(test2) --- tests/test_smp_ble_transport.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_smp_ble_transport.py b/tests/test_smp_ble_transport.py index 8efcc1c..526d019 100644 --- a/tests/test_smp_ble_transport.py +++ b/tests/test_smp_ble_transport.py @@ -107,6 +107,10 @@ async def test_connect( await SMPBLETransport().connect("device name", 1.0) mock_find_device_by_name.reset_mock() + # restore mock return values for next tests + mock_find_device_by_address.return_value = BLEDevice("address", "name", None) + mock_find_device_by_name.return_value = BLEDevice("address", "name", None) + # assert that on Windows with MAC, the bonded-device fallback is attempted if sys.platform == "win32": mock_find_device_by_address.return_value = None From 3892d0433155abaebf1ed9ffb0b638084406e96a Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 11:46:02 -0600 Subject: [PATCH 4/9] upkeep(test3) --- tests/test_smp_ble_transport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_smp_ble_transport.py b/tests/test_smp_ble_transport.py index 526d019..6e191a0 100644 --- a/tests/test_smp_ble_transport.py +++ b/tests/test_smp_ble_transport.py @@ -112,13 +112,13 @@ async def test_connect( mock_find_device_by_name.return_value = BLEDevice("address", "name", None) # assert that on Windows with MAC, the bonded-device fallback is attempted - if sys.platform == "win32": + with patch("sys.platform", "win32"): mock_find_device_by_address.return_value = None t = SMPBLETransport() await t.connect("00:00:00:00:00:00", 1.0) # Verify that BleakClient was called with the MAC address (fallback attempt) assert t._client is not None - mock_find_device_by_address.reset_mock() + mock_find_device_by_address.reset_mock() # assert that connect is awaited t = SMPBLETransport() From ee1d37775a0dc0ad9e4d3bb6c63d858b97bb5685 Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 12:08:20 -0600 Subject: [PATCH 5/9] Fixed connection when using the fallback --- src/smpclient/transport/ble.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index cd79d99..905670d 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -80,10 +80,14 @@ def __init__(self, winrt: WinRTClientArgs = {}) -> None: self._max_write_without_response_size = 20 """Initially set to BLE minimum; may be mutated by the `connect()` method.""" + self._using_windows_bonded_fallback = False + """Tracks if we're using Windows bonded device fallback (MAC address connection).""" + logger.debug(f"Initialized {self.__class__.__name__}") @override async def connect(self, address: str, timeout_s: float) -> None: + self._using_windows_bonded_fallback = False logger.debug(f"Scanning for {address=}") device: BLEDevice | None = ( await BleakScanner.find_device_by_address(address, timeout=timeout_s) @@ -115,6 +119,7 @@ async def connect(self, address: str, timeout_s: float) -> None: ) if self._winrt_backend(self._client._backend): self._client._backend._device_info = int(address.replace(":", ""), 16) + self._using_windows_bonded_fallback = True else: raise SMPBLETransportDeviceNotFound(f"Device '{address}' not found") @@ -123,9 +128,22 @@ async def connect(self, address: str, timeout_s: float) -> None: self._disconnected_event.clear() logger.debug(f"Connected to {device=}") - smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) - if smp_characteristic is None: - raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") + # Windows bonded devices need service discovery; wait for it to complete + if self._using_windows_bonded_fallback: + logger.debug("Waiting for service discovery on Windows bonded device") + max_retries = 10 + for attempt in range(max_retries): + smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) + if smp_characteristic is not None: + logger.debug(f"Service discovery completed on attempt {attempt + 1}") + break + await asyncio.sleep(0.1) + else: + raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") + else: + smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) + if smp_characteristic is None: + raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") logger.debug(f"Found SMP characteristic: {smp_characteristic=}") logger.info(f"{smp_characteristic.max_write_without_response_size=}") From 8b102f6ff52fbd03514b3cfe5d4d18d981696519 Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 12:44:51 -0600 Subject: [PATCH 6/9] Explicit gatt discovery for bonded windows devices --- src/smpclient/transport/ble.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index 905670d..90454b3 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -131,19 +131,11 @@ async def connect(self, address: str, timeout_s: float) -> None: # Windows bonded devices need service discovery; wait for it to complete if self._using_windows_bonded_fallback: logger.debug("Waiting for service discovery on Windows bonded device") - max_retries = 10 - for attempt in range(max_retries): - smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) - if smp_characteristic is not None: - logger.debug(f"Service discovery completed on attempt {attempt + 1}") - break - await asyncio.sleep(0.1) - else: - raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") - else: - smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) - if smp_characteristic is None: - raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") + await self._client.get_services() + + smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) + if smp_characteristic is None: + raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") logger.debug(f"Found SMP characteristic: {smp_characteristic=}") logger.info(f"{smp_characteristic.max_write_without_response_size=}") From 29bd85299ebed26720ca05937b025042b0457469 Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 12:52:53 -0600 Subject: [PATCH 7/9] fix gatt disco --- src/smpclient/transport/ble.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index 90454b3..7448c76 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -131,11 +131,20 @@ async def connect(self, address: str, timeout_s: float) -> None: # Windows bonded devices need service discovery; wait for it to complete if self._using_windows_bonded_fallback: logger.debug("Waiting for service discovery on Windows bonded device") - await self._client.get_services() - - smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) - if smp_characteristic is None: - raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") + max_retries = 10 + for attempt in range(max_retries): + if self._client.services is not None: # <-- Add this check + smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) + if smp_characteristic is not None: + logger.debug(f"Service discovery completed on attempt {attempt + 1}") + break + await asyncio.sleep(0.1) + else: + raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") + else: + smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) + if smp_characteristic is None: + raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") logger.debug(f"Found SMP characteristic: {smp_characteristic=}") logger.info(f"{smp_characteristic.max_write_without_response_size=}") From d7d072a5fa408ea2bfe4b7e7b84b30eb7a1a97b5 Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 12:56:34 -0600 Subject: [PATCH 8/9] fix gatt disco2 --- src/smpclient/transport/ble.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index 7448c76..43fbde6 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -131,15 +131,22 @@ async def connect(self, address: str, timeout_s: float) -> None: # Windows bonded devices need service discovery; wait for it to complete if self._using_windows_bonded_fallback: logger.debug("Waiting for service discovery on Windows bonded device") - max_retries = 10 + max_retries = 50 # Increase to 5 seconds for attempt in range(max_retries): - if self._client.services is not None: # <-- Add this check + logger.debug(f"Attempt {attempt + 1}: services={self._client.services}") + if self._client.services is not None: + logger.debug(f"Services found: {list(self._client.services.services.keys())}") smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) if smp_characteristic is not None: logger.debug(f"Service discovery completed on attempt {attempt + 1}") break + else: + logger.debug(f"SMP characteristic not found yet, available characteristics: {[c.uuid for s in self._client.services for c in s.characteristics]}") await asyncio.sleep(0.1) else: + logger.error(f"Service discovery failed. Final services state: {self._client.services}") + if self._client.services is not None: + logger.error(f"Available services: {list(self._client.services.services.keys())}") raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") else: smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) From d512389dad314b754ac72d3453fc7e0338031773 Mon Sep 17 00:00:00 2001 From: intercreate-gab Date: Sat, 2 May 2026 13:50:35 -0600 Subject: [PATCH 9/9] fix(DFU works) --- src/smpclient/transport/ble.py | 42 +++++++++++----------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/src/smpclient/transport/ble.py b/src/smpclient/transport/ble.py index 43fbde6..83acda5 100644 --- a/src/smpclient/transport/ble.py +++ b/src/smpclient/transport/ble.py @@ -103,22 +103,14 @@ async def connect(self, address: str, timeout_s: float) -> None: disconnected_callback=self._set_disconnected_event, ) elif sys.platform == "win32" and MAC_ADDRESS_PATTERN.match(address): - # Bonded devices on Windows may not be actively advertising so - # find_device_by_address() returns None. BleakClientWinRT skips its - # internal scan and calls from_bluetooth_address_async() directly when - # _device_info (mac int) is pre-set before connect(). - logger.warning( - f"Device '{address}' not found via BLE scan; " - "trying Windows bonded-device connection" - ) + logger.warning(f"WinBLE : Device '{address}' not found via BLE scan; trying MAC address connection") self._client = BleakClient( address, services=(str(SMP_SERVICE_UUID),), winrt=self._winrt, disconnected_callback=self._set_disconnected_event, ) - if self._winrt_backend(self._client._backend): - self._client._backend._device_info = int(address.replace(":", ""), 16) + # IMPORTANT: Do not set self._client._backend._device_info here, it will cancel any service discovery self._using_windows_bonded_fallback = True else: raise SMPBLETransportDeviceNotFound(f"Device '{address}' not found") @@ -130,28 +122,20 @@ async def connect(self, address: str, timeout_s: float) -> None: # Windows bonded devices need service discovery; wait for it to complete if self._using_windows_bonded_fallback: - logger.debug("Waiting for service discovery on Windows bonded device") - max_retries = 50 # Increase to 5 seconds + logger.debug("WinBLE : Waiting for services to populate") + max_retries = 50 for attempt in range(max_retries): - logger.debug(f"Attempt {attempt + 1}: services={self._client.services}") - if self._client.services is not None: - logger.debug(f"Services found: {list(self._client.services.services.keys())}") - smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) - if smp_characteristic is not None: - logger.debug(f"Service discovery completed on attempt {attempt + 1}") - break - else: - logger.debug(f"SMP characteristic not found yet, available characteristics: {[c.uuid for s in self._client.services for c in s.characteristics]}") + if self._client.services.services: + logger.debug(f"WinBLE : Services populated on attempt {attempt + 1}: {list(self._client.services.services.keys())}") + break await asyncio.sleep(0.1) else: - logger.error(f"Service discovery failed. Final services state: {self._client.services}") - if self._client.services is not None: - logger.error(f"Available services: {list(self._client.services.services.keys())}") - raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") - else: - smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) - if smp_characteristic is None: - raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") + logger.error(f"WinBLE : Service discovery failed. Services still empty after {max_retries} attempts") + raise SMPBLETransportNotSMPServer("WinBLE : Service discovery failed - no services found.") + + smp_characteristic = self._client.services.get_characteristic(SMP_CHARACTERISTIC_UUID) + if smp_characteristic is None: + raise SMPBLETransportNotSMPServer("Missing the SMP characteristic UUID.") logger.debug(f"Found SMP characteristic: {smp_characteristic=}") logger.info(f"{smp_characteristic.max_write_without_response_size=}")