bumble 0.0.210__py3-none-any.whl → 0.0.212__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 +8 -4
- bumble/apps/console.py +2 -2
- bumble/apps/pair.py +185 -32
- bumble/att.py +13 -12
- bumble/avctp.py +2 -2
- bumble/avdtp.py +122 -68
- bumble/avrcp.py +11 -5
- bumble/core.py +13 -7
- bumble/{crypto.py → crypto/__init__.py} +11 -95
- bumble/crypto/builtin.py +652 -0
- bumble/crypto/cryptography.py +84 -0
- bumble/device.py +365 -185
- bumble/drivers/intel.py +3 -0
- bumble/gatt.py +3 -5
- bumble/gatt_client.py +5 -3
- bumble/gatt_server.py +8 -6
- bumble/hci.py +81 -4
- bumble/hfp.py +44 -20
- bumble/hid.py +24 -12
- bumble/host.py +24 -0
- bumble/keys.py +64 -48
- bumble/l2cap.py +19 -9
- bumble/pandora/host.py +11 -11
- bumble/pandora/l2cap.py +2 -2
- bumble/pandora/security.py +72 -56
- bumble/profiles/aics.py +3 -5
- bumble/profiles/ancs.py +3 -1
- bumble/profiles/ascs.py +11 -5
- bumble/profiles/asha.py +11 -6
- bumble/profiles/csip.py +1 -3
- bumble/profiles/gatt_service.py +1 -3
- bumble/profiles/hap.py +16 -33
- bumble/profiles/mcp.py +12 -9
- bumble/profiles/vcs.py +5 -5
- bumble/profiles/vocs.py +6 -9
- bumble/rfcomm.py +17 -8
- bumble/smp.py +14 -8
- {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/METADATA +4 -4
- {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/RECORD +44 -42
- {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/WHEEL +1 -1
- {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/entry_points.txt +0 -0
- {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/licenses/LICENSE +0 -0
- {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/top_level.txt +0 -0
bumble/_version.py
CHANGED
bumble/apps/bench.py
CHANGED
|
@@ -121,9 +121,9 @@ def print_connection(connection):
|
|
|
121
121
|
|
|
122
122
|
params.append(
|
|
123
123
|
'Parameters='
|
|
124
|
-
f'{connection.parameters.connection_interval
|
|
124
|
+
f'{connection.parameters.connection_interval:.2f}/'
|
|
125
125
|
f'{connection.parameters.peripheral_latency}/'
|
|
126
|
-
f'{connection.parameters.supervision_timeout
|
|
126
|
+
f'{connection.parameters.supervision_timeout:.2f} '
|
|
127
127
|
)
|
|
128
128
|
|
|
129
129
|
params.append(f'MTU={connection.att_mtu}')
|
|
@@ -1256,6 +1256,7 @@ class Central(Connection.Listener):
|
|
|
1256
1256
|
self.device.classic_enabled = self.classic
|
|
1257
1257
|
|
|
1258
1258
|
# Set up a pairing config factory with minimal requirements.
|
|
1259
|
+
self.device.config.keystore = "JsonKeyStore"
|
|
1259
1260
|
self.device.pairing_config_factory = lambda _: PairingConfig(
|
|
1260
1261
|
sc=False, mitm=False, bonding=False
|
|
1261
1262
|
)
|
|
@@ -1291,8 +1292,10 @@ class Central(Connection.Listener):
|
|
|
1291
1292
|
logging.info(color('### Connected', 'cyan'))
|
|
1292
1293
|
self.connection.listener = self
|
|
1293
1294
|
print_connection(self.connection)
|
|
1294
|
-
|
|
1295
|
-
|
|
1295
|
+
|
|
1296
|
+
if not self.classic:
|
|
1297
|
+
phy = await self.connection.get_phy()
|
|
1298
|
+
print_connection_phy(phy)
|
|
1296
1299
|
|
|
1297
1300
|
# Switch roles if needed.
|
|
1298
1301
|
if self.role_switch:
|
|
@@ -1406,6 +1409,7 @@ class Peripheral(Device.Listener, Connection.Listener):
|
|
|
1406
1409
|
self.device.classic_enabled = self.classic
|
|
1407
1410
|
|
|
1408
1411
|
# Set up a pairing config factory with minimal requirements.
|
|
1412
|
+
self.device.config.keystore = "JsonKeyStore"
|
|
1409
1413
|
self.device.pairing_config_factory = lambda _: PairingConfig(
|
|
1410
1414
|
sc=False, mitm=False, bonding=False
|
|
1411
1415
|
)
|
bumble/apps/console.py
CHANGED
|
@@ -335,9 +335,9 @@ class ConsoleApp:
|
|
|
335
335
|
elif self.connected_peer:
|
|
336
336
|
connection = self.connected_peer.connection
|
|
337
337
|
connection_parameters = (
|
|
338
|
-
f'{connection.parameters.connection_interval}/'
|
|
338
|
+
f'{connection.parameters.connection_interval:.2f}/'
|
|
339
339
|
f'{connection.parameters.peripheral_latency}/'
|
|
340
|
-
f'{connection.parameters.supervision_timeout}'
|
|
340
|
+
f'{connection.parameters.supervision_timeout:.2f}'
|
|
341
341
|
)
|
|
342
342
|
if self.connection_phy is not None:
|
|
343
343
|
phy_state = (
|
bumble/apps/pair.py
CHANGED
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
import asyncio
|
|
19
19
|
import os
|
|
20
20
|
import logging
|
|
21
|
+
import struct
|
|
22
|
+
|
|
21
23
|
import click
|
|
22
24
|
from prompt_toolkit.shortcuts import PromptSession
|
|
23
25
|
|
|
26
|
+
from bumble.a2dp import make_audio_sink_service_sdp_records
|
|
24
27
|
from bumble.colors import color
|
|
25
28
|
from bumble.device import Device, Peer
|
|
26
29
|
from bumble.transport import open_transport_or_link
|
|
@@ -30,16 +33,20 @@ from bumble.smp import error_name as smp_error_name
|
|
|
30
33
|
from bumble.keys import JsonKeyStore
|
|
31
34
|
from bumble.core import (
|
|
32
35
|
AdvertisingData,
|
|
36
|
+
Appearance,
|
|
33
37
|
ProtocolError,
|
|
34
38
|
PhysicalTransport,
|
|
39
|
+
UUID,
|
|
35
40
|
)
|
|
36
41
|
from bumble.gatt import (
|
|
37
42
|
GATT_DEVICE_NAME_CHARACTERISTIC,
|
|
38
43
|
GATT_GENERIC_ACCESS_SERVICE,
|
|
44
|
+
GATT_HEART_RATE_SERVICE,
|
|
45
|
+
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
|
39
46
|
Service,
|
|
40
47
|
Characteristic,
|
|
41
|
-
CharacteristicValue,
|
|
42
48
|
)
|
|
49
|
+
from bumble.hci import OwnAddressType
|
|
43
50
|
from bumble.att import (
|
|
44
51
|
ATT_Error,
|
|
45
52
|
ATT_INSUFFICIENT_AUTHENTICATION_ERROR,
|
|
@@ -62,7 +69,7 @@ class Waiter:
|
|
|
62
69
|
self.linger = linger
|
|
63
70
|
|
|
64
71
|
def terminate(self):
|
|
65
|
-
if not self.linger:
|
|
72
|
+
if not self.linger and not self.done.done:
|
|
66
73
|
self.done.set_result(None)
|
|
67
74
|
|
|
68
75
|
async def wait_until_terminated(self):
|
|
@@ -193,7 +200,7 @@ class Delegate(PairingDelegate):
|
|
|
193
200
|
|
|
194
201
|
# -----------------------------------------------------------------------------
|
|
195
202
|
async def get_peer_name(peer, mode):
|
|
196
|
-
if
|
|
203
|
+
if peer.connection.transport == PhysicalTransport.BR_EDR:
|
|
197
204
|
return await peer.request_name()
|
|
198
205
|
|
|
199
206
|
# Try to get the peer name from GATT
|
|
@@ -225,13 +232,14 @@ def read_with_error(connection):
|
|
|
225
232
|
raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
|
|
226
233
|
|
|
227
234
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
+
# -----------------------------------------------------------------------------
|
|
236
|
+
def sdp_records():
|
|
237
|
+
service_record_handle = 0x00010001
|
|
238
|
+
return {
|
|
239
|
+
service_record_handle: make_audio_sink_service_sdp_records(
|
|
240
|
+
service_record_handle
|
|
241
|
+
)
|
|
242
|
+
}
|
|
235
243
|
|
|
236
244
|
|
|
237
245
|
# -----------------------------------------------------------------------------
|
|
@@ -239,15 +247,19 @@ def on_connection(connection, request):
|
|
|
239
247
|
print(color(f'<<< Connection: {connection}', 'green'))
|
|
240
248
|
|
|
241
249
|
# Listen for pairing events
|
|
242
|
-
connection.on(
|
|
243
|
-
connection.on(
|
|
250
|
+
connection.on(connection.EVENT_PAIRING_START, on_pairing_start)
|
|
251
|
+
connection.on(connection.EVENT_PAIRING, lambda keys: on_pairing(connection, keys))
|
|
244
252
|
connection.on(
|
|
245
|
-
|
|
253
|
+
connection.EVENT_CLASSIC_PAIRING, lambda: on_classic_pairing(connection)
|
|
254
|
+
)
|
|
255
|
+
connection.on(
|
|
256
|
+
connection.EVENT_PAIRING_FAILURE,
|
|
257
|
+
lambda reason: on_pairing_failure(connection, reason),
|
|
246
258
|
)
|
|
247
259
|
|
|
248
260
|
# Listen for encryption changes
|
|
249
261
|
connection.on(
|
|
250
|
-
|
|
262
|
+
connection.EVENT_CONNECTION_ENCRYPTION_CHANGE,
|
|
251
263
|
lambda: on_connection_encryption_change(connection),
|
|
252
264
|
)
|
|
253
265
|
|
|
@@ -288,6 +300,20 @@ async def on_pairing(connection, keys):
|
|
|
288
300
|
Waiter.instance.terminate()
|
|
289
301
|
|
|
290
302
|
|
|
303
|
+
# -----------------------------------------------------------------------------
|
|
304
|
+
@AsyncRunner.run_in_task()
|
|
305
|
+
async def on_classic_pairing(connection):
|
|
306
|
+
print(color('***-----------------------------------', 'cyan'))
|
|
307
|
+
print(
|
|
308
|
+
color(
|
|
309
|
+
f'*** Paired [Classic]! (peer identity={connection.peer_address})', 'cyan'
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
print(color('***-----------------------------------', 'cyan'))
|
|
313
|
+
await asyncio.sleep(POST_PAIRING_DELAY)
|
|
314
|
+
Waiter.instance.terminate()
|
|
315
|
+
|
|
316
|
+
|
|
291
317
|
# -----------------------------------------------------------------------------
|
|
292
318
|
@AsyncRunner.run_in_task()
|
|
293
319
|
async def on_pairing_failure(connection, reason):
|
|
@@ -305,6 +331,7 @@ async def pair(
|
|
|
305
331
|
mitm,
|
|
306
332
|
bond,
|
|
307
333
|
ctkd,
|
|
334
|
+
advertising_address,
|
|
308
335
|
identity_address,
|
|
309
336
|
linger,
|
|
310
337
|
io,
|
|
@@ -313,6 +340,8 @@ async def pair(
|
|
|
313
340
|
request,
|
|
314
341
|
print_keys,
|
|
315
342
|
keystore_file,
|
|
343
|
+
advertise_service_uuids,
|
|
344
|
+
advertise_appearance,
|
|
316
345
|
device_config,
|
|
317
346
|
hci_transport,
|
|
318
347
|
address_or_name,
|
|
@@ -328,29 +357,33 @@ async def pair(
|
|
|
328
357
|
|
|
329
358
|
# Expose a GATT characteristic that can be used to trigger pairing by
|
|
330
359
|
# responding with an authentication error when read
|
|
331
|
-
if mode
|
|
332
|
-
device.le_enabled = True
|
|
360
|
+
if mode in ('le', 'dual'):
|
|
333
361
|
device.add_service(
|
|
334
362
|
Service(
|
|
335
|
-
|
|
363
|
+
GATT_HEART_RATE_SERVICE,
|
|
336
364
|
[
|
|
337
365
|
Characteristic(
|
|
338
|
-
|
|
339
|
-
Characteristic.Properties.READ
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
CharacteristicValue(
|
|
343
|
-
read=read_with_error, write=write_with_error
|
|
344
|
-
),
|
|
366
|
+
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
|
367
|
+
Characteristic.Properties.READ,
|
|
368
|
+
Characteristic.READ_REQUIRES_AUTHENTICATION,
|
|
369
|
+
bytes(1),
|
|
345
370
|
)
|
|
346
371
|
],
|
|
347
372
|
)
|
|
348
373
|
)
|
|
349
374
|
|
|
350
|
-
#
|
|
351
|
-
if mode
|
|
375
|
+
# LE and Classic support
|
|
376
|
+
if mode in ('classic', 'dual'):
|
|
352
377
|
device.classic_enabled = True
|
|
353
378
|
device.classic_smp_enabled = ctkd
|
|
379
|
+
if mode in ('le', 'dual'):
|
|
380
|
+
device.le_enabled = True
|
|
381
|
+
if mode == 'dual':
|
|
382
|
+
device.le_simultaneous_enabled = True
|
|
383
|
+
|
|
384
|
+
# Setup SDP
|
|
385
|
+
if mode in ('classic', 'dual'):
|
|
386
|
+
device.sdp_service_records = sdp_records()
|
|
354
387
|
|
|
355
388
|
# Get things going
|
|
356
389
|
await device.power_on()
|
|
@@ -436,13 +469,109 @@ async def pair(
|
|
|
436
469
|
print(color(f'Pairing failed: {error}', 'red'))
|
|
437
470
|
|
|
438
471
|
else:
|
|
439
|
-
if mode
|
|
440
|
-
# Advertise so that peers can find us and connect
|
|
441
|
-
|
|
442
|
-
|
|
472
|
+
if mode in ('le', 'dual'):
|
|
473
|
+
# Advertise so that peers can find us and connect.
|
|
474
|
+
# Include the heart rate service UUID in the advertisement data
|
|
475
|
+
# so that devices like iPhones can show this device in their
|
|
476
|
+
# Bluetooth selector.
|
|
477
|
+
service_uuids_16 = []
|
|
478
|
+
service_uuids_32 = []
|
|
479
|
+
service_uuids_128 = []
|
|
480
|
+
if advertise_service_uuids:
|
|
481
|
+
for uuid in advertise_service_uuids:
|
|
482
|
+
uuid = uuid.replace("-", "")
|
|
483
|
+
if len(uuid) == 4:
|
|
484
|
+
service_uuids_16.append(UUID(uuid))
|
|
485
|
+
elif len(uuid) == 8:
|
|
486
|
+
service_uuids_32.append(UUID(uuid))
|
|
487
|
+
elif len(uuid) == 32:
|
|
488
|
+
service_uuids_128.append(UUID(uuid))
|
|
489
|
+
else:
|
|
490
|
+
print(color('Invalid UUID format', 'red'))
|
|
491
|
+
return
|
|
492
|
+
else:
|
|
493
|
+
service_uuids_16.append(GATT_HEART_RATE_SERVICE)
|
|
494
|
+
|
|
495
|
+
flags = AdvertisingData.Flags.LE_LIMITED_DISCOVERABLE_MODE
|
|
496
|
+
if mode == 'le':
|
|
497
|
+
flags |= AdvertisingData.Flags.BR_EDR_NOT_SUPPORTED
|
|
498
|
+
if mode == 'dual':
|
|
499
|
+
flags |= AdvertisingData.Flags.SIMULTANEOUS_LE_BR_EDR_CAPABLE
|
|
500
|
+
|
|
501
|
+
ad_structs = [
|
|
502
|
+
(
|
|
503
|
+
AdvertisingData.FLAGS,
|
|
504
|
+
bytes([flags]),
|
|
505
|
+
),
|
|
506
|
+
(AdvertisingData.COMPLETE_LOCAL_NAME, 'Bumble'.encode()),
|
|
507
|
+
]
|
|
508
|
+
if service_uuids_16:
|
|
509
|
+
ad_structs.append(
|
|
510
|
+
(
|
|
511
|
+
AdvertisingData.INCOMPLETE_LIST_OF_16_BIT_SERVICE_CLASS_UUIDS,
|
|
512
|
+
b"".join(bytes(uuid) for uuid in service_uuids_16),
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
if service_uuids_32:
|
|
516
|
+
ad_structs.append(
|
|
517
|
+
(
|
|
518
|
+
AdvertisingData.INCOMPLETE_LIST_OF_32_BIT_SERVICE_CLASS_UUIDS,
|
|
519
|
+
b"".join(bytes(uuid) for uuid in service_uuids_32),
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
if service_uuids_128:
|
|
523
|
+
ad_structs.append(
|
|
524
|
+
(
|
|
525
|
+
AdvertisingData.INCOMPLETE_LIST_OF_128_BIT_SERVICE_CLASS_UUIDS,
|
|
526
|
+
b"".join(bytes(uuid) for uuid in service_uuids_128),
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
if advertise_appearance:
|
|
531
|
+
advertise_appearance = advertise_appearance.upper()
|
|
532
|
+
try:
|
|
533
|
+
advertise_appearance_int = int(advertise_appearance)
|
|
534
|
+
except ValueError:
|
|
535
|
+
category, subcategory = advertise_appearance.split('/')
|
|
536
|
+
try:
|
|
537
|
+
category_enum = Appearance.Category[category]
|
|
538
|
+
except ValueError:
|
|
539
|
+
print(
|
|
540
|
+
color(f'Invalid appearance category {category}', 'red')
|
|
541
|
+
)
|
|
542
|
+
return
|
|
543
|
+
subcategory_class = Appearance.SUBCATEGORY_CLASSES[
|
|
544
|
+
category_enum
|
|
545
|
+
]
|
|
546
|
+
try:
|
|
547
|
+
subcategory_enum = subcategory_class[subcategory]
|
|
548
|
+
except ValueError:
|
|
549
|
+
print(color(f'Invalid subcategory {subcategory}', 'red'))
|
|
550
|
+
return
|
|
551
|
+
advertise_appearance_int = int(
|
|
552
|
+
Appearance(category_enum, subcategory_enum)
|
|
553
|
+
)
|
|
554
|
+
ad_structs.append(
|
|
555
|
+
(
|
|
556
|
+
AdvertisingData.APPEARANCE,
|
|
557
|
+
struct.pack('<H', advertise_appearance_int),
|
|
558
|
+
)
|
|
559
|
+
)
|
|
560
|
+
device.advertising_data = bytes(AdvertisingData(ad_structs))
|
|
561
|
+
await device.start_advertising(
|
|
562
|
+
auto_restart=True,
|
|
563
|
+
own_address_type=(
|
|
564
|
+
OwnAddressType.PUBLIC
|
|
565
|
+
if advertising_address == 'public'
|
|
566
|
+
else OwnAddressType.RANDOM
|
|
567
|
+
),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
if mode in ('classic', 'dual'):
|
|
443
571
|
# Become discoverable and connectable
|
|
444
572
|
await device.set_discoverable(True)
|
|
445
573
|
await device.set_connectable(True)
|
|
574
|
+
print(color('Ready for connections on', 'blue'), device.public_address)
|
|
446
575
|
|
|
447
576
|
# Run until the user asks to exit
|
|
448
577
|
await Waiter.instance.wait_until_terminated()
|
|
@@ -462,7 +591,10 @@ class LogHandler(logging.Handler):
|
|
|
462
591
|
# -----------------------------------------------------------------------------
|
|
463
592
|
@click.command()
|
|
464
593
|
@click.option(
|
|
465
|
-
'--mode',
|
|
594
|
+
'--mode',
|
|
595
|
+
type=click.Choice(['le', 'classic', 'dual']),
|
|
596
|
+
default='le',
|
|
597
|
+
show_default=True,
|
|
466
598
|
)
|
|
467
599
|
@click.option(
|
|
468
600
|
'--sc',
|
|
@@ -484,6 +616,10 @@ class LogHandler(logging.Handler):
|
|
|
484
616
|
help='Enable CTKD',
|
|
485
617
|
show_default=True,
|
|
486
618
|
)
|
|
619
|
+
@click.option(
|
|
620
|
+
'--advertising-address',
|
|
621
|
+
type=click.Choice(['random', 'public']),
|
|
622
|
+
)
|
|
487
623
|
@click.option(
|
|
488
624
|
'--identity-address',
|
|
489
625
|
type=click.Choice(['random', 'public']),
|
|
@@ -512,9 +648,20 @@ class LogHandler(logging.Handler):
|
|
|
512
648
|
@click.option('--print-keys', is_flag=True, help='Print the bond keys before pairing')
|
|
513
649
|
@click.option(
|
|
514
650
|
'--keystore-file',
|
|
515
|
-
metavar='
|
|
651
|
+
metavar='FILENAME',
|
|
516
652
|
help='File in which to store the pairing keys',
|
|
517
653
|
)
|
|
654
|
+
@click.option(
|
|
655
|
+
'--advertise-service-uuid',
|
|
656
|
+
metavar="UUID",
|
|
657
|
+
multiple=True,
|
|
658
|
+
help="Advertise a GATT service UUID (may be specified more than once)",
|
|
659
|
+
)
|
|
660
|
+
@click.option(
|
|
661
|
+
'--advertise-appearance',
|
|
662
|
+
metavar='APPEARANCE',
|
|
663
|
+
help='Advertise an Appearance ID (int value or string)',
|
|
664
|
+
)
|
|
518
665
|
@click.argument('device-config')
|
|
519
666
|
@click.argument('hci_transport')
|
|
520
667
|
@click.argument('address-or-name', required=False)
|
|
@@ -524,6 +671,7 @@ def main(
|
|
|
524
671
|
mitm,
|
|
525
672
|
bond,
|
|
526
673
|
ctkd,
|
|
674
|
+
advertising_address,
|
|
527
675
|
identity_address,
|
|
528
676
|
linger,
|
|
529
677
|
io,
|
|
@@ -532,6 +680,8 @@ def main(
|
|
|
532
680
|
request,
|
|
533
681
|
print_keys,
|
|
534
682
|
keystore_file,
|
|
683
|
+
advertise_service_uuid,
|
|
684
|
+
advertise_appearance,
|
|
535
685
|
device_config,
|
|
536
686
|
hci_transport,
|
|
537
687
|
address_or_name,
|
|
@@ -550,6 +700,7 @@ def main(
|
|
|
550
700
|
mitm,
|
|
551
701
|
bond,
|
|
552
702
|
ctkd,
|
|
703
|
+
advertising_address,
|
|
553
704
|
identity_address,
|
|
554
705
|
linger,
|
|
555
706
|
io,
|
|
@@ -558,6 +709,8 @@ def main(
|
|
|
558
709
|
request,
|
|
559
710
|
print_keys,
|
|
560
711
|
keystore_file,
|
|
712
|
+
advertise_service_uuid,
|
|
713
|
+
advertise_appearance,
|
|
561
714
|
device_config,
|
|
562
715
|
hci_transport,
|
|
563
716
|
address_or_name,
|
bumble/att.py
CHANGED
|
@@ -770,27 +770,25 @@ class AttributeValue(Generic[_T]):
|
|
|
770
770
|
def __init__(
|
|
771
771
|
self,
|
|
772
772
|
read: Union[
|
|
773
|
-
Callable[[
|
|
774
|
-
Callable[[
|
|
773
|
+
Callable[[Connection], _T],
|
|
774
|
+
Callable[[Connection], Awaitable[_T]],
|
|
775
775
|
None,
|
|
776
776
|
] = None,
|
|
777
777
|
write: Union[
|
|
778
|
-
Callable[[
|
|
779
|
-
Callable[[
|
|
778
|
+
Callable[[Connection, _T], None],
|
|
779
|
+
Callable[[Connection, _T], Awaitable[None]],
|
|
780
780
|
None,
|
|
781
781
|
] = None,
|
|
782
782
|
):
|
|
783
783
|
self._read = read
|
|
784
784
|
self._write = write
|
|
785
785
|
|
|
786
|
-
def read(self, connection:
|
|
786
|
+
def read(self, connection: Connection) -> Union[_T, Awaitable[_T]]:
|
|
787
787
|
if self._read is None:
|
|
788
788
|
raise InvalidOperationError('AttributeValue has no read function')
|
|
789
789
|
return self._read(connection)
|
|
790
790
|
|
|
791
|
-
def write(
|
|
792
|
-
self, connection: Optional[Connection], value: _T
|
|
793
|
-
) -> Union[Awaitable[None], None]:
|
|
791
|
+
def write(self, connection: Connection, value: _T) -> Union[Awaitable[None], None]:
|
|
794
792
|
if self._write is None:
|
|
795
793
|
raise InvalidOperationError('AttributeValue has no write function')
|
|
796
794
|
return self._write(connection, value)
|
|
@@ -836,6 +834,9 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
|
|
836
834
|
READ_REQUIRES_AUTHORIZATION = Permissions.READ_REQUIRES_AUTHORIZATION
|
|
837
835
|
WRITE_REQUIRES_AUTHORIZATION = Permissions.WRITE_REQUIRES_AUTHORIZATION
|
|
838
836
|
|
|
837
|
+
EVENT_READ = "read"
|
|
838
|
+
EVENT_WRITE = "write"
|
|
839
|
+
|
|
839
840
|
value: Union[AttributeValue[_T], _T, None]
|
|
840
841
|
|
|
841
842
|
def __init__(
|
|
@@ -868,7 +869,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
|
|
868
869
|
def decode_value(self, value: bytes) -> _T:
|
|
869
870
|
return value # type: ignore
|
|
870
871
|
|
|
871
|
-
async def read_value(self, connection:
|
|
872
|
+
async def read_value(self, connection: Connection) -> bytes:
|
|
872
873
|
if (
|
|
873
874
|
(self.permissions & self.READ_REQUIRES_ENCRYPTION)
|
|
874
875
|
and connection is not None
|
|
@@ -906,11 +907,11 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
|
|
906
907
|
else:
|
|
907
908
|
value = self.value
|
|
908
909
|
|
|
909
|
-
self.emit(
|
|
910
|
+
self.emit(self.EVENT_READ, connection, b'' if value is None else value)
|
|
910
911
|
|
|
911
912
|
return b'' if value is None else self.encode_value(value)
|
|
912
913
|
|
|
913
|
-
async def write_value(self, connection:
|
|
914
|
+
async def write_value(self, connection: Connection, value: bytes) -> None:
|
|
914
915
|
if (
|
|
915
916
|
(self.permissions & self.WRITE_REQUIRES_ENCRYPTION)
|
|
916
917
|
and connection is not None
|
|
@@ -947,7 +948,7 @@ class Attribute(utils.EventEmitter, Generic[_T]):
|
|
|
947
948
|
else:
|
|
948
949
|
self.value = decoded_value
|
|
949
950
|
|
|
950
|
-
self.emit(
|
|
951
|
+
self.emit(self.EVENT_WRITE, connection, decoded_value)
|
|
951
952
|
|
|
952
953
|
def __repr__(self):
|
|
953
954
|
if isinstance(self.value, bytes):
|
bumble/avctp.py
CHANGED
|
@@ -166,8 +166,8 @@ class Protocol:
|
|
|
166
166
|
|
|
167
167
|
# Register to receive PDUs from the channel
|
|
168
168
|
l2cap_channel.sink = self.on_pdu
|
|
169
|
-
l2cap_channel.on(
|
|
170
|
-
l2cap_channel.on(
|
|
169
|
+
l2cap_channel.on(l2cap_channel.EVENT_OPEN, self.on_l2cap_channel_open)
|
|
170
|
+
l2cap_channel.on(l2cap_channel.EVENT_CLOSE, self.on_l2cap_channel_close)
|
|
171
171
|
|
|
172
172
|
def on_l2cap_channel_open(self):
|
|
173
173
|
logger.debug(color("<<< AVCTP channel open", "magenta"))
|