bumble 0.0.211__py3-none-any.whl → 0.0.213__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 (95) 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 +11 -9
  5. bumble/apps/bench.py +482 -31
  6. bumble/apps/console.py +5 -5
  7. bumble/apps/controller_info.py +47 -10
  8. bumble/apps/controller_loopback.py +7 -3
  9. bumble/apps/controllers.py +2 -2
  10. bumble/apps/device_info.py +2 -2
  11. bumble/apps/gatt_dump.py +2 -2
  12. bumble/apps/gg_bridge.py +2 -2
  13. bumble/apps/hci_bridge.py +2 -2
  14. bumble/apps/l2cap_bridge.py +2 -2
  15. bumble/apps/lea_unicast/app.py +6 -1
  16. bumble/apps/pair.py +204 -43
  17. bumble/apps/pandora_server.py +2 -2
  18. bumble/apps/rfcomm_bridge.py +1 -1
  19. bumble/apps/scan.py +2 -2
  20. bumble/apps/show.py +4 -2
  21. bumble/apps/speaker/speaker.html +1 -0
  22. bumble/apps/speaker/speaker.js +113 -62
  23. bumble/apps/speaker/speaker.py +126 -18
  24. bumble/at.py +4 -4
  25. bumble/att.py +15 -18
  26. bumble/avc.py +7 -7
  27. bumble/avctp.py +5 -5
  28. bumble/avdtp.py +138 -88
  29. bumble/avrcp.py +52 -58
  30. bumble/colors.py +2 -2
  31. bumble/controller.py +84 -23
  32. bumble/core.py +13 -7
  33. bumble/{crypto.py → crypto/__init__.py} +11 -95
  34. bumble/crypto/builtin.py +652 -0
  35. bumble/crypto/cryptography.py +84 -0
  36. bumble/device.py +688 -345
  37. bumble/drivers/__init__.py +2 -2
  38. bumble/drivers/common.py +0 -2
  39. bumble/drivers/intel.py +40 -40
  40. bumble/drivers/rtk.py +28 -35
  41. bumble/gatt.py +7 -9
  42. bumble/gatt_adapters.py +4 -5
  43. bumble/gatt_client.py +31 -34
  44. bumble/gatt_server.py +15 -17
  45. bumble/hci.py +2635 -2878
  46. bumble/helpers.py +4 -5
  47. bumble/hfp.py +76 -57
  48. bumble/hid.py +24 -12
  49. bumble/host.py +117 -34
  50. bumble/keys.py +68 -52
  51. bumble/l2cap.py +329 -403
  52. bumble/link.py +6 -270
  53. bumble/pairing.py +23 -20
  54. bumble/pandora/__init__.py +1 -1
  55. bumble/pandora/config.py +2 -2
  56. bumble/pandora/device.py +6 -6
  57. bumble/pandora/host.py +38 -39
  58. bumble/pandora/l2cap.py +4 -4
  59. bumble/pandora/security.py +73 -57
  60. bumble/pandora/utils.py +3 -3
  61. bumble/profiles/aics.py +3 -5
  62. bumble/profiles/ancs.py +3 -1
  63. bumble/profiles/ascs.py +143 -136
  64. bumble/profiles/asha.py +13 -8
  65. bumble/profiles/bap.py +3 -4
  66. bumble/profiles/csip.py +3 -5
  67. bumble/profiles/device_information_service.py +2 -2
  68. bumble/profiles/gap.py +2 -2
  69. bumble/profiles/gatt_service.py +1 -3
  70. bumble/profiles/hap.py +42 -58
  71. bumble/profiles/le_audio.py +4 -4
  72. bumble/profiles/mcp.py +16 -13
  73. bumble/profiles/vcs.py +8 -10
  74. bumble/profiles/vocs.py +6 -9
  75. bumble/rfcomm.py +27 -18
  76. bumble/rtp.py +1 -2
  77. bumble/sdp.py +2 -2
  78. bumble/smp.py +71 -69
  79. bumble/tools/rtk_util.py +2 -2
  80. bumble/transport/__init__.py +2 -16
  81. bumble/transport/android_netsim.py +5 -5
  82. bumble/transport/common.py +4 -4
  83. bumble/transport/pyusb.py +2 -2
  84. bumble/utils.py +2 -5
  85. bumble/vendor/android/hci.py +118 -200
  86. bumble/vendor/zephyr/hci.py +32 -27
  87. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/METADATA +5 -5
  88. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/RECORD +92 -93
  89. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/WHEEL +1 -1
  90. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/entry_points.txt +0 -1
  91. bumble/apps/link_relay/__init__.py +0 -0
  92. bumble/apps/link_relay/link_relay.py +0 -289
  93. bumble/apps/link_relay/logging.yml +0 -21
  94. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/licenses/LICENSE +0 -0
  95. {bumble-0.0.211.dist-info → bumble-0.0.213.dist-info}/top_level.txt +0 -0
bumble/apps/bench.py CHANGED
@@ -23,6 +23,7 @@ import os
23
23
  import statistics
24
24
  import struct
25
25
  import time
26
+ from typing import Optional
26
27
 
27
28
  import click
28
29
 
@@ -35,7 +36,15 @@ from bumble.core import (
35
36
  CommandTimeoutError,
36
37
  )
37
38
  from bumble.colors import color
38
- from bumble.device import Connection, ConnectionParametersPreferences, Device, Peer
39
+ from bumble.core import ConnectionPHY
40
+ from bumble.device import (
41
+ CigParameters,
42
+ CisLink,
43
+ Connection,
44
+ ConnectionParametersPreferences,
45
+ Device,
46
+ Peer,
47
+ )
39
48
  from bumble.gatt import Characteristic, CharacteristicValue, Service
40
49
  from bumble.hci import (
41
50
  HCI_LE_1M_PHY,
@@ -45,6 +54,7 @@ from bumble.hci import (
45
54
  HCI_Constant,
46
55
  HCI_Error,
47
56
  HCI_StatusError,
57
+ HCI_IsoDataPacket,
48
58
  )
49
59
  from bumble.sdp import (
50
60
  SDP_BROWSE_GROUP_LIST_ATTRIBUTE_ID,
@@ -55,7 +65,7 @@ from bumble.sdp import (
55
65
  DataElement,
56
66
  ServiceAttribute,
57
67
  )
58
- from bumble.transport import open_transport_or_link
68
+ from bumble.transport import open_transport
59
69
  import bumble.rfcomm
60
70
  import bumble.core
61
71
  from bumble.utils import AsyncRunner
@@ -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(
@@ -121,9 +142,9 @@ def print_connection(connection):
121
142
 
122
143
  params.append(
123
144
  'Parameters='
124
- f'{connection.parameters.connection_interval * 1.25:.2f}/'
145
+ f'{connection.parameters.connection_interval:.2f}/'
125
146
  f'{connection.parameters.peripheral_latency}/'
126
- f'{connection.parameters.supervision_timeout * 10} '
147
+ f'{connection.parameters.supervision_timeout:.2f} '
127
148
  )
128
149
 
129
150
  params.append(f'MTU={connection.att_mtu}')
@@ -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,17 +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
1489
  self.device.pairing_config_factory = lambda _: PairingConfig(
1260
1490
  sc=False, mitm=False, bonding=False
1261
1491
  )
1262
1492
 
1493
+ await pre_power_on(self.device, self.classic)
1263
1494
  await self.device.power_on()
1264
-
1265
- if self.classic:
1266
- await self.device.set_discoverable(False)
1267
- 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
+ )
1268
1502
 
1269
1503
  logging.info(
1270
1504
  color(f'### Connecting to {self.peripheral_address}...', 'cyan')
@@ -1339,7 +1573,72 @@ class Central(Connection.Listener):
1339
1573
  )
1340
1574
  )
1341
1575
 
1342
- 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)
1343
1642
 
1344
1643
  await scenario.run()
1345
1644
  await asyncio.sleep(DEFAULT_LINGER_TIME)
@@ -1375,24 +1674,38 @@ class Peripheral(Device.Listener, Connection.Listener):
1375
1674
  scenario_factory,
1376
1675
  mode_factory,
1377
1676
  classic,
1677
+ iso,
1378
1678
  extended_data_length,
1379
1679
  role_switch,
1680
+ le_scan,
1681
+ le_advertise,
1682
+ classic_page_scan,
1683
+ classic_inquiry_scan,
1380
1684
  ):
1381
1685
  self.transport = transport
1382
1686
  self.classic = classic
1687
+ self.iso = iso
1383
1688
  self.scenario_factory = scenario_factory
1384
1689
  self.mode_factory = mode_factory
1385
1690
  self.extended_data_length = extended_data_length
1386
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
1387
1695
  self.scenario = None
1388
1696
  self.mode = None
1389
1697
  self.device = None
1390
1698
  self.connection = None
1391
1699
  self.connected = asyncio.Event()
1392
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
+
1393
1706
  async def run(self):
1394
1707
  logging.info(color('>>> Connecting to HCI...', 'green'))
1395
- async with await open_transport_or_link(self.transport) as (
1708
+ async with await open_transport(self.transport) as (
1396
1709
  hci_source,
1397
1710
  hci_sink,
1398
1711
  ):
@@ -1406,19 +1719,22 @@ class Peripheral(Device.Listener, Connection.Listener):
1406
1719
  self.mode = self.mode_factory(self.device)
1407
1720
  self.scenario = self.scenario_factory(self.mode)
1408
1721
  self.device.classic_enabled = self.classic
1722
+ self.device.cis_enabled = self.iso
1409
1723
 
1410
1724
  # Set up a pairing config factory with minimal requirements.
1411
1725
  self.device.pairing_config_factory = lambda _: PairingConfig(
1412
1726
  sc=False, mitm=False, bonding=False
1413
1727
  )
1414
1728
 
1729
+ await pre_power_on(self.device, self.classic)
1415
1730
  await self.device.power_on()
1416
-
1417
- if self.classic:
1418
- await self.device.set_discoverable(True)
1419
- await self.device.set_connectable(True)
1420
- else:
1421
- 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
+ )
1422
1738
 
1423
1739
  if self.classic:
1424
1740
  logging.info(
@@ -1440,7 +1756,21 @@ class Peripheral(Device.Listener, Connection.Listener):
1440
1756
  logging.info(color('### Connected', 'cyan'))
1441
1757
  print_connection(self.connection)
1442
1758
 
1443
- 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
+
1444
1774
  await self.scenario.run()
1445
1775
  await asyncio.sleep(DEFAULT_LINGER_TIME)
1446
1776
 
@@ -1449,10 +1779,14 @@ class Peripheral(Device.Listener, Connection.Listener):
1449
1779
  self.connection = connection
1450
1780
  self.connected.set()
1451
1781
 
1452
- # Stop being discoverable and connectable
1782
+ # Stop being discoverable and connectable if possible
1453
1783
  if self.classic:
1454
- AsyncRunner.spawn(self.device.set_discoverable(False))
1455
- 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))
1456
1790
 
1457
1791
  # Request a new data length if needed
1458
1792
  if not self.classic and self.extended_data_length:
@@ -1473,7 +1807,9 @@ class Peripheral(Device.Listener, Connection.Listener):
1473
1807
  self.scenario.reset()
1474
1808
 
1475
1809
  if self.classic:
1810
+ logging.info(color("*** Enabling inquiry scan", "blue"))
1476
1811
  AsyncRunner.spawn(self.device.set_discoverable(True))
1812
+ logging.info(color("*** Enabling page scan", "blue"))
1477
1813
  AsyncRunner.spawn(self.device.set_connectable(True))
1478
1814
 
1479
1815
  def on_connection_parameters_update(self):
@@ -1546,6 +1882,12 @@ def create_mode_factory(ctx, default_mode):
1546
1882
  credits_threshold=ctx.obj['rfcomm_credits_threshold'],
1547
1883
  )
1548
1884
 
1885
+ if mode == 'iso-server':
1886
+ return IsoServer(device)
1887
+
1888
+ if mode == 'iso-client':
1889
+ return IsoClient(device)
1890
+
1549
1891
  raise ValueError('invalid mode')
1550
1892
 
1551
1893
  return create_mode
@@ -1573,6 +1915,9 @@ def create_scenario_factory(ctx, default_scenario):
1573
1915
  return Receiver(packet_io, ctx.obj['linger'])
1574
1916
 
1575
1917
  if scenario == 'ping':
1918
+ if isinstance(packet_io, (IsoClient, IsoServer)):
1919
+ raise ValueError('ping not supported with ISO')
1920
+
1576
1921
  return Ping(
1577
1922
  packet_io,
1578
1923
  start_delay=ctx.obj['start_delay'],
@@ -1584,6 +1929,9 @@ def create_scenario_factory(ctx, default_scenario):
1584
1929
  )
1585
1930
 
1586
1931
  if scenario == 'pong':
1932
+ if isinstance(packet_io, (IsoClient, IsoServer)):
1933
+ raise ValueError('pong not supported with ISO')
1934
+
1587
1935
  return Pong(packet_io, ctx.obj['linger'])
1588
1936
 
1589
1937
  raise ValueError('invalid scenario')
@@ -1607,6 +1955,8 @@ def create_scenario_factory(ctx, default_scenario):
1607
1955
  'l2cap-server',
1608
1956
  'rfcomm-client',
1609
1957
  'rfcomm-server',
1958
+ 'iso-client',
1959
+ 'iso-server',
1610
1960
  ]
1611
1961
  ),
1612
1962
  )
@@ -1619,6 +1969,7 @@ def create_scenario_factory(ctx, default_scenario):
1619
1969
  )
1620
1970
  @click.option(
1621
1971
  '--extended-data-length',
1972
+ metavar='<TX-OCTETS>/<TX-TIME>',
1622
1973
  help='Request a data length upon connection, specified as tx_octets/tx_time',
1623
1974
  )
1624
1975
  @click.option(
@@ -1626,6 +1977,26 @@ def create_scenario_factory(ctx, default_scenario):
1626
1977
  type=click.Choice(['central', 'peripheral']),
1627
1978
  help='Request role switch upon connection (central or peripheral)',
1628
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
+ )
1629
2000
  @click.option(
1630
2001
  '--rfcomm-channel',
1631
2002
  type=int,
@@ -1751,6 +2122,10 @@ def bench(
1751
2122
  att_mtu,
1752
2123
  extended_data_length,
1753
2124
  role_switch,
2125
+ le_scan,
2126
+ le_advertise,
2127
+ classic_page_scan,
2128
+ classic_inquiry_scan,
1754
2129
  packet_size,
1755
2130
  packet_count,
1756
2131
  start_delay,
@@ -1799,7 +2174,12 @@ def bench(
1799
2174
  else None
1800
2175
  )
1801
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
1802
2181
  ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
2182
+ ctx.obj['iso'] = mode in ('iso-client', 'iso-server')
1803
2183
 
1804
2184
 
1805
2185
  @bench.command()
@@ -1821,28 +2201,94 @@ def bench(
1821
2201
  @click.option('--phy', type=click.Choice(['1m', '2m', 'coded']), help='PHY to use')
1822
2202
  @click.option('--authenticate', is_flag=True, help='Authenticate (RFComm only)')
1823
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
+ )
1824
2244
  @click.pass_context
1825
2245
  def central(
1826
- 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,
1827
2261
  ):
1828
2262
  """Run as a central (initiates the connection)"""
1829
2263
  scenario_factory = create_scenario_factory(ctx, 'send')
1830
2264
  mode_factory = create_mode_factory(ctx, 'gatt-client')
1831
- classic = ctx.obj['classic']
1832
2265
 
1833
2266
  async def run_central():
1834
2267
  await Central(
1835
2268
  transport,
1836
2269
  peripheral_address,
1837
- classic,
1838
2270
  scenario_factory,
1839
2271
  mode_factory,
1840
2272
  connection_interval,
1841
2273
  phy,
1842
2274
  authenticate,
1843
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'],
1844
2286
  ctx.obj['extended_data_length'],
1845
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'],
1846
2292
  ).run()
1847
2293
 
1848
2294
  asyncio.run(run_central())
@@ -1862,8 +2308,13 @@ def peripheral(ctx, transport):
1862
2308
  scenario_factory,
1863
2309
  mode_factory,
1864
2310
  ctx.obj['classic'],
2311
+ ctx.obj['iso'],
1865
2312
  ctx.obj['extended_data_length'],
1866
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'],
1867
2318
  ).run()
1868
2319
 
1869
2320
  asyncio.run(run_peripheral())