bumble 0.0.208__py3-none-any.whl → 0.0.209__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/att.py +10 -2
- bumble/controller.py +7 -8
- bumble/core.py +5 -4
- bumble/device.py +25 -26
- bumble/gatt.py +16 -0
- bumble/hci.py +76 -71
- bumble/host.py +8 -3
- bumble/l2cap.py +2 -2
- bumble/link.py +2 -2
- bumble/pandora/host.py +12 -11
- bumble/pandora/security.py +2 -3
- bumble/pandora/utils.py +2 -2
- bumble/profiles/ancs.py +514 -0
- bumble/smp.py +3 -3
- bumble/transport/usb.py +1 -3
- {bumble-0.0.208.dist-info → bumble-0.0.209.dist-info}/METADATA +1 -1
- {bumble-0.0.208.dist-info → bumble-0.0.209.dist-info}/RECORD +22 -21
- {bumble-0.0.208.dist-info → bumble-0.0.209.dist-info}/WHEEL +1 -1
- {bumble-0.0.208.dist-info → bumble-0.0.209.dist-info}/LICENSE +0 -0
- {bumble-0.0.208.dist-info → bumble-0.0.209.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.208.dist-info → bumble-0.0.209.dist-info}/top_level.txt +0 -0
bumble/pandora/host.py
CHANGED
|
@@ -25,7 +25,6 @@ from .config import Config
|
|
|
25
25
|
from bumble.core import (
|
|
26
26
|
BT_BR_EDR_TRANSPORT,
|
|
27
27
|
BT_LE_TRANSPORT,
|
|
28
|
-
BT_PERIPHERAL_ROLE,
|
|
29
28
|
UUID,
|
|
30
29
|
AdvertisingData,
|
|
31
30
|
Appearance,
|
|
@@ -47,6 +46,8 @@ from bumble.hci import (
|
|
|
47
46
|
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
|
48
47
|
Address,
|
|
49
48
|
Phy,
|
|
49
|
+
Role,
|
|
50
|
+
OwnAddressType,
|
|
50
51
|
)
|
|
51
52
|
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
|
52
53
|
from google.protobuf import empty_pb2 # pytype: disable=pyi-error
|
|
@@ -114,11 +115,11 @@ SECONDARY_PHY_TO_BUMBLE_PHY_MAP: Dict[SecondaryPhy, Phy] = {
|
|
|
114
115
|
SECONDARY_CODED: Phy.LE_CODED,
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType,
|
|
118
|
-
host_pb2.PUBLIC:
|
|
119
|
-
host_pb2.RANDOM:
|
|
120
|
-
host_pb2.RESOLVABLE_OR_PUBLIC:
|
|
121
|
-
host_pb2.RESOLVABLE_OR_RANDOM:
|
|
118
|
+
OWN_ADDRESS_MAP: Dict[host_pb2.OwnAddressType, OwnAddressType] = {
|
|
119
|
+
host_pb2.PUBLIC: OwnAddressType.PUBLIC,
|
|
120
|
+
host_pb2.RANDOM: OwnAddressType.RANDOM,
|
|
121
|
+
host_pb2.RESOLVABLE_OR_PUBLIC: OwnAddressType.RESOLVABLE_OR_PUBLIC,
|
|
122
|
+
host_pb2.RESOLVABLE_OR_RANDOM: OwnAddressType.RESOLVABLE_OR_RANDOM,
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
|
|
@@ -250,7 +251,7 @@ class HostService(HostServicer):
|
|
|
250
251
|
connection = await self.device.connect(
|
|
251
252
|
address,
|
|
252
253
|
transport=BT_LE_TRANSPORT,
|
|
253
|
-
own_address_type=request.own_address_type,
|
|
254
|
+
own_address_type=OwnAddressType(request.own_address_type),
|
|
254
255
|
)
|
|
255
256
|
except ConnectionError as e:
|
|
256
257
|
if e.error_code == HCI_PAGE_TIMEOUT_ERROR:
|
|
@@ -378,7 +379,7 @@ class HostService(HostServicer):
|
|
|
378
379
|
def on_connection(connection: bumble.device.Connection) -> None:
|
|
379
380
|
if (
|
|
380
381
|
connection.transport == BT_LE_TRANSPORT
|
|
381
|
-
and connection.role ==
|
|
382
|
+
and connection.role == Role.PERIPHERAL
|
|
382
383
|
):
|
|
383
384
|
connections.put_nowait(connection)
|
|
384
385
|
|
|
@@ -496,7 +497,7 @@ class HostService(HostServicer):
|
|
|
496
497
|
def on_connection(connection: bumble.device.Connection) -> None:
|
|
497
498
|
if (
|
|
498
499
|
connection.transport == BT_LE_TRANSPORT
|
|
499
|
-
and connection.role ==
|
|
500
|
+
and connection.role == Role.PERIPHERAL
|
|
500
501
|
):
|
|
501
502
|
connections.put_nowait(connection)
|
|
502
503
|
|
|
@@ -509,7 +510,7 @@ class HostService(HostServicer):
|
|
|
509
510
|
await self.device.start_advertising(
|
|
510
511
|
target=target,
|
|
511
512
|
advertising_type=advertising_type,
|
|
512
|
-
own_address_type=request.own_address_type,
|
|
513
|
+
own_address_type=OwnAddressType(request.own_address_type),
|
|
513
514
|
)
|
|
514
515
|
|
|
515
516
|
if not request.connectable:
|
|
@@ -558,7 +559,7 @@ class HostService(HostServicer):
|
|
|
558
559
|
await self.device.start_scanning(
|
|
559
560
|
legacy=request.legacy,
|
|
560
561
|
active=not request.passive,
|
|
561
|
-
own_address_type=request.own_address_type,
|
|
562
|
+
own_address_type=OwnAddressType(request.own_address_type),
|
|
562
563
|
scan_interval=(
|
|
563
564
|
int(request.interval)
|
|
564
565
|
if request.interval
|
bumble/pandora/security.py
CHANGED
|
@@ -24,11 +24,10 @@ from bumble import hci
|
|
|
24
24
|
from bumble.core import (
|
|
25
25
|
BT_BR_EDR_TRANSPORT,
|
|
26
26
|
BT_LE_TRANSPORT,
|
|
27
|
-
BT_PERIPHERAL_ROLE,
|
|
28
27
|
ProtocolError,
|
|
29
28
|
)
|
|
30
29
|
from bumble.device import Connection as BumbleConnection, Device
|
|
31
|
-
from bumble.hci import HCI_Error
|
|
30
|
+
from bumble.hci import HCI_Error, Role
|
|
32
31
|
from bumble.utils import EventWatcher
|
|
33
32
|
from bumble.pairing import PairingConfig, PairingDelegate as BasePairingDelegate
|
|
34
33
|
from google.protobuf import any_pb2 # pytype: disable=pyi-error
|
|
@@ -318,7 +317,7 @@ class SecurityService(SecurityServicer):
|
|
|
318
317
|
|
|
319
318
|
if (
|
|
320
319
|
connection.transport == BT_LE_TRANSPORT
|
|
321
|
-
and connection.role ==
|
|
320
|
+
and connection.role == Role.PERIPHERAL
|
|
322
321
|
):
|
|
323
322
|
connection.request_pairing()
|
|
324
323
|
else:
|
bumble/pandora/utils.py
CHANGED
|
@@ -20,11 +20,11 @@ import inspect
|
|
|
20
20
|
import logging
|
|
21
21
|
|
|
22
22
|
from bumble.device import Device
|
|
23
|
-
from bumble.hci import Address
|
|
23
|
+
from bumble.hci import Address, AddressType
|
|
24
24
|
from google.protobuf.message import Message # pytype: disable=pyi-error
|
|
25
25
|
from typing import Any, Dict, Generator, MutableMapping, Optional, Tuple
|
|
26
26
|
|
|
27
|
-
ADDRESS_TYPES: Dict[str,
|
|
27
|
+
ADDRESS_TYPES: Dict[str, AddressType] = {
|
|
28
28
|
"public": Address.PUBLIC_DEVICE_ADDRESS,
|
|
29
29
|
"random": Address.RANDOM_DEVICE_ADDRESS,
|
|
30
30
|
"public_identity": Address.PUBLIC_IDENTITY_ADDRESS,
|
bumble/profiles/ancs.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Apple Notification Center Service (ANCS).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# -----------------------------------------------------------------------------
|
|
20
|
+
# Imports
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import asyncio
|
|
24
|
+
import dataclasses
|
|
25
|
+
import datetime
|
|
26
|
+
import enum
|
|
27
|
+
import logging
|
|
28
|
+
import struct
|
|
29
|
+
from typing import Optional, Sequence, Union
|
|
30
|
+
|
|
31
|
+
from pyee import EventEmitter
|
|
32
|
+
|
|
33
|
+
from bumble.att import ATT_Error
|
|
34
|
+
from bumble.device import Peer
|
|
35
|
+
from bumble.gatt import (
|
|
36
|
+
Characteristic,
|
|
37
|
+
GATT_ANCS_SERVICE,
|
|
38
|
+
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
|
39
|
+
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
|
|
40
|
+
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
|
|
41
|
+
TemplateService,
|
|
42
|
+
)
|
|
43
|
+
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
|
|
44
|
+
from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
|
|
45
|
+
from bumble.utils import OpenIntEnum
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# -----------------------------------------------------------------------------
|
|
49
|
+
# Constants
|
|
50
|
+
# -----------------------------------------------------------------------------
|
|
51
|
+
_DEFAULT_ATTRIBUTE_MAX_LENGTH = 65535
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# -----------------------------------------------------------------------------
|
|
55
|
+
# Logging
|
|
56
|
+
# -----------------------------------------------------------------------------
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# -----------------------------------------------------------------------------
|
|
61
|
+
# Protocol
|
|
62
|
+
# -----------------------------------------------------------------------------
|
|
63
|
+
class ActionId(OpenIntEnum):
|
|
64
|
+
POSITIVE = 0
|
|
65
|
+
NEGATIVE = 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class AppAttributeId(OpenIntEnum):
|
|
69
|
+
DISPLAY_NAME = 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CategoryId(OpenIntEnum):
|
|
73
|
+
OTHER = 0
|
|
74
|
+
INCOMING_CALL = 1
|
|
75
|
+
MISSED_CALL = 2
|
|
76
|
+
VOICEMAIL = 3
|
|
77
|
+
SOCIAL = 4
|
|
78
|
+
SCHEDULE = 5
|
|
79
|
+
EMAIL = 6
|
|
80
|
+
NEWS = 7
|
|
81
|
+
HEALTH_AND_FITNESS = 8
|
|
82
|
+
BUSINESS_AND_FINANCE = 9
|
|
83
|
+
LOCATION = 10
|
|
84
|
+
ENTERTAINMENT = 11
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CommandId(OpenIntEnum):
|
|
88
|
+
GET_NOTIFICATION_ATTRIBUTES = 0
|
|
89
|
+
GET_APP_ATTRIBUTES = 1
|
|
90
|
+
PERFORM_NOTIFICATION_ACTION = 2
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class EventId(OpenIntEnum):
|
|
94
|
+
NOTIFICATION_ADDED = 0
|
|
95
|
+
NOTIFICATION_MODIFIED = 1
|
|
96
|
+
NOTIFICATION_REMOVED = 2
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class EventFlags(enum.IntFlag):
|
|
100
|
+
SILENT = 1 << 0
|
|
101
|
+
IMPORTANT = 1 << 1
|
|
102
|
+
PRE_EXISTING = 1 << 2
|
|
103
|
+
POSITIVE_ACTION = 1 << 3
|
|
104
|
+
NEGATIVE_ACTION = 1 << 4
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class NotificationAttributeId(OpenIntEnum):
|
|
108
|
+
APP_IDENTIFIER = 0
|
|
109
|
+
TITLE = 1
|
|
110
|
+
SUBTITLE = 2
|
|
111
|
+
MESSAGE = 3
|
|
112
|
+
MESSAGE_SIZE = 4
|
|
113
|
+
DATE = 5
|
|
114
|
+
POSITIVE_ACTION_LABEL = 6
|
|
115
|
+
NEGATIVE_ACTION_LABEL = 7
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclasses.dataclass
|
|
119
|
+
class NotificationAttribute:
|
|
120
|
+
attribute_id: NotificationAttributeId
|
|
121
|
+
value: Union[str, int, datetime.datetime]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclasses.dataclass
|
|
125
|
+
class AppAttribute:
|
|
126
|
+
attribute_id: AppAttributeId
|
|
127
|
+
value: str
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclasses.dataclass
|
|
131
|
+
class Notification:
|
|
132
|
+
event_id: EventId
|
|
133
|
+
event_flags: EventFlags
|
|
134
|
+
category_id: CategoryId
|
|
135
|
+
category_count: int
|
|
136
|
+
notification_uid: int
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_bytes(cls, data: bytes) -> Notification:
|
|
140
|
+
return cls(
|
|
141
|
+
event_id=EventId(data[0]),
|
|
142
|
+
event_flags=EventFlags(data[1]),
|
|
143
|
+
category_id=CategoryId(data[2]),
|
|
144
|
+
category_count=data[3],
|
|
145
|
+
notification_uid=int.from_bytes(data[4:8], 'little'),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def __bytes__(self) -> bytes:
|
|
149
|
+
return struct.pack(
|
|
150
|
+
"<BBBBI",
|
|
151
|
+
self.event_id,
|
|
152
|
+
self.event_flags,
|
|
153
|
+
self.category_id,
|
|
154
|
+
self.category_count,
|
|
155
|
+
self.notification_uid,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ErrorCode(OpenIntEnum):
|
|
160
|
+
UNKNOWN_COMMAND = 0xA0
|
|
161
|
+
INVALID_COMMAND = 0xA1
|
|
162
|
+
INVALID_PARAMETER = 0xA2
|
|
163
|
+
ACTION_FAILED = 0xA3
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ProtocolError(Exception):
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class CommandError(Exception):
|
|
171
|
+
def __init__(self, error_code: ErrorCode) -> None:
|
|
172
|
+
self.error_code = error_code
|
|
173
|
+
|
|
174
|
+
def __str__(self) -> str:
|
|
175
|
+
return f"CommandError(error_code={self.error_code.name})"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# -----------------------------------------------------------------------------
|
|
179
|
+
# GATT Server-side
|
|
180
|
+
# -----------------------------------------------------------------------------
|
|
181
|
+
class Ancs(TemplateService):
|
|
182
|
+
UUID = GATT_ANCS_SERVICE
|
|
183
|
+
|
|
184
|
+
notification_source_characteristic: Characteristic
|
|
185
|
+
data_source_characteristic: Characteristic
|
|
186
|
+
control_point_characteristic: Characteristic
|
|
187
|
+
|
|
188
|
+
def __init__(self) -> None:
|
|
189
|
+
# TODO not the final implementation
|
|
190
|
+
self.notification_source_characteristic = Characteristic(
|
|
191
|
+
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
|
|
192
|
+
Characteristic.Properties.NOTIFY,
|
|
193
|
+
Characteristic.Permissions.READABLE,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# TODO not the final implementation
|
|
197
|
+
self.data_source_characteristic = Characteristic(
|
|
198
|
+
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
|
|
199
|
+
Characteristic.Properties.NOTIFY,
|
|
200
|
+
Characteristic.Permissions.READABLE,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# TODO not the final implementation
|
|
204
|
+
self.control_point_characteristic = Characteristic(
|
|
205
|
+
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
|
|
206
|
+
Characteristic.Properties.WRITE,
|
|
207
|
+
Characteristic.Permissions.WRITEABLE,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
super().__init__(
|
|
211
|
+
[
|
|
212
|
+
self.notification_source_characteristic,
|
|
213
|
+
self.data_source_characteristic,
|
|
214
|
+
self.control_point_characteristic,
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# -----------------------------------------------------------------------------
|
|
220
|
+
# GATT Client-side
|
|
221
|
+
# -----------------------------------------------------------------------------
|
|
222
|
+
class AncsProxy(ProfileServiceProxy):
|
|
223
|
+
SERVICE_CLASS = Ancs
|
|
224
|
+
|
|
225
|
+
notification_source: CharacteristicProxy[Notification]
|
|
226
|
+
data_source: CharacteristicProxy
|
|
227
|
+
control_point: CharacteristicProxy[bytes]
|
|
228
|
+
|
|
229
|
+
def __init__(self, service_proxy: ServiceProxy):
|
|
230
|
+
self.notification_source = SerializableCharacteristicProxyAdapter(
|
|
231
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
232
|
+
GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC
|
|
233
|
+
),
|
|
234
|
+
Notification,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
self.data_source = service_proxy.get_required_characteristic_by_uuid(
|
|
238
|
+
GATT_ANCS_DATA_SOURCE_CHARACTERISTIC
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
self.control_point = service_proxy.get_required_characteristic_by_uuid(
|
|
242
|
+
GATT_ANCS_CONTROL_POINT_CHARACTERISTIC
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class AncsClient(EventEmitter):
|
|
247
|
+
_expected_response_command_id: Optional[CommandId]
|
|
248
|
+
_expected_response_notification_uid: Optional[int]
|
|
249
|
+
_expected_response_app_identifier: Optional[str]
|
|
250
|
+
_expected_app_identifier: Optional[str]
|
|
251
|
+
_expected_response_tuples: int
|
|
252
|
+
_response_accumulator: bytes
|
|
253
|
+
|
|
254
|
+
def __init__(self, ancs_proxy: AncsProxy) -> None:
|
|
255
|
+
super().__init__()
|
|
256
|
+
self._ancs_proxy = ancs_proxy
|
|
257
|
+
self._command_semaphore = asyncio.Semaphore()
|
|
258
|
+
self._response: Optional[asyncio.Future] = None
|
|
259
|
+
self._reset_response()
|
|
260
|
+
self._started = False
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
async def for_peer(cls, peer: Peer) -> Optional[AncsClient]:
|
|
264
|
+
ancs_proxy = await peer.discover_service_and_create_proxy(AncsProxy)
|
|
265
|
+
if ancs_proxy is None:
|
|
266
|
+
return None
|
|
267
|
+
return cls(ancs_proxy)
|
|
268
|
+
|
|
269
|
+
async def start(self) -> None:
|
|
270
|
+
await self._ancs_proxy.notification_source.subscribe(self._on_notification)
|
|
271
|
+
await self._ancs_proxy.data_source.subscribe(self._on_data)
|
|
272
|
+
self._started = True
|
|
273
|
+
|
|
274
|
+
async def stop(self) -> None:
|
|
275
|
+
await self._ancs_proxy.notification_source.unsubscribe(self._on_notification)
|
|
276
|
+
await self._ancs_proxy.data_source.unsubscribe(self._on_data)
|
|
277
|
+
self._started = False
|
|
278
|
+
|
|
279
|
+
def _reset_response(self) -> None:
|
|
280
|
+
self._expected_response_command_id = None
|
|
281
|
+
self._expected_response_notification_uid = None
|
|
282
|
+
self._expected_app_identifier = None
|
|
283
|
+
self._expected_response_tuples = 0
|
|
284
|
+
self._response_accumulator = b""
|
|
285
|
+
|
|
286
|
+
def _on_notification(self, notification: Notification) -> None:
|
|
287
|
+
logger.debug(f"ANCS NOTIFICATION: {notification}")
|
|
288
|
+
self.emit("notification", notification)
|
|
289
|
+
|
|
290
|
+
def _on_data(self, data: bytes) -> None:
|
|
291
|
+
logger.debug(f"ANCS DATA: {data.hex()}")
|
|
292
|
+
|
|
293
|
+
if not self._response:
|
|
294
|
+
logger.warning("received unexpected data, discarding")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
self._response_accumulator += data
|
|
298
|
+
|
|
299
|
+
# Try to parse the accumulated data until we have all we need.
|
|
300
|
+
if not self._response_accumulator:
|
|
301
|
+
logger.warning("empty data from data source")
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
command_id = self._response_accumulator[0]
|
|
305
|
+
if command_id != self._expected_response_command_id:
|
|
306
|
+
logger.warning(
|
|
307
|
+
"unexpected response command id: "
|
|
308
|
+
f"expected {self._expected_response_command_id} "
|
|
309
|
+
f"but got {command_id}"
|
|
310
|
+
)
|
|
311
|
+
self._reset_response()
|
|
312
|
+
if not self._response.done():
|
|
313
|
+
self._response.set_exception(ProtocolError())
|
|
314
|
+
|
|
315
|
+
if len(self._response_accumulator) < 5:
|
|
316
|
+
# Not enough data yet.
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
attributes: list[Union[NotificationAttribute, AppAttribute]] = []
|
|
320
|
+
|
|
321
|
+
if command_id == CommandId.GET_NOTIFICATION_ATTRIBUTES:
|
|
322
|
+
(notification_uid,) = struct.unpack_from(
|
|
323
|
+
"<I", self._response_accumulator, 1
|
|
324
|
+
)
|
|
325
|
+
if notification_uid != self._expected_response_notification_uid:
|
|
326
|
+
logger.warning(
|
|
327
|
+
"unexpected response notification uid: "
|
|
328
|
+
f"expected {self._expected_response_notification_uid} "
|
|
329
|
+
f"but got {notification_uid}"
|
|
330
|
+
)
|
|
331
|
+
self._reset_response()
|
|
332
|
+
if not self._response.done():
|
|
333
|
+
self._response.set_exception(ProtocolError())
|
|
334
|
+
|
|
335
|
+
attribute_data = self._response_accumulator[5:]
|
|
336
|
+
while len(attribute_data) >= 3:
|
|
337
|
+
attribute_id, attribute_data_length = struct.unpack_from(
|
|
338
|
+
"<BH", attribute_data, 0
|
|
339
|
+
)
|
|
340
|
+
if len(attribute_data) < 3 + attribute_data_length:
|
|
341
|
+
return
|
|
342
|
+
str_value = attribute_data[3 : 3 + attribute_data_length].decode(
|
|
343
|
+
"utf-8"
|
|
344
|
+
)
|
|
345
|
+
value: Union[str, int, datetime.datetime]
|
|
346
|
+
if attribute_id == NotificationAttributeId.MESSAGE_SIZE:
|
|
347
|
+
value = int(str_value)
|
|
348
|
+
elif attribute_id == NotificationAttributeId.DATE:
|
|
349
|
+
year = int(str_value[:4])
|
|
350
|
+
month = int(str_value[4:6])
|
|
351
|
+
day = int(str_value[6:8])
|
|
352
|
+
hour = int(str_value[9:11])
|
|
353
|
+
minute = int(str_value[11:13])
|
|
354
|
+
second = int(str_value[13:15])
|
|
355
|
+
value = datetime.datetime(year, month, day, hour, minute, second)
|
|
356
|
+
else:
|
|
357
|
+
value = str_value
|
|
358
|
+
attributes.append(
|
|
359
|
+
NotificationAttribute(NotificationAttributeId(attribute_id), value)
|
|
360
|
+
)
|
|
361
|
+
attribute_data = attribute_data[3 + attribute_data_length :]
|
|
362
|
+
elif command_id == CommandId.GET_APP_ATTRIBUTES:
|
|
363
|
+
if 0 not in self._response_accumulator[1:]:
|
|
364
|
+
# No null-terminated string yet.
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
app_identifier_length = self._response_accumulator.find(0, 1) - 1
|
|
368
|
+
app_identifier = self._response_accumulator[
|
|
369
|
+
1 : 1 + app_identifier_length
|
|
370
|
+
].decode("utf-8")
|
|
371
|
+
if app_identifier != self._expected_response_app_identifier:
|
|
372
|
+
logger.warning(
|
|
373
|
+
"unexpected response app identifier: "
|
|
374
|
+
f"expected {self._expected_response_app_identifier} "
|
|
375
|
+
f"but got {app_identifier}"
|
|
376
|
+
)
|
|
377
|
+
self._reset_response()
|
|
378
|
+
if not self._response.done():
|
|
379
|
+
self._response.set_exception(ProtocolError())
|
|
380
|
+
|
|
381
|
+
attribute_data = self._response_accumulator[1 + app_identifier_length + 1 :]
|
|
382
|
+
while len(attribute_data) >= 3:
|
|
383
|
+
attribute_id, attribute_data_length = struct.unpack_from(
|
|
384
|
+
"<BH", attribute_data, 0
|
|
385
|
+
)
|
|
386
|
+
if len(attribute_data) < 3 + attribute_data_length:
|
|
387
|
+
return
|
|
388
|
+
attributes.append(
|
|
389
|
+
AppAttribute(
|
|
390
|
+
AppAttributeId(attribute_id),
|
|
391
|
+
attribute_data[3 : 3 + attribute_data_length].decode("utf-8"),
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
attribute_data = attribute_data[3 + attribute_data_length :]
|
|
395
|
+
else:
|
|
396
|
+
logger.warning(f"unexpected response command id {command_id}")
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
if len(attributes) < self._expected_response_tuples:
|
|
400
|
+
# We have not received all the tuples yet.
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
if not self._response.done():
|
|
404
|
+
self._response.set_result(attributes)
|
|
405
|
+
|
|
406
|
+
async def _send_command(self, command: bytes) -> None:
|
|
407
|
+
try:
|
|
408
|
+
await self._ancs_proxy.control_point.write_value(
|
|
409
|
+
command, with_response=True
|
|
410
|
+
)
|
|
411
|
+
except ATT_Error as error:
|
|
412
|
+
raise CommandError(error_code=ErrorCode(error.error_code)) from error
|
|
413
|
+
|
|
414
|
+
async def get_notification_attributes(
|
|
415
|
+
self,
|
|
416
|
+
notification_uid: int,
|
|
417
|
+
attributes: Sequence[
|
|
418
|
+
Union[NotificationAttributeId, tuple[NotificationAttributeId, int]]
|
|
419
|
+
],
|
|
420
|
+
) -> list[NotificationAttribute]:
|
|
421
|
+
if not self._started:
|
|
422
|
+
raise RuntimeError("client not started")
|
|
423
|
+
|
|
424
|
+
command = struct.pack(
|
|
425
|
+
"<BI", CommandId.GET_NOTIFICATION_ATTRIBUTES, notification_uid
|
|
426
|
+
)
|
|
427
|
+
for attribute in attributes:
|
|
428
|
+
attribute_max_length = 0
|
|
429
|
+
if isinstance(attribute, tuple):
|
|
430
|
+
attribute_id, attribute_max_length = attribute
|
|
431
|
+
if attribute_id not in (
|
|
432
|
+
NotificationAttributeId.TITLE,
|
|
433
|
+
NotificationAttributeId.SUBTITLE,
|
|
434
|
+
NotificationAttributeId.MESSAGE,
|
|
435
|
+
):
|
|
436
|
+
raise ValueError(
|
|
437
|
+
"this attribute does not allow specifying a max length"
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
attribute_id = attribute
|
|
441
|
+
if attribute_id in (
|
|
442
|
+
NotificationAttributeId.TITLE,
|
|
443
|
+
NotificationAttributeId.SUBTITLE,
|
|
444
|
+
NotificationAttributeId.MESSAGE,
|
|
445
|
+
):
|
|
446
|
+
attribute_max_length = _DEFAULT_ATTRIBUTE_MAX_LENGTH
|
|
447
|
+
|
|
448
|
+
if attribute_max_length:
|
|
449
|
+
command += struct.pack("<BH", attribute_id, attribute_max_length)
|
|
450
|
+
else:
|
|
451
|
+
command += struct.pack("B", attribute_id)
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
async with self._command_semaphore:
|
|
455
|
+
self._expected_response_notification_uid = notification_uid
|
|
456
|
+
self._expected_response_tuples = len(attributes)
|
|
457
|
+
self._expected_response_command_id = (
|
|
458
|
+
CommandId.GET_NOTIFICATION_ATTRIBUTES
|
|
459
|
+
)
|
|
460
|
+
self._response = asyncio.Future()
|
|
461
|
+
|
|
462
|
+
# Send the command.
|
|
463
|
+
await self._send_command(command)
|
|
464
|
+
|
|
465
|
+
# Wait for the response.
|
|
466
|
+
return await self._response
|
|
467
|
+
finally:
|
|
468
|
+
self._reset_response()
|
|
469
|
+
|
|
470
|
+
async def get_app_attributes(
|
|
471
|
+
self, app_identifier: str, attributes: Sequence[AppAttributeId]
|
|
472
|
+
) -> list[AppAttribute]:
|
|
473
|
+
if not self._started:
|
|
474
|
+
raise RuntimeError("client not started")
|
|
475
|
+
|
|
476
|
+
command = (
|
|
477
|
+
bytes([CommandId.GET_APP_ATTRIBUTES])
|
|
478
|
+
+ app_identifier.encode("utf-8")
|
|
479
|
+
+ b"\0"
|
|
480
|
+
)
|
|
481
|
+
for attribute_id in attributes:
|
|
482
|
+
command += struct.pack("B", attribute_id)
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
async with self._command_semaphore:
|
|
486
|
+
self._expected_response_app_identifier = app_identifier
|
|
487
|
+
self._expected_response_tuples = len(attributes)
|
|
488
|
+
self._expected_response_command_id = CommandId.GET_APP_ATTRIBUTES
|
|
489
|
+
self._response = asyncio.Future()
|
|
490
|
+
|
|
491
|
+
# Send the command.
|
|
492
|
+
await self._send_command(command)
|
|
493
|
+
|
|
494
|
+
# Wait for the response.
|
|
495
|
+
return await self._response
|
|
496
|
+
finally:
|
|
497
|
+
self._reset_response()
|
|
498
|
+
|
|
499
|
+
async def perform_action(self, notification_uid: int, action: ActionId) -> None:
|
|
500
|
+
if not self._started:
|
|
501
|
+
raise RuntimeError("client not started")
|
|
502
|
+
|
|
503
|
+
command = struct.pack(
|
|
504
|
+
"<BIB", CommandId.PERFORM_NOTIFICATION_ACTION, notification_uid, action
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
async with self._command_semaphore:
|
|
508
|
+
await self._send_command(command)
|
|
509
|
+
|
|
510
|
+
async def perform_positive_action(self, notification_uid: int) -> None:
|
|
511
|
+
return await self.perform_action(notification_uid, ActionId.POSITIVE)
|
|
512
|
+
|
|
513
|
+
async def perform_negative_action(self, notification_uid: int) -> None:
|
|
514
|
+
return await self.perform_action(notification_uid, ActionId.NEGATIVE)
|
bumble/smp.py
CHANGED
|
@@ -46,13 +46,13 @@ from pyee import EventEmitter
|
|
|
46
46
|
from .colors import color
|
|
47
47
|
from .hci import (
|
|
48
48
|
Address,
|
|
49
|
+
Role,
|
|
49
50
|
HCI_LE_Enable_Encryption_Command,
|
|
50
51
|
HCI_Object,
|
|
51
52
|
key_with_value,
|
|
52
53
|
)
|
|
53
54
|
from .core import (
|
|
54
55
|
BT_BR_EDR_TRANSPORT,
|
|
55
|
-
BT_CENTRAL_ROLE,
|
|
56
56
|
BT_LE_TRANSPORT,
|
|
57
57
|
AdvertisingData,
|
|
58
58
|
InvalidArgumentError,
|
|
@@ -1975,7 +1975,7 @@ class Manager(EventEmitter):
|
|
|
1975
1975
|
|
|
1976
1976
|
# Look for a session with this connection, and create one if none exists
|
|
1977
1977
|
if not (session := self.sessions.get(connection.handle)):
|
|
1978
|
-
if connection.role ==
|
|
1978
|
+
if connection.role == Role.CENTRAL:
|
|
1979
1979
|
logger.warning('Remote starts pairing as Peripheral!')
|
|
1980
1980
|
pairing_config = self.pairing_config_factory(connection)
|
|
1981
1981
|
session = self.session_proxy(
|
|
@@ -1995,7 +1995,7 @@ class Manager(EventEmitter):
|
|
|
1995
1995
|
|
|
1996
1996
|
async def pair(self, connection: Connection) -> None:
|
|
1997
1997
|
# TODO: check if there's already a session for this connection
|
|
1998
|
-
if connection.role !=
|
|
1998
|
+
if connection.role != Role.CENTRAL:
|
|
1999
1999
|
logger.warning('Start pairing as Peripheral!')
|
|
2000
2000
|
pairing_config = self.pairing_config_factory(connection)
|
|
2001
2001
|
session = self.session_proxy(
|
bumble/transport/usb.py
CHANGED
|
@@ -115,9 +115,7 @@ async def open_usb_transport(spec: str) -> Transport:
|
|
|
115
115
|
self.acl_out = acl_out
|
|
116
116
|
self.acl_out_transfer = device.getTransfer()
|
|
117
117
|
self.acl_out_transfer_ready = asyncio.Semaphore(1)
|
|
118
|
-
self.packets
|
|
119
|
-
asyncio.Queue()
|
|
120
|
-
) # Queue of packets waiting to be sent
|
|
118
|
+
self.packets = asyncio.Queue[bytes]() # Queue of packets waiting to be sent
|
|
121
119
|
self.loop = asyncio.get_running_loop()
|
|
122
120
|
self.queue_task = None
|
|
123
121
|
self.cancel_done = self.loop.create_future()
|