bumble 0.0.220__py3-none-any.whl → 0.0.221__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 (102) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +5 -5
  3. bumble/apps/auracast.py +746 -473
  4. bumble/apps/bench.py +4 -5
  5. bumble/apps/console.py +5 -10
  6. bumble/apps/controller_info.py +12 -7
  7. bumble/apps/controller_loopback.py +1 -2
  8. bumble/apps/device_info.py +2 -3
  9. bumble/apps/gatt_dump.py +0 -1
  10. bumble/apps/lea_unicast/app.py +1 -1
  11. bumble/apps/pair.py +49 -46
  12. bumble/apps/pandora_server.py +2 -2
  13. bumble/apps/player/player.py +10 -12
  14. bumble/apps/rfcomm_bridge.py +10 -11
  15. bumble/apps/scan.py +1 -3
  16. bumble/apps/speaker/speaker.py +3 -4
  17. bumble/at.py +4 -5
  18. bumble/att.py +91 -25
  19. bumble/audio/io.py +5 -3
  20. bumble/avc.py +1 -2
  21. bumble/avctp.py +2 -3
  22. bumble/avdtp.py +53 -57
  23. bumble/avrcp.py +25 -27
  24. bumble/codecs.py +15 -15
  25. bumble/colors.py +7 -8
  26. bumble/controller.py +663 -391
  27. bumble/core.py +41 -49
  28. bumble/crypto/__init__.py +2 -1
  29. bumble/crypto/builtin.py +2 -8
  30. bumble/data_types.py +2 -1
  31. bumble/decoder.py +2 -3
  32. bumble/device.py +171 -142
  33. bumble/drivers/__init__.py +3 -2
  34. bumble/drivers/intel.py +6 -8
  35. bumble/drivers/rtk.py +1 -1
  36. bumble/gatt.py +9 -9
  37. bumble/gatt_adapters.py +6 -6
  38. bumble/gatt_client.py +110 -60
  39. bumble/gatt_server.py +209 -139
  40. bumble/hci.py +87 -74
  41. bumble/helpers.py +5 -5
  42. bumble/hfp.py +27 -26
  43. bumble/hid.py +9 -9
  44. bumble/host.py +44 -50
  45. bumble/keys.py +17 -17
  46. bumble/l2cap.py +1015 -218
  47. bumble/link.py +26 -159
  48. bumble/ll.py +200 -0
  49. bumble/pairing.py +14 -15
  50. bumble/pandora/__init__.py +2 -2
  51. bumble/pandora/device.py +6 -4
  52. bumble/pandora/host.py +19 -10
  53. bumble/pandora/l2cap.py +8 -9
  54. bumble/pandora/security.py +18 -16
  55. bumble/pandora/utils.py +4 -4
  56. bumble/profiles/aics.py +6 -8
  57. bumble/profiles/ams.py +3 -5
  58. bumble/profiles/ancs.py +11 -11
  59. bumble/profiles/ascs.py +5 -5
  60. bumble/profiles/asha.py +10 -9
  61. bumble/profiles/bass.py +9 -3
  62. bumble/profiles/battery_service.py +1 -2
  63. bumble/profiles/csip.py +9 -10
  64. bumble/profiles/device_information_service.py +16 -17
  65. bumble/profiles/gap.py +3 -4
  66. bumble/profiles/gatt_service.py +0 -1
  67. bumble/profiles/gmap.py +12 -13
  68. bumble/profiles/hap.py +3 -3
  69. bumble/profiles/heart_rate_service.py +7 -8
  70. bumble/profiles/le_audio.py +1 -1
  71. bumble/profiles/mcp.py +28 -28
  72. bumble/profiles/pacs.py +13 -17
  73. bumble/profiles/pbp.py +16 -0
  74. bumble/profiles/vcs.py +2 -2
  75. bumble/profiles/vocs.py +6 -9
  76. bumble/rfcomm.py +19 -18
  77. bumble/sdp.py +12 -11
  78. bumble/smp.py +20 -30
  79. bumble/snoop.py +2 -1
  80. bumble/tools/generate_company_id_list.py +1 -1
  81. bumble/tools/intel_util.py +2 -2
  82. bumble/tools/rtk_fw_download.py +1 -1
  83. bumble/tools/rtk_util.py +1 -1
  84. bumble/transport/__init__.py +1 -2
  85. bumble/transport/android_emulator.py +2 -3
  86. bumble/transport/android_netsim.py +49 -40
  87. bumble/transport/common.py +9 -9
  88. bumble/transport/file.py +1 -2
  89. bumble/transport/hci_socket.py +2 -3
  90. bumble/transport/pty.py +3 -5
  91. bumble/transport/pyusb.py +8 -5
  92. bumble/transport/serial.py +1 -2
  93. bumble/transport/vhci.py +1 -2
  94. bumble/transport/ws_server.py +2 -3
  95. bumble/utils.py +22 -9
  96. bumble/vendor/android/hci.py +4 -2
  97. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/METADATA +3 -2
  98. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/RECORD +102 -101
  99. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/WHEEL +0 -0
  100. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/entry_points.txt +0 -0
  101. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/licenses/LICENSE +0 -0
  102. {bumble-0.0.220.dist-info → bumble-0.0.221.dist-info}/top_level.txt +0 -0
bumble/profiles/hap.py CHANGED
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
  import asyncio
21
21
  import logging
22
22
  from dataclasses import dataclass, field
23
- from typing import Any, Optional, Union
23
+ from typing import Any
24
24
 
25
25
  from bumble import att, gatt, gatt_adapters, gatt_client, utils
26
26
  from bumble.core import InvalidArgumentError, InvalidStateError
@@ -145,7 +145,7 @@ class PresetChangedOperation:
145
145
  return bytes([self.prev_index]) + bytes(self.preset_record)
146
146
 
147
147
  change_id: ChangeId
148
- additional_parameters: Union[Generic, int]
148
+ additional_parameters: Generic | int
149
149
 
150
150
  def to_bytes(self, is_last: bool) -> bytes:
151
151
  if isinstance(self.additional_parameters, PresetChangedOperation.Generic):
@@ -235,7 +235,7 @@ class HearingAccessService(gatt.TemplateService):
235
235
  preset_records: dict[int, PresetRecord] # key is the preset index
236
236
  read_presets_request_in_progress: bool
237
237
 
238
- other_server_in_binaural_set: Optional[HearingAccessService] = None
238
+ other_server_in_binaural_set: HearingAccessService | None = None
239
239
 
240
240
  preset_changed_operations_history_per_device: dict[
241
241
  Address, list[PresetChangedOperation]
@@ -20,7 +20,6 @@ from __future__ import annotations
20
20
 
21
21
  import struct
22
22
  from enum import IntEnum
23
- from typing import Optional
24
23
 
25
24
  from bumble import core
26
25
  from bumble.att import ATT_Error
@@ -207,13 +206,13 @@ class HeartRateService(TemplateService):
207
206
  class HeartRateServiceProxy(ProfileServiceProxy):
208
207
  SERVICE_CLASS = HeartRateService
209
208
 
210
- heart_rate_measurement: Optional[
211
- CharacteristicProxy[HeartRateService.HeartRateMeasurement]
212
- ]
213
- body_sensor_location: Optional[
214
- CharacteristicProxy[HeartRateService.BodySensorLocation]
215
- ]
216
- heart_rate_control_point: Optional[CharacteristicProxy[int]]
209
+ heart_rate_measurement: (
210
+ CharacteristicProxy[HeartRateService.HeartRateMeasurement] | None
211
+ )
212
+ body_sensor_location: (
213
+ CharacteristicProxy[HeartRateService.BodySensorLocation] | None
214
+ )
215
+ heart_rate_control_point: CharacteristicProxy[int] | None
217
216
 
218
217
  def __init__(self, service_proxy):
219
218
  self.service_proxy = service_proxy
@@ -137,7 +137,7 @@ class Metadata:
137
137
  values.append(str(decoded))
138
138
 
139
139
  return '\n'.join(
140
- f'{indent}{key}: {" " * (max_key_length-len(key))}{value}'
140
+ f'{indent}{key}: {" " * (max_key_length - len(key))}{value}'
141
141
  for key, value in zip(keys, values)
142
142
  )
143
143
 
bumble/profiles/mcp.py CHANGED
@@ -22,7 +22,7 @@ import asyncio
22
22
  import dataclasses
23
23
  import enum
24
24
  import struct
25
- from typing import TYPE_CHECKING, ClassVar, Optional
25
+ from typing import TYPE_CHECKING, ClassVar
26
26
 
27
27
  from typing_extensions import Self
28
28
 
@@ -196,7 +196,7 @@ class MediaControlService(gatt.TemplateService):
196
196
 
197
197
  UUID = gatt.GATT_MEDIA_CONTROL_SERVICE
198
198
 
199
- def __init__(self, media_player_name: Optional[str] = None) -> None:
199
+ def __init__(self, media_player_name: str | None = None) -> None:
200
200
  self.track_position = 0
201
201
 
202
202
  self.media_player_name_characteristic = gatt.Characteristic(
@@ -337,32 +337,32 @@ class MediaControlServiceProxy(
337
337
  EVENT_TRACK_DURATION = "track_duration"
338
338
  EVENT_TRACK_POSITION = "track_position"
339
339
 
340
- media_player_name: Optional[gatt_client.CharacteristicProxy[bytes]] = None
341
- media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
342
- media_player_icon_url: Optional[gatt_client.CharacteristicProxy[bytes]] = None
343
- track_changed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
344
- track_title: Optional[gatt_client.CharacteristicProxy[bytes]] = None
345
- track_duration: Optional[gatt_client.CharacteristicProxy[bytes]] = None
346
- track_position: Optional[gatt_client.CharacteristicProxy[bytes]] = None
347
- playback_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
348
- seeking_speed: Optional[gatt_client.CharacteristicProxy[bytes]] = None
349
- current_track_segments_object_id: Optional[
350
- gatt_client.CharacteristicProxy[bytes]
351
- ] = None
352
- current_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
353
- next_track_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
354
- parent_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
355
- current_group_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
356
- playing_order: Optional[gatt_client.CharacteristicProxy[bytes]] = None
357
- playing_orders_supported: Optional[gatt_client.CharacteristicProxy[bytes]] = None
358
- media_state: Optional[gatt_client.CharacteristicProxy[bytes]] = None
359
- media_control_point: Optional[gatt_client.CharacteristicProxy[bytes]] = None
360
- media_control_point_opcodes_supported: Optional[
361
- gatt_client.CharacteristicProxy[bytes]
362
- ] = None
363
- search_control_point: Optional[gatt_client.CharacteristicProxy[bytes]] = None
364
- search_results_object_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
365
- content_control_id: Optional[gatt_client.CharacteristicProxy[bytes]] = None
340
+ media_player_name: gatt_client.CharacteristicProxy[bytes] | None = None
341
+ media_player_icon_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
342
+ media_player_icon_url: gatt_client.CharacteristicProxy[bytes] | None = None
343
+ track_changed: gatt_client.CharacteristicProxy[bytes] | None = None
344
+ track_title: gatt_client.CharacteristicProxy[bytes] | None = None
345
+ track_duration: gatt_client.CharacteristicProxy[bytes] | None = None
346
+ track_position: gatt_client.CharacteristicProxy[bytes] | None = None
347
+ playback_speed: gatt_client.CharacteristicProxy[bytes] | None = None
348
+ seeking_speed: gatt_client.CharacteristicProxy[bytes] | None = None
349
+ current_track_segments_object_id: gatt_client.CharacteristicProxy[bytes] | None = (
350
+ None
351
+ )
352
+ current_track_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
353
+ next_track_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
354
+ parent_group_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
355
+ current_group_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
356
+ playing_order: gatt_client.CharacteristicProxy[bytes] | None = None
357
+ playing_orders_supported: gatt_client.CharacteristicProxy[bytes] | None = None
358
+ media_state: gatt_client.CharacteristicProxy[bytes] | None = None
359
+ media_control_point: gatt_client.CharacteristicProxy[bytes] | None = None
360
+ media_control_point_opcodes_supported: (
361
+ gatt_client.CharacteristicProxy[bytes] | None
362
+ ) = None
363
+ search_control_point: gatt_client.CharacteristicProxy[bytes] | None = None
364
+ search_results_object_id: gatt_client.CharacteristicProxy[bytes] | None = None
365
+ content_control_id: gatt_client.CharacteristicProxy[bytes] | None = None
366
366
 
367
367
  if TYPE_CHECKING:
368
368
  media_control_point_notifications: asyncio.Queue[bytes]
bumble/profiles/pacs.py CHANGED
@@ -21,7 +21,7 @@ from __future__ import annotations
21
21
  import dataclasses
22
22
  import logging
23
23
  import struct
24
- from typing import Optional, Sequence, Union
24
+ from collections.abc import Sequence
25
25
 
26
26
  from bumble import gatt, gatt_adapters, gatt_client, hci
27
27
  from bumble.profiles import le_audio
@@ -39,7 +39,7 @@ class PacRecord:
39
39
  '''Published Audio Capabilities Service, Table 3.2/3.4.'''
40
40
 
41
41
  coding_format: hci.CodingFormat
42
- codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
42
+ codec_specific_capabilities: CodecSpecificCapabilities | bytes
43
43
  metadata: le_audio.Metadata = dataclasses.field(default_factory=le_audio.Metadata)
44
44
 
45
45
  @classmethod
@@ -56,7 +56,7 @@ class PacRecord:
56
56
  offset += 1
57
57
  metadata = le_audio.Metadata.from_bytes(data[offset : offset + metadata_size])
58
58
 
59
- codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
59
+ codec_specific_capabilities: CodecSpecificCapabilities | bytes
60
60
  if coding_format.codec_id == hci.CodecID.VENDOR_SPECIFIC:
61
61
  codec_specific_capabilities = codec_specific_capabilities_bytes
62
62
  else:
@@ -101,10 +101,10 @@ class PacRecord:
101
101
  class PublishedAudioCapabilitiesService(gatt.TemplateService):
102
102
  UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
103
103
 
104
- sink_pac: Optional[gatt.Characteristic[bytes]]
105
- sink_audio_locations: Optional[gatt.Characteristic[bytes]]
106
- source_pac: Optional[gatt.Characteristic[bytes]]
107
- source_audio_locations: Optional[gatt.Characteristic[bytes]]
104
+ sink_pac: gatt.Characteristic[bytes] | None
105
+ sink_audio_locations: gatt.Characteristic[bytes] | None
106
+ source_pac: gatt.Characteristic[bytes] | None
107
+ source_audio_locations: gatt.Characteristic[bytes] | None
108
108
  available_audio_contexts: gatt.Characteristic[bytes]
109
109
  supported_audio_contexts: gatt.Characteristic[bytes]
110
110
 
@@ -115,9 +115,9 @@ class PublishedAudioCapabilitiesService(gatt.TemplateService):
115
115
  available_source_context: ContextType,
116
116
  available_sink_context: ContextType,
117
117
  sink_pac: Sequence[PacRecord] = (),
118
- sink_audio_locations: Optional[AudioLocation] = None,
118
+ sink_audio_locations: AudioLocation | None = None,
119
119
  source_pac: Sequence[PacRecord] = (),
120
- source_audio_locations: Optional[AudioLocation] = None,
120
+ source_audio_locations: AudioLocation | None = None,
121
121
  ) -> None:
122
122
  characteristics = []
123
123
 
@@ -183,14 +183,10 @@ class PublishedAudioCapabilitiesService(gatt.TemplateService):
183
183
  class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
184
184
  SERVICE_CLASS = PublishedAudioCapabilitiesService
185
185
 
186
- sink_pac: Optional[gatt_client.CharacteristicProxy[list[PacRecord]]] = None
187
- sink_audio_locations: Optional[gatt_client.CharacteristicProxy[AudioLocation]] = (
188
- None
189
- )
190
- source_pac: Optional[gatt_client.CharacteristicProxy[list[PacRecord]]] = None
191
- source_audio_locations: Optional[gatt_client.CharacteristicProxy[AudioLocation]] = (
192
- None
193
- )
186
+ sink_pac: gatt_client.CharacteristicProxy[list[PacRecord]] | None = None
187
+ sink_audio_locations: gatt_client.CharacteristicProxy[AudioLocation] | None = None
188
+ source_pac: gatt_client.CharacteristicProxy[list[PacRecord]] | None = None
189
+ source_audio_locations: gatt_client.CharacteristicProxy[AudioLocation] | None = None
194
190
  available_audio_contexts: gatt_client.CharacteristicProxy[tuple[ContextType, ...]]
195
191
  supported_audio_contexts: gatt_client.CharacteristicProxy[tuple[ContextType, ...]]
196
192
 
bumble/profiles/pbp.py CHANGED
@@ -22,6 +22,7 @@ import enum
22
22
 
23
23
  from typing_extensions import Self
24
24
 
25
+ from bumble import core, data_types, gatt
25
26
  from bumble.profiles import le_audio
26
27
 
27
28
 
@@ -46,3 +47,18 @@ class PublicBroadcastAnnouncement:
46
47
  return cls(
47
48
  features=features, metadata=le_audio.Metadata.from_bytes(metadata_ltv)
48
49
  )
50
+
51
+ def get_advertising_data(self) -> bytes:
52
+ return bytes(
53
+ core.AdvertisingData(
54
+ [
55
+ data_types.ServiceData16BitUUID(
56
+ gatt.GATT_PUBLIC_BROADCAST_ANNOUNCEMENT_SERVICE, bytes(self)
57
+ )
58
+ ]
59
+ )
60
+ )
61
+
62
+ def __bytes__(self) -> bytes:
63
+ metadata_bytes = bytes(self.metadata)
64
+ return bytes([self.features, len(metadata_bytes)]) + metadata_bytes
bumble/profiles/vcs.py CHANGED
@@ -20,9 +20,9 @@ from __future__ import annotations
20
20
 
21
21
  import dataclasses
22
22
  import enum
23
- from typing import Sequence
23
+ from collections.abc import Sequence
24
24
 
25
- from bumble import att, device, gatt, gatt_adapters, gatt_client, utils
25
+ from bumble import att, device, gatt, gatt_adapters, gatt_client
26
26
 
27
27
  # -----------------------------------------------------------------------------
28
28
  # Constants
bumble/profiles/vocs.py CHANGED
@@ -18,7 +18,6 @@
18
18
  # -----------------------------------------------------------------------------
19
19
  import struct
20
20
  from dataclasses import dataclass
21
- from typing import Optional
22
21
 
23
22
  from bumble import utils
24
23
  from bumble.att import ATT_Error
@@ -69,7 +68,7 @@ class ErrorCode(utils.OpenIntEnum):
69
68
  class VolumeOffsetState:
70
69
  volume_offset: int = 0
71
70
  change_counter: int = 0
72
- attribute: Optional[Characteristic] = None
71
+ attribute: Characteristic | None = None
73
72
 
74
73
  def __bytes__(self) -> bytes:
75
74
  return struct.pack('<hB', self.volume_offset, self.change_counter)
@@ -93,7 +92,7 @@ class VolumeOffsetState:
93
92
  @dataclass
94
93
  class VocsAudioLocation:
95
94
  audio_location: AudioLocation = AudioLocation.NOT_ALLOWED
96
- attribute: Optional[Characteristic] = None
95
+ attribute: Characteristic | None = None
97
96
 
98
97
  def __bytes__(self) -> bytes:
99
98
  return struct.pack('<I', self.audio_location)
@@ -118,7 +117,6 @@ class VolumeOffsetControlPoint:
118
117
  volume_offset_state: VolumeOffsetState
119
118
 
120
119
  async def on_write(self, connection: Connection, value: bytes) -> None:
121
-
122
120
  opcode = value[0]
123
121
  if opcode != SetVolumeOffsetOpCode.SET_VOLUME_OFFSET:
124
122
  raise ATT_Error(ErrorCode.OPCODE_NOT_SUPPORTED)
@@ -148,7 +146,7 @@ class VolumeOffsetControlPoint:
148
146
  @dataclass
149
147
  class AudioOutputDescription:
150
148
  audio_output_description: str = ''
151
- attribute: Optional[Characteristic] = None
149
+ attribute: Characteristic | None = None
152
150
 
153
151
  @classmethod
154
152
  def from_bytes(cls, data: bytes):
@@ -173,11 +171,10 @@ class VolumeOffsetControlService(TemplateService):
173
171
 
174
172
  def __init__(
175
173
  self,
176
- volume_offset_state: Optional[VolumeOffsetState] = None,
177
- audio_location: Optional[VocsAudioLocation] = None,
178
- audio_output_description: Optional[AudioOutputDescription] = None,
174
+ volume_offset_state: VolumeOffsetState | None = None,
175
+ audio_location: VocsAudioLocation | None = None,
176
+ audio_output_description: AudioOutputDescription | None = None,
179
177
  ) -> None:
180
-
181
178
  self.volume_offset_state = (
182
179
  VolumeOffsetState() if volume_offset_state is None else volume_offset_state
183
180
  )
bumble/rfcomm.py CHANGED
@@ -22,7 +22,8 @@ import collections
22
22
  import dataclasses
23
23
  import enum
24
24
  import logging
25
- from typing import TYPE_CHECKING, Callable, Optional, Union
25
+ from collections.abc import Callable
26
+ from typing import TYPE_CHECKING
26
27
 
27
28
  from typing_extensions import Self
28
29
 
@@ -119,7 +120,7 @@ RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
119
120
 
120
121
  # -----------------------------------------------------------------------------
121
122
  def make_service_sdp_records(
122
- service_record_handle: int, channel: int, uuid: Optional[UUID] = None
123
+ service_record_handle: int, channel: int, uuid: UUID | None = None
123
124
  ) -> list[sdp.ServiceAttribute]:
124
125
  """
125
126
  Create SDP records for an RFComm service given a channel number and an
@@ -186,7 +187,7 @@ async def find_rfcomm_channels(connection: Connection) -> dict[int, list[UUID]]:
186
187
  )
187
188
  for attribute_lists in search_result:
188
189
  service_classes: list[UUID] = []
189
- channel: Optional[int] = None
190
+ channel: int | None = None
190
191
  for attribute in attribute_lists:
191
192
  # The layout is [[L2CAP_PROTOCOL], [RFCOMM_PROTOCOL, RFCOMM_CHANNEL]].
192
193
  if attribute.id == sdp.SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
@@ -207,7 +208,7 @@ async def find_rfcomm_channels(connection: Connection) -> dict[int, list[UUID]]:
207
208
  # -----------------------------------------------------------------------------
208
209
  async def find_rfcomm_channel_with_uuid(
209
210
  connection: Connection, uuid: str | UUID
210
- ) -> Optional[int]:
211
+ ) -> int | None:
211
212
  """Searches an RFCOMM channel associated with given UUID from service records.
212
213
 
213
214
  Args:
@@ -473,15 +474,15 @@ class DLC(utils.EventEmitter):
473
474
  self.state = DLC.State.INIT
474
475
  self.role = multiplexer.role
475
476
  self.c_r = 1 if self.role == Multiplexer.Role.INITIATOR else 0
476
- self.connection_result: Optional[asyncio.Future] = None
477
- self.disconnection_result: Optional[asyncio.Future] = None
477
+ self.connection_result: asyncio.Future | None = None
478
+ self.disconnection_result: asyncio.Future | None = None
478
479
  self.drained = asyncio.Event()
479
480
  self.drained.set()
480
481
  # Queued packets when sink is not set.
481
482
  self._enqueued_rx_packets: collections.deque[bytes] = collections.deque(
482
483
  maxlen=DEFAULT_RX_QUEUE_SIZE
483
484
  )
484
- self._sink: Optional[Callable[[bytes], None]] = None
485
+ self._sink: Callable[[bytes], None] | None = None
485
486
 
486
487
  # Compute the MTU
487
488
  max_overhead = 4 + 1 # header with 2-byte length + fcs
@@ -490,11 +491,11 @@ class DLC(utils.EventEmitter):
490
491
  )
491
492
 
492
493
  @property
493
- def sink(self) -> Optional[Callable[[bytes], None]]:
494
+ def sink(self) -> Callable[[bytes], None] | None:
494
495
  return self._sink
495
496
 
496
497
  @sink.setter
497
- def sink(self, sink: Optional[Callable[[bytes], None]]) -> None:
498
+ def sink(self, sink: Callable[[bytes], None] | None) -> None:
498
499
  self._sink = sink
499
500
  # Dump queued packets to sink
500
501
  if sink:
@@ -712,7 +713,7 @@ class DLC(utils.EventEmitter):
712
713
  self.drained.set()
713
714
 
714
715
  # Stream protocol
715
- def write(self, data: Union[bytes, str]) -> None:
716
+ def write(self, data: bytes | str) -> None:
716
717
  # We can only send bytes
717
718
  if not isinstance(data, bytes):
718
719
  if isinstance(data, str):
@@ -769,10 +770,10 @@ class Multiplexer(utils.EventEmitter):
769
770
 
770
771
  EVENT_DLC = "dlc"
771
772
 
772
- connection_result: Optional[asyncio.Future]
773
- disconnection_result: Optional[asyncio.Future]
774
- open_result: Optional[asyncio.Future]
775
- acceptor: Optional[Callable[[int], Optional[tuple[int, int]]]]
773
+ connection_result: asyncio.Future | None
774
+ disconnection_result: asyncio.Future | None
775
+ open_result: asyncio.Future | None
776
+ acceptor: Callable[[int], tuple[int, int] | None] | None
776
777
  dlcs: dict[int, DLC]
777
778
 
778
779
  def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
@@ -784,7 +785,7 @@ class Multiplexer(utils.EventEmitter):
784
785
  self.connection_result = None
785
786
  self.disconnection_result = None
786
787
  self.open_result = None
787
- self.open_pn: Optional[RFCOMM_MCC_PN] = None
788
+ self.open_pn: RFCOMM_MCC_PN | None = None
788
789
  self.open_rx_max_credits = 0
789
790
  self.acceptor = None
790
791
 
@@ -1031,8 +1032,8 @@ class Multiplexer(utils.EventEmitter):
1031
1032
 
1032
1033
  # -----------------------------------------------------------------------------
1033
1034
  class Client:
1034
- multiplexer: Optional[Multiplexer]
1035
- l2cap_channel: Optional[l2cap.ClassicChannel]
1035
+ multiplexer: Multiplexer | None
1036
+ l2cap_channel: l2cap.ClassicChannel | None
1036
1037
 
1037
1038
  def __init__(
1038
1039
  self, connection: Connection, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
@@ -1145,7 +1146,7 @@ class Server(utils.EventEmitter):
1145
1146
  # Notify
1146
1147
  self.emit(self.EVENT_START, multiplexer)
1147
1148
 
1148
- def accept_dlc(self, channel_number: int) -> Optional[tuple[int, int]]:
1149
+ def accept_dlc(self, channel_number: int) -> tuple[int, int] | None:
1149
1150
  return self.dlc_configs.get(channel_number)
1150
1151
 
1151
1152
  def on_dlc(self, dlc: DLC) -> None:
bumble/sdp.py CHANGED
@@ -20,7 +20,8 @@ from __future__ import annotations
20
20
  import asyncio
21
21
  import logging
22
22
  import struct
23
- from typing import TYPE_CHECKING, Iterable, NewType, Optional, Sequence, Union
23
+ from collections.abc import Iterable, Sequence
24
+ from typing import TYPE_CHECKING, NewType
24
25
 
25
26
  from typing_extensions import Self
26
27
 
@@ -497,7 +498,7 @@ class ServiceAttribute:
497
498
  @staticmethod
498
499
  def find_attribute_in_list(
499
500
  attribute_list: Iterable[ServiceAttribute], attribute_id: int
500
- ) -> Optional[DataElement]:
501
+ ) -> DataElement | None:
501
502
  return next(
502
503
  (
503
504
  attribute.value
@@ -528,7 +529,7 @@ class ServiceAttribute:
528
529
  def to_string(self, with_colors=False):
529
530
  if with_colors:
530
531
  return (
531
- f'Attribute(id={color(self.id_name(self.id),"magenta")},'
532
+ f'Attribute(id={color(self.id_name(self.id), "magenta")},'
532
533
  f'value={self.value})'
533
534
  )
534
535
 
@@ -778,11 +779,11 @@ class SDP_ServiceSearchAttributeResponse(SDP_PDU):
778
779
  class Client:
779
780
  def __init__(self, connection: Connection, mtu: int = 0) -> None:
780
781
  self.connection = connection
781
- self.channel: Optional[l2cap.ClassicChannel] = None
782
+ self.channel: l2cap.ClassicChannel | None = None
782
783
  self.mtu = mtu
783
784
  self.request_semaphore = asyncio.Semaphore(1)
784
- self.pending_request: Optional[SDP_PDU] = None
785
- self.pending_response: Optional[asyncio.futures.Future[SDP_PDU]] = None
785
+ self.pending_request: SDP_PDU | None = None
786
+ self.pending_response: asyncio.futures.Future[SDP_PDU] | None = None
786
787
  self.next_transaction_id = 0
787
788
 
788
789
  async def connect(self) -> None:
@@ -898,7 +899,7 @@ class Client:
898
899
  async def search_attributes(
899
900
  self,
900
901
  uuids: Iterable[core.UUID],
901
- attribute_ids: Iterable[Union[int, tuple[int, int]]],
902
+ attribute_ids: Iterable[int | tuple[int, int]],
902
903
  ) -> list[list[ServiceAttribute]]:
903
904
  """
904
905
  Search for attributes by UUID and attribute IDs.
@@ -970,7 +971,7 @@ class Client:
970
971
  async def get_attributes(
971
972
  self,
972
973
  service_record_handle: int,
973
- attribute_ids: Iterable[Union[int, tuple[int, int]]],
974
+ attribute_ids: Iterable[int | tuple[int, int]],
974
975
  ) -> list[ServiceAttribute]:
975
976
  """
976
977
  Get attributes for a service.
@@ -1042,10 +1043,10 @@ class Client:
1042
1043
  # -----------------------------------------------------------------------------
1043
1044
  class Server:
1044
1045
  CONTINUATION_STATE = bytes([0x01, 0x00])
1045
- channel: Optional[l2cap.ClassicChannel]
1046
+ channel: l2cap.ClassicChannel | None
1046
1047
  Service = NewType('Service', list[ServiceAttribute])
1047
1048
  service_records: dict[int, Service]
1048
- current_response: Union[None, bytes, tuple[int, list[int]]]
1049
+ current_response: None | bytes | tuple[int, list[int]]
1049
1050
 
1050
1051
  def __init__(self, device: Device) -> None:
1051
1052
  self.device = device
@@ -1123,7 +1124,7 @@ class Server:
1123
1124
  self,
1124
1125
  continuation_state: bytes,
1125
1126
  transaction_id: int,
1126
- ) -> Optional[bool]:
1127
+ ) -> bool | None:
1127
1128
  # Check if this is a valid continuation
1128
1129
  if len(continuation_state) > 1:
1129
1130
  if (