bumble 0.0.204__py3-none-any.whl → 0.0.207__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.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +626 -87
- bumble/apps/bench.py +225 -147
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +4 -1
- bumble/device.py +993 -48
- bumble/gatt.py +35 -6
- bumble/gatt_client.py +14 -2
- bumble/hci.py +812 -14
- bumble/host.py +359 -63
- bumble/l2cap.py +3 -16
- bumble/profiles/aics.py +19 -38
- bumble/profiles/ascs.py +6 -18
- bumble/profiles/asha.py +5 -5
- bumble/profiles/bass.py +10 -19
- bumble/profiles/gatt_service.py +166 -0
- bumble/profiles/gmap.py +193 -0
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/pacs.py +48 -16
- bumble/profiles/tmap.py +3 -9
- bumble/profiles/{vcp.py → vcs.py} +33 -28
- bumble/profiles/vocs.py +54 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +2 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/RECORD +37 -34
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.207.dist-info}/top_level.txt +0 -0
bumble/profiles/aics.py
CHANGED
|
@@ -451,54 +451,35 @@ class AICSServiceProxy(ProfileServiceProxy):
|
|
|
451
451
|
def __init__(self, service_proxy: ServiceProxy) -> None:
|
|
452
452
|
self.service_proxy = service_proxy
|
|
453
453
|
|
|
454
|
-
if not (
|
|
455
|
-
characteristics := service_proxy.get_characteristics_by_uuid(
|
|
456
|
-
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC
|
|
457
|
-
)
|
|
458
|
-
):
|
|
459
|
-
raise gatt.InvalidServiceError("Audio Input State Characteristic not found")
|
|
460
454
|
self.audio_input_state = SerializableCharacteristicAdapter(
|
|
461
|
-
|
|
455
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
456
|
+
GATT_AUDIO_INPUT_STATE_CHARACTERISTIC
|
|
457
|
+
),
|
|
458
|
+
AudioInputState,
|
|
462
459
|
)
|
|
463
460
|
|
|
464
|
-
if not (
|
|
465
|
-
characteristics := service_proxy.get_characteristics_by_uuid(
|
|
466
|
-
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC
|
|
467
|
-
)
|
|
468
|
-
):
|
|
469
|
-
raise gatt.InvalidServiceError(
|
|
470
|
-
"Gain Settings Attribute Characteristic not found"
|
|
471
|
-
)
|
|
472
461
|
self.gain_settings_properties = SerializableCharacteristicAdapter(
|
|
473
|
-
|
|
462
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
463
|
+
GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC
|
|
464
|
+
),
|
|
465
|
+
GainSettingsProperties,
|
|
474
466
|
)
|
|
475
467
|
|
|
476
|
-
|
|
477
|
-
|
|
468
|
+
self.audio_input_status = PackedCharacteristicAdapter(
|
|
469
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
478
470
|
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
"Audio Input Status Characteristic not found"
|
|
483
|
-
)
|
|
484
|
-
self.audio_input_status = PackedCharacteristicAdapter(characteristics[0], 'B')
|
|
471
|
+
),
|
|
472
|
+
'B',
|
|
473
|
+
)
|
|
485
474
|
|
|
486
|
-
|
|
487
|
-
|
|
475
|
+
self.audio_input_control_point = (
|
|
476
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
488
477
|
GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC
|
|
489
478
|
)
|
|
490
|
-
)
|
|
491
|
-
raise gatt.InvalidServiceError(
|
|
492
|
-
"Audio Input Control Point Characteristic not found"
|
|
493
|
-
)
|
|
494
|
-
self.audio_input_control_point = characteristics[0]
|
|
479
|
+
)
|
|
495
480
|
|
|
496
|
-
|
|
497
|
-
|
|
481
|
+
self.audio_input_description = UTF8CharacteristicAdapter(
|
|
482
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
498
483
|
GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC
|
|
499
484
|
)
|
|
500
|
-
)
|
|
501
|
-
raise gatt.InvalidServiceError(
|
|
502
|
-
"Audio Input Description Characteristic not found"
|
|
503
|
-
)
|
|
504
|
-
self.audio_input_description = UTF8CharacteristicAdapter(characteristics[0])
|
|
485
|
+
)
|
bumble/profiles/ascs.py
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
# Imports
|
|
18
18
|
# -----------------------------------------------------------------------------
|
|
19
19
|
from __future__ import annotations
|
|
20
|
+
|
|
20
21
|
import enum
|
|
21
22
|
import logging
|
|
22
23
|
import struct
|
|
@@ -258,8 +259,8 @@ class AseReasonCode(enum.IntEnum):
|
|
|
258
259
|
|
|
259
260
|
# -----------------------------------------------------------------------------
|
|
260
261
|
class AudioRole(enum.IntEnum):
|
|
261
|
-
SINK =
|
|
262
|
-
SOURCE =
|
|
262
|
+
SINK = device.CisLink.Direction.CONTROLLER_TO_HOST
|
|
263
|
+
SOURCE = device.CisLink.Direction.HOST_TO_CONTROLLER
|
|
263
264
|
|
|
264
265
|
|
|
265
266
|
# -----------------------------------------------------------------------------
|
|
@@ -354,16 +355,7 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
354
355
|
cis_link.on('disconnection', self.on_cis_disconnection)
|
|
355
356
|
|
|
356
357
|
async def post_cis_established():
|
|
357
|
-
await self.
|
|
358
|
-
hci.HCI_LE_Setup_ISO_Data_Path_Command(
|
|
359
|
-
connection_handle=cis_link.handle,
|
|
360
|
-
data_path_direction=self.role,
|
|
361
|
-
data_path_id=0x00, # Fixed HCI
|
|
362
|
-
codec_id=hci.CodingFormat(hci.CodecID.TRANSPARENT),
|
|
363
|
-
controller_delay=0,
|
|
364
|
-
codec_configuration=b'',
|
|
365
|
-
)
|
|
366
|
-
)
|
|
358
|
+
await cis_link.setup_data_path(direction=self.role)
|
|
367
359
|
if self.role == AudioRole.SINK:
|
|
368
360
|
self.state = self.State.STREAMING
|
|
369
361
|
await self.service.device.notify_subscribers(self, self.value)
|
|
@@ -511,12 +503,8 @@ class AseStateMachine(gatt.Characteristic):
|
|
|
511
503
|
self.state = self.State.RELEASING
|
|
512
504
|
|
|
513
505
|
async def remove_cis_async():
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
connection_handle=self.cis_link.handle,
|
|
517
|
-
data_path_direction=self.role,
|
|
518
|
-
)
|
|
519
|
-
)
|
|
506
|
+
if self.cis_link:
|
|
507
|
+
await self.cis_link.remove_data_path(self.role)
|
|
520
508
|
self.state = self.State.IDLE
|
|
521
509
|
await self.service.device.notify_subscribers(self, self.value)
|
|
522
510
|
|
bumble/profiles/asha.py
CHANGED
|
@@ -288,8 +288,8 @@ class AshaServiceProxy(gatt_client.ProfileServiceProxy):
|
|
|
288
288
|
'psm_characteristic',
|
|
289
289
|
),
|
|
290
290
|
):
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
291
|
+
setattr(
|
|
292
|
+
self,
|
|
293
|
+
attribute_name,
|
|
294
|
+
self.service_proxy.get_required_characteristic_by_uuid(uuid),
|
|
295
|
+
)
|
bumble/profiles/bass.py
CHANGED
|
@@ -354,34 +354,25 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
|
|
|
354
354
|
SERVICE_CLASS = BroadcastAudioScanService
|
|
355
355
|
|
|
356
356
|
broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
|
|
357
|
-
broadcast_receive_states: List[gatt.
|
|
357
|
+
broadcast_receive_states: List[gatt.DelegatedCharacteristicAdapter]
|
|
358
358
|
|
|
359
359
|
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
|
360
360
|
self.service_proxy = service_proxy
|
|
361
361
|
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
self.broadcast_audio_scan_control_point = (
|
|
363
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
364
364
|
gatt.GATT_BROADCAST_AUDIO_SCAN_CONTROL_POINT_CHARACTERISTIC
|
|
365
365
|
)
|
|
366
|
-
)
|
|
367
|
-
raise gatt.InvalidServiceError(
|
|
368
|
-
"Broadcast Audio Scan Control Point characteristic not found"
|
|
369
|
-
)
|
|
370
|
-
self.broadcast_audio_scan_control_point = characteristics[0]
|
|
366
|
+
)
|
|
371
367
|
|
|
372
|
-
if not (
|
|
373
|
-
characteristics := service_proxy.get_characteristics_by_uuid(
|
|
374
|
-
gatt.GATT_BROADCAST_RECEIVE_STATE_CHARACTERISTIC
|
|
375
|
-
)
|
|
376
|
-
):
|
|
377
|
-
raise gatt.InvalidServiceError(
|
|
378
|
-
"Broadcast Receive State characteristic not found"
|
|
379
|
-
)
|
|
380
368
|
self.broadcast_receive_states = [
|
|
381
|
-
gatt.
|
|
382
|
-
characteristic,
|
|
369
|
+
gatt.DelegatedCharacteristicAdapter(
|
|
370
|
+
characteristic,
|
|
371
|
+
decode=lambda x: BroadcastReceiveState.from_bytes(x) if x else None,
|
|
372
|
+
)
|
|
373
|
+
for characteristic in service_proxy.get_characteristics_by_uuid(
|
|
374
|
+
gatt.GATT_BROADCAST_RECEIVE_STATE_CHARACTERISTIC
|
|
383
375
|
)
|
|
384
|
-
for characteristic in characteristics
|
|
385
376
|
]
|
|
386
377
|
|
|
387
378
|
async def send_control_point_operation(
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Copyright 2021-2025 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
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import struct
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from bumble import att
|
|
21
|
+
from bumble import gatt
|
|
22
|
+
from bumble import gatt_client
|
|
23
|
+
from bumble import crypto
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from bumble import device
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# -----------------------------------------------------------------------------
|
|
30
|
+
class GenericAttributeProfileService(gatt.TemplateService):
|
|
31
|
+
'''See Vol 3, Part G - 7 - DEFINED GENERIC ATTRIBUTE PROFILE SERVICE.'''
|
|
32
|
+
|
|
33
|
+
UUID = gatt.GATT_GENERIC_ATTRIBUTE_SERVICE
|
|
34
|
+
|
|
35
|
+
client_supported_features_characteristic: gatt.Characteristic | None = None
|
|
36
|
+
server_supported_features_characteristic: gatt.Characteristic | None = None
|
|
37
|
+
database_hash_characteristic: gatt.Characteristic | None = None
|
|
38
|
+
service_changed_characteristic: gatt.Characteristic | None = None
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
server_supported_features: gatt.ServerSupportedFeatures | None = None,
|
|
43
|
+
database_hash_enabled: bool = True,
|
|
44
|
+
service_change_enabled: bool = True,
|
|
45
|
+
) -> None:
|
|
46
|
+
|
|
47
|
+
if server_supported_features is not None:
|
|
48
|
+
self.server_supported_features_characteristic = gatt.Characteristic(
|
|
49
|
+
uuid=gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC,
|
|
50
|
+
properties=gatt.Characteristic.Properties.READ,
|
|
51
|
+
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
52
|
+
value=bytes([server_supported_features]),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if database_hash_enabled:
|
|
56
|
+
self.database_hash_characteristic = gatt.Characteristic(
|
|
57
|
+
uuid=gatt.GATT_DATABASE_HASH_CHARACTERISTIC,
|
|
58
|
+
properties=gatt.Characteristic.Properties.READ,
|
|
59
|
+
permissions=gatt.Characteristic.Permissions.READABLE,
|
|
60
|
+
value=gatt.CharacteristicValue(read=self.get_database_hash),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if service_change_enabled:
|
|
64
|
+
self.service_changed_characteristic = gatt.Characteristic(
|
|
65
|
+
uuid=gatt.GATT_SERVICE_CHANGED_CHARACTERISTIC,
|
|
66
|
+
properties=gatt.Characteristic.Properties.INDICATE,
|
|
67
|
+
permissions=gatt.Characteristic.Permissions(0),
|
|
68
|
+
value=b'',
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (database_hash_enabled and service_change_enabled) or (
|
|
72
|
+
server_supported_features
|
|
73
|
+
and (
|
|
74
|
+
server_supported_features & gatt.ServerSupportedFeatures.EATT_SUPPORTED
|
|
75
|
+
)
|
|
76
|
+
): # TODO: Support Multiple Handle Value Notifications
|
|
77
|
+
self.client_supported_features_characteristic = gatt.Characteristic(
|
|
78
|
+
uuid=gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC,
|
|
79
|
+
properties=(
|
|
80
|
+
gatt.Characteristic.Properties.READ
|
|
81
|
+
| gatt.Characteristic.Properties.WRITE
|
|
82
|
+
),
|
|
83
|
+
permissions=(
|
|
84
|
+
gatt.Characteristic.Permissions.READABLE
|
|
85
|
+
| gatt.Characteristic.Permissions.WRITEABLE
|
|
86
|
+
),
|
|
87
|
+
value=bytes(1),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
super().__init__(
|
|
91
|
+
characteristics=[
|
|
92
|
+
c
|
|
93
|
+
for c in (
|
|
94
|
+
self.service_changed_characteristic,
|
|
95
|
+
self.client_supported_features_characteristic,
|
|
96
|
+
self.database_hash_characteristic,
|
|
97
|
+
self.server_supported_features_characteristic,
|
|
98
|
+
)
|
|
99
|
+
if c is not None
|
|
100
|
+
],
|
|
101
|
+
primary=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def get_attribute_data(cls, attribute: att.Attribute) -> bytes:
|
|
106
|
+
if attribute.type in (
|
|
107
|
+
gatt.GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
|
108
|
+
gatt.GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
|
109
|
+
gatt.GATT_INCLUDE_ATTRIBUTE_TYPE,
|
|
110
|
+
gatt.GATT_CHARACTERISTIC_ATTRIBUTE_TYPE,
|
|
111
|
+
gatt.GATT_CHARACTERISTIC_EXTENDED_PROPERTIES_DESCRIPTOR,
|
|
112
|
+
):
|
|
113
|
+
return (
|
|
114
|
+
struct.pack("<H", attribute.handle)
|
|
115
|
+
+ attribute.type.to_bytes()
|
|
116
|
+
+ attribute.value
|
|
117
|
+
)
|
|
118
|
+
elif attribute.type in (
|
|
119
|
+
gatt.GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR,
|
|
120
|
+
gatt.GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
|
121
|
+
gatt.GATT_SERVER_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
|
122
|
+
gatt.GATT_CHARACTERISTIC_PRESENTATION_FORMAT_DESCRIPTOR,
|
|
123
|
+
gatt.GATT_CHARACTERISTIC_AGGREGATE_FORMAT_DESCRIPTOR,
|
|
124
|
+
):
|
|
125
|
+
return struct.pack("<H", attribute.handle) + attribute.type.to_bytes()
|
|
126
|
+
|
|
127
|
+
return b''
|
|
128
|
+
|
|
129
|
+
def get_database_hash(self, connection: device.Connection | None) -> bytes:
|
|
130
|
+
assert connection
|
|
131
|
+
|
|
132
|
+
m = b''.join(
|
|
133
|
+
[
|
|
134
|
+
self.get_attribute_data(attribute)
|
|
135
|
+
for attribute in connection.device.gatt_server.attributes
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return crypto.aes_cmac(m=m, k=bytes(16))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class GenericAttributeProfileServiceProxy(gatt_client.ProfileServiceProxy):
|
|
143
|
+
SERVICE_CLASS = GenericAttributeProfileService
|
|
144
|
+
|
|
145
|
+
client_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
|
146
|
+
None
|
|
147
|
+
)
|
|
148
|
+
server_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
|
149
|
+
None
|
|
150
|
+
)
|
|
151
|
+
database_hash_characteristic: gatt_client.CharacteristicProxy | None = None
|
|
152
|
+
service_changed_characteristic: gatt_client.CharacteristicProxy | None = None
|
|
153
|
+
|
|
154
|
+
_CHARACTERISTICS = {
|
|
155
|
+
gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC: 'client_supported_features_characteristic',
|
|
156
|
+
gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC: 'server_supported_features_characteristic',
|
|
157
|
+
gatt.GATT_DATABASE_HASH_CHARACTERISTIC: 'database_hash_characteristic',
|
|
158
|
+
gatt.GATT_SERVICE_CHANGED_CHARACTERISTIC: 'service_changed_characteristic',
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
|
162
|
+
self.service_proxy = service_proxy
|
|
163
|
+
|
|
164
|
+
for uuid, attribute_name in self._CHARACTERISTICS.items():
|
|
165
|
+
if characteristics := self.service_proxy.get_characteristics_by_uuid(uuid):
|
|
166
|
+
setattr(self, attribute_name, characteristics[0])
|
bumble/profiles/gmap.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
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 the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""LE Audio - Gaming Audio Profile"""
|
|
16
|
+
|
|
17
|
+
# -----------------------------------------------------------------------------
|
|
18
|
+
# Imports
|
|
19
|
+
# -----------------------------------------------------------------------------
|
|
20
|
+
import struct
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from bumble.gatt import (
|
|
24
|
+
TemplateService,
|
|
25
|
+
DelegatedCharacteristicAdapter,
|
|
26
|
+
Characteristic,
|
|
27
|
+
GATT_GAMING_AUDIO_SERVICE,
|
|
28
|
+
GATT_GMAP_ROLE_CHARACTERISTIC,
|
|
29
|
+
GATT_UGG_FEATURES_CHARACTERISTIC,
|
|
30
|
+
GATT_UGT_FEATURES_CHARACTERISTIC,
|
|
31
|
+
GATT_BGS_FEATURES_CHARACTERISTIC,
|
|
32
|
+
GATT_BGR_FEATURES_CHARACTERISTIC,
|
|
33
|
+
)
|
|
34
|
+
from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
|
|
35
|
+
from enum import IntFlag
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# -----------------------------------------------------------------------------
|
|
39
|
+
# Classes
|
|
40
|
+
# -----------------------------------------------------------------------------
|
|
41
|
+
class GmapRole(IntFlag):
|
|
42
|
+
UNICAST_GAME_GATEWAY = 1 << 0
|
|
43
|
+
UNICAST_GAME_TERMINAL = 1 << 1
|
|
44
|
+
BROADCAST_GAME_SENDER = 1 << 2
|
|
45
|
+
BROADCAST_GAME_RECEIVER = 1 << 3
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UggFeatures(IntFlag):
|
|
49
|
+
UGG_MULTIPLEX = 1 << 0
|
|
50
|
+
UGG_96_KBPS_SOURCE = 1 << 1
|
|
51
|
+
UGG_MULTISINK = 1 << 2
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class UgtFeatures(IntFlag):
|
|
55
|
+
UGT_SOURCE = 1 << 0
|
|
56
|
+
UGT_80_KBPS_SOURCE = 1 << 1
|
|
57
|
+
UGT_SINK = 1 << 2
|
|
58
|
+
UGT_64_KBPS_SINK = 1 << 3
|
|
59
|
+
UGT_MULTIPLEX = 1 << 4
|
|
60
|
+
UGT_MULTISINK = 1 << 5
|
|
61
|
+
UGT_MULTISOURCE = 1 << 6
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BgsFeatures(IntFlag):
|
|
65
|
+
BGS_96_KBPS = 1 << 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BgrFeatures(IntFlag):
|
|
69
|
+
BGR_MULTISINK = 1 << 0
|
|
70
|
+
BGR_MULTIPLEX = 1 << 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# -----------------------------------------------------------------------------
|
|
74
|
+
# Server
|
|
75
|
+
# -----------------------------------------------------------------------------
|
|
76
|
+
class GamingAudioService(TemplateService):
|
|
77
|
+
UUID = GATT_GAMING_AUDIO_SERVICE
|
|
78
|
+
|
|
79
|
+
gmap_role: Characteristic
|
|
80
|
+
ugg_features: Optional[Characteristic] = None
|
|
81
|
+
ugt_features: Optional[Characteristic] = None
|
|
82
|
+
bgs_features: Optional[Characteristic] = None
|
|
83
|
+
bgr_features: Optional[Characteristic] = None
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
gmap_role: GmapRole,
|
|
88
|
+
ugg_features: Optional[UggFeatures] = None,
|
|
89
|
+
ugt_features: Optional[UgtFeatures] = None,
|
|
90
|
+
bgs_features: Optional[BgsFeatures] = None,
|
|
91
|
+
bgr_features: Optional[BgrFeatures] = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
characteristics = []
|
|
94
|
+
|
|
95
|
+
ugg_features = UggFeatures(0) if ugg_features is None else ugg_features
|
|
96
|
+
ugt_features = UgtFeatures(0) if ugt_features is None else ugt_features
|
|
97
|
+
bgs_features = BgsFeatures(0) if bgs_features is None else bgs_features
|
|
98
|
+
bgr_features = BgrFeatures(0) if bgr_features is None else bgr_features
|
|
99
|
+
|
|
100
|
+
self.gmap_role = Characteristic(
|
|
101
|
+
uuid=GATT_GMAP_ROLE_CHARACTERISTIC,
|
|
102
|
+
properties=Characteristic.Properties.READ,
|
|
103
|
+
permissions=Characteristic.Permissions.READABLE,
|
|
104
|
+
value=struct.pack('B', gmap_role),
|
|
105
|
+
)
|
|
106
|
+
characteristics.append(self.gmap_role)
|
|
107
|
+
|
|
108
|
+
if gmap_role & GmapRole.UNICAST_GAME_GATEWAY:
|
|
109
|
+
self.ugg_features = Characteristic(
|
|
110
|
+
uuid=GATT_UGG_FEATURES_CHARACTERISTIC,
|
|
111
|
+
properties=Characteristic.Properties.READ,
|
|
112
|
+
permissions=Characteristic.Permissions.READABLE,
|
|
113
|
+
value=struct.pack('B', ugg_features),
|
|
114
|
+
)
|
|
115
|
+
characteristics.append(self.ugg_features)
|
|
116
|
+
|
|
117
|
+
if gmap_role & GmapRole.UNICAST_GAME_TERMINAL:
|
|
118
|
+
self.ugt_features = Characteristic(
|
|
119
|
+
uuid=GATT_UGT_FEATURES_CHARACTERISTIC,
|
|
120
|
+
properties=Characteristic.Properties.READ,
|
|
121
|
+
permissions=Characteristic.Permissions.READABLE,
|
|
122
|
+
value=struct.pack('B', ugt_features),
|
|
123
|
+
)
|
|
124
|
+
characteristics.append(self.ugt_features)
|
|
125
|
+
|
|
126
|
+
if gmap_role & GmapRole.BROADCAST_GAME_SENDER:
|
|
127
|
+
self.bgs_features = Characteristic(
|
|
128
|
+
uuid=GATT_BGS_FEATURES_CHARACTERISTIC,
|
|
129
|
+
properties=Characteristic.Properties.READ,
|
|
130
|
+
permissions=Characteristic.Permissions.READABLE,
|
|
131
|
+
value=struct.pack('B', bgs_features),
|
|
132
|
+
)
|
|
133
|
+
characteristics.append(self.bgs_features)
|
|
134
|
+
|
|
135
|
+
if gmap_role & GmapRole.BROADCAST_GAME_RECEIVER:
|
|
136
|
+
self.bgr_features = Characteristic(
|
|
137
|
+
uuid=GATT_BGR_FEATURES_CHARACTERISTIC,
|
|
138
|
+
properties=Characteristic.Properties.READ,
|
|
139
|
+
permissions=Characteristic.Permissions.READABLE,
|
|
140
|
+
value=struct.pack('B', bgr_features),
|
|
141
|
+
)
|
|
142
|
+
characteristics.append(self.bgr_features)
|
|
143
|
+
|
|
144
|
+
super().__init__(characteristics)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# -----------------------------------------------------------------------------
|
|
148
|
+
# Client
|
|
149
|
+
# -----------------------------------------------------------------------------
|
|
150
|
+
class GamingAudioServiceProxy(ProfileServiceProxy):
|
|
151
|
+
SERVICE_CLASS = GamingAudioService
|
|
152
|
+
|
|
153
|
+
def __init__(self, service_proxy: ServiceProxy) -> None:
|
|
154
|
+
self.service_proxy = service_proxy
|
|
155
|
+
|
|
156
|
+
self.gmap_role = DelegatedCharacteristicAdapter(
|
|
157
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
158
|
+
GATT_GMAP_ROLE_CHARACTERISTIC
|
|
159
|
+
),
|
|
160
|
+
decode=lambda value: GmapRole(value[0]),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
164
|
+
GATT_UGG_FEATURES_CHARACTERISTIC
|
|
165
|
+
):
|
|
166
|
+
self.ugg_features = DelegatedCharacteristicAdapter(
|
|
167
|
+
characteristic=characteristics[0],
|
|
168
|
+
decode=lambda value: UggFeatures(value[0]),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
172
|
+
GATT_UGT_FEATURES_CHARACTERISTIC
|
|
173
|
+
):
|
|
174
|
+
self.ugt_features = DelegatedCharacteristicAdapter(
|
|
175
|
+
characteristic=characteristics[0],
|
|
176
|
+
decode=lambda value: UgtFeatures(value[0]),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
180
|
+
GATT_BGS_FEATURES_CHARACTERISTIC
|
|
181
|
+
):
|
|
182
|
+
self.bgs_features = DelegatedCharacteristicAdapter(
|
|
183
|
+
characteristic=characteristics[0],
|
|
184
|
+
decode=lambda value: BgsFeatures(value[0]),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
188
|
+
GATT_BGR_FEATURES_CHARACTERISTIC
|
|
189
|
+
):
|
|
190
|
+
self.bgr_features = DelegatedCharacteristicAdapter(
|
|
191
|
+
characteristic=characteristics[0],
|
|
192
|
+
decode=lambda value: BgrFeatures(value[0]),
|
|
193
|
+
)
|
bumble/profiles/le_audio.py
CHANGED
|
@@ -17,23 +17,35 @@
|
|
|
17
17
|
# -----------------------------------------------------------------------------
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import dataclasses
|
|
20
|
+
import enum
|
|
20
21
|
import struct
|
|
21
|
-
from typing import List, Type
|
|
22
|
+
from typing import Any, List, Type
|
|
22
23
|
from typing_extensions import Self
|
|
23
24
|
|
|
25
|
+
from bumble.profiles import bap
|
|
24
26
|
from bumble import utils
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
# -----------------------------------------------------------------------------
|
|
28
30
|
# Classes
|
|
29
31
|
# -----------------------------------------------------------------------------
|
|
32
|
+
class AudioActiveState(utils.OpenIntEnum):
|
|
33
|
+
NO_AUDIO_DATA_TRANSMITTED = 0x00
|
|
34
|
+
AUDIO_DATA_TRANSMITTED = 0x01
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AssistedListeningStream(utils.OpenIntEnum):
|
|
38
|
+
UNSPECIFIED_AUDIO_ENHANCEMENT = 0x00
|
|
39
|
+
|
|
40
|
+
|
|
30
41
|
@dataclasses.dataclass
|
|
31
42
|
class Metadata:
|
|
32
43
|
'''Bluetooth Assigned Numbers, Section 6.12.6 - Metadata LTV structures.
|
|
33
44
|
|
|
34
|
-
As Metadata fields may extend, and
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
As Metadata fields may extend, and the spec may not guarantee the uniqueness of
|
|
46
|
+
tags, we don't automatically parse the Metadata data into specific classes.
|
|
47
|
+
Users of this class may decode the data by themselves, or use the Entry.decode
|
|
48
|
+
method.
|
|
37
49
|
'''
|
|
38
50
|
|
|
39
51
|
class Tag(utils.OpenIntEnum):
|
|
@@ -57,6 +69,44 @@ class Metadata:
|
|
|
57
69
|
tag: Metadata.Tag
|
|
58
70
|
data: bytes
|
|
59
71
|
|
|
72
|
+
def decode(self) -> Any:
|
|
73
|
+
"""
|
|
74
|
+
Decode the data into an object, if possible.
|
|
75
|
+
|
|
76
|
+
If no specific object class exists to represent the data, the raw data
|
|
77
|
+
bytes are returned.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
if self.tag in (
|
|
81
|
+
Metadata.Tag.PREFERRED_AUDIO_CONTEXTS,
|
|
82
|
+
Metadata.Tag.STREAMING_AUDIO_CONTEXTS,
|
|
83
|
+
):
|
|
84
|
+
return bap.ContextType(struct.unpack("<H", self.data)[0])
|
|
85
|
+
|
|
86
|
+
if self.tag in (
|
|
87
|
+
Metadata.Tag.PROGRAM_INFO,
|
|
88
|
+
Metadata.Tag.PROGRAM_INFO_URI,
|
|
89
|
+
Metadata.Tag.BROADCAST_NAME,
|
|
90
|
+
):
|
|
91
|
+
return self.data.decode("utf-8")
|
|
92
|
+
|
|
93
|
+
if self.tag == Metadata.Tag.LANGUAGE:
|
|
94
|
+
return self.data.decode("ascii")
|
|
95
|
+
|
|
96
|
+
if self.tag == Metadata.Tag.CCID_LIST:
|
|
97
|
+
return list(self.data)
|
|
98
|
+
|
|
99
|
+
if self.tag == Metadata.Tag.PARENTAL_RATING:
|
|
100
|
+
return self.data[0]
|
|
101
|
+
|
|
102
|
+
if self.tag == Metadata.Tag.AUDIO_ACTIVE_STATE:
|
|
103
|
+
return AudioActiveState(self.data[0])
|
|
104
|
+
|
|
105
|
+
if self.tag == Metadata.Tag.ASSISTED_LISTENING_STREAM:
|
|
106
|
+
return AssistedListeningStream(self.data[0])
|
|
107
|
+
|
|
108
|
+
return self.data
|
|
109
|
+
|
|
60
110
|
@classmethod
|
|
61
111
|
def from_bytes(cls: Type[Self], data: bytes) -> Self:
|
|
62
112
|
return cls(tag=Metadata.Tag(data[0]), data=data[1:])
|
|
@@ -66,6 +116,29 @@ class Metadata:
|
|
|
66
116
|
|
|
67
117
|
entries: List[Entry] = dataclasses.field(default_factory=list)
|
|
68
118
|
|
|
119
|
+
def pretty_print(self, indent: str) -> str:
|
|
120
|
+
"""Convenience method to generate a string with one key-value pair per line."""
|
|
121
|
+
|
|
122
|
+
max_key_length = 0
|
|
123
|
+
keys = []
|
|
124
|
+
values = []
|
|
125
|
+
for entry in self.entries:
|
|
126
|
+
key = entry.tag.name
|
|
127
|
+
max_key_length = max(max_key_length, len(key))
|
|
128
|
+
keys.append(key)
|
|
129
|
+
decoded = entry.decode()
|
|
130
|
+
if isinstance(decoded, enum.Enum):
|
|
131
|
+
values.append(decoded.name)
|
|
132
|
+
elif isinstance(decoded, bytes):
|
|
133
|
+
values.append(decoded.hex())
|
|
134
|
+
else:
|
|
135
|
+
values.append(str(decoded))
|
|
136
|
+
|
|
137
|
+
return '\n'.join(
|
|
138
|
+
f'{indent}{key}: {" " * (max_key_length-len(key))}{value}'
|
|
139
|
+
for key, value in zip(keys, values)
|
|
140
|
+
)
|
|
141
|
+
|
|
69
142
|
@classmethod
|
|
70
143
|
def from_bytes(cls: Type[Self], data: bytes) -> Self:
|
|
71
144
|
entries = []
|
|
@@ -81,3 +154,13 @@ class Metadata:
|
|
|
81
154
|
|
|
82
155
|
def __bytes__(self) -> bytes:
|
|
83
156
|
return b''.join([bytes(entry) for entry in self.entries])
|
|
157
|
+
|
|
158
|
+
def __str__(self) -> str:
|
|
159
|
+
entries_str = []
|
|
160
|
+
for entry in self.entries:
|
|
161
|
+
decoded = entry.decode()
|
|
162
|
+
entries_str.append(
|
|
163
|
+
f'{entry.tag.name}: '
|
|
164
|
+
f'{decoded.hex() if isinstance(decoded, bytes) else decoded!r}'
|
|
165
|
+
)
|
|
166
|
+
return f'Metadata(entries={", ".join(entry_str for entry_str in entries_str)})'
|