bumble 0.0.194__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/_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.194'
16
- __version_tuple__ = version_tuple = (0, 0, 194)
15
+ __version__ = version = '0.0.195'
16
+ __version_tuple__ = version_tuple = (0, 0, 195)
@@ -0,0 +1,407 @@
1
+ # Copyright 2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # -----------------------------------------------------------------------------
16
+ # Imports
17
+ # -----------------------------------------------------------------------------
18
+ from __future__ import annotations
19
+ import asyncio
20
+ import dataclasses
21
+ import logging
22
+ import os
23
+ from typing import cast, Dict, Optional, Tuple
24
+
25
+ import click
26
+ import pyee
27
+
28
+ from bumble.colors import color
29
+ import bumble.company_ids
30
+ import bumble.core
31
+ import bumble.device
32
+ import bumble.gatt
33
+ import bumble.hci
34
+ import bumble.profiles.bap
35
+ import bumble.profiles.pbp
36
+ import bumble.transport
37
+ import bumble.utils
38
+
39
+
40
+ # -----------------------------------------------------------------------------
41
+ # Logging
42
+ # -----------------------------------------------------------------------------
43
+ logger = logging.getLogger(__name__)
44
+
45
+
46
+ # -----------------------------------------------------------------------------
47
+ # Constants
48
+ # -----------------------------------------------------------------------------
49
+ AURACAST_DEFAULT_DEVICE_NAME = "Bumble Auracast"
50
+ AURACAST_DEFAULT_DEVICE_ADDRESS = bumble.hci.Address("F0:F1:F2:F3:F4:F5")
51
+
52
+
53
+ # -----------------------------------------------------------------------------
54
+ # Discover Broadcasts
55
+ # -----------------------------------------------------------------------------
56
+ class BroadcastDiscoverer:
57
+ @dataclasses.dataclass
58
+ class Broadcast(pyee.EventEmitter):
59
+ name: str
60
+ sync: bumble.device.PeriodicAdvertisingSync
61
+ rssi: int = 0
62
+ public_broadcast_announcement: Optional[
63
+ bumble.profiles.pbp.PublicBroadcastAnnouncement
64
+ ] = None
65
+ broadcast_audio_announcement: Optional[
66
+ bumble.profiles.bap.BroadcastAudioAnnouncement
67
+ ] = None
68
+ basic_audio_announcement: Optional[
69
+ bumble.profiles.bap.BasicAudioAnnouncement
70
+ ] = None
71
+ appearance: Optional[bumble.core.Appearance] = None
72
+ biginfo: Optional[bumble.device.BIGInfoAdvertisement] = None
73
+ manufacturer_data: Optional[Tuple[str, bytes]] = None
74
+
75
+ def __post_init__(self) -> None:
76
+ super().__init__()
77
+ self.sync.on('establishment', self.on_sync_establishment)
78
+ self.sync.on('loss', self.on_sync_loss)
79
+ self.sync.on('periodic_advertisement', self.on_periodic_advertisement)
80
+ self.sync.on('biginfo_advertisement', self.on_biginfo_advertisement)
81
+
82
+ self.establishment_timeout_task = asyncio.create_task(
83
+ self.wait_for_establishment()
84
+ )
85
+
86
+ async def wait_for_establishment(self) -> None:
87
+ await asyncio.sleep(5.0)
88
+ if self.sync.state == bumble.device.PeriodicAdvertisingSync.State.PENDING:
89
+ print(
90
+ color(
91
+ '!!! Periodic advertisement sync not established in time, '
92
+ 'canceling',
93
+ 'red',
94
+ )
95
+ )
96
+ await self.sync.terminate()
97
+
98
+ def update(self, advertisement: bumble.device.Advertisement) -> None:
99
+ self.rssi = advertisement.rssi
100
+ for service_data in advertisement.data.get_all(
101
+ bumble.core.AdvertisingData.SERVICE_DATA
102
+ ):
103
+ assert isinstance(service_data, tuple)
104
+ service_uuid, data = service_data
105
+ assert isinstance(data, bytes)
106
+
107
+ if (
108
+ service_uuid
109
+ == bumble.gatt.GATT_PUBLIC_BROADCAST_ANNOUNCEMENT_SERVICE
110
+ ):
111
+ self.public_broadcast_announcement = (
112
+ bumble.profiles.pbp.PublicBroadcastAnnouncement.from_bytes(data)
113
+ )
114
+ continue
115
+
116
+ if (
117
+ service_uuid
118
+ == bumble.gatt.GATT_BROADCAST_AUDIO_ANNOUNCEMENT_SERVICE
119
+ ):
120
+ self.broadcast_audio_announcement = (
121
+ bumble.profiles.bap.BroadcastAudioAnnouncement.from_bytes(data)
122
+ )
123
+ continue
124
+
125
+ self.appearance = advertisement.data.get( # type: ignore[assignment]
126
+ bumble.core.AdvertisingData.APPEARANCE
127
+ )
128
+
129
+ if manufacturer_data := advertisement.data.get(
130
+ bumble.core.AdvertisingData.MANUFACTURER_SPECIFIC_DATA
131
+ ):
132
+ assert isinstance(manufacturer_data, tuple)
133
+ company_id = cast(int, manufacturer_data[0])
134
+ data = cast(bytes, manufacturer_data[1])
135
+ self.manufacturer_data = (
136
+ bumble.company_ids.COMPANY_IDENTIFIERS.get(
137
+ company_id, f'0x{company_id:04X}'
138
+ ),
139
+ data,
140
+ )
141
+
142
+ def print(self) -> None:
143
+ print(
144
+ color('Broadcast:', 'yellow'),
145
+ self.sync.advertiser_address,
146
+ color(self.sync.state.name, 'green'),
147
+ )
148
+ print(f' {color("Name", "cyan")}: {self.name}')
149
+ if self.appearance:
150
+ print(f' {color("Appearance", "cyan")}: {str(self.appearance)}')
151
+ print(f' {color("RSSI", "cyan")}: {self.rssi}')
152
+ print(f' {color("SID", "cyan")}: {self.sync.sid}')
153
+
154
+ if self.manufacturer_data:
155
+ print(
156
+ f' {color("Manufacturer Data", "cyan")}: '
157
+ f'{self.manufacturer_data[0]} -> {self.manufacturer_data[1].hex()}'
158
+ )
159
+
160
+ if self.broadcast_audio_announcement:
161
+ print(
162
+ f' {color("Broadcast ID", "cyan")}: '
163
+ f'{self.broadcast_audio_announcement.broadcast_id}'
164
+ )
165
+
166
+ if self.public_broadcast_announcement:
167
+ print(
168
+ f' {color("Features", "cyan")}: '
169
+ f'{self.public_broadcast_announcement.features}'
170
+ )
171
+ print(
172
+ f' {color("Metadata", "cyan")}: '
173
+ f'{self.public_broadcast_announcement.metadata}'
174
+ )
175
+
176
+ if self.basic_audio_announcement:
177
+ print(color(' Audio:', 'cyan'))
178
+ print(
179
+ color(' Presentation Delay:', 'magenta'),
180
+ self.basic_audio_announcement.presentation_delay,
181
+ )
182
+ for subgroup in self.basic_audio_announcement.subgroups:
183
+ print(color(' Subgroup:', 'magenta'))
184
+ print(color(' Codec ID:', 'yellow'))
185
+ print(
186
+ color(' Coding Format: ', 'green'),
187
+ subgroup.codec_id.coding_format.name,
188
+ )
189
+ print(
190
+ color(' Company ID: ', 'green'),
191
+ subgroup.codec_id.company_id,
192
+ )
193
+ print(
194
+ color(' Vendor Specific Codec ID:', 'green'),
195
+ subgroup.codec_id.vendor_specific_codec_id,
196
+ )
197
+ print(
198
+ color(' Codec Config:', 'yellow'),
199
+ subgroup.codec_specific_configuration,
200
+ )
201
+ print(color(' Metadata: ', 'yellow'), subgroup.metadata)
202
+
203
+ for bis in subgroup.bis:
204
+ print(color(f' BIS [{bis.index}]:', 'yellow'))
205
+ print(
206
+ color(' Codec Config:', 'green'),
207
+ bis.codec_specific_configuration,
208
+ )
209
+
210
+ if self.biginfo:
211
+ print(color(' BIG:', 'cyan'))
212
+ print(
213
+ color(' Number of BIS:', 'magenta'),
214
+ self.biginfo.num_bis,
215
+ )
216
+ print(
217
+ color(' PHY: ', 'magenta'),
218
+ self.biginfo.phy.name,
219
+ )
220
+ print(
221
+ color(' Framed: ', 'magenta'),
222
+ self.biginfo.framed,
223
+ )
224
+ print(
225
+ color(' Encrypted: ', 'magenta'),
226
+ self.biginfo.encrypted,
227
+ )
228
+
229
+ def on_sync_establishment(self) -> None:
230
+ self.establishment_timeout_task.cancel()
231
+ self.emit('change')
232
+
233
+ def on_sync_loss(self) -> None:
234
+ self.basic_audio_announcement = None
235
+ self.biginfo = None
236
+ self.emit('change')
237
+
238
+ def on_periodic_advertisement(
239
+ self, advertisement: bumble.device.PeriodicAdvertisement
240
+ ) -> None:
241
+ if advertisement.data is None:
242
+ return
243
+
244
+ for service_data in advertisement.data.get_all(
245
+ bumble.core.AdvertisingData.SERVICE_DATA
246
+ ):
247
+ assert isinstance(service_data, tuple)
248
+ service_uuid, data = service_data
249
+ assert isinstance(data, bytes)
250
+
251
+ if service_uuid == bumble.gatt.GATT_BASIC_AUDIO_ANNOUNCEMENT_SERVICE:
252
+ self.basic_audio_announcement = (
253
+ bumble.profiles.bap.BasicAudioAnnouncement.from_bytes(data)
254
+ )
255
+ break
256
+
257
+ self.emit('change')
258
+
259
+ def on_biginfo_advertisement(
260
+ self, advertisement: bumble.device.BIGInfoAdvertisement
261
+ ) -> None:
262
+ self.biginfo = advertisement
263
+ self.emit('change')
264
+
265
+ def __init__(
266
+ self,
267
+ device: bumble.device.Device,
268
+ filter_duplicates: bool,
269
+ sync_timeout: float,
270
+ ):
271
+ self.device = device
272
+ self.filter_duplicates = filter_duplicates
273
+ self.sync_timeout = sync_timeout
274
+ self.broadcasts: Dict[bumble.hci.Address, BroadcastDiscoverer.Broadcast] = {}
275
+ self.status_message = ''
276
+ device.on('advertisement', self.on_advertisement)
277
+
278
+ async def run(self) -> None:
279
+ self.status_message = color('Scanning...', 'green')
280
+ await self.device.start_scanning(
281
+ active=False,
282
+ filter_duplicates=False,
283
+ )
284
+
285
+ def refresh(self) -> None:
286
+ # Clear the screen from the top
287
+ print('\033[H')
288
+ print('\033[0J')
289
+ print('\033[H')
290
+
291
+ # Print the status message
292
+ print(self.status_message)
293
+ print("==========================================")
294
+
295
+ # Print all broadcasts
296
+ for broadcast in self.broadcasts.values():
297
+ broadcast.print()
298
+ print('------------------------------------------')
299
+
300
+ # Clear the screen to the bottom
301
+ print('\033[0J')
302
+
303
+ def on_advertisement(self, advertisement: bumble.device.Advertisement) -> None:
304
+ if (
305
+ broadcast_name := advertisement.data.get(
306
+ bumble.core.AdvertisingData.BROADCAST_NAME
307
+ )
308
+ ) is None:
309
+ return
310
+ assert isinstance(broadcast_name, str)
311
+
312
+ if broadcast := self.broadcasts.get(advertisement.address):
313
+ broadcast.update(advertisement)
314
+ self.refresh()
315
+ return
316
+
317
+ bumble.utils.AsyncRunner.spawn(
318
+ self.on_new_broadcast(broadcast_name, advertisement)
319
+ )
320
+
321
+ async def on_new_broadcast(
322
+ self, name: str, advertisement: bumble.device.Advertisement
323
+ ) -> None:
324
+ periodic_advertising_sync = await self.device.create_periodic_advertising_sync(
325
+ advertiser_address=advertisement.address,
326
+ sid=advertisement.sid,
327
+ sync_timeout=self.sync_timeout,
328
+ filter_duplicates=self.filter_duplicates,
329
+ )
330
+ broadcast = self.Broadcast(
331
+ name,
332
+ periodic_advertising_sync,
333
+ )
334
+ broadcast.on('change', self.refresh)
335
+ broadcast.update(advertisement)
336
+ self.broadcasts[advertisement.address] = broadcast
337
+ periodic_advertising_sync.on('loss', lambda: self.on_broadcast_loss(broadcast))
338
+ self.status_message = color(
339
+ f'+Found {len(self.broadcasts)} broadcasts', 'green'
340
+ )
341
+ self.refresh()
342
+
343
+ def on_broadcast_loss(self, broadcast: Broadcast) -> None:
344
+ del self.broadcasts[broadcast.sync.advertiser_address]
345
+ bumble.utils.AsyncRunner.spawn(broadcast.sync.terminate())
346
+ self.status_message = color(
347
+ f'-Found {len(self.broadcasts)} broadcasts', 'green'
348
+ )
349
+ self.refresh()
350
+
351
+
352
+ async def run_discover_broadcasts(
353
+ filter_duplicates: bool, sync_timeout: float, transport: str
354
+ ) -> None:
355
+ async with await bumble.transport.open_transport(transport) as (
356
+ hci_source,
357
+ hci_sink,
358
+ ):
359
+ device = bumble.device.Device.with_hci(
360
+ AURACAST_DEFAULT_DEVICE_NAME,
361
+ AURACAST_DEFAULT_DEVICE_ADDRESS,
362
+ hci_source,
363
+ hci_sink,
364
+ )
365
+ await device.power_on()
366
+ discoverer = BroadcastDiscoverer(device, filter_duplicates, sync_timeout)
367
+ await discoverer.run()
368
+ await hci_source.terminated
369
+
370
+
371
+ # -----------------------------------------------------------------------------
372
+ # Main
373
+ # -----------------------------------------------------------------------------
374
+ @click.group()
375
+ @click.pass_context
376
+ def auracast(
377
+ ctx,
378
+ ):
379
+ ctx.ensure_object(dict)
380
+
381
+
382
+ @auracast.command('discover-broadcasts')
383
+ @click.option(
384
+ '--filter-duplicates', is_flag=True, default=False, help='Filter duplicates'
385
+ )
386
+ @click.option(
387
+ '--sync-timeout',
388
+ metavar='SYNC_TIMEOUT',
389
+ type=float,
390
+ default=5.0,
391
+ help='Sync timeout (in seconds)',
392
+ )
393
+ @click.argument('transport')
394
+ @click.pass_context
395
+ def discover_broadcasts(ctx, filter_duplicates, sync_timeout, transport):
396
+ """Discover public broadcasts"""
397
+ asyncio.run(run_discover_broadcasts(filter_duplicates, sync_timeout, transport))
398
+
399
+
400
+ def main():
401
+ logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
402
+ auracast()
403
+
404
+
405
+ # -----------------------------------------------------------------------------
406
+ if __name__ == "__main__":
407
+ main() # pylint: disable=no-value-for-parameter
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
 
151
- else:
152
- phy_state = ''
153
- data_length = ''
154
- connection_parameters = ''
156
+ params.append(f'MTU={connection.att_mtu}')
155
157
 
156
- mtu = connection.att_mtu
158
+ else:
159
+ params.append(f'Role={HCI_Constant.role_name(connection.role)}')
157
160
 
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
@@ -1034,6 +1042,10 @@ class RfcommServer(StreamedPacketIO):
1034
1042
 
1035
1043
  def on_dlc(self, dlc):
1036
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
1037
1049
  dlc.sink = self.on_packet
1038
1050
  self.io_sink = dlc.write
1039
1051
  self.dlc = dlc
@@ -1063,6 +1075,7 @@ class Central(Connection.Listener):
1063
1075
  authenticate,
1064
1076
  encrypt,
1065
1077
  extended_data_length,
1078
+ role_switch,
1066
1079
  ):
1067
1080
  super().__init__()
1068
1081
  self.transport = transport
@@ -1073,6 +1086,7 @@ class Central(Connection.Listener):
1073
1086
  self.authenticate = authenticate
1074
1087
  self.encrypt = encrypt or authenticate
1075
1088
  self.extended_data_length = extended_data_length
1089
+ self.role_switch = role_switch
1076
1090
  self.device = None
1077
1091
  self.connection = None
1078
1092
 
@@ -1123,6 +1137,11 @@ class Central(Connection.Listener):
1123
1137
  role = self.role_factory(mode)
1124
1138
  self.device.classic_enabled = self.classic
1125
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
+
1126
1145
  await self.device.power_on()
1127
1146
 
1128
1147
  if self.classic:
@@ -1151,6 +1170,10 @@ class Central(Connection.Listener):
1151
1170
  self.connection.listener = self
1152
1171
  print_connection(self.connection)
1153
1172
 
1173
+ # Switch roles if needed.
1174
+ if self.role_switch:
1175
+ await switch_roles(self.connection, self.role_switch)
1176
+
1154
1177
  # Wait a bit after the connection, some controllers aren't very good when
1155
1178
  # we start sending data right away while some connection parameters are
1156
1179
  # updated post connection
@@ -1212,20 +1235,30 @@ class Central(Connection.Listener):
1212
1235
  def on_connection_data_length_change(self):
1213
1236
  print_connection(self.connection)
1214
1237
 
1238
+ def on_role_change(self):
1239
+ print_connection(self.connection)
1240
+
1215
1241
 
1216
1242
  # -----------------------------------------------------------------------------
1217
1243
  # Peripheral
1218
1244
  # -----------------------------------------------------------------------------
1219
1245
  class Peripheral(Device.Listener, Connection.Listener):
1220
1246
  def __init__(
1221
- 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,
1222
1254
  ):
1223
1255
  self.transport = transport
1224
1256
  self.classic = classic
1225
- self.extended_data_length = extended_data_length
1226
1257
  self.role_factory = role_factory
1227
- self.role = None
1228
1258
  self.mode_factory = mode_factory
1259
+ self.extended_data_length = extended_data_length
1260
+ self.role_switch = role_switch
1261
+ self.role = None
1229
1262
  self.mode = None
1230
1263
  self.device = None
1231
1264
  self.connection = None
@@ -1248,6 +1281,11 @@ class Peripheral(Device.Listener, Connection.Listener):
1248
1281
  self.role = self.role_factory(self.mode)
1249
1282
  self.device.classic_enabled = self.classic
1250
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
+
1251
1289
  await self.device.power_on()
1252
1290
 
1253
1291
  if self.classic:
@@ -1274,6 +1312,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1274
1312
 
1275
1313
  await self.connected.wait()
1276
1314
  logging.info(color('### Connected', 'cyan'))
1315
+ print_connection(self.connection)
1277
1316
 
1278
1317
  await self.mode.on_connection(self.connection)
1279
1318
  await self.role.run()
@@ -1290,7 +1329,7 @@ class Peripheral(Device.Listener, Connection.Listener):
1290
1329
  AsyncRunner.spawn(self.device.set_connectable(False))
1291
1330
 
1292
1331
  # Request a new data length if needed
1293
- if self.extended_data_length:
1332
+ if not self.classic and self.extended_data_length:
1294
1333
  logging.info("+++ Requesting extended data length")
1295
1334
  AsyncRunner.spawn(
1296
1335
  connection.set_data_length(
@@ -1298,6 +1337,10 @@ class Peripheral(Device.Listener, Connection.Listener):
1298
1337
  )
1299
1338
  )
1300
1339
 
1340
+ # Switch roles if needed.
1341
+ if self.role_switch:
1342
+ AsyncRunner.spawn(switch_roles(connection, self.role_switch))
1343
+
1301
1344
  def on_disconnection(self, reason):
1302
1345
  logging.info(color(f'!!! Disconnection: reason={reason}', 'red'))
1303
1346
  self.connection = None
@@ -1319,6 +1362,9 @@ class Peripheral(Device.Listener, Connection.Listener):
1319
1362
  def on_connection_data_length_change(self):
1320
1363
  print_connection(self.connection)
1321
1364
 
1365
+ def on_role_change(self):
1366
+ print_connection(self.connection)
1367
+
1322
1368
 
1323
1369
  # -----------------------------------------------------------------------------
1324
1370
  def create_mode_factory(ctx, default_mode):
@@ -1448,6 +1494,11 @@ def create_role_factory(ctx, default_role):
1448
1494
  '--extended-data-length',
1449
1495
  help='Request a data length upon connection, specified as tx_octets/tx_time',
1450
1496
  )
1497
+ @click.option(
1498
+ '--role-switch',
1499
+ type=click.Choice(['central', 'peripheral']),
1500
+ help='Request role switch upon connection (central or peripheral)',
1501
+ )
1451
1502
  @click.option(
1452
1503
  '--rfcomm-channel',
1453
1504
  type=int,
@@ -1512,7 +1563,7 @@ def create_role_factory(ctx, default_role):
1512
1563
  '--packet-size',
1513
1564
  '-s',
1514
1565
  metavar='SIZE',
1515
- type=click.IntRange(8, 4096),
1566
+ type=click.IntRange(8, 8192),
1516
1567
  default=500,
1517
1568
  help='Packet size (client or ping role)',
1518
1569
  )
@@ -1572,6 +1623,7 @@ def bench(
1572
1623
  mode,
1573
1624
  att_mtu,
1574
1625
  extended_data_length,
1626
+ role_switch,
1575
1627
  packet_size,
1576
1628
  packet_count,
1577
1629
  start_delay,
@@ -1614,12 +1666,12 @@ def bench(
1614
1666
  ctx.obj['repeat_delay'] = repeat_delay
1615
1667
  ctx.obj['pace'] = pace
1616
1668
  ctx.obj['linger'] = linger
1617
-
1618
1669
  ctx.obj['extended_data_length'] = (
1619
1670
  [int(x) for x in extended_data_length.split('/')]
1620
1671
  if extended_data_length
1621
1672
  else None
1622
1673
  )
1674
+ ctx.obj['role_switch'] = role_switch
1623
1675
  ctx.obj['classic'] = mode in ('rfcomm-client', 'rfcomm-server')
1624
1676
 
1625
1677
 
@@ -1663,6 +1715,7 @@ def central(
1663
1715
  authenticate,
1664
1716
  encrypt or authenticate,
1665
1717
  ctx.obj['extended_data_length'],
1718
+ ctx.obj['role_switch'],
1666
1719
  ).run()
1667
1720
 
1668
1721
  asyncio.run(run_central())
@@ -1679,10 +1732,11 @@ def peripheral(ctx, transport):
1679
1732
  async def run_peripheral():
1680
1733
  await Peripheral(
1681
1734
  transport,
1682
- ctx.obj['classic'],
1683
- ctx.obj['extended_data_length'],
1684
1735
  role_factory,
1685
1736
  mode_factory,
1737
+ ctx.obj['classic'],
1738
+ ctx.obj['extended_data_length'],
1739
+ ctx.obj['role_switch'],
1686
1740
  ).run()
1687
1741
 
1688
1742
  asyncio.run(run_peripheral())