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.
Files changed (44) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/bench.py +8 -4
  3. bumble/apps/console.py +2 -2
  4. bumble/apps/pair.py +185 -32
  5. bumble/att.py +13 -12
  6. bumble/avctp.py +2 -2
  7. bumble/avdtp.py +122 -68
  8. bumble/avrcp.py +11 -5
  9. bumble/core.py +13 -7
  10. bumble/{crypto.py → crypto/__init__.py} +11 -95
  11. bumble/crypto/builtin.py +652 -0
  12. bumble/crypto/cryptography.py +84 -0
  13. bumble/device.py +365 -185
  14. bumble/drivers/intel.py +3 -0
  15. bumble/gatt.py +3 -5
  16. bumble/gatt_client.py +5 -3
  17. bumble/gatt_server.py +8 -6
  18. bumble/hci.py +81 -4
  19. bumble/hfp.py +44 -20
  20. bumble/hid.py +24 -12
  21. bumble/host.py +24 -0
  22. bumble/keys.py +64 -48
  23. bumble/l2cap.py +19 -9
  24. bumble/pandora/host.py +11 -11
  25. bumble/pandora/l2cap.py +2 -2
  26. bumble/pandora/security.py +72 -56
  27. bumble/profiles/aics.py +3 -5
  28. bumble/profiles/ancs.py +3 -1
  29. bumble/profiles/ascs.py +11 -5
  30. bumble/profiles/asha.py +11 -6
  31. bumble/profiles/csip.py +1 -3
  32. bumble/profiles/gatt_service.py +1 -3
  33. bumble/profiles/hap.py +16 -33
  34. bumble/profiles/mcp.py +12 -9
  35. bumble/profiles/vcs.py +5 -5
  36. bumble/profiles/vocs.py +6 -9
  37. bumble/rfcomm.py +17 -8
  38. bumble/smp.py +14 -8
  39. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/METADATA +4 -4
  40. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/RECORD +44 -42
  41. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/WHEEL +1 -1
  42. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/entry_points.txt +0 -0
  43. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/licenses/LICENSE +0 -0
  44. {bumble-0.0.210.dist-info → bumble-0.0.212.dist-info}/top_level.txt +0 -0
bumble/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.0.210'
21
- __version_tuple__ = version_tuple = (0, 0, 210)
20
+ __version__ = version = '0.0.212'
21
+ __version_tuple__ = version_tuple = (0, 0, 212)
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 * 1.25:.2f}/'
124
+ f'{connection.parameters.connection_interval:.2f}/'
125
125
  f'{connection.parameters.peripheral_latency}/'
126
- f'{connection.parameters.supervision_timeout * 10} '
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
- phy = await self.connection.get_phy()
1295
- print_connection_phy(phy)
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 mode == 'classic':
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
- def write_with_error(connection, _value):
229
- if not connection.is_encrypted:
230
- raise ATT_Error(ATT_INSUFFICIENT_ENCRYPTION_ERROR)
231
-
232
- if not AUTHENTICATION_ERROR_RETURNED[1]:
233
- AUTHENTICATION_ERROR_RETURNED[1] = True
234
- raise ATT_Error(ATT_INSUFFICIENT_AUTHENTICATION_ERROR)
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('pairing_start', on_pairing_start)
243
- connection.on('pairing', lambda keys: on_pairing(connection, keys))
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
- 'pairing_failure', lambda reason: on_pairing_failure(connection, reason)
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
- 'connection_encryption_change',
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 == 'le':
332
- device.le_enabled = True
360
+ if mode in ('le', 'dual'):
333
361
  device.add_service(
334
362
  Service(
335
- '50DB505C-8AC4-4738-8448-3B1D9CC09CC5',
363
+ GATT_HEART_RATE_SERVICE,
336
364
  [
337
365
  Characteristic(
338
- '552957FB-CF1F-4A31-9535-E78847E1A714',
339
- Characteristic.Properties.READ
340
- | Characteristic.Properties.WRITE,
341
- Characteristic.READABLE | Characteristic.WRITEABLE,
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
- # Select LE or Classic
351
- if mode == 'classic':
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 == 'le':
440
- # Advertise so that peers can find us and connect
441
- await device.start_advertising(auto_restart=True)
442
- else:
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', type=click.Choice(['le', 'classic']), default='le', show_default=True
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='<filename>',
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[[Optional[Connection]], _T],
774
- Callable[[Optional[Connection]], Awaitable[_T]],
773
+ Callable[[Connection], _T],
774
+ Callable[[Connection], Awaitable[_T]],
775
775
  None,
776
776
  ] = None,
777
777
  write: Union[
778
- Callable[[Optional[Connection], _T], None],
779
- Callable[[Optional[Connection], _T], Awaitable[None]],
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: Optional[Connection]) -> Union[_T, Awaitable[_T]]:
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: Optional[Connection]) -> bytes:
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('read', connection, b'' if value is None else value)
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: Optional[Connection], value: bytes) -> None:
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('write', connection, decoded_value)
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("open", self.on_l2cap_channel_open)
170
- l2cap_channel.on("close", self.on_l2cap_channel_close)
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"))