bumble 0.0.180__py3-none-any.whl → 0.0.182__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 (42) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/bench.py +397 -133
  3. bumble/apps/ble_rpa_tool.py +63 -0
  4. bumble/apps/console.py +4 -4
  5. bumble/apps/controller_info.py +64 -6
  6. bumble/apps/controller_loopback.py +200 -0
  7. bumble/apps/l2cap_bridge.py +32 -24
  8. bumble/apps/pair.py +6 -8
  9. bumble/att.py +53 -11
  10. bumble/controller.py +159 -24
  11. bumble/crypto.py +10 -0
  12. bumble/device.py +580 -113
  13. bumble/drivers/__init__.py +27 -31
  14. bumble/drivers/common.py +45 -0
  15. bumble/drivers/rtk.py +11 -4
  16. bumble/gatt.py +66 -51
  17. bumble/gatt_server.py +30 -22
  18. bumble/hci.py +258 -91
  19. bumble/helpers.py +14 -0
  20. bumble/hfp.py +37 -27
  21. bumble/hid.py +282 -61
  22. bumble/host.py +158 -93
  23. bumble/l2cap.py +11 -6
  24. bumble/link.py +55 -1
  25. bumble/profiles/asha_service.py +2 -2
  26. bumble/profiles/bap.py +1247 -0
  27. bumble/profiles/cap.py +52 -0
  28. bumble/profiles/csip.py +119 -9
  29. bumble/rfcomm.py +31 -20
  30. bumble/smp.py +1 -1
  31. bumble/transport/__init__.py +51 -22
  32. bumble/transport/android_emulator.py +1 -1
  33. bumble/transport/common.py +2 -1
  34. bumble/transport/hci_socket.py +1 -4
  35. bumble/transport/usb.py +1 -1
  36. bumble/utils.py +3 -6
  37. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/METADATA +1 -1
  38. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/RECORD +42 -37
  39. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/entry_points.txt +1 -0
  40. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/LICENSE +0 -0
  41. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/WHEEL +0 -0
  42. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/top_level.txt +0 -0
bumble/apps/bench.py CHANGED
@@ -80,12 +80,13 @@ SPEED_TX_UUID = 'E789C754-41A1-45F4-A948-A0A1A90DBA53'
80
80
  SPEED_RX_UUID = '016A2CC7-E14B-4819-935F-1F56EAE4098D'
81
81
 
82
82
  DEFAULT_RFCOMM_UUID = 'E6D55659-C8B4-4B85-96BB-B1143AF6D3AE'
83
- DEFAULT_L2CAP_PSM = 1234
83
+ DEFAULT_L2CAP_PSM = 128
84
84
  DEFAULT_L2CAP_MAX_CREDITS = 128
85
- DEFAULT_L2CAP_MTU = 1022
85
+ DEFAULT_L2CAP_MTU = 1024
86
86
  DEFAULT_L2CAP_MPS = 1024
87
87
 
88
88
  DEFAULT_LINGER_TIME = 1.0
89
+ DEFAULT_POST_CONNECTION_WAIT_TIME = 1.0
89
90
 
90
91
  DEFAULT_RFCOMM_CHANNEL = 8
91
92
 
@@ -95,7 +96,7 @@ DEFAULT_RFCOMM_CHANNEL = 8
95
96
  # -----------------------------------------------------------------------------
96
97
  def parse_packet(packet):
97
98
  if len(packet) < 1:
98
- print(
99
+ logging.info(
99
100
  color(f'!!! Packet too short (got {len(packet)} bytes, need >= 1)', 'red')
100
101
  )
101
102
  raise ValueError('packet too short')
@@ -103,7 +104,7 @@ def parse_packet(packet):
103
104
  try:
104
105
  packet_type = PacketType(packet[0])
105
106
  except ValueError:
106
- print(color(f'!!! Invalid packet type 0x{packet[0]:02X}', 'red'))
107
+ logging.info(color(f'!!! Invalid packet type 0x{packet[0]:02X}', 'red'))
107
108
  raise
108
109
 
109
110
  return (packet_type, packet[1:])
@@ -111,7 +112,7 @@ def parse_packet(packet):
111
112
 
112
113
  def parse_packet_sequence(packet_data):
113
114
  if len(packet_data) < 5:
114
- print(
115
+ logging.info(
115
116
  color(
116
117
  f'!!!Packet too short (got {len(packet_data)} bytes, need >= 5)',
117
118
  'red',
@@ -155,7 +156,7 @@ def print_connection(connection):
155
156
 
156
157
  mtu = connection.att_mtu
157
158
 
158
- print(
159
+ logging.info(
159
160
  f'{color("@@@ Connection:", "yellow")} '
160
161
  f'{connection_parameters} '
161
162
  f'{data_length} '
@@ -239,6 +240,23 @@ async def find_rfcomm_channel_with_uuid(connection: Connection, uuid: str) -> in
239
240
  return 0
240
241
 
241
242
 
243
+ def log_stats(title, stats):
244
+ stats_min = min(stats)
245
+ stats_max = max(stats)
246
+ stats_avg = sum(stats) / len(stats)
247
+ logging.info(
248
+ color(
249
+ (
250
+ f'### {title} stats: '
251
+ f'min={stats_min:.2f}, '
252
+ f'max={stats_max:.2f}, '
253
+ f'average={stats_avg:.2f}'
254
+ ),
255
+ 'cyan',
256
+ )
257
+ )
258
+
259
+
242
260
  class PacketType(enum.IntEnum):
243
261
  RESET = 0
244
262
  SEQUENCE = 1
@@ -252,45 +270,88 @@ PACKET_FLAG_LAST = 1
252
270
  # Sender
253
271
  # -----------------------------------------------------------------------------
254
272
  class Sender:
255
- def __init__(self, packet_io, start_delay, packet_size, packet_count):
273
+ def __init__(
274
+ self,
275
+ packet_io,
276
+ start_delay,
277
+ repeat,
278
+ repeat_delay,
279
+ pace,
280
+ packet_size,
281
+ packet_count,
282
+ ):
256
283
  self.tx_start_delay = start_delay
257
284
  self.tx_packet_size = packet_size
258
285
  self.tx_packet_count = packet_count
259
286
  self.packet_io = packet_io
260
287
  self.packet_io.packet_listener = self
288
+ self.repeat = repeat
289
+ self.repeat_delay = repeat_delay
290
+ self.pace = pace
261
291
  self.start_time = 0
262
292
  self.bytes_sent = 0
293
+ self.stats = []
263
294
  self.done = asyncio.Event()
264
295
 
265
296
  def reset(self):
266
297
  pass
267
298
 
268
299
  async def run(self):
269
- print(color('--- Waiting for I/O to be ready...', 'blue'))
300
+ logging.info(color('--- Waiting for I/O to be ready...', 'blue'))
270
301
  await self.packet_io.ready.wait()
271
- print(color('--- Go!', 'blue'))
272
-
273
- if self.tx_start_delay:
274
- print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
275
- await asyncio.sleep(self.tx_start_delay)
276
-
277
- print(color('=== Sending RESET', 'magenta'))
278
- await self.packet_io.send_packet(bytes([PacketType.RESET]))
279
- self.start_time = time.time()
280
- for tx_i in range(self.tx_packet_count):
281
- packet_flags = PACKET_FLAG_LAST if tx_i == self.tx_packet_count - 1 else 0
282
- packet = struct.pack(
283
- '>bbI',
284
- PacketType.SEQUENCE,
285
- packet_flags,
286
- tx_i,
287
- ) + bytes(self.tx_packet_size - 6)
288
- print(color(f'Sending packet {tx_i}: {len(packet)} bytes', 'yellow'))
289
- self.bytes_sent += len(packet)
290
- await self.packet_io.send_packet(packet)
302
+ logging.info(color('--- Go!', 'blue'))
303
+
304
+ for run in range(self.repeat + 1):
305
+ self.done.clear()
306
+
307
+ if run > 0 and self.repeat and self.repeat_delay:
308
+ logging.info(color(f'*** Repeat delay: {self.repeat_delay}', 'green'))
309
+ await asyncio.sleep(self.repeat_delay)
310
+
311
+ if self.tx_start_delay:
312
+ logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
313
+ await asyncio.sleep(self.tx_start_delay)
314
+
315
+ logging.info(color('=== Sending RESET', 'magenta'))
316
+ await self.packet_io.send_packet(bytes([PacketType.RESET]))
317
+ self.start_time = time.time()
318
+ self.bytes_sent = 0
319
+ for tx_i in range(self.tx_packet_count):
320
+ packet_flags = (
321
+ PACKET_FLAG_LAST if tx_i == self.tx_packet_count - 1 else 0
322
+ )
323
+ packet = struct.pack(
324
+ '>bbI',
325
+ PacketType.SEQUENCE,
326
+ packet_flags,
327
+ tx_i,
328
+ ) + bytes(self.tx_packet_size - 6 - self.packet_io.overhead_size)
329
+ logging.info(
330
+ color(
331
+ f'Sending packet {tx_i}: {self.tx_packet_size} bytes', 'yellow'
332
+ )
333
+ )
334
+ self.bytes_sent += len(packet)
335
+ await self.packet_io.send_packet(packet)
291
336
 
292
- await self.done.wait()
293
- print(color('=== Done!', 'magenta'))
337
+ if self.pace is None:
338
+ continue
339
+
340
+ if self.pace > 0:
341
+ await asyncio.sleep(self.pace / 1000)
342
+ else:
343
+ await self.packet_io.drain()
344
+
345
+ await self.done.wait()
346
+
347
+ run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
348
+ logging.info(color(f'=== {run_counter} Done!', 'magenta'))
349
+
350
+ if self.repeat:
351
+ log_stats('Run', self.stats)
352
+
353
+ if self.repeat:
354
+ logging.info(color('--- End of runs', 'blue'))
294
355
 
295
356
  def on_packet_received(self, packet):
296
357
  try:
@@ -301,7 +362,8 @@ class Sender:
301
362
  if packet_type == PacketType.ACK:
302
363
  elapsed = time.time() - self.start_time
303
364
  average_tx_speed = self.bytes_sent / elapsed
304
- print(
365
+ self.stats.append(average_tx_speed)
366
+ logging.info(
305
367
  color(
306
368
  f'@@@ Received ACK. Speed: average={average_tx_speed:.4f}'
307
369
  f' ({self.bytes_sent} bytes in {elapsed:.2f} seconds)',
@@ -315,17 +377,21 @@ class Sender:
315
377
  # Receiver
316
378
  # -----------------------------------------------------------------------------
317
379
  class Receiver:
318
- def __init__(self, packet_io):
380
+ expected_packet_index: int
381
+ start_timestamp: float
382
+ last_timestamp: float
383
+
384
+ def __init__(self, packet_io, linger):
319
385
  self.reset()
320
386
  self.packet_io = packet_io
321
387
  self.packet_io.packet_listener = self
388
+ self.linger = linger
322
389
  self.done = asyncio.Event()
323
390
 
324
391
  def reset(self):
325
392
  self.expected_packet_index = 0
326
- self.start_timestamp = 0.0
327
- self.last_timestamp = 0.0
328
- self.bytes_received = 0
393
+ self.measurements = [(time.time(), 0)]
394
+ self.total_bytes_received = 0
329
395
 
330
396
  def on_packet_received(self, packet):
331
397
  try:
@@ -333,44 +399,50 @@ class Receiver:
333
399
  except ValueError:
334
400
  return
335
401
 
336
- now = time.time()
337
-
338
402
  if packet_type == PacketType.RESET:
339
- print(color('=== Received RESET', 'magenta'))
403
+ logging.info(color('=== Received RESET', 'magenta'))
340
404
  self.reset()
341
- self.start_timestamp = now
342
405
  return
343
406
 
344
407
  try:
345
408
  packet_flags, packet_index = parse_packet_sequence(packet_data)
346
409
  except ValueError:
347
410
  return
348
- print(
411
+ logging.info(
349
412
  f'<<< Received packet {packet_index}: '
350
- f'flags=0x{packet_flags:02X}, {len(packet)} bytes'
413
+ f'flags=0x{packet_flags:02X}, '
414
+ f'{len(packet) + self.packet_io.overhead_size} bytes'
351
415
  )
352
416
 
353
417
  if packet_index != self.expected_packet_index:
354
- print(
418
+ logging.info(
355
419
  color(
356
420
  f'!!! Unexpected packet, expected {self.expected_packet_index} '
357
421
  f'but received {packet_index}'
358
422
  )
359
423
  )
360
424
 
361
- elapsed_since_start = now - self.start_timestamp
362
- elapsed_since_last = now - self.last_timestamp
363
- self.bytes_received += len(packet)
425
+ now = time.time()
426
+ elapsed_since_start = now - self.measurements[0][0]
427
+ elapsed_since_last = now - self.measurements[-1][0]
428
+ self.measurements.append((now, len(packet)))
429
+ self.total_bytes_received += len(packet)
364
430
  instant_rx_speed = len(packet) / elapsed_since_last
365
- average_rx_speed = self.bytes_received / elapsed_since_start
366
- print(
431
+ average_rx_speed = self.total_bytes_received / elapsed_since_start
432
+ window = self.measurements[-64:]
433
+ windowed_rx_speed = sum(measurement[1] for measurement in window[1:]) / (
434
+ window[-1][0] - window[0][0]
435
+ )
436
+ logging.info(
367
437
  color(
368
- f'Speed: instant={instant_rx_speed:.4f}, average={average_rx_speed:.4f}',
438
+ 'Speed: '
439
+ f'instant={instant_rx_speed:.4f}, '
440
+ f'windowed={windowed_rx_speed:.4f}, '
441
+ f'average={average_rx_speed:.4f}',
369
442
  'yellow',
370
443
  )
371
444
  )
372
445
 
373
- self.last_timestamp = now
374
446
  self.expected_packet_index = packet_index + 1
375
447
 
376
448
  if packet_flags & PACKET_FLAG_LAST:
@@ -379,52 +451,104 @@ class Receiver:
379
451
  struct.pack('>bbI', PacketType.ACK, packet_flags, packet_index)
380
452
  )
381
453
  )
382
- print(color('@@@ Received last packet', 'green'))
383
- self.done.set()
454
+ logging.info(color('@@@ Received last packet', 'green'))
455
+ if not self.linger:
456
+ self.done.set()
384
457
 
385
458
  async def run(self):
386
459
  await self.done.wait()
387
- print(color('=== Done!', 'magenta'))
460
+ logging.info(color('=== Done!', 'magenta'))
388
461
 
389
462
 
390
463
  # -----------------------------------------------------------------------------
391
464
  # Ping
392
465
  # -----------------------------------------------------------------------------
393
466
  class Ping:
394
- def __init__(self, packet_io, start_delay, packet_size, packet_count):
467
+ def __init__(
468
+ self,
469
+ packet_io,
470
+ start_delay,
471
+ repeat,
472
+ repeat_delay,
473
+ pace,
474
+ packet_size,
475
+ packet_count,
476
+ ):
395
477
  self.tx_start_delay = start_delay
396
478
  self.tx_packet_size = packet_size
397
479
  self.tx_packet_count = packet_count
398
480
  self.packet_io = packet_io
399
481
  self.packet_io.packet_listener = self
482
+ self.repeat = repeat
483
+ self.repeat_delay = repeat_delay
484
+ self.pace = pace
400
485
  self.done = asyncio.Event()
401
486
  self.current_packet_index = 0
402
487
  self.ping_sent_time = 0.0
403
488
  self.latencies = []
489
+ self.min_stats = []
490
+ self.max_stats = []
491
+ self.avg_stats = []
404
492
 
405
493
  def reset(self):
406
494
  pass
407
495
 
408
496
  async def run(self):
409
- print(color('--- Waiting for I/O to be ready...', 'blue'))
497
+ logging.info(color('--- Waiting for I/O to be ready...', 'blue'))
410
498
  await self.packet_io.ready.wait()
411
- print(color('--- Go!', 'blue'))
499
+ logging.info(color('--- Go!', 'blue'))
412
500
 
413
- if self.tx_start_delay:
414
- print(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
415
- await asyncio.sleep(self.tx_start_delay)
501
+ for run in range(self.repeat + 1):
502
+ self.done.clear()
416
503
 
417
- print(color('=== Sending RESET', 'magenta'))
418
- await self.packet_io.send_packet(bytes([PacketType.RESET]))
504
+ if run > 0 and self.repeat and self.repeat_delay:
505
+ logging.info(color(f'*** Repeat delay: {self.repeat_delay}', 'green'))
506
+ await asyncio.sleep(self.repeat_delay)
419
507
 
420
- await self.send_next_ping()
508
+ if self.tx_start_delay:
509
+ logging.info(color(f'*** Startup delay: {self.tx_start_delay}', 'blue'))
510
+ await asyncio.sleep(self.tx_start_delay)
421
511
 
422
- await self.done.wait()
423
- average_latency = sum(self.latencies) / len(self.latencies)
424
- print(color(f'@@@ Average latency: {average_latency:.2f}'))
425
- print(color('=== Done!', 'magenta'))
512
+ logging.info(color('=== Sending RESET', 'magenta'))
513
+ await self.packet_io.send_packet(bytes([PacketType.RESET]))
514
+
515
+ self.current_packet_index = 0
516
+ self.latencies = []
517
+ await self.send_next_ping()
518
+
519
+ await self.done.wait()
520
+
521
+ min_latency = min(self.latencies)
522
+ max_latency = max(self.latencies)
523
+ avg_latency = sum(self.latencies) / len(self.latencies)
524
+ logging.info(
525
+ color(
526
+ '@@@ Latencies: '
527
+ f'min={min_latency:.2f}, '
528
+ f'max={max_latency:.2f}, '
529
+ f'average={avg_latency:.2f}'
530
+ )
531
+ )
532
+
533
+ self.min_stats.append(min_latency)
534
+ self.max_stats.append(max_latency)
535
+ self.avg_stats.append(avg_latency)
536
+
537
+ run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
538
+ logging.info(color(f'=== {run_counter} Done!', 'magenta'))
539
+
540
+ if self.repeat:
541
+ log_stats('Min Latency', self.min_stats)
542
+ log_stats('Max Latency', self.max_stats)
543
+ log_stats('Average Latency', self.avg_stats)
544
+
545
+ if self.repeat:
546
+ logging.info(color('--- End of runs', 'blue'))
426
547
 
427
548
  async def send_next_ping(self):
549
+ if self.pace:
550
+ await asyncio.sleep(self.pace / 1000)
551
+
428
552
  packet = struct.pack(
429
553
  '>bbI',
430
554
  PacketType.SEQUENCE,
@@ -433,7 +557,7 @@ class Ping:
433
557
  else 0,
434
558
  self.current_packet_index,
435
559
  ) + bytes(self.tx_packet_size - 6)
436
- print(color(f'Sending packet {self.current_packet_index}', 'yellow'))
560
+ logging.info(color(f'Sending packet {self.current_packet_index}', 'yellow'))
437
561
  self.ping_sent_time = time.time()
438
562
  await self.packet_io.send_packet(packet)
439
563
 
@@ -453,7 +577,7 @@ class Ping:
453
577
  if packet_type == PacketType.ACK:
454
578
  latency = elapsed * 1000
455
579
  self.latencies.append(latency)
456
- print(
580
+ logging.info(
457
581
  color(
458
582
  f'<<< Received ACK [{packet_index}], latency={latency:.2f}ms',
459
583
  'green',
@@ -463,7 +587,7 @@ class Ping:
463
587
  if packet_index == self.current_packet_index:
464
588
  self.current_packet_index += 1
465
589
  else:
466
- print(
590
+ logging.info(
467
591
  color(
468
592
  f'!!! Unexpected packet, expected {self.current_packet_index} '
469
593
  f'but received {packet_index}'
@@ -481,10 +605,13 @@ class Ping:
481
605
  # Pong
482
606
  # -----------------------------------------------------------------------------
483
607
  class Pong:
484
- def __init__(self, packet_io):
608
+ expected_packet_index: int
609
+
610
+ def __init__(self, packet_io, linger):
485
611
  self.reset()
486
612
  self.packet_io = packet_io
487
613
  self.packet_io.packet_listener = self
614
+ self.linger = linger
488
615
  self.done = asyncio.Event()
489
616
 
490
617
  def reset(self):
@@ -497,7 +624,7 @@ class Pong:
497
624
  return
498
625
 
499
626
  if packet_type == PacketType.RESET:
500
- print(color('=== Received RESET', 'magenta'))
627
+ logging.info(color('=== Received RESET', 'magenta'))
501
628
  self.reset()
502
629
  return
503
630
 
@@ -505,7 +632,7 @@ class Pong:
505
632
  packet_flags, packet_index = parse_packet_sequence(packet_data)
506
633
  except ValueError:
507
634
  return
508
- print(
635
+ logging.info(
509
636
  color(
510
637
  f'<<< Received packet {packet_index}: '
511
638
  f'flags=0x{packet_flags:02X}, {len(packet)} bytes',
@@ -514,7 +641,7 @@ class Pong:
514
641
  )
515
642
 
516
643
  if packet_index != self.expected_packet_index:
517
- print(
644
+ logging.info(
518
645
  color(
519
646
  f'!!! Unexpected packet, expected {self.expected_packet_index} '
520
647
  f'but received {packet_index}'
@@ -529,12 +656,12 @@ class Pong:
529
656
  )
530
657
  )
531
658
 
532
- if packet_flags & PACKET_FLAG_LAST:
659
+ if packet_flags & PACKET_FLAG_LAST and not self.linger:
533
660
  self.done.set()
534
661
 
535
662
  async def run(self):
536
663
  await self.done.wait()
537
- print(color('=== Done!', 'magenta'))
664
+ logging.info(color('=== Done!', 'magenta'))
538
665
 
539
666
 
540
667
  # -----------------------------------------------------------------------------
@@ -547,41 +674,42 @@ class GattClient:
547
674
  self.speed_tx = None
548
675
  self.packet_listener = None
549
676
  self.ready = asyncio.Event()
677
+ self.overhead_size = 0
550
678
 
551
679
  async def on_connection(self, connection):
552
680
  peer = Peer(connection)
553
681
 
554
682
  if self.att_mtu:
555
- print(color(f'*** Requesting MTU update: {self.att_mtu}', 'blue'))
683
+ logging.info(color(f'*** Requesting MTU update: {self.att_mtu}', 'blue'))
556
684
  await peer.request_mtu(self.att_mtu)
557
685
 
558
- print(color('*** Discovering services...', 'blue'))
686
+ logging.info(color('*** Discovering services...', 'blue'))
559
687
  await peer.discover_services()
560
688
 
561
689
  speed_services = peer.get_services_by_uuid(SPEED_SERVICE_UUID)
562
690
  if not speed_services:
563
- print(color('!!! Speed Service not found', 'red'))
691
+ logging.info(color('!!! Speed Service not found', 'red'))
564
692
  return
565
693
  speed_service = speed_services[0]
566
- print(color('*** Discovering characteristics...', 'blue'))
694
+ logging.info(color('*** Discovering characteristics...', 'blue'))
567
695
  await speed_service.discover_characteristics()
568
696
 
569
697
  speed_txs = speed_service.get_characteristics_by_uuid(SPEED_TX_UUID)
570
698
  if not speed_txs:
571
- print(color('!!! Speed TX not found', 'red'))
699
+ logging.info(color('!!! Speed TX not found', 'red'))
572
700
  return
573
701
  self.speed_tx = speed_txs[0]
574
702
 
575
703
  speed_rxs = speed_service.get_characteristics_by_uuid(SPEED_RX_UUID)
576
704
  if not speed_rxs:
577
- print(color('!!! Speed RX not found', 'red'))
705
+ logging.info(color('!!! Speed RX not found', 'red'))
578
706
  return
579
707
  self.speed_rx = speed_rxs[0]
580
708
 
581
- print(color('*** Subscribing to RX', 'blue'))
709
+ logging.info(color('*** Subscribing to RX', 'blue'))
582
710
  await self.speed_rx.subscribe(self.on_packet_received)
583
711
 
584
- print(color('*** Discovery complete', 'blue'))
712
+ logging.info(color('*** Discovery complete', 'blue'))
585
713
 
586
714
  connection.on('disconnection', self.on_disconnection)
587
715
  self.ready.set()
@@ -596,6 +724,9 @@ class GattClient:
596
724
  async def send_packet(self, packet):
597
725
  await self.speed_tx.write_value(packet)
598
726
 
727
+ async def drain(self):
728
+ pass
729
+
599
730
 
600
731
  # -----------------------------------------------------------------------------
601
732
  # GattServer
@@ -605,6 +736,7 @@ class GattServer:
605
736
  self.device = device
606
737
  self.packet_listener = None
607
738
  self.ready = asyncio.Event()
739
+ self.overhead_size = 0
608
740
 
609
741
  # Setup the GATT service
610
742
  self.speed_tx = Characteristic(
@@ -633,10 +765,10 @@ class GattServer:
633
765
 
634
766
  def on_rx_subscription(self, _connection, notify_enabled, _indicate_enabled):
635
767
  if notify_enabled:
636
- print(color('*** RX subscription', 'blue'))
768
+ logging.info(color('*** RX subscription', 'blue'))
637
769
  self.ready.set()
638
770
  else:
639
- print(color('*** RX un-subscription', 'blue'))
771
+ logging.info(color('*** RX un-subscription', 'blue'))
640
772
  self.ready.clear()
641
773
 
642
774
  def on_tx_write(self, _, value):
@@ -646,6 +778,9 @@ class GattServer:
646
778
  async def send_packet(self, packet):
647
779
  await self.device.notify_subscribers(self.speed_rx, packet)
648
780
 
781
+ async def drain(self):
782
+ pass
783
+
649
784
 
650
785
  # -----------------------------------------------------------------------------
651
786
  # StreamedPacketIO
@@ -657,6 +792,7 @@ class StreamedPacketIO:
657
792
  self.rx_packet = b''
658
793
  self.rx_packet_header = b''
659
794
  self.rx_packet_need = 0
795
+ self.overhead_size = 2
660
796
 
661
797
  def on_packet(self, packet):
662
798
  while packet:
@@ -684,7 +820,7 @@ class StreamedPacketIO:
684
820
 
685
821
  async def send_packet(self, packet):
686
822
  if not self.io_sink:
687
- print(color('!!! No sink, dropping packet', 'red'))
823
+ logging.info(color('!!! No sink, dropping packet', 'red'))
688
824
  return
689
825
 
690
826
  # pylint: disable-next=not-callable
@@ -708,13 +844,14 @@ class L2capClient(StreamedPacketIO):
708
844
  self.max_credits = max_credits
709
845
  self.mtu = mtu
710
846
  self.mps = mps
847
+ self.l2cap_channel = None
711
848
  self.ready = asyncio.Event()
712
849
 
713
850
  async def on_connection(self, connection: Connection) -> None:
714
851
  connection.on('disconnection', self.on_disconnection)
715
852
 
716
853
  # Connect a new L2CAP channel
717
- print(color(f'>>> Opening L2CAP channel on PSM = {self.psm}', 'yellow'))
854
+ logging.info(color(f'>>> Opening L2CAP channel on PSM = {self.psm}', 'yellow'))
718
855
  try:
719
856
  l2cap_channel = await connection.create_l2cap_channel(
720
857
  spec=l2cap.LeCreditBasedChannelSpec(
@@ -724,14 +861,15 @@ class L2capClient(StreamedPacketIO):
724
861
  mps=self.mps,
725
862
  )
726
863
  )
727
- print(color('*** L2CAP channel:', 'cyan'), l2cap_channel)
864
+ logging.info(color(f'*** L2CAP channel: {l2cap_channel}', 'cyan'))
728
865
  except Exception as error:
729
- print(color(f'!!! Connection failed: {error}', 'red'))
866
+ logging.info(color(f'!!! Connection failed: {error}', 'red'))
730
867
  return
731
868
 
732
- l2cap_channel.sink = self.on_packet
733
- l2cap_channel.on('close', self.on_l2cap_close)
734
869
  self.io_sink = l2cap_channel.write
870
+ self.l2cap_channel = l2cap_channel
871
+ l2cap_channel.on('close', self.on_l2cap_close)
872
+ l2cap_channel.sink = self.on_packet
735
873
 
736
874
  self.ready.set()
737
875
 
@@ -739,7 +877,11 @@ class L2capClient(StreamedPacketIO):
739
877
  pass
740
878
 
741
879
  def on_l2cap_close(self):
742
- print(color('*** L2CAP channel closed', 'red'))
880
+ logging.info(color('*** L2CAP channel closed', 'red'))
881
+
882
+ async def drain(self):
883
+ assert self.l2cap_channel
884
+ await self.l2cap_channel.drain()
743
885
 
744
886
 
745
887
  # -----------------------------------------------------------------------------
@@ -765,7 +907,9 @@ class L2capServer(StreamedPacketIO):
765
907
  ),
766
908
  handler=self.on_l2cap_channel,
767
909
  )
768
- print(color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow'))
910
+ logging.info(
911
+ color(f'### Listening for L2CAP connection on PSM {psm}', 'yellow')
912
+ )
769
913
 
770
914
  async def on_connection(self, connection):
771
915
  connection.on('disconnection', self.on_disconnection)
@@ -774,18 +918,23 @@ class L2capServer(StreamedPacketIO):
774
918
  pass
775
919
 
776
920
  def on_l2cap_channel(self, l2cap_channel):
777
- print(color('*** L2CAP channel:', 'cyan'), l2cap_channel)
921
+ logging.info(color(f'*** L2CAP channel: {l2cap_channel}', 'cyan'))
778
922
 
779
923
  self.io_sink = l2cap_channel.write
924
+ self.l2cap_channel = l2cap_channel
780
925
  l2cap_channel.on('close', self.on_l2cap_close)
781
926
  l2cap_channel.sink = self.on_packet
782
927
 
783
928
  self.ready.set()
784
929
 
785
930
  def on_l2cap_close(self):
786
- print(color('*** L2CAP channel closed', 'red'))
931
+ logging.info(color('*** L2CAP channel closed', 'red'))
787
932
  self.l2cap_channel = None
788
933
 
934
+ async def drain(self):
935
+ assert self.l2cap_channel
936
+ await self.l2cap_channel.drain()
937
+
789
938
 
790
939
  # -----------------------------------------------------------------------------
791
940
  # RfcommClient
@@ -796,6 +945,7 @@ class RfcommClient(StreamedPacketIO):
796
945
  self.device = device
797
946
  self.channel = channel
798
947
  self.uuid = uuid
948
+ self.rfcomm_session = None
799
949
  self.ready = asyncio.Event()
800
950
 
801
951
  async def on_connection(self, connection):
@@ -804,39 +954,44 @@ class RfcommClient(StreamedPacketIO):
804
954
  # Find the channel number if not specified
805
955
  channel = self.channel
806
956
  if channel == 0:
807
- print(
957
+ logging.info(
808
958
  color(f'@@@ Discovering channel number from UUID {self.uuid}', 'cyan')
809
959
  )
810
960
  channel = await find_rfcomm_channel_with_uuid(connection, self.uuid)
811
- print(color(f'@@@ Channel number = {channel}', 'cyan'))
961
+ logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
812
962
  if channel == 0:
813
- print(color('!!! No RFComm service with this UUID found', 'red'))
963
+ logging.info(color('!!! No RFComm service with this UUID found', 'red'))
814
964
  await connection.disconnect()
815
965
  return
816
966
 
817
967
  # Create a client and start it
818
- print(color('*** Starting RFCOMM client...', 'blue'))
968
+ logging.info(color('*** Starting RFCOMM client...', 'blue'))
819
969
  rfcomm_client = bumble.rfcomm.Client(connection)
820
970
  rfcomm_mux = await rfcomm_client.start()
821
- print(color('*** Started', 'blue'))
971
+ logging.info(color('*** Started', 'blue'))
822
972
 
823
- print(color(f'### Opening session for channel {channel}...', 'yellow'))
973
+ logging.info(color(f'### Opening session for channel {channel}...', 'yellow'))
824
974
  try:
825
975
  rfcomm_session = await rfcomm_mux.open_dlc(channel)
826
- print(color('### Session open', 'yellow'), rfcomm_session)
976
+ logging.info(color(f'### Session open: {rfcomm_session}', 'yellow'))
827
977
  except bumble.core.ConnectionError as error:
828
- print(color(f'!!! Session open failed: {error}', 'red'))
978
+ logging.info(color(f'!!! Session open failed: {error}', 'red'))
829
979
  await rfcomm_mux.disconnect()
830
980
  return
831
981
 
832
982
  rfcomm_session.sink = self.on_packet
833
983
  self.io_sink = rfcomm_session.write
984
+ self.rfcomm_session = rfcomm_session
834
985
 
835
986
  self.ready.set()
836
987
 
837
988
  def on_disconnection(self, _):
838
989
  pass
839
990
 
991
+ async def drain(self):
992
+ assert self.rfcomm_session
993
+ await self.rfcomm_session.drain()
994
+
840
995
 
841
996
  # -----------------------------------------------------------------------------
842
997
  # RfcommServer
@@ -844,6 +999,7 @@ class RfcommClient(StreamedPacketIO):
844
999
  class RfcommServer(StreamedPacketIO):
845
1000
  def __init__(self, device, channel):
846
1001
  super().__init__()
1002
+ self.dlc = None
847
1003
  self.ready = asyncio.Event()
848
1004
 
849
1005
  # Create and register a server
@@ -855,7 +1011,7 @@ class RfcommServer(StreamedPacketIO):
855
1011
  # Setup the SDP to advertise this channel
856
1012
  device.sdp_service_records = make_sdp_records(channel_number)
857
1013
 
858
- print(
1014
+ logging.info(
859
1015
  color(
860
1016
  f'### Listening for RFComm connection on channel {channel_number}',
861
1017
  'yellow',
@@ -869,9 +1025,14 @@ class RfcommServer(StreamedPacketIO):
869
1025
  pass
870
1026
 
871
1027
  def on_dlc(self, dlc):
872
- print(color('*** DLC connected:', 'blue'), dlc)
1028
+ logging.info(color(f'*** DLC connected: {dlc}', 'blue'))
873
1029
  dlc.sink = self.on_packet
874
1030
  self.io_sink = dlc.write
1031
+ self.dlc = dlc
1032
+
1033
+ async def drain(self):
1034
+ assert self.dlc
1035
+ await self.dlc.drain()
875
1036
 
876
1037
 
877
1038
  # -----------------------------------------------------------------------------
@@ -935,12 +1096,12 @@ class Central(Connection.Listener):
935
1096
  self.connection_parameter_preferences = None
936
1097
 
937
1098
  async def run(self):
938
- print(color('>>> Connecting to HCI...', 'green'))
1099
+ logging.info(color('>>> Connecting to HCI...', 'green'))
939
1100
  async with await open_transport_or_link(self.transport) as (
940
1101
  hci_source,
941
1102
  hci_sink,
942
1103
  ):
943
- print(color('>>> Connected', 'green'))
1104
+ logging.info(color('>>> Connected', 'green'))
944
1105
 
945
1106
  central_address = DEFAULT_CENTRAL_ADDRESS
946
1107
  self.device = Device.with_hci(
@@ -952,7 +1113,13 @@ class Central(Connection.Listener):
952
1113
 
953
1114
  await self.device.power_on()
954
1115
 
955
- print(color(f'### Connecting to {self.peripheral_address}...', 'cyan'))
1116
+ if self.classic:
1117
+ await self.device.set_discoverable(False)
1118
+ await self.device.set_connectable(False)
1119
+
1120
+ logging.info(
1121
+ color(f'### Connecting to {self.peripheral_address}...', 'cyan')
1122
+ )
956
1123
  try:
957
1124
  self.connection = await self.device.connect(
958
1125
  self.peripheral_address,
@@ -960,21 +1127,26 @@ class Central(Connection.Listener):
960
1127
  transport=BT_BR_EDR_TRANSPORT if self.classic else BT_LE_TRANSPORT,
961
1128
  )
962
1129
  except CommandTimeoutError:
963
- print(color('!!! Connection timed out', 'red'))
1130
+ logging.info(color('!!! Connection timed out', 'red'))
964
1131
  return
965
1132
  except bumble.core.ConnectionError as error:
966
- print(color(f'!!! Connection error: {error}', 'red'))
1133
+ logging.info(color(f'!!! Connection error: {error}', 'red'))
967
1134
  return
968
1135
  except HCI_StatusError as error:
969
- print(color(f'!!! Connection failed: {error.error_name}'))
1136
+ logging.info(color(f'!!! Connection failed: {error.error_name}'))
970
1137
  return
971
- print(color('### Connected', 'cyan'))
1138
+ logging.info(color('### Connected', 'cyan'))
972
1139
  self.connection.listener = self
973
1140
  print_connection(self.connection)
974
1141
 
1142
+ # Wait a bit after the connection, some controllers aren't very good when
1143
+ # we start sending data right away while some connection parameters are
1144
+ # updated post connection
1145
+ await asyncio.sleep(DEFAULT_POST_CONNECTION_WAIT_TIME)
1146
+
975
1147
  # Request a new data length if requested
976
1148
  if self.extended_data_length:
977
- print(color('+++ Requesting extended data length', 'cyan'))
1149
+ logging.info(color('+++ Requesting extended data length', 'cyan'))
978
1150
  await self.connection.set_data_length(
979
1151
  self.extended_data_length[0], self.extended_data_length[1]
980
1152
  )
@@ -982,16 +1154,16 @@ class Central(Connection.Listener):
982
1154
  # Authenticate if requested
983
1155
  if self.authenticate:
984
1156
  # Request authentication
985
- print(color('*** Authenticating...', 'cyan'))
1157
+ logging.info(color('*** Authenticating...', 'cyan'))
986
1158
  await self.connection.authenticate()
987
- print(color('*** Authenticated', 'cyan'))
1159
+ logging.info(color('*** Authenticated', 'cyan'))
988
1160
 
989
1161
  # Encrypt if requested
990
1162
  if self.encrypt:
991
1163
  # Enable encryption
992
- print(color('*** Enabling encryption...', 'cyan'))
1164
+ logging.info(color('*** Enabling encryption...', 'cyan'))
993
1165
  await self.connection.encrypt()
994
- print(color('*** Encryption on', 'cyan'))
1166
+ logging.info(color('*** Encryption on', 'cyan'))
995
1167
 
996
1168
  # Set the PHY if requested
997
1169
  if self.phy is not None:
@@ -1000,7 +1172,7 @@ class Central(Connection.Listener):
1000
1172
  tx_phys=[self.phy], rx_phys=[self.phy]
1001
1173
  )
1002
1174
  except HCI_Error as error:
1003
- print(
1175
+ logging.info(
1004
1176
  color(
1005
1177
  f'!!! Unable to set the PHY: {error.error_name}', 'yellow'
1006
1178
  )
@@ -1010,9 +1182,10 @@ class Central(Connection.Listener):
1010
1182
 
1011
1183
  await role.run()
1012
1184
  await asyncio.sleep(DEFAULT_LINGER_TIME)
1185
+ await self.connection.disconnect()
1013
1186
 
1014
1187
  def on_disconnection(self, reason):
1015
- print(color(f'!!! Disconnection: reason={reason}', 'red'))
1188
+ logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
1016
1189
  self.connection = None
1017
1190
 
1018
1191
  def on_connection_parameters_update(self):
@@ -1047,12 +1220,12 @@ class Peripheral(Device.Listener, Connection.Listener):
1047
1220
  self.connected = asyncio.Event()
1048
1221
 
1049
1222
  async def run(self):
1050
- print(color('>>> Connecting to HCI...', 'green'))
1223
+ logging.info(color('>>> Connecting to HCI...', 'green'))
1051
1224
  async with await open_transport_or_link(self.transport) as (
1052
1225
  hci_source,
1053
1226
  hci_sink,
1054
1227
  ):
1055
- print(color('>>> Connected', 'green'))
1228
+ logging.info(color('>>> Connected', 'green'))
1056
1229
 
1057
1230
  peripheral_address = DEFAULT_PERIPHERAL_ADDRESS
1058
1231
  self.device = Device.with_hci(
@@ -1072,7 +1245,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1072
1245
  await self.device.start_advertising(auto_restart=True)
1073
1246
 
1074
1247
  if self.classic:
1075
- print(
1248
+ logging.info(
1076
1249
  color(
1077
1250
  '### Waiting for connection on'
1078
1251
  f' {self.device.public_address}...',
@@ -1080,14 +1253,14 @@ class Peripheral(Device.Listener, Connection.Listener):
1080
1253
  )
1081
1254
  )
1082
1255
  else:
1083
- print(
1256
+ logging.info(
1084
1257
  color(
1085
1258
  f'### Waiting for connection on {peripheral_address}...',
1086
1259
  'cyan',
1087
1260
  )
1088
1261
  )
1089
1262
  await self.connected.wait()
1090
- print(color('### Connected', 'cyan'))
1263
+ logging.info(color('### Connected', 'cyan'))
1091
1264
 
1092
1265
  await self.mode.on_connection(self.connection)
1093
1266
  await self.role.run()
@@ -1098,9 +1271,14 @@ class Peripheral(Device.Listener, Connection.Listener):
1098
1271
  self.connection = connection
1099
1272
  self.connected.set()
1100
1273
 
1274
+ # Stop being discoverable and connectable
1275
+ if self.classic:
1276
+ AsyncRunner.spawn(self.device.set_discoverable(False))
1277
+ AsyncRunner.spawn(self.device.set_connectable(False))
1278
+
1101
1279
  # Request a new data length if needed
1102
1280
  if self.extended_data_length:
1103
- print("+++ Requesting extended data length")
1281
+ logging.info("+++ Requesting extended data length")
1104
1282
  AsyncRunner.spawn(
1105
1283
  connection.set_data_length(
1106
1284
  self.extended_data_length[0], self.extended_data_length[1]
@@ -1108,10 +1286,14 @@ class Peripheral(Device.Listener, Connection.Listener):
1108
1286
  )
1109
1287
 
1110
1288
  def on_disconnection(self, reason):
1111
- print(color(f'!!! Disconnection: reason={reason}', 'red'))
1289
+ logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
1112
1290
  self.connection = None
1113
1291
  self.role.reset()
1114
1292
 
1293
+ if self.classic:
1294
+ AsyncRunner.spawn(self.device.set_discoverable(True))
1295
+ AsyncRunner.spawn(self.device.set_connectable(True))
1296
+
1115
1297
  def on_connection_parameters_update(self):
1116
1298
  print_connection(self.connection)
1117
1299
 
@@ -1139,10 +1321,22 @@ def create_mode_factory(ctx, default_mode):
1139
1321
  return GattServer(device)
1140
1322
 
1141
1323
  if mode == 'l2cap-client':
1142
- return L2capClient(device, psm=ctx.obj['l2cap_psm'])
1324
+ return L2capClient(
1325
+ device,
1326
+ psm=ctx.obj['l2cap_psm'],
1327
+ mtu=ctx.obj['l2cap_mtu'],
1328
+ mps=ctx.obj['l2cap_mps'],
1329
+ max_credits=ctx.obj['l2cap_max_credits'],
1330
+ )
1143
1331
 
1144
1332
  if mode == 'l2cap-server':
1145
- return L2capServer(device, psm=ctx.obj['l2cap_psm'])
1333
+ return L2capServer(
1334
+ device,
1335
+ psm=ctx.obj['l2cap_psm'],
1336
+ mtu=ctx.obj['l2cap_mtu'],
1337
+ mps=ctx.obj['l2cap_mps'],
1338
+ max_credits=ctx.obj['l2cap_max_credits'],
1339
+ )
1146
1340
 
1147
1341
  if mode == 'rfcomm-client':
1148
1342
  return RfcommClient(
@@ -1168,23 +1362,29 @@ def create_role_factory(ctx, default_role):
1168
1362
  return Sender(
1169
1363
  packet_io,
1170
1364
  start_delay=ctx.obj['start_delay'],
1365
+ repeat=ctx.obj['repeat'],
1366
+ repeat_delay=ctx.obj['repeat_delay'],
1367
+ pace=ctx.obj['pace'],
1171
1368
  packet_size=ctx.obj['packet_size'],
1172
1369
  packet_count=ctx.obj['packet_count'],
1173
1370
  )
1174
1371
 
1175
1372
  if role == 'receiver':
1176
- return Receiver(packet_io)
1373
+ return Receiver(packet_io, ctx.obj['linger'])
1177
1374
 
1178
1375
  if role == 'ping':
1179
1376
  return Ping(
1180
1377
  packet_io,
1181
1378
  start_delay=ctx.obj['start_delay'],
1379
+ repeat=ctx.obj['repeat'],
1380
+ repeat_delay=ctx.obj['repeat_delay'],
1381
+ pace=ctx.obj['pace'],
1182
1382
  packet_size=ctx.obj['packet_size'],
1183
1383
  packet_count=ctx.obj['packet_count'],
1184
1384
  )
1185
1385
 
1186
1386
  if role == 'pong':
1187
- return Pong(packet_io)
1387
+ return Pong(packet_io, ctx.obj['linger'])
1188
1388
 
1189
1389
  raise ValueError('invalid role')
1190
1390
 
@@ -1229,7 +1429,7 @@ def create_role_factory(ctx, default_role):
1229
1429
  @click.option(
1230
1430
  '--rfcomm-uuid',
1231
1431
  default=DEFAULT_RFCOMM_UUID,
1232
- help='RFComm service UUID to use (ignored is --rfcomm-channel is not 0)',
1432
+ help='RFComm service UUID to use (ignored if --rfcomm-channel is not 0)',
1233
1433
  )
1234
1434
  @click.option(
1235
1435
  '--l2cap-psm',
@@ -1237,13 +1437,31 @@ def create_role_factory(ctx, default_role):
1237
1437
  default=DEFAULT_L2CAP_PSM,
1238
1438
  help='L2CAP PSM to use',
1239
1439
  )
1440
+ @click.option(
1441
+ '--l2cap-mtu',
1442
+ type=int,
1443
+ default=DEFAULT_L2CAP_MTU,
1444
+ help='L2CAP MTU to use',
1445
+ )
1446
+ @click.option(
1447
+ '--l2cap-mps',
1448
+ type=int,
1449
+ default=DEFAULT_L2CAP_MPS,
1450
+ help='L2CAP MPS to use',
1451
+ )
1452
+ @click.option(
1453
+ '--l2cap-max-credits',
1454
+ type=int,
1455
+ default=DEFAULT_L2CAP_MAX_CREDITS,
1456
+ help='L2CAP maximum number of credits allowed for the peer',
1457
+ )
1240
1458
  @click.option(
1241
1459
  '--packet-size',
1242
1460
  '-s',
1243
1461
  metavar='SIZE',
1244
1462
  type=click.IntRange(8, 4096),
1245
1463
  default=500,
1246
- help='Packet size (server role)',
1464
+ help='Packet size (client or ping role)',
1247
1465
  )
1248
1466
  @click.option(
1249
1467
  '--packet-count',
@@ -1251,7 +1469,7 @@ def create_role_factory(ctx, default_role):
1251
1469
  metavar='COUNT',
1252
1470
  type=int,
1253
1471
  default=10,
1254
- help='Packet count (server role)',
1472
+ help='Packet count (client or ping role)',
1255
1473
  )
1256
1474
  @click.option(
1257
1475
  '--start-delay',
@@ -1259,7 +1477,39 @@ def create_role_factory(ctx, default_role):
1259
1477
  metavar='SECONDS',
1260
1478
  type=int,
1261
1479
  default=1,
1262
- help='Start delay (server role)',
1480
+ help='Start delay (client or ping role)',
1481
+ )
1482
+ @click.option(
1483
+ '--repeat',
1484
+ metavar='N',
1485
+ type=int,
1486
+ default=0,
1487
+ help=(
1488
+ 'Repeat the run N times (client and ping roles)'
1489
+ '(0, which is the fault, to run just once) '
1490
+ ),
1491
+ )
1492
+ @click.option(
1493
+ '--repeat-delay',
1494
+ metavar='SECONDS',
1495
+ type=int,
1496
+ default=1,
1497
+ help=('Delay, in seconds, between repeats'),
1498
+ )
1499
+ @click.option(
1500
+ '--pace',
1501
+ metavar='MILLISECONDS',
1502
+ type=int,
1503
+ default=0,
1504
+ help=(
1505
+ 'Wait N milliseconds between packets '
1506
+ '(0, which is the fault, to send as fast as possible) '
1507
+ ),
1508
+ )
1509
+ @click.option(
1510
+ '--linger',
1511
+ is_flag=True,
1512
+ help="Don't exit at the end of a run (server and pong roles)",
1263
1513
  )
1264
1514
  @click.pass_context
1265
1515
  def bench(
@@ -1272,9 +1522,16 @@ def bench(
1272
1522
  packet_size,
1273
1523
  packet_count,
1274
1524
  start_delay,
1525
+ repeat,
1526
+ repeat_delay,
1527
+ pace,
1528
+ linger,
1275
1529
  rfcomm_channel,
1276
1530
  rfcomm_uuid,
1277
1531
  l2cap_psm,
1532
+ l2cap_mtu,
1533
+ l2cap_mps,
1534
+ l2cap_max_credits,
1278
1535
  ):
1279
1536
  ctx.ensure_object(dict)
1280
1537
  ctx.obj['device_config'] = device_config
@@ -1284,9 +1541,16 @@ def bench(
1284
1541
  ctx.obj['rfcomm_channel'] = rfcomm_channel
1285
1542
  ctx.obj['rfcomm_uuid'] = rfcomm_uuid
1286
1543
  ctx.obj['l2cap_psm'] = l2cap_psm
1544
+ ctx.obj['l2cap_mtu'] = l2cap_mtu
1545
+ ctx.obj['l2cap_mps'] = l2cap_mps
1546
+ ctx.obj['l2cap_max_credits'] = l2cap_max_credits
1287
1547
  ctx.obj['packet_size'] = packet_size
1288
1548
  ctx.obj['packet_count'] = packet_count
1289
1549
  ctx.obj['start_delay'] = start_delay
1550
+ ctx.obj['repeat'] = repeat
1551
+ ctx.obj['repeat_delay'] = repeat_delay
1552
+ ctx.obj['pace'] = pace
1553
+ ctx.obj['linger'] = linger
1290
1554
 
1291
1555
  ctx.obj['extended_data_length'] = (
1292
1556
  [int(x) for x in extended_data_length.split('/')]