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
bumble/pandora/host.py CHANGED
@@ -371,9 +371,7 @@ class HostService(HostServicer):
371
371
  scan_response_data=scan_response_data,
372
372
  )
373
373
 
374
- pending_connection: asyncio.Future[bumble.device.Connection] = (
375
- asyncio.get_running_loop().create_future()
376
- )
374
+ connections: asyncio.Queue[bumble.device.Connection] = asyncio.Queue()
377
375
 
378
376
  if request.connectable:
379
377
 
@@ -382,7 +380,7 @@ class HostService(HostServicer):
382
380
  connection.transport == BT_LE_TRANSPORT
383
381
  and connection.role == BT_PERIPHERAL_ROLE
384
382
  ):
385
- pending_connection.set_result(connection)
383
+ connections.put_nowait(connection)
386
384
 
387
385
  self.device.on('connection', on_connection)
388
386
 
@@ -397,8 +395,7 @@ class HostService(HostServicer):
397
395
  await asyncio.sleep(1)
398
396
  continue
399
397
 
400
- connection = await pending_connection
401
- pending_connection = asyncio.get_running_loop().create_future()
398
+ connection = await connections.get()
402
399
 
403
400
  cookie = any_pb2.Any(value=connection.handle.to_bytes(4, 'big'))
404
401
  yield AdvertiseResponse(connection=Connection(cookie=cookie))
@@ -492,6 +489,8 @@ class HostService(HostServicer):
492
489
  target = Address(target_bytes, Address.RANDOM_DEVICE_ADDRESS)
493
490
  advertising_type = AdvertisingType.DIRECTED_CONNECTABLE_LOW_DUTY
494
491
 
492
+ connections: asyncio.Queue[bumble.device.Connection] = asyncio.Queue()
493
+
495
494
  if request.connectable:
496
495
 
497
496
  def on_connection(connection: bumble.device.Connection) -> None:
@@ -499,7 +498,7 @@ class HostService(HostServicer):
499
498
  connection.transport == BT_LE_TRANSPORT
500
499
  and connection.role == BT_PERIPHERAL_ROLE
501
500
  ):
502
- pending_connection.set_result(connection)
501
+ connections.put_nowait(connection)
503
502
 
504
503
  self.device.on('connection', on_connection)
505
504
 
@@ -517,12 +516,8 @@ class HostService(HostServicer):
517
516
  await asyncio.sleep(1)
518
517
  continue
519
518
 
520
- pending_connection: asyncio.Future[bumble.device.Connection] = (
521
- asyncio.get_running_loop().create_future()
522
- )
523
-
524
519
  self.log.debug('Wait for LE connection...')
525
- connection = await pending_connection
520
+ connection = await connections.get()
526
521
 
527
522
  self.log.debug(
528
523
  f"Advertise: Connected to {connection.peer_address} (handle={connection.handle})"
bumble/profiles/aics.py CHANGED
@@ -24,16 +24,13 @@ import struct
24
24
  from dataclasses import dataclass
25
25
  from typing import Optional
26
26
 
27
- from bumble import gatt
28
27
  from bumble.device import Connection
29
28
  from bumble.att import ATT_Error
30
29
  from bumble.gatt import (
30
+ Attribute,
31
31
  Characteristic,
32
- SerializableCharacteristicAdapter,
33
- PackedCharacteristicAdapter,
34
32
  TemplateService,
35
33
  CharacteristicValue,
36
- UTF8CharacteristicAdapter,
37
34
  GATT_AUDIO_INPUT_CONTROL_SERVICE,
38
35
  GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
39
36
  GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
@@ -42,6 +39,14 @@ from bumble.gatt import (
42
39
  GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
43
40
  GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
44
41
  )
42
+ from bumble.gatt_adapters import (
43
+ CharacteristicProxy,
44
+ PackedCharacteristicProxyAdapter,
45
+ SerializableCharacteristicAdapter,
46
+ SerializableCharacteristicProxyAdapter,
47
+ UTF8CharacteristicAdapter,
48
+ UTF8CharacteristicProxyAdapter,
49
+ )
45
50
  from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
46
51
  from bumble.utils import OpenIntEnum
47
52
 
@@ -124,7 +129,7 @@ class AudioInputState:
124
129
  mute: Mute = Mute.NOT_MUTED
125
130
  gain_mode: GainMode = GainMode.MANUAL
126
131
  change_counter: int = 0
127
- attribute_value: Optional[CharacteristicValue] = None
132
+ attribute: Optional[Attribute] = None
128
133
 
129
134
  def __bytes__(self) -> bytes:
130
135
  return bytes(
@@ -151,10 +156,8 @@ class AudioInputState:
151
156
  self.change_counter = (self.change_counter + 1) % (CHANGE_COUNTER_MAX_VALUE + 1)
152
157
 
153
158
  async def notify_subscribers_via_connection(self, connection: Connection) -> None:
154
- assert self.attribute_value is not None
155
- await connection.device.notify_subscribers(
156
- attribute=self.attribute_value, value=bytes(self)
157
- )
159
+ assert self.attribute is not None
160
+ await connection.device.notify_subscribers(attribute=self.attribute)
158
161
 
159
162
 
160
163
  @dataclass
@@ -315,24 +318,28 @@ class AudioInputDescription:
315
318
  '''
316
319
 
317
320
  audio_input_description: str = "Bluetooth"
318
- attribute_value: Optional[CharacteristicValue] = None
321
+ attribute: Optional[Attribute] = None
319
322
 
320
323
  def on_read(self, _connection: Optional[Connection]) -> str:
321
324
  return self.audio_input_description
322
325
 
323
326
  async def on_write(self, connection: Optional[Connection], value: str) -> None:
324
327
  assert connection
325
- assert self.attribute_value
328
+ assert self.attribute
326
329
 
327
330
  self.audio_input_description = value
328
- await connection.device.notify_subscribers(
329
- attribute=self.attribute_value, value=value
330
- )
331
+ await connection.device.notify_subscribers(attribute=self.attribute)
331
332
 
332
333
 
333
334
  class AICSService(TemplateService):
334
335
  UUID = GATT_AUDIO_INPUT_CONTROL_SERVICE
335
336
 
337
+ audio_input_state_characteristic: Characteristic[AudioInputState]
338
+ audio_input_type_characteristic: Characteristic[bytes]
339
+ audio_input_status_characteristic: Characteristic[bytes]
340
+ audio_input_control_point_characteristic: Characteristic[bytes]
341
+ gain_settings_properties_characteristic: Characteristic[GainSettingsProperties]
342
+
336
343
  def __init__(
337
344
  self,
338
345
  audio_input_state: Optional[AudioInputState] = None,
@@ -374,9 +381,7 @@ class AICSService(TemplateService):
374
381
  ),
375
382
  AudioInputState,
376
383
  )
377
- self.audio_input_state.attribute_value = (
378
- self.audio_input_state_characteristic.value
379
- )
384
+ self.audio_input_state.attribute = self.audio_input_state_characteristic
380
385
 
381
386
  self.gain_settings_properties_characteristic = (
382
387
  SerializableCharacteristicAdapter(
@@ -425,8 +430,8 @@ class AICSService(TemplateService):
425
430
  ),
426
431
  )
427
432
  )
428
- self.audio_input_description.attribute_value = (
429
- self.audio_input_control_point_characteristic.value
433
+ self.audio_input_description.attribute = (
434
+ self.audio_input_control_point_characteristic
430
435
  )
431
436
 
432
437
  super().__init__(
@@ -448,57 +453,43 @@ class AICSService(TemplateService):
448
453
  class AICSServiceProxy(ProfileServiceProxy):
449
454
  SERVICE_CLASS = AICSService
450
455
 
456
+ audio_input_state: CharacteristicProxy[AudioInputState]
457
+ gain_settings_properties: CharacteristicProxy[GainSettingsProperties]
458
+ audio_input_status: CharacteristicProxy[int]
459
+ audio_input_control_point: CharacteristicProxy[bytes]
460
+
451
461
  def __init__(self, service_proxy: ServiceProxy) -> None:
452
462
  self.service_proxy = service_proxy
453
463
 
454
- if not (
455
- characteristics := service_proxy.get_characteristics_by_uuid(
464
+ self.audio_input_state = SerializableCharacteristicProxyAdapter(
465
+ service_proxy.get_required_characteristic_by_uuid(
456
466
  GATT_AUDIO_INPUT_STATE_CHARACTERISTIC
457
- )
458
- ):
459
- raise gatt.InvalidServiceError("Audio Input State Characteristic not found")
460
- self.audio_input_state = SerializableCharacteristicAdapter(
461
- characteristics[0], AudioInputState
467
+ ),
468
+ AudioInputState,
462
469
  )
463
470
 
464
- if not (
465
- characteristics := service_proxy.get_characteristics_by_uuid(
471
+ self.gain_settings_properties = SerializableCharacteristicProxyAdapter(
472
+ service_proxy.get_required_characteristic_by_uuid(
466
473
  GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC
467
- )
468
- ):
469
- raise gatt.InvalidServiceError(
470
- "Gain Settings Attribute Characteristic not found"
471
- )
472
- self.gain_settings_properties = SerializableCharacteristicAdapter(
473
- characteristics[0], GainSettingsProperties
474
+ ),
475
+ GainSettingsProperties,
474
476
  )
475
477
 
476
- if not (
477
- characteristics := service_proxy.get_characteristics_by_uuid(
478
+ self.audio_input_status = PackedCharacteristicProxyAdapter(
479
+ service_proxy.get_required_characteristic_by_uuid(
478
480
  GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC
479
- )
480
- ):
481
- raise gatt.InvalidServiceError(
482
- "Audio Input Status Characteristic not found"
483
- )
484
- self.audio_input_status = PackedCharacteristicAdapter(characteristics[0], 'B')
481
+ ),
482
+ 'B',
483
+ )
485
484
 
486
- if not (
487
- characteristics := service_proxy.get_characteristics_by_uuid(
485
+ self.audio_input_control_point = (
486
+ service_proxy.get_required_characteristic_by_uuid(
488
487
  GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC
489
488
  )
490
- ):
491
- raise gatt.InvalidServiceError(
492
- "Audio Input Control Point Characteristic not found"
493
- )
494
- self.audio_input_control_point = characteristics[0]
489
+ )
495
490
 
496
- if not (
497
- characteristics := service_proxy.get_characteristics_by_uuid(
491
+ self.audio_input_description = UTF8CharacteristicProxyAdapter(
492
+ service_proxy.get_required_characteristic_by_uuid(
498
493
  GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC
499
494
  )
500
- ):
501
- raise gatt.InvalidServiceError(
502
- "Audio Input Description Characteristic not found"
503
- )
504
- self.audio_input_description = UTF8CharacteristicAdapter(characteristics[0])
495
+ )
bumble/profiles/ascs.py CHANGED
@@ -17,6 +17,7 @@
17
17
  # Imports
18
18
  # -----------------------------------------------------------------------------
19
19
  from __future__ import annotations
20
+
20
21
  import enum
21
22
  import logging
22
23
  import struct
@@ -258,8 +259,8 @@ class AseReasonCode(enum.IntEnum):
258
259
 
259
260
  # -----------------------------------------------------------------------------
260
261
  class AudioRole(enum.IntEnum):
261
- SINK = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.CONTROLLER_TO_HOST
262
- SOURCE = hci.HCI_LE_Setup_ISO_Data_Path_Command.Direction.HOST_TO_CONTROLLER
262
+ SINK = device.CisLink.Direction.CONTROLLER_TO_HOST
263
+ SOURCE = device.CisLink.Direction.HOST_TO_CONTROLLER
263
264
 
264
265
 
265
266
  # -----------------------------------------------------------------------------
@@ -300,7 +301,7 @@ class AseStateMachine(gatt.Characteristic):
300
301
  presentation_delay = 0
301
302
 
302
303
  # Additional parameters in ENABLING, STREAMING, DISABLING State
303
- metadata = le_audio.Metadata()
304
+ metadata: le_audio.Metadata
304
305
 
305
306
  def __init__(
306
307
  self,
@@ -312,6 +313,7 @@ class AseStateMachine(gatt.Characteristic):
312
313
  self.ase_id = ase_id
313
314
  self._state = AseStateMachine.State.IDLE
314
315
  self.role = role
316
+ self.metadata = le_audio.Metadata()
315
317
 
316
318
  uuid = (
317
319
  gatt.GATT_SINK_ASE_CHARACTERISTIC
@@ -354,16 +356,7 @@ class AseStateMachine(gatt.Characteristic):
354
356
  cis_link.on('disconnection', self.on_cis_disconnection)
355
357
 
356
358
  async def post_cis_established():
357
- await self.service.device.send_command(
358
- hci.HCI_LE_Setup_ISO_Data_Path_Command(
359
- connection_handle=cis_link.handle,
360
- data_path_direction=self.role,
361
- data_path_id=0x00, # Fixed HCI
362
- codec_id=hci.CodingFormat(hci.CodecID.TRANSPARENT),
363
- controller_delay=0,
364
- codec_configuration=b'',
365
- )
366
- )
359
+ await cis_link.setup_data_path(direction=self.role)
367
360
  if self.role == AudioRole.SINK:
368
361
  self.state = self.State.STREAMING
369
362
  await self.service.device.notify_subscribers(self, self.value)
@@ -511,12 +504,8 @@ class AseStateMachine(gatt.Characteristic):
511
504
  self.state = self.State.RELEASING
512
505
 
513
506
  async def remove_cis_async():
514
- await self.service.device.send_command(
515
- hci.HCI_LE_Remove_ISO_Data_Path_Command(
516
- connection_handle=self.cis_link.handle,
517
- data_path_direction=self.role,
518
- )
519
- )
507
+ if self.cis_link:
508
+ await self.cis_link.remove_data_path(self.role)
520
509
  self.state = self.State.IDLE
521
510
  await self.service.device.notify_subscribers(self, self.value)
522
511
 
bumble/profiles/asha.py CHANGED
@@ -134,12 +134,14 @@ class AshaService(gatt.TemplateService):
134
134
  ),
135
135
  )
136
136
 
137
- self.audio_control_point_characteristic = gatt.Characteristic(
138
- gatt.GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
139
- gatt.Characteristic.Properties.WRITE
140
- | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
141
- gatt.Characteristic.WRITEABLE,
142
- gatt.CharacteristicValue(write=self._on_audio_control_point_write),
137
+ self.audio_control_point_characteristic: gatt.Characteristic[bytes] = (
138
+ gatt.Characteristic(
139
+ gatt.GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
140
+ gatt.Characteristic.Properties.WRITE
141
+ | gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
142
+ gatt.Characteristic.WRITEABLE,
143
+ gatt.CharacteristicValue(write=self._on_audio_control_point_write),
144
+ )
143
145
  )
144
146
  self.audio_status_characteristic = gatt.Characteristic(
145
147
  gatt.GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC,
@@ -147,7 +149,7 @@ class AshaService(gatt.TemplateService):
147
149
  gatt.Characteristic.READABLE,
148
150
  bytes([AudioStatus.OK]),
149
151
  )
150
- self.volume_characteristic = gatt.Characteristic(
152
+ self.volume_characteristic: gatt.Characteristic[bytes] = gatt.Characteristic(
151
153
  gatt.GATT_ASHA_VOLUME_CHARACTERISTIC,
152
154
  gatt.Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
153
155
  gatt.Characteristic.WRITEABLE,
@@ -166,13 +168,13 @@ class AshaService(gatt.TemplateService):
166
168
  struct.pack('<H', self.psm),
167
169
  )
168
170
 
169
- characteristics = [
171
+ characteristics = (
170
172
  self.read_only_properties_characteristic,
171
173
  self.audio_control_point_characteristic,
172
174
  self.audio_status_characteristic,
173
175
  self.volume_characteristic,
174
176
  self.le_psm_out_characteristic,
175
- ]
177
+ )
176
178
 
177
179
  super().__init__(characteristics)
178
180
 
@@ -288,8 +290,8 @@ class AshaServiceProxy(gatt_client.ProfileServiceProxy):
288
290
  'psm_characteristic',
289
291
  ),
290
292
  ):
291
- if not (
292
- characteristics := self.service_proxy.get_characteristics_by_uuid(uuid)
293
- ):
294
- raise gatt.InvalidServiceError(f"Missing {uuid} Characteristic")
295
- setattr(self, attribute_name, characteristics[0])
293
+ setattr(
294
+ self,
295
+ attribute_name,
296
+ self.service_proxy.get_required_characteristic_by_uuid(uuid),
297
+ )
bumble/profiles/bass.py CHANGED
@@ -20,11 +20,12 @@ from __future__ import annotations
20
20
  import dataclasses
21
21
  import logging
22
22
  import struct
23
- from typing import ClassVar, List, Optional, Sequence
23
+ from typing import ClassVar, Optional, Sequence
24
24
 
25
25
  from bumble import core
26
26
  from bumble import device
27
27
  from bumble import gatt
28
+ from bumble import gatt_adapters
28
29
  from bumble import gatt_client
29
30
  from bumble import hci
30
31
  from bumble import utils
@@ -52,7 +53,7 @@ def encode_subgroups(subgroups: Sequence[SubgroupInfo]) -> bytes:
52
53
  )
53
54
 
54
55
 
55
- def decode_subgroups(data: bytes) -> List[SubgroupInfo]:
56
+ def decode_subgroups(data: bytes) -> list[SubgroupInfo]:
56
57
  num_subgroups = data[0]
57
58
  offset = 1
58
59
  subgroups = []
@@ -273,7 +274,7 @@ class BroadcastReceiveState:
273
274
  pa_sync_state: PeriodicAdvertisingSyncState
274
275
  big_encryption: BigEncryption
275
276
  bad_code: bytes
276
- subgroups: List[SubgroupInfo]
277
+ subgroups: list[SubgroupInfo]
277
278
 
278
279
  @classmethod
279
280
  def from_bytes(cls, data: bytes) -> BroadcastReceiveState:
@@ -354,34 +355,27 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
354
355
  SERVICE_CLASS = BroadcastAudioScanService
355
356
 
356
357
  broadcast_audio_scan_control_point: gatt_client.CharacteristicProxy
357
- broadcast_receive_states: List[gatt.SerializableCharacteristicAdapter]
358
+ broadcast_receive_states: list[
359
+ gatt_client.CharacteristicProxy[Optional[BroadcastReceiveState]]
360
+ ]
358
361
 
359
362
  def __init__(self, service_proxy: gatt_client.ServiceProxy):
360
363
  self.service_proxy = service_proxy
361
364
 
362
- if not (
363
- characteristics := service_proxy.get_characteristics_by_uuid(
365
+ self.broadcast_audio_scan_control_point = (
366
+ service_proxy.get_required_characteristic_by_uuid(
364
367
  gatt.GATT_BROADCAST_AUDIO_SCAN_CONTROL_POINT_CHARACTERISTIC
365
368
  )
366
- ):
367
- raise gatt.InvalidServiceError(
368
- "Broadcast Audio Scan Control Point characteristic not found"
369
- )
370
- self.broadcast_audio_scan_control_point = characteristics[0]
369
+ )
371
370
 
372
- if not (
373
- characteristics := service_proxy.get_characteristics_by_uuid(
374
- gatt.GATT_BROADCAST_RECEIVE_STATE_CHARACTERISTIC
375
- )
376
- ):
377
- raise gatt.InvalidServiceError(
378
- "Broadcast Receive State characteristic not found"
379
- )
380
371
  self.broadcast_receive_states = [
381
- gatt.SerializableCharacteristicAdapter(
382
- characteristic, BroadcastReceiveState
372
+ gatt_adapters.DelegatedCharacteristicProxyAdapter(
373
+ characteristic,
374
+ decode=lambda x: BroadcastReceiveState.from_bytes(x) if x else None,
375
+ )
376
+ for characteristic in service_proxy.get_characteristics_by_uuid(
377
+ gatt.GATT_BROADCAST_RECEIVE_STATE_CHARACTERISTIC
383
378
  )
384
- for characteristic in characteristics
385
379
  ]
386
380
 
387
381
  async def send_control_point_operation(
@@ -16,14 +16,20 @@
16
16
  # -----------------------------------------------------------------------------
17
17
  # Imports
18
18
  # -----------------------------------------------------------------------------
19
- from ..gatt_client import ProfileServiceProxy
20
- from ..gatt import (
19
+ from typing import Optional
20
+
21
+ from bumble.gatt_client import ProfileServiceProxy
22
+ from bumble.gatt import (
21
23
  GATT_BATTERY_SERVICE,
22
24
  GATT_BATTERY_LEVEL_CHARACTERISTIC,
23
25
  TemplateService,
24
26
  Characteristic,
25
27
  CharacteristicValue,
28
+ )
29
+ from bumble.gatt_client import CharacteristicProxy
30
+ from bumble.gatt_adapters import (
26
31
  PackedCharacteristicAdapter,
32
+ PackedCharacteristicProxyAdapter,
27
33
  )
28
34
 
29
35
 
@@ -32,6 +38,8 @@ class BatteryService(TemplateService):
32
38
  UUID = GATT_BATTERY_SERVICE
33
39
  BATTERY_LEVEL_FORMAT = 'B'
34
40
 
41
+ battery_level_characteristic: Characteristic[int]
42
+
35
43
  def __init__(self, read_battery_level):
36
44
  self.battery_level_characteristic = PackedCharacteristicAdapter(
37
45
  Characteristic(
@@ -49,13 +57,15 @@ class BatteryService(TemplateService):
49
57
  class BatteryServiceProxy(ProfileServiceProxy):
50
58
  SERVICE_CLASS = BatteryService
51
59
 
60
+ battery_level: Optional[CharacteristicProxy[int]]
61
+
52
62
  def __init__(self, service_proxy):
53
63
  self.service_proxy = service_proxy
54
64
 
55
65
  if characteristics := service_proxy.get_characteristics_by_uuid(
56
66
  GATT_BATTERY_LEVEL_CHARACTERISTIC
57
67
  ):
58
- self.battery_level = PackedCharacteristicAdapter(
68
+ self.battery_level = PackedCharacteristicProxyAdapter(
59
69
  characteristics[0], pack_format=BatteryService.BATTERY_LEVEL_FORMAT
60
70
  )
61
71
  else:
@@ -19,7 +19,6 @@
19
19
  import struct
20
20
  from typing import Optional, Tuple
21
21
 
22
- from bumble.gatt_client import ServiceProxy, ProfileServiceProxy, CharacteristicProxy
23
22
  from bumble.gatt import (
24
23
  GATT_DEVICE_INFORMATION_SERVICE,
25
24
  GATT_FIRMWARE_REVISION_STRING_CHARACTERISTIC,
@@ -32,9 +31,12 @@ from bumble.gatt import (
32
31
  GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
33
32
  TemplateService,
34
33
  Characteristic,
35
- DelegatedCharacteristicAdapter,
36
- UTF8CharacteristicAdapter,
37
34
  )
35
+ from bumble.gatt_adapters import (
36
+ DelegatedCharacteristicProxyAdapter,
37
+ UTF8CharacteristicProxyAdapter,
38
+ )
39
+ from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
38
40
 
39
41
 
40
42
  # -----------------------------------------------------------------------------
@@ -62,7 +64,7 @@ class DeviceInformationService(TemplateService):
62
64
  ieee_regulatory_certification_data_list: Optional[bytes] = None,
63
65
  # TODO: pnp_id
64
66
  ):
65
- characteristics = [
67
+ characteristics: list[Characteristic[bytes]] = [
66
68
  Characteristic(
67
69
  uuid,
68
70
  Characteristic.Properties.READ,
@@ -107,14 +109,14 @@ class DeviceInformationService(TemplateService):
107
109
  class DeviceInformationServiceProxy(ProfileServiceProxy):
108
110
  SERVICE_CLASS = DeviceInformationService
109
111
 
110
- manufacturer_name: Optional[UTF8CharacteristicAdapter]
111
- model_number: Optional[UTF8CharacteristicAdapter]
112
- serial_number: Optional[UTF8CharacteristicAdapter]
113
- hardware_revision: Optional[UTF8CharacteristicAdapter]
114
- firmware_revision: Optional[UTF8CharacteristicAdapter]
115
- software_revision: Optional[UTF8CharacteristicAdapter]
116
- system_id: Optional[DelegatedCharacteristicAdapter]
117
- ieee_regulatory_certification_data_list: Optional[CharacteristicProxy]
112
+ manufacturer_name: Optional[CharacteristicProxy[str]]
113
+ model_number: Optional[CharacteristicProxy[str]]
114
+ serial_number: Optional[CharacteristicProxy[str]]
115
+ hardware_revision: Optional[CharacteristicProxy[str]]
116
+ firmware_revision: Optional[CharacteristicProxy[str]]
117
+ software_revision: Optional[CharacteristicProxy[str]]
118
+ system_id: Optional[CharacteristicProxy[tuple[int, int]]]
119
+ ieee_regulatory_certification_data_list: Optional[CharacteristicProxy[bytes]]
118
120
 
119
121
  def __init__(self, service_proxy: ServiceProxy):
120
122
  self.service_proxy = service_proxy
@@ -128,7 +130,7 @@ class DeviceInformationServiceProxy(ProfileServiceProxy):
128
130
  ('software_revision', GATT_SOFTWARE_REVISION_STRING_CHARACTERISTIC),
129
131
  ):
130
132
  if characteristics := service_proxy.get_characteristics_by_uuid(uuid):
131
- characteristic = UTF8CharacteristicAdapter(characteristics[0])
133
+ characteristic = UTF8CharacteristicProxyAdapter(characteristics[0])
132
134
  else:
133
135
  characteristic = None
134
136
  self.__setattr__(field, characteristic)
@@ -136,7 +138,7 @@ class DeviceInformationServiceProxy(ProfileServiceProxy):
136
138
  if characteristics := service_proxy.get_characteristics_by_uuid(
137
139
  GATT_SYSTEM_ID_CHARACTERISTIC
138
140
  ):
139
- self.system_id = DelegatedCharacteristicAdapter(
141
+ self.system_id = DelegatedCharacteristicProxyAdapter(
140
142
  characteristics[0],
141
143
  encode=lambda v: DeviceInformationService.pack_system_id(*v),
142
144
  decode=DeviceInformationService.unpack_system_id,
bumble/profiles/gap.py CHANGED
@@ -25,14 +25,15 @@ from bumble.core import Appearance
25
25
  from bumble.gatt import (
26
26
  TemplateService,
27
27
  Characteristic,
28
- CharacteristicAdapter,
29
- DelegatedCharacteristicAdapter,
30
- UTF8CharacteristicAdapter,
31
28
  GATT_GENERIC_ACCESS_SERVICE,
32
29
  GATT_DEVICE_NAME_CHARACTERISTIC,
33
30
  GATT_APPEARANCE_CHARACTERISTIC,
34
31
  )
35
- from bumble.gatt_client import ProfileServiceProxy, ServiceProxy
32
+ from bumble.gatt_adapters import (
33
+ DelegatedCharacteristicProxyAdapter,
34
+ UTF8CharacteristicProxyAdapter,
35
+ )
36
+ from bumble.gatt_client import CharacteristicProxy, ProfileServiceProxy, ServiceProxy
36
37
 
37
38
  # -----------------------------------------------------------------------------
38
39
  # Logging
@@ -49,6 +50,9 @@ logger = logging.getLogger(__name__)
49
50
  class GenericAccessService(TemplateService):
50
51
  UUID = GATT_GENERIC_ACCESS_SERVICE
51
52
 
53
+ device_name_characteristic: Characteristic[bytes]
54
+ appearance_characteristic: Characteristic[bytes]
55
+
52
56
  def __init__(
53
57
  self, device_name: str, appearance: Union[Appearance, Tuple[int, int], int] = 0
54
58
  ):
@@ -84,8 +88,8 @@ class GenericAccessService(TemplateService):
84
88
  class GenericAccessServiceProxy(ProfileServiceProxy):
85
89
  SERVICE_CLASS = GenericAccessService
86
90
 
87
- device_name: Optional[CharacteristicAdapter]
88
- appearance: Optional[DelegatedCharacteristicAdapter]
91
+ device_name: Optional[CharacteristicProxy[str]]
92
+ appearance: Optional[CharacteristicProxy[Appearance]]
89
93
 
90
94
  def __init__(self, service_proxy: ServiceProxy):
91
95
  self.service_proxy = service_proxy
@@ -93,14 +97,14 @@ class GenericAccessServiceProxy(ProfileServiceProxy):
93
97
  if characteristics := service_proxy.get_characteristics_by_uuid(
94
98
  GATT_DEVICE_NAME_CHARACTERISTIC
95
99
  ):
96
- self.device_name = UTF8CharacteristicAdapter(characteristics[0])
100
+ self.device_name = UTF8CharacteristicProxyAdapter(characteristics[0])
97
101
  else:
98
102
  self.device_name = None
99
103
 
100
104
  if characteristics := service_proxy.get_characteristics_by_uuid(
101
105
  GATT_APPEARANCE_CHARACTERISTIC
102
106
  ):
103
- self.appearance = DelegatedCharacteristicAdapter(
107
+ self.appearance = DelegatedCharacteristicProxyAdapter(
104
108
  characteristics[0],
105
109
  decode=lambda value: Appearance.from_int(
106
110
  struct.unpack_from('<H', value, 0)[0],