bumble 0.0.212__py3-none-any.whl → 0.0.214__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 (92) hide show
  1. bumble/_version.py +2 -2
  2. bumble/a2dp.py +6 -0
  3. bumble/apps/README.md +0 -3
  4. bumble/apps/auracast.py +14 -11
  5. bumble/apps/bench.py +482 -37
  6. bumble/apps/console.py +3 -3
  7. bumble/apps/controller_info.py +44 -12
  8. bumble/apps/controller_loopback.py +7 -7
  9. bumble/apps/controllers.py +4 -5
  10. bumble/apps/device_info.py +4 -5
  11. bumble/apps/gatt_dump.py +5 -5
  12. bumble/apps/gg_bridge.py +5 -5
  13. bumble/apps/hci_bridge.py +5 -4
  14. bumble/apps/l2cap_bridge.py +5 -5
  15. bumble/apps/lea_unicast/app.py +8 -3
  16. bumble/apps/pair.py +19 -11
  17. bumble/apps/pandora_server.py +2 -2
  18. bumble/apps/player/player.py +2 -3
  19. bumble/apps/rfcomm_bridge.py +3 -4
  20. bumble/apps/scan.py +4 -5
  21. bumble/apps/show.py +6 -4
  22. bumble/apps/speaker/speaker.html +1 -0
  23. bumble/apps/speaker/speaker.js +113 -62
  24. bumble/apps/speaker/speaker.py +123 -19
  25. bumble/apps/unbond.py +2 -3
  26. bumble/apps/usb_probe.py +2 -3
  27. bumble/at.py +4 -4
  28. bumble/att.py +2 -6
  29. bumble/avc.py +7 -7
  30. bumble/avctp.py +3 -3
  31. bumble/avdtp.py +16 -20
  32. bumble/avrcp.py +42 -54
  33. bumble/colors.py +2 -2
  34. bumble/controller.py +174 -45
  35. bumble/device.py +398 -182
  36. bumble/drivers/__init__.py +2 -2
  37. bumble/drivers/common.py +0 -2
  38. bumble/drivers/intel.py +37 -40
  39. bumble/drivers/rtk.py +28 -35
  40. bumble/gatt.py +4 -4
  41. bumble/gatt_adapters.py +4 -5
  42. bumble/gatt_client.py +26 -31
  43. bumble/gatt_server.py +7 -11
  44. bumble/hci.py +2648 -2909
  45. bumble/helpers.py +4 -5
  46. bumble/hfp.py +32 -37
  47. bumble/host.py +104 -35
  48. bumble/keys.py +5 -5
  49. bumble/l2cap.py +312 -409
  50. bumble/link.py +16 -280
  51. bumble/logging.py +65 -0
  52. bumble/pairing.py +23 -20
  53. bumble/pandora/__init__.py +2 -2
  54. bumble/pandora/config.py +2 -2
  55. bumble/pandora/device.py +6 -6
  56. bumble/pandora/host.py +27 -28
  57. bumble/pandora/l2cap.py +2 -2
  58. bumble/pandora/security.py +6 -6
  59. bumble/pandora/utils.py +3 -3
  60. bumble/profiles/ams.py +404 -0
  61. bumble/profiles/ascs.py +142 -131
  62. bumble/profiles/asha.py +2 -2
  63. bumble/profiles/bap.py +3 -4
  64. bumble/profiles/csip.py +2 -2
  65. bumble/profiles/device_information_service.py +2 -2
  66. bumble/profiles/gap.py +2 -2
  67. bumble/profiles/hap.py +34 -33
  68. bumble/profiles/le_audio.py +4 -4
  69. bumble/profiles/mcp.py +4 -4
  70. bumble/profiles/vcs.py +3 -5
  71. bumble/rfcomm.py +10 -10
  72. bumble/rtp.py +1 -2
  73. bumble/sdp.py +2 -2
  74. bumble/smp.py +62 -63
  75. bumble/tools/intel_util.py +3 -2
  76. bumble/tools/rtk_util.py +6 -5
  77. bumble/transport/__init__.py +2 -16
  78. bumble/transport/android_netsim.py +5 -5
  79. bumble/transport/common.py +4 -4
  80. bumble/transport/pyusb.py +2 -2
  81. bumble/utils.py +2 -5
  82. bumble/vendor/android/hci.py +118 -200
  83. bumble/vendor/zephyr/hci.py +32 -27
  84. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/METADATA +4 -3
  85. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/RECORD +89 -90
  86. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/WHEEL +1 -1
  87. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/entry_points.txt +0 -1
  88. bumble/apps/link_relay/__init__.py +0 -0
  89. bumble/apps/link_relay/link_relay.py +0 -289
  90. bumble/apps/link_relay/logging.yml +0 -21
  91. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/licenses/LICENSE +0 -0
  92. {bumble-0.0.212.dist-info → bumble-0.0.214.dist-info}/top_level.txt +0 -0
bumble/apps/bench.py CHANGED
@@ -19,10 +19,10 @@ import asyncio
19
19
  import dataclasses
20
20
  import enum
21
21
  import logging
22
- import os
23
22
  import statistics
24
23
  import struct
25
24
  import time
25
+ from typing import Optional
26
26
 
27
27
  import click
28
28
 
@@ -35,7 +35,15 @@ from bumble.core import (
35
35
  CommandTimeoutError,
36
36
  )
37
37
  from bumble.colors import color
38
- from bumble.device import Connection, ConnectionParametersPreferences, Device, Peer
38
+ from bumble.core import ConnectionPHY
39
+ from bumble.device import (
40
+ CigParameters,
41
+ CisLink,
42
+ Connection,
43
+ ConnectionParametersPreferences,
44
+ Device,
45
+ Peer,
46
+ )
39
47
  from bumble.gatt import Characteristic, CharacteristicValue, Service
40
48
  from bumble.hci import (
41
49
  HCI_LE_1M_PHY,
@@ -45,6 +53,7 @@ from bumble.hci import (
45
53
  HCI_Constant,
46
54
  HCI_Error,
47
55
  HCI_StatusError,
56
+ HCI_IsoDataPacket,
48
57
  )
49
58
  from bumble.sdp import (
50
59
  SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
@@ -55,11 +64,12 @@ from bumble.sdp import (
55
64
  DataElement,
56
65
  ServiceAttribute,
57
66
  )
58
- from bumble.transport import open_transport_or_link
67
+ from bumble.transport import open_transport
59
68
  import bumble.rfcomm
60
69
  import bumble.core
61
70
  from bumble.utils import AsyncRunner
62
71
  from bumble.pairing import PairingConfig
72
+ import bumble.logging
63
73
 
64
74
 
65
75
  # -----------------------------------------------------------------------------
@@ -75,17 +85,28 @@ DEFAULT_CENTRAL_ADDRESS = 'F0:F0:F0:F0:F0:F0'
75
85
  DEFAULT_CENTRAL_NAME = 'Speed Central'
76
86
  DEFAULT_PERIPHERAL_ADDRESS = 'F1:F1:F1:F1:F1:F1'
77
87
  DEFAULT_PERIPHERAL_NAME = 'Speed Peripheral'
88
+ DEFAULT_ADVERTISING_INTERVAL = 100
78
89
 
79
90
  SPEED_SERVICE_UUID = '50DB505C-8AC4-4738-8448-3B1D9CC09CC5'
80
91
  SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
81
92
  SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
82
93
 
83
94
  DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
95
+
84
96
  DEFAULT_L2CAP_PSM = 128
85
97
  DEFAULT_L2CAP_MAX_CREDITS = 128
86
98
  DEFAULT_L2CAP_MTU = 1024
87
99
  DEFAULT_L2CAP_MPS = 1024
88
100
 
101
+ DEFAULT_ISO_MAX_SDU_C_TO_P = 251
102
+ DEFAULT_ISO_MAX_SDU_P_TO_C = 251
103
+ DEFAULT_ISO_SDU_INTERVAL_C_TO_P = 10000
104
+ DEFAULT_ISO_SDU_INTERVAL_P_TO_C = 10000
105
+ DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P = 35
106
+ DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C = 35
107
+ DEFAULT_ISO_RTN_C_TO_P = 3
108
+ DEFAULT_ISO_RTN_P_TO_C = 3
109
+
89
110
  DEFAULT_LINGER_TIME = 1.0
90
111
  DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
91
112
 
@@ -102,14 +123,14 @@ def le_phy_name(phy_id):
102
123
  )
103
124
 
104
125
 
105
- def print_connection_phy(phy):
126
+ def print_connection_phy(phy: ConnectionPHY) -> None:
106
127
  logging.info(
107
128
  color('@@@ PHY: ', 'yellow') + f'TX:{le_phy_name(phy.tx_phy)}/'
108
129
  f'RX:{le_phy_name(phy.rx_phy)}'
109
130
  )
110
131
 
111
132
 
112
- def print_connection(connection):
133
+ def print_connection(connection: Connection) -> None:
113
134
  params = []
114
135
  if connection.transport == PhysicalTransport.LE:
115
136
  params.append(
@@ -134,6 +155,34 @@ def print_connection(connection):
134
155
  logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
135
156
 
136
157
 
158
+ def print_cis_link(cis_link: CisLink) -> None:
159
+ logging.info(color("@@@ CIS established", "green"))
160
+ logging.info(color('@@@ ISO interval: ', 'green') + f"{cis_link.iso_interval}ms")
161
+ logging.info(color('@@@ NSE: ', 'green') + f"{cis_link.nse}")
162
+ logging.info(color('@@@ Central->Peripheral:', 'green'))
163
+ if cis_link.phy_c_to_p is not None:
164
+ logging.info(
165
+ color('@@@ PHY: ', 'green') + f"{cis_link.phy_c_to_p.name}"
166
+ )
167
+ logging.info(
168
+ color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_c_to_p}µs"
169
+ )
170
+ logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_c_to_p}")
171
+ logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_c_to_p}")
172
+ logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_c_to_p}")
173
+ logging.info(color('@@@ Peripheral->Central:', 'green'))
174
+ if cis_link.phy_p_to_c is not None:
175
+ logging.info(
176
+ color('@@@ PHY: ', 'green') + f"{cis_link.phy_p_to_c.name}"
177
+ )
178
+ logging.info(
179
+ color('@@@ Latency: ', 'green') + f"{cis_link.transport_latency_p_to_c}µs"
180
+ )
181
+ logging.info(color('@@@ BN: ', 'green') + f"{cis_link.bn_p_to_c}")
182
+ logging.info(color('@@@ FT: ', 'green') + f"{cis_link.ft_p_to_c}")
183
+ logging.info(color('@@@ Max PDU: ', 'green') + f"{cis_link.max_pdu_p_to_c}")
184
+
185
+
137
186
  def make_sdp_records(channel):
138
187
  return {
139
188
  0x00010001: [
@@ -197,6 +246,51 @@ async def switch_roles(connection, role):
197
246
  logging.info(f'{color("### Role switch failed:", "red")} {error}')
198
247
 
199
248
 
249
+ async def pre_power_on(device: Device, classic: bool) -> None:
250
+ device.classic_enabled = classic
251
+
252
+ # Set up a pairing config factory with minimal requirements.
253
+ device.config.keystore = "JsonKeyStore"
254
+ device.pairing_config_factory = lambda _: PairingConfig(
255
+ sc=False, mitm=False, bonding=False
256
+ )
257
+
258
+
259
+ async def post_power_on(
260
+ device: Device,
261
+ le_scan: Optional[tuple[int, int]],
262
+ le_advertise: Optional[int],
263
+ classic_page_scan: bool,
264
+ classic_inquiry_scan: bool,
265
+ ) -> None:
266
+ if classic_page_scan:
267
+ logging.info(color("*** Enabling page scan", "blue"))
268
+ await device.set_connectable(True)
269
+ if classic_inquiry_scan:
270
+ logging.info(color("*** Enabling inquiry scan", "blue"))
271
+ await device.set_discoverable(True)
272
+
273
+ if le_scan:
274
+ scan_window, scan_interval = le_scan
275
+ logging.info(
276
+ color(
277
+ f"*** Starting LE scanning [{scan_window}ms/{scan_interval}ms]",
278
+ "blue",
279
+ )
280
+ )
281
+ await device.start_scanning(
282
+ scan_interval=scan_interval, scan_window=scan_window
283
+ )
284
+
285
+ if le_advertise:
286
+ logging.info(color(f"*** Starting LE advertising [{le_advertise}ms]", "blue"))
287
+ await device.start_advertising(
288
+ advertising_interval_min=le_advertise,
289
+ advertising_interval_max=le_advertise,
290
+ auto_restart=True,
291
+ )
292
+
293
+
200
294
  # -----------------------------------------------------------------------------
201
295
  # Packet
202
296
  # -----------------------------------------------------------------------------
@@ -414,7 +508,8 @@ class Sender:
414
508
  self.bytes_sent += len(packet)
415
509
  await self.packet_io.send_packet(packet)
416
510
 
417
- await self.done.wait()
511
+ if self.packet_io.can_receive():
512
+ await self.done.wait()
418
513
 
419
514
  run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
420
515
  logging.info(color(f'=== {run_counter} Done!', 'magenta'))
@@ -444,6 +539,9 @@ class Sender:
444
539
  )
445
540
  self.done.set()
446
541
 
542
+ def is_sender(self):
543
+ return True
544
+
447
545
 
448
546
  # -----------------------------------------------------------------------------
449
547
  # Receiver
@@ -491,7 +589,8 @@ class Receiver:
491
589
  logging.info(
492
590
  color(
493
591
  f'!!! Unexpected packet, expected {self.expected_packet_index} '
494
- f'but received {packet.sequence}'
592
+ f'but received {packet.sequence}',
593
+ 'red',
495
594
  )
496
595
  )
497
596
 
@@ -534,6 +633,9 @@ class Receiver:
534
633
  await self.done.wait()
535
634
  logging.info(color('=== Done!', 'magenta'))
536
635
 
636
+ def is_sender(self):
637
+ return False
638
+
537
639
 
538
640
  # -----------------------------------------------------------------------------
539
641
  # Ping
@@ -669,7 +771,8 @@ class Ping:
669
771
  color(
670
772
  f'!!! Unexpected packet, '
671
773
  f'expected {self.next_expected_packet_index} '
672
- f'but received {packet.sequence}'
774
+ f'but received {packet.sequence}',
775
+ 'red',
673
776
  )
674
777
  )
675
778
 
@@ -677,6 +780,9 @@ class Ping:
677
780
  self.done.set()
678
781
  return
679
782
 
783
+ def is_sender(self):
784
+ return True
785
+
680
786
 
681
787
  # -----------------------------------------------------------------------------
682
788
  # Pong
@@ -721,7 +827,8 @@ class Pong:
721
827
  logging.info(
722
828
  color(
723
829
  f'!!! Unexpected packet, expected {self.expected_packet_index} '
724
- f'but received {packet.sequence}'
830
+ f'but received {packet.sequence}',
831
+ 'red',
725
832
  )
726
833
  )
727
834
 
@@ -743,6 +850,9 @@ class Pong:
743
850
  await self.done.wait()
744
851
  logging.info(color('=== Done!', 'magenta'))
745
852
 
853
+ def is_sender(self):
854
+ return False
855
+
746
856
 
747
857
  # -----------------------------------------------------------------------------
748
858
  # GattClient
@@ -906,6 +1016,9 @@ class StreamedPacketIO:
906
1016
  # pylint: disable-next=not-callable
907
1017
  self.io_sink(struct.pack('>H', len(packet)) + packet)
908
1018
 
1019
+ def can_receive(self):
1020
+ return True
1021
+
909
1022
 
910
1023
  # -----------------------------------------------------------------------------
911
1024
  # L2capClient
@@ -1177,6 +1290,96 @@ class RfcommServer(StreamedPacketIO):
1177
1290
  await self.dlc.drain()
1178
1291
 
1179
1292
 
1293
+ # -----------------------------------------------------------------------------
1294
+ # IsoClient
1295
+ # -----------------------------------------------------------------------------
1296
+ class IsoClient(StreamedPacketIO):
1297
+ def __init__(
1298
+ self,
1299
+ device: Device,
1300
+ ) -> None:
1301
+ super().__init__()
1302
+ self.device = device
1303
+ self.ready = asyncio.Event()
1304
+ self.cis_link: Optional[CisLink] = None
1305
+
1306
+ async def on_connection(
1307
+ self, connection: Connection, cis_link: CisLink, sender: bool
1308
+ ) -> None:
1309
+ connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
1310
+ self.cis_link = cis_link
1311
+ self.io_sink = cis_link.write
1312
+ await cis_link.setup_data_path(
1313
+ cis_link.Direction.HOST_TO_CONTROLLER
1314
+ if sender
1315
+ else cis_link.Direction.CONTROLLER_TO_HOST
1316
+ )
1317
+ cis_link.sink = self.on_iso_packet
1318
+ self.ready.set()
1319
+
1320
+ def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
1321
+ self.on_packet(iso_packet.iso_sdu_fragment)
1322
+
1323
+ def on_disconnection(self, _):
1324
+ pass
1325
+
1326
+ async def drain(self):
1327
+ if self.cis_link is None:
1328
+ return
1329
+ await self.cis_link.drain()
1330
+
1331
+ def can_receive(self):
1332
+ return False
1333
+
1334
+
1335
+ # -----------------------------------------------------------------------------
1336
+ # IsoServer
1337
+ # -----------------------------------------------------------------------------
1338
+ class IsoServer(StreamedPacketIO):
1339
+ def __init__(
1340
+ self,
1341
+ device: Device,
1342
+ ):
1343
+ super().__init__()
1344
+ self.device = device
1345
+ self.cis_link: Optional[CisLink] = None
1346
+ self.ready = asyncio.Event()
1347
+
1348
+ logging.info(
1349
+ color(
1350
+ '### Listening for ISO connection',
1351
+ 'yellow',
1352
+ )
1353
+ )
1354
+
1355
+ async def on_connection(
1356
+ self, connection: Connection, cis_link: CisLink, sender: bool
1357
+ ) -> None:
1358
+ connection.on(connection.EVENT_DISCONNECTION, self.on_disconnection)
1359
+ self.io_sink = cis_link.write
1360
+ await cis_link.setup_data_path(
1361
+ cis_link.Direction.HOST_TO_CONTROLLER
1362
+ if sender
1363
+ else cis_link.Direction.CONTROLLER_TO_HOST
1364
+ )
1365
+ cis_link.sink = self.on_iso_packet
1366
+ self.ready.set()
1367
+
1368
+ def on_iso_packet(self, iso_packet: HCI_IsoDataPacket) -> None:
1369
+ self.on_packet(iso_packet.iso_sdu_fragment)
1370
+
1371
+ def on_disconnection(self, _):
1372
+ pass
1373
+
1374
+ async def drain(self):
1375
+ if self.cis_link is None:
1376
+ return
1377
+ await self.cis_link.drain()
1378
+
1379
+ def can_receive(self):
1380
+ return False
1381
+
1382
+
1180
1383
  # -----------------------------------------------------------------------------
1181
1384
  # Central
1182
1385
  # -----------------------------------------------------------------------------
@@ -1185,26 +1388,52 @@ class Central(Connection.Listener):
1185
1388
  self,
1186
1389
  transport,
1187
1390
  peripheral_address,
1188
- classic,
1189
1391
  scenario_factory,
1190
1392
  mode_factory,
1191
1393
  connection_interval,
1192
1394
  phy,
1193
1395
  authenticate,
1194
1396
  encrypt,
1397
+ iso,
1398
+ iso_sdu_interval_c_to_p,
1399
+ iso_sdu_interval_p_to_c,
1400
+ iso_max_sdu_c_to_p,
1401
+ iso_max_sdu_p_to_c,
1402
+ iso_max_transport_latency_c_to_p,
1403
+ iso_max_transport_latency_p_to_c,
1404
+ iso_rtn_c_to_p,
1405
+ iso_rtn_p_to_c,
1406
+ classic,
1195
1407
  extended_data_length,
1196
1408
  role_switch,
1409
+ le_scan,
1410
+ le_advertise,
1411
+ classic_page_scan,
1412
+ classic_inquiry_scan,
1197
1413
  ):
1198
1414
  super().__init__()
1199
1415
  self.transport = transport
1200
1416
  self.peripheral_address = peripheral_address
1201
1417
  self.classic = classic
1418
+ self.iso = iso
1419
+ self.iso_sdu_interval_c_to_p = iso_sdu_interval_c_to_p
1420
+ self.iso_sdu_interval_p_to_c = iso_sdu_interval_p_to_c
1421
+ self.iso_max_sdu_c_to_p = iso_max_sdu_c_to_p
1422
+ self.iso_max_sdu_p_to_c = iso_max_sdu_p_to_c
1423
+ self.iso_max_transport_latency_c_to_p = iso_max_transport_latency_c_to_p
1424
+ self.iso_max_transport_latency_p_to_c = iso_max_transport_latency_p_to_c
1425
+ self.iso_rtn_c_to_p = iso_rtn_c_to_p
1426
+ self.iso_rtn_p_to_c = iso_rtn_p_to_c
1202
1427
  self.scenario_factory = scenario_factory
1203
1428
  self.mode_factory = mode_factory
1204
1429
  self.authenticate = authenticate
1205
1430
  self.encrypt = encrypt or authenticate
1206
1431
  self.extended_data_length = extended_data_length
1207
1432
  self.role_switch = role_switch
1433
+ self.le_scan = le_scan
1434
+ self.le_advertise = le_advertise
1435
+ self.classic_page_scan = classic_page_scan
1436
+ self.classic_inquiry_scan = classic_inquiry_scan
1208
1437
  self.device = None
1209
1438
  self.connection = None
1210
1439
 
@@ -1241,7 +1470,7 @@ class Central(Connection.Listener):
1241
1470
 
1242
1471
  async def run(self):
1243
1472
  logging.info(color('>>> Connecting to HCI...', 'green'))
1244
- async with await open_transport_or_link(self.transport) as (
1473
+ async with await open_transport(self.transport) as (
1245
1474
  hci_source,
1246
1475
  hci_sink,
1247
1476
  ):
@@ -1254,18 +1483,22 @@ class Central(Connection.Listener):
1254
1483
  mode = self.mode_factory(self.device)
1255
1484
  scenario = self.scenario_factory(mode)
1256
1485
  self.device.classic_enabled = self.classic
1486
+ self.device.cis_enabled = self.iso
1257
1487
 
1258
1488
  # Set up a pairing config factory with minimal requirements.
1259
- self.device.config.keystore = "JsonKeyStore"
1260
1489
  self.device.pairing_config_factory = lambda _: PairingConfig(
1261
1490
  sc=False, mitm=False, bonding=False
1262
1491
  )
1263
1492
 
1493
+ await pre_power_on(self.device, self.classic)
1264
1494
  await self.device.power_on()
1265
-
1266
- if self.classic:
1267
- await self.device.set_discoverable(False)
1268
- await self.device.set_connectable(False)
1495
+ await post_power_on(
1496
+ self.device,
1497
+ self.le_scan,
1498
+ self.le_advertise,
1499
+ self.classic_page_scan,
1500
+ self.classic_inquiry_scan,
1501
+ )
1269
1502
 
1270
1503
  logging.info(
1271
1504
  color(f'### Connecting to {self.peripheral_address}...', 'cyan')
@@ -1340,7 +1573,72 @@ class Central(Connection.Listener):
1340
1573
  )
1341
1574
  )
1342
1575
 
1343
- await mode.on_connection(self.connection)
1576
+ # Setup ISO streams.
1577
+ if self.iso:
1578
+ if scenario.is_sender():
1579
+ sdu_interval_c_to_p = (
1580
+ self.iso_sdu_interval_c_to_p or DEFAULT_ISO_SDU_INTERVAL_C_TO_P
1581
+ )
1582
+ sdu_interval_p_to_c = self.iso_sdu_interval_p_to_c or 0
1583
+ max_transport_latency_c_to_p = (
1584
+ self.iso_max_transport_latency_c_to_p
1585
+ or DEFAULT_ISO_MAX_TRANSPORT_LATENCY_C_TO_P
1586
+ )
1587
+ max_transport_latency_p_to_c = (
1588
+ self.iso_max_transport_latency_p_to_c or 0
1589
+ )
1590
+ max_sdu_c_to_p = (
1591
+ self.iso_max_sdu_c_to_p or DEFAULT_ISO_MAX_SDU_C_TO_P
1592
+ )
1593
+ max_sdu_p_to_c = self.iso_max_sdu_p_to_c or 0
1594
+ rtn_c_to_p = self.iso_rtn_c_to_p or DEFAULT_ISO_RTN_C_TO_P
1595
+ rtn_p_to_c = self.iso_rtn_p_to_c or 0
1596
+ else:
1597
+ sdu_interval_p_to_c = (
1598
+ self.iso_sdu_interval_p_to_c or DEFAULT_ISO_SDU_INTERVAL_P_TO_C
1599
+ )
1600
+ sdu_interval_c_to_p = self.iso_sdu_interval_c_to_p or 0
1601
+ max_transport_latency_p_to_c = (
1602
+ self.iso_max_transport_latency_p_to_c
1603
+ or DEFAULT_ISO_MAX_TRANSPORT_LATENCY_P_TO_C
1604
+ )
1605
+ max_transport_latency_c_to_p = (
1606
+ self.iso_max_transport_latency_c_to_p or 0
1607
+ )
1608
+ max_sdu_p_to_c = (
1609
+ self.iso_max_sdu_p_to_c or DEFAULT_ISO_MAX_SDU_P_TO_C
1610
+ )
1611
+ max_sdu_c_to_p = self.iso_max_sdu_c_to_p or 0
1612
+ rtn_p_to_c = self.iso_rtn_p_to_c or DEFAULT_ISO_RTN_P_TO_C
1613
+ rtn_c_to_p = self.iso_rtn_c_to_p or 0
1614
+ cis_handles = await self.device.setup_cig(
1615
+ CigParameters(
1616
+ cig_id=1,
1617
+ sdu_interval_c_to_p=sdu_interval_c_to_p,
1618
+ sdu_interval_p_to_c=sdu_interval_p_to_c,
1619
+ max_transport_latency_c_to_p=max_transport_latency_c_to_p,
1620
+ max_transport_latency_p_to_c=max_transport_latency_p_to_c,
1621
+ cis_parameters=[
1622
+ CigParameters.CisParameters(
1623
+ cis_id=2,
1624
+ max_sdu_c_to_p=max_sdu_c_to_p,
1625
+ max_sdu_p_to_c=max_sdu_p_to_c,
1626
+ rtn_c_to_p=rtn_c_to_p,
1627
+ rtn_p_to_c=rtn_p_to_c,
1628
+ )
1629
+ ],
1630
+ )
1631
+ )
1632
+ cis_link = (
1633
+ await self.device.create_cis([(cis_handles[0], self.connection)])
1634
+ )[0]
1635
+ print_cis_link(cis_link)
1636
+
1637
+ await mode.on_connection(
1638
+ self.connection, cis_link, scenario.is_sender()
1639
+ )
1640
+ else:
1641
+ await mode.on_connection(self.connection)
1344
1642
 
1345
1643
  await scenario.run()
1346
1644
  await asyncio.sleep(DEFAULT_LINGER_TIME)
@@ -1376,24 +1674,38 @@ class Peripheral(Device.Listener, Connection.Listener):
1376
1674
  scenario_factory,
1377
1675
  mode_factory,
1378
1676
  classic,
1677
+ iso,
1379
1678
  extended_data_length,
1380
1679
  role_switch,
1680
+ le_scan,
1681
+ le_advertise,
1682
+ classic_page_scan,
1683
+ classic_inquiry_scan,
1381
1684
  ):
1382
1685
  self.transport = transport
1383
1686
  self.classic = classic
1687
+ self.iso = iso
1384
1688
  self.scenario_factory = scenario_factory
1385
1689
  self.mode_factory = mode_factory
1386
1690
  self.extended_data_length = extended_data_length
1387
1691
  self.role_switch = role_switch
1692
+ self.le_scan = le_scan
1693
+ self.classic_page_scan = classic_page_scan
1694
+ self.classic_inquiry_scan = classic_inquiry_scan
1388
1695
  self.scenario = None
1389
1696
  self.mode = None
1390
1697
  self.device = None
1391
1698
  self.connection = None
1392
1699
  self.connected = asyncio.Event()
1393
1700
 
1701
+ if le_advertise:
1702
+ self.le_advertise = le_advertise
1703
+ else:
1704
+ self.le_advertise = 0 if classic else DEFAULT_ADVERTISING_INTERVAL
1705
+
1394
1706
  async def run(self):
1395
1707
  logging.info(color('>>> Connecting to HCI...', 'green'))
1396
- async with await open_transport_or_link(self.transport) as (
1708
+ async with await open_transport(self.transport) as (
1397
1709
  hci_source,
1398
1710
  hci_sink,
1399
1711
  ):
@@ -1407,20 +1719,22 @@ class Peripheral(Device.Listener, Connection.Listener):
1407
1719
  self.mode = self.mode_factory(self.device)
1408
1720
  self.scenario = self.scenario_factory(self.mode)
1409
1721
  self.device.classic_enabled = self.classic
1722
+ self.device.cis_enabled = self.iso
1410
1723
 
1411
1724
  # Set up a pairing config factory with minimal requirements.
1412
- self.device.config.keystore = "JsonKeyStore"
1413
1725
  self.device.pairing_config_factory = lambda _: PairingConfig(
1414
1726
  sc=False, mitm=False, bonding=False
1415
1727
  )
1416
1728
 
1729
+ await pre_power_on(self.device, self.classic)
1417
1730
  await self.device.power_on()
1418
-
1419
- if self.classic:
1420
- await self.device.set_discoverable(True)
1421
- await self.device.set_connectable(True)
1422
- else:
1423
- await self.device.start_advertising(auto_restart=True)
1731
+ await post_power_on(
1732
+ self.device,
1733
+ self.le_scan,
1734
+ self.le_advertise,
1735
+ self.classic or self.classic_page_scan,
1736
+ self.classic or self.classic_inquiry_scan,
1737
+ )
1424
1738
 
1425
1739
  if self.classic:
1426
1740
  logging.info(
@@ -1442,7 +1756,21 @@ class Peripheral(Device.Listener, Connection.Listener):
1442
1756
  logging.info(color('### Connected', 'cyan'))
1443
1757
  print_connection(self.connection)
1444
1758
 
1445
- await self.mode.on_connection(self.connection)
1759
+ if self.iso:
1760
+
1761
+ async def on_cis_request(cis_link: CisLink) -> None:
1762
+ logging.info(color("@@@ Accepting CIS", "green"))
1763
+ await self.device.accept_cis_request(cis_link)
1764
+ print_cis_link(cis_link)
1765
+
1766
+ await self.mode.on_connection(
1767
+ self.connection, cis_link, self.scenario.is_sender()
1768
+ )
1769
+
1770
+ self.connection.on(self.connection.EVENT_CIS_REQUEST, on_cis_request)
1771
+ else:
1772
+ await self.mode.on_connection(self.connection)
1773
+
1446
1774
  await self.scenario.run()
1447
1775
  await asyncio.sleep(DEFAULT_LINGER_TIME)
1448
1776
 
@@ -1451,10 +1779,14 @@ class Peripheral(Device.Listener, Connection.Listener):
1451
1779
  self.connection = connection
1452
1780
  self.connected.set()
1453
1781
 
1454
- # Stop being discoverable and connectable
1782
+ # Stop being discoverable and connectable if possible
1455
1783
  if self.classic:
1456
- AsyncRunner.spawn(self.device.set_discoverable(False))
1457
- AsyncRunner.spawn(self.device.set_connectable(False))
1784
+ if not self.classic_inquiry_scan:
1785
+ logging.info(color("*** Stopping inquiry scan", "blue"))
1786
+ AsyncRunner.spawn(self.device.set_discoverable(False))
1787
+ if not self.classic_page_scan:
1788
+ logging.info(color("*** Stopping page scan", "blue"))
1789
+ AsyncRunner.spawn(self.device.set_connectable(False))
1458
1790
 
1459
1791
  # Request a new data length if needed
1460
1792
  if not self.classic and self.extended_data_length:
@@ -1475,7 +1807,9 @@ class Peripheral(Device.Listener, Connection.Listener):
1475
1807
  self.scenario.reset()
1476
1808
 
1477
1809
  if self.classic:
1810
+ logging.info(color("*** Enabling inquiry scan", "blue"))
1478
1811
  AsyncRunner.spawn(self.device.set_discoverable(True))
1812
+ logging.info(color("*** Enabling page scan", "blue"))
1479
1813
  AsyncRunner.spawn(self.device.set_connectable(True))
1480
1814
 
1481
1815
  def on_connection_parameters_update(self):
@@ -1548,6 +1882,12 @@ def create_mode_factory(ctx, default_mode):
1548
1882
  credits_threshold=ctx.obj['rfcomm_credits_threshold'],
1549
1883
  )
1550
1884
 
1885
+ if mode == 'iso-server':
1886
+ return IsoServer(device)
1887
+
1888
+ if mode == 'iso-client':
1889
+ return IsoClient(device)
1890
+
1551
1891
  raise ValueError('invalid mode')
1552
1892
 
1553
1893
  return create_mode
@@ -1575,6 +1915,9 @@ def create_scenario_factory(ctx, default_scenario):
1575
1915
  return Receiver(packet_io, ctx.obj['linger'])
1576
1916
 
1577
1917
  if scenario == 'ping':
1918
+ if isinstance(packet_io, (IsoClient, IsoServer)):
1919
+ raise ValueError('ping not supported with ISO')
1920
+
1578
1921
  return Ping(
1579
1922
  packet_io,
1580
1923
  start_delay=ctx.obj['start_delay'],
@@ -1586,6 +1929,9 @@ def create_scenario_factory(ctx, default_scenario):
1586
1929
  )
1587
1930
 
1588
1931
  if scenario == 'pong':
1932
+ if isinstance(packet_io, (IsoClient, IsoServer)):
1933
+ raise ValueError('pong not supported with ISO')
1934
+
1589
1935
  return Pong(packet_io, ctx.obj['linger'])
1590
1936
 
1591
1937
  raise ValueError('invalid scenario')
@@ -1609,6 +1955,8 @@ def create_scenario_factory(ctx, default_scenario):
1609
1955
  'l2cap-server',
1610
1956
  'rfcomm-client',
1611
1957
  'rfcomm-server',
1958
+ 'iso-client',
1959
+ 'iso-server',
1612
1960
  ]
1613
1961
  ),
1614
1962
  )
@@ -1621,6 +1969,7 @@ def create_scenario_factory(ctx, default_scenario):
1621
1969
  )
1622
1970
  @click.option(
1623
1971
  '--extended-data-length',
1972
+ metavar='<TX-OCTETS>/<TX-TIME>',
1624
1973
  help='Request a data length upon connection, specified as tx_octets/tx_time',
1625
1974
  )
1626
1975
  @click.option(
@@ -1628,6 +1977,26 @@ def create_scenario_factory(ctx, default_scenario):
1628
1977
  type=click.Choice(['central', 'peripheral']),
1629
1978
  help='Request role switch upon connection (central or peripheral)',
1630
1979
  )
1980
+ @click.option(
1981
+ '--le-scan',
1982
+ metavar='<WINDOW>/<INTERVAL>',
1983
+ help='Perform an LE scan with a given window and interval (milliseconds)',
1984
+ )
1985
+ @click.option(
1986
+ '--le-advertise',
1987
+ metavar='<INTERVAL>',
1988
+ help='Advertise with a given interval (milliseconds)',
1989
+ )
1990
+ @click.option(
1991
+ '--classic-page-scan',
1992
+ is_flag=True,
1993
+ help='Enable Classic page scanning',
1994
+ )
1995
+ @click.option(
1996
+ '--classic-inquiry-scan',
1997
+ is_flag=True,
1998
+ help='Enable Classic enquiry scanning',
1999
+ )
1631
2000
  @click.option(
1632
2001
  '--rfcomm-channel',
1633
2002
  type=int,
@@ -1753,6 +2122,10 @@ def bench(
1753
2122
  att_mtu,
1754
2123
  extended_data_length,
1755
2124
  role_switch,
2125
+ le_scan,
2126
+ le_advertise,
2127
+ classic_page_scan,
2128
+ classic_inquiry_scan,
1756
2129
  packet_size,
1757
2130
  packet_count,
1758
2131
  start_delay,
@@ -1801,7 +2174,12 @@ def bench(
1801
2174
  else None
1802
2175
  )
1803
2176
  ctx.obj['role_switch'] = role_switch
2177
+ ctx.obj['le_scan'] = [float(x) for x in le_scan.split('/')] if le_scan else None
2178
+ ctx.obj['le_advertise'] = float(le_advertise) if le_advertise else None
2179
+ ctx.obj['classic_page_scan'] = classic_page_scan
2180
+ ctx.obj['classic_inquiry_scan'] = classic_inquiry_scan
1804
2181
  ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
2182
+ ctx.obj['iso'] = mode in ('iso-client', 'iso-server')
1805
2183
 
1806
2184
 
1807
2185
  @bench.command()
@@ -1823,28 +2201,94 @@ def bench(
1823
2201
  @click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
1824
2202
  @click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
1825
2203
  @click.option('--encrypt', is_flag=True, help='Encrypt the connection (RFComm only)')
2204
+ @click.option(
2205
+ '--iso-sdu-interval-c-to-p',
2206
+ type=int,
2207
+ help='ISO SDU central -> peripheral (microseconds)',
2208
+ )
2209
+ @click.option(
2210
+ '--iso-sdu-interval-p-to-c',
2211
+ type=int,
2212
+ help='ISO SDU interval peripheral -> central (microseconds)',
2213
+ )
2214
+ @click.option(
2215
+ '--iso-max-sdu-c-to-p',
2216
+ type=int,
2217
+ help='ISO max SDU central -> peripheral',
2218
+ )
2219
+ @click.option(
2220
+ '--iso-max-sdu-p-to-c',
2221
+ type=int,
2222
+ help='ISO max SDU peripheral -> central',
2223
+ )
2224
+ @click.option(
2225
+ '--iso-max-transport-latency-c-to-p',
2226
+ type=int,
2227
+ help='ISO max transport latency central -> peripheral (milliseconds)',
2228
+ )
2229
+ @click.option(
2230
+ '--iso-max-transport-latency-p-to-c',
2231
+ type=int,
2232
+ help='ISO max transport latency peripheral -> central (milliseconds)',
2233
+ )
2234
+ @click.option(
2235
+ '--iso-rtn-c-to-p',
2236
+ type=int,
2237
+ help='ISO RTN central -> peripheral (integer count)',
2238
+ )
2239
+ @click.option(
2240
+ '--iso-rtn-p-to-c',
2241
+ type=int,
2242
+ help='ISO RTN peripheral -> central (integer count)',
2243
+ )
1826
2244
  @click.pass_context
1827
2245
  def central(
1828
- ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
2246
+ ctx,
2247
+ transport,
2248
+ peripheral_address,
2249
+ connection_interval,
2250
+ phy,
2251
+ authenticate,
2252
+ encrypt,
2253
+ iso_sdu_interval_c_to_p,
2254
+ iso_sdu_interval_p_to_c,
2255
+ iso_max_sdu_c_to_p,
2256
+ iso_max_sdu_p_to_c,
2257
+ iso_max_transport_latency_c_to_p,
2258
+ iso_max_transport_latency_p_to_c,
2259
+ iso_rtn_c_to_p,
2260
+ iso_rtn_p_to_c,
1829
2261
  ):
1830
2262
  """Run as a central (initiates the connection)"""
1831
2263
  scenario_factory = create_scenario_factory(ctx, 'send')
1832
2264
  mode_factory = create_mode_factory(ctx, 'gatt-client')
1833
- classic = ctx.obj['classic']
1834
2265
 
1835
2266
  async def run_central():
1836
2267
  await Central(
1837
2268
  transport,
1838
2269
  peripheral_address,
1839
- classic,
1840
2270
  scenario_factory,
1841
2271
  mode_factory,
1842
2272
  connection_interval,
1843
2273
  phy,
1844
2274
  authenticate,
1845
2275
  encrypt or authenticate,
2276
+ ctx.obj['iso'],
2277
+ iso_sdu_interval_c_to_p,
2278
+ iso_sdu_interval_p_to_c,
2279
+ iso_max_sdu_c_to_p,
2280
+ iso_max_sdu_p_to_c,
2281
+ iso_max_transport_latency_c_to_p,
2282
+ iso_max_transport_latency_p_to_c,
2283
+ iso_rtn_c_to_p,
2284
+ iso_rtn_p_to_c,
2285
+ ctx.obj['classic'],
1846
2286
  ctx.obj['extended_data_length'],
1847
2287
  ctx.obj['role_switch'],
2288
+ ctx.obj['le_scan'],
2289
+ ctx.obj['le_advertise'],
2290
+ ctx.obj['classic_page_scan'],
2291
+ ctx.obj['classic_inquiry_scan'],
1848
2292
  ).run()
1849
2293
 
1850
2294
  asyncio.run(run_central())
@@ -1864,19 +2308,20 @@ def peripheral(ctx, transport):
1864
2308
  scenario_factory,
1865
2309
  mode_factory,
1866
2310
  ctx.obj['classic'],
2311
+ ctx.obj['iso'],
1867
2312
  ctx.obj['extended_data_length'],
1868
2313
  ctx.obj['role_switch'],
2314
+ ctx.obj['le_scan'],
2315
+ ctx.obj['le_advertise'],
2316
+ ctx.obj['classic_page_scan'],
2317
+ ctx.obj['classic_inquiry_scan'],
1869
2318
  ).run()
1870
2319
 
1871
2320
  asyncio.run(run_peripheral())
1872
2321
 
1873
2322
 
1874
2323
  def main():
1875
- logging.basicConfig(
1876
- level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
1877
- format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
1878
- datefmt="%H:%M:%S",
1879
- )
2324
+ bumble.logging.setup_basic_logging('INFO')
1880
2325
  bench()
1881
2326
 
1882
2327