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.
- bumble/_version.py +2 -2
- bumble/apps/auracast.py +626 -87
- bumble/apps/bench.py +227 -148
- bumble/apps/controller_info.py +23 -7
- bumble/apps/device_info.py +50 -4
- bumble/apps/lea_unicast/app.py +61 -201
- bumble/apps/pair.py +13 -8
- bumble/apps/show.py +6 -6
- bumble/att.py +10 -11
- bumble/audio/__init__.py +17 -0
- bumble/audio/io.py +553 -0
- bumble/controller.py +24 -9
- bumble/core.py +4 -1
- bumble/device.py +993 -48
- bumble/drivers/common.py +2 -0
- bumble/drivers/intel.py +593 -24
- bumble/gatt.py +67 -12
- bumble/gatt_client.py +14 -2
- bumble/gatt_server.py +12 -1
- bumble/hci.py +854 -33
- bumble/host.py +363 -64
- bumble/l2cap.py +3 -16
- bumble/pairing.py +3 -0
- bumble/profiles/aics.py +45 -80
- bumble/profiles/ascs.py +6 -18
- bumble/profiles/asha.py +5 -5
- bumble/profiles/bass.py +9 -21
- bumble/profiles/device_information_service.py +4 -1
- bumble/profiles/gatt_service.py +166 -0
- bumble/profiles/gmap.py +193 -0
- bumble/profiles/heart_rate_service.py +5 -6
- bumble/profiles/le_audio.py +87 -4
- bumble/profiles/pacs.py +48 -16
- bumble/profiles/tmap.py +3 -9
- bumble/profiles/{vcp.py → vcs.py} +33 -28
- bumble/profiles/vocs.py +299 -0
- bumble/sdp.py +223 -93
- bumble/smp.py +8 -3
- bumble/tools/intel_fw_download.py +130 -0
- bumble/tools/intel_util.py +154 -0
- bumble/transport/usb.py +8 -2
- bumble/utils.py +22 -7
- bumble/vendor/android/hci.py +29 -4
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/METADATA +12 -10
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/RECORD +49 -43
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/WHEEL +1 -1
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/entry_points.txt +3 -0
- bumble/apps/lea_unicast/liblc3.wasm +0 -0
- {bumble-0.0.203.dist-info → bumble-0.0.207.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
32
|
+
SerializableCharacteristicAdapter,
|
|
33
|
+
PackedCharacteristicAdapter,
|
|
32
34
|
TemplateService,
|
|
33
35
|
CharacteristicValue,
|
|
34
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
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=
|
|
373
|
+
value=self.audio_input_state,
|
|
385
374
|
),
|
|
386
|
-
|
|
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 =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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 =
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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 =
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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
|
-
|
|
469
|
+
service_proxy.get_required_characteristic_by_uuid(
|
|
470
|
+
GATT_AUDIO_INPUT_STATUS_CHARACTERISTIC
|
|
471
|
+
),
|
|
499
472
|
'B',
|
|
500
473
|
)
|
|
501
474
|
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
513
|
-
|
|
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 =
|
|
262
|
-
SOURCE =
|
|
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.
|
|
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
|
-
|
|
515
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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) ->
|
|
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
|
-
|
|
366
|
-
|
|
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,
|
|
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,
|
|
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])
|