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.
Files changed (51) hide show
  1. bumble/_version.py +9 -4
  2. bumble/apps/auracast.py +631 -98
  3. bumble/apps/bench.py +238 -157
  4. bumble/apps/console.py +19 -12
  5. bumble/apps/controller_info.py +23 -7
  6. bumble/apps/device_info.py +50 -4
  7. bumble/apps/gg_bridge.py +1 -1
  8. bumble/apps/lea_unicast/app.py +61 -201
  9. bumble/att.py +51 -37
  10. bumble/audio/__init__.py +17 -0
  11. bumble/audio/io.py +553 -0
  12. bumble/controller.py +24 -9
  13. bumble/core.py +305 -156
  14. bumble/device.py +1090 -99
  15. bumble/gatt.py +36 -226
  16. bumble/gatt_adapters.py +374 -0
  17. bumble/gatt_client.py +52 -33
  18. bumble/gatt_server.py +5 -5
  19. bumble/hci.py +812 -14
  20. bumble/host.py +367 -65
  21. bumble/l2cap.py +3 -16
  22. bumble/pairing.py +5 -5
  23. bumble/pandora/host.py +7 -12
  24. bumble/profiles/aics.py +48 -57
  25. bumble/profiles/ascs.py +8 -19
  26. bumble/profiles/asha.py +16 -14
  27. bumble/profiles/bass.py +16 -22
  28. bumble/profiles/battery_service.py +13 -3
  29. bumble/profiles/device_information_service.py +16 -14
  30. bumble/profiles/gap.py +12 -8
  31. bumble/profiles/gatt_service.py +167 -0
  32. bumble/profiles/gmap.py +198 -0
  33. bumble/profiles/hap.py +8 -6
  34. bumble/profiles/heart_rate_service.py +20 -4
  35. bumble/profiles/le_audio.py +87 -4
  36. bumble/profiles/mcp.py +11 -9
  37. bumble/profiles/pacs.py +61 -16
  38. bumble/profiles/tmap.py +8 -12
  39. bumble/profiles/{vcp.py → vcs.py} +35 -29
  40. bumble/profiles/vocs.py +62 -85
  41. bumble/sdp.py +223 -93
  42. bumble/smp.py +1 -1
  43. bumble/utils.py +12 -2
  44. bumble/vendor/android/hci.py +1 -1
  45. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/METADATA +13 -11
  46. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/RECORD +50 -46
  47. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/WHEEL +1 -1
  48. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/entry_points.txt +1 -0
  49. bumble/apps/lea_unicast/liblc3.wasm +0 -0
  50. {bumble-0.0.204.dist-info → bumble-0.0.208.dist-info}/LICENSE +0 -0
  51. {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])
@@ -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 bumble import att, gatt, gatt_client
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 = gatt.PackedCharacteristicAdapter(
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 = gatt.PackedCharacteristicAdapter(
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 ..gatt_client import ProfileServiceProxy
24
- from ..att import ATT_Error
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
- SerializableCharacteristicAdapter,
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
 
@@ -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 Spec doesn't forbid duplication, we don't parse
35
- Metadata into a key-value style dataclass here. Rather, we encourage users to parse
36
- again outside the lib.
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 = 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),
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,