bumble 0.0.194__py3-none-any.whl → 0.0.198__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.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +692 -0
- bumble/apps/bench.py +77 -23
- bumble/apps/console.py +5 -20
- bumble/apps/controller_info.py +3 -3
- bumble/apps/device_info.py +230 -0
- bumble/apps/gatt_dump.py +4 -0
- bumble/apps/lea_unicast/app.py +16 -17
- bumble/at.py +12 -6
- bumble/avc.py +8 -5
- bumble/avctp.py +3 -2
- bumble/avdtp.py +5 -1
- bumble/avrcp.py +2 -1
- bumble/codecs.py +17 -13
- bumble/colors.py +6 -2
- bumble/core.py +726 -122
- bumble/device.py +817 -117
- bumble/drivers/rtk.py +13 -8
- bumble/gatt.py +6 -1
- bumble/gatt_client.py +10 -4
- bumble/hci.py +283 -20
- bumble/hid.py +24 -28
- bumble/host.py +29 -0
- bumble/l2cap.py +24 -17
- bumble/link.py +8 -3
- bumble/pandora/host.py +3 -2
- bumble/profiles/ascs.py +739 -0
- bumble/profiles/bap.py +85 -862
- bumble/profiles/bass.py +440 -0
- bumble/profiles/csip.py +4 -4
- bumble/profiles/gap.py +110 -0
- bumble/profiles/heart_rate_service.py +4 -3
- bumble/profiles/le_audio.py +83 -0
- bumble/profiles/mcp.py +448 -0
- bumble/profiles/pacs.py +210 -0
- bumble/profiles/pbp.py +46 -0
- bumble/profiles/tmap.py +89 -0
- bumble/rfcomm.py +14 -3
- bumble/sdp.py +13 -11
- bumble/smp.py +20 -8
- bumble/snoop.py +5 -4
- bumble/transport/__init__.py +8 -2
- bumble/transport/android_emulator.py +9 -3
- bumble/transport/android_netsim.py +9 -7
- bumble/transport/common.py +46 -18
- bumble/transport/pyusb.py +2 -2
- bumble/transport/unix.py +56 -0
- bumble/transport/usb.py +57 -46
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/RECORD +54 -43
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.194.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
bumble/profiles/bap.py
CHANGED
|
@@ -24,14 +24,14 @@ import enum
|
|
|
24
24
|
import struct
|
|
25
25
|
import functools
|
|
26
26
|
import logging
|
|
27
|
-
from typing import
|
|
27
|
+
from typing import List
|
|
28
|
+
from typing_extensions import Self
|
|
28
29
|
|
|
29
30
|
from bumble import core
|
|
30
|
-
from bumble import colors
|
|
31
|
-
from bumble import device
|
|
32
31
|
from bumble import hci
|
|
33
32
|
from bumble import gatt
|
|
34
|
-
from bumble import
|
|
33
|
+
from bumble import utils
|
|
34
|
+
from bumble.profiles import le_audio
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
# -----------------------------------------------------------------------------
|
|
@@ -115,7 +115,7 @@ class ContextType(enum.IntFlag):
|
|
|
115
115
|
EMERGENCY_ALARM = 0x0800
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
class SamplingFrequency(
|
|
118
|
+
class SamplingFrequency(utils.OpenIntEnum):
|
|
119
119
|
'''Bluetooth Assigned Numbers, Section 6.12.5.1 - Sampling Frequency'''
|
|
120
120
|
|
|
121
121
|
# fmt: off
|
|
@@ -240,7 +240,7 @@ class SupportedFrameDuration(enum.IntFlag):
|
|
|
240
240
|
DURATION_10000_US_PREFERRED = 0b0010
|
|
241
241
|
|
|
242
242
|
|
|
243
|
-
class AnnouncementType(
|
|
243
|
+
class AnnouncementType(utils.OpenIntEnum):
|
|
244
244
|
'''Basic Audio Profile, 3.5.3. Additional Audio Stream Control Service requirements'''
|
|
245
245
|
|
|
246
246
|
# fmt: off
|
|
@@ -248,231 +248,6 @@ class AnnouncementType(enum.IntEnum):
|
|
|
248
248
|
TARGETED = 0x01
|
|
249
249
|
|
|
250
250
|
|
|
251
|
-
# -----------------------------------------------------------------------------
|
|
252
|
-
# ASE Operations
|
|
253
|
-
# -----------------------------------------------------------------------------
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
class ASE_Operation:
|
|
257
|
-
'''
|
|
258
|
-
See Audio Stream Control Service - 5 ASE Control operations.
|
|
259
|
-
'''
|
|
260
|
-
|
|
261
|
-
classes: Dict[int, Type[ASE_Operation]] = {}
|
|
262
|
-
op_code: int
|
|
263
|
-
name: str
|
|
264
|
-
fields: Optional[Sequence[Any]] = None
|
|
265
|
-
ase_id: List[int]
|
|
266
|
-
|
|
267
|
-
class Opcode(enum.IntEnum):
|
|
268
|
-
# fmt: off
|
|
269
|
-
CONFIG_CODEC = 0x01
|
|
270
|
-
CONFIG_QOS = 0x02
|
|
271
|
-
ENABLE = 0x03
|
|
272
|
-
RECEIVER_START_READY = 0x04
|
|
273
|
-
DISABLE = 0x05
|
|
274
|
-
RECEIVER_STOP_READY = 0x06
|
|
275
|
-
UPDATE_METADATA = 0x07
|
|
276
|
-
RELEASE = 0x08
|
|
277
|
-
|
|
278
|
-
@staticmethod
|
|
279
|
-
def from_bytes(pdu: bytes) -> ASE_Operation:
|
|
280
|
-
op_code = pdu[0]
|
|
281
|
-
|
|
282
|
-
cls = ASE_Operation.classes.get(op_code)
|
|
283
|
-
if cls is None:
|
|
284
|
-
instance = ASE_Operation(pdu)
|
|
285
|
-
instance.name = ASE_Operation.Opcode(op_code).name
|
|
286
|
-
instance.op_code = op_code
|
|
287
|
-
return instance
|
|
288
|
-
self = cls.__new__(cls)
|
|
289
|
-
ASE_Operation.__init__(self, pdu)
|
|
290
|
-
if self.fields is not None:
|
|
291
|
-
self.init_from_bytes(pdu, 1)
|
|
292
|
-
return self
|
|
293
|
-
|
|
294
|
-
@staticmethod
|
|
295
|
-
def subclass(fields):
|
|
296
|
-
def inner(cls: Type[ASE_Operation]):
|
|
297
|
-
try:
|
|
298
|
-
operation = ASE_Operation.Opcode[cls.__name__[4:].upper()]
|
|
299
|
-
cls.name = operation.name
|
|
300
|
-
cls.op_code = operation
|
|
301
|
-
except:
|
|
302
|
-
raise KeyError(f'PDU name {cls.name} not found in Ase_Operation.Opcode')
|
|
303
|
-
cls.fields = fields
|
|
304
|
-
|
|
305
|
-
# Register a factory for this class
|
|
306
|
-
ASE_Operation.classes[cls.op_code] = cls
|
|
307
|
-
|
|
308
|
-
return cls
|
|
309
|
-
|
|
310
|
-
return inner
|
|
311
|
-
|
|
312
|
-
def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
|
|
313
|
-
if self.fields is not None and kwargs:
|
|
314
|
-
hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
|
|
315
|
-
if pdu is None:
|
|
316
|
-
pdu = bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
|
|
317
|
-
kwargs, self.fields
|
|
318
|
-
)
|
|
319
|
-
self.pdu = pdu
|
|
320
|
-
|
|
321
|
-
def init_from_bytes(self, pdu: bytes, offset: int):
|
|
322
|
-
return hci.HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
|
|
323
|
-
|
|
324
|
-
def __bytes__(self) -> bytes:
|
|
325
|
-
return self.pdu
|
|
326
|
-
|
|
327
|
-
def __str__(self) -> str:
|
|
328
|
-
result = f'{colors.color(self.name, "yellow")} '
|
|
329
|
-
if fields := getattr(self, 'fields', None):
|
|
330
|
-
result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ')
|
|
331
|
-
else:
|
|
332
|
-
if len(self.pdu) > 1:
|
|
333
|
-
result += f': {self.pdu.hex()}'
|
|
334
|
-
return result
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
@ASE_Operation.subclass(
|
|
338
|
-
[
|
|
339
|
-
[
|
|
340
|
-
('ase_id', 1),
|
|
341
|
-
('target_latency', 1),
|
|
342
|
-
('target_phy', 1),
|
|
343
|
-
('codec_id', hci.CodingFormat.parse_from_bytes),
|
|
344
|
-
('codec_specific_configuration', 'v'),
|
|
345
|
-
],
|
|
346
|
-
]
|
|
347
|
-
)
|
|
348
|
-
class ASE_Config_Codec(ASE_Operation):
|
|
349
|
-
'''
|
|
350
|
-
See Audio Stream Control Service 5.1 - Config Codec Operation
|
|
351
|
-
'''
|
|
352
|
-
|
|
353
|
-
target_latency: List[int]
|
|
354
|
-
target_phy: List[int]
|
|
355
|
-
codec_id: List[hci.CodingFormat]
|
|
356
|
-
codec_specific_configuration: List[bytes]
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
@ASE_Operation.subclass(
|
|
360
|
-
[
|
|
361
|
-
[
|
|
362
|
-
('ase_id', 1),
|
|
363
|
-
('cig_id', 1),
|
|
364
|
-
('cis_id', 1),
|
|
365
|
-
('sdu_interval', 3),
|
|
366
|
-
('framing', 1),
|
|
367
|
-
('phy', 1),
|
|
368
|
-
('max_sdu', 2),
|
|
369
|
-
('retransmission_number', 1),
|
|
370
|
-
('max_transport_latency', 2),
|
|
371
|
-
('presentation_delay', 3),
|
|
372
|
-
],
|
|
373
|
-
]
|
|
374
|
-
)
|
|
375
|
-
class ASE_Config_QOS(ASE_Operation):
|
|
376
|
-
'''
|
|
377
|
-
See Audio Stream Control Service 5.2 - Config Qos Operation
|
|
378
|
-
'''
|
|
379
|
-
|
|
380
|
-
cig_id: List[int]
|
|
381
|
-
cis_id: List[int]
|
|
382
|
-
sdu_interval: List[int]
|
|
383
|
-
framing: List[int]
|
|
384
|
-
phy: List[int]
|
|
385
|
-
max_sdu: List[int]
|
|
386
|
-
retransmission_number: List[int]
|
|
387
|
-
max_transport_latency: List[int]
|
|
388
|
-
presentation_delay: List[int]
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
@ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
|
|
392
|
-
class ASE_Enable(ASE_Operation):
|
|
393
|
-
'''
|
|
394
|
-
See Audio Stream Control Service 5.3 - Enable Operation
|
|
395
|
-
'''
|
|
396
|
-
|
|
397
|
-
metadata: bytes
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
@ASE_Operation.subclass([[('ase_id', 1)]])
|
|
401
|
-
class ASE_Receiver_Start_Ready(ASE_Operation):
|
|
402
|
-
'''
|
|
403
|
-
See Audio Stream Control Service 5.4 - Receiver Start Ready Operation
|
|
404
|
-
'''
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
@ASE_Operation.subclass([[('ase_id', 1)]])
|
|
408
|
-
class ASE_Disable(ASE_Operation):
|
|
409
|
-
'''
|
|
410
|
-
See Audio Stream Control Service 5.5 - Disable Operation
|
|
411
|
-
'''
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
@ASE_Operation.subclass([[('ase_id', 1)]])
|
|
415
|
-
class ASE_Receiver_Stop_Ready(ASE_Operation):
|
|
416
|
-
'''
|
|
417
|
-
See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation
|
|
418
|
-
'''
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
@ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
|
|
422
|
-
class ASE_Update_Metadata(ASE_Operation):
|
|
423
|
-
'''
|
|
424
|
-
See Audio Stream Control Service 5.7 - Update Metadata Operation
|
|
425
|
-
'''
|
|
426
|
-
|
|
427
|
-
metadata: List[bytes]
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
@ASE_Operation.subclass([[('ase_id', 1)]])
|
|
431
|
-
class ASE_Release(ASE_Operation):
|
|
432
|
-
'''
|
|
433
|
-
See Audio Stream Control Service 5.8 - Release Operation
|
|
434
|
-
'''
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
class AseResponseCode(enum.IntEnum):
|
|
438
|
-
# fmt: off
|
|
439
|
-
SUCCESS = 0x00
|
|
440
|
-
UNSUPPORTED_OPCODE = 0x01
|
|
441
|
-
INVALID_LENGTH = 0x02
|
|
442
|
-
INVALID_ASE_ID = 0x03
|
|
443
|
-
INVALID_ASE_STATE_MACHINE_TRANSITION = 0x04
|
|
444
|
-
INVALID_ASE_DIRECTION = 0x05
|
|
445
|
-
UNSUPPORTED_AUDIO_CAPABILITIES = 0x06
|
|
446
|
-
UNSUPPORTED_CONFIGURATION_PARAMETER_VALUE = 0x07
|
|
447
|
-
REJECTED_CONFIGURATION_PARAMETER_VALUE = 0x08
|
|
448
|
-
INVALID_CONFIGURATION_PARAMETER_VALUE = 0x09
|
|
449
|
-
UNSUPPORTED_METADATA = 0x0A
|
|
450
|
-
REJECTED_METADATA = 0x0B
|
|
451
|
-
INVALID_METADATA = 0x0C
|
|
452
|
-
INSUFFICIENT_RESOURCES = 0x0D
|
|
453
|
-
UNSPECIFIED_ERROR = 0x0E
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
class AseReasonCode(enum.IntEnum):
|
|
457
|
-
# fmt: off
|
|
458
|
-
NONE = 0x00
|
|
459
|
-
CODEC_ID = 0x01
|
|
460
|
-
CODEC_SPECIFIC_CONFIGURATION = 0x02
|
|
461
|
-
SDU_INTERVAL = 0x03
|
|
462
|
-
FRAMING = 0x04
|
|
463
|
-
PHY = 0x05
|
|
464
|
-
MAXIMUM_SDU_SIZE = 0x06
|
|
465
|
-
RETRANSMISSION_NUMBER = 0x07
|
|
466
|
-
MAX_TRANSPORT_LATENCY = 0x08
|
|
467
|
-
PRESENTATION_DELAY = 0x09
|
|
468
|
-
INVALID_ASE_CIS_MAPPING = 0x0A
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
class AudioRole(enum.IntEnum):
|
|
472
|
-
SINK = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.CONTROLLER_TO_HOST
|
|
473
|
-
SOURCE = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.HOST_TO_CONTROLLER
|
|
474
|
-
|
|
475
|
-
|
|
476
251
|
@dataclasses.dataclass
|
|
477
252
|
class UnicastServerAdvertisingData:
|
|
478
253
|
"""Advertising Data for ASCS."""
|
|
@@ -613,7 +388,7 @@ class CodecSpecificConfiguration:
|
|
|
613
388
|
* Basic Audio Profile, 4.3.2 - Codec_Specific_Capabilities LTV requirements
|
|
614
389
|
'''
|
|
615
390
|
|
|
616
|
-
class Type(
|
|
391
|
+
class Type(utils.OpenIntEnum):
|
|
617
392
|
# fmt: off
|
|
618
393
|
SAMPLING_FREQUENCY = 0x01
|
|
619
394
|
FRAME_DURATION = 0x02
|
|
@@ -681,645 +456,93 @@ class CodecSpecificConfiguration:
|
|
|
681
456
|
|
|
682
457
|
|
|
683
458
|
@dataclasses.dataclass
|
|
684
|
-
class
|
|
685
|
-
|
|
686
|
-
codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
|
|
687
|
-
# TODO: Parse Metadata
|
|
688
|
-
metadata: bytes = b''
|
|
459
|
+
class BroadcastAudioAnnouncement:
|
|
460
|
+
broadcast_id: int
|
|
689
461
|
|
|
690
462
|
@classmethod
|
|
691
|
-
def from_bytes(cls, data: bytes) ->
|
|
692
|
-
|
|
693
|
-
codec_specific_capabilities_size = data[offset]
|
|
694
|
-
|
|
695
|
-
offset += 1
|
|
696
|
-
codec_specific_capabilities_bytes = data[
|
|
697
|
-
offset : offset + codec_specific_capabilities_size
|
|
698
|
-
]
|
|
699
|
-
offset += codec_specific_capabilities_size
|
|
700
|
-
metadata_size = data[offset]
|
|
701
|
-
metadata = data[offset : offset + metadata_size]
|
|
702
|
-
|
|
703
|
-
codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
|
|
704
|
-
if coding_format.codec_id == hci.CodecID.VENDOR_SPECIFIC:
|
|
705
|
-
codec_specific_capabilities = codec_specific_capabilities_bytes
|
|
706
|
-
else:
|
|
707
|
-
codec_specific_capabilities = CodecSpecificCapabilities.from_bytes(
|
|
708
|
-
codec_specific_capabilities_bytes
|
|
709
|
-
)
|
|
710
|
-
|
|
711
|
-
return PacRecord(
|
|
712
|
-
coding_format=coding_format,
|
|
713
|
-
codec_specific_capabilities=codec_specific_capabilities,
|
|
714
|
-
metadata=metadata,
|
|
715
|
-
)
|
|
716
|
-
|
|
717
|
-
def __bytes__(self) -> bytes:
|
|
718
|
-
capabilities_bytes = bytes(self.codec_specific_capabilities)
|
|
719
|
-
return (
|
|
720
|
-
bytes(self.coding_format)
|
|
721
|
-
+ bytes([len(capabilities_bytes)])
|
|
722
|
-
+ capabilities_bytes
|
|
723
|
-
+ bytes([len(self.metadata)])
|
|
724
|
-
+ self.metadata
|
|
725
|
-
)
|
|
726
|
-
|
|
463
|
+
def from_bytes(cls, data: bytes) -> Self:
|
|
464
|
+
return cls(int.from_bytes(data[:3], 'little'))
|
|
727
465
|
|
|
728
|
-
# -----------------------------------------------------------------------------
|
|
729
|
-
# Server
|
|
730
|
-
# -----------------------------------------------------------------------------
|
|
731
|
-
class PublishedAudioCapabilitiesService(gatt.TemplateService):
|
|
732
|
-
UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
|
|
733
|
-
|
|
734
|
-
sink_pac: Optional[gatt.Characteristic]
|
|
735
|
-
sink_audio_locations: Optional[gatt.Characteristic]
|
|
736
|
-
source_pac: Optional[gatt.Characteristic]
|
|
737
|
-
source_audio_locations: Optional[gatt.Characteristic]
|
|
738
|
-
available_audio_contexts: gatt.Characteristic
|
|
739
|
-
supported_audio_contexts: gatt.Characteristic
|
|
740
|
-
|
|
741
|
-
def __init__(
|
|
742
|
-
self,
|
|
743
|
-
supported_source_context: ContextType,
|
|
744
|
-
supported_sink_context: ContextType,
|
|
745
|
-
available_source_context: ContextType,
|
|
746
|
-
available_sink_context: ContextType,
|
|
747
|
-
sink_pac: Sequence[PacRecord] = [],
|
|
748
|
-
sink_audio_locations: Optional[AudioLocation] = None,
|
|
749
|
-
source_pac: Sequence[PacRecord] = [],
|
|
750
|
-
source_audio_locations: Optional[AudioLocation] = None,
|
|
751
|
-
) -> None:
|
|
752
|
-
characteristics = []
|
|
753
|
-
|
|
754
|
-
self.supported_audio_contexts = gatt.Characteristic(
|
|
755
|
-
uuid=gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC,
|
|
756
|
-
properties=gatt.Characteristic.Properties.READ,
|
|
757
|
-
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
758
|
-
value=struct.pack('<HH', supported_sink_context, supported_source_context),
|
|
759
|
-
)
|
|
760
|
-
characteristics.append(self.supported_audio_contexts)
|
|
761
|
-
|
|
762
|
-
self.available_audio_contexts = gatt.Characteristic(
|
|
763
|
-
uuid=gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC,
|
|
764
|
-
properties=gatt.Characteristic.Properties.READ
|
|
765
|
-
| gatt.Characteristic.Properties.NOTIFY,
|
|
766
|
-
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
767
|
-
value=struct.pack('<HH', available_sink_context, available_source_context),
|
|
768
|
-
)
|
|
769
|
-
characteristics.append(self.available_audio_contexts)
|
|
770
|
-
|
|
771
|
-
if sink_pac:
|
|
772
|
-
self.sink_pac = gatt.Characteristic(
|
|
773
|
-
uuid=gatt.GATT_SINK_PAC_CHARACTERISTIC,
|
|
774
|
-
properties=gatt.Characteristic.Properties.READ,
|
|
775
|
-
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
776
|
-
value=bytes([len(sink_pac)]) + b''.join(map(bytes, sink_pac)),
|
|
777
|
-
)
|
|
778
|
-
characteristics.append(self.sink_pac)
|
|
779
|
-
|
|
780
|
-
if sink_audio_locations is not None:
|
|
781
|
-
self.sink_audio_locations = gatt.Characteristic(
|
|
782
|
-
uuid=gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC,
|
|
783
|
-
properties=gatt.Characteristic.Properties.READ,
|
|
784
|
-
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
785
|
-
value=struct.pack('<I', sink_audio_locations),
|
|
786
|
-
)
|
|
787
|
-
characteristics.append(self.sink_audio_locations)
|
|
788
|
-
|
|
789
|
-
if source_pac:
|
|
790
|
-
self.source_pac = gatt.Characteristic(
|
|
791
|
-
uuid=gatt.GATT_SOURCE_PAC_CHARACTERISTIC,
|
|
792
|
-
properties=gatt.Characteristic.Properties.READ,
|
|
793
|
-
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
794
|
-
value=bytes([len(source_pac)]) + b''.join(map(bytes, source_pac)),
|
|
795
|
-
)
|
|
796
|
-
characteristics.append(self.source_pac)
|
|
797
|
-
|
|
798
|
-
if source_audio_locations is not None:
|
|
799
|
-
self.source_audio_locations = gatt.Characteristic(
|
|
800
|
-
uuid=gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC,
|
|
801
|
-
properties=gatt.Characteristic.Properties.READ,
|
|
802
|
-
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
803
|
-
value=struct.pack('<I', source_audio_locations),
|
|
804
|
-
)
|
|
805
|
-
characteristics.append(self.source_audio_locations)
|
|
806
466
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
class
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
cig_id = 0
|
|
837
|
-
cis_id = 0
|
|
838
|
-
sdu_interval = 0
|
|
839
|
-
framing = 0
|
|
840
|
-
phy = 0
|
|
841
|
-
max_sdu = 0
|
|
842
|
-
retransmission_number = 0
|
|
843
|
-
max_transport_latency = 0
|
|
844
|
-
presentation_delay = 0
|
|
845
|
-
|
|
846
|
-
# Additional parameters in ENABLING, STREAMING, DISABLING State
|
|
847
|
-
# TODO: Parse this
|
|
848
|
-
metadata = b''
|
|
849
|
-
|
|
850
|
-
def __init__(
|
|
851
|
-
self,
|
|
852
|
-
role: AudioRole,
|
|
853
|
-
ase_id: int,
|
|
854
|
-
service: AudioStreamControlService,
|
|
855
|
-
) -> None:
|
|
856
|
-
self.service = service
|
|
857
|
-
self.ase_id = ase_id
|
|
858
|
-
self._state = AseStateMachine.State.IDLE
|
|
859
|
-
self.role = role
|
|
860
|
-
|
|
861
|
-
uuid = (
|
|
862
|
-
gatt.GATT_SINK_ASE_CHARACTERISTIC
|
|
863
|
-
if role == AudioRole.SINK
|
|
864
|
-
else gatt.GATT_SOURCE_ASE_CHARACTERISTIC
|
|
865
|
-
)
|
|
866
|
-
super().__init__(
|
|
867
|
-
uuid=uuid,
|
|
868
|
-
properties=gatt.Characteristic.Properties.READ
|
|
869
|
-
| gatt.Characteristic.Properties.NOTIFY,
|
|
870
|
-
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
871
|
-
value=gatt.CharacteristicValue(read=self.on_read),
|
|
872
|
-
)
|
|
467
|
+
@dataclasses.dataclass
|
|
468
|
+
class BasicAudioAnnouncement:
|
|
469
|
+
@dataclasses.dataclass
|
|
470
|
+
class BIS:
|
|
471
|
+
index: int
|
|
472
|
+
codec_specific_configuration: CodecSpecificConfiguration
|
|
473
|
+
|
|
474
|
+
@dataclasses.dataclass
|
|
475
|
+
class CodecInfo:
|
|
476
|
+
coding_format: hci.CodecID
|
|
477
|
+
company_id: int
|
|
478
|
+
vendor_specific_codec_id: int
|
|
479
|
+
|
|
480
|
+
@classmethod
|
|
481
|
+
def from_bytes(cls, data: bytes) -> Self:
|
|
482
|
+
coding_format = hci.CodecID(data[0])
|
|
483
|
+
company_id = int.from_bytes(data[1:3], 'little')
|
|
484
|
+
vendor_specific_codec_id = int.from_bytes(data[3:5], 'little')
|
|
485
|
+
return cls(coding_format, company_id, vendor_specific_codec_id)
|
|
486
|
+
|
|
487
|
+
@dataclasses.dataclass
|
|
488
|
+
class Subgroup:
|
|
489
|
+
codec_id: BasicAudioAnnouncement.CodecInfo
|
|
490
|
+
codec_specific_configuration: CodecSpecificConfiguration
|
|
491
|
+
metadata: le_audio.Metadata
|
|
492
|
+
bis: List[BasicAudioAnnouncement.BIS]
|
|
493
|
+
|
|
494
|
+
presentation_delay: int
|
|
495
|
+
subgroups: List[BasicAudioAnnouncement.Subgroup]
|
|
873
496
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
497
|
+
@classmethod
|
|
498
|
+
def from_bytes(cls, data: bytes) -> Self:
|
|
499
|
+
presentation_delay = int.from_bytes(data[:3], 'little')
|
|
500
|
+
subgroups = []
|
|
501
|
+
offset = 4
|
|
502
|
+
for _ in range(data[3]):
|
|
503
|
+
num_bis = data[offset]
|
|
504
|
+
offset += 1
|
|
505
|
+
codec_id = cls.CodecInfo.from_bytes(data[offset : offset + 5])
|
|
506
|
+
offset += 5
|
|
507
|
+
codec_specific_configuration_length = data[offset]
|
|
508
|
+
offset += 1
|
|
509
|
+
codec_specific_configuration = data[
|
|
510
|
+
offset : offset + codec_specific_configuration_length
|
|
511
|
+
]
|
|
512
|
+
offset += codec_specific_configuration_length
|
|
513
|
+
metadata_length = data[offset]
|
|
514
|
+
offset += 1
|
|
515
|
+
metadata = le_audio.Metadata.from_bytes(
|
|
516
|
+
data[offset : offset + metadata_length]
|
|
891
517
|
)
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
518
|
+
offset += metadata_length
|
|
519
|
+
|
|
520
|
+
bis = []
|
|
521
|
+
for _ in range(num_bis):
|
|
522
|
+
bis_index = data[offset]
|
|
523
|
+
offset += 1
|
|
524
|
+
bis_codec_specific_configuration_length = data[offset]
|
|
525
|
+
offset += 1
|
|
526
|
+
bis_codec_specific_configuration = data[
|
|
527
|
+
offset : offset + bis_codec_specific_configuration_length
|
|
528
|
+
]
|
|
529
|
+
offset += bis_codec_specific_configuration_length
|
|
530
|
+
bis.append(
|
|
531
|
+
cls.BIS(
|
|
532
|
+
bis_index,
|
|
533
|
+
CodecSpecificConfiguration.from_bytes(
|
|
534
|
+
bis_codec_specific_configuration
|
|
535
|
+
),
|
|
910
536
|
)
|
|
911
537
|
)
|
|
912
|
-
if self.role == AudioRole.SINK:
|
|
913
|
-
self.state = self.State.STREAMING
|
|
914
|
-
await self.service.device.notify_subscribers(self, self.value)
|
|
915
|
-
|
|
916
|
-
cis_link.acl_connection.abort_on('flush', post_cis_established())
|
|
917
|
-
self.cis_link = cis_link
|
|
918
|
-
|
|
919
|
-
def on_cis_disconnection(self, _reason) -> None:
|
|
920
|
-
self.cis_link = None
|
|
921
|
-
|
|
922
|
-
def on_config_codec(
|
|
923
|
-
self,
|
|
924
|
-
target_latency: int,
|
|
925
|
-
target_phy: int,
|
|
926
|
-
codec_id: hci.CodingFormat,
|
|
927
|
-
codec_specific_configuration: bytes,
|
|
928
|
-
) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
929
|
-
if self.state not in (
|
|
930
|
-
self.State.IDLE,
|
|
931
|
-
self.State.CODEC_CONFIGURED,
|
|
932
|
-
self.State.QOS_CONFIGURED,
|
|
933
|
-
):
|
|
934
|
-
return (
|
|
935
|
-
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
936
|
-
AseReasonCode.NONE,
|
|
937
|
-
)
|
|
938
|
-
|
|
939
|
-
self.max_transport_latency = target_latency
|
|
940
|
-
self.phy = target_phy
|
|
941
|
-
self.codec_id = codec_id
|
|
942
|
-
if codec_id.codec_id == hci.CodecID.VENDOR_SPECIFIC:
|
|
943
|
-
self.codec_specific_configuration = codec_specific_configuration
|
|
944
|
-
else:
|
|
945
|
-
self.codec_specific_configuration = CodecSpecificConfiguration.from_bytes(
|
|
946
|
-
codec_specific_configuration
|
|
947
|
-
)
|
|
948
|
-
|
|
949
|
-
self.state = self.State.CODEC_CONFIGURED
|
|
950
|
-
|
|
951
|
-
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
952
|
-
|
|
953
|
-
def on_config_qos(
|
|
954
|
-
self,
|
|
955
|
-
cig_id: int,
|
|
956
|
-
cis_id: int,
|
|
957
|
-
sdu_interval: int,
|
|
958
|
-
framing: int,
|
|
959
|
-
phy: int,
|
|
960
|
-
max_sdu: int,
|
|
961
|
-
retransmission_number: int,
|
|
962
|
-
max_transport_latency: int,
|
|
963
|
-
presentation_delay: int,
|
|
964
|
-
) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
965
|
-
if self.state not in (
|
|
966
|
-
AseStateMachine.State.CODEC_CONFIGURED,
|
|
967
|
-
AseStateMachine.State.QOS_CONFIGURED,
|
|
968
|
-
):
|
|
969
|
-
return (
|
|
970
|
-
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
971
|
-
AseReasonCode.NONE,
|
|
972
|
-
)
|
|
973
|
-
|
|
974
|
-
self.cig_id = cig_id
|
|
975
|
-
self.cis_id = cis_id
|
|
976
|
-
self.sdu_interval = sdu_interval
|
|
977
|
-
self.framing = framing
|
|
978
|
-
self.phy = phy
|
|
979
|
-
self.max_sdu = max_sdu
|
|
980
|
-
self.retransmission_number = retransmission_number
|
|
981
|
-
self.max_transport_latency = max_transport_latency
|
|
982
|
-
self.presentation_delay = presentation_delay
|
|
983
|
-
|
|
984
|
-
self.state = self.State.QOS_CONFIGURED
|
|
985
|
-
|
|
986
|
-
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
987
|
-
|
|
988
|
-
def on_enable(self, metadata: bytes) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
989
|
-
if self.state != AseStateMachine.State.QOS_CONFIGURED:
|
|
990
|
-
return (
|
|
991
|
-
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
992
|
-
AseReasonCode.NONE,
|
|
993
|
-
)
|
|
994
|
-
|
|
995
|
-
self.metadata = metadata
|
|
996
|
-
self.state = self.State.ENABLING
|
|
997
|
-
|
|
998
|
-
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
999
|
-
|
|
1000
|
-
def on_receiver_start_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
1001
|
-
if self.state != AseStateMachine.State.ENABLING:
|
|
1002
|
-
return (
|
|
1003
|
-
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
1004
|
-
AseReasonCode.NONE,
|
|
1005
|
-
)
|
|
1006
|
-
self.state = self.State.STREAMING
|
|
1007
|
-
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
1008
|
-
|
|
1009
|
-
def on_disable(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
1010
|
-
if self.state not in (
|
|
1011
|
-
AseStateMachine.State.ENABLING,
|
|
1012
|
-
AseStateMachine.State.STREAMING,
|
|
1013
|
-
):
|
|
1014
|
-
return (
|
|
1015
|
-
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
1016
|
-
AseReasonCode.NONE,
|
|
1017
|
-
)
|
|
1018
|
-
if self.role == AudioRole.SINK:
|
|
1019
|
-
self.state = self.State.QOS_CONFIGURED
|
|
1020
|
-
else:
|
|
1021
|
-
self.state = self.State.DISABLING
|
|
1022
|
-
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
1023
|
-
|
|
1024
|
-
def on_receiver_stop_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
1025
|
-
if (
|
|
1026
|
-
self.role != AudioRole.SOURCE
|
|
1027
|
-
or self.state != AseStateMachine.State.DISABLING
|
|
1028
|
-
):
|
|
1029
|
-
return (
|
|
1030
|
-
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
1031
|
-
AseReasonCode.NONE,
|
|
1032
|
-
)
|
|
1033
|
-
self.state = self.State.QOS_CONFIGURED
|
|
1034
|
-
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
1035
|
-
|
|
1036
|
-
def on_update_metadata(
|
|
1037
|
-
self, metadata: bytes
|
|
1038
|
-
) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
1039
|
-
if self.state not in (
|
|
1040
|
-
AseStateMachine.State.ENABLING,
|
|
1041
|
-
AseStateMachine.State.STREAMING,
|
|
1042
|
-
):
|
|
1043
|
-
return (
|
|
1044
|
-
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
1045
|
-
AseReasonCode.NONE,
|
|
1046
|
-
)
|
|
1047
|
-
self.metadata = metadata
|
|
1048
|
-
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
1049
|
-
|
|
1050
|
-
def on_release(self) -> Tuple[AseResponseCode, AseReasonCode]:
|
|
1051
|
-
if self.state == AseStateMachine.State.IDLE:
|
|
1052
|
-
return (
|
|
1053
|
-
AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
|
|
1054
|
-
AseReasonCode.NONE,
|
|
1055
|
-
)
|
|
1056
|
-
self.state = self.State.RELEASING
|
|
1057
|
-
|
|
1058
|
-
async def remove_cis_async():
|
|
1059
|
-
await self.service.device.send_command(
|
|
1060
|
-
hci.HCI_LE_Remove_ISO_Data_Path_Command(
|
|
1061
|
-
connection_handle=self.cis_link.handle,
|
|
1062
|
-
data_path_direction=self.role,
|
|
1063
|
-
)
|
|
1064
|
-
)
|
|
1065
|
-
self.state = self.State.IDLE
|
|
1066
|
-
await self.service.device.notify_subscribers(self, self.value)
|
|
1067
|
-
|
|
1068
|
-
self.service.device.abort_on('flush', remove_cis_async())
|
|
1069
|
-
return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
|
|
1070
|
-
|
|
1071
|
-
@property
|
|
1072
|
-
def state(self) -> State:
|
|
1073
|
-
return self._state
|
|
1074
|
-
|
|
1075
|
-
@state.setter
|
|
1076
|
-
def state(self, new_state: State) -> None:
|
|
1077
|
-
logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
|
|
1078
|
-
self._state = new_state
|
|
1079
|
-
self.emit('state_change')
|
|
1080
538
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
self.codec_specific_configuration
|
|
1088
|
-
)
|
|
1089
|
-
additional_parameters = (
|
|
1090
|
-
struct.pack(
|
|
1091
|
-
'<BBBH',
|
|
1092
|
-
self.preferred_framing,
|
|
1093
|
-
self.preferred_phy,
|
|
1094
|
-
self.preferred_retransmission_number,
|
|
1095
|
-
self.preferred_max_transport_latency,
|
|
1096
|
-
)
|
|
1097
|
-
+ self.supported_presentation_delay_min.to_bytes(3, 'little')
|
|
1098
|
-
+ self.supported_presentation_delay_max.to_bytes(3, 'little')
|
|
1099
|
-
+ self.preferred_presentation_delay_min.to_bytes(3, 'little')
|
|
1100
|
-
+ self.preferred_presentation_delay_max.to_bytes(3, 'little')
|
|
1101
|
-
+ bytes(self.codec_id)
|
|
1102
|
-
+ bytes([len(codec_specific_configuration_bytes)])
|
|
1103
|
-
+ codec_specific_configuration_bytes
|
|
1104
|
-
)
|
|
1105
|
-
elif self.state == self.State.QOS_CONFIGURED:
|
|
1106
|
-
additional_parameters = (
|
|
1107
|
-
bytes([self.cig_id, self.cis_id])
|
|
1108
|
-
+ self.sdu_interval.to_bytes(3, 'little')
|
|
1109
|
-
+ struct.pack(
|
|
1110
|
-
'<BBHBH',
|
|
1111
|
-
self.framing,
|
|
1112
|
-
self.phy,
|
|
1113
|
-
self.max_sdu,
|
|
1114
|
-
self.retransmission_number,
|
|
1115
|
-
self.max_transport_latency,
|
|
539
|
+
subgroups.append(
|
|
540
|
+
cls.Subgroup(
|
|
541
|
+
codec_id,
|
|
542
|
+
CodecSpecificConfiguration.from_bytes(codec_specific_configuration),
|
|
543
|
+
metadata,
|
|
544
|
+
bis,
|
|
1116
545
|
)
|
|
1117
|
-
+ self.presentation_delay.to_bytes(3, 'little')
|
|
1118
|
-
)
|
|
1119
|
-
elif self.state in (
|
|
1120
|
-
self.State.ENABLING,
|
|
1121
|
-
self.State.STREAMING,
|
|
1122
|
-
self.State.DISABLING,
|
|
1123
|
-
):
|
|
1124
|
-
additional_parameters = (
|
|
1125
|
-
bytes([self.cig_id, self.cis_id, len(self.metadata)]) + self.metadata
|
|
1126
546
|
)
|
|
1127
|
-
else:
|
|
1128
|
-
additional_parameters = b''
|
|
1129
|
-
|
|
1130
|
-
return bytes([self.ase_id, self.state]) + additional_parameters
|
|
1131
|
-
|
|
1132
|
-
@value.setter
|
|
1133
|
-
def value(self, _new_value):
|
|
1134
|
-
# Readonly. Do nothing in the setter.
|
|
1135
|
-
pass
|
|
1136
547
|
|
|
1137
|
-
|
|
1138
|
-
return self.value
|
|
1139
|
-
|
|
1140
|
-
def __str__(self) -> str:
|
|
1141
|
-
return (
|
|
1142
|
-
f'AseStateMachine(id={self.ase_id}, role={self.role.name} '
|
|
1143
|
-
f'state={self._state.name})'
|
|
1144
|
-
)
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
class AudioStreamControlService(gatt.TemplateService):
|
|
1148
|
-
UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
|
|
1149
|
-
|
|
1150
|
-
ase_state_machines: Dict[int, AseStateMachine]
|
|
1151
|
-
ase_control_point: gatt.Characteristic
|
|
1152
|
-
_active_client: Optional[device.Connection] = None
|
|
1153
|
-
|
|
1154
|
-
def __init__(
|
|
1155
|
-
self,
|
|
1156
|
-
device: device.Device,
|
|
1157
|
-
source_ase_id: Sequence[int] = [],
|
|
1158
|
-
sink_ase_id: Sequence[int] = [],
|
|
1159
|
-
) -> None:
|
|
1160
|
-
self.device = device
|
|
1161
|
-
self.ase_state_machines = {
|
|
1162
|
-
**{
|
|
1163
|
-
id: AseStateMachine(role=AudioRole.SINK, ase_id=id, service=self)
|
|
1164
|
-
for id in sink_ase_id
|
|
1165
|
-
},
|
|
1166
|
-
**{
|
|
1167
|
-
id: AseStateMachine(role=AudioRole.SOURCE, ase_id=id, service=self)
|
|
1168
|
-
for id in source_ase_id
|
|
1169
|
-
},
|
|
1170
|
-
} # ASE state machines, by ASE ID
|
|
1171
|
-
|
|
1172
|
-
self.ase_control_point = gatt.Characteristic(
|
|
1173
|
-
uuid=gatt.GATT_ASE_CONTROL_POINT_CHARACTERISTIC,
|
|
1174
|
-
properties=gatt.Characteristic.Properties.WRITE
|
|
1175
|
-
| gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
|
|
1176
|
-
| gatt.Characteristic.Properties.NOTIFY,
|
|
1177
|
-
permissions=gatt.Characteristic.Permissions.WRITEABLE,
|
|
1178
|
-
value=gatt.CharacteristicValue(write=self.on_write_ase_control_point),
|
|
1179
|
-
)
|
|
1180
|
-
|
|
1181
|
-
super().__init__([self.ase_control_point, *self.ase_state_machines.values()])
|
|
1182
|
-
|
|
1183
|
-
def on_operation(self, opcode: ASE_Operation.Opcode, ase_id: int, args):
|
|
1184
|
-
if ase := self.ase_state_machines.get(ase_id):
|
|
1185
|
-
handler = getattr(ase, 'on_' + opcode.name.lower())
|
|
1186
|
-
return (ase_id, *handler(*args))
|
|
1187
|
-
else:
|
|
1188
|
-
return (ase_id, AseResponseCode.INVALID_ASE_ID, AseReasonCode.NONE)
|
|
1189
|
-
|
|
1190
|
-
def _on_client_disconnected(self, _reason: int) -> None:
|
|
1191
|
-
for ase in self.ase_state_machines.values():
|
|
1192
|
-
ase.state = AseStateMachine.State.IDLE
|
|
1193
|
-
self._active_client = None
|
|
1194
|
-
|
|
1195
|
-
def on_write_ase_control_point(self, connection, data):
|
|
1196
|
-
if not self._active_client and connection:
|
|
1197
|
-
self._active_client = connection
|
|
1198
|
-
connection.once('disconnection', self._on_client_disconnected)
|
|
1199
|
-
|
|
1200
|
-
operation = ASE_Operation.from_bytes(data)
|
|
1201
|
-
responses = []
|
|
1202
|
-
logger.debug(f'*** ASCS Write {operation} ***')
|
|
1203
|
-
|
|
1204
|
-
if operation.op_code == ASE_Operation.Opcode.CONFIG_CODEC:
|
|
1205
|
-
for ase_id, *args in zip(
|
|
1206
|
-
operation.ase_id,
|
|
1207
|
-
operation.target_latency,
|
|
1208
|
-
operation.target_phy,
|
|
1209
|
-
operation.codec_id,
|
|
1210
|
-
operation.codec_specific_configuration,
|
|
1211
|
-
):
|
|
1212
|
-
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
|
1213
|
-
elif operation.op_code == ASE_Operation.Opcode.CONFIG_QOS:
|
|
1214
|
-
for ase_id, *args in zip(
|
|
1215
|
-
operation.ase_id,
|
|
1216
|
-
operation.cig_id,
|
|
1217
|
-
operation.cis_id,
|
|
1218
|
-
operation.sdu_interval,
|
|
1219
|
-
operation.framing,
|
|
1220
|
-
operation.phy,
|
|
1221
|
-
operation.max_sdu,
|
|
1222
|
-
operation.retransmission_number,
|
|
1223
|
-
operation.max_transport_latency,
|
|
1224
|
-
operation.presentation_delay,
|
|
1225
|
-
):
|
|
1226
|
-
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
|
1227
|
-
elif operation.op_code in (
|
|
1228
|
-
ASE_Operation.Opcode.ENABLE,
|
|
1229
|
-
ASE_Operation.Opcode.UPDATE_METADATA,
|
|
1230
|
-
):
|
|
1231
|
-
for ase_id, *args in zip(
|
|
1232
|
-
operation.ase_id,
|
|
1233
|
-
operation.metadata,
|
|
1234
|
-
):
|
|
1235
|
-
responses.append(self.on_operation(operation.op_code, ase_id, args))
|
|
1236
|
-
elif operation.op_code in (
|
|
1237
|
-
ASE_Operation.Opcode.RECEIVER_START_READY,
|
|
1238
|
-
ASE_Operation.Opcode.DISABLE,
|
|
1239
|
-
ASE_Operation.Opcode.RECEIVER_STOP_READY,
|
|
1240
|
-
ASE_Operation.Opcode.RELEASE,
|
|
1241
|
-
):
|
|
1242
|
-
for ase_id in operation.ase_id:
|
|
1243
|
-
responses.append(self.on_operation(operation.op_code, ase_id, []))
|
|
1244
|
-
|
|
1245
|
-
control_point_notification = bytes(
|
|
1246
|
-
[operation.op_code, len(responses)]
|
|
1247
|
-
) + b''.join(map(bytes, responses))
|
|
1248
|
-
self.device.abort_on(
|
|
1249
|
-
'flush',
|
|
1250
|
-
self.device.notify_subscribers(
|
|
1251
|
-
self.ase_control_point, control_point_notification
|
|
1252
|
-
),
|
|
1253
|
-
)
|
|
1254
|
-
|
|
1255
|
-
for ase_id, *_ in responses:
|
|
1256
|
-
if ase := self.ase_state_machines.get(ase_id):
|
|
1257
|
-
self.device.abort_on(
|
|
1258
|
-
'flush',
|
|
1259
|
-
self.device.notify_subscribers(ase, ase.value),
|
|
1260
|
-
)
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
# -----------------------------------------------------------------------------
|
|
1264
|
-
# Client
|
|
1265
|
-
# -----------------------------------------------------------------------------
|
|
1266
|
-
class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
|
|
1267
|
-
SERVICE_CLASS = PublishedAudioCapabilitiesService
|
|
1268
|
-
|
|
1269
|
-
sink_pac: Optional[gatt_client.CharacteristicProxy] = None
|
|
1270
|
-
sink_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
|
|
1271
|
-
source_pac: Optional[gatt_client.CharacteristicProxy] = None
|
|
1272
|
-
source_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
|
|
1273
|
-
available_audio_contexts: gatt_client.CharacteristicProxy
|
|
1274
|
-
supported_audio_contexts: gatt_client.CharacteristicProxy
|
|
1275
|
-
|
|
1276
|
-
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
|
1277
|
-
self.service_proxy = service_proxy
|
|
1278
|
-
|
|
1279
|
-
self.available_audio_contexts = service_proxy.get_characteristics_by_uuid(
|
|
1280
|
-
gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
|
|
1281
|
-
)[0]
|
|
1282
|
-
self.supported_audio_contexts = service_proxy.get_characteristics_by_uuid(
|
|
1283
|
-
gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
|
|
1284
|
-
)[0]
|
|
1285
|
-
|
|
1286
|
-
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
1287
|
-
gatt.GATT_SINK_PAC_CHARACTERISTIC
|
|
1288
|
-
):
|
|
1289
|
-
self.sink_pac = characteristics[0]
|
|
1290
|
-
|
|
1291
|
-
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
1292
|
-
gatt.GATT_SOURCE_PAC_CHARACTERISTIC
|
|
1293
|
-
):
|
|
1294
|
-
self.source_pac = characteristics[0]
|
|
1295
|
-
|
|
1296
|
-
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
1297
|
-
gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
|
|
1298
|
-
):
|
|
1299
|
-
self.sink_audio_locations = characteristics[0]
|
|
1300
|
-
|
|
1301
|
-
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
1302
|
-
gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
|
|
1303
|
-
):
|
|
1304
|
-
self.source_audio_locations = characteristics[0]
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
|
|
1308
|
-
SERVICE_CLASS = AudioStreamControlService
|
|
1309
|
-
|
|
1310
|
-
sink_ase: List[gatt_client.CharacteristicProxy]
|
|
1311
|
-
source_ase: List[gatt_client.CharacteristicProxy]
|
|
1312
|
-
ase_control_point: gatt_client.CharacteristicProxy
|
|
1313
|
-
|
|
1314
|
-
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
|
1315
|
-
self.service_proxy = service_proxy
|
|
1316
|
-
|
|
1317
|
-
self.sink_ase = service_proxy.get_characteristics_by_uuid(
|
|
1318
|
-
gatt.GATT_SINK_ASE_CHARACTERISTIC
|
|
1319
|
-
)
|
|
1320
|
-
self.source_ase = service_proxy.get_characteristics_by_uuid(
|
|
1321
|
-
gatt.GATT_SOURCE_ASE_CHARACTERISTIC
|
|
1322
|
-
)
|
|
1323
|
-
self.ase_control_point = service_proxy.get_characteristics_by_uuid(
|
|
1324
|
-
gatt.GATT_ASE_CONTROL_POINT_CHARACTERISTIC
|
|
1325
|
-
)[0]
|
|
548
|
+
return cls(presentation_delay, subgroups)
|