bumble 0.0.193__py3-none-any.whl → 0.0.195__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 +407 -0
- bumble/apps/bench.py +146 -35
- bumble/apps/controller_info.py +3 -3
- bumble/apps/rfcomm_bridge.py +511 -0
- bumble/core.py +689 -115
- bumble/device.py +441 -12
- bumble/hci.py +250 -12
- bumble/host.py +25 -0
- bumble/l2cap.py +5 -2
- bumble/pandora/host.py +3 -2
- bumble/profiles/bap.py +101 -5
- bumble/profiles/le_audio.py +49 -0
- bumble/profiles/pbp.py +46 -0
- bumble/rfcomm.py +158 -61
- bumble/sdp.py +1 -1
- {bumble-0.0.193.dist-info → bumble-0.0.195.dist-info}/METADATA +1 -1
- {bumble-0.0.193.dist-info → bumble-0.0.195.dist-info}/RECORD +22 -18
- {bumble-0.0.193.dist-info → bumble-0.0.195.dist-info}/entry_points.txt +1 -0
- {bumble-0.0.193.dist-info → bumble-0.0.195.dist-info}/LICENSE +0 -0
- {bumble-0.0.193.dist-info → bumble-0.0.195.dist-info}/WHEEL +0 -0
- {bumble-0.0.193.dist-info → bumble-0.0.195.dist-info}/top_level.txt +0 -0
bumble/device.py
CHANGED
|
@@ -16,22 +16,21 @@
|
|
|
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 json
|
|
31
|
+
import logging
|
|
32
|
+
import secrets
|
|
33
|
+
import sys
|
|
35
34
|
from typing import (
|
|
36
35
|
Any,
|
|
37
36
|
Callable,
|
|
@@ -81,6 +80,7 @@ from .hci import (
|
|
|
81
80
|
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
|
82
81
|
HCI_MITM_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
|
83
82
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
|
83
|
+
HCI_OPERATION_CANCELLED_BY_HOST_ERROR,
|
|
84
84
|
HCI_R2_PAGE_SCAN_REPETITION_MODE,
|
|
85
85
|
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
86
86
|
HCI_SUCCESS,
|
|
@@ -102,11 +102,16 @@ from .hci import (
|
|
|
102
102
|
HCI_LE_Accept_CIS_Request_Command,
|
|
103
103
|
HCI_LE_Add_Device_To_Resolving_List_Command,
|
|
104
104
|
HCI_LE_Advertising_Report_Event,
|
|
105
|
+
HCI_LE_BIGInfo_Advertising_Report_Event,
|
|
105
106
|
HCI_LE_Clear_Resolving_List_Command,
|
|
106
107
|
HCI_LE_Connection_Update_Command,
|
|
107
108
|
HCI_LE_Create_Connection_Cancel_Command,
|
|
108
109
|
HCI_LE_Create_Connection_Command,
|
|
109
110
|
HCI_LE_Create_CIS_Command,
|
|
111
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Command,
|
|
112
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command,
|
|
113
|
+
HCI_LE_Periodic_Advertising_Report_Event,
|
|
114
|
+
HCI_LE_Periodic_Advertising_Terminate_Sync_Command,
|
|
110
115
|
HCI_LE_Enable_Encryption_Command,
|
|
111
116
|
HCI_LE_Extended_Advertising_Report_Event,
|
|
112
117
|
HCI_LE_Extended_Create_Connection_Command,
|
|
@@ -248,6 +253,8 @@ DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS = l2cap.L2CAP_LE_CREDIT_BASED_CONN
|
|
|
248
253
|
DEVICE_DEFAULT_ADVERTISING_TX_POWER = (
|
|
249
254
|
HCI_LE_Set_Extended_Advertising_Parameters_Command.TX_POWER_NO_PREFERENCE
|
|
250
255
|
)
|
|
256
|
+
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP = 0
|
|
257
|
+
DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT = 5.0
|
|
251
258
|
|
|
252
259
|
# fmt: on
|
|
253
260
|
# pylint: enable=line-too-long
|
|
@@ -552,6 +559,70 @@ class AdvertisingEventProperties:
|
|
|
552
559
|
)
|
|
553
560
|
|
|
554
561
|
|
|
562
|
+
# -----------------------------------------------------------------------------
|
|
563
|
+
@dataclass
|
|
564
|
+
class PeriodicAdvertisement:
|
|
565
|
+
address: Address
|
|
566
|
+
sid: int
|
|
567
|
+
tx_power: int = (
|
|
568
|
+
HCI_LE_Periodic_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
|
|
569
|
+
)
|
|
570
|
+
rssi: int = HCI_LE_Periodic_Advertising_Report_Event.RSSI_NOT_AVAILABLE
|
|
571
|
+
is_truncated: bool = False
|
|
572
|
+
data_bytes: bytes = b''
|
|
573
|
+
|
|
574
|
+
# Constants
|
|
575
|
+
TX_POWER_NOT_AVAILABLE: ClassVar[int] = (
|
|
576
|
+
HCI_LE_Periodic_Advertising_Report_Event.TX_POWER_INFORMATION_NOT_AVAILABLE
|
|
577
|
+
)
|
|
578
|
+
RSSI_NOT_AVAILABLE: ClassVar[int] = (
|
|
579
|
+
HCI_LE_Periodic_Advertising_Report_Event.RSSI_NOT_AVAILABLE
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
def __post_init__(self) -> None:
|
|
583
|
+
self.data = (
|
|
584
|
+
None if self.is_truncated else AdvertisingData.from_bytes(self.data_bytes)
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# -----------------------------------------------------------------------------
|
|
589
|
+
@dataclass
|
|
590
|
+
class BIGInfoAdvertisement:
|
|
591
|
+
address: Address
|
|
592
|
+
sid: int
|
|
593
|
+
num_bis: int
|
|
594
|
+
nse: int
|
|
595
|
+
iso_interval: int
|
|
596
|
+
bn: int
|
|
597
|
+
pto: int
|
|
598
|
+
irc: int
|
|
599
|
+
max_pdu: int
|
|
600
|
+
sdu_interval: int
|
|
601
|
+
max_sdu: int
|
|
602
|
+
phy: Phy
|
|
603
|
+
framed: bool
|
|
604
|
+
encrypted: bool
|
|
605
|
+
|
|
606
|
+
@classmethod
|
|
607
|
+
def from_report(cls, address: Address, sid: int, report) -> Self:
|
|
608
|
+
return cls(
|
|
609
|
+
address,
|
|
610
|
+
sid,
|
|
611
|
+
report.num_bis,
|
|
612
|
+
report.nse,
|
|
613
|
+
report.iso_interval,
|
|
614
|
+
report.bn,
|
|
615
|
+
report.pto,
|
|
616
|
+
report.irc,
|
|
617
|
+
report.max_pdu,
|
|
618
|
+
report.sdu_interval,
|
|
619
|
+
report.max_sdu,
|
|
620
|
+
Phy(report.phy),
|
|
621
|
+
report.framing != 0,
|
|
622
|
+
report.encryption != 0,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
555
626
|
# -----------------------------------------------------------------------------
|
|
556
627
|
# TODO: replace with typing.TypeAlias when the code base is all Python >= 3.10
|
|
557
628
|
AdvertisingChannelMap = HCI_LE_Set_Extended_Advertising_Parameters_Command.ChannelMap
|
|
@@ -795,6 +866,201 @@ class AdvertisingSet(EventEmitter):
|
|
|
795
866
|
self.emit('termination', status)
|
|
796
867
|
|
|
797
868
|
|
|
869
|
+
# -----------------------------------------------------------------------------
|
|
870
|
+
class PeriodicAdvertisingSync(EventEmitter):
|
|
871
|
+
class State(Enum):
|
|
872
|
+
INIT = 0
|
|
873
|
+
PENDING = 1
|
|
874
|
+
ESTABLISHED = 2
|
|
875
|
+
CANCELLED = 3
|
|
876
|
+
ERROR = 4
|
|
877
|
+
LOST = 5
|
|
878
|
+
TERMINATED = 6
|
|
879
|
+
|
|
880
|
+
_state: State
|
|
881
|
+
sync_handle: Optional[int]
|
|
882
|
+
advertiser_address: Address
|
|
883
|
+
sid: int
|
|
884
|
+
skip: int
|
|
885
|
+
sync_timeout: float # Sync timeout, in seconds
|
|
886
|
+
filter_duplicates: bool
|
|
887
|
+
status: int
|
|
888
|
+
advertiser_phy: int
|
|
889
|
+
periodic_advertising_interval: int
|
|
890
|
+
advertiser_clock_accuracy: int
|
|
891
|
+
|
|
892
|
+
def __init__(
|
|
893
|
+
self,
|
|
894
|
+
device: Device,
|
|
895
|
+
advertiser_address: Address,
|
|
896
|
+
sid: int,
|
|
897
|
+
skip: int,
|
|
898
|
+
sync_timeout: float,
|
|
899
|
+
filter_duplicates: bool,
|
|
900
|
+
) -> None:
|
|
901
|
+
super().__init__()
|
|
902
|
+
self._state = self.State.INIT
|
|
903
|
+
self.sync_handle = None
|
|
904
|
+
self.device = device
|
|
905
|
+
self.advertiser_address = advertiser_address
|
|
906
|
+
self.sid = sid
|
|
907
|
+
self.skip = skip
|
|
908
|
+
self.sync_timeout = sync_timeout
|
|
909
|
+
self.filter_duplicates = filter_duplicates
|
|
910
|
+
self.status = HCI_SUCCESS
|
|
911
|
+
self.advertiser_phy = 0
|
|
912
|
+
self.periodic_advertising_interval = 0
|
|
913
|
+
self.advertiser_clock_accuracy = 0
|
|
914
|
+
self.data_accumulator = b''
|
|
915
|
+
|
|
916
|
+
@property
|
|
917
|
+
def state(self) -> State:
|
|
918
|
+
return self._state
|
|
919
|
+
|
|
920
|
+
@state.setter
|
|
921
|
+
def state(self, state: State) -> None:
|
|
922
|
+
logger.debug(f'{self} -> {state.name}')
|
|
923
|
+
self._state = state
|
|
924
|
+
self.emit('state_change')
|
|
925
|
+
|
|
926
|
+
async def establish(self) -> None:
|
|
927
|
+
if self.state != self.State.INIT:
|
|
928
|
+
raise InvalidStateError('sync not in init state')
|
|
929
|
+
|
|
930
|
+
options = HCI_LE_Periodic_Advertising_Create_Sync_Command.Options(0)
|
|
931
|
+
if self.filter_duplicates:
|
|
932
|
+
options |= (
|
|
933
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Command.Options.DUPLICATE_FILTERING_INITIALLY_ENABLED
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
response = await self.device.send_command(
|
|
937
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Command(
|
|
938
|
+
options=options,
|
|
939
|
+
advertising_sid=self.sid,
|
|
940
|
+
advertiser_address_type=self.advertiser_address.address_type,
|
|
941
|
+
advertiser_address=self.advertiser_address,
|
|
942
|
+
skip=self.skip,
|
|
943
|
+
sync_timeout=int(self.sync_timeout * 100),
|
|
944
|
+
sync_cte_type=0,
|
|
945
|
+
)
|
|
946
|
+
)
|
|
947
|
+
if response.status != HCI_Command_Status_Event.PENDING:
|
|
948
|
+
raise HCI_StatusError(response)
|
|
949
|
+
|
|
950
|
+
self.state = self.State.PENDING
|
|
951
|
+
|
|
952
|
+
async def terminate(self) -> None:
|
|
953
|
+
if self.state in (self.State.INIT, self.State.CANCELLED, self.State.TERMINATED):
|
|
954
|
+
return
|
|
955
|
+
|
|
956
|
+
if self.state == self.State.PENDING:
|
|
957
|
+
self.state = self.State.CANCELLED
|
|
958
|
+
response = await self.device.send_command(
|
|
959
|
+
HCI_LE_Periodic_Advertising_Create_Sync_Cancel_Command(),
|
|
960
|
+
)
|
|
961
|
+
if response.status == HCI_SUCCESS:
|
|
962
|
+
if self in self.device.periodic_advertising_syncs:
|
|
963
|
+
self.device.periodic_advertising_syncs.remove(self)
|
|
964
|
+
return
|
|
965
|
+
|
|
966
|
+
if self.state in (self.State.ESTABLISHED, self.State.ERROR, self.State.LOST):
|
|
967
|
+
self.state = self.State.TERMINATED
|
|
968
|
+
await self.device.send_command(
|
|
969
|
+
HCI_LE_Periodic_Advertising_Terminate_Sync_Command(
|
|
970
|
+
sync_handle=self.sync_handle
|
|
971
|
+
)
|
|
972
|
+
)
|
|
973
|
+
self.device.periodic_advertising_syncs.remove(self)
|
|
974
|
+
|
|
975
|
+
def on_establishment(
|
|
976
|
+
self,
|
|
977
|
+
status,
|
|
978
|
+
sync_handle,
|
|
979
|
+
advertiser_phy,
|
|
980
|
+
periodic_advertising_interval,
|
|
981
|
+
advertiser_clock_accuracy,
|
|
982
|
+
) -> None:
|
|
983
|
+
self.status = status
|
|
984
|
+
|
|
985
|
+
if self.state == self.State.CANCELLED:
|
|
986
|
+
# Somehow, we receive an established event after trying to cancel, most
|
|
987
|
+
# likely because the cancel command was sent too late, when the sync was
|
|
988
|
+
# already established, but before the established event was sent.
|
|
989
|
+
# We need to automatically terminate.
|
|
990
|
+
logger.debug(
|
|
991
|
+
"received established event for cancelled sync, will terminate"
|
|
992
|
+
)
|
|
993
|
+
self.state = self.State.ESTABLISHED
|
|
994
|
+
AsyncRunner.spawn(self.terminate())
|
|
995
|
+
return
|
|
996
|
+
|
|
997
|
+
if status == HCI_SUCCESS:
|
|
998
|
+
self.sync_handle = sync_handle
|
|
999
|
+
self.advertiser_phy = advertiser_phy
|
|
1000
|
+
self.periodic_advertising_interval = periodic_advertising_interval
|
|
1001
|
+
self.advertiser_clock_accuracy = advertiser_clock_accuracy
|
|
1002
|
+
self.state = self.State.ESTABLISHED
|
|
1003
|
+
self.emit('establishment')
|
|
1004
|
+
return
|
|
1005
|
+
|
|
1006
|
+
# We don't need to keep a reference anymore
|
|
1007
|
+
if self in self.device.periodic_advertising_syncs:
|
|
1008
|
+
self.device.periodic_advertising_syncs.remove(self)
|
|
1009
|
+
|
|
1010
|
+
if status == HCI_OPERATION_CANCELLED_BY_HOST_ERROR:
|
|
1011
|
+
self.state = self.State.CANCELLED
|
|
1012
|
+
self.emit('cancellation')
|
|
1013
|
+
return
|
|
1014
|
+
|
|
1015
|
+
self.state = self.State.ERROR
|
|
1016
|
+
self.emit('error')
|
|
1017
|
+
|
|
1018
|
+
def on_loss(self):
|
|
1019
|
+
self.state = self.State.LOST
|
|
1020
|
+
self.emit('loss')
|
|
1021
|
+
|
|
1022
|
+
def on_periodic_advertising_report(self, report) -> None:
|
|
1023
|
+
self.data_accumulator += report.data
|
|
1024
|
+
if (
|
|
1025
|
+
report.data_status
|
|
1026
|
+
== HCI_LE_Periodic_Advertising_Report_Event.DataStatus.DATA_INCOMPLETE_MORE_TO_COME
|
|
1027
|
+
):
|
|
1028
|
+
return
|
|
1029
|
+
|
|
1030
|
+
self.emit(
|
|
1031
|
+
'periodic_advertisement',
|
|
1032
|
+
PeriodicAdvertisement(
|
|
1033
|
+
self.advertiser_address,
|
|
1034
|
+
self.sid,
|
|
1035
|
+
report.tx_power,
|
|
1036
|
+
report.rssi,
|
|
1037
|
+
is_truncated=(
|
|
1038
|
+
report.data_status
|
|
1039
|
+
== HCI_LE_Periodic_Advertising_Report_Event.DataStatus.DATA_INCOMPLETE_TRUNCATED_NO_MORE_TO_COME
|
|
1040
|
+
),
|
|
1041
|
+
data_bytes=self.data_accumulator,
|
|
1042
|
+
),
|
|
1043
|
+
)
|
|
1044
|
+
self.data_accumulator = b''
|
|
1045
|
+
|
|
1046
|
+
def on_biginfo_advertising_report(self, report) -> None:
|
|
1047
|
+
self.emit(
|
|
1048
|
+
'biginfo_advertisement',
|
|
1049
|
+
BIGInfoAdvertisement.from_report(self.advertiser_address, self.sid, report),
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
def __str__(self) -> str:
|
|
1053
|
+
return (
|
|
1054
|
+
'PeriodicAdvertisingSync('
|
|
1055
|
+
f'state={self.state.name}, '
|
|
1056
|
+
f'sync_handle={self.sync_handle}, '
|
|
1057
|
+
f'sid={self.sid}, '
|
|
1058
|
+
f'skip={self.skip}, '
|
|
1059
|
+
f'filter_duplicates={self.filter_duplicates}'
|
|
1060
|
+
')'
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
|
|
798
1064
|
# -----------------------------------------------------------------------------
|
|
799
1065
|
class LePhyOptions:
|
|
800
1066
|
# Coded PHY preference
|
|
@@ -1409,6 +1675,20 @@ def try_with_connection_from_address(function):
|
|
|
1409
1675
|
return wrapper
|
|
1410
1676
|
|
|
1411
1677
|
|
|
1678
|
+
# Decorator that converts the first argument from a sync handle to a periodic
|
|
1679
|
+
# advertising sync object
|
|
1680
|
+
def with_periodic_advertising_sync_from_handle(function):
|
|
1681
|
+
@functools.wraps(function)
|
|
1682
|
+
def wrapper(self, sync_handle, *args, **kwargs):
|
|
1683
|
+
if (sync := self.lookup_periodic_advertising_sync(sync_handle)) is None:
|
|
1684
|
+
raise ValueError(
|
|
1685
|
+
f'no periodic advertising sync for handle: 0x{sync_handle:04x}'
|
|
1686
|
+
)
|
|
1687
|
+
return function(self, sync, *args, **kwargs)
|
|
1688
|
+
|
|
1689
|
+
return wrapper
|
|
1690
|
+
|
|
1691
|
+
|
|
1412
1692
|
# Decorator that adds a method to the list of event handlers for host events.
|
|
1413
1693
|
# This assumes that the method name starts with `on_`
|
|
1414
1694
|
def host_event_handler(function):
|
|
@@ -1439,6 +1719,7 @@ class Device(CompositeEventEmitter):
|
|
|
1439
1719
|
Address, List[asyncio.Future[Union[Connection, Tuple[Address, int, int]]]]
|
|
1440
1720
|
]
|
|
1441
1721
|
advertisement_accumulators: Dict[Address, AdvertisementDataAccumulator]
|
|
1722
|
+
periodic_advertising_syncs: List[PeriodicAdvertisingSync]
|
|
1442
1723
|
config: DeviceConfiguration
|
|
1443
1724
|
legacy_advertiser: Optional[LegacyAdvertiser]
|
|
1444
1725
|
sco_links: Dict[int, ScoLink]
|
|
@@ -1524,6 +1805,7 @@ class Device(CompositeEventEmitter):
|
|
|
1524
1805
|
[l2cap.L2CAP_Information_Request.EXTENDED_FEATURE_FIXED_CHANNELS]
|
|
1525
1806
|
)
|
|
1526
1807
|
self.advertisement_accumulators = {} # Accumulators, by address
|
|
1808
|
+
self.periodic_advertising_syncs = []
|
|
1527
1809
|
self.scanning = False
|
|
1528
1810
|
self.scanning_is_passive = False
|
|
1529
1811
|
self.discovering = False
|
|
@@ -1574,6 +1856,7 @@ class Device(CompositeEventEmitter):
|
|
|
1574
1856
|
|
|
1575
1857
|
# Extended advertising.
|
|
1576
1858
|
self.extended_advertising_sets: Dict[int, AdvertisingSet] = {}
|
|
1859
|
+
self.connecting_extended_advertising_sets: Dict[int, AdvertisingSet] = {}
|
|
1577
1860
|
|
|
1578
1861
|
# Legacy advertising.
|
|
1579
1862
|
# The advertising and scan response data, as well as the advertising interval
|
|
@@ -1706,6 +1989,18 @@ class Device(CompositeEventEmitter):
|
|
|
1706
1989
|
|
|
1707
1990
|
return None
|
|
1708
1991
|
|
|
1992
|
+
def lookup_periodic_advertising_sync(
|
|
1993
|
+
self, sync_handle: int
|
|
1994
|
+
) -> Optional[PeriodicAdvertisingSync]:
|
|
1995
|
+
return next(
|
|
1996
|
+
(
|
|
1997
|
+
sync
|
|
1998
|
+
for sync in self.periodic_advertising_syncs
|
|
1999
|
+
if sync.sync_handle == sync_handle
|
|
2000
|
+
),
|
|
2001
|
+
None,
|
|
2002
|
+
)
|
|
2003
|
+
|
|
1709
2004
|
@deprecated("Please use create_l2cap_server()")
|
|
1710
2005
|
def register_l2cap_server(self, psm, server) -> int:
|
|
1711
2006
|
return self.l2cap_channel_manager.register_server(psm, server)
|
|
@@ -2368,6 +2663,116 @@ class Device(CompositeEventEmitter):
|
|
|
2368
2663
|
if advertisement := accumulator.update(report):
|
|
2369
2664
|
self.emit('advertisement', advertisement)
|
|
2370
2665
|
|
|
2666
|
+
async def create_periodic_advertising_sync(
|
|
2667
|
+
self,
|
|
2668
|
+
advertiser_address: Address,
|
|
2669
|
+
sid: int,
|
|
2670
|
+
skip: int = DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_SKIP,
|
|
2671
|
+
sync_timeout: float = DEVICE_DEFAULT_PERIODIC_ADVERTISING_SYNC_TIMEOUT,
|
|
2672
|
+
filter_duplicates: bool = False,
|
|
2673
|
+
) -> PeriodicAdvertisingSync:
|
|
2674
|
+
# Check that there isn't already an equivalent entry
|
|
2675
|
+
if any(
|
|
2676
|
+
sync.advertiser_address == advertiser_address and sync.sid == sid
|
|
2677
|
+
for sync in self.periodic_advertising_syncs
|
|
2678
|
+
):
|
|
2679
|
+
raise ValueError("equivalent entry already created")
|
|
2680
|
+
|
|
2681
|
+
# Create a new entry
|
|
2682
|
+
sync = PeriodicAdvertisingSync(
|
|
2683
|
+
device=self,
|
|
2684
|
+
advertiser_address=advertiser_address,
|
|
2685
|
+
sid=sid,
|
|
2686
|
+
skip=skip,
|
|
2687
|
+
sync_timeout=sync_timeout,
|
|
2688
|
+
filter_duplicates=filter_duplicates,
|
|
2689
|
+
)
|
|
2690
|
+
|
|
2691
|
+
self.periodic_advertising_syncs.append(sync)
|
|
2692
|
+
|
|
2693
|
+
# Check if any sync should be started
|
|
2694
|
+
await self._update_periodic_advertising_syncs()
|
|
2695
|
+
|
|
2696
|
+
return sync
|
|
2697
|
+
|
|
2698
|
+
async def _update_periodic_advertising_syncs(self) -> None:
|
|
2699
|
+
# Check if there's already a pending sync
|
|
2700
|
+
if any(
|
|
2701
|
+
sync.state == PeriodicAdvertisingSync.State.PENDING
|
|
2702
|
+
for sync in self.periodic_advertising_syncs
|
|
2703
|
+
):
|
|
2704
|
+
logger.debug("at least one sync pending, nothing to update yet")
|
|
2705
|
+
return
|
|
2706
|
+
|
|
2707
|
+
# Start the next sync that's waiting to be started
|
|
2708
|
+
if ready := next(
|
|
2709
|
+
(
|
|
2710
|
+
sync
|
|
2711
|
+
for sync in self.periodic_advertising_syncs
|
|
2712
|
+
if sync.state == PeriodicAdvertisingSync.State.INIT
|
|
2713
|
+
),
|
|
2714
|
+
None,
|
|
2715
|
+
):
|
|
2716
|
+
await ready.establish()
|
|
2717
|
+
return
|
|
2718
|
+
|
|
2719
|
+
@host_event_handler
|
|
2720
|
+
def on_periodic_advertising_sync_establishment(
|
|
2721
|
+
self,
|
|
2722
|
+
status: int,
|
|
2723
|
+
sync_handle: int,
|
|
2724
|
+
advertising_sid: int,
|
|
2725
|
+
advertiser_address: Address,
|
|
2726
|
+
advertiser_phy: int,
|
|
2727
|
+
periodic_advertising_interval: int,
|
|
2728
|
+
advertiser_clock_accuracy: int,
|
|
2729
|
+
) -> None:
|
|
2730
|
+
for periodic_advertising_sync in self.periodic_advertising_syncs:
|
|
2731
|
+
if (
|
|
2732
|
+
periodic_advertising_sync.advertiser_address == advertiser_address
|
|
2733
|
+
and periodic_advertising_sync.sid == advertising_sid
|
|
2734
|
+
):
|
|
2735
|
+
periodic_advertising_sync.on_establishment(
|
|
2736
|
+
status,
|
|
2737
|
+
sync_handle,
|
|
2738
|
+
advertiser_phy,
|
|
2739
|
+
periodic_advertising_interval,
|
|
2740
|
+
advertiser_clock_accuracy,
|
|
2741
|
+
)
|
|
2742
|
+
|
|
2743
|
+
AsyncRunner.spawn(self._update_periodic_advertising_syncs())
|
|
2744
|
+
|
|
2745
|
+
return
|
|
2746
|
+
|
|
2747
|
+
logger.warning(
|
|
2748
|
+
"periodic advertising sync establishment for unknown address/sid"
|
|
2749
|
+
)
|
|
2750
|
+
|
|
2751
|
+
@host_event_handler
|
|
2752
|
+
@with_periodic_advertising_sync_from_handle
|
|
2753
|
+
def on_periodic_advertising_sync_loss(
|
|
2754
|
+
self, periodic_advertising_sync: PeriodicAdvertisingSync
|
|
2755
|
+
):
|
|
2756
|
+
periodic_advertising_sync.on_loss()
|
|
2757
|
+
|
|
2758
|
+
@host_event_handler
|
|
2759
|
+
@with_periodic_advertising_sync_from_handle
|
|
2760
|
+
def on_periodic_advertising_report(
|
|
2761
|
+
self,
|
|
2762
|
+
periodic_advertising_sync: PeriodicAdvertisingSync,
|
|
2763
|
+
report: HCI_LE_Periodic_Advertising_Report_Event,
|
|
2764
|
+
):
|
|
2765
|
+
periodic_advertising_sync.on_periodic_advertising_report(report)
|
|
2766
|
+
|
|
2767
|
+
@host_event_handler
|
|
2768
|
+
@with_periodic_advertising_sync_from_handle
|
|
2769
|
+
def on_biginfo_advertising_report(
|
|
2770
|
+
self,
|
|
2771
|
+
periodic_advertising_sync: PeriodicAdvertisingSync,
|
|
2772
|
+
report: HCI_LE_BIGInfo_Advertising_Report_Event,
|
|
2773
|
+
):
|
|
2774
|
+
periodic_advertising_sync.on_biginfo_advertising_report(report)
|
|
2775
|
+
|
|
2371
2776
|
async def start_discovery(self, auto_restart: bool = True) -> None:
|
|
2372
2777
|
await self.send_command(
|
|
2373
2778
|
HCI_Write_Inquiry_Mode_Command(inquiry_mode=HCI_EXTENDED_INQUIRY_MODE),
|
|
@@ -3605,14 +4010,28 @@ class Device(CompositeEventEmitter):
|
|
|
3605
4010
|
)
|
|
3606
4011
|
return
|
|
3607
4012
|
|
|
3608
|
-
if
|
|
3609
|
-
|
|
4013
|
+
if connection := self.lookup_connection(connection_handle):
|
|
4014
|
+
# We have already received the connection complete event.
|
|
4015
|
+
self._complete_le_extended_advertising_connection(
|
|
4016
|
+
connection, advertising_set
|
|
4017
|
+
)
|
|
3610
4018
|
return
|
|
3611
4019
|
|
|
4020
|
+
# Associate the connection handle with the advertising set, the connection
|
|
4021
|
+
# will complete later.
|
|
4022
|
+
logger.debug(
|
|
4023
|
+
f'the connection with handle {connection_handle:04X} will complete later'
|
|
4024
|
+
)
|
|
4025
|
+
self.connecting_extended_advertising_sets[connection_handle] = advertising_set
|
|
4026
|
+
|
|
4027
|
+
def _complete_le_extended_advertising_connection(
|
|
4028
|
+
self, connection: Connection, advertising_set: AdvertisingSet
|
|
4029
|
+
) -> None:
|
|
3612
4030
|
# Update the connection address.
|
|
3613
4031
|
connection.self_address = (
|
|
3614
4032
|
advertising_set.random_address
|
|
3615
|
-
if advertising_set.
|
|
4033
|
+
if advertising_set.random_address is not None
|
|
4034
|
+
and advertising_set.advertising_parameters.own_address_type
|
|
3616
4035
|
in (OwnAddressType.RANDOM, OwnAddressType.RESOLVABLE_OR_RANDOM)
|
|
3617
4036
|
else self.public_address
|
|
3618
4037
|
)
|
|
@@ -3743,6 +4162,16 @@ class Device(CompositeEventEmitter):
|
|
|
3743
4162
|
if role == HCI_CENTRAL_ROLE or not self.supports_le_extended_advertising:
|
|
3744
4163
|
# We can emit now, we have all the info we need
|
|
3745
4164
|
self._emit_le_connection(connection)
|
|
4165
|
+
return
|
|
4166
|
+
|
|
4167
|
+
if role == HCI_PERIPHERAL_ROLE and self.supports_le_extended_advertising:
|
|
4168
|
+
if advertising_set := self.connecting_extended_advertising_sets.pop(
|
|
4169
|
+
connection_handle, None
|
|
4170
|
+
):
|
|
4171
|
+
# We have already received the advertising set termination event.
|
|
4172
|
+
self._complete_le_extended_advertising_connection(
|
|
4173
|
+
connection, advertising_set
|
|
4174
|
+
)
|
|
3746
4175
|
|
|
3747
4176
|
@host_event_handler
|
|
3748
4177
|
def on_connection_failure(self, transport, peer_address, error_code):
|