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/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, bumble.hci.OwnAddressType] = {
118
- host_pb2.PUBLIC: bumble.hci.OwnAddressType.PUBLIC,
119
- host_pb2.RANDOM: bumble.hci.OwnAddressType.RANDOM,
120
- host_pb2.RESOLVABLE_OR_PUBLIC: bumble.hci.OwnAddressType.RESOLVABLE_OR_PUBLIC,
121
- host_pb2.RESOLVABLE_OR_RANDOM: bumble.hci.OwnAddressType.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 == BT_PERIPHERAL_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 == BT_PERIPHERAL_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
@@ -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 == BT_PERIPHERAL_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, int] = {
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,
@@ -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 == BT_CENTRAL_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 != BT_CENTRAL_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: asyncio.Queue[bytes] = (
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: bumble
3
- Version: 0.0.208
3
+ Version: 0.0.209
4
4
  Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
5
5
  Author-email: Google <bumble-dev@google.com>
6
6
  Project-URL: Homepage, https://github.com/google/bumble