bumble 0.0.211__py3-none-any.whl → 0.0.213__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 (95) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +6 -0
  3. bumble/apps/README.md +0 -3
  4. bumble/apps/auracast.py +11 -9
  5. bumble/apps/bench.py +482 -31
  6. bumble/apps/console.py +5 -5
  7. bumble/apps/controller_info.py +47 -10
  8. bumble/apps/controller_loopback.py +7 -3
  9. bumble/apps/controllers.py +2 -2
  10. bumble/apps/device_info.py +2 -2
  11. bumble/apps/gatt_dump.py +2 -2
  12. bumble/apps/gg_bridge.py +2 -2
  13. bumble/apps/hci_bridge.py +2 -2
  14. bumble/apps/l2cap_bridge.py +2 -2
  15. bumble/apps/lea_unicast/app.py +6 -1
  16. bumble/apps/pair.py +204 -43
  17. bumble/apps/pandora_server.py +2 -2
  18. bumble/apps/rfcomm_bridge.py +1 -1
  19. bumble/apps/scan.py +2 -2
  20. bumble/apps/show.py +4 -2
  21. bumble/apps/speaker/speaker.html +1 -0
  22. bumble/apps/speaker/speaker.js +113 -62
  23. bumble/apps/speaker/speaker.py +126 -18
  24. bumble/at.py +4 -4
  25. bumble/att.py +15 -18
  26. bumble/avc.py +7 -7
  27. bumble/avctp.py +5 -5
  28. bumble/avdtp.py +138 -88
  29. bumble/avrcp.py +52 -58
  30. bumble/colors.py +2 -2
  31. bumble/controller.py +84 -23
  32. bumble/core.py +13 -7
  33. bumble/{crypto.py → crypto/__init__.py} +11 -95
  34. bumble/crypto/builtin.py +652 -0
  35. bumble/crypto/cryptography.py +84 -0
  36. bumble/device.py +688 -345
  37. bumble/drivers/__init__.py +2 -2
  38. bumble/drivers/common.py +0 -2
  39. bumble/drivers/intel.py +40 -40
  40. bumble/drivers/rtk.py +28 -35
  41. bumble/gatt.py +7 -9
  42. bumble/gatt_adapters.py +4 -5
  43. bumble/gatt_client.py +31 -34
  44. bumble/gatt_server.py +15 -17
  45. bumble/hci.py +2635 -2878
  46. bumble/helpers.py +4 -5
  47. bumble/hfp.py +76 -57
  48. bumble/hid.py +24 -12
  49. bumble/host.py +117 -34
  50. bumble/keys.py +68 -52
  51. bumble/l2cap.py +329 -403
  52. bumble/link.py +6 -270
  53. bumble/pairing.py +23 -20
  54. bumble/pandora/__init__.py +1 -1
  55. bumble/pandora/config.py +2 -2
  56. bumble/pandora/device.py +6 -6
  57. bumble/pandora/host.py +38 -39
  58. bumble/pandora/l2cap.py +4 -4
  59. bumble/pandora/security.py +73 -57
  60. bumble/pandora/utils.py +3 -3
  61. bumble/profiles/aics.py +3 -5
  62. bumble/profiles/ancs.py +3 -1
  63. bumble/profiles/ascs.py +143 -136
  64. bumble/profiles/asha.py +13 -8
  65. bumble/profiles/bap.py +3 -4
  66. bumble/profiles/csip.py +3 -5
  67. bumble/profiles/device_information_service.py +2 -2
  68. bumble/profiles/gap.py +2 -2
  69. bumble/profiles/gatt_service.py +1 -3
  70. bumble/profiles/hap.py +42 -58
  71. bumble/profiles/le_audio.py +4 -4
  72. bumble/profiles/mcp.py +16 -13
  73. bumble/profiles/vcs.py +8 -10
  74. bumble/profiles/vocs.py +6 -9
  75. bumble/rfcomm.py +27 -18
  76. bumble/rtp.py +1 -2
  77. bumble/sdp.py +2 -2
  78. bumble/smp.py +71 -69
  79. bumble/tools/rtk_util.py +2 -2
  80. bumble/transport/__init__.py +2 -16
  81. bumble/transport/android_netsim.py +5 -5
  82. bumble/transport/common.py +4 -4
  83. bumble/transport/pyusb.py +2 -2
  84. bumble/utils.py +2 -5
  85. bumble/vendor/android/hci.py +118 -200
  86. bumble/vendor/zephyr/hci.py +32 -27
  87. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/METADATA +5 -5
  88. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/RECORD +92 -93
  89. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/WHEEL +1 -1
  90. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/entry_points.txt +0 -1
  91. bumble/apps/link_relay/__init__.py +0 -0
  92. bumble/apps/link_relay/link_relay.py +0 -289
  93. bumble/apps/link_relay/logging.yml +0 -21
  94. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/licenses/LICENSE +0 -0
  95. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/top_level.txt +0 -0
bumble/profiles/gap.py CHANGED
@@ -19,7 +19,7 @@
19
19
  # -----------------------------------------------------------------------------
20
20
  import logging
21
21
  import struct
22
- from typing import Optional, Tuple, Union
22
+ from typing import Optional, Union
23
23
 
24
24
  from bumble.core import Appearance
25
25
  from bumble.gatt import (
@@ -54,7 +54,7 @@ class GenericAccessService(TemplateService):
54
54
  appearance_characteristic: Characteristic[bytes]
55
55
 
56
56
  def __init__(
57
- self, device_name: str, appearance: Union[Appearance, Tuple[int, int], int] = 0
57
+ self, device_name: str, appearance: Union[Appearance, tuple[int, int], int] = 0
58
58
  ):
59
59
  if isinstance(appearance, int):
60
60
  appearance_int = appearance
@@ -127,9 +127,7 @@ class GenericAttributeProfileService(gatt.TemplateService):
127
127
 
128
128
  return b''
129
129
 
130
- def get_database_hash(self, connection: device.Connection | None) -> bytes:
131
- assert connection
132
-
130
+ def get_database_hash(self, connection: device.Connection) -> bytes:
133
131
  m = b''.join(
134
132
  [
135
133
  self.get_attribute_data(attribute)
bumble/profiles/hap.py CHANGED
@@ -20,7 +20,7 @@ import asyncio
20
20
  import functools
21
21
  from dataclasses import dataclass, field
22
22
  import logging
23
- from typing import Any, Dict, List, Optional, Set, Union
23
+ from typing import Any, Optional, Union
24
24
 
25
25
  from bumble import att, gatt, gatt_adapters, gatt_client
26
26
  from bumble.core import InvalidArgumentError, InvalidStateError
@@ -228,23 +228,25 @@ class HearingAccessService(gatt.TemplateService):
228
228
  hearing_aid_preset_control_point: gatt.Characteristic[bytes]
229
229
  active_preset_index_characteristic: gatt.Characteristic[bytes]
230
230
  active_preset_index: int
231
- active_preset_index_per_device: Dict[Address, int]
231
+ active_preset_index_per_device: dict[Address, int]
232
232
 
233
233
  device: Device
234
234
 
235
235
  server_features: HearingAidFeatures
236
- preset_records: Dict[int, PresetRecord] # key is the preset index
236
+ preset_records: dict[int, PresetRecord] # key is the preset index
237
237
  read_presets_request_in_progress: bool
238
238
 
239
- preset_changed_operations_history_per_device: Dict[
240
- Address, List[PresetChangedOperation]
239
+ other_server_in_binaural_set: Optional[HearingAccessService] = None
240
+
241
+ preset_changed_operations_history_per_device: dict[
242
+ Address, list[PresetChangedOperation]
241
243
  ]
242
244
 
243
245
  # Keep an updated list of connected client to send notification to
244
- currently_connected_clients: Set[Connection]
246
+ currently_connected_clients: set[Connection]
245
247
 
246
248
  def __init__(
247
- self, device: Device, features: HearingAidFeatures, presets: List[PresetRecord]
249
+ self, device: Device, features: HearingAidFeatures, presets: list[PresetRecord]
248
250
  ) -> None:
249
251
  self.active_preset_index_per_device = {}
250
252
  self.read_presets_request_in_progress = False
@@ -266,13 +268,13 @@ class HearingAccessService(gatt.TemplateService):
266
268
  # associate the lowest index as the current active preset at startup
267
269
  self.active_preset_index = sorted(self.preset_records.keys())[0]
268
270
 
269
- @device.on('connection') # type: ignore
271
+ @device.on(device.EVENT_CONNECTION)
270
272
  def on_connection(connection: Connection) -> None:
271
- @connection.on('disconnection') # type: ignore
273
+ @connection.on(connection.EVENT_DISCONNECTION)
272
274
  def on_disconnection(_reason) -> None:
273
275
  self.currently_connected_clients.remove(connection)
274
276
 
275
- @connection.on('pairing') # type: ignore
277
+ @connection.on(connection.EVENT_PAIRING)
276
278
  def on_pairing(*_: Any) -> None:
277
279
  self.on_incoming_paired_connection(connection)
278
280
 
@@ -333,11 +335,10 @@ class HearingAccessService(gatt.TemplateService):
333
335
  # Update the active preset index if needed
334
336
  await self.notify_active_preset_for_connection(connection)
335
337
 
336
- utils.cancel_on_event(connection, 'disconnection', on_connection_async())
338
+ connection.cancel_on_disconnection(on_connection_async())
337
339
 
338
- def _on_read_active_preset_index(
339
- self, __connection__: Optional[Connection]
340
- ) -> bytes:
340
+ def _on_read_active_preset_index(self, connection: Connection) -> bytes:
341
+ del connection # Unused
341
342
  return bytes([self.active_preset_index])
342
343
 
343
344
  # TODO this need to be triggered when device is unbonded
@@ -345,18 +346,13 @@ class HearingAccessService(gatt.TemplateService):
345
346
  self.preset_changed_operations_history_per_device.pop(addr)
346
347
 
347
348
  async def _on_write_hearing_aid_preset_control_point(
348
- self, connection: Optional[Connection], value: bytes
349
+ self, connection: Connection, value: bytes
349
350
  ):
350
- assert connection
351
-
352
351
  opcode = HearingAidPresetControlPointOpcode(value[0])
353
352
  handler = getattr(self, '_on_' + opcode.name.lower())
354
353
  await handler(connection, value)
355
354
 
356
- async def _on_read_presets_request(
357
- self, connection: Optional[Connection], value: bytes
358
- ):
359
- assert connection
355
+ async def _on_read_presets_request(self, connection: Connection, value: bytes):
360
356
  if connection.att_mtu < 49: # 2.5. GATT sub-procedure requirements
361
357
  logging.warning(f'HAS require MTU >= 49: {connection}')
362
358
 
@@ -385,7 +381,7 @@ class HearingAccessService(gatt.TemplateService):
385
381
  utils.AsyncRunner.spawn(self._read_preset_response(connection, presets))
386
382
 
387
383
  async def _read_preset_response(
388
- self, connection: Connection, presets: List[PresetRecord]
384
+ self, connection: Connection, presets: list[PresetRecord]
389
385
  ):
390
386
  # If the ATT bearer is terminated before all notifications or indications are sent, then the server shall consider the Read Presets Request operation aborted and shall not either continue or restart the operation when the client reconnects.
391
387
  try:
@@ -471,10 +467,7 @@ class HearingAccessService(gatt.TemplateService):
471
467
  for connection in self.currently_connected_clients:
472
468
  await self._preset_changed_operation(connection)
473
469
 
474
- async def _on_write_preset_name(
475
- self, connection: Optional[Connection], value: bytes
476
- ):
477
- assert connection
470
+ async def _on_write_preset_name(self, connection: Connection, value: bytes):
478
471
 
479
472
  if self.read_presets_request_in_progress:
480
473
  raise att.ATT_Error(att.ErrorCode.PROCEDURE_ALREADY_IN_PROGRESS)
@@ -522,10 +515,7 @@ class HearingAccessService(gatt.TemplateService):
522
515
  for connection in self.currently_connected_clients:
523
516
  await self.notify_active_preset_for_connection(connection)
524
517
 
525
- async def set_active_preset(
526
- self, connection: Optional[Connection], value: bytes
527
- ) -> None:
528
- assert connection
518
+ async def set_active_preset(self, value: bytes) -> None:
529
519
  index = value[1]
530
520
  preset = self.preset_records.get(index, None)
531
521
  if (
@@ -542,16 +532,11 @@ class HearingAccessService(gatt.TemplateService):
542
532
  self.active_preset_index = index
543
533
  await self.notify_active_preset()
544
534
 
545
- async def _on_set_active_preset(
546
- self, connection: Optional[Connection], value: bytes
547
- ):
548
- await self.set_active_preset(connection, value)
535
+ async def _on_set_active_preset(self, _: Connection, value: bytes):
536
+ await self.set_active_preset(value)
549
537
 
550
- async def set_next_or_previous_preset(
551
- self, connection: Optional[Connection], is_previous
552
- ):
538
+ async def set_next_or_previous_preset(self, is_previous):
553
539
  '''Set the next or the previous preset as active'''
554
- assert connection
555
540
 
556
541
  if self.active_preset_index == 0x00:
557
542
  raise att.ATT_Error(ErrorCode.PRESET_OPERATION_NOT_POSSIBLE)
@@ -580,48 +565,47 @@ class HearingAccessService(gatt.TemplateService):
580
565
  self.active_preset_index = first_preset.index
581
566
  await self.notify_active_preset()
582
567
 
583
- async def _on_set_next_preset(
584
- self, connection: Optional[Connection], __value__: bytes
585
- ) -> None:
586
- await self.set_next_or_previous_preset(connection, False)
568
+ async def _on_set_next_preset(self, _: Connection, __value__: bytes) -> None:
569
+ await self.set_next_or_previous_preset(False)
587
570
 
588
- async def _on_set_previous_preset(
589
- self, connection: Optional[Connection], __value__: bytes
590
- ) -> None:
591
- await self.set_next_or_previous_preset(connection, True)
571
+ async def _on_set_previous_preset(self, _: Connection, __value__: bytes) -> None:
572
+ await self.set_next_or_previous_preset(True)
592
573
 
593
574
  async def _on_set_active_preset_synchronized_locally(
594
- self, connection: Optional[Connection], value: bytes
575
+ self, _: Connection, value: bytes
595
576
  ):
596
577
  if (
597
578
  self.server_features.preset_synchronization_support
598
- == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
579
+ == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
599
580
  ):
600
581
  raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
601
- await self.set_active_preset(connection, value)
602
- # TODO (low priority) inform other server of the change
582
+ await self.set_active_preset(value)
583
+ if self.other_server_in_binaural_set:
584
+ await self.other_server_in_binaural_set.set_active_preset(value)
603
585
 
604
586
  async def _on_set_next_preset_synchronized_locally(
605
- self, connection: Optional[Connection], __value__: bytes
587
+ self, _: Connection, __value__: bytes
606
588
  ):
607
589
  if (
608
590
  self.server_features.preset_synchronization_support
609
- == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
591
+ == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
610
592
  ):
611
593
  raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
612
- await self.set_next_or_previous_preset(connection, False)
613
- # TODO (low priority) inform other server of the change
594
+ await self.set_next_or_previous_preset(False)
595
+ if self.other_server_in_binaural_set:
596
+ await self.other_server_in_binaural_set.set_next_or_previous_preset(False)
614
597
 
615
598
  async def _on_set_previous_preset_synchronized_locally(
616
- self, connection: Optional[Connection], __value__: bytes
599
+ self, _: Connection, __value__: bytes
617
600
  ):
618
601
  if (
619
602
  self.server_features.preset_synchronization_support
620
- == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_SUPPORTED
603
+ == PresetSynchronizationSupport.PRESET_SYNCHRONIZATION_IS_NOT_SUPPORTED
621
604
  ):
622
605
  raise att.ATT_Error(ErrorCode.PRESET_SYNCHRONIZATION_NOT_SUPPORTED)
623
- await self.set_next_or_previous_preset(connection, True)
624
- # TODO (low priority) inform other server of the change
606
+ await self.set_next_or_previous_preset(True)
607
+ if self.other_server_in_binaural_set:
608
+ await self.other_server_in_binaural_set.set_next_or_previous_preset(True)
625
609
 
626
610
 
627
611
  # -----------------------------------------------------------------------------
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
  import dataclasses
20
20
  import enum
21
21
  import struct
22
- from typing import Any, List, Type
22
+ from typing import Any
23
23
  from typing_extensions import Self
24
24
 
25
25
  from bumble.profiles import bap
@@ -108,13 +108,13 @@ class Metadata:
108
108
  return self.data
109
109
 
110
110
  @classmethod
111
- def from_bytes(cls: Type[Self], data: bytes) -> Self:
111
+ def from_bytes(cls: type[Self], data: bytes) -> Self:
112
112
  return cls(tag=Metadata.Tag(data[0]), data=data[1:])
113
113
 
114
114
  def __bytes__(self) -> bytes:
115
115
  return bytes([len(self.data) + 1, self.tag]) + self.data
116
116
 
117
- entries: List[Entry] = dataclasses.field(default_factory=list)
117
+ entries: list[Entry] = dataclasses.field(default_factory=list)
118
118
 
119
119
  def pretty_print(self, indent: str) -> str:
120
120
  """Convenience method to generate a string with one key-value pair per line."""
@@ -140,7 +140,7 @@ class Metadata:
140
140
  )
141
141
 
142
142
  @classmethod
143
- def from_bytes(cls: Type[Self], data: bytes) -> Self:
143
+ def from_bytes(cls: type[Self], data: bytes) -> Self:
144
144
  entries = []
145
145
  offset = 0
146
146
  length = len(data)
bumble/profiles/mcp.py CHANGED
@@ -29,7 +29,7 @@ from bumble import gatt
29
29
  from bumble import gatt_client
30
30
  from bumble import utils
31
31
 
32
- from typing import Type, Optional, ClassVar, Dict, TYPE_CHECKING
32
+ from typing import Optional, ClassVar, TYPE_CHECKING
33
33
  from typing_extensions import Self
34
34
 
35
35
  # -----------------------------------------------------------------------------
@@ -167,7 +167,7 @@ class ObjectId(int):
167
167
  '''See Media Control Service 4.4.2. Object ID field.'''
168
168
 
169
169
  @classmethod
170
- def create_from_bytes(cls: Type[Self], data: bytes) -> Self:
170
+ def create_from_bytes(cls: type[Self], data: bytes) -> Self:
171
171
  return cls(int.from_bytes(data, byteorder='little', signed=False))
172
172
 
173
173
  def __bytes__(self) -> bytes:
@@ -182,7 +182,7 @@ class GroupObjectType:
182
182
  object_id: ObjectId
183
183
 
184
184
  @classmethod
185
- def from_bytes(cls: Type[Self], data: bytes) -> Self:
185
+ def from_bytes(cls: type[Self], data: bytes) -> Self:
186
186
  return cls(
187
187
  object_type=ObjectType(data[0]),
188
188
  object_id=ObjectId.create_from_bytes(data[1:]),
@@ -287,11 +287,8 @@ class MediaControlService(gatt.TemplateService):
287
287
  )
288
288
 
289
289
  async def on_media_control_point(
290
- self, connection: Optional[device.Connection], data: bytes
290
+ self, connection: device.Connection, data: bytes
291
291
  ) -> None:
292
- if not connection:
293
- raise core.InvalidStateError()
294
-
295
292
  opcode = MediaControlPointOpcode(data[0])
296
293
 
297
294
  await connection.device.notify_subscriber(
@@ -313,7 +310,7 @@ class MediaControlServiceProxy(
313
310
  ):
314
311
  SERVICE_CLASS = MediaControlService
315
312
 
316
- _CHARACTERISTICS: ClassVar[Dict[str, core.UUID]] = {
313
+ _CHARACTERISTICS: ClassVar[dict[str, core.UUID]] = {
317
314
  'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
318
315
  'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC,
319
316
  'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC,
@@ -338,6 +335,12 @@ class MediaControlServiceProxy(
338
335
  'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
339
336
  }
340
337
 
338
+ EVENT_MEDIA_STATE = "media_state"
339
+ EVENT_TRACK_CHANGED = "track_changed"
340
+ EVENT_TRACK_TITLE = "track_title"
341
+ EVENT_TRACK_DURATION = "track_duration"
342
+ EVENT_TRACK_POSITION = "track_position"
343
+
341
344
  media_player_name: Optional[gatt_client.CharacteristicProxy[bytes]] = None
342
345
  media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
343
346
  media_player_icon_url: Optional[gatt_client.CharacteristicProxy[bytes]] = None
@@ -432,20 +435,20 @@ class MediaControlServiceProxy(
432
435
  self.media_control_point_notifications.put_nowait(data)
433
436
 
434
437
  def _on_media_state(self, data: bytes) -> None:
435
- self.emit('media_state', MediaState(data[0]))
438
+ self.emit(self.EVENT_MEDIA_STATE, MediaState(data[0]))
436
439
 
437
440
  def _on_track_changed(self, data: bytes) -> None:
438
441
  del data
439
- self.emit('track_changed')
442
+ self.emit(self.EVENT_TRACK_CHANGED)
440
443
 
441
444
  def _on_track_title(self, data: bytes) -> None:
442
- self.emit('track_title', data.decode("utf-8"))
445
+ self.emit(self.EVENT_TRACK_TITLE, data.decode("utf-8"))
443
446
 
444
447
  def _on_track_duration(self, data: bytes) -> None:
445
- self.emit('track_duration', struct.unpack_from('<i', data)[0])
448
+ self.emit(self.EVENT_TRACK_DURATION, struct.unpack_from('<i', data)[0])
446
449
 
447
450
  def _on_track_position(self, data: bytes) -> None:
448
- self.emit('track_position', struct.unpack_from('<i', data)[0])
451
+ self.emit(self.EVENT_TRACK_POSITION, struct.unpack_from('<i', data)[0])
449
452
 
450
453
 
451
454
  class GenericMediaControlServiceProxy(MediaControlServiceProxy):
bumble/profiles/vcs.py CHANGED
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
  import dataclasses
21
21
  import enum
22
22
 
23
- from typing import Optional, Sequence
23
+ from typing import Sequence
24
24
 
25
25
  from bumble import att
26
26
  from bumble import utils
@@ -91,6 +91,8 @@ class VolumeState:
91
91
  class VolumeControlService(gatt.TemplateService):
92
92
  UUID = gatt.GATT_VOLUME_CONTROL_SERVICE
93
93
 
94
+ EVENT_VOLUME_STATE_CHANGE = "volume_state_change"
95
+
94
96
  volume_state: gatt.Characteristic[bytes]
95
97
  volume_control_point: gatt.Characteristic[bytes]
96
98
  volume_flags: gatt.Characteristic[bytes]
@@ -144,14 +146,12 @@ class VolumeControlService(gatt.TemplateService):
144
146
  included_services=list(included_services),
145
147
  )
146
148
 
147
- def _on_read_volume_state(self, _connection: Optional[device.Connection]) -> bytes:
149
+ def _on_read_volume_state(self, _connection: device.Connection) -> bytes:
148
150
  return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
149
151
 
150
152
  def _on_write_volume_control_point(
151
- self, connection: Optional[device.Connection], value: bytes
153
+ self, connection: device.Connection, value: bytes
152
154
  ) -> None:
153
- assert connection
154
-
155
155
  opcode = VolumeControlPointOpcode(value[0])
156
156
  change_counter = value[1]
157
157
 
@@ -161,12 +161,10 @@ class VolumeControlService(gatt.TemplateService):
161
161
  handler = getattr(self, '_on_' + opcode.name.lower())
162
162
  if handler(*value[2:]):
163
163
  self.change_counter = (self.change_counter + 1) % 256
164
- utils.cancel_on_event(
165
- connection,
166
- 'disconnection',
167
- connection.device.notify_subscribers(attribute=self.volume_state),
164
+ connection.cancel_on_disconnection(
165
+ connection.device.notify_subscribers(attribute=self.volume_state)
168
166
  )
169
- self.emit('volume_state_change')
167
+ self.emit(self.EVENT_VOLUME_STATE_CHANGE)
170
168
 
171
169
  def _on_relative_volume_down(self) -> bool:
172
170
  old_volume = self.volume_setting
bumble/profiles/vocs.py CHANGED
@@ -86,7 +86,7 @@ class VolumeOffsetState:
86
86
  assert self.attribute is not None
87
87
  await connection.device.notify_subscribers(attribute=self.attribute)
88
88
 
89
- def on_read(self, _connection: Optional[Connection]) -> bytes:
89
+ def on_read(self, _connection: Connection) -> bytes:
90
90
  return bytes(self)
91
91
 
92
92
 
@@ -103,11 +103,10 @@ class VocsAudioLocation:
103
103
  audio_location = AudioLocation(struct.unpack('<I', data)[0])
104
104
  return cls(audio_location)
105
105
 
106
- def on_read(self, _connection: Optional[Connection]) -> bytes:
106
+ def on_read(self, _connection: Connection) -> bytes:
107
107
  return bytes(self)
108
108
 
109
- async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
110
- assert connection
109
+ async def on_write(self, connection: Connection, value: bytes) -> None:
111
110
  assert self.attribute
112
111
 
113
112
  self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
@@ -118,8 +117,7 @@ class VocsAudioLocation:
118
117
  class VolumeOffsetControlPoint:
119
118
  volume_offset_state: VolumeOffsetState
120
119
 
121
- async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
122
- assert connection
120
+ async def on_write(self, connection: Connection, value: bytes) -> None:
123
121
 
124
122
  opcode = value[0]
125
123
  if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
@@ -159,11 +157,10 @@ class AudioOutputDescription:
159
157
  def __bytes__(self) -> bytes:
160
158
  return self.audio_output_description.encode('utf-8')
161
159
 
162
- def on_read(self, _connection: Optional[Connection]) -> bytes:
160
+ def on_read(self, _connection: Connection) -> bytes:
163
161
  return bytes(self)
164
162
 
165
- async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
166
- assert connection
163
+ async def on_write(self, connection: Connection, value: bytes) -> None:
167
164
  assert self.attribute
168
165
 
169
166
  self.audio_output_description = value.decode('utf-8')
bumble/rfcomm.py CHANGED
@@ -22,7 +22,7 @@ import asyncio
22
22
  import collections
23
23
  import dataclasses
24
24
  import enum
25
- from typing import Callable, Dict, List, Optional, Tuple, Union, TYPE_CHECKING
25
+ from typing import Callable, Optional, Union, TYPE_CHECKING
26
26
  from typing_extensions import Self
27
27
 
28
28
 
@@ -123,7 +123,7 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
123
123
  # -----------------------------------------------------------------------------
124
124
  def make_service_sdp_records(
125
125
  service_record_handle: int, channel: int, uuid: Optional[UUID] = None
126
- ) -> List[sdp.ServiceAttribute]:
126
+ ) -> list[sdp.ServiceAttribute]:
127
127
  """
128
128
  Create SDP records for an RFComm service given a channel number and an
129
129
  optional UUID. A Service Class Attribute is included only if the UUID is not None.
@@ -169,7 +169,7 @@ def make_service_sdp_records(
169
169
 
170
170
 
171
171
  # -----------------------------------------------------------------------------
172
- async def find_rfcomm_channels(connection: Connection) -> Dict[int, List[UUID]]:
172
+ async def find_rfcomm_channels(connection: Connection) -> dict[int, list[UUID]]:
173
173
  """Searches all RFCOMM channels and their associated UUID from SDP service records.
174
174
 
175
175
  Args:
@@ -188,7 +188,7 @@ async def find_rfcomm_channels(connection: Connection) -> Dict[int, List[UUID]]:
188
188
  ],
189
189
  )
190
190
  for attribute_lists in search_result:
191
- service_classes: List[UUID] = []
191
+ service_classes: list[UUID] = []
192
192
  channel: Optional[int] = None
193
193
  for attribute in attribute_lists:
194
194
  # The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
@@ -275,7 +275,7 @@ class RFCOMM_Frame:
275
275
  self.fcs = compute_fcs(bytes([self.address, self.control]) + self.length)
276
276
 
277
277
  @staticmethod
278
- def parse_mcc(data) -> Tuple[int, bool, bytes]:
278
+ def parse_mcc(data) -> tuple[int, bool, bytes]:
279
279
  mcc_type = data[0] >> 2
280
280
  c_r = bool((data[0] >> 1) & 1)
281
281
  length = data[1]
@@ -442,6 +442,9 @@ class RFCOMM_MCC_MSC:
442
442
 
443
443
  # -----------------------------------------------------------------------------
444
444
  class DLC(utils.EventEmitter):
445
+ EVENT_OPEN = "open"
446
+ EVENT_CLOSE = "close"
447
+
445
448
  class State(enum.IntEnum):
446
449
  INIT = 0x00
447
450
  CONNECTING = 0x01
@@ -529,7 +532,7 @@ class DLC(utils.EventEmitter):
529
532
  self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
530
533
 
531
534
  self.change_state(DLC.State.CONNECTED)
532
- self.emit('open')
535
+ self.emit(self.EVENT_OPEN)
533
536
 
534
537
  def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
535
538
  if self.state == DLC.State.CONNECTING:
@@ -550,7 +553,7 @@ class DLC(utils.EventEmitter):
550
553
  self.disconnection_result.set_result(None)
551
554
  self.disconnection_result = None
552
555
  self.multiplexer.on_dlc_disconnection(self)
553
- self.emit('close')
556
+ self.emit(self.EVENT_CLOSE)
554
557
  else:
555
558
  logger.warning(
556
559
  color(
@@ -733,7 +736,7 @@ class DLC(utils.EventEmitter):
733
736
  self.disconnection_result.cancel()
734
737
  self.disconnection_result = None
735
738
  self.change_state(DLC.State.RESET)
736
- self.emit('close')
739
+ self.emit(self.EVENT_CLOSE)
737
740
 
738
741
  def __str__(self) -> str:
739
742
  return (
@@ -763,11 +766,13 @@ class Multiplexer(utils.EventEmitter):
763
766
  DISCONNECTED = 0x05
764
767
  RESET = 0x06
765
768
 
769
+ EVENT_DLC = "dlc"
770
+
766
771
  connection_result: Optional[asyncio.Future]
767
772
  disconnection_result: Optional[asyncio.Future]
768
773
  open_result: Optional[asyncio.Future]
769
- acceptor: Optional[Callable[[int], Optional[Tuple[int, int]]]]
770
- dlcs: Dict[int, DLC]
774
+ acceptor: Optional[Callable[[int], Optional[tuple[int, int]]]]
775
+ dlcs: dict[int, DLC]
771
776
 
772
777
  def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
773
778
  super().__init__()
@@ -785,7 +790,7 @@ class Multiplexer(utils.EventEmitter):
785
790
  # Become a sink for the L2CAP channel
786
791
  l2cap_channel.sink = self.on_pdu
787
792
 
788
- l2cap_channel.on('close', self.on_l2cap_channel_close)
793
+ l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
789
794
 
790
795
  def change_state(self, new_state: State) -> None:
791
796
  logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
@@ -901,7 +906,7 @@ class Multiplexer(utils.EventEmitter):
901
906
  self.dlcs[pn.dlci] = dlc
902
907
 
903
908
  # Re-emit the handshake completion event
904
- dlc.on('open', lambda: self.emit('dlc', dlc))
909
+ dlc.on(dlc.EVENT_OPEN, lambda: self.emit(self.EVENT_DLC, dlc))
905
910
 
906
911
  # Respond to complete the handshake
907
912
  dlc.accept()
@@ -1076,13 +1081,15 @@ class Client:
1076
1081
 
1077
1082
  # -----------------------------------------------------------------------------
1078
1083
  class Server(utils.EventEmitter):
1084
+ EVENT_START = "start"
1085
+
1079
1086
  def __init__(
1080
1087
  self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
1081
1088
  ) -> None:
1082
1089
  super().__init__()
1083
1090
  self.device = device
1084
- self.acceptors: Dict[int, Callable[[DLC], None]] = {}
1085
- self.dlc_configs: Dict[int, Tuple[int, int]] = {}
1091
+ self.acceptors: dict[int, Callable[[DLC], None]] = {}
1092
+ self.dlc_configs: dict[int, tuple[int, int]] = {}
1086
1093
 
1087
1094
  # Register ourselves with the L2CAP channel manager
1088
1095
  self.l2cap_server = device.create_l2cap_server(
@@ -1122,7 +1129,9 @@ class Server(utils.EventEmitter):
1122
1129
 
1123
1130
  def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
1124
1131
  logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
1125
- l2cap_channel.on('open', lambda: self.on_l2cap_channel_open(l2cap_channel))
1132
+ l2cap_channel.on(
1133
+ l2cap_channel.EVENT_OPEN, lambda: self.on_l2cap_channel_open(l2cap_channel)
1134
+ )
1126
1135
 
1127
1136
  def on_l2cap_channel_open(self, l2cap_channel: l2cap.ClassicChannel) -> None:
1128
1137
  logger.debug(f'$$$ L2CAP channel open: {l2cap_channel}')
@@ -1130,12 +1139,12 @@ class Server(utils.EventEmitter):
1130
1139
  # Create a new multiplexer for the channel
1131
1140
  multiplexer = Multiplexer(l2cap_channel, Multiplexer.Role.RESPONDER)
1132
1141
  multiplexer.acceptor = self.accept_dlc
1133
- multiplexer.on('dlc', self.on_dlc)
1142
+ multiplexer.on(multiplexer.EVENT_DLC, self.on_dlc)
1134
1143
 
1135
1144
  # Notify
1136
- self.emit('start', multiplexer)
1145
+ self.emit(self.EVENT_START, multiplexer)
1137
1146
 
1138
- def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
1147
+ def accept_dlc(self, channel_number: int) -> Optional[tuple[int, int]]:
1139
1148
  return self.dlc_configs.get(channel_number)
1140
1149
 
1141
1150
  def on_dlc(self, dlc: DLC) -> None:
bumble/rtp.py CHANGED
@@ -17,7 +17,6 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
19
  import struct
20
- from typing import List
21
20
 
22
21
 
23
22
  # -----------------------------------------------------------------------------
@@ -60,7 +59,7 @@ class MediaPacket:
60
59
  sequence_number: int,
61
60
  timestamp: int,
62
61
  ssrc: int,
63
- csrc_list: List[int],
62
+ csrc_list: list[int],
64
63
  payload_type: int,
65
64
  payload: bytes,
66
65
  ) -> None:
bumble/sdp.py CHANGED
@@ -19,7 +19,7 @@ from __future__ import annotations
19
19
  import asyncio
20
20
  import logging
21
21
  import struct
22
- from typing import Iterable, NewType, Optional, Union, Sequence, Type, TYPE_CHECKING
22
+ from typing import Iterable, NewType, Optional, Union, Sequence, TYPE_CHECKING
23
23
  from typing_extensions import Self
24
24
 
25
25
  from bumble import core, l2cap
@@ -547,7 +547,7 @@ class SDP_PDU:
547
547
  SDP_SERVICE_ATTRIBUTE_REQUEST: SDP_SERVICE_ATTRIBUTE_RESPONSE,
548
548
  SDP_SERVICE_SEARCH_ATTRIBUTE_REQUEST: SDP_SERVICE_SEARCH_ATTRIBUTE_RESPONSE,
549
549
  }
550
- sdp_pdu_classes: dict[int, Type[SDP_PDU]] = {}
550
+ sdp_pdu_classes: dict[int, type[SDP_PDU]] = {}
551
551
  name = None
552
552
  pdu_id = 0
553
553