bumble 0.0.208__py3-none-any.whl → 0.0.210__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.
Files changed (77) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +7 -7
  3. bumble/apps/auracast.py +37 -29
  4. bumble/apps/bench.py +9 -7
  5. bumble/apps/console.py +1 -1
  6. bumble/apps/lea_unicast/app.py +6 -2
  7. bumble/apps/pair.py +4 -3
  8. bumble/apps/player/player.py +3 -3
  9. bumble/apps/rfcomm_bridge.py +1 -1
  10. bumble/apps/speaker/speaker.py +4 -2
  11. bumble/att.py +12 -5
  12. bumble/avc.py +5 -5
  13. bumble/avdtp.py +9 -10
  14. bumble/avrcp.py +18 -19
  15. bumble/bridge.py +2 -2
  16. bumble/controller.py +13 -15
  17. bumble/core.py +61 -60
  18. bumble/device.py +193 -162
  19. bumble/drivers/__init__.py +2 -2
  20. bumble/gap.py +1 -1
  21. bumble/gatt.py +16 -0
  22. bumble/gatt_adapters.py +3 -3
  23. bumble/gatt_client.py +27 -21
  24. bumble/gatt_server.py +9 -10
  25. bumble/hci.py +109 -90
  26. bumble/hfp.py +3 -3
  27. bumble/hid.py +4 -3
  28. bumble/host.py +30 -19
  29. bumble/keys.py +3 -3
  30. bumble/l2cap.py +21 -19
  31. bumble/link.py +5 -6
  32. bumble/pairing.py +3 -3
  33. bumble/pandora/__init__.py +5 -5
  34. bumble/pandora/host.py +30 -23
  35. bumble/pandora/l2cap.py +2 -2
  36. bumble/pandora/security.py +17 -19
  37. bumble/pandora/utils.py +2 -2
  38. bumble/profiles/aics.py +6 -6
  39. bumble/profiles/ancs.py +513 -0
  40. bumble/profiles/ascs.py +17 -10
  41. bumble/profiles/asha.py +5 -5
  42. bumble/profiles/bass.py +1 -1
  43. bumble/profiles/csip.py +10 -10
  44. bumble/profiles/gatt_service.py +12 -12
  45. bumble/profiles/hap.py +16 -16
  46. bumble/profiles/mcp.py +26 -24
  47. bumble/profiles/pacs.py +6 -6
  48. bumble/profiles/pbp.py +1 -1
  49. bumble/profiles/vcs.py +6 -4
  50. bumble/profiles/vocs.py +3 -3
  51. bumble/rfcomm.py +8 -8
  52. bumble/sdp.py +1 -1
  53. bumble/smp.py +39 -33
  54. bumble/transport/__init__.py +24 -19
  55. bumble/transport/android_emulator.py +8 -4
  56. bumble/transport/android_netsim.py +8 -5
  57. bumble/transport/common.py +5 -1
  58. bumble/transport/file.py +1 -1
  59. bumble/transport/hci_socket.py +1 -1
  60. bumble/transport/pty.py +1 -1
  61. bumble/transport/pyusb.py +3 -3
  62. bumble/transport/serial.py +1 -1
  63. bumble/transport/tcp_client.py +1 -1
  64. bumble/transport/tcp_server.py +1 -1
  65. bumble/transport/udp.py +1 -1
  66. bumble/transport/unix.py +1 -1
  67. bumble/transport/usb.py +1 -3
  68. bumble/transport/vhci.py +2 -2
  69. bumble/transport/ws_client.py +6 -1
  70. bumble/transport/ws_server.py +1 -1
  71. bumble/utils.py +89 -76
  72. {bumble-0.0.208.dist-info → bumble-0.0.210.dist-info}/METADATA +3 -2
  73. {bumble-0.0.208.dist-info → bumble-0.0.210.dist-info}/RECORD +77 -76
  74. {bumble-0.0.208.dist-info → bumble-0.0.210.dist-info}/WHEEL +1 -1
  75. {bumble-0.0.208.dist-info → bumble-0.0.210.dist-info}/entry_points.txt +0 -0
  76. {bumble-0.0.208.dist-info → bumble-0.0.210.dist-info/licenses}/LICENSE +0 -0
  77. {bumble-0.0.208.dist-info → bumble-0.0.210.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,513 @@
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
+
32
+ from bumble.att import ATT_Error
33
+ from bumble.device import Peer
34
+ from bumble.gatt import (
35
+ Characteristic,
36
+ GATT_ANCS_SERVICE,
37
+ GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
38
+ GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
39
+ GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
40
+ TemplateService,
41
+ )
42
+ from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
43
+ from bumble.gatt_adapters import SerializableCharacteristicProxyAdapter
44
+ from bumble import utils
45
+
46
+
47
+ # -----------------------------------------------------------------------------
48
+ # Constants
49
+ # -----------------------------------------------------------------------------
50
+ _DEFAULT_ATTRIBUTE_MAX_LENGTH = 65535
51
+
52
+
53
+ # -----------------------------------------------------------------------------
54
+ # Logging
55
+ # -----------------------------------------------------------------------------
56
+ logger = logging.getLogger(__name__)
57
+
58
+
59
+ # -----------------------------------------------------------------------------
60
+ # Protocol
61
+ # -----------------------------------------------------------------------------
62
+ class ActionId(utils.OpenIntEnum):
63
+ POSITIVE = 0
64
+ NEGATIVE = 1
65
+
66
+
67
+ class AppAttributeId(utils.OpenIntEnum):
68
+ DISPLAY_NAME = 0
69
+
70
+
71
+ class CategoryId(utils.OpenIntEnum):
72
+ OTHER = 0
73
+ INCOMING_CALL = 1
74
+ MISSED_CALL = 2
75
+ VOICEMAIL = 3
76
+ SOCIAL = 4
77
+ SCHEDULE = 5
78
+ EMAIL = 6
79
+ NEWS = 7
80
+ HEALTH_AND_FITNESS = 8
81
+ BUSINESS_AND_FINANCE = 9
82
+ LOCATION = 10
83
+ ENTERTAINMENT = 11
84
+
85
+
86
+ class CommandId(utils.OpenIntEnum):
87
+ GET_NOTIFICATION_ATTRIBUTES = 0
88
+ GET_APP_ATTRIBUTES = 1
89
+ PERFORM_NOTIFICATION_ACTION = 2
90
+
91
+
92
+ class EventId(utils.OpenIntEnum):
93
+ NOTIFICATION_ADDED = 0
94
+ NOTIFICATION_MODIFIED = 1
95
+ NOTIFICATION_REMOVED = 2
96
+
97
+
98
+ class EventFlags(enum.IntFlag):
99
+ SILENT = 1 << 0
100
+ IMPORTANT = 1 << 1
101
+ PRE_EXISTING = 1 << 2
102
+ POSITIVE_ACTION = 1 << 3
103
+ NEGATIVE_ACTION = 1 << 4
104
+
105
+
106
+ class NotificationAttributeId(utils.OpenIntEnum):
107
+ APP_IDENTIFIER = 0
108
+ TITLE = 1
109
+ SUBTITLE = 2
110
+ MESSAGE = 3
111
+ MESSAGE_SIZE = 4
112
+ DATE = 5
113
+ POSITIVE_ACTION_LABEL = 6
114
+ NEGATIVE_ACTION_LABEL = 7
115
+
116
+
117
+ @dataclasses.dataclass
118
+ class NotificationAttribute:
119
+ attribute_id: NotificationAttributeId
120
+ value: Union[str, int, datetime.datetime]
121
+
122
+
123
+ @dataclasses.dataclass
124
+ class AppAttribute:
125
+ attribute_id: AppAttributeId
126
+ value: str
127
+
128
+
129
+ @dataclasses.dataclass
130
+ class Notification:
131
+ event_id: EventId
132
+ event_flags: EventFlags
133
+ category_id: CategoryId
134
+ category_count: int
135
+ notification_uid: int
136
+
137
+ @classmethod
138
+ def from_bytes(cls, data: bytes) -> Notification:
139
+ return cls(
140
+ event_id=EventId(data[0]),
141
+ event_flags=EventFlags(data[1]),
142
+ category_id=CategoryId(data[2]),
143
+ category_count=data[3],
144
+ notification_uid=int.from_bytes(data[4:8], 'little'),
145
+ )
146
+
147
+ def __bytes__(self) -> bytes:
148
+ return struct.pack(
149
+ "<BBBBI",
150
+ self.event_id,
151
+ self.event_flags,
152
+ self.category_id,
153
+ self.category_count,
154
+ self.notification_uid,
155
+ )
156
+
157
+
158
+ class ErrorCode(utils.OpenIntEnum):
159
+ UNKNOWN_COMMAND = 0xA0
160
+ INVALID_COMMAND = 0xA1
161
+ INVALID_PARAMETER = 0xA2
162
+ ACTION_FAILED = 0xA3
163
+
164
+
165
+ class ProtocolError(Exception):
166
+ pass
167
+
168
+
169
+ class CommandError(Exception):
170
+ def __init__(self, error_code: ErrorCode) -> None:
171
+ self.error_code = error_code
172
+
173
+ def __str__(self) -> str:
174
+ return f"CommandError(error_code={self.error_code.name})"
175
+
176
+
177
+ # -----------------------------------------------------------------------------
178
+ # GATT Server-side
179
+ # -----------------------------------------------------------------------------
180
+ class Ancs(TemplateService):
181
+ UUID = GATT_ANCS_SERVICE
182
+
183
+ notification_source_characteristic: Characteristic
184
+ data_source_characteristic: Characteristic
185
+ control_point_characteristic: Characteristic
186
+
187
+ def __init__(self) -> None:
188
+ # TODO not the final implementation
189
+ self.notification_source_characteristic = Characteristic(
190
+ GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC,
191
+ Characteristic.Properties.NOTIFY,
192
+ Characteristic.Permissions.READABLE,
193
+ )
194
+
195
+ # TODO not the final implementation
196
+ self.data_source_characteristic = Characteristic(
197
+ GATT_ANCS_DATA_SOURCE_CHARACTERISTIC,
198
+ Characteristic.Properties.NOTIFY,
199
+ Characteristic.Permissions.READABLE,
200
+ )
201
+
202
+ # TODO not the final implementation
203
+ self.control_point_characteristic = Characteristic(
204
+ GATT_ANCS_CONTROL_POINT_CHARACTERISTIC,
205
+ Characteristic.Properties.WRITE,
206
+ Characteristic.Permissions.WRITEABLE,
207
+ )
208
+
209
+ super().__init__(
210
+ [
211
+ self.notification_source_characteristic,
212
+ self.data_source_characteristic,
213
+ self.control_point_characteristic,
214
+ ]
215
+ )
216
+
217
+
218
+ # -----------------------------------------------------------------------------
219
+ # GATT Client-side
220
+ # -----------------------------------------------------------------------------
221
+ class AncsProxy(ProfileServiceProxy):
222
+ SERVICE_CLASS = Ancs
223
+
224
+ notification_source: CharacteristicProxy[Notification]
225
+ data_source: CharacteristicProxy
226
+ control_point: CharacteristicProxy[bytes]
227
+
228
+ def __init__(self, service_proxy: ServiceProxy):
229
+ self.notification_source = SerializableCharacteristicProxyAdapter(
230
+ service_proxy.get_required_characteristic_by_uuid(
231
+ GATT_ANCS_NOTIFICATION_SOURCE_CHARACTERISTIC
232
+ ),
233
+ Notification,
234
+ )
235
+
236
+ self.data_source = service_proxy.get_required_characteristic_by_uuid(
237
+ GATT_ANCS_DATA_SOURCE_CHARACTERISTIC
238
+ )
239
+
240
+ self.control_point = service_proxy.get_required_characteristic_by_uuid(
241
+ GATT_ANCS_CONTROL_POINT_CHARACTERISTIC
242
+ )
243
+
244
+
245
+ class AncsClient(utils.EventEmitter):
246
+ _expected_response_command_id: Optional[CommandId]
247
+ _expected_response_notification_uid: Optional[int]
248
+ _expected_response_app_identifier: Optional[str]
249
+ _expected_app_identifier: Optional[str]
250
+ _expected_response_tuples: int
251
+ _response_accumulator: bytes
252
+
253
+ def __init__(self, ancs_proxy: AncsProxy) -> None:
254
+ super().__init__()
255
+ self._ancs_proxy = ancs_proxy
256
+ self._command_semaphore = asyncio.Semaphore()
257
+ self._response: Optional[asyncio.Future] = None
258
+ self._reset_response()
259
+ self._started = False
260
+
261
+ @classmethod
262
+ async def for_peer(cls, peer: Peer) -> Optional[AncsClient]:
263
+ ancs_proxy = await peer.discover_service_and_create_proxy(AncsProxy)
264
+ if ancs_proxy is None:
265
+ return None
266
+ return cls(ancs_proxy)
267
+
268
+ async def start(self) -> None:
269
+ await self._ancs_proxy.notification_source.subscribe(self._on_notification)
270
+ await self._ancs_proxy.data_source.subscribe(self._on_data)
271
+ self._started = True
272
+
273
+ async def stop(self) -> None:
274
+ await self._ancs_proxy.notification_source.unsubscribe(self._on_notification)
275
+ await self._ancs_proxy.data_source.unsubscribe(self._on_data)
276
+ self._started = False
277
+
278
+ def _reset_response(self) -> None:
279
+ self._expected_response_command_id = None
280
+ self._expected_response_notification_uid = None
281
+ self._expected_app_identifier = None
282
+ self._expected_response_tuples = 0
283
+ self._response_accumulator = b""
284
+
285
+ def _on_notification(self, notification: Notification) -> None:
286
+ logger.debug(f"ANCS NOTIFICATION: {notification}")
287
+ self.emit("notification", notification)
288
+
289
+ def _on_data(self, data: bytes) -> None:
290
+ logger.debug(f"ANCS DATA: {data.hex()}")
291
+
292
+ if not self._response:
293
+ logger.warning("received unexpected data, discarding")
294
+ return
295
+
296
+ self._response_accumulator += data
297
+
298
+ # Try to parse the accumulated data until we have all we need.
299
+ if not self._response_accumulator:
300
+ logger.warning("empty data from data source")
301
+ return
302
+
303
+ command_id = self._response_accumulator[0]
304
+ if command_id != self._expected_response_command_id:
305
+ logger.warning(
306
+ "unexpected response command id: "
307
+ f"expected {self._expected_response_command_id} "
308
+ f"but got {command_id}"
309
+ )
310
+ self._reset_response()
311
+ if not self._response.done():
312
+ self._response.set_exception(ProtocolError())
313
+
314
+ if len(self._response_accumulator) < 5:
315
+ # Not enough data yet.
316
+ return
317
+
318
+ attributes: list[Union[NotificationAttribute, AppAttribute]] = []
319
+
320
+ if command_id == CommandId.GET_NOTIFICATION_ATTRIBUTES:
321
+ (notification_uid,) = struct.unpack_from(
322
+ "<I", self._response_accumulator, 1
323
+ )
324
+ if notification_uid != self._expected_response_notification_uid:
325
+ logger.warning(
326
+ "unexpected response notification uid: "
327
+ f"expected {self._expected_response_notification_uid} "
328
+ f"but got {notification_uid}"
329
+ )
330
+ self._reset_response()
331
+ if not self._response.done():
332
+ self._response.set_exception(ProtocolError())
333
+
334
+ attribute_data = self._response_accumulator[5:]
335
+ while len(attribute_data) >= 3:
336
+ attribute_id, attribute_data_length = struct.unpack_from(
337
+ "<BH", attribute_data, 0
338
+ )
339
+ if len(attribute_data) < 3 + attribute_data_length:
340
+ return
341
+ str_value = attribute_data[3 : 3 + attribute_data_length].decode(
342
+ "utf-8"
343
+ )
344
+ value: Union[str, int, datetime.datetime]
345
+ if attribute_id == NotificationAttributeId.MESSAGE_SIZE:
346
+ value = int(str_value)
347
+ elif attribute_id == NotificationAttributeId.DATE:
348
+ year = int(str_value[:4])
349
+ month = int(str_value[4:6])
350
+ day = int(str_value[6:8])
351
+ hour = int(str_value[9:11])
352
+ minute = int(str_value[11:13])
353
+ second = int(str_value[13:15])
354
+ value = datetime.datetime(year, month, day, hour, minute, second)
355
+ else:
356
+ value = str_value
357
+ attributes.append(
358
+ NotificationAttribute(NotificationAttributeId(attribute_id), value)
359
+ )
360
+ attribute_data = attribute_data[3 + attribute_data_length :]
361
+ elif command_id == CommandId.GET_APP_ATTRIBUTES:
362
+ if 0 not in self._response_accumulator[1:]:
363
+ # No null-terminated string yet.
364
+ return
365
+
366
+ app_identifier_length = self._response_accumulator.find(0, 1) - 1
367
+ app_identifier = self._response_accumulator[
368
+ 1 : 1 + app_identifier_length
369
+ ].decode("utf-8")
370
+ if app_identifier != self._expected_response_app_identifier:
371
+ logger.warning(
372
+ "unexpected response app identifier: "
373
+ f"expected {self._expected_response_app_identifier} "
374
+ f"but got {app_identifier}"
375
+ )
376
+ self._reset_response()
377
+ if not self._response.done():
378
+ self._response.set_exception(ProtocolError())
379
+
380
+ attribute_data = self._response_accumulator[1 + app_identifier_length + 1 :]
381
+ while len(attribute_data) >= 3:
382
+ attribute_id, attribute_data_length = struct.unpack_from(
383
+ "<BH", attribute_data, 0
384
+ )
385
+ if len(attribute_data) < 3 + attribute_data_length:
386
+ return
387
+ attributes.append(
388
+ AppAttribute(
389
+ AppAttributeId(attribute_id),
390
+ attribute_data[3 : 3 + attribute_data_length].decode("utf-8"),
391
+ )
392
+ )
393
+ attribute_data = attribute_data[3 + attribute_data_length :]
394
+ else:
395
+ logger.warning(f"unexpected response command id {command_id}")
396
+ return
397
+
398
+ if len(attributes) < self._expected_response_tuples:
399
+ # We have not received all the tuples yet.
400
+ return
401
+
402
+ if not self._response.done():
403
+ self._response.set_result(attributes)
404
+
405
+ async def _send_command(self, command: bytes) -> None:
406
+ try:
407
+ await self._ancs_proxy.control_point.write_value(
408
+ command, with_response=True
409
+ )
410
+ except ATT_Error as error:
411
+ raise CommandError(error_code=ErrorCode(error.error_code)) from error
412
+
413
+ async def get_notification_attributes(
414
+ self,
415
+ notification_uid: int,
416
+ attributes: Sequence[
417
+ Union[NotificationAttributeId, tuple[NotificationAttributeId, int]]
418
+ ],
419
+ ) -> list[NotificationAttribute]:
420
+ if not self._started:
421
+ raise RuntimeError("client not started")
422
+
423
+ command = struct.pack(
424
+ "<BI", CommandId.GET_NOTIFICATION_ATTRIBUTES, notification_uid
425
+ )
426
+ for attribute in attributes:
427
+ attribute_max_length = 0
428
+ if isinstance(attribute, tuple):
429
+ attribute_id, attribute_max_length = attribute
430
+ if attribute_id not in (
431
+ NotificationAttributeId.TITLE,
432
+ NotificationAttributeId.SUBTITLE,
433
+ NotificationAttributeId.MESSAGE,
434
+ ):
435
+ raise ValueError(
436
+ "this attribute does not allow specifying a max length"
437
+ )
438
+ else:
439
+ attribute_id = attribute
440
+ if attribute_id in (
441
+ NotificationAttributeId.TITLE,
442
+ NotificationAttributeId.SUBTITLE,
443
+ NotificationAttributeId.MESSAGE,
444
+ ):
445
+ attribute_max_length = _DEFAULT_ATTRIBUTE_MAX_LENGTH
446
+
447
+ if attribute_max_length:
448
+ command += struct.pack("<BH", attribute_id, attribute_max_length)
449
+ else:
450
+ command += struct.pack("B", attribute_id)
451
+
452
+ try:
453
+ async with self._command_semaphore:
454
+ self._expected_response_notification_uid = notification_uid
455
+ self._expected_response_tuples = len(attributes)
456
+ self._expected_response_command_id = (
457
+ CommandId.GET_NOTIFICATION_ATTRIBUTES
458
+ )
459
+ self._response = asyncio.Future()
460
+
461
+ # Send the command.
462
+ await self._send_command(command)
463
+
464
+ # Wait for the response.
465
+ return await self._response
466
+ finally:
467
+ self._reset_response()
468
+
469
+ async def get_app_attributes(
470
+ self, app_identifier: str, attributes: Sequence[AppAttributeId]
471
+ ) -> list[AppAttribute]:
472
+ if not self._started:
473
+ raise RuntimeError("client not started")
474
+
475
+ command = (
476
+ bytes([CommandId.GET_APP_ATTRIBUTES])
477
+ + app_identifier.encode("utf-8")
478
+ + b"\0"
479
+ )
480
+ for attribute_id in attributes:
481
+ command += struct.pack("B", attribute_id)
482
+
483
+ try:
484
+ async with self._command_semaphore:
485
+ self._expected_response_app_identifier = app_identifier
486
+ self._expected_response_tuples = len(attributes)
487
+ self._expected_response_command_id = CommandId.GET_APP_ATTRIBUTES
488
+ self._response = asyncio.Future()
489
+
490
+ # Send the command.
491
+ await self._send_command(command)
492
+
493
+ # Wait for the response.
494
+ return await self._response
495
+ finally:
496
+ self._reset_response()
497
+
498
+ async def perform_action(self, notification_uid: int, action: ActionId) -> None:
499
+ if not self._started:
500
+ raise RuntimeError("client not started")
501
+
502
+ command = struct.pack(
503
+ "<BIB", CommandId.PERFORM_NOTIFICATION_ACTION, notification_uid, action
504
+ )
505
+
506
+ async with self._command_semaphore:
507
+ await self._send_command(command)
508
+
509
+ async def perform_positive_action(self, notification_uid: int) -> None:
510
+ return await self.perform_action(notification_uid, ActionId.POSITIVE)
511
+
512
+ async def perform_negative_action(self, notification_uid: int) -> None:
513
+ return await self.perform_action(notification_uid, ActionId.NEGATIVE)
bumble/profiles/ascs.py CHANGED
@@ -23,6 +23,7 @@ import logging
23
23
  import struct
24
24
  from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
25
25
 
26
+ from bumble import utils
26
27
  from bumble import colors
27
28
  from bumble.profiles.bap import CodecSpecificConfiguration
28
29
  from bumble.profiles import le_audio
@@ -343,8 +344,10 @@ class AseStateMachine(gatt.Characteristic):
343
344
  and cis_id == self.cis_id
344
345
  and self.state == self.State.ENABLING
345
346
  ):
346
- acl_connection.abort_on(
347
- 'flush', self.service.device.accept_cis_request(cis_handle)
347
+ utils.cancel_on_event(
348
+ acl_connection,
349
+ 'flush',
350
+ self.service.device.accept_cis_request(cis_handle),
348
351
  )
349
352
 
350
353
  def on_cis_establishment(self, cis_link: device.CisLink) -> None:
@@ -361,7 +364,9 @@ class AseStateMachine(gatt.Characteristic):
361
364
  self.state = self.State.STREAMING
362
365
  await self.service.device.notify_subscribers(self, self.value)
363
366
 
364
- cis_link.acl_connection.abort_on('flush', post_cis_established())
367
+ utils.cancel_on_event(
368
+ cis_link.acl_connection, 'flush', post_cis_established()
369
+ )
365
370
  self.cis_link = cis_link
366
371
 
367
372
  def on_cis_disconnection(self, _reason) -> None:
@@ -509,7 +514,7 @@ class AseStateMachine(gatt.Characteristic):
509
514
  self.state = self.State.IDLE
510
515
  await self.service.device.notify_subscribers(self, self.value)
511
516
 
512
- self.service.device.abort_on('flush', remove_cis_async())
517
+ utils.cancel_on_event(self.service.device, 'flush', remove_cis_async())
513
518
  return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
514
519
 
515
520
  @property
@@ -594,7 +599,7 @@ class AudioStreamControlService(gatt.TemplateService):
594
599
  UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
595
600
 
596
601
  ase_state_machines: Dict[int, AseStateMachine]
597
- ase_control_point: gatt.Characteristic
602
+ ase_control_point: gatt.Characteristic[bytes]
598
603
  _active_client: Optional[device.Connection] = None
599
604
 
600
605
  def __init__(
@@ -691,7 +696,8 @@ class AudioStreamControlService(gatt.TemplateService):
691
696
  control_point_notification = bytes(
692
697
  [operation.op_code, len(responses)]
693
698
  ) + b''.join(map(bytes, responses))
694
- self.device.abort_on(
699
+ utils.cancel_on_event(
700
+ self.device,
695
701
  'flush',
696
702
  self.device.notify_subscribers(
697
703
  self.ase_control_point, control_point_notification
@@ -700,7 +706,8 @@ class AudioStreamControlService(gatt.TemplateService):
700
706
 
701
707
  for ase_id, *_ in responses:
702
708
  if ase := self.ase_state_machines.get(ase_id):
703
- self.device.abort_on(
709
+ utils.cancel_on_event(
710
+ self.device,
704
711
  'flush',
705
712
  self.device.notify_subscribers(ase, ase.value),
706
713
  )
@@ -710,9 +717,9 @@ class AudioStreamControlService(gatt.TemplateService):
710
717
  class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
711
718
  SERVICE_CLASS = AudioStreamControlService
712
719
 
713
- sink_ase: List[gatt_client.CharacteristicProxy]
714
- source_ase: List[gatt_client.CharacteristicProxy]
715
- ase_control_point: gatt_client.CharacteristicProxy
720
+ sink_ase: List[gatt_client.CharacteristicProxy[bytes]]
721
+ source_ase: List[gatt_client.CharacteristicProxy[bytes]]
722
+ ase_control_point: gatt_client.CharacteristicProxy[bytes]
716
723
 
717
724
  def __init__(self, service_proxy: gatt_client.ServiceProxy):
718
725
  self.service_proxy = service_proxy
bumble/profiles/asha.py CHANGED
@@ -259,11 +259,11 @@ class AshaService(gatt.TemplateService):
259
259
  # -----------------------------------------------------------------------------
260
260
  class AshaServiceProxy(gatt_client.ProfileServiceProxy):
261
261
  SERVICE_CLASS = AshaService
262
- read_only_properties_characteristic: gatt_client.CharacteristicProxy
263
- audio_control_point_characteristic: gatt_client.CharacteristicProxy
264
- audio_status_point_characteristic: gatt_client.CharacteristicProxy
265
- volume_characteristic: gatt_client.CharacteristicProxy
266
- psm_characteristic: gatt_client.CharacteristicProxy
262
+ read_only_properties_characteristic: gatt_client.CharacteristicProxy[bytes]
263
+ audio_control_point_characteristic: gatt_client.CharacteristicProxy[bytes]
264
+ audio_status_point_characteristic: gatt_client.CharacteristicProxy[bytes]
265
+ volume_characteristic: gatt_client.CharacteristicProxy[bytes]
266
+ psm_characteristic: gatt_client.CharacteristicProxy[bytes]
267
267
 
268
268
  def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
269
269
  self.service_proxy = service_proxy
bumble/profiles/bass.py CHANGED
@@ -354,7 +354,7 @@ class BroadcastAudioScanService(gatt.TemplateService):
354
354
  class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
355
355
  SERVICE_CLASS = BroadcastAudioScanService
356
356
 
357
- broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
357
+ broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy[bytes]
358
358
  broadcast_receive_states: list[
359
359
  gatt_client.CharacteristicProxy[Optional[BroadcastReceiveState]]
360
360
  ]
bumble/profiles/csip.py CHANGED
@@ -99,10 +99,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
99
99
  UUID = gatt.GATT_COORDINATED_SET_IDENTIFICATION_SERVICE
100
100
 
101
101
  set_identity_resolving_key: bytes
102
- set_identity_resolving_key_characteristic: gatt.Characteristic
103
- coordinated_set_size_characteristic: Optional[gatt.Characteristic] = None
104
- set_member_lock_characteristic: Optional[gatt.Characteristic] = None
105
- set_member_rank_characteristic: Optional[gatt.Characteristic] = None
102
+ set_identity_resolving_key_characteristic: gatt.Characteristic[bytes]
103
+ coordinated_set_size_characteristic: Optional[gatt.Characteristic[bytes]] = None
104
+ set_member_lock_characteristic: Optional[gatt.Characteristic[bytes]] = None
105
+ set_member_rank_characteristic: Optional[gatt.Characteristic[bytes]] = None
106
106
 
107
107
  def __init__(
108
108
  self,
@@ -170,7 +170,7 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
170
170
  else:
171
171
  assert connection
172
172
 
173
- if connection.transport == core.BT_LE_TRANSPORT:
173
+ if connection.transport == core.PhysicalTransport.LE:
174
174
  key = await connection.device.get_long_term_key(
175
175
  connection_handle=connection.handle, rand=b'', ediv=0
176
176
  )
@@ -203,10 +203,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
203
203
  class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
204
204
  SERVICE_CLASS = CoordinatedSetIdentificationService
205
205
 
206
- set_identity_resolving_key: gatt_client.CharacteristicProxy
207
- coordinated_set_size: Optional[gatt_client.CharacteristicProxy] = None
208
- set_member_lock: Optional[gatt_client.CharacteristicProxy] = None
209
- set_member_rank: Optional[gatt_client.CharacteristicProxy] = None
206
+ set_identity_resolving_key: gatt_client.CharacteristicProxy[bytes]
207
+ coordinated_set_size: Optional[gatt_client.CharacteristicProxy[bytes]] = None
208
+ set_member_lock: Optional[gatt_client.CharacteristicProxy[bytes]] = None
209
+ set_member_rank: Optional[gatt_client.CharacteristicProxy[bytes]] = None
210
210
 
211
211
  def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
212
212
  self.service_proxy = service_proxy
@@ -242,7 +242,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
242
242
  else:
243
243
  connection = self.service_proxy.client.connection
244
244
  device = connection.device
245
- if connection.transport == core.BT_LE_TRANSPORT:
245
+ if connection.transport == core.PhysicalTransport.LE:
246
246
  key = await device.get_long_term_key(
247
247
  connection_handle=connection.handle, rand=b'', ediv=0
248
248
  )