bumble 0.0.222__py3-none-any.whl → 0.0.224__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 (43) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/controller_info.py +90 -114
  3. bumble/apps/controller_loopback.py +11 -9
  4. bumble/apps/gg_bridge.py +1 -1
  5. bumble/apps/hci_bridge.py +3 -1
  6. bumble/apps/l2cap_bridge.py +1 -1
  7. bumble/apps/rfcomm_bridge.py +1 -1
  8. bumble/apps/scan.py +10 -4
  9. bumble/apps/speaker/speaker.py +1 -1
  10. bumble/apps/usb_probe.py +15 -2
  11. bumble/att.py +97 -32
  12. bumble/avctp.py +1 -1
  13. bumble/avdtp.py +3 -3
  14. bumble/avrcp.py +366 -190
  15. bumble/bridge.py +10 -2
  16. bumble/controller.py +14 -1
  17. bumble/core.py +1 -1
  18. bumble/device.py +999 -577
  19. bumble/drivers/intel.py +45 -39
  20. bumble/drivers/rtk.py +102 -43
  21. bumble/gatt.py +2 -2
  22. bumble/gatt_client.py +5 -4
  23. bumble/gatt_server.py +100 -1
  24. bumble/hci.py +1367 -844
  25. bumble/hid.py +2 -2
  26. bumble/host.py +339 -157
  27. bumble/l2cap.py +13 -6
  28. bumble/pandora/l2cap.py +1 -1
  29. bumble/profiles/battery_service.py +25 -34
  30. bumble/profiles/heart_rate_service.py +130 -121
  31. bumble/rfcomm.py +1 -1
  32. bumble/sdp.py +2 -2
  33. bumble/smp.py +8 -3
  34. bumble/snoop.py +111 -1
  35. bumble/transport/android_netsim.py +1 -1
  36. bumble/vendor/android/hci.py +108 -86
  37. bumble/vendor/zephyr/hci.py +24 -18
  38. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/METADATA +4 -3
  39. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/RECORD +43 -43
  40. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/WHEEL +1 -1
  41. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/entry_points.txt +0 -0
  42. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/licenses/LICENSE +0 -0
  43. {bumble-0.0.222.dist-info → bumble-0.0.224.dist-info}/top_level.txt +0 -0
bumble/avrcp.py CHANGED
@@ -26,7 +26,7 @@ from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequen
26
26
  from dataclasses import dataclass, field
27
27
  from typing import ClassVar, SupportsBytes, TypeVar
28
28
 
29
- from bumble import avc, avctp, core, hci, l2cap, utils
29
+ from bumble import avc, avctp, core, hci, l2cap, sdp, utils
30
30
  from bumble.colors import color
31
31
  from bumble.device import Connection, Device
32
32
  from bumble.sdp import (
@@ -55,13 +55,15 @@ AVRCP_PID = 0x110E
55
55
  AVRCP_BLUETOOTH_SIG_COMPANY_ID = 0x001958
56
56
 
57
57
 
58
- _UINT64_BE_METADATA = {
59
- 'parser': lambda data, offset: (
60
- offset + 8,
61
- int.from_bytes(data[offset : offset + 8], byteorder='big'),
62
- ),
63
- 'serializer': lambda x: x.to_bytes(8, byteorder='big'),
64
- }
58
+ _UINT64_BE_METADATA = hci.metadata(
59
+ {
60
+ 'parser': lambda data, offset: (
61
+ offset + 8,
62
+ int.from_bytes(data[offset : offset + 8], byteorder='big'),
63
+ ),
64
+ 'serializer': lambda x: x.to_bytes(8, byteorder='big'),
65
+ }
66
+ )
65
67
 
66
68
 
67
69
  class PduId(utils.OpenIntEnum):
@@ -92,7 +94,7 @@ class PduId(utils.OpenIntEnum):
92
94
 
93
95
 
94
96
  class CharacterSetId(hci.SpecableEnum):
95
- UTF_8 = 0x06
97
+ UTF_8 = 0x6A
96
98
 
97
99
 
98
100
  class MediaAttributeId(hci.SpecableEnum):
@@ -192,82 +194,43 @@ class TargetFeatures(enum.IntFlag):
192
194
 
193
195
 
194
196
  # -----------------------------------------------------------------------------
195
- def make_controller_service_sdp_records(
196
- service_record_handle: int,
197
- avctp_version: tuple[int, int] = (1, 4),
198
- avrcp_version: tuple[int, int] = (1, 6),
199
- supported_features: int | ControllerFeatures = 1,
200
- ) -> list[ServiceAttribute]:
201
- avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
202
- avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
197
+ @dataclass
198
+ class ControllerServiceSdpRecord:
199
+ service_record_handle: int
200
+ avctp_version: tuple[int, int] = (1, 4)
201
+ avrcp_version: tuple[int, int] = (1, 6)
202
+ supported_features: int | ControllerFeatures = ControllerFeatures(1)
203
203
 
204
- attributes = [
205
- ServiceAttribute(
206
- SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
207
- DataElement.unsigned_integer_32(service_record_handle),
208
- ),
209
- ServiceAttribute(
210
- SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
211
- DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
212
- ),
213
- ServiceAttribute(
214
- SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
215
- DataElement.sequence(
216
- [
217
- DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
218
- DataElement.uuid(core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
219
- ]
204
+ def to_service_attributes(self) -> list[ServiceAttribute]:
205
+ avctp_version_int = self.avctp_version[0] << 8 | self.avctp_version[1]
206
+ avrcp_version_int = self.avrcp_version[0] << 8 | self.avrcp_version[1]
207
+
208
+ attributes = [
209
+ ServiceAttribute(
210
+ SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
211
+ DataElement.unsigned_integer_32(self.service_record_handle),
220
212
  ),
221
- ),
222
- ServiceAttribute(
223
- SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
224
- DataElement.sequence(
225
- [
226
- DataElement.sequence(
227
- [
228
- DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
229
- DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
230
- ]
231
- ),
232
- DataElement.sequence(
233
- [
234
- DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
235
- DataElement.unsigned_integer_16(avctp_version_int),
236
- ]
237
- ),
238
- ]
213
+ ServiceAttribute(
214
+ SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
215
+ DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
239
216
  ),
240
- ),
241
- ServiceAttribute(
242
- SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
243
- DataElement.sequence(
244
- [
245
- DataElement.sequence(
246
- [
247
- DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
248
- DataElement.unsigned_integer_16(avrcp_version_int),
249
- ]
250
- ),
251
- ]
217
+ ServiceAttribute(
218
+ SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
219
+ DataElement.sequence(
220
+ [
221
+ DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
222
+ DataElement.uuid(core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE),
223
+ ]
224
+ ),
252
225
  ),
253
- ),
254
- ServiceAttribute(
255
- SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
256
- DataElement.unsigned_integer_16(supported_features),
257
- ),
258
- ]
259
- if supported_features & ControllerFeatures.SUPPORTS_BROWSING:
260
- attributes.append(
261
226
  ServiceAttribute(
262
- SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
227
+ SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
263
228
  DataElement.sequence(
264
229
  [
265
230
  DataElement.sequence(
266
231
  [
267
232
  DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
268
- DataElement.unsigned_integer_16(
269
- avctp.AVCTP_BROWSING_PSM
270
- ),
233
+ DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
271
234
  ]
272
235
  ),
273
236
  DataElement.sequence(
@@ -279,87 +242,130 @@ def make_controller_service_sdp_records(
279
242
  ]
280
243
  ),
281
244
  ),
282
- )
283
- return attributes
284
-
285
-
286
- # -----------------------------------------------------------------------------
287
- def make_target_service_sdp_records(
288
- service_record_handle: int,
289
- avctp_version: tuple[int, int] = (1, 4),
290
- avrcp_version: tuple[int, int] = (1, 6),
291
- supported_features: int | TargetFeatures = 0x23,
292
- ) -> list[ServiceAttribute]:
293
- # TODO: support a way to compute the supported features from a feature list
294
- avctp_version_int = avctp_version[0] << 8 | avctp_version[1]
295
- avrcp_version_int = avrcp_version[0] << 8 | avrcp_version[1]
296
-
297
- attributes = [
298
- ServiceAttribute(
299
- SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
300
- DataElement.unsigned_integer_32(service_record_handle),
301
- ),
302
- ServiceAttribute(
303
- SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
304
- DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
305
- ),
306
- ServiceAttribute(
307
- SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
308
- DataElement.sequence(
309
- [
310
- DataElement.uuid(core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
311
- ]
245
+ ServiceAttribute(
246
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
247
+ DataElement.sequence(
248
+ [
249
+ DataElement.sequence(
250
+ [
251
+ DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
252
+ DataElement.unsigned_integer_16(avrcp_version_int),
253
+ ]
254
+ ),
255
+ ]
256
+ ),
312
257
  ),
313
- ),
314
- ServiceAttribute(
315
- SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
316
- DataElement.sequence(
317
- [
318
- DataElement.sequence(
319
- [
320
- DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
321
- DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
322
- ]
323
- ),
324
- DataElement.sequence(
325
- [
326
- DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
327
- DataElement.unsigned_integer_16(avctp_version_int),
328
- ]
329
- ),
330
- ]
258
+ ServiceAttribute(
259
+ SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
260
+ DataElement.unsigned_integer_16(self.supported_features),
331
261
  ),
332
- ),
333
- ServiceAttribute(
334
- SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
335
- DataElement.sequence(
336
- [
262
+ ]
263
+ if self.supported_features & ControllerFeatures.SUPPORTS_BROWSING:
264
+ attributes.append(
265
+ ServiceAttribute(
266
+ SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
337
267
  DataElement.sequence(
338
268
  [
339
- DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
340
- DataElement.unsigned_integer_16(avrcp_version_int),
269
+ DataElement.sequence(
270
+ [
271
+ DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
272
+ DataElement.unsigned_integer_16(
273
+ avctp.AVCTP_BROWSING_PSM
274
+ ),
275
+ ]
276
+ ),
277
+ DataElement.sequence(
278
+ [
279
+ DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
280
+ DataElement.unsigned_integer_16(avctp_version_int),
281
+ ]
282
+ ),
341
283
  ]
342
284
  ),
343
- ]
285
+ ),
286
+ )
287
+ return attributes
288
+
289
+ @classmethod
290
+ async def find(cls, connection: Connection) -> list[ControllerServiceSdpRecord]:
291
+ async with sdp.Client(connection) as sdp_client:
292
+ search_result = await sdp_client.search_attributes(
293
+ uuids=[core.BT_AV_REMOTE_CONTROL_CONTROLLER_SERVICE],
294
+ attribute_ids=[
295
+ SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
296
+ SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
297
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
298
+ SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
299
+ ],
300
+ )
301
+
302
+ records: list[ControllerServiceSdpRecord] = []
303
+ for attribute_lists in search_result:
304
+ record = cls(0)
305
+ for attribute in attribute_lists:
306
+ if attribute.id == SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID:
307
+ record.service_record_handle = attribute.value.value
308
+ elif attribute.id == SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
309
+ # [[L2CAP, PSM], [AVCTP, version]]
310
+ record.avctp_version = (
311
+ attribute.value.value[1].value[1].value >> 8,
312
+ attribute.value.value[1].value[1].value & 0xFF,
313
+ )
314
+ elif (
315
+ attribute.id
316
+ == SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
317
+ ):
318
+ # [[AV_REMOTE_CONTROL, version]]
319
+ record.avrcp_version = (
320
+ attribute.value.value[0].value[1].value >> 8,
321
+ attribute.value.value[0].value[1].value & 0xFF,
322
+ )
323
+ elif attribute.id == SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID:
324
+ record.supported_features = ControllerFeatures(
325
+ attribute.value.value
326
+ )
327
+ records.append(record)
328
+ return records
329
+
330
+
331
+ # -----------------------------------------------------------------------------
332
+ @dataclass
333
+ class TargetServiceSdpRecord:
334
+ service_record_handle: int
335
+ avctp_version: tuple[int, int] = (1, 4)
336
+ avrcp_version: tuple[int, int] = (1, 6)
337
+ supported_features: int | TargetFeatures = TargetFeatures(0x23)
338
+
339
+ def to_service_attributes(self) -> list[ServiceAttribute]:
340
+ # TODO: support a way to compute the supported features from a feature list
341
+ avctp_version_int = self.avctp_version[0] << 8 | self.avctp_version[1]
342
+ avrcp_version_int = self.avrcp_version[0] << 8 | self.avrcp_version[1]
343
+
344
+ attributes = [
345
+ ServiceAttribute(
346
+ SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
347
+ DataElement.unsigned_integer_32(self.service_record_handle),
348
+ ),
349
+ ServiceAttribute(
350
+ SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
351
+ DataElement.sequence([DataElement.uuid(SDP_PUBLIC_BROWSE_ROOT)]),
352
+ ),
353
+ ServiceAttribute(
354
+ SDP_SERVICE_CLASS_ID_LIST_ATTRIBUTE_ID,
355
+ DataElement.sequence(
356
+ [
357
+ DataElement.uuid(core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE),
358
+ ]
359
+ ),
344
360
  ),
345
- ),
346
- ServiceAttribute(
347
- SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
348
- DataElement.unsigned_integer_16(supported_features),
349
- ),
350
- ]
351
- if supported_features & TargetFeatures.SUPPORTS_BROWSING:
352
- attributes.append(
353
361
  ServiceAttribute(
354
- SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
362
+ SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
355
363
  DataElement.sequence(
356
364
  [
357
365
  DataElement.sequence(
358
366
  [
359
367
  DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
360
- DataElement.unsigned_integer_16(
361
- avctp.AVCTP_BROWSING_PSM
362
- ),
368
+ DataElement.unsigned_integer_16(avctp.AVCTP_PSM),
363
369
  ]
364
370
  ),
365
371
  DataElement.sequence(
@@ -371,8 +377,90 @@ def make_target_service_sdp_records(
371
377
  ]
372
378
  ),
373
379
  ),
374
- )
375
- return attributes
380
+ ServiceAttribute(
381
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
382
+ DataElement.sequence(
383
+ [
384
+ DataElement.sequence(
385
+ [
386
+ DataElement.uuid(core.BT_AV_REMOTE_CONTROL_SERVICE),
387
+ DataElement.unsigned_integer_16(avrcp_version_int),
388
+ ]
389
+ ),
390
+ ]
391
+ ),
392
+ ),
393
+ ServiceAttribute(
394
+ SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
395
+ DataElement.unsigned_integer_16(self.supported_features),
396
+ ),
397
+ ]
398
+ if self.supported_features & TargetFeatures.SUPPORTS_BROWSING:
399
+ attributes.append(
400
+ ServiceAttribute(
401
+ SDP_ADDITIONAL_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
402
+ DataElement.sequence(
403
+ [
404
+ DataElement.sequence(
405
+ [
406
+ DataElement.uuid(core.BT_L2CAP_PROTOCOL_ID),
407
+ DataElement.unsigned_integer_16(
408
+ avctp.AVCTP_BROWSING_PSM
409
+ ),
410
+ ]
411
+ ),
412
+ DataElement.sequence(
413
+ [
414
+ DataElement.uuid(core.BT_AVCTP_PROTOCOL_ID),
415
+ DataElement.unsigned_integer_16(avctp_version_int),
416
+ ]
417
+ ),
418
+ ]
419
+ ),
420
+ ),
421
+ )
422
+ return attributes
423
+
424
+ @classmethod
425
+ async def find(cls, connection: Connection) -> list[TargetServiceSdpRecord]:
426
+ async with sdp.Client(connection) as sdp_client:
427
+ search_result = await sdp_client.search_attributes(
428
+ uuids=[core.BT_AV_REMOTE_CONTROL_TARGET_SERVICE],
429
+ attribute_ids=[
430
+ SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID,
431
+ SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID,
432
+ SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID,
433
+ SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID,
434
+ ],
435
+ )
436
+
437
+ records: list[TargetServiceSdpRecord] = []
438
+ for attribute_lists in search_result:
439
+ record = cls(0)
440
+ for attribute in attribute_lists:
441
+ if attribute.id == SDP_SERVICE_RECORD_HANDLE_ATTRIBUTE_ID:
442
+ record.service_record_handle = attribute.value.value
443
+ elif attribute.id == SDP_PROTOCOL_DESCRIPTOR_LIST_ATTRIBUTE_ID:
444
+ # [[L2CAP, PSM], [AVCTP, version]]
445
+ record.avctp_version = (
446
+ attribute.value.value[1].value[1].value >> 8,
447
+ attribute.value.value[1].value[1].value & 0xFF,
448
+ )
449
+ elif (
450
+ attribute.id
451
+ == SDP_BLUETOOTH_PROFILE_DESCRIPTOR_LIST_ATTRIBUTE_ID
452
+ ):
453
+ # [[AV_REMOTE_CONTROL, version]]
454
+ record.avrcp_version = (
455
+ attribute.value.value[0].value[1].value >> 8,
456
+ attribute.value.value[0].value[1].value & 0xFF,
457
+ )
458
+ elif attribute.id == SDP_SUPPORTED_FEATURES_ATTRIBUTE_ID:
459
+ record.supported_features = TargetFeatures(
460
+ attribute.value.value
461
+ )
462
+ records.append(record)
463
+ return records
376
464
 
377
465
 
378
466
  # -----------------------------------------------------------------------------
@@ -491,14 +579,12 @@ class BrowseableItem:
491
579
  **hci.HCI_Object.dict_from_bytes(data, offset + 3, subclass.fields)
492
580
  )
493
581
  instance._payload = data[3:]
494
- return offset + length, instance
582
+ return offset + length + 3, instance
495
583
 
496
584
  def __bytes__(self) -> bytes:
497
585
  if self._payload is None:
498
586
  self._payload = hci.HCI_Object.dict_to_bytes(self.__dict__, self.fields)
499
- return (
500
- struct.pack('>BH', self.item_type, len(self._payload) + 3) + self._payload
501
- )
587
+ return struct.pack('>BH', self.item_type, len(self._payload)) + self._payload
502
588
 
503
589
  _Item = TypeVar('_Item', bound='BrowseableItem')
504
590
 
@@ -601,11 +687,11 @@ class MediaPlayerItem(BrowseableItem):
601
687
  metadata=MajorPlayerType.type_metadata(1)
602
688
  )
603
689
  player_sub_type: PlayerSubType = field(
604
- metadata=PlayerSubType.type_metadata(4, byteorder='big')
690
+ metadata=PlayerSubType.type_metadata(4, byteorder='little')
605
691
  )
606
692
  play_status: PlayStatus = field(metadata=PlayStatus.type_metadata(1))
607
693
  feature_bitmask: Features = field(
608
- metadata=Features.type_metadata(16, byteorder='big')
694
+ metadata=Features.type_metadata(16, byteorder='little')
609
695
  )
610
696
  character_set_id: CharacterSetId = field(
611
697
  metadata=CharacterSetId.type_metadata(2, byteorder='big')
@@ -634,7 +720,7 @@ class FolderItem(BrowseableItem):
634
720
 
635
721
  folder_uid: int = field(metadata=_UINT64_BE_METADATA)
636
722
  folder_type: FolderType = field(metadata=FolderType.type_metadata(1))
637
- is_playable: FolderType = field(metadata=Playable.type_metadata(1))
723
+ is_playable: Playable = field(metadata=Playable.type_metadata(1))
638
724
  character_set_id: CharacterSetId = field(
639
725
  metadata=CharacterSetId.type_metadata(2, byteorder='big')
640
726
  )
@@ -876,7 +962,7 @@ class GetPlayStatusCommand(Command):
876
962
  class GetElementAttributesCommand(Command):
877
963
  pdu_id = PduId.GET_ELEMENT_ATTRIBUTES
878
964
 
879
- identifier: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
965
+ identifier: int = field(metadata=_UINT64_BE_METADATA)
880
966
  attribute_ids: Sequence[MediaAttributeId] = field(
881
967
  metadata=MediaAttributeId.type_metadata(
882
968
  4, list_begin=True, list_end=True, byteorder='big'
@@ -951,7 +1037,7 @@ class ChangePathCommand(Command):
951
1037
 
952
1038
  uid_counter: int = field(metadata=hci.metadata('>2'))
953
1039
  direction: Direction = field(metadata=Direction.type_metadata(1))
954
- folder_uid: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
1040
+ folder_uid: int = field(metadata=_UINT64_BE_METADATA)
955
1041
 
956
1042
 
957
1043
  # -----------------------------------------------------------------------------
@@ -961,7 +1047,7 @@ class GetItemAttributesCommand(Command):
961
1047
  pdu_id = PduId.GET_ITEM_ATTRIBUTES
962
1048
 
963
1049
  scope: Scope = field(metadata=Scope.type_metadata(1))
964
- uid: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
1050
+ uid: int = field(metadata=_UINT64_BE_METADATA)
965
1051
  uid_counter: int = field(metadata=hci.metadata('>2'))
966
1052
  start_item: int = field(metadata=hci.metadata('>4'))
967
1053
  end_item: int = field(metadata=hci.metadata('>4'))
@@ -999,7 +1085,7 @@ class PlayItemCommand(Command):
999
1085
  pdu_id = PduId.PLAY_ITEM
1000
1086
 
1001
1087
  scope: Scope = field(metadata=Scope.type_metadata(1))
1002
- uid: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
1088
+ uid: int = field(metadata=_UINT64_BE_METADATA)
1003
1089
  uid_counter: int = field(metadata=hci.metadata('>2'))
1004
1090
 
1005
1091
 
@@ -1010,7 +1096,7 @@ class AddToNowPlayingCommand(Command):
1010
1096
  pdu_id = PduId.ADD_TO_NOW_PLAYING
1011
1097
 
1012
1098
  scope: Scope = field(metadata=Scope.type_metadata(1))
1013
- uid: int = field(metadata=hci.metadata(_UINT64_BE_METADATA))
1099
+ uid: int = field(metadata=_UINT64_BE_METADATA)
1014
1100
  uid_counter: int = field(metadata=hci.metadata('>2'))
1015
1101
 
1016
1102
 
@@ -1204,6 +1290,10 @@ class InformBatteryStatusOfCtResponse(Response):
1204
1290
  @dataclass
1205
1291
  class GetPlayStatusResponse(Response):
1206
1292
  pdu_id = PduId.GET_PLAY_STATUS
1293
+
1294
+ # TG doesn't support Song Length or Position.
1295
+ UNAVAILABLE = 0xFFFFFFFF
1296
+
1207
1297
  song_length: int = field(metadata=hci.metadata(">4"))
1208
1298
  song_position: int = field(metadata=hci.metadata(">4"))
1209
1299
  play_status: PlayStatus = field(metadata=PlayStatus.type_metadata(1))
@@ -1521,16 +1611,33 @@ class Delegate:
1521
1611
  def __init__(self, status_code: StatusCode) -> None:
1522
1612
  self.status_code = status_code
1523
1613
 
1614
+ class AvcError(Exception):
1615
+ """The delegate AVC method failed, with a specified status code."""
1616
+
1617
+ def __init__(self, status_code: avc.ResponseFrame.ResponseCode) -> None:
1618
+ self.status_code = status_code
1619
+
1524
1620
  supported_events: list[EventId]
1621
+ supported_company_ids: list[int]
1525
1622
  volume: int
1623
+ playback_status: PlayStatus
1526
1624
 
1527
- def __init__(self, supported_events: Iterable[EventId] = ()) -> None:
1625
+ def __init__(
1626
+ self,
1627
+ supported_events: Iterable[EventId] = (),
1628
+ supported_company_ids: Iterable[int] = (AVRCP_BLUETOOTH_SIG_COMPANY_ID,),
1629
+ ) -> None:
1630
+ self.supported_company_ids = list(supported_company_ids)
1528
1631
  self.supported_events = list(supported_events)
1529
1632
  self.volume = 0
1633
+ self.playback_status = PlayStatus.STOPPED
1530
1634
 
1531
1635
  async def get_supported_events(self) -> list[EventId]:
1532
1636
  return self.supported_events
1533
1637
 
1638
+ async def get_supported_company_ids(self) -> list[int]:
1639
+ return self.supported_company_ids
1640
+
1534
1641
  async def set_absolute_volume(self, volume: int) -> None:
1535
1642
  """
1536
1643
  Set the absolute volume.
@@ -1543,6 +1650,19 @@ class Delegate:
1543
1650
  async def get_absolute_volume(self) -> int:
1544
1651
  return self.volume
1545
1652
 
1653
+ async def on_key_event(
1654
+ self,
1655
+ key: avc.PassThroughFrame.OperationId,
1656
+ pressed: bool,
1657
+ data: bytes,
1658
+ ) -> None:
1659
+ logger.debug(
1660
+ "@@@ on_key_event: key=%s, pressed=%s, data=%s", key, pressed, data.hex()
1661
+ )
1662
+
1663
+ async def get_playback_status(self) -> PlayStatus:
1664
+ return self.playback_status
1665
+
1546
1666
  # TODO add other delegate methods
1547
1667
 
1548
1668
 
@@ -1756,6 +1876,19 @@ class Protocol(utils.EventEmitter):
1756
1876
  if isinstance(capability, EventId)
1757
1877
  )
1758
1878
 
1879
+ async def get_supported_company_ids(self) -> list[int]:
1880
+ """Get the list of events supported by the connected peer."""
1881
+ response_context = await self.send_avrcp_command(
1882
+ avc.CommandFrame.CommandType.STATUS,
1883
+ GetCapabilitiesCommand(GetCapabilitiesCommand.CapabilityId.COMPANY_ID),
1884
+ )
1885
+ response = self._check_response(response_context, GetCapabilitiesResponse)
1886
+ return list(
1887
+ int.from_bytes(capability, 'big')
1888
+ for capability in response.capabilities
1889
+ if isinstance(capability, bytes)
1890
+ )
1891
+
1759
1892
  async def get_play_status(self) -> SongAndPlayStatus:
1760
1893
  """Get the play status of the connected peer."""
1761
1894
  response_context = await self.send_avrcp_command(
@@ -2052,16 +2185,28 @@ class Protocol(utils.EventEmitter):
2052
2185
  return
2053
2186
 
2054
2187
  if isinstance(command, avc.PassThroughCommandFrame):
2055
- # TODO: delegate
2056
- response = avc.PassThroughResponseFrame(
2057
- avc.ResponseFrame.ResponseCode.ACCEPTED,
2058
- command.subunit_type,
2059
- command.subunit_id,
2060
- command.state_flag,
2061
- command.operation_id,
2062
- command.operation_data,
2063
- )
2064
- self.send_response(transaction_label, response)
2188
+
2189
+ async def dispatch_key_event() -> None:
2190
+ try:
2191
+ await self.delegate.on_key_event(
2192
+ command.operation_id,
2193
+ command.state_flag == avc.PassThroughFrame.StateFlag.PRESSED,
2194
+ command.operation_data,
2195
+ )
2196
+ response_code = avc.ResponseFrame.ResponseCode.ACCEPTED
2197
+ except Delegate.AvcError as error:
2198
+ logger.exception("delegate method raised exception")
2199
+ response_code = error.status_code
2200
+ except Exception:
2201
+ logger.exception("delegate method raised exception")
2202
+ response_code = avc.ResponseFrame.ResponseCode.REJECTED
2203
+ self.send_passthrough_response(
2204
+ transaction_label=transaction_label,
2205
+ command=command,
2206
+ response_code=response_code,
2207
+ )
2208
+
2209
+ utils.AsyncRunner.spawn(dispatch_key_event())
2065
2210
  return
2066
2211
 
2067
2212
  # TODO handle other types
@@ -2141,6 +2286,8 @@ class Protocol(utils.EventEmitter):
2141
2286
  self._on_set_absolute_volume_command(transaction_label, command)
2142
2287
  elif isinstance(command, RegisterNotificationCommand):
2143
2288
  self._on_register_notification_command(transaction_label, command)
2289
+ elif isinstance(command, GetPlayStatusCommand):
2290
+ self._on_get_play_status_command(transaction_label, command)
2144
2291
  else:
2145
2292
  # Not supported.
2146
2293
  # TODO: check that this is the right way to respond in this case.
@@ -2364,17 +2511,27 @@ class Protocol(utils.EventEmitter):
2364
2511
  logger.debug(f"<<< AVRCP command PDU: {command}")
2365
2512
 
2366
2513
  async def get_supported_events() -> None:
2514
+ capabilities: Sequence[bytes | SupportsBytes]
2367
2515
  if (
2368
2516
  command.capability_id
2369
- != GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
2517
+ == GetCapabilitiesCommand.CapabilityId.EVENTS_SUPPORTED
2370
2518
  ):
2371
- raise core.InvalidArgumentError()
2372
-
2373
- supported_events = await self.delegate.get_supported_events()
2519
+ capabilities = await self.delegate.get_supported_events()
2520
+ elif (
2521
+ command.capability_id == GetCapabilitiesCommand.CapabilityId.COMPANY_ID
2522
+ ):
2523
+ company_ids = await self.delegate.get_supported_company_ids()
2524
+ capabilities = [
2525
+ company_id.to_bytes(3, 'big') for company_id in company_ids
2526
+ ]
2527
+ else:
2528
+ raise core.InvalidArgumentError(
2529
+ f"Unsupported capability: {command.capability_id}"
2530
+ )
2374
2531
  self.send_avrcp_response(
2375
2532
  transaction_label,
2376
2533
  avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
2377
- GetCapabilitiesResponse(command.capability_id, supported_events),
2534
+ GetCapabilitiesResponse(command.capability_id, capabilities),
2378
2535
  )
2379
2536
 
2380
2537
  self._delegate_command(transaction_label, command, get_supported_events())
@@ -2395,6 +2552,26 @@ class Protocol(utils.EventEmitter):
2395
2552
 
2396
2553
  self._delegate_command(transaction_label, command, set_absolute_volume())
2397
2554
 
2555
+ def _on_get_play_status_command(
2556
+ self, transaction_label: int, command: GetPlayStatusCommand
2557
+ ) -> None:
2558
+ logger.debug("<<< AVRCP command PDU: %s", command)
2559
+
2560
+ async def get_playback_status() -> None:
2561
+ play_status: PlayStatus = await self.delegate.get_playback_status()
2562
+ self.send_avrcp_response(
2563
+ transaction_label,
2564
+ avc.ResponseFrame.ResponseCode.IMPLEMENTED_OR_STABLE,
2565
+ GetPlayStatusResponse(
2566
+ # TODO: Delegate this.
2567
+ song_length=GetPlayStatusResponse.UNAVAILABLE,
2568
+ song_position=GetPlayStatusResponse.UNAVAILABLE,
2569
+ play_status=play_status,
2570
+ ),
2571
+ )
2572
+
2573
+ self._delegate_command(transaction_label, command, get_playback_status())
2574
+
2398
2575
  def _on_register_notification_command(
2399
2576
  self, transaction_label: int, command: RegisterNotificationCommand
2400
2577
  ) -> None:
@@ -2410,28 +2587,27 @@ class Protocol(utils.EventEmitter):
2410
2587
  )
2411
2588
  return
2412
2589
 
2590
+ response: Response
2413
2591
  if command.event_id == EventId.VOLUME_CHANGED:
2414
2592
  volume = await self.delegate.get_absolute_volume()
2415
2593
  response = RegisterNotificationResponse(VolumeChangedEvent(volume))
2416
- self.send_avrcp_response(
2417
- transaction_label,
2418
- avc.ResponseFrame.ResponseCode.INTERIM,
2419
- response,
2420
- )
2421
- self._register_notification_listener(transaction_label, command)
2422
- return
2423
-
2424
- if command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
2425
- # TODO: testing only, use delegate
2594
+ elif command.event_id == EventId.PLAYBACK_STATUS_CHANGED:
2595
+ playback_status = await self.delegate.get_playback_status()
2426
2596
  response = RegisterNotificationResponse(
2427
- PlaybackStatusChangedEvent(play_status=PlayStatus.PLAYING)
2428
- )
2429
- self.send_avrcp_response(
2430
- transaction_label,
2431
- avc.ResponseFrame.ResponseCode.INTERIM,
2432
- response,
2597
+ PlaybackStatusChangedEvent(play_status=playback_status)
2433
2598
  )
2434
- self._register_notification_listener(transaction_label, command)
2599
+ elif command.event_id == EventId.NOW_PLAYING_CONTENT_CHANGED:
2600
+ playback_status = await self.delegate.get_playback_status()
2601
+ response = RegisterNotificationResponse(NowPlayingContentChangedEvent())
2602
+ else:
2603
+ logger.warning("Event supported but not handled %s", command.event_id)
2435
2604
  return
2436
2605
 
2606
+ self.send_avrcp_response(
2607
+ transaction_label,
2608
+ avc.ResponseFrame.ResponseCode.INTERIM,
2609
+ response,
2610
+ )
2611
+ self._register_notification_listener(transaction_label, command)
2612
+
2437
2613
  self._delegate_command(transaction_label, command, register_notification())