bumble 0.0.147__py3-none-any.whl → 0.0.149__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/bench.py +4 -2
- bumble/apps/console.py +213 -17
- bumble/apps/gg_bridge.py +3 -4
- bumble/apps/pair.py +21 -4
- bumble/att.py +4 -4
- bumble/device.py +128 -100
- bumble/gap.py +2 -2
- bumble/gatt.py +61 -51
- bumble/gatt_client.py +63 -10
- bumble/gatt_server.py +20 -2
- bumble/host.py +12 -17
- bumble/keys.py +27 -11
- bumble/pairing.py +184 -0
- bumble/profiles/asha_service.py +6 -5
- bumble/profiles/battery_service.py +1 -1
- bumble/profiles/device_information_service.py +5 -3
- bumble/profiles/heart_rate_service.py +3 -3
- bumble/rfcomm.py +1 -1
- bumble/smp.py +21 -88
- {bumble-0.0.147.dist-info → bumble-0.0.149.dist-info}/LICENSE +19 -0
- {bumble-0.0.147.dist-info → bumble-0.0.149.dist-info}/METADATA +3 -1
- {bumble-0.0.147.dist-info → bumble-0.0.149.dist-info}/RECORD +26 -25
- {bumble-0.0.147.dist-info → bumble-0.0.149.dist-info}/WHEEL +0 -0
- {bumble-0.0.147.dist-info → bumble-0.0.149.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.147.dist-info → bumble-0.0.149.dist-info}/top_level.txt +0 -0
bumble/gatt.py
CHANGED
|
@@ -28,7 +28,7 @@ import enum
|
|
|
28
28
|
import functools
|
|
29
29
|
import logging
|
|
30
30
|
import struct
|
|
31
|
-
from typing import Optional, Sequence
|
|
31
|
+
from typing import Optional, Sequence, List
|
|
32
32
|
|
|
33
33
|
from .colors import color
|
|
34
34
|
from .core import UUID, get_dict_key_by_value
|
|
@@ -259,63 +259,68 @@ class Characteristic(Attribute):
|
|
|
259
259
|
See Vol 3, Part G - 3.3 CHARACTERISTIC DEFINITION
|
|
260
260
|
'''
|
|
261
261
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
262
|
+
uuid: UUID
|
|
263
|
+
properties: Characteristic.Properties
|
|
264
|
+
|
|
265
|
+
class Properties(enum.IntFlag):
|
|
266
|
+
"""Property flags"""
|
|
267
|
+
|
|
268
|
+
BROADCAST = 0x01
|
|
269
|
+
READ = 0x02
|
|
270
|
+
WRITE_WITHOUT_RESPONSE = 0x04
|
|
271
|
+
WRITE = 0x08
|
|
272
|
+
NOTIFY = 0x10
|
|
273
|
+
INDICATE = 0x20
|
|
274
|
+
AUTHENTICATED_SIGNED_WRITES = 0x40
|
|
275
|
+
EXTENDED_PROPERTIES = 0x80
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def from_string(properties_str: str) -> Characteristic.Properties:
|
|
279
|
+
property_names: List[str] = []
|
|
280
|
+
for property in Characteristic.Properties:
|
|
281
|
+
if property.name is None:
|
|
282
|
+
raise TypeError()
|
|
283
|
+
property_names.append(property.name)
|
|
284
|
+
|
|
285
|
+
def string_to_property(property_string) -> Characteristic.Properties:
|
|
286
|
+
for property in zip(Characteristic.Properties, property_names):
|
|
287
|
+
if property_string == property[1]:
|
|
288
|
+
return property[0]
|
|
289
|
+
raise TypeError(f"Unable to convert {property_string} to Property")
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
return functools.reduce(
|
|
293
|
+
lambda x, y: x | string_to_property(y),
|
|
294
|
+
properties_str.split(","),
|
|
295
|
+
Characteristic.Properties(0),
|
|
296
|
+
)
|
|
297
|
+
except TypeError:
|
|
298
|
+
raise TypeError(
|
|
299
|
+
f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by commas: {','.join(property_names)}\nGot: {properties_str}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# For backwards compatibility these are defined here
|
|
303
|
+
# For new code, please use Characteristic.Properties.X
|
|
304
|
+
BROADCAST = Properties.BROADCAST
|
|
305
|
+
READ = Properties.READ
|
|
306
|
+
WRITE_WITHOUT_RESPONSE = Properties.WRITE_WITHOUT_RESPONSE
|
|
307
|
+
WRITE = Properties.WRITE
|
|
308
|
+
NOTIFY = Properties.NOTIFY
|
|
309
|
+
INDICATE = Properties.INDICATE
|
|
310
|
+
AUTHENTICATED_SIGNED_WRITES = Properties.AUTHENTICATED_SIGNED_WRITES
|
|
311
|
+
EXTENDED_PROPERTIES = Properties.EXTENDED_PROPERTIES
|
|
304
312
|
|
|
305
313
|
def __init__(
|
|
306
314
|
self,
|
|
307
315
|
uuid,
|
|
308
|
-
properties,
|
|
316
|
+
properties: Characteristic.Properties,
|
|
309
317
|
permissions,
|
|
310
318
|
value=b'',
|
|
311
319
|
descriptors: Sequence[Descriptor] = (),
|
|
312
320
|
):
|
|
313
321
|
super().__init__(uuid, permissions, value)
|
|
314
322
|
self.uuid = self.type
|
|
315
|
-
|
|
316
|
-
self.properties = Characteristic.string_to_properties(properties)
|
|
317
|
-
else:
|
|
318
|
-
self.properties = properties
|
|
323
|
+
self.properties = properties
|
|
319
324
|
self.descriptors = descriptors
|
|
320
325
|
|
|
321
326
|
def get_descriptor(self, descriptor_type):
|
|
@@ -325,12 +330,15 @@ class Characteristic(Attribute):
|
|
|
325
330
|
|
|
326
331
|
return None
|
|
327
332
|
|
|
333
|
+
def has_properties(self, properties: Characteristic.Properties) -> bool:
|
|
334
|
+
return self.properties & properties == properties
|
|
335
|
+
|
|
328
336
|
def __str__(self):
|
|
329
337
|
return (
|
|
330
338
|
f'Characteristic(handle=0x{self.handle:04X}, '
|
|
331
339
|
f'end=0x{self.end_group_handle:04X}, '
|
|
332
340
|
f'uuid={self.uuid}, '
|
|
333
|
-
f'
|
|
341
|
+
f'{self.properties!s})'
|
|
334
342
|
)
|
|
335
343
|
|
|
336
344
|
|
|
@@ -340,6 +348,8 @@ class CharacteristicDeclaration(Attribute):
|
|
|
340
348
|
See Vol 3, Part G - 3.3.1 CHARACTERISTIC DECLARATION
|
|
341
349
|
'''
|
|
342
350
|
|
|
351
|
+
characteristic: Characteristic
|
|
352
|
+
|
|
343
353
|
def __init__(self, characteristic, value_handle):
|
|
344
354
|
declaration_bytes = (
|
|
345
355
|
struct.pack('<BH', characteristic.properties, value_handle)
|
|
@@ -355,8 +365,8 @@ class CharacteristicDeclaration(Attribute):
|
|
|
355
365
|
return (
|
|
356
366
|
f'CharacteristicDeclaration(handle=0x{self.handle:04X}, '
|
|
357
367
|
f'value_handle=0x{self.value_handle:04X}, '
|
|
358
|
-
f'uuid={self.characteristic.uuid},
|
|
359
|
-
f'{
|
|
368
|
+
f'uuid={self.characteristic.uuid}, '
|
|
369
|
+
f'{self.characteristic.properties!s})'
|
|
360
370
|
)
|
|
361
371
|
|
|
362
372
|
|
bumble/gatt_client.py
CHANGED
|
@@ -27,7 +27,8 @@ from __future__ import annotations
|
|
|
27
27
|
import asyncio
|
|
28
28
|
import logging
|
|
29
29
|
import struct
|
|
30
|
-
from
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from typing import List, Optional, Dict, Tuple, Callable, Union, Any
|
|
31
32
|
|
|
32
33
|
from pyee import EventEmitter
|
|
33
34
|
|
|
@@ -62,7 +63,6 @@ from .gatt import (
|
|
|
62
63
|
GATT_PRIMARY_SERVICE_ATTRIBUTE_TYPE,
|
|
63
64
|
GATT_REQUEST_TIMEOUT,
|
|
64
65
|
GATT_SECONDARY_SERVICE_ATTRIBUTE_TYPE,
|
|
65
|
-
Service,
|
|
66
66
|
Characteristic,
|
|
67
67
|
ClientCharacteristicConfigurationBits,
|
|
68
68
|
)
|
|
@@ -139,12 +139,21 @@ class ServiceProxy(AttributeProxy):
|
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
class CharacteristicProxy(AttributeProxy):
|
|
142
|
+
properties: Characteristic.Properties
|
|
142
143
|
descriptors: List[DescriptorProxy]
|
|
144
|
+
subscribers: Dict[Any, Callable]
|
|
143
145
|
|
|
144
|
-
def __init__(
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
client,
|
|
149
|
+
handle,
|
|
150
|
+
end_group_handle,
|
|
151
|
+
uuid,
|
|
152
|
+
properties: int,
|
|
153
|
+
):
|
|
145
154
|
super().__init__(client, handle, end_group_handle, uuid)
|
|
146
155
|
self.uuid = uuid
|
|
147
|
-
self.properties = properties
|
|
156
|
+
self.properties = Characteristic.Properties(properties)
|
|
148
157
|
self.descriptors = []
|
|
149
158
|
self.descriptors_discovered = False
|
|
150
159
|
self.subscribers = {} # Map from subscriber to proxy subscriber
|
|
@@ -159,7 +168,9 @@ class CharacteristicProxy(AttributeProxy):
|
|
|
159
168
|
async def discover_descriptors(self):
|
|
160
169
|
return await self.client.discover_descriptors(self)
|
|
161
170
|
|
|
162
|
-
async def subscribe(
|
|
171
|
+
async def subscribe(
|
|
172
|
+
self, subscriber: Optional[Callable] = None, prefer_notify=True
|
|
173
|
+
):
|
|
163
174
|
if subscriber is not None:
|
|
164
175
|
if subscriber in self.subscribers:
|
|
165
176
|
# We already have a proxy subscriber
|
|
@@ -186,7 +197,7 @@ class CharacteristicProxy(AttributeProxy):
|
|
|
186
197
|
return (
|
|
187
198
|
f'Characteristic(handle=0x{self.handle:04X}, '
|
|
188
199
|
f'uuid={self.uuid}, '
|
|
189
|
-
f'
|
|
200
|
+
f'{self.properties!s})'
|
|
190
201
|
)
|
|
191
202
|
|
|
192
203
|
|
|
@@ -213,6 +224,7 @@ class ProfileServiceProxy:
|
|
|
213
224
|
# -----------------------------------------------------------------------------
|
|
214
225
|
class Client:
|
|
215
226
|
services: List[ServiceProxy]
|
|
227
|
+
cached_values: Dict[int, Tuple[datetime, bytes]]
|
|
216
228
|
|
|
217
229
|
def __init__(self, connection):
|
|
218
230
|
self.connection = connection
|
|
@@ -225,6 +237,7 @@ class Client:
|
|
|
225
237
|
) # Notification subscribers, by attribute handle
|
|
226
238
|
self.indication_subscribers = {} # Indication subscribers, by attribute handle
|
|
227
239
|
self.services = []
|
|
240
|
+
self.cached_values = {}
|
|
228
241
|
|
|
229
242
|
def send_gatt_pdu(self, pdu):
|
|
230
243
|
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
|
@@ -309,6 +322,35 @@ class Client:
|
|
|
309
322
|
if c.uuid == uuid
|
|
310
323
|
]
|
|
311
324
|
|
|
325
|
+
def get_attribute_grouping(
|
|
326
|
+
self, attribute_handle: int
|
|
327
|
+
) -> Optional[
|
|
328
|
+
Union[
|
|
329
|
+
ServiceProxy,
|
|
330
|
+
Tuple[ServiceProxy, CharacteristicProxy],
|
|
331
|
+
Tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
|
|
332
|
+
]
|
|
333
|
+
]:
|
|
334
|
+
"""
|
|
335
|
+
Get the attribute(s) associated with an attribute handle
|
|
336
|
+
"""
|
|
337
|
+
for service in self.services:
|
|
338
|
+
if service.handle == attribute_handle:
|
|
339
|
+
return service
|
|
340
|
+
if service.handle <= attribute_handle <= service.end_group_handle:
|
|
341
|
+
for characteristic in service.characteristics:
|
|
342
|
+
if characteristic.handle == attribute_handle:
|
|
343
|
+
return (service, characteristic)
|
|
344
|
+
if (
|
|
345
|
+
characteristic.handle
|
|
346
|
+
<= attribute_handle
|
|
347
|
+
<= characteristic.end_group_handle
|
|
348
|
+
):
|
|
349
|
+
for descriptor in characteristic.descriptors:
|
|
350
|
+
if descriptor.handle == attribute_handle:
|
|
351
|
+
return (service, characteristic, descriptor)
|
|
352
|
+
return None
|
|
353
|
+
|
|
312
354
|
def on_service_discovered(self, service):
|
|
313
355
|
'''Add a service to the service list if it wasn't already there'''
|
|
314
356
|
already_known = False
|
|
@@ -678,8 +720,8 @@ class Client:
|
|
|
678
720
|
return
|
|
679
721
|
|
|
680
722
|
if (
|
|
681
|
-
characteristic.properties & Characteristic.NOTIFY
|
|
682
|
-
and characteristic.properties & Characteristic.INDICATE
|
|
723
|
+
characteristic.properties & Characteristic.Properties.NOTIFY
|
|
724
|
+
and characteristic.properties & Characteristic.Properties.INDICATE
|
|
683
725
|
):
|
|
684
726
|
if prefer_notify:
|
|
685
727
|
bits = ClientCharacteristicConfigurationBits.NOTIFICATION
|
|
@@ -687,10 +729,10 @@ class Client:
|
|
|
687
729
|
else:
|
|
688
730
|
bits = ClientCharacteristicConfigurationBits.INDICATION
|
|
689
731
|
subscribers = self.indication_subscribers
|
|
690
|
-
elif characteristic.properties & Characteristic.NOTIFY:
|
|
732
|
+
elif characteristic.properties & Characteristic.Properties.NOTIFY:
|
|
691
733
|
bits = ClientCharacteristicConfigurationBits.NOTIFICATION
|
|
692
734
|
subscribers = self.notification_subscribers
|
|
693
|
-
elif characteristic.properties & Characteristic.INDICATE:
|
|
735
|
+
elif characteristic.properties & Characteristic.Properties.INDICATE:
|
|
694
736
|
bits = ClientCharacteristicConfigurationBits.INDICATION
|
|
695
737
|
subscribers = self.indication_subscribers
|
|
696
738
|
else:
|
|
@@ -800,6 +842,7 @@ class Client:
|
|
|
800
842
|
|
|
801
843
|
offset += len(part)
|
|
802
844
|
|
|
845
|
+
self.cache_value(attribute_handle, attribute_value)
|
|
803
846
|
# Return the value as bytes
|
|
804
847
|
return attribute_value
|
|
805
848
|
|
|
@@ -934,6 +977,8 @@ class Client:
|
|
|
934
977
|
)
|
|
935
978
|
if not subscribers:
|
|
936
979
|
logger.warning('!!! received notification with no subscriber')
|
|
980
|
+
|
|
981
|
+
self.cache_value(notification.attribute_handle, notification.attribute_value)
|
|
937
982
|
for subscriber in subscribers:
|
|
938
983
|
if callable(subscriber):
|
|
939
984
|
subscriber(notification.attribute_value)
|
|
@@ -945,6 +990,8 @@ class Client:
|
|
|
945
990
|
subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
|
|
946
991
|
if not subscribers:
|
|
947
992
|
logger.warning('!!! received indication with no subscriber')
|
|
993
|
+
|
|
994
|
+
self.cache_value(indication.attribute_handle, indication.attribute_value)
|
|
948
995
|
for subscriber in subscribers:
|
|
949
996
|
if callable(subscriber):
|
|
950
997
|
subscriber(indication.attribute_value)
|
|
@@ -953,3 +1000,9 @@ class Client:
|
|
|
953
1000
|
|
|
954
1001
|
# Confirm that we received the indication
|
|
955
1002
|
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
|
1003
|
+
|
|
1004
|
+
def cache_value(self, attribute_handle: int, value: bytes):
|
|
1005
|
+
self.cached_values[attribute_handle] = (
|
|
1006
|
+
datetime.now(),
|
|
1007
|
+
value,
|
|
1008
|
+
)
|
bumble/gatt_server.py
CHANGED
|
@@ -27,7 +27,7 @@ import asyncio
|
|
|
27
27
|
import logging
|
|
28
28
|
from collections import defaultdict
|
|
29
29
|
import struct
|
|
30
|
-
from typing import List, Tuple, Optional
|
|
30
|
+
from typing import List, Tuple, Optional, TypeVar, Type
|
|
31
31
|
from pyee import EventEmitter
|
|
32
32
|
|
|
33
33
|
from .colors import color
|
|
@@ -135,6 +135,21 @@ class Server(EventEmitter):
|
|
|
135
135
|
return attribute
|
|
136
136
|
return None
|
|
137
137
|
|
|
138
|
+
AttributeGroupType = TypeVar('AttributeGroupType', Service, Characteristic)
|
|
139
|
+
|
|
140
|
+
def get_attribute_group(
|
|
141
|
+
self, handle: int, group_type: Type[AttributeGroupType]
|
|
142
|
+
) -> Optional[AttributeGroupType]:
|
|
143
|
+
return next(
|
|
144
|
+
(
|
|
145
|
+
attribute
|
|
146
|
+
for attribute in self.attributes
|
|
147
|
+
if isinstance(attribute, group_type)
|
|
148
|
+
and attribute.handle <= handle <= attribute.end_group_handle
|
|
149
|
+
),
|
|
150
|
+
None,
|
|
151
|
+
)
|
|
152
|
+
|
|
138
153
|
def get_service_attribute(self, service_uuid: UUID) -> Optional[Service]:
|
|
139
154
|
return next(
|
|
140
155
|
(
|
|
@@ -228,7 +243,10 @@ class Server(EventEmitter):
|
|
|
228
243
|
# unless there is one already
|
|
229
244
|
if (
|
|
230
245
|
characteristic.properties
|
|
231
|
-
& (
|
|
246
|
+
& (
|
|
247
|
+
Characteristic.Properties.NOTIFY
|
|
248
|
+
| Characteristic.Properties.INDICATE
|
|
249
|
+
)
|
|
232
250
|
and characteristic.get_descriptor(
|
|
233
251
|
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
|
|
234
252
|
)
|
bumble/host.py
CHANGED
|
@@ -94,10 +94,9 @@ HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1
|
|
|
94
94
|
|
|
95
95
|
# -----------------------------------------------------------------------------
|
|
96
96
|
class Connection:
|
|
97
|
-
def __init__(self, host, handle,
|
|
97
|
+
def __init__(self, host, handle, peer_address, transport):
|
|
98
98
|
self.host = host
|
|
99
99
|
self.handle = handle
|
|
100
|
-
self.role = role
|
|
101
100
|
self.peer_address = peer_address
|
|
102
101
|
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
|
103
102
|
self.transport = transport
|
|
@@ -396,8 +395,8 @@ class Host(AbortableEventEmitter):
|
|
|
396
395
|
|
|
397
396
|
def supports_command(self, command):
|
|
398
397
|
# Find the support flag position for this command
|
|
399
|
-
for
|
|
400
|
-
for
|
|
398
|
+
for octet, flags in enumerate(HCI_SUPPORTED_COMMANDS_FLAGS):
|
|
399
|
+
for flag_position, value in enumerate(flags):
|
|
401
400
|
if value == command:
|
|
402
401
|
# Check if the flag is set
|
|
403
402
|
if octet < len(self.local_supported_commands) and flag_position < 8:
|
|
@@ -410,7 +409,7 @@ class Host(AbortableEventEmitter):
|
|
|
410
409
|
@property
|
|
411
410
|
def supported_commands(self):
|
|
412
411
|
commands = []
|
|
413
|
-
for
|
|
412
|
+
for octet, flags in enumerate(self.local_supported_commands):
|
|
414
413
|
if octet < len(HCI_SUPPORTED_COMMANDS_FLAGS):
|
|
415
414
|
for flag in range(8):
|
|
416
415
|
if flags & (1 << flag) != 0:
|
|
@@ -534,7 +533,7 @@ class Host(AbortableEventEmitter):
|
|
|
534
533
|
if event.status == HCI_SUCCESS:
|
|
535
534
|
# Create/update the connection
|
|
536
535
|
logger.debug(
|
|
537
|
-
f'### CONNECTION: [0x{event.connection_handle:04X}] '
|
|
536
|
+
f'### LE CONNECTION: [0x{event.connection_handle:04X}] '
|
|
538
537
|
f'{event.peer_address} as {HCI_Constant.role_name(event.role)}'
|
|
539
538
|
)
|
|
540
539
|
|
|
@@ -543,7 +542,6 @@ class Host(AbortableEventEmitter):
|
|
|
543
542
|
connection = Connection(
|
|
544
543
|
self,
|
|
545
544
|
event.connection_handle,
|
|
546
|
-
event.role,
|
|
547
545
|
event.peer_address,
|
|
548
546
|
BT_LE_TRANSPORT,
|
|
549
547
|
)
|
|
@@ -560,7 +558,6 @@ class Host(AbortableEventEmitter):
|
|
|
560
558
|
event.connection_handle,
|
|
561
559
|
BT_LE_TRANSPORT,
|
|
562
560
|
event.peer_address,
|
|
563
|
-
None,
|
|
564
561
|
event.role,
|
|
565
562
|
connection_parameters,
|
|
566
563
|
)
|
|
@@ -589,7 +586,6 @@ class Host(AbortableEventEmitter):
|
|
|
589
586
|
connection = Connection(
|
|
590
587
|
self,
|
|
591
588
|
event.connection_handle,
|
|
592
|
-
BT_CENTRAL_ROLE,
|
|
593
589
|
event.bd_addr,
|
|
594
590
|
BT_BR_EDR_TRANSPORT,
|
|
595
591
|
)
|
|
@@ -602,7 +598,6 @@ class Host(AbortableEventEmitter):
|
|
|
602
598
|
BT_BR_EDR_TRANSPORT,
|
|
603
599
|
event.bd_addr,
|
|
604
600
|
None,
|
|
605
|
-
BT_CENTRAL_ROLE,
|
|
606
601
|
None,
|
|
607
602
|
)
|
|
608
603
|
else:
|
|
@@ -622,8 +617,7 @@ class Host(AbortableEventEmitter):
|
|
|
622
617
|
if event.status == HCI_SUCCESS:
|
|
623
618
|
logger.debug(
|
|
624
619
|
f'### DISCONNECTION: [0x{event.connection_handle:04X}] '
|
|
625
|
-
f'{connection.peer_address}
|
|
626
|
-
f'{HCI_Constant.role_name(connection.role)}, '
|
|
620
|
+
f'{connection.peer_address} '
|
|
627
621
|
f'reason={event.reason}'
|
|
628
622
|
)
|
|
629
623
|
del self.connections[event.connection_handle]
|
|
@@ -739,10 +733,6 @@ class Host(AbortableEventEmitter):
|
|
|
739
733
|
f'role change for {event.bd_addr}: '
|
|
740
734
|
f'{HCI_Constant.role_name(event.new_role)}'
|
|
741
735
|
)
|
|
742
|
-
if connection := self.find_connection_by_bd_addr(
|
|
743
|
-
event.bd_addr, BT_BR_EDR_TRANSPORT
|
|
744
|
-
):
|
|
745
|
-
connection.role = event.new_role
|
|
746
736
|
self.emit('role_change', event.bd_addr, event.new_role)
|
|
747
737
|
else:
|
|
748
738
|
logger.debug(
|
|
@@ -849,7 +839,12 @@ class Host(AbortableEventEmitter):
|
|
|
849
839
|
self.emit('authentication_io_capability_request', event.bd_addr)
|
|
850
840
|
|
|
851
841
|
def on_hci_io_capability_response_event(self, event):
|
|
852
|
-
|
|
842
|
+
self.emit(
|
|
843
|
+
'authentication_io_capability_response',
|
|
844
|
+
event.bd_addr,
|
|
845
|
+
event.io_capability,
|
|
846
|
+
event.authentication_requirements,
|
|
847
|
+
)
|
|
853
848
|
|
|
854
849
|
def on_hci_user_confirmation_request_event(self, event):
|
|
855
850
|
self.emit(
|
bumble/keys.py
CHANGED
|
@@ -20,15 +20,19 @@
|
|
|
20
20
|
# -----------------------------------------------------------------------------
|
|
21
21
|
# Imports
|
|
22
22
|
# -----------------------------------------------------------------------------
|
|
23
|
+
from __future__ import annotations
|
|
23
24
|
import asyncio
|
|
24
25
|
import logging
|
|
25
26
|
import os
|
|
26
27
|
import json
|
|
27
|
-
from typing import Optional
|
|
28
|
+
from typing import TYPE_CHECKING, Optional
|
|
28
29
|
|
|
29
30
|
from .colors import color
|
|
30
31
|
from .hci import Address
|
|
31
32
|
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from .device import Device
|
|
35
|
+
|
|
32
36
|
|
|
33
37
|
# -----------------------------------------------------------------------------
|
|
34
38
|
# Logging
|
|
@@ -173,13 +177,13 @@ class KeyStore:
|
|
|
173
177
|
separator = '\n'
|
|
174
178
|
|
|
175
179
|
@staticmethod
|
|
176
|
-
def create_for_device(
|
|
177
|
-
if
|
|
180
|
+
def create_for_device(device: Device) -> Optional[KeyStore]:
|
|
181
|
+
if device.config.keystore is None:
|
|
178
182
|
return None
|
|
179
183
|
|
|
180
|
-
keystore_type =
|
|
184
|
+
keystore_type = device.config.keystore.split(':', 1)[0]
|
|
181
185
|
if keystore_type == 'JsonKeyStore':
|
|
182
|
-
return JsonKeyStore.
|
|
186
|
+
return JsonKeyStore.from_device(device)
|
|
183
187
|
|
|
184
188
|
return None
|
|
185
189
|
|
|
@@ -204,7 +208,9 @@ class JsonKeyStore(KeyStore):
|
|
|
204
208
|
self.directory_name = os.path.join(
|
|
205
209
|
appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR), self.KEYS_DIR
|
|
206
210
|
)
|
|
207
|
-
json_filename =
|
|
211
|
+
json_filename = (
|
|
212
|
+
f'{self.namespace}.json'.lower().replace(':', '-').replace('/p', '-p')
|
|
213
|
+
)
|
|
208
214
|
self.filename = os.path.join(self.directory_name, json_filename)
|
|
209
215
|
else:
|
|
210
216
|
self.filename = filename
|
|
@@ -213,9 +219,19 @@ class JsonKeyStore(KeyStore):
|
|
|
213
219
|
logger.debug(f'JSON keystore: {self.filename}')
|
|
214
220
|
|
|
215
221
|
@staticmethod
|
|
216
|
-
def
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
def from_device(device: Device) -> Optional[JsonKeyStore]:
|
|
223
|
+
if not device.config.keystore:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
params = device.config.keystore.split(':', 1)[1:]
|
|
227
|
+
|
|
228
|
+
# Use a namespace based on the device address
|
|
229
|
+
if device.public_address not in (Address.ANY, Address.ANY_RANDOM):
|
|
230
|
+
namespace = str(device.public_address)
|
|
231
|
+
elif device.random_address != Address.ANY_RANDOM:
|
|
232
|
+
namespace = str(device.random_address)
|
|
233
|
+
else:
|
|
234
|
+
namespace = JsonKeyStore.DEFAULT_NAMESPACE
|
|
219
235
|
if params:
|
|
220
236
|
filename = params[0]
|
|
221
237
|
else:
|
|
@@ -241,7 +257,7 @@ class JsonKeyStore(KeyStore):
|
|
|
241
257
|
json.dump(db, output, sort_keys=True, indent=4)
|
|
242
258
|
|
|
243
259
|
# Atomically replace the previous file
|
|
244
|
-
os.
|
|
260
|
+
os.replace(temp_filename, self.filename)
|
|
245
261
|
|
|
246
262
|
async def delete(self, name: str) -> None:
|
|
247
263
|
db = await self.load()
|
|
@@ -257,7 +273,7 @@ class JsonKeyStore(KeyStore):
|
|
|
257
273
|
db = await self.load()
|
|
258
274
|
|
|
259
275
|
namespace = db.setdefault(self.namespace, {})
|
|
260
|
-
namespace
|
|
276
|
+
namespace.setdefault(name, {}).update(keys.to_dict())
|
|
261
277
|
|
|
262
278
|
await self.save(db)
|
|
263
279
|
|