bumble 0.0.180__py3-none-any.whl → 0.0.182__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 (42) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/bench.py +397 -133
  3. bumble/apps/ble_rpa_tool.py +63 -0
  4. bumble/apps/console.py +4 -4
  5. bumble/apps/controller_info.py +64 -6
  6. bumble/apps/controller_loopback.py +200 -0
  7. bumble/apps/l2cap_bridge.py +32 -24
  8. bumble/apps/pair.py +6 -8
  9. bumble/att.py +53 -11
  10. bumble/controller.py +159 -24
  11. bumble/crypto.py +10 -0
  12. bumble/device.py +580 -113
  13. bumble/drivers/__init__.py +27 -31
  14. bumble/drivers/common.py +45 -0
  15. bumble/drivers/rtk.py +11 -4
  16. bumble/gatt.py +66 -51
  17. bumble/gatt_server.py +30 -22
  18. bumble/hci.py +258 -91
  19. bumble/helpers.py +14 -0
  20. bumble/hfp.py +37 -27
  21. bumble/hid.py +282 -61
  22. bumble/host.py +158 -93
  23. bumble/l2cap.py +11 -6
  24. bumble/link.py +55 -1
  25. bumble/profiles/asha_service.py +2 -2
  26. bumble/profiles/bap.py +1247 -0
  27. bumble/profiles/cap.py +52 -0
  28. bumble/profiles/csip.py +119 -9
  29. bumble/rfcomm.py +31 -20
  30. bumble/smp.py +1 -1
  31. bumble/transport/__init__.py +51 -22
  32. bumble/transport/android_emulator.py +1 -1
  33. bumble/transport/common.py +2 -1
  34. bumble/transport/hci_socket.py +1 -4
  35. bumble/transport/usb.py +1 -1
  36. bumble/utils.py +3 -6
  37. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/METADATA +1 -1
  38. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/RECORD +42 -37
  39. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/entry_points.txt +1 -0
  40. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/LICENSE +0 -0
  41. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/WHEEL +0 -0
  42. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/top_level.txt +0 -0
bumble/profiles/bap.py ADDED
@@ -0,0 +1,1247 @@
1
+ # Copyright 2021-2023 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
+ # -----------------------------------------------------------------------------
17
+ # Imports
18
+ # -----------------------------------------------------------------------------
19
+ from __future__ import annotations
20
+
21
+ from collections.abc import Sequence
22
+ import dataclasses
23
+ import enum
24
+ import struct
25
+ import functools
26
+ import logging
27
+ from typing import Optional, List, Union, Type, Dict, Any, Tuple, cast
28
+
29
+ from bumble import colors
30
+ from bumble import device
31
+ from bumble import hci
32
+ from bumble import gatt
33
+ from bumble import gatt_client
34
+
35
+
36
+ # -----------------------------------------------------------------------------
37
+ # Logging
38
+ # -----------------------------------------------------------------------------
39
+ logger = logging.getLogger(__name__)
40
+
41
+ # -----------------------------------------------------------------------------
42
+ # Constants
43
+ # -----------------------------------------------------------------------------
44
+
45
+
46
+ class AudioLocation(enum.IntFlag):
47
+ '''Bluetooth Assigned Numbers, Section 6.12.1 - Audio Location'''
48
+
49
+ # fmt: off
50
+ NOT_ALLOWED = 0x00000000
51
+ FRONT_LEFT = 0x00000001
52
+ FRONT_RIGHT = 0x00000002
53
+ FRONT_CENTER = 0x00000004
54
+ LOW_FREQUENCY_EFFECTS_1 = 0x00000008
55
+ BACK_LEFT = 0x00000010
56
+ BACK_RIGHT = 0x00000020
57
+ FRONT_LEFT_OF_CENTER = 0x00000040
58
+ FRONT_RIGHT_OF_CENTER = 0x00000080
59
+ BACK_CENTER = 0x00000100
60
+ LOW_FREQUENCY_EFFECTS_2 = 0x00000200
61
+ SIDE_LEFT = 0x00000400
62
+ SIDE_RIGHT = 0x00000800
63
+ TOP_FRONT_LEFT = 0x00001000
64
+ TOP_FRONT_RIGHT = 0x00002000
65
+ TOP_FRONT_CENTER = 0x00004000
66
+ TOP_CENTER = 0x00008000
67
+ TOP_BACK_LEFT = 0x00010000
68
+ TOP_BACK_RIGHT = 0x00020000
69
+ TOP_SIDE_LEFT = 0x00040000
70
+ TOP_SIDE_RIGHT = 0x00080000
71
+ TOP_BACK_CENTER = 0x00100000
72
+ BOTTOM_FRONT_CENTER = 0x00200000
73
+ BOTTOM_FRONT_LEFT = 0x00400000
74
+ BOTTOM_FRONT_RIGHT = 0x00800000
75
+ FRONT_LEFT_WIDE = 0x01000000
76
+ FRONT_RIGHT_WIDE = 0x02000000
77
+ LEFT_SURROUND = 0x04000000
78
+ RIGHT_SURROUND = 0x08000000
79
+
80
+
81
+ class AudioInputType(enum.IntEnum):
82
+ '''Bluetooth Assigned Numbers, Section 6.12.2 - Audio Input Type'''
83
+
84
+ # fmt: off
85
+ UNSPECIFIED = 0x00
86
+ BLUETOOTH = 0x01
87
+ MICROPHONE = 0x02
88
+ ANALOG = 0x03
89
+ DIGITAL = 0x04
90
+ RADIO = 0x05
91
+ STREAMING = 0x06
92
+ AMBIENT = 0x07
93
+
94
+
95
+ class ContextType(enum.IntFlag):
96
+ '''Bluetooth Assigned Numbers, Section 6.12.3 - Context Type'''
97
+
98
+ # fmt: off
99
+ PROHIBITED = 0x0000
100
+ CONVERSATIONAL = 0x0002
101
+ MEDIA = 0x0004
102
+ GAME = 0x0008
103
+ INSTRUCTIONAL = 0x0010
104
+ VOICE_ASSISTANTS = 0x0020
105
+ LIVE = 0x0040
106
+ SOUND_EFFECTS = 0x0080
107
+ NOTIFICATIONS = 0x0100
108
+ RINGTONE = 0x0200
109
+ ALERTS = 0x0400
110
+ EMERGENCY_ALARM = 0x0800
111
+
112
+
113
+ class SamplingFrequency(enum.IntEnum):
114
+ '''Bluetooth Assigned Numbers, Section 6.12.5.1 - Sampling Frequency'''
115
+
116
+ # fmt: off
117
+ FREQ_8000 = 0x01
118
+ FREQ_11025 = 0x02
119
+ FREQ_16000 = 0x03
120
+ FREQ_22050 = 0x04
121
+ FREQ_24000 = 0x05
122
+ FREQ_32000 = 0x06
123
+ FREQ_44100 = 0x07
124
+ FREQ_48000 = 0x08
125
+ FREQ_88200 = 0x09
126
+ FREQ_96000 = 0x0A
127
+ FREQ_176400 = 0x0B
128
+ FREQ_192000 = 0x0C
129
+ FREQ_384000 = 0x0D
130
+ # fmt: on
131
+
132
+ @classmethod
133
+ def from_hz(cls, frequency: int) -> SamplingFrequency:
134
+ return {
135
+ 8000: SamplingFrequency.FREQ_8000,
136
+ 11025: SamplingFrequency.FREQ_11025,
137
+ 16000: SamplingFrequency.FREQ_16000,
138
+ 22050: SamplingFrequency.FREQ_22050,
139
+ 24000: SamplingFrequency.FREQ_24000,
140
+ 32000: SamplingFrequency.FREQ_32000,
141
+ 44100: SamplingFrequency.FREQ_44100,
142
+ 48000: SamplingFrequency.FREQ_48000,
143
+ 88200: SamplingFrequency.FREQ_88200,
144
+ 96000: SamplingFrequency.FREQ_96000,
145
+ 176400: SamplingFrequency.FREQ_176400,
146
+ 192000: SamplingFrequency.FREQ_192000,
147
+ 384000: SamplingFrequency.FREQ_384000,
148
+ }[frequency]
149
+
150
+ @property
151
+ def hz(self) -> int:
152
+ return {
153
+ SamplingFrequency.FREQ_8000: 8000,
154
+ SamplingFrequency.FREQ_11025: 11025,
155
+ SamplingFrequency.FREQ_16000: 16000,
156
+ SamplingFrequency.FREQ_22050: 22050,
157
+ SamplingFrequency.FREQ_24000: 24000,
158
+ SamplingFrequency.FREQ_32000: 32000,
159
+ SamplingFrequency.FREQ_44100: 44100,
160
+ SamplingFrequency.FREQ_48000: 48000,
161
+ SamplingFrequency.FREQ_88200: 88200,
162
+ SamplingFrequency.FREQ_96000: 96000,
163
+ SamplingFrequency.FREQ_176400: 176400,
164
+ SamplingFrequency.FREQ_192000: 192000,
165
+ SamplingFrequency.FREQ_384000: 384000,
166
+ }[self]
167
+
168
+
169
+ class SupportedSamplingFrequency(enum.IntFlag):
170
+ '''Bluetooth Assigned Numbers, Section 6.12.4.1 - Sample Frequency'''
171
+
172
+ # fmt: off
173
+ FREQ_8000 = 1 << (SamplingFrequency.FREQ_8000 - 1)
174
+ FREQ_11025 = 1 << (SamplingFrequency.FREQ_11025 - 1)
175
+ FREQ_16000 = 1 << (SamplingFrequency.FREQ_16000 - 1)
176
+ FREQ_22050 = 1 << (SamplingFrequency.FREQ_22050 - 1)
177
+ FREQ_24000 = 1 << (SamplingFrequency.FREQ_24000 - 1)
178
+ FREQ_32000 = 1 << (SamplingFrequency.FREQ_32000 - 1)
179
+ FREQ_44100 = 1 << (SamplingFrequency.FREQ_44100 - 1)
180
+ FREQ_48000 = 1 << (SamplingFrequency.FREQ_48000 - 1)
181
+ FREQ_88200 = 1 << (SamplingFrequency.FREQ_88200 - 1)
182
+ FREQ_96000 = 1 << (SamplingFrequency.FREQ_96000 - 1)
183
+ FREQ_176400 = 1 << (SamplingFrequency.FREQ_176400 - 1)
184
+ FREQ_192000 = 1 << (SamplingFrequency.FREQ_192000 - 1)
185
+ FREQ_384000 = 1 << (SamplingFrequency.FREQ_384000 - 1)
186
+ # fmt: on
187
+
188
+ @classmethod
189
+ def from_hz(cls, frequencies: Sequence[int]) -> SupportedSamplingFrequency:
190
+ MAPPING = {
191
+ 8000: SupportedSamplingFrequency.FREQ_8000,
192
+ 11025: SupportedSamplingFrequency.FREQ_11025,
193
+ 16000: SupportedSamplingFrequency.FREQ_16000,
194
+ 22050: SupportedSamplingFrequency.FREQ_22050,
195
+ 24000: SupportedSamplingFrequency.FREQ_24000,
196
+ 32000: SupportedSamplingFrequency.FREQ_32000,
197
+ 44100: SupportedSamplingFrequency.FREQ_44100,
198
+ 48000: SupportedSamplingFrequency.FREQ_48000,
199
+ 88200: SupportedSamplingFrequency.FREQ_88200,
200
+ 96000: SupportedSamplingFrequency.FREQ_96000,
201
+ 176400: SupportedSamplingFrequency.FREQ_176400,
202
+ 192000: SupportedSamplingFrequency.FREQ_192000,
203
+ 384000: SupportedSamplingFrequency.FREQ_384000,
204
+ }
205
+
206
+ return functools.reduce(
207
+ lambda x, y: x | MAPPING[y],
208
+ frequencies,
209
+ cls(0),
210
+ )
211
+
212
+
213
+ class FrameDuration(enum.IntEnum):
214
+ '''Bluetooth Assigned Numbers, Section 6.12.5.2 - Frame Duration'''
215
+
216
+ # fmt: off
217
+ DURATION_7500_US = 0x00
218
+ DURATION_10000_US = 0x01
219
+
220
+
221
+ class SupportedFrameDuration(enum.IntFlag):
222
+ '''Bluetooth Assigned Numbers, Section 6.12.4.2 - Frame Duration'''
223
+
224
+ # fmt: off
225
+ DURATION_7500_US_SUPPORTED = 0b0001
226
+ DURATION_10000_US_SUPPORTED = 0b0010
227
+ DURATION_7500_US_PREFERRED = 0b0001
228
+ DURATION_10000_US_PREFERRED = 0b0010
229
+
230
+
231
+ # -----------------------------------------------------------------------------
232
+ # ASE Operations
233
+ # -----------------------------------------------------------------------------
234
+
235
+
236
+ class ASE_Operation:
237
+ '''
238
+ See Audio Stream Control Service - 5 ASE Control operations.
239
+ '''
240
+
241
+ classes: Dict[int, Type[ASE_Operation]] = {}
242
+ op_code: int
243
+ name: str
244
+ fields: Optional[Sequence[Any]] = None
245
+ ase_id: List[int]
246
+
247
+ class Opcode(enum.IntEnum):
248
+ # fmt: off
249
+ CONFIG_CODEC = 0x01
250
+ CONFIG_QOS = 0x02
251
+ ENABLE = 0x03
252
+ RECEIVER_START_READY = 0x04
253
+ DISABLE = 0x05
254
+ RECEIVER_STOP_READY = 0x06
255
+ UPDATE_METADATA = 0x07
256
+ RELEASE = 0x08
257
+
258
+ @staticmethod
259
+ def from_bytes(pdu: bytes) -> ASE_Operation:
260
+ op_code = pdu[0]
261
+
262
+ cls = ASE_Operation.classes.get(op_code)
263
+ if cls is None:
264
+ instance = ASE_Operation(pdu)
265
+ instance.name = ASE_Operation.Opcode(op_code).name
266
+ instance.op_code = op_code
267
+ return instance
268
+ self = cls.__new__(cls)
269
+ ASE_Operation.__init__(self, pdu)
270
+ if self.fields is not None:
271
+ self.init_from_bytes(pdu, 1)
272
+ return self
273
+
274
+ @staticmethod
275
+ def subclass(fields):
276
+ def inner(cls: Type[ASE_Operation]):
277
+ try:
278
+ operation = ASE_Operation.Opcode[cls.__name__[4:].upper()]
279
+ cls.name = operation.name
280
+ cls.op_code = operation
281
+ except:
282
+ raise KeyError(f'PDU name {cls.name} not found in Ase_Operation.Opcode')
283
+ cls.fields = fields
284
+
285
+ # Register a factory for this class
286
+ ASE_Operation.classes[cls.op_code] = cls
287
+
288
+ return cls
289
+
290
+ return inner
291
+
292
+ def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
293
+ if self.fields is not None and kwargs:
294
+ hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
295
+ if pdu is None:
296
+ pdu = bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
297
+ kwargs, self.fields
298
+ )
299
+ self.pdu = pdu
300
+
301
+ def init_from_bytes(self, pdu: bytes, offset: int):
302
+ return hci.HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
303
+
304
+ def __bytes__(self) -> bytes:
305
+ return self.pdu
306
+
307
+ def __str__(self) -> str:
308
+ result = f'{colors.color(self.name, "yellow")} '
309
+ if fields := getattr(self, 'fields', None):
310
+ result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ')
311
+ else:
312
+ if len(self.pdu) > 1:
313
+ result += f': {self.pdu.hex()}'
314
+ return result
315
+
316
+
317
+ @ASE_Operation.subclass(
318
+ [
319
+ [
320
+ ('ase_id', 1),
321
+ ('target_latency', 1),
322
+ ('target_phy', 1),
323
+ ('codec_id', hci.CodingFormat.parse_from_bytes),
324
+ ('codec_specific_configuration', 'v'),
325
+ ],
326
+ ]
327
+ )
328
+ class ASE_Config_Codec(ASE_Operation):
329
+ '''
330
+ See Audio Stream Control Service 5.1 - Config Codec Operation
331
+ '''
332
+
333
+ target_latency: List[int]
334
+ target_phy: List[int]
335
+ codec_id: List[hci.CodingFormat]
336
+ codec_specific_configuration: List[bytes]
337
+
338
+
339
+ @ASE_Operation.subclass(
340
+ [
341
+ [
342
+ ('ase_id', 1),
343
+ ('cig_id', 1),
344
+ ('cis_id', 1),
345
+ ('sdu_interval', 3),
346
+ ('framing', 1),
347
+ ('phy', 1),
348
+ ('max_sdu', 2),
349
+ ('retransmission_number', 1),
350
+ ('max_transport_latency', 2),
351
+ ('presentation_delay', 3),
352
+ ],
353
+ ]
354
+ )
355
+ class ASE_Config_QOS(ASE_Operation):
356
+ '''
357
+ See Audio Stream Control Service 5.2 - Config Qos Operation
358
+ '''
359
+
360
+ cig_id: List[int]
361
+ cis_id: List[int]
362
+ sdu_interval: List[int]
363
+ framing: List[int]
364
+ phy: List[int]
365
+ max_sdu: List[int]
366
+ retransmission_number: List[int]
367
+ max_transport_latency: List[int]
368
+ presentation_delay: List[int]
369
+
370
+
371
+ @ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
372
+ class ASE_Enable(ASE_Operation):
373
+ '''
374
+ See Audio Stream Control Service 5.3 - Enable Operation
375
+ '''
376
+
377
+ metadata: bytes
378
+
379
+
380
+ @ASE_Operation.subclass([[('ase_id', 1)]])
381
+ class ASE_Receiver_Start_Ready(ASE_Operation):
382
+ '''
383
+ See Audio Stream Control Service 5.4 - Receiver Start Ready Operation
384
+ '''
385
+
386
+
387
+ @ASE_Operation.subclass([[('ase_id', 1)]])
388
+ class ASE_Disable(ASE_Operation):
389
+ '''
390
+ See Audio Stream Control Service 5.5 - Disable Operation
391
+ '''
392
+
393
+
394
+ @ASE_Operation.subclass([[('ase_id', 1)]])
395
+ class ASE_Receiver_Stop_Ready(ASE_Operation):
396
+ '''
397
+ See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation
398
+ '''
399
+
400
+
401
+ @ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
402
+ class ASE_Update_Metadata(ASE_Operation):
403
+ '''
404
+ See Audio Stream Control Service 5.7 - Update Metadata Operation
405
+ '''
406
+
407
+ metadata: List[bytes]
408
+
409
+
410
+ @ASE_Operation.subclass([[('ase_id', 1)]])
411
+ class ASE_Release(ASE_Operation):
412
+ '''
413
+ See Audio Stream Control Service 5.8 - Release Operation
414
+ '''
415
+
416
+
417
+ class AseResponseCode(enum.IntEnum):
418
+ # fmt: off
419
+ SUCCESS = 0x00
420
+ UNSUPPORTED_OPCODE = 0x01
421
+ INVALID_LENGTH = 0x02
422
+ INVALID_ASE_ID = 0x03
423
+ INVALID_ASE_STATE_MACHINE_TRANSITION = 0x04
424
+ INVALID_ASE_DIRECTION = 0x05
425
+ UNSUPPORTED_AUDIO_CAPABILITIES = 0x06
426
+ UNSUPPORTED_CONFIGURATION_PARAMETER_VALUE = 0x07
427
+ REJECTED_CONFIGURATION_PARAMETER_VALUE = 0x08
428
+ INVALID_CONFIGURATION_PARAMETER_VALUE = 0x09
429
+ UNSUPPORTED_METADATA = 0x0A
430
+ REJECTED_METADATA = 0x0B
431
+ INVALID_METADATA = 0x0C
432
+ INSUFFICIENT_RESOURCES = 0x0D
433
+ UNSPECIFIED_ERROR = 0x0E
434
+
435
+
436
+ class AseReasonCode(enum.IntEnum):
437
+ # fmt: off
438
+ NONE = 0x00
439
+ CODEC_ID = 0x01
440
+ CODEC_SPECIFIC_CONFIGURATION = 0x02
441
+ SDU_INTERVAL = 0x03
442
+ FRAMING = 0x04
443
+ PHY = 0x05
444
+ MAXIMUM_SDU_SIZE = 0x06
445
+ RETRANSMISSION_NUMBER = 0x07
446
+ MAX_TRANSPORT_LATENCY = 0x08
447
+ PRESENTATION_DELAY = 0x09
448
+ INVALID_ASE_CIS_MAPPING = 0x0A
449
+
450
+
451
+ class AudioRole(enum.IntEnum):
452
+ SINK = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.CONTROLLER_TO_HOST
453
+ SOURCE = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.HOST_TO_CONTROLLER
454
+
455
+
456
+ # -----------------------------------------------------------------------------
457
+ # Utils
458
+ # -----------------------------------------------------------------------------
459
+
460
+
461
+ def bits_to_channel_counts(data: int) -> List[int]:
462
+ pos = 0
463
+ counts = []
464
+ while data != 0:
465
+ # Bit 0 = count 1
466
+ # Bit 1 = count 2, and so on
467
+ pos += 1
468
+ if data & 1:
469
+ counts.append(pos)
470
+ data >>= 1
471
+ return counts
472
+
473
+
474
+ def channel_counts_to_bits(counts: Sequence[int]) -> int:
475
+ return sum(set([1 << (count - 1) for count in counts]))
476
+
477
+
478
+ # -----------------------------------------------------------------------------
479
+ # Structures
480
+ # -----------------------------------------------------------------------------
481
+
482
+
483
+ @dataclasses.dataclass
484
+ class CodecSpecificCapabilities:
485
+ '''See:
486
+ * Bluetooth Assigned Numbers, 6.12.4 - Codec Specific Capabilities LTV Structures
487
+ * Basic Audio Profile, 4.3.1 - Codec_Specific_Capabilities LTV requirements
488
+ '''
489
+
490
+ class Type(enum.IntEnum):
491
+ # fmt: off
492
+ SAMPLING_FREQUENCY = 0x01
493
+ FRAME_DURATION = 0x02
494
+ AUDIO_CHANNEL_COUNT = 0x03
495
+ OCTETS_PER_FRAME = 0x04
496
+ CODEC_FRAMES_PER_SDU = 0x05
497
+
498
+ supported_sampling_frequencies: SupportedSamplingFrequency
499
+ supported_frame_durations: SupportedFrameDuration
500
+ supported_audio_channel_counts: Sequence[int]
501
+ min_octets_per_codec_frame: int
502
+ max_octets_per_codec_frame: int
503
+ supported_max_codec_frames_per_sdu: int
504
+
505
+ @classmethod
506
+ def from_bytes(cls, data: bytes) -> CodecSpecificCapabilities:
507
+ offset = 0
508
+ # Allowed default values.
509
+ supported_audio_channel_counts = [1]
510
+ supported_max_codec_frames_per_sdu = 1
511
+ while offset < len(data):
512
+ length, type = struct.unpack_from('BB', data, offset)
513
+ offset += 2
514
+ value = int.from_bytes(data[offset : offset + length - 1], 'little')
515
+ offset += length - 1
516
+
517
+ if type == CodecSpecificCapabilities.Type.SAMPLING_FREQUENCY:
518
+ supported_sampling_frequencies = SupportedSamplingFrequency(value)
519
+ elif type == CodecSpecificCapabilities.Type.FRAME_DURATION:
520
+ supported_frame_durations = SupportedFrameDuration(value)
521
+ elif type == CodecSpecificCapabilities.Type.AUDIO_CHANNEL_COUNT:
522
+ supported_audio_channel_counts = bits_to_channel_counts(value)
523
+ elif type == CodecSpecificCapabilities.Type.OCTETS_PER_FRAME:
524
+ min_octets_per_sample = value & 0xFFFF
525
+ max_octets_per_sample = value >> 16
526
+ elif type == CodecSpecificCapabilities.Type.CODEC_FRAMES_PER_SDU:
527
+ supported_max_codec_frames_per_sdu = value
528
+
529
+ # It is expected here that if some fields are missing, an error should be raised.
530
+ return CodecSpecificCapabilities(
531
+ supported_sampling_frequencies=supported_sampling_frequencies,
532
+ supported_frame_durations=supported_frame_durations,
533
+ supported_audio_channel_counts=supported_audio_channel_counts,
534
+ min_octets_per_codec_frame=min_octets_per_sample,
535
+ max_octets_per_codec_frame=max_octets_per_sample,
536
+ supported_max_codec_frames_per_sdu=supported_max_codec_frames_per_sdu,
537
+ )
538
+
539
+ def __bytes__(self) -> bytes:
540
+ return struct.pack(
541
+ '<BBHBBBBBBBBHHBBB',
542
+ 3,
543
+ CodecSpecificCapabilities.Type.SAMPLING_FREQUENCY,
544
+ self.supported_sampling_frequencies,
545
+ 2,
546
+ CodecSpecificCapabilities.Type.FRAME_DURATION,
547
+ self.supported_frame_durations,
548
+ 2,
549
+ CodecSpecificCapabilities.Type.AUDIO_CHANNEL_COUNT,
550
+ channel_counts_to_bits(self.supported_audio_channel_counts),
551
+ 5,
552
+ CodecSpecificCapabilities.Type.OCTETS_PER_FRAME,
553
+ self.min_octets_per_codec_frame,
554
+ self.max_octets_per_codec_frame,
555
+ 2,
556
+ CodecSpecificCapabilities.Type.CODEC_FRAMES_PER_SDU,
557
+ self.supported_max_codec_frames_per_sdu,
558
+ )
559
+
560
+
561
+ @dataclasses.dataclass
562
+ class CodecSpecificConfiguration:
563
+ '''See:
564
+ * Bluetooth Assigned Numbers, 6.12.5 - Codec Specific Configuration LTV Structures
565
+ * Basic Audio Profile, 4.3.2 - Codec_Specific_Capabilities LTV requirements
566
+ '''
567
+
568
+ class Type(enum.IntEnum):
569
+ # fmt: off
570
+ SAMPLING_FREQUENCY = 0x01
571
+ FRAME_DURATION = 0x02
572
+ AUDIO_CHANNEL_ALLOCATION = 0x03
573
+ OCTETS_PER_FRAME = 0x04
574
+ CODEC_FRAMES_PER_SDU = 0x05
575
+
576
+ sampling_frequency: SamplingFrequency
577
+ frame_duration: FrameDuration
578
+ audio_channel_allocation: AudioLocation
579
+ octets_per_codec_frame: int
580
+ codec_frames_per_sdu: int
581
+
582
+ @classmethod
583
+ def from_bytes(cls, data: bytes) -> CodecSpecificConfiguration:
584
+ offset = 0
585
+ # Allowed default values.
586
+ audio_channel_allocation = AudioLocation.NOT_ALLOWED
587
+ codec_frames_per_sdu = 1
588
+ while offset < len(data):
589
+ length, type = struct.unpack_from('BB', data, offset)
590
+ offset += 2
591
+ value = int.from_bytes(data[offset : offset + length - 1], 'little')
592
+ offset += length - 1
593
+
594
+ if type == CodecSpecificConfiguration.Type.SAMPLING_FREQUENCY:
595
+ sampling_frequency = SamplingFrequency(value)
596
+ elif type == CodecSpecificConfiguration.Type.FRAME_DURATION:
597
+ frame_duration = FrameDuration(value)
598
+ elif type == CodecSpecificConfiguration.Type.AUDIO_CHANNEL_ALLOCATION:
599
+ audio_channel_allocation = AudioLocation(value)
600
+ elif type == CodecSpecificConfiguration.Type.OCTETS_PER_FRAME:
601
+ octets_per_codec_frame = value
602
+ elif type == CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU:
603
+ codec_frames_per_sdu = value
604
+
605
+ # It is expected here that if some fields are missing, an error should be raised.
606
+ return CodecSpecificConfiguration(
607
+ sampling_frequency=sampling_frequency,
608
+ frame_duration=frame_duration,
609
+ audio_channel_allocation=audio_channel_allocation,
610
+ octets_per_codec_frame=octets_per_codec_frame,
611
+ codec_frames_per_sdu=codec_frames_per_sdu,
612
+ )
613
+
614
+ def __bytes__(self) -> bytes:
615
+ return struct.pack(
616
+ '<BBBBBBBBIBBHBBB',
617
+ 2,
618
+ CodecSpecificConfiguration.Type.SAMPLING_FREQUENCY,
619
+ self.sampling_frequency,
620
+ 2,
621
+ CodecSpecificConfiguration.Type.FRAME_DURATION,
622
+ self.frame_duration,
623
+ 5,
624
+ CodecSpecificConfiguration.Type.AUDIO_CHANNEL_ALLOCATION,
625
+ self.audio_channel_allocation,
626
+ 3,
627
+ CodecSpecificConfiguration.Type.OCTETS_PER_FRAME,
628
+ self.octets_per_codec_frame,
629
+ 2,
630
+ CodecSpecificConfiguration.Type.CODEC_FRAMES_PER_SDU,
631
+ self.codec_frames_per_sdu,
632
+ )
633
+
634
+
635
+ @dataclasses.dataclass
636
+ class PacRecord:
637
+ coding_format: hci.CodingFormat
638
+ codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
639
+ # TODO: Parse Metadata
640
+ metadata: bytes = b''
641
+
642
+ @classmethod
643
+ def from_bytes(cls, data: bytes) -> PacRecord:
644
+ offset, coding_format = hci.CodingFormat.parse_from_bytes(data, 0)
645
+ codec_specific_capabilities_size = data[offset]
646
+
647
+ offset += 1
648
+ codec_specific_capabilities_bytes = data[
649
+ offset : offset + codec_specific_capabilities_size
650
+ ]
651
+ offset += codec_specific_capabilities_size
652
+ metadata_size = data[offset]
653
+ metadata = data[offset : offset + metadata_size]
654
+
655
+ codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
656
+ if coding_format.codec_id == hci.CodecID.VENDOR_SPECIFIC:
657
+ codec_specific_capabilities = codec_specific_capabilities_bytes
658
+ else:
659
+ codec_specific_capabilities = CodecSpecificCapabilities.from_bytes(
660
+ codec_specific_capabilities_bytes
661
+ )
662
+
663
+ return PacRecord(
664
+ coding_format=coding_format,
665
+ codec_specific_capabilities=codec_specific_capabilities,
666
+ metadata=metadata,
667
+ )
668
+
669
+ def __bytes__(self) -> bytes:
670
+ capabilities_bytes = bytes(self.codec_specific_capabilities)
671
+ return (
672
+ bytes(self.coding_format)
673
+ + bytes([len(capabilities_bytes)])
674
+ + capabilities_bytes
675
+ + bytes([len(self.metadata)])
676
+ + self.metadata
677
+ )
678
+
679
+
680
+ # -----------------------------------------------------------------------------
681
+ # Server
682
+ # -----------------------------------------------------------------------------
683
+ class PublishedAudioCapabilitiesService(gatt.TemplateService):
684
+ UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
685
+
686
+ sink_pac: Optional[gatt.Characteristic]
687
+ sink_audio_locations: Optional[gatt.Characteristic]
688
+ source_pac: Optional[gatt.Characteristic]
689
+ source_audio_locations: Optional[gatt.Characteristic]
690
+ available_audio_contexts: gatt.Characteristic
691
+ supported_audio_contexts: gatt.Characteristic
692
+
693
+ def __init__(
694
+ self,
695
+ supported_source_context: ContextType,
696
+ supported_sink_context: ContextType,
697
+ available_source_context: ContextType,
698
+ available_sink_context: ContextType,
699
+ sink_pac: Sequence[PacRecord] = [],
700
+ sink_audio_locations: Optional[AudioLocation] = None,
701
+ source_pac: Sequence[PacRecord] = [],
702
+ source_audio_locations: Optional[AudioLocation] = None,
703
+ ) -> None:
704
+ characteristics = []
705
+
706
+ self.supported_audio_contexts = gatt.Characteristic(
707
+ uuid=gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC,
708
+ properties=gatt.Characteristic.Properties.READ,
709
+ permissions=gatt.Characteristic.Permissions.READABLE,
710
+ value=struct.pack('<HH', supported_sink_context, supported_source_context),
711
+ )
712
+ characteristics.append(self.supported_audio_contexts)
713
+
714
+ self.available_audio_contexts = gatt.Characteristic(
715
+ uuid=gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC,
716
+ properties=gatt.Characteristic.Properties.READ
717
+ | gatt.Characteristic.Properties.NOTIFY,
718
+ permissions=gatt.Characteristic.Permissions.READABLE,
719
+ value=struct.pack('<HH', available_sink_context, available_source_context),
720
+ )
721
+ characteristics.append(self.available_audio_contexts)
722
+
723
+ if sink_pac:
724
+ self.sink_pac = gatt.Characteristic(
725
+ uuid=gatt.GATT_SINK_PAC_CHARACTERISTIC,
726
+ properties=gatt.Characteristic.Properties.READ,
727
+ permissions=gatt.Characteristic.Permissions.READABLE,
728
+ value=bytes([len(sink_pac)]) + b''.join(map(bytes, sink_pac)),
729
+ )
730
+ characteristics.append(self.sink_pac)
731
+
732
+ if sink_audio_locations is not None:
733
+ self.sink_audio_locations = gatt.Characteristic(
734
+ uuid=gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC,
735
+ properties=gatt.Characteristic.Properties.READ,
736
+ permissions=gatt.Characteristic.Permissions.READABLE,
737
+ value=struct.pack('<I', sink_audio_locations),
738
+ )
739
+ characteristics.append(self.sink_audio_locations)
740
+
741
+ if source_pac:
742
+ self.source_pac = gatt.Characteristic(
743
+ uuid=gatt.GATT_SOURCE_PAC_CHARACTERISTIC,
744
+ properties=gatt.Characteristic.Properties.READ,
745
+ permissions=gatt.Characteristic.Permissions.READABLE,
746
+ value=bytes([len(source_pac)]) + b''.join(map(bytes, source_pac)),
747
+ )
748
+ characteristics.append(self.source_pac)
749
+
750
+ if source_audio_locations is not None:
751
+ self.source_audio_locations = gatt.Characteristic(
752
+ uuid=gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC,
753
+ properties=gatt.Characteristic.Properties.READ,
754
+ permissions=gatt.Characteristic.Permissions.READABLE,
755
+ value=struct.pack('<I', source_audio_locations),
756
+ )
757
+ characteristics.append(self.source_audio_locations)
758
+
759
+ super().__init__(characteristics)
760
+
761
+
762
+ class AseStateMachine(gatt.Characteristic):
763
+ class State(enum.IntEnum):
764
+ # fmt: off
765
+ IDLE = 0x00
766
+ CODEC_CONFIGURED = 0x01
767
+ QOS_CONFIGURED = 0x02
768
+ ENABLING = 0x03
769
+ STREAMING = 0x04
770
+ DISABLING = 0x05
771
+ RELEASING = 0x06
772
+
773
+ cis_link: Optional[device.CisLink] = None
774
+
775
+ # Additional parameters in CODEC_CONFIGURED State
776
+ preferred_framing = 0 # Unframed PDU supported
777
+ preferred_phy = 0
778
+ preferred_retransmission_number = 13
779
+ preferred_max_transport_latency = 100
780
+ supported_presentation_delay_min = 0
781
+ supported_presentation_delay_max = 0
782
+ preferred_presentation_delay_min = 0
783
+ preferred_presentation_delay_max = 0
784
+ codec_id = hci.CodingFormat(hci.CodecID.LC3)
785
+ codec_specific_configuration: Union[CodecSpecificConfiguration, bytes] = b''
786
+
787
+ # Additional parameters in QOS_CONFIGURED State
788
+ cig_id = 0
789
+ cis_id = 0
790
+ sdu_interval = 0
791
+ framing = 0
792
+ phy = 0
793
+ max_sdu = 0
794
+ retransmission_number = 0
795
+ max_transport_latency = 0
796
+ presentation_delay = 0
797
+
798
+ # Additional parameters in ENABLING, STREAMING, DISABLING State
799
+ # TODO: Parse this
800
+ metadata = b''
801
+
802
+ def __init__(
803
+ self,
804
+ role: AudioRole,
805
+ ase_id: int,
806
+ service: AudioStreamControlService,
807
+ ) -> None:
808
+ self.service = service
809
+ self.ase_id = ase_id
810
+ self._state = AseStateMachine.State.IDLE
811
+ self.role = role
812
+
813
+ uuid = (
814
+ gatt.GATT_SINK_ASE_CHARACTERISTIC
815
+ if role == AudioRole.SINK
816
+ else gatt.GATT_SOURCE_ASE_CHARACTERISTIC
817
+ )
818
+ super().__init__(
819
+ uuid=uuid,
820
+ properties=gatt.Characteristic.Properties.READ
821
+ | gatt.Characteristic.Properties.NOTIFY,
822
+ permissions=gatt.Characteristic.Permissions.READABLE,
823
+ value=gatt.CharacteristicValue(read=self.on_read),
824
+ )
825
+
826
+ self.service.device.on('cis_request', self.on_cis_request)
827
+ self.service.device.on('cis_establishment', self.on_cis_establishment)
828
+
829
+ def on_cis_request(
830
+ self,
831
+ acl_connection: device.Connection,
832
+ cis_handle: int,
833
+ cig_id: int,
834
+ cis_id: int,
835
+ ) -> None:
836
+ if cis_id == self.cis_id and self.state == self.State.ENABLING:
837
+ acl_connection.abort_on(
838
+ 'flush', self.service.device.accept_cis_request(cis_handle)
839
+ )
840
+
841
+ def on_cis_establishment(self, cis_link: device.CisLink) -> None:
842
+ if cis_link.cis_id == self.cis_id and self.state == self.State.ENABLING:
843
+ self.state = self.State.STREAMING
844
+ self.cis_link = cis_link
845
+
846
+ async def post_cis_established():
847
+ await self.service.device.send_command(
848
+ hci.HCI_LE_Setup_ISO_Data_Path_Command(
849
+ connection_handle=cis_link.handle,
850
+ data_path_direction=self.role,
851
+ data_path_id=0x00, # Fixed HCI
852
+ codec_id=hci.CodingFormat(hci.CodecID.TRANSPARENT),
853
+ controller_delay=0,
854
+ codec_configuration=b'',
855
+ )
856
+ )
857
+ await self.service.device.notify_subscribers(self, self.value)
858
+
859
+ cis_link.acl_connection.abort_on('flush', post_cis_established())
860
+
861
+ def on_config_codec(
862
+ self,
863
+ target_latency: int,
864
+ target_phy: int,
865
+ codec_id: hci.CodingFormat,
866
+ codec_specific_configuration: bytes,
867
+ ) -> Tuple[AseResponseCode, AseReasonCode]:
868
+ if self.state not in (
869
+ self.State.IDLE,
870
+ self.State.CODEC_CONFIGURED,
871
+ self.State.QOS_CONFIGURED,
872
+ ):
873
+ return (
874
+ AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
875
+ AseReasonCode.NONE,
876
+ )
877
+
878
+ self.max_transport_latency = target_latency
879
+ self.phy = target_phy
880
+ self.codec_id = codec_id
881
+ if codec_id.codec_id == hci.CodecID.VENDOR_SPECIFIC:
882
+ self.codec_specific_configuration = codec_specific_configuration
883
+ else:
884
+ self.codec_specific_configuration = CodecSpecificConfiguration.from_bytes(
885
+ codec_specific_configuration
886
+ )
887
+
888
+ self.state = self.State.CODEC_CONFIGURED
889
+
890
+ return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
891
+
892
+ def on_config_qos(
893
+ self,
894
+ cig_id: int,
895
+ cis_id: int,
896
+ sdu_interval: int,
897
+ framing: int,
898
+ phy: int,
899
+ max_sdu: int,
900
+ retransmission_number: int,
901
+ max_transport_latency: int,
902
+ presentation_delay: int,
903
+ ) -> Tuple[AseResponseCode, AseReasonCode]:
904
+ if self.state not in (
905
+ AseStateMachine.State.CODEC_CONFIGURED,
906
+ AseStateMachine.State.QOS_CONFIGURED,
907
+ ):
908
+ return (
909
+ AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
910
+ AseReasonCode.NONE,
911
+ )
912
+
913
+ self.cig_id = cig_id
914
+ self.cis_id = cis_id
915
+ self.sdu_interval = sdu_interval
916
+ self.framing = framing
917
+ self.phy = phy
918
+ self.max_sdu = max_sdu
919
+ self.retransmission_number = retransmission_number
920
+ self.max_transport_latency = max_transport_latency
921
+ self.presentation_delay = presentation_delay
922
+
923
+ self.state = self.State.QOS_CONFIGURED
924
+
925
+ return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
926
+
927
+ def on_enable(self, metadata: bytes) -> Tuple[AseResponseCode, AseReasonCode]:
928
+ if self.state != AseStateMachine.State.QOS_CONFIGURED:
929
+ return (
930
+ AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
931
+ AseReasonCode.NONE,
932
+ )
933
+
934
+ self.metadata = metadata
935
+ self.state = self.State.ENABLING
936
+
937
+ return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
938
+
939
+ def on_receiver_start_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
940
+ if self.state != AseStateMachine.State.ENABLING:
941
+ return (
942
+ AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
943
+ AseReasonCode.NONE,
944
+ )
945
+ self.state = self.State.STREAMING
946
+ return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
947
+
948
+ def on_disable(self) -> Tuple[AseResponseCode, AseReasonCode]:
949
+ if self.state not in (
950
+ AseStateMachine.State.ENABLING,
951
+ AseStateMachine.State.STREAMING,
952
+ ):
953
+ return (
954
+ AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
955
+ AseReasonCode.NONE,
956
+ )
957
+ self.state = self.State.DISABLING
958
+ return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
959
+
960
+ def on_receiver_stop_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
961
+ if self.state != AseStateMachine.State.DISABLING:
962
+ return (
963
+ AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
964
+ AseReasonCode.NONE,
965
+ )
966
+ self.state = self.State.QOS_CONFIGURED
967
+ return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
968
+
969
+ def on_update_metadata(
970
+ self, metadata: bytes
971
+ ) -> Tuple[AseResponseCode, AseReasonCode]:
972
+ if self.state not in (
973
+ AseStateMachine.State.ENABLING,
974
+ AseStateMachine.State.STREAMING,
975
+ ):
976
+ return (
977
+ AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
978
+ AseReasonCode.NONE,
979
+ )
980
+ self.metadata = metadata
981
+ return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
982
+
983
+ def on_release(self) -> Tuple[AseResponseCode, AseReasonCode]:
984
+ if self.state == AseStateMachine.State.IDLE:
985
+ return (
986
+ AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
987
+ AseReasonCode.NONE,
988
+ )
989
+ self.state = self.State.RELEASING
990
+
991
+ async def remove_cis_async():
992
+ await self.service.device.send_command(
993
+ hci.HCI_LE_Remove_ISO_Data_Path_Command(
994
+ connection_handle=self.cis_link.handle,
995
+ data_path_direction=self.role,
996
+ )
997
+ )
998
+ self.state = self.State.IDLE
999
+ await self.service.device.notify_subscribers(self, self.value)
1000
+
1001
+ self.service.device.abort_on('flush', remove_cis_async())
1002
+ return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1003
+
1004
+ @property
1005
+ def state(self) -> State:
1006
+ return self._state
1007
+
1008
+ @state.setter
1009
+ def state(self, new_state: State) -> None:
1010
+ logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
1011
+ self._state = new_state
1012
+
1013
+ @property
1014
+ def value(self):
1015
+ '''Returns ASE_ID, ASE_STATE, and ASE Additional Parameters.'''
1016
+
1017
+ if self.state == self.State.CODEC_CONFIGURED:
1018
+ codec_specific_configuration_bytes = bytes(
1019
+ self.codec_specific_configuration
1020
+ )
1021
+ additional_parameters = (
1022
+ struct.pack(
1023
+ '<BBBH',
1024
+ self.preferred_framing,
1025
+ self.preferred_phy,
1026
+ self.preferred_retransmission_number,
1027
+ self.preferred_max_transport_latency,
1028
+ )
1029
+ + self.supported_presentation_delay_min.to_bytes(3, 'little')
1030
+ + self.supported_presentation_delay_max.to_bytes(3, 'little')
1031
+ + self.preferred_presentation_delay_min.to_bytes(3, 'little')
1032
+ + self.preferred_presentation_delay_max.to_bytes(3, 'little')
1033
+ + bytes(self.codec_id)
1034
+ + bytes([len(codec_specific_configuration_bytes)])
1035
+ + codec_specific_configuration_bytes
1036
+ )
1037
+ elif self.state == self.State.QOS_CONFIGURED:
1038
+ additional_parameters = (
1039
+ bytes([self.cig_id, self.cis_id])
1040
+ + self.sdu_interval.to_bytes(3, 'little')
1041
+ + struct.pack(
1042
+ '<BBHBH',
1043
+ self.framing,
1044
+ self.phy,
1045
+ self.max_sdu,
1046
+ self.retransmission_number,
1047
+ self.max_transport_latency,
1048
+ )
1049
+ + self.presentation_delay.to_bytes(3, 'little')
1050
+ )
1051
+ elif self.state in (
1052
+ self.State.ENABLING,
1053
+ self.State.STREAMING,
1054
+ self.State.DISABLING,
1055
+ ):
1056
+ additional_parameters = (
1057
+ bytes([self.cig_id, self.cis_id, len(self.metadata)]) + self.metadata
1058
+ )
1059
+ else:
1060
+ additional_parameters = b''
1061
+
1062
+ return bytes([self.ase_id, self.state]) + additional_parameters
1063
+
1064
+ @value.setter
1065
+ def value(self, _new_value):
1066
+ # Readonly. Do nothing in the setter.
1067
+ pass
1068
+
1069
+ def on_read(self, _: Optional[device.Connection]) -> bytes:
1070
+ return self.value
1071
+
1072
+ def __str__(self) -> str:
1073
+ return (
1074
+ f'AseStateMachine(id={self.ase_id}, role={self.role.name} '
1075
+ f'state={self._state.name})'
1076
+ )
1077
+
1078
+
1079
+ class AudioStreamControlService(gatt.TemplateService):
1080
+ UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
1081
+
1082
+ ase_state_machines: Dict[int, AseStateMachine]
1083
+ ase_control_point: gatt.Characteristic
1084
+
1085
+ def __init__(
1086
+ self,
1087
+ device: device.Device,
1088
+ source_ase_id: Sequence[int] = [],
1089
+ sink_ase_id: Sequence[int] = [],
1090
+ ) -> None:
1091
+ self.device = device
1092
+ self.ase_state_machines = {
1093
+ **{
1094
+ id: AseStateMachine(role=AudioRole.SINK, ase_id=id, service=self)
1095
+ for id in sink_ase_id
1096
+ },
1097
+ **{
1098
+ id: AseStateMachine(role=AudioRole.SOURCE, ase_id=id, service=self)
1099
+ for id in source_ase_id
1100
+ },
1101
+ } # ASE state machines, by ASE ID
1102
+
1103
+ self.ase_control_point = gatt.Characteristic(
1104
+ uuid=gatt.GATT_ASE_CONTROL_POINT_CHARACTERISTIC,
1105
+ properties=gatt.Characteristic.Properties.WRITE
1106
+ | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
1107
+ | gatt.Characteristic.Properties.NOTIFY,
1108
+ permissions=gatt.Characteristic.Permissions.WRITEABLE,
1109
+ value=gatt.CharacteristicValue(write=self.on_write_ase_control_point),
1110
+ )
1111
+
1112
+ super().__init__([self.ase_control_point, *self.ase_state_machines.values()])
1113
+
1114
+ def on_operation(self, opcode: ASE_Operation.Opcode, ase_id: int, args):
1115
+ if ase := self.ase_state_machines.get(ase_id):
1116
+ handler = getattr(ase, 'on_' + opcode.name.lower())
1117
+ return (ase_id, *handler(*args))
1118
+ else:
1119
+ return (ase_id, AseResponseCode.INVALID_ASE_ID, AseReasonCode.NONE)
1120
+
1121
+ def on_write_ase_control_point(self, connection, data):
1122
+ operation = ASE_Operation.from_bytes(data)
1123
+ responses = []
1124
+ logger.debug(f'*** ASCS Write {operation} ***')
1125
+
1126
+ if operation.op_code == ASE_Operation.Opcode.CONFIG_CODEC:
1127
+ for ase_id, *args in zip(
1128
+ operation.ase_id,
1129
+ operation.target_latency,
1130
+ operation.target_phy,
1131
+ operation.codec_id,
1132
+ operation.codec_specific_configuration,
1133
+ ):
1134
+ responses.append(self.on_operation(operation.op_code, ase_id, args))
1135
+ elif operation.op_code == ASE_Operation.Opcode.CONFIG_QOS:
1136
+ for ase_id, *args in zip(
1137
+ operation.ase_id,
1138
+ operation.cig_id,
1139
+ operation.cis_id,
1140
+ operation.sdu_interval,
1141
+ operation.framing,
1142
+ operation.phy,
1143
+ operation.max_sdu,
1144
+ operation.retransmission_number,
1145
+ operation.max_transport_latency,
1146
+ operation.presentation_delay,
1147
+ ):
1148
+ responses.append(self.on_operation(operation.op_code, ase_id, args))
1149
+ elif operation.op_code in (
1150
+ ASE_Operation.Opcode.ENABLE,
1151
+ ASE_Operation.Opcode.UPDATE_METADATA,
1152
+ ):
1153
+ for ase_id, *args in zip(
1154
+ operation.ase_id,
1155
+ operation.metadata,
1156
+ ):
1157
+ responses.append(self.on_operation(operation.op_code, ase_id, args))
1158
+ elif operation.op_code in (
1159
+ ASE_Operation.Opcode.RECEIVER_START_READY,
1160
+ ASE_Operation.Opcode.DISABLE,
1161
+ ASE_Operation.Opcode.RECEIVER_STOP_READY,
1162
+ ASE_Operation.Opcode.RELEASE,
1163
+ ):
1164
+ for ase_id in operation.ase_id:
1165
+ responses.append(self.on_operation(operation.op_code, ase_id, []))
1166
+
1167
+ control_point_notification = bytes(
1168
+ [operation.op_code, len(responses)]
1169
+ ) + b''.join(map(bytes, responses))
1170
+ self.device.abort_on(
1171
+ 'flush',
1172
+ self.device.notify_subscribers(
1173
+ self.ase_control_point, control_point_notification
1174
+ ),
1175
+ )
1176
+
1177
+ for ase_id, *_ in responses:
1178
+ if ase := self.ase_state_machines.get(ase_id):
1179
+ self.device.abort_on(
1180
+ 'flush',
1181
+ self.device.notify_subscribers(ase, ase.value),
1182
+ )
1183
+
1184
+
1185
+ # -----------------------------------------------------------------------------
1186
+ # Client
1187
+ # -----------------------------------------------------------------------------
1188
+ class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
1189
+ SERVICE_CLASS = PublishedAudioCapabilitiesService
1190
+
1191
+ sink_pac: Optional[gatt_client.CharacteristicProxy] = None
1192
+ sink_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
1193
+ source_pac: Optional[gatt_client.CharacteristicProxy] = None
1194
+ source_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
1195
+ available_audio_contexts: gatt_client.CharacteristicProxy
1196
+ supported_audio_contexts: gatt_client.CharacteristicProxy
1197
+
1198
+ def __init__(self, service_proxy: gatt_client.ServiceProxy):
1199
+ self.service_proxy = service_proxy
1200
+
1201
+ self.available_audio_contexts = service_proxy.get_characteristics_by_uuid(
1202
+ gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
1203
+ )[0]
1204
+ self.supported_audio_contexts = service_proxy.get_characteristics_by_uuid(
1205
+ gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
1206
+ )[0]
1207
+
1208
+ if characteristics := service_proxy.get_characteristics_by_uuid(
1209
+ gatt.GATT_SINK_PAC_CHARACTERISTIC
1210
+ ):
1211
+ self.sink_pac = characteristics[0]
1212
+
1213
+ if characteristics := service_proxy.get_characteristics_by_uuid(
1214
+ gatt.GATT_SOURCE_PAC_CHARACTERISTIC
1215
+ ):
1216
+ self.source_pac = characteristics[0]
1217
+
1218
+ if characteristics := service_proxy.get_characteristics_by_uuid(
1219
+ gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
1220
+ ):
1221
+ self.sink_audio_locations = characteristics[0]
1222
+
1223
+ if characteristics := service_proxy.get_characteristics_by_uuid(
1224
+ gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
1225
+ ):
1226
+ self.source_audio_locations = characteristics[0]
1227
+
1228
+
1229
+ class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
1230
+ SERVICE_CLASS = AudioStreamControlService
1231
+
1232
+ sink_ase: List[gatt_client.CharacteristicProxy]
1233
+ source_ase: List[gatt_client.CharacteristicProxy]
1234
+ ase_control_point: gatt_client.CharacteristicProxy
1235
+
1236
+ def __init__(self, service_proxy: gatt_client.ServiceProxy):
1237
+ self.service_proxy = service_proxy
1238
+
1239
+ self.sink_ase = service_proxy.get_characteristics_by_uuid(
1240
+ gatt.GATT_SINK_ASE_CHARACTERISTIC
1241
+ )
1242
+ self.source_ase = service_proxy.get_characteristics_by_uuid(
1243
+ gatt.GATT_SOURCE_ASE_CHARACTERISTIC
1244
+ )
1245
+ self.ase_control_point = service_proxy.get_characteristics_by_uuid(
1246
+ gatt.GATT_ASE_CONTROL_POINT_CHARACTERISTIC
1247
+ )[0]