bumble 0.0.180__py3-none-any.whl → 0.0.182__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 +397 -133
- bumble/apps/ble_rpa_tool.py +63 -0
- bumble/apps/console.py +4 -4
- bumble/apps/controller_info.py +64 -6
- bumble/apps/controller_loopback.py +200 -0
- bumble/apps/l2cap_bridge.py +32 -24
- bumble/apps/pair.py +6 -8
- bumble/att.py +53 -11
- bumble/controller.py +159 -24
- bumble/crypto.py +10 -0
- bumble/device.py +580 -113
- bumble/drivers/__init__.py +27 -31
- bumble/drivers/common.py +45 -0
- bumble/drivers/rtk.py +11 -4
- bumble/gatt.py +66 -51
- bumble/gatt_server.py +30 -22
- bumble/hci.py +258 -91
- bumble/helpers.py +14 -0
- bumble/hfp.py +37 -27
- bumble/hid.py +282 -61
- bumble/host.py +158 -93
- bumble/l2cap.py +11 -6
- bumble/link.py +55 -1
- bumble/profiles/asha_service.py +2 -2
- bumble/profiles/bap.py +1247 -0
- bumble/profiles/cap.py +52 -0
- bumble/profiles/csip.py +119 -9
- bumble/rfcomm.py +31 -20
- bumble/smp.py +1 -1
- bumble/transport/__init__.py +51 -22
- bumble/transport/android_emulator.py +1 -1
- bumble/transport/common.py +2 -1
- bumble/transport/hci_socket.py +1 -4
- bumble/transport/usb.py +1 -1
- bumble/utils.py +3 -6
- {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/METADATA +1 -1
- {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/RECORD +42 -37
- {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/entry_points.txt +1 -0
- {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/LICENSE +0 -0
- {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/WHEEL +0 -0
- {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright 2023 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
|
+
import click
|
|
16
|
+
from bumble.colors import color
|
|
17
|
+
from bumble.hci import Address
|
|
18
|
+
from bumble.helpers import generate_irk, verify_rpa_with_irk
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
def cli():
|
|
23
|
+
'''
|
|
24
|
+
This is a tool for generating IRK, RPA,
|
|
25
|
+
and verifying IRK/RPA pairs
|
|
26
|
+
'''
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command()
|
|
30
|
+
def gen_irk() -> None:
|
|
31
|
+
print(generate_irk().hex())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.command()
|
|
35
|
+
@click.argument("irk", type=str)
|
|
36
|
+
def gen_rpa(irk: str) -> None:
|
|
37
|
+
irk_bytes = bytes.fromhex(irk)
|
|
38
|
+
rpa = Address.generate_private_address(irk_bytes)
|
|
39
|
+
print(rpa.to_string(with_type_qualifier=False))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@click.command()
|
|
43
|
+
@click.argument("irk", type=str)
|
|
44
|
+
@click.argument("rpa", type=str)
|
|
45
|
+
def verify_rpa(irk: str, rpa: str) -> None:
|
|
46
|
+
address = Address(rpa)
|
|
47
|
+
irk_bytes = bytes.fromhex(irk)
|
|
48
|
+
if verify_rpa_with_irk(address, irk_bytes):
|
|
49
|
+
print(color("Verified", "green"))
|
|
50
|
+
else:
|
|
51
|
+
print(color("Not Verified", "red"))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
cli.add_command(gen_irk)
|
|
56
|
+
cli.add_command(gen_rpa)
|
|
57
|
+
cli.add_command(verify_rpa)
|
|
58
|
+
cli()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# -----------------------------------------------------------------------------
|
|
62
|
+
if __name__ == '__main__':
|
|
63
|
+
main()
|
bumble/apps/console.py
CHANGED
|
@@ -777,7 +777,7 @@ class ConsoleApp:
|
|
|
777
777
|
if not service:
|
|
778
778
|
continue
|
|
779
779
|
values = [
|
|
780
|
-
attribute.read_value(connection)
|
|
780
|
+
await attribute.read_value(connection)
|
|
781
781
|
for connection in self.device.connections.values()
|
|
782
782
|
]
|
|
783
783
|
if not values:
|
|
@@ -796,11 +796,11 @@ class ConsoleApp:
|
|
|
796
796
|
if not characteristic:
|
|
797
797
|
continue
|
|
798
798
|
values = [
|
|
799
|
-
attribute.read_value(connection)
|
|
799
|
+
await attribute.read_value(connection)
|
|
800
800
|
for connection in self.device.connections.values()
|
|
801
801
|
]
|
|
802
802
|
if not values:
|
|
803
|
-
values = [attribute.read_value(None)]
|
|
803
|
+
values = [await attribute.read_value(None)]
|
|
804
804
|
|
|
805
805
|
# TODO: future optimization: convert CCCD value to human readable string
|
|
806
806
|
|
|
@@ -944,7 +944,7 @@ class ConsoleApp:
|
|
|
944
944
|
|
|
945
945
|
# send data to any subscribers
|
|
946
946
|
if isinstance(attribute, Characteristic):
|
|
947
|
-
attribute.write_value(None, value)
|
|
947
|
+
await attribute.write_value(None, value)
|
|
948
948
|
if attribute.has_properties(Characteristic.NOTIFY):
|
|
949
949
|
await self.device.gatt_server.notify_subscribers(attribute)
|
|
950
950
|
if attribute.has_properties(Characteristic.INDICATE):
|
bumble/apps/controller_info.py
CHANGED
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
import asyncio
|
|
19
19
|
import os
|
|
20
20
|
import logging
|
|
21
|
+
import time
|
|
22
|
+
|
|
21
23
|
import click
|
|
22
|
-
from bumble.company_ids import COMPANY_IDENTIFIERS
|
|
23
24
|
|
|
25
|
+
from bumble.company_ids import COMPANY_IDENTIFIERS
|
|
24
26
|
from bumble.colors import color
|
|
25
27
|
from bumble.core import name_or_number
|
|
26
28
|
from bumble.hci import (
|
|
@@ -32,10 +34,14 @@ from bumble.hci import (
|
|
|
32
34
|
HCI_Command,
|
|
33
35
|
HCI_Command_Complete_Event,
|
|
34
36
|
HCI_Command_Status_Event,
|
|
37
|
+
HCI_READ_BUFFER_SIZE_COMMAND,
|
|
38
|
+
HCI_Read_Buffer_Size_Command,
|
|
35
39
|
HCI_READ_BD_ADDR_COMMAND,
|
|
36
40
|
HCI_Read_BD_ADDR_Command,
|
|
37
41
|
HCI_READ_LOCAL_NAME_COMMAND,
|
|
38
42
|
HCI_Read_Local_Name_Command,
|
|
43
|
+
HCI_LE_READ_BUFFER_SIZE_COMMAND,
|
|
44
|
+
HCI_LE_Read_Buffer_Size_Command,
|
|
39
45
|
HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND,
|
|
40
46
|
HCI_LE_Read_Maximum_Data_Length_Command,
|
|
41
47
|
HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND,
|
|
@@ -44,6 +50,7 @@ from bumble.hci import (
|
|
|
44
50
|
HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
|
|
45
51
|
HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
|
|
46
52
|
HCI_LE_Read_Suggested_Default_Data_Length_Command,
|
|
53
|
+
HCI_Read_Local_Version_Information_Command,
|
|
47
54
|
)
|
|
48
55
|
from bumble.host import Host
|
|
49
56
|
from bumble.transport import open_transport_or_link
|
|
@@ -59,7 +66,7 @@ def command_succeeded(response):
|
|
|
59
66
|
|
|
60
67
|
|
|
61
68
|
# -----------------------------------------------------------------------------
|
|
62
|
-
async def get_classic_info(host):
|
|
69
|
+
async def get_classic_info(host: Host) -> None:
|
|
63
70
|
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
|
64
71
|
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
|
65
72
|
if command_succeeded(response):
|
|
@@ -80,7 +87,7 @@ async def get_classic_info(host):
|
|
|
80
87
|
|
|
81
88
|
|
|
82
89
|
# -----------------------------------------------------------------------------
|
|
83
|
-
async def get_le_info(host):
|
|
90
|
+
async def get_le_info(host: Host) -> None:
|
|
84
91
|
print()
|
|
85
92
|
|
|
86
93
|
if host.supports_command(HCI_LE_READ_NUMBER_OF_SUPPORTED_ADVERTISING_SETS_COMMAND):
|
|
@@ -137,7 +144,32 @@ async def get_le_info(host):
|
|
|
137
144
|
|
|
138
145
|
|
|
139
146
|
# -----------------------------------------------------------------------------
|
|
140
|
-
async def
|
|
147
|
+
async def get_acl_flow_control_info(host: Host) -> None:
|
|
148
|
+
print()
|
|
149
|
+
|
|
150
|
+
if host.supports_command(HCI_READ_BUFFER_SIZE_COMMAND):
|
|
151
|
+
response = await host.send_command(
|
|
152
|
+
HCI_Read_Buffer_Size_Command(), check_result=True
|
|
153
|
+
)
|
|
154
|
+
print(
|
|
155
|
+
color('ACL Flow Control:', 'yellow'),
|
|
156
|
+
f'{response.return_parameters.hc_total_num_acl_data_packets} '
|
|
157
|
+
f'packets of size {response.return_parameters.hc_acl_data_packet_length}',
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if host.supports_command(HCI_LE_READ_BUFFER_SIZE_COMMAND):
|
|
161
|
+
response = await host.send_command(
|
|
162
|
+
HCI_LE_Read_Buffer_Size_Command(), check_result=True
|
|
163
|
+
)
|
|
164
|
+
print(
|
|
165
|
+
color('LE ACL Flow Control:', 'yellow'),
|
|
166
|
+
f'{response.return_parameters.hc_total_num_le_acl_data_packets} '
|
|
167
|
+
f'packets of size {response.return_parameters.hc_le_acl_data_packet_length}',
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# -----------------------------------------------------------------------------
|
|
172
|
+
async def async_main(latency_probes, transport):
|
|
141
173
|
print('<<< connecting to HCI...')
|
|
142
174
|
async with await open_transport_or_link(transport) as (hci_source, hci_sink):
|
|
143
175
|
print('<<< connected')
|
|
@@ -145,6 +177,23 @@ async def async_main(transport):
|
|
|
145
177
|
host = Host(hci_source, hci_sink)
|
|
146
178
|
await host.reset()
|
|
147
179
|
|
|
180
|
+
# Measure the latency if requested
|
|
181
|
+
latencies = []
|
|
182
|
+
if latency_probes:
|
|
183
|
+
for _ in range(latency_probes):
|
|
184
|
+
start = time.time()
|
|
185
|
+
await host.send_command(HCI_Read_Local_Version_Information_Command())
|
|
186
|
+
latencies.append(1000 * (time.time() - start))
|
|
187
|
+
print(
|
|
188
|
+
color('HCI Command Latency:', 'yellow'),
|
|
189
|
+
(
|
|
190
|
+
f'min={min(latencies):.2f}, '
|
|
191
|
+
f'max={max(latencies):.2f}, '
|
|
192
|
+
f'average={sum(latencies)/len(latencies):.2f}'
|
|
193
|
+
),
|
|
194
|
+
'\n',
|
|
195
|
+
)
|
|
196
|
+
|
|
148
197
|
# Print version
|
|
149
198
|
print(color('Version:', 'yellow'))
|
|
150
199
|
print(
|
|
@@ -168,6 +217,9 @@ async def async_main(transport):
|
|
|
168
217
|
# Get the LE info
|
|
169
218
|
await get_le_info(host)
|
|
170
219
|
|
|
220
|
+
# Print the ACL flow control info
|
|
221
|
+
await get_acl_flow_control_info(host)
|
|
222
|
+
|
|
171
223
|
# Print the list of commands supported by the controller
|
|
172
224
|
print()
|
|
173
225
|
print(color('Supported Commands:', 'yellow'))
|
|
@@ -177,10 +229,16 @@ async def async_main(transport):
|
|
|
177
229
|
|
|
178
230
|
# -----------------------------------------------------------------------------
|
|
179
231
|
@click.command()
|
|
232
|
+
@click.option(
|
|
233
|
+
'--latency-probes',
|
|
234
|
+
metavar='N',
|
|
235
|
+
type=int,
|
|
236
|
+
help='Send N commands to measure HCI transport latency statistics',
|
|
237
|
+
)
|
|
180
238
|
@click.argument('transport')
|
|
181
|
-
def main(transport):
|
|
239
|
+
def main(latency_probes, transport):
|
|
182
240
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
|
183
|
-
asyncio.run(async_main(transport))
|
|
241
|
+
asyncio.run(async_main(latency_probes, transport))
|
|
184
242
|
|
|
185
243
|
|
|
186
244
|
# -----------------------------------------------------------------------------
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
from bumble.colors import color
|
|
24
|
+
from bumble.hci import (
|
|
25
|
+
HCI_READ_LOOPBACK_MODE_COMMAND,
|
|
26
|
+
HCI_Read_Loopback_Mode_Command,
|
|
27
|
+
HCI_WRITE_LOOPBACK_MODE_COMMAND,
|
|
28
|
+
HCI_Write_Loopback_Mode_Command,
|
|
29
|
+
LoopbackMode,
|
|
30
|
+
)
|
|
31
|
+
from bumble.host import Host
|
|
32
|
+
from bumble.transport import open_transport_or_link
|
|
33
|
+
import click
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Loopback:
|
|
37
|
+
"""Send and receive ACL data packets in local loopback mode"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, packet_size: int, packet_count: int, transport: str):
|
|
40
|
+
self.transport = transport
|
|
41
|
+
self.packet_size = packet_size
|
|
42
|
+
self.packet_count = packet_count
|
|
43
|
+
self.connection_handle: Optional[int] = None
|
|
44
|
+
self.connection_event = asyncio.Event()
|
|
45
|
+
self.done = asyncio.Event()
|
|
46
|
+
self.expected_cid = 0
|
|
47
|
+
self.bytes_received = 0
|
|
48
|
+
self.start_timestamp = 0.0
|
|
49
|
+
self.last_timestamp = 0.0
|
|
50
|
+
|
|
51
|
+
def on_connection(self, connection_handle: int, *args):
|
|
52
|
+
"""Retrieve connection handle from new connection event"""
|
|
53
|
+
if not self.connection_event.is_set():
|
|
54
|
+
# save first connection handle for ACL
|
|
55
|
+
# subsequent connections are SCO
|
|
56
|
+
self.connection_handle = connection_handle
|
|
57
|
+
self.connection_event.set()
|
|
58
|
+
|
|
59
|
+
def on_l2cap_pdu(self, connection_handle: int, cid: int, pdu: bytes):
|
|
60
|
+
"""Calculate packet receive speed"""
|
|
61
|
+
now = time.time()
|
|
62
|
+
print(f'<<< Received packet {cid}: {len(pdu)} bytes')
|
|
63
|
+
assert connection_handle == self.connection_handle
|
|
64
|
+
assert cid == self.expected_cid
|
|
65
|
+
self.expected_cid += 1
|
|
66
|
+
if cid == 0:
|
|
67
|
+
self.start_timestamp = now
|
|
68
|
+
else:
|
|
69
|
+
elapsed_since_start = now - self.start_timestamp
|
|
70
|
+
elapsed_since_last = now - self.last_timestamp
|
|
71
|
+
self.bytes_received += len(pdu)
|
|
72
|
+
instant_rx_speed = len(pdu) / elapsed_since_last
|
|
73
|
+
average_rx_speed = self.bytes_received / elapsed_since_start
|
|
74
|
+
print(
|
|
75
|
+
color(
|
|
76
|
+
f'@@@ RX speed: instant={instant_rx_speed:.4f},'
|
|
77
|
+
f' average={average_rx_speed:.4f}',
|
|
78
|
+
'cyan',
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
self.last_timestamp = now
|
|
83
|
+
|
|
84
|
+
if self.expected_cid == self.packet_count:
|
|
85
|
+
print(color('@@@ Received last packet', 'green'))
|
|
86
|
+
self.done.set()
|
|
87
|
+
|
|
88
|
+
async def run(self):
|
|
89
|
+
"""Run a loopback throughput test"""
|
|
90
|
+
print(color('>>> Connecting to HCI...', 'green'))
|
|
91
|
+
async with await open_transport_or_link(self.transport) as (
|
|
92
|
+
hci_source,
|
|
93
|
+
hci_sink,
|
|
94
|
+
):
|
|
95
|
+
print(color('>>> Connected', 'green'))
|
|
96
|
+
|
|
97
|
+
host = Host(hci_source, hci_sink)
|
|
98
|
+
await host.reset()
|
|
99
|
+
|
|
100
|
+
# make sure data can fit in one l2cap pdu
|
|
101
|
+
l2cap_header_size = 4
|
|
102
|
+
max_packet_size = host.acl_packet_queue.max_packet_size - l2cap_header_size
|
|
103
|
+
if self.packet_size > max_packet_size:
|
|
104
|
+
print(
|
|
105
|
+
color(
|
|
106
|
+
f'!!! Packet size ({self.packet_size}) larger than max supported'
|
|
107
|
+
f' size ({max_packet_size})',
|
|
108
|
+
'red',
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if not host.supports_command(
|
|
114
|
+
HCI_WRITE_LOOPBACK_MODE_COMMAND
|
|
115
|
+
) or not host.supports_command(HCI_READ_LOOPBACK_MODE_COMMAND):
|
|
116
|
+
print(color('!!! Loopback mode not supported', 'red'))
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
# set event callbacks
|
|
120
|
+
host.on('connection', self.on_connection)
|
|
121
|
+
host.on('l2cap_pdu', self.on_l2cap_pdu)
|
|
122
|
+
|
|
123
|
+
loopback_mode = LoopbackMode.LOCAL
|
|
124
|
+
|
|
125
|
+
print(color('### Setting loopback mode', 'blue'))
|
|
126
|
+
await host.send_command(
|
|
127
|
+
HCI_Write_Loopback_Mode_Command(loopback_mode=LoopbackMode.LOCAL),
|
|
128
|
+
check_result=True,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
print(color('### Checking loopback mode', 'blue'))
|
|
132
|
+
response = await host.send_command(
|
|
133
|
+
HCI_Read_Loopback_Mode_Command(), check_result=True
|
|
134
|
+
)
|
|
135
|
+
if response.return_parameters.loopback_mode != loopback_mode:
|
|
136
|
+
print(color('!!! Loopback mode mismatch', 'red'))
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
await self.connection_event.wait()
|
|
140
|
+
print(color('### Connected', 'cyan'))
|
|
141
|
+
|
|
142
|
+
print(color('=== Start sending', 'magenta'))
|
|
143
|
+
start_time = time.time()
|
|
144
|
+
bytes_sent = 0
|
|
145
|
+
for cid in range(0, self.packet_count):
|
|
146
|
+
# using the cid as an incremental index
|
|
147
|
+
host.send_l2cap_pdu(
|
|
148
|
+
self.connection_handle, cid, bytes(self.packet_size)
|
|
149
|
+
)
|
|
150
|
+
print(
|
|
151
|
+
color(
|
|
152
|
+
f'>>> Sending packet {cid}: {self.packet_size} bytes', 'yellow'
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
bytes_sent += self.packet_size # don't count L2CAP or HCI header sizes
|
|
156
|
+
await asyncio.sleep(0) # yield to allow packet receive
|
|
157
|
+
|
|
158
|
+
await self.done.wait()
|
|
159
|
+
print(color('=== Done!', 'magenta'))
|
|
160
|
+
|
|
161
|
+
elapsed = time.time() - start_time
|
|
162
|
+
average_tx_speed = bytes_sent / elapsed
|
|
163
|
+
print(
|
|
164
|
+
color(
|
|
165
|
+
f'@@@ TX speed: average={average_tx_speed:.4f} ({bytes_sent} bytes'
|
|
166
|
+
f' in {elapsed:.2f} seconds)',
|
|
167
|
+
'green',
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# -----------------------------------------------------------------------------
|
|
173
|
+
@click.command()
|
|
174
|
+
@click.option(
|
|
175
|
+
'--packet-size',
|
|
176
|
+
'-s',
|
|
177
|
+
metavar='SIZE',
|
|
178
|
+
type=click.IntRange(8, 4096),
|
|
179
|
+
default=500,
|
|
180
|
+
help='Packet size',
|
|
181
|
+
)
|
|
182
|
+
@click.option(
|
|
183
|
+
'--packet-count',
|
|
184
|
+
'-c',
|
|
185
|
+
metavar='COUNT',
|
|
186
|
+
type=int,
|
|
187
|
+
default=10,
|
|
188
|
+
help='Packet count',
|
|
189
|
+
)
|
|
190
|
+
@click.argument('transport')
|
|
191
|
+
def main(packet_size, packet_count, transport):
|
|
192
|
+
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
|
193
|
+
|
|
194
|
+
loopback = Loopback(packet_size, packet_count, transport)
|
|
195
|
+
asyncio.run(loopback.run())
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# -----------------------------------------------------------------------------
|
|
199
|
+
if __name__ == '__main__':
|
|
200
|
+
main()
|
bumble/apps/l2cap_bridge.py
CHANGED
|
@@ -49,14 +49,16 @@ class ServerBridge:
|
|
|
49
49
|
self.tcp_port = tcp_port
|
|
50
50
|
|
|
51
51
|
async def start(self, device: Device) -> None:
|
|
52
|
-
# Listen for incoming L2CAP
|
|
52
|
+
# Listen for incoming L2CAP channel connections
|
|
53
53
|
device.create_l2cap_server(
|
|
54
54
|
spec=l2cap.LeCreditBasedChannelSpec(
|
|
55
55
|
psm=self.psm, mtu=self.mtu, mps=self.mps, max_credits=self.max_credits
|
|
56
56
|
),
|
|
57
|
-
handler=self.
|
|
57
|
+
handler=self.on_channel,
|
|
58
|
+
)
|
|
59
|
+
print(
|
|
60
|
+
color(f'### Listening for channel connection on PSM {self.psm}', 'yellow')
|
|
58
61
|
)
|
|
59
|
-
print(color(f'### Listening for CoC connection on PSM {self.psm}', 'yellow'))
|
|
60
62
|
|
|
61
63
|
def on_ble_connection(connection):
|
|
62
64
|
def on_ble_disconnection(reason):
|
|
@@ -73,7 +75,7 @@ class ServerBridge:
|
|
|
73
75
|
await device.start_advertising(auto_restart=True)
|
|
74
76
|
|
|
75
77
|
# Called when a new L2CAP connection is established
|
|
76
|
-
def
|
|
78
|
+
def on_channel(self, l2cap_channel):
|
|
77
79
|
print(color('*** L2CAP channel:', 'cyan'), l2cap_channel)
|
|
78
80
|
|
|
79
81
|
class Pipe:
|
|
@@ -83,7 +85,7 @@ class ServerBridge:
|
|
|
83
85
|
self.l2cap_channel = l2cap_channel
|
|
84
86
|
|
|
85
87
|
l2cap_channel.on('close', self.on_l2cap_close)
|
|
86
|
-
l2cap_channel.sink = self.
|
|
88
|
+
l2cap_channel.sink = self.on_channel_sdu
|
|
87
89
|
|
|
88
90
|
async def connect_to_tcp(self):
|
|
89
91
|
# Connect to the TCP server
|
|
@@ -128,7 +130,7 @@ class ServerBridge:
|
|
|
128
130
|
if self.tcp_transport is not None:
|
|
129
131
|
self.tcp_transport.close()
|
|
130
132
|
|
|
131
|
-
def
|
|
133
|
+
def on_channel_sdu(self, sdu):
|
|
132
134
|
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
|
133
135
|
if self.tcp_transport is None:
|
|
134
136
|
print(color('!!! TCP socket not open, dropping', 'red'))
|
|
@@ -183,7 +185,7 @@ class ClientBridge:
|
|
|
183
185
|
peer_name = writer.get_extra_info('peer_name')
|
|
184
186
|
print(color(f'<<< TCP connection from {peer_name}', 'magenta'))
|
|
185
187
|
|
|
186
|
-
def
|
|
188
|
+
def on_channel_sdu(sdu):
|
|
187
189
|
print(color(f'<<< [L2CAP SDU]: {len(sdu)} bytes', 'cyan'))
|
|
188
190
|
l2cap_to_tcp_pipe.write(sdu)
|
|
189
191
|
|
|
@@ -209,7 +211,7 @@ class ClientBridge:
|
|
|
209
211
|
writer.close()
|
|
210
212
|
return
|
|
211
213
|
|
|
212
|
-
l2cap_channel.sink =
|
|
214
|
+
l2cap_channel.sink = on_channel_sdu
|
|
213
215
|
l2cap_channel.on('close', on_l2cap_close)
|
|
214
216
|
|
|
215
217
|
# Start a flow control pipe from L2CAP to TCP
|
|
@@ -274,23 +276,29 @@ async def run(device_config, hci_transport, bridge):
|
|
|
274
276
|
@click.pass_context
|
|
275
277
|
@click.option('--device-config', help='Device configuration file', required=True)
|
|
276
278
|
@click.option('--hci-transport', help='HCI transport', required=True)
|
|
277
|
-
@click.option('--psm', help='PSM for L2CAP
|
|
279
|
+
@click.option('--psm', help='PSM for L2CAP', type=int, default=1234)
|
|
278
280
|
@click.option(
|
|
279
|
-
'--l2cap-
|
|
280
|
-
help='Maximum L2CAP
|
|
281
|
+
'--l2cap-max-credits',
|
|
282
|
+
help='Maximum L2CAP Credits',
|
|
281
283
|
type=click.IntRange(1, 65535),
|
|
282
284
|
default=128,
|
|
283
285
|
)
|
|
284
286
|
@click.option(
|
|
285
|
-
'--l2cap-
|
|
286
|
-
help='L2CAP
|
|
287
|
-
type=click.IntRange(
|
|
288
|
-
|
|
287
|
+
'--l2cap-mtu',
|
|
288
|
+
help='L2CAP MTU',
|
|
289
|
+
type=click.IntRange(
|
|
290
|
+
l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MTU,
|
|
291
|
+
l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MTU,
|
|
292
|
+
),
|
|
293
|
+
default=1024,
|
|
289
294
|
)
|
|
290
295
|
@click.option(
|
|
291
|
-
'--l2cap-
|
|
292
|
-
help='L2CAP
|
|
293
|
-
type=click.IntRange(
|
|
296
|
+
'--l2cap-mps',
|
|
297
|
+
help='L2CAP MPS',
|
|
298
|
+
type=click.IntRange(
|
|
299
|
+
l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MIN_MPS,
|
|
300
|
+
l2cap.L2CAP_LE_CREDIT_BASED_CONNECTION_MAX_MPS,
|
|
301
|
+
),
|
|
294
302
|
default=1024,
|
|
295
303
|
)
|
|
296
304
|
def cli(
|
|
@@ -298,17 +306,17 @@ def cli(
|
|
|
298
306
|
device_config,
|
|
299
307
|
hci_transport,
|
|
300
308
|
psm,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
l2cap_max_credits,
|
|
310
|
+
l2cap_mtu,
|
|
311
|
+
l2cap_mps,
|
|
304
312
|
):
|
|
305
313
|
context.ensure_object(dict)
|
|
306
314
|
context.obj['device_config'] = device_config
|
|
307
315
|
context.obj['hci_transport'] = hci_transport
|
|
308
316
|
context.obj['psm'] = psm
|
|
309
|
-
context.obj['max_credits'] =
|
|
310
|
-
context.obj['mtu'] =
|
|
311
|
-
context.obj['mps'] =
|
|
317
|
+
context.obj['max_credits'] = l2cap_max_credits
|
|
318
|
+
context.obj['mtu'] = l2cap_mtu
|
|
319
|
+
context.obj['mps'] = l2cap_mps
|
|
312
320
|
|
|
313
321
|
|
|
314
322
|
# -----------------------------------------------------------------------------
|
bumble/apps/pair.py
CHANGED
|
@@ -52,11 +52,13 @@ from bumble.att import (
|
|
|
52
52
|
class Waiter:
|
|
53
53
|
instance = None
|
|
54
54
|
|
|
55
|
-
def __init__(self):
|
|
55
|
+
def __init__(self, linger=False):
|
|
56
56
|
self.done = asyncio.get_running_loop().create_future()
|
|
57
|
+
self.linger = linger
|
|
57
58
|
|
|
58
59
|
def terminate(self):
|
|
59
|
-
self.
|
|
60
|
+
if not self.linger:
|
|
61
|
+
self.done.set_result(None)
|
|
60
62
|
|
|
61
63
|
async def wait_until_terminated(self):
|
|
62
64
|
return await self.done
|
|
@@ -302,7 +304,7 @@ async def pair(
|
|
|
302
304
|
hci_transport,
|
|
303
305
|
address_or_name,
|
|
304
306
|
):
|
|
305
|
-
Waiter.instance = Waiter()
|
|
307
|
+
Waiter.instance = Waiter(linger=linger)
|
|
306
308
|
|
|
307
309
|
print('<<< connecting to HCI...')
|
|
308
310
|
async with await open_transport_or_link(hci_transport) as (hci_source, hci_sink):
|
|
@@ -396,7 +398,6 @@ async def pair(
|
|
|
396
398
|
address_or_name,
|
|
397
399
|
transport=BT_LE_TRANSPORT if mode == 'le' else BT_BR_EDR_TRANSPORT,
|
|
398
400
|
)
|
|
399
|
-
pairing_failure = False
|
|
400
401
|
|
|
401
402
|
if not request:
|
|
402
403
|
try:
|
|
@@ -405,11 +406,8 @@ async def pair(
|
|
|
405
406
|
else:
|
|
406
407
|
await connection.authenticate()
|
|
407
408
|
except ProtocolError as error:
|
|
408
|
-
pairing_failure = True
|
|
409
409
|
print(color(f'Pairing failed: {error}', 'red'))
|
|
410
410
|
|
|
411
|
-
if not linger or pairing_failure:
|
|
412
|
-
return
|
|
413
411
|
else:
|
|
414
412
|
if mode == 'le':
|
|
415
413
|
# Advertise so that peers can find us and connect
|
|
@@ -459,7 +457,7 @@ class LogHandler(logging.Handler):
|
|
|
459
457
|
help='Enable CTKD',
|
|
460
458
|
show_default=True,
|
|
461
459
|
)
|
|
462
|
-
@click.option('--linger', default=
|
|
460
|
+
@click.option('--linger', default=False, is_flag=True, help='Linger after pairing')
|
|
463
461
|
@click.option(
|
|
464
462
|
'--io',
|
|
465
463
|
type=click.Choice(
|