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 +2 -2
- bumble/apps/bench.py +69 -12
- bumble/apps/rfcomm_bridge.py +511 -0
- bumble/l2cap.py +5 -2
- bumble/rfcomm.py +148 -60
- bumble/sdp.py +1 -1
- {bumble-0.0.193.dist-info → bumble-0.0.194.dist-info}/METADATA +1 -1
- {bumble-0.0.193.dist-info → bumble-0.0.194.dist-info}/RECORD +12 -11
- {bumble-0.0.193.dist-info → bumble-0.0.194.dist-info}/entry_points.txt +1 -0
- {bumble-0.0.193.dist-info → bumble-0.0.194.dist-info}/LICENSE +0 -0
- {bumble-0.0.193.dist-info → bumble-0.0.194.dist-info}/WHEEL +0 -0
- {bumble-0.0.193.dist-info → bumble-0.0.194.dist-info}/top_level.txt +0 -0
bumble/_version.py
CHANGED
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__(
|
|
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.
|
|
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.
|
|
946
|
-
dlc_options['
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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['
|
|
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.
|
|
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
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
370
|
+
initial_credits: int
|
|
369
371
|
|
|
370
372
|
def __post_init__(self) -> None:
|
|
371
|
-
if self.
|
|
373
|
+
if self.initial_credits < 1 or self.initial_credits > 7:
|
|
372
374
|
logger.warning(
|
|
373
|
-
f'
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
458
|
-
|
|
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.
|
|
464
|
-
self.
|
|
465
|
-
self.
|
|
466
|
-
self.
|
|
467
|
-
self.
|
|
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(
|
|
477
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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.
|
|
654
|
+
max_frame_size=self.rx_max_frame_size,
|
|
622
655
|
max_retransmissions=0,
|
|
623
|
-
|
|
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.
|
|
632
|
-
return self.
|
|
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],
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
993
|
-
self.
|
|
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(
|
|
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) ->
|
|
1039
|
-
return
|
|
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
|
|
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.
|
|
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,5 +1,5 @@
|
|
|
1
1
|
bumble/__init__.py,sha256=Q8jkz6rgl95IMAeInQVt_2GLoJl3DcEP2cxtrQ-ho5c,110
|
|
2
|
-
bumble/_version.py,sha256=
|
|
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=
|
|
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=
|
|
34
|
-
bumble/sdp.py,sha256=
|
|
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=
|
|
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.
|
|
144
|
-
bumble-0.0.
|
|
145
|
-
bumble-0.0.
|
|
146
|
-
bumble-0.0.
|
|
147
|
-
bumble-0.0.
|
|
148
|
-
bumble-0.0.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|