bumble 0.0.195__py3-none-any.whl → 0.0.199__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 (61) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +351 -66
  3. bumble/apps/console.py +5 -20
  4. bumble/apps/device_info.py +230 -0
  5. bumble/apps/gatt_dump.py +4 -0
  6. bumble/apps/lea_unicast/app.py +16 -17
  7. bumble/apps/pair.py +32 -5
  8. bumble/at.py +12 -6
  9. bumble/att.py +56 -40
  10. bumble/avc.py +8 -5
  11. bumble/avctp.py +3 -2
  12. bumble/avdtp.py +7 -3
  13. bumble/avrcp.py +2 -1
  14. bumble/codecs.py +17 -13
  15. bumble/colors.py +6 -2
  16. bumble/core.py +37 -7
  17. bumble/decoder.py +14 -10
  18. bumble/device.py +382 -111
  19. bumble/drivers/rtk.py +32 -13
  20. bumble/gatt.py +30 -20
  21. bumble/gatt_client.py +15 -29
  22. bumble/gatt_server.py +14 -6
  23. bumble/hci.py +322 -32
  24. bumble/hid.py +24 -28
  25. bumble/host.py +20 -6
  26. bumble/l2cap.py +24 -17
  27. bumble/link.py +8 -3
  28. bumble/pandora/__init__.py +3 -0
  29. bumble/pandora/l2cap.py +310 -0
  30. bumble/profiles/aics.py +520 -0
  31. bumble/profiles/ascs.py +739 -0
  32. bumble/profiles/asha.py +295 -0
  33. bumble/profiles/bap.py +1 -874
  34. bumble/profiles/bass.py +440 -0
  35. bumble/profiles/csip.py +4 -4
  36. bumble/profiles/gap.py +110 -0
  37. bumble/profiles/hap.py +665 -0
  38. bumble/profiles/heart_rate_service.py +4 -3
  39. bumble/profiles/le_audio.py +43 -9
  40. bumble/profiles/mcp.py +448 -0
  41. bumble/profiles/pacs.py +210 -0
  42. bumble/profiles/tmap.py +89 -0
  43. bumble/profiles/vcp.py +5 -3
  44. bumble/rfcomm.py +4 -2
  45. bumble/sdp.py +13 -11
  46. bumble/smp.py +43 -12
  47. bumble/snoop.py +5 -4
  48. bumble/transport/__init__.py +8 -2
  49. bumble/transport/android_emulator.py +9 -3
  50. bumble/transport/android_netsim.py +9 -7
  51. bumble/transport/common.py +46 -18
  52. bumble/transport/pyusb.py +21 -4
  53. bumble/transport/unix.py +56 -0
  54. bumble/transport/usb.py +57 -46
  55. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/METADATA +41 -41
  56. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/RECORD +60 -49
  57. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/WHEEL +1 -1
  58. bumble/profiles/asha_service.py +0 -193
  59. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/LICENSE +0 -0
  60. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/entry_points.txt +0 -0
  61. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/top_level.txt +0 -0
bumble/profiles/bap.py CHANGED
@@ -24,15 +24,12 @@ import enum
24
24
  import struct
25
25
  import functools
26
26
  import logging
27
- from typing import Optional, List, Union, Type, Dict, Any, Tuple
27
+ from typing import List
28
28
  from typing_extensions import Self
29
29
 
30
30
  from bumble import core
31
- from bumble import colors
32
- from bumble import device
33
31
  from bumble import hci
34
32
  from bumble import gatt
35
- from bumble import gatt_client
36
33
  from bumble import utils
37
34
  from bumble.profiles import le_audio
38
35
 
@@ -251,231 +248,6 @@ class AnnouncementType(utils.OpenIntEnum):
251
248
  TARGETED = 0x01
252
249
 
253
250
 
254
- # -----------------------------------------------------------------------------
255
- # ASE Operations
256
- # -----------------------------------------------------------------------------
257
-
258
-
259
- class ASE_Operation:
260
- '''
261
- See Audio Stream Control Service - 5 ASE Control operations.
262
- '''
263
-
264
- classes: Dict[int, Type[ASE_Operation]] = {}
265
- op_code: int
266
- name: str
267
- fields: Optional[Sequence[Any]] = None
268
- ase_id: List[int]
269
-
270
- class Opcode(enum.IntEnum):
271
- # fmt: off
272
- CONFIG_CODEC = 0x01
273
- CONFIG_QOS = 0x02
274
- ENABLE = 0x03
275
- RECEIVER_START_READY = 0x04
276
- DISABLE = 0x05
277
- RECEIVER_STOP_READY = 0x06
278
- UPDATE_METADATA = 0x07
279
- RELEASE = 0x08
280
-
281
- @staticmethod
282
- def from_bytes(pdu: bytes) -> ASE_Operation:
283
- op_code = pdu[0]
284
-
285
- cls = ASE_Operation.classes.get(op_code)
286
- if cls is None:
287
- instance = ASE_Operation(pdu)
288
- instance.name = ASE_Operation.Opcode(op_code).name
289
- instance.op_code = op_code
290
- return instance
291
- self = cls.__new__(cls)
292
- ASE_Operation.__init__(self, pdu)
293
- if self.fields is not None:
294
- self.init_from_bytes(pdu, 1)
295
- return self
296
-
297
- @staticmethod
298
- def subclass(fields):
299
- def inner(cls: Type[ASE_Operation]):
300
- try:
301
- operation = ASE_Operation.Opcode[cls.__name__[4:].upper()]
302
- cls.name = operation.name
303
- cls.op_code = operation
304
- except:
305
- raise KeyError(f'PDU name {cls.name} not found in Ase_Operation.Opcode')
306
- cls.fields = fields
307
-
308
- # Register a factory for this class
309
- ASE_Operation.classes[cls.op_code] = cls
310
-
311
- return cls
312
-
313
- return inner
314
-
315
- def __init__(self, pdu: Optional[bytes] = None, **kwargs) -> None:
316
- if self.fields is not None and kwargs:
317
- hci.HCI_Object.init_from_fields(self, self.fields, kwargs)
318
- if pdu is None:
319
- pdu = bytes([self.op_code]) + hci.HCI_Object.dict_to_bytes(
320
- kwargs, self.fields
321
- )
322
- self.pdu = pdu
323
-
324
- def init_from_bytes(self, pdu: bytes, offset: int):
325
- return hci.HCI_Object.init_from_bytes(self, pdu, offset, self.fields)
326
-
327
- def __bytes__(self) -> bytes:
328
- return self.pdu
329
-
330
- def __str__(self) -> str:
331
- result = f'{colors.color(self.name, "yellow")} '
332
- if fields := getattr(self, 'fields', None):
333
- result += ':\n' + hci.HCI_Object.format_fields(self.__dict__, fields, ' ')
334
- else:
335
- if len(self.pdu) > 1:
336
- result += f': {self.pdu.hex()}'
337
- return result
338
-
339
-
340
- @ASE_Operation.subclass(
341
- [
342
- [
343
- ('ase_id', 1),
344
- ('target_latency', 1),
345
- ('target_phy', 1),
346
- ('codec_id', hci.CodingFormat.parse_from_bytes),
347
- ('codec_specific_configuration', 'v'),
348
- ],
349
- ]
350
- )
351
- class ASE_Config_Codec(ASE_Operation):
352
- '''
353
- See Audio Stream Control Service 5.1 - Config Codec Operation
354
- '''
355
-
356
- target_latency: List[int]
357
- target_phy: List[int]
358
- codec_id: List[hci.CodingFormat]
359
- codec_specific_configuration: List[bytes]
360
-
361
-
362
- @ASE_Operation.subclass(
363
- [
364
- [
365
- ('ase_id', 1),
366
- ('cig_id', 1),
367
- ('cis_id', 1),
368
- ('sdu_interval', 3),
369
- ('framing', 1),
370
- ('phy', 1),
371
- ('max_sdu', 2),
372
- ('retransmission_number', 1),
373
- ('max_transport_latency', 2),
374
- ('presentation_delay', 3),
375
- ],
376
- ]
377
- )
378
- class ASE_Config_QOS(ASE_Operation):
379
- '''
380
- See Audio Stream Control Service 5.2 - Config Qos Operation
381
- '''
382
-
383
- cig_id: List[int]
384
- cis_id: List[int]
385
- sdu_interval: List[int]
386
- framing: List[int]
387
- phy: List[int]
388
- max_sdu: List[int]
389
- retransmission_number: List[int]
390
- max_transport_latency: List[int]
391
- presentation_delay: List[int]
392
-
393
-
394
- @ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
395
- class ASE_Enable(ASE_Operation):
396
- '''
397
- See Audio Stream Control Service 5.3 - Enable Operation
398
- '''
399
-
400
- metadata: bytes
401
-
402
-
403
- @ASE_Operation.subclass([[('ase_id', 1)]])
404
- class ASE_Receiver_Start_Ready(ASE_Operation):
405
- '''
406
- See Audio Stream Control Service 5.4 - Receiver Start Ready Operation
407
- '''
408
-
409
-
410
- @ASE_Operation.subclass([[('ase_id', 1)]])
411
- class ASE_Disable(ASE_Operation):
412
- '''
413
- See Audio Stream Control Service 5.5 - Disable Operation
414
- '''
415
-
416
-
417
- @ASE_Operation.subclass([[('ase_id', 1)]])
418
- class ASE_Receiver_Stop_Ready(ASE_Operation):
419
- '''
420
- See Audio Stream Control Service 5.6 - Receiver Stop Ready Operation
421
- '''
422
-
423
-
424
- @ASE_Operation.subclass([[('ase_id', 1), ('metadata', 'v')]])
425
- class ASE_Update_Metadata(ASE_Operation):
426
- '''
427
- See Audio Stream Control Service 5.7 - Update Metadata Operation
428
- '''
429
-
430
- metadata: List[bytes]
431
-
432
-
433
- @ASE_Operation.subclass([[('ase_id', 1)]])
434
- class ASE_Release(ASE_Operation):
435
- '''
436
- See Audio Stream Control Service 5.8 - Release Operation
437
- '''
438
-
439
-
440
- class AseResponseCode(enum.IntEnum):
441
- # fmt: off
442
- SUCCESS = 0x00
443
- UNSUPPORTED_OPCODE = 0x01
444
- INVALID_LENGTH = 0x02
445
- INVALID_ASE_ID = 0x03
446
- INVALID_ASE_STATE_MACHINE_TRANSITION = 0x04
447
- INVALID_ASE_DIRECTION = 0x05
448
- UNSUPPORTED_AUDIO_CAPABILITIES = 0x06
449
- UNSUPPORTED_CONFIGURATION_PARAMETER_VALUE = 0x07
450
- REJECTED_CONFIGURATION_PARAMETER_VALUE = 0x08
451
- INVALID_CONFIGURATION_PARAMETER_VALUE = 0x09
452
- UNSUPPORTED_METADATA = 0x0A
453
- REJECTED_METADATA = 0x0B
454
- INVALID_METADATA = 0x0C
455
- INSUFFICIENT_RESOURCES = 0x0D
456
- UNSPECIFIED_ERROR = 0x0E
457
-
458
-
459
- class AseReasonCode(enum.IntEnum):
460
- # fmt: off
461
- NONE = 0x00
462
- CODEC_ID = 0x01
463
- CODEC_SPECIFIC_CONFIGURATION = 0x02
464
- SDU_INTERVAL = 0x03
465
- FRAMING = 0x04
466
- PHY = 0x05
467
- MAXIMUM_SDU_SIZE = 0x06
468
- RETRANSMISSION_NUMBER = 0x07
469
- MAX_TRANSPORT_LATENCY = 0x08
470
- PRESENTATION_DELAY = 0x09
471
- INVALID_ASE_CIS_MAPPING = 0x0A
472
-
473
-
474
- class AudioRole(enum.IntEnum):
475
- SINK = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.CONTROLLER_TO_HOST
476
- SOURCE = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.HOST_TO_CONTROLLER
477
-
478
-
479
251
  @dataclasses.dataclass
480
252
  class UnicastServerAdvertisingData:
481
253
  """Advertising Data for ASCS."""
@@ -683,51 +455,6 @@ class CodecSpecificConfiguration:
683
455
  )
684
456
 
685
457
 
686
- @dataclasses.dataclass
687
- class PacRecord:
688
- coding_format: hci.CodingFormat
689
- codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
690
- # TODO: Parse Metadata
691
- metadata: bytes = b''
692
-
693
- @classmethod
694
- def from_bytes(cls, data: bytes) -> PacRecord:
695
- offset, coding_format = hci.CodingFormat.parse_from_bytes(data, 0)
696
- codec_specific_capabilities_size = data[offset]
697
-
698
- offset += 1
699
- codec_specific_capabilities_bytes = data[
700
- offset : offset + codec_specific_capabilities_size
701
- ]
702
- offset += codec_specific_capabilities_size
703
- metadata_size = data[offset]
704
- metadata = data[offset : offset + metadata_size]
705
-
706
- codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
707
- if coding_format.codec_id == hci.CodecID.VENDOR_SPECIFIC:
708
- codec_specific_capabilities = codec_specific_capabilities_bytes
709
- else:
710
- codec_specific_capabilities = CodecSpecificCapabilities.from_bytes(
711
- codec_specific_capabilities_bytes
712
- )
713
-
714
- return PacRecord(
715
- coding_format=coding_format,
716
- codec_specific_capabilities=codec_specific_capabilities,
717
- metadata=metadata,
718
- )
719
-
720
- def __bytes__(self) -> bytes:
721
- capabilities_bytes = bytes(self.codec_specific_capabilities)
722
- return (
723
- bytes(self.coding_format)
724
- + bytes([len(capabilities_bytes)])
725
- + capabilities_bytes
726
- + bytes([len(self.metadata)])
727
- + self.metadata
728
- )
729
-
730
-
731
458
  @dataclasses.dataclass
732
459
  class BroadcastAudioAnnouncement:
733
460
  broadcast_id: int
@@ -819,603 +546,3 @@ class BasicAudioAnnouncement:
819
546
  )
820
547
 
821
548
  return cls(presentation_delay, subgroups)
822
-
823
-
824
- # -----------------------------------------------------------------------------
825
- # Server
826
- # -----------------------------------------------------------------------------
827
- class PublishedAudioCapabilitiesService(gatt.TemplateService):
828
- UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
829
-
830
- sink_pac: Optional[gatt.Characteristic]
831
- sink_audio_locations: Optional[gatt.Characteristic]
832
- source_pac: Optional[gatt.Characteristic]
833
- source_audio_locations: Optional[gatt.Characteristic]
834
- available_audio_contexts: gatt.Characteristic
835
- supported_audio_contexts: gatt.Characteristic
836
-
837
- def __init__(
838
- self,
839
- supported_source_context: ContextType,
840
- supported_sink_context: ContextType,
841
- available_source_context: ContextType,
842
- available_sink_context: ContextType,
843
- sink_pac: Sequence[PacRecord] = (),
844
- sink_audio_locations: Optional[AudioLocation] = None,
845
- source_pac: Sequence[PacRecord] = (),
846
- source_audio_locations: Optional[AudioLocation] = None,
847
- ) -> None:
848
- characteristics = []
849
-
850
- self.supported_audio_contexts = gatt.Characteristic(
851
- uuid=gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC,
852
- properties=gatt.Characteristic.Properties.READ,
853
- permissions=gatt.Characteristic.Permissions.READABLE,
854
- value=struct.pack('<HH', supported_sink_context, supported_source_context),
855
- )
856
- characteristics.append(self.supported_audio_contexts)
857
-
858
- self.available_audio_contexts = gatt.Characteristic(
859
- uuid=gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC,
860
- properties=gatt.Characteristic.Properties.READ
861
- | gatt.Characteristic.Properties.NOTIFY,
862
- permissions=gatt.Characteristic.Permissions.READABLE,
863
- value=struct.pack('<HH', available_sink_context, available_source_context),
864
- )
865
- characteristics.append(self.available_audio_contexts)
866
-
867
- if sink_pac:
868
- self.sink_pac = gatt.Characteristic(
869
- uuid=gatt.GATT_SINK_PAC_CHARACTERISTIC,
870
- properties=gatt.Characteristic.Properties.READ,
871
- permissions=gatt.Characteristic.Permissions.READABLE,
872
- value=bytes([len(sink_pac)]) + b''.join(map(bytes, sink_pac)),
873
- )
874
- characteristics.append(self.sink_pac)
875
-
876
- if sink_audio_locations is not None:
877
- self.sink_audio_locations = gatt.Characteristic(
878
- uuid=gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC,
879
- properties=gatt.Characteristic.Properties.READ,
880
- permissions=gatt.Characteristic.Permissions.READABLE,
881
- value=struct.pack('<I', sink_audio_locations),
882
- )
883
- characteristics.append(self.sink_audio_locations)
884
-
885
- if source_pac:
886
- self.source_pac = gatt.Characteristic(
887
- uuid=gatt.GATT_SOURCE_PAC_CHARACTERISTIC,
888
- properties=gatt.Characteristic.Properties.READ,
889
- permissions=gatt.Characteristic.Permissions.READABLE,
890
- value=bytes([len(source_pac)]) + b''.join(map(bytes, source_pac)),
891
- )
892
- characteristics.append(self.source_pac)
893
-
894
- if source_audio_locations is not None:
895
- self.source_audio_locations = gatt.Characteristic(
896
- uuid=gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC,
897
- properties=gatt.Characteristic.Properties.READ,
898
- permissions=gatt.Characteristic.Permissions.READABLE,
899
- value=struct.pack('<I', source_audio_locations),
900
- )
901
- characteristics.append(self.source_audio_locations)
902
-
903
- super().__init__(characteristics)
904
-
905
-
906
- class AseStateMachine(gatt.Characteristic):
907
- class State(enum.IntEnum):
908
- # fmt: off
909
- IDLE = 0x00
910
- CODEC_CONFIGURED = 0x01
911
- QOS_CONFIGURED = 0x02
912
- ENABLING = 0x03
913
- STREAMING = 0x04
914
- DISABLING = 0x05
915
- RELEASING = 0x06
916
-
917
- cis_link: Optional[device.CisLink] = None
918
-
919
- # Additional parameters in CODEC_CONFIGURED State
920
- preferred_framing = 0 # Unframed PDU supported
921
- preferred_phy = 0
922
- preferred_retransmission_number = 13
923
- preferred_max_transport_latency = 100
924
- supported_presentation_delay_min = 0
925
- supported_presentation_delay_max = 0
926
- preferred_presentation_delay_min = 0
927
- preferred_presentation_delay_max = 0
928
- codec_id = hci.CodingFormat(hci.CodecID.LC3)
929
- codec_specific_configuration: Union[CodecSpecificConfiguration, bytes] = b''
930
-
931
- # Additional parameters in QOS_CONFIGURED State
932
- cig_id = 0
933
- cis_id = 0
934
- sdu_interval = 0
935
- framing = 0
936
- phy = 0
937
- max_sdu = 0
938
- retransmission_number = 0
939
- max_transport_latency = 0
940
- presentation_delay = 0
941
-
942
- # Additional parameters in ENABLING, STREAMING, DISABLING State
943
- # TODO: Parse this
944
- metadata = b''
945
-
946
- def __init__(
947
- self,
948
- role: AudioRole,
949
- ase_id: int,
950
- service: AudioStreamControlService,
951
- ) -> None:
952
- self.service = service
953
- self.ase_id = ase_id
954
- self._state = AseStateMachine.State.IDLE
955
- self.role = role
956
-
957
- uuid = (
958
- gatt.GATT_SINK_ASE_CHARACTERISTIC
959
- if role == AudioRole.SINK
960
- else gatt.GATT_SOURCE_ASE_CHARACTERISTIC
961
- )
962
- super().__init__(
963
- uuid=uuid,
964
- properties=gatt.Characteristic.Properties.READ
965
- | gatt.Characteristic.Properties.NOTIFY,
966
- permissions=gatt.Characteristic.Permissions.READABLE,
967
- value=gatt.CharacteristicValue(read=self.on_read),
968
- )
969
-
970
- self.service.device.on('cis_request', self.on_cis_request)
971
- self.service.device.on('cis_establishment', self.on_cis_establishment)
972
-
973
- def on_cis_request(
974
- self,
975
- acl_connection: device.Connection,
976
- cis_handle: int,
977
- cig_id: int,
978
- cis_id: int,
979
- ) -> None:
980
- if (
981
- cig_id == self.cig_id
982
- and cis_id == self.cis_id
983
- and self.state == self.State.ENABLING
984
- ):
985
- acl_connection.abort_on(
986
- 'flush', self.service.device.accept_cis_request(cis_handle)
987
- )
988
-
989
- def on_cis_establishment(self, cis_link: device.CisLink) -> None:
990
- if (
991
- cis_link.cig_id == self.cig_id
992
- and cis_link.cis_id == self.cis_id
993
- and self.state == self.State.ENABLING
994
- ):
995
- cis_link.on('disconnection', self.on_cis_disconnection)
996
-
997
- async def post_cis_established():
998
- await self.service.device.send_command(
999
- hci.HCI_LE_Setup_ISO_Data_Path_Command(
1000
- connection_handle=cis_link.handle,
1001
- data_path_direction=self.role,
1002
- data_path_id=0x00, # Fixed HCI
1003
- codec_id=hci.CodingFormat(hci.CodecID.TRANSPARENT),
1004
- controller_delay=0,
1005
- codec_configuration=b'',
1006
- )
1007
- )
1008
- if self.role == AudioRole.SINK:
1009
- self.state = self.State.STREAMING
1010
- await self.service.device.notify_subscribers(self, self.value)
1011
-
1012
- cis_link.acl_connection.abort_on('flush', post_cis_established())
1013
- self.cis_link = cis_link
1014
-
1015
- def on_cis_disconnection(self, _reason) -> None:
1016
- self.cis_link = None
1017
-
1018
- def on_config_codec(
1019
- self,
1020
- target_latency: int,
1021
- target_phy: int,
1022
- codec_id: hci.CodingFormat,
1023
- codec_specific_configuration: bytes,
1024
- ) -> Tuple[AseResponseCode, AseReasonCode]:
1025
- if self.state not in (
1026
- self.State.IDLE,
1027
- self.State.CODEC_CONFIGURED,
1028
- self.State.QOS_CONFIGURED,
1029
- ):
1030
- return (
1031
- AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
1032
- AseReasonCode.NONE,
1033
- )
1034
-
1035
- self.max_transport_latency = target_latency
1036
- self.phy = target_phy
1037
- self.codec_id = codec_id
1038
- if codec_id.codec_id == hci.CodecID.VENDOR_SPECIFIC:
1039
- self.codec_specific_configuration = codec_specific_configuration
1040
- else:
1041
- self.codec_specific_configuration = CodecSpecificConfiguration.from_bytes(
1042
- codec_specific_configuration
1043
- )
1044
-
1045
- self.state = self.State.CODEC_CONFIGURED
1046
-
1047
- return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1048
-
1049
- def on_config_qos(
1050
- self,
1051
- cig_id: int,
1052
- cis_id: int,
1053
- sdu_interval: int,
1054
- framing: int,
1055
- phy: int,
1056
- max_sdu: int,
1057
- retransmission_number: int,
1058
- max_transport_latency: int,
1059
- presentation_delay: int,
1060
- ) -> Tuple[AseResponseCode, AseReasonCode]:
1061
- if self.state not in (
1062
- AseStateMachine.State.CODEC_CONFIGURED,
1063
- AseStateMachine.State.QOS_CONFIGURED,
1064
- ):
1065
- return (
1066
- AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
1067
- AseReasonCode.NONE,
1068
- )
1069
-
1070
- self.cig_id = cig_id
1071
- self.cis_id = cis_id
1072
- self.sdu_interval = sdu_interval
1073
- self.framing = framing
1074
- self.phy = phy
1075
- self.max_sdu = max_sdu
1076
- self.retransmission_number = retransmission_number
1077
- self.max_transport_latency = max_transport_latency
1078
- self.presentation_delay = presentation_delay
1079
-
1080
- self.state = self.State.QOS_CONFIGURED
1081
-
1082
- return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1083
-
1084
- def on_enable(self, metadata: bytes) -> Tuple[AseResponseCode, AseReasonCode]:
1085
- if self.state != AseStateMachine.State.QOS_CONFIGURED:
1086
- return (
1087
- AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
1088
- AseReasonCode.NONE,
1089
- )
1090
-
1091
- self.metadata = metadata
1092
- self.state = self.State.ENABLING
1093
-
1094
- return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1095
-
1096
- def on_receiver_start_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
1097
- if self.state != AseStateMachine.State.ENABLING:
1098
- return (
1099
- AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
1100
- AseReasonCode.NONE,
1101
- )
1102
- self.state = self.State.STREAMING
1103
- return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1104
-
1105
- def on_disable(self) -> Tuple[AseResponseCode, AseReasonCode]:
1106
- if self.state not in (
1107
- AseStateMachine.State.ENABLING,
1108
- AseStateMachine.State.STREAMING,
1109
- ):
1110
- return (
1111
- AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
1112
- AseReasonCode.NONE,
1113
- )
1114
- if self.role == AudioRole.SINK:
1115
- self.state = self.State.QOS_CONFIGURED
1116
- else:
1117
- self.state = self.State.DISABLING
1118
- return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1119
-
1120
- def on_receiver_stop_ready(self) -> Tuple[AseResponseCode, AseReasonCode]:
1121
- if (
1122
- self.role != AudioRole.SOURCE
1123
- or self.state != AseStateMachine.State.DISABLING
1124
- ):
1125
- return (
1126
- AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
1127
- AseReasonCode.NONE,
1128
- )
1129
- self.state = self.State.QOS_CONFIGURED
1130
- return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1131
-
1132
- def on_update_metadata(
1133
- self, metadata: bytes
1134
- ) -> Tuple[AseResponseCode, AseReasonCode]:
1135
- if self.state not in (
1136
- AseStateMachine.State.ENABLING,
1137
- AseStateMachine.State.STREAMING,
1138
- ):
1139
- return (
1140
- AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
1141
- AseReasonCode.NONE,
1142
- )
1143
- self.metadata = metadata
1144
- return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1145
-
1146
- def on_release(self) -> Tuple[AseResponseCode, AseReasonCode]:
1147
- if self.state == AseStateMachine.State.IDLE:
1148
- return (
1149
- AseResponseCode.INVALID_ASE_STATE_MACHINE_TRANSITION,
1150
- AseReasonCode.NONE,
1151
- )
1152
- self.state = self.State.RELEASING
1153
-
1154
- async def remove_cis_async():
1155
- await self.service.device.send_command(
1156
- hci.HCI_LE_Remove_ISO_Data_Path_Command(
1157
- connection_handle=self.cis_link.handle,
1158
- data_path_direction=self.role,
1159
- )
1160
- )
1161
- self.state = self.State.IDLE
1162
- await self.service.device.notify_subscribers(self, self.value)
1163
-
1164
- self.service.device.abort_on('flush', remove_cis_async())
1165
- return (AseResponseCode.SUCCESS, AseReasonCode.NONE)
1166
-
1167
- @property
1168
- def state(self) -> State:
1169
- return self._state
1170
-
1171
- @state.setter
1172
- def state(self, new_state: State) -> None:
1173
- logger.debug(f'{self} state change -> {colors.color(new_state.name, "cyan")}')
1174
- self._state = new_state
1175
- self.emit('state_change')
1176
-
1177
- @property
1178
- def value(self):
1179
- '''Returns ASE_ID, ASE_STATE, and ASE Additional Parameters.'''
1180
-
1181
- if self.state == self.State.CODEC_CONFIGURED:
1182
- codec_specific_configuration_bytes = bytes(
1183
- self.codec_specific_configuration
1184
- )
1185
- additional_parameters = (
1186
- struct.pack(
1187
- '<BBBH',
1188
- self.preferred_framing,
1189
- self.preferred_phy,
1190
- self.preferred_retransmission_number,
1191
- self.preferred_max_transport_latency,
1192
- )
1193
- + self.supported_presentation_delay_min.to_bytes(3, 'little')
1194
- + self.supported_presentation_delay_max.to_bytes(3, 'little')
1195
- + self.preferred_presentation_delay_min.to_bytes(3, 'little')
1196
- + self.preferred_presentation_delay_max.to_bytes(3, 'little')
1197
- + bytes(self.codec_id)
1198
- + bytes([len(codec_specific_configuration_bytes)])
1199
- + codec_specific_configuration_bytes
1200
- )
1201
- elif self.state == self.State.QOS_CONFIGURED:
1202
- additional_parameters = (
1203
- bytes([self.cig_id, self.cis_id])
1204
- + self.sdu_interval.to_bytes(3, 'little')
1205
- + struct.pack(
1206
- '<BBHBH',
1207
- self.framing,
1208
- self.phy,
1209
- self.max_sdu,
1210
- self.retransmission_number,
1211
- self.max_transport_latency,
1212
- )
1213
- + self.presentation_delay.to_bytes(3, 'little')
1214
- )
1215
- elif self.state in (
1216
- self.State.ENABLING,
1217
- self.State.STREAMING,
1218
- self.State.DISABLING,
1219
- ):
1220
- additional_parameters = (
1221
- bytes([self.cig_id, self.cis_id, len(self.metadata)]) + self.metadata
1222
- )
1223
- else:
1224
- additional_parameters = b''
1225
-
1226
- return bytes([self.ase_id, self.state]) + additional_parameters
1227
-
1228
- @value.setter
1229
- def value(self, _new_value):
1230
- # Readonly. Do nothing in the setter.
1231
- pass
1232
-
1233
- def on_read(self, _: Optional[device.Connection]) -> bytes:
1234
- return self.value
1235
-
1236
- def __str__(self) -> str:
1237
- return (
1238
- f'AseStateMachine(id={self.ase_id}, role={self.role.name} '
1239
- f'state={self._state.name})'
1240
- )
1241
-
1242
-
1243
- class AudioStreamControlService(gatt.TemplateService):
1244
- UUID = gatt.GATT_AUDIO_STREAM_CONTROL_SERVICE
1245
-
1246
- ase_state_machines: Dict[int, AseStateMachine]
1247
- ase_control_point: gatt.Characteristic
1248
- _active_client: Optional[device.Connection] = None
1249
-
1250
- def __init__(
1251
- self,
1252
- device: device.Device,
1253
- source_ase_id: Sequence[int] = [],
1254
- sink_ase_id: Sequence[int] = [],
1255
- ) -> None:
1256
- self.device = device
1257
- self.ase_state_machines = {
1258
- **{
1259
- id: AseStateMachine(role=AudioRole.SINK, ase_id=id, service=self)
1260
- for id in sink_ase_id
1261
- },
1262
- **{
1263
- id: AseStateMachine(role=AudioRole.SOURCE, ase_id=id, service=self)
1264
- for id in source_ase_id
1265
- },
1266
- } # ASE state machines, by ASE ID
1267
-
1268
- self.ase_control_point = gatt.Characteristic(
1269
- uuid=gatt.GATT_ASE_CONTROL_POINT_CHARACTERISTIC,
1270
- properties=gatt.Characteristic.Properties.WRITE
1271
- | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
1272
- | gatt.Characteristic.Properties.NOTIFY,
1273
- permissions=gatt.Characteristic.Permissions.WRITEABLE,
1274
- value=gatt.CharacteristicValue(write=self.on_write_ase_control_point),
1275
- )
1276
-
1277
- super().__init__([self.ase_control_point, *self.ase_state_machines.values()])
1278
-
1279
- def on_operation(self, opcode: ASE_Operation.Opcode, ase_id: int, args):
1280
- if ase := self.ase_state_machines.get(ase_id):
1281
- handler = getattr(ase, 'on_' + opcode.name.lower())
1282
- return (ase_id, *handler(*args))
1283
- else:
1284
- return (ase_id, AseResponseCode.INVALID_ASE_ID, AseReasonCode.NONE)
1285
-
1286
- def _on_client_disconnected(self, _reason: int) -> None:
1287
- for ase in self.ase_state_machines.values():
1288
- ase.state = AseStateMachine.State.IDLE
1289
- self._active_client = None
1290
-
1291
- def on_write_ase_control_point(self, connection, data):
1292
- if not self._active_client and connection:
1293
- self._active_client = connection
1294
- connection.once('disconnection', self._on_client_disconnected)
1295
-
1296
- operation = ASE_Operation.from_bytes(data)
1297
- responses = []
1298
- logger.debug(f'*** ASCS Write {operation} ***')
1299
-
1300
- if operation.op_code == ASE_Operation.Opcode.CONFIG_CODEC:
1301
- for ase_id, *args in zip(
1302
- operation.ase_id,
1303
- operation.target_latency,
1304
- operation.target_phy,
1305
- operation.codec_id,
1306
- operation.codec_specific_configuration,
1307
- ):
1308
- responses.append(self.on_operation(operation.op_code, ase_id, args))
1309
- elif operation.op_code == ASE_Operation.Opcode.CONFIG_QOS:
1310
- for ase_id, *args in zip(
1311
- operation.ase_id,
1312
- operation.cig_id,
1313
- operation.cis_id,
1314
- operation.sdu_interval,
1315
- operation.framing,
1316
- operation.phy,
1317
- operation.max_sdu,
1318
- operation.retransmission_number,
1319
- operation.max_transport_latency,
1320
- operation.presentation_delay,
1321
- ):
1322
- responses.append(self.on_operation(operation.op_code, ase_id, args))
1323
- elif operation.op_code in (
1324
- ASE_Operation.Opcode.ENABLE,
1325
- ASE_Operation.Opcode.UPDATE_METADATA,
1326
- ):
1327
- for ase_id, *args in zip(
1328
- operation.ase_id,
1329
- operation.metadata,
1330
- ):
1331
- responses.append(self.on_operation(operation.op_code, ase_id, args))
1332
- elif operation.op_code in (
1333
- ASE_Operation.Opcode.RECEIVER_START_READY,
1334
- ASE_Operation.Opcode.DISABLE,
1335
- ASE_Operation.Opcode.RECEIVER_STOP_READY,
1336
- ASE_Operation.Opcode.RELEASE,
1337
- ):
1338
- for ase_id in operation.ase_id:
1339
- responses.append(self.on_operation(operation.op_code, ase_id, []))
1340
-
1341
- control_point_notification = bytes(
1342
- [operation.op_code, len(responses)]
1343
- ) + b''.join(map(bytes, responses))
1344
- self.device.abort_on(
1345
- 'flush',
1346
- self.device.notify_subscribers(
1347
- self.ase_control_point, control_point_notification
1348
- ),
1349
- )
1350
-
1351
- for ase_id, *_ in responses:
1352
- if ase := self.ase_state_machines.get(ase_id):
1353
- self.device.abort_on(
1354
- 'flush',
1355
- self.device.notify_subscribers(ase, ase.value),
1356
- )
1357
-
1358
-
1359
- # -----------------------------------------------------------------------------
1360
- # Client
1361
- # -----------------------------------------------------------------------------
1362
- class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
1363
- SERVICE_CLASS = PublishedAudioCapabilitiesService
1364
-
1365
- sink_pac: Optional[gatt_client.CharacteristicProxy] = None
1366
- sink_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
1367
- source_pac: Optional[gatt_client.CharacteristicProxy] = None
1368
- source_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
1369
- available_audio_contexts: gatt_client.CharacteristicProxy
1370
- supported_audio_contexts: gatt_client.CharacteristicProxy
1371
-
1372
- def __init__(self, service_proxy: gatt_client.ServiceProxy):
1373
- self.service_proxy = service_proxy
1374
-
1375
- self.available_audio_contexts = service_proxy.get_characteristics_by_uuid(
1376
- gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
1377
- )[0]
1378
- self.supported_audio_contexts = service_proxy.get_characteristics_by_uuid(
1379
- gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
1380
- )[0]
1381
-
1382
- if characteristics := service_proxy.get_characteristics_by_uuid(
1383
- gatt.GATT_SINK_PAC_CHARACTERISTIC
1384
- ):
1385
- self.sink_pac = characteristics[0]
1386
-
1387
- if characteristics := service_proxy.get_characteristics_by_uuid(
1388
- gatt.GATT_SOURCE_PAC_CHARACTERISTIC
1389
- ):
1390
- self.source_pac = characteristics[0]
1391
-
1392
- if characteristics := service_proxy.get_characteristics_by_uuid(
1393
- gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
1394
- ):
1395
- self.sink_audio_locations = characteristics[0]
1396
-
1397
- if characteristics := service_proxy.get_characteristics_by_uuid(
1398
- gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
1399
- ):
1400
- self.source_audio_locations = characteristics[0]
1401
-
1402
-
1403
- class AudioStreamControlServiceProxy(gatt_client.ProfileServiceProxy):
1404
- SERVICE_CLASS = AudioStreamControlService
1405
-
1406
- sink_ase: List[gatt_client.CharacteristicProxy]
1407
- source_ase: List[gatt_client.CharacteristicProxy]
1408
- ase_control_point: gatt_client.CharacteristicProxy
1409
-
1410
- def __init__(self, service_proxy: gatt_client.ServiceProxy):
1411
- self.service_proxy = service_proxy
1412
-
1413
- self.sink_ase = service_proxy.get_characteristics_by_uuid(
1414
- gatt.GATT_SINK_ASE_CHARACTERISTIC
1415
- )
1416
- self.source_ase = service_proxy.get_characteristics_by_uuid(
1417
- gatt.GATT_SOURCE_ASE_CHARACTERISTIC
1418
- )
1419
- self.ase_control_point = service_proxy.get_characteristics_by_uuid(
1420
- gatt.GATT_ASE_CONTROL_POINT_CHARACTERISTIC
1421
- )[0]