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/ascs.py CHANGED
@@ -18,10 +18,13 @@
18
18
  # -----------------------------------------------------------------------------
19
19
  from __future__ import annotations
20
20
 
21
+ from dataclasses import dataclass, field
21
22
  import enum
23
+ import functools
22
24
  import logging
23
25
  import struct
24
- from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, Union
26
+ from typing import Any, Optional, Union, TypeVar
27
+ from collections.abc import Sequence
25
28
 
26
29
  from bumble import utils
27
30
  from bumble import colors
@@ -48,11 +51,11 @@ class ASE_Operation:
48
51
  See Audio Stream Control Service - 5 ASE Control operations.
49
52
  '''
50
53
 
51
- classes: Dict[int, Type[ASE_Operation]] = {}
52
- op_code: int
54
+ classes: dict[int, type[ASE_Operation]] = {}
55
+ op_code: Opcode
53
56
  name: str
54
57
  fields: Optional[Sequence[Any]] = None
55
- ase_id: List[int]
58
+ ase_id: Sequence[int]
56
59
 
57
60
  class Opcode(enum.IntEnum):
58
61
  # fmt: off
@@ -65,51 +68,30 @@ class ASE_Operation:
65
68
  UPDATE_METADATA = 0x07
66
69
  RELEASE = 0x08
67
70
 
68
- @staticmethod
69
- def from_bytes(pdu: bytes) -> ASE_Operation:
71
+ @classmethod
72
+ def from_bytes(cls, pdu: bytes) -> ASE_Operation:
70
73
  op_code = pdu[0]
71
74
 
72
- cls = ASE_Operation.classes.get(op_code)
73
- if cls is None:
74
- instance = ASE_Operation(pdu)
75
- instance.name = ASE_Operation.Opcode(op_code).name
76
- instance.op_code = op_code
77
- return instance
78
- self = cls.__new__(cls)
79
- ASE_Operation.__init__(self, pdu)
80
- if self.fields is not None:
81
- self.init_from_bytes(pdu, 1)
82
- return self
83
-
84
- @staticmethod
85
- def subclass(fields):
86
- def inner(cls: Type[ASE_Operation]):
87
- try:
88
- operation = ASE_Operation.Opcode[cls.__name__[4:].upper()]
89
- cls.name = operation.name
90
- cls.op_code = operation
91
- except:
92
- raise KeyError(f'PDU name {cls.name} not found in Ase_Operation.Opcode')
93
- cls.fields = fields
94
-
95
- # Register a factory for this class
96
- ASE_Operation.classes[cls.op_code] = cls
97
-
98
- return cls
99
-
100
- return inner
101
-
102
- def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
103
- if self.fields is not None and kwargs:
104
- hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
105
- if pdu is None:
106
- pdu = bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
107
- kwargs, self.fields
108
- )
109
- self.pdu = pdu
75
+ clazz = ASE_Operation.classes[op_code]
76
+ return clazz(
77
+ **hci.HCI_Object.dict_from_bytes(pdu, offset=1, fields=clazz.fields)
78
+ )
79
+
80
+ _OP = TypeVar("_OP", bound="ASE_Operation")
81
+
82
+ @classmethod
83
+ def subclass(cls, clazz: type[_OP]) -> type[_OP]:
84
+ clazz.name = f"ASE_{clazz.op_code.name.upper()}"
85
+ clazz.fields = hci.HCI_Object.fields_from_dataclass(clazz)
86
+ # Register a factory for this class
87
+ ASE_Operation.classes[clazz.op_code] = clazz
88
+ return clazz
110
89
 
111
- def init_from_bytes(self, pdu: bytes, offset: int):
112
- return hci.HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
90
+ @functools.cached_property
91
+ def pdu(self) -> bytes:
92
+ return bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
93
+ self.__dict__, self.fields
94
+ )
113
95
 
114
96
  def __bytes__(self) -> bytes:
115
97
  return self.pdu
@@ -124,105 +106,128 @@ class ASE_Operation:
124
106
  return result
125
107
 
126
108
 
127
- @ASE_Operation.subclass(
128
- [
129
- [
130
- ('ase_id', 1),
131
- ('target_latency', 1),
132
- ('target_phy', 1),
133
- ('codec_id', hci.CodingFormat.parse_from_bytes),
134
- ('codec_specific_configuration', 'v'),
135
- ],
136
- ]
137
- )
109
+ @ASE_Operation.subclass
110
+ @dataclass
138
111
  class ASE_Config_Codec(ASE_Operation):
139
112
  '''
140
113
  See Audio Stream Control Service 5.1 - Config Codec Operation
141
114
  '''
142
115
 
143
- target_latency: List[int]
144
- target_phy: List[int]
145
- codec_id: List[hci.CodingFormat]
146
- codec_specific_configuration: List[bytes]
147
-
148
-
149
- @ASE_Operation.subclass(
150
- [
151
- [
152
- ('ase_id', 1),
153
- ('cig_id', 1),
154
- ('cis_id', 1),
155
- ('sdu_interval', 3),
156
- ('framing', 1),
157
- ('phy', 1),
158
- ('max_sdu', 2),
159
- ('retransmission_number', 1),
160
- ('max_transport_latency', 2),
161
- ('presentation_delay', 3),
162
- ],
163
- ]
164
- )
116
+ op_code = ASE_Operation.Opcode.CONFIG_CODEC
117
+
118
+ ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
119
+ target_latency: Sequence[int] = field(metadata=hci.metadata(1))
120
+ target_phy: Sequence[int] = field(metadata=hci.metadata(1))
121
+ codec_id: Sequence[hci.CodingFormat] = field(
122
+ metadata=hci.metadata(hci.CodingFormat.parse_from_bytes)
123
+ )
124
+ codec_specific_configuration: Sequence[bytes] = field(
125
+ metadata=hci.metadata('v', list_end=True)
126
+ )
127
+
128
+
129
+ @ASE_Operation.subclass
130
+ @dataclass
165
131
  class ASE_Config_QOS(ASE_Operation):
166
132
  '''
167
133
  See Audio Stream Control Service 5.2 - Config Qos Operation
168
134
  '''
169
135
 
170
- cig_id: List[int]
171
- cis_id: List[int]
172
- sdu_interval: List[int]
173
- framing: List[int]
174
- phy: List[int]
175
- max_sdu: List[int]
176
- retransmission_number: List[int]
177
- max_transport_latency: List[int]
178
- presentation_delay: List[int]
136
+ op_code = ASE_Operation.Opcode.CONFIG_QOS
179
137
 
138
+ ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
139
+ cig_id: Sequence[int] = field(metadata=hci.metadata(1))
140
+ cis_id: Sequence[int] = field(metadata=hci.metadata(1))
141
+ sdu_interval: Sequence[int] = field(metadata=hci.metadata(3))
142
+ framing: Sequence[int] = field(metadata=hci.metadata(1))
143
+ phy: Sequence[int] = field(metadata=hci.metadata(1))
144
+ max_sdu: Sequence[int] = field(metadata=hci.metadata(2))
145
+ retransmission_number: Sequence[int] = field(metadata=hci.metadata(1))
146
+ max_transport_latency: Sequence[int] = field(metadata=hci.metadata(2))
147
+ presentation_delay: Sequence[int] = field(metadata=hci.metadata(3, list_end=True))
180
148
 
181
- @ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
149
+
150
+ @ASE_Operation.subclass
151
+ @dataclass
182
152
  class ASE_Enable(ASE_Operation):
183
153
  '''
184
154
  See Audio Stream Control Service 5.3 - Enable Operation
185
155
  '''
186
156
 
187
- metadata: bytes
157
+ op_code = ASE_Operation.Opcode.ENABLE
158
+
159
+ ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
160
+ metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
188
161
 
189
162
 
190
- @ASE_Operation.subclass([[('ase_id', 1)]])
163
+ @ASE_Operation.subclass
164
+ @dataclass
191
165
  class ASE_Receiver_Start_Ready(ASE_Operation):
192
166
  '''
193
167
  See Audio Stream Control Service 5.4 - Receiver Start Ready Operation
194
168
  '''
195
169
 
170
+ op_code = ASE_Operation.Opcode.RECEIVER_START_READY
171
+
172
+ ase_id: Sequence[int] = field(
173
+ metadata=hci.metadata(1, list_begin=True, list_end=True)
174
+ )
175
+
196
176
 
197
- @ASE_Operation.subclass([[('ase_id', 1)]])
177
+ @ASE_Operation.subclass
178
+ @dataclass
198
179
  class ASE_Disable(ASE_Operation):
199
180
  '''
200
181
  See Audio Stream Control Service 5.5 - Disable Operation
201
182
  '''
202
183
 
184
+ op_code = ASE_Operation.Opcode.DISABLE
203
185
 
204
- @ASE_Operation.subclass([[('ase_id', 1)]])
186
+ ase_id: Sequence[int] = field(
187
+ metadata=hci.metadata(1, list_begin=True, list_end=True)
188
+ )
189
+
190
+
191
+ @ASE_Operation.subclass
192
+ @dataclass
205
193
  class ASE_Receiver_Stop_Ready(ASE_Operation):
206
194
  '''
207
195
  See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation
208
196
  '''
209
197
 
198
+ op_code = ASE_Operation.Opcode.RECEIVER_STOP_READY
199
+
200
+ ase_id: Sequence[int] = field(
201
+ metadata=hci.metadata(1, list_begin=True, list_end=True)
202
+ )
203
+
210
204
 
211
- @ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
205
+ @ASE_Operation.subclass
206
+ @dataclass
212
207
  class ASE_Update_Metadata(ASE_Operation):
213
208
  '''
214
209
  See Audio Stream Control Service 5.7 - Update Metadata Operation
215
210
  '''
216
211
 
217
- metadata: List[bytes]
212
+ op_code = ASE_Operation.Opcode.UPDATE_METADATA
218
213
 
214
+ ase_id: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
215
+ metadata: Sequence[bytes] = field(metadata=hci.metadata('v', list_end=True))
219
216
 
220
- @ASE_Operation.subclass([[('ase_id', 1)]])
217
+
218
+ @ASE_Operation.subclass
219
+ @dataclass
221
220
  class ASE_Release(ASE_Operation):
222
221
  '''
223
222
  See Audio Stream Control Service 5.8 - Release Operation
224
223
  '''
225
224
 
225
+ op_code = ASE_Operation.Opcode.RELEASE
226
+
227
+ ase_id: Sequence[int] = field(
228
+ metadata=hci.metadata(1, list_begin=True, list_end=True)
229
+ )
230
+
226
231
 
227
232
  class AseResponseCode(enum.IntEnum):
228
233
  # fmt: off
@@ -276,6 +281,8 @@ class AseStateMachine(gatt.Characteristic):
276
281
  DISABLING = 0x05
277
282
  RELEASING = 0x06
278
283
 
284
+ EVENT_STATE_CHANGE = "state_change"
285
+
279
286
  cis_link: Optional[device.CisLink] = None
280
287
 
281
288
  # Additional parameters in CODEC_CONFIGURED State
@@ -329,25 +336,23 @@ class AseStateMachine(gatt.Characteristic):
329
336
  value=gatt.CharacteristicValue(read=self.on_read),
330
337
  )
331
338
 
332
- self.service.device.on('cis_request', self.on_cis_request)
333
- self.service.device.on('cis_establishment', self.on_cis_establishment)
339
+ self.service.device.on(
340
+ self.service.device.EVENT_CIS_REQUEST, self.on_cis_request
341
+ )
342
+ self.service.device.on(
343
+ self.service.device.EVENT_CIS_ESTABLISHMENT, self.on_cis_establishment
344
+ )
334
345
 
335
- def on_cis_request(
336
- self,
337
- acl_connection: device.Connection,
338
- cis_handle: int,
339
- cig_id: int,
340
- cis_id: int,
341
- ) -> None:
346
+ def on_cis_request(self, cis_link: device.CisLink) -> None:
342
347
  if (
343
- cig_id == self.cig_id
344
- and cis_id == self.cis_id
348
+ cis_link.cig_id == self.cig_id
349
+ and cis_link.cis_id == self.cis_id
345
350
  and self.state == self.State.ENABLING
346
351
  ):
347
352
  utils.cancel_on_event(
348
- acl_connection,
353
+ cis_link.acl_connection,
349
354
  'flush',
350
- self.service.device.accept_cis_request(cis_handle),
355
+ self.service.device.accept_cis_request(cis_link),
351
356
  )
352
357
 
353
358
  def on_cis_establishment(self, cis_link: device.CisLink) -> None:
@@ -356,7 +361,7 @@ class AseStateMachine(gatt.Characteristic):
356
361
  and cis_link.cis_id == self.cis_id
357
362
  and self.state == self.State.ENABLING
358
363
  ):
359
- cis_link.on('disconnection', self.on_cis_disconnection)
364
+ cis_link.on(cis_link.EVENT_DISCONNECTION, self.on_cis_disconnection)
360
365
 
361
366
  async def post_cis_established():
362
367
  await cis_link.setup_data_path(direction=self.role)
@@ -378,7 +383,7 @@ class AseStateMachine(gatt.Characteristic):
378
383
  target_phy: int,
379
384
  codec_id: hci.CodingFormat,
380
385
  codec_specific_configuration: bytes,
381
- ) -> Tuple[AseResponseCode, AseReasonCode]:
386
+ ) -> tuple[AseResponseCode, AseReasonCode]:
382
387
  if self.state not in (
383
388
  self.State.IDLE,
384
389
  self.State.CODEC_CONFIGURED,
@@ -414,7 +419,7 @@ class AseStateMachine(gatt.Characteristic):
414
419
  retransmission_number: int,
415
420
  max_transport_latency: int,
416
421
  presentation_delay: int,
417
- ) -> Tuple[AseResponseCode, AseReasonCode]:
422
+ ) -> tuple[AseResponseCode, AseReasonCode]:
418
423
  if self.state not in (
419
424
  AseStateMachine.State.CODEC_CONFIGURED,
420
425
  AseStateMachine.State.QOS_CONFIGURED,
@@ -438,7 +443,7 @@ class AseStateMachine(gatt.Characteristic):
438
443
 
439
444
  return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
440
445
 
441
- def on_enable(self, metadata: bytes) -> Tuple[AseResponseCode, AseReasonCode]:
446
+ def on_enable(self, metadata: bytes) -> tuple[AseResponseCode, AseReasonCode]:
442
447
  if self.state != AseStateMachine.State.QOS_CONFIGURED:
443
448
  return (
444
449
  AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -450,7 +455,7 @@ class AseStateMachine(gatt.Characteristic):
450
455
 
451
456
  return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
452
457
 
453
- def on_receiver_start_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
458
+ def on_receiver_start_ready(self) -> tuple[AseResponseCode, AseReasonCode]:
454
459
  if self.state != AseStateMachine.State.ENABLING:
455
460
  return (
456
461
  AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -459,7 +464,7 @@ class AseStateMachine(gatt.Characteristic):
459
464
  self.state = self.State.STREAMING
460
465
  return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
461
466
 
462
- def on_disable(self) -> Tuple[AseResponseCode, AseReasonCode]:
467
+ def on_disable(self) -> tuple[AseResponseCode, AseReasonCode]:
463
468
  if self.state not in (
464
469
  AseStateMachine.State.ENABLING,
465
470
  AseStateMachine.State.STREAMING,
@@ -474,7 +479,7 @@ class AseStateMachine(gatt.Characteristic):
474
479
  self.state = self.State.DISABLING
475
480
  return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
476
481
 
477
- def on_receiver_stop_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
482
+ def on_receiver_stop_ready(self) -> tuple[AseResponseCode, AseReasonCode]:
478
483
  if (
479
484
  self.role != AudioRole.SOURCE
480
485
  or self.state != AseStateMachine.State.DISABLING
@@ -488,7 +493,7 @@ class AseStateMachine(gatt.Characteristic):
488
493
 
489
494
  def on_update_metadata(
490
495
  self, metadata: bytes
491
- ) -> Tuple[AseResponseCode, AseReasonCode]:
496
+ ) -> tuple[AseResponseCode, AseReasonCode]:
492
497
  if self.state not in (
493
498
  AseStateMachine.State.ENABLING,
494
499
  AseStateMachine.State.STREAMING,
@@ -500,7 +505,7 @@ class AseStateMachine(gatt.Characteristic):
500
505
  self.metadata = le_audio.Metadata.from_bytes(metadata)
501
506
  return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
502
507
 
503
- def on_release(self) -> Tuple[AseResponseCode, AseReasonCode]:
508
+ def on_release(self) -> tuple[AseResponseCode, AseReasonCode]:
504
509
  if self.state == AseStateMachine.State.IDLE:
505
510
  return (
506
511
  AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
@@ -510,7 +515,7 @@ class AseStateMachine(gatt.Characteristic):
510
515
 
511
516
  async def remove_cis_async():
512
517
  if self.cis_link:
513
- await self.cis_link.remove_data_path(self.role)
518
+ await self.cis_link.remove_data_path([self.role])
514
519
  self.state = self.State.IDLE
515
520
  await self.service.device.notify_subscribers(self, self.value)
516
521
 
@@ -525,7 +530,7 @@ class AseStateMachine(gatt.Characteristic):
525
530
  def state(self, new_state: State) -> None:
526
531
  logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
527
532
  self._state = new_state
528
- self.emit('state_change')
533
+ self.emit(self.EVENT_STATE_CHANGE)
529
534
 
530
535
  @property
531
536
  def value(self):
@@ -584,7 +589,7 @@ class AseStateMachine(gatt.Characteristic):
584
589
  # Readonly. Do nothing in the setter.
585
590
  pass
586
591
 
587
- def on_read(self, _: Optional[device.Connection]) -> bytes:
592
+ def on_read(self, _: device.Connection) -> bytes:
588
593
  return self.value
589
594
 
590
595
  def __str__(self) -> str:
@@ -598,7 +603,7 @@ class AseStateMachine(gatt.Characteristic):
598
603
  class AudioStreamControlService(gatt.TemplateService):
599
604
  UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
600
605
 
601
- ase_state_machines: Dict[int, AseStateMachine]
606
+ ase_state_machines: dict[int, AseStateMachine]
602
607
  ase_control_point: gatt.Characteristic[bytes]
603
608
  _active_client: Optional[device.Connection] = None
604
609
 
@@ -643,7 +648,9 @@ class AudioStreamControlService(gatt.TemplateService):
643
648
  ase.state = AseStateMachine.State.IDLE
644
649
  self._active_client = None
645
650
 
646
- def on_write_ase_control_point(self, connection, data):
651
+ def on_write_ase_control_point(
652
+ self, connection: device.Connection, data: bytes
653
+ ) -> None:
647
654
  if not self._active_client and connection:
648
655
  self._active_client = connection
649
656
  connection.once('disconnection', self._on_client_disconnected)
@@ -652,7 +659,7 @@ class AudioStreamControlService(gatt.TemplateService):
652
659
  responses = []
653
660
  logger.debug(f'*** ASCS Write {operation} ***')
654
661
 
655
- if operation.op_code == ASE_Operation.Opcode.CONFIG_CODEC:
662
+ if isinstance(operation, ASE_Config_Codec):
656
663
  for ase_id, *args in zip(
657
664
  operation.ase_id,
658
665
  operation.target_latency,
@@ -661,7 +668,7 @@ class AudioStreamControlService(gatt.TemplateService):
661
668
  operation.codec_specific_configuration,
662
669
  ):
663
670
  responses.append(self.on_operation(operation.op_code, ase_id, args))
664
- elif operation.op_code == ASE_Operation.Opcode.CONFIG_QOS:
671
+ elif isinstance(operation, ASE_Config_QOS):
665
672
  for ase_id, *args in zip(
666
673
  operation.ase_id,
667
674
  operation.cig_id,
@@ -675,20 +682,20 @@ class AudioStreamControlService(gatt.TemplateService):
675
682
  operation.presentation_delay,
676
683
  ):
677
684
  responses.append(self.on_operation(operation.op_code, ase_id, args))
678
- elif operation.op_code in (
679
- ASE_Operation.Opcode.ENABLE,
680
- ASE_Operation.Opcode.UPDATE_METADATA,
681
- ):
685
+ elif isinstance(operation, (ASE_Enable, ASE_Update_Metadata)):
682
686
  for ase_id, *args in zip(
683
687
  operation.ase_id,
684
688
  operation.metadata,
685
689
  ):
686
690
  responses.append(self.on_operation(operation.op_code, ase_id, args))
687
- elif operation.op_code in (
688
- ASE_Operation.Opcode.RECEIVER_START_READY,
689
- ASE_Operation.Opcode.DISABLE,
690
- ASE_Operation.Opcode.RECEIVER_STOP_READY,
691
- ASE_Operation.Opcode.RELEASE,
691
+ elif isinstance(
692
+ operation,
693
+ (
694
+ ASE_Receiver_Start_Ready,
695
+ ASE_Disable,
696
+ ASE_Receiver_Stop_Ready,
697
+ ASE_Release,
698
+ ),
692
699
  ):
693
700
  for ase_id in operation.ase_id:
694
701
  responses.append(self.on_operation(operation.op_code, ase_id, []))
@@ -717,8 +724,8 @@ class AudioStreamControlService(gatt.TemplateService):
717
724
  class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
718
725
  SERVICE_CLASS = AudioStreamControlService
719
726
 
720
- sink_ase: List[gatt_client.CharacteristicProxy[bytes]]
721
- source_ase: List[gatt_client.CharacteristicProxy[bytes]]
727
+ sink_ase: list[gatt_client.CharacteristicProxy[bytes]]
728
+ source_ase: list[gatt_client.CharacteristicProxy[bytes]]
722
729
  ase_control_point: gatt_client.CharacteristicProxy[bytes]
723
730
 
724
731
  def __init__(self, service_proxy: gatt_client.ServiceProxy):
bumble/profiles/asha.py CHANGED
@@ -19,7 +19,7 @@
19
19
  import enum
20
20
  import struct
21
21
  import logging
22
- from typing import List, Optional, Callable, Union, Any
22
+ from typing import Optional, Callable, Union, Any
23
23
 
24
24
  from bumble import l2cap
25
25
  from bumble import utils
@@ -88,6 +88,11 @@ class AudioStatus(utils.OpenIntEnum):
88
88
  class AshaService(gatt.TemplateService):
89
89
  UUID = gatt.GATT_ASHA_SERVICE
90
90
 
91
+ EVENT_STARTED = "started"
92
+ EVENT_STOPPED = "stopped"
93
+ EVENT_DISCONNECTED = "disconnected"
94
+ EVENT_VOLUME_CHANGED = "volume_changed"
95
+
91
96
  audio_sink: Optional[Callable[[bytes], Any]]
92
97
  active_codec: Optional[Codec] = None
93
98
  audio_type: Optional[AudioType] = None
@@ -98,7 +103,7 @@ class AshaService(gatt.TemplateService):
98
103
  def __init__(
99
104
  self,
100
105
  capability: int,
101
- hisyncid: Union[List[int], bytes],
106
+ hisyncid: Union[list[int], bytes],
102
107
  device: Device,
103
108
  psm: int = 0,
104
109
  audio_sink: Optional[Callable[[bytes], Any]] = None,
@@ -195,7 +200,7 @@ class AshaService(gatt.TemplateService):
195
200
 
196
201
  # Handler for audio control commands
197
202
  async def _on_audio_control_point_write(
198
- self, connection: Optional[Connection], value: bytes
203
+ self, connection: Connection, value: bytes
199
204
  ) -> None:
200
205
  _logger.debug(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
201
206
  opcode = value[0]
@@ -211,14 +216,14 @@ class AshaService(gatt.TemplateService):
211
216
  f'volume={self.volume}, '
212
217
  f'other_state={self.other_state}'
213
218
  )
214
- self.emit('started')
219
+ self.emit(self.EVENT_STARTED)
215
220
  elif opcode == OpCode.STOP:
216
221
  _logger.debug('### STOP')
217
222
  self.active_codec = None
218
223
  self.audio_type = None
219
224
  self.volume = None
220
225
  self.other_state = None
221
- self.emit('stopped')
226
+ self.emit(self.EVENT_STOPPED)
222
227
  elif opcode == OpCode.STATUS:
223
228
  _logger.debug('### STATUS: %s', PeripheralStatus(value[1]).name)
224
229
 
@@ -231,7 +236,7 @@ class AshaService(gatt.TemplateService):
231
236
  self.audio_type = None
232
237
  self.volume = None
233
238
  self.other_state = None
234
- self.emit('disconnected')
239
+ self.emit(self.EVENT_DISCONNECTED)
235
240
 
236
241
  connection.once('disconnection', on_disconnection)
237
242
 
@@ -242,10 +247,10 @@ class AshaService(gatt.TemplateService):
242
247
  )
243
248
 
244
249
  # Handler for volume control
245
- def _on_volume_write(self, connection: Optional[Connection], value: bytes) -> None:
250
+ def _on_volume_write(self, connection: Connection, value: bytes) -> None:
246
251
  _logger.debug(f'--- VOLUME Write:{value[0]}')
247
252
  self.volume = value[0]
248
- self.emit('volume_changed')
253
+ self.emit(self.EVENT_VOLUME_CHANGED)
249
254
 
250
255
  # Register an L2CAP CoC server
251
256
  def _on_connection(self, channel: l2cap.LeCreditBasedChannel) -> None:
bumble/profiles/bap.py CHANGED
@@ -24,7 +24,6 @@ import enum
24
24
  import struct
25
25
  import functools
26
26
  import logging
27
- from typing import List
28
27
  from typing_extensions import Self
29
28
 
30
29
  from bumble import core
@@ -282,7 +281,7 @@ class UnicastServerAdvertisingData:
282
281
  # -----------------------------------------------------------------------------
283
282
 
284
283
 
285
- def bits_to_channel_counts(data: int) -> List[int]:
284
+ def bits_to_channel_counts(data: int) -> list[int]:
286
285
  pos = 0
287
286
  counts = []
288
287
  while data != 0:
@@ -527,7 +526,7 @@ class BasicAudioAnnouncement:
527
526
  codec_id: hci.CodingFormat
528
527
  codec_specific_configuration: CodecSpecificConfiguration
529
528
  metadata: le_audio.Metadata
530
- bis: List[BasicAudioAnnouncement.BIS]
529
+ bis: list[BasicAudioAnnouncement.BIS]
531
530
 
532
531
  def __bytes__(self) -> bytes:
533
532
  metadata_bytes = bytes(self.metadata)
@@ -545,7 +544,7 @@ class BasicAudioAnnouncement:
545
544
  )
546
545
 
547
546
  presentation_delay: int
548
- subgroups: List[BasicAudioAnnouncement.Subgroup]
547
+ subgroups: list[BasicAudioAnnouncement.Subgroup]
549
548
 
550
549
  @classmethod
551
550
  def from_bytes(cls, data: bytes) -> Self:
bumble/profiles/csip.py CHANGED
@@ -19,7 +19,7 @@
19
19
  from __future__ import annotations
20
20
  import enum
21
21
  import struct
22
- from typing import Optional, Tuple
22
+ from typing import Optional
23
23
 
24
24
  from bumble import core
25
25
  from bumble import crypto
@@ -164,12 +164,10 @@ class CoordinatedSetIdentificationService(gatt.TemplateService):
164
164
 
165
165
  super().__init__(characteristics)
166
166
 
167
- async def on_sirk_read(self, connection: Optional[device.Connection]) -> bytes:
167
+ async def on_sirk_read(self, connection: device.Connection) -> bytes:
168
168
  if self.set_identity_resolving_key_type == SirkType.PLAINTEXT:
169
169
  sirk_bytes = self.set_identity_resolving_key
170
170
  else:
171
- assert connection
172
-
173
171
  if connection.transport == core.PhysicalTransport.LE:
174
172
  key = await connection.device.get_long_term_key(
175
173
  connection_handle=connection.handle, rand=b'', ediv=0
@@ -230,7 +228,7 @@ class CoordinatedSetIdentificationProxy(gatt_client.ProfileServiceProxy):
230
228
  ):
231
229
  self.set_member_rank = characteristics[0]
232
230
 
233
- async def read_set_identity_resolving_key(self) -> Tuple[SirkType, bytes]:
231
+ async def read_set_identity_resolving_key(self) -> tuple[SirkType, bytes]:
234
232
  '''Reads SIRK and decrypts if encrypted.'''
235
233
  response = await self.set_identity_resolving_key.read_value()
236
234
  if len(response) != SET_IDENTITY_RESOLVING_KEY_LENGTH + 1:
@@ -17,7 +17,7 @@
17
17
  # Imports
18
18
  # -----------------------------------------------------------------------------
19
19
  import struct
20
- from typing import Optional, Tuple
20
+ from typing import Optional
21
21
 
22
22
  from bumble.gatt import (
23
23
  GATT_DEVICE_INFORMATION_SERVICE,
@@ -60,7 +60,7 @@ class DeviceInformationService(TemplateService):
60
60
  hardware_revision: Optional[str] = None,
61
61
  firmware_revision: Optional[str] = None,
62
62
  software_revision: Optional[str] = None,
63
- system_id: Optional[Tuple[int, int]] = None, # (OUI, Manufacturer ID)
63
+ system_id: Optional[tuple[int, int]] = None, # (OUI, Manufacturer ID)
64
64
  ieee_regulatory_certification_data_list: Optional[bytes] = None,
65
65
  # TODO: pnp_id
66
66
  ):