bumble 0.0.193__py3-none-any.whl → 0.0.194__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.193'
16
- __version_tuple__ = version_tuple = (0, 0, 193)
15
+ __version__ = version = '0.0.194'
16
+ __version_tuple__ = version_tuple = (0, 0, 194)
bumble/apps/bench.py CHANGED
@@ -899,14 +899,26 @@ class L2capServer(StreamedPacketIO):
899
899
  # RfcommClient
900
900
  # -----------------------------------------------------------------------------
901
901
  class RfcommClient(StreamedPacketIO):
902
- def __init__(self, device, channel, uuid, l2cap_mtu, max_frame_size, window_size):
902
+ def __init__(
903
+ self,
904
+ device,
905
+ channel,
906
+ uuid,
907
+ l2cap_mtu,
908
+ max_frame_size,
909
+ initial_credits,
910
+ max_credits,
911
+ credits_threshold,
912
+ ):
903
913
  super().__init__()
904
914
  self.device = device
905
915
  self.channel = channel
906
916
  self.uuid = uuid
907
917
  self.l2cap_mtu = l2cap_mtu
908
918
  self.max_frame_size = max_frame_size
909
- self.window_size = window_size
919
+ self.initial_credits = initial_credits
920
+ self.max_credits = max_credits
921
+ self.credits_threshold = credits_threshold
910
922
  self.rfcomm_session = None
911
923
  self.ready = asyncio.Event()
912
924
 
@@ -940,12 +952,17 @@ class RfcommClient(StreamedPacketIO):
940
952
  logging.info(color(f'### Opening session for channel {channel}...', 'yellow'))
941
953
  try:
942
954
  dlc_options = {}
943
- if self.max_frame_size:
955
+ if self.max_frame_size is not None:
944
956
  dlc_options['max_frame_size'] = self.max_frame_size
945
- if self.window_size:
946
- dlc_options['window_size'] = self.window_size
957
+ if self.initial_credits is not None:
958
+ dlc_options['initial_credits'] = self.initial_credits
947
959
  rfcomm_session = await rfcomm_mux.open_dlc(channel, **dlc_options)
948
960
  logging.info(color(f'### Session open: {rfcomm_session}', 'yellow'))
961
+ if self.max_credits is not None:
962
+ rfcomm_session.rx_max_credits = self.max_credits
963
+ if self.credits_threshold is not None:
964
+ rfcomm_session.rx_credits_threshold = self.credits_threshold
965
+
949
966
  except bumble.core.ConnectionError as error:
950
967
  logging.info(color(f'!!! Session open failed: {error}', 'red'))
951
968
  await rfcomm_mux.disconnect()
@@ -969,8 +986,19 @@ class RfcommClient(StreamedPacketIO):
969
986
  # RfcommServer
970
987
  # -----------------------------------------------------------------------------
971
988
  class RfcommServer(StreamedPacketIO):
972
- def __init__(self, device, channel, l2cap_mtu):
989
+ def __init__(
990
+ self,
991
+ device,
992
+ channel,
993
+ l2cap_mtu,
994
+ max_frame_size,
995
+ initial_credits,
996
+ max_credits,
997
+ credits_threshold,
998
+ ):
973
999
  super().__init__()
1000
+ self.max_credits = max_credits
1001
+ self.credits_threshold = credits_threshold
974
1002
  self.dlc = None
975
1003
  self.ready = asyncio.Event()
976
1004
 
@@ -981,7 +1009,12 @@ class RfcommServer(StreamedPacketIO):
981
1009
  rfcomm_server = bumble.rfcomm.Server(device, **server_options)
982
1010
 
983
1011
  # Listen for incoming DLC connections
984
- channel_number = rfcomm_server.listen(self.on_dlc, channel)
1012
+ dlc_options = {}
1013
+ if max_frame_size is not None:
1014
+ dlc_options['max_frame_size'] = max_frame_size
1015
+ if initial_credits is not None:
1016
+ dlc_options['initial_credits'] = initial_credits
1017
+ channel_number = rfcomm_server.listen(self.on_dlc, channel, **dlc_options)
985
1018
 
986
1019
  # Setup the SDP to advertise this channel
987
1020
  device.sdp_service_records = make_sdp_records(channel_number)
@@ -1004,6 +1037,10 @@ class RfcommServer(StreamedPacketIO):
1004
1037
  dlc.sink = self.on_packet
1005
1038
  self.io_sink = dlc.write
1006
1039
  self.dlc = dlc
1040
+ if self.max_credits is not None:
1041
+ dlc.rx_max_credits = self.max_credits
1042
+ if self.credits_threshold is not None:
1043
+ dlc.rx_credits_threshold = self.credits_threshold
1007
1044
 
1008
1045
  async def drain(self):
1009
1046
  assert self.dlc
@@ -1321,7 +1358,9 @@ def create_mode_factory(ctx, default_mode):
1321
1358
  uuid=ctx.obj['rfcomm_uuid'],
1322
1359
  l2cap_mtu=ctx.obj['rfcomm_l2cap_mtu'],
1323
1360
  max_frame_size=ctx.obj['rfcomm_max_frame_size'],
1324
- window_size=ctx.obj['rfcomm_window_size'],
1361
+ initial_credits=ctx.obj['rfcomm_initial_credits'],
1362
+ max_credits=ctx.obj['rfcomm_max_credits'],
1363
+ credits_threshold=ctx.obj['rfcomm_credits_threshold'],
1325
1364
  )
1326
1365
 
1327
1366
  if mode == 'rfcomm-server':
@@ -1329,6 +1368,10 @@ def create_mode_factory(ctx, default_mode):
1329
1368
  device,
1330
1369
  channel=ctx.obj['rfcomm_channel'],
1331
1370
  l2cap_mtu=ctx.obj['rfcomm_l2cap_mtu'],
1371
+ max_frame_size=ctx.obj['rfcomm_max_frame_size'],
1372
+ initial_credits=ctx.obj['rfcomm_initial_credits'],
1373
+ max_credits=ctx.obj['rfcomm_max_credits'],
1374
+ credits_threshold=ctx.obj['rfcomm_credits_threshold'],
1332
1375
  )
1333
1376
 
1334
1377
  raise ValueError('invalid mode')
@@ -1427,9 +1470,19 @@ def create_role_factory(ctx, default_role):
1427
1470
  help='RFComm maximum frame size',
1428
1471
  )
1429
1472
  @click.option(
1430
- '--rfcomm-window-size',
1473
+ '--rfcomm-initial-credits',
1474
+ type=int,
1475
+ help='RFComm initial credits',
1476
+ )
1477
+ @click.option(
1478
+ '--rfcomm-max-credits',
1479
+ type=int,
1480
+ help='RFComm max credits',
1481
+ )
1482
+ @click.option(
1483
+ '--rfcomm-credits-threshold',
1431
1484
  type=int,
1432
- help='RFComm window size',
1485
+ help='RFComm credits threshold',
1433
1486
  )
1434
1487
  @click.option(
1435
1488
  '--l2cap-psm',
@@ -1530,7 +1583,9 @@ def bench(
1530
1583
  rfcomm_uuid,
1531
1584
  rfcomm_l2cap_mtu,
1532
1585
  rfcomm_max_frame_size,
1533
- rfcomm_window_size,
1586
+ rfcomm_initial_credits,
1587
+ rfcomm_max_credits,
1588
+ rfcomm_credits_threshold,
1534
1589
  l2cap_psm,
1535
1590
  l2cap_mtu,
1536
1591
  l2cap_mps,
@@ -1545,7 +1600,9 @@ def bench(
1545
1600
  ctx.obj['rfcomm_uuid'] = rfcomm_uuid
1546
1601
  ctx.obj['rfcomm_l2cap_mtu'] = rfcomm_l2cap_mtu
1547
1602
  ctx.obj['rfcomm_max_frame_size'] = rfcomm_max_frame_size
1548
- ctx.obj['rfcomm_window_size'] = rfcomm_window_size
1603
+ ctx.obj['rfcomm_initial_credits'] = rfcomm_initial_credits
1604
+ ctx.obj['rfcomm_max_credits'] = rfcomm_max_credits
1605
+ ctx.obj['rfcomm_credits_threshold'] = rfcomm_credits_threshold
1549
1606
  ctx.obj['l2cap_psm'] = l2cap_psm
1550
1607
  ctx.obj['l2cap_mtu'] = l2cap_mtu
1551
1608
  ctx.obj['l2cap_mps'] = l2cap_mps
@@ -0,0 +1,511 @@
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
+ import asyncio
19
+ import logging
20
+ import os
21
+ import time
22
+ from typing import Optional
23
+
24
+ import click
25
+
26
+ from bumble.colors import color
27
+ from bumble.device import Device, DeviceConfiguration, Connection
28
+ from bumble import core
29
+ from bumble import hci
30
+ from bumble import rfcomm
31
+ from bumble import transport
32
+ from bumble import utils
33
+
34
+
35
+ # -----------------------------------------------------------------------------
36
+ # Constants
37
+ # -----------------------------------------------------------------------------
38
+ DEFAULT_RFCOMM_UUID = "E6D55659-C8B4-4B85-96BB-B1143AF6D3AE"
39
+ DEFAULT_MTU = 4096
40
+ DEFAULT_CLIENT_TCP_PORT = 9544
41
+ DEFAULT_SERVER_TCP_PORT = 9545
42
+
43
+ TRACE_MAX_SIZE = 48
44
+
45
+
46
+ # -----------------------------------------------------------------------------
47
+ class Tracer:
48
+ """
49
+ Trace data buffers transmitted from one endpoint to another, with stats.
50
+ """
51
+
52
+ def __init__(self, channel_name: str) -> None:
53
+ self.channel_name = channel_name
54
+ self.last_ts: float = 0.0
55
+
56
+ def trace_data(self, data: bytes) -> None:
57
+ now = time.time()
58
+ elapsed_s = now - self.last_ts if self.last_ts else 0
59
+ elapsed_ms = int(elapsed_s * 1000)
60
+ instant_throughput_kbps = ((len(data) / elapsed_s) / 1000) if elapsed_s else 0.0
61
+
62
+ hex_str = data[:TRACE_MAX_SIZE].hex() + (
63
+ "..." if len(data) > TRACE_MAX_SIZE else ""
64
+ )
65
+ print(
66
+ f"[{self.channel_name}] {len(data):4} bytes "
67
+ f"(+{elapsed_ms:4}ms, {instant_throughput_kbps: 7.2f}kB/s) "
68
+ f" {hex_str}"
69
+ )
70
+
71
+ self.last_ts = now
72
+
73
+
74
+ # -----------------------------------------------------------------------------
75
+ class ServerBridge:
76
+ """
77
+ RFCOMM server bridge: waits for a peer to connect an RFCOMM channel.
78
+ The RFCOMM channel may be associated with a UUID published in an SDP service
79
+ description, or simply be on a system-assigned channel number.
80
+ When the connection is made, the bridge connects a TCP socket to a remote host and
81
+ bridges the data in both directions, with flow control.
82
+ When the RFCOMM channel is closed, the bridge disconnects the TCP socket
83
+ and waits for a new channel to be connected.
84
+ """
85
+
86
+ READ_CHUNK_SIZE = 4096
87
+
88
+ def __init__(
89
+ self, channel: int, uuid: str, trace: bool, tcp_host: str, tcp_port: int
90
+ ) -> None:
91
+ self.device: Optional[Device] = None
92
+ self.channel = channel
93
+ self.uuid = uuid
94
+ self.tcp_host = tcp_host
95
+ self.tcp_port = tcp_port
96
+ self.rfcomm_channel: Optional[rfcomm.DLC] = None
97
+ self.tcp_tracer: Optional[Tracer]
98
+ self.rfcomm_tracer: Optional[Tracer]
99
+
100
+ if trace:
101
+ self.tcp_tracer = Tracer(color("RFCOMM->TCP", "cyan"))
102
+ self.rfcomm_tracer = Tracer(color("TCP->RFCOMM", "magenta"))
103
+ else:
104
+ self.rfcomm_tracer = None
105
+ self.tcp_tracer = None
106
+
107
+ async def start(self, device: Device) -> None:
108
+ self.device = device
109
+
110
+ # Create and register a server
111
+ rfcomm_server = rfcomm.Server(self.device)
112
+
113
+ # Listen for incoming DLC connections
114
+ self.channel = rfcomm_server.listen(self.on_rfcomm_channel, self.channel)
115
+
116
+ # Setup the SDP to advertise this channel
117
+ service_record_handle = 0x00010001
118
+ self.device.sdp_service_records = {
119
+ service_record_handle: rfcomm.make_service_sdp_records(
120
+ service_record_handle, self.channel, core.UUID(self.uuid)
121
+ )
122
+ }
123
+
124
+ # We're ready for a connection
125
+ self.device.on("connection", self.on_connection)
126
+ await self.set_available(True)
127
+
128
+ print(
129
+ color(
130
+ (
131
+ f"### Listening for RFCOMM connection on {device.public_address}, "
132
+ f"channel {self.channel}"
133
+ ),
134
+ "yellow",
135
+ )
136
+ )
137
+
138
+ async def set_available(self, available: bool):
139
+ # Become discoverable and connectable
140
+ assert self.device
141
+ await self.device.set_connectable(available)
142
+ await self.device.set_discoverable(available)
143
+
144
+ def on_connection(self, connection):
145
+ print(color(f"@@@ Bluetooth connection: {connection}", "blue"))
146
+ connection.on("disconnection", self.on_disconnection)
147
+
148
+ # Don't accept new connections until we're disconnected
149
+ utils.AsyncRunner.spawn(self.set_available(False))
150
+
151
+ def on_disconnection(self, reason: int):
152
+ print(
153
+ color("@@@ Bluetooth disconnection:", "red"),
154
+ hci.HCI_Constant.error_name(reason),
155
+ )
156
+
157
+ # We're ready for a new connection
158
+ utils.AsyncRunner.spawn(self.set_available(True))
159
+
160
+ # Called when an RFCOMM channel is established
161
+ @utils.AsyncRunner.run_in_task()
162
+ async def on_rfcomm_channel(self, rfcomm_channel):
163
+ print(color("*** RFCOMM channel:", "cyan"), rfcomm_channel)
164
+
165
+ # Connect to the TCP server
166
+ print(
167
+ color(
168
+ f"### Connecting to TCP {self.tcp_host}:{self.tcp_port}",
169
+ "yellow",
170
+ )
171
+ )
172
+ try:
173
+ reader, writer = await asyncio.open_connection(self.tcp_host, self.tcp_port)
174
+ except OSError:
175
+ print(color("!!! Connection failed", "red"))
176
+ await rfcomm_channel.disconnect()
177
+ return
178
+
179
+ # Pipe data from RFCOMM to TCP
180
+ def on_rfcomm_channel_closed():
181
+ print(color("*** RFCOMM channel closed", "cyan"))
182
+ writer.close()
183
+
184
+ def write_rfcomm_data(data):
185
+ if self.rfcomm_tracer:
186
+ self.rfcomm_tracer.trace_data(data)
187
+
188
+ writer.write(data)
189
+
190
+ rfcomm_channel.sink = write_rfcomm_data
191
+ rfcomm_channel.on("close", on_rfcomm_channel_closed)
192
+
193
+ # Pipe data from TCP to RFCOMM
194
+ while True:
195
+ try:
196
+ data = await reader.read(self.READ_CHUNK_SIZE)
197
+
198
+ if len(data) == 0:
199
+ print(color("### TCP end of stream", "yellow"))
200
+ if rfcomm_channel.state == rfcomm.DLC.State.CONNECTED:
201
+ await rfcomm_channel.disconnect()
202
+ return
203
+
204
+ if self.tcp_tracer:
205
+ self.tcp_tracer.trace_data(data)
206
+
207
+ rfcomm_channel.write(data)
208
+ await rfcomm_channel.drain()
209
+ except Exception as error:
210
+ print(f"!!! Exception: {error}")
211
+ break
212
+
213
+ writer.close()
214
+ await writer.wait_closed()
215
+ print(color("~~~ Bye bye", "magenta"))
216
+
217
+
218
+ # -----------------------------------------------------------------------------
219
+ class ClientBridge:
220
+ """
221
+ RFCOMM client bridge: connects to a BR/EDR device, then waits for an inbound
222
+ TCP connection on a specified port number. When a TCP client connects, an
223
+ RFCOMM connection to the device is established, and the data is bridged in both
224
+ directions, with flow control.
225
+ When the TCP connection is closed by the client, the RFCOMM channel is
226
+ disconnected, but the connection to the device remains, ready for a new TCP client
227
+ to connect.
228
+ """
229
+
230
+ READ_CHUNK_SIZE = 4096
231
+
232
+ def __init__(
233
+ self,
234
+ channel: int,
235
+ uuid: str,
236
+ trace: bool,
237
+ address: str,
238
+ tcp_host: str,
239
+ tcp_port: int,
240
+ encrypt: bool,
241
+ ):
242
+ self.channel = channel
243
+ self.uuid = uuid
244
+ self.trace = trace
245
+ self.address = address
246
+ self.tcp_host = tcp_host
247
+ self.tcp_port = tcp_port
248
+ self.encrypt = encrypt
249
+ self.device: Optional[Device] = None
250
+ self.connection: Optional[Connection] = None
251
+ self.rfcomm_client: Optional[rfcomm.Client]
252
+ self.rfcomm_mux: Optional[rfcomm.Multiplexer]
253
+ self.tcp_connected: bool = False
254
+
255
+ self.tcp_tracer: Optional[Tracer]
256
+ self.rfcomm_tracer: Optional[Tracer]
257
+
258
+ if trace:
259
+ self.tcp_tracer = Tracer(color("RFCOMM->TCP", "cyan"))
260
+ self.rfcomm_tracer = Tracer(color("TCP->RFCOMM", "magenta"))
261
+ else:
262
+ self.rfcomm_tracer = None
263
+ self.tcp_tracer = None
264
+
265
+ async def connect(self) -> None:
266
+ if self.connection:
267
+ return
268
+
269
+ print(color(f"@@@ Connecting to Bluetooth {self.address}", "blue"))
270
+ assert self.device
271
+ self.connection = await self.device.connect(
272
+ self.address, transport=core.BT_BR_EDR_TRANSPORT
273
+ )
274
+ print(color(f"@@@ Bluetooth connection: {self.connection}", "blue"))
275
+ self.connection.on("disconnection", self.on_disconnection)
276
+
277
+ if self.encrypt:
278
+ print(color("@@@ Encrypting Bluetooth connection", "blue"))
279
+ await self.connection.encrypt()
280
+ print(color("@@@ Bluetooth connection encrypted", "blue"))
281
+
282
+ self.rfcomm_client = rfcomm.Client(self.connection)
283
+ try:
284
+ self.rfcomm_mux = await self.rfcomm_client.start()
285
+ except BaseException as e:
286
+ print(color("!!! Failed to setup RFCOMM connection", "red"), e)
287
+ raise
288
+
289
+ async def start(self, device: Device) -> None:
290
+ self.device = device
291
+ await device.set_connectable(False)
292
+ await device.set_discoverable(False)
293
+
294
+ # Called when a TCP connection is established
295
+ async def on_tcp_connection(reader, writer):
296
+ print(color("<<< TCP connection", "magenta"))
297
+ if self.tcp_connected:
298
+ print(
299
+ color("!!! TCP connection already active, rejecting new one", "red")
300
+ )
301
+ writer.close()
302
+ return
303
+ self.tcp_connected = True
304
+
305
+ try:
306
+ await self.pipe(reader, writer)
307
+ except BaseException as error:
308
+ print(color("!!! Exception while piping data:", "red"), error)
309
+ return
310
+ finally:
311
+ writer.close()
312
+ await writer.wait_closed()
313
+ self.tcp_connected = False
314
+
315
+ await asyncio.start_server(
316
+ on_tcp_connection,
317
+ host=self.tcp_host if self.tcp_host != "_" else None,
318
+ port=self.tcp_port,
319
+ )
320
+ print(
321
+ color(
322
+ f"### Listening for TCP connections on port {self.tcp_port}", "magenta"
323
+ )
324
+ )
325
+
326
+ async def pipe(
327
+ self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
328
+ ) -> None:
329
+ # Resolve the channel number from the UUID if needed
330
+ if self.channel == 0:
331
+ await self.connect()
332
+ assert self.connection
333
+ channel = await rfcomm.find_rfcomm_channel_with_uuid(
334
+ self.connection, self.uuid
335
+ )
336
+ if channel:
337
+ print(color(f"### Found RFCOMM channel {channel}", "yellow"))
338
+ else:
339
+ print(color(f"!!! RFCOMM channel with UUID {self.uuid} not found"))
340
+ return
341
+ else:
342
+ channel = self.channel
343
+
344
+ # Connect a new RFCOMM channel
345
+ await self.connect()
346
+ assert self.rfcomm_mux
347
+ print(color(f"*** Opening RFCOMM channel {channel}", "green"))
348
+ try:
349
+ rfcomm_channel = await self.rfcomm_mux.open_dlc(channel)
350
+ print(color(f"*** RFCOMM channel open: {rfcomm_channel}", "green"))
351
+ except Exception as error:
352
+ print(color(f"!!! RFCOMM open failed: {error}", "red"))
353
+ return
354
+
355
+ # Pipe data from RFCOMM to TCP
356
+ def on_rfcomm_channel_closed():
357
+ print(color("*** RFCOMM channel closed", "green"))
358
+
359
+ def write_rfcomm_data(data):
360
+ if self.trace:
361
+ self.rfcomm_tracer.trace_data(data)
362
+
363
+ writer.write(data)
364
+
365
+ rfcomm_channel.on("close", on_rfcomm_channel_closed)
366
+ rfcomm_channel.sink = write_rfcomm_data
367
+
368
+ # Pipe data from TCP to RFCOMM
369
+ while True:
370
+ try:
371
+ data = await reader.read(self.READ_CHUNK_SIZE)
372
+
373
+ if len(data) == 0:
374
+ print(color("### TCP end of stream", "yellow"))
375
+ if rfcomm_channel.state == rfcomm.DLC.State.CONNECTED:
376
+ await rfcomm_channel.disconnect()
377
+ self.tcp_connected = False
378
+ return
379
+
380
+ if self.tcp_tracer:
381
+ self.tcp_tracer.trace_data(data)
382
+
383
+ rfcomm_channel.write(data)
384
+ await rfcomm_channel.drain()
385
+ except Exception as error:
386
+ print(f"!!! Exception: {error}")
387
+ break
388
+
389
+ print(color("~~~ Bye bye", "magenta"))
390
+
391
+ def on_disconnection(self, reason: int) -> None:
392
+ print(
393
+ color("@@@ Bluetooth disconnection:", "red"),
394
+ hci.HCI_Constant.error_name(reason),
395
+ )
396
+ self.connection = None
397
+
398
+
399
+ # -----------------------------------------------------------------------------
400
+ async def run(device_config, hci_transport, bridge):
401
+ print("<<< connecting to HCI...")
402
+ async with await transport.open_transport_or_link(hci_transport) as (
403
+ hci_source,
404
+ hci_sink,
405
+ ):
406
+ print("<<< connected")
407
+
408
+ if device_config:
409
+ device = Device.from_config_file_with_hci(
410
+ device_config, hci_source, hci_sink
411
+ )
412
+ else:
413
+ device = Device.from_config_with_hci(
414
+ DeviceConfiguration(), hci_source, hci_sink
415
+ )
416
+ device.classic_enabled = True
417
+
418
+ # Let's go
419
+ await device.power_on()
420
+ try:
421
+ await bridge.start(device)
422
+
423
+ # Wait until the transport terminates
424
+ await hci_source.wait_for_termination()
425
+ except core.ConnectionError as error:
426
+ print(color(f"!!! Bluetooth connection failed: {error}", "red"))
427
+ except Exception as error:
428
+ print(f"Exception while running bridge: {error}")
429
+
430
+
431
+ # -----------------------------------------------------------------------------
432
+ @click.group()
433
+ @click.pass_context
434
+ @click.option(
435
+ "--device-config",
436
+ metavar="CONFIG_FILE",
437
+ help="Device configuration file",
438
+ )
439
+ @click.option(
440
+ "--hci-transport", metavar="TRANSPORT_NAME", help="HCI transport", required=True
441
+ )
442
+ @click.option("--trace", is_flag=True, help="Trace bridged data to stdout")
443
+ @click.option(
444
+ "--channel",
445
+ metavar="CHANNEL_NUMER",
446
+ help="RFCOMM channel number",
447
+ type=int,
448
+ default=0,
449
+ )
450
+ @click.option(
451
+ "--uuid",
452
+ metavar="UUID",
453
+ help="UUID for the RFCOMM channel",
454
+ default=DEFAULT_RFCOMM_UUID,
455
+ )
456
+ def cli(
457
+ context,
458
+ device_config,
459
+ hci_transport,
460
+ trace,
461
+ channel,
462
+ uuid,
463
+ ):
464
+ context.ensure_object(dict)
465
+ context.obj["device_config"] = device_config
466
+ context.obj["hci_transport"] = hci_transport
467
+ context.obj["trace"] = trace
468
+ context.obj["channel"] = channel
469
+ context.obj["uuid"] = uuid
470
+
471
+
472
+ # -----------------------------------------------------------------------------
473
+ @cli.command()
474
+ @click.pass_context
475
+ @click.option("--tcp-host", help="TCP host", default="localhost")
476
+ @click.option("--tcp-port", help="TCP port", default=DEFAULT_SERVER_TCP_PORT)
477
+ def server(context, tcp_host, tcp_port):
478
+ bridge = ServerBridge(
479
+ context.obj["channel"],
480
+ context.obj["uuid"],
481
+ context.obj["trace"],
482
+ tcp_host,
483
+ tcp_port,
484
+ )
485
+ asyncio.run(run(context.obj["device_config"], context.obj["hci_transport"], bridge))
486
+
487
+
488
+ # -----------------------------------------------------------------------------
489
+ @cli.command()
490
+ @click.pass_context
491
+ @click.argument("bluetooth-address")
492
+ @click.option("--tcp-host", help="TCP host", default="_")
493
+ @click.option("--tcp-port", help="TCP port", default=DEFAULT_CLIENT_TCP_PORT)
494
+ @click.option("--encrypt", is_flag=True, help="Encrypt the connection")
495
+ def client(context, bluetooth_address, tcp_host, tcp_port, encrypt):
496
+ bridge = ClientBridge(
497
+ context.obj["channel"],
498
+ context.obj["uuid"],
499
+ context.obj["trace"],
500
+ bluetooth_address,
501
+ tcp_host,
502
+ tcp_port,
503
+ encrypt,
504
+ )
505
+ asyncio.run(run(context.obj["device_config"], context.obj["hci_transport"], bridge))
506
+
507
+
508
+ # -----------------------------------------------------------------------------
509
+ logging.basicConfig(level=os.environ.get("BUMBLE_LOGLEVEL", "WARNING").upper())
510
+ if __name__ == "__main__":
511
+ cli(obj={}) # pylint: disable=no-value-for-parameter
bumble/l2cap.py CHANGED
@@ -70,6 +70,7 @@ L2CAP_LE_SIGNALING_CID = 0x05
70
70
 
71
71
  L2CAP_MIN_LE_MTU = 23
72
72
  L2CAP_MIN_BR_EDR_MTU = 48
73
+ L2CAP_MAX_BR_EDR_MTU = 65535
73
74
 
74
75
  L2CAP_DEFAULT_MTU = 2048 # Default value for the MTU we are willing to accept
75
76
 
@@ -832,7 +833,9 @@ class ClassicChannel(EventEmitter):
832
833
 
833
834
  # Wait for the connection to succeed or fail
834
835
  try:
835
- return await self.connection_result
836
+ return await self.connection.abort_on(
837
+ 'disconnection', self.connection_result
838
+ )
836
839
  finally:
837
840
  self.connection_result = None
838
841
 
@@ -2225,7 +2228,7 @@ class ChannelManager:
2225
2228
  # Connect
2226
2229
  try:
2227
2230
  await channel.connect()
2228
- except Exception as e:
2231
+ except BaseException as e:
2229
2232
  del connection_channels[source_cid]
2230
2233
  raise e
2231
2234
 
bumble/rfcomm.py CHANGED
@@ -106,9 +106,11 @@ CRC_TABLE = bytes([
106
106
  0XBA, 0X2B, 0X59, 0XC8, 0XBD, 0X2C, 0X5E, 0XCF
107
107
  ])
108
108
 
109
- RFCOMM_DEFAULT_L2CAP_MTU = 2048
110
- RFCOMM_DEFAULT_WINDOW_SIZE = 7
111
- RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000
109
+ RFCOMM_DEFAULT_L2CAP_MTU = 2048
110
+ RFCOMM_DEFAULT_INITIAL_CREDITS = 7
111
+ RFCOMM_DEFAULT_MAX_CREDITS = 32
112
+ RFCOMM_DEFAULT_CREDIT_THRESHOLD = RFCOMM_DEFAULT_MAX_CREDITS // 2
113
+ RFCOMM_DEFAULT_MAX_FRAME_SIZE = 2000
112
114
 
113
115
  RFCOMM_DYNAMIC_CHANNEL_NUMBER_START = 1
114
116
  RFCOMM_DYNAMIC_CHANNEL_NUMBER_END = 30
@@ -365,12 +367,12 @@ class RFCOMM_MCC_PN:
365
367
  ack_timer: int
366
368
  max_frame_size: int
367
369
  max_retransmissions: int
368
- window_size: int
370
+ initial_credits: int
369
371
 
370
372
  def __post_init__(self) -> None:
371
- if self.window_size < 1 or self.window_size > 7:
373
+ if self.initial_credits < 1 or self.initial_credits > 7:
372
374
  logger.warning(
373
- f'Error Recovery Window size {self.window_size} is out of range [1, 7].'
375
+ f'Initial credits {self.initial_credits} is out of range [1, 7].'
374
376
  )
375
377
 
376
378
  @staticmethod
@@ -382,7 +384,7 @@ class RFCOMM_MCC_PN:
382
384
  ack_timer=data[3],
383
385
  max_frame_size=data[4] | data[5] << 8,
384
386
  max_retransmissions=data[6],
385
- window_size=data[7] & 0x07,
387
+ initial_credits=data[7] & 0x07,
386
388
  )
387
389
 
388
390
  def __bytes__(self) -> bytes:
@@ -396,7 +398,7 @@ class RFCOMM_MCC_PN:
396
398
  (self.max_frame_size >> 8) & 0xFF,
397
399
  self.max_retransmissions & 0xFF,
398
400
  # Only 3 bits are meaningful.
399
- self.window_size & 0x07,
401
+ self.initial_credits & 0x07,
400
402
  ]
401
403
  )
402
404
 
@@ -446,40 +448,43 @@ class DLC(EventEmitter):
446
448
  DISCONNECTED = 0x04
447
449
  RESET = 0x05
448
450
 
449
- connection_result: Optional[asyncio.Future]
450
- _sink: Optional[Callable[[bytes], None]]
451
- _enqueued_rx_packets: collections.deque[bytes]
452
-
453
451
  def __init__(
454
452
  self,
455
453
  multiplexer: Multiplexer,
456
454
  dlci: int,
457
- max_frame_size: int,
458
- window_size: int,
455
+ tx_max_frame_size: int,
456
+ tx_initial_credits: int,
457
+ rx_max_frame_size: int,
458
+ rx_initial_credits: int,
459
459
  ) -> None:
460
460
  super().__init__()
461
461
  self.multiplexer = multiplexer
462
462
  self.dlci = dlci
463
- self.max_frame_size = max_frame_size
464
- self.window_size = window_size
465
- self.rx_credits = window_size
466
- self.rx_threshold = window_size // 2
467
- self.tx_credits = window_size
463
+ self.rx_max_frame_size = rx_max_frame_size
464
+ self.rx_initial_credits = rx_initial_credits
465
+ self.rx_max_credits = RFCOMM_DEFAULT_MAX_CREDITS
466
+ self.rx_credits = rx_initial_credits
467
+ self.rx_credits_threshold = RFCOMM_DEFAULT_CREDIT_THRESHOLD
468
+ self.tx_max_frame_size = tx_max_frame_size
469
+ self.tx_credits = tx_initial_credits
468
470
  self.tx_buffer = b''
469
471
  self.state = DLC.State.INIT
470
472
  self.role = multiplexer.role
471
473
  self.c_r = 1 if self.role == Multiplexer.Role.INITIATOR else 0
472
- self.connection_result = None
474
+ self.connection_result: Optional[asyncio.Future] = None
475
+ self.disconnection_result: Optional[asyncio.Future] = None
473
476
  self.drained = asyncio.Event()
474
477
  self.drained.set()
475
478
  # Queued packets when sink is not set.
476
- self._enqueued_rx_packets = collections.deque(maxlen=DEFAULT_RX_QUEUE_SIZE)
477
- self._sink = None
479
+ self._enqueued_rx_packets: collections.deque[bytes] = collections.deque(
480
+ maxlen=DEFAULT_RX_QUEUE_SIZE
481
+ )
482
+ self._sink: Optional[Callable[[bytes], None]] = None
478
483
 
479
484
  # Compute the MTU
480
485
  max_overhead = 4 + 1 # header with 2-byte length + fcs
481
486
  self.mtu = min(
482
- max_frame_size, self.multiplexer.l2cap_channel.peer_mtu - max_overhead
487
+ tx_max_frame_size, self.multiplexer.l2cap_channel.peer_mtu - max_overhead
483
488
  )
484
489
 
485
490
  @property
@@ -525,20 +530,35 @@ class DLC(EventEmitter):
525
530
  self.emit('open')
526
531
 
527
532
  def on_ua_frame(self, _frame: RFCOMM_Frame) -> None:
528
- if self.state != DLC.State.CONNECTING:
533
+ if self.state == DLC.State.CONNECTING:
534
+ # Exchange the modem status with the peer
535
+ msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
536
+ mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.MSC, c_r=1, data=bytes(msc))
537
+ logger.debug(f'>>> MCC MSC Command: {msc}')
538
+ self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
539
+
540
+ self.change_state(DLC.State.CONNECTED)
541
+ if self.connection_result:
542
+ self.connection_result.set_result(None)
543
+ self.connection_result = None
544
+ self.multiplexer.on_dlc_open_complete(self)
545
+ elif self.state == DLC.State.DISCONNECTING:
546
+ self.change_state(DLC.State.DISCONNECTED)
547
+ if self.disconnection_result:
548
+ self.disconnection_result.set_result(None)
549
+ self.disconnection_result = None
550
+ self.multiplexer.on_dlc_disconnection(self)
551
+ self.emit('close')
552
+ else:
529
553
  logger.warning(
530
- color('!!! received SABM when not in CONNECTING state', 'red')
554
+ color(
555
+ (
556
+ '!!! received UA frame when not in '
557
+ 'CONNECTING or DISCONNECTING state'
558
+ ),
559
+ 'red',
560
+ )
531
561
  )
532
- return
533
-
534
- # Exchange the modem status with the peer
535
- msc = RFCOMM_MCC_MSC(dlci=self.dlci, fc=0, rtc=1, rtr=1, ic=0, dv=1)
536
- mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.MSC, c_r=1, data=bytes(msc))
537
- logger.debug(f'>>> MCC MSC Command: {msc}')
538
- self.send_frame(RFCOMM_Frame.uih(c_r=self.c_r, dlci=0, information=mcc))
539
-
540
- self.change_state(DLC.State.CONNECTED)
541
- self.multiplexer.on_dlc_open_complete(self)
542
562
 
543
563
  def on_dm_frame(self, frame: RFCOMM_Frame) -> None:
544
564
  # TODO: handle all states
@@ -609,6 +629,19 @@ class DLC(EventEmitter):
609
629
  self.connection_result = asyncio.get_running_loop().create_future()
610
630
  self.send_frame(RFCOMM_Frame.sabm(c_r=self.c_r, dlci=self.dlci))
611
631
 
632
+ async def disconnect(self) -> None:
633
+ if self.state != DLC.State.CONNECTED:
634
+ raise InvalidStateError('invalid state')
635
+
636
+ self.disconnection_result = asyncio.get_running_loop().create_future()
637
+ self.change_state(DLC.State.DISCONNECTING)
638
+ self.send_frame(
639
+ RFCOMM_Frame.disc(
640
+ c_r=1 if self.role == Multiplexer.Role.INITIATOR else 0, dlci=self.dlci
641
+ )
642
+ )
643
+ await self.disconnection_result
644
+
612
645
  def accept(self) -> None:
613
646
  if self.state != DLC.State.INIT:
614
647
  raise InvalidStateError('invalid state')
@@ -618,9 +651,9 @@ class DLC(EventEmitter):
618
651
  cl=0xE0,
619
652
  priority=7,
620
653
  ack_timer=0,
621
- max_frame_size=self.max_frame_size,
654
+ max_frame_size=self.rx_max_frame_size,
622
655
  max_retransmissions=0,
623
- window_size=self.window_size,
656
+ initial_credits=self.rx_initial_credits,
624
657
  )
625
658
  mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.PN, c_r=0, data=bytes(pn))
626
659
  logger.debug(f'>>> PN Response: {pn}')
@@ -628,8 +661,8 @@ class DLC(EventEmitter):
628
661
  self.change_state(DLC.State.CONNECTING)
629
662
 
630
663
  def rx_credits_needed(self) -> int:
631
- if self.rx_credits <= self.rx_threshold:
632
- return self.window_size - self.rx_credits
664
+ if self.rx_credits <= self.rx_credits_threshold:
665
+ return self.rx_max_credits - self.rx_credits
633
666
 
634
667
  return 0
635
668
 
@@ -689,6 +722,17 @@ class DLC(EventEmitter):
689
722
  async def drain(self) -> None:
690
723
  await self.drained.wait()
691
724
 
725
+ def abort(self) -> None:
726
+ logger.debug(f'aborting DLC: {self}')
727
+ if self.connection_result:
728
+ self.connection_result.cancel()
729
+ self.connection_result = None
730
+ if self.disconnection_result:
731
+ self.disconnection_result.cancel()
732
+ self.disconnection_result = None
733
+ self.change_state(DLC.State.RESET)
734
+ self.emit('close')
735
+
692
736
  def __str__(self) -> str:
693
737
  return f'DLC(dlci={self.dlci},state={self.state.name})'
694
738
 
@@ -711,7 +755,7 @@ class Multiplexer(EventEmitter):
711
755
  connection_result: Optional[asyncio.Future]
712
756
  disconnection_result: Optional[asyncio.Future]
713
757
  open_result: Optional[asyncio.Future]
714
- acceptor: Optional[Callable[[int], bool]]
758
+ acceptor: Optional[Callable[[int], Optional[Tuple[int, int]]]]
715
759
  dlcs: Dict[int, DLC]
716
760
 
717
761
  def __init__(self, l2cap_channel: l2cap.ClassicChannel, role: Role) -> None:
@@ -723,11 +767,15 @@ class Multiplexer(EventEmitter):
723
767
  self.connection_result = None
724
768
  self.disconnection_result = None
725
769
  self.open_result = None
770
+ self.open_pn: Optional[RFCOMM_MCC_PN] = None
771
+ self.open_rx_max_credits = 0
726
772
  self.acceptor = None
727
773
 
728
774
  # Become a sink for the L2CAP channel
729
775
  l2cap_channel.sink = self.on_pdu
730
776
 
777
+ l2cap_channel.on('close', self.on_l2cap_channel_close)
778
+
731
779
  def change_state(self, new_state: State) -> None:
732
780
  logger.debug(f'{self} state change -> {color(new_state.name, "cyan")}')
733
781
  self.state = new_state
@@ -791,6 +839,7 @@ class Multiplexer(EventEmitter):
791
839
  'rfcomm',
792
840
  )
793
841
  )
842
+ self.open_result = None
794
843
  else:
795
844
  logger.warning(f'unexpected state for DM: {self}')
796
845
 
@@ -828,9 +877,16 @@ class Multiplexer(EventEmitter):
828
877
  else:
829
878
  if self.acceptor:
830
879
  channel_number = pn.dlci >> 1
831
- if self.acceptor(channel_number):
880
+ if dlc_params := self.acceptor(channel_number):
832
881
  # Create a new DLC
833
- dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size)
882
+ dlc = DLC(
883
+ self,
884
+ dlci=pn.dlci,
885
+ tx_max_frame_size=pn.max_frame_size,
886
+ tx_initial_credits=pn.initial_credits,
887
+ rx_max_frame_size=dlc_params[0],
888
+ rx_initial_credits=dlc_params[1],
889
+ )
834
890
  self.dlcs[pn.dlci] = dlc
835
891
 
836
892
  # Re-emit the handshake completion event
@@ -848,8 +904,17 @@ class Multiplexer(EventEmitter):
848
904
  # Response
849
905
  logger.debug(f'>>> PN Response: {pn}')
850
906
  if self.state == Multiplexer.State.OPENING:
851
- dlc = DLC(self, pn.dlci, pn.max_frame_size, pn.window_size)
907
+ assert self.open_pn
908
+ dlc = DLC(
909
+ self,
910
+ dlci=pn.dlci,
911
+ tx_max_frame_size=pn.max_frame_size,
912
+ tx_initial_credits=pn.initial_credits,
913
+ rx_max_frame_size=self.open_pn.max_frame_size,
914
+ rx_initial_credits=self.open_pn.initial_credits,
915
+ )
852
916
  self.dlcs[pn.dlci] = dlc
917
+ self.open_pn = None
853
918
  dlc.connect()
854
919
  else:
855
920
  logger.warning('ignoring PN response')
@@ -887,7 +952,7 @@ class Multiplexer(EventEmitter):
887
952
  self,
888
953
  channel: int,
889
954
  max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE,
890
- window_size: int = RFCOMM_DEFAULT_WINDOW_SIZE,
955
+ initial_credits: int = RFCOMM_DEFAULT_INITIAL_CREDITS,
891
956
  ) -> DLC:
892
957
  if self.state != Multiplexer.State.CONNECTED:
893
958
  if self.state == Multiplexer.State.OPENING:
@@ -895,17 +960,19 @@ class Multiplexer(EventEmitter):
895
960
 
896
961
  raise InvalidStateError('not connected')
897
962
 
898
- pn = RFCOMM_MCC_PN(
963
+ self.open_pn = RFCOMM_MCC_PN(
899
964
  dlci=channel << 1,
900
965
  cl=0xF0,
901
966
  priority=7,
902
967
  ack_timer=0,
903
968
  max_frame_size=max_frame_size,
904
969
  max_retransmissions=0,
905
- window_size=window_size,
970
+ initial_credits=initial_credits,
971
+ )
972
+ mcc = RFCOMM_Frame.make_mcc(
973
+ mcc_type=MccType.PN, c_r=1, data=bytes(self.open_pn)
906
974
  )
907
- mcc = RFCOMM_Frame.make_mcc(mcc_type=MccType.PN, c_r=1, data=bytes(pn))
908
- logger.debug(f'>>> Sending MCC: {pn}')
975
+ logger.debug(f'>>> Sending MCC: {self.open_pn}')
909
976
  self.open_result = asyncio.get_running_loop().create_future()
910
977
  self.change_state(Multiplexer.State.OPENING)
911
978
  self.send_frame(
@@ -915,15 +982,31 @@ class Multiplexer(EventEmitter):
915
982
  information=mcc,
916
983
  )
917
984
  )
918
- result = await self.open_result
919
- self.open_result = None
920
- return result
985
+ return await self.open_result
921
986
 
922
987
  def on_dlc_open_complete(self, dlc: DLC) -> None:
923
988
  logger.debug(f'DLC [{dlc.dlci}] open complete')
989
+
924
990
  self.change_state(Multiplexer.State.CONNECTED)
991
+
925
992
  if self.open_result:
926
993
  self.open_result.set_result(dlc)
994
+ self.open_result = None
995
+
996
+ def on_dlc_disconnection(self, dlc: DLC) -> None:
997
+ logger.debug(f'DLC [{dlc.dlci}] disconnection')
998
+ self.dlcs.pop(dlc.dlci, None)
999
+
1000
+ def on_l2cap_channel_close(self) -> None:
1001
+ logger.debug('L2CAP channel closed, cleaning up')
1002
+ if self.open_result:
1003
+ self.open_result.cancel()
1004
+ self.open_result = None
1005
+ if self.disconnection_result:
1006
+ self.disconnection_result.cancel()
1007
+ self.disconnection_result = None
1008
+ for dlc in self.dlcs.values():
1009
+ dlc.abort()
927
1010
 
928
1011
  def __str__(self) -> str:
929
1012
  return f'Multiplexer(state={self.state.name})'
@@ -982,15 +1065,13 @@ class Client:
982
1065
 
983
1066
  # -----------------------------------------------------------------------------
984
1067
  class Server(EventEmitter):
985
- acceptors: Dict[int, Callable[[DLC], None]]
986
-
987
1068
  def __init__(
988
1069
  self, device: Device, l2cap_mtu: int = RFCOMM_DEFAULT_L2CAP_MTU
989
1070
  ) -> None:
990
1071
  super().__init__()
991
1072
  self.device = device
992
- self.multiplexer = None
993
- self.acceptors = {}
1073
+ self.acceptors: Dict[int, Callable[[DLC], None]] = {}
1074
+ self.dlc_configs: Dict[int, Tuple[int, int]] = {}
994
1075
 
995
1076
  # Register ourselves with the L2CAP channel manager
996
1077
  self.l2cap_server = device.create_l2cap_server(
@@ -998,7 +1079,13 @@ class Server(EventEmitter):
998
1079
  handler=self.on_connection,
999
1080
  )
1000
1081
 
1001
- def listen(self, acceptor: Callable[[DLC], None], channel: int = 0) -> int:
1082
+ def listen(
1083
+ self,
1084
+ acceptor: Callable[[DLC], None],
1085
+ channel: int = 0,
1086
+ max_frame_size: int = RFCOMM_DEFAULT_MAX_FRAME_SIZE,
1087
+ initial_credits: int = RFCOMM_DEFAULT_INITIAL_CREDITS,
1088
+ ) -> int:
1002
1089
  if channel:
1003
1090
  if channel in self.acceptors:
1004
1091
  # Busy
@@ -1018,6 +1105,8 @@ class Server(EventEmitter):
1018
1105
  return 0
1019
1106
 
1020
1107
  self.acceptors[channel] = acceptor
1108
+ self.dlc_configs[channel] = (max_frame_size, initial_credits)
1109
+
1021
1110
  return channel
1022
1111
 
1023
1112
  def on_connection(self, l2cap_channel: l2cap.ClassicChannel) -> None:
@@ -1035,15 +1124,14 @@ class Server(EventEmitter):
1035
1124
  # Notify
1036
1125
  self.emit('start', multiplexer)
1037
1126
 
1038
- def accept_dlc(self, channel_number: int) -> bool:
1039
- return channel_number in self.acceptors
1127
+ def accept_dlc(self, channel_number: int) -> Optional[Tuple[int, int]]:
1128
+ return self.dlc_configs.get(channel_number)
1040
1129
 
1041
1130
  def on_dlc(self, dlc: DLC) -> None:
1042
1131
  logger.debug(f'@@@ new DLC connected: {dlc}')
1043
1132
 
1044
1133
  # Let the acceptor know
1045
- acceptor = self.acceptors.get(dlc.dlci >> 1)
1046
- if acceptor:
1134
+ if acceptor := self.acceptors.get(dlc.dlci >> 1):
1047
1135
  acceptor(dlc)
1048
1136
 
1049
1137
  def __enter__(self) -> Self:
bumble/sdp.py CHANGED
@@ -997,7 +997,7 @@ class Server:
997
997
  try:
998
998
  handler(sdp_pdu)
999
999
  except Exception as error:
1000
- logger.warning(f'{color("!!! Exception in handler:", "red")} {error}')
1000
+ logger.exception(f'{color("!!! Exception in handler:", "red")} {error}')
1001
1001
  self.send_response(
1002
1002
  SDP_ErrorResponse(
1003
1003
  transaction_id=sdp_pdu.transaction_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bumble
3
- Version: 0.0.193
3
+ Version: 0.0.194
4
4
  Summary: Bluetooth Stack for Apps, Emulation, Test and Experimentation
5
5
  Home-page: https://github.com/google/bumble
6
6
  Author: Google
@@ -1,5 +1,5 @@
1
1
  bumble/__init__.py,sha256=Q8jkz6rgl95IMAeInQVt_2GLoJl3DcEP2cxtrQ-ho5c,110
2
- bumble/_version.py,sha256=Gd5zR-hy663VWkpBiQSeTTwcqqLABTk22YvYNZ1-ZgY,415
2
+ bumble/_version.py,sha256=bNzvTCoTQ0mZBbxGZ_NgDoKjW449n_BkE6JvREc5fNw,415
3
3
  bumble/a2dp.py,sha256=VEeAOCfT1ZqpwnEgel6DJ32vxR8jYX3IAaBfCqPdWO8,22675
4
4
  bumble/at.py,sha256=kdrcsx2C8Rg61EWESD2QHwpZntkXkRBJLrPn9auv9K8,2961
5
5
  bumble/att.py,sha256=TGzhhBKCQPA_P_eDDSNASJVfa3dCr-QzzrRB3GekrI0,32366
@@ -26,18 +26,18 @@ bumble/hfp.py,sha256=OsBDREelxhLMi_UZO9Kxlqzbts08CcGxoiicUgrYlXg,75353
26
26
  bumble/hid.py,sha256=Dd4rsmkRxcxt1IjoozJdu9Qd-QWruKJfsiYqTT89NDk,20590
27
27
  bumble/host.py,sha256=2hT-HRAlxPhVMoXUwn5E1-M90bbCNsWOam5nV3fnF1o,47175
28
28
  bumble/keys.py,sha256=WbIQ7Ob81mW75qmEPQ2rBLfnqBMA-ts2yowWXP9UaCY,12654
29
- bumble/l2cap.py,sha256=8m_1Kv6Tk-M-DilkAz_OXx0XsiLUhXycEZjUkICwyj0,81064
29
+ bumble/l2cap.py,sha256=YjUoLVR8gYpPzF4JyLoEE8Jhuu72fbW-eXYhXnFphSc,81170
30
30
  bumble/link.py,sha256=QiiMSCZ0z0ko2oUEMYg6nbq-h5A_3DLN4pjqAx_E-SA,23980
31
31
  bumble/pairing.py,sha256=tgPUba6xNxMi-2plm3xfRlzHq-uPRNZEIGWaN0qNGCs,9853
32
32
  bumble/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- bumble/rfcomm.py,sha256=M93g5CxBVvKEEDr_gxbg3immwnWIu_LP158NXVc5B64,36667
34
- bumble/sdp.py,sha256=_Jp3Ui7dwIm-1t5vvIDBzPOs_gLmFpnZBnPcYtgu6nY,45287
33
+ bumble/rfcomm.py,sha256=nfagIDp26xBEwwzrpyWJt0NKCWTMOQvPUWrSPvIUGOA,40361
34
+ bumble/sdp.py,sha256=yA3gkyyFaLkt-nHff3Ge5BgFgqX9uVgr56fWJVA1py8,45289
35
35
  bumble/smp.py,sha256=PcQj8mDoM8fBc4gKECHoOs0A2ukUAaSZQGdgLj6YzB0,76277
36
36
  bumble/snoop.py,sha256=_QfF36eylBW6Snd-_KYOwKaGiM8i_Ed-B5XoFIPt3Dg,5631
37
37
  bumble/utils.py,sha256=e0i-4d28-9zP3gYcd1rdNd669rkPnRs5oJCERUEDfxo,15099
38
38
  bumble/apps/README.md,sha256=XTwjRAY-EJWDXpl1V8K3Mw8B7kIqzUIUizRjVBVhoIE,1769
39
39
  bumble/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- bumble/apps/bench.py,sha256=wwflVPDw_xaN_duZJF2oCrzAlIuJVR7rEKtVVTmzq1s,52359
40
+ bumble/apps/bench.py,sha256=7tZH14ykiNcX5g3_Lw-s2ciuiAW8G4E2wSCjkwclU0Q,54300
41
41
  bumble/apps/ble_rpa_tool.py,sha256=ZQtsbfnLPd5qUAkEBPpNgJLRynBBc7q_9cDHKUW2SQ0,1701
42
42
  bumble/apps/console.py,sha256=rVR2jmP6Yd76B4zzGPYnpJFtgeYgq19CL6DMSe2-A1M,46093
43
43
  bumble/apps/controller_info.py,sha256=pgi6leHpwGdi3-kFUc7uFfuyGPTkNEoOws8cWycQVT0,9249
@@ -49,6 +49,7 @@ bumble/apps/hci_bridge.py,sha256=KISv352tKnsQsoxjkDiCQbMFmhnPWdnug5wSFAAXxEs,403
49
49
  bumble/apps/l2cap_bridge.py,sha256=524VgEmgCP4g7T0UdgmsePmNVhDFRJECeaZ_uzKsbco,13062
50
50
  bumble/apps/pair.py,sha256=COU2D7YAIn4lo5iuM0ClObA1zZqQCdrXOcnsiCm0YlQ,17529
51
51
  bumble/apps/pandora_server.py,sha256=5qaoLCpcZE2KsGO21-7t6Vg4dBjBWbnyOQXwrLhxkuE,1397
52
+ bumble/apps/rfcomm_bridge.py,sha256=PSszh4Qh1IsIw8ETs0fevOCAXEdVtqlgnV-ruzqGrZI,17215
52
53
  bumble/apps/scan.py,sha256=b6hIppiJqDfR7VFW2wl3-lkPdFvHLqYZKY8VjjNnhls,8366
53
54
  bumble/apps/show.py,sha256=8w0-8jLtN6IM6_58pOHbEmE1Rmxm71O48ACrXixC2jk,6218
54
55
  bumble/apps/unbond.py,sha256=LDPWpmgKLMGYDdIFGTdGciFDcUliZ0OmseEbGfJ-MAM,3176
@@ -140,9 +141,9 @@ bumble/vendor/android/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
140
141
  bumble/vendor/android/hci.py,sha256=GZrkhaWmcMt1JpnRhv0NoySGkf2H4lNUV2f_omRZW0I,10741
141
142
  bumble/vendor/zephyr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
142
143
  bumble/vendor/zephyr/hci.py,sha256=d83bC0TvT947eN4roFjLkQefWtHOoNsr4xib2ctSkvA,3195
143
- bumble-0.0.193.dist-info/LICENSE,sha256=FvaYh4NRWIGgS_OwoBs5gFgkCmAghZ-DYnIGBZPuw-s,12142
144
- bumble-0.0.193.dist-info/METADATA,sha256=gtflo1GIuoKchDFFY0xhmfpzgV8fK78dXSg4NXc349c,5753
145
- bumble-0.0.193.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
146
- bumble-0.0.193.dist-info/entry_points.txt,sha256=UkNj1KMZDhzOb7O4OU7Jn4YI5KaxJZgQF2GF64BwOlQ,883
147
- bumble-0.0.193.dist-info/top_level.txt,sha256=tV6JJKaHPYMFiJYiBYFW24PCcfLxTJZdlu6BmH3Cb00,7
148
- bumble-0.0.193.dist-info/RECORD,,
144
+ bumble-0.0.194.dist-info/LICENSE,sha256=FvaYh4NRWIGgS_OwoBs5gFgkCmAghZ-DYnIGBZPuw-s,12142
145
+ bumble-0.0.194.dist-info/METADATA,sha256=DtzxP6olXav-014VOgDx0WaJokHXKAS0tU0w4xVcjoM,5753
146
+ bumble-0.0.194.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
147
+ bumble-0.0.194.dist-info/entry_points.txt,sha256=AOFf_gnWbZ7jk5fzspxXHCQUay1ik71pK3HYO7sZQsk,937
148
+ bumble-0.0.194.dist-info/top_level.txt,sha256=tV6JJKaHPYMFiJYiBYFW24PCcfLxTJZdlu6BmH3Cb00,7
149
+ bumble-0.0.194.dist-info/RECORD,,
@@ -10,6 +10,7 @@ bumble-l2cap-bridge = bumble.apps.l2cap_bridge:main
10
10
  bumble-link-relay = bumble.apps.link_relay.link_relay:main
11
11
  bumble-pair = bumble.apps.pair:main
12
12
  bumble-pandora-server = bumble.apps.pandora_server:main
13
+ bumble-rfcomm-bridge = bumble.apps.rfcomm_bridge:main
13
14
  bumble-rtk-fw-download = bumble.tools.rtk_fw_download:main
14
15
  bumble-rtk-util = bumble.tools.rtk_util:main
15
16
  bumble-scan = bumble.apps.scan:main