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/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[gatt_client.CharacteristicProxy] = None
176
- sink_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
177
- source_pac: Optional[gatt_client.CharacteristicProxy] = None
178
- source_audio_locations: Optional[gatt_client.CharacteristicProxy] = None
179
- available_audio_contexts: gatt_client.CharacteristicProxy
180
- supported_audio_contexts: gatt_client.CharacteristicProxy
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 = service_proxy.get_characteristics_by_uuid(
186
- gatt.GATT_AVAILABLE_AUDIO_CONTEXTS_CHARACTERISTIC
187
- )[0]
188
- self.supported_audio_contexts = service_proxy.get_characteristics_by_uuid(
189
- gatt.GATT_SUPPORTED_AUDIO_CONTEXTS_CHARACTERISTIC
190
- )[0]
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 = characteristics[0]
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 = characteristics[0]
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 = characteristics[0]
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 = characteristics[0]
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
- characteristics[0],
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-2024 Google LLC
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.volume_state_bytes
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.PackedCharacteristicAdapter(
215
- service_proxy.get_characteristics_by_uuid(
219
+ self.volume_state = gatt.SerializableCharacteristicAdapter(
220
+ service_proxy.get_required_characteristic_by_uuid(
216
221
  gatt.GATT_VOLUME_STATE_CHARACTERISTIC
217
- )[0],
218
- 'BBB',
222
+ ),
223
+ VolumeState,
219
224
  )
220
225
 
221
- self.volume_control_point = service_proxy.get_characteristics_by_uuid(
226
+ self.volume_control_point = service_proxy.get_required_characteristic_by_uuid(
222
227
  gatt.GATT_VOLUME_CONTROL_POINT_CHARACTERISTIC
223
- )[0]
228
+ )
224
229
 
225
- self.volume_flags = gatt.PackedCharacteristicAdapter(
226
- service_proxy.get_characteristics_by_uuid(
230
+ self.volume_flags = gatt.DelegatedCharacteristicAdapter(
231
+ service_proxy.get_required_characteristic_by_uuid(
227
232
  gatt.GATT_VOLUME_FLAGS_CHARACTERISTIC
228
- )[0],
229
- 'B',
233
+ ),
234
+ decode=lambda data: VolumeFlags(data[0]),
230
235
  )
bumble/profiles/vocs.py CHANGED
@@ -27,8 +27,8 @@ from bumble.gatt import (
27
27
  DelegatedCharacteristicAdapter,
28
28
  TemplateService,
29
29
  CharacteristicValue,
30
+ SerializableCharacteristicAdapter,
30
31
  UTF8CharacteristicAdapter,
31
- InvalidServiceError,
32
32
  GATT_VOLUME_OFFSET_CONTROL_SERVICE,
33
33
  GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
34
34
  GATT_AUDIO_LOCATION_CHARACTERISTIC,
@@ -82,9 +82,7 @@ class VolumeOffsetState:
82
82
 
83
83
  async def notify_subscribers_via_connection(self, connection: Connection) -> None:
84
84
  assert self.attribute_value is not None
85
- await connection.device.notify_subscribers(
86
- attribute=self.attribute_value, value=bytes(self)
87
- )
85
+ await connection.device.notify_subscribers(attribute=self.attribute_value)
88
86
 
89
87
  def on_read(self, _connection: Optional[Connection]) -> bytes:
90
88
  return bytes(self)
@@ -111,9 +109,7 @@ class VocsAudioLocation:
111
109
  assert self.attribute_value
112
110
 
113
111
  self.audio_location = AudioLocation(int.from_bytes(value, 'little'))
114
- await connection.device.notify_subscribers(
115
- attribute=self.attribute_value, value=value
116
- )
112
+ await connection.device.notify_subscribers(attribute=self.attribute_value)
117
113
 
118
114
 
119
115
  @dataclass
@@ -169,9 +165,7 @@ class AudioOutputDescription:
169
165
  assert self.attribute_value
170
166
 
171
167
  self.audio_output_description = value.decode('utf-8')
172
- await connection.device.notify_subscribers(
173
- attribute=self.attribute_value, value=value
174
- )
168
+ await connection.device.notify_subscribers(attribute=self.attribute_value)
175
169
 
176
170
 
177
171
  # -----------------------------------------------------------------------------
@@ -203,37 +197,30 @@ class VolumeOffsetControlService(TemplateService):
203
197
  VolumeOffsetControlPoint(self.volume_offset_state)
204
198
  )
205
199
 
206
- self.volume_offset_state_characteristic = DelegatedCharacteristicAdapter(
207
- Characteristic(
208
- uuid=GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
209
- properties=(
210
- Characteristic.Properties.READ | Characteristic.Properties.NOTIFY
211
- ),
212
- permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
213
- value=CharacteristicValue(read=self.volume_offset_state.on_read),
200
+ self.volume_offset_state_characteristic = Characteristic(
201
+ uuid=GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC,
202
+ properties=(
203
+ Characteristic.Properties.READ | Characteristic.Properties.NOTIFY
214
204
  ),
215
- encode=lambda value: bytes(value),
205
+ permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
206
+ value=CharacteristicValue(read=self.volume_offset_state.on_read),
216
207
  )
217
208
 
218
- self.audio_location_characteristic = DelegatedCharacteristicAdapter(
219
- Characteristic(
220
- uuid=GATT_AUDIO_LOCATION_CHARACTERISTIC,
221
- properties=(
222
- Characteristic.Properties.READ
223
- | Characteristic.Properties.NOTIFY
224
- | Characteristic.Properties.WRITE_WITHOUT_RESPONSE
225
- ),
226
- permissions=(
227
- Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
228
- | Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
229
- ),
230
- value=CharacteristicValue(
231
- read=self.audio_location.on_read,
232
- write=self.audio_location.on_write,
233
- ),
209
+ self.audio_location_characteristic = Characteristic(
210
+ uuid=GATT_AUDIO_LOCATION_CHARACTERISTIC,
211
+ properties=(
212
+ Characteristic.Properties.READ
213
+ | Characteristic.Properties.NOTIFY
214
+ | Characteristic.Properties.WRITE_WITHOUT_RESPONSE
215
+ ),
216
+ permissions=(
217
+ Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
218
+ | Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
219
+ ),
220
+ value=CharacteristicValue(
221
+ read=self.audio_location.on_read,
222
+ write=self.audio_location.on_write,
234
223
  ),
235
- encode=lambda value: bytes(value),
236
- decode=VocsAudioLocation.from_bytes,
237
224
  )
238
225
  self.audio_location.attribute_value = self.audio_location_characteristic.value
239
226
 
@@ -244,25 +231,22 @@ class VolumeOffsetControlService(TemplateService):
244
231
  value=CharacteristicValue(write=self.volume_offset_control_point.on_write),
245
232
  )
246
233
 
247
- self.audio_output_description_characteristic = DelegatedCharacteristicAdapter(
248
- Characteristic(
249
- uuid=GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
250
- properties=(
251
- Characteristic.Properties.READ
252
- | Characteristic.Properties.NOTIFY
253
- | Characteristic.Properties.WRITE_WITHOUT_RESPONSE
254
- ),
255
- permissions=(
256
- Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
257
- | Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
258
- ),
259
- value=CharacteristicValue(
260
- read=self.audio_output_description.on_read,
261
- write=self.audio_output_description.on_write,
262
- ),
263
- )
234
+ self.audio_output_description_characteristic = Characteristic(
235
+ uuid=GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC,
236
+ properties=(
237
+ Characteristic.Properties.READ
238
+ | Characteristic.Properties.NOTIFY
239
+ | Characteristic.Properties.WRITE_WITHOUT_RESPONSE
240
+ ),
241
+ permissions=(
242
+ Characteristic.Permissions.READ_REQUIRES_ENCRYPTION
243
+ | Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION
244
+ ),
245
+ value=CharacteristicValue(
246
+ read=self.audio_output_description.on_read,
247
+ write=self.audio_output_description.on_write,
248
+ ),
264
249
  )
265
-
266
250
  self.audio_output_description.attribute_value = (
267
251
  self.audio_output_description_characteristic.value
268
252
  )
@@ -287,44 +271,29 @@ class VolumeOffsetControlServiceProxy(ProfileServiceProxy):
287
271
  def __init__(self, service_proxy: ServiceProxy) -> None:
288
272
  self.service_proxy = service_proxy
289
273
 
290
- if not (
291
- characteristics := service_proxy.get_characteristics_by_uuid(
274
+ self.volume_offset_state = SerializableCharacteristicAdapter(
275
+ service_proxy.get_required_characteristic_by_uuid(
292
276
  GATT_VOLUME_OFFSET_STATE_CHARACTERISTIC
293
- )
294
- ):
295
- raise InvalidServiceError("Volume Offset State characteristic not found")
296
- self.volume_offset_state = DelegatedCharacteristicAdapter(
297
- characteristics[0], decode=VolumeOffsetState.from_bytes
277
+ ),
278
+ VolumeOffsetState,
298
279
  )
299
280
 
300
- if not (
301
- characteristics := service_proxy.get_characteristics_by_uuid(
302
- GATT_AUDIO_LOCATION_CHARACTERISTIC
303
- )
304
- ):
305
- raise InvalidServiceError("Audio Location characteristic not found")
306
281
  self.audio_location = DelegatedCharacteristicAdapter(
307
- characteristics[0],
308
- encode=lambda value: bytes(value),
309
- decode=VocsAudioLocation.from_bytes,
282
+ service_proxy.get_required_characteristic_by_uuid(
283
+ GATT_AUDIO_LOCATION_CHARACTERISTIC
284
+ ),
285
+ encode=lambda value: bytes([int(value)]),
286
+ decode=lambda data: AudioLocation(data[0]),
310
287
  )
311
288
 
312
- if not (
313
- characteristics := service_proxy.get_characteristics_by_uuid(
289
+ self.volume_offset_control_point = (
290
+ service_proxy.get_required_characteristic_by_uuid(
314
291
  GATT_VOLUME_OFFSET_CONTROL_POINT_CHARACTERISTIC
315
292
  )
316
- ):
317
- raise InvalidServiceError(
318
- "Volume Offset Control Point characteristic not found"
319
- )
320
- self.volume_offset_control_point = characteristics[0]
293
+ )
321
294
 
322
- if not (
323
- characteristics := service_proxy.get_characteristics_by_uuid(
295
+ self.audio_output_description = UTF8CharacteristicAdapter(
296
+ service_proxy.get_required_characteristic_by_uuid(
324
297
  GATT_AUDIO_OUTPUT_DESCRIPTION_CHARACTERISTIC
325
298
  )
326
- ):
327
- raise InvalidServiceError(
328
- "Audio Output Description characteristic not found"
329
- )
330
- self.audio_output_description = UTF8CharacteristicAdapter(characteristics[0])
299
+ )