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.
Files changed (42) hide show
  1. bumble/_version.py +2 -2
  2. bumble/apps/bench.py +397 -133
  3. bumble/apps/ble_rpa_tool.py +63 -0
  4. bumble/apps/console.py +4 -4
  5. bumble/apps/controller_info.py +64 -6
  6. bumble/apps/controller_loopback.py +200 -0
  7. bumble/apps/l2cap_bridge.py +32 -24
  8. bumble/apps/pair.py +6 -8
  9. bumble/att.py +53 -11
  10. bumble/controller.py +159 -24
  11. bumble/crypto.py +10 -0
  12. bumble/device.py +580 -113
  13. bumble/drivers/__init__.py +27 -31
  14. bumble/drivers/common.py +45 -0
  15. bumble/drivers/rtk.py +11 -4
  16. bumble/gatt.py +66 -51
  17. bumble/gatt_server.py +30 -22
  18. bumble/hci.py +258 -91
  19. bumble/helpers.py +14 -0
  20. bumble/hfp.py +37 -27
  21. bumble/hid.py +282 -61
  22. bumble/host.py +158 -93
  23. bumble/l2cap.py +11 -6
  24. bumble/link.py +55 -1
  25. bumble/profiles/asha_service.py +2 -2
  26. bumble/profiles/bap.py +1247 -0
  27. bumble/profiles/cap.py +52 -0
  28. bumble/profiles/csip.py +119 -9
  29. bumble/rfcomm.py +31 -20
  30. bumble/smp.py +1 -1
  31. bumble/transport/__init__.py +51 -22
  32. bumble/transport/android_emulator.py +1 -1
  33. bumble/transport/common.py +2 -1
  34. bumble/transport/hci_socket.py +1 -4
  35. bumble/transport/usb.py +1 -1
  36. bumble/utils.py +3 -6
  37. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/METADATA +1 -1
  38. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/RECORD +42 -37
  39. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/entry_points.txt +1 -0
  40. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/LICENSE +0 -0
  41. {bumble-0.0.180.dist-info → bumble-0.0.182.dist-info}/WHEEL +0 -0
  42. {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):
@@ -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 async_main(transport):
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()
@@ -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 CoC connections
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.on_coc,
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 on_coc(self, l2cap_channel):
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.on_coc_sdu
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 on_coc_sdu(self, sdu):
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 on_coc_sdu(sdu):
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 = on_coc_sdu
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 CoC', type=int, default=1234)
279
+ @click.option('--psm', help='PSM for L2CAP', type=int, default=1234)
278
280
  @click.option(
279
- '--l2cap-coc-max-credits',
280
- help='Maximum L2CAP CoC Credits',
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-coc-mtu',
286
- help='L2CAP CoC MTU',
287
- type=click.IntRange(23, 65535),
288
- default=1022,
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-coc-mps',
292
- help='L2CAP CoC MPS',
293
- type=click.IntRange(23, 65533),
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
- l2cap_coc_max_credits,
302
- l2cap_coc_mtu,
303
- l2cap_coc_mps,
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'] = l2cap_coc_max_credits
310
- context.obj['mtu'] = l2cap_coc_mtu
311
- context.obj['mps'] = l2cap_coc_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.done.set_result(None)
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=True, is_flag=True, help='Linger after pairing')
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(