idasit/bleak/backends/corebluetooth/scanner.py
bparodi@lezzo.org 41c244e903 first commit
2024-12-14 14:55:37 +01:00

185 lines
6.4 KiB
Python

import logging
from typing import Any, Dict, List, Literal, Optional, TypedDict
import objc
from CoreBluetooth import CBPeripheral
from Foundation import NSBundle
from ...exc import BleakError
from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner
from .CentralManagerDelegate import CentralManagerDelegate
from .utils import cb_uuid_to_str
logger = logging.getLogger(__name__)
class CBScannerArgs(TypedDict, total=False):
"""
Platform-specific :class:`BleakScanner` args for the CoreBluetooth backend.
"""
use_bdaddr: bool
"""
If true, use Bluetooth address instead of UUID.
.. warning:: This uses an undocumented IOBluetooth API to get the Bluetooth
address and may break in the future macOS releases. `It is known to not
work on macOS 10.15 <https://github.com/hbldh/bleak/issues/1286>`_.
"""
class BleakScannerCoreBluetooth(BaseBleakScanner):
"""The native macOS Bleak BLE Scanner.
Documentation:
https://developer.apple.com/documentation/corebluetooth/cbcentralmanager
CoreBluetooth doesn't explicitly use Bluetooth addresses to identify peripheral
devices because private devices may obscure their Bluetooth addresses. To cope
with this, CoreBluetooth utilizes UUIDs for each peripheral. Bleak uses
this for the BLEDevice address on macOS.
Args:
detection_callback:
Optional function that will be called each time a device is
discovered or advertising data has changed.
service_uuids:
Optional list of service UUIDs to filter on. Only advertisements
containing this advertising data will be received. Required on
macOS >= 12.0, < 12.3 (unless you create an app with ``py2app``).
scanning_mode:
Set to ``"passive"`` to avoid the ``"active"`` scanning mode. Not
supported on macOS! Will raise :class:`BleakError` if set to
``"passive"``
**timeout (float):
The scanning timeout to be used, in case of missing
``stopScan_`` method.
"""
def __init__(
self,
detection_callback: Optional[AdvertisementDataCallback],
service_uuids: Optional[List[str]],
scanning_mode: Literal["active", "passive"],
*,
cb: CBScannerArgs,
**kwargs
):
super(BleakScannerCoreBluetooth, self).__init__(
detection_callback, service_uuids
)
self._use_bdaddr = cb.get("use_bdaddr", False)
if scanning_mode == "passive":
raise BleakError("macOS does not support passive scanning")
self._manager = CentralManagerDelegate.alloc().init()
self._timeout: float = kwargs.get("timeout", 5.0)
if (
objc.macos_available(12, 0)
and not objc.macos_available(12, 3)
and not self._service_uuids
):
# See https://github.com/hbldh/bleak/issues/720
if NSBundle.mainBundle().bundleIdentifier() == "org.python.python":
logger.error(
"macOS 12.0, 12.1 and 12.2 require non-empty service_uuids kwarg, otherwise no advertisement data will be received"
)
async def start(self) -> None:
self.seen_devices = {}
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 = {
cb_uuid_to_str(k): bytes(v) for k, v in service_data_dict_raw.items()
}
# Process manufacturer data into a more friendly format
manufacturer_binary_data = a.get("kCBAdvDataManufacturerData")
manufacturer_data = {}
if manufacturer_binary_data:
manufacturer_id = int.from_bytes(
manufacturer_binary_data[0:2], byteorder="little"
)
manufacturer_value = bytes(manufacturer_binary_data[2:])
manufacturer_data[manufacturer_id] = manufacturer_value
# set tx_power data if available
tx_power = a.get("kCBAdvDataTxPowerLevel")
advertisement_data = AdvertisementData(
local_name=a.get("kCBAdvDataLocalName"),
manufacturer_data=manufacturer_data,
service_data=service_data,
service_uuids=service_uuids,
tx_power=tx_power,
rssi=r,
platform_data=(p, a, r),
)
if self._use_bdaddr:
# HACK: retrieveAddressForPeripheral_ is undocumented but seems to do the trick
address_bytes: bytes = (
self._manager.central_manager.retrieveAddressForPeripheral_(p)
)
if address_bytes is None:
logger.debug(
"Could not get Bluetooth address for %s. Ignoring this device.",
p.identifier().UUIDString(),
)
address = address_bytes.hex(":").upper()
else:
address = p.identifier().UUIDString()
device = self.create_or_update_device(
address,
p.name(),
(p, self._manager.central_manager.delegate()),
advertisement_data,
)
self.call_detection_callbacks(device, advertisement_data)
self._manager.callbacks[id(self)] = callback
await self._manager.start_scan(self._service_uuids)
async def stop(self) -> None:
await self._manager.stop_scan()
self._manager.callbacks.pop(id(self), None)
def set_scanning_filter(self, **kwargs) -> None:
"""Set scanning filter for the scanner.
.. note::
This is not implemented for macOS yet.
Raises:
``NotImplementedError``
"""
raise NotImplementedError(
"Need to evaluate which macOS versions to support first..."
)
# macOS specific methods
@property
def is_scanning(self):
# TODO: Evaluate if newer macOS than 10.11 has isScanning.
try:
return self._manager.isScanning_
except Exception:
return None