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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) 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/at.py +12 -6
  8. bumble/avc.py +8 -5
  9. bumble/avctp.py +3 -2
  10. bumble/avdtp.py +5 -1
  11. bumble/avrcp.py +2 -1
  12. bumble/codecs.py +17 -13
  13. bumble/colors.py +6 -2
  14. bumble/core.py +37 -7
  15. bumble/device.py +382 -111
  16. bumble/drivers/rtk.py +13 -8
  17. bumble/gatt.py +6 -1
  18. bumble/gatt_client.py +10 -4
  19. bumble/hci.py +50 -25
  20. bumble/hid.py +24 -28
  21. bumble/host.py +4 -0
  22. bumble/l2cap.py +24 -17
  23. bumble/link.py +8 -3
  24. bumble/profiles/ascs.py +739 -0
  25. bumble/profiles/bap.py +1 -874
  26. bumble/profiles/bass.py +440 -0
  27. bumble/profiles/csip.py +4 -4
  28. bumble/profiles/gap.py +110 -0
  29. bumble/profiles/heart_rate_service.py +4 -3
  30. bumble/profiles/le_audio.py +43 -9
  31. bumble/profiles/mcp.py +448 -0
  32. bumble/profiles/pacs.py +210 -0
  33. bumble/profiles/tmap.py +89 -0
  34. bumble/rfcomm.py +4 -2
  35. bumble/sdp.py +13 -11
  36. bumble/smp.py +20 -8
  37. bumble/snoop.py +5 -4
  38. bumble/transport/__init__.py +8 -2
  39. bumble/transport/android_emulator.py +9 -3
  40. bumble/transport/android_netsim.py +9 -7
  41. bumble/transport/common.py +46 -18
  42. bumble/transport/pyusb.py +2 -2
  43. bumble/transport/unix.py +56 -0
  44. bumble/transport/usb.py +57 -46
  45. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/METADATA +41 -41
  46. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/RECORD +50 -42
  47. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/LICENSE +0 -0
  49. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/entry_points.txt +0 -0
  50. {bumble-0.0.195.dist-info → bumble-0.0.198.dist-info}/top_level.txt +0 -0
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
@@ -0,0 +1,210 @@
1
+ # Copyright 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
13
+
14
+ """LE Audio - Published Audio Capabilities Service"""
15
+
16
+ # -----------------------------------------------------------------------------
17
+ # Imports
18
+ # -----------------------------------------------------------------------------
19
+ from __future__ import annotations
20
+ import dataclasses
21
+ import logging
22
+ import struct
23
+ from typing import Optional, Sequence, Union
24
+
25
+ from bumble.profiles.bap import AudioLocation, CodecSpecificCapabilities, ContextType
26
+ from bumble.profiles import le_audio
27
+ from bumble import gatt
28
+ from bumble import gatt_client
29
+ from bumble import hci
30
+
31
+
32
+ # -----------------------------------------------------------------------------
33
+ # Logging
34
+ # -----------------------------------------------------------------------------
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ # -----------------------------------------------------------------------------
39
+ @dataclasses.dataclass
40
+ class PacRecord:
41
+ '''Published Audio Capabilities Service, Table 3.2/3.4.'''
42
+
43
+ coding_format: hci.CodingFormat
44
+ codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
45
+ metadata: le_audio.Metadata = dataclasses.field(default_factory=le_audio.Metadata)
46
+
47
+ @classmethod
48
+ def from_bytes(cls, data: bytes) -> PacRecord:
49
+ offset, coding_format = hci.CodingFormat.parse_from_bytes(data, 0)
50
+ codec_specific_capabilities_size = data[offset]
51
+
52
+ offset += 1
53
+ codec_specific_capabilities_bytes = data[
54
+ offset : offset + codec_specific_capabilities_size
55
+ ]
56
+ offset += codec_specific_capabilities_size
57
+ metadata_size = data[offset]
58
+ offset += 1
59
+ metadata = le_audio.Metadata.from_bytes(data[offset : offset + metadata_size])
60
+
61
+ codec_specific_capabilities: Union[CodecSpecificCapabilities, bytes]
62
+ if coding_format.codec_id == hci.CodecID.VENDOR_SPECIFIC:
63
+ codec_specific_capabilities = codec_specific_capabilities_bytes
64
+ else:
65
+ codec_specific_capabilities = CodecSpecificCapabilities.from_bytes(
66
+ codec_specific_capabilities_bytes
67
+ )
68
+
69
+ return PacRecord(
70
+ coding_format=coding_format,
71
+ codec_specific_capabilities=codec_specific_capabilities,
72
+ metadata=metadata,
73
+ )
74
+
75
+ def __bytes__(self) -> bytes:
76
+ capabilities_bytes = bytes(self.codec_specific_capabilities)
77
+ metadata_bytes = bytes(self.metadata)
78
+ return (
79
+ bytes(self.coding_format)
80
+ + bytes([len(capabilities_bytes)])
81
+ + capabilities_bytes
82
+ + bytes([len(metadata_bytes)])
83
+ + metadata_bytes
84
+ )
85
+
86
+
87
+ # -----------------------------------------------------------------------------
88
+ # Server
89
+ # -----------------------------------------------------------------------------
90
+ class PublishedAudioCapabilitiesService(gatt.TemplateService):
91
+ UUID = gatt.GATT_PUBLISHED_AUDIO_CAPABILITIES_SERVICE
92
+
93
+ sink_pac: Optional[gatt.Characteristic]
94
+ sink_audio_locations: Optional[gatt.Characteristic]
95
+ source_pac: Optional[gatt.Characteristic]
96
+ source_audio_locations: Optional[gatt.Characteristic]
97
+ available_audio_contexts: gatt.Characteristic
98
+ supported_audio_contexts: gatt.Characteristic
99
+
100
+ def __init__(
101
+ self,
102
+ supported_source_context: ContextType,
103
+ supported_sink_context: ContextType,
104
+ available_source_context: ContextType,
105
+ available_sink_context: ContextType,
106
+ sink_pac: Sequence[PacRecord] = (),
107
+ sink_audio_locations: Optional[AudioLocation] = None,
108
+ source_pac: Sequence[PacRecord] = (),
109
+ source_audio_locations: Optional[AudioLocation] = None,
110
+ ) -> None:
111
+ characteristics = []
112
+
113
+ self.supported_audio_contexts = gatt.Characteristic(
114
+ uuid=gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC,
115
+ properties=gatt.Characteristic.Properties.READ,
116
+ permissions=gatt.Characteristic.Permissions.READABLE,
117
+ value=struct.pack('<HH', supported_sink_context, supported_source_context),
118
+ )
119
+ characteristics.append(self.supported_audio_contexts)
120
+
121
+ self.available_audio_contexts = gatt.Characteristic(
122
+ uuid=gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC,
123
+ properties=gatt.Characteristic.Properties.READ
124
+ | gatt.Characteristic.Properties.NOTIFY,
125
+ permissions=gatt.Characteristic.Permissions.READABLE,
126
+ value=struct.pack('<HH', available_sink_context, available_source_context),
127
+ )
128
+ characteristics.append(self.available_audio_contexts)
129
+
130
+ if sink_pac:
131
+ self.sink_pac = gatt.Characteristic(
132
+ uuid=gatt.GATT_SINK_PAC_CHARACTERISTIC,
133
+ properties=gatt.Characteristic.Properties.READ,
134
+ permissions=gatt.Characteristic.Permissions.READABLE,
135
+ value=bytes([len(sink_pac)]) + b''.join(map(bytes, sink_pac)),
136
+ )
137
+ characteristics.append(self.sink_pac)
138
+
139
+ if sink_audio_locations is not None:
140
+ self.sink_audio_locations = gatt.Characteristic(
141
+ uuid=gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC,
142
+ properties=gatt.Characteristic.Properties.READ,
143
+ permissions=gatt.Characteristic.Permissions.READABLE,
144
+ value=struct.pack('<I', sink_audio_locations),
145
+ )
146
+ characteristics.append(self.sink_audio_locations)
147
+
148
+ if source_pac:
149
+ self.source_pac = gatt.Characteristic(
150
+ uuid=gatt.GATT_SOURCE_PAC_CHARACTERISTIC,
151
+ properties=gatt.Characteristic.Properties.READ,
152
+ permissions=gatt.Characteristic.Permissions.READABLE,
153
+ value=bytes([len(source_pac)]) + b''.join(map(bytes, source_pac)),
154
+ )
155
+ characteristics.append(self.source_pac)
156
+
157
+ if source_audio_locations is not None:
158
+ self.source_audio_locations = gatt.Characteristic(
159
+ uuid=gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC,
160
+ properties=gatt.Characteristic.Properties.READ,
161
+ permissions=gatt.Characteristic.Permissions.READABLE,
162
+ value=struct.pack('<I', source_audio_locations),
163
+ )
164
+ characteristics.append(self.source_audio_locations)
165
+
166
+ super().__init__(characteristics)
167
+
168
+
169
+ # -----------------------------------------------------------------------------
170
+ # Client
171
+ # -----------------------------------------------------------------------------
172
+ class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
173
+ SERVICE_CLASS = PublishedAudioCapabilitiesService
174
+
175
+ sink_pac: Optional[gatt_client.CharacteristicProxy] = None
176
+ sink_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
177
+ source_pac: Optional[gatt_client.CharacteristicProxy] = None
178
+ source_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
179
+ available_audio_contexts: gatt_client.CharacteristicProxy
180
+ supported_audio_contexts: gatt_client.CharacteristicProxy
181
+
182
+ def __init__(self, service_proxy: gatt_client.ServiceProxy):
183
+ self.service_proxy = service_proxy
184
+
185
+ self.available_audio_contexts = service_proxy.get_characteristics_by_uuid(
186
+ gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
187
+ )[0]
188
+ self.supported_audio_contexts = service_proxy.get_characteristics_by_uuid(
189
+ gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
190
+ )[0]
191
+
192
+ if characteristics := service_proxy.get_characteristics_by_uuid(
193
+ gatt.GATT_SINK_PAC_CHARACTERISTIC
194
+ ):
195
+ self.sink_pac = characteristics[0]
196
+
197
+ if characteristics := service_proxy.get_characteristics_by_uuid(
198
+ gatt.GATT_SOURCE_PAC_CHARACTERISTIC
199
+ ):
200
+ self.source_pac = characteristics[0]
201
+
202
+ if characteristics := service_proxy.get_characteristics_by_uuid(
203
+ gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
204
+ ):
205
+ self.sink_audio_locations = characteristics[0]
206
+
207
+ if characteristics := service_proxy.get_characteristics_by_uuid(
208
+ gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
209
+ ):
210
+ self.source_audio_locations = characteristics[0]