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/_version.py
CHANGED
bumble/apps/bench.py
CHANGED
|
@@ -558,11 +558,13 @@ class GattServer:
|
|
|
558
558
|
# Setup the GATT service
|
|
559
559
|
self.speed_tx = Characteristic(
|
|
560
560
|
SPEED_TX_UUID,
|
|
561
|
-
Characteristic.WRITE,
|
|
561
|
+
Characteristic.Properties.WRITE,
|
|
562
562
|
Characteristic.WRITEABLE,
|
|
563
563
|
CharacteristicValue(write=self.on_tx_write),
|
|
564
564
|
)
|
|
565
|
-
self.speed_rx = Characteristic(
|
|
565
|
+
self.speed_rx = Characteristic(
|
|
566
|
+
SPEED_RX_UUID, Characteristic.Properties.NOTIFY, 0
|
|
567
|
+
)
|
|
566
568
|
|
|
567
569
|
speed_service = Service(
|
|
568
570
|
SPEED_SERVICE_UUID,
|
bumble/apps/console.py
CHANGED
|
@@ -24,10 +24,12 @@ import logging
|
|
|
24
24
|
import os
|
|
25
25
|
import random
|
|
26
26
|
import re
|
|
27
|
-
|
|
27
|
+
import humanize
|
|
28
|
+
from typing import Optional, Union
|
|
28
29
|
from collections import OrderedDict
|
|
29
30
|
|
|
30
31
|
import click
|
|
32
|
+
from prettytable import PrettyTable
|
|
31
33
|
|
|
32
34
|
from prompt_toolkit import Application
|
|
33
35
|
from prompt_toolkit.history import FileHistory
|
|
@@ -125,7 +127,8 @@ class ConsoleApp:
|
|
|
125
127
|
|
|
126
128
|
def __init__(self):
|
|
127
129
|
self.known_addresses = set()
|
|
128
|
-
self.
|
|
130
|
+
self.known_remote_attributes = []
|
|
131
|
+
self.known_local_attributes = []
|
|
129
132
|
self.device = None
|
|
130
133
|
self.connected_peer = None
|
|
131
134
|
self.top_tab = 'device'
|
|
@@ -162,6 +165,8 @@ class ConsoleApp:
|
|
|
162
165
|
'device': None,
|
|
163
166
|
'local-services': None,
|
|
164
167
|
'remote-services': None,
|
|
168
|
+
'local-values': None,
|
|
169
|
+
'remote-values': None,
|
|
165
170
|
},
|
|
166
171
|
'filter': {
|
|
167
172
|
'address': None,
|
|
@@ -172,10 +177,11 @@ class ConsoleApp:
|
|
|
172
177
|
'disconnect': None,
|
|
173
178
|
'discover': {'services': None, 'attributes': None},
|
|
174
179
|
'request-mtu': None,
|
|
175
|
-
'read': LiveCompleter(self.
|
|
176
|
-
'write': LiveCompleter(self.
|
|
177
|
-
'
|
|
178
|
-
'
|
|
180
|
+
'read': LiveCompleter(self.known_remote_attributes),
|
|
181
|
+
'write': LiveCompleter(self.known_remote_attributes),
|
|
182
|
+
'local-write': LiveCompleter(self.known_local_attributes),
|
|
183
|
+
'subscribe': LiveCompleter(self.known_remote_attributes),
|
|
184
|
+
'unsubscribe': LiveCompleter(self.known_remote_attributes),
|
|
179
185
|
'set-phy': {'1m': None, '2m': None, 'coded': None},
|
|
180
186
|
'set-default-phy': None,
|
|
181
187
|
'quit': None,
|
|
@@ -207,6 +213,8 @@ class ConsoleApp:
|
|
|
207
213
|
self.log_text = FormattedTextControl(
|
|
208
214
|
get_cursor_position=lambda: Point(0, max(0, len(self.log_lines) - 1))
|
|
209
215
|
)
|
|
216
|
+
self.local_values_text = FormattedTextControl()
|
|
217
|
+
self.remote_values_text = FormattedTextControl()
|
|
210
218
|
self.log_height = Dimension(min=7, weight=4)
|
|
211
219
|
self.log_max_lines = 100
|
|
212
220
|
self.log_lines = []
|
|
@@ -221,10 +229,18 @@ class ConsoleApp:
|
|
|
221
229
|
Frame(Window(self.local_services_text), title='Local Services'),
|
|
222
230
|
filter=Condition(lambda: self.top_tab == 'local-services'),
|
|
223
231
|
),
|
|
232
|
+
ConditionalContainer(
|
|
233
|
+
Frame(Window(self.local_values_text), title='Local Values'),
|
|
234
|
+
filter=Condition(lambda: self.top_tab == 'local-values'),
|
|
235
|
+
),
|
|
224
236
|
ConditionalContainer(
|
|
225
237
|
Frame(Window(self.remote_services_text), title='Remote Services'),
|
|
226
238
|
filter=Condition(lambda: self.top_tab == 'remote-services'),
|
|
227
239
|
),
|
|
240
|
+
ConditionalContainer(
|
|
241
|
+
Frame(Window(self.remote_values_text), title='Remote Values'),
|
|
242
|
+
filter=Condition(lambda: self.top_tab == 'remote-values'),
|
|
243
|
+
),
|
|
228
244
|
ConditionalContainer(
|
|
229
245
|
Frame(Window(self.log_text, height=self.log_height), title='Log'),
|
|
230
246
|
filter=Condition(lambda: self.top_tab == 'log'),
|
|
@@ -366,17 +382,19 @@ class ConsoleApp:
|
|
|
366
382
|
|
|
367
383
|
def show_remote_services(self, services):
|
|
368
384
|
lines = []
|
|
369
|
-
del self.
|
|
385
|
+
del self.known_remote_attributes[:]
|
|
370
386
|
for service in services:
|
|
371
387
|
lines.append(("ansicyan", f"{service}\n"))
|
|
372
388
|
|
|
373
389
|
for characteristic in service.characteristics:
|
|
374
390
|
lines.append(('ansimagenta', f' {characteristic} + \n'))
|
|
375
|
-
self.
|
|
391
|
+
self.known_remote_attributes.append(
|
|
376
392
|
f'{service.uuid.to_hex_str()}.{characteristic.uuid.to_hex_str()}'
|
|
377
393
|
)
|
|
378
|
-
self.
|
|
379
|
-
|
|
394
|
+
self.known_remote_attributes.append(
|
|
395
|
+
f'*.{characteristic.uuid.to_hex_str()}'
|
|
396
|
+
)
|
|
397
|
+
self.known_remote_attributes.append(f'#{characteristic.handle:X}')
|
|
380
398
|
for descriptor in characteristic.descriptors:
|
|
381
399
|
lines.append(("ansigreen", f" {descriptor}\n"))
|
|
382
400
|
|
|
@@ -385,12 +403,31 @@ class ConsoleApp:
|
|
|
385
403
|
|
|
386
404
|
def show_local_services(self, attributes):
|
|
387
405
|
lines = []
|
|
406
|
+
del self.known_local_attributes[:]
|
|
388
407
|
for attribute in attributes:
|
|
389
408
|
if isinstance(attribute, Service):
|
|
409
|
+
# Save the most recent service for use later
|
|
410
|
+
service = attribute
|
|
390
411
|
lines.append(("ansicyan", f"{attribute}\n"))
|
|
391
|
-
elif isinstance(attribute,
|
|
412
|
+
elif isinstance(attribute, Characteristic):
|
|
413
|
+
# CharacteristicDeclaration includes all info from Characteristic
|
|
414
|
+
# no need to print it twice
|
|
415
|
+
continue
|
|
416
|
+
elif isinstance(attribute, CharacteristicDeclaration):
|
|
417
|
+
# Save the most recent characteristic declaration for use later
|
|
418
|
+
characteristic_declaration = attribute
|
|
419
|
+
self.known_local_attributes.append(
|
|
420
|
+
f'{service.uuid.to_hex_str()}.{attribute.characteristic.uuid.to_hex_str()}'
|
|
421
|
+
)
|
|
422
|
+
self.known_local_attributes.append(
|
|
423
|
+
f'#{attribute.characteristic.handle:X}'
|
|
424
|
+
)
|
|
392
425
|
lines.append(("ansimagenta", f" {attribute}\n"))
|
|
393
426
|
elif isinstance(attribute, Descriptor):
|
|
427
|
+
self.known_local_attributes.append(
|
|
428
|
+
f'{service.uuid.to_hex_str()}.{characteristic_declaration.characteristic.uuid.to_hex_str()}.{attribute.type.to_hex_str()}'
|
|
429
|
+
)
|
|
430
|
+
self.known_local_attributes.append(f'#{attribute.handle:X}')
|
|
394
431
|
lines.append(("ansigreen", f" {attribute}\n"))
|
|
395
432
|
else:
|
|
396
433
|
lines.append(("ansiyellow", f"{attribute}\n"))
|
|
@@ -494,7 +531,7 @@ class ConsoleApp:
|
|
|
494
531
|
|
|
495
532
|
self.show_attributes(attributes)
|
|
496
533
|
|
|
497
|
-
def
|
|
534
|
+
def find_remote_characteristic(self, param) -> Optional[CharacteristicProxy]:
|
|
498
535
|
if not self.connected_peer:
|
|
499
536
|
return None
|
|
500
537
|
parts = param.split('.')
|
|
@@ -516,6 +553,38 @@ class ConsoleApp:
|
|
|
516
553
|
|
|
517
554
|
return None
|
|
518
555
|
|
|
556
|
+
def find_local_attribute(
|
|
557
|
+
self, param
|
|
558
|
+
) -> Optional[Union[Characteristic, Descriptor]]:
|
|
559
|
+
parts = param.split('.')
|
|
560
|
+
if len(parts) == 3:
|
|
561
|
+
service_uuid = UUID(parts[0])
|
|
562
|
+
characteristic_uuid = UUID(parts[1])
|
|
563
|
+
descriptor_uuid = UUID(parts[2])
|
|
564
|
+
return self.device.gatt_server.get_descriptor_attribute(
|
|
565
|
+
service_uuid, characteristic_uuid, descriptor_uuid
|
|
566
|
+
)
|
|
567
|
+
if len(parts) == 2:
|
|
568
|
+
service_uuid = UUID(parts[0])
|
|
569
|
+
characteristic_uuid = UUID(parts[1])
|
|
570
|
+
characteristic_attributes = (
|
|
571
|
+
self.device.gatt_server.get_characteristic_attributes(
|
|
572
|
+
service_uuid, characteristic_uuid
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
if characteristic_attributes:
|
|
576
|
+
return characteristic_attributes[1]
|
|
577
|
+
return None
|
|
578
|
+
elif len(parts) == 1:
|
|
579
|
+
if parts[0].startswith('#'):
|
|
580
|
+
attribute_handle = int(f'{parts[0][1:]}', 16)
|
|
581
|
+
attribute = self.device.gatt_server.get_attribute(attribute_handle)
|
|
582
|
+
if isinstance(attribute, (Characteristic, Descriptor)):
|
|
583
|
+
return attribute
|
|
584
|
+
return None
|
|
585
|
+
|
|
586
|
+
return None
|
|
587
|
+
|
|
519
588
|
async def rssi_monitor_loop(self):
|
|
520
589
|
while True:
|
|
521
590
|
if self.monitor_rssi and self.connected_peer:
|
|
@@ -674,10 +743,109 @@ class ConsoleApp:
|
|
|
674
743
|
'device',
|
|
675
744
|
'local-services',
|
|
676
745
|
'remote-services',
|
|
746
|
+
'local-values',
|
|
747
|
+
'remote-values',
|
|
677
748
|
}:
|
|
678
749
|
self.top_tab = params[0]
|
|
679
750
|
self.ui.invalidate()
|
|
680
751
|
|
|
752
|
+
while self.top_tab == 'local-values':
|
|
753
|
+
await self.do_show_local_values()
|
|
754
|
+
await asyncio.sleep(1)
|
|
755
|
+
|
|
756
|
+
while self.top_tab == 'remote-values':
|
|
757
|
+
await self.do_show_remote_values()
|
|
758
|
+
await asyncio.sleep(1)
|
|
759
|
+
|
|
760
|
+
async def do_show_local_values(self):
|
|
761
|
+
prettytable = PrettyTable()
|
|
762
|
+
field_names = ["Service", "Characteristic", "Descriptor"]
|
|
763
|
+
|
|
764
|
+
# if there's no connections, add a column just for value
|
|
765
|
+
if not self.device.connections:
|
|
766
|
+
field_names.append("Value")
|
|
767
|
+
|
|
768
|
+
# if there are connections, add a column for each connection's value
|
|
769
|
+
for connection in self.device.connections.values():
|
|
770
|
+
field_names.append(f"Connection {connection.handle}")
|
|
771
|
+
|
|
772
|
+
for attribute in self.device.gatt_server.attributes:
|
|
773
|
+
if isinstance(attribute, Characteristic):
|
|
774
|
+
service = self.device.gatt_server.get_attribute_group(
|
|
775
|
+
attribute.handle, Service
|
|
776
|
+
)
|
|
777
|
+
if not service:
|
|
778
|
+
continue
|
|
779
|
+
values = [
|
|
780
|
+
attribute.read_value(connection)
|
|
781
|
+
for connection in self.device.connections.values()
|
|
782
|
+
]
|
|
783
|
+
if not values:
|
|
784
|
+
values = [attribute.read_value(None)]
|
|
785
|
+
prettytable.add_row([f"{service.uuid}", attribute.uuid, ""] + values)
|
|
786
|
+
|
|
787
|
+
elif isinstance(attribute, Descriptor):
|
|
788
|
+
service = self.device.gatt_server.get_attribute_group(
|
|
789
|
+
attribute.handle, Service
|
|
790
|
+
)
|
|
791
|
+
if not service:
|
|
792
|
+
continue
|
|
793
|
+
characteristic = self.device.gatt_server.get_attribute_group(
|
|
794
|
+
attribute.handle, Characteristic
|
|
795
|
+
)
|
|
796
|
+
if not characteristic:
|
|
797
|
+
continue
|
|
798
|
+
values = [
|
|
799
|
+
attribute.read_value(connection)
|
|
800
|
+
for connection in self.device.connections.values()
|
|
801
|
+
]
|
|
802
|
+
if not values:
|
|
803
|
+
values = [attribute.read_value(None)]
|
|
804
|
+
|
|
805
|
+
# TODO: future optimization: convert CCCD value to human readable string
|
|
806
|
+
|
|
807
|
+
prettytable.add_row(
|
|
808
|
+
[service.uuid, characteristic.uuid, attribute.type] + values
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
prettytable.field_names = field_names
|
|
812
|
+
self.local_values_text.text = prettytable.get_string()
|
|
813
|
+
self.ui.invalidate()
|
|
814
|
+
|
|
815
|
+
async def do_show_remote_values(self):
|
|
816
|
+
prettytable = PrettyTable(
|
|
817
|
+
field_names=[
|
|
818
|
+
"Connection",
|
|
819
|
+
"Service",
|
|
820
|
+
"Characteristic",
|
|
821
|
+
"Descriptor",
|
|
822
|
+
"Time",
|
|
823
|
+
"Value",
|
|
824
|
+
]
|
|
825
|
+
)
|
|
826
|
+
for connection in self.device.connections.values():
|
|
827
|
+
for handle, (time, value) in connection.gatt_client.cached_values.items():
|
|
828
|
+
row = [connection.handle]
|
|
829
|
+
attribute = connection.gatt_client.get_attributes(handle)
|
|
830
|
+
if not attribute:
|
|
831
|
+
continue
|
|
832
|
+
if len(attribute) == 3:
|
|
833
|
+
row.extend(
|
|
834
|
+
[attribute[0].uuid, attribute[1].uuid, attribute[2].type]
|
|
835
|
+
)
|
|
836
|
+
elif len(attribute) == 2:
|
|
837
|
+
row.extend([attribute[0].uuid, attribute[1].uuid, ""])
|
|
838
|
+
elif len(attribute) == 1:
|
|
839
|
+
row.extend([attribute[0].uuid, "", ""])
|
|
840
|
+
else:
|
|
841
|
+
continue
|
|
842
|
+
|
|
843
|
+
row.extend([humanize.naturaltime(time), value])
|
|
844
|
+
prettytable.add_row(row)
|
|
845
|
+
|
|
846
|
+
self.remote_values_text.text = prettytable.get_string()
|
|
847
|
+
self.ui.invalidate()
|
|
848
|
+
|
|
681
849
|
async def do_get_phy(self, _):
|
|
682
850
|
if not self.connected_peer:
|
|
683
851
|
self.show_error('not connected')
|
|
@@ -720,7 +888,7 @@ class ConsoleApp:
|
|
|
720
888
|
self.show_error('not connected')
|
|
721
889
|
return
|
|
722
890
|
|
|
723
|
-
characteristic = self.
|
|
891
|
+
characteristic = self.find_remote_characteristic(params[0])
|
|
724
892
|
if characteristic is None:
|
|
725
893
|
self.show_error('no such characteristic')
|
|
726
894
|
return
|
|
@@ -745,15 +913,43 @@ class ConsoleApp:
|
|
|
745
913
|
except ValueError:
|
|
746
914
|
value = str.encode(params[1]) # must be a string
|
|
747
915
|
|
|
748
|
-
characteristic = self.
|
|
916
|
+
characteristic = self.find_remote_characteristic(params[0])
|
|
749
917
|
if characteristic is None:
|
|
750
918
|
self.show_error('no such characteristic')
|
|
751
919
|
return
|
|
752
920
|
|
|
753
921
|
# use write with response if supported
|
|
754
|
-
with_response = characteristic.properties & Characteristic.WRITE
|
|
922
|
+
with_response = characteristic.properties & Characteristic.Properties.WRITE
|
|
755
923
|
await characteristic.write_value(value, with_response=with_response)
|
|
756
924
|
|
|
925
|
+
async def do_local_write(self, params):
|
|
926
|
+
if len(params) != 2:
|
|
927
|
+
self.show_error(
|
|
928
|
+
'invalid syntax', 'expected local-write <attribute> <value>'
|
|
929
|
+
)
|
|
930
|
+
return
|
|
931
|
+
|
|
932
|
+
if params[1].upper().startswith("0X"):
|
|
933
|
+
value = bytes.fromhex(params[1][2:]) # parse as hex string
|
|
934
|
+
else:
|
|
935
|
+
try:
|
|
936
|
+
value = int(params[1]).to_bytes(2, "little") # try as 2 byte integer
|
|
937
|
+
except ValueError:
|
|
938
|
+
value = str.encode(params[1]) # must be a string
|
|
939
|
+
|
|
940
|
+
attribute = self.find_local_attribute(params[0])
|
|
941
|
+
if not attribute:
|
|
942
|
+
self.show_error('invalid syntax', 'unable to find attribute')
|
|
943
|
+
return
|
|
944
|
+
|
|
945
|
+
# send data to any subscribers
|
|
946
|
+
if isinstance(attribute, Characteristic):
|
|
947
|
+
attribute.write_value(None, value)
|
|
948
|
+
if attribute.has_properties(Characteristic.NOTIFY):
|
|
949
|
+
await self.device.gatt_server.notify_subscribers(attribute)
|
|
950
|
+
if attribute.has_properties(Characteristic.INDICATE):
|
|
951
|
+
await self.device.gatt_server.indicate_subscribers(attribute)
|
|
952
|
+
|
|
757
953
|
async def do_subscribe(self, params):
|
|
758
954
|
if not self.connected_peer:
|
|
759
955
|
self.show_error('not connected')
|
|
@@ -763,7 +959,7 @@ class ConsoleApp:
|
|
|
763
959
|
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
|
764
960
|
return
|
|
765
961
|
|
|
766
|
-
characteristic = self.
|
|
962
|
+
characteristic = self.find_remote_characteristic(params[0])
|
|
767
963
|
if characteristic is None:
|
|
768
964
|
self.show_error('no such characteristic')
|
|
769
965
|
return
|
|
@@ -783,7 +979,7 @@ class ConsoleApp:
|
|
|
783
979
|
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
|
784
980
|
return
|
|
785
981
|
|
|
786
|
-
characteristic = self.
|
|
982
|
+
characteristic = self.find_remote_characteristic(params[0])
|
|
787
983
|
if characteristic is None:
|
|
788
984
|
self.show_error('no such characteristic')
|
|
789
985
|
return
|
bumble/apps/gg_bridge.py
CHANGED
|
@@ -230,13 +230,13 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
|
|
|
230
230
|
)
|
|
231
231
|
self.tx_characteristic = Characteristic(
|
|
232
232
|
GG_GATTLINK_TX_CHARACTERISTIC_UUID,
|
|
233
|
-
Characteristic.NOTIFY,
|
|
233
|
+
Characteristic.Properties.NOTIFY,
|
|
234
234
|
Characteristic.READABLE,
|
|
235
235
|
)
|
|
236
236
|
self.tx_characteristic.on('subscription', self.on_tx_subscription)
|
|
237
237
|
self.psm_characteristic = Characteristic(
|
|
238
238
|
GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID,
|
|
239
|
-
Characteristic.READ | Characteristic.NOTIFY,
|
|
239
|
+
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
|
240
240
|
Characteristic.READABLE,
|
|
241
241
|
bytes([psm, 0]),
|
|
242
242
|
)
|
|
@@ -339,8 +339,7 @@ async def run(
|
|
|
339
339
|
|
|
340
340
|
# Create a UDP to TX bridge (receive from TX, send to UDP)
|
|
341
341
|
bridge.tx_socket, _ = await loop.create_datagram_endpoint(
|
|
342
|
-
|
|
343
|
-
lambda: asyncio.DatagramProtocol(),
|
|
342
|
+
asyncio.DatagramProtocol,
|
|
344
343
|
remote_addr=(send_host, send_port),
|
|
345
344
|
)
|
|
346
345
|
|
bumble/apps/pair.py
CHANGED
|
@@ -24,7 +24,7 @@ from prompt_toolkit.shortcuts import PromptSession
|
|
|
24
24
|
from bumble.colors import color
|
|
25
25
|
from bumble.device import Device, Peer
|
|
26
26
|
from bumble.transport import open_transport_or_link
|
|
27
|
-
from bumble.
|
|
27
|
+
from bumble.pairing import PairingDelegate, PairingConfig
|
|
28
28
|
from bumble.smp import error_name as smp_error_name
|
|
29
29
|
from bumble.keys import JsonKeyStore
|
|
30
30
|
from bumble.core import ProtocolError
|
|
@@ -264,6 +264,7 @@ async def pair(
|
|
|
264
264
|
sc,
|
|
265
265
|
mitm,
|
|
266
266
|
bond,
|
|
267
|
+
ctkd,
|
|
267
268
|
io,
|
|
268
269
|
prompt,
|
|
269
270
|
request,
|
|
@@ -302,7 +303,8 @@ async def pair(
|
|
|
302
303
|
[
|
|
303
304
|
Characteristic(
|
|
304
305
|
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
|
305
|
-
Characteristic.READ
|
|
306
|
+
Characteristic.Properties.READ
|
|
307
|
+
| Characteristic.Properties.WRITE,
|
|
306
308
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
|
307
309
|
CharacteristicValue(
|
|
308
310
|
read=read_with_error, write=write_with_error
|
|
@@ -316,6 +318,7 @@ async def pair(
|
|
|
316
318
|
if mode == 'classic':
|
|
317
319
|
device.classic_enabled = True
|
|
318
320
|
device.le_enabled = False
|
|
321
|
+
device.classic_smp_enabled = ctkd
|
|
319
322
|
|
|
320
323
|
# Get things going
|
|
321
324
|
await device.power_on()
|
|
@@ -342,8 +345,13 @@ async def pair(
|
|
|
342
345
|
print(color(f'Pairing failed: {error}', 'red'))
|
|
343
346
|
return
|
|
344
347
|
else:
|
|
345
|
-
|
|
346
|
-
|
|
348
|
+
if mode == 'le':
|
|
349
|
+
# Advertise so that peers can find us and connect
|
|
350
|
+
await device.start_advertising(auto_restart=True)
|
|
351
|
+
else:
|
|
352
|
+
# Become discoverable and connectable
|
|
353
|
+
await device.set_discoverable(True)
|
|
354
|
+
await device.set_connectable(True)
|
|
347
355
|
|
|
348
356
|
# Run until the user asks to exit
|
|
349
357
|
await Waiter.instance.wait_until_terminated()
|
|
@@ -378,6 +386,13 @@ class LogHandler(logging.Handler):
|
|
|
378
386
|
@click.option(
|
|
379
387
|
'--bond', type=bool, default=True, help='Enable bonding', show_default=True
|
|
380
388
|
)
|
|
389
|
+
@click.option(
|
|
390
|
+
'--ctkd',
|
|
391
|
+
type=bool,
|
|
392
|
+
default=True,
|
|
393
|
+
help='Enable CTKD',
|
|
394
|
+
show_default=True,
|
|
395
|
+
)
|
|
381
396
|
@click.option(
|
|
382
397
|
'--io',
|
|
383
398
|
type=click.Choice(
|
|
@@ -404,6 +419,7 @@ def main(
|
|
|
404
419
|
sc,
|
|
405
420
|
mitm,
|
|
406
421
|
bond,
|
|
422
|
+
ctkd,
|
|
407
423
|
io,
|
|
408
424
|
prompt,
|
|
409
425
|
request,
|
|
@@ -426,6 +442,7 @@ def main(
|
|
|
426
442
|
sc,
|
|
427
443
|
mitm,
|
|
428
444
|
bond,
|
|
445
|
+
ctkd,
|
|
429
446
|
io,
|
|
430
447
|
prompt,
|
|
431
448
|
request,
|
bumble/att.py
CHANGED
|
@@ -190,7 +190,7 @@ class ATT_Error(ProtocolError):
|
|
|
190
190
|
super().__init__(
|
|
191
191
|
error_code,
|
|
192
192
|
error_namespace='att',
|
|
193
|
-
error_name=ATT_PDU.error_name(
|
|
193
|
+
error_name=ATT_PDU.error_name(error_code),
|
|
194
194
|
)
|
|
195
195
|
self.att_handle = att_handle
|
|
196
196
|
self.message = message
|
|
@@ -750,10 +750,10 @@ class Attribute(EventEmitter):
|
|
|
750
750
|
permissions_str.split(","),
|
|
751
751
|
0,
|
|
752
752
|
)
|
|
753
|
-
except TypeError:
|
|
753
|
+
except TypeError as exc:
|
|
754
754
|
raise TypeError(
|
|
755
|
-
f"Attribute::permissions error:\nExpected a string containing any of the keys,
|
|
756
|
-
)
|
|
755
|
+
f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {','.join(Attribute.PERMISSION_NAMES.values())}\nGot: {permissions_str}"
|
|
756
|
+
) from exc
|
|
757
757
|
|
|
758
758
|
def __init__(self, attribute_type, permissions, value=b''):
|
|
759
759
|
EventEmitter.__init__(self)
|