bumble 0.0.201__py3-none-any.whl → 0.0.202__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bumble/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.0.201'
16
- __version_tuple__ = version_tuple = (0, 0, 201)
15
+ __version__ = version = '0.0.202'
16
+ __version_tuple__ = version_tuple = (0, 0, 202)
bumble/apps/bench.py CHANGED
@@ -19,6 +19,7 @@ import asyncio
19
19
  import enum
20
20
  import logging
21
21
  import os
22
+ import statistics
22
23
  import struct
23
24
  import time
24
25
 
@@ -194,17 +195,19 @@ def make_sdp_records(channel):
194
195
  }
195
196
 
196
197
 
197
- def log_stats(title, stats):
198
+ def log_stats(title, stats, precision=2):
198
199
  stats_min = min(stats)
199
200
  stats_max = max(stats)
200
- stats_avg = sum(stats) / len(stats)
201
+ stats_avg = statistics.mean(stats)
202
+ stats_stdev = statistics.stdev(stats)
201
203
  logging.info(
202
204
  color(
203
205
  (
204
206
  f'### {title} stats: '
205
- f'min={stats_min:.2f}, '
206
- f'max={stats_max:.2f}, '
207
- f'average={stats_avg:.2f}'
207
+ f'min={stats_min:.{precision}f}, '
208
+ f'max={stats_max:.{precision}f}, '
209
+ f'average={stats_avg:.{precision}f}, '
210
+ f'stdev={stats_stdev:.{precision}f}'
208
211
  ),
209
212
  'cyan',
210
213
  )
@@ -448,9 +451,9 @@ class Ping:
448
451
  self.repeat_delay = repeat_delay
449
452
  self.pace = pace
450
453
  self.done = asyncio.Event()
451
- self.current_packet_index = 0
452
- self.ping_sent_time = 0.0
453
- self.latencies = []
454
+ self.ping_times = []
455
+ self.rtts = []
456
+ self.next_expected_packet_index = 0
454
457
  self.min_stats = []
455
458
  self.max_stats = []
456
459
  self.avg_stats = []
@@ -477,60 +480,57 @@ class Ping:
477
480
  logging.info(color('=== Sending RESET', 'magenta'))
478
481
  await self.packet_io.send_packet(bytes([PacketType.RESET]))
479
482
 
480
- self.current_packet_index = 0
481
- self.latencies = []
482
- await self.send_next_ping()
483
+ packet_interval = self.pace / 1000
484
+ start_time = time.time()
485
+ self.next_expected_packet_index = 0
486
+ for i in range(self.tx_packet_count):
487
+ target_time = start_time + (i * packet_interval)
488
+ now = time.time()
489
+ if now < target_time:
490
+ await asyncio.sleep(target_time - now)
491
+
492
+ packet = struct.pack(
493
+ '>bbI',
494
+ PacketType.SEQUENCE,
495
+ (PACKET_FLAG_LAST if i == self.tx_packet_count - 1 else 0),
496
+ i,
497
+ ) + bytes(self.tx_packet_size - 6)
498
+ logging.info(color(f'Sending packet {i}', 'yellow'))
499
+ self.ping_times.append(time.time())
500
+ await self.packet_io.send_packet(packet)
483
501
 
484
502
  await self.done.wait()
485
503
 
486
- min_latency = min(self.latencies)
487
- max_latency = max(self.latencies)
488
- avg_latency = sum(self.latencies) / len(self.latencies)
504
+ min_rtt = min(self.rtts)
505
+ max_rtt = max(self.rtts)
506
+ avg_rtt = statistics.mean(self.rtts)
507
+ stdev_rtt = statistics.stdev(self.rtts)
489
508
  logging.info(
490
509
  color(
491
- '@@@ Latencies: '
492
- f'min={min_latency:.2f}, '
493
- f'max={max_latency:.2f}, '
494
- f'average={avg_latency:.2f}'
510
+ '@@@ RTTs: '
511
+ f'min={min_rtt:.2f}, '
512
+ f'max={max_rtt:.2f}, '
513
+ f'average={avg_rtt:.2f}, '
514
+ f'stdev={stdev_rtt:.2f}'
495
515
  )
496
516
  )
497
517
 
498
- self.min_stats.append(min_latency)
499
- self.max_stats.append(max_latency)
500
- self.avg_stats.append(avg_latency)
518
+ self.min_stats.append(min_rtt)
519
+ self.max_stats.append(max_rtt)
520
+ self.avg_stats.append(avg_rtt)
501
521
 
502
522
  run_counter = f'[{run + 1} of {self.repeat + 1}]' if self.repeat else ''
503
523
  logging.info(color(f'=== {run_counter} Done!', 'magenta'))
504
524
 
505
525
  if self.repeat:
506
- log_stats('Min Latency', self.min_stats)
507
- log_stats('Max Latency', self.max_stats)
508
- log_stats('Average Latency', self.avg_stats)
526
+ log_stats('Min RTT', self.min_stats)
527
+ log_stats('Max RTT', self.max_stats)
528
+ log_stats('Average RTT', self.avg_stats)
509
529
 
510
530
  if self.repeat:
511
531
  logging.info(color('--- End of runs', 'blue'))
512
532
 
513
- async def send_next_ping(self):
514
- if self.pace:
515
- await asyncio.sleep(self.pace / 1000)
516
-
517
- packet = struct.pack(
518
- '>bbI',
519
- PacketType.SEQUENCE,
520
- (
521
- PACKET_FLAG_LAST
522
- if self.current_packet_index == self.tx_packet_count - 1
523
- else 0
524
- ),
525
- self.current_packet_index,
526
- ) + bytes(self.tx_packet_size - 6)
527
- logging.info(color(f'Sending packet {self.current_packet_index}', 'yellow'))
528
- self.ping_sent_time = time.time()
529
- await self.packet_io.send_packet(packet)
530
-
531
533
  def on_packet_received(self, packet):
532
- elapsed = time.time() - self.ping_sent_time
533
-
534
534
  try:
535
535
  packet_type, packet_data = parse_packet(packet)
536
536
  except ValueError:
@@ -542,21 +542,23 @@ class Ping:
542
542
  return
543
543
 
544
544
  if packet_type == PacketType.ACK:
545
- latency = elapsed * 1000
546
- self.latencies.append(latency)
545
+ elapsed = time.time() - self.ping_times[packet_index]
546
+ rtt = elapsed * 1000
547
+ self.rtts.append(rtt)
547
548
  logging.info(
548
549
  color(
549
- f'<<< Received ACK [{packet_index}], latency={latency:.2f}ms',
550
+ f'<<< Received ACK [{packet_index}], RTT={rtt:.2f}ms',
550
551
  'green',
551
552
  )
552
553
  )
553
554
 
554
- if packet_index == self.current_packet_index:
555
- self.current_packet_index += 1
555
+ if packet_index == self.next_expected_packet_index:
556
+ self.next_expected_packet_index += 1
556
557
  else:
557
558
  logging.info(
558
559
  color(
559
- f'!!! Unexpected packet, expected {self.current_packet_index} '
560
+ f'!!! Unexpected packet, '
561
+ f'expected {self.next_expected_packet_index} '
560
562
  f'but received {packet_index}'
561
563
  )
562
564
  )
@@ -565,8 +567,6 @@ class Ping:
565
567
  self.done.set()
566
568
  return
567
569
 
568
- AsyncRunner.spawn(self.send_next_ping())
569
-
570
570
 
571
571
  # -----------------------------------------------------------------------------
572
572
  # Pong
@@ -583,8 +583,11 @@ class Pong:
583
583
 
584
584
  def reset(self):
585
585
  self.expected_packet_index = 0
586
+ self.receive_times = []
586
587
 
587
588
  def on_packet_received(self, packet):
589
+ self.receive_times.append(time.time())
590
+
588
591
  try:
589
592
  packet_type, packet_data = parse_packet(packet)
590
593
  except ValueError:
@@ -599,10 +602,16 @@ class Pong:
599
602
  packet_flags, packet_index = parse_packet_sequence(packet_data)
600
603
  except ValueError:
601
604
  return
605
+ interval = (
606
+ self.receive_times[-1] - self.receive_times[-2]
607
+ if len(self.receive_times) >= 2
608
+ else 0
609
+ )
602
610
  logging.info(
603
611
  color(
604
612
  f'<<< Received packet {packet_index}: '
605
- f'flags=0x{packet_flags:02X}, {len(packet)} bytes',
613
+ f'flags=0x{packet_flags:02X}, {len(packet)} bytes, '
614
+ f'interval={interval:.4f}',
606
615
  'green',
607
616
  )
608
617
  )
@@ -623,8 +632,35 @@ class Pong:
623
632
  )
624
633
  )
625
634
 
626
- if packet_flags & PACKET_FLAG_LAST and not self.linger:
627
- self.done.set()
635
+ if packet_flags & PACKET_FLAG_LAST:
636
+ if len(self.receive_times) >= 3:
637
+ # Show basic stats
638
+ intervals = [
639
+ self.receive_times[i + 1] - self.receive_times[i]
640
+ for i in range(len(self.receive_times) - 1)
641
+ ]
642
+ log_stats('Packet intervals', intervals, 3)
643
+
644
+ # Show a histogram
645
+ bin_count = 20
646
+ bins = [0] * bin_count
647
+ interval_min = min(intervals)
648
+ interval_max = max(intervals)
649
+ interval_range = interval_max - interval_min
650
+ bin_thresholds = [
651
+ interval_min + i * (interval_range / bin_count)
652
+ for i in range(bin_count)
653
+ ]
654
+ for interval in intervals:
655
+ for i in reversed(range(bin_count)):
656
+ if interval >= bin_thresholds[i]:
657
+ bins[i] += 1
658
+ break
659
+ for i in range(bin_count):
660
+ logging.info(f'@@@ >= {bin_thresholds[i]:.4f}: {bins[i]}')
661
+
662
+ if not self.linger:
663
+ self.done.set()
628
664
 
629
665
  async def run(self):
630
666
  await self.done.wait()
@@ -942,9 +978,12 @@ class RfcommClient(StreamedPacketIO):
942
978
  channel = await bumble.rfcomm.find_rfcomm_channel_with_uuid(
943
979
  connection, self.uuid
944
980
  )
945
- logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
946
- if channel == 0:
947
- logging.info(color('!!! No RFComm service with this UUID found', 'red'))
981
+ if channel:
982
+ logging.info(color(f'@@@ Channel number = {channel}', 'cyan'))
983
+ else:
984
+ logging.warning(
985
+ color('!!! No RFComm service with this UUID found', 'red')
986
+ )
948
987
  await connection.disconnect()
949
988
  return
950
989
 
@@ -1054,6 +1093,8 @@ class RfcommServer(StreamedPacketIO):
1054
1093
  if self.credits_threshold is not None:
1055
1094
  dlc.rx_credits_threshold = self.credits_threshold
1056
1095
 
1096
+ self.ready.set()
1097
+
1057
1098
  async def drain(self):
1058
1099
  assert self.dlc
1059
1100
  await self.dlc.drain()
@@ -1068,7 +1109,7 @@ class Central(Connection.Listener):
1068
1109
  transport,
1069
1110
  peripheral_address,
1070
1111
  classic,
1071
- role_factory,
1112
+ scenario_factory,
1072
1113
  mode_factory,
1073
1114
  connection_interval,
1074
1115
  phy,
@@ -1081,7 +1122,7 @@ class Central(Connection.Listener):
1081
1122
  self.transport = transport
1082
1123
  self.peripheral_address = peripheral_address
1083
1124
  self.classic = classic
1084
- self.role_factory = role_factory
1125
+ self.scenario_factory = scenario_factory
1085
1126
  self.mode_factory = mode_factory
1086
1127
  self.authenticate = authenticate
1087
1128
  self.encrypt = encrypt or authenticate
@@ -1134,7 +1175,7 @@ class Central(Connection.Listener):
1134
1175
  DEFAULT_CENTRAL_NAME, central_address, hci_source, hci_sink
1135
1176
  )
1136
1177
  mode = self.mode_factory(self.device)
1137
- role = self.role_factory(mode)
1178
+ scenario = self.scenario_factory(mode)
1138
1179
  self.device.classic_enabled = self.classic
1139
1180
 
1140
1181
  # Set up a pairing config factory with minimal requirements.
@@ -1215,7 +1256,7 @@ class Central(Connection.Listener):
1215
1256
 
1216
1257
  await mode.on_connection(self.connection)
1217
1258
 
1218
- await role.run()
1259
+ await scenario.run()
1219
1260
  await asyncio.sleep(DEFAULT_LINGER_TIME)
1220
1261
  await self.connection.disconnect()
1221
1262
 
@@ -1246,7 +1287,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1246
1287
  def __init__(
1247
1288
  self,
1248
1289
  transport,
1249
- role_factory,
1290
+ scenario_factory,
1250
1291
  mode_factory,
1251
1292
  classic,
1252
1293
  extended_data_length,
@@ -1254,11 +1295,11 @@ class Peripheral(Device.Listener, Connection.Listener):
1254
1295
  ):
1255
1296
  self.transport = transport
1256
1297
  self.classic = classic
1257
- self.role_factory = role_factory
1298
+ self.scenario_factory = scenario_factory
1258
1299
  self.mode_factory = mode_factory
1259
1300
  self.extended_data_length = extended_data_length
1260
1301
  self.role_switch = role_switch
1261
- self.role = None
1302
+ self.scenario = None
1262
1303
  self.mode = None
1263
1304
  self.device = None
1264
1305
  self.connection = None
@@ -1278,7 +1319,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1278
1319
  )
1279
1320
  self.device.listener = self
1280
1321
  self.mode = self.mode_factory(self.device)
1281
- self.role = self.role_factory(self.mode)
1322
+ self.scenario = self.scenario_factory(self.mode)
1282
1323
  self.device.classic_enabled = self.classic
1283
1324
 
1284
1325
  # Set up a pairing config factory with minimal requirements.
@@ -1315,7 +1356,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1315
1356
  print_connection(self.connection)
1316
1357
 
1317
1358
  await self.mode.on_connection(self.connection)
1318
- await self.role.run()
1359
+ await self.scenario.run()
1319
1360
  await asyncio.sleep(DEFAULT_LINGER_TIME)
1320
1361
 
1321
1362
  def on_connection(self, connection):
@@ -1344,7 +1385,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1344
1385
  def on_disconnection(self, reason):
1345
1386
  logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
1346
1387
  self.connection = None
1347
- self.role.reset()
1388
+ self.scenario.reset()
1348
1389
 
1349
1390
  if self.classic:
1350
1391
  AsyncRunner.spawn(self.device.set_discoverable(True))
@@ -1426,13 +1467,13 @@ def create_mode_factory(ctx, default_mode):
1426
1467
 
1427
1468
 
1428
1469
  # -----------------------------------------------------------------------------
1429
- def create_role_factory(ctx, default_role):
1430
- role = ctx.obj['role']
1431
- if role is None:
1432
- role = default_role
1470
+ def create_scenario_factory(ctx, default_scenario):
1471
+ scenario = ctx.obj['scenario']
1472
+ if scenario is None:
1473
+ scenarion = default_scenario
1433
1474
 
1434
- def create_role(packet_io):
1435
- if role == 'sender':
1475
+ def create_scenario(packet_io):
1476
+ if scenario == 'send':
1436
1477
  return Sender(
1437
1478
  packet_io,
1438
1479
  start_delay=ctx.obj['start_delay'],
@@ -1443,10 +1484,10 @@ def create_role_factory(ctx, default_role):
1443
1484
  packet_count=ctx.obj['packet_count'],
1444
1485
  )
1445
1486
 
1446
- if role == 'receiver':
1487
+ if scenario == 'receive':
1447
1488
  return Receiver(packet_io, ctx.obj['linger'])
1448
1489
 
1449
- if role == 'ping':
1490
+ if scenario == 'ping':
1450
1491
  return Ping(
1451
1492
  packet_io,
1452
1493
  start_delay=ctx.obj['start_delay'],
@@ -1457,12 +1498,12 @@ def create_role_factory(ctx, default_role):
1457
1498
  packet_count=ctx.obj['packet_count'],
1458
1499
  )
1459
1500
 
1460
- if role == 'pong':
1501
+ if scenario == 'pong':
1461
1502
  return Pong(packet_io, ctx.obj['linger'])
1462
1503
 
1463
- raise ValueError('invalid role')
1504
+ raise ValueError('invalid scenario')
1464
1505
 
1465
- return create_role
1506
+ return create_scenario
1466
1507
 
1467
1508
 
1468
1509
  # -----------------------------------------------------------------------------
@@ -1470,7 +1511,7 @@ def create_role_factory(ctx, default_role):
1470
1511
  # -----------------------------------------------------------------------------
1471
1512
  @click.group()
1472
1513
  @click.option('--device-config', metavar='FILENAME', help='Device configuration file')
1473
- @click.option('--role', type=click.Choice(['sender', 'receiver', 'ping', 'pong']))
1514
+ @click.option('--scenario', type=click.Choice(['send', 'receive', 'ping', 'pong']))
1474
1515
  @click.option(
1475
1516
  '--mode',
1476
1517
  type=click.Choice(
@@ -1503,7 +1544,7 @@ def create_role_factory(ctx, default_role):
1503
1544
  '--rfcomm-channel',
1504
1545
  type=int,
1505
1546
  default=DEFAULT_RFCOMM_CHANNEL,
1506
- help='RFComm channel to use',
1547
+ help='RFComm channel to use (specify 0 for channel discovery via SDP)',
1507
1548
  )
1508
1549
  @click.option(
1509
1550
  '--rfcomm-uuid',
@@ -1565,7 +1606,7 @@ def create_role_factory(ctx, default_role):
1565
1606
  metavar='SIZE',
1566
1607
  type=click.IntRange(8, 8192),
1567
1608
  default=500,
1568
- help='Packet size (client or ping role)',
1609
+ help='Packet size (send or ping scenario)',
1569
1610
  )
1570
1611
  @click.option(
1571
1612
  '--packet-count',
@@ -1573,7 +1614,7 @@ def create_role_factory(ctx, default_role):
1573
1614
  metavar='COUNT',
1574
1615
  type=int,
1575
1616
  default=10,
1576
- help='Packet count (client or ping role)',
1617
+ help='Packet count (send or ping scenario)',
1577
1618
  )
1578
1619
  @click.option(
1579
1620
  '--start-delay',
@@ -1581,7 +1622,7 @@ def create_role_factory(ctx, default_role):
1581
1622
  metavar='SECONDS',
1582
1623
  type=int,
1583
1624
  default=1,
1584
- help='Start delay (client or ping role)',
1625
+ help='Start delay (send or ping scenario)',
1585
1626
  )
1586
1627
  @click.option(
1587
1628
  '--repeat',
@@ -1589,7 +1630,7 @@ def create_role_factory(ctx, default_role):
1589
1630
  type=int,
1590
1631
  default=0,
1591
1632
  help=(
1592
- 'Repeat the run N times (client and ping roles)'
1633
+ 'Repeat the run N times (send and ping scenario)'
1593
1634
  '(0, which is the fault, to run just once) '
1594
1635
  ),
1595
1636
  )
@@ -1613,13 +1654,13 @@ def create_role_factory(ctx, default_role):
1613
1654
  @click.option(
1614
1655
  '--linger',
1615
1656
  is_flag=True,
1616
- help="Don't exit at the end of a run (server and pong roles)",
1657
+ help="Don't exit at the end of a run (receive and pong scenarios)",
1617
1658
  )
1618
1659
  @click.pass_context
1619
1660
  def bench(
1620
1661
  ctx,
1621
1662
  device_config,
1622
- role,
1663
+ scenario,
1623
1664
  mode,
1624
1665
  att_mtu,
1625
1666
  extended_data_length,
@@ -1645,7 +1686,7 @@ def bench(
1645
1686
  ):
1646
1687
  ctx.ensure_object(dict)
1647
1688
  ctx.obj['device_config'] = device_config
1648
- ctx.obj['role'] = role
1689
+ ctx.obj['scenario'] = scenario
1649
1690
  ctx.obj['mode'] = mode
1650
1691
  ctx.obj['att_mtu'] = att_mtu
1651
1692
  ctx.obj['rfcomm_channel'] = rfcomm_channel
@@ -1699,7 +1740,7 @@ def central(
1699
1740
  ctx, transport, peripheral_address, connection_interval, phy, authenticate, encrypt
1700
1741
  ):
1701
1742
  """Run as a central (initiates the connection)"""
1702
- role_factory = create_role_factory(ctx, 'sender')
1743
+ scenario_factory = create_scenario_factory(ctx, 'send')
1703
1744
  mode_factory = create_mode_factory(ctx, 'gatt-client')
1704
1745
  classic = ctx.obj['classic']
1705
1746
 
@@ -1708,7 +1749,7 @@ def central(
1708
1749
  transport,
1709
1750
  peripheral_address,
1710
1751
  classic,
1711
- role_factory,
1752
+ scenario_factory,
1712
1753
  mode_factory,
1713
1754
  connection_interval,
1714
1755
  phy,
@@ -1726,13 +1767,13 @@ def central(
1726
1767
  @click.pass_context
1727
1768
  def peripheral(ctx, transport):
1728
1769
  """Run as a peripheral (waits for a connection)"""
1729
- role_factory = create_role_factory(ctx, 'receiver')
1770
+ scenario_factory = create_scenario_factory(ctx, 'receive')
1730
1771
  mode_factory = create_mode_factory(ctx, 'gatt-server')
1731
1772
 
1732
1773
  async def run_peripheral():
1733
1774
  await Peripheral(
1734
1775
  transport,
1735
- role_factory,
1776
+ scenario_factory,
1736
1777
  mode_factory,
1737
1778
  ctx.obj['classic'],
1738
1779
  ctx.obj['extended_data_length'],
@@ -1743,7 +1784,11 @@ def peripheral(ctx, transport):
1743
1784
 
1744
1785
 
1745
1786
  def main():
1746
- logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
1787
+ logging.basicConfig(
1788
+ level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper(),
1789
+ format="[%(asctime)s.%(msecs)03d] %(levelname)s:%(name)s:%(message)s",
1790
+ datefmt="%H:%M:%S",
1791
+ )
1747
1792
  bench()
1748
1793
 
1749
1794
 
@@ -237,6 +237,7 @@ class ClientBridge:
237
237
  address: str,
238
238
  tcp_host: str,
239
239
  tcp_port: int,
240
+ authenticate: bool,
240
241
  encrypt: bool,
241
242
  ):
242
243
  self.channel = channel
@@ -245,6 +246,7 @@ class ClientBridge:
245
246
  self.address = address
246
247
  self.tcp_host = tcp_host
247
248
  self.tcp_port = tcp_port
249
+ self.authenticate = authenticate
248
250
  self.encrypt = encrypt
249
251
  self.device: Optional[Device] = None
250
252
  self.connection: Optional[Connection] = None
@@ -274,6 +276,11 @@ class ClientBridge:
274
276
  print(color(f"@@@ Bluetooth connection: {self.connection}", "blue"))
275
277
  self.connection.on("disconnection", self.on_disconnection)
276
278
 
279
+ if self.authenticate:
280
+ print(color("@@@ Authenticating Bluetooth connection", "blue"))
281
+ await self.connection.authenticate()
282
+ print(color("@@@ Bluetooth connection authenticated", "blue"))
283
+
277
284
  if self.encrypt:
278
285
  print(color("@@@ Encrypting Bluetooth connection", "blue"))
279
286
  await self.connection.encrypt()
@@ -491,8 +498,9 @@ def server(context, tcp_host, tcp_port):
491
498
  @click.argument("bluetooth-address")
492
499
  @click.option("--tcp-host", help="TCP host", default="_")
493
500
  @click.option("--tcp-port", help="TCP port", default=DEFAULT_CLIENT_TCP_PORT)
501
+ @click.option("--authenticate", is_flag=True, help="Authenticate the connection")
494
502
  @click.option("--encrypt", is_flag=True, help="Encrypt the connection")
495
- def client(context, bluetooth_address, tcp_host, tcp_port, encrypt):
503
+ def client(context, bluetooth_address, tcp_host, tcp_port, authenticate, encrypt):
496
504
  bridge = ClientBridge(
497
505
  context.obj["channel"],
498
506
  context.obj["uuid"],
@@ -500,6 +508,7 @@ def client(context, bluetooth_address, tcp_host, tcp_port, encrypt):
500
508
  bluetooth_address,
501
509
  tcp_host,
502
510
  tcp_port,
511
+ authenticate,
503
512
  encrypt,
504
513
  )
505
514
  asyncio.run(run(context.obj["device_config"], context.obj["hci_transport"], bridge))
bumble/avc.py CHANGED
@@ -134,6 +134,8 @@ class Frame:
134
134
  opcode_offset = 3
135
135
  elif subunit_id == 6:
136
136
  raise core.InvalidPacketError("reserved subunit ID")
137
+ else:
138
+ raise core.InvalidPacketError("invalid subunit ID")
137
139
 
138
140
  opcode = Frame.OperationCode(data[opcode_offset])
139
141
  operands = data[opcode_offset + 1 :]
bumble/gatt_client.py CHANGED
@@ -898,6 +898,12 @@ class Client:
898
898
  ) and subscriber in subscribers:
899
899
  subscribers.remove(subscriber)
900
900
 
901
+ # The characteristic itself is added as subscriber. If it is the
902
+ # last remaining subscriber, we remove it, such that the clean up
903
+ # works correctly. Otherwise the CCCD never is set back to 0.
904
+ if len(subscribers) == 1 and characteristic in subscribers:
905
+ subscribers.remove(characteristic)
906
+
901
907
  # Cleanup if we removed the last one
902
908
  if not subscribers:
903
909
  del subscriber_set[characteristic.handle]
bumble/l2cap.py CHANGED
@@ -1911,6 +1911,7 @@ class ChannelManager:
1911
1911
  data = sum(1 << cid for cid in self.fixed_channels).to_bytes(8, 'little')
1912
1912
  else:
1913
1913
  result = L2CAP_Information_Response.NOT_SUPPORTED
1914
+ data = b''
1914
1915
 
1915
1916
  self.send_control_frame(
1916
1917
  connection,
bumble/link.py CHANGED
@@ -122,6 +122,8 @@ class LocalLink:
122
122
  elif transport == BT_BR_EDR_TRANSPORT:
123
123
  destination_controller = self.find_classic_controller(destination_address)
124
124
  source_address = sender_controller.public_address
125
+ else:
126
+ raise ValueError("unsupported transport type")
125
127
 
126
128
  if destination_controller is not None:
127
129
  destination_controller.on_link_acl_data(source_address, transport, data)
bumble/profiles/bap.py CHANGED
@@ -102,6 +102,7 @@ class ContextType(enum.IntFlag):
102
102
 
103
103
  # fmt: off
104
104
  PROHIBITED = 0x0000
105
+ UNSPECIFIED = 0x0001
105
106
  CONVERSATIONAL = 0x0002
106
107
  MEDIA = 0x0004
107
108
  GAME = 0x0008
@@ -350,6 +351,7 @@ class CodecSpecificCapabilities:
350
351
  supported_max_codec_frames_per_sdu = value
351
352
 
352
353
  # It is expected here that if some fields are missing, an error should be raised.
354
+ # pylint: disable=possibly-used-before-assignment,used-before-assignment
353
355
  return CodecSpecificCapabilities(
354
356
  supported_sampling_frequencies=supported_sampling_frequencies,
355
357
  supported_frame_durations=supported_frame_durations,
@@ -426,6 +428,7 @@ class CodecSpecificConfiguration:
426
428
  codec_frames_per_sdu = value
427
429
 
428
430
  # It is expected here that if some fields are missing, an error should be raised.
431
+ # pylint: disable=possibly-used-before-assignment,used-before-assignment
429
432
  return CodecSpecificConfiguration(
430
433
  sampling_frequency=sampling_frequency,
431
434
  frame_duration=frame_duration,
bumble/sdp.py CHANGED
@@ -434,6 +434,8 @@ class DataElement:
434
434
  if size != 1:
435
435
  raise InvalidArgumentError('boolean must be 1 byte')
436
436
  size_index = 0
437
+ else:
438
+ raise RuntimeError("internal error - self.type not supported")
437
439
 
438
440
  self.bytes = bytes([self.type << 3 | size_index]) + size_bytes + data
439
441
  return self.bytes
bumble/smp.py CHANGED
@@ -1839,7 +1839,7 @@ class Session:
1839
1839
  if self.is_initiator:
1840
1840
  if self.pairing_method == PairingMethod.OOB:
1841
1841
  self.send_pairing_random_command()
1842
- else:
1842
+ elif self.pairing_method == PairingMethod.PASSKEY:
1843
1843
  self.send_pairing_confirm_command()
1844
1844
  else:
1845
1845
  if self.pairing_method == PairingMethod.PASSKEY:
@@ -370,11 +370,13 @@ class PumpedPacketSource(ParserSource):
370
370
  self.parser.feed_data(packet)
371
371
  except asyncio.CancelledError:
372
372
  logger.debug('source pump task done')
373
- self.terminated.set_result(None)
373
+ if not self.terminated.done():
374
+ self.terminated.set_result(None)
374
375
  break
375
376
  except Exception as error:
376
377
  logger.warning(f'exception while waiting for packet: {error}')
377
- self.terminated.set_exception(error)
378
+ if not self.terminated.done():
379
+ self.terminated.set_exception(error)
378
380
  break
379
381
 
380
382
  self.pump_task = asyncio.create_task(pump_packets())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bumble
3
- Version: 0.0.201
3
+ Version: 0.0.202
4
4
  Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
5
5
  Home-page: https://github.com/google/bumble
6
6
  Author: Google
@@ -35,9 +35,10 @@ Provides-Extra: development
35
35
  Requires-Dist: black==24.3; extra == "development"
36
36
  Requires-Dist: grpcio-tools>=1.62.1; extra == "development"
37
37
  Requires-Dist: invoke>=1.7.3; extra == "development"
38
- Requires-Dist: mypy==1.10.0; extra == "development"
38
+ Requires-Dist: mobly>=1.12.2; extra == "development"
39
+ Requires-Dist: mypy==1.12.0; extra == "development"
39
40
  Requires-Dist: nox>=2022; extra == "development"
40
- Requires-Dist: pylint==3.1.0; extra == "development"
41
+ Requires-Dist: pylint==3.3.1; extra == "development"
41
42
  Requires-Dist: pyyaml>=6.0; extra == "development"
42
43
  Requires-Dist: types-appdirs>=1.4.3; extra == "development"
43
44
  Requires-Dist: types-invoke>=1.7.3; extra == "development"
@@ -1,9 +1,9 @@
1
1
  bumble/__init__.py,sha256=Q8jkz6rgl95IMAeInQVt_2GLoJl3DcEP2cxtrQ-ho5c,110
2
- bumble/_version.py,sha256=hc4syiO9GdDB7IANWoNKzXpgMbnaNzyfU3UJ-aEo69M,415
2
+ bumble/_version.py,sha256=0o3Qq5M65Ds-43dG5u5R1z10hjSzTudi88nO90cBlv4,415
3
3
  bumble/a2dp.py,sha256=_dCq-qyG5OglDVlaOFwAgFe_ugvHuEdEYL-kWFf6sWQ,31775
4
4
  bumble/at.py,sha256=Giu2VUSJKH-jIh10lOfumiqy-FyO99Ra6nJ7UiWQ0H8,3114
5
5
  bumble/att.py,sha256=jFUcIDP3JYYdazWAVynGy0pcgNOzsldRzoBhhOkLCRI,32767
6
- bumble/avc.py,sha256=OZz5_DYenPeg2PcNM3dpgV5PUs4D7FBSNOcSqsuRJ2w,16329
6
+ bumble/avc.py,sha256=cO1-8x7BvuBCQVJg-9nTibkLFC4y0_2SNERZ8_7Kn_c,16407
7
7
  bumble/avctp.py,sha256=yHAjJRjLGtR0Q-iWcLS7cJRz5Jr2YiRmZd6LZV4Xjt4,9935
8
8
  bumble/avdtp.py,sha256=2ki_BE4SHiu3Sx9oHCknfjF-bBcgPB9TsyF5upciUYI,76773
9
9
  bumble/avrcp.py,sha256=P_pLVpP3kRtoD2y0Ca0NTEEe1mA4SXO_ldtc4OP6Tcc,69976
@@ -18,7 +18,7 @@ bumble/decoder.py,sha256=0-VNWZT-u7lvK3qBpAuYT0M6Rz_bMgMi4CjfUXX_6RM,9728
18
18
  bumble/device.py,sha256=vroAnNWlBW6_jw1KgmoG4ZWKyauLm8abS1XFrTX084Y,195024
19
19
  bumble/gap.py,sha256=dRU2_TWvqTDx80hxeSbXlWIeWvptWH4_XbItG5y948Q,2138
20
20
  bumble/gatt.py,sha256=a07mQ3O0LFt5zgUzSUwa4Jak_FXOXSFX-njxnQNS_zY,39014
21
- bumble/gatt_client.py,sha256=gIpgNQj5WNm9RirXXT4vuXQZ4WK82IFMOqnfqBtoYRU,42809
21
+ bumble/gatt_client.py,sha256=2Y9rp5kn-aPC2aC0maiWmvrixcWvCWSktgEdjj_xr5w,43202
22
22
  bumble/gatt_server.py,sha256=pafGMeAuGAAELnr_1pB_l3CcR5u4H4Y1-MRHjN53gdE,37449
23
23
  bumble/hci.py,sha256=e2JqP-D1h7b7vHxFbmdsLJtOacdiuMizzGgBWmCAhxQ,286058
24
24
  bumble/helpers.py,sha256=m0w4UgFFNDEnXwHrDyfRlcBObdVed2fqXGL0lvR3c8s,12733
@@ -26,20 +26,20 @@ bumble/hfp.py,sha256=h5IcgxKnlFWapeJHcNDTvxup9oAE5CcZju92sOusTGc,75619
26
26
  bumble/hid.py,sha256=hJKm6qhNa0kQTGmp_VxNh3-ywgBDdJpPPFcvtFiRL0A,20335
27
27
  bumble/host.py,sha256=pPH7HTQ6ogOToj4Y5Jl985MKjbVlG_jeOcJrV0NTSqU,49204
28
28
  bumble/keys.py,sha256=WbIQ7Ob81mW75qmEPQ2rBLfnqBMA-ts2yowWXP9UaCY,12654
29
- bumble/l2cap.py,sha256=Bu6oTD3DzqLOKiarynT1cfQefNgR0gCJoKxnwQJl2_o,81398
30
- bumble/link.py,sha256=MYKsIKpbbAkh_ldxPZKu_GhS90ImC0IQtCAqbY4Ddg4,24034
29
+ bumble/l2cap.py,sha256=PJxoXV6h1hhHlkKKY3KGuF8l3HTpV-C-mAE6jfTkLY4,81421
30
+ bumble/link.py,sha256=SU7Ls2Lyg1XuY8x6yP9tAC83SYmMTU2a-vQ_CWCfq90,24107
31
31
  bumble/pairing.py,sha256=tgPUba6xNxMi-2plm3xfRlzHq-uPRNZEIGWaN0qNGCs,9853
32
32
  bumble/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  bumble/rfcomm.py,sha256=dh5t5vlDEfw3yHgQfzegMYPnShP8Zo-3ScABUvmXNLI,40751
34
34
  bumble/rtp.py,sha256=388X3aCv-QrWJ37r_VPqXYJtvNGWPsHnJasqs41g_-s,3487
35
- bumble/sdp.py,sha256=aajRQybcMWh_Kc4IuQahix6bXnHg61OXl1gSdrSysl4,45465
36
- bumble/smp.py,sha256=FNLn-pt2n6CKNttQGJJDnfaeof2SVkxlapwySHWAVJY,77640
35
+ bumble/sdp.py,sha256=I45-qVmFcdRVd65iBuYTwrlKZX_AXAjnXrj4wjLR2co,45554
36
+ bumble/smp.py,sha256=Mc2q_tG3_ATJDrx5ZkPpNI7g6h6g1vRvH8OOQDbI39c,77685
37
37
  bumble/snoop.py,sha256=1mzwmp9LToUXbPnFsLrt8S4UHs0kqzbu7LDydwbmkZI,5715
38
38
  bumble/utils.py,sha256=e0i-4d28-9zP3gYcd1rdNd669rkPnRs5oJCERUEDfxo,15099
39
39
  bumble/apps/README.md,sha256=XTwjRAY-EJWDXpl1V8K3Mw8B7kIqzUIUizRjVBVhoIE,1769
40
40
  bumble/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  bumble/apps/auracast.py,sha256=hrG6dW79UkMXqcMLgYjziWEjcVibFUITvjPb4LX1lxM,24686
42
- bumble/apps/bench.py,sha256=nPACg5gQWxJ-6phze12FVCv1_WS2UjrWR7OUWj0o8PU,56350
42
+ bumble/apps/bench.py,sha256=RitVnx9mzmW-1cB4O7wan-lQeFDlazdjTW-BQcuTDb0,58410
43
43
  bumble/apps/ble_rpa_tool.py,sha256=ZQtsbfnLPd5qUAkEBPpNgJLRynBBc7q_9cDHKUW2SQ0,1701
44
44
  bumble/apps/console.py,sha256=rwD9y3g8Mm_mAEvrcXjbtcv5d8mwF3yTbmE6Vet2BEk,45300
45
45
  bumble/apps/controller_info.py,sha256=WScuK5Ytp5aFEosAcgRTCEeey6SmxDFmyB7KBhgVx6s,11759
@@ -52,7 +52,7 @@ bumble/apps/hci_bridge.py,sha256=KISv352tKnsQsoxjkDiCQbMFmhnPWdnug5wSFAAXxEs,403
52
52
  bumble/apps/l2cap_bridge.py,sha256=524VgEmgCP4g7T0UdgmsePmNVhDFRJECeaZ_uzKsbco,13062
53
53
  bumble/apps/pair.py,sha256=NtDxLfdnlOY_ZEtVNGVXWjv6_x_hndok_ydhV6zkFtI,18503
54
54
  bumble/apps/pandora_server.py,sha256=5qaoLCpcZE2KsGO21-7t6Vg4dBjBWbnyOQXwrLhxkuE,1397
55
- bumble/apps/rfcomm_bridge.py,sha256=PSszh4Qh1IsIw8ETs0fevOCAXEdVtqlgnV-ruzqGrZI,17215
55
+ bumble/apps/rfcomm_bridge.py,sha256=bAdDz84YpYkEfZ6vanQ_VUEpEF4MS4Y9fmbXB4bhoi4,17633
56
56
  bumble/apps/scan.py,sha256=b6hIppiJqDfR7VFW2wl3-lkPdFvHLqYZKY8VjjNnhls,8366
57
57
  bumble/apps/show.py,sha256=8w0-8jLtN6IM6_58pOHbEmE1Rmxm71O48ACrXixC2jk,6218
58
58
  bumble/apps/unbond.py,sha256=LDPWpmgKLMGYDdIFGTdGciFDcUliZ0OmseEbGfJ-MAM,3176
@@ -86,7 +86,7 @@ bumble/profiles/__init__.py,sha256=yBGC8Ti5LvZuoh1F42XtfrBilb39T77_yuxESZeX2yI,5
86
86
  bumble/profiles/aics.py,sha256=U0K03XqOKtqahXtq7U47WxftsAD7tkiDeUqxfaF4pwk,18540
87
87
  bumble/profiles/ascs.py,sha256=Y2_fd_5LOb9hon60LcQl3R-FoAIsOd_J37RZpveh-7c,25471
88
88
  bumble/profiles/asha.py,sha256=QXUp7ImuMD48L0mpuNxPcIkv8VvoENynCuF2PEVqzFM,10373
89
- bumble/profiles/bap.py,sha256=wLnrbXso203rgw0WNa91qIdMdjuIoGc9VqOHc9Ey_b4,19421
89
+ bumble/profiles/bap.py,sha256=E_D6dGMWgTM2HsAk7y-iK-IWVJxfPtHOB5KZZkYV6yw,19613
90
90
  bumble/profiles/bass.py,sha256=Wiqum0Wsr5PpVzTAPDcyKLTfJoKXJUYOzqB320aSiUs,14950
91
91
  bumble/profiles/battery_service.py,sha256=w-uF4jLoDozJOoykimb2RkrKjVyCke6ts2-h-F1PYyc,2292
92
92
  bumble/profiles/cap.py,sha256=6gH7oOnUKjOggMPuB7rtbwj0AneoNmnWzQ_iR3io8e0,1945
@@ -109,7 +109,7 @@ bumble/tools/rtk_util.py,sha256=TwZhupHQrQYsYHLdRGyzXKd24pwCk8kkzqK1Rj2guco,5087
109
109
  bumble/transport/__init__.py,sha256=Z01fvuKpqAbhJd0wYcGhW09W2tycM71ck80XoZ8a87Q,7012
110
110
  bumble/transport/android_emulator.py,sha256=6HR2cEqdU0XbOldwxCtQuXtvwOUYhRfHkPz0TRt3mbo,4382
111
111
  bumble/transport/android_netsim.py,sha256=P-keFdM9-iU_HQQYirYX-yEJtEM_gItzi9srNzWRQiI,17196
112
- bumble/transport/common.py,sha256=bJWYH-vRJJl0nWwlAjGTHrRQFdZayKkH7YoGor5abH8,16659
112
+ bumble/transport/common.py,sha256=caAHY0JqYmE91rrDFkTPX_FtUWtz4quqgQpMRSI9Jsc,16769
113
113
  bumble/transport/file.py,sha256=eVM2V6Nk2nDAFdE7Rt01ZI3JdTovsH9OEU1gKYPJjpE,2010
114
114
  bumble/transport/hci_socket.py,sha256=EdgWi3-O5yvYcH4R4BkPtG79pnUo7GQtXWawuUHDoDQ,6331
115
115
  bumble/transport/pty.py,sha256=grTl-yvjMWHflNwuME4ccVqDbk6NIEgQMgH6Y9lf1fU,2732
@@ -165,9 +165,9 @@ bumble/vendor/android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
165
165
  bumble/vendor/android/hci.py,sha256=GZrkhaWmcMt1JpnRhv0NoySGkf2H4lNUV2f_omRZW0I,10741
166
166
  bumble/vendor/zephyr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
167
167
  bumble/vendor/zephyr/hci.py,sha256=d83bC0TvT947eN4roFjLkQefWtHOoNsr4xib2ctSkvA,3195
168
- bumble-0.0.201.dist-info/LICENSE,sha256=FvaYh4NRWIGgS_OwoBs5gFgkCmAghZ-DYnIGBZPuw-s,12142
169
- bumble-0.0.201.dist-info/METADATA,sha256=pyz0Jl2iwL7S-5PbYkfC-nfla3lyy7off2EAJhPVH18,5673
170
- bumble-0.0.201.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
171
- bumble-0.0.201.dist-info/entry_points.txt,sha256=2TAnDAHiYVEo9Gnugk29QIsHpCgRgnPqBszLSgIX2T0,984
172
- bumble-0.0.201.dist-info/top_level.txt,sha256=tV6JJKaHPYMFiJYiBYFW24PCcfLxTJZdlu6BmH3Cb00,7
173
- bumble-0.0.201.dist-info/RECORD,,
168
+ bumble-0.0.202.dist-info/LICENSE,sha256=FvaYh4NRWIGgS_OwoBs5gFgkCmAghZ-DYnIGBZPuw-s,12142
169
+ bumble-0.0.202.dist-info/METADATA,sha256=rao_wk6XWfEzXwGDY0rdDLSByA_rBfAiS9NSf9v111I,5726
170
+ bumble-0.0.202.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
171
+ bumble-0.0.202.dist-info/entry_points.txt,sha256=2TAnDAHiYVEo9Gnugk29QIsHpCgRgnPqBszLSgIX2T0,984
172
+ bumble-0.0.202.dist-info/top_level.txt,sha256=tV6JJKaHPYMFiJYiBYFW24PCcfLxTJZdlu6BmH3Cb00,7
173
+ bumble-0.0.202.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5