bumble 0.0.203__py3-none-any.whl → 0.0.207__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/auracast.py +626 -87
  3. bumble/apps/bench.py +227 -148
  4. bumble/apps/controller_info.py +23 -7
  5. bumble/apps/device_info.py +50 -4
  6. bumble/apps/lea_unicast/app.py +61 -201
  7. bumble/apps/pair.py +13 -8
  8. bumble/apps/show.py +6 -6
  9. bumble/att.py +10 -11
  10. bumble/audio/__init__.py +17 -0
  11. bumble/audio/io.py +553 -0
  12. bumble/controller.py +24 -9
  13. bumble/core.py +4 -1
  14. bumble/device.py +993 -48
  15. bumble/drivers/common.py +2 -0
  16. bumble/drivers/intel.py +593 -24
  17. bumble/gatt.py +67 -12
  18. bumble/gatt_client.py +14 -2
  19. bumble/gatt_server.py +12 -1
  20. bumble/hci.py +854 -33
  21. bumble/host.py +363 -64
  22. bumble/l2cap.py +3 -16
  23. bumble/pairing.py +3 -0
  24. bumble/profiles/aics.py +45 -80
  25. bumble/profiles/ascs.py +6 -18
  26. bumble/profiles/asha.py +5 -5
  27. bumble/profiles/bass.py +9 -21
  28. bumble/profiles/device_information_service.py +4 -1
  29. bumble/profiles/gatt_service.py +166 -0
  30. bumble/profiles/gmap.py +193 -0
  31. bumble/profiles/heart_rate_service.py +5 -6
  32. bumble/profiles/le_audio.py +87 -4
  33. bumble/profiles/pacs.py +48 -16
  34. bumble/profiles/tmap.py +3 -9
  35. bumble/profiles/{vcp.py → vcs.py} +33 -28
  36. bumble/profiles/vocs.py +299 -0
  37. bumble/sdp.py +223 -93
  38. bumble/smp.py +8 -3
  39. bumble/tools/intel_fw_download.py +130 -0
  40. bumble/tools/intel_util.py +154 -0
  41. bumble/transport/usb.py +8 -2
  42. bumble/utils.py +22 -7
  43. bumble/vendor/android/hci.py +29 -4
  44. {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
  45. {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/RECORD +49 -43
  46. {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
  47. {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +3 -0
  48. bumble/apps/lea_unicast/liblc3.wasm +0 -0
  49. {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
  50. {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/top_level.txt +0 -0
bumble/profiles/aics.py CHANGED
@@ -17,6 +17,7 @@
17
17
  # -----------------------------------------------------------------------------
18
18
  # Imports
19
19
  # -----------------------------------------------------------------------------
20
+ from __future__ import annotations
20
21
  import logging
21
22
  import struct
22
23
 
@@ -28,10 +29,11 @@ from bumble.device import Connection
28
29
  from bumble.att import ATT_Error
29
30
  from bumble.gatt import (
30
31
  Characteristic,
31
- DelegatedCharacteristicAdapter,
32
+ SerializableCharacteristicAdapter,
33
+ PackedCharacteristicAdapter,
32
34
  TemplateService,
33
35
  CharacteristicValue,
34
- PackedCharacteristicAdapter,
36
+ UTF8CharacteristicAdapter,
35
37
  GATT_AUDIO_INPUT_CONTROL_SERVICE,
36
38
  GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
37
39
  GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
@@ -154,9 +156,6 @@ class AudioInputState:
154
156
  attribute=self.attribute_value, value=bytes(self)
155
157
  )
156
158
 
157
- def on_read(self, _connection: Optional[Connection]) -> bytes:
158
- return bytes(self)
159
-
160
159
 
161
160
  @dataclass
162
161
  class GainSettingsProperties:
@@ -173,7 +172,7 @@ class GainSettingsProperties:
173
172
  (gain_settings_unit, gain_settings_minimum, gain_settings_maximum) = (
174
173
  struct.unpack('BBB', data)
175
174
  )
176
- GainSettingsProperties(
175
+ return GainSettingsProperties(
177
176
  gain_settings_unit, gain_settings_minimum, gain_settings_maximum
178
177
  )
179
178
 
@@ -186,9 +185,6 @@ class GainSettingsProperties:
186
185
  ]
187
186
  )
188
187
 
189
- def on_read(self, _connection: Optional[Connection]) -> bytes:
190
- return bytes(self)
191
-
192
188
 
193
189
  @dataclass
194
190
  class AudioInputControlPoint:
@@ -321,21 +317,14 @@ class AudioInputDescription:
321
317
  audio_input_description: str = "Bluetooth"
322
318
  attribute_value: Optional[CharacteristicValue] = None
323
319
 
324
- @classmethod
325
- def from_bytes(cls, data: bytes):
326
- return cls(audio_input_description=data.decode('utf-8'))
327
-
328
- def __bytes__(self) -> bytes:
329
- return self.audio_input_description.encode('utf-8')
330
-
331
- def on_read(self, _connection: Optional[Connection]) -> bytes:
332
- return self.audio_input_description.encode('utf-8')
320
+ def on_read(self, _connection: Optional[Connection]) -> str:
321
+ return self.audio_input_description
333
322
 
334
- async def on_write(self, connection: Optional[Connection], value: bytes) -> None:
323
+ async def on_write(self, connection: Optional[Connection], value: str) -> None:
335
324
  assert connection
336
325
  assert self.attribute_value
337
326
 
338
- self.audio_input_description = value.decode('utf-8')
327
+ self.audio_input_description = value
339
328
  await connection.device.notify_subscribers(
340
329
  attribute=self.attribute_value, value=value
341
330
  )
@@ -375,26 +364,29 @@ class AICSService(TemplateService):
375
364
  self.audio_input_state, self.gain_settings_properties
376
365
  )
377
366
 
378
- self.audio_input_state_characteristic = DelegatedCharacteristicAdapter(
367
+ self.audio_input_state_characteristic = SerializableCharacteristicAdapter(
379
368
  Characteristic(
380
369
  uuid=GATT_AUDIO_INPUT_STATE_CHARACTERISTIC,
381
370
  properties=Characteristic.Properties.READ
382
371
  | Characteristic.Properties.NOTIFY,
383
372
  permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
384
- value=CharacteristicValue(read=self.audio_input_state.on_read),
373
+ value=self.audio_input_state,
385
374
  ),
386
- encode=lambda value: bytes(value),
375
+ AudioInputState,
387
376
  )
388
377
  self.audio_input_state.attribute_value = (
389
378
  self.audio_input_state_characteristic.value
390
379
  )
391
380
 
392
- self.gain_settings_properties_characteristic = DelegatedCharacteristicAdapter(
393
- Characteristic(
394
- uuid=GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
395
- properties=Characteristic.Properties.READ,
396
- permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
397
- value=CharacteristicValue(read=self.gain_settings_properties.on_read),
381
+ self.gain_settings_properties_characteristic = (
382
+ SerializableCharacteristicAdapter(
383
+ Characteristic(
384
+ uuid=GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC,
385
+ properties=Characteristic.Properties.READ,
386
+ permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
387
+ value=self.gain_settings_properties,
388
+ ),
389
+ GainSettingsProperties,
398
390
  )
399
391
  )
400
392
 
@@ -402,7 +394,7 @@ class AICSService(TemplateService):
402
394
  uuid=GATT_AUDIO_INPUT_TYPE_CHARACTERISTIC,
403
395
  properties=Characteristic.Properties.READ,
404
396
  permissions=Characteristic.Permissions.READ_REQUIRES_ENCRYPTION,
405
- value=audio_input_type,
397
+ value=bytes(audio_input_type, 'utf-8'),
406
398
  )
407
399
 
408
400
  self.audio_input_status_characteristic = Characteristic(
@@ -412,18 +404,14 @@ class AICSService(TemplateService):
412
404
  value=bytes([self.audio_input_status]),
413
405
  )
414
406
 
415
- self.audio_input_control_point_characteristic = DelegatedCharacteristicAdapter(
416
- Characteristic(
417
- uuid=GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
418
- properties=Characteristic.Properties.WRITE,
419
- permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
420
- value=CharacteristicValue(
421
- write=self.audio_input_control_point.on_write
422
- ),
423
- )
407
+ self.audio_input_control_point_characteristic = Characteristic(
408
+ uuid=GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC,
409
+ properties=Characteristic.Properties.WRITE,
410
+ permissions=Characteristic.Permissions.WRITE_REQUIRES_ENCRYPTION,
411
+ value=CharacteristicValue(write=self.audio_input_control_point.on_write),
424
412
  )
425
413
 
426
- self.audio_input_description_characteristic = DelegatedCharacteristicAdapter(
414
+ self.audio_input_description_characteristic = UTF8CharacteristicAdapter(
427
415
  Characteristic(
428
416
  uuid=GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC,
429
417
  properties=Characteristic.Properties.READ
@@ -463,58 +451,35 @@ class AICSServiceProxy(ProfileServiceProxy):
463
451
  def __init__(self, service_proxy: ServiceProxy) -> None:
464
452
  self.service_proxy = service_proxy
465
453
 
466
- if not (
467
- characteristics := service_proxy.get_characteristics_by_uuid(
454
+ self.audio_input_state = SerializableCharacteristicAdapter(
455
+ service_proxy.get_required_characteristic_by_uuid(
468
456
  GATT_AUDIO_INPUT_STATE_CHARACTERISTIC
469
- )
470
- ):
471
- raise gatt.InvalidServiceError("Audio Input State Characteristic not found")
472
- self.audio_input_state = DelegatedCharacteristicAdapter(
473
- characteristic=characteristics[0], decode=AudioInputState.from_bytes
457
+ ),
458
+ AudioInputState,
474
459
  )
475
460
 
476
- if not (
477
- characteristics := service_proxy.get_characteristics_by_uuid(
461
+ self.gain_settings_properties = SerializableCharacteristicAdapter(
462
+ service_proxy.get_required_characteristic_by_uuid(
478
463
  GATT_GAIN_SETTINGS_ATTRIBUTE_CHARACTERISTIC
479
- )
480
- ):
481
- raise gatt.InvalidServiceError(
482
- "Gain Settings Attribute Characteristic not found"
483
- )
484
- self.gain_settings_properties = PackedCharacteristicAdapter(
485
- characteristics[0],
486
- 'BBB',
464
+ ),
465
+ GainSettingsProperties,
487
466
  )
488
467
 
489
- if not (
490
- characteristics := service_proxy.get_characteristics_by_uuid(
491
- GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC
492
- )
493
- ):
494
- raise gatt.InvalidServiceError(
495
- "Audio Input Status Characteristic not found"
496
- )
497
468
  self.audio_input_status = PackedCharacteristicAdapter(
498
- characteristics[0],
469
+ service_proxy.get_required_characteristic_by_uuid(
470
+ GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC
471
+ ),
499
472
  'B',
500
473
  )
501
474
 
502
- if not (
503
- characteristics := service_proxy.get_characteristics_by_uuid(
475
+ self.audio_input_control_point = (
476
+ service_proxy.get_required_characteristic_by_uuid(
504
477
  GATT_AUDIO_INPUT_CONTROL_POINT_CHARACTERISTIC
505
478
  )
506
- ):
507
- raise gatt.InvalidServiceError(
508
- "Audio Input Control Point Characteristic not found"
509
- )
510
- self.audio_input_control_point = characteristics[0]
479
+ )
511
480
 
512
- if not (
513
- characteristics := service_proxy.get_characteristics_by_uuid(
481
+ self.audio_input_description = UTF8CharacteristicAdapter(
482
+ service_proxy.get_required_characteristic_by_uuid(
514
483
  GATT_AUDIO_INPUT_DESCRIPTION_CHARACTERISTIC
515
484
  )
516
- ):
517
- raise gatt.InvalidServiceError(
518
- "Audio Input Description Characteristic not found"
519
- )
520
- self.audio_input_description = characteristics[0]
485
+ )
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
  # -----------------------------------------------------------------------------
@@ -354,16 +355,7 @@ class AseStateMachine(gatt.Characteristic):
354
355
  cis_link.on('disconnection', self.on_cis_disconnection)
355
356
 
356
357
  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
- )
358
+ await cis_link.setup_data_path(direction=self.role)
367
359
  if self.role == AudioRole.SINK:
368
360
  self.state = self.State.STREAMING
369
361
  await self.service.device.notify_subscribers(self, self.value)
@@ -511,12 +503,8 @@ class AseStateMachine(gatt.Characteristic):
511
503
  self.state = self.State.RELEASING
512
504
 
513
505
  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
- )
506
+ if self.cis_link:
507
+ await self.cis_link.remove_data_path(self.role)
520
508
  self.state = self.State.IDLE
521
509
  await self.service.device.notify_subscribers(self, self.value)
522
510
 
bumble/profiles/asha.py CHANGED
@@ -288,8 +288,8 @@ class AshaServiceProxy(gatt_client.ProfileServiceProxy):
288
288
  'psm_characteristic',
289
289
  ),
290
290
  ):
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])
291
+ setattr(
292
+ self,
293
+ attribute_name,
294
+ self.service_proxy.get_required_characteristic_by_uuid(uuid),
295
+ )
bumble/profiles/bass.py CHANGED
@@ -276,10 +276,7 @@ class BroadcastReceiveState:
276
276
  subgroups: List[SubgroupInfo]
277
277
 
278
278
  @classmethod
279
- def from_bytes(cls, data: bytes) -> Optional[BroadcastReceiveState]:
280
- if not data:
281
- return None
282
-
279
+ def from_bytes(cls, data: bytes) -> BroadcastReceiveState:
283
280
  source_id = data[0]
284
281
  _, source_address = hci.Address.parse_address_preceded_by_type(data, 2)
285
282
  source_adv_sid = data[8]
@@ -362,29 +359,20 @@ class BroadcastAudioScanServiceProxy(gatt_client.ProfileServiceProxy):
362
359
  def __init__(self, service_proxy: gatt_client.ServiceProxy):
363
360
  self.service_proxy = service_proxy
364
361
 
365
- if not (
366
- characteristics := service_proxy.get_characteristics_by_uuid(
362
+ self.broadcast_audio_scan_control_point = (
363
+ service_proxy.get_required_characteristic_by_uuid(
367
364
  gatt.GATT_BROADCAST_AUDIO_SCAN_CONTROL_POINT_CHARACTERISTIC
368
365
  )
369
- ):
370
- raise gatt.InvalidServiceError(
371
- "Broadcast Audio Scan Control Point characteristic not found"
372
- )
373
- self.broadcast_audio_scan_control_point = characteristics[0]
366
+ )
374
367
 
375
- if not (
376
- characteristics := service_proxy.get_characteristics_by_uuid(
377
- gatt.GATT_BROADCAST_RECEIVE_STATE_CHARACTERISTIC
378
- )
379
- ):
380
- raise gatt.InvalidServiceError(
381
- "Broadcast Receive State characteristic not found"
382
- )
383
368
  self.broadcast_receive_states = [
384
369
  gatt.DelegatedCharacteristicAdapter(
385
- characteristic, decode=BroadcastReceiveState.from_bytes
370
+ characteristic,
371
+ decode=lambda x: BroadcastReceiveState.from_bytes(x) if x else None,
372
+ )
373
+ for characteristic in service_proxy.get_characteristics_by_uuid(
374
+ gatt.GATT_BROADCAST_RECEIVE_STATE_CHARACTERISTIC
386
375
  )
387
- for characteristic in characteristics
388
376
  ]
389
377
 
390
378
  async def send_control_point_operation(
@@ -64,7 +64,10 @@ class DeviceInformationService(TemplateService):
64
64
  ):
65
65
  characteristics = [
66
66
  Characteristic(
67
- uuid, Characteristic.Properties.READ, Characteristic.READABLE, field
67
+ uuid,
68
+ Characteristic.Properties.READ,
69
+ Characteristic.READABLE,
70
+ bytes(field, 'utf-8'),
68
71
  )
69
72
  for (field, uuid) in (
70
73
  (manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
@@ -0,0 +1,166 @@
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
+ return (
114
+ struct.pack("<H", attribute.handle)
115
+ + attribute.type.to_bytes()
116
+ + attribute.value
117
+ )
118
+ elif attribute.type in (
119
+ gatt.GATT_CHARACTERISTIC_USER_DESCRIPTION_DESCRIPTOR,
120
+ gatt.GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
121
+ gatt.GATT_SERVER_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR,
122
+ gatt.GATT_CHARACTERISTIC_PRESENTATION_FORMAT_DESCRIPTOR,
123
+ gatt.GATT_CHARACTERISTIC_AGGREGATE_FORMAT_DESCRIPTOR,
124
+ ):
125
+ return struct.pack("<H", attribute.handle) + attribute.type.to_bytes()
126
+
127
+ return b''
128
+
129
+ def get_database_hash(self, connection: device.Connection | None) -> bytes:
130
+ assert connection
131
+
132
+ m = b''.join(
133
+ [
134
+ self.get_attribute_data(attribute)
135
+ for attribute in connection.device.gatt_server.attributes
136
+ ]
137
+ )
138
+
139
+ return crypto.aes_cmac(m=m, k=bytes(16))
140
+
141
+
142
+ class GenericAttributeProfileServiceProxy(gatt_client.ProfileServiceProxy):
143
+ SERVICE_CLASS = GenericAttributeProfileService
144
+
145
+ client_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
146
+ None
147
+ )
148
+ server_supported_features_characteristic: gatt_client.CharacteristicProxy | None = (
149
+ None
150
+ )
151
+ database_hash_characteristic: gatt_client.CharacteristicProxy | None = None
152
+ service_changed_characteristic: gatt_client.CharacteristicProxy | None = None
153
+
154
+ _CHARACTERISTICS = {
155
+ gatt.GATT_CLIENT_SUPPORTED_FEATURES_CHARACTERISTIC: 'client_supported_features_characteristic',
156
+ gatt.GATT_SERVER_SUPPORTED_FEATURES_CHARACTERISTIC: 'server_supported_features_characteristic',
157
+ gatt.GATT_DATABASE_HASH_CHARACTERISTIC: 'database_hash_characteristic',
158
+ gatt.GATT_SERVICE_CHANGED_CHARACTERISTIC: 'service_changed_characteristic',
159
+ }
160
+
161
+ def __init__(self, service_proxy: gatt_client.ServiceProxy) -> None:
162
+ self.service_proxy = service_proxy
163
+
164
+ for uuid, attribute_name in self._CHARACTERISTICS.items():
165
+ if characteristics := self.service_proxy.get_characteristics_by_uuid(uuid):
166
+ setattr(self, attribute_name, characteristics[0])