bumble 0.0.195__py3-none-any.whl → 0.0.199__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +351 -66
  3. bumble/apps/console.py +5 -20
  4. bumble/apps/device_info.py +230 -0
  5. bumble/apps/gatt_dump.py +4 -0
  6. bumble/apps/lea_unicast/app.py +16 -17
  7. bumble/apps/pair.py +32 -5
  8. bumble/at.py +12 -6
  9. bumble/att.py +56 -40
  10. bumble/avc.py +8 -5
  11. bumble/avctp.py +3 -2
  12. bumble/avdtp.py +7 -3
  13. bumble/avrcp.py +2 -1
  14. bumble/codecs.py +17 -13
  15. bumble/colors.py +6 -2
  16. bumble/core.py +37 -7
  17. bumble/decoder.py +14 -10
  18. bumble/device.py +382 -111
  19. bumble/drivers/rtk.py +32 -13
  20. bumble/gatt.py +30 -20
  21. bumble/gatt_client.py +15 -29
  22. bumble/gatt_server.py +14 -6
  23. bumble/hci.py +322 -32
  24. bumble/hid.py +24 -28
  25. bumble/host.py +20 -6
  26. bumble/l2cap.py +24 -17
  27. bumble/link.py +8 -3
  28. bumble/pandora/__init__.py +3 -0
  29. bumble/pandora/l2cap.py +310 -0
  30. bumble/profiles/aics.py +520 -0
  31. bumble/profiles/ascs.py +739 -0
  32. bumble/profiles/asha.py +295 -0
  33. bumble/profiles/bap.py +1 -874
  34. bumble/profiles/bass.py +440 -0
  35. bumble/profiles/csip.py +4 -4
  36. bumble/profiles/gap.py +110 -0
  37. bumble/profiles/hap.py +665 -0
  38. bumble/profiles/heart_rate_service.py +4 -3
  39. bumble/profiles/le_audio.py +43 -9
  40. bumble/profiles/mcp.py +448 -0
  41. bumble/profiles/pacs.py +210 -0
  42. bumble/profiles/tmap.py +89 -0
  43. bumble/profiles/vcp.py +5 -3
  44. bumble/rfcomm.py +4 -2
  45. bumble/sdp.py +13 -11
  46. bumble/smp.py +43 -12
  47. bumble/snoop.py +5 -4
  48. bumble/transport/__init__.py +8 -2
  49. bumble/transport/android_emulator.py +9 -3
  50. bumble/transport/android_netsim.py +9 -7
  51. bumble/transport/common.py +46 -18
  52. bumble/transport/pyusb.py +21 -4
  53. bumble/transport/unix.py +56 -0
  54. bumble/transport/usb.py +57 -46
  55. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/METADATA +41 -41
  56. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/RECORD +60 -49
  57. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/WHEEL +1 -1
  58. bumble/profiles/asha_service.py +0 -193
  59. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/LICENSE +0 -0
  60. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/entry_points.txt +0 -0
  61. {bumble-0.0.195.dist-info → bumble-0.0.199.dist-info}/top_level.txt +0 -0
@@ -17,33 +17,67 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  from __future__ import annotations
19
19
  import dataclasses
20
- from typing import List
20
+ import struct
21
+ from typing import List, Type
21
22
  from typing_extensions import Self
22
23
 
24
+ from bumble import utils
25
+
23
26
 
24
27
  # -----------------------------------------------------------------------------
25
28
  # Classes
26
29
  # -----------------------------------------------------------------------------
27
30
  @dataclasses.dataclass
28
31
  class Metadata:
32
+ '''Bluetooth Assigned Numbers, Section 6.12.6 - Metadata LTV structures.
33
+
34
+ As Metadata fields may extend, and Spec doesn't forbid duplication, we don't parse
35
+ Metadata into a key-value style dataclass here. Rather, we encourage users to parse
36
+ again outside the lib.
37
+ '''
38
+
39
+ class Tag(utils.OpenIntEnum):
40
+ # fmt: off
41
+ PREFERRED_AUDIO_CONTEXTS = 0x01
42
+ STREAMING_AUDIO_CONTEXTS = 0x02
43
+ PROGRAM_INFO = 0x03
44
+ LANGUAGE = 0x04
45
+ CCID_LIST = 0x05
46
+ PARENTAL_RATING = 0x06
47
+ PROGRAM_INFO_URI = 0x07
48
+ AUDIO_ACTIVE_STATE = 0x08
49
+ BROADCAST_AUDIO_IMMEDIATE_RENDERING_FLAG = 0x09
50
+ ASSISTED_LISTENING_STREAM = 0x0A
51
+ BROADCAST_NAME = 0x0B
52
+ EXTENDED_METADATA = 0xFE
53
+ VENDOR_SPECIFIC = 0xFF
54
+
29
55
  @dataclasses.dataclass
30
56
  class Entry:
31
- tag: int
57
+ tag: Metadata.Tag
32
58
  data: bytes
33
59
 
34
- entries: List[Entry]
60
+ @classmethod
61
+ def from_bytes(cls: Type[Self], data: bytes) -> Self:
62
+ return cls(tag=Metadata.Tag(data[0]), data=data[1:])
63
+
64
+ def __bytes__(self) -> bytes:
65
+ return bytes([len(self.data) + 1, self.tag]) + self.data
66
+
67
+ entries: List[Entry] = dataclasses.field(default_factory=list)
35
68
 
36
69
  @classmethod
37
- def from_bytes(cls, data: bytes) -> Self:
70
+ def from_bytes(cls: Type[Self], data: bytes) -> Self:
38
71
  entries = []
39
72
  offset = 0
40
73
  length = len(data)
41
- while length >= 2:
74
+ while offset < length:
42
75
  entry_length = data[offset]
43
- entry_tag = data[offset + 1]
44
- entry_data = data[offset + 2 : offset + 2 + entry_length - 1]
45
- entries.append(cls.Entry(entry_tag, entry_data))
46
- length -= entry_length
76
+ offset += 1
77
+ entries.append(cls.Entry.from_bytes(data[offset : offset + entry_length]))
47
78
  offset += entry_length
48
79
 
49
80
  return cls(entries)
81
+
82
+ def __bytes__(self) -> bytes:
83
+ return b''.join([bytes(entry) for entry in self.entries])
bumble/profiles/mcp.py ADDED
@@ -0,0 +1,448 @@
1
+ # Copyright 2021-2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ # -----------------------------------------------------------------------------
17
+ # Imports
18
+ # -----------------------------------------------------------------------------
19
+ from __future__ import annotations
20
+
21
+ import asyncio
22
+ import dataclasses
23
+ import enum
24
+ import struct
25
+
26
+ from bumble import core
27
+ from bumble import device
28
+ from bumble import gatt
29
+ from bumble import gatt_client
30
+ from bumble import utils
31
+
32
+ from typing import Type, Optional, ClassVar, Dict, TYPE_CHECKING
33
+ from typing_extensions import Self
34
+
35
+ # -----------------------------------------------------------------------------
36
+ # Constants
37
+ # -----------------------------------------------------------------------------
38
+
39
+
40
+ class PlayingOrder(utils.OpenIntEnum):
41
+ '''See Media Control Service 3.15. Playing Order.'''
42
+
43
+ SINGLE_ONCE = 0x01
44
+ SINGLE_REPEAT = 0x02
45
+ IN_ORDER_ONCE = 0x03
46
+ IN_ORDER_REPEAT = 0x04
47
+ OLDEST_ONCE = 0x05
48
+ OLDEST_REPEAT = 0x06
49
+ NEWEST_ONCE = 0x07
50
+ NEWEST_REPEAT = 0x08
51
+ SHUFFLE_ONCE = 0x09
52
+ SHUFFLE_REPEAT = 0x0A
53
+
54
+
55
+ class PlayingOrderSupported(enum.IntFlag):
56
+ '''See Media Control Service 3.16. Playing Orders Supported.'''
57
+
58
+ SINGLE_ONCE = 0x0001
59
+ SINGLE_REPEAT = 0x0002
60
+ IN_ORDER_ONCE = 0x0004
61
+ IN_ORDER_REPEAT = 0x0008
62
+ OLDEST_ONCE = 0x0010
63
+ OLDEST_REPEAT = 0x0020
64
+ NEWEST_ONCE = 0x0040
65
+ NEWEST_REPEAT = 0x0080
66
+ SHUFFLE_ONCE = 0x0100
67
+ SHUFFLE_REPEAT = 0x0200
68
+
69
+
70
+ class MediaState(utils.OpenIntEnum):
71
+ '''See Media Control Service 3.17. Media State.'''
72
+
73
+ INACTIVE = 0x00
74
+ PLAYING = 0x01
75
+ PAUSED = 0x02
76
+ SEEKING = 0x03
77
+
78
+
79
+ class MediaControlPointOpcode(utils.OpenIntEnum):
80
+ '''See Media Control Service 3.18. Media Control Point.'''
81
+
82
+ PLAY = 0x01
83
+ PAUSE = 0x02
84
+ FAST_REWIND = 0x03
85
+ FAST_FORWARD = 0x04
86
+ STOP = 0x05
87
+ MOVE_RELATIVE = 0x10
88
+ PREVIOUS_SEGMENT = 0x20
89
+ NEXT_SEGMENT = 0x21
90
+ FIRST_SEGMENT = 0x22
91
+ LAST_SEGMENT = 0x23
92
+ GOTO_SEGMENT = 0x24
93
+ PREVIOUS_TRACK = 0x30
94
+ NEXT_TRACK = 0x31
95
+ FIRST_TRACK = 0x32
96
+ LAST_TRACK = 0x33
97
+ GOTO_TRACK = 0x34
98
+ PREVIOUS_GROUP = 0x40
99
+ NEXT_GROUP = 0x41
100
+ FIRST_GROUP = 0x42
101
+ LAST_GROUP = 0x43
102
+ GOTO_GROUP = 0x44
103
+
104
+
105
+ class MediaControlPointResultCode(enum.IntFlag):
106
+ '''See Media Control Service 3.18.2. Media Control Point Notification.'''
107
+
108
+ SUCCESS = 0x01
109
+ OPCODE_NOT_SUPPORTED = 0x02
110
+ MEDIA_PLAYER_INACTIVE = 0x03
111
+ COMMAND_CANNOT_BE_COMPLETED = 0x04
112
+
113
+
114
+ class MediaControlPointOpcodeSupported(enum.IntFlag):
115
+ '''See Media Control Service 3.19. Media Control Point Opcodes Supported.'''
116
+
117
+ PLAY = 0x00000001
118
+ PAUSE = 0x00000002
119
+ FAST_REWIND = 0x00000004
120
+ FAST_FORWARD = 0x00000008
121
+ STOP = 0x00000010
122
+ MOVE_RELATIVE = 0x00000020
123
+ PREVIOUS_SEGMENT = 0x00000040
124
+ NEXT_SEGMENT = 0x00000080
125
+ FIRST_SEGMENT = 0x00000100
126
+ LAST_SEGMENT = 0x00000200
127
+ GOTO_SEGMENT = 0x00000400
128
+ PREVIOUS_TRACK = 0x00000800
129
+ NEXT_TRACK = 0x00001000
130
+ FIRST_TRACK = 0x00002000
131
+ LAST_TRACK = 0x00004000
132
+ GOTO_TRACK = 0x00008000
133
+ PREVIOUS_GROUP = 0x00010000
134
+ NEXT_GROUP = 0x00020000
135
+ FIRST_GROUP = 0x00040000
136
+ LAST_GROUP = 0x00080000
137
+ GOTO_GROUP = 0x00100000
138
+
139
+
140
+ class SearchControlPointItemType(utils.OpenIntEnum):
141
+ '''See Media Control Service 3.20. Search Control Point.'''
142
+
143
+ TRACK_NAME = 0x01
144
+ ARTIST_NAME = 0x02
145
+ ALBUM_NAME = 0x03
146
+ GROUP_NAME = 0x04
147
+ EARLIEST_YEAR = 0x05
148
+ LATEST_YEAR = 0x06
149
+ GENRE = 0x07
150
+ ONLY_TRACKS = 0x08
151
+ ONLY_GROUPS = 0x09
152
+
153
+
154
+ class ObjectType(utils.OpenIntEnum):
155
+ '''See Media Control Service 4.4.1. Object Type field.'''
156
+
157
+ TASK = 0
158
+ GROUP = 1
159
+
160
+
161
+ # -----------------------------------------------------------------------------
162
+ # Classes
163
+ # -----------------------------------------------------------------------------
164
+
165
+
166
+ class ObjectId(int):
167
+ '''See Media Control Service 4.4.2. Object ID field.'''
168
+
169
+ @classmethod
170
+ def create_from_bytes(cls: Type[Self], data: bytes) -> Self:
171
+ return cls(int.from_bytes(data, byteorder='little', signed=False))
172
+
173
+ def __bytes__(self) -> bytes:
174
+ return self.to_bytes(6, 'little')
175
+
176
+
177
+ @dataclasses.dataclass
178
+ class GroupObjectType:
179
+ '''See Media Control Service 4.4. Group Object Type.'''
180
+
181
+ object_type: ObjectType
182
+ object_id: ObjectId
183
+
184
+ @classmethod
185
+ def from_bytes(cls: Type[Self], data: bytes) -> Self:
186
+ return cls(
187
+ object_type=ObjectType(data[0]),
188
+ object_id=ObjectId.create_from_bytes(data[1:]),
189
+ )
190
+
191
+ def __bytes__(self) -> bytes:
192
+ return bytes([self.object_type]) + bytes(self.object_id)
193
+
194
+
195
+ # -----------------------------------------------------------------------------
196
+ # Server
197
+ # -----------------------------------------------------------------------------
198
+ class MediaControlService(gatt.TemplateService):
199
+ '''Media Control Service server implementation, only for testing currently.'''
200
+
201
+ UUID = gatt.GATT_MEDIA_CONTROL_SERVICE
202
+
203
+ def __init__(self, media_player_name: Optional[str] = None) -> None:
204
+ self.track_position = 0
205
+
206
+ self.media_player_name_characteristic = gatt.Characteristic(
207
+ uuid=gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
208
+ properties=gatt.Characteristic.Properties.READ
209
+ | gatt.Characteristic.Properties.NOTIFY,
210
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
211
+ value=media_player_name or 'Bumble Player',
212
+ )
213
+ self.track_changed_characteristic = gatt.Characteristic(
214
+ uuid=gatt.GATT_TRACK_CHANGED_CHARACTERISTIC,
215
+ properties=gatt.Characteristic.Properties.NOTIFY,
216
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
217
+ value=b'',
218
+ )
219
+ self.track_title_characteristic = gatt.Characteristic(
220
+ uuid=gatt.GATT_TRACK_TITLE_CHARACTERISTIC,
221
+ properties=gatt.Characteristic.Properties.READ
222
+ | gatt.Characteristic.Properties.NOTIFY,
223
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
224
+ value=b'',
225
+ )
226
+ self.track_duration_characteristic = gatt.Characteristic(
227
+ uuid=gatt.GATT_TRACK_DURATION_CHARACTERISTIC,
228
+ properties=gatt.Characteristic.Properties.READ
229
+ | gatt.Characteristic.Properties.NOTIFY,
230
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
231
+ value=b'',
232
+ )
233
+ self.track_position_characteristic = gatt.Characteristic(
234
+ uuid=gatt.GATT_TRACK_POSITION_CHARACTERISTIC,
235
+ properties=gatt.Characteristic.Properties.READ
236
+ | gatt.Characteristic.Properties.WRITE
237
+ | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
238
+ | gatt.Characteristic.Properties.NOTIFY,
239
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
240
+ | gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
241
+ value=b'',
242
+ )
243
+ self.media_state_characteristic = gatt.Characteristic(
244
+ uuid=gatt.GATT_MEDIA_STATE_CHARACTERISTIC,
245
+ properties=gatt.Characteristic.Properties.READ
246
+ | gatt.Characteristic.Properties.NOTIFY,
247
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
248
+ value=b'',
249
+ )
250
+ self.media_control_point_characteristic = gatt.Characteristic(
251
+ uuid=gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC,
252
+ properties=gatt.Characteristic.Properties.WRITE
253
+ | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
254
+ | gatt.Characteristic.Properties.NOTIFY,
255
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
256
+ | gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
257
+ value=gatt.CharacteristicValue(write=self.on_media_control_point),
258
+ )
259
+ self.media_control_point_opcodes_supported_characteristic = gatt.Characteristic(
260
+ uuid=gatt.GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC,
261
+ properties=gatt.Characteristic.Properties.READ
262
+ | gatt.Characteristic.Properties.NOTIFY,
263
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
264
+ value=b'',
265
+ )
266
+ self.content_control_id_characteristic = gatt.Characteristic(
267
+ uuid=gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
268
+ properties=gatt.Characteristic.Properties.READ,
269
+ permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
270
+ value=b'',
271
+ )
272
+
273
+ super().__init__(
274
+ [
275
+ self.media_player_name_characteristic,
276
+ self.track_changed_characteristic,
277
+ self.track_title_characteristic,
278
+ self.track_duration_characteristic,
279
+ self.track_position_characteristic,
280
+ self.media_state_characteristic,
281
+ self.media_control_point_characteristic,
282
+ self.media_control_point_opcodes_supported_characteristic,
283
+ self.content_control_id_characteristic,
284
+ ]
285
+ )
286
+
287
+ async def on_media_control_point(
288
+ self, connection: Optional[device.Connection], data: bytes
289
+ ) -> None:
290
+ if not connection:
291
+ raise core.InvalidStateError()
292
+
293
+ opcode = MediaControlPointOpcode(data[0])
294
+
295
+ await connection.device.notify_subscriber(
296
+ connection,
297
+ self.media_control_point_characteristic,
298
+ value=bytes([opcode, MediaControlPointResultCode.SUCCESS]),
299
+ )
300
+
301
+
302
+ class GenericMediaControlService(MediaControlService):
303
+ UUID = gatt.GATT_GENERIC_MEDIA_CONTROL_SERVICE
304
+
305
+
306
+ # -----------------------------------------------------------------------------
307
+ # Client
308
+ # -----------------------------------------------------------------------------
309
+ class MediaControlServiceProxy(
310
+ gatt_client.ProfileServiceProxy, utils.CompositeEventEmitter
311
+ ):
312
+ SERVICE_CLASS = MediaControlService
313
+
314
+ _CHARACTERISTICS: ClassVar[Dict[str, core.UUID]] = {
315
+ 'media_player_name': gatt.GATT_MEDIA_PLAYER_NAME_CHARACTERISTIC,
316
+ 'media_player_icon_object_id': gatt.GATT_MEDIA_PLAYER_ICON_OBJECT_ID_CHARACTERISTIC,
317
+ 'media_player_icon_url': gatt.GATT_MEDIA_PLAYER_ICON_URL_CHARACTERISTIC,
318
+ 'track_changed': gatt.GATT_TRACK_CHANGED_CHARACTERISTIC,
319
+ 'track_title': gatt.GATT_TRACK_TITLE_CHARACTERISTIC,
320
+ 'track_duration': gatt.GATT_TRACK_DURATION_CHARACTERISTIC,
321
+ 'track_position': gatt.GATT_TRACK_POSITION_CHARACTERISTIC,
322
+ 'playback_speed': gatt.GATT_PLAYBACK_SPEED_CHARACTERISTIC,
323
+ 'seeking_speed': gatt.GATT_SEEKING_SPEED_CHARACTERISTIC,
324
+ 'current_track_segments_object_id': gatt.GATT_CURRENT_TRACK_SEGMENTS_OBJECT_ID_CHARACTERISTIC,
325
+ 'current_track_object_id': gatt.GATT_CURRENT_TRACK_OBJECT_ID_CHARACTERISTIC,
326
+ 'next_track_object_id': gatt.GATT_NEXT_TRACK_OBJECT_ID_CHARACTERISTIC,
327
+ 'parent_group_object_id': gatt.GATT_PARENT_GROUP_OBJECT_ID_CHARACTERISTIC,
328
+ 'current_group_object_id': gatt.GATT_CURRENT_GROUP_OBJECT_ID_CHARACTERISTIC,
329
+ 'playing_order': gatt.GATT_PLAYING_ORDER_CHARACTERISTIC,
330
+ 'playing_orders_supported': gatt.GATT_PLAYING_ORDERS_SUPPORTED_CHARACTERISTIC,
331
+ 'media_state': gatt.GATT_MEDIA_STATE_CHARACTERISTIC,
332
+ 'media_control_point': gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC,
333
+ 'media_control_point_opcodes_supported': gatt.GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC,
334
+ 'search_control_point': gatt.GATT_SEARCH_CONTROL_POINT_CHARACTERISTIC,
335
+ 'search_results_object_id': gatt.GATT_SEARCH_RESULTS_OBJECT_ID_CHARACTERISTIC,
336
+ 'content_control_id': gatt.GATT_CONTENT_CONTROL_ID_CHARACTERISTIC,
337
+ }
338
+
339
+ media_player_name: Optional[gatt_client.CharacteristicProxy] = None
340
+ media_player_icon_object_id: Optional[gatt_client.CharacteristicProxy] = None
341
+ media_player_icon_url: Optional[gatt_client.CharacteristicProxy] = None
342
+ track_changed: Optional[gatt_client.CharacteristicProxy] = None
343
+ track_title: Optional[gatt_client.CharacteristicProxy] = None
344
+ track_duration: Optional[gatt_client.CharacteristicProxy] = None
345
+ track_position: Optional[gatt_client.CharacteristicProxy] = None
346
+ playback_speed: Optional[gatt_client.CharacteristicProxy] = None
347
+ seeking_speed: Optional[gatt_client.CharacteristicProxy] = None
348
+ current_track_segments_object_id: Optional[gatt_client.CharacteristicProxy] = None
349
+ current_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
350
+ next_track_object_id: Optional[gatt_client.CharacteristicProxy] = None
351
+ parent_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
352
+ current_group_object_id: Optional[gatt_client.CharacteristicProxy] = None
353
+ playing_order: Optional[gatt_client.CharacteristicProxy] = None
354
+ playing_orders_supported: Optional[gatt_client.CharacteristicProxy] = None
355
+ media_state: Optional[gatt_client.CharacteristicProxy] = None
356
+ media_control_point: Optional[gatt_client.CharacteristicProxy] = None
357
+ media_control_point_opcodes_supported: Optional[gatt_client.CharacteristicProxy] = (
358
+ None
359
+ )
360
+ search_control_point: Optional[gatt_client.CharacteristicProxy] = None
361
+ search_results_object_id: Optional[gatt_client.CharacteristicProxy] = None
362
+ content_control_id: Optional[gatt_client.CharacteristicProxy] = None
363
+
364
+ if TYPE_CHECKING:
365
+ media_control_point_notifications: asyncio.Queue[bytes]
366
+
367
+ def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
368
+ utils.CompositeEventEmitter.__init__(self)
369
+ self.service_proxy = service_proxy
370
+ self.lock = asyncio.Lock()
371
+ self.media_control_point_notifications = asyncio.Queue()
372
+
373
+ for field, uuid in self._CHARACTERISTICS.items():
374
+ if characteristics := service_proxy.get_characteristics_by_uuid(uuid):
375
+ setattr(self, field, characteristics[0])
376
+
377
+ async def subscribe_characteristics(self) -> None:
378
+ if self.media_control_point:
379
+ await self.media_control_point.subscribe(self._on_media_control_point)
380
+ if self.media_state:
381
+ await self.media_state.subscribe(self._on_media_state)
382
+ if self.track_changed:
383
+ await self.track_changed.subscribe(self._on_track_changed)
384
+ if self.track_title:
385
+ await self.track_title.subscribe(self._on_track_title)
386
+ if self.track_duration:
387
+ await self.track_duration.subscribe(self._on_track_duration)
388
+ if self.track_position:
389
+ await self.track_position.subscribe(self._on_track_position)
390
+
391
+ async def write_control_point(
392
+ self, opcode: MediaControlPointOpcode
393
+ ) -> MediaControlPointResultCode:
394
+ '''Writes a Media Control Point Opcode to peer and waits for the notification.
395
+
396
+ The write operation will be executed when there isn't other pending commands.
397
+
398
+ Args:
399
+ opcode: opcode defined in `MediaControlPointOpcode`.
400
+
401
+ Returns:
402
+ Response code provided in `MediaControlPointResultCode`
403
+
404
+ Raises:
405
+ InvalidOperationError: Server does not have Media Control Point Characteristic.
406
+ InvalidStateError: Server replies a notification with mismatched opcode.
407
+ '''
408
+ if not self.media_control_point:
409
+ raise core.InvalidOperationError("Peer does not have media control point")
410
+
411
+ async with self.lock:
412
+ await self.media_control_point.write_value(
413
+ bytes([opcode]),
414
+ with_response=False,
415
+ )
416
+
417
+ (
418
+ response_opcode,
419
+ response_code,
420
+ ) = await self.media_control_point_notifications.get()
421
+ if response_opcode != opcode:
422
+ raise core.InvalidStateError(
423
+ f"Expected {opcode} notification, but get {response_opcode}"
424
+ )
425
+ return MediaControlPointResultCode(response_code)
426
+
427
+ def _on_media_control_point(self, data: bytes) -> None:
428
+ self.media_control_point_notifications.put_nowait(data)
429
+
430
+ def _on_media_state(self, data: bytes) -> None:
431
+ self.emit('media_state', MediaState(data[0]))
432
+
433
+ def _on_track_changed(self, data: bytes) -> None:
434
+ del data
435
+ self.emit('track_changed')
436
+
437
+ def _on_track_title(self, data: bytes) -> None:
438
+ self.emit('track_title', data.decode("utf-8"))
439
+
440
+ def _on_track_duration(self, data: bytes) -> None:
441
+ self.emit('track_duration', struct.unpack_from('<i', data)[0])
442
+
443
+ def _on_track_position(self, data: bytes) -> None:
444
+ self.emit('track_position', struct.unpack_from('<i', data)[0])
445
+
446
+
447
+ class GenericMediaControlServiceProxy(MediaControlServiceProxy):
448
+ SERVICE_CLASS = GenericMediaControlService