bumble 0.0.204__py3-none-any.whl → 0.0.208__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 +9 -4
- bumble/apps/auracast.py +631 -98
- bumble/apps/bench.py +238 -157
- bumble/apps/console.py +19 -12
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/gg_bridge.py +1 -1
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/att.py +51 -37
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +305 -156
- bumble/device.py +1090 -99
- bumble/gatt.py +36 -226
- bumble/gatt_adapters.py +374 -0
- bumble/gatt_client.py +52 -33
- bumble/gatt_server.py +5 -5
- bumble/hci.py +812 -14
- bumble/host.py +367 -65
- bumble/l2cap.py +3 -16
- bumble/pairing.py +5 -5
- bumble/pandora/host.py +7 -12
- bumble/profiles/aics.py +48 -57
- bumble/profiles/ascs.py +8 -19
- bumble/profiles/asha.py +16 -14
- bumble/profiles/bass.py +16 -22
- bumble/profiles/battery_service.py +13 -3
- bumble/profiles/device_information_service.py +16 -14
- bumble/profiles/gap.py +12 -8
- bumble/profiles/gatt_service.py +167 -0
- bumble/profiles/gmap.py +198 -0
- bumble/profiles/hap.py +8 -6
- bumble/profiles/heart_rate_service.py +20 -4
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/mcp.py +11 -9
- bumble/profiles/pacs.py +61 -16
- bumble/profiles/tmap.py +8 -12
- bumble/profiles/{vcp.py → vcs.py} +35 -29
- bumble/profiles/vocs.py +62 -85
- bumble/sdp.py +223 -93
- bumble/smp.py +1 -1
- bumble/utils.py +12 -2
- bumble/vendor/android/hci.py +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
- {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
assert isinstance(attribute.value, bytes)
|
|
114
|
+
return (
|
|
115
|
+
struct.pack("<H", attribute.handle)
|
|
116
|
+
+ attribute.type.to_bytes()
|
|
117
|
+
+ attribute.value
|
|
118
|
+
)
|
|
119
|
+
elif attribute.type in (
|
|
120
|
+
gatt.GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR,
|
|
121
|
+
gatt.GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
|
122
|
+
gatt.GATT_SERVER_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
|
|
123
|
+
gatt.GATT_CHARACTERISTIC_PRESENTATION_FORMAT_DESCRIPTOR,
|
|
124
|
+
gatt.GATT_CHARACTERISTIC_AGGREGATE_FORMAT_DESCRIPTOR,
|
|
125
|
+
):
|
|
126
|
+
return struct.pack("<H", attribute.handle) + attribute.type.to_bytes()
|
|
127
|
+
|
|
128
|
+
return b''
|
|
129
|
+
|
|
130
|
+
def get_database_hash(self, connection: device.Connection | None) -> bytes:
|
|
131
|
+
assert connection
|
|
132
|
+
|
|
133
|
+
m = b''.join(
|
|
134
|
+
[
|
|
135
|
+
self.get_attribute_data(attribute)
|
|
136
|
+
for attribute in connection.device.gatt_server.attributes
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return crypto.aes_cmac(m=m, k=bytes(16))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class GenericAttributeProfileServiceProxy(gatt_client.ProfileServiceProxy):
|
|
144
|
+
SERVICE_CLASS = GenericAttributeProfileService
|
|
145
|
+
|
|
146
|
+
client_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
|
147
|
+
None
|
|
148
|
+
)
|
|
149
|
+
server_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
|
|
150
|
+
None
|
|
151
|
+
)
|
|
152
|
+
database_hash_characteristic: gatt_client.CharacteristicProxy | None = None
|
|
153
|
+
service_changed_characteristic: gatt_client.CharacteristicProxy | None = None
|
|
154
|
+
|
|
155
|
+
_CHARACTERISTICS = {
|
|
156
|
+
gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC: 'client_supported_features_characteristic',
|
|
157
|
+
gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC: 'server_supported_features_characteristic',
|
|
158
|
+
gatt.GATT_DATABASE_HASH_CHARACTERISTIC: 'database_hash_characteristic',
|
|
159
|
+
gatt.GATT_SERVICE_CHANGED_CHARACTERISTIC: 'service_changed_characteristic',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
|
163
|
+
self.service_proxy = service_proxy
|
|
164
|
+
|
|
165
|
+
for uuid, attribute_name in self._CHARACTERISTICS.items():
|
|
166
|
+
if characteristics := self.service_proxy.get_characteristics_by_uuid(uuid):
|
|
167
|
+
setattr(self, attribute_name, characteristics[0])
|
bumble/profiles/gmap.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
Characteristic,
|
|
26
|
+
GATT_GAMING_AUDIO_SERVICE,
|
|
27
|
+
GATT_GMAP_ROLE_CHARACTERISTIC,
|
|
28
|
+
GATT_UGG_FEATURES_CHARACTERISTIC,
|
|
29
|
+
GATT_UGT_FEATURES_CHARACTERISTIC,
|
|
30
|
+
GATT_BGS_FEATURES_CHARACTERISTIC,
|
|
31
|
+
GATT_BGR_FEATURES_CHARACTERISTIC,
|
|
32
|
+
)
|
|
33
|
+
from bumble.gatt_adapters import DelegatedCharacteristicProxyAdapter
|
|
34
|
+
from bumble.gatt_client import CharacteristicProxy, 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
|
+
ugg_features: Optional[CharacteristicProxy[UggFeatures]] = None
|
|
154
|
+
ugt_features: Optional[CharacteristicProxy[UgtFeatures]] = None
|
|
155
|
+
bgs_features: Optional[CharacteristicProxy[BgsFeatures]] = None
|
|
156
|
+
bgr_features: Optional[CharacteristicProxy[BgrFeatures]] = None
|
|
157
|
+
|
|
158
|
+
def __init__(self, service_proxy: ServiceProxy) -> None:
|
|
159
|
+
self.service_proxy = service_proxy
|
|
160
|
+
|
|
161
|
+
self.gmap_role = DelegatedCharacteristicProxyAdapter(
|
|
162
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
163
|
+
GATT_GMAP_ROLE_CHARACTERISTIC
|
|
164
|
+
),
|
|
165
|
+
decode=lambda value: GmapRole(value[0]),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
169
|
+
GATT_UGG_FEATURES_CHARACTERISTIC
|
|
170
|
+
):
|
|
171
|
+
self.ugg_features = DelegatedCharacteristicProxyAdapter(
|
|
172
|
+
characteristics[0],
|
|
173
|
+
decode=lambda value: UggFeatures(value[0]),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
177
|
+
GATT_UGT_FEATURES_CHARACTERISTIC
|
|
178
|
+
):
|
|
179
|
+
self.ugt_features = DelegatedCharacteristicProxyAdapter(
|
|
180
|
+
characteristics[0],
|
|
181
|
+
decode=lambda value: UgtFeatures(value[0]),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
185
|
+
GATT_BGS_FEATURES_CHARACTERISTIC
|
|
186
|
+
):
|
|
187
|
+
self.bgs_features = DelegatedCharacteristicProxyAdapter(
|
|
188
|
+
characteristics[0],
|
|
189
|
+
decode=lambda value: BgsFeatures(value[0]),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if characteristics := service_proxy.get_characteristics_by_uuid(
|
|
193
|
+
GATT_BGR_FEATURES_CHARACTERISTIC
|
|
194
|
+
):
|
|
195
|
+
self.bgr_features = DelegatedCharacteristicProxyAdapter(
|
|
196
|
+
characteristics[0],
|
|
197
|
+
decode=lambda value: BgrFeatures(value[0]),
|
|
198
|
+
)
|
bumble/profiles/hap.py
CHANGED
|
@@ -18,14 +18,15 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import asyncio
|
|
20
20
|
import functools
|
|
21
|
-
from
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
import logging
|
|
23
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
|
24
|
+
|
|
25
|
+
from bumble import att, gatt, gatt_adapters, gatt_client
|
|
22
26
|
from bumble.core import InvalidArgumentError, InvalidStateError
|
|
23
27
|
from bumble.device import Device, Connection
|
|
24
28
|
from bumble.utils import AsyncRunner, OpenIntEnum
|
|
25
29
|
from bumble.hci import Address
|
|
26
|
-
from dataclasses import dataclass, field
|
|
27
|
-
import logging
|
|
28
|
-
from typing import Any, Dict, List, Optional, Set, Union
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
# -----------------------------------------------------------------------------
|
|
@@ -631,11 +632,12 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
|
|
631
632
|
|
|
632
633
|
hearing_aid_preset_control_point: gatt_client.CharacteristicProxy
|
|
633
634
|
preset_control_point_indications: asyncio.Queue
|
|
635
|
+
active_preset_index_notification: asyncio.Queue
|
|
634
636
|
|
|
635
637
|
def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
|
|
636
638
|
self.service_proxy = service_proxy
|
|
637
639
|
|
|
638
|
-
self.server_features =
|
|
640
|
+
self.server_features = gatt_adapters.PackedCharacteristicProxyAdapter(
|
|
639
641
|
service_proxy.get_characteristics_by_uuid(
|
|
640
642
|
gatt.GATT_HEARING_AID_FEATURES_CHARACTERISTIC
|
|
641
643
|
)[0],
|
|
@@ -648,7 +650,7 @@ class HearingAccessServiceProxy(gatt_client.ProfileServiceProxy):
|
|
|
648
650
|
)[0]
|
|
649
651
|
)
|
|
650
652
|
|
|
651
|
-
self.active_preset_index =
|
|
653
|
+
self.active_preset_index = gatt_adapters.PackedCharacteristicProxyAdapter(
|
|
652
654
|
service_proxy.get_characteristics_by_uuid(
|
|
653
655
|
gatt.GATT_ACTIVE_PRESET_INDEX_CHARACTERISTIC
|
|
654
656
|
)[0],
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
# -----------------------------------------------------------------------------
|
|
17
17
|
# Imports
|
|
18
18
|
# -----------------------------------------------------------------------------
|
|
19
|
+
from __future__ import annotations
|
|
19
20
|
from enum import IntEnum
|
|
20
21
|
import struct
|
|
22
|
+
from typing import Optional
|
|
21
23
|
|
|
22
24
|
from bumble import core
|
|
23
|
-
from
|
|
24
|
-
from
|
|
25
|
-
from ..gatt import (
|
|
25
|
+
from bumble.att import ATT_Error
|
|
26
|
+
from bumble.gatt import (
|
|
26
27
|
GATT_HEART_RATE_SERVICE,
|
|
27
28
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
|
28
29
|
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
|
@@ -30,10 +31,13 @@ from ..gatt import (
|
|
|
30
31
|
TemplateService,
|
|
31
32
|
Characteristic,
|
|
32
33
|
CharacteristicValue,
|
|
33
|
-
|
|
34
|
+
)
|
|
35
|
+
from bumble.gatt_adapters import (
|
|
34
36
|
DelegatedCharacteristicAdapter,
|
|
35
37
|
PackedCharacteristicAdapter,
|
|
38
|
+
SerializableCharacteristicAdapter,
|
|
36
39
|
)
|
|
40
|
+
from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
# -----------------------------------------------------------------------------
|
|
@@ -43,6 +47,10 @@ class HeartRateService(TemplateService):
|
|
|
43
47
|
CONTROL_POINT_NOT_SUPPORTED = 0x80
|
|
44
48
|
RESET_ENERGY_EXPENDED = 0x01
|
|
45
49
|
|
|
50
|
+
heart_rate_measurement_characteristic: Characteristic[HeartRateMeasurement]
|
|
51
|
+
body_sensor_location_characteristic: Characteristic[BodySensorLocation]
|
|
52
|
+
heart_rate_control_point_characteristic: Characteristic[int]
|
|
53
|
+
|
|
46
54
|
class BodySensorLocation(IntEnum):
|
|
47
55
|
OTHER = 0
|
|
48
56
|
CHEST = 1
|
|
@@ -198,6 +206,14 @@ class HeartRateService(TemplateService):
|
|
|
198
206
|
class HeartRateServiceProxy(ProfileServiceProxy):
|
|
199
207
|
SERVICE_CLASS = HeartRateService
|
|
200
208
|
|
|
209
|
+
heart_rate_measurement: Optional[
|
|
210
|
+
CharacteristicProxy[HeartRateService.HeartRateMeasurement]
|
|
211
|
+
]
|
|
212
|
+
body_sensor_location: Optional[
|
|
213
|
+
CharacteristicProxy[HeartRateService.BodySensorLocation]
|
|
214
|
+
]
|
|
215
|
+
heart_rate_control_point: Optional[CharacteristicProxy[int]]
|
|
216
|
+
|
|
201
217
|
def __init__(self, service_proxy):
|
|
202
218
|
self.service_proxy = service_proxy
|
|
203
219
|
|
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/mcp.py
CHANGED
|
@@ -208,7 +208,7 @@ class MediaControlService(gatt.TemplateService):
|
|
|
208
208
|
properties=gatt.Characteristic.Properties.READ
|
|
209
209
|
| gatt.Characteristic.Properties.NOTIFY,
|
|
210
210
|
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
|
211
|
-
value=media_player_name or 'Bumble Player',
|
|
211
|
+
value=(media_player_name or 'Bumble Player').encode(),
|
|
212
212
|
)
|
|
213
213
|
self.track_changed_characteristic = gatt.Characteristic(
|
|
214
214
|
uuid=gatt.GATT_TRACK_CHANGED_CHARACTERISTIC,
|
|
@@ -247,14 +247,16 @@ class MediaControlService(gatt.TemplateService):
|
|
|
247
247
|
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
|
|
248
248
|
value=b'',
|
|
249
249
|
)
|
|
250
|
-
self.media_control_point_characteristic
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
250
|
+
self.media_control_point_characteristic: gatt.Characteristic[bytes] = (
|
|
251
|
+
gatt.Characteristic(
|
|
252
|
+
uuid=gatt.GATT_MEDIA_CONTROL_POINT_CHARACTERISTIC,
|
|
253
|
+
properties=gatt.Characteristic.Properties.WRITE
|
|
254
|
+
| gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE
|
|
255
|
+
| gatt.Characteristic.Properties.NOTIFY,
|
|
256
|
+
permissions=gatt.Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
|
|
257
|
+
| gatt.Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
|
|
258
|
+
value=gatt.CharacteristicValue(write=self.on_media_control_point),
|
|
259
|
+
)
|
|
258
260
|
)
|
|
259
261
|
self.media_control_point_opcodes_supported_characteristic = gatt.Characteristic(
|
|
260
262
|
uuid=gatt.GATT_MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_CHARACTERISTIC,
|