bumble 0.0.213__py3-none-any.whl → 0.0.215__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 (123) hide show
  1. bumble/_version.py +16 -3
  2. bumble/a2dp.py +15 -16
  3. bumble/apps/auracast.py +14 -38
  4. bumble/apps/bench.py +10 -15
  5. bumble/apps/ble_rpa_tool.py +1 -0
  6. bumble/apps/console.py +22 -25
  7. bumble/apps/controller_info.py +20 -25
  8. bumble/apps/controller_loopback.py +6 -10
  9. bumble/apps/controllers.py +2 -3
  10. bumble/apps/device_info.py +4 -5
  11. bumble/apps/gatt_dump.py +3 -3
  12. bumble/apps/gg_bridge.py +7 -8
  13. bumble/apps/hci_bridge.py +4 -3
  14. bumble/apps/l2cap_bridge.py +5 -5
  15. bumble/apps/lea_unicast/app.py +16 -26
  16. bumble/apps/pair.py +30 -43
  17. bumble/apps/pandora_server.py +5 -4
  18. bumble/apps/player/player.py +20 -24
  19. bumble/apps/rfcomm_bridge.py +4 -10
  20. bumble/apps/scan.py +17 -8
  21. bumble/apps/show.py +4 -5
  22. bumble/apps/speaker/speaker.py +23 -27
  23. bumble/apps/unbond.py +3 -3
  24. bumble/apps/usb_probe.py +2 -4
  25. bumble/att.py +241 -246
  26. bumble/audio/io.py +5 -9
  27. bumble/avc.py +2 -2
  28. bumble/avctp.py +6 -7
  29. bumble/avdtp.py +19 -22
  30. bumble/avrcp.py +1097 -589
  31. bumble/codecs.py +2 -0
  32. bumble/controller.py +142 -35
  33. bumble/core.py +567 -248
  34. bumble/crypto/__init__.py +2 -2
  35. bumble/crypto/builtin.py +1 -1
  36. bumble/crypto/cryptography.py +2 -4
  37. bumble/data_types.py +1025 -0
  38. bumble/device.py +319 -267
  39. bumble/drivers/__init__.py +3 -2
  40. bumble/drivers/intel.py +3 -4
  41. bumble/drivers/rtk.py +26 -9
  42. bumble/gap.py +4 -4
  43. bumble/gatt.py +3 -2
  44. bumble/gatt_adapters.py +3 -11
  45. bumble/gatt_client.py +69 -81
  46. bumble/gatt_server.py +124 -124
  47. bumble/hci.py +114 -18
  48. bumble/helpers.py +19 -26
  49. bumble/hfp.py +10 -21
  50. bumble/hid.py +22 -16
  51. bumble/host.py +191 -103
  52. bumble/keys.py +5 -3
  53. bumble/l2cap.py +138 -104
  54. bumble/link.py +18 -19
  55. bumble/logging.py +65 -0
  56. bumble/pairing.py +7 -6
  57. bumble/pandora/__init__.py +9 -8
  58. bumble/pandora/config.py +3 -1
  59. bumble/pandora/device.py +3 -2
  60. bumble/pandora/host.py +38 -36
  61. bumble/pandora/l2cap.py +22 -21
  62. bumble/pandora/security.py +15 -15
  63. bumble/pandora/utils.py +5 -3
  64. bumble/profiles/aics.py +11 -11
  65. bumble/profiles/ams.py +403 -0
  66. bumble/profiles/ancs.py +6 -7
  67. bumble/profiles/ascs.py +14 -9
  68. bumble/profiles/asha.py +8 -12
  69. bumble/profiles/bap.py +11 -23
  70. bumble/profiles/bass.py +2 -7
  71. bumble/profiles/battery_service.py +3 -4
  72. bumble/profiles/cap.py +1 -2
  73. bumble/profiles/csip.py +2 -6
  74. bumble/profiles/device_information_service.py +2 -2
  75. bumble/profiles/gap.py +4 -4
  76. bumble/profiles/gatt_service.py +1 -4
  77. bumble/profiles/gmap.py +5 -5
  78. bumble/profiles/hap.py +62 -59
  79. bumble/profiles/heart_rate_service.py +5 -4
  80. bumble/profiles/le_audio.py +3 -1
  81. bumble/profiles/mcp.py +3 -7
  82. bumble/profiles/pacs.py +3 -6
  83. bumble/profiles/pbp.py +2 -0
  84. bumble/profiles/tmap.py +2 -3
  85. bumble/profiles/vcs.py +2 -8
  86. bumble/profiles/vocs.py +8 -8
  87. bumble/rfcomm.py +11 -14
  88. bumble/rtp.py +1 -0
  89. bumble/sdp.py +10 -8
  90. bumble/smp.py +151 -159
  91. bumble/snoop.py +5 -5
  92. bumble/tools/generate_company_id_list.py +1 -0
  93. bumble/tools/intel_fw_download.py +3 -3
  94. bumble/tools/intel_util.py +5 -4
  95. bumble/tools/rtk_fw_download.py +6 -3
  96. bumble/tools/rtk_util.py +26 -8
  97. bumble/transport/__init__.py +19 -15
  98. bumble/transport/android_emulator.py +8 -13
  99. bumble/transport/android_netsim.py +19 -18
  100. bumble/transport/common.py +12 -15
  101. bumble/transport/file.py +1 -1
  102. bumble/transport/hci_socket.py +4 -6
  103. bumble/transport/pty.py +5 -6
  104. bumble/transport/pyusb.py +7 -10
  105. bumble/transport/serial.py +2 -1
  106. bumble/transport/tcp_client.py +2 -2
  107. bumble/transport/tcp_server.py +11 -14
  108. bumble/transport/udp.py +3 -3
  109. bumble/transport/unix.py +67 -1
  110. bumble/transport/usb.py +6 -6
  111. bumble/transport/vhci.py +0 -1
  112. bumble/transport/ws_client.py +2 -1
  113. bumble/transport/ws_server.py +3 -2
  114. bumble/utils.py +20 -5
  115. bumble/vendor/android/hci.py +1 -2
  116. bumble/vendor/zephyr/hci.py +0 -1
  117. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/METADATA +4 -2
  118. bumble-0.0.215.dist-info/RECORD +183 -0
  119. bumble-0.0.213.dist-info/RECORD +0 -180
  120. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/WHEEL +0 -0
  121. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/entry_points.txt +0 -0
  122. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/licenses/LICENSE +0 -0
  123. {bumble-0.0.213.dist-info → bumble-0.0.215.dist-info}/top_level.txt +0 -0
bumble/avrcp.py CHANGED
@@ -16,16 +16,18 @@
16
16
  # Imports
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
+
19
20
  import asyncio
20
- from dataclasses import dataclass
21
21
  import enum
22
+ import functools
22
23
  import logging
23
24
  import struct
25
+ from dataclasses import dataclass, field
24
26
  from typing import (
25
27
  AsyncIterator,
26
28
  Awaitable,
27
29
  Callable,
28
- cast,
30
+ ClassVar,
29
31
  Iterable,
30
32
  List,
31
33
  Optional,
@@ -33,28 +35,24 @@ from typing import (
33
35
  SupportsBytes,
34
36
  TypeVar,
35
37
  Union,
38
+ cast,
36
39
  )
37
40
 
38
-
41
+ from bumble import avc, avctp, core, hci, l2cap, utils
39
42
  from bumble.colors import color
40
- from bumble.device import Device, Connection
43
+ from bumble.device import Connection, Device
41
44
  from bumble.sdp import (
42
- SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
45
+ SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
46
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
43
47
  SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
44
- SDP_PUBLIC_BROWSE_ROOT,
45
48
  SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
49
+ SDP_PUBLIC_BROWSE_ROOT,
46
50
  SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
47
- SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
51
+ SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
48
52
  SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
49
53
  DataElement,
50
54
  ServiceAttribute,
51
55
  )
52
- from bumble import utils
53
- from bumble import core
54
- from bumble import l2cap
55
- from bumble import avc
56
- from bumble import avctp
57
-
58
56
 
59
57
  # -----------------------------------------------------------------------------
60
58
  # Logging
@@ -69,18 +67,153 @@ AVRCP_PID = 0x110E
69
67
  AVRCP_BLUETOOTH_SIG_COMPANY_ID = 0x001958
70
68
 
71
69
 
70
+ _UINT64_BE_METADATA = {
71
+ 'parser': lambda data, offset: (
72
+ offset + 8,
73
+ int.from_bytes(data[offset : offset + 8], byteorder='big'),
74
+ ),
75
+ 'serializer': lambda x: x.to_bytes(8, byteorder='big'),
76
+ }
77
+
78
+
79
+ class PduId(utils.OpenIntEnum):
80
+ GET_CAPABILITIES = 0x10
81
+ LIST_PLAYER_APPLICATION_SETTING_ATTRIBUTES = 0x11
82
+ LIST_PLAYER_APPLICATION_SETTING_VALUES = 0x12
83
+ GET_CURRENT_PLAYER_APPLICATION_SETTING_VALUE = 0x13
84
+ SET_PLAYER_APPLICATION_SETTING_VALUE = 0x14
85
+ GET_PLAYER_APPLICATION_SETTING_ATTRIBUTE_TEXT = 0x15
86
+ GET_PLAYER_APPLICATION_SETTING_VALUE_TEXT = 0x16
87
+ INFORM_DISPLAYABLE_CHARACTER_SET = 0x17
88
+ INFORM_BATTERY_STATUS_OF_CT = 0x18
89
+ GET_ELEMENT_ATTRIBUTES = 0x20
90
+ GET_PLAY_STATUS = 0x30
91
+ REGISTER_NOTIFICATION = 0x31
92
+ REQUEST_CONTINUING_RESPONSE = 0x40
93
+ ABORT_CONTINUING_RESPONSE = 0x41
94
+ SET_ABSOLUTE_VOLUME = 0x50
95
+ SET_ADDRESSED_PLAYER = 0x60
96
+ SET_BROWSED_PLAYER = 0x70
97
+ GET_FOLDER_ITEMS = 0x71
98
+ CHANGE_PATH = 0x72
99
+ GET_ITEM_ATTRIBUTES = 0x73
100
+ PLAY_ITEM = 0x74
101
+ GET_TOTAL_NUMBER_OF_ITEMS = 0x75
102
+ SEARCH = 0x80
103
+ ADD_TO_NOW_PLAYING = 0x90
104
+
105
+
106
+ class CharacterSetId(hci.SpecableEnum):
107
+ UTF_8 = 0x06
108
+
109
+
110
+ class MediaAttributeId(hci.SpecableEnum):
111
+ TITLE = 0x01
112
+ ARTIST_NAME = 0x02
113
+ ALBUM_NAME = 0x03
114
+ TRACK_NUMBER = 0x04
115
+ TOTAL_NUMBER_OF_TRACKS = 0x05
116
+ GENRE = 0x06
117
+ PLAYING_TIME = 0x07
118
+ DEFAULT_COVER_ART = 0x08
119
+
120
+
121
+ class PlayStatus(hci.SpecableEnum):
122
+ STOPPED = 0x00
123
+ PLAYING = 0x01
124
+ PAUSED = 0x02
125
+ FWD_SEEK = 0x03
126
+ REV_SEEK = 0x04
127
+ ERROR = 0xFF
128
+
129
+
130
+ class EventId(hci.SpecableEnum):
131
+ PLAYBACK_STATUS_CHANGED = 0x01
132
+ TRACK_CHANGED = 0x02
133
+ TRACK_REACHED_END = 0x03
134
+ TRACK_REACHED_START = 0x04
135
+ PLAYBACK_POS_CHANGED = 0x05
136
+ BATT_STATUS_CHANGED = 0x06
137
+ SYSTEM_STATUS_CHANGED = 0x07
138
+ PLAYER_APPLICATION_SETTING_CHANGED = 0x08
139
+ NOW_PLAYING_CONTENT_CHANGED = 0x09
140
+ AVAILABLE_PLAYERS_CHANGED = 0x0A
141
+ ADDRESSED_PLAYER_CHANGED = 0x0B
142
+ UIDS_CHANGED = 0x0C
143
+ VOLUME_CHANGED = 0x0D
144
+
145
+ def __bytes__(self) -> bytes:
146
+ return bytes([int(self)])
147
+
148
+
149
+ class StatusCode(hci.SpecableEnum):
150
+ INVALID_COMMAND = 0x00
151
+ INVALID_PARAMETER = 0x01
152
+ PARAMETER_CONTENT_ERROR = 0x02
153
+ INTERNAL_ERROR = 0x03
154
+ OPERATION_COMPLETED = 0x04
155
+ UID_CHANGED = 0x05
156
+ INVALID_DIRECTION = 0x07
157
+ NOT_A_DIRECTORY = 0x08
158
+ DOES_NOT_EXIST = 0x09
159
+ INVALID_SCOPE = 0x0A
160
+ RANGE_OUT_OF_BOUNDS = 0x0B
161
+ FOLDER_ITEM_IS_NOT_PLAYABLE = 0x0C
162
+ MEDIA_IN_USE = 0x0D
163
+ NOW_PLAYING_LIST_FULL = 0x0E
164
+ SEARCH_NOT_SUPPORTED = 0x0F
165
+ SEARCH_IN_PROGRESS = 0x10
166
+ INVALID_PLAYER_ID = 0x11
167
+ PLAYER_NOT_BROWSABLE = 0x12
168
+ PLAYER_NOT_ADDRESSED = 0x13
169
+ NO_VALID_SEARCH_RESULTS = 0x14
170
+ NO_AVAILABLE_PLAYERS = 0x15
171
+ ADDRESSED_PLAYER_CHANGED = 0x16
172
+
173
+
174
+ class Scope(hci.SpecableEnum):
175
+ MEDIA_PLAYER_LIST = 0x00
176
+ MEDIA_PLAYER_VIRTUAL_FILESYSTEM = 0x01
177
+ SEARCH = 0x02
178
+ NOW_PLAYING = 0x03
179
+
180
+
181
+ class ControllerFeatures(enum.IntFlag):
182
+ # fmt: off
183
+ CATEGORY_1 = 1 << 0
184
+ CATEGORY_2 = 1 << 1
185
+ CATEGORY_3 = 1 << 2
186
+ CATEGORY_4 = 1 << 3
187
+ SUPPORTS_BROWSING = 1 << 6
188
+ SUPPORTS_COVER_ART_GET_IMAGE_PROPERTIES_FEATURE = 1 << 7
189
+ SUPPORTS_COVER_ART_GET_IMAGE_FEATURE = 1 << 8
190
+ SUPPORTS_COVER_ART_GET_LINKED_THUMBNAIL_FEATURE = 1 << 9
191
+
192
+
193
+ class TargetFeatures(enum.IntFlag):
194
+ # fmt: off
195
+ CATEGORY_1 = 1 << 0
196
+ CATEGORY_2 = 1 << 1
197
+ CATEGORY_3 = 1 << 2
198
+ CATEGORY_4 = 1 << 3
199
+ PLAYER_APPLICATION_SETTINGS = 1 << 4
200
+ GROUP_NAVIGATION = 1 << 5
201
+ SUPPORTS_BROWSING = 1 << 6
202
+ SUPPORTS_MULTIPLE_MEDIA_PLAYER_APPLICATIONS = 1 << 7
203
+ SUPPORTS_COVER_ART = 1 << 8
204
+
205
+
72
206
  # -----------------------------------------------------------------------------
73
207
  def make_controller_service_sdp_records(
74
208
  service_record_handle: int,
75
209
  avctp_version: tuple[int, int] = (1, 4),
76
210
  avrcp_version: tuple[int, int] = (1, 6),
77
- supported_features: int = 1,
211
+ supported_features: Union[int, ControllerFeatures] = 1,
78
212
  ) -> list[ServiceAttribute]:
79
- # TODO: support a way to compute the supported features from a feature list
80
213
  avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
81
214
  avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
82
215
 
83
- return [
216
+ attributes = [
84
217
  ServiceAttribute(
85
218
  SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
86
219
  DataElement.unsigned_integer_32(service_record_handle),
@@ -135,6 +268,31 @@ def make_controller_service_sdp_records(
135
268
  DataElement.unsigned_integer_16(supported_features),
136
269
  ),
137
270
  ]
271
+ if supported_features & ControllerFeatures.SUPPORTS_BROWSING:
272
+ attributes.append(
273
+ ServiceAttribute(
274
+ SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
275
+ DataElement.sequence(
276
+ [
277
+ DataElement.sequence(
278
+ [
279
+ DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
280
+ DataElement.unsigned_integer_16(
281
+ avctp.AVCTP_BROWSING_PSM
282
+ ),
283
+ ]
284
+ ),
285
+ DataElement.sequence(
286
+ [
287
+ DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
288
+ DataElement.unsigned_integer_16(avctp_version_int),
289
+ ]
290
+ ),
291
+ ]
292
+ ),
293
+ ),
294
+ )
295
+ return attributes
138
296
 
139
297
 
140
298
  # -----------------------------------------------------------------------------
@@ -142,13 +300,13 @@ def make_target_service_sdp_records(
142
300
  service_record_handle: int,
143
301
  avctp_version: tuple[int, int] = (1, 4),
144
302
  avrcp_version: tuple[int, int] = (1, 6),
145
- supported_features: int = 0x23,
303
+ supported_features: Union[int, TargetFeatures] = 0x23,
146
304
  ) -> list[ServiceAttribute]:
147
305
  # TODO: support a way to compute the supported features from a feature list
148
306
  avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
149
307
  avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
150
308
 
151
- return [
309
+ attributes = [
152
310
  ServiceAttribute(
153
311
  SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
154
312
  DataElement.unsigned_integer_32(service_record_handle),
@@ -202,17 +360,320 @@ def make_target_service_sdp_records(
202
360
  DataElement.unsigned_integer_16(supported_features),
203
361
  ),
204
362
  ]
363
+ if supported_features & TargetFeatures.SUPPORTS_BROWSING:
364
+ attributes.append(
365
+ ServiceAttribute(
366
+ SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
367
+ DataElement.sequence(
368
+ [
369
+ DataElement.sequence(
370
+ [
371
+ DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
372
+ DataElement.unsigned_integer_16(
373
+ avctp.AVCTP_BROWSING_PSM
374
+ ),
375
+ ]
376
+ ),
377
+ DataElement.sequence(
378
+ [
379
+ DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
380
+ DataElement.unsigned_integer_16(avctp_version_int),
381
+ ]
382
+ ),
383
+ ]
384
+ ),
385
+ ),
386
+ )
387
+ return attributes
205
388
 
206
389
 
207
390
  # -----------------------------------------------------------------------------
208
- def _decode_attribute_value(value: bytes, character_set: CharacterSetId) -> str:
391
+ def _parse_string(data: bytes, offset: int, length_size: int) -> tuple[int, str]:
392
+ length = int.from_bytes(
393
+ data[offset : offset + length_size], byteorder='big', signed=False
394
+ )
395
+ offset += length_size
396
+ encoded = data[offset : offset + length]
209
397
  try:
210
- if character_set == CharacterSetId.UTF_8:
211
- return value.decode("utf-8")
212
- return value.decode("ascii")
398
+ decoded = encoded.decode("utf-8")
213
399
  except UnicodeDecodeError:
214
- logger.warning(f"cannot decode string with bytes: {value.hex()}")
215
- return ""
400
+ # This can decode anything.
401
+ decoded = encoded.decode("latin1")
402
+ return offset + length, decoded
403
+
404
+
405
+ # -----------------------------------------------------------------------------
406
+ def _serialize_string(value: str, length_size: int) -> bytes:
407
+ encoded = value.encode("utf-8")
408
+ return len(encoded).to_bytes(length_size, byteorder='big', signed=False) + encoded
409
+
410
+
411
+ # -----------------------------------------------------------------------------
412
+ def _string_spec(length_size: int):
413
+ return {
414
+ 'parser': functools.partial(_parse_string, length_size=length_size),
415
+ 'serializer': functools.partial(_serialize_string, length_size=length_size),
416
+ }
417
+
418
+
419
+ # -----------------------------------------------------------------------------
420
+ @dataclass
421
+ class MediaAttribute(hci.HCI_Dataclass_Object):
422
+ attribute_id: MediaAttributeId = field(
423
+ metadata=MediaAttributeId.type_metadata(4, byteorder='big')
424
+ )
425
+ character_set_id: CharacterSetId = field(
426
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
427
+ )
428
+ attribute_value: str = field(metadata=hci.metadata(_string_spec(2)))
429
+
430
+
431
+ # -----------------------------------------------------------------------------
432
+ @dataclass
433
+ class SongAndPlayStatus:
434
+ song_length: int
435
+ song_position: int
436
+ play_status: PlayStatus
437
+
438
+
439
+ # -----------------------------------------------------------------------------
440
+ class ApplicationSetting:
441
+ class AttributeId(hci.SpecableEnum):
442
+ EQUALIZER_ON_OFF = 0x01
443
+ REPEAT_MODE = 0x02
444
+ SHUFFLE_ON_OFF = 0x03
445
+ SCAN_ON_OFF = 0x04
446
+
447
+ class EqualizerOnOffStatus(hci.SpecableEnum):
448
+ OFF = 0x01
449
+ ON = 0x02
450
+
451
+ class RepeatModeStatus(hci.SpecableEnum):
452
+ OFF = 0x01
453
+ SINGLE_TRACK_REPEAT = 0x02
454
+ ALL_TRACK_REPEAT = 0x03
455
+ GROUP_REPEAT = 0x04
456
+
457
+ class ShuffleOnOffStatus(hci.SpecableEnum):
458
+ OFF = 0x01
459
+ ALL_TRACKS_SHUFFLE = 0x02
460
+ GROUP_SHUFFLE = 0x03
461
+
462
+ class ScanOnOffStatus(hci.SpecableEnum):
463
+ OFF = 0x01
464
+ ALL_TRACKS_SCAN = 0x02
465
+ GROUP_SCAN = 0x03
466
+
467
+ class GenericValue(hci.SpecableEnum):
468
+ pass
469
+
470
+
471
+ # -----------------------------------------------------------------------------
472
+ @dataclass
473
+ class AttributeValueEntry(hci.HCI_Dataclass_Object):
474
+ attribute_id: MediaAttributeId = field(
475
+ metadata=MediaAttributeId.type_metadata(4, byteorder='big')
476
+ )
477
+ character_set_id: CharacterSetId = field(
478
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
479
+ )
480
+ attribute_value: str = field(metadata=hci.metadata(_string_spec(2)))
481
+
482
+
483
+ # -----------------------------------------------------------------------------
484
+ class BrowseableItem:
485
+ """6.10.2 Browseable items."""
486
+
487
+ class Type(hci.SpecableEnum):
488
+ MEDIA_PLAYER = 0x01
489
+ FOLDER = 0x02
490
+ MEDIA_ELEMENT = 0x03
491
+
492
+ item_type: ClassVar[Type]
493
+ _payload: Optional[bytes] = None
494
+
495
+ subclasses: ClassVar[dict[Type, type[BrowseableItem]]] = {}
496
+ fields: ClassVar[hci.Fields] = ()
497
+
498
+ @classmethod
499
+ def parse_from_bytes(cls, data: bytes, offset: int) -> tuple[int, BrowseableItem]:
500
+ item_type, length = struct.unpack_from('>BH', data, offset)
501
+ subclass = cls.subclasses[BrowseableItem.Type(item_type)]
502
+ instance = subclass(
503
+ **hci.HCI_Object.dict_from_bytes(data, offset + 3, subclass.fields)
504
+ )
505
+ instance._payload = data[3:]
506
+ return offset + length, instance
507
+
508
+ def __bytes__(self) -> bytes:
509
+ if self._payload is None:
510
+ self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
511
+ return (
512
+ struct.pack('>BH', self.item_type, len(self._payload) + 3) + self._payload
513
+ )
514
+
515
+ _Item = TypeVar('_Item', bound='BrowseableItem')
516
+
517
+ @classmethod
518
+ def item(cls, subclass: type[_Item]) -> type[_Item]:
519
+ cls.subclasses[subclass.item_type] = subclass
520
+ subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
521
+ return subclass
522
+
523
+
524
+ # -----------------------------------------------------------------------------
525
+ @BrowseableItem.item
526
+ @dataclass
527
+ class MediaPlayerItem(BrowseableItem):
528
+ item_type = BrowseableItem.Type.MEDIA_PLAYER
529
+
530
+ class MajorPlayerType(hci.SpecableFlag):
531
+ AUDIO = 0x01
532
+ VIDEO = 0x02
533
+ BROADCASTING_AUDIO = 0x04
534
+ BROADCASTING_VIDEO = 0x08
535
+
536
+ class PlayerSubType(hci.SpecableFlag):
537
+ AUDIO_BOOK = 0x01
538
+ PODCAST = 0x02
539
+
540
+ class Features(hci.SpecableFlag):
541
+ SELECT = 1 << 0
542
+ UP = 1 << 1
543
+ DOWN = 1 << 2
544
+ LEFT = 1 << 3
545
+ RIGHT = 1 << 4
546
+ RIGHT_UP = 1 << 5
547
+ RIGHT_DOWN = 1 << 6
548
+ LEFT_UP = 1 << 7
549
+ LEFT_DOWN = 1 << 8
550
+ ROOT_MENU = 1 << 9
551
+ SETUP_MENU = 1 << 10
552
+ CONTENTS_MENU = 1 << 11
553
+ FAVORITE_MENU = 1 << 12
554
+ EXIT = 1 << 13
555
+ NUM_0 = 1 << 14
556
+ NUM_1 = 1 << 15
557
+ NUM_2 = 1 << 16
558
+ NUM_3 = 1 << 17
559
+ NUM_4 = 1 << 18
560
+ NUM_5 = 1 << 19
561
+ NUM_6 = 1 << 20
562
+ NUM_7 = 1 << 21
563
+ NUM_8 = 1 << 22
564
+ NUM_9 = 1 << 23
565
+ DOT = 1 << 24
566
+ ENTER = 1 << 25
567
+ CLEAR = 1 << 26
568
+ CHANNEL_UP = 1 << 27
569
+ CHANNEL_DOWN = 1 << 28
570
+ PREVIOUS_CHANNEL = 1 << 29
571
+ SOUND_SELECT = 1 << 30
572
+ INPUT_SELECT = 1 << 31
573
+ DISPLAY_INFORMATION = 1 << 32
574
+ HELP = 1 << 33
575
+ PAGE_UP = 1 << 34
576
+ PAGE_DOWN = 1 << 35
577
+ POWER = 1 << 36
578
+ VOLUME_UP = 1 << 37
579
+ VOLUME_DOWN = 1 << 38
580
+ MUTE = 1 << 39
581
+ PLAY = 1 << 40
582
+ STOP = 1 << 41
583
+ PAUSE = 1 << 42
584
+ RECORD = 1 << 43
585
+ REWIND = 1 << 44
586
+ FAST_FORWARD = 1 << 45
587
+ EJECT = 1 << 46
588
+ FORWARD = 1 << 47
589
+ BACKWARD = 1 << 48
590
+ ANGLE = 1 << 49
591
+ SUBPICTURE = 1 << 50
592
+ F1 = 1 << 51
593
+ F2 = 1 << 52
594
+ F3 = 1 << 53
595
+ F4 = 1 << 54
596
+ F5 = 1 << 55
597
+ VENDOR_UNIQUE = 1 << 56
598
+ BASIC_GROUP_NAVIGATION = 1 << 57
599
+ ADVANCED_CONTROL_PLAYER = 1 << 58
600
+ BROWSING = 1 << 59
601
+ SEARCHING = 1 << 60
602
+ ADD_TO_NOW_PLAYING = 1 << 61
603
+ UI_DS_UNIQUE_IN_PLAYER_BROWSE_TREE = 1 << 62
604
+ ONLY_BROWSABLE_WHEN_ADDRESSED = 1 << 63
605
+ ONLY_SEARCHABLE_WHEN_ADDRESSED = 1 << 64
606
+ NOW_PLAYING = 1 << 65
607
+ UID_PERSISTENCY = 1 << 66
608
+ NUMBER_OF_ITEMS = 1 << 67
609
+ COVER_ART = 1 << 68
610
+
611
+ player_id: int = field(metadata=hci.metadata('>2'))
612
+ major_player_type: MajorPlayerType = field(
613
+ metadata=MajorPlayerType.type_metadata(1)
614
+ )
615
+ player_sub_type: PlayerSubType = field(
616
+ metadata=PlayerSubType.type_metadata(4, byteorder='big')
617
+ )
618
+ play_status: PlayStatus = field(metadata=PlayStatus.type_metadata(1))
619
+ feature_bitmask: Features = field(
620
+ metadata=Features.type_metadata(16, byteorder='big')
621
+ )
622
+ character_set_id: CharacterSetId = field(
623
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
624
+ )
625
+ displayable_name: str = field(metadata=hci.metadata(_string_spec(2)))
626
+
627
+
628
+ # -----------------------------------------------------------------------------
629
+ @BrowseableItem.item
630
+ @dataclass
631
+ class FolderItem(BrowseableItem):
632
+ item_type = BrowseableItem.Type.FOLDER
633
+
634
+ class FolderType(hci.SpecableEnum):
635
+ MIXED = 0x00
636
+ TITLES = 0x01
637
+ ALBUMS = 0x02
638
+ ARTISTS = 0x03
639
+ GENRES = 0x04
640
+ PLAYLISTS = 0x05
641
+ YEARS = 0x06
642
+
643
+ class Playable(hci.SpecableEnum):
644
+ NOT_PLAYABLE = 0x00
645
+ PLAYABLE = 0x01
646
+
647
+ folder_uid: int = field(metadata=_UINT64_BE_METADATA)
648
+ folder_type: FolderType = field(metadata=FolderType.type_metadata(1))
649
+ is_playable: FolderType = field(metadata=Playable.type_metadata(1))
650
+ character_set_id: CharacterSetId = field(
651
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
652
+ )
653
+ displayable_name: str = field(metadata=hci.metadata(_string_spec(2)))
654
+
655
+
656
+ # -----------------------------------------------------------------------------
657
+ @BrowseableItem.item
658
+ @dataclass
659
+ class MediaElementItem(BrowseableItem):
660
+ item_type = BrowseableItem.Type.MEDIA_ELEMENT
661
+
662
+ class MediaType(hci.SpecableEnum):
663
+ AUDIO = 0x00
664
+ VIDEO = 0x01
665
+
666
+ media_element_uid: int = field(metadata=_UINT64_BE_METADATA)
667
+ media_type: MediaType = field(metadata=MediaType.type_metadata(1))
668
+ character_set_id: CharacterSetId = field(
669
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
670
+ )
671
+ displayable_name: str = field(metadata=hci.metadata(_string_spec(2)))
672
+ attribute_value_entry_list: Sequence[AttributeValueEntry] = field(
673
+ metadata=hci.metadata(
674
+ AttributeValueEntry.parse_from_bytes, list_begin=True, list_end=True
675
+ )
676
+ )
216
677
 
217
678
 
218
679
  # -----------------------------------------------------------------------------
@@ -223,10 +684,10 @@ class PduAssembler:
223
684
  6.3.1 AVRCP specific AV//C commands
224
685
  """
225
686
 
226
- pdu_id: Optional[Protocol.PduId]
687
+ pdu_id: Optional[PduId]
227
688
  payload: bytes
228
689
 
229
- def __init__(self, callback: Callable[[Protocol.PduId, bytes], None]) -> None:
690
+ def __init__(self, callback: Callable[[PduId, bytes], None]) -> None:
230
691
  self.callback = callback
231
692
  self.reset()
232
693
 
@@ -235,7 +696,7 @@ class PduAssembler:
235
696
  self.parameter = b''
236
697
 
237
698
  def on_pdu(self, pdu: bytes) -> None:
238
- pdu_id = Protocol.PduId(pdu[0])
699
+ pdu_id = PduId(pdu[0])
239
700
  packet_type = Protocol.PacketType(pdu[1] & 3)
240
701
  parameter_length = struct.unpack_from('>H', pdu, 2)[0]
241
702
  parameter = pdu[4 : 4 + parameter_length]
@@ -267,677 +728,794 @@ class PduAssembler:
267
728
  assert self.pdu_id is not None
268
729
  try:
269
730
  self.callback(self.pdu_id, self.parameter)
270
- except Exception as error:
271
- logger.exception(color(f'!!! exception in callback: {error}', 'red'))
731
+ except Exception:
732
+ logger.exception(color('!!! exception in callback', 'red'))
272
733
 
273
734
  self.reset()
274
735
 
275
736
 
276
737
  # -----------------------------------------------------------------------------
277
- @dataclass
278
738
  class Command:
279
- pdu_id: Protocol.PduId
280
- parameter: bytes
739
+ pdu_id: ClassVar[PduId]
740
+ _payload: Optional[bytes] = None
281
741
 
282
- def to_string(self, properties: dict[str, str]) -> str:
283
- properties_str = ",".join(
284
- [f"{name}={value}" for name, value in properties.items()]
285
- )
286
- return f"Command[{self.pdu_id.name}]({properties_str})"
742
+ _Command = TypeVar('_Command', bound='Command')
743
+ subclasses: ClassVar[dict[int, type[Command]]] = {}
744
+ fields: ClassVar[hci.Fields] = ()
287
745
 
288
- def __str__(self) -> str:
289
- return self.to_string({"parameters": self.parameter.hex()})
746
+ @classmethod
747
+ def command(cls, subclass: type[_Command]) -> type[_Command]:
748
+ cls.subclasses[subclass.pdu_id] = subclass
749
+ subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
750
+ return subclass
290
751
 
291
- def __repr__(self) -> str:
292
- return str(self)
752
+ @classmethod
753
+ def from_bytes(cls, pdu_id: int, pdu: bytes) -> Command:
754
+ if not (subclass := cls.subclasses.get(pdu_id)):
755
+ raise core.InvalidPacketError(f"Unimplemented PDU {pdu_id}")
756
+ instance = subclass(**hci.HCI_Object.dict_from_bytes(pdu, 0, subclass.fields))
757
+ instance._payload = pdu[0:]
758
+ return instance
759
+
760
+ def __bytes__(self) -> bytes:
761
+ if self._payload is None:
762
+ self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
763
+ return self._payload
293
764
 
294
765
 
295
766
  # -----------------------------------------------------------------------------
767
+ @Command.command
768
+ @dataclass
296
769
  class GetCapabilitiesCommand(Command):
297
- class CapabilityId(utils.OpenIntEnum):
770
+ pdu_id = PduId.GET_CAPABILITIES
771
+
772
+ class CapabilityId(hci.SpecableEnum):
298
773
  COMPANY_ID = 0x02
299
774
  EVENTS_SUPPORTED = 0x03
300
775
 
301
- capability_id: CapabilityId
776
+ capability_id: CapabilityId = field(metadata=CapabilityId.type_metadata(1))
302
777
 
303
- @classmethod
304
- def from_bytes(cls, pdu: bytes) -> GetCapabilitiesCommand:
305
- return cls(cls.CapabilityId(pdu[0]))
306
778
 
307
- def __init__(self, capability_id: CapabilityId) -> None:
308
- super().__init__(Protocol.PduId.GET_CAPABILITIES, bytes([capability_id]))
309
- self.capability_id = capability_id
779
+ # -----------------------------------------------------------------------------
780
+ @Command.command
781
+ @dataclass
782
+ class ListPlayerApplicationSettingAttributesCommand(Command):
783
+ pdu_id = PduId.LIST_PLAYER_APPLICATION_SETTING_ATTRIBUTES
784
+
310
785
 
311
- def __str__(self) -> str:
312
- return self.to_string({"capability_id": self.capability_id.name})
786
+ # -----------------------------------------------------------------------------
787
+ @Command.command
788
+ @dataclass
789
+ class ListPlayerApplicationSettingValuesCommand(Command):
790
+ pdu_id = PduId.LIST_PLAYER_APPLICATION_SETTING_VALUES
791
+
792
+ attribute: ApplicationSetting.AttributeId = field(
793
+ metadata=ApplicationSetting.AttributeId.type_metadata(1)
794
+ )
313
795
 
314
796
 
315
797
  # -----------------------------------------------------------------------------
316
- class GetPlayStatusCommand(Command):
317
- @classmethod
318
- def from_bytes(cls, _: bytes) -> GetPlayStatusCommand:
319
- return cls()
798
+ @Command.command
799
+ @dataclass
800
+ class GetCurrentPlayerApplicationSettingValueCommand(Command):
801
+ pdu_id = PduId.GET_CURRENT_PLAYER_APPLICATION_SETTING_VALUE
802
+
803
+ attribute: Sequence[ApplicationSetting.AttributeId] = field(
804
+ metadata=ApplicationSetting.AttributeId.type_metadata(
805
+ 1, list_begin=True, list_end=True
806
+ )
807
+ )
808
+
809
+
810
+ # -----------------------------------------------------------------------------
811
+ @Command.command
812
+ @dataclass
813
+ class SetPlayerApplicationSettingValueCommand(Command):
814
+ pdu_id = PduId.SET_PLAYER_APPLICATION_SETTING_VALUE
815
+
816
+ attribute: Sequence[ApplicationSetting.AttributeId] = field(
817
+ metadata=ApplicationSetting.AttributeId.type_metadata(1, list_begin=True)
818
+ )
819
+ value: Sequence[int] = field(metadata=hci.metadata(1, list_end=True))
820
+
821
+
822
+ # -----------------------------------------------------------------------------
823
+ @Command.command
824
+ @dataclass
825
+ class GetPlayerApplicationSettingAttributeTextCommand(Command):
826
+ pdu_id = PduId.GET_PLAYER_APPLICATION_SETTING_ATTRIBUTE_TEXT
827
+
828
+ attribute: Sequence[ApplicationSetting.AttributeId] = field(
829
+ metadata=ApplicationSetting.AttributeId.type_metadata(
830
+ 1, list_begin=True, list_end=True
831
+ )
832
+ )
833
+
834
+
835
+ # -----------------------------------------------------------------------------
836
+ @Command.command
837
+ @dataclass
838
+ class GetPlayerApplicationSettingValueTextCommand(Command):
839
+ pdu_id = PduId.GET_PLAYER_APPLICATION_SETTING_VALUE_TEXT
320
840
 
321
- def __init__(self) -> None:
322
- super().__init__(Protocol.PduId.GET_PLAY_STATUS, b'')
841
+ attribute: ApplicationSetting.AttributeId = field(
842
+ metadata=ApplicationSetting.AttributeId.type_metadata(1)
843
+ )
844
+ value: Sequence[int] = field(
845
+ metadata=hci.metadata(1, list_begin=True, list_end=True)
846
+ )
323
847
 
324
848
 
325
849
  # -----------------------------------------------------------------------------
850
+ @Command.command
851
+ @dataclass
852
+ class InformDisplayableCharacterSetCommand(Command):
853
+ pdu_id = PduId.INFORM_DISPLAYABLE_CHARACTER_SET
854
+
855
+ character_set_id: Sequence[CharacterSetId] = field(
856
+ metadata=CharacterSetId.type_metadata(
857
+ 2, list_begin=True, list_end=True, byteorder='big'
858
+ )
859
+ )
860
+
861
+
862
+ # -----------------------------------------------------------------------------
863
+ @Command.command
864
+ @dataclass
865
+ class InformBatteryStatusOfCtCommand(Command):
866
+ pdu_id = PduId.INFORM_BATTERY_STATUS_OF_CT
867
+
868
+ class BatteryStatus(hci.SpecableEnum):
869
+ NORMAL = 0x00
870
+ WARNING = 0x01
871
+ CRITICAL = 0x02
872
+ EXTERNAL = 0x03
873
+ FULL_CHARGE = 0x04
874
+
875
+ battery_status: BatteryStatus = field(metadata=BatteryStatus.type_metadata(1))
876
+
877
+
878
+ # -----------------------------------------------------------------------------
879
+ @Command.command
880
+ @dataclass
881
+ class GetPlayStatusCommand(Command):
882
+ pdu_id = PduId.GET_PLAY_STATUS
883
+
884
+
885
+ # -----------------------------------------------------------------------------
886
+ @Command.command
887
+ @dataclass
326
888
  class GetElementAttributesCommand(Command):
327
- identifier: int
328
- attribute_ids: list[MediaAttributeId]
889
+ pdu_id = PduId.GET_ELEMENT_ATTRIBUTES
329
890
 
330
- @classmethod
331
- def from_bytes(cls, pdu: bytes) -> GetElementAttributesCommand:
332
- identifier = struct.unpack_from(">Q", pdu)[0]
333
- num_attributes = pdu[8]
334
- attribute_ids = [MediaAttributeId(pdu[9 + i]) for i in range(num_attributes)]
335
- return cls(identifier, attribute_ids)
336
-
337
- def __init__(
338
- self, identifier: int, attribute_ids: Sequence[MediaAttributeId]
339
- ) -> None:
340
- parameter = struct.pack(">QB", identifier, len(attribute_ids)) + b''.join(
341
- [struct.pack(">I", int(attribute_id)) for attribute_id in attribute_ids]
891
+ identifier: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
892
+ attribute_ids: Sequence[MediaAttributeId] = field(
893
+ metadata=MediaAttributeId.type_metadata(
894
+ 4, list_begin=True, list_end=True, byteorder='big'
342
895
  )
343
- super().__init__(Protocol.PduId.GET_ELEMENT_ATTRIBUTES, parameter)
344
- self.identifier = identifier
345
- self.attribute_ids = list(attribute_ids)
896
+ )
346
897
 
347
898
 
348
899
  # -----------------------------------------------------------------------------
900
+ @Command.command
901
+ @dataclass
349
902
  class SetAbsoluteVolumeCommand(Command):
903
+ pdu_id = PduId.SET_ABSOLUTE_VOLUME
350
904
  MAXIMUM_VOLUME = 0x7F
351
905
 
352
- volume: int
906
+ volume: int = field(metadata=hci.metadata(1))
353
907
 
354
- @classmethod
355
- def from_bytes(cls, pdu: bytes) -> SetAbsoluteVolumeCommand:
356
- return cls(pdu[0])
357
908
 
358
- def __init__(self, volume: int) -> None:
359
- super().__init__(Protocol.PduId.SET_ABSOLUTE_VOLUME, bytes([volume]))
360
- self.volume = volume
909
+ # -----------------------------------------------------------------------------
910
+ @Command.command
911
+ @dataclass
912
+ class RegisterNotificationCommand(Command):
913
+ pdu_id = PduId.REGISTER_NOTIFICATION
361
914
 
362
- def __str__(self) -> str:
363
- return self.to_string({"volume": str(self.volume)})
915
+ event_id: EventId = field(metadata=EventId.type_metadata(1))
916
+ playback_interval: int = field(metadata=hci.metadata('>4'))
364
917
 
365
918
 
366
919
  # -----------------------------------------------------------------------------
367
- class RegisterNotificationCommand(Command):
368
- event_id: EventId
369
- playback_interval: int
920
+ @Command.command
921
+ @dataclass
922
+ class SetAddressedPlayerCommand(Command):
923
+ pdu_id = PduId.SET_ADDRESSED_PLAYER
370
924
 
371
- @classmethod
372
- def from_bytes(cls, pdu: bytes) -> RegisterNotificationCommand:
373
- event_id = EventId(pdu[0])
374
- playback_interval = struct.unpack_from(">I", pdu, 1)[0]
375
- return cls(event_id, playback_interval)
376
-
377
- def __init__(self, event_id: EventId, playback_interval: int) -> None:
378
- super().__init__(
379
- Protocol.PduId.REGISTER_NOTIFICATION,
380
- struct.pack(">BI", int(event_id), playback_interval),
381
- )
382
- self.event_id = event_id
383
- self.playback_interval = playback_interval
384
-
385
- def __str__(self) -> str:
386
- return self.to_string(
387
- {
388
- "event_id": self.event_id.name,
389
- "playback_interval": str(self.playback_interval),
390
- }
925
+ player_id: int = field(metadata=hci.metadata('>2'))
926
+
927
+
928
+ # -----------------------------------------------------------------------------
929
+ @Command.command
930
+ @dataclass
931
+ class SetBrowsedPlayerCommand(Command):
932
+ pdu_id = PduId.SET_BROWSED_PLAYER
933
+
934
+ player_id: int = field(metadata=hci.metadata('>2'))
935
+
936
+
937
+ # -----------------------------------------------------------------------------
938
+ @Command.command
939
+ @dataclass
940
+ class GetFolderItemsCommand(Command):
941
+ pdu_id = PduId.GET_FOLDER_ITEMS
942
+
943
+ scope: Scope = field(metadata=Scope.type_metadata(1))
944
+ start_item: int = field(metadata=hci.metadata('>4'))
945
+ end_item: int = field(metadata=hci.metadata('>4'))
946
+ # When attributes is empty, all attributes will be requested.
947
+ attributes: Sequence[MediaAttributeId] = field(
948
+ metadata=MediaAttributeId.type_metadata(
949
+ 4, list_begin=True, list_end=True, byteorder='big'
391
950
  )
951
+ )
952
+
953
+
954
+ # -----------------------------------------------------------------------------
955
+ @Command.command
956
+ @dataclass
957
+ class ChangePathCommand(Command):
958
+ pdu_id = PduId.CHANGE_PATH
959
+
960
+ class Direction(hci.SpecableEnum):
961
+ UP = 0
962
+ DOWN = 1
963
+
964
+ uid_counter: int = field(metadata=hci.metadata('>2'))
965
+ direction: Direction = field(metadata=Direction.type_metadata(1))
966
+ folder_uid: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
967
+
968
+
969
+ # -----------------------------------------------------------------------------
970
+ @Command.command
971
+ @dataclass
972
+ class GetItemAttributesCommand(Command):
973
+ pdu_id = PduId.GET_ITEM_ATTRIBUTES
974
+
975
+ scope: Scope = field(metadata=Scope.type_metadata(1))
976
+ uid: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
977
+ uid_counter: int = field(metadata=hci.metadata('>2'))
978
+ start_item: int = field(metadata=hci.metadata('>4'))
979
+ end_item: int = field(metadata=hci.metadata('>4'))
980
+ # When attributes is empty, all attributes will be requested.
981
+ attributes: Sequence[MediaAttributeId] = field(
982
+ metadata=MediaAttributeId.type_metadata(1, list_begin=True, list_end=True)
983
+ )
984
+
985
+
986
+ # -----------------------------------------------------------------------------
987
+ @Command.command
988
+ @dataclass
989
+ class GetTotalNumberOfItemsCommand(Command):
990
+ pdu_id = PduId.GET_TOTAL_NUMBER_OF_ITEMS
991
+
992
+ scope: Scope = field(metadata=Scope.type_metadata(1))
993
+
994
+
995
+ # -----------------------------------------------------------------------------
996
+ @Command.command
997
+ @dataclass
998
+ class SearchCommand(Command):
999
+ pdu_id = PduId.SEARCH
1000
+
1001
+ character_set_id: CharacterSetId = field(
1002
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
1003
+ )
1004
+ search_string: str = field(metadata=hci.metadata(_string_spec(2)))
392
1005
 
393
1006
 
394
1007
  # -----------------------------------------------------------------------------
1008
+ @Command.command
395
1009
  @dataclass
1010
+ class PlayItemCommand(Command):
1011
+ pdu_id = PduId.PLAY_ITEM
1012
+
1013
+ scope: Scope = field(metadata=Scope.type_metadata(1))
1014
+ uid: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
1015
+ uid_counter: int = field(metadata=hci.metadata('>2'))
1016
+
1017
+
1018
+ # -----------------------------------------------------------------------------
1019
+ @Command.command
1020
+ @dataclass
1021
+ class AddToNowPlayingCommand(Command):
1022
+ pdu_id = PduId.ADD_TO_NOW_PLAYING
1023
+
1024
+ scope: Scope = field(metadata=Scope.type_metadata(1))
1025
+ uid: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
1026
+ uid_counter: int = field(metadata=hci.metadata('>2'))
1027
+
1028
+
1029
+ # -----------------------------------------------------------------------------
396
1030
  class Response:
397
- pdu_id: Protocol.PduId
398
- parameter: bytes
1031
+ pdu_id: PduId
1032
+ _payload: Optional[bytes] = None
399
1033
 
400
- def to_string(self, properties: dict[str, str]) -> str:
401
- properties_str = ",".join(
402
- [f"{name}={value}" for name, value in properties.items()]
403
- )
404
- return f"Response[{self.pdu_id.name}]({properties_str})"
1034
+ fields: ClassVar[hci.Fields] = ()
1035
+ subclasses: ClassVar[dict[PduId, type[Response]]] = {}
405
1036
 
406
- def __str__(self) -> str:
407
- return self.to_string({"parameter": self.parameter.hex()})
1037
+ _Response = TypeVar('_Response', bound='Response')
408
1038
 
409
- def __repr__(self) -> str:
410
- return str(self)
1039
+ @classmethod
1040
+ def response(cls, subclass: type[_Response]) -> type[_Response]:
1041
+ subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
1042
+ if pdu_id := getattr(subclass, 'pdu_id', None):
1043
+ cls.subclasses[pdu_id] = subclass
1044
+ return subclass
1045
+
1046
+ def __bytes__(self) -> bytes:
1047
+ if self._payload is None:
1048
+ self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
1049
+ return self._payload
1050
+
1051
+ @classmethod
1052
+ def from_bytes(cls, pdu: bytes, pdu_id: PduId) -> Response:
1053
+ if not (subclass := cls.subclasses.get(pdu_id)):
1054
+ raise core.InvalidArgumentError(f"Unimplemented packet {pdu_id.name}")
1055
+ return subclass.from_parameters(pdu)
1056
+
1057
+ @classmethod
1058
+ def from_parameters(cls, parameters: bytes) -> Response:
1059
+ instance = cls(**hci.HCI_Object.dict_from_bytes(parameters, 0, cls.fields))
1060
+ instance._payload = parameters
1061
+ return instance
411
1062
 
412
1063
 
413
1064
  # -----------------------------------------------------------------------------
1065
+ @Response.response
1066
+ @dataclass
414
1067
  class RejectedResponse(Response):
415
- status_code: Protocol.StatusCode
1068
+ pdu_id: PduId
1069
+ status_code: StatusCode = field(metadata=StatusCode.type_metadata(1))
416
1070
 
417
1071
  @classmethod
418
- def from_bytes(cls, pdu_id: Protocol.PduId, pdu: bytes) -> RejectedResponse:
419
- return cls(pdu_id, Protocol.StatusCode(pdu[0]))
420
-
421
- def __init__(
422
- self, pdu_id: Protocol.PduId, status_code: Protocol.StatusCode
423
- ) -> None:
424
- super().__init__(pdu_id, bytes([int(status_code)]))
425
- self.status_code = status_code
426
-
427
- def __str__(self) -> str:
428
- return self.to_string(
429
- {
430
- "status_code": self.status_code.name,
431
- }
432
- )
1072
+ def from_bytes(cls, pdu: bytes, pdu_id: PduId) -> Response:
1073
+ return cls(pdu_id=pdu_id, status_code=StatusCode(pdu[0]))
433
1074
 
434
1075
 
435
1076
  # -----------------------------------------------------------------------------
1077
+ @Response.response
1078
+ @dataclass
436
1079
  class NotImplementedResponse(Response):
1080
+ pdu_id: PduId
1081
+ parameters: bytes = field(metadata=hci.metadata('*'))
1082
+
437
1083
  @classmethod
438
- def from_bytes(cls, pdu_id: Protocol.PduId, pdu: bytes) -> NotImplementedResponse:
439
- return cls(pdu_id, pdu[1:])
1084
+ def from_bytes(cls, pdu: bytes, pdu_id: PduId) -> Response:
1085
+ return cls(pdu_id=pdu_id, parameters=pdu)
440
1086
 
441
1087
 
442
1088
  # -----------------------------------------------------------------------------
1089
+ @Response.response
1090
+ @dataclass
443
1091
  class GetCapabilitiesResponse(Response):
1092
+ pdu_id = PduId.GET_CAPABILITIES
444
1093
  capability_id: GetCapabilitiesCommand.CapabilityId
445
- capabilities: list[Union[SupportsBytes, bytes]]
1094
+ capabilities: Sequence[Union[SupportsBytes, bytes]]
446
1095
 
447
1096
  @classmethod
448
- def from_bytes(cls, pdu: bytes) -> GetCapabilitiesResponse:
449
- if len(pdu) < 2:
1097
+ def from_parameters(cls, parameters: bytes) -> Response:
1098
+ if len(parameters) < 2:
450
1099
  # Possibly a reject response.
451
1100
  return cls(GetCapabilitiesCommand.CapabilityId(0), [])
452
1101
 
453
1102
  # Assume that the payloads all follow the same pattern:
454
1103
  # <CapabilityID><CapabilityCount><Capability*>
455
- capability_id = GetCapabilitiesCommand.CapabilityId(pdu[0])
456
- capability_count = pdu[1]
1104
+ capability_id = GetCapabilitiesCommand.CapabilityId(parameters[0])
1105
+ capability_count = parameters[1]
457
1106
 
458
1107
  capabilities: list[Union[SupportsBytes, bytes]]
459
1108
  if capability_id == GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED:
460
- capabilities = [EventId(pdu[2 + x]) for x in range(capability_count)]
1109
+ capabilities = [EventId(parameters[2 + x]) for x in range(capability_count)]
461
1110
  else:
462
- capability_size = (len(pdu) - 2) // capability_count
1111
+ capability_size = (len(parameters) - 2) // capability_count
463
1112
  capabilities = [
464
- pdu[x : x + capability_size]
465
- for x in range(2, len(pdu), capability_size)
1113
+ parameters[x : x + capability_size]
1114
+ for x in range(2, len(parameters), capability_size)
466
1115
  ]
467
1116
 
468
1117
  return cls(capability_id, capabilities)
469
1118
 
470
- def __init__(
471
- self,
472
- capability_id: GetCapabilitiesCommand.CapabilityId,
473
- capabilities: Sequence[Union[SupportsBytes, bytes]],
474
- ) -> None:
475
- super().__init__(
476
- Protocol.PduId.GET_CAPABILITIES,
477
- bytes([capability_id, len(capabilities)])
478
- + b''.join(bytes(capability) for capability in capabilities),
479
- )
480
- self.capability_id = capability_id
481
- self.capabilities = list(capabilities)
482
-
483
- def __str__(self) -> str:
484
- return self.to_string(
485
- {
486
- "capability_id": self.capability_id.name,
487
- "capabilities": str(self.capabilities),
488
- }
1119
+ def __post_init__(self) -> None:
1120
+ self._payload = bytes([self.capability_id, len(self.capabilities)]) + b''.join(
1121
+ bytes(capability) for capability in self.capabilities
489
1122
  )
490
1123
 
491
1124
 
492
1125
  # -----------------------------------------------------------------------------
493
- class GetPlayStatusResponse(Response):
494
- song_length: int
495
- song_position: int
496
- play_status: PlayStatus
497
-
498
- @classmethod
499
- def from_bytes(cls, pdu: bytes) -> GetPlayStatusResponse:
500
- (song_length, song_position) = struct.unpack_from(">II", pdu, 0)
501
- play_status = PlayStatus(pdu[8])
502
-
503
- return cls(song_length, song_position, play_status)
1126
+ @Response.response
1127
+ @dataclass
1128
+ class ListPlayerApplicationSettingAttributesResponse(Response):
1129
+ pdu_id = PduId.LIST_PLAYER_APPLICATION_SETTING_ATTRIBUTES
504
1130
 
505
- def __init__(
506
- self,
507
- song_length: int,
508
- song_position: int,
509
- play_status: PlayStatus,
510
- ) -> None:
511
- super().__init__(
512
- Protocol.PduId.GET_PLAY_STATUS,
513
- struct.pack(">IIB", song_length, song_position, int(play_status)),
514
- )
515
- self.song_length = song_length
516
- self.song_position = song_position
517
- self.play_status = play_status
518
-
519
- def __str__(self) -> str:
520
- return self.to_string(
521
- {
522
- "song_length": str(self.song_length),
523
- "song_position": str(self.song_position),
524
- "play_status": self.play_status.name,
525
- }
1131
+ attribute: Sequence[ApplicationSetting.AttributeId] = field(
1132
+ metadata=ApplicationSetting.AttributeId.type_metadata(
1133
+ 1, list_begin=True, list_end=True
526
1134
  )
1135
+ )
527
1136
 
528
1137
 
529
1138
  # -----------------------------------------------------------------------------
530
- class GetElementAttributesResponse(Response):
531
- attributes: list[MediaAttribute]
1139
+ @Response.response
1140
+ @dataclass
1141
+ class ListPlayerApplicationSettingValuesResponse(Response):
1142
+ pdu_id = PduId.LIST_PLAYER_APPLICATION_SETTING_VALUES
532
1143
 
533
- @classmethod
534
- def from_bytes(cls, pdu: bytes) -> GetElementAttributesResponse:
535
- num_attributes = pdu[0]
536
- offset = 1
537
- attributes: list[MediaAttribute] = []
538
- for _ in range(num_attributes):
539
- (
540
- attribute_id_int,
541
- character_set_id_int,
542
- attribute_value_length,
543
- ) = struct.unpack_from(">IHH", pdu, offset)
544
- attribute_value_bytes = pdu[
545
- offset + 8 : offset + 8 + attribute_value_length
546
- ]
547
- attribute_id = MediaAttributeId(attribute_id_int)
548
- character_set_id = CharacterSetId(character_set_id_int)
549
- attribute_value = _decode_attribute_value(
550
- attribute_value_bytes, character_set_id
551
- )
552
- attributes.append(
553
- MediaAttribute(attribute_id, character_set_id, attribute_value)
554
- )
555
- offset += 8 + attribute_value_length
556
-
557
- return cls(attributes)
558
-
559
- def __init__(self, attributes: Sequence[MediaAttribute]) -> None:
560
- parameter = bytes([len(attributes)])
561
- for attribute in attributes:
562
- attribute_value_bytes = attribute.attribute_value.encode("utf-8")
563
- parameter += (
564
- struct.pack(
565
- ">IHH",
566
- int(attribute.attribute_id),
567
- int(CharacterSetId.UTF_8),
568
- len(attribute_value_bytes),
569
- )
570
- + attribute_value_bytes
571
- )
572
- super().__init__(
573
- Protocol.PduId.GET_ELEMENT_ATTRIBUTES,
574
- parameter,
575
- )
576
- self.attributes = list(attributes)
577
-
578
- def __str__(self) -> str:
579
- attribute_strs = [str(attribute) for attribute in self.attributes]
580
- return self.to_string(
581
- {
582
- "attributes": f"[{', '.join(attribute_strs)}]",
583
- }
584
- )
1144
+ value: Sequence[int] = field(
1145
+ metadata=hci.metadata(1, list_begin=True, list_end=True)
1146
+ )
585
1147
 
586
1148
 
587
1149
  # -----------------------------------------------------------------------------
588
- class SetAbsoluteVolumeResponse(Response):
589
- volume: int
590
-
591
- @classmethod
592
- def from_bytes(cls, pdu: bytes) -> SetAbsoluteVolumeResponse:
593
- return cls(pdu[0])
594
-
595
- def __init__(self, volume: int) -> None:
596
- super().__init__(Protocol.PduId.SET_ABSOLUTE_VOLUME, bytes([volume]))
597
- self.volume = volume
1150
+ @Response.response
1151
+ @dataclass
1152
+ class GetCurrentPlayerApplicationSettingValueResponse(Response):
1153
+ pdu_id = PduId.GET_CURRENT_PLAYER_APPLICATION_SETTING_VALUE
598
1154
 
599
- def __str__(self) -> str:
600
- return self.to_string({"volume": str(self.volume)})
1155
+ attribute: Sequence[ApplicationSetting.AttributeId] = field(
1156
+ metadata=ApplicationSetting.AttributeId.type_metadata(1, list_begin=True)
1157
+ )
1158
+ value: Sequence[int] = field(metadata=hci.metadata(1, list_end=True))
601
1159
 
602
1160
 
603
1161
  # -----------------------------------------------------------------------------
604
- class RegisterNotificationResponse(Response):
605
- event: Event
1162
+ @Response.response
1163
+ @dataclass
1164
+ class SetPlayerApplicationSettingValueResponse(Response):
1165
+ pdu_id = PduId.SET_PLAYER_APPLICATION_SETTING_VALUE
606
1166
 
607
- @classmethod
608
- def from_bytes(cls, pdu: bytes) -> RegisterNotificationResponse:
609
- return cls(Event.from_bytes(pdu))
610
1167
 
611
- def __init__(self, event: Event) -> None:
612
- super().__init__(
613
- Protocol.PduId.REGISTER_NOTIFICATION,
614
- bytes(event),
615
- )
616
- self.event = event
1168
+ # -----------------------------------------------------------------------------
1169
+ @Response.response
1170
+ @dataclass
1171
+ class GetPlayerApplicationSettingAttributeTextResponse(Response):
1172
+ pdu_id = PduId.GET_PLAYER_APPLICATION_SETTING_ATTRIBUTE_TEXT
617
1173
 
618
- def __str__(self) -> str:
619
- return self.to_string(
620
- {
621
- "event": str(self.event),
622
- }
623
- )
1174
+ attribute: Sequence[ApplicationSetting.AttributeId] = field(
1175
+ metadata=ApplicationSetting.AttributeId.type_metadata(1, list_begin=True)
1176
+ )
1177
+ character_set_id: Sequence[CharacterSetId] = field(
1178
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
1179
+ )
1180
+ attribute_string: Sequence[str] = field(
1181
+ metadata=hci.metadata(_string_spec(1), list_end=True)
1182
+ )
624
1183
 
625
1184
 
626
1185
  # -----------------------------------------------------------------------------
627
- class EventId(utils.OpenIntEnum):
628
- PLAYBACK_STATUS_CHANGED = 0x01
629
- TRACK_CHANGED = 0x02
630
- TRACK_REACHED_END = 0x03
631
- TRACK_REACHED_START = 0x04
632
- PLAYBACK_POS_CHANGED = 0x05
633
- BATT_STATUS_CHANGED = 0x06
634
- SYSTEM_STATUS_CHANGED = 0x07
635
- PLAYER_APPLICATION_SETTING_CHANGED = 0x08
636
- NOW_PLAYING_CONTENT_CHANGED = 0x09
637
- AVAILABLE_PLAYERS_CHANGED = 0x0A
638
- ADDRESSED_PLAYER_CHANGED = 0x0B
639
- UIDS_CHANGED = 0x0C
640
- VOLUME_CHANGED = 0x0D
1186
+ @Response.response
1187
+ @dataclass
1188
+ class GetPlayerApplicationSettingValueTextResponse(Response):
1189
+ pdu_id = PduId.GET_PLAYER_APPLICATION_SETTING_VALUE_TEXT
641
1190
 
642
- def __bytes__(self) -> bytes:
643
- return bytes([int(self)])
1191
+ value: Sequence[int] = field(metadata=hci.metadata(1, list_begin=True))
1192
+ character_set_id: Sequence[CharacterSetId] = field(
1193
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
1194
+ )
1195
+ attribute_string: Sequence[str] = field(
1196
+ metadata=hci.metadata(_string_spec(1), list_end=True)
1197
+ )
644
1198
 
645
1199
 
646
1200
  # -----------------------------------------------------------------------------
647
- class CharacterSetId(utils.OpenIntEnum):
648
- UTF_8 = 0x06
1201
+ @Response.response
1202
+ @dataclass
1203
+ class InformDisplayableCharacterSetResponse(Response):
1204
+ pdu_id = PduId.INFORM_DISPLAYABLE_CHARACTER_SET
649
1205
 
650
1206
 
651
1207
  # -----------------------------------------------------------------------------
652
- class MediaAttributeId(utils.OpenIntEnum):
653
- TITLE = 0x01
654
- ARTIST_NAME = 0x02
655
- ALBUM_NAME = 0x03
656
- TRACK_NUMBER = 0x04
657
- TOTAL_NUMBER_OF_TRACKS = 0x05
658
- GENRE = 0x06
659
- PLAYING_TIME = 0x07
660
- DEFAULT_COVER_ART = 0x08
1208
+ @Response.response
1209
+ @dataclass
1210
+ class InformBatteryStatusOfCtResponse(Response):
1211
+ pdu_id = PduId.INFORM_BATTERY_STATUS_OF_CT
661
1212
 
662
1213
 
663
1214
  # -----------------------------------------------------------------------------
1215
+ @Response.response
664
1216
  @dataclass
665
- class MediaAttribute:
666
- attribute_id: MediaAttributeId
667
- character_set_id: CharacterSetId
668
- attribute_value: str
1217
+ class GetPlayStatusResponse(Response):
1218
+ pdu_id = PduId.GET_PLAY_STATUS
1219
+ song_length: int = field(metadata=hci.metadata(">4"))
1220
+ song_position: int = field(metadata=hci.metadata(">4"))
1221
+ play_status: PlayStatus = field(metadata=PlayStatus.type_metadata(1))
669
1222
 
670
1223
 
671
1224
  # -----------------------------------------------------------------------------
672
- class PlayStatus(utils.OpenIntEnum):
673
- STOPPED = 0x00
674
- PLAYING = 0x01
675
- PAUSED = 0x02
676
- FWD_SEEK = 0x03
677
- REV_SEEK = 0x04
678
- ERROR = 0xFF
1225
+ @Response.response
1226
+ @dataclass
1227
+ class GetElementAttributesResponse(Response):
1228
+ pdu_id = PduId.GET_ELEMENT_ATTRIBUTES
1229
+ attributes: Sequence[MediaAttribute] = field(
1230
+ metadata=hci.metadata(
1231
+ MediaAttribute.parse_from_bytes, list_begin=True, list_end=True
1232
+ )
1233
+ )
679
1234
 
680
1235
 
681
1236
  # -----------------------------------------------------------------------------
1237
+ @Response.response
682
1238
  @dataclass
683
- class SongAndPlayStatus:
684
- song_length: int
685
- song_position: int
686
- play_status: PlayStatus
1239
+ class SetAbsoluteVolumeResponse(Response):
1240
+ pdu_id = PduId.SET_ABSOLUTE_VOLUME
1241
+ volume: int = field(metadata=hci.metadata(1))
687
1242
 
688
1243
 
689
1244
  # -----------------------------------------------------------------------------
690
- class ApplicationSetting:
691
- class AttributeId(utils.OpenIntEnum):
692
- EQUALIZER_ON_OFF = 0x01
693
- REPEAT_MODE = 0x02
694
- SHUFFLE_ON_OFF = 0x03
695
- SCAN_ON_OFF = 0x04
696
-
697
- class EqualizerOnOffStatus(utils.OpenIntEnum):
698
- OFF = 0x01
699
- ON = 0x02
1245
+ @Response.response
1246
+ @dataclass
1247
+ class RegisterNotificationResponse(Response):
1248
+ pdu_id = PduId.REGISTER_NOTIFICATION
1249
+ event: Event = field(
1250
+ metadata=hci.metadata(
1251
+ lambda data, offset: (len(data), Event.from_bytes(data[offset:]))
1252
+ )
1253
+ )
700
1254
 
701
- class RepeatModeStatus(utils.OpenIntEnum):
702
- OFF = 0x01
703
- SINGLE_TRACK_REPEAT = 0x02
704
- ALL_TRACK_REPEAT = 0x03
705
- GROUP_REPEAT = 0x04
706
1255
 
707
- class ShuffleOnOffStatus(utils.OpenIntEnum):
708
- OFF = 0x01
709
- ALL_TRACKS_SHUFFLE = 0x02
710
- GROUP_SHUFFLE = 0x03
711
-
712
- class ScanOnOffStatus(utils.OpenIntEnum):
713
- OFF = 0x01
714
- ALL_TRACKS_SCAN = 0x02
715
- GROUP_SCAN = 0x03
1256
+ # -----------------------------------------------------------------------------
1257
+ @Response.response
1258
+ @dataclass
1259
+ class SetAddressedPlayerResponse(Response):
1260
+ pdu_id = PduId.SET_ADDRESSED_PLAYER
716
1261
 
717
- class GenericValue(utils.OpenIntEnum):
718
- pass
1262
+ status: StatusCode = field(metadata=StatusCode.type_metadata(1))
719
1263
 
720
1264
 
721
1265
  # -----------------------------------------------------------------------------
1266
+ @Response.response
722
1267
  @dataclass
723
- class Event:
724
- event_id: EventId
725
-
726
- @classmethod
727
- def from_bytes(cls, pdu: bytes) -> Event:
728
- event_id = EventId(pdu[0])
729
- subclass = EVENT_SUBCLASSES.get(event_id, GenericEvent)
730
- return subclass.from_bytes(pdu)
1268
+ class SetBrowsedPlayerResponse(Response):
1269
+ pdu_id = PduId.SET_BROWSED_PLAYER
731
1270
 
732
- def __bytes__(self) -> bytes:
733
- return bytes([self.event_id])
1271
+ status: StatusCode = field(metadata=StatusCode.type_metadata(1))
1272
+ uid_counter: int = field(metadata=hci.metadata('>2'))
1273
+ numbers_of_items: int = field(metadata=hci.metadata('>4'))
1274
+ character_set_id: CharacterSetId = field(
1275
+ metadata=CharacterSetId.type_metadata(2, byteorder='big')
1276
+ )
1277
+ folder_names: Sequence[str] = field(
1278
+ metadata=hci.metadata(_string_spec(2), list_begin=True, list_end=True)
1279
+ )
734
1280
 
735
1281
 
736
1282
  # -----------------------------------------------------------------------------
1283
+ @Response.response
737
1284
  @dataclass
738
- class GenericEvent(Event):
739
- data: bytes
1285
+ class GetFolderItemsResponse(Response):
1286
+ pdu_id = PduId.GET_FOLDER_ITEMS
740
1287
 
741
- @classmethod
742
- def from_bytes(cls, pdu: bytes) -> GenericEvent:
743
- return cls(event_id=EventId(pdu[0]), data=pdu[1:])
1288
+ status: StatusCode
1289
+ uid_counter: int
1290
+ items: Sequence[BrowseableItem]
744
1291
 
745
- def __bytes__(self) -> bytes:
746
- return bytes([self.event_id]) + self.data
1292
+ @classmethod
1293
+ def from_parameters(cls, parameters: bytes) -> Response:
1294
+ status, uid_counter, count = struct.unpack_from('>BHH', parameters)
1295
+ items: list[BrowseableItem] = []
1296
+ offset = 5
1297
+ for _ in range(count):
1298
+ offset, item = BrowseableItem.parse_from_bytes(parameters, offset)
1299
+ items.append(item)
1300
+ instance = cls(status=StatusCode(status), uid_counter=uid_counter, items=items)
1301
+ instance._payload = parameters
1302
+ return instance
1303
+
1304
+ def __post_init__(self) -> None:
1305
+ if self._payload is None:
1306
+ self._payload = struct.pack(
1307
+ '>BHH', self.status, self.uid_counter, len(self.items)
1308
+ ) + b''.join(map(bytes, self.items))
747
1309
 
748
1310
 
749
1311
  # -----------------------------------------------------------------------------
1312
+ @Response.response
750
1313
  @dataclass
751
- class PlaybackStatusChangedEvent(Event):
752
- play_status: PlayStatus
753
-
754
- @classmethod
755
- def from_bytes(cls, pdu: bytes) -> PlaybackStatusChangedEvent:
756
- return cls(play_status=PlayStatus(pdu[1]))
757
-
758
- def __init__(self, play_status: PlayStatus) -> None:
759
- super().__init__(EventId.PLAYBACK_STATUS_CHANGED)
760
- self.play_status = play_status
1314
+ class ChangePathResponse(Response):
1315
+ pdu_id = PduId.CHANGE_PATH
761
1316
 
762
- def __bytes__(self) -> bytes:
763
- return bytes([self.event_id]) + bytes([self.play_status])
1317
+ status: StatusCode = field(metadata=StatusCode.type_metadata(1))
1318
+ number_of_items: int = field(metadata=hci.metadata('>4'))
764
1319
 
765
1320
 
766
1321
  # -----------------------------------------------------------------------------
1322
+ @Response.response
767
1323
  @dataclass
768
- class PlaybackPositionChangedEvent(Event):
769
- playback_position: int
1324
+ class GetItemAttributesResponse(Response):
1325
+ pdu_id = PduId.GET_ITEM_ATTRIBUTES
770
1326
 
771
- @classmethod
772
- def from_bytes(cls, pdu: bytes) -> PlaybackPositionChangedEvent:
773
- return cls(playback_position=struct.unpack_from(">I", pdu, 1)[0])
1327
+ status: StatusCode = field(metadata=StatusCode.type_metadata(1))
1328
+ attribute_value_entry_list: Sequence[AttributeValueEntry] = field(
1329
+ metadata=hci.metadata(
1330
+ AttributeValueEntry.parse_from_bytes, list_begin=True, list_end=True
1331
+ )
1332
+ )
774
1333
 
775
- def __init__(self, playback_position: int) -> None:
776
- super().__init__(EventId.PLAYBACK_POS_CHANGED)
777
- self.playback_position = playback_position
778
1334
 
779
- def __bytes__(self) -> bytes:
780
- return bytes([self.event_id]) + struct.pack(">I", self.playback_position)
1335
+ # -----------------------------------------------------------------------------
1336
+ @Response.response
1337
+ @dataclass
1338
+ class GetTotalNumberOfItemsResponse(Response):
1339
+ pdu_id = PduId.GET_TOTAL_NUMBER_OF_ITEMS
1340
+
1341
+ status: StatusCode = field(metadata=StatusCode.type_metadata(1))
1342
+ uid_counter: int = field(metadata=hci.metadata('>2'))
1343
+ number_of_items: int = field(metadata=hci.metadata('>4'))
781
1344
 
782
1345
 
783
1346
  # -----------------------------------------------------------------------------
1347
+ @Response.response
784
1348
  @dataclass
785
- class TrackChangedEvent(Event):
786
- identifier: bytes
1349
+ class SearchResponse(Response):
1350
+ pdu_id = PduId.SEARCH
787
1351
 
788
- @classmethod
789
- def from_bytes(cls, pdu: bytes) -> TrackChangedEvent:
790
- return cls(identifier=pdu[1:])
1352
+ status: StatusCode = field(metadata=StatusCode.type_metadata(1))
1353
+ uid_counter: int = field(metadata=hci.metadata('>2'))
1354
+ number_of_items: int = field(metadata=hci.metadata('>4'))
791
1355
 
792
- def __init__(self, identifier: bytes) -> None:
793
- super().__init__(EventId.TRACK_CHANGED)
794
- self.identifier = identifier
795
1356
 
796
- def __bytes__(self) -> bytes:
797
- return bytes([self.event_id]) + self.identifier
1357
+ # -----------------------------------------------------------------------------
1358
+ @Response.response
1359
+ @dataclass
1360
+ class PlayItemResponse(Response):
1361
+ pdu_id = PduId.PLAY_ITEM
1362
+
1363
+ status: StatusCode = field(metadata=StatusCode.type_metadata(1))
798
1364
 
799
1365
 
800
1366
  # -----------------------------------------------------------------------------
1367
+ @Response.response
801
1368
  @dataclass
802
- class PlayerApplicationSettingChangedEvent(Event):
803
- @dataclass
804
- class Setting:
805
- attribute_id: ApplicationSetting.AttributeId
806
- value_id: utils.OpenIntEnum
1369
+ class AddToNowPlayingResponse(Response):
1370
+ pdu_id = PduId.ADD_TO_NOW_PLAYING
807
1371
 
808
- player_application_settings: list[Setting]
1372
+ status: StatusCode = field(metadata=StatusCode.type_metadata(1))
809
1373
 
810
- @classmethod
811
- def from_bytes(cls, pdu: bytes) -> PlayerApplicationSettingChangedEvent:
812
- def setting(attribute_id_int: int, value_id_int: int):
813
- attribute_id = ApplicationSetting.AttributeId(attribute_id_int)
814
- value_id: utils.OpenIntEnum
815
- if attribute_id == ApplicationSetting.AttributeId.EQUALIZER_ON_OFF:
816
- value_id = ApplicationSetting.EqualizerOnOffStatus(value_id_int)
817
- elif attribute_id == ApplicationSetting.AttributeId.REPEAT_MODE:
818
- value_id = ApplicationSetting.RepeatModeStatus(value_id_int)
819
- elif attribute_id == ApplicationSetting.AttributeId.SHUFFLE_ON_OFF:
820
- value_id = ApplicationSetting.ShuffleOnOffStatus(value_id_int)
821
- elif attribute_id == ApplicationSetting.AttributeId.SCAN_ON_OFF:
822
- value_id = ApplicationSetting.ScanOnOffStatus(value_id_int)
823
- else:
824
- value_id = ApplicationSetting.GenericValue(value_id_int)
825
1374
 
826
- return cls.Setting(attribute_id, value_id)
1375
+ # -----------------------------------------------------------------------------
1376
+ class Event:
1377
+ event_id: EventId
1378
+ _pdu: Optional[bytes] = None
827
1379
 
828
- settings = [
829
- setting(pdu[2 + (i * 2)], pdu[2 + (i * 2) + 1]) for i in range(pdu[1])
830
- ]
831
- return cls(player_application_settings=settings)
1380
+ _Event = TypeVar('_Event', bound='Event')
1381
+ subclasses: ClassVar[dict[int, type[Event]]] = {}
1382
+ fields: ClassVar[hci.Fields] = ()
832
1383
 
833
- def __init__(self, player_application_settings: Sequence[Setting]) -> None:
834
- super().__init__(EventId.PLAYER_APPLICATION_SETTING_CHANGED)
835
- self.player_application_settings = list(player_application_settings)
1384
+ @classmethod
1385
+ def event(cls, subclass: type[_Event]) -> type[_Event]:
1386
+ cls.subclasses[subclass.event_id] = subclass
1387
+ subclass.fields = hci.HCI_Object.fields_from_dataclass(subclass)
1388
+ return subclass
1389
+
1390
+ @classmethod
1391
+ def from_bytes(cls, pdu: bytes) -> Event:
1392
+ if not (subclass := cls.subclasses.get(pdu[0])):
1393
+ raise core.InvalidPacketError(f"Unimplemented Event {pdu[0]}")
1394
+ instance = subclass(**hci.HCI_Object.dict_from_bytes(pdu, 1, subclass.fields))
1395
+ instance._pdu = pdu
1396
+ return instance
836
1397
 
837
1398
  def __bytes__(self) -> bytes:
838
- return (
839
- bytes([self.event_id])
840
- + bytes([len(self.player_application_settings)])
841
- + b''.join(
842
- [
843
- bytes([setting.attribute_id, setting.value_id])
844
- for setting in self.player_application_settings
845
- ]
1399
+ if self._pdu is None:
1400
+ self._pdu = bytes([self.event_id]) + hci.HCI_Object.dict_to_bytes(
1401
+ self.__dict__, self.fields
846
1402
  )
847
- )
1403
+ return self._pdu
848
1404
 
849
1405
 
850
1406
  # -----------------------------------------------------------------------------
851
1407
  @dataclass
852
- class NowPlayingContentChangedEvent(Event):
853
- @classmethod
854
- def from_bytes(cls, pdu: bytes) -> NowPlayingContentChangedEvent:
855
- return cls()
1408
+ class GenericEvent(Event):
1409
+ event_id: EventId = field(metadata=EventId.type_metadata(1))
1410
+ data: bytes = field(metadata=hci.metadata('*'))
1411
+
856
1412
 
857
- def __init__(self) -> None:
858
- super().__init__(EventId.NOW_PLAYING_CONTENT_CHANGED)
1413
+ GenericEvent.fields = hci.HCI_Object.fields_from_dataclass(GenericEvent)
859
1414
 
860
1415
 
861
1416
  # -----------------------------------------------------------------------------
1417
+ @Event.event
862
1418
  @dataclass
863
- class AvailablePlayersChangedEvent(Event):
864
- @classmethod
865
- def from_bytes(cls, pdu: bytes) -> AvailablePlayersChangedEvent:
866
- return cls()
1419
+ class PlaybackStatusChangedEvent(Event):
1420
+ event_id = EventId.PLAYBACK_STATUS_CHANGED
1421
+ play_status: PlayStatus = field(metadata=PlayStatus.type_metadata(1))
1422
+
867
1423
 
868
- def __init__(self) -> None:
869
- super().__init__(EventId.AVAILABLE_PLAYERS_CHANGED)
1424
+ # -----------------------------------------------------------------------------
1425
+ @Event.event
1426
+ @dataclass
1427
+ class PlaybackPositionChangedEvent(Event):
1428
+ event_id = EventId.PLAYBACK_POS_CHANGED
1429
+ playback_position: int = field(metadata=hci.metadata('>4'))
870
1430
 
871
1431
 
872
1432
  # -----------------------------------------------------------------------------
1433
+ @Event.event
873
1434
  @dataclass
874
- class AddressedPlayerChangedEvent(Event):
875
- @dataclass
876
- class Player:
877
- player_id: int
878
- uid_counter: int
1435
+ class TrackChangedEvent(Event):
1436
+ event_id = EventId.TRACK_CHANGED
1437
+ identifier: bytes = field(metadata=hci.metadata('*'))
879
1438
 
880
- @classmethod
881
- def from_bytes(cls, pdu: bytes) -> AddressedPlayerChangedEvent:
882
- player_id, uid_counter = struct.unpack_from("<HH", pdu, 1)
883
- return cls(cls.Player(player_id, uid_counter))
884
1439
 
885
- def __init__(self, player: Player) -> None:
886
- super().__init__(EventId.ADDRESSED_PLAYER_CHANGED)
887
- self.player = player
1440
+ # -----------------------------------------------------------------------------
1441
+ @Event.event
1442
+ @dataclass
1443
+ class PlayerApplicationSettingChangedEvent(Event):
1444
+ event_id = EventId.PLAYER_APPLICATION_SETTING_CHANGED
888
1445
 
889
- def __bytes__(self) -> bytes:
890
- return bytes([self.event_id]) + struct.pack(
891
- ">HH", self.player.player_id, self.player.uid_counter
1446
+ @dataclass
1447
+ class Setting(hci.HCI_Dataclass_Object):
1448
+ attribute_id: ApplicationSetting.AttributeId = field(
1449
+ metadata=ApplicationSetting.AttributeId.type_metadata(1)
892
1450
  )
1451
+ value_id: Union[
1452
+ ApplicationSetting.EqualizerOnOffStatus,
1453
+ ApplicationSetting.RepeatModeStatus,
1454
+ ApplicationSetting.ShuffleOnOffStatus,
1455
+ ApplicationSetting.ScanOnOffStatus,
1456
+ ApplicationSetting.GenericValue,
1457
+ ] = field(metadata=hci.metadata(1))
1458
+
1459
+ def __post_init__(self) -> None:
1460
+ super().__post_init__()
1461
+ if self.attribute_id == ApplicationSetting.AttributeId.EQUALIZER_ON_OFF:
1462
+ self.value_id = ApplicationSetting.EqualizerOnOffStatus(self.value_id)
1463
+ elif self.attribute_id == ApplicationSetting.AttributeId.REPEAT_MODE:
1464
+ self.value_id = ApplicationSetting.RepeatModeStatus(self.value_id)
1465
+ elif self.attribute_id == ApplicationSetting.AttributeId.SHUFFLE_ON_OFF:
1466
+ self.value_id = ApplicationSetting.ShuffleOnOffStatus(self.value_id)
1467
+ elif self.attribute_id == ApplicationSetting.AttributeId.SCAN_ON_OFF:
1468
+ self.value_id = ApplicationSetting.ScanOnOffStatus(self.value_id)
1469
+ else:
1470
+ self.value_id = ApplicationSetting.GenericValue(self.value_id)
1471
+
1472
+ player_application_settings: Sequence[Setting] = field(
1473
+ metadata=hci.metadata(Setting.parse_from_bytes, list_begin=True, list_end=True)
1474
+ )
893
1475
 
894
1476
 
895
1477
  # -----------------------------------------------------------------------------
1478
+ @Event.event
896
1479
  @dataclass
897
- class UidsChangedEvent(Event):
898
- uid_counter: int
899
-
900
- @classmethod
901
- def from_bytes(cls, pdu: bytes) -> UidsChangedEvent:
902
- return cls(uid_counter=struct.unpack_from(">H", pdu, 1)[0])
1480
+ class NowPlayingContentChangedEvent(Event):
1481
+ event_id = EventId.NOW_PLAYING_CONTENT_CHANGED
903
1482
 
904
- def __init__(self, uid_counter: int) -> None:
905
- super().__init__(EventId.UIDS_CHANGED)
906
- self.uid_counter = uid_counter
907
1483
 
908
- def __bytes__(self) -> bytes:
909
- return bytes([self.event_id]) + struct.pack(">H", self.uid_counter)
1484
+ # -----------------------------------------------------------------------------
1485
+ @Event.event
1486
+ @dataclass
1487
+ class AvailablePlayersChangedEvent(Event):
1488
+ event_id = EventId.AVAILABLE_PLAYERS_CHANGED
910
1489
 
911
1490
 
912
1491
  # -----------------------------------------------------------------------------
1492
+ @Event.event
913
1493
  @dataclass
914
- class VolumeChangedEvent(Event):
915
- volume: int
1494
+ class AddressedPlayerChangedEvent(Event):
1495
+ event_id = EventId.ADDRESSED_PLAYER_CHANGED
916
1496
 
917
- @classmethod
918
- def from_bytes(cls, pdu: bytes) -> VolumeChangedEvent:
919
- return cls(volume=pdu[1])
1497
+ @dataclass
1498
+ class Player(hci.HCI_Dataclass_Object):
1499
+ player_id: int = field(metadata=hci.metadata('>2'))
1500
+ uid_counter: int = field(metadata=hci.metadata('>2'))
920
1501
 
921
- def __init__(self, volume: int) -> None:
922
- super().__init__(EventId.VOLUME_CHANGED)
923
- self.volume = volume
1502
+ player: Player = field(metadata=hci.metadata(Player.parse_from_bytes))
924
1503
 
925
- def __bytes__(self) -> bytes:
926
- return bytes([self.event_id]) + bytes([self.volume])
1504
+
1505
+ # -----------------------------------------------------------------------------
1506
+ @Event.event
1507
+ @dataclass
1508
+ class UidsChangedEvent(Event):
1509
+ event_id = EventId.UIDS_CHANGED
1510
+ uid_counter: int = field(metadata=hci.metadata('>2'))
927
1511
 
928
1512
 
929
1513
  # -----------------------------------------------------------------------------
930
- EVENT_SUBCLASSES: dict[EventId, type[Event]] = {
931
- EventId.PLAYBACK_STATUS_CHANGED: PlaybackStatusChangedEvent,
932
- EventId.PLAYBACK_POS_CHANGED: PlaybackPositionChangedEvent,
933
- EventId.TRACK_CHANGED: TrackChangedEvent,
934
- EventId.PLAYER_APPLICATION_SETTING_CHANGED: PlayerApplicationSettingChangedEvent,
935
- EventId.NOW_PLAYING_CONTENT_CHANGED: NowPlayingContentChangedEvent,
936
- EventId.AVAILABLE_PLAYERS_CHANGED: AvailablePlayersChangedEvent,
937
- EventId.ADDRESSED_PLAYER_CHANGED: AddressedPlayerChangedEvent,
938
- EventId.UIDS_CHANGED: UidsChangedEvent,
939
- EventId.VOLUME_CHANGED: VolumeChangedEvent,
940
- }
1514
+ @Event.event
1515
+ @dataclass
1516
+ class VolumeChangedEvent(Event):
1517
+ event_id = EventId.VOLUME_CHANGED
1518
+ volume: int = field(metadata=hci.metadata(1))
941
1519
 
942
1520
 
943
1521
  # -----------------------------------------------------------------------------
@@ -952,7 +1530,7 @@ class Delegate:
952
1530
  class Error(Exception):
953
1531
  """The delegate method failed, with a specified status code."""
954
1532
 
955
- def __init__(self, status_code: Protocol.StatusCode) -> None:
1533
+ def __init__(self, status_code: StatusCode) -> None:
956
1534
  self.status_code = status_code
957
1535
 
958
1536
  supported_events: list[EventId]
@@ -994,51 +1572,6 @@ class Protocol(utils.EventEmitter):
994
1572
  CONTINUE = 0b10
995
1573
  END = 0b11
996
1574
 
997
- class PduId(utils.OpenIntEnum):
998
- GET_CAPABILITIES = 0x10
999
- LIST_PLAYER_APPLICATION_SETTING_ATTRIBUTES = 0x11
1000
- LIST_PLAYER_APPLICATION_SETTING_VALUES = 0x12
1001
- GET_CURRENT_PLAYER_APPLICATION_SETTING_VALUE = 0x13
1002
- SET_PLAYER_APPLICATION_SETTING_VALUE = 0x14
1003
- GET_PLAYER_APPLICATION_SETTING_ATTRIBUTE_TEXT = 0x15
1004
- GET_PLAYER_APPLICATION_SETTING_VALUE_TEXT = 0x16
1005
- INFORM_DISPLAYABLE_CHARACTER_SET = 0x17
1006
- INFORM_BATTERY_STATUS_OF_CT = 0x18
1007
- GET_ELEMENT_ATTRIBUTES = 0x20
1008
- GET_PLAY_STATUS = 0x30
1009
- REGISTER_NOTIFICATION = 0x31
1010
- REQUEST_CONTINUING_RESPONSE = 0x40
1011
- ABORT_CONTINUING_RESPONSE = 0x41
1012
- SET_ABSOLUTE_VOLUME = 0x50
1013
- SET_ADDRESSED_PLAYER = 0x60
1014
- SET_BROWSED_PLAYER = 0x70
1015
- GET_FOLDER_ITEMS = 0x71
1016
- GET_TOTAL_NUMBER_OF_ITEMS = 0x75
1017
-
1018
- class StatusCode(utils.OpenIntEnum):
1019
- INVALID_COMMAND = 0x00
1020
- INVALID_PARAMETER = 0x01
1021
- PARAMETER_CONTENT_ERROR = 0x02
1022
- INTERNAL_ERROR = 0x03
1023
- OPERATION_COMPLETED = 0x04
1024
- UID_CHANGED = 0x05
1025
- INVALID_DIRECTION = 0x07
1026
- NOT_A_DIRECTORY = 0x08
1027
- DOES_NOT_EXIST = 0x09
1028
- INVALID_SCOPE = 0x0A
1029
- RANGE_OUT_OF_BOUNDS = 0x0B
1030
- FOLDER_ITEM_IS_NOT_PLAYABLE = 0x0C
1031
- MEDIA_IN_USE = 0x0D
1032
- NOW_PLAYING_LIST_FULL = 0x0E
1033
- SEARCH_NOT_SUPPORTED = 0x0F
1034
- SEARCH_IN_PROGRESS = 0x10
1035
- INVALID_PLAYER_ID = 0x11
1036
- PLAYER_NOT_BROWSABLE = 0x12
1037
- PLAYER_NOT_ADDRESSED = 0x13
1038
- NO_VALID_SEARCH_RESULTS = 0x14
1039
- NO_AVAILABLE_PLAYERS = 0x15
1040
- ADDRESSED_PLAYER_CHANGED = 0x16
1041
-
1042
1575
  class InvalidPidError(Exception):
1043
1576
  """A response frame with ipid==1 was received."""
1044
1577
 
@@ -1117,7 +1650,7 @@ class Protocol(utils.EventEmitter):
1117
1650
 
1118
1651
  @staticmethod
1119
1652
  def _check_vendor_dependent_frame(
1120
- frame: Union[avc.VendorDependentCommandFrame, avc.VendorDependentResponseFrame]
1653
+ frame: Union[avc.VendorDependentCommandFrame, avc.VendorDependentResponseFrame],
1121
1654
  ) -> bool:
1122
1655
  if frame.company_id != AVRCP_BLUETOOTH_SIG_COMPANY_ID:
1123
1656
  logger.debug("unsupported company id, ignoring")
@@ -1152,7 +1685,9 @@ class Protocol(utils.EventEmitter):
1152
1685
  A 'connection' event will be emitted when a connection is made, and a 'start'
1153
1686
  event will be emitted when the protocol is ready to be used on that connection.
1154
1687
  """
1155
- device.register_l2cap_server(avctp.AVCTP_PSM, self._on_avctp_connection)
1688
+ device.create_l2cap_server(
1689
+ l2cap.ClassicChannelSpec(avctp.AVCTP_PSM), self._on_avctp_connection
1690
+ )
1156
1691
 
1157
1692
  async def connect(self, connection: Connection) -> None:
1158
1693
  """
@@ -1213,7 +1748,7 @@ class Protocol(utils.EventEmitter):
1213
1748
  self.send_rejected_avrcp_response(
1214
1749
  transaction_label,
1215
1750
  command.pdu_id,
1216
- Protocol.StatusCode.INTERNAL_ERROR,
1751
+ StatusCode.INTERNAL_ERROR,
1217
1752
  )
1218
1753
 
1219
1754
  utils.AsyncRunner.spawn(call())
@@ -1248,7 +1783,7 @@ class Protocol(utils.EventEmitter):
1248
1783
  GetElementAttributesCommand(element_identifier, attribute_ids),
1249
1784
  )
1250
1785
  response = self._check_response(response_context, GetElementAttributesResponse)
1251
- return response.attributes
1786
+ return list(response.attributes)
1252
1787
 
1253
1788
  async def monitor_events(
1254
1789
  self, event_id: EventId, playback_interval: int = 0
@@ -1331,7 +1866,7 @@ class Protocol(utils.EventEmitter):
1331
1866
  if not isinstance(event, PlayerApplicationSettingChangedEvent):
1332
1867
  logger.warning("unexpected event class")
1333
1868
  continue
1334
- yield event.player_application_settings
1869
+ yield list(event.player_application_settings)
1335
1870
 
1336
1871
  async def monitor_now_playing_content(self) -> AsyncIterator[None]:
1337
1872
  """Monitor Now Playing changes from the connected peer."""
@@ -1601,29 +2136,24 @@ class Protocol(utils.EventEmitter):
1601
2136
  avc.CommandFrame.CommandType.NOTIFY,
1602
2137
  ):
1603
2138
  # TODO: catch exceptions from delegates
1604
- if pdu_id == self.PduId.GET_CAPABILITIES:
1605
- self._on_get_capabilities_command(
1606
- transaction_label, GetCapabilitiesCommand.from_bytes(pdu)
1607
- )
1608
- elif pdu_id == self.PduId.SET_ABSOLUTE_VOLUME:
1609
- self._on_set_absolute_volume_command(
1610
- transaction_label, SetAbsoluteVolumeCommand.from_bytes(pdu)
1611
- )
1612
- elif pdu_id == self.PduId.REGISTER_NOTIFICATION:
1613
- self._on_register_notification_command(
1614
- transaction_label, RegisterNotificationCommand.from_bytes(pdu)
1615
- )
2139
+ command = Command.from_bytes(pdu_id, pdu)
2140
+ if isinstance(command, GetCapabilitiesCommand):
2141
+ self._on_get_capabilities_command(transaction_label, command)
2142
+ elif isinstance(command, SetAbsoluteVolumeCommand):
2143
+ self._on_set_absolute_volume_command(transaction_label, command)
2144
+ elif isinstance(command, RegisterNotificationCommand):
2145
+ self._on_register_notification_command(transaction_label, command)
1616
2146
  else:
1617
2147
  # Not supported.
1618
2148
  # TODO: check that this is the right way to respond in this case.
1619
2149
  logger.debug("unsupported PDU ID")
1620
2150
  self.send_rejected_avrcp_response(
1621
- transaction_label, pdu_id, self.StatusCode.INVALID_PARAMETER
2151
+ transaction_label, pdu_id, StatusCode.INVALID_PARAMETER
1622
2152
  )
1623
2153
  else:
1624
2154
  logger.debug("unsupported command type")
1625
2155
  self.send_rejected_avrcp_response(
1626
- transaction_label, pdu_id, self.StatusCode.INVALID_COMMAND
2156
+ transaction_label, pdu_id, StatusCode.INVALID_COMMAND
1627
2157
  )
1628
2158
 
1629
2159
  self.receive_command_state = None
@@ -1648,34 +2178,16 @@ class Protocol(utils.EventEmitter):
1648
2178
  # more appropriate.
1649
2179
  response: Optional[Response] = None
1650
2180
  if response_code == avc.ResponseFrame.ResponseCode.REJECTED:
1651
- response = RejectedResponse.from_bytes(pdu_id, pdu)
2181
+ response = RejectedResponse(pdu_id=pdu_id, status_code=StatusCode(pdu[0]))
1652
2182
  elif response_code == avc.ResponseFrame.ResponseCode.NOT_IMPLEMENTED:
1653
- response = NotImplementedResponse.from_bytes(pdu_id, pdu)
2183
+ response = NotImplementedResponse(pdu_id=pdu_id, parameters=pdu)
1654
2184
  elif response_code in (
1655
2185
  avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
1656
2186
  avc.ResponseFrame.ResponseCode.INTERIM,
1657
2187
  avc.ResponseFrame.ResponseCode.CHANGED,
1658
2188
  avc.ResponseFrame.ResponseCode.ACCEPTED,
1659
2189
  ):
1660
- if pdu_id == self.PduId.GET_CAPABILITIES:
1661
- response = GetCapabilitiesResponse.from_bytes(pdu)
1662
- elif pdu_id == self.PduId.GET_PLAY_STATUS:
1663
- response = GetPlayStatusResponse.from_bytes(pdu)
1664
- elif pdu_id == self.PduId.GET_ELEMENT_ATTRIBUTES:
1665
- response = GetElementAttributesResponse.from_bytes(pdu)
1666
- elif pdu_id == self.PduId.SET_ABSOLUTE_VOLUME:
1667
- response = SetAbsoluteVolumeResponse.from_bytes(pdu)
1668
- elif pdu_id == self.PduId.REGISTER_NOTIFICATION:
1669
- response = RegisterNotificationResponse.from_bytes(pdu)
1670
- else:
1671
- logger.debug("unexpected PDU ID")
1672
- pending_command.response.set_exception(
1673
- core.ProtocolError(
1674
- error_code=None,
1675
- error_namespace="avrcp",
1676
- details="unexpected PDU ID",
1677
- )
1678
- )
2190
+ response = Response.from_bytes(pdu=pdu, pdu_id=PduId(pdu_id))
1679
2191
  else:
1680
2192
  logger.debug("unexpected response code")
1681
2193
  pending_command.response.set_exception(
@@ -1762,10 +2274,8 @@ class Protocol(utils.EventEmitter):
1762
2274
  # TODO: fragmentation
1763
2275
  # Send the command.
1764
2276
  logger.debug(f">>> AVRCP command PDU: {command}")
1765
- pdu = (
1766
- struct.pack(">BBH", command.pdu_id, 0, len(command.parameter))
1767
- + command.parameter
1768
- )
2277
+ payload = bytes(command)
2278
+ pdu = struct.pack(">BBH", command.pdu_id, 0, len(payload)) + payload
1769
2279
  command_frame = avc.VendorDependentCommandFrame(
1770
2280
  command_type,
1771
2281
  avc.Frame.SubunitType.PANEL,
@@ -1809,10 +2319,8 @@ class Protocol(utils.EventEmitter):
1809
2319
  ) -> None:
1810
2320
  # TODO: fragmentation
1811
2321
  logger.debug(f">>> AVRCP response PDU: {response}")
1812
- pdu = (
1813
- struct.pack(">BBH", response.pdu_id, 0, len(response.parameter))
1814
- + response.parameter
1815
- )
2322
+ payload = bytes(response)
2323
+ pdu = struct.pack(">BBH", response.pdu_id, 0, len(payload)) + payload
1816
2324
  response_frame = avc.VendorDependentResponseFrame(
1817
2325
  response_code,
1818
2326
  avc.Frame.SubunitType.PANEL,
@@ -1835,7 +2343,7 @@ class Protocol(utils.EventEmitter):
1835
2343
  self.send_response(transaction_label, response)
1836
2344
 
1837
2345
  def send_rejected_avrcp_response(
1838
- self, transaction_label: int, pdu_id: Protocol.PduId, status_code: StatusCode
2346
+ self, transaction_label: int, pdu_id: PduId, status_code: StatusCode
1839
2347
  ) -> None:
1840
2348
  self.send_avrcp_response(
1841
2349
  transaction_label,
@@ -1844,7 +2352,7 @@ class Protocol(utils.EventEmitter):
1844
2352
  )
1845
2353
 
1846
2354
  def send_not_implemented_avrcp_response(
1847
- self, transaction_label: int, pdu_id: Protocol.PduId
2355
+ self, transaction_label: int, pdu_id: PduId
1848
2356
  ) -> None:
1849
2357
  self.send_avrcp_response(
1850
2358
  transaction_label,
@@ -1900,7 +2408,7 @@ class Protocol(utils.EventEmitter):
1900
2408
  if command.event_id not in supported_events:
1901
2409
  logger.debug("event not supported")
1902
2410
  self.send_not_implemented_avrcp_response(
1903
- transaction_label, self.PduId.REGISTER_NOTIFICATION
2411
+ transaction_label, PduId.REGISTER_NOTIFICATION
1904
2412
  )
1905
2413
  return
1906
2414