bumble 0.0.194__py3-none-any.whl → 0.0.198__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +692 -0
- bumble/apps/bench.py +77 -23
- bumble/apps/console.py +5 -20
- bumble/apps/controller_info.py +3 -3
- bumble/apps/device_info.py +230 -0
- bumble/apps/gatt_dump.py +4 -0
- bumble/apps/lea_unicast/app.py +16 -17
- bumble/at.py +12 -6
- bumble/avc.py +8 -5
- bumble/avctp.py +3 -2
- bumble/avdtp.py +5 -1
- bumble/avrcp.py +2 -1
- bumble/codecs.py +17 -13
- bumble/colors.py +6 -2
- bumble/core.py +726 -122
- bumble/device.py +817 -117
- bumble/drivers/rtk.py +13 -8
- bumble/gatt.py +6 -1
- bumble/gatt_client.py +10 -4
- bumble/hci.py +283 -20
- bumble/hid.py +24 -28
- bumble/host.py +29 -0
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/pandora/host.py +3 -2
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/bap.py +85 -862
- bumble/profiles/bass.py +440 -0
- bumble/profiles/csip.py +4 -4
- bumble/profiles/gap.py +110 -0
- bumble/profiles/heart_rate_service.py +4 -3
- bumble/profiles/le_audio.py +83 -0
- bumble/profiles/mcp.py +448 -0
- bumble/profiles/pacs.py +210 -0
- bumble/profiles/pbp.py +46 -0
- bumble/profiles/tmap.py +89 -0
- bumble/rfcomm.py +14 -3
- bumble/sdp.py +13 -11
- bumble/smp.py +20 -8
- bumble/snoop.py +5 -4
- bumble/transport/__init__.py +8 -2
- bumble/transport/android_emulator.py +9 -3
- bumble/transport/android_netsim.py +9 -7
- bumble/transport/common.py +46 -18
- bumble/transport/pyusb.py +2 -2
- bumble/transport/unix.py +56 -0
- bumble/transport/usb.py +57 -46
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/RECORD +54 -43
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/device.py
CHANGED
|
@@ -16,22 +16,22 @@
|
|
|
16
16
|
# Imports
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
|
-
from enum import IntEnum
|
|
20
|
-
import copy
|
|
21
|
-
import functools
|
|
22
|
-
import json
|
|
23
19
|
import asyncio
|
|
24
|
-
import
|
|
25
|
-
import secrets
|
|
26
|
-
import sys
|
|
20
|
+
from collections.abc import Iterable
|
|
27
21
|
from contextlib import (
|
|
28
22
|
asynccontextmanager,
|
|
29
23
|
AsyncExitStack,
|
|
30
24
|
closing,
|
|
31
|
-
AbstractAsyncContextManager,
|
|
32
25
|
)
|
|
26
|
+
import copy
|
|
33
27
|
from dataclasses import dataclass, field
|
|
34
|
-
from
|
|
28
|
+
from enum import Enum, IntEnum
|
|
29
|
+
import functools
|
|
30
|
+
import itertools
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
import secrets
|
|
34
|
+
import sys
|
|
35
35
|
from typing import (
|
|
36
36
|
Any,
|
|
37
37
|
Callable,
|
|
@@ -51,6 +51,7 @@ from typing_extensions import Self
|
|
|
51
51
|
|
|
52
52
|
from pyee import EventEmitter
|
|
53
53
|
|
|
54
|
+
from bumble import hci
|
|
54
55
|
from .colors import color
|
|
55
56
|
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
|
56
57
|
from .gatt import Characteristic, Descriptor, Service
|
|
@@ -81,6 +82,7 @@ from .hci import (
|
|
|
81
82
|
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
|
82
83
|
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
|
83
84
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
|
85
|
+
HCI_OPERATION_CANCELLED_BY_HOST_ERROR,
|
|
84
86
|
HCI_R2_PAGE_SCAN_REPETITION_MODE,
|
|
85
87
|
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
86
88
|
HCI_SUCCESS,
|
|
@@ -102,11 +104,17 @@ from .hci import (
|
|
|
102
104
|
HCI_LE_Accept_CIS_Request_Command,
|
|
103
105
|
HCI_LE_Add_Device_To_Resolving_List_Command,
|
|
104
106
|
HCI_LE_Advertising_Report_Event,
|
|
107
|
+
HCI_LE_BIGInfo_Advertising_Report_Event,
|
|
105
108
|
HCI_LE_Clear_Resolving_List_Command,
|
|
106
109
|
HCI_LE_Connection_Update_Command,
|
|
107
110
|
HCI_LE_Create_Connection_Cancel_Command,
|
|
108
111
|
HCI_LE_Create_Connection_Command,
|
|
109
112
|
HCI_LE_Create_CIS_Command,
|
|
113
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Command,
|
|
114
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command,
|
|
115
|
+
HCI_LE_Periodic_Advertising_Report_Event,
|
|
116
|
+
HCI_LE_Periodic_Advertising_Sync_Transfer_Command,
|
|
117
|
+
HCI_LE_Periodic_Advertising_Terminate_Sync_Command,
|
|
110
118
|
HCI_LE_Enable_Encryption_Command,
|
|
111
119
|
HCI_LE_Extended_Advertising_Report_Event,
|
|
112
120
|
HCI_LE_Extended_Create_Connection_Command,
|
|
@@ -162,21 +170,29 @@ from .hci import (
|
|
|
162
170
|
OwnAddressType,
|
|
163
171
|
LeFeature,
|
|
164
172
|
LeFeatureMask,
|
|
173
|
+
LmpFeatureMask,
|
|
165
174
|
Phy,
|
|
166
175
|
phy_list_to_bits,
|
|
167
176
|
)
|
|
168
177
|
from .host import Host
|
|
169
|
-
from .gap import GenericAccessService
|
|
178
|
+
from .profiles.gap import GenericAccessService
|
|
170
179
|
from .core import (
|
|
171
180
|
BT_BR_EDR_TRANSPORT,
|
|
172
181
|
BT_CENTRAL_ROLE,
|
|
173
182
|
BT_LE_TRANSPORT,
|
|
174
183
|
BT_PERIPHERAL_ROLE,
|
|
175
184
|
AdvertisingData,
|
|
185
|
+
BaseBumbleError,
|
|
176
186
|
ConnectionParameterUpdateError,
|
|
177
187
|
CommandTimeoutError,
|
|
188
|
+
ConnectionParameters,
|
|
178
189
|
ConnectionPHY,
|
|
190
|
+
InvalidArgumentError,
|
|
191
|
+
InvalidOperationError,
|
|
179
192
|
InvalidStateError,
|
|
193
|
+
NotSupportedError,
|
|
194
|
+
OutOfResourcesError,
|
|
195
|
+
UnreachableError,
|
|
180
196
|
)
|
|
181
197
|
from .utils import (
|
|
182
198
|
AsyncRunner,
|
|
@@ -191,13 +207,13 @@ from .keys import (
|
|
|
191
207
|
KeyStore,
|
|
192
208
|
PairingKeys,
|
|
193
209
|
)
|
|
194
|
-
from
|
|
195
|
-
from
|
|
196
|
-
from
|
|
197
|
-
from
|
|
198
|
-
from
|
|
199
|
-
from
|
|
200
|
-
from
|
|
210
|
+
from bumble import pairing
|
|
211
|
+
from bumble import gatt_client
|
|
212
|
+
from bumble import gatt_server
|
|
213
|
+
from bumble import smp
|
|
214
|
+
from bumble import sdp
|
|
215
|
+
from bumble import l2cap
|
|
216
|
+
from bumble import core
|
|
201
217
|
|
|
202
218
|
if TYPE_CHECKING:
|
|
203
219
|
from .transport.common import TransportSource, TransportSink
|
|
@@ -248,6 +264,9 @@ DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS = l2cap.L2CAP_LE_CREDIT_BASED_CONN
|
|
|
248
264
|
DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
|
|
249
265
|
HCI_LE_Set_Extended_Advertising_Parameters_Command.TX_POWER_NO_PREFERENCE
|
|
250
266
|
)
|
|
267
|
+
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
|
|
268
|
+
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
|
|
269
|
+
DEVICE_DEFAULT_LE_RPA_TIMEOUT = 15 * 60 # 15 minutes (in seconds)
|
|
251
270
|
|
|
252
271
|
# fmt: on
|
|
253
272
|
# pylint: enable=line-too-long
|
|
@@ -259,6 +278,8 @@ DEVICE_MAX_HIGH_DUTY_CYCLE_CONNECTABLE_DIRECTED_ADVERTISING_DURATION = 1.28
|
|
|
259
278
|
# -----------------------------------------------------------------------------
|
|
260
279
|
# Classes
|
|
261
280
|
# -----------------------------------------------------------------------------
|
|
281
|
+
class ObjectLookupError(BaseBumbleError):
|
|
282
|
+
"""Error raised when failed to lookup an object."""
|
|
262
283
|
|
|
263
284
|
|
|
264
285
|
# -----------------------------------------------------------------------------
|
|
@@ -552,6 +573,70 @@ class AdvertisingEventProperties:
|
|
|
552
573
|
)
|
|
553
574
|
|
|
554
575
|
|
|
576
|
+
# -----------------------------------------------------------------------------
|
|
577
|
+
@dataclass
|
|
578
|
+
class PeriodicAdvertisement:
|
|
579
|
+
address: Address
|
|
580
|
+
sid: int
|
|
581
|
+
tx_power: int = (
|
|
582
|
+
HCI_LE_Periodic_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
|
|
583
|
+
)
|
|
584
|
+
rssi: int = HCI_LE_Periodic_Advertising_Report_Event.RSSI_NOT_AVAILABLE
|
|
585
|
+
is_truncated: bool = False
|
|
586
|
+
data_bytes: bytes = b''
|
|
587
|
+
|
|
588
|
+
# Constants
|
|
589
|
+
TX_POWER_NOT_AVAILABLE: ClassVar[int] = (
|
|
590
|
+
HCI_LE_Periodic_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
|
|
591
|
+
)
|
|
592
|
+
RSSI_NOT_AVAILABLE: ClassVar[int] = (
|
|
593
|
+
HCI_LE_Periodic_Advertising_Report_Event.RSSI_NOT_AVAILABLE
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
def __post_init__(self) -> None:
|
|
597
|
+
self.data = (
|
|
598
|
+
None if self.is_truncated else AdvertisingData.from_bytes(self.data_bytes)
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
# -----------------------------------------------------------------------------
|
|
603
|
+
@dataclass
|
|
604
|
+
class BIGInfoAdvertisement:
|
|
605
|
+
address: Address
|
|
606
|
+
sid: int
|
|
607
|
+
num_bis: int
|
|
608
|
+
nse: int
|
|
609
|
+
iso_interval: int
|
|
610
|
+
bn: int
|
|
611
|
+
pto: int
|
|
612
|
+
irc: int
|
|
613
|
+
max_pdu: int
|
|
614
|
+
sdu_interval: int
|
|
615
|
+
max_sdu: int
|
|
616
|
+
phy: Phy
|
|
617
|
+
framed: bool
|
|
618
|
+
encrypted: bool
|
|
619
|
+
|
|
620
|
+
@classmethod
|
|
621
|
+
def from_report(cls, address: Address, sid: int, report) -> Self:
|
|
622
|
+
return cls(
|
|
623
|
+
address,
|
|
624
|
+
sid,
|
|
625
|
+
report.num_bis,
|
|
626
|
+
report.nse,
|
|
627
|
+
report.iso_interval,
|
|
628
|
+
report.bn,
|
|
629
|
+
report.pto,
|
|
630
|
+
report.irc,
|
|
631
|
+
report.max_pdu,
|
|
632
|
+
report.sdu_interval,
|
|
633
|
+
report.max_sdu,
|
|
634
|
+
Phy(report.phy),
|
|
635
|
+
report.framing != 0,
|
|
636
|
+
report.encryption != 0,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
555
640
|
# -----------------------------------------------------------------------------
|
|
556
641
|
# TODO: replace with typing.TypeAlias when the code base is all Python >= 3.10
|
|
557
642
|
AdvertisingChannelMap = HCI_LE_Set_Extended_Advertising_Parameters_Command.ChannelMap
|
|
@@ -795,6 +880,206 @@ class AdvertisingSet(EventEmitter):
|
|
|
795
880
|
self.emit('termination', status)
|
|
796
881
|
|
|
797
882
|
|
|
883
|
+
# -----------------------------------------------------------------------------
|
|
884
|
+
class PeriodicAdvertisingSync(EventEmitter):
|
|
885
|
+
class State(Enum):
|
|
886
|
+
INIT = 0
|
|
887
|
+
PENDING = 1
|
|
888
|
+
ESTABLISHED = 2
|
|
889
|
+
CANCELLED = 3
|
|
890
|
+
ERROR = 4
|
|
891
|
+
LOST = 5
|
|
892
|
+
TERMINATED = 6
|
|
893
|
+
|
|
894
|
+
_state: State
|
|
895
|
+
sync_handle: Optional[int]
|
|
896
|
+
advertiser_address: Address
|
|
897
|
+
sid: int
|
|
898
|
+
skip: int
|
|
899
|
+
sync_timeout: float # Sync timeout, in seconds
|
|
900
|
+
filter_duplicates: bool
|
|
901
|
+
status: int
|
|
902
|
+
advertiser_phy: int
|
|
903
|
+
periodic_advertising_interval: int
|
|
904
|
+
advertiser_clock_accuracy: int
|
|
905
|
+
|
|
906
|
+
def __init__(
|
|
907
|
+
self,
|
|
908
|
+
device: Device,
|
|
909
|
+
advertiser_address: Address,
|
|
910
|
+
sid: int,
|
|
911
|
+
skip: int,
|
|
912
|
+
sync_timeout: float,
|
|
913
|
+
filter_duplicates: bool,
|
|
914
|
+
) -> None:
|
|
915
|
+
super().__init__()
|
|
916
|
+
self._state = self.State.INIT
|
|
917
|
+
self.sync_handle = None
|
|
918
|
+
self.device = device
|
|
919
|
+
self.advertiser_address = advertiser_address
|
|
920
|
+
self.sid = sid
|
|
921
|
+
self.skip = skip
|
|
922
|
+
self.sync_timeout = sync_timeout
|
|
923
|
+
self.filter_duplicates = filter_duplicates
|
|
924
|
+
self.status = HCI_SUCCESS
|
|
925
|
+
self.advertiser_phy = 0
|
|
926
|
+
self.periodic_advertising_interval = 0
|
|
927
|
+
self.advertiser_clock_accuracy = 0
|
|
928
|
+
self.data_accumulator = b''
|
|
929
|
+
|
|
930
|
+
@property
|
|
931
|
+
def state(self) -> State:
|
|
932
|
+
return self._state
|
|
933
|
+
|
|
934
|
+
@state.setter
|
|
935
|
+
def state(self, state: State) -> None:
|
|
936
|
+
logger.debug(f'{self} -> {state.name}')
|
|
937
|
+
self._state = state
|
|
938
|
+
self.emit('state_change')
|
|
939
|
+
|
|
940
|
+
async def establish(self) -> None:
|
|
941
|
+
if self.state != self.State.INIT:
|
|
942
|
+
raise InvalidStateError('sync not in init state')
|
|
943
|
+
|
|
944
|
+
options = HCI_LE_Periodic_Advertising_Create_Sync_Command.Options(0)
|
|
945
|
+
if self.filter_duplicates:
|
|
946
|
+
options |= (
|
|
947
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Command.Options.DUPLICATE_FILTERING_INITIALLY_ENABLED
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
response = await self.device.send_command(
|
|
951
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Command(
|
|
952
|
+
options=options,
|
|
953
|
+
advertising_sid=self.sid,
|
|
954
|
+
advertiser_address_type=self.advertiser_address.address_type,
|
|
955
|
+
advertiser_address=self.advertiser_address,
|
|
956
|
+
skip=self.skip,
|
|
957
|
+
sync_timeout=int(self.sync_timeout * 100),
|
|
958
|
+
sync_cte_type=0,
|
|
959
|
+
)
|
|
960
|
+
)
|
|
961
|
+
if response.status != HCI_Command_Status_Event.PENDING:
|
|
962
|
+
raise HCI_StatusError(response)
|
|
963
|
+
|
|
964
|
+
self.state = self.State.PENDING
|
|
965
|
+
|
|
966
|
+
async def terminate(self) -> None:
|
|
967
|
+
if self.state in (self.State.INIT, self.State.CANCELLED, self.State.TERMINATED):
|
|
968
|
+
return
|
|
969
|
+
|
|
970
|
+
if self.state == self.State.PENDING:
|
|
971
|
+
self.state = self.State.CANCELLED
|
|
972
|
+
response = await self.device.send_command(
|
|
973
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command(),
|
|
974
|
+
)
|
|
975
|
+
if response.return_parameters == HCI_SUCCESS:
|
|
976
|
+
if self in self.device.periodic_advertising_syncs:
|
|
977
|
+
self.device.periodic_advertising_syncs.remove(self)
|
|
978
|
+
return
|
|
979
|
+
|
|
980
|
+
if self.state in (self.State.ESTABLISHED, self.State.ERROR, self.State.LOST):
|
|
981
|
+
self.state = self.State.TERMINATED
|
|
982
|
+
if self.sync_handle is not None:
|
|
983
|
+
await self.device.send_command(
|
|
984
|
+
HCI_LE_Periodic_Advertising_Terminate_Sync_Command(
|
|
985
|
+
sync_handle=self.sync_handle
|
|
986
|
+
)
|
|
987
|
+
)
|
|
988
|
+
self.device.periodic_advertising_syncs.remove(self)
|
|
989
|
+
|
|
990
|
+
async def transfer(self, connection: Connection, service_data: int = 0) -> None:
|
|
991
|
+
if self.sync_handle is not None:
|
|
992
|
+
await connection.transfer_periodic_sync(self.sync_handle, service_data)
|
|
993
|
+
|
|
994
|
+
def on_establishment(
|
|
995
|
+
self,
|
|
996
|
+
status,
|
|
997
|
+
sync_handle,
|
|
998
|
+
advertiser_phy,
|
|
999
|
+
periodic_advertising_interval,
|
|
1000
|
+
advertiser_clock_accuracy,
|
|
1001
|
+
) -> None:
|
|
1002
|
+
self.status = status
|
|
1003
|
+
|
|
1004
|
+
if self.state == self.State.CANCELLED:
|
|
1005
|
+
# Somehow, we receive an established event after trying to cancel, most
|
|
1006
|
+
# likely because the cancel command was sent too late, when the sync was
|
|
1007
|
+
# already established, but before the established event was sent.
|
|
1008
|
+
# We need to automatically terminate.
|
|
1009
|
+
logger.debug(
|
|
1010
|
+
"received established event for cancelled sync, will terminate"
|
|
1011
|
+
)
|
|
1012
|
+
self.state = self.State.ESTABLISHED
|
|
1013
|
+
AsyncRunner.spawn(self.terminate())
|
|
1014
|
+
return
|
|
1015
|
+
|
|
1016
|
+
if status == HCI_SUCCESS:
|
|
1017
|
+
self.sync_handle = sync_handle
|
|
1018
|
+
self.advertiser_phy = advertiser_phy
|
|
1019
|
+
self.periodic_advertising_interval = periodic_advertising_interval
|
|
1020
|
+
self.advertiser_clock_accuracy = advertiser_clock_accuracy
|
|
1021
|
+
self.state = self.State.ESTABLISHED
|
|
1022
|
+
self.emit('establishment')
|
|
1023
|
+
return
|
|
1024
|
+
|
|
1025
|
+
# We don't need to keep a reference anymore
|
|
1026
|
+
if self in self.device.periodic_advertising_syncs:
|
|
1027
|
+
self.device.periodic_advertising_syncs.remove(self)
|
|
1028
|
+
|
|
1029
|
+
if status == HCI_OPERATION_CANCELLED_BY_HOST_ERROR:
|
|
1030
|
+
self.state = self.State.CANCELLED
|
|
1031
|
+
self.emit('cancellation')
|
|
1032
|
+
return
|
|
1033
|
+
|
|
1034
|
+
self.state = self.State.ERROR
|
|
1035
|
+
self.emit('error')
|
|
1036
|
+
|
|
1037
|
+
def on_loss(self):
|
|
1038
|
+
self.state = self.State.LOST
|
|
1039
|
+
self.emit('loss')
|
|
1040
|
+
|
|
1041
|
+
def on_periodic_advertising_report(self, report) -> None:
|
|
1042
|
+
self.data_accumulator += report.data
|
|
1043
|
+
if (
|
|
1044
|
+
report.data_status
|
|
1045
|
+
== HCI_LE_Periodic_Advertising_Report_Event.DataStatus.DATA_INCOMPLETE_MORE_TO_COME
|
|
1046
|
+
):
|
|
1047
|
+
return
|
|
1048
|
+
|
|
1049
|
+
self.emit(
|
|
1050
|
+
'periodic_advertisement',
|
|
1051
|
+
PeriodicAdvertisement(
|
|
1052
|
+
self.advertiser_address,
|
|
1053
|
+
self.sid,
|
|
1054
|
+
report.tx_power,
|
|
1055
|
+
report.rssi,
|
|
1056
|
+
is_truncated=(
|
|
1057
|
+
report.data_status
|
|
1058
|
+
== HCI_LE_Periodic_Advertising_Report_Event.DataStatus.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME
|
|
1059
|
+
),
|
|
1060
|
+
data_bytes=self.data_accumulator,
|
|
1061
|
+
),
|
|
1062
|
+
)
|
|
1063
|
+
self.data_accumulator = b''
|
|
1064
|
+
|
|
1065
|
+
def on_biginfo_advertising_report(self, report) -> None:
|
|
1066
|
+
self.emit(
|
|
1067
|
+
'biginfo_advertisement',
|
|
1068
|
+
BIGInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
def __str__(self) -> str:
|
|
1072
|
+
return (
|
|
1073
|
+
'PeriodicAdvertisingSync('
|
|
1074
|
+
f'state={self.state.name}, '
|
|
1075
|
+
f'sync_handle={self.sync_handle}, '
|
|
1076
|
+
f'sid={self.sid}, '
|
|
1077
|
+
f'skip={self.skip}, '
|
|
1078
|
+
f'filter_duplicates={self.filter_duplicates}'
|
|
1079
|
+
')'
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
|
|
798
1083
|
# -----------------------------------------------------------------------------
|
|
799
1084
|
class LePhyOptions:
|
|
800
1085
|
# Coded PHY preference
|
|
@@ -867,6 +1152,15 @@ class Peer:
|
|
|
867
1152
|
async def discover_attributes(self) -> List[gatt_client.AttributeProxy]:
|
|
868
1153
|
return await self.gatt_client.discover_attributes()
|
|
869
1154
|
|
|
1155
|
+
async def discover_all(self):
|
|
1156
|
+
await self.discover_services()
|
|
1157
|
+
for service in self.services:
|
|
1158
|
+
await self.discover_characteristics(service=service)
|
|
1159
|
+
|
|
1160
|
+
for service in self.services:
|
|
1161
|
+
for characteristic in service.characteristics:
|
|
1162
|
+
await self.discover_descriptors(characteristic=characteristic)
|
|
1163
|
+
|
|
870
1164
|
async def subscribe(
|
|
871
1165
|
self,
|
|
872
1166
|
characteristic: gatt_client.CharacteristicProxy,
|
|
@@ -906,12 +1200,29 @@ class Peer:
|
|
|
906
1200
|
return self.gatt_client.get_services_by_uuid(uuid)
|
|
907
1201
|
|
|
908
1202
|
def get_characteristics_by_uuid(
|
|
909
|
-
self,
|
|
1203
|
+
self,
|
|
1204
|
+
uuid: core.UUID,
|
|
1205
|
+
service: Optional[Union[gatt_client.ServiceProxy, core.UUID]] = None,
|
|
910
1206
|
) -> List[gatt_client.CharacteristicProxy]:
|
|
1207
|
+
if isinstance(service, core.UUID):
|
|
1208
|
+
return list(
|
|
1209
|
+
itertools.chain(
|
|
1210
|
+
*[
|
|
1211
|
+
self.get_characteristics_by_uuid(uuid, s)
|
|
1212
|
+
for s in self.get_services_by_uuid(service)
|
|
1213
|
+
]
|
|
1214
|
+
)
|
|
1215
|
+
)
|
|
1216
|
+
|
|
911
1217
|
return self.gatt_client.get_characteristics_by_uuid(uuid, service)
|
|
912
1218
|
|
|
913
|
-
def create_service_proxy(
|
|
914
|
-
|
|
1219
|
+
def create_service_proxy(
|
|
1220
|
+
self, proxy_class: Type[_PROXY_CLASS]
|
|
1221
|
+
) -> Optional[_PROXY_CLASS]:
|
|
1222
|
+
if proxy := proxy_class.from_client(self.gatt_client):
|
|
1223
|
+
return cast(_PROXY_CLASS, proxy)
|
|
1224
|
+
|
|
1225
|
+
return None
|
|
915
1226
|
|
|
916
1227
|
async def discover_service_and_create_proxy(
|
|
917
1228
|
self, proxy_class: Type[_PROXY_CLASS]
|
|
@@ -1008,6 +1319,7 @@ class Connection(CompositeEventEmitter):
|
|
|
1008
1319
|
handle: int
|
|
1009
1320
|
transport: int
|
|
1010
1321
|
self_address: Address
|
|
1322
|
+
self_resolvable_address: Optional[Address]
|
|
1011
1323
|
peer_address: Address
|
|
1012
1324
|
peer_resolvable_address: Optional[Address]
|
|
1013
1325
|
peer_le_features: Optional[LeFeatureMask]
|
|
@@ -1055,6 +1367,7 @@ class Connection(CompositeEventEmitter):
|
|
|
1055
1367
|
handle,
|
|
1056
1368
|
transport,
|
|
1057
1369
|
self_address,
|
|
1370
|
+
self_resolvable_address,
|
|
1058
1371
|
peer_address,
|
|
1059
1372
|
peer_resolvable_address,
|
|
1060
1373
|
role,
|
|
@@ -1066,6 +1379,7 @@ class Connection(CompositeEventEmitter):
|
|
|
1066
1379
|
self.handle = handle
|
|
1067
1380
|
self.transport = transport
|
|
1068
1381
|
self.self_address = self_address
|
|
1382
|
+
self.self_resolvable_address = self_resolvable_address
|
|
1069
1383
|
self.peer_address = peer_address
|
|
1070
1384
|
self.peer_resolvable_address = peer_resolvable_address
|
|
1071
1385
|
self.peer_name = None # Classic only
|
|
@@ -1099,6 +1413,7 @@ class Connection(CompositeEventEmitter):
|
|
|
1099
1413
|
None,
|
|
1100
1414
|
BT_BR_EDR_TRANSPORT,
|
|
1101
1415
|
device.public_address,
|
|
1416
|
+
None,
|
|
1102
1417
|
peer_address,
|
|
1103
1418
|
None,
|
|
1104
1419
|
role,
|
|
@@ -1192,11 +1507,9 @@ class Connection(CompositeEventEmitter):
|
|
|
1192
1507
|
|
|
1193
1508
|
try:
|
|
1194
1509
|
await asyncio.wait_for(self.device.abort_on('flush', abort), timeout)
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
self.remove_listener('disconnection', abort.set_result)
|
|
1199
|
-
self.remove_listener('disconnection_failure', abort.set_exception)
|
|
1510
|
+
finally:
|
|
1511
|
+
self.remove_listener('disconnection', abort.set_result)
|
|
1512
|
+
self.remove_listener('disconnection_failure', abort.set_exception)
|
|
1200
1513
|
|
|
1201
1514
|
async def set_data_length(self, tx_octets, tx_time) -> None:
|
|
1202
1515
|
return await self.device.set_data_length(self, tx_octets, tx_time)
|
|
@@ -1227,6 +1540,11 @@ class Connection(CompositeEventEmitter):
|
|
|
1227
1540
|
async def get_phy(self):
|
|
1228
1541
|
return await self.device.get_connection_phy(self)
|
|
1229
1542
|
|
|
1543
|
+
async def transfer_periodic_sync(
|
|
1544
|
+
self, sync_handle: int, service_data: int = 0
|
|
1545
|
+
) -> None:
|
|
1546
|
+
await self.device.transfer_periodic_sync(self, sync_handle, service_data)
|
|
1547
|
+
|
|
1230
1548
|
# [Classic only]
|
|
1231
1549
|
async def request_remote_name(self):
|
|
1232
1550
|
return await self.device.request_remote_name(self)
|
|
@@ -1257,7 +1575,9 @@ class Connection(CompositeEventEmitter):
|
|
|
1257
1575
|
f'Connection(handle=0x{self.handle:04X}, '
|
|
1258
1576
|
f'role={self.role_name}, '
|
|
1259
1577
|
f'self_address={self.self_address}, '
|
|
1260
|
-
f'
|
|
1578
|
+
f'self_resolvable_address={self.self_resolvable_address}, '
|
|
1579
|
+
f'peer_address={self.peer_address}, '
|
|
1580
|
+
f'peer_resolvable_address={self.peer_resolvable_address})'
|
|
1261
1581
|
)
|
|
1262
1582
|
|
|
1263
1583
|
|
|
@@ -1272,13 +1592,15 @@ class DeviceConfiguration:
|
|
|
1272
1592
|
advertising_interval_min: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
|
1273
1593
|
advertising_interval_max: int = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
|
1274
1594
|
le_enabled: bool = True
|
|
1275
|
-
# LE host enable 2nd parameter
|
|
1276
1595
|
le_simultaneous_enabled: bool = False
|
|
1596
|
+
le_privacy_enabled: bool = False
|
|
1597
|
+
le_rpa_timeout: int = DEVICE_DEFAULT_LE_RPA_TIMEOUT
|
|
1277
1598
|
classic_enabled: bool = False
|
|
1278
1599
|
classic_sc_enabled: bool = True
|
|
1279
1600
|
classic_ssp_enabled: bool = True
|
|
1280
1601
|
classic_smp_enabled: bool = True
|
|
1281
1602
|
classic_accept_any: bool = True
|
|
1603
|
+
classic_interlaced_scan_enabled: bool = True
|
|
1282
1604
|
connectable: bool = True
|
|
1283
1605
|
discoverable: bool = True
|
|
1284
1606
|
advertising_data: bytes = bytes(
|
|
@@ -1289,7 +1611,10 @@ class DeviceConfiguration:
|
|
|
1289
1611
|
irk: bytes = bytes(16) # This really must be changed for any level of security
|
|
1290
1612
|
keystore: Optional[str] = None
|
|
1291
1613
|
address_resolution_offload: bool = False
|
|
1614
|
+
address_generation_offload: bool = False
|
|
1292
1615
|
cis_enabled: bool = False
|
|
1616
|
+
identity_address_type: Optional[int] = None
|
|
1617
|
+
io_capability: int = pairing.PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT
|
|
1293
1618
|
|
|
1294
1619
|
def __post_init__(self) -> None:
|
|
1295
1620
|
self.gatt_services: List[Dict[str, Any]] = []
|
|
@@ -1374,7 +1699,9 @@ def with_connection_from_handle(function):
|
|
|
1374
1699
|
@functools.wraps(function)
|
|
1375
1700
|
def wrapper(self, connection_handle, *args, **kwargs):
|
|
1376
1701
|
if (connection := self.lookup_connection(connection_handle)) is None:
|
|
1377
|
-
raise
|
|
1702
|
+
raise ObjectLookupError(
|
|
1703
|
+
f'no connection for handle: 0x{connection_handle:04x}'
|
|
1704
|
+
)
|
|
1378
1705
|
return function(self, connection, *args, **kwargs)
|
|
1379
1706
|
|
|
1380
1707
|
return wrapper
|
|
@@ -1389,7 +1716,7 @@ def with_connection_from_address(function):
|
|
|
1389
1716
|
for connection in self.connections.values():
|
|
1390
1717
|
if connection.peer_address == address:
|
|
1391
1718
|
return function(self, connection, *args, **kwargs)
|
|
1392
|
-
raise
|
|
1719
|
+
raise ObjectLookupError('no connection for address')
|
|
1393
1720
|
|
|
1394
1721
|
return wrapper
|
|
1395
1722
|
|
|
@@ -1409,6 +1736,20 @@ def try_with_connection_from_address(function):
|
|
|
1409
1736
|
return wrapper
|
|
1410
1737
|
|
|
1411
1738
|
|
|
1739
|
+
# Decorator that converts the first argument from a sync handle to a periodic
|
|
1740
|
+
# advertising sync object
|
|
1741
|
+
def with_periodic_advertising_sync_from_handle(function):
|
|
1742
|
+
@functools.wraps(function)
|
|
1743
|
+
def wrapper(self, sync_handle, *args, **kwargs):
|
|
1744
|
+
if (sync := self.lookup_periodic_advertising_sync(sync_handle)) is None:
|
|
1745
|
+
raise ValueError(
|
|
1746
|
+
f'no periodic advertising sync for handle: 0x{sync_handle:04x}'
|
|
1747
|
+
)
|
|
1748
|
+
return function(self, sync, *args, **kwargs)
|
|
1749
|
+
|
|
1750
|
+
return wrapper
|
|
1751
|
+
|
|
1752
|
+
|
|
1412
1753
|
# Decorator that adds a method to the list of event handlers for host events.
|
|
1413
1754
|
# This assumes that the method name starts with `on_`
|
|
1414
1755
|
def host_event_handler(function):
|
|
@@ -1425,8 +1766,9 @@ device_host_event_handlers: List[str] = []
|
|
|
1425
1766
|
# -----------------------------------------------------------------------------
|
|
1426
1767
|
class Device(CompositeEventEmitter):
|
|
1427
1768
|
# Incomplete list of fields.
|
|
1428
|
-
random_address: Address
|
|
1429
|
-
public_address: Address
|
|
1769
|
+
random_address: Address # Random address that may change with RPA
|
|
1770
|
+
public_address: Address # Public address (obtained from the controller)
|
|
1771
|
+
static_address: Address # Random address that can be set but does not change
|
|
1430
1772
|
classic_enabled: bool
|
|
1431
1773
|
name: str
|
|
1432
1774
|
class_of_device: int
|
|
@@ -1439,6 +1781,7 @@ class Device(CompositeEventEmitter):
|
|
|
1439
1781
|
Address, List[asyncio.Future[Union[Connection, Tuple[Address, int, int]]]]
|
|
1440
1782
|
]
|
|
1441
1783
|
advertisement_accumulators: Dict[Address, AdvertisementDataAccumulator]
|
|
1784
|
+
periodic_advertising_syncs: List[PeriodicAdvertisingSync]
|
|
1442
1785
|
config: DeviceConfiguration
|
|
1443
1786
|
legacy_advertiser: Optional[LegacyAdvertiser]
|
|
1444
1787
|
sco_links: Dict[int, ScoLink]
|
|
@@ -1524,6 +1867,7 @@ class Device(CompositeEventEmitter):
|
|
|
1524
1867
|
[l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS]
|
|
1525
1868
|
)
|
|
1526
1869
|
self.advertisement_accumulators = {} # Accumulators, by address
|
|
1870
|
+
self.periodic_advertising_syncs = []
|
|
1527
1871
|
self.scanning = False
|
|
1528
1872
|
self.scanning_is_passive = False
|
|
1529
1873
|
self.discovering = False
|
|
@@ -1554,26 +1898,33 @@ class Device(CompositeEventEmitter):
|
|
|
1554
1898
|
config = config or DeviceConfiguration()
|
|
1555
1899
|
self.config = config
|
|
1556
1900
|
|
|
1557
|
-
self.public_address = Address('00:00:00:00:00:00')
|
|
1558
1901
|
self.name = config.name
|
|
1902
|
+
self.public_address = Address.ANY
|
|
1559
1903
|
self.random_address = config.address
|
|
1904
|
+
self.static_address = config.address
|
|
1560
1905
|
self.class_of_device = config.class_of_device
|
|
1561
1906
|
self.keystore = None
|
|
1562
1907
|
self.irk = config.irk
|
|
1563
1908
|
self.le_enabled = config.le_enabled
|
|
1564
|
-
self.classic_enabled = config.classic_enabled
|
|
1565
1909
|
self.le_simultaneous_enabled = config.le_simultaneous_enabled
|
|
1910
|
+
self.le_privacy_enabled = config.le_privacy_enabled
|
|
1911
|
+
self.le_rpa_timeout = config.le_rpa_timeout
|
|
1912
|
+
self.le_rpa_periodic_update_task: Optional[asyncio.Task] = None
|
|
1913
|
+
self.classic_enabled = config.classic_enabled
|
|
1566
1914
|
self.cis_enabled = config.cis_enabled
|
|
1567
1915
|
self.classic_sc_enabled = config.classic_sc_enabled
|
|
1568
1916
|
self.classic_ssp_enabled = config.classic_ssp_enabled
|
|
1569
1917
|
self.classic_smp_enabled = config.classic_smp_enabled
|
|
1918
|
+
self.classic_interlaced_scan_enabled = config.classic_interlaced_scan_enabled
|
|
1570
1919
|
self.discoverable = config.discoverable
|
|
1571
1920
|
self.connectable = config.connectable
|
|
1572
1921
|
self.classic_accept_any = config.classic_accept_any
|
|
1573
1922
|
self.address_resolution_offload = config.address_resolution_offload
|
|
1923
|
+
self.address_generation_offload = config.address_generation_offload
|
|
1574
1924
|
|
|
1575
1925
|
# Extended advertising.
|
|
1576
1926
|
self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
|
|
1927
|
+
self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {}
|
|
1577
1928
|
|
|
1578
1929
|
# Legacy advertising.
|
|
1579
1930
|
# The advertising and scan response data, as well as the advertising interval
|
|
@@ -1625,10 +1976,23 @@ class Device(CompositeEventEmitter):
|
|
|
1625
1976
|
if isinstance(address, str):
|
|
1626
1977
|
address = Address(address)
|
|
1627
1978
|
self.random_address = address
|
|
1979
|
+
self.static_address = address
|
|
1628
1980
|
|
|
1629
1981
|
# Setup SMP
|
|
1630
1982
|
self.smp_manager = smp.Manager(
|
|
1631
|
-
self,
|
|
1983
|
+
self,
|
|
1984
|
+
pairing_config_factory=lambda connection: pairing.PairingConfig(
|
|
1985
|
+
identity_address_type=(
|
|
1986
|
+
pairing.PairingConfig.AddressType(self.config.identity_address_type)
|
|
1987
|
+
if self.config.identity_address_type
|
|
1988
|
+
else None
|
|
1989
|
+
),
|
|
1990
|
+
delegate=pairing.PairingDelegate(
|
|
1991
|
+
io_capability=pairing.PairingDelegate.IoCapability(
|
|
1992
|
+
self.config.io_capability
|
|
1993
|
+
)
|
|
1994
|
+
),
|
|
1995
|
+
),
|
|
1632
1996
|
)
|
|
1633
1997
|
|
|
1634
1998
|
self.l2cap_channel_manager.register_fixed_channel(smp.SMP_CID, self.on_smp_pdu)
|
|
@@ -1706,6 +2070,18 @@ class Device(CompositeEventEmitter):
|
|
|
1706
2070
|
|
|
1707
2071
|
return None
|
|
1708
2072
|
|
|
2073
|
+
def lookup_periodic_advertising_sync(
|
|
2074
|
+
self, sync_handle: int
|
|
2075
|
+
) -> Optional[PeriodicAdvertisingSync]:
|
|
2076
|
+
return next(
|
|
2077
|
+
(
|
|
2078
|
+
sync
|
|
2079
|
+
for sync in self.periodic_advertising_syncs
|
|
2080
|
+
if sync.sync_handle == sync_handle
|
|
2081
|
+
),
|
|
2082
|
+
None,
|
|
2083
|
+
)
|
|
2084
|
+
|
|
1709
2085
|
@deprecated("Please use create_l2cap_server()")
|
|
1710
2086
|
def register_l2cap_server(self, psm, server) -> int:
|
|
1711
2087
|
return self.l2cap_channel_manager.register_server(psm, server)
|
|
@@ -1798,7 +2174,7 @@ class Device(CompositeEventEmitter):
|
|
|
1798
2174
|
spec=spec,
|
|
1799
2175
|
)
|
|
1800
2176
|
else:
|
|
1801
|
-
raise
|
|
2177
|
+
raise InvalidArgumentError(f'Unexpected mode {spec}')
|
|
1802
2178
|
|
|
1803
2179
|
def send_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes) -> None:
|
|
1804
2180
|
self.host.send_l2cap_pdu(connection_handle, cid, pdu)
|
|
@@ -1840,26 +2216,26 @@ class Device(CompositeEventEmitter):
|
|
|
1840
2216
|
HCI_Write_LE_Host_Support_Command(
|
|
1841
2217
|
le_supported_host=int(self.le_enabled),
|
|
1842
2218
|
simultaneous_le_host=int(self.le_simultaneous_enabled),
|
|
1843
|
-
)
|
|
2219
|
+
),
|
|
2220
|
+
check_result=True,
|
|
1844
2221
|
)
|
|
1845
2222
|
|
|
1846
2223
|
if self.le_enabled:
|
|
1847
|
-
#
|
|
1848
|
-
if self.
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2224
|
+
# Generate a random address if not set.
|
|
2225
|
+
if self.static_address == Address.ANY_RANDOM:
|
|
2226
|
+
self.static_address = Address.generate_static_address()
|
|
2227
|
+
|
|
2228
|
+
# If LE Privacy is enabled, generate an RPA
|
|
2229
|
+
if self.le_privacy_enabled:
|
|
2230
|
+
self.random_address = Address.generate_private_address(self.irk)
|
|
2231
|
+
logger.info(f'Initial RPA: {self.random_address}')
|
|
2232
|
+
if self.le_rpa_timeout > 0:
|
|
2233
|
+
# Start a task to periodically generate a new RPA
|
|
2234
|
+
self.le_rpa_periodic_update_task = asyncio.create_task(
|
|
2235
|
+
self._run_rpa_periodic_update()
|
|
1854
2236
|
)
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
address_bytes = response.return_parameters.random_number[
|
|
1858
|
-
:5
|
|
1859
|
-
] + bytes([response.return_parameters.random_number[5] | 0xC0])
|
|
1860
|
-
|
|
1861
|
-
# Create a static random address from the random bytes
|
|
1862
|
-
self.random_address = Address(address_bytes)
|
|
2237
|
+
else:
|
|
2238
|
+
self.random_address = self.static_address
|
|
1863
2239
|
|
|
1864
2240
|
if self.random_address != Address.ANY_RANDOM:
|
|
1865
2241
|
logger.debug(
|
|
@@ -1884,7 +2260,8 @@ class Device(CompositeEventEmitter):
|
|
|
1884
2260
|
await self.send_command(
|
|
1885
2261
|
HCI_LE_Set_Address_Resolution_Enable_Command(
|
|
1886
2262
|
address_resolution_enable=1
|
|
1887
|
-
)
|
|
2263
|
+
),
|
|
2264
|
+
check_result=True,
|
|
1888
2265
|
)
|
|
1889
2266
|
|
|
1890
2267
|
if self.cis_enabled:
|
|
@@ -1892,7 +2269,8 @@ class Device(CompositeEventEmitter):
|
|
|
1892
2269
|
HCI_LE_Set_Host_Feature_Command(
|
|
1893
2270
|
bit_number=LeFeature.CONNECTED_ISOCHRONOUS_STREAM,
|
|
1894
2271
|
bit_value=1,
|
|
1895
|
-
)
|
|
2272
|
+
),
|
|
2273
|
+
check_result=True,
|
|
1896
2274
|
)
|
|
1897
2275
|
|
|
1898
2276
|
if self.classic_enabled:
|
|
@@ -1915,6 +2293,21 @@ class Device(CompositeEventEmitter):
|
|
|
1915
2293
|
await self.set_connectable(self.connectable)
|
|
1916
2294
|
await self.set_discoverable(self.discoverable)
|
|
1917
2295
|
|
|
2296
|
+
if self.classic_interlaced_scan_enabled:
|
|
2297
|
+
if self.host.supports_lmp_features(LmpFeatureMask.INTERLACED_PAGE_SCAN):
|
|
2298
|
+
await self.send_command(
|
|
2299
|
+
hci.HCI_Write_Page_Scan_Type_Command(page_scan_type=1),
|
|
2300
|
+
check_result=True,
|
|
2301
|
+
)
|
|
2302
|
+
|
|
2303
|
+
if self.host.supports_lmp_features(
|
|
2304
|
+
LmpFeatureMask.INTERLACED_INQUIRY_SCAN
|
|
2305
|
+
):
|
|
2306
|
+
await self.send_command(
|
|
2307
|
+
hci.HCI_Write_Inquiry_Scan_Type_Command(scan_type=1),
|
|
2308
|
+
check_result=True,
|
|
2309
|
+
)
|
|
2310
|
+
|
|
1918
2311
|
# Done
|
|
1919
2312
|
self.powered_on = True
|
|
1920
2313
|
|
|
@@ -1923,9 +2316,45 @@ class Device(CompositeEventEmitter):
|
|
|
1923
2316
|
|
|
1924
2317
|
async def power_off(self) -> None:
|
|
1925
2318
|
if self.powered_on:
|
|
2319
|
+
if self.le_rpa_periodic_update_task:
|
|
2320
|
+
self.le_rpa_periodic_update_task.cancel()
|
|
2321
|
+
|
|
1926
2322
|
await self.host.flush()
|
|
2323
|
+
|
|
1927
2324
|
self.powered_on = False
|
|
1928
2325
|
|
|
2326
|
+
async def update_rpa(self) -> bool:
|
|
2327
|
+
"""
|
|
2328
|
+
Try to update the RPA.
|
|
2329
|
+
|
|
2330
|
+
Returns:
|
|
2331
|
+
True if the RPA was updated, False if it could not be updated.
|
|
2332
|
+
"""
|
|
2333
|
+
|
|
2334
|
+
# Check if this is a good time to rotate the address
|
|
2335
|
+
if self.is_advertising or self.is_scanning or self.is_le_connecting:
|
|
2336
|
+
logger.debug('skipping RPA update')
|
|
2337
|
+
return False
|
|
2338
|
+
|
|
2339
|
+
random_address = Address.generate_private_address(self.irk)
|
|
2340
|
+
response = await self.send_command(
|
|
2341
|
+
HCI_LE_Set_Random_Address_Command(random_address=self.random_address)
|
|
2342
|
+
)
|
|
2343
|
+
if response.return_parameters == HCI_SUCCESS:
|
|
2344
|
+
logger.info(f'new RPA: {random_address}')
|
|
2345
|
+
self.random_address = random_address
|
|
2346
|
+
return True
|
|
2347
|
+
else:
|
|
2348
|
+
logger.warning(f'failed to set RPA: {response.return_parameters}')
|
|
2349
|
+
return False
|
|
2350
|
+
|
|
2351
|
+
async def _run_rpa_periodic_update(self) -> None:
|
|
2352
|
+
"""Update the RPA periodically"""
|
|
2353
|
+
while self.le_rpa_timeout != 0:
|
|
2354
|
+
await asyncio.sleep(self.le_rpa_timeout)
|
|
2355
|
+
if not self.update_rpa():
|
|
2356
|
+
logger.debug("periodic RPA update failed")
|
|
2357
|
+
|
|
1929
2358
|
async def refresh_resolving_list(self) -> None:
|
|
1930
2359
|
assert self.keystore is not None
|
|
1931
2360
|
|
|
@@ -1933,7 +2362,7 @@ class Device(CompositeEventEmitter):
|
|
|
1933
2362
|
# Create a host-side address resolver
|
|
1934
2363
|
self.address_resolver = smp.AddressResolver(resolving_keys)
|
|
1935
2364
|
|
|
1936
|
-
if self.address_resolution_offload:
|
|
2365
|
+
if self.address_resolution_offload or self.address_generation_offload:
|
|
1937
2366
|
await self.send_command(HCI_LE_Clear_Resolving_List_Command())
|
|
1938
2367
|
|
|
1939
2368
|
# Add an empty entry for non-directed address generation.
|
|
@@ -1959,7 +2388,7 @@ class Device(CompositeEventEmitter):
|
|
|
1959
2388
|
def supports_le_features(self, feature: LeFeatureMask) -> bool:
|
|
1960
2389
|
return self.host.supports_le_features(feature)
|
|
1961
2390
|
|
|
1962
|
-
def supports_le_phy(self, phy):
|
|
2391
|
+
def supports_le_phy(self, phy: int) -> bool:
|
|
1963
2392
|
if phy == HCI_LE_1M_PHY:
|
|
1964
2393
|
return True
|
|
1965
2394
|
|
|
@@ -1968,7 +2397,7 @@ class Device(CompositeEventEmitter):
|
|
|
1968
2397
|
HCI_LE_CODED_PHY: LeFeatureMask.LE_CODED_PHY,
|
|
1969
2398
|
}
|
|
1970
2399
|
if phy not in feature_map:
|
|
1971
|
-
raise
|
|
2400
|
+
raise InvalidArgumentError('invalid PHY')
|
|
1972
2401
|
|
|
1973
2402
|
return self.supports_le_features(feature_map[phy])
|
|
1974
2403
|
|
|
@@ -1976,6 +2405,10 @@ class Device(CompositeEventEmitter):
|
|
|
1976
2405
|
def supports_le_extended_advertising(self):
|
|
1977
2406
|
return self.supports_le_features(LeFeatureMask.LE_EXTENDED_ADVERTISING)
|
|
1978
2407
|
|
|
2408
|
+
@property
|
|
2409
|
+
def supports_le_periodic_advertising(self):
|
|
2410
|
+
return self.supports_le_features(LeFeatureMask.LE_PERIODIC_ADVERTISING)
|
|
2411
|
+
|
|
1979
2412
|
async def start_advertising(
|
|
1980
2413
|
self,
|
|
1981
2414
|
advertising_type: AdvertisingType = AdvertisingType.UNDIRECTED_CONNECTABLE_SCANNABLE,
|
|
@@ -2028,7 +2461,7 @@ class Device(CompositeEventEmitter):
|
|
|
2028
2461
|
# Decide what peer address to use
|
|
2029
2462
|
if advertising_type.is_directed:
|
|
2030
2463
|
if target is None:
|
|
2031
|
-
raise
|
|
2464
|
+
raise InvalidArgumentError('directed advertising requires a target')
|
|
2032
2465
|
peer_address = target
|
|
2033
2466
|
else:
|
|
2034
2467
|
peer_address = Address.ANY
|
|
@@ -2135,7 +2568,7 @@ class Device(CompositeEventEmitter):
|
|
|
2135
2568
|
and advertising_data
|
|
2136
2569
|
and scan_response_data
|
|
2137
2570
|
):
|
|
2138
|
-
raise
|
|
2571
|
+
raise InvalidArgumentError(
|
|
2139
2572
|
"Extended advertisements can't have both data and scan \
|
|
2140
2573
|
response data"
|
|
2141
2574
|
)
|
|
@@ -2151,7 +2584,9 @@ class Device(CompositeEventEmitter):
|
|
|
2151
2584
|
if handle not in self.extended_advertising_sets
|
|
2152
2585
|
)
|
|
2153
2586
|
except StopIteration as exc:
|
|
2154
|
-
raise
|
|
2587
|
+
raise OutOfResourcesError(
|
|
2588
|
+
"all valid advertising handles already in use"
|
|
2589
|
+
) from exc
|
|
2155
2590
|
|
|
2156
2591
|
# Use the device's random address if a random address is needed but none was
|
|
2157
2592
|
# provided.
|
|
@@ -2250,14 +2685,14 @@ class Device(CompositeEventEmitter):
|
|
|
2250
2685
|
) -> None:
|
|
2251
2686
|
# Check that the arguments are legal
|
|
2252
2687
|
if scan_interval < scan_window:
|
|
2253
|
-
raise
|
|
2688
|
+
raise InvalidArgumentError('scan_interval must be >= scan_window')
|
|
2254
2689
|
if (
|
|
2255
2690
|
scan_interval < DEVICE_MIN_SCAN_INTERVAL
|
|
2256
2691
|
or scan_interval > DEVICE_MAX_SCAN_INTERVAL
|
|
2257
2692
|
):
|
|
2258
|
-
raise
|
|
2693
|
+
raise InvalidArgumentError('scan_interval out of range')
|
|
2259
2694
|
if scan_window < DEVICE_MIN_SCAN_WINDOW or scan_window > DEVICE_MAX_SCAN_WINDOW:
|
|
2260
|
-
raise
|
|
2695
|
+
raise InvalidArgumentError('scan_interval out of range')
|
|
2261
2696
|
|
|
2262
2697
|
# Reset the accumulators
|
|
2263
2698
|
self.advertisement_accumulators = {}
|
|
@@ -2285,7 +2720,7 @@ class Device(CompositeEventEmitter):
|
|
|
2285
2720
|
scanning_phy_count += 1
|
|
2286
2721
|
|
|
2287
2722
|
if scanning_phy_count == 0:
|
|
2288
|
-
raise
|
|
2723
|
+
raise InvalidArgumentError('at least one scanning PHY must be enabled')
|
|
2289
2724
|
|
|
2290
2725
|
await self.send_command(
|
|
2291
2726
|
HCI_LE_Set_Extended_Scan_Parameters_Command(
|
|
@@ -2368,6 +2803,120 @@ class Device(CompositeEventEmitter):
|
|
|
2368
2803
|
if advertisement := accumulator.update(report):
|
|
2369
2804
|
self.emit('advertisement', advertisement)
|
|
2370
2805
|
|
|
2806
|
+
async def create_periodic_advertising_sync(
|
|
2807
|
+
self,
|
|
2808
|
+
advertiser_address: Address,
|
|
2809
|
+
sid: int,
|
|
2810
|
+
skip: int = DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP,
|
|
2811
|
+
sync_timeout: float = DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT,
|
|
2812
|
+
filter_duplicates: bool = False,
|
|
2813
|
+
) -> PeriodicAdvertisingSync:
|
|
2814
|
+
# Check that the controller supports the feature.
|
|
2815
|
+
if not self.supports_le_periodic_advertising:
|
|
2816
|
+
raise NotSupportedError()
|
|
2817
|
+
|
|
2818
|
+
# Check that there isn't already an equivalent entry
|
|
2819
|
+
if any(
|
|
2820
|
+
sync.advertiser_address == advertiser_address and sync.sid == sid
|
|
2821
|
+
for sync in self.periodic_advertising_syncs
|
|
2822
|
+
):
|
|
2823
|
+
raise ValueError("equivalent entry already created")
|
|
2824
|
+
|
|
2825
|
+
# Create a new entry
|
|
2826
|
+
sync = PeriodicAdvertisingSync(
|
|
2827
|
+
device=self,
|
|
2828
|
+
advertiser_address=advertiser_address,
|
|
2829
|
+
sid=sid,
|
|
2830
|
+
skip=skip,
|
|
2831
|
+
sync_timeout=sync_timeout,
|
|
2832
|
+
filter_duplicates=filter_duplicates,
|
|
2833
|
+
)
|
|
2834
|
+
|
|
2835
|
+
self.periodic_advertising_syncs.append(sync)
|
|
2836
|
+
|
|
2837
|
+
# Check if any sync should be started
|
|
2838
|
+
await self._update_periodic_advertising_syncs()
|
|
2839
|
+
|
|
2840
|
+
return sync
|
|
2841
|
+
|
|
2842
|
+
async def _update_periodic_advertising_syncs(self) -> None:
|
|
2843
|
+
# Check if there's already a pending sync
|
|
2844
|
+
if any(
|
|
2845
|
+
sync.state == PeriodicAdvertisingSync.State.PENDING
|
|
2846
|
+
for sync in self.periodic_advertising_syncs
|
|
2847
|
+
):
|
|
2848
|
+
logger.debug("at least one sync pending, nothing to update yet")
|
|
2849
|
+
return
|
|
2850
|
+
|
|
2851
|
+
# Start the next sync that's waiting to be started
|
|
2852
|
+
if ready := next(
|
|
2853
|
+
(
|
|
2854
|
+
sync
|
|
2855
|
+
for sync in self.periodic_advertising_syncs
|
|
2856
|
+
if sync.state == PeriodicAdvertisingSync.State.INIT
|
|
2857
|
+
),
|
|
2858
|
+
None,
|
|
2859
|
+
):
|
|
2860
|
+
await ready.establish()
|
|
2861
|
+
return
|
|
2862
|
+
|
|
2863
|
+
@host_event_handler
|
|
2864
|
+
def on_periodic_advertising_sync_establishment(
|
|
2865
|
+
self,
|
|
2866
|
+
status: int,
|
|
2867
|
+
sync_handle: int,
|
|
2868
|
+
advertising_sid: int,
|
|
2869
|
+
advertiser_address: Address,
|
|
2870
|
+
advertiser_phy: int,
|
|
2871
|
+
periodic_advertising_interval: int,
|
|
2872
|
+
advertiser_clock_accuracy: int,
|
|
2873
|
+
) -> None:
|
|
2874
|
+
for periodic_advertising_sync in self.periodic_advertising_syncs:
|
|
2875
|
+
if (
|
|
2876
|
+
periodic_advertising_sync.advertiser_address == advertiser_address
|
|
2877
|
+
and periodic_advertising_sync.sid == advertising_sid
|
|
2878
|
+
):
|
|
2879
|
+
periodic_advertising_sync.on_establishment(
|
|
2880
|
+
status,
|
|
2881
|
+
sync_handle,
|
|
2882
|
+
advertiser_phy,
|
|
2883
|
+
periodic_advertising_interval,
|
|
2884
|
+
advertiser_clock_accuracy,
|
|
2885
|
+
)
|
|
2886
|
+
|
|
2887
|
+
AsyncRunner.spawn(self._update_periodic_advertising_syncs())
|
|
2888
|
+
|
|
2889
|
+
return
|
|
2890
|
+
|
|
2891
|
+
logger.warning(
|
|
2892
|
+
"periodic advertising sync establishment for unknown address/sid"
|
|
2893
|
+
)
|
|
2894
|
+
|
|
2895
|
+
@host_event_handler
|
|
2896
|
+
@with_periodic_advertising_sync_from_handle
|
|
2897
|
+
def on_periodic_advertising_sync_loss(
|
|
2898
|
+
self, periodic_advertising_sync: PeriodicAdvertisingSync
|
|
2899
|
+
):
|
|
2900
|
+
periodic_advertising_sync.on_loss()
|
|
2901
|
+
|
|
2902
|
+
@host_event_handler
|
|
2903
|
+
@with_periodic_advertising_sync_from_handle
|
|
2904
|
+
def on_periodic_advertising_report(
|
|
2905
|
+
self,
|
|
2906
|
+
periodic_advertising_sync: PeriodicAdvertisingSync,
|
|
2907
|
+
report: HCI_LE_Periodic_Advertising_Report_Event,
|
|
2908
|
+
):
|
|
2909
|
+
periodic_advertising_sync.on_periodic_advertising_report(report)
|
|
2910
|
+
|
|
2911
|
+
@host_event_handler
|
|
2912
|
+
@with_periodic_advertising_sync_from_handle
|
|
2913
|
+
def on_biginfo_advertising_report(
|
|
2914
|
+
self,
|
|
2915
|
+
periodic_advertising_sync: PeriodicAdvertisingSync,
|
|
2916
|
+
report: HCI_LE_BIGInfo_Advertising_Report_Event,
|
|
2917
|
+
):
|
|
2918
|
+
periodic_advertising_sync.on_biginfo_advertising_report(report)
|
|
2919
|
+
|
|
2371
2920
|
async def start_discovery(self, auto_restart: bool = True) -> None:
|
|
2372
2921
|
await self.send_command(
|
|
2373
2922
|
HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE),
|
|
@@ -2463,23 +3012,52 @@ class Device(CompositeEventEmitter):
|
|
|
2463
3012
|
] = None,
|
|
2464
3013
|
own_address_type: int = OwnAddressType.RANDOM,
|
|
2465
3014
|
timeout: Optional[float] = DEVICE_DEFAULT_CONNECT_TIMEOUT,
|
|
3015
|
+
always_resolve: bool = False,
|
|
2466
3016
|
) -> Connection:
|
|
2467
3017
|
'''
|
|
2468
3018
|
Request a connection to a peer.
|
|
2469
|
-
|
|
3019
|
+
|
|
3020
|
+
When the transport is BLE, this method cannot be called if there is already a
|
|
2470
3021
|
pending connection.
|
|
2471
3022
|
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
3023
|
+
Args:
|
|
3024
|
+
peer_address:
|
|
3025
|
+
Address or name of the device to connect to.
|
|
3026
|
+
If a string is passed:
|
|
3027
|
+
If the string is an address followed by a `@` suffix, the `always_resolve`
|
|
3028
|
+
argument is implicitly set to True, so the connection is made to the
|
|
3029
|
+
address after resolution.
|
|
3030
|
+
If the string is any other address, the connection is made to that
|
|
3031
|
+
address (with or without address resolution, depending on the
|
|
3032
|
+
`always_resolve` argument).
|
|
3033
|
+
For any other string, a scan for devices using that string as their name
|
|
3034
|
+
is initiated, and a connection to the first matching device's address
|
|
3035
|
+
is made. In that case, `always_resolve` is ignored.
|
|
3036
|
+
|
|
3037
|
+
connection_parameters_preferences:
|
|
3038
|
+
(BLE only, ignored for BR/EDR)
|
|
3039
|
+
* None: use the 1M PHY with default parameters
|
|
3040
|
+
* map: each entry has a PHY as key and a ConnectionParametersPreferences
|
|
3041
|
+
object as value
|
|
2476
3042
|
|
|
2477
|
-
|
|
3043
|
+
own_address_type:
|
|
3044
|
+
(BLE only, ignored for BR/EDR)
|
|
3045
|
+
OwnAddressType.RANDOM to use this device's random address, or
|
|
3046
|
+
OwnAddressType.PUBLIC to use this device's public address.
|
|
3047
|
+
|
|
3048
|
+
timeout:
|
|
3049
|
+
Maximum time to wait for a connection to be established, in seconds.
|
|
3050
|
+
Pass None for an unlimited time.
|
|
3051
|
+
|
|
3052
|
+
always_resolve:
|
|
3053
|
+
(BLE only, ignored for BR/EDR)
|
|
3054
|
+
If True, always initiate a scan, resolving addresses, and connect to the
|
|
3055
|
+
address that resolves to `peer_address`.
|
|
2478
3056
|
'''
|
|
2479
3057
|
|
|
2480
3058
|
# Check parameters
|
|
2481
3059
|
if transport not in (BT_LE_TRANSPORT, BT_BR_EDR_TRANSPORT):
|
|
2482
|
-
raise
|
|
3060
|
+
raise InvalidArgumentError('invalid transport')
|
|
2483
3061
|
|
|
2484
3062
|
# Adjust the transport automatically if we need to
|
|
2485
3063
|
if transport == BT_LE_TRANSPORT and not self.le_enabled:
|
|
@@ -2493,11 +3071,19 @@ class Device(CompositeEventEmitter):
|
|
|
2493
3071
|
|
|
2494
3072
|
if isinstance(peer_address, str):
|
|
2495
3073
|
try:
|
|
2496
|
-
|
|
2497
|
-
peer_address
|
|
2498
|
-
|
|
2499
|
-
|
|
3074
|
+
if transport == BT_LE_TRANSPORT and peer_address.endswith('@'):
|
|
3075
|
+
peer_address = Address.from_string_for_transport(
|
|
3076
|
+
peer_address[:-1], transport
|
|
3077
|
+
)
|
|
3078
|
+
always_resolve = True
|
|
3079
|
+
logger.debug('forcing address resolution')
|
|
3080
|
+
else:
|
|
3081
|
+
peer_address = Address.from_string_for_transport(
|
|
3082
|
+
peer_address, transport
|
|
3083
|
+
)
|
|
3084
|
+
except (InvalidArgumentError, ValueError):
|
|
2500
3085
|
# If the address is not parsable, assume it is a name instead
|
|
3086
|
+
always_resolve = False
|
|
2501
3087
|
logger.debug('looking for peer by name')
|
|
2502
3088
|
peer_address = await self.find_peer_by_name(
|
|
2503
3089
|
peer_address, transport
|
|
@@ -2508,10 +3094,16 @@ class Device(CompositeEventEmitter):
|
|
|
2508
3094
|
transport == BT_BR_EDR_TRANSPORT
|
|
2509
3095
|
and peer_address.address_type != Address.PUBLIC_DEVICE_ADDRESS
|
|
2510
3096
|
):
|
|
2511
|
-
raise
|
|
3097
|
+
raise InvalidArgumentError('BR/EDR addresses must be PUBLIC')
|
|
2512
3098
|
|
|
2513
3099
|
assert isinstance(peer_address, Address)
|
|
2514
3100
|
|
|
3101
|
+
if transport == BT_LE_TRANSPORT and always_resolve:
|
|
3102
|
+
logger.debug('resolving address')
|
|
3103
|
+
peer_address = await self.find_peer_by_identity_address(
|
|
3104
|
+
peer_address
|
|
3105
|
+
) # TODO: timeout
|
|
3106
|
+
|
|
2515
3107
|
def on_connection(connection):
|
|
2516
3108
|
if transport == BT_LE_TRANSPORT or (
|
|
2517
3109
|
# match BR/EDR connection event against peer address
|
|
@@ -2559,7 +3151,7 @@ class Device(CompositeEventEmitter):
|
|
|
2559
3151
|
)
|
|
2560
3152
|
)
|
|
2561
3153
|
if not phys:
|
|
2562
|
-
raise
|
|
3154
|
+
raise InvalidArgumentError('at least one supported PHY needed')
|
|
2563
3155
|
|
|
2564
3156
|
phy_count = len(phys)
|
|
2565
3157
|
initiating_phys = phy_list_to_bits(phys)
|
|
@@ -2631,7 +3223,7 @@ class Device(CompositeEventEmitter):
|
|
|
2631
3223
|
)
|
|
2632
3224
|
else:
|
|
2633
3225
|
if HCI_LE_1M_PHY not in connection_parameters_preferences:
|
|
2634
|
-
raise
|
|
3226
|
+
raise InvalidArgumentError('1M PHY preferences required')
|
|
2635
3227
|
|
|
2636
3228
|
prefs = connection_parameters_preferences[HCI_LE_1M_PHY]
|
|
2637
3229
|
result = await self.send_command(
|
|
@@ -2731,7 +3323,7 @@ class Device(CompositeEventEmitter):
|
|
|
2731
3323
|
if isinstance(peer_address, str):
|
|
2732
3324
|
try:
|
|
2733
3325
|
peer_address = Address(peer_address)
|
|
2734
|
-
except
|
|
3326
|
+
except InvalidArgumentError:
|
|
2735
3327
|
# If the address is not parsable, assume it is a name instead
|
|
2736
3328
|
logger.debug('looking for peer by name')
|
|
2737
3329
|
peer_address = await self.find_peer_by_name(
|
|
@@ -2741,7 +3333,7 @@ class Device(CompositeEventEmitter):
|
|
|
2741
3333
|
assert isinstance(peer_address, Address)
|
|
2742
3334
|
|
|
2743
3335
|
if peer_address == Address.NIL:
|
|
2744
|
-
raise
|
|
3336
|
+
raise InvalidArgumentError('accept on nil address')
|
|
2745
3337
|
|
|
2746
3338
|
# Create a future so that we can wait for the request
|
|
2747
3339
|
pending_request_fut = asyncio.get_running_loop().create_future()
|
|
@@ -2854,7 +3446,7 @@ class Device(CompositeEventEmitter):
|
|
|
2854
3446
|
if isinstance(peer_address, str):
|
|
2855
3447
|
try:
|
|
2856
3448
|
peer_address = Address(peer_address)
|
|
2857
|
-
except
|
|
3449
|
+
except InvalidArgumentError:
|
|
2858
3450
|
# If the address is not parsable, assume it is a name instead
|
|
2859
3451
|
logger.debug('looking for peer by name')
|
|
2860
3452
|
peer_address = await self.find_peer_by_name(
|
|
@@ -2897,10 +3489,10 @@ class Device(CompositeEventEmitter):
|
|
|
2897
3489
|
|
|
2898
3490
|
async def set_data_length(self, connection, tx_octets, tx_time) -> None:
|
|
2899
3491
|
if tx_octets < 0x001B or tx_octets > 0x00FB:
|
|
2900
|
-
raise
|
|
3492
|
+
raise InvalidArgumentError('tx_octets must be between 0x001B and 0x00FB')
|
|
2901
3493
|
|
|
2902
3494
|
if tx_time < 0x0148 or tx_time > 0x4290:
|
|
2903
|
-
raise
|
|
3495
|
+
raise InvalidArgumentError('tx_time must be between 0x0148 and 0x4290')
|
|
2904
3496
|
|
|
2905
3497
|
return await self.send_command(
|
|
2906
3498
|
HCI_LE_Set_Data_Length_Command(
|
|
@@ -3013,15 +3605,26 @@ class Device(CompositeEventEmitter):
|
|
|
3013
3605
|
check_result=True,
|
|
3014
3606
|
)
|
|
3015
3607
|
|
|
3608
|
+
async def transfer_periodic_sync(
|
|
3609
|
+
self, connection: Connection, sync_handle: int, service_data: int = 0
|
|
3610
|
+
) -> None:
|
|
3611
|
+
return await self.send_command(
|
|
3612
|
+
HCI_LE_Periodic_Advertising_Sync_Transfer_Command(
|
|
3613
|
+
connection_handle=connection.handle,
|
|
3614
|
+
service_data=service_data,
|
|
3615
|
+
sync_handle=sync_handle,
|
|
3616
|
+
),
|
|
3617
|
+
check_result=True,
|
|
3618
|
+
)
|
|
3619
|
+
|
|
3016
3620
|
async def find_peer_by_name(self, name, transport=BT_LE_TRANSPORT):
|
|
3017
3621
|
"""
|
|
3018
|
-
Scan for a peer with a
|
|
3622
|
+
Scan for a peer with a given name and return its address.
|
|
3019
3623
|
"""
|
|
3020
3624
|
|
|
3021
3625
|
# Create a future to wait for an address to be found
|
|
3022
3626
|
peer_address = asyncio.get_running_loop().create_future()
|
|
3023
3627
|
|
|
3024
|
-
# Scan/inquire with event handlers to handle scan/inquiry results
|
|
3025
3628
|
def on_peer_found(address, ad_data):
|
|
3026
3629
|
local_name = ad_data.get(AdvertisingData.COMPLETE_LOCAL_NAME, raw=True)
|
|
3027
3630
|
if local_name is None:
|
|
@@ -3030,13 +3633,13 @@ class Device(CompositeEventEmitter):
|
|
|
3030
3633
|
if local_name.decode('utf-8') == name:
|
|
3031
3634
|
peer_address.set_result(address)
|
|
3032
3635
|
|
|
3033
|
-
|
|
3636
|
+
listener = None
|
|
3034
3637
|
was_scanning = self.scanning
|
|
3035
3638
|
was_discovering = self.discovering
|
|
3036
3639
|
try:
|
|
3037
3640
|
if transport == BT_LE_TRANSPORT:
|
|
3038
3641
|
event_name = 'advertisement'
|
|
3039
|
-
|
|
3642
|
+
listener = self.on(
|
|
3040
3643
|
event_name,
|
|
3041
3644
|
lambda advertisement: on_peer_found(
|
|
3042
3645
|
advertisement.address, advertisement.data
|
|
@@ -3048,7 +3651,7 @@ class Device(CompositeEventEmitter):
|
|
|
3048
3651
|
|
|
3049
3652
|
elif transport == BT_BR_EDR_TRANSPORT:
|
|
3050
3653
|
event_name = 'inquiry_result'
|
|
3051
|
-
|
|
3654
|
+
listener = self.on(
|
|
3052
3655
|
event_name,
|
|
3053
3656
|
lambda address, class_of_device, eir_data, rssi: on_peer_found(
|
|
3054
3657
|
address, eir_data
|
|
@@ -3062,21 +3665,67 @@ class Device(CompositeEventEmitter):
|
|
|
3062
3665
|
|
|
3063
3666
|
return await self.abort_on('flush', peer_address)
|
|
3064
3667
|
finally:
|
|
3065
|
-
if
|
|
3066
|
-
self.remove_listener(event_name,
|
|
3668
|
+
if listener is not None:
|
|
3669
|
+
self.remove_listener(event_name, listener)
|
|
3067
3670
|
|
|
3068
3671
|
if transport == BT_LE_TRANSPORT and not was_scanning:
|
|
3069
3672
|
await self.stop_scanning()
|
|
3070
3673
|
elif transport == BT_BR_EDR_TRANSPORT and not was_discovering:
|
|
3071
3674
|
await self.stop_discovery()
|
|
3072
3675
|
|
|
3676
|
+
async def find_peer_by_identity_address(self, identity_address: Address) -> Address:
|
|
3677
|
+
"""
|
|
3678
|
+
Scan for a peer with a resolvable address that can be resolved to a given
|
|
3679
|
+
identity address.
|
|
3680
|
+
"""
|
|
3681
|
+
|
|
3682
|
+
# Create a future to wait for an address to be found
|
|
3683
|
+
peer_address = asyncio.get_running_loop().create_future()
|
|
3684
|
+
|
|
3685
|
+
def on_peer_found(address, _):
|
|
3686
|
+
if address == identity_address:
|
|
3687
|
+
if not peer_address.done():
|
|
3688
|
+
logger.debug(f'*** Matching public address found for {address}')
|
|
3689
|
+
peer_address.set_result(address)
|
|
3690
|
+
return
|
|
3691
|
+
|
|
3692
|
+
if address.is_resolvable:
|
|
3693
|
+
resolved_address = self.address_resolver.resolve(address)
|
|
3694
|
+
if resolved_address == identity_address:
|
|
3695
|
+
if not peer_address.done():
|
|
3696
|
+
logger.debug(f'*** Matching identity found for {address}')
|
|
3697
|
+
peer_address.set_result(address)
|
|
3698
|
+
return
|
|
3699
|
+
|
|
3700
|
+
was_scanning = self.scanning
|
|
3701
|
+
event_name = 'advertisement'
|
|
3702
|
+
listener = None
|
|
3703
|
+
try:
|
|
3704
|
+
listener = self.on(
|
|
3705
|
+
event_name,
|
|
3706
|
+
lambda advertisement: on_peer_found(
|
|
3707
|
+
advertisement.address, advertisement.data
|
|
3708
|
+
),
|
|
3709
|
+
)
|
|
3710
|
+
|
|
3711
|
+
if not self.scanning:
|
|
3712
|
+
await self.start_scanning(filter_duplicates=True)
|
|
3713
|
+
|
|
3714
|
+
return await self.abort_on('flush', peer_address)
|
|
3715
|
+
finally:
|
|
3716
|
+
if listener is not None:
|
|
3717
|
+
self.remove_listener(event_name, listener)
|
|
3718
|
+
|
|
3719
|
+
if not was_scanning:
|
|
3720
|
+
await self.stop_scanning()
|
|
3721
|
+
|
|
3073
3722
|
@property
|
|
3074
|
-
def pairing_config_factory(self) -> Callable[[Connection], PairingConfig]:
|
|
3723
|
+
def pairing_config_factory(self) -> Callable[[Connection], pairing.PairingConfig]:
|
|
3075
3724
|
return self.smp_manager.pairing_config_factory
|
|
3076
3725
|
|
|
3077
3726
|
@pairing_config_factory.setter
|
|
3078
3727
|
def pairing_config_factory(
|
|
3079
|
-
self, pairing_config_factory: Callable[[Connection], PairingConfig]
|
|
3728
|
+
self, pairing_config_factory: Callable[[Connection], pairing.PairingConfig]
|
|
3080
3729
|
) -> None:
|
|
3081
3730
|
self.smp_manager.pairing_config_factory = pairing_config_factory
|
|
3082
3731
|
|
|
@@ -3175,7 +3824,7 @@ class Device(CompositeEventEmitter):
|
|
|
3175
3824
|
|
|
3176
3825
|
async def encrypt(self, connection, enable=True):
|
|
3177
3826
|
if not enable and connection.transport == BT_LE_TRANSPORT:
|
|
3178
|
-
raise
|
|
3827
|
+
raise InvalidArgumentError('`enable` parameter is classic only.')
|
|
3179
3828
|
|
|
3180
3829
|
# Set up event handlers
|
|
3181
3830
|
pending_encryption = asyncio.get_running_loop().create_future()
|
|
@@ -3194,11 +3843,12 @@ class Device(CompositeEventEmitter):
|
|
|
3194
3843
|
if connection.transport == BT_LE_TRANSPORT:
|
|
3195
3844
|
# Look for a key in the key store
|
|
3196
3845
|
if self.keystore is None:
|
|
3197
|
-
raise
|
|
3846
|
+
raise InvalidOperationError('no key store')
|
|
3198
3847
|
|
|
3848
|
+
logger.debug(f'Looking up key for {connection.peer_address}')
|
|
3199
3849
|
keys = await self.keystore.get(str(connection.peer_address))
|
|
3200
3850
|
if keys is None:
|
|
3201
|
-
raise
|
|
3851
|
+
raise InvalidOperationError('keys not found in key store')
|
|
3202
3852
|
|
|
3203
3853
|
if keys.ltk is not None:
|
|
3204
3854
|
ltk = keys.ltk.value
|
|
@@ -3209,7 +3859,7 @@ class Device(CompositeEventEmitter):
|
|
|
3209
3859
|
rand = keys.ltk_central.rand
|
|
3210
3860
|
ediv = keys.ltk_central.ediv
|
|
3211
3861
|
else:
|
|
3212
|
-
raise
|
|
3862
|
+
raise InvalidOperationError('no LTK found for peer')
|
|
3213
3863
|
|
|
3214
3864
|
if connection.role != HCI_CENTRAL_ROLE:
|
|
3215
3865
|
raise InvalidStateError('only centrals can start encryption')
|
|
@@ -3484,7 +4134,7 @@ class Device(CompositeEventEmitter):
|
|
|
3484
4134
|
return cis_link
|
|
3485
4135
|
|
|
3486
4136
|
# Mypy believes this is reachable when context is an ExitStack.
|
|
3487
|
-
raise
|
|
4137
|
+
raise UnreachableError()
|
|
3488
4138
|
|
|
3489
4139
|
# [LE only]
|
|
3490
4140
|
@experimental('Only for testing.')
|
|
@@ -3605,18 +4255,38 @@ class Device(CompositeEventEmitter):
|
|
|
3605
4255
|
)
|
|
3606
4256
|
return
|
|
3607
4257
|
|
|
3608
|
-
if
|
|
3609
|
-
|
|
4258
|
+
if connection := self.lookup_connection(connection_handle):
|
|
4259
|
+
# We have already received the connection complete event.
|
|
4260
|
+
self._complete_le_extended_advertising_connection(
|
|
4261
|
+
connection, advertising_set
|
|
4262
|
+
)
|
|
3610
4263
|
return
|
|
3611
4264
|
|
|
4265
|
+
# Associate the connection handle with the advertising set, the connection
|
|
4266
|
+
# will complete later.
|
|
4267
|
+
logger.debug(
|
|
4268
|
+
f'the connection with handle {connection_handle:04X} will complete later'
|
|
4269
|
+
)
|
|
4270
|
+
self.connecting_extended_advertising_sets[connection_handle] = advertising_set
|
|
4271
|
+
|
|
4272
|
+
def _complete_le_extended_advertising_connection(
|
|
4273
|
+
self, connection: Connection, advertising_set: AdvertisingSet
|
|
4274
|
+
) -> None:
|
|
3612
4275
|
# Update the connection address.
|
|
3613
4276
|
connection.self_address = (
|
|
3614
4277
|
advertising_set.random_address
|
|
3615
|
-
if advertising_set.
|
|
4278
|
+
if advertising_set.random_address is not None
|
|
4279
|
+
and advertising_set.advertising_parameters.own_address_type
|
|
3616
4280
|
in (OwnAddressType.RANDOM, OwnAddressType.RESOLVABLE_OR_RANDOM)
|
|
3617
4281
|
else self.public_address
|
|
3618
4282
|
)
|
|
3619
4283
|
|
|
4284
|
+
if advertising_set.advertising_parameters.own_address_type in (
|
|
4285
|
+
OwnAddressType.RANDOM,
|
|
4286
|
+
OwnAddressType.PUBLIC,
|
|
4287
|
+
):
|
|
4288
|
+
connection.self_resolvable_address = None
|
|
4289
|
+
|
|
3620
4290
|
# Setup auto-restart of the advertising set if needed.
|
|
3621
4291
|
if advertising_set.auto_restart:
|
|
3622
4292
|
connection.once(
|
|
@@ -3652,12 +4322,23 @@ class Device(CompositeEventEmitter):
|
|
|
3652
4322
|
@host_event_handler
|
|
3653
4323
|
def on_connection(
|
|
3654
4324
|
self,
|
|
3655
|
-
connection_handle,
|
|
3656
|
-
transport,
|
|
3657
|
-
peer_address,
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
4325
|
+
connection_handle: int,
|
|
4326
|
+
transport: int,
|
|
4327
|
+
peer_address: Address,
|
|
4328
|
+
self_resolvable_address: Optional[Address],
|
|
4329
|
+
peer_resolvable_address: Optional[Address],
|
|
4330
|
+
role: int,
|
|
4331
|
+
connection_parameters: ConnectionParameters,
|
|
4332
|
+
) -> None:
|
|
4333
|
+
# Convert all-zeros addresses into None.
|
|
4334
|
+
if self_resolvable_address == Address.ANY_RANDOM:
|
|
4335
|
+
self_resolvable_address = None
|
|
4336
|
+
if (
|
|
4337
|
+
peer_resolvable_address == Address.ANY_RANDOM
|
|
4338
|
+
or not peer_address.is_resolved
|
|
4339
|
+
):
|
|
4340
|
+
peer_resolvable_address = None
|
|
4341
|
+
|
|
3661
4342
|
logger.debug(
|
|
3662
4343
|
f'*** Connection: [0x{connection_handle:04X}] '
|
|
3663
4344
|
f'{peer_address} {"" if role is None else HCI_Constant.role_name(role)}'
|
|
@@ -3678,17 +4359,18 @@ class Device(CompositeEventEmitter):
|
|
|
3678
4359
|
|
|
3679
4360
|
return
|
|
3680
4361
|
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
4362
|
+
if peer_resolvable_address is None:
|
|
4363
|
+
# Resolve the peer address if we can
|
|
4364
|
+
if self.address_resolver:
|
|
4365
|
+
if peer_address.is_resolvable:
|
|
4366
|
+
resolved_address = self.address_resolver.resolve(peer_address)
|
|
4367
|
+
if resolved_address is not None:
|
|
4368
|
+
logger.debug(f'*** Address resolved as {resolved_address}')
|
|
4369
|
+
peer_resolvable_address = peer_address
|
|
4370
|
+
peer_address = resolved_address
|
|
3690
4371
|
|
|
3691
4372
|
self_address = None
|
|
4373
|
+
own_address_type: Optional[int] = None
|
|
3692
4374
|
if role == HCI_CENTRAL_ROLE:
|
|
3693
4375
|
own_address_type = self.connect_own_address_type
|
|
3694
4376
|
assert own_address_type is not None
|
|
@@ -3717,12 +4399,18 @@ class Device(CompositeEventEmitter):
|
|
|
3717
4399
|
else self.random_address
|
|
3718
4400
|
)
|
|
3719
4401
|
|
|
4402
|
+
# Some controllers may return local resolvable address even not using address
|
|
4403
|
+
# generation offloading. Ignore the value to prevent SMP failure.
|
|
4404
|
+
if own_address_type in (OwnAddressType.RANDOM, OwnAddressType.PUBLIC):
|
|
4405
|
+
self_resolvable_address = None
|
|
4406
|
+
|
|
3720
4407
|
# Create a connection.
|
|
3721
4408
|
connection = Connection(
|
|
3722
4409
|
self,
|
|
3723
4410
|
connection_handle,
|
|
3724
4411
|
transport,
|
|
3725
4412
|
self_address,
|
|
4413
|
+
self_resolvable_address,
|
|
3726
4414
|
peer_address,
|
|
3727
4415
|
peer_resolvable_address,
|
|
3728
4416
|
role,
|
|
@@ -3733,9 +4421,10 @@ class Device(CompositeEventEmitter):
|
|
|
3733
4421
|
|
|
3734
4422
|
if role == HCI_PERIPHERAL_ROLE and self.legacy_advertiser:
|
|
3735
4423
|
if self.legacy_advertiser.auto_restart:
|
|
4424
|
+
advertiser = self.legacy_advertiser
|
|
3736
4425
|
connection.once(
|
|
3737
4426
|
'disconnection',
|
|
3738
|
-
lambda _: self.abort_on('flush',
|
|
4427
|
+
lambda _: self.abort_on('flush', advertiser.start()),
|
|
3739
4428
|
)
|
|
3740
4429
|
else:
|
|
3741
4430
|
self.legacy_advertiser = None
|
|
@@ -3743,6 +4432,16 @@ class Device(CompositeEventEmitter):
|
|
|
3743
4432
|
if role == HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising:
|
|
3744
4433
|
# We can emit now, we have all the info we need
|
|
3745
4434
|
self._emit_le_connection(connection)
|
|
4435
|
+
return
|
|
4436
|
+
|
|
4437
|
+
if role == HCI_PERIPHERAL_ROLE and self.supports_le_extended_advertising:
|
|
4438
|
+
if advertising_set := self.connecting_extended_advertising_sets.pop(
|
|
4439
|
+
connection_handle, None
|
|
4440
|
+
):
|
|
4441
|
+
# We have already received the advertising set termination event.
|
|
4442
|
+
self._complete_le_extended_advertising_connection(
|
|
4443
|
+
connection, advertising_set
|
|
4444
|
+
)
|
|
3746
4445
|
|
|
3747
4446
|
@host_event_handler
|
|
3748
4447
|
def on_connection_failure(self, transport, peer_address, error_code):
|
|
@@ -3948,7 +4647,7 @@ class Device(CompositeEventEmitter):
|
|
|
3948
4647
|
return await pairing_config.delegate.confirm(auto=True)
|
|
3949
4648
|
|
|
3950
4649
|
async def na() -> bool:
|
|
3951
|
-
|
|
4650
|
+
raise UnreachableError()
|
|
3952
4651
|
|
|
3953
4652
|
# See Bluetooth spec @ Vol 3, Part C 5.2.2.6
|
|
3954
4653
|
methods = {
|
|
@@ -4409,5 +5108,6 @@ class Device(CompositeEventEmitter):
|
|
|
4409
5108
|
return (
|
|
4410
5109
|
f'Device(name="{self.name}", '
|
|
4411
5110
|
f'random_address="{self.random_address}", '
|
|
4412
|
-
f'public_address="{self.public_address}"
|
|
5111
|
+
f'public_address="{self.public_address}", '
|
|
5112
|
+
f'static_address="{self.static_address}")'
|
|
4413
5113
|
)
|