Skip to content

Commit 71e298c

Browse files
committed
BleakScanner: Add ScanMode enum, support active scan mode fall-back
Use ScanMode enum instead of a string literal for the `scanning_mode` parameter of the `BleakScanner`. Support fall-back to active scanning mode on platforms where passive scanning mode is not possible if `TRY_PASSIVE` mode is used. Add noqa comment to avoid "N806 variable ... in function should be lowercase".
1 parent fc3cbbd commit 71e298c

File tree

8 files changed

+118
-62
lines changed

8 files changed

+118
-62
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Added
1515
* Added optional hack to use Bluetooth address instead of UUID on macOS.
1616
* Added ``BleakScanner.find_device_by_name()`` class method.
1717
* Added ``BleakNoPassiveScanError`` exception.
18+
* Added ``ScanMode`` enum for ``BleakScanner`` scanning type.
1819

1920
Changed
2021
-------
@@ -24,6 +25,7 @@ Changed
2425
* ``BaseBleakClient.services`` is now ``None`` instead of empty service collection
2526
until services are discovered.
2627
* Include thread name in ``BLEAK_LOGGING`` output. Merged #1144.
28+
* ``scanning_mode`` parameter of ``BleakScanner`` is now ``ScanMode`` enum instead of string literal.
2729

2830
Fixed
2931
-----

bleak/__init__.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import os
1515
import sys
1616
import uuid
17+
import warnings
1718
from typing import (
1819
TYPE_CHECKING,
1920
Awaitable,
@@ -45,6 +46,7 @@
4546
AdvertisementData,
4647
AdvertisementDataCallback,
4748
AdvertisementDataFilter,
49+
ScanMode,
4850
BaseBleakScanner,
4951
get_platform_scanner_backend_type,
5052
)
@@ -88,9 +90,10 @@ class BleakScanner:
8890
containing this advertising data will be received. Required on
8991
macOS >= 12.0, < 12.3 (unless you create an app with ``py2app``).
9092
scanning_mode:
91-
Set to ``"passive"`` to avoid the ``"active"`` scanning mode.
93+
Set to ``ScanMode.PASSIVE`` or ``ScanMode.TRY_PASSIVE`` to avoid
94+
the default active scanning mode.
9295
Passive scanning is not supported on macOS! Will raise
93-
:class:`BleakError` if set to ``"passive"`` on macOS.
96+
:class:`BleakError` if set to ``ScanMode.PASSIVE`` on macOS.
9497
bluez:
9598
Dictionary of arguments specific to the BlueZ backend.
9699
cb:
@@ -114,14 +117,22 @@ def __init__(
114117
self,
115118
detection_callback: Optional[AdvertisementDataCallback] = None,
116119
service_uuids: Optional[List[str]] = None,
117-
scanning_mode: Literal["active", "passive"] = "active",
120+
scanning_mode: ScanMode = ScanMode.ACTIVE,
118121
*,
119122
bluez: BlueZScannerArgs = {},
120123
cb: CBScannerArgs = {},
121124
backend: Optional[Type[BaseBleakScanner]] = None,
122125
**kwargs,
123126
):
124-
PlatformBleakScanner = (
127+
if isinstance(scanning_mode, str):
128+
scanning_mode = ScanMode(scanning_mode.lower())
129+
warnings.warn(
130+
f"The scanning_mode is now {scanning_mode} instead of string literal",
131+
DeprecationWarning,
132+
stacklevel=2,
133+
)
134+
135+
PlatformBleakScanner = ( # noqa: N806
125136
get_platform_scanner_backend_type() if backend is None else backend
126137
)
127138

@@ -418,7 +429,7 @@ def __init__(
418429
backend: Optional[Type[BaseBleakClient]] = None,
419430
**kwargs,
420431
):
421-
PlatformBleakClient = (
432+
PlatformBleakClient = ( # noqa: N806
422433
get_platform_client_backend_type() if backend is None else backend
423434
)
424435

bleak/backends/bluezdbus/scanner.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@
66
from dbus_fast import Variant
77

88
if sys.version_info[:2] < (3, 8):
9-
from typing_extensions import Literal, TypedDict
9+
from typing_extensions import TypedDict
1010
else:
11-
from typing import Literal, TypedDict
12-
13-
from ...exc import BleakError
14-
from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner
11+
from typing import TypedDict
12+
13+
from ...exc import BleakError, BleakNoPassiveScanError
14+
from ..scanner import (
15+
AdvertisementData,
16+
AdvertisementDataCallback,
17+
BaseBleakScanner,
18+
ScanMode,
19+
)
1520
from .advertisement_monitor import OrPatternLike
1621
from .defs import Device1
1722
from .manager import get_global_bluez_manager
@@ -121,7 +126,7 @@ def __init__(
121126
self,
122127
detection_callback: Optional[AdvertisementDataCallback],
123128
service_uuids: Optional[List[str]],
124-
scanning_mode: Literal["active", "passive"],
129+
scanning_mode: ScanMode,
125130
*,
126131
bluez: BlueZScannerArgs,
127132
**kwargs,
@@ -162,12 +167,12 @@ def __init__(
162167

163168
self._or_patterns = bluez.get("or_patterns")
164169

165-
if self._scanning_mode == "passive" and service_uuids:
170+
if self._scanning_mode.passive and service_uuids:
166171
logger.warning(
167172
"service uuid filtering is not implemented for passive scanning, use bluez or_patterns as a workaround"
168173
)
169174

170-
if self._scanning_mode == "passive" and not self._or_patterns:
175+
if self._scanning_mode.passive and not self._or_patterns:
171176
raise BleakError("passive scanning mode requires bluez or_patterns")
172177

173178
async def start(self):
@@ -180,14 +185,24 @@ async def start(self):
180185

181186
self.seen_devices = {}
182187

183-
if self._scanning_mode == "passive":
184-
self._stop = await manager.passive_scan(
185-
adapter_path,
186-
self._or_patterns,
187-
self._handle_advertising_data,
188-
self._handle_device_removed,
189-
)
190-
else:
188+
if self._scanning_mode.passive:
189+
try:
190+
self._stop = await manager.passive_scan(
191+
adapter_path,
192+
self._or_patterns,
193+
self._handle_advertising_data,
194+
self._handle_device_removed,
195+
)
196+
except BleakNoPassiveScanError as e:
197+
if self._scanning_mode == ScanMode.TRY_PASSIVE:
198+
logger.warning(
199+
f"Passive scan not possible, using active scan ({e})"
200+
)
201+
self._scanning_mode = ScanMode.ACTIVE
202+
else:
203+
raise
204+
205+
if self._scanning_mode == ScanMode.ACTIVE:
191206
self._stop = await manager.active_scan(
192207
adapter_path,
193208
self._filters,

bleak/backends/corebluetooth/scanner.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
from typing import Any, Dict, List, Optional
44

55
if sys.version_info[:2] < (3, 8):
6-
from typing_extensions import Literal, TypedDict
6+
from typing_extensions import TypedDict
77
else:
8-
from typing import Literal, TypedDict
8+
from typing import TypedDict
99

1010
import objc
1111
from CoreBluetooth import CBPeripheral
1212
from Foundation import NSBundle
1313

1414
from ...exc import BleakNoPassiveScanError
15-
from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner
15+
from ..scanner import (
16+
AdvertisementData,
17+
AdvertisementDataCallback,
18+
BaseBleakScanner,
19+
ScanMode,
20+
)
1621
from .CentralManagerDelegate import CentralManagerDelegate
1722
from .utils import cb_uuid_to_str
1823

@@ -65,7 +70,7 @@ def __init__(
6570
self,
6671
detection_callback: Optional[AdvertisementDataCallback],
6772
service_uuids: Optional[List[str]],
68-
scanning_mode: Literal["active", "passive"],
73+
scanning_mode: ScanMode,
6974
*,
7075
cb: CBScannerArgs,
7176
**kwargs
@@ -76,8 +81,10 @@ def __init__(
7681

7782
self._use_bdaddr = cb.get("use_bdaddr", False)
7883

79-
if scanning_mode == "passive":
84+
if scanning_mode == ScanMode.PASSIVE:
8085
raise BleakNoPassiveScanError("macOS does not support passive scanning")
86+
elif scanning_mode == ScanMode.TRY_PASSIVE:
87+
logger.warning("macOS does not support passive scanning, using active scan")
8188

8289
self._manager = CentralManagerDelegate.alloc().init()
8390
self._timeout: float = kwargs.get("timeout", 5.0)

bleak/backends/p4android/scanner.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@
1111
else:
1212
from asyncio import timeout as async_timeout
1313

14-
if sys.version_info[:2] < (3, 8):
15-
from typing_extensions import Literal
16-
else:
17-
from typing import Literal
18-
1914
from android.broadcast import BroadcastReceiver
2015
from android.permissions import Permission, request_permissions
2116
from jnius import cast, java_method
2217

2318
from ...exc import BleakError
24-
from ..scanner import AdvertisementData, AdvertisementDataCallback, BaseBleakScanner
19+
from ..scanner import (
20+
AdvertisementData,
21+
AdvertisementDataCallback,
22+
BaseBleakScanner,
23+
ScanMode,
24+
)
2525
from . import defs, utils
2626

2727
logger = logging.getLogger(__name__)
@@ -49,12 +49,12 @@ def __init__(
4949
self,
5050
detection_callback: Optional[AdvertisementDataCallback],
5151
service_uuids: Optional[List[str]],
52-
scanning_mode: Literal["active", "passive"],
52+
scanning_mode: ScanMode,
5353
**kwargs,
5454
):
5555
super(BleakScannerP4Android, self).__init__(detection_callback, service_uuids)
5656

57-
if scanning_mode == "passive":
57+
if scanning_mode.passive:
5858
self.__scan_mode = defs.ScanSettings.SCAN_MODE_OPPORTUNISTIC
5959
else:
6060
self.__scan_mode = defs.ScanSettings.SCAN_MODE_LOW_LATENCY

bleak/backends/scanner.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import abc
22
import asyncio
3+
import enum
34
import inspect
45
import os
56
import platform
@@ -85,6 +86,43 @@ def __repr__(self) -> str:
8586
return f"AdvertisementData({', '.join(kwargs)})"
8687

8788

89+
class ScanMode(enum.Enum):
90+
"""Bluetooth scanning mode"""
91+
92+
ACTIVE = "active"
93+
"""
94+
Perform active scan (default for most platforms)
95+
96+
This type of scan is typically used when the potential Central device would like more info
97+
than can be provided in an ADV_IND packet, before making a decision to connect to it. After
98+
advertisement is received, the Scanner will request more information by issuing a SCAN_REQ
99+
packet in the advertising interval. The Advertiser responds with more information (like
100+
friendly name and supported profiles) in a SCAN_RSP packet.
101+
"""
102+
103+
PASSIVE = "passive"
104+
"""
105+
Perform passive scan (not supported on all platforms)
106+
107+
In passive scanning mode, the Scanner just listens for advertising packets. When such packet
108+
is detected, the module reports the discovered device. The Advertiser is never aware that
109+
packets were received by the Scanner.
110+
"""
111+
112+
TRY_PASSIVE = "try_passive"
113+
"""
114+
Try to use passive scanning mode, if not possible fall-back to active scanning mode
115+
116+
As passive scanning mode is not possible on all platforms, this option can be used to prefer
117+
passive scanning, or automatically switch to active scanning if passive is not possible.
118+
"""
119+
120+
@property
121+
def passive(self) -> bool:
122+
"""Helper method to check whether passive mode should [try to] be used [first]"""
123+
return self in (self.PASSIVE, self.TRY_PASSIVE)
124+
125+
88126
AdvertisementDataCallback = Callable[
89127
[BLEDevice, AdvertisementData],
90128
Optional[Awaitable[None]],

bleak/backends/winrt/scanner.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import asyncio
22
import logging
3-
import sys
43
from typing import Dict, List, NamedTuple, Optional
54
from uuid import UUID
65

@@ -11,12 +10,12 @@
1110
BluetoothLEAdvertisementType,
1211
)
1312

14-
if sys.version_info[:2] < (3, 8):
15-
from typing_extensions import Literal
16-
else:
17-
from typing import Literal
18-
19-
from ..scanner import AdvertisementDataCallback, BaseBleakScanner, AdvertisementData
13+
from ..scanner import (
14+
AdvertisementDataCallback,
15+
BaseBleakScanner,
16+
AdvertisementData,
17+
ScanMode,
18+
)
2019
from ...assigned_numbers import AdvertisementDataType
2120

2221

@@ -73,7 +72,7 @@ def __init__(
7372
self,
7473
detection_callback: Optional[AdvertisementDataCallback],
7574
service_uuids: Optional[List[str]],
76-
scanning_mode: Literal["active", "passive"],
75+
scanning_mode: ScanMode,
7776
**kwargs,
7877
):
7978
super(BleakScannerWinRT, self).__init__(detection_callback, service_uuids)
@@ -82,8 +81,7 @@ def __init__(
8281
self._advertisement_pairs: Dict[int, _RawAdvData] = {}
8382
self._stopped_event = None
8483

85-
# case insensitivity is for backwards compatibility on Windows only
86-
if scanning_mode.lower() == "passive":
84+
if scanning_mode.passive:
8785
self._scanning_mode = BluetoothLEScanningMode.PASSIVE
8886
else:
8987
self._scanning_mode = BluetoothLEScanningMode.ACTIVE

examples/passive_scan.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@
99
"""
1010
import argparse
1111
import asyncio
12-
import contextlib
1312
import logging
1413
from typing import Optional, List, Dict, Any
1514

1615
import bleak
17-
from bleak import BLEDevice, AdvertisementData
16+
from bleak import BleakScanner, BLEDevice, AdvertisementData, ScanMode
1817

1918
logger = logging.getLogger(__name__)
2019

@@ -70,30 +69,16 @@ def get_core_bluetooth_scanning_params() -> Dict[str, Any]:
7069
}.get(bleak.get_platform_scanner_backend_type().__name__, lambda: {})()
7170

7271

73-
@contextlib.asynccontextmanager
74-
async def scanner(**kwargs):
75-
try:
76-
async with bleak.BleakScanner(**kwargs) as bleak_scanner:
77-
yield bleak_scanner
78-
79-
except bleak.exc.BleakNoPassiveScanError as e:
80-
logger.error(f"Passive scanning not possible, using active scanning ({e})")
81-
82-
del kwargs["scanning_mode"]
83-
async with bleak.BleakScanner(**kwargs) as bleak_scanner:
84-
yield bleak_scanner
85-
86-
8772
async def main(args: argparse.Namespace):
8873
def scan_callback(device: BLEDevice, adv_data: AdvertisementData):
8974
logger.info("%s: %r", device.address, adv_data)
9075

91-
async with scanner(
76+
async with BleakScanner(
9277
detection_callback=scan_callback,
9378
**_get_os_specific_scanning_params(
9479
uuids=args.services, macos_use_bdaddr=args.macos_use_bdaddr
9580
),
96-
scanning_mode="passive",
81+
scanning_mode=ScanMode.TRY_PASSIVE,
9782
):
9883
await asyncio.sleep(60)
9984

0 commit comments

Comments
 (0)