bumble 0.0.193__py3-none-any.whl → 0.0.195__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/apps/bench.py CHANGED
@@ -40,6 +40,8 @@ from bumble.hci import (
40
40
  HCI_LE_1M_PHY,
41
41
  HCI_LE_2M_PHY,
42
42
  HCI_LE_CODED_PHY,
43
+ HCI_CENTRAL_ROLE,
44
+ HCI_PERIPHERAL_ROLE,
43
45
  HCI_Constant,
44
46
  HCI_Error,
45
47
  HCI_StatusError,
@@ -57,6 +59,7 @@ from bumble.transport import open_transport_or_link
57
59
  import bumble.rfcomm
58
60
  import bumble.core
59
61
  from bumble.utils import AsyncRunner
62
+ from bumble.pairing import PairingConfig
60
63
 
61
64
 
62
65
  # -----------------------------------------------------------------------------
@@ -128,40 +131,34 @@ def le_phy_name(phy_id):
128
131
 
129
132
 
130
133
  def print_connection(connection):
134
+ params = []
131
135
  if connection.transport == BT_LE_TRANSPORT:
132
- phy_state = (
136
+ params.append(
133
137
  'PHY='
134
138
  f'TX:{le_phy_name(connection.phy.tx_phy)}/'
135
139
  f'RX:{le_phy_name(connection.phy.rx_phy)}'
136
140
  )
137
141
 
138
- data_length = (
142
+ params.append(
139
143
  'DL=('
140
144
  f'TX:{connection.data_length[0]}/{connection.data_length[1]},'
141
145
  f'RX:{connection.data_length[2]}/{connection.data_length[3]}'
142
146
  ')'
143
147
  )
144
- connection_parameters = (
148
+
149
+ params.append(
145
150
  'Parameters='
146
151
  f'{connection.parameters.connection_interval * 1.25:.2f}/'
147
152
  f'{connection.parameters.peripheral_latency}/'
148
153
  f'{connection.parameters.supervision_timeout * 10} '
149
154
  )
150
155
 
156
+ params.append(f'MTU={connection.att_mtu}')
157
+
151
158
  else:
152
- phy_state = ''
153
- data_length = ''
154
- connection_parameters = ''
159
+ params.append(f'Role={HCI_Constant.role_name(connection.role)}')
155
160
 
156
- mtu = connection.att_mtu
157
-
158
- logging.info(
159
- f'{color("@@@ Connection:", "yellow")} '
160
- f'{connection_parameters} '
161
- f'{data_length} '
162
- f'{phy_state} '
163
- f'MTU={mtu}'
164
- )
161
+ logging.info(color('@@@ Connection: ', 'yellow') + ' '.join(params))
165
162
 
166
163
 
167
164
  def make_sdp_records(channel):
@@ -214,6 +211,17 @@ def log_stats(title, stats):
214
211
  )
215
212
 
216
213
 
214
+ async def switch_roles(connection, role):
215
+ target_role = HCI_CENTRAL_ROLE if role == "central" else HCI_PERIPHERAL_ROLE
216
+ if connection.role != target_role:
217
+ logging.info(f'{color("### Switching roles to:", "cyan")} {role}')
218
+ try:
219
+ await connection.switch_role(target_role)
220
+ logging.info(color('### Role switch complete', 'cyan'))
221
+ except HCI_Error as error:
222
+ logging.info(f'{color("### Role switch failed:", "red")} {error}')
223
+
224
+
217
225
  class PacketType(enum.IntEnum):
218
226
  RESET = 0
219
227
  SEQUENCE = 1
@@ -899,14 +907,26 @@ class L2capServer(StreamedPacketIO):
899
907
  # RfcommClient
900
908
  # -----------------------------------------------------------------------------
901
909
  class RfcommClient(StreamedPacketIO):
902
- def __init__(self, device, channel, uuid, l2cap_mtu, max_frame_size, window_size):
910
+ def __init__(
911
+ self,
912
+ device,
913
+ channel,
914
+ uuid,
915
+ l2cap_mtu,
916
+ max_frame_size,
917
+ initial_credits,
918
+ max_credits,
919
+ credits_threshold,
920
+ ):
903
921
  super().__init__()
904
922
  self.device = device
905
923
  self.channel = channel
906
924
  self.uuid = uuid
907
925
  self.l2cap_mtu = l2cap_mtu
908
926
  self.max_frame_size = max_frame_size
909
- self.window_size = window_size
927
+ self.initial_credits = initial_credits
928
+ self.max_credits = max_credits
929
+ self.credits_threshold = credits_threshold
910
930
  self.rfcomm_session = None
911
931
  self.ready = asyncio.Event()
912
932
 
@@ -940,12 +960,17 @@ class RfcommClient(StreamedPacketIO):
940
960
  logging.info(color(f'### Opening session for channel {channel}...', 'yellow'))
941
961
  try:
942
962
  dlc_options = {}
943
- if self.max_frame_size:
963
+ if self.max_frame_size is not None:
944
964
  dlc_options['max_frame_size'] = self.max_frame_size
945
- if self.window_size:
946
- dlc_options['window_size'] = self.window_size
965
+ if self.initial_credits is not None:
966
+ dlc_options['initial_credits'] = self.initial_credits
947
967
  rfcomm_session = await rfcomm_mux.open_dlc(channel, **dlc_options)
948
968
  logging.info(color(f'### Session open: {rfcomm_session}', 'yellow'))
969
+ if self.max_credits is not None:
970
+ rfcomm_session.rx_max_credits = self.max_credits
971
+ if self.credits_threshold is not None:
972
+ rfcomm_session.rx_credits_threshold = self.credits_threshold
973
+
949
974
  except bumble.core.ConnectionError as error:
950
975
  logging.info(color(f'!!! Session open failed: {error}', 'red'))
951
976
  await rfcomm_mux.disconnect()
@@ -969,8 +994,19 @@ class RfcommClient(StreamedPacketIO):
969
994
  # RfcommServer
970
995
  # -----------------------------------------------------------------------------
971
996
  class RfcommServer(StreamedPacketIO):
972
- def __init__(self, device, channel, l2cap_mtu):
997
+ def __init__(
998
+ self,
999
+ device,
1000
+ channel,
1001
+ l2cap_mtu,
1002
+ max_frame_size,
1003
+ initial_credits,
1004
+ max_credits,
1005
+ credits_threshold,
1006
+ ):
973
1007
  super().__init__()
1008
+ self.max_credits = max_credits
1009
+ self.credits_threshold = credits_threshold
974
1010
  self.dlc = None
975
1011
  self.ready = asyncio.Event()
976
1012
 
@@ -981,7 +1017,12 @@ class RfcommServer(StreamedPacketIO):
981
1017
  rfcomm_server = bumble.rfcomm.Server(device, **server_options)
982
1018
 
983
1019
  # Listen for incoming DLC connections
984
- channel_number = rfcomm_server.listen(self.on_dlc, channel)
1020
+ dlc_options = {}
1021
+ if max_frame_size is not None:
1022
+ dlc_options['max_frame_size'] = max_frame_size
1023
+ if initial_credits is not None:
1024
+ dlc_options['initial_credits'] = initial_credits
1025
+ channel_number = rfcomm_server.listen(self.on_dlc, channel, **dlc_options)
985
1026
 
986
1027
  # Setup the SDP to advertise this channel
987
1028
  device.sdp_service_records = make_sdp_records(channel_number)
@@ -1001,9 +1042,17 @@ class RfcommServer(StreamedPacketIO):
1001
1042
 
1002
1043
  def on_dlc(self, dlc):
1003
1044
  logging.info(color(f'*** DLC connected: {dlc}', 'blue'))
1045
+ if self.credits_threshold is not None:
1046
+ dlc.rx_threshold = self.credits_threshold
1047
+ if self.max_credits is not None:
1048
+ dlc.rx_max_credits = self.max_credits
1004
1049
  dlc.sink = self.on_packet
1005
1050
  self.io_sink = dlc.write
1006
1051
  self.dlc = dlc
1052
+ if self.max_credits is not None:
1053
+ dlc.rx_max_credits = self.max_credits
1054
+ if self.credits_threshold is not None:
1055
+ dlc.rx_credits_threshold = self.credits_threshold
1007
1056
 
1008
1057
  async def drain(self):
1009
1058
  assert self.dlc
@@ -1026,6 +1075,7 @@ class Central(Connection.Listener):
1026
1075
  authenticate,
1027
1076
  encrypt,
1028
1077
  extended_data_length,
1078
+ role_switch,
1029
1079
  ):
1030
1080
  super().__init__()
1031
1081
  self.transport = transport
@@ -1036,6 +1086,7 @@ class Central(Connection.Listener):
1036
1086
  self.authenticate = authenticate
1037
1087
  self.encrypt = encrypt or authenticate
1038
1088
  self.extended_data_length = extended_data_length
1089
+ self.role_switch = role_switch
1039
1090
  self.device = None
1040
1091
  self.connection = None
1041
1092
 
@@ -1086,6 +1137,11 @@ class Central(Connection.Listener):
1086
1137
  role = self.role_factory(mode)
1087
1138
  self.device.classic_enabled = self.classic
1088
1139
 
1140
+ # Set up a pairing config factory with minimal requirements.
1141
+ self.device.pairing_config_factory = lambda _: PairingConfig(
1142
+ sc=False, mitm=False, bonding=False
1143
+ )
1144
+
1089
1145
  await self.device.power_on()
1090
1146
 
1091
1147
  if self.classic:
@@ -1114,6 +1170,10 @@ class Central(Connection.Listener):
1114
1170
  self.connection.listener = self
1115
1171
  print_connection(self.connection)
1116
1172
 
1173
+ # Switch roles if needed.
1174
+ if self.role_switch:
1175
+ await switch_roles(self.connection, self.role_switch)
1176
+
1117
1177
  # Wait a bit after the connection, some controllers aren't very good when
1118
1178
  # we start sending data right away while some connection parameters are
1119
1179
  # updated post connection
@@ -1175,20 +1235,30 @@ class Central(Connection.Listener):
1175
1235
  def on_connection_data_length_change(self):
1176
1236
  print_connection(self.connection)
1177
1237
 
1238
+ def on_role_change(self):
1239
+ print_connection(self.connection)
1240
+
1178
1241
 
1179
1242
  # -----------------------------------------------------------------------------
1180
1243
  # Peripheral
1181
1244
  # -----------------------------------------------------------------------------
1182
1245
  class Peripheral(Device.Listener, Connection.Listener):
1183
1246
  def __init__(
1184
- self, transport, classic, extended_data_length, role_factory, mode_factory
1247
+ self,
1248
+ transport,
1249
+ role_factory,
1250
+ mode_factory,
1251
+ classic,
1252
+ extended_data_length,
1253
+ role_switch,
1185
1254
  ):
1186
1255
  self.transport = transport
1187
1256
  self.classic = classic
1188
- self.extended_data_length = extended_data_length
1189
1257
  self.role_factory = role_factory
1190
- self.role = None
1191
1258
  self.mode_factory = mode_factory
1259
+ self.extended_data_length = extended_data_length
1260
+ self.role_switch = role_switch
1261
+ self.role = None
1192
1262
  self.mode = None
1193
1263
  self.device = None
1194
1264
  self.connection = None
@@ -1211,6 +1281,11 @@ class Peripheral(Device.Listener, Connection.Listener):
1211
1281
  self.role = self.role_factory(self.mode)
1212
1282
  self.device.classic_enabled = self.classic
1213
1283
 
1284
+ # Set up a pairing config factory with minimal requirements.
1285
+ self.device.pairing_config_factory = lambda _: PairingConfig(
1286
+ sc=False, mitm=False, bonding=False
1287
+ )
1288
+
1214
1289
  await self.device.power_on()
1215
1290
 
1216
1291
  if self.classic:
@@ -1237,6 +1312,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1237
1312
 
1238
1313
  await self.connected.wait()
1239
1314
  logging.info(color('### Connected', 'cyan'))
1315
+ print_connection(self.connection)
1240
1316
 
1241
1317
  await self.mode.on_connection(self.connection)
1242
1318
  await self.role.run()
@@ -1253,7 +1329,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1253
1329
  AsyncRunner.spawn(self.device.set_connectable(False))
1254
1330
 
1255
1331
  # Request a new data length if needed
1256
- if self.extended_data_length:
1332
+ if not self.classic and self.extended_data_length:
1257
1333
  logging.info("+++ Requesting extended data length")
1258
1334
  AsyncRunner.spawn(
1259
1335
  connection.set_data_length(
@@ -1261,6 +1337,10 @@ class Peripheral(Device.Listener, Connection.Listener):
1261
1337
  )
1262
1338
  )
1263
1339
 
1340
+ # Switch roles if needed.
1341
+ if self.role_switch:
1342
+ AsyncRunner.spawn(switch_roles(connection, self.role_switch))
1343
+
1264
1344
  def on_disconnection(self, reason):
1265
1345
  logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
1266
1346
  self.connection = None
@@ -1282,6 +1362,9 @@ class Peripheral(Device.Listener, Connection.Listener):
1282
1362
  def on_connection_data_length_change(self):
1283
1363
  print_connection(self.connection)
1284
1364
 
1365
+ def on_role_change(self):
1366
+ print_connection(self.connection)
1367
+
1285
1368
 
1286
1369
  # -----------------------------------------------------------------------------
1287
1370
  def create_mode_factory(ctx, default_mode):
@@ -1321,7 +1404,9 @@ def create_mode_factory(ctx, default_mode):
1321
1404
  uuid=ctx.obj['rfcomm_uuid'],
1322
1405
  l2cap_mtu=ctx.obj['rfcomm_l2cap_mtu'],
1323
1406
  max_frame_size=ctx.obj['rfcomm_max_frame_size'],
1324
- window_size=ctx.obj['rfcomm_window_size'],
1407
+ initial_credits=ctx.obj['rfcomm_initial_credits'],
1408
+ max_credits=ctx.obj['rfcomm_max_credits'],
1409
+ credits_threshold=ctx.obj['rfcomm_credits_threshold'],
1325
1410
  )
1326
1411
 
1327
1412
  if mode == 'rfcomm-server':
@@ -1329,6 +1414,10 @@ def create_mode_factory(ctx, default_mode):
1329
1414
  device,
1330
1415
  channel=ctx.obj['rfcomm_channel'],
1331
1416
  l2cap_mtu=ctx.obj['rfcomm_l2cap_mtu'],
1417
+ max_frame_size=ctx.obj['rfcomm_max_frame_size'],
1418
+ initial_credits=ctx.obj['rfcomm_initial_credits'],
1419
+ max_credits=ctx.obj['rfcomm_max_credits'],
1420
+ credits_threshold=ctx.obj['rfcomm_credits_threshold'],
1332
1421
  )
1333
1422
 
1334
1423
  raise ValueError('invalid mode')
@@ -1405,6 +1494,11 @@ def create_role_factory(ctx, default_role):
1405
1494
  '--extended-data-length',
1406
1495
  help='Request a data length upon connection, specified as tx_octets/tx_time',
1407
1496
  )
1497
+ @click.option(
1498
+ '--role-switch',
1499
+ type=click.Choice(['central', 'peripheral']),
1500
+ help='Request role switch upon connection (central or peripheral)',
1501
+ )
1408
1502
  @click.option(
1409
1503
  '--rfcomm-channel',
1410
1504
  type=int,
@@ -1427,9 +1521,19 @@ def create_role_factory(ctx, default_role):
1427
1521
  help='RFComm maximum frame size',
1428
1522
  )
1429
1523
  @click.option(
1430
- '--rfcomm-window-size',
1524
+ '--rfcomm-initial-credits',
1431
1525
  type=int,
1432
- help='RFComm window size',
1526
+ help='RFComm initial credits',
1527
+ )
1528
+ @click.option(
1529
+ '--rfcomm-max-credits',
1530
+ type=int,
1531
+ help='RFComm max credits',
1532
+ )
1533
+ @click.option(
1534
+ '--rfcomm-credits-threshold',
1535
+ type=int,
1536
+ help='RFComm credits threshold',
1433
1537
  )
1434
1538
  @click.option(
1435
1539
  '--l2cap-psm',
@@ -1459,7 +1563,7 @@ def create_role_factory(ctx, default_role):
1459
1563
  '--packet-size',
1460
1564
  '-s',
1461
1565
  metavar='SIZE',
1462
- type=click.IntRange(8, 4096),
1566
+ type=click.IntRange(8, 8192),
1463
1567
  default=500,
1464
1568
  help='Packet size (client or ping role)',
1465
1569
  )
@@ -1519,6 +1623,7 @@ def bench(
1519
1623
  mode,
1520
1624
  att_mtu,
1521
1625
  extended_data_length,
1626
+ role_switch,
1522
1627
  packet_size,
1523
1628
  packet_count,
1524
1629
  start_delay,
@@ -1530,7 +1635,9 @@ def bench(
1530
1635
  rfcomm_uuid,
1531
1636
  rfcomm_l2cap_mtu,
1532
1637
  rfcomm_max_frame_size,
1533
- rfcomm_window_size,
1638
+ rfcomm_initial_credits,
1639
+ rfcomm_max_credits,
1640
+ rfcomm_credits_threshold,
1534
1641
  l2cap_psm,
1535
1642
  l2cap_mtu,
1536
1643
  l2cap_mps,
@@ -1545,7 +1652,9 @@ def bench(
1545
1652
  ctx.obj['rfcomm_uuid'] = rfcomm_uuid
1546
1653
  ctx.obj['rfcomm_l2cap_mtu'] = rfcomm_l2cap_mtu
1547
1654
  ctx.obj['rfcomm_max_frame_size'] = rfcomm_max_frame_size
1548
- ctx.obj['rfcomm_window_size'] = rfcomm_window_size
1655
+ ctx.obj['rfcomm_initial_credits'] = rfcomm_initial_credits
1656
+ ctx.obj['rfcomm_max_credits'] = rfcomm_max_credits
1657
+ ctx.obj['rfcomm_credits_threshold'] = rfcomm_credits_threshold
1549
1658
  ctx.obj['l2cap_psm'] = l2cap_psm
1550
1659
  ctx.obj['l2cap_mtu'] = l2cap_mtu
1551
1660
  ctx.obj['l2cap_mps'] = l2cap_mps
@@ -1557,12 +1666,12 @@ def bench(
1557
1666
  ctx.obj['repeat_delay'] = repeat_delay
1558
1667
  ctx.obj['pace'] = pace
1559
1668
  ctx.obj['linger'] = linger
1560
-
1561
1669
  ctx.obj['extended_data_length'] = (
1562
1670
  [int(x) for x in extended_data_length.split('/')]
1563
1671
  if extended_data_length
1564
1672
  else None
1565
1673
  )
1674
+ ctx.obj['role_switch'] = role_switch
1566
1675
  ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
1567
1676
 
1568
1677
 
@@ -1606,6 +1715,7 @@ def central(
1606
1715
  authenticate,
1607
1716
  encrypt or authenticate,
1608
1717
  ctx.obj['extended_data_length'],
1718
+ ctx.obj['role_switch'],
1609
1719
  ).run()
1610
1720
 
1611
1721
  asyncio.run(run_central())
@@ -1622,10 +1732,11 @@ def peripheral(ctx, transport):
1622
1732
  async def run_peripheral():
1623
1733
  await Peripheral(
1624
1734
  transport,
1625
- ctx.obj['classic'],
1626
- ctx.obj['extended_data_length'],
1627
1735
  role_factory,
1628
1736
  mode_factory,
1737
+ ctx.obj['classic'],
1738
+ ctx.obj['extended_data_length'],
1739
+ ctx.obj['role_switch'],
1629
1740
  ).run()
1630
1741
 
1631
1742
  asyncio.run(run_peripheral())
@@ -27,7 +27,7 @@ from bumble.colors import color
27
27
  from bumble.core import name_or_number
28
28
  from bumble.hci import (
29
29
  map_null_terminated_utf8_string,
30
- LeFeatureMask,
30
+ LeFeature,
31
31
  HCI_SUCCESS,
32
32
  HCI_VERSION_NAMES,
33
33
  LMP_VERSION_NAMES,
@@ -140,7 +140,7 @@ async def get_le_info(host: Host) -> None:
140
140
 
141
141
  print(color('LE Features:', 'yellow'))
142
142
  for feature in host.supported_le_features:
143
- print(LeFeatureMask(feature).name)
143
+ print(f' {LeFeature(feature).name}')
144
144
 
145
145
 
146
146
  # -----------------------------------------------------------------------------
@@ -224,7 +224,7 @@ async def async_main(latency_probes, transport):
224
224
  print()
225
225
  print(color('Supported Commands:', 'yellow'))
226
226
  for command in host.supported_commands:
227
- print(' ', HCI_Command.command_name(command))
227
+ print(f' {HCI_Command.command_name(command)}')
228
228
 
229
229
 
230
230
  # -----------------------------------------------------------------------------