Skip to content

Commit

Permalink
backends: filter discovered devices
Browse files Browse the repository at this point in the history
This is a follow-up to a818521 ("backends/scanner: always filter by
service_uuids") to also filter discovered devices by the service UUIDs.

This was overlooked in that change and on Windows actually caused a
regression.

Fixes: #1576
  • Loading branch information
dlech committed May 21, 2024
1 parent b8149a5 commit 443db9a
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 22 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ Changed
* Retrieve the BLE address required by ``BleakClientWinRT`` from scan response if advertising is None (WinRT).
* Changed type hint for ``adv`` attribute of ``bleak.backends.winrt.scanner._RawAdvData``.

Fixed
-----
* Fixed ``discovered_devices_and_advertisement_data`` returning devices that should
be filtered out by service UUIDs. Fixes #1576.

`0.22.1`_ (2024-05-07)
======================

Expand Down
5 changes: 4 additions & 1 deletion bleak/backends/bluezdbus/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,17 @@ def _handle_advertising_data(self, path: str, props: Device1) -> None:
path: The D-Bus object path of the device.
props: The D-Bus object properties of the device.
"""
_service_uuids = props.get("UUIDs", [])

if not self.is_allowed_uuid(_service_uuids):
return

# Get all the information wanted to pack in the advertisement data
_local_name = props.get("Name")
_manufacturer_data = {
k: bytes(v) for k, v in props.get("ManufacturerData", {}).items()
}
_service_data = {k: bytes(v) for k, v in props.get("ServiceData", {}).items()}
_service_uuids = props.get("UUIDs", [])

# Get tx power data
tx_power = props.get("TxPower")
Expand Down
11 changes: 7 additions & 4 deletions bleak/backends/corebluetooth/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ async def start(self) -> None:

def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None:

service_uuids = [
cb_uuid_to_str(u) for u in a.get("kCBAdvDataServiceUUIDs", [])
]

if not self.is_allowed_uuid(service_uuids):
return

# Process service data
service_data_dict_raw = a.get("kCBAdvDataServiceData", {})
service_data = {
Expand All @@ -108,10 +115,6 @@ def callback(p: CBPeripheral, a: Dict[str, Any], r: int) -> None:
manufacturer_value = bytes(manufacturer_binary_data[2:])
manufacturer_data[manufacturer_id] = manufacturer_value

service_uuids = [
cb_uuid_to_str(u) for u in a.get("kCBAdvDataServiceUUIDs", [])
]

# set tx_power data if available
tx_power = a.get("kCBAdvDataTxPowerLevel")

Expand Down
3 changes: 3 additions & 0 deletions bleak/backends/p4android/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ def _handle_scan_result(self, result) -> None:
if service_uuids is not None:
service_uuids = [service_uuid.toString() for service_uuid in service_uuids]

if not self.is_allowed_uuid(service_uuids):
return

manufacturer_data = record.getManufacturerSpecificData()
manufacturer_data = {
manufacturer_data.keyAt(index): bytes(manufacturer_data.valueAt(index))
Expand Down
51 changes: 35 additions & 16 deletions bleak/backends/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,32 +197,51 @@ def remove() -> None:

return remove

def call_detection_callbacks(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
def is_allowed_uuid(self, service_uuids: Optional[List[str]]) -> bool:
"""
Calls all registered detection callbacks.
Check if the advertisement data contains any of the service UUIDs
matching the filter. If no filter is set, this will always return
``True``.
Backend implementations should call this method when an advertisement
event is received from the OS.
"""
Args:
service_uuids: The service UUIDs from the advertisement data.
Returns:
``True`` if the advertisement data should be allowed or ``False``
if the advertisement data should be filtered out.
"""
# Backends will make best effort to filter out advertisements that
# don't match the service UUIDs, but if other apps are scanning at the
# same time or something like that, we may still receive advertisements
# that don't match. So we need to do more filtering here to get the
# expected behavior.

if self._service_uuids:
if not advertisement_data.service_uuids:
return
if not self._service_uuids:
# if there is no filter, everything is allowed
return True

if not service_uuids:
# if there is a filter the advertisement data doesn't contain any
# service UUIDs, filter it out
return False

for uuid in advertisement_data.service_uuids:
if uuid in self._service_uuids:
break
else:
# if there were no matching service uuids, the don't call the callback
return
for uuid in service_uuids:
if uuid in self._service_uuids:
# match was found, keep this advertisement
return True

# there were no matching service uuids, filter this one out
return False

def call_detection_callbacks(
self, device: BLEDevice, advertisement_data: AdvertisementData
) -> None:
"""
Calls all registered detection callbacks.
Backend implementations should call this method when an advertisement
event is received from the OS.
"""

for callback in self._ad_callbacks.values():
callback(device, advertisement_data)
Expand Down
3 changes: 3 additions & 0 deletions bleak/backends/winrt/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ def _received_handler(
data = bytes(section.data)
service_data[str(UUID(bytes=bytes(data[15::-1])))] = data[16:]

if not self.is_allowed_uuid(uuids):
return

# Use the BLEDevice to populate all the fields for the advertisement data to return
advertisement_data = AdvertisementData(
local_name=local_name,
Expand Down
11 changes: 10 additions & 1 deletion examples/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ async def main(args: argparse.Namespace):
print("scanning for 5 seconds, please wait...")

devices = await BleakScanner.discover(
return_adv=True, cb=dict(use_bdaddr=args.macos_use_bdaddr)
return_adv=True,
service_uuids=args.services,
cb=dict(use_bdaddr=args.macos_use_bdaddr),
)

for d, a in devices.values():
Expand All @@ -31,6 +33,13 @@ async def main(args: argparse.Namespace):
if __name__ == "__main__":
parser = argparse.ArgumentParser()

parser.add_argument(
"--services",
metavar="<uuid>",
nargs="*",
help="UUIDs of one or more services to filter for",
)

parser.add_argument(
"--macos-use-bdaddr",
action="store_true",
Expand Down

0 comments on commit 443db9a

Please sign in to comment.