bumble 0.0.203__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 +227 -148
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/apps/pair.py +13 -8
- bumble/apps/show.py +6 -6
- bumble/att.py +10 -11
- 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/drivers/common.py +2 -0
- bumble/drivers/intel.py +593 -24
- bumble/gatt.py +67 -12
- bumble/gatt_client.py +14 -2
- bumble/gatt_server.py +12 -1
- bumble/hci.py +854 -33
- bumble/host.py +363 -64
- bumble/l2cap.py +3 -16
- bumble/pairing.py +3 -0
- bumble/profiles/aics.py +45 -80
- bumble/profiles/ascs.py +6 -18
- bumble/profiles/asha.py +5 -5
- bumble/profiles/bass.py +9 -21
- bumble/profiles/device_information_service.py +4 -1
- bumble/profiles/gatt_service.py +166 -0
- bumble/profiles/gmap.py +193 -0
- bumble/profiles/heart_rate_service.py +5 -6
- 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 +299 -0
- bumble/sdp.py +223 -93
- bumble/smp.py +8 -3
- bumble/tools/intel_fw_download.py +130 -0
- bumble/tools/intel_util.py +154 -0
- bumble/transport/usb.py +8 -2
- bumble/utils.py +22 -7
- bumble/vendor/android/hci.py +29 -4
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/RECORD +49 -43
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +3 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/top_level.txt +0 -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
|
+
)
|
|
@@ -30,6 +30,7 @@ from ..gatt import (
|
|
|
30
30
|
TemplateService,
|
|
31
31
|
Characteristic,
|
|
32
32
|
CharacteristicValue,
|
|
33
|
+
SerializableCharacteristicAdapter,
|
|
33
34
|
DelegatedCharacteristicAdapter,
|
|
34
35
|
PackedCharacteristicAdapter,
|
|
35
36
|
)
|
|
@@ -150,15 +151,14 @@ class HeartRateService(TemplateService):
|
|
|
150
151
|
body_sensor_location=None,
|
|
151
152
|
reset_energy_expended=None,
|
|
152
153
|
):
|
|
153
|
-
self.heart_rate_measurement_characteristic =
|
|
154
|
+
self.heart_rate_measurement_characteristic = SerializableCharacteristicAdapter(
|
|
154
155
|
Characteristic(
|
|
155
156
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
|
156
157
|
Characteristic.Properties.NOTIFY,
|
|
157
158
|
0,
|
|
158
159
|
CharacteristicValue(read=read_heart_rate_measurement),
|
|
159
160
|
),
|
|
160
|
-
|
|
161
|
-
encode=lambda value: bytes(value),
|
|
161
|
+
HeartRateService.HeartRateMeasurement,
|
|
162
162
|
)
|
|
163
163
|
characteristics = [self.heart_rate_measurement_characteristic]
|
|
164
164
|
|
|
@@ -204,9 +204,8 @@ class HeartRateServiceProxy(ProfileServiceProxy):
|
|
|
204
204
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
205
205
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC
|
|
206
206
|
):
|
|
207
|
-
self.heart_rate_measurement =
|
|
208
|
-
characteristics[0],
|
|
209
|
-
decode=HeartRateService.HeartRateMeasurement.from_bytes,
|
|
207
|
+
self.heart_rate_measurement = SerializableCharacteristicAdapter(
|
|
208
|
+
characteristics[0], HeartRateService.HeartRateMeasurement
|
|
210
209
|
)
|
|
211
210
|
else:
|
|
212
211
|
self.heart_rate_measurement = None
|
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)})'
|
bumble/profiles/pacs.py
CHANGED
|
@@ -72,6 +72,19 @@ class PacRecord:
|
|
|
72
72
|
metadata=metadata,
|
|
73
73
|
)
|
|
74
74
|
|
|
75
|
+
@classmethod
|
|
76
|
+
def list_from_bytes(cls, data: bytes) -> list[PacRecord]:
|
|
77
|
+
"""Parse a serialized list of records preceded by a one byte list length."""
|
|
78
|
+
record_count = data[0]
|
|
79
|
+
records = []
|
|
80
|
+
offset = 1
|
|
81
|
+
for _ in range(record_count):
|
|
82
|
+
record = PacRecord.from_bytes(data[offset:])
|
|
83
|
+
offset += len(bytes(record))
|
|
84
|
+
records.append(record)
|
|
85
|
+
|
|
86
|
+
return records
|
|
87
|
+
|
|
75
88
|
def __bytes__(self) -> bytes:
|
|
76
89
|
capabilities_bytes = bytes(self.codec_specific_capabilities)
|
|
77
90
|
metadata_bytes = bytes(self.metadata)
|
|
@@ -172,39 +185,58 @@ class PublishedAudioCapabilitiesService(gatt.TemplateService):
|
|
|
172
185
|
class PublishedAudioCapabilitiesServiceProxy(gatt_client.ProfileServiceProxy):
|
|
173
186
|
SERVICE_CLASS = PublishedAudioCapabilitiesService
|
|
174
187
|
|
|
175
|
-
sink_pac: Optional[
|
|
176
|
-
sink_audio_locations: Optional[
|
|
177
|
-
source_pac: Optional[
|
|
178
|
-
source_audio_locations: Optional[
|
|
179
|
-
available_audio_contexts:
|
|
180
|
-
supported_audio_contexts:
|
|
188
|
+
sink_pac: Optional[gatt.DelegatedCharacteristicAdapter] = None
|
|
189
|
+
sink_audio_locations: Optional[gatt.DelegatedCharacteristicAdapter] = None
|
|
190
|
+
source_pac: Optional[gatt.DelegatedCharacteristicAdapter] = None
|
|
191
|
+
source_audio_locations: Optional[gatt.DelegatedCharacteristicAdapter] = None
|
|
192
|
+
available_audio_contexts: gatt.DelegatedCharacteristicAdapter
|
|
193
|
+
supported_audio_contexts: gatt.DelegatedCharacteristicAdapter
|
|
181
194
|
|
|
182
195
|
def __init__(self, service_proxy: gatt_client.ServiceProxy):
|
|
183
196
|
self.service_proxy = service_proxy
|
|
184
197
|
|
|
185
|
-
self.available_audio_contexts =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
198
|
+
self.available_audio_contexts = gatt.DelegatedCharacteristicAdapter(
|
|
199
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
200
|
+
gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
|
|
201
|
+
),
|
|
202
|
+
decode=lambda x: tuple(map(ContextType, struct.unpack('<HH', x))),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.supported_audio_contexts = gatt.DelegatedCharacteristicAdapter(
|
|
206
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
207
|
+
gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
|
|
208
|
+
),
|
|
209
|
+
decode=lambda x: tuple(map(ContextType, struct.unpack('<HH', x))),
|
|
210
|
+
)
|
|
191
211
|
|
|
192
212
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
193
213
|
gatt.GATT_SINK_PAC_CHARACTERISTIC
|
|
194
214
|
):
|
|
195
|
-
self.sink_pac =
|
|
215
|
+
self.sink_pac = gatt.DelegatedCharacteristicAdapter(
|
|
216
|
+
characteristics[0],
|
|
217
|
+
decode=PacRecord.list_from_bytes,
|
|
218
|
+
)
|
|
196
219
|
|
|
197
220
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
198
221
|
gatt.GATT_SOURCE_PAC_CHARACTERISTIC
|
|
199
222
|
):
|
|
200
|
-
self.source_pac =
|
|
223
|
+
self.source_pac = gatt.DelegatedCharacteristicAdapter(
|
|
224
|
+
characteristics[0],
|
|
225
|
+
decode=PacRecord.list_from_bytes,
|
|
226
|
+
)
|
|
201
227
|
|
|
202
228
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
203
229
|
gatt.GATT_SINK_AUDIO_LOCATION_CHARACTERISTIC
|
|
204
230
|
):
|
|
205
|
-
self.sink_audio_locations =
|
|
231
|
+
self.sink_audio_locations = gatt.DelegatedCharacteristicAdapter(
|
|
232
|
+
characteristics[0],
|
|
233
|
+
decode=lambda x: AudioLocation(struct.unpack('<I', x)[0]),
|
|
234
|
+
)
|
|
206
235
|
|
|
207
236
|
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
208
237
|
gatt.GATT_SOURCE_AUDIO_LOCATION_CHARACTERISTIC
|
|
209
238
|
):
|
|
210
|
-
self.source_audio_locations =
|
|
239
|
+
self.source_audio_locations = gatt.DelegatedCharacteristicAdapter(
|
|
240
|
+
characteristics[0],
|
|
241
|
+
decode=lambda x: AudioLocation(struct.unpack('<I', x)[0]),
|
|
242
|
+
)
|
bumble/profiles/tmap.py
CHANGED
|
@@ -25,7 +25,6 @@ from bumble.gatt import (
|
|
|
25
25
|
TemplateService,
|
|
26
26
|
Characteristic,
|
|
27
27
|
DelegatedCharacteristicAdapter,
|
|
28
|
-
InvalidServiceError,
|
|
29
28
|
GATT_TELEPHONY_AND_MEDIA_AUDIO_SERVICE,
|
|
30
29
|
GATT_TMAP_ROLE_CHARACTERISTIC,
|
|
31
30
|
)
|
|
@@ -74,15 +73,10 @@ class TelephonyAndMediaAudioServiceProxy(ProfileServiceProxy):
|
|
|
74
73
|
def __init__(self, service_proxy: ServiceProxy):
|
|
75
74
|
self.service_proxy = service_proxy
|
|
76
75
|
|
|
77
|
-
if not (
|
|
78
|
-
characteristics := service_proxy.get_characteristics_by_uuid(
|
|
79
|
-
GATT_TMAP_ROLE_CHARACTERISTIC
|
|
80
|
-
)
|
|
81
|
-
):
|
|
82
|
-
raise InvalidServiceError('TMAP Role characteristic not found')
|
|
83
|
-
|
|
84
76
|
self.role = DelegatedCharacteristicAdapter(
|
|
85
|
-
|
|
77
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
78
|
+
GATT_TMAP_ROLE_CHARACTERISTIC
|
|
79
|
+
),
|
|
86
80
|
decode=lambda value: Role(
|
|
87
81
|
struct.unpack_from('<H', value, 0)[0],
|
|
88
82
|
),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2021-
|
|
1
|
+
# Copyright 2021-2025 Google LLC
|
|
2
2
|
#
|
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
4
|
# you may not use this file except in compliance with the License.
|
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
# Imports
|
|
18
18
|
# -----------------------------------------------------------------------------
|
|
19
19
|
from __future__ import annotations
|
|
20
|
+
import dataclasses
|
|
20
21
|
import enum
|
|
21
22
|
|
|
23
|
+
from typing import Optional, Sequence
|
|
24
|
+
|
|
22
25
|
from bumble import att
|
|
23
26
|
from bumble import device
|
|
24
27
|
from bumble import gatt
|
|
25
28
|
from bumble import gatt_client
|
|
26
29
|
|
|
27
|
-
from typing import Optional, Sequence
|
|
28
30
|
|
|
29
31
|
# -----------------------------------------------------------------------------
|
|
30
32
|
# Constants
|
|
@@ -67,6 +69,20 @@ class VolumeControlPointOpcode(enum.IntEnum):
|
|
|
67
69
|
MUTE = 0x06
|
|
68
70
|
|
|
69
71
|
|
|
72
|
+
@dataclasses.dataclass
|
|
73
|
+
class VolumeState:
|
|
74
|
+
volume_setting: int
|
|
75
|
+
mute: int
|
|
76
|
+
change_counter: int
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_bytes(cls, data: bytes) -> VolumeState:
|
|
80
|
+
return cls(data[0], data[1], data[2])
|
|
81
|
+
|
|
82
|
+
def __bytes__(self) -> bytes:
|
|
83
|
+
return bytes([self.volume_setting, self.mute, self.change_counter])
|
|
84
|
+
|
|
85
|
+
|
|
70
86
|
# -----------------------------------------------------------------------------
|
|
71
87
|
# Server
|
|
72
88
|
# -----------------------------------------------------------------------------
|
|
@@ -126,16 +142,8 @@ class VolumeControlService(gatt.TemplateService):
|
|
|
126
142
|
included_services=list(included_services),
|
|
127
143
|
)
|
|
128
144
|
|
|
129
|
-
@property
|
|
130
|
-
def volume_state_bytes(self) -> bytes:
|
|
131
|
-
return bytes([self.volume_setting, self.muted, self.change_counter])
|
|
132
|
-
|
|
133
|
-
@volume_state_bytes.setter
|
|
134
|
-
def volume_state_bytes(self, new_value: bytes) -> None:
|
|
135
|
-
self.volume_setting, self.muted, self.change_counter = new_value
|
|
136
|
-
|
|
137
145
|
def _on_read_volume_state(self, _connection: Optional[device.Connection]) -> bytes:
|
|
138
|
-
return self.
|
|
146
|
+
return bytes(VolumeState(self.volume_setting, self.muted, self.change_counter))
|
|
139
147
|
|
|
140
148
|
def _on_write_volume_control_point(
|
|
141
149
|
self, connection: Optional[device.Connection], value: bytes
|
|
@@ -153,14 +161,9 @@ class VolumeControlService(gatt.TemplateService):
|
|
|
153
161
|
self.change_counter = (self.change_counter + 1) % 256
|
|
154
162
|
connection.abort_on(
|
|
155
163
|
'disconnection',
|
|
156
|
-
connection.device.notify_subscribers(
|
|
157
|
-
attribute=self.volume_state,
|
|
158
|
-
value=self.volume_state_bytes,
|
|
159
|
-
),
|
|
160
|
-
)
|
|
161
|
-
self.emit(
|
|
162
|
-
'volume_state', self.volume_setting, self.muted, self.change_counter
|
|
164
|
+
connection.device.notify_subscribers(attribute=self.volume_state),
|
|
163
165
|
)
|
|
166
|
+
self.emit('volume_state_change')
|
|
164
167
|
|
|
165
168
|
def _on_relative_volume_down(self) -> bool:
|
|
166
169
|
old_volume = self.volume_setting
|
|
@@ -207,24 +210,26 @@ class VolumeControlServiceProxy(gatt_client.ProfileServiceProxy):
|
|
|
207
210
|
SERVICE_CLASS = VolumeControlService
|
|
208
211
|
|
|
209
212
|
volume_control_point: gatt_client.CharacteristicProxy
|
|
213
|
+
volume_state: gatt.SerializableCharacteristicAdapter
|
|
214
|
+
volume_flags: gatt.DelegatedCharacteristicAdapter
|
|
210
215
|
|
|
211
216
|
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
|
212
217
|
self.service_proxy = service_proxy
|
|
213
218
|
|
|
214
|
-
self.volume_state = gatt.
|
|
215
|
-
service_proxy.
|
|
219
|
+
self.volume_state = gatt.SerializableCharacteristicAdapter(
|
|
220
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
216
221
|
gatt.GATT_VOLUME_STATE_CHARACTERISTIC
|
|
217
|
-
)
|
|
218
|
-
|
|
222
|
+
),
|
|
223
|
+
VolumeState,
|
|
219
224
|
)
|
|
220
225
|
|
|
221
|
-
self.volume_control_point = service_proxy.
|
|
226
|
+
self.volume_control_point = service_proxy.get_required_characteristic_by_uuid(
|
|
222
227
|
gatt.GATT_VOLUME_CONTROL_POINT_CHARACTERISTIC
|
|
223
|
-
)
|
|
228
|
+
)
|
|
224
229
|
|
|
225
|
-
self.volume_flags = gatt.
|
|
226
|
-
service_proxy.
|
|
230
|
+
self.volume_flags = gatt.DelegatedCharacteristicAdapter(
|
|
231
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
227
232
|
gatt.GATT_VOLUME_FLAGS_CHARACTERISTIC
|
|
228
|
-
)
|
|
229
|
-
|
|
233
|
+
),
|
|
234
|
+
decode=lambda data: VolumeFlags(data[0]),
|
|
230
235
|
)
|