bumble 0.0.181__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.
@@ -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 (
@@ -48,6 +50,7 @@ from bumble.hci import (
48
50
  HCI_LE_Read_Maximum_Advertising_Data_Length_Command,
49
51
  HCI_LE_READ_SUGGESTED_DEFAULT_DATA_LENGTH_COMMAND,
50
52
  HCI_LE_Read_Suggested_Default_Data_Length_Command,
53
+ HCI_Read_Local_Version_Information_Command,
51
54
  )
52
55
  from bumble.host import Host
53
56
  from bumble.transport import open_transport_or_link
@@ -166,7 +169,7 @@ async def get_acl_flow_control_info(host: Host) -> None:
166
169
 
167
170
 
168
171
  # -----------------------------------------------------------------------------
169
- async def async_main(transport):
172
+ async def async_main(latency_probes, transport):
170
173
  print('<<< connecting to HCI...')
171
174
  async with await open_transport_or_link(transport) as (hci_source, hci_sink):
172
175
  print('<<< connected')
@@ -174,6 +177,23 @@ async def async_main(transport):
174
177
  host = Host(hci_source, hci_sink)
175
178
  await host.reset()
176
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
+
177
197
  # Print version
178
198
  print(color('Version:', 'yellow'))
179
199
  print(
@@ -209,10 +229,16 @@ async def async_main(transport):
209
229
 
210
230
  # -----------------------------------------------------------------------------
211
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
+ )
212
238
  @click.argument('transport')
213
- def main(transport):
239
+ def main(latency_probes, transport):
214
240
  logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
215
- asyncio.run(async_main(transport))
241
+ asyncio.run(async_main(latency_probes, transport))
216
242
 
217
243
 
218
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/controller.py CHANGED
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
 
20
20
  import logging
21
21
  import asyncio
22
+ import dataclasses
22
23
  import itertools
23
24
  import random
24
25
  import struct
@@ -42,6 +43,7 @@ from bumble.hci import (
42
43
  HCI_LE_1M_PHY,
43
44
  HCI_SUCCESS,
44
45
  HCI_UNKNOWN_HCI_COMMAND_ERROR,
46
+ HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
45
47
  HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
46
48
  HCI_VERSION_BLUETOOTH_CORE_5_0,
47
49
  Address,
@@ -53,6 +55,7 @@ from bumble.hci import (
53
55
  HCI_Connection_Request_Event,
54
56
  HCI_Disconnection_Complete_Event,
55
57
  HCI_Encryption_Change_Event,
58
+ HCI_Synchronous_Connection_Complete_Event,
56
59
  HCI_LE_Advertising_Report_Event,
57
60
  HCI_LE_Connection_Complete_Event,
58
61
  HCI_LE_Read_Remote_Features_Complete_Event,
@@ -60,10 +63,11 @@ from bumble.hci import (
60
63
  HCI_Packet,
61
64
  HCI_Role_Change_Event,
62
65
  )
63
- from typing import Optional, Union, Dict, TYPE_CHECKING
66
+ from typing import Optional, Union, Dict, Any, TYPE_CHECKING
64
67
 
65
68
  if TYPE_CHECKING:
66
- from bumble.transport.common import TransportSink, TransportSource
69
+ from bumble.link import LocalLink
70
+ from bumble.transport.common import TransportSink
67
71
 
68
72
  # -----------------------------------------------------------------------------
69
73
  # Logging
@@ -79,15 +83,18 @@ class DataObject:
79
83
 
80
84
 
81
85
  # -----------------------------------------------------------------------------
86
+ @dataclasses.dataclass
82
87
  class Connection:
83
- def __init__(self, controller, handle, role, peer_address, link, transport):
84
- self.controller = controller
85
- self.handle = handle
86
- self.role = role
87
- self.peer_address = peer_address
88
- self.link = link
88
+ controller: Controller
89
+ handle: int
90
+ role: int
91
+ peer_address: Address
92
+ link: Any
93
+ transport: int
94
+ link_type: int
95
+
96
+ def __post_init__(self):
89
97
  self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
90
- self.transport = transport
91
98
 
92
99
  def on_hci_acl_data_packet(self, packet):
93
100
  self.assembler.feed_packet(packet)
@@ -106,10 +113,10 @@ class Connection:
106
113
  class Controller:
107
114
  def __init__(
108
115
  self,
109
- name,
116
+ name: str,
110
117
  host_source=None,
111
118
  host_sink: Optional[TransportSink] = None,
112
- link=None,
119
+ link: Optional[LocalLink] = None,
113
120
  public_address: Optional[Union[bytes, str, Address]] = None,
114
121
  ):
115
122
  self.name = name
@@ -359,12 +366,13 @@ class Controller:
359
366
  if connection is None:
360
367
  connection_handle = self.allocate_connection_handle()
361
368
  connection = Connection(
362
- self,
363
- connection_handle,
364
- BT_PERIPHERAL_ROLE,
365
- peer_address,
366
- self.link,
367
- BT_LE_TRANSPORT,
369
+ controller=self,
370
+ handle=connection_handle,
371
+ role=BT_PERIPHERAL_ROLE,
372
+ peer_address=peer_address,
373
+ link=self.link,
374
+ transport=BT_LE_TRANSPORT,
375
+ link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
368
376
  )
369
377
  self.peripheral_connections[peer_address] = connection
370
378
  logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
@@ -418,12 +426,13 @@ class Controller:
418
426
  if connection is None:
419
427
  connection_handle = self.allocate_connection_handle()
420
428
  connection = Connection(
421
- self,
422
- connection_handle,
423
- BT_CENTRAL_ROLE,
424
- peer_address,
425
- self.link,
426
- BT_LE_TRANSPORT,
429
+ controller=self,
430
+ handle=connection_handle,
431
+ role=BT_CENTRAL_ROLE,
432
+ peer_address=peer_address,
433
+ link=self.link,
434
+ transport=BT_LE_TRANSPORT,
435
+ link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
427
436
  )
428
437
  self.central_connections[peer_address] = connection
429
438
  logger.debug(
@@ -568,6 +577,7 @@ class Controller:
568
577
  peer_address=peer_address,
569
578
  link=self.link,
570
579
  transport=BT_BR_EDR_TRANSPORT,
580
+ link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
571
581
  )
572
582
  self.classic_connections[peer_address] = connection
573
583
  logger.debug(
@@ -621,6 +631,42 @@ class Controller:
621
631
  )
622
632
  )
623
633
 
634
+ def on_classic_sco_connection_complete(
635
+ self, peer_address: Address, status: int, link_type: int
636
+ ):
637
+ if status == HCI_SUCCESS:
638
+ # Allocate (or reuse) a connection handle
639
+ connection_handle = self.allocate_connection_handle()
640
+ connection = Connection(
641
+ controller=self,
642
+ handle=connection_handle,
643
+ # Role doesn't matter in SCO.
644
+ role=BT_CENTRAL_ROLE,
645
+ peer_address=peer_address,
646
+ link=self.link,
647
+ transport=BT_BR_EDR_TRANSPORT,
648
+ link_type=link_type,
649
+ )
650
+ self.classic_connections[peer_address] = connection
651
+ logger.debug(f'New SCO connection handle: 0x{connection_handle:04X}')
652
+ else:
653
+ connection_handle = 0
654
+
655
+ self.send_hci_packet(
656
+ HCI_Synchronous_Connection_Complete_Event(
657
+ status=status,
658
+ connection_handle=connection_handle,
659
+ bd_addr=peer_address,
660
+ link_type=link_type,
661
+ # TODO: Provide SCO connection parameters.
662
+ transmission_interval=0,
663
+ retransmission_window=0,
664
+ rx_packet_length=0,
665
+ tx_packet_length=0,
666
+ air_mode=0,
667
+ )
668
+ )
669
+
624
670
  ############################################################
625
671
  # Advertising support
626
672
  ############################################################
@@ -740,6 +786,68 @@ class Controller:
740
786
  )
741
787
  self.link.classic_accept_connection(self, command.bd_addr, command.role)
742
788
 
789
+ def on_hci_enhanced_setup_synchronous_connection_command(self, command):
790
+ '''
791
+ See Bluetooth spec Vol 4, Part E - 7.1.45 Enhanced Setup Synchronous Connection command
792
+ '''
793
+
794
+ if self.link is None:
795
+ return
796
+
797
+ if not (
798
+ connection := self.find_classic_connection_by_handle(
799
+ command.connection_handle
800
+ )
801
+ ):
802
+ self.send_hci_packet(
803
+ HCI_Command_Status_Event(
804
+ status=HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
805
+ num_hci_command_packets=1,
806
+ command_opcode=command.op_code,
807
+ )
808
+ )
809
+ return
810
+
811
+ self.send_hci_packet(
812
+ HCI_Command_Status_Event(
813
+ status=HCI_SUCCESS,
814
+ num_hci_command_packets=1,
815
+ command_opcode=command.op_code,
816
+ )
817
+ )
818
+ self.link.classic_sco_connect(
819
+ self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
820
+ )
821
+
822
+ def on_hci_enhanced_accept_synchronous_connection_request_command(self, command):
823
+ '''
824
+ See Bluetooth spec Vol 4, Part E - 7.1.46 Enhanced Accept Synchronous Connection Request command
825
+ '''
826
+
827
+ if self.link is None:
828
+ return
829
+
830
+ if not (connection := self.find_classic_connection_by_address(command.bd_addr)):
831
+ self.send_hci_packet(
832
+ HCI_Command_Status_Event(
833
+ status=HCI_UNKNOWN_CONNECTION_IDENTIFIER_ERROR,
834
+ num_hci_command_packets=1,
835
+ command_opcode=command.op_code,
836
+ )
837
+ )
838
+ return
839
+
840
+ self.send_hci_packet(
841
+ HCI_Command_Status_Event(
842
+ status=HCI_SUCCESS,
843
+ num_hci_command_packets=1,
844
+ command_opcode=command.op_code,
845
+ )
846
+ )
847
+ self.link.classic_accept_sco_connection(
848
+ self, connection.peer_address, HCI_Connection_Complete_Event.ESCO_LINK_TYPE
849
+ )
850
+
743
851
  def on_hci_switch_role_command(self, command):
744
852
  '''
745
853
  See Bluetooth spec Vol 4, Part E - 7.2.8 Switch Role command
bumble/device.py CHANGED
@@ -61,7 +61,6 @@ from .hci import (
61
61
  HCI_LE_1M_PHY_BIT,
62
62
  HCI_LE_2M_PHY,
63
63
  HCI_LE_2M_PHY_LE_SUPPORTED_FEATURE,
64
- HCI_LE_CLEAR_RESOLVING_LIST_COMMAND,
65
64
  HCI_LE_CODED_PHY,
66
65
  HCI_LE_CODED_PHY_BIT,
67
66
  HCI_LE_CODED_PHY_LE_SUPPORTED_FEATURE,
@@ -86,7 +85,7 @@ from .hci import (
86
85
  HCI_Constant,
87
86
  HCI_Create_Connection_Cancel_Command,
88
87
  HCI_Create_Connection_Command,
89
- HCI_Create_Connection_Command,
88
+ HCI_Connection_Complete_Event,
90
89
  HCI_Disconnect_Command,
91
90
  HCI_Encryption_Change_Event,
92
91
  HCI_Error,
@@ -3319,8 +3318,21 @@ class Device(CompositeEventEmitter):
3319
3318
  def on_connection_request(self, bd_addr, class_of_device, link_type):
3320
3319
  logger.debug(f'*** Connection request: {bd_addr}')
3321
3320
 
3321
+ # Handle SCO request.
3322
+ if link_type in (
3323
+ HCI_Connection_Complete_Event.SCO_LINK_TYPE,
3324
+ HCI_Connection_Complete_Event.ESCO_LINK_TYPE,
3325
+ ):
3326
+ if connection := self.find_connection_by_bd_addr(
3327
+ bd_addr, transport=BT_BR_EDR_TRANSPORT
3328
+ ):
3329
+ self.emit('sco_request', connection, link_type)
3330
+ else:
3331
+ logger.error(f'SCO request from a non-connected device {bd_addr}')
3332
+ return
3333
+
3322
3334
  # match a pending future using `bd_addr`
3323
- if bd_addr in self.classic_pending_accepts:
3335
+ elif bd_addr in self.classic_pending_accepts:
3324
3336
  future, *_ = self.classic_pending_accepts.pop(bd_addr)
3325
3337
  future.set_result((bd_addr, class_of_device, link_type))
3326
3338